QNX system architecture -- Chapter 3:Interprocess Communication (IPC)

从微内核构建全面的POSIX操作系统,进程间通信起到至关重要的作用。当各种提供服务的进程加到微内核中,IPC做为粘合层,把这些部件连结成一个紧密的整体。

尽管消息传递是QNX Neutrino RTOS IPC的主要形式,还有其他集中形式的IPC,除非提到,这些其他形式的IPC都是构建在本地消息传递基础之上。策略是在内核中创建一个简单的,鲁棒,易扩展的的IPC服务。更多复杂的IPC服务可以在此基础上实现。

比较微内核和单内核中高级IPC服务(比如pipes和FIFOs)的性能,性能指标是相当的。

QNX提供了如下形式的IPC

Service:

Implemented in:

Message-passing

Kernel

Signals

Kernel

POSIX message queues

External process

Shared memory

Process manager

Pipes

External process

FIFOs

External process

设计者可以选择这些服务,基于带宽需求,队列需求,网络透明等。如何权衡是复杂的,但是灵活性非常有必要。

做为实现微内核工程设计的一部分,使用消息传递作为基本的IPC原语是深思熟虑的。作为一种IPC通信形式,消息传递是同步操作并且有数据复制。让我们更详细的探究这两个属性。

Synchronous message passing

同步消息是QNX中的主要IPC形式。

线程调用MsgSend()到另外一个线程,发送线程阻塞,目标线程调用MsgReceive()处理收到的消息,然后执行MsgReply()。如果一个线程执行MsgReceive()时,没有未处理的消息,那么该进程阻塞直到另外一个线程执行MsgSend()。

在QNX中,服务器线程一半都是循环执行的,等待从Client线程接收到一个消息。像先前描述的,不论是server还是Client都可能处在READY状态,而没有在CPU上执行,此时是由于线程优先级和调度策略,而不是线程被阻塞。

让我们首先看一下Client线程。

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第1张图片

  • 如果client线程调用了MsgSend(),而server线程没有调用msgReceive(),那么client线程变为SEND阻塞状态。一旦server线程调用了MsgReceive(),内核改变client线程状态为REPLY阻塞,表示server线程已经收到了消息,现在必须发送reply。当服务线程调用了MsgReply(),client线程变成READY状态。

  • 如果client线程调用MsgSend(),而Server线程阻塞在MsgReceive(),那么Client线程立刻变为REPLY阻塞,完全忽略掉SEND-blocked状态。

  • 如果server线程失败,退出或者消失,Client线程变为READY,MsgSend()调用返回一个错误。

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第2张图片

  • 如果Server线程调用了MsgReceive(),并且没有其他线程发送消息给Server线程,那么Server线程变成RECEIVE阻塞。当另外一个线程发送给它,那么Server线程变成READY。

  • 如果server线程调用MsgReceive(),另外一个线程已经发送了消息给它,那么MsgReceice()立刻返回。这种情况下,server不会阻塞。

  • 如果server线程调用MsgReply(),那么线程不会阻塞。

发送线程是天生具有同步执行属性的,因为发送数据请求促使发送线程阻塞,接收线程被调度执行。不需要其他kernel工作决定哪个线程限制性。执行和数据移动直接从一个上下文转到另外一个。

这些消息原语并不包括数据队列能力,如有必要,可以在接收线程实现排队。发送线程通常只是等待一个响应,队列会带来不要的负载和负载性。因此,发送线程不需要单独的,显示的阻塞调用来等待响应。

发送和接收操作是阻塞的和同步的,而MsgReply()或者MsgError()则不会阻塞。因为Client线程已经阻塞等待响应,所以不需要额外的同步操作。这允许server响应一个消息后继续处理,kernel或者其他网络代码异步的传输应答数据给发送线程并改变发送线程为准备执行状态。因为大部分servers是趋向于做些处理准备介绍下一次请求,所以工作良好。

MsgReply() vs MsgError()

MsgReply()函数用来返回状态以及一些数据给Client。MsgError()仅仅返回client的状态。这两个函数都会解除client的阻塞状态。

Message copying

因为消息服务直接复制消息到另外一个进程的地址空间,而不需要中间buffer,因此消息分发性能接近了硬件内存带框。

