Linux Filesystem Architecture: VFS Layer, Inodes, dentry Cache, FHS 3.0 & Mount Security
Executive Summary: In Linux, "everything is a file descriptor"—whether a block NVMe device, an ext4/XFS/Btrfs regular file, a kernel memory structure (
/proc,/sys), a UNIX domain socket, or an anonymous pipe. This architectural reference dissects the Linux Virtual Filesystem Switch (VFS) kernel abstraction, the separation between directory entries (dentries) and Inodes (struct inode), hard links vs. symbolic links, the Filesystem Hierarchy Standard (FHS 3.0), and hardened/etc/fstab/ systemd.mountflags.
-
VFS:
superblock,inode&dentry
Understand how the Linux kernel unifies ext4, XFS, Btrfs, ZFS, NFS, and
procfsbehind four core C structs (super_block,inode,dentry,file). -
Inode Pointers & Link Semantics
Distinguish directory-entry Hard Links (sharing identical Inode numbers on a single filesystem) from cross-filesystem Symbolic Links (
S_IFLNK). -
FHS 3.0 & UsrMerge Topology
Navigate
/usr,/etc,/var,/run,/proc, and/syswith precision and understand why/binand/libare symlinks to/usr/binand/usr/lib. -
Mount Isolation &
findmnt
Audit block UUIDs, inspect pseudo-filesystems via
findmnt --real, and prevent unlinked open-file disk exhaustion (lsof +L1).
1. Kernel VFS Architecture: How Directories, Inodes & Data Blocks Relate
A common misconception is that a file's name is stored "inside" the file. In reality, an Inode (index node) contains all metadata about a file except its name: UID/GID ownership, POSIX mode bits, byte size, atime/mtime/ctime/crtime timestamps, link count (st_nlink), and extent/block pointers to physical NVMe/SSD sectors. A Directory is simply a lookup table mapping human-readable filenames (dentries) to Inode numbers:
%%{init: {'flowchart': {'wrappingWidth': 600, 'subGraphTitleMargin': {'top': 15, 'bottom': 15}}}}%%
flowchart TB
subgraph DirTable["Directory Data Block (/etc/nginx/)"]
D1["dentry: 'nginx.conf'<br/>-> Inode #131074"]
D2["dentry: 'nginx.conf.hardlink'<br/>-> Inode #131074"]
D3["dentry: 'sites-enabled/app'<br/>-> Inode #131099 (Symlink)"]
end
subgraph InodeTable["Filesystem Inode Table"]
InodeA["Inode #131074 (Regular File)<br/>UID: 0, Mode: 0644, Links: 2<br/>Size: 4096B, Extents ->"]
InodeB["Inode #131099 (S_IFLNK)<br/>Target Path String:<br/>'../sites-available/app'"]
end
subgraph DataBlocks["NVMe Physical Storage Extents"]
Extent1["Physical Block #849201<br/>'user www-data; worker_processes auto;...'"]
end
D1 --> InodeA
D2 --> InodeA
D3 --> InodeB
InodeA --> Extent1
Why Unlinking a File Does Not Immediately Free Disk Space (st_nlink + Open FDs)
The kernel only reclaims an Inode and its physical data blocks when two counters both reach zero:
1. i_nlink == 0: Zero directory hard links point to the Inode (rm calls the unlink(2) syscall, which decrements i_nlink).
2. i_count == 0: Zero running processes hold an open file descriptor (struct file) to the Inode.
Diagnosing the 'Disk 100% Full in df -h, but du -sh Shows Half Empty' Production Incident
If a daemon (nginx, rsyslog, docker) holds an open file descriptor to a 50 GiB log file and an administrator runs rm /var/log/app/huge.log without signaling the daemon to reopen its logs (SIGUSR1 / SIGHUP), i_nlink drops to 0 (so ls and du can no longer see the file), but i_count remains 1. The 50 GiB remains allocated on disk until the process closes the FD or is restarted—or until you truncate the file in-place via /proc/<PID>/fd/<FD>:
2. Filesystem Hierarchy Standard (FHS 3.0) & Modern UsrMerge
Modern Linux distributions (Debian 12+, Ubuntu, Fedora, Arch) implement the /usr Merge (UsrMerge), consolidating /bin, /sbin, /lib, and /lib64 as symbolic links into /usr/bin and /usr/lib so the entire vendor-supplied operating system resides inside a single top-level /usr tree that can be mounted read-only or snapshotted atomically:
| Hierarchy Path | Volatility & Scope | Architectural Purpose & Production Contents |
|---|---|---|
/usr |
Static, Read-Only Capable | Vendor-packaged binaries (/usr/bin), shared libraries (/usr/lib), headers (/usr/include), and architecture-independent data (/usr/share). |
/etc |
Static, Host-Specific | Local system configuration files (sshd_config, fstab, nftables.conf). Never store binaries or large data here. |
/var |
Variable, Persistent | State mutated during normal operation: /var/log (journald/syslog), /var/lib (PostgreSQL, Docker, LVM state), /var/spool, /var/cache. |
/run |
Ephemeral (tmpfs in RAM) |
Runtime daemon state since boot: PID files, UNIX domain sockets (/run/systemd, /run/docker.sock), active mount locks. |
/proc & /sys |
Virtual Kernel Interfaces (0 B on disk) |
Live kernel memory windows exposing process tables (/proc/<PID>/), sysctl tunables (/proc/sys/), and device/cgroup trees (/sys/fs/cgroup/). |
/opt & /srv |
Persistent Service Root | Self-contained third-party or compose stacks (/opt/traefik) and site-specific served data (/srv/borg, /srv/nfs). |
3. Inspecting Inodes, Block Topologies & Mount Namespaces
Use stat, lsblk, findmnt, and df -i to audit storage health, Inode exhaustion, and filesystem UUIDs:
# 1. Inspect exact Inode metadata (Inode number, link count, octal permissions, timestamps)
stat /etc/fstab
# 2. Display block device tree with filesystem types, labels, UUIDs, and mountpoints
lsblk -f
# 3. View clean hierarchical tree of real block-backed filesystems (excluding virtual cgroup/proc mounts)
findmnt --real
# 4. Check BOTH block capacity AND Inode table capacity (millions of tiny files can exhaust 100% of ext4 Inodes!)
df -hT
df -i / /var
4. Hardened /etc/fstab Configuration & Systemd Automounts
Never reference unstable kernel device nodes (/dev/sda1, /dev/nvme0n1p2) in /etc/fstab—PCIe/SATA enumeration order can shift across hardware changes. Always mount by cryptographic filesystem UUID= (from blkid) and include nofail on secondary data/backup drives so a degraded secondary disk never drops the server into an unreachable emergency shell during boot:
# <file system> <mount point> <type> <options> <dump> <pass>
UUID=4b8e91a2-7c1d-4f3a-9e2b-1a2b3c4d5e6f / ext4 errors=remount-ro,noatime 0 1
UUID=8F2A-4C91 /boot/efi vfat umask=0077,shortname=winnt 0 2
UUID=9c3d82b1-6e4f-4a1b-8c2d-3e4f5a6b7c8d /var/log ext4 defaults,nodev,nosuid,noexec,noatime 0 2
UUID=1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d /mnt/backup ext4 defaults,nodev,nosuid,noexec,noatime,nofail,x-systemd.device-timeout=10s 0 2
tmpfs /tmp tmpfs defaults,nodev,nosuid,noexec,size=4G,mode=1777 0 0
Validate any changes to /etc/fstab before rebooting using findmnt --verify: