Linux的辅助数据和传递文件描述符

简介

首先,明确传递文件描述符的意义。一般来说,在多进程网络编程中,我们设置一个主进程用于监听新来的连接,设置一个进程池,用于处理这些连接。但是,与线程池不同,进程池各个进程之间的空间是独立的,直接共享主进程建立新连接的文件描述符,此时,需要主进程发送连接文件描述符给子进程。

文件描述符本身仅仅是个数字,具体回顾这个笔记。传递文件描述符只是一个比较方便的称呼,而本质上传递的是文件描述符对应内核中的地址。我们需要借助于辅助数据来获取文件描述符对应的地址。注意,辅助数据是由内核进行填充的,我们只需要指定有关的标志即可,具体流程参照下文。

发送数据和辅助数据

辅助数据本身不是单独的一个结构,而是作为struct msghdr的子结构出现的。在这里给出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 */
    size_t msg_controllen;   /* ancillary data buffer len */
    int msg_flags;           /* flags on received message */
};

给出发送数据的一个详细解释:
msg_name是用于存储地址的,在如果发送数据的时候,没有建立连接,那么需要在这里指定地址和端口,msg_namelen是地址数据的长度。不过,传递文件描述符的时候,一般是通过管道或者是已经建立的连接,所以这两个都是NULL就行。

msg_iov是直接发送的数据块。Linux中,使用readvwritev函数完成,这个结构和这两个函数,可以发送任意的字节流数据,具体方法参考这篇笔记

msg_control表示的就是辅助数据的首地址。该类型的数据结构如下:

struct cmsghdr {
	size_t cmsg_len;    /* Data byte count, including header
                           (type is socklen_t in POSIX) */
	int cmsg_level;     /* Originating protocol */
    int cmsg_type;   /* Protocol-specific type */
    /* followed by unsigned char cmsg_data[]; */
};

这个数据只能通过cmg系列的宏函数函数进行操作。而且,该数据操作前必须进行数据对其操作,具体可以通过共用体实现。

给出一个结构图,该结构包含两个cmsghdr结构:

宏函数的定义如下:

给出协议和类型的宏信息:

一般宏操作流程是,通过一个for循环处理,不断迭代到NULL即可:

struct msghdr msg;
struct cmsghdr *cmsgptr;

// 填充msg结构体
// 调用recvmsg接收数据

for (cmsgptr = CMSG_FIRSTHDR(&msg); cmsgptr != NULL;
     cmsgptr = CMSG_MXTHDR(&msg, cmsgptr)) {
    if (cmsgptr->cmsg_level == ... &&
        cmsgptr->cmsg_type == ...) {
        u_char *ptr;
        ptr = CMSG_DATA(cmsgptr);
        // 处理ptr指向的数据
    }
}

recvmsg和sendmsg

这两个函数是用于接收和发送辅助数据的,函数的定义如下:

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

具体只要先处理完成对应的msg,然后通过这两个函数进行收发即可。如果涉及到多个描述符,那么需要通过上一节的for循环处理。

这里需要单独声明一个特殊点:数据发送的时候,我们需要指明cmsghdr::cmg_type类型,借助宏操作完成地址数据的赋值,最后通过函数进行发送,数据地址的数据传送由函数自动完成,具体参照下面的代码实例。

**第二个特殊点:**文件描述在发送的时候,处于“飞行状态”,此时即使使用close关闭,也不会实际关闭内核的资源。

代码实例

主进程打开一个文件描述符,然后把描述符发送给子进程,之后主进程关闭文件描述符,在子进程中测试文件描述符,代码参考自《Linux高性能服务器编程》。在运行代码之前,在代码的同级目录下,新建text.txt文件,内容是Hello world !

#include <sys/socket.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <assert.h>
#include <string.h>

// 注意理解CMSG_LEN的意义,参照上图,是添加完int数据后整个结构的长度
static const int CONTROL_LEN = CMSG_LEN (sizeof (int) );

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

    iov[0].iov_base = buf;
    iov[0].iov_len = 1;
    msg.msg_name = NULL;  // 通过管道,所以不用指明地址
    msg.msg_namelen = 0;
    msg.msg_iov = iov;
    msg.msg_iovlen = 1;

    cmsghdr cmsg;
    cmsg.cmsg_len = CONTROL_LEN;
    // 这里的level和type相当于说明了是要发送文件描述符,
    // 参照上图的协议和域类型。下文同理
    cmsg.cmsg_level = SOL_SOCKET;
    cmsg.cmsg_type = SCM_RIGHTS;
    // 强制类型转化,取内容赋值
    * (int*) CMSG_DATA (&cmsg) = fd_to_send;
    msg.msg_control = &cmsg;
    msg.msg_controllen = CONTROL_LEN;

    sendmsg (fd, &msg, 0);
}

int recv_fd (int fd) {
    struct iovec iov[1];
    struct msghdr msg;
    char buf[0];

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

    cmsghdr cmsg;
    msg.msg_control = &cmsg;
    msg.msg_controllen = CONTROL_LEN;

    recvmsg (fd, &msg, 0);
	// 注意这里,接收完后是指针,实际上还要取内容
    int fd_to_read = * (int*) CMSG_DATA (&cmsg);
    return fd_to_read;
}

int main() {
    int pipefd[2];
    int fd_to_pass = 0;
    // PF_UNIX也可以替换成SOL_SOCKET,和上文的宏类型对应
    if (socketpair (PF_UNIX, SOCK_STREAM, 0, pipefd) < 0) {
        perror ("socketpair() error\n");
        exit (1);
    }

    pid_t pid = fork();
    if (pid < 0) {
        perror ("fork() error\n");
        exit (1);
    } else if (pid == 0) {  // 孩子进程
        close (pipefd[0]);
        fd_to_pass = open ("text.txt", O_RDWR, 0666);
        send_fd (pipefd[1], (fd_to_pass > 0 ? fd_to_pass : 0) );
        exit (0);
    } else {  // 父进程
        close (pipefd[1]);
        fd_to_pass = recv_fd (pipefd[0]);
        char buf[1024];
        memset (buf, 0, 1024);
        read (fd_to_pass, buf, 1024);
        printf ("I got fd %d and data %s\n", fd_to_pass, buf);
        close (fd_to_pass);
    }

    exit (0);
}

代码输出:

I got fd 4 and data Hello world !
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值