Systems

Inodes and Filesystem Layout

The on-disk record that is your file — everything except its name

An inode (index node) is a fixed-size on-disk structure that stores all of a file's metadata — its type, permission bits, owner, size, link count, and timestamps — together with the pointers to the data blocks that hold its contents. The one thing an inode does not store is the filename: names live in directory entries (dentries) that map a name to an inode number. That single design decision, dating to the original Unix filesystem (Ken Thompson, ~1971), is why hard links can give one file many names, why mv within a filesystem is O(1), and why you can run out of files while gigabytes of disk sit empty.

  • What it storesMetadata + block pointers (not the name)
  • Classic pointer count15 (12 direct, 1 single, 1 double, 1 triple indirect)
  • ext4 inode size256 bytes (default), extent trees replace pointers
  • Path lookup costO(path depth) name→inode steps, cached in the dcache
  • Name-to-inode mapDirectory entry (dentry)
  • Failure modeInode exhaustion → ENOSPC with free blocks (df -i)

Interactive visualization

Press play, or step through manually. The visualization is yours to drive — try it before reading on.

Open visualization fullscreen ↗

Watch the 60-second explainer

A condensed visual walkthrough — narrated, captioned, under a minute.

Why inodes matter

Nearly every question a program asks about a file — "how big is it?", "am I allowed to write it?", "where do its bytes live on disk?" — is answered by the inode, not by the name you typed. The name is just a lookup key. Understanding that split explains a surprising amount of everyday behavior: why deleting a file that a running process still has open frees no space, why two files with different names can be literally the same bytes, and why a backup tool that copies "the file" must decide whether to preserve inode identity or duplicate the data.

The layout of inodes and blocks on disk is also where filesystem performance is won or lost. Keeping a file's inode near its data, and keeping sibling files near each other, turns random seeks into short hops — the whole reason ext4 organizes the disk into block groups. Get the layout wrong and even a fast NVMe drive spends its bandwidth chasing scattered metadata.

How the inode names and locates a file

Think of three distinct objects, each with a job:

  • Directory entry (dentry) — a small record inside a directory's data that pairs a filename with an inode number. A directory is nothing more than a file whose contents are a list of these (name → inode#) pairs (in ext4, stored as a hashed B-tree, HTree, for fast lookup).
  • Inode — the fixed-size record, found by indexing the inode table with the inode number. It carries the metadata and the block pointers. There is exactly one inode per file, no matter how many names point at it.
  • Data blocks — the fixed-size disk blocks (commonly 4 KiB) that actually hold the file's bytes, reached through the inode's pointers.

To open /home/ann/notes.txt, the kernel resolves the path one component at a time. It starts at the root inode (a well-known fixed number, usually 2 on ext), reads its directory data to find the dentry for home, gets an inode number, loads that inode, confirms it is a directory, reads it to resolve ann, and repeats for notes.txt. Each step is a name-to-inode translation. Repeated lookups are served from the in-memory dentry cache (dcache), so a hot path costs no disk I/O at all.

Direct, indirect, double and triple indirect pointers

How does a tiny, fixed-size inode point at a file that might be gigabytes? The classic Unix answer is a multi-level index. A traditional inode holds 15 block pointers:

  • 12 direct pointers — each names one data block. With 4 KiB blocks that covers files up to 48 KiB with a single inode read and zero extra indirection. Most files on a real system are this small, so the common case is fast.
  • 1 single indirect pointer — points at a block full of pointers. With 4-byte pointers, a 4 KiB block holds 1024 of them, addressing another 1024 blocks (4 MiB).
  • 1 double indirect pointer — points at a block of pointers to blocks of pointers, addressing up to 1024² ≈ 1 million blocks (~4 GiB).
  • 1 triple indirect pointer — one more level, 1024³ ≈ 1 billion blocks (~4 TiB).

The elegance is that access cost scales with how far into the file you read, not with the file's total size. Reading byte 0 costs one inode lookup; reading a byte in the triple-indirect region costs four block reads (inode → L1 → L2 → L3 → data). Small files stay cheap; huge files remain addressable from the same small structure.

Why ext4 replaced pointers with extents

Per-block pointers are wasteful for large, contiguous files: a 1 GiB video needs a quarter-million pointers just to say "these blocks, in order." ext4 introduced extents — an extent is a triple (logical block, physical block, length) that describes a run of up to 32,768 contiguous blocks with a single record. The inode holds a small extent tree; only fragmented files fall back to more nodes. This cuts metadata dramatically and makes sequential I/O nearly seek-free. XFS and Btrfs use similar extent-based B+ trees, while the traditional block-pointer scheme still describes classic UFS.

