附录A 性能测量

A.1 概述

本书讨论了六种类型的消息传递:

管道;

FIFO;

Posix消息队列;

System V消息队列;

门;

Sun RPC。

和五种类型的同步:

互斥锁和条件变量;

读写锁;

fcntl记录上锁;

Posix信号量;

System V信号量。

我们现在开发一些简单的程序来测量这些IPC类型的性能,这样有助于我们就何时该使用某种特定形式的IPC做出明智的决策。

比较不同形式的消息传递时,我们感兴趣的是以下两种测量尺度。

(1)带宽(bandwidth):数据通过IPC通道转移的速度。为测量该值,我们从一个进程向另一个进程发送大量数据(几百万字节)。我们还给不同大小的I/O操作(例如管道和FIFO的write和read操作)测量该值,期待发现带宽随每个I/O操作数据量的增长而增长的规律。

(2)延迟(latency):一个小的IPC消息从一个进程到另一个进程再返回所花的时间。我们测量的是只有1个字节的消息从一个进程到另一个进程再回来的时间(往返时间)。

在现实世界中,带宽告诉我们大块数据通过一个IPC通道发送出去需花多长时间,然而IPC也用于传送小的控制消息,系统处理这些小消息所需的时间就由延迟提供。这两个数都很重要。

为测量各种同步形式的性能,我们来修改将处于共享内存区中的一个计数器持续加1的程序,该程序或者由多个线程给该计数器每次加1,或者由多个进程给该计数器每次加1。既然加1是个简单的操作,因此它所需的时间差不多由同步原语操作的时间决定。

本附录中用于测量各种形式IPC之性能的简单程序大体上基于[McVoy and Staelin 1996]中描述的lmbench标准测量程序(benchmark)套件。这是一套复杂精致的标准测量程序,用于测量一个Unix系统的许多特征(上下文切换时间、I/O吞吐量,等等),而不光是IPC。它们的源代码是公开可得的:http://www.bitmover.com/lmbench。

本附录中给出的各个数值是为让我们比较本书中讲述的各种技术而提供的。所暗含的一个动机是展示测量这些值是多么地简单。在从各种技术中作出选择之前,你必须测量自己的系统的这些性能数值。不幸的是,就像测量数值之易的程度一样,当检测到反常现象时,在没法访问出问题的内核或函数库的源代码的情况下,解释起来往往非常困难。

A.2 结果

现在汇总出自本附录的所有结果,目的是在逐个查看我们给出的各个程序的过程中提供方便的指引。

用于所有测量的两个系统是运行Solaris 2.6的一台SparcStation 4/110和运行Digital Unix 4.0B的一台Digital Alpha(DEC 3000型号300,Pelican)。往该Solaris系统的/etc/system文件中添加了以下各行:

set msgsys:msginfo_msgmax = 16384

set msgsys:msginfo_msgmnb = 32768

set msgsys:msginfo_msgseg = 4096

这样允许在System V消息队列中出现大小为16384字节的消息(图A-2)。通过作为sysconfig程序的输入指定以下各行,对该Digital Unix系统进行同样的修改:

ipc:

msg-max = 16384

msg-mnb = 32768

A.2.1 消息传递带宽结果

图A-2列出了在运行Solaris 2.6的一台Sparc主机上测得的带宽结果,图A-3图示了这些值。图A-4列出了在运行Digital Unix 4.0B的一台Alpha主机上测得的带宽结果,图A-5图示了这些值。

正如我们可能预期的那样,带宽通常随消息大小的增长而增长。由于System V消息队列的实现具有较小的内核限制值(3.8节),因此最大的消息是16384字节,而且即使对于这个大小的消息,内核默认值也不得不增加。Solaris系统的带宽在4096字节以上时减小的可能原因在于内部消息队列限制的配置。为跟UNPv1比较,我们还给出了TCP套接字和Unix域套接字的值。这两个值是使用lmbench软件包中的程序测得的,只使用了大小为65536字节的一种消息。对于TCP套接字的测量,它的两个进程处于同一台主机中。

