At 11:47 PM on a weeknight, a UPI app on a phone in Pune sends a payment instruction: debit ₹500 from account A, credit ₹500 to account B. Somewhere in the chain of banks and NPCI switches, a server process opens a transaction-log file and starts writing the update. At 11:47:00.3, before the write finishes, the machine loses power — a tripped breaker, a kernel panic, a rack failure, it does not matter which. When the machine reboots, the account balances must be exactly what they were before the debit, or exactly what they should be after both halves of the transfer completed. They must never show ₹500 gone from A with nothing credited to B. No banking system in India tolerates that outcome, and yet the raw hardware underneath every one of these machines offers no such guarantee at all — a disk write that is interrupted mid-sector can leave garbage behind. The component responsible for closing that gap between "unreliable block device" and "money that cannot vanish" is the file system. This chapter builds that component from first principles: what a file actually is once you strip away the folder icon, how a file system turns a flat address space of disk blocks into named, growable, crash-safe objects, and why the guarantee your UPI transaction relies on is a specific, analyzable mechanism called journaling — not a vague promise.
What a disk actually offers, and what a file system must build on top of it
A hard disk or SSD, at the hardware interface level, exposes almost nothing resembling a "file." It exposes a flat array of fixed-size blocks (commonly 512 bytes or 4096 bytes each), addressable only by a numeric block address, and it supports exactly two operations: read block N, write block N. There are no names, no directories, no notion of "this block belongs to that document," and no built-in guarantee about what happens if power is cut mid-write. Every property you rely on when you double-click a file — a human-readable name, the ability to grow the file by appending, the fact that two different files don't silently overwrite each other's bytes, the fact that a half-written file doesn't corrupt an unrelated file — is manufactured entirely in software by the file system. This is exactly the same move you have already seen elsewhere in CS: a hash table manufactures O(1) average lookup on top of an array that only supports lookup-by-index, and a virtual memory system manufactures the illusion of a private, contiguous address space on top of physical RAM. A file system manufactures the illusion of named, variable-length, safely-growable objects on top of a flat, fixed-size, unsafe block array.
To do this, a file system reserves a portion of the disk for its own bookkeeping and leaves the rest for data. Four structures recur, with variations, across almost every file system you will encounter (FAT32, ext2/ext3/ext4, NTFS, APFS):
The superblock is a fixed block, usually at a known offset, holding global metadata: total block count, block size, free block count, and pointers to the other structures. It is read once at mount time and tells the OS how to interpret everything else on the volume.
The inode (in Unix-family terminology; NTFS calls the analogous structure an MFT record) is a per-file metadata record: owner, permissions, size, timestamps, and — critically — the list of which disk blocks hold this file's actual bytes. The inode does not store the file's name.
The directory is itself just a file, but with a constrained structure: a list of (name, inode number) pairs. This indirection is why the same file's contents can appear under two different names (a hard link) — both directory entries point at the same inode.
Free space tracking — most commonly a bitmap with one bit per block, 1 for allocated, 0 for free — lets the allocator find room for new data quickly without scanning every inode on the disk.
Worked example: how many bytes can one inode address?
The classic Unix inode design (used by ext2 and, conceptually, still visible inside ext3) does not store an arbitrarily long list of block numbers directly in the inode, because the inode itself is a small fixed-size record and a very large file might need millions of block pointers. Instead it uses a layered indirection scheme. Assume, concretely: block size = 4096 bytes, and each block pointer (block address) occupies 4 bytes. The inode holds:
- 12 direct pointers — each points straight at one 4096-byte data block.
- 1 singly indirect pointer — points at one block that is entirely filled with more pointers, each of which points at a data block.
- 1 doubly indirect pointer — points at a block of pointers, each of which points at another block of pointers, each of which points at a data block.
- 1 triply indirect pointer — one more layer of indirection deep.
First derive how many pointers fit in one block: 4096 bytes ÷ 4 bytes per pointer = 1024 pointers per block. Call this P = 1024.
Now count addressable data blocks level by level:
Direct: 12 blocks.
Singly indirect: 1 indirect block × P pointers = 1024 blocks.
Doubly indirect: P pointer-blocks, each holding P pointers to data = P × P = 1024 × 1024 = 1,048,576 blocks.
Triply indirect: P × P × P = 1024³ = 1,073,741,824 blocks.
Total addressable blocks = 12 + 1024 + 1,048,576 + 1,073,741,824 = 1,074,791,436 blocks.
Multiply by the block size to get the maximum file size: 1,074,791,436 × 4096 bytes = 4,402,345,721,856 bytes. Divide by 1024⁴ (1 TiB = 1,099,511,627,776 bytes) to convert: 4,402,345,721,856 ÷ 1,099,511,627,776 ≈ 4.004 TiB. So a 4 KiB-block inode with this layout can, in principle, address just over 4 TiB in a single file — and notice where that number comes from: the triply-indirect term (1024³ blocks) accounts for 1,073,741,824 of the 1,074,791,436 total blocks, i.e. 99.9% of the reachable space. The direct pointers exist purely to make small files fast (one pointer hop instead of three), while the deep indirection exists purely to make large files possible at all. Real ext2 volumes carry additional 32-bit limits elsewhere in the superblock that can push the practical ceiling below this theoretical pointer-arithmetic bound — but the mechanism above, not a magic constant, is what determines the limit, and it is exactly this three-level indirection that later designs (ext4's extents, for instance) replaced because walking three pointer hops to read the tail of a huge file is slow.
Allocating space: a first-fit bitmap allocator, traced
When a file needs to grow, the allocator must find free blocks and mark them used in the bitmap. A simple, common strategy is first-fit: scan the bitmap from the start, and hand out the first run of free blocks long enough to satisfy the request.
def allocate_blocks(bitmap, blocks_needed):
run_start = -1
run_length = 0
for i, used in enumerate(bitmap):
if not used:
if run_start == -1:
run_start = i
run_length += 1
if run_length == blocks_needed:
for j in range(run_start, run_start + blocks_needed):
bitmap[j] = 1
return run_start
else:
run_start = -1
run_length = 0
return -1 # no contiguous run large enough
Trace it on bitmap = [1, 1, 0, 0, 0, 1, 0, 0, 0, 0] with blocks_needed = 3. i=0: bit is 1 (used) → reset. i=1: bit is 1 → reset. i=2: bit is 0 → run_start=2, run_length=1. i=3: bit is 0 → run_length=2. i=4: bit is 0 → run_length=3, which equals blocks_needed → mark indices 2, 3, 4 as used and return 2. The function returns 2, and the bitmap is now [1, 1, 1, 1, 1, 1, 0, 0, 0, 0]. Every step in that trace is deterministic given the code above — there is no hidden state. Notice the allocator never looked past index 4, even though there might be a longer free run further along; first-fit optimizes for allocation speed, not for minimizing long-term fragmentation, which is precisely why real file systems periodically need defragmentation or use smarter strategies (best-fit, or grouping allocations into extents) as they age.
Crash consistency: why the UPI transaction survives the power cut
Return to the opening scenario. The naive way to update the transaction-log file is: overwrite the relevant bytes in place, on disk, directly. If the power dies mid-write, the block on disk can end up in an mixed state — part old data, part new data, possibly with a corrupted checksum the application never expected to see. This is the actual mechanism, not a hand-wave: a single 4096-byte block write is not atomic with respect to sudden power loss: the disk controller may have physically written 1500 of those 4096 bytes when power cut, leaving the other 2596 bytes holding whatever was there before, or garbage.
Journaling file systems (ext3, ext4, NTFS's transaction log, and analogous mechanisms elsewhere) close this gap with a write-ahead log, and the sequence matters precisely: (1) write a description of the intended change — "update these blocks to these new values" — into a separate journal area on disk, tagged as an in-progress transaction; (2) write a commit record into the journal marking the transaction complete — the change is now durable, even though the real, final location has not been touched yet; (3) write the actual data to its real location (the "checkpoint"), which can happen lazily, at any point after commit, even across a reboot. Recovery after a crash is a simple rule applied to the journal alone: scan it for transactions; any transaction with a commit record is replayed (its logged changes are (re)applied to the real location to guarantee they took effect, whether or not the checkpoint had already run before the crash); any transaction without a commit record is discarded entirely, as if it never started — safe to discard precisely because step 3 never touches the real location until step 2 has already made the change durable.
Apply that rule to the three crash points that could occur during the ₹500 transfer. Crash during step 1 (writing the intent record) or crash between steps 1 and 2: no commit record exists in the journal, so on reboot the whole transaction is thrown away — both balances are exactly as they were before the transfer was attempted, because the real, final location was never touched, and the bank's higher-level retry logic can safely resend the instruction. Crash between step 2 and step 3 (commit flushed, real-location write not yet done): the commit record already exists, so recovery replays the transaction — it (re)writes the data to its real location straight from the journal's durable copy, bringing both balances to exactly the post-transfer state the commit already promised. Crash after step 3: the commit record exists and the real location is already updated, so recovery replays the same write a second time, which changes nothing — this is exactly why the replay step must be idempotent. In every one of these three cases, the outcome is binary — fully applied or fully discarded — and the "half-applied" state that would corrupt a ledger is structurally impossible to observe after recovery completes, precisely because recovery only ever consults the commit record, never the partial progress of the checkpoint write itself.
Common misconception: "deleting a file erases its data"
A student running rm txnlog.dat, or dragging a file to the Recycle Bin and emptying it, typically assumes the bytes are gone from the disk platter or flash cells at that instant. They are not. The unlink() operation that deletion ultimately calls does exactly two things: it removes the (name → inode number) entry from the directory, and it decrements the inode's link count. Only when that link count reaches zero and no process still holds the file open does the file system reclaim the inode and flip the corresponding bits back to 0 in the free-space bitmap — and even then, "reclaiming" means the blocks are now eligible to be overwritten by future allocations, not that anything has actively zeroed their contents. Until some later write happens to land on those same blocks, the old bytes are still physically present and recoverable with forensic tools that read raw blocks and cross-reference the freed bitmap positions. This is precisely why hard links exist (two directory entries, one inode, one link count shared between them — deleting one name leaves the data reachable through the other) and precisely why secure-deletion tools like shred exist as a separate, explicit step: ordinary deletion is a metadata operation, not a data-erasure operation, and treating them as the same thing is the misconception to unlearn here.
Active recall
Attempt each question before reading its answer.
Q1. A file system uses 8 KiB (8192-byte) blocks and 4-byte block pointers, with 10 direct pointers, 1 singly indirect pointer, and 1 doubly indirect pointer in each inode (no triple indirect). Compute the maximum file size.
Q2. Why can appending a single byte to a very large file (one already using triple-indirect blocks) require more disk I/O than appending a single byte to a small file that only uses direct pointers?
Q3. Run the first-fit allocator from this chapter, by hand, on bitmap = [0, 0, 1, 1, 0, 0, 0, 1, 0] requesting 2 blocks. Which index does it return, and why might that be a worse choice than an allocator that looked ahead to the run at indices 4–6?
Q4. In the three-step journaling protocol described above, suppose the power fails exactly between step 2 (commit record written) and step 3 (data written to its real location). Does the file system still guarantee consistency? Explain what recovery does.
Q5. A classmate says: "When I delete a file, its blocks are erased instantly, so free space appears instantly and the data is gone forever." Identify precisely what part of this claim is wrong.
Worked answers.
A1. Pointers per block = 8192 ÷ 4 = 2048. Direct: 10 × 8192 = 81,920 bytes. Singly indirect: 2048 × 8192 = 16,777,216 bytes. Doubly indirect: 2048 × 2048 × 8192 = 4,194,304 × 8192 = 34,359,738,368 bytes. Total = 81,920 + 16,777,216 + 34,359,738,368 = 34,376,597,504 bytes ≈ 32.02 GiB (the doubly-indirect term, exactly 32 GiB since 2048² × 8192 = 2²² × 2¹³ = 2³⁵ bytes and 2³⁵ ÷ 2³⁰ = 32, dominates the total).
A2. Reaching a block addressed through triple indirection requires the file system to read (or, when appending past the end of an allocated region, allocate and write) up to three separate pointer blocks — the triple-indirect block, the double-indirect block it points to, and the single-indirect block that finally points at the data block — before the actual data write happens. A file using only direct pointers reaches its data with a single pointer lookup already sitting in the inode itself. More indirection layers between the inode and the byte being written means more block reads/writes per operation.
A3. i=0: 0 → run_start=0, len=1. i=1: 0 → len=2, equals blocks_needed → allocate indices 0–1, return 0. First-fit stops at the very first sufficient run it finds, even though indices 4–6 form a longer run of 3 free blocks that a later, larger request might have used more efficiently; first-fit trades allocation speed for a higher chance of leaving small, awkward free fragments scattered across the disk.
A4. Yes, consistency still holds — but by replay, not by discard. The commit record was already written to the journal in step 2, so on recovery the file system finds this transaction complete and replays it: it (re)writes the data to its real location using the journal's durable copy, exactly as step 3 would have done if the crash hadn't interrupted it. The file system state after recovery is identical to the state the transaction was committing to, because the journal — not the real location — is what recovery trusts; the real location catching up is just a formality the crash merely delayed.
A5. The error is in "erased instantly" and "gone forever." Deletion (unlink()) only removes the directory's (name → inode) entry and decrements the inode's link count; the data blocks are marked free in the bitmap only once the link count and open-file count both reach zero, and marking a block free means it is merely eligible for future reuse, not actively zeroed. The bytes remain physically present and recoverable until a later allocation happens to overwrite that same block — which is why hard links can keep data reachable after one name is deleted, and why secure deletion requires a separate explicit overwrite step.
Think About It
Think about this: How would you explain file systems: persistent storage abstraction to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.
Practice Exercises
Now it is time to practice! Complete these challenges to solidify your understanding:
- Exercise 1: Write a short program that demonstrates the core concept from this chapter. Test it with at least 3 different inputs.
- Exercise 2: Find a real-world example where file systems: persistent storage abstraction is used in an Indian company (like TCS, Infosys, Flipkart, or ISRO). Write a paragraph explaining the connection.
- Exercise 3: Create a mind-map connecting file systems: persistent storage abstraction to at least 3 other topics you have studied.
Key Takeaways — Summary and Recap
Let us recap what we covered: the core ideas behind file systems: persistent storage abstraction, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.