0%

riscv kvm 方案代码调研

内存虚拟化

涉及到两个主题:

  1. GPA->HVA
  2. HVA->HPA

略过 GVA->GPA, 这个是 guest os 中的 mmu 地址翻译过程.

GPA->HVA

KVM-Qemu 方案中,GPA->HVA 的转换,是通过 ioctl 中的 KVM_SET_USER_MEMORY_REGION 命令来实现的.

数据结构

  • 虚拟机使用slot来组织物理内存,每个slot对应一个struct kvm_memory_slot,一个虚拟机的所有slot构成了它的物理地址空间;
  • 用户态使用struct kvm_userspace_memory_region来设置内存slot,在内核中使用struct kvm_memslots结构来将kvm_memory_slot组织起来;
  • struct kvm_userspace_memory_region结构体中,包含了slot的 ID 号用于查找对应的slot,此外还包含了物理内存起始地址及大小,以及 HVA 地址,HVA 地址是在用户进程地址空间中分配的,也就是 Qemu 进程地址空间中的一段区域;

流程分析

数据结构部分已经罗列了大体的关系,那么在KVM_SET_USER_MEMORY_REGION时,围绕的操作就是slots的创建、删除,更新等操作

  • 当用户要设置内存区域时,最终会调用到__kvm_set_memory_region函数,在该函数中完成所有的逻辑处理;
  • __kvm_set_memory_region函数,首先会对传入的struct kvm_userspace_memory_region的各个字段进行合法性检测判断,主要是包括了地址的对齐,范围的检测等;
  • 如果传入的参数中memory_size为 0,那么会将对应slot进行删除操作;
  • 根据用户传入的参数,设置slot的处理方式:KVM_MR_CREATEKVM_MR_MOVEKVM_MEM_READONLY
  • 根据用户传递的参数决定是否需要分配脏页的 bitmap,标识页是否可用;
  • 最终调用kvm_set_memslot来设置和更新slot信息;

kvm_set_memslot

具体的memslot的设置在kvm_set_memslot函数中完成,slot的操作流程如下:

  • 首先分配一个新的memslots,并将原来的memslots内容复制到新的memslots中;
  • 如果针对slot的操作是删除或者移动,首先根据旧的slot id号从memslots中找到原来的slot,将该slot设置成不可用状态,再将memslots安装回去。这个安装的意思,就是 RCU 的 assignment 操作,不理解这个的,建议去看看之前的 RCU 系列文章。由于slot不可用了,需要解除 stage2 的映射;
  • kvm_arch_prepare_memory_region函数,用于处理新的slot可能跨越多个用户进程 VMA 区域的问题,如果为设备区域,还需要将该区域映射到Guest GPA中;
  • update_memslots用于更新整个memslotsmemslots基于 PFN 来进行排序的,添加、删除、移动等操作都是基于这个条件。由于都是有序的,因此可以选择二分法来进行查找操作;
  • 将添加新的slot后的memslots安装回 KVM 中;
  • kvfree用于将原来的memslots释放掉;
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
-+ kvm_set_memslot(struct kvm *kvm, struct kvm_memory_slot *old, struct kvm_memory_slot *new, enum kvm_mr_change change)
\ -+ kvm_invalidate_memslot(kvm, old, invalid_slot);
"KVM_MR_DELETE 或 KVM_MR_MOVE 情况下需要无效化该slot以告知vcpu 该slot映射已经不存在了, 如果该内存插槽在KVM虚拟机中正在使用,则此函数将导致任何对该内存区域的访问引发异常。"
\ -+ kvm_arch_flush_shadow_memslot(kvm, old);
\ -+ gstage_unmap_range(kvm, gpa, size, false); "riscv 解除stage2 映射"
\ -+ gstage_op_pte(kvm, addr, ptep, ptep_level, GSTAGE_OP_CLEAR);
\ - set_pte(ptep, __pte(0)); "G-stage pte 清除, 进而导致vcpu 触发page fault"
| -+ kvm_prepare_memory_region(kvm, old, new, change);
\ -+ kvm_arch_prepare_memory_region(kvm, old, new, change); "riscv 实现"
"change 为 KVM_MR_CREATE 或 KVM_MR_MOVE 或 KVM_MR_FLAGS_ONLY 情况时"
\ - hva = new->userspace_addr;
| - size = new->npages << PAGE_SHIFT; "新申请的memslot range "
| - reg_end = hva + size;
| - base_gpa = new->base_gfn << PAGE_SHIFT;
"新申请的memslot 内存区域可能跨多个 VMA, 所以需要遍历所有的VMA, 查看 vma 是否覆盖了 hva的地址范围"
| -+ while (vma = find_vma(current->mm, hva)) "查到的vma 包含了hva地址, current->mm 代表查找当前进程即qemu的vma"
\ - pa = (phys_addr_t)vma->vm_pgoff << PAGE_SHIFT;
| - vm_start = max(hva, vma->vm_start);
| - pa += vm_start - vma->vm_start; "pa 为 HPA"
| - gpa = base_gpa + (vm_start - hva);
| -|+ if (vma->vm_flags & VM_PFNMAP) "预先映射 gpa->hpa 页表的前提是触及的vma 有 VM_PFNMAP 的flag"
\ -+ gstage_ioremap(kvm, gpa, pa, size(vm_end - vm_start), writable);
"维护gpa -> hpa映射关系"
\ - end = (gpa + size + PAGE_SIZE - 1) & PAGE_MASK;
| - pfn = __phys_to_pfn(hpa); "hpa 物理页号"
| -+ for (addr = gpa; addr < end; addr += PAGE_SIZE)
\ - pte = pfn_pte(pfn, PAGE_KERNEL);
| - kvm_mmu_topup_memory_cache(&pcache, gstage_pgd_levels); "预分配mmu 页表本身所需的内存"
| -+ gstage_set_pte(kvm, level:0, &pcache, GPA:addr, &pte);
\ - current_level = gstage_pgd_levels - 1; "这里只看到 sv39x4, gstage_pgd_levels=3"
| - pte_t *next_ptep = (pte_t *)kvm->arch.pgd; "kvm 为虚拟机的实例, pgd 为G-stage 第一级页表基址"
| - pte_t *ptep = &next_ptep[gstage_pte_index(addr, current_level)]; "取出第一级页表项"
| -+ while (current_level != level(0))
\ - gstage_pte_leaf(ptep) "检查不应为叶子节点"
| -|+ if(!pte_val(*ptep)) "下一级页表项无效, 需要创建下一级页表项"
"从预分配内存中找出一个可用的内存地址放下一级页表项"
\ - next_ptep = kvm_mmu_memory_cache_alloc(pcache);
| - *ptep = pfn_pte(PFN_DOWN(__pa(next_ptep)), __pgprot(_PAGE_TABLE));
"更新当前页表项, 保存下一级页表项 ppn 及 权限"
-|+ else "下一级页表项有效"
\ - next_ptep = (pte_t *)gstage_pte_page_vaddr(*ptep); "直接找出下一级页表项地址"
| - current_level--; "处理下一级页表项"
| - ptep = &next_ptep[gstage_pte_index(addr, current_level)]; "ptep 更新为下一级页表项地址"
| - *ptep = *pte; "最终更新叶子节点为 HPA的pte"
| -+ gstage_remote_tlb_flush(kvm, current_level, addr); "G-stage 页表更新后, 还需要刷新tlb"\
\ -+ kvm_riscv_hfence_gvma_vmid_gpa(kvm, -1UL, 0, addr, BIT(order), order);
\ ---+ kvm_riscv_hfence_process(vcpu)
\ -+ case KVM_RISCV_HFENCE_GVMA_VMID_GPA:
\ - kvm_riscv_local_hfence_gvma_vmid_gpa(READ_ONCE(v->vmid), d.addr, d.size, d.order);
| - hva = vm_end; "更新hva"
| -+ kvm_create_memslot(kvm, new); "if KVM_MR_CREATE"
| -+ kvm_delete_memslot(kvm, old, invalid_slot); "if KVM_MR_DELETE"
| -+ kvm_move_memslot(kvm, old, new, invalid_slot); "if KVM_MR_MOVE"
| -+ kvm_update_flags_memslot(kvm, old, new); "if KVM_MR_FLAGS_ONLY"
| -+ kvm_commit_memory_region(kvm, old, new, change);

在 riscv 中, kvm_arch_prepare_memory_region 的实现判断如果vma 有 VM_PFNMAP 的flag, 会调用 gstage_ioremap 进而调用 gstage_set_pte 建立 GPA->HPA的G-stage 页表

最终建立页表映射关系的函数为 gstage_set_pte.
而不是所有的vma 都有这个flag, 大部分的 GPA->HPA的 G-stage 页表映射的建立还是在触发Guest Page Fault 时处理的.

kvm_commit_memory_region

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-+ kvm_commit_memory_region(struct kvm *kvm,
struct kvm_memory_slot *old,
const struct kvm_memory_slot *new,
enum kvm_mr_change change)
\ - kvm->nr_memslot_pages -= old->npages; "if KVM_MR_DELETE"
| - kvm->nr_memslot_pages += new->npages; "if KVM_MR_CREATE"
| -+ kvm_arch_commit_memory_region(kvm, old, new, change);
\ -|+ if(change != KVM_MR_DELETE && new->flags & KVM_MEM_LOG_DIRTY_PAGES)
\ -+ gstage_wp_memory_region(kvm, new->id);
\ - kvm_memslots *slots = kvm_memslots(kvm); "找到memslots"
| - kvm_memory_slot *memslot = id_to_memslot(slots, slot); "根据slot id 找到对应的slot"
| - start = memslot->base_gfn << PAGE_SHIFT;
| - end = (memslot->base_gfn + memslot->npages) << PAGE_SHIFT;
| -+ gstage_wp_range(kvm, start, end);
\ -+ while (addr < end)
\ -+ gstage_op_pte(kvm, addr, ptep, ptep_level, GSTAGE_OP_WP);
\ - set_pte(ptep, __pte(pte_val(*ptep) & ~_PAGE_WRITE)); "操作G-stage 页表pte, 去除W权限, write protector"
| - kvm_flush_remote_tlbs(kvm);

GPA->HPA

