4. io_uring 支持的网络操作类型

io_uring 最初以文件 I/O 为主,但随着内核版本演进,它把 socket 上的关键操作也纳入了统一的提交/完成队列模型。理解这一点的关键,不是记住一堆 opcode 名字,而是看清它们各自对应 socket 生命周期中的哪个阶段、需要哪些参数、以及完成事件里返回什么。本节围绕 IORING_OP_SEND、IORING_OP_RECV、IORING_OP_ACCEPT、IORING_OP_CONNECT 这四个最常用的网络 opcode 展开,并顺带说明 IORING_OP_SENDMSG、IORING_OP_RECVMSG、IORING_OP_SHUTDOWN、IORING_OP_CLOSE 等配套操作。

一、网络 opcode 在 socket 生命周期中的位置

一个典型的 TCP 服务端 socket 会经历“创建 → 绑定监听 → 接受连接 → 收发数据 → 关闭”几个阶段。io_uring 并不替代 socket()、bind()、listen() 这些同步系统调用,它主要接管的是那些会阻塞等待的操作:接受连接、建立连接、发送、接收。也就是说,先用普通系统调用把 socket 准备好,再把后续的等待型操作提交到 io_uring 队列里异步完成。

下面这张图展示了四个核心 opcode 与 socket 阶段、所需参数、完成结果之间的对应关系,可作为本节的总览。

网络 opcode 与 socket 阶段对应关系 监听 socket 已连接 socket 数据收发 关闭/半关闭 IORING_OP_ACCEPT fd = 监听 fd addr / addrlen flags res = 新连接 fd IORING_OP_CONNECT fd = 客户端 fd addr / addrlen (无 flags) res = 0 表示成功 IORING_OP_SEND fd = 已连接 fd buf / len flags(MSG_*) res = 已发送字节数 IORING_OP_RECV fd = 已连接 fd buf / len flags(MSG_*) res = 已接收字节数 配套 opcode(按需组合) SENDMSG / RECVMSG msghdr 支持多段 iovec 可携带控制消息 SHUTDOWN how = SHUT_RD/WR/RDWR 半关闭连接 CLOSE fd 关闭 可异步回收 POLL_ADD 就绪事件等待 与收发配合

二、ACCEPT:把“等待新连接”变成完成事件

IORING_OP_ACCEPT 提交时,sqe->fd 填监听 socket 的描述符,sqe->addr 与 sqe->addrlen 指向用于接收对端地址的缓冲区,sqe->accept_flags 可传 SOCK_NONBLOCK、SOCK_CLOEXEC 等标志。它的语义与 accept4() 一致:当监听队列为空时,请求不会立即失败,而是挂起等待,直到有新连接到来才产生 CQE。CQE 的 res 字段就是新连接的 fd;若为负值,则是对应的错误码,例如 -EAGAIN(在非阻塞且无连接时)、-ECONNABORTED 等。

使用要点是:监听 fd 本身仍需先用 socket()、bind()、listen() 创建并设为非阻塞。io_uring 的 accept 请求可以同时提交多个,形成“预投递”的接受队列,这样新连接到来时能更快被接走。需要注意,addr 缓冲区在请求完成前必须保持有效,不能是栈上即将失效的局部变量。

三、CONNECT:异步建立出站连接

IORING_OP_CONNECT 对应 connect(),sqe->fd 是客户端 socket,sqe->addr 和 sqe->addrlen 指定目标地址。它的完成语义与阻塞式 connect 略有不同:对于非阻塞 socket,如果连接不能立即建立,内核会返回 -EINPROGRESS,此时连接仍在进行中,需要后续通过 IORING_OP_POLL_ADD 等待可写事件,或用另一个 connect 请求继续推进。当 CQE 的 res 为 0 时表示连接成功;为负值则需按错误码处理,常见的有 -ECONNREFUSED、-ETIMEDOUT、-ENETUNREACH。

