UNIX网络编程 卷2:进程间通信(第2版)

978-7-115-36720-4
作者: 【美】W. Richard Stevens
译者: 无
编辑: 杨海玲

图书目录:

详情

本书是一部UNIX 网络编程的经典之作!进程间通信(IPC)几乎是所有Unix程序性能的关键,理解IPC也是理解如何开发不同主机间网络应用程序的必要条件。本书从对Posix IPC 和System V IPC的内部结构开始讨论,全面深入地介绍了4 种IPC形式:消息传递、同步、共享内存及远程过程调用。本书内容详尽且具权威性,几乎每章都提供精选的习题,并提供了部分习题的答案。

图书摘要

本书是一部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, Second Edition, 9780130810816 by W. Richard Stevens, published by Pearson Education, Inc., publishing as Prentice Hall PTR, 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 PEARSON EDUCATION ASIA LTD. And POSTS & TELECOM PRESS Copyright © 2015.

本书中文简体字版由Pearson Education Asia Ltd.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有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本书在本书中简记如下:

在一本以“网络编程”为书名一部分的书中讨论IPC看似有点奇怪,但事实上IPC经常用于网络应用程序。我在《UNIX网络编程》1990年版的前言里就指出:“想知道如何为网络开发软件,必须先理解进程间通信(IPC)。”

本书完全重写并扩充了1990年版《UNIX网络编程》的第3章和第18章。字数统计表明,现在的内容是第1版的5倍。新版的主要改动归纳如下。

本书既可以用作IPC的教程,也可以用作有经验的程序员的参考书。全书划分为以下4个主要部分:

但许多读者可能只对特定的部分感兴趣。第2章总结了所有Posix IPC函数共有的特性,第3章归纳了所有System V IPC函数共有的特性,第12章介绍了Posix和System V的共享内存,但书中多数章节都可以独立于其他章节阅读。所有读者都应该阅读第1章,尤其是1.6节,该节介绍了一些贯穿全书的包装函数。讨论Posix IPC的各章与讨论System V IPC的各章彼此独立,有关管道、FIFO和记录锁的几章不属于上述两个阵营,关于RPC的两章也独立于其他IPC方法。

为了方便读者把本书作为参考书,本书提供了完整的全文索引,并在最后几页总结了每个函数和结构的详细描述在正文中的哪里可以找到。为了给不按顺序阅读本书的读者提供方便,我们在书中为各个主题提供了大量的交叉引用。

书中所有示例的源代码可以从作者主页(列在前言的最后)获得。学习本书讲述的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月于亚利桑那州图森市

