【问题标题】:flock(): is it possible to merely check if the file is already locked, without actually acquiring the lock if not?flock():是否可以仅检查文件是否已被锁定,如果没有则实际获取锁定?
【发布时间】:2015-03-16 00:23:43
【问题描述】:

我的用例如下:我有一个程序强制在任何给定时间只能运行它的一个实例,因此在启动时它总是试图抓住一个标准位置的锁文件,然后终止如果文件已经被锁定。一切正常,但现在我想使用一个新的命令行选项来增强程序,当指定该选项时,将导致程序只打印出程序的 status report 然后终止(在上面描述的主锁守卫之前),这将包括锁文件是否已经被锁定,正在运行的进程的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 有兴趣在之后实际写入文件获取锁,所以这是一个独特的问题:

【问题讨论】:

    标签: c locking flock


    【解决方案1】:

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

    【讨论】:

    • @bgoldst 上面的内容很到位。简而言之,任何这样的 API 来轮询锁定状态而不尝试获取它都会返回一个在计算后立即失效的结果。这是一个固有的设计问题;它无法修复。事实上,甚至不可能自动完成您所要求的其余部分:拥有锁定文件的进程可以在您看到文件仍然存在并锁定后解锁它并退出,但在您可以探测该进程之前。或者,它可以在您看到文件解锁之后但在您使用 printf 报告之前锁定文件。
    【解决方案2】:

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

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

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

    【讨论】:

    • I don't see what's wrong with the approach of placing a lock on the file and immediately releasing it - 问题是,在瞬间,其他试图用 LOCK_EX|LOCK_NB 锁定它的东西将无法获得锁定。如果 program1 以 if(!flock(fp,LOCK_EX|LOCK_NB)){die("already running! this program can only run a single instance at a time!");} 开头,那么如果 program2 只想检查 program1 是否正在运行,那么程序将在瞬间运行失败..
    【解决方案3】:

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

    (例如,在当前的 Linux 系统中,flock()fcntl() 锁是分开的,不会对本地文件进行交互,但会对驻留在 NFS 文件系统上的文件进行交互。拥有/var/lock 并不奇怪在服务器集群中的 NFS 文件系统上,尤其是故障转移和 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。我有保留与这种初始化相关的功能的习惯,包括定义/构建锁文件路径等以及自动插件注册功能(使用opendir()/readdir()/dlopen()/dlsym()/@ 987654350@),在同一个文件中; lockfile 函数倾向于在内部调用(由构建 lockfile 路径的函数),因此最终具有内部链接。

    您可以随意使用、重用或修改该功能;我认为它属于公共领域,或在无法用于公共领域的情况下以CC0 许可。

    描述符是故意“泄露”的,以便在进程退出时(而不是之前)由操作系统关闭(并释放其上的锁)。

    如果您的进程进行了大量的工作后清理,在此期间您确实希望允许此进程的另一个副本,您可以保留描述符,并在您希望释放锁。

    【讨论】:

    • 令人印象深刻的功能!我很欣赏这些知识。给你几个问题:(1)你为什么在定义函数时使用内部链接?这不是可重用库的合适功能,因此它应该具有外部链接吗? (2) 你能解决我关于fcntl 锁定方法的原始问题吗? IOW,是否可以使用fcntl-style 锁定来仅检查文件是否已锁定,如果没有实际获取锁定?
    • @bgoldst:(1)出于习惯。我倾向于在同一个源文件中由初始化函数调用这种函数。
    • @bgoldst: (2) 不,你不能仅仅检查文件是否被锁定(尽管你可以使用fcntl()咨询记录锁定)。但是,您不想这样做。您可以做的是 try 获取锁,而不会阻塞 (flock(fd,LOCK_EX|LOCK_UN))。如果能放上锁,就成功了。如果文件上已经有一个冲突的锁,它会以-1errno==EWOULDBLOCK 失败,您需要决定是否要在没有锁的情况下继续。使用fcntl()锁时的锁定逻辑与上面完全相同:您尝试在所有情况下获取锁。
    • @NominalAnimal 无法处理文件共享的flock() 的意外/中断可能比释放锁要少得多,因为进程下的某个其他线程(或您自己的线程)打开了文件并关闭它导致锁在lockf下被释放。使用不使用羊群可能会产生误导。
    猜你喜欢
    • 2012-04-09
    • 2012-10-13
    • 1970-01-01
    • 1970-01-01
    • 2016-04-25
    • 1970-01-01
    • 1970-01-01
    • 2012-12-23
    • 1970-01-01
    相关资源
    最近更新 更多