0/4 已展开

LLM 分析

rcu-tasks: Defer IRQ-disabled callback enqueue to irq_work

系列基线信息

字段
标题[PATCH] rcu-tasks: Defer IRQ-disabled callback enqueue to irq_work
作者Matt Fleming (Cloudflare)
版本v1(尚未提交 v2)
规模单 patch,约 +103/-43 行
Message-ID20260713101245.3207634-1-matt@readmodwrite.com
来源sched-ext 频道
RCU MaintainerPaul E. McKenney

明确目的

call_rcu_tasks_generic() 在 IRQ 已禁用的上下文中被调用时,会直接尝试获取 cbs_gbl_lock,从而与调度器 runqueue 锁形成 ABBA 死锁。

具体触发路径:sched_ext 的 task-storage 拆卸 → 持有 rq->lock → 调用 call_rcu_tasks_generic() → 尝试获取 cbs_gbl_lock;而另一条路径(rcu_tasks_one_gpprintktry_to_wake_up)恰好持有 cbs_gbl_lock 后又请求 rq->lock,形成循环等待。

本 patch 的目标:在 IRQ 禁用场景下,将回调入队延迟到 irq_work 中执行,避免在 IRQ 禁用上下文中拿锁,从根本上消除锁序反转。


遍历代码

1. 新增数据结构

每个 per-CPU 的 rcu_tasks_percpu 新增两个字段:

struct irq_work rtp_irq_bypass_work;   // 延迟排空用的 IRQ work
struct llist_head rtp_irq_bypass_list; // 无锁回调暂存链表

初始化时 rtp_irq_bypass_work 绑定为 IRQ_WORK_INIT_HARD(硬 IRQ 上下文执行),rtp_irq_bypass_list 初始化为空链表。

2. call_rcu_tasks_generic() 快速路径变更

原函数入口处检测 irqs_disabled()

bool irqsoff = irqs_disabled();
  • IRQ 开启:走原有路径,直接拿锁、入队,逻辑不变。
  • IRQ 禁用:将回调挂入 rtp_irq_bypass_listllist_add,无锁),然后通过 irq_work_queue() 排空 work,立即返回,不拿任何自旋锁。

3. call_rcu_tasks_iw_drain_irq_bypass() — irq_work 回调

由硬 IRQ 上下文触发,调用 rcu_tasks_drain_irq_bypass(rtpcp)

4. rcu_tasks_drain_irq_bypass() — 核心排空逻辑

  1. local_irq_save() + rcu_read_lock() — 确保 IRQ 和 RCU 读侧保护。
  2. call_rcu_tasks_lock() — 尝试拿 per-CPU cblist 锁,返回是否争抢过。
  3. llist_del_all() — 一次性取出 bypass 链表全部节点。
  4. 若无节点,解锁返回。
  5. llist_reverse_order() — 恢复原始入队顺序(FIFO 语义)。
  6. 遍历每个回调,调用 call_rcu_tasks_enqueue_locked() 正式入队。
  7. 若争抢过,调用 rcu_tasks_need_queue_adjust() 检查是否需要扩展为 per-CPU 模式。
  8. 若需唤醒 kthread,调用 rcuwait_wake_up()

5. call_rcu_tasks_enqueue_locked() — 提取的入队逻辑

将原来 call_rcu_tasks_generic() 中入队、lazy timer、urgent_gp 等逻辑提取为独立函数,供正常路径和排空路径共用。

6. rcu_barrier_tasks_generic()rcu_tasks_need_gpcb() 的补丁

两处都在操作 cblist 前增加 rcu_tasks_drain_irq_bypass(rtpcp) 调用,确保 barrier 和 GP 检测时 bypass 链表中的回调不会被遗漏。


