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 duped 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.