QNX IPC机制

QNX微内核介绍

QNX基础知识

IPC进程间通信

IPC在QNX Neutrino (微内核) 从一个嵌入式实时系统向一个全面的POSIX系统转变起着至关重要的作用
在QNX中,消息传递是IPC的主要形式,也提供了其他的形式,除非有特殊的说明,否则这些形式也都是基于本地消息传递而实现的。QNX Neutrino提供以下形式的IPC:
IPC

Channels and connections

IPC中消息传递是基于CS架构实现的,最简单的,制定对方进程号,要发送的一方,将消息加一个头,告诉内核把该消息发送给 pid12345就行了,其实QNX4的时候时这么做的. 但是

  1. 如果服务器有两个线程,分别进行不同的服务, 你或者可以把消息发送给 pid 12345 tid3 就可以, 但如果某一服务,由一组线程进行,就没办法了

  2. 为此, QNX6就抽象出了Channel这个概念, 一个Channel,就是一个服务的入口, 至于这个channel到底有多少线程为其服务, 由server负责 ChannelId = ChannelCreate(flags)

  3. 一个server可以有多个channels, 而客户端再向频道发送消息前, 需要先建立连接 Connection, 然后再将消息在connection上发送出去, 如果需要, 一个频道可以建立多个连接. ConnectionId = ConnectAttach(Node, Pid, Chid, Index, Flag)

API create/attach destory/detach
Server Chid = ChannelCreate(flags) ChannelDestroty(Chid)
Client Coid = ConnectAttach(node, pid, chid, _NTO_SIDE_CHANNEL, flags) ConnectDetach(Coid )
  • Node: server端机器号, 如果是通过透明分布式网络是这个值决定了哪一台机器; 如果客户端和服务器端在同一台机器时用数字0或者ND_LOCAL_NODE
  • pid: process id
  • chid: connection id

在QNX Neutrino中,消息传递是面向通道(channel)和连接(connection)的,而不是直接从线程到线程的。接收消息的线程需要创建一个channel,发送消息的线程需要与该channel建立connection.
服务器使用MsgReceive()接收消息时需要使用channels,客户端则需要创建connections,以连接到服务器的通道上,连接建立好之后,客户端便可通过MsgSend()来发送消息了。如果进程中有很多线程都连接到一个通道上,为了提高效率,这些所有的连接都会映射到同一个内核对象中。在进程中,channels和connecttions会用一个小的整型标识符来标记。客户端connections会直接映射到文件描述符,在架构上这是一个关键点,可以消除另一层转换,不需要根据文件描述符来确定往哪里发消息,而是直接将消息发往文件描述符即可
Channel & Connect
在这里插入图片描述

与channel有关联的列表:

Receive,等待消息的LIFO线程队列;
Send,已发送消息但还未被接收的优先级FIFO线程队列;
Reply, 已发送消息,并且已经被收到,但尚未回复的无序线程列表;
不管在上述哪个列表中,线程都是阻塞状态,多个线程和多个客户端可能等待在同一个channel上
在这里插入图片描述

Synchronous message passing

比较传统的IPC方式是基于主从式架构(CS), 并且是双向通信
在这里插入图片描述
发送(Send),接收(Receive)和应答(Reply) SRR过程

API client server
send MsgSend(ConnectionId, SendBuf, SendLen, ReplyBuf, ReplyLen)
receive ReceiveId = MsgReceive(ChannelId, ReceiveBuffer, ReceiveBufLength, &MsgInfo)
reply MsgReply(RceiveId, ReplyStatus, ReplyBuf, ReplyLen)

MsgReceive中第四个参数的类型为 struct _msg_info info 用于处理可变的信息长度
MsgRead() 与 MsgWrite(rcvid, buffer, len, offset) 用于客户端与服务端分块处理较大的信息

客户端

当客户端建立connection到channel后,

  1. client调用MsgSend(), 其状态将变为阻塞blocked状态:
    如果server还没调用MsgReceive(), client线程状态则为SEND blocked
    一旦server调用了MsgReceive(), client线程状态变为REPLY blocked, 同时将SendBuf里的东西复制到ReceiveBuf中
    当server线程执行MsgReply(), client线程状态就变成了READY

  2. 如果client线程调用MsgSend()后,而server线程正阻塞在MsgReceive()上, 则client线程状态直接跳过SEND blocked,直接变成REPLY blocked;

  3. 当server线程失败、退出、或者消失了,client线程状态变成READY,此时MsgSend()会返回一个错误值。

