高階套介面-(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 。
- struct msghdr {
- void *msg_name;
- socklen_t msg_namelen;
- struct iovec *msg_iov;
- size_t msg_iovlen;
- void *msg_control;
- size_t msg_controllen;
- int msg_flags;
- };
結構成員可以分為下面的四組,這樣看起來就清晰多了:
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 標頭檔案定義,它沒有什麼特別的。
- struct iovec {
- ptr_t iov_base; /* Starting address */
- size_t iov_len; /* Length in bytes */
- };
有了 iovec ,就可以使用 readv 和 writev 函式在一次函式呼叫中讀取或是寫入多個緩衝區,顯然比多次 read ,write 更有效率。 readv 和 writev 的函式原型如下:
- #include <sys/uio.h>
- int readv(int fd, const struct iovec *vector, int count);
- 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 之前也許要有更多的填充位元組。
- struct cmsghdr {
- socklen_t cmsg_len;
- int cmsg_level;
- int cmsg_type;
- /* u_char cmsg_data[]; */
- };
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() 巨集呼叫來得到所需的總空間。
下面的例子反映了二者的區別:
- printf("CMSG_SPACE(sizeof(short))=%d/n", CMSG_SPACE(sizeof(short))); // 返回16
- printf("CMSG_LEN(sizeof(short))=%d/n", CMSG_LEN(sizeof(short))); // 返回14
CMSG_DATA() 巨集
輸入引數:指向 cmsghdr 結構的指標 ;
返回跟隨在頭部以及填充位元組之後的附屬資料的第一個位元組 ( 如果存在 ) 的地址,比如傳遞描述符時,程式碼將是如下的形式:
- struct cmsgptr *cmptr;
- . . .
- 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。
通過這兩個巨集可以很容易遍歷所有的附屬資料,像下面的形式:
- struct msghdr msgh;
- struct cmsghdr *cmsg;
- for (cmsg = CMSG_FIRSTHDR(&msgh); cmsg != NULL;
- cmsg = CMSG_NXTHDR(&msgh,cmsg) {
- // 得到了cmmsg,就能通過CMSG_DATA()巨集取得輔助資料了
函式 sendmsg 和 recvmsg
函式原型如下:
- #include <sys/types.h>
- #include <sys/socket.h>
- int sendmsg(int s, const struct msghdr *msg, unsigned int flags);
- int recvmsg(int s, struct msghdr *msg, unsigned int flags);
二者的引數說明如下:
s, 套接字通道,對於 sendmsg 是傳送套接字,對於 recvmsg 則對應於接收套接字;
msg ,資訊頭結構指標;
flags , 可選的標記位, 這與 send 或是 sendto 函式呼叫的標記相同。
函式的返回值為實際傳送 / 接收的位元組數。否則返回 -1 表明發生了錯誤。
具體參考 APUE 的高階 I/O 部分,介紹的很詳細。
好了準備工作已經做完了,下面就準備進入正題。