1. 程式人生 > >高階套介面-(sendmsg和recvmsg)

高階套介面-(sendmsg和recvmsg)

程序間傳遞描述符一

每個程序都擁有自己獨立的程序空間,這使得描述符在程序之間的傳遞變得有點複雜,這個屬於高階程序間通訊的內容,下面就來說說。順便把 Linux 和 Windows 平臺都講講。

Linux 下的描述符傳遞

Linux 系統系下,子程序會自動繼承父程序已開啟的描述符,實際應用中,可能父程序需要向子程序傳遞“後開啟的描述符”,或者子程序需要向父程序傳遞;或者兩個程序可能是無關的,顯然這需要一套傳遞機制。

簡單的說,首先需要在這兩個程序之間建立一個 Unix 域套接字介面作為訊息傳遞的通道( Linux 系統上使用socketpair 函式可以很方面便的建立起傳遞通道),然後傳送程序呼叫 sendmsg 向通道傳送一個特殊的訊息,核心將對這個訊息做特殊處理,從而將開啟的描述符傳遞到接收程序。

然後接收方呼叫 recvmsg 從通道接收訊息,從而得到開啟的描述符。然而實際操作起來並不像看起來那樣單純。

先來看幾個注意點:

1 需要注意的是傳遞描述符並不是傳遞一個 int 型的描述符編號,而是在接收程序中建立一個新的描述符,並且在核心的檔案表中,它與傳送程序傳送的描述符指向相同的項。

2 在程序之間可以傳遞任意型別的描述符,比如可以是 pipe , open , mkfifo 或 socket , accept 等函式返回的描述符,而不限於套接字。

3 一個描述符在傳遞過程中(從呼叫 sendmsg 傳送到呼叫 recvmsg 接收),核心會將其標記為“在飛行中”( in flight )。在這段時間內,即使傳送方試圖關閉該描述符,核心仍會為接收程序保持開啟狀態。傳送描述符會使其引用計數加 1 。

4 描述符是通過輔助資料傳送的(結構體 msghdr 的 msg_control 成員),在傳送和接收描述符時,總是傳送至少1 個位元組的資料,即使這個資料沒有任何實際意義。否則當接收返回 0 時,接收方將不能區分這意味著“沒有資料”(但輔助資料可能有套接字)還是“檔案結束符”。

5 具體實現時, msghdr 的 msg_control 緩衝區必須與 cmghdr 結構對齊,可以看到後面程式碼的實現使用了一個union 結構來保證這一點。

msghdr 和 cmsghdr 結構體

上面說過,描述符是通過結構體 msghdr 的 msg_control 成員送的,因此在繼續向下進行之前,有必要了解一下msghdr 和 cmsghdr 結構體,先來看看 msghdr 。

  1. struct msghdr {  
  2.     void       *msg_name;  
  3.     socklen_t    msg_namelen;  
  4.     struct iovec  *msg_iov;  
  5.     size_t       msg_iovlen;  
  6.     void       *msg_control;  
  7.     size_t       msg_controllen;  
  8.     int          msg_flags;  
  9. };   

結構成員可以分為下面的四組,這樣看起來就清晰多了:

1 套介面地址成員 msg_name 與 msg_namelen ;

只有當通道是資料報套介面時才需要; msg_name 指向要傳送或是接收資訊的套介面地址。 msg_namelen 指明瞭這個套介面地址的長度。

msg_name 在呼叫 recvmsg 時指向接收地址,在呼叫 sendmsg 時指向目的地址。注意, msg_name 定義為一個(void *) 資料型別,因此並不需要將套介面地址顯示轉換為 (struct sockaddr *) 。

2 I/O 向量引用 msg_iov 與 msg_iovlen

它是實際的資料緩衝區,從下面的程式碼能看到,我們的 1 個位元組就交給了它;這個 msg_iovlen 是 msg_iov 的個數,不是什麼長度。

msg_iov 成員指向一個 struct iovec 陣列, iovc 結構體在 sys/uio.h 標頭檔案定義,它沒有什麼特別的。

  1. struct iovec {  
  2.      ptr_t iov_base; /* Starting address */  
  3.      size_t iov_len; /* Length in bytes */  
  4. };  

有了 iovec ,就可以使用 readv 和 writev 函式在一次函式呼叫中讀取或是寫入多個緩衝區,顯然比多次 read ,write 更有效率。 readv 和 writev 的函式原型如下:

  1. #include <sys/uio.h>  
  2. int readv(int fd, const struct iovec *vector, int count);  
  3. int writev(int fd, const struct iovec *vector, int count);  

3 附屬資料緩衝區成員 msg_control 與 msg_controllen ,描述符就是通過它傳送的,後面將會看到, msg_control指向附屬資料緩衝區,而 msg_controllen 指明瞭緩衝區大小。

4 接收資訊標記位 msg_flags ;忽略

