导言

在 Linux 系统编程中,进程间通信(IPC)是核心课题。传统 IPC 机制如管道、消息队列和共享内存各有优势,但在高性能服务器、分布式系统等场景下存在局限,亟需更灵活的通信方式。

一、文件描述符传输的原理

文件描述符作为进程私有资源标识,无法直接跨进程传递。因为不同进程的文件描述符表相互独立,同一数值在不同进程中可能指向不同资源。例如父进程中文件描述符 3 指向网络套接字,直接传递数值 3 给子进程,子进程的 3 可能指向标准输入。因此,需借助 Unix 域套接字等专门 IPC 机制传递。

在 Linux 中,文件描述符是进程访问 I/O 资源的抽象句柄,默认具有进程私有性。而在高并发服务器、分布式文件系统等场景下,传递文件描述符能提升资源复用与协作效率。其实现核心是利用 Linux 辅助数据机制,常通过 socketpair 创建 UNIX 域套接字,配合 sendmsgrecvmsg 系统调用完成。

二、代码实现分析

伪代码部分扩展

为了更清晰地展示文件描述符传输的核心逻辑,以下通过伪代码对关键函数SendFdRecvFd进行流程拆解,并补充更直观的主程序调用逻辑。

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 与运行状态信息;SendFdRecvFd函数则构成文件描述符传输功能的核心实现。以下对宏定义及关键函数进行详细阐释:

宏定义详解

  1. FREEBUSY:通过enum枚举类型定义,用于标识工作进程的运行状态。其中,FREE表示进程处于空闲状态,可接收新任务;BUSY则表示进程正在处理任务。在多进程服务器架构中,主进程可依据该状态信息,合理调度并分配新连接至空闲进程。

  2. CMSG_LEN:该宏用于计算辅助数据的长度。在文件描述符传输过程中,需借助辅助数据携带描述符信息。例如,CMSG_LEN (sizeof(int))可准确计算传递单个int类型文件描述符所需的辅助数据空间,确保cmsghdr结构体能够分配足够内存存储相关信息。

三、recvmsg函数参数详解

recvmsg函数原型为ssize_t recvmsg (int sockfd, struct msghdr *msg, int flags),用于从套接字sockfd接收消息。下面对其参数进行详细讲解:

  1. sockfd:表示用于接收消息的套接字描述符,该套接字可以是流套接字(如SOCK_STREAM)或数据报套接字(如SOCK_DGRAM),并且通常是通过socket函数创建、accept函数接收,或在进程间文件描述符传输场景中,使用ocketpair函数创建的 UNIX 域套接字,该类型套接字能有效支持控制信息(如文件描述符)的传递。

  2. 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_namemsg_namelen:因为连接已建立,源地址已知,所以这两个字段通常设置为NULL和0 。
  • msg_iovmsg_iovlen:msg_iov指向iovec结构体数组,iov_base指向数据缓冲区的起始地址,iov_len表示缓冲区的长度。msg_iovlen则表示msg_iov数组中元素的个数,通过这种方式可以实现分散 - 聚集 I/O,即一次接收多个不连续内存区域的数据。在文件描述符传输示例中,msg_iov数组通常只包含一个元素,用于接收普通数据(但普通数据并非传输核心,文件描述符才是关键)。
  • msg_controlmsg_controllenmsg_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指针赋值给msghdrmsg_control成员,设置msg_controllen为辅助数据长度,最后调用recvmsg函数接收消息,完成文件描述符接收。

四、接发函数实现

sendmsg函数原型为ssize_t sendmsg (int sockfd, const struct msghdr *msg, int flags),其功能是在指定套接字sockfd上发送消息。在SendFd函数中,使用sendmsg发送文件描述符的流程如下:

  • 初始化msghdr结构体hdriovec结构体数组,用于发送数据;

  • 分配并配置cmsghdr结构体,确保msg_lencmsg_levelcmsg_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 定义了完整的编译流程:

  • 首先分别对main.csend.crecv.c进行编译,生成对应的目标文件main.osend.orecv.o,同时启用调试信息(-g)和警告提示(-Wall)选项;

  • 最后将三个目标文件链接生成可执行文件server,并根据程序需求链接pthread库(若涉及多线程操作)。