Linux中的多种文件锁接口
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_SETLKW
和F_GETLK
。它们都接收一个struct flock*
类型的fl
参数:
struct flock
包含的字段如下:1
2
3
4
5
6
7
8
9
10
11
12struct 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_SETLK
和F_SETLKW
用于设置文件锁,锁的参数通过fl
传入。前者在发生锁冲突时会返回错误,后者则会阻塞等待锁被释放。F_GETLK
的行为类似于trylock,它检查能否拿到fl
指定的文件锁。如果可以,将fl->l_type
设置为F_UNLCK
;如果不可以,通过fl
返回与之冲突的锁的信息。如果有多个锁与fl
冲突,返回任意一个。
struct flock
中l_pid
字段的存在表示该类型锁建立的实际上是进程
<=> 文件之间的锁关系,也就是说:
如果已经一个进程通过
SETLK
设置了文件锁,意味着整个进程都获取了这把锁。因此,后续同进程中对同一文件的SETLK
不会失败/阻塞,而是会被视为对锁的更新。即便是同一进程下的不同线程也是如此。1
2
3
4
5
6
7
8ret = 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
15pid = 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
21pid = 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 | ret = flock(fd, LOCK_EX); |
同时,flock()
文件锁也不会在任何一个fd被关闭时就隐式释放文件锁,而是在最后一个指向某open
file description的fd被关闭时才会释放:
1 | ret = flock(fd, LOCK_EX); |
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
7ret = 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相关联的,只不过是以进程为粒度收到信号。