0/5 已展开

LLM 分析

RCU Tasks:用 irq_work 延迟 IRQ-off 回调入队

系列概况

  • 标题:[PATCH 6.18.y] rcu-tasks: Defer IRQ-disabled callback enqueue to irq_work
  • 作者:Matt Fleming
  • 版本:面向 6.18.y 的首次投稿(无 v2 标记)
  • 规模:1 个实际 patch
  • 修改文件:1 个,kernel/rcu/tasks.h
  • 代码统计:+148/-36
  • Message-ID:20260710095359.2643791-1-matt@readmodwrite.com
  • 完整性:完整;b4 收到 5 封邮件、识别 1 个 patch,数量匹配且无缺片或 tip-bot2 污染

一句话总结:IRQ 已关闭时不再直接争抢 RCU Tasks 回调队列锁,而是先无锁暂存,再由 hard irq_work 排入正式队列,以切断与调度器 rq->lock 的 ABBA 死锁环。

补丁目的

call_rcu_tasks_generic() 可从 sched-ext/BPF task storage 清理路径进入;此时调用者可能已经关闭 IRQ,并持有运行队列锁 rq->lock。提交 ab97152f88a4 引入“竞争过高时扩展为 per-CPU 回调队列”的机制后,入队慢路径可能进一步获取全局 cbs_gbl_lock

另一侧,RCU Tasks grace-period/wakeup 路径可能先持有 cbs_gbl_lock,再经打印、唤醒进入调度器并尝试获取 rq->lock。两个 CPU 以相反顺序拿锁,形成死锁。

旧流程的问题

CPU 0:sched_ext_free()                 CPU 1:rcu_tasks_one_gp()
  task_rq_lock()                          lock(cbs_gbl_lock) [B]
    lock(rq->lock) [A]                    printk / try_to_wake_up()
  scx_exit_task()                           lock(rq->lock) -> 等 A
    bpf_task_storage_delete()
      call_rcu_tasks_generic()
        lock(cbs_gbl_lock) -> 等 B

                 A 等 B,B 等 A:ABBA 死锁

问题不在普通无竞争入队,而在 IRQ-off 调用者遇到回调队列竞争后,仍可能在未知外层锁之下取得 per-CPU 锁,随后为扩容取得全局锁。

新流程

call_rcu_tasks_generic(rhp, func)
          |
     原始上下文已关 IRQ?
        /          
      否            是
      |              |
  锁住 cblist     llist_add(rhp, bypass_list)
  正常入队          |  (无锁,不碰 cbs_* 锁)
      |           irq_work_queue(hard work)
      |              |
      |         稍后 drain:
      |         del_all -> reverse -> 锁住 cblist
      |              |              -> 正式入队
      +--------------+
                     |
             必要时唤醒 GP kthread

旧:IRQ-off 调用者同步进入正式回调队列,竞争时可能触发全局队列扩容。

新:IRQ-off 调用者仅写入 per-CPU 无锁旁路;获得安全执行上下文的 hard irq_work 再完成正式入队。IRQ 原本开启的 fast path 保持直接入队。

关键实现

1. per-CPU 无锁旁路

struct rcu_tasks_percpu 新增 rtp_irq_bypass_listrtp_irq_bypass_work。入口在执行 local_irq_save() 前先记录 irqs_disabled(),所以能区分“调用者本来就关 IRQ”和“函数为了保护自身临时关 IRQ”。前者执行:

llist_add((struct llist_node *)rhp, &rtpcp->rtp_irq_bypass_list);
irq_work_queue(&rtpcp->rtp_irq_bypass_work);

rcu_head 的首字段可作为 llist_node 链接使用;此路径不取得 cbs_pcpu_lockcbs_gbl_lock,因此不会把未知外层锁带入原来的锁环。

2. 排干时恢复顺序并复用统一入队逻辑

rcu_tasks_drain_irq_bypass()llist_del_all() 原子摘取一批回调。llist_add() 形成 LIFO 链,所以先用 llist_reverse_order() 恢复本批次的提交顺序,再逐个调用新抽出的 call_rcu_tasks_enqueue_locked()

该 helper 统一处理 lazy timer、urgent_gpwakeme_after_rcurcu_segcblist_enqueue(),避免直接路径与延迟路径语义分叉。锁竞争统计和扩容分别抽成 rcu_tasks_need_queue_adjust()rcu_tasks_adjust_cbs();排干释放 per-CPU 锁后才做全局扩容。

3. barrier 与 GP 扫描主动排干

rcu_barrier_tasks_generic() 在 entrain barrier 回调前先排干旁路,并在同一 per-CPU 锁保护下完成“旁路到 cblist”的转移,防止 barrier 越过在途回调而提前返回。rcu_tasks_need_gpcb() 扫描正式队列前也先排干,否则 GP 线程可能看不到旁路中等待处理的回调。

4. 初始化与唤醒语义

每种 RCU Tasks flavor 的 per-CPU 对象静态初始化 hard irq_work,cblist_init_generic() 再初始化旁路链表。排干汇总 needwake,在回调正式入队后唤醒已存在的 GP kthread;IRQ-enabled 原路径仍通过既有 rtp_irq_work 延迟唤醒。

类比

这像仓库门口增设“免锁投递箱”。手里还握着另一把关键钥匙(rq->lock)的快递员,不再排队争抢仓库总门锁(cbs_gbl_lock),只把包裹放进投递箱并按铃;专门的收件员(hard irq_work)稍后按顺序转入仓库。这样仓库管理员与快递员不会互相等对方交出钥匙。

Highlight:风险与注意点

  • Sashiko 机器人提出 High 告警:若子系统初始化前已有 IRQ-off 回调写入旁路,随后 init_llist_head() 会把链头清空,可能永久丢失早期回调。旧代码虽注明“不支持初始化前入队”,却会按需初始化 cblist;新旁路绕过了该检查。线程没有人类回复确认或否定,故这是未解决风险,不能当成误报。
  • llist_reverse_order() 只恢复每次摘取批次内的 FIFO;并发新回调会留给下一次排干,这是 lockless 批处理的预期边界,但需要压力测试覆盖连续入队和 irq_work 重排队。
  • barrier 正确性依赖“摘旁路、转入 cblist、entrain barrier”与同一 per-CPU 锁串行;后续改锁范围时不能拆开该约束。
  • IRQ-off 回调会增加一次 irq_work 调度及批量排干延迟,换取避免锁反转;IRQ-enabled fast path 不新增该延迟。
  • Paul E. McKenney 仅评价方案“看起来合理”,未给出 Reviewed-by/Acked-by。他指出补丁不能干净应用到 mainline 或 -rcu,并要求 forward-port,以同时防范 call_rcu_tasks()call_rcu_tasks_rude() 的类似问题;Matt 已同意,但本线程没有后续版本。

版本变化

线程只有这份 6.18.y 投稿,没有 v2。维护者讨论形成的后续要求是 forward-port 到 mainline/-rcu。当前本地 mainline 的 kernel/rcu/tasks.h 仍保留旧的同步入队和竞争后获取 cbs_gbl_lock 流程,也未找到该补丁提交,因此不能把本投稿描述为已合入 mainline。

一句话总结

补丁用“per-CPU lockless llist + hard irq_work 排干”隔离 IRQ-off 调用者与 RCU Tasks 队列锁,直接打断 rq->lock -> cbs_gbl_lock 的死锁边;设计方向获维护者初步肯定,但早期初始化回调可能丢失的机器人告警尚未闭环,且 mainline forward-port 仍待提交。