Because the name and the inode are separate, several dentries can carry the same inode number. Each is a hard link — a first-class, indistinguishable name for one file. The inode keeps a link count (st_nlink). Creating a hard link increments it; unlink() (what rm calls) decrements it. The data blocks are freed only when the count reaches zero — and even then, not while a process still holds the file open (the kernel tracks an in-memory open count too). This is the mechanism behind the classic trick of creating a temp file, unlinking it immediately, and continuing to read and write through the open descriptor: the data lives on with zero names until the last descriptor closes.

A directory itself has a link count of at least 2 the moment it is created: one from its parent's dentry and one from its own . entry; every subdirectory adds one more via its ... That is why st_nlink on a directory equals two plus the number of subdirectories. To prevent unbreakable cycles, most systems forbid hard-linking directories.

Hard linkSymbolic linkCopy (cp)
Points atAn inode numberA pathname stringNothing — new inode
Shares data?Yes (same blocks)Indirectly, via the target nameNo — independent bytes
Own inode?No (shares one)Yes (tiny file)Yes
Can cross filesystems?No (inode numbers are per-fs)YesYes
Survives target deletion?Yes (count > 0)No — danglesN/A (separate)
Can link a directory?No (blocked)YesYes (recursive)
Disk cost of the linkOne dentry (~bytes)One dentry + a small inodeFull file size

Filesystem layout: superblock and block groups

