Linux支持多种文件锁相关的语义。

其中,POSIX文件锁,flock()文件锁和OFD文件锁都是建议性锁(advisory lock),需要接口的使用者主动拿锁才能发挥作用。换言之,如果有程序无视文件锁,直接去读写文件,上述3种文件锁都对此无可奈何。

file lease是一种租约,语义和锁略有不同,不过Linux的实现中也把它和文件锁放在了一起。

fcntl() POSIX文件锁

这是Linux中对Unix record lock的实现,是POSIX标准规定的。在fcntl()中通过{SET,GET}LK设置的就是这种锁。

该锁的接口包括3种fcntl()命令,即F_SETLK F_SETLKWF_GETLK。它们都接收一个struct flock*类型的fl参数:

  • struct flock包含的字段如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct flock {
    ...
    short l_type; /* Type of lock: F_RDLCK,
    F_WRLCK, F_UNLCK */
    short l_whence; /* How to interpret l_start:
    SEEK_SET, SEEK_CUR, SEEK_END */
    off_t l_start; /* Starting offset for lock */
    off_t l_len; /* Number of bytes to lock */
    pid_t l_pid; /* PID of process blocking our lock
    (set by F_GETLK and F_OFD_GETLK) */
    ...
    };

    可以看出,POSIX文件锁是细粒度的,可以锁定文件的一部分。(l_whence,l_start,l_len)三元组指定了文件中需要锁定的一段区间,l_pid则是拿锁的进程号。锁的类型分为读锁和写锁,一段区间上可以有多个读者,但最多只能有一个写者。

  • F_SETLKF_SETLKW用于设置文件锁,锁的参数通过fl传入。前者在发生锁冲突时会返回错误,后者则会阻塞等待锁被释放。

  • F_GETLK的行为类似于trylock,它检查能否拿到fl指定的文件锁。如果可以,将fl->l_type设置为F_UNLCK;如果不可以,通过fl返回与之冲突的锁的信息。如果有多个锁与fl冲突,返回任意一个。

struct flockl_pid字段的存在表示该类型锁建立的实际上是进程 <=> 文件之间的锁关系,也就是说:

  • 如果已经一个进程通过SETLK设置了文件锁,意味着整个进程都获取了这把锁。因此,后续同进程中对同一文件的SETLK不会失败/阻塞,而是会被视为对锁的更新。即便是同一进程下的不同线程也是如此。

    1
    2
    3
    4
    5
    6
    7
    8
    ret = fcntl(fd, F_SETLK, &fl);
    assert(ret == 0);

    ret = fcntl(fd, F_SETLK, &fl2); // 不会失败,视为锁更新
    assert(ret == 0);

    ret = fcntl(fd, F_GETLK, &fl);
    assert(ret == 0 && fl.l_type == F_UNLCK); // 可以正常拿锁

    但如果另一个进程试图拿锁,则会发生锁冲突。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    pid = fork();

    if (pid > 0) {
    // parent
    ret = fcntl(fd, F_SETLK, &fl); // 父进程先拿写锁
    assert(ret == 0);

    wait(NULL);
    } else if (pid == 0) {
    // child
    sleep(3);

    ret = fcntl(fd, F_SETLK, &fl2); // 子进程再拿写锁会失败
    assert(ret == -1 && (errno == EAGAIN || errno == EACCES));
    }
  • POSIX文件锁针对的目标是文件(inode),不管一个进程中打开了同一个文件多少次,或者通过dup() fork()拿到了指向同一个open file description的多个fd,它们都指向同一个文件,也共享着同一把锁。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    pid = fork();

    if (pid > 0) {
    // parent
    ret = fcntl(fd, F_SETLK, &fl); // 父进程先拿写锁
    assert(ret == 0);

    fd2 = dup(fd);
    fl.l_type = F_UNLCK;

    ret = fcntl(fd, F_SETLK, &fl); // 父进程通过一个新fd放锁
    assert(ret == 0);

    wait(NULL);
    } else if (pid == 0) {
    // child
    sleep(3);

    ret = fcntl(fd, F_SETLK, &fl2); // fd上的锁被父进程通过fd2放掉了,子进程可以拿到锁
    assert(ret == 0);
    }
  • 除了显式通过fl->l_type == F_UNLCK放锁以外,关闭一个文件描述符或者进程终止都会导致锁被隐含地释放。

