Linux 社区 io_uring MPSCQ task_work 重构

原始问题:详细介绍一下 Linux 社区 io_uring: MPSCQ task_work 重构 · 2026-07-08

io_uring MPSCQ Task_Work 重构详解

📋 问题定位

🎯 背景与动机

重构前,io_uring 的 task_work 使用 Linux 内核标准的 llist(无锁链表)管理。llist 是 LIFO(后进先出)顺序的,为了确保按入队顺序公平执行,消费端必须先做 llist_reverse_order() 的 O(n) 翻转。此外,当一次批量处理被 max_entries 截断时,剩余条目必须被暂存到单独的 retry_llist/retry_list 中。

这种方案有两个核心缺陷:

  1. O(n) 翻转开销 — 在高吞吐场景下成为显著的 CPU 开销
  2. 生产者非 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 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。