linux進程間傳遞描述符

來源:互聯網
上載者:User
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(文章有改動)

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.