flock是Linux系统提供的一个系统级的锁,可以用作进程间的同步,man 2 flock,显示其用法是

1
2
3
4
5
NAME
       flock - apply or remove an advisory lock on an open file
SYNOPSIS
       #include <sys/file.h>
       int flock(int fd, int operation);

operation可以取值:

  • LOCK_SH 共享锁,读锁
  • LOCK_EX 互斥锁,写锁
  • LOCK_UN 解锁操作

另外一个参数fd是打开的文件描述符,所以文件锁与文件系统关系密切,其实现原理也是挂在打开的文件结构体上。因此当文件被关闭时,也会触发解锁操作。

在shell中的用法是

1
flock -x command arg # 互斥锁

本文记录一次死锁的问题,程序是多线程的,同时需要fork一个子进程去执行脚本。我们希望执行脚本与程序本身的函数调用互斥。于是在脚本中获取互斥锁,在函数执行前获取共享锁,从而达到互斥的目的。

刚开始测试时运行良好,后来出现程序卡死的问题。下面来模拟一下当时的情况:
demo.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/types.h>
#include <unistd.h>
#include <wait.h>

#define println(fmt, args...) printf(fmt"\n", ##args)
#define log(fmt, args...) println("[p%u][t%lu]"fmt, getpid(), pthread_self(), ##args)

const char *lockpath = "./f.lock";

int flock_wrap(int fd, int op) {
    int ret = flock(fd, op);
    if (ret == -1) {
        perror("flock error");
    }
    return ret;
}

int flock_sh(int fd) {
    log("flock sh, fd: %d", fd);
    return flock_wrap(fd, LOCK_SH | LOCK_NB); 
}

int flock_ex(int fd) {
    log("flock ex, fd: %d", fd);
    return flock_wrap(fd, LOCK_SH | LOCK_NB);
}

int unlock(int fd) {
    log("flock unlock, fd: %d", fd);
    return flock_wrap(fd, LOCK_UN);
}

typedef void *(*StartRoutine)(void *);
pthread_t spawn(StartRoutine start, void *arg) {
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, start, arg);
    if (ret == -1) {
        perror("pthread create error");
        return 0;
    }
    return tid;
}

void do_something() {
    log("do some things...");
    sleep(2);
}

void do_something_wrap() {
    int fd = open(lockpath, O_WRONLY | O_CREAT);
    log("open %s as %d", lockpath, fd);
    if (fd == -1) {
        perror("open error");
        return;
    }
    if (flock_sh(fd) != -1) {
        do_something();
    }
    log("close %d", fd);
    close(fd);
}

void *thread_start(void *notused) {
    do_something_wrap();
    return NULL;
}

void fork_exec() {
    pid_t pid = fork();
    if (pid == 0) {
        log("In child");
        execl("/bin/bash", "/bin/bash", "./do.sh", NULL);
        exit(0);
    } else {
        waitpid(pid, NULL, WUNTRACED);
    }
}

int main(int argc, char **argv) {
    log("main");
    pthread_t tid = spawn(thread_start, NULL);
    sleep(1);
    fork_exec();
    pthread_join(tid, NULL);
    return 0;
}

do.sh:

1
2
3
echo "[shell]=====start====="
flock -x f.lock echo "[shell]in flock ex"
echo "[shell]======end======"

程序在子进程中打开锁文件,尝试获取共享锁,然后关闭文件。

打印日志如下:

1
2
3
4
5
6
7
$ [p15641][t140500877805376]main
[p15641][t140500877801024]open ./f.lock as 3
[p15641][t140500877801024]flock sh, fd: 3
[p15641][t140500877801024]do some things...
[p15643][t140500877805376]In child
[shell]=====start=====
[p15641][t140500877801024]close 3

使用top,可以看到三个被卡住的进程

1
2
3
15641 lcl       20   0   10960   1108   1016 S   0.0   0.0   0:00.00 fl
15643 lcl       20   0    7752   3372   3124 S   0.0   0.0   0:00.00 bash
15644 lcl       20   0    6172    944    856 S   0.0   0.0   0:00.00 flock

使用strace查看

1
2
3
4
5
6
7
8
9
$ strace -p 15641
strace: Process 15641 attached
wait4(15643,
$ strace -p 15643
strace: Process 15643 attached
wait4(-1,
$ strace -p 15644
strace: Process 15644 attached
flock(4, LOCK_EX

fl主程序卡在wait,shell脚本的flock命令卡在等待文件锁上。但是我们的程序中文件已经关闭了,为什么flock会被卡住呢?
通过proc信息查看

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
$ ls -l /proc/15641/fd
total 0
lrwx------ 1 lcl lcl 64 Oct 28 20:01 0 -> /dev/pts/0
lrwx------ 1 lcl lcl 64 Oct 28 20:01 1 -> /dev/pts/0
lrwx------ 1 lcl lcl 64 Oct 28 20:01 2 -> /dev/pts/0
$ ls -l /proc/15643/fd
total 0
lrwx------ 1 lcl lcl 64 Oct 28 20:01 0 -> /dev/pts/0
lrwx------ 1 lcl lcl 64 Oct 28 20:01 1 -> /dev/pts/0
lrwx------ 1 lcl lcl 64 Oct 28 20:01 2 -> /dev/pts/0
lr-x------ 1 lcl lcl 64 Oct 28 20:01 255 -> /home/lcl/repo/c/flock/do.sh
l-wx------ 1 lcl lcl 64 Oct 28 20:01 3 -> /home/lcl/repo/c/flock/f.lock
$ ls -l /proc/15644/fd
total 0
lrwx------ 1 lcl lcl 64 Oct 28 20:01 0 -> /dev/pts/0
lrwx------ 1 lcl lcl 64 Oct 28 20:01 1 -> /dev/pts/0
lrwx------ 1 lcl lcl 64 Oct 28 20:01 2 -> /dev/pts/0
l-wx------ 1 lcl lcl 64 Oct 28 20:01 3 -> /home/lcl/repo/c/flock/f.lock
lr-x------ 1 lcl lcl 64 Oct 28 20:01 4 -> /home/lcl/repo/c/flock/f.lock

可以看到在PID15643和PID15644中,指向f.lock的fd 3依然存在,只是在PID15641中被关闭了。
所以这是一个文件锁、多进程多线程文件继承的问题。在线程中打开的文件,被子进程继承,当关闭文件时,执行了exec的子进程已经断开和原主进程的关系,打开的文件得以保留。因此f.lock这个文件并没有被所有进程关闭,挂在这个文件上的锁也没有被解锁,导致了死锁问题。

其过程如下:

其解决方案是,使用显式的UNLOCK进行解锁,再设置文件的close_on_exec属性加一重保险。