在进程之间传递描述符,我们通常会想到fork一个子进程,通过子进程共享父进程所有打开的描述符,来实现进程之间传递描述符。但是有时候我们需要子进程向父进程传递描述符,或者在没有血缘关系的两个进程之间传递文件描述符,比如客户端向服务端请求打开某个文件或者设备,服务端进行打开操作并把描述符传递给客户端,这样可以实现对客户端屏蔽打开文件或者设备的细节。
我们应当注意的是这里传递的描述符不是数值,而是发送描述符进程的PCB中文件描述符表里以该文件描述符为索引的元素----指向File结构体的指针
对于上面描述的需求,Linux系统提供了sendmsg和recvmsg这样一组系统调用:
#include <sys/types.h>
#include <sys/socket.h>
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);
我们重点说明msg参数,sockfd和flags参数与recv和send这一组系统调用中的一样,不再赘述。 struct 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 */
};
我们将成员分为4组来看,这样会更方便些:
1.套接字地址成员 msg_name 和msg_namelen:
由于该系统的调用是通用数据读写函数,其支持TCP和 UDP,所以msg里必须包含像recvfrom和sento这样一组系统调用参数中的socket地址和地址长度 的成员,即msg_name和msg_namelen。UDP协议下,才使用这两个成员,若不使用这两个成员可分别设置为:NULL和0。
2.I/O向量引用 msg_iov和msg_iovlen:
msg_iov是一个类型为struct iovec的结构体指针 ,该结构体内容如下:
struct iovec { /* Scatter/gather array items */
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};
其中iov_base指向一个缓冲区,iov_len为缓冲区的大小。
msg_iov指向一个struct iovec数组,msg_iovlen为数组的长度。
有了这组成员就可以集中写和分散读,像readv和writev这组系统调用一样,其函数原型如下:
#include <sys/uio.h>
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
3.辅助数据成员 msg_control和msg_controllen:
描述符就是通过这组成员发送的,msg_control指向辅助数据缓冲区,msg_controllen为该缓冲区的大小。该辅助数据缓冲区可包含多个辅助数据,每个辅助数据前有一个头部,是一个struct cmsghdr结构体,在该结构体和辅助数据之间可能存在填充字节(看具体的操作系统),这是因为辅助数据在内存中紧跟结构体,为了保证该辅助数据在内存中的字节对齐,系统可能会在其和结构体之间填充字节,关于字节对齐可以看下—>这篇博文
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 */
};
cmsg_len为辅助数据的字节数,包含头部的大小,由于存在填充字节的情况,系统为我们提供了宏CMSG_LEN来获取辅助数据的字节数,我们后文再介绍。
cmsg_level表示原始的协议级别(如SOL_SOCKET);
cmsg_type表示控制信息类型(例如, SCM_RIGHTS ,附属数据对象是文件描述符;SCM_CREDENTIALS ,附属数据对象是一个包含证书信息的结构);
传递描述符的情况下,cmsg_level和cmsg_type我们分别设为SOL_SOCKET和SCM_RIGHTS。
4.msg_flags成员无需设定,它会复制recvmsg/sendmsg的flags参数的内容以影响数据读写过程。
现在我们介绍完了recvmsg/sendmsg这组系统调用,终于可以进入描述符传递的环节了
通过上面的介绍我们可以知道,传递描述符的关键是msg里的辅助数据,但是因为它涉及到内存对齐,我们并不好处理它,好在Linux系统为我们提供了一组宏,现在我们来认识这组宏。
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_DATA()宏
输入参数:指向辅助数据头部的cmsghdr 结构的指针
返回跟在头部cmsghdr和填充字节后的辅助数据的地址。
CMSG_FIRSTHDR()宏
输入参数:指向struct msghdr结构的指针
返回指向辅助数据缓冲区中第一个辅助数据的头部struct cmsghdr结构的指针,不存在则返回NULL
CMSG_NXTHDR()宏
输入参数:指向struct msghdr结构的指针,指向当前struct cmsghdr结构的指针
返回指向下一个辅助数据的头部(struct cmsghdr结构)的指针,不存在则返回 NULL。
通过上面两个宏的搭配使用可以遍历辅助数据缓冲区里的所有辅助数据:
struct msghdr msg;
struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg);
for(;cmsg !=NULL;CMSG = CMSG_NXTHDR(&MSG,CMSG))
//得到cmsg就可以通过CMSG_DATA宏得到辅助数据了。
CMSG_LEN()宏
输入参数:辅助数据的大小。
返回cmsghdr+填充字节+辅助数据的大小,用于设置cmsghdr结构里的msg_len成员。
CMSG_SPACE()宏
输入参数:辅助数据的大小
返回cmsghdr+填充字节+辅助数据+填充字节的大小。注意这里与CMSG_LEN()的 区别,还是字节对齐的问题,为了方便我们将cmsghdr+填充字节+辅助数据称为一个辅助数据对象,辅助数据缓冲区里可以有多个辅助数据对象,为了保证辅助数据对象的字节对齐,系统可能会在前一个辅助数据对象后面填充字节,我们把填充的字节归为前一个辅助数据对象,此时辅助数据对象为:cmsghdr+填充字节+辅助数据+填充字节,并且解决了多个辅助数据对象的字节对齐问题。这里的填充字节可以看为系统为了解决一个结构体数组的字节对齐问题,给数组中每个结构体末尾进行了填充字节。
下面这个图片很好展示了,msg_control指向的辅助数据缓冲区的内存分布,CMSG_LEN()宏和CMSG_SPACE()宏返回的是什么,各个表示长度的成员到底指的是什么长度。该图片来自---->这篇博文
至此,所有的前置知识已经说完,我们正式进入描述符的传递:
我们应当注意的是:
1.虽然recvmsg/sendmsg是通用的读写函数,但是当我们用它们来传递文件描述符时,只能通过Unix 域套接字来传递,因为我们传递的是一个进程中指向file结构体的指针,只有两个进程在同一个计算机上,这个指针才正确,并且struct msghdr结构中的control指向的cmsghdr结构里的cmsg_level设置的是SOL_SOCKET。
2.在一次传递描述符的过程中,sendmsg发送描述符后,发送进程打开的file结构体引用计数会加1,内核会将描述符标记为“在飞行中",发送进程关闭该文件描述符,file结构体也不会被销毁,即使描述符还没被接收进程接收。
3.在本文最开始已经说明传递描述符并不是传递它的数值,所以接受进程接受到描述符的数值并不一定和发送进程发送的描述符数值相同,而是为接受进程当前最小未被使用的描述符。
4.描述符是通过辅助数据发送的,我们在发送描述符的时候应该总是至少发送1字节的数据,即使这个数据没有任何实际意义,否则接收方无法判断是否收到描述符。
5.在进程之间可以传递任意类型的描述符,比如可以是 pipe , open , mkfifo 或 socket , accept ,epoll_create等函数返回的描述符。所以我们称之为传递描述符,而不说传递文件描述符。
6.在《UNIX网络编程》一书中说msg_control必须为cmsghdr结构适当的对齐,它通过声明一个由cmsghdr结构体和一个字符数组构成的联合体来实现:
void send_fd(int fd,int fd_to_send)
{
struct msghdr msg;
char iobuf[1];
struct iovec io = {
.iov_base = iobuf,
.iov_len = sizeof(iobuf)
};
union { /* Ancillary data buffer, wrapped in a union
in order to ensure it is suitably aligned */
char buf[CMSG_SPACE(sizeof(int))];
struct cmsghdr cmsg;
} u;
u.cmsg.cmsg_level = SOL_SOCKET;
u.cmsg.cmsg_type = SCM_RIGHTS;
u.cmsg.cmsg_len = CMSG_LEN(sizeof(int));
*(int *)CMSG_DATA(&u.cmsg) = fd_to_send; /* Initialize the payload */
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = u.buf;
msg.msg_controllen = sizeof(u.buf);
sendmsg(fd,&msg,0);
}
它并没有给出这样做的理由,我个人的理解是并不是一定要使msg_control为cmsghdr结构适当对齐,struct msghdr结构体里的msg_control指向的是辅助数据缓冲区,这个辅助数据缓冲区是由我们来指定的,所以只要这个缓冲区的大小满足辅助数据和其头部struct cmsghdr结构体通过填充字节后的大小即可:
void send_fd(int fd,int fd_to_send)
{
struct msghdr msg;
struct cmsghdr *cmsg;
char iobuf[1];
struct iovec io = {
.iov_base = iobuf,
.iov_len = sizeof(iobuf)
};
char buf[CMSG_SPACE(sizeof(int))];
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = buf;
msg.msg_controllen = sizeof(buf);
//cmsg = (struct cmsghdr*)buf;
cmsg = CMSG_FIRSTHDR(&msg);
//上面两种方式都可以
cmsg->cmsg_level = SOL_SOCKET;
cmsg->cmsg_type = SCM_RIGHTS;
cmsg->cmsg_len = CMSG_LEN(sizeof(int));
*(int *)CMSG_DATA(cmsg) = fd_to_send; /* Initialize the payload */
sendmsg(fd,&msg,0);
}
我们应当注意的是,上面两种写法都使用了字符数组,这只是为了获得一个满足辅助数据缓冲区大小的内存块来做辅助数据缓冲区,并不是说辅助数据在内存中的存在形式为字符串,辅助数据在内存的存在形式前面那个图片已经很形象的说明了。
虽然说我们并不是一定要通过联合体的方式来使msg_control为cmsghdr结构适当的对齐,但当有多个辅助数据对象(cmsghdr+填充字节+辅助数据(描述符)+填充字节)时,这样做无疑是较好的选择,一个联合体即为一个辅助数据对象,我们可以通过申请一个联合体数组来做辅助数据缓冲区,这样的代码结构是清晰明了的。如果我们依旧只使用字符数组,那么就需要一个二维字符数组来做辅助数据缓冲区,这样的代码结构无疑是糟糕的。
当我们在父子进程之间传递描述符时,我们可以通过socketpair 函数来建立匿名管道,该匿名管道为全双工通信。其函数原型如下:
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int socketpair(int domain, int type, int protocol, int sv[2]);
下面通过完整的代码,来展示进程之间是如何传递描述符的:
#include<stdio.h>
#include<iostream>
#include<sys/socket.h>
#include<sys/types.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
void send_fd(int fd,int fd_to_send)
{
struct msghdr msg;
char iobuf[1];
struct iovec io = {
.iov_base = iobuf,
.iov_len = sizeof(iobuf)
};
union { /* Ancillary data buffer, wrapped in a union
in order to ensure it is suitably aligned */
char buf[CMSG_SPACE(sizeof(int))];
struct cmsghdr cmsg;
} u;
u.cmsg.cmsg_level = SOL_SOCKET;
u.cmsg.cmsg_type = SCM_RIGHTS;
u.cmsg.cmsg_len = CMSG_LEN(sizeof(int));
*(int *)CMSG_DATA(&u.cmsg) = fd_to_send; /* Initialize the payload */
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = u.buf;
msg.msg_controllen = sizeof(u.buf);
sendmsg(fd,&msg,0);
}
int recv_fd(int fd)
{
struct msghdr msg;
char iobuf[1];
struct iovec io = {
.iov_base = iobuf,
.iov_len = sizeof(iobuf)
};
union { /* Ancillary data buffer, wrapped in a union
in order to ensure it is suitably aligned */
char buf[CMSG_SPACE(sizeof(int))];
struct cmsghdr cmsg;
} u;
msg.msg_name = NULL;
msg.msg_namelen = 0;
msg.msg_iov = &io;
msg.msg_iovlen = 1;
msg.msg_control = u.buf;
msg.msg_controllen = sizeof(u.buf);
recvmsg(fd,&msg,0);
return *(int *)CMSG_DATA(&u.cmsg);
}
int main(void)
{
int pipefd[2];
socketpair(AF_UNIX,SOCK_DGRAM,0,pipefd);
pid_t pid = fork();
if(pid == 0)
{
close(pipefd[0]);
int fd = open("./IPC_SEM.cpp",O_RDWR);
send_fd(pipefd[1],fd);
close(fd);
close(pipefd[1]);
exit(0);
}
else if(pid > 0){
close(pipefd[1]);
int fd = recv_fd(pipefd[0]);
char buf[BUFSIZ];
memset(buf,'\0',sizeof(buf));
read(fd,buf,sizeof(buf));
printf("I got fd %d\n%s\n",fd,buf);
close(fd);
close(pipefd[0]);
}
}
参考资料:
《Linux高性能服务器编程》 游双 著
阿里云【Nebula系列】通过UNIX域套接字传递描述符的应用
linux网络编程之socket(十六):通过UNIX域套接字传递描述符和 sendmsg/recvmsg 函数
进程间传递描述符一