客户端
客户端在频道上进行接收,

  1. server线程调用MsgReceive()时,当没有线程给它发送消息,它的状态为RECEIVE blocked,当有线程发送时变为READY
  2. server线程调用MsgReceive()时,当已经有其他线程给它发送过消息,MsgReceive()会立马返回,而不会阻塞;
  3. server线程调用MsgReply()时,不会阻塞;
    应答时,可以调用MsgError()告诉发送方有错误发送,MsgError(rcvid, EINVAL), MsgReply(rcvid, EINVAL, 0, 0), MsgSend()返回-1,而errno被设为EINVAL

Message copying

QNX的消息服务,是直接将消息从一个线程的地址空间拷贝到另一个线程地址空间,不需要中间缓冲,因此消息传递的性能接近底层硬件的内存带宽。消息内容对内核来说没有特殊的意义,只对消息的发送和接收者才有意义.
当然,除了用线性的缓冲区进行消息传递以外,QNX也提供了定义良好的消息类型iov_t来"汇集"数据,以便能扩充或替代系统提供的服务。
消息在拷贝的时候,支持分块传输,也就是不要求连续的缓冲区,发送和接收线程可以指定IOV向量表,在这个表中去指定消息在内存中的位置。这个与DMA的scatter/gather机制类似multipart transfer
分块传输也用在文件系统中,比如读数据的时候,将文件系统缓存中的数据分块读到用户提供的空间内,如下图:
在这里插入图片描述

对于简单的单块消息传递,就不需要通过IOV(input/output vector)的形式了,直接指向缓冲区即可。对于发送和接收的接口,多块发送和单块发送如下:
在这里插入图片描述
在这里插入图片描述
举个例子:
“header” 与 “databuf” 是不连续的两块数据,
虽然在客户端的Header同databuf是两块不相邻的内存,但传递到服务器端的ReceiveBuffer里,就是连续的了。也就是说在服务器端,要想得到原来databuf里的数据,只需要(ReceiveBuffer + sizeof(header))就可以了。(要注意数据结构对齐)

// 户端:		"header" 与 "databuf" 是不连续的两块数据
SETIOV(&iov[0], &header, sizeof(header));
SETIOV(&iov[1], databuf, datalen);
MsgSendvs(ConnectionId, iov, 2, Replybf, ReplyLen);
// 服务器端:	"header"与"databuf"被连续地存在ReceiveBuffer里
ReceiveId = MsgReceive(ChannelId, ReceiveBuffer, ReceiveBufLength, &MsgInfo); 
header = (struct header *)ReceiveBuffer;
databuf = (char *)((char *)header + sizeof(*header));

客户端与服务器端的最基本的通信
分片处理大长度信息

Pulses

脉冲其实更像一个短消息,也是在“连接Connection”上发送的。脉冲最大的特点是它是异步的。发送方不必要等接收方应答,直接可以继续执行。
但是,这种异步性也给脉冲带来了限制。脉冲能携带的数据量有限,只有一个**8位的"code"域 (1byte)用来区分不同的脉冲,和一个32位的“value"域 (4字节)**来携带数据。脉冲最主要的用途就是用来进行“通知”(Notification)。不仅是用户程序,内核也会生成发送特殊的“系统脉冲”到用户程序,以通知某一特殊情况的发生。
在这里插入图片描述

pulse API send receive
MsgSendPulse(coid, priority, code, value) MsgReceivePulse()
MsgDeliverEvent() MsgReceive()

MsgSendPulse() 只在一个进程中的通知,用与同一个进程中一个线程要通知另一个线程的情形, 其中 code 8bits; value 32bits
MsgDeliverEvent() 在跨进程的时候的通知
MsgReceivePulse() 用于频道上只有pulse的接收
MsgReceive() 用于频道上既接收message又接收pulse

脉冲的接收比较简单,如果你知道频道上不会有别的消息,只有脉冲的话,可以用MsgReceivePulse()来只接收脉冲;
如果频道既可以接收消息,也可以接收脉冲时,就直接用MsgReceive(),只要确保接收缓冲(ReveiveBuf)至少可以容下一个脉冲(sizeof struct _pulse)就可以了。
在后一种情况下,如果MsgReceive()返回的rcvid是0,就代表接收到了一个脉冲,反之,则收到了一个消息。所以,一个既接收脉冲,又接收消息的服务器,可以是这样的
struct _pulse 定义

union {
    
    
    struct _pulse pulse;
    msg_header   header;
} msgs;
if ((rcvid = MsgReceive(chid, &msgs, sizeof(msgs), &info)) == -1) {
    
    
    perror("MsgReceive");
    continue;
}
if (rcvid == 0) {
    
    
// 此时为pulse信号
    process_pulse(&msgs, &info);
} else {
    
    
    process_message(&msgs, &info);
}