A.2.2 消息传递延迟结果

图A-1列出了在Solaris 2.6和Digital Unix 4.0B下测得的延迟结果。

图A-1 使用各种形式的IPC交换一个1字节消息的延迟

在A.4节中我们将给出测量其中前6个值的程序,剩下3个值则出自lmbench套件。对于TCP和UDP的测量,它们的两个进程都处于同一台主机中。

图A-2 各种类型消息传递的带宽(Solaris 2.6)

图A-3 各种类型消息传递的带宽(Solaris 2.6)

图A-4 各种类型消息传递的带宽(Digital Unix 4.0B)

图A-5 各种类型消息传递的带宽(Digital Unix 4.0B)

A.2.3 线程同步结果

图A-6列出了Solaris 2.6下使用各种形式的同步,由一个或多个线程给处于共享内存区中的一个计数器持续加1所需的时间,图A-7图示了这些值。每个线程给该计数器加1共1 000 000次,这样的线程数则从1到5变化。图A-8列出了Digital Unix 4.0B下的这些值,图A-9图示了这些值。

增加线程数的原因在于验证使用同步技术的代码是正确的,并查看时间是否随线程数的增加而开始非线性地增长。对于fcntl记录上锁我们只能测量单个线程,因为这种同步形式工作于进程间,而不是单个进程内的多个线程间。

Digital Unix下,线程数多于一个时两种Posix信号量类型的时间变得非常大,表明存在某种类型的反常。我们没有图示这些值。

出现这些比预期值大的数值的可能原因之一是,本程序是一个病态的同步测试程序。也就是说,各个线程除同步外什么都不干,因而其中的锁基本上所有时间都为某个线程所锁住。既然默认情况下线程是以进程竞用范围属性创建的,因此每当一个线程失去它的时间片时,它可能仍持有该锁,于是切换来运行的新线程可能立即阻塞。

图A-6 给处于共享内存区中的一个计数器持续加1所需的时间(Solaris 2.6)

图A-7 给处于共享内存区中的一个计数器持续加1所需的时间(Solaris 2.6)

图A-8 给处于共享内存区中的一个计数器持续加1所需的时间(Digital Unix 4.0B)

图A-9 给处于共享内存区中的一个计数器持续加1所需的时间(Digital Unix 4.0B)

A.2.4 进程同步结果

图A-6、图A-7、图A-8和图A-9展示了各种同步技术用于同步单个进程内的各个线程时测得的结果。图A-10和图A-11给出了Solaris 2.6下在不同进程间共享计数器时这些同步技术的性能。图A-12和图A-13给出了Digital Unix 4.0B下的进程同步结果。这些结果与对应的线程同步结果类似,不过两种Posix信号量形式的值现在类似于Solaris的结果。我们只画出了fcntl记录上锁的第一个值,因为其余各值太大了。正如我们在7.2节中注出的那样,Digital Unix 4.0B不支持PTHREAD_PROCESS_SHARED特性,因此我们无法测量不同进程间的互斥锁值。在Digital Unix下当涉及多个进程时,我们再次看到了Posix信号量的某种类型的反常。

图A-10 给处于共享内存区中的一个计数器持续加1所需的时间(Solaris 2.6)

图A-11 给处于共享内存区中的一个计数器持续加1所需的时间(Solaris 2.6)

图A-12 给处于共享内存区中的一个计数器持续加1所需的时间(Digital Unix 4.0B)

图A-13 给处于共享内存区中的一个计数器持续加1所需的时间(Digital Unix 4.0B)

A.3 消息传递带宽程序

本节给出测量管道、Posix消息队列、System V消息队列、门和Sun RPC的带宽的各个程序。我们已在图A-2和图A-3中给出了这些程序的结果。

A.3.1 管道带宽程序

图A-14展示了我们将描述的程序的概貌。

图A-15给出了我们的bw_pipe程序的前半部分,它测量一个管道的带宽。

图A-14 测量一个管道的带宽的程序的概貌

