The miracle you already take for granted
Back in the file-I/O rung you learned to open, read, write, and close a file through a file descriptor, and you met the slogan that in Unix everything is a file. Look closely at how strange that is. The same `read(fd, buf, n)` call works whether `fd` refers to a text file on your SSD, a video on a USB stick formatted FAT32, a file living on a server three countries away over NFS, or a fake file like /proc/cpuinfo that no disk ever stored. You never tell `read()` which one it is. Somehow a single call reaches into wildly different machinery and gets bytes back.
That machinery is the Virtual Filesystem, usually shortened to VFS. It is the part of the kernel that sits directly behind your system calls and in front of every concrete filesystem. When your `read()` crosses into the kernel, it does not land in ext4 or FAT32 code; it lands in the VFS, and the VFS dispatches it to whichever filesystem actually owns that file. The virtual filesystem is the reason the file-I/O API you already know is so small and so uniform: there is exactly one set of calls, and the VFS is the switchboard that routes each one to the right place.
Notice the shape of the problem the VFS solves, because you have solved it yourself in user space. You want one caller to drive many different implementations behind a fixed interface — the same problem a function pointer solves in C, the same problem a virtual method table solves in C++, the same problem a trait object solves in Rust. The VFS is simply that idea applied inside the kernel: polymorphism for filesystems. The rest of this guide is really about how the kernel builds that polymorphism out of plain C structs, since the kernel has no language-level objects to lean on.
Polymorphism with no objects: structs of function pointers
Here is the whole trick in one sentence: a file in the kernel carries a pointer to a table of operations, and `read()` just calls through that table. The table is a struct whose members are function pointers — in Linux it is literally called the file_operations struct — with one slot per operation: a `read` slot, a `write` slot, an `open` slot, a `release` slot for close, and so on. Each filesystem fills that table with its own functions. ext4 puts ext4_file_read in the read slot; a character device driver puts its own routine there. The VFS never knows or cares which; it only knows the slot.
user space: read(fd, buf, n)
| (one system call, crosses into the kernel)
v
VFS dispatch: f = fd_table[fd]; /* the open file object */
f->f_op->read(f, buf, n); /* call THROUGH the table */
|
+---------------+----------------+----------------+
v v v v
ext4_file_read fat_file_read nfs_file_read proc_cpuinfo_read
(reads a disk (reads FAT (sends an RPC (formats a string
block) cluster) over the net) on the fly)
struct file_operations { /* the "vtable" for a file */
ssize_t (*read) (struct file *, char *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char *, size_t, loff_t *);
int (*open) (struct inode *, struct file *);
int (*release)(struct inode *, struct file *); /* close */
/* ... more slots ... */
};This is exactly a C++ vtable built by hand, and it is worth seeing the connection plainly. In C++ the compiler hides the table of function pointers and the indirect call inside the language; the kernel, written in C, writes that table out as an ordinary struct and does the indirect call itself with `f->f_op->read(...)`. There is nothing magic about "object-oriented kernels": it is function pointers all the way down. When you later read kernel source and see `f_op`, `i_op`, or `s_op`, you are looking at one of these operation tables hanging off a file, an inode, or a superblock.
The four objects the VFS thinks in
The VFS does not model a filesystem as one big blob; it factors it into four kinds of in-memory object, each with its own operation table. The superblock represents a whole mounted filesystem — one per mount, holding global facts like the block size and the root of the tree. The inode represents one file's metadata and identity: its size, owner, permissions, timestamps, and crucially the pointers to where its data lives. The dentry (directory entry) represents one name in the tree and links a name to an inode. And the file object represents one open instance — a particular descriptor with its own file offset and access mode.
Keeping the inode and the dentry separate is the design choice that makes Unix names work the way they do, and it is worth pausing on. The name is not the file. A dentry holds a name and points at an inode; several dentries can point at the same inode, and that is precisely what a hard link is — two names, one underlying file, one inode. It is also why deleting a name is called `unlink()`: you are removing a dentry, and the inode itself only truly disappears when its link count drops to zero. The file object being separate from the inode is what lets two processes open the same file and each keep an independent offset — two file objects, one shared inode.
Each of these objects carries its own operations table, and they divide the labor cleanly. The superblock operations know how to allocate and write back inodes for the whole filesystem; the inode operations handle name-space changes on metadata — create, lookup, mkdir, rename, unlink; and the file operations, the table from the last section, handle content — read, write, seek, mmap. A good rule of thumb: if it changes the directory structure or a file's metadata, it goes through inode operations; if it moves bytes in and out of an already-open file, it goes through file operations.
Walking a path: how /home/ana/notes.txt becomes a file
Now we can answer the most basic question: when you call open("/home/ana/notes.txt", ...), how does a string of characters turn into an inode the kernel can read from? The answer is path resolution, a step-by-step walk down the tree. The kernel never treats the path as one lookup; it splits it on the slashes and resolves it one component at a time, because each component might cross a directory the previous one only just identified.
- Start at a root. An absolute path like /home/... starts at the filesystem root inode; a relative path starts at the process's current working directory. This starting inode becomes the "current directory" for the walk.
- Take the next component, here "home". Call the current directory inode's lookup operation, asking it: "do you contain a name 'home', and if so, which inode is it?" This is an inode operation, and it is where the concrete filesystem actually reads the directory.
- lookup returns the inode for 'home'. Make that the new current directory and repeat for 'ana', then for 'notes.txt'. Each step descends one level, reusing the result of the step before it.
- When the last component resolves, you have the target file's inode. The VFS builds a fresh file object, points its operations table at that inode's file operations, installs it in the process's descriptor table, and hands you back the small integer fd.
Doing a real disk lookup for every component of every path would be punishingly slow — busy systems resolve millions of paths a second, and the same directories like /usr/lib get walked constantly. So the VFS keeps a dentry cache (the "dcache") in memory: a hash table mapping a parent directory plus a name to the dentry it resolved to last time. On a cache hit the walk skips the on-disk lookup entirely. This cache is a pure performance layer in front of the real filesystem, and it is one of the hottest data structures in the whole kernel.
Where the abstraction pays off: mounts and fake files
The clearest proof that the VFS abstraction is real is that totally unrelated things plug into it as equals. Mounting grafts one filesystem's tree onto a directory of another: mount a USB stick at /mnt/usb and, from then on, when path resolution reaches the /mnt/usb dentry it finds it is a mount point and transparently hops to the root of the stick's filesystem. The walk continues into FAT32 without your `open()` ever knowing the ground changed under it. One namespace, many filesystems stitched together, and the seam is invisible — that seam is the whole payoff of having a uniform interface.
Even more striking are the files that are not files at all. A pseudo-filesystem like /proc or /sys exposes kernel data through the file interface without any backing storage. Read /proc/cpuinfo and its read operation simply formats a string on the fly from live kernel state — there is no block on any disk holding that text; it is generated the instant you ask. This is the everything-is-a-file slogan taken to its logical end: because reading a file just means "call the read slot of this object," anything that can fill in a read slot can pretend to be a file. The cat tool reading /proc/cpuinfo cannot tell, and does not need to.
What you carry into the rest of this rung
This guide deliberately stayed above the disk. We talked about inodes, dentries, and superblocks as the kernel's in-memory abstractions, and waved a hand at "pointers to where the data lives" without saying how those pointers are arranged on real storage. That on-disk reality is exactly where the next guides go. Guide 2 cracks open the inode and the directory and shows how extents and on-disk layout pack files onto blocks. Guide 3 asks what happens when the power fails mid-write, and how journaling keeps the filesystem consistent across a crash. Guide 4 visits copy-on-write designs like ZFS and Btrfs that take a radically different approach to the same problem.
Hold onto the central image, because every later guide hangs from it. There is a single, uniform interface at the top — the small set of calls you already know — and there are many concrete implementations at the bottom, each one a filled-in set of operation tables. The VFS is the dispatch layer between them, and everything from a USB stick to /proc to a network share is just another implementation that satisfies the contract. When guide 5 finally pins down what fsync() actually promises about durability, you will see that even that promise is, at bottom, one more operation in the file table — and that the honesty of the whole stack depends on each filesystem implementing it truthfully.