sched discussion
[PATCH RFC 2/2] sched/core: Defer preempted remote vCPU task clock updates
LLM 分析
sched/core 与 KVM x86:修复 guest 内 vCPU 抢占期间的 task clock 记账
系列概况
- 标题:
[PATCH RFC 0/2] Fix KVM guest scheduling accounting issue related to stealtime - 作者:Dongli Zhang dongli.zhang@oracle.com
- 版本:RFC v1(base-commit
818bebeb63dd6bf5f4e07e145f6cdbace520a34c) - 规模:2 个补丁,共 3 个文件,+57/-26 行
- 修改文件:
arch/x86/kvm/x86.c(+32/-26)kernel/sched/core.c(+24/0)kernel/sched/sched.h(+1/0)
- 代码统计:2 文件 +25 行 + 1 文件 +1 行 = +57 / -26(合并 diff)
- Message-ID:
20260824012716.753022-1-dongli.zhang@oracle.com(封面);...-2-...(patch 1);...-3-...(patch 2) - 完整性:完整(封面 + 2 patch + 末尾附复现 Python 脚本)
补丁目的
修复 KVM guest 内跨 vCPU 进行 runqueue clock_task 更新时的 steal time 记账漏洞。
当 vCPU A 通过 update_rq_clock_task() 给 vCPU B 做记账,而 vCPU B 被 host 抢占 stall 时,host 只在 B 重新进入 guest 之前才更新 stealtime。在 stall 期间:
- vCPU A 看到的
vcpu_is_preempted(B) == true,stealtime 却还没有增加; - guest 把这段被 host 偷走的时间错误地记成 B 上 task 的 runtime;
- 等 host 终于更新 stealtime 时,由于 elapsed delta 已经用过,stealtime 无法全部扣除,任务额外承担一次调度惩罚。
复现:作者用 runtime_stall_detector_v2.py 在 guest 里开了 5 个 pin 到 vCPU 2 的线程,再用 host 上的 chrt -f 90 在 pCPU 10 上 stall 10 秒。修复前有 worker 报告 gap=49.718478 sec(约 10s 真实 stall + 约 40s 额外惩罚),修复后 5 个 worker 都恢复到约 10s。
旧流程的问题
在裸金属上,update_rq_clock_task() 让远端 CPU 帮忙给本 rq 推进 clock_task,逻辑闭环。但在 KVM guest 里,这一闭环被 vcpu_is_preempted() 撕开:
- B 被 host 抢占,B 的 stealtime 冻结,但 host 上的 B-stalled 时间是真实流逝的;
- A 用 IRQ + steal delta 推 B 的
clock_task,这段应该被 steal 扣掉的时间被错误地记成 B 上 task 的运行时间; - 真正进入 guest 时 stealtime 才更新,但此时 delta 已经消费过,无法回填。
新流程
把 KVM 侧的 stealtime 发布和 guest 侧的 clock_task 推迟配对起来:
- KVM host(patch 1/2):
record_steal_time()在clear preempted=0之前先把 stealtime 写回用户态缓冲区,保证vcpu_is_preempted() == false与最新 stealtime 同步可见。 - Guest scheduler(patch 2/2):
update_rq_clock_task()检测rq_cpu != smp_processor_id() && vcpu_is_preempted(rq_cpu)时,把 delta 累加到新的rq->deferred_clock_task上,立刻 return;当下一次发现 B 不再被抢占时,再把 deferred_clock_task 折回 delta,一起送进 IRQ/steal 账本。
这样 A 推迟记账,B 一旦恢复,delta 一并结清,stealtime 一定能在它覆盖的窗口内被扣掉。
Patch 概览
| Patch | 文件 | 关键改动 |
|---|---|---|
| 1/2 | arch/x86/kvm/x86.c | record_steal_time 内把 user_access_begin ... version+1 ... smp_wmb ... steal ... user_access_end 整段往写 preempted=0 之前挪,并在末尾追加 smp_wmb(),保证版本号切换对远端观测者可见 |
| 2/2 | kernel/sched/core.c + sched.h | rq 新增 u64 deferred_clock_task;update_rq_clock_task() 在 CONFIG_PARAVIRT_TIME_ACCOUNTING 下检查 paravirt_steal_rq_enabled 与 vcpu_is_preempted() |
关键实现
Patch 1(KVM 侧)核心顺序:
static void record_steal_time(struct kvm_vcpu *vcpu)
{
...
if (!user_access_begin(st, sizeof(*st)))
return;
unsafe_get_user(version, &st->version, out);
if (version & 1)
version += 1; /* first time write, random junk */
version += 1;
unsafe_put_user(version, &st->version, out);
/* Pairs with the guest side virt_rmb() in kvm_steal_clock(). */
smp_wmb();
unsafe_get_user(steal, &st->steal, out);
steal += current->sched_info.run_delay - vcpu->arch.st.last_steal;
vcpu->arch.st.last_steal = current->sched_info.run_delay;
unsafe_put_user(steal, &st->steal, out);
version += 1;
unsafe_put_user(version, &st->version, out);
/*
* Publish the stealtime before making the vCPU look runnable to
* the guest.
*/
smp_wmb();
...
unsafe_put_user(0, &st->preempted, out); /* now clear */
vcpu->arch.st.preempted = 0;
out:
user_access_end();
mark_page_dirty_in_slot(vcpu->kvm, ghc->memslot, gpa_to_gfn(ghc->gpa));
}
Patch 2(guest scheduler 侧)核心:
static void update_rq_clock_task(struct rq *rq, s64 delta)
{
#ifdef CONFIG_PARAVIRT_TIME_ACCOUNTING
if (static_key_false(¶virt_steal_rq_enabled)) {
int rq_cpu = cpu_of(rq);
/*
* A remote CPU can update this rq before the owner vCPU
* has re-entered the guest and refreshed its stealtime
* state. Do not charge that elapsed time to the current
* task until stealtime can be sampled after the vCPU is
* no longer preempted.
*/
if (rq_cpu != raw_smp_processor_id() &&
vcpu_is_preempted(rq_cpu)) {
rq->deferred_clock_task += delta; /* remote rq & preempted: defer */
return;
}
if (rq->deferred_clock_task) { /* once updatable, fold back */
delta += rq->deferred_clock_task;
rq->deferred_clock_task = 0;
}
}
#endif
/* existing IRQ + steal accounting logic unchanged */
...
}
类比
把 clock_task 想象成咖啡馆的营业额流水。stealtime 是老板被叫去开会(被 host 抢占)期间没来得及入账的销售额。旧流程:收银员 A 在 B 被叫去开会时直接按当时账面流水记了一笔,老板回来才发现那一小时根本没卖东西。新流程:
- 老板出门前先把手头的销售小票贴进账本(KVM 侧 patch 1:清 preempted 前先写 stealtime);
- 收银员 A 看到 B 被叫走,就先把这段时间的入账放进抽屉
deferred_clock_task,等 B 真正回来再一起补登(patch 2)。
这样无论谁先看到 B 恢复上班,账面和实际销售额一定对得上。
ASCII 流程图
Old flow (guest) KVM host vCPU B state
--------------- -------- ------------
vCPU A: update_rq_clock_task(B)
delta += IRQ_delta
delta += steal_delta <-- steal value still pre-preempt
rq_B.clock_task += delta <-- host-stolen time charged as runtime
... after 10s ...
record_steal_time(B)
write stealtime
preempted = 0
B is runnable again
New flow (guest + host coordinated)
--------------- -------- ------------
vCPU A: update_rq_clock_task(B)
vcpu_is_preempted(B) == 1
rq_B.deferred_clock_task += delta <-- stash
return
B about to enter guest:
record_steal_time(B)
write stealtime
smp_wmb()
preempted = 0 <-- patch 1: publish first, clear later
B is runnable again
vCPU A: update_rq_clock_task(B)
vcpu_is_preempted(B) == 0
delta += rq_B.deferred_clock_task <-- fold back in
rq_B.deferred_clock_task = 0
rq_B.clock_task += delta + steal_delta
IRQ + steal deducted together
Highlight:风险与注意点
- 必须 host + guest 同时打补丁:patch 1 单独落地无效(guest 仍会读到旧 preempted=1 而推迟),patch 2 单独落地也只能延后记账而看不到新鲜 stealtime。任一侧漏改,问题依旧。
static_key_false(¶virt_steal_rq_enabled)守卫:避免在非 paravirt 或未启用CONFIG_PARAVIRT_TIME_ACCOUNTING时多走分支;__maybe_unused steal注释也暗示编译器在关闭配置时忽略 steal 累计路径。- 配对的 memory barrier:patch 1 在两个版本号切换之间各放一次
smp_wmb(),对应 guestkvm_steal_clock()里的virt_rmb()。少一道 wmb 都会让远端 vCPU 看到新 preempted 但旧 stealtime 的窗口,重新踩坑。 vcpu_is_preempted()语义:guest 用的是 arch 提供的 pv 辅助位(例如 x86 的vcpu_info.preempted),其可见性由 patch 1 的smp_wmb()保证;这层合约写进了 patch 2 的注释里。runtime_stall_detector_v2.py仅作复现:脚本里的gap > 30才退出,复现场景里 worker 5 报告约 9.99s、worker 3 报告约 49.7s,差距 40s 即额外惩罚量级,可作为后续回归用例的判据。
版本变化
当前为 RFC v1,仅一个版本,无 vN->vN+1 可比较。
一句话总结
把 KVM host 发布 stealtime 的时机从 vCPU 进 guest 前提前到清 preempted 前,并在 guest update_rq_clock_task() 端把被抢占期间的远端 clock_task delta 暂存到 rq->deferred_clock_task,等远端 vCPU 不再被抢占时再一并扣减,从而让 guest 调度器在 KVM 下也能正确把 host 偷走的时间从 task runtime 里剥掉。