3、记录锁
当两个人同时编辑一个文件的时候,在大多数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 <fcntl.h>
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一起返回 */
};
这个结构描述的信息:
- 所需要的锁的类型:F_RDLCK(共享读锁),F_WRLCK(互斥写锁),或者F_UNLCK(解锁一个区域)。
- 将要加锁或者解锁的起始字节偏移(l_start和l_whence)。
- 区域的字节大小(l_len)。
- 可以阻塞当前进程(只通过F_GETLK返回)的持有锁的进程ID。
对于区域的上锁和解锁,有各种规则和标准。
- 用于指定区域的起始偏移的两个元素和lseek函数的最后两个参数类似。l_whence成员被设置为SEEK_SET,SEEK_CUR,或者SEEK_END。
- 锁的起始和扩展可以超过文件的结尾,但是不能越过文件的开头。
- 如果l_len为0,那么意思是说锁被扩展到尽可能大的文件偏移。这允许我们从文件的任意一个位置开始开始锁定一个区域,达到超过并包含后追加到文件结尾的数据(我们不用尝试猜测可能向文件添加了多少个字节)。
- 为了锁住整个文件,我们设置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这一个区域了。
举例
1、请求和释放锁的例子:下面的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; /* 相对于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))
2、测试锁的例子:这里给出一个lock_test函数,用于对锁的测试,如果锁存在,将会阻塞参数指定的特定请求,并且返回持有锁的进程ID。否则函数返回0(就是false)。
#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 或者 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命令,并不会获取我们自己是否持有锁,这样的信息。
3、死锁的例子:死锁出现在两个进程互相等待对方持有的锁住的资源的时候。当一个进程持有一个锁区域,然后它向另外一个进程申请锁,因为没有申请到而进入睡眠,这个时候就可能发生死锁。
这里就给出了一个例子,例子中父子进程分别持有锁,并且向文件中写数据,然后又分别向对方申请对方持有的锁。具体代码不给出了,具体参见参考资料。
当发现死锁的时候,内核会选择一个进程接收错误并且返回。在这个例子中,选择子进程接收错误并返回,但是这是和实现相关的特性。有些系统中总是选择子进程接收错误,还有些系统中总是选择父进程接受错误,还有些系统你会发现在尝试使用多个锁的时候,在父子进程中都会发生错误。
锁的继承和释放
有三个规则用来管理锁的继承和释放:
1、锁是和进程与文件相关的。这有两个含义。第一个含义很明显,就是当进程终止的时候,它所持有的所有的锁都会被释放;第二个含义就不是那么明显了,意思是当一个文件描述符被关闭的时候,进程持有的所有那个文件描述符所引用的文件上面的锁将都会被释放。具体点说:
fd1 = open(pathname, ...);
read_lock(fd1, ...);
fd2 = dup(fd1);
close(fd2);
当我们调用close(fd2)的时候,在fd1上面获取的锁也会被释放(因为fd1引用同一个文件)。我们使用dup替代open的时候,也会发生同样的事情如下:
fd1 = open(pathname, ...);
read_lock(fd1, ...);
fd2 = open(pathname, ...)
close(fd2);
这里在fd2上面打开的文件和fd1是同样的文件。
2、子进程不会通过fork继承锁。这个意思是说,当父进程获取锁之后调用fork,那么子进程对于父进程所获取的那个锁来说是另外一个进程。子进程需要调用fcntl函数向继承自父亲的文件描述符申请自己的锁。这个是很重要的,因为锁存在的意义就是阻止多个进程同时向同样的文件中写入数据,如果子进程通过fork从父进程那里继承了锁,那么父子进程就都可以同时向同样的文件中写入数据了。
3、锁通过exec函数调用,被新的进程继承。但是需要注意的是,如果文件描述符号的close-on-exec标记被设置了,那么当这个文件描述是符号在exec中被关闭的时候,这个文件上面相应的所有的锁都会被释放。
FreeBSD上面的实现
这里给出了一个FreeBSD上面实现时的数据结构,可以帮助我们理解前面规则1中说的“锁是和进程与文件相关的”意思。
代码如下:
fd1 = open(pathname, ...);
write_lock(fd1, 0, SEEK_SET, 1);
if ((pid = fork()) > 0) {
fd2 = dup(fd1);
fd3 = open(pathname, ...);
} else if (pid == 0) {
read_lock(fd1, 1, SEEK_SET, 1);
}
pause();
由这个代码,会在内存中生成两个进程的进程表,文件表,以及文件索引表,其图示关系这里就不给出了,具体参见参考资料。这里给出一些相对关键的描述:
我们可以知道,运行完前面的代码之后,无论是父还是子进程,其文件描述符号fd1,fd2,fd3无论是如何打开的,最终都指向了同一个索引节点(因为它们本身就代表的同一个文件)就是i-node结构。我们看到在这个i-node结构中有一个lockf结构的成员链表,链表中的每一个lockf结构变量都描述了一个特定进程的锁区域,其中lockf结构中有一个成员变量指明这个锁对应的进程的进程ID。从这个例子中,我们可以知道,目前i-node结构中得lockf结构链表中有两个成员,一个表示子进程的锁,一个表示父进程的锁。
在父进程中,关闭fd1,fd2,fd3中的任何一个,都会导致父进程的锁被释放。释放的时候,内核会遍历i-node中的锁链表,找到调用进程对应的锁并且释放。内核无法知道也不关心父进程是通过这三个文件描述符号中的哪一个来获取到锁的。
例子:在前面我们看到过守护进程使用文件锁来确保同一时刻只有一个守护进程的实例在运行。这里,给出前面守护进程给文件加写锁所使用的那个lockfile函数实现。
我们可以使用如下宏定义将lockfile定义为write_lock的一个宏(write_lock前面已经给出过定义):
#define lockfile(fd) write_lock((fd), 0, SEEK_SET, 0)
代码如下:
#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));
}
关于文件结尾的加锁解锁
向相对文件结尾的位置加锁或者解锁的时候需要注意一些事情。大多数的实现会根据文件当前的位置和长度,将值为SEEK_CUR或SEEK_END的l_whence转换成文件的绝对位移。然而我们需要经常相对于文件的当前位置和长度指定一个锁(一个“整体”的操作),因为我们不能使用lseek来获得文件的当前位移(这是因为我们不持有文件的锁,这样其他的进程就有机会在lseek和lock之间改变文件的长度)。
考虑如下的步骤:
writew_lock(fd, 0, SEEK_END, 0);
write(fd, buf, 1);
un_lock(fd, 0, SEEK_END);
write(fd, buf, 1);
这个代码序列,可能不会做到你所期望的事情。它从当前的文件结尾获取到了一个可以扩展的写锁,这个锁将会覆盖将来我们追加到文件中的任何数据(但是这个锁当前没有锁住文件的任何内容,特别注意没有锁最后一个字节)。假设我们做第一个写操作的时候正在文件的结尾,那样将会使文件扩展一个字节并且那个扩展的字节将会被锁住。接下来的unlock操作会使将来追加到文件中的数据不再被锁住了,但是它并没有去掉当前新增的文件的最后一个字符的锁(因为文件最后一个字符的位置是SEEK_END-1)。当第二个写操作发生的时候,文件的结尾又被扩展了一个字节,但是这个字节就不是被锁的了。通过这一系列的操作,最后文件中有一个被锁住的字节(就是第一次写的那个字符被锁住了),参考资料中用图示的方式对其进行了描述,这里就省略了。
当文件的一个部分被锁住的时候,内核会将指定的偏移转换成文件的绝对偏移位置。除了使用SEEK_SET可以指定文件的绝对偏移之外,fcntl还允许我们指定相对于文件某个位置的偏移:SEEK_CUR指定相对当前位置的偏移,SEEK_END指定相对文件结尾的偏移。内核需要记住相应的锁,但是这个锁本身和文件的当前位置以及文件结尾没有关系(尽管这个锁是有范围的),因为当前的位置以及文件的结尾是不断变化的,但是不能因为这些属性发生了变化就改变已经存在的锁的状态(也就是说这个覆盖了一定范围的锁是固定的,不会随着文件的长度当前文件的指针得变化而变化和移动)。
这里,如果我们想要将第一次写入的字节的锁移除,那么我们可以指定length为-1。用负数的长度值代表指定位置的前面。
建议锁和强制锁
假设有一个包含访问数据库函数的库。如果库中的所有函数以一种一直的方式处理记录锁,那么我们就说任何使用这些函数访问数据库的进程集合都是协作的进程。如果只使用这些函数来访问数据库,那么可以让这些数据库访问函数使用强制锁。但是强制锁不能组织其他具有写权限的进程向数据库写数据。这些进程因为没有使用那个库中的函数来访问数据库,所以它们就是非协作进程。
强制锁导致内核对进程所访问的文件的每一次打开和读写进行检查和验证,看它们是否符合锁的规则。有时候,强制锁也被称作强制模式(enforcement-mode)的锁。
我们在前面的资料中可以知道,Linux2.4.22和Solaris9提供了强制记录锁,但是FreeBSD5.2.1和MacOS X 10.3并没有提供。强制锁并不是Single UNIX Specification的一部分。在Linux上,如果你想要使用强制锁,需要通过使用mount命令的"-o mand"选项在文件系统的级别上将它激活。
对于一个特定的文件的强制锁,我们是通过打开set-group-ID位并且关闭group-execute位来将其激活的。可以这样做的原因是,本来关闭group-execute位的同时打开set-group-ID是没有意义的(我们可以查阅前面的章节来确认这一点),所以 SVR3的设计者们选择了这个方式来指定对一个特定文件的锁不是建议锁(advisory locking)而是强制锁(mandatory locking)。
当一个进程尝试读写一个文件,而其他进程持有这个文件的读或者是写的强制锁的时候,运行的结果取决于进程的读写类型,其他进程持有的锁类型,以及操作文件是否阻塞。
文中用一个表描述了这个规则,这里就不重复了,具体参见参考资料,大致描述一下这个表格:
表格的大致意思就是:和前面的读写锁规则类似,读是可以共享操作的,写却是互斥的,如果出现了不能读或者写的情况,那么根据所操作的文件的特性要么阻塞,要么返回EAGAIN错误。
除了上述表格描述的读写函数的行为,其它进程持有强制锁,对文件的open操作也会有所影响。一般来说,即使其他进程持有文件的强制锁,open函数也会成功地返回,而接下来的读写操作就会依照上述表格的规则进行。但是调用open时候指定了O_TRUNC 或者 O_CREAT,那么无论是否指定了O_NONBLOCK,open都会立即返回EAGAIN错误。
只有Solaris将O_CREAT视为一种错误的情况,Linux允许open的O_CREAT标记在文件持有强制锁的时候也可以被指定。对于O_TRUNC生成open的错误是有意义的,因为如果文件具有读或者写锁的时候,无法对文件进行truncate。但是为open的O_CREAT生成错误就没有太大的意义了,因为这个标记的意思是当文件不存在的时候创建一个文件,而只有文件的存在的时候,其它进程才会持有这个文件的记录锁。
open调用对锁冲突进行如此的处理,会导致一些意外的结果。在本章的一个练习中,有一个测试程序,它运行的时候打开一个可以具有强制锁模式的文件,然后申请了整个文件的读锁,然后进入了睡眠。在睡眠期间,一些典型的UNIX系统应用程序行为的如下:
a)ed编辑器可以编辑这个文件,并且结果会写入到磁盘中去!这个时候强制锁记录的检查没有一点效果,使用系统调用trace可以看到,ed编辑器是把新的内容写到了一个临时的文件中,然后将原始的文件删除,再将临时的文件重新命名为原始的文件。由于强制锁对unlink函数没有效果,所以发生了这样的情况。
在Solaris中,进程的trace系统调用通过truss命令获得。FreeBSD和Mac OS X使用ktrace和kdump命令。Linux使用strace命令来跟踪进程发起的系统调用。
b)vi编辑器无法编辑文件。它可以读取文件中的内容,但是当它尝试向文件写入新的内容的时候,会返回EAGAIN错误。如果我们尝试向文件中追加新的数据,那么写操作会被阻塞。vi所表现出来的行为就是我们所期望的行为。
c)使用Korn shell的>和>>操作符号来覆盖或者向文件追加内容会导致"cannot create"错误。
d)使用Bourne shell的时候,>操作符号会返回错误,但是>>符号会阻塞,直到强制锁被移除,然后继续执行。(导致两种shell的>>追加操作符号不同的原因是:Korn shell使用O_CREAT和O_APPEND来打开文件,而我们前面说过使用O_CREAT会产生错误;Bourne Shell在文件已经存在的时候,不指定O_CREAT,所以打开操作是成功的,但是后来的写操作却发生了阻塞)
根据你所使用的系统的不同,结果也会不同。ed编辑器的处理却绕过了这些不同。另外,注意一个居心叵测的用户可能会利用Mandatory来达到他邪恶的目的。
例子:参考资料中给出了一段程序用来检测一个系统是否支持强制锁。
这个程序创建了一个文件并且打开这个文件的强制锁特性。程序然后分成两个进程:父进程和子进程。父进程获取整个文件的写锁。子进程首先设置它自己的文件描述符号为非阻塞状态,然后尝试获取文件的读锁,这样期望会获得一个错误,让我们看到系统是返回EACCES或者EAGAIN。然后,子进程回到文件的开始,尝试从文件中读取数据。如果强制锁是支持的,那么读的操作将会返回EACCES或者EAGAIN(因为文件描述符号是非阻塞的),否则读操作会返回它所读取的数据。(通过上面的这个描述,我们可以知道,如果锁是非强制的,那么我们需要自己通过对锁的申请来控制数据的访问,而如果锁是强制的话读写的系统调用里面就进行了自动的检测我们其实就不用显式加锁了_)。
程序的描述大致如上,源代码就不给出了,具体参见参考资料:
在Solaris 9上面运行程序,那么会返回如下输出:
$ ./a.out temp.lock
read_lock of already-locked region returns 11
read failed (mandatory locking works): Resource temporarily unavailable
通过"man 2 intro"我们可以知道,errno为11表示EAGAIN错误。
在FreeBSD 5.2.1上面运行这个程序,我们得到如下输出:
$ ./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程序执行excute。当vi运行的时候,文件就是写状态的了,所以其它的用户无法修改它。当vi结束的时候,父进程等待到了子进程并返回,然后我们的前端程序就结束了。
事实上,我们可以写一个这样的小的程序前端,但是它不能工作。问题是,大多数的编辑器读取它的输入文件,然后就将文件关闭了。这样,当文件描述符号被关闭的时候,这个文件上面的锁就被释放了。也就是说,编辑器在读取到文件内容之后,就关闭了它所打开的文件,导致锁被释放。我们没有办法在前端程序中阻止这个事情的发生。
我们将在后面的章节中,在一个数据库程序库中使用记录锁,提供多进程并发访问的功能。我们也给出了它的执行时间,并看到记录锁对一个进程到底有什么影响。
网友评论