QNX® Neutrino 進程間通信

介紹

Interprocess Communication(IPC,進程間通信)在QNX Neutrino從一個嵌入式實時系統向一個全面的POSIX系統轉變起着至關重要的作用。IPC是將在內核中提供各種服務的進程內聚在一起的粘合劑。在QNX中,消息傳遞是IPC的主要形式,也提供了其他的形式,除非有特殊的說明,否則這些形式也都是基於本地消息傳遞而實現的。

將更高級別的 IPC 服務(如通過我們的消息傳遞實現的管道和 FIFO)與其宏內核對應物進行比較的基準測試表明性能相當。

QNX Neutrino提供以下形式的IPC:

Service: Implemented in:
Message-passing Kernel
Signals Kernel
POSIX message queues External process
Shared memory Process manager
Pipes External process
FIFOs External process

設計人員可以根據帶寬要求,排隊需求,網絡透明度等選擇這些服務。權衡可能很複雜,但靈活性很實用。

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機制類似。

分塊傳輸也用在文件系統中,比如讀數據的時候,將文件系統緩存中的數據分塊讀到用戶提供的空間內,如下圖:

Simple messages

對於簡單的單塊消息傳遞,就不需要通過IOV(input/output vector)的形式了,直接指向緩衝區即可。對於發送和接收的接口,多塊發送和單塊發送如下:

在消息發送原語的情況下——它接受一個發送和一個回覆緩衝區——這引入了四種變體:

Function Send message Reply message
MsgSend() Simple Simple
MsgSendsv() Simple IOV
MsgSendvs() IOV Simple
MsgSendv() IOV IOV

其他採用直接消息的消息傳遞原語只是在其名稱中刪除尾隨的“v”:

IOV Simple direct
MsgReceivev() MsgReceive()
MsgReceivePulsev() MsgReceivePulse()
MsgReplyv() MsgReply()
MsgReadv() MsgRead()
MsgWritev() MsgWrite()

Channels and connections

在QNX Neutrino中,消息傳遞是面向通道(channel)和連接(connection)的,而不是直接從線程到線程的。接收消息的線程需要創建一個channel,發送消息的線程需要與該channel建立connection。 服務器使用MsgReceive()接收消息時需要使用channels,客戶端則需要創建connections,以連接到服務器的通道上,連接建立好之後,客戶端便可通過MsgSend()來發送消息了。如果進程中有很多線程都連接到一個通道上,爲了提高效率,這些所有的連接都會映射到同一個內核對象中。在進程中,channelsconnecttions會用一個小的整型標識符來標記。客戶端connections會直接映射到文件描述符,在架構上這是一個關鍵點,可以消除另一層轉換,不需要根據文件描述符來確定往哪裏發消息,而是直接將消息發往文件描述符即可。

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有關聯的列表:

  • Receive,等待消息的LIFO線程隊列;
  • Send,已發送消息但還未被接收的優先級FIFO線程隊列;
  • Reply, 已發送消息,並且已經被收到,但尚未回覆的無序線程列表; 不管在上述哪個列表中,線程都是阻塞狀態,多個線程和多個客戶端可能等待在同一個channel上。

Pulses

除了同步發送/接收/回覆服務外,QNX還支持固定大小的非阻塞消息,這種消息被稱爲Pulse,攜帶一個小的負載(四個字節數據,加一個字節的代碼)。Pulse通常被用在中斷處理函數中,用作通知機制;也允許服務器在不阻塞客戶端的情況下,向客戶端發送信號。

優先級繼承與消息

服務器進程按照優先級順序來接收消息和脈衝,當服務器中的線程接收請求時,它們將繼承發送線程的優先級。請求服務器工作的線程的優先級被保留,服務器工作將以適當的優先級執行,這種消息驅動的優先級繼承避免了優先級反轉的問題。

Message-passing 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

異步系統的一個重要問題是事件通知需要運行信號處理程序。異步IPC難以徹底對系統進行測試,此外也難以確保信號處理程序按預期的運行。基於Send/Receive/Reply構建的同步、非隊列系統結構,可以讓應用程序的架構更健壯。

