☰
Serenity OS sendfd 系统调用详解:通过本地套接字传递文件描述符
2026/10/18 10:13:14 网站建设 项目流程

Serenity OS sendfd 系统调用详解:通过本地套接字传递文件描述符

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

本文以 Serenity OS 手册页sendfd(2)为主体,完整解读sendfd()系统调用的接口语义、返回值与错误码,并结合内核源码(Kernel/Syscalls/sendfd.cpp、Kernel/Net/LocalSocket.cpp)剖析描述符队列、容量上限与权限校验的底层实现,同时对照接收端recvfd()与用户态封装,帮助读者掌握在 Serenity OS 中跨进程传递文件描述符的完整机制与可运行语义。

一、接口定义与基本语义

sendfd的官方手册页位于 Base/usr/share/man/man2/sendfd.md,其接口定义如下:

#include <sys/socket.h> int sendfd(int sockfd, int fd);

该函数用于向通过sockfd连接的本机(local domain)套接字对端发送一个已打开的文件描述符。手册页给出的语义要点有两条,它们直接决定了使用方式:

  1. 非阻塞调用:sendfd()不会等待对端接收,如果等待对端接收的描述符数量已经过多,调用直接失败;
  2. 带外传递:文件描述符通过带外(out-of-band)通道发送,不影响套接字的常规数据流——也就是说,描述符传递与read/write传输的普通字节流相互独立。

在用户态 LibC 中,该函数的声明位于 Userland/Libraries/LibC/sys/socket.h(第 37–38 行,与recvfd相邻),其实现位于 Userland/Libraries/LibC/sys/socket.cpp:

int sendfd(int sockfd, int fd) { int rc = syscall(SC_sendfd, sockfd, fd); __RETURN_WITH_ERRNO(rc, rc, -1); }

从源码结构看,sendfd并不是 POSIX 标准接口,而是 Serenity OS 通过系统调用号SC_sendfd(在 Kernel/API/Syscall.h 中定义)直接暴露的私有系统调用。这一点在 C++ 层的跨平台封装中体现得很清楚:Core::LocalSocket在 Serenity 内核上调用sendfd,而在 Linux / Hurd / BSD / Haiku 上则退化为sendmsg+SCM_RIGHTS辅助消息的方式实现同等能力,见 Userland/Libraries/LibCore/Socket.cpp。

二、返回值与错误码

手册页对返回值的定义:成功发送时返回 0;失败时返回 -1,并设置errno。

错误码手册页含义内核中的触发点(源码佐证)
EBADFsockfd或fd不是已打开的文件描述符系统调用入口处open_file_description()按 fd 查找描述符失败
ENOTSOCKsockfd不指向一个套接字!socket_description->is_socket()
EAFNOSUPPORTsockfd不是 local domain 套接字!socket.is_local()
ENOTCONNsockfd指向的套接字未连接!socket.is_connected()
EINVALsockfd不是已连接或已 accept 的套接字LocalSocket::sendfd()中端点角色不是Connected或Accepted
EBUSY对端已有过多文件描述符在等待接收发送队列长度超过 128

注意ENOTCONN与EINVAL的分工:前者检查“套接字是否建立了连接”这一网络状态,后者检查“这个套接字端点在连接中的角色是否合法”——只有客户端连接端(Connected)和服务端已接受端(Accepted)才允许发送描述符,见下文实现分析。

三、内核实现:校验链与描述符队列

3.1 系统调用入口的校验顺序

系统调用处理函数Process::sys$sendfd位于 Kernel/Syscalls/sendfd.cpp,其校验顺序与手册页错误码一一对应:

