问题:
在 guest shell 中发起 reboot, guest os 完成关机后挂死.
guest os 无 log 输出, kvm 无异常 log.
1 2 3 4 5 6 7 8 9
| The system is going down NOW! Sent SIGTERM to all processes logout Sent SIGKILL to all processes Requesting system reboot
|
从现象上看, 首先需要查 vcpu 在 kvm 中的调度是否正常.
需要的关键信息即 vcpu 的信息.
Guest 运行情况
通过 vcpu->arch. Guest_csr 和 vcpu->arch. Guest_context 可以得到 guest 陷入到 kvm 前的 csr 和通用寄存器状态.
1 2 3 4 5 6 7 8 9 10 11 12
| guest_csr = { vsstatus = 0x200000020, vsie = 0x222, vstvec = 0xffffffff800031f4, vsscratch = 0x0, vsepc = 0xffffffff805b107a, vscause = 0x8000000000000005, vstval = 0x0, hvip = 0x0, vsatp = 0xa000100000082924, scounteren = 0x7 },
|
如果 host kernel 和 guest kernel 使用的同一份 Image, 其虚拟地址映射的符号应该是一致的, 因为 kernel va 的映射起始地址是一样的.
如果 host kernel 和 guest kernel 使用的 Image 不同, 可以查看 qemu 映射的 memory-layout
Qemu 命令行 ctrl+a+c, 切换到 qemu 命令行
1 2
| # info mtree 0000000080000000-0000000087ffffff (prio 0, ram): riscv_virt_board.ram "GPA 地址"
|
Kernel 的 log
1 2 3 4 5 6 7 8
| [ 0.000000] Virtual kernel memory layout: [ 0.000000] fixmap : 0xff1bfffffea00000 - 0xff1bffffff000000 (6144 kB) [ 0.000000] pci io : 0xff1bffffff000000 - 0xff1c000000000000 ( 16 MB) [ 0.000000] vmemmap : 0xff1c000000000000 - 0xff20000000000000 (1024 TB) [ 0.000000] vmalloc : 0xff20000000000000 - 0xff60000000000000 (16384 TB) [ 0.000000] modules : 0xffffffff019c7000 - 0xffffffff80000000 (2022 MB) [ 0.000000] lowmem : 0xff60000000000000 - 0xff60000008000000 ( 128 MB) [ 0.000000] kernel : 0xffffffff80000000 - 0xffffffffffffffff (2047 MB)
|
对照 kernel 编译的 System. Map 即可找到 gva 对应的函数符号
qemu-kvm 框架下并不支持 kernel 的调试, 可以先通过 dump 手段将 guest kernel 的运行的 coredump 导出.
Qemu 生成 coredump
通过 qemu 命令行生成 guest kernel 的 coredump.
Ctrl+a+c 打开 qemu 命令行
1 2
| # 生成coredump文件为 core.file dump-guest-memory core.file
|
Gdb 调试 coredump
1
| riscv64-unknown-linux-gnu-gdb -ex "core guest.core" -ex "file virt_build/vmlinux"
|
从 coredump 后发现, pc 还停在 0x80000000
处, 怀疑是 reboot 后还没有 poweron.
另外 guest_csr 的 vsepc 与该处的 pc 值不同也说明了这一点.
需要分析代码流程及 vcpu task 挂死后 vcpu 数据的分析
为了方便后面分析, 需要先梳理下怎么在 kernel 的运行信息中找到 vcpu 的数据, 包括在线场景和离线场景.
找到 vcpu 数据
从 qemu 的 info cpus 找到其 vcpu 绑定的线程 id
1 2
| (qemu) info cpus * CPU #0: thread_id=142
|
从 Per-cpu 变量 kvm_running_vcpu 找到 vcpu
在 kvm 中, 当前 cpu 的运行的 vcpu 数据被写到了 Per-cpu 变量 kvm_running_vcpu 中.
host 中找到 kvm_running_vcpu 的地址 (全局变量, 其他 cpu 上都是一样的)
1 2
| p/x &kvm_running_vcpu $1 = 0xffffffff80c19390
|
host 中找到 __per_cpu_offset
1 2
| p/x __per_cpu_offset $2 = {[0x0] = 0xff6000013e397000, [0x1] = 0xff6000013e3aa000, [0x2] = 0x0 <repeats 62 times>}
|
找到 cpu 0 上的kvm_running_vcpu 的 vcpu 数据
1 2 3 4
| c 0xffffffff80c19390+0xff6000013e397000 = 0xff600000befb0390 x/a 0xff600000befb0390 >>> x/a 0xff600000befb0390 0xff600000befb0390: 0x0
|
同理找到 cpu 1 上的 kvm_running_vcpu 的 vcpu
1 2 3
| c 0xffffffff80c19390+0xff6000013e3aa000 = 0xff600000befc3390 >>> x/a 0xff600000befc3390 0xff600000befc3390: 0xff60000080120000
|
可知 vcpu 运行在 cpu 1 上.
查看 vcpu 的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| >>> p (*(struct kvm_vcpu*)0xff60000080120000)->stats_id $7 = "kvm-142/vcpu-0", '\000' <repeats 33 times> "stats_id 与 info cpus 查看的信息一致"
>>> p (*(struct kvm_vcpu*)0xff60000080120000)->arch "查看arch riscv 数据" power_off = true, pause = false,
>>> p *((*(struct kvm_vcpu*)0xff60000080120000)->wait->task) "vcpu->wait->task vcpu 所在的task" __state = 0x1, "TASK_INTERRUPTIBLE" on_cpu = 1, prio = 120, pid = 142
>>> p ((*(struct kvm_vcpu*)0xff60000080120000)->wait->task->pid) pid = 142
>>> p ((*(struct kvm_vcpu*)0xff60000080120000)->wait->task->thread) thread = { ra = 0xffffffff8084d530, sp = 0xff200000008d3c90, ... }
|
从 file 中导出 vcpu 信息
在 vcpu task 未调度时, kvm_running_vcpu 并不会保存 vcpu 的地址信息.
在这种情况下, 还需要从其他数据结构中导出 vcpu 的地址.
Kernel 未保存全局的 kvm 变量, 也未保存全局的 vcpu 的信息, 但是 qemu 进程保存了打开的文件的信息.
从之前调研 kvm guest vm 及 vcpu 的创建及开机过程, 可知 qemu 打开了两个匿名文件 kvm-vm
保存了 kvm 的信息, 打开了 kvm-vcpu-x
保存了 vcpu 的信息, 只要找到 qemu 进程的 task 信息即可顺藤摸瓜找到 vm 及 vcpu 的数据.
通过 qemu 进程打开的 kvm-vm
匿名文件和 kvm-vcpu:0
vcpu 的匿名文件反向追踪到 file 结构体
ps
或 top 查询到 qemu 的主线程
1 2 3 4 5 6 7 8
| ls -l /proc/131/fd lrwx------ 1 root root 64 Jun 1 02:49 0 -> /dev/pts/0 lrwx------ 1 root root 64 Jun 1 02:49 1 -> /dev/pts/0 lrwx------ 1 root root 64 Jun 1 02:49 10 -> /dev/kvm lrwx------ 1 root root 64 Jun 1 02:49 11 -> anon_inode:kvm-vm lrwx------ 1 root root 64 Jun 1 02:49 12 -> anon_inode:kvm-vcpu:0 lrwx------ 1 root root 64 Jun 1 02:49 13 -> /root/rootfs.ext2 lrwx------ 1 root root 64 Jun 1 02:49 14 -> anon_inode:[eventfd]
|
可以看到 11 号为 kvm-vm
的 fd. 12 号为 kvm-vcpu:0
的 fd
在离线场景中, 或客户提供的 log 中, 查看该信息, 找到 kvm 的 fd. 找到 vcpu 的 fd.
找到 qemu 对应的 task 结构体
通过 Kernel gdb-script 找到 vm
Make scripts_gdb
Gdb 调试时加入
add-auto-load-safe-path </path/to/linux-build>
source </path/to/linux-build>/vmlinux-gdb.py
在 kvm_running_vcpu
per-cpu 变量为空时, 即所有 vcpu 未运行时, 可以通过 vmlinux-gdb.py
提供的 lx_task_by_pid
函数找到对应的 task 数据
1 2 3
| >>> apropos lx "help信息" >>> lx-ps "打印所有的task, 找到qemu 主进程 pid" >>> p $lx_task_by_pid(140) "打印 qemu 主线程task"
|
lx-ps
是通过 init_task.tasks.next
链表找出的所有的 task 信息, next 为 list_head tasks 成员变量, 由 container_of 通过 list_head 类型的 tasks 变量地址找到对应的 task 结构体, list_head 成员变量在 task 结构体的偏移可以通过 init_task. Tasks 的地址减去 init_task 的地址得到.
1 2 3 4 5 6 7 8 9 10 11 12
| # 可以通过如下的方法索引出所有的task >>> p &init_task.tasks $5 = (struct list_head *) 0xffffffff8140de28 <init_task+872> >>> p &init_task $6 = (struct task_struct *) 0xffffffff8140dac0 <init_task> >>> p (*(struct task_struct*)(0xff60000080098368-872))->tasks $8 = { next = 0xff60000080098f28, prev = 0xffffffff8140de28 <init_task+872> } >>> p (*(struct task_struct*)(0xff60000080098f28-872))->tasks ...
|
根据 qemu 主线程 task 的 files 找到对应的打开的 “kvm-vm” 的 file 结构体
1
| >>> p $lx_task_by_pid(140).files.fdtab.fd
|
通过 f_op
确定是 “/dev/kvm”的 file 结构体, f_op 为 kvm_vm_fops
1 2
| >>> p (*(struct file*)($lx_task_by_pid(140).files.fdtab.fd[11])).f_op $76 = (const file_operations *) 0xffffffff80e02708 <kvm_vm_fops>
|
确定后, file->private_data
即为 kvm 的地址
1 2
| >>> p (*(struct file*)($lx_task_by_pid(140).files.fdtab.fd[11])).private_data $77 = (void *) 0xff60000080fde000
|
Kernel 中通过 kvm 遍历 vcpu
1 2 3 4
| #define kvm_for_each_vcpu(idx, vcpup, kvm) \ xa_for_each_range(&kvm->vcpu_array, idx, vcpup, 0, \ (atomic_read(&kvm->online_vcpus) - 1)) kvm_for_each_vcpu(i, vcpu, kvm)
|
对应 gdb 中通过 kvm 遍历 vcpu 需要看下 kernel 中相关的数据结构
通过 kvm->vcpu_array->xa_head 找出 vcpu 的数组的 xa_node, 再通过 node->slots[offset]
根据 vcpu 的 offset 找到对应的 vcpu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
static inline struct xa_node *xa_to_node(const void *entry) { return (struct xa_node *)((unsigned long)entry - 2); }
>>> p (*(struct kvm*) 0xff60000080fde000)->vcpu_array->xa_head "定位 kvm->vcpu_array->xa_head" $80 = (void *) 0xff60000080120000 "找到所有的 vcpu的地址, 如果只有一个vcpu, 则该 slots 则没有有效项, 只有xa_head 是唯一的vcpu 地址" >>> p (*(struct xa_node*)(0xff60000080120000-2))->slots $7 = {[0] = 0, [1] = 0, [2] = 0 ...} "多个vcpu的情况" >>> p (*(struct xa_node*)(0xff600000951d348a-2))->slots $7 = {[0] = 0xff60000081368000, [1] = 0xff6000008136a8e0, [2] = 0xff6000008136d1c0, [3] = 0x0 <repeats 61 times>}
>>> p (*(struct kvm_vcpu*)0xff60000081858000)->vcpu_id >>> p (*(struct kvm_vcpu*)0xff6000008185a8e0)->stats_id "通过stats_id 可以找到对应的vcpu 所在的线程id, 及vcpu_fd 的name" $19 = "kvm-132/vcpu-1", '\000' <repeats 33 times>
|
当然也可直接通过 kvm-vcpu-x
的匿名文件的 fd 找到 vcpu
1 2 3
| >>> p (*(struct file*)($lx_task_by_pid(140).files.fdtab.fd[12])).f_op $76 = (const file_operations *) <addr> <kvm_vcpu_fops> "通过 kvm_vcpu_fops 证明是vcpufd" >>> p (*(struct file*)($lx_task_by_pid(140).files.fdtab.fd[12])).private_data "private_data 即保存的vcpu的地址"
|
vcpu task 挂死堆栈分析
从 vcpu->wait->task->thread 的 sp ra 经过计算后可以推出该 task 切换出去前的堆栈
1 2 3 4 5 6
| >>> p ((*(struct kvm_vcpu*)0xff60000080120000)->wait->task->thread) thread = { ra = 0xffffffff8084d530, sp = 0xff200000008d3c90, ... }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| >>> disassemble 0xffffffff8084d530 Dump of assembler code for function __schedule: 0xffffffff8084d292 <+0>: addi sp,sp,-128 0xffffffff8084d294 <+2>: sd s0,112(sp) 0xffffffff8084d296 <+4>: sd s3,88(sp) 0xffffffff8084d298 <+6>: sd s5,72(sp) 0xffffffff8084d29a <+8>: sd s6,64(sp) 0xffffffff8084d29c <+10>: sd ra,120(sp) "上一个 ra 存在 sp+120的地方"
>>> x/a 0xff200000008d3c90+120 "sp+120" 0xff200000008d3d08: 0xffffffff8084d9e8 <schedule+72> >>> disass 0xffffffff8084d9e8 0xffffffff8084d9a0 <+0>: addi sp,sp,-32 0xffffffff8084d9a2 <+2>: sd s0,16(sp) 0xffffffff8084d9a4 <+4>: sd s1,8(sp) 0xffffffff8084d9a6 <+6>: sd ra,24(sp) >>> x/a 0xff200000008d3c90+128+24 "sp+128+24" 0xff200000008d3d28: 0xffffffff80017468 <kvm_arch_vcpu_ioctl_run+888> >>> x/16i 0xffffffff80017468-16 x/16i 0xffffffff80017468-16 0xffffffff80017458 <kvm_arch_vcpu_ioctl_run+872>: ld a5,0(a4) 0xffffffff8001745a <kvm_arch_vcpu_ioctl_run+874>: srli a5,a5,0x2 0xffffffff8001745c <kvm_arch_vcpu_ioctl_run+876>: andi a5,a5,1 0xffffffff8001745e <kvm_arch_vcpu_ioctl_run+878>: bnez a5,0xffffffff80017394 <kvm_arch_vcpu_ioctl_run+676> 0xffffffff80017460 <kvm_arch_vcpu_ioctl_run+880>: auipc ra,0x836 0xffffffff80017464 <kvm_arch_vcpu_ioctl_run+884>: jalr 1344(ra) -> 0xffffffff80017468 <kvm_arch_vcpu_ioctl_run+888>: auipc a3,0x0 0xffffffff8001746c <kvm_arch_vcpu_ioctl_run+892>: addi a3,a3,-252 0xffffffff80017470 <kvm_arch_vcpu_ioctl_run+896>: j 0xffffffff8001736c <kvm_arch_vcpu_ioctl_run+636> 0xffffffff80017472 <kvm_arch_vcpu_ioctl_run+898>: ebreak 0xffffffff80017474 <kvm_arch_vcpu_ioctl_run+900>: j 0xffffffff800171be <kvm_arch_vcpu_ioctl_run+206> 0xffffffff80017476 <kvm_arch_vcpu_ioctl_run+902>: fence w,w 0xffffffff8001747a <kvm_arch_vcpu_ioctl_run+906>: li a5,256 0xffffffff8001747e <kvm_arch_vcpu_ioctl_run+910>: addi a4,s1,56 0xffffffff80017482 <kvm_arch_vcpu_ioctl_run+914>: amoor.d zero,a5,(a4) 0xffffffff80017486 <kvm_arch_vcpu_ioctl_run+918>: j 0xffffffff800173b0 <kvm_arch_vcpu_ioctl_run+704>
|
为了比较容易反推堆栈的代码, 可以通过 objdump 打印比较完整的反汇编代码的所在行数.
1
| riscv64-unknown-linux-gnu-objdump -DSrtlFCz vmlinux > vmlinux.full.S
|
因为 dump 出的反汇编文件非常大, 打开整个文件会很卡, 可以借助 grep <addr> -B/-A/-C <行数>
指定搜索的前后文信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| ❯ grep "ffffffff80017468" vmlinux.full.S -C 12 ffffffff80017458: 631c ld a5,0(a4) ffffffff8001745a: 8389 srli a5,a5,0x2 signal_pending_state(): /home/liguang/program/riscv-lab/linux/virt_build/../include/linux/sched/signal.h:418 if (!signal_pending(p)) ffffffff8001745c: 8b85 andi a5,a5,1 ffffffff8001745e: fb9d bnez a5,ffffffff80017394 <kvm_arch_vcpu_ioctl_run+0x2a4> (File Offset: 0x18394) kvm_riscv_check_vcpu_requests(): /home/liguang/program/riscv-lab/linux/virt_build/../arch/riscv/kvm/vcpu.c:1043 rcuwait_wait_event(wait, ffffffff80017460: 00836097 auipc ra,0x836 ffffffff80017464: 540080e7 jalr 1344(ra) # ffffffff8084d9a0 <schedule> (File Offset: 0x84e9a0) -> ffffffff80017468: 00000697 auipc a3,0x0 ffffffff8001746c: f0468693 addi a3,a3,-252 # ffffffff8001736c <kvm_arch_vcpu_ioctl_run+0x27c> (File Offset: 0x1836c) ffffffff80017470: bdf5 j ffffffff8001736c <kvm_arch_vcpu_ioctl_run+0x27c> (File Offset: 0x1836c) srcu_read_unlock(): /home/liguang/program/riscv-lab/linux/virt_build/../include/linux/srcu.h:285 WARN_ON_ONCE(idx & ~0x1); ffffffff80017472: 9002 ebreak ffffffff80017474: b3a9 j ffffffff800171be <kvm_arch_vcpu_ioctl_run+0xce> (File Offset: 0x181be) __kvm_make_request(): /home/liguang/program/riscv-lab/linux/virt_build/../include/linux/kvm_host.h:2041 smp_wmb(); ffffffff80017476: 0110000f fence w,w set_bit():
|
最终定位到 vcpu.c:1043 行, 由于默认是 -Os
编译, 优化后有些函数调用关系缺失了, 但大概能推出对应的堆栈
1 2 3 4
| ----> schedule(); "切到了其他进程" ---> rcuwait_wait_event(wait,(!vcpu->arch.power_off) && (!vcpu->arch.pause), TASK_INTERRUPTIBLE); "调用了 set_current_state(TASK_INTERRUPTIBLE);" --> kvm_riscv_check_vcpu_requests(vcpu); -> kvm_arch_vcpu_ioctl_run()
|
最终落到了下面的 for 循环中. 正常情况下是监听的 condition 状态, condition 是 vcpu->arch.power_off
和 vcpu->arch.pause
同时为 false 时, 该 for 循环才会退出.
当前 vcpu->arch.power_off=true
是不正常的, 也就是 guest 没有触发过 vcpu 的 poweron, 导致 vcpu 一直处在 kvm_riscv_check_vcpu_requests
的 rcu_wait_event 的 for 循环中.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #define rcuwait_wait_event(w, condition, state) \ ({ \ int __ret = 0; \ prepare_to_rcuwait(w); \ for (;;) { \ set_current_state(state); \ if (condition) \ break; \ \ if (signal_pending_state(state, current)) { \ __ret = -EINTR; \ break; \ } \ \ => schedule(); \ } \ finish_rcuwait(w); \ __ret; \ })
|
查询 vcpu->arch.power_off
的置位情况, 只有 kvm_riscv_vcpu_power_on
函数将其置为了 true.
代码逻辑应该是有问题的, guest 重启时发送 sbi 的 SBI_SRST_RESET_TYPE_COLD_REBOOT 陷入 kvm, kvm 处理该 sbi 消息, 将所有 vcpu->arch.power_off 置为 on.
并发送了 KVM_REQ_SLEEP
请求, 在该状态检查时, vcpu 是 power_off 状态, 导致一直陷入到 kvm_riscv_check_vcpu_requests
rcu_wait_event(!vcpu->arch.power_off) && (!vcpu->arch.pause))
的 for 循环中, guest 得不到执行, 而 kvm_riscv_vcpu_power_on 这个函数又是 guest sbi ecall 才会调用的函数, 所以 vcpu->arch.power_off
状态没有机会变为 false.
1 2 3 4 5 6 7 8 9
| >>> p &vcpu->arch.power_off $2 = (bool *) 0xff60000002f4bfc0
>>> p vcpu->wait->task->pid $8 = 161 "reboot 后 vcpu 被复用" vcpu=0xff60000002f4a8e0 p &vcpu->arch.power_off $3 = (bool *) 0xff60000002f4bfc0
|
解决方案
在 qemu 中添加 vcpu power_on 的逻辑, qemu 完成 device reset 后紧接着调用 vcpu 的 power_on 使 vcpu->arch.power_off 回到 false 状态.
添加上述逻辑后遇到的问题:
vcpu crash 问题分析解决
先看下 vcpu crash 的问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| [ 53.061320] kvm [129]: fault_addr 0xffffffff80003594 memslot 0x0 stval 0xffffffff80003594 [ 53.061660] kvm [129]: VCPU exit error -95 [ 53.062231] kvm [129]: SEPC=0xffffffff80003594 SSTATUS=0x200004120 HSTATUS=0x2002001c0 [ 53.066559] kvm [129]: SCAUSE=0x14 STVAL=0xffffffff80003594 HTVAL=0x3fffffffe0000d65 HTINST=0x0 error: kvm run failed Operation not supported pc ffffffff80003594 mhartid 0000000000000000 mstatus 0000000200000120 mip 0000000000000000 mie 0000000000000222 mideleg 0000000000000000 medeleg 0000000000000000 mtvec 0000000000000000 mepc 0000000000000000 mcause 0000000000000000 mtval 0000000000000000 mscratch 0000000000000000 x0/zero 0000000000000000 x1/ra ffffffff8003f2a6 x2/sp ff200000007cbcd0 x3/gp ffffffff814fd4e8 x4/tp ff6000000243de00 x5/t0 ffffffff80843aa8 x6/t1 0000000000000001 x7/t2 52203a746f6f6265 x8/s0 ff200000007cbce0 x9/s1 ff600000021bcad8 x10/a0 0000000000000000 x11/a1 0000000087000000 x12/a2 0000000000000000 x13/a3 0000000000000000 x14/a4 0000000000000000 x15/a5 0000000000000000 x16/a6 0000000000000000 x17/a7 0000000053525354 x18/s2 ffffffffffffffff x19/s3 0000000000000000 x20/s4 0000000000000000 x21/s5 0000000000008000 x22/s6 0000000000000019 x23/s7 0000000000000000 x24/s8 0000000000000000 x25/s9 0000000000000000 x26/s10 0000000000000000 x27/s11 0000000000000000 x28/t3 ff60000001818f00 x29/t4 ff60000001818f00 x30/t5 ff60000001818000 x31/t6 ff200000007cbb58
|
vcpu 出现 guest insn exception (20 0x14), crash 的 fault_addr 为 0xffffffff80003594
首先这个 fault_addr 是不正常的, 这个地址直接就是 gva, 并没有转换为 gpa, 所以在 kvm 中没有在 memslot 找到对应的 hva.
gva 为什么没有转换为 gpa, 而 gva 的符号地址落在了 guest kernel 的 handle_exception
处, 原因只能是 vstap
被清 0 了.
第一点怀疑 vstap 被 kvm 错误置位了, 找出所有 vstap 可能在 kvm 中被改写的地方
从 vcpu 的调用中追踪 vstap 的改写逻辑发现都是正常的, 而且即使把 vsatp 的改写逻辑去掉, 依然会触发其他异常.
排除该点怀疑.
第二点怀疑 reset 后 guest_context guest_csr 状态不对, 在 vcpu reset 后第一次进入 guest 地方打点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66
| p/x (*(struct kvm_vcpu*)0xff60000002cd0000).arch.guest_context $25 = { zero = 0x0, ra = 0xffffffff8003f2a6, sp = 0xff200000007cbcd0, gp = 0xffffffff814fd4e8, tp = 0xff60000002424680, t0 = 0xffffffff80843aa8, t1 = 0x1, t2 = 0x52203a746f6f6265, s0 = 0xff200000007cbce0, s1 = 0xff600000021bcad8, a0 = 0x0, a1 = 0x87000000, a2 = 0x0, a3 = 0x0, a4 = 0x0, a5 = 0x0, a6 = 0x0, a7 = 0x53525354, s2 = 0xffffffffffffffff, s3 = 0x0, s4 = 0x0, s5 = 0x8000, s6 = 0x19, s7 = 0x0, s8 = 0x0, s9 = 0x0, s10 = 0x0, s11 = 0x0, t3 = 0xff60000001818f00, t4 = 0xff60000001818f00, t5 = 0xff60000001818000, t6 = 0xff200000007cbb58, sepc = 0x80000000, sstatus = 0x200004120, hstatus = 0x200200180, fp = { f = { f = {[0x0] = 0x0 <repeats 32 times>}, fcsr = 0x0 }, d = { f = {[0x0] = 0x0 <repeats 32 times>}, fcsr = 0x0 }, q = { f = {[0x0] = 0x0 <repeats 64 times>}, fcsr = 0x0, reserved = {[0x0] = 0x0, [0x1] = 0x0, [0x2] = 0x0} } } } >>> p/x (*(struct kvm_vcpu*)0xff60000002cd0000).arch.guest_csr $26 = { vsstatus = 0xa00000022, vsie = 0x222, vstvec = 0xffffffff80003594, vsscratch = 0x0, vsepc = 0xffffffff8006d566, vscause = 0x8000000000000005, vstval = 0x0, hvip = 0x0, vsatp = 0x0, scounteren = 0x7 }
|
通过上面寄存器的状态, 可以明显发现异常, 通用寄存器的状态不对, v* 开头的 csr 状态也不对, vstvec 和报错的栈正好对上, 而 guest_context.sepc 是 guest kernel reset 开始执行的代码地址.
可以充分怀疑是 vcpu 的通用寄存器和 v* 开头的 csr 状态没有重置导致了 vcpu reset 上电后状态是错乱的, guest kernel reset 后还未建页表时触发了 guest insn exception 走到了 vstvec, 从而陷入了 kvm, htval/stval 的地址因为此时 vstap 还是 0, 所以直接报的 gva 地址.
为什么 vcpu 的寄存器在 reset 后没有复位? kvm 中因为 vcpu 复用, 没走销毁的流程, 而是在 reset 中的关机阶段 power_off 了, vcpu 仅仅不进行调度, kvm 从 vcpu 的 task 切换到了其他进程.
vcpu 的寄存器还保留了 vcpu 关机前的状态, kvm 并没有处置的理由, 应该由 qemu 告诉 kvm vcpu 何时完成重置的.
梳理 qemu vm 重置的逻辑: (分先后顺序)
- rom 的重置
- ram 状态的重置
- 外设状态的重置
- cpu 的重置
qemu 中的各实例调用 qemu_register_reset
注册 reset 的 callback, qemu 在 ioctl(vcpufd, KVM_RUN) 调用结束后收到 kvm 的退出原因 KVM_SYSTEM_EVENT_RESET
, 进而触发了 qemu_system_reset->qemu_devices_reset
触发各实例注册的 reset callback.
在 cpu 的重置中,
riscv 的调用链:
riscv_harts_cpu_reset->cpu_reset->riscv_cpu_reset->kvm_riscv_reset_vcpu
首先 riscv_hart_realize 中通过 qemu_register_reset 注册了 riscv_harts_cpu_reset 的 reset callback
该 callback 调用了 CPU_CLASS 的 cpu_reset
而 RISCVCPUClass
在 class 初始化时注册了 parent class CPUClass
的 cpu_reset 的接口为 riscv_cpu_reset
device_class_set_parent_reset(dc, riscv_cpu_reset, &mcc->parent_reset);
在开启 kvm 后, 最终通过 kvm_riscv_reset_vcpu 重置了 qemu 保存的 vcpu 的寄存器状态, 但这里只重置了 sepc a0 a1 vstap. 这明显是有问题的.
在 cpu 状态重置完后, 通过 cpu_synchronize_all_post_reset
最终调用 kvm_arch_put_registers 将重置后的 vcpu 的寄存器状态通过 ioctl(KVM_SET_ONE_REG) 设置回了 kvm 的 vcpu 中.
这里只需要修改 qemu 的 kvm_riscv_reset_vcpu
函数将所有的通用寄存器和 v 开头的寄存器重置即可.
smp crash 分析解决
分析 riscv 的启动过程, 只有 boot_hart 在 reset 后需要立即上电, 其他 vcpu 是走的 sbi 的接口最终调用 kvm_riscv_vcpu_power_on
上电的
这里只需要修改 qemu 的流程, 在 reset 最后由 qemu 发起 vcpu 上电的流程中判断 vcpu 的 index, 只对 boot_hart 上电即可.
System_reset 导致的 crash
方案修改完后, 在测试时, 需要对其他场景进行测试.
虚拟机的重启行为不仅有 guest shell 内自己调用 reboot 进行重启, 更重要的还有虚拟机管理软件上提供的重启功能 (界面上的重启按钮等)
对 qemu 而言, 最终都会调到其 system_reset
command.
测试发现, smp 场景下, system_reset 后, kvm 出现 crash, 而单核场景下 kvm 不会出现 crash.
对比发现, 不是修改引入.
1 2 3 4 5 6 7 8 9
| [ 53.739556] kvm [150]: VCPU exit error -95 [ 53.739563] kvm [148]: VCPU exit error -95 [ 53.739557] kvm [149]: VCPU exit error -95 [ 53.740957] kvm [149]: SEPC=0x0 SSTATUS=0x200004120 HSTATUS=0x2002001c0 [ 53.740957] kvm [148]: SEPC=0x0 SSTATUS=0x200004120 HSTATUS=0x2002001c0 [ 53.741054] kvm [148]: SCAUSE=0x14 STVAL=0x0 HTVAL=0x0 HTINST=0x0 [ 53.741058] kvm [149]: SCAUSE=0x14 STVAL=0x0 HTVAL=0x0 HTINST=0x0 [ 53.756187] kvm [150]: SEPC=0x0 SSTATUS=0x200004120 HSTATUS=0x2002001c0 [ 53.757797] kvm [150]: SCAUSE=0x14 STVAL=0x0 HTVAL=0x0 HTINST=0x0
|
那这个该怎么解释呢? 联想之前的 smp 场景的问题, 怀疑点仍然是 kvm 内部的 vcpu->arch.power_off 状态出问题了.
通过 qemu 下发 system_reset 与 guest 内部 shell 执行 reboot 有什么区别呢, 为什么上述方案不能解决 qemu system_reset 导致的重启呢?
根据代码调用的路径, 最主要的点在
- guest 内部 shell 执行的 reboot 通过 sbi 陷入到 kvm 时, 将所有 vcpu->arch.power_off 全部置为 on 了即 vcpu 全部断电
- qemu 下发 system_reset 并未处理 kvm 的 vcpu->arch.power_off, 只是把所有 vcpu 线程退出了 KVM_RUN 的循环. 结束了 ioctl(KVM_RUN)的调用, 这一点与 guest shell reboot 的行为是一样的. 这也能解释为什么非 smp 场景下, system_reset 可以正常重启. 因为 boot_hart 的 vcpu 未断电, 所以可以正常调度.
根据这个区别点, 很容易想到在修复 smp crash 的方案中, 将不是 boot_hart 的 vcpu, 主动给 kvm 发送 vcpu 断电的命令, 让非 boot_hart 的 vcpu 的 vcpu->arch.power_off 回到 on 状态.
加入这个修改后, smp 场景下测试 qemu 下发 system_reset 可以正常重启.
小结
整体回顾下 reboot 流程, 这里为什么需要把 reset 流程总结一下, 因为上面的方案中无论是 qemu 通过 ioctl 设置 vcpu->arch.power_off 还是通过 ioctl 设置 vcpu 的寄存器和 csr, 都要保证 vcpu 不能处在 KVM_RUN 的小循环下 (红色实线的循环), 必须结束 ioctl(KVM_RUN) 退到用户态后重发 ioctl(KVM_RUN). 否则是没有时机同步这些的. 即使可以同步, 也非常容易引起时序混乱.

qemu 的 vcpu 线程可以通过其他线程设置 pthread_kill signal 将阻塞在 ioctl(KVM_RUN)
的系统调用退出 ioctl.
在 vcpu 的 kvm task 因 power_off 导致处于 rcu_wait_event 循环 schedual 出不来时, signal 导致该 task 收到信号后, 在 rcu_wait_event 检测到了 signal_pending_state 退出循环, 进入下一轮循环后, kvm 调用了 kvm_handle_signal_exit 结束了本次 ioctl(KVM_RUN)
调用回到了 qemu 中.

upstream 修复
upstream 提交change 修复
https://github.com/qemu/qemu/commit/8633951530cc923f1e7a6cd250f670f24c0ed817