Skip to content

The VFS

A VFS (Virtual Filesystem, sometimes Virtual Filesystem Switch) is the layer that lets a kernel expose a single, uniform file interface (open, read, write, directory traversal) regardless of what filesystem format actually backs a given file. Without it, every piece of code that touches files would need separate logic for every filesystem type it might encounter; with it, that logic is written once, against the VFS’s own abstractions, and each filesystem implementation is responsible only for translating between its own on-disk format and those abstractions.

A kernel that supports more than one filesystem format (FAT and ext2, say) faces a choice: either every piece of code that opens or reads a file needs to know which filesystem format it’s dealing with and branch accordingly, or a common interface absorbs that difference once, at the boundary between generic file-handling code and filesystem-specific code. The VFS is that boundary. Code above it, a program calling open, or the kernel’s own code loading an executable, only ever interacts with VFS objects; code below it, one implementation per supported filesystem format, is responsible for populating those objects correctly from whatever the underlying format actually stores.

Four objects, closely mirroring the traditional Unix VFS design, appear in essentially every implementation of this pattern, each representing a different level of the filesystem hierarchy.

A superblock represents an entire mounted filesystem instance: one exists per mounted volume, holding filesystem-wide information (total size, block size, a pointer to the root directory’s inode) and a set of operations specific to that filesystem type for tasks like allocating a new inode or writing filesystem-wide metadata back to disk.

An inode represents a single file or directory’s metadata (size, permissions, timestamps, and, critically, where its actual data lives on the underlying storage), independent of what that file is currently named or where it currently sits in a directory hierarchy, since the same underlying file can be reachable through more than one directory entry (a hard link) or through none at all (a file kept open after being unlinked).

A dentry (directory entry) represents a name-to-inode mapping within a specific directory (the fact that the name "passwd" inside a particular directory currently refers to a particular inode), and is what directory traversal actually walks through, since resolving a path like /etc/passwd means following a chain of dentries, each resolving one path component to the inode (and, if that inode is itself a directory, the further dentries) needed to continue.

A file object (sometimes called a file descriptor’s underlying kernel structure, distinct from the small integer a process sees) represents an open instance of a file, tracking a current read/write offset and access mode, separate from the inode itself, since two processes can have the same file open simultaneously with entirely independent offsets into it.

Each of these objects carries a set of function pointers (an operations table) populated by whichever filesystem implementation owns the underlying inode or superblock, and generic VFS code calls through these pointers rather than containing any filesystem-specific logic itself.

struct inode_operations {
int (*read)(struct inode *inode, void *buf, size_t count, off_t offset);
int (*write)(struct inode *inode, const void *buf, size_t count, off_t offset);
struct inode *(*lookup)(struct inode *dir, const char *name);
};
// generic VFS code, with no knowledge of which filesystem "inode" belongs to
int vfs_read(struct inode *inode, void *buf, size_t count, off_t offset) {
return inode->ops->read(inode, buf, count, offset);
}

A FAT implementation’s read walks the FAT cluster chain described allocation-table-style; an ext2 implementation’s read walks a block-pointer tree instead. This is entirely different logic, invisible to anything calling vfs_read, which only ever sees a function pointer it invokes uniformly regardless of which implementation is actually behind it.

Mounting is the operation that attaches one filesystem’s root directory at a specific point within another, already-mounted filesystem’s directory hierarchy: the mechanism that lets a single, unified directory tree span multiple underlying filesystems and even multiple underlying storage devices without a program navigating it needing to know where one filesystem ends and another begins. At the VFS level, mounting associates a superblock (created by initializing the filesystem-specific driver for the storage being mounted) with a specific dentry in the already-mounted hierarchy, such that path resolution transparently crosses into the newly mounted filesystem’s own dentry and inode structures once it reaches that point.

A minimal VFS implementation for a hobby kernel does not need every abstraction described above from the outset: a kernel supporting exactly one filesystem format, with no intention of supporting a second, gains comparatively little from a full VFS indirection layer and can reasonably call filesystem-specific functions directly. The VFS pattern earns its complexity specifically once a second filesystem format needs supporting, at which point retrofitting the abstraction onto code that was written assuming only one format tends to be considerably more work than designing around the indirection from the start, even before a second filesystem actually exists. This is a case where anticipating a near-certain future requirement is more defensible than premature generalization usually is.

  1. ^ S. R. Kleiman, “Vnodes: An Architecture for Multiple File System Types in Sun UNIX,” USENIX Summer 1986 (the original paper describing the vnode/VFS architecture this pattern derives from).
  • FAT: one concrete filesystem implementation that would sit behind this layer.
  • ext2: a second, structurally different filesystem implementation behind the same VFS abstractions.
  • Journaling: a technique that changes crash recovery without changing anything this layer’s callers see.
  • ISO 9660: a third, read-only filesystem implementation behind the same abstractions, structured very differently from either ext2 or FAT.
  • Physical Memory: the frame allocator disk buffers ultimately draw memory from.
  • Buffer Cache: the layer directly underneath this one, holding recently read or written disk blocks in memory.
  • NVMe: a block-level driver whose queue pairs this layer’s disk buffers eventually flow through.
  • initrd: the temporary root this layer’s mount abstraction pivots away from once the real filesystem is available.
  • Partition Tables, MBR and GPT: what locates a specific volume before this layer ever sees it.