土法炼钢兴趣小组的算法知识备份

io_uring 高级特性:榨干性能极限

目录

在掌握了基础用法后,我们已经能写出比 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(&params, 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, &params);

注意: 启用 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 文件复制

下面是一个结合了 SQPOLLFixed Files 的高性能文件复制工具。

/* examples/io_uring/03-sqpoll-cp.c */
// ... (代码片段) ...

    // Init with SQPOLL
    params.flags = IORING_SETUP_SQPOLL;
    io_uring_queue_init_params(QUEUE_DEPTH, &ring, &params);

    // 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 支持

返回 io_uring 系列索引


By .