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線程。

  • 如果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()調用返回一個錯誤。

  • 如果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設施。

多部分傳輸也被文件系統廣泛使用。在讀取時,使用帶有n個數據部分的消息將數據直接從文件系統高速緩存複製到應用程序中。每個部分指向高速緩存並補償高速緩存塊在存儲器中不連續的事實,其中讀取開始或結束在塊內。

比如,對於cache塊大小爲512字節,使用5部分消息可以讀取1454字節數據。

由於消息數據是在地址空間之間顯式複製的(而不是通過執行頁表操作),因此可以在堆棧上輕鬆分配消息,而不是從用於MMU“page flipping.”的頁面對齊內存的特殊池中分配消息。因此,許多可以簡單地表達在客戶端和服務器進程之間實現API的庫例程,而無需精心設計IPC特定的內存分配調用。
例如,客戶端線程用來請求文件系統管理器代表它執行lseek的代碼實現如下:

#include <unistd.h>  
#include <errno.h>  
#include <sys/iomsg.h>  

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.

對於服務進程來說應該實現如下事件循環來接收和處理消息:

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它們還允許服務器在不阻塞客戶端的情況下發送信號。

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. 線程之間關係按樹狀管理,發送操作僅能從子進程到父進程。

第一條規則明顯的用來避免兩個進程互相鎖死,第二個規則的原因我們需要進一步解釋。如下圖是一些合作的進程組。

樹狀關係中同一級線程之間不會發送消息,發送操作只限於父子之間,而且都是孩子發向父親。

這種方式的一個例子是client應用發送消息到database服務進程,然後database進程發送到filesystem進程。因爲發送線程阻塞等待目標線程應答,因此目標線程不應該發送消息給發送線程,否則會發生死鎖。

但是線程樹中的高級線程如何通知低級別線程之前請求的操作結果呢?(這裏假定低級線程不想等待最後一次發送的響應結果)。

QNX提供了一種非常靈活的架構,MsgDeliverEvent()內核調用發送非阻塞事件。所有的異步服務都可以使用這個函數實現。比如,服務器端select()調用是一個API,應用程序可以用它來等待I/O事件的完成。此外異步通知機制可以作爲反向通道,高級線程發送消息給低級線程,我們也可以使用它構造 timers,hardware總端或者其他事件源的通知系統。

還有一個問題是,高級線程如何請求低級線程執行某些工作,而無需冒着發送死鎖風險。低級線程作爲一個工作線程,服務於高級線程,執行高級線程請求的工作。低級線程發送工作彙報,高級線程並不會響應這個發送。效果上,高級線程的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中

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的信號傳遞機制。 由於信號是異步事件,因此它們也通過事件傳遞機制實現。

原始POSIX規範僅定義了進程上的信號操作。 在多線程進程中,遵循以下規則:

  • 信號操作保持在進程級別。如果線程忽略或捕獲信號,則會影響進程中的所有線程。

  • 信號掩碼保持在線程級別。如果線程阻塞信號,它隻影響該線程。

  • 針對線程的未標記信號將僅傳遞給該線程。

  • 針對進程的未標記信號被傳遞到沒有阻塞信號的第一個線程。如果所有線程都阻塞了信號,則信號將在進程中排隊直到任何線程忽略或取消阻塞信號。如果忽略,則將刪除該過程中的信號。如果解除阻塞,信號將從進程移動到解除阻塞的線程。

當信號針對具有大量線程的進程時,必須掃描線程表,查找信號未阻塞的線程。大多數多線程進程的標準做法是屏蔽所有線程中的信號,但只有一個線程專用於處理它們。爲了提高過程信號傳遞的效率,內核將緩存接受信號的最後一個線程並且將始終嘗試首先向其發送信號。

POSIX標準包括排隊實時信號的概念。 QNX Neutrino RTOS支持任意信號的可選排隊,而不僅僅是實時信號。可以在進程內逐個信號地指定排隊。每個信號可以具有相關的8位代碼和32位值。

這與前面描述的消息脈衝非常相似。內核利用這種相似性並使用通用代碼來管理信號和脈衝。使用_SIGMAX - signo將信號編號映射到脈衝優先級。結果,信號以優先級順序傳送,較低的信號數具有較高的優先級。 這符合POSIX標準,該標準規定現有信號優先於新的實時信號。

Special signals
如前所述,OS定義了總共64個信號。
它們的範圍如下:

八個特殊信號不能被忽略或捕獲。嘗試調用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中,創建的所有消息隊列都將出現在目錄下的文件名空間中:

Message-queue functions

Shared memory

共享內存提供最高帶寬的IPC。

創建共享內存對象後,可以使用指向對象的進程直接讀取和寫入對象。這意味着對共享內存的訪問本身是不同步的。如果進程正在更新共享內存區域,則必須注意防止其他進程讀取或更新同一區域。即使在讀取的簡單情況下,另一個過程也可能獲得不穩定且不一致的信息。

爲了解決這些問題,共享內存通常與其中一個同步原語結合使用,以在進程之間進行原子更新。如果更新的粒度很小,則同步原語本身將限制使用共享內存的固有高帶寬。因此,當用於將大量數據更新爲塊時,共享存儲器是最有效的。

