Getting_Started_with_QNX_Neutrino -- Chapter 2:Message Passing

在本章中,我們將介紹QNX Neutrino最具特色的功能,即Message Passing。Message Passing是操作系統微內核架構的核心,爲操作系統提供了模塊化。

A small microkernel and message passing

QNX Neutrino的主要優勢之一是它具有可擴展性。通過“可擴展性”,我的意思是它可以定製在具有嚴格內存限制的小型嵌入式盒子上工作,也可以延伸到具有幾乎無限內存的多處理器SMP盒的大型網絡。

QNX Neutrino通過使每個提供服務的組件模塊化來實現其可擴展性。這樣,您只需要在最終系統中包含所需的組件就可實現這些功能。通過在設計中使用線程,您還可以幫助它擴展到SMP系統(我們將在本章中看到線程的更多用途)。

這在QNX系統初始設計中就一直沿用的理念,並且一直延續至今。這樣的設計關鍵是一個小型微內核架構,其傳統上將模塊作爲可選組件整合到單片內核中。

您,系統架構師,決定需要使用哪些模塊。您是否需要項目中的文件系統?如果是,則添加一個。如果你不需要,那麼不需要包含。你需要一個串口驅動嗎?無論答案是肯定還是否定,這都不會影響(也不會影響)您之前關於文件系統的決定。

在運行時,您可以決定正在運行的系統中包含哪些系統組件。您可以在實時系統中動態刪除組件,並在其他時間重新安裝它們或其他系統。這些“drivers”有什麼特別之處嗎?不,他們只是常規的用戶級程序,恰好用硬件執行特定的工作。實際上,我們將在資源管理器章節中看到如何編寫它們。

實現這一目標的關鍵是Message Passing。在QNX Neutrino下,功能模塊並不是直接綁定到OS內核中,而是與內核建立某種特殊的聯繫,這種聯繫即通過它們之間的消息傳遞進行通信。內核基本上只負責線程級服務(例如,調度)。事實上,消息傳遞並不僅僅用於此功能模塊的安裝和卸載技巧 - 它幾乎是所有其他服務的基本構建塊(例如,內存分配是通過向進程管理器發送的消息來執行的)。當然,也有一些服務是由直接內核調用提供的。

考慮打開一個文件並向其寫入一個數據塊。這是通過從應用程序發送到QNX Neutrino的可安裝組件(稱爲文件系統)的許多消息來實現的。該消息告訴文件系統打開一個文件,然後另一條消息告訴它寫一些數據(幷包含該數據)。不過不用擔心 - QNX Neutrino操作系統可以非常快速地執行消息傳遞。

Message passing and client/server

想象一下應用程序從文件系統讀取數據。在QNX lingo中,應用程序是一個從服務器請求數據的客戶端。

此客戶端/服務器模型引入了與Message Passing相關的幾個進程狀態(我們在“進程和線程”一章中討論了這些狀態)。最初,服務器正在等待消息從某個地方到達。此時,服務器被稱爲接收阻止(也稱爲RECEIVE狀態)。這是一些示例pidin輸出:

在上面的示例中,pseudo-tty服務器(稱爲devc-pty)是進程ID 4,其有一個線程(線程ID 1),以優先級10 Round-Robin運行,並且是接收阻塞的,等待來自通道ID爲1的消息。

收到消息後,服務器進入READY狀態,並且能夠運行。如果它恰好是最高優先級的READY進程,它將獲得CPU並可以執行某些處理。由於它是一個服務器,它會查看它剛剛收到的消息,並決定如何處理它。在某些時候,服務器將完成消息告訴它要做的任何工作,然後將“回覆”給客戶端。我們切換到客戶端。最初客戶端正在運行,消耗CPU,直到它決定發送消息。客戶端從READY更改爲send-blocked或reply-blocked,具體取決於它向其發送消息的服務器的狀態。

通常,你會更頻繁的看到回覆阻塞狀態,與發送阻塞狀態相比。這是因爲回覆阻止狀態意味着:

服務器已收到消息,現在正在處理它。在某些時候,服務器將完成處理,並將其回覆給客戶端。客戶端此時處於被阻塞狀態,等待服務器的回覆。

與發送阻塞狀態對比:

服務器尚未收到消息,很可能是因爲它正忙於處理另一條消息。當服務器轉向“接收”客戶端的消息時,您將從發送阻止狀態進入回覆阻塞狀態。

實際上,如果您看到一個發送阻止的進程,則意味着以下兩種情況之一:

  1. 在服務器忙於爲客戶端提供服務的情況下,您碰巧take a snapshot of the system,併爲該服務器收到了新的請求。這是正常情況; 您可以通過再次運行pidin來驗證它以獲取新快照。這次你可能會看到該進程不再被阻止發送。

  2. 服務器遇到了錯誤,無論出於何種原因,都不再收聽請求。發生這種情況時,您會看到許多進程在一臺服務器上發送阻止。要驗證這一點,請再次運行pidin,觀察客戶端進程的阻塞狀態沒有變化。

這是一個示例,顯示了一個回覆阻止客戶端及其被阻止的服務器:

這表明程序esh(嵌入式shell)已經向進程號1(內核和進程管理器,procnto-smp-instr)發送了一條消息,現在正在等待回覆。

現在您瞭解了客戶端/服務器體系結構中消息傳遞的基礎知識。

所以現在你可能會想,“我是否必須編寫特殊的QNX Neutrino Message Passing API來打開文件或讀寫一些數據?!?”

你不必編寫任何Message Passing函數,除非你想“陷入困境”(我將在稍後討論)。實際上,讓我向您展示一些傳遞消息的客戶端代碼:

#include <fcntl.h>
#include <unistd.h>

 

int main (void)
{
int fd;
fd = open ("filename", O_WRONLY);
write (fd, "This is message passing\n", 24);
close (fd);
return (EXIT_SUCCESS);
}

看到?標準C代碼,沒有什麼棘手的。Message Passing由QNX Neutrino C庫完成。您只需發出標準POSIX 1003.1或ANSI C函數調用,C庫就會爲您執行消息傳遞工作。

在上面的例子中,我們看到三個函數被調用,三個不同的消息被髮送:

open() sent an “open” message
write() sent a “write” message
close() sent a “close” message

當我們查看資源管理器時(在資源管理器章節中),我們將更詳細地討論消息本身,但是現在您需要知道的是發送了不同類型的消息。

讓我們退一步,將其與傳統操作系統中的示例進行對比。

客戶端代碼將保持不變,並且供應商提供的C庫將隱藏差異。在這樣的系統上,open()函數調用將調用內核函數,該函數然後將直接調用文件系統,該文件系統將執行一些代碼,並返回文件描述符。write()和close()調用會做同樣的事情。

所以?以這種方式做事有什麼好處嗎? 繼續閱讀!

Network-distributed message passing

假設我們想要更改上面的示例,以便與網絡上的其他節點進行通信。你可能認爲我們必須調用特殊的函數調用來“聯網”。這是網絡版的代碼:

#include <fcntl.h>
#include <unistd.h>

 

int main (void)
{
int fd;
fd = open ("/net/wintermute/home/rk/filename", O_WRONLY);
write (fd, "This is message passing\n", 24);
close (fd);
return (EXIT_SUCCESS);
}

如果您認爲兩個版本的代碼幾乎相同,那麼您是對的。它確實如此。

在傳統的操作系統中,C庫open()調用進入內核,查看文件名,然後內核調用網絡文件系統(NFS)代碼,該代碼確定/net/wintermute/home/rk/filename實際上在哪裏。然後,NFS調用網絡驅動程序並在節點wintermute上向內核發送消息,然後重複我們在原始示例中描述的過程。請注意,在這種情況下,實際上涉及兩個文件系統; 一個是NFS客戶端文件系統,一個是遠程文件系統。不幸的是,取決於遠程文件系統和NFS的實現,由於不兼容性,某些操作可能無法按預期工作(例如,文件鎖定)。

在QNX Neutrino下,C庫open()創建了它將發送到本地文件系統的相同消息,並將其發送到節點wintermute上的文件系統。在本地和遠程情況下,使用完全相同的文件系統。

這是QNX Neutrino的另一個基本特徵:網絡分佈式操作本質上是自由的(或者說是通用的),因爲通過Message Passing完成客戶端與服務器的解耦工作。

在傳統內核上,存在“雙重標準”,其中本地服務以單向實現,而遠程(網絡)服務以完全不同的方式實現。

What it means for you

Message Passing is elegant and network-distributed。所以呢?它給你帶來了什麼?

嗯,這意味着你的程序繼承了這些特性 - 它們也可以成爲network-distributed,其工作遠遠少於其他系統。但我覺得最有用的好處是它們讓你以一種漂亮優雅的模塊化方式測試軟件。

您可能曾參與大型項目,許多人必須提供不同的軟件。當然,其中一些人遲早比其他人做得好。

這些項目經常在兩個階段出現問題:最初是在項目定義時,很難確定一個人的開發工作在哪裏結束而另一個人開始,然後是在測試/集成時,何時無法進行完整的系統集成測試,因爲所有的作品都不可用。

通過Message Passing方式,項目的各個組件可以非常容易地分離開來,從而實現非常簡單的設計和簡單的測試。如果您想根據現有範例考慮這一點,它與面向對象編程(OOP)中使用的概念非常相似。

歸結起來的是,測試可以逐件進行。您可以設置一個簡單的程序,將消息發送到您的服務器進程,並且由於該服務器進程的輸入和輸出是(或應該!)有詳細記錄,您可以確定該進程是否正常運行。哎呀,這些測試用例甚至可以自動化並放置在定期運行的迴歸套件中!

 

The philosophy of QNX Neutrino

Message Passing是QNX Neutrino哲學的核心。瞭解消息傳遞的用途和含義將是有效利用操作系統的關鍵。在我們進入細節之前,讓我們先看一下理論。

Multiple threads

儘管客戶端/服務器模型易於理解且最常用,但主題還有兩個其他變體。第一個是使用多個線程(本節的主題),第二個是稱爲服務器/子服務器的模型,它有時對一般設計有用,但在網絡分佈式設計中確實很有用。兩者的結合可以非常強大,特別是在SMP盒網絡上!

正如我們在“進程和線程”一章中所討論的那樣,QNX Neutrino能夠在同一進程中運行多個線程。當我們將它與Message Passing結合起來時,我們如何才能利用它?

答案很簡單。我們可以啓動一個線程池(使用我們在Processes and Threads章節中討論過的thread_pool _ *()函數),每個函數都可以處理來自客戶端的消息:

這樣,當客戶端向我們發送消息時,只要工作完成,我們就不關心哪個線程獲取它。這具有許多優點。與僅使用一個線程爲多個客戶端服務相比,爲多個客戶端提供多線程服務的能力是一個強大的概念。主要優點是內核可以在各種客戶端之間多任務處理服務器,而不需要服務器本身執行多任務處理。

在單處理器機器上,運行一堆線程意味着它們都在相互競爭CPU時間。

但是,在SMP盒子上,我們可以讓多個線程競爭多個CPU,同時在這些多個CPU之間共享相同的數據區域。這意味着我們僅受該特定計算機上可用CPU數量的限制。

Server/subserver

現在讓我們看一下服務器/子服務器模型,然後我們將它與多線程模型結合起來。

在此模型中,服務器仍然向客戶端提供服務,但由於這些請求可能需要很長時間才能完成,因此我們需要能夠啓動請求並仍能夠處理來自其他客戶端的新請求