内核并没有给消息内容赋予任何特定含义,消息体内的数据含义是由发送者和接收者定义的。当然也提供了良好定义的消息类型,以便用户写的进程和线程增强和替代系统自带的服务。

消息传递原语支持多部分传输,因此从一个线程的地址空间传递到另一个线程的消息不需要预先存在于单个连续的缓冲区中。 相反,发送和接收线程都可以指定一个向量表,该向量表指示发送和接收消息片段在内存中的位置。请注意,发送方和接收方的各个部分的大小可能不同。

多部分传输允许发送具有与数据块分离的标题块的消息,而无需消耗性能的数据复制以创建连续消息(多部分传输的消息包含一个头部和数据部)。此外,如果底层数据结构是环形缓冲区,则指定三部分消息将允许将环形缓冲区内的标头和两个不相交的范围作为单个原子消息发送。这个概念的硬件等同于分散/聚集DMA设施。

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第3张图片

多部分传输也被文件系统广泛使用。在读取时,使用带有n个数据部分的消息将数据直接从文件系统高速缓存复制到应用程序中。每个部分指向高速缓存并补偿高速缓存块在存储器中不连续的事实,其中读取开始或结束在块内。

比如,对于cache块大小为512字节,使用5部分消息可以读取1454字节数据。

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第4张图片

由于消息数据是在地址空间之间显式复制的(而不是通过执行页表操作),因此可以在堆栈上轻松分配消息,而不是从用于MMU“page flipping.”的页面对齐内存的特殊池中分配消息。因此,许多可以简单地表达在客户端和服务器进程之间实现API的库例程,而无需精心设计IPC特定的内存分配调用。
例如,客户端线程用来请求文件系统管理器代表它执行lseek的代码实现如下:

#include   
#include   
#include   

off64_t lseek64(int fd, off64_t offset, int whence) 
{  
    io_lseek_t                            msg;  
    off64_t                               off;    
    msg.i.type = _IO_LSEEK;  
    msg.i.combine_len = sizeof msg.i;  
    msg.i.offset = offset;  
    msg.i.whence = whence;  
    msg.i.zero = 0;  

    if(MsgSend(fd, &msg.i, sizeof msg.i, &off, sizeof off) == -1) {  
        return -1;  
    }  

    return off;  
}  

off64_t tell64(int fd) {  
    return lseek64(fd, 0, SEEK_CUR);  
}    

off_t lseek(int fd, off_t offset, int whence) {  
    return lseek64(fd, offset, whence);  
}  

off_t tell(int fd) {  
    return lseek64(fd, 0, SEEK_CUR);  
}  

上述代码在进程栈上分配message结构,然后设置这个结构的各个成员,发送给关联到fd的文件系统manager 。返回值指明操作是否成功。

注意:因为大部分消息传递的数据量非常小,复制消息通常要比控制MMU page tables要快。对于大块的数据传输,进程间的共享内存通常是更好的选择。

Simple messages

对于简单的single-part消息,操作系统提供了函数直接访问buffer指针,不需要使用IOV。这种情况下,部分序号被替换为直接指向的消息尺寸。

对于消息发送原语,根据发送和响应buffer的不同,引入了四个变种

Function

Send message

Reply message

MsgSend()

Simple

Simple

MsgSendsv()

Simple

IOV

MsgSendvs()

IOV

Simple

MsgSendv()

IOV

IOV

其他的消息原语如果只是用一个直接消息buffer,那么只需去掉后缀v即可。

IOV

Simple direct

MsgReceivev()

MsgReceive()

MsgReceivePulsev()

MsgReceivePulse()

MsgReplyv()

MsgReply()

MsgReadv()

MsgRead()

MsgWritev()

MsgWrite()

Channels and connections

在QNX Neutrino RTOS中,message passing是通过channels和connections,而不是面向线程的。一个线程如果希望接收消息首先要创建一个channel;另外一个线程希望发送消息给这个线程,则必须创建一个连接,绑定到这个channel上。

server通过内核调用创建一个channels,使用MsgReceive()在channels上接收消息。Client创建一个connections,并连接到servers提供的channels上。一旦连接被建立,clients使用MsgSend()。如果多个线程都绑定到同一个channel,所有的连接都映射到相同的内核对象上。channels和connections被命名为进程内的一个整数标识符。而客户端connections直接映射为文件描述符。