图A-15 一个管道带宽测量程序的main函数

命令行参数

11~15 命令行参数指定待执行的循环数(以下测量中的典型值为5)、待传送的M字节 [1] 数(值为10的参数导致传送10×1024×1024字节)以及每次write和read的字节数(我们给出的测量结果中该值在1024和65536之间变化)。

分配缓冲区并触及它的各个页面

16~17 valloc是malloc的版本之一,它从某个页面边界开始分配所请求数量的内存空间。我们的函数touch(图A-17)在该缓冲区的每个页面中存入1字节的数据,从而迫使内核把构成该缓冲区的每个页面置换进内存。我们在进行计时之前完成这些工作。

valloc不属于Posix.1函数,Unix 98把它列为一个“代传(legacy)”接口:早期版本的X/Open规范需要它,但它现在是可选的。如果valloc不受支持,我们的Valloc包裹函数就调用malloc来实现它的功能。

创建两个管道

18~19 创建两个管道,其中contpipe[0]和contpipe[1]用于在开始每次传送之前同步一读一写两个进程,datapipe[0]和datapipe[1]用于真正的数据传送。

调用fork派生子进程

20~31 行派生一个子进程,该子进程(它的fork返回值为0)调用writer函数,父进程则调用reader函数。父进程中reader函数调用nloop次。我们的start_time函数在该循环开始前即刻调用,我们的stop_time函数在该循环终止后马上调用。图A-17给出了这两个函数。所输出的带宽为每次循环传送的总字节数除以传送数据所花的时间(stop_time返回的是自调用start_time以来流逝的微秒数),再乘以循环次数。子进程随后被父进程发送的SIGTERM信号杀死,程序随后终止。

图A-16给出了bw_pipe程序的后半部分,它包含两个函数writer和reader。

图A-16 测量一个管道的带宽的writer和reader函数

writer函数

33~44 本函数是由子进程调用的一个无限循环。子进程通过在控制管道读出一个整数(该整数指定子进程应写入数据管道的字节数),来等待父进程表明自身已准备好接收数据。接收到这个通知后,子进程通过管道向父进程写入数据,每次write调用写xfersize个字节。

reader函数

45~54 本函数是由父进程在一个循环中调用的。它每次被调用时往控制管道写入一个整数,告诉子进程应往数据管道中写入多少字节的数据。本函数随后在一个循环中调用read,直到全部数据都接收完为止。

图A-17 给出了我们的start_time、stop_time和touch函数。

图A-17 计时函数:start_time、stop_time和touch

图A-18给出了tv_sub函数;它在两个timeval结构间做减法运算,并把结果存回第一个结构中。

图A-18 tv_sub函数:两个timeval结构相减

在运行Solaris 2.6的一台Sparc主机上接连运行我们的程序5次,得到如下结果:

solaris % bw_pipe 5 10 65536

bandwidth: 13.722 MB/sec

solaris % bw_pipe 5 10 65536

bandwidth: 13.781 MB/sec

solaris % bw_pipe 5 10 65536

bandwidth: 13.685 MB/sec

solaris % bw_pipe 5 10 65536

bandwidth: 13.665 MB/sec

solaris % bw_pipe 5 10 65536

bandwidth: 13.584 MB/sec

每次我们指定5轮循环,每轮循环传送10 485 760个字节,每次write和read调用收发65536个字节。这5次程序运行的平均值为图A-2中所示的13.7MB/s。

A.3.2 Posix消息队列带宽程序

图A-19是一个Posix消息队列带宽测量程序的main函数。图A-20给出了其中的writer和reader函数。该程序与前面那个管道带宽测量程序类似。

图A-19 Posix消息队列带宽测量程序的main函数

图A-19(续)

图A-20 测量一个Posix消息队列的带宽的wirter和reader函数