gstage_page_fault 根据guest page fault 时的htval/stval 记录的fault addr 为 GPA, 通过GPA 在memslots中找到对应的memslot, 进而找到 hva 和 hpa, 根据hva所在的vma 的page_size 确定是建立大页还是普通的G-stage 页表
, 最终通过 gstage_set_pte 函数建立G-stage 页表.
中途还处理了hpa的dirty 和 memslot 中的dirty_bitmap 以及 G-stage 页表pte的dirty位

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
-+ gstage_page_fault(struct kvm_vcpu *vcpu, struct kvm_run *run,
struct kvm_cpu_trap *trap)
\ - fault_addr = (trap->htval << 2) | (trap->stval & 0x3); "guest page fault 时 通过htval 或 stval 记录 GPA"
| - gfn = fault_addr >> PAGE_SHIFT; "GPA的页号"
| - memslot = gfn_to_memslot(vcpu->kvm, gfn); "通过GPA 页号找到kvm对应的memslot"
| - hva = gfn_to_hva_memslot_prot(memslot, gfn, &writeable); "通过memslot gpa页号找到 hva"
"找到了hva, 说明是内存, 不会陷入到 emulate_load emulate_store io模拟"
| -+ kvm_riscv_gstage_map(vcpu, memslot, GPA:fault_addr, hva,
is_write:(trap->scause == EXC_STORE_GUEST_PAGE_FAULT) ? true : false);
\ - vma = find_vma_intersection(current->mm, hva, hva + 1); "通过hva 查找vma"
| - kvm_mmu_topup_memory_cache(pcache, gstage_pgd_levels); "预分配页表本身所需内存"
| -+ hfn = gfn_to_pfn_prot(kvm, gfn, is_write, &writeable); "gpa页号通过 memslot 找到 hva 再找到 hpa页号"
\ - addr = __gfn_to_hva_many(slot, gfn, NULL, write_fault); "先通过gpa 找到hva"
| - return hva_to_pfn(addr, atomic, async, write_fault, writable); "再通过hva 找到hpa 得到hfn"
| -|+ if (writeable)
\ - kvm_set_pfn_dirty(hfn); "hfn pte 置dirty"
| - mark_page_dirty(kvm, gfn); "对应memslot的 dirty_bitmap 置位, 维护虚拟机状态"
| -+ gstage_map_page(kvm, pcache, gpa, hfn << PAGE_SHIFT,
page_size:vma_pagesize, page_rdonly:false, page_exec:true);
"根据page_size 确定建几级页表, 比如该vma的page_size 很大, 超过了 1个page, 就可以考虑建大页"
\ - gstage_page_size_to_level(page_size, &level);
| - new_pte = pfn_pte(PFN_DOWN(hpa), prot); "更新pte 状态, prot 根据 page_rdonly page_exec 确定 RWX权限"
| - new_pte = pte_mkdirty(new_pte); "pte 添加 Dirty状态"
| - gstage_set_pte(kvm, level, pcache, gpa, &new_pte); "建立 G-stage GPA->HPA 页表"
| -|+ else
\ - ret = gstage_map_page(kvm, pcache, gpa, hfn << PAGE_SHIFT,
vma_pagesize, true, true);

cpu 虚拟化

vcpu 调用流

梳理下vcpu 调度的流程, 需要结合下qemu 来看下

KVM_VCPU_RUN

qemu 用户态代码, qemu 为每个vcpu 建了一个新的线程, 专门用来运行vcpu, 主要逻辑就是一个while 循环调用ioctl(KVM_RUN), 如果vcpu guest 一直运行, 则该次ioctl 不会结束,

  • 只有当guest 因请求导致异常陷入kvm 后, kvm 判断需要用户态处理该请求时, 才会结束这次ioctl的调用, 回到qemu 中
  • qemu 接着判断这次需要用户态处理的原因, 处理完后, 再进行下一轮while 循环, 继续发送ioctl(KVM_RUN)
  • qemu 将本次处理的结果在下一轮循环中告诉kvm, kvm 在处理完qemu处理的结果后, 再进到guest, 结束本次guest的请求.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
while(1) {
ret = ioctl(vcpufd, KVM_RUN, NULL);
switch (run->exit_reason)
case KVM_EXIT_IO:
kvm_handle_io(run->io.port, attrs,
(uint8_t *)run + run->io.data_offset,
run->io.direction,
run->io.size,
run->io.count);
...
case KVM_EXIT_MMIO:
address_space_rw(&address_space_memory,
run->mmio.phys_addr, attrs,
run->mmio.data,
run->mmio.len,
run->mmio.is_write);
...
}