http://www.kohala.com/~rstevens

 书中所有示例的源代码也可以从异步社区网站(https://www.epubit.com)本书网页免费注册下载。——编者注.



IPC是进程间通信(interprocess communication)的简称。传统上该术语描述的是运行在某个操作系统之上的不同进程间各种消息传递(message passing)的方式。本书还讲述多种形式的同步(synchronization),因为像共享内存区这样的较新式的通信需要某种形式的同步参与运作。

在Unix操作系统过去30年的演变史中,消息传递历经了如下几个发展阶段。

3

在谈论Unix进程时,有亲缘关系(related)的说法意味着所论及的进程具有某个共同的祖先。说得更明白点,这些有亲缘关系的进程是从该祖先进程经过一次或多次fork派生来的。一个常见的例子是在某个进程调用fork两次,派生出两个子进程。我们说这两个子进程是有亲缘关系的。同样,每个子进程与其父进程也是有亲缘关系的。考虑到IPC,父进程可以在调用fork前建立某种形式的IPC(例如管道或消息队列),因为它知道随后派生的两个子进程将穿越fork继承该IPC对象。我们随图1-6详细讨论各种IPC对象的继承性。我们还得注意,从理论上说,所有Unix进程与init进程都有亲缘关系,它是在系统自举时启动所有初始化进程的祖先进程。然而从实践上说,进程亲缘关系开始于一个登录shell(称为一个会话)以及由该shell派生的所有进程。APUE的第9章详细讨论会话和进程亲缘关系。 本书将全文使用缩进的插入式注解(如此处所示)来说明实现上的细节、历史上的观点以及其他琐事。

看一看由Unix提供的各种同步形式的演变同样颇有教益。

4

按照传统的Unix编程模型,我们在一个系统上运行多个进程,每个进程都有各自的地址空间。Unix进程间的信息共享可以有多种方式。图1-1对此作了总结。

图1-1 Unix进程间共享信息的三种方式

(1)左边的两个进程共享存留于文件系统中某个文件上的某些信息。为访问这些信息,每个进程都得穿越内核(例如readwritelseek等)。当一个文件有待更新时,某种形式的同步是必要的,这样既可保护多个写入者,防止相互串扰,也可保护一个或多个读出者,防止写入者的干扰。

(2)中间的两个进程共享驻留于内核中的某些信息。管道是这种共享类型的一个例子,System V消息队列和System V信号量也是。现在访问共享信息的每次操作涉及对内核的一次系统调用。

(3)右边的两个进程有一个双方都能访问的共享内存区。每个进程一旦设置好该共享内存区,就能根本不涉及内核而访问其中的数据。共享该内存区的进程需要某种形式的同步。

注意没有任何东西限制任何IPC技术只能使用两个进程。我们讲述的技术适用于任意数目的进程。在图1-1中只展示两个进程是为了简单起见。

5

虽然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的消息队列、信号量和共享内存区必须至少是随内核持续的,但也可以是随文件系统持续的,具体取决于实现。

6

(3)随文件系统持续的(filesystem-persistent)IPC对象一直存在到显式删除该对象为止。即使内核重新自举了,该对象还是保持其值。Posix消息队列、信号量和共享内存区如果是使用映射文件实现的(不是必需条件),那么它们就是随文件系统持续的。

在定义一个IPC对象的持续性时我们必须小心,因为它并不总是像看起来的那样。例如管道内的数据是在内核中维护的,但管道具备的是随进程的持续性而不是随内核的持续性:最后一个将某个管道打开着用于读的进程关闭该管道后,内核将丢弃所有的数据并删除该管道。类似地,尽管FIFO在文件系统中有名字,它们也只是具备随进程的持续性,因为最后一个将某个FIFO打开着的进程关闭该FIFO后,FIFO中的数据都被丢弃。

图1-3汇总了将在本书中讲述的各种类型IPC对象的持续性。

图1-3 各种类型IPC对象的持续性

注意该列表中没有任何类型的IPC具备随文件系统的持续性,但是我们说过有三种类型的Posix IPC可能会具备该持续性,这取决于它们的实现。显然,向一个文件写入数据提供了随文件系统的持续性,但这通常不作为一种IPC形式使用。多数形式的IPC并没有在系统重新自举后继续存在的打算,因为进程不可能跨越重新自举继续存活。对于一种给定形式的IPC,要求它具备随文件系统的持续性可能会使其性能降级,而IPC的一个基本的设计目标是高性能。

7

当两个或多个无亲缘关系的进程使用某种类型的IPC对象来彼此交换信息时,该IPC对象必须有一个某种形式的名字(name)或标识符(identifier),这样其中一个进程(往往是服务器)可以创建该IPC对象,其余进程则可以指定同一个IPC对象。

管道没有名字(因此不能用于无亲缘关系的进程间),但是FIFO有一个在文件系统中的Unix路径名作为其标识符(因此可用于无亲缘关系的进程间)。在以后各章具体讲述其他形式的IPC时,我们将使用另外的命名约定。对于一种给定的IPC类型,其可能的名字的集合称为它的名字空间(name space)。名字空间非常重要,因为对于除普通管道以外的所有形式的IPC来说,名字是客户与服务器彼此连接以交换消息的手段。

图1-4汇总了不同形式的IPC所用的命名约定。

图1-4 各种形式IPC的名字空间

我们还指出哪些形式的IPC是由1996年版的Posix.1和Unix 98标准化的,这两个标准本身则在1.7节详细讨论。为了比较的目的,我们还包含了3种类型的套接字,它们在UNPv1中具体讲述。注意套接字API(应用程序编程接口)是由Posix工作组标准化的,最终应该成为某个未来的Posix.1标准的一部分。

尽管Posix.1标准化了信号量,它们仍然是可选的特性。图1-5汇总了Posix.1和Unix 98对各种IPC特性的说明。每种特性有强制、未定义和可选三种选择。对于可选的特性,我们指出了其中每种特性受支持时(通常在<unistd.h>头文件中)定义的常值的名字,例如_POSIX_THREADS。注意,Unix 98是Posix.1的超集。

8

图1-5 各种形式IPC的可用性

我们需要理解forkexec_exit函数对于所讨论的各种形式的IPC的影响(_exit是由exit调用的一个函数)。图1-6对此作了总结。

图1-6 调用forkexec_exit对于IPC的影响

表中多数特性将在以后的章节中讲述,不过我们需要强调几点。首先,考虑到无名同步变量(互斥锁、条件变量、读写锁、基于内存的信号量),从一个具有多个线程的进程中调用fork将变得混乱不堪。[Butenhof 1997]的6.1节提供了其中的细节。我们在表中只是简单地注明:如果这些变量驻留在共享内存区中,而且创建时设置了进程间共享属性,那么对于能访问该共享内存区的任意进程来说,其任意线程能继续访问这些变量。其次,System V IPC的三种形式没有打开或关闭的说法。我们将从图6-8和习题11.1和习题14.1中看出,访问这三种形式的IPC所需知道的只是一个标识符,因此知道该标识符的任何进程都能访问它们,尽管信号量和共享内存区可附带提出某种特殊处理要求。

9~10

在现实程序中,我们必须检查每个函数调用是否返回错误。由于碰到错误时终止程序执行是个惯例,因此我们可以通过定义包裹函数(wrapper function)来缩短程序的长度。包裹函数执行实际的函数调用,测试其返回值,并在碰到错误时终止进程。我们使用的命名约定是将函数名第一个字母改为大写字母,例如:

Sem_post(ptr);

图1-7定义了这个包裹函数。

图1-7 sem_post函数的包裹函数

每当你遇到一个以大写字母打头的函数名时,它就是我们所说的包裹函数。它调用一个名字相同但以相应小写字母开头的实际函数。当碰到错误时,包裹函数总是在输出一个出错消息后终止。

我们在讲解书中提供的源代码时,所指代的总是被调用的最低层函数(例如sem_post),而不是包裹函数(例如Sem_post)。类似地,书后的索引也总是指代被调用的最低层函数,而不是指代包裹函数。

刚刚展示的源代码格式全书都在使用。每一非空行都被编号。代码的正文说明部分的左边标有起始与结束的行号。有的段落开始处含有一个醒目的简短标题,概述本段代码的内容。

源代码片段起始与结束处的水平线标出了该片段所在源代码文件名,本例中就是lib目录下的wrapunix.c文件。既然本书所有例子的源代码都可免费获得(见前言),你就可以凭这个文件名找到相应的文件。阅读本书的过程中,编译、运行并修改这些程序是学习进程间通信概念的好方法。

尽管包裹函数不见得如何节省代码量,当在第7章中讨论线程时,我们会发现线程函数出错时并不设置标准的Unix errno变量;相反,本该设置errno的值改由线程函数作为其返回值返回调用者。这意味着我们每次调用任意一个线程函数时,都得分配一个变量来保存函数返回值,然后在调用我们的err_sys函数(图C-4)前,把errno设置成所保存的值。为避免源代码中到处出现花括弧,我们可以使用C语言的逗号运算符,把给errno赋值与调用err_sys组合成单个语句,如下所示:

11
int n; 

if ( (n = pthread_mutex_lock(&ndone_mutex)) != 0)
    errno = n, err_sys("pthread_mutex_lock error");

另一种办法是定义一个新的出错处理函数,它需要的另一个参数是系统的错误号。但是我们可以将这段代码简化得更容易些:

Pthread_mutex_lock(&ndone_mutex);

其前提是定义自己的包裹函数,如图1-8所示。

图1-8 给pthread_mutex_lock定义的包裹函数

仔细推敲编码,我们可改用宏代替函数,从而稍稍提高运行效率,不过即使有过的话,包裹函数也很少是程序性能的瓶颈所在。

选择将函数名的第一个字母大写是一种较折中的方法。还有许多其他方法:例如用e作为函数名的前缀(如[Kernighan and Pike 1984]第184页所示),或者用_e作为函数名的后缀等。同样提供确实在调用某个其他函数的可视化指示,我们的方法看来是最少分散人们的注意力的。

这种技巧还有助于检查那些其错误返回值通常被忽略的函数,例如closepthread_mutex_lock

本书后面的例子中我们将普遍使用包裹函数,除非必须检查某个确定的错误并处理它(而不是终止进程)。我们并不给出所有包裹函数的源代码,但它们是免费可得的(见前言)。

每当在一个Unix函数中发生错误时,全局变量errno将被设置成一个指示错误类型的正数,函数本身则通常返回-1。我们的err_sys函数检查errno的值并输出相应的出错消息,例如,errno的值等于EAGAIN时的出错消息为“Resource temporarily unavailable”(资源暂时不可用)。

errno的值只在某个函数发生错误时设置。如果该函数不返回错误,errno的值就无定义。所有正的错误值都是常值,具有以E打头的全部为大写字母的名字,通常定义在头文件<sys/errno.h>中。没有值为0的错误。

12

在多线程环境中,每个线程必须有自己的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标准经历了以下若干代。

第一个Posix标准是出现于1986年称为“IEEEIX”的试用版本。Posix这个名字是由Richard Stallman建议使用的。

743页中有四分之一强的篇幅是标题为“Rationale and Notes”(原理与注解)的附录。这些原理含有历史性信息以及某些特性必须加入或删除的理由,它们通常跟正式标准一样有教益。
遗憾的是IEEE标准在因特网上不是免费可得的。其订购信息在[IEEE 1996]的参考文献说明中给出。
注意信号量在实时标准中定义,它与在Pthreads标准中定义的互斥锁和条件变量相分离,这足以解释它们的API中存在的某些差异。
最后注意读写锁(尚)不属于任何Posix标准。我们将在笫8章中详细讨论。

13

将来某个时候印制的新版本的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年合并而成的组织。它是由厂家、业界最终用户、政府部门和学术机构组成的国际组织。它们的标准经历了以下若干代。

14

单一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)文件服务器:客户-服务器应用程序,客户向服务器发送一个路径名,服务器把该文件的内容返回给客户。