从结构上讲,这是一个关键点。 通过将客户端连接直接映射到FD,我们已经消除了另一层转换。 我们不需要“弄清楚”基于文件描述符发送消息的位置(例如,通过read(fd)调用)。 相反,只需把消息发送到这个文件描述符即可(也就是connection ID)。

Function

Description

ChannelCreate()

Create a channel to receive messages on.

ChannelDestroy()

Destroy a channel.

ConnectAttach()

Create a connection to send messages on.

ConnectDetach()

Detach a connection.

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第5张图片

对于服务进程来说应该实现如下事件循环来接收和处理消息:

chid = ChannelCreate(flags);  

SETIOV(&iov, &msg, sizeof(msg));  

for(;;) {  
    rcv_id = MsgReceivev( chid, &iov, parts, &info );  
    switch( msg.type ) {  
        /* Perform message processing here */  
        }  

    MsgReplyv( rcv_id, &iov, rparts );  
    }  

循环允许服务线程从channel上connection的接收消息。

channel有以及几个消息列表:

  • Receive

一个LIFO等待消息线程队列。

  • Send

一个优先级为FIFO线程队列, 线程已经发送消息但是还没有接收到

  • Reply

一个未排序的线程列表,已经发送且被收到,但是还没有reply

在任何一个列表中,等待线程被阻塞。可以有多个线程和多个客户端在一个channel上等待。

Pulses

除了同步Send/Receive/Reply服务,OS也支持固定尺寸,非阻塞的消息。比如pulses消息仅携带非常小的负载(4字节数据,加一字节code)

Pulses包含相对小的负载: 8 bits of code和32bits数据。Pulses经常用做中断处理函数的提示机制。Pulses它们还允许服务器在不阻塞客户端的情况下发送信号。

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第6张图片

Priority inheritance and messages

一个服务进程接收消息和pulses按照优先级顺序。当服务内的线程接收到请求,他们继承了发送线程的优先级,因此,向Server发起请求的线程优先级被保留,server以客户端线程的优先级运行。这个消息驱动的优先级集成可以避免优先级反转问题。

例如,假定系统包含如下线程。

  • 一个服务线程,优先级22

  • client线程T1,优先级13

  • client线程T2,优先级10

如果没有优先级继承,那么如果T2发送一条消息给server,它的有效优先级变成了server的22,所以T2的优先级被反转了。

实际上当server收到一个消息,它的有效优先级变成了消息发送者的最高优先级。在这个情况下,T2的优先级低于服务器的优先级,所以当server收到这个消息时,因此服务器接收消息时,会发生服务器的优先级更改,即它的有效优先级被替换为T2的优先级。

接下来,假设T1在服务器仍处于优先级10时向服务器发送消息。由于T1的优先级高于服务器的当前优先级,因此当T1发送消息时,服务器优先级发生变化

在server接收到消息后,需要更改优先级,避免另外一种优先级反转。如果server的优先级保持在10不变,另外一个线程T3运行在优先级11,server不得不等待T3一段时间才会去接收T1的消息。也就是T1被一个低优先级的线程T3耽搁了。

可以在调用ChannelCreate指定_NTO_CHF_FIXED_PRIORITY标记,关闭优先级继承。如果你正在使用adaptive partitioning,这个标记也促使接收线程不要运行在发送线程的partitions上。

Message-passing API

消息传递API包含如下函数

Function

Description

MsgSend()

Send a message and block until reply.

MsgReceive()

Wait for a message.

MsgReceivePulse()

Wait for a tiny, nonblocking message (pulse).

MsgReply()

Reply to a message.

MsgError()

Reply only with an error status. No message bytes are transferred.

MsgRead()

Read additional data from a received message.

MsgWrite()

Write additional data to a reply message.

MsgInfo()

Obtain info on a received message.

MsgSendPulse()

Send a tiny, nonblocking message (pulse).

MsgDeliverEvent()

Deliver an event to a client.

MsgKeyData()

Key a message to allow security checks.

Robust implementations with Send/Receive/Reply

通过Send/Receive/Reply构造QNX应用为一组合作的进程和线程,使得系统使用同步通知。IPC因此发生在系统特定状态转换,而不是异步的操作。