脉冲的发送,最直接的就是MsgSendPulse()。不过,这个函数通常只在一个进程中,用在一个线程要通知另一个线程的情形。
在跨进程的时候,通常不会用到这个函数,而是用到下面将要提到的 MsgDeliverEvent()
与消息传递相比,消息传递永远是在进程间进行的。也就是说,不会有一个进程向内核发送数据的情形, 而脉冲就不一样,除了用户进程间可以发脉冲以外,内核也会向用户进程发送“系统脉冲”来通知某一事件的发生。

MsgDeliverEvent()

在现实情况中,客户端与服务器端并不是很容易区分开来的。有的服务器端为了处理客户端的请求,本身就需要向别的服务器发送消息;有的客户端需要从不同的服务器那里得到服务,而不能阻塞在某一特定的服务器上;还有的时候,两个进程间的数据是互相流动的

也许有人认为,两个进程互为通讯就可以了。每个进程都建立自己的频道,然后都与对方的频道建一个连接就好了;这样,需要的时候,就可以直接通过连接向对方发送消息了。就好象管道(pipe)或是socketpair一样。请注意,这种设计在QNX的消息传递中是应该避免的。因为很容易就造成死锁。一个常见的情形是这样的:
进程A:MsgSend() 到进程B
进程B:MsgReceive()接收到消息
进程B:处理消息,然后MsgSend()给进程A
因为进程A正在阻塞状态中,无法接收并处理B的请求;所以A会在STATE_REPLY里,而B则会因MsgSend()而进入STATE_SEND,两个进程就互为死锁住了。
当然,如果A和B都使用多线程,专门用一个线程来MsgReceive(),这个情形或许可以避免;但你要保证 MsgReceive()的线程不会去MsgSend(),否则一样会死锁。在程序简单的时候或许你还有控制,如果程序变得复杂,又或者你写的只是一个程序库,别人怎么来用你完全没有控制
在QNX中,正确的方法是这样的。

客户端: 准备一个“通知事件”(Notification Event),并把这个事件用MsgSend()发给服务器端,意思是:“如果xxx情况发生的话,请用这个事件通知我”。
服务器: 收到这个消息后,记录下当时的rcvid,和传过来的事件,然后应答“好的,知道了”。
客户端: 因为有了服务器的应答,客户端不再阻塞,可以去做别的事
…过了一段时间
服务器: 在某个时刻,客户端所要求的“xxx情况”满足了,服务器调用 MsgDeliverEvent(rcvid, event);以通知客户端
客户端: 收到通知,再用MsgSend()发关“xxx 情况的数据在哪里?”
服务器: 用MsgReply()把数据返回给客户端

