linux進程間傳遞描述符
每個進程都擁有自己獨立的進程空間,這使得描述符在進程之間的傳遞變得有點複雜,這個屬於進階處理序間通訊的內容,下面就來說說。
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 部分,介紹的很詳細。
好了準備工作已經做完了,下面就準備進入正題。
發送描述符
經過了前面的準備工作,是時候發送描述符了,先來看看函數原型:
int write_fd(int fd, void *ptr, int nbytes, int sendfd);
參數說明如下:
@fd :發送 TCP 通訊端介面;這個可以是使用socketpair返回的發送通訊端介面
@ptr :發送資料的緩衝區指標;
@nbytes :發送的位元組數;
@sendfd :向接收進程發送的描述符;
函數傳回值為寫入的位元組數, <0 說明發送失敗;
廢話少說,代碼先上,發送描述符的代碼相對簡單一些,說明見代碼內注釋。
先說明一下,舊的 Unix 系統使用的是 msg_accrights 域來傳遞描述符,因此我們需要使用宏HAVE_MSGHDR_MSG_CONTROL 以期能同時支援這兩種版本。
int write_fd(int fd, void *ptr, int nbytes, int sendfd) { struct msghdr msg; struct iovec iov[1]; // 有些系統使用的是舊的msg_accrights域來傳遞描述符,Linux下是新的msg_control欄位 #ifdef HAVE_MSGHDR_MSG_CONTROL union{ // 前面說過,保證cmsghdr和msg_control的對齊 struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; }control_un; struct cmsghdr *cmptr; // 設定輔助緩衝區和長度 msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); // 只需要一組附屬資料就夠了,直接通過CMSG_FIRSTHDR取得 cmptr = CMSG_FIRSTHDR(&msg); // 設定必要的欄位,資料和長度 cmptr->cmsg_len = CMSG_LEN(sizeof(int)); // fd類型是int,設定長度 cmptr->cmsg_level = SOL_SOCKET; cmptr->cmsg_type = SCM_RIGHTS; // 指明發送的是描述符 *((int*)CMSG_DATA(cmptr)) = sendfd; // 把fd寫入輔助資料中 #else msg.msg_accrights = (caddr_t)&sendfd; // 這箇舊的更方便啊 msg.msg_accrightslen = sizeof(int); #endif // UDP才需要,無視 msg.msg_name = NULL; msg.msg_namelen = 0; // 別忘了設定資料緩衝區,實際上1個位元組就夠了 iov[0].iov_base = ptr; iov[0].iov_len = nbytes; msg.msg_iov = iov; msg.msg_iovlen = 1; return sendmsg(fd, &msg, 0); }
接收描述符
發送方準備好之後,接收方準備接收,函數原型為:
int read_fd(int fd, void *ptr, int nbytes, int *recvfd);
參數說明如下:
@fd :接收 TCP 通訊端介面; 這個可以是使用 socketpair返回的接收通訊端介面
@ptr :接收資料的緩衝區指標;
@nbytes :接收緩衝區大小;
@recvfd :用來接收發送進程發送來的描述符;
函數傳回值為讀取的位元組數, <0 說明讀取失敗;
接收函數代碼如下,相比發送要複雜一些。
int read_fd(int fd, void *ptr, int nbytes, int *recvfd) { struct msghdr msg; struct iovec iov[1]; int n; int newfd; #ifdef HAVE_MSGHDR_MSG_CONTROL union{ // 對齊 struct cmsghdr cm; char control[CMSG_SPACE(sizeof(int))]; }control_un; struct cmsghdr *cmptr; // 設定輔助資料緩衝區和長度 msg.msg_control = control_un.control; msg.msg_controllen = sizeof(control_un.control); #else msg.msg_accrights = (caddr_t) &newfd; // 這個簡單 msg.msg_accrightslen = sizeof(int); #endif // TCP無視 msg.msg_name = NULL; msg.msg_namelen = 0; // 設定資料緩衝區 iov[0].iov_base = ptr; iov[0].iov_len = nbytes; msg.msg_iov = iov; msg.msg_iovlen = 1; // 設定結束,準備接收 if((n = recvmsg(fd, &msg, 0)) <= 0) { return n; } #ifdef HAVE_MSGHDR_MSG_CONTROL // 檢查是否收到了輔助資料,以及長度,回憶上一節的CMSG宏 cmptr = CMSG_FIRSTHDR(&msg); if((cmptr != NULL) && (cmptr->cmsg_len == CMSG_LEN(sizeof(int)))) { // 還是必要的檢查 if(cmptr->cmsg_level != SOL_SOCKET) { printf("control level != SOL_SOCKET/n"); exit(-1); } if(cmptr->cmsg_type != SCM_RIGHTS) { printf("control type != SCM_RIGHTS/n"); exit(-1); } // 好了,描述符在這 *recvfd = *((int*)CMSG_DATA(cmptr)); } else { if(cmptr == NULL) printf("null cmptr, fd not passed./n"); else printf("message len[%d] if incorrect./n", cmptr->cmsg_len); *recvfd = -1; // descriptor was not passed } #else if(msg.msg_accrightslen == sizeof(int)) *recvfd = newfd; else *recvfd = -1; #endif return n; }
發送和接收函數就這麼多,就像上面看到的,進程間傳遞通訊端還是有點麻煩的。
原文地址:http://blog.csdn.net/sparkliang/article/details/5486069(文章有改動)