进程间传递打开的文件描述符,并不是传递文件描述符的值。那么在传递时究竟传递了什么?我们要先搞明白这个问题。

1、文件描述符

文件描述符的值与文件没有任何联系,只是该文件在进程中的一个标识,所以同一文件在不同进程中的文件描述符可能不一样,相同值 的文件描述符在不同进程中可能标识不同文件。

文件数据结构

Linux使用三种数据结构表示打开的文件:
文件描述符表 :进程级的列表,内含若干项,每一项都存储了当前进程中一个文件描述符及其对应的文件表指针。
文件表 :内核级的列表,内含若干文件表项,每一个文件表项对应文件描述符表中的一项,即,不同进程打开的同一文件对应内核中的不同文件表项,这样能够使每个进程都有它自己的对该文件的当前偏移量。
v节点 :内核级的列表,每个打开的文件都只有一个v节点,包含文件类型,对文件进行操作的函数指针,有的还包括 i 节点。

三者的对应关系如下图所示:

不难发现,在进程之间传递 “文件描述符” 并不只是传递一个int型的标识符,而是需要一套特殊的传递机制。本篇博客介绍的方法基于sendmsgrecvmsg这两个函数。

首先在两个进程之间建立 UNIX域 socket 作为消息传递的通道,然后发送进程调用 sendmsg 向通道发送特殊的消息,内核对该消息做特殊的处理,从而将打开的文件描述符传递到接收进程。且接收方和发送方的文件描述符指向内核中相同的文件表项。所以,进程间传递文件描述符也算是实现了进程间共享文件,如下图所示:

2、使用UNIX域 socket 实现传递文件描述符

创建UNIX域很简单,用socketpair函数即可,难的是如何使用sendmsgrecvmsg函数进行发送和接收。两个函数的定义如下:

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

参数sockfd的含义不必多说,msg可理解为被发送/接收的数据,在recvmsg中他的行为类似于传出参数,flags参数与recv/send的同名参数含义一样。成功时返回实际发送/接收的字节数,失败返回-1。

该函数最难理解的就是msg的类型 msghdr文件描述符就是通过msghdr结构体的msg_control成员发送的,而msg_control又是 cmsghdr 类型,下面将重点介绍他们。

msghdr 结构体

结构体定义如下:

struct msghdr
{void*         msg_name;                socklen_t     msg_namelen;          struct iovec* msg_iov;              int           msg_iovlen;           void*         msg_control;          socklen_t     msg_conntrollen;      int           msg_flags;            最后这个参数不管
}

结构体成员两两一组分为3组,这样分析清晰很多:

① socket 地址成员:msg_namemag_namelen

msg_name是指向socket地址的指针,msg_namelensocket地址的长度,他们只有在 通道使用UDP协议时才被使用。对于其他通道直接将两个参数设置为 NULL0即可。

对于recvmsg函数,他们是传出参数,会返回发送方的 socket 地址。
msg_name 定义为void *类型,因此并不需要将其显式转换为 struct sockaddr *

② 待发送的分散数据块:msg_iovmsg_iovlen

msg_iov是一个结构体数组,每个结构体都封装一块内存的起始地址和长度,其定义如下:

struct iovec
{void* iov_base;    内存起始地址size_t iov_len;   这块内存的长度
};

msg_iovlen指定了内存块的个数,即,结构体数组的长度。

对于recvmsg而言,数据将被读取并存放在msg_iovlen块分散的内存中,这些内存的位置和长度由msg_iov指向的iovec数组指定,这称为分散读。

对于sendmsg而言,msg_iovlen块分散内存中的数据将被一并发送,这称为集中写。

③ 辅助(或附属)数据:msg_controlmsg_controllen

msg_control指向辅助数据起始地址,msg_controllen指明辅助数据的长度。msg_control的类型是struct cmsghdr*,其定义如下:

struct cmsghdr
{size_t cmsg_len;    辅助数据的总长度,由 CMSG_LEN 宏(马上讲)直接获取int    cmsg_level;  表示通道使用的的原始协议级别,与 setsockopt 函数的 level 参数相同int    cmsg_type;   /* Protocol-specific type */控制信息类型,例如,SCM_RIGHTS,辅助数据是文件描述符;SCM_CREDENTIALS,辅助数据是一个包含证书信息的结构/* followed by unsigned char cmsg_data[]; */被注释的 cmsg_data 指明了物理内存中真正辅助数据的位置,帮助理解
};

辅助信息分三部分,分别是 cmsghdr结构体(又称头部)填充(对齐)字节数据部分(数据部分后面可能还有填充字节,这是为了对齐),在内存中也是按此顺序排布。虽说这部分共称辅助数据,但其实真正的辅助数据只有后面的数据部分。

注意,辅助数据不止一段。每段辅助数据都由cmsghdr结构体开始,每个cmsghdr结构体只记录自己这一段辅助数据的大小。所以最终整个辅助数据大小还需要进行求和(求和方法马上讲)。

