Linux | 一次 flock 实现多进程互斥的踩坑记录


TLDR;在flock和unlink连用的情况下,多进程竞争可能出现 flock 文件锁失效的问题。

背景

flock() 可对一个文件进行加锁,通常用于实现多进程间互斥访问一个文件[1]。

因为有些程序在多实例运行的情况下,会带来不可预期的结果。例如,crontab 中单任务多实例问题(详细解释?)。 flock() 提供的互斥特性也可用来实现程序的单一实例执行:执行程序启动进程 A 获取文件锁并运行,再次执行该程序启动的进程 B 拿不到文件锁就退出或等待锁的释放。我们期望实现的就是上述单一实例运行的目标。

一个简单的利用文件锁实现单一实例运行的错误用法实例如下:

  • 进程启动时尝试打开 lock 文件(不存在则创建)
  • 调用 flock 进行加锁,加锁成功则继续执行应用逻辑,失败则退出
  • 应用逻辑执行完成后进程退出,调用process_exit()清理现场

    • close g_lock_fd
    • 调用 unlink()删除 g_lock_file 文件
#include<stdio.h>
#include <sys/file.h>
#include<unistd.h>

char g_lock_path[] = "/var/run/file.lock";
int g_lock_fd = -1;

int process_exit(void)
{
    if (g_lock_fd != -1) {
        close(g_lock_fd);
        g_lock_fd = -1;
    }

    if (g_lock_path[0]) {
        unlink(g_lock_path);
        g_lock_path[0] = '\0';
    }
    return 0;
}

int main(void)
{
    int rc = 0;

    g_lock_fd = open(g_lock_path, O_RDONLY | O_CREAT, 0600);
    if (g_lock_fd == -1) {
        printf("Cannot open lock file %s\n", g_lock_path);
        return -1;
    }

    rc = flock(g_lock_fd, LOCK_EX | LOCK_NB);
    if (rc != 0) {
        printf("flock file failed, exit.\n");
        return -1;
    }
    
    // app logic

    process_exit();
    return 0;
}

开始踩坑

看完上述代码和对代码基本逻辑的描述,如果您能一眼看出上述“看起来很符合逻辑的代码”存在问题,那您一定是经验丰富的高手,一定要联系我,让我向您多学习学习!在我看来这种实现是完全合理的,完全能防住多实例运行的情况。哈哈,不过你越自信,就越不会怀疑是这里的实现有坑,就会一直找不到原因(不要问我怎么知道的....)。

打住,直接看坑,在下面这种情况下会出现文件锁失效的问题:

  • (1)在进程 A 运行的过程中,进程 B 启动并且打开了文件锁对应的文件(_/var/run/file.lock_)
  • (2)这时又刚好调度到进程 A,进程 A 开始退出逻辑,执行close(g_lock_fd)
  • (3)进程 A 继续执行unlink(g_lock_path);并完全退出
  • (4)进程 B 执行flock() 成功,开始执行应用逻辑
process A              |   process B
running                |
                       |   (1) open lock file 
(2)close(g_lock_fd)    |
(3)unlink(g_lock_path) |
                       |   (4)flock(g_lock_fd, LOCK_EX | LOCK_NB)

到这里还是保证了单实例运行,毕竟在进程 B 开始执行应用逻辑之前,进程 A 已经退出执行应用逻辑了。但是在这种情况下,进程 C 启动将直接获取文件锁成功并进入应用逻辑。

这是因为在进程 B 打开锁文件后,进程 A 执行 unlink 将删除该文件名。虽然当前进程 B 打开了这个文件,但是在文件系统中这个文件名已经被清除了。进程 C 启动时通过 Open 将创建一个新的相同文件名的文件,这是通过 flock 可以直接成功获取锁。(上述涉及到文件系统相关知识,开个新坑下篇文章详细总结一下,这里主要分析问题,不再赘述。)

unlink() deletes a name from the file system. If that name was the last link to a file and no processes have the file open the file is deleted and the space it was using is made available for reuse.

通过复现上述过程,我们可以看到进程 B 中的 fd 3 指向了 /var/run/file.lock 但标记了 delete。此时再执行进程 C ,进程 C 会新建文件并通过 flock 加锁成功并进入应用逻辑。应用的单实例运行完全没保证呀(哭。

root@dingmos:~/workspaces/reproduce# ls /proc/727/fd -lh
total 0
lrwx------ 1 root root 64 Apr 15 12:08 0 -> /dev/pts/5
lrwx------ 1 root root 64 Apr 15 12:08 1 -> /dev/pts/5
l-wx------ 1 root root 64 Apr 15 12:08 19 -> /root/.vscode-server/data/logs/20230415T115923/remoteagent.log
lrwx------ 1 root root 64 Apr 15 12:08 2 -> /dev/pts/5
l-wx------ 1 root root 64 Apr 15 12:08 20 -> /root/.vscode-server/data/logs/20230415T115923/remoteptyhost.log
lrwx------ 1 root root 64 Apr 15 12:08 21 -> /dev/ptmx
lrwx------ 1 root root 64 Apr 15 12:08 22 -> /dev/pts/4
lrwx------ 1 root root 64 Apr 15 12:08 23 -> /dev/ptmx
lrwx------ 1 root root 64 Apr 15 12:08 24 -> /dev/pts/5
lr-x------ 1 root root 64 Apr 15 12:08 25 -> 'pipe:[33106]'
lr-x------ 1 root root 64 Apr 15 12:08 3 -> '/var/run/file.lock (deleted)'

针对上述问题的解决方案:其实就不需要通过 unlink 来主动删除文件,让其一直存在就好了,缺点就只是会一直残留该文件。应该没有其他坑了吧。

参考资料

[1]. 被遗忘的桃源——flock 文件锁
[2]. unlink(2) - Linux man page
[3]. # flock(2) - Linux man page

声明:一丁点儿的网络日志|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - Linux | 一次 flock 实现多进程互斥的踩坑记录


勿在浮沙筑高台,每天进步一丁点儿!