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
Comments | NOTHING