iinit is simple:

void iinit() {
  int i = 0;
  initlock(&itable.lock, "itable");
  for(i = 0; i < NINODE; i++) {
    initsleeplock(&itable.inode[i].lock, "inode");
  }
}

It initializes the in-memory inode cache. Same pattern as binit but even simpler.

The data structure

struct {
  struct spinlock lock;
  struct inode inode[NINODE];
} itable;

NINODE is 50. So the kernel can hold at most 50 inodes in memory at once. This is a cache, just like the buffer cache — disk inodes live on disk permanently, but when the kernel needs to work with one, it loads a copy into this table.

What iinit does

First it initializes the spinlock that protects the table itself. This lock guards the allocation of slots — finding a free entry, incrementing reference counts, checking device and inode numbers. Short operations, so a spinlock is appropriate.

Then it loops through all 50 slots and initializes a sleeplock on each one. These per-inode sleeplocks protect the inode’s contents — its type, size, block addresses, and so on. Reading an inode from disk is slow (goes through bread which might hit the disk), so you want a sleeplock that lets other processes run while you wait, not a spinlock that burns CPU.

What iinit doesn’t do

It doesn’t read anything from disk. It doesn’t load the superblock. It doesn’t know what files exist. It just prepares empty slots with initialized locks. The actual filesystem initialization happens later in fsinit, which is called from forkret — not from main() directly — because fsinit calls bread which calls sleep, and the sleep/wakeup mechanism requires a running process context. During main(), there’s no process yet.

The two-lock pattern

Same idea as the buffer cache. The spinlock (itable.lock) protects the table structure — who owns which slot, reference counts. Fast, never held across disk I/O. The sleeplocks (per-inode ip->lock) protect the data inside each inode. Held across disk reads and writes, so processes can sleep while waiting.

This separation shows up in the inode API: iget acquires the spinlock to find or allocate a slot and bump the reference count, but doesn’t lock the inode or read from disk. ilock acquires the sleeplock and reads the inode from disk if needed. They’re intentionally separate so you can hold a long-term reference to an inode (keeping ref > 0 so it stays in the table) without blocking other processes from using it.