☰
进程间传递文件描述符
2026/9/29 3:17:53 网站建设 项目流程

进程A将打开的文件描述符传给另外一个进程B,那么进程B和进程A对此文件有相同的权限了么?

是的,进程B会获得与进程A对该文件的相同权限。

这背后的核心原理是:传递的并非文件描述符这个整数值本身,而是内核中指向同一个打开文件对象的“通行证”。

核心机制:内核级的“指针”复制

  1. 文件描述符的本质:在Linux中,文件描述符(fd)对于每个进程是独立的,它只是进程内部文件描述符表的一个索引。进程A的fd=3和进程B的fd=3完全不相关。

  2. 传递的真正对象:当通过sendmsg和SCM_RIGHTS传递文件描述符时,内核执行的操作是:在进程B的文件描述符表中创建一个新的表项,但这个新表项指向的,是内核中与进程A完全相同的那个file结构体对象。

    • 可以把这个file结构体理解为内核为这个“打开的文件”维护的核心控制块。

    • 它包含了文件的访问权限(读写等)、当前文件偏移量等所有关键状态信息。

结论:权限与状态同步

由于进程A和进程B的fd最终指向了同一个内核file对象,因此:

  • 权限相同:它们共享该file对象中记录的访问权限。进程A以只读方式打开,进程B拿到的也是只读权限。

  • 状态共享:它们也共享文件偏移量等状态。这意味着,如果进程A读取了部分数据,进程B随后读取时会从A停止的位置继续,因为它们操作的是同一个文件表项。

  • 引用计数:内核中该file对象的引用计数会增加,以确保在两个进程都关闭fd之前,该对象不会被释放。

这个机制可以看作是跨进程的dup调用,其效果与父子进程通过fork()继承文件描述符是一样的-。

总结

通过SCM_RIGHTS传递文件描述符,本质上是让接收进程获得了一个指向发送进程已打开文件的“新钥匙”。这把“新钥匙”虽然外观(fd整数值)不同,但开启的是同一扇门,因此拥有完全相同的访问权限。

如何将本进程打开的文件描述符共享给其他进程使用?请用c/c++语言实现一个demo样例

下面是一个完整的 C++ Demo,演示如何使用Unix 域套接字 (AF_UNIX)和SCM_RIGHTS辅助消息,在父子进程间传递文件描述符。


核心原理

文件描述符是进程私有的整数索引,不能直接通过共享内存或普通数据传递。正确的做法是通过 Unix 域套接字发送SCM_RIGHTS类型的辅助数据(ancillary data),让内核在接收方进程中创建一个新的文件描述符,指向与发送方同一个内核文件对象(struct file)。

传递后,接收方获得的文件描述符与发送方拥有完全相同的权限和共享的文件偏移量。

Linux 利用Unix 域套接字(Unix Domain Socket)和SCM_RIGHTS这种特殊的辅助数据(Ancillary Data),在进程间传递文件描述符​​​​​​​。

其核心原理是:传递的不是一个整数,而是指向同一个内核“打开文件描述”(struct file)的指针。

具体实现过程如下:

1. 理解文件描述符的本质

在Linux内核中,每个打开的文件都由一个struct file对象管理-。进程通过文件描述符表(fd_array)中的索引(即我们常说的fd数字)来找到这个struct file对象。

2. 神秘的SCM_RIGHTS

SCM_RIGHTS是一个特殊的消息类型,它告诉内核:“请特殊处理这个消息里的数据,它们不是普通字节,而是需要传递的文件描述符。”

这个机制依赖于Unix 域套接字,因为它允许数据在内核空间直接传递,而无需经过网络协议栈。

3. 内核的“魔法”:传递过程剖析

当使用sendmsg和recvmsg系统调用时,内核在幕后完成了关键工作。

  • 发送端 (sendmsg):

    1. 获取文件对象:内核找到发送方要传递的fd所对应的struct file对象。

    2. 打包指针:内核将这个struct file对象的指针打包进特殊的辅助数据中,并附加到发送消息上。

    3. 增加引用计数:调用get_file(),增加该struct file的引用计数,确保在接收方使用期间文件不会被关闭。

  • 接收端 (recvmsg):

    1. 解析并创建新fd:内核从接收到的辅助数据中解析出struct file指针。

    2. 分配新fd:在接收进程的文件描述符表中,找到一个空闲的位置(新的fd数字)。

    3. 建立映射:将新分配的fd数字指向发送端传来的同一个struct file对象。

    4. 传递到用户态:内核将新的fd数字作为recvmsg的结果返回给接收进程。