注意,在我们创建消息队列时,必须指定该程序上能存在的最大消息数,我们把它指定为4。IPC通道的容量能够影响性能,因为写进程在阻塞于某个mqsend调用之前能够发送这么多的消息,然后由内核将上下文切换到读进程。因此本程序的性能依赖于这个魔数。Solaris 2.6下把该数从4改为8并不影响图A-2中的各个数值,然而Digital Unix 4.0B下的同样变动却使性能下降了12%。我们原本会猜想消息数较多时性能将增长,因为这可以让上下文切换数降低一半。然而如果用到了内存映射文件,那么这样一来将使该文件的大小增加一倍,所映射的内存区大小也增长一倍。

A.3.3 System V消息队列带宽程序

图A-21是一个System V消息队列带宽测量程序的main函数,图A-22给出了其中的writer和reader函数。

图A-21 一个System V消息队列带宽测量程序的main函数

图A-22 测量一个System V消息队列的带宽的writer和reader函数

A.3.4 门带宽程序

门API带宽测量程序比本节中先前给出的程序要复杂,因为在创建其中的门之前必须fork出子进程。父进程接着创建该门,并通过写一个管道来通知子进程该门能被打开。

不像图A-14的另一个变化是,reader函数不接收数据。相反,数据是由名为server的函数接收的,它是对应门的服务器过程。图A-23给出了该程序的概貌。

门只在Solaris下受支持,于是我们通过假设采用全双工管道(4.4节)来简化程序本身。

与先前的程序相比,另一个变化在于消息传递和过程调用间的基本差异。例如在我们的Posix消息队列程序中,写入者只是在一个循环中往一个队列写入消息,这种写操作是异步的。到某个时刻队列满了,或者写进程丧失了自己的处理器时间片,读出者就开始运行,读出这些消息。举例来说,如果该队列可容纳8个消息,而且写入者每次运行时写入8个消息,读出者每次运行时读出所有8个消息,那么发送N个消息将涉及N/4次上下文切换(其中N/8次是从写入者到读出者,另外N/8次是从读出者到写入者)。然而门API是同步的:调用者每次调用door_call时阻塞,直到服务器过程返回才能恢复。交换N个消息现在涉及N×2次上下文切换。测量RPC调用的带宽时,我们将碰到同样的问题。尽管上下文切换数增加了,从图A-3可以看出,当消息大小在约25000字节或以上时,门却提供了最快的IPC带宽。

图A-24给出了该程序的main函数。writer、server和reader函数则在图A-25中给出。

图A-23 门API带宽测量程序的概貌

图A-24 门API带宽测量程序的main函数

图A-24(续)

图A-25 测量门API的带宽的writer、server和reader函数

图A-25(续)

A.3.5 Sun RPC带宽程序

既然Sun RPC中的过程调用是同步的,那么我们有已随前面的门程序提到过的同样限制。使用RPC时生成一个客户和一个服务器两个程序更为容易,因为它们是由rpcgen生成的。图A-26给出了本程序的RPC说明书文件。我们声明了单个过程,它以一个可变长度不透明数据作为输入,不返回任何东西。

图A-26 Sun RPC带宽测量程序的RPC说明书文件

图A-27给出了我们的客户程序,图A-28给出了我们的服务器过程。我们把协议(TCP或UDP)指定为客户程序的一个命令行参数,以允许分别测量这两种协议。

图A-27 测量Sun RPC的带宽的RPC客户程序

图A-27(续)

图A-28 测量Sun RPC的带宽的RPC服务器过程

A.4 消息传递延迟程序

本节给出测量管道、Posix消息队列、System V消息队列、门和Sun RPC的延迟的各个程序。图A-1给出了它们的性能值。

A.4.1 管道延迟程序

图A-29给出了测量一个管道的延迟的程序。

图A-29 测量一个管道的延迟的程序

doit函数

2~9 本函数在父进程中运行,它所花的时钟时间将被测量出来。它往一个管道写入1个字节(该字节将由子进程读出)后从另一个管道读出1个字节(该字节由子进程写入)。这就是我们所描述的延迟:从发送一个小消息到接收作为应答的一个小消息所花的时间。

创建管道

