第 3 章 进程间通信
3.1 进程间通信的方法
当多个进程在一台计算机上同时执行时,它们之间会形成一种关系。即使是从程序上看不出有联系的几个进程,在同时运行时相互之间也会产生联系。因为它们都要使用机器上的资源,而这些资源是有限的,需要大家按照某种秩序来分别使用。当某资源被其他进程占用了,下一个要使用该资源的进程就要等待,待该资源被释放后再使用。那些在设计时就要求彼此之间共操作的多个进程,在运行时更要频繁地通信、传递数据、同步相互的动作。
进程间通信的目的有三种:
- 同步有关进程的执行
- 改变进程的执行方向
- 在进程间传递数据
所谓同步进程的执行,一方面是指一个进程要在获得另一个进程的某些执行结果后才能向下执行,另一方面是指多个进程互斥地使用共同资源。同步有关进程执行的方法有:二值信号灯、多值信号灯、文件锁、记录锁等。
改变进程的执行方向是指从进程的外部改变进程正在执行的顺序,让进程去执行另外的程序。改变进程执行方向的手段有信号、软中断和事件通告。
进程间传递数据的方法有消息、消息队列、共享内存、管道、命名管道(FIFO)等等。多数传递数据的方法需要使用同步手段协调不同进程对数据的读写。
可以证明,诸多的同步方法是能够相互表达的,诸多的数据传递方法也是能够相互实现的,只不过某种方法在某种场合用起来更加方便而已。在实现一个系统时,总是选择某些进程间通信的方法作为一个系统内部通信的基本方法,在内核中实现,而在内核外实现进程间的其他通信方法。
多个进程同时运行会竞争资源、需要协作、还要交换数据,因此必须使用进程间通信。IPC 的目的包括同步进程执行、改变进程行为、以及在进程之间传递数据。各种通信和同步方式本质上都能互相模拟,但系统通常选择一种作为核心机制,其它方式都基于它实现。
3.2 QNX 进程间通信
QNX 微内核支持三种基本类型的进程间通信:消息、代理和信号。下面是对这三种通信方法的说明:
- 消息(Message):同步、可靠、带回复的通信,既传数据又做同步(发送者通常会阻塞等待回复)。
- 代理(Proxy / Pulse-style event):轻量级的事件通知,不需要完整的请求/应答交互,适合频繁或实时的事件通知(发送者通常不阻塞)。
- 信号(Signal):异步通知机制(类似传统 Unix signal),用于简单的异步事件或控制(送达后被接收线程异步处理)。
消息 = 可靠的同步 RPC(数据 + 同步),代理/脉冲 = 轻量异步事件通知(低开销),信号 = 传统的异步控制机制(粗粒度)。在 QNX 中,这三种机制互补,工程上常常混合使用以满足可靠性与性能的平衡。
3.2.1 通过消息进行进程间通信
在 QNX 中,一个消息是一个将若干字节数据封装在一起组成的数据包,可以从一个进程同步(即发送进程将一直等待到对方进程接收)地传送到另一个进程。QNX 不对消息的内容附加任何解释信息,因此消息的内容仅对消息的发送者及接收者有意义。
(1)消息传递原语
进程可使用如下 C 语言函数来直接与另一个进程进行通信。
| C 函数 | 功能 |
|---|---|
| Send() | 发送消息 |
| Receive() | 接收消息 |
| Reply() | 回答发送进程 |
这些函数既可在网络环境中使用也可在本地环境中使用。
注意:进程之间除非希望直接进行通信,一般情况下不需要使用 Send ()、Receive () 和 Reply () 函数。由于 QNX 的 C 函数库是建立在消息传递的基础之上的,所以当进程使用像管道(pipe)这样的标准服务时将以间接方式使用消息传递机制。

图3-1 进程A向进程B发送一条消息,B进程接收该消息,进行处理并给予回答
图 3-1 表明所发生的一个简单的事件序列:两个进程(进程 A 和进程 B)使用 Send ()、Receive () 和 Reply () 相互进行通信。
- 进程 A 通过发送 Send 请求向进程 B 发送了一条消息。这时,进程 A 进入 Send 后的 SEND 阻塞状态,直到进程 B 调用了 Receive () 来接收消息。
- 进程 B 调用了 Receive (),收到进程 A 的消息。此时进程 A 的状态改变为 REPLY 阻塞状态,直到收到一条对该消息的回答。这期间进程 B 不停止运行(注意,如果进程 B 在进程 A 发送消息前就已经调用了 Receive (),进程 B 在消息到达前将处于 RECEIVE 阻塞状态。在这种情况下,进程 A 则在发送消息后立即进入 REPLY 阻塞状态,而进程 B 则解除阻塞重新运行)。
- 进程 B 对从进程 A 收到的消息进行处理,然后调用 Reply () 函数进行回答。该回答消息被传送到进程 A,进程 A 将被调度继续运行。由于 Reply () 不会使进程停止而等待,所以进程 B 也将被调度继续运行。哪一个进程先运行将取决于进程 A 和进程 B 的优先级情况。
(2)进程的同步
消息传递不仅允许进程之间互相传递数据,而且提供一种使几个进程的运行同步的手段。让我们仍旧看一看上面的例子。进程 A 一旦发出了 Send 请求,在收到对该消息的回答之前将不再继续运行。这就确保了进程 B 为进程 A 执行的处理过程在进程 A 继续运行前能够全部完成。同样,一旦进程 B 发出了 Receive () 请求,在收到另一条消息之前进程 B 也将不再继续往下运行。
在 QNX 中,进程 B 调用 Receive() 时是否阻塞完全取决于消息是否已经到达。 如果消息已经等在队列里,Receive() 立即返回,B 不会阻塞;如果消息还没到,B 进入 RECEIVE 阻塞,直到有消息送来。
关于 QNX 调度进程的详细说明,可参见 "进程调度" 一章。
(3)阻塞状态
当一个进程由于必须等待消息协议的某些部分执行完毕而被禁止继续运行时,就可以说该进程处于阻塞状态。
表 3-2 列出了进程的各种阻塞状态。
| 如果一个进程发出了 | 该进程状态 |
|---|---|
| Send () 请求并且它所发送的消息尚未被接收进程收到 | SEND 阻塞 |
| Send () 请求,并且该消息已被接收进程收到但接收进程尚未给予回答 | REPLY 阻塞 |
| Receive () 请求,并且尚未收到任何消息 | RECEIVE 阻塞 |
这些状态之间的变换过程可见图 3-2:

图 3-2 在典型的发送-接收-回答事件中进程的状态变化情况
至于所有可能的进程状态细节情况,可参见 "进程管理器" 一章。
(4)使用 Send ()、Receive () 和 Reply ()
现在详细说明 Send ()、Receive () 和 Reply () 函数的调用方法。这里仍用上面进程 A 和进程 B 为例进行说明。
Send()
假定进程 A 通过发送一个请求向进程 B 发送消息。它将调用下面的 Send () 函数来发送该请求:
Send (pid, smsg, rmsg, smsg_len, rmsg_len);
下面是对调用参数的说明:
- pid:进程标识符,表示消息的接收进程(即进程 B);进程标识符是操作系统用于区别不同进程的标识符
- smsg:消息缓冲区(即保存待发送消息的缓冲区)
- rmsg:回答缓冲区(用于保存从进程 B 返回的回答消息)
- smsg_len:待发送消息的长度
- rmsg_len:进程 A 可以接收的回答消息的最大长度
注意:使用 Send 调用时,发送出去的消息长度将不会超过 smsg_len 个字节,接收时收到的消息长度也不会多于 rmsg_len 个字节,这样可以确保缓冲区的内容不会发生意外溢出。
Receive()
进程 B 可以通过用 Receive () 函数来接收进程 A 用 Send () 发来的消息:pid = Receive ( 0, msg, msg_len);
下面是调用参数的说明:
- 0:表示进程 B 准备接收来自任何进程的消息
- msg:用于接收消息的缓冲区
- msg_len:接收消息缓冲区的最大长度
如果发送方调用 Send () 时所指定的 smsg_len 与接收方调用 Receive () 时所指定的 msg_len 大小不同,则将使用两者中的较小者作为接收方接收缓冲区的最大数据长度。
Receive () 返回时,返回接收的消息的发送进程(即进程 A)的进程标识符 pid。
Reply()
进程 B 在成功地收到来自进程 A 的消息后进行相应的处理(执行相应的代码),然后应调用下面的 Reply () 函数来向进程 A 返回一个回答消息:Reply (pid, reply, reply_len);
下面是调用参数的说明:
- pid:接收该回答消息的进程(即进程 A)的标识符
- reply:回答消息缓冲区
- reply_len:回答消息缓冲区中待传输数据的长度
如果接收方调用 Reply () 时所指定的 reply_len 和发送方用 Send () 时指定的 rmsg_len 大小不同,将使用两者中的较小者作为最终传输字节个数的依据。
回答消息的内容一般为对进程 A 请求的处理结果(例如,进程 A 请求从数据库中查找数据,进程 B 根据请求进行查找,并将查找的结果回答给进程 A)。
上述的消息发送、接收、回答刻划了一个典型的客户 / 服务器的结构,发送消息的进程(进程 A)是一个客户进程,接收、回答的进程(进程 B)是一个服务器进程。
(5)由回答驱动的消息传递
前面的消息传递例子说明了消息传递中最通常的用法 ------ 服务器进程在提供服务过程中将总是使自己不断处于 RECEIVE 阻塞状态中,以便等待来自客户端的请求。在这期间,客户进程所发送的消息将启动服务器进程的一个动作的执行,服务器进程则完成该动作的执行过程并向客户进程发送回答消息。这种形式的消息传递被叫做由发送驱动的消息传递。
在 QNX 中,由发送驱动的消息传递意味着:服务器一直阻塞在 Receive() 中,只有当客户端 Send() 一条消息时,服务器才被唤醒执行相应工作。
另外,还有第二种形式的消息传递用法:由回答驱动的消息传递方式。这种方式虽然不及由发送驱动的消息传递使用得多,却也常常不失为一种可用的方式。在这种方式中,动作是由 Reply () 来启动的。用回答驱动方式,一个 "工作进程" 首先向服务器发送一条消息表示它可以开始工作了。服务器并不马上回答,而是仅仅 "记住" 该 "工作进程" 已发送过这一消息。然后该 "工作进程" 进入 REPLY 阻塞状态。在后来的某一时刻,当服务器需要某项结果时,就向这些可使用的 "工作进程" 发送回答消息,来要求 "工作进程" 执行某个动作。然后,该 "工作进程" 按服务器的要求执行所指定的动作,并通过向服务器发送一个消息提交执行的结果。
由回答驱动的消息传递:客户端(worker)通过 Send() 报告"我空闲了",然后阻塞等待。服务器稍后用 Reply() 分配任务,worker 被唤醒并执行任务。
图 3-3 中说明了回答驱动的消息传递的工作过程。图中有一个服务器进程和 n 个工作进程,工作进程通过 Send () 原语告诉服务器进程它可以工作,服务器登记下该工作进程,但并不马上回答,该工作进程处于 REPLY 阻塞状态。在某个时刻,服务器进程收到了客户进程的一条请求消息,服务器进程将此消息用 Reply () 原语发给某工作进程,该工作进程解除阻塞,按 Reply 消息中的要求执行某些动作,再用 Send () 原语把执行结果送给服务器进程。服务器进程则把该结果用 Reply () 原语送给客户进程。
图中的 1、2 等表示在这一过程中发生的动作以及动作发生的顺序:
- 工作进程向服务器进程发送消息,表示自己可工作,然后处于等待回答的阻塞状态。一个服务器进程可以有多个工作进程。
- 服务器进程登记工作进程,并继续运行。
- 客户进程向服务器进程发出请求,然后处于等待回答阻塞状态。
- 服务器进程从工作进程登记表中取一个等待回答的工作进程,用 Reply () 将客户进程请求发送给该工作进程,该工作进程解除阻塞,继续运行。服务器也继续运行。
- 工作进程执行的结果用 Send () 发送给服务器进程,服务器进程再登记该工作进程。该工作进程又处于等待回答的阻塞状态。
- 服务器进程用 Reply () 将结果发送给客户进程,客户进程解除阻塞继续运行,服务器也继续运行。

