分类: LINUX
2012-02-07 20:47:20
++++++APUE读书笔记-14高级输入输出-03记录锁(1)++++++
3、记录锁(1)
================================================
当两个人同时编辑一个文件的时候,在大多数unix系统上面,文件的最终状态取决于那个最后写文件的进程。在有一些应用程序中,例如数据库系统,进程需要保证只有它自己在写这个文件。商业化的unix系统通过记录锁的机制来提供这样的功能(在本书的后面就给出了一个使用记录锁实现的简单的数据库的函数库)。
记录锁(record locking)可以用来描述一种功能,它在一个进程读写文件的某一个部分时,保护文件不被其它进程相应的内容。在Unix中"record"这个词容易引起歧义,因为unix系统中对于文件并没有记录的概念,一个稍微好点的描述应该是字节锁(byte-range locking),因为它只是用来表示文件被保护的那部分在文件中的字节范围。
历史信息:
早期unix的一个问题就是,它不能用来运行数据库系统,因为没有可以锁一个文件某个区域的锁机制。随着unix向商业化计算环境的迈进,许多组织开始向其中添加各种记录锁的机制(当然它们之间各不相同)。
早期的伯克利发行版本,只支持flock函数,这个函数只能用来锁住文件的整个区域而不是部分区域。
记录锁在System V的第3个发行版本中通过fcntl函数被添加进来。lockf就是基于此提供了一个简单的接口,这个函数允许调用这锁住文件的任何范围,从整个文件,到单个字节。
POSIX.1选择了对fcntl函数进行了标准化,本节给出了各种系统可以支持的记录锁的形式。注意Single UNIX Specification把lockf函数包含在其XSI扩展中了。
这里各种系统支持的记录锁的形式有(Advisory,Mandatory,fcntl,lockf,flock),如下表所示。
不同系统所支持的记录锁
+--------------------------------------------------------------+
| System | Advisory | Mandatory | fcntl | lockf | flock |
|---------------+----------+-----------+-------+-------+-------|
| SUS | • | | • | XSI | |
|---------------+----------+-----------+-------+-------+-------|
| FreeBSD 5.2.1 | • | | • | • | • |
|---------------+----------+-----------+-------+-------+-------|
| Linux 2.4.22 | • | • | • | • | • |
|---------------+----------+-----------+-------+-------+-------|
| Mac OS X 10.3 | • | | • | • | • |
|---------------+----------+-----------+-------+-------+-------|
| Solaris 9 | • | • | • | • | • |
+--------------------------------------------------------------+
在后面会对advisory和mandatory锁的区别进行描述,这里我们只描述POSIX中的fcntl函数。
记录锁原来是1980年被Joh Bass加入到版本7中的。进入到内核的系统调用入口是一个叫做locking的函数。这个函数提供了mandatory记录锁,并且广泛应用在许多System III的版本中,Xenix系统采用了这个函数,其他基于intel的System V衍生系统系统,例如Open Server 5在Xenix兼容的库中还在支持它。
fcntl记录锁
前面已经进行过它的介绍,其声明如下:
#include
int fcntl(int filedes, int cmd, ... /* struct flock *flockptr */ );
返回:如果成功则取决于cmd参数,如果出错则返回1(其值实际一般为-1)。
用于记录锁的cmd参数是F_GETLK,F_SETLK,或者F_SETLKW。第三个参数(后面我们称flockptr)是一个指向flock结构的指针。
struct flock {
short l_type; /* F_RDLCK, F_WRLCK, 或者 F_UNLCK */
off_t l_start; /* 相对于l_whence的字节偏移量 */
short l_whence; /* SEEK_SET(绝对位置), SEEK_CUR(当前位置), 或者 SEEK_END(结尾) */
off_t l_len; /* 字节长度;0代表一直锁到EOF */
pid_t l_pid; /* 和F_GETLK一起返回 */
};
这个结构描述的信息:
a.所需要的锁的类型:F_RDLCK(共享读锁),F_WRLCK(互斥写锁),或者F_UNLCK(解锁一个区域)。
b.将要加锁或者解锁的起始字节偏移(l_start和l_whence)。
c.区域的字节大小(l_len)。
d.可以阻塞当前进程(只通过F_GETLK返回)的持有锁的进程ID。
对于区域的上锁和解锁,有各种规则和标准。
a. 用于指定区域的起始偏移的两个元素和lseek函数的最后两个参数类似。l_whence成员被设置为SEEK_SET,SEEK_CUR,或者SEEK_END。
b. 锁的起始和扩展可以超过文件的结尾,但是不能越过文件的开头。
c. 如果l_len为0,那么意思是说锁被扩展到尽可能大的文件偏移。这允许我们从文件的任意一个位置开始开始锁定一个区域,达到超过并包含后追加到文件结尾的数据(我们不用尝试猜测可能向文件添加了多少个字节)。
d. 为了锁住整个文件,我们设置l_start和l_whence指向文件的开始(有很多方法可以指定文件的开始,最常用的方法就是指定 l_start为0并且指定l_whence为SEEK_SET),指定l_len为0。
我们提到了两种类型的锁:一个是共享读锁(l_type为F_RDLCK)以及互斥写锁(F_WRLCK)。对于这个锁,一个基本的规则就是,(在某一个时刻)对于给定的字节,可以有任何数目的进程持有共享读锁,但是只能有一个进程可以持有互斥写锁。另外,如果有一个或者多个进程拥有了一个字节上的共享读锁,那么那个字节上不能有互斥写锁;反之如果一个字节上有了互斥写锁,那么那个字节上不能有任何的其它锁(写和读都不能有)。在本节用一个图表描述了这个关系,这里就不给出了,想要直观地看到这个关系的话请参见那个图表。
上面描述的锁规则,是用于多个进程之间的,而不是同一个进程中的多个锁。如果一个进程在文件的范围内持有一个锁,那么这个进程接下来如果尝试向同样的范围申请添加锁的话那么将会将原来的锁替换成新的锁。因此,一个进程如果在文件上的1632字节处持有了一个写锁,之后它想要向这个1632字节处加入读锁,那么这个请求也会成功(假设我们没有和其他的进程竞争地向文件的同样一个地方添加锁),并且之前的写锁会被后来申请的读锁替换。
为了获得一个读取锁,文件描述符号必须以读取的方式被打开;为了获得一个写入锁,文件描述符号必须以写入的方式被打开。我们现在描述一个fcntl函数的这三个命令:
F_GETLK:
用于决定flockptr参数描述的锁是否被其他的锁阻塞。如果已经存在其他的锁那么如果我们创建锁的话将会被阻止,并且那个已经存在的锁的信息将会被写入到flockptr参数中,覆盖我们之前传入的值。如果没有其他的锁存在,那么我们创建锁的操作就不会被阻止了,并且flockptr参数指向的数据结构的l_type成员被设置为F_UNLCK,其他的成员保持不变。
F_SETLK:
设置锁为flockptr参数表示的锁。如果我们尝试获取一个读锁(l_type值为F_RDLCK),或者写锁(l_type值为F_WRLCK),但是根据前面的规则系统无法给我们锁,那么这个时候fcntl会立即返回,并且将errno设置成EACCES或者EAGAIN。
尽管POSIX允许实现返回错误代码(errno),本书描述的四个系统在锁请求无法满足的时候也会返回EAGAIN。这个命令也用来清除flockptr所表示的锁的状态(l_type的值为F_UNLCK)。
F_SETLKW:
这个命令是F_SETLK的阻塞版本(W代表的意思是wait)。如果请求的读或者写锁是由于其它进程的锁和请求区域重叠,导致无法成功,那么调用进程将会睡眠。进程会在锁变成可用的时候或者当收到一个信号的时候被唤醒。
需要注意的是,我们使用F_GETLK测试锁然后使用F_SETLK或者F_SETLKW获取锁,这两步操作并不是原子的操作。我们无法保证在这两次fcntl期间不会有其他的进程进入,并且获取同样的锁。如果我们不想在等待锁变成可用的期间阻塞,那么我们必须处理F_SETLK返回的错误。
需要注意的,对于一个特殊的情况POSIX.1并没有指明在这个情况的时候会发生什么。这个情况就是:当一个进程拥有了文件指定范围的读锁,另外一个进程由于这个读锁的存在当它在同样的区域请求写锁导致阻塞,然后第三个进程在同样的区域请求读锁。如果让第三个进程请求读锁成功(因为这不和前面的加锁规则冲突,所以可以有多个读锁存在,所以可以成功),那么第二各请求写锁的进程将可能等待更长的时间(因为写锁必须等所有锁都释放才能请求成功,而在它等待的期间锁的数目没有减少反而增加了)。
当设置或者释放文件上面的锁的时候,系统将会根据请求合并或者分割相应的区域。例如,如果我们锁住了100-199的字节区域,然后释放150字节,那么内核会保持100-149以及151到199字节区域上面的锁。本文中有一个图示便于直观地了解这个情况,这里不给出了。
如果我们再将150字节处锁上,那么系统同样会将其邻近的两个有锁区域与之合并,这样锁区域就由原来的100-149,150,151-199三个区域合并成100-199这一个区域了。
请求和释放锁的例子
下面的lock_reg函数,可以方便我们,不用在每次申请锁的时候都需要填充其中的每一个成员了。
#include
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; /* 相对于l_whence的字节偏移 */
lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
lock.l_len = len; /* 字节数 (0 表示 EOF,即文件结尾) */
return(fcntl(fd, cmd, &lock));
}
由于大多数的操作都是加锁或者解锁的操作(F_GETLK很少被用到),所以定义了下面的宏:
#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))
测试锁的例子:
这里,给出了一个lock_test函数,用于对锁的测试,如果
如果锁存在,将会阻塞参数指定的特定请求,并且返回持有锁的进程ID。否则函数返回0(就是false)。
#include
pid_t lock_test(int fd, int type, off_t offset, int whence, off_t len)
{
struct flock lock;
lock.l_type = type; /* F_RDLCK 或者 F_WRLCK */
lock.l_start = offset; /* 相对于l_whence的字节偏移 */
lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
lock.l_len = len; /* 字节数 (0 表示 EOF,即文件结尾) */
if (fcntl(fd, F_GETLK, &lock) < 0)
err_sys("fcntl error");
if (lock.l_type == F_UNLCK)
return(0); /* 如果区域没有被其他的进程锁住,那么返回false */
return(lock.l_pid); /* 如果区域被其他的进程锁住,返回持有锁的进程的id,也就是true */
}
我们一般使用如下的两个宏来调用这个函数:
#define is_read_lockable(fd, offset, whence, len) \
(lock_test((fd), F_RDLCK, (offset), (whence), (len)) == 0)
#define is_write_lockable(fd, offset, whence, len) \
(lock_test((fd), F_WRLCK, (offset), (whence), (len)) == 0)
需要注意的是进程不能使用lock_test函数来查看它本身当前是否持有文件某一个区域内的锁。F_GETLK命令所描述的信息是:返回的锁如果存在的话那么那个锁应该是阻止我们创建自己的锁。因为F_SETLK和F_SETLKW命令在本进程已经持有锁的情况下,也会设置成功并且会替换本进程原来的锁,也就是说我们永远不会因为自己持有锁而被阻塞;所以,使用F_GETLK命令,并不会获取我们自己是否持有锁,这样的信息。
死锁的例子:
死锁出现在两个进程互相等待对方持有的锁住的资源的时候。当一个进程持有一个锁区域,然后它向另外一个进程申请锁,因为没有申请到而进入睡眠,这个时候就可能发生死锁。
这里就给出了一个例子,例子中父子进程分别持有锁,并且向文件中写数据,然后又分别向对方申请对方持有的锁。具体代码不给出了,具体参见参考资料。
当发现死锁的时候,内核会选择一个进程接收错误并且返回。在这个例子中,选择子进程接收错误并返回,但是这是和实现相关的特性。有些系统中总是选择子进程接收错误,还有些系统中总是选择父进程接受错误,还有些系统你会发现在尝试使用多个锁的时候,在父子进程中都会发生错误。
参考: