QNX IPC机制
QNX相关历史文章:
介绍
Interprocess Communication(IPC,进程间通信)在QNX Neutrino从一个嵌入式实时系统向一个全面的POSIX系统转变起着至关重要的作用。IPC是将在内核中提供各种服务的进程内聚在一起的粘合剂。在QNX中,消息传递是IPC的主要形式,也提供了其他的形式,除非有特殊的说明,否则这些形式也都是基于本地消息传递而实现的。QNX Neutrino提供以下形式的IPC:
IPC
Synchronous message passing
一个线程调用MsgSend()
往目标线程发送消息时会阻塞住,直到目标线程调用MsgReceive()
,进行消息处理并调用MsgReply()
回复后才会解除阻塞。
在QNX Neutrino中,服务器线程通常是循环的,等待接收客户端发过来的消息。可以看看客户端线程和服务器线程在消息传递过程中的状态变化:
- 客户端线程
- 客户端线程调用
MsgSend()
后,如果服务器线程还没调用MsgReceive()
,客户端线程状态则为SEND blocked
,一旦服务器线程调用了MsgReceive()
,客户端线程状态变为REPLY blocked
,当服务器线程执行MsgReply()
后,客户端线程状态就变成了READY
; - 如果客户端线程调用
MsgSend()
后,而服务器线程正阻塞在MsgReceive()
上, 则客户端线程状态直接跳过SEND blocked
,直接变成REPLY blocked
; - 当服务器线程失败、退出、或者消失了,客户端线程状态变成
READY
,此时MsgSend()
会返回一个错误值。
- 服务器线程
- 服务器线程调用
MsgReceive()
时,当没有线程给它发送消息,它的状态为RECEIVE blocked
,当有线程发送时变为READY
; - 服务器线程调用
MsgReceive()
时,当已经有其他线程给它发送过消息,MsgReceive()
会立马返回,而不会阻塞; - 服务器线程调用
MsgReply()
时,不会阻塞;
Message copying
QNX的消息服务,是直接将消息从一个线程的地址空间拷贝到另一个线程地址空间,不需要中间缓冲,因此消息传递的性能接近底层硬件的内存带宽。消息内容对内核来说没有特殊的意义,只对消息的发送和接收者才有意义,当然,QNX也提供了定义良好的消息类型,以便能扩充或替代系统提供的服务。
消息在拷贝的时候,支持分块传输,也就是不要求连续的缓冲区,发送和接收线程可以指定向量表,在这个表中去指定消息在内存中的位置。这个与DMA的scatter/gather
机制类似。
分块传输也用在文件系统中,比如读数据的时候,将文件系统缓存中的数据分块读到用户提供的空间内,如下图:
对于简单的单块消息传递,就不需要通过IOV
(input/output vector)的形式了,直接指向缓冲区即可。对于发送和接收的接口,多块发送和单块发送如下:
Channels and connections
在QNX Neutrino中,消息传递是面向通道(channel)和连接(connection)的,而不是直接从线程到线程的。接收消息的线程需要创建一个channel
,发送消息的线程需要与该channel
建立connection
。
服务器使用MsgReceive()
接收消息时需要使用channels
,客户端则需要创建connections
,以连接到服务器的通道上,连接建立好之后,客户端便可通过MsgSend()
来发送消息了。如果进程中有很多线程都连接到一个通道上,为了提高效率,这些所有的连接都会映射到同一个内核对象中。在进程中,channels
和connecttions
会用一个小的整型标识符来标记。客户端connections
会直接映射到文件描述符,在架构上这是一个关键点,可以消除另一层转换,不需要根据文件描述符来确定往哪里发消息,而是直接将消息发往文件描述符即可。
Connections map elegantly into file descriptors
有几个与channel
有关联的列表:
- Receive,等待消息的LIFO线程队列;
- Send,已发送消息但还未被接收的优先级FIFO线程队列;
- Reply, 已发送消息,并且已经被收到,但尚未回复的无序线程列表;
不管在上述哪个列表中,线程都是阻塞状态,多个线程和多个客户端可能等待在同一个channel
上。
threads blocked while in a channel queue
Pulses
除了同步发送/接收/回复服务外,QNX还支持固定大小的非阻塞消息,这种消息被称为Pulse
,携带一个小的负载(四个字节数据,加一个字节的代码)。Pulse
通常被用在中断处理函数中,用作通知机制;也允许服务器在不阻塞客户端的情况下,向客户端发送信号。
优先级继承与消息
服务器进程按照优先级顺序来接收消息和脉冲,当服务器中的线程接收请求时,它们将继承发送线程的优先级。请求服务器工作的线程的优先级被保留,服务器工作将以适当的优先级执行,这种消息驱动的优先级继承避免了优先级反转的问题。
Message-passing API
Robust implementations with Send/Receive/Reply
异步系统的一个重要问题是事件通知需要运行信号处理程序。异步IPC难以彻底对系统进行测试,此外也难以确保信号处理程序按预期的运行。基于Send/Receive/Reply
构建的同步、非队列系统结构,可以让应用程序的架构更健壮。
在使用各种IPC机制时,避免死锁是一个难题,在QNX中只需要遵循两个原则,就可以构建无死锁系统:
- 永远不要两个线程相互发送消息;
- 将线程组织为层级结构,并只向上发送消息;
Threads should always send up to higher-level threads
上层的线程可以通过MsgSendPulse()
或MsgDeliverEvent()
来传递非阻塞消息或事件:
A higher-level thread can "send" a pulse event
Events
QNX Neutrino提供异步事件通知机制,事件源可能有三种:
- 调用
MsgDeliverEvent()
接口发送事件 - 中断处理函数
- 定时器到期
事件本身可以有多种类型:Pulse
、中断、各种形式的信号、强制解除阻塞的事件等。
考虑到事件本身的多样性,服务器实现所有的异步通知显然不太合适,更好的方式是客户端提供一个数据结构或者cookie
,服务器调用MsgDeliverEvent()
时将事件类型写进cookie
中。
The client sends a sigevent to the server
ionotify()
函数是客户端线程请求异步事件通知的一种方式,许多POSIX异步服务都基于这个之上来构建的,比如mq_notify
和select
等。
Signals
信号类似于软中断,QNX支持的信号如下:
QNX Neutrino扩展了信号传递机制,允许信号针对特定的线程,而不是简单的针对包含线程的进程。由于信号是异步事件,它们通过事件传递机制实现。接口如下:
Signal delivery
当一个服务器线程想通知一个客户端线程时,有两种合理的事件选择:
Pulse
或信号
-
Pulse
,需要客户端创建一个channel
,并且调用MsgReceive()
接收; - 信号,只需要调用
sigwaitinfo()
,不需要创建channel
;
POSIX message queues
POSIX通过message queues
定义一组非阻塞的消息传递机制。消息队列为命名对象,针对这些对象可以进行读取和写入,作为离散消息的优先级队列,消息队列具有比管道更多的结构,为应用程序提供了更多的通信控制。QNX Neutrino内核不包含message queues
,它的实现在内核之外。
QNX Neutrino提供了两种message queues
的实现:
- mqueue,使用mqueue资源管理的传统实现
- mq,使用mq服务和非同步消息的替代实现
QNX消息机制与POSIX的Message queues
有一个根本的区别:,QNX的消息机制通过内存拷贝来实现消息的传递;而POSIX的消息队列通过将消息进行存取来实现消息的传递。QNX的消息机制比POSIX的消息队列效率更高,但有时为了POSIX的灵活,需要适当的牺牲一点效率。
消息队列与文件类似,操作的接口相近。
Shared memory
共享内存提供了最高带宽的IPC机制,一旦创建了共享内存对象,访问对象的进程可以使用指针直接对其进行读写操作。共享内存本身是不同步的,需要结合同步原语一起使用,信号量和互斥锁都适合与共享内存一块使用,信号量一般用于进程之间的同步,而互斥锁通常用于线程之间的同步,通通常来说互斥锁的效率会比信号量要高。
共享内存与消息传递结合起来的IPC机制,可以提供以下特点:
- 非常高的性能(共享内存)
- 同步(消息传递)
- 跨网络传递(消息传递)
QNX中消息传递通过拷贝完成,当消息较大时,可以通过共享内存来完成,发送消息时不需要发送整个消息内容,只需将消息保存到共享内存中,并将地址传递过去即可。
通常会使用mmap来将共享内存区域映射到进程地址空间中来,如下图所示:
Arguments to mmap
Typed memory
类型化内存是POSIX规范中定义的功能,它是高级实时扩展的一部分。
POSIX类型化内存,提供了一个接口来打开内存对象(以操作系统特定的方式定义),并对它们执行映射操作。这个对提供BSP/板级特定的地址布局与设备驱动或用户代码之间的抽象时非常有用。
Pipes and FIFOs
管道是一种非命名IO通道,用于在多个进程之间的通信,一个进程往管道写,其他进程从管道读取。管道一般用于平行的两个进程单向的传递数据,如果要双向通信的话,就应该使用消息传递了。
FIFOs与管道本质是一样的,不同点在于FIFOs会在文件系统中保存为一个永久的命名文件。