Filesystems · Filesystem Fundamentals
Rename Atomicity and the Replacement of a Directory Entry
How rename changes two directory entries without a window of invisibility, which cross-filesystem cases the guarantee excludes, and why the call is the primitive behind atomic file updates.
A program that must replace a configuration file without ever letting a reader see a half-written one has exactly one portable tool: write the new content to a temporary name in the same directory, then rename it over the old name. The technique works because rename is atomic with respect to concurrent lookups: a reader sees either the old file or the new one, never a missing name and never a mixture. The guarantee is narrower than “atomic” suggests, and the exclusions are as instructive as the promise.
Two entries change, one instant
The operation being performed is not a copy. rename("a", "b") removes the entry named a and makes the name b refer to the inode that a referred to. When b already exists, the entry for b is removed first and the inode it named has its link count decremented, possibly freeing it.
On a local filesystem the kernel performs the whole change while holding the directory locks, and a concurrent open("b") either resolves before the change or after it. The standard states the requirement directly: the operation is atomic with respect to other processes, so a pathname either resolves to the old object or to the new one, and there is no intermediate state in which b names nothing.
The cost of that guarantee is serialisation. Two renames into the same directory contend on the directory’s lock, and the kernel must also order the two entries’ updates against the journal or copy-on-write transaction that records them. On a directory under heavy create-and-rename traffic, the directory lock becomes the bottleneck, which is why build systems that stage outputs in a shared directory scale poorly compared with one staging directory per worker.
The four cases the standard distinguishes
POSIX splits the behaviour by what the source and destination are, and each case has its own failure:
| Source | Destination exists | Result |
|---|---|---|
| file | no | entry created; nothing else changes |
| file | regular file | destination removed, then source linked; the old destination’s inode loses a link |
| file | directory | fails with EISDIR |
| directory | empty directory | destination removed, then source linked |
| directory | non-empty directory | fails with ENOTEMPTY or EEXIST |
| directory | regular file | fails with ENOTDIR |
The rule for directories is the interesting one. Replacing an empty directory is allowed and is the mechanism behind tools that swap a directory into place. Replacing a non-empty one is refused because the operation would orphan every file inside it: their entries would still exist but their parent would have no name, and no path could reach them. Refusing keeps every reachable inode reachable by name, an invariant that fsck relies on.
A destination that is a mount point is also refused, with EBUSY, because removing the entry would leave the mounted filesystem attached to a path that no longer exists.
Why the temporary file must be in the same directory
The atomicity guarantee is scoped to one filesystem. If the source and destination are on different mounts, rename fails with EXDEV and no partial operation occurs. The standard requires the failure rather than a copy, because a copy cannot be atomic: it creates the destination gradually, and a reader can observe a partial file, a truncated file, or an absent one.
That is the reason the idiom places the temporary file in the same directory as the target. A temporary file in /tmp and a target in /srv are usually on different filesystems, and the rename fails. Even when they happen to share a filesystem, the guarantee is only about names in the same filesystem’s namespace, and a mount boundary anywhere in the path can change the answer. Same-directory placement removes the question.
The idiom also needs the temporary file to be written and closed before the rename. A rename of a file that is still open is legal and the open description keeps working, but the data may not be durable yet. Making the sequence crash-safe requires an fsync on the temporary file before the rename and, on most filesystems, an fsync on the directory afterwards to persist the entry. What each of those calls actually guarantees is the subject of fsync, fdatasync, and What Survives a Power Loss.
What a crash can leave behind
The atomicity guarantee is about concurrent observers, not about power loss. A filesystem that has journalled the two entry updates can replay them and arrive at either the old or the new state, and a filesystem that has not may arrive at neither. The recovery property that matters is that the directory is not left referring to a freed inode.
Ext4 records a rename as a transaction covering the source directory block, the destination directory block, and the inode link counts. Replay after a crash applies the whole transaction or none of it, so the visible outcome is one of the two end states. The ordering between the rename and the file’s data blocks is a separate question: the rename may be durable while the file’s contents are not, which is exactly the case the fsync-before-rename sequence closes.
Filesystems with different guarantees exist. Some network filesystems implement rename as a sequence of operations on the server and do not promise atomicity to clients that cache directory state, so a program that relies on the guarantee has to know which filesystem it is running on; POSIX describes the guarantee for the local case and leaves the remote case to the protocol. Code that depends on the guarantee should be explicit about which filesystem it is running on rather than assuming the local behaviour everywhere.
Renaming directories and the parent link
Renaming a directory changes its .. entry, because the directory now sits under a different parent. The on-disk .. must be rewritten, and the old and new parents’ link counts must be adjusted: the old parent loses one, the new parent gains one. Three inodes change in one operation, which is why a directory rename is more expensive than a file rename and why the kernel takes locks on the parents in a defined order to avoid deadlock between two concurrent renames in opposite directions.
The move is refused if the destination is a descendant of the source. rename("/a", "/a/b/c") would create a cycle in the tree, and the check walks upward from the destination to see whether the source appears among its ancestors. That walk is bounded by the tree depth and is the only case in which rename needs to consult more than the two immediate parents.
Moving a directory across a mount boundary is refused for the same reason as files: the subtree would have to be copied, and the atomicity would be lost. Tools that move a large tree between filesystems do it by creating the destination, copying, and deleting the source, which is not atomic and leaves a window in which both copies exist.
The cost of replacing a large file
Replacing a file by rename does not copy the old content, and it does not wait for the new content to reach the disk unless the program asks. The new inode already exists and is fully written; the rename only repoints a name. The old inode is freed when its last link disappears, and its blocks return to the allocator.
That last step is where the surprise lives. If any process still holds the old file open, the inode survives with a link count of zero, and its blocks stay allocated until the description closes. A service that replaces a multi-gigabyte file every hour and keeps a descriptor open on the old version accumulates one generation of dead space per open descriptor. The same mechanism, viewed from the other side, is the reason a log rotator can rename the active log and have the writing process continue into the renamed file without losing a byte.
Where rename is not enough
Three limitations are worth stating because each produces a class of bug that no amount of care at the call site removes.
First, a rename cannot make a set of files change together. Replacing ten files by ten renames exposes ten intermediate states to a reader that opens them in sequence. Directories are the usual answer: build a complete new directory, then rename the directory itself over the old one. That works only if the destination directory is empty at the moment of replacement, which means the old directory must be moved aside first, and the two renames are not one atomic operation. Filesystems with directory-level transactions or snapshots close that gap, but the POSIX interface does not.
Second, a rename does not invalidate open descriptors. A process that resolved the old name before the rename continues to read the old inode. This is correct and usually desirable, but it means a reader cannot be forced to see the new content without being restarted or re-opening the file by name.
Third, the atomicity is per-name, not per-file-content. A rename makes a name point at a complete inode, but it says nothing about whether that inode’s data is on stable storage. The two properties are independent, and programs that need both must request both. The directory entry that rename rewrote is itself a record in a directory file whose layout is described in Directory Entries as a Name-to-Inode Map, and the guarantee is a statement about how that record is replaced under a lock rather than about anything stored in the inode itself.
References
- rename(2) — Linux manual page — man7.org
- The Open Group Base Specifications Issue 7, 2018 edition — rename() — The Open Group
- Ext4 Data Structures and Algorithms — Directory Entries — Linux kernel documentation