ErrorOr<FlatPtr> Process::sys$sendfd(int sockfd, int fd) { VERIFY_NO_PROCESS_BIG_LOCK(this); TRY(require_promise(Pledge::sendfd)); // 1. 安全能力检查 auto socket_description = TRY(open_file_description(sockfd)); // 2. 失败 => EBADF if (!socket_description->is_socket()) return ENOTSOCK; // 3. 必须是套接字 auto& socket = *socket_description->socket(); if (!socket.is_local()) return EAFNOSUPPORT; // 4. 必须是 local 域 if (!socket.is_connected()) return ENOTCONN; // 5. 必须已连接 auto passing_description = TRY(open_file_description(fd)); // 校验 fd => EBADF auto& local_socket = static_cast<LocalSocket&>(socket); TRY(local_socket.sendfd(*socket_description, move(passing_description))); return 0; }

几个值得注意的实现事实:

  • Pledge 能力门禁:入口处的require_promise(Pledge::sendfd)表明,进程必须事先声明sendfd能力(Pledge 机制),否则调用会被拒绝。这意味着即使拿到合法套接字,未声明该能力的程序也无法传递描述符——这是 Serenity OS 安全模型的典型体现。
  • 描述符以引用传递:通过open_file_description(fd)取到OpenFileDescription的引用计数指针后move进套接字队列,因此传递的是“同一个打开文件”,对端获得的是与发送端共享打开状态的描述符副本,而非文件路径。
  • 入口先校验sockfd,再校验待传递的fd,与手册页中EBADF同时覆盖两个参数的描述一致。

3.2 LocalSocket 中的双端队列与 EBUSY 上限

真正执行发送的是LocalSocket::sendfd(),位于 Kernel/Net/LocalSocket.cpp:

ErrorOr<void> LocalSocket::sendfd(OpenFileDescription const& socket_description, NonnullRefPtr<OpenFileDescription> passing_description) { MutexLocker locker(mutex()); auto role = this->role(socket_description); if (role != Role::Connected && role != Role::Accepted) return set_so_error(EINVAL); auto& queue = sendfd_queue_for(socket_description); // FIXME: Figure out how we should limit this properly. if (queue.size() > 128) return set_so_error(EBUSY); SOCKET_TRY(queue.try_append(move(passing_description))); return {}; }

从源码结构看,其工作机制可以归纳为三点:

  1. 角色决定队列归属。本地套接字两端各有角色:连接发起方为Connected,服务端accept()后的端点为Accepted,由 Kernel/Net/LocalSocket.h 中的两个成员向量分别承载:

    Vector<NonnullRefPtr<OpenFileDescription>> m_fds_for_client; Vector<NonnullRefPtr<OpenFileDescription>> m_fds_for_server;

    sendfd_queue_for()(Kernel/Net/LocalSocket.cpp)按“发送方是谁”选择队列:客户端(Connected)发送时写入m_fds_for_server,服务端(Accepted)发送时写入m_fds_for_client——即描述符总是进入对端的接收队列。recvfd_queue_for()(同文件第 488–497 行)做对称映射,保证发送与接收落在同一队列上。

  2. 128 的硬上限就是手册页中EBUSY的由来。sendfd是非阻塞调用,当某方向上已排队等待对端接收的描述符数量超过 128 个时立即返回EBUSY。源码中的FIXME: Figure out how we should limit this properly注释说明这一上限目前是经验值,后续可能调整,读者在依赖该阈值时应以当前仓库源码为准。

  3. 互斥锁保护。整个“查角色—选队列—判上限—入队”过程在套接字私有互斥锁内完成(函数开头的MutexLocker locker(mutex()),且两个queue_for辅助函数都带有VERIFY(mutex().is_exclusively_locked_by_current_thread())),保证并发sendfd时队列长度判断不会竞争。

四、接收端对照:recvfd 与队列的消费方式

sendfd必须与recvfd(2) 配对使用,手册页“See also”一节即指向它。接收端语义与本节实现直接呼应:

  • 原型int recvfd(int sockfd, int options),成功时返回收到的文件描述符(非负整数),失败返回 -1;
  • 同样是非阻塞调用:队列为空时返回EAGAIN,对应LocalSocket::recvfd()(Kernel/Net/LocalSocket.cpp)中queue.is_empty()分支;
  • options支持O_CLOEXEC:在内核实现 Kernel/Syscalls/sendfd.cpp 中,系统先预留一个 fd 槽位(m_fds...allocate()),再从队列take_first()取出描述符装入,并按选项设置FD_CLOEXEC标志,使收到的 fd 在exec时自动关闭;
  • 错误集与sendfd略有差异:recvfd没有ENOTCONN(不要求连接建立检查,只做 local 域与角色校验),空队列时是EAGAIN而非EBUSY。

此外内核还提供批量的LocalSocket::recvfds()(同文件第 538–555 行),一次性从队列最多取出n个描述符并逐个回调处理,从源码结构看,这为“一次exec传递多个 fd”这类场景提供了基础。

五、用户态调用链与用法示例

5.1 C++ 层封装

在 Serenity OS 的 C++ 标准库中,sendfd/recvfd的调用链为:

  • Core::System::sendfd()/Core::System::recvfd():对 LibC 薄封装并将负返回值转成Error,见 Userland/Libraries/LibCore/System.cpp;
  • Core::LocalSocket::send_fd(int fd)/receive_fd(int flags):对象化接口,在 Serenity 上转发到上述系统封装,在其他 Unix 平台退化为SCM_RIGHTS实现,见 Userland/Libraries/LibCore/Socket.cpp。

5.2 典型用法(依据手册页语义示例)

结合sendfd(2)与recvfd(2)手册页描述的语义,一个最小用法示意如下:

// 发送端 int sock[2]; socketpair(AF_LOCAL, SOCK_STREAM, 0, sock); // 建立本地对端连接 /* ... 由对端 connect/accept 后,将 fd 传给本地对端 ... */ int open_fd = open("/etc/motd", O_RDONLY); if (sendfd(sock[0], open_fd) < 0) { perror("sendfd"); // 可能为 EBUSY:对端接收队列已满(超过 128) } // 接收端 int received = recvfd(sock[1], O_CLOEXEC); // 无排队 fd 时返回 -1 / EAGAIN if (received < 0) { perror("recvfd"); }

使用时需牢记手册页给出的两个约束:其一,sendfd只支持local 域套接字,AF_UNIX/AF_INET等其他域会收到EAFNOSUPPORT;其二,由于两个调用都是非阻塞的,接收方应先确认对方已发送(例如通过数据流带一个信号字节)再调用recvfd,否则拿到的是EAGAIN。

六、历史渊源与适用前提

手册页 History 一节指出:sendfd()最早由Plan 9 from User Space引入。这一渊源解释了它的接口风格——以“描述符作为带外对象在本地对端之间传递”为核心抽象,而非 POSIXSCM_RIGHTS的辅助消息风格。

适用前提小结(均以当前仓库为准):

  • 仅在 Serenity OS 本地套接字(AF_LOCAL)场景可用,ENOTSOCK/EAFNOSUPPORT会拦截一切非本地域尝试;
  • 发送端与接收端进程都需通过 Pledge 声明对应能力(sendfd/recvfd);
  • 单向描述符队列长度超过128时发送方立即收到EBUSY,属于源码中的现行限制而非手册页承诺的长期契约;
  • 跨平台移植时,Core::LocalSocket提供了基于SCM_RIGHTS的等价实现路径,但sendfd系统调用本身为 Serenity 私有接口。

参考资料

  • 手册页:sendfd(2)、recvfd(2)
  • 系统调用实现:Kernel/Syscalls/sendfd.cpp
  • 本地套接字队列实现:Kernel/Net/LocalSocket.cpp、Kernel/Net/LocalSocket.h
  • LibC 封装:Userland/Libraries/LibC/sys/socket.cpp、Userland/Libraries/LibC/sys/socket.h
  • C++ 层封装:Userland/Libraries/LibCore/System.cpp、Userland/Libraries/LibCore/Socket.cpp
  • 系统调用号定义:Kernel/API/Syscall.h

【免费下载链接】serenityThe Serenity Operating System 🐞项目地址: https://gitcode.com/GitHub_Trending/se/serenity

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询