QNX IPC
進程間通信在將微內核從嵌入式實時內核轉換爲完整的POSIX操作系統的過程中起着至關重要的作用。隨着各種提供服務的進程被添加到微內核中,IPC是將這些組件連接成一個內聚整體的粘合劑。雖然在QNX中微子RTOS中,消息傳遞是IPC的主要形式,但是也有其他幾種形式。除非另有說明,否則這些其他形式的IPC是建立在我們的native消息傳遞之上的。該策略是創建一個簡單、健壯的IPC服務,可以通過微內核中的簡化代碼路徑進行性能調優;這樣就可以實現更多雜亂的IPC服務特性。
更高級的IPC服務(如通過我們的消息傳遞實現的管道和fifo)與一體化內核基準比較,顯示出了相當可觀的性能。
QNX中微子至少提供以下形式的IPC:
- Message-passing Implemented in Kernel
- Signals Implemented in Kernel
- POSIX message queue External process
- Share memory Process Manager
- Pipes External Process
- FIFOS External Process
設計人員可以根據帶寬需求、排隊需求、網絡透明性等來選擇這些服務。權衡可能很複雜,但靈活性也是有用的。
作爲定義微內核的工程工作的一部分,將重點放在作爲基本IPC原語的消息傳遞上是經過深思熟慮的。作爲IPC的一種形式,消息傳遞(在MsgSend()、MsgReceive()和MsgReply()中實現)就是是同步,並複製數據。讓我們更詳細地研究這兩個屬性。
Synchronous message passing 同步的消息傳遞
同步消息傳遞是QNX中微子RTOS中IPC的主要形式。向另一個線程(可能在另一個進程中)執行MsgSend()的線程將被阻塞,直到目標線程執行MsgReceive()、處理消息並執行MsgReply()。如果一個線程執行MsgReceive()而沒有一個先前發送的消息被掛起,它將阻塞,直到另一個線程執行了MsgSend ()。在QNX中微子中,服務器線程通常循環,等待從客戶端線程接收消息。
如前所述,線程(無論是服務器還是客戶端)位於準備狀態,如果它可以使用CPU。由於它和其他線程的優先級和調度策略,它實際上可能不會獲得任何CPU時間,但是線程不會被阻塞。讓我們先來看看客戶端線程:
- 如果客戶端線程調用MsgSend(),而服務器線程還沒有調用MsgReceive(),然後客戶端線程變爲SEND阻塞。一旦服務器線程調用MsgReceive(),內核將客戶端線程的狀態更改爲Reply阻塞,這意味着服務器線程已經收到消息,現在必須應答。
當服務器線程調用MsgReply()時,客戶機線程就變成READY。 - 如果客戶端線程調用MsgSend(),而服務器線程已經在MsgReceive()上被阻塞,那麼客戶端線程立即變成REPLY阻塞,完全跳過SEND阻塞狀態。
- 如果服務器線程失敗、退出或消失,則客戶端線程變成準備READY,MsgSend()返回錯誤。
接下來,讓我們考慮服務器線程:
4. 如果服務器線程調用MsgReceive(),並且沒有其他線程發送給它,那麼服務器線程將成爲RECEIVE阻塞。當另一個線程發送給它時,服務器線程就準備好了。
5. 如果服務器線程調用MsgReceive(),而另一個線程已經向它發送了消息,那麼MsgReceive()立即返回消息。在這種情況下,服務器線程不會阻塞。
6. 如果服務器線程調用MsgReply(),它不會被阻塞。
這種固有的阻塞會同步發送線程的執行,因爲請求發送數據的行爲也會導致發送線程被阻塞,而接收線程被調度執行。這並不需要內核進行顯式的工作來確定下一步運行哪個線程(與大多數其他形式的IPC一樣)。執行和數據直接從一個上下文移動到另一個上下文。
這些消息傳遞原語中省略了數據排隊功能,因爲在接收線程中需要時可以實現排隊。發送線程通常準備等待響應;排隊是不必要的開銷和複雜。(它減慢了跟非排隊情況比較)。因此,發送線程不需要進行單獨的、顯式的阻塞調用來等待響應(如果使用了其他IPC,就需要)。
雖然發送和接收操作是阻塞和同步的,但MsgReply()(或MsgError())不會阻塞。由於客戶端線程在等待回覆時已經被阻塞,因此不需要額外的同步,因此不需要阻塞MsgReply()。
這允許服務器響應客戶機並繼續處理,而內核和/或網絡代碼異步地將響應數據傳遞給發送線程,並將其標記爲準備執行。由於大多數服務器都傾向於進行一些處理以準備接收下一個請求(此時它們將再次阻塞),所以這樣做效果很好。
MsgReply()函數的作用是:向客戶端返回一個狀態零或多個字節。另一方面,MsgError()只用於向客戶端返回一個狀態。這兩個函數都將解除客戶機對其MsgSend()的阻塞。
Message copying 信息複製
由於我們的消息傳遞服務直接將消息從一個線程的地址空間複製到另一個線程,而沒有中間緩衝,因此消息傳遞性能接近底層硬件的內存帶寬。
內核對消息的內容沒有什麼特殊的意義——消息中的數據只有發送方和接收方相互定義的意義。然而,還提供了“定義良好的”消息類型,以便用戶編寫的進程或線程可以補充或替代系統提供的服務。
消息傳遞原語支持多部分傳輸,因此從一個線程的地址空間傳遞到另一個線程的消息不必預先存在於一個連續的緩衝區中。相反,發送和接收線程都可以指定一個向量表,指出發送和接收消息片段在內存中的位置。注意,發送方和接收方的各個部分的大小可能不同。
多部分傳輸允許將消息頭塊與數據塊分開發送,而無需對數據進行性能消耗的複製以創建連續消息。此外,如果底層數據結構是一個環形緩衝區,那麼指定一個由三部分組成的消息將允許將環形緩衝區中的頭和兩個不相交的範圍作爲單個原子消息發送。與此概念等價的硬件是DMA設施。
多部分傳輸也被文件系統廣泛使用。在讀取時,使用包含n個數據部分的消息將數據直接從文件系統緩存複製到應用程序中。每個部分有指針指向緩存,因爲緩存塊在內存中不是連續的,而讀取操作開始或結尾不止跨一個內存塊。
例如,緩存塊大小爲512字節,讀取1454字節就可以滿足5個部分的消息:
由於消息數據是在地址空間之間顯式複製的(而不是通過頁表操作),因此可以在堆棧上輕鬆地分配消息,而不是從用於MMU“頁面翻轉”的頁面對齊內存的特殊池中分配消息。因此,許多實現客戶機和服務器進程之間API的庫例程可以簡單地表達出來,而不需要複雜的特定於ipc的內存分配調用。
例如, 下面客戶端線程用來請求文件系統manager執行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);
}
這段代碼本質上在堆棧上構建了一個消息結構,用各種常量填充它,並從調用線程傳遞參數,然後將它發送給與fd相關的文件系統管理器。應答指示操作的成功或失敗。
這個實現並不會妨礙內核檢測大型消息傳輸並選擇爲這些情況實現“頁面翻轉”。由於傳遞的大多數消息都非常小,因此複製消息通常比操作MMU頁表要快。對於批量數據傳輸,進程之間共享內存(通過消息傳遞或其他同步原語進行通知)也是一個可行的選擇。
simple messages 簡單的消息
對於簡單的單部分消息,OS提供了直接將指針指向緩衝區的函數,而不需要IOV(輸入/輸出向量)。在這種情況下,部件的數量(part of number)被直接指向的message的size所代替。
在消息發送原語的情況下——它接受一個發送和一個應答緩衝區——這裏引入了四種變體:
Function | Send message | Reply message |
---|---|---|
MsgSend() | Simple | Simple |
MsgSendsv() | Simple | IOV |
MsgSendvs() | IOV | simple |
MsgSendv() | IOV | IOV |
其他消息傳遞原語只需在它們的名稱中去掉末尾的“v”:
IOV | simple |
---|---|
MsgReceivev() | MsgReceive() |
MsgReceivePulsev() | MsgReceivePulse() |
MsgReplyv() | MsgReply() |
MsgReadv() | MsgRead() |
MsgWritev() | MsgWrite() |
channels and connections 通道跟連接
在QNX中微子RTOS中,消息傳遞直接針對通道和連接,而不是直接針對線程之間的連接。希望接收消息的線程首先創建一個通道;另一個希望向該線程發送消息的線程必須首先通過“附加attaching”該通道建立連接。
channels 是在消息內核調用中申請的,並且被使用在server調用MsgReceive()上。connection是由客戶線程創建,用來連接到server的channel上。一旦建立了連接,客戶端就可以通過它們MsgSend消息。
如果一個進程中的許多線程都連接到同一個通道,那麼爲了提高效率,這些連接都映射到同一個內核對象。通道和連接在進程中由一個小的整數標識符命名。客戶端連接直接映射到文件描述符。
在架構上,這是一個關鍵點。通過讓客戶端連接直接映射到FDs,我們消除了另一層轉換。我們不需要去’找出’消息的發送地。相反,我們可以直接向“文件描述符”發送消息(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 );
}
這個循環允許線程從與通道有連接的任何線程接收消息。
NOTE:服務器還可以使用name_attach()創建一個通道,並將名稱與之關聯。然後,發送方進程可以使用name_open()來定位該名稱並創建到它的連接。
通道有幾個與之相關的消息列表:
- Receive: 一個後進先出線程隊列,線程等待着message。
- Send: 一個具有優先級的先進先出的隊列,其中的線程,已經發送消息但是消息還沒有被收到。
- Reply: 一個無序線程列表,線程已經發送了消息,消息也已經被收到了,但是還沒有回覆。
而在這些列表中,等待的線程被阻塞(即SEND-、RECV-或 REPLY-blocked)。多個線程和多個客戶端可以在一個通道上等待。
Pulses 脈衝
除了同步發送/接收/應答服務之外,操作系統還支持固定大小的非阻塞消息。這些被稱爲脈衝,攜帶一個小的有效負載(四個字節的數據加上一個單字節代碼)。
脈衝封裝相對較小的有效負載——8位代碼和32位數據。脈衝通常用作中斷處理程序中的通知機制。它們還允許服務器在不阻塞客戶機的情況下發出信號。
優先級繼承和消息
服務器進程按優先級順序接收消息和脈衝。當服務器中的線程接收請求時,它們將繼承發送線程的優先級(但不包括調度策略服務器進程按優先級順序接收消息和脈衝。當服務器中的線程接收請求時,它們將繼承發送線程的優先級(但不包括調度策略)。
因此,請求服務器工作的線程的相對優先級將被保留,並且服務器工作將以適當的優先級執行。這種消息驅動的優先級繼承避免了優先級反轉問題。
例如,假設系統包含以下內容:
- a server thread, at priority 22
- a client thread, T1, at priority 13
- a client thread, T2, at priority 10
在沒有優先級繼承的情況下,如果T2向服務器發送一條消息,那麼它實際上在優先級爲22的情況下完成了工作,因此T2的優先級被顛倒了。
實際發生的情況是,當服務器接收到消息時,它的有效優先級更改爲發送方的最高優先級,在本例中,T2的優先級低於服務器的優先級,因此當服務器接收到消息時,服務器的有效優先級將發生變化。
接下來,假設T1向服務器發送了一條消息,而它的優先級仍然是10。由於T1的優先級高於服務器當前的優先級,所以當T1發送消息時,服務器的優先級發生變化。
更改發生在服務器接收到消息之前,以避免出現另一種優先級反轉的情況。如果服務器的優先級保持在10,而另一個線程,T3,以優先級11開始運行,服務器必須等待,直到T3允許它有一些CPU時間,以便它最終可以接收到T1的消息。因此,T1將被低優先級線程T3延遲。
您可以通過在調用ChannelCreate()時指定_NTO_CHF_FIXED_PRIORITY標誌來關閉優先級繼承。如果使用自適應分區,此標誌還會導致接收線程不能在發送線程的分區中運行。
消息傳遞 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. |
Send/Receive/Reply的健壯實現
通過使用Send/Receive/Replay將QNX APP架構成爲一個多個線程和多個進程協作的團隊。這種架構導致QNX 是一個使用同步通知的系統。所以IPC 發生在指定轉變時發生,而不是異步。
異步系統的一個重要問題是,事件通知需要運行信號處理程序。異步IPC可能使徹底測試系統的操作變得困難,並確保無論信號處理程序何時運行,處理都將按預期的方式繼續也變得困難。
應用程序常常試圖通過依賴顯式打開和關閉的“窗口”來避免這種情況,在此期間信號將被接受。
使用圍繞發送/接收/應答構建的同步、非排隊的系統體系結構,可以非常容易地實現和交付健壯的應用程序體系結構。
在從排隊的IPC、共享內存和其他同步原語的各種組合構建應用程序時,避免死鎖是另一個難題。例如,假設線程A直到線程B釋放互斥鎖2才釋放互斥鎖1。不幸的是,如果線程B處於在線程A釋放互斥鎖1之前不釋放互斥鎖2的狀態,則會導致僵局。爲了確保死鎖不會在系統運行時發生,常常調用仿真工具。
發送/接收/應答IPC原語允許構造無死鎖的系統,只需要觀察這些簡單的規則:
- 永遠不要讓兩個線程互相發送
- 總是把你的線程安排在一個層次結構中,發送總是沿樹向上。
第一個規則是明顯避免僵局的情況,但第二個規則需要進一步解釋。例如:合作線程和進程團隊安排如下:
在這裏,層次結構中任何給定級別的線程都不會相互發送,而是隻向上發送。
這方面的一個例子可能是發送到數據庫服務器進程的客戶機應用程序,而數據庫服務器進程又發送到文件系統進程。當發送線程阻塞在目標線程的reply,而這時候目標線程不會阻塞在sending線程, 這樣死鎖不會發生。
但是,一個高級別線程如何通知一個低級別線程它有先前請求的操作的結果呢?(假設較低級別的線程不期望望發送時等待響應結果(最後一次發送)。)
QNX中微子RTOS通過MsgDeliverEvent()內核調用提供了一個非常靈活的體系結構來交付非阻塞事件。所有的通用異步服務都可以用它來實現。服務端的select()調用一個應用程序可用於允許線程在一組文件描述符上等待I/O事件完成的API。除了需要一個異步通知機制作爲從高級別線程到低級別線程的通知的“反向通道”之外,我們還可以圍繞此構建一個可靠的通知系統,用於定時器、硬件中斷和其他事件源。
一個相關的問題是,高級線程如何能夠請求低級線程的工作而不用Send給它,從而避免死鎖的風險。低級線程僅作爲高級線程的工作線程,根據請求執行工作。較低級別的線程將發送以"報告工作"形式,但較高級別的線程不會在那時回覆。它將延遲響應,直到更高級的線程工作完成,並且它將使用“描述工作”的數據進行響應(這是一個非阻塞操作)。實際上,應答是用來啓動工作的,而不是用來發送工作的,後者巧妙地避開了規則#1。
#Events 事件
QNX中微子內核設計的一個重大進步是事件處理子系統。POSIX及其實時擴展定義了許多異步通知方法(例如,Unix 信號是不能排隊或傳遞數據的,而POSIX實時信號卻可以排隊並傳遞數據,等等)。
內核還定義了額外的、特定於QNX中微子的通知技術,比如脈衝。實現所有這些事件機制可能會消耗大量代碼空間,因此我們的實現策略是在單個、豐富的事件子系統上構建所有這些通知方法。
這種方法的一個好處是,一種通知技術獨有的功能可以提供給其他技術。例如,應用程序可以將POSIX實時信號的相同隊列服務應用於UNIX信號。這可以簡化應用程序中信號處理程序的健壯實現。
執行線程遇到的事件可以來自以下三個來源:
- a MsgDeliverEvent() kernel call invoked by a thread 線程調用MsgDeliverEvent()系統調用
- an interrupt handler 中斷處理函數
- the expiry of a timer 計時器的過期
事件本身可以是許多不同類型中的任何一種:QNX中微子脈衝、中斷、各種形式的信號和強制的“解除阻塞”事件。“Unblock”是一種方法,通過這種方法,線程可以在沒有任何顯式事件實際交付的情況下從故意阻塞的狀態釋放出來。
考慮到事件類型的多樣性,以及應用程序需要請求最適合其需要的異步通知技術,要求服務器進程(上一節中的高級線程)攜帶支持所有這些選項的代碼將會很困難。
相反,客戶端線程可以向服務器提供一個數據結構(或“cookie”),以便稍後使用。當服務器需要通知客戶端線程時,它將調用
MsgDeliverEvent()和微內核將在客戶端線程上設置cookie中編碼的事件類型。
I/O notification
ionotify()函數的作用是:客戶端線程可以請求異步事件傳遞。許多POSIX異步服務(例如,mq_notify()和select())都構建在ionotify()之上。在對文件描述符fd執行I/O時,線程可以選擇等待I/O事件完成(對於寫()情況),或等待數據到達(對於讀()情況)。
相比讓線程阻塞在對資源管理器的讀寫服務請求上,ionotify()能夠允許client 線程post一個event到資源管理器(resource manager)然後在IO條件發生的時候,接受到事件通知。以這種方式等待允許線程繼續執行和響應事件源,而不僅僅是單個I/O請求。
select()調用是使用I/O通知實現的,它允許一個線程阻塞和等待多個fd上的混合I/O事件,同時繼續響應其他形式的IPC。
以下是可以交付所請求事件的條件:
- _NOTIFY_COND_OUTPUT : 輸出緩衝區中有更多數據的空間。
- _NOTIFY_COND_INPUT: 資源管理器定義的總數據可讀。
- _NOTIFY_COND_OBAND:資源管理器定義的“out-of-band"數據可用
Signals 信號
該操作系統支持32個標準的POSIX信號(as UNIX)和POSIX實時信號,這兩個信號都是具有統一功能的,由內核實現的64個信號中編號的。而POSIX標準定義的實時信號與unix形式不同(posix實時信號包含數據的四個字節,字節代碼和可能排隊等候交付),posix 實時信號可以顯式地選中或者去掉一個信號,而允許這個實現仍然符合標準。
順便說一句,如果應用程序需要,unix風格的信號可以選擇POSIX實時信號隊列。QNX中微子RTOS還擴展l了POSIX的信號傳遞機制,允許信號針對特定的線程,而不是簡單地針對包含線程的進程。由於信號是異步事件,所以它們也是通過事件傳遞機制實現的。
microkernel call | POSIX call | Description |
---|---|---|
SignalKill | kill(),pthread_kill(),raise(),sigqueue() | 在進程組,進程中,線程中設置信號 |
SignalAction() | sigaction() | 定義接收到信號後要採取的行動 |
SignalProcmask() | sigprocmask(),pthread_sigmask() | 改變一個線程的信號阻塞掩碼。 |
SignalSuspend() | sigsuspend(), pause() | 阻塞,直到信號調用信號處理程序。 |
SignalWaitinfo() | sigwaitinfo() | 等待信號並返回信息。 |
最初的POSIX規範只定義了進程上的信號操作。在多線程進程中,遵循以下規則:
- 信號動作是在進程級維護的。如果一個線程忽略或捕獲一個信號,它將影響進程中的所有線程。
- 信號掩碼保持在線程級。如果一個線程阻塞了一個信號,它只會影響這個線程。
- 針對某個線程的未忽略信號將單獨傳遞給該線程。
- 針對進程的未忽略信號被傳遞到第一個沒有阻塞該信號的線程。如果所有線程都阻塞了該信號,則該信號將在進程中排隊,直到任何線程忽略或解除阻塞該信號。如果忽略,進程上的信號將被刪除。如果解除阻塞,信號將從進程移動到解除阻塞的線程。
當一個信號針對一個有大量線程的進程時,必須掃描線程表,尋找一個沒有阻塞信號的線程。大多數多線程進程的標準實踐是在除一個線程之外的所有線程中屏蔽信號,該線程專門用於處理這些信號。爲了提高進程信號傳遞的效率,內核將緩存最後一個接受信號的線程,並始終嘗試首先將信號傳遞給它。
POSIX標準包括排隊實時信號的概念。QNX的中微子RTOS支持任意信號的可選排隊,而不僅僅是實時信號。可以在進程中逐個信號地指定隊列。每個信號可以有一個相關的8位代碼和一個32位值。
這與前面描述的消息脈衝非常相似。內核利用了這種相似性,並使用通用代碼來管理信號和脈衝。使用_SIGMAX - signo將信號號映射到脈衝優先級。因此,信號以優先級順序傳遞,信號數越低,優先級越高。這符合POSIX標準,該標準規定現有信號優先於新的實時信號.
NOTE:在信號處理程序中使用浮點運算是不安全的。
特殊信號
如前所述,操作系統總共定義了64個信號。它們的範圍如下:
Signal range | Description |
---|---|
1-57 | 57個POSIX信號(包括傳統的UNIX信號) |
41-56 | 16 POSIX實時信號(SIGRTMIN to)SIGRTMAX) |
57-64 | 8個特殊用途的QNX中微子信號 |
這八種特殊信號不能被忽視或捕捉。調用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()內核調用來同步通知。以下代碼將被阻塞,直到收到第8個特殊信號:
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()調用來通知客戶機。對於通知中的事件,有兩種合理的選擇:脈衝或信號。
當客戶機client 本身是其他客戶機的服務器時,脈衝是首選的方法,在這種情況下,這個客戶機將創建一個用於接受消息的通道,並且還可以接受脈衝。
對於大多數簡單的客戶端,情況並非如此。爲了接收脈衝,一個簡單的客戶端將被迫爲此創建一個通道。如果信號被配置爲同步信號(信號被阻塞)和排隊,則可以使用信號來代替脈衝(信號被阻塞)和排隊——這正是特殊信號的配置方式。客戶端將使用簡單的sigwaitinfo()調用替換MsgReceive()調用,後者用於等待通道上的脈衝,而sigwaitinfo()調用用於等待信號。
八種特殊信號包括用於特殊目的的命名信號:SIGSELECT Used by select() to wait for I/O from multiple servers.
信號總結
這個表描述了每個信號的含義:
signal | description |
---|---|
SIGABRT | 異常終止信號,例如由abort()函數發出的終止信號。 |
SIGALRM | 超時信號如由alarm()函數發出。 |
SIGBUS | 指示內存奇偶校驗錯誤(QNX)Neutrino-specific解釋)。請注意如果您的進程在此錯誤的信號處理程序中發生第二個錯誤,則該進程將終止。 |
SIGCHLD (or SIGCLD) | 子進程終止。默認操作是忽略信號。 |
SIGCONT | 如果進程不是HELD,默認操作是忽略信號,否則 continue |
SIGDEADLK | 互斥死鎖發生。如果一個進程在持有互斥鎖時死亡,而您沒有調用SyncMutexEvent()來設置要在互斥鎖死亡時傳遞給互斥鎖所有者的事件,那麼內核將傳遞一個SIGDEADLK給所有在沒有超時的情況下等待互斥鎖的線程。注意,SIGDEADLK和SIGEMT指的是同一個信號。一些實用程序(例如,gdb、ksh、slay和kill)知道SIGEMT,但不知道SIGDEADLCK。 |
SIGEMT | EMT指令(仿真器陷阱)。注意,SIGEMT和SIGDEADLK指的是同一個信號。 |
SIGFPE | 錯誤的算術運算(整數或浮點數),如除0或導致溢出的操作。請注意,當您的進程處於該錯誤的信號處理程序中時,如果發生第二個錯誤,該進程將被終止。 |
SIGHUP | 在控制終端檢測到會話領導人死亡或掛起。 |
SIGILL | 檢測無效的硬件指令。如果在你的信號處理程序中第二次發生這個錯誤,進程將會終止。請求I/O特權有可能會導致這個信號。線程請求類似特權:1:PROCMGR_AID_IO 2:ThreadCtl( _NTO_TCTL_IO, 0 ); |
SIGINT | 互動注意信號(中斷)。 |
SIGIOT | IOT指令(非x86硬件生成)。 |
SIGKILL | 終止信號只用於緊急情況。這個信號不能被捕捉或忽略。 |
SIGPIPE | 嘗試在沒有讀取器的管道上進行寫入。 |
SIGPOLL (or SIGIO) | Pollable event occurred. |
SIGPWR | 電源故障或重啓。 |
SIGQUIT | 互動的終止信號。 |
SIGSEGV | 檢測無效的內存引用。注意,當進程在的信號處理程序中時,發生第二次此故障,該進程將被終止。 |
SIGSTOP | 停止進程(默認)。這個信號不能被捕獲或忽略。 |
SIGSYS | 錯誤參數的系統調用 |
SIGTERM | 終端信號? |
SIGTRAP | 不受支持的軟件中斷。 |
SIGTSTP | 由鍵盤產生的停止信號。 |
SIGTTIN | 試圖從控制終端讀取背景信息。 |
SIGTTOU | 後臺寫試圖控制終端 |
SIGURG | socket出現緊急情況 |
SIGUSR1 | 保留爲應用程序定義的信號1。 |
SIGUSR2 | 保留爲應用程序定義的信號2。 |
SIGWINCH | Window size changed. |
POSIX消息隊列
POSIX定義了一組稱爲消息隊列的非阻塞消息傳遞工具。與管道一樣,消息隊列也是使用“讀取器”和“寫入器”操作的命名對象。作爲離散消息的優先級隊列,消息隊列具有比管道更多的結構,併爲應用程序提供對通信的更多控制。
*NOTE:要在QNX中微子RTOS中使用POSIX消息隊列,消息隊列服務器必須正在運行。QNX中微子有兩種消息隊列實現:
- 使用mqueue資源管理器的“傳統”實現。
- 使用mq服務器和異步消息的替代實現。
與我們固有的消息傳遞原語不同,POSIX消息隊列駐留在內核之外。
爲什麼使用POSIX消息隊列?
POSIX消息隊列爲許多實時程序員提供了一個熟悉的接口。它們類似於許多實時可執行文件的“郵箱”。
我們的消息和POSIX消息隊列之間有一個根本的區別。
我們的消息塊——它們直接在發送消息的進程的地址空間之間複製數據。另一方面,POSIX消息隊列實現了一種存儲轉發設計,在這種設計中,發送方不需要阻塞,並且可能有許多未完成的消息排隊。POSIX消息隊列獨立於使用它們的進程而存在。您可能會在一種設計中使用消息隊列,在這種設計中,隨着時間的推移,許多指定的隊列將由各種進程操作。
對於原始性能,在傳輸數據上,POSIX消息隊列將比QNX中微子本地消息慢。然而,隊列的靈活性可能會使這種小小的性能損失變得值得。
類文件接口
消息隊列類似於文件,至少就其接口而言類似文件。您可以使用mq_open()打開一個消息隊列,使用mq_close()關閉它,然後使用mq_unlink()銷燬它。要將數據放入(“寫”)並從(“讀”)消息隊列中取出數據,可以使用mq_send()和mq_receive()。
爲了嚴格遵守POSIX,您應該創建以單個斜槓(/)開頭的消息隊列,並且不包含其他斜槓。但是請注意,我們通過支持可能包含多個斜線的路徑名來擴展POSIX標準。例如,這允許公司將其所有消息隊列置於公司名稱之下,並更有把握地分發產品,以確保隊列名稱不會與另一家公司的名稱發生衝突。
在QNX中微子中,所有創建的消息隊列將出現在以下目錄的文件名空間中:
- /dev/mqueue 您正在使用傳統的(mqueue)實現
- /dev/mq 您正在使用備用(mq)實現
For example, with the traditional implementation:
mq_open() name: | Pathname of message queue: |
---|---|
/data | /dev/mqueue/data |
/qnx/data | /dev/mqueue/qnx/data |
您可以使用ls命令顯示系統中的所有消息隊列,如下圖所示:ls -Rl /dev/mqueue 打印的size是等待消息的數量。
Message-queue functions
POSIX消息隊列通過以下功能進行管理:
- mq_open(): Open a message queue
- mq_close(): Close a message queue
- mq_unlink(): Remove a message queue
- mq_send(): Remove a message queue
- mq_receive(): Receive a message from the message queue
- mq_notify():告訴調用進程消息隊列中有可用的消息
- mq_setattr(): Set message queue attributes
- mq_getattr(): Get message queue attributes
Shared memory 共享內存
共享內存提供了可用的最高帶寬IPC。一旦創建了共享內存對象,訪問該對象的進程可以使用指針直接讀寫該對象。這意味着對共享內存的訪問本身是不同步的。如果一個進程正在更新共享內存的一個區域,則必須小心防止另一個進程讀取或更新相同的區域。即使在簡單的讀操作中,其他進程也可能得到不穩定的信息。
爲了解決這些問題,共享內存通常與一個同步原語結合使用,在進程之間進行原子性更新。如果更新的粒度很小,那麼同步原語本身將限制使用共享內存固有的高帶寬。因此,當將大量數據作爲一個塊進行更新時,共享內存的效率最高。
信號量和互斥對象都是用於共享內存的合適的同步原語。通過POSIX實時進程間同步標準引入了信號量。在POSIX線程同步標準中引入了互斥鎖。互斥鎖也可以在不同進程的線程之間使用。POSIX認爲這是一個可選的功能;我們支持它。通常,互斥鎖比信號量更有效。
帶有消息傳遞的共享內存
共享內存和消息傳遞可以結合起來提供IPC,它提供:
- 非常高的性能(shared memory)
- synchronization (message-passing)
- network transparency (Message-passing)
使用消息傳遞,客戶端向服務器發送請求並阻塞。服務器按優先級順序從客戶端接收消息,處理它們,並在可以滿足請求時進行響應。此時,客戶機已被解除阻塞並繼續運行。發送消息的行爲本身就提供了客戶機和服務器之間的自然同步。與通過消息傳遞複製所有數據不同,消息可以包含對共享內存區域的引用,因此服務器可以直接讀寫數據。這可以用一個簡單的例子來解釋。
讓我們假設一個圖形服務器接受來自客戶端的繪製圖像請求,並將它們呈現到一個圖形卡上的幀緩衝區中。僅使用消息傳遞,客戶機將向服務器發送包含圖像數據的消息。這將導致圖像數據從客戶機的地址空間複製到服務器的地址空間。然後,服務器將呈現圖像併發出簡短的回覆。
如果客戶機沒有將圖像數據內聯到消息中,而是將引用發送到包含圖像數據的共享內存區域,那麼服務器可以直接訪問客戶機的數據。
客戶機在服務器上由於發送消息而被阻塞,所以服務器知道共享內存中的數據是穩定的,並且在服務器響應之前不會更改。這種消息傳遞和共享內存的組合實現了自然同步和非常高的性能。
這種操作模型也可以反過來–服務器可以生成數據並將其提供給客戶機。例如,假設客戶機向服務器發送一條消息,該消息請求直接從CD-ROM將視頻數據讀入客戶機提供的共享內存緩衝區。在更改共享內存時,客戶機將在服務器上被阻塞。當服務器響應並且客戶端繼續時,共享內存將是穩定的,以便客戶端訪問。這種類型的設計可以使用多個共享內存區域進行流水線操作。
簡單的共享內存不能在通過網絡連接的不同計算機上的進程之間使用。另一方面,消息傳遞是網絡透明的。服務器可以爲本地客戶端使用共享內存,爲遠程客戶端使用數據的完整消息傳遞。這允許您提供高性能的服務器,同時也是網絡透明的。
在實踐中,消息傳遞原語的速度對於大多數IPC需求來說已經足夠快了。只有在具有非常高帶寬的特殊應用中,才需要考慮組合方法的附加複雜性。
創建共享內存對象
一個進程中的多個線程共享該進程的內存。要在進程之間共享內存,您必須首先創建一個共享內存區域,然後將該區域映射到進程的地址空間。共享內存區域的創建和操作使用以下調用:
- 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以前映射的地址,使用munmap()執行更多的控制 QNX Neutrino
- mprotect() :更改共享內存區域上的保護。 POSIX
- msync():將內存與物理存儲器同步。POSIX
- shm_ctl(),shm_ctl_special() :爲共享內存對象提供特殊屬性。 QNX Neutrino
- shm_unlink():刪除共享內存區域。 POSIX
POSIX共享內存是通過進程管理器(procnto)在QNX中微子RTOS中實現的。以上調用被實現爲傳遞給procnto的消息(參見本書的Process Manager一章)。
shm_open()函數接受與open()相同的參數,並向對象返回一個文件描述符。與常規文件一樣,此函數允許您創建新的共享內存對象或打開現有的共享內存對象。
NOTE: 您必須打開文件描述符用讀訪問權限;如果您還想要在內存對象中寫入,那麼您還需要寫訪問權,除非您指定了一個私有(MAP_PRIVATE)映射。
當創建一個新的共享內存對象時,該對象的大小被設置爲零。要設置大小,可以使用ftruncate()—用於設置文件大小的函數或者shm_ctl ()。
mmap()
一旦有了共享內存對象的文件描述符,就可以使用mmap()函數將該對象或它的一部分映射到進程的地址空間。mmap()函數是QNX中微子內存管理的基礎,值得詳細討論它的功能。
NOTE:還可以使用mmap()將文件和類型化內存對象映射到進程的地址空間。
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 );
簡單來說就是:“在offset_within_shared_memory中映射與fd相關的共享內存對象的共享內存字節長度length。”mmap()函數將嘗試將內存放在地址空間中where_i_want_it所在的地址。內存將被賦予memory_protected指定的保護,映射將根據mapping_flags完成。
三個參數fd、offset_within_shared_memory和length定義了要映射到的特定共享對象一部分。通常在整個共享對象中進行映射,在這種情況下,偏移量爲零,長度爲共享對象的大小(以字節爲單位)。在Intel處理器上,長度是頁面大小的倍數,頁面大小爲4096字節。
mmap()的返回值將是映射對象的進程的地址空間中的地址。參數where_i_want_it被系統用作對象放置位置的提示。如果可能,對象將被放置在請求的地址。大多數應用程序指定一個零地址,這使系統可以自由地將對象放置在它希望的地方。
memory_protections參數可以爲以下類型:
- 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_SHARED: 映射可以由多個進程共享;更改將傳遞到基礎對象。
- MAP_PRIVATE:映射是調用進程的私有映射;更改不會傳遞到底層對象。函數的作用是:分配系統內存並做一份複製。
MAP_SHARED類型用於在進程之間設置共享內存;MAP_PRIVATE有更特殊的用途。您可以在上面的類型中OR或上多個標記,以進一步定義映射。這些在QNX中微子C庫參考的mmap()條目中有詳細描述。一些更有趣的標誌是:
MAP_ANON:映射不與任何文件描述符關聯的匿名內存;您必須將fd參數設置爲NOFD。函數的作用是:分配內存,默認情況下,用0來填充分配的內存;“ 詳細見“Initializing allocated memory”。
MAP_FIXED:將對象映射到由where_i_want_it指定的地址。如果共享內存區域包含指針,那麼您可能需要在所有映射該區域的進程中強制該區域使用相同的地址。這可以通過使用區域內的偏移量來代替直接指針來避免。
MAP_PHYS:此標誌表示希望處理物理內存。fd參數設置爲NOFD。當不使用MAP_ANON時,offset_within_shared_memory指定要映射的確切物理地址(例如,對於視頻幀緩衝區)。如果與MAP_ANON一起使用,則分配物理上連續的內存(例如,爲DMA緩衝區)。可以使用MAP_NOX64K和MAP_BELOW16M進一步定義MAP_ANON分配的內存和地址限制出現在某些DMA形式中。
NOTE:您應該使用mmap_device_memory()而不是MAP_PHYS,除非您正在分配物理上連續的內存。
MAP_NOX64K:與MAP_PHYS | MAP_ANON一起使用。分配的內存區域不會跨越64 kb的邊界。這是舊的16位PC DMA所需要的。
MAP_BELOW16M:與MAP_PHYS | MAP_ANON一起使用。分配的內存區域將駐留在16mb以下的物理內存中。這在使用ISA 總線的DMA 時是必需的。
MAP_NOINIT:放寬POSIX要求,使分配的內存爲零;
使用上面描述的映射標誌,一個進程可以很容易地在進程之間共享內存:
fd = shm_open("datapoints", O_RDWR);
addr = mmap(0, len, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);
或爲總線控制的PCI網卡分配一個DMA buffer:
addr = mmap(0, 262144, PROT_READ|PROT_WRITE|PROT_NOCACHE,
MAP_PHYS|MAP_ANON, NOFD, 0);
可以使用munmap()從地址空間取消對共享內存對象的全部或部分映射。這個原語並不侷限於取消對共享內存的映射—它可以用於取消對進程中任何內存區域的映射。當與mmap()的MAP_ANON標誌一起使用時,您可以很容易地實現一個私有的頁面級分配器/釋放器.
可以使用mprotect()更改內存映射區域上的保護。與munmap()一樣,mprotect()並不侷限於共享內存區域——它可以更改進程中任何內存區域的保護。
初始化分配的內存
POSIX要求mmap()zero它所分配的任何內存。初始化內存可能需要一段時間,因此QNX中微子提供了一種放鬆POSIX需求的方法。這允許更快的啓動,但可能是一個安全問題。
避免初始化內存需要執行unmapping的進程和執行mapping的進程的合作:
- munmap_flags()函數是一個非posix函數,類似於munmap(),但讓你控制當內存下一次映射時會發生什麼:
int munmap_flags( void *addr, size_t len, unsigned flags );
如果將flags參數指定爲0,則munmap_flags()的行爲與munmap()相同。
以下位控制分配時內存的清除:
UNMAP_INIT_REQUIRED:在下一次分配底層物理內存時,需要將頁面全部初始化爲0。
UNMAP_INIT_OPTIONAL:將底層物理內存的初始化設置爲零是可選的。
- 如果您將MAP_NOINIT標誌指定給mmap(),並且mmap()的物理內存以前使用UNMAP_INIT_OPTIONAL munmap(),則POSIX要求將內存歸零的要求是寬鬆的。
默認情況下,內核初始化內存,但是您可以通過使用-m選項的procnto來控制它。這個選項的參數是一個字符串,讓你啓用或禁用內存管理器的一些方面:
i:munmap()的作用就好像指定了UNMAP_INIT_REQUIRED一樣
~i:munmap()的作用就好像指定了UNMAP_INIT_OPTIONAL一樣。
默認情況下,當釋放內存供以後重用時,該內存的內容保持不變;無論擁有遺留內存的應用程序是什麼,它都將保持不變,直到下一次該內存被另一個進程分配時爲止。在QNX中微子6.6及以後版本中,procnto的-m選項允許您控制取消映射時的默認行爲:
- -mc:釋放內存時清除內存。
- -m~c: 釋放內存時不清除內存(默認情況下)。當釋放內存供以後重用時,內存的內容保持不變;無論擁有遺留內存的應用程序是什麼,它都將保持不變,直到下一次該內存被另一個進程分配時爲止。此時,在內存被傳遞給下一個進程之前,它被歸零。
Typed memory
類型化內存是POSIX在1003.1規範中定義的功能。它是高級實時擴展的一部分,清單位於<sys/mman.h >頭文件。
類型化內存將以下函數添加到C庫:
- posix_typed_mem_open():打開一個類型化內存對象。該函數返回一個文件描述符,然後可以將該文件描述符傳遞給mmap(),以建立類型化內存對象的內存映射。
- posix_typed_mem_get_info():獲取關於類型化內存對象的信息(當前可用內存的數量)。
POSIX類型化內存爲打開內存對象(以特定於操作系統的方式定義)並在這些對象上執行映射操作提供了一個接口。在BSP或特定於板的地址佈局與設備驅動程序或用戶代碼之間提供了一個抽象。
實現定義的行爲
POSIX指定以特定於實現的方式創建和定義類型化內存池(或對象)。
Seeding of typed memory regions
在QNX中微子下,類型化內存對象是從系統頁面的asinfo部分指定的內存區域定義的。因此,類型化內存對象直接映射到由startup定義的地址空間層次結構(asinfo段)。類型化內存對象還繼承asinfo中定義的屬性,即內存段的物理地址(或界限)。
通常,asinfo條目的命名和屬性是任意的,完全由用戶控制。然而,也有一些強制性的條目:
- memory:處理器的物理尋址能力,在32位CPU上通常是4GB.
- ram: 系統上所有的RAM。這可能包含多個條目.
- sysram:系統內存, 即內存已經被分配給操作系統來管理。這也可能包含多個條目。
因爲按照慣例sysram是分配給操作系統的內存,所以這個內存池與操作系統用來滿足匿名mmap()和malloc()請求的內存池是相同的。可以使用as_add()函數在啓動時創建其他條目。
Naming of typed memory regions
類型化內存區域的名稱直接派生自asinfo段的名稱。asinfo部分本身描述了一個層次結構,因此類型化內存對象的命名是一個層次結構。
下面是一個系統配置示例:
name | Range(start,end) |
---|---|
/memory | 0, 0xFFFFFFFF |
/memory/ram | 0, 0x1FFFFFF |
/memory/ram/sysram | 0x1000, 0x1FFFFFF |
/memory/isa/ram/dma | 0x1000, 0xFFFFFF |
/memory/ram/dma | 0x1000, 0x1FFFFFF |
傳遞給posix_typed_mem_open()的名稱遵循上述命名約定。POSIX允許實現定義當名稱不是以斜槓(/)開頭時會發生什麼。關於開放的決議規則如下:
- 如果名稱以/開頭,則執行精確匹配。
- 名稱可能包含中間/字符。這些被認爲是路徑分量分隔符。如果指定了多個路徑組件,則從下往上匹配它們(與解析文件名的方式相反)。
- 如果名稱不是以前導/開頭,則對指定的pathname組件執行尾匹配.
以下是posix_typed_mem_open()如何使用上面的示例配置解析名稱的一些示例:
This name: | Resolves to: | See: |
---|---|---|
/memory | /memory Rule 1 | |
/memory/ram | /memory/ram Rule 2 | |
/sysram | Fails | |
sysram | /memory/ram/sysram Rule 3 |
Pathname space and typed memory
類型化內存名稱層次結構是通過/dev/ tymem之下的process manager名稱空間導出的。應用程序可以列出這個層次結構,並查看系統頁面中的asinfo條目,以獲得關於類型化內存的信息。
NOTE:與共享內存對象不同,您不能通過名稱空間接口打開類型化內存,因爲posix_typed_mem_open()接受額外的參數tflag,這是必需的,open() API中沒有提供。
mmap() allocation flags and typed memory objects
對於類型化內存,考慮了以下分配和映射的一般情況:
- 類型化內存池顯式地從(POSIX_TYPED_MEM_ALLOCATE和POSIX_TYPED_MEM_ALLOCATE_CONTIG)分配。一個匿名對象的MAP_SHARED:
mmap(0, 0x1000, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANON,NOFD, 0);
內存被分配了,不能用於其他分配,但是如果您派生進程,子進程也可以訪問它。當最後一個到內存的映射被刪除時,內存被釋放。
請注意,就像有人使用mem_offset()和MAP_PHYS來獲得對先前分配的內存的訪問一樣,其他人可以使用POSIX_TYPED_MEM_MAP_ALLOCATABLE(或者沒有標記)打開類型化內存對象,並通過這種方式獲得對相同物理內存的訪問。
POSIX_TYPED_MEM_ALLOCATE_CONTIG 就像 MAP_ANON | MAP_SHARED一樣,它會導致一個連續的分配。
- POSIX_TYPED_MEM_MAP_ALLOCATABLE用於創建到對象的映射,而不需要分配或釋放位置。這相當於到物理內存的共享映射。
您應該只使用MAP_SHARED映射,因爲對MAP_PRIVATE映射的寫操作(通常)將在普通的匿名內存中爲進程創建一個私有副本。
如果不指定任何標誌,或者指定POSIX_TYPED_MEM_MAP_ALLOCATABLE,則mmap()的偏移參數指定類型內存區域中的起始物理地址;如果類型化內存區域是不連續的(多個asinfo條目),那麼允許的偏移量也是不連續的,並且不像共享內存對象那樣從零開始。如果您指定的[paddr, paddr + size]區域位於允許的類型化內存對象地址之外,則mmap()失敗返回ENXIO。
Permissions and typed memory objects
類型化內存對象的權限由UNIX權限控制。posix_typed_mem_open()的oflags參數指定所需的訪問權限,這些標誌將根據類型內存對象的權限掩碼進行檢查。
POSIX沒有指定如何將權限分配給類型化內存對象。在QNX中微子下,默認權限是在系統啓動時分配的。默認情況下,root是所有者和組,具有讀寫權限;其他人沒有任何權限。目前,還沒有改變對象權限的機制。將來,可能會擴展該實現以允許chmod()和chown()修改權限。
Object length and offset definitions
您可以使用posix_typed_mem_get_info()來檢索對象的大小。posix_typed_mem_get_info()調用填充posix_typed_mem_info結構,其中包含posix_tmi_length字段,該字段包含類型化內存對象的大小。
正如POSIX所指定的,length字段是動態的,幷包含該對象的當前可分配大小(實際上是對象的空閒大小POSIX_TYPED_MEM_ALLOCATE和POSIX_TYPED_MEM_ALLOCATE_CONTIG)。如果使用tflag爲0或POSIX_TYPED_MEM_MAP_ALLOCATABLE打開對象,則長度字段設置爲0。
在類型化內存對象中進行映射時,通常會向mmap()傳遞一個偏移量。偏移量是對象中應該開始映射的位置的物理地址。只有在使用tflag爲0或POSIX_TYPED_MEM_MAP_ALLOCATABLE打開對象時,偏移量才合適。如果您使用POSIX_TYPED_MEM_ALLOCATE或POSIX_TYPED_MEM_ALLOCATE_CONTIG打開類型化內存對象,非零偏移會導致對mmap()的調用失敗,錯誤爲EINVAL。
Interaction with other POSIX APIs
類型化內存可以與其他POSIX api交互。
- rlimits:POSIX setrlimit() api提供了設置進程可以使用的虛擬和物理內存的極限的能力。由於類型化內存操作可以在普通RAM (sysram)上操作,並且會在進程的地址空間中創建映射,所以在進行rlimit覈算時需要考慮這些操作。以下規則特別適用:
- mmap()爲類型化內存對象創建的任何映射都被計算在進程的RLIMIT_VMEM或RLIMIT_AS limit中。
- 類型化內存從不對RLIMIT_DATA計數。
- POSIX file-descriptor functions:您可以使用posix_typed_memory_open()返回的文件描述符進行POSIX基於文件描述符的調用,如下所示:
- fstat(fd,…),它填充stat結構,就像它填充共享內存對象一樣,只是size字段不包含類型化內存對象的大小。
- close(fd) 關閉文件描述符。
- dup()和dup2()複製文件句柄。
- posix_mem_offset()的行爲與POSIX規範中記錄的一樣。
practical examples
下面是一些如何使用類型化內存的示例:
- 從系統RAM中分配連續內存
下面是一個從系統RAM中分配連續內存的代碼片段:
int fd = posix_typed_mem_open( "/memory/ram/sysram", O_RDWR,
POSIX_TYPED_MEM_ALLOCATE_CONTIG);
void *vaddr = mmap( NULL, size, PROT_READ | PROT_WRITE,
MAP_PRIVATE, fd, 0);
- 定義包內存並從中分配
假設您有特殊的內存(比如fast SRAM),希望用於包內存。這個SRAM沒有放在全局系統RAM池中。相反,在啓動時,我們使用as_add()(參見構建的自定義映像啓動程序一章)爲包內存添加一個asinfo條目:
as_add(phys_addr, phys_addr + size - 1, AS_ATTR_NONE,
"packet_memory", mem_id);
其中,phys_addr是SRAM的物理地址,size是SRAM的大小,mem_id是父節點的ID(通常是內存,由as_default()返回)。
這段代碼爲packet_memory創建一個asinfo條目,然後可以將其用作POSIX typed memory。下面的代碼允許不同的應用程序從packet_memory分配頁面:
int fd = posix_typed_mem_open( "packet_memory", O_RDWR,POSIX_TYPED_MEM_ALLOCATE);
void *vaddr = mmap( NULL, size, PROT_READ | PROT_WRITE,MAP_SHARED, fd, 0);
或者,您可能希望使用包內存作爲直接共享的物理緩衝區。在這種情況下,應用程序將使用它如下:
int fd = posix_typed_mem_open( "packet_memory", O_RDWR,
POSIX_TYPED_MEM_MAP_ALLOCATABLE);
void *vaddr = mmap( NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, offset);
- 定義一個dma安全區域
在某些硬件上,由於芯片組或內存控制器的限制,可能無法對系統中的任意地址執行DMA。在某些情況下,芯片組只能對所有物理RAM的一個子集進行DMA。傳統上,如果不靜態地保留驅動程序DMA緩衝區的RAM的一部分(這是潛在的浪費),就很難解決這個問題。類型化內存提供了一個乾淨的抽象來解決這個問題。這裏有一個例子:
在啓動時,使用as_add_containing()(參見構建嵌入式系統的自定義映像啓動程序一章)來定義一個用於dma安全內存的asinfo條目。讓這個條目成爲ram的子條目:
as_add_containing( dma_addr, dma_addr + size - 1,AS_ATTR_RAM, "dma", "ram");
dma_addr是dma安全RAM的起始位置,size是dma安全區域的大小。這段代碼爲dma創建一個asinfo條目,它是ram的一個子條目。然後驅動程序可以使用它來分配dma安全緩衝區:
int fd = posix_typed_mem_open( "ram/dma", O_RDWR,
POSIX_TYPED_MEM_ALLOCATE_CONTIG);
void *vaddr = mmap( NULL, size, PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
管道和fifo
管道和fifo都是連接進程的兩種隊列形式。爲了在QNX中微子RTOS中使用管道或FIFOs,管道資源管理器(pipe)必須正在運行。
管道
管道是作爲兩個或多個協作進程之間的I/O通道的未命名文件:一個進程向管道中寫入,另一個進程從管道中讀取。管道管理器負責緩衝數據。緩衝區大小在<limit.h >文件中定義爲PIPE_BUF。管道的兩端一旦閉合就被移走。函數pathconf()返回極限的值。
當兩個進程希望並行運行時,通常使用管道,數據從一個進程以單一方向移動到另一個進程。(如果需要雙向通信,則應該使用消息。)管道的典型應用程序是將一個程序的輸出連接到另一個程序的輸入。這種連接通常是由shell實現的。例如:ls | more. 將標準輸出從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 |