背景
RCU-tasks 是 RCU 家族中专为无法显式标记读侧临界区的场景准备的 flavor,没有 rcu_read_lock() 之类的读侧原语,grace period 的判定依赖任务自己走到安全点。ftrace、BPF trampoline 等机制用 synchronize_rcu_tasks() 等待全系统所有任务经过一次静止状态,之后才安全地修改或释放 trampoline 结构。
任务经过静止状态的情形有四种:
- 自愿上下文切换
- 执行用户态代码
- 进入 idle
- 主动调用 cond_resched_tasks_rcu_qs() 上报
非自愿的抢占不在其中,被抢占的任务可能正停在读侧临界区中间。grace period 开始时,RCU-tasks 的 GP kthread 扫描全部任务,把正在运行队列上的非 idle 任务挂上 holdout 链表并记录 nvcsw 快照,之后反复扫描这张链表,任务经过静止状态或阻塞离开运行队列后被移出,链表清空 grace period 才结束。滞留超时就触发 stall 告警,打印任务状态与调用栈。
RCU-tasks grace period 与 holdout 检测
┌─────────────────────────────────────┐
│ grace period 开始 扫描全部任务 │
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ 运行中的非 idle 任务进 holdout 链表 │ 同时记录 nvcsw 快照
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ 反复扫描 移出已上报静止状态的任务 │ 自愿切换 用户态 idle 主动上报都算
└──────────────────┬──────────────────┘
▼
┌─────────────────────────────────────┐
│ 超时仍滞留触发 stall 告警 │ 打印任务状态与调用栈
└─────────────────────────────────────┘
direct reclaim 的扫描路径也在这个约束之下。shrink_lruvec() 的主循环按 LRU 逐轮扫描回收,每轮结束调一次 cond_resched(),意图是让出 CPU。但 cond_resched() 只在有调度需求时才真正调度;在 CONFIG_PREEMPTION 内核上任何点都可被抢占,它被实现成空操作(CONFIG_PREEMPT_DYNAMIC 下被静态调用修正为直接返回 0)。于是回收任务在循环里既不会发生自愿切换,也不会上报静止状态。
问题
- shrink_lruvec() 扫描循环只调 cond_resched(),CONFIG_PREEMPTION 内核上是空操作
- direct reclaim 没有时间上限,回收任务可长时间停在扫描循环里
- 非自愿抢占不算 RCU-tasks 静止状态,任务一直无法离开 holdout 链表
- 线上观测到回收任务滞留触发 rcu_tasks stall 告警,调用栈停在 shrink_lruvec()
方案
把 shrink_lruvec() 扫描循环里的 cond_resched() 换成 cond_resched_tasks_rcu_qs(),每一轮扫描结束都主动向 RCU-tasks 上报一次静止状态,不再以发生调度为前提。
#define cond_resched_tasks_rcu_qs() \
do { \
rcu_tasks_qs(current, false); \
cond_resched(); \
} while (0)
上报静止状态与让出 CPU 解耦
┌───────────────────────────────┐
┌───▶│ rcu_tasks_qs 主动上报静止状态 │
┌───────────────────────────┐ │ └───────────────────────────────┘
│ cond_resched_tasks_rcu_qs ├───┤
└───────────────────────────┘ │ ┌───────────────────────────────┐
└───▶│ cond_resched 有需求才调度 │
└───────────────────────────────┘
前半段 rcu_tasks_qs(current, false) 检查当前任务的 rcu_tasks_holdout 标记,若已被 GP kthread 置位就清掉,效果等同声明 grace period 不必再等自己。第二个参数是它与被抢占路径的分界:调度器在上下文切换时也调这个宏,自愿切换传 false 算静止状态,被抢占传 true 则不算。后半段 cond_resched() 保持原语义,有调度需求时仍然让出 CPU。上报与调度就此解耦,cond_resched() 是不是空操作不再影响 RCU-tasks 的判定。
安全性由循环位置保证:上报点在每轮扫描结束、未持有 lru_lock 的上下文里,本来就是 cond_resched() 的合法调用点,RCU 子系统自身的长循环也用同一个宏做上报。开销上,标记未置位时只是一次 per-task 读取,grace period 期间才多一次写。
收益
作者未提供性能数据,从代码逻辑推断的预期收益:
- direct reclaim 任务不再滞留 holdout 链表,消除 rcu_tasks stall 告警
- grace period 不再被长扫描循环拖延,trampoline 等 RCU-tasks 回调得以及时执行
- 无 grace period 时每轮循环仅多一次标记读取,开销可忽略