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)套接字对端发送一个已打开的文件描述符。手册页给出的语义要点有两条,它们直接决定了使用方式:
- 非阻塞调用:
sendfd()不会等待对端接收,如果等待对端接收的描述符数量已经过多,调用直接失败; - 带外传递:文件描述符通过带外(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。
| 错误码 | 手册页含义 | 内核中的触发点(源码佐证) |
|---|---|---|
EBADF | sockfd或fd不是已打开的文件描述符 | 系统调用入口处open_file_description()按 fd 查找描述符失败 |
ENOTSOCK | sockfd不指向一个套接字 | !socket_description->is_socket() |
EAFNOSUPPORT | sockfd不是 local domain 套接字 | !socket.is_local() |
ENOTCONN | sockfd指向的套接字未连接 | !socket.is_connected() |
EINVAL | sockfd不是已连接或已 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 {}; }从源码结构看,其工作机制可以归纳为三点:
角色决定队列归属。本地套接字两端各有角色:连接发起方为
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 行)做对称映射,保证发送与接收落在同一队列上。128 的硬上限就是手册页中
EBUSY的由来。sendfd是非阻塞调用,当某方向上已排队等待对端接收的描述符数量超过 128 个时立即返回EBUSY。源码中的FIXME: Figure out how we should limit this properly注释说明这一上限目前是经验值,后续可能调整,读者在依赖该阈值时应以当前仓库源码为准。互斥锁保护。整个“查角色—选队列—判上限—入队”过程在套接字私有互斥锁内完成(函数开头的
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),仅供参考