VFS: From Pathname to Open File
On this page
After deleting a file, an already open file descriptor (fd) can still read from it; opening the same path twice does not necessarily share the read position. By following the three layers of name, object, and open instance, the interface relationships of the VFS can be connected.
The VFS provides a unified entry point, while specific read/write operations, wait conditions, and capabilities are still implemented by objects such as files, pipes, and devices.
| Phenomenon to Explain | Reading Location |
|---|---|
| Why do the read positions of two fds affect each other? | Section 1: Open Instances |
| Why is the path checked but the open still incorrect? | Sections 2–3: Lookup and Operation Dispatch |
When do unlink and close truly release resources? | Section 4: Reference Experiments |
| Why is the content different for the same path in containers? | Sections 5–6: Mount Views and Positioning |
1. Distinguish Between Name, Object, and Open Instance
| Object | Question Answered | Key Boundary |
|---|---|---|
super_block | Which filesystem instance? | Does not correspond one-to-one with mount points; multiple mounts can share it |
inode | Which filesystem object? | Contains type, permissions, size, etc.; identity is not based on the path string |
dentry | Who does this name in the parent directory correspond to? | It is an in-memory name lookup object, not an exact copy of the on-disk directory entry |
struct file | What state does this open instance have? | Corresponds to the open file description, saving offset, status flags, and operation table |
| fd | Through which table entry does the process access the open instance? | Small integers can be reused; FD_CLOEXEC etc. belong to the descriptor layer |
| mount + dentry | In which mount view is the object located? | Together they form struct path; a dentry alone is insufficient to describe the complete location |
Hard links provide multiple names for the same inode; a symbolic link is itself another inode, and resolution continues by looking up its content. For regular local files, (st_dev, st_ino) can be used to compare file identity at that time, but inode numbers may be reused after deletion and cannot be directly treated as permanent global IDs. In stacked filesystem environments, their identity mapping rules should also be considered.
Read the offset changes according to the reference relationships in the diagram:
| How the fd was obtained | Open Instance | Subsequent Read Position |
|---|---|---|
| Independent open of the same path | Usually each gets its own struct file | Each advances independently |
dup, or inheritance via fork | Can reference the same struct file | A read by one party advances the shared offset |
pread | Uses an existing open instance, but explicitly specifies an offset | Does not advance the shared position |
In the diagram, A and B share the open instance, so after A reads, B continues from the new offset; C is opened independently and is not affected by this offset change. See open(2).
2. Why Path Lookup Requires dentry and mount
When resolving /srv/data/a, you need to process directory permissions, directory entries, symbolic links, and mount boundaries component by component. Relative paths start from the working directory or the directory fd of openat(); absolute paths start from the caller's root directory view. The last component is also affected by flags such as creation, exclusivity, and following symbolic links.
Negative dentry caches the lookup result that "this name has no associated inode under this parent directory." It can reduce repeated failed lookups, but it is not proof of permanent non-existence: creation, renaming, cache invalidation, and remote filesystem re-validation can all change the result.
Fast Path: Reduce per-component coordination when cache relationships are stable
RCU-walk reads stable relationships under RCU protection and sequence checks, reducing reference counting and lock operations. When encountering concurrent changes or situations requiring blocking verification, it can switch to REF-walk or re-lookup.
Therefore, a dcache hit does not guarantee that the entire open is lock-free and wait-free: permission checks and the final open still have their own synchronization requirements. See Path Lookup Design.
Why might it still be wrong if the lookup finds existence before opening?
Suppose a program checks that a does not exist, another process immediately creates a, and then the first program opens the other's file with a normal open(O_CREAT). The facts have changed between the two system calls. For creating new files, exclusive requirements should be handed to O_CREAT | O_EXCL; requirements such as restricting paths from escaping a specified directory or prohibiting certain types of symbolic links can be handled using the parsing constraints of openat2() according to the threat model, rather than just doing string prefix checks.
O_TMPFILE is an independent open method for creating an anonymous inode in the filesystem where the specified directory resides, requiring filesystem support; it is not a syntactic sugar for creating a random name and immediately deleting it. Whether it can later be linked to a directory is also constrained by open flags, permissions, and linking methods.
3. Operation Tables Handle Division of Labor, Not Guaranteeing All Capabilities Exist
| Operation Table | Common Responsibilities | Entry Point for Reading Source Code |
|---|---|---|
super_operations | Inode allocation/recycling, filesystem synchronization, etc. | Management of filesystem instances |
inode_operations | Directory lookup, creation, linking, attribute operations | Name and metadata operations |
file_operations | Read/write, positioning, mapping, poll, ioctl, fsync | Operations on opened objects |
address_space_operations | Page cache filling, prefetching, writeback, etc. | Collaboration between cache and backend storage |
dentry_operations | Re-validation, name comparison, etc. | Filesystem-specific name semantics |
The kernel provides generic auxiliary implementations, which filesystems can choose to reuse; this cannot be understood as missing any callback will be automatically filled in. When a capability is not implemented, the call may fail or have default behavior specified by the interface. For example, pipes do not support random positioning like regular files, and device read() does not need to enter the page cache. See the VFS Documentation for operation contracts; specific signatures should be based on the target kernel header files.
For regular files, buffered reads usually check the page cache and wait or initiate backend reads if not ready; the backend can be a block device or a network service. Buffered writes usually modify the cache first, but may wait due to memory allocation, old data reads, or dirty page throttling. O_SYNC, explicit synchronization, and direct I/O change the completion conditions, so "write only modifies memory and never waits for storage here" is an incorrect generalization.
4. unlink, close, and Final Release Are Three Separate Things
unlink() deletes a name in a directory. If there are still hard links, it can still be opened through other names; even if the link count drops to zero, existing open references can usually still access the content. When disk space is reclaimed depends on open references, mappings, filesystem recycling mechanisms, snapshots, and other factors.
| Operation | Relationship Directly Changed | References That May Still Retain Content |
|---|---|---|
| unlink | Deletes the binding from a name to an object | Other hard links, open references, mappings, etc. |
| close(fd) | Removes the current descriptor reference | Other references such as dup, inheritance, or mappings |
| munmap | Revokes that mapping relationship | Other mappings, open references, etc. |
The driver's release should be understood as releasing based on the last open reference, not counting each close; callbacks like flush have different trigger semantics. File mappings can also remain valid after the original fd is closed.
First, operate on names and fds separately in the diagram: independent open, dup, read, unlink, and close change different references. Before the last reference disappears, the content and offset still have a clear owner.
unlink revokes the name, close revokes the descriptor. dup shares the open instance and offset; independent open uses independent offsets.
Below, we only verify shared offsets and "name deleted, open reference can still read" within a temporary directory:
= /
=
=
=
=
assert == b
assert == b # Shares offset with a
assert == b # Offset of the second open is independent
assert == b
assert == 4
=
assert not
assert . == 0
assert == b
This verifies the semantics of local regular files, not power-loss persistence experiments, and cannot be directly used to deduce the post-deletion behavior of all network filesystems.
5. Mounting Is Connecting a Tree into the Name Space
Mounting allows path traversal to enter another directory tree at a certain position. The content at that original position is usually hidden, not deleted. A bind mount can connect an existing subtree to another location, continuing to share underlying objects, so "each mount reads the disk and creates an independent superblock" is incorrect.
A mount namespace isolates the mount view, not the file data copy. Accessing the same file through a shared filesystem in two namespaces can still see each other's writes; whether mount events propagate depends on propagation relationships such as shared/private/slave. You cannot assert that all mount changes are completely isolated just by creating a namespace.
Source Code Entry: Configure Filesystem First, Then Mount View
The new mount interface separates the steps of configuring the filesystem context, obtaining the filesystem tree, and connecting the mount into the position, facilitating object management via fds and reporting parameter errors. The kernel-side fs_context and user-side fsopen/fsconfig/fsmount/move_mount belong to related but different levels of interfaces; when reading the source code, you should track the configuration and mounting stages separately. See Mount API.
6. Locating to the Hierarchy from Phenomena
| Phenomenon | What to Distinguish First |
|---|---|
| Read positions of two fds affect each other | Whether they reference the same open instance, whether obtained via dup/fork |
| Space not significantly reduced after deleting a large file | Whether still open, mapped, or retained by snapshots; don't just check filenames |
| Path exists but open fails | Search for permissions, mount views, symbolic links, final open permissions, and concurrent changes |
| Different content for the same path in containers | root, mount namespace, and mount override relationships |
| open is slow but subsequent read is fast | Name/permission re-validation and data caching are different paths |
Read-only observations can start with findmnt, /proc/self/mountinfo, and /proc/self/fd. /proc/sys/fs/dentry-state is a system-level cache statistic; fields should be compared with current kernel documentation and do not represent the hit rate of a specific directory. Further reading of fs/namei.c, fs/dcache.c, fs/namespace.c, and the data path connects to Page Cache and I/O.