4. 关键细节与注意事项
  • fd数字可能不同:发送端的fd数字是3,接收端可能是7。

  • 必须发送至少1字节数据:在Linux上,通过SOCK_STREAM类型的Unix域套接字传递时,sendmsg调用中必须伴随至少1字节的普通数据,否则辅助数据可能不会被发送。

  • 传递的是“打开文件描述”的引用:准确地说,传递的是对打开文件描述(open file description)的引用。双方共享文件偏移量、打开模式等状态。

  • 父子进程默认继承:fork()创建的子进程会继承父进程的文件描述符表,这是最简单直接的传递方式。

完整 Demo 代码

代码结构
  • 父进程:创建一个 Unix 域套接字对 (socketpair),然后fork()子进程

  • 子进程:打开一个文件,将 fd 通过套接字发送给父进程

  • 父进程:接收 fd,并从中读取内容验证

#include <sys/socket.h> #include <sys/un.h> #include <sys/types.h> #include <sys/wait.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> // ============ 发送端:通过 socket 发送文件描述符 ============ int send_fd(int socket_fd, int fd_to_send) { // 必须至少发送 1 字节数据(某些系统要求)[reference:4] char dummy = 0; struct iovec iov = { .iov_base = &dummy, .iov_len = 1 }; // 分配存放辅助数据的缓冲区(需满足对齐要求)[reference:5] union { char buf[CMSG_SPACE(sizeof(int))]; struct cmsghdr align; } control_msg; memset(control_msg.buf, 0, sizeof(control_msg.buf)); struct msghdr msg = { .msg_name = NULL, .msg_namelen = 0, .msg_iov = &iov, .msg_iovlen = 1, .msg_control = control_msg.buf, .msg_controllen = sizeof(control_msg.buf), .msg_flags = 0 }; // 设置 SCM_RIGHTS 辅助消息 struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); cmsg->cmsg_level = SOL_SOCKET; cmsg->cmsg_type = SCM_RIGHTS; cmsg->cmsg_len = CMSG_LEN(sizeof(int)); // 将待传递的 fd 复制到辅助数据中 *(int *)CMSG_DATA(cmsg) = fd_to_send; ssize_t ret = sendmsg(socket_fd, &msg, 0); if (ret == -1) { perror("sendmsg"); return -1; } return 0; } // ============ 接收端:通过 socket 接收文件描述符 ============ int recv_fd(int socket_fd) { char dummy; struct iovec iov = { .iov_base = &dummy, .iov_len = 1 }; union { char buf[CMSG_SPACE(sizeof(int))]; struct cmsghdr align; } control_msg; memset(control_msg.buf, 0, sizeof(control_msg.buf)); struct msghdr msg = { .msg_name = NULL, .msg_namelen = 0, .msg_iov = &iov, .msg_iovlen = 1, .msg_control = control_msg.buf, .msg_controllen = sizeof(control_msg.buf), .msg_flags = 0 }; ssize_t ret = recvmsg(socket_fd, &msg, 0); if (ret == -1) { perror("recvmsg"); return -1; } // 遍历辅助消息,查找 SCM_RIGHTS 类型 struct cmsghdr *cmsg = CMSG_FIRSTHDR(&msg); for (; cmsg != NULL; cmsg = CMSG_NXTHDR(&msg, cmsg)) { if (cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) { int received_fd = *(int *)CMSG_DATA(cmsg); return received_fd; } } fprintf(stderr, "No SCM_RIGHTS message received\n"); return -1; } // ============ 主程序:父子进程演示 ============ int main() { int sock_fds[2]; // 1. 创建 Unix 域套接字对(用于父子进程通信)[reference:9] if (socketpair(AF_UNIX, SOCK_STREAM, 0, sock_fds) == -1) { perror("socketpair"); exit(EXIT_FAILURE); } pid_t pid = fork(); if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); } if (pid == 0) { // ===== 子进程(发送方) ===== close(sock_fds[0]); // 关闭不需要的一端 // 打开一个文件(以读写方式) int fd = open("test.txt", O_RDWR | O_CREAT, 0644); if (fd == -1) { perror("child: open"); exit(EXIT_FAILURE); } // 写入一些测试数据 const char *msg = "Hello from child!\n"; write(fd, msg, strlen(msg)); // 将文件描述符发送给父进程 printf("Child: sending fd=%d to parent\n", fd); if (send_fd(sock_fds[1], fd) == -1) { exit(EXIT_FAILURE); } // 注意:子进程可以继续使用这个 fd,也可以关闭它 // 即使子进程关闭,父进程收到的 fd 仍然有效(因为内核引用计数) close(fd); close(sock_fds[1]); printf("Child: done\n"); exit(EXIT_SUCCESS); } else { // ===== 父进程(接收方) ===== close(sock_fds[1]); // 关闭不需要的一端 // 接收子进程传来的文件描述符 int received_fd = recv_fd(sock_fds[0]); if (received_fd == -1) { exit(EXIT_FAILURE); } printf("Parent: received fd=%d\n", received_fd); // 从收到的 fd 中读取数据(验证权限和偏移量共享) char buf[256]; ssize_t n = read(received_fd, buf, sizeof(buf) - 1); if (n > 0) { buf[n] = '\0'; printf("Parent: read from received fd: %s", buf); } // 父进程也可以写入(因为权限与子进程相同) const char *parent_msg = "Hello from parent!\n"; write(received_fd, parent_msg, strlen(parent_msg)); close(received_fd); close(sock_fds[0]); // 等待子进程结束 wait(NULL); printf("Parent: done\n"); } return 0; }

