Optimal lock file method

Viewed 37770

Windows has an option to open a file with exclusive access rights. Unix doesn't.

In order to ensure exclusive access to some file or device, it is common practice in Unix to use a lock file usually stored in the /var/lock directory.

The C instruction open( "/var/lock/myLock.lock", O_RDWR | O_CREAT | O_EXCL, 0666 ) returns -1 if the lock file already exist, otherwise it creates it. The function is atomic and ensures there is not race condition.

When the resource is released, the lock file is deleted by the following instruction remove( "/var/lock/myLock.lock" ).

There are two problems with this method.

  1. The program may terminate without removing the lock. For instance because it is killed, crashes or whatever. The lock file remains in place, and will prevent any access to the resource even though it is not used anymore.

  2. The lock file is created with group and world write privilege but it is a common practice to configure accounts to use a permission mask that will clear the group and world write permission. Thus, if we had a reliable method to determine that the lock is orphan (not use), a user not owner of the file will not be allowed to remove it.

For the record, I use the lock file to ensure exclusive access to the device connected to the serial port (/dev/ttyUSBx in fact). Advisory method, requiring cooperation, is OK. But exclusive access should be ensured between different users.

Is there a better synchronization method than the lock file? How to determine if the process who created the lock file is still running? How to make it possible for another user to remove the lock file if not in use?

One solution I came up with was to use the file as Unix socket file. If the file exist, try to connect using the file. If it fails, we may assume the owner process of the file is dead. This requires to have a thread looping on socket accept() in the owner process. Unfortunately, the system wouldn't be atomic anymore.

6 Answers

Be careful with lock and release lock functions implemented like mentioned in one of the answers, i.e. like this:

int tryGetLock( char const *lockName )
{
    mode_t m = umask( 0 );
    int fd = open( lockName, O_RDWR|O_CREAT, 0666 );
    umask( m );
    if( fd >= 0 && flock( fd, LOCK_EX | LOCK_NB ) < 0 )
    {
        close( fd );
        fd = -1;
    }
    return fd;
}

and:

void releaseLock( int fd, char const *lockName )
{
    if( fd < 0 )
        return;
    remove( lockName );
    close( fd );
}

The problem is that releaseLock's remove call introduces a race situation bug. Let's consider there are three processes all trying to acquire the exclusive flock with nasty timing:

  • Process #1 has opened the lock file, acquired the flock, and is about to call the unlock function but not yet done that.
  • Process #2 has called open to open the file pointed be lockName, and has got a file descriptor of it, but not yet called flock. That is, the file pointed by lockName is opened two times now.
  • Process #3 is not yet started.

With nasty timing, it is possible that process #1 calls first remove() and close() (the order doesn't matter), and then process #2 calls the flock using the file descriptor already opened, but which is not anymore the file named lockName but a file descriptor that is not linked to any directory entry.

Now, if the process #3 is started, the open() call of it creates the lockName file, and acquires the lock on it as that file is not locked. As an outcome, processes #2 and #3 both think they both own the lock on fileName -> a bug.

The issue in the implementation is that remove() (or more unlink()) only unlinks the name from the directory entry - the file descriptor referring to that file is still usable. One can create then another file having the same name, but still the already opened fd is referring to a different place.

This can be demonstrated adding delay to the lock function:

int tryGetLock( char const *lockName)
{
    mode_t m = umask( 0 );
    int fd = open( lockName, O_RDWR|O_CREAT, 0666 );
    umask( m );
    printf("Opened the file. Press enter to continue...");
    fgetc(stdin);
    printf("Continuing by acquiring the lock.\n");
    if( fd >= 0 && flock( fd, LOCK_EX | LOCK_NB ) < 0 )
    {
        close( fd );
        fd = -1;
    }
    return fd;
}

static const char *lockfile = "/tmp/mylock.lock";

int main(int argc, char *argv[0])
{
    int lock = tryGetLock(lockfile);
    if (lock == -1) {
        printf("Getting lock failed\n");
        return 1;
    }

    printf("Acquired the lock. Press enter to release the lock...");
    fgetc(stdin);

    printf("Releasing...");
    releaseLock(lock, lockfile);
    printf("Done!\n");
    return 0;

}

  1. Try starting process #1, and hit enter once to acquire the lock.
  2. Then start process #2 on another terminal,
  3. Press another enter on the terminal where process #1 is running to release the lock. 4. Continue with process #2 by pressing enter once so that it acquires the lock.
  4. Then open another terminal where to run process #3. In there, press enter once to acquire the lock.

The "impossible" happens: processes #2 and #3 think they both have the exclusive lock.

This might be rare to happen in practice at least with usual applications, but nevertheless the implementation is not correct.

Also, creating a file with mode 0666 might be a security risk.

I don't have "reputation to comment", and this is also quite old question, but people are still referring to this and doing something similar, so that's why to add this note as an answer.

Related