QUESTION
Why can an open fd keep reading a file after it is unlinked?
A pathname is a name in a directory entry that refers to an inode number, while an open fd refers through a dentry/path to an inode and struct file. unlink removes only one name from the directory; the inode data remains until both open file references and the link count reach zero.
Within one filesystem, rename provides atomic name replacement from the namespace perspective. It does not guarantee that the data and new directory entry survive a crash, so file fsync and directory fsync must be designed separately.
STRUCTURE
Structure diagram
before rename
- config → inode A
- .tmp → inode B
- reader fd → inode A
- B: new contents + fsync
renameat
- directory lock
- atomic name replacement
- update config entry
- directory fsync is separate
after rename
- config → inode B
- no .tmp name
- existing reader fd → inode A
- new open → inode B
The pathname changes to the new inode, but an already-open old fd continues to refer to the old inode. The old inode remains until its open references disappear.
CALL PATH
Call path
Atomicity means an observer sees either the old name state or the new one; durability means which state remains after power loss. Do not collapse these two properties into one term.
This diagram is not for memorizing function names. Follow which return value, file descriptor, memory mapping, or wait queue is passed to the next stage.
SOURCE COORDINATES
Linux 6.18.37 LTS source locations
Go beyond the glibc function to the files where the syscall implementation meets kernel objects. Each link points to the original file at the same tag.
| File | Function / structure | What to inspect |
|---|---|---|
| fs/namei.c | do_unlinkat(), do_renameat2(), vfs_rename() | Remove directory entries and replace names |
| fs/open.c | do_sys_openat2() | Turn a pathname into a struct file reference |
| fs/sync.c | do_fsync(), vfs_fsync_range() | Request durability from the file and filesystem |
COMPLETE PROGRAM
Complete runnable example
The code below is not pseudocode with explanatory lines omitted. It is a minimal example that can be built and run as a file.
cc -std=c17 -Wall -Wextra -O2 atomic_replace.c -o atomic_replace01#define _GNU_SOURCE
02#include <fcntl.h>
03#include <stdio.h>
04#include <string.h>
05#include <unistd.h>
06
07int main(int argc, char **argv)
08{
09 if (argc != 3)
10 return 2;
11 int dirfd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
12 if (dirfd < 0)
13 return 1;
14
15 const char *tmp = ".replace.tmp";
16 int fd = openat(dirfd, tmp, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0644);
17 if (fd < 0)
18 return 1;
19 size_t length = strlen(argv[2]);
20 if (write(fd, argv[2], length) != (ssize_t)length || fsync(fd) < 0)
21 return 1;
22 if (close(fd) < 0)
23 return 1;
24 if (renameat(dirfd, tmp, dirfd, argv[1]) < 0)
25 return 1;
26 if (fsync(dirfd) < 0)
27 return 1;
28 close(dirfd);
29 return 0;
30}
CODE NOTES
Code notes
open(".", O_RDONLYHolds the same directory fd for both target and temporary file, fixing one base for creation, rename, and directory fsync.
O_CREAT | O_EXCLGuarantees that a new inode is created without overwriting an existing temporary name. A collision requires a unique-name strategy.
fsync(fd)Sends the new file contents and inode metadata toward storage before rename. A successful write alone does not guarantee crash persistence.
renameat(dirfdAtomically replaces the name within one directory on the same mount. A rename across filesystems returns EXDEV.
fsync(dirfd)Synchronizes the directory so the new name and removal of the previous name persist across a crash.
DETAILS
Detailed behavior
Link count and open count are different references
Adding a hard link increments the inode link count but does not change the number of open file descriptions. Even with link count 0 after unlink, data blocks cannot be reclaimed immediately while an open fd or mmap remains.
This is why a file shown as '(deleted)' in /proc/PID/fd can continue occupying disk space.
Readers of a renamed target never see an intermediate file
When a completed temporary inode is renamed onto the target, pathname lookup by another process obtains either the old or new inode. It does not see partially written contents as it could if the target itself were truncated and written.
An fd that already had the old target open continues to refer to the old inode after rename.
Clean up the temporary name on error paths too
If write, fsync, close, or rename fails, unlink the temporary inode. Define signal-safe cleanup and restart policy, and do not use a predictable shared temporary name.
On filesystems that support O_TMPFILE with linkat(AT_EMPTY_PATH), the file can be prepared as an unnamed inode and then published.
OBJECTS
Objects and lifetimes
| Object | Creation and release | Values to inspect |
|---|---|---|
directory entry | Created or changed by link/rename and removed by unlink | name, parent inode, target inode |
inode | Retained while a link or open/mmap reference exists | i_nlink, size, timestamps |
struct file | Refers to the inode independently of pathname changes for the lifetime of an open fd | f_path, f_pos, f_mode |
FAILURE PATH
Failure conditions and common misconceptions
| Observed symptom | Likely causes | How to verify |
|---|---|---|
| Disk space is not reclaimed | A process still has the deleted file open | lsof +L1, /proc/PID/fd |
| The new name is absent after power loss | Directory fsync was omitted | Check documented filesystem durability and perform fault testing |
| rename returns EXDEV | Source and target are on different mounts | Check stat st_dev and findmnt |
LAB
Verify it yourself
- Keep a large file open in a process, unlink it, and inspect lsof +L1 and df.
- While a reader repeatedly opens the target, have a writer replace it by rename and verify that no intermediate length is observed.
- In a virtual machine, compare forced power loss after only file fsync with forced power loss after directory fsync as well.
./atomic_replace config.txt 'new value'strace -e trace=openat,write,fsync,renameat,unlink,close ./atomic_replace config.txt 'new value'PRIMARY REFERENCES