kvm 中vcpu run的流程(精简)

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
67
```erlang
-+ kvm_vcpu_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg)
\ - kvm_arch_vcpu_async_ioctl(filp, ioctl, arg); "riscv not implement"
| -+ case KVM_RUN "ioctl 值"
\ - oldpid = rcu_access_pointer(vcpu->pid); "保存vcpu->pid 值, 这个值表示用户态qemu 创建的vcpu 线程id"
| -|+ if oldpid != task_pid(current) "线程id不一样"
\ -+ kvm_arch_vcpu_run_pid_change(vcpu) "riscv not implement"
| - newpid = get_task_pid(current, PIDTYPE_PID);
| - rcu_assign_pointer(vcpu->pid, newpid); "vcpu 指向新的线程id"
| -+ r = kvm_arch_vcpu_ioctl_run(vcpu);
\ - kvm_run *run = vcpu->run;
| - vcpu->arch.ran_atleast_once = true; "标记运行过"
| -+ switch (run->exit_reason)
\ -|+ case KVM_EXIT_MMIO: "处理寄存器"
"比如guest 此时 [ld t0, 地址], 此处为 qemu 读到的mmio 地址的值, 最后要调用这个函数将值塞到 guest_context.t0中"
\ - kvm_riscv_vcpu_mmio_return(vcpu, vcpu->run);
| -|+ case KVM_EXIT_RISCV_SBI:
\ - kvm_riscv_vcpu_sbi_return(vcpu, vcpu->run); "Process SBI value returned from user-space"
| -|+ case KVM_EXIT_RISCV_CSR:
\ - kvm_riscv_vcpu_csr_return(vcpu, vcpu->run); "Process CSR value returned from user-space"
| -+ vcpu_load(vcpu);
\ -+ kvm_arch_vcpu_load(vcpu, cpu);
\ - kvm_vcpu_csr *csr = &vcpu->arch.guest_csr; "guest_csr 在前面KVM_VCPU_CREATE 创建时已经初始化过了"
| - csr_write(CSR_VSEPC, csr->vsepc); "读取vcpu->guest_csr 中的v开头的csr, 恢复到 cpu的对应csr中"
| -+ while(ret > 0)
\ - kvm_riscv_gstage_vmid_update(vcpu); "更新vmid"
| - kvm_riscv_check_vcpu_requests(vcpu);
| - local_irq_disable(); "关闭 hs kernel的中断"
| - kvm_riscv_vcpu_flush_interrupts(vcpu); "根据qemu 注入的 irqs_pending sync vcpu->guest_csr 的hvip"
| - kvm_riscv_update_hvip(vcpu); "根据vcpu->guest_csr的hvip 更新 cpu的 hvip csr"
| -+ kvm_riscv_vcpu_enter_exit(vcpu); "切换vcpu 虚拟机运行"
\ -+ __kvm_riscv_switch_to(&vcpu->arch); "进入 guest"
"同时设置stvec 为 __kvm_switch_return, 这样因异常或中断退出guest 返回host 时, 会走到 __kvm_switch_return 中"
-----------> "因异常或中断退出guest, 回到kvm上下文, 退出guest 切换到 host 时, 会保存guest 的上下文到 guest_context 中" <--------
| ---+ __kvm_switch_return "保存guest 上下文, 恢复host 上下文, 恢复stvec 为 hs kernel 原本的stvec"
| -
| - trap.sepc = vcpu->arch.guest_context.sepc;
| - trap.htval = csr_read(CSR_HTVAL);
| - trap.scause = csr_read(CSR_SCAUSE);
| - kvm_riscv_vcpu_sync_interrupts(vcpu); "guest 有可能会操作 sip.VSSIP 导致 hvip.VSSIP 变化了, 这里要跟host 中的hvip 变量同步一下"
| - local_irq_enable(); "开启hs kernel 的中断, 如果有hs kernel 的异常/中断的pending, 会在这个地方进入到hs kernel的异常/中断处理流程中"
| -+ kvm_riscv_vcpu_exit(vcpu, run, &trap); "判断异常的原因, 对异常进行处理"
\ - switch (trap->scause)
| -|+ case EXC_INST_ILLEGAL
| -|+ case EXC_VIRTUAL_INST_FAULT
| -|+ case EXC_INST_GUEST_PAGE_FAULT | EXC_LOAD_GUEST_PAGE_FAULT | EXC_STORE_GUEST_PAGE_FAULT
\ -+ gstage_page_fault(vcpu, run, trap);
\ - switch (trap->scause)
| -|+ case EXC_LOAD_GUEST_PAGE_FAULT
\ -+ return kvm_riscv_vcpu_mmio_load(vcpu, run, fault_addr, trap->htinst);
\ -|+ if (!kvm_io_bus_read(vcpu, KVM_MMIO_BUS, fault_addr, len, data_buf) "如果kvm能自己处理, 则不会陷入用户态, 由kvm_io 设备处理"
\ - kvm_riscv_vcpu_mmio_return(vcpu, run); "处理寄存器"
"比如guest 此时 [ld t0, 地址], kvm从kvm_io设备中读出该地址的值, 最后要调用这个函数将值塞到 guest_context.t0中"
| <== ret = 1 \ - return 1 " 不需要陷入用户态, ret 返回1, while继续, 下一轮重新进入guest 中"
| -|+ else
\ - vcpu->stat.mmio_exit_user++;
| - run->exit_reason = KVM_EXIT_MMIO; "陷入用户态 qemu, qemu需要知道原因"
| <== ret = 0 | - return 0; "由用户态 qemu 进行处理, 这个地方return 0, 退出while 循环, 本轮 ioctl(vcpufd, KVM_RUN, NULL) 调用结束, 回到qemu用户态"
| -|+ case EXC_STORE_GUEST_PAGE_FAULT
| <== \ -+ return kvm_riscv_vcpu_mmio_store(vcpu, run, fault_addr, trap->htinst);
| - kvm_riscv_gstage_map(vcpu, memslot, fault_addr, hva, is_write) "如果能通过gpa 找到hva, 对应ram 的地址, 则直接建立G-stage 页表就行了"
| <== | - return 1; " 继续while 循环, 下一轮进入guest"
| -|+ case EXC_SUPERVISOR_SYSCALL
\ -+ kvm_riscv_vcpu_sbi_ecall(vcpu, run);
| -+ vcpu_put(vcpu); "上面return 0 时代表需要进入qemu 中进行处理, 继续走到这里"
\ -+ kvm_arch_vcpu_put(vcpu); "主要是读取v 开头的csr, 保存到vcpu->guest_csr 下"
| - "结束ioctl 调用"

为了更容易理解, 只对mmio load 这一种情况做一下简要的流程介绍:

图中两个循环, 一个是qemu 中的 (黄色线) , 一个是kvm中的 (红色线)
当guest 因load 指令陷入到 kvm 中时, 查找GPA 没找到 关联的hva (说明是MMIO 地址, 不是RAM 地址)
如果kvm 可以自己处理, 即通过遍历 KVM_MMIO_BUS 上注册的kvm_io_dev 设备的地址范围, 查找gpa 是否在该范围内

  • 如果在 KVM_MMIO_BUS 地址范围, 则由kvm_io_dev的对应的read 函数进行处理
  • 如果不在, 则需要退出kvm的 KVM_RUN 的循环, 结束ioctl(KVM_RUN), 回到qemu 用户态, 原因为 KVM_EXIT_MMIO, 由qemu 在AddressSpace 查找对应的MemoryRegion, 调用MemoryRegion的read函数进行处理, 处理完后, qemu 进行下一轮循环, 重新发起 ioctl(KVM_RUN) 进入kvm
  • 上述两种方式读到了mmio 地址 (GPA) 的值, 最终都需要调用 kvm_riscv_vcpu_mmio_return 将该地址的值填到对应的 guest 发起的load 目标寄存器中.
  • 最后通过 kvm_riscv_vcpu_enter_exit 函数重新进入guest 中.

注意host 上下文和 guest 上下文的保存恢复过程, 上述为了简述只写了通用寄存器的保存恢复流程, 实际还有一些cpu的 csr 需要保存恢复. 如 host的 scounteren sstatus hstatus hvip hgatp 等 (一个物理cpu可以托管多个vcpu, 而每个vcpu的这几个csr可能都是不同的)
guest 的主要的v开头的csr 属于是vcpu 专属的, 这部分放在了vcpu->guest_csr 中, 需要在 vcpu_put 和 vcpu_load 中进行保存和恢复. 这一部分为什么没在 kvm_riscv_vcpu_enter_exit 时进行处理是因为, kvm的 KVM_RUN的循环是针对同一个vcpuid的, 离开了这个循环才有可能在本物理cpu上调度其他vcpuid的vcpu, 将v 开头的csr保存恢复放在 KVM_RUN 循环外避免了 无意义的保存恢复.

vCPU 调度

首先需要知道 qemu-kvm 框架下vcpu 和 虚拟机实例 vm 的角色.

KVM中的一个客户机(VM 实例)是作为一个用户空间进程(qemu-kvm)运行。和其他普通的用户空间进程一样,由内核调度。
多个客户机(VM 实例)就是宿主机中的多个QEMU进程
而一个客户机的多个vCPU就是一个QEMU进程中的多个线程。

因为vCPU实际上是宿主机上的一个线程,所以vCPU的调度是依赖宿主机Linux内核中的CPU调度算法。
即kvm 中并没有为vcpu 做专门的调度.

宿主机调度时机基于宿主机的每一次的tick处理函数, 根据运行的时间片或负载调整内核task的切换.
用户态的进程和线程落到kernel 中都是task.
宿主机因tick 中断会从vcpu guest 中退出来陷入到 host kernel 中的tick 处理函数中.
host kernel 根据对应的调度策略选择切换到其他的task.

Linux调度算法有三个比较著名:

  1. O(1) scheduler
  2. Comletely Fair Scheduler(CFS)
  3. BF Scheduler(BFS)

对smp的支持

在多个hart 上并行执行多个vcpu的task.
vcpu task 因为是属于qemu的线程, 所以vcpu 可以更换物理hart运行.
linux 的CFS算法会根据smp负载情况调度qemu的线程, 使vcpu 运行在负载较低的物理hart上.

hs timer tick 调度细节

从源码上看, kvm 在进guest 之前, 将stvec 变成了kvm的 __kvm_switch_return
在vcpu 正在运行时, 如果因为 hs kernel的timer tick 到了, hs kernel 会因中断打断vcpu guest 的运行, 进而会先走到 __kvm_switch_return
__kvm_switch_return 会保存guest上下文/恢复host的上下文, 将stvec 恢复成hs kernel 原本的stvec, 接着处理kvm循环的后半部分,
接着会调用 local_irq_enable 这个函数会启用hs kernel 的中断, 开启后因为 sip & sie timer的pending, 所以会接着走timer的tick 的处理函数处理该pending, 也就进入了cpu调度的部分. cpu可以根据负载情况从 vcpu run的task 切换到别的task上.

但是注意此时vcpu run的task 还是在这个物理hart上, 从源码上看不允许将该task 切换到别的物理hart上执行, kvm的 KVM_RUN的循环没有退出, vcpu的资源也没有释放, vcpu在kvm中的相关资源(timer 中断, 更复杂的场景如支持中断直通)没有切换物理hart的时机, vcpu->cpu 并没有改变的时机, 只有KVM_RUN 结束后回到qemu的循环时, vcpu的资源才会释放, 该vcpu才能跑到其他的物理hart上.
这个可能会导致vcpu 长期绑到了一个物理hart上.

timer 虚拟化

从操作系统的角度来看一下 timer 的作用吧:

通过 timer 的中断,OS 实现的功能包括但不局限于上图:

  • 定时器的维护,包括用户态和内核态,当指定时间段过去后触发事件操作,比如 IO 操作注册的超时定时器等;
  • 更新系统的运行时间、wall time 等,此外还保存当前的时间和日期,以便能通过time()等接口返回给用户程序,内核中也可以利用其作为文件和网络包的时间戳;
  • 调度器在调度任务分配给 CPU 时,也会去对 task 的运行时间进行统计计算,比如 CFS 调度,Round-Robin 调度等;
  • 资源使用统计,比如系统负载的记录等,此外用户使用 top 命令也能进行查看;

riscv 中 对timer的虚拟化支持

  • mtime csr 时间来自于clint, 所有cpu 共享, 统一维护一个时间基准.
  • htimedelta, 每个cpu 有一个htimedelta csr, 各自记录与mtime 之间的offset.
  • time csr, 在V=1时, time = mtime + htimedelta; 在kvm中(HS-mode下), time = mtime

示例如下:

6ms 的时间段里,每个 vCPU 运行 3ms,Hypervisor 可以使用 htimedelta 寄存器来将 vCPU 的时间调整为其实际的运行时间;

用户层访问

可以从用户态对 vtimer 进行读写操作,比如 Qemu 中,流程如下:

1
2
3
4
5
6
7
8
9
10
11
12
---> qemu <---
-+ ioctl(vcpu_fd, KVM_GET_ONE_REG, 0)
---> kernel <---
\ -+ kvm_arch_vcpu_ioctl
\ -+ case KVM_GET_ONE_REG
\ -+ kvm_riscv_vcpu_get_reg(vcpu, &reg);
\ -+ kvm_riscv_vcpu_get_reg_timer
\ -+ case KVM_REG_RISCV_TIMER_REG(time)
\ -+ reg_val = kvm_riscv_current_cycles(gt);
\ - csr_read(CSR_TIME) + gt->time_delta;
| -+ case KVM_REG_RISCV_TIMER_REG(compare)
\ - reg_val = t->next_cycles;
  • 用户态创建完 vcpu 后,可以通过 vcpu 的文件描述符来进行寄存器的读写操作;
  • ioctl 通过KVM_SET_ONE_REG/KVM_GET_ONE_REG将最终触发寄存器的读写;
  • 如果操作的是 timer 的相关寄存器,则通过kvm_riscv_vcpu_set_reg_timerkvm_riscv_vcpu_get_reg_timer来完成;
  • 用户态hypervisor(qemu) 可以获得该vcpu的运行时间(time) 和 compare (t->next_cycles)

guest 访问

主要文件: arch/riscv/kvm/vcpu_timer.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
27
28
29
30
-+ kvm_arch_init_vm(struct kvm *kvm, unsigned long type)
\ -+ kvm_riscv_guest_timer_init(kvm);
\ -+ gt->time_delta = -get_cycles64();
\ - csr_read(CSR_TIME); "获取 time csr 作为time_delta 值"
-+ kvm_arch_vcpu_create(struct kvm_vcpu *vcpu)
\ -+ kvm_riscv_vcpu_timer_init(vcpu);
\ - kvm_vcpu_timer *t = &vcpu->arch.timer;
| - hrtimer_init(&t->hrt, CLOCK_MONOTONIC, HRTIMER_MODE_REL); "初始化单调timer 定时器, 相对时间"
| - t->hrt.function = kvm_riscv_vcpu_hrtimer_expired; "定时器restart 函数"
-+ kvm_arch_vcpu_load(struct kvm_vcpu *vcpu, int cpu) "vcpu load 时更新 htimedelta"
\ -+ kvm_riscv_vcpu_timer_restore(vcpu);
\ - csr_write(CSR_HTIMEDELTA, (u32)(gt->time_delta)); "更新htimedelta csr"
-+ case SBI_EXT_0_1_SET_TIMER: "guest os ecall 调用sbi, sbi 请求由kvm 接管"
\ - cp = &vcpu->arch.guest_context "guest_context 中保存了guest 运行时的通用寄存器"
| - next_cycle = (u64)cp->a0;
| -+ kvm_riscv_vcpu_timer_next_event(vcpu, next_cycle);
\ -+ kvm_riscv_vcpu_unset_interrupt(vcpu, IRQ_VS_TIMER);
\ - clear_bit(irq, &vcpu->arch.irqs_pending); "irqs_pending 中去除 IRQ_VS_TIMER"
| - delta_ns = kvm_riscv_delta_cycles2ns(ncycles, gt, t);
| - t->next_cycles = ncycles;
| -+ hrtimer_start(&t->hrt, ktime_set(0, delta_ns), HRTIMER_MODE_REL); "启动定时器, 周期为 delta_ns"
\ ----+ kvm_riscv_vcpu_hrtimer_expired(struct hrtimer *h) "定时器到期时, 调用该restart 函数"
\ -|+ if (kvm_riscv_current_cycles(gt) < t->next_cycles) "定时器到期小于ncycles时, "
\ - delta_ns = kvm_riscv_delta_cycles2ns(t->next_cycles, gt, t);
| - hrtimer_forward_now(&t->hrt, ktime_set(0, delta_ns)); "重定时定时器"
| - return HRTIMER_RESTART;
| -|+ else
\ -+ kvm_riscv_vcpu_set_interrupt(vcpu, IRQ_VS_TIMER); "设置 vcpu->arch.irqs_pending IRQ_VS_TIMER"
\ - set_bit(irq, &vcpu->arch.irqs_pending);
| - kvm_vcpu_kick(vcpu);

上述流程有些混乱, 先梳理一下

先回忆一下普通os的tick:

  • 设置time
  • 设置compare
    time 追上compare 后, 触发timer 中断, 在中断处理函数中设置下一轮的compare. 循环往复.

guest timer tick 处理

从前文 kvm_riscv_vcpu_get_reg_timer 中的 KVM_REG_RISCV_TIMER_REG(compare) 来看, 就比较容易理解上述流程了.
os 中调度时, tick 到期后, 需要重新设置compare.
在riscv 中, 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 运行环境

定时器到期时, 触发中断重新陷入到 kvm 中, 处理函数为 kvm_riscv_vcpu_hrtimer_expired

注意这时并不一定打断了设置compare的 vcpu的运行, 该函数有可能是被其他cpu 异步处理的.

  • 该函数首先判断到期的时间是否是小于 guest 设置的compare, 如果小于的话, 说明时间还没到, 需要重新计算定时时间, 启动定时器
  • 时间到了, 设置对应 vcpu->arch.irqs_pending 加入 IRQ_VS_TIMER, 如果是异步的处理, 调用 kvm_vcpu_kick(vcpu), 让该vcpu 退出运行, 回到kvm.
  • kvm 在下一轮 run vcpu 运行时会检查 vcpu->arch.irqs_pending, 如果有 IRQ_VS_TIMER, 会通过hvip 向该vcpu 注入虚拟timer 中断. 这样vcpu 再运行时, 在guest os中, 就能收到timer 中断, 进而由其 timer 中断处理函数处理. 这个流程可以理解为普通的os的tick到期的情况.

上述过程可见, guest timer tick 处理路径还是比较长的, 效率也比较低

guest time

从上面看更新 htimedelta的地方只有 gt->time_delta , 这个变量的赋值来自于 kvm_arch_init_vm, 为负的vm 实例的创建时间.
所以vcpu的 time 为 mtime+htimedelta = vm的运行时间.

sstc vstimecmp

riscv 支持sstc 后, 增加了vstimecmp的csr

开启/关闭 Sstc 的 stimecmp 功能是通过设置

  • menvcfg.STCE (bit 31) 开启后支持 HS-mode的stimecmp 功能
  • henvcfg.STCE (bit 31) 开启后支持 VS-mode的stimecmp 功能

如果 menvcfg.STCE=0, 则 VS 访问 stimecmp 会导致 illegal insn exception
如果 henvcfg.STCE=0, menvcfg.STCE=1, 则VS访问 stimecmp 会导致virtual insn execption

在Sstc.stimecmp 开启后还要开启 mcounteren 和 hcounteren的TM 位后, guest 才能直接访问 stimecmp 而不会导致异常.
在 mcounteren.tm=1 且 hcounteren.tm=1 时, guest 可以直接访问 stimecmp csr, 而tm位被清0后, guest 访问stimecmp csr 会导致virtual insn exception.

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.

中断虚拟化

本章分两部分:

  • 不支持中断直通, 即只是中断注入的情况
  • 中断直通 aia

plic 中断注入

中断控制器是qemu 模拟出来的
参考前面的介绍

通常 kvm_init函数会调用kvm_irqchip_create函数,后者在vm fd上调用ioctl(KVM_CREATE_IRQCHIP)告诉内核需要在KVM中模拟PLIC中断芯片。
但是会检查 KVM_CAP_IRQCHIP 能力, 这里riscv 并不支持, 所以这条路不会走

1
2
3
-+ kvm_irqchip_create(KVMState *s)
\ - kvm_check_extension(s, KVM_CAP_IRQCHIP) "这个地方检查没过, riscv 并不支持"
| - ret = kvm_arch_irqchip_create(s);

大概看下 qemu中中断注册以及中断控制器模拟的流程:

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
s->irqchip = virt_create_plic(memmap, 0, base_hartid, hart_count);
if (i == 0) {
mmio_irqchip = s->irqchip;
virtio_irqchip = s->irqchip;
pcie_irqchip = s->irqchip;
}

-+ riscv_cpu_init(Object *obj)
\ -+ qdev_init_gpio_in(DEVICE(cpu), riscv_cpu_set_irq, IRQ_LOCAL_MAX + IRQ_LOCAL_GUEST_MAX);
\ - NamedGPIOList *gpio_list = qdev_get_named_gpio_list(DEVICE(cpu), name); "name为NULL"
| -+ gpio_list->in = qemu_extend_irqs(gpio_list->in, gpio_list->num_in, handler, opaque, n);
\ -+ for i in n "n为IRQ_LOCAL_MAX + IRQ_LOCAL_GUEST_MAX 共79"
\ -+ gpio_list->in[i] = qemu_allocate_irq(handler, opaque, i); "新建了79个irq"
\ - irq = IRQ(object_new(TYPE_IRQ));
| - irq->handler = handler;
| - irq->opaque = opaque;
| - irq->n = n;
-+ s->irqchip[i] = virt_create_plic(memmap, i, base_hartid, hart_count);
\ -+ sifive_plic_create(hwaddr addr, char *hart_config, num_harts, hartid_base, num_sources)
\ - DeviceState *dev = qdev_new(TYPE_SIFIVE_PLIC);
| -+ sysbus_realize_and_unref(SYS_BUS_DEVICE(dev), &error_fatal);
\ ---+ sifive_plic_realize(DeviceState *dev) "mmio_irqchip = s->irqchip[i] irq设备的初始化"
\ -+ qdev_init_gpio_in(dev, sifive_plic_irq_request, s->num_sources); "sifive_plic_irq_request 为handler"
\ - NamedGPIOList *gpio_list = qdev_get_named_gpio_list(dev, NULL);
| - gpio_list->in = qemu_extend_irqs(gpio_list->in, gpio_list->num_in, sifive_plic_irq_request, opaque, n);
| -+ qdev_init_gpio_out(dev, s->s_external_irqs, s->num_harts);
\ - NamedGPIOList *gpio_list = qdev_get_named_gpio_list(dev, NULL);
| -+ for i in num_harts
\ - object_property_add_link(OBJECT(dev), propname, TYPE_IRQ, &s->s_external_irqs[i],
object_property_allow_set_link, OBJ_PROP_LINK_STRONG);
| -+ for i in num_harts
\ - CPUState *cpu = qemu_get_cpu(hartid_base + i);
| -|+ if plic->addr_config[j].mode == PLICMode_S
\ -+ qdev_connect_gpio_out(dev, i, qdev_get_gpio_in(DEVICE(cpu), IRQ_S_EXT)); "i 为cpuid"
\ - input_pin = qdev_get_gpio_in(DEVICE(cpu), IRQ_S_EXT)
"找到前面为cpu创建的gpio_list->in[IRQ_S_EXT], IRQ_S_EXT为9, 对应supervisor external interrupt"
| - object_property_set_link(OBJECT(dev), propname, OBJECT(input_pin), &error_abort);
"这个地方建立了链接 object_property_add_link 时 s_external_irqs[i] 指向了 cpu 的gpio_list->in[IRQ_S_EXT]"
"/machine/soc0/harts[0]/unnamed-gpio-in[9]"
-+ serial_mm_init(system_memory, memmap[VIRT_UART0].base, 0,
qdev_get_gpio_in(DEVICE(mmio_irqchip), UART0_IRQ),
399193, serial_hd(0), DEVICE_LITTLE_ENDIAN);
\ -+ qemu_irq irq = qdev_get_gpio_in(DEVICE(mmio_irqchip), UART0_IRQ)
\ -+ NamedGPIOList *gpio_list = qdev_get_named_gpio_list(DEVICE(mmio_irqchip), NULL);
"这个地方会找到 sifive_plic 设备初始化时建的gpio_list"
\ -+ for node in dev->gpios
\ - g_strcmp0(name, ngl->name) == 0
| - return ngl
| - return gpio_list->in[UART0_IRQ];
| -+ SerialMM *smm = SERIAL_MM(qdev_new(TYPE_SERIAL_MM));
\ ---+ serial_mm_realize(DeviceState *dev)
\ -+ sysbus_init_irq(SYS_BUS_DEVICE(smm), &smm->serial.irq);
\ - qdev_init_gpio_out_named(DEVICE(dev), &smm->serial.irq, SYSBUS_DEVICE_GPIO_IRQ, 1);
| -+ sysbus_connect_irq(SYS_BUS_DEVICE(smm), 0, irq);
\ -+ qdev_connect_gpio_out_named(DEVICE(dev), SYSBUS_DEVICE_GPIO_IRQ, n, irq);
\ - object_property_set_link(OBJECT(dev), propname, OBJECT(input_pin), );
"smm->serial.irq link 指向 mmio_irqchip 即 sifive_plic中断控制器的 gpio_list[in]->[UART0_IRQ]"
-+ sifive_plic_irq_request(void *opaque, int irq, int level)
\ - SiFivePLICState *s = opaque;
| - sifive_plic_set_pending(s, irq, level > 0);
| -+ sifive_plic_update(s);
\ - PLICMode mode = plic->addr_config[addrid].mode;
| -+ case PLICMode_S:
\ -+ qemu_set_irq(plic->s_external_irqs[hartid - plic->hartid_base], level);
"s->external_irqs 指向了 cpu的gpio_list->in[IRQ_S_EXT]"
\ -+ irq->handler(irq->opaque, irq->n, level);
\ -+ riscv_cpu_set_irq(opaque, irq, level) "指向了为cpu安装的中断处理函数 riscv_cpu_set_irq"
\ -+ case IRQ_S_EXT:
\ -+ kvm_riscv_set_irq(cpu, irq, level);
\ -+ kvm_vcpu_ioctl(CPU(cpu), KVM_INTERRUPT, &virq);
--------------------------------> 陷入kernel <------------------------------------------------
\ -+ kvm_riscv_vcpu_set_interrupt(vcpu, IRQ_VS_EXT);
\ - set_bit(irq, &vcpu->arch.irqs_pending);
| - set_bit(irq, &vcpu->arch.irqs_pending_mask);
| - kvm_vcpu_kick(vcpu);
"Kick a sleeping VCPU, or a guest VCPU in guest mode, into host kernel mode."
-+ kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)
\ -+ kvm_riscv_vcpu_flush_interrupts(vcpu);
\ - val = READ_ONCE(vcpu->arch.irqs_pending[0]) & mask;
| - csr->hvip |= val;
| -+ kvm_riscv_update_hvip(vcpu);
\ - csr_write(CSR_HVIP, csr->hvip);
| - kvm_riscv_vcpu_enter_exit(vcpu); "进入guest"

从上面的过程分析调用链
qemu使用GPIO来实现中断系统

  1. 首先cpu有gpio_in 接口
  2. 中断控制器 sifive_plic 有gpio_in 和 gpio_out, gpio_out 和 cpu 的gpio_in 接口关联
  3. 设备的gpio_out 和 sifive_plic的gpio_in 接口关联
  4. 当有中断发生时, 设备通过gpio_out 通知 sifive_plic, sifive_plic 通过gpio_out 通知 cpu的gpio_in
  5. cpu的gpio_in 的中断处理函数riscv_cpu_set_irq处理最终的中断, 并将中断状态通过kvm ioctl(KVM_INTERRUPT) 命令通知kvm, 由kvm完成最终的中断注入流程, 更新hvip
  6. 进入guest os后(V=1), hvip 注入中断会导致 V=1 时sip 置位, 如果sie相应位置位了, vcpu 会跳到中断入口, guest os 进而处理中断

中断注册(使能)流程

不支持设备直通时, 外设都是qemu 模拟出来的, 外设注册中断也是通过 模拟的中断控制器注册的中断
guest 在request_irq 的最后会通过设置cpu 亲和性使能特定的中断.

使能的逻辑最终会陷入到 sifive_plic_write enable 区域

1
2
//addrid 代表hart的偏移, wordid 代表中断号的偏移
plic->enable[addrid * plic->bitfield_words + wordid] = value;

中断触发流程

我们可以以uart 为入口点, uart调用了qdev_get_gpio_in(DEVICE(mmio_irqchip), UART0_IRQ) 绑定了 UART_IRQ=10的中断号的中断

先看下uart mmio write处理的调用栈, 这里模拟了uart的中断控制器, 写入uart的配置寄存器后, uart需要触发中断给到kvm, kvm再注入到guest.
这个中断的栈是比较重要的, 建议后面再详细看一下, 涉及了serial mmio 配置寄存器模拟的write, qemu调用printf 打印guest的信息, 以及qemu 打印完后, 模拟中断控制器, 将中断回给kvm, kvm最终将中断注入到guest的过程.

qemu中最终的中断处理函数是为 cpu 注册的中断处理函数 riscv_cpu_set_irq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#0  riscv_cpu_set_irq (opaque=0xaaaaaadc047c80, irq=9, level=1) at ../target/riscv/cpu.c:695
#1 0x00aaaaaadb76d1d8 in qemu_set_irq (irq=0xaaaaaadc05e670, level=1) at ../hw/core/irq.c:45
#2 0x00aaaaaadb645f60 in sifive_plic_update (plic=0xaaaaaadc06aea0) at ../hw/intc/sifive_plic.c:121
#3 0x00aaaaaadb646bc0 in sifive_plic_irq_request (opaque=0xaaaaaadc06aea0, irq=10, level=0) at ../hw/intc/sifive_plic.c:316
#4 0x00aaaaaadb76d1d8 in qemu_set_irq (irq=0xaaaaaadc06c880, level=0) at ../hw/core/irq.c:45
#5 0x00aaaaaadb28c880 in qemu_irq_lower (irq=0xaaaaaadc06c880) at /home/liguang/program/3rdparty/buildroot-2022.08.1/output/build/qemu-7.0.0/include/hw/irq.h:17
#6 0x00aaaaaadb28cfcc in serial_update_irq (s=0xaaaaaadc3c47b0) at ../hw/char/serial.c:144
#7 0x00aaaaaadb28d654 in serial_xmit (s=0xaaaaaadc3c47b0) at ../hw/char/serial.c:251
#8 0x00aaaaaadb28dc04 in serial_ioport_write (opaque=0xaaaaaadc3c47b0, addr=0, val=103,
size=1) at ../hw/char/serial.c:359
#9 0x00aaaaaadb28f268 in serial_mm_write (opaque=0xaaaaaadc3c4490, addr=0, value=103, size=1) at ../hw/char/serial.c:1009
#10 0x00aaaaaadb5dca84 in memory_region_write_accessor (mr=0xaaaaaadc3c4920, addr=0, value=0xffffff8da80008, size=1, shift=0, mask=255, attrs=...) at ../softmmu/memory.c:492
#11 0x00aaaaaadb5dcdc8 in access_with_adjusted_size (addr=0, value=0xffffff8da80008, size=1, access_size_min=1, access_size_max=8, access_fn=0xaaaaaadb5dc970 <memory_region_write_accessor>, mr=0xaaaaaadc3c4920, attrs=...) at ../softmmu/memory.c:554
#12 0x00aaaaaadb5e04c4 in memory_region_dispatch_write (mr=0xaaaaaadc3c4920, addr=0, data=103, op=MO_8, attrs=...) at ../softmmu/memory.c:1514
#13 0x00aaaaaadb5f0df0 in flatview_write_continue (fv=0xaaaaaadc42f380, addr=268435456, attrs=..., ptr=0xffffff8d27f028, len=1, addr1=0, l=1, mr=0xaaaaaadc3c4920) at ../softmmu/physmem.c:2814
#14 0x00aaaaaadb5f0fb4 in flatview_write (fv=0xaaaaaadc42f380, addr=268435456, attrs=..., buf=0xffffff8d27f028, len=1) at ../softmmu/physmem.c:2856
#15 0x00aaaaaadb5f14b4 in address_space_write (as=0xaaaaaadbdd8c48 <address_space_memory>, addr=268435456, attrs=..., buf=0xffffff8d27f028, len=1) at ../softmmu/physmem.c:2952
#16 0x00aaaaaadb5f1558 in address_space_rw (as=0xaaaaaadbdd8c48 <address_space_memory>, addr=268435456, attrs=..., buf=0xffffff8d27f028, len=1, is_write=true) at ../softmmu/physmem.c:2962
#17 0x00aaaaaadb759448 in kvm_cpu_exec (cpu=0xaaaaaadc047c80) at ../accel/kvm/kvm-all.c:2929
#18 0x00aaaaaadb75b1b4 in kvm_vcpu_thread_fn (arg=0xaaaaaadc047c80) at ../accel/kvm/kvm-accel-ops.c:49
#19 0x00aaaaaadb983e94 in qemu_thread_start (args=0xaaaaaadc06ad00) at ../util/qemu-thread-posix.c:556

外设发送中断给cpu时, 在sifive_plic_irq_request 中检查中断的使能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-+ sifive_plic_irq_request(void *opaque, int irq, int level)
\ -+ sifive_plic_set_pending(s, irq, level > 0);
\ - atomic_set_masked(&plic->pending[irq >> 5], 1 << (irq & 31), -!!level); "设置中断号 对应pending位"
| -+ sifive_plic_update(s);
\ -+ for (addrid = 0; addrid < plic->num_addrs; addrid++) "遍历hart"
\ -+ level = !!sifive_plic_claimed(plic, addrid);
\ -+ for (i = 0; i < plic->bitfield_words; i++) "遍历中断号"
\ - pending_enabled_not_claimed = "检查中断号pending 以及 hart 对应的enable"
(plic->pending[i] & ~plic->claimed[i]) &
plic->enable[addrid * plic->bitfield_words + i];
| -|+ if pending_enabled_not_claimed
\ - return max_irq;
| -|+ else
\ - return 0;
"判断给M-mode 还是给 S-mode, level为0代表关闭中断, level 为 1 代表需要注入中断"
| - uint32_t hartid = plic->addr_config[addrid].hartid;
| -+ case PLICMode_M:
\ - qemu_set_irq(plic->m_external_irqs[hartid - plic->hartid_base], level);
"enable了该中断的hart 需要注入中断, 其他的hart 不需要注入中断, hvip IRQ_VS_EXT 位 置0"
| -+ case PLICMode_S:
\ - qemu_set_irq(plic->s_external_irqs[hartid - plic->hartid_base], level);

guest 中断处理流程

kvm 在 kvm_riscv_vcpu_enter_exit 重新进入guest 后, 先前hvip 注入的IRQ_VS_EXT(10) 号中断转换为 sip IRQ_EXT(9) 号中断,
guest os 直接跳到 stvec 的中断入口的地方.

guest os 在中断处理函数中, 最终需要查中断控制器, 检查是哪一个外设的中断, 该查询操作最终陷入到 sifive_plic_read函数中
落到 pending_base的地址,

1
2
3
4
5
6
static uint64_t sifive_plic_read(void *opaque, hwaddr addr, unsigned size) {
} else if (addr_between(addr, plic->pending_base, plic->num_sources >> 3)) {
uint32_t word = (addr - plic->pending_base) >> 2;
return plic->pending[word];
...
}

查到对应的中断号后, 还需要设置claim, 声明中断已经处理完成.
该操作会落到 context_base 地址, 为写请求, 所以落到 write 函数里.

1
2
3
4
5
6
7
8
9
10
11
static void sifive_plic_write(void *opaque, hwaddr addr, uint64_t value, unsigned size) {
else if (addr_between(addr, plic->context_base,
plic->num_addrs * plic->context_stride)) {
...
} else if (contextid == 4) {
if (value < plic->num_sources) {
sifive_plic_set_claimed(plic, value, false);
sifive_plic_update(plic);
}
...
}

通过 sifive_plic_set_claimed 函数声明中断处理完成, 通过 sifive_plic_update 关闭该hart 上的中断信号, 即清除hvip的IRQ_VS_EXT位.

1
2
bool level = !!sifive_plic_claimed(plic, addrid);
qemu_set_irq(plic->s_external_irqs[hartid - plic->hartid_base], level);

AIA imsic 中断处理

该章节中涉及的代码来自于 https://github.com/avpatel/linux/ riscv_kvm_aia_irqchip_v3 分支

qemu 版本 github 主线 https://github.com/qemu/qemuhttps://github.com/avpatel/qemu
kvmtool 版本 https://github.com/avpatel/kvmtool riscv_aia_v1 分支

使用 qemu linux kernel x86 平台模拟 riscv 主机
使用 kvmtool 编译的 lkvm 执行 guest kernel, guest kernel 和 host kernel 使用的都是 riscv_kvm_aia_irqchip_v3 编译出的kernel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"host 模拟riscv 主机"
$QEMU -m 2G -nographic \
-M virt,aclint=on,aia=aplic-imsic,aia-guests=3 \
-kernel $KERNEL_BIN \
-drive file=$ROOT_IMAGE,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 \
-append "root=/dev/vda rw earlycon=sbi console=ttyS0 crashkernel=64M" \
-smp $SMP \
$DEBUG \
-device virtio-net-device,netdev=net0 \
-netdev user,id=net0,net=192.168.100.1/24,hostfwd=tcp::${HOST_PORT}-:22,hostfwd=tcp::6666-:6666 \
-device virtio-rng-pci \
-device edu \
-bios $OPENSBI_BIN \

"kvmtool 启动 kvm-mode guest"
./lkvm run -k Image -d rootfs.ext2

在介绍imsic 之前, 先看下riscv kvm 的virtual insn exception 的陷入路径
其中guest 读写imsic 相关csr 的模拟会经由这个路径最终转发到kvm进行处理

1
2
3
4
5
6
7
8
9
10
11
12
-+ kvm_riscv_vcpu_exit(struct kvm_vcpu *vcpu, struct kvm_run *run,
struct kvm_cpu_trap *trap)
\ - switch (trap->scause)
| -+ case EXC_VIRTUAL_INST_FAULT: "同时check vcpu->arch.guest_context.hstatus & HSTATUS_SPV"
\ -+ virtual_inst_fault(vcpu, run, trap);
\ - insn = trap->stval;
| - switch ((insn & INSN_OPCODE_MASK) >> INSN_OPCODE_SHIFT)
| -+ case INSN_OPCODE_SYSTEM:
\ -+ system_opcode_insn(vcpu, run, insn);
\ -+ for (i = 0; i < ARRAY_SIZE(system_opcode_funcs); i++) "遍历 system_opcode_funcs"
\ - ifn = &system_opcode_funcs[i]; "(insn & ifn->mask) == ifn->match"
| - ifn->func(vcpu, run, insn) "match ifn->mask opcode 时转入对应的处理函数"

看一下 system_opcode_funcs, 包含了对guest 中因 csrrw csrrs csrrc csrrwi CSRRSI CSRRCI 及 WFI 指令导致异常陷入kvm时的处理

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
static const struct insn_func system_opcode_funcs[] = {
{
.mask = INSN_MASK_CSRRW,
.match = INSN_MATCH_CSRRW,
.func = csr_insn,
},
{
.mask = INSN_MASK_CSRRS,
.match = INSN_MATCH_CSRRS,
.func = csr_insn,
},
{
.mask = INSN_MASK_CSRRC,
.match = INSN_MATCH_CSRRC,
.func = csr_insn,
},
{
.mask = INSN_MASK_CSRRWI,
.match = INSN_MATCH_CSRRWI,
.func = csr_insn,
},
{
.mask = INSN_MASK_CSRRSI,
.match = INSN_MATCH_CSRRSI,
.func = csr_insn,
},
{
.mask = INSN_MASK_CSRRCI,
.match = INSN_MATCH_CSRRCI,
.func = csr_insn,
},
{
.mask = INSN_MASK_WFI,
.match = INSN_MATCH_WFI,
.func = wfi_insn,
},
};

来看一下 csr_insn

1
2
3
4
5
6
7
8
9
10
-+ csr_insn(struct kvm_vcpu *vcpu, struct kvm_run *run, ulong insn)
\ - csr_num = insn >> SH_RS2; " csrw/csrr 的操作csr"
| - rs1_num = (insn >> SH_RS1) & MASK_RX; "csrw/csrr 中的通用寄存器 index"
| - rs1_val = GET_RS1(insn, &vcpu->arch.guest_context); ""
| - csr_funcs = { .base = CSR_SIREG,      .count = 1, .func = kvm_riscv_vcpu_aia_rmw_ireg }, \
{ .base = CSR_STOPEI,     .count = 1, .func = kvm_riscv_vcpu_aia_rmw_topei },
| -+ for (i = 0; i < ARRAY_SIZE(csr_funcs); i++) "遍历csr_funcs"
\ -|+ if ((tcfn->base <= csr_num) && (csr_num < (tcfn->base + tcfn->count)))
"match 参数传入的csr_num 与 csr_funcs 注册的 csr_num"
\ -+ cfn->func(vcpu, csr_num, &val, new_val, wr_mask) "调用对应的 csr_funcs 注册的函数"

如操作的csr 为 stopei 寄存器, 则会由 kvm_riscv_vcpu_aia_rmw_topei 进行处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-+ kvm_riscv_vcpu_aia_rmw_topei(struct kvm_vcpu *vcpu,
unsigned int csr_num,
unsigned long *val,
unsigned long new_val,
unsigned long wr_mask)
\ -+ kvm_riscv_vcpu_aia_imsic_rmw(vcpu, KVM_RISCV_AIA_IMSIC_TOPEI,
val, new_val, wr_mask) "wr_mask 代表为写请求"
\ -+ imsic_swfile_topei_rmw(vcpu, val, new_val, wr_mask);
\ - imsic *imsic = vcpu->arch.aia.imsic_state; "imsic_state 是保存在vcpu 中的"
\ -+ topei = __imsic_swfile_topei(imsic);
\ -+ for (i = 1; i < max_msi; i++) "max_msi 来自于 swfile->eithreshold 和 imsic->nr_msis"
\ -|+ if (test_bit(i, swfile->eie) && test_bit(i, swfile->eip)) "i 越小, 代表优先级越高, i 从0开始遍历,
eip&eie 的i位都置1了, 说明i位中断pending, 代表最高优先级的中断"
\ - return (i << TOPEI_ID_SHIFT) | i;
| - *val = topei; "读最大优先级的pending 中断"
| - "省略csrw stopei 的处理..."

上述逻辑符合 riscv-aia spec 中的描述, 在csrw/csrr stopei 时需要陷入到kvm中进行处理
这里需要关注下 属于vcpu的 imsic_state 结构

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
struct imsic_mrif_eix {
unsigned long eip[BITS_PER_TYPE(u64) / BITS_PER_LONG];
unsigned long eie[BITS_PER_TYPE(u64) / BITS_PER_LONG];
};

struct imsic_mrif {
struct imsic_mrif_eix eix[IMSIC_MAX_EIX];
unsigned long eithreshold;
unsigned long eidelivery;
};

struct imsic {
struct kvm_io_device iodev;

u32 nr_msis;
u32 nr_eix;
u32 nr_hw_eix;
/* IMSIC VS-file */
rwlock_t vsfile_lock;
int vsfile_cpu;
int vsfile_hgei;
void __iomem *vsfile_va;
phys_addr_t vsfile_pa;

/* IMSIC SW-file */
struct imsic_mrif *swfile;
phys_addr_t swfile_pa;
};

为什么需要软件维护 swfile 这样的数据

为什么模拟这么一套 imsic的机制进来, 可能是为了兼容 guest os的aia 中断机制.
且imsic 做到了kvm 中进行模拟, 应该比做到qemu 中模拟中断控制器的效率更高.
对应 kvm_irqchip_in_kernel 的情况, qemu 中调用 kvm_riscv_aia_create -> kvm_device_access(aia_fd, KVM_DEV_RISCV_AIA_GRP_CTRL, KVM_DEV_RISCV_AIA_CTRL_INIT, NULL, true, NULL); 对aia 进行初始化, 初始化后
kvm->arch.aia.initialized = true;

在vcpu 退出到kvm后, 下一轮进入guest 前, 会对imsic 的状态进行更新

kvm_riscv_vcpu_aia_update

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
-+ kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)
\ -+ while (ret > 0) {
\ - kvm_riscv_vcpu_aia_update(vcpu);
| - kvm_riscv_vcpu_enter_exit(vcpu);
| - kvm_riscv_vcpu_exit(vcpu, run, &trap);

-+ kvm_riscv_vcpu_aia_update(struct kvm_vcpu *vcpu)
\ -+ kvm_riscv_vcpu_aia_imsic_update(vcpu);
\ - kvm_vcpu_aia *vaia = &vcpu->arch.aia_context;
| - imsic = vaia->imsic_state;
| - old_hgei = imsic->hgei;
| - old_hgei_cpu = imsic->hgei_cpu;
| -|+ if (old_hgei_cpu != vcpu->cpu) "初次分配 IMSIC VS-file"
\ - hgctrl = per_cpu_ptr(&aia_hgei, cpu)
| - ret = __ffs(hgctrl->free_bitmap); "返回最低位之后第一个1的位置"
\ -+ kvm_riscv_aia_alloc_hgei(vcpu->cpu, vcpu,
&new_vsfile_va, &new_vsfile_pa); "分配新的IMSIC VS-file"
\ - lc = imsic_get_local_config(cpu); "硬件 imsic 支持, lc 会传回imsic 驱动中的 imsic_local_config"
| - *hgei_va = lc->msi_va + (ret * IMSIC_MMIO_PAGE_SZ);
| - *hgei_pa = lc->msi_pa + (ret * IMSIC_MMIO_PAGE_SZ); "硬件imsic 的guest interrupt file的mmio地址?"
| - new_vsfile_hgei = ret;
| - imsic_vsfile_local_clear(new_hgei, imsic->nr_hw_longs); "清零 IMSIC VS-file"
| - kvm_riscv_gstage_ioremap(kvm, gpa:vcpu->arch.aia_context.imsic_addr, "建立imsic gpa -> hpa 的 G-stage 页表"
hpa:new_vsfile_pa, IMSIC_MMIO_PAGE_SZ, true, true);
"arch.aia.imsic_addr 是由qemu 传过来的, 配置的 memmap[VIRT_IMSIC_S].base"
| - imsic->vsfile_cpu = vcpu->cpu; "imsic 关联物理cpu"
| - imsic->vsfile_va = new_vsfile_va;
| - imsic->vsfile_pa = new_vsfile_pa;
| -+ imsic_swfile_read(vcpu, clear: true, &tmrif); "/* Read and clear register state from IMSIC SW-file */"
"读入 tmrif 临时内存"
\ -+ memcpy(mrif, imsic->swfile, sizeof(*mrif)); "将 imsic->swfile 拷贝给tmrif"
| -+ imsic_vsfile_local_update(new_vsfile_hgei, imsic->nr_hw_eix, &tmrif);
\ - new_hstatus |= ((unsigned long)vsfile_hgei) << HSTATUS_VGEIN_SHIFT;
"更新 hstatus, 注意hstatus.vgein, vsfile_hgei为 vcpu 在 cpu->free_bitmap 的占位"
| - csr_write(CSR_HSTATUS, new_hstatus);
| -+ for (i = 0; i < nr_eix; i++) "nr_eix = imsic->nr_hw_eix 为 eip/eie 的数目, 遍历eip/eie"
\ - eix = &mrif->eix[i];
| -+ imsic_eix_set(IMSIC_EIP0 + i * 2, eix->eip[0]);
"从 imsic->swfile 拷贝到的临时内存中取出eip的值, 最终通过 CSR_VSISELECT CSR_VSIREG 更新真实imsic 中断控制器的 eip,
下eie 的设置同eip"
\ - csr_write(CSR_VSISELECT, __c);
| - csr_set(CSR_VSIREG, __v);
| - imsic_eix_set(IMSIC_EIE0 + i * 2, eix->eie[0]);
| - csr_write(CSR_HSTATUS, old_hstatus); "这个地方又把hstatus 设置回old_hstatus"

简单分析下上面函数的几个重点:

  1. hstatus.vgein的设置, 为物理hart 分配vcpu时, 会在其关联的 free_bitmap 中分配占位, 而该占位的index 可以视为 vcpuid, 将vcpuid 最终存到了hstatus.vgein下
    更新物理imsic 中断控制器的eip/eie 前设置了新的hstatus.vgein, 更新完后又恢复到old hstatus.vgein, 正好证明imsic 控制器的guest interrupt file 是由hstatus.vgein 片选的.
  2. 注意软件维护的imsic->swfile 内存, imsic 数据结构是关联到vcpu的, 而swfile 代表的是软件维护的备份的 guest interrupt file 内存, 设置硬件imsic 的eip/eie 的数据来源为swfile.
  3. 该函数中通过 kvm_riscv_gstage_ioremap 设置了一次 gpa->hpa 的 G-stage 页表, gpa 为 vcpu->arch.aia.imsic_addr, hpa 为从硬件关联的 imsic 中断控制器(drivers/irqchip/riscv-imsic.c) 中 imsic_get_local_config 取到的 msi_pa + vcpu(guest_index) 的 guest interrupt file 的 mmio 地址, 建立了 G-stage 页表后, guest os 访问 qemu 模拟的 imsic 控制器的 mmio 地址就能直接访问到真实的 imsic 的 guest interrupt file 的 mmio 地址上了? 这一块还是有写疑问, 需要后面 check #TODO 这个地方进一步调研后发现建立的 io G-stage 页表并没有带有 PTE_U 的 flag, 而根据 SPEC 的行为, 没有 PTE_U 时, load/store 操作 gva 时会导致 G-stage 翻译失败, 陷入 LOAD/STORE guest page fault. 也就是说 guest os 直接访问 imsic 的地址时, 并没有因为映射了 G-stage 页表就会通过 mmu 的 2-stage 地址转换, 反而还是会陷入 kvm. 那么设置 G-stage 页表的作用是什么呢? #TODO

接下来我们看下更新 kvm 为 vcpu 维护的 imsic->swfile.

guest 有三种方式设置直通的中断

  • guest 通过 csrw/csrr 访问 siselect sireg 设置 eip/eie.
  • guest 通过直接操作 imsic 控制器的 mmio 地址设置 eip/eie, 访问 seteipnum 地址触发中断.
  • 直通的 pcie 设备(走 vfio 驱动) 通过发送 msi 消息触发 msi 的中断.

guest 访问 siselect sireg

这种方式同 访问stopei, guest os 在 csrr/csrw sireg 陷入kvm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| -  new_val = rs1_val;
| - isel = csr_read(CSR_VSISELECT) & ISELECT_MASK; "陷入到kvm后, guest的siselect 转换为host的 viselect,
不用额外处理 guest 中 siselect 的读写?"
-+ kvm_riscv_vcpu_aia_imsic_rmw(struct kvm_vcpu *vcpu, unsigned long isel,
unsigned long *val, unsigned long new_val,
unsigned long wr_mask)
\ - imsic = vcpu->arch.aia_context.imsic_state;
| -+ imsic_mrif_rmw(imsic->swfile, imsic->nr_eix, isel, val, new_val, wr_mask);
\ - switch (isel)
| -+ case IMSIC_EIP0 ... IMSIC_EIP63:
\ - pend = true;
| - num = isel - IMSIC_EIP0;
| - eix = &mrif->eix[num / 2];
| - ei = (pend) ? &eix->eip[0] : &eix->eie[0];
| -+ imsic_mrif_atomic_rmw(imsic->swfile, ei, new_val, wr_mask); "更新imsic->swfile 对应的 eipx 状态"
| - "... 省略eie的处理, 过程同eip"

最终将 eip/eie 的状态保存了kvm维护的vcpu->imsic->swfile中, vcpu 下一轮进入guest 中通过前面介绍的kvm_riscv_vcpu_aia_update 更新到硬件imsic相应的guest interrupt file中.

访问 imsic mmio 由kvm 注入中断

guest 通过访问imsic mmio 地址, 通过 SETIPNUM 地址向某一个vcpu 设置中断.
doorbell 机制, vcpu 没在运行, 此时通过访问 imsic 的 mmio 地址通过 SETIPNUM 向某一个 vcpu 触发中断.

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
----->qemu<-------
-+ kvm_riscv_aia_create( "kvm_irqchip_in_kernel 时"
aplic_s, msimode, socket,
VIRT_IRQCHIP_NUM_SOURCES,
hart_count,
memmap[VIRT_APLIC_S].base + socket * memmap[VIRT_APLIC_S].size,
memmap[VIRT_IMSIC_S].base + socket * VIRT_IMSIC_GROUP_MAX_SIZE);
\ - aia_fd = kvm_create_device(kvm_state, KVM_DEV_TYPE_RISCV_AIA, false);
| -+ for (int i = 0; i < hart_count; i++)
\ -+ kvm_device_access(aia_fd, KVM_DEV_RISCV_AIA_GRP_ADDR, KVM_DEV_RISCV_AIA_ADDR_IMSIC(i),
&imsic_addr, true, NULL);
| -+ kvm_device_access(aia_fd, KVM_DEV_RISCV_AIA_GRP_CTRL, KVM_DEV_RISCV_AIA_CTRL_INIT, NULL, true, NULL);
------> kernel <-----------
\ -+ case KVM_DEV_RISCV_AIA_GRP_CTRL:
\ -+ case KVM_DEV_RISCV_AIA_CTRL_INIT:
\ -+ r = aia_init(dev->kvm);
\ -+ kvm_for_each_vcpu(idx, vcpu, kvm) "遍历kvm的所有vcpu"
\ -+ kvm_riscv_vcpu_aia_imsic_init(vcpu);
\ - imsic = kzalloc(sizeof(*imsic), GFP_KERNEL); "为每个vcpu 分配imsic"
| - swfile_page = alloc_pages(GFP_KERNEL | __GFP_ZERO, get_order(sizeof(*imsic->swfile)));
"分配 swfile 内存"
| - imsic->swfile = page_to_virt(swfile_page);
| -+ kvm_iodevice_init(&imsic->iodev, &imsic_iodoev_ops);
"创建 kvm_io_dev, 读写函数为 imsic_mmio_read imsic_mmio_write"
\ - imsic_iodoev_ops = {.read = imsic_mmio_read, .write = imsic_mmio_write}
| - kvm_io_bus_register_dev(kvm, KVM_MMIO_BUS, vcpu->arch.aia_context.imsic_addr,
KVM_DEV_RISCV_IMSIC_SIZE, &imsic->iodev); "将该设备加到 KVM_MMIO_BUS 总线上
地址范围为 [imsic_addr , imsic_addr+ KVM_DEV_RISCV_IMSIC_SIZE]"

"guest 访问 gpa 为 [imsic_addr , imsic_addr+ KVM_DEV_RISCV_IMSIC_SIZE] 的地址范围时, 应该是SETIPNUM 地址
会先由kvm 查找 KVM_MMIO_BUS 上的地址范围, 落在其内, 最终转发给其对应的读写函数, 具体过程参考arm的vgic的处理流程"
1. ->
-+ imsic_mmio_write(struct kvm_vcpu *vcpu, struct kvm_io_device *dev, gpa_t addr, int len, const void *val)
\ - msi.address_hi = addr >> 32;
| - msi.address_lo = (u32)addr;
| - msi.data = *((const u32 *)val);
| -+ kvm_riscv_aia_inject_msi(vcpu->kvm, &msi);
\ - target = (((gpa_t)msi->address_hi) << 32) | msi->address_lo;
| - tppn = target >> IMSIC_MMIO_PAGE_SHIFT;
| - iid = msi->data; "中断号"
| - g = tppn & (BIT(aia->nr_guest_bits) - 1); "通过 imsic 的mmio地址判断 给到哪个 guest interrupt file, 即算出 guest index"
| -+ kvm_for_each_vcpu(idx, vcpu, kvm) "遍历kvm的所有vcpu"
\ - ippn = vcpu->arch.aia_context.imsic_addr >> IMSIC_MMIO_PAGE_SHIFT;
"vcpu的imsic_addr 是特定的, 这个地址是前面 qemu 通过 KVM_DEV_RISCV_AIA_GRP_ADDR 发到kvm的"
| -|+ if (ippn == tppn)
\ - toff = target & (IMSIC_MMIO_PAGE_SZ - 1); "toff 代表 IMSIC_MMIO_SETIPNUM_BE 还是 IMSIC_MMIO_SETIPNUM_LE"
\ -+ kvm_riscv_vcpu_aia_imsic_inject(vcpu, guest_index:g, offset:toff, iid);
\ - iid = (offset == IMSIC_MMIO_SETIPNUM_BE) ? __swab32(iid) : iid;
| - eix = &imsic->swfile->eix[iid / BITS_PER_TYPE(u64)];
| - set_bit(iid & (BITS_PER_TYPE(u64) - 1), eix->eip); "由中断号 iid 定位到对应的eip, 设置swfile的eip"
| -|+ if (imsic->vsfile_cpu >= 0)
\ - writel(iid, imsic->vsfile_va + IMSIC_MMIO_SETIPNUM_LE); "直接写host 真实的中断控制器的 vs interrupt file的内存, 触发vs中断."
| - kvm_vcpu_kick(vcpu);
| -|+ else
\ -+ imsic_swfile_extirq_update(vcpu);
\ - imsic_mrif_topei(imsic->swfile, imsic->nr_eix, imsic->nr_msis))
"更新 vcpu的 imsic->swfile 的 stopei 为对应的中断号和优先级"
| -+ kvm_riscv_vcpu_set_interrupt(vcpu, IRQ_VS_EXT); "直接由hvip 给vcpu 注入中断"
\ - set_bit(irq, vcpu->arch.irqs_pending);
| - kvm_vcpu_kick(vcpu); "将vcpu 踢出来, 退出到kvm, 下一轮进guest 时会通过 kvm_riscv_vcpu_aia_update 函数
将swfile 更新到硬件中断控制器的guest interrupt file 中"

第一条路径更适合于其他vcpu 向某一个vcpu 注入直通的中断

  • 如果可以直接拿到真实 imsic virtual interrupt file 的地址, 该地址是从 host imsic 驱动中拿到的该 vcpu 对应的 cpu 上的 virtual interrupt file 地址, 则可以直接写 SETIPNUM 触发相应的 guest 直通中断.
  • 如拿不到真实 imsic virtual interrupt file 地址, 则更新中断到了 vcpu->imsic->swfile 内存中, 而 kvm_vcpu_kick 将 vcpu 踢出来, 退出到 kvm, 下一轮进 guest 时会通过 kvm_riscv_vcpu_aia_update 函数将 vcpu->imsic->swfile 状态更新到硬件中断控制器的 guest interrupt file 中

值得注意的是, 经过调试发现, guest os 中发送 ipi 中断 (imsic_ipi_send 函数), 也会经过上述路径最终写入到了 virtual interrupt file 的 SETIPNUM, 触发了对应 vcpu 的 guest 中断, 但是最终还是走了 kvm_vcpu_kick 将对应 vcpu 从 sleep 或 guest 运行状态中拖出到 kvm 下? 如果 vcpu 处在 guest 运行时, 不是可以直接触发直通中断吗, 为什么还需要 kick 呢? 这一点不太理解 #TODO

1
2
3
4
5
6
7
8
9
10
11
12
13
2. ->
-+ case KVM_SIGNAL_MSI:
\ -+ kvm_send_userspace_msi(kvm, &msi);
\ - route.msi.address_lo = msi->address_lo;
| - route.msi.address_hi = msi->address_hi;
| - route.msi.data = msi->data;
| - route.msi.devid = msi->devid;
| -+ kvm_set_msi(&route, kvm, KVM_USERSPACE_IRQ_SOURCE_ID, 1, false)
\ - msi.address_lo = e->msi.address_lo;
| - msi.address_hi = e->msi.address_hi;
| - msi.data = e->msi.data;
| - msi.devid = e->msi.devid;
| -+ kvm_riscv_aia_inject_msi(kvm, &msi);

第二条路径中, user_space 通过ioctl(KVM_SIGNAL_MSI) 发送msi 消息过来注册中断, 这个通常是pcie设备的模拟, pcie 设备注册中断时会通过该路径注册中断.

imsic doorbell 中断

在 aia_hgei_init 函数中, 注册了IRQ_S_GEXT (12)的中断处理函数 hgei_interrupt, 在IRQ_S_GEXT 的中断到来时进入 hgei_interrupt 进行处理, 读对应的直通guest中断映射的vcpuid, 唤醒对应的vcpu进入 KVM_RUN的循环中, 进而处理对应的直通中断.
处理imsic->swfile的pending, 注入中断到虚拟机.

1
2
3
4
5
6
7
8
9
10
-+ kvm_riscv_aia_init
\ -+ aia_hgei_init
\ -+ domain = irq_find_matching_fwnode(riscv_get_intc_hwnode(), DOMAIN_BUS_ANY);
\ - hgei_parent_irq = irq_create_mapping(domain, IRQ_S_GEXT); "hwirq 映射的 virq"
| - request_percpu_irq(hgei_parent_irq, hgei_interrupt, "riscv-kvm", &aia_hgei); "doorbell的中断处理函数为 hgei_interrupt"
-+ hgei_interrupt
\ - hgei_mask = csr_read(CSR_HGEIP) & csr_read(CSR_HGEIE);
| - csr_clear(CSR_HGEIE, hgei_mask); "清理hgeie"
| -+ for_each_set_bit(i, &hgei_mask, BITS_PER_LONG)
\ - kvm_vcpu_kick(hgctrl->owners[i]) "hgei_mask 代表vcpuid, 踢vcpu 到 KVM_RUN 循环中, 紧接着处理对应的异常"

qemu MSI/MSIX 信息

当guest os kernel 写入vfio-device的配置空间配置msi/msix时 (向msi/msi-x的capability structure中写入msi/msi-x enable bit), 会陷入到kvm mmio_exit, 进而陷入到qemu中提前注册的处理函数 vfio_pci_write_config
最终下发ioctl KVM_SET_GSI_ROUTING 发给kvm, kvm 经由imsic 注册对应的 kvm_kernel_irq_routing_entry, msi 类型, 其entry的 set 设置为 kvm_set_msi
这一层为 guest的pcie 设备启用msi的中断

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
memory_region_init_io(&e->mmio, OBJECT(e), &pcie_mmcfg_ops, e, "pcie-mmcfg-mmio", PCIE_MMCFG_SIZE_MAX);
static const MemoryRegionOps pcie_mmcfg_ops = {
.read = pcie_mmcfg_data_read,
.write = pcie_mmcfg_data_write,
.endianness = DEVICE_LITTLE_ENDIAN,
};
-+ pci_host_config_write_common(pci_dev, addr, limit, val, len);
\ -+ pci_host_config_write_common(pci_dev, addr, limit, val, len);
\ -+ pci_dev->config_write(pci_dev, addr, val, MIN(len, limit - addr));
\ -+ vfio_pci_write_config(pci_dev, addr, val, MIN(len, limit - addr));
\ -|+ if (pdev->cap_present & QEMU_PCI_CAP_MSI
\ -+ vfio_msi_enable(vdev);
\ - vdev->nr_vectors = msi_nr_vectors_allocated(&vdev->pdev);
| -+ for (i = 0; i < vdev->nr_vectors; i++)
\ - VFIOMSIVector *vector = &vdev->msi_vectors[i];
| -+ vfio_add_kvm_msi_virq(vdev, vector, i, false);
\ -+ vector->virq = kvm_irqchip_add_msi_route(&vfio_route_change, vector_n, &vdev->pdev)
\ - virq = kvm_irqchip_get_virq(s);
| - kroute.gsi = virq;
| - kroute.type = KVM_IRQ_ROUTING_MSI;
| - kroute.u.msi.address_lo = (uint32_t)msg.address;
| - kroute.u.msi.data = le32_to_cpu(msg.data);
| - kroute.u.msi.devid = pci_requester_id(dev);
| -+ kvm_add_routing_entry(s, &kroute); "将kroute 写到 KVMState 中"
| - c->changes++;
| -+ vfio_commit_kvm_msi_virq_batch(vdev);
\ -+ kvm_irqchip_commit_route_changes(&c);
\ -|+ if c->changes
\ -+ kvm_irqchip_commit_routes(c->s);
\ - kvm_vm_ioctl(s, KVM_SET_GSI_ROUTING, s->irq_routes);
--------->kernel<-----------------
-+ case KVM_SET_GSI_ROUTING:
\ -|+ if kvm_arch_can_set_irq_routing
\ -+ kvm_set_irq_routing(kvm, entries, routing.nr, routing.flags);
\ -+ for (i = 0; i < nr; ++i)
\ -+ setup_routing_entry(kvm, new, e, ue);
\ -+ kvm_set_routing_entry(kvm, e, ue);
\ - switch (ue->type)
| -+ case KVM_IRQ_ROUTING_MSI:
\ - e->set = kvm_set_msi;
"entry的set函数设置为 kvm_set_msi, 进而接入 aia的中断注册函数"
| - e->msi.address_lo = ue->u.msi.address_lo; "标记地址"
| - e->msi.address_hi = ue->u.msi.address_hi;
| - e->msi.data = ue->u.msi.data; "标记eventid"
| - e->msi.devid = ue->u.msi.devid; "标记 deviceid"
| -+ for (i = 0; i < vdev->nr_vectors; i++)
\ -+ vfio_connect_kvm_msi_virq(&vdev->msi_vectors[i]);
\ - event_notifier_init(&vector->kvm_interrupt, 0) "注册kvm_interrupt的eventfd, 不激活"
| -+ kvm_irqchip_add_irqfd_notifier_gsi(kvm_state, n:&vector->kvm_interrupt, NULL, vector->virq)
\ -+ kvm_irqchip_assign_irqfd(s, event:n, rn, virq, true);
\ - fd = event_notifier_get_fd(event);
| - irqfd = {.fd = fd, .gsi = virq, } "从eventfd 构建 irqfd"
| -+ kvm_vm_ioctl(s, KVM_IRQFD, &irqfd)
------------------->kernel<----------------------------
\ -+ case KVM_IRQFD
\ -+ kvm_irqfd(struct kvm *kvm, struct kvm_irqfd *args)
\ -+ kvm_irqfd_assign(kvm, args);
\ - irqfd->gsi = args->gsi; "gsi 就是 virq"
| - INIT_WORK(&irqfd->inject, irqfd_inject);
"初始化工作队列, 该eventfd对应的处理函数为 irqfd_inject"
| - eventfd = eventfd_ctx_fileget(f.file); "fd 分配eventfd"
| - irqfd->eventfd = eventfd;
| - events = vfs_poll(f.file, &irqfd->pt); "在fd上poll 等待事件"
| -|+ if (events & EPOLLIN)
\ -+ schedule_work(&irqfd->inject); "只考虑有数据,即POLLIN的情形, 调用inject 回调"
\ -+ irqfd_inject(struct work_struct *work)
\ - irqfd = container_of(work, struct kvm_kernel_irqfd, inject);
| - struct kvm *kvm = irqfd->kvm; "找到对应的虚拟机实例"
| - "irqfd_inject函数用来注入使用中断控制器芯片的中断类型,
if判断中断是边沿触发还是水平触发,如果是边沿触发则会调用两次kvm_set_irq"
| -+ kvm_set_irq(kvm, KVM_IRQFD_RESAMPLE_IRQ_SOURCE_ID, irqfd->gsi, 1, false);
\ - i = kvm_irq_map_gsi(kvm, irq_set, irq = irqfd->gsi);
"获取该gsi索引上注册的中断路由项(kvm_kernel_irq_routing_entry),
这个中断路由项是由arch的 kvm_set_routing_entry 注册的"
| -+ while i--
\ - irq_set[i].set(&irq_set[i], kvm, irq_source_id, level, line_status);
"会挨个调用每个中断路由项上的set方法触发中断"
| -+ vfio_enable_vectors(vdev, msix:false);
"构造irq_set, 准备发送给kernel的vfio驱动"
\ - irq_set->flags = VFIO_IRQ_SET_DATA_EVENTFD | VFIO_IRQ_SET_ACTION_TRIGGER;
| - irq_set->index = VFIO_PCI_MSI_IRQ_INDEX
| - fds = (int32_t *)&irq_set->data;
| -+ for (i = 0; i < vdev->nr_vectors; i++)
\ -+ fd = event_notifier_get_fd(&vdev->msi_vectors[i].kvm_interrupt)
| - fds[i] = fd;
| -+ ioctl(vdev->vbasedev.fd, VFIO_DEVICE_SET_IRQS, irq_set); "发起 VFIO_DEVICE_SET_IRQS"
----------->kernel vfio驱动<---------------
\ -+ case VFIO_DEVICE_SET_IRQS
\ -+ vfio_pci_ioctl_set_irqs(vdev, uarg);
"irq_set 的内容填到hdr中"
\ -+ vfio_pci_set_irqs_ioctl(vdev, hdr.flags, hdr.index, hdr.start, hdr.count, data);
\ - switch (index)
| -+ case VFIO_PCI_MSI_IRQ_INDEX:
\ - switch (flags & VFIO_IRQ_SET_ACTION_TYPE_MASK)
| -+ case VFIO_IRQ_SET_ACTION_TRIGGER
\ -+ func = vfio_pci_set_msi_trigger;
| -+ func: vfio_pci_set_msi_trigger(vdev, index, start, count, flags, data);
\ -|+ if (flags & VFIO_IRQ_SET_DATA_EVENTFD)
"vdev->irq_type = VFIO_PCI_MSI_IRQ_INDEX 时"
\ -+ vfio_msi_set_block(vdev, start, count, fds, msix);
\ -+ for (i = 0, j = start; i < count && !ret; i++, j++)
\ - fd = fds[i]
| -+ vfio_msi_set_vector_signal(vdev, j, fd, msix:false);
\ - irq = pci_irq_vector(pdev, vector);
| - trigger = eventfd_ctx_fdget(fd); "获取eventfd"
| - vdev->ctx[vector].name = kasprintf(GFP_KERNEL_ACCOUNT, "vfio-msi%s[%d](%s)",
msix ? "x" : "", vector, pci_name(pdev)); "格式化输出中断名"杯盘占比扩大一定是青光眼吗
| -+ request_irq(irq, vfio_msihandler, 0, vdev->ctx[vector].name, trigger);
"申请中断, irq 为 virq"
"中断处理函数为 vfio_msihandler, trigger 为eventfd"
\ -----+ vfio_msihandler(trigger) "中断到来时, 来到该函数处理"
\ -+ eventfd_signal(trigger, 1); "触发eventfd"
\ ----+ irqfd_inject() "触发 kvm对端 在该eventfd 上poll的线程, 激活其 工作队列, 最终走到 irqfd_inject 函数"
\ - "省略...."
| -+ irq_set[i].set(&irq_set[i], kvm, irq_source_id, level, line_status); "会挨个调用每个中断路由项上的set方法触发中断"
\ -+ e->set: kvm_set_msi(struct kvm_kernel_irq_routing_entry *e, kvm, irq_source_id, level, line_status)
\ -+ kvm_riscv_aia_inject_msi(kvm, &msi); "最终走到 imsic的 中断注入流程"

在 irgfd_inject 中回调 e->set 即 kvm_set_msi 函数, 最终走入 kvm_riscv_aia_inject_msi 中触发对应的中断.
irqfd_inject 函数通过将中断信号发送到指定的IRQ文件描述符来向虚拟机注入中断。虚拟机将接收到该中断信号,并根据中断的类型和处理程序进行相应的处理。

关注下 irqfd 的触发方, 发起方是vfio 注册的中断处理函数
在 vfio 驱动中, 为pcie的每个 vector 注册了一个中断, 中断处理函数为 vfio_msihandler
在 guest 启用 pcie 设备启用该vector 上的中断时, 会因设置pcie的配置空间最终走到 vfio_msi_enable 函数中.

  • 首先为每个vector 注册了一个 irq_entry, 每个entry的set 函数设置为了 kvm_set_msi 函数
  • 为每个vector 注册了一个irqfd, 并开启了一个 work 工作队列, 等待eventfd poll 事件到来, 等到之后进入 irqfd_inject 函数处理
  • 为每个vector 申请了一个中断, 中断处理函数为 vfio_msihandler, 中断名”vfio-msi[i]”
  • 在pcie 设备因中断到来时, 先触发host kernel的该函数, 写vector绑定的eventfd
  • 写vector绑定的eventfd 会导致 irqfd 对应的work 工作队列被激活, 最终进入 irqfd_inject 函数路由相关的中断entry, 最终走到 imsic的中断注入函数中 kvm_set_msi->kvm_riscv_aia_inject_msi
  • kvm_riscv_aia_inject_msi 会解析msi消息, 判断target 是给到哪个vcpu的, 访问的是不是 setipnum 地址, 如果是setipnum, 则更新swfile->stopei, 更新swfile eip, 开启对应的guest 直通的中断, hvip 注入中断给vcpu, 将vcpu 唤醒