# The VFS: How 'Everything Is a File' Actually Works

> The same read() works on ext4 files, /proc fictions, sockets, and pipes because Linux routes every file operation through one dispatch layer. The VFS is the kernel's biggest interface — and its object model is worth stealing.

URL: https://harshith.in/blog/vfs-everything-is-a-file/
Author: Harshith Sunku (Staff Engineer) — https://harshith.in/about/
Tags: linux, kernel, filesystems

`cat` works on `/etc/hostname` (bytes on an ext4 disk), `/proc/cpuinfo` (a
string the kernel invents at open time), `/dev/urandom` (a CSPRNG), and a
FUSE-mounted S3 bucket (userspace daemon speaking HTTP). Same binary, same
`read()` syscall, radically different machinery underneath. The layer that
makes this work — the Virtual File System — is the oldest and most successful
polymorphism story in the kernel, and understanding its four objects turns a
lot of Linux mysteries (hard links, the dentry cache, "why is deleting still
using disk space") into obvious consequences.

## Dispatch: vtables in C

Every open file reaches the VFS through a `struct file`, which carries a
pointer to a `file_operations` table — functions the backing implementation
supplied:

```c
const struct file_operations ext4_file_operations = {
    .read_iter  = ext4_file_read_iter,
    .write_iter = ext4_file_write_iter,
    .mmap       = ext4_file_mmap,
    .fsync      = ext4_sync_file,
    ...
};
```

`read(fd, ...)` resolves to `file->f_op->read_iter(...)` — that's the whole
trick. ext4 supplies functions that consult the page cache and disk; procfs
supplies ones that `sprintf` kernel state; a socket's table routes to the
networking stack. "Everything is a file" doesn't mean everything *is* one — it
means everything agreed to the same vtable. (It's also why the abstraction
frays exactly where the vtable does: `lseek` on a socket fails because its
entry says so, and ioctl became the junk drawer for everything that didn't fit
the interface.)

## The Object Model

Four structs carry the state, and the distinctions between them answer real
questions:

- **inode** — the file itself: size, permissions, timestamps, where the data
  lives. Crucially, *no name*. One per file, however many names it has.
- **dentry** — a name→inode link, one path component each. Hard links are just
  multiple dentries pointing at one inode, which instantly explains the rules:
  `rm` deletes a *name*; the inode (and its blocks) survive until the last
  name **and the last open file** drop — hence the "deleted 40 GB log but the
  disk is still full until the daemon restarts" classic (`lsof +L1` finds the
  culprit), and hence the idiom of open-then-unlink temp files.
- **file** — one *opening* of an inode: offset, flags, the f_op table. Two
  processes reading the same file have two file objects, one inode. A `dup`ed
  or forked fd *shares* one file object — which is why parent and child
  advance each other's file offset, a genuinely surprising fact the first
  time it bites.
- **superblock** — one mounted filesystem: block size, root dentry, and its
  own operations table.

## The Cache That Makes Paths Cheap

Resolving `/home/harshith/src/main.c` means walking four components, checking
permissions at each, and would mean four rounds of disk metadata — so the VFS
caches aggressively. The **dentry cache** memoizes name→inode lookups
(including *negative* entries: "no such file" is cached too, which is why a
hot ENOENT loop costs almost nothing); the **inode cache** keeps the metadata
behind them. Both show up as gigabytes of "used" memory in `slabtop`
(`dentry`, `ext4_inode_cache`) that free(1) counts as reclaimable — the source
of eternal "Linux ate my RAM" confusion. A path walk that hits dcache all the
way (the RCU-protected fast path) touches no locks and no disk; this cache is
the single biggest reason `stat()`-heavy workloads (builds, `git status`,
shell completion) are fast on Linux.

## Files Without Disks

The payoff of the design is how cheap new "filesystems" are. procfs and sysfs
are pure fiction — reading `/proc/self/status` runs a formatter over the
task_struct, nothing is stored anywhere. tmpfs is the page cache wearing a
filesystem hat. **FUSE** extends the vtable across the kernel boundary: each
operation becomes a message to a userspace daemon, which is how sshfs,
s3fs-style mounts and every cloud-drive client implement a filesystem without
writing kernel code — for the price of context switches per op (grep on sshfs
teaches the price viscerally). And because the interface is uniform, tools
compose for free: `strace`, `tar`, your editor — none of them know or care
which vtable they're exercising. That's the API-design lesson worth stealing:
a small, uniform interface with a well-defined escape hatch beats a rich
specialized one, even when some members fake it.

## Takeaways

- The VFS is vtable dispatch: read() → file->f_op->read_iter, supplied by
  ext4, procfs, sockets, FUSE — same interface, any backend.
- inode = the file, dentry = a name for it, file = an opening of it; hard
  links, deleted-but-open files, and shared offsets after fork all fall out.
- dcache/icache (including negative entries) make path resolution nearly free
  and masquerade as "used" RAM in slabtop.
- /proc and sysfs store nothing; FUSE ships the vtable to userspace — files
  are an interface, not a storage format.
- When file semantics surprise you, ask which object holds the state: the
  answer disambiguates almost every case.
