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

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

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

  • 10
    点赞
  • 54
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Unix/Linux 系统下,我们可以使用 `sendmsg` 和 `recvmsg` 函数来传递文件句柄。下面是一个简单的示例代码: #### 发送进程代码 ```c #include <sys/types.h> #include <sys/socket.h> #include <sys/uio.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <fcntl.h> #include <errno.h> #define CONTROLLEN CMSG_LEN(sizeof(int)) int send_fd(int fd, int fd_to_send) { struct iovec iov[1]; struct msghdr msg; char buf[2]; // 发送任意数据 int ret; union { struct cmsghdr cm; char control[CONTROLLEN]; } control_un; struct cmsghdr *pcmsg; iov[0].iov_base = buf; iov[0].iov_len = 2; msg.msg_iov = iov; msg.msg_iovlen = 1; msg.msg_name = NULL; msg.msg_namelen = 0; if (fd_to_send >= 0) { msg.msg_control = control_un.control; msg.msg_controllen = CONTROLLEN; pcmsg = CMSG_FIRSTHDR(&msg); pcmsg->cmsg_len = CONTROLLEN; pcmsg->cmsg_level = SOL_SOCKET; pcmsg->cmsg_type = SCM_RIGHTS; *(int *)CMSG_DATA(pcmsg) = fd_to_send; } else { msg.msg_control = NULL; msg.msg_controllen = 0; buf[1] = 0; } buf[0] = 0; if ((ret = sendmsg(fd, &msg, 0)) < 0) { perror("sendmsg"); } return ret; } ``` #### 接收进程代码 ```c int recv_fd(int fd) { struct iovec iov[1]; struct msghdr msg; char buf[2]; int ret; union { struct cmsghdr cm; char control[CONTROLLEN]; } control_un; struct cmsghdr *pcmsg; iov[0].iov_base = buf; iov[0].iov_len = 2; msg.msg_iov = iov; msg.msg_iovlen = 1; msg.msg_name = NULL; msg.msg_namelen = 0; msg.msg_control = control_un.control; msg.msg_controllen = CONTROLLEN; if ((ret = recvmsg(fd, &msg, 0)) < 0) { perror("recvmsg"); return -1; } if (buf[0] != 0) { printf("error: received %d bytes\n", ret); return -1; } if ((pcmsg = CMSG_FIRSTHDR(&msg)) != NULL && pcmsg->cmsg_len == CONTROLLEN) { if (pcmsg->cmsg_level != SOL_SOCKET) { printf("error: control level != SOL_SOCKET\n"); return -1; } if (pcmsg->cmsg_type != SCM_RIGHTS) { printf("error: control type != SCM_RIGHTS\n"); return -1; } return *(int *)CMSG_DATA(pcmsg); } else { return -1; } } ``` 使用示例: ```c #include <sys/types.h> #include <sys/socket.h> #include <stdio.h> #include <stdlib.h> int main() { int fd[2]; int ret; char buf[256]; if (socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0) { perror("socketpair"); exit(1); } int send_fd = open("file.txt", O_RDONLY); if (send_fd < 0) { perror("open"); exit(1); } if ((ret = fork()) < 0) { perror("fork"); exit(1); } if (ret == 0) { // 子进程 close(fd[0]); int recv_fd = recv_fd(fd[1]); if (recv_fd < 0) { exit(1); } printf("received fd: %d\n", recv_fd); read(recv_fd, buf, 256); printf("read: %s\n", buf); close(recv_fd); exit(0); } else { // 父进程 close(fd[1]); send_fd(fd[0], send_fd); close(send_fd); waitpid(ret, NULL, 0); exit(0); } } ``` 在这个示例中,父进程打开了 `file.txt` 文件,并将其文件描述符通过 `send_fd` 函数发送给了子进程。子进程通过 `recv_fd` 函数接收到了文件描述符,并读取了文件中的内容。注意,这个示例中并没有考虑错误处理,实际应用中需要根据实际情况进行完善。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值