Skip to content
daniel@cosenza:~/blog
LinuxDeep Dive May 20, 2026 4 min read

The Linux Virtual File System: One Interface, Many Filesystems

How the VFS layer lets ext4, XFS, Btrfs, NFS, and procfs all answer to the same read/write/open calls.

A process calling read() on a file doesn’t know or care whether that file lives on an ext4 partition, an NFS share, or is actually /proc/cpuinfo, generated on demand by the kernel. That uniformity is the entire purpose of the Virtual File System (VFS): an abstraction layer that lets every concrete filesystem implementation answer to the same small set of operations, so userspace only ever needs to know one API.

The four core objects

The VFS is built around four object types, each represented by a kernel structure with a table of function pointers that a concrete filesystem fills in with its own implementation.

A superblock (struct super_block) represents a mounted filesystem instance as a whole — its size, block size, and a pointer to the operations that manage inodes on it. An inode (struct inode) represents a single file’s metadata — permissions, owner, size, timestamps — independent of its name (a file can have multiple names via hard links, but only one inode). A dentry (directory entry) represents a name in the directory hierarchy, mapping that name to an inode and forming the tree structure /, /home, /home/user are walked through. A file object (struct file) represents an open file — the thing holding a read/write offset, tied to a specific process’s file descriptor table.

open("/home/user/notes.txt")


   dentry: "notes.txt" ──► inode ──► superblock (ext4 instance)


   file object (per-process, tracks offset, flags)

Operation tables: the actual abstraction

Each of these structures carries a pointer to an operations tablesuper_operations, inode_operations, file_operations — a struct of function pointers that the concrete filesystem populates at mount time. When userspace calls read(), the kernel doesn’t contain a giant switch statement for every filesystem type; it simply calls file->f_op->read(), and which function that resolves to depends entirely on which filesystem the file lives on.

// simplified sketch of struct file_operations
struct file_operations {
    ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
    ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
    int (*open)(struct inode *, struct file *);
    ...
};

ext4’s read implementation walks its own on-disk extent trees; NFS’s read implementation makes a network RPC call; procfs’s read implementation generates text on the fly from live kernel state. The calling code in sys_read() is identical in all three cases — it just follows the function pointer.

Mounting: attaching a superblock to the namespace

mount is the operation that ties a filesystem’s superblock into the directory tree at a specific point, making its root dentry reachable from the mount point’s path:

mount -t ext4 /dev/sda1 /mnt/data
mount -t nfs 192.168.1.10:/export /mnt/nfs
cat /proc/mounts

Once mounted, path lookups that cross /mnt/data transparently switch from whatever filesystem’s dentries were there before to ext4’s, entirely invisibly to the process doing the lookup — the VFS resolves this “mount point crossing” as part of ordinary path walking.

Pseudo-filesystems: procfs and sysfs

Because the VFS abstraction only requires something behind the operation tables — not necessarily on-disk storage — the kernel also implements filesystems with no backing block device at all. /proc and /sys are the two most important: reading /proc/loadavg doesn’t read a “file” off any disk, it calls into a kernel function that formats live system state as text on demand.

cat /proc/meminfo
mount -t proc proc /proc

This is the underlying reason /proc and /sys “just work” with ordinary shell tools like cat, grep, and echo — they’re real files as far as the VFS and every userspace tool are concerned, just backed by kernel code instead of disk blocks.

The dentry and inode caches

Because path lookups (resolving /home/user/notes.txt into an inode) would be prohibitively expensive if every component required a fresh disk read, the VFS maintains a dentry cache (dcache) and inode cache, keeping recently-used name-to-inode mappings in memory:

# Kernel slab statistics show cache sizes directly
grep -A1 dentry /proc/slabinfo

Under memory pressure, these caches are reclaimed like any other reclaimable kernel memory — which is why free on a Linux system often shows a large “cached” figure that isn’t actually unavailable, just memory the VFS is opportunistically holding onto until something else needs it.

Why this design has aged well

The VFS’s operation-table abstraction is precisely why Linux gained Btrfs, then later added persistent-memory filesystem support, containerized overlay filesystems (overlayfs), and FUSE (filesystems implemented entirely in userspace) without requiring any change to read(), write(), or open() themselves. Every new filesystem is just a new set of functions behind the same four operation tables — the interface userspace and the rest of the kernel depend on hasn’t needed to change since it was designed.