15

(2)生产者-消费者:一个或多个线程或进程(生产者)把数据放到一个共享缓冲区中,另有一个或多个线程或进程(消费者)对该共享缓冲区中的数据进行操作。

(3)序列号持续增1:一个或多个线程或进程给一个共享的序列号持续增1。该序列号有时在一个共享文件中,有时在共享内存区中。

第一个例子分析各种形式的消息传递,另外两个例子则分析各种类型的同步和共享内存区。

为了提供本书所涵盖的不同主题的索引,图1-9、图1-10和图1-11汇总了我们开发的程序及它们的源代码所在的起始图号和页码。

图1-9 不同版本的文件服务器客户-服务器例子

图1-10 不同版本的生产者-消费者例子

图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。

16~17

1.1 图1-1中我们展示了两个进程访问单个文件的情形。如果这两个进程都只是往该文件的末尾添加新的数据(譬如说这是一个日志文件),那么需要什么类型的同步?

1.2 查看一下你的系统中的<error.h>头文件是如何定义errno变量的。

1.3 在图1-5上标注出你使用的Unix系统所支持的特性。

 意思是用新的出错处理函数代替原来的err_sys,这样对线程函数的调用可直接作为新的出错处理函数中增设的参数用,即 err_sys_new("pthread_mutex_lock error", pthread_mutex_lock(&ndone_mutex))。——译者注


