简介
首先,明确传递文件描述符的意义。一般来说,在多进程网络编程中,我们设置一个主进程用于监听新来的连接,设置一个进程池,用于处理这些连接。但是,与线程池不同,进程池各个进程之间的空间是独立的,直接共享主进程建立新连接的文件描述符,此时,需要主进程发送连接文件描述符给子进程。
文件描述符本身仅仅是个数字,具体回顾这个笔记。传递文件描述符只是一个比较方便的称呼,而本质上传递的是文件描述符对应内核中的地址。我们需要借助于辅助数据来获取文件描述符对应的地址。注意,辅助数据是由内核进行填充的,我们只需要指定有关的标志即可,具体流程参照下文。
发送数据和辅助数据
辅助数据本身不是单独的一个结构,而是作为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中,使用readv和writev函数完成,这个结构和这两个函数,可以发送任意的字节流数据,具体方法参考这篇笔记。
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 !