19~20 创建两个管道,fork一个子进程,形成图4-6所示的布局(不过没有关闭每个管道的未用端,这是不会有问题的)。本测试程序确实需要两个管道,因为管道是半双工的,而我们希望父子进程间有双向通信。

子进程回射只有1个字节的消息

22~27 子进程执行一个无限循环,每次读出一个只有1个字节的消息后就把它发射回来。

测量父进程

29~34 父进程首先调用doit给子进程发送一个只有1个字节的消息,并读出它的也是只有1个字节的应答。这使得两个进程都处于运行状态。父进程然后在一个循环中调用doit函数,同时测量所花的时钟时间。

在运行Solaris 2.6的一台Sparc主机上接连运行该程序5次,得到如下结果:

solaris % lat_pipe 10000

latency: 278.633 usec

solaris % lat_pipe 10000

latency: 397.810 usec

solaris % lat_pipe 10000

latency: 392.567 usec

solaris % lat_pipe 10000

latency: 266.572 usec

solaris % lat_pipe 10000

latency: 284.559 usec

这5次程序运行的平均值为324微秒,它就是图A-1中给出的值。这些时间包括两次上下文切换(从父进程到子进程,然后是从子进程到父进程)、四个系统调用(父进程的write、子进程的read、子进程的write、父进程的read)以及每个方向1字节数据的管道开销。

A.4.2 Posix消息队列延迟程序

图A-30给出了一个Posix消息队列延迟测量程序。

图A-30 一个Posix消息队列延迟测量程序

图A-30(续)

25~28 创建两个消息队列,一个用于从父进程到子进程的消息传递,另一个用于从子进程到父进程的消息传递。尽管Posix消息具有优先级,从而允许我们给两个不同方向的消息赋不同的优先级,mq_receive却总是返回队列中的下一个消息。因此,我们不能仅给本测试程序使用一个队列。

A.4.3 System V消息队列延迟程序

图A-31给出了一个System V消息队列延迟测量程序。

本程序只创建一个消息队列,它含有两个不同方向的消息:从父进程到子进程和从子进程

到父进程。前者的类型字段为1,后者的类型字段为2。doit中msgrcv的第四个参数为2,子进程中msgrcv的第四个参数为1,它们都只读出所指定类型的消息。

在9.3节和11.3节中我们提到过,许多由内核定义的结构不能静态地初始化,因为Posix.1和Unix 98只保证在这样的结构中存在特定的成员。这两个标准不保证这些成员的顺序,更何况这样的结构还可以含有其他的非标准成员。然而在本程序中我们还是静态地初始化msgbuf结构,因为System V消息列保证该结构含有一个long类型的消息类型成员,后跟真正的数据。

图A-31 一个System V消息队列延迟测量程序

A.4.4 门延迟程序

图A-32给出了门API的延迟测量程序。子进程创建其中的门,然后给该门关联以server函数。父进程接着打开该门,并在一个循环中激活door_call。作为参数传递的是1个字节的数据,返回值则不存在。

图A-32 门API的延迟测量程序

A.4.5 Sun RPC延迟程序

为测量Sun RPC API的延迟,我们编写一个客户和一个服务器两个程序(类似于测量Sun RPC带宽的做法)。我们使用同样的RPC说明书文件(图A-26),不过这次我们的客户程序调用空过程。回想习题16.11,我们知道该过程没有参数,也没有返回值,而这正好是测量延迟所需的。图A-33给出了客户程序。跟习题16.11的解答一样,我们必须通过直接调用clnt_call来调用空过程,在客户程序存根中不提供它的存根函数。

图A-33 测量Sun RPC延迟的Sun RPC客户程序

我们使用图A-28中的服务器函数编译出服务器程序,不过该函数从来不被调用。既然是使用rpcgen来构造客户程序和服务器程序的,那么我们需要定义至少一个服务器过程,但是可不调用它。使用rpcgen的原因在于它自动产生带空过程的服务器程序的main函数,而我们正需要这些。

A.5 线程同步程序

