第12章 高级I/O


下载 第1 2章 高 级 I / O 12.1 引言 本章内容包括:非阻塞 I / O、记录锁、系统 V流机制、I / O多路转接( s e l e c t和p o l l函数)、 r e a d v和w r i t e v函数,以及存储映照I / O(m m a p)。第1 4章、第1 5章中的进程间通信,以及以后 各章中的很多实例都要使用本章所述的概念和函数。 12.2 非阻塞I / O 1 0 . 5节中曾将系统调用分成两类:低速系统调用和其他。低速系统调用是可能会使进程永 远阻塞的一类系统调用: • 如果数据并不存在,则读文件可能会使调用者永远阻塞(例如读管道,终端设备和网络设备)。 • 如果数据不能立即被接受,则写这些同样的文件也会使调用者永远阻塞。 • 在某些条件发生之前,打开文件会被阻塞(例如打开一个终端设备可能需等到与之连接 的调制解调器应答;又例如若以只写方式打开 F I F O,那么在没有其他进程已用读方式打开该 F I F O时也要等待)。 • 对已经加上强制性记录锁的文件进行读、写。 • 某些i o c t l操作。 • 某些进程间通信函数(见第1 4章)。 虽然读、写磁盘文件会使调用在短暂时间内阻塞,但并不能将它们视为“低速”。 非阻塞I / O使我们可以调用不会永远阻塞的 I / O操作,例如o p e n , r e a d和w r i t e。如果这种操作 不能完成,则立即出错返回,表示该操作如继续执行将继续阻塞下去。 对于一个给定的描述符有两种方法对其指定非阻塞 I / O: (1) 如果是调用o p e n以获得该描述符,则可指定O _ N O N B L O C K标志(见3 . 3节)。 (2) 对于已经打开的一个描述符,则可调用 f c n t l打开O _ N O N B L O C K文件状态标志(见3 . 1 3 节)。程序3 - 5中的函数可用来为一个描述符打开任一文件状态标志。 早期的系统V版本使用标志O _ N D E L AY指定非阻塞方式。在这些版本中,如 果无数据可读,则 r e a d返回值0。而U N I X又常将r e a d的返回值0解释为文件结束, 两者有所混淆。 P O S I X . 1则提供了一个非阻塞标志,它的名字和语义都与 O _ N D E L AY不同。P O S I X . 1要求,对于一个非阻塞的描述符如果无数据可读,则 r e a d返回-1,并且e r r n o被设置为 E A G A I N。S V R 4支持较老的 O _ N D E L AY和 P O S I X . 1的O _ N O N B L O C K,但在本书的实例中只使用 P O S I X . 1规定的特征。 O _ N D E L AY的使用只是为了向后兼容,不应在新应用程序中使用。 4 . 3 B S D为f c n t l提供F N D E L AY标志,其语义也稍有区别。它不只影响该描述 符的文件状态标志,还将终端设备或套接口的标志更改成非阻塞的,因此影响了 终端或套接口的所有用户,不只是影响共享同一文件表项的用户( 4 . 3 B S D非阻塞 I / O只对终端和套接口起作用)。如果对一个非阻塞描述符的操作不能无阻塞地完 成,那么4 . 3 B S D返回E W O U L D B L O C K。4 . 3 + B S D提供P O S I X . 1的O _ N O N B L O C K 标志,但其语义却类似于4 . 3 B S D的F N D E L AY。非阻塞I / O通常用来处理终端设备 或网络连接,而这些设备通常一次由一个进程使用。这就意味着 B S D语义的更改 通常不会影响我们。出错返回 E W O U L D B L O C K而不是P O S I X . 1的E A G A I N,这造 成了可移植性问题,必须处理这一问题。 4 . 3 + B S D也支持F I F O,非阻塞I / O也对 F I F O起作用。 实例 程序1 2 - 1是一个非阻塞I / O的实例,它从标准输入读 100 000字节,并试图将它们写到标准 输出上。该程序先将标准输出设置为非阻塞的,然后用 f o r循环进行输出,每次写的结果都在 标准出错上打印。函数c l r- f l类似于程序3 - 5中的s e t _ f 1,但与s e t _ f 1的功能相反,它清除1个或多 个标志位。 程序12-1 长的非阻塞写 若标准输出是普通文件,则可以期望w r i t e只执行一次。 $ 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 一次写 2 7 4 U N I X环境高级编程 下载 $ ls -l temp.file 检验输出文件长度 -rw-rw-r-- 1 stevens 100000 Nov 21 16:27 temp.file 但是,若标准输出是终端,则期望w r i t e有时会返回一个数字,有时则出错返回。下面是在一个 系统上运行程序1 2 - 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 这种错2 1 1次 ⋯ nwrite = 4096,errno = 0 nwrite = -1, errno = 11 这种错6 5 8次 ⋯ nwrite = 4096,errno = 0 nwrite = -1, errno = 11 这种错6 0 4次 ⋯ nwrite = 4096,errno = 0 nwrite = -1, errno = 11 这种错1 0 4 7次 ⋯ nwrite = -1, errno = 11 这种错1 0 4 6次 ⋯ nwrite = 4096, errno = 0 ⋯等等 在该系统上,errno 11是E A G A I N。系统上的终端驱动程序总是一次接收 4 0 9 6或8 1 9 2字节。在 另一个系统上,前三个 w r i t e返回2 0 0 5,1 8 2 2和1 8 11,然后是9 6次出错返回,接着是返回 1 8 4 6 等等。每个w r i t e能写多少字节依赖于系统。 此程序若在S V R 4中运行,则其结果完全不同于前面的情况。当输出到终端上时,输出整 个输入文件只需要一个w r i t e。显然,非阻塞方式并不构成区别。创建一个更大的输入文件,并 且系统为运行该程序增加了程序缓存。程序的这种运行方式(即输出一整个文件,只调用一次 w r i t e)一直继续到输入文件长度达到约 700 000字节。达到此长度后,每一个 w r i t e都返回出错 E A G A I N。(输入文件则决不会再输出到终端上— 该程序只是连续地产生出错消息流。) 发生这种情况是因为:在 S V R 4中终端驱动程序通过流I / O系统连接到程序(1 2 . 4节将详细 说明流)。流系统有它自己的缓存,它一次能从程序接收更多的数据。 S V R 4的行为也依赖于终 端类型— 硬连线终端、控制台设备或伪终端。 在此实例中,程序发出了数千个w r i t e调用,但是只有2 0个左右是真正输出数据的,其余的 则出错返回。这种形式的循环称为轮询,在多用户系统上它浪费了 C P U时间。1 2 . 5节将介绍非 阻塞描述符的I / O多路转接,这是一种进行这种操作的更加有效的方法。 第1 7章将会用到非阻塞I / O,我们将要输出到终端设备( P o s t S c r i p t打印机)而且不希望在 w r i t e上阻塞。 12.3 记录锁 当两个人同时编辑一个文件时,其后果将如何呢?在很多U N I X系统中,该文件的最后状态 取决于写该文件的最后一个进程。但是对于有些应用程序,例如数据库,有时进程需要确保它 正在单独写一个文件。为了向进程提供这种功能,较新的 U N I X系统提供了记录锁机制。(第1 6 第 1 2章 高 级 I/O 2 7 5下载 章包含了使用记录锁的数据库子程序库。) 记录锁(record locking)的功能是:一个进程正在读或修改文件的某个部分时,可以阻止 其他进程修改同一文件区。对于 U N I X,“记录”这个定语也是误用,因为 U N I X内核根本没有 使用文件记录这种概念。一个更适合的术语可能是“区域锁”,因为它锁定的只是文件的一个 区域(也可能是整个文件)。 12.3.1 历史 表1 2 - 1列出了各种U N I X系统提供的不同形式的记录锁。 表12-1 各种U N I X系统支持的记录锁形式 本节的最后将说明建议性锁和强制性锁之间的区别。 P O S I X . 1选择了以f c n t l函数为基础的 系统V风格的记录锁。这种风格也得到4.3BSD Reno版本的支持。 早期的伯克利版只支持BSD flock函数。此函数只锁整个文件,而不锁文件中的一个区域。 但是P O S I X . 1的f c n t l函数可以锁文件中的任一区域,大至整个文件,小至单个字节。 本书只说明P O S I X . 1的f c n t l锁。系统V的l o c k f函数只是f c n t l函数的一个界面。 记录锁是1 9 8 0年由John Bass最早加到V 7上的。内核中相应的系统调用入口表 项是名为l o c k i n g的函数。此函数提供了强制性记录锁功能,它被用在很多制造商的 系统III版本中。Xenix系统采用了此函数,SVR4在Xenix兼容库中仍旧支持该函数。 S V R 2是系统V中第一个支持f c n t l风格记录锁的版本(1 9 8 4年)。 12.3.2 fcntl记录锁 3 . 1 3节中已经给出了f c n t l函数的原型,为了叙说方便,这里再重复一次。 #include #include #include int fcnt1(int f i l e d e s,int c m d,.../* struct flock *f l o c k p t r * / ) ; 返回:若成功则依赖于c m d(见下),若出错则为 - 1 对于记录锁,c m d是F _ G E T L K、F _ S E T L K或F _ S E T L K W。第三个参数(称其为 f l o c k p t r) 是一个指向f l o c k结构的指针。 2 7 6 U N I X环境高级编程 下载 系 统 建议性 强制性 f c n t 1 l o c k f f l o c k P O S I X . 1 • • X P G 3 • • S V R 2 • • • SVR3, SVR4 • • • • 4 . 3 B S D • • 4.3BSD Reno • • • f l o c k结构说明: • 所希望的锁类型:F _ R D L C K(共享读锁)、F _ W R L C K(独占性写锁)或F _ U N L C K(解 锁一个区域) • 要加锁或解锁的区域的起始地址,由l _ s t a r t和l _ w h e n c e两者决定。l _ s t a t是相对位移量(字 节),l _ w h e n c e则决定了相对位移量的起点。这与l s e e k函数(见3 . 6节)中最后两个参数类似。 • 区域的长度,由l _ l e n表示。 关于加锁和解锁区域的说明还要注意下列各点: • 该区域可以在当前文件尾端处开始或越过其尾端处开始,但是不能在文件起始位置之前 开始或越过该起始位置。 • 如若l _ l e n为0,则表示锁的区域从其起点(由 l _ s t a r t和l _ w h e n c e决定)开始直至最大可能 位置为止。也就是不管添写到该文件中多少数据,它都处于锁的范围。 • 为了锁整个文件,通常的方法是将 l _ s t a r t说明为0,l _ w h e n c e说明为S E E K _ S E T,l _ l e n说 明为0。 上面提到了两种类型的锁:共享读锁( l _ t y p e为L _ R D L C K)和独占写琐( L _ W R L C K)。 基本规则是:多个进程在一个给定的字节 上可以有一把共享的读锁,但是在一个给 定字节上的写锁则只能由一个进程独用。 更进一步而言,如果在一个给定字节上已 经有一把或多把读锁,则不能在该字节上 再加写锁;如果在一个字节上已经有一把 独占性的写锁,则不能再对它加任何读锁。 在表1 2 - 2中示出了这些规则。 加读锁时,该描述符必须是读打开; 加写锁时,该描述符必须是写打开。 以下说明f c n t l函数的三种命令: • F_GETLK 决定由f l o c k p t r所描述的锁是否被另外一把锁所排斥(阻塞)。如果存在一把 锁,它阻止创建由f l o c k p t r所描述的锁,则这把现存的锁的信息写到 f l o c k p t r指向的结构中。如 果不存在这种情况,则除了将 l _ t y p e设置为F _ U N L C K之外,f l o c k p t r所指向结构中的其他信息 保持不变。 • F_SETLK 设置由f l o c k p t r所描述的锁。如果试图建立一把按上述兼容性规则并不允许的 锁,则f c n t l立即出错返回,此时e r r n o设置为E A C C E S或E A G A I N。 S V R 2和S V R 4返回E A C C E S,但手册页警告将来返回E A G A I N。4 . 3 + B S D则返 回E A G A I N。P O S I X . 1允许这两种情况。 此命令也用来清除由f l o c k p t r说明的锁(l _ t y p e为F _ U N L C K)。 第 1 2章 高 级 I/O 2 7 7下载 无锁 读锁 写锁 要求 一把或多 把读锁 一把写锁 可以 可以 可以 拒绝 拒绝 拒绝 区域当前有 表12-2 不同类型锁之间的兼容性 • F_SETLKW 这是F _ S E T L K的阻塞版本(命令名中的 W表示等待(w a i t))。如果由于存 在其他锁,那么按兼容性规则由 f l o c k p t r所要求的锁不能被创建,则调用进程睡眠。如果捕捉 到信号则睡眠中断。 应当了解,用F _ G E T L K测试能否建立一把锁,然后用 F _ S E T L K和F _ S E T L K W企图建立一 把锁,这两者不是一个原子操作。在这两个操作之间可能会有另一个进程插入并建立一把相关 的锁,使原来测试到的情况发生变化,如果不希望在建立锁时可能产生的长期阻塞,则应使用 F _ S E T L K,并对返回结果进行测试,以判别是否成功地建立了所要求的锁。 在设置或释放文件上的一把锁时,系统按需组合或裂开相邻区。例如,若对字节 0 ~ 9 9设置 一把读锁,然后对字节0 ~ 4 9设置一把写锁,则有两个加锁区: 0 ~ 4 9字节(写锁)及5 0 ~ 9 9(读 锁)。又如,若1 0 0 ~ 1 9 9字节是加锁的区,需解锁第 1 5 0字节,则内核将维持两把锁,一把用于 1 0 0 ~ 1 4 9字节,另一把用于1 5 1 ~ 1 9 9字节。 实例— 要求和释放一把锁 为了避免每次分配f l o c k结构,然后又填入各项信息,可以用程序 1 2 - 2中的函数l o c k _ r e g来 处理这些细节。 程序12-2 加锁和解锁一个文件区域的函数 因为大多数锁调用是加锁或解锁一个文件区域(命令 F _ G E T L K很少使用),故通常使用下 列五个宏,它们都定义在o u r h d r. h中(见附录B)。 #define read_lock(fd,offset,whence,len) \ l o c k _ r e g ( f d , F _ S E T L K , F _ R D L C K , o f f s e t , w h e n c e , l e n ) #define readw_lock(fd,offset,whence,len) \ l o c k _ r e g ( f d , F _ S E T L K W , F _ R D L C K , o f f s e t , w h e n c e , l e n ) #define write_lock(fd,offset,whence,len) \ l o c k _ r e g ( f d , F _ S E T L K , F _ W R L C K , o f f s e t , w h e n c e , l e n ) #define writew_lock(fd,offset,whence,len) \ l o c k _ r e g ( f d , F _ S E T L K W , F _ W R L C K , o f f s e t , w h e n c e , l e n ) #define un_lock(fd,offset,whence,len) \ l o c k _ r e g ( f d , F _ S E T L K , F _ U N L C K , o f f s e t , w h e n c e , l e n ) 我们用与l s e e k函数同样的顺序定义这些宏中的三个参数。 实例— 测试一把锁 程序1 2 - 3定义了一个函数l o c k _ t e s t,可用其测试一把锁。 2 7 8 U N I X环境高级编程 下载 程序12-3 测试一个锁条件的函数 如果存在一把锁,它阻塞由参数说明的锁,则此函数返回持有这把现存锁的进程的 I D,否 则此函数返回0。通常用下面两个宏来调用此函数(它们也定义在 o u r h d r. h)。 #define is_readlock(fd,offset,whence,len) \ l o c k _ t e s t ( f d , F _ R D L C K , o f f s e t , w h e n c e , l e n ) #define is_writelock(fd,offset,whence,len) \ l o c k _ t e s t ( f d , F _ W R L C K , o f f s e t , w h e n c e , l e n ) 实例— 死锁 如果两个进程相互等待对方持有并且不释放(锁定)的资源时,则这两个进程就处于死锁 状态。如果一个进程已经控制了文件中的一个加锁区域,然后它又试图对另一个进程控制的区 域加锁,则它就会睡眠,在这种情况下,有发生死锁的可能性。 程序1 2 - 4给出了一个死锁的例子。子进程锁字节 0,父进程锁字节1。然后,它们中的每一 个又试图锁对方已经加锁的字节。在该程序中使用了 8 . 8节中介绍的父 -子进程同步例程 (T E L L _ x x x和WA I T _ x x x),使得对方都能建立第一把锁。运行程序1 2 - 4得到: 程序12-4 死锁检测实例 第 1 2章 高 级 I/O 2 7 9下载 $ a . o u t 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 检测到死锁时,内核必须选择一个进程收到出错返回。在本实例中,选择了子进程,这是 一个实现细节。当此程序在另一个系统上运行时,一半次数是子进程接到出错信息,另一半则 是父进程。 12.3.3 锁的隐含继承和释放 关于记录锁的自动继承和释放有三条规则: (1) 锁与进程、文件两方面有关。这有两重含意:第一重很明显,当一个进程终止时,它 所建立的锁全部释放;第二重意思就不很明显,任何时候关闭一个描述符时,则该进程通过这 一描述符可以存访的文件上的任何一把锁都被释放(这些锁都是该进程设置的)。这就意味着 如果执行下列四步: fd1=open(pathname, ...); read_lock(fd1, ...); f d 2 = d u p ( f d 1 ) ; c l o s e ( f d 2 ) ; 则在c l o s e(f d 2)后,在f d 1上设置的锁被释放。如果将d u p代换为o p e n,其效果也一样: fd1=open(pathname, ...); read_lock(fd1, ...); fd2=open(pathname, ...); 2 8 0 U N I X环境高级编程 下载 c l o s e ( f d 2 ) ; (2) 由f o r k产生的子程序不继承父进程所设置的锁。这意味着,若一个进程得到一把锁,然 后调用f o r k,那么对于父进程获得的锁而言,子进程被视为另一个进程,对于从父进程处继承 过来的任一描述符,子进程要调用 f c n t l以获得它自己的锁。这与锁的作用是相一致的。锁的作 用是阻止多个进程同时写同一个文件(或同一文件区域)。如果子进程继承父进程的锁,则父、 子进程就可以同时写同一个文件。 (3) 在执行e x e c后,新程序可以继承原执行程序的锁。 P O S I X . 1没有要求这一点。但是,S V R 4和4 . 3 + B S D都支持这一点。 12.3.4 4.3+BSD的实现 先简要地观察4 . 3 + B S D实现中使用的数据结构,从中可以看到锁是与进程、文件相关联的。 考虑一个进程,它执行下列语句(忽略出错返回): 图1 2 - 1显示了父、子进程暂停(执行pause( ))后的数据结构情况。 图12-1 关于记录锁的4 . 3 + B S D数据结构 第 1 2章 高 级 I/O 2 8 1下载 父进程表项 子进程表项 文件表 文件状态标志 当前文件位移 v节点指针 文件状态标志 当前文件位移 v节点指针 f d标志 f d标志 v节点表 v节点信息 i节点信息 当前文件长度 锁链接表 链接 标志等 起始位移量 长度 进程I D 链接 标志等 起始位移量 长度 进程I D 图3 - 3和图8 - 1中已显示了o p e n、f o r k以及d u p后的数据结构。有了记录锁后,在原来的这些 图上新加了f l o c k结构,它们由i节点结构开始相互连接起来。注意,每个 f l o c k结构说明了一个 给定进程的一个加锁区域。图中显示了两个 f l o c k结构,一个是由父进程调用w r i t e _ l o c k形成的, 另一个则是由子进程调用r e a d _ l o c k形成的。每一个结构都包含了相应进程I D。 在父进程中,关闭f d 1、f d 2和f d 3中的任意一个都将释放由父进程设置的写锁。在关闭这三 个描述符中的任意一个时,内核会从该描述符所关连的 i节点开始,逐个检查f l o c k连接表中各 项,并释放由调用进程持有的各把锁。内核并不清楚也不关心父进程是用哪一个描述符来设置 这把锁的。 实例 建议性锁可由精灵进程使用以保证该精灵进程只有一个副本在运行。起动时,很多精灵进 程都把它们的进程I D写到一个各自专用的P I D文件上。系统停机时,可以从这些文件中取用这 些精灵进程的进程I D。防止一个精灵进程有多份副本同时运行的方法是:在精灵进程开始运行 时,在它的进程I D文件上设置一把写锁。如果在它运行时一直保持这把锁,则不可能再起动它 的其他副本。程序1 2 - 5实现了这一技术。 因为进程I D文件可能包含以前的精灵进程 I D,而且其长度可能长于当前进程的 I D,例如 该文件中以前的内容可能是 1 2 3 4 5 \ n,而现在的进程 I D是6 5 4,我们希望该文件现在只包含 6 5 4 \ n,而不是6 5 4 \ n 5 \ n,所以在写该文件时,先将其截短为 0。注意,要在设置了锁之后再调 用截短文件长度的函数 f t r u n c a t e。在调用o p e n时不能指定O _ T R U N C,因为这样做会在有一个 这种精灵进程运行并对该文件加锁时也会使该文件截短为 0。(如果使用强制性锁而不是建议性 锁,则可使用O _ T R U N C。本节最后将讨论强制性锁。) 在本实例中,也对该描述符设置了运行时关闭标志。这是因为精灵进程常常 f o r k并e x e c其 他进程,并无需在另一个进程中使该文件也处在打开状态。 程序12-5 精灵进程阻止其多份副本同时运行的起动代码 2 8 2 U N I X环境高级编程 下载 实例 在相对文件尾端加锁或解锁时需要特别小心。大多数实现按照 l _ w h e n c e的S E E K _ C U R或 S E E K _ E N D值,用文件当前位置或当前长度以及 l _ s t a r t得到绝对的文件位移量。但是,通常需 要相对于文件的当前位置或当前长度指定一把锁。 程序1 2 - 6写一个大文件,一次一个字节。在每次循环中 ,从文件当前尾端开始处加锁直到将 来可能扩充到的尾端为止(最后一个参数,长度指定为 0),并写1个字节。然后解除这把锁, 写另一个字节。如果系统用“从当前尾端开始,直到将来可能扩充的尾端”这种记法来跟踪锁, 那么这段程序能够正常工作。但是如果系统将相对位移量变换成绝对位移量就会有问题。在 S V R 4中运行此程序的确会发生问题: 程序12-6 显示相对于文件末尾的锁的问题的程序 第 1 2章 高 级 I/O 2 8 3下载 $ a . o u t writew_lock error: No record locks available $ ls-l temp.lock -rw-r--r-- 1 stevens other 592 Nov 1 04:41 temp.lock (内核返回E N O L C K。它表示内核中的锁表已用完。)分析系统是如何做的会从中得到教益。 图1 2 - 2显示了第一次调用w r i t e w _ l o c k和w r i t e之后的文件状态。 因为在w r i t e w _ l o c k调用中,指定“直至将来可能扩充到尾端”,所以图中锁定区域超过了 所写的第一个字节。 然后调用u n _ l o c k。从当前尾端处开始直至将来可能扩充到的尾端为止解锁,它将图 1 2 - 2中 箭头的右端缩回到第一字节位置端部。然后将第二个字节写到文件中。图 1 2 - 3显示了调用 u n _ l o c k以及写了第二个字节后的文件状态。 在经过第二次f o r循环后,在文件上共写了4个字节。图1 2 - 4显示了此时文件及锁的状态。 这种情况不断重复,直至内核为该进程用完了锁结构。此时,f c n t l出错返回,e r r n o设置为E N O L C K。 在此程序中,每次写的字节数是已知的,所以可将un_lock的第二个参数(其值将赋与l_start) 改换成所写字节数的负值(在本程序中是-1)。这就使得un_lock去除上次加的锁。 实际上,在开发_ d b _ w r i t e d a t和_ d b _ w r i t e i d x函数时,作者的系统出现了问题。 1 6 . 7节给出了关于此问题的一个稍稍不同的方法。 12.3.5 建议性锁和强制性锁 考虑数据库存取例程序。如果该库中所有函数都以一致的方法处理记录锁,则称使用这些 函数存取数据库的任何进程集为合作进程( cooperating pro c e s s)。如果这些函数是唯一的用来 存取数据库的函数,那么它们使用建议性锁是可行的。但是建议性锁并不能阻止对数据库文件 有写许可权的任何其他进程写数据库文件。不使用协同一致的方法(数据库存取例程库)来存 取数据库的进程是一个非合作进程。 强制性锁机制中,内核对每一个 o p e n、r e a d和w r i t e都要检查调用进程对正在存取的文件是 否违背了某一把锁的作用。 表1 2 - 1显示了S V R 4提供强制性记录锁,而P O S I X . 1不提供。 对一个特定文件打开其设置 -组- I D位,关闭其组-执行位则对该文件启动了强制性锁机制。 2 8 4 U N I X环境高级编程 下载 图12-2 第一次调用w r i t e w _ l o c k 和w r i t e之后的文件状态 图12-3 调用u n _ l o c k和 写第二字节后的状态 图12-4 第二次f o r循环后 文件及锁的状态 加锁 加锁 加锁 加锁 第一个 字节 第一个 字节 第二个 字节 第一个 字节 第二个 字节 第三个 字节 第四个 字节 (回忆程序4 - 4)。因为当组-执行位关闭时,设置-组- I D位不再有意义,所以S V R 3的设计者借用 两者的这种组合来指定对一个文件的锁是强制性的而非建议性的。 如果一个进程试图读、写一个强制性锁起作用的文件,而欲读、写的部分又由其他进程加 上了读、写锁,此时会发生什么呢?对这一问题的回答取决于三方面的因素:操作类型( r e a d 或w r i t e),其他进程保有的锁的类型(读锁或写锁),以及有关描述符是阻塞还是非阻塞的。表 1 2 - 3列出了这8种可能性。 表12-3 强制性锁对其他进程读、写的影响 除了表1 2 - 3中的r e a d , w r i t e函数,其他进程的强制性锁也会对o p e n函数产生影响。通常,即 使正在打开的文件具有强制性记录锁,该打开操作也会成功。下面的 r e a d或w r i t e依从于表1 2 - 3 中所示的规则。但是,如果欲打开的文件具有强制性锁(读锁或写锁),而且o p e n调用中的f l a g 为O _ T R U N C或O _ C R E AT,则不论是否指定 O _ N O N B L O C K , o p e n都立即出错返回,e r r n o设置 为E A G A I N。(对O _ T R U N C情况出错返回是有意义的,因为其他进程对该文件持有读、写锁, 所以不能将其截短为0。对O _ C R E AT情况在返回时也设置e r r n o则无意义,因为该标志的意义是 如果该文件不存在则创建,由于其他进程对该文件持有记录锁,因而该文件肯定是存在的。) 这种处理方式可能导致令人惊异的结果。我们曾编写过一个程序,它打开一个文件(其模 式指定为强制性锁),然后对该文件的整体设置一把读锁,然后进入睡眠一段时间。在这段睡 眠时间内,用某些常规的U N I X程序和操作符对该文件进行处理,发现下列情况: • 可用e d编辑程序对该文件进行编辑操作,而且编辑结果可以写回磁盘!强制性记录锁对 此毫无影响。对e d操作进行跟踪分析发现,e d将新内容写到一个临时文件中,然后删除原文件, 最后将临时文件名改名为原文件名。于是,发现强制性锁机制对 u n l i n k函数没有影响。 在S V R 4中,用 t r u s s ( 1 )命令可以得到一个进程的系统调用跟踪信息,在 4 . 3 + B S D中,则使用k t r a c e ( 1 )和k d u m p ( 1 )命令。 • 不能用v i编辑程序编辑该文件。 v i可以读该文件,但是如果试图将新的数据写到该文件 中,则出错返回( E A G A I N)。如果试图将新数据添加到该文件中,则 w r i t e阻塞。v i的这种行 为与所希望的一样。 • 使用K o r n S h e l l的>和> >算符重写或添写到该文件中,产生出错信息“ cannot creat”。 • 在Bourne shell下使用>算符出错,但是使用> >算符则阻塞,在删除了强制性锁后再继续 进行处理。(执行添加操作所产生的区别是因为: Korn Shell以O _ C R E AT和O _ A P P E N D标志打 开文件,而上面已提及指定 O _ C R E AT会产生出错返回。但是,Bourne shell在该文件已存在时 并不指定O _ C R E AT,所以o p e n成功,而下一个w r i t e则阻塞。) 从这样一个例子中可见,在使用强制性锁时还需有所警惕。 一个别有用心的用户可以对大家都可读的文件加一把读锁(强制性),这样就能阻止任何 其他人写该文件(当然,该文件应当是强制性锁机制起作用的,这可能要求该用户能够更改该 文件的许可权位)。考虑一个数据库文件,它是大家都可读的,并且是强制性锁机制起作用的。 第 1 2章 高 级 I/O 2 8 5下载 在区域上的读锁 在区域上的写锁 阻塞描述符,试图 非阻塞描述符,试图 读 写 读 写 可以 阻塞 阻塞 阻塞 可以 E A G A I N EAGAIN EAGAIN 如果一个别有用心的用户对该整个文件保有一把读锁,则其他进程不能再写该文件。 实例 程序1 2 - 7用于确定一个系统是否支持强制性锁机制。 程序12-7 确定是否支持强制性锁 2 8 6 U N I X环境高级编程 下载 此程序首先创建一个文件,并使强制性锁机制对其起作用。然后 f o r k一个子进程。父进程 对整个文件设置一把写锁,子进程则将该文件的描述符设置为非阻塞的,然后企图对该文件设 置一把读锁,我们期望这会出错返回,并希望看到系统返回值是 E A C C E S或E A G A I N。接着, 子进程将文件读、写位置调整到文件起点,并试图读该文件。如果系统提供强制性锁机制,则 r e a d应返回E A C C E S或E A G A I N(因为该描述符是非阻塞的)。否则r e a d返回所读的数据。在 S V R 4中运行此程序(该系统支持强制性锁机制),得到: $ a . o u t read_lock of already-locked region returns 13 read failed (mandatory locking works):No more processes 查看系统头文件或i n t r o ( 2 )手册页,可以看到错误1 3对应于E A C C E S。从例子中还可以看到, 在r e a d出错返回信息部分中包含有“No more processes”。这通常来自于f o r k,表示已用完了进 程表项。 若采用4 . 3 + B S D系统,则得到: $ a . o u t read_lock of already_locked region returns 35 read OK (no mandatory locking),buff=ab 其中,e r r n o 3 5对应于E A G A I N。该系统不支持强制性锁。 实例 让我们回到本节的第一个问题:当两个人同时编辑同一个文件将会怎样呢?一般的 U N I X 文本编辑器并不使用记录锁,所以对此问题的回答仍然是:该文件的最后结果取决于写该文件 的最后一个进程。(4 . 3 + B S D的v i编辑器确实有一个编译选择项使运行时建议性记录锁起作用, 但是这一选择项并不是默认可用的。)即使我们在一个编辑器,例如 v i中使用了建议性锁,可 是这把锁并不能阻止其他用户使用另一个没有使用建议性记录锁的编辑器。 若系统提供强制性记录锁,那么可以修改常用的编辑器(如果有该编辑器的源代码)。如 没有该编辑器的源代码,那么可以试一试下述方法。编写一个 v i的前端程序。该程序立即调用 f o r k,然后父进程等待子进程终止,子进程打开在命令行中指定的文件,使强制性锁起作用, 对整个文件设置一把写锁,然后运行 v i。在v i运行时,该文件是加了写锁的,所以其他用户不 能修改它。当v i结束时,父进程从w a i t返回,此时自编的前端程序也就结束。本例中假定锁能 跨越e x e c,这正是前面所说的S V R 4的情况(S V R 4是提供强制性锁的唯一系统)。 这种类型的前端程序是可以编写的,但却往往不能起作用。问题出在大多数编辑器(至少 是v i和e d)读它们的输入文件,然后关闭它。只要引用被编辑文件的描述符关闭了,那么加在 该文件上的锁就被释放了。这意味着,在编辑器读了该文件的内容,然后关闭了它,那么锁也 就不存在了。前端程序中没有任何方法可以阻止这一点。 第1 6章的数据库函数库使用了记录锁以阻止多个进程的并发存取。本章则提供了时间测量 以观察记录锁对进程的影响。 第 1 2章 高 级 I/O 2 8 7下载 12.4 流 流是系统V提供的构造内核设备驱动程序和网络协议包的一种通用方法,对流进行讨论的 目的是理解下列各点: (1) 系统V的终端界面。 (2) I/O多路复用中轮询函数的使用(见1 2 . 5 . 2节)。 (3) 基于流管道和命名流管道的实现(见1 5 . 2和1 2 . 5 . 2节)。 流机制是由Dennis Ritchie 发展起来的〔Ritchie 1984〕,其目的是用通用、灵 活的方法改写传统的字符 I / O系统并适应网络协议的需要,后来流机制被加入 S V R 3。S V R 4则提供了对流(基于流的终端I / O系统)的全面支持。〔AT&T 1990d〕 对S V R 4实现进行了说明。 请注意不要将本章说明的流与标准I / O库(见5 . 2节)中使用的流相混淆。 流在用户进程和设备驱动程序之间提供了一条全双工通路。流无需和实际硬件设备直接对 话— 流也可以用作为伪设备驱动程序。图1 2 - 5示出了一个简单流的基本结构。 在流首之下可以压入处理模块。这可以用 i o c t l实现。图1 2 - 6示出了一个包含一个处理模块 的流。各方框之间用两根带箭头的线连接,以强调流的全双工特征。 任一个数的处理模块可以压入流。我们使用术语“压入”,是因为每一新模块总是插到流 首之下,而将以前压入的模块下压。(这类似于后进、先出的栈。)图1 2 - 6标出了流的两侧,分 别称为顺流(d o w n s t r e a m)和逆流(u p s t r e a m)。写到流首的数据将顺流而下传送。由设备驱 动程序读到的数据则逆流向上传送。 流模块是作为内核的一部分执行的,这类似于设备驱动程序,当构造内核时,流模块连编 进入内核。大多数系统不允许将末连编进入内核的流模块压入流。 图11 - 2中示出了基于流的终端系统的一般结构。图中标出的“读、写函数”是流首。标注 为“终端行规程”的框是一个流处理模块。该处理模块的实际名称是 l d t e r m。(各种流模块的 手册页在〔AT&T 1990d〕的第7节和〔AT&T 1991〕的第7节中。) 用第3章中说明的函数存取流,它们是: o p e n、c l o s e、r e a d、w r i t e和i o c t l。另外,在S V R 3 2 8 8 U N I X环境高级编程 下载 图12-5 一个简单流 用户进程 流首(系统调用界面) 图12-6 具有处理模块的流 用户进程 设备驱动程序(或伪 设备驱动程序) 顺流 逆流 流首 处理模块 设备驱动程序 内核 内核 内核中增加了3个支持流的新函数(g e t m s g、p u t m s g和p o l l),在S V R 4中又加了两个处理流不同 优先级波段消息的函数( g e t p m s g和p u t p m s g)。本节将说明这些新函数,为流打开的路径名通 常在/ d e v目录之下。用l s - l查看设备名,就能判断该设备是否为流设备。所有流设备都是字符特 殊文件。 虽然某些有关流的文献暗示我们可以编写处理模块,并将它们压入流中,但是编写这些模 块如同编写设备驱动程序一样,需要专门的技术。 在流机制之前,终端是用现存的c l i s t机制处理的。( B a c h〔1 9 8 6〕的1 0 . 3 . 1节和 L e ff l e r等〔1 9 8 9〕的9 . 6节)分别说明S V R 2和4 . 3 B S D中的clist 机制。将基于字符的 设备添加到内核中通常涉及编写设备驱动程序,将所有有关部分都安排在驱动程 序中。对新设备的存取典型地通过原始设备进行,这意味着每个用户的 r e a d , w r i t e 都直通进入设备驱动程序。流机制使这种交互作用方式更加灵活,条理清晰,使 得数据可以用流消息方式在流首和驱动程序之间传送,并使任意数的中间处理模 块可对数据进行操作。 12.4.1 流消息 流的所有输入和输出都基于消息。流首和用户进程使用 r e a d、w r i t e、g e t m s g、g e t p m s g、 p u t m s g和p u t p m s g交换消息。在流首、各处理模块和设备驱动程序之间,消息可以顺流而下, 也可以逆流而上。 在用户进程和流首之间,消息由下列几部分组成:消息类型、可选择的控制信息以及可选 择的数据。表1 2 - 4列出了对应于w r i t e、p u t m s g和p u t p m s g的不同参数,所产生的不同消息类型。 控制信息和数据存放在s t r b u f结构中: 表12-4 为w r i t e、p u t m s g和p u t p m s g产生的流消息的类型 第 1 2章 高 级 I/O 2 8 9下载 函 数 控制? 数据? 波 段 标 志 产生的消息类型 普通 普通 普通 高优先级 出错, E I N VA L 出错, E I N VA L 出错, E I N VA L 出错, E I N VA L 普通 (普通) 优先级波段 优先级波段 高优先级 是 否 是 是或否 是或否 是或否 不使用 不使用 不使用 不使用 不使用 不使用 否 否 是 是 否 是或否 否 否 否 是 是 是 否 是或否 是或否 否 是 是 是或否 是或否 是或否 是或否 是或否 0~2 5 5 0~2 5 5 0 1~2 5 5 0 1~2 5 5 0 0 非0 不使用 无消息发送,返回 0 无消息发送,返回0 当用p u t m s g或p u t p m s g发送消息时,l e n指定缓存中数据的字节数。当用g e t m s g或g e t p m s g接 收消息时,maxlen 指定缓存长度(使内核不会溢出缓存),而l e n则由内核设置,说明存放在缓 存中的数据量。0长消息是允许的,l e n为-1说明没有控制信息或数据。 为什么需要传送控制信息和数据两者呢?提供这两者使我们可以实现用户进程和流之间的 服务界面。O l a n d e r, McGrath和I s r a e l〔1 9 8 6〕说明了系统V服务界面的原先实现。AT & T [ 1 9 9 0 d ] 第5章详细说明了服务界面,还使用了一个简单的实例。可能最为人了解的服务界面是系统 V的 传输层界面( T L I ),它提供了网络系统界面,S t e v e n s〔1 9 9 0〕第7章对此进行了说明。 控制信息的另一个例子是发送一个无连接的网络消息(数据报)。为了发送该消息,需要 说明消息的内容(数据)和该消息的目的地址(控制信息)。如果不能将数据和控制一起发送, 那么就要某种专门设计的方案。例如,可以用 i o c t l说明地址,然后用w r i t e发送数据。另一种技 术可能要求:地址占用数据的前 N个字节,用w r i t e写数据。将控制信息与数据分开,并且提供 处理两者的函数(p u t m s g和g e t m s g)是处理这种问题的较清晰的方法。 有约2 5种不同类型的消息,但是只有少数几种用于用户进程和流首之间。其余的则只在内 核中顺流、逆流传送。(对于编写流处理模块的人员而言,这些消息是非常有用的,但是对编 写用户级代码的人员而言,则可忽略它们。)在我们所使用的函数( r e a d、w r i t e、g e t m s g、 g e t p m s g、p u t m s g和p u t p m s g)中,只涉及三种消息类型,它们是: • M_DATA(I / O的用户数据)。 • M_PROTO(协议控制信息)。 • M_PCPROTO(高优先级协议控制信息)。 流中的消息都有一个排队优先级: • 高优先级消息(最高优先级)。 • 优先波段消息。 • 普通消息(最低优先级)。 普通消息是优先波段为0的消息。优先波段消息的波段可在1 ~ 2 5 5之间,波段愈高,优先级也愈 高。 每个流模块有两个输入队列。一个接收来自它上面模块的消息(这种消息从流首向驱动程 序顺流传送)。另一个接收来自它下面模块的消息(这种消息从驱动程序向流首逆流传送)。在 输入队列中的消息按优先级从高到低排列。表 1 2 - 4列出了针对w r i t e、p u t m s g和p u t p m s g的不同 参数,产生不同优先级的消息。 有一些消息我们未加考虑。例如,若流首从它下面接收到 M _ S I G消息,则产生一信号。这 种方法用于终端行规程模块向相关前台进程组发送终端产生的信号 12.4.2 putmsg和p u t p m s g函数 p u t m s g和p u t p m s g函数用于将流消息(控制信息或数据,或两者)写至流中。这两个函数 的区别是后者允许对消息指定一个优先波段。 #include int putmsg(int f i l e d e s,const struct strbuf *c t l p t r, const struct strbuf *d a t a p t r,int f l a g) ; int putpmsg(int f i l e d e s,const struct strbuf *c t l p t r, const struct strbuf *d a t a p t r,int b a n d,int f l a g) ; 两个函数返回:若成功则为 0,若出错则为 - 1 2 9 0 U N I X环境高级编程 下载 对流也可以使用w r i t e函数,它等效于不带任何控制信息,f l a g为0的p u t m s g。 这两个函数可以产生三种不同优先级的消息:普通、优先波段和高优先级。表 1 2 - 4详细列 出了这两个函数中几个参数的各种可能组合,以及所产生的不同类型的消息。 在表1 2 - 4中,消息控制列中的“否”对应于空 c t l p t r参数,或c t l p t r- > l e n为-1。该列中的 “是”对应于c t l p t r非空,以及c t l p t r- > l e n大于等于0。这些说明同样适用于消息的数据部分(用 d a t a p t r代替c t l p t r)。 12.4.3 流ioctl 操作 3 . 1 4节曾提到过i o c t l函数,它能做其他I / O函数不能处理的事情。流系统中继续采用了该函数。 在S V R 4中,使用i o c t l可对流执行2 9种不同的操作。关于这些操作的说明请见 s t r e a m i o ( 7 )手 册页〔AT & T 1 9 9 0 d〕,头文件< s t r o p t s . h >应包括在使用这些操作的C代码中。i o c t l的第二个参数 re q u e s t说明执行2 9个操作中的哪一个。所有 re q u e s t都以I _开始。第三个参数则与 re q u e s t有关。 有时第三个参数是一个整型值,有时它是指向一个整型或一个数据结构的指针。 实例— i s a s t r e a m函数 有时需要确定一个描述符是否引用一个流。这与调用 i s a t t y函数来确定一个描述符是否引 用一个终端设备相类似(见11 . 9节)。S V R 4提供i s a s t r e a m函数。 int isastream(int f i l e d e s) ; 返回:若是流返回1,否则返回0 (由于某种原因,S V R 4的设计者忘记将此函数的原型放在头文件中,所以不能为此函数写 一条# i n c l u d e语句。) 与i s a t t y类似,它通常是用一个只对流设备才有效的 i o c t l函数来进行测试的。程序1 2 - 8是该 函数的一种可能的实现。它使用I_CANPUT ioctl来测试由第三个参数说明的优先波段是否可写。 如果该i o c t l执行成功,则它对所涉及的流并未作任何改变。 程序12-8 检查描述符是否引用流设备 程序1 2 - 9可用于测试此函数。 程序12-9 测试i s a s t r e a m函数 第 1 2章 高 级 I/O 2 9 1下载 运行此程序,得到很多由i o c t l函数返回的出错信息。 $ a.out /dev/tty /dev/vidadm /dev/null /etc/motd /dev/tty:/dev/tty:streams device /dev/vidadm:/dev/vidadm:not a stream:Invalid argument /dev/null:/dev/null:not a stream:No suck device /etc/motd: /etc/motd:not a stream:Not a typewriter / d e v / t t y在S V R之下是个流设备,这与我们所期望的一致。 / d e v / v i d a d m不是一个流设备,但 是它是支持其他i o c t l请求的字符特殊文件。对于不知道这种 i o c t l请求的设备,它返回E I N VA L。 / d e v / n u l l是一种不支持任何 i o c t l操作的字符特殊文件,所以 i o c t l返回 E N O D E V。最后, / e t c / m o t d是一个普通文件,而不是字符特殊文件,所以返回 E N O T T Y(这是在这种情况下的典 型返回值)。 E N O T T Y(Not a typewriter)是个历史产物,当i o c t l企图对并不引用字符特殊 设备的描述符进行操作时,U N I X内核都返回E N O T T Y。 实例 如果i o c t l的参数re q u e s t是I _ L I S T,则系统返回该流上所有模块的名字,包括最顶端的驱动 程序。(指明最顶端的原因是:在多路转接驱动程序的情况下,有多个驱动程序。 AT & T 〔1 9 9 0 d〕)第1 0章讨论了多路转接驱动程序的细节。)其第三个参数应当是指向s t r _ l i s t结构的指 针。 struct str_list { i n t sl_nmods; /* number of entries in array */ struct str_mlist *sl_modlist; /* ptr to first element of array */ } ; 应将s l _ m o d l i s t设置为指向s t r _ m l i s t结构数组的第一个元素,将 s l _ n m o d s设置为该数组中的 项数。 struct str_mlist { char 1_name[FMNAMESZ+1]; /* null terminated module name */ } ; 常数F M N A M E S Z定义在头文件 < s y s / c o n f . h >中,其值常常是 8。l _ n a m e的实际长度是 2 9 2 U N I X环境高级编程 下载 F M N A M E S Z + 1,增加1个字节是为了存放n u l l终止符。 如果i o c t l的第三个参数是0,则该函数返回的是模块数,而不是模块名。我们将先用这种 i o c t l调用确定模块数,然后再分配所要求的s t r _ m l i s t结构数。 程序1 2 - 1 0例示了I _ L I S T操作。由i o c t l返回的名字列表并不对模块和驱动程序进行区分,因 为该列表的最后一项是处于流底部的驱动程序,所以在打印时将其标明为驱动程序。 程序12-10 打印流中的模块名 $ w h o s t e v e n s c o n s o l e Sep 25 06:12 s t e v e n s p t s 0 0 1 Oct 12 07:12 $ a.out /dev/pts001 #modules= 4 module: ttcompat module: ldterm module: ptem driver: pts $ a.out /dev/console 第 1 2章 高 级 I/O 2 9 3下载 #modules= 5 module: ttcompat module: ldterm module: ansi module: char driver: cmux 在这两种情形中,顶上的两个流模块都是一样的( t t c o m p a t和l d t e r m),但是余下的模块和 驱动程序则不同。第1 9章将说明伪终端的情形。 12.4.4 write至流设备 在表1 2 - 4中可以看到w r i t e至流设备产生一个M _ D ATA消息。一般而言,这确实如此,但是 也还有一些情况需要考虑。首先,流中顶部的一个处理模块规定了可顺流传送的最小、最大数 据包长度(无法查询该模块中规定的这些值)。如果w r i t e的数据长度超过最大值,则流首将这 一数据分解成最大长度的若干数据包。最后一个数据包的长度则小于最大值。 接着要考虑的是:如果向流write 0个字节,又将如何呢?除非流涉及管道或 F I F O,否则就 顺流发送0长消息。对于管道F I F O,为与以前版本兼容,系统的默认处理方式是忽略 0长w r i t e。 可以用i o c t l设置实现管道和F I F O的流写方式以更改这种默认处理方式。 12.4.5 写方式 可以用i o c t l取得和设置一个流的写方式。如果将 re q u e s t设置为I _ G W R O P T,第三个参数为 指向一个整型变量的指针,则该流的当前写方式就在该整型量中返回。如果将 re q u e s t设置为 I _ S W R O P T,第三个参数是一个整型值,则其值就成为该流新的写方式。如同处理文件描述符 标志和文件状态标志(见 3 . 1 3节)一样,总是应当先取当前写方式值 ,然后修改它,而不只是将写 方式设置为某个绝对值(很可能要关闭某些原来打开的位)。 目前,只定义了两个写方式值。 • SNDZERO 对管道和F I F O的0长写会造成顺流传送一个0长消息。按系统默认,0长写不 发送消息。 • SNDPIPE 在流上已出错后,若调用w r i t e或p u t m s g,则向调用进程发送S I G P I P E信号。 流也有读方式,我们先说明g e t m s g和g e t p m s g函数,然后再说明读方式。 12.4.6 getmsg和g e t p m s g函数 #include int getmsg(int f i l e d e s,struct strbuf *c t l p t r, struct strbuf *d a t a p t r,int * f l a g p t r) ; int getpmsg(int f i l e d e s,struct strbuf *c t l p t r, struct strbuf *d a t a p t r, int *b a n d p t r, int *f l a g p t r) ; 两个函数返回:若成功则为非负值,若出错则为 - 1 注意,f l a g p t r和b a n d p t r是指向整型的指针。在调用之前,这两个指针所指向的整型单元中 应设置成所希望的消息类型,在返回时,此整型量设置为所读到的消息的类型。 2 9 4 U N I X环境高级编程 下载 如果f l a g p t r指向的整型单元的值是 0,则g e t m s g返回流首读队列中的下一个消息。如果下 一个消息是高优先权消息,则在返回时, f l a g p t r所指向的整型单元设置为 R S _ H I P R I。如果希 望只接收高优先权消息,则在调用 g e t m s g之前必须将 f l a g p t r所指向的整型单元设置为 R S _ H I P R I。 g e t p m s g使用了一个不同的常数集。并且它使用b a n d p t r指明特定的优先波段。 这两个函数有很多条件来确定返回给调用者何种消息: ( a )f l a g p t r和b a n d p t r所指向的值, (b) 流队列中消息的类型,(c) 是否指明非空d a t a p t r和c t l p t r, (d) c t l p t r- >m a x l e n和d a t a p t r- >m a x l e n 的值。对使用g e t m s g而言,并不需要了解所有这些细节。如欲了解这些细节请参阅 g e t m s g(2) 手册页。 12.4.7 读方式 如果r e a d流设备会发生些什么呢?有两个潜在的问题: ( 1 )如果读到流中消息的记录边界将 会怎样?( 2 )如果调用r e a d,而流中下一个消息有控制信息又将如何?对第一种情况的默认处理 方式被称为字节流方式。在这种方式中, r e a d从流中取数据直至满足了要求,或者已经没有数 据。在这种方式中,忽略流中消息的边界。对第二种情况的默认处理是,如果在队列的前端有 控制消息,则r e a d出错返回。可以改变这两种默认处理方式。 调用i o c t l时,若将re q u e s t设置为I _ G R D O P T,第三个参数又是指向一个整型单元的指针, 则对该流的当前读方式在该整型单元中返回。如果将 re q u e s t设置为I _ S R D O P T,第三个参数是 整型值,则该流的读方式设置为该值。读方式值有下列三个: • RNORM 普通,字节流方式(与上面说明的相同)。这是默认方式。 • RMSGN 消息不删除方式。读从流中取数据直至读到所要求的字节数,或者到达消息边 界。如果某次读只用了消息的一部分,则其余下部分仍留在流中,以供下一个读取。 • RMSGD 消息删除方式。这与不删除方式的区别是,如果某次读只用消息的一部分。则 余下部分就被删除,不再使用。 在读方式中还可指定另外三个常数,以便设置在读到流中包含协议信息的消息时 r e a d的处 理方法: • RPROTNORM 协议-普通方式。r e a d出错返回,e r r n o设置为E B A D M S G。这是默认方式。 • RPROTDAT 协议-数据方式。r e a d将控制部分作为数据返回给调用者。 • RPROTDIS 协议-删除方式。r e a d删除消息中的控制信息,但是返回消息中的数据。 实例 程序1 2 - 11是在程序3 - 3的基础上改写的,它用g e t m s g代替了r e a d。如果在S V R 4之下(其管 道和终端都是用流实现的)运行此程序则得: $ echo hello,world | a.out 使用流实现要求的管道 f l a g = 0 , c t l . l e n = - 1 , d a t . l e n = 1 3 h e l l o , w o r l d f l a g = 0 , c t l . l e n = 0 , d a t . l e n = 0 流挂断 $ a . o u t 使用流实现要求的终端 this is line 1 f l a g = 0 , c t l . l e n = - 1 , d a t . l e n = 1 5 this is line 1 and line 2 f l a g = 0 , c t l . l e n = - 1 , d a t . l e n = 1 1 第 1 2章 高 级 I/O 2 9 5下载 and line 2 ˆ D 键入自定义的终端E O F字符 f l a g = 0 , c t l . l e n = - 1 , d a t . l e n = 0 文件结尾与挂断不相同 $ a.out < /etc/motd getmsg error:Not a stream device 当管道被关闭时(当e c h o终止时),它对程序1 2 - 11表现为一个流挂断——控制长度和数据长度 都设置为0。(1 4 . 2节将讨论管道。)但是对于终端,键入文件结束字符,只使返回的数据长度 为0。这与挂断并不相同。如所预料的一样,将标准输入重新定向到一个非流设备, g e t m s g出 错返回。 程序1 2 - 11 用g e t m s g将标准输入复制到标准输出 12.5 I/O多路转接 当从一个描述符读,然后又写到另一个描述符时,可以在下列形式的循环中使用阻塞 I / O: while ( (n=read(STDIN_FILENO, buf, BUFSIZ) ) > 0) if (write (STDOUT_FILENO, buf, n) != n) err_sys (write error”) ; 这种形式的阻塞I / O到处可见。但是如果必须读两个描述符又将如何呢?如果仍旧使用阻塞 I / O, 那么就可能长时间阻塞在一个描述符上,而另一个描述符虽有很多数据却不能得到及时处理。 所以为了处理这种情况显然需要另一种不同的技术。 让我们概略地观察一个调制解调器拨号程序的工作情况(该程序将在第 1 8章中介绍)。该 程序读终端(标准输入),将所得数据写到调制解调器上;同时读调制解调器,将所得数据写 到终端上(标准输出)。图1 2 - 7显示这种工作情况。 2 9 6 U N I X环境高级编程 下载 图12-7 调制解调器拨号程序概观 执行这段程序的进程有两个输入,两个输出。如果对这两个输入都使用阻塞 r e a d,那么就可能 在一个输入上长期阻塞,而另一个输入的数据则被丢失。 处理这种特殊问题的一种方法是:设置两个进程,每个进程处理一条数据通路。图 1 2 - 8中 显示了这种安排。 图12-8 使用两个进程实现调制解调器拨号程序 如果使用两个进程,则可使每个进程都执行阻塞 r e a d。但是也产生了这两个进程间相互配合问 题。如果子进程接收到文件结束符(由于电话线的一端已经挂断,使调制解调器也挂断),那 么该子进程终止,然后父进程接收到 S I G C H L D信号。但是,如若父进程终止(用户在终端上 键入了文件结束符),那么父进程应通知子进程停止工作。为此可以使用一个信号(例如 S I G U S R 1)。这使程序变得更加复杂。 另一个方式是仍旧使用一个进程执行该程序,但调用非阻塞 I / O读取数据,其基本思想是: 将两个输入描述符都设置为非阻塞的,对第一个描述符发一个 r e a d。如果该输入上有数据,则 读数据并处理它。如果无数据可读,则 r e a d立即返回。然后对第二个描述符作用样的处理。在 此之后,等待若干秒再读第一个描述符。这种形式的循环称为轮询。这种方法的不足之处是浪 费C P U时间。大多数时间实际上是无数据可读,但是仍不断反复执行 r e a d,这浪费了C P U时间。 在每次循环后要等多长时间再执行下一轮循环也很难确定。轮询技术在支持非阻塞 I / O的系统 上都可使用,但是在多任务系统中应当避免使用。 还有一种技术称之为异步 I / O(asynchronous I/O)。其基本思想是进程告诉内核,当一个 描述符已准备好可以进行 I / O时,用一个信号通知它。这种技术有两个问题。第一个是并非所 有系统都支持这种机制(现在它还不是 P O S I X的组成部分,可能将来会是)。S V R 4为此技术提 供S I G P O L L信号,但是仅当描述符引用流设备时,此信号才能工作。 4 . 3 + B S D有一个类似的 信号S I G I O,但也有类似的限制— 仅当描述符引用终端设备或网络时才能工作。这种技术的 第二个问题是,这种信号对每个进程而言只有 1个。如果使该信号对两个描述符都起作用,那 么在接到此信号时进程无法判别是哪一个描述符已准备好可以进行 I / O。为了确定是哪一个描 述符已准备好,仍需将这两个描述符都设置为非阻塞的,并顺序试执行 I / O。1 2 . 6节将简要说 明异步I / O。 一种比较好的技术是使用I / O多路转接(I/O multiplexing)。其基本思想是:先构造一张有 关描述符的表,然后调用一个函数,它要到这些描述符中的一个已准备好进行 I / O时才返回。 在返回时,它告诉进程哪一个描述符已准备好可以进行 I / O。 第 1 2章 高 级 I/O 2 9 7下载 使用终端 的用户 调制解调器 拨号进程 调制解调器 电话线 使用终端 的用户 调制解调器 拨号(父进程) 调制解调器 电话线 调制解调器 拨号(子进程) I / O多路转接至今还不是 P O S I X的组成部分。S V R 4和4 . 3 + B S D都提供s e l e c t函 数以执行I / O多路转接。p o l l函数只由S V R 4提供。S V R 4实际上用p o l l实现s e l e c t。 I / O多路转接在4 . 2 + B S D中是用s e l e c t函数提供的。虽然该函数主要用于终端 I / O和网络I / O,但它对其他描述符同样是起作用的。 S V R 3在增加流机制时增加了 p o l l函数。但在S V R 4之前,p o l l只对流设备起作用。S V R 4支持对任一描述符起作 用的p o l l。 s e l e c t和p o l l的可中断性 中断的系统调用的自动再起动是由4 . 2 + B S D引进的(见1 0 . 5节),但当时s e l e c t函数是不再起 动的。这种特性延续到4 . 3 + B S D,即使指定了S A _ R E S TA RT也是为此。但是,在S V R 4之下,如 果指定了S A _ R E S TA RT,那么s e l e c t和p o l l也是自动再起动的。为了将软件移植到S V R 4时阻止这 一点,如果信号可能中断s e l e c t或p o l l,则总是使用s i g n a l _ i n t r函数(见程序1 0 - 1 3)。 12.5.1 select函数 select函数使我们在SVR4和4.3+BSD之下可以执行I/O多路转接,传向select的参数告诉内核: (1) 我们所关心的描述符。 (2) 对于每个描述符我们所关心的条件(是否读一个给定的描述符?是否想写一个给定的 描述符?是否关心一个描述符的异常条件?)。 (3) 希望等待多长时间(可以永远等待,等待一个固定量时间,或完全不等待)。 从s e l e c t返回时,内核告诉我们: (1) 已准备好的描述符的数量。 (2) 哪一个描述符已准备好读、写或异常条件。 使用这种返回值,就可调用相应的I / O函数(一般是r e a d或w r i t e),并且确知该函数不会阻塞。 #include /* fd_set data type */ #include /* struct timeval */ #include /* function prototype might be here */ int select (int m a x f d p 1, fd_set *re a d f d s, fd_set *w r i t e f d s, fd_set *e x c e p t f d s, struct timeval * t v p t r) ; 返回:准备就绪的描述符数,若超时则为 0,若出错则为 - 1 先说明最后一个参数,它指定愿意等待的时间。 struct timeval{ long tv_sec; /* seconds */ long tv_usec; /* and microseconds */ }; 有三种情况: • t v p t r= =NULL 永远等待。如果捕捉到一个信号则中断此无限期等待。当所指定的描述符中的一个已准备 好或捕捉到一个信号则返回。如果捕捉到一个信号,则 s e l e c t返回-1, e r r n o设置为E I N T R。 • t v p t r- >t v _ s e c= =0 && t v p t r- >t v _ u s e c= =0 2 9 8 U N I X环境高级编程 下载 完全不等待。测试所有指定的描述符并立即返回。这是得到多个描述符的状态而不阻塞 s e l e c t函数的轮询方法。 • t v p t r- >t v _ s e c ! =0 | | t v p t r- >t v _ u s e c! =0 等待指定的秒数和微秒数。当指定的描述符之一已准备好,或当指定的时间值已经超过时 立即返回。如果在超时时还没有一个描述符准备好,则返回值是 0,(如果系统不提供微秒分辨 率,则t v p t r- >t v _ u s e c值取整到最近的支持值。)与第一种情况一样,这种等待可被捕捉到的信 号中断。 中间三个参数re a d f d s、w r i t e f d s和e x c e p t f d s是指向描述符集的指针。这三个描述符集说明了 我们关心的可读、可写或处于异常条件的各个描述符。每个描述符集存放在一个 f d _ s e t数据类 型中。这种数据类型的实现可见图1 2 - 9,它为每一可能的描述符保持了一位。 图12-9 对s e l e c t指定读、写和异常条件描述符 对f g _ s e t数据类型可以进行的处理是: ( a )分配一个这种类型的变量, ( b )将这种类型的一个 变量赋与同类型的另一个变量,或( c )对于这种类型的变量使用下列四个宏: 以下列方式说明了一个描述符集后: fd_set rset; int fd; 必须用F D _ Z E R O清除其所有位: FD_ZERO (&rset); 然后在其中设置我们关心的各位: FD_SET (fd,&rset); FD_SET (STDIN_FILENO,&rset); 从s e l e c t返回时,用F D _ I S S E T测试该集中的一个给定位是否仍旧设置: if (FD_ISSET(fd, &rset)){ . . . } 第 1 2章 高 级 I/O 2 9 9下载 每个可能描述符一位 数据类型 s e l e c t中间三个参数中的任意一个(或全部)可以是空指针,这表示对相应条件并不关心。如 果所有三个指针都是空指针,则 s e l e c t提供了较s l e e p更精确的计时器(回忆1 0 . 1 9节,s l e e p等待 整数秒,而对于s e l e c t,其等待的时间可以小于1秒;其实际分辨率取决于系统时钟。)习题1 2 . 6 给出了这样一个函数。 s e l e c t第一个参数m a x f d p1的意思是“最大f d加1(max fd plus 1)”。在三个描述符集中找出 最高描述符编号值,然后加 1,这就是第一个参数值。也可将第一个参数设置为 F D _ S E T S I Z E, 这是一个< s y s / t y p e s . h >中的常数,它说明了最大的描述符数(经常是 2 5 6或1 0 2 4)。但是对大多 数应用程序而言,此值太大了。确实,大多数应用程序只应用 3 ~ 1 0个描述符。如果将第三个参 数设置为最高描述符编号值加 1,内核就只需在此范围内寻找打开的位,而不必在数百位的大 范围内搜索。 例如,若编写下列代码: fd_set readset, writeset; F D _ Z E R O ( & r e a d s e t ) ; F D _ Z E R O ( & w r i t e s e t ) ; FD_SET(0, &readset); FD_SET(3, &readset); FD_SET(1, &writeset); FD_SET(2, &writeset); select (4, &readset, &writeset, NULL, NULL); 然后,图1 2 - 1 0显示了这两个描述符集的情况。 图12-10 select的样本描述符集 因为描述符编号从0开始,所以要在最大描述符编号值上加 1。第一个参数实际上是要检查的描 述符数(从描述符0开始)。 s e l e c t有三个可能的返回值。 (1) 返回值-1表示出错。这是可能发生的,例如在所指定的描述符都没有准备好时捕捉到 一个信号。 (2) 返回值0表示没有描述符准备好。若指定的描述符都没有准备好,而且指定的时间已经 超过,则发生这种情况。 (3) 返回一个正值说明了已经准备好的描述符数,在这种情况下,三个描述符集中仍旧打 开的位是对应于已准备好的描述符位。 3 0 0 U N I X环境高级编程 下载 这些位未查看 注意,除非返回正值,否则在返回后检查描述符集是没有意义的。若捕捉到 信号或计时器超时,那么描述符集的值取决于实现。确实,若计时器超时, 4 . 3 + B S D并不改变描述符集,而S V R 4则清除描述符集。 在S V R 4和B S D的s e l e c t实现之间,有另一些差异。 B S D系统总是返回每一个 集中准备就绪的描述符数之和。若两个集中的同一描述符准备就绪(例如,读集 和写集),则该描述符计两次。不幸, S V R 4更改了这一点,若同一描述符在多个 集中准备就绪,该描述符只计一次。这再一次显示了我们将会碰到的问题,直至 P O S I X标准化了s e l e c t这样的函数才能解决此问题。 对于“准备好”的意思要作一些更具体的说明: (1) 若对读集(re a d f d s)中的一个描述符的r e a d不会阻塞,则此描述符是准备好的。 (2) 若对写集(w r i t e f d s)中的一个描述符的w r i t e不会阻塞,则此描述符是准备好的。 (3) 若对异常条件集( e x c e p t f d s)中的一个描述符有一个未决异常条件,则此描述符是准 备好的。现在,异常条件包括: ( a )在网络连接上到达指定波特率外的数据,或者 ( b )在处于数 据包方式的伪终端上发生了某些条件。(S t e v e n s〔1 9 9 0〕的1 5 . 1 0节中说明了这种条件。) 应当理解一个描述符阻塞与否并不影响 s e l e c t是否阻塞。也就是说,如果希望读一个非阻 塞描述符,并且以超时值为 5秒调用s e l e c t,则s e l e c t最多阻塞5秒。相类似,如果指定一个无限 的超时值,则s e l e c t阻塞到对该描述符数据准备好,或捕捉到一个信号。 如果在一个描述符上碰到了文件结束,则 s e l e c t认为该描述符是可读的。然后调用r e a d,它 返回0,这是U N I X指示到达文件结尾处的方法。(很多人错误地认为,当到达文件结尾处时, s e l e c t会指示一个异常条件。) 12.5.2 poll函数 S V R 4的p o l l函数类似于s e l e c t,但是其调用形式则有所不同。我们将会看到, p o l l与流系统 紧紧相关,虽然在S V R 4中,可以对任一描述符都使用该函数。 #include #include int poll(struct pollfd f d a rr a y[],unsigned long n f d s,int t i m e o u t) ; 返回:准备就绪的描述符数,若超时则为 0,若出错则为 - 1 与s e l e c t不同,p o l l不是为每个条件构造一个描述符集,而是构造一个 p o l l f d结构数组,每 个数组元素指定一个描述符编号以及对其所关心的条件。 struct pollfd { i n t f d ; /* file descriptor to check, or < 0 to ignore */ s h o r t e v e n t s ; /* events of interest on fd */ s h o r t r e v e n t s ; /* events that occurred on fd */ } ; f d a rr a y数组中的元素数由n f d s说明。 由于某种未知的原因, S V R 3说明n f d s的类型为unsigned long,这似乎是太大 了。在S V R 4手册p o l l的原型中,第二个参数的数据类型为 s i z e _ t,但在< p o l l . h >包 含的实际原型中,第二个参数的数据类型仍说明为 unsigned long。 第 1 2章 高 级 I/O 3 0 1下载 S V R 4的S V I D〔AT & T 1 9 8 9〕说明p o l l的第一个参数是struct pollfd f d a rr a y[ ],而 S V R 4手册页〔AT&T 1990 d〕则说明该参数为struct pollfd *f d a rr a y。在C语言中, 这两种说明是等价的。我们使用第一种说明以重申 f d a rr a y指向一个结构数组,而 不是指向单个结构的指针。 应将e v e n t s成员设置为表1 2 - 5中所示值的一个或几个。通过这些值告诉内核我们对该描述 符关心的是什么。返回时,内核设置 r e v e n t s成员,以说明对该描述符发生了什么事件。(注意, p o l l没有更改e v e n t s成员,这与s e l e c t不同,s e l e c t修改其参数以指示哪一个描述符已准备好了。) 表12-5 poll的e v e n t s和r e v e n t s标志 表1 2 - 5中头四行测试可读性,接着三行测试可写性,最后三行则是异常条件。最后三行是 由内核在返回时设置的。即使在 e v e n t s字段中没有指定这三个值,如果相应条件发生,则在 r e v e n t s中也返回它们。 当一个描述符被挂断后(P O L L H U P),就不能再写向该描述符。但是仍可能从该描述符读 取到数据。 p o l l的最后一个参数说明我们想要等待多少时间。如同 s e l e c t一样,有三种不同的情形: • timeout == INFTIM 永远等待。常数INFTIM定义在,其值通常是-1。当所指定 的描述符中的一个已准备好,或捕捉到一个信号则返回。如果捕捉到一个信号,则p o l l返回-1, errno设置为EINTR。 • t i m e o u t == 0 不等待。测试所有描述符并立即返回。这是得到很多个描述符的状态而不 阻塞p o l l函数的轮询方法。 • timeout > 0 等待t i m e o u t毫秒。当指定的描述符之一已准备好,或指定的时间值已超过时 立即返回。如果已超时但是还没有一个描述符准备好,则返回值是 0。(如果系统不提供毫秒分 辨率,则t i m e o u t值取整到最近的支持值。) 应当理解文件结束与挂断之间的区别。如果正在终端输入数据,并键入文件结束字符, P O L L I N被打开,于是就可读文件结束指示( r e a d返回0)。P O L L H U P在r e v e n t s中没有打开。如 果读调制解调器,并且电话线已挂断,则在 r e v e n t s中将接到P O L L H U P。 与s e l e c t一样,不论一个描述符是否阻塞,并不影响p o l l是否阻塞。 3 0 2 U N I X环境高级编程 下载 对e v e n t s 的输入 名 称 从r e v e n t s得 到的结果 说 明 可读除高优级外的数据,不阻塞 可读普通(优先波段0 )数据,不阻塞 可读0优先波段数据,不阻塞 可读高优先级数据,不阻塞 可与普通数据,不阻塞 与P O L L O U T相同 可写非0优先波段数据,不阻塞 已出错 已挂起 此描述符并不引用一打开文件 12.6 异步I / O 使用s e l e c t和p o l l可以实现异步I / O。关于描述符的状态,系统并不主动告诉我们任何信息 ,我 们需要主动地进行查询(调用 s e l e c t或p o l l)。如在第1 0章中所述,信号机构提供一种异步形式 的通知某种事件已发生的方法。S V R 4和4 . 3 + B S D提供了使用一个信号(在S V R 4中是S I G P O L L, 在4 . 3 + B S D中是S I G I O)的异步I / O方法,该信号通知进程,对某个描述符所关心的某个事件已 经发生。 我们已了解到在 S V R 4中s e l e c t和p o l l对任意描述符都能工作。在 4 B S D中, s e l e c t对任意描述符都能工作。但是关于异步I / O却有限制。在S V R 4中,异步I / O只 对设备起作用。在4 . 3 + B S D中,异步I / O只对终端和网络起作用。 S V R 4和4 . 3 + B S D所支持的异步I / O的一个限制是每个进程只有一个信号。如果要对几个描 述符进行异步I / O,那么在进程接收到该信号时并不知道这一信号对应于哪一个描述符。 12.6.1 SVR4 在系统V中,异步 I / O是流系统的一部分。它只对流设备起作用。 S V R 4异步I / O信号是 S I G P O L L。 为了对一个流设备起动异步 I / O,需要调用 i o c t l,而其第二个参数( re q u e s t)则为 I _ S E T S I G。第三个参数则是由表1 2 - 6中一个或多个常数构成的整型值。这些常数在 < s t r o p t s . h > 中定义。 表12-6 产生S I G P O L L信号的条件 表1 2 - 6中“已到达”的意思是“已到达流首的读队列”。 除了调用i o c t l以说明产生S I G P O L L信号的条件,也应为该信号建立一个信号处理程序。回 忆表1 0 - 1,对于S I G P O L L的默认动作是终止该进程,所以应在调用i o c t l之前建立信号处理程序。 12.6.2 4.3+BSD 在4 . 3 + B S D中,异步I / O是两个信号S I G I O和S I G U R G的组合。前者是通用异步I / O信号,后 者则只被用来通知进程在网络连接上到达了非规定波特率的数据。 第 1 2章 高 级 I/O 3 0 3下载 常 数 说 明 非高优先级的消息已到达 一普通消息已到达 一0优先波段消息已到达 若此常数说明为S _ R D B A N D,则当一非0优先波段消息到达时产生S I G U R G信号而非S I G P O L L 一高优先级消息已到达 写队列不再满 与S _ O U T P U T一样 可发送一非0优先波段消息 包含S I G P O L L信号的流信号消息已到达 M _ E R R O R消息已到达 M _ H A N G U P消息已到达 为了接收S I G I O信号,需执行下列三步: (1) 调用s i g n a l或s i g a c t i o n为该信号建立一个信号处理程序。 (2) 以命令F _ S E TO W N(见3 . 1 3节)调用f c n t l来设置进程I D和进程组I D,它们将接收对于 该描述符的信号。 (3) 以命令F _ S E T F L调用f c n t l设置O _ A S Y N C状态标志,使在该描述符上可以进行异步 I / O (见表3 - 2)。 第( 3 )步仅用于指向终端或网络的描述符,这是 4 . 3 + B S D异步传输设施的一个基本的限制。 对于S I G U R G信号,只需执行第( 1 )步和第( 2 )步。该信号仅对于指向支持带外数据的网络连 接的描述符而产生。 12.7 readv和w r i t e v函数 r e a d v和w r i t e v函数用于在一个函数调用中读、写多个非连续缓存。有时也将这两个函数称 为散布读(scatter re a d)和聚集写(gather write)。 #include #include ssize_t readv(int f i l e d e s,const struct iovec i o v[ ],int i o v c n t) ; ssize_t writev(int f i l e d e s,const struct iovec i o v[ ],int i o v c n t) ; 两个函数返回:已读、写的字节数,若出错则为 - 1 这两个函数的第二个参数是指向i o v e c结构数组的一个指针: struct iovec { void *iov_base; /* starting address of buffer */ size_t iov_len; /* size of buffer */ } ; i o v数组中的元素数由i o v c n t说明。 这两个函数始于4 . 2 B S D,现在S V R 4也提供它们。 这两个函数的原型以及它们使用的 i o v e c结构在各种有关文献资料中略有差 别。如果比较它们在 S V R 4程序员手册〔AT&T 1990e〕、S V R 4的S V I D〔AT & T 1 9 8 9〕,以及S V R 4和4 . 3 + B S D < s y s / u i o . h >头文件中的定义,那么它们之间都有差 别。其部分原因是: S V I D和S V R 4程序员手册对应于 1988 POSIX.1标准,而非 1 9 9 0版。上面示出的原型和结构定义对应于 r e a d和w r i t e的P O S I X . 1定义:缓存地 址是void *,缓存长度是s i z e _ t ,返回值是s s i z e _ t。 注意,r e a d v的第二个参数被说明为 c o n s t。这取自4 . 3 + B S D中该函数的原型, 在S V R 4的手册中则无此修饰词。对于 r e a d v此修饰词有效,因为并不修改 i o v e c结 构的成员——此函数只修改 i o v _ b a s e所指向的存储区。 4 . 3 B S D和S V R 4将i o v c n t限制为1 6。4 . 3 + B S D定义了常数U I O _ M A X I O V,当前 其值定义为 1 0 2 4。S V I D声称常数 I O V _ M A X提供了系统 V限制,但是它没有在 S V R 4的头文件中定义。 图1 2 - 11显示了r e a d v和w r i t e v的参数和i o v e c结构之间的关系。w r i t e v以顺序i o v[0], i o v[ 1 ]至 3 0 4 U N I X环境高级编程 下载 iov[iovcnt-1] 从缓存中聚集输出数据。writev返回输出的字节总数,它应等于所有缓存长度之和。 r e a d v则将读入的数据按上述同样顺序散布到缓存中。 r e a d v总是先填满一个缓存,然后再 填写下一个。r e a d v返回读得的总字节数。如果遇到文件结尾,已无数据可读,则返回 0。 图1 2 - 11 readv和w r i t e v的i o v e c结构 实例 在1 6 . 7节的_ d b _ w r i t e i d x函数中,需将两个缓存连续地写到一个文件中。第二个缓存是调 用者传递过来的一个参数,第一个缓存是我们创建的,它包含了第二个缓存的长度,文件中其 他信息的位移量。有三种方法可以实现这一要求: (1) 调用w r t r e两次,一次一个缓存。 (2) 分配一个大到足以包含两个缓存的新缓存。将两个缓存的内容复制到新缓存中。然后 对该缓存调用w r t r e一次。 (3) 调用w r i t e v输出两缓存。 1 6 . 7节中使用了w r i t e v。将它与另外两种方法进行比较,对我们很有启发。表 1 2 - 7显示了 上面所述三种方法的结果。 表12-7 比较w r i t e v和其他技术所得的时间结果 所用的测试程序输出1 0 0字节的头文件,接着又输出2 0 0字节的数据。这样做10 000次,产 生了一个百万字节的文件。该程序按上面所述方法写了 3个版本,各运行一次,测得它们各使 用的用户C P U时间、系统C P U时间和时钟时间。它们的单位都是秒。 正如我们所预料的,调用 w r i t e两次的系统时间是调用 w r i t e一次或调用w r i t e v一次的两倍, 这与表3 - 1的结果类似。 要注意的是:C P U时间(用户加系统)几乎是个常数,无论是在缓存复制后用一个 w r i t e还 是用一个w r i t e v。两者的区别只是在用户空间(缓存复制)或系统空间( w r i t e v)下执行的时 间多一点。在S PA R C系统上运行时,其和是4 . 9秒,而在8 0 3 8 6系统下运行则是约8 . 0秒。 对于表1 2 - 7最后要说明的是,在S PA R C上本测试所用的时钟时间主要用于磁盘数据传输上, 第 1 2章 高 级 I/O 3 0 5下载 缓存0 缓存1 缓存I 操 作 两次w r i t e 缓存复制,然后一次w r i t e 一次w r i t e 用户 系统 时钟 用户 系统 时钟 而在3 8 6系统上则主要用于C P U方面。 总而言之,我们一般采用r e a d v和w r i t e v,而不采用多次r e a d和w r i t e。时间结果表明采用缓 存复制后用一个w r i t e与采用一个w r i t e v所用C P U时间几乎一样,但一般说来,因为前者还需要 分配一个临时缓存用于存储及复制,所以后者更复杂。 12.8 readn和w r i t e n函数 某些设备,特别是终端、网络和S V R 4的流设备有下列两种性质: (1) 一次r e a d操作所返回的数据可能少于所要求的数据,即使还没达到文件尾端。这不是 一个错误,应当继续读该设备。 (2) 一次w r i t e操作的返回值也可能少于指定输出的字节数。这可能是由若干因素造成的, 例如,下游模块的流量控制限制。这也不是错误,应当继续写余下的数据至该设备。(通常, 只有对非阻塞描述符,或捕捉到一个信号时,才发生这种 w r i t e返回。) 在读、写磁盘文件时没有这两种性质。 在第1 8章中,我们将写一个流管道(基于 S V R 4流或BSD UNIX域套接口),其中需要考虑 这些特性。下面两个函数 r e a d n和w r i t e n的功能是读、写指定的N字节数据,并处理返回值小于 要求值的情况。这两个函数只是按需多次调用r e a d和w r i t e直至读、写了N字节数据。 #include "o u r h d r . h" ssize_t readn(int f i l e d e s,void *b u f f,size_t n b y t e s) ; ssize_t writen(int f i l e d e s,void *b u f f,size_t n b y t e s) ; 两个函数返回:已读、写字节数,若出错则为 - 1 在要将数据写到上面提到的设备上时,就可调用 w r i t e n,但是仅当先就知道要接收数据的数量 时,才调用r e a d n(通常,调用r e a d以接收来自这些设备的数据)。 程序1 2 - 1 2、程序1 2 - 1 3是w r i t e n和r e a d n的一种实现。 程序12-12 writen函数 3 0 6 U N I X环境高级编程 下载 程序12-13 readn函数 12.9 存储映射I / O 存储映射I / O使一个磁盘文件与存储空间中的一个缓存相映射。于是当从缓存中取数据, 就相当于读文件中的相应字节。与其类似,将数据存入缓存,则相应字节就自动地写入文件。 这样,就可以在不使用r e a d和w r i t e的情况下执行I / O。 为了使用这种功能,应首先告诉内核将一个给定的文件映射到一个存储区域中。这是由 m m a p函数实现的。 #include #include caddr_t mmap(caddr_t a d d r, size_t l e n,int p ro t,int f l a g, int f i l e d e s,off_t o f f) ; 返回:若成功则为映射区的起始地址,若出错则为 - 1 存储映照I / O已经用了很多年。4 . 1 B S D(1 9 8 1)以其v r e a d和v w r i t e函数提供了 一种不同形式的存储映射I / O。4 . 2 B S D没有使用这两个函数,而是希望使用 m m a p 函数。但是由于 L e ff l e r等[ 1 9 8 9 ] 2 . 5节中说明的理由, 4 . 2 B S D实际并没有包含 m m a p函数。G i n g e l l , M o r a n和S h a n n o n〔1 9 8 7〕说明了m m a p的一种实现。现在, S V R 4和4 . 3 + B S D都支持m m a p函数。 数据类型c a d d r _ t通常定义为char *。a d d r参数用于指定映射存储区的起始地址。通常将其设 置为0,这表示由系统选择该映射区的起始地址。此函数的返回地址是:该映射区的起始地址。 f i l e d e s指定要被映射文件的描述符。在映射该文件到一个地址空间之前,先要打开该文件。 l e n是映射的字节数。o f f是要映射字节在文件中的起始位移量(下面将说明对o f f值有某些限制)。 第 1 2章 高 级 I/O 3 0 7下载 在说明其余参数之前,先看一下存储映射文件的基本情况。图 1 2 - 1 2显示了一个存储映射 文件。(见图7 - 3中进程存储空间的典型安排情况。) 图12-12 存储映射文件的例子 在此图中,“起始地址”是m m a p的返回值。在图中,映射存储区位于堆和栈之间:这属于实现 细节,各种实现之间可能不同。 p ro c参数说明映射存储区的保护要求。 见表1 2 - 8。 对于映射存储区所指定的保护要求与 文件的o p e n方法匹配。例如,若该文件是 只读打开的,那么对映射存储区就不能指 定P R O T _ W R I T E。 f l a g参数影响映射存储区的多种属性: • MAP_FIXED 返回值必须等于a d d r。因为这不利于可移植性,所以不鼓励使用此标志。 如果未指定此标志,而且a d d r非0,则内核只把a d d r视为何处设置映射区的一种建议。 通过将a d d r指定为0可获得最大可移植性。 • MAP_SHARED 这一标志说明了本进程对映射区所进行的存储操作的配置。此标志指定 存储操作修改映射文件— 也就是,存储操作相当于对该文件 w r i t e。必须指定本标志或下一个 标志(M A P _ P R I VAT E)。 • MAP_PRIVATE 本标志说明,对映射区的存储操作导致创建该映射文件的一个副本。所 有后来对该映射区的存访都是存访该副本,而不是原始文件。 4 . 3 + B S D还有另外一些 M A P _ x x x标志值,它们是这种现实所特有的。详细情况请参见 4.3+BSD mmap(2)手册页。 o f f和a d d r的值(如果指定了M A P _ F I X E D)通常应当是系统虚存页长度的倍数。在 S V R中, 3 0 8 U N I X环境高级编程 下载 高地址 栈 文件的存储 映射部分 堆 非初始化数据 ( b s s ) 初始化数据 正文低地址 起始地址 文件: 文件的存储 映射部分 说 明 区域可读 区域可写 区域可存取( 4 . 3 + B S D无) 区域可执行 表12-8 存储映射区的保护 虚存页长度可用带参数 S C _ PA G E S I Z E的s y s c o n f函数(见2 . 5 . 4节)得到。在4 . 3 + B S D之下,页 长度由头文件< s y s / p a r a m . h >中的常数N B P G定义。因为o f f和a d d r常常指定为0,所以这种要求 一般并不是问题。 因为映射文件的起动位移量受系统虚存页长度的限制,那么如果映射区的长度不是页长度 的整数倍时,将如何呢?假定文件长1 2字节,系统页长为5 1 2字节,则系统通常提供5 1 2字节的 映射区,其中后5 0 0字节被设为0。可以修改这5 0 0字节,但任何变动都不会在文件中反映出来。 与映射存储区相关有两个信号:S I G S E G V和S I G B U S。信号S I G S E G V通常用于指示进程试 图存取它不能存取的存储区。如果进程企图存数据到用 m m a p指定为只读的映射存储区,那么 也产生此信号。如果存取映射区的某个部分,而在存取时这一部分已不存在,则产生 S I G B U S 信号。例如,用文件长度映射一个文件,但在存访该映射区之前,另一个进程已将该文件截短。 此时,如果进程企图存取对应于该文件尾端部分的映射区,则接收到 S I G B U S信号。 在f o r k之后,子进程继承存储映射区(因为子进程复制父进程地址空间,而存储映射区是 该地址空间中的一部分),但是由于同样的理由,e x e c后的新程序则不继承此存储映射区。 进程终止时,或调用了 m u n m a p之后,存储映射区就被自动去除。关闭文件描述符 f i l e d e s 并不解除映射区。 #include #include int munmap(caddr_t a d d r,size_t l e n) ; 返回:若成功则为0,若出错则为 - 1 munmap 并不影响被映射的对象— 也就是说,调用m u n m a p并不使映射区的内容写到磁盘文件 上。对于M A P _ S H A R E D区磁盘文件的更新,在写到存储映射区时按内核虚存算法自动进行。 某些系统提供了一个m s y n c函数,它类似于f s y n c函数(见4 . 2 4节),但对存储 映射区起作用。 实例 程序1 2 - 1 4用存储映射I / O复制一个文件(类似于 c p ( 1 )命令)。首先打开两个文件,然后调 用f s t a t得到输入文件的长度。在调用 m m a p和设置输出文件长度时都需使用输入文件长度。调 用l s e e k,然后写一个字节以设置输出文件的长度。如果不设置输出文件的长度,则对输出文件 调用m m a p也可以,但是对相关存储区的第一次存访会产生 S I G B U S。也可使用f t r u n c a t e函数来 设置输出文件的长度,但是并非所有系统都支持该函数扩充文件长度(见 4 . 1 3节)。 然后对每个文件调用m m a p,将文件映射到存储区,最后调用 m e m c p y将输入缓存的内容复 制到输出缓存。在从输入缓存( s r c)取数据字节时,内核自动读输入文件;在将数据存入输 出缓存(d s t)时,内核自动将数据写到输出文件中。 程序12-14 用存储映射I / O复制文件 第 1 2章 高 级 I/O 3 0 9下载 将存储区映射复制与用r e a d,w r i t e进行的复制(缓存长度为8 1 9 2)相比较,得到表1 2 - 9中 所示的结果。 表12-9 read/write与m m a p / m e m c p y比较的时间结果 时间单位是秒,被复制文件的长度是约3百万字节。 对于S PA R C,两种复制方式的 C P U时间(用户+系统)相同,都是 2 . 6秒。(这与表1 2 - 7中 w r i t e v的情况类似)。对于3 8 6,m m a p / m e m c p y方式大约是r e a d / w r i t e方式的一半。 使用mmap时,SPARC和386系统时间都减少的原因是:内核直接对映射存储缓存作I/O操作。 而在read/write方式,内核要在用户缓存和它自己的缓存之间进行复制,然后用其缓存作I/O。 另一个要注意的是,使用m m a p / m e m c p y时,时钟时间至少减半。 将一个普通文件复制到另一个普通文件中时,存储映射 I / O比较快。但是有一些限制,如 不能用其在某些设备之间(例如网络设备或终端设备)进行复制,并且对被复制的文件进行映 3 1 0 U N I X环境高级编程 下载 操 作 S PA R C 8 0 3 3 6 用户 系统 时钟 用户 系统 时钟 r e a d / w r i t e 0 . 0 2 . 6 11 . 0 0 . 0 5 . 3 11 . 2 m m a p / m e m c p y 0 . 9 1 . 7 3 . 7 0 . 3 2 . 7 5 . 7 射后,也要注意该文件的长度是否改变。尽管如此,有很多应用程序会从存储映射 I / O得到好 处,因为它处理的是存储空间而不是读、写一个文件,所以常常可以简化算法。从存储映射 I / O中得益的一个例子是帧缓存设备,该设备引用一个位 -映射显示。 K r i e g e r, S t u m m和U n r a u〔1 9 9 2〕第5章说明了一个使用存储映射I / O的标准I / O库。 1 4 . 9节将返回到存储映射I / O,其中有一个例子,说明在 S V R 4和4 . 3 + B S D之下如何使用存 储映射I / O在有关进程间提供共享存储区。 12.10 小结 本章说明了很多高级I / O功能,其中大多数将在后面章节的例子中使用: • 非阻塞I / O— 发一个I / O操作,不使其阻塞。 • 记录锁。 • 系统V流机制。 • I/O多路转接— s e l e c t和p o l l函数。 • readv和w r i t e v函数。 • 存储空间映射I / O(m m a p)。 习题 1 2 . 1用删除程序1 2 - 6 f o r循环中第二次调用w r i t e的语句后结果如何,为什么? 1 2 . 2用查看系统中< s y s / t y p e s . h >头文件,并研究s e l e c t和四个F D _宏的实现。 1 2 . 3用< s y s / t y p e s . h >头文件中定义了f d _ s e t数据类型可以处理的最大描述符数,假设需要将 描述符数增加到2 0 4 8,该如何实现? 1 2 . 4用比较处理信号量集的函数(见 1 0 . 11节)和f d _ s e t描述符集的函数,并研究在你的系 统上实现它们的方法。 1 2 . 5用g e t m s g可以返回多少种不同的信息? 1 2 . 6用用s e l e c t或p o l l实现一个与s l e e p类似的函数s l e e p _ u s,不同之处是要等待指定的若干 微秒。比较这个函数和B S D中的u s l e e p函数。 1 2 . 7用是否可以利用建议性锁来实现程序 1 0 - 1 7中的函数T E L L _ WA I T、T E L L _ PA R E N T、 T E L L _ C H I L D、WA I T _ PA R E N T以及WA I T _ C H I L D?如果可以,编写这些函数并测试其功能。 1 2 . 8用用s e l e c t或p o l l测试管道的容量。将其值与第2章的P I P E _ B U F的值比较。 1 2 . 9用运行程序1 2 - 1 4拷贝一个文件,检查输入文件的上一次访问时间是否改变了? 1 2 . 1 0用在程序1 2 - 1 4中m m a p后调用c l o s e关闭输入文件,以验证关闭描述符不会使内存映 射I / O失效。 第 1 2章 高 级 I/O 3 1 1下载
还剩38页未读

继续阅读

下载pdf到电脑,查找使用更方便

pdf的实际排版效果,会与网站的显示效果略有不同!!

需要 3 金币 [ 分享pdf获得金币 ] 0 人已下载

下载pdf

pdf贡献者

xnchall

贡献于2015-09-02

下载需要 3 金币 [金币充值 ]
亲,您也可以通过 分享原创pdf 来获得金币奖励!
下载pdf