本文只介绍 POSIX.1 标准的 fcntl 锁。在 fcntl 函数介绍一节我们曾介绍了 fcntl 函数在设置打开文件标志时的作用,这里将继续它在记录锁上扮演的角色。不过首先还是先来重温一下它的函数原型。
#includeint fcntl(int fd, int cmd, .../* struct flock *flockptr */); /* 返回值:若成功,则依赖于 cmd;否则,返回 -1 */ struct flock{ short l_type; // F_RDLCK, F_WRLCK, or F_UNLCK short l_whence; // SEEK_SET, SEEK_CUR, or SEEK_END off_t l_start; // offset in bytes, relative to l_whence off_t l_len; // length, in bytes; 0 means lock to EOF pid_t l_pid; // returned with F_GETLK };
在此先说明 flock 的结构如下。
(1)所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)或 F_UNLCK(解锁一个区域)。
(2)要加锁或解锁区域的起始字节偏移量(l_start 和 l_whence,同 lseek 函数)。
(3)区域的字节长度。
(4)进程 l_pid 持有的锁能阻塞当前进程(仅由 F_GETLK 返回)。
关于加锁或解锁区域的说明还要注意下列几项规则。
(1)锁可以在当前文件尾端或者越过尾端处开始,但不能在文件起始位置之前开始。
(2)如若 l_len 为 0,则表示锁的范围可以扩展到最大可能偏移量。这意味着不管向文件中追加了多少数据,它们都可以处于锁的范围内,而且起始位置可以是文件中的任意一个位置。
(3)要对整个文件加锁,可以设置 l_start 和 l_whence 指向文件的起始位置,并且指定 l_len 为 0。
另外,关于读锁和写锁之间的兼容性规则如下表所示。
注意,这里说明的兼容性规则使用于不同进程提出的锁请求,但并不适用于单个进程提出的多个锁请求。如果一个进程对一个文件区间已经有了一把锁,若它又企图在同一区间再加一把锁,那么新锁将替换已有的锁,而不管是什么类型的锁。另外,加读锁时,描述符 fd 必须是读打开;加写锁时,该描述符 fd 必须是写打开。
接下来再来说明一下 cmd 参数用于记录锁时使用的 3 个值。
* F_GETLK:判断由 flockptr 所描述的锁是否会被另外一把锁所排斥(阻塞)。如果存在一把锁会阻止创建由 flockptr 所描述的锁,则该现有锁的信息将重写 flockptr 指向的信息。否则,除了 l_type 被设置为 F_UNLCK 之外,flockptr 所指向结构中的其他信息保持不变。
* F_SETLK:设置由 flockptr 所描述的锁。如果试图获取一把违反兼容性规则的锁,那么 fcntl 会立即出错返回,并把 errno 设置为 EACCES 或 EAGAIN(多数实现返回 EAGAIN)。此命令也用来清除由 flockptr 指定的锁(l_type 为 F_UNLCK)。
* F_SETLKW:这个命令是 F_SETLK 的阻塞版本。如果请求的锁因另一个进程当前已经对所请求区域的某部分进行了加锁而不能被授予,那么调用进程就会进入休眠,直到请求创建的锁可用或者休眠被信号中断。
应当了解,用 F_GETLK 测试后再企图用 F_SETLK 或 F_SETLKW 来建立锁的操作不是一个原子操作。此外,如果不希望在等待锁变为可用时产生阻塞,就必须处理由 F_SETLK 返回的可能的出错。另外,当读锁请求比较频繁时,可能使写锁请求产生饥饿现象。
在设置或释放文件上的一把锁时,系统会按要求组合或分裂相邻区。例如,若第 100~199 字节是加锁的区,然后解锁第 150 字节,则内核将维持两把锁,一把用于第 100~149 字节,另一把用于第 151~199 字节。当又对第 150 字节加锁时,系统将会再把 3 个相邻的加锁区合并成一个区。
关于记录锁的自动继承和释放有以下 3 条规则:
(1)锁与进程和文件两者相关联。这表示:当一个进程终止时,它所建立的锁会全部释放;以及无论何时 close 一个文件描述符时,该进程在这一描述符引用的文件上设置的任何一把锁都会释放,而不管是通过哪一个文件描述符设置的。
(2)由 fork 产生的子进程不继承父进程所设置的记录锁,以免父子进程同时写一个文件。
(3)在执行 exec 后,新程序可以继承原执行程序的锁。但如果一个文件描述符设置了执行时关闭标志,那么当作为 exec 的一部分关闭该文件描述符时,将释放相应文件的所有锁。
可以通过观察 FreeBSD 实现中使用的数据结构来进一步理解这几条规则。考虑一个进程,它执行了下列语句:
fd1 = open(pathname, ...);
write_lock(fd1, ...); // parent write locks
if((pid = fork()) > 0){
fd2 = dup(fd1);
fd3 = open(pathname, ...);
}else if(pid == 0){
read_lock(fd1, ...); // child read locks
}
pause();
下图显示了父进程和子进程执行 pause 后的数据结构情况。
图中的 lockf 结构代表记录锁,每个 lockf 结构都描述了一个给定进程的一个加锁区域。图中显示了两个 lockf 结构,一个是父进程调用 write_lock 形成的,另一个是子进程调用 read_lock 形成的。每一个都包含了相应的进程 ID。在父进程中,关闭 fd1、fd2 或 fd3 中的任意一个都将释放由父进程设置的写锁。在关闭其中的任意一个时,内核会从该描述符所关联的 i 节点开始,逐个检查 lockf 链接表中的各项,找到并释放由调用进程持有的各把锁,而并不关心父进程是用其中的哪一个来设置这把锁的。
在前面 守护进程惯例一节中曾提到可以使用一把文件锁来保证只有守护进程的唯一副本在运行,下面就是其中用到的函数 lockfile 的实现。
#include#include int lockfile(int fd){ struct flock fl; fl.l_type = F_WRLCK; fl.l_start = 0; fl.l_whence = SEEK_SET; fl.l_len = 0; return fcntl(fd, F_SETLK, &fl); }