Linux io_uring 用户态库 · 深度解析

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

目录

  1. io_uring 与 liburing 是什么
  2. 为什么会出现 io_uring(历史与对比)
  3. 核心架构:双 ring 共享内存模型
  4. 三个系统调用
  5. 关键数据结构(SQE / CQE / 句柄)
  6. 编程模型:六步范式
  7. 支持的 60+ 种操作(IORING_OP_*)
  8. 运行模式与高级特性(SQPOLL / IOPOLL / DEFER_TASKRUN ...)
  9. 资源预注册(Buffers / Files / Buf Ring)
  10. 高级特性:Linked / Multishot / Cancel / Zero-Copy
  11. 仓库目录与代码结构
  12. 实现框架:模块分层与依赖图 NEW
  13. 实现细节:fast path 与内存屏障
  14. 完整函数调用图(六大流程) NEW
  15. 源码逐文件深度分析(带行号锚点) NEW
  16. DeepWiki 章节对照与关键摘录 NEW
  17. 性能特性与对比 epoll
  18. 业界采用情况
  19. 坑、限制与最佳实践
  20. 总览速查表
  21. liburing 公共 API 大全(按功能分组) NEW
  22. 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 / poll1980s~每次都要扫描整个 fd 集合,O(n)
epoll2002 (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_uring2019 (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 RingIORING_OFF_SQ_RINGapp 写 tail,kernel 读 + 写 headhead/tail/flags/dropped + 一个 index → SQE 的数组
SQE ArrayIORING_OFF_SQESapp 写实际的 64 字节(或 128 字节)SQE 内容
CQ RingIORING_OFF_CQ_RINGkernel 写 tail,app 读 + 写 headhead/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)425io_uring_queue_init()创建 ring,返回 fd;填 io_uring_params 给应用做 mmap
io_uring_enter(fd, to_submit, min_complete, flags, sig)426io_uring_submit() / wait_cqe()通知内核处理 SQ 中的请求,并/或等待 CQ 完成
io_uring_register(fd, op, arg, nr)427io_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:249 的 enum io_uring_op,截至当前版本约 65+ 种。按用途分类:

类别典型 opcode典型 prep helper
文件 I/OREAD / WRITE / READV / WRITEV / READ_FIXED / WRITE_FIXED / READ_MULTISHOT / FSYNC / FALLOCATE / FADVISE / SYNC_FILE_RANGEio_uring_prep_read()prep_writev_fixed() ...
文件元数据OPENAT / OPENAT2 / CLOSE / STATX / RENAMEAT / UNLINKAT / MKDIRAT / SYMLINKAT / LINKAT / FTRUNCATE / [F]GETXATTR / [F]SETXATTRio_uring_prep_openat() ...
网络 I/OACCEPT / CONNECT / BIND / LISTEN / SEND / RECV / SENDMSG / RECVMSG / SOCKET / SHUTDOWNio_uring_prep_accept()prep_recv() ...
零拷贝SEND_ZC / SENDMSG_ZC / RECV_ZC / SPLICE / TEEio_uring_prep_send_zc()prep_splice()
轮询 / 定时POLL_ADD / POLL_REMOVE / TIMEOUT / TIMEOUT_REMOVE / LINK_TIMEOUT / EPOLL_CTL / EPOLL_WAITio_uring_prep_poll_add()prep_timeout()
取消 / 控制ASYNC_CANCEL / NOP / FILES_UPDATE / PROVIDE_BUFFERS / REMOVE_BUFFERS / MSG_RING / FIXED_FD_INSTALLio_uring_prep_cancel()prep_msg_ring()
同步原语WAITID / FUTEX_WAIT / FUTEX_WAKE / FUTEX_WAITVio_uring_prep_futex_*()
设备/passthroughURING_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 指定的 CPUper-core 设计
IORING_SETUP_IOPOLL用 NVMe 轮询模式完成;CQE 不再走中断NVMe 直通、低延迟存储
IORING_SETUP_COOP_TASKRUN"协作式"任务回调;不强制 IPI 中断目标线程,节省调度开销5.18+;通用提速
IORING_SETUP_DEFER_TASKRUN更进一步:仅在调用 io_uring_enter 时才执行 task work6.0+;和 SINGLE_ISSUER 配合,性能最优,proxy 示例的默认配置
IORING_SETUP_SINGLE_ISSUER承诺只有一个线程提交 SQE5.18+;与 DEFER_TASKRUN 搭配
IORING_SETUP_SQE128SQE 大小变 128B(含 80B cmd 字段)NVMe passthrough、自定义命令
IORING_SETUP_CQE32CQE 大小变 32B需要更大返回值的 op
IORING_SETUP_NO_SQARRAY去掉 SQ index 数组的间接寻址(默认从 6.6 开启)提速 ~5%
IORING_SETUP_NO_MMAP由应用自己分配大页内存传入HugeTLB 大页加速、多 ring 共享一页
IORING_SETUP_REGISTERED_FD_ONLY只暴露 registered fd,不分配普通 fdfd 表压力大的服务
IORING_SETUP_TASKRUN_FLAG有 task work 时设置 SQ flag,应用主动检查需要精确控制何时进内核
IORING_SETUP_HYBRID_IOPOLLiopoll 但允许少量休眠(节能)新内核新增
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 | sqe->fd = index
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.h2149主对外头文件,包含所有 inline io_uring_prep_*()
src/include/liburing/io_uring.h1124kernel UAPI(struct io_uring_sqe / cqeIORING_OP_* 等定义)
src/include/liburing/barrier.h跨架构内存屏障 (io_uring_smp_load_acquire 等)
src/setup.c709ring 创建、mmap/munmap、参数推算
src/queue.c497SQ flush、__io_uring_submitget_cqe 主循环
src/register.c529所有 register/unregister API(buffers / files / eventfd / buf_ring / napi / clock)
src/sanitize.c177ASan 集成,校验用户传入的 buffer/iovec
src/syscall.c / syscall.h~30三个 syscall 的薄封装
src/ffi.c15把所有内联函数实例化成符号供 FFI 使用
src/nolibc.c60无 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.hqueue.csetup.cregister.csyscall.hlib.h / barrier.hkernel UAPI
用户应用✅ 唯一入口(透明)
setup.c✅ 反向引用类型self✅ __sys_*✅ barrier 不用✅ 通过 syscall.h
queue.c✅ 类型 + inlineself✅ __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.0io_uring_queue_init / queue_exit / submit / wait_cqe / register_buffers / register_files / get_probe …setup.c, queue.c, register.c
LIBURING_2.1io_uring_register_iowq_max_workers / register_ring_fd / register_buf_ring …register.c
LIBURING_2.2io_uring_get_sqe(重新暴露符号,见 8be8af4afcb4)/ submit_and_wait_timeoutqueue.c (LIBURING_INTERNAL)
LIBURING_2.3 ~ LIBURING_2.6register_napi / register_clock / setup_buf_ring / free_buf_ring / register_restrictions / register_sync_msg …setup.c, register.c
LIBURING_2.7 ~ LIBURING_2.9register_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_sqe → io_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_cqe → io_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_WAKEUPtail 写入与 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–40sq_ring_needs_enter判断是否真的需要进内核(非 SQPOLL 一律 true;SQPOLL 看 NEED_WAKEUP),fast path 的核心分支
42–51cq_ring_needs_flush / cq_ring_needs_enter检查 CQ 是否溢出 / 有 task work 待跑(IORING_SQ_CQ_OVERFLOW / IORING_SQ_TASKRUN)。
53–60struct 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–152io_uring_get_events仅做 GETEVENTS 的 enter,不提交不等待,用于"刷一下 task work"。
154–197io_uring_peek_batch_cqe(_)批量取 CQE 的非阻塞接口;如果 CQ 满则触发 flush。
203–238__io_uring_flush_sq把本地 sqe_tail 推到共享 *ktail,包含 SQ_REWIND 特殊处理与 SQPOLL release barrier
244–268io_uring_wait_cqes_new使用 IORING_ENTER_EXT_ARG 一次 syscall 完成「带 timeout 的等待」。
287–308__io_uring_submit_timeout老内核(无 EXT_ARG)的兼容路径:消耗 1 个 SQE 做 timeout。
310–326io_uring_wait_cqes对外的 wait API;分新内核/老内核分支。
338–358io_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–478io_uring_submit / submit_and_wait对外 API;分别 wait_nr=0 / wait_nr=N。
480–483io_uring_submit_and_get_events提交后立刻取一波 events(不阻塞)。
486–490io_uring_get_sqe (LIBURING_INTERNAL)给"老程序链接 .so"留的兼容符号,转调头文件中的 inline 版本。
492–497__io_uring_sqring_waitSQPOLL 时等待 SQ 有空闲槽位。

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

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

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

函数对应 IORING_REGISTER_* opcode
13–28do_register统一通道;处理 USE_REGISTERED_RING 改写
30–84register_buffers / _tags / _sparse / _update_tag / unregister_buffersBUFFERS / BUFFERS2 / BUFFERS_UPDATE
86–230register_files / _tags / _sparse / _update / unregister_filesFILES / FILES2 / FILES_UPDATE / FILES_UPDATE2
~230–340register_eventfd / unregister / asyncEVENTFD / EVENTFD_ASYNC
~340–420register_personality / restrictions / enable_rings / iowq_aff / iowq_max_workersPERSONALITY / RESTRICTIONS / ENABLE_RINGS / IOWQ_AFF / IOWQ_MAX_WORKERS
~420–500register_buf_ring / unregister_buf_ring / register_napi / clock / region / zcrx_ifqPBUF_RING / NAPI / CLOCK / REGION / ZCRX_IFQ
~500–529register_sync_msg / register_ring_fd / unregister_ring_fdSYNC_MSG(5.18+ 同步取消 / 跨 ring)与 RING_FDS

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

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

头文件分 5 个区块:

行号区段内容
1–80License / 包含 / 类型 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–17001500+ 行的 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:486 在 LIBURING_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/writeio_uring
异步类型"就绪通知",I/O 本身仍同步真正异步:发起后立刻返回
支持的 fdsocket / pipe / eventfd(不支持普通 file)所有:file / socket / pipe / signal / device / dirent / ...
每次 op 的 syscall 数1 (epoll_wait) + 1 (read/write) ≥ 20~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 天然契合已生产
RedpandaC++ 重写 Kafka,磁盘 + 网络全 io_uring已生产
PostgreSQL 17/18新增 io_method=io_uring(异步 I/O backend)已合入主干
CephBlueStore 的 io_uring backend稳定可选
QEMUvirtio-blk / virtio-net 的后端已生产
Tokio (Rust)Tokio 提供 tokio-uring crate;某些子项目(如 Cloudflare Pingora 的实验分支)已用稳定
libuv5.1+ 内核自动用 io_uring 做文件 I/O;网络仍 epoll已生产
Node.js通过 libuv 间接受益已生产
fioJens Axboe 自己的工具,io_uring engine 是其首选已生产
RocksDBMulti-Get + io_uring 实验实验
Cloudflare边缘服务的 file I/O已采用
Metafolly 内部 IoUringBackend已生产

15. 坑、限制与最佳实践

15.1 内核版本依赖

kernel关键里程碑
5.1io_uring 引入,仅基础 read/write
5.5–5.7SQPOLL 改进、buffered file IO
5.10FEAT_EXT_ARG,wait timeout 不再需要发额外 SQE
5.13稀疏注册(register_files_sparse
5.18COOP_TASKRUN、SINGLE_ISSUER、register_ring_fd
5.19provided buffer ring
6.0DEFER_TASKRUN(性能跃升)
6.4–6.6NO_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 维护者)
LicenseMIT(库本体),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)
三个 syscallio_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_versionint (void)动态库主版本号(例如 2)。
io_uring_minor_versionint (void)动态库次版本号(例如 9)。
io_uring_check_versionbool (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_ringstruct io_uring_probe *(void) / (struct io_uring *)申请并填充 probe(当前内核支持的 op 列表)。
io_uring_free_probevoid (struct io_uring_probe *)释放 probe。
io_uring_opcode_supportedint (const struct io_uring_probe *p, int op)判断某个 IORING_OP_* 在当前内核是否可用。
io_uring_register_probeint (ring, probe, nr)同上但直接走 ring。

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

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

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

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

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

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

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

17.5 CQE 收割(完成侧)

函数 / 宏签名语义
io_uring_wait_cqeint (ring, struct io_uring_cqe **)阻塞等待 1 个 CQE。
io_uring_wait_cqe_nrint (ring, **cqe, unsigned nr)等待 ≥ nr 个 CQE。
io_uring_wait_cqe_timeoutint (ring, **cqe, ts)带超时(基于 FEAT_EXT_ARG,无需消耗 SQE)。
io_uring_wait_cqesint (ring, **cqe, wait_nr, ts, sigmask)带 sigmask + 超时的批量等待。
io_uring_wait_cqes_min_timeoutint (ring, **cqe, wait_nr, ts, min_ts_usec, sigmask)2.8+:min/max 双超时。
io_uring_peek_cqeint (ring, **cqe)非阻塞 peek 一个 CQE(无则 -EAGAIN)。
io_uring_peek_batch_cqeunsigned (ring, **cqes, count)批量 peek,返回实际数量。
io_uring_for_each_cqe(ring, head, cqe)遍历 所有已 ready 的 CQE(最常用 fast path 收割)。
io_uring_cqe_seenvoid (ring, cqe)消费 1 个 CQE:等价 cq_advance(1)
io_uring_cq_advancevoid (ring, nr)批量消费 nr 个 CQE(与 for_each_cqe 配合)。
io_uring_cq_readyunsigned (const ring)CQ 中已就绪未消费数。
io_uring_cq_has_overflowbool (const ring)是否发生过 CQ overflow(务必 检查!)。
io_uring_get_eventsint (ring)仅触发内核 flush overflow 队列、不提交 SQE。
io_uring_cqe_get_data / _get_data64void */__u64 (const cqe)从 CQE 取 user_data。
io_uring_cqe_nrunsigned (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_clock2.8+:切换 ring 内部 timer 用的时钟源(如 CLOCK_MONOTONIC → CLOCK_BOOTTIME)。
io_uring_set_iowait2.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 / _offset2.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_head2.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_reg2.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 / _task2.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_msg2.11+:同步发 MSG_RING
io_uring_register_region2.9+:把任意内存区域注册到 ring(供 ZCRX / 大页 / hugetlb 等使用)。
io_uring_register_ifq2.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_lengthpayload 长度(已扣掉 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_readREADfd, buf, nbytes, offset等价 pread
io_uring_prep_writeWRITEfd, buf, nbytes, offset等价 pwrite
io_uring_prep_readv / _writevREADV/WRITEVfd, iovec*, nr_vecs, offsetscatter/gather I/O。
io_uring_prep_readv2 / _writev2READV/WRITEVrw_flags(RWF_HIPRI/DSYNC…)带 preadv2/pwritev2 标志。
io_uring_prep_read_fixed / _write_fixedREAD_FIXED/WRITE_FIXEDbuf_index使用 预注册 buffer,免拷贝/免 get_user_pages。
io_uring_prep_readv_fixed / _writev_fixedREADV_FIXED/WRITEV_FIXEDiovec + buf_indexvectored fixed 版本。
io_uring_prep_read_multishotREAD_MULTISHOTfd, nbytes, offset, buf_group一次提交反复触发,配合 provided buffer。
17.15.2 文件元数据 / 同步 / advise
函数op说明
io_uring_prep_fsyncFSYNCfd + fsync_flags(0=fsync,IORING_FSYNC_DATASYNC=fdatasync)。
io_uring_prep_sync_file_rangeSYNC_FILE_RANGE区间同步。
io_uring_prep_statxSTATX异步 statx
io_uring_prep_fadvise / _fadvise64FADVISEposix_fadvise 的异步版本(64 表示 64 位 len)。
io_uring_prep_madvise / _madvise64MADVISEmadvise 异步版。
io_uring_prep_fallocateFALLOCATEfd + mode + offset + len。
io_uring_prep_ftruncateFTRUNCATEfd + len,2.4+。
17.15.3 打开 / 关闭 / fd 表
函数op说明
io_uring_prep_openOPENAT语法糖:openat(AT_FDCWD, path, flags, mode)
io_uring_prep_openatOPENATdfd + path。
io_uring_prep_openat2OPENAT2带 open_how(resolve flags 等)。
io_uring_prep_open_direct / _openat_direct / _openat2_direct同上直接 open 到固定 fd 表槽位IORING_FILE_INDEX_ALLOC 表示自动分配。
io_uring_prep_close / _close_directCLOSE关 fd / 关 fixed file 槽位。
io_uring_prep_fixed_fd_installFIXED_FD_INSTALL把 fixed 槽位"具现"成真实 fd(给外部 syscall 用)。
io_uring_prep_files_updateFILES_UPDATE异步更新已注册 fd 表槽位。
17.15.4 目录 / 路径操作
函数op说明
io_uring_prep_mkdir / _mkdiratMKDIRAT异步 mkdir(at)。
io_uring_prep_unlink / _unlinkatUNLINKAT异步 unlink(at)。
io_uring_prep_rename / _renameatRENAMEAT异步 rename(at)。
io_uring_prep_link / _linkatLINKAT硬链接。
io_uring_prep_symlink / _symlinkatSYMLINKAT软链接。
17.15.5 扩展属性 xattr
函数op说明
io_uring_prep_getxattr / _setxattrGETXATTR/SETXATTR按路径。
io_uring_prep_fgetxattr / _fsetxattrFGETXATTR/FSETXATTR按 fd。
17.15.6 网络:socket 生命周期
函数op说明
io_uring_prep_socketSOCKETdomain + type + protocol。
io_uring_prep_socket_directSOCKET结果直接放入 fixed file 槽位。
io_uring_prep_socket_direct_allocSOCKET自动分配 fixed 槽位。
io_uring_prep_bindBINDsockaddr + addrlen。
io_uring_prep_listenLISTENbacklog。
io_uring_prep_acceptACCEPT填 addr + addrlen + flags。
io_uring_prep_accept_directACCEPT+ file_index → 直接进 fixed 表。
io_uring_prep_multishot_accept / _directACCEPT一次提交,每个新连接生成一个 CQE,无需重复 prep。
io_uring_prep_connectCONNECT异步 connect。
io_uring_prep_shutdownSHUTDOWNshutdown(2)。
17.15.7 网络:发送(send / sendmsg / zero-copy)
函数op说明
io_uring_prep_sendSEND等价 send。
io_uring_prep_sendtoSENDsend + 目的地址(UDP)。
io_uring_prep_send_set_addr在 send SQE 上追加目的地址(更显式)。
io_uring_prep_send_bundleSEND一次拼多个 provided buffer 进同一帧(减少 syscall)。
io_uring_prep_send_zcSEND_ZC零拷贝发送:产生 2 个 CQE(first = bytes sent,second = NOTIF buffer 可释放)。
io_uring_prep_send_zc_fixedSEND_ZCzero-copy + fixed buffer。
io_uring_prep_sendmsgSENDMSG带 msghdr(含 cmsg)。
io_uring_prep_sendmsg_zcSENDMSG_ZCsendmsg + zero-copy。
io_uring_prep_sendmsg_zc_fixedSENDMSG_ZC+ fixed iov。
17.15.8 网络:接收(recv / recvmsg / multishot)
函数op说明
io_uring_prep_recvRECV等价 recv。
io_uring_prep_recv_multishotRECV一次提交、内核每次有数据就触发 CQE,配合 provided buffer ring。
io_uring_prep_recvmsgRECVMSG带 msghdr。
io_uring_prep_recvmsg_multishotRECVMSGmultishot recvmsg,输出格式见 §17.14 辅助宏。
17.15.9 epoll 兼容层
函数op说明
io_uring_prep_epoll_ctlEPOLL_CTL异步 epoll_ctl(ADD/MOD/DEL)。
io_uring_prep_epoll_waitEPOLL_WAIT异步 epoll_wait(让 io_uring 代为等待 epoll fd)。
17.15.10 poll
函数op说明
io_uring_prep_poll_addPOLL_ADD对 fd 关注 poll_mask;一次 CQE 后失效。
io_uring_prep_poll_multishotPOLL_ADD持续触发型 poll。
io_uring_prep_poll_removePOLL_REMOVE按 user_data 取消。
io_uring_prep_poll_updatePOLL_REMOVE原子地修改 poll mask / user_data。
17.15.11 timeout / 链路超时
函数op说明
io_uring_prep_timeoutTIMEOUT定时器:到点产生一个 CQE。
io_uring_prep_timeout_removeTIMEOUT_REMOVE取消未触发的 timeout。
io_uring_prep_timeout_updateTIMEOUT_REMOVE修改 timeout 时间。
io_uring_prep_link_timeoutLINK_TIMEOUT给"前一个用 IOSQE_IO_LINK 链上的 SQE"附加超时。
17.15.12 cancel
函数op说明
io_uring_prep_cancelASYNC_CANCEL按 user_data(指针)取消请求。
io_uring_prep_cancel64ASYNC_CANCEL按 u64 user_data 取消。
io_uring_prep_cancel_fdASYNC_CANCEL取消 fd 上的所有未完成请求。
17.15.13 splice / tee / pipe
函数op说明
io_uring_prep_spliceSPLICEfd_in/off_in → fd_out/off_out + nbytes + flags。
io_uring_prep_teeTEE管道间复制(不消费)。
io_uring_prep_pipePIPE2.10+:异步 pipe2,返回 2 个 fd。
io_uring_prep_pipe_directPIPE直接进 fixed file 表。
17.15.14 msg_ring(ring 间通信)
函数op说明
io_uring_prep_msg_ringMSG_RING把一条消息(user_data + len)投递给另一个 ring,对端收到一个 CQE。
io_uring_prep_msg_ring_cqe_flagsMSG_RING+ cqe_flags 透传。
io_uring_prep_msg_ring_fdMSG_RING把一个 fd 传递给另一个 ring。
io_uring_prep_msg_ring_fd_allocMSG_RING同上 + 对端自动分配 fixed 槽位。
17.15.15 提供 buffer(旧接口,新代码用 buf_ring §17.8)
函数op说明
io_uring_prep_provide_buffersPROVIDE_BUFFERS注册一组 buffer 到 bgid 组。
io_uring_prep_remove_buffersREMOVE_BUFFERS取消注册。
17.15.16 nop / waitid / futex
函数op说明
io_uring_prep_nopNOP调试/测试占位(仍走完整流程)。
io_uring_prep_nop128NOPSQE128 模式下的 nop。
io_uring_prep_waitidWAITID异步 waitid(等待子进程状态)。
io_uring_prep_futex_waitFUTEX_WAIT异步 futex_wait。
io_uring_prep_futex_wakeFUTEX_WAKEfutex_wake。
io_uring_prep_futex_waitvFUTEX_WAITVvectored futex 等待。
17.15.17 uring_cmd(设备 passthrough)
函数op说明
io_uring_prep_uring_cmdURING_CMD把任意设备 ioctl 发给驱动(NVMe / ublk 等)。
io_uring_prep_uring_cmd128URING_CMDSQE128 模式版本(NVMe 大命令)。
io_uring_prep_cmd_sockURING_CMDsocket-level ioctl(getsockopt/setsockopt 等)。
io_uring_prep_cmd_getsocknameURING_CMD异步 getsockname。
io_uring_prep_cmd_discardURING_CMD2.9+:异步 BLKDISCARD(SSD trim)。

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

场景推荐 API 组合
最简起步(写第一个 io_uring demo)queue_init → get_sqe → prep_read/write → submit_and_wait(1) → peek_cqe → cqe_seen → queue_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|DEFER_TASKRUN|COOP_TASKRUN) + setup_buf_ring + prep_multishot_accept_direct + prep_recv_multishot + prep_send_zc + buf_ring_cq_advance
低延迟网络(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|DEFER_TASKRUN) + prep_msg_ring / msg_ring_fd 做跨核投递;clone_buffers 共享注册 buffer
等待多种事件源合一用 io_uring 接管 epoll:prep_epoll_wait 或 prep_poll_multishot;或者 register_eventfd 让 io_uring 也可被 epoll 等待
设备 passthrough(用户态 NVMe / ublk)queue_init_params(SQE128|CQE32) + get_sqe128 + prep_uring_cmd128 + cqe_iter_*
安全沙箱化(限制可用 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 满;正常稍后重试。
-EBUSYCQ 已满,必须先消费;或资源被占用。
-ECANCELED请求被 prep_cancel 或 link_timeout 取消。
-ENOBUFSprovided buffer / buf_ring 已耗尽。补充后重试。
-EOPNOTSUPP当前内核不支持此 op,务必先 opcode_supported 探测。
-EINTR被信号中断;带 sigmask 的 wait 可避免。
-ETIME带 timeout 的 wait 到期。
SQE flag (sqe->flags)语义
IOSQE_FIXED_FILEfd 字段是 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_NONEMPTYrecv multishot 中:socket 还有数据可读。
IORING_CQE_F_NOTIFsend_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
liburingio_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 创建 ringsyscall(__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 ringmmap(..., 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 + closemunmap(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 符号,方便跨语言绑定。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