io_uring 是 Linux 5.1 引入的革命性异步 I/O 接口,通过共享内存环形缓冲区实现零系统调用的 I/O 提交与完成通知。本文对比 epoll 与 io_uring 的设计差异,实测 100 万 QPS 下的延迟分布与 CPU 开销。
一、io_uring 核心设计
io_uring 使用两个无锁环形缓冲区:Submission Queue(SQ)和 Completion Queue(CQ)。用户态向 SQ 写入 I/O 请求,内核处理后将结果写入 CQ,整个过程无需系统调用。
#include
#include
int main() {
struct io_uring ring;
io_uring_queue_init(256, &ring, 0);
int fd = open("test.txt", O_RDONLY | O_DIRECT);
char buf[4096] __attribute__((aligned(4096)));
// 获取一个 SQE
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, fd, buf, sizeof(buf), 0);
io_uring_sqe_set_data(sqe, buf);
// 提交请求(可批量提交多个 SQE)
io_uring_submit(&ring);
// 等待完成
struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
printf("Read %d bytes\n", cqe->res);
io_uring_cqe_seen(&ring, cqe);
io_uring_queue_exit(&ring);
return 0;
}
二、Fixed Files 与 Fixed Buffers
io_uring 支持预注册文件描述符和缓冲区,避免每次 I/O 操作时的内核对象查找和内存映射开销。在高频 I/O 场景下,Fixed Files 可减少约 30% 的 CPU 开销。
// 预注册文件描述符
int fds[] = {fd1, fd2, fd3};
io_uring_register_files(&ring, fds, 3);
// 使用 fixed file
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_read(sqe, IOSQE_FIXED_FILE, buf, 4096, 0);
sqe->fd = 0; // 使用 fds[0]
// 预注册缓冲区
struct iovec iovs[16];
io_uring_register_buffers(&ring, iovs, 16);
三、epoll vs io_uring 性能对比
在 100 万 QPS 的 TCP echo 服务器场景下进行对比测试:epoll 需要每次 read/write 各一次系统调用,而 io_uring 通过 SQ 批量提交和 CQ 批量收割,将系统调用次数降低 90% 以上。
// io_uring 批量收割完成事件
struct io_uring_cqe *cqes[32];
int nr = io_uring_peek_batch_cqe(&ring, cqes, 32);
for (int i = 0; i < nr; i++) {
handle_completion(cqes[i]);
}
io_uring_cq_advance(&ring, nr);
四、零拷贝 I/O
io_uring 支持 splice 和 sendfile 的异步版本,实现真正的零拷贝数据传输。结合 registered buffers,数据从磁盘到网络可以在内核态完成,完全不经过用户态。
// 异步 splice:文件 → socket,零拷贝
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_splice(sqe, file_fd, 0, socket_fd, 0, size, 0);
io_uring_submit(&ring);
五、内核旁路与 DPDK 对比
io_uring 提供了接近 DPDK 的性能,但无需独占网卡或轮询 CPU 核心。对于大多数应用场景,io_uring 是更实用的高性能 I/O 方案,兼顾了性能与通用性。
io_uring 代表了 Linux I/O 的未来方向,从数据库到 Web 服务器,越来越多的项目正在迁移到 io_uring 以获得极致的 I/O 性能。
← 返回首页