Filesystems Explained

September 15, 2026 • 5 min read

Filesystems Explained
Table of Contents

Part of the series:Operating Systems

The virtual memory post ended with a boundary: RAM forgets and swap is scratch, so anything worth keeping needs a durable home. Disks hold blocks; humans want files with names, folders, and permissions that survive power loss. The bridge is the file system1. This post opens it up. We will look at what a file really is, how directories map names to files, how the system survives crashes mid-write, who is allowed to do what, and the path a single open takes from name to bytes.

Files and inodes: names point at metadata

A file is two things: a name users see, and numbered blocks holding bytes. Between them sits the inode2 (index node): one record per file holding everything except the name itself (size, owner, permissions, timestamps, and pointers to the data blocks). Directory entries map names to inode numbers; the inode maps onward to blocks.

directory entry              inode 4217                 data blocks
/home/you/notes.txt ──▶  no. 4217 ──▶ size, owner, ──▶ block 91
                                         mode, times,     block 92
                                         pointers…        block 118

Separating names from metadata buys two features for free. Hard links are two names pointing at one inode: same file, two doors, edits visible through both, blocks freed only when the last name (and open handle) goes away. And renaming is cheap: it rewrites a directory entry, never moves bytes.

Large files need many pointers, so inodes layer them: a few direct pointers for small files, then indirect blocks (a block full of pointers), double-indirect, triple-indirect for the enormous. Small files cost one inode; huge files scale without changing the record’s size.

Directories: tables all the way down

A directory3 is a file with a special format: a table mapping names to inode numbers. Opening /home/you/notes.txt walks the path component by component, starting from the root inode: look up home in /, get its inode, look up you in it, get that inode, look up notes.txt, arrive at inode 4217. Each step is a lookup in a table; the kernel caches recent translations (the dentry cache) so repeated walks stay fast.

/ (inode 2)         /home (inode 88)      /home/you (inode 301)
home → 88     ──▶   you → 301       ──▶   notes.txt → 4217

Mounting grafts one filesystem’s tree onto another’s directory: the mounted root covers the mount point, and paths cross the boundary invisibly. Your program sees one tree; the kernel sees a table of mounted filesystems stitched together.

Crash consistency: the write that was half done

Power can die between any two disk writes, leaving structures half-updated: a file’s size bumped but its blocks unrecorded, an inode allocated but unlinked. The classic answer is journaling4: write the intention to a sequential log first, then apply to the real structures, then mark the log entry done. After a crash, the kernel replays completed intentions and discards partial ones: every operation either happened or did not, never half.

1. journal: "create notes.txt → inode 4217, blocks 91–92"  (sequential, fast)
2. apply:   write inode, directory entry, bitmaps           (in place)
3. journal: mark done, reclaim log space
   ── crash anywhere ──▶ replay redoes step 2 or drops it

Alternatives trade differently. Copy-on-write filesystems never overwrite live data: they write new versions elsewhere and flip one pointer atomically, so the old tree is always intact (snapshots fall out naturally). fsck-style consistency checking scans and repairs after the fact instead of preventing damage. All three serve one contract: a crash may lose the last seconds, but never the filesystem’s coherence.

Permissions: who may do what

Every inode carries an owner, a group, and mode bits5: read, write, execute for user, group, and others (rwxr-xr-- reads as owner-all, group-read-execute, other-read). Directories reuse the bits with shifted meaning: read lists entries, write creates and deletes, execute traverses through. The kernel checks on every open: match the process’s user and groups against the bits, allow or deny with EACCES.

-rw-r--r--  you  devs   notes.txt   (owner rw, group r, other r)
drwxr-x---  you  devs   project/    (group members enter, others barred)

Setuid, the set-group-ID bit, and the sticky bit extend the scheme (run-as-owner executables, shared-directory protections), and modern systems layer access-control lists for per-user entries beyond the three classes. Permissions are checked at open time against names; open file descriptors keep working even if the name is later unlinked, which is how safe temporary files and atomic replacement (write temp, rename over) work.

The big picture

Blocks become files through inodes (metadata without names), names resolve through directories (tables mapping to inodes, walked component by component), journaling makes updates atomic across crashes, and mode bits gate every open by user and group. One open call walks, checks, and translates; reads and writes then stream through the page cache to disk in the background.

But files are only half of I/O. Keyboards, screens, and sockets are not files on disk, yet programs read and write them through the same syscalls, blocking, waking, and moving bytes down to hardware that answers in interrupts. Unifying that path, and the syscalls that drive it, is the next post’s subject.

Footnotes

  1. File system - Wikipedia

  2. Inode - Wikipedia

  3. Directory (computing) - Wikipedia

  4. Journaling file system - Wikipedia

  5. File-system permissions - Wikipedia