File Descriptor / I/O · Linux userspace / kernel ABI

inode, link, unlink, atomic rename

Separates pathname and inode lifetimes, and lays out atomic replacement and durability procedures using unlink on open files and temporary-file rename.

Series
14 / 38
Build
cc -std=c17 -Wall -Wextra -O2 atomic_replace.c -o atomic_replace
Run
./atomic_replace config.txt 'new value'
Kernel
Linux 6.18.37 LTS

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 diagram

Figure 1. Directory-entry and open-fd targets before and after rename

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

Figure 2. From userspace code to observable results
open temp same directory
write loop write new contents
fsync file flush data/metadata
renameat atomically replace name
fsync dir persist directory change

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.

Figure 3. Major points along the kernel-internal path
filename_lookup old/new parent
vfs_rename locks and permissions
fs rename change directory entries
d_move update dcache
fsync writeback/barrier

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.

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.

FileFunction / structureWhat 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 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.

Buildcc -std=c17 -Wall -Wextra -O2 atomic_replace.c -o atomic_replace
01#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

Source line 11open(".", O_RDONLY

Holds the same directory fd for both target and temporary file, fixing one base for creation, rename, and directory fsync.

Source line 16O_CREAT | O_EXCL

Guarantees that a new inode is created without overwriting an existing temporary name. A collision requires a unique-name strategy.

Source line 20fsync(fd)

Sends the new file contents and inode metadata toward storage before rename. A successful write alone does not guarantee crash persistence.

Source line 24renameat(dirfd

Atomically replaces the name within one directory on the same mount. A rename across filesystems returns EXDEV.

Source line 26fsync(dirfd)

Synchronizes the directory so the new name and removal of the previous name persist across a crash.

Detailed behavior

01

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.

02

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.

03

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 and lifetimes

ObjectCreation and releaseValues to inspect
directory entryCreated or changed by link/rename and removed by unlinkname, parent inode, target inode
inodeRetained while a link or open/mmap reference existsi_nlink, size, timestamps
struct fileRefers to the inode independently of pathname changes for the lifetime of an open fdf_path, f_pos, f_mode

Failure conditions and common misconceptions

Observed symptomLikely causesHow to verify
Disk space is not reclaimedA process still has the deleted file openlsof +L1, /proc/PID/fd
The new name is absent after power lossDirectory fsync was omittedCheck documented filesystem durability and perform fault testing
rename returns EXDEVSource and target are on different mountsCheck stat st_dev and findmnt

Verify it yourself

  1. Keep a large file open in a process, unlink it, and inspect lsof +L1 and df.
  2. While a reader repeatedly opens the target, have a writer replace it by rename and verify that no intermediate length is observed.
  3. In a virtual machine, compare forced power loss after only file fsync with forced power loss after directory fsync as well.
Run./atomic_replace config.txt 'new value'
Tracestrace -e trace=openat,write,fsync,renameat,unlink,close ./atomic_replace config.txt 'new value'

Primary sources