异步系统的一个重要问题是事件通知需要运行信号处理程序。异步IPC可能难以彻底测试系统的运行,并确保无论何时信号处理程序运行,该处理将按预期继续。应用程序经常试图通过依赖打开和关闭的“窗口”来避免这种情况,在此期间信号将被容忍。

通过使用Send/Receive/Reply构建的同步,非排队系统架构,可以非常容易地实现和交付强大的应用程序架构。

当我们用各种队列IPC,共享内存和其他五花八门同步原语构造应用时,避免死锁是一个困难的问题。比如,假定线程A不会释放mutex 1直到线程B释放了mutex 2。不幸的是线程B所在的状态不会释放mutex 2,直到线程A释放mutex1,导致了死锁。模拟工具可以用来检测系统是否会发生死锁。

Send/Receive/Reply IPC原语可以很简单的构造出无死锁的系统,只需遵守如下规则:

  1. 两个线程之间不要互相发送消息。

  2. 线程之间关系按树状管理,发送操作仅能从子进程到父进程。

第一条规则明显的用来避免两个进程互相锁死,第二个规则的原因我们需要进一步解释。如下图是一些合作的进程组。

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第7张图片

树状关系中同一级线程之间不会发送消息,发送操作只限于父子之间,而且都是孩子发向父亲。

这种方式的一个例子是client应用发送消息到database服务进程,然后database进程发送到filesystem进程。因为发送线程阻塞等待目标线程应答,因此目标线程不应该发送消息给发送线程,否则会发生死锁。

但是线程树中的高级线程如何通知低级别线程之前请求的操作结果呢?(这里假定低级线程不想等待最后一次发送的响应结果)。

QNX提供了一种非常灵活的架构,MsgDeliverEvent()内核调用发送非阻塞事件。所有的异步服务都可以使用这个函数实现。比如,服务器端select()调用是一个API,应用程序可以用它来等待I/O事件的完成。此外异步通知机制可以作为反向通道,高级线程发送消息给低级线程,我们也可以使用它构造 timers,hardware总端或者其他事件源的通知系统。

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第8张图片

还有一个问题是,高级线程如何请求低级线程执行某些工作,而无需冒着发送死锁风险。低级线程作为一个工作线程,服务于高级线程,执行高级线程请求的工作。低级线程发送工作汇报,高级线程并不会响应这个发送。效果上,高级线程的notify用来启动工作,低级线程用发送消息汇报执行效果。

Events

QNX内核中非常先进的的一个设计是事件处理子系统。POSIX和它的实时扩展定义了一定数目的异步通知方法(例如,UNIX信号不会入队或者传输数据,POSIX实时信号可以入队和传输数据)

内核也定义了额外的,QNX特定的通知机制,比如pulses。实现这些事件机制需要消耗一定的代码空间,所以我们的实现策略是在一个简单,丰富的事件子系统上构造这些特定的实现。

一个执行线程收到的事件有如下来源:

  • 某个线程调用了MsgDeliverEvent()

  • 一个中断处理handler

  • 一个timer超时

时间本身可以有如下不同类型:

QNX pulses,中断,各种形式的信号,以及强制unblock事件。Unblock是一种方法,可以解除正在阻塞的线程,不要要显示的发送正在实际等待的事件。

给定这些事件类型,应用程序需要能力请求究竟哪一个异步事件通知技术最适合他们的需求,请求server进程执行代码支持所有选项不大可行。

 Client线程可以指定一个数据结构或者cookie给server。当server需要通知client线程,Server调用MsgDeliverEvent()然后microkernel设置时间类型到client线程的cookie中

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第9张图片

I/O notification

ionotify()函数client线程请求异步事件发送的一种方法。

一些POSIX异步服务(比如mq_notify和client端select()操作)是建立在ionotify之上的。当在某个文件描述符上执行I/O,线程可以选择等待I/O事件完成或者数据到达。而不是线程阻塞在执行读写请求的资源管理进程,ionotify()可以允许client线程发送一个事件给资源管理器,client线程需要在指定I/O条件发生时收到通知。使用这种方式,允许线程继续执行和响应其他事件源。

select()调用使用I/O通知实现,允许一个线程阻塞等待多个fd上的多种I/O事件的发生。