如果我們嘗試使用傳統的單線程客戶端/服務器模型執行此操作,一旦收到並啓動了一個請求,除非我們定期停止我們正在做的事情,否則我們將無法再接收任何其他請求,快速查看是否還有其他待處理請求,將這些請求放在工作隊列中,然後繼續,將注意力分散到工作隊列中的各種作業上。效率不高。你實際上通過多個作業之間的“時間切片”來複制內核的工作!

想象一下如果你這樣做會是什麼樣子。你在辦公桌前,有人帶着一個裝滿工作的文件夾向你走來。你開始研究它了。當你忙着工作的時候,你會注意到有人站在你的隔間的門口,同時有更多優先工作需要處理(當然)!現在你的辦公桌上有兩堆工作。你在一堆上花了幾分鐘,切換到另一堆,等等,一直看着你的門口,看看是否還有其他人帶着更多的工作。

服務器/子服務器模型在這裏會更有意義。在這個模型中,我們有一個服務器,可以創建其他幾個進程(子服務器)。這些子服務器每個都向服務器發送一條消息,但服務器在收到客戶端請求之前不會回覆它們。然後,它通過回覆它應該執行的作業將客戶端的請求傳遞給其中一個子服務器。下圖說明了這一點。注意箭頭的方向-它們表示發送的方向!

如果你正在做這樣的工作,你首先要僱用一些額外的員工。這些員工都會來找你(正如子服務器向服務器發送消息 - 因此關於上圖中箭頭的註釋),尋找工作要做。最初,您可能沒有,所以您不會回覆他們的查詢。當有人帶着一個裝滿工作的文件夾進入你的辦公室時,你會告訴你的一位員工(子服務器),“這裏有一些工作要做。”然後那個員工(子服務器),就去做了工作。隨着其他工作的進入,您將其委託給其他員工(子服務器),。

這個模型的訣竅在於它是reply-driven - 當您回覆子服務器時,工作就開始了。標準客戶端/服務器模型是send-driven的,因爲在向服務器發送消息時工作開始

那麼爲什麼客戶會進入您的辦公室,而不是您僱傭的員工辦公室?你爲什麼“仲裁”這項工作?答案很簡單:你是負責執行特定任務的協調員。由您來確保工作完成。與您一起工作的客戶知道您,但他們不知道您(可能是臨時)員工的姓名或位置。

您可能懷疑,您當然可以將多線程服務器與服務器/子服務器模型混合使用。主要技巧是確定“問題”的哪些部分最適合通過網絡分佈(通常那些不會過多地消耗網絡帶寬的部分)以及哪些部分最適合分佈在 SMP架構(通常是那些想要使用公共數據區域的部分)。

那麼我們爲什麼要使用一個呢?使用服務器/子服務器方法,我們可以在網絡上的多臺計算機上分配工作。這實際上意味着我們僅受網絡上可用機器數量的限制(當然還有網絡帶寬)。將其與通過網絡分佈的一堆SMP盒上的多個線程相結合,產生“計算集羣”,其中中央“仲裁器”委託(通過服務器/子服務器模型)工作到網絡上的SMP盒。

Some examples

現在我們將考慮每種方法的幾個例子。

Send-driven (client/server)

文件系統,串行端口,控制檯和聲卡都使用客戶端/服務器模型。C語言應用程序承擔客戶端的角色並向這些服務器發送請求。服務器執行指定的任何工作,並回復答案。

其中在一些傳統的“客戶端/服務器”的服務器中可能實際是reply-driven(服務器/子服務器)服務器!這是因爲,對於最終客戶端而言,其服務器是爲標準服務器,即使服務器本身使用服務器/子服務器方法來完成工作。我的意思是,客戶端仍然向它認爲是“服務提供過程”發送消息。實際發生的是“服務提供過程”簡單地將客戶端的工作委託給不同的進程(子服務器)。

Reply-driven (server/subserver)

一種比較流行的reply-driven程序是分佈在網絡上的分形圖形程序。主程序將屏幕劃分爲若干區域,例如64個區域。在啓動時,主程序將獲得可參與此活動的節點列表。主程序啓動子程序(子服務器),每個節點上有一個子程序,然後等待子程序發送給主程序。

然後,主程序重複選擇“未填充”區域(屏幕上的64個),並通過回覆將分形計算工作委託給另一個節點上的子程序。當子程序完成計算後,它會將結果發送回主程序服務器,主程序服務器會在屏幕上顯示結果。

因爲子程序發送給主程序,現在由主程序再次回覆更多的工作。主程序繼續這樣做,直到屏幕上的所有64個區域都已填滿。

An important subtlety

因爲主程序將工作委託給子程序,所以主程序不能在任何一個程序上被阻止打斷! 在傳統的send-driven方法模型中,您希望主服務器創建一個子程序然後發送給它。不幸的是,主程序不會應答,直到工作計劃已完成,這意味着主程序不能同時發送給另一個字程序,在一定程度上有,否定了具有多個工作節點的優勢。

此問題的解決方案是讓子工作程序啓動,並通過向主程序發送消息詢問是否有任何工作要做。我們再一次使用圖中箭頭的方向來指示發送的方向。現在工人程序正在等待master回覆。當某些東西告訴master做一些工作時,它會回覆一個或多個工作人員,這會導致他們離開並完成工作。這讓工人可以開展業務; 主程序仍然可以響應新的請求(它不會被阻止等待其中一個工作人員的回覆)。

Multithreaded server

從客戶端的角度來看,多線程服務器與單線程服務器無法區分。事實上,服務器的設計者可以通過啓動另一個線程來“打開”多線程。無論如何,服務器仍然可以在SMP配置中使用多個CPU,即使它只爲一個“客戶端”提供服務。這意味着什麼?讓我們重新審視分形圖形示例。當一個子服務器從服務器獲得“計算”的請求時,絕對沒有什麼能阻止子服務器在多個CPU上啓動多個線程來爲一個請求提供服務。事實上,爲了使應用程序在具有一些SMP盒和一些單CPU盒的網絡中更好地擴展,服務器和子服務器最初可以交換消息,從而子服務器告訴服務器它有多少CPU - 這讓它知道多少要求它可以同時服務。然後,服務器將排隊更多的SMP盒請求,允許SMP盒比單CPU盒做更多的工作。

Using message passing

現在我們已經看到了消息傳遞中涉及的基本概念,並瞭解到即使像C庫這樣的常見日常事物也使用它,讓我們來看看其中的一些細節。

Architecture & structure

我們一直在談論“客戶”和“服務器”。我還使用了三個關鍵短語:

我特意使用這些短語,因爲它們密切反映了QNX Neutrino Message Passing操作中使用的實際函數名稱。

以下是QNX Neutrino下可用消息傳遞的完整功能列表(按字母順序排列):

ChannelCreate(), ChannelDestroy()
ConnectAttach(), ConnectDetach()
MsgDeliverEvent()
MsgError()
MsgRead(), MsgReadv()
MsgReceive(), MsgReceivePulse(), MsgReceivev()
MsgReply(), MsgReplyv()
MsgSend(), MsgSendnc(), MsgSendsv(), MsgSendsvnc(), MsgSendv(),

MsgSendvnc(), MsgSendvs(),
MsgSendvsnc()
MsgWrite(), MsgWritev()

不要讓這個名單壓倒你!您可以使用列表中的一小部分調用來編寫非常有用的客戶端/服務器應用程序-當您習慣這些想法時,您會發現某些其他函數在某些情況下非常有用。

The client

客戶端想要向服務器發送請求,處於阻止狀態直到服務器完成請求,然後當請求完成並且客戶端被解除阻塞時,才能獲得“回答”。

這意味着兩件事:客戶端需要能夠建立與服務器的連接,然後通過消息傳輸數據 - 從客戶端到服務器的消息(“發送”消息)和從服務器返回到服務器的消息 客戶端(“回覆”消息,服務器的回覆)。

Establishing a connection

那麼,讓我們依次看看這些功能。我們需要做的第一件事是建立連接。我們使用ConnectAttach()函數執行此操作,如下所示:

ConnectAttach()有三個標識符:nd,即節點描述符,pid,即進程ID,以及chid,即通道ID。這三個ID(通常稱爲“ND / PID / CHID”)唯一標識客戶端要連接的服務器。我們將忽略索引和標誌(只需將它們設置爲0)。

因此,我們假設我們要連接到節點上的進程ID 77,通道ID 1。這是執行此操作的代碼示例:

如您所見,通過指定nd爲零,我們告訴內核我們希望在節點上建立連接。

Note:我怎麼知道我想和進程ID 77和通道ID 1對話?我們很快就會看到(參見下面的“Finding the server's ND/PID/CHID”)。

此時,我有一個連接ID,一個小整數,用於唯一標識從客戶端到特定服務器上特定通道的連接。

我可以根據需要多次發送到服務器時使用此連接ID。 當我完成它之後,我可以通過以下方式銷燬它:

Sending messages

使用MsgSend *()函數系列的某些變體實現在客戶端上傳遞的消息。我們將看看最簡單的成員MsgSend():

Let's send a simple message to process ID 77, channel ID 1:

#include <sys/neutrino.h>
char *smsg = "This is the outgoing buffer";
char rmsg [200];
int coid;


// establish a connection
coid = ConnectAttach (0, 77, 1, 0, 0);
if (coid == -1) {
fprintf (stderr, "Couldn't ConnectAttach to 0/77/1!\n");
perror (NULL);
exit (EXIT_FAILURE);
}


// send the message
if (MsgSend (coid, smsg, strlen (smsg) + 1, rmsg, sizeof (rmsg)) == -1)

{
fprintf (stderr, "Error during MsgSend\n");
perror (NULL);
exit (EXIT_FAILURE);
}

if (strlen (rmsg) > 0) {
printf ("Process ID 77 returns \"%s\"\n", rmsg);
}

假設進程ID 77是一個活動服務器,期望它的通道ID 1上的特定格式的消息。在服務器收到消息之後,它將處理它並在某些時候回覆結果。此時,MsgSend()將返回0表示一切順利。如果服務器在回覆中向我們發送任何數據,我們將使用最後一行代碼打印它(我們假設我們正在獲取NUL終止的ASCII數據)。

The server

現在我們已經看到了客戶端,讓我們來看看服務器。客戶端使用ConnectAttach()創建與服務器的連接,然後使用MsgSend()進行所有消息傳遞。

Creating the channel

這意味着服務器必須創建一個通道 - 這是客戶端在發出ConnectAttach()函數調用時連接的東西。創建通道後,服務器通常會永久保留它。通過ChannelCreate()函數創建通道,並通過ChannelDestroy()函數銷燬:

我們稍後會回到flags參數(在下面的“Channel flags”部分)。 現在,我們只使用0.因此,要創建一個通道,服務器會發出:

所以我們有一個通道。此時,客戶端可以(通過ConnectAttach())連接到此通道並開始發送消息:

Message handling

就消息傳遞方面而言,服務器處理兩個階段的消息傳遞;“接收”階段和“回覆”階段:

我們將首先看看這些函數的兩個簡單版本,MsgReceive()和MsgReply(),然後再看一些變體。

從圖中可以看出,我們需要討論四件事:

  1. 客戶端發出MsgSend()並指定其發送緩衝區(smsg指針和sbytes長度)。這將被轉移到服務器的MsgReceive()函數提供的緩衝區中,rmsg的長度爲rbytes。客戶端現已被阻止。

  2. 服務器的MsgReceive()函數解除阻塞,並返回rcvid,服務器稍後將使用該rcvid進行回覆。此時,數據可供服務器使用。

  3. 服務器已完成消息的處理,現在使用從MsgReceive()獲取的rcvid將其傳遞給MsgReply()。請注意,MsgReply()函數將具有定義大小(sbytes)的緩衝區(smsg)作爲要傳輸到客戶端的數據的位置。數據現在由內核傳輸。

  4. 最後,sts參數由內核傳輸,並顯示爲客戶端MsgSend()的返回值。客戶端現在解鎖。

您可能已經注意到每個緩衝區傳輸有兩種大小(在客戶端發送的情況下,客戶端有sbytes,服務器端有rbytes;服務器回覆情況下,服務器端有sbytes,客戶端有rbytes。)存在兩組大小,以便每個組件的程序員可以指定其緩衝區的大小。這樣做是爲了增加安全性。

在我們的示例中,MsgSend()緩衝區的大小與消息字符串的長度相同。讓我們看看服務器,看看那裏的大小是如何使用的。

Server framework
Here's the overall structure of a server:

#include <sys/neutrino.h>
...
void server (void)
{
int rcvid; // indicates who we should reply to
int chid; // the channel ID
char message [512]; // big enough for our purposes

 

// create a channel
chid = ChannelCreate (0);
// this is typical of a server: it runs forever
while (1) {
// get the message, and print it
rcvid = MsgReceive (chid, message, sizeof (message), NULL);
printf ("Got a message, rcvid is %X\n", rcvid);

printf ("Message was \"%s\".\n", message);

// now, prepare the reply. We reuse "message"

strcpy (message, "This is the reply");

MsgReply (rcvid, EOK, message, sizeof (message));
}
}

如您所見,MsgReceive()告訴內核它可以處理大小爲sizeof (message)或512字節)的消息。我們的示例客戶端(上面)只發送了28個字節(字符串的長度)。下圖說明了:

內核傳輸兩種大小指定的最小值。在我們的例子中,內核將傳輸28個字節。服務器將被解鎖並顯示客戶端的消息。剩餘的484個字節(512字節緩衝區)將保持不受影響。

我們再次使用MsgReply()遇到相同的情況。MsgReply()函數表示要傳輸512個字節,但我們客戶端的MsgSend()函數已指定最多可傳輸200個字節。所以內核再次傳遞最小值。在這種情況下,客戶端可以接受的200個字節限制了傳輸大小。(這裏有一個有趣的方面是,一旦服務器傳輸數據,如果客戶端沒有收到所有數據,就像在我們的例子中那樣,沒有辦法讓數據恢復 - 它就永遠消失了。)

請記住,這種“修剪”操作是正常的和預期的行爲。

當我們討論通過網絡傳遞的消息時,您會發現傳輸的數據量存在微小的“問題”。 我們將在下面的“Networked message-passing differences”中看到這一點。

The send-hierarchy

在消息傳遞環境中可能不明顯的一件事是需要遵循嚴格的發送層次結構。這意味着兩個線程永遠不應該相互發送消息; 相反,它們應該被組織起來,使每個線程佔據一個層次; 所有發送從一個級別到更高級別,從不到相同或更低級別。讓兩個線程相互發送消息的問題是,最終你會遇到死鎖問題; 兩個線程都在等待彼此回覆它們各自的消息。由於線程被阻塞,它們永遠不會有機會運行並執行回覆,因此最終會有兩個(或更多!)掛起的線程。

爲線程分配級別的方法是將最外層的客戶端放在最高級別,然後從那裏開始工作。例如,如果你有一個依賴於某個數據庫服務器的圖形用戶界面,而數據庫服務器又依賴於文件系統,而文件系統又依賴於塊文件系統驅動程序,那麼你就擁有了不同的自然層次結構流程。發送將從最外面的客戶端(圖形用戶界面)向下流到下層服務器; 回覆將以相反的方向流動。

雖然這在大多數情況下肯定有效,但您會遇到需要“中斷”發送層次結構的情況。這絕不是通過簡單地違反發送層次結構併發送“反對流”的消息來完成的,而是通過使用MsgDeliverEvent()函數完成的,稍後我們將對此進行介紹。

Receive IDs, channels, and other parameters

我們還沒有談到上面例子中的各種參數,所以我們可以只關注消息傳遞。現在讓我們來看看。

More about channels

在上面的服務器示例中,我們看到服務器只創建了一個通道。它當然可以創建更多,但通常,服務器不會這樣做。(具有兩個通道的服務器最明顯的例子是透明分佈式處理(TDP,也稱爲Qnet)本機網絡管理器 - 絕對是一個奇怪的軟件!)

事實證明,實際上並不需要在現實世界中創建多個渠道。通道的主要目的是爲服務器提供一個明確定義的位置來“監聽”消息,併爲客戶端提供一個明確定義的位置來發送消息(通過連接方式)。關於您在服務器中擁有多個頻道的唯一情況是服務器是否要提供不同的服務或不同的服務類別,具體取決於消息到達的頻道。例如,第二個信道可用作丟棄喚醒脈衝的地方 - 這確保它們被視爲與到達第一信道的消息不同的“服務等級”。

在上一段中,我曾說過你可以在服務器中運行一個線程池,準備接受來自客戶端的消息,並且哪個線程獲得請求並不重要。這是頻道抽象的另一個方面。在以前版本的QNX系列操作系統(特別是QNX 4)中,客戶端將在由節點ID和進程ID標識的服務器上定位消息。由於QNX 4是單線程的,這意味着不會混淆有關“向誰發送”消息。但是,一旦你引入了線程,就必須決定如何處理線程(實際上,“服務提供者”)。由於線程是短暫的,因此讓客戶端連接到特定的節點ID,進程ID和線程ID實際上沒有意義。另外,如果那個特定的線程很忙呢?我們必須提供一些方法來允許客戶端在定義的服務提供線程池中選擇“非忙線程”。

嗯,這正是一個通道。它是“服務線程池”的“address”。這裏的含義是,一堆線程可以在特定通道上發出MsgReceive()函數調用,並且阻塞,一次只有一個線程獲取消息。

Who sent the message?

通常,服務器需要知道是誰向其發送了消息。有許多的原因:

• accounting
• access control
• context association

• class of service
• compatibility
• etc.

讓客戶端向發送的每條消息提供此信息將是繁瑣的(並且存在安全漏洞)。因此,每當MsgReceive()函數解鎖時,內核都會填充一個結構,因爲它有一條消息。此結構的類型爲struct _msg_info,包含以下內容:

struct _msg_info
{
uint32_t
nd;
uint32_t
srcnd;
pid_t
pid;
int32_t
tid;
int32_t
chid;
int32_t
scoid;
int32_t
coid;
int16_t
priority;
int16_t
flags;
size64_t
msglen;
size64_t
srcmsglen;
size64_t
dstmsglen;
};

您將它作爲最後一個參數傳遞給MsgReceive()函數。如果傳遞NULL,則沒有任何反應。(稍後可以通過MsgInfo()調用檢索信息,因此它不會永遠消失!)

我們來看看這些領域:

nd, srcnd, pid, and tid

節點描述符,進程ID和客戶端的線程ID。(注意,nd是發送節點的接收節點的節點描述符;srcnd是接收節點的發送節點的節點描述符。這有一個很好的理由,我們將在下面的 Some notes on NDs中看到。)

Priority

發送線程的優先級。
chidcoid
該消息被髮送到信道ID,和所使用的連接ID。

Scoid

服務器連接ID。這是內核用於將消息從服務器路由回客戶端的內部 標識符。你不需要知道它,除了有趣的事實,它將是一個唯一代表客戶端的小整數。

Flags

包含各種標誌位,包括以下內容:
• _NTO_MI_BITS_64和_NTO_MI_BITS_DIFF告訴您發件人使用的是64位體系結構,或者使用的字體大小不同於您使用的字體。
• _NTO_MI_ENDIAN_BIG和_NTO_MI_ENDIAN_DIFF告訴您發送計算機的字節順序(如果消息來自具有不同字節序的計算機的網絡)。
•內部使用_NTO_MI_NET_CRED_DIRTY。
如果您確定您的程序與發件人不兼容,則可以返回錯誤,例如ENOTSUP。 請參閱本章後面的“回覆沒有數據或錯誤”。

Msglen

收到的字節數

Srcmsglen

客戶端發送的源消息的長度(以字節爲單位)。 這可能大於msglen中的值,就像接收的數據少於發送的數據一樣。請注意,僅當在ChannelCreate()的flags參數中爲接收到消息的通道設置了_NTO_CHF_SENDER_LEN時,此成員纔有效。

Dstmsglen

客戶端回覆緩衝區的長度,以字節爲單位。只有在ChannelCreate()的參數中爲接收到消息的通道設置_NTO_CHF_REPLY_LEN標誌時,此字段纔有效。

The receive ID (a.k.a. the client cookie)

這是一個關鍵的代碼片段,因爲它說明了從客戶端接收消息之間的綁定,然後能夠(稍後)回覆該特定客戶端。receive ID是一個整數,充當“magic cookie”,如果您想稍後與客戶端進行交互,則需要保留它。如果丟了會怎麼樣?它消失了。客戶端不會從MsgSend()中解除阻塞,直到您(服務器)死亡,或者客戶端在消息傳遞調用上有超時(即便如此,它也很棘手; 請參閱QNX Neutrino C庫參考中的TimerTimeout()函數,以及關於它在“內核超時”下的“Clocks, Timers, and Getting A Kick Every So Often”章節中使用的討論)。

注意:不要依賴於接收ID的值來具有任何特定含義 - 它可能會在未來版本的操作系統中發生變化。您可以假設它將是唯一的,因爲您將永遠不會有兩個由相同接收ID標識的未完成客戶端(在這種情況下,當您執行MsgReply()時內核無法區分它們)。

另請注意,除了一個特殊情況(稍後我們將看到的MsgDeliverEvent()函數)之外,一旦完成了MsgReply(),該特定的接收ID就不再具有意義

Replying to the client

MsgReply()接受接收ID,狀態,消息指針和消息大小。我們剛剛討論了接收ID; 它標識應該將回復消息發送給誰。status變量指示應該傳遞給客戶端的MsgSend()函數的返回狀態。最後,消息指針和大小指示應該發送的可選回覆消息的位置和大小。

MsgReply()函數可能看起來非常簡單(並且它是),但其應用程序需要進行一些檢查。

Not replying to the client

在通過MsgReceive()接受來自其他客戶端的新消息之前,絕對沒有強制要求您回覆客戶端!這可以在許多不同的場景中使用。

在典型的設備驅動程序中,客戶端可能會提出長時間不會被服務的請求。例如,客戶端可能會要求模數轉換器(ADC)設備驅動程序“出去並收集45秒的樣本。”與此同時,ADC驅動程序不應該只關閉45秒,而不去操作別的事情!其他客戶端可能希望提供服務請求(例如,可能存在多個模擬通道,或者可能存在應立即可用的狀態信息等)。

在架構上,ADC驅動程序將簡單地對從MsgReceive()獲取的接收ID進行排隊,啓動45秒的累積過程,然後關閉並處理其他請求。當45秒結束並且樣本已累積時,ADC驅動程序可以找到與請求關聯的接收ID,然後回覆客戶端。

在reply-driven的服務器/子服務器模型(其中一些“客戶端”是子服務器)的情況下,您還希望阻止對客戶端的回覆。由於子服務器正在尋找工作,您只需記下他們的接收ID並將其存儲起來。當實際工作到達時,那時你纔會回覆子服務器,從而表明它應該做一些工作。

Replying with no data, or an errno

當您最終回覆客戶端時,不要求您傳輸任何數據。這用於兩種情況。