在使用各種IPC機制時,避免死鎖是一個難題,在QNX中只需要遵循兩個原則,就可以構建無死鎖系統:

  • 永遠不要兩個線程相互發送消息;
  • 將線程組織爲層級結構,並只向上發送消息;

上層的線程可以通過MsgSendPulse()MsgDeliverEvent()來傳遞非阻塞消息或事件:

Events

QNX Neutrino提供異步事件通知機制,事件源可能有三種:

  • 調用MsgDeliverEvent()接口發送事件

  • 中斷處理函數

  • 定時器到期

事件本身可以有多種類型:Pulse、中斷、各種形式的信號、強制解除阻塞的事件等。 考慮到事件本身的多樣性,服務器實現所有的異步通知顯然不太合適,更好的方式是客戶端提供一個數據結構或者cookie,服務器調用MsgDeliverEvent()時將事件類型寫進cookie中。

ionotify()函數是客戶端線程請求異步事件通知的一種方式,許多POSIX異步服務都基於這個之上來構建的,比如mq_notifyselect等。

Signals

信號類似於軟中斷,QNX支持的信號如下:

Signal range Description
1 ... 56 56 POSIX signals (including traditional UNIX signals)
41 ... 56 16 POSIX realtime signals (SIGRTMIN to SIGRTMAX)
57 ... 64 Eight special-purpose QNX Neutrino signals

QNX Neutrino擴展了信號傳遞機制,允許信號針對特定的線程,而不是簡單的針對包含線程的進程。由於信號是異步事件,它們通過事件傳遞機制實現。接口如下:

Microkernel call POSIX call Description
SignalKill() kill(), pthread_kill(), raise(), sigqueue() Set a signal on a process group, process, or thread.
SignalAction() sigaction() Define action to take on receipt of a signal.
SignalProcmask() sigprocmask(), pthread_sigmask() Change signal blocked mask of a thread.
SignalSuspend() sigsuspend(), pause() Block until a signal invokes a signal handler.
SignalWaitinfo() sigwaitinfo() Wait for signal and return info on it.

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

  • 由 CPU 異常(例如 SIGSEGV、SIGBUS)引起的信號總是傳遞給引起異常的線程。
  • 信號動作保持在過程級別。如果線程爲信號指定操作(例如,忽略或捕獲它),則該操作會影響進程內的所有線程。
  • 信號掩碼保持在線程級別。如果一個線程阻塞一個信號,阻塞隻影響那個線程。
  • 針對某個線程的未忽略信號僅傳遞給該線程。
  • 針對進程的未忽略信號被傳遞到第一個沒有阻塞信號的線程。如果所有線程都阻塞了該信號,則該信號將在進程上排隊,直到任何線程忽略或解除該信號的阻塞。如果忽略,進程上的信號將被刪除。如果解除阻塞,信號將從進程移動到解除阻塞的線程。

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

信號概要

