flock(): is it possible to merely check if the file is already locked, without actually acquiring the lock if not?
Asked Answered
B

4

12

My use case is as follows: I have a program that enforces that only one instance of it can be running at any given time, so at startup it always tries to grab hold of a lock file in a standard location, and terminates if the file is already locked. That's all working fine, but now I want to enhance the program with a new command-line option which, when specified, will cause the program to just print out a status report for the program and then terminate (prior to the main lock guard described above), which will include whether the lock file is already locked or not, what the pid of the running process is (if such exists), and some program state queried from a database.

So as you can see, when invoked in this "status report" mode, my program should not actually acquire the lock if it is available. I just want to know if the file is already locked or not, so I can inform the user as part of the status report.

From my searching, there does not appear to be any way of doing this. Rather, the only possible solution seems to be to call flock() with the non-blocking flag, and then, if you actually acquired the lock, you can release it immediately. Something like this:

if (flock(fileno(lockFile), LOCK_EX|LOCK_NB ) == -1) {
    if (errno == EWOULDBLOCK) {
        printf("lock file is locked\n");
    } else {
        // error
    } // end if
} else {
    flock(fileno(lockFile), LOCK_UN );
    printf("lock file is unlocked\n");
} // end if

I suppose it's not such a big deal to acquire the lock and then release it immediately, but I was wondering if there's any better solution out there that doesn't involve a brief and unnecessary acquisition of the lock?

Note: There are already a couple of similar questions whose titles may make it seem like they're identical to this question, but it is clear from the contents of those questions that the OPs are interested in actually writing to the file after acquiring the lock, so this is a distinct question:

Blanc answered 16/3, 2015 at 0:23 Comment(0)
Z
11

You cannot do this reliably. Processes are asynchronous: when you fail to acquire the lock, there is no guarantee that the file will still be locked by the time you print the locked status. Similarly, if you manage to acquire the lock, You then immediately release it, so by the time you print the unlocked status, the file my have been locked by another process. If there are a lot of contenders trying to lock this file, the likelihood of the status message being out of sync is high. Attackers can take advantage of this kind of approximation to penetrate systems.

If you were to rely on this check in a script to perform any kind of concurrent work, all bets are off. If it is just producing an informative status, you should use the past tense in the status messages:

if (flock(fileno(lockFile), LOCK_EX|LOCK_NB) == -1) {
    if (errno == EWOULDBLOCK) {
        printf("lock file was locked\n");
    } else {
        // error
    }
} else {
    flock(fileno(lockFile), LOCK_UN);
    printf("lock file was unlocked\n");
}
Zeller answered 23/3, 2015 at 10:35 Comment(1)
@Blanc This above nails it. Briefly, any such API to poll lock status without attempting to grab it would return a result that is stale as soon as it was computed. This is an inherent design problem; It cannot be fixed. In fact, it's not even possible to do the rest of what you asked atomically: The process owning the lockfile could unlock it and exit after you've seen the file still existing and locked, but before you could probe for the process. Alternatively it could lock the file after you saw it unlocked but before you reported so with the printf.Gudrun
G
6

I don't see what's wrong with the approach of placing a lock on the file and immediately releasing it. In my opinion, you are doing it just as I would do it.

That said, there is another locking API in Unix: fcntl locks. See man fcntl on Linux. It has F_SETLK to acquire or release a lock, and F_GETLK to test whether a lock can be placed. The fcntl locks are slightly different that flock locks: they are advisory record locks placed on a region of the file, not for the whole file.

There is a third api too: lockf(3). You can use F_LOCK to lock a file, and F_TEST to test if the file region can be locked. The lockf(3) API has been implemented as a wrapper on top of fcntl(2) locking on Linux, but that may not be true on other operating systems.

Gasaway answered 23/3, 2015 at 10:33 Comment(1)
I don't see what's wrong with the approach of placing a lock on the file and immediately releasing it - the problem is that then, for a split second, something else trying to lock it with LOCK_EX|LOCK_NB will fail to get the lock. if program1 starts with if(!flock(fp,LOCK_EX|LOCK_NB)){die("already running! this program can only run a single instance at a time!");} then there is a split second where the program will fail to run if program2 want to just check if program1 is running or not..Sick
R
6