如果回覆的唯一目的是取消阻止客戶端,您可以選擇不回覆數據。假設客戶端只是希望在某個特定事件發生之前被阻止,但它不需要知道哪個事件。在這種情況下,MsgReply()函數不需要任何數據; 接收ID足夠:

這將取消阻止客戶端(但不返回任何數據)並返回EOK“成功”指示。

稍作修改,您可能希望將錯誤狀態返回給客戶端。在這種情況下,你不能使用MsgReply(),但必須使用MsgError():

在上面的示例中,服務器檢測到客戶端正在嘗試寫入只讀文件系統,而不是返回任何實際數據,只是將EROFS的錯誤返回給客戶端。

或者(我們將很快查看調用),您可能已經傳輸了數據(通過MsgWrite()),並且沒有額外的數據要傳輸。

爲什麼這兩個calls方式?它們略有不同。雖然MsgError()和MsgReply()都將取消阻止客戶端,但MsgError()不會傳輸任何其他數據,將導致客戶端的MsgSend()函數返回-1,並將導致客戶端將errno設置爲傳遞的任何內容作爲MsgError()的第二個參數。

另一方面,MsgReply()可以傳輸數據(由第三個和第四個參數指示),並將導致客戶端的MsgSend()函數返回作爲第二個參數傳遞給MsgReply()的任何內容。MsgReply()對客戶端的errno沒有影響。

通常,如果您只返回通過/失敗指示(並且沒有數據),則使用MsgError(),而如果要返回數據,則使用MsgReply()。傳統上,當您返回數據時,MsgReply()的第二個參數將是一個正整數,表示返回的字節數。

Finding the server's ND/PID/CHID

您已經注意到在ConnectAttach()函數中,我們需要節點描述符(ND),進程ID(PID)和通道ID(CHID)才能連接到服務器。到目前爲止,我們還沒有談到客戶端如何找到這個ND / PID / CHID信息。

如果一個進程創建另一個進程,則很容易 - 進程創建調用返回新創建進程的進程ID。創建進程可以將命令行上自己的PID和CHID傳遞給新創建的進程,或者新創建的進程可以發出getppid()函數調用以獲取其父進程的PID並假設“衆所周知的”CHID。

如果我們有兩個完全陌生的人怎麼辦?例如,如果第三方創建了服務器並且您編寫的應用程序想要與該服務器通信,則會出現這種情況。真正的問題是,“服務器如何宣傳其位置?”