图3-3 使用工作进程执行用户请求的示意图
由上可见,回答驱动的消息传递方式适用于服务窗口一类应用。服务器进程作为一个窗口,连续快速地接收客户的请求,然后把请求分配给工作进程去处理,再把工作进程处理的结果回答给客户进程。根据系统的能力,设置一定数量的工作进程,超过这个数量的客户请求,就不再进行处理,而是回答客户进程系统现在忙,使系统保持适当的工作负荷。工作进程可分布在不同的节点机上,充分发挥分布系统的能力。
(6)附加说明
这里说明有关消息传递中的一些需要记住的事情:
- 消息数据将被保存在发送进程中直到接收进程准备接收该消息。内核中没有该消息数据的任何备份。这期间由于发送进程处于 SEND 阻塞状态,不会发生无意中误改消息数据的事情,因此不会有安全上的问题。
- 当 Reply () 请求被发送后,作为一个完整操作的一部分,内核将把消息回答数据从回答进程复制到被 REPLY 阻塞了的进程中。Reply () 的调用不会阻塞回答的进程,被 REPLY 阻塞了的进程在数据被复制到它的缓冲区后将恢复运行。
- 发送进程在发送消息前不需要知道接收进程的状态,如果接收进程在发送进程发送消息时未准备接收消息,此时发送进程将进入 SEND 阻塞状态。
- 一个进程在需要时可以发送一条长度为 0 的消息,或者发送一个长度为 0 的回答,或者既发送一条长度为 0 的消息又发送一个长度为 0 的回答。虽然消息没有携带任何有效数据,但这种消息仍然是有效的,它可以用来通知接收进程一些状态(例如"空白心跳")或用于某种控制信号。
- 从开发者的观点来看,通过向服务器进程发送一条 Send () 消息来获得一项服务实际上与通过调用一个函数库的子过程获得同一服务是等价的。两种情况中都需要建立一些数据结构再进行 Send () 或库函数调用;两者都需要等待调用的执行返回,当服务调用返回后,再取回返回结果,对错误条件、运行处理结果作进一步检查或做其它事情等。
- 一个进程可以接收来自多个进程的消息。一般情况下,接收进程将按照消息被发送的先后顺序依次接收消息,但系统也可规定接收进程按发送进程的优先级别来确定消息的接收顺序。