下面是请求事件发生的条件:

  • _NOTIFY_COND_OUTPUT - output buffer有空间接收数据

  • _NOTIFY_COND_INPUT - 资源管理器定义数目的数据可读

  • _NOTIFY_COND_OBAND - 资源管理器定义 带外数据可用。

Signals

操作系统支持32个标准POSIX信号(如在UNIX中)以及POSIX实时信号,两者均由内核实现的64个信号编号,具有统一的功能。 虽然POSIX标准将实时信号定义为与UNIX风格信号不同(因为它们可能包含四个字节的数据和一个字节代码,并且可能排队等待传送),但可以基于每个信号显式选择或取消选择此功能。 允许这种融合的实现仍然符合标准。
顺便提一下,如果应用程序需要,UNIX风格的信号可以选择POSIX实时信号排队。 QNX Neutrino RTOS还通过允许信号针对特定线程而不是简单地在包含线程的进程中扩展POSIX的信号传递机制。 由于信号是异步事件,因此它们也通过事件传递机制实现。

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第10张图片

原始POSIX规范仅定义了进程上的信号操作。 在多线程进程中,遵循以下规则:

  • 信号操作保持在进程级别。如果线程忽略或捕获信号,则会影响进程中的所有线程。

  • 信号掩码保持在线程级别。如果线程阻塞信号,它只影响该线程。

  • 针对线程的未标记信号将仅传递给该线程。

  • 针对进程的未标记信号被传递到没有阻塞信号的第一个线程。如果所有线程都阻塞了信号,则信号将在进程中排队直到任何线程忽略或取消阻塞信号。如果忽略,则将删除该过程中的信号。如果解除阻塞,信号将从进程移动到解除阻塞的线程。

当信号针对具有大量线程的进程时,必须扫描线程表,查找信号未阻塞的线程。大多数多线程进程的标准做法是屏蔽所有线程中的信号,但只有一个线程专用于处理它们。为了提高过程信号传递的效率,内核将缓存接受信号的最后一个线程并且将始终尝试首先向其发送信号。

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第11张图片

POSIX标准包括排队实时信号的概念。 QNX Neutrino RTOS支持任意信号的可选排队,而不仅仅是实时信号。可以在进程内逐个信号地指定排队。每个信号可以具有相关的8位代码和32位值。

这与前面描述的消息脉冲非常相似。内核利用这种相似性并使用通用代码来管理信号和脉冲。使用_SIGMAX - signo将信号编号映射到脉冲优先级。结果,信号以优先级顺序传送,较低的信号数具有较高的优先级。 这符合POSIX标准,该标准规定现有信号优先于新的实时信号。

Special signals
如前所述,OS定义了总共64个信号。
它们的范围如下:QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第12张图片

八个特殊信号不能被忽略或捕获。尝试调用signal()或sigaction()函数或SignalAction()内核调用来更改它们将失败并出现EINVAL错误。

此外,这些信号始终被阻止并启用信号排队。尝试通过sigprocmask()函数或SignalProcmask()内核调用解除阻塞这些信号将被悄然忽略。

可以使用以下标准信号调用将常规信号编程为此行为。特殊信号使编程人员无法编写此代码,并保护信号免于意外更改此行为。

sigset_t *set;
struct sigaction action;
sigemptyset(&set);
sigaddset(&set, signo);
sigprocmask(SIG_BLOCK, &set, NULL);
action.sa_handler = SIG_DFL;
action.sa_flags = SA_SIGINFO;
sigaction(signo, &action, NULL);

此配置使这些信号适用于使用sigwaitinfo()函数或SignalWaitinfo()内核调用的同步通知。以下代码将阻塞,直到收到第八个特殊信号:

sigset_t *set;
siginfo_t info;
sigemptyset(&set);
sigaddset(&set, SIGRTMAX + 8);
sigwaitinfo(&set, &info);
printf("Received signal %d with code %d and value %d\n",
info.si_signo,
info.si_code,
info.si_value.sival_int);

由于信号始终被阻止,如果特殊信号在sigwaitinfo()函数之外传递,则程序不会被中断或终止。 由于信号排队始终处于启用状态,因此信号不会丢失 - 它们将排队等待下一个sigwaitinfo()调用。

这些信号旨在解决通用的IPC要求,其中服务器希望通知客户端它具有可用于客户端的信息。服务器将使用MsgDeliverEvent()调用来通知客户端。通知中有两种合理的事件选择:脉冲或信号。