以下三种类型的IPC合称为“Posix IPC”:

Posix IPC在访问它们的函数和描述它们的信息上有一些类似点。本章讲述所有这些共同属性:用于标识的路径名、打开或创建时指定的标志以及访问权限。

图2-1汇总了所有Posix IPC函数。

图2-1 Posix IPC函数汇总

在图1-4中我们指出,三种类型的Posix IPC都使用“Posix IPC名字”进行标识。mq_opensem_openshm_open这三个函数的第一个参数就是这样的一个名字,它可能是某个文件系统中的一个真正的路径名,也可能不是。Posix.1是这么描述Posix IPC名字的。

因此,为便于移植起见,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)

19~20

它们的单个参数是指向某个stat结构的指针,其内容由fstatlstatstat这三个函数填入。如果所指定的IPC对象(消息队列、信号量或共享内存区对象)是作为一种独特的文件类型实现的,而且参数所指向的stat结构访问这样的文件类型,那么这三个宏计算出一个非零值。否则,计算出的值为0。

不幸的是,这三个宏没有多大用处,因为无法保证这三种类型的IPC使用一种独特的文件类型实现。举例来说,在Solaris 2.6下,这三个宏的计算结果总是0。

测试某个文件是否为给定文件类型的所有其他宏的名字都以S_IS开头,而且它们的单个参数是某个stat结构的st_mode成员。由于上面三个新宏的参数不同于其他宏,因此它们的名字改为以S_TYPEIS开头。

解决上述移植性问题的另一种办法是自己定义一个名为px_ipc_name的函数,它为定位Posix IPC名字而添加上正确的前缀目录。

本书中我们给自己定义的非标准系统函数都使用这样的版式:围绕函数原型和返回值的方框是虚框。开头包含的头文件通常是我们的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给出了该函数的实现。

图2-2 我们的px_ipc_name函数

这也许是你第一次碰到snprintf函数。许多现有代码调用的是sprintf,但是sprintf不检查目标缓冲区是否溢出,不过snprintf要求其第二个参数是目标缓冲区的大小,因此可确保缓冲区不溢出。提供能有意溢出一个程序的sprintf缓冲区的输入数据是黑客们已使用很多年的一种攻破系统的方法。

