Filesinodels -istathard linklink count

Inodes

On Unix-style file systems the inode is the file: a fixed-size record holding mode, owner, size, timestamps, link count and the block map — everything except the name, which lives in directories that point at it by number.

Unix-styleLinux
▶ InteractiveInterview question
Progress

The problem

df -h says 40% free. touch x says "No space left on device". Every engineer meets this once, usually at 3 a.m., and the explanation is that a file system has two things that can run out — and only one of them is bytes.

From a name to an inode to blocks

Linux

A directory on ext4, XFS, btrfs or APFS is a list of (name, inode number) pairs — a hash-indexed tree on ext4 (dir_index), a B+tree on XFS. The name resolves to a number; the number indexes the file system’s inode table (ext4) or inode B-tree (XFS, btrfs). The inode is the record found there. It is the file; the name is a pointer to it, and there may be several such pointers or, transiently, none (Files, Paths and Names).

What the inode holds: mode (type — regular, directory, symlink, device, socket, FIFO — and the permission bits), uid/gid, size in bytes, timestamps (atime last access, mtime last content change, ctime last inode change; ext4 also has a creation time visible via statx), link count, the number of blocks allocated, and the block map. What it does not hold: its name, and its path. stat prints all of it; ls -i prints just the number.

The inode number is unique within one file system, not across the machine. Two files on different mounts can share number 12; the pair (device, inode) is the real identity, which is what rsync, tar and backup tools use to recognise hard links and what find -samefile compares. On XFS the inode number encodes its location (allocation group, block, offset), which is why XFS numbers are large and sparse.

`stat` shows the inode; `ls -i` shows two names sharing one
$ ln app.log app.log.link
$ ls -i app.log app.log.link
918273 app.log   918273 app.log.link
$ stat app.log
  File: app.log
  Size: 84213487616  Blocks: 164479472  IO Block: 4096   regular file
Device: 259,2   Inode: 918273   Links: 2
Access: (0644/-rw-r--r--)  Uid: ( 1001/     app)   Gid: ( 1001/     app)
Access: 2026-08-25 09:12:01.104 +0000
Modify: 2026-08-25 09:11:59.980 +0000
Change: 2026-08-25 09:12:00.002 +0000
 Birth: 2026-08-16 02:00:11.000 +0000

Direct and indirect pointers vs extents

Linux

The classic Unix block map, still used by ext2 and ext3, is an array inside the inode: 12 direct pointers to data blocks, then one pointer to a single-indirect block (itself a block full of 1024 pointers with 4 KiB blocks and 4-byte addresses), one to a double-indirect block (1024 × 1024), and one to a triple-indirect block. Small files — the vast majority — need only the direct pointers; the first 48 KiB is reachable with zero extra reads, the next 4 MiB with one, and so on up to about 4 TiB. Reading byte 3 GB of a file means following three levels of pointers, each a potential disk read.

The cost is that a contiguous 1 GB file still needs 262,144 individual pointers spread across indirect blocks, all of which must be read to map it, and all of which must be written to free it (deleting a large file on ext3 was famously slow). Extents replace the pointer array with (logical block, physical block, length) triples: one entry can describe up to 128 MiB of contiguous space on ext4. The inode holds four extents inline; larger or fragmented files spill into an extent tree, a B+tree whose leaves are extents. XFS, btrfs, APFS and NTFS all use extent-shaped maps in their own tree structures.

Extents are why fragmentation matters again in a way it did not on a fresh pointer-based file system: a file appended in tiny increments while other files are also growing ends up with thousands of short extents, and its read pattern becomes random rather than sequential. filefrag -v shows the map; e4defrag and XFS’s xfs_fsr rewrite files contiguously. Preallocation (fallocate) is how databases and log writers reserve a contiguous run up front.

  • Pointer-based map: constant inode size, depth grows with file size, one extra read per level.
  • Extent-based map: one entry per contiguous run; sequential files are tiny to describe; fragmented files grow a tree.
  • Blocks: in stat is the allocated count in 512-byte units; a sparse file has a size far larger than its blocks.

Hard links, link count and what rm does

Unix-style

The link count in the inode is the number of directory entries that point at it. ln adds an entry and increments it; rmunlink(2) — removes an entry and decrements it. Nothing about the data changes until the count hits zero and the last open descriptor closes; only then are the blocks returned to the free bitmap and the inode itself freed. A directory’s count starts at 2 (its own entry in the parent, plus its own .) and grows by one for every subdirectory’s .., which is why ls -ld on a directory with 5 subdirectories shows 7.

Hard links cannot cross file systems (an inode number means nothing on another device) and, on almost every system, cannot point at directories (it would create cycles that .. could not resolve). Symbolic links have neither restriction because they are separate inodes whose data is a path string; that is why ls -l on a symlink shows a size equal to the target path’s length and why a symlink survives its target being deleted — as a dangling pointer.

