背景

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 锁竞争与等待时间都显著下降。