snprintf不是标准ANSI C的一部分,但这个标准的修订版C9X正在考虑。不过,许多厂家提供的标准C函数库含有这个函数。我们在本书中使用snprintf,如果你的系统不提供这个函数,那就使用我们自己的通过调用sprintf实现的版本。

mq_opensem_openshm_open这三个创建或打开一个IPC对象的函数,它们的名为oflag的第二个参数指定怎样打开所请求的对象。这与标准open函数的第二个参数类似。图2-3给出了可组合构成该参数的各种常值。

图2-3 打开或创建Posix IPC对象所用的各种oflag常值

前3行指定怎样打开对象:只读、只写或读写。消息队列能以其中任何一种模式打开,信号量的打开不指定任何模式(任意信号量操作,都需要读写访问权),共享内存区对象则不能以只写模式打开。

图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页)或使用shell的umask命令来设置。

跟新创建的文件一样,当创建一个新的消息队列、信号量或共享内存区对象时,其用户ID被置为当前进程的有效用户ID。信号量或共享内存区对象的组ID被置为当前进程的有效组ID或某个系统默认组ID。新消息队列对象的组ID则被置为当前进程的有效组ID(APUE第77~78页详细讨论了用户ID和组ID。)

这三种Posix IPC类型在设置组ID上存在的差异多少有点奇怪。由open新创建的文件的组ID或者是当前进程的有效组ID,或者是该文件所在目录的组ID,但是IPC函数不能假定系统为IPC对象创建了一个在文件系统中的路径名。

21~23

O_EXCL 如果该标志和O_CREAT一起指定,那么IPC函数只在所指定名字的消息队列、信号量或共享内存区对象不存在时才创建新的对象。如果该对象已经存在,而且指定了O_CREAT|O_EXCL,那么返回一个EEXIST错误。

考虑到其他进程的存在,检查所指定名字的消息队列、信号量或共享内存区对象的存在与否和创建它(如果它不存在)这两步必须是原子的(atomic)。我们将在3.4节看到适用于SystemV IPC的两个类似标志。

O_NONBLOCK 该标志使得一个消息队列在队列为空时的读或队列填满时的写不被阻塞。我们将在5.4节随mq_receivemq_send这两个函数详细讨论该标志。 - O_TRUNC 如果以读写模式打开了一个已存在的共享内存区对象,那么该标志将使得该对象的长度被截成0。

图2-5展示了打开一个IPC对象的真正逻辑流程。我们将在2.4节通过访问权限的测试说明该图。图2-6是展示图2-5中逻辑的另一种形式。

图2-5 打开或创建一个IPC对象的逻辑

图2-6 创建或打开一个IPC对象的逻辑

24

注意图2-6指定了O_CREAT标志但没有指定O_EXCL标志的中间那行,我们无法得到一个指示以判别是创建了一个新对象,还是在引用一个已存在的对象。

新的消息队列、有名信号量或共享内存区对象是由其oflag参数中含有O_CREAT标志的mq_opensem_openshm_open函数创建的。如图2-4所注,权限位与这些IPC类型的每个对象相关联,就像它们与每个Unix文件相关联一样。

当同样由这三个函数打开一个已存在的消息队列、信号量或共享内存区对象时(或者未指定O_CREAT,或者指定了O_CREAT但没有指定O_EXCL,同时对象已经存在),将基于如下信息执行权限测试:

(1)创建时赋予该IPC对象的权限位;

(2)所请求的访问类型(O_RDONLYO_WRONLYO_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对象,但它属于某个合适的组,那么访问权的授予与拒绝只依赖于组访问权限——其他用户访问权限绝不会考虑。

25

我们从图2-3中指出,sem_open不使用O_RDONLYO_WRONLYO_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节中所述的权限测试的?

26

2.2 当一个程序打开一个Posix IPC对象时,它怎样才能判定是创建了一个新对象还是在引用一个已有的对象?

  snprinf现在已经是C99标准的函数。——编者注

 此处为APUE第1版英文原版书页码,第2版为第97~100页,第2版中文版为第80~82页。——编者注

 同样为第1版英文原版书中页码,第2版为第16~17页,第2版中文版为第12~13页。——编者注


相关图书

Linux常用命令自学手册
Linux常用命令自学手册
庖丁解牛Linux操作系统分析
庖丁解牛Linux操作系统分析
Linux后端开发工程实践
Linux后端开发工程实践
轻松学Linux:从Manjaro到Arch Linux
轻松学Linux:从Manjaro到Arch Linux
Linux高性能网络详解:从DPDK、RDMA到XDP
Linux高性能网络详解:从DPDK、RDMA到XDP
跟老韩学Linux架构(基础篇)
跟老韩学Linux架构(基础篇)

相关文章

相关课程