liburing 总结: 用户态库 · 深度解析

Linux io_uring 用户态库 · 深度解析

仓库:axboe/liburing · 作者:Jens Axboe(Linux block layer maintainer) · License:MIT (lib) / GPL (kernel headers) · 当前版本:2.9

目录

  1. [io_uring 与 liburing 是什么](#io_uring 与 liburing 是什么)
  2. [为什么会出现 io_uring(历史与对比)](#为什么会出现 io_uring(历史与对比))
  3. [核心架构:双 ring 共享内存模型](#核心架构:双 ring 共享内存模型)
  4. 三个系统调用
  5. [关键数据结构(SQE / CQE / 句柄)](#关键数据结构(SQE / CQE / 句柄))
  6. 编程模型:六步范式
  7. [支持的 60+ 种操作(IORING_OP_*)](#支持的 60+ 种操作(IORING_OP_*))
  8. [运行模式与高级特性(SQPOLL / IOPOLL / DEFER_TASKRUN ...)](#运行模式与高级特性(SQPOLL / IOPOLL / DEFER_TASKRUN ...))
  9. [资源预注册(Buffers / Files / Buf Ring)](#资源预注册(Buffers / Files / Buf Ring))
  10. [高级特性:Linked / Multishot / Cancel / Zero-Copy](#高级特性:Linked / Multishot / Cancel / Zero-Copy)
  11. 仓库目录与代码结构
  12. 实现框架:模块分层与依赖图 NEW
  13. [实现细节:fast path 与内存屏障](#实现细节:fast path 与内存屏障)
  14. 完整函数调用图(六大流程) NEW
  15. 源码逐文件深度分析(带行号锚点) NEW
  16. [DeepWiki 章节对照与关键摘录](#DeepWiki 章节对照与关键摘录) NEW
  17. [性能特性与对比 epoll](#性能特性与对比 epoll)
  18. 业界采用情况
  19. 坑、限制与最佳实践
  20. 总览速查表
  21. [liburing 公共 API 大全(按功能分组)](#liburing 公共 API 大全(按功能分组)) NEW
  22. [raw io_uring vs liburing 对照实现(TCP echo server)](#raw io_uring vs liburing 对照实现(TCP echo server)) NEW

1. io_uring 与 liburing 是什么

io_uring 是 Linux 内核 5.1 (2019) 引入的全新异步 I/O 接口,由 Jens Axboe 设计。它通过用户态与内核共享的 两个 ring buffer (提交队列 SQ + 完成队列 CQ)传递 I/O 请求,批处理 + 零拷贝 地完成异步 I/O。

liburing 是 io_uring 的 官方用户态 C 库,封装了三个原始 syscall,并提供:

  • Ring 创建 / 销毁 / mmap 管理
  • 类型安全的 io_uring_prep_*() 操作准备 API(覆盖 60+ 种操作)
  • SQ/CQ 队列读写与内存屏障封装(应用不必关心 acquire/release 语义)
  • 资源预注册管理(buffers / files / eventfd / ring fd)
  • FFI 子库(liburing-ffi)供 Rust / Go / Python 等其他语言绑定

一句话定位liburing 之于 io_uring,相当于 libpthread 之于内核线程 ------ 它不是必须的(你完全可以直接 syscall),但如果你要写代码而不是论文,请用它。

2. 为什么会出现 io_uring(历史与对比)

Linux 异步 I/O 的演进史几乎是一部"屡战屡败"的历史,io_uring 是第六代尝试,也是第一个真正成功的:

方案 年代 问题
select / poll 1980s~ 每次都要扫描整个 fd 集合,O(n)
epoll 2002 (2.5.44) 仅做"I/O 就绪通知 ",不做异步 I/O 本身;不支持文件 I/O;每个 socket 仍需 syscall
POSIX AIO --- 用户态线程池模拟,性能差
Linux libaio (io_submit) 2002 只支持 O_DIRECT 文件;网络/buffered I/O 直接退化为同步;syscall 开销仍高
SCM-rights / sendfile / splice --- 非通用
io_uring 2019 (5.1) 统一 :file / network / poll / timeout / fsync / splice 全异步;共享内存 + batch 几乎消除 syscall 开销

背景:现代硬件(NVMe SSD ~5M IOPS 、100GbE 网卡)让 syscall 本身(每次 ~1μs)成为主要瓶颈。io_uring 通过共享内存 + 可选的内核轮询线程,让 I/O 提交完全无需 syscall

3. 核心架构:双 ring 共享内存模型

USERSPACE KERNEL ┌──────────────────────────────────┐ ┌──────────────────────────┐ │ Application │ │ io_uring kernel side │ │ │ │ │ │ io_uring_get_sqe() ─── write ──►│ │ │ │ │ │ │ │ io_uring_submit() ┌───────►│ SQ ring │ ──► consume SQE ──┐ │ │ (or none if SQPOLL) │ mmap │ (head/ │ submit to │ │ │ │ shared │ tail) │ block/net stack │ │ │ │ memory │ + array│ ▼ │ │ │ │ + SQEs │ ┌──── do I/O ───┐ │ │ │ │ │ │ block layer │ │ │ │ │ │ │ socket layer │ │ │ │ │ │ │ fs layer │ │ │ │ │ │ └───────┬───────┘ │ │ │ │ │ │ │ │ io_uring_wait_cqe() ◄─┤ │ CQ ring │ ◄── post CQE │ │ (or peek) │ mmap │ (head/ │ (kernel writes) │ │ │ │ tail │ │ │ io_uring_cqe_seen() ─┘ │ + CQEs)│ │ └──────────────────────────────────┘ └──────────────────────────┘ ▲ ▲ │ │ └─────── 共享内存(mmap:3 个区域)──────────────┘ ① SQ Ring (head/tail/flags + index array) ② SQE Array (实际的 sqe 内容) ③ CQ Ring (head/tail/flags + cqe 数组)

三个 mmap 区域 (在 src/setup.c:io_uring_mmap() 中建立):

区域 偏移宏 读写方 作用
SQ Ring IORING_OFF_SQ_RING app 写 tail,kernel 读 + 写 head head/tail/flags/dropped + 一个 index → SQE 的数组
SQE Array IORING_OFF_SQES app 写 实际的 64 字节(或 128 字节)SQE 内容
CQ Ring IORING_OFF_CQ_RING kernel 写 tail,app 读 + 写 head head/tail/flags/overflow + CQE 数组

较新内核(IORING_FEAT_SINGLE_MMAP)允许 SQ Ring 和 CQ Ring 共享一段 mmap,进一步减少 vma。

关键设计:内核与用户态都不需要锁。SQ 是 SPSC(单生产单消费),CQ 也是 SPSC,依靠 acquire/release 内存屏障同步。

4. 三个系统调用

syscall 编号 (x86) liburing 封装 作用
io_uring_setup(entries, params) 425 io_uring_queue_init() 创建 ring,返回 fd;填 io_uring_params 给应用做 mmap
io_uring_enter(fd, to_submit, min_complete, flags, sig) 426 io_uring_submit() / wait_cqe() 通知内核处理 SQ 中的请求,并/或等待 CQ 完成
io_uring_register(fd, op, arg, nr) 427 io_uring_register_buffers() / files() / ... 注册长生命周期资源(buffers / files / eventfd / ring fd / ...)

使用 SQPOLL 模式 时,连 io_uring_enter 都可以省去 ------ 内核线程会主动轮询 SQ ring,应用提交 SQE 后只需写 tail 就完成。

5. 关键数据结构

5.1 应用句柄:struct io_uring

定义在 src/include/liburing.h:167

复制代码
struct io_uring {
    struct io_uring_sq sq;       // SQ 元数据 + sqes 指针
    struct io_uring_cq cq;       // CQ 元数据 + cqes 指针
    unsigned flags;              // IORING_SETUP_*(SQPOLL/IOPOLL/COOP_TASKRUN/...)
    int      ring_fd;            // setup 返回的 fd
    unsigned features;           // 内核支持的 IORING_FEAT_*
    int      enter_ring_fd;      // 注册后可用的 registered ring fd
    __u8     int_flags;          // liburing 内部 flag
};

5.2 提交项:struct io_uring_sqe(64 字节,可选 128)

定义在 src/include/liburing/io_uring.h:30-114

复制代码
struct io_uring_sqe {
    __u8   opcode;          // IORING_OP_*(READ/WRITE/SEND/RECV/ACCEPT...)
    __u8   flags;           // IOSQE_*(IO_LINK / FIXED_FILE / BUFFER_SELECT...)
    __u16  ioprio;
    __s32  fd;              // 操作的 fd(或 fixed file index)
    union { __u64 off; __u64 addr2; ... };  // 文件偏移 / 第二地址
    union { __u64 addr; ... };               // buffer 地址
    __u32  len;             // 长度
    union { /* 各种 op 专属 flags */ };
    __u64  user_data;       // 用户数据,原样回到 CQE
    union { __u16 buf_index; __u16 buf_group; };
    __u16  personality;
    union { __s32 splice_fd_in; __u32 file_index; ... };
    union { __u64 addr3; __u8 cmd[0]; };    // 128B SQE 时 cmd 用于 passthrough
};

5.3 完成项:struct io_uring_cqe(16 字节,可选 32)

复制代码
struct io_uring_cqe {
    __u64  user_data;       // 与对应 SQE 的 user_data 相同
    __s32  res;             // syscall 风格返回值(>=0 成功,<0 -errno)
    __u32  flags;           // IORING_CQE_F_*(BUFFER / MORE / SOCK_NONEMPTY ...)
    __u64  big_cqe[];       // CQE32 模式下额外 16B
};

6. 编程模型:六步范式

┌──────────────────────────────────────────────────────────────────┐ │ ① io_uring_queue_init(QD, &ring, flags) │ │ └─► setup syscall + mmap 三个区域 │ ├──────────────────────────────────────────────────────────────────┤ │ ② sqe = io_uring_get_sqe(&ring) │ │ io_uring_prep_readv(sqe, fd, iov, 1, offset) │ │ io_uring_sqe_set_data(sqe, my_ctx) │ │ └─► 只是写共享内存,不进内核 │ ├──────────────────────────────────────────────────────────────────┤ │ ③ io_uring_submit(&ring) │ │ └─► 写 SQ tail(acquire/release barrier) │ │ 非 SQPOLL:调用 io_uring_enter() │ │ SQPOLL:内核线程已经在轮询,无需 syscall │ ├──────────────────────────────────────────────────────────────────┤ │ ④ io_uring_wait_cqe(&ring, &cqe) / peek_cqe / wait_cqes │ │ └─► 检查 CQ tail,必要时调 io_uring_enter(GETEVENTS) │ ├──────────────────────────────────────────────────────────────────┤ │ ⑤ if (cqe->res < 0) error; else use cqe->res / user_data │ │ io_uring_cqe_seen(&ring, cqe) ── 推 CQ head │ │ 或者批量:io_uring_for_each_cqe + io_uring_cq_advance(n) │ ├──────────────────────────────────────────────────────────────────┤ │ ⑥ io_uring_queue_exit(&ring) │ │ └─► munmap + close(fd) │ └──────────────────────────────────────────────────────────────────┘

最小可运行示例(来自 examples/io_uring-test.c

复制代码
#include "liburing.h"

struct io_uring ring;
io_uring_queue_init(QD, &ring, 0);

int fd = open(argv[1], O_RDONLY | O_DIRECT);
struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
io_uring_prep_readv(sqe, fd, &iov, 1, offset);

io_uring_submit(&ring);

struct io_uring_cqe *cqe;
io_uring_wait_cqe(&ring, &cqe);
if (cqe->res >= 0) { /* 读到了 cqe->res 字节 */ }
io_uring_cqe_seen(&ring, cqe);

io_uring_queue_exit(&ring);

7. 支持的 60+ 种操作(IORING_OP_*)

定义在 src/include/liburing/io_uring.h:249enum io_uring_op,截至当前版本约 65+ 种。按用途分类:

类别 典型 opcode 典型 prep helper
文件 I/O READ / WRITE / READV / WRITEV / READ_FIXED / WRITE_FIXED / READ_MULTISHOT / FSYNC / FALLOCATE / FADVISE / SYNC_FILE_RANGE io_uring_prep_read(), prep_writev_fixed() ...
文件元数据 OPENAT / OPENAT2 / CLOSE / STATX / RENAMEAT / UNLINKAT / MKDIRAT / SYMLINKAT / LINKAT / FTRUNCATE / [F]GETXATTR / [F]SETXATTR io_uring_prep_openat() ...
网络 I/O ACCEPT / CONNECT / BIND / LISTEN / SEND / RECV / SENDMSG / RECVMSG / SOCKET / SHUTDOWN io_uring_prep_accept(), prep_recv() ...
零拷贝 SEND_ZC / SENDMSG_ZC / RECV_ZC / SPLICE / TEE io_uring_prep_send_zc(), prep_splice()
轮询 / 定时 POLL_ADD / POLL_REMOVE / TIMEOUT / TIMEOUT_REMOVE / LINK_TIMEOUT / EPOLL_CTL / EPOLL_WAIT io_uring_prep_poll_add(), prep_timeout()
取消 / 控制 ASYNC_CANCEL / NOP / FILES_UPDATE / PROVIDE_BUFFERS / REMOVE_BUFFERS / MSG_RING / FIXED_FD_INSTALL io_uring_prep_cancel(), prep_msg_ring()
同步原语 WAITID / FUTEX_WAIT / FUTEX_WAKE / FUTEX_WAITV io_uring_prep_futex_*()
设备/passthrough URING_CMD / URING_CMD128 (NVMe passthrough、socket cmd 等) io_uring_prep_cmd_sock()

liburing.h 中超过 1500 行 都是这些 io_uring_prep_* 内联辅助函数。

8. 运行模式与高级特性(IORING_SETUP_* flags)

flag 含义 典型适用
IORING_SETUP_SQPOLL 内核创建一个轮询线程,持续轮询 SQ ring;应用 submit 时无需 syscall 极致延迟、syscall 完全消除;缺点:占 1 个 CPU;需要 root 或 CAP_SYS_NICE(旧内核)
IORING_SETUP_SQ_AFF 把 SQPOLL 线程绑到 sq_thread_cpu 指定的 CPU per-core 设计
IORING_SETUP_IOPOLL 用 NVMe 轮询模式完成;CQE 不再走中断 NVMe 直通、低延迟存储
IORING_SETUP_COOP_TASKRUN "协作式"任务回调;不强制 IPI 中断目标线程,节省调度开销 5.18+;通用提速
IORING_SETUP_DEFER_TASKRUN 更进一步:仅在调用 io_uring_enter 时才执行 task work 6.0+;和 SINGLE_ISSUER 配合,性能最优,proxy 示例的默认配置
IORING_SETUP_SINGLE_ISSUER 承诺只有一个线程提交 SQE 5.18+;与 DEFER_TASKRUN 搭配
IORING_SETUP_SQE128 SQE 大小变 128B(含 80B cmd 字段) NVMe passthrough、自定义命令
IORING_SETUP_CQE32 CQE 大小变 32B 需要更大返回值的 op
IORING_SETUP_NO_SQARRAY 去掉 SQ index 数组的间接寻址(默认从 6.6 开启) 提速 ~5%
IORING_SETUP_NO_MMAP 由应用自己分配大页内存传入 HugeTLB 大页加速、多 ring 共享一页
IORING_SETUP_REGISTERED_FD_ONLY 只暴露 registered fd,不分配普通 fd fd 表压力大的服务
IORING_SETUP_TASKRUN_FLAG 有 task work 时设置 SQ flag,应用主动检查 需要精确控制何时进内核
IORING_SETUP_HYBRID_IOPOLL iopoll 但允许少量休眠(节能) 新内核新增
IORING_SETUP_R_DISABLED 创建后默认禁用,需调用 io_uring_enable_rings 开启 受限模式(与 restrictions 配合)

2024+ 新代码推荐组合IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN | IORING_SETUP_COOP_TASKRUN,再加 NO_SQARRAY。这是 examples/proxy.c 中的最佳实践默认值。

9. 资源预注册(Register API)

原理:把长生命周期资源预先注册到内核,每次 I/O 通过 index 引用,省掉每次 op 的 fd 引用计数 / 用户态地址校验 / mmap 等开销。

API 说明
io_uring_register_buffers() 注册一组 iovec 给后续 READ_FIXED / WRITE_FIXED 用,省去每次 pin 内存
io_uring_register_buffers_sparse() 预留 N 个槽位,运行时再 update(5.13+)
io_uring_register_files() 注册 fd 数组;后续操作用 `IOSQE_FIXED_FILE
io_uring_register_files_update() 动态替换某个槽位的 fd
io_uring_register_eventfd() 有新 CQE 时通过 eventfd 唤醒(适合与 epoll 集成)
io_uring_register_ring_fd() 把 ring fd 自身注册(5.18+),io_uring_enter 用 registered index 而非 fd
io_uring_register_buf_ring() 注册 provided buffer ring(5.19+),实现 zero-copy recv 的关键
io_uring_register_iowq_max_workers() 限制 io-wq worker 数
io_uring_register_napi() NAPI busy poll,用网卡轮询替代中断(6.x)
io_uring_register_clock() 选择 timeout 用的时钟源
io_uring_register_restrictions() 限制可用 op / register / fixed file,提升安全性(沙箱场景)
Provided Buffer Ring(性能利器)

传统:每个 recv 操作都要预先指定 buffer 地址,连接数多时内存浪费严重。

Provided Buffer Ring:应用预先把一批 buffer 挂到一个 ring 上,内核在完成 recv动态选一个 ,CQE 用 IORING_CQE_F_BUFFER 告诉应用是哪一个。

10. 高级特性

① Linked Operations

SQE 设 IOSQE_IO_LINK,整组按顺序执行;前一个失败则后续 short-circuit。常见用法:read(file) → write(socket) 实现 sendfile-like 流水线。

② Multishot Operations

一个 SQE 持续产生 CQE,直到取消或出错(CQE 带 IORING_CQE_F_MORE)。已支持:POLL_ADD_MULTI / RECV_MULTISHOT / ACCEPT_MULTISHOT / READ_MULTISHOT / TIMEOUT_MULTISHOT。让"百万连接 accept"只用 1 个 SQE。

③ Async Cancel

io_uring_prep_cancel() 通过 user_data / fd / opcode 取消 in-flight 请求;支持 CANCEL_ALL / CANCEL_ANY 批量。新增 io_uring_register_sync_cancel() 同步取消。

④ Zero-Copy

SEND_ZC / SENDMSG_ZC:发送时不拷贝 buffer 到内核,每个发送会有 2 个 CQE (操作完成 + buffer 可释放)。
RECV_ZC + ZCRX:使用专用接收队列,把网卡 DMA 到的 page 直接交给应用 ------ 真正的"网卡到用户态零拷贝"。

⑤ Direct Descriptors(fixed files)

OPENAT 等支持 file_index = IORING_FILE_INDEX_ALLOC,让内核分配一个 不进入进程 fd 表 的"direct descriptor",避免 fd 表锁竞争。需要时用 FIXED_FD_INSTALL 转成正常 fd。

⑥ Msg Ring

IORING_OP_MSG_RING:在不同 ring 之间传递消息(IORING_MSG_DATA)或 fd(IORING_MSG_SEND_FD),无需 socketpair / pipe,是 per-core 多 ring 架构的核间通信原语。

⑦ Restrictions

启动后限制 ring 只能用某些 op、某些 fixed file,配合 R_DISABLED 使用,做沙箱。

⑧ NAPI Busy Poll

把网卡的 NAPI 轮询绑到 io_uring 等待路径,消灭中断,5G/100GbE 场景延迟可降到微秒级。

11. 仓库目录与代码结构

路径 行数 作用
src/include/liburing.h 2149 主对外头文件,包含所有 inline io_uring_prep_*()
src/include/liburing/io_uring.h 1124 kernel UAPI(struct io_uring_sqe / cqeIORING_OP_* 等定义)
src/include/liburing/barrier.h --- 跨架构内存屏障 (io_uring_smp_load_acquire 等)
src/setup.c 709 ring 创建、mmap/munmap、参数推算
src/queue.c 497 SQ flush、__io_uring_submitget_cqe 主循环
src/register.c 529 所有 register/unregister API(buffers / files / eventfd / buf_ring / napi / clock)
src/sanitize.c 177 ASan 集成,校验用户传入的 buffer/iovec
src/syscall.c / syscall.h ~30 三个 syscall 的薄封装
src/ffi.c 15 把所有内联函数实例化成符号供 FFI 使用
src/nolibc.c 60 无 libc 模式(x86-64 / aarch64 默认开)
src/arch/ --- per-arch 屏障实现(x86 / arm64 / generic / sysz)
examples/ --- ~20 个示例:io_uring-cp / proxy / send-zerocopy / zcrx / napi-busy-poll / link-cp / ucontext-cp
test/ --- 268 个测试用例(既测 liburing,也测 kernel 行为)
man/ --- 200+ 个 man page(io_uring.7、每个 prep helper 都有 .3)

注意 :仓库的"重头戏"是 test/(占大部分代码量)。大部分 liburing 提交其实是 kernel io_uring 的回归测试,因为 Jens Axboe 同时是 kernel 那一侧的维护者。

11.5 实现框架:模块分层与依赖图 NEW

liburing 整体只有 ~2 000 行 实现 + ~2 100 行 内联 helper,但层次划分非常清晰。下面这张分层依赖图是把 src/ 全部源文件按"对外暴露 → 内部协助 → 系统调用"逐层归类后画出来的:

┌─────────────────────────────────────────────────────────────────────────┐ │ L4 对外公共 API(应用直接调用) │ │ src/include/liburing.h (2149 行, ALL static inline) │ │ ├─ ring 句柄 / SQ / CQ 类型定义 │ │ ├─ io_uring_get_sqe / io_uring_initialize_sqe │ │ ├─ io_uring_prep_<60+ ops> ◄─── 占整个文件 ~75% │ │ ├─ io_uring_sq_ready / sq_space_left / cq_ready / cq_advance │ │ ├─ io_uring_cqe_seen / cqe_get_data / sqe_set_data │ │ ├─ io_uring_for_each_cqe (macro + iterator) │ │ └─ buf_ring helpers (init / add / advance / cq_advance) │ ├─────────────────────────────────────────────────────────────────────────┤ │ L3 实现层(编译为 .so / .a 的真正 C 代码) │ │ ┌─────────────────┬──────────────────┬───────────────────────────┐ │ │ │ setup.c 709行 │ queue.c 497行 │ register.c 529行 │ │ │ │ ring 创建 │ submit / wait │ 所有 register/unregister │ │ │ │ mmap 三段 │ CQE 收割 │ buffers/files/buf_ring/ │ │ │ │ buf_ring setup │ flush_sq │ eventfd/iowq/napi/clock │ │ │ │ probe / mlock │ get_cqe │ restrictions/region 等 │ │ │ └─────────────────┴──────────────────┴───────────────────────────┘ │ │ ┌─────────────────┬──────────────────┬───────────────────────────┐ │ │ │ sanitize.c 177行│ syscall.c 30行 │ ffi.c 15行 / nolibc.c 60行│ │ │ │ ASan 侧建模 │ 3 syscall 薄包 │ FFI shim / 无 libc 直跳 │ │ │ │ buffer 校验 │ enter / setup / │ arch/{x86,aarch64,riscv, │ │ │ │ │ register │ generic}/syscall.h │ │ │ └─────────────────┴──────────────────┴───────────────────────────┘ │ ├─────────────────────────────────────────────────────────────────────────┤ │ L2 内部基础设施 │ │ lib.h / int_flags.h / setup.h / arch/<arch>/{lib,syscall,barrier}.h │ │ ├─ INT_FLAG_*(liburing 内部状态:REG_RING / CQ_ENTER / APP_MEM) │ │ ├─ uring_unlikely / IO_URING_READ_ONCE / WRITE_ONCE │ │ └─ smp_load_acquire / smp_store_release / smp_mb (per-arch) │ ├─────────────────────────────────────────────────────────────────────────┤ │ L1 Linux 内核 UAPI(由内核拷贝并保留的共享头文件) │ │ src/include/liburing/io_uring.h (1124 行) │ │ ├─ struct io_uring_sqe / io_uring_cqe / io_uring_params │ │ ├─ enum io_uring_op (IORING_OP_NOP ... IORING_OP_URING_CMD128) │ │ ├─ IORING_SETUP_* / IORING_FEAT_* / IORING_ENTER_* / IOSQE_* │ │ ├─ IORING_REGISTER_* / IORING_CQE_F_* / mmap offset 宏 │ │ └─ buf_ring / msg_ring / restriction 等扩展结构 │ ├─────────────────────────────────────────────────────────────────────────┤ │ L0 Linux Kernel (fs/io_uring.c, io_uring/*) │ │ syscalls 425 / 426 / 427 │ └─────────────────────────────────────────────────────────────────────────┘

11.5.1 模块依赖矩阵

调用方 ↓ / 依赖 → liburing.h queue.c setup.c register.c syscall.h lib.h / barrier.h kernel UAPI
用户应用 ✅ 唯一入口 --- --- --- --- --- (透明)
setup.c ✅ 反向引用类型 --- self --- ✅ _sys* ✅ barrier 不用 ✅ 通过 syscall.h
queue.c ✅ 类型 + inline self --- --- ✅ _sys* ✅ READ_ONCE / smp_*
register.c ✅ 类型 --- --- self ✅ _sys* ✅ sanitize
sanitize.c --- --- --- --- ---
ffi.c ✅ 把 inline 实例化 --- --- --- --- --- ---

设计观察

① 整个库 没有循环依赖 ,是严格的单向 DAG(app → liburing.h → {setup,queue,register}.c → syscall.h → kernel);

② 性能关键路径(get_sqe / cqe_seen / for_each_cqe / load_sq_head全部 在头文件里 static inline,编译期就被打平进调用方,没有跨编译单元的函数调用开销;

③ "重型" 的 register/setup 路径走 普通函数 + __cold 注解,让编译器把它们放到 cold section,不污染热路径 icache。

11.5.2 ABI 暴露:liburing.map

liburing 通过 version script 严格控制对外符号。以下是导出符号的分类(统计自 src/liburing.map):

版本节 典型符号 对应实现文件
LIBURING_2.0 io_uring_queue_init / queue_exit / submit / wait_cqe / register_buffers / register_files / get_probe ... setup.c, queue.c, register.c
LIBURING_2.1 io_uring_register_iowq_max_workers / register_ring_fd / register_buf_ring ... register.c
LIBURING_2.2 io_uring_get_sqe(重新暴露符号,见 8be8af4afcb4)/ submit_and_wait_timeout queue.c (LIBURING_INTERNAL)
LIBURING_2.3 ~ LIBURING_2.6 register_napi / register_clock / setup_buf_ring / free_buf_ring / register_restrictions / register_sync_msg ... setup.c, register.c
LIBURING_2.7 ~ LIBURING_2.9 register_zcrx_ifq / register_region / submit_and_wait_min_timeout / sqring_wait ... register.c, queue.c

FFI 子库(liburing-ffi.so)则把所有 static inline 也实例化成符号(见 src/liburing-ffi.map),供无法消费 inline 的语言 binding 使用。

12. 实现细节:fast path 与内存屏障

12.1 提交 fast path(__io_uring_submit in queue.c:434)

复制代码
static int __io_uring_submit(struct io_uring *ring, unsigned submitted,
                             unsigned wait_nr, bool getevents) {
    bool cq_needs_enter = getevents || wait_nr || cq_ring_needs_enter(ring);
    unsigned flags = ring_enter_flags(ring);

    if (sq_ring_needs_enter(ring, submitted, &flags) || cq_needs_enter) {
        if (cq_needs_enter)
            flags |= IORING_ENTER_GETEVENTS;
        ret = __sys_io_uring_enter(ring->enter_ring_fd, submitted,
                                   wait_nr, flags, NULL);
    } else
        ret = submitted;     // SQPOLL 模式:完全不进内核!
    return ret;
}

在 SQPOLL 模式下,如果内核线程未休眠(!IORING_SQ_NEED_WAKEUP),整个 submit 路径连 syscall 都没有,纯内存写。这是 io_uring 的核心性能优势。

12.2 SQ tail 推进(__io_uring_flush_sq in queue.c:203)

复制代码
if (sq->sqe_head != tail) {
    sq->sqe_head = tail;
    if (!(ring->flags & IORING_SETUP_SQPOLL))
        *sq->ktail = tail;                        // 非共享:普通 store 即可
    else
        io_uring_smp_store_release(sq->ktail, tail); // SQPOLL:必须 release,否则
                                                     // 内核可能看到旧 SQE 数据
}

12.3 CQE 收割(io_uring_for_each_cqe in liburing.h:481)

复制代码
// 用迭代器一次性处理一批 CQE,最后用 io_uring_cq_advance(n) 一次推 head
struct io_uring_cqe_iter __ITER__ = io_uring_cqe_iter_init(ring);
//   tail = io_uring_smp_load_acquire(ring->cq.ktail);  // 关键 acquire
while (io_uring_cqe_iter_next(&__ITER__, &cqe)) {
    process(cqe);
}
io_uring_cq_advance(ring, n);
//   io_uring_smp_store_release(cq->khead, *cq->khead + n);

整套 SQ/CQ 同步只用了 4 个原子原语:READ_ONCE / WRITE_ONCE / smp_load_acquire / smp_store_release,定义在 barrier.h,per-arch 适配。

12.5 完整函数调用图(六大流程) NEW

下面是从源码中机械抽取出来的真实调用关系(不是文档想象,每条箭头都对应源文件中的调用语句)。看完这六张图,你基本就读完了 liburing 的核心。

① 初始化流程:io_uring_queue_init

app │ ▼ io_uring_queue_init(entries, ring, flags) [setup.c:441] │ memset(p,0); p.flags = flags; ▼ io_uring_queue_init_params(entries, ring, &p) [setup.c:428] │ ▼ io_uring_queue_init_try_nosqarr(entries, ring, &p, NULL, 0) [setup.c:389] │ // 先尝试 IORING_SETUP_NO_SQARRAY,若内核太老(-EINVAL)退化 │ ▼ __io_uring_queue_init_params(entries, ring, &p, buf, buf_size) [setup.c:312] │ ├── (if NO_MMAP) ─► io_uring_alloc_huge(...) [setup.c:223] ──► __sys_mmap (anon|hugetlb) │ ├── ────────────────► __sys_io_uring_setup(entries, &p) ◄── syscall 425 │ ├── (else) ─► io_uring_queue_mmap(fd, &p, ring) [setup.c:170] │ └─► io_uring_mmap(fd, &p, sq, cq) [setup.c:110] │ ├─ __sys_mmap × 2~3 (SQ ring / CQ ring / SQE array) │ └─ io_uring_setup_ring_pointers(&p, sq, cq) [setup.c:70] │ // 把 khead/ktail/kflags/kring_mask/... │ // 指针指向 mmap 共享区的对应偏移 │ ├── 填充 sq_array[i] = i (除非 NO_SQARRAY) [setup.c:362] └── 设置 ring->ring_fd / enter_ring_fd / int_flags

② 提交流程:io_uring_get_sqeio_uring_prep_*io_uring_submit

app [仅 1 次 syscall(甚至 0 次)] │ ▼ sqe = io_uring_get_sqe(&ring) [liburing.h:2058] │ └─► _io_uring_get_sqe(&ring) [liburing.h:1938] │ ├─ head = io_uring_load_sq_head(ring) [liburing.h:1704] │ │ └─ (SQPOLL) smp_load_acquire(*sq.khead) // 读内核游标 │ ├─ if (tail - head >= ring_entries) return NULL; │ ├─ sqe = &sq.sqes[(tail & mask) << sqe_shift]; // 仅一次内存写 │ └─ io_uring_initialize_sqe(sqe); [liburing.h:579] │ ▼ io_uring_prep_read(sqe, fd, buf, len, off) [liburing.h:1029] │ └─► io_uring_prep_rw(IORING_OP_READ, sqe, fd, ...) [liburing.h:592] │ opcode = OP_READ; sqe->fd = fd; sqe->addr = buf; sqe->len = len; sqe->off = off; │ ▼ io_uring_sqe_set_data(sqe, ctx) [liburing.h:523] // user_data = ctx │ ▼ (上面所有都是纯内存写,不进内核) │ io_uring_submit(&ring) [queue.c:465] └─► __io_uring_submit_and_wait(ring, 0) [queue.c:455] └─► __io_uring_submit(ring, __io_uring_flush_sq(ring), 0, false) │ │ ┌─ __io_uring_flush_sq(ring) [queue.c:203] │ │ sq->sqe_head = sq->sqe_tail = tail; │ │ if (!SQPOLL) *sq->ktail = tail; // 普通 store │ │ else smp_store_release(*sq->ktail, tail); // 给内核线程看 │ │ return tail - READ_ONCE(*sq->khead); │ │ ┌─ liburing_sanitize_ring(ring) // ASan only │ │ ┌─ sq_ring_needs_enter(ring, submitted, &flags) [queue.c:17] │ │ 非 SQPOLL ⇒ true(必须 enter) │ │ SQPOLL:smp_mb() 后读 *sq.kflags 看 NEED_WAKEUP │ ▼ if (sq_ring_needs_enter || cq_needs_enter) __sys_io_uring_enter(fd, submitted, 0, flags, NULL) ◄── syscall 426 else return submitted; ★ SQPOLL 0 syscall 路径

③ 等待 / 收割 CQE:io_uring_wait_cqeio_uring_cqe_seen

app │ ▼ io_uring_wait_cqe(&ring, &cqe) [liburing.h:1921] └─► __io_uring_peek_cqe(ring, &cqe, NULL) [liburing.h:1866] │ tail = smp_load_acquire(*cq.ktail); // 关键 acquire │ if (head != tail) { *cqe = cqes[(head & mask)<<shift]; return 0; } │ else *cqe = NULL; │ ▼ (没有现成的 CQE?) io_uring_wait_cqe_nr(ring, cqe_ptr, 1) └─► __io_uring_get_cqe(ring, cqe_ptr, 0, 1, NULL) [queue.c:133] └─► _io_uring_get_cqe(ring, cqe_ptr, &data) [queue.c:62] │ do { │ __io_uring_peek_cqe(ring, &cqe, &nr_avail); // 再 peek 一次 │ if (cqe || nr_avail >= wait_nr) break (no enter); │ // 必须进内核 │ __sys_io_uring_enter2(fd, submit, wait_nr, ◄── syscall 426 / EXT_ARG │ IORING_ENTER_GETEVENTS | flags, arg, sz); │ // 醒来后回去 peek │ } while (1); │ 返回后 app 处理 cqe,然后: │ ▼ io_uring_cqe_seen(&ring, cqe) [liburing.h:507] └─► io_uring_cq_advance(ring, io_uring_cqe_nr(cqe)) [liburing.h:489] └─► smp_store_release(cq.khead, *cq.khead + nr); // 关键 release,告诉内核 slot 可重用

批量收割推荐用迭代器宏 + 一次 cq_advance(n)

io_uring_for_each_cqe(ring, head, cqe) { [liburing.h:481] process(cqe); // 内部展开为 io_uring_cqe_iter,仅做一次 acquire load tail n++; } io_uring_cq_advance(ring, n); // 仅做一次 release store head

④ submit + wait 一体化:io_uring_submit_and_wait_timeout

io_uring_submit_and_wait_timeout(ring, &cqe, wait_nr, ts, sigmask) [queue.c:408] └─► __io_uring_submit_and_wait_timeout(ring, &cqe, wait_nr, ts, 0, sigmask) [queue.c:360] │ ├── if ts && FEAT_EXT_ARG ─► 构造 io_uring_getevents_arg │ get_data{ submit=__io_uring_flush_sq(ring), wait_nr, EXT_ARG, &arg, has_ts } │ └─► _io_uring_get_cqe(ring, &cqe, &data) [queue.c:62] │ └─► __sys_io_uring_enter2(...) // 一次 syscall 同时 │ // ① 提交 ② 带 timeout 等 │ ├── if ts && !FEAT_EXT_ARG (老内核 ≤5.10) │ to_submit = __io_uring_submit_timeout(ring, wait_nr, ts) [queue.c:287] │ // 内部 prep_timeout → 占用 1 个 SQE 模拟 timeout │ return __io_uring_get_cqe(ring, &cqe, to_submit, wait_nr, sigmask); │ └── else (无 ts) to_submit = __io_uring_flush_sq(ring); return __io_uring_get_cqe(ring, &cqe, to_submit, wait_nr, sigmask);

⑤ 资源注册:io_uring_register_* 全部走的统一通道

io_uring_register_buffers(ring, iovecs, n) [register.c:73] io_uring_register_files(ring, files, n) [register.c:191] io_uring_register_ring_fd(ring) [register.c:~] io_uring_register_buf_ring(ring, ®, flags) [register.c:~] io_uring_register_eventfd(ring, fd) [register.c:~] io_uring_register_napi(ring, &napi) [register.c:~] io_uring_register_iowq_max_workers(ring, val) [register.c:~] io_uring_register_restrictions(ring, res, n) [register.c:~] io_uring_register_clock(ring, &arg) [register.c:~] ...30+ register_* / unregister_* 函数... │ ▼ 全都调用同一个 helper: do_register(ring, opcode, arg, nr_args) [register.c:13] │ // 1. ASan 校验地址 │ liburing_sanitize_address(arg); │ // 2. 选择 fd:若 ring 已 register 自己,则带 USE_REGISTERED_RING 位 │ if (ring->int_flags & INT_FLAG_REG_REG_RING) { │ opcode |= IORING_REGISTER_USE_REGISTERED_RING; │ fd = ring->enter_ring_fd; │ } else fd = ring->ring_fd; │ ▼ __sys_io_uring_register(fd, opcode, arg, nr_args) ◄── syscall 427

⑥ 销毁流程:io_uring_queue_exit

io_uring_queue_exit(ring) [setup.c:452] │ ├── if (!INT_FLAG_APP_MEM) // app 自带 buf 时不释放 │ __sys_munmap(sq.sqes, sq.sqes_sz); │ io_uring_unmap_rings(sq, cq) [setup.c:62] │ __sys_munmap(sq.ring_ptr, sq.ring_sz); │ if (cq != sq) __sys_munmap(cq.ring_ptr, cq.ring_sz); │ ├── if (INT_FLAG_REG_RING) │ io_uring_unregister_ring_fd(ring) // 释放 enter_ring_fd 注册槽 │ └── if (ring_fd != -1) __sys_close(ring_fd);

12.5.7 内存屏障地图(最容易出 bug 的点)

地点 动作 为什么必须这样
__io_uring_flush_sq **queue.c:**225-228 SQPOLL 模式下 smp_store_release(sq.ktail, tail) 内核线程在另一 CPU 上轮询;必须保证 SQE 内容写入 先于 tail 推进,否则它会读到 stale 的 sqe。
sq_ring_needs_enter **queue.c:**31-37 SQPOLL:先 smp_mb() 再读 *sq.kflags & NEED_WAKEUP tail 写入与 flag 读取需要全屏障;否则可能漏唤醒。
io_uring_load_sq_head **liburing.h:**1712-1715 SQPOLL 用 smp_load_acquire(sq.khead),否则普通读 非 SQPOLL 时 head 由用户驱动,无需 acquire;SQPOLL 时内核会推进 head,必须 acquire 才能保证后续读 SQE 内容是新的。
__io_uring_peek_cqe **liburing.h:**1866 tail = smp_load_acquire(cq.ktail) 读 CQE 内容前必须 acquire tail;否则可能读到内核还没写完的 CQE。
io_uring_cq_advance **liburing.h:**499 smp_store_release(cq.khead, head + nr) 必须保证应用对 CQE 内容的所有读 先于 head 推进;否则内核可能复用 slot 写入新 CQE,导致 use-after-free 类似问题。
io_uring_buf_ring_advance **liburing.h:**1996 smp_store_release(&br->tail, new_tail) 同理:buffer 内容必须先写好,再让内核看到新 tail。

12.6 源码逐文件深度分析 NEW

12.6.1 src/queue.c(497 行)--- SQ flush / CQE 收割中枢

函数 / 结构 角色
17--40 sq_ring_needs_enter 判断是否真的需要进内核(非 SQPOLL 一律 true;SQPOLL 看 NEED_WAKEUP),fast path 的核心分支
42--51 cq_ring_needs_flush / cq_ring_needs_enter 检查 CQ 是否溢出 / 有 task work 待跑(IORING_SQ_CQ_OVERFLOW / IORING_SQ_TASKRUN)。
53--60 struct get_data 把 submit/wait_nr/flags/timeout/sigmask 打包到一处,避免函数参数列表爆炸。
62--131 _io_uring_get_cqe 主循环:peek → 决定是否 enter → enter 后再 peek。所有 wait 类 API 最终都到这里。
133--145 __io_uring_get_cqe 带 sigmask 的薄壳;构造 get_data{ sz=_NSIG/8, arg=sigmask }
147--152 io_uring_get_events 仅做 GETEVENTS 的 enter,不提交不等待,用于"刷一下 task work"。
154--197 io_uring_peek_batch_cqe(_) 批量取 CQE 的非阻塞接口;如果 CQ 满则触发 flush。
203--238 __io_uring_flush_sq 把本地 sqe_tail 推到共享 *ktail,包含 SQ_REWIND 特殊处理与 SQPOLL release barrier
244--268 io_uring_wait_cqes_new 使用 IORING_ENTER_EXT_ARG 一次 syscall 完成「带 timeout 的等待」。
287--308 __io_uring_submit_timeout 老内核(无 EXT_ARG)的兼容路径:消耗 1 个 SQE 做 timeout。
310--326 io_uring_wait_cqes 对外的 wait API;分新内核/老内核分支。
338--358 io_uring_submit_and_wait_reg 已注册的 reg_wait 数组做等待,省去每次构造 getevents_arg 的成本(最新优化)。
360--416 __io_uring_submit_and_wait_timeout 系列 统一处理「flush + enter(GETEVENTS+EXT_ARG)」,分老/新内核。
434--453 __io_uring_submit 整个库的 fast path 心脏:决定 enter / 不 enter,是否带 GETEVENTS。
455--478 io_uring_submit / submit_and_wait 对外 API;分别 wait_nr=0 / wait_nr=N。
480--483 io_uring_submit_and_get_events 提交后立刻取一波 events(不阻塞)。
486--490 io_uring_get_sqe (LIBURING_INTERNAL) 给"老程序链接 .so"留的兼容符号,转调头文件中的 inline 版本。
492--497 __io_uring_sqring_wait SQPOLL 时等待 SQ 有空闲槽位。

12.6.2 src/setup.c(709 行)--- ring 创建 / mmap / probe

函数 / 结构 角色
15--25 __fls / roundup_pow2 将 entries 向上取整到 2 的幂,是 mask 优化的前提。
27--60 get_sq_cq_entries 校验/夹紧 SQ/CQ 大小(CQ 默认 2× SQ;可被 IORING_SETUP_CQSIZE 覆盖)。
62--68 io_uring_unmap_rings 对应 io_uring_mmap 的释放;**处理 SINGLE_MMAP(cq.ring_ptr == sq.ring_ptr)**避免双 munmap。
70--96 io_uring_setup_ring_pointers 把 sq/cq 内部指针指向 mmap 偏移,是"用户态如何看到内核的 head/tail"的关键所在。
110--162 io_uring_mmap 实际 mmap 三段(或 SINGLE_MMAP 二段):SQ_RING / CQ_RING / SQES。
170--175 io_uring_queue_mmap __cold 对外暴露给"已经自己 setup 拿到 fd 的高级用户"。
187--213 io_uring_ring_dontfork __cold 对所有 mmap 区域做 madvise(MADV_DONTFORK),防止 fork 后子进程拿到危险的共享内存。
223--310 io_uring_alloc_huge NO_MMAP 模式:用 hugetlb 大页让多个 ring 共享一段大块内存(per-core 架构必备)。
312--387 __io_uring_queue_init_params 核心组装函数:alloc_huge? → __sys_io_uring_setupqueue_mmap → 填 sq_array → 设置 int_flags。
389--405 io_uring_queue_init_try_nosqarr "先 try NO_SQARRAY,老内核 -EINVAL 就退化"------这是 liburing 在新老内核兼容的典型套路。
419--435 io_uring_queue_init_mem / params 对外 API 的两个变体(自带内存 / 普通)。
441--450 io_uring_queue_init __cold "hello world" 入口,最常被 app 调用。
452--470 io_uring_queue_exit __cold 对应清理:munmap → unregister_ring_fd → close。
472--510 io_uring_get_probe(_ring) / free_probe 查询内核支持哪些 op;常用于 "feature detection"。
512--612 rings_size / memory_size / mlock_size 系列 给应用算所需 mlock 内存量(5.11- 内核才有意义)。
614--709 br_setup / setup_buf_ring / free_buf_ring provided buffer ring 的 mmap 包装;hppa 上走 IOU_PBUF_RING_MMAP,其他平台 anon mmap + register。

12.6.3 src/register.c(529 行)--- 所有 register_*

函数 对应 IORING_REGISTER_* opcode
13--28 do_register 统一通道;处理 USE_REGISTERED_RING 改写
30--84 register_buffers / _tags / _sparse / _update_tag / unregister_buffers BUFFERS / BUFFERS2 / BUFFERS_UPDATE
86--230 register_files / _tags / _sparse / _update / unregister_files FILES / FILES2 / FILES_UPDATE / FILES_UPDATE2
~230--340 register_eventfd / unregister / async EVENTFD / EVENTFD_ASYNC
~340--420 register_personality / restrictions / enable_rings / iowq_aff / iowq_max_workers PERSONALITY / RESTRICTIONS / ENABLE_RINGS / IOWQ_AFF / IOWQ_MAX_WORKERS
~420--500 register_buf_ring / unregister_buf_ring / register_napi / clock / region / zcrx_ifq PBUF_RING / NAPI / CLOCK / REGION / ZCRX_IFQ
~500--529 register_sync_msg / register_ring_fd / unregister_ring_fd SYNC_MSG(5.18+ 同步取消 / 跨 ring)与 RING_FDS

共同模式 :所有函数都只做"打包结构 + 调 do_register"。真正复杂度全在内核侧,liburing 这一层只是 ABI 翻译。

12.6.4 src/include/liburing.h(2149 行)--- 头文件即库

头文件分 5 个区块:

行号区段 内容
1--80 License / 包含 / 类型 forward / 64-bit data 宏
78--180 核心数据结构struct io_uring_sq / io_uring_cq / io_uring
180--500 对外 函数原型 (实现在 queue.c / setup.c / register.c)+ 队列辅助 inline(cq_advance / cqe_seen / sqe_set_data / sqe_set_flags / cqe_iter
500--1700 1500+ 行io_uring_prep_* 内联辅助。每个 op 一个函数,互相通过 io_uring_prep_rw(行 592)共享底层。
1700--2149 查询型 inline(sq_ready / sq_space_left / cq_ready / cq_has_overflow / load_sq_head);get_sqe / get_sqe128buf_ring helpers;FFI 兼容符号;C++ extern "C" 收尾。

为什么 get_sqe 既是 inline 又是 .so 符号?

liburing-2.2 把 io_uring_get_sqe 改成了 static inline 以提速;但旧二进制还在动态链接 liburing.so:io_uring_get_sqe 这个符号。为了保持 ABI 兼容,queue.c:486LIBURING_INTERNAL 宏下重新提供了一个非 inline 的导出符号,内部直接转调 _io_uring_get_sqe。这是大型 C 库进行 ABI 平滑迁移的教科书案例 。详见 commit 8be8af4afcb452dcdbba35c8

12.6.5 src/include/liburing/io_uring.h(1124 行)--- 内核 UAPI

这个文件是 从 Linux 内核 include/uapi/linux/io_uring.h 同步过来的,liburing 不修改它(只随内核更新)。它定义了所有应用 ↔ 内核之间共享的 ABI:

  • 第 30--114 行:struct io_uring_sqe(64B / 128B 联合体大杂烩)
  • 第 ~120--240 行:IOSQE_* sqe flags / IORING_SETUP_* setup flags / IORING_FEAT_* 内核特性 / IORING_ENTER_* enter flags
  • 第 249--317 行:enum io_uring_op ------ 65 个 opcode(IORING_OP_NOP ... IORING_OP_URING_CMD128)
  • 第 ~430--490 行:struct io_uring_cqe + IORING_CQE_F_*
  • 第 ~500--570 行:struct io_uring_params / sq_off / cq_off(mmap 偏移描述符)
  • 第 ~600--1100 行:register/probe/restriction/buf_ring/msg_ring/zcrx/napi/region 等扩展结构

12.7 DeepWiki 章节对照与关键摘录 NEW

本节把 DeepWiki @ axboe/liburing(last indexed: e3d35ea5,2026-02-09)的章节结构与本文档逐一对照,方便交叉验证。

DeepWiki 章节 本文档对应位置 关键结论(DeepWiki 摘录 + 本文校验)
1. Overview § 1, § 2 "io_uring uses a pair of shared memory ring buffers... eliminates much of the overhead associated with traditional async I/O." ------ 与本地源码 src/setup.c:110-162(mmap 三段)一致。
1.1 Architecture § 3, § 5, § 11.5, § 12.5 提到三类共享 mmap、SQ/CQ 结构、5 类 syscall flag、SQPOLL / IOPOLL / NO_SQARRAY 三种特殊模式。本文档进一步给出了真实代码行号。
2.1 Initialization and Setup § 6, § 12.5①, § 12.6.2 明确指出 io_uring_queue_init__io_uring_queue_init_params__sys_io_uring_setup + io_uring_queue_mmap,与本文 § 12.5① 调用图完全一致。
2.2 Submission and Completion Queues § 6, § 12.5②③④, § 12.6.1 "After preparing one or more SQEs, submit them to the kernel: io_uring_submit() [src/queue.c:465-468] flushes by ① 更新 SQ tail [src/queue.c:203-238] ② 调用 io_uring_enter [src/queue.c:434-453]"。本文 § 12.5② 把这条链画成了完整 6 步图。
2.3 Resource Registration § 9, § 12.5⑤, § 12.6.3 列出 BUFFERS / FILES / EVENTFD / PBUF_RING 等 opcode;本文 § 12.5⑤ 进一步指出"所有 register 函数共用 do_register"这一关键统一点。
2.4 Buffer Management § 9 (Provided Buffer Ring) provided buffer ring 的工作原理(应用挂 buffer / 内核选 / CQE 带 BUFFER flag)。
3. I/O Operations § 7 "60+ io_uring_prep_* functions [src/include/liburing.h:584-1500]" ------ 与本文 § 7 一致。
4.4 SQPOLL and Advanced Modes § 8, § 12, § 12.5⑦ "No syscalls for submission when SQPOLL thread active ... check IORING_SQ_NEED_WAKEUP before assuming polling active"。完美对应本文 § 12.1 fast path 与 sq_ring_needs_enter(queue.c:17)。
4.1 Linked Ops / 4.2 Multishot / 4.3 Cancel § 10 对应本文 § 10 的 ① / ② / ③ 三张高级特性卡片。
5. Example Applications § 11 (examples 子目录), § 6 最小示例 "proxy.c / send-zerocopy.c / zcrx.c"。本文已在 § 8 推荐 proxy.c 作为新代码起点。
6. Testing & Validation § 11 (test/ 268 个用例) DeepWiki 强调 "测试本身就是文档"------本文 § 11 已点明仓库的"重头戏在 test/ 而非 src/"。
12.7.1 DeepWiki 的内存屏障表(直接引用)
位置 操作 目的
SQ tail update [queue.c:228] store_release(*ktail, tail) Ensure SQEs written before tail visible
CQ tail read [liburing.h:1866] load_acquire(*ktail) Ensure CQEs loaded after tail read
CQ head update [liburing.h:499] store_release(*khead, head) Ensure CQEs processed before head advanced
SQPOLL check [queue.c:31] smp_mb() Ordering of tail write vs flag read

DeepWiki vs 本文档定位差异

• DeepWiki 章节多为 API 索引型 文档(每章一个 API 类目),覆盖广但不画完整调用链;

• 本文档 § 12.5 / § 12.6 是**「完整调用链 + 行号锚点」** 视角,更适合"想读懂 fast path 是怎么做到 0 syscall"的开发者。

两者建议配合使用 :用 DeepWiki 找入口 → 用本文档跟踪到具体行 → 用 git blame 看历史。

13. 性能特性与对比 epoll

维度 epoll + read/write io_uring
异步类型 "就绪通知",I/O 本身仍同步 真正异步:发起后立刻返回
支持的 fd socket / pipe / eventfd(不支持普通 file) 所有:file / socket / pipe / signal / device / dirent / ...
每次 op 的 syscall 数 1 (epoll_wait) + 1 (read/write) ≥ 2 0~1(SQPOLL 时可全程 0)
批处理 需手动循环 1 次 enter 提交/收割任意多
buffer 拷贝 每次 read/write 一次 register_buffers + READ_FIXED 后免拷贝
fd 表锁 direct descriptor 完全绕开
内核中断 每个 I/O 完成都中断 IOPOLL / NAPI busy poll 可消灭中断
编程难度 简单 需理解 SQ/CQ + 各种 flag
跨平台 Linux 2.6+ 仅 Linux 5.1+(5.6+ 才稳定,6.x 大量新 feature)
典型 benchmark 数据(社区报告,因负载不同会大幅波动)
  • NVMe 4K 随机读:epoll/libaio ~1.0M IOPS → io_uring ~3.5M IOPS(IOPOLL 模式)
  • echo server 100 字节包:epoll ~600k QPS → io_uring multishot recv + send_zc ~2.5M QPS(per-core)
  • nginx 替换为 io_uring 后端(实验项目):吞吐 +15~30%
  • Postgres 16 io_uring 后端(社区 fork):随机读 +50%

14. 业界采用情况

项目 用途 状态
ScyllaDB / Seastar 后端存储 I/O,per-core 模型与 io_uring 天然契合 已生产
Redpanda C++ 重写 Kafka,磁盘 + 网络全 io_uring 已生产
PostgreSQL 17/18 新增 io_method=io_uring(异步 I/O backend) 已合入主干
Ceph BlueStore 的 io_uring backend 稳定可选
QEMU virtio-blk / virtio-net 的后端 已生产
Tokio (Rust) Tokio 提供 tokio-uring crate;某些子项目(如 Cloudflare Pingora 的实验分支)已用 稳定
libuv 5.1+ 内核自动用 io_uring 做文件 I/O;网络仍 epoll 已生产
Node.js 通过 libuv 间接受益 已生产
fio Jens Axboe 自己的工具,io_uring engine 是其首选 已生产
RocksDB Multi-Get + io_uring 实验 实验
Cloudflare 边缘服务的 file I/O 已采用
Meta folly 内部 IoUringBackend 已生产

15. 坑、限制与最佳实践

15.1 内核版本依赖

kernel 关键里程碑
5.1 io_uring 引入,仅基础 read/write
5.5--5.7 SQPOLL 改进、buffered file IO
5.10 FEAT_EXT_ARG,wait timeout 不再需要发额外 SQE
5.13 稀疏注册(register_files_sparse
5.18 COOP_TASKRUN、SINGLE_ISSUER、register_ring_fd
5.19 provided buffer ring
6.0 DEFER_TASKRUN(性能跃升)
6.4--6.6 NO_SQARRAY、NAPI、READ_MULTISHOT
6.8+ FUTEX、ZCRX、io_uring zerocopy recv

反过来:liburing 不依赖特定 kernel。新 liburing 在旧 kernel 上仍能编译运行,只是不能用新 op;同样旧 liburing 在新 kernel 上也能跑。

15.2 常见坑

  • CQ overflow :CQ 默认是 SQ 的 2 倍,但 multishot / SEND_ZC 会产生不止 1 个 CQE,可能挤满。务必 检查 IORING_SQ_CQ_OVERFLOW 或开 NODROP
  • MEMLOCK 限制 :5.11- 内核需要 RLIMIT_MEMLOCK 足够大;5.12+ 改为 cgroup 计费。
  • Fork 后行为 :mmap 区域 fork 后会跟着子进程;使用 io_uring_ring_dontfork() 防止。
  • SQPOLL CPU 占用 100% :如果没流量也会一直跑,记得调 sq_thread_idle
  • 同一线程 submit + wait :DEFER_TASKRUN 要求"谁创建 ring 谁等待",跨线程会 EEXIST
  • fixed buffer 与 fork/munmap:注册的 buffer 一旦被 munmap 或 fork-CoW 就会出问题,注册前最好 mlock。
  • 安全 CVE 历史 :5.x 早期存在多个权限提升 CVE,导致部分发行版(如 ChromeOS、Android、Google Production)默认禁用 io_uring。生产部署前检查内核版本与补丁。

15.3 最佳实践速记

  • 新代码起手:SINGLE_ISSUER | DEFER_TASKRUN | COOP_TASKRUN
  • 大量长连接:multishot accept + multishot recv + provided buffer ring + send_zc。
  • NVMe:IOPOLL + READ_FIXED
  • echo / proxy 服务:参考 examples/proxy.c,已是当前最优实践。
  • 多 ring:每核一个 ring + MSG_RING 做 ring 间通信。
  • 跨语言:链接 liburing-ffi 而非 liburing,避免 inline 符号丢失。

16. 总览速查表

维度 内容
项目定位 Linux io_uring 内核接口的官方 C 用户态封装库
作者 / 维护 Jens Axboe(也是 kernel io_uring / fio / blktrace 维护者)
License MIT(库本体),GPL-2.0 / Linux-syscall-note(kernel UAPI 头)
语言 C(核心 ~2k 行实现 + 2k 行 inline helpers)
kernel 依赖 5.1+;推荐 6.0+;最新 op 需 6.6+
核心抽象 SQ ring(提交) + CQ ring(完成) + SQE 数组(共享 mmap)
三个 syscall io_uring_setup / enter / register
支持的 op 数 60+(文件 / 网络 / 元数据 / poll / 同步原语 / passthrough)
性能关键 共享内存批量 + SQPOLL 零 syscall + IOPOLL 无中断 + register 减拷贝 + multishot 减重复
典型用户 ScyllaDB / Redpanda / PostgreSQL 17 / Ceph / QEMU / fio / Cloudflare / Meta(folly) / Tokio
主要风险 kernel 版本碎片化;早期 CVE 历史;编程模型门槛
替代方案 epoll + libaio(旧)、SPDK(NVMe 用户态全栈)、DPDK(网络用户态全栈)
相关项目 kernel fs/io_uring.c;fio --ioengine=io_uringtokio-uring(Rust);Java io_uring binding

一句话总结
io_uring 是过去 20 年 Linux 异步 I/O 第一次"做对"的方案;liburing 是它的官方"人话"封装。 对于运行在 Linux 5.1+ 的高性能服务(数据库、流系统、代理、HFT),io_uring + liburing 已经是 2024+ 的事实首选

17. liburing 公共 API 大全(按功能分组) NEW

本节是面向使用者的接口字典 :覆盖 liburing 2.9(当前 src/liburing.map 中所有导出符号)+ src/include/liburing.h 中所有 IOURINGINLINE 内联帮助函数。

掌握下表后,日常应用层代码无需再读任何源码 ,只需要:① 找到所属类目,② 看签名与一句话语义,③ 必要时 man 3 函数名 看完整手册。

命名约定速记

io_uring_*:所有公开 API 的统一前缀(前缀 __io_uring_* 为内部辅助、应用一般不直接调用)。

io_uring_prep_*:填充一个 SQE 的"操作准备"函数 ------ 102 个,覆盖所有 IORING_OP_*

io_uring_register_* / unregister_*:资源预注册到 ring 的管理函数。

io_uring_*_direct:使用 direct descriptor (固定 fd 表槽位)而非真实 fd 的变种。

io_uring_*_fixed:使用 fixed buffer (预注册 iovec)而非裸 buf 的变种。

*_multishot:一次提交、多次产生 CQE 的"长效"变种。

*_zc:zero-copy 变种(需 send_zc 通知 CQE)。

17.1 库版本与能力探测

函数 / 宏 签名 语义
io_uring_major_version int (void) 动态库主版本号(例如 2)。
io_uring_minor_version int (void) 动态库次版本号(例如 9)。
io_uring_check_version bool (int major, int minor) 判断 运行时 库 ≥ 给定版本。
IO_URING_VERSION_MAJOR / IO_URING_VERSION_MINOR 宏(编译期常量) 编译时 头文件版本,用于条件编译。
IO_URING_CHECK_VERSION(maj, min) 编译时版本比较。
io_uring_get_probe / io_uring_get_probe_ring struct io_uring_probe *(void) / (struct io_uring *) 申请并填充 probe(当前内核支持的 op 列表)。
io_uring_free_probe void (struct io_uring_probe *) 释放 probe。
io_uring_opcode_supported int (const struct io_uring_probe *p, int op) 判断某个 IORING_OP_* 在当前内核是否可用。
io_uring_register_probe int (ring, probe, nr) 同上但直接走 ring。

17.2 Ring 生命周期:创建 / 销毁 / 重塑

函数 签名 语义
io_uring_queue_init int (unsigned entries, struct io_uring *ring, unsigned flags) 最常用入口 。entries=SQ 深度(向上取 2 幂),flags 为 IORING_SETUP_*
io_uring_queue_init_params int (entries, ring, struct io_uring_params *p) 带参版本:可指定 SQPOLL/IOPOLL/CQ 大小/wq_fd 等所有 setup 参数。
io_uring_queue_init_mem int (entries, ring, params, void *buf, size_t buf_size) 2.5+:用调用方提供的内存(已 mmap 或大页)做 ring,零额外 mmap。
io_uring_queue_mmap int (int fd, params, ring) 底层 mmap 函数,一般通过上面三个间接调用。
io_uring_queue_exit void (struct io_uring *ring) 反向:munmap + close。
io_uring_ring_dontfork int (ring) 对 ring 内存执行 madvise(MADV_DONTFORK),防止 fork 后子进程继承 ring。
io_uring_resize_rings int (ring, params) 2.9+:动态重塑 SQ/CQ 大小(部分内核版本支持)。
io_uring_mlock_size ssize_t (unsigned entries, unsigned flags) 5.11- 内核估算 mlock 占用(5.12+ 已不需要)。
io_uring_mlock_size_params ssize_t (entries, params) 同上,带参版本。
io_uring_memory_size ssize_t (entries, flags) 2.11+:估算 ring 总内存占用。
io_uring_memory_size_params ssize_t (entries, params) 同上,带参。
io_uring_enable_rings int (ring) 对配合 IORING_SETUP_R_DISABLED 创建的 ring 启用提交(先注册 restrictions 再启用)。

17.3 原始 syscall 封装(直通内核)

这些是给"高级用户 / 自定义封装层"用的,普通应用使用 §17.2/17.4/17.5 即可。

函数 签名 语义
io_uring_setup int (unsigned entries, struct io_uring_params *p) 直通 __NR_io_uring_setup
io_uring_enter int (fd, to_submit, min_complete, flags, sigset_t *) 直通 __NR_io_uring_enter(旧签名)。
io_uring_enter2 int (fd, to_submit, min_complete, flags, void *arg, size_t sz) io_uring_getevents_arg 的新签名,支持 ext_arg。
io_uring_register int (fd, opcode, const void *arg, nr_args) 直通 __NR_io_uring_register

17.4 SQE 获取与提交(提交侧)

函数 签名 语义
io_uring_get_sqe struct io_uring_sqe *(struct io_uring *) 从 SQ 取一个空 SQE;满了返回 NULL。
io_uring_get_sqe128 struct io_uring_sqe *(ring) 同上,但用于 SQE128 模式(128 字节大 SQE)。
io_uring_sqe_set_data void (sqe, void *data) 设置 user_data 指针(应用层 cookie)。
io_uring_sqe_set_data64 void (sqe, __u64) 同上,64 位整型版本。
io_uring_sqe_set_flags void (sqe, unsigned) 设置 IOSQE_*(FIXED_FILE/IO_LINK/ASYNC/BUFFER_SELECT...)。
io_uring_sqe_set_buf_group void (sqe, int bgid) 设置 provided buffer group ID + IOSQE_BUFFER_SELECT
io_uring_submit int (struct io_uring *) 提交所有已 prep 的 SQE;返回实际提交数。
io_uring_submit_and_wait int (ring, unsigned wait_nr) 提交并阻塞等待 ≥ wait_nr 个 CQE。
io_uring_submit_and_wait_timeout int (ring, **cqe, wait_nr, ts, sigmask) 带超时版本。
io_uring_submit_and_wait_min_timeout int (ring, **cqe, wait_nr, ts, min_wait_us, sigmask) 2.8+:min/max 双超时(避免短间隔过多唤醒)。
io_uring_submit_and_wait_reg int (ring, **cqe, wait_nr, int reg_index) 2.9+:使用预注册的 wait 参数(见 17.10),零结构体复制
io_uring_submit_and_get_events int (ring) 提交 + 立即收割(不阻塞)。
io_uring_sq_ready unsigned (const ring) SQ 中已 prep 但未提交的 SQE 数量。
io_uring_sq_space_left unsigned (const ring) SQ 剩余可写空间。
io_uring_sqring_wait int (ring) SQPOLL 模式下等待 SQ 出现空闲槽位。
io_uring_load_sq_head unsigned (const ring) 读 SQ head(含 acquire 屏障)。

17.5 CQE 收割(完成侧)

函数 / 宏 签名 语义
io_uring_wait_cqe int (ring, struct io_uring_cqe **) 阻塞等待 1 个 CQE。
io_uring_wait_cqe_nr int (ring, **cqe, unsigned nr) 等待 ≥ nr 个 CQE。
io_uring_wait_cqe_timeout int (ring, **cqe, ts) 带超时(基于 FEAT_EXT_ARG,无需消耗 SQE)。
io_uring_wait_cqes int (ring, **cqe, wait_nr, ts, sigmask) 带 sigmask + 超时的批量等待。
io_uring_wait_cqes_min_timeout int (ring, **cqe, wait_nr, ts, min_ts_usec, sigmask) 2.8+:min/max 双超时。
io_uring_peek_cqe int (ring, **cqe) 非阻塞 peek 一个 CQE(无则 -EAGAIN)。
io_uring_peek_batch_cqe unsigned (ring, **cqes, count) 批量 peek,返回实际数量。
io_uring_for_each_cqe(ring, head, cqe) 遍历 所有已 ready 的 CQE(最常用 fast path 收割)。
io_uring_cqe_seen void (ring, cqe) 消费 1 个 CQE:等价 cq_advance(1)
io_uring_cq_advance void (ring, nr) 批量消费 nr 个 CQE(与 for_each_cqe 配合)。
io_uring_cq_ready unsigned (const ring) CQ 中已就绪未消费数。
io_uring_cq_has_overflow bool (const ring) 是否发生过 CQ overflow(务必 检查!)。
io_uring_get_events int (ring) 仅触发内核 flush overflow 队列、不提交 SQE。
io_uring_cqe_get_data / _get_data64 void */__u64 (const cqe) 从 CQE 取 user_data。
io_uring_cqe_nr unsigned (const cqe) CQE32 模式下:当前 cqe 占几个槽。
io_uring_cqe_iter_init / _iter_next 结构 + 迭代器 遍历 CQE32 的辅助器(自动处理大 CQE 步长)。

17.6 CQ eventfd / 时钟 / iowait

函数 语义
io_uring_register_eventfd 注册一个 eventfd,每次 CQE 到达内核写一次 → epoll 兼容。
io_uring_register_eventfd_async 仅"异步完成"才写 eventfd(同步完成不打扰)。
io_uring_unregister_eventfd 取消注册。
io_uring_cq_eventfd_enabled 当前 eventfd 通知是否开启(运行时可切换)。
io_uring_cq_eventfd_toggle 动态开/关 eventfd 通知。
io_uring_register_clock 2.8+:切换 ring 内部 timer 用的时钟源(如 CLOCK_MONOTONICCLOCK_BOOTTIME)。
io_uring_set_iowait 2.10+:是否让 io_uring 等待计入进程 iowait 统计(影响 %iowait、cpufreq)。

17.7 Fixed Buffer(注册 iovec,零拷贝读写)

函数 语义
io_uring_register_buffers 注册一组 iovec,之后用 read_fixed/write_fixed/send_zc_fixed 等通过 buf_index 引用。
io_uring_register_buffers_tags 同上,附带 64 位 tag(解绑时通过 CQE 通知)。
io_uring_register_buffers_sparse 预留 nr 个 空槽位,后续 update 填充。
io_uring_register_buffers_update_tag 更新已注册槽位(含 tag)。
io_uring_unregister_buffers 解绑全部。
io_uring_clone_buffers / _offset 2.8/2.9+:把 src ring 的注册 buffer 克隆到 dst ring(多 ring 共享,无需重新 pin)。

17.8 Buffer Ring(提供型 buffer,配合 multishot recv/read)

函数 语义
io_uring_setup_buf_ring 一站式 :mmap + register_buf_ring,返回 io_uring_buf_ring*
io_uring_free_buf_ring 一站式销毁。
io_uring_register_buf_ring 底层注册(应用已自行分配 ring 时用)。
io_uring_unregister_buf_ring 底层取消注册。
io_uring_buf_ring_init 初始化 ring 的 tail。
io_uring_buf_ring_mask 给定 nentries 返回 mask = nentries-1。
io_uring_buf_ring_add 追加 1 个 buffer:(addr, len, bid, mask, buf_offset)。
io_uring_buf_ring_advance 推进 tail 暴露 N 个 buffer 给内核。
io_uring_buf_ring_cq_advance 合并:消费 N 个 CQE + 推进 buf_ring tail(最常用 fast path)。
__io_uring_buf_ring_cq_advance 同上的内部低层版本(已知 mask 时省一次取)。
io_uring_buf_ring_available 内核侧已未消费的 buffer 数。
io_uring_buf_ring_head 2.6+:从内核读 buf_ring head(监控/调优用)。

17.9 Fixed Files / Direct Descriptor(注册 fd 表)

函数 语义
io_uring_register_files 注册一组 fd 到内核侧 fd 表,op 通过 index 引用并设 IOSQE_FIXED_FILE
io_uring_register_files_tags 带 tag 版本。
io_uring_register_files_sparse 预留 nr 个空槽。
io_uring_register_files_update 动态更新槽位(替换 fd)。
io_uring_register_files_update_tag 同上带 tag。
io_uring_register_file_alloc_range 规定 IORING_FILE_INDEX_ALLOC 的可用槽位区间。
io_uring_unregister_files 解绑全部。

17.10 Ring fd 注册与 wait 参数预注册

函数 语义
io_uring_register_ring_fd 注册 ring fd 本身:之后所有 io_uring_enter 不需要带 ring fd 参数(关键 fast path 优化)。
io_uring_unregister_ring_fd 取消。
io_uring_close_ring_fd 关闭 ring fd(在 register_ring_fd 后可立即关)。
io_uring_register_wait_reg 2.9+:把一批 io_uring_reg_wait(含 timeout/sigmask/min_wait)预注册,后续用 index 引用 → 零参数复制

17.11 Personality / Restrictions / 安全

函数 语义
io_uring_register_personality 注册当前进程凭证为 personality;op 通过 sqe->personality 切换身份执行(特权/降权场景)。
io_uring_unregister_personality 解绑一个 personality id。
io_uring_register_restrictions 配合 IORING_SETUP_R_DISABLED:限制 ring 只能用某些 op / register / sqe_flags。提交白名单。
io_uring_register_bpf_filter / _task 2.15+:用 BPF 程序过滤 op(seccomp 风格)。

17.12 io-wq 线程池与 NAPI

函数 语义
io_uring_register_iowq_aff 设置 io-wq 线程 CPU 亲和(要求 _GNU_SOURCE + cpu_set_t)。
io_uring_unregister_iowq_aff 清除亲和。
io_uring_register_iowq_max_workers 设置 bounded/unbounded 最大 worker 数(values[0]/values[1])。
io_uring_register_napi 开启 NAPI busy poll(极低延迟网络)。
io_uring_unregister_napi 关闭。

17.13 同步操作 / 内存区域 / ZCRX

函数 语义
io_uring_register_sync_cancel 从同步上下文直接取消请求(不需要 prep + submit)。
io_uring_register_sync_msg 2.11+:同步发 MSG_RING
io_uring_register_region 2.9+:把任意内存区域注册到 ring(供 ZCRX / 大页 / hugetlb 等使用)。
io_uring_register_ifq 2.10+:注册网卡 ZCRX queue(zero-copy recv 入口)。

17.14 recvmsg multishot 输出解析辅助

recvmsg_multishot 把多个消息打包写入同一 buffer,不能直接当 msghdr 看;要用下面这套宏解出每条记录。

函数 语义
io_uring_recvmsg_validate 检查 buffer 是否含完整一帧(边界检查)。
io_uring_recvmsg_name 取出 src addr 部分(struct sockaddr)。
io_uring_recvmsg_payload 取出 payload 起点。
io_uring_recvmsg_payload_length payload 长度(已扣掉 name + cmsg)。
io_uring_recvmsg_cmsg_firsthdr / _nexthdr 遍历控制消息 cmsg。

17.15 io_uring_prep_*:102 个操作准备函数

所有 prep 函数共同语义 :填充 SQE(含 op 码、fd、addr、len、offset、特性标志),不与内核交互;调用后必须 io_uring_submit() 才生效。

下表按业务域分组,签名只保留关键参数(完整签名见 man 3 io_uring_prep_*src/include/liburing.h)。

17.15.1 文件读写(最常用)
函数 对应 IORING_OP 关键参数 说明
io_uring_prep_read READ fd, buf, nbytes, offset 等价 pread
io_uring_prep_write WRITE fd, buf, nbytes, offset 等价 pwrite
io_uring_prep_readv / _writev READV/WRITEV fd, iovec*, nr_vecs, offset scatter/gather I/O。
io_uring_prep_readv2 / _writev2 READV/WRITEV + rw_flags(RWF_HIPRI/DSYNC...) 带 preadv2/pwritev2 标志。
io_uring_prep_read_fixed / _write_fixed READ_FIXED/WRITE_FIXED + buf_index 使用 预注册 buffer,免拷贝/免 get_user_pages。
io_uring_prep_readv_fixed / _writev_fixed READV_FIXED/WRITEV_FIXED iovec + buf_index vectored fixed 版本。
io_uring_prep_read_multishot READ_MULTISHOT fd, nbytes, offset, buf_group 一次提交反复触发,配合 provided buffer。
17.15.2 文件元数据 / 同步 / advise
函数 op 说明
io_uring_prep_fsync FSYNC fd + fsync_flags(0=fsync,IORING_FSYNC_DATASYNC=fdatasync)。
io_uring_prep_sync_file_range SYNC_FILE_RANGE 区间同步。
io_uring_prep_statx STATX 异步 statx
io_uring_prep_fadvise / _fadvise64 FADVISE posix_fadvise 的异步版本(64 表示 64 位 len)。
io_uring_prep_madvise / _madvise64 MADVISE madvise 异步版。
io_uring_prep_fallocate FALLOCATE fd + mode + offset + len。
io_uring_prep_ftruncate FTRUNCATE fd + len,2.4+。
17.15.3 打开 / 关闭 / fd 表
函数 op 说明
io_uring_prep_open OPENAT 语法糖:openat(AT_FDCWD, path, flags, mode)
io_uring_prep_openat OPENAT dfd + path。
io_uring_prep_openat2 OPENAT2 open_how(resolve flags 等)。
io_uring_prep_open_direct / _openat_direct / _openat2_direct 同上 直接 open 到固定 fd 表槽位IORING_FILE_INDEX_ALLOC 表示自动分配。
io_uring_prep_close / _close_direct CLOSE 关 fd / 关 fixed file 槽位。
io_uring_prep_fixed_fd_install FIXED_FD_INSTALL 把 fixed 槽位"具现"成真实 fd(给外部 syscall 用)。
io_uring_prep_files_update FILES_UPDATE 异步更新已注册 fd 表槽位。
17.15.4 目录 / 路径操作
函数 op 说明
io_uring_prep_mkdir / _mkdirat MKDIRAT 异步 mkdir(at)。
io_uring_prep_unlink / _unlinkat UNLINKAT 异步 unlink(at)。
io_uring_prep_rename / _renameat RENAMEAT 异步 rename(at)。
io_uring_prep_link / _linkat LINKAT 硬链接。
io_uring_prep_symlink / _symlinkat SYMLINKAT 软链接。
17.15.5 扩展属性 xattr
函数 op 说明
io_uring_prep_getxattr / _setxattr GETXATTR/SETXATTR 按路径。
io_uring_prep_fgetxattr / _fsetxattr FGETXATTR/FSETXATTR 按 fd。
17.15.6 网络:socket 生命周期
函数 op 说明
io_uring_prep_socket SOCKET domain + type + protocol。
io_uring_prep_socket_direct SOCKET 结果直接放入 fixed file 槽位。
io_uring_prep_socket_direct_alloc SOCKET 自动分配 fixed 槽位。
io_uring_prep_bind BIND sockaddr + addrlen。
io_uring_prep_listen LISTEN backlog。
io_uring_prep_accept ACCEPT 填 addr + addrlen + flags。
io_uring_prep_accept_direct ACCEPT + file_index → 直接进 fixed 表。
io_uring_prep_multishot_accept / _direct ACCEPT 一次提交,每个新连接生成一个 CQE,无需重复 prep。
io_uring_prep_connect CONNECT 异步 connect。
io_uring_prep_shutdown SHUTDOWN shutdown(2)。
17.15.7 网络:发送(send / sendmsg / zero-copy)
函数 op 说明
io_uring_prep_send SEND 等价 send。
io_uring_prep_sendto SEND send + 目的地址(UDP)。
io_uring_prep_send_set_addr --- 在 send SQE 上追加目的地址(更显式)。
io_uring_prep_send_bundle SEND 一次拼多个 provided buffer 进同一帧(减少 syscall)。
io_uring_prep_send_zc SEND_ZC 零拷贝发送:产生 2 个 CQE(first = bytes sent,second = NOTIF buffer 可释放)。
io_uring_prep_send_zc_fixed SEND_ZC zero-copy + fixed buffer。
io_uring_prep_sendmsg SENDMSG 带 msghdr(含 cmsg)。
io_uring_prep_sendmsg_zc SENDMSG_ZC sendmsg + zero-copy。
io_uring_prep_sendmsg_zc_fixed SENDMSG_ZC + fixed iov。
17.15.8 网络:接收(recv / recvmsg / multishot)
函数 op 说明
io_uring_prep_recv RECV 等价 recv。
io_uring_prep_recv_multishot RECV 一次提交、内核每次有数据就触发 CQE,配合 provided buffer ring。
io_uring_prep_recvmsg RECVMSG 带 msghdr。
io_uring_prep_recvmsg_multishot RECVMSG multishot recvmsg,输出格式见 §17.14 辅助宏。
17.15.9 epoll 兼容层
函数 op 说明
io_uring_prep_epoll_ctl EPOLL_CTL 异步 epoll_ctl(ADD/MOD/DEL)。
io_uring_prep_epoll_wait EPOLL_WAIT 异步 epoll_wait(让 io_uring 代为等待 epoll fd)。
17.15.10 poll
函数 op 说明
io_uring_prep_poll_add POLL_ADD 对 fd 关注 poll_mask;一次 CQE 后失效。
io_uring_prep_poll_multishot POLL_ADD 持续触发型 poll。
io_uring_prep_poll_remove POLL_REMOVE 按 user_data 取消。
io_uring_prep_poll_update POLL_REMOVE 原子地修改 poll mask / user_data。
17.15.11 timeout / 链路超时
函数 op 说明
io_uring_prep_timeout TIMEOUT 定时器:到点产生一个 CQE。
io_uring_prep_timeout_remove TIMEOUT_REMOVE 取消未触发的 timeout。
io_uring_prep_timeout_update TIMEOUT_REMOVE 修改 timeout 时间。
io_uring_prep_link_timeout LINK_TIMEOUT 给"前一个用 IOSQE_IO_LINK 链上的 SQE"附加超时。
17.15.12 cancel
函数 op 说明
io_uring_prep_cancel ASYNC_CANCEL 按 user_data(指针)取消请求。
io_uring_prep_cancel64 ASYNC_CANCEL 按 u64 user_data 取消。
io_uring_prep_cancel_fd ASYNC_CANCEL 取消 fd 上的所有未完成请求。
17.15.13 splice / tee / pipe
函数 op 说明
io_uring_prep_splice SPLICE fd_in/off_in → fd_out/off_out + nbytes + flags。
io_uring_prep_tee TEE 管道间复制(不消费)。
io_uring_prep_pipe PIPE 2.10+:异步 pipe2,返回 2 个 fd。
io_uring_prep_pipe_direct PIPE 直接进 fixed file 表。
17.15.14 msg_ring(ring 间通信)
函数 op 说明
io_uring_prep_msg_ring MSG_RING 把一条消息(user_data + len)投递给另一个 ring,对端收到一个 CQE。
io_uring_prep_msg_ring_cqe_flags MSG_RING + cqe_flags 透传。
io_uring_prep_msg_ring_fd MSG_RING 把一个 fd 传递给另一个 ring。
io_uring_prep_msg_ring_fd_alloc MSG_RING 同上 + 对端自动分配 fixed 槽位。
17.15.15 提供 buffer(旧接口,新代码用 buf_ring §17.8)
函数 op 说明
io_uring_prep_provide_buffers PROVIDE_BUFFERS 注册一组 buffer 到 bgid 组。
io_uring_prep_remove_buffers REMOVE_BUFFERS 取消注册。
17.15.16 nop / waitid / futex
函数 op 说明
io_uring_prep_nop NOP 调试/测试占位(仍走完整流程)。
io_uring_prep_nop128 NOP SQE128 模式下的 nop。
io_uring_prep_waitid WAITID 异步 waitid(等待子进程状态)。
io_uring_prep_futex_wait FUTEX_WAIT 异步 futex_wait。
io_uring_prep_futex_wake FUTEX_WAKE futex_wake。
io_uring_prep_futex_waitv FUTEX_WAITV vectored futex 等待。
17.15.17 uring_cmd(设备 passthrough)
函数 op 说明
io_uring_prep_uring_cmd URING_CMD 把任意设备 ioctl 发给驱动(NVMe / ublk 等)。
io_uring_prep_uring_cmd128 URING_CMD SQE128 模式版本(NVMe 大命令)。
io_uring_prep_cmd_sock URING_CMD socket-level ioctl(getsockopt/setsockopt 等)。
io_uring_prep_cmd_getsockname URING_CMD 异步 getsockname。
io_uring_prep_cmd_discard URING_CMD 2.9+:异步 BLKDISCARD(SSD trim)。

17.16 典型使用模式速查(按场景配 API)

场景 推荐 API 组合
最简起步(写第一个 io_uring demo) queue_initget_sqeprep_read/writesubmit_and_wait(1)peek_cqecqe_seenqueue_exit
高吞吐文件 I/O(NVMe / 数据库) queue_init_params(IOPOLL) + register_buffers + prep_read_fixed/write_fixed + register_files + submit_and_get_events + for_each_cqe
echo / proxy 服务(大量短连接 + 小包) `queue_init_params(SINGLE_ISSUER
低延迟网络(HFT / 实时音视频) 上一行 + register_napi + register_ring_fd + register_wait_reg + submit_and_wait_reg
无 syscall 提交(背景任务持续 I/O) SETUP_SQPOLL + register_files + sq_space_left 轮询 + get_sqe;提交 0 syscall(仅 SQ tail store)
取消单个/批量请求 异步:prep_cancel(user_data) / prep_cancel_fd;同步:register_sync_cancel
多 ring 协作(per-core 模型) 每核 `queue_init(SINGLE_ISSUER
等待多种事件源合一 用 io_uring 接管 epoll:prep_epoll_waitprep_poll_multishot;或者 register_eventfd 让 io_uring 也可被 epoll 等待
设备 passthrough(用户态 NVMe / ublk) `queue_init_params(SQE128
安全沙箱化(限制可用 op) SETUP_R_DISABLED + register_restrictions(op/register/sqe_flag 三类白名单)+ enable_rings;或 register_bpf_filter(2.15+)

17.17 标准代码骨架(直接抄走改)

复制代码
// ===== 通用 skeleton:单 ring,N 次异步读 =====
#include <liburing.h>
#include <fcntl.h>

int main(int argc, char **argv) {
    struct io_uring ring;
    struct io_uring_params p = {0};
    p.flags = IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN
            | IORING_SETUP_COOP_TASKRUN;

    if (io_uring_queue_init_params(/*entries=*/256, &ring, &p) < 0)
        return 1;

    // 可选:注册 ring fd 以减少 syscall 参数复制
    io_uring_register_ring_fd(&ring);

    int fd = open(argv[1], O_RDONLY | O_DIRECT);
    char buf[4096] __attribute__((aligned(4096)));

    // 提交一个 read
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, fd, buf, sizeof(buf), /*offset=*/0);
    io_uring_sqe_set_data64(sqe, /*user_data=*/0xC0FFEE);

    io_uring_submit_and_wait(&ring, 1);

    // 收割
    struct io_uring_cqe *cqe;
    unsigned head, count = 0;
    io_uring_for_each_cqe(&ring, head, cqe) {
        // cqe->res = 实际字节数 或 -errno
        // cqe->user_data = 之前 set_data64 的值
        count++;
    }
    io_uring_cq_advance(&ring, count);

    close(fd);
    io_uring_queue_exit(&ring);
    return 0;
}

17.18 错误处理与常用 flag 速查

错误码(cqe->res 或返回值) 含义 / 处置
-EAGAIN 无 CQE 可 peek / SQ 满;正常稍后重试。
-EBUSY CQ 已满,必须先消费;或资源被占用。
-ECANCELED 请求被 prep_cancel 或 link_timeout 取消。
-ENOBUFS provided buffer / buf_ring 已耗尽。补充后重试。
-EOPNOTSUPP 当前内核不支持此 op,务必opcode_supported 探测。
-EINTR 被信号中断;带 sigmask 的 wait 可避免。
-ETIME 带 timeout 的 wait 到期。
SQE flag (sqe->flags) 语义
IOSQE_FIXED_FILE fd 字段是 fixed file 表索引而非真实 fd。
IOSQE_IO_DRAIN 等之前所有请求完成再执行(barrier)。
IOSQE_IO_LINK 与下一个 SQE 形成链;前者失败 → 后者级联取消。
IOSQE_IO_HARDLINK 同上但失败不取消下一个。
IOSQE_ASYNC 强制走 io-wq 异步路径(不走 inline fast path)。
IOSQE_BUFFER_SELECT 从 provided buffer / buf_ring 自动取 buffer。
IOSQE_CQE_SKIP_SUCCESS 成功时不产生 CQE(仅失败/取消时产生),减 CQ 压力。
CQE flag (cqe->flags) 语义
IORING_CQE_F_BUFFER 使用了 provided buffer,bid 在 flags >> IORING_CQE_BUFFER_SHIFT
IORING_CQE_F_MORE 这是 multishot 的一个 CQE,后续还会有;置 0 表示终止。
IORING_CQE_F_SOCK_NONEMPTY recv multishot 中:socket 还有数据可读。
IORING_CQE_F_NOTIF send_zc 的第二个通知 CQE(buffer 可释放)。

三句话总结

资源类register_*,操作类 用 prep_*,剩下少数(提交/收割/版本)独立。

② 应用 99% 的代码只在 §17.4 + §17.5 + §17.15 三节里活动。

③ 写完先用 opcode_supported 探测内核能力,再决定走 multishot/zc/fixed 的高级路径。

18. raw io_uring vs liburing 对照实现(TCP echo server) NEW

这一节用同一个业务 ------ 单连接 TCP echo 服务端 (端口 9999,循环 accept → read → write → close)------ 给出 两份功能完全等价 的代码:

实现 文件 行数 依赖
原始 syscall(不用 liburing) io_uring_compare/echo_server_raw.c ~339 <linux/io_uring.h> + 3 个 syscall
liburing io_uring_compare/echo_server_liburing.c ~116 -luring

两份都可以在本仓库里直接编译运行:

复制代码
cd io_uring_compare
make LIB=local                  # 使用本仓库构建的 liburing.a
./echo_raw &       nc 127.0.0.1 9999     # 终端 A 起服务,终端 B 测试
./echo_liburing &  nc 127.0.0.1 9999

18.1 原始 io_uring 流程(raw 版本逐步拆解)

当你 用 liburing,自己直接面对内核接口时,必须亲手完成下面 13 步。下表的"代码片段"取自 echo_server_raw.c

# 步骤 关键代码(raw)
syscall 创建 ring syscall(__NR_io_uring_setup, entries, &p)
计算三个 mmap 区域大小 sq_size = p.sq_off.array + p.sq_entries*sizeof(unsigned) cq_size = p.cq_off.cqes + p.cq_entries*sizeof(cqe) sqes_size = p.sq_entries*sizeof(sqe)
mmap SQ ring mmap(..., fd, IORING_OFF_SQ_RING)
mmap CQ ring(或复用) mmap(..., fd, IORING_OFF_CQ_RING)
mmap SQE 数组 mmap(..., fd, IORING_OFF_SQES)
把 12 个 offset 翻译成指针 sq.head = sq_ptr + p.sq_off.head; sq.tail = sq_ptr + p.sq_off.tail; ... 一共 7+5 个
取 SQE(读 head/tail + 满检查 + mask) head = atomic_load_acquire(sq.head); if (tail-head >= entries) return NULL; return &sq.sqes[tail & *sq.ring_mask];
填 SQE 各字段 s->opcode = IORING_OP_RECV; s->fd = ...; s->addr = (uintptr_t)buf; s->len = ...; s->user_data = ...;
写 array 索引 + 推 SQ tail (release) sq.array[idx] = idx; atomic_store_release(sq.tail, tail+1);
syscall 触发内核 syscall(__NR_io_uring_enter, fd, to_submit, min, IORING_ENTER_GETEVENTS, NULL, _NSIG/8)
自旋读 CQE(tail-head > 0 取一个) tail = atomic_load_acquire(cq.tail); *out = cq.cqes[head & *cq.ring_mask];
推 CQ head (release) atomic_store_release(cq.head, head+1);
退出:munmap × 3 + close munmap(sq_ptr, sq_size); munmap(sqes_ptr, sqes_size); ...; close(ring_fd);

其中 第 ⑥/⑨/⑫ 步 是最容易写错的:内存屏障语义(acquire 读 head/tail、release 写 head/tail)、sq.array[idx]=idx 这个间接映射、以及 FEAT_SINGLE_MMAP 时 SQ/CQ 是否共享 mmap,都是开发者反复踩坑的点。liburing 把这些「不会变的模板」一次性写对了。

18.2 同样功能用 liburing 怎么写

看一下 echo_server_liburing.c 的核心循环(已删掉错误处理便于对比):

复制代码
struct io_uring ring;
io_uring_queue_init(QUEUE_DEPTH, &ring, 0);          // = 上面 ①②③④⑤⑥

for (;;) {
    /* accept */
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);             // = ⑦
    io_uring_prep_accept(sqe, listen_fd, NULL, NULL, 0);            // = ⑧
    io_uring_submit_and_wait(&ring, 1);                             // = ⑨ + ⑩
    struct io_uring_cqe *cqe; io_uring_wait_cqe(&ring, &cqe);       // = ⑪
    int conn = cqe->res;
    io_uring_cqe_seen(&ring, cqe);                                  // = ⑫

    for (;;) {
        sqe = io_uring_get_sqe(&ring);
        io_uring_prep_recv(sqe, conn, buf, sizeof(buf), 0);
        io_uring_submit_and_wait(&ring, 1);
        io_uring_wait_cqe(&ring, &cqe);
        int n = cqe->res; io_uring_cqe_seen(&ring, cqe);
        if (n <= 0) break;

        sqe = io_uring_get_sqe(&ring);
        io_uring_prep_send(sqe, conn, buf, n, 0);
        io_uring_submit_and_wait(&ring, 1);
        io_uring_wait_cqe(&ring, &cqe);
        io_uring_cqe_seen(&ring, cqe);
    }
    close(conn);
}
io_uring_queue_exit(&ring);                                          // = ⑬

对比原始版的 339 行(含手工 ring 句柄结构体 + 6 个 helper),liburing 版只有 116 行,且没有一处涉及内存屏障 / mmap / SQE 字段。

18.3 一图看懂 ------ liburing 接口究竟「封装」了 io_uring 的哪些操作

io_uring 原生步骤(raw 版本你必须自己写) liburing 提供的封装
io_uring_setup syscall ② 计算 SQ/CQ/SQE 三块 mmap 大小 ③ mmap(IORING_OFF_SQ_RING)mmap(IORING_OFF_CQ_RING)(或复用) ⑤ mmap(IORING_OFF_SQES) ⑥ 把 sq_off / cq_off 12 个 offset 翻译成可用指针 io_uring_queue_init(entries, &ring, flags) (或带参版 io_uring_queue_init_params
⑦ 读 head/tail(acquire)、满检查、按 mask 算 SQE 下标 io_uring_get_sqe(&ring)
⑧ 手动写 SQE 的 opcode / fd / addr / len / off / user_data / accept_flags / fsync_flags / rw_flags / msg_flags / buf_index ... 102 个 io_uring_prep_* (accept / recv / send / read / write / openat / fsync / connect / poll / cancel / ..., 每个 IORING_OP 都有一个对应 helper,没有遗漏
sq.array[idx] = idx + SQ tail 的 release 写 io_uring_submit() 内部完成(__io_uring_flush_sq
io_uring_enter syscall(带 GETEVENTS/SQ_WAKEUP 等 flag 与 sigset 大小) io_uring_submit() / io_uring_submit_and_wait(n) / io_uring_submit_and_wait_timeout()
⑪ 自旋读 CQ head/tail(acquire),按 mask 算 cqe 下标, CQ 空时再 io_uring_enter(0,1,GETEVENTS) io_uring_wait_cqe() / _peek_cqe() / _wait_cqe_timeout() /_peek_batch_cqe() / 宏 io_uring_for_each_cqe()
⑫ CQ head 的 release 写 io_uring_cqe_seen()io_uring_cq_advance(n)
munmap × 3 + close(ring_fd) io_uring_queue_exit(&ring)
--- SQPOLL 模式下:判断是否需要 wakeup --- SINGLE_MMAP 特性识别 --- overflow 标志的检查 --- register_ring_fd 后 syscall 是否带 fd... 全部内置在 __io_uring_submit / __io_uring_get_cqe 的 fast path, 应用层无感知

18.4 一句话总结:liburing 的本质

liburing ≡ 以下四类东西的总和:

  1. 三个 syscall 的薄 wrapperio_uring_setup / enter / register)------ §17.3。
  2. 固定不变的模板代码:mmap 三块共享内存、计算所有指针偏移、release/acquire 屏障 ------ §17.2 + §17.4 + §17.5。
  3. 每一个 IORING_OP_* 的 SQE 填充模板 :102 个 io_uring_prep_*,让你不必直接动 64 字节裸结构体 ------ §17.15。
  4. 常见组合的便利函数submit_and_waitfor_each_cqecqe_seensetup_buf_ringregister_buffers_sparse ... ------ 散落在 §17.6--§17.14。

它不引入任何运行时行为、状态机或线程模型 ------ 所有调度、I/O 时序、零拷贝路径都由内核 io_uring 决定。liburing 的作用,只是让你「不必每次都把那段一字不差的内核 ABI 模板代码重写一遍」。

什么时候可以不用 liburing?

当你在写新语言的绑定(Rust / Go / Zig...)、需要把 ring 嵌进自定义 runtime、或单纯想理解内核行为 ------ 那时直接面对 syscall + mmap 是合理的。liburing 也专门提供了 liburing-ffi 子库,把所有 IOURINGINLINE 也外化成 ABI 符号,方便跨语言绑定。