Infra, Illustrated.
LINUX FOUNDATION · STORAGE CONSOLE

Filesystem & Storage

Demystify the Linux storage stack by tracing pathnames through the Virtual File System (VFS), directory entry caches, and inode tables down to physical block devices.

← Back to Linux MapREFERENCE HOST · devbox-01DEEP DIVE · FOUNDATIONALPublished Sep 15, 2026 · Reviewed Sep 18, 2026
CHAPTER 02 / 08 · 8 VISUAL LESSONS

What this chapter builds

This chapter turns the “everything is a file” shortcut into a precise model of names, open handles, cached pages, filesystems, mounts, and block devices.

By the end, you can

  • Trace pathname lookup through dentries and inodes
  • Distinguish a directory entry from the open file it names
  • Diagnose block, inode, and open-unlinked exhaustion
  • Expand a cloud volume through every required layer

Objects to keep straight

dentryinodestruct filepage cachemountblock device

Applied task: Explain why df reports a full filesystem after a deleted log disappears from du.

SCENARIO · devbox-01 / nginxCore concepts
OBJECT Core conceptsSTATE ready to inspectOWNER VFS / filesystem
01 / STORAGE · SYSTEM MODEL

Before looking at the mechanics, what actually is a File or an Inode?

What are the foundational primitives of Linux Storage?

DEVBOX-01 · CORE CONCEPTSGLOSSARY

File Descriptor (FD)

Analogy: A Ticket to Read/Write

When a process opens a file, the kernel hands it an integer (like 3 or 4) called a File Descriptor. The process uses this ticket to read or write data. When it closes the file, the ticket is destroyed.

VFS (Virtual File System)

Analogy: A Universal Translator

Linux supports many filesystems (ext4, XFS, NFS). The VFS is a unified abstraction layer. User programs just call read() or write(), and the VFS translates it for the specific underlying disk format.

Inode

Analogy: A Passport with Metadata

A file name is just a string in a directory. The ACTUAL file is an Inode—a data structure storing permissions, ownership, timestamps, and pointers to where the actual data blocks live on the hard drive.

Mount

Analogy: Grafting a Branch on a Tree

Unlike Windows (C:\, D:\), Linux has one single directory tree starting at "/". Disks and network drives are "mounted" (attached) as subdirectories (like /mnt/usb) onto that single tree.

Paths point to Inodes. Processes hold File Descriptors. The VFS connects them to the raw disk blocks.

Storage in Linux is heavily abstracted to make everything look like a unified tree. A file path (like /var/log/syslog) is just a human-friendly pointer to an Inode. Processes never interact with Inodes or disk blocks directly; they interact with File Descriptors. The kernel handles all the complex translation in the background.

Model scope. “Everything is a file” is shorthand for a broad descriptor-based interface. Device nodes and procfs entries participate in VFS pathname operations, while sockets usually enter a process through socket()/accept() rather than a filesystem pathname.

INSPECT · stat -c "Inode: %i" /etc/passwd
Inode: 131075
NEXT PROBE · ls -l /proc/$$/fd
lrwx------ 1 root root 64 0 -> /dev/pts/0
lrwx------ 1 root root 64 1 -> /dev/pts/0
Primary source ↗
02 / STORAGE · SYSTEM MODEL

Directories are in-memory lookup maps; metadata lives in Inodes.

How does the Linux kernel translate a path string into disk sectors?

DEVBOX-01 · SYSTEM MODELREADY TO INSPECT
1. PATH STRING/srv/app/configParsed component by component
2. RAM CACHEDentry CacheHash maps name to Inode #262145
3. METADATAInode #262145Mode 0644, UID 33, Extent Tree
4. PAGE CACHEPage Cache (RAM)4KB memory pages from disk
5. STORAGEext4 /dev/vdc1Physical NVMe blocks
TRACE THE SYSTEM PATH

All steps are visible. Advance to inspect one transition.

Filenames exist only inside directory data; all file properties and block locations live in the Inode.

When a process invokes open(), the kernel Virtual File System (VFS) resolves the path by walking directory entries (dentries) component by component. Dentries are cached in RAM (dcache). The leaf dentry points to an Inode number. The Inode contains all file metadata (permissions, owner, size, timestamps) and an extent tree mapping logical file offsets to physical block sectors on the underlying block device.

Model scope. The dentry cache (dcache) and inode cache are dynamically shrunk by kernel kswapd under memory pressure, which can cause sudden latency spikes if uncached lookups hit disk.

INSPECT · stat /srv/app/config
  File: /srv/app/config
  Size: 4096      Blocks: 8          IO Block: 4096   regular file
Device: 259,1   Inode: 262145      Links: 1
Access: (0644/-rw-r--r--)  Uid: (33/www-data)   Gid: (33/www-data)
NEXT PROBE · ls -di /srv /srv/app /srv/app/config
131072 /srv  262144 /srv/app  262145 /srv/app/config
Primary source ↗
03 / STORAGE · SYSTEM MODEL

Stitching independent filesystems into a single unified directory tree.

How do different disks and virtual filesystems connect under a single / root?