为测量各种同步技术所需的时间,我们创建一定数目的线程(图A-6和图A-8给出的测量结果使用1个到5个线程),每个线程给处于共享内存区中的一个计数器加1很多次(一个很大的次数),各线程使用相应于当前测量的同步形式来协调对于该共享计数器的访问。

A.5.1 Posix互斥锁程序

图A-34给出了Posix互斥锁测量程序的全局变量和main函数。

图A-34 测量Posix互斥锁同步的全局变量和main函数

共享的数据

4~9 各线程间共享的数据由互斥锁本身和计数器构成。该互斥锁是静态初始化的。

给互斥锁上锁并创建线程

20~26 主线程在创建其他线程前给共享数据的互斥锁上锁,这样在所有线程都创建出来并且主线程释放该互斥锁之前,没有一个线程能够获取该互斥锁。主线程接着调用我们的set_concurrency函数,并创建出各个线程。每个线程执行接下来给出的incr函数。

启动定时器并释放互斥锁

27~36 一旦所有线程都已创建,主线程就启动定时器并释放互斥锁。它接下去等待所有线程结束,到时候就停止定时器,输出所计的总微秒数。

图A-35给出了每个线程执行的incr函数。

图A-35 使用一个Posix互斥锁给一个共享的计数器加1

在临界区中给计数器加1

44~46 获取共享数据的互斥锁后给共享的计数器加1。接着释放该互斥锁。

A.5.2 读写锁程序

使用读写锁的程序由刚才使用Posix互斥锁的程序稍加修改而成。每个线程在给共享的计数器加1前必须获取该计数器读写锁中的写入锁。

实现第8章中讲述的Posix读写锁的系统并不多,Posix读写锁是Unix 98的一部分内容, Posix.1j工作组目前正在考虑它的标准化。本附录中给出的读写锁测量结果是在Solaris 2.6下做出的,它使用在rwlock(3T)手册页面中描述的Solaris读写锁。该实现提供了与提议中的读写锁同样的功能,以我们在第8章中给出的函数为接口来使用这些函数所需要的包裹函数非常简单。

Digital Unix 4.0B下的测量结果是使用Digital的线程无关服务读写锁做出的,这种读写锁在tis_rwlock手册页面中描述。我们不再给出为这些读写锁对图A-36和图A-37进行的简单修改。

图A-36给出了main函数,图A-37给出了incr函数。

图A-36 读写锁同步测量程序的main函数

图A-36(续)

图A-37 使用一个读写锁给一个共享的计数器加1

A.5.3 Posix基于内存的信号量程序

我们既测量Posix基于内存的信号量,也测量Posix有名信号量。图A-39给出了基于内存的信号量的测量程序的main函数,图A-38给出了它的incr函数。

图A-38 使用一个Posix基于内存的信号量给一个共享的计数器加1

图A-39 Posix基于内存信号量的同步的测量程序的main函数

18~19 创建一个值为0的信号量,而把给sem_init的第二个参数指定为0表明所创建的信号量将在调用进程的各个线程间共享。

20~27 创建出所有的线程后,主线程启动定时器并调用sem_post一次。

A.5.4 Posix有名信号量程序

图A-41给出了Posix有名信号量测量程序的main函数,图A-40给出了它的incr函数。

图A-40 Posix有名信号量的同步的测量程序的main函数

图A-41 使用一个Posix有名信号量给一个共享的计数器加1

图A-41(续)

A.5.5 System V信号量程序

图A-42给出了System V信号量测量程序的main函数,图A-43给出它的incr函数。

图A-42 测量System V信号量的同步的程序的main函数

图A-42(续)

图A-43 使用一个System V信号量给一个共享的计数器加1

20~23 创建一个仅有一个成员的信号量集,并把它的值初始化为0。

24~29 初始化两个semop结构,一个用于挂出该信号量,另一个用于等待该信号量。注意这两个结构的sem_flg成员均为0,也就是说没有指定SEM_UNDO标志。

A.5.6 带SEM_UNDO特性的System V信号量程序