信號量和互斥量都是適用於共享內存的同步原語。信號量是用POSIX實時標準引入的,用於進程間同步。使用POSIX線程標準引入互斥鎖以進行線程同步。互斥體也可以在不同進程中的線程之間使用。POSIX認爲這是一項可選功能; 我們支持它。通常,互斥量比信號量更有效。

Shared memory with message passing

共享內存和消息傳遞可以組合在一起提供IPC,提供:

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

  • 同步(消息傳遞)

  • 網絡透明度(消息傳遞)

使用消息傳遞,客戶端向服務器發送請求並阻止。服務器以優先級順序從客戶端接收消息,處理它們,並在滿足請求時進行回覆。此時,客戶端被解鎖並繼續。發送消息的行爲提供了客戶端和服務器之間的自然同步。消息可以包含對共享內存區域的引用,而不是通過消息傳遞複製所有數據,因此服務器可以直接讀取或寫入數據。最好用一個簡單的例子來解釋。

假設圖形服務器接受來自客戶端的繪圖圖像請求並將它們渲染到圖形卡上的幀緩衝區中。 單獨使用消息傳遞,客戶端將向服務器發送包含圖像數據的消息。 這將導致從客戶端的地址空間到服務器的地址空間的圖像數據的副本。 然後,服務器將呈現圖像併發出簡短回覆。

如果客戶端沒有與消息內聯發送圖像數據,而是發送對包含圖像數據的共享內存區域的引用,則服務器可以直接訪問客戶端的數據。

由於客戶端被阻塞在服務器上作爲發送它的消息的結果,服務器知道,在共享存儲器中的數據是穩定的,並且不會改變,直到服務器的答覆。消息傳遞和共享內存的這種組合實現了自然同步和非常高的性能。

此操作模型也可以反轉 - 服務器可以生成數據並將其提供給客戶端。例如,假設客戶端向服務器發送消息,該消息將直接從DVD讀取視頻數據到客戶端提供的共享內存緩衝區。 在更改共享內存時,將在服務器上阻止客戶端。當服務器回覆並且客戶端繼續時,共享內存將穩定以供客戶端訪問。這種類型的設計可以使用多個共享內存區域進行流水線操作。

在通過網絡連接的不同計算機上的進程之間不能使用簡單的共享內存。另一方面,消息傳遞是網絡透明的。服務器可以爲本地客戶端使用共享內存,併爲遠程客戶端使用數據的完整消息傳遞。這使您可以提供網絡透明的高性能服務器。

在實踐中,消息傳遞原語對於大多數IPC需求而言足夠快。組合方法的增加的複雜性僅需要考慮具有非常高帶寬的特殊應用。

Creating a shared-memory object

進程中的多個線程共享該進程的內存。要在進程之間共享內存,必須首先創建共享內存區域,然後將該區域映射到進程的地址空間。使用以下調用創建和操作共享內存區域:

POSIX共享內存通過進程管理器(procnto)在QNX Neutrino RTOS中實現。上述調用是作爲procnto的消息實現的(參見本書中的Process Manager章節)。

shm_open()函數採用與open()相同的參數,並向對象返回文件描述符。與常規文件一樣,此函數允許您創建新的共享內存對象或打開現有的共享內存對象。

注意:您必須打開文件描述符才能讀; 如果要在內存對象中寫入,則還需要寫訪問權限,除非指定私有(MAP_PRIVATE)映射。

創建新的共享內存對象時,對象的大小設置爲零。要設置大小,請使用ftruncate()- 用於設置文件大小的函數 - 或shm_ctl()。

mmap()

一旦有了共享內存對象的文件描述符,就可以使用mmap()函數將對象或其中的一部分映射到進程的地址空間。

mmap()函數是QNX Neutrino中內存管理的基石,值得對其功能進行詳細討論。

函數定義如下:

簡單來說,這表示:“在與fd相關聯的共享內存對象中的offset_within_shared_memory中以共享內存的長度字節映射。”

mmap()函數將嘗試將內存放在地址空間中的地址where_i_want_it中。內存將被賦予memory_protections指定的保護,映射將根據mapping_flags完成。

三個參數fd,offset_within_shared_memory和length定義了要映射的特定共享對象的一部分。在整個共享對象中映射是常見的,在這種情況下,偏移量將爲零,長度將是共享的大小對象以字節爲單位在Intel處理器上,長度將是頁面大小的倍數,即4096字節。

mmap()的返回值將是進程映射對象的地址空間中的地址。參數where_i_want_it用作系統提示您放置對象的位置。如果可能,該對象將被放置在所請求的地址。大多數應用程序指定地址爲零,這使系統可以自由地將對象放置在其希望的位置。

當您使用共享內存區域訪問可由硬件修改的雙端口內存(例如,視頻幀緩衝區或者內存映射網絡再或者通信板)時,應使用PROT_NOCACHE清單。如果沒有此清單,處理器可能會從先前緩存的讀取中返回“陳舊”數據。

mapping_flags確定內存的映射方式。 這些標誌分爲兩部分 - 第一部分是一個類型,必須指定爲以下之一:

MAP_SHARED類型是用於在進程之間設置共享內存的類型; MAP_PRIVATE具有更多專門用途。

您可以在上面的類型中添加多個標誌以進一步定義映射。這些在QNX Neutrino C Library Reference中的mmap()條目中有詳細描述。一些更有趣的標誌是:

源文件:《Interprocess Communication (IPC)》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章