輪到 cmsghdr 結構了,附屬資訊可以包括若干個單獨的附屬資料物件。在每一個物件之前都有一個 struct cmsghdr結構。頭部之後是填充位元組,然後是物件本身。最後,附屬資料物件之後,下一個 cmsghdr 之前也許要有更多的填充位元組。

  1. struct cmsghdr {  
  2.     socklen_t cmsg_len;  
  3.     int       cmsg_level;  
  4.     int       cmsg_type;  
  5.     /* u_char     cmsg_data[]; */  
  6. };  

cmsg_len   附屬資料的位元組數,這包含結構頭的尺寸,這個值是由 CMSG_LEN() 巨集計算的;

cmsg_level  表明了原始的協議級別 ( 例如, SOL_SOCKET) ;

cmsg_type  表明了控制資訊型別 ( 例如, SCM_RIGHTS ,附屬資料物件是檔案描述符; SCM_CREDENTIALS ,附屬資料物件是一個包含證書資訊的結構 ) ;

被註釋的 cmsg_data 用來指明實際的附屬資料的位置,幫助理解。

對於 cmsg_level 和 cmsg_type ,當下我們只關心 SOL_SOCKET 和 SCM_RIGHTS 。

msghdr 和 cmsghdr 輔助巨集

這些結構還是挺複雜的, Linux 系統提供了一系列的巨集來簡化我們的工作,這些巨集可以在不同的 UNIX 平臺之間進行移植。這些巨集是由 cmsg(3) 的 man 手冊頁描述的,先來認識一下:

#include <sys/socket.h>

struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);

struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);

size_t CMSG_ALIGN(size_t length);

size_t CMSG_SPACE(size_t length);

size_t CMSG_LEN(size_t length);

void *CMSG_DATA(struct cmsghdr *cmsg);

CMSG_LEN() 巨集

輸入引數:附屬資料緩衝區中的物件大小;

計算 cmsghdr 頭結構加上附屬資料大小,包括必要的對其欄位,這個值用來設定 cmsghdr 物件的 cmsg_len 成員。

CMSG_SPACE() 巨集

輸入引數:附屬資料緩衝區中的物件大小;

計算 cmsghdr 頭結構加上附屬資料大小,幷包括對其欄位和可能的結尾填充字元,注意 CMSG_LEN() 值並不包括可能的結尾填充字元。 CMSG_SPACE() 巨集對於確定所需的緩衝區尺寸是十分有用的。

注意如果在緩衝區中有多個附屬資料,一定要同時新增多個 CMSG_SPACE() 巨集呼叫來得到所需的總空間。

下面的例子反映了二者的區別:

  1. printf("CMSG_SPACE(sizeof(short))=%d/n", CMSG_SPACE(sizeof(short))); // 返回16  
  2. printf("CMSG_LEN(sizeof(short))=%d/n", CMSG_LEN(sizeof(short))); // 返回14  

CMSG_DATA() 巨集

輸入引數:指向 cmsghdr 結構的指標 ;

返回跟隨在頭部以及填充位元組之後的附屬資料的第一個位元組 ( 如果存在 ) 的地址,比如傳遞描述符時,程式碼將是如下的形式:

  1. struct cmsgptr *cmptr;  
  2. . . .  
  3. int fd = *(int *)CMSG_DATA(cmptr); // 傳送:*(int *)CMSG_DATA(cmptr) = fd;  

CMSG_FIRSTHDR() 巨集

輸入引數:指向 struct msghdr 結構的指標;

返回指向附屬資料緩衝區內的第一個附屬物件的 struct cmsghdr 指標。如果不存在附屬資料物件則返回的指標值為NULL 。

CMSG_NXTHDR() 巨集

輸入引數:指向 struct msghdr 結構的指標,指向當前 struct cmsghdr 的指標;

這個用於返回下一個附屬資料物件的 struct cmsghdr 指標,如果沒有下一個附屬資料物件,這個巨集就會返回 NULL。

通過這兩個巨集可以很容易遍歷所有的附屬資料,像下面的形式:

  1. struct msghdr msgh;  
  2. struct cmsghdr *cmsg;  
  3. for (cmsg = CMSG_FIRSTHDR(&msgh); cmsg != NULL;  
  4.     cmsg = CMSG_NXTHDR(&msgh,cmsg) {  
  5.     // 得到了cmmsg,就能通過CMSG_DATA()巨集取得輔助資料了   

函式 sendmsg 和 recvmsg

函式原型如下:

  1. #include <sys/types.h>  
  2. #include <sys/socket.h>  
  3. int sendmsg(int s, const struct msghdr *msg, unsigned int flags);  
  4. int recvmsg(int s, struct msghdr *msg, unsigned int flags);  

二者的引數說明如下:

s, 套接字通道,對於 sendmsg 是傳送套接字,對於 recvmsg 則對應於接收套接字;

msg ,資訊頭結構指標;

flags , 可選的標記位, 這與 send 或是 sendto 函式呼叫的標記相同。

函式的返回值為實際傳送 / 接收的位元組數。否則返回 -1 表明發生了錯誤。

具體參考 APUE 的高階 I/O 部分,介紹的很詳細。

好了準備工作已經做完了,下面就準備進入正題。