How to change symlinks atomically
Posted by Tom Moertel Mon, 22 Aug 2005 16:00:00 GMT
Many people don’t realize that changing the target of a symbolic link (symlink) is not an atomic operation. “Changing” a symlink really means deleting it and creating a new link with the same file name. For example, if I have a symlink current that points to a directory old, and I want to change it to point to a directory new, I might use the following command:
$ ln -snf new current
Strace shows what really happens when I run the command:
$ strace ln -snf new current 2>&1 | grep link
unlink("current") = 0
symlink("new", "current") = 0
First, the existing symlink is deleted via the unlink system call. Then a new, identically named symlink is created via the symlink system call. It’s a two-step process, and in between the steps, there is no symlink.
This can be a problem if you expect the symlink to be there always, such as when using the link to point to the active version of a live web site. If you change the symlink while deploying a new version of your site, for example, the web server might try to dereference the link during the small window of time when it doesn’t exist. Oops.
The solution to this problem is to effect the change by creating a new symlink and then renaming it over the old symlink. On Unix-like systems, renaming is an atomic operation, and thus the symlink “change” will be atomic too. By hand, the process looks like this:
$ ln -s new current_tmp && mv -Tf current_tmp current
In Ruby, I make atomic symlinking available everywhere by extending the Pathname class with a new method atomic_symlink:
require 'pathname'
class Pathname
def atomic_symlink(old)
suffix = [Array.new(6){rand(256).chr}.join].pack("m").strip.tr('/','_');
tmplink = Pathname.new(self.to_s + "_" + suffix)
tmplink.make_symlink(old)
begin
tmplink.rename(self)
rescue
# if rename fails, we must remove the temporary link manually
File.unlink(tmplink.to_s)
raise
end
end
endThis code is nothing more than a robustified version of the by-hand method. It picks better names for temporary links, and it cleans up after itself, should something go wrong, but otherwise it does the same thing.
Given how easy it is to change symlinks atomically, why do it any other way? Life is hard enough without having to worry about another race condition.