图3-4 服务器从客户F、客户C和客户B处接收了消息(此时尚未作回答),但还没有收来自客户A、客户D和客户E的消息
(7)高级的机制
QNX 提供下面这些高级的消息传递机制:
- 有条件地接收消息
- 读或写一条消息的部分内容
- 由多个部分构成的消息
有条件地接收消息
通常,当一个进程希望接收消息时,它使用 Receive () 来等待一条消息的到达。这是接收消息时最常用的方式,并适用于大多数环境。
然而,在有些情况下,进程虽然可能需要了解是否存在待读消息,但又不希望在待读消息尚未到达时被 RECEIVE 阻塞。例如,一个正在自由运转的设备由于不能产生中断,因此需要使用一个进程对该设备进行快速轮询,但与此同时该进程又必须及时响应来自其它进程的消息,这种情况下的进程应该使用 Creceive () 函数来读取消息,以便做到如有消息则读取之,如无则立即返回。
切记,由于 Creceive () 允许一个进程利用自己的优先级别连续地占用处理器时间,所以如有可能应尽量不用 Creceive ()。对上述例子,可用两个进程 A 和 B,进程 A 对设备进行快速轮询,进程 B 使用 Receive () 函数及时接收来自其它进程的消息。进程 A、B 间若要传送数据,则 A(或 B)先用信号(信号将在后面介绍)使 B(或 A)进入等待 A(或 B)发送消息的 RECEIVE 阻塞态,再用 Send () 函数发送数据消息。
读或写一条消息的部分内容
有时,人们希望一次只对消息的部分内容进行读或写,以便利用原先已为该消息分配的缓冲区空间,而不是重新另外分配一块工作空间。
例如,一个 I/O 管理器会收到一些要求写入数据的消息,这些写入数据由一个固定大小的头部后随一个长度可变的数据内容组成,并且头部部分给出了数据部分的字节个数信息(0~64kB 字节)。I/O 服务器可以只接收头部消息,然后使用 Readmsg () 函数直接将长度不固定的数据部分读到一个合适的输入缓冲区。如果该数据的长度超过了 I/O 管理器的缓冲区长度,该管理器会发送多个 Readmsg () 请求来分几次完成数据的传输。类似地,reply 进程也可使用 Writemsg () 函数来分几次传送数据,在 send 进程的 reply 缓冲区为空时将这些数据复制到该缓冲区,从而减少对 I/O 管理器的内部缓冲区大小的需求。
由多个消息部分构成的消息
直到现在,消息还是被看作一些字节的集合。然而,消息常常由两个或更多个消息部分组成。例如,一个消息可能有一个固定长度的头部后面紧随着一个可变长度的数据。为了使该消息的各组成部分无须复制到一个临时工作缓冲区即能迅速地发送或接收,可以利用两个或更多个消息缓冲区来构造一个多部分消息。这一机制可以帮助 QNX I/O 管理器(如 Dev 和 Fsys)实现高速率的存取。
下列函数对处理由多个消息部分构成的消息很有用:
Creceivemx ()
Readmsgmx ()
Receivemx ()
Replymx ()
Sendmx ()
Writemsgmx ()
plaintext
由多个消息分组组成的消息
┌─────────┐┌─────────┐ ┌─────────┐
│ 部分1 │ │ 部分2 │ ⋯⋯ │ 部分n │
└─────────┘└─────────┘ └─────────┘
▲ ▲ ▲
│ │ │
┌─────────┐┌─────────┐ ┌─────────┐
│控制块1 ││控制块2 │ ⋯⋯ │控制块n │
└─────────┘└─────────┘ └─────────┘
(m×控制结构)
图 3-5:可以使用 n 个 mx 控制结构来定义多部分消息,微内核将其组装成单个数据流
(8)保留的消息码
虽然用户未被要求这样做,但 QNX 使用一个被称作消息码的 16 位字作为它自己所有消息的开头。表 3-3 列出了 QNX 系统进程所使用的消息码的保留范围。
表 3-3 消息码
| 保留范围 | 描述 |
|---|---|
| 0x0000-0x00FF | 进程管理器消息 |
| 0x0100-0x01FF | I/O 消息(对所有 I/O 服务器通用) |
| 0x0200-0x02FF | 文件系统管理器消息 |
| 0x0300-0x03FF | 设备管理器消息 |
| 0x0400-0x04FF | 网络管理器消息 |
| 0x0500-0x0FFF | 为未来的 QNX 系统进程保留 |
3.2.2 通过代理进行进程间通信
一个代理是一种非阻塞式的消息,特别适用于事件通知亦即发送进程不需要与接收进程进行交互的场合。一个代理的唯一作用是向拥有该代理的特定进程传送一个预定消息。和消息一样,代理可通过网络进行工作。
通过使用代理,一个进程或一个中断处理程序可以在不被阻塞或不必等待回答的情况下向另一个进程发送消息。
下面是几个需要使用代理的例子:
- 一个进程希望通知另一个进程发生了一个事件,但又不能向后一进程发送消息(这是因为发送消息将引起发送进程被阻塞,并且要等到接收进程执行了一个 Receive () 和一个 Reply () 后发送进程的阻塞状态才会被解除)。
- 一个进程希望向另一个进程发送数据,但它既不需要对方进行回答也不需要知道对方是否收到了该消息。
- 一个中断处理程序希望向一个进程发送通知,告诉它现在可以对一些数据进行处理。
一个进程调用 qnx_proxy_attach () 函数便可以创建自己的代理。其它任何知道该代理的标识符的进程或中断程序都能够调用 Trigger () 来促使该代理发送已事先定义好的消息。微内核将负责处理 Trigger () 请求。
一个代理可以被触发多次,每触发一次都会引起它发送一条消息。一个代理进程最多可以积攒上 65535 条消息来发送。