The link count is also a correctness tool. Package managers and content-addressed stores (Nix, pnpm’s node_modules, Git’s object store on the same volume) deduplicate by hard-linking identical files; a build directory with 40,000 files can cost almost nothing in blocks. The trap is that editing "one" of them edits all of them — tools that care copy-on-write first (cp --reflink on btrfs/XFS/APFS does this at the block level).

Running out of inodes with space to spare

Linux

ext4 decides how many inodes exist when the file system is created: by default one inode per 16 KiB of space (-i in mkfs.ext4), stored in fixed inode tables inside each block group. A 100 GB volume gets about 6.5 million inodes. That is plenty for a workstation and hopeless for a mail spool, a PHP session directory, a CI cache, a Docker overlay directory or a node_modules tree with millions of 200-byte files: each consumes a whole inode and a whole 4 KiB block, and the inode table fills while most of the bitmap is still zero. touch then fails with ENOSPC — the same error as a full disk — and df -h shows space free.

df -i shows the inode count and usage; a IUse% of 100 with a Use% of 40 is the diagnosis. The cure is finding and deleting (or archiving into a single tarball) the directory with the most small files — find / -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head — because you cannot add inodes to an existing ext4 volume without reformatting. XFS allocates inode chunks on demand and is limited only by a percentage of the volume (maxpct, default 25%); btrfs has no fixed inode limit at all. That is a real reason to choose XFS for a small-file-heavy volume.

The mirror-image failure is also real: a volume formatted with a large inode ratio for a media library cannot then host a source tree. Both are decided at mkfs time and both are invisible until they are not. tune2fs -l shows the numbers for ext4; check them before you commit a volume to a workload.

Free space, no free inodes
$ touch /var/cache/sessions/x
touch: cannot touch '/var/cache/sessions/x': No space left on device
$ df -h /var
Filesystem      Size  Used Avail Use% Mounted on
/dev/sdb1       200G   78G  122G  40% /var
$ df -i /var
Filesystem       Inodes    IUsed  IFree IUse% Mounted on
/dev/sdb1      13107200 13107200      0  100% /var

Key points

  • The inode is the file: mode, owner, size, timestamps, link count, block map. Names live in directories as (name, inode number) pairs.
  • Inode numbers are unique per file system, not per machine; identity is (device, inode).
  • ext2/ext3 map blocks with direct, single-, double- and triple-indirect pointers; ext4, XFS, btrfs, APFS and NTFS use extents — one entry per contiguous run.
  • ln increments the link count, rm decrements it; the inode and its blocks are freed only at zero links and zero open descriptors.
  • ext4 fixes the inode count at mkfs; millions of tiny files exhaust inodes while df -h shows free space. df -i is the diagnostic; XFS and btrfs do not have the fixed limit.

Why does this exist?

Mechanisms are answers to constraints. Open each question before reading the answer.

Why store the metadata in a separate record instead of in the directory entry?

So that one file can have several names and so that renaming does not copy metadata. FAT stores metadata in the directory entry, and as a result has no hard links and no way to keep a deleted-but-open file alive.

Why did file systems move from block pointers to extents?

Because storage and files both grew by a thousand-fold: a pointer per 4 KiB block made large files expensive to map, free and check, while extents describe a contiguous gigabyte in one entry and make sequential layout explicit.

Why does ext4 fix the inode count in advance?

Its inode tables sit at known offsets inside each block group so an inode number maps to a disk location with arithmetic — no index needed. The price of that simplicity is a limit chosen before the workload is known.

Inode explorer

Inode explorer
Unix-style file systems separate the name (a directory entry) from the file (an inode plus its blocks). Everything odd about hard links and deleted-but-open files follows from that.
inode #902
mode   -rw-r--r--   uid 1000  gid 1000
size   16384 bytes  blocks 4   links 1
atime  2026-08-25 09:12:03   mtime 2026-08-25 09:11:58   ctime 2026-08-25 09:11:58
direct [40 41 42 47 · · · · · · · ·]
single indirect → ∅
/var/log/app/today.log → #902
Blocks (green direct · purple indirect block · blue via indirect)
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
The inode is the file. Names are directory entries that point at it.
Unix-style

How it fails

What the failure looks like from inside real software.

  • ENOSPC with free space: inode table exhausted by millions of small files (sessions, cache, mail, node_modules). df -i confirms; deleting large files does not help.
  • Backup or du counts a hard-linked tree many times over, or a restore silently turns links into independent copies that then drift apart.
  • Editing a file in a pnpm or Nix store changes every project that links it, because they share one inode.
  • A large append-only log has 200,000 extents after a year; sequential reads have become random reads. filefrag shows it; preallocation would have prevented it.
  • A tool compares files by inode number across two mounts and declares unrelated files identical.
  • Deleting a multi-terabyte file on an old pointer-based file system freezes the writer for many seconds while every indirect block is walked.