Zoom out from a single file to the whole volume. An ext-family filesystem partitions the disk into equal-size block groups. Laying out each group with its own metadata gives locality (a file's inode near its data) and fault isolation (corruption stays local). Every group typically contains:

  • Superblock — the master descriptor: total inode and block counts, block size, free counts, mount state (clean / needs fsck), and layout parameters. The primary lives near the start; backup superblocks are scattered across groups so a damaged primary is recoverable (fsck -b 32768).
  • Group descriptor table — where each group's bitmaps and inode table live.
  • Block bitmap — one bit per data block in the group: allocated or free. Allocation is a bitmap scan.
  • Inode bitmap — one bit per inode in the group's slice of the inode table.
  • Inode table — the fixed array of inode structures for this group. Its size is decided at mkfs time and never grows.
  • Data blocks — the remaining space, holding file and directory contents.

ext4 refines this with flex groups (bunching the bitmaps and inode tables of several groups together so the large data regions stay contiguous) and meta block groups (to scale past the limits of a single group-descriptor block). The intuition, though, is unchanged since the Berkeley Fast File System (McKusick, 1984): cluster metadata with the data it describes and keep related files close.

The catch: inode exhaustion

Because ext2/3/4 fix the inode count at format time, you can exhaust inodes long before you exhaust blocks. The default ratio is one inode per 16 KiB of disk (the bytes-per-inode parameter). A directory tree of millions of tiny files — mail spools, PHP session files, cache shards, npm's node_modules — burns through inodes while leaving most block space untouched. The symptom is baffling the first time: writes fail with ENOSPC, "No space left on device," yet df -h shows plenty free. The tell is df -i, which reports 100% inode usage.

# Blocks look fine...
$ df -h /data
Filesystem      Size  Used Avail Use% Mounted on
/dev/sdb1       200G   47G  143G  25% /data

# ...but inodes are gone:
$ df -i /data
Filesystem      Inodes   IUsed IFree IUse% Mounted on
/dev/sdb1     13107200 13107200     0  100% /data

# The fix is either delete tiny files, or reformat with more inodes:
$ mkfs.ext4 -N 40000000 /dev/sdb1        # explicit inode count
$ mkfs.ext4 -i 4096     /dev/sdb1        # or: one inode per 4 KiB

Modern filesystems designed for this workload — XFS and Btrfs — allocate inodes dynamically from free space, so inode count grows with the disk and this failure mode largely disappears. The tradeoff is that ext4's fixed table makes fsck and inode lookup trivially indexed, whereas dynamic allocation needs a B-tree to find inodes.

A worked example: what really happens on rename and delete

$ echo "hello" > a.txt      # allocate inode 5001, one data block, dentry a.txt→5001
$ ls -i a.txt
5001 a.txt

$ ln a.txt b.txt            # NEW dentry b.txt→5001; link count on 5001 goes 1→2
$ ls -i a.txt b.txt
5001 a.txt   5001 b.txt      # same inode — literally the same file

$ stat -c '%h' a.txt        # hard-link count
2

$ mv a.txt c.txt            # O(1): remove dentry a.txt, add dentry c.txt→5001. No data moves.
$ rm b.txt                  # unlink b.txt; link count 2→1. Data still intact via c.txt.
$ rm c.txt                  # unlink c.txt; link count 1→0 → data blocks and inode freed.

The lesson: mv within one filesystem never touches file data — it just edits dentries. Across filesystems it must copy-then-delete, because inode numbers are not portable between volumes. And rm does not "delete a file"; it removes a name and, only when the last name (and last open handle) is gone, reclaims the inode and its blocks.

Common misconceptions and pitfalls

  • "The filename is stored in the inode." It is not. The inode is nameless; the directory owns the name. This is the single most common misconception, and every other inode behavior follows from correcting it.
  • "rm frees space immediately." Only if that was the last link and no process has the file open. A deleted-but-open log file can silently consume a disk until the process restarts — a classic production incident.
  • "Out of space means out of blocks." ENOSPC also fires on inode exhaustion. Always check both df -h and df -i.
  • "You can hard-link across drives." No — inode numbers are meaningful only within one filesystem, so hard links cannot cross a mount point. Use a symlink instead.
  • "Copying preserves inode identity." cp allocates a brand-new inode and duplicates the bytes. If you wanted the same file under a new name, you wanted a link, not a copy.
  • "Bigger disk automatically means more files." On ext4 the inode count is frozen at mkfs. Growing the partition with resize2fs adds blocks and proportional inodes only for the newly added space — plan the ratio up front for small-file workloads.

Frequently asked questions

What does an inode store, and what does it NOT store?

An inode stores a file's metadata — type, permission bits, owner UID/GID, size, link count, and the atime/mtime/ctime timestamps — plus the pointers to the data blocks that hold the file's contents. It does NOT store the filename. Names live in directory entries (dentries), each of which maps a human-readable name to an inode number. This separation is exactly why a single file can appear under multiple names via hard links: several dentries point at the same inode.

What are direct, indirect, double indirect, and triple indirect block pointers?

Classic Unix inodes hold 15 block pointers. The first 12 are direct pointers to data blocks — for small files that's all you need. The 13th is a single indirect pointer to a block full of pointers, the 14th is a double indirect pointer (a block of blocks of pointers), and the 15th is a triple indirect pointer. With a 4 KiB block and 4-byte pointers, each indirect block holds 1024 entries, so the tiers address roughly 12 blocks, then 1024, then 1024², then 1024³ — letting a small fixed-size inode reference terabyte-scale files while keeping small-file access to a single lookup.

What is the difference between a hard link and a symbolic link?

A hard link is another directory entry pointing at the same inode number — the two names are peers, indistinguishable, and the file's data survives until the inode's link count drops to zero. A symbolic link is its own tiny file whose contents are a pathname string; it points at a name, not an inode, so it can cross filesystems and dangle if the target is deleted. Deleting one hard link only decrements the count; deleting the target of a symlink leaves a broken link.

What is the superblock and what happens if it is corrupted?

The superblock is the master record of the filesystem: total block and inode counts, block size, free counts, the state (clean or needs-check), and layout parameters. If the primary superblock is corrupted, the filesystem may fail to mount. This is why ext-family filesystems scatter backup superblocks across block groups — you can recover with a command like 'fsck -b 32768' to point fsck at a backup copy.

Can you run out of inodes before running out of disk space?

Yes. On ext2/3/4 the number of inodes is fixed when you run mkfs, based on the bytes-per-inode ratio (default one inode per 16 KiB). A directory full of tiny files — mail spools, cache shards, session files — can exhaust the inode table while gigabytes of block space remain free. You will see ENOSPC ('No space left on device') even though df shows free space; 'df -i' reveals 100% inode usage. XFS and Btrfs allocate inodes dynamically and largely avoid this failure mode.

How does the kernel turn a path like /home/ann/notes.txt into an inode?

The kernel walks the path component by component. Starting from the root inode, it reads the directory's data blocks to find the dentry for 'home', which yields an inode number; it loads that inode, confirms it is a directory, and reads it to resolve 'ann', then again for 'notes.txt'. Each step is a name-to-inode lookup. The kernel caches these resolutions in the dentry cache (dcache) so repeated lookups skip the disk reads entirely.

Why do ext4 and other filesystems use block groups?

A block group is a contiguous span of the disk that bundles its own inode table, block and inode bitmaps, and data blocks. Grouping keeps a file's inode near its data and keeps files in the same directory in the same group, which minimizes seek distance on spinning disks and improves locality on SSDs. It also localizes the damage from corruption and lets fsck check groups in parallel. ext4 further coalesces block groups into flex groups to batch the metadata.