sched-ext discussion
[PATCH 6.18.y] rcu-tasks: Defer IRQ-disabled callback enqueue to irq_work
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_list 和 rtp_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_lock 或 cbs_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_gp、wakeme_after_rcu 和 rcu_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 仍待提交。