第十二章 高级I/O
时间:2005-08-31 来源:mantou
12.1 引言
本章内容包括:非阻塞I/O、记录锁、系统V流机制、I/O多路转接(select和poll
函数)、readv和writev函数,以及存储映照I/O(mmap)。在第十四章、十五章中
说明的进程间通信,以及以后各章中的很多实例都要使用本章所述的概念和函数。
12.2 非阻塞I/O
在10.5节中曾将系统调用分成两类:低速系统调用和其它。低速系统调用是可能会
使进程永远阻塞的一类系统调用:
l 如果数据并不存在,则读文件可能会使调用者永远阻塞(例如读管道,终端设备
和网络设备)。
l 如果数据不能立即被接受,则写这些同样的文件也会使调用者永远阻塞。
l 在某些条件发生之前,打开文件会被阻塞(例如打开一个终端设备可能需等待到
一个连接的调制解调器应答;又例如若以只写方式打开FIFO,那么在没有其它进程
已用读方式打开该FIFO时也要等待)。
l 对已经加上了强制性记录锁的文件进行读、写。
l 某些ioctl操作。
l 某些进程间通信函数(第十四章)。
虽然读、写磁盘文件会使调用在短暂时间内阻塞,但并不能将它们视为"低速"。
非阻塞I/O使我们可以调用不会永远阻塞的I/O操作,例如open,read和write。如果
这种操作不能完成,则立即出错返回,表示该操作如继续执行将继续阻塞下去。
对于一个给定的描述符有两种方法对其指定非阻塞I/O:
1. 如果是调用open以获得该描述符,则可指定O_NONBLOCK标志(3.3节)。
2. 对于已经打开的一个描述符,则可调用fcntl,对其打开O_NONBOCK文件状态标
志(3.13节)。在程序3.5中的函数可用来为一个描述符打开任一文件状态标志。
早期的系统V版本使用标志O_NDELAY指定非阻塞方式。在这些版本中,如果无数据
可读,则read返回值0。而Unix又常将read的返回值0解释为文件结束,两者有所混
淆。PISIX.1则提供了一个非阻塞标志,它的名字和语义都与O_NDELAY不同。PISI
X.1要求,对于一个非阻塞的描述符如果无数据可读,则read返回-1,并且errno被
设置为EAGAIN。SVR4支持较老的O_NDELAY和POSIX.1的O_NONBLOCK,但在本书的实
例中只使用POSIX.1规定的特征。O_NDELAY是为了向后兼容性,不应在新应用程序
中使用。
4.3BSD为fcntl提供FNDELAY标志,其语义也稍有区别。它不只影响该描述符的文件
状态标志,它将终端设备或套接口的标志更改成非阻塞的,因此影响了终端或套接
口的所有用户,不只是影响共享同一文件表项的用户(4.3BSD非阻塞I/O只对终端
和套接口起作用)。如果对一个非阻塞描述符的操作不能无阻塞地完成,那么4.3
BSD返回EWOULDBLOCK。4.3+BSD提供POSIX.1的O_NONBLOCK标志,但其语义却类似于
4.3BSD的FNDELAY。非阻塞I/O通常用来处理终端设备或网络连接,而这些设备通常
一次由一个进程使用。这就意味着BSD语义的更改通常不会影响我们。出错返回EW
OULDBLOCK而不是POSIX.1的EAGAIN,这造成了可移植性问题,我们必须处理这一问
题。4.3+BSD也支持FIFO,非阻塞I/O也对FIFO起作用。
实例
程序12.1是一个非阻塞I/O的实例,它从标准输入读100,000字节,并试图将它们
写到标准输出上。该程序先将标准输出设置为非阻塞的,然后用for循环进行输出
,每次写的结果都在标准出错上打印。函数ctl-f1类似于程序3.5中的set-f1,但
与set-f1的功能相反,它清除1个或多个标志位。
#include <sys/types.h>
#include <errno.h>
#include <fcntl.h>
#include "ourhdr.h"
char buf[100000];
int
main(void)
{
int ntowrite, nwrite;
char *ptr;
ntowrite = read(STDIN_FILENO, buf, sizeof(buf));
fprintf(stderr, "read %d bytes ", ntowrite);
set_fl(STDOUT_FILENO, O_NONBLOCK); /* set nonblocking */
for (ptr = buf; ntowrite > 0; ) {
errno = 0;
nwrite = write(STDOUT_FILENO, ptr, ntowrite);
fprintf(stderr, "nwrite = %d, errno = %d ", nwrite, errno);
if (nwrite > 0) {
ptr += nwrite;
ntowrite -= nwrite;
}
}
clr_fl(STDOUT_FILENO, O_NONBLOCK); /* clear nonblocking */
exit(0);
}
程序12.1 长的非阻塞写
若标准输出是普通文件,则可以期望write只执行一次。
$ ls -l /etc/termcap
印文件长度
-rw-rw-r-1 root 133439 Oct 11 1990 /etc/termcap
$a.out < /etc/termcap >temp.file 先试一普
文件
read 100000 bytes
nwrite-100000, errno=0 一次写
$ls -l temp.file
验输出文件长度
-rw-rw-r-1 stevens 100000 Nev 21 16:27 temp.file
但是,若标准输出是终端,则可期望write有时会返回一个数字,有时则出错返回
。下面是在一个系统上运行程序12.1的结果:
$ a.out < /etc/termcap 2>stderr.out 向终端输出
大量输出至终端
$ cat stderr.out
read 100000 bytes
nwrite=8192, errno=0
nwrite=8192, errno=0
nwrite=-1, errno=11
庵执?11次
…
nwrite=4096,errno=0
nwrite=-1, errno=11
庵执?98次
…
nwrite=4096,errno=0
nwrite=-1, errno=11
庵执?04次
…
nwrite=4096,errno=0
nwrite=-1, errno=11
庵执?047次
…
nwrite=-1, errno=11
庵执?046次
…
nwrite=4096,errno=0 …等等
在该系统上,errno11是EAGAIN。此系统上的终端驱动程序总是一次接收4096或81
92字节。在另一个系统上,前三个write返回2005,1822和1811,然后是96次出错
返回,接着是返回1864等等。
每个write能写多少字节是依赖于系统的。
此程序在SVR中运行,则其结果完全不同于前面的情况。当输出到终端上时,输出
该整个输入文件只需要一个write。显然,非阻塞方式并不构成区别。创建了一个
较大的输入文件,并且系统为运行该程序增加了程序缓存。程序的这种运行方式(
即输出一整个文件,只调用一次write)一直继续到输入文件长度到达约700,000
字节。到达此长度后,每一个write都返回出错EAGAIN。(输入文件则决不会再输
出到终端上-该程序只是连续地产生出错消息流。)
所以发生这种情况是因为:在SVR4中终端驱动程序通过流I/O系统连接到程序。(
12.4节将详细说明流。)流系统有它自己的缓存,它一次能从程序接收更多的数据
。SVR4的行为也依赖于终端类型-硬连线终端、控制台设备或伪终端。
在此实例中,程序发出了数千个write调用,但是只有20个左右是真正输出数据的
,其余的则出错返回。这种形式的循环称为轮询,在多用户系统上它浪费了CPU时
间。在12.5节中,我们将会看到对于非阻塞描述符的I/O,多路转接是进行这种操
作的更加有效的方法。
在第十七章,我们将会用到非阻塞I/O,那时我们要输出到终端设备(PostScript
打印机)而且不希望在write上阻塞。
12.3 记录锁(Record Locking)
当两个人同时编辑一个文件时,其后果将如何呢?在很多Unix系统中,该文件的最
后状态取决于写该文件的最后一个进程。但是对于有些应用程序,例如数据库,有
时进程需要确保它正在单独写一个文件。为了向进程提供这种能力,较新的Unix系
统提供了记录锁机制。(在第十六章中包含了使用记录锁的数据库子程序库。)
记录锁机制的功能是:一个进程正在读或修改文件的某个部分时,可以阻止其它进
程修改同一文件区。对于Unix,"记录"这个定语也是误用,因为Unix系统核根本没
有使用文件记录这种概念。一个更适合的术语可能是"区域锁",因为它锁定的只是
文件的一个区域(也可能是整个文件)。
历史
图12.1 示出了各种Unix系统提供的不同形式的记录锁。
图12.1 各种Unix系统支持的记录锁形式
在本节的最后部分将说明建议性锁和强制性锁之间的区别。POSIX.1选择了以fcnt
l函数为基础的系统V风格的记录锁。这种风格也得到4.3BSD Reno版本的支持
。
早期的贝克莱版只支持BSD flock函数。此函数只锁整个文件,而不锁文件中的一
个区域。但是POSIX.1的fcntl函数可以锁文件中的任一区域,大至整个文件,小至
单个字节。
在本书中只说明POSIX.1的fcntl锁。系统V的lockf函数只是fcntl函数的一个界面
。
记录锁是1980年由John Bass最早加到Version7上的。系统核中相应系统
调用入
口表项是名为locking的函数。此函数提供了强制性记录锁功能,它传到
了很多制
造商的系统III版本。Xenix系统采用了此函数,SVR4在Xenix兼容库中仍
旧支
持该函数。
SVR2是系统V中第一个支持fcntl风格记录锁的版本(1984)。
fcntl记录锁
3.13节中已经给出了fcntl函数的原型,为了叙说方便,这里再重复一次。
_______________________________________________________________________
________
#include <sys/types.h>
#include <unistd.h>
#include <fcnt1.h>
int fcnt1(int filedes,int cmd,…/* struct flock *flockptr */);
返回:若成功依赖于cmd(见下),?nbsp;
错为-1
_______________________________________________________________________
________
对于记录锁,cmd是F_GETLK、F_SETLK或F_SETLKW。第三个参数(我们将其称为fl
ockptr)是一个指向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; /*长度(字节),O表示锁至EOF */
pid_t l_pid; /*随F--FETLK命令返回
}
flock结构说明:
l 所希望的锁类型:F_RDLCK(共享读锁)、F_WRLCK(独占性写锁)、或F_UNLCK
(解锁一个区域)
l 要加锁或解锁的区域的起始地址,它由l_stant和l__whence两者决定。l_stat是
相对位移量(字节),l_whence则决定了相对位移康钠鸬恪U庥雔seek中的使用
方法一样。
l 区域的长度,这由l_len表示。
关于加锁和解锁区域的说明还要注意下列各点:
l 该区域可以在当前文件尾端处开始或超过其尾端处开始,但是不能在文件起始位
置之前开始或越过该起始位置。
l 如若l_len为0,则表示锁的区域从其起点(由l_start和l_whence决定)开始直
至最大可能位置为止。也就是不管添写到该文件中多少数据,它都处于锁的范围。
l 为了锁整个文件,通常的方法是将l_start说明为0,l_whence说明为SEEK_SET,
l_len说明为0。
上面提到了两个锁类型:共享读锁(l_type为F_RDLCK)和独占写琐(F_WRLCK)。
基本规则是:多个进程在一个给定的字节上可以有一把共享的读锁,但是在一个给
定字节上的写锁则只能由一个进程独用。更进一步而言,如果在一个给定加字节上
已经有一把或多把读锁,则不能在该字节上再加写锁;如果在一个字节上已经有一
把独占性的写锁,则不能再对它加任何读锁。在图12.2中示出了这些规则。
图12.2 不同类型锁之间的兼容性
为了加读锁,该描述符必须是读打开,为了加写锁,该描述符必须是写打开。
现在说明fcntl函数的三种命令。
F_GETLK 决定由flockptr所说明的锁是否被另外一把锁所排斥(阻塞)。如
果存在一把锁,它阻止创建由flockptr所描述符的锁,则这把现存的锁的信息写到
flockptr指向的结构中;如果不存在这种情况,则除l_type设置为F_UNLCK之外,
flockptr所指向结构中的其它信息、保持不变。
F_SETLK 设置由flockptr所描述的锁。如果试图建立一把按上述兼容性规则
并不允许的锁,则fcntl立即出错返回,此时errno设置为EACCES或EAGAIN。
SVR2和SVR4返回EACCES,但手册页警告将来返回EAGAIN。4.3+BS
D则返回EAGAIN。POSIX.1允许这两种情况。
此命令也用来清除由flockptr说明的锁(l_type为F_UNLCK)。
F_SETLKW 这是F_SETLK的阻塞版本(命令名中的W表示等待(wait))。如果
由于存在其它锁,那么按兼容性规则由flockptr所要求的锁不能被创建,则调用进
程睡眠。如果捕捉到信号则睡眠中断。
应当了解,用F_GETLK测试能否建立一把锁,然后用F_SETLK和F_SETLKW企图建立一
把锁,这两者不是一个原子操作。在这两个操作之间可能会有另一个进程插入并建
立一把相关的锁,使原来测试到的情况发生变化,如果不希望在建立锁时可能产生
的长期阻塞,则应使用F_SETLK,并对返回结果进行测试,以判别是否成功地建立
了所要求的锁。
在设置或释放在一个文件上的一把锁时。系统按需组合或裂开相邻区。例如若100
-199字节是加锁的区,然后解锁第150字节,则系统核将维持两把锁,一把是从10
0-149字节,另一把是从151-199字节。
实例-要求和释放一把锁
为了免于每次分配flock结构,然后又填入各项信息,可以用程序12.2中的函数lo
ck_reg来处理这些细节。
#include <sys/types.h>
#include <fcntl.h>
#include "ourhdr.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) );
}
程序12.2 锁和解锁一个文件区域的函数
因为大多数锁调用是锁或解锁一个文件区域(命令F_GETLK很少使用)。我们通常
使用下列五个宏,它们都定义在ourhdr.h中(附录B)。
#define read_lock(fd,offset,whence,len)
lock_reg(fd,F_SETLK,F_RDLCK,offset,whence,len)
#define needw_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函数中的同样顺序定义了这些宏中的三个参数。
实例-测试一把锁
程序12.3定义了一个函数lock_test,可用其测试一把锁。
#include <sys/types.h>
#include <fcntl.h>
#include "ourhdr.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 is not locked by anothe
proc */
return(lock.l_pid); /* true, return pid of lock owner */
}
程序12.3 测试一个锁条件的函数
如果存在一把锁,它阻塞由参数说明的锁,则此函数返回持有这把现存锁的进程的
ID,否则此函数返回0。通常用下面两个宏来调用此函数(它们也定义在ourhdr.h
)。
#define is_read_lockable(fd,offset,whence,len)
lock_test(fd,F_RDLCK,offset,whence,len)
#define is_write_lockable(fd,offset,whence,len)
lock_test(fd,F_WRLCK,offset,whence,len)
实例-死锁
如果两个进程相互等待对方持有并且不释放(锁定)的资源时,则这两个进程就处
于死锁状态。如果一个进程已经控制了一个文件中的一个加锁区域,然后它又试图
对另一个进程控制的区域加锁,则它就会睡眠,在这种情况下,有发生死锁的可能
性。
程序12.4示出了一个死锁的例子。子进程锁字节0,父进程锁字节1。然后,它们中
的每一个又试图锁对方已经加了锁的字节。在该程序中使用了8.8节中介绍的父-子
进程同步例程(TELL_xxx,WAIT_xxx),使得对方都能建立第一把锁。运行程序12
.4得到:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#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 ", 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写?nbsp;
if (fork() > 0) {
fd2 = dup(fdl);
fd3 = open(pathname, …);
pause;
} else {
read_lock(fd1, 1, SEEK_SET, 1); 子进程在字节1读?nbsp;
pause;
}
图12.3 显示了父、子进程暂停(执行pause( ))后的数据结构情况。
图12.3 关于记录锁的4.3+BSD数据结构
在以前的图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,而且其长度还可能长于当前进程的I
D,例如该文件中以前的内容可能是12345 ,而现在的进程ID是654,我们希望该
文件现在只包含654 ,而不是654 5,所以在写该文件时,先将其截短为0。注意
,要在设置了锁之后再调用截短文件长度的函数ftruncate。在调用open时不能指
定O_TRUNC,因为这样做会在有一个这种精灵进程运行并对该文件加了锁时也会使
该文件截短为0。(如果使用强制性锁而不是建议性锁,则可使用O_TRUNC。在本节
最后部分将讨论强制性锁。)
在本实例中,也对该描述符设置exec时关闭(close-on-exec)标志。这是因为精
灵进程常常fork并exec其它进程,无需在另一个进程中使该文件也处在打开状态。
#include <sys/types.h>
#include <sys/stat.h>
#include <errno.h>
#include <fcntl.h>
#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 ru
ning */
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 ", 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 精灵进程阻止其多份副本同时运行的起动代码
实例
在相对文件尾端加锁或解锁时需要特别小心。大多数实现按照I_whence的SEEK_CU
R或SEEN_END值,用文件当前位置或当前长度以及l_start得到绝对的文件位移量。
但是,通常我们需要相对于文件的当前位置或当前长度指定一把锁。
程序12.6写一个文件,一次一个字节。每次循环中,从文件当前尾端开始处加锁直到
将来可能扩充到的尾端为止(最后一个参数,长度,指定为0),然后写1个字节。
然后解除这把锁,写另一个字节。如果系统用"从当前尾端开始,直到将来可能扩
充的尾端"这种记法来跟踪锁,那么这段程序能够正常工作。但是如果系统将相对
位移量变换成绝对位移量就会有问题。在SVR4中运行此程序的确会发生问题:
#include <sys/types.h>
#include <sys/stat.h>
/* 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 精灵进程阻止其多份副本同时运行的起动代码
实例
在相对文件尾端加锁或解锁时需要特别小心。大多数实现按照I_whence的SEEK_CU
R或SEEN_END值,用文件当前位置或当前长度以及l_start得到绝对的文件位移量。
但是,通常我们需要相对于文件的当前位置或当前长度指定一把锁。
程序12.6写一个文件,一次一个字节。每次循环中,从文件当前尾端开始处加锁直到
将来可能扩充到的尾端为止(最后一个参数,长度,指定为0),然后写1个字节。
然后解除这把锁,写另一个字节。如果系统用"从当前尾端开始,直到将来可能扩
充的尾端"这种记法来跟踪锁,那么这段程序能够正常工作。但是如果系统将相对
位移量变换成绝对位移量就会有问题。在SVR4中运行此程序的确会发生问题:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "ourhdr.h"
int
main(void)
{
int i, fd;
if ( (fd = open("temp.lock", O_RDWR | O_CREAT | O_TRUNC,
FILE_MODE)) < 0)
err_sys("open error");
for (i = 0; i < 1000000; i++) { /* try to write 2 Mbytes */
/* lock from current EOF to EOF */
if (writew_lock(fd, 0, SEEK_END, 0) < 0)
err_sys("writew_lock error");
if (write(fd, &fd, 1) != 1)
err_sys("write error");
if (un_lock(fd, 0, SEEK_END, 0) < 0)
err_sys("un_lock error");
if (write(fd, &fd, 1) != 1)
err_sys("write error");
}
exit(0);
赋与l_start)改换成所写字节数的负值(在本程序中是-1)。这就使得un_lock去
除上次加的锁。
建议性锁和强制性锁
考虑数据库存取例程序。如果该库中所有函数都以一致的方法处理记录锁,则我们
称使用这些函数存取数据库的任何进程集为合作进程。如果这些函数是唯一的用来
存取数据库的函数,那么它们使用建议性锁是可行的。但是建议性锁并不能阻止对
数据库文件有写许可权的任何其它进程写数据库文件。不使用协同一致的方法(数
据库存取例程库)来存取数据库的进程是一个非合作进程。
强制性锁机制中,系统核对每一个open、read和write都要检查调用进程对正在存
取的文件是否违背了某一把锁的作用。
对一个特定文件打开其设置_组_ID位,关闭其组_执行位则对该文件启动了强制性
锁机制。(回忆程序4.4)。因为当组_ 执行位关闭时,设置_组_ID位不再有意义
,所以SVR3的设计者借用两者的这种组合来指定对一个文件的锁是强制性的而非建
议性的。
如果一个进程试图读、写一个强制性锁起作用的文件,而欲读、写的部分又由其它
进程加上了读、写锁,此时会发生什么呢?对这一问题的回答取决于三方面的因素
:操作类型(read或write),其它进程保有的锁的类型(读锁或写锁),以及有
关描述符是阻塞还是非阻塞的。图12.7显示了这八种可能性。
图12.7 强制性锁对其它进程读、写的影响
除了图12.7中的read,write函数,其它进程的强制性锁也会对open函数产生影响。
通常,即使正在打开的文件具有强制性记录锁,该打开操作也会成功。下面的rea
d或write依从于图12.7中所示的规则。但是,如果欲打开的文件具有强制性锁(读
锁或写锁),而且open调用中的flag为O_TRUNC或O_CREAT,则不论是否指定O_NON
BLOCK,open都立即出错返回,erron设置为EAGAIN。(对O_TRUNC情况出错返回是有
意义的,因为其它进程对该文件持有读、写锁,所以不能将其截短为0。对O_CREA
T情况在返回时也设置erron则无意义,因为该标志的意义是如果该文件不存在则创
建,由于其它进程对该文件持有记录锁,因而该文件肯定是存在的。)
这种处理方式可能导致令人惊异的结果。我们曾编写过一个程序,它打开一个文件
(其mode指定为强制性锁),然后对该文件的整体设置一把读锁,然后进入睡眠一
段时间。在这段睡眠时间内,用某些常规的Unix程序和操作符对该文件进行处理,
发现下列情况:
l 可用ed编辑程序对该文件进行编辑操作,而且编辑结果写回磁盘!强制性记录锁
对此毫无影响。对ed操作进行跟踪分析发现,ed将新内容写到一个临时文件中,然
后删除原文件,最后将临时文件名改名为原文件名。于是,发现强制性锁机制对u
nlink函数没有影响。
在SVR4中,用truss(1)命令可以得到一个进程的系统调用跟踪信息,在4.3+BSD中
,则使用ktrace(1)和kdump(1)命令。
l 不能用vi编辑程序编辑该文件。vi可以读该文件,但是如果试图将新的数据写到
该文件中,则出错返回(EAGAIN)。如果试图将新数据添加到该文件中,则write
阻塞。vi的这种行为与所希望的一样。
l 使用KornShell的>和》算符重写或添写到该文件中,产生出错信息"cannot cre
at"。
l 在Bourne Shell下使用>算符出错,但是使用》算符则阻塞,在删除了强制性锁
后再继续进行处理。(执行添加操作所产生的区别是因为:Korn Shell以O_CREAT
和O_APPEND标志打开文件,而上面已提及指定O_CREAT会产生出错返回。但是,Bo
urne Shell在该文件已存在时并不指定O_CREAT,所以open成功,而下一个write则
阻塞。)
从这样一个例子中可见,在使用强制性锁时还需有所警惕。
一个别有用心的用户可以对大家都可读的文件加一把读锁(强制性),这样就能阻
止任何其它人写该文件(当然,该文件应当是强制性锁机制起作用的,这可能要求
该用户能够更改该文件的许可权位。)考虑一个数据库文件,它是大家都可读的,
并且是强制性锁机制起作用的。如果一个别有用心的用户对该整个文件保有一把读
锁,则其它进程不能再写该文件。
实例
程序12.7 检查一个系统是否支持强制性锁机制。
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <errno.h>
#include <fcntl.h>
#include "ourhdr.h"
int
main(void)
{
int fd;
pid_t pid;
char buff[5];
struct stat statbuf;
if ( (fd = open("templock", 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 ", errno);
/* now try to read the mandatory locked file */
if (lseek(fd, 0, SEEK_SET) == -1)
err_sys("lseek error");
if (read(fd, buff, 2) < 0)
err_ret("read failed (mandatory locking works)");
else
printf("read OK (no mandatory locking), buff = %2.2s ", buff);
}
exit(0);
}
程序12.7 检查是否支持强制性锁
此程序首先创建一个文件,并使强制性锁机制对其起作用。然后fork一个子进程。
父进程对整个文件设置一把写锁,子进程则将该文件的描述符设置为非阻塞的,然
后企图对该文件设置一把读锁,我们期望这会出错返回,并希望看到系统返回值是
EACCES或EAGAIN。接着,子进程将文件读、写位置调整到文件起点,并试图读该文
件。如果系统提供强制性锁机制,则read应返回EACCES或EAGAIN(因为该描述符是
非阻塞的)。否则read返回所读的数据。在SVR4中运行此程序(该系统支持强制性
锁机制),得到:
$ a.out
read_lock of already-locked region returns 13
read failed (mandatory locking works):No more processes
查看系统头文件或intro(2)手册页,可以看到错误13对应于EACCES。从例子中还可
以看到,在read出错返回信息部分中包含有"No more processes"。这通常来自于
fork,表示已用完了进程表项。
$ a.out
read_lock of already_locked region returns 35
read OK (no mandatory locking),buff=ab
其中,errno35对应于EAGAIN。该系统不支持强制性锁。
实例
让我们回到本节的第一个问题:当两个人同时编辑同一个文件将会怎样呢?一般的
UNIX文本编辑器并不使用记录锁,所以对此问题的回答仍然是:该文件的最后结果
取决于写该文件的最后一个进程。(4.3+BSD vi编辑器确实有一个编译时选择项使
运行时建议性记录锁起作用,但是这一选择项并不是缺省可用的。)即使我们在一
个编辑器,例如vi中使用了建议性锁,可是这把锁并不能阻止其他用户使用另一个
编辑器,该编辑器没有使用建议性记录锁。
若系统提供强制性记录锁,那么我们可以修改常用的编辑器(如果我们有该编辑器
的源代码。)如若我们没有该编辑器的源代码,那么我们可以试一试下述方法。编
写一个vi的前端程序。该程序立即调用fork,然后父进程等待子进程终止,子进程
打开在命令行中指定的文件,使强制性锁起作用,对整个文件设置一把写锁,然后
exec vi。在vi运行时,该文件是加了写锁的,所以其他用户不能修改它。当vi结
束时,父进程从wait返回,此时我们自编的前端程序也就结束。在本例中假定锁能
跨越exec,这正是我们前面所说的SVR4的情况(SVR4是我们说过的提供强制性锁的
唯一系统)。
这种类型的前端程序是可以编写的,但却往往不能起作用。问题出在大多数编辑器
(至少是vi和ed)读它们的输入文件,然后关闭它。只要引用被编辑文件的描述符
关闭了,那么加在该文件上的锁就被释放了。这意味着,在编辑器读了该文件的内
容,然后关闭了它,那么锁也就不存在了。在前端程序中没有任何方法可以阻止这
一点。
在第十六章的数据库函数库中,我们使用了记录锁以阻止多个进程的并发存取。在
本章,我们提供了时间测量以观察记录锁对进程的影响。
12.4流(Streams)
流是系统V提供的构造内核设备驱动程序和网络协议包等的一种通用方法,我们对
流进行讨论的目的是理解下列各点:
(a) 系统V的终端界面。
(b) I/O多路复用中轮询函数的使用(12.5.2节)。
(c) 基于流的管道和命名流管道的实现(15.2和12.5.2节)。
流机制是由Dennis Ritchie 发展起来的[Ritchie 1984],其目的是用通用、灵活的
方法改写传统的字符I/O系统并适应网络协议的需要,后来流机制被加入SVR3。SV
R4则提供了对流(基于流的终端I/O系统)的全面支持。[AT&T 1990d]对SVR4实现
进行了说明。
请注意不要将本章说明的流与标准I/O库(5.2节)中使用的流相混淆。
一个流在用户进程和设备驱动程序之间提供了一条全双工通路。流无需和实际硬件
设备直接对话-流也可以用作为伪设备驱动程序。图12.8示出了一个简单流的基本
结构。
图12.8 一个简单流
在流首之下可以压入处理模块。这可以用ioctl实现。图12.9示出了一个包含一个
处理模块的流。各方框之间用两根带箭头的线连接,以强调流的全双工特征。
任意个数的处理模块可以压入流。我们使用术语"压入",这是因为每一新模块总是
插到流首之下,而将以前压入的模块下压。(这类似于后进、先出的栈。)在图1
2.9中,我们标出了流的两侧,分别称为顺流(downstream)和逆流(upstream)
。写到流首的数据将顺流而下传送。由设备驱动程序读到的数据则逆流向上传送。
图12.9 具有处理模块的流
流模块是作为核心部分执行的,这类似于设备驱动程序,当构造核心时,流模块连
编进入核心。大多数系统不允许将末连编进入核心的流模块压入流。
在图11.2中示出了基于流的终端系统的一般结构。图中标出的"读、写"函数是流首
。标注为"终端行规程"的框是一个流处理模块。该处理模块的实际名称是ldterm。
(各种流模块的手册页在[AT&T 1990d]的第7节和[AT&T 1991]的第7节中。
用第三章中说明的函数存取流,它们是:open、close、read、write和ioctl。另
外,在SVR3核中增加了3个支持流的新函数(getmsg、putmsg、和poll),在SVR4
中又加了两个处理流不同优先级波段消息的函数(getpmsg和putpmsg)。本节将说
明这些新函数,我们为流打开的路径名通常在/dev目录之下。用ls -l查看设备名
,就能判断该设备是否为流设备。所有流设备都是字符特殊文件。
虽然某些有关流的文献暗示我们可以编写处理模块,并将它们压入流中,但是编写
这些模块如同编写设备驱动程序一样,需要专门的技术。
在流机制之前,终端是用现存的clist机制处理的。(Bach [1986]的10.3.1节和 L
effler et al[1989]的9.6节)分别说明SVR2和4.3BSD中的clist 机制。将基于字符
的设备添加到核心中通常涉及编写设备驱动程序,将所有有关部分都安排在驱动程
序中。对新设备的存取典型地通过原始设备进行,这意味着每个用户的read,writ
e都直通进入设备驱动程序。流机制使这种交互作用方式更加灵活,条理清晰,使
得数据可以用流消息方式在流首和驱动程序之间传送,并使任意数的中间处理模块
可对数据进行操作。
流消息
流的所有输入和输出都基于消息。流首和用户使用read、write、getmsg、getpms
g、putmsg和putpmsg交换消息。在流首、各处理模块和设备驱动程序之间,消息可
以顺流而下,也可以逆流而上。
在用户进程和流首之间,消息由下列几部分组成:消息类型、可选择的控制信息,
以及可选择的数据。在图12.10中示出了对应于write、putmsg和putpmsg的不同参
数,所产生的不同消息类型。控制信息和数据存放在strbuf结构中:
struct strbuf {
int maxlen; 缓存大小
int Len; 当前在缓存中的字节数
char *buf 缓存指针
};
图12.10 为write、putmsg和putpmsg产生的流消息的类型
当用putmsg或putpmsg发送消息时,len指定缓存中数据的字节数。当用getmsg或g
etpmsg接收消息时,maxlen 指定缓存长度(使核心不会溢出缓存),而len则由核
心设置,说明存放在缓存中的数据量。0长消息是允许的,len为-1说明没有控制信
息或数据。
为什么我们需要传送控制信息和数据两者呢?提供这两者使我们可以实现用户进程
和流之间的界面。Olander,McGrath和Israel[1986]说明了系统V服务界面的原先实
现。AT&T[1990d]第五章详细说明了服务界面,还使用了一个简单的实例。可能最为
人了解的服务界面是系统V的传输层界面(TLI),它提供了网络系统界面,Stevens
[1990]第七章对此进行了说明。
控制信息的另一个例子是发送一个无连接的网络消息(数据报)。为了发送该消息
,我们需要说明消息的内容(数据)和该消息的目的地址(控制信息)。如果我们
不能将数据和控制一起发送,那么就要某种专门设计的方案。例如,我们可以用i
octl说明地址,然后用write发送数据。另一种技术可能要求:地址占用数据的前
N个字节,用write写数据。将控制信息与数据分开,并且提供处理两者的函数(p
utmsg和getmsg)是处理这种问题的较清晰的方法。
有约25种不同类型的消息,但是只有少数几种用于用户进程和流首之间。其余的则
只在核心中顺流、逆流传送。(对于编写流处理模块的人员而言,这些消息是非常
有用的,但是对编写用户级代码的人员而言,则可忽略它们。)在我们所使用的函
数(read、write、getmsg、getpmsg、putmsg和putpmsg)中,只涉及三种消息类
型,它们是:
l M_DATA(I/O的用户数据);
l M_PROTO(协议控制信息);
l M_PCPROTO(高优先级协议控制信息)。
流中的消息都有一个排队优先级;
l 高优先级消息(最高优先级);
l 优先波段消息;
l 普通消息(最低优先级)。
普通消息是优先波段为0的消息。优先波段消息的波段可在1-255之间,波段愈高,
优先级也愈高。
每个流模块有两个输入队列。一个接收来自它上面模块的消息(这种消息从流首向
驱动程序顺流传送)。另一个接收来自它下面模块的消息(这种消息从驱动程序向
流首逆流传送)。在输入队列中的消息按优先级从高到低