脉冲是客户端的首选方法,也可以是其他客户端的服务器。在这种情况下,客户端将创建一个用于接收消息的通道,并且还可以接收脉冲。

对于大多数简单的客户来说,情况并非如此。为了接收脉冲,将迫使简单的客户端为此明确目的创建信道。 如果信号被配置为同步(即,信号被阻塞)并且排队,则可以使用信号代替脉冲 - 这正是特殊信号的配置方式。客户端将使用简单的sigwaitinfo()调用替换用于等待通道上的脉冲的MsgReceive()调用,以等待信号。

The eight special signals include named signals for special purposes:

SIGSELECT

Used by select() to wait for I/O from multiple servers.

POSIX message queues

POSIX定义了一组非阻塞的消息传送能力,称为消息队列。和pipe类似,消息队列是命名对象,供readers和writers操作。消息队列和pipe相比有更多的结构,在通信过程中,提供了更多的控制。

和消息传递原语不同,POSIX消息队列是在kernel外部实现的

Why use POSIX message queues?

POSIX消息队列为实时系统开发者提供了熟悉的接口。类似于实时系统中的邮箱。

QNX消息队列和POSIX消息队列有根本的不同。我们的消息块数据是直接在发送进程和接收线程地址空间复制。而POSIX消息队列,则实现了存储转发设计,发送者不会阻塞并且可以有很多消息排队。POSIX消息队列是独立于使用他们的线程存在的。多个命名消息队列可以被不同的进程操作。

从性能角度来说,POSIX消息队列要比QNX消息传送数据慢。但是,消息队列带来的灵活性,值得我们牺牲这点性能。

File-like interface

消息队列类似于文件,至少就其接口而言。

使用mq_open()打开消息队列,使用mq_close()将其关闭,然后使用mq_unlink()将其销毁。要将数据放入(“write”)并将其从(“read”)消息队列中取出,可以使用mq_send()和mq_receive()。

对于严格的POSIX一致性,您应该创建以单斜杠(/)开头并且不包含其他斜杠的消息队列。但请注意,我们通过支持可能包含多个斜杠的路径名来扩展POSIX标准。例如,这允许公司将其所有消息队列放在其公司名称下,并更有信心地分发产品,使队列名称不会与另一家公司的名称冲突。

在QNX Neutrino中,创建的所有消息队列都将出现在目录下的文件名空间中:

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第13张图片

Message-queue functions QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第14张图片

Shared memory

共享内存提供最高带宽的IPC。

创建共享内存对象后,可以使用指向对象的进程直接读取和写入对象。这意味着对共享内存的访问本身是不同步的。如果进程正在更新共享内存区域,则必须注意防止其他进程读取或更新同一区域。即使在读取的简单情况下,另一个过程也可能获得不稳定且不一致的信息。

为了解决这些问题,共享内存通常与其中一个同步原语结合使用,以在进程之间进行原子更新。如果更新的粒度很小,则同步原语本身将限制使用共享内存的固有高带宽。因此,当用于将大量数据更新为块时,共享存储器是最有效的。

信号量和互斥量都是适用于共享内存的同步原语。信号量是用POSIX实时标准引入的,用于进程间同步。使用POSIX线程标准引入互斥锁以进行线程同步。互斥体也可以在不同进程中的线程之间使用。POSIX认为这是一项可选功能; 我们支持它。通常,互斥量比信号量更有效。

Shared memory with message passing

共享内存和消息传递可以组合在一起提供IPC,提供:

  • 非常高的性能(共享内存)

  • 同步(消息传递)

  • 网络透明度(消息传递)

使用消息传递,客户端向服务器发送请求并阻止。服务器以优先级顺序从客户端接收消息,处理它们,并在满足请求时进行回复。此时,客户端被解锁并继续。发送消息的行为提供了客户端和服务器之间的自然同步。消息可以包含对共享内存区域的引用,而不是通过消息传递复制所有数据,因此服务器可以直接读取或写入数据。最好用一个简单的例子来解释。

假设图形服务器接受来自客户端的绘图图像请求并将它们渲染到图形卡上的帧缓冲区中。 单独使用消息传递,客户端将向服务器发送包含图像数据的消息。 这将导致从客户端的地址空间到服务器的地址空间的图像数据的副本。 然后,服务器将呈现图像并发出简短回复。