POSIX文件锁的这些性质局限了其使用场景,例如:

  • 无法在同一进程下的多个线程之间使用。
  • 进程可能意外地失去文件锁:如果用户逻辑拿了/etc/passwd的锁,而某个引入的库里打开-读取-关闭了这个文件,用户的锁就会在无感知的情况下被释放。

flock()

int flock(int fd, int op)系统调用提供了另一种来自BSD的文件锁语义。op的取值包括LOCK_SH(共享锁),LOCK_EX(互斥锁)和LOCK_UN,还可以加上LOCK_NB位把阻塞语义变成非阻塞语义。

与POSIX文件锁相比,flock()不能对文件的某个区间范围进行细粒度加锁,而只能锁定整个文件。

不同于POSIX文件锁,flock()是与open file description相关连的,也就是说:

  • 通过dup()或者fork()得到的fd实际上与原来的fd指向同一个open file description,此时重复加锁操作不会阻塞,而是被视为修改已有锁的状态
  • 通过open()打开同一文件得到的fd指向一个新的open file description,此时重复加锁会相互冲突,导致阻塞/失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
ret = flock(fd, LOCK_EX);
assert(ret == 0);

ret = flock(fd, LOCK_SH | LOCK_NB); // 同一fd,成功
assert(ret == 0);

fd2 = dup(fd);
ret = flock(fd2, LOCK_EX | LOCK_NB);// 同一open file description,成功
assert(ret == 0);

fd2 = open("example.txt", O_RDWR, 0666);
ret = flock(fd2, LOCK_EX | LOCK_NB); // 不同open file description,失败
assert(ret == -1 && errno == EWOULDBLOCK);

同时,flock()文件锁也不会在任何一个fd被关闭时就隐式释放文件锁,而是在最后一个指向某open file description的fd被关闭时才会释放:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
ret = flock(fd, LOCK_EX);
assert(ret == 0);

fd2 = dup(fd);
ret = flock(fd2, LOCK_EX | LOCK_NB);
assert(ret == 0);

close(fd2); // 仍有fd指向open file description,锁不会被释放

fd2 = open("example.txt", O_RDWR, 0666);
ret = flock(fd2, LOCK_EX | LOCK_NB); // 失败
assert(ret == -1 && errno == EWOULDBLOCK);
close(fd2);

close(fd); // fd是最后一个引用,此时锁被释放

fd2 = open("example.txt", O_RDWR, 0666);
ret = flock(fd2, LOCK_EX | LOCK_NB); // 成功
assert(ret == 0);

fcntl() OFD文件锁

fcntl()系统调用还支持F_OFD_{SETLK,SETLKW,GETLK}的operand,这里的OFD是open file description的缩写。

OFD文件锁可以看作POSIX文件锁和flock()文件锁的结合:

  • 一方面,OFD锁与不带OFD版本的F_{SETLK,SETLKW,GETLK}一样,都接收struct flock*类型的参数,使得它可以锁定文件中的某一区间。

  • 另一方面,OFD锁和flock()锁类似,都是针对open file description而非整个进程的。

    1
    2
    3
    4
    5
    6
    7
    ret = fcntl(fd, F_OFD_SETLK, &fl);
    assert(ret == 0);

    fd2 = open("example.txt", O_RDWR | O_CREAT, 0666);

    ret = fcntl(fd2, F_OFD_SETLK, &fl2); // 不同的open file description,冲突
    assert(ret == -1 && (errno == EAGAIN || errno == EACCES));

file lease

fcntl()F_{SET,GET}LEASE operand提供了文件租约的接口,它们在某些方面与锁比较类似:

  • F_SETLEASE接收F_{RD,WR,UN}LCK参数,分别代表读租约、写租约和解除租约。

    • 进程获得租约后,会在其他进程试图open()(以读或者以写的形式打开文件分别对应读租约和写租约)或truncate()文件时收到信号。在此期间,其他试图访问文件的进程会被阻塞。

    • 收到信号后,进程需要在lease-break-time指定的时间内释放/降级租约。否则OS会强制执行这一操作。

      降级租约:其他进程以读形式打开文件时,允许当前线程将写租约降级成读租约。

  • F_GETLEASE返回当前持有的租约。

与OFD文件锁类似,file lease同样也是与open file description相关联的,只不过是以进程为粒度收到信号。

参考资料

  1. c - What is the difference between locking with fcntl and flock? - Stack Overflow
  2. File-private POSIX locks (LWN.net)