(图 3-6:一个客户进程触发一个代理三次,导致服务器从该代理处收到三条 "已预备好" 的消息)
3.2.3 通过信号进行进程间通信
信号是一种已使用了几十年的被各种操作系统用于进程之间异步通信的传统方法。QNX 支持一个内容丰富的 POSIX 兼容的信号集合,以及部分传统的 UNIX 信号和 QNX 特有信号。
(1)信号产生和接收
可以使用实用程序或函数调用的方法来产生一个发送给进程的信号。一个进程可以向自己发送一个信号。
如果你希望从 Shell 中产生一个信号,则可调用 kill 或 slay 实用程序。如果你希望从一个进程内部产生一个信号,则可调用 C 函数 kill () 或 raise ()。
一个进程可以用下面三种方法之一来接收信号,用哪一种方法接收将取决于该进程对信号处理方式的设置。
-
如果该进程未对一个信号指定一个特殊处理动作,则信号发生时将采用缺省动作来处理该信号。通常情况下的缺省动作是结束该进程。
-
在 信号 机制中,"缺省动作" 是指当进程收到信号时,如果没有指定特别的处理方式,系统就会使用一个默认的动作来处理这个信号。例如:
SIGTERM(终止信号):如果进程没有自定义的处理方式,默认动作就是终止该进程。SIGSTOP:用于停止进程,这个信号无法被忽略。
意思就是,如果进程没有特别的处理方法来应对信号,那系统就会用默认的方式来处理它。
-
-
一个进程可以被设置成忽略一个信号。如果一个进程忽略一个信号,则该信号发生时将对该进程无任何作用(注意,SIGCONT、SIGKILL 和 SIGSTOP 信号通常是不可被忽略的)。
-
进程可以对一个信号提供一个信号处理程序 ------ 信号处理程序是位于进程中的一个函数,该函数在信号发生时被调用。当进程中包含了对信号的处理程序时,就说该进程具有捕捉信号的能力。进程捕捉到信号,实际上也意味着该进程接收到了一种软件中断。信号在传送过程中将不传递任何数据。
从产生到被提交这段时间,信号被说成是待决的("待决" (pending)是指信号已经发送,但还没有被进程处理的状态。)。在一个特定时刻,一个进程可以有多个不同的待决信号。当进程被微内核的调度程序设置为准备运行状态时,其所有待决信号将被提交给该进程。进程自己不能改变待决信号提交的顺序。
(2)信号描述
表 3-4 信号列表
| 信号名称 | 信号说明 |
|---|---|
| SIGABRT | 异常结束信号。可调用 abort() 函数来产生该信号 |
| SIGALRM | 超时信号。可调用 alarm () 函数来产生该信号 |
| SIGBUS | 发生内存校验错误(QNX 特有解释)。注意,如果在该信号处理程序正在处理该故障时再次发生这种故障将导致该进程结束运行 |
| SIGCHLD | 子进程结束。缺省动作是忽略该信号 |
| SIGCONT | 如果进程状态为 HELD 则继续运行。当该进程为非 HELD 状态时缺省动作是忽略该信号 |
| SIGDEV | 当设备管理器中发生一个重要的被请求事件时产生该信号 |
| SIGFPE | 错误的计算操作(整数或浮点):如被零除或操作导致溢出。注意,如果在该信号处理程序正在处理这种错误时再次发生这种故障将导致该进程结束运行 |
| SIGHUP | 检测到发起端对话中断或控制终端被挂起 |
| SIGILL | 检测到非法硬件指令。注意,如果在该信号处理程序正在处理这种错误时再次发生这种故障将导致该进程结束运行 |
| SIGINT | 人机交互中发出的(按 Break 键),引起主机注意的信号 |
| SIGKILL | 结束信号 ------ 仅应用于紧急事故状态。该信号不能被捕捉或忽略。注意,拥有超级用户权限的服务器可以调用 qnx_pflags () 函数来保护自己不受该信号作用 |
| SIGPIPE | 企图在没有接收者的情况下向一个管道(pipe)执行写操作 |
| SIGPWR | 通过调用 Ctrl-Alt-Shift-Del 或 Shut down 实用程序发出的软启动请求 |
| SIGQUIT | 通过交互发出的结束信号 |
| SIGSEGV | 检测到非法内存引用。注意,如果该信号处理程序正在处理这种错误时再次发生这种故障将导致该进程结束运行 |
| SIGSTOP | HOLD 处理信号。缺省动作是冻结该进程的运行。注意,拥有超级用户权限的服务器可以调用 qnx_pflags () 函数来保护自己不受该信号作用 |
| SIGTERM | 结束信号 |
| SIGTSTP | QNX 不支持 |
| SIGTTIN | QNX 不支持 |
| SIGTTOU | QNX 不支持 |
| SIGUSR1 | 被保留用作应用程序定义信号 |
| SIGUSR2 | 被保留用作应用程序定义信号 |
| SIGWINCH | 改变窗口大小 |
(3)信号处理方式
使用 ANSI C 的 Signal 函数或 POSIX 的 Sigaction 函数,可以在程序中根据需要来定义每个信号的默认、忽略、捕捉等处理方式(除了可被忽略、不可被捕捉的信号外)。
利用 sigaction () 函数可以在已有的信号处理环境之上实现更有效的控制。你可以在任何时间改变一类信号的处理方式。如果你将一个信号的处理方式设置为忽略,则该进程所有待决的这类信号将立即全被撤消。
- 默认处理:进程会根据该信号的类型执行操作,常见的默认动作包括终止进程、忽略信号等。
- 忽略信号:进程收到信号后什么也不做,直接忽略该信号。
- 自定义处理:通过自定义信号处理函数来响应信号,在信号发生时执行你指定的代码。
对利用自定义的信号处理函数来捕捉信号的进程,应注意下面的问题:
- 信号处理函数很像一个软件中断处理程序,它的执行与进程的其它部分是异步进行的,因此进程在运行的任何阶段中(包括对库函数的调用期间)随时可能会进入信号处理程序。信号处理程序执行结束后一般返回到中断点继续运行被中断的进程。
- 若你不想从信号处理程序返回到被中断的进程,则可以用 siglongjmp () 或 longjmp () 来离开信号处理程序。但使用 siglongjmp () 会更好一些,使用 longjmp () 时,信号将被保持为阻塞状态。
(4)阻塞信号
有时,用户可能希望对信号暂时不予提交,但不改变该信号被提交后的处理方法。QNX 提供一组暂缓提交信号的函数。暂缓提交的信号称为被阻塞的信号,一个被阻塞的信号保持为待决状态,一旦被解除阻塞,将立即被提交给进程。
当进程正在对一个特定信号执行一个信号处理程序时,QNX 将自动阻塞后来提交的同样信号,这意味着你不必担心会形成信号处理的嵌套调用。对后来提交的同类信号来说,信号处理程序的每一次调用都将是一个不可被中断的完整操作过程(原子操作)。如果进程从信号处理程序正常返回,待决信号将自动被解除阻塞。
注意,一些 UNIX 系统对信号处理程序的实现有一点缺陷。对上述情况,它们不是阻塞信号而是将信号处理方式重新设置为缺省动作,结果导致一些 UNIX 应用程序需要从信号处理程序内部调用 signal () 函数来重新启用信号处理程序。这种做法有两个问题:一,如果另一信号到达时,该进程正在执行信号处理程序并且尚未调用 signal (),在这种情况下你的程序将被强迫终止运行;二,如果在信号处理程序刚刚调用了 signal () 但还没结束当前的处理程序时该信号就到达了,你的程序会因此进入对信号处理程序的嵌套调用状态。QNX 支持信号阻塞功能,从而避免了这些问题,并使你不必在信号处理程序中调用 signal ()。如果你想通过长距离跳转方式离开信号处理程序,可以通过用 siglongjmp () 函数来实现跳转。
(5)信号和消息
信号和消息之间有一种重要的相互作用。如果一个信号产生时进程正处于 SEND 阻塞或 RECEIVE 阻塞状态,并且已指定了对该信号的处理程序,按照信号的规则将会出现如下动作:
- 该进程被解除阻塞,继续运行。
- 执行信号处理过程。
- 从 Send () 或 Receive () 返回时提示出错。
如果进程正处于 SEND 阻塞状态,由于接收进程此时实际上已无法收到消息,因此不会带来什么问题。但是,如果信号产生时,进程正处于 REPLY 阻塞状态,若按上述动作处理则会出现问题:该进程将不能知道被发送的消息是否已得到处理,因而也不能知道是否要重新尝试调用 Send ()。
QNX 对这一问题提出了自己的解决办法,下面对此作一简单说明。
QNX 把一个接收消息的进程看作一个服务器进程,把一个发送消息的进程看作一个客户进程。当客户进程在处于 REPLY 阻塞状态的情况下收到信号时,内核将把这一情况以及信号的类型及时通知到对应的服务器进程,同时客户进程将被内核设为 SIGNAL 阻塞状态,并使原先收到的信号成为待决信号。然后,它的服务器进程可以在下面两件事之间选择执行:
- 正常地响应原先的请求,使客户进程(亦即发送进程)的消息得到相应的处理。
- 释放所有有关资源,并返回一个指明客户进程已被一个信号解除阻塞的出错码,使客户进程能收到一个 "清除错误" 指示。
然后,当服务器进程向曾经被 SIGNAL 阻塞了的客户进程作 REPLY 回答的时候,客户进程将恢复运行,并且在从 Send () 返回后进入对该信号的处理过程。
图 3-7 说明信号对消息传递过程的影响,图中的 1、2 等表示所采取的动作以及动作的顺序。

