fs: inode and open-file tables that grow, with a hash for iget

The inode table was NINODE (200) entries, searched end to end on every
iget(), and a panic -- "iget: no inodes" -- when all of them were held.
Held means open, a cwd, a running binary or a directory mid-walk, so a
few busy processes reached it. iget() now finds an inode through a hash
on (dev, inum), takes a free one from a list, and makes more from a
slab cache once the NINODE in the image are all in use.

The open-file table had the same shape, NFILE (512) and then ENFILE,
and gets the same treatment: a free list, and the slab past NFILE.
/proc/fsstat's peaks now read against how many exist.

getrlimit(RLIMIT_NOFILE) said unlimited, which musl turns into
sysconf(_SC_OPEN_MAX) == -1, after which zsh believed it had no
descriptors and `exec {fd}<file` failed on the first one. It reports
NOFILE now. And a comment in sys_fs.c held a "*/" that ended it early.

Checked under QEMU: six processes holding 115 files each, 713 open
descriptions and 341 inodes at once, all back to the baseline after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S1giNvrHTrPMPw7V6x1kQ4
This commit is contained in:
Nexoniarz 2026-10-23 13:39:14 +02:00
commit 09e07423e8
6 changed files with 161 additions and 40 deletions

View file

@ -9,18 +9,38 @@
#include "fsstat.h"
#include "posix.h"
#include "errno.h"
#include "kmem.h"
extern struct devsw devsw[];
// Open file descriptions, shared by every process. NFILE of them in the
// kernel image and more from a slab cache once those are all open; a closed
// one goes on the free list. This was NFILE and then ENFILE, which a few
// processes each holding a hundred descriptors -- an editor, a build, a
// shell -- were already a third of the way to.
struct {
struct spinlock lock;
struct file file[NFILE];
struct file *free;
struct kmem_cache *more;
uint n;
} ftable;
void
fileinit(void)
{
initlock(&ftable.lock, "ftable");
for(int i = NFILE - 1; i >= 0; i--){
ftable.file[i].fnext = ftable.free;
ftable.free = &ftable.file[i];
}
ftable.n = NFILE;
}
uint
file_slots(void)
{
return ftable.n;
}
struct file*
@ -29,21 +49,28 @@ filealloc(void)
struct file *f;
acquire(&ftable.lock);
for(f = ftable.file; f < ftable.file + NFILE; f++){
if(f->ref == 0){
f->ref = 1;
FSSTAT_TAKE(fsstat.file_used, fsstat.file_peak); // 0 -> 1
// Cleared here rather than left to the caller: every creator sets
// readable/writable/off, but only open() sets nonblock, so a struct file
// recycled from a non-blocking device would hand its flag to the next
// pipe or socket that happened to land in this slot.
f->nonblock = 0;
if((f = ftable.free) != 0){
ftable.free = f->fnext;
} else {
// The slab allocator takes only its own lock and kalloc's.
if(ftable.more == 0)
ftable.more = kmem_cache_create("file", sizeof(struct file));
if(ftable.more == 0 || (f = kmem_cache_alloc(ftable.more)) == 0){
release(&ftable.lock);
return f;
return 0;
}
memset(f, 0, sizeof *f);
ftable.n++;
}
f->ref = 1;
FSSTAT_TAKE(fsstat.file_used, fsstat.file_peak); // 0 -> 1
// Cleared here rather than left to the caller: every creator sets
// readable/writable/off, but only open() sets nonblock, so a struct file
// recycled from a non-blocking device would hand its flag to the next
// pipe or socket that happened to land in this slot.
f->nonblock = 0;
release(&ftable.lock);
return 0;
return f;
}
struct file*
@ -72,6 +99,8 @@ fileclose(struct file *f)
ff = *f;
f->ref = 0;
f->type = FD_NONE;
f->fnext = ftable.free;
ftable.free = f;
FSSTAT_DROP(fsstat.file_used); // 1 -> 0
release(&ftable.lock);

View file

@ -45,6 +45,7 @@
#include "file.h"
#include "fat.h"
#include "errno.h"
#include "kmem.h"
extern uint uptime(void);
@ -215,17 +216,80 @@ bfree(int dev, uint b)
// Inode table
// ===========================================================================
// Every inode somebody holds, found by a hash on (dev, inum). One with no
// reference goes on the free list and out of the hash, so a lookup never has
// to skip it.
//
// This was NINODE entries searched end to end on every iget(), and "iget: no
// inodes" -- a panic -- when all 200 were held. Held means open, a cwd, an
// exec'd binary, or a directory some path walk is in the middle of, so 128
// processes with a few files each reached it. The table now starts as those
// NINODE and takes more from a slab cache when they are all held; it never
// gives them back, which costs 280 bytes for each inode that was ever held at
// once, and makes the panic mean "out of kernel memory".
#define INODE_NHASH 4096 // a power of two
struct {
struct spinlock lock;
struct inode inode[NINODE];
struct inode *hash[INODE_NHASH];
struct inode *free; // through hnext, ref 0
struct inode *all; // through anext
struct kmem_cache *more;
uint n;
} itable;
static inline uint
ihash(uint dev, uint64 inum)
{
uint64 h = (inum ^ ((uint64)dev << 40)) * 0x9E3779B97F4A7C15ULL;
return (uint)(h >> 52) & (INODE_NHASH - 1);
}
static void
inode_new(struct inode *ip)
{
initsleeplock(&ip->lock, "inode");
ip->anext = itable.all;
itable.all = ip;
ip->hnext = itable.free;
itable.free = ip;
itable.n++;
}
void
iinit(void)
{
initlock(&itable.lock, "itable");
for (int i = 0; i < NINODE; i++)
initsleeplock(&itable.inode[i].lock, "inode");
for (int i = NINODE - 1; i >= 0; i--)
inode_new(&itable.inode[i]);
}
// How many inode slots exist, for /proc/fsstat.
uint
inode_slots(void)
{
return itable.n;
}
// The held inode for (dev, inum), or 0. Under itable.lock.
static struct inode *
ifind(uint dev, uint64 inum)
{
for (struct inode *ip = itable.hash[ihash(dev, inum)]; ip; ip = ip->hnext)
if (ip->dev == dev && ip->inum == inum)
return ip;
return 0;
}
static void
iunhash(struct inode *ip)
{
struct inode **pp = &itable.hash[ihash(ip->dev, ip->inum)];
while (*pp && *pp != ip)
pp = &(*pp)->hnext;
if (*pp)
*pp = ip->hnext;
}
struct inode *iget(uint dev, uint64 inum);
@ -450,20 +514,28 @@ iget(uint dev, uint64 inum)
struct inode *ip, *empty;
acquire(&itable.lock);
empty = 0;
for (ip = &itable.inode[0]; ip < &itable.inode[NINODE]; ip++) {
if (ip->ref > 0 && ip->dev == dev && ip->inum == inum) {
ip->ref++;
release(&itable.lock);
return ip;
}
if (empty == 0 && ip->ref == 0)
empty = ip;
if ((ip = ifind(dev, inum)) != 0) {
ip->ref++;
release(&itable.lock);
return ip;
}
if ((empty = itable.free) != 0) {
itable.free = empty->hnext;
} else {
// The slab allocator takes only its own lock and kalloc's.
if (itable.more == 0)
itable.more = kmem_cache_create("inode", sizeof(struct inode));
if (itable.more == 0 || (empty = kmem_cache_alloc(itable.more)) == 0)
panic("iget: no inodes");
memset(empty, 0, sizeof *empty);
inode_new(empty);
itable.free = empty->hnext;
}
if (empty == 0)
panic("iget: no inodes");
ip = empty;
ip->hnext = itable.hash[ihash(dev, inum)];
itable.hash[ihash(dev, inum)] = ip;
ip->dev = dev;
ip->inum = inum;
ip->ref = 1;
@ -481,11 +553,7 @@ iheld(uint dev, uint64 inum)
{
int held = 0;
acquire(&itable.lock);
for (struct inode *ip = &itable.inode[0]; ip < &itable.inode[NINODE]; ip++)
if (ip->ref > 0 && ip->dev == dev && ip->inum == inum) {
held = 1;
break;
}
held = ifind(dev, inum) != 0;
release(&itable.lock);
return held;
}
@ -497,7 +565,7 @@ ibusy(uint dev, uint64 except)
{
int busy = 0;
acquire(&itable.lock);
for (struct inode *ip = &itable.inode[0]; ip < &itable.inode[NINODE]; ip++)
for (struct inode *ip = itable.all; ip; ip = ip->anext)
if (ip->ref > 0 && ip->dev == dev && ip->inum != except) {
busy = 1;
break;
@ -584,8 +652,12 @@ iput(struct inode *ip)
acquire(&itable.lock);
}
if (--ip->ref == 0)
if (--ip->ref == 0) {
FSSTAT_DROP(fsstat.inode_used);
iunhash(ip);
ip->hnext = itable.free;
itable.free = ip;
}
release(&itable.lock);
}