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()的文檔裏找到,這裏就不再重複了。

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