Do not use flock(). It does not work reliably if the lock file directory happens to be a network filesystem (for example, NFS) and the OS you're using does not implement flock() using fcntl() advisory record locking.

(For example, in current Linux systems, flock() and fcntl() locks are separate and do not interact on local files, but do interact on files residing on NFS filesystems. It is not that strange to have /var/lock on an NFS filesystem in server clusters, especially failover and web server systems, so this is, in my opinion, a real issue you should consider.)

Edited to add: If for some external reason you are constrained to use flock(), you can use flock(fd, LOCK_EX|LOCK_NB) to try to obtain the exclusive lock. This call will never block (wait for the lock to be released), but will fail with -1 and errno == EWOULDBLOCK if the file is already locked. Similar to the fcntl() locking scheme explained in detail below, you try to obtain the exclusive lock (without blocking); if successful, you keep the lock file descriptor open, and let the operating system release the lock automatically when the process exits. If the nonblocking lock fails, you must choose whether you will abort, or proceed anyway.

You can accomplish your goals by using POSIX.1 functions and fcntl() advisory record locks (covering the entire file). The semantics are standard across all POSIXy systems, so this approach will work on all POSIXy and unix-like systems.

Features of fcntl() locks are simple, but nonintuitive. When any descriptor referring to the lock file is closed, the advisory locks on that file are released. When the process exits, the advisory locks on all open files are automatically released. Locks are maintained across an exec*(). Locks are not inherited via fork(), nor are they released in the parent (even when marked close-on-exec). (If the descriptors are close-on-exec, then they will be automatically closed in the child process. Otherwise the child process will have an open descriptor to the file, but not any fcntl() locks. Closing the descriptors in the child process will not affect the parent's lock on the file.)

Therefore the correct strategy is very simple: Open the lock file exactly once, and use fcntl(fd,F_SETLK,&lock) to place an exclusive all-file advisory lock without blocking: if there is a conflicting lock, it will fail immediately, instead of blocking until the lock can be acquired. Keep the descriptor open, and let the operating system auto-release the lock when your process exits.

For example:

#define _POSIX_C_SOURCE 200809L
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

/* Open and exclusive-lock file, creating it (-rw-------)
 * if necessary. If fdptr is not NULL, the descriptor is
 * saved there. The descriptor is never one of the standard
 * descriptors STDIN_FILENO, STDOUT_FILENO, or STDERR_FILENO.
 * If successful, the function returns 0.
 * Otherwise, the function returns nonzero errno:
 *     EINVAL: Invalid lock file path
 *     EMFILE: Too many open files
 *     EALREADY: Already locked
 * or one of the open(2)/creat(2) errors.
*/
static int lockfile(const char *const filepath, int *const fdptr)
{
    struct flock lock;
    int used = 0; /* Bits 0 to 2: stdin, stdout, stderr */
    int fd;

    /* In case the caller is interested in the descriptor,
     * initialize it to -1 (invalid). */
    if (fdptr)
        *fdptr = -1;

    /* Invalid path? */
    if (filepath == NULL || *filepath == '\0')
        return errno = EINVAL;

    /* Open the file. */
    do {
        fd = open(filepath, O_RDWR | O_CREAT, 0600);
    } while (fd == -1 && errno == EINTR);
    if (fd == -1) {
        if (errno == EALREADY)
            errno = EIO;
        return errno;
    }

    /* Move fd away from the standard descriptors. */
    while (1)
        if (fd == STDIN_FILENO) {
            used |= 1;
            fd = dup(fd);
        } else
        if (fd == STDOUT_FILENO) {
            used |= 2;
            fd = dup(fd);
        } else
        if (fd == STDERR_FILENO) {
            used |= 4;
            fd = dup(fd);
        } else
            break;

    /* Close the standard descriptors we temporarily used. */
    if (used & 1)
        close(STDIN_FILENO);
    if (used & 2)
        close(STDOUT_FILENO);
    if (used & 4)
        close(STDERR_FILENO);

    /* Did we run out of descriptors? */
    if (fd == -1)
        return errno = EMFILE;    

    /* Exclusive lock, cover the entire file (regardless of size). */
    lock.l_type = F_WRLCK;
    lock.l_whence = SEEK_SET;
    lock.l_start = 0;
    lock.l_len = 0;
    if (fcntl(fd, F_SETLK, &lock) == -1) {
        /* Lock failed. Close file and report locking failure. */
        close(fd);
        return errno = EALREADY;
    }

    /* Save descriptor, if the caller wants it. */
    if (fdptr)
        *fdptr = fd;

    return 0;
}

The reason the above makes sure it does not accidentally reuse a standard descriptor, is because I've been bitten by it in a very rare case. (I wanted to exec an user-specified process while holding a lock, but redirecting the standard input and output to currently controlling terminal.)

The use is very simple:

    int result;

    result = lockfile(YOUR_LOCKFILE_PATH, NULL);
    if (result == 0) {
        /* Have an exclusive lock on YOUR_LOCKFILE_PATH */
    } else
    if (result == EALREADY) {
        /* YOUR_LOCKFILE_PATH is already locked by another process */
    } else {
        /* Cannot lock YOUR_LOCKFILE_PATH, see strerror(result). */
    }

Edited to add: I used internal linkage (static) for the above function just out of habit. If the lock file is user-specific, it should use ~/.yourapplication/lockfile; if it is system-wide, it should use e.g. /var/lock/yourapplication/lockfile. I have a habit of keeping the functions related to this kind of initialization stuff, including defining/building the lockfile path etc. as well automatic plugin registration function (using opendir()/readdir()/dlopen()/dlsym()/closedir()), in the same file; the lockfile function tends to be called internally (by the function that builds the lockfile path), and thus ends up having internal linkage.

Feel free to use, reuse, or modify the function as you wish; I consider it to be in public domain, or licensed under CC0 where public domain dedication is not possible.

The descriptor is "leaked" intentionally, so that it will be closed (and the lock on it released) by the operating system when the process exits, but not before.

If there is a lot of post-work cleanups your process does, during which you do wish to allow another copy of this process, you can retain the descriptor, and just close(thatfd) at the point where you wish to release the lock.

Rugged answered 28/3, 2015 at 4:33 Comment(4)
Impressive function! I appreciate the knowledge. Couple of questions for you: (1) Why did you use internal linkage when defining the function? Wouldn't this be a suitable function for a reusable library, and thus it should have external linkage? (2) Can you address my original question with respect to the fcntl method of locking? IOW, is it possible to use fcntl-style locking to merely check if the file is already locked, without actually acquiring the lock if not?Blanc
@bgoldst: (1) Out of habit. I tend to have this kind of function called by a initialization function in the same source file.Rugged
@bgoldst: (2) No, you cannot merely check if the file is locked (although you could if you used fcntl() advisory record locks). However, you don't want to. What you can do, is try to acquire the lock, without blocking (flock(fd,LOCK_EX|LOCK_UN)). If the lock can be placed, it will succeed. If there is already a conflicting lock on the file, it will fail with -1 and errno==EWOULDBLOCK, and you need to decide whether you want to continue without the lock or not. The locking logic is exactly the same as above when using fcntl() locks: You try to get the lock in all cases.Rugged
@NominalAnimal the surprise/disruption of flock() not working on file shares is probably way less surprising than the lock being released because some other thread under the process (or your own thread) opened the file and closed it which resulted in the lock being released as it is under lockf. It is probably misleading to lead with don't use flock.Pyrogallol
I
0
LockFile(File* hLockfile)
{
    flock(fileno(hLockfile), LOCK_EX); // lock it exclusively
}

UnlockFile(File *hLockfile)
{
    flock(fileno(hLockfile), LOCK_UN); // remove the exclusive lock
}

bool IsLocked(const char* filename)
{
     bool b = false;
     FILE *fp = fopen(filename, "r+b");
     if (fp != NULL)
     {
          // Test it with a shared lock (so all "testing" processes will succeed unless the exclusive lock is in place)
          int nLockF = flock(fileno(fp), LOCK_SH | LOCK_NB); 
          if (nLockF == 0)
          {
               // we could lock it, so it wasn't locked exclusively
               b = false;
               // release the shared lock
               flock(fileno(fp), LOCK_UN);
          }
          else
          {
               // we couldn't lock it, so it was locked exclusively
               b = true;
          }
          fclose(fp);
     }
     else
     {
         b = false;
     }
     return b;
}
Insomnolence answered 22/7 at 18:45 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Unprejudiced

© 2022 - 2024 — McMap. All rights reserved.