rename(2) is atomic on any filesystem worth using. The POSIX spec says so,
and every production filesystem you’d bother running enforces it. If src and
dst are on the same filesystem, rename(src, dst) replaces dst in a
single VFS operation: no window where a reader opens the path and finds nothing.
What rename(2) can’t do: refuse to overwrite if the destination already
exists, or swap two paths without either disappearing. Those two gaps are why
renameat2(2) exists.
Synopsis
#include <fcntl.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>
/* glibc wrapper added in 2.28; use syscall(2) on older systems */
int renameat2(int olddirfd, const char *oldpath,
int newdirfd, const char *newpath,
unsigned int flags);
olddirfd and newdirfd follow the same AT_FDCWD convention as openat(2):
pass AT_FDCWD to use the process’s current directory. flags is where this
gets interesting.
RENAME_NOREPLACE
renameat2(AT_FDCWD, "incoming/new-config",
AT_FDCWD, "etc/app.conf",
RENAME_NOREPLACE);
Fails with EEXIST if etc/app.conf already exists. rename(2) would
silently clobber it. This is the right flag any time you want an atomic “move
this file into place only if nothing is there yet,” without the
check-then-rename race that an access(2) + rename(2) pair has.
RENAME_EXCHANGE
renameat2(AT_FDCWD, "etc/app.conf.new",
AT_FDCWD, "etc/app.conf",
RENAME_EXCHANGE);
Both paths swap names in a single VFS operation. etc/app.conf now contains
what etc/app.conf.new had; etc/app.conf.new now contains the old live
config. Both must exist before the call; if either is missing you get ENOENT.
No window exists where a reader opening etc/app.conf finds nothing. No window
where the process gets an incomplete write, because you wrote the new file
fully before the swap. After the call you have the old config sitting in
.new, ready for inspection or deletion. This is the correct way to hot-swap a
config file and it has been available since kernel 3.15.
Minimal C example for a deploy tool:
#include <fcntl.h>
#include <sys/syscall.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
/* Write new config to etc/app.conf.new first, then: */
int swap_config(void) {
int r = syscall(SYS_renameat2,
AT_FDCWD, "etc/app.conf.new",
AT_FDCWD, "etc/app.conf",
2 /* RENAME_EXCHANGE */);
if (r < 0) {
perror("renameat2 EXCHANGE");
return -1;
}
/* etc/app.conf.new now holds the previous config */
unlink("etc/app.conf.new");
return 0;
}
Pass 2 literally if you don’t have <linux/fs.h> handy. RENAME_NOREPLACE
is 1. RENAME_EXCHANGE is 2. They are not combinable.
The glibc situation
glibc didn’t grow a renameat2 wrapper until 2.28 (August 2018). Anything
running glibc 2.27 or older (RHEL 7, Ubuntu 16.04, any container image that
hasn’t been updated since the Obama administration) needs syscall(2) directly:
#include <sys/syscall.h>
#include <unistd.h>
#ifndef __NR_renameat2
# if defined(__x86_64__)
# define __NR_renameat2 316
# elif defined(__aarch64__)
# define __NR_renameat2 276
# else
# error "unknown __NR_renameat2 for this arch"
# endif
#endif
static inline int sys_renameat2(int olddfd, const char *old,
int newdfd, const char *new,
unsigned flags) {
return (int)syscall(__NR_renameat2, olddfd, old, newdfd, new, flags);
}
Check arch/x86/entry/syscalls/syscall_64.tbl in the kernel source for your
architecture’s number if you’re targeting something other than x86-64 or arm64.
What filesystems support it
ext4, XFS, btrfs, tmpfs, and overlayfs all support RENAME_EXCHANGE. FAT,
exFAT, and most network filesystems do not; you get EINVAL or ENOSYS.
NFSv3 has no atomic exchange primitive at the protocol level. If you’re
deploying over NFS you’re still stuck with the write-to-tmp-then-rename pattern
and accepting the overwrite.
When not to bother
If you’re just doing a write-to-tmp-then-rename and don’t care about the old
file, plain rename(2) is fine. The atomicity guarantee is the same. Use
RENAME_EXCHANGE when you need the old copy intact after the swap, or when
losing the destination for any window would be wrong. Use RENAME_NOREPLACE
when idempotency requires detecting an existing file rather than clobbering it.
For more on related syscalls in this space, the copy_file_range(2) tip covers
kernel-side file copying and mount_setattr(2) covers live mount flag
changes, both of which pair naturally with deployment tooling.
