VFS Inode Object

The inode (struct inode, defined in include/linux/fs.h) is the kernel’s in-memory representation of a single filesystem object — a regular file, directory, symlink, device node, FIFO, or socket. It holds the file’s metadata: its type and permission bits (i_mode), owner (i_uid/i_gid), size (i_size), timestamps, hard-link count (i_nlink), and inode number (i_ino). Crucially, there is exactly one in-memory inode per file, no matter how many names point at it — names live in dentries, and many dentries can share one inode (that is what a hard link is). The inode also carries the per-file operation tables (i_op, i_fop) through which the VFS dispatches into the concrete filesystem, and i_mapping, the handle to the file’s cached data pages. This note describes the 6.12 LTS (2024-11-17) layout.

Mental Model

Think of the inode as the identity card of a file, and the dentry as a name tag. A person (the file’s data and metadata) has exactly one identity card; they may be addressed by several name tags (/home/alice/report.txt, /backup/r.txt), but those name tags all reference the same card. The number on the card is i_ino; the count of name tags currently issued is i_nlink. Two completely independent reference counts govern the inode’s existence, and conflating them is the classic inode misconception:

  • i_nlink — the on-disk hard-link count: how many directory entries (names) point at this inode. Persisted to disk. When it reaches zero and nobody has the file open, the on-disk inode and its data blocks are freed.
  • i_count — the in-memory reference count: how many kernel subsystems are currently holding this struct inode pinned in RAM. When it reaches zero, the in-memory object becomes a candidate for eviction from the inode cache — but the file may still exist on disk.
flowchart LR
  subgraph NAMES["Names (dentries)"]
    D1["dentry<br/>/home/alice/report.txt"]
    D2["dentry<br/>/backup/r.txt"]
  end
  D1 -->|d_inode| IN
  D2 -->|d_inode| IN
  IN["struct inode<br/>i_ino=8231  i_nlink=2<br/>i_mode  i_uid  i_size<br/>i_op  i_fop  i_mapping"]
  IN -->|i_sb| SB["struct super_block<br/>(the mounted fs)"]
  IN -->|i_op| IOP["inode_operations<br/>(lookup, create, ...)"]
  IN -->|i_fop| FOP["file_operations<br/>(read, write, ...)"]
  IN -->|i_mapping| AS["address_space<br/>(page cache)"]
  IN -->|i_dentry hlist| NAMES

The inode as the hub. What it shows: two dentries (two hard-linked names) both point at one inode via d_inode; the inode in turn points back at its names (i_dentry), up to its superblock (i_sb), out to its operation tables (i_op/i_fop), and at its page cache (i_mapping). Insight: the inode is the convergence point — names fan in, behavior and data fan out. i_nlink=2 is the count of names; it is unrelated to how many processes have the file open.

What the Inode Holds — Field by Field