如果客户端没有与消息内联发送图像数据,而是发送对包含图像数据的共享内存区域的引用,则服务器可以直接访问客户端的数据。

由于客户端被阻塞在服务器上作为发送它的消息的结果,服务器知道,在共享存储器中的数据是稳定的,并且不会改变,直到服务器的答复。消息传递和共享内存的这种组合实现了自然同步和非常高的性能。

此操作模型也可以反转 - 服务器可以生成数据并将其提供给客户端。例如,假设客户端向服务器发送消息,该消息将直接从DVD读取视频数据到客户端提供的共享内存缓冲区。 在更改共享内存时,将在服务器上阻止客户端。当服务器回复并且客户端继续时,共享内存将稳定以供客户端访问。这种类型的设计可以使用多个共享内存区域进行流水线操作。

在通过网络连接的不同计算机上的进程之间不能使用简单的共享内存。另一方面,消息传递是网络透明的。服务器可以为本地客户端使用共享内存,并为远程客户端使用数据的完整消息传递。这使您可以提供网络透明的高性能服务器。

在实践中,消息传递原语对于大多数IPC需求而言足够快。组合方法的增加的复杂性仅需要考虑具有非常高带宽的特殊应用。

Creating a shared-memory object

进程中的多个线程共享该进程的内存。要在进程之间共享内存,必须首先创建共享内存区域,然后将该区域映射到进程的地址空间。使用以下调用创建和操作共享内存区域:

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第15张图片

POSIX共享内存通过进程管理器(procnto)在QNX Neutrino RTOS中实现。上述调用是作为procnto的消息实现的(参见本书中的Process Manager章节)。

shm_open()函数采用与open()相同的参数,并向对象返回文件描述符。与常规文件一样,此函数允许您创建新的共享内存对象或打开现有的共享内存对象。

注意:您必须打开文件描述符才能读; 如果要在内存对象中写入,则还需要写访问权限,除非指定私有(MAP_PRIVATE)映射。

创建新的共享内存对象时,对象的大小设置为零。要设置大小,请使用ftruncate()- 用于设置文件大小的函数 - 或shm_ctl()。

mmap()

一旦有了共享内存对象的文件描述符,就可以使用mmap()函数将对象或其中的一部分映射到进程的地址空间。

mmap()函数是QNX Neutrino中内存管理的基石,值得对其功能进行详细讨论。

函数定义如下:

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第16张图片

简单来说,这表示:“在与fd相关联的共享内存对象中的offset_within_shared_memory中以共享内存的长度字节映射。”

mmap()函数将尝试将内存放在地址空间中的地址where_i_want_it中。内存将被赋予memory_protections指定的保护,映射将根据mapping_flags完成。

三个参数fd,offset_within_shared_memory和length定义了要映射的特定共享对象的一部分。在整个共享对象中映射是常见的,在这种情况下,偏移量将为零,长度将是共享的大小对象以字节为单位在Intel处理器上,长度将是页面大小的倍数,即4096字节。

QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第17张图片

mmap()的返回值将是进程映射对象的地址空间中的地址。参数where_i_want_it用作系统提示您放置对象的位置。如果可能,该对象将被放置在所请求的地址。大多数应用程序指定地址为零,这使系统可以自由地将对象放置在其希望的位置。QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第18张图片

当您使用共享内存区域访问可由硬件修改的双端口内存(例如,视频帧缓冲区或者内存映射网络再或者通信板)时,应使用PROT_NOCACHE清单。如果没有此清单,处理器可能会从先前缓存的读取中返回“陈旧”数据。

mapping_flags确定内存的映射方式。 这些标志分为两部分 - 第一部分是一个类型,必须指定为以下之一:QNX system architecture -- Chapter 3:Interprocess Communication (IPC)_第19张图片

MAP_SHARED类型是用于在进程之间设置共享内存的类型; MAP_PRIVATE具有更多专门用途。

您可以在上面的类型中添加多个标志以进一步定义映射。这些在QNX Neutrino C Library Reference中的mmap()条目中有详细描述。一些更有趣的标志是:

源文件:《Interprocess Communication (IPC)》

你可能感兴趣的:(QNX)