Nice tip, but the manual example doesn’t work – it just moves the current_tmp symlink into the directory pointed to by current.
Any idea how to fix it?
Darn! I forgot the -T flag on mv. Fixed.
Thanks for the catch, Eiki!
Cheers,
Tom
Ah, okay. I’ve been doing it with a line of perl instead, like: perl -e ‘rename(“current_tmp”, “current”)’
But I think I like yours better.
Thanks!
Excellent post! Thanks :)
The ‘-T’ in ‘mv -T’ is kind of recent, an alternative for older systems is the slightly gaudier
‘rename current_tmp current current_tmp’
Also passes the ‘strace’ atomic test in that it calls rename(2) w/o calling unlink(2) first.
3 years down the road and still informative. Thanks for the post.
I think you just saved me a lot of hassle with this post. Sending it though our office now :)
Thanks Tom,
I think this will let me revert back to a libc.so.6 pointing to libc-2.2.5.so rather than libc-3.2.2.so without breaking my system and forcing me to roll out my monitor/kbd/mouse for my KVM switch.
Many thanks,
-Zoid http://www.givemeopensourceordeath.net
Precise answer to a common question, thanks.
Hmm, did you strace the mv -Tf?
strace mv -Tf new old unlink(“old”) = 0 rename(“new”, “old”) = 0
Hmm…
That’s with: mv (GNU coreutils) 5.93
On: Linux XXXXXXXX 2.6.16.46-0.12-bigsmp #1 SMP Thu May 17 14:00:09 UTC 2007 i686 athlon i386 GNU/Linux
@Darin K:
If your new and old files are on the same filesystem, the mv command ought to use the rename(2) system call with no prior unlink(2):
Cheers,
Tom
Should’ve used OS X, IMO. It’s atomic there.
truss ln -nsf new old __sysctl(0x7fffffffe110,0x2,0x7fffffffe12c,0x7fffffffe120,0x0,0x0) = 0 (0x0) mmap(0x0,656,PROT_READ|PROT_WRITE,MAP_ANON,-1,0x0) = 34365186048 (0x800532000) munmap(0x800532000,656) = 0 (0x0) __sysctl(0x7fffffffe180,0x2,0x80063b648,0x7fffffffe178,0x0,0x0) = 0 (0x0) mmap(0x0,32768,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_ANON,-1,0x0) = 34365186048 (0x800532000) issetugid(0x800533015,0x80052cce4,0x800647d30,0x800647d00,0x6331,0x0) = 0 (0x0) open("/etc/libmap.conf",O_RDONLY,0666) ERR#2 'No such file or directory' open("/var/run/ld-elf.so.hints",O_RDONLY,057) = 3 (0x3) read(3,"Ehnt\^A\0\0\0\M^@\0\0\0Z\0\0\0\0"...,128) = 128 (0x80) lseek(3,0x80,SEEK_SET) = 128 (0x80) read(3,"/lib:/usr/lib:/usr/lib/compat:/u"...,90) = 90 (0x5a) close(3) = 0 (0x0) access("/lib/libc.so.7",0) = 0 (0x0) open("/lib/libc.so.7",O_RDONLY,030732440) = 3 (0x3) fstat(3,{ mode=-r--r--r-- ,inode=94247,size=1295416,blksize=16384 }) = 0 (0x0) pread(0x3,0x80063a500,0x1000,0x0,0x101010101010101,0x8080808080808080) = 4096 (0x1000) mmap(0x0,2367488,PROT_NONE,MAP_PRIVATE|MAP_ANON|MAP_NOCORE,-1,0x0) = 34366324736 (0x800648000) mmap(0x800648000,1081344,PROT_READ|PROT_EXEC,MAP_PRIVATE|MAP_FIXED|MAP_NOCORE,3,0x0) = 34366324736 (0x800648000) mmap(0x800850000,126976,PROT_READ|PROT_WRITE,MAP_PRIVATE|MAP_FIXED,3,0x108000) = 34368454656 (0x800850000) mprotect(0x80086f000,110592,PROT_READ|PROT_WRITE) = 0 (0x0) close(3) = 0 (0x0) sysarch(0x81,0x7fffffffe200,0x800537088,0x0,0xffffffffffce0450,0x800663e78) = 0 (0x0) mmap(0x0,640,PROT_READ|PROT_WRITE,MAP_ANON,-1,0x0) = 34365218816 (0x80053a000) munmap(0x80053a000,640) = 0 (0x0) mmap(0x0,43696,PROT_READ|PROT_WRITE,MAP_ANON,-1,0x0) = 34365218816 (0x80053a000) munmap(0x80053a000,43696) = 0 (0x0) sigprocmask(SIG_BLOCK,SIGHUP|SIGINT|SIGQUIT|SIGKILL|SIGPIPE|SIGALRM|SIGTERM|SIGURG|SIGSTOP|SIGTSTP|SIGCONT|SIGCHLD|SIGTTIN|SIGTTOU|SIGIO|SIGXCPU|SIGXFSZ|SIGVTALRM|SIGPROF|SIGWINCH|SIGINFO|SIGUSR1|SIGUSR2,0x0) = 0 (0x0) sigprocmask(SIG_SETMASK,0x0,0x0) = 0 (0x0) __sysctl(0x7fffffffe190,0x2,0x502380,0x7fffffffe188,0x0,0x0) = 0 (0x0) sigprocmask(SIG_BLOCK,SIGHUP|SIGINT|SIGQUIT|SIGKILL|SIGPIPE|SIGALRM|SIGTERM|SIGURG|SIGSTOP|SIGTSTP|SIGCONT|SIGCHLD|SIGTTIN|SIGTTOU|SIGIO|SIGXCPU|SIGXFSZ|SIGVTALRM|SIGPROF|SIGWINCH|SIGINFO|SIGUSR1|SIGUSR2,0x0) = 0 (0x0) sigprocmask(SIG_SETMASK,0x0,0x0) = 0 (0x0) lstat("old",0x7fffffffd780) ERR#2 'No such file or directory' lstat("old",0x7fffffffd780) ERR#2 'No such file or directory' symlink("new","old") = 0 (0x0) sigprocmask(SIG_BLOCK,SIGHUP|SIGINT|SIGQUIT|SIGKILL|SIGPIPE|SIGALRM|SIGTERM|SIGURG|SIGSTOP|SIGTSTP|SIGCONT|SIGCHLD|SIGTTIN|SIGTTOU|SIGIO|SIGXCPU|SIGXFSZ|SIGVTALRM|SIGPROF|SIGWINCH|SIGINFO|SIGUSR1|SIGUSR2,0x0) = 0 (0x0) sigprocmask(SIG_SETMASK,0x0,0x0) = 0 (0x0) sigprocmask(SIG_BLOCK,SIGHUP|SIGINT|SIGQUIT|SIGKILL|SIGPIPE|SIGALRM|SIGTERM|SIGURG|SIGSTOP|SIGTSTP|SIGCONT|SIGCHLD|SIGTTIN|SIGTTOU|SIGIO|SIGXCPU|SIGXFSZ|SIGVTALRM|SIGPROF|SIGWINCH|SIGINFO|SIGUSR1|SIGUSR2,0x0) = 0 (0x0) sigprocmask(SIG_SETMASK,0x0,0x0) = 0 (0x0) process exit, rval = 0Hexley: It doesn’t seem like the symlink old existed when you ran you test. (Note the failing lstat(2) system calls prior to the symlink(2) call.) How would you know, then, whether the symlink gets updated atomically?
This is interesting… I was looking for something on this topic (since I just got bit by doing the simplistic
rm linkname && ln -s newtarget linknameon a busy live web app). So, I tried out bothln -snfandmv -Tfmethods myself.This is on Ubuntu 9.10:
Linux ubuntubox 2.6.31-23-generic #74-Ubuntu SMP Mon Feb 28 22:20:11 UTC 2011 x86_64 GNU/LinuxFor
ln -snf, I see this:So yes, the
Thestraceshows thatunlink()happens beforesymlink()—butls -lishows that the old and new symlinks have the same inode (296110).mv -Tfapproach, on the other hand, does this:So—the
rename()happens in one call, but the inode of the symlink changes (from 296110 to 296118).What I don’t know is: which is better?
For extra credit: why did I get the same inodes for the directories and symlinks when I deleted them and recreated them?? Actually, I suspect that the answer to this question explains why
ln -snfkeeps the same inode.Mike,
The before and after symlinks have the same inode not because of some indivisible connection between them but because the filesystem will recycle freed inodes. For example, if I create a file, delete it, and then create a new file (and no files are created in between) they will often be assigned the same inode:
As to your question about which is better,
rename()is, for the reason documented in its man(2) page:Cheers,
Tom