DEVBOX-01 · SYSTEM MODELREADY TO INSPECT
Use findmnt to inspect the real mount hierarchy, filesystem types, and active mount flags.

Linux exposes all storage devices under a single unified root directory (/). When mount() is invoked, the kernel creates a struct mount linking a target directory dentry to the root dentry of the mounted filesystem. Any previous files residing in the target directory become shadowed (hidden) until unmounted. The kernel VFS intercepts lookups crossing mount points and transparently switches to the child filesystem driver.

Model scope. Containers use mount namespaces and bind mounts (mount --bind) to expose specific host directories or volumes inside container root filesystems.

INSPECT · findmnt /srv
TARGET SOURCE    FSTYPE OPTIONS
/srv   /dev/vdb1 ext4   rw,relatime,errors=remount-ro
NEXT PROBE · lsblk -f /dev/vdb
NAME   FSTYPE FSVER LABEL UUID                                 FSAVAIL FSUSE% MOUNTPOINTS
vdb
└─vdb1 ext4   1.0         9b31d87e-45a1-432e-9d21-4f1122334455   48.2G     4% /srv
Primary source ↗
04 / STORAGE · ARCHITECTURAL COMPARISON

Inodes, hard links, and symlinks: understanding POSIX file semantics.

What is the structural difference between a hard link and a symbolic link?

DEVBOX-01 · ARCHITECTURAL COMPARISON5 ENTITIES COMPARED
01Regular File (-)

Direct inode holding data block pointers via ext4 extent trees.

$ stat -c "%F %i %h" /srv/site/index.html
regular file 262145 1
02Hard Link

A second directory entry pointing to the EXACT same Inode number. Deleting one leaves data intact until nlink drops to 0.

$ stat -c "%i %h" /srv/site/index.html /srv/site/home.html
262145 2
262145 2  # Same inode!
03Symbolic Link (l)

An independent inode containing the target path string as data. Breaks if target is moved or deleted.

$ ls -l /srv/site/current
lrwxrwxrwx 1 root root 10 current -> index.html
04Block Device (b)

Kernel interface providing buffered random access to fixed-size blocks (NVMe, SSD).

$ ls -l /dev/vdb1
brw-rw---- 1 root disk 259, 1 /dev/vdb1
05Character Device (c)

Kernel interface providing unbuffered byte-stream access (TTYs, /dev/urandom).

$ ls -l /dev/urandom
crw-rw-rw- 1 root root 1, 9 /dev/urandom
Hard links share an Inode and survive renaming; symlinks store a path string and break if the destination moves.

The first character of ls -l indicates the file type. A hard link creates an additional directory entry pointing to an existing inode, incrementing its link counter (nlink). Hard links cannot span across different filesystems because inode numbers are only unique within a single filesystem. In contrast, a symbolic link (symlink) is an independent file whose data payload is simply a path string pointing to another target.

Model scope. Directories cannot have hard links created by user processes to prevent directory loops in the filesystem DAG.

INSPECT · ls -la /tmp | head -n 4
drwxrwxrwt 15 root root 4096 Sep 18 19:40 .
drwxr-xr-x 19 root root 4096 Sep 18 12:00 ..
srwxrwxrwx  1 mysql mysql  0 Sep 18 12:05 mysql.sock
-rw-------  1 root root  123 Sep 18 12:10 test.log
NEXT PROBE · stat -c "%F links:%h inode:%i" /srv/site/index.html
regular file links:1 inode:262145
Primary source ↗
05 / STORAGE · COMPARISON MODEL

Two independent exhaustion vectors: Data Blocks vs Inode Slots.

Why does a write fail with "No space left on device" when df -h shows 50% free?

DEVBOX-01 · COMPARISON MODELBLOCKED FIXTURE
DATA BLOCKS (df -h)50% Used · 50% Free
Space remains for file writes
INODE SLOTS (df -i)100% Used · 0 Free!
ENOSPC: No metadata slots left
FAILURE / ALTERNATE STATE SIMULATION

Default fixture: Filesystems like ext4 preallocate a fixed number of inodes during mkfs. Every file, directory, symlink, or socket consumes exactly one inode, regardless of file size. When an application generates millions of zero-byte or tiny files (e.g., PHP sessions, cache tokens, or build artifacts), all inode slots become consumed long before disk storage is full. The kernel returns ENOSPC (No space left on device) for any new touch or mkdir.

Always run df -i alongside df -h when diagnosing mysterious disk-full alerts in production.

Filesystems like ext4 preallocate a fixed number of inodes during mkfs. Every file, directory, symlink, or socket consumes exactly one inode, regardless of file size. When an application generates millions of zero-byte or tiny files (e.g., PHP sessions, cache tokens, or build artifacts), all inode slots become consumed long before disk storage is full. The kernel returns ENOSPC (No space left on device) for any new touch or mkdir.

Model scope. Modern filesystems like XFS and Btrfs dynamically allocate inodes as needed and do not suffer from the rigid fixed-inode limitation of classic ext4.

