博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

进程间传递socket描述符

Posted on 2016-02-14 15:03  bw_0927  阅读(2381)  评论(1编辑  收藏  举报

http://www.cnblogs.com/nufangrensheng/p/3571370.html

 

 

Linux系统中提供了一种特殊的方法,可以从一个进程中将一个已经打开的文件描述符传递给其他的任何进程。其基本过程如下:

 

(1)创建一个字节流或者数据报的UNIX域套接字

 

如果目标是fork()一个子进程,让子进程打开描述符并将它返回给父进程,那么父进程可以用socketpair()创建一个流管道,用它来传递描述字。

 

如果进程之间没有亲缘关系,那么服务器必须创建一个UNIX域字节流套接字,绑定一个路径名,让客户连接到这个套接字。然后客户端可以向服务器发送一个请求以打开某个描述字,服务器将描述符通过UNIX域套接字传回。

 

(2)进程可以用任何返回描述符的UNIX函数打开一个描述符:例如open()、pipe()、mkfifo()、socket()或者accept()。可以在进程间传递任何类型的描述符。

 

(3)发送进程建立一个msghdr结构,其中包含要传递的描述符。在POSIX中说明该描述符作为辅助数据发送,但老的实现使用msg_accright成员。发送进程调用sendmsg()通过第一部得到的UNIX域套接字发出套接字。这时这个描述符是在飞行中的。即使在发送进程调用sendmsg()之后,但在接受进程调用recvmsg()之前将描述符关闭,它仍会为接收进程保持打开状态。描述符的发送导致它的访问统计数加1。

 

(4)接收进程调用recvmsg()在UNIX域套接字上接收套接字。通常接收进程收到的描述符的编号和发送进程中的描述符的编号不同,但这没有问题。传递描述符不是传递描述符的编号,而是在接收进程中建立一个新的描述符,指向内核的文件表中与发送进程发送的描述符相同的项。

 

 

 

在技术上,发送进程实际上向接收进程传送一个指向一打开文件表项的指针。该指针被分配存放在接收进程的第一个可用描述符项中。(注意,不要造成错觉,以为发送进程和接收进程中的描述符编号是相同的,通常它们是不同的。)两个进程共享同一打开文件表项,在这一点上与fork之后,父、子进程共享打开文件表项的情况完全相同。

当发送进程将描述符传送给接收进程后,通常它关闭该描述符。发送进程关闭该描述符并不造成关闭该文件或设备,其原因是该描述符对应的文件仍被视为由接收者进程打开(即使接收进程尚未接收到该描述符)。

 

http://www.pureage.info/2015/03/19/passing-file-descriptors.html

====================================================

首先,必须声明,“进程间传递文件描述符”这个说法是错误的。

在处理文件时,内核空间和用户空间使用的主要对象是不同的。对用户程序来说,一个文件由一个文件描述符标识。该描述符是一个整数,在所有有关文件的操作中用作标识文件的参数。文件描述符是在打开文件时由内核分配,只在一个进程内部有效。两个不同进程可以使用同样的文件描述符,但二者并不指向同一个文件。基于同一个描述符来共享文件是不可能的。

《深入理解Linux内核架构》

这里说的“进程间传递文件描述符”是指,A进程打开文件fileA,获得文件描述符为fdA,现在A进程要通过某种方法,根据fdA,使得另一个进程B,获得一个新的文件描述符fdB,这个fdB在进程B中的作用,跟fdA在进程A中的作用一样。即在fdB上的操作,即是对fileA的操作。

这看似不可能的操作,是怎么进行的呢?

案是使用匿名Unix域套接字,即socketpair()和sendmsg/recvmsg来实现。

关于socketpair

UNIX domain sockets provide both stream and datagram interfaces. The UNIX
domain datagram service is reliable, however. Messages are neither lost nor delivered
out of order. UNIX domain sockets are like a cross between sockets and pipes. You can
use the network-oriented socket interfaces with them, or you can use the socketpair
function to create a pair of unnamed, connected, UNIX domain sockets.

APUE 3rd edition,17.2

socketpair的原型为:

#include <sys/types.h>
#include <sys/socket.h>

int socketpair(int d, int type, int protocol, int sv[2]);

传入的参数sv为一个整型数组,有两个元素。当调用成功后,这个数组的两个元素即为2个文件描述符。

一对连接起来的Unix匿名域套接字就建立起来了,它们就像一个全双工的管道,每一端都既可读也可写。

socket_pair.jpg

即,往sv[

 

关于sendmsg/recvmsg

通过socket发送数据,主要有三组系统调用,分别是

  1. send/recv(与write/read类似,面向连接的)
  2. sendto/recvfrom(sendto与send的差别在于,sendto可以面向无连接,recvfrom与recv的区别是,recvfrom可以获取sender方的地址)
  3. sendmsg/recvmsg. 通过sendmsg,可以用msghdr参数,来指定多个缓冲区来发送数据,与writev系统调用类似

sendmsg函数原型如下:

#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

其中,根据POSIX.1 msghdr的定义至少应该包含下面几个成员:

struct msghdr {
    void *msg_name; /* optional address */
    socklen_t msg_namelen; /* address size in bytes */
    struct iovec *msg_iov; /* array of I/O buffers */
    int msg_iovlen; /* number of elements in array */
    void *msg_control; /* ancillary data */
    socklen_t msg_controllen; /* number of ancillary bytes */
    int msg_flags; /* flags for received message */
};

在Linux的manual page中,msghdr的定义为:

struct msghdr {
    void         *msg_name;       /* optional address */
    socklen_t     msg_namelen;    /* size of address */
    struct iovec *msg_iov;        /* scatter/gather array */
    size_t        msg_iovlen;     /* # elements in msg_iov */
    void         *msg_control;    /* ancillary data, see below */
    socklen_t     msg_controllen; /* ancillary data buffer len */
    int           msg_flags;      /* flags on received message */
};

查看Linux内核源代码(3.18.1),可知msghdr的准确定义为:

struct msghdr {
    void        *msg_name;  /* ptr to socket address structure */
    int     msg_namelen;    /* size of socket address structure */
    struct iovec    *msg_iov;   /* scatter/gather array */
    __kernel_size_t msg_iovlen; /* # elements in msg_iov */
    void        *msg_control;   /* ancillary data */
    __kernel_size_t msg_controllen; /* ancillary data buffer length */
    unsigned int    msg_flags;  /* flags on received message */
};

可见,与Manual paga中的描述一致。

其中,前两个成员msg_name和msg_namelen是用来在发送datagram时,指定目的地址的。如果是面向连接的,这两个成员变量可以不用。

接下来的两个成员,msg_iov和msg_iovlen,则是用来指定发送缓冲区数组的。其中,msg_iovlen是iovec类型的元素的个数。每一个缓冲区的起始地址和大小由iovec类型自包含,iovec的定义为:

struct iovec {
    void *iov_base;   /* Starting address */
    size_t iov_len;   /* Number of bytes */
};

成员msg_flags用来描述接受到的消息的性质,由调用recvmsg时传入的flags参数设置。recvmsg的函数原型为:

#include <sys/socket.h>
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

与sendmsg相对应,recvmsg用msghdr结构指定多个缓冲区来存放读取到的结果。flags参数用来修改recvmsg的默认行为。传入的flags参数在调用完recvmsg后,会被设置到msg所指向的msghdr类型的msg_flags变量中。flags可以为如下值:
flags_in_msghdr.jpg

回来继续讲sendmsg和msghdr结构。

msghdr结构中剩下的两个成员,msg_control和msg_contorllen,是用来发送或接收控制信息的。其中,msg_control指向一个cmsghdr的结构体,msg_controllen成员是控制信息所占用的字节数。

注意,msg_controllen与前面的msg_iovlen不同,msg_iovlen是指的由成员msg_iov所指向的iovec型的数组的元素个数,而msg_controllen,则是所有控制信息所占用的总的字节数。

其实,msg_control也可能是个数组,但msg_controllen并不是该cmsghdr类型的数组的元素的个数。在Manual page中,关于msg_controllen有这么一段描述:

To create ancillary data, first initialize the msg_controllen member of the msghdr with the length of the control message buffer. Use CMSG_FIRSTHDR() on the msghdr to get the first control message and CMSG_NEXTHDR to get all subsequent ones. In each control message, initialize cmsg_len (with CMSG_LEN), the other cmsghdr header fields, and the data portion using CMSG_DATA. Finally, the msg_controllen field of the msghdr should be set to the sum of the CMSG_SPACE() of the length of all control messages in the buffer.

在Linux 的Manual page(man cmsg)中,cmsghdr的定义为:

struct cmsghdr {
    socklen_t   cmsg_len;   /* data byte count, including header */
    int         cmsg_level; /* originating protocol */
    int         cmsg_type;  /* protocol-specific type */
    /* followed by  unsigned char   cmsg_data[]; */
};

注意,控制信息的数据部分,是直接存储在cmsg_type之后的。但中间可能有一些由于对齐产生的填充字节,由于这些填充数据的存在,对于这些控制数据的访问,必须使用Linux提供的一些专用宏来完成。这些宏包括如下几个:

#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);
unsigned char *CMSG_DATA(struct cmsghdr *cmsg);

其中:

CMSG_FIRSTHDR()返回msgh所指向的msghdr类型的缓冲区中的第一个cmsghdr结构体的指针。

CMSG_NXTHDR()返回传入的cmsghdr类型的指针的下一个cmsghdr结构体的指针。

CMSG_ALIGN()根据传入的length大小,返回一个包含了添加对齐作用的填充数据后的大小。

CMSG_SPACE()中传入的参数length指的是一个控制信息元素(即一个cmsghdr结构体)后面数据部分的字节数,返回的是这个控制信息的总的字节数,即包含了头部(即cmsghdr各成员)、数据部分和填充数据的总和

CMSG_DATA根据传入的cmsghdr指针参数,返回其后面数据部分的指针。

CMSG_LEN传入的参数是一个控制信息中的数据部分的大小,返回的是这个根据这个数据部分大小,需要配置的cmsghdr结构体中cmsg_len成员的值。这个大小将为对齐添加的填充数据也包含在内。

用一张图来表示这几个变量和宏的关系为:
cmsghdr.jpg

如前所述,msghdr结构中,msg_controllen成员的大小为所有cmsghdr控制元素调用CMSG_SPACE()后相加的和

讲了这么多msghdr,cmsghdr,还是没有讲到如何传递文件描述符。其实很简单,本来sendmsg是和send一样,是用来传送数据的,只不过其数据部分的buffer由参数msg_iov来指定,至此,其行为和send可以说是类似的。

但是sendmsg提供了可以传递控制信息的功能,我们要实现的传递描述符这一功能,就必须要用到这个控制信息。在msghdr变量的cmsghdr成员中,由控制头cmsg_level和cmsg_type来设置"传递文件描述符"这一属性,并将要传递的文件描述符作为数据部分,保存在cmsghdr变量的后面。这样就可以实现传递文件描述符这一功能,在此时,是不需要使用msg_iov来传递数据的。

具体的说,为msghdr的成员msg_control分配一个cmsghdr的空间,将该cmsghdr结构的cmsg_level设置为SOL_SOCKET,cmsg_type设置为SCM_RIGHTS,并将要传递的文件描述符作为数据部分,调用sendmsg即可。其中,SCM表示socket-level control message,SCM_RIGHTS表示我们要传递访问权限。

弄清楚了发送部分,文件描述符的接收部分就好说了。跟发送部分一样,为控制信息配置好属性,并在其后分配一个文件描述符的数据部分后,在成功调用recvmsg后,控制信息的数据部分就是在接收进程中的新的文件描述符了,接收进程可直接对该文件描述符进行操作。

Nginx中传递文件描述符的代码实现

关于如何在进程间传递文件描述符,我们已经理的差不多了。下面看看Nginx中是如何做的。

Nginx中发送文件描述符的相关代码为:

ngx_int_t
ngx_write_channel(ngx_socket_t s, ngx_channel_t *ch, size_t size,
    ngx_log_t *log)
{
    ssize_t             n;
    ngx_err_t           err;
    struct iovec        iov[1];
    struct msghdr       msg;

#if (NGX_HAVE_MSGHDR_MSG_CONTROL)

    union {
        struct cmsghdr  cm;                  msghdr 的 msg_control 缓冲区必须与 cmghdr 结构对齐
        char            space[CMSG_SPACE(sizeof(int))];
    } cmsg;

    if (ch->fd == -1) {
        msg.msg_control = NULL;
        msg.msg_controllen = 0;

    } else {
        msg.msg_control = (caddr_t) &cmsg;
        msg.msg_controllen = sizeof(cmsg);

        ngx_memzero(&cmsg, sizeof(cmsg));

        cmsg.cm.cmsg_len = CMSG_LEN(sizeof(int));
        cmsg.cm.cmsg_level = SOL_SOCKET;
        cmsg.cm.cmsg_type = SCM_RIGHTS;

        /*
         * We have to use ngx_memcpy() instead of simple
         *   *(int *) CMSG_DATA(&cmsg.cm) = ch->fd;
         * because some gcc 4.4 with -O2/3/s optimization issues the warning:
         *   dereferencing type-punned pointer will break strict-aliasing rules
         *
         * Fortunately, gcc with -O1 compiles this ngx_memcpy()
         * in the same simple assignment as in the code above
         */

        ngx_memcpy(CMSG_DATA(&cmsg.cm), &ch->fd, sizeof(int));
    }

    msg.msg_flags = 0;

#else

    if (ch->fd == -1) {
        msg.msg_accrights = NULL;
        msg.msg_accrightslen = 0;

    } else {
        msg.msg_accrights = (caddr_t) &ch->fd;
        msg.msg_accrightslen = sizeof(int);
    }

#endif

    iov[0].iov_base = (char *) ch;
    iov[0].iov_len = size;

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    n = sendmsg(s, &msg, 0);

    if (n == -1) {
        err = ngx_errno;
        if (err == NGX_EAGAIN) {
            return NGX_AGAIN;
        }

        ngx_log_error(NGX_LOG_ALERT, log, err, "sendmsg() failed");
        return NGX_ERROR;
    }

    return NGX_OK;
}

其中,参数s就是一个用socketpair创建的管道的一端,要传送的文件描述符位于参数ch所指向的结构体中。ch结构体本身,包含要传送的文件描述符和其他成员,则通过io_vec类型的成员msg_iov传送。

接收部分的代码为:

ngx_int_t
ngx_read_channel(ngx_socket_t s, ngx_channel_t *ch, size_t size, ngx_log_t *log)
{
    ssize_t             n;
    ngx_err_t           err;
    struct iovec        iov[1];
    struct msghdr       msg;

#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
    union {
        struct cmsghdr  cm;
        char            space[CMSG_SPACE(sizeof(int))];
    } cmsg;
#else
    int                 fd;
#endif

    iov[0].iov_base = (char *) ch;
    iov[0].iov_len = size;

    msg.msg_name = NULL;
    msg.msg_namelen = 0;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

#if (NGX_HAVE_MSGHDR_MSG_CONTROL)
    msg.msg_control = (caddr_t) &cmsg;
    msg.msg_controllen = sizeof(cmsg);
#else
    msg.msg_accrights = (caddr_t) &fd;
    msg.msg_accrightslen = sizeof(int);
#endif

    n = recvmsg(s, &msg, 0);

    if (n == -1) {
        err = ngx_errno;
        if (err == NGX_EAGAIN) {
            return NGX_AGAIN;
        }

        ngx_log_error(NGX_LOG_ALERT, log, err, "recvmsg() failed");
        return NGX_ERROR;
    }

    if (n == 0) {
        ngx_log_debug0(NGX_LOG_DEBUG_CORE, log, 0, "recvmsg() returned zero");
        return NGX_ERROR;
    }

    if ((size_t) n < sizeof(ngx_channel_t)) {
        ngx_log_error(NGX_LOG_ALERT, log, 0,
                      "recvmsg() returned not enough data: %z", n);
        return NGX_ERROR;
    }

#if (NGX_HAVE_MSGHDR_MSG_CONTROL)

    if (ch->command == NGX_CMD_OPEN_CHANNEL) {

        if (cmsg.cm.cmsg_len < (socklen_t) CMSG_LEN(sizeof(int))) {
            ngx_log_error(NGX_LOG_ALERT, log, 0,
                          "recvmsg() returned too small ancillary data");
            return NGX_ERROR;
        }

        if (cmsg.cm.cmsg_level != SOL_SOCKET || cmsg.cm.cmsg_type != SCM_RIGHTS)
        {
            ngx_log_error(NGX_LOG_ALERT, log, 0,
                          "recvmsg() returned invalid ancillary data "
                          "level %d or type %d",
                          cmsg.cm.cmsg_level, cmsg.cm.cmsg_type);
            return NGX_ERROR;
        }

        /* ch->fd = *(int *) CMSG_DATA(&cmsg.cm); */

        ngx_memcpy(&ch->fd, CMSG_DATA(&cmsg.cm), sizeof(int));
    }

    if (msg.msg_flags & (MSG_TRUNC|MSG_CTRUNC)) {
        ngx_log_error(NGX_LOG_ALERT, log, 0,
                      "recvmsg() truncated data");
    }

#else

    if (ch->command == NGX_CMD_OPEN_CHANNEL) {
        if (msg.msg_accrightslen != sizeof(int)) {
            ngx_log_error(NGX_LOG_ALERT, log, 0,
                          "recvmsg() returned no ancillary data");
            return NGX_ERROR;
        }

        ch->fd = fd;
    }

#endif

    return n;
}

该代码配合发送部分的代码来读,意义很明确。只不过,在我们上面所讲的基础上,Nginx将ch变量作为发送和接收的数据(此数据指放在iovec缓冲区中的数据,而非控制信息中的数据部分),并用一个成员ch->command实现了一个简单的协议,使得这一对函数功能更通用。

 

===========================================

http://blog.sina.com.cn/s/blog_4ed630e801000be4.html

我们知道父进程在子进程被fork出来之前打开的文件描述符是能被子进程继承下来的,但是一旦子进程已经创建后,父进程打开的文件描述符要怎样才能传递给子进程呢?Unix提供相应的技术来满足这一需求,这就是同一台主机上进程间的文件描述符传递,很美妙而且强大的技术。

想象一下我们试图实现一个服务器,接收多个客户端的连接,我们欲采用多个子进程并发的形式来处理多客户端的同时连接,这时候我们可能有两种想法:
1、客户端每建立一条连接,我们fork出一个子进程负责处理该连接;
2、预先创建一个进程池,客户端每建立一条链接,服务器就从该池中选出一个空闲(Idle)子进程来处理该连接。
后者显然更高效,因为减少了子进程创建的性能损耗,反应的及时性大大增强。这里恰恰就出现了我们前面提到的问题,所有子进程都是在服务器Listen到一条连接以前就已经fork出来了,也就是说新的连接描述符子进程是不知道的,需要父进程传递给它,它接收到相应的连接描述符后,才能与相应的客户端进行通信处理。这里我们就可以使用'传递文件描述符'的方式来实现。

在'UNIX网络编程第1卷'的14.7小节中对这种技术有详细的阐述,实际上这种技术就是利用sendmsg和recvmsg在一定的UNIX域套接口(或者是某种管道)上发送和接收一种特殊的消息,这种消息可以承载'文件描述符'罢了,当然操作系统内核对这种消息作了特殊的处理。在具体一点儿'文件描述符'是作为辅助数据(Ancillary Data)通过msghdr结构中的成员msg_control(老版本中称为msg_accrights)发送和接收的。值得一提的是发送进程在将'文件描述符'发送出去后,即使立即关闭该文件描述符,该文件描述符对应的文件设备也没有被真正的关闭,其引用计数仍然大于一,直到接收进程成功接收后,再关闭该文件描述符,如果这时文件设备的引用计数为0,那么才真正关闭该文件设备。

OK,下面是一个简单的文件描述符传递的例子,该例子实现这样一个功能:即子进程负责在父进程传递给它的文件描述符对应的文件尾加上特定的'LOGO'字符串。例子环境为Solaris 9 + GCC 3.2

 

/* test_fdpass.c */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>

#include <sys/socket.h> /* for socketpair */

#define MY_LOGO         "-- Tony Bai"

static int send_fd(int fd, int fd_to_send)
{
        struct iovec    iov[1];
        struct msghdr   msg;
        char            buf[1];

        if (fd_to_send >= 0) {
                msg.msg_accrights       = (caddr_t)&fd_to_send;
                msg.msg_accrightslen    = sizeof(int);
        } else {
                msg.msg_accrights       = (caddr_t)NULL;
                msg.msg_accrightslen    = 0;
        }

        msg.msg_name    = NULL;
        msg.msg_namelen = 0;

        iov[0].iov_base = buf;
        iov[0].iov_len  = 1;
        msg.msg_iov     = iov;
        msg.msg_iovlen  = 1;

        if(sendmsg(fd, &msg, 0) < 0) {
                printf("sendmsg error, errno is %d\n", errno);
                return errno;
        }

        return 0;
}

static int recv_fd(int fd, int *fd_to_recv)
{
        struct iovec    iov[1];
        struct msghdr   msg;
        char            buf[1];

        msg.msg_accrights       = (caddr_t)fd_to_recv;
        msg.msg_accrightslen    = sizeof(int);

        msg.msg_name    = NULL;
        msg.msg_namelen = 0;

        iov[0].iov_base = buf;
        iov[0].iov_len  = 1;
        msg.msg_iov     = iov;
        msg.msg_iovlen  = 1;

        if (recvmsg(fd, &msg, 0) < 0) {
                return errno;
        }

        if(msg.msg_accrightslen != sizeof(int)) {
                *fd_to_recv = -1;
        }

        return 0;
}

int x_sock_set_block(int sock, int on)
{
        int             val;
        int             rv;

        val = fcntl(sock, F_GETFL, 0);
        if (on) {
                rv = fcntl(sock, F_SETFL, ~O_NONBLOCK&val);
        } else {
                rv = fcntl(sock, F_SETFL, O_NONBLOCK|val);
        }

        if (rv) {
                return errno;
        }

        return 0;
}

int main() {
        pid_t   pid;
        int     sockpair[2];
        int     rv;
        char    fname[256];
        int     fd;

        rv = socketpair(AF_UNIX, SOCK_STREAM, 0, sockpair);
        if (rv < 0) {
                printf("Call socketpair error, errno is %d\n", errno);
                return errno;
        }

        pid = fork();
        if (pid == 0) {
                /* in child */
                close(sockpair[1]);

                for ( ; ; ) {
                        rv = x_sock_set_block(sockpair[0], 1);
                        if (rv != 0) {
                                printf("[CHILD]: x_sock_set_block error, errno is %d\n", rv);
                                break;
                        }

                        rv = recv_fd(sockpair[0], &fd);
                        if (rv < 0) {
                                printf("[CHILD]: recv_fd error, errno is %d\n", rv);
                                break;
                        }

                        if (fd < 0) {
                                printf("[CHILD]: child process exit normally!\n");
                                break;
                        }

                       /* 处理fd描述符对应的文件 */
                        rv = write(fd, MY_LOGO, strlen(MY_LOGO));
                        if (rv < 0) {
                                printf("[CHILD]: write error, errno is %d\n", rv);
                        } else {
                                printf("[CHILD]: append logo successfully\n");
                        }
                        close(fd);
                }

                exit(0);
        }

        /* in parent */
        for ( ; ; ) {
                memset(fname, 0, sizeof(fname));
                printf("[PARENT]: please enter filename:\n");
                scanf("%s", fname);

                if (strcmp(fname, "exit") == 0) {
                        rv = send_fd(sockpair[1], -1);
                        if (rv < 0) {
                                printf("[PARENT]: send_fd error, errno is %d\n", rv);
                        }
                        break;
                }

                fd = open(fname, O_RDWR | O_APPEND);
                if (fd < 0) {
                        if (errno == ENOENT) {
                                printf("[PARENT]: can't find file '%s'\n", fname);
                                continue;
                        }
                        printf("[PARENT]: open file error, errno is %d\n", errno);
                }

                rv = send_fd(sockpair[1], fd);
                if (rv != 0) {
                        printf("[PARENT]: send_fd error, errno is %d\n", rv);
                }

                close(fd);
        }

        wait(NULL);
        return 0;
}

  

编译:gcc -o test_fdpass -lsocket -lnsl test_fdpass.c
执行:test_fdpass(事先在同一目录下创建一个文件kk.log)

[PARENT]: please enter filename:
kk.log
[CHILD]: append logo successfully
[PARENT]: please enter filename:
cc.log
[PARENT]: can't find file 'cc.log'
exit
[CHILD]: child process exit normally!

你可以发现kk.log内容的末尾已经加上了我的独特LOGO '-- Tony Bai'。^_^
关于文件描述符传递的更多细节, W. Richard Stevens的'UNIX网络编程第1卷'和'UNIX环境高级编程'两本书中都有详细说明,参读即可。

 

======================

http://blog.csdn.net/sparkliang/article/details/5486069

进程间传递描述符一

每个进程都拥有自己独立的进程空间,这使得描述符在进程之间的传递变得有点复杂,这个属于高级进程间通信的内容,下面就来说说。顺便把 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 。

 

[cpp] view plaincopy
 
  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 头文件定义,它没有什么特别的。

 

[cpp] view plaincopy
 
  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 的函数原型如下:

 

[cpp] view plaincopy
 
  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);  

 

 

附属数据缓冲区成员 msg_control 与 msg_controllen ,描述符就是通过它发送的,后面将会看到, msg_control指向附属数据缓冲区,而 msg_controllen 指明了缓冲区大小。

4 接收信息标记位 msg_flags ;忽略

 

轮到 cmsghdr 结构了,附属信息可以包括若干个单独的附属数据对象。在每一个对象之前都有一个 struct cmsghdr结构。头部之后是填充字节,然后是对象本身。最后,附属数据对象之后,下一个 cmsghdr 之前也许要有更多的填充字节

cmsghdr.jpg 

[cpp] view plaincopy
 
  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() 宏调用来得到所需的总空间。

下面的例子反映了二者的区别:

 

[cpp] view plaincopy
 
  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 结构的指针 ;

返回跟随在头部以及填充字节之后的附属数据的第一个字节 ( 如果存在 ) 的地址,比如传递描述符时,代码将是如下的形式:

 

[cpp] view plaincopy
 
  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。

通过这两个宏可以很容易遍历所有的附属数据,像下面的形式:

 

[cpp] view plaincopy
 
  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

函数原型如下:

 

[cpp] view plaincopy
 
  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 部分,介绍的很详细。

好了准备工作已经做完了,下面就准备进入正题。

 

发送描述符

经过了前面的准备工作,是时候发送描述符了,先来看看函数原型:

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 说明读取失败;

接收函数代码如下,相比发送要复杂一些。

 

[cpp] view plaincopy
 
  1. int read_fd(int fd, void *ptr, int nbytes, int *recvfd)  
  2. {  
  3.     struct msghdr msg;  
  4.     struct iovec iov[1];  
  5.     int n;  
  6.     int newfd;  
  7. #ifdef HAVE_MSGHDR_MSG_CONTROL  
  8.     union{ // 对齐  
  9.     struct cmsghdr cm;  
  10.     char control[CMSG_SPACE(sizeof(int))];  
  11.     }control_un;  
  12.     struct cmsghdr *cmptr;  
  13.     // 设置辅助数据缓冲区和长度  
  14.     msg.msg_control = control_un.control;  
  15.     msg.msg_controllen = sizeof(control_un.control);  
  16. #else  
  17.     msg.msg_accrights = (caddr_t) &newfd; // 这个简单  
  18.     msg.msg_accrightslen = sizeof(int);  
  19. #endif   
  20.       
  21.     // TCP无视  
  22.     msg.msg_name = NULL;  
  23.     msg.msg_namelen = 0;  
  24.     // 设置数据缓冲区  
  25.     iov[0].iov_base = ptr;  
  26.     iov[0].iov_len = nbytes;  
  27.     msg.msg_iov = iov;  
  28.     msg.msg_iovlen = 1;  
  29.     // 设置结束,准备接收  
  30.     if((n = recvmsg(fd, &msg, 0)) <= 0)  
  31.     {  
  32.         return n;  
  33.     }  
  34. #ifdef HAVE_MSGHDR_MSG_CONTROL  
  35.     // 检查是否收到了辅助数据,以及长度,回忆上一节的CMSG宏  
  36.     cmptr = CMSG_FIRSTHDR(&msg);  
  37.     if((cmptr != NULL) && (cmptr->cmsg_len == CMSG_LEN(sizeof(int))))  
  38.     {  
  39.     // 还是必要的检查  
  40.         if(cmptr->cmsg_level != SOL_SOCKET)  
  41.         {  
  42.             printf("control level != SOL_SOCKET/n");  
  43.             exit(-1);  
  44.         }  
  45.         if(cmptr->cmsg_type != SCM_RIGHTS)  
  46.         {  
  47.             printf("control type != SCM_RIGHTS/n");  
  48.             exit(-1);  
  49.         }  
  50.     // 好了,描述符在这  
  51.         *recvfd = *((int*)CMSG_DATA(cmptr));  
  52.     }  
  53.     else  
  54.     {  
  55.         if(cmptr == NULL) printf("null cmptr, fd not passed./n");  
  56.         else printf("message len[%d] if incorrect./n", cmptr->cmsg_len);  
  57.         *recvfd = -1; // descriptor was not passed  
  58.     }  
  59. #else  
  60.     if(msg.msg_accrightslen == sizeof(int)) *recvfd = newfd;   
  61.     else *recvfd = -1;  
  62. #endif  
  63.     return n;  
  64. }  

 

发送和接收函数就这么多,就像上面看到的,进程间传递套接字还是有点麻烦的。Linux的就介绍完了,后面在简单说说Windows是如何传递的,话说MSDN真的方便哪。