All chapters in this track
- 01Chapter 1: Why OS Fundamentals Matter to Application Engineers
- 02Chapter 2: Processes, Threads, and the Process Lifecycle
- 03Chapter 3: The Memory Hierarchy: Registers to Disk
- 04Chapter 4: CPU Scheduling Algorithms
- 05Chapter 5: Memory Management I: Paging and Segmentation
- 06Chapter 6: Memory Management II: Virtual Memory and Page Replacement
- 07Chapter 7: File Systems: How Data Is Actually Stored and Retrieved
- 08Chapter 8: Interprocess Communication: Pipes, Shared Memory, Message Queues, Sockets
- 09Chapter 9: I/O Management and Interrupts
- 10Chapter 10: Deadlocks at the OS Level: Conditions, Prevention, Detection, Recovery
- 11Chapter 11: Virtualization and Containers: A Conceptual Introduction
- 12Chapter 12: When OS Abstractions Leak: Failure Modes Application Engineers Actually Hit
- 13Chapter 13: Walkthrough: What Happens When You Run a Program
- 14Chapter 14: Common OS Interview Questions: Paging, Page Faults, and Deadlock Scenarios
- 15Chapter 15: OS Concepts in System Design and Production Debugging
Chapter 7 of 15
Chapter 7: File Systems: How Data Is Actually Stored and Retrieved
Why this matters
Files feel like a simple, obvious abstraction — a name, some bytes, done. But "delete a large file and immediately reclaim disk space," "why does copying many small files take so much longer than one large file of the same total size," and "what does it mean that two file names can point to the same file" all have answers that require knowing what's actually underneath the file abstraction. This chapter builds that foundation.
The core concept
[BEGINNER] A file system organizes raw disk blocks into the named, hierarchical files and directories applications interact with. Two things are usually conflated but are actually separate: the file's name (which lives in a directory, mapping a name to an inode number) and the file's actual data and metadata (stored in an inode — a structure holding size, permissions, timestamps, and pointers to the actual data blocks on disk, but notably not the filename itself).
Directory "documents" (just a table mapping names to inode numbers):
+---------------+-------------+
| Name | Inode # |
+---------------+-------------+
| report.txt | 4821 |
| notes.txt | 9013 |
+---------------+-------------+
Inode 4821 (the actual file — no name stored here):
+------------------------------------------+
| Size: 2048 bytes | Permissions: rw-r--r-- |
| Owner: uid 1000 | Modified: 2026-01-15 |
| Data block pointers: [block 55, block 91] |
+------------------------------------------+
Actual data blocks 55 and 91 on disk: the real file content
Hard links and symbolic links, correctly distinguished
[SENIOR INSIGHT] Because a filename is just a directory entry pointing to an inode, multiple names can point to the exact same inode — this is a hard link. Both names are fully equivalent; there's no "original" and "copy," and the file's data is only actually freed when the last remaining hard link to that inode is removed (this is precisely why rming a file doesn't necessarily free disk space immediately — if another hard link or an open file descriptor still references the same inode, the data persists).
$ echo "content" > report.txt
$ ln report.txt report_backup.txt # HARD link: a second name for the SAME inode
$ ls -i report.txt report_backup.txt
4821 report.txt
4821 report_backup.txt # identical inode number — truly the same file
$ rm report.txt # only removes ONE directory entry
$ cat report_backup.txt # still works — the inode and data are still referenced
content
A symbolic link (symlink), by contrast, is a small special file whose content is just a path string pointing to another filename — not the same inode at all. Deleting the target breaks the symlink (it becomes "dangling"), unlike a hard link, which is indistinguishable from the "original."
$ ln -s report.txt report_link.txt # SOFT link: a separate file containing the path "report.txt"
$ ls -i report.txt report_link.txt
4821 report.txt
7734 report_link.txt # DIFFERENT inode — a small file that just stores a path
$ rm report.txt
$ cat report_link.txt # broken — the target it points to no longer exists
cat: report_link.txt: No such file or directory
[IN PRACTICE] This distinction directly explains a common production gotcha: a process that has a file open keeps its data alive even after the file is rm'd, because the process's open file descriptor is itself effectively another reference to the inode — the directory entry is gone, but the inode (and its data blocks) persists until every reference (hard links and open file descriptors) is gone. This is the standard trick behind "delete a huge log file to free disk space, but df still shows no space freed" — the process still writing to that log file holds it open, and the space isn't reclaimed until that process closes the file (or is restarted).
Why many small files are slower than one large file of the same total size
[IN PRACTICE] Every file operation (open, read, close) carries fixed overhead: a directory lookup, an inode read, and (critically, per Chapter 3's memory hierarchy) potentially a disk seek if the file's data blocks aren't contiguous or cached. A single 100 MB file might require a handful of sequential disk reads. Ten thousand 10 KB files scattered across a directory tree require ten thousand separate directory lookups, inode reads, and (if not cached) potentially ten thousand separate seeks — even though the total data volume is identical. This is a direct, practical application of Chapter 3's memory hierarchy lesson: the number of slow operations matters far more than the total bytes moved.
Journaling: how file systems survive a crash
[BEGINNER] A journaling file system (ext4, NTFS, APFS, and most modern file systems) writes a short description of an intended change to a dedicated journal area before actually modifying the file system's real data structures. If the system crashes mid-operation, the file system can replay (or safely discard) incomplete journal entries on next mount, restoring the file system to a consistent state instead of leaving it corrupted mid-update. This is the direct file-system-level analog of a database transaction log (covered in depth in the companion Database book) — the same underlying idea (write intent durably before acting, so a crash mid-action is always recoverable) applied at a different layer of the stack.
Common mistakes
- Assuming
rmalways immediately frees disk space. If another hard link or an open file descriptor still references the inode, the space isn't reclaimed — a very common cause of "I deleted a big file butdfshows no change" confusion. - Confusing hard links and symlinks, especially assuming a hard link "breaks" if the original is deleted the way a symlink does — hard links have no concept of "original," they're equivalent references to the same inode.
- Not accounting for per-file operation overhead when designing a system that creates many small files (e.g., using one file per cache entry, or per log line) — the fixed cost per file operation, multiplied by file count, can dominate over raw data volume (Chapter 3's hierarchy lesson again).
- Assuming a journaling file system makes application-level data loss on crash impossible. Journaling protects the file system's own metadata/structure consistency; it does not guarantee that an application's in-flight write (that hadn't been flushed/fsync'd) survives a crash — that's a separate, application-level durability concern.
Checklist: reasoning about file systems in practice
- When disk space isn't freed after deleting a file, check for other hard links (
ls -ito compare inode numbers) and processes still holding it open (lsof <file>orlsof +L1for already-deleted-but-open files). - Avoid designing systems that create very large numbers of small files where a single larger file (or a database) would serve the same purpose with far less per-operation overhead.
- Understand journaling protects file-system-structure consistency, not application-level write durability — an application that needs a write to survive a crash still needs to call
fsync/equivalent explicitly. - Use hard links only within the same file system/partition (they can't cross file system boundaries, since inode numbers are only unique within one file system) — symlinks can point anywhere, including across file systems.
Discussion / interview questions
-
"What's the difference between a hard link and a symbolic link?" Strong answer: A hard link is a second directory entry pointing to the exact same inode — fully equivalent to the "original," with no dependency between them; a symlink is a separate small file whose content is a path string pointing elsewhere, which breaks if the target is removed.
-
"Why might deleting a large file not immediately free up disk space?" Strong answer: The inode and its data blocks are only freed when every reference to them is gone — other hard links, or a process with the file still open via a file descriptor, keep the data alive even after the visible directory entry is removed.
-
"Why is copying ten thousand small files often much slower than copying one large file of the same total size?" Strong answer: Every file operation carries fixed per-file overhead (directory lookup, inode access, potential seeks for non-contiguous/uncached data); this overhead is paid once for a single large file but ten thousand times for ten thousand small files, even though total bytes moved is identical.
-
"What does a journaling file system protect against, and what does it NOT protect against?" Strong answer: Protects the file system's own metadata/structural consistency across a crash, by recording intended changes before applying them; does not guarantee an application's own in-flight, not-yet-flushed writes survive a crash — that requires the application to explicitly
fsyncor equivalent.
Chapter summary / cheat sheet
- A file's name (in a directory entry) and its data/metadata (in an inode) are separate — multiple names (hard links) can reference one inode.
- A hard link is an equivalent second name for the same inode; a symlink is a separate file containing a path, which breaks if the target disappears.
- Disk space is only reclaimed when every reference to an inode — hard links and open file descriptors alike — is gone, explaining the "deleted but space not freed" gotcha.
- Many small files carry much higher fixed per-file overhead than one large file with the same total data, directly following from Chapter 3's memory hierarchy lesson.
- Journaling file systems protect file-system-structure consistency across a crash; they don't substitute for application-level
fsync-based write durability.