导言 在 Linux 系统编程中,进程间通信(IPC)是核心课题。传统 IPC 机制如管道、消息队列和共享内存各有优势,但在高性能服务器、分布式系统等场景下存在局限,亟需更灵活的通信方式。
一、文件描述符传输的原理 文件描述符作为进程私有资源标识,无法直接跨进程传递。因为不同进程的文件描述符表相互独立,同一数值在不同进程中可能指向不同资源。例如父进程中文件描述符 3 指向网络套接字,直接传递数值 3 给子进程,子进程的 3 可能指向标准输入。因此,需借助 Unix 域套接字等专门 IPC 机制传递。
在 Linux 中,文件描述符是进程访问 I/O 资源的抽象句柄,默认具有进程私有性。而在高并发服务器、分布式文件系统等场景下,传递文件描述符能提升资源复用与协作效率。其实现核心是利用 Linux 辅助数据机制,常通过 socketpair
创建 UNIX 域套接字,配合 sendmsg
和 recvmsg
系统调用完成。
二、代码实现分析 伪代码部分扩展
为了更清晰地展示文件描述符传输的核心逻辑,以下通过伪代码对关键函数SendFd
和RecvFd
进行流程拆解,并补充更直观的主程序调用逻辑。
SendFd
函数伪代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 函数 SendFd(sockfd, fdtosend) // 初始化消息头结构体 声明 msghdr 结构体 hdr 清空 hdr 内容 // 定义并初始化数据缓冲区 声明数据缓冲区 buf,内容为 "Tell my monther" 声明 iovec 结构体数组 iov,长度为 1 设置 iov[0].iov_base 指向 buf 设置 iov[0].iov_len 为 buf 的长度 设置 hdr.msg_iov 指向 iov 设置 hdr.msg_iovlen 为 1 // 分配并配置辅助数据结构体 动态分配 cmsghdr 结构体空间 pmsg,长度为 CMSG_LEN(sizeof(int)) 设置 pmsg.cmsg_len 为 CMSG_LEN(sizeof(int)) 设置 pmsg.cmsg_level 为 SOL_SOCKET 设置 pmsg.cmsg_type 为 SCM_RIGHTS 将待发送的文件描述符 fdtosend 写入辅助数据区域 // 关联辅助数据到消息头 设置 hdr.msg_control 指向 pmsg 设置 hdr.msg_controllen 为辅助数据长度 // 发送消息 调用 sendmsg(sockfd, &hdr, 0) 发送消息 检查 sendmsg 函数返回值,若失败抛出错误 返回 0
RecvFd
函数伪代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 函数 RecvFd(sockfd, pfdtorecv) // 初始化消息头结构体 声明 msghdr 结构体 hdr 清空 hdr 内容 // 定义并初始化接收缓冲区 声明接收缓冲区 buf,长度为 1024 并初始化为 0 声明 iovec 结构体数组 iov,长度为 1 设置 iov[0].iov_base 指向 buf 设置 iov[0].iov_len 为 1024 设置 hdr.msg_iov 指向 iov 设置 hdr.msg_iovlen 为 1 // 分配并配置辅助数据结构体 动态分配 cmsghdr 结构体空间 pmsg,长度为 CMSG_LEN(sizeof(int)) 设置 pmsg.cmsg_len 为 CMSG_LEN(sizeof(int)) 设置 pmsg.cmsg_level 为 SOL_SOCKET 设置 pmsg.cmsg_type 为 SCM_RIGHTS // 关联辅助数据到消息头 设置 hdr.msg_control 指向 pmsg 设置 hdr.msg_controllen 为辅助数据长度 // 接收消息 调用 recvmsg(sockfd, &hdr, 0) 接收消息 检查 recvmsg 函数返回值,若失败抛出错误 // 提取文件描述符 从辅助数据区域提取文件描述符,赋值给 *pfdtorecv 返回 0
头文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 #include <my_header.h> #include <unistd.h> #include <fcntl.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/wait.h> enum { FREE, BUSY }; typedef struct workerData_s { pid_t pid; int status; } workerData_t; int MakeWorker(const int workernum, workerData_t *workaddr); //使用sendmsg和recvmsg进行进程间文件描述符的传输 //需要管道来实现通信 管道一条可以实现全双工 //无法单纯使用管道来通信(无法传输文件描述符指向的对象) 所以使用函数socketpair int SendFd(int sockfd, int fdtosend); int RecvFd(int sockfd, int *pfdtorecv);
上述代码片段定义了关键数据结构与函数原型。其中,workerData_t
结构体用于管理工作进程的进程 ID 与运行状态信息;SendFd
和RecvFd
函数则构成文件描述符传输功能的核心实现。以下对宏定义及关键函数进行详细阐释:
宏定义详解
FREE
与 BUSY
:通过enum
枚举类型定义,用于标识工作进程的运行状态。其中,FREE表示进程处于空闲状态,可接收新任务;BUSY则表示进程正在处理任务。在多进程服务器架构中,主进程可依据该状态信息,合理调度并分配新连接至空闲进程。
CMSG_LEN
:该宏用于计算辅助数据的长度。在文件描述符传输过程中,需借助辅助数据携带描述符信息。例如,CMSG_LEN
(sizeof(int))
可准确计算传递单个int类型文件描述符所需的辅助数据空间,确保cmsghdr
结构体能够分配足够内存存储相关信息。
三、recvmsg
函数参数详解 recvmsg
函数原型为ssize_t
recvmsg
(int sockfd, struct msghdr *msg, int flags)
,用于从套接字sockfd
接收消息。下面对其参数进行详细讲解:
sockfd
:表示用于接收消息的套接字描述符,该套接字可以是流套接字(如SOCK_STREAM
)或数据报套接字(如SOCK_DGRAM
),并且通常是通过socket
函数创建、accept
函数接收,或在进程间文件描述符传输场景中,使用ocketpair
函数创建的 UNIX 域套接字,该类型套接字能有效支持控制信息(如文件描述符)的传递。
msg
:指向一个msghdr
结构体,该结构体用于存储接收到的消息的具体内容、辅助数据以及相关控制信息,其定义如下:
1 2 3 4 5 6 7 8 9 struct msghdr { void *msg_name; /* 可选的源地址指针,对于无连接套接字(如SOCK_DGRAM),接收时可获取发送方地址;对于面向连接套接字(如SOCK_STREAM),通常设为NULL */ socklen_t msg_namelen; /* msg_name指针指向地址的长度 */ struct iovec *msg_iov; /* 指向iovec结构体数组的指针,用于分散 - 聚集I/O,存储消息的实际数据部分 */ int msg_iovlen; /* iovec结构体数组中的元素个数 */ void *msg_control; /* 指向辅助数据(控制信息)的指针,如在传递文件描述符时,辅助数据用于携带文件描述符信息 */ socklen_t msg_controllen; /* 辅助数据的长度 */ int msg_flags; /* 消息标志,通常设为0,可用于设置如MSG_OOB等特殊标志 */ };
msg_name
和 msg_namelen
:因为连接已建立,源地址已知,所以这两个字段通常设置为NULL和0 。
msg_iov
和 msg_iovlen:msg_iov
指向iovec
结构体数组,iov_base
指向数据缓冲区的起始地址,iov_len
表示缓冲区的长度。msg_iovlen
则表示msg_iov
数组中元素的个数,通过这种方式可以实现分散 - 聚集 I/O,即一次接收多个不连续内存区域的数据。在文件描述符传输示例中,msg_iov
数组通常只包含一个元素,用于接收普通数据(但普通数据并非传输核心,文件描述符才是关键)。
msg_control
和 msg_controllen
:msg_control
指向辅助数据的起始地址,在接收文件描述符时,辅助数据中会包含文件描述符信息;msg_controllen
则指定辅助数据的长度,通常通过CMSG_LEN
宏来计算,确保能够正确容纳文件描述符等控制信息。
msg_flags
:用于设置接收消息时的额外选项,常见标志如MSG_OOB
(接收带外数据)、MSG_DONTWAIT
(非阻塞操作)等,在一般的文件描述符传输场景中,该参数通常设置为0 ,表示使用默认行为。
flags:该参数用于指定接收消息时的额外选项,常见取值如MSG_DONTWAIT(使操作非阻塞)、MSG_OOB(接收带外数据)等。在进行文件描述符接收时,通常将flags设为0,即采用默认的阻塞式接收方式,以确保消息可靠接收。
在RecvFd
函数中,使用recvmsg
接收文件描述符的具体步骤如下:
初始化msghdr
结构体hdr
,并配置iovec
结构体数组,用于接收普通数据(示例中预分配了足够空间的缓冲区,但普通数据并非传输核心,文件描述符才是关键)。
通过malloc
动态分配cmsghdr
结构体空间,并设置其成员:
cmsg_len
:利用CMSG_LEN宏计算辅助数据长度;
cmsg_level
:设置为SOL_SOCKET,表明控制信息与套接字相关;
cmsg_type
:设置为SCM_RIGHTS,该类型专门用于在 Linux 中传递文件描述符;
接收时,通过CMSG_DATA
(pmsg
)从辅助数据的实际存储区域提取文件描述符。
将cmsghdr
指针赋值给msghdr
的msg_control
成员,设置msg_controllen
为辅助数据长度,最后调用recvmsg
函数接收消息,完成文件描述符接收。
四、接发函数实现 sendmsg
函数原型为ssize_t
sendmsg
(int sockfd, const struct msghdr *msg, int flags)
,其功能是在指定套接字sockfd
上发送消息。在SendFd
函数中,使用sendmsg
发送文件描述符的流程如下:
初始化msghdr
结构体hdr
及iovec
结构体数组,用于发送数据;
分配并配置cmsghdr
结构体,确保msg_len
、cmsg_level
和cmsg_type
正确,通过SCM_RIGHTS类型设置辅助数据携带文件描述符;
将待发送的文件描述符写入辅助数据区域,通过sendmsg
函数发送消息,实现文件描述符传递。
以下是具体的发送和接收函数实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include "head.h" int SendFd(int sockfd, int fdtosend) { struct msghdr hdr; bzero(&hdr, sizeof(hdr)); char buf[] = "Tell my monther"; struct iovec iov[1]; iov->iov_base = buf; iov->iov_len = 15; hdr.msg_iov = iov; hdr.msg_iovlen = 1; struct cmsghdr *pmsg = (struct cmsghdr*)malloc(CMSG_LEN(sizeof(int))); pmsg->cmsg_len = CMSG_LEN(sizeof(int)); pmsg->cmsg_level = SOL_SOCKET; pmsg->cmsg_type = SCM_RIGHTS; *(int*)CMSG_DATA(pmsg) = fdtosend; hdr.msg_control = pmsg; hdr.msg_controllen = CMSG_LEN(sizeof(int)); int ret = sendmsg(sockfd, &hdr, 0); ERROR_CHECK(ret, -1, "sendmsg"); return 0; }
SendFd
函数通过sendmsg
实现文件描述符发送,其核心在于正确配置msghdr
结构体,特别是利用cmsghdr
结构体设置辅助数据,通过SCM_RIGHTS类型完成文件描述符传递。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include "head.h" int RecvFd(int sockfd, int *pfdtorecv) { struct msghdr hdr; bzero(&hdr, sizeof(hdr)); char buf[1024] = {0}; struct iovec iov[1]; iov->iov_base = buf; iov->iov_len = 1024; hdr.msg_iov = iov; hdr.msg_iovlen = 1; struct cmsghdr *pmsg = (struct cmsghdr*)malloc(CMSG_LEN(sizeof(int))); pmsg->cmsg_len = CMSG_LEN(sizeof(int)); pmsg->cmsg_level = SOL_SOCKET; pmsg->cmsg_type = SCM_RIGHTS; //传文件对象 hdr.msg_control = pmsg; hdr.msg_controllen = CMSG_LEN(sizeof(int)); int ret = recvmsg(sockfd, &hdr, 0); ERROR_CHECK(ret, -1, "sendmsg"); printf("buf == %s, fdtorecv == %d\n", buf, *(int*)CMSG_DATA(pmsg)); *pfdtorecv = *(int*)CMSG_DATA(pmsg); return 0; }
RecvFd
函数通过recvmsg
接收文件描述符,通过与发送端一致的msghdr结构配置,从辅助数据中准确提取文件描述符,确保接收的描述符与发送端指向同一文件对象。
主程序示例展示了上述功能的调用过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include "head.h" int main(int argc, char *argv[]) { int fds[2]; socketpair(AF_LOCAL, SOCK_STREAM, 0, fds); //父程序 if (fork()) { close(fds[0]); int fd = open("chat.txt", O_RDWR|O_CREAT|O_TRUNC, 0775); ERROR_CHECK(fd, -1, "open"); printf("Father fd == %d\n", fd); write(fd, "Tell my monther", sizeof("Tell my monther")); SendFd(fds[1], fd); wait(NULL); } else { close(fds[1]); int fd = -1; RecvFd(fds[0], &fd); printf("childfd == %d\n", fd); ssize_t wret = write(fd, ", I am Ok!", sizeof("I am Ok!")); ERROR_CHECK(fd, -1, "RecvFd"); ERROR_CHECK(wret, -1, "write"); } return 0; }
主程序通过socketpair
创建一对 UNIX 域套接字,并利用fork系统调用生成子进程。父进程首先打开文件并写入数据,随后通过套接字将文件描述符发送给子进程;子进程接收描述符后,直接向同一文件追加内容。尽管父子进程中的文件描述符数值可能不同(如父进程为 3,子进程为 4),但二者指向相同的文件对象,从而实现跨进程的文件协同操作。
五、编译指令分析 以下是该程序的 Makefile
编译规则:
1 2 3 4 5 6 7 8 9 10 11 server: main.o recv.o send.o gcc main.o recv.o send.o -o server -lpthread main.o: main1.c gcc -c main1.c -o main.o -g -Wall send.o: send.c gcc -c send.c -o send.o -g -Wall recv.o: recv.c gcc -c recv.c -o recv.o -g -Wall
该 Makefile
定义了完整的编译流程: