当两个人同时编辑一个文件时,其后果将如何呢?在大多数UNIX系统中,该文件的最后状态取决于写该文件的最后一个进程。但是对于有些应用程序,如数据库,进程有时需要确保它正在单独写一个文件。为了向进程提供这种功能,商用UNIX系统提供了记录锁机制。(第20章包含了使用记录锁的数据库函数库。)
记录锁(record locking)的功能是:当第一个进程正在读或修改文件的某个部分时,使用记录锁可以阻止其他进程修改同一文件区。对于 UNIX 系统而言,“记录”这个词是一种误用,因为 UNIX 系统内核根本没有使用文件记录这种概念。一个更适合的术语可能是字节范围锁(byte-range locking),因为它锁定的只是文件中的一个区域(也可能是整个文件)。
1.历史
对早期UNIX系统的其中一个批评是它们不能用来运行数据库系统,其原因是这些系统不支持对部分文件加锁。在UNIX系统寻找进入商用计算环境的途径时,很多系统开发小组以各种不同方式增加了对记录锁的支持。
早期的伯克利版本只支持flock
函数。该函数只能对整个文件加锁,不能对文件中的一部分加锁。
SVR3通过fcntl
函数增加了记录锁功能。在此基础上构造了lockf
函数,它提供了一个简化的接口。这些函数允许调用者对一个文件中任意字节数的区域加锁,长至整个文件,短至文件中的一个字节。
POSIX.1标准的基础是fcntl
方法。图14-2列出了各种系统提供的不同形式的记录锁。注意,Single UNIX Specification在其XSI扩展中包括了lockf
。
图14-2 各种UNIX系统支持的记录锁形式
本节最后部分将说明建议性锁和强制性锁之间的区别。本书只介绍POSIX.1的fcntl锁。
记录锁是 1980 年由 John Bass 最早添加到 V7 上的。内核中相应的系统调用入口项是名为locking的函数。此函数提供了强制性记录锁功能,它被用在很多System III版本中。Xenix系统采用了此函数,某些基于Intel的System V派生版本,如OpenServer 5,在Xenix兼容库中仍旧支持该函数。
2.fcntl记录锁
3.14节中已经给出了fcntl
函数的原型,为了叙说方便,这里再重复一次。
#include <fcnt1.h>
int fcnt1(int fd, int cmd, .../* struct flock *flockptr */);
//返回值:若成功,依赖于cmd(见下),否则,返回−1
对于记录锁,cmd是F_GETLK
、F_SETLK
或F_SETLKW
。第三个参数(我们将调用flockptr)是一个指向flock
结构的指针。
struct flock {
short l_type; /* F_RDLCK, F_WRLCK, or F_UNLCK*/
short l_whence; /* SEEK_SET, SEEK_CUR, orSEEK_END */
off_t l_start; /* offset in bytes, relative tol_whence */
off_t l_len; /* length, in bytes; 0 means lockto EOF */
pid_t l_pid; /* returned with F_GETLK */
};
对flock
结构说明如下。
- 所希望的锁类型:
F_RDLCK
(共享读锁)、F_WRLCK
(独占性写锁)或F_UNLCK
(解锁一个区域)。 - 要加锁或解锁区域的起始字节偏移量(
l_start
和l_whence
)。 - 区域的字节长度(
l_len
)。 - 进程的ID(
l_pid
)持有的锁能阻塞当前进程(仅由F_GETLK
返回)。
关于加锁或解锁区域的说明还要注意下列几项规则。
- 指定区域起始偏移量的两个元素与
lseek
函数(见3.6节)中最后两个参数类似。l_whence
可选用的值是SEEK_SET
、SEEK_CUR
或SEEK_END
。 - 锁可以在当前文件尾端处开始或者越过尾端处开始,但是不能在文件起始位置之前开始。
- 如若
l_len
为0,则表示锁的范围可以扩展到最大可能偏移量。这意味着不管向该文件中追加写了多少数据,它们都可以处于锁的范围内(不必猜测会有多少字节被追加写到了文件之后),而且起始位置可以是文件中的任意一个位置。 - 为了对整个文件加锁,我们设置
l_start
和l_whence
指向文件的起始位置,并且指定长度(l_len
)为0。(有多种方法可以指定文件起始处,但常用的方法是将l_start
指定为0,l_whence
指定为SEEK_SET
。)
上面提到了两种类型的锁:共享读锁(l_type
为L_RDLCK
)和独占性写锁(L_WRLCK
)。基本规则是:任意多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给定字节上只能有一个进程有一把独占写锁。进一步而言,如果在一个给定字节上已经有一把或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一把独占性写锁,则不能再对它加任何读锁。在图14-3中示出了这些兼容性规则。
上面说明的兼容性规则适用于不同进程提出的锁请求,并不适用于单个进程提出的多个锁请求。如果一个进程对一个文件区间已经有了一把锁,后来该进程又企图在同一文件区间再加一把锁,那么新锁将替换已有锁。因此,若一进程在某文件的16~32 字节区间有一把写锁,然后又试图在 16~32 字节区间加一把读锁,那么该请求将成功执行,原来的写锁会被替换为读锁。
加读锁时,该描述符必须是读打开。加写锁时,该描述符必须是写打开。
下面说明一下函数的种命令。
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
。
虽然POSIX.1 允许实现返回这两种出错代码中的任何一种,但本书说明的4种实现在锁请求不能得到满足时,都返回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字节。图14-4说明了这种情况下的字节范围锁。
图14-4 文件字节范围锁
假定我们又对第150字节加锁,那么系统将会再把3个相邻的加锁区合并成一个区(第100~199字节)。其结果如图14-4中的第一个图所示,又跟开始的时候一样了。
实例:请求和释放一把锁
为了避免每次分配flock
结构,然后又填入各项信息,可以用图14-5所示的程序中的函数lock_reg
来处理所有这些细节。
#include "apue.h"
#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.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
lock.l_len = len; /* #bytes (0 means to EOF) */
return(fcntl(fd, cmd, &lock));
}
图14-5 加锁或解锁一个文件区域的函数
因为大多数锁调用是加锁或解锁一个文件区域(命令F_GETLK
很少使用),故通常使用下列5个宏中的一个,这5个宏都定义在apue.h
中(见附录B)。
#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))
我们有目的地用与lseek
函数同样的顺序定义了这些宏中的前3个参数。
实例:测试一把锁
图14-6中定义了一个函数lock_test
,我们将用它测试一把锁。
#include "apue.h"
#include <fcntl.h>
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 or F_WRLCK */
lock.l_start = offset; /* byte offset, relative to l_whence */
lock.l_whence = whence; /* SEEK_SET, SEEK_CUR, SEEK_END */
lock.l_len = len; /* #bytes (0 means to EOF) */
if (fcntl(fd, F_GETLK, &lock) < 0)
err_sys("fcntl error");
if (lock.l_type == F_UNLCK)
return(0); /* false, region isn't locked by another proc */
return(lock.l_pid); /* true, return pid of lock owner */
}
图14-6 测试一个锁条件的函数
如果存在一把锁,它阻塞由参数指定的锁请求,则此函数返回持有这把现有锁的进程的进程ID,否则此函数返回0。通常用下面两个宏来调用此函数(它们也定义在apue.h中)。
#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
命令决不会报告调用进程自己持有的锁。
实例:死锁
如果两个进程相互等待对方持有并且不释放(锁定)的资源时,则这两个进程就处于死锁状态。如果一个进程已经控制了文件中的一个加锁区域,然后它又试图对另一个进程控制的区域加锁,那么它就会休眠,在这种情况下,有发生死锁的可能性。
图14-7所示的程序给出了一个死锁的例子。子进程对第0字节加锁,父进程对第1字节加锁。然后,它们中的每一个又试图对对方已经加锁的字节加锁。在该程序中使用了8.9节中介绍的父进程和子进程同步例程(TELL_xxx
和WAIT_xxx
),以便每个进程能够等待另一个进程获得它设置的第一把锁。
#include "apue.h"
#include <fcntl.h>
static void
lockabyte(const char *name, int fd, off_t offset)
{
if (writew_lock(fd, offset, SEEK_SET, 1) < 0)
err_sys("%s: writew_lock error", name);
printf("%s: got the lock, byte %lld\n", name, (long long)offset);
}
int
main(void)
{
int fd;
pid_t pid;
/*
* Create a file and write two bytes to it.
*/
if ((fd = creat("templock", FILE_MODE)) < 0)
err_sys("creat error");
if (write(fd, "ab", 2) != 2)
err_sys("write error");
TELL_WAIT();
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid == 0) { /* child */
lockabyte("child", fd, 0);
TELL_PARENT(getppid());
WAIT_PARENT();
lockabyte("child", fd, 1);
} else { /* parent */
lockabyte("parent", fd, 1);
TELL_CHILD(pid);
WAIT_CHILD();
lockabyte("parent", fd, 0);
}
exit(0);
}
图14-7 死锁检测实例
运行图14-7中的程序得到:
$ ./a.out
parent: got the lock, byte 1
child: got the lock, byte 0
parent: writew_lock error: Resource deadlock avoided
child: got the lock, byte 1
检测到死锁时,内核必须选择一个进程接收出错返回。在本实例中,选择了父进程,但这是一个实现细节。在某些系统上,子进程总是接到出错信息,在另一些系统上,父进程总是接到出错信息。在某些系统上,当试图使用多把锁时,有时是子进程接到出错信息,有时则是父进程接到出错信息。
3.锁的隐含继承和释放
关于记录锁的自动继承和释放有3条规则。
(1) 锁与进程和文件两者相关联。这有两重含义:第一重很明显,当一个进程终止时,它所建立的锁全部释放;第二重则不太明显,无论一个描述符何时关闭,该进程通过这一描述符引用的文件上的任何一把锁都会释放(这些锁都是该进程设置的)。这就意味着,如果执行下列4步:
fd1 = open(pathname, ...);
read_lock(fd1, ...);
fd2 = dup(fd1);
close(fd2);
则在close(fd2)
后,在fd1
上设置的锁被释放。如果将dup
替换为open
,其效果也一样:
fd1 = open(pathname, ...);
read_lock(fd1, ...);
fd2 = open(pathname, ...);
close(fd2);
(2) 由fork
产生的子进程不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然后调用 fork
,那么对于父进程获得的锁而言,子进程被视为另一个进程。对于通过 fork
从父进程处继承过来的描述符,子进程需要调用 fcntl
才能获得它自己的锁。这个约束是有道理的,因为锁的作用是阻止多个进程同时写同一个文件。如果子进程通过fork
继承父进程的锁,则父进程和子进程就可以同时写同一个文件。
(3) 在执行exec
后,新程序可以继承原执行程序的锁。但是注意,如果对一个文件描述符设置了执行时关闭标志,那么当作为exec
的一部分关闭该文件描述符时,将释放相应文件的所有锁。
4.FreeBSD实现
先简要地观察FreeBSD实现中使用的数据结构。这会帮助我们进一步理解记录锁的自动继承和释放的第一条规则:锁与进程和文件两者相关联。
考虑一个进程,它执行下列语句(忽略出错返回)。
fd1 = open(pathname, ...);
write_lock(fd1, 0, SEEK_SET, 1);
/* parent writelocks byte 0 */
if ((pid = fork()) > 0) {
/* parent */
fd2 = dup(fd1);
fd3 = open(pathname, ...);
} else if (pid == 0) {
read_lock(fd1, 1, SEEK_SET, 1);/* child read locksbyte 1 */
}
pause();
图14-8显示了父进程和子进程暂停(执行pause()
)后的数据结构情况。
图14-8 关于记录锁的FreeBSD数据结构
前面已经给出了open
、fork
以及dup
调用后的数据结构(见图3-9和图8-2)。有了记录锁后,在原来的这些图上新加了lockf
结构,它们由i
节点结构开始相互链接起来。每个lockf
结构描述了一个给定进程的一个加锁区域(由偏移量和长度定义的)。图中显示了两个lockf
结构,一个是由父进程调用write_lock
形成的,另一个则是由子进程调用read_lock
形成的。每一个结构都包含了相应的进程ID。
在父进程中,关闭fd1
、fd2
或fd3
中的任意一个都将释放由父进程设置的写锁。在关闭这3个描述符中的任意一个时,内核会从该描述符所关联的i
节点开始,逐个检查lockf
链接表中的各项,并释放由调用进程持有的各把锁。内核并不清楚(也不关心)父进程是用这3个描述中的哪一个来设置这把锁的。
实例
在图13-6所示的程序中,我们了解到,守护进程可用一把文件锁来保证只有该守护进程的唯一副本在运行。图14-9展示了lockfile
函数的实现,守护进程可用该函数在文件上加写锁。
#include <unistd.h>
#include <fcntl.h>
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));
}
图14-9 在文件整体上加一把写锁
另一种方法是用write_lock
函数定义lockfile
函数。
#define lockfile(fd) write_lock((fd), 0, SEEK_SET, 0)
5.在文件尾端加锁
在对相对于文件尾端的字节范围加锁或解锁时需要特别小心。大多数实现按照l_whence
的SEEK_CUR
或SEEK_END
值,用l_start
以及文件当前位置或当前长度得到绝对文件偏移量。但是,常常需要相对于文件的当前长度指定一把锁,但又不能调用fstat
来得到当前文件长度,因为我们在该文件上没有锁。(在 fstat
和锁调用之间,可能会有另一个进程改变该文件长度。)
考虑以下代码序列:
writew_lock(fd, 0, SEEK_END, 0);
write(fd, buf, 1);
un_lock(fd, 0, SEEK_END);
write(fd, buf, 1);
该代码序列所做的可能并不是你所期望的。它得到一把写锁,该写锁从当前文件尾端起,包括以后可能追加写到该文件的任何数据。假定,该文件偏移量处于文件尾端时,执行第一个write
,这个操作将文件延伸了1个字节,而该字节将被加锁。跟随其后的是解锁操作,其作用是对以后追加写到文件上的数据不再加锁。但在其之前刚追加写的一个字节则保留加锁状态。当执行第二个写时,文件尾端又延伸了1个字节,但该字节并未加锁。由此代码序列造成的文件锁状态如图14-10所示。
图14-10 文件区域锁
当对文件的一部分加锁时,内核将指定的偏移量变换成绝对文件偏移量。另外,除了指定一个绝对偏移量(SEEK_SET
)之外,fcntl
还允许我们相对于文件中的某个点指定该偏移量,这个点是指当前偏移量(SEEK_CUR
)或文件尾端(SEEK_END
)。当前偏移量和文件尾端可能会不断变化,而这种变化又不应影响现有锁的状态,所以内核必须独立于当前文件偏移量或文件尾端而记住锁。
如果想解除的锁中包括第一次write
所写的1 个字节,那么应指定长度为−1。负的长度值表示在指定偏移量之前的字节数。
6.建议性锁和强制性锁
考虑数据库访问例程库。如果该库中所有函数都以一致的方法处理记录锁,则称使用这些函数访问数据库的进程集为合作进程(cooperating process)。如果这些函数是唯一地用来访问数据库的函数,那么它们使用建议性锁是可行的。但是建议性锁并不能阻止对数据库文件有写权限的任何其他进程写这个数据库文件。不使用数据库访问例程库协同一致的方法来访问数据库的进程是非合作进程。
强制性锁会让内核检查每一个 open
、read
和 write
,验证调用进程是否违背了正在访问的文件上的某一把锁。强制性锁有时也称为强迫方式锁(enforcement-mode locking)。
从图14-2中可以看出,Linux 3.2.0和Solaris 10提供强制性记录锁,而FreeBSD 8.0和Mac OS X 10.6.8则不提供。强制性记录锁不是Single UNIX Specification的组成部分。在Linux中,如果用户想要使用强制性锁,则需要在各个文件系统基础上用mount命令的-o mand选项来打开该机制。
如果一个进程试图读read
或写write
一个强制性锁起作用的文件,而欲读、写的部分又由其他进程加上了锁,此时会发生什么呢?对这一问题的回答取决于3方面的因素:操作类型(read
或write
)、其他进程持有的锁的类型(读锁或写锁)以及read
或write
的描述符是阻塞还是非阻塞的。图14-11列出了8种可能性。
图14-11 强制性锁对其他进程的read和write的影响
除了图14-11中的read
和write
函数,另一个进程持有的强制性锁也会对open
函数产生影响。通常,即使正在打开的文件具有强制性记录锁,该open
也会成功。随后的read
或write
依从于图14-11中所示的规则。但是,如果欲打开的文件具有强制性记录锁(读锁或写锁),而且open
调用中的标志指定为O_TRUNC
或O_CREAT
,则不论是否指定O_NONBLOCK
,open
都立即出错返回,errno
设置为EAGAIN
。
只有Solaris对O_CREAT标志处理为出错。当打开一个具强制性锁的文件时,Linux允许指定O_CREAT标志。对O_TRUNC标志产生open出错是有意义的,因为对于一个文件来讲,若另一个进程持有它的读锁或写锁,那么它就不能被截短为0。但是对O_CREAT标志在返回时设置出错就没什么意义了,因为该标志表示,只有在该文件不存在时才创建,但由于另一个进程持有该文件的记录锁,所以该文件肯定是存在的。
这种open
的锁冲突处理方式可能会导致令人惊异的结果。在开发本节习题的时候,我们曾编写过一个测试程序,它打开一个文件(其模式指定为强制性锁),对该文件整体设置一把读锁,然后休眠一段时间。(回忆图 14-11,读锁应当阻止其他进程写该文件。)在这段休眠时间内,用某些典型的UNIX系统程序和操作符对该文件进行处理,发现下列情况。
- 可用ed编辑器对该文件进行编辑操作,而且编辑结果可以写回磁盘!强制性记录锁根本不起作用。用某些UNIX系统版本提供的系统调用跟踪特性,对ed操作进行跟踪分析发现,ed将新内容写到一个临时文件中,然后删除原文件,最后将临时文件名改为原文件名。强制性锁机制对
unlink
函数没有影响,于是这一切就发生了。
在FreeBSD 8.0和Solaris 10中,用truss(1)命令可以得到一个进程的系统调用跟踪信息。Linux 3.2.0出于相同的目的提供了strace(1)命令。Mac OS X 10.6.8提供了dtruss(1m)命令来追踪系统调用,但该命令的使用需要超级用户的权限。
- 不能用vi 编辑器编辑该文件。vi 可以读该文件的内容,但是如果试图将新的数据写到该文件中,就会出错返回(EAGAIN)。如果试图将新数据追加写到该文件中,则
write
阻塞。vi的这种行为与我们所希望的一样。 - 使用Korn shell的
>
和>>
操作符重写或追加写该文件,会产生出误信息“cannot create”。 - 在Bourne shell下使用
>
操作符也会出错,但是使用>>
操作符时只阻塞,在解除强制性锁后会继续进行处理。(这两种shell在执行追加写操作时之所以会产生的差异,是因为Kornshell以O_CREAT
和O_APPEND
标志打开文件,而上面已提及指定O_CREAT
会产生出错返回。但是, Bourne shell在该文件已存在时并不指定O_CREAT
,所以open
成功,而下一个write
则阻塞。)
产生的结果随所用操作系统版本的不同而不同。从这样一个习题中可见,在使用强制性锁时还需有所警惕。从ed实例可以看到,强制性锁是可以设法避开的。
一个恶意用户可以使用强制性记录锁,对大家都可读的文件加一把读锁,这样就能阻止任何人写该文件(当然,该文件应当是强制性锁机制起作用的,这可能要求该用户能够更改该文件的权限位)。考虑一个数据库文件,它是大家都可读的,并且是强制性锁机制起作用的。如果一个恶意用户要对整个这个文件持有一把读锁,其他进程就不能再写该文件。
实例
图14-12中的程序可以用于确定一个系统是否支持强制性锁机制。
#include "apue.h"
#include <errno.h>
#include <fcntl.h>
#include <sys/wait.h>
int
main(int argc, char *argv[])
{
int fd;
pid_t pid;
char buf[5];
struct stat statbuf;
if (argc != 2) {
fprintf(stderr, "usage: %s filename\n", argv[0]);
exit(1);
}
if ((fd = open(argv[1], O_RDWR | O_CREAT | O_TRUNC, FILE_MODE)) < 0)
err_sys("open error");
if (write(fd, "abcdef", 6) != 6)
err_sys("write error");
/* turn on set-group-ID and turn off group-execute */
if (fstat(fd, &statbuf) < 0)
err_sys("fstat error");
if (fchmod(fd, (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0)
err_sys("fchmod error");
TELL_WAIT();
if ((pid = fork()) < 0) {
err_sys("fork error");
} else if (pid > 0) { /* parent */
/* write lock entire file */
if (write_lock(fd, 0, SEEK_SET, 0) < 0)
err_sys("write_lock error");
TELL_CHILD(pid);
if (waitpid(pid, NULL, 0) < 0)
err_sys("waitpid error");
} else { /* child */
WAIT_PARENT(); /* wait for parent to set lock */
set_fl(fd, O_NONBLOCK);
/* first let's see what error we get if region is locked */
if (read_lock(fd, 0, SEEK_SET, 0) != -1) /* no wait */
err_sys("child: read_lock succeeded");
printf("read_lock of already-locked region returns %d\n",
errno);
/* now try to read the mandatory locked file */
if (lseek(fd, 0, SEEK_SET) == -1)
err_sys("lseek error");
if (read(fd, buf, 2) < 0)
err_ret("read failed (mandatory locking works)");
else
printf("read OK (no mandatory locking), buf = %2.2s\n",
buf);
}
exit(0);
}
图14-12 确定是否支持强制性锁
此程序首先创建一个文件,并使强制性锁机制对其起作用。然后程序分出一个父进程和一个子进程。父进程对整个文件设置一把写锁,子进程则先将该文件的描述符设置为非阻塞的,然后企图对该文件设置一把读锁,我们期望这会出错返回,并希望看到系统返回是 EACCES
或EAGAIN
。接着,子进程将文件读、写位置调整到文件起点,并试图读read
该文件。如果系统提供强制性锁机制,则 read
应返回EACCES
或 EAGAIN
(因为该描述符是非阻塞的),否则read
返回所读的数据。在Solaris 10上运行此程序(该系统支持强制性锁机制),得到:
$ ./a.out temp.lock
read_lock of already-locked region returns 11
read failed (mandatory locking works): Resource tem-porarily unavailable
查看系统头文件或intro(2)
手册页,可以看到errno
值11
对应于EAGAIN
。若在FreeBSD 8.0运行此程序,则得到:
$ ./a.out temp.lock
read_lock of already_locked region returns 35
read OK (no mandatory locking), buf = ab
其中,errno
值35
对应于EAGAIN。该系统不支持强制性锁。
实例
让我们回到本节的第一个问题:当两个人同时编辑同一个文件时将会怎样呢?一般的 UNIX系统文本编辑器并不使用记录锁,所以对此问题的回答仍然是:该文件的最后结果取决于写该文件的最后一个进程。
某些版本的vi编辑器使用建议性记录锁。即使我们使用这种版本的vi编辑器,它仍然不能阻止其他用户使用另一个没有使用建议性记录锁的编辑器。
若系统提供强制性记录锁,那么我们可以修改自己常用的编辑器来使用它(如果我们有该编辑器的源代码)。如果没有该编辑器的源代码,那么可以试一试下述方法。编写一个vi的前端程序。该程序立即调用fork,然后父进程只等待子进程完成。子进程打开在命令行中指定的文件,使强制性锁起作用,对整个文件设置一把写锁,然后执行vi。在vi运行时,该文件是加了写锁的,所以其他用户不能修改它。当vi结束时,父进程从wait
返回,自编的前端程序结束。
虽然可以编写这种类型的小型前端程序,但它却不起作用。问题出在大多数编辑器读它们的输入文件,然后关闭它。只要引用被编辑文件的描述符关闭了,那么加在该文件上的锁就被释放了。这意味着,在编辑器读了该文件的内容后,随即关闭了该文件,那么锁也就不存在了。这个前端程序中没有任何方法可以阻止这一点。
在第 20 章中,我们将使用数据库函数库中的记录锁来提供多个进程的并发访问。我们还将提供一些时间测量,以观察记录锁对进程的影响。