Linux 社区 io_uring MPSCQ task_work 重构
io_uring MPSCQ Task_Work 重构详解
📋 问题定位
- 内核子系统: io_uring 任务工作 (task_work) 基础设施
- 核心数据结构:
struct mpscq— 多生产者单消费者 FIFO 无锁队列 - 涉及文件:
io_uring/mpscq.h,io_uring/tw.c,io_uring/tw.h,io_uring/tctx.c,io_uring/sqpoll.c,io_uring/io_uring.c - 主要作者: Jens Axboe
🎯 背景与动机
重构前,io_uring 的 task_work 使用 Linux 内核标准的 llist(无锁链表)管理。llist 是 LIFO(后进先出)顺序的,为了确保按入队顺序公平执行,消费端必须先做 llist_reverse_order() 的 O(n) 翻转。此外,当一次批量处理被 max_entries 截断时,剩余条目必须被暂存到单独的 retry_llist/retry_list 中。
这种方案有两个核心缺陷:
- O(n) 翻转开销 — 在高吞吐场景下成为显著的 CPU 开销
- 生产者非 wait-free —
llist_add()使用cmpxchg重试循环
🎯 重构架构
重构的核心是用 MPSCQ(基于 Dmitry Vyukov 的侵入式 MPSC 节点队列算法)替代 llist。
数据结构
struct mpscq {
struct llist_node *tail; // 生产者侧:队列尾部
struct llist_node stub; // 哨兵节点,tail==&stub 表示空队列
};
// 消费者游标由调用方持有(放入不同 cacheline,避免伪共享)
struct io_uring_task {
struct mpscq task_list; // 普通 task_work 队列
struct llist_node *task_head; // 消费者游标
...
};
struct io_ring_ctx {
struct mpscq work_list; // 本地 (DEFER_TASKRUN) 队列
struct llist_node *work_head; // 消费者游标
...
};
消费者游标与生产者队列分离的设计,是为了让频繁写入的消费者游标(每次 mpscq_pop() 都写)不与生产者共享 cacheline,避免生产者 xchg() 操作因缓存行失效而变慢。
核心算法
入队 mpscq_push() — wait-free:
// single xchg + store, no retry loops
bool mpscq_push(struct mpscq *q, struct llist_node *node) {
node->next = NULL; // 1. 初始化
struct llist_node *prev = xchg(&q->tail, node); // 2. 原子交换:成为新尾
prev->next = node; // 3. 链接到前驱
return prev == &q->stub; // 返回是否之前为空
}
两步发布(xchg 使节点可见为先,store 链接到前驱为后)意味着存在一个短暂窗口——节点已是新尾但还不可从头可达。此时 mpscq_pop() 返回 NULL,消费端必须重试。
出队 mpscq_pop():
struct llist_node *mpscq_pop(struct mpscq *q, struct llist_node **head) {
struct llist_node *cur = *head;
struct llist_node *next = READ_ONCE(cur->next);
if (unlikely(!next)) // 窗口期:不可达
return NULL;
if (cur == &q->stub) {
// 从哨兵开始:取第一个真实节点
*head = next;
return next;
}
*head = next; // 前进游标
return cur;
}
链表末节点的处理使用 try_cmpxchg 的 stub 重新插入机制,确保消费端不会与生产者的链接操作竞争。
💡 收益效果
| 场景 | 重构前 | 重构后 | 提升 |
|---|---|---|---|
| 8 客户端 multishot recv | 2.64% overhead @ ~46Gb/s | 2.11% overhead @ ~53Gb/s | ~15% 更少开销, ~15% 更多吞吐 |
| 1024 客户端单 ring | 3.50% overhead @ ~24Gb/s (2.22% 在 llist_reverse_order) |
1.32% overhead @ ~26Gb/s | ~62% 更少开销, ~8% 更多吞吐 |
| ublk 4KB (Caleb Sander) | — | — | 4% 提升 |
数据来源:commit d46ab2c98aba
💡 副作用化简
MPSCQ 重构不仅带来了性能收益,还大幅简化了代码架构:
| 删减组件 | 说明 |
|---|---|
retry_llist |
本地 task_work 的截断遗留列表 |
retry_list |
普通 task_work 的截断遗留列表 |
fallback_llist / fallback_work |
每个 io_ring_ctx 专用的退避分发机制 |
io_fallback_req_func() |
ctx 级退避 work handler |
__io_fallback_tw() |
队列重新分发函数 |
tw_pending 位 |
防止重复 task_work_add() 的防护位 |
nr_tw 字段 |
io_kiocb 上的 task_work 计数 |
io_handle_tw_list() |
遍历翻转后 llist 的辅助函数 |
总计移除了约 250 行代码,整个退避路径从”生产者排入 → 分发给每个 ctx 的独立 llist → 调度每个 ctx 的退避 work”简化为”直接用标准 tctx_task_work_run() 执行”。
💡 应用前景
此重构已被合入 Linux 主线,构成 io_uring task_work双队列架构的基础:
io_req_task_work_add()
├── IORING_SETUP_DEFER_TASKRUN → io_req_local_work_add()
│ └── mpscq_push(&ctx->work_list)
└── 默认 → io_req_normal_work_add()
└── mpscq_push(&tctx->task_list)
+ task_work_add(tctx->task_work) // 若队列之前为空
这种架构未来可扩展到其他需要高性能 FIFO 无锁队列的内核子系统。mpscq.h 作为独立头文件不依赖 io_uring 特定类型,理论上可被其他模块复用。
📜 关键 Commit 汇总
| Commit SHA | 标题 | 作用 |
|—|—|—|
| 50cb44bd0d5f | io_uring/mpscq: add lockless multi-producer, single-consumer FIFO queue | 引入 MPSCQ 数据结构 |
| d46ab2c98aba | io_uring: switch local task_work to a mpscq | 本地 DEFER_TASKRUN 队列切换到 MPSCQ |
| de7341ffe49e | io_uring: switch normal task_work to a mpscq | 普通 tctx task_work 队列切换到 MPSCQ |
| ca4aa97194ae | io_uring: get rid of tw_pending for !DEFER task work | 利用 mpscq_pop_emptied() 移除 tw_pending 位 |
| df58c2161684 | io_uring: run the tctx task_work fallback directly | tctx 退避直接复用标准运行路径 |
| 576cce91480a | io_uring: remove the per-ctx fallback task_work machinery | 移除整个 per-ctx 退避机制 |
参考来源
本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。