在实际使用时,需要我们填充的是cmsghdr结构体 和 数据部分。Linux为我们提供了如下宏来填充他们:

#include <sys/socket.h>
#include <sys/param.h>
#include <sys/socket.h>
size_t CMSG_LEN(size_t length);
void* CMSG_DATA(struct cmsghdr *cmsg);
struct cmsghdr *CMSG_FIRSTHDR(struct msghdr *msgh);
struct cmsghdr *CMSG_NXTHDR(struct msghdr *msgh, struct cmsghdr *cmsg);
size_t CMSG_SPACE(size_t length);
size_t CMSG_ALIGN(size_t length);

CMSG_LEN() 宏:
传入参数:只需传入数据(第三)部分对象的大小。
返回值:系统自动计算整个辅助数据的大小(不包结尾的填充字节)并返回。
用途:直接把该宏赋值给msghdrcmsg_len成员。

CMSG_DATA() 宏:
传入参数:指向cmsghdr结构体的指针。
返回值:返回跟随在头部以及填充字节之后的辅助数据的第一个字节(如果存在,对于recv)的地址。
用途:设置我们要传递的数据。例如要传递文件描述符时,代码如下

struct cmsgptr* cmptr;
int fd = *(int*)CMSG_DATA(cmptr);  // 发送:*(int *)CMSG_DATA(cmptr) = fd;

CMSG_FIRSTHDR() 宏:
输入参数:指向msghdr结构体的指针。
返回值:指向整个辅助数据中的第一段辅助数据的 struct cmsghdr 指针。如果不存在辅助数据则返回NULL

CMSG_NXTHDR() 宏:
输入参数:指向msghdr结构体的指针,和指向当前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()宏取得辅助数据了
}

CMSG_SPACE() 宏:
输入参数:只需传入数据(第三)部分对象的大小。
返回值:计算每一段辅助数据的大小,包括结尾(两段辅助数据之间)的填充字节,注意,CMSG_LEN()并不包括结尾的填充字节。
用途:计算整个辅助数据所需的总大小。如果有多段辅助数据,要使用多个CMSG_SPACE()宏计算所有段辅助数据所需的总内存空间。

CMSG_LEN() 和 CMSG_SPACE()的区别:
printf("CMSG_LEN(sizeof(short))=%d/n", CMSG_LEN(sizeof(short)));         返回14
printf("CMSG_SPACE(sizeof(short))=%d/n", CMSG_SPACE(sizeof(short)));     返回16,说明这段辅助数据最后还有2字节的填充字节

CMSG_ALIGN()宏:
用的不多,先不管他。

综上,文件描述符是通过msghdr的 辅助数据的 数据部分发送的。了解了msghdrcmsghdr两个结构体我们就可以使用sendmsgrecvmsg函数发送文件描述符了。

3、实例程序

/* 本程序实现子进程打开一个文件描述符,然后将其传递给父进程,父进程通过其获得文件内容 */
#include<sys/socket.h>
#include<fcntl.h>
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<assert.h>
#include<string.h>static const int CONTROL_LEN = CMSG_LEN(sizeof(int));/*** @brief 发送目标文件描述符* @param fd        传递信息的 UNIX 域 文件描述符* @param fd_to_send  待发送的文件描述符*/
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 cm;          /* 这是辅助数据头部结构体,文件描述符就是通过这个结构体以及后面的数据部分发送的 */cm.cmsg_len = CONTROL_LEN;  /* 辅助数据的字节数,包扩头部和真正的数据 */cm.cmsg_level = SOL_SOCKET;    /* 表示原始协议级别,与 setsockopt 的 level 参数相同 */cm.cmsg_type = SCM_RIGHTS;  /* 控制信息类型,SCM_RIGHTS 表示传送的内容是访问权 */*(int*)CMSG_DATA(&cm) = fd_to_send;/* 设置真正数据部分为我们想发送的文件描述符 */msg.msg_control = &cm;     /* 设置辅助数据 */msg.msg_controllen = CONTROL_LEN;sendmsg(fd, &msg, 0);
}/*** @brief 接受文件描述符* @param fd 传递信息的 UNIX 域 文件描述符*/
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 cm;msg.msg_control = &cm;msg.msg_controllen = CONTROL_LEN;recvmsg(fd, &msg, 0);int fd_to_read = *(int*)CMSG_DATA(&cm);return fd_to_read;
}int main()
{int pipefd[2];int fd_to_pass = 0;/* 创建父,子进程间的管道,文件描述符 pipefd[0] 和 pipefd[1] 都是 UNIX 域 socket */int ret = socketpair(PF_UNIX, SOCK_STREAM, 0, pipefd);assert(ret != -1);pid_t pid = fork();assert(pid >= 0);if (pid == 0)   /* 子进程 */{close(pipefd[0]);fd_to_pass = open("test.txt", O_RDWR, 0666);/* 子进程通过管道将文件描述符发送到父进程,如果文件 test.txt 打开失败,则子进程将标准输入文件描述符发送到父进程 */send_fd(pipefd[1], (fd_to_pass > 0) ? fd_to_pass : 0);close(fd_to_pass);exit(0);}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);
}

