UNIX高级编程指南(十三)之三
p align="JUSTIFY">实例-死锁
如果两个进程相互等待对方持有并且不释放(锁定)的资源时,则这两个进程就处于死锁状态。如果一个进程已经控制了一个文件中的一个加锁区域,然后它又试图对另一个进程控制的区域加锁,则它就会睡眠,在这种情况下,有发生死锁的可能性。
程序12.4示出了一个死锁的例子。子进程锁字节0,父进程锁字节1。然后,它们中的每一个又试图锁对方已经加了锁的字节。在该程序中使用了8.8节中介绍的父-子进程同步例程(TELL_xxx,WAIT_xxx),使得对方都能建立第一把锁。运行程序12.4得到:
#include
#include
#include
#include ourhdr.h
static void lockabyte(const char *, int, off_t);
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);
}
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 %d/n, name, offset);
}
程序12.4 死锁检测实例
$ a.out
child:got the lock,byte 0
parent:got the lock,byte 1
child:writew_lock error:Deadlock situation detected/avoided
parent:got the lock,byte 0
检测到死锁时,系统核必须选择一个进程收到出错返回。在本实例中,选择了子进程,这是一个实现? 。当此程序在另一个系统上运行时,一半次数是子进程接到出错信息,另一半则是父进程。
锁的隐含继承和释放
关于记录锁的自动继承和释放有三条规则:
1. 锁与一个进程、一个文件两方面有关。这有两重含意。第一重是很明显的,当一个进程终止时,它所建立的锁全部释放。第二重意思就不很明显,任何时候关闭一个描述符时,则该进程通过这一描述符可以存访的文件上的任何一把锁都被释放(这些锁都是该进程设置的)。这就意味着如果执行下列四步:
fd1=open(pathname,…);
read_lock(fd1,…);
fd2=dup(fd1);
close(fd2);
则在close(fd2)后,在fd1上设置的锁被释放。如果将dup代换为open,其效果也一样:
fd1=open(palhname,…);
read_lock(fd1,…);
fd2=open(palhname,…);
close(fd2);
2. 由fork产生的子程序不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然后调用fork,那么对于父进程获得的锁而言,子进程被视为另一个进程,对于从父进程处继承过来的任一描述符,子进程要调用fcntl以获得它自己的锁。这与锁的作用是相一致的。锁的作用是阻止多个进程同时写同一个文件(或同一文件区域)。如果子进程继承父进程的锁,则父、子进程就可以同时写同一个文件。
3. 在执行exec后,新程序可以继承原执行程序的锁。POSIX.1没有要求这一点。但是,SVR4和4.3+BSD都支持这一点4.3+BSD的实现先简要地观察4.3+BSD实现中使用的数据结构,从中可以看到锁是与一个进程、一个文件相关联的。
考虑一个进程,它执行下列语句(忽略出错返回):
fd1 = open(pathname, … );
write_lock(fd1, 0, SEEK_SET, 1); 父进程在字节0写?
if (fork() 0) {
fd2 = dup(fdl);
fd3 = open(pathname, …);
pause;
}
else {
read_lock(fd1, 1, SEEK_SET, 1); 子进程在字节1读?
pause;
}
在以前的图3.4和8.1中已显示了open、fork以及dup后的数据结构有了记录锁后,在原来的这些图上新加了flock结构,它们由i_node结构开始相互连接起来。注意,每个flock结构说明了一个给定进程的一个加锁区域。在图中显示了两个flock结构,一个是由父进程调用write_lock形成的,另一个则由子进程调用read_lock形成的。每一个结构都包含了相应进程ID。
在父进程中,关闭fd1、fd2和fd3中的任何一个都释放由父进程设置的写锁。在关闭这三个描述符中的任何一个时,系统核会从该描述符所关连的i_node开始,逐个检查flock连接表中各项,释放由调用进程持有的各把锁。系统核并不清楚也不关心父进程是用哪一个描述符来设置这把锁的。
实例:
建议性锁可由精灵进程使用以保证该精灵进程只有一个副本在运行。在起动时,很多精灵进程都把它们的进程ID写到一个它们各自专用的一个PID文件上。当系统停机时,可以从这些文件中取用这些精灵进程的进程ID。防止一个精灵进程有多份副本同时运行的方法是:在精灵进程开始运行时,在它的进程ID文件上企图设置一把写锁。如果在它运行时一直保持这把锁,则就不可能再起动它的其它副本。程序1.2.5实现了这一技术。
因为进程ID文件可能包含以前的精灵进程ID,而且其长度还可能长于当前进程的ID,例如该文件中以前的内容可能是12345/n,而现在的进程ID是654,我们希望该文件现在只包含654/n,而不是654/n5,所以在写该文件时,先将其截短为0。注意,要在设置了锁之后再调用截短文件长度的函数ftruncate。在调用open时不能指定O_TRUNC,因为这样做会在有一个这种精灵进程运行并对该文件加了锁时也会使该文件截短为0。(如果使用强制性锁而不是建议性锁,则可使用O_TRUNC。在本节最后部分将讨论强制性锁。)
在本实例中,也对该描述符设置exec时关闭(close-on-exec)标志。这是因为精灵进程常常fork并exec其它进程,无需在另一个进程中使该文件也处在打开状态。
#include
#include
#include
#include
#include ourhdr.h
#define PIDFILE daemon.pid
int
main(void)
{
int fd, val;
char buf[10];
if ( (fd = open(PIDFILE, O_WRONLY | O_CREAT, FILE_MODE)) 0)
err_sys(open error); /* try and set a write lock on the entire file */
if (write_lock(fd, 0, SEEK_SET, 0) 0) {
if (errno == EACCES || errno == EAGAIN) exit(0);
/* gracefully exit, daemon is already runing */
else
err_sys(write_lock error);
} /* truncate to zero length, now that we have the lock */
if (ftruncate(fd, 0) 0)
err_sys(ftruncate error); /* and write our process ID */
sprintf(buf, %d/n, getpid());
if (write(fd, buf, strlen(buf)) != strlen(buf)) err_sys(write error);
/* set close-on-exec flag for descriptor */
if ( (val = fcntl(fd, F_GETFD, 0)) 0) err_sys(fcntl F_GETFD error);
val |= FD_CLOEXEC;
if (fcntl(fd, F_SETFD, val) 0) err_sys(fcntl F_SETFD error);
/* leave file open until we terminate: lock will be held */
/* do whatever ... */
exit(0);
}
程序12.5 精灵进程阻止其多份副本同时运行的起动代码