INSPECT · df -h /srv
Filesystem      Size  Used Avail Use% Mounted on
/dev/vdb1        50G   25G   25G  50% /srv
NEXT PROBE · df -i /srv
Filesystem     Inodes   IUsed IFree IUse% Mounted on
/dev/vdb1     3276800 3276800     0  100% /srv
Primary source ↗
06 / STORAGE · SYSTEM MODEL

The phantom disk leak: unlinked files held open by running processes.

Why does df still show 100% full after deleting a huge 20GB log file?

DEVBOX-01 · KERNEL VFS LABREADY TO INSPECT
DIRECTORY ENTRY (DENTRY)User-space path resolution
PathPoints to Inode
/srv/log/access.log262150
PROCESS FD TABLEKernel process memory
ProcessFDPoints to Inode
nginx (421)14 (w)262150
🗄️INODE 262150
Hard Links (i_nlink)1
Open Handles (f_count)1
Data Blocks Size20 GB
State: ALIVE (Refcount > 0)
20 GB RETAINED ON DISK
INTERACTIVE SIMULATION

Click rm access.log to see why space isn't freed when a file is deleted while still open.

Never rm active log files; truncate them using : > file.log or reload the daemon to release the file handle.

Linux separates directory entries from physical storage blocks using two distinct counters: the link count (i_nlink in the inode) and the open file descriptor count (f_count in the kernel). Running rm only unlinks the directory entry (setting nlink to 0). du inspects the directory hierarchy, so it will no longer find the file. However, because the daemon still has the file open, the kernel preserves the inode and disk blocks until the process closes the file descriptor or terminates.

Model scope. If you already deleted the file path, you can reclaim space without restarting by truncating via procfs: : > /proc/<pid>/fd/<fd>.

INSPECT · lsof +L1 /srv
COMMAND   PID     USER   FD   TYPE DEVICE SIZE/OFF NLINK      NODE NAME
nginx     421 www-data   14w   REG  259,1 21474836480     0    262150 /srv/log/access.log (deleted)
NEXT PROBE · truncate -s 0 /proc/421/fd/14
# Space reclaimed instantly without restarting service: 20GB freed on /srv
Primary source ↗
07 / STORAGE · RUNBOOK STATE MACHINE

From raw NVMe block device to persistent production mount.

How do you safely partition, format, and mount storage across host reboots?

DEVBOX-01 · RUNBOOK STATE MACHINEACTION 01 / 06 ACTIVE
RUNBOOK ACTION STEPPER

Action 01: Inspect raw block device topology

$ lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT
vdb   50G disk
└─vdb1 50G part /srv

Field Note: Verify the device name and ensure it does not already contain an active filesystem.

Always use UUID in /etc/fstab and test with mount -a before rebooting.

Storage provisioning follows five deterministic layers: raw block device → partition table (GPT) → filesystem structures (ext4/XFS) → target mount point dentry → /etc/fstab automation. Mounting by filesystem UUID eliminates catastrophic data corruption caused by non-deterministic device enumeration (e.g. /dev/sda becoming /dev/sdb).

Model scope. Filesystem check order (the 6th field in fstab): 1 for root (/), 2 for other local drives, 0 to skip fsck (e.g. XFS or network mounts).

INSPECT · findmnt --fstab /srv
TARGET SOURCE                                    FSTYPE OPTIONS
/srv   UUID=9b31d87e-45a1-432e-9d21-4f1122334455 ext4   defaults,nofail
NEXT PROBE · sudo blkid -s UUID -o value /dev/vdb1
9b31d87e-45a1-432e-9d21-4f1122334455
Primary source ↗
08 / STORAGE · RUNBOOK STATE MACHINE

Online volume expansion: hypervisor, partition table, and filesystem.

Why does df still show the old size after expanding an EBS/cloud volume?

DEVBOX-01 · RUNBOOK STATE MACHINEACTION 01 / 05 ACTIVE
RUNBOOK ACTION STEPPER

Action 01: Expand virtual disk in cloud console/API

$ aws ec2 modify-volume --volume-id vol-12345 --size 100
{
  "VolumeModification": { "TargetSize": 100, "Progress": 0, "ModificationState": "modifying" }
}

Field Note: The cloud hypervisor expands the backing virtual block device without unmounting.

Volume expansion requires 3 manual steps: Cloud size → growpart (partition) → resize2fs/xfs_growfs (filesystem).

Expanding storage requires notifying three distinct layers in order: 1. Cloud hypervisor increases block device size; 2. Partition table is expanded via growpart to encompass the new sector range; 3. Filesystem driver is instructed via resize2fs or xfs_growfs to extend its block groups and superblocks across the newly available partition space.

Model scope. NVMe devices on newer EC2 instances use names like /dev/nvme1n1p1. growpart syntax requires a space before partition number: sudo growpart /dev/nvme1n1 1.

INSPECT · df -h /srv
Filesystem      Size  Used Avail Use% Mounted on
/dev/vdb1        99G   25G   74G  25% /srv
NEXT PROBE · lsblk -o NAME,SIZE,MOUNTPOINT /dev/vdb
NAME   SIZE MOUNTPOINT
vdb    100G 
└─vdb1 100G /srv
Primary source ↗