0%

riscv kvm timer bug 问题分析

问题背景

在使用最新版本 qemu (8.0) 搭配 linux kernel (5.19.16) 模拟出的 riscv 主机上, 使用 riscv 版本的 qemu 跑 kvm-mode 的 guest kernel 无法执行.
现象为没有 host kernel 的 log, 没有 guest kernel 的 log 输出.

问题初步分析

在 kvm_riscv_vcpu_enter_exit 处下断点看下 vcpu 的调度情况
发现 vcpu 调度是正常的

但在 kvm_riscv_vcpu_exit 处打 log 发现每次 guest 退出的原因都是因为
sbi 设置 timecmp

通过 qemu 的 command dump-guest-memory 命令导出 guest 的 coredump
guest 停在了通过 sbi 设置 timecmp 的地方.

根据这个现象, guest kernel 一直在处理 timer tick, 却不能走其他的流程, 对比正常的 log, 启动过程中从 guest 退出到 kvm 的原因是多种多样的, 所以可以首先怀疑是 guest timer tick 出了问题.

对比最新版本 qemu (8.0) 搭配 linux kernel (主线 6.x) 版本, 可以正常运行 guest.
对比 linux kernel kvm 中对 guest timer tick 的处理, 发现加入了 sstc 的 feature.

初步验证

sstc 可以作为第一个疑点, 需要首先排除下是否是这个 feature 导致的.

在 host qemu (8.0) 中关掉 sstc 的 feature, 即关掉模拟 riscv 主机的 sstc 的 feature, 再次运行 guest, 发现 guest 正常运行了.

调研 sstc 的 SPEC 文档, sstc 引入了 vstimecmp 寄存器, guest 可以通过直接设置 vstimecmp 来避免陷入 kvm 来处理 timer tick.

但调研 SPEC 结束后, 发现不能解释为什么 old kernel (5.19.16) kvm 上不使用 vstimecmp 应该是没问题的, guest 通过 sbi 设置 timecmp 是走了通过 host kvm 来处理 guest timer tick 的逻辑.

guest os 设置 compare 发 SBI_EXT_0_1_SET_TIMER 的请求
导致陷入到 kvm 中, kvm 接收的请求中第一个参数 next_cycle 即为下一轮的 compare 数.

在 guest 因设置 compare 请求导致陷入到 kvm 中, kvm 处理 SBI_EXT_0_1_SET_TIMER:

  • 首先清除了 IRQ_VS_TIMER 中断, 确保本轮 tick 中不再有有的 timer 中断产生
  • 计算下一轮到期的时间 delta_ns (cycle 转换为 host 的纳秒单位)
  • 启动定时器, 到期时间为当前时间+delta_ns
  • 退出 kvm, 重返到 vcpu guest

定时器到期时, 触发中断重新陷入到 kvm 中, 处理函数为 kvm_riscv_vcpu_hrtimer_expired
, 通过 hvip 注入 IRQ_VS_TIMER 中断, 让 guest 处理 timer tick, 走到对应的 tick 处理函数, 紧接着通过 sbi SBI_EXT_0_1_SET_TIMER 设置下一轮 compare.

这套机制的问题只是效率慢 (一次 timer tick 的处理需要两次陷入 kvm), 但机制是没问题的.

再次深入调研 sstc 的 SPEC, 注意到这个描述:

The interrupt remains posted until vstimecmp becomes
Greater than (time + htimedelta) - typically as a result of writing vstimecmp

会不会是 vstimecmp 没处理是默认的 0, 所以 compare 总是比 time 小, 导致一直触发 VSTIP 的中断, guest 一直在处理 VSTIP 中断, 不能让出 cpu 来处理其他的流程.

想到这一点, 可以简单的验证下:

kvm_arch_vcpu_load 中设置 vstimecmp 为最大值
csr_write(0x24D, -1UL);

修改后验证 qemu (8.0)+old kernel (5.19.17) 模拟的 riscv 主机上跑 guest, guest 正常运行了, 证明正是这个问题导致的.

梳理 guest timer 的流程

在找到 root cause 后, 有时间需要对 guest riscv timer tick 的流程做一下梳理.
这里直接拿最新的 linux kernel 版本进行分析.

