内存虚拟化
涉及到两个主题:
GPA->HVA
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_CREATE
,KVM_MR_MOVE
,KVM_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
用于更新整个memslots
,memslots
基于 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, -1 UL, 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 & 0 x3) ; "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调度算法有三个比较著名:
O(1) scheduler
Comletely Fair Scheduler(CFS)
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, ®) ; \ -+ 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_timer
和kvm_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来实现中断系统
首先cpu有gpio_in 接口
中断控制器 sifive_plic 有gpio_in 和 gpio_out, gpio_out 和 cpu 的gpio_in 接口关联
设备的gpio_out 和 sifive_plic的gpio_in 接口关联
当有中断发生时, 设备通过gpio_out 通知 sifive_plic, sifive_plic 通过gpio_out 通知 cpu的gpio_in
cpu的gpio_in 的中断处理函数riscv_cpu_set_irq
处理最终的中断, 并将中断状态通过kvm ioctl(KVM_INTERRUPT)
命令通知kvm, 由kvm完成最终的中断注入流程, 更新hvip
进入guest os后(V=1), hvip 注入中断会导致 V=1 时sip 置位, 如果sie相应位置位了, vcpu 会跳到中断入口, guest os 进而处理中断
中断注册(使能)流程 不支持设备直通时, 外设都是qemu 模拟出来的, 外设注册中断也是通过 模拟的中断控制器注册的中断 guest 在request_irq 的最后会通过设置cpu 亲和性使能特定的中断.
使能的逻辑最终会陷入到 sifive_plic_write
enable 区域
1 2 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=0 xaaaaaadc047c80, irq=9 , level=1 ) at ../target/riscv/cpu.c:695 #1 0 x00aaaaaadb76d1d8 in qemu_set_irq (irq=0 xaaaaaadc05e670, level=1 ) at ../hw/core/irq.c:45 #2 0 x00aaaaaadb645f60 in sifive_plic_update (plic=0 xaaaaaadc06aea0) at ../hw/intc/sifive_plic.c:121 #3 0 x00aaaaaadb646bc0 in sifive_plic_irq_request (opaque=0 xaaaaaadc06aea0, irq=10 , level=0 ) at ../hw/intc/sifive_plic.c:316 #4 0 x00aaaaaadb76d1d8 in qemu_set_irq (irq=0 xaaaaaadc06c880, level=0 ) at ../hw/core/irq.c:45 #5 0 x00aaaaaadb28c880 in qemu_irq_lower (irq=0 xaaaaaadc06c880) at /home/liguang/program/3 rdparty/buildroot-2022.08 .1 /output/build/qemu-7.0 .0 /include/hw/irq.h:17 #6 0 x00aaaaaadb28cfcc in serial_update_irq (s=0 xaaaaaadc3c47b0) at ../hw/char/serial.c:144 #7 0 x00aaaaaadb28d654 in serial_xmit (s=0 xaaaaaadc3c47b0) at ../hw/char/serial.c:251 #8 0 x00aaaaaadb28dc04 in serial_ioport_write (opaque=0 xaaaaaadc3c47b0, addr=0 , val=103 , size=1 ) at ../hw/char/serial.c:359 #9 0 x00aaaaaadb28f268 in serial_mm_write (opaque=0 xaaaaaadc3c4490, addr=0 , value=103 , size=1 ) at ../hw/char/serial.c:1009 #10 0 x00aaaaaadb5dca84 in memory_region_write_accessor (mr=0 xaaaaaadc3c4920, addr=0 , value=0 xffffff8da80008, size=1 , shift=0 , mask=255 , attrs=...) at ../softmmu/memory.c:492 #11 0 x00aaaaaadb5dcdc8 in access_with_adjusted_size (addr=0 , value=0 xffffff8da80008, size=1 , access_size_min=1 , access_size_max=8 , access_fn=0 xaaaaaadb5dc970 <memory_region_write_accessor>, mr=0 xaaaaaadc3c4920, attrs=...) at ../softmmu/memory.c:554 #12 0 x00aaaaaadb5e04c4 in memory_region_dispatch_write (mr=0 xaaaaaadc3c4920, addr=0 , data=103 , op=MO_8, attrs=...) at ../softmmu/memory.c:1514 #13 0 x00aaaaaadb5f0df0 in flatview_write_continue (fv=0 xaaaaaadc42f380, addr=268435456 , attrs=..., ptr=0 xffffff8d27f028, len=1 , addr1=0 , l=1 , mr=0 xaaaaaadc3c4920) at ../softmmu/physmem.c:2814 #14 0 x00aaaaaadb5f0fb4 in flatview_write (fv=0 xaaaaaadc42f380, addr=268435456 , attrs=..., buf=0 xffffff8d27f028, len=1 ) at ../softmmu/physmem.c:2856 #15 0 x00aaaaaadb5f14b4 in address_space_write (as=0 xaaaaaadbdd8c48 <address_space_memory>, addr=268435456 , attrs=..., buf=0 xffffff8d27f028, len=1 ) at ../softmmu/physmem.c:2952 #16 0 x00aaaaaadb5f1558 in address_space_rw (as=0 xaaaaaadbdd8c48 <address_space_memory>, addr=268435456 , attrs=..., buf=0 xffffff8d27f028, len=1 , is_write=true ) at ../softmmu/physmem.c:2962 #17 0 x00aaaaaadb759448 in kvm_cpu_exec (cpu=0 xaaaaaadc047c80) at ../accel/kvm/kvm-all.c:2929 #18 0 x00aaaaaadb75b1b4 in kvm_vcpu_thread_fn (arg=0 xaaaaaadc047c80) at ../accel/kvm/kvm-accel-ops.c:49 #19 0 x00aaaaaadb983e94 in qemu_thread_start (args=0 xaaaaaadc06ad00) 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/qemu 或 https://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; rwlock_t vsfile_lock; int vsfile_cpu; int vsfile_hgei; void __iomem *vsfile_va; phys_addr_t vsfile_pa; 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"
简单分析下上面函数的几个重点:
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 片选的.
注意软件维护的imsic->swfile 内存, imsic 数据结构是关联到vcpu的, 而swfile 代表的是软件维护的备份的 guest interrupt file 内存, 设置硬件imsic 的eip/eie 的数据来源为swfile.
该函数中通过 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 唤醒