关键点说明

要点说明
必须发送至少 1 字节数据sendmsg()要求至少有 1 字节的普通数据(iovec),否则某些系统上辅助数据不会被发送
使用 CMSG_宏操作*必须用CMSG_FIRSTHDR、CMSG_DATA、CMSG_SPACE等宏来操作辅助数据,以保证内存对齐正确
socketpairvs 命名套接字socketpair()适用于父子/亲缘进程-;不相关进程可使用命名 Unix 域套接字 (bind+connect)
引用计数机制即使发送方关闭了原始 fd,接收方收到的 fd 仍然有效,因为内核中struct file的引用计数增加了
权限与偏移量共享接收方获得的 fd 与发送方指向同一个内核文件对象,因此权限相同、文件偏移量也共享

编译与运行

g++ -std=c++11 -o fd_pass fd_pass.cpp ./fd_pass

预期输出:

Child: sending fd=3 to parent Child: done Parent: received fd=4 Parent: read from received fd: Hello from child! Parent: done

注意:接收方得到的 fd 数值(如 4)与发送方的 fd 数值(如 3)可能不同,这是正常的——它们只是各自进程描述符表中的不同索引,但指向内核中同一个文件对象。

扩展到不相关进程

如果需要在不相关的进程间传递 fd,只需将socketpair()替换为命名 Unix 域套接字:

  • 服务端:socket(AF_UNIX, SOCK_STREAM, 0)→bind()到固定路径 →listen()→accept()

  • 客户端:socket(AF_UNIX, SOCK_STREAM, 0)→connect()到同一路径

其余send_fd/recv_fd逻辑完全一致。

SCM_RIGHTS详解

SCM_RIGHTS是 Linux 中通过 Unix 域套接字传递文件描述符的“标准方法”-。它的核心是传递一个指向内核中打开文件描述(struct file)的引用,而非一个简单的整数。

📖 定义:它是什么?

SCM_RIGHTS是一种辅助消息(Ancillary Message)类型-,允许一个进程向另一个进程发送一组打开的文件描述符。

  • 传递的是引用:接收方获得的是对同一个内核文件对象的新引用,相当于执行了一次dup()。

  • 文件描述符数字可能不同:发送方的 fd 是 3,接收方收到的可能是。

