Table of contents
Open Table of contents
- TL;DR
- 1. Why:为什么 Linux 异步 I/O 在 io_uring 之前是一片废墟
- 2. 架构:三段 mmap + 双环协议
- 3. 三个核心系统调用
- 4. liburing:生产代码应该用的封装
- 5. C++17 例:multishot accept 的 echo server
- 6. 错误码与边界行为(必须理解)
- 7. 内核版本时间线(决定你能用哪些特性)
- 8. Pitfalls(至少 10 个,每个都踩过人)
- 9. 对比:io_uring vs epoll vs libaio vs 线程池
- 10. 安全:io_uring 为何被多家大厂禁用
- 11. 生产 Checklist
- 参考(authoritative sources)
TL;DR
io_uring 的本质不是”更快的 epoll”,而是用户态和内核态共享两个 mmap 环(SQ 提交环 / CQ 完成环)+ 完成模型(submit then wait for completion),统一覆盖文件 / socket / timer / futex 的异步。相较 epoll 的 readiness 模型,它在存储 I/O(libaio 无法 buffered)和syscall 密集场景(SQPOLL 下热路径零 syscall)有质变;对已优化过的 epoll 网络栈提升只有 10–30%。工程上的真实门槛是内核版本下限 ≥ 5.15、容器 seccomp 默认屏蔽、安全 CVE 历史重、多云厂商禁用,以及一条必须铭刻的规则:CQE 的完成顺序和 SQE 的提交顺序无关,必须靠 user_data 关联。
一句警示:Docker 25.0+ 默认 seccomp profile 屏蔽
io_uring_setup/enter/register,容器里跑会EPERM。设计基于 io_uring 的服务前先确认部署环境。
1. Why:为什么 Linux 异步 I/O 在 io_uring 之前是一片废墟
Linux 在 io_uring 之前有四种”异步” I/O 方案,每一种都在某个维度上破产:
| 方案 | 模型 | 关键缺陷 |
|---|---|---|
| epoll + 非阻塞 rw | readiness | 只告诉你”可读/可写”,真正搬数据还得 read/write 系统调用;regular file 永远 ready,磁盘 I/O 根本不 async |
POSIX AIO (aio_read) | completion(伪) | glibc 用户态线程池模拟,每个 op 一次 pthread 切换 + 回调 |
libaio (io_submit) | completion | 仅 O_DIRECT 真异步;buffered I/O 静默退化为阻塞。每 op 两次 syscall + iocb 拷贝 |
线程池(read + N 线程) | completion | M:N 切换、cache miss、锁竞争,高 IOPS 下永远跑不过事件驱动 |
epoll 解决的只是网络 readiness 通知。libaio 的 io_setup(2) 接口在内核里被维护了十年都无法扩展到网络 fd,也无法处理 buffered I/O——Jens Axboe(块层 maintainer)在 2019 年 LWN 上的原话是:“it is clear libaio will never reach its goals.”
io_uring 的突破在两个维度上同时成立:
- 完成模型(completion-based):内核做完 I/O 后把结果直接塞进 CQ,用户态不用再发起
read——一次 SQE 完成全部工作 - 零拷贝元数据通道:SQ/CQ 是 mmap 共享内存,用户写 SQE、内核写 CQE,都不需要把 iocb 从用户态拷进内核
再叠加 SQPOLL 模式(内核线程轮询 SQ,热路径 0 syscall),就是今天 io_uring 的性能故事。
2. 架构:三段 mmap + 双环协议
2.1 内存布局
io_uring_setup(2) 返回一个 fd,用户 mmap 该 fd 拿到三段共享区(偏移来自 <linux/io_uring.h>):
| mmap 偏移常量 | 内容 | 方向 |
|---|---|---|
IORING_OFF_SQ_RING | SQ 的 head/tail 指针 + array[] 间接索引 | 用户写 tail,内核读 head |
IORING_OFF_CQ_RING | CQ 的 head/tail 指针 + CQE 数组 | 内核写 tail,用户读 head |
IORING_OFF_SQES | SQE 对象池(每项 64B,SETUP_SQE128 后 128B) | 用户填 |
内核 5.4+ 的 IORING_FEAT_SINGLE_MMAP 允许 SQ/CQ ring 合并为一次 mmap;6.5+ 的 IORING_SETUP_NO_MMAP 允许用户自己传入 huge pages。
2.2 数据结构
// <linux/io_uring.h> 简化版
struct io_uring_sqe { // 64 B
__u8 opcode; // IORING_OP_READ / WRITE / RECV / ACCEPT ...
__u8 flags; // IOSQE_FIXED_FILE / IO_LINK / CQE_SKIP_SUCCESS ...
__u16 ioprio;
__s32 fd; // 或 fixed file index
__u64 off;
__u64 addr; // 用户 buffer
__u32 len;
__u32 op_flags;
__u64 user_data; // ★ 完成时原样回显到 CQE,是关联 SQE→CQE 的唯一钩子
// ...
};
struct io_uring_cqe { // 16 B
__u64 user_data; // 原样回显
__s32 res; // 成功返回值 或 -errno
__u32 flags; // IORING_CQE_F_MORE / F_BUFFER ...
};
user_data 是连接 SQE 与 CQE 的唯一手段。因为多个 I/O 并发提交后,完成顺序和提交顺序无关,必须靠 user_data 识别是谁。
2.3 提交协议(内核如何知道有新 SQE)
用户侧流程(liburing 已封装,这里讲原理):
1. tail = sq->tail // 非原子读,自己写的
2. idx = tail & sq->ring_mask
3. 填充 sqes[idx] // opcode/fd/addr/len/user_data
4. sq->array[idx] = idx // 间接索引(6.6+ NO_SQARRAY 可省)
5. smp_store_release(sq->tail, tail + 1) // 发布屏障
6. io_uring_enter(...) // 通知内核(SQPOLL 模式下省略)
原子 release/acquire 配对:用户写完 SQE 后 release,内核 acquire 读到 tail 更新,才能安全读取 SQE 内容——绕开任何锁。
2.4 核心加速机制
| 机制 | 内核版本 | 作用 |
|---|---|---|
SQPOLL (IORING_SETUP_SQPOLL) | 5.1 | 内核起专线程轮询 SQ,热路径 0 syscall;代价是 1 核常驻 CPU |
IOPOLL (IORING_SETUP_IOPOLL) | 5.1 | 块设备忙轮询,需 O_DIRECT + 驱动支持 |
Fixed Files (REGISTER_FILES) | 5.1 | fd 数组预注册,SQE 的 fd 变 index,省掉每次 fget/fput |
Fixed Buffers (REGISTER_BUFFERS) | 5.1 | 预 pin 用户内存页,READ_FIXED/WRITE_FIXED 免去每次 get_user_pages |
Linked SQE (IOSQE_IO_LINK) | 5.3 | 相邻 SQE 串依赖链,链中任意一环失败则后续 -ECANCELED |
| Multishot (poll/accept/recv) | 5.13–5.20 | 一个 SQE 长期产生多个 CQE,用 IORING_CQE_F_MORE 标记”还会有” |
| SEND_ZC 零拷贝 | 5.19 | 避免用户页到内核 skb 的拷贝,大包收益明显 |
| SINGLE_ISSUER + DEFER_TASKRUN | 6.0 / 6.1 | 单线程提交约束 + 延迟 task work,消除 IPI |
3. 三个核心系统调用
| syscall | 作用 |
|---|---|
io_uring_setup(entries, params) | 创建 ring,返回 fd;params 回填偏移量和 features |
io_uring_enter(fd, to_submit, min_complete, flags, sig) | 提交 + 等待一体(IORING_ENTER_GETEVENTS 同时 wait) |
io_uring_register(fd, opcode, arg, nr) | 注册 buffers / files / eventfd / ring fd / restrictions |
重要的 params.flags(按版本):
5.1 IOPOLL SQPOLL SQ_AFF CQSIZE CLAMP ATTACH_WQ
5.10 R_DISABLED
5.18 SUBMIT_ALL
5.19 COOP_TASKRUN TASKRUN_FLAG SQE128 CQE32
6.0 SINGLE_ISSUER
6.1 DEFER_TASKRUN
6.5 NO_MMAP REGISTERED_FD_ONLY
6.6 NO_SQARRAY
生产建议默认组合:SINGLE_ISSUER | COOP_TASKRUN | DEFER_TASKRUN(要求 6.1+),在单线程 reactor 下实测能再降 15–20% CPU。
4. liburing:生产代码应该用的封装
裸 io_uring 协议每版都可能加字段,直接 mmap/写 tail 会让代码耦合到具体内核版本。liburing 是 Jens Axboe 亲自维护的 C 库,API 稳定、跨版本兼容、自动选择最优路径(如 CQE32 或否)。
核心 API(<liburing.h>):
// 初始化
int io_uring_queue_init(unsigned entries, struct io_uring *ring, unsigned flags);
int io_uring_queue_init_params(unsigned entries, struct io_uring *ring,
struct io_uring_params *p);
void io_uring_queue_exit(struct io_uring *ring);
// 提交侧
struct io_uring_sqe *io_uring_get_sqe(struct io_uring *ring);
void io_uring_prep_read (sqe, fd, buf, nbytes, offset);
void io_uring_prep_write (sqe, fd, buf, nbytes, offset);
void io_uring_prep_recv (sqe, fd, buf, len, flags);
void io_uring_prep_send (sqe, fd, buf, len, flags);
void io_uring_prep_accept (sqe, fd, addr, addrlen, flags);
void io_uring_prep_multishot_accept(sqe, fd, addr, addrlen, flags); // 5.19+
void io_uring_prep_connect(sqe, fd, addr, addrlen);
void io_uring_prep_close (sqe, fd);
void io_uring_prep_openat (sqe, dfd, path, flags, mode);
void io_uring_prep_timeout(sqe, ts, count, flags);
void io_uring_sqe_set_data64(sqe, __u64 data);
int io_uring_submit(struct io_uring *ring);
int io_uring_submit_and_wait(struct io_uring *ring, unsigned wait_nr);
// 完成侧
int io_uring_wait_cqe(struct io_uring *ring, struct io_uring_cqe **cqe);
int io_uring_peek_cqe(struct io_uring *ring, struct io_uring_cqe **cqe);
unsigned io_uring_peek_batch_cqe(ring, cqes, count);
void io_uring_cqe_seen(struct io_uring *ring, struct io_uring_cqe *cqe);
// 注册(性能优化)
int io_uring_register_buffers(ring, iov, nr);
int io_uring_register_files (ring, fds, nr);
int io_uring_register_eventfd(ring, fd);
io_uring_cqe_seen 不是可选的——不调就不前进 CQ head,CQ 填满后新 CQE 会溢出(5.5 前丢弃,5.5+ 靠 backlog 兜底但同样有上限)。
5. C++17 例:multishot accept 的 echo server
完整可编译例(需内核 5.19+ 支持 multishot_accept):
// g++ -std=c++17 -O2 echo.cpp -luring
#include <liburing.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include <cerrno>
#include <cstdio>
#include <cstdint>
#include <cstring>
enum Op : uint8_t { ACCEPT, READ, WRITE };
// Req 生命周期必须覆盖 SQE 提交到 CQE 到达的整段时间——堆分配最稳妥
struct Req {
Op op;
int fd;
uint32_t len;
char buf[4096];
};
int main() {
int listen_fd = ::socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
int one = 1;
::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
sockaddr_in a{};
a.sin_family = AF_INET;
a.sin_port = htons(9000);
a.sin_addr.s_addr = INADDR_ANY;
if (::bind(listen_fd, reinterpret_cast<sockaddr*>(&a), sizeof(a)) < 0) {
::perror("bind"); return 1;
}
if (::listen(listen_fd, 1024) < 0) { ::perror("listen"); return 1; }
io_uring ring;
io_uring_params p{};
// 需 Linux 6.1+;若更低版本去掉 DEFER_TASKRUN / SINGLE_ISSUER
p.flags = IORING_SETUP_COOP_TASKRUN
| IORING_SETUP_SINGLE_ISSUER
| IORING_SETUP_DEFER_TASKRUN;
if (io_uring_queue_init_params(4096, &ring, &p) < 0) {
::perror("io_uring_queue_init_params"); return 1;
}
// 一次性 arm multishot accept:之后每来一个连接产生一个 CQE
auto* acc = new Req{ACCEPT, listen_fd, 0, {}};
io_uring_sqe* sqe = io_uring_get_sqe(&ring);
io_uring_prep_multishot_accept(sqe, listen_fd, nullptr, nullptr, 0);
io_uring_sqe_set_data64(sqe, reinterpret_cast<uint64_t>(acc));
io_uring_submit(&ring);
for (;;) {
io_uring_cqe* cqe = nullptr;
int rc = io_uring_wait_cqe(&ring, &cqe);
if (rc < 0) {
if (rc == -EINTR) continue;
::fprintf(stderr, "wait_cqe: %s\n", ::strerror(-rc));
break;
}
auto* r = reinterpret_cast<Req*>(cqe->user_data);
int res = cqe->res;
uint32_t flags = cqe->flags;
switch (r->op) {
case ACCEPT:
if (res < 0) {
::fprintf(stderr, "accept: %s\n", ::strerror(-res));
} else {
auto* rd = new Req{READ, res, 0, {}};
sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, res, rd->buf, sizeof(rd->buf), 0);
io_uring_sqe_set_data64(sqe, reinterpret_cast<uint64_t>(rd));
}
// multishot: 没带 F_MORE 说明已终结,需重新 arm
if (!(flags & IORING_CQE_F_MORE)) {
sqe = io_uring_get_sqe(&ring);
io_uring_prep_multishot_accept(sqe, listen_fd, nullptr, nullptr, 0);
io_uring_sqe_set_data64(sqe, reinterpret_cast<uint64_t>(r));
}
break;
case READ:
if (res <= 0) { ::close(r->fd); delete r; break; }
r->op = WRITE;
r->len = static_cast<uint32_t>(res);
sqe = io_uring_get_sqe(&ring);
io_uring_prep_send(sqe, r->fd, r->buf, r->len, 0);
io_uring_sqe_set_data64(sqe, reinterpret_cast<uint64_t>(r));
break;
case WRITE:
if (res < 0) { ::close(r->fd); delete r; break; }
// 生产代码要处理 short write:res < r->len 时续写剩余
r->op = READ;
sqe = io_uring_get_sqe(&ring);
io_uring_prep_recv(sqe, r->fd, r->buf, sizeof(r->buf), 0);
io_uring_sqe_set_data64(sqe, reinterpret_cast<uint64_t>(r));
break;
}
io_uring_cqe_seen(&ring, cqe);
io_uring_submit(&ring);
}
io_uring_queue_exit(&ring);
return 0;
}
关键点:
Req必须堆分配,栈上会在函数返回后失效——CQE 到达时 buffer 已死user_data打包为Req*;高并发下要用结构化 tag(op 枚举 + fd + gen)避免 UAF- multishot 在
!(flags & F_MORE)时必须重新 arm - 没做 short write / short read 的续写循环,生产代码必须补
6. 错误码与边界行为(必须理解)
| 错误 | 场景 | 处理 |
|---|---|---|
cqe->res >= 0 | 成功;语义和对应同步 syscall 一致 | 注意 short read/write(res < 请求 len) |
cqe->res == -EAGAIN | 内部调度被打断(罕见) | 重提 SQE |
cqe->res == -EINTR | 被信号打断(通常是 enter 调用) | 重试 |
cqe->res == -ECANCELED | 被 ASYNC_CANCEL / 链断 / timeout link 取消 | 清理关联资源 |
cqe->res == -EALREADY | 取消失败(op 已进入执行阶段) | 设计为可容忍 |
cqe->res == -EOPNOTSUPP | 当前内核 / 文件系统不支持该 opcode | 回退到同步或 epoll |
cqe->res == -EINVAL | SQE 参数非法;包含 bit 位未清零 | 永远先 memset SQE |
io_uring_enter 返回 -EBUSY | CQ 满,无法再提交 | 先消费 CQE 再提交 |
关键陷阱:io_uring_prep_* 不会自动清零 SQE 其他字段。liburing 内部对 union 大部分位做了清理,但自定义时务必 memset(sqe, 0, sizeof(*sqe)) 再设字段。
7. 内核版本时间线(决定你能用哪些特性)
| 内核 | 关键特性 |
|---|---|
| 5.1 (2019-05) | 首发:READV/WRITEV/FSYNC/POLL_ADD,REGISTER_BUFFERS/FILES,SQPOLL(需 CAP_SYS_ADMIN) |
| 5.3 | IOSQE_IO_LINK、SENDMSG/RECVMSG |
| 5.5 | ACCEPT/CONNECT/ASYNC_CANCEL,FEAT_NODROP(CQ 溢出 backlog) |
| 5.6 | READ/WRITE 标量版、OPENAT/CLOSE/STATX,async buffered read |
| 5.7 | SPLICE/PROVIDE_BUFFERS,FEAT_FAST_POLL |
| 5.11 | FEAT_EXT_ARG;SQPOLL 改为 CAP_SYS_NICE |
| 5.12 | FEAT_NATIVE_WORKERS(真正任务身份 worker) |
| 5.13 | multi-shot POLL;SQPOLL 无需任何特权 |
| 5.15 LTS | REGISTER_IOWQ_MAX_WORKERS,direct descriptors |
| 5.18 | REGISTER_RING_FDS、MSG_RING、SETUP_SUBMIT_ALL |
| 5.19 | SEND_ZC 零拷贝、SETUP_COOP_TASKRUN/SQE128/CQE32、multishot ACCEPT |
| 6.0 | SETUP_SINGLE_ISSUER |
| 6.1 LTS | SETUP_DEFER_TASKRUN(强烈推荐开) |
| 6.5 | NO_MMAP(用户自带内存,huge pages 友好)、WAITID |
| 6.6 LTS | NO_SQARRAY,/proc/sys/kernel/io_uring_disabled 开关 |
| 6.7 | FUTEX_WAIT/WAKE、READ_MULTISHOT |
生产下限建议:5.15 LTS(保底);要完整享受 io_uring 性能:6.1 LTS / 6.6 LTS。
8. Pitfalls(至少 10 个,每个都踩过人)
- 完成顺序 ≠ 提交顺序。并发的 SQE 内核可能按任何顺序完成。必须用
user_data关联,不能假设”先提交先完成”。链式依赖用IOSQE_IO_LINK。 - Buffer 生命周期覆盖不足。SQE 提交后 buffer 被异步 I/O 持有,直到 CQE 到达才能释放。栈上 buffer、函数返回后析构的对象都是 UAF 陷阱。
- 没调
io_uring_cqe_seen。CQ head 不前进,很快堆满导致 overflow;5.5 前直接丢 CQE,5.5+ 靠 backlog 但 backlog 非无限,SQ 提交会退化为同步等待。 - SQE 脏字段。
io_uring_prep_*不保证清空所有字段。复用旧 SQE 时残留的flags、ioprio、union 尾部字节会导致-EINVAL。 - CQ size 等于 SQE size。默认 CQ = 2×SQ,高扇出场景(multishot、一个 SQE 多 CQE)容易溢出。显式设
p.cq_entries = 4 * entries。 - SQPOLL 以为是免费的。它消耗一个常驻 CPU 核,空闲
sq_thread_idlems 后挂起,挂起后你的第一次提交要走IORING_ENTER_SQ_WAKEUP再唤醒,延迟反而变大。低负载服务开 SQPOLL 是负优化。 - ASYNC_CANCEL 不保证成功。常回
-EALREADY(已进入执行)或-ENOENT(没找到)。设计必须把”取消失败”当正常分支。 - multishot 没处理
F_MORE消失。F_MORE未置位意味着这条 multishot 已终结(比如 accept 被 shutdown、poll 一次触发结束),必须重新 arm,否则服务直接卡死不再 accept 新连接。 - 多线程共享一个 ring。
io_uring_get_sqe/io_uring_submit不是无锁线程安全的。最佳实践是一线程一 ring(thread-local),必要时用MSG_RING跨线程投递。SETUP_SINGLE_ISSUER(6.0+)让内核校验并解锁DEFER_TASKRUN优化。 - 容器内直接失败。Docker 25.0+ 默认 seccomp profile 屏蔽
io_uring_setup/enter/register(moby#47532, PR #46762);RHEL 默认io_uring_disabled=2。部署前必确认宿主策略,不然启动就EPERM。 - fixed files 的注册时机。
REGISTER_FILES_UPDATE(5.5+)前只能先 UNREGISTER 再 REGISTER,全量替换代价 O(N)。长连接服务的 fd 集合变化频繁时注册表应按 slot 复用。 - 链式超时的逻辑。
LINK_TIMEOUT必须紧跟在被它保护的 SQE 之后,不是独立 submit。超时到期会给前一个 SQE 发-ECANCELED,而不是超时本身成为错误。
9. 对比:io_uring vs epoll vs libaio vs 线程池
| 维度 | io_uring | epoll | POSIX AIO | libaio (io_submit) | 线程池 |
|---|---|---|---|---|---|
| 模型 | completion | readiness | completion(伪) | completion | completion |
| regular file 异步 | ✅ | ❌(永远 ready) | ✅(线程模拟) | 仅 O_DIRECT | ✅ |
| 网络异步 | ✅ | ✅ | 有限 | ❌ | ✅ |
| 每 op syscall | 0(SQPOLL)– 1(batched) | ≥2(wait + rw) | 多(线程切换) | 2(submit + getevents) | 多(切换) |
| 元数据拷贝 | mmap 共享 0 拷贝 | event 结构拷贝 | 用户态线程 | iocb 入核拷贝 | 全用户态 |
| 最低内核 | 5.1(推荐 6.1+) | 2.5.45 | 任意 | 2.5 | 任意 |
| 用户 API 复杂度 | 高(liburing 中) | 中 | 低 | 高 | 低 |
| 零拷贝发送 | SEND_ZC (5.19+) | 需 MSG_ZEROCOPY | ❌ | ❌ | 需 MSG_ZEROCOPY |
| 可取消 | ASYNC_CANCEL(弱) | close fd | aio_cancel(弱) | 弱 | 自实现 |
| 容器默认可用 | ❌(Docker seccomp 屏蔽) | ✅ | ✅ | 视版本 | ✅ |
| 安全 CVE 风险 | 高(kCTF 占 60% 利用) | 低 | 低 | 低 | 低 |
选型建议:
- 纯网络服务、延迟不敏感:继续 epoll,不值得付 io_uring 的复杂度和安全税
- 高 IOPS 存储引擎(DB、KV、FS 层):io_uring 是唯一选项,libaio 在 buffered 下直接废
- 网络 + 磁盘统一 event loop(代理、转发、对象存储):io_uring 的统一接口是质变
- 容器化部署 + 公有云:先确认 seccomp 策略,否则直接排除 io_uring
10. 安全:io_uring 为何被多家大厂禁用
2023 年 Google 披露:提交到 kCTF VRP 的 Linux 内核 exploit 中 60% 利用 io_uring,合计付出约 100 万美元赏金。原因:
- 代码量庞大、异步路径引入大量新引用计数 / 生命周期
- 新增
URING_CMD(5.19+)是 ioctl 级别的直通通道,扩大攻击面 - 迭代极快,5.1 到 6.x 新 opcode 和 flag 几乎每版都加
禁用/限制 io_uring 的环境:
| 环境 | 策略 |
|---|---|
| ChromeOS | 完全禁用 |
| Android(应用层) | seccomp 禁止 |
| Google 生产服务器 | 全线禁用 |
| Docker 25.0+(默认 seccomp) | 屏蔽 setup/enter/register |
| GKE AutoPilot | 屏蔽 |
| RHEL 9 默认 | io_uring_disabled=2(完全禁用) |
6.6+ 提供 /proc/sys/kernel/io_uring_disabled 三档:0 允许所有、1 仅特权用户、2 禁用。生产容器场景默认取向是 1 或 2。
结论:基于 io_uring 的服务在部署文档里必须明确”不兼容默认 Docker seccomp”、“要求特定 sysctl”,否则用户会遇到诡异 EPERM。
11. 生产 Checklist
- 内核版本 ≥ 5.15(保底),目标 6.1 LTS / 6.6 LTS
- liburing ≥ 2.3(支持 multishot、CQE32、NO_MMAP)
- SQ 2^N(1024–4096 常见),CQ ≥ 2×SQ,multishot 场景
p.cq_entries = 4×entries - 单线程一 ring,开
SINGLE_ISSUER | COOP_TASKRUN | DEFER_TASKRUN(6.1+) - 热点 fd 走
REGISTER_FILES+IOSQE_FIXED_FILE - 热点 buffer 走
REGISTER_BUFFERS+READ_FIXED/WRITE_FIXED - SQPOLL 默认不开;仅当实测 syscall 是瓶颈再开,
sq_thread_idle设 100–1000 ms - 每个 CQE 检查
res < 0;专门处理-ECANCELED/-EAGAIN/-EINTR/-EOPNOTSUPP - 处理
short read/write(字节数小于请求长度必须续提交) - multishot 路径检查
!(flags & IORING_CQE_F_MORE)并 re-arm - Buffer 生命周期审计:所有异步 buffer 堆分配或属于长生命周期对象
- 长等待操作配
LINK_TIMEOUT避免孤儿 SQE - 用
IORING_REGISTER_PROBE探测 opcode 支持;不支持时回退 epoll / 线程池 - 部署文档写明:Docker 用户需自定义 seccomp profile(允许
io_uring_setup/enter/register),或在宿主/proc/sys/kernel/io_uring_disabled放行 - 指标埋点:
cq->overflow、sq->dropped、每秒 submit/complete 数、链断率、worker 线程数 - 及时跟进 CVE,订阅
linux-io_uring@vger.kernel.org
参考(authoritative sources)
man7.org/linux/man-pages/man7/io_uring.7.html及io_uring_setup(2)、io_uring_enter(2)、io_uring_register(2)- Jens Axboe, “Efficient IO with io_uring”(kernel.dk/io_uring.pdf)
- LWN 776703(Axboe 初稿)、810414(5.5/5.6 演进)、879724(安全讨论)
axboe/liburingGitHub:README、man/ 目录、examples/- Google Security Blog: “Learnings from kCTF VRP’s 42 Linux kernel exploits”(2023)
- moby/moby#47532 及 PR #46762(Docker seccomp 屏蔽的理由与历史)
- Unixism, “Lord of the io_uring”(入门到底层教程)
- Red Hat Developer: “Why you should use io_uring for network I/O”