具体的例子,可以参考MsgDeliverEvent()的文档说明
[QNX的方式]
(http://developer.blackberry.com/playbook/native/reference/com.qnx.doc.neutrino.lib_ref/topic/m/msgdeliverevent.html)

Robust implementations with Send/Receive/Reply

异步系统的一个重要问题是事件通知需要运行信号处理程序。异步IPC难以彻底对系统进行测试,此外也难以确保信号处理程序按预期的运行。基于Send/Receive/Reply构建的同步、非队列系统结构,可以让应用程序的架构更健壮
使用各种IPC机制时,避免死锁是一个难题,在QNX中只需要遵循两个原则,就可以构建无死锁系统:

  • 永远不要两个线程相互发送消息;
  • 将线程组织为层级结构,并只向上发送消息;
    Threads should always send up to higher-level threads
    上层的线程可以通过MsgSendPulse()或MsgDeliverEvent()来传递非阻塞消息或事件:
    Higher-level thread can "send" a pulse event
Path Name(路径名)

现在来回想一下我们最初的例子,客户端与服务器是怎样取得连接的?客户端需要服务器的 nd, pid, chid,才能与服务器正确地建立连接。在我们的例子里,我们是让服务器显示这几个数,然后在客户端的启动时,通过命令行里传给客户端。但是,在一个现实的系统里,进程不断地启动、终止;服务器与客户端的起动过程也无法控制,这种方法显然是行不通的。

QNX的解决办法,是把“路径名”与上述的“服务频道”概念巧妙地结合起来。让服务器进程可以注册一个路径名,与服务频道的nd, pid, chid关联起来。这样,客户端就不需要知道服务器的nd, pid, chid,而只要请求连接版务器路径名就可以了。具体来说 name_attach()就是用来建立一个频道,并为频道注册一个名字的;而name_open()则是用来连接注册过的服务器频道;具体的例子,可以在name_attach()的文档里找到.

优先级继承与消息

服务器进程按照优先级顺序来接收消息和脉冲,当服务器中的线程接收请求时,它们将继承发送线程的优先级。请求服务器工作的线程的优先级被保留,服务器工作将以适当的优先级执行,这种消息驱动的优先级继承避免了优先级反转的问题

Message-passing API

在这里插入图片描述

Events

QNX Neutrino提供异步事件通知机制,event是一种notification, 可以从thread到thread, 也可以从kernel到thread, 事件源可能有三种:
在这里插入图片描述

  • 调用MsgDeliverEvent()接口发送事件
  • 中断处理函数
  • 定时器到期
    事件本身可以有多种类型:Pulse、中断、各种形式的信号、强制解除阻塞的事件等
    考虑到事件本身的多样性,服务器实现所有的异步通知显然不太合适,更好的方式是客户端提供一个数据结构或者cookie,服务器调用MsgDeliverEvent()时将事件类型写进cookie中
    the client sends a sigevent to the server
    ionotify()函数是客户端线程请求异步事件通知的一种方式,许多POSIX异步服务都基于这个之上来构建的,比如mq_notify和select等

Signals

QNX支持的信号
QNX Neutrino 拓展了信号传递机制, 允许信号针对特定的线程, 而不是简单的针对包含线程的进程.
由于信号是异步事件, 它们通过事件传递机制实现, 接口如下:
Signal接口
Signal Delivery
当一个服务器线程想通知一个客户端线程时,有两种合理的事件选择:Pulse或信号

  • Pulse,需要客户端创建一个channel,并且调用MsgReceive()接收;
  • 信号,只需要调用sigwaitinfo(),不需要创建channel;

POSIX message queues

OSIX通过message queues定义一组非阻塞的消息传递机制。消息队列为命名对象,针对这些对象可以进行读取和写入,作为离散消息的优先级队列,消息队列具有比管道更多的结构,为应用程序提供了更多的通信控制。
QNX Neutrino内核不包含message queues,它的实现在内核之外。
QNX Neutrino提供了两种message queues的实现:

  • mqueue,使用mqueue资源管理的传统实现
  • mq,使用mq服务和非同步消息的替代实现

QNX消息机制与POSIX的Message queues有一个根本的区别:

  • QNX的消息机制通过内存拷贝来实现消息的传递;而POSIX的消息队列通过将消息进行存取来实现消息的传递。QNX的消息机制比POSIX的消息队列效率更高,但有时为了POSIX的灵活,需要适当的牺牲一点效率。

消息队列与文件类似,操作的接口接近:
QNX message queue API

Shared Memory

共享内存提供了最高带宽的IPC机制,一旦创建了共享内存对象,访问对象的进程可以使用指针直接对其进行读写操作。共享内存本身是不同步的,需要结合同步原语一起使用,信号量和互斥锁都适合与共享内存一块使用,信号量一般用于进程之间的同步,而互斥锁通常用于线程之间的同步,通通常来说互斥锁的效率会比信号量要高。

共享内存与消息传递结合起来的IPC机制,可以提供以下特点:

  • 非常高的性能 (共享内存)
  • 同步 (消息传递)
  • 跨网络传递 (消息传递)

QNX中消息传递通过拷贝完成,当消息较大时,可以通过共享内存来完成,发送消息时不需要发送整个消息内容,只需将消息保存到共享内存中,并将地址传递过去即可
QNX shm API
通常会使用mmap来将共享内存区域映射到进程地址空间中来,如下图所示:
Arguments to mmap
通过设置shared memory,同样的物理没存可以被多个进程访问:
在这里插入图片描述

Process进程间通过shared memory 通信同步策略:

在这里插入图片描述

Typed memory

类型化内存是POSIX规范中定义的功能,它是高级实时扩展的一部分
POSIX类型化内存,提供了一个接口来打开内存对象(以操作系统特定的方式定义),并对它们执行映射操作。这个对提供BSP/板级特定的地址布局与设备驱动或用户代码之间的抽象时非常有用。

Pipes and FIFOs

管道是一种非命名IO通道,用于在多个进程之间的通信,一个进程往管道写,其他进程从管道读取。管道一般用于平行的两个进程单向的传递数据,如果要双向通信的话,就应该使用消息传递了。
FIFOs与管道本质是一样的,不同点在于FIFOs会在文件系统中保存为一个永久的命名文件

源参考
从API开始理解QNX
知乎参考

猜你喜欢

转载自blog.csdn.net/weixin_44280688/article/details/103234235