⚙️ 工作原理:它如何工作?

  1. 准备阶段:通信双方通过socketpair()创建一对连接的 Unix 域套接字-。

  2. 发送端:使用sendmsg()系统调用-。

    • 在struct msghdr中设置辅助数据,指定类型为SCM_RIGHTS。

    • 将要传递的 fd 的整数值放入辅助数据中。

    • 内核通过fget_raw()找到 fd 对应的struct file指针,增加其引用计数,并将指针打包进消息。

  3. 接收端:使用recvmsg()系统调用-。

    • 内核从消息中解析出struct file指针。

    • 在接收进程的 fd 表中分配一个空闲位置,指向同一个struct file。

    • 将新的 fd 数字返回给用户态程序。

💻 核心步骤与代码示例

发送端伪代码:

// 1. 创建并连接socket int sock = socket(AF_UNIX, SOCK_STREAM, 0); connect(sock, ...); // 2. 打开要传递的文件 int fd_to_send = open("myfile.txt", O_RDWR); // 3. 准备辅助数据 (Ancillary Data) struct msghdr msg = {0}; struct cmsghdr *cmsg; char buf[CMSG_SPACE(sizeof(int))]; msg.msg_control = buf; msg.msg_controllen = sizeof(buf); cmsg = CMSG_FIRSTHDR(&msg); cmsg->cmsg_level = SOL_SOCKET; cmsg->cmsg_type = SCM_RIGHTS; cmsg->cmsg_len = CMSG_LEN(sizeof(int)); *(int *)CMSG_DATA(cmsg) = fd_to_send; // 将fd放入数据部分 // 4. 必须发送至少1字节的普通数据 msg.msg_iov = &iov; msg.msg_iovlen = 1; iov.iov_base = "x"; iov.iov_len = 1; // 5. 发送 sendmsg(sock, &msg, 0);

接收端伪代码:

// 1. 创建并监听socket int listen_sock = socket(AF_UNIX, SOCK_STREAM, 0); bind(listen_sock, ...); int conn_sock = accept(listen_sock, ...); // 2. 准备接收辅助数据 struct msghdr msg = {0}; struct cmsghdr *cmsg; char buf[CMSG_SPACE(sizeof(int))]; msg.msg_control = buf; msg.msg_controllen = sizeof(buf); // 3. 接收数据(至少1字节) char data[1]; struct iovec iov = {data, sizeof(data)}; msg.msg_iov = &iov; msg.msg_iovlen = 1; recvmsg(conn_sock, &msg, 0); // 4. 解析出文件描述符 cmsg = CMSG_FIRSTHDR(&msg); if (cmsg && cmsg->cmsg_level == SOL_SOCKET && cmsg->cmsg_type == SCM_RIGHTS) { int received_fd = *(int *)CMSG_DATA(cmsg); // 提取fd[reference:17] // 现在可以使用 received_fd 了 }

⚠️ 重要注意事项

  • 必须发送数据:在 Linux 的SOCK_STREAM套接字上,sendmsg()必须至少发送 1 字节的普通数据,否则辅助数据可能不会被发送。在SOCK_DGRAM上虽非必须,但为了可移植性也建议这样做。

  • 数量限制:单次sendmsg()调用能传递的 fd 数量有限,由内核常量SCM_MAX_FD定义,通常为253。

  • 缓冲区大小:接收端的辅助数据缓冲区必须足够大,否则超出部分的 fd 会被自动关闭。

  • 资源限制:如果接收的 fd 数量会导致进程超过RLIMIT_NOFILE限制,超出的 fd 会被自动关闭。

🔒 安全与高级话题

  • 权限检查:Linux 安全模块(如 Smack)可能会在接收端检查security_file_receive(),拒绝则可能截断消息-。

  • 自身安全:SCM_RIGHTS机制本身也曾出现过漏洞,如CVE-2019-5596(fd 泄漏)-和CVE-2023-52656(与io_uring的交互问题)。

  • 现代编程:在 Rust 中,推荐使用unix_ancillary等库,它们提供了更安全、自动管理生命周期的 API。

💎 总结

SCM_RIGHTS是 Unix 域套接字提供的一个强大特性,它使得在进程间安全、高效地传递文件描述符成为可能。flatpak、D-Bus-等众多系统组件都依赖此机制来实现高级的进程间通信与资源管理。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询