flock():是否可以仅检查文件是否已被锁定,如果没有实际获取锁定?

时间:2015-03-16 00:23:44

标签: c locking flock

我的用例如下:我有一个程序强制在任何给定时间只能运行它的一个实例,因此在启动时它总是试图抓住标准位置的锁文件,并终止如果文件已被锁定。这一切都运行良好,但现在我想用一个新的命令行选项来增强程序,当指定时,将导致程序只打印出程序的状态报告然后终止(在上面描述的主锁定保护之前),其将包括锁定文件是否已被锁定,正在运行的进程的pid是什么(如果存在),以及从数据库查询的某些程序状态。

所以你可以看到,在这个"状态报告"模式,我的程序应实际获取锁定(如果可用)。我只是想知道文件是否已被锁定,因此我可以将其作为状态报告的一部分通知用户。

从我的搜索中,似乎没有办法做到这一点。相反,唯一可能的解决方案似乎是使用非阻塞标志调用flock(),然后,如果您实际获得了锁,则可以立即释放它。像这样:

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

我认为获得锁定然后立即释放它并不是一件大事,但我想知道是否有更好的解决方案,并没有涉及简短的不必要的锁定获取?

注意:已经有几个类似的问题,其标题可能会使它看起来与这个问题相同,但从这些问题的内容中可以清楚地看出,OP有兴趣实际写入获取锁定后的文件,所以这是一个独特的问题:

3 个答案:

答案 0 :(得分:9)

你不能可靠。进程是异步的:当您无法获取锁定时,无法保证在您打印locked状态时文件仍会被锁定。同样,如果你设法获得锁定,那么你会立即释放它,所以当你打印unlocked状态时,我的文件被另一个进程锁定了。如果有很多竞争者试图锁定此文件,则状态消息不同步的可能性很高。攻击者可以利用这种近似来穿透系统。

如果你依靠这个检查脚本来执行任何类型的并发工作,那么所有的赌注都会被取消。如果它只是产生信息状态,您应该在状态消息中使用过去时:

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");
}

答案 1 :(得分:5)

我没有看到锁定文件并立即释放它的方法有什么问题。在我看来,你就像我一样。

也就是说,Unix中还有另一个锁定API:fcntl锁定。请参阅Linux上的man fcntl。它有F_SETLK获取或释放锁,F_GETLK来测试是否可以放置锁。 fcntl锁与flock锁稍有不同:它们是放置在文件区域上的咨询记录锁,而不是整个文件。

还有第三个api:lockf(3)。您可以使用F_LOCK来锁定文件,并使用F_TEST来测试是否可以锁定文件区域。 lockf(3) API已作为Linux上fcntl(2)锁定之上的包装器实现,但在其他操作系统上可能不是这样。

答案 2 :(得分:4)

请勿使用flock()。如果锁定文件目录恰好是网络文件系统(例如,NFS)并且您使用的操作系统未使用flock()建议记录锁定实现fcntl(),则它无法可靠地工作。 / p>

(例如,在当前的Linux系统中,flock()fcntl()锁是独立的,不在本地文件上交互,但在驻留在NFS文件系统上的文件上进行交互。这并不奇怪在服务器群集中的NFS文件系统上有/var/lock,特别是故障转移和Web服务器系统,所以在我看来,这是一个你应该考虑的真正问题。)

编辑添加:如果由于某些外部原因限制您使用flock(),则可以使用flock(fd, LOCK_EX|LOCK_NB)尝试获取独占锁定。此调用永远不会阻塞(等待释放锁定),但如果文件已被锁定,则会失败并返回-1和errno == EWOULDBLOCK。与下面详细说明的fcntl()锁定方案类似,您尝试获取独占锁(不阻塞);如果成功,则保持锁定文件描述符处于打开状态,并在进程退出时让操作系统自动释放锁定。如果非阻塞锁定失败,您必须选择是中止还是继续。

您可以使用POSIX.1函数和fcntl()咨询记录锁(覆盖整个文件)来实现目标。语义是所有POSIXy系统的标准,因此这种方法适用于所有POSIXy和类Unix系统。

fcntl()锁的功能很简单,但不直观。当关闭锁定文件的任何描述符时,将释放该文件上的建议锁定。进程退出时,将自动释放所有打开文件的建议锁定。锁定在exec*()内维护。锁不会通过fork()继承,也不会在父级中释放(即使标记为close-on-exec)。 (如果描述符是close-on-exec,那么它们将在子进程中自动关闭。否则子进程将具有文件的开放描述符,但不具有任何fcntl()锁。关闭描述符。子进程不会影响父对文件的锁定。)

因此,正确的策略非常简单:只打开锁定文件一次,并使用fcntl(fd,F_SETLK,&lock)放置一个独占的全文件建议锁而不会阻塞:如果存在冲突锁定,它将立即失败,而是阻塞直到可以获得锁定。保持描述符处于打开状态,让操作系统在您的进程退出时自动释放锁定。

例如:

#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;
}

上述原因确保它不会意外地重复使用标准描述符,因为我在极少数情况下被它咬过。 (我想在持有锁的同时执行用户指定的进程,但是将标准输入和输出重定向到当前控制的终端。)

使用非常简单:

    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). */
    }

编辑添加:我出于习惯而使用内部链接(static)来完成上述功能。如果锁定文件是特定于用户的,则应使用~/.yourapplication/lockfile;如果它是系统范围的,它应该使用例如/var/lock/yourapplication/lockfile。我习惯保持与这种初始化相关的函数,包括定义/构建lockfile路径等以及自动插件注册功能(使用opendir() / readdir() / dlopen() / dlsym() / closedir()),在同一个文件中;锁文件函数往往在内部调用(通过构建锁文件路径的函数),因此最终会有内部链接。

随意使用,重复使用或修改功能;我认为它属于公共领域,或者在CC0下获得许可,但公共领域的奉献是不可能的。

描述符是&#34;泄露&#34;故意,以便在进程退出时,操作系统将关闭(并释放锁定),但不是之前。

如果您的流程有很多后期工作清理,在此期间您确实希望允许此流程的另一个副本,则可以保留描述符,并在您希望的位置close(thatfd)释放锁。