首先整理 sstc 的 feature.

guest 在满足上述 [m/h]envcfg.STCE[m/h]counteren.TM 后可以直接操作 stimecmp csr 来设置 timer tick, 就不必再通过 sbi 陷入到 kvm 中设置 compare, 也不需要 kvm 维护定时器, 同时也不需要 kvm 注入 timer 虚拟中断给 vcpu, 减少了两次陷入 kvm, 大大提升了 guest 处理 tick 的效率.
Tick 到期后, 由硬件来保证 vcpu 直接收到 timer 的虚拟中断, vcpu 不用陷入到 kvm 就可以处理一轮轮的 tick.

为什么上述问题中, guest os 却是设置 compare 发 SBI_EXT_0_1_SET_TIMER 的请求
这是因为这个问题中, guest kernel 用的 old kernel (5.19.17), 并未合入 sstc feature 的相关修改.

可以直接看下 guest kernel 处理 timer tick 的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
start_kernel
+-> time_init
+-> timer_probe
+-> cpuhp_setup_state
+-> __cpuhp_setup_state
+-> __cpuhp_setup_state_cpuslocked
+-> cpuhp_issue_call
+-> cpuhp_invoke_callback
+-> riscv_timer_starting_cpu
+-> clockevents_config_and_register
+-> clockevents_register_device
+-> tick_check_new_device
+-> tick_setup_device
+-> tick_setup_periodic
/* event_handler回调 */
+-> tick_handle_periodic
+-> clockevents_program_event
+-> dev->set_next_event
+-> riscv_clock_next_event

old kernel (5.19.17)

1
2
3
4
5
6
7
static int riscv_clock_next_event(unsigned long delta,
struct clock_event_device *ce)
{
csr_set(CSR_IE, IE_TIE);
sbi_set_timer(get_cycles64() + delta);
return 0;
}

而最新主线上的 kernel (6.x)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
static int riscv_clock_next_event(unsigned long delta,
struct clock_event_device *ce)
{
u64 next_tval = get_cycles64() + delta;

csr_set(CSR_IE, IE_TIE);
if (static_branch_likely(&riscv_sstc_available)) {
...
csr_write(CSR_STIMECMP, next_tval);
} else
sbi_set_timer(next_tval);

return 0;
}

guest kernel 未合入 sstc feature 修改前, guest 只能通过 sbi SBI_EXT_0_1_SET_TIMER 设置 compare.
需要陷入到 kvm 中设置 compare.

guest kernel 合入了 sstc feature 后, guest kernel 可以检测主机是否支持 sstc,

  • 如支持, guest 通过 stimecmp (vstimecmp) 来设置 compare, 最大化的提高 timer tick 的处理效率.
  • 不支持, guest 仍通过 sbi SBI_EXT_0_1_SET_TIMER 设置 compare.

而最新的 linux kvm 版本合入了 sstc 的 feature, 则需要考虑 guest kernel 的兼容性, 同时兼容老的 kernel (不支持 sstc) 和新的 kernel(支持 sstc)

