记录锁
基本概念
若两个人同时编辑一个文件,其后果将如何呢?在很多unix系统中,该文件的最后状态取决于写该文件的最后一个进程。但是对于有些应用程序(例如数据库),进程有时需要确保它正在单独写一个文件。为了向进程提供这种功能,商用unix系统提供了记录锁机制。
记录锁的功能是:当一个进程正在读或修改文件的某个部分时,它可以阻止其他进程修改同一文件区。对于unix系统而言,"记录"该词并不恰当,更适合的术语可能是字节范围锁(byte-range locking),因为它锁定的只是文件中的一个区域(当然,也可能是整个文件)。
POSIX.1 形式记录锁
本文主要介绍POSIX.1的fcntl锁,其函数原型为:
#include <fcntl.h>
int fcntl(int filedes, int cmd, ... /* struct flock *flockptr */ );
返回值:若成功则依赖于cmd,若出错则返回-1
第三个参数flockptr是一个指向flock结构的指针:
struct flock { short l_type; /* F_RDLCK, F_WRLCK, or F_UNLCK */ off_t l_start; /* offset in bytes, relative to l_whence */ short l_whence; /* SEEK_SET, SEEK_CUR, or SEEK_END */ 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两者决定。
3)区域的长度由l_len表示。
4)ID为l_pid的进程持有一把当前进程请求的锁,使当前进程阻塞(仅由F_GETLK返回)
以下是对加锁和解锁区域的一些说明:
1)l_start和l_whence两参数的用法和lseek函数的最后两参数类似,l_whence可选用的值是SEEK_SET,SEEK_CUR,SEEK_END。
2)该区域可以在文件的当前尾端或之后开始,但是不能在文件的起始位置之前开始。
3)若l_len为0,则表示锁的区域从其起点(由l_start和l_whence决定)开始直至最大可能偏移量为止,也就是说不管向该文件中添加多少数据,它们都处于锁的范围内。
4)为了锁整个文件,我们设置l_start和l_whence,使锁的起点在文件起始处,并且使l_len为0(有多种方法可以指定文件起始处,最常用的做法是使l_start为0,l_whence为SEEK_SET)。
共享读锁和独占写锁的基本规则:
多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上只能有一个进程独占的写锁。进一步而言,如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一把独占性的写锁,则不能再对它加任何读锁。
以上规则适合于不同进程提出的锁请求,并不适用于单个进程提出的多个锁请求。如果一个进程对一个文件区域已经有了一把锁,后来该进程又企图在同一文件区域再加一把锁,那么新锁将替换掉老锁。
加(获取)读锁时,该描述符必须是读打开;加写锁时,该描述符必须是写打开。
fcntl函数的三种锁处理命令
F_GETLK 判断由flockptr所描述的锁是否会被另外一把锁所阻塞。如果现存一把锁,它阻止创建由flockptr所描述的锁,则用该现存锁的信息覆盖flockptr指向的结构。如果不存在上述情况,则除了将l_type设置为F_UNLCK之外,flockptr所指向结构中的其他信息保持不变。
F_SETLK 设置由flockptr所描述的锁。如果试图建立一把读锁(l_type设为F_RDLCK)或写锁(l_type设为F_WRLCK),而按上述兼容性规则不被允许,则fcntl立即出错返回,此时errno设置为EACCES或EAGAIN。
此命令也用来清除由flockptr说明的锁(l_type为F_UNLCK)
F_SETLKW 这是F_SETLK的阻塞版本(命令名中的W表示wait)。如果因为当前在所请求区间的某个部分已被另一个进程锁住,按兼容性规则,由flockptr所请求的锁不能被创建,调用进程进入休眠。如果请求创建的锁已经可用,或者休眠状态被某个信号中断,则该进程被唤醒。
注意:在设置或释放文件上的锁时,系统按要求组合或裂开相邻区。
实例:请求和释放一把锁
为了避免每次分配flock结构,然后又填入各项信息,可以用下面的lock_reg函数来处理所有细节:
#include <fcntl.h> int lock_reg(int fd, int cmd, int type, off_t offset, int whence, off_t len) { struct flock lock; lock.l_type = type; /* F_RDLCK, F_WRLCK, F_UNLCK */ lock.l_start = offset; /* byte offset, relative to l_whence */ lock.whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END*/ lock.l_len = len; /* #bytes (0 means to EOF) */ return (fcntl(fd, cmd, &lock)); }
因为大多数锁调用是加锁或解锁一个文件区域(命令F_GETLK极少使用),我们通常使用下列5个宏:
#define read_lock(fd, offset, whence, len) \ lock_reg( (fd), F_SETLK, F_RDLCK, (offset), (whence), (len) ) #define readw_lock(fd, offset, whence, len) \ lock_reg( (fd), F_SETLKW, F_RDLCK, (offset), (whence), (len) ) #define write_lock(fd, offset, whence, len) \ lock_reg( (fd), F_SETLK, F_WRLCK, (offset), (whence), (len) ) #define writew_lock(fd, offset, whence, len) \ lock_reg( (fd), F_SETLKW, F_WRLCK, (offset), (whence), (len) ) #define un_lock(fd, offset, whence, len) \ lock_reg( (fd), F_SETLK, F_UNLCK, (offset), (whence), (len) )
记录锁的自动继承和释放规则
1)锁是与进程、文件两者相关联的。这有两重含义:第一重很明显,当一个进程终止时,它锁建立的锁全部释放;第二重意思不是很明显,任何时候关闭一个描述符时,则该进程通过这一描述符可以引用的文件上的任何一把锁都被释放(这些锁都是改进程设置的)。
2)由fork产生的子进程不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程获得的锁而言,子进程被视为另一个进程,对于从父进程处继承过来的任一描述符,子进程需要调用fcntl才能获得它自己的锁。这与锁的作用是相一致的。锁的作用是阻止多个进程同时写同一个文件(或同一个文件区域)。如果子进程继承父进程的锁,那么父、子进程就可以同时写同一个文件。
3)在执行exec后,新程序可以继承原执行程序的锁。但是,如果对一个文件描述符设置了close-on-exec标志,那么当作为exec的一部分关闭该文件描述符时,对应文件的所有锁都被释放了。
强制性锁
强制性锁使内核对每一个open、read和write系统调用都进行检查,检查调用进程对正在访问的文件是否违背了某一把锁的作用。强制性锁有时也被称为强迫方式锁。
对一个特定文件打开其设置组ID位并关闭其组执行位,则对该文件开启了强制性锁机制。