VFS Inode Object
The inode (
struct inode, defined ininclude/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, andi_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 thisstruct inodepinned 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.”
| Job | Fields (v6.12 fs.h) | What it means | Serialized by |
|---|---|---|---|
| Identity / type | i_mode, i_ino, i_rdev, i_generation, i_flags, i_opflags | type + permission bits; inode number within this superblock; device id for device nodes; NFS-handle generation counter | i_rwsem for changes via setattr; i_ino is effectively immutable once I_NEW clears |
| Ownership | i_uid, i_gid, i_acl, i_default_acl | kuid_t/kgid_t (already namespace-resolved), POSIX ACL cache | i_rwsem exclusive (setattr per locking.rst) |
| Size / allocation | i_size, i_blocks, i_bytes, i_blkbits, i_write_hint | logical 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() |
| Timestamps | i_atime_sec, i_mtime_sec, i_ctime_sec + matching _nsec | last read / last content change / last metadata change | accessors only (inode_set_ctime_current() etc.); never assign the fields |
| Names | i_nlink (union with __i_nlink), i_dentry (union with i_rcu) | on-disk hard-link count; the list of cached dentries naming this inode | i_nlink via the set/clear/inc/drop_nlink() helpers under the directory’s i_rwsem; i_dentry under i_lock |
| Behaviour | i_op, i_fop (union with free_inode), i_sb | metadata vtable, open-file vtable, owning mount instance | assigned once at instantiation; read-mostly, ____cacheline_aligned tables |
| Data | i_mapping, i_data, i_flctx, i_devices | page-cache handle and the embedded address_space; POSIX-lock context; the per-cdev list for device nodes | owned by The Page Cache and address_space; i_data.i_pages has its own XArray lock |
| Lifecycle | i_state, i_lru, i_sb_list, i_hash, i_io_list, dirtied_when, dirtied_time_when | the I_* bit set; membership of the LRU, the superblock’s inode list, the global hash, and the writeback lists | i_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 counts | i_count, i_writecount, i_readcount, i_dio_count | in-memory holders; writers (negative = MAP_DENYWRITE-style deny); RO opens; in-flight direct I/O | all atomic_t; i_count drops to zero under i_lock via atomic_dec_and_lock() |
| Versioning | i_version, i_sequence | NFS change attribute; a futex-hashing sequence number | atomic64_t |
| The overlay union | i_pipe | i_cdev | i_link | i_dir_seq | four mutually exclusive interpretations selected by i_mode — see below | context-dependent; i_link requires a release barrier and an RCU grace period |
| fs-private | i_private, plus the fs’s own struct wrapped around this one | driver/filesystem scratch pointer | the 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_nsecin releases after 6.12. Reason: the 6.12 layout is directly verified, but the series’ later history was not traced here. To resolve: grepinclude/linux/fs.hatv6.18forI_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) | Members | What selects the live member | Why it is safe |
|---|---|---|---|
i_nlink / __i_nlink (line 656) | const unsigned int i_nlink, unsigned int __i_nlink | not a real alternation — the same storage exposed twice | the 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_rcu | lifecycle phase — i_dentry while the inode is alive, i_rcu only once it is being freed | by 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 destruction | same 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_seq | i_mode’s type bits — FIFO, character device, symlink, directory | the 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_pipehangs the pipe buffer ring off a FIFO inode. This is whymkfifoproduces 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 fromi_pipe.alloc_inode()explicitly zeroes it (inode->i_pipe = NULL,fs/inode.cline 185) so a non-FIFO inode can never be mistaken for one with a stale pipe.i_cdevpoints at the character device this inode names. It is the reasoni_rdevandi_cdevare different things:i_rdevis the number stored on disk in the device node, andi_cdevis the in-memory driver object that number was resolved to on first open.evict()callscd_forget(inode)forS_ISCHRinodes precisely to unhook this pointer before the inode dies.i_linkis a fast path for symlinks: if a filesystem parks the target string here,fs/namei.creads it withres = READ_ONCE(inode->i_link)(line 1847) and skips the->get_link()call entirely. Only ifi_linkis NULL does the walker dispatch into the filesystem.vfs.rststates the two obligations this creates:i_linkmust not be freed until after an RCU grace period, and writing it afteriget()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_seqis 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 cancmpxchgthe counter from an even value to odd;end_dir_add()publishesn + 2withsmp_store_release()(fs/dcache.clines 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 directoryi_mutexwith the shared-readi_rwsemin v4.7 (verified by existence-check:include/linux/fs.hat v4.6 declaresstruct mutex i_mutex;and contains noi_rwsem; at v4.7i_mutexis gone andi_rwsemappears). 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.
The Two Reference Counts: i_nlink vs i_count
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 freedAfter 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:
-
In-memory generic vs in-memory fs-specific. Each filesystem embeds the VFS
struct inodeinside a larger, filesystem-private struct —struct ext4_inode_infofor ext4,struct xfs_inodefor XFS,struct shmem_inode_infofor tmpfs. The generic inode is recovered from a pointer to the fs-specific struct (and vice versa) with thecontainer_ofpattern, usually wrapped in a helper likeEXT4_I(inode). When the VFS hands a filesystemstruct inode *inode, the filesystem callsEXT4_I(inode)to reach its own extra fields (extent tree root, reservation state, etc.). The fs allocates the combined struct in its ownalloc_inodesuperblock op; the VFS never sees the private tail. -
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 genericstruct inode(and its private tail); when the inode is dirtied and written back, it marshals the other direction. Soi_mode,i_size, timestamps, and block pointers exist in both representations, in different layouts, and the filesystem’swrite_inodeop 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:openlives infile_operations, notinode_operations, because the per-openstruct fileis allocated first and then itsopenmethod is invoked. When the inode is a device node,i_fopis swapped at open time for the driver’s file ops — that is how opening/dev/sdaends 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 anaddress_space i_datadirectly, and for an ordinary filei_mappingsimply points at&inode->i_data. The indirection exists because some inodes (notably block-device inodes) wanti_mappingto point at a different inode’si_dataso 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.
| Method | Called for | i_rwsem held by the VFS | Optional? |
|---|---|---|---|
lookup | resolve 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 error | shared | required for any directory |
create | open(O_CREAT), creat(2). The dentry handed in is guaranteed negative | exclusive | only to support regular files |
link | link(2) | exclusive (both the directory and the victim) | only to support hard links |
unlink | unlink(2) | exclusive on both, plus exclusive on the victim inode | only to support deletion |
symlink | symlink(2) | exclusive | only to support symlinks |
mkdir | mkdir(2) | exclusive | only to support subdirectories |
rmdir | rmdir(2) | exclusive on both, plus exclusive on the victim | only to support removing subdirectories |
mknod | mknod(2) — device nodes, FIFOs, sockets | exclusive | only to support those types |
rename | rename(2)/renameat2(2); must return -EINVAL for unknown flags; handles RENAME_NOREPLACE and RENAME_EXCHANGE | exclusive on both parents and some children; cross-directory also takes the per-superblock s_vfs_rename_sem | only to support rename |
get_link | follow 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 proceed | none | only for symlinks; still required even if i_link is set |
readlink | now only an override for readlink(2) when get_link uses nd_jump_link() or the object is not really a symlink | none | rarely needed |
permission | access 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-walk | none | optional — generic check used if NULL |
getattr | stat(2)/statx(2) | none | optional |
setattr | chmod, chown, truncate, utimes | exclusive | optional |
update_time | update a timestamp or i_version; if NULL, “the VFS will update the inode itself and call mark_inode_dirty_sync” | none | optional |
get_inode_acl / get_acl / set_acl | POSIX ACL access | none / none / (via setattr paths) | optional |
listxattr | listxattr(2) | none | optional |
fiemap | ioctl(FS_IOC_FIEMAP) extent mapping | none | optional |
fileattr_get / fileattr_set | FS_IOC_GETFLAGS/FS_IOC_SETFLAGS and the FSXATTR pair; if unset, fall back to f_op->ioctl() | “no or exclusive” / exclusive | optional |
atomic_open | the 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_CREAT | optional; a latency optimisation for network filesystems |
tmpfile | the end of an O_TMPFILE open — “equivalent to atomically creating, opening and unlinking a file in given directory” | none | optional |
get_offset_ctx | supply the offset context for a directory using simple_offset_dir_operations | none | required 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
permissionprototype. Reason:locking.rstat v6.12 listsint (*permission)(struct mnt_idmap *, struct inode *, int, unsigned int)(four parameters), whilefs.hat the same tag declaresint (*permission)(struct mnt_idmap *, struct inode *, int)(three) — the documentation lags the header, andvfs.rstagrees withfs.h. The semantics (rcu-walk viaMAY_NOT_BLOCKinside the mask,-ECHILDto retry) are consistent across all three. To resolve: treatfs.hat 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 markedI_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 secondopen()of the same file free of disk I/O. - (B) Flushed, then freed.
drop == 0but caching was refused — becauseI_DONTCACHEis set, or becauseSB_ACTIVEis clear (the filesystem is being unmounted). Only this branch setsI_WILL_FREE, and it does so purely to mark the window in whichwrite_inode_now()runs withi_countalready 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 toI_FREEINGwithout ever settingI_WILL_FREE, andevict()frees the on-disk inode and its data blocks.
A correction worth stating plainly
It is natural to assume
I_WILL_FREEis the “about to be deleted” flag on the unlink path. It is the opposite: on the unlink pathdropis 1, so branch (B) is skipped entirely andI_WILL_FREEis never set. The header comment agrees —I_WILL_FREE“must be set when callingwrite_inode_now()ifi_countis 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.
| Bit | Value | Role | Set by | Cleared by |
|---|---|---|---|---|
I_NEW | 1 << 0 | lock + 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_SYNC | 1 << 1 | lock + completion. Writeback of this inode is running; also pins the inode in memory for the flusher | the writeback machinery entering writeback_single_inode() | the same code on completion, with a wake-up on the bit address |
I_LRU_ISOLATING | 1 << 2 | inode is pinned while being isolated from the LRU without holding i_count | the inode shrinker’s isolate callback | when isolation finishes; evict() calls inode_wait_for_lru_isolating() before proceeding |
I_DIRTY_SYNC | 1 << 3 | inode 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_DATASYNC | 1 << 4 | data-relevant metadata changed (e.g. i_size), so fdatasync() must write the inode | __mark_inode_dirty(I_DIRTY_DATASYNC) | writeback |
I_DIRTY_PAGES | 1 << 5 | the inode has dirty pages; “inode itself may be clean” | page dirtying through the address_space | writeback of those pages |
I_WILL_FREE | 1 << 6 | narrow window in iput_final() branch (B): write_inode_now() is being called with i_count == 0 | iput_final() only when drop == 0 and caching was refused | iput_final() itself, immediately before setting I_FREEING |
I_FREEING | 1 << 7 | the inode is being freed but may still have dirty pages/buffers attached | iput_final(), and the reclaim/unmount paths | never — it persists through I_CLEAR until destroy_inode() |
I_CLEAR | 1 << 8 | added by clear_inode(): the inode is now clean and can be destroyed | clear_inode(), as i_state = I_FREEING | I_CLEAR | never; the object is freed next |
I_REFERENCED | 1 << 9 | “recently referenced on the LRU list” — the second-chance bit for inode reclaim | iput_final() / lookup hits on an LRU inode | the shrinker’s first pass, which gives the inode one more round instead of evicting it |
I_LINKABLE | 1 << 10 | an O_TMPFILE inode is allowed to be given a name by linkat() despite having i_nlink == 0 | vfs_tmpfile() when O_EXCL was not requested | after the link completes |
I_DIRTY_TIME | 1 << 11 | timestamps are dirty and lazytime is in effect, so the update may be deferred | timestamp updates under lazytime | when I_DIRTY_INODE gets set, or on the deferred flush |
I_WB_SWITCH | 1 << 12 | cgroup writeback: this inode is being moved between bdi_writeback contexts | inode_switch_wbs() | inode_switch_wbs_work_fn() when the switch completes |
I_OVL_INUSE | 1 << 13 | overlayfs claims exclusive ownership of an upper/work directory inode among overlay mounts | overlayfs at mount | overlayfs at unmount |
I_CREATING | 1 << 14 | a new object’s inode is mid-setup | inode_insert5() / d_tmpfile() paths | unlock_new_inode() / d_instantiate_new() |
I_DONTCACHE | 1 << 15 | “evict inode as soon as it is not used anymore” — opts this inode out of the LRU entirely | d_mark_dontcache(), or a filesystem that wants no caching | not cleared; it forces branch (B) in iput_final() |
I_SYNC_QUEUED | 1 << 16 | the inode sits on a b_io/b_more_io writeback list, so mark_inode_dirty() must not move it between dirty lists | queuing for writeback | dequeuing |
I_PINNING_NETFS_WB | 1 << 17 | the inode pins a netfs/fscache object for writeback | the netfs writeback path | when 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_SWITCHandI_PINNING_NETFS_WB. Reason:fs.hdocuments 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. TheI_NEW,I_SYNC,I_WILL_FREE,I_FREEINGandI_CLEARrows are read directly fromfs/inode.c. To resolve: readfs/namei.c:vfs_tmpfile(),fs/fs-writeback.c:inode_switch_wbs*(), andfs/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.txtThree 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=2A 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 linkDirectories 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 # tmpfsBirth 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 +0200Birth 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 245707Two things that would have made better measurements were blocked on this host, and the substitutes are worth recording:
/proc/slabinfois unreadable.ls -l /proc/slabinfoshows-r-------- 1 root root(mode 0400), andhead -2 /proc/slabinforeturnsPermission deniedas uid 1000. Soinode_cache’s object size and object count could not be read directly. The working substitute is the cgroup v2memory.statslab_reclaimablecounter for the caller’s own cgroup — becauseinode_cachepanddentry_cacheare both created withSLAB_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_cachescannot 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
- “
ctimeis creation time.” No.ctimeis 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 exposedstx_btime(birth time) where the filesystem records it (ext4, XFS, Btrfs do; not all do). Readingctimeas birth time is a frequent bug. - “Disk is full but
dfshows space free.” A process holding a large unlinked file open keeps its blocks allocated (i_nlink == 0,i_count > 0).duwon’t find the file (no name), but the blocks are still gone.lsof +L1lists files with link count below 1; truncating via/proc/<pid>/fd/Nor 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_inoalone 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 -ishows this; creating files then fails withENOSPCdespitedfshowing free space. Modern dynamic-inode filesystems (XFS, Btrfs) largely avoid this. - Modifying
i_nlinkdirectly. A filesystem that assignsinode->__i_nlinkinstead of callinginc_nlink()/drop_nlink()corrupts the VFS’s view and trips warnings; the union withconst unsigned int i_nlinkexists precisely to make direct writes a compile error on the read alias.
See Also
- VFS Dentry Object — the names that point at an inode; the
i_dentry/d_aliaslist that ties many names to one inode - VFS Operation Tables — the contents of
i_opandi_fopand how the VFS dispatches through them - The Page Cache and address_space — what
i_mapping/i_dataactually manage - VFS Superblock Object — the mounted instance an inode belongs to (
i_sb) - The struct file and Open File Description — an open handle, one of the holders of
i_count - The Virtual File System Layer — the abstraction the inode is part of
- Path Resolution and Name Lookup — how a path walk arrives at an inode via dentries
- Dentry States and Negative Dentries — the dentry-side lifecycle, and why caching a missing name is the dangerous half
- The Dentry Cache — the hash, the LRU and the shrinker that actually decide when an inode’s last name disappears
- Shrinkers and Slab Reclaim — how
inode_cachepanddentry_cacheare trimmed under pressure; owns the reclaim side entirely - Address Space and the Page Cache XArray — the index
i_mappingpoints at, in detail - VFS File Object — the per-open object whose existence keeps
i_countabove zero - RCU-Walk and Ref-Walk — why
i_linkneeds a release barrier and an RCU grace period - Filesystem Registration and Mounting Internals — where
sb->s_op->alloc_inodeand the fs-private wrapper struct get installed - overlayfs Union Filesystem — copy-up and whiteouts are operations on exactly the dentry/inode pair described here, and
I_OVL_INUSEis overlayfs’s claim on an upper-directory inode - XFS Filesystem Internals — a filesystem with dynamically allocated inodes, the counter-example to ext4’s fixed inode table
- MOC: Linux Filesystems and VFS MOC