The idea: a name is resolved one directory at a time

A program calls open("/home/ana/notes.txt", O_RDONLY) and gets back a small number, 3. In between, the kernel's VFS (virtual file system layer, the part common to ext4, XFS, NFS and the rest) does two jobs. It turns the name into an inode, one directory at a time. Then it makes a struct file, the open file with its own offset, and puts a pointer to it in the process's fd table. The number you get back is just an index into that table.

Three levels from left to right: per-process fd tables, open file descriptions (struct file with f_pos and f_count), and the inode. On the left, fd 3 and fd 4 of one process and fd 3 of a forked child all point to one struct file with f_pos 300 and f_count 3. On the right, two separate open() calls give fd 5 and fd 6, two struct files with f_pos 100 and f_pos 0. All struct files point to the same inode 1203.
dup() and fork() copy the pointer, so they share one struct file and one offset; two open() calls make two struct files that only share the inode.

The page follows one process, pid 1200, running as user ana. There are two ext4 file systems: / on nvme0n1p2 and /home on nvme0n1p3. Pick a system call in action and press Do It. The top band shows the path being walked, the middle band the dentry cache, and the bottom band the fd tables, struct files, inodes and directory blocks on disk.

The walk: one component at a time

path_init picks the starting point. An absolute path starts at the process's root dentry. A relative path like notes.txt starts at its current directory (cwd), so nothing before it is looked up. Then link_path_walk takes the names one by one. The walk's position is a pair, nd.path = { vfsmount, dentry }: which mounted file system and which directory in it. For each name it checks permission on the current directory, looks the name up, and moves into the result. (Demo: same open again, then a relative path: after chdir("/home/ana"), open("notes.txt") is a one-step walk.)

The dentry cache: hits, misses and negative dentries

A dentry (struct dentry) is a cached answer to "in this directory, which inode does this name point to?". The dcache is a hash table keyed by (parent dentry, name). lookup_fast → __d_lookup checks it first. A hit costs no I/O at all. On a miss, lookup_slow asks the file system: ext4_lookup searches the directory's blocks (from the page cache, or from the disk if they are not there) and reads the inode, and d_add puts a new dentry in the cache.

A name that is not there is cached too, as a negative dentry: a dentry with no inode. The next open() of the same missing name gets ENOENT straight from the cache. Programs that search a list of directories (the shell's PATH, the dynamic linker, Python's sys.path) miss all the time, so this matters. (Demo: ENOENT twice.)

Walk of /home/ana/notes.txtdcache hitsmissesdisk reads
cold (after drop_caches, cwd /)1 (home)2 (ana, notes.txt)2 directory blocks
the same open again300

Some dentries can't be dropped. Each dentry holds a reference to its parent, and the root dentries, the mount points, every process's cwd and every open file hold theirs. echo 3 > /proc/sys/vm/drop_caches (the drop caches action) frees the rest, along with the page cache.

Permissions on the way

For every directory the walk passes through, may_lookup checks x (search) permission. At the end, may_open checks the open mode on the file: r for O_RDONLY, w for O_WRONLY. A directory without x stops the walk with EACCES, even when the file inside is readable by everyone. The name is never even looked up. (Demo: EACCES: /home/bob is drwx------ bob.)

BitOn a fileOn a directory
rread the byteslist the names (ls)
wchange the bytescreate, delete and rename names in it
xrun itpass through it: look up a name you already know

How the kernel chooses between the owner, group and other bits, and where root's capabilities and setuid come in, is on Linux Permissions: uid, rwx, setuid and Capabilities.

Crossing a mount point

/home exists twice: as a plain directory on the root file system, and as the root of nvme0n1p3, mounted on top of it. The kernel marks the first dentry DCACHE_MOUNTED. When the walk lands on it, it looks up the vfsmount mounted there and continues from that file system's root dentry: nd.path.mnt changes, and the inode numbers start again at 2. Which file systems are mounted where is per mount namespace, so a container walking the same path can end up somewhere else (see What a Container Really Is).

Two dentry trees. Left, nvme0n1p2: / (inode 2) with the directory home (inode 131), marked as a mount point. Right, nvme0n1p3: its root / (inode 2) with ana (inode 12) and bob (inode 13), and notes.txt (inode 1203) under ana. An arrow labelled vfsmount goes from home on the left to the root on the right; the walk of /home/ana/notes.txt is drawn across it in four steps.
The walk reaches home on the root file system, follows the vfsmount, and goes on in the other file system's tree.

A symlink is an inode whose content is a path. When the walk meets one, it reads that text (for a short link it is stored in the inode itself, so there is no extra block to read) and goes on with it. A relative target like notes.txt is followed from the symlink's own directory, and an absolute one starts again at /. The kernel follows at most 40 links in one lookup and then fails with ELOOP. O_NOFOLLOW refuses a symlink in the last component. (Demo: symlink followed: latest → notes.txt; the fd ends up on inode 1203, not on the link's inode 1207.)

What open() returns

When the walk ends on the file, do_open fills in a struct file (the kernel allocates it with alloc_empty_file just before the walk; the page draws it at the end), the open file description in POSIX terms. It holds f_pos (the offset the next read starts at), f_mode and f_flags (how it was opened), f_count (how many references it has) and f_path, which leads to the dentry and inode. get_unused_fd_flags picks the lowest free fd, 3 when only stdin, stdout and stderr are open, and fd_install stores the pointer there in the process's files_struct → fdtable. The first field of /proc/sys/fs/file-nr counts the struct files in the whole system. See A Linux System Call, Step by Step for how openat gets into the kernel.

dup, fork and two opens

new fdnew struct fileoffsetinode
dup(3)yes, in the same tableno, f_count + 1sharedsame
fork()the child gets a copy of the whole tableno, every f_count + 1shared with the parentsame
open() againyesyesits own, starts at 0same

That is why a shell can run (echo a; echo b) > log and get both lines: the two processes write through one struct file, so the second write starts where the first ended. Two separate open() calls without O_APPEND would overwrite each other. fork copies the fd table the way it copies the page table (see Copy-on-Write fork()), but the open files themselves are shared, not copied. close() drops one reference, and the struct file is freed when f_count reaches 0. (Demo: dup and fork share an offset, open doesn't.)

RCU-walk and ref-walk

The page draws the slow, simple version of the walk. Real lookups first try RCU-walk: under rcu_read_lock, with no locks and no reference counts, checking a sequence count on each dentry to make sure it did not change underneath. Most lookups of cached paths finish this way, which matters because every core may be resolving paths at once. If anything gets in the way (a miss, a dentry being renamed, a file system that needs to revalidate), the walk returns -ECHILD and starts again in ref-walk mode, taking a reference on each dentry as it goes, as drawn here.

See also File System for what happens after open(): the page cache, write(), fsync() and the journal.

What the page leaves out

Creating files (O_CREAT, O_EXCL) and the locks on the parent directory, O_PATH, openat2() and its RESOLVE_* flags, .. and chroot, bind mounts and mount propagation, groups, ACLs, capabilities and security modules like SELinux, the order in which the dcache evicts dentries, network file systems that revalidate dentries (d_revalidate), O_CLOEXEC and close_range, and the limits themselves: a real process may have 1024 fds or more, and this page draws 6 per process and 5 struct files.