ASCII 流程图

                   IRQ-disabled 上下文调用
                   call_rcu_tasks_generic()
                            |
                 +----------+----------+
                 |                     |
            irqs_disabled()        irqs_enabled()
                 |                     |
         llist_add(bypass)     直接拿锁入队
                 |              call_rcu_tasks_
         irq_work_queue()      enqueue_locked()
                 |                     |
         [返回,不拿锁]           [正常完成]
                 |
         === 硬 IRQ 上下文 ===
                 |
     call_rcu_tasks_iw_drain_irq_bypass()
                 |
         rcu_tasks_drain_irq_bypass()
                 |
         llist_del_all(bypass)
                 |
         llist_reverse_order()
                 |
         call_rcu_tasks_enqueue_locked()
         (正式入队到 per-CPU cblist)

死锁路径对比

=== 修复前 (ABBA 死锁) ===

  CPU 0                         CPU 1
  ------                         ------
  sched_ext_free()
    rq->lock  <---+
                  |              rcu_tasks_one_gp()
  call_rcu_tasks_generic()        cbs_gbl_lock  <---+
    cbs_gbl_lock  <---+                              |
                       |            printk()
                       |              try_to_wake_up()
                       |                rq->lock  <---+
                       |                                 |
                       +------------- 循环等待 -----------+

=== 修复后 (IRQ 禁用时绕过) ===

  CPU 0 (IRQs off)
  ------
  sched_ext_free()
    rq->lock
  call_rcu_tasks_generic()
    llist_add(bypass)   <-- 无锁!
    irq_work_queue()    <-- 延迟处理
    [返回,不拿 cbs_gbl_lock]

概念类比

想象一个银行柜台(cbs_gbl_lock)和保安岗(rq->lock)。顾客 A 在保安岗登记后去柜台排队,顾客 B 在柜台办完事后去保安岗登记——如果两人同时进行,A 等柜台、B 等保安岗,就死锁了。

修复方案:当顾客已经站在保安岗旁(IRQ 禁用 = 安全区域限制),不再让他直接去柜台排队,而是让他把需求投进一个"快速投递箱"(rtp_irq_bypass_list),然后由一个专门的跑腿员(irq_work)在合适时机把投递箱里的需求统一送到柜台。跑腿员只在保安岗空闲时才去柜台,避免了两个人同时占着各自的资源等对方。


Highlight 突出问题

  1. rcu_barrier_tasks_generic() 需排空所有队列:Paul McKenney 指出,RCU Tasks 有时存在多个回调队列,barrier 必须排空所有队列。当前 patch 的 barrier 排空逻辑可能不完整,v2 需要确保覆盖所有 per-CPU bypass 链表。
  2. CPU hotplug 场景:CPU 下线时若 bypass 链表还有未排空的回调,谁来处理?Paul 提到正在讨论的 call_rcu()/call_srcu() deferral 系列会解决此问题,v2 应 piggyback 其上。
  3. 与 BPF 程序的交互:Paul 提到 off-list 讨论中发现了 BPF 程序与 CPU hotplug 之间"有趣的关系",影响 call_rcu_tasks() 路径,但细节尚未公开。
  4. patch 已过时:v1 不再能干净地应用到 Paul 的树,需要提交 v2。

版本演进

版本状态关键改动
v1已提交,需重做初始实现 IRQ bypass 机制;Paul 反馈 barrier 需排空所有队列;建议等待 call_rcu()/call_srcu() deferral 系列后再做 v2

与其他相关 patch 系列的关联

  • call_rcu()/call_srcu() deferral 系列(Paul McKenney 团队正在开发):同样采用 irqsoff 局部变量检测 + 延迟入队模式,将提供可复用的 helper 函数。Matt 同意 v2 将 piggyback 在此系列之上。
  • 该系列还将明确 CPU 下线时 deferred callback 的处理方式。

一句话总结

通过在 IRQ 禁用时将 RCU Tasks 回调暂存到无锁 llist 并延迟到 irq_work 排空,消除了 call_rcu_tasks_generic() 与调度器 runqueue 锁之间的 ABBA 死锁;v2 将等待上游 call_rcu()/call_srcu() deferral 基础设施就绪后重新提交。