Frontend Labs
⋯

Chapter 15 — Filesystems: inodes, directories, and journaling

In one sentence

A filesystem separates a file's actual data from its metadata (an "inode" holding size, permissions, timestamps, and pointers to the data) from its name (which just lives in a directory, pointing at an inode) — and understanding this split explains several classic "wait, that shouldn't be possible" filesystem behaviours.

Why this matters

"df says I have space but I get 'no space left on device'," "why does deleting a huge open log file not free disk space until the process restarts," and "why can I rename a file across directories instantly" are all answered by this one model.

How it actually works

   THREE SEPARATE THINGS, commonly conflated as "a file":

     THE DATA           the actual bytes, stored in disk blocks
                        somewhere.
     THE INODE          a fixed-size metadata record: size, permissions,
                        owner, timestamps, and (crucially) POINTERS to
                        which disk blocks hold the data. Identified by an
                        INODE NUMBER (see it with `ls -i`).
     THE DIRECTORY
     ENTRY               just a NAME mapped to an inode number, living
                        inside a directory (which is itself just a
                        special kind of file). Multiple directory entries
                        (even in different directories) CAN point to the
                        SAME inode -- this is exactly what a HARD LINK
                        is (Chapter 25).

   THIS EXPLAINS SEVERAL THINGS THAT OTHERWISE SEEM WEIRD:
     - deleting a file (`rm`) doesn't necessarily delete the DATA -- it
       removes a directory entry and DECREMENTS the inode's "link
       count." The data is only actually freed when the link count AND
       the number of processes with the file open both reach zero. This
       is why a huge log file, deleted while a process still has it
       open, doesn't free disk space until that process closes it or
       restarts.
     - renaming/moving a file WITHIN the same filesystem is nearly
       instant, regardless of file size -- it's just updating a
       directory entry to point at the same inode, no data is actually
       moved. (Moving ACROSS filesystems is different -- it has to
       actually copy the data, then delete the original.)
     - a filesystem can run out of INODES before it runs out of DATA
       SPACE -- each filesystem has a FIXED number of inodes, set when
       it was created, independent of how much raw storage capacity
       exists.

   JOURNALING (ext4, XFS, and most modern filesystems use it): before
   making a metadata change, the filesystem first writes a small record
   of its INTENT to a journal (a dedicated log area). If the system
   crashes mid-operation, the filesystem can REPLAY the journal on
   next boot to finish or cleanly roll back the interrupted operation,
   instead of leaving the filesystem's own internal structures in an
   inconsistent, possibly-corrupted state. This is why modern Linux
   boxes almost never need the old, slow, full filesystem-consistency
   check after an unclean shutdown that was routine decades ago.

Real-world example

   "No space left on device" while `df -h` clearly shows free space is
   a genuinely common, well-documented real production incident, and
   it's a DIRECT consequence of this chapter's inode/data split: every
   filesystem is created with a FIXED NUMBER of inodes (`df -i` shows
   this, separately from `df -h`'s DATA space), and a workload that
   creates a very large number of SMALL files -- a classic real
   example being a mail server storing each email as a separate tiny
   file (the Maildir format), or a web application writing huge numbers
   of small session/cache files -- can exhaust the inode count while
   still having plenty of raw disk space free. This specific failure
   mode is documented extensively across sysadmin/SRE knowledge bases
   (Red Hat, DigitalOcean, and countless "why can't I write this file
   when df says I have space" Stack Overflow threads) precisely because
   it's a genuinely counter-intuitive symptom until you understand that
   "disk space" and "inode count" are two SEPARATE, independently
   exhaustible resources.

Try it yourself

# See inode usage vs. data usage -- two separate numbers
df -h .      # data space
df -i .      # inode count

# See a file's inode number, and watch a hard link share it
echo "hello" > original.txt
ls -i original.txt
ln original.txt hardlink.txt
ls -i hardlink.txt          # SAME inode number as original.txt
rm original.txt
cat hardlink.txt            # still works -- the data wasn't deleted,
                            # only one of its two directory entries was
rm hardlink.txt

# Watch "delete a file, space isn't freed until the process closes it"
python3 -c "
import time
f = open('/tmp/bigfile.tmp', 'w')
f.write('x' * 50_000_000)
f.flush()
import subprocess
subprocess.run(['rm', '/tmp/bigfile.tmp'])   # 'deleted' but still open
print('file removed from directory, but check: lsof | grep deleted')
time.sleep(5)
f.close()   # NOW the space is actually freed
"

Build it in Go

Journaling protects the filesystem's own structures, not your file's contents. Chapter 77 implements the write → fsync → rename → fsync-directory recipe that databases and package managers use to make file replacement atomic and durable, and measures its cost (about 1 ms per write on Linux, versus 0.07 ms for a plain write that isn't safe).

Common mistakes

  • "df -h shows free space, so I can't be out of disk space." Check df -i too — inode exhaustion is a separate, real failure mode.
  • Assuming rm-ing a file always immediately frees disk space. Not if another process still has it open — the space is freed only when the last reference (directory entry AND open file handle) goes away.
  • Confusing a hard link with a symbolic link (symlink). A hard link shares the SAME inode (can't cross filesystems, invisible as a "link" at all); a symlink is a separate, small file that just contains a PATH string, and breaks if the target moves (Chapter 25 covers both properly).

Check yourself

  1. What are the three separate things commonly lumped together as "a file"?
  2. Why doesn't deleting an open file immediately free its disk space?
  3. Why can a filesystem run out of space for NEW FILES while df -h shows plenty of free bytes?
  4. What does journaling protect against, and why did it reduce the need for long filesystem-check-on-boot delays?

Further reading

  • Book (free): OSTEP, "Files and Directories" and "Crash Consistency: FSCK and Journaling" chapters.
  • Reference: man 2 stat, man 1 df (the -i flag), man 2 link.
  • Article: search "linux inode exhaustion no space left on device" for current, practical troubleshooting writeups.

Generated from real-life-os-guide.md · 8 Oct 2026 16:36