来看下对应的处理逻辑:

  • 模拟的 riscv 主机支持 sstc 时:

    1. 对于支持 sstc 的新的 guest kernel, 在 guest 运行后, 就不用管 guest 的 timer tick 了, 但是 guest 退出运行后, 处于 block 时, 仍然需要关注 guest 的 timer tick.
    2. 对于不支持 sstc 的旧的 guest kernel, 需要处理 guest 发过来的 sbi SBI_EXT_0_1_SET_TIMER 消息. 但 guest 运行时, 也不需要通过主机定时器来处理 guest 的 timer tick. 因为 host 是支持 sstc 的, 只是 guest kernel 未检测 sstc 而已. 只要 kvm 在收到 sbi SBI_EXT_0_1_SET_TIMER 后直接设置 vstimecmp 就可以了, 这样时间到了后, 硬件会设置 VSTIP, guest 就可以处理 timer tick 中断走到 tick 处理函数了.
  • 模拟的 riscv 主机不支持 sstc 时:

    1. 退回到通过 host kvm 设置定时器来处理 guest 的 timer tick.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int kvm_riscv_vcpu_timer_init(struct kvm_vcpu *vcpu)
{
struct kvm_vcpu_timer *t = &vcpu->arch.timer;

if (t->init_done)
return -EINVAL;

hrtimer_init(&t->hrt, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
t->init_done = true;
t->next_set = false;

/* Enable sstc for every vcpu if available in hardware */
if (riscv_isa_extension_available(NULL, SSTC)) {
t->sstc_enabled = true;
t->hrt.function = kvm_riscv_vcpu_vstimer_expired;
t->timer_next_event = kvm_riscv_vcpu_update_vstimecmp;
} else {
t->sstc_enabled = false;
t->hrt.function = kvm_riscv_vcpu_hrtimer_expired;
t->timer_next_event = kvm_riscv_vcpu_update_hrtimer;
}

return 0;
}

riscv 主机支持 sstc

看下 riscv 主机支持 sstc 的策略:
t->timer_next_event = kvm_riscv_vcpu_update_vstimecmp;

该函数是在处理 guest 发来的 sbi SBI_EXT_0_1_SET_TIMER 的处理函数

1
2
3
4
5
6
7
-+ kvm_sbi_ext_v01_handler
\ -+ case SBI_EXT_0_1_SET_TIMER:
\ - next_cycle = (u64)cp->a0;
| -+ kvm_riscv_vcpu_timer_next_event(vcpu, next_cycle);
\ -+ t->timer_next_event(vcpu, ncycles);
\ -+ kvm_riscv_vcpu_update_vstimecmp()
\ - csr_write(CSR_VSTIMECMP, ncycles);

对应支持 sstc 模拟 riscv 主机的第二种情况.
而第一种情况 guest 运行中 guest kernel 自己设置 vstimecmp, 不需要陷入 kvm, 所以 kvm 也不用处理.

再看下
t->hrt.function = kvm_riscv_vcpu_vstimer_expired;

这个是 host kvm 定时器的超期函数, 这个函数是为了处理 guest kernel 因 block 得不到执行时的定时器处理函数, 该定时器是类似于 doorbell 机制, 在 guest kernel 未能调度时, 需要 kvm 设置定时器处理 guest 的 tick. 保证 vcpu 可以有调度的机会.

1
2
3
4
5
6
7
8
9
10
11
12
13
"退出ioctl(KVM_RUN)的小循环时, 调用了 vcpu_put 释放vcpu资源"
-+ kvm_arch_vcpu_ioctl_run(vcpu)
\ -+ vcpu_put(vcpu)
\ -+ kvm_arch_vcpu_put(vcpu);
\ -+ kvm_riscv_vcpu_timer_save(vcpu);
\ -|+ if kvm_vcpu_is_blocking(vcpu)
\ -+ kvm_riscv_vcpu_timer_blocking(vcpu);
"启动定时器"
\ - hrtimer_start(&t->hrt, ktime_set(0, delta_ns), HRTIMER_MODE_REL);
"定时器到时处理函数"
-+ kvm_riscv_vcpu_vstimer_expired(struct hrtimer *h)
\ - kvm_vcpu_kick(vcpu); "唤醒vcpu"
| - return HRTIMER_NORESTART; "不重启定时器"

riscv 主机不支持 sstc

回退 kvm 主机为 guest timer tick 设置定时器
t->timer_next_event = kvm_riscv_vcpu_update_hrtimer;

在 guest 因设置 compare 请求导致陷入到 kvm 中, kvm 处理 SBI_EXT_0_1_SET_TIMER:
kvm_riscv_vcpu_update_hrtimer

  • 首先清除了 IRQ_VS_TIMER 中断, 确保本轮 tick 中不再有新的 timer 中断产生
  • 计算下一轮到期的时间 delta_ns (cycle 转换为 host 的纳秒单位)
  • 启动定时器, 到期时间为当前时间+delta_ns
  • 退出 kvm, 重返到 vcpu guest

定时器到期时, 触发中断重新陷入到 kvm 中, 处理函数为 kvm_riscv_vcpu_hrtimer_expired
, 通过 hvip 注入 IRQ_VS_TIMER 中断, 让 guest 处理 timer tick, 走到对应的 tick 处理函数, 紧接着通过 sbi SBI_EXT_0_1_SET_TIMER 设置下一轮 compare.