第55章 文件加锁
概述
虽然使用信号量可以完成同步,通常使用文件锁更好,因为内核能够自动将锁与文件关联起来,两种文件锁API:
- flock对整个文件加锁,源自BSD
- fcntl对一个文件区域加锁,源自System V
虽然文件锁通常与文件IO一起使用,但可以作为一项更为通用的同步技术
混合使用文件锁和stdio函数
由于stdio库会在用户空间进行缓冲,在混合使用stdio函数与加锁技术时需谨慎,可以采用如下方式:
- 使用read/write取代stdio库函数执行文件IO
- 对文件加锁之后立即刷新stdio库,且在释放锁之前立即再次刷新这个流
使用setbuf禁用stdio缓冲
劝告式和强制式加锁
劝告式:默认,表示一个进程可以简单地忽略另一个进程在文件上放置的锁
- 强制式:强制一个进程执行IO时要遵循其他进程持有的锁
使用flock加锁
```include
int flock(int fd, int operation); // 若成功,返回0,若出错,返回-1 // 任意数量的进程可同时持有一个文件上的共享锁,但在同一时刻只有一个进程能够持有一个文件的互斥锁(会拒绝其他的互斥和共享请求) // operation的取值: LOCK_SH:共享锁 LOCK_EX:互斥锁 LOCK_UN:解锁 LOCK_NB:发起非阻塞式请求 // 默认情况下,如果另一个进程已经持有了一把不兼容的锁(只有当另一个进程持有共享锁且当前进程请求共享锁才兼容),调用会阻塞 // 不管进程对文件的访问模式是什么,都可以对其加共享锁或互斥锁 // 通过再次调用flock并将operation指定为恰当的值可以将一个既有共享锁转换为互斥锁(反之亦然),锁的转换并非原子的,会先删除既有的锁,再创建一个新锁
##### 锁继承与释放的语义
// 对fd加锁 flock(fd, LOCK_EX); // new_fd和fd都指向同一把锁 new_fd = dup(fd); // 通过fd解锁 flock(new_fd, LOCK_UN);
```
fd1 = open("a.txt", O_RDWR);
fd2 = open("a.txt", O_RDWR);
flock(fd1, LOCK_EX);
// 阻塞
flock(fd2, LOCK_EX);
flock(fd, LOCK_EX);
if (0 == fork())
// 释放与父进程共享的锁
flock(fd, LOCK_UN);
flock的限制
- 只能对整个文件加锁
- 只能放置劝告式锁
- 很多NFS实现不识别flock锁
fcntl锁将弥补这些不足
使用fcntl加锁
fcntl可以对整个文件或文件的一部分加锁,称为记录锁或POSIX文件锁,记录锁通常应用在普通文件才有意义,Linux上可以对任何类型的文件加记录锁
#include <fcntl.h>
int fcntl(int fildes, int cmd, ...);
// 任意数量的进程能够持有一块区域的读锁,只有一个进程能持有一把写锁,且这把锁会将其他进程的读锁和写锁排除在外
// 一般而言,应用程序只需对所需的最小字节范围加锁,进而取得更大的并发性
// cmd的取值:
F_SETLK:获取或释放锁,如果另一个进程持有一把待加锁区域的不兼容锁,失败返回EAGAIN错误
F_SETLKW:与F_SETLK类似,如果另一个进程持有一把待加锁区域的不兼容锁,阻塞直到请求满足,一般需要通过alarm或setitimer设置一个超时时间
F_GETLK:检测释放能够获取指定区域的锁,实际并不获取锁,l_type必须是F_RDLCK,或F_WRLCK
struct flock
{
short l_type; /* 锁的类型: F_RDLCK, F_WRLCK, F_UNLCK */
short l_whence; /* 文件位置: SEEK_SET, SEEK_CUR, SEEK_END */
off_t l_start; /* 锁相对于文件位置的偏移量 */
off_t l_len; /* 锁的长度,0表示l_whence和l_start确定的位置到文件尾的范围 */
pid_t l_pid; /* PID of process blocking our lock (F_GETLK only) */
};
获取和释放锁的细节
- 解锁一块文件区域总会立即成功,即使不持有锁
- 任何时刻,一个进程只能持有一个文件区域的一种锁,同一区域添加新锁,如果与既有锁类型一样,不会发生任何事情,如果与既有类型不一样,则返回一个错误或阻塞
- 一个进程永远无法将自己所在文件区域之外
在已经持有锁的区域中间加一把模式不同的锁会产生三个锁,新锁的两端会创建两个跟之前模式一样的更小的锁
锁继承与释放的语义
与flock不同,fork创建的子进程不会继承记录锁
- 记录锁在exec期间会保留
- 一个进程的所有线程会共享记录锁
记录锁同时与一个进程和一个i-node关联,所以当进程终止时所有记录锁会释放,进程关闭一个文件描述符时,该文件描述符对应文件上的记录锁会释放
锁定饿死和排队加锁请求的优先级
Linux上,一系列的读锁可能导致一个被阻塞的写锁饿死,甚至无限期的饿死,规则如下:
排队的锁请求被准予的顺序是不确定的
- 写者并不比读者拥有更高的优先权,反之亦然
强制加锁
Linux允许fcntl记录锁是强制的,表示需要对每个文件IO操作进行检查以判断其他进程在执行IO所在的文件区域上是否持有任何不兼容的锁
挂载文件系统时添加-o mand选项,程序中mount指定MS_MANDLOCK标记: ``` mount -o mand /dev/sda10 /testfs
文件上强制加锁是启用set-group-ID和关闭group-execute来完成,这种组合只在这个场景有意义:
chmod g+s,g-x /testfs/file
```
强制加锁对文件IO操作的影响
如果存在进程持有一个文件任意部分的强制读锁或写锁,就无法在该文件创建一个共享内存映射,反之亦然
强制加锁的警告
应该避免使用强制锁,其作用并没有看起来那么大,而且存在一些潜在的缺陷:
- 持有一把强制锁并不能阻止其他进程删除此文件,只要在父目录拥有合适的权限
- 在公开访问的文件启用强制锁需要慎重,因为即使特权进程也无法覆盖一个强制锁
- 使用强制锁存在性能开销,内核必须检查文件上是否存在冲突的锁
/proc/locks文件
通过检查此文件可以查看系统中当前存在的锁,包含flock和fcntl创建的,每把锁包含8个字段:
- 序号
- 创建方式:FLOCK表示flock创建,POSIX表示fcntl创建
- 模式:ANVISORY或MANDATORY
- 类型:READ或WRITE
- PID
- 所属文件:三个冒号分割的数字表示文件系统的主要和次要设备号及i-node号
- 锁的起始字节,对flock永远是0
- 锁的结束字节,对flock永远是EOF
单例模式
一般daemon需要以单例模式运行,可以让daemon在一个标准目录创建一个文件并在该文件持有一把写锁,如果启用daemon的其他实例,在获取写锁时就会失败,一般/var/run是放置此类锁的目录,daemon会将其PID写入锁文件,并以.pid作为扩展名老式加锁技术
open的O_CREATE和O_EXCL与unlink
O_CREATE和O_EXCL可以保证原子性的检查文件存在并创建文件,如果两个进程在创建一个文件时同时指定这些标记,只有一个进程会成功,后面跟着close即可完成加锁,释放锁利用unlink完成,其缺陷: