当前位置: 动力学知识库 > 问答 > 编程问答 >

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

问题描述:

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:

  • Check if a file is already locked using flock()?
  • How to check if a file is locked or not?

网友答案:

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");
}
网友答案:

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.

网友答案:

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.

分享给朋友:
您可能感兴趣的文章:
随机阅读: