背景
vmstat 用一个叫 vmstat_shepherd 的守护工作周期性检查各 CPU 的 per-cpu 计数,若有待刷新的,就给那个 CPU 排一个 vmstat_update。排之前它先用 delayed_work_pending() 问一句"这个 CPU 的 vmstat_update 是不是已经排过了",避免重复。
┌──────────────────────────────────┐
│ vmstat 守护检查各 CPU 是否待更新 │
└────────────────┬─────────────────┘
▼
┌──────────────────────────────────┐
│ 用待决位判断,但正执行时该位已清 │ 守护误以为没排,又排一次
└────────────────┬─────────────────┘
▼
┌──────────────────────────────────┐
│ 重复刷新触发反复取 zone 锁 │ 72 核上可观测到同一时钟周期内调 2 次
└──────────────────────────────────┘
问题在于 delayed_work_pending() 只测 WORK_STRUCT_PENDING_BIT,而这个位在 worker 线程把 work 取走去执行的那一刻就被清掉了。于是 vmstat_update 正在 CPU 上跑的时候,delayed_work_pending() 返回 false;若此刻 need_update() 也为真(per-cpu 计数还没清零到一半),shepherd 就以为没排过,用 delay=0 再排一次,让 vmstat_update 跑完立刻再跑一遍。在 72-CPU 系统上这个 race 很容易看到:修复前不少 CPU 的 2 次调用间隔远低于 500 jiffies(round_jiffies_relative 能产生的最小值),最极端的甚至 0 jiffies,同一 jiffy 内调了 2 次。
问题
- delayed_work_pending 只测待决位,work 正执行时已清
- vmstat_update 正跑时守护误判为未排
- 用 delay=0 再排一次,双重调度
- 重复刷新反复取 zone 锁
方案
改用 work_busy() 替代 delayed_work_pending()。
work_busy() 对 WORK_BUSY_PENDING(timer 已 armed 或 work 已入队)和 WORK_BUSY_RUNNING(work 正在执行)都返回非零,所以 vmstat_update 无论在排队还是在跑,shepherd 都能正确判断为"忙"而跳过。
旧:只看待决位
┌────────────────────────┐
│ 正在执行时待决位已清 │
└───────────┬────────────┘
▼
┌────────────────────────┐
│ 守护误判未排,重复调度 │
└────────────────────────┘
新:看待决加正执行
┌────────────────────────────┐
│ 待决或正执行都算忙 │
└─────────────┬──────────────┘
▼
┌────────────────────────────┐
│ 守护正确跳过,不再重复调度 │ 残余竞态罕见,只偶尔小间隔
└────────────────────────────┘
修复后所有 sub-jiffy 间隔和多数 sub-100-jiffy 间隔都消失了,剩下的少数早期调用间隔落在 700-999 jiffies,那是 round_jiffies_relative 对齐到更近的 jiffie-second 边界所致,与这个 race 无关。
需诚实说明 work_busy() 自身仍有 race:在检查与随后 queue_delayed_work_on 之间,vmstat_update 可能正好跑完,work 既不 pending 也不 running,shepherd 仍会排第二次。修复后这个残余 race 罕见,只产生偶尔的小间隔,相比 delayed_work_pending 的系统性双重调度已是大幅改善。
收益
每个多余的 vmstat_update 都有连带代价:它会逐 zone 把闲置的 per-CPU 页倒回 buddy,每次都取 zone 自旋锁。消除双重调度直接降低 zone 锁竞争。作者在 72-CPU stress-ng 工作负载上用 perf lock contention 测量:
| 指标 | 变化 |
|---|---|
| free_pcppages_bulk 竞争次数 | ~-55% |
| free_pcppages_bulk 总等待时间 | ~-57% |
| free_pcppages_bulk 最大等待时间 | ~-47% |
作为大系统的可扩展性优化,消除守护的系统性双重调度后,free_pcppages_bulk 的 zone 锁竞争与等待时间都显著下降。