尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SerenityOS accept(2) 系统调用完全指南:从 man 手册到内核源码的服务器连接接受机制解析

SerenityOS accept(2) 系统调用完全指南:从 man 手册到内核源码的服务器连接接受机制解析 SerenityOS accept(2) 系统调用完全指南从 man 手册到内核源码的服务器连接接受机制解析【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenityaccept(2)是 SerenityOS 网络编程中服务端套接字流程socket → bind → listen → accept的收尾环节负责从监听队列中取出一条客户端连接并为它分配一个全新的、唯一文件描述符。本文以系统自带手册 Base/usr/share/man/man2/accept.md 为骨架结合用户态 LibC 封装、内核sys$accept4实现、Socket内核对象以及内核级测试用例完整讲解其语义、阻塞/非阻塞行为、错误码与底层原理帮助你在 SerenityOS 上编写正确的并发服务器代码。1. 函数签名与头文件accept(2)的原型定义在sys/socket.h中与 POSIX 标准保持一致#include sys/socket.h int accept(int sockfd, sockaddr* addr, socklen_t* addrlen);三个参数的含义参数含义sockfd服务端监听套接字的文件描述符必须已经过socket(2)创建、bind(2)绑定地址、listen(2)进入监听状态addr指向sockaddr的指针用于接收客户端地址传nullptr表示不关心对端地址addrlen指向socklen_t的指针调用前应初始化为addr缓冲区能容纳的最大字节数返回后会被改写为实际写入的地址长度手册明确描述了它的核心行为该函数会阻塞直到至少有一个客户端尝试连接服务器其余客户端会排队等待被接受with other clients being queued up for accepting。成功时内核会为每个被接受的连接创建一个携带全新文件描述符的新套接字并返回该描述符如果addr非空其中会写入新连接客户端的地址如果addrlen非空调用前它表示addr缓冲区允许写入的最大长度调用后被覆盖为实际写入addr的地址长度。2. 用户态封装LibC 中的 accept 与 accept4在 SerenityOS 的用户态 C 库中accept并非直接发起系统调用而是薄薄一层封装。查看 Userland/Libraries/LibC/sys/socket.cpp 可以看到// https://pubs.opengroup.org/onlinepubs/9699919799/functions/accept.html int accept(int sockfd, sockaddr* addr, socklen_t* addrlen) { __pthread_maybe_cancel(); return accept4(sockfd, addr, addrlen, 0); } int accept4(int sockfd, sockaddr* addr, socklen_t* addrlen, int flags) { Syscall::SC_accept4_params params { addr, addrlen, sockfd, flags }; int rc syscall(SC_accept4, params); __RETURN_WITH_ERRNO(rc, rc, -1); }这里有三个值得注意的细节accept内部实际调用的是accept4flags固定传0。内核只暴露SC_accept4这一个系统调用号见 Kernel/API/Syscall.h 中的struct SC_accept4_paramsaccept是它在 C 库层面的兼容包装调用前会执行__pthread_maybe_cancel()使accept成为可被线程取消的取消点这符合 POSIX 对阻塞类函数的约定返回值统一走__RETURN_WITH_ERRNO宏负数内核错误码在此处转换为-1返回并把errno置为对应值因此用户代码中应当检查 -1而非 0。与 Linux 类似SerenityOS 同样提供了accept4(2)供需要原子设置标志位的程序使用flags支持SOCK_NONBLOCK与SOCK_CLOEXEC下文第 5 节详述其内核处理。3. 内核实现sys$accept4 的完整执行路径系统调用的真正实现在 Kernel/Syscalls/socket.cpp 的Process::sys$accept4中。整个过程可以拆解为以下阶段1参数拷贝与权限检查ErrorOrFlatPtr Process::sys$accept4(UserspaceSyscall::SC_accept4_params const* user_params) { VERIFY_NO_PROCESS_BIG_LOCK(this); TRY(require_promise(Pledge::accept)); auto params TRY(copy_typed_from_user(user_params)); ...VERIFY_NO_PROCESS_BIG_LOCK表明该路径不持有进程大锁属于无锁并行路径require_promise(Pledge::accept)则是 SerenityOSpledge 安全机制的体现——调用进程必须先通过pledge(accept)承诺此能力否则系统调用直接返回错误EPERM由 promise 机制产生这为网络服务提供了最小权限模型。2校验监听套接字int accepting_socket_fd params.sockfd; ... fd_allocation TRY(fds.allocate()); accepting_socket_description TRY(fds.open_file_description(accepting_socket_fd)); ... if (!accepting_socket_description-is_socket()) return ENOTSOCK; auto socket *accepting_socket_description-socket();fds.allocate()在进入阻塞等待之前就预先分配一个新的文件描述符槽位ScopedDescriptionAllocation保证异常路径自动回滚避免了阻塞期间 fd 槽位耗尽的问题通过open_file_description校验sockfd是否为有效描述符无效则对应EBADFD随后用is_socket()确认它指向套接字否则返回ENOTSOCK。3阻塞循环排队、唤醒与中断LockRefPtrSocket accepted_socket; for (;;) { accepted_socket socket.accept(); if (accepted_socket) break; if (!accepting_socket_description-is_blocking()) return EAGAIN; auto unblock_flags Thread::FileBlocker::BlockFlags::None; if (Thread::current()-blockThread::AcceptBlocker({}, *accepting_socket_description, unblock_flags).was_interrupted()) return EINTR; }这是理解accept(2)语义的关键循环先尝试socket.accept()从内核 Socket 对象的待接受队列中取连接取到则跳出循环取不到时若套接字被标记为非阻塞is_blocking()为假立即返回EAGAIN否则当前线程进入AcceptBlocker阻塞状态等待新的连接进入队列后由evaluate_block_conditions()唤醒若阻塞期间收到信号.was_interrupted()为真返回EINTR。这正是手册中EAGAIN非阻塞且队列为空与EINTR信号中断阻塞两条错误码的内核来源。4回写客户端地址if (user_address) { sockaddr_un address_buffer {}; address_size min(sizeof(sockaddr_un), static_castsize_t(address_size)); accepted_socket-get_peer_address((sockaddr*)address_buffer, address_size); TRY(copy_to_user(user_address, address_buffer, address_size)); TRY(copy_to_user(user_address_size, address_size)); }当调用方传入非空addr时内核先读出调用方提供的addrlen即缓冲区容量用min()将其与sockaddr_un大小取较小值通过get_peer_address()获取对端地址写入用户缓冲区最后把实际长度回写进addrlen——与手册描述的值入/值出语义完全一致。注意这里先copy_from_user读入容量再copy_to_user回写实际长度两个指针都可能被安全校验。5构造新描述符并完成连接建立auto accepted_socket_description TRY(OpenFileDescription::try_create(*accepted_socket)); accepted_socket_description-set_readable(true); accepted_socket_description-set_writable(true); if (flags SOCK_NONBLOCK) accepted_socket_description-set_blocking(false); int fd_flags 0; if (flags SOCK_CLOEXEC) fd_flags | FD_CLOEXEC; TRY(m_fds.with_exclusive( - ErrorOrvoid { fds[fd_allocation.fd].set(move(accepted_socket_description), fd_flags); return {}; })); // NOTE: Moving this state to Completed is what causes connect() to unblock on the client side. accepted_socket-set_setup_state(Socket::SetupState::Completed); return fd_allocation.fd;新套接字被标记为可读可写并依据flags应用SOCK_NONBLOCK与SOCK_CLOEXEC属性。最后一行注释揭示了 accept 在连接建立中的特殊地位将新连接的状态置为SetupState::Completed正是唤醒客户端connect(2)阻塞的那个动作——也就是说在 SerenityOS 中TCP 连接的建立完成时刻是由服务端执行accept来最终敲定的。4. 内核 Socket 对象accept 的底层数据源Socket::accept()定义在 Kernel/Net/Socket.cpp是sys$accept4反复调用的核心RefPtrSocket Socket::accept() { MutexLocker locker(mutex()); if (m_pending.is_empty()) return nullptr; dbgln_if(SOCKET_DEBUG, Socket({}) de-queueing connection, this); auto client m_pending.take_first(); VERIFY(!client-is_connected()); auto process Process::current(); client-set_acceptor(process); client-m_connected true; client-set_role(Role::Accepted); if (!m_pending.is_empty()) evaluate_block_conditions(); return client; }要点如下待接受队列为空时返回nullptr由上层循环决定是EAGAIN非阻塞还是继续阻塞阻塞模式取出的客户端套接字被标记为m_connected true角色切换为Role::Accepted并记录接受方进程set_acceptor通过MutexLocker保证多线程服务程序并发调用accept时队列操作的安全性。与之配套的数据结构在 Kernel/Net/Socket.henum class SetupState { Unstarted, // we havent tried to set the socket up yet InProgress, // were in the process of setting things up - for TCP maybe weve sent a SYN packet Completed, // the setup process is complete, but not necessarily successful }; ... bool can_accept() const { return !m_pending.is_empty(); } RefPtrSocket accept();客户端连接的入队则由Socket::queue_connection_from()完成Kernel/Net/Socket.cpp当待接受队列长度达到m_backlog上限时新连接会收到ECONNREFUSED否则追加进m_pending并唤醒阻塞中的 acceptor——这正是listen(2)的 backlog 参数详见 Kernel/Syscalls/socket.cpp与accept排队语义的内核衔接点。从类型层次看Socket是File的子类支持AF_LOCAL本地域与AF_INETIPv4两种地址族Kernel/Net/Socket.cpp因此accept对 Unix 域套接字与 TCP 套接字同样适用。5. 返回值与错误码详解手册给出的返回规则返回值为正数时即新连接的文件描述符返回-1时错误记录在errno中。最重要的错误码汇总如下错误码触发条件内核来源EBADFDsockfd是无效的文件描述符open_file_description查找失败ENOTSOCKsockfd有效但不是套接字is_socket()检查Kernel/Syscalls/socket.cppEMFILE进程的文件描述符表已满无法为新连接分配 fdfds.allocate()失败EAGAIN套接字非阻塞且待接受队列为空阻塞循环中的非阻塞分支Kernel/Syscalls/socket.cppEINTR阻塞等待期间被信号中断AcceptBlocker被中断Kernel/Syscalls/socket.cpp其中EAGAIN的手册语义尤其值得强调用户应当稍后再试一次accept(2)——在事件驱动的服务端程序中这通常意味着把监听描述符重新交给事件循环等待可读事件。另外pledge 机制下未声明acceptpromise 会返回EPERM由require_promise(Pledge::accept)产生这也属于调用方需要了解的安全边界。6. 阻塞、非阻塞与 accept4 标志位阻塞/非阻塞行为取决于监听套接字描述符自身的O_NONBLOCK状态而非accept的某个参数。在sys$accept4中阻塞模式默认队列为空时线程挂起在AcceptBlocker上直到有连接入队或收到信号非阻塞模式is_blocking()为假队列为空立即返回EAGAINSOCK_NONBLOCK标志通过accept4传入时仅对新返回的连接生效——accepted_socket_description-set_blocking(false)保证新连接从一开始就以非阻塞方式工作避免 accept 后额外调用fcntl修改描述符状态避免竞态SOCK_CLOEXEC标志为新描述符设置FD_CLOEXEC在exec时自动关闭防止文件描述符泄漏到子进程。由于accept在 LibC 中就是accept4(..., 0)直接使用accept时新连接会继承监听套接字的阻塞属性这两个标志位只能通过accept4获得。7. 实战示例完整服务端生命周期将手册内容与源码机制结合一个规范的 SerenityOS TCP 服务端骨架如下#include sys/socket.h #include netinet/in.h #include arpa/inet.h #include unistd.h #include stdio.h int main() { int server_fd socket(AF_INET, SOCK_STREAM, 0); if (server_fd 0) { perror(socket); return 1; } sockaddr_in sin {}; sin.sin_family AF_INET; sin.sin_port htons(8080); sin.sin_addr.s_addr htonl(INADDR_ANY); if (bind(server_fd, (sockaddr*)sin, sizeof(sin)) 0) { perror(bind); return 1; } if (listen(server_fd, 16) 0) { // backlog 16超出部分将被拒绝 perror(listen); return 1; } for (;;) { sockaddr_in client_addr {}; socklen_t client_len sizeof(client_addr); // 必须先初始化为缓冲区容量 int client_fd accept(server_fd, (sockaddr*)client_addr, client_len); if (client_fd 0) { if (errno EINTR) // 被信号打断继续循环 continue; perror(accept); break; } // client_len 已被改写为实际地址长度client_fd 为新连接使用后务必 close printf(accepted connection from fd %d\n, client_fd); close(client_fd); } close(server_fd); return 0; }注意事项client_len调用前必须初始化为sizeof(client_addr)否则内核将无法得知缓冲区容量每次循环必须close(client_fd)否则长期运行的服务会耗尽文件描述符并触发EMFILE单线程串行 accept 时处理逻辑越短越好否则应引入多线程或在 accept 后把新 fd 交给事件循环。8. 内核测试验证TestTCPSocket仓库中的内核测试 Tests/Kernel/TestTCPSocket.cpp 完整覆盖了socket → bind → listen → accept → recv → close的全链路与本文描述的实现逐一对得上static void* server_handler(void* accept_semaphore) { int server_fd socket(AF_INET, SOCK_STREAM, 0); EXPECT(server_fd 0); sockaddr_in sin {}; sin.sin_family AF_INET; sin.sin_port htons(port); sin.sin_addr.s_addr htonl(INADDR_LOOPBACK); int rc bind(server_fd, (sockaddr*)(sin), sizeof(sin)); EXPECT_EQ(rc, 0); rc listen(server_fd, 1); EXPECT_EQ(rc, 0); ... int client_fd accept(server_fd, nullptr, nullptr); EXPECT(client_fd 0); u8 data; int nread recv(client_fd, data, sizeof(data), 0); EXPECT_EQ(nread, 1); EXPECT_EQ(data, A); ... }该测试使用回环地址INADDR_LOOPBACK与固定端口 1337服务端在独立线程中阻塞在accept上客户端connect成功后发送一个字节A服务端通过recv验证数据到达Tests/Kernel/TestTCPSocket.cpp。它同时验证了accept的阻塞行为、返回 fd 可用于数据收发、以及 close 流程——是阅读accept(2)实现时最直接的活教材。9. 延伸阅读手册原文Base/usr/share/man/man2/accept.md用户态封装accept/accept4位于 Userland/Libraries/LibC/sys/socket.cpp声明位于 Userland/Libraries/LibC/sys/socket.h内核系统调用入口Process::sys$accept4位于 Kernel/Syscalls/socket.cpp内核 Socket 对象Socket::accept()、queue_connection_from()位于 Kernel/Net/Socket.cppSetupState、Role等定义于 Kernel/Net/Socket.h系统调用参数结构SC_accept4_params位于 Kernel/API/Syscall.h配套的listen(2)内核实现backlog 归一化逻辑Kernel/Syscalls/socket.cpp内核测试用例Tests/Kernel/TestTCPSocket.cpp【免费下载链接】serenityThe Serenity Operating System 项目地址: https://gitcode.com/GitHub_Trending/se/serenity创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表