有很多方法可以做到這一點; 我們將看看其中的四個,按照編程“優雅”的順序遞增:

  1. 打開一個衆所周知的文件名並將ND / PID / CHID存儲在那裏。這是UNIX風格的服務器採用的傳統方法,它們打開一個文件(例如/etc/httpd.pid),將其進程ID作爲ASCII字符串寫入,並期望客戶端打開文件並獲取進程ID。

  2. 使用全局變量來通告ND/PID/CHID信息。這通常用於需要向自己發送消息的多線程服務器,並且就其性質而言是非常有限的情況。

  3. 使用名稱 - 位置函數(name_attach()和name_detach(),然後使用客戶端的name_open()和name_close()函數。

  4. 接管路徑名空間的一部分併成爲資源管理器。當我們在資源管理器章節中查看資源管理器時,我們將討論這個問題。

第一種方法非常簡單,但可能會遇到“路徑名污染”,其中/ etc目錄中包含各種*.pid文件。由於文件是持久的(這意味着它們在創建過程終止並且機器重新啓動後仍然存在),因此沒有明顯的方法來清理這些文件,除了可能有一個“嚴峻的收割機”任務,看看這些東西是否仍然有效。

還有另一個相關的問題。由於創建文件的進程可能會在不刪除文件的情況下死亡,因此在嘗試向其發送消息之前無法知道進程是否仍處於活動狀態。更糟糕的是,文件中指定的ND/PID/CHID可能非常陳舊,以至於它會被其他程序重用!您發送給該程序的消息最多會被拒絕,最壞的情況可能會造成損害。所以這種方法已經出來了。

第二種方法,我們使用全局變量來通告ND / PID / CHID值,這不是一般解決方案,因爲它依賴於客戶端能夠訪問全局變量。由於這需要共享內存,因此它無法在網絡中運行!這通常用於微小的測試用例程序或非常特殊的情況,但總是在多線程程序的上下文中使用。實際上,所有發生的事情是程序中的一個線程是客戶端,另一個線程是服務器。 服務器線程創建通道,然後將通道ID放入一個全局變量(進程中所有線程的節點ID和進程ID相同,因此不需要通告它們。)然後客戶端線程接收 全局通道ID並對其執行ConnectAttach()。

第三種方法,我們使用name_attach()name_detach()函數,其適用於簡單的客戶/服務器情況。

在最後一種方法中,服務器成爲資源管理器絕對是最乾淨的,也是推薦的通用解決方案。“How”的機制將在資源管理器章節中變得清晰,但是現在,您需要知道的是服務器將特定路徑名註冊爲其“權限域”,並且客戶端執行簡單的open()那個路徑名。

我不能強調這一點:POSIX文件描述符使用連接ID實現;也就是說,文件描述符是一個連接ID!這種方案的優點在於,由於從open()返回的文件描述符是連接ID,因此客戶端無需進一步工作即可使用該特定連接。例如,當客戶端稍後調用read()並將文件描述符傳遞給它時,這會將很少的開銷轉換爲MsgSend()函數。

What about priorities?

如果低優先級進程和高優先級進程同時向服務器發送消息,該怎麼辦?

消息始終按優先級順序傳遞。

如果兩個進程“同時”發送消息,則優先級較高的進程的整個消息首先被傳遞給服務器。
如果兩個進程具有相同的優先級,那麼消息將按時間順序傳遞(因爲在單處理器機器上沒有絕對同時發生的事情 - 即使在SMP盒上也會有一些順序,因爲CPU會仲裁內核訪問,在他們中間)。

當我們在本章後面討論優先級倒置時,我們將回到這個問題引入的其他一些細微之處。

Reading and writing data

到目前爲止,您已經看到了基本的消息傳遞原語。正如我之前提到的,這些都是你需要的。但是,有一些額外的功能可以讓生活更輕鬆。

讓我們考慮使用客戶端和服務器的示例,我們可能需要其他功能。客戶端發出MsgSend()以將一些數據傳輸到服務器。在客戶端發出MsgSend()後它會阻塞;它正在等待服務器回覆。

一個有趣的事情發生在服務器端。服務器已調用MsgReceive()以從客戶端接收消息。根據您爲消息選擇的設計,服務器可能會或可能不知道客戶端消息的大小。爲什麼服務器不知道消息有多大?考慮我們一直在使用的文件系統示例。假設客戶端:

如果服務器執行MsgReceive()並指定緩衝區大小(例如1024字節),則按預期工作。由於我們的客戶端只發送了一條小信息(28字節),因此我們沒有任何問題。

但是,如果客戶端發送大於1024字節的內容,例如1兆字節,該怎麼辦?

服務器如何優雅地處理這個問題?我們可以任意地說,不允許客戶端寫入超過n個字節。然後,在write()的客戶端C庫代碼中,我們可以查看此要求並將寫入請求分成幾個n字節的請求。這就很尷尬了。

這個例子的另一個問題是,“應該有多大?”

您可以看到這種方法有很多缺點:

  • 所有使用有限大小的消息傳輸的函數都必須在C庫中進行修改,以便該函數對請求進行打包。這本身就是相當多的工作。此外,它可能會對多線程函數產生意外的副作用-如果來自一個線程的消息的第一部分被髮送,然後客戶端中的另一個線程搶佔當前線程併發送其自己的消息。原來的線程離開了哪裏?

  • 現在必須準備好所有服務器以處理可能到達的最大可能消息大小。這意味着所有服務器都必須具有較大的數據區域,否則庫將不得不將大量請求分解爲許多較小的請求,從而影響速度。

幸運的是,這個問題有一個相當簡單的解決方法,也給我們帶來了一些好處。

兩個函數MsgRead()和MsgWrite()在這裏特別有用。要記住的重要事實是客戶端是BLOCK狀態。這意味着客戶端不會在服務器嘗試檢查數據結構時更改數據結構。

在多線程客戶端中,另一個線程可能會混淆在服務器上阻塞的客戶端線程的數據區域。這被認爲是一個錯誤(糟糕的設計) - 服務器線程假定它具有對客戶端數據區的獨佔訪問權,直到服務器線程解除對客戶端的阻塞。

MsgRead()允許您的服務器從被阻止的客戶端的地址空間讀取數據,從客戶端指定的“發送”緩衝區的開頭開始偏移字節,進入msg爲nbytes指定的緩衝區。服務器不會阻止,客戶端也不會解除阻止。MsgRead()返回實際讀取的字節數,如果有錯誤則返回 -1。

那麼讓我們考慮一下我們如何在write()示例中使用它。C Library write()函數構造一個消息,該消息帶有一個頭,它發送到文件系統服務器fs-qnx6。服務器通過MsgReceive()接收消息的一小部分,查看它,並決定將消息的其餘部分放在何處。 fs-qnx6 服務器可能會決定將數據放入已分配的緩存緩衝區的最佳位置。

因此,客戶端決定向文件系統發送4 KB。(注意:C庫如何在數據前面插入一個小標題,以便文件系統可以告訴它實際上是什麼類型的請求 - 當我們查看多部分消息時,我們會回到這一點,更詳細的 我們看看資源管理器。)

文件系統只讀取足夠的數據(標題)來確定它是什麼類型的消息:

// part of the headers, fictionalized for example purposes
struct _io_write {
uint16_t type;
uint16_t combine_len;
uint32_t nbytes;
uint32_t xtype;
};
typedef union {
uint16_t type;

struct _io_read io_read;
struct _io_write io_write;
...
} header_t;


header_t header; // declare the header
rcvid = MsgReceive (chid, &header, sizeof (header), NULL);
switch (header.type) {
...
case _IO_WRITE:
number_of_bytes = header.io_write.nbytes;
...

此時,fs-qnx6知道4 KB位於客戶端的地址空間中(因爲消息在結構的nbytes成員中告訴它)並且應該將其傳輸到緩存緩衝區。 fs-qnx6 服務器可能會發出:

請注意,消息傳輸已指定sizeof(header.io_write)的偏移量,以便跳過客戶端C庫添加的寫入標頭。我們假設cache_buffer [index] .size實際上是4096(或更多)字節。

同樣,爲了將數據寫入客戶端的地址空間,我們有:

MsgWrite()允許您的服務器將數據寫入客戶端的地址空間,從客戶端指定的“接收”緩衝區的開頭開始偏移字節。在服務器空間有限但客戶希望從服務器獲取大量信息的情況下,此功能最有用。

例如,使用數據採集驅動程序,客戶端可以指定4兆字節的數據區域並告訴驅動程序獲取4兆字節的數據。驅動程序真的不需要像這樣的大區域,以防萬一有人要求進行大量的數據傳輸。

驅動程序可能具有128 KB的DMA數據傳輸區域,然後使用MsgWrite()將其逐個消息傳遞到客戶端的地址空間(當然,每次增加128 KB的偏移量)。然後,當寫入最後一條數據時,驅動程序將MsgReply()發送給客戶端。

請注意,MsgWrite()允許您在各個位置編寫數據組件,然後使用MsgReply()喚醒客戶端:

或者在客戶端緩衝區開頭寫入標題後喚醒客戶端:

對於編寫未知數量的數據,這是一個相當優雅的技巧,只有在編寫完成後才能知道您編寫了多少數據。如果您在傳輸數據後使用此方法編寫標題,則必須記住在客戶端數據區的開頭留出標題空間!

Multipart messages

到目前爲止,我們只展示了從客戶端地址空間中的一個緩衝區到服務器地址空間中另一個緩衝區的消息傳輸。(在回覆期間,服務器空間中的一個緩衝區進入客戶端空間中的另一個緩衝區。)

雖然這種方法對於大多數應用程序來說足夠好,但它可能導致效率低下。回想一下,我們的write()C庫代碼佔用了傳遞給它的緩衝區,並在它的前面插入了一個小的頭部信息。使用我們到目前爲止學到的東西,你會期望C庫實現類似這樣的write()(以下不是真正的源代碼):

ssize_t write (int fd, const void *buf, size_t nbytes)
{
char *newbuf;
io_write_t *wptr;
ssize_t nwritten;
newbuf = malloc (nbytes + sizeof (io_write_t));

 

// fill in the write_header at the beginning
wptr = (io_write_t *) newbuf;
wptr -> type = _IO_WRITE;
wptr -> nbytes = nbytes;

 

// store the actual data from the client
memcpy (newbuf + sizeof (io_write_t), buf, nbytes);

// send the message to the server
nwritten = MsgSend (fd, newbuf, nbytes + sizeof (io_write_t),

newbuf, sizeof (io_write_t));
free (newbuf);
return (nwritten);
}

看看發生了什麼? 一些壞事:

  • write()現在必須能夠爲malloc()提供足夠大的緩衝區,以便客戶端數據(可能相當大)和標頭。標頭的大小不是問題 - 在這種情況下,它是12個字節。

  • 我們必須將數據複製兩次:一次通過memcpy(),另一次爲MsgSend。

  • 我們必須建立一個指向io_write_t類型的指針並將其指向緩衝區的開頭,而不是本機訪問它(這是一個小麻煩)。

由於內核無論如何都要複製數據,如果我們可以告訴它數據的一部分(標題)位於某個地址,而另一部分(數據本身)位於某處,那將是很好的。否則,我們無需手動組裝緩衝區和複製數據。

幸運的是,QNX Neutrino實現了一種讓我們做到這一點的機制!該機制稱爲IOV,代表“輸入/輸出向量”。

讓我們先看看一些代碼,然後我們將討論會發生什麼:

#include <sys/neutrino.h>
ssize_t write (int fd, const void *buf, size_t nbytes)
{
io_write_t whdr;
iov_t iov [2];
// set up the IOV to point to both parts:
SETIOV (iov + 0, &whdr, sizeof (whdr));
SETIOV (iov + 1, buf, nbytes);
// fill in the io_write_t at the beginning
whdr.type = _IO_WRITE;
whdr.nbytes = nbytes;
// send the message to the server
return (MsgSendv (coid, iov, 2, iov, 1));
}

首先,注意沒有malloc()和memcpy()。接下來,請注意iov_t類型的使用。這是一個包含地址和長度對的結構,我們已經分配了兩個(名爲iov)。

iov_t類型定義由<sys / neutrino.h>自動包含,定義爲:

給定這種結構,我們用寫頭(第一部分)和來自客戶端的數據(第二部分)填充地址和長度對。有一個名爲SETIOV()的便利宏可以爲我們完成任務。它被正式定義爲:

SETIOV()接受iov_t,並將地址和長度數據填充到IOV中。

另請注意,由於我們創建的IOV指向標頭,因此我們可以在不使用malloc()的情況下在堆棧上分配標頭。這可能是一個祝福和一個詛咒 - 當標題非常小時,這是一個祝福,因爲你避免了動態內存分配的麻煩,但是當標題很大時它可能是一個詛咒,因爲它可以消耗一大堆的堆棧空間。通常,頭部非常小。

無論如何,重要的工作是由MsgSendv()完成的,它採用與我們在前一個例子中使用的MsgSend()函數幾乎相同的參數:

參數含義如下:

Coid

我們發送的連接ID,與MsgSend()一樣。

sparts and rparts

iov_t參數指定的發送和接收部分的數量。在我們的示例中,我們將sparts設置爲2表示我們正在發送一個由2部分組成的消息,並且rpart爲1表示我們正在收到一個部分的回覆。

siov and riov

iov_t數組指示我們希望發送的地址和長度對。在上面的例子中,我們設置2部分siov指向標題和客戶端數據,1部分riov指向標題。

這是內核查看數據的方式:

內核只是將數據從客戶端空間中的IOV的每個部分無縫複製到服務器的空間(然後返回,用於回覆)。實際上,內核正在執行聚集 - 分散操作。

要記住以下幾點:

  • 部件數量“限制”爲512 KB; 然而,我們的2的例子是典型的。

  • 內核只是將一個IOV中指定的數據從一個地址空間複製到另一個地址空間。

  • 源和目標IOV不必相同。

爲什麼最後一點如此重要?要回答這個問題,讓我們來看看大局。在客戶端,假設我們發佈了:

在服務器端,(假設它是文件系統,fs-qnx6),我們有4 KB的緩存塊,我們希望有效地將消息直接接收到緩存塊中。 理想情況下,我們想寫一些這樣的代碼:

這段代碼完全符合您的期望:它設置了一個4部分的IOV結構,將結構的第一部分設置爲指向標題,接下來的三部分指向緩存塊37,16和22 。(這些數字代表恰好在特定時間可用的緩存塊。)

這是一個圖形表示:

然後調用MsgReceivev()函數,指示我們將從指定的通道(chid參數)接收消息,並且我們將提供4部分IOV結構。 這也顯示了IOV結構本身。

(除了它的IOV功能,MsgReceivev()就像MsgReceive()一樣運行。)

哎呀!當我們引入MsgReceive()函數時,我們犯了同樣的錯誤。在我們實際收到消息之前,我們如何知道我們收到的消息類型以及與之相關的數據量?

我們可以像以前一樣解決這個問題:

這是初始的MsgReceive()(請注意,我們沒有使用IOV表單 - 實際上沒有必要使用單部分消息),找出它是什麼類型的消息,然後繼續閱讀數據從客戶端的地址空間(從偏移sizeof (header.io_write)處開始)進入由3部分IOV指定的緩存緩衝區。

請注意,我們從使用4部分IOV(在第一個示例中)切換到3部分IOV。那是因爲在第一個例子中,4部分IOV的第一部分是標題,我們使用MsgReceive()直接讀取,而4部分IOV的最後三部分與3部分IOV相同他們指定了我們希望數據的去向。

您可以想象我們如何執行讀取請求的回覆:

1.找到與請求的數據對應的緩存條目。

2.用這些條目填充IOV結構。

3.使用MsgWritev()(或MsgReplyv())將數據傳輸到客戶端。

請注意,如果數據不是在高速緩存塊(或其他數據結構)的開頭就開始,則這不是問題。只需將第一個IOV偏移到指向數據開始的位置,然後修改大小。

What about the other versions?

除MsgSend *()系列之外的所有消息傳遞函數都具有相同的一般形式:如果函數的末尾有v,則需要IOV和部分數; 否則,它需要一個指針和一個長度。

MsgSend *()系列在消息緩衝區的源和目標方面有四個主要變化,並結合內核調用本身的兩種變體。

看如下列表:

“線性”是指傳遞void *類型的單個緩衝區及其長度。記住這個的簡單方法是“v”代表“向量”,並且與適當的參數 - 第一個或第二個在同一個地方,分別指“發送”或“接收”。

嗯......看起來像MsgSendsv()和MsgSendsvnc()函數是相同的,不是嗎?嗯,是的,就他們的參數而言,確實如此。不同之處在於它們是否是取消點。“nc”版本不是取消點,而非“nc”版本是。(有關取消點和可取消性的更多信息,請參閱pthread_cancel()下的QNX Neutrino C庫參考。)

Implementation

您可能已經懷疑MsgRead(),MsgReceive(),MsgSend()和MsgWrite()函數的所有變體都是密切相關的。(唯一的例外是MsgReceivePulse()-我們很快就會看到這個。)

你應該使用哪些?嗯,這是一個哲學辯論。我個人的偏好是混合搭配。

如果我只發送或接收單部分消息,爲什麼還要爲設置IOV的複雜性而煩惱呢?無論您是自己設置還是讓內核/庫執行它,設置它們的微小CPU開銷基本相同。單部分消息方法使內核不必進行地址空間操作,並且速度稍快一些。

你應該使用IOV功能嗎?絕對! 每當您發現自己處理多部分消息時,請使用它們。 當您只使用幾行代碼使用多部分message傳輸時,切勿複製數據。這可以通過最小化數據在系統中複製的次數來保持system screaming;傳遞指針比將數據複製到新緩衝區要快得多。

Pulses

到目前爲止,我們所討論的所有消息都爲BLOCK狀態的客戶端。一旦調用MsgSend(),它就是客戶端的休息時間。客戶端休眠,直到服務器回覆。

但是,有些情況下消息的發送者無法阻止。我們將看一下Interrupts和“Clocks, Timers, and Getting a Kick Every So Often”的章節中的一些示例,但是現在我們應該理解這個概念。

實現非阻塞發送的機制稱爲脈衝。脈衝是一條微小的信息:

•可以承載40位有效負載(8位代碼和32位數據)
•對發件人沒有阻止
•可以像任何其他消息一樣接收
•如果接收器未被阻止等待,則排隊

Receiving a pulse message

接收脈衝非常簡單:MsgReceive()提供一個很小的,定義良好的消息,就像一個線程發送了一條正常的消息一樣。唯一的區別是你不能MsgReply()這個消息 - 畢竟,脈衝的整個想法是它是異步的。在本節中,我們將看一下另一個函數MsgReceivePulse(),它對處理脈衝非常有用。

關於脈衝的唯一“有趣”的事情是從MsgReceive()函數返回的接收ID爲零。這表明這是一個脈衝,而不是來自客戶的常規消息。

您經常會看到服務器中的代碼如下所示:

#include <sys/neutrino.h>
rcvid = MsgReceive (chid, …);
if (rcvid == 0) { // it's a pulse
// determine the type of pulse
// handle it
} else { // it's a regular message
// determine the type of message
// handle it
}

What's in a pulse?

好的,所以您收到此消息的接收ID爲零。它實際上是什麼樣的?從<sys/neutrino.h>頭文件中,這裏是_pulse結構的定義:

struct _pulse {
uint16_t
type;
uint16_t
subtype;
int8_t
code;

uint8_t zero [3];
union sigval
value;
int32_t
scoid;
};

typesubtype成員都爲零(進一步表明這是一個脈衝)。codevalue成員被設置爲脈衝的發送者確定的任何內容。通常,code將指示脈衝發送的原因;該value將是與脈衝相關的32位數據值。這兩個領域是“40位”內容的來源; 其他字段不是用戶可調整的。

內核保留了code的負值情況,爲程序員留下了127個value,因爲他們認爲合適。

value成員實際上是一個聯合:

因此(擴展上面的服務器示例),您經常會看到如下代碼:

#include <sys/neutrino.h>
rcvid = MsgReceive (chid, …
if (rcvid == 0) { // it's a pulse
// determine the type of pulse
switch (msg.pulse.code) {
case MY_PULSE_TIMER:
// One of your timers went off, do something
// about it...
break;
case MY_PULSE_HWINT:
// A hardware interrupt service routine sent
// you a pulse. There's a value in the "value"
// member that you need to examine:
val = msg.pulse.value.sival_int;
// Do something about it...
break;
case _PULSE_CODE_UNBLOCK:
// A pulse from the kernel, indicating a client

// unblock was received, do something about it...
break;
// etc...
} else { // it's a regular message
// determine the type of message
// handle it
}

當然,這段代碼假設你已經設置了你的msg結構來包含struct _pulse脈衝; 成員,並且定義了清單常量MY_PULSE_TIMER和MY_PULSE_HWINT。脈衝代碼_PULSE_CODE_UNBLOCK是上述負編號內核脈衝之一。您可以在<sys/neutrino.h>中找到它們的完整列表以及值字段的簡要說明。

 

The MsgReceivePulse() function

MsgReceive()和MsgReceivev()函數將接收“常規”消息或脈衝。可能存在您只想接收脈衝的情況。最好的例子是在服務器中,您收到客戶端要求執行某項操作的請求,但尚未完成請求(可能您需要進行長時間的硬件操作)。在這樣的設計中,您通常會設置硬件(或計時器或其他),以便在發生重大事件時向您發送脈衝。

如果您使用經典的“等待無限循環消息”設計來編寫服務器,您可能遇到一個客戶端向您發送請求的情況,然後,當您等待脈衝進入(表示請求完成)時,另一個客戶端會向您發送另一個請求。通常,這正是您想要的 - 畢竟,您希望能夠同時爲多個客戶端提供服務。但是,可能有充分的理由說明爲什麼這是不可接受的 - 爲客戶提供服務可能是如此資源密集,您希望限制客戶端的數量。

在這種情況下,您現在需要能夠“選擇性地”僅接收脈衝,而不是常規消息。這是MsgReceivePulse()發揮作用的地方:

如您所見,您使用與MsgReceive()相同的參數;通道ID,緩衝區(及其大小)以及info參數。(我們在“Who sent the message?”中討論了上面的info參數。)注意,在脈衝的情況下不使用info參數;你可能會問爲什麼它出現在參數列表中。簡單的回答:在實現中以這種方式更容易實現。只需傳遞一個NULL!

MsgReceivePulse()函數只接收脈衝。所以,如果你有一個通過MsgReceivePulse()阻塞了多個線程的通道,(並且沒有通過MsgReceive()阻止它的線程),並且客戶端試圖向你的服務器發送一條消息,那麼客戶端將保持SEND-blocked,直到一個線程發出MsgReceive()調用。在此期間,脈衝將通過MsgReceivePulse()函數傳輸。

如果混合使用MsgReceivePulse()和MsgReceive(),唯一可以保證的是MsgReceivePulse()只能獲得脈衝。MsgReceive()可以獲取脈衝或消息!這是因爲,通常情況下,MsgReceivePulse()函數的使用是爲您要排除向服務器的常規消息傳遞的情況保留的。

這確實引起了一些混亂。由於MsgReceive()函數既可以接收消息又可以接收脈衝,但MsgReceivePulse()函數只能接收脈衝,那麼如何處理使用這兩種函數的服務器?通常,這裏的答案是你有一個執行MsgReceive()的線程池。此線程池(一個或多個線程;數量取決於您準備同時服務的客戶端數量)負責處理客戶端調用(服務請求)。由於您正在嘗試控制“提供服務的線程”的數量,並且由於其中一些線程可能需要阻塞,等待脈衝到達(例如,來自某些硬件或來自其他線程),因此您需要通常使用MsgReceivePulse()阻止提供服務的線程。這可以確保客戶端請求在您等待脈衝時不會“潛入”(因爲MsgReceivePulse()只接收脈衝)。

The MsgDeliverEvent() function

如上文“發送層次結構”中所述,有些情況下您需要打破發送的自然流。

如果您的客戶端向服務器發送了消息,結果可能暫時不可用,並且客戶端不想處於Block,則可能會出現這種情況。當然,你也可以通過線程來部分地解決這個問題,讓客戶端在阻塞服務器調用上簡單地“use up”一個線程,但是這對於較大的系統來說可能無法很好地擴展(你可能會使用很多線程來等待許多不同的服務器)。假設您不想使用線程,而是希望服務器立即回覆客戶端,“我很快就會回覆您的請求。”此時,由於服務器回覆,客戶端現在將會繼續處理。一旦服務器完成了客戶端給出的任何任務,服務器現在需要某種方式告訴客戶端,“嘿,醒來,我已經完成了。”顯然,正如我們在上面的發送層次結構討論中看到的那樣,你可以沒有服務器向客戶端發送消息,因爲如果客戶端在同一時刻向服務器發送消息,這可能會導致死鎖。那麼,服務器如何在不違反發送層次結構的情況下向客戶端“發送”消息?

它實際上是一個多步操作。以下是它的工作原理:

  1. 客戶端創建一個struct sigevent結構,並將其填入。

  2. 客戶端向服務器發送消息,有效地說:“爲我執行此特定任務,立即回覆,順便說一句,這是一個結構設置,您應該在工作完成時通知我。”

  3. 服務器接收消息(包括struct sigevent),將struct sigevent和接收ID存儲起來,並立即回覆給客戶端。

  4. 客戶端現在正在運行,服務器也是如此。

  5. 當服務器完成工作時,服務器使用MsgDeliverEvent()通知客戶端工作現在已完成。

我們將在“How to fill in the struct sigevent.”中詳細介紹Clocks,Timers和Getting a Kick Every Sounds中的struct sigevent。現在,只需將struct sigevent視爲“ 黑盒子“以某種方式包含服務器用來通知客戶端的事件。由於服務器存儲了struct sigevent和來自客戶端的接收ID,因此服務器現在可以調用MsgDeliverEvent()將客戶端選擇的事件傳遞給客戶端:

請注意,MsgDeliverEvent()函數接受兩個參數,即接收ID(在rcvid中)和要在事件中傳遞的事件。服務器不會以任何方式修改或檢查事件!這一點很重要,因爲它允許服務器提供客戶端選擇的任何類型的事件,而無需在服務器上進行任何特定處理。 (但是,服務器可以使用MsgVerifyEvent()函數驗證事件是否有效。)

rcvid是服務器從客戶端獲取的接收ID。請注意,這確實是一個特例。通常,在服務器回覆客戶端之後,接收ID不再具有任何含義(原因是客戶端未被阻止,服務器無法再次解鎖,或者從客戶端讀取數據或向客戶端寫入數據,等等。)。但在這種情況下,接收ID包含足夠的信息,以便內核能夠決定事件應該傳遞到哪個客戶端。當服務器調用MsgDeliverEvent()函數時,服務器不會阻塞 - 這是對服務器的非阻塞調用。客戶端將事件(由內核)傳遞給它,然後可以執行適當的任何操作。

Channel flags

當我們介紹服務器(在“服務器”中)時,我們提到ChannelCreate()函數接受一個flags參數,我們只是將它保留爲零。

現在是時候解釋flags了。我們只檢查一些可能的標誌值:

_NTO_CHF_FIXED_PRIORITY

接收線程不會根據發送方的優先級更改優先級。(我們在下面的“Priority inheritance”部分中詳細討論優先級問題)。通常(即,如果您未指定此標誌),接收線程的優先級將更改爲發送方的優先級。

_NTO_CHF_UNBLOCK

每當客戶端線程嘗試解除阻塞時,內核都會發送一個脈衝。服務器必須回覆客戶端才能允許客戶端取消阻止。我們將在下面討論這個,因爲它對客戶端和服務器都有一些非常有趣的結果。

_NTO_CHF_THREAD_DEATH

只要在此通道上阻塞的線程死亡,內核就會發出脈衝。這對於希望始終爲服務請求維護固定“線程池”的服務器非常有用。

_NTO_CHF_DISCONNECT

只要來自單個客戶端的所有連接都與服務器斷開連接,內核就會發送脈衝。

_NTO_CHF_SENDER_LEN

內核提供客戶端的消息大小作爲提供給服務器的信息的一部分(struct _msg_info結構的srcmsglen成員)。

_NTO_CHF_REPLY_LEN

內核提供客戶端的回覆消息緩衝區大小,作爲提供給服務器的信息的一部分(struct _msg_info結構的dstmsglen成員)。

_NTO_CHF_COID_DISCONNECT

每當該進程擁有的任何連接因另一端的通道消失而終止時,內核就會發送一個脈衝。

_NTO_CHF_UNBLOCK

我們來看看_NTO_CHF_UNBLOCK標誌;它對客戶端和服務器都有一些有趣的褶皺。

通常(即,當服務器未指定_NTO_CHF_UNBLOCK標誌的情況下)當客戶端希望從MsgSend()(以及相關的MsgSendv(),MsgSendvs()等函數族)解鎖時,客戶端簡單地解除阻塞。客戶端可能希望由於接收到信號或內核超時而解除阻塞(請參閱QNX Neutrino C庫參考中的TimerTimeout()函數,以及“Clocks, Timers,
and Getting a Kick Every So Often
”章節)。不幸的是,服務器不知道客戶端已解除阻止,不再等待回覆。請注意,除非在非常特殊的情況下需要服務器與其所有客戶端之間的協作,否則無法寫入關閉此標誌的可靠服務器。

假設您有一個具有多個線程的服務器,所有線程都在服務器的MsgReceive()函數中被阻止。客戶端向服務器發送消息,服務器的一個線程接收它。此時,客戶端被阻止,服務器中的線程正在主動處理請求。現在,在服務器線程有機會回覆客戶端之前,客戶端從MsgSend()解除阻塞(讓我們假設它是由於信號)。

請記住,服務器線程仍在代表客戶端處理請求。但由於客戶端現在已被解除阻塞(客戶端的MsgSend()將返回EINTR),客戶端可以自由地向服務器發送另一個請求。由於QNX Neutrino服務器的架構,另一個線程將從客戶端收到另一條消息,具有完全相同的接收ID!服務器無法區分這兩個請求!當第一個線程完成並回復客戶端時,它實際上是回覆客戶端發送的第二條消息,而不是第一條消息(因爲線程實際上認爲它正在做)。因此,服務器的第一個線程回覆客戶端的第二條消息。

這很糟糕;但讓我們更進一步。現在服務器的第二個線程完成請求並嘗試回覆客戶端。但是由於服務器的第一個線程已經回覆到客戶端,因此客戶端現在被解除阻塞,服務器的第二個線程從其回覆中收到錯誤。

此問題僅限於多線程服務器,因爲在單線程服務器中,服務器線程仍將忙於處理客戶端的第一個請求。這意味着即使客戶端現在已解除阻止並再次發送到服務器,客戶端現在將進入SEND阻塞狀態(而不是REPLY-blocked狀態),允許服務器完成處理,回覆客戶端(這將導致錯誤,因爲客戶端不再被REPLY阻止),然後服務器將從客戶端接收第二條消息。這裏真正的問題是服務器代表客戶端執行無用的處理(客戶端的第一個請求)。處理沒用,因爲客戶端不再等待該工作的結果。

解決方案(在多線程服務器的情況下)是讓服務器爲其ChannelCreate()調用指定_NTO_CHF_UNBLOCK標誌。這告訴內核,“告訴我客戶端何時嘗試解鎖我(通過向我發送脈衝),但不要讓客戶端解鎖! 我會自己解鎖客戶端。“

要記住的關鍵是這個服務器標誌通過不允許客戶端解除阻塞來改變客戶端的行爲,直到服務器說它可以這樣做。

在單線程服務器中,會發生以下情況:

這沒有幫助客戶端解鎖它,但它確實確保服務器不會混淆。在這種示例中,服務器很可能只是忽略它從內核獲得的脈衝。這樣做是可以的 - 這裏的假設是讓客戶端阻塞直到服務器準備好數據是安全的。

如果您希望服務器根據內核發送的脈衝進行操作,有兩種方法可以執行此操作:

  • 在服務器中創建另一個偵聽消息的線程(特別是偵聽來自內核的脈衝)。第二個線程將負責取消第一個線程中正在進行的操作。兩個線程中的一個將回復客戶端。

  • 不要在線程本身中執行客戶端的工作,而是排隊工作。這通常在服務器將客戶端的工作存儲在隊列中並且服務器是事件驅動的應用程序中完成。通常,到達服務器的消息之一表示客戶端的工作現在已完成,服務器應該回復。在這種情況下,當內核脈衝到達時,服務器代表客戶端取消正在執行的工作並回復。

您選擇哪種方法取決於服務器的工作類型。在第一種情況下,服務器代表客戶端主動執行工作,所以你真的沒有選擇 - 你必須有一個第二個線程來監聽來自內核的unblock-pulse(或者你可以在線程內定期輪詢以查看脈衝是否已到達,但通常不鼓勵輪詢)。

在第二種情況下,服務器還有其他工作要做 - 可能已經命令一塊硬件“去收集數據。”在這種情況下,無論如何,服務器的線程將被阻塞在MsgReceive()函數上,等待硬件指示命令已完成。

在任何一種情況下,服務器都必須回覆客戶端,否則客戶端將保持阻止狀態。

Synchronization problem

即使您如上所述使用_NTO_CHF_UNBLOCK標誌,仍然需要處理一個同步問題。 假設您在MsgReceive()函數上阻塞了多個服務器線程,等待消息或脈衝,客戶端會向您發送消息。一個線程關閉並開始客戶端的工作。當發生這種情況時,客戶端希望解除阻塞,因此內核會生成解除阻塞脈衝。服務器中的另一個線程接收此脈衝。此時,存在競爭條件 - 第一個線程可能正準備回覆客戶端。如果第二個線程(獲得脈衝)做了回覆,那麼客戶端有可能解除阻塞並向服務器發送另一條消息,服務器的第一個線程現在有機會運行並使用第一個請求的數據回覆客戶端的第二個請求:

或者,如果獲得脈衝的線程即將回復客戶端,並且第一個線程做出回覆,那麼您具有相同的情況 - 第一個線程解除阻塞客戶端,誰發送另一個請求,以及第二個線程( 獲得脈衝)現在取消阻止客戶端的第二個請求。

情況是你有兩個並行的執行流程(一個由消息引起,一個由脈衝引起)。通常,我們會立即將此視爲需要互斥鎖的情況。不幸的是,這會導致問題 - 必須在MsgReceive()之後立即獲取互斥鎖並在MsgReply()之前釋放互斥鎖。雖然這確實有效,但它會破壞解鎖脈衝的全部目的!(服務器要麼得到消息並忽略解除阻塞脈衝,直到它回覆客戶端,否則服務器將獲得解除阻塞脈衝並取消客戶端的第二次操作。)

一個看起來很有希望(但最終註定要失敗)的解決方案是擁有一個細粒度的互斥體。 我的意思是一個互斥鎖,只能在控制流的一小部分周圍鎖定和解鎖(你應該使用互斥鎖的方式,而不是像上面提到的那樣阻塞整個處理部分)。您在服務器中設置了“我們已經回覆了嗎?”標誌,當您收到消息時,此標誌將被清除,並在您回覆消息時進行設置。 就在你回覆郵件之前,你要檢查標誌。如果該標誌指示該消息已被回覆,則您將跳過回覆。 在檢查和設置標誌時,互斥鎖將被鎖定和解鎖。

不幸的是,這不起作用,因爲我們並不總是處理兩個並行的執行流程 - 客戶端在處理期間不會總是被信號擊中(導致解除阻塞脈衝)。這是它破壞的場景:

  • 客戶端向服務器發送消息;客戶端現在被阻止,服務器現在正在運行。

  • 由於服務器收到來自客戶端的請求,標誌將重置爲0,表示我們仍需要回復客戶端。

  • 服務器正常回復客戶端(因爲標誌設置爲0)並將標誌設置爲1,表示如果unblock-pulse到達,則應忽略該標誌。

  • (問題從這裏開始。)客戶端向服務器發送第二條消息,幾乎在發送後立即被信號命中; 內核向服務器發送一個unblock-pulse。

  • 接收消息的服務器線程即將獲取互斥鎖以檢查標誌,但是沒有完全到達(它被搶佔)。

  • 另一個服務器線程現在獲取脈衝,因爲該標誌仍然從上次設置爲1,忽略脈衝。

  • 現在服務器的第一個線程獲取互斥鎖並清除標誌。

  • 此時,unblock事件已丟失。

如果您優化標誌以指示更多狀態(例如接收脈衝,回覆脈衝,收到消息,回覆消息),您仍將遇到同步競爭條件,因爲您無法在兩者之間創建原子綁定 標誌和接收和回複函數調用。(從根本上說,這就是問題所在 - 在MsgReceive()之後和調整標誌之前的小時間窗口,以及在MsgReply()之前調整標誌之後。)解決這個問題的唯一方法就是讓 內核跟蹤你的標誌。

Using the _NTO_MI_UNBLOCK_REQ

幸運的是,內核會在消息信息結構中將您的標誌作爲單個位跟蹤(您作爲MsgReceive()的最後一個參數傳遞的struct _msg_info,或者您可以在給定接收ID後通過調用獲取MsgInfo())。

標誌被稱爲_NTO_MI_UNBLOCK_REQ,並且如果客戶端希望解除阻塞(例如,在接收到信號之後)則設置該標誌。

這意味着在多線程服務器中,您通常會有一個正在執行客戶端工作的“工作”線程,以及另一個將接收解除阻塞消息的線程(或其他一些消息;我們只關注取消阻止消息 現在)。 當您從客戶端獲得解除阻止消息時,您將爲自己設置一個標誌,讓您的程序知道線程希望解除阻止。

有兩種情況需要考慮:

•“工人”線程被阻止; 要麼
•“worker”線程正在運行。

如果工作線程被阻止,則需要讓獲取unblock消息的線程喚醒它。例如,如果它正在等待資源,它可能會被阻止。當工作線程喚醒時,它應檢查_NTO_MI_UNBLOCK_REQ標誌,如果設置,則以中止狀態回覆。如果未設置標誌,則線程可以執行喚醒時執行的任何正常處理。

或者,如果工作線程正在運行,它應該定期檢查解除阻塞線程可能設置的“自我標記”,如果設置了標誌,它應該以中止狀態回覆客戶端。請注意,這只是一個優化:在未優化的情況下,工作線程將不斷調用接收ID上的“MsgInfo”並檢查_NTO_MI_UNBLOCK_REQ位本身。

Message passing over a network

爲了清楚起見,我已經避免談論你如何使用網絡上的消息傳遞,儘管這是QNX Neutrino靈活性的關鍵部分!

到目前爲止,您學到的所有內容都適用於通過網絡傳遞的消息。
在本章的前面,我向您展示了一個例子:

#include <fcntl.h>
#include <unistd.h>
int main (void)
{
int fd;
fd = open ("/net/wintermute/home/rk/filename", O_WRONLY);
write (fd, "This is message passing\n", 24);
close (fd);
return (EXIT_SUCCESS);
}

當時,我說這是“使用通過網絡傳遞消息的示例。”客戶端創建與ND/PID/CHID(恰好位於不同節點上)的連接,並且服務器執行 MsgReceive()在其頻道上。在這種情況下,客戶端和服務器與本地單節點情況相同。你可以在這裏停止閱讀 - 關於通過網絡傳遞的消息真的沒有任何“棘手”。但對於那些對此如何感到好奇的讀者,請繼續閱讀!

現在我們已經看到了本地消息傳遞的一些細節,我們可以更深入地討論通過網絡傳遞的消息是如何工作的。雖然這個討論可能看起來很複雜,但它實際上歸結爲兩個階段:名稱解析,一旦得到解決,簡單的消息傳遞。

這是一個圖表,說明了我們將要討論的步驟:

在圖中,我們的節點稱爲magenta,並且,如示例所暗示的,目標節點稱爲wintermute。
讓我們分析客戶端程序使用Qnet通過網絡訪問服務器時發生的交互:

  1. 客戶端的open()函數被告知打開一個恰好在其前面有/net的文件名。(名稱/net是Qnet顯示的默認名稱。)此客戶端不知道誰負責該特定路徑名,因此它連接到process manager(步驟1)以找出實際擁有該資源的人員。無論我們是通過網絡傳遞消息還是自動發生,都可以完成此操作。由於本地QNX Neutrino網絡管理器Qnet“擁有”以/net開頭的所有路徑名,因此流程管理器將信息返回給客戶端它向Qnet詢問路徑名。

  2. 客戶端現在向Qnet的資源管理器線程發送消息,希望Qnet能夠處理該請求。但是,此節點上的Qnet不負責提供客戶端所需的最終服務,因此它告訴客戶端它應該實際聯繫節點wintermute上的進程管理器。(這樣做的方法是通過“重定向”響應,它爲客戶端提供應該聯繫的服務器的ND/PID/CHID。)此重定向響應也由客戶端庫自動處理。

  3. 客戶端現在連接到wintermute上的process manager。這涉及通過Qnet的網絡處理程序線程發送節點外消息。客戶端節點上的Qnet進程獲取消息並通過介質將其傳輸到遠程Qnet,遠程Qnet將其傳遞給wintermute上的process manager。那裏的process manager解析了路徑名的其餘部分(在我們的例子中,那將是“/home/rk/filename”部分)併發回一個重定向消息。此重定向消息遵循相反的路徑(從服務器的Qnet通過介質到客戶端節點上的Qnet,最後返回到客戶端)。此重定向消息現在包含客戶端首先要聯繫的服務器的位置,即服務於客戶端請求的服務器的ND/PID/CHID。(在我們的示例中,服務器是一個文件系統。)

  4. 客戶端現在將請求發送到該服務器。此處遵循的路徑與上面步驟3中遵循的路徑相同,只是直接聯繫服務器而不是通過流程管理器。

一旦建立了步驟1到3,步驟4就是所有未來通信的模型。在上面的客戶端示例中,open(),read()和close()消息都採用路徑編號4。請注意,客戶端的open()是觸發這一系列事件的原因 - 但實際上是 如所描述的那樣打開消息流(通過路徑號4)。

對於真正感興趣的讀者:我遺漏了一步。在第2步中,當客戶向Qnet詢問wintermute時,Qnet需要弄清楚wintermute是誰。這可能導致Qnet再執行一次網絡事務來解析節點名稱。如果我們假設Qnet已經知道wintermute,那麼上面給出的圖是正確的。

Networked message passing differences

因此,一旦建立連接,所有進一步的消息傳遞流程都使用上圖中的步驟4。這可能會導致您錯誤地認爲通過網絡傳遞的消息與本地情況下傳遞的消息相同。不幸的是,事實並非如此。以下是不同之處:

  • 延誤時間更長

  • 無論節點是否處於活動狀態,ConnectAttach()都會返回成功 - 在第一個消息傳遞時發生實際錯誤指示。

  • MsgDeliverEvent()不保證可靠

  • MsgReply(),MsgRead(),MsgWrite()現在處於blocking calls,而在本地情況下它們不是。

  • MsgReceive()可能無法接收客戶端發送的所有數據;服務器可能需要調用MsgRead()來完成剩下的工作。

Longer delays

由於消息傳遞現在通過某種介質完成,而不是直接由內核控制的內存到內存副本,因此您可以預期傳輸消息所需的時間將顯着增加(100 MB以太網與100 MHz 64- 位寬DRAM將是一個或兩個數量級更慢的速度)。此外,最重要的是協議開銷(最小)和有損網絡上的重試。

Impact on ConnectAttach()

當您調用ConnectAttach()時,您將指定ND,PID和CHID。在QNX Neutrino中發生的所有事情是內核將連接ID返回到上圖中所示的Qnet“網絡處理程序”線程。由於沒有發送任何消息,因此您不會被告知您剛剛連接的節點是否仍然存在。在正常使用中,這不是問題,因爲大多數客戶端不會自己做ConnectAttach() - 相反,他們將使用庫調用open()的服務,它執行ConnectAttach(),然後 乎立即發出“open”的消息。這具有幾乎立即指示遠程節點是否存活的效果。

Impact on MsgDeliverEvent()

當服務器在本地調用MsgDeliverEvent()時,內核有責任將事件傳遞給目標線程。 在網絡方式上,服務器仍然調用MsgDeliverEvent(),但內核將該事件的“代理”傳遞給Qnet,由Qnet將代理交付給另一個(客戶端)Qnet,然後他們將交付實際的事件發生在客戶端。事情可能會在服務器端搞砸,因爲MsgDeliverEvent()函數調用是非阻塞的 - 這意味着一旦服務器調用了MsgDeliverEvent(),它就會運行。現在轉過來說“我討厭告訴你這件事,但你知道我說的MsgDeliverEvent()成功了嗎?爲時已晚,好吧,它沒有!”

Impact on MsgReply(), MsgRead(), and MsgWrite()

爲了防止MsgDeliverEvent()與MsgReply(),MsgRead()和MsgWrite()一起提到的問題,這些函數在網絡上使用時轉換爲阻塞調用。在本地,他們只是簡單地傳輸數據並立即解鎖。在網絡上,我們必須(在MsgReply()的情況下)確保數據已經傳送到客戶端或(在其他兩個的情況下)通過網絡實際傳輸數據到客戶端或從客戶端傳輸數據。

Impact on MsgReceive()

最後,MsgReceive()也受到影響(在網絡情況下)。當服務器的MsgReceive()取消阻塞時,Qnet並非所有客戶端的數據都可以通過網絡傳輸。這是出於性能原因而完成的。

struct _msg_info中有兩個標誌作爲MsgReceive()的最後一個參數傳遞(我們在上面的“Who sent the message?”中詳細介紹了這個結構):

msglen

指示MsgReceive()實際傳輸了多少數據(Qnet喜歡傳輸8 KB)。

Srcmsglen

指示客戶端要傳輸的數據量(由客戶端確定)。

因此,如果客戶端希望通過網絡傳輸1兆字節的數據,則服務器的MsgReceive()將解除阻塞,並且msglen將設置爲8192(表示緩衝區中有8192個字節可用),而srcmsglen將設置爲1048576(表明客戶端試圖發送1兆字節)。

然後,服務器使用MsgRead()從客戶端的地址空間獲取其餘數據。

Some notes on NDs

關於消息傳遞,我們尚未談到的另一個“有趣”的事情是“節點描述符”或簡稱“ND”的整個業務。

回想一下,我們在示例中使用了符號節點名稱,例如/net/wintermute。在QNX 4(QNX Neutrino之前的操作系統的先前版本)下,本機網絡基於節點ID的概念,節點ID是網絡上唯一的小整數。因此,我們將討論“節點61”或“節點1”,這反映在函數調用中。

在QNX Neutrino下,所有節點都在內部以32位數量引用,但它不是網絡唯一的! 我的意思是,wintermute可能會將spud視爲節點描述符編號“7”,而spud可能會將magenta視爲節點描述符編號“7”。讓我擴展一下,爲您提供更好的圖片。此表顯示了三個節點(wintermute,spud和foobar)可能使用的一些示例節點描述符:

注意每個節點的節點描述符本身是如何爲零。還要注意wintermute的spud節點描述符是如何“7”,foobar的spud節點描述符也是如此。但是,foobar的wintermute節點描述符是“4”,而foobar的spud節點描述符是“6”。正如我所說,它們在網絡中並不是唯一的,儘管它們在每個節點上都是唯一的。您可以有效地將它們視爲文件描述符 - 如果兩個進程訪問同一個文件,它們可能具有相同的文件描述符,但它們可能不會;它取決於誰在何時打開哪個文件。

幸運的是,您不必擔心節點描述符,原因如下:

  1. 您通常會執行的大多數節點外消息傳遞將通過更高級別的函數調用(例如open(),如上例所示)。

  2. 節點描述符不被緩存 - 如果你得到它,你應該立即使用它然後忘記它。

  3. 有一些庫調用將路徑名(如/net/magenta)轉換爲節點描述符。

要使用節點描述符,您需要包含文件<sys/netmgr.h>,因爲它包含一堆netmgr _ *()函數。

您可以使用函數netmgr_strtond()將字符串轉換爲節點描述符。一旦有了這個節點描述符,就可以在ConnectAttach()函數調用中立即使用它。具體來說,您不應該將其緩存在數據結構中!原因是一旦與該特定節點的所有連接斷開,本機網絡管理器可以決定重用它。因此,如果/net/magenta的節點描述符爲“7”,並且連接到它,發送消息,然後斷開連接,則本機網絡管理器可能會再次返回節點描述符“7”對於不同的節點。

由於節點描述符並不是每個網絡唯一的,因此出現的問題是“你如何在網絡中傳遞這些東西?”顯然,magenta對節點描述符“7”的看法將與wintermute完全不同。這裏有兩種解決方案:

  • 不要傳遞節點描述符;請改用符號名稱(例如/net/wintermute)。

  • 使用netmgr_remote_nd()函數。

首先是一個很好的通用解決方案。第二種解決方案使用起來相當簡單:

此函數有兩個參數:remote_nd是目標機器的節點描述符,local_nd是要轉換爲遠程機器視點的節點描述符(從本地機器的角度來看)。結果是從遠程計算機的角度來看有效的節點描述符。

例如,假設wintermute是我們的本地機器。我們有一個節點描述符“7”,它在我們的本地機器上有效並指向magenta。我們想知道的是節點描述符magenta用來與我們交談的內容:

這可能會打印類似於:

這表示在magenta上,節點描述符“4”指的是我們的節點。(注意使用特殊常量ND_LOCAL_NODE,它實際上爲零,表示“此節點。”)

現在,回想一下我們說(在Who sent the message?”中)struct _msg_info包含兩個節點描述符:

我們在說明中對這兩個字段說:

  • nd是發送節點的接收節點的節點描述符

  • srcnd是接收節點的發送節點的節點描述符

因此,對於上面的示例,其中wintermute是本地節點,magenta是遠程節點,當magenta向我們發送消息(wintermute)時,我們期望:

  • nd將包含7

  • srcnd將包含4

Priority inheritance

實時操作系統中的一個有趣問題是稱爲優先級倒置的現象。

優先級倒置表現爲,例如,低優先級線程消耗所有可用的CPU時間,即使優先級較高的線程已準備好運行。

現在你可能在想,“等一下!你說優先級較高的線程總是搶佔優先級較低的線程!怎麼會這樣?”

這是真的;優先級較高的線程將始終搶佔較低優先級的線程。但有趣的事情可能發生。讓我們看一下我們有三個線程的場景(在三個不同的進程中,爲了簡單起見),“L”是我們的低優先級線程,“H”是我們的高優先級線程,“S”是服務器。

此圖顯示了三個線程及其優先級:

目前,H正在運行。S,優先級較高的服務器線程,現在沒有任何事情可做,所以它正在等待消息並在MsgReceive()中被阻止。L想要運行,但其優先級低於運行時的H。一切都如你所料,對吧?

現在H決定它要睡100毫秒 - 也許它需要等待一些緩慢的硬件。此時,L正在運行。

這是事情變得有趣的地方。

作爲其正常操作的一部分,L向服務器線程S發送消息,導致S進入READY並且(因爲它是READY的最高優先級線程)開始運行。不幸的是,L發送給S的消息是“計算到小數點後5000位”。

顯然,這需要超過100毫秒。因此,當H的100毫秒達到並且H變爲READY時,猜猜是什麼?它不會運行,因爲S是READY並且優先級更高!

發生的事情是,低優先級線程通過優先級更高的線程利用CPU來防止更高優先級的線程運行。這是優先倒置。

要解決它,我們需要討論優先級繼承。一個簡單的解決方法是讓服務器S繼承客戶端線程的優先級:

在這種情況下,當H的100毫秒睡眠完成時,它會進入READY狀態,因爲它是最高優先級的READY線程,所以它會運行。

不錯,但還有一個“陷阱”。

假設H現在決定它也想進行計算。它想要計算第5034個素數,因此它向S發送消息,並進入block狀態。

但是,S仍在計算pi,優先級爲5!在我們的示例系統中,有很多其他線程在優先級高於5的情況下運行,它們正在利用CPU,有效地確保了S沒有太多時間來計算pi。

這是優先級倒置的另一種形式。在這種情況下,優先級較低的線程阻止了更高優先級的線程訪問資源。將此與優先級倒置的第一種形式進行對比,其中優先級較低的線程實際上消耗了CPU - 在這種情況下,它只是阻止了更高優先級的線程獲取CPU - 它不消耗任何CPU本身。

幸運的是,這裏的解決方案也相當簡單。將服務器的優先級提升爲所有被阻止客戶端中的最高者:

這樣我們就可以讓L的工作優先於L,但我們確保H在CPU上獲得了一個公平的破解。

So what's the trick?

沒有訣竅!QNX Neutrino會自動爲您完成此操作。(如果不需要,可以關閉優先級繼承;請參閱ChannelCreate()函數文檔中的_NTO_CHF_FIXED_PRIORITY標誌。)

但是,這裏有一個小的設計問題。你如何將優先級恢復到改變之前的狀態?

您的服務器正在運行,爲來自客戶端的請求提供服務,並在從MsgReceive()調用中解除阻塞時自動調整其優先級。但什麼時候應該將其優先級調整回MsgReceive()調用之前的優先級呢?

有兩種情況需要考慮:

  • 服務器在正確地爲客戶端提供服務後執行一些額外的處理。這應該在服務器的優先級而不是客戶端完成。

  • 服務器立即執行另一個MsgReceive()來處理下一個客戶端請求。

在第一種情況下,當服務器不再爲該客戶端工作時,服務器以客戶端的優先級運行是不正確的!解決方案非常簡單。使用pthread_setschedparam()或pthread_setschedprio()函數(在“Processes and Threads”一章中討論)將優先級恢復爲應該的優先級。那另一個案子怎麼樣?答案很簡單:誰在乎呢?

想一想。如果服務器在優先級爲29時變爲RECEIVE阻塞而與優先級爲2時有何不同?問題的事實是它被RECEIVE阻止了!它沒有獲得任何CPU時間,因此它的優先級無關緊要。一旦MsgReceive()函數解除對服務器的阻塞,服務器就會繼承(新)客戶端的優先級,一切都按預期工作。

Summary

消息傳遞是一個非常強大的概念,是構建QNX Neutrino(實際上是所有過去的QNX操作系統)的主要功能之一。

通過消息傳遞,客戶端和服務器交換消息(同一進程中的線程到線程,同一節點上不同進程中的線程到線程,或者網絡中不同節點上的不同進程中的線程到線程))。客戶端發送消息並阻塞,直到服務器收到消息,處理消息並回復客戶端。

消息傳遞的主要優點是:

  • 消息內容不會根據目標位置(本地與網絡)發生變化。

  • 消息爲客戶端和服務器提供“乾淨”的解耦點。

  • 隱式同步和序列化有助於簡化應用程序的設計。

原網頁:Message Passing

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