如果你認爲本系列文章對你有所幫助,請大家有錢的捧個錢場,點擊此處贊助,贊助額0.1元起步,多少隨意
聲明:本文只用於個人學習交流,若不慎造成侵權,請及時聯繫我,立即予以改正
鋒影
email:[email protected]
介紹
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
機制類似。
multipart transfer
分塊傳輸也用在文件系統中,比如讀數據的時候,將文件系統緩存中的數據分塊讀到用戶提供的空間內,如下圖:
對於簡單的單塊消息傳遞,就不需要通過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
通常被用在中斷處理函數中,用作通知機制;也允許服務器在不阻塞客戶端的情況下,向客戶端發送信號。
Pulses pack a small payload
優先級繼承與消息
服務器進程按照優先級順序來接收消息和脈衝,當服務器中的線程接收請求時,它們將繼承發送線程的優先級。請求服務器工作的線程的優先級被保留,服務器工作將以適當的優先級執行,這種消息驅動的優先級繼承避免了優先級反轉的問題。
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會在文件系統中保存爲一個永久的命名文件。