书名:UNIX网络编程 卷2:进程间通信(第2版)
ISBN:978-7-115-51780-7
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 [美] W. 理查德·史蒂文斯(W. Richard Stevens)
责任编辑 杨海玲
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
本书是UNIX网络编程的经典之作。进程间通信(IPC)几乎是所有UNIX程序性能的关键,理解IPC也是理解如何开发不同主机间网络应用程序的必要条件。本卷从对Posix IPC和System V IPC的内部结构开始讨论,全面深入地介绍了4种IPC形式:消息传递(管道、FIFO、消息队列)、同步(互斥锁、条件变量、读写锁、文件与记录锁、信号量)、共享内存(匿名共享内存、具名共享内存)及远程过程调用(Solaris 门、Sun RPC)。附录中给出了测量各种IPC形式性能的方法。
本书内容详尽且具权威性,几乎每章都提供精选的习题,并提供了部分习题的答案,是网络研究和开发人员理想的参考书。
Authorized translation from the English language edition, entitled UNIX NETWORK PROGRAMMING, VOLUME 2: INTERPROCESS COMMUNICATIONS, 2nd Edition, ISBN: 0130810819 by STEVENS, W. RICHARD, published by Pearson Education, Inc, Copyright © 1999 by Prentice Hall PTR.
All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.
CHINESE SIMPLIFIED language edition published by POSTS & TELECOM PRESS, Copyright © 2019.
本书中文简体字版由Pearson Education Inc授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。
本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。
版权所有,侵权必究。
大多数重要的程序都涉及进程间通信(Interprocess Communication, IPC)。这是受下述设计原则影响的自然结果:把应用程序设计为一组相互通信的小片断比将其设计为单个庞大的程序更好。从历史角度看,应用程序有如下几种构建方法。
(1) 用一个庞大的程序完成全部工作。程序的各部分可以实现为函数,函数之间通过参数、返回值和全局变量来交换信息。
(2) 使用多个程序,程序之间用某种形式的IPC进行通信。许多标准的UNIX工具都是按这种风格设计的,它们使用shell管道(IPC的一种形式)在程序之间传递信息。
(3) 使用一个包含多个线程的程序,线程之间使用某种IPC。这里仍然使用术语IPC,尽管通信是在线程之间而不是在进程之间进行的。
还可以把后两种设计形式结合起来:用多个进程来实现,其中每个进程包含几个线程。在这种情况下,进程内部的线程之间可以通信,不同的进程之间也可以通信。
上面讲述了可以把完成给定任务所需的工作分到多个进程中,或许还可以进一步分到进程内的多个线程中。在包含多个处理器(CPU)的系统中,多个进程也许可以(在不同的CPU上)同时运行,或许给定进程内的多个线程也能同时运行。因此,把任务分到多个进程或线程中有望减少完成指定任务的时间。
本书详细描述了以下4种不同的IPC形式:
(1) 消息传递(管道、FIFO和消息队列);
(2) 同步(互斥量、条件变量、读写锁、文件和记录锁、信号量);
(3) 共享内存(匿名的和具名的);
(4) 远程过程调用(Solaris门和Sun RPC)。
本书不讨论如何编写通过计算机网络通信的程序。这种通信通常涉及使用TCP/IP协议族的套接字API,相关主题在卷1 [Stevens 1998]中有详细讨论。
有人可能会提出质疑:不应该使用单主机或非网络IPC(本卷的主题),所有程序都应该在网络上的多台主机上同时运行。但在日常实践中,单主机IPC往往比网络通信快得多,而且有时还简单些。共享内存、同步等方法通常也只能用于单主机,跨网络时可能无法使用。经验和历史表明,非网络IPC(本卷)与跨网络IPC(卷1)都是需要的。
本卷建立在卷1和我写的另外4本书的基础上,这5本书在本书中简记如下:
UNPv1:UNIX Network Programming, Volume 1 [Stevens 1998];
APUE:Advanced Programming in the UNIX Environment [Stevens 1992];
TCPv1:TCP/IP Illustrated, Volume 1 [Stevens 1994];
TCPv2:TCP/IP Illustrated, Volume 2 [Wright and Stevens 1995];
TCPv3:TCP/IP Illustrated, Volume 3 [Stevens 1996]。
在一本书名包含“网络编程”的书中讨论IPC看似有点奇怪,但事实上IPC经常用于网络应用程序。我在《UNIX网络编程》1990年版的前言里就指出:“想知道如何为网络开发软件,必须先理解进程间通信(IPC)。”
本书完全重写并扩充了1990年版《UNIX网络编程》的第3章和第18章。字数统计表明,现在的内容是第1版的5倍。新版的主要改动归纳如下。
不仅讨论了“System V IPC”的三种形式(消息队列、信号量以及共享内存),还对实现了这些IPC的新的Posix函数进行了介绍。(1.7节将详细介绍Posix标准族。)我认为使用Posix IPC函数是大势所趋,因为它们比System V中的相应部分更具优势。
讨论了用于同步的Posix函数:互斥锁、条件变量以及读写锁。它们可用于线程或进程的同步,而且往往在访问共享内存时使用。
本卷假定使用Posix线程环境(称为“Pthreads”),许多示例都是用多线程而不是多进程构建的。
对管道、FIFO和记录锁的讨论侧重于从它们的Posix定义出发。
本卷不仅描述了IPC机制及其使用方法,还实现了Posix消息队列、读写锁与Posix信号量(都可以实现为用户库)。这些实现可以把多种不同的特性捆绑起来(例如,Posix信号量的一种实现用到了互斥量、条件变量和内存映射I/O),还强调了我们在应用程序中经常要处理的一些问题(如竞争状态、错误处理、内存泄漏和变长参数列表)。理解某种特性的实现通常有助于了解如何使用该特性。
对RPC的讨论侧重于Sun的RPC包。在此之前讲述了新的Solaris门API,它类似于RPC但用于单主机。这么一来我们就介绍了许多在调用其他进程中的过程时需要考虑的特性,而不用关心网络方面的细节。
本书既可以用作IPC的教程,也可以用作有经验的程序员的参考书。全书划分为以下4个主要部分:
消息传递;
同步;
共享内存;
远程过程调用。
但许多读者可能只对特定的部分感兴趣。第2章总结了所有Posix IPC函数共有的特性,第3章归纳了所有System V IPC函数共有的特性,第12章介绍了Posix和System V的共享内存,但书中多数章节都可以独立于其他章节阅读。所有读者都应该阅读第1章,尤其是1.6节,该节介绍了一些贯穿全书的包装函数。讨论Posix IPC的各章与讨论System V IPC的各章彼此独立,有关管道、FIFO和记录锁的几章不属于上述两个阵营,关于RPC的两章也独立于其他IPC方法。
为了方便读者把本书作为参考书,本书提供了完整的全文索引,并在最后几页总结了每个函数和结构的详细描述在正文中的哪里可以找到。为了给不按顺序阅读本书的读者提供方便,我们在书中为各个主题提供了大量的交叉引用。
书中所有示例的源代码可以从作者主页(列在前言的最后)获得[1]。学习本书讲述的IPC技术的最好方法就是下载这些程序,对其进行修改和改进。只有这样实际编写代码才能深入理解有关概念和方法。每章末尾都提供了大量的习题,大部分已在附录D中给出答案。
本书的最新勘误表也可以从作者主页获取。
尽管封面上只出现了作者一个人的名字,但一本高质量的书的出版需要许多人的共同努力。首先要感谢我的家人,他们在我写书的那段时间里承担了一切。再次感谢你们:Sally、Bill、Ellen和David。
感谢技术审稿人给出的宝贵的反馈意见(打印出来有135页)。他们发现了许多错误,指出了需要更多解释的地方,并对表达、用词和代码提出了许多修改建议,他们是Gavin Bowe、Allen Briggs、Dave Butenhof、Wan-Teh Chang、Chris Cleeland、Bob Friesenhahn、Andrew Gierth、Scott Johnson、Marty Leisner、Larry McVoy、Craig Metz、Bob Nelson、Steve Rago、Jim Reid、Swamy K. Sitarama、Jon C. Snader、Ian Lance Taylor、Rich Teer和Andy Tucker。
下列诸位通过电子邮件回答过我的问题,有人甚至回答过很多问题。澄清这些问题提高了本书的准确性并改进了语言表达,他们是David Bausum、Dave Butenhof、Bill Gallmeister、Mukesh Kacker、Brian Kernighan、Larry McVoy、Steve Rago、Keith Skowran、Bart Smaalders、Andy Tucker和John Wait。
特别感谢GSquared的Larry Rafsky提供了很多帮助。像以往一样,感谢国家光学天文台(NOAO)、Sidney Wolff、Richard Wolff和Steve Grandi,他们为我提供了网络与主机的访问权限。DEC公司的Jim Bound、Matt Thomas、Mary Clouter和Barb Glover提供了用于本书多数示例的Alpha系统。书中的一部分代码是在其他Unix系统上测试的:感谢Red Hat软件公司的Michael Johnson提供了最新版本的Red Hat Linux,感谢IBM奥斯汀实验室的Dave Marquardt和Jessie Haug提供了RS/6000系统以及最新版本的AIX的访问权限。
最后还要感谢Prentice Hall的优秀员工(本书的编辑Mary Franz,还有Noreen Regina、Sophie Papanikolaou和Patti Guerrieri)给予的帮助,尤其是在很紧的时间内完成一切所付出的努力。
我制作了本书的最终电子版(PostScript格式),最后排版成现在的书。我用James Clark编写的优秀的groff包为本书排版,该软件包安装在一台运行Solaris 2.6的SparcStation工作站上。(认为troff已经过时的报导显然太夸张了。)我使用vi编辑器键入了所有的138 897个单词,用gpic程序绘制了72幅插图(其中用到了许多由Gary Wright编写的宏),用gtbl程序生成了35张表格,为全书添加了索引(用到了Jon Bentley与Brian Kernighan编写的一组awk脚本),并设计了最终的版式。我录入书中的8 046行C语言源代码,使用的是Dave Hanson的loom程序、GNU的indent程序和Gary Wright写的一些脚本。
欢迎读者以电子邮件的方式反馈意见、提出建议或订正错误。
W. Richard Stevens
1998年7月于亚利桑那州图森市
[1] 书中所有示例的源代码也可以从异步社区网站(https://www.epubit.com)本书网页免费注册下载。——编者注
本书由异步社区出品,社区(https://www.epubit.com/)为您提供后续服务。
本书提供源代码下载,要获得源代码,请在异步社区本书页面中点击,跳转到下载界面,按提示进行操作即可。注意:为保证购书读者的权益,该操作会给出相关提示,要求输入提取码进行验证。
作者和编辑尽最大努力来确保书中内容的准确性,但难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区,按书名搜索,进入本书页面,单击“提交勘误”,输入勘误信息,单击“提交”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
我们的联系邮箱是contact@epubit.com.cn。
如果您对本书有任何疑问或建议,请您发邮件给我们,并请在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们;有意出版图书的作者也可以到异步社区在线提交投稿(直接访问www.epubit.com/ selfpublish/submission即可)。
如果您来自学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”是人民邮电出版社旗下IT专业图书社区,致力于出版精品IT技术图书和相关学习产品,为作译者提供优质出版服务。异步社区创办于2015年8月,提供大量精品IT技术图书和电子书,以及高品质技术文章和视频课程。更多详情请访问异步社区官网https://www.epubit.com。
“异步图书”是由异步社区编辑团队策划出版的精品IT专业图书的品牌,依托于人民邮电出版社近30年的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的LOGO。异步图书的出版领域包括软件开发、大数据、AI、测试、前端、网络技术等。
异步社区
微信服务号
IPC是进程间通信(interprocess communication)的简称。传统上该术语描述的是运行在某个操作系统之上的不同进程间各种消息传递(message passing)的方式。本书还讲述多种形式的同步(synchronization),因为像共享内存区这样的较新式的通信需要某种形式的同步参与运作。
在Unix操作系统过去30年的演变史中,消息传递历经了如下几个发展阶段。
在谈论Unix进程时,有亲缘关系(related)的说法意味着所论及的进程具有某个共同的祖先。说得更明白点,这些有亲缘关系的进程是从该祖先进程经过一次或多次fork
派生来的。一个常见的例子是在某个进程调用fork
两次,派生出两个子进程。我们说这两个子进程是有亲缘关系的。同样,每个子进程与其父进程也是有亲缘关系的。考虑到IPC,父进程可以在调用fork
前建立某种形式的IPC(例如管道或消息队列),因为它知道随后派生的两个子进程将穿越fork
继承该IPC对象。我们随图1-6详细讨论各种IPC对象的继承性。我们还得注意,从理论上说,所有Unix进程与init
进程都有亲缘关系,它是在系统自举时启动所有初始化进程的祖先进程。然而从实践上说,进程亲缘关系开始于一个登录shell(称为一个会话)以及由该shell派生的所有进程。APUE的第9章详细讨论会话和进程亲缘关系。
本书将全文使用缩进的插入式注解(如此处所示)来说明实现上的细节、历史上的观点以及其他琐事。
看一看由Unix提供的各种同步形式的演变同样颇有教益。
按照传统的Unix编程模型,我们在一个系统上运行多个进程,每个进程都有各自的地址空间。Unix进程间的信息共享可以有多种方式。图1-1对此作了总结。
图1-1 Unix进程间共享信息的三种方式
(1) 左边的两个进程共享存留于文件系统中某个文件上的某些信息。为访问这些信息,每个进程都得穿越内核(例如read
、write
、lseek
等)。当一个文件有待更新时,某种形式的同步是必要的,这样既可保护多个写入者,防止相互串扰,也可保护一个或多个读出者,防止写入者的干扰。
(2) 中间的两个进程共享驻留于内核中的某些信息。管道是这种共享类型的一个例子,System V消息队列和System V信号量也是。现在访问共享信息的每次操作涉及对内核的一次系统调用。
(3) 右边的两个进程有一个双方都能访问的共享内存区。每个进程一旦设置好该共享内存区,就能根本不涉及内核而访问其中的数据。共享该内存区的进程需要某种形式的同步。
注意没有任何东西限制任何IPC技术只能使用两个进程。我们讲述的技术适用于任意数目的进程。在图1-1中只展示两个进程是为了简单起见。
虽然Unix系统中进程的概念已使用了很久,一个给定进程内多个线程(thread)的概念却相对较新。Posix.1线程标准(称为“Pthreads”)是于1995年通过的。从IPC角度看来,一个给定进程内的所有线程共享同样的全局变量(也就是说共享内存区的概念对这种模型来说是内在的)。然而我们必须关注的是各个线程间对全局数据的同步访问。同步尽管不是一种明确的IPC形式,但它确实伴随许多形式的IPC使用,以控制对某些共享数据的访问。
本书中我们讲述进程间的IPC和线程间的IPC。我们假设有一个线程环境,并作类似如下形式的陈述:“如果管道为空,调用线程就阻塞在它的read
调用上,直到某个线程往该管道写入数据。”要是你的系统不支持线程,那你可以将该句子中的“线程”替换成“进程”,从而提供“阻塞在对空管道的read
调用上”的经典Unix定义。然而在支持线程的系统上,只有对空管道调用read
的那个线程阻塞,同一进程中的其余线程才可以继续执行。向该空管道写数据的工作既可以由同一进程中的另一个线程去做,也可以由另一个进程中的某个线程去做。
附录B汇总了线程的某些特征以及全书都用到的5个基本的Pthread函数。
我们可以把任意类型的IPC的持续性(persistence)定义成该类型的一个对象一直存在多长时间。图1-2展示了三种类型的持续性。
图1-2 IPC对象的持续性
(1) 随进程持续的(process-persistent)IPC对象一直存在到打开着该对象的最后一个进程关闭该对象为止。例如管道和FIFO就是这种对象。
(2) 随内核持续的(kernel-persistent)IPC对象一直存在到内核重新自举或显式删除该对象为止。例如System V的消息队列、信号量和共享内存区就是此类对象。Posix的消息队列、信号量和共享内存区必须至少是随内核持续的,但也可以是随文件系统持续的,具体取决于实现。
(3) 随文件系统持续的(filesystem-persistent)IPC对象一直存在到显式删除该对象为止。即使内核重新自举了,该对象还是保持其值。Posix消息队列、信号量和共享内存区如果是使用映射文件实现的(不是必需条件),那么它们就是随文件系统持续的。
在定义一个IPC对象的持续性时我们必须小心,因为它并不总是像看起来的那样。例如管道内的数据是在内核中维护的,但管道具备的是随进程的持续性而不是随内核的持续性:最后一个将某个管道打开着用于读的进程关闭该管道后,内核将丢弃所有的数据并删除该管道。类似地,尽管FIFO在文件系统中有名字,它们也只是具备随进程的持续性,因为最后一个将某个FIFO打开着的进程关闭该FIFO后,FIFO中的数据都被丢弃。
图1-3汇总了将在本书中讲述的各种类型IPC对象的持续性。
IPC类型 |
持续性 |
---|---|
管道 |
随进程 |
FIFO |
随进程 |
Posix互斥锁 |
随进程 |
Posix条件变量 |
随进程 |
Posix读写锁 |
随进程 |
|
随进程 |
Posix消息队列 |
随内核 |
Posix有名信号量 |
随内核 |
Posix基于内存的信号量 |
随进程 |
Posix共享内存区 |
随内核 |
System V消息队列 |
随内核 |
System V信号量 |
随内核 |
System V共享内存区 |
随内核 |
TCP套接字 |
随进程 |
UDP套接字 |
随进程 |
Unix域套接字 |
随进程 |
图1-3 各种类型IPC对象的持续性
注意该列表中没有任何类型的IPC具备随文件系统的持续性,但是我们说过有三种类型的Posix IPC可能会具备该持续性,这取决于它们的实现。显然,向一个文件写入数据提供了随文件系统的持续性,但这通常不作为一种IPC形式使用。多数形式的IPC并没有在系统重新自举后继续存在的打算,因为进程不可能跨越重新自举继续存活。对于一种给定形式的IPC,要求它具备随文件系统的持续性可能会使其性能降级,而IPC的一个基本的设计目标是高性能。
当两个或多个无亲缘关系的进程使用某种类型的IPC对象来彼此交换信息时,该IPC对象必须有一个某种形式的名字(name)或标识符(identifier),这样其中一个进程(往往是服务器)可以创建该IPC对象,其余进程则可以指定同一个IPC对象。
管道没有名字(因此不能用于无亲缘关系的进程间),但是FIFO有一个在文件系统中的Unix路径名作为其标识符(因此可用于无亲缘关系的进程间)。在以后各章具体讲述其他形式的IPC时,我们将使用另外的命名约定。对于一种给定的IPC类型,其可能的名字的集合称为它的名字空间(name space)。名字空间非常重要,因为对于除普通管道以外的所有形式的IPC来说,名字是客户与服务器彼此连接以交换消息的手段。
图1-4汇总了不同形式的IPC所用的命名约定。
我们还指出哪些形式的IPC是由1996年版的Posix.1和Unix 98标准化的,这两个标准本身则在1.7节详细讨论。为了比较的目的,我们还包含了三种类型的套接字,它们在UNPv1中具体讲述。注意套接字API(应用程序编程接口)是由Posix.1g工作组标准化的,最终应该成为某个未来的Posix.1标准的一部分。
IPC类型 |
用于打开或创建IPC的名字空间 |
IPC打开后的标识 |
Posix.1 1996 |
Unix 98 |
---|---|---|---|---|
管道 |
(没有名字) |
描述符 |
● |
● |
FIFO |
路径名 |
描述符 |
● |
● |
Posix互斥锁 |
(没有名字) |
|
● |
● |
Posix条件变量 |
(没有名字) |
|
● |
● |
Posix读写锁 |
(没有名字) |
|
● |
|
|
路径名 |
描述符 |
● |
● |
Posix消息队列 |
Posix IPC名字 |
|
● |
● |
Posix有名信号量 |
Posix IPC名字 |
|
● |
● |
Posix基于内存的信号量 |
(没有名字) |
|
● |
● |
Posix共享内存区 |
Posix IPC名字 |
描述符 |
● |
● |
System V消息队列 |
|
System V IPC标识符 |
● |
|
System V信号量 |
|
System V IPC标识符 |
● |
|
System V共享内存区 |
|
System V IPC标识符 |
● |
|
门 |
路径名 |
描述符 |
||
Sun RPC |
程序/版本 |
RPC句柄 |
||
TCP套接字 |
IP地址与TCP端口 |
描述符 |
.1g |
● |
UDP套接字 |
IP地址与UDP端口 |
描述符 |
.1g |
● |
Unix域套接字 |
路径名 |
描述符 |
.1g |
● |
图1-4 各种形式IPC的名字空间
尽管Posix.1标准化了信号量,它们仍然是可选的特性。图1-5汇总了Posix.1和Unix 98对各种IPC特性的说明。每种特性有强制、未定义和可选三种选择。对于可选的特性,我们指出了其中每种特性受支持时(通常在<unistd.h>
头文件中)定义的常值的名字,例如_POSIX_THREADS
。注意,Unix 98是Posix.1的超集。
IPC类型 |
Posix.1 1996 |
Unix 98 |
---|---|---|
管道 |
强制 |
强制 |
FIFO |
强制 |
强制 |
Posix互斥锁 |
|
强制 |
Posix条件变量 |
|
强制 |
进程间共享的互斥锁/条件变量 |
|
强制 |
Posix读写锁 |
(未定义) |
强制 |
|
强制 |
强制 |
Posix消息队列 |
|
|
Posix信号量 |
|
|
Posix共享内存区 |
|
|
System V消息队列 |
(未定义) |
强制 |
System V信号量 |
(未定义) |
强制 |
System V共享内存区 |
(未定义) |
强制 |
门 |
(未定义) |
(未定义) |
Sun RPC |
(未定义) |
(未定义) |
|
|
强制 |
实时信号 |
|
|
图1-5 各种形式IPC的可用性
fork
、exec
和exit
对IPC对象的影响我们需要理解fork
、exec
和_exit
函数对于所讨论的各种形式的IPC的影响(_exit
是由exit
调用的一个函数)。图1-6对此作了总结。
IPC类型 |
|
|
|
---|---|---|---|
管道和FIFO |
子进程取得父进程的所有打开着的描述符的副本 |
所有打开着的描述符继续打开着,除非已设置描述符的 |
关闭所有打开着的描述符,最后一个关闭时删除管道或FIFO中残留的所有数据 |
Posix消息队列 |
子进程取得父进程的所有打开着的消息队列描述符的副本 |
关闭所有打开着的消息队列描述符 |
关闭所有打开着的消息队列描述符 |
System V消息队列 |
没有效果 |
没有效果 |
没有效果 |
Posix互斥锁和条件变量 |
若驻留在共享内存区中而且具有进程间共享属性,则共享 |
除非在继续打开着的共享内存区中而且具有进程间共享属性,否则消失 |
除非在继续打开着的共享内存区中而且具有进程间共享属性,否则消失 |
Posix读写锁 |
若驻留在共享内存区中而且具有进程间共享属性,则共享 |
除非在继续打开着的共享内存区中而且具有进程间共享属性,否则消失 |
除非在继续打开着的共享内存区中而且具有进程间共享属性,否则消失 |
Posix基于内存的信号量 |
若驻留在共享内存区中而且具有进程间共享属性,则共享 |
除非在继续打开着的共享内存区中而且具有进程间共享属性,否则消失 |
除非在继续打开着的共享内存区中而且具有进程间共享属性,否则消失 |
Posix有名信号量 |
父进程中所有打开着的有名信号量在子进程中继续打开着 |
关闭所有打开着的有名信号量 |
关闭所有打开着的有名信号量 |
System V信号量 |
子进程中所有 |
所有 |
所有 |
|
子进程不继承由父进程持有的锁 |
只要描述符继续打开着,锁就不变 |
解开由进程持有的所有未处理的锁 |
|
父进程中的内存映射存留到子进程中 |
去除内存映射 |
去除内存映射 |
Posix共享内存区 |
父进程中的内存映射存留到子进程中 |
去除内存映射 |
去除内存映射 |
System V共享内存区 |
附接着的共享内存区在子进程中继续附接着 |
断开所有附接着的共享内存区 |
断开所有附接着的共享内存区 |
门 |
子进程取得父进程的所有打开着的描述符,但是客户在门描述符上激活其过程时,只有父进程是服务器 |
所有门描述符都应关闭,因为它们创建时设置了 |
关闭所有打开着的描述符 |
图1-6 调用fork
、exec
和_exit
对于IPC的影响
表中多数特性将在以后的章节中讲述,不过我们需要强调几点。首先,考虑到无名同步变量(互斥锁、条件变量、读写锁、基于内存的信号量),从一个具有多个线程的进程中调用fork
将变得混乱不堪。[Butenhof 1997]的6.1节提供了其中的细节。我们在表中只是简单地注明:如果这些变量驻留在共享内存区中,而且创建时设置了进程间共享属性,那么对于能访问该共享内存区的任意进程来说,其任意线程能继续访问这些变量。其次,System V IPC的三种形式没有打开或关闭的说法。我们将从图6-8和习题11.1和习题14.1中看出,访问这三种形式的IPC所需知道的只是一个标识符,因此知道该标识符的任何进程都能访问它们,尽管信号量和共享内存区可附带提出某种特殊处理要求。
在现实程序中,我们必须检查每个函数调用是否返回错误。由于碰到错误时终止程序执行是个惯例,因此我们可以通过定义包裹函数(wrapper function)来缩短程序的长度。包裹函数执行实际的函数调用,测试其返回值,并在碰到错误时终止进程。我们使用的命名约定是将函数名第一个字母改为大写字母,例如:
Sem_post(ptr);
图1-7定义了这个包裹函数。
------------------------------------lib/wrapunix.c
387 void
388 Sem_post(sem_t *sem)
389 {
390 if (sem_post(sem) == -1)
391 err_sys("sem_post error");
392 }
------------------------------------lib/wrapunix.c
图1-7 sem_post
函数的包裹函数
每当你遇到一个以大写字母打头的函数名时,它就是我们所说的包裹函数。它调用一个名字相同但以相应小写字母开头的实际函数。当碰到错误时,包裹函数总是在输出一个出错消息后终止。
我们在讲解书中提供的源代码时,所指代的总是被调用的最低层函数(例如sem_post
),而不是包裹函数(例如Sem_post
)。类似地,书后的索引也总是指代被调用的最低层函数,而不是指代包裹函数。
刚刚展示的源代码格式全书都在使用。每一非空行都被编号。代码的正文说明部分的左边标有起始与结束的行号。有的段落开始处含有一个醒目的简短标题,概述本段代码的内容。
源代码片段起始与结束处的水平线标出了该片段所在源代码文件名,本例中就是lib
目录下的wrapunix.c
文件。既然本书所有例子的源代码都可免费获得(见前言),你就可以凭这个文件名找到相应的文件。阅读本书的过程中,编译、运行并修改这些程序是学习进程间通信概念的好方法。
尽管包裹函数不见得如何节省代码量,当在第7章中讨论线程时,我们会发现线程函数出错时并不设置标准的Unix errno
变量;相反,本该设置errno
的值改由线程函数作为其返回值返回调用者。这意味着我们每次调用任意一个线程函数时,都得分配一个变量来保存函数返回值,然后在调用我们的err_sys
函数(图C-4)前,把errno
设置成所保存的值。为避免源代码中到处出现花括弧,我们可以使用C语言的逗号运算符,把给errno
赋值与调用err_sys
组合成单个语句,如下所示:
int n;
if ( (n = pthread_mutex_lock(&ndone_mutex)) != 0)
errno = n, err_sys("pthread_mutex_lock error");
另一种办法是定义一个新的出错处理函数,它需要的另一个参数是系统的错误号[1]。但是我们可以将这段代码简化得更容易些:
Pthread_mutex_lock(&ndone_mutex);
其前提是定义自己的包裹函数,如图1-8所示。
------------------------------------lib/wrappthread.c
125 void
126 Pthread_mutex_lock(pthread_mutex_t *mptr)
127 {
128 int n;
129 if ( (n = pthread_mutex_lock(mptr)) == 0)
130 return;
131 errno = n;
132 err_sys("pthread_mutex_lock error");
133 }
------------------------------------lib/wrappthread.c
图1-8 给pthread_mutex_lock
定义的包裹函数
仔细推敲编码,我们可改用宏代替函数,从而稍稍提高运行效率,不过即使有过的话,包裹函数也很少是程序性能的瓶颈所在。
选择将函数名的第一个字母大写是一种较折中的方法。还有许多其他方法:例如用e
作为函数名的前缀(如[Kernighan and Pike 1984]第184页所示),或者用_e
作为函数名的后缀等。同样提供确实在调用某个其他函数的可视化指示,我们的方法看来是最少分散人们的注意力的。
这种技巧还有助于检查那些其错误返回值通常被忽略的函数,例如close
和pthread_ mutex_lock
。
本书后面的例子中我们将普遍使用包裹函数,除非必须检查某个确定的错误并处理它(而不是终止进程)。我们并不给出所有包裹函数的源代码,但它们是免费可得的(见前言)。
errno
值每当在一个Unix函数中发生错误时,全局变量errno
将被设置成一个指示错误类型的正数,函数本身则通常返回-1。我们的err_sys
函数检查errno
的值并输出相应的出错消息,例如,errno
的值等于EAGAIN
时的出错消息为“Resource temporarily unavailable”(资源暂时不可用)。
errno
的值只在某个函数发生错误时设置。如果该函数不返回错误,errno
的值就无定义。所有正的错误值都是常值,具有以E
打头的全部为大写字母的名字,通常定义在头文件<sys/errno.h>
中。没有值为0的错误。
在多线程环境中,每个线程必须有自己的errno
变量。提供一个局限于线程的errno
变量的隐式请求是自动处理的,不过通常需要告诉编译器所编译的程序是可重入的。给编译器指定类似-D_REENTRANT
或-D_POSIX_C_SOURCE=199506L
这样的命令行选项是较典型的方法。<errno.h>
头文件往往把errno定义成一个宏,当常值_REENTRANT
有定义时,该宏就扩展成一个函数,由它访问errno
变量的某个局限于线程的副本。
全书使用类似“mq_send
函数返回EMSGSIZE
错误”的用语来简略地表示这样的意思:该函数返回一个错误(典型情况是返回值为-1
),并且在errno
中设置了指定的常值。
有关Unix标准化的大多数活动是由Posix和Open Group做的。
Posix是“可移植操作系统接口”(Portable Operating System Interface)的首字母缩写。它并不是一个单一标准,而是一个由电气与电子工程师学会(即IEEE)开发的一系列标准。Posix标准还是由ISO(国际标准化组织)和IEC(国际电工委员会)采纳的国际标准,这两个组织合称为ISO/IEC。Posix标准经历了以下若干代。
fork
、exec
、信号、定时器)、进程环境(用户ID、进程组)、文件与目录(所有I/O函数)、终端I/O、系统数据库(口令文件和用户组文件)、tar
与cpio
归档格式。第一个Posix标准是出现于1986年称为“IEEEIX”的试用版本。Posix这个名字是由Richard Stallman建议使用的。
awk
、basename
、vi
和yacc
等)。本书称这个标准为Posix.2。mmap
和共享内存区)、执行调度、时钟与定时器、消息队列。743页中有四分之一强的篇幅是标题为“Rationale and Notes”(原理与注解)的附录。这些原理含有历史性信息以及某些特性必须加入或删除的理由,它们通常跟正式标准一样有教益。
遗憾的是IEEE标准在因特网上不是免费可得的。其订购信息在[IEEE 1996]的参考文献说明中给出。
注意信号量在实时标准中定义,它与在Pthreads标准中定义的互斥锁和条件变量相分离,这足以解释它们的API中存在的某些差异。
最后注意读写锁(尚)不属于任何Posix标准。我们将在笫8章中详细讨论。
将来某个时候印制的新版本的IEEE Std 1003.1应包括P1003.lg标准,它是我们在UNPvl中讲述的网络编程API(套接字和XTI)。
1996年版的Posix.1标准的前言中声称ISO/IEC 9945由下面三个部分构成。
第一部分和第二部分就是我们所称的Posix.1和Posix.2。
Posix标准化工作仍将继续,任何论述到它的书籍都在跟踪这项工作。
Open Group是由X/Open公司(1984年成立)和开放软件基金会(OSF,1988年成立)于1996年合并而成的组织。它是由厂家、业界最终用户、政府部门和学术机构组成的国际组织。它们的标准经历了以下若干代。
http://www.UNIX-systems.org/version2
。单一Unix规范的许多文档可在因特网上从这个站点免费取得。
当今大多数Unix系统符合Posix.1和Posix.2的某个版本。我们使用限定词“某个”是因为Posix每次更新(例如1993年增加实时扩展,1996年增加Pthreads内容),厂家都得花一两年(甚至更长的时间)去实现并加入最近的更新内容。
从历史上看,多数Unix系统或者源自Berkeley,或者源自System V,不过这些差别在慢慢消失,因为大多数厂家已开始采用Posix标准。仍然存在的主要差别在于系统管理,这是一个目前还没有任何Posix标准可循的领域。
运行本书中大多数例子的平台是Solaris 2.6和Digital Unix 4.0B。其原因在于写到此处时(1997年末到1998年初),只有这两种Unix系统支持System V IPC、Posix IPC及Posix线程。
为分析各种特性,全书主要使用了三种交互模式。
(1) 文件服务器:客户-服务器应用程序,客户向服务器发送一个路径名,服务器把该文件的内容返回给客户。
(2) 生产者-消费者:一个或多个线程或进程(生产者)把数据放到一个共享缓冲区中,另有一个或多个线程或进程(消费者)对该共享缓冲区中的数据进行操作。
(3) 序列号持续增1:一个或多个线程或进程给一个共享的序列号持续增1。该序列号有时在一个共享文件中,有时在共享内存区中。
第一个例子分析各种形式的消息传递,另外两个例子则分析各种类型的同步和共享内存区。
为了提供本书所涵盖的不同主题的索引,图1-9、图1-10和图1-11汇总了我们开发的程序及它们的源代码所在的起始图号和页码。
图 号 |
说 明 |
---|---|
4-8 |
使用两个管道,父子进程间 |
4-15 |
使用 |
4-16 |
使用两个FIFO,父子进程间 |
4-18 |
使用两个FIFO,独立的服务器,与服务器无亲缘关系的客户 |
4-23 |
使用多个FIFO,独立的迭代服务器,多个客户 |
4-25 |
使用管道或FIFO:在字节流上构筑记录 |
6-9 |
使用两个System V消息队列 |
6-15 |
使用一个System V消息队列,多个客户 |
6-20 |
每个客户使用一个System V消息队列,多个客户 |
15-18 |
使用穿越门的描述符传递 |
图1-9 不同版本的文件服务器客户-服务器例子
图 号 |
说 明 |
---|---|
7-2 |
只用互斥锁,多个生产者,单个消费者 |
7-6 |
互斥锁和条件变量,多个生产者,单个消费者 |
10-17 |
Posix有名信号量,单个生产者,单个消费者 |
10-20 |
Posix基于内存的信号量,单个生产者,单个消费者 |
10-21 |
Posix基于内存的信号量,多个生产者,单个消费者 |
10-24 |
Posix基于内存的信号量,多个生产者,多个消费者 |
10-33 |
Posix基于内存的信号量,单个生产者,单个消费者,多个缓冲区 |
图1-10 不同版本的生产者-消费者例子
图 号 |
说 明 |
---|---|
9-1 |
序列号在文件中,不上锁 |
9-3 |
序列号在文件中, |
9-12 |
序列号在文件中,使用 |
10-19 |
序列号在文件中,Posix有名信号量上锁 |
12-10 |
序列号在 |
12-12 |
序列号在 |
12-14 |
序列号在4.4BSD匿名共享内存区,Posix有名信号量上锁 |
12-15 |
序列号在SVR4 |
13-7 |
序列号在Posix共享内存区,Posix基于内存的信号量上锁 |
A-34 |
性能测量:线程间互斥锁上锁 |
A-36 |
性能测量:线程间读写锁上锁 |
A-39 |
性能测量:线程间Posix基于内存的信号量上锁 |
A-41 |
性能测量:线程间Posix有名信号量上锁 |
A-42 |
性能测量:线程间System V信号量上锁 |
A-45 |
性能测量:线程间 |
A-48 |
性能测量:线程间互斥锁上锁 |
图1-11 不同版本的序列号持续增1例子
IPC传统上是Unix中一个杂乱不堪的领域。虽然有了各种各样的解决办法,但没有一个是完美的。我们的讨论分成4个主要领域:
(1) 消息传递(管道、FIFO、消息队列);
(2) 同步(互斥锁、条件变量、读写锁、信号量);
(3) 共享内存区(匿名共享内存区、有名共享内存区);
(4) 过程调用(Solaris门、Sun RPC)。
我们考虑单个进程中多个线程间的IPC以及多个进程间的IPC。
各种类型IPC的持续性可以是随进程持续的、随内核持续的或随文件系统持续的,这取决于IPC对象存在时间的长短。在为给定的应用选择所用的IPC类型时,我们必须清楚相应IPC对象的持续性。
各种类型IPC的另一个特性是名字空间,也就是使用IPC对象的进程和线程标识各个IPC对象的方式。各种类型的IPC有些没有名字(管道、互斥锁、条件变量、读写锁),有些具有在文件系统中的名字(FIFO),有些具有将在第2章中讲述的Posix IPC名字,有些则具有其他类型的名字(将在第3章中讲述的System V IPC键或标识符)。典型做法是:服务器以某个名字创建一个IPC对象,客户则使用该名字访问同一个IPC对象。
本书中所有源代码使用1.6节中讲述的包裹函数来缩短篇幅,同时达到检查每个函数调用是否返回错误的目的。我们的包裹函数都以一个大写字母开头。
IEEE Posix标准一直是多数厂家努力遵循的标准,其中Posix.1定义了访问Unix的基本C接口,Posix.2定义了标准命令。然而商业标准也在迅速地吸纳并扩展Posix标准,著名的有Open Group的Unix标准,例如Unix 98。
1.1 图1-1中我们展示了两个进程访问单个文件的情形。如果这两个进程都只是往该文件的末尾添加新的数据(譬如说这是一个日志文件),那么需要什么类型的同步?
1.2 查看一下你的系统中的<error.h>
头文件是如何定义errno
变量的。
1.3 在图1-5上标注出你使用的Unix系统所支持的特性。
[1] 意思是用新的出错处理函数代替原来的err_sys
,这样对线程函数的调用可直接作为新的出错处理函数中增设的参数用,即err_sys_new("pthread_mutex_lock error", pthread_mutex_lock(&ndone_mutex))
。——译者注
以下三种类型的IPC合称为“Posix IPC”:
Posix IPC在访问它们的函数和描述它们的信息上有一些类似点。本章讲述所有这些共同属性:用于标识的路径名、打开或创建时指定的标志以及访问权限。
图2-1汇总了所有Posix IPC函数。
消息队列 | 信号量 | 共享内存区 | |
---|---|---|---|
头文件 | <mqueue.h> |
<semaphore.h> |
<sys/mman.h> |
创建、打开或删除IPC的函数 | mq_open mq_close mq_unlink |
sem_open sem_close sem_unlink |
shm_open shm_unlink |
sem_init sem_destroy |
|||
控制IPC操作的函数 | mq_getattr mq_setattr |
ftruncate fstat |
|
IPC操作函数 | mq_send mq_receive mq_notify |
sem_wait sem_trywait sem_post sem_getvalue |
mmap munmap |
图2-1 Posix IPC函数汇总
在图1-4中我们指出,三种类型的Posix IPC都使用“Posix IPC名字”进行标识。mq_open
、sem_open
和shm_open
这三个函数的第一个参数就是这样的一个名字,它可能是某个文件系统中的一个真正的路径名,也可能不是。Posix.1是这么描述Posix IPC名字的。
PATH_MAX
字节构成,包括结尾的空字节)。因此,为便于移植起见,Posix IPC名字必须以一个斜杠符打头,并且不能再含有任何其他斜杠符。遗憾的是这些规则还不够,仍会出现移植性问题。
Solaris 2.6要求有打头的斜杠符,但是不允许有另外的斜杠符。假设要创建的是一个消息队列,创建函数将在/tmp
中创建三个以.MQ
开头的文件。例如,如果给mq_open
的参数为/queue.1234
,那么这三个文件分别为/tmp/.MQDqueue.1234
、/tmp/.MQLqueue.1234
和/tmp/.MQPqueue.1234
。Digital Unix 4.0B则在文件系统中创建所指定的路径名。
当我们指定一个只有单个斜杠符(作为首字符)的名字时,移植性问题就发生了:我们必须在根目录中具有写权限。例如,/tmp.1234
符合Posix规则,在Solaris下也可行,但是Digital Unix却会试图创建这个文件,这时除非我们有在根目录中的写权限,否则这样的尝试将失败。如果我们指定一个/tmp/test.1234
这样的名字,那么在以该名字创建一个真正文件的所有系统上都将成功(前提是/tmp
目录存在,而且我们在该目录中有写权限,对于多数Unix系统来说,这是正常情况),在Solaris下则失败。
为避免这些移植性问题,我们应该把Posix IPC名字的#define
行放在一个便于修改的头文件中,这样应用程序转移到另一个系统上时,只需修改这个头文件。
这是一个标准试图变得相当通用(本例子中,实时标准试图允许消息队列、信号量和共享内存区都在现有的Unix内核中实现,而且在独立的无盘系统上也能工作),结果标准的具体实现却变得不可移植的个例之一。在Posix中,这种现象称为“造成不标准的标准方式”(a standard way of being nonstandard)。
Posix.1定义了三个宏:
S_TYPEISMQ(buf)
S_TYPEISSEM(buf)
S_TYPEISSHM(buf)
它们的单个参数是指向某个stat
结构的指针,其内容由fstat
、lstat
或stat
这三个函数填入。如果所指定的IPC对象(消息队列、信号量或共享内存区对象)是作为一种独特的文件类型实现的,而且参数所指向的stat
结构访问这样的文件类型,那么这三个宏计算出一个非零值。否则,计算出的值为0。
不幸的是,这三个宏没有多大用处,因为无法保证这三种类型的IPC使用一种独特的文件类型实现。举例来说,在Solaris 2.6下,这三个宏的计算结果总是0。
测试某个文件是否为给定文件类型的所有其他宏的名字都以S_IS
开头,而且它们的单个参数是某个stat
结构的st_mode
成员。由于上面三个新宏的参数不同于其他宏,因此它们的名字改为以S_TYPEIS
开头。
px_ipc_name
函数解决上述移植性问题的另一种办法是自己定义一个名为px_ipc_name
的函数,它为定位Posix IPC名字而添加上正确的前缀目录。
#include "unpipc.h"
char *px_ipc_name(const char *name);
均返回:若成功则为非空指针,若出错则为NULL
本书中我们给自己定义的非标准系统函数都使用这样的版式:围绕函数原型和返回值的方框是虚框。开头包含的头文件通常是我们的unpipc.h
(图C-l)。
name参数中不能有任何斜杠符。例如,调用
px_ipc_name("test1")
在Solaris 2.6下返回一个指向字符串/testl
的指针,在Digital Unix 4.0B下返回一个指向字符串/tmp/test1
的指针。存放结果字符串的内存空间是动态分配的,并可通过调用free
释放。另外,环境变量PX_IPC_NAME
能够覆盖默认目录。
图2-2给出了该函数的实现。
------------------------------------lib/px_ipc_name.c
1 #include "unpipc.h"
2 char *
3 px_ipc_name(const char *name)
4 {
5 char *dir, *dst, *slash;
6 if ( (dst = malloc(PATH_MAX)) == NULL)
7 return(NULL);
8 /* can override default directory with environment variable */
9 if ( (dir = getenv("PX_IPC_NAME")) == NULL) {
10 #ifdef POSIX_IPC_PREFIX
11 dir = POSIX_IPC_PREFIX; /* from "config.h" */
12 #else
13 dir = "/tmp/"; /* default */
14 #endif
15 }
16 /* dir must end in a slash */
17 slash = (dir[strlen(dir) - 1] == '/') ? "" : "/";
18 snprintf(dst, PATH_MAX, "%s%s%s", dir, slash, name);
19 return(dst); /* caller can free() this pointer */
20 }
------------------------------------lib/px_ipc_name.c
图2-2 我们的px_ipc_name
函数
这也许是你第一次碰到snprintf
函数。许多现有代码调用的是sprintf
,但是sprintf
不检查目标缓冲区是否溢出,不过snprintf
要求其第二个参数是目标缓冲区的大小,因此可确保缓冲区不溢出。提供能有意溢出一个程序的sprintf
缓冲区的输入数据是黑客们已使用很多年的一种攻破系统的方法。
snprintf
不是标准ANSI C的一部分,但这个标准的修订版C9X正在考虑。[1]不过,许多厂家提供的标准C函数库含有这个函数。我们在本书中使用snprintf
,如果你的系统不提供这个函数,那就使用我们自己的通过调用sprintf
实现的版本。
mq_open
、sem_open
和shm_open
这三个创建或打开一个IPC对象的函数,它们的名为oflag的第二个参数指定怎样打开所请求的对象。这与标准open
函数的第二个参数类似。图2-3给出了可组合构成该参数的各种常值。
前3行指定怎样打开对象:只读、只写或读写。消息队列能以其中任何一种模式打开,信号量的打开不指定任何模式(任意信号量操作,都需要读写访问权),共享内存区对象则不能以只写模式打开。
说 明 |
|
|
|
---|---|---|---|
只读 |
|
|
|
只写 |
|
||
读写 |
|
|
|
若不存在则创建 |
|
|
|
排他性创建 |
|
|
|
非阻塞模式 |
|
||
若已存在则截短 |
|
图2-3 打开或创建Posix IPC对象所用的各种oflag常值
图2-3中余下4行标志是可选的。
O_CREAT
若不存在则创建由函数第一个参数所指定名字的消息队列、信号量或共享内存区对象(同时检查O_EXCL
标志,我们不久将要说明)。
创建一个新的消息队列、信号量或共享内存区对象时,至少需要另外一个称为mode的参数。该参数指定权限位,它是由图2-4中所示常值按位或形成的。
常 值 |
说 明 |
---|---|
|
用户(属主)读 |
|
用户(属主)写 |
|
(属)组成员读 |
|
(属)组成员写 |
|
其他用户读 |
|
其他用户写 |
图2-4 创建新的IPC对象所用的mode常值
这些常值定义在<sys/stat.h>
头文件中。所指定的权限位受当前进程的文件模式创建掩码(file mode creation mask)修正,而该掩码可通过调用umask
函数(APUE第83~85页[2])或使用shell的umask
命令来设置。
跟新创建的文件一样,当创建一个新的消息队列、信号量或共享内存区对象时,其用户ID被置为当前进程的有效用户ID。信号量或共享内存区对象的组ID被置为当前进程的有效组ID或某个系统默认组ID。新消息队列对象的组ID则被置为当前进程的有效组ID(APUE第77~78页[3]详细讨论了用户ID和组ID。)
这三种Posix IPC类型在设置组ID上存在的差异多少有点奇怪。由open
新创建的文件的组ID或者是当前进程的有效组ID,或者是该文件所在目录的组ID,但是IPC函数不能假定系统为IPC对象创建了一个在文件系统中的路径名。
O_EXCL
如果该标志和O_CREAT
一起指定,那么IPC函数只在所指定名字的消息队列、信号量或共享内存区对象不存在时才创建新的对象。如果该对象已经存在,而且指定了O_CREAT|O_EXCL
,那么返回一个EEXIST
错误。
考虑到其他进程的存在,检查所指定名字的消息队列、信号量或共享内存区对象的存在与否和创建它(如果它不存在)这两步必须是原子的(atomic)。我们将在3.4节看到适用于System V IPC的两个类似标志。
O_NONBLOCK
该标志使得一个消息队列在队列为空时的读或队列填满时的写不被阻塞。我们将在5.4节随mq_receive
和mq_send
这两个函数详细讨论该标志。
O_TRUNC
如果以读写模式打开了一个已存在的共享内存区对象,那么该标志将使得该对象的长度被截成0。
图2-5展示了打开一个IPC对象的真正逻辑流程。我们将在2.4节通过访问权限的测试说明该图。图2-6是展示图2-5中逻辑的另一种形式。
图2-5 打开或创建一个IPC对象的逻辑
oflag标志 |
对象不存在 |
对象已存在 |
---|---|---|
无特殊标志 |
出错, |
成功,引用已存在对象 |
|
成功,创建新对象 |
成功,引用已存在对象 |
|
成功,创建新对象 |
出错, |
图2-6 创建或打开一个IPC对象的逻辑
注意图2-6指定了O_CREAT
标志但没有指定O_EXCL
标志的中间那行,我们无法得到一个指示以判别是创建了一个新对象,还是在引用一个已存在的对象。
新的消息队列、有名信号量或共享内存区对象是由其oflag参数中含有O_CREAT
标志的mq_open
、sem_open
或shm_open
函数创建的。如图2-4所示,权限位与这些IPC类型的每个对象相关联,就像它们与每个Unix文件相关联一样。
当同样由这三个函数打开一个已存在的消息队列、信号量或共享内存区对象时(或者未指定O_CREAT
,或者指定了O_CREAT
但没有指定O_EXCL
,同时对象已经存在),将基于如下信息执行权限测试:
(1) 创建时赋予该IPC对象的权限位;
(2) 所请求的访问类型(O_RDONLY
、O_WRONLY
或O_RDWR
);
(3) 调用进程的有效用户ID、有效组ID以及各个辅助组ID(若支持的话)。
大多数Unix内核按如下步骤执行权限测试。
(1) 如果当前进程的有效用户ID为0(超级用户),那就允许访问。
(2) 在当前进程的有效用户ID等于该IPC对象的属主ID的前提下,如果相应的用户访问权限位已设置,那就允许访问,否则拒绝访问。
这里相应的访问权限位的意思是:如果当前进程为读访问而打开该IPC对象,那么用户读权限位必须设置;如果当前进程为写访问而打开该IPC对象,那么用户写权限位必须设置。
(3) 在当前进程的有效组ID或它的某个辅助组ID等于该IPC对象的组ID的前提下,如果相应的组访问权限位已设置,那就允许访问,否则拒绝访问。
(4) 如果相应的其他用户访问权限位已设置,那就允许访问,否则拒绝访问。
这4个步骤是按所列的顺序尝试的。因此,如果当前进程拥有该IPC对象(第2步),那么访问权的授予与拒绝只依赖于用户访问权限——组访问权限绝不会考虑。类似地,如果当前进程不拥有该IPC对象,但它属于某个合适的组,那么访问权的授予与拒绝只依赖于组访问权限——其他用户访问权限绝不会考虑。
我们从图2-3中指出,sem_open
不使用O_RDONLY
、O_WRONLY
或O_RDWR
标志。然而在10.2节我们将指出,某些Unix实现采用O_RDWR
,因为只要使用一个信号量,都涉及读写该信号量的值。
三种类型的Posix IPC——消息队列、信号量、共享内存区——都是用路径名标识的。但是这些路径名既可以是文件系统中的实际路径名,也可以不是,而这点不一致性会导致一个移植性问题。全书采用的解决办法是使用我们自己的px_ipc_name
函数。
当创建或打开一个IPC对象时,我们指定一组类似于open
函数所用的标志。创建一个新的IPC对象时,我们必须给这个新对象指定访问权限,所用的是同样由open
函数使用的S_
xxx常值(见图2-4)。当打开一个已存在的IPC对象时,所执行的权限测试与打开一个已存在的文件时一样。
2.1 使用Posix IPC的程序,其SUID与SGID位(APUE的4.4节)是如何影响2.4节中所述的权限测试的?
2.2 当一个程序打开一个Posix IPC对象时,它怎样才能判定是创建了一个新对象还是在引用一个已有的对象?
[1] snprinf
现在已经是C99标准的函数。——编者注
[2] 此处为APUE第1版英文原版书页码,第2版为第97~100页,第2版中文版为第80~82页。——编者注
[3] 同样为第1版英文原版书中页码,第2版为第16~17页,第2版中文版为第12~13页。——编者注