2025-11-30 · io_uring / linux
io_uring 系列文章
探索 Linux 下一代高性能异步 I/O 接口 io_uring,从核心原理到性能对比,再到与现有网络库的集成。
在掌握了基础用法后,我们已经能写出比 epoll
更高效的代码。但 io_uring
的野心不止于此。为了追求极致的性能,它引入了几个进阶特性。
在标准模式下,我们需要调用
io_uring_submit(底层是
io_uring_enter
系统调用)来通知内核处理请求。虽然支持批处理,但系统调用本身的开销(上下文切换、CPU
模式切换)在高频场景下依然可观。
SQPOLL (Submission Queue Polling) 模式下,内核会启动一个独立的内核线程(Kernel Thread),专门轮询 SQ 环形缓冲区。
只需在初始化时设置 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能力)。
在 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);在网络编程中,我们通常不知道客户端会发多少数据。为了安全,我们往往为每个连接预分配一个 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;下面是一个结合了 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
通过 SQPOLL、Fixed Files 和 Provided
Buffers,io_uring 将 I/O
性能推向了极致。它不仅消除了系统调用开销,还优化了内核内部的资源管理。
下一篇,我们将回到本博客的主题,看看老牌网络库 Libevent 是如何拥抱这一新技术的。
上一篇: 04-echo-server.md - 实战:TCP Echo Server 下一篇: 06-libevent-support.md - Libevent 支持
把当前热点继续串成多页阅读,而不是停在单篇消费。
2025-11-30 · io_uring / linux
探索 Linux 下一代高性能异步 I/O 接口 io_uring,从核心原理到性能对比,再到与现有网络库的集成。
2025-11-30 · linux / io_uring
全方位对比 io_uring 与 epoll:从系统调用开销、内存管理到编程模型,分析 io_uring 在高性能 I/O 场景中的优势与局限。
2025-11-30 · linux / io_uring
手把手教你使用 liburing 编写第一个 io_uring 程序。详解 io_uring_queue_init, io_uring_submit 等核心 API 的使用流程。
2025-11-30 · linux / io_uring
并非所有场景都适合 io_uring。深入分析快路径延迟、内存隐性成本、生态复杂度等因素,帮助你做出理性的技术选型。
在掌握了基础用法后,我们已经能写出比 epoll
更高效的代码。但 io_uring
的野心不止于此。为了追求极致的性能,它引入了几个进阶特性。
在标准模式下,我们需要调用
io_uring_submit(底层是
io_uring_enter
系统调用)来通知内核处理请求。虽然支持批处理,但系统调用本身的开销(上下文切换、CPU
模式切换)在高频场景下依然可观。
SQPOLL (Submission Queue Polling) 模式下,内核会启动一个独立的内核线程(Kernel Thread),专门轮询 SQ 环形缓冲区。
只需在初始化时设置 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能力)。
在 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);在网络编程中,我们通常不知道客户端会发多少数据。为了安全,我们往往为每个连接预分配一个 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;下面是一个结合了 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
通过 SQPOLL、Fixed Files 和 Provided
Buffers,io_uring 将 I/O
性能推向了极致。它不仅消除了系统调用开销,还优化了内核内部的资源管理。
下一篇,我们将回到本博客的主题,看看老牌网络库 Libevent 是如何拥抱这一新技术的。
上一篇: 04-echo-server.md - 实战:TCP Echo Server 下一篇: 06-libevent-support.md - Libevent 支持
把当前热点继续串成多页阅读,而不是停在单篇消费。
2025-11-30 · io_uring / linux
探索 Linux 下一代高性能异步 I/O 接口 io_uring,从核心原理到性能对比,再到与现有网络库的集成。
2025-11-30 · linux / io_uring
全方位对比 io_uring 与 epoll:从系统调用开销、内存管理到编程模型,分析 io_uring 在高性能 I/O 场景中的优势与局限。
2025-11-30 · linux / io_uring
手把手教你使用 liburing 编写第一个 io_uring 程序。详解 io_uring_queue_init, io_uring_submit 等核心 API 的使用流程。
2025-11-30 · linux / io_uring
并非所有场景都适合 io_uring。深入分析快路径延迟、内存隐性成本、生态复杂度等因素,帮助你做出理性的技术选型。