Computing History · Unix Lineage
One Rooted Namespace: How Mounting Replaced Separate Volumes
Why Unix names every file under a single root, how mount grafts a filesystem onto a directory, and what the resulting ambiguity costs in path resolution.
Systems that preceded Unix mostly named files by volume: a file belonged to a pack or a disk, and the name included the device. Unix removed the device from the name and kept one tree. Every file on every mounted filesystem appears at some path under /, and no path component identifies which physical device holds the data. The decision is small in statement and large in consequence, because it moves the identity of the filesystem out of the name and into a lookup table consulted during traversal.
The mount point as a graft
A filesystem is a self-contained tree with its own root directory inode. Mounting attaches that tree at an existing directory in another tree. The directory is the mount point, and it must exist before the mount, because the mount operation replaces the directory’s contents rather than creating a name. After the mount, path resolution that reaches the mount point and continues downward switches to the root of the mounted filesystem.
The original directory is not destroyed. It is covered. The inode still exists, its entries are still there, and unmounting reveals them again. This is why a mount point directory is conventionally empty: anything in it becomes inaccessible for the duration of the mount, and a process holding a descriptor to the covered directory or to a file inside it can still read that data, which is one of the few ways to reach it.
The graft is recorded per mount point, not per filesystem. The same filesystem can be mounted at two places in the tree, and both paths reach the same inodes. The same directory can have exactly one filesystem mounted on it at a time; a second mount on the same point stacks on top, hiding the first, and Linux’s mount namespace allows that stacking as a general mechanism rather than an error.
#include <stdint.h>
#include <stdio.h>
#include <sys/stat.h>
int main(int argc, char **argv) {
struct stat st;
if (argc < 2) return 1;
if (stat(argv[1], &st) < 0) { perror("stat"); return 1; }
printf("device %ju inode %ju\n",
(uintmax_t)st.st_dev, (uintmax_t)st.st_ino);
return 0;
}
The st_dev field is the identifier of the filesystem holding the inode, and it changes across a mount boundary while st_ino does not have to be unique between filesystems. Two paths can therefore name inodes with the same number on different devices, and the pair (device, inode) is the actual identity of a file. Every program that compares files by inode must compare both fields, and the standard library function that does so takes two stat structures rather than two numbers.
Resolution is a walk with a switching rule
Path resolution starts at the root of the calling process’s namespace and consumes one component at a time. At each step the kernel holds a reference to a directory inode, looks up the next component in that directory, and obtains the inode for it. If the resulting inode is a directory that is a mount point, resolution switches to the mounted filesystem’s root before the next component is looked up.
The switching rule has to be stated precisely because the naive version is wrong. The mount table is keyed by (parent filesystem, mount point inode), not by path string, so a bind mount or a mount inside a mount still resolves correctly even though its path is ambiguous. A path that arrives at the mount point through a different route, such as a hard link to the directory in another filesystem, does not trigger the switch, which is why hard links to directories are prohibited: they would create a directory reachable from two places with two different mount behaviours.
Symbolic links complicate the walk because they introduce a new path that must be resolved from the start, and the kernel imposes a limit on how many link traversals one resolution may perform. Linux uses 40 for the total number of symbolic links followed, and returns ELOOP beyond that. The limit exists to terminate loops such as a link pointing at itself; it also means that a legitimate chain of more than 40 links fails.
Each component lookup is a permission check. The kernel requires execute permission on every directory traversed, read permission only when listing. This is why a directory with mode --x can be traversed by path but not listed, and why a program that holds an open descriptor to a directory can continue to resolve relative to it even after the permissions on an ancestor are revoked: the descriptor bypasses the re-check of ancestors. The bits themselves are checked as described in the permissions article, with the owner class taking precedence over group and other.
The consequences of removing the device from the name
The first consequence is that a path is portable across configurations in a way a device-qualified name is not. A program that opens /etc/hosts does not know or care whether /etc is on the root filesystem, on a separate partition, on a network filesystem, or on a memory-backed filesystem. Administrators can move a subtree to another device without changing any program, which is the entire point of the separation and the reason /usr, /var and /home are conventionally separate filesystems.
The second is that the namespace is a policy surface. Because mounting is an operation on the tree rather than a property of the data, the same filesystem can appear at different places for different processes. Linux mount namespaces make this explicit: each process can have a private mount table, so a container sees its own / assembled from selected parts of the host’s. The mechanism that made the tree uniform also made it a thing that can be composed per process.
The third is that path names no longer tell the whole story about durability, capacity, or semantics. /tmp and /var/tmp may be on the same device or not, may be cleared on boot or not, and may enforce quotas or not. A program cannot infer from the path whether a rename is atomic across two directories; rename is atomic only within one filesystem, and a cross-filesystem rename must be implemented as copy-then-unlink by the caller or the library. rename returns EXDEV in that case, and the caller is expected to handle it. This is a direct cost of hiding the device: the one operation that depends on device identity no longer has it available in the name.
Where the single tree strains
The model assumes a tree, and a tree cannot express a file with two parents. Hard links give a file two names within one filesystem, which is a directed acyclic graph rather than a tree, and the link count in the inode tracks how many names exist. The count is why unlink does not necessarily free anything, as described in the descriptor article, and why a directory’s link count is at least two: its own name and its . entry.
Hard links cannot cross filesystems, because a directory entry is a structure inside a specific filesystem and cannot name an inode in another. Symbolic links cross freely, at the cost of a second resolution and the possibility of dangling. The two mechanisms are not alternatives; they answer different questions, and the fact that both exist is a symptom of forcing a general graph into a tree.
The second strain is that the mount table is global mutable state with no transactional interface. A mount or unmount changes what a path means for every process that resolves it afterwards, and there is no way to hold a path stable against a concurrent mount except by holding an open descriptor or by resolving with openat relative to a descriptor. The *at family of system calls exists partly for this: a descriptor to a directory fixes the meaning of subsequent relative lookups, so a program that opens a directory and then operates relative to it cannot be redirected by a rename of an ancestor. Path strings are not stable handles, and the interface eventually grew handles that are.
The third is that the root itself is a policy decision. chroot changes the root for a process and its descendants, and it is not a security boundary on its own because a process with the ability to create device nodes or to mount can escape; the mount namespace plus user namespaces are the mechanisms that made the boundary real. Both are extensions of the same idea: the tree is uniform, and uniformity means the top of the tree can be chosen.
References
- Linux man-pages: path_resolution(7) — Michael Kerrisk, man-pages project
- The UNIX Time-Sharing System — Ritchie & Thompson, Communications of the ACM, 1974
- Linux man-pages: mount(2) — Michael Kerrisk, man-pages project
- The Design and Implementation of the 4.4BSD Operating System — McKusick, Bostic, Karels, Quarterman; Addison-Wesley, 1996