在掌握了基础用法后,我们已经能写出比 epoll
更高效的代码。但 io_uring
的野心不止于此。为了追求极致的性能,它引入了几个“黑科技”特性。
1. SQPOLL:零系统调用 (Zero Syscall)
在标准模式下,我们需要调用
io_uring_submit(底层是
io_uring_enter
系统调用)来通知内核处理请求。虽然支持批处理,但系统调用本身的开销(上下文切换、CPU
模式切换)在高频场景下依然可观。
SQPOLL (Submission Queue Polling) 模式下,内核会启动一个独立的内核线程(Kernel Thread),专门轮询 SQ 环形缓冲区。
1.1 工作原理对比
sequenceDiagram
participant App as 用户程序
participant Kernel as 内核/硬件
Note over App, Kernel: 标准模式
App->>App: 填充 SQE
App->>Kernel: Syscall: io_uring_enter
Kernel->>Kernel: 处理 I/O
Kernel-->>App: 返回
Note over App, Kernel: SQPOLL 模式
loop 内核线程轮询
Kernel->>Kernel: 检查 SQ...
end
App->>App: 填充 SQE
Note right of App: 无需 Syscall!
Kernel->>Kernel: 发现 SQE -> 处理 I/O
1.2 启用方式
只需在初始化时设置 IORING_SETUP_SQPOLL
标志:
struct io_uring_params params;
memset(¶ms, 0, sizeof(params));
params.flags = IORING_SETUP_SQPOLL;
// 可选:绑定 CPU 核心
// params.flags |= IORING_SETUP_SQ_AFF;
// params.sq_thread_cpu = 1;
io_uring_queue_init_params(4096, &ring, ¶ms);注意: 启用 SQPOLL 需要 root 权限(或
CAP_SYS_NICE能力)。
2. Fixed Files:注册文件描述符
在 Linux 内核中,将 fd(整数)转换为
struct file *(内核对象)是有开销的(原子操作、引用计数)。如果一个连接需要频繁读写,这种开销会累积。
io_uring 允许我们预先“注册”一组
fd。注册后,内核会持有这些文件的引用。后续提交请求时,我们直接使用索引(Index)代替
fd,内核即可跳过查找和引用计数步骤。
// 1. 注册 fd 数组
int fds[2] = { src_fd, dst_fd };
io_uring_register_files(&ring, fds, 2);
// 2. 提交请求时使用索引
// 使用 IOSQE_FIXED_FILE 标志
// fd 参数填索引 (如 0 代表 src_fd)
io_uring_prep_read(sqe, 0, buf, len, offset);
io_uring_sqe_set_flags(sqe, IOSQE_FIXED_FILE);3. Provided Buffers:自动缓冲区选择
在网络编程中,我们通常不知道客户端会发多少数据。为了安全,我们往往为每个连接预分配一个 Buffer(如 4KB)。如果有 10k 个连接,就需要 40MB 内存,即使大部分连接是空闲的。
Provided Buffers
允许我们向内核提供一个“缓冲区池”。当有数据到达时,内核自动从池中取出一个
Buffer 存放数据,并告诉我们用了哪一个。
这极大地减少了内存占用。
// 1. 提供一组 Buffer 给内核 (Group ID = 1)
io_uring_prep_provide_buffers(sqe, bufs, 1024, 100, 1);
// 2. 提交 Read 请求时,设置 IOSQE_BUFFER_SELECT
// 不需要传入具体的 buf 地址,只需传入 Group ID
io_uring_prep_read(sqe, fd, NULL, len, 0);
io_uring_sqe_set_flags(sqe, IOSQE_BUFFER_SELECT);
sqe->buf_group = 1;
// 3. 完成时,cqe->flags 包含 Buffer ID
int bid = cqe->flags >> 16;4. 实战:SQPOLL 文件复制
下面是一个结合了 SQPOLL 和 Fixed Files 的高性能文件复制工具。
/* examples/io_uring/03-sqpoll-cp.c */
// ... (代码片段) ...
// Init with SQPOLL
params.flags = IORING_SETUP_SQPOLL;
io_uring_queue_init_params(QUEUE_DEPTH, &ring, ¶ms);
// Register Files
int fds[2] = { src_fd, dst_fd };
io_uring_register_files(&ring, fds, 2);
// Loop
while (offset < file_sz) {
// Read using Index 0
io_uring_prep_read(sqe, 0, buf, len, offset);
io_uring_sqe_set_flags(sqe, IOSQE_FIXED_FILE);
io_uring_submit(&ring); // 唤醒内核线程 (如果休眠)
// ... Wait ...
// Write using Index 1
io_uring_prep_write(sqe, 1, buf, len, offset);
io_uring_sqe_set_flags(sqe, IOSQE_FIXED_FILE);
io_uring_submit(&ring);
}完整代码: 03-sqpoll-cp.c
5. 总结
通过 SQPOLL、Fixed Files 和 Provided
Buffers,io_uring 将 I/O
性能推向了极致。它不仅消除了系统调用开销,还优化了内核内部的资源管理。
下一篇,我们将回到本博客的主题,看看老牌网络库 Libevent 是如何拥抱这一新技术的。
上一篇: 04-echo-server.md - 实战:TCP Echo Server 下一篇: 06-libevent-support.md - Libevent 支持