Virtual File Systems: An Abstraction Over Real Storage

Building an Operating System Kernel: Concurrency, I/O & Synchronization

Chapter 8 · Virtual File Systems: An Abstraction Over Real Storage

Chapter 6 built one real device. A real system has many — RAM, disk, and often things that aren't devices at all but need to look like files anyway. A virtual file system is the layer that makes all of them answer to the same calls. This chapter builds a small, real one, then asks a genuine question: can this site's own Building a Database Engine project's real record-oriented storage be mounted underneath it?

Finding 1: One Interface, Two Genuinely Different Real Backends

class VFS: def mount(self, path, backend): self.mounts[path] = backend def read(self, path, offset, length): return self.mounts[path].read(offset, length) def write(self, path, offset, data): self.mounts[path].write(offset, data)
Verified directly — the same calls, two genuinely different real media
MemoryBackend wraps a real page table plus physical memory frames — reusing Course 1's own translate() pattern directly. DiskBackend is a completely separate byte array standing in for disk sectors. The exact same vfs.read()/vfs.write() calls correctly reach both — b'HELLO FROM RAM' and b'HELLO FROM DISK', each round-tripped correctly through its own path. Calling code never needed to know which backend served which.

Finding 2: Real Mount-Point Isolation

Verified directly — identical offsets on different mounts never collide
Writing b'AAAA' to /a at offset 0 and b'BBBB' to /b at the same offset 0 never collides — each mount resolves that offset through its own, completely independent page table. The VFS's own path-based routing is what keeps genuinely separate storage genuinely separate.

Finding 3: A Genuine Investigation — Does a HeapFile Mount Cleanly?

Building a Database Engine's own storage is fundamentally record-oriented: insert_record(data) → record_id, get_record(record_id) → data. A real, minimal version of that same interface, put directly to the test:

Verified directly — a genuine, confirmed incompatibility, not assumed
Mounting MinimalHeapFile directly under the VFS and calling read() raises: 'MinimalHeapFile' object has no attribute 'read'. Its own real interface has no byte-offset concept at all — record placement is chosen by the heap file itself, and records vary in length. "Offset 10" means nothing to a store with no stable, caller-addressable byte space in the first place.

Finding 4: A Real, Working Adapter Bridges the Two Interfaces

class HeapFileByteAdapter: # treats every record, concatenated in insertion order, # as one virtual byte stream -- deliberately READ-ONLY def read(self, offset, length): out = bytearray(); remaining = length; pos = offset for start, end, record_id in self._offset_index(): if pos >= end: continue record_data = self.heap.get_record(record_id) chunk = record_data[pos - start : pos - start + remaining] out.extend(chunk); pos += len(chunk); remaining -= len(chunk) if remaining <= 0: break return bytes(out)
Verified directly — a real read spanning two records' own boundary, reconstructed byte-for-byte
Mounted under the VFS, a 10-byte read starting mid-way through the first record and ending mid-way through the second reconstructs exactly the expected cross-record slice. write() is deliberately unsupported and raises NotImplementedError — an honest scope limit, not a silent failure: records are inserted, never addressed by a caller-chosen byte offset. Mounting a genuinely different kind of storage under a uniform interface was possible — but it needed a real adapter, not a direct mount.

Where This Connects

This chapter's findingWhat it connects to
MemoryBackend's own translate() logicCourse 1 Chapter 6's own Syscall class — the identical page-table-translation pattern, reused directly
A genuine interface mismatch found by actually trying itoskernel1's own capstone pick_next() mismatch — both real incompatibilities surfaced only by attempting the real integration, not by reasoning about it in the abstract
HeapFile's own record-oriented interfaceBuilding a Database Engine's own Storage & Query Fundamentals Chapter 3 — the real course this adapter connects to
A deliberately read-only adapterChapter 5's own MessageQueue — both chapters scope an abstraction honestly rather than faking support for an operation that can't be done correctly

Hands-On Exercises

Exercise 1

Read from a path that was never mounted. Confirm it fails with a clear, immediate error rather than silently returning empty data, and explain why that distinction matters.

📄 View solution
Exercise 2

Mount a second, different backend at a path that's already mounted. Confirm what happens to the original backend's own data, and explain why this is a real, honest gap worth knowing about.

📄 View solution
Exercise 3

Read a byte range through the HeapFileByteAdapter that falls entirely within a single record, rather than spanning two. Confirm it's handled correctly by the same unmodified read() method Finding 4 used for the cross-record case.

📄 View solution

Chapter 8 Quick Reference

  • VFS: one uniform read(path, offset, length)/write(path, offset, data) interface, routing by mounted path
  • Verified Finding 1: the same calls correctly reach two genuinely different real storage media
  • Verified Finding 2: mount-point routing keeps identical offsets on different paths genuinely isolated
  • Verified Finding 3: a record-oriented HeapFile genuinely cannot mount directly — a real, confirmed interface incompatibility
  • Verified Finding 4: a real, working (deliberately read-only) adapter bridges the two interfaces, verified across a real cross-record read
  • Golden rule: a uniform interface doesn't mean every backend fits it for free — some genuinely need an adapter, and knowing which is a real, testable question, not a guess
  • Next chapter: System Calls in Practice — a small syscall library tying process, memory, scheduling, and I/O together