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

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

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

因此如您所见,在这种 "status report" 模式下调用时,我的程序应该 而不是 实际获取锁(如果它可用)。我只想知道文件是否已被锁定,所以我可以在状态报告中通知用户。

根据我的搜索,似乎没有任何方法可以做到这一点。相反,唯一可能的解决方案似乎是使用 non-blocking 标志调用 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 有兴趣实际写信给获取锁后的文件,所以这是一个独特的问题:

我看不出锁定文件并立即释放它的方法有什么问题。在我看来,你做的和我做的一样。

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

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

你无法做到这一点可靠。进程是异步的:当您未能获得锁时,无法保证在您打印 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");
}

不要使用 flock()。如果锁定文件目录恰好是网络文件系统(例如 NFS)并且您正在使用的 OS 未实现 flock() 使用 fcntl() 建议记录锁定,则它无法可靠地工作.

(例如,在当前的 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() 锁的特点很简单,但不直观。当引用锁定文件的 any 描述符关闭时,该文件上的咨询锁被释放。当进程退出时,所有打开文件上的建议锁都会自动释放。锁在 exec*() 中维护。锁不会通过 fork() 继承,也不会在父级中释放(即使标记为 close-on-exec)。 (如果描述符是 close-on-exec,那么它们将在子进程中自动关闭。否则子进程将有一个打开的文件描述符,但没有任何 fcntl() 锁。关闭描述符子进程不会影响父进程对文件的锁定。)

因此正确的策略很简单:打开锁文件exactly once,然后使用fcntl(fd,F_SETLK,&lock)放置一个独占的all-file advisory 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 == '[=10=]')
        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。我习惯保留与这种初始化相关的功能,包括 defining/building 锁定文件路径等以及自动插件注册功能(使用 opendir()/readdir()/dlopen()/dlsym()/closedir()), 在同一个文件中; lockfile 函数倾向于在内部调用(由构建 lockfile 路径的函数),因此最终具有内部链接。

随意使用、重用或修改功能;我认为它在 public 域中,或者在 CC0 下获得许可,其中 public 域奉献是不可能的。

描述符是 "leaked" 有意设计的,以便在进程退出时由操作系统关闭(并释放其上的锁),而不是在此之前。

如果您的进程进行了大量 post 工作清理,在此期间您确实希望允许该进程的另一个副本,您可以保留描述符,并且只需 close(thatfd)您希望释放锁定的位置。