- 进程 A 向进程 B 发送消息。进程 B 接收了消息,进程 A 等待回答(处于回答阻塞状态)。
- 进程 B 继续执行。
- 进程 A 被一个信号作用,变成 SIGNAL 阻塞状态,该信号被设为待决状态。(SIGNAL 阻塞只是信号到来的瞬时状态。内核随后把 A 放到 Reply 阻塞,让 B 强制回复异常信息。B 回复后,A 才结束 Send 并执行信号处理。)
- 内核发送一个关于信号的消息(也称信号消息),告诉进程 B:进程 A 被信号阻塞。进程 B 接收 "信号消息" 后,进程 A 变成 Reply () 阻塞状态。
- 进程 B 释放所有资源,并将 "客户进程被信号作用" 的 REPLY 回答信息返回给进程 A。
- Send () 返回,然后响应信号,执行信号处理过程。
3.2.4 通过信号灯进行进程间通信
当多个进程同时要占用相同的资源时,就需要使用一种机制来协调它们。举例来说,我们有一个资源,如打印机,一个进程正在使用它,另一个进程就不能在此时向它发送数据,否则,打印出的结果就是混乱的。为避免出现混乱,我们使用一个变量 P,当它为 1 时,说明打印机可以使用(未被其他进程占用);当它为 0 时,说明打印机不可使用(已被占用)。
每个进程在使用打印机前,都要先查看一下变量 P。若 P=0,则等待;若 P=1,则将 P 置为 0,然后使用打印机。打印结束了再将 P 置为 1。
这样做似乎可以解决问题,但仔细一想就会发现毛病:如果当进程 A 查看变量 P 时,得知变量 P 的内容为 1;但此时进程 A 被调度暂停运行,进程 B 开始运行,也查看 P,也得知变量 P 的内容为 1,进程 B 于是将 P 置为 0,开始打印,此时,进程 A 又被调度运行,进程 A 也将把 P 置为 0,并开始打印。在这种情形下,两个进程将同时占用打印机,变量 P 没有起到协调的作用。
分析出现毛病的原因是由于:查看 P 是否为 1 和将 P 置为 0 两个操作是可分割的。
若操作系统能提供不可分割的原语,使进程把对一个变量的多个操作当做一个不可分割的过程来进行,就可以起到在共享资源的进程之间进行协调的作用,从而解决上述问题。信号灯所提供的正是这种机制。
信号灯是一种常用的进程间同步彼此动作的通信工具,它允许进程在信号灯上进行 "供给"(sem_post ())和 "等待"(sem_wait ())操作,来控制进程的唤醒和睡眠。"供给" 操作使信号灯的值增加,"等待" 操作使信号灯的值减少。
如果进程对一个值为正的信号灯进行 sem_wait () 操作,则该进程不会被阻塞;如果它是对一个值为 0 或负的信号灯进行 sem_wait () 操作,则该进程就会被阻塞,并且直到其它进程执行了 sem_post () 操作后才能恢复运行。一个信号灯可以在处于等待状态之前被进程多次调用 sem_post () 实行多次供给,以便允许一个或更多个进程执行 sem_post () 操作而又不被阻塞。
注意:sem_post () 操作使信号灯的值增加,并且会唤醒等待此信号灯的睡眠进程,如果有多个进程在等待此信号灯,则唤醒优先级最高的睡眠进程;若有多个具有同样的最高优先级的睡眠进程,则唤醒等待时间最长的进程。
信号灯可在内核中实现也可在内核外实现,本系统是在内核外实现信号灯的。此外,共享内存、消息队列、文件锁、记录锁等进程间通信方法都是在本系统内核外实现的。
3.3 网络中 QNX 进程间的通信
3.3.1 虚拟电路
一个 QNX 应用进程能够像与本地进程进行通信一样与网络中另一台计算机上的 QNX 进程进行通信。实际上,从 QNX 应用程序的角度来看,本地的资源与网络中的资源并没有什么区别。
这种令人惊奇的透明性是通过虚拟电路(VC)来实现的。虚拟电路是由网络管理器提供的一种在网络中提交消息、代理和信号的手段。
出于下面几个原因,虚拟电路充分而有效地提供了利用网络所有资源的能力:
- 当建立一条虚拟电路时,QNX 允许对处理消息所使用的长度给予定义,这意味着可以用预先分配资源的办法处理消息。当用户需要发送的消息长度超过规定长度时,虚拟电路将自动调整消息的最大处理长度来适应新的消息处理需要。
- 如果驻留在不同节点上的两个进程需要通过一条以上的虚拟电路进行相互通信,则 QNX 将使用共享虚拟电路来解决这个问题,使这两个进程之间实际上只存在一条虚拟电路。这种情形通常出现在一个进程访问一个远程文件系统中多个文件的时候。
- 如果一个进程被连接到一个已存在的共享虚拟电路上,并且该进程请求使用一个比当前所用缓冲区更大的缓冲区空间,则缓冲区长度将按新的需求自动得到调整。
- 当一个进程结束时,它所使用的虚拟电路将自动被释放。
3.3.2 虚拟进程
一个发送进程负责在自己和通信对方进程之间建立虚拟电路。发送进程通常可调用 qnx_vc_attach () 函数来完成这一工作。该调用除了创建虚拟电路外,还会在虚拟电路的每一端建立一个虚拟进程标识符(VID)。对虚拟电路两端的进程来说,自己这端的 VID 看起来就像它希望与之通信的远程进程的进程标识符。两个节点上的进程就通过这些 VID 相互进行通信。
例如,在下面的说明中,一条虚拟电路将 PID1 连接到 PID2。在 PID1 所在的节点 20 上,VID2 代表 PID2,在 PID2 所在的节点 40 上,VID1 代表 PID1。PID1 和 PID2 都可以像和本地其它进程打交道一样和自己所在节点上的 VID 打交道(如发送消息、接收消息、使用信号和等待等等)。例如,PID1 可以向自己一端的 VID2 发送一条消息,而 VID2 将该消息经由网络转送到代表 PID1 的另一端的 VID1。然后,另一端的 VID1 再将消息送到 PID2 处。
(图 3-8:使用虚拟电路处理网络通信。当 PID1 向 VID2 发送消息,发送的请求通过虚拟电路,再由 VID1 发向 PID2)
plaintext
节点20 节点40
Send() 虚拟电路 Send()
PID1 ------→ VID2 ------→ VID1 ------→ PID2
我们可以通过"替身"的概念来理解这个逻辑:
- 在节点 20 上(PID1 的家):
- PID1 想给远处的 PID2 发消息。
- 但是 PID2 不在本地,PID1 够不着。
- 因此,系统在节点 20 上放了一个 PID2 的"替身",名字叫 VID2(即代表 PID2 的虚拟 ID)。
- PID1 只需要把消息发给这个替身 VID2 即可。
- 对应图示左半部分:
PID1 ------→ VID2
- 在节点 40 上(PID2 的家):
- 网络把消息传过来了,需要递交给 PID2。
- 对于 PID2 来说,它接收消息时,系统希望它感觉像是从本地收到的。
- 因此,系统在节点 40 上放了一个 PID1 的"替身",名字叫 VID1(即代表 PID1 的虚拟 ID)。
- 这就好比是替身 VID1 亲手把消息交给了 PID2。
- 对应图示右半部分:
VID1 ------→ PID2
总结一下命名逻辑:
- VID2 = PID2 的影子(所以它必须在 PID1 那边,让 PID1 能看见)。
- VID1 = PID1 的影子(所以它必须在 PID2 那边,让 PID2 能看见)。
每个 VID 将维持一条包含如下信息的连接:
- 本地进程标识符(PID)
- 远程进程标识符(RPID)
- 远程节点标识符(RNID)
- 远程虚拟进程标识符(RVID)
通常,用户不需要直接和虚拟电路打交道。比如,当一个应用程序想要通过网络存取一个 I/O 资源时,函数库中的 open () 函数将替应用程序创建一条虚拟电路,而应用程序在创建和使用该虚拟电路的过程中无须与该虚拟电路直接发生联系。再举一个例子,当一个应用程序调用 qnx_name_locate () 来寻找一个服务器所在位置时,qnx_name_locate () 函数将替应用程序建立一条虚拟电路。对该应用程序来说,这条虚拟电路看上去就和一个 PID 一样。
关于 qnx_name_locate () 的更多的信息可参见有关 "进程符号名" 的讨论。
3.3.3 虚拟代理
就像一条允许进程同远程节点交换消息的虚拟电路一样,一个虚拟代理允许从远程节点触发一个代理。
与虚拟电路不同的是,虚拟电路将固定的两个进程捆绑一起,而虚拟代理允许远程节点上的任何进程触发它。
虚拟代理可用 qnx_proxy_rem_attach () 来创建,该函数使用节点标识符(nid_t)和代理标识符(pid_t)作为调用参数。在远程节点中创建一个虚拟代理时,该虚拟代理将与调用者所在节点上的代理进行交互通信。注意,qnx_proxy_rem_attach () 将在调用者所在节点上自动创建虚拟电路。
(图 3-9:在远程节点上建立一个虚拟代理,该虚拟代理将和调用者所在节点上的代理通信)
节点 20 节点 40
.---------------------------. .---------------------------.
| | | |
| +----------+ | | +----------+ |
| | 调用者 | | | | 远程进程 | |
| +----------+ | | +----------+ |
| | | | ^ |
| | Send() | | . |
| v | | . |
| +----------+ | | . Trigger() |
| | 代理 | | | . |
| +----------+ | | . |
| | | | . |
| | Trigger() | | . |
| v | | . |
| /---------\ | 虚拟电路 | /---------\ |
| ( VID ) ==========================> ( 虚拟代理 ) |
| \---------/ | | \---------/ |
| | | |
'---------------------------' '---------------------------'
图解说明:
- 节点 20 (左侧) :
- 调用者:发起请求的进程。
- Send() :调用者通过
Send()方法将请求发送给本地的"代理"。 - 代理:本地的代理对象,负责封装请求。
- Trigger():代理触发操作,将数据传递给底层的 VID。
- VID:虚拟 ID(Virtual ID),作为通信的出口点。
- 虚拟电路 (中间) :
- 连接两个节点的粗箭头,表示数据传输的通道。
- 节点 40 (右侧) :
- 虚拟代理/端口(右下圆圈):接收来自虚拟电路的数据。
- Trigger():通过虚线向上触发,将数据传递给目标进程。
- 远程进程:最终接收消息的目标进程。
3.3.4 结束虚拟电路
下面的几个原因将导致进程不能使用一条已建好的虚拟电路进行通信:
- 运行该进程的计算机被关机
- 计算机与网络的电缆连接被断开
- 通信对方进程已被停止运行
上述任何情况的存在都会使进程不能通过虚拟电路传递消息。因此,应该经常检查是否存在这些情况,以使应用程序能够采取相应的动作或适当地停止运行。如果不这样做,就可能使有用资源陷入不必要的挂起局面。
每个节点上的进程管理器将负责检查本节点上虚拟电路的完整性,方法是:
- 每当在一条虚拟电路上成功地完成了一个消息的传送时,与该虚拟电路有关的时间邮戳将得到修改,以表明最后一次动作时间。
- 每隔一定时间(该时间段的值可在安装时设定),进程管理器对每条虚拟电路检查一次。如果此期间在某条虚拟电路上尚未进行过任何消息传送,则进程管理器将向该虚拟电路的另一端节点上的进程管理器发送一个用于测试网络完整性的数据包。
- 如果无任何回答消息,或者回答有问题,则该虚拟电路将被标注为存在问题。尝试一定次数(在安装时设定该次数值)仍然如此时,将重新建立该虚拟电路。
- 所有的尝试失败后,虚拟电路被拆除,在该虚拟电路上被阻塞的所有进程被解除阻塞并设为 READY 状态(进程中,与该虚拟电路有关的通信原语的调用被返回一个出错码)。
可以调用 netpoll 实用程序来控制与上述完整性检查有关的各种参数。