SerenityOS recvfd(2) 手册解析:通过本地域套接字带外传递文件描述符
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
本篇围绕 Serenity OS 系统调用手册页 recvfd.md 展开,完整覆盖其函数原型、options标志、返回值与错误码语义,并结合内核源码 sys$recvfd 实现 与 LocalSocket::recvfd 的实现细节,说明 fd 如何以带外(out-of-band)方式在本地套接字两端传递。读完后你将掌握:如何在两个 SerenityOS 进程间通过AF_LOCAL套接字传递打开的文件描述符、recvfd()的非阻塞语义与全部错误码的内核出处、以及它与配对调用sendfd()的协作方式。
1. 功能概述:跨进程传递打开的 fd
recvfd()用于从经sockfd连接的本地套接字对端进程接收一个已打开的文件描述符(file descriptor)。它与sendfd()构成一对对称系统调用:
| 调用 | 原型 | 语义 | 返回 |
|---|---|---|---|
sendfd | int sendfd(int sockfd, int fd); | 发送一个打开的 fd 给本地套接字对端 | 成功返回 0,失败返回 -1 |
recvfd | int recvfd(int sockfd, int options); | 接收对端发来的 fd | 成功返回新分配的非负 fd,失败返回 -1 |
两者都要求sockfd指向本地域(local domain)套接字,且 fd 的传递是带外的:走独立的 fd 队列,不影响套接字常规数据流(read/write缓冲区)的内容与顺序。这一机制源自 Plan 9 from User Space,SerenityOS 将其纳入自身系统调用集。
1.1 与常规数据流的分离
这一点在内核结构上可以直接印证。LocalSocket 为每对连接维护了两个DoubleBuffer数据缓冲(m_for_client/m_for_server,见 LocalSocket 构造函数)用于常规读写;而sendfd/recvfd使用的m_fds_for_client/m_fds_for_server两个Vector<NonnullRefPtr<OpenFileDescription>>队列是完全独立的。从源码结构看,两个通道的阻塞判定互不影响,因此传 fd 不会干扰同一连接上的普通字节流通信。
1.2 系统调用层与 LibC 封装
recvfd在系统调用表中的注册条目见 Syscall.h:
S(recvfd, NeedsBigProcessLock::No)NeedsBigProcessLock::No表明该调用不要求持有进程大锁,执行路径较短。用户态入口位于 LibC 的 sys/socket.cpp:
int recvfd(int sockfd, int options) { int rc = syscall(SC_recvfd, sockfd, options); ... }原型声明见 sys/socket.h。与手册页一致,需要#include <sys/socket.h>后即可调用。
2. Synopsis 与 options 参数
手册页给出的函数原型为:
#include <sys/socket.h> int recvfd(int sockfd, int options);options是一个位掩码,目前手册页仅定义了一个标志:
O_CLOEXEC:接收到的新 fd 应设置 close-on-exec 标志,即调用进程后续exec时该 fd 自动关闭(exec(2) 手册页交叉引用的执行语义)。
在内核实现中,该标志的解析只有一步:sys$recvfd 中若options & O_CLOEXEC,则向新 fd 写入FD_CLOEXEC标志位,随后连同收到的OpenFileDescription一起挂入调用进程的 fd 表。换言之,recvfd()成功返回的 fd 是调用进程 fd 表中新分配的最小空闲号,它引用的是对端发来的同一份打开文件描述对象(共享打开状态,如文件偏移、异步信号状态)。
3. 非阻塞语义与接收流程
手册页明确指出:recvfd()是非阻塞调用,若套接字队列中没有等待接收的 fd 则会失败(返回EAGAIN)。内核侧的完整调用链为:
- 前置检查(sys$recvfd):
require_promise(Pledge::recvfd)——进程必须已pledge了recvfd能力,否则拒绝(EDEFAULT);open_file_description(sockfd)失败则返回对应错误(fd 未打开即EBADF);- 不是套接字 →
ENOTSOCK;不是本地套接字 →EAFNOSUPPORT。
- 预分配 fd 槽位:先在调用进程 fd 表中
allocate()一个 fd 号,为即将收到的描述符预留位置。 - 出队:LocalSocket::recvfd 在套接字互斥锁下按当前 fd 的角色(
Role::Connected或Role::Accepted)选出对应的recvfd队列;队列为空则返回EAGAIN,否则queue.take_first()取出队首的OpenFileDescription并转移所有权。 - 落入 fd 表:回到 sys$recvfd,将描述符连同
O_CLOEXEC转换后的标志写入已分配的 fd 槽位,返回 fd 号。
角色(role)与队列的对应关系由 recvfd_queue_for / sendfd_queue_for 决定:客户端角色(Connected)从m_fds_for_client收、往m_fds_for_server发;被接受端角色(Accepted)反之。两个方向各自独立排队,互不干扰。
4. 返回值与错误码(附内核出处)
成功时recvfd()返回收到的 fd(非负整数);失败返回 -1 并设置errno。手册页列出的五个错误在内核中的产生位置如下:
| errno | 手册页含义 | 内核产生位置 |
|---|---|---|
EBADF | sockfd不是打开的 fd | open_file_description(sockfd)解析失败,见 sys$recvfd |
ENOTSOCK | sockfd不指向套接字 | !socket_description->is_socket(),见 sys$recvfd |
EAFNOSUPPORT | sockfd不是本地域套接字 | !socket.is_local(),见 sys$recvfd |
EINVAL | sockfd不是已连接/已接受的套接字 | LocalSocket::recvfd 中 role 既非Connected也非Accepted |
EAGAIN | 该套接字上没有排队等待的 fd | LocalSocket::recvfd 队列为空时 |
与之配对的sendfd()(手册页见 sendfd.md)还额外定义了两个错误:ENOTCONN(sys$sendfd 中!socket.is_connected())与EBUSY(LocalSocket::sendfd 中对端待收队列超过 128 个 fd 时拒绝,源码中该上限标注为 FIXME、尚待确定正式的限制策略)。这解释了为何recvfd()是非阻塞的:队列无界增长不受鼓励,发送端满 128 即快速失败,接收端则以EAGAIN表示"当前没有可取的 fd"。
5. 典型使用模式
基于前文语义,一次 fd 传递的完整流程示意如下(以AF_LOCAL流式套接字为例):
#include <sys/socket.h> #include <fcntl.h> /* 接收方:先建立本地连接(bind/listen/accept 或 connect 完成后再取 fd) */ int client = /* accept() 返回的已接受套接字,或 connect() 后的对端套接字 */; int fd = recvfd(client, O_CLOEXEC); /* 要求 pledge 过 "recvfd" */ if (fd < 0) { /* errno: EBADF/ENOTSOCK/EAFNOSUPPORT/EINVAL/EAGAIN */ } /* 发送方:在与 client 同一连接的另一个 fd 上执行 sendfd(peer_fd, fd_to_pass); 要求 pledge 过 "sendfd";返回 0 表示入队成功。 */要点归纳:
- 两端必须先完成本地套接字的连接/接受,使角色达到
Connected或Accepted,否则会收到EINVAL; recvfd()返回EAGAIN时通常意味着对端尚未sendfd或已取空队列,可稍后重试(该调用本身不会挂起);- 传过来的是"打开的文件描述符",接收方得到的是一个全新 fd 号但共享同一打开文件状态,可直接读写、
dup或再传给第三方。
6. 历史与延伸阅读
recvfd()最早由 Plan 9 from User Space 引入,SerenityOS 保留了该命名与语义并扩展了options位掩码(当前仅O_CLOEXEC)。相关手册页与源码入口:
- 配对调用手册页:sendfd(2)
- 系统调用实现:Kernel/Syscalls/sendfd.cpp(
sys$sendfd与sys$recvfd同文件实现) - 内核套接字层队列:Kernel/Net/LocalSocket.cpp
- 系统调用注册表:Kernel/API/Syscall.h
- 用户态封装:Userland/Libraries/LibC/sys/socket.cpp
适用前提:以上行为描述基于当前仓库内核与 LibC 的实际实现;sendfd端 128 个 fd 的队列上限是源码中明确标注待完善的临时值(FIXME),后续版本可能调整,以仓库当时的 LocalSocket::sendfd 为准。
【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考