The 6.12 struct inode (verified in fs.h, struct inode { at line 632) opens with the fields that path walking and permission checks touch most:

struct inode {
    umode_t            i_mode;     /* file type + permission bits */
    unsigned short     i_opflags;
    kuid_t             i_uid;      /* owner user id  */
    kgid_t             i_gid;      /* owner group id */
    unsigned int       i_flags;
    /* ... posix acl pointers (if CONFIG_FS_POSIX_ACL) ... */
    const struct inode_operations *i_op;   /* metadata ops table   */
    struct super_block *i_sb;              /* the mounted instance */
    struct address_space *i_mapping;       /* page cache handle    */
    /* ... */
    unsigned long      i_ino;     /* inode number (within this sb) */
    union {
        const unsigned int i_nlink;   /* read directly... */
        unsigned int __i_nlink;       /* ...but modify via helpers */
    };
    dev_t              i_rdev;    /* device id, for device nodes */
    loff_t             i_size;    /* file size in bytes */
    time64_t           i_atime_sec;  /* access time (split storage) */
    time64_t           i_mtime_sec;  /* data-modification time */
    time64_t           i_ctime_sec;  /* inode-change time */
    u32                i_atime_nsec, i_mtime_nsec, i_ctime_nsec;
    /* ... */
    u32                i_state;   /* I_NEW / I_DIRTY* / I_FREEING ... */
    struct rw_semaphore i_rwsem;  /* the inode lock */
    /* ... */
    struct hlist_node  i_hash;    /* link in the global inode hash */
    /* ... */
    union {
        struct hlist_head i_dentry;  /* list of dentries (names) */
        struct rcu_head   i_rcu;
    };
    atomic_t           i_count;   /* in-memory reference count */
    /* ... */
    union {
        const struct file_operations *i_fop;  /* open-file ops table */
        void (*free_inode)(struct inode *);
    };
    struct address_space i_data;  /* the embedded page-cache mapping */
    void               *i_private;/* fs/device private pointer */
} __randomize_layout;

The fields grouped by purpose, and what protects each

struct inode looks like a grab-bag until you group it by job. There are really seven jobs: say what the file is, say who owns it, say how big it is, say when it changed, say where its names and its data are, say how to operate on it, and let the kernel manage the object’s own lifetime. The last column is the one that is hardest to recover from reading the struct alone: the kernel’s canonical statement of inode locking is a comment block at the top of fs/inode.c (lines 27–56), which says in full that inode->i_lock protects i_state, i_hash, __iget() and i_io_list; the inode LRU list locks protect i_lru; sb->s_inode_list_lock protects i_sb_list; bdi->wb.list_lock also protects i_io_list; and inode_hash_lock protects the hash table and i_hash. The alias list is documented separately, at the top of fs/dcache.c (lines 40–47): “dcache->d_inode->i_lock protects: i_dentry, d_u.d_alias, d_inode of aliases.”

JobFields (v6.12 fs.h)What it meansSerialized by
Identity / typei_mode, i_ino, i_rdev, i_generation, i_flags, i_opflagstype + permission bits; inode number within this superblock; device id for device nodes; NFS-handle generation counteri_rwsem for changes via setattr; i_ino is effectively immutable once I_NEW clears
Ownershipi_uid, i_gid, i_acl, i_default_aclkuid_t/kgid_t (already namespace-resolved), POSIX ACL cachei_rwsem exclusive (setattr per locking.rst)
Size / allocationi_size, i_blocks, i_bytes, i_blkbits, i_write_hintlogical length; 512-byte blocks actually allocated; sub-block remainder; log2(blocksize)i_lock — the declaration carries the comment /* i_blocks, i_bytes, maybe i_size */; on 32-bit SMP i_size additionally uses i_size_seqcount via i_size_read()/i_size_write()
Timestampsi_atime_sec, i_mtime_sec, i_ctime_sec + matching _nseclast read / last content change / last metadata changeaccessors only (inode_set_ctime_current() etc.); never assign the fields
Namesi_nlink (union with __i_nlink), i_dentry (union with i_rcu)on-disk hard-link count; the list of cached dentries naming this inodei_nlink via the set/clear/inc/drop_nlink() helpers under the directory’s i_rwsem; i_dentry under i_lock
Behaviouri_op, i_fop (union with free_inode), i_sbmetadata vtable, open-file vtable, owning mount instanceassigned once at instantiation; read-mostly, ____cacheline_aligned tables
Datai_mapping, i_data, i_flctx, i_devicespage-cache handle and the embedded address_space; POSIX-lock context; the per-cdev list for device nodesowned by The Page Cache and address_space; i_data.i_pages has its own XArray lock
Lifecyclei_state, i_lru, i_sb_list, i_hash, i_io_list, dirtied_when, dirtied_time_whenthe I_* bit set; membership of the LRU, the superblock’s inode list, the global hash, and the writeback listsi_state/i_hash/i_io_list → i_lock; i_lru → the LRU list lock; i_sb_list → sb->s_inode_list_lock; the hash table itself → inode_hash_lock
Reference countsi_count, i_writecount, i_readcount, i_dio_countin-memory holders; writers (negative = MAP_DENYWRITE-style deny); RO opens; in-flight direct I/Oall atomic_t; i_count drops to zero under i_lock via atomic_dec_and_lock()
Versioningi_version, i_sequenceNFS change attribute; a futex-hashing sequence numberatomic64_t
The overlay unioni_pipe | i_cdev | i_link | i_dir_seqfour mutually exclusive interpretations selected by i_mode — see belowcontext-dependent; i_link requires a release barrier and an RCU grace period
fs-privatei_private, plus the fs’s own struct wrapped around this onedriver/filesystem scratch pointerthe filesystem’s own rules

The __randomize_layout attribute on the closing brace means that with CONFIG_RANDSTRUCT the compiler is free to permute these fields, so no code may depend on the declared order or on offsets; the deliberate hot/cold split described in the header comment is a default, not a guarantee.

Notes on the individual fields

i_mode packs both the file type (regular, directory, symlink, block/char device, FIFO, socket — the S_IFMT bits) and the nine classic permission bits plus setuid/setgid/sticky. The userspace stat(2) value st_mode is this field (inode(7)). i_uid/i_gid are kuid_t/kgid_t — kernel-internal IDs that already account for the user namespace, not raw integers; they are mapped to the namespace-visible IDs at the syscall boundary.

i_ino is the inode number — unique within a single mounted filesystem (a single superblock), not globally. This is why two files on different filesystems can share an inode number and why hard links cannot cross filesystem boundaries: a directory entry stores only an inode number, which is meaningless outside its own superblock (inode(7)). The comment in fs.h is explicit that i_ino is “Stat data, not accessed from path walking” — it sits after the hot path-walk fields deliberately, for cache locality.

Timestamps. In 6.12, the three timestamps are stored as split time64_t seconds + u32 nanoseconds fields (i_atime_sec/i_atime_nsec, etc.) — not as struct timespec64. Filesystem code must not read these fields directly; it goes through accessors such as inode_get_ctime(), inode_set_ctime_current(), inode_set_mtime() (declared in fs.h around line 1586+). The three times mean: atime = last read (often suppressed for performance, e.g. relatime/noatime); mtime = last content change; ctime = last inode-metadata change (permissions, ownership, link count) — and ctime is not “creation time.”

When the timestamp split happened, pinned by existence-checking. Fetching include/linux/fs.h at successive tags and grepping for the field names dates the change exactly. At v6.10 the inode still declares struct timespec64 __i_atime; struct timespec64 __i_mtime; struct timespec64 __i_ctime;; at v6.11 those three lines are gone and time64_t i_atime_sec; time64_t i_mtime_sec; time64_t i_ctime_sec; u32 i_atime_nsec; … stand in their place. The commit is Jeff Layton’s 3aa63a569c64 “fs: switch timespec64 fields in inode to discrete integers” (authored 2024-05-17, merged for 6.11), and its message states the motivation in one line: “Adjacent struct timespec64’s pack poorly. Switch them to discrete integer fields for the seconds and nanoseconds. This shaves 8 bytes off struct inode with a garden-variety Fedora Kconfig on x86_64, but that also moves the i_lock into the previous cacheline, away from the fields it protects.” The same commit moves i_generation above i_lock to undo that cache-line damage — a concrete illustration that struct inode’s field order is tuned for cache behaviour, not readability. The change was suggested by Linus Torvalds and applied by Christian Brauner. An earlier and unrelated rename, i_ctime → __i_ctime plus the inode_get_ctime()/inode_set_ctime_*() accessor family, landed in the 6.6 cycle; the accessors are why the 6.11 field split could be done without touching filesystem code.

Uncertain

Verify: that the multigrain-timestamp work (which repeatedly touched this same area and was reverted at least once in 2023) does not change the meaning of i_ctime_sec/i_ctime_nsec in releases after 6.12. Reason: the 6.12 layout is directly verified, but the series’ later history was not traced here. To resolve: grep include/linux/fs.h at v6.18 for I_CTIME_QUERIED / multigrain helpers and compare the accessor semantics. uncertain

i_size is the logical file length in bytes; i_blocks (further down) counts 512-byte device blocks actually allocated — the two diverge for sparse files (a file with holes has i_size larger than i_blocks × 512).

i_rdev is meaningful only for device-node inodes (block/char special files): it encodes the major/minor device number that selects the driver. For regular files it is unused; the inode’s i_fop is overridden at open time to point at the device driver’s file operations (see VFS Operation Tables).

Three Unions: How One Struct Serves Six File Types

struct inode must represent a regular file, a directory, a symlink, a block device, a character device, a FIFO and a socket — and it must do so in one fixed-size object that the kernel allocates millions of. The design answer is unions keyed by information the inode already carries. There are three of them in v6.12 and they are worth reading as a group, because each uses a different discriminator.

Union (v6.12 fs.h)MembersWhat selects the live memberWhy it is safe
i_nlink / __i_nlink (line 656)const unsigned int i_nlink, unsigned int __i_nlinknot a real alternation — the same storage exposed twicethe const alias is the only name filesystems may read; writing needs set_nlink()/inc_nlink()/drop_nlink()/clear_nlink(), and the header says so in a comment above the union
i_dentry / i_rcu (line 706)struct hlist_head i_dentry, struct rcu_head i_rculifecycle phase — i_dentry while the inode is alive, i_rcu only once it is being freedby the time call_rcu() needs the rcu_head, no dentry can still alias the inode, so the alias list is provably empty
i_fop / free_inode (line 715)const struct file_operations *i_fop, void (*free_inode)(struct inode *)lifecycle phase — i_fop while alive, free_inode during RCU-deferred destructionsame trick: an inode past evict() will never be opened again, so its i_fop slot is dead storage that the destructor callback can reuse
the overlay union (line 724)struct pipe_inode_info *i_pipe, struct cdev *i_cdev, char *i_link, unsigned i_dir_seqi_mode’s type bits — FIFO, character device, symlink, directorythe four types are mutually exclusive by construction; S_ISFIFO/S_ISCHR/S_ISLNK/S_ISDIR are never simultaneously true

The fourth union is the interesting one, because each member is a completely different kind of thing, and each has a visible effect on a system call:

  • i_pipe hangs the pipe buffer ring off a FIFO inode. This is why mkfifo produces a filesystem object that has an inode number and permissions like any other file, yet whose “contents” are a kernel ring buffer rather than disk blocks — the name and the metadata come from the filesystem; the data path comes from i_pipe. alloc_inode() explicitly zeroes it (inode->i_pipe = NULL, fs/inode.c line 185) so a non-FIFO inode can never be mistaken for one with a stale pipe.
  • i_cdev points at the character device this inode names. It is the reason i_rdev and i_cdev are different things: i_rdev is the number stored on disk in the device node, and i_cdev is the in-memory driver object that number was resolved to on first open. evict() calls cd_forget(inode) for S_ISCHR inodes precisely to unhook this pointer before the inode dies.
  • i_link is a fast path for symlinks: if a filesystem parks the target string here, fs/namei.c reads it with res = READ_ONCE(inode->i_link) (line 1847) and skips the ->get_link() call entirely. Only if i_link is NULL does the walker dispatch into the filesystem. vfs.rst states the two obligations this creates: i_link must not be freed until after an RCU grace period, and writing it after iget() requires a release barrier — both because a lockless RCU walk may be dereferencing it concurrently. (ext4 uses this for inline “fast symlinks” whose target fits in the inode itself.)
  • i_dir_seq is not a pointer at all but an odd/even sequence counter on directory inodes, used by the parallel-lookup machinery. start_dir_add() spins until it can cmpxchg the counter from an even value to odd; end_dir_add() publishes n + 2 with smp_store_release() (fs/dcache.c lines 2438–2454). d_alloc_parallel() samples it before and after and retries if it moved. A directory inode has no pipe, no cdev and no symlink body, so the slot is free — and putting the counter in the inode rather than in a side table is what let Al Viro replace the directory i_mutex with the shared-read i_rwsem in v4.7 (verified by existence-check: include/linux/fs.h at v4.6 declares struct mutex i_mutex; and contains no i_rwsem; at v4.7 i_mutex is gone and i_rwsem appears). The design was presented at LSFMM 2016: the problem was that “there is a need to ensure that there are never two hashed dentries with the same parent and name at the same time”, and the solution was to track in-progress lookups on fields that “are unused at that point, so they can be repurposed” (LWN, VFS parallel lookups, 2016).

The payoff of the last one is large and easy to miss: because lookup takes i_rwsem shared (locking.rst), many threads can resolve different names in the same hot directory concurrently, while create, mkdir, unlink and friends still take it exclusive.

This distinction is the conceptual heart of the inode and the single most common source of confusion.

i_nlink is the on-disk hard-link count. Every directory entry that names this inode contributes one to i_nlink. A freshly created regular file has i_nlink == 1 (the one name you gave it). link(2) adds a second name and increments it to 2; unlink(2) removes a name and decrements it. The fs.h definition wraps it in a union — const unsigned int i_nlink for reading, __i_nlink for writing — and the comment mandates that filesystems modify it only through the helpers set_nlink(), inc_nlink(), drop_nlink(), clear_nlink(), never by direct assignment, so the VFS can keep its bookkeeping consistent. Directories have i_nlink ≥ 2 even when empty, because . is a self-link and every subdirectory’s .. links back to the parent.

i_count is the in-memory reference count. It counts the kernel-internal holders of this struct inode: an open file, a cached dentry that has the inode instantiated, a mount pinning a root inode, writeback in progress, and so on. iget-family functions take a reference (raise i_count); iput() drops one. In fs/inode.c, alloc_inode() initializes it with atomic_set(&inode->i_count, 1) (line 169), and iput() (line 1889) does the decrement.

The killer example that proves the distinction. Open a file, then delete it while it is still open:

fd = open("/tmp/scratch", O_RDWR);   # i_nlink == 1, i_count >= 1
unlink("/tmp/scratch");              # i_nlink -> 0, but i_count still >= 1
# ... the program keeps reading and writing fd successfully ...
close(fd);                           # i_count -> 0; NOW the disk inode + blocks are freed

After unlink, the name is gone — ls will not show the file, and the path no longer resolves — but i_nlink == 0 does not free the file, because i_count is still positive. The inode and its data blocks live on, invisibly, until the last descriptor closes. This is exactly the mechanism behind “deleted but still held open” files that you find via /proc/<pid>/fd/, and behind O_TMPFILE anonymous temp files. It is also why deleting a huge logfile that a daemon still holds open does not reclaim disk space until you restart (or truncate) the daemon — a real production gotcha. The decision to actually free the on-disk inode is made in iput() → iput_final() → evict(): the inode is reclaimed from disk only when both i_count hits zero and i_nlink is already zero.

The On-Disk Inode vs the In-Memory Inode

struct inode is the VFS’s generic in-memory view. It is not the on-disk format, and it is not the whole story per filesystem. Two separate distinctions matter:

  1. In-memory generic vs in-memory fs-specific. Each filesystem embeds the VFS struct inode inside a larger, filesystem-private struct — struct ext4_inode_info for ext4, struct xfs_inode for XFS, struct shmem_inode_info for tmpfs. The generic inode is recovered from a pointer to the fs-specific struct (and vice versa) with the container_of pattern, usually wrapped in a helper like EXT4_I(inode). When the VFS hands a filesystem struct inode *inode, the filesystem calls EXT4_I(inode) to reach its own extra fields (extent tree root, reservation state, etc.). The fs allocates the combined struct in its own alloc_inode superblock op; the VFS never sees the private tail.

  2. In-memory vs on-disk. The bytes physically stored on the device are a different, fixed-format structure — struct ext4_inode (the 128- or 256-byte on-disk record), xfs_dinode, and so on. When a file is first accessed, the filesystem reads that on-disk record and marshals its fields into the generic struct inode (and its private tail); when the inode is dirtied and written back, it marshals the other direction. So i_mode, i_size, timestamps, and block pointers exist in both representations, in different layouts, and the filesystem’s write_inode op is the translator. Detailed on-disk layouts belong to the per-filesystem notes (ext4 Filesystem Internals, XFS Filesystem Internals); the takeaway here is that the VFS inode is a cached, normalized projection of an on-disk record.

The Operation Tables and the Mapping

Three pointers connect the inode to behavior and data; their contents are detailed in VFS Operation Tables and The Page Cache and address_space, so this note only states what each is and why it hangs off the inode.

  • i_op (const struct inode_operations *) — metadata operations that act on the file as a named object: lookup (find a child in a directory), create, link, unlink, mkdir, rename, symlink, getattr/setattr, permission, xattr hooks. These are predominantly directory operations and namespace mutations. The vfs.rst documents the full table.
  • i_fop (const struct file_operations *) — operations on an open file: read_iter, write_iter, mmap, open, release, fsync, llseek, ioctl. Note the subtlety the VFS docs call out: open lives in file_operations, not inode_operations, because the per-open struct file is allocated first and then its open method is invoked. When the inode is a device node, i_fop is swapped at open time for the driver’s file ops — that is how opening /dev/sda ends up in the block driver.
  • i_mapping (struct address_space *) — the handle to the file’s cached pages, i.e. the page cache. The inode embeds an address_space i_data directly, and for an ordinary file i_mapping simply points at &inode->i_data. The indirection exists because some inodes (notably block-device inodes) want i_mapping to point at a different inode’s i_data so all opens of the same device share one page cache. The mechanics of readahead, dirtying, and writeback through this mapping are owned by The Page Cache and address_space.

inode_operations in full — contract, locking, and what is optional

VFS Operation Tables explains why the VFS dispatches through function-pointer tables and walks the call sites; what follows is the inode’s own table enumerated from the header, with the one piece of information neither the header nor vfs.rst puts in one place: which mode of i_rwsem the VFS holds when it calls each method. That column is transcribed from the locking matrix in Documentation/filesystems/locking.rst, which prefixes it with “locking rules: all may block” — so no i_op method may be called from atomic context, with the single documented exception noted for permission. The method list is read from struct inode_operations in fs.h line 2127; the contracts are from vfs.rst.

MethodCalled fori_rwsem held by the VFSOptional?
lookupresolve a child name in this directory; must call d_add(), and on a miss must add a NULL inode (a negative dentry) rather than return an errorsharedrequired for any directory
createopen(O_CREAT), creat(2). The dentry handed in is guaranteed negativeexclusiveonly to support regular files
linklink(2)exclusive (both the directory and the victim)only to support hard links
unlinkunlink(2)exclusive on both, plus exclusive on the victim inodeonly to support deletion
symlinksymlink(2)exclusiveonly to support symlinks
mkdirmkdir(2)exclusiveonly to support subdirectories
rmdirrmdir(2)exclusive on both, plus exclusive on the victimonly to support removing subdirectories
mknodmknod(2) — device nodes, FIFOs, socketsexclusiveonly to support those types
renamerename(2)/renameat2(2); must return -EINVAL for unknown flags; handles RENAME_NOREPLACE and RENAME_EXCHANGEexclusive on both parents and some children; cross-directory also takes the per-superblock s_vfs_rename_semonly to support rename
get_linkfollow a symlink; returns the body to traverse. May be called in RCU mode — signalled by a NULL dentry argument — and must then return ERR_PTR(-ECHILD) if it cannot proceednoneonly for symlinks; still required even if i_link is set
readlinknow only an override for readlink(2) when get_link uses nd_jump_link() or the object is not really a symlinknonerarely needed
permissionaccess checks. May be called in rcu-walk mode (mask & MAY_NOT_BLOCK); must then neither block nor store to the inode, and returns -ECHILD to be retried in ref-walknoneoptional — generic check used if NULL
getattrstat(2)/statx(2)noneoptional
setattrchmod, chown, truncate, utimesexclusiveoptional
update_timeupdate a timestamp or i_version; if NULL, “the VFS will update the inode itself and call mark_inode_dirty_sync”noneoptional
get_inode_acl / get_acl / set_aclPOSIX ACL accessnone / none / (via setattr paths)optional
listxattrlistxattr(2)noneoptional
fiemapioctl(FS_IOC_FIEMAP) extent mappingnoneoptional
fileattr_get / fileattr_setFS_IOC_GETFLAGS/FS_IOC_SETFLAGS and the FSXATTR pair; if unset, fall back to f_op->ioctl()“no or exclusive” / exclusiveoptional
atomic_openthe last component of an open: look up, possibly create, and open in one step. Only called when that component is negative or needs lookup — cached positive dentries still go through f_op->open(). May decline via finish_no_open()shared, exclusive if O_CREAToptional; a latency optimisation for network filesystems
tmpfilethe end of an O_TMPFILE open — “equivalent to atomically creating, opening and unlinking a file in given directory”noneoptional
get_offset_ctxsupply the offset context for a directory using simple_offset_dir_operationsnonerequired only for that helper

Three things fall out of reading the table rather than the prose. First, almost everything is optional: a filesystem that only serves read-only regular files needs essentially lookup, and may leave the rest NULL. Second, the directory methods dominate — inode_operations is mostly a namespace-mutation interface, which is why a regular file’s i_op is usually a tiny table of getattr/setattr/permission while a directory’s is large. Third, the locking column shows the real concurrency model of the VFS namespace: one shared-mode method (lookup) and a wall of exclusive-mode mutators, which is exactly why directory-heavy workloads scale on reads and serialise on creates.

Uncertain

Verify: the permission prototype. Reason: locking.rst at v6.12 lists int (*permission)(struct mnt_idmap *, struct inode *, int, unsigned int) (four parameters), while fs.h at the same tag declares int (*permission)(struct mnt_idmap *, struct inode *, int) (three) — the documentation lags the header, and vfs.rst agrees with fs.h. The semantics (rcu-walk via MAY_NOT_BLOCK inside the mask, -ECHILD to retry) are consistent across all three. To resolve: treat fs.h at the pinned tag as authoritative for signatures and file a doc fix. uncertain

Inode Lifecycle: Allocation, the Cache, and Reclaim

Inodes are expensive to fetch (a disk read) and are reused constantly during path walks, so the kernel keeps a global inode cache keyed by (superblock, inode number).

The hash table. Cached inodes hang off a global hash table, inode_hashtable, sized at boot by inode_init_early()/inode_init() in fs/inode.c via alloc_large_system_hash("Inode-cache", ...). Each inode links in through its i_hash (struct hlist_node) member. The struct inode itself is allocated from a dedicated slab cache, inode_cachep (kmem_cache_create("inode_cache", ...), SLAB_RECLAIM_ACCOUNT so it counts as reclaimable memory).

Getting an inode: iget_locked. A filesystem that needs the inode for a given number calls iget_locked(sb, ino) (fs/inode.c, line 1401). The logic, read directly from the source:

struct inode *iget_locked(struct super_block *sb, unsigned long ino)
{
    struct hlist_head *head = inode_hashtable + hash(sb, ino);
    struct inode *inode;
again:
    inode = find_inode_fast(sb, head, ino, false);  /* probe the hash */
    if (inode) {
        wait_on_inode(inode);                        /* if I_NEW, wait */
        return inode;                                /* cache hit */
    }
    inode = alloc_inode(sb);                         /* cache miss: allocate */
    if (inode) {
        spin_lock(&inode_hash_lock);
        old = find_inode_fast(sb, head, ino, true);  /* re-check under lock */
        if (!old) {
            inode->i_ino = ino;
            inode->i_state = I_NEW;                   /* mark "not yet filled" */
            hlist_add_head_rcu(&inode->i_hash, head); /* publish to hash */
            /* ... */
            return inode;                             /* caller must fill + unlock */
        }
        /* somebody else created it; drop ours, use theirs */
        destroy_inode(inode);
        inode = old;
    }
    return inode;
}

On a cache hit the existing inode (already populated) is returned with i_count bumped. On a cache miss a fresh inode is allocated, marked I_NEW, and published into the hash empty; the filesystem then reads the on-disk record, fills the fields, and calls unlock_new_inode() to clear I_NEW and wake any waiter. I_NEW doubles as a mutex and a completion: a second thread racing to fetch the same inode finds it in the hash, sees I_NEW, and blocks in wait_on_inode() until the first thread finishes — guaranteeing exactly one in-memory inode per (sb, ino). Filesystems creating a brand-new inode (not reading an existing one) instead call new_inode(sb), which allocates and adds to the superblock list but does not hash by an on-disk number.

Releasing an inode: iput. iput(inode) (line 1889) drops one reference:

void iput(struct inode *inode)
{
    if (!inode) return;
    if (atomic_dec_and_lock(&inode->i_count, &inode->i_lock)) {
        /* refcount just hit zero, i_lock now held */
        iput_final(inode);
    }
}

atomic_dec_and_lock decrements i_count and, only if it reaches zero, takes i_lock and proceeds to iput_final(). Reading iput_final() directly (fs/inode.c line 1837) rather than paraphrasing it matters here, because the decision is not “is i_nlink zero?” but a three-way branch on a value the superblock gets to override:

static void iput_final(struct inode *inode)
{
	WARN_ON(inode->i_state & I_NEW);
 
	if (op->drop_inode)                     /* fs may override the policy */
		drop = op->drop_inode(inode);
	else
		drop = generic_drop_inode(inode);   /* = !i_nlink || inode_unhashed() */
 
	if (!drop && !(inode->i_state & I_DONTCACHE) && (sb->s_flags & SB_ACTIVE)) {
		__inode_add_lru(inode, true);        /* (A) keep it cached */
		spin_unlock(&inode->i_lock);
		return;
	}
 
	state = inode->i_state;
	if (!drop) {                             /* (B) keep-worthy but must go */
		WRITE_ONCE(inode->i_state, state | I_WILL_FREE);
		spin_unlock(&inode->i_lock);
		write_inode_now(inode, 1);           /* flush it first */
		spin_lock(&inode->i_lock);
		state = inode->i_state & ~I_WILL_FREE;
	}
 
	WRITE_ONCE(inode->i_state, state | I_FREEING);   /* (C) tear down */
	if (!list_empty(&inode->i_lru))
		inode_lru_list_del(inode);
	spin_unlock(&inode->i_lock);
	evict(inode);
}

generic_drop_inode() is one line in fs.h (line 3097): return !inode->i_nlink || inode_unhashed(inode);. So the three outcomes are:

  • (A) Cached. drop == 0 (the file still has names and is hashed), the inode is not marked I_DONTCACHE, and the superblock is still active → the inode goes on the per-superblock LRU and stays in RAM. This is the common case, and it is what makes a second open() of the same file free of disk I/O.
  • (B) Flushed, then freed. drop == 0 but caching was refused — because I_DONTCACHE is set, or because SB_ACTIVE is clear (the filesystem is being unmounted). Only this branch sets I_WILL_FREE, and it does so purely to mark the window in which write_inode_now() runs with i_count already at zero. The bit is cleared again immediately afterwards.
  • (C) Freed directly. drop == 1 — i_nlink == 0 (the unlinked-while-open case) or the inode is already unhashed. The inode goes straight to I_FREEING without ever setting I_WILL_FREE, and evict() frees the on-disk inode and its data blocks.

A correction worth stating plainly

It is natural to assume I_WILL_FREE is the “about to be deleted” flag on the unlink path. It is the opposite: on the unlink path drop is 1, so branch (B) is skipped entirely and I_WILL_FREE is never set. The header comment agrees — I_WILL_FREE “must be set when calling write_inode_now() if i_count is zero”, which is a statement about writeback safety, not about deletion. An earlier revision of this note had this backwards.

evict() then does the real teardown (fs/inode.c line 700): unhook from the writeback and superblock lists, inode_wait_for_writeback() (because the flusher thread may still be mid-write — I_FREEING guarantees it will not start new work, but running work must drain), call the filesystem’s evict_inode (or, if it has none, truncate_inode_pages_final() followed by clear_inode()), cd_forget() for character devices, remove_inode_hash(), wake anyone blocked on __I_NEW, and finally destroy_inode(). clear_inode() is where I_CLEAR appears, and it does not replace I_FREEING — the assignment is literally inode->i_state = I_FREEING | I_CLEAR;, and evict() asserts that exact value with BUG_ON(inode->i_state != (I_FREEING | I_CLEAR)) before destroying the object.

Reclaim under memory pressure. Clean, unreferenced inodes on the LRU are reclaimed by the inode shrinker when memory is tight (inode_cachep is created with SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|SLAB_ACCOUNT, fs/inode.c lines 2437–2442, so its pages count as reclaimable and its allocations are cgroup-charged). Because a positive dentry holds a reference on its inode, an inode cannot be reclaimed until every dentry naming it has gone first — dentry and inode reclaim are therefore strictly ordered, not merely “coupled”. The shrinker side is owned by The Dentry Cache and Shrinkers and Slab Reclaim; what belongs here is the consequence for the object: most inodes die by shrinker, not by unlink.

i_state — Every Bit, and What Sets and Clears It

i_state is a u32 bitfield, protected by inode->i_lock, that is simultaneously a dirty-tracking mask, a lifecycle marker, and two synchronisation primitives. The header’s own summary of the split (fs.h lines 2360–2443) is: “Four bits determine the dirty state of the inode… Four bits define the lifetime of an inode… Two bits are used for locking and completion notification, I_NEW and I_SYNC.” The table below is enumerated from the #define block at lines 2444–2469 — every bit that exists at v6.12, not a remembered subset.

BitValueRoleSet byCleared by
I_NEW1 << 0lock + completion. A newly hashed inode that has not been filled in yet. Doubles as a mutex: a second thread that finds it in the hash blocks in wait_on_inode()iget_locked()/iget5_locked() on a cache miss, and inode_insert5()unlock_new_inode(), which also wakes waiters on the __I_NEW bit address
I_SYNC1 << 1lock + completion. Writeback of this inode is running; also pins the inode in memory for the flusherthe writeback machinery entering writeback_single_inode()the same code on completion, with a wake-up on the bit address
I_LRU_ISOLATING1 << 2inode is pinned while being isolated from the LRU without holding i_countthe inode shrinker’s isolate callbackwhen isolation finishes; evict() calls inode_wait_for_lru_isolating() before proceeding
I_DIRTY_SYNC1 << 3inode metadata is dirty but need not be written by fdatasync() — “timestamp updates are the usual cause”__mark_inode_dirty(I_DIRTY_SYNC)writeback
I_DIRTY_DATASYNC1 << 4data-relevant metadata changed (e.g. i_size), so fdatasync() must write the inode__mark_inode_dirty(I_DIRTY_DATASYNC)writeback
I_DIRTY_PAGES1 << 5the inode has dirty pages; “inode itself may be clean”page dirtying through the address_spacewriteback of those pages
I_WILL_FREE1 << 6narrow window in iput_final() branch (B): write_inode_now() is being called with i_count == 0iput_final() only when drop == 0 and caching was refusediput_final() itself, immediately before setting I_FREEING
I_FREEING1 << 7the inode is being freed but may still have dirty pages/buffers attachediput_final(), and the reclaim/unmount pathsnever — it persists through I_CLEAR until destroy_inode()
I_CLEAR1 << 8added by clear_inode(): the inode is now clean and can be destroyedclear_inode(), as i_state = I_FREEING | I_CLEARnever; the object is freed next
I_REFERENCED1 << 9“recently referenced on the LRU list” — the second-chance bit for inode reclaimiput_final() / lookup hits on an LRU inodethe shrinker’s first pass, which gives the inode one more round instead of evicting it
I_LINKABLE1 << 10an O_TMPFILE inode is allowed to be given a name by linkat() despite having i_nlink == 0vfs_tmpfile() when O_EXCL was not requestedafter the link completes
I_DIRTY_TIME1 << 11timestamps are dirty and lazytime is in effect, so the update may be deferredtimestamp updates under lazytimewhen I_DIRTY_INODE gets set, or on the deferred flush
I_WB_SWITCH1 << 12cgroup writeback: this inode is being moved between bdi_writeback contextsinode_switch_wbs()inode_switch_wbs_work_fn() when the switch completes
I_OVL_INUSE1 << 13overlayfs claims exclusive ownership of an upper/work directory inode among overlay mountsoverlayfs at mountoverlayfs at unmount
I_CREATING1 << 14a new object’s inode is mid-setupinode_insert5() / d_tmpfile() pathsunlock_new_inode() / d_instantiate_new()
I_DONTCACHE1 << 15“evict inode as soon as it is not used anymore” — opts this inode out of the LRU entirelyd_mark_dontcache(), or a filesystem that wants no cachingnot cleared; it forces branch (B) in iput_final()
I_SYNC_QUEUED1 << 16the inode sits on a b_io/b_more_io writeback list, so mark_inode_dirty() must not move it between dirty listsqueuing for writebackdequeuing
I_PINNING_NETFS_WB1 << 17the inode pins a netfs/fscache object for writebackthe netfs writeback pathwhen the pin is released

Two composites exist for convenience: I_DIRTY_INODE = I_DIRTY_SYNC | I_DIRTY_DATASYNC, I_DIRTY = I_DIRTY_INODE | I_DIRTY_PAGES, and I_DIRTY_ALL = I_DIRTY | I_DIRTY_TIME. The split between I_DIRTY_SYNC and I_DIRTY_DATASYNC exists for exactly one reason, stated in the header: so that “we don’t have to write inode on fdatasync() when only e.g. the timestamps have changed” — that is the entire difference between fsync(2) and fdatasync(2) expressed as one bit.

Uncertain

Verify: the “Set by”/“Cleared by” column for I_LINKABLE, I_CREATING, I_WB_SWITCH and I_PINNING_NETFS_WB. Reason: fs.h documents each bit’s meaning but names the setter only for some; the setters above were traced by grepping the v6.12 tree for each symbol, not read from a single authoritative statement, and the netfs and cgroup-writeback paths were not read end to end. The I_NEW, I_SYNC, I_WILL_FREE, I_FREEING and I_CLEAR rows are read directly from fs/inode.c. To resolve: read fs/namei.c:vfs_tmpfile(), fs/fs-writeback.c:inode_switch_wbs*(), and fs/netfs/. uncertain

Note also a small documentation rot worth knowing about if you grep for it: the comment block describes a bit named I_PINNING_FSCACHE_WB, but the #define three dozen lines below it reads I_PINNING_NETFS_WB. The comment was not updated when fscache writeback moved under netfs. Grep the #defines, not the comments.

The lifecycle as a state machine

stateDiagram-v2
  [*] --> Allocated: alloc_inode() sets i_count = 1
  Allocated --> INEW: iget_locked() miss<br/>i_state = I_NEW, hashed
  INEW --> Live: unlock_new_inode()<br/>clears I_NEW, wakes waiters
  state "I_NEW (filling in)" as INEW
  state "Live, i_count > 0" as Live
  state "Dirty" as Dirty
  state "I_SYNC (writeback running)" as Sync
  state "On LRU, i_count == 0" as Lru
  state "I_WILL_FREE" as WillFree
  state "I_FREEING" as Freeing
  state "I_FREEING | I_CLEAR" as Clear

  Live --> Dirty: __mark_inode_dirty()<br/>sets I_DIRTY_SYNC / DATASYNC / PAGES / TIME
  Dirty --> Sync: flusher picks it up<br/>sets I_SYNC
  Sync --> Live: writeback done<br/>clears I_SYNC and the I_DIRTY_* bits
  Live --> Lru: iput_final branch A<br/>drop == 0, SB_ACTIVE, no I_DONTCACHE
  Lru --> Live: iget hit, i_count bumped<br/>sets I_REFERENCED
  Lru --> Freeing: shrinker evicts a clean inode
  Live --> WillFree: iput_final branch B<br/>drop == 0 but I_DONTCACHE or unmount
  WillFree --> Freeing: after write_inode_now(), I_WILL_FREE cleared
  Live --> Freeing: iput_final branch C<br/>drop == 1, i_nlink == 0 or unhashed
  Freeing --> Clear: clear_inode() sets I_CLEAR<br/>keeping I_FREEING
  Clear --> [*]: destroy_inode(), RCU-deferred free

The struct inode lifecycle in terms of i_state. What it shows: the three distinct exits from iput_final() (A: stay on the LRU; B: flush then free; C: free immediately), the dirty/writeback cycle that a live inode can traverse any number of times, and the fact that I_CLEAR is added to I_FREEING rather than replacing it. Insight: the only path that reaches evict() because the file was deleted is branch C, and it bypasses I_WILL_FREE entirely — the bit that looks like “about to be deleted” is really “being written back with no references”. Note also that Lru → Live is the reason a re-opened file costs no I/O, and that the shrinker, not unlink, is what usually kills an inode.

Watching It On a Running System

Everything in this note is observable from an unprivileged shell. The transcripts below were taken on Linux 7.1.13-200.fc44.x86_64 (Fedora, x86-64) on 2026-09-12 — a newer kernel than the v6.12 source this note is pinned to, so treat them as corroboration of the object’s shape, not as evidence about v6.12 internals.

One inode, several names. stat reports %i (the inode number) and %h (the link count), which are i_ino and i_nlink verbatim:

$ echo "hello inode" > a.txt
$ stat -c 'name=%n ino=%i nlink=%h size=%s blocks=%b' a.txt
name=a.txt ino=1400095 nlink=1 size=12 blocks=8
 
$ ln a.txt b.txt                 # link(2): a second name for the same inode
$ mkdir sub && ln a.txt sub/c.txt   # a third, in a different directory
$ stat -c 'name=%n ino=%i nlink=%h' a.txt b.txt sub/c.txt
name=a.txt     ino=1400095 nlink=3
name=b.txt     ino=1400095 nlink=3
name=sub/c.txt ino=1400095 nlink=3
 
$ ls -li a.txt b.txt sub/c.txt
1400095 -rw-r--r--. 3 linman linman 12 Sep 12 16:51 a.txt
1400095 -rw-r--r--. 3 linman linman 12 Sep 12 16:51 b.txt
1400095 -rw-r--r--. 3 linman linman 12 Sep 12 16:51 sub/c.txt

Three names, one inode number, and the link count is 3 as seen through every one of them — because there is only one i_nlink, in one struct inode, and all three dentries point at it. Writing through one name is immediately visible through another, which is the observable consequence of there being a single i_mapping:

$ echo "appended via b" >> b.txt
$ cat a.txt
hello inode
appended via b
 
$ rm b.txt                       # unlink(2): drop_nlink(), 3 -> 2
$ stat -c 'name=%n ino=%i nlink=%h' a.txt sub/c.txt
name=a.txt     ino=1400095 nlink=2
name=sub/c.txt ino=1400095 nlink=2

A symbolic link is the control case: it gets its own inode, with its own link count.

$ ln -s a.txt s.lnk
$ stat -c 'name=%n ino=%i nlink=%h type=%F' a.txt s.lnk
name=a.txt ino=1400095 nlink=2 type=regular file
name=s.lnk ino=1400097 nlink=1 type=symbolic link

Directories start at 2. A fresh empty directory has i_nlink == 2 — one for its name in the parent, one for its own .. Each subdirectory’s .. adds another:

$ mkdir -p d/x d/y
$ stat -c 'name=%n nlink=%h type=%F' d d/x
name=d   nlink=4 type=directory     # "d" + "d/." + "d/x/.." + "d/y/.."
name=d/x nlink=2 type=directory     # "d/x" + "d/x/."

unlink is not delete. This is the sharpest demonstration in the note, and it is the one that turns i_nlink vs i_count from a fact into an intuition. The script writes 256 MiB, fsyncs it, then unlinks it while keeping the descriptor open, sampling statvfs() — the same numbers df reports — at each step:

fs used bytes baseline        : 23015628800
fs used after writing 256 MiB : 23284064256  (delta +268435456)
fs used after unlink, fd open : 23284064256  (delta +268435456)   <-- nothing freed
du -sh . (no name -> not seen): 32
fstat st_blocks*512           : 268435456
fs used after close(fd)       : 23015628800  (delta +0)            <-- freed now

And the inode seen through the still-open descriptor:

fstat before unlink: st_ino=1400116 st_nlink=1 st_size=67108864 st_blocks=131072
fstat AFTER  unlink: st_ino=1400116 st_nlink=0 st_size=67108864 st_blocks=131072
path exists?        False
readlink /proc/self/fd/N: .../big.bin (deleted)
pread back after unlink : b'still writable'

st_nlink is 0 and the path is gone, yet the inode number is unchanged, the blocks are still allocated, and reads and writes still succeed. du reports 32 bytes for the directory because du walks names and there is no longer a name to walk; statvfs reports the full 256 MiB because the blocks are still allocated. The space comes back at close() — not at unlink() — because that is the moment i_count reaches zero and iput_final() takes branch (C). The (deleted) suffix on the /proc/<pid>/fd/ symlink is d_path() telling you the dentry is unhashed. This reproduced identically on tmpfs and on btrfs; it is a VFS-level property, not a filesystem one.

sequenceDiagram
  autonumber
  participant App as Process
  participant VFS as VFS
  participant IN as struct inode
  participant FS as Filesystem
  App->>VFS: open("big.bin", O_RDWR)
  VFS->>IN: iget + dget, i_count++
  Note over IN: i_nlink = 1, i_count = 1
  App->>VFS: unlink("big.bin")
  VFS->>FS: i_op->unlink()
  FS->>IN: drop_nlink()
  Note over IN: i_nlink = 0, i_count = 1
  VFS-->>App: 0 (success)
  App->>VFS: pwrite(fd, ...)
  VFS->>IN: still fully usable
  IN-->>App: bytes written
  Note over IN: statvfs still counts the blocks
  App->>VFS: close(fd)
  VFS->>IN: iput(), i_count -> 0
  IN->>IN: generic_drop_inode -> !i_nlink = true, drop = 1
  IN->>IN: I_FREEING, then evict()
  IN->>FS: sb->s_op->evict_inode()
  FS-->>IN: on-disk inode and data blocks released
  Note over IN: only now does df show the space back

The unlink-while-open sequence. What it shows: unlink(2) only decrements i_nlink; the object stays fully alive because i_count is still positive, and the on-disk release happens inside close(2), several steps later. Insight: the two counters answer two different questions — “does this file still have a name?” (i_nlink) and “is anyone still using this object?” (i_count) — and disk space is freed only when both answers are no. This is the mechanism behind the classic “I deleted the log file and df didn’t change” incident: the daemon still holds the descriptor, so step 8 never runs.

Inode numbers are per-superblock. The same box, two filesystems, and the pair (st_dev, st_ino) is the only unique key:

$ stat -c 'file=%n dev=%D ino=%i' /home/linman/.cache/w1demo/a.txt /tmp/.../demo/a.txt
file=/home/linman/.cache/w1demo/a.txt dev=36 ino=11275869    # btrfs
file=/tmp/.../demo/a.txt              dev=35 ino=1400095     # tmpfs

Birth time is not ctime. stat on a filesystem that records it shows four distinct times; a metadata-only change moves Change and nothing else:

$ stat a.txt
Access: 2026-09-12 16:51:40.150120722 +0200
Modify: 2026-09-12 16:51:40.149120709 +0200
Change: 2026-09-12 16:51:57.643348335 +0200
 Birth: 2026-09-12 16:51:40.142120618 +0200

Birth came from statx(2)’s STX_BTIME; it is not a struct inode field at all — the VFS has no birth-time member, and filesystems that support it fill kstat.btime from their own on-disk record inside ->getattr(). Change (that is, ctime) is 17 seconds later than Modify because a chmod ran in between: metadata changed, contents did not.

Cache-wide counters, and the walls. /proc/sys/fs/inode-nr gives nr_inodes and nr_free_inodes (the second is the number on the “unused” list, not free disk inodes); inode-state is the same figures plus padding:

$ cat /proc/sys/fs/inode-nr
1798299	245707

Two things that would have made better measurements were blocked on this host, and the substitutes are worth recording:

  • /proc/slabinfo is unreadable. ls -l /proc/slabinfo shows -r-------- 1 root root (mode 0400), and head -2 /proc/slabinfo returns Permission denied as uid 1000. So inode_cache’s object size and object count could not be read directly. The working substitute is the cgroup v2 memory.stat slab_reclaimable counter for the caller’s own cgroup — because inode_cachep and dentry_cache are both created with SLAB_ACCOUNT, allocations are charged to the allocating cgroup, and a delta across a workload measures the cache growth that workload caused. This is used in VFS Dentry Object to derive bytes-per-dentry.
  • /proc/sys/vm/drop_caches cannot be written. It is mode --w------- root root, so a genuinely cold-cache measurement is impossible unprivileged; every timing below is warm-cache, and the numbers are lower bounds on the benefit of caching, never upper bounds.

Failure Modes and Common Misunderstandings

  • “ctime is creation time.” No. ctime is inode-change time (the last time metadata — mode, owner, link count, size — changed). Linux historically had no creation/birth time in the inode at all; statx(2) later exposed stx_btime (birth time) where the filesystem records it (ext4, XFS, Btrfs do; not all do). Reading ctime as birth time is a frequent bug.
  • “Disk is full but df shows space free.” A process holding a large unlinked file open keeps its blocks allocated (i_nlink == 0, i_count > 0). du won’t find the file (no name), but the blocks are still gone. lsof +L1 lists files with link count below 1; truncating via /proc/<pid>/fd/N or restarting the holder reclaims the space.
  • “Inode number uniquely identifies a file.” Only within one filesystem. (st_dev, st_ino) is the unique key; st_ino alone is not. Two different mounts routinely reuse the same numbers.
  • “Out of disk space” vs “out of inodes.” A filesystem with a fixed inode table (ext4) can exhaust its inode count while data blocks remain free — df -i shows this; creating files then fails with ENOSPC despite df showing free space. Modern dynamic-inode filesystems (XFS, Btrfs) largely avoid this.
  • Modifying i_nlink directly. A filesystem that assigns inode->__i_nlink instead of calling inc_nlink()/drop_nlink() corrupts the VFS’s view and trips warnings; the union with const unsigned int i_nlink exists precisely to make direct writes a compile error on the read alias.

See Also