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
end

This 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.

Posted in , ,
Tags , ,
15 comments
no trackbacks
Reddit Delicious

Comments

  1. Eiki Martinson said 778 days later:

    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?

  2. Tom Moertel said 778 days later:

    Darn! I forgot the -T flag on mv. Fixed.

    Thanks for the catch, Eiki!

    Cheers,
    Tom

  3. Eiki Martinson said 819 days later:

    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!

  4. anon said 925 days later:

    Excellent post! Thanks :)

  5. anon said 955 days later:

    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.

  6. Josh said 1222 days later:

    3 years down the road and still informative. Thanks for the post.

  7. H. Kolk said 1308 days later:

    I think you just saved me a lot of hassle with this post. Sending it though our office now :)

  8. Zoid said 1415 days later:

    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

  9. Aymeric said 1911 days later:

    Precise answer to a common question, thanks.

  10. Darin K said 1925 days later:

    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

  11. Tom Moertel said 1925 days later:

    @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):

    $ mkdir 1 2
    $ ln -snf 1 old
    $ ln -snf 2 new
    $ strace mv -Tf new old 2>&1 | egrep 'old|new'
    execve("/bin/mv", ["mv", "-Tf", "new", "old"], [/* 46 vars */]) = 0
    lstat("new", {st_mode=S_IFLNK|0777, st_size=1, ...}) = 0
    lstat("old", {st_mode=S_IFLNK|0777, st_size=1, ...}) = 0
    rename("new", "old")                    = 0
    
    $ rpm -qf /bin/mv
    coreutils-8.5-7.fc14.x86_64
    
    $ uname -or
    2.6.35.6-48.fc14.x86_64 GNU/Linux
    

    Cheers,
    Tom

  12. Hexley said 2040 days later:

    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 = 0
  13. Tom Moertel said 2040 days later:

    Hexley: 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?

  14. Mike Matthews said 2075 days later:

    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 linkname on a busy live web app). So, I tried out both ln -snf and mv -Tf methods 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/Linux

    For ln -snf, I see this:

    
    $ rm -rf * && mkdir foo1 foo2 && ln -s foo1 foo && ls -li 
    total 8
    296110 lrwxrwxrwx 1 nobody nobody    4 2011-04-28 12:13 foo -> foo1
    368815 drwxr-xr-x 2 nobody nobody 4096 2011-04-28 12:13 foo1
    377007 drwxr-xr-x 2 nobody nobody 4096 2011-04-28 12:13 foo2
    
    $ strace ln -snf foo2 foo 2>&1 | grep foo 
    execve("/bin/ln", ["ln", "-snf", "foo2", "foo"], [/* 36 vars */]) = 0
    lstat("foo", {st_mode=S_IFLNK|0777, st_size=4, ...}) = 0
    lstat("foo", {st_mode=S_IFLNK|0777, st_size=4, ...}) = 0
    stat("foo2", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
    symlink("foo2", "foo")                  = -1 EEXIST (File exists)
    unlink("foo")                           = 0
    symlink("foo2", "foo")                  = 0
    
    $ ls -li 
    total 8
    296110 lrwxrwxrwx 1 nobody nobody    4 2011-04-28 12:13 foo -> foo2
    368815 drwxr-xr-x 2 nobody nobody 4096 2011-04-28 12:13 foo1
    377007 drwxr-xr-x 2 nobody nobody 4096 2011-04-28 12:13 foo2
    
    

    So yes, the strace shows that unlink() happens before symlink()—but ls -li shows that the old and new symlinks have the same inode (296110).

    The mv -Tf approach, on the other hand, does this:
    
    $ rm -rf * && mkdir foo1 foo2 && ln -s foo1 foo && ls -li 
    total 8
    296110 lrwxrwxrwx 1 nobody nobody    4 2011-04-28 12:13 foo -> foo1
    368815 drwxr-xr-x 2 nobody nobody 4096 2011-04-28 12:13 foo1
    377007 drwxr-xr-x 2 nobody nobody 4096 2011-04-28 12:13 foo2
    
    $ ln -s foo2 foo.new && ls -li 
    total 8
    296110 lrwxrwxrwx 1 nobody nobody    4 2011-04-28 12:13 foo -> foo1
    368815 drwxr-xr-x 2 nobody nobody 4096 2011-04-28 12:13 foo1
    377007 drwxr-xr-x 2 nobody nobody 4096 2011-04-28 12:13 foo2
    296118 lrwxrwxrwx 1 nobody nobody    4 2011-04-28 12:13 foo.new -> foo2
    
    $ strace mv -Tf foo.new foo 2>&1 | grep foo 
    execve("/bin/mv", ["mv", "-Tf", "foo.new", "foo"], [/* 36 vars */]) = 0
    lstat("foo.new", {st_mode=S_IFLNK|0777, st_size=4, ...}) = 0
    lstat("foo", {st_mode=S_IFLNK|0777, st_size=4, ...}) = 0
    rename("foo.new", "foo")                = 0
    
    $ ls -li
    total 8
    296118 lrwxrwxrwx 1 nobody nobody    4 2011-04-28 12:13 foo -> foo2
    368815 drwxr-xr-x 2 nobody nobody 4096 2011-04-28 12:13 foo1
    377007 drwxr-xr-x 2 nobody nobody 4096 2011-04-28 12:13 foo2
    
    

    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 -snf keeps the same inode.

  15. Tom Moertel said 2075 days later:

    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:

    $ touch foo
    $ stat foo  # Inode: 4981351
    
    $ rm foo
    $ touch bar
    $ stat bar  # Inode: 4981351
    

    As to your question about which is better, rename() is, for the reason documented in its man(2) page:

    If newpath already exists it will be atomically replaced (subject to a few conditions; see ERRORS below), so that there is no point at which another process attempting to access newpath will find it missing.

    Cheers,
    Tom

Trackbacks

Use the following link to trackback from your own site:
http://blog.moertel.com/articles/trackback/49

(leave url/email »)

   Comment Markup Help Preview comment