QNX學習 -- API之訊息傳遞
大家都知道QNX是個微核心結構的作業系統,靠的是程式間通訊來實現整個系統功能的。那麼具體到寫一個程式的時候,到底這個通訊是如何完成的呢?這章就是具體介紿最底層的訊息傳遞API的。訊息傳遞是通過核心進行的,所以所謂的API,實際也就是最底層的核心呼叫了。需要指出的是,真正在QNX上寫程式的時候,很少會直接用到這些API,而是利用更高層的API,不過,知道這些底層的API對於將來理解建立在這些API上的介面,應該會有幫助的。
頻道(Channel)與連線(Connect)
訊息傳遞是基於伺服器與客戶端的模式來進行的,那麼客戶端怎樣才能與伺服器端通訊呢?最簡單的,當然是指定對方的程式號。要傳送的一方,將訊息加一個頭,告訴核心“把這個訊息發給pid 12345"就行了。其實這也是QNX4時候的做法。但QNX6開始完整支援POSIX執行緒後,這種方法似乎就不太適合了。如果伺服器,有兩個執行緒,分別進行不同的服務,那該怎麼辦呢?或者你會說“把這個訊息發給pid 12345 tid 3"就行了。可是,如果某一個服務,不是由單一執行緒來進行服務的,而是有一組執行緒進行的,那又怎麼辦呢?為此,QNX6抽象出了”頻道“(Channel)這個概念。一個頻道,就是一個服務的入口;至於這個頻道到底具體有多少執行緒為其服務,那都是伺服器端自己的事情。一個伺服器如果有多個服務,它也可以開多個頻道。而客戶端,在向“頻道”傳送訊息前,需要先建立連線(Connection),然後將訊息在連線上發出去。這樣同一個客戶端,如果需要,可以與同一個頻道建立多個連線。所以,大致上通訊的準備過程是這樣的:
伺服器
程式碼:
ChannelId = ChannelCreate(Flags);
客戶端
程式碼:
ConnectionId = ConnectAttach(Node, Pid, Chid, Index, Flag);
伺服器端就不用解釋了,客戶端要建立連線的話,它需要Node,這個就是機器號。如果過網路(透明分佈處理)時這個值決定了哪一臺機器;如果客戶端與伺服器在同一臺機器裡時,這個數字是0,或者說ND_LOCAL_NODE;pid是服備器的程式號;而chid就是伺服器呼叫 ChannelCreate()後得到的頻道號了。Index與Flag以後再討論。基本上客戶端就是同"Node這臺機器裡的,Pid這個程式的,Chid頻道"做一個連線。有了連線以後,就可以進行訊息傳遞了。
連線的終止是ConnectDetach(),而頻道的結束則是ChannelDestroy()了。不過,一般伺服器都是長久存在的,不大有需要ChannelDestroy()的時候。
傳送(Send),接收(Receive)和應答(Reply)
QNX的訊息傳遞,與我們傳統常見的程式間通訊最大的不同,就是這是一個"同步的"訊息傳遞。一個訊息傳遞,都要經過傳送,接收和應答三個部份,所謂的 SRR過程。具體來說,客戶端在連線上"傳送"訊息,一旦傳送,客戶端會被阻塞,伺服器端會接收到訊息,進行處理,最後,將處理結果"應答"給客戶端;只有伺服器"應答"了以後,客戶端的阻塞狀態才會被解除。這種同步的過程,不但保證的客戶端與伺服器端的時序,也大大簡化了程式設計。具體用API來說,就是這樣。
伺服器
程式碼:
ReceiveId = MsgReceive(ChannelId, ReceiveBuffer, ReceiveBufLength, &MsgInfo);
(... 檢查Buffer裡的訊息進行處理 ...)
MsgReply(RceeiveId, ReplyStatus, ReplyBuf, ReplyLen);
客戶端
程式碼:
MsgSend(ConnectionId, SendBuf, SendLen, ReplyBuf, ReplyLen);
(... 由OS將這個執行緒掛起 ...)
(... 當伺服器MsgReply()後,OS 解除執行緒的阻塞狀態, 客戶端可以檢查自己的 ReceiveBuf 看看應答結果 ...)
伺服器端在頻道上進行接收,處理完後應答;客戶端則是在連線上傳送,要注意在傳送的同時,客戶端還提供了接收應答用的緩衝。如果你細心的話,或許你會問,伺服器端的MsgReceive()與客戶端的MsgSend()沒有同步,會不會有問題呢?比如,如果MsgSend()時,伺服器沒有在 MsgReceive(),會出什麼事呢?答案是OS依然會把傳送執行緒掛起,傳送執行緒從執行狀態(RUNNING)轉入“傳送阻塞”狀態(SEND BLOCK),一直等到伺服器來MsgReceive()時,再將SendBuf裡的東西複製到ReceiveBuffer裡去,同時傳送執行緒的狀態變成 “應答阻塞”(REPLY BLOCK)。
同樣的,如果伺服器呼叫MsgReceive()時,沒有客戶端,伺服器執行緒也會被掛起,進入“接收阻塞”狀態(RECEIVE BLOCK)。
在應答時,還可以用MsgError()來告訴傳送方有錯誤發生了。因為MsgReply()也可以返回一個狀態,或許你會問這兩者之間有什麼區別?MsgReply(rcvid, EINVAL, 0, 0);的結果是,MsgSend() 這個函式的返回值是22(EINVAL);而MsgError(rcvid, EINVAL);的結果,是MsgSend()返回-1,而errno被設為EINVAL。
資料區與iov
除了用線性的緩衝區進行訊息傳遞以外,為了方便使用,還提供了用iov_t來“彙集”資料。也就是說,可以一次傳送幾塊資料。好象下面的圖這樣子。雖然在客戶端藍色的Header同紅色的databuf是兩塊不相鄰的記憶體,但傳遞到伺服器端的ReceiveBuffer裡,就是連續的了。也就是說在伺服器端,要想得到原來databuf裡的資料,只需要(ReceiveBuffer + sizeof(header))就可以了。(要注意資料結構對其)
客戶端
程式碼:
SETIOV(&iov[0], &header, sizeof(header));
SETIOV(&iov[1], databuf, datalen);
MsgSendvs(ConnectionId, iov, 2, Replybf, ReplyLen);
"header" 與 "databuf"是不連續的兩塊資料。伺服器接收後,"header"與"databuf"被連續地存在ReceiveBuffer裡。
程式碼:
ReceiveId = MsgReceive(ChannelId, ReceiveBuffer, ReceiveBufLength, &MsgInfo);
header = (struct header *)ReceiveBUffer;
databuf = (char *)((char *)header + sizeof(*header));
例子
好了,有了以上這些基本函式(核心呼叫),我們就可以寫一個客戶端和一個伺服器端,進行最基本的通訊了。
服務囂:這個伺服器,準務好頻道後,就從頻道上接收資訊。如果資訊是字串”Hello“的話,這個伺服器應答一個”World“字串。如果收到的信處是字串“Ni Hao", 那麼它會應答”Zhong Guo",其它任何訊息都用MsgError()回答一個錯誤。
程式碼:
$ cat simple_server.c
// Simple server
#include <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/neutrino.h>
int main()
{
int chid, rcvid, status;
char buf[128];
if ((chid = ChannelCreate(0)) == -1) {
perror("ChannelCreate");
return -1;
}
printf("Server is ready, pid = %d, chid = %d\n", getpid(), chid);
for (;;) {
if ((rcvid = MsgReceive(chid, buf, sizeof(buf), NULL)) == -1) {
perror("MsgReceive");
return -1;
}
printf("Server: Received '%s'\n", buf);
/* Based on what we receive, return some message */
if (strcmp(buf, "Hello") == 0) {
MsgReply(rcvid, 0, "World", strlen("World") + 1);
} else if (strcmp(buf, "Ni Hao") == 0) {
MsgReply(rcvid, 0, "Zhong Guo", strlen("Zhong Guo") + 1);
} else {
MsgError(rcvid, EINVAL);
}
}
ChannelDestroy(chid);
return 0;
}
客戶端:客戶端通過從命令列得到的伺服器的程式號與頻道號,與伺服器建立連線。然後向伺服器傳送三遍"Hello"和”Ni Hao",並檢查返回值。最後發一個“unknown"看是不是MsgSend()會得到一個出錯返回。
程式碼:
$ cat simple_client.c
//simple client
#include <stdio.h>
#include <string.h>
#include <sys/neutrino.h>
int main(int argc, char **argv)
{
pid_t spid;
int chid, coid, i;
char buf[128];
if (argc < 3) {
fprintf(stderr, "Usage: simple_client <pid> <chid>\n");
return -1;
}
spid = atoi(argv[1]);
chid = atoi(argv[2]);
if ((coid = ConnectAttach(0, spid, chid, 0, 0)) == -1) {
perror("ConnectAttach");
return -1;
}
/* sent 3 pairs of "Hello" and "Ni Hao" */
for (i = 0; i < 3; i++) {
sprintf(buf, "Hello");
printf("client: sent '%s'\n", buf);
if (MsgSend(coid, buf, strlen(buf) + 1, buf, sizeof(buf)) != 0) {
perror("MsgSend");
return -1;
}
printf("client: returned '%s'\n", buf);
sprintf(buf, "Ni Hao");
printf("client: sent '%s'\n", buf);
if (MsgSend(coid, buf, strlen(buf) + 1, buf, sizeof(buf)) != 0) {
perror("MsgSend");
return -1;
}
printf("client: returned '%s'\n", buf);
}
/* sent a bad message, see if we get an error */
sprintf(buf, "Unknown");
printf("client: sent '%s'\n", buf);
if (MsgSend(coid, buf, strlen(buf) + 1, buf, sizeof(buf)) != 0) {
perror("MsgSend");
return -1;
}
ConnectDetach(coid);
return 0;
}
分別編譯後的執行結果是這樣的:
伺服器:
程式碼:
$ ./simple_server
Server is ready, pid = 36409378, chid = 2
Server: Received 'Hello'
Server: Received 'Ni Hao'
Server: Received 'Hello'
Server: Received 'Ni Hao'
Server: Received 'Hello'
Server: Received 'Ni Hao'
Server: Received 'Unknown'
Server: Received ''
客戶端:
程式碼:
$ ./simple_client 36409378 2
client: sent 'Hello'
client: returned 'World'
client: sent 'Ni Hao'
client: returned 'Zhong Guo'
client: sent 'Hello'
client: returned 'World'
client: sent 'Ni Hao'
client: returned 'Zhong Guo'
client: sent 'Hello'
client: returned 'World'
client: sent 'Ni Hao'
client: returned 'Zhong Guo'
client: sent 'Unknown'
MsgSend: Invalid argument
可變訊息長度
從上面的程式也可以看出來,訊息傳遞的實質是把資料從一個緩衝,複製到(另一個程式的)另一個緩衝裡去。問題是,如何確定緩衝的大小呢?上述的例子裡,伺服器端用了一個128位元組的緩衝,萬一客戶端傳送一個比如說512位元組的訊息,是不是訊息傳遞就會出錯了呢?
答案是,傳遞依然成功,但是,只有SendBuffer的最初的128個位元組的資料會被複制。設計思想是,伺服器必須發現這樣的情形,並設法取得完整的資料。
在MsgRecieve()時,第四個引數是一個 struct _msg_info。核心會在進行訊息傳遞的同時,填充這個結構,從而告訴讓你得到一些資訊。在這個結構中,"msglen"告訴你這次訊息傳遞你實際收到了多少位元組(在我們的例子裡,就是128),而"srcmsglen"則告訴你傳送方的實際Buffer會有多大(在我們的例子裡,是512)。通過比較這兩個值,伺服器端就可以判斷有沒有收到全部資料。
一旦伺服器知道了還有更多的資料沒有收到,那該怎麼辦呢?QNX提供了 MsgRead()這個特殊函式。伺服器端可以用這個函式,從傳送緩衝中“讀取”資料。MsgRead()基本上就是告訴核心,從傳送緩衝的某個指定偏移開始,讀取一定長的資料回來。所以伺服器端這部份的程式碼基本上是這樣的。
程式碼:
int rcvid;
struct _msg_info info;
char buf[128], *totalmsg;
...
rcvid = MsgReceive(chid, buf, 128, &info);
...
if (info->srcmsglen > info->msglen) {
totalmsg = malloc(info->srcmsglen);
if (!totalmsg) {
MsgError(rcvid, ENOMEM);
continue;
}
memcpy(totalmsg, buf , 128);
if (MsgRead(rcvid, &totalmsg[128], 128, info->srcmsglen - info->msglen) == -1) {
MsgError(rcvid, EINVAL);
continue;
}
} else {
totalmsg = buf;
}
/* Now totalmsg point to a full message, don't forget to free() it later on,
* if totalmsg is malloc()'d here
*/
你或者會問,為什麼訊息接收都已經結束了,伺服器端還能去讀取客戶端的資料?這是因為從一開始我們就提到的,QNX的訊息傳遞是“同步”的。還記得嗎?在伺服器端“應答”之前,客戶端是被阻塞的;也說是說客戶端的傳送緩衝會一直保留在那裡,不會變化。(另外再開個執行緒去把這個緩衝搞亂甚至free掉?當然可以。不過,這是你客戶端程式的BUG了)
與此相近的,有的時候,伺服器需要返回大量的資料給客戶端(比如說1M)。伺服器不希望 malloc(1024 * 1024),然後MsgReply(),然後再free()。(在嵌入式程式裡,經常地進行malloc()/free()不是一個很好的習慣)那麼伺服器也可以用一個小的定長緩衝,比方說16K,然後把資料“一部份一部份地寫回”客戶端的應答緩衝裡。好象下面的樣子。要記得最後還是要做一個 MsgReply() 以讓客戶端繼續執行。
程式碼:
char *buf[16 * 1024];
unsigned offset;
for (offset = 0; offset < 1024 * 1024; offset += 16 * 1024) {
/* moving data into buffer */
MsgWrite(rcvid, buffer, 16 * 1024, offset);
}
/* 1MB returned, Reply() to let client go */
MsgReply(rcvid, 0, 0, 0);
例項
以下是QNX的C庫中的read()和write()函式實裝,有了前面的基礎,應該很好理解了。先不管fd是如何得到的,只要理解fd就是 ConnectAttach()返加的連線號就可以了。雖然read()是從伺服器取得資料,而write()是向伺服器輸出資料,但實質上,它們都是向伺服器提出一個請求,由伺服器來應答。而對於write()來說,這是一個io_write_t,一個MsgWritev()把請求與要傳遞的資料一起發給伺服器;而對於read()來說,請求被封裝在 io_read_t 裡,MsgSend()把這請求傳給伺服器,read()的結果緩衝,則做為應答緩衝,由伺服器MsgReply()時填入。
read():
#include <unistd.h>
#include <sys/iomsg.h>
ssize_t read(int fd, void *buff, size_t nbytes) {
io_read_t msg;
msg.i.type = _IO_READ;
msg.i.combine_len = sizeof msg.i;
msg.i.nbytes = nbytes;
msg.i.xtype = _IO_XTYPE_NONE;
msg.i.zero = 0;
return MsgSend(fd, &msg.i, sizeof msg.i, buff, nbytes);
}
#include <unistd.h>
#include <sys/iomsg.h>
ssize_t write(int fd, const void *buff, size_t nbytes) {
io_write_t msg;
iov_t iov[2];
msg.i.type = _IO_WRITE;
msg.i.combine_len = sizeof msg.i;
msg.i.xtype = _IO_XTYPE_NONE;
msg.i.nbytes = nbytes;
msg.i.zero = 0;
SETIOV(iov + 0, &msg.i, sizeof msg.i);
SETIOV(iov + 1, buff, nbytes);
return MsgSendv(fd, iov, 2, 0, 0);
}
伺服器端應該是怎樣進行處理的?想想MsgRead()/MsgWrite(),你應該不難想像伺服器端是如何工作的吧。
脈衝(Pulse)
脈衝其實更像一個短訊息,也是在“連線”上傳送的。脈衝最大的特點是它是非同步的。傳送方不必要等接收方應答,直接可以繼續執行。但是,這種非同步性也給脈衝帶來了限制。脈衝能攜帶的資料量有限,只有一個8位的"code"域用來區分不同的脈衝,和一個32位的“value"域來攜帶資料。脈衝最主要的用途就是用來進行“通知”(Notification)。不僅是使用者程式,核心也會生成傳送特殊的“系統脈衝”到使用者程式,以通知某一特殊情況的發生。
脈衝的接收比較簡單,如果你知道頻道上不會有別的訊息,只有脈衝的話,可以用MsgReceivePulse()來只接收脈衝;如果頻道既可以接收訊息,也可以接收脈衝時,就直接用MsgReceive(),只要確保接收緩衝(ReveiveBuf)至少可以容下一個脈衝(sizeof struct _pulse)就可以了。在後一種情況下,如果MsgReceive()返回的rcvid是0,就代表接收到了一個脈衝,反之,則收到了一個訊息。所以,一個既接收脈衝,又接收訊息的伺服器,可以是這樣的。
union {
struct _pulse pulse;
msg_header header;
} msgs;
if ((rcvid = MsgReceive(chid, &msgs, sizeof(msgs), &info)) == -1) {
perror("MsgReceive");
continue;
}
if (rcvid == 0) {
process_pulse(&msgs, &info);
} else {
process_message(&msgs, &info);
}
脈衝的傳送,最直接的就是MsgSendPulse()。不過,這個函式通常只在一個程式中,用在一個執行緒要通知另一個執行緒的情形。在跨程式的時候,通常不會用到這個函式,而是用到下面將要提到的 MsgDeliverEvent()。
與訊息傳遞相比,訊息傳遞永遠是在程式間進行的。也就是說,不會有一個程式向核心傳送資料的情形。而脈衝就不一樣,除了使用者程式間可以發脈衝以外,核心也會向使用者程式傳送“系統脈衝”來通知某一事件的發生。
訊息傳遞的方向與MsgDeliverEvent()
從一開始就提到,QNX的訊息傳遞是客戶、伺服器型的。也就是說,總是由客戶端向伺服器端傳送請求,等待被回覆的。但在現實情況中,客戶端與伺服器端並不是很容易區分開來的。有的伺服器端為了處理客戶端的請求,本身就需要向別的伺服器傳送訊息;有的客戶端需要從不同的伺服器那裡得到服務,而不能阻塞在某一特定的伺服器上;還有的時候,兩個程式間的資料是互相流動的,這應該怎麼辦呢?
也許有人認為,兩個程式互為通訊就可以了。每個程式都建立自己的頻道,然後都與對方的頻道建一個連線就好了;這樣,需要的時候,就可以直接通過連線向對方傳送訊息了。就好象管道(pipe)或是socketpair一樣。請注意,這種設計在QNX的訊息傳遞中是應該避免的。因為很容易就造成死鎖。一個常見的情形是這樣的。
程式A:MsgSend() 到程式B
程式B:MsgReceive()接收到訊息
程式B:處理訊息,然後MsgSend()給程式A
因為程式A正在阻塞狀態中,無法接收並處理B的請求;所以A會在STATE_REPLY裡,而B則會因MsgSend()而進入STATE_SEND,兩個程式就互為死鎖住了。當然,如果A和B都使用多執行緒,專門用一個執行緒來MsgReceive(),這個情形或許可以避免;但你要保證 MsgReceive()的執行緒不會去MsgSend(),否則一樣會死鎖。在程式簡單的時候或許你還有控制,如果程式變得複雜,又或者你寫的只是一個程式庫,別人怎麼來用你完全沒有控制,那麼最好還是不要用這種設計。
在QNX中,正確的方法是這樣的。
客戶端: 準備一個“通知事件”(Notification Event),並把這個事件用MsgSend()發給伺服器端,意思是:“如果xxx情況發生的話,請用這個事件通知我”。
伺服器: 收到這個訊息後,記錄下當時的rcvid,和傳過來的事件,然後應答“好的,知道了”。
客戶端: 因為有了伺服器的應答,客戶端不再阻塞,可以去做別的事
伺服器: 在某個時刻,客戶端所要求的“xxx情況”滿足了,伺服器呼叫 MsgDeliverEvent(rcvid, event);以通知客戶端
客戶端: 收到通知,再用MsgSend()發關“xxx 情況的資料在哪裡?”
伺服器: 用MsgReply()把資料返回給客戶端
具體的例子,可以參考MsgDeliverEvent()的文件說明。
路徑名(Path Name)
現在來回想一下我們最初的例子,客戶端與伺服器是怎樣取得連線的?客戶端需要伺服器的 nd, pid, chid,才能與伺服器正確地建立連線。在我們的例子裡,我們是讓伺服器顯示這幾個數,然後在客戶端的啟動時,通過命令列裡傳給客戶端。但是,在一個現實的系統裡,程式不斷地啟動、終止;伺服器與客戶端的起動過程也無法控制,這種方法顯然是行不通的。
QNX的解決辦法,是把“路徑名”與上述的“服務頻道”概念巧妙地結合起來。讓伺服器程式可以註冊一個路徑名,與服務頻道的nd, pid, chid關聯起來。這樣,客戶端就不需要知道伺服器的nd, pid, chid,而只要請求連線版務器路徑名就可以了。具體來說 name_attach()就是用來建立一個頻道,併為頻道註冊一個名字的;而name_open()則是用來連線註冊過的伺服器頻道;具體的例子,可以在name_attach()的文件裡找到,這裡就不再重複了。
相關文章
- vue---元件間傳遞訊息(父子傳遞訊息,兄弟傳遞訊息)Vue元件
- 學習在.NET Core中使用RabbitMQ進行訊息傳遞之持久化(二MQ持久化
- 深度學習與圖神經網路學習分享:訊息傳遞模式深度學習神經網路模式
- Chrome Extension 訊息傳遞Chrome
- Flutter中訊息傳遞Flutter
- Android之Handler訊息傳遞機制詳解Android
- Apache Kafka訊息傳遞策略ApacheKafka
- flutter 訊息傳遞機制Flutter
- Handler訊息傳遞機制
- Spring Boot 參考指南(訊息傳遞)Spring Boot
- SpringMVC之學習(2)值得接收和傳遞SpringMVC
- Laravel集合探學系列——高階訊息傳遞實現(二)Laravel
- Java中用Aeron實現UDP訊息傳遞JavaUDP
- Pulsar 入門實戰(1)--Pulsar 訊息傳遞
- NATS訊息傳遞與REST效能比較 | VinsguruREST
- Docker學習之搭建ActiveMQ訊息服務DockerMQ
- Flutter學習之Route跳轉及資料傳遞Flutter
- 深入學習js之——引數按值傳遞#9JS
- 兄弟元件之間資訊傳遞元件
- Shell學習【引數傳遞】
- .NET 8 中利用 MediatR 實現高效訊息傳遞
- 基於WebSocket的實時訊息傳遞設計Web
- RabbitMQ 和訊息傳遞常用一些術語MQ
- 訊息佇列之如何保證訊息的可靠傳輸佇列
- jmeter學習指南之深入分析跨域傳遞cookieJMeter跨域Cookie
- MFC六大核心機制之五、六:訊息對映和命令傳遞
- CCF CSP201903-4訊息傳遞介面(c++100)C++
- Android Handler訊息傳遞機制:圖文解析工作原理Android
- android 訊息傳遞機制進階EventBus的深入探究Android
- RabbitMQ學習(三)之 “訊息佇列高階使用”MQ佇列
- 語音通知簡訊 API:一種新型的資訊傳遞方式API
- redis學習(七) 訊息通知Redis
- 在ASP.NET Core 中使用 .NET Aspire 訊息傳遞元件ASP.NET元件
- 跨共識訊息格式XCM有幾種傳遞機制?
- Python程式專題8:分佈叢集的訊息傳遞Python
- Go 微服務:基於 RabbitMQ 和 AMQP 進行訊息傳遞Go微服務MQ
- Rabbitmq可靠訊息投遞,訊息確認機制MQ
- 【PWA學習與實踐】(7)使用Notification API來進行訊息提醒API