实际编程中,通常把 connect 与 poll 组合使用:先提交 connect,若返回 -EINPROGRESS,再提交一个针对该 fd 的 POLLOUT 等待,poll 完成后用 getsockopt(SO_ERROR) 读取真正的连接结果。这是异步连接的标准处理路径。

四、SEND 与 RECV:数据面的核心

IORING_OP_SEND 和 IORING_OP_RECV 分别对应 send() 与 recv()。提交时 sqe->fd 是已连接 socket,sqe->buf 与 sqe->len 指定数据缓冲区,sqe->msg_flags 可传 MSG_NOSIGNAL、MSG_DONTWAIT 等标志。CQE 的 res 是实际发送或接收的字节数,可能小于请求长度,因此应用层必须处理“部分收发”的情况,通常配合循环提交或记录偏移量。

当需要一次发送多段缓冲区,或接收时想同时拿到对端地址、控制消息时,应改用 IORING_OP_SENDMSG 和 IORING_OP_RECVMSG。它们接收一个 struct msghdr 指针,支持 iovec 数组,适合零拷贝拼接、带 ancillary data 的场景。两者的完成结果同样是字节数或负错误码。

下面给出一段提交 RECV 请求的示例,展示 SQE 字段的填充方式(示例参数,非实测结果):

struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, conn_fd, buf, buf_len, 0);
sqe->user_data = (uint64_t)conn_ctx;   // 用于完成时定位连接
io_uring_submit(&ring);

// 完成阶段
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
int n = cqe->res;                       // >0 收到字节数;0 对端关闭;<0 错误
io_uring_cqe_seen(&ring, cqe);

五、参数与完成结果的判读要点

四个 opcode 的 SQE 字段并非完全一致,容易混淆的地方集中在 addr、msg_flags 和 accept_flags 的复用上。下表整理了关键字段与完成结果的对应关系,便于提交前核对。

opcode 关键 SQE 字段 CQE res 含义 典型负错误码
IORING_OP_ACCEPT fd、addr、addrlen、accept_flags 新连接 fd -EAGAIN、-ECONNABORTED
IORING_OP_CONNECT fd、addr、addrlen 0 成功 -EINPROGRESS、-ECONNREFUSED
IORING_OP_SEND fd、buf、len、msg_flags 已发送字节数 -EPIPE、-EAGAIN
IORING_OP_RECV fd、buf、len、msg_flags 已接收字节数(0 表示对端关闭) -ECONNRESET、-EAGAIN
提交网络请求前,务必确认 socket 已设为非阻塞。io_uring 的异步语义依赖非阻塞 fd,否则内核可能在工作线程中阻塞,影响并发表现。
ACCEPT 的 addr 缓冲区、SEND/RECV 的 buf 缓冲区,在对应 CQE 返回之前都不能释放或复用。若使用栈上数组,必须保证其生命周期覆盖整个异步等待过程,否则会出现数据错乱或崩溃。

六、组合使用与常见模式

把上述 opcode 串起来,就能构成一个完整的异步网络处理循环。服务端典型模式是:预投递若干 ACCEPT;每当一个 ACCEPT 完成,拿到新 fd 后立即投递 RECV;RECV 完成后处理数据并投递 SEND;SEND 完成后再投递下一个 RECV,形成“收—处理—发—再收”的闭环。客户端则是 CONNECT(必要时配合 POLLOUT)→ SEND → RECV 的循环。

需要强调的是,io_uring 并不强制把所有操作都异步化。对于短连接、低频请求,同步调用可能更简单;而对于高并发、大量等待型操作的场景,把 ACCEPT、RECV、SEND 放进 io_uring 才能体现其减少系统调用、批量提交、统一完成通知的优势。选择哪些 opcode 进入队列,取决于连接数量、消息频率和延迟要求,而不是“越多越好”。

掌握这四个 opcode 的字段含义、完成结果判读和生命周期约束,是后续实现多路复用、连接池、背压控制等功能的基础。下一阶段可以进一步关注 IOSQE_IO_LINK、IOSQE_ASYNC 等提交标志如何影响这些网络请求的执行顺序与调度方式。