ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

进程间传递文件描述符

进程间传递文件描述符 进程A将打开的文件描述符传给另外一个进程B那么进程B和进程A对此文件有相同的权限了么是的进程B会获得与进程A对该文件的相同权限。这背后的核心原理是传递的并非文件描述符这个整数值本身而是内核中指向同一个打开文件对象的“通行证”。核心机制内核级的“指针”复制文件描述符的本质在Linux中文件描述符fd对于每个进程是独立的它只是进程内部文件描述符表的一个索引。进程A的fd3和进程B的fd3完全不相关。传递的真正对象当通过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_RIGHTSSCM_RIGHTS是一个特殊的消息类型它告诉内核“请特殊处理这个消息里的数据它们不是普通字节而是需要传递的文件描述符。”这个机制依赖于Unix 域套接字因为它允许数据在内核空间直接传递而无需经过网络协议栈。3. 内核的“魔法”传递过程剖析当使用sendmsg和recvmsg系统调用时内核在幕后完成了关键工作。发送端 (sendmsg)获取文件对象内核找到发送方要传递的fd所对应的struct file对象。打包指针内核将这个struct file对象的指针打包进特殊的辅助数据中并附加到发送消息上。增加引用计数调用get_file()增加该struct file的引用计数确保在接收方使用期间文件不会被关闭。接收端 (recvmsg)解析并创建新fd内核从接收到的辅助数据中解析出struct file指针。分配新fd在接收进程的文件描述符表中找到一个空闲的位置新的fd数字。建立映射将新分配的fd数字指向发送端传来的同一个struct file对象。传递到用户态内核将新的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 域套接字 (bindconnect)引用计数机制即使发送方关闭了原始 fd接收方收到的 fd 仍然有效因为内核中struct file的引用计数增加了权限与偏移量共享接收方获得的 fd 与发送方指向同一个内核文件对象因此权限相同、文件偏移量也共享编译与运行g -stdc11 -o fd_pass fd_pass.cpp ./fd_pass预期输出Child: sending fd3 to parent Child: done Parent: received fd4 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接收方收到的可能是。⚙️ 工作原理它如何工作准备阶段通信双方通过socketpair()创建一对连接的 Unix 域套接字-。发送端使用sendmsg()系统调用-。在struct msghdr中设置辅助数据指定类型为SCM_RIGHTS。将要传递的 fd 的整数值放入辅助数据中。内核通过fget_raw()找到 fd 对应的struct file指针增加其引用计数并将指针打包进消息。接收端使用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-5596fd 泄漏-和CVE-2023-52656与io_uring的交互问题。现代编程在 Rust 中推荐使用unix_ancillary等库它们提供了更安全、自动管理生命周期的 API。 总结SCM_RIGHTS是 Unix 域套接字提供的一个强大特性它使得在进程间安全、高效地传递文件描述符成为可能。flatpak、D-Bus-等众多系统组件都依赖此机制来实现高级的进程间通信与资源管理。
返回列表