• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

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

武飞扬头像
For Nine
帮助1

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

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 时,接收方将不能确定没有收到数据(但辅助数据可能有文件描述符)。

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhfkbjif
系列文章
更多 icon
同类精品
更多 icon
继续加载