测量具有SEM_UNDO特性的System V信号量的程序与图A-42相比的唯一差别是:它把两个semop结构的sem_flg成员设置成SEM_UNDO而不是0。我们不再给出这个简单修改后的版本。

A.5.7 fcntl记录上锁程序

最后一个程序使用fcntl记录上锁提供同步。图A-45给出了它的main函数。该程序只在指定单个线程时才会成功地运行,因为fcntl锁是在不同进程间而不是单个进程的不同线程间共享的。当指定多个线程时,每个线程总能获取所请求的锁(也就是说writew_lock调用从不阻塞,因为调用进程早已拥有该锁),因而计数器的最终值是错误的。

18~22 待创建并随后用于上锁的文件的路径名是作为一个命令行参数指定的。这样允许我们就驻留在不同文件系统上的文件测量这个程序。我们预期当该文件处在某个通过NFS安装的文件系统上时,该程序的运行变慢,这种情况要求两个系统(NFS客户主机和NFS服务器主机)都支持NFS记录上锁。

图A-44给出了使用记录上锁的incr函数。

图A-44 使用fcntl记录上锁给一个共享的计数器加1

图A-45 fcntl记录上锁测量程序的main函数

图A-45(续)

A.6 进程同步程序

上一节给出的程序中,多个线程间共享一个计数器比较简单:我们只需把该计数器作为一个全局变量存放。我们现在修改这些程序以提供不同进程间的同步。

为在一个父进程和它的各个子进程间共享该计数器,我们把它存放在由图A-46给出的my_shm函数分配的共享内存区中。

图A-46 给一个父进程和它的各个子进程创建一个共享内存区

如果系统支持MAP_ANON标志(12.4节),我们就使用它,否则我们把/dev/zero(12.5节)映射到内存。

进一步的修改依赖于同步类型以及调用fork时底层支撑数据类型发生的变化。我们已在10.12节讲述过其中一些细节。

Posix互斥锁:互斥锁必须存放在共享内存区中(跟共享的计数器在一起),而且初始化互斥锁时必须设置PTHREAD_PROCESS_SHARED属性。我们稍后给出这个程序的代码。

Posix读写锁:读写锁必须存放在共享内存区中(跟共享的计数器在一起),而且初始化读写锁时必须设置PTHREAD_PROCESS_SHARED属性。

Posix基于内存的信号量:信号量必须存放在共享内存区中(跟共享的计数器在一起),而且sem_init的第二个参数必须为1,从而指定该信号量是在进程间共享的。

Posix有名信号量:我们既可以让父进程和每个子进程都调用sem_open,也可以只让父进程调用sem_open,因为我们知道该信号量将通过fork由子进程共享。

System V信号量:不必编写任何特殊的代码,因为这种信号量总是能够在进程间共享。子进程只需知道父进程所创建信号量的标识符。

fcntl记录上锁:不必编写任何特殊的代码,因为描述符可通过fork由子进程共享。

我们只给出Posix互斥锁程序的代码。

Posix互斥锁程序

Posix互斥锁程序的main函数使用一个Posix互斥锁来提供进程间的同步形式,如图A-48所示。图A-47给出了它的incr函数。

图A-47 测量进程间Posix互斥锁上锁的incr函数

图A-48 进程间Posix互斥锁上锁测量程序的main函数

图A-48(续)

19~20 既然使用多个进程(一个父进程的多个子进程),我们就必须把shared结构置于共享内存区中。我们调用图A-46给出的my_shm函数做到这一点。

21~26 既然互斥锁处于共享内存区中,我们就不能静态地对它初始化,而必须在设置一个PTHREAD_PROCESS_SHARED属性对象后调用pthread_mutex_init初始化它。该互斥锁一开始是锁着的。

27~36 创建出所有子进程后,父进程启动定时器,并给互斥锁解锁。

37~43 父进程等待所有子进程都结束,然后停止定时器。


[1]. megabyte一词存在歧义。我们在它代表220 字节时译为M字节,在它代表106 字节时译为兆字节。kilobyte和gigabyte两词也有类似译法。——译者注