Signal Description Default action
SIGABRT Abnormal termination, issued by functions such as abort() Kill the process and write a dump file
SIGALRM Alarm clock, issued by functions such as alarm() Kill the process
SIGBUS Bus error, or a memory parity error (a QNX Neutrino-specific interpretation). If a second fault occurs while your process is in a signal handler for this fault, the process is terminated. Kill the process and write a dump file
SIGCHLD or SIGCLD A child process terminated Ignore the signal, but still let the process's children become zombies
SIGCONT Continue the process. You can't block this signal. Make the process continue if it's STOPPED; otherwise ignore the signal
SIGDEADLK A mutex deadlock occurred. If a process dies while holding a non-robust mutex and no still-existing process has called SyncMutexEvent() to set up an event to be delivered when the mutex owner dies, the kernel delivers a SIGDEADLK to all threads that are waiting on the mutex without a timeout. The kernel also delivers this signal if an event has been set up but its target is the same process that has died and this process didn't call procmgr_guardian() to designate another process as the new parent to its children. Note that it's up to any guardian process to revive the mutex by calling SyncMutexRevive(), as the signal is not sent in this case.SIGDEADLK and SIGEMT refer to the same signal. Some utilities (e.g., gdb, ksh, slay, and kill) know about SIGEMT, but not SIGDEADLK. Kill the process and write a dump file
SIGEMT EMT instruction (emulation trap)SIGDEADLK and SIGEMT refer to the same signal. Some utilities (e.g., gdb, ksh, slay, and kill) know about SIGEMT, but not SIGDEADLK. Kill the process and write a dump file
SIGFPE Floating point exception Kill the process and write a dump file
SIGHUP Hangup; the session leader died, or the controlling terminal closed Kill the process
SIGILLa Illegal hardware instruction. If a second fault occurs while your thread is in a signal handler for this fault, the process is terminated. Kill the process and write a dump file
SIGINT Interrupt; typically generated when you press CtrlC or CtrlBreak (you can change this with stty) Kill the process
SIGIO Asynchronous I/O Ignore the signal
SIGIOT I/O trap; a synonym for SIGABRT Kill the process
SIGKILL Kill. You can't block or catch this signal. Kill the process
SIGPIPE Write on pipe with no reader Kill the process
SIGPOLL System V name for SIGIO Ignore the signal
SIGPROF Profiling timer expired. POSIX has marked this signal as obsolescent; QNX Neutrino doesn't support profiling timers or send this signal. Kill the process
SIGPWR Power failure Ignore the signal
SIGQUIT Quit; typically generated when you press Ctrl–**** (you can change this with stty) Kill the process and write a dump file
SIGSEGV Segmentation violation; an invalid memory reference was detected. If a second fault occurs while your process is in a signal handler for this fault, the process will be terminated. Kill the process and write a dump file
SIGSTOP Stop the process. You can't block or catch this signal. Stop the process
SIGSYS Bad argument to system call Kill the process and write a dump file
SIGTERM Termination signal Kill the process
SIGTRAP Trace trap Kill the process and write a dump file
SIGTSTP Stop signal from tty; typically generated when you press CtrlZ (you can change this with stty) Stop the process
SIGTTIN Background read attempted from control terminal Stop the process
SIGTTOU Background write attempted to control terminal Stop the process
SIGURG Urgent condition on I/O channel Ignore the signal
SIGUSR1 User-defined signal 1 Kill the process
SIGUSR2 User-defined signal 2 Kill the process
SIGVTALRM Virtual timer expired. POSIX has marked this signal as obsolescent; QNX Neutrino doesn't support virtual timers or send this signal. Kill the process
SIGWINCH The size of the terminal window changed Ignore the signal
SIGXCPU Soft CPU time limit exceeded see the RLIMIT_CPU resource for setrlimit()) Kill the process and write a dump file

當一個服務器線程想通知一個客戶端線程時,有兩種合理的事件選擇: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的靈活,需要適當的犧牲一點效率。

消息隊列與文件類似,操作的接口相近。

Function Description
mq_open() Open a message queue
mq_close() Close a message queue
mq_unlink() Remove a message queue
mq_send() Add a message to the message queue
mq_receive() Receive a message from the message queue
mq_notify() Tell the calling process that a message is available on a message queue
mq_setattr() Set message queue attributes
mq_getattr() Get message queue attributes

Shared memory

共享內存提供了最高帶寬的IPC機制,一旦創建了共享內存對象,訪問對象的進程可以使用指針直接對其進行讀寫操作。共享內存本身是不同步的,需要結合同步原語一起使用,信號量和互斥鎖都適合與共享內存一塊使用,信號量一般用於進程之間的同步,而互斥鎖通常用於線程之間的同步,通通常來說互斥鎖的效率會比信號量要高。

共享內存與消息傳遞結合起來的IPC機制,可以提供以下特點:

  • 非常高的性能(共享內存)
  • 同步(消息傳遞)
  • 跨網絡傳遞(消息傳遞)

QNX中消息傳遞通過拷貝完成,當消息較大時,可以通過共享內存來完成,發送消息時不需要發送整個消息內容,只需將消息保存到共享內存中,並將地址傳遞過去即可。