3、注意事项

一个描述符在传递过程中(从调用 sendmsg 发送到调用 recvmsg 接收),内核会将其标记为“在飞行中(in flight )。在这段时间内,即使发送方试图关闭该描述符,内核仍会为接收进程保持打开状态。因为发送文件描述符会使其引用计数加 1 。

文件描述符是通过辅助数据发送的,而不是正经的数据段,所以在发送时,总是发送至少 1 个字节的正经数据,即使这个数据没有任何实际意义。否则,当接收返回 0 时,接收方将不能确定没有收到数据(但辅助数据可能有文件描述符)。

Linux高级进程编程———在任意两个进程间传递文件描述符:使用 sendmsg 和 recvmsg 实现相关推荐

  1. Linux 进程间传递文件描述符

    文章目录 文件描述符 文件数据结构 共享文件 UNIX域socket实现传递文件描述符 进程间传递打开的文件描述符,并不是传递文件描述符的值.先说一下文件描述符. 文件描述符 对内核来说,所有打开的文 ...

  2. Linux中进程间传递文件描述符的方法

    在进行fork调用后,由于子进程会拷贝父进程的资源,所以父进程中打开的文件描述符在子进程中仍然保持着打开,我们很容易的就将父进程的描述符传递给了子进程.但是除了这种情况下,如果想将某个父进程在子进程创 ...

  3. android进程间传递文件描述符原理

    在Linux中,进程打开一个文件,返回一个整数的文件描述符,然后就可以在这个文件描述符上对该文件进行操作.那么文件描述符和文件到底是什么关系?进程使用的是虚拟地址,不同进程间是地址隔离的,如何在两个进 ...

  4. 进程间传递文件描述符--sendmsg,recvmsg(可用)

    UNIX域套接字可以在同一台主机上各进程之间传递文件描述符. 下面先来看两个函数: #include <sys/types.h> #include <sys/socket.h> ...

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

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

  6. 不相干进程之间传递文件描述符

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

  7. 【Linux】基础IO(万字详解) —— 系统文件IO | 文件描述符fd | 重定向原理

  8. Linux系统学习笔记:文件描述符标志

    文件描述符标志的概念 文件描述符标志(目前就只有一个close-on-exec): 它仅仅是一个标志,当进程fork一个子进程的时候,在子进程中调用了exec函数时就用到了这个标志.意义是执行exec ...

  9. Linux文件描述符详解

    文章目录 一.概念 `1.1.特点` 优点 缺点 二.手动创建文件描述符 `2.1.语法` 2.1.1.创建 2.1.2.调用 2.1.3.关闭 `2.2.重定向输出的文件描述符` `2.3.重定向输 ...

最新文章

  1. java中getter_Java中的Getter和Setters解释了
  2. 国际机器人联合会:全球工业机器人2019报告
  3. 组件方式开发 Web App全站 学习视频 分享
  4. 内存和swap查看 内存是拿来用的 不是看的
  5. 一个回滚段收缩的实例
  6. PyQt窗体视觉样式
  7. tcp/ip协议初识
  8. 【渝粤教育】国家开放大学2018年春季 0704-22T民法基础与实务 参考试题
  9. abb机器人离线编程软件叫做_Robotstudio软件:ABB机器人机器视觉位姿引导虚拟仿真...
  10. java 游戏 异步框架_基于Java的轻量级异步编程框架
  11. Shortest Prefixes(poj 2001)
  12. 霍营派出所办理居住证
  13. 百度App性能优化工具篇 - Thor原理及实践
  14. 数据结构-平衡二叉树(AVL树)
  15. Python_Flask
  16. 【动手撸深度学习】细粒辨花 一文实践清华博士Densenet
  17. 【乌拉喵.教程】IIC总线介绍及FPGA编程
  18. vue实现省市区三级联动地址选择组件
  19. 【数据治理】数据治理方案技术调研 Atlas VS Datahub VS Amundsen
  20. 《Activiti 深入BPM工作流》--- 什么是activiti的Service, 如何创建?

热门文章

  1. HTML-JS-CODING
  2. 无法启动此程序,因为计算机中丢失D3DCOMPILER _47.dIl. 尝试重
  3. 第三章 LD3320语音识别模块的使用
  4. C语言常用语句之-分支语句
  5. python 使用API并将获取到的数据可视化的基本方法(详细)
  6. 医学统计学中RR、OR和HR三个关于比值的概念
  7. RooT最好软件,root手机最好的软件
  8. 盒子移动的问题,拖拽问题
  9. cisco rommon 维护路由器
  10. HCIP-DATACOM H12-831(101-120)