Function Description Classification
shm_open() Open (or create) a shared memory region. POSIX
close() Close a shared memory region. POSIX
mmap() Map a shared memory region into a process's address space. POSIX
munmap() Unmap a shared memory region from a process's address space. POSIX
munmap_flags() Unmap previously mapped addresses, exercising more control than possible with munmap() QNX Neutrino
mprotect() Change protections on a shared memory region. POSIX
msync() Synchronize memory with physical storage. POSIX
shm_ctl(), shm_ctl_special() Give special attributes to a shared memory object. QNX Neutrino
shm_unlink() Remove a shared memory region. POSIX

通常會使用mmap來將共享內存區域映射到進程地址空間中來,如下圖所示:

void * mmap( void *where_i_want_it,
             size_t length,
             int memory_protections,
             int mapping_flags,
             int fd,
             off_t offset_within_shared_memory );

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

可以爲 memory_protections 指定以下保護類型:

Manifest Description
PROT_EXEC Memory may be executed.
PROT_NOCACHE Memory should not be cached.
PROT_NONE No access allowed.
PROT_READ Memory may be read.
PROT_WRITE Memory may be written.

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

這些標誌分爲兩部分-第一部分是類型,必須指定爲以下之一:

Map type Description
MAP_SHARED The mapping may be shared by many processes; changes are propagated back to the underlying object.
MAP_PRIVATE The mapping is private to the calling process; changes aren't propagated back to the underlying object. The mmap() function allocates system RAM and makes a copy of the object.

Typed memory

類型化內存是POSIX規範中定義的功能,它是高級實時擴展的一部分。 POSIX類型化內存,提供了一個接口來打開內存對象(以操作系統特定的方式定義),並對它們執行映射操作。這個對提供BSP/板級特定的地址佈局與設備驅動或用戶代碼之間的抽象時非常有用。

POSIX 指定以特定於實現的方式創建和定義類型化內存池(或對象)。 在 QNX Neutrino 下,類型化內存對象是根據系統頁面 asinfo 部分中指定的內存區域定義的。 因此,類型化內存對象直接映射到啓動定義的地址空間層次結構(asinfo 段)。 類型化內存對象還繼承了 asinfo 中定義的屬性,即內存段的物理地址(或邊界)。

一般來說,asinfo 條目的命名和屬性是任意的,完全在用戶的控制之下。 但是,有一些強制性條目:

  • memory 處理器的物理可尋址性,通常在 32 位 CPU 上爲 4GB(物理尋址擴展更多)。
  • ram 系統上的所有 RAM。這可能包含多個條目。
  • sysram 系統 RAM,即分配給操作系統管理的內存。 這也可能由多個條目組成。 操作系統將此池用於系統上的所有通用內存分配,包括代碼、數據、堆棧、堆、內核和進程管理器數據、共享內存對象和內存映射文件

您可以創建額外的條目,但只能在啓動時使用 as_add() 函數(參見構建嵌入式系統的“啓動庫”一章)。

Pipes and FIFOs

管道(Pipes)

管道是一種非命名IO通道,用於在多個進程之間的通信,一個進程往管道寫,其他進程從管道讀取。管道一般用於平行的兩個進程單向的傳遞數據,如果要雙向通信的話,就應該使用消息傳遞了。

管道管理器負責緩衝數據。緩衝區大小在<limits.h>文件中定義爲 PIPE_BUF。一旦兩端都已關閉,就將其移除。函數 pathconf()返回限制的值。管道通常在兩個進程並行運行時使用,數據在一個方向上從一個進程移動到另一個進程。(如果需要雙向通信,則應使用消息。)管道的典型應用是將一個程序的輸出連接到另一個程序的輸入。這種連接通常由 shell 完成。例如:

ls | more

If you want to: Use the:
Create pipes from within the shell pipe symbol (“
Create pipes from within programs pipe() or popen() functions

FIFOs

FIFOs與管道本質是一樣的,不同點在於FIFOs會在文件系統中保存爲一個永久的命名文件。

If you want to: Use the:
Create FIFOs from within the shell mkfifo utility
Create FIFOs from within programs mkfifo() function
Remove FIFOs from within the shell rm utility
Remove FIFOs from within programs remove() or unlink() function

參考文獻:

System Architecture - Interprocess Communication (IPC)

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