0%

riscv-qemu arch 分析

调试riscv kernel

kernel 版本 5.19.16

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export ARCH=riscv
export CROSS_COMPILE=...bin/...gnu-
make defconfig O=build
make menuconfig O=build

Virtualization -->
<*> Kernel-based Virtual Machine (KVM) Support
Kernel hacking -->
-*- Kernel debugging
Compile-time checks and compiler options -->
Debug information (Generate DWARF Version 4 debuginfo) -->
[*] Compile the kernel with debug info
[*] Provide GDB scripts for kernel debugging
Kernel Features -->
[] Randomize the address of the kernel image "关闭kernel 地址随机化"

debug qemu

#qemu_debug
buildroot 中的qemu 编译时会产生build.ninja文件, 修改build.ninja文件
将全部的-Os -g0 修改为 -O1 -gdwarf-2-O0 -gdwarf-2 即可.

KVM 初始化

riscv kvm_init 的流程
kvm 主要的代码 在 virt/kvm
arch相关的驱动代码在 arch/riscv/kvm
这个部分涉及到 kvm.ko 加载的过程

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
-+ module_init(riscv_kvm_init);
\ -+ kvm_init(NULL, sizeof(struct kvm_vcpu), 0, THIS_MODULE);
\ -+ kvm_arch_init(NULL); "riscv specific"
\ - check riscv_isa_extension_available(NULL, h) "必须打开h-extension"
| - check not sbi_spec_is_0_1() "sbi 版本必须是0.2以上"
| - check sbi_probe_extension(SBI_EXT_RFENCE) "sbi必须有RFENCE extension 支持"
| -+ kvm_riscv_gstage_mode_detect(); "g-stage 页表是用 sv57x4 还是 sv48x4, sv57 模式使用5级页表, sv48使用4级页表"
\ - gstage_mode = (HGATP_MODE_SV48X4 << HGATP_MODE_SHIFT); or SV57X4
| - gstage_pgd_levels = 4; or 5 "四级页表或五级页表"
| - kvm_riscv_local_hfence_gvma_all(); "HFENCE.GVMA 0 0 汇编指令
刷新 tlb 中 G-stage 和 VS-stage 且对最终的HPA 设置过PMP的cache entry
flush all G-stage or VS-stage address-translation cache entries that
have cached PMP settings corresponding to the final translated supervisor physical address."
| - kvm_riscv_gstage_vmid_detect(); "刷新guest tlb, 检查hgatp 寄存器的vmid bits 是否够用"
| - kvm_irqfd_init() "创建工作队列, 用于处理VM的shutdown操作"
| -+ cpuhp_setup_state_nocalls(CPUHP_AP_KVM_STARTING, "kvm/cpu:starting", kvm_starting_cpu, kvm_dying_cpu);
"设置物理cpu 热插拔时的回调函数"
\ - kvm_starting_cpu(unsigned int cpu) "startup 回调函数"
| - kvm_dying_cpu(unsigned int cpu) "teardown 回调函数"
| -+ register_reboot_notifier(&kvm_reboot_notifier); "注册重启时的回调函数"
\ - blocking_notifier_chain_register(&reboot_notifier_list, &kvm_reboot_notifier);
| -+ kvm_reboot_notifier.notifier_call = kvm_reboot "回调函数为 kvm_reboot"
\ - on_each_cpu(hardware_disable_nolock, NULL, 1);
| -+ hardware_disable_nolock(void *junk)
\ -+ kvm_arch_hardware_disable()
\ - csr_write(CSR_HEDELEG, 0);
| - csr_write(CSR_HIDELEG, 0);
| - csr_write(CSR_VSIE, 0);
| - csr_write(CSR_HVIP, 0);
| - kvm_vcpu_cache = kmem_cache_create_usercopy("kvm_vcpu", vcpu_size, vcpu_align, ...)
"创建用于分配kvm_vcpu的slab 高速缓存"
| - kvm_async_pf_init() "创建用于分配kvm_async_pf的slab 高速缓存"
| - misc_register(&kvm_dev); "注册misc字符设备 /dev/kvm 节点"
| -+ register_syscore_ops(&kvm_syscore_ops); "注册suspend/resume 时的回调, 与系统休眠唤醒有关"
\ -|+ kvm_suspend "休眠时挂起VM"
\ - hardware_disable_nolock(NULL); "前面已经讲了"
| -|+ kvm_resume "唤醒时恢复VM"
\ -+ hardware_enable_nolock(NULL)
\ - csr_write(CSR_HEDELEG, EXC_INST_MISALIGNED | EXC_BREAKPOINT | EXC_SYSCALL |
EXC_INST_PAGE_FAULT | EXC_LOAD_PAGE_FAULT | EXC_STORE_PAGE_FAULT);
"riscv 设置HEDELEG 虚拟异常代理"
| - csr_write(CSR_HIDELEG, IRQ_VS_SOFT | IRQ_VS_TIMER | IRQ_VS_EXT);
"riscv 设置 hideleg 虚拟中断代理"
| - csr_write(CSR_HVIP, 0);
| - kvm_vfio_ops_init(); "注册vfio 操作, riscv not implement"

QEMU-KVM 框架

  • Qemu (Quick Emulator):是虚拟化方案的用户态组成部分,它有两种模式:
    • Emulator**,模拟器,模拟各种硬件,使用的是二进制翻译技术;
    • Virtualiser 虚拟机 KVM 模式,通过ioctlKVM内核模块进行交互,完成虚拟化功能
      本篇只讲KVM 模式
  • Qemu为每个VM虚拟机创建一个进程,针对每个vCPU创建一个线程,Guest的系统和应用运行在vCPU之上;
  • Qemu能模拟I/O功能,而这部分功能KVM可能并不是全部支持,执行流程如下
    • 虚拟机VM中的程序执行I/O操作,VM退出进入KVM,KVM进行判断处理并将控制权交给Qemu,由Qemu来模拟I/O设备来响应程序的I/O请求;
  • KVM内核模块,依赖于底层硬件的虚拟化支持,主要的功能是初始化CPU硬件,打开虚拟化模式,将虚拟化客户机运行在虚拟机模式下,并对虚拟化客户机的运行提供一定的支持;
  • KVM内核模块,实现CPU的虚拟化、内存的虚拟化等,而外设IO的虚拟化,通常不由KVM模块负责,只有对性能要求很高的虚拟设备才需要由KVM内核模块来负责,因此也就有KVM + Qemu的组合方案了;

QEMU-KVM 调用 KVM 内核模块启动虚拟机的流程概要

  • 获取 /dev/kvm fd(文件描述符)
  • 创建虚拟机,获取虚拟机的句柄
    KVM_CREATE_VM 可以理解成 KVM 为虚拟机创建了对应的数据结构,然后,KVM 会返回一个文件句柄来代表该虚拟机。
    针对这个句柄执行 ioctl 调用即可完成对虚拟机执行相应的管理,比如:
    • 创建用户空间虚拟地址(Virtual Address)
    • 客户机物理地址GPA(Guest Physical Address)以及主机物理地址HPA(Host Physical Address)之间的映射关系
  • 为虚拟机映射内存和其他的 PCI 设备,以及信号处理的初始化。
    1
    ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &mem);
  • 将虚拟机镜像数据映射到内存,相当于物理机的 boot 过程,把操作系统内核映射到内存。
  • 创建 vCPU,并为 vCPU 分配内存空间。KVM_CREATE_VCPU 时,KVM 为每一个 vCPU 生成对应的文件句柄,对其执行相应的 ioctl 调用,就可以对 vCPU 进行管理
    1
    2
    ioctl(vmfd, KVM_CREATE_VCPU, vcpuid);
    vcpu->kvm_run_mmap_size = ioctl(kvm->dev_fd, KVM_GET_VCPU_MMAP_SIZE, 0);
  • 创建 vCPU 个数的线程并运行虚拟机。
    1
    ioctl(kvm->vcpus->vcpu_fd, KVM_RUN, 0);
  • 线程进入循环,监听并捕获虚拟机退出原因,做相应的处理。这里的退出并不一定指的是虚拟机关机,虚拟机如果遇到 I/O 操作,访问硬件设备,缺页中断等都会执行退出。执行退出可以理解为将 CPU 执行上下文返回到 QEMU。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    open("/dev/kvm")
    ioctl(KVM_CREATE_VM)
    ioctl(KVM_CREATE_VCPU)
    for (;;) {
    ioctl(KVM_RUN)
    switch (exit_reason) { /* 分析退出原因,并执行相应操作 */
    case KVM_EXIT_IO: /* ... */
    case KVM_EXIT_HLT: /* ... */
    }
    }

用户态精简代码简要分析

QEMU与KVM整体架构图
简单看下qemu 和 kvm 的交互, 精简版的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
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <linux/kvm.h>
#include <sys/mman.h>
#include <unistd.h>

int main() {
printf("main\n");
struct kvm_sregs sregs;
int ret;
int kvmfd = open("/dev/kvm", O_RDWR);
ioctl(kvmfd, KVM_GET_API_VERSION, NULL);
int vmfd = ioctl(kvmfd, KVM_CREATE_VM, 0);
unsigned char *ram = mmap(NULL, 0x5000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
int kfd = open("/root/image.bin", O_RDONLY); //use virt baremental bin
read(kfd, ram, 0x5000);
struct kvm_userspace_memory_region mem = {
.slot = 0,
.guest_phys_addr = 0,
.memory_size = 0x5000,
.userspace_addr = (unsigned long) ram,
};
ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &mem);
int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, 0);
int mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);
printf("mmap_size %d\n", mmap_size);
struct kvm_run *run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
ret = ioctl(vcpufd, KVM_GET_SREGS, &sregs); // riscv not implement
sregs.cs.base = 0;
sregs.cs.selector = 0;
ret = ioctl(vcpufd, KVM_SET_SREGS, &sregs); // riscv not implement
struct kvm_regs regs = {
.rip = 0,
};
ret = ioctl(vcpufd, KVM_SET_REGS, &regs); // riscv not implement
while(1) {
ret = ioctl(vcpufd, KVM_RUN, NULL);
if(ret == -1) {
printf("exit unknown\n");
return -1;
}
switch(run->exit_reason) {
case KVM_EXIT_HLT:
puts("KVM_EXIT_HLT");
return 0;
case KVM_EXIT_IO:
putchar(*((char*)run) + run->io.data_offset);
break;
case KVM_EXIT_FAIL_ENTRY:
puts("entry error");
return -1;
case KVM_EXIT_MMIO: // need handle mmio, otherwize will loop mmio_return
// handle_mmio() ...
ret = 0;
break;
default:
puts("other error");
printf("exit reason: %d\n", run->exit_reason);
return -1;
}
}
return 0;
}

下面简单拆解下上面都做了什么?

ioctl KVM_CREATE_VM - 创建虚拟机,获取虚拟机的句柄

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-+ kvm_dev_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg)
\ -+ case KVM_CREATE_VM "ioctl 为KVM_CREATE_VM"
\ -+ kvm_dev_ioctl_create_vm(arg);
\ -+ kvm_create_vm(type); "创建kvm 结构体" 见下面分析 -->
| - snprintf(kvm->stats_id, sizeof(kvm->stats_id), "kvm-%d", task_pid_nr(current)); "创建status_id"
| - r = get_unused_fd_flags(O_CLOEXEC); "找可用的fd , 关联下面创建的file 结构体, 该fd 就代表了虚拟机"
| -+ file = anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR); "创建一个隐式文件代表该虚拟机"
\ - kvm_vm_fops.release = kvm_vm_release "注意kvm_vm_fops, 为该文件指定了 fops"
| - .unlocked_ioctl = kvm_vm_ioctl,
| - .llseek        = noop_llseek,
| - kvm_create_vm_debugfs(kvm, r) "为该文件设置debugfs"
| - kvm_uevent_notify_change(KVM_EVENT_CREATE_VM, kvm); "为虚拟机创建构造 uevent 消息, 用户空间可以收到通知"
| - fd_install(r, file); "安装 file 结构体, 将创建的文件对象指针存到该进程的打开文件数组中"
| - return r; "返回给用户空间fd"
| - return r

kvm_create_vm

1
2
3
4
5
6
-+ kvm_create_vm(type)
\ -+ kvm = kvm_arch_alloc_vm()
| - kvm_eventfd_init(kvm); "注册eventfd"
| - INIT_LIST_HEAD(&kvm->devices); "初始化 devices 链表"
| -+ kvm_arch_init_vm(kvm, type); "调用arch 指令集的specific init_vm" 见下面分析 -->
| - list_add(&kvm->vm_list, &vm_list); "加入vm_list 链表"

上面过程写的比较粗, 该函数是通用函数, 重点是分配了kvm 结构体, 将指针传给了特定指令集的kvm_arch_init_vm函数, 该函数会对kvm做进一步处理

kvm_arch_init_vm

接下来详细看下riscvkvm_arch_init_vm做了什么

1
2
3
4
5
6
7
8
9
10
11
-+ kvm_arch_init_vm(struct kvm *kvm, unsigned long type)
\ -+ kvm_riscv_gstage_alloc_pgd(kvm) "PGD 第一级, 也叫页全局目录, 还有PUD (二级) PMD (三级) PTE"
\ - pgd_page = alloc_pages(GFP_KERNEL | __GFP_ZERO, get_order(gstage_pgd_size));
"gstage_pgd_size 为 1 << (12+2), 分配了连续的4个页"
| - kvm->arch.pgd = page_to_virt(pgd_page); "记录虚拟页地址"
| - kvm->arch.pgd_phys = page_to_phys(pgd_page); "记录物理页号"
| -+ kvm_riscv_gstage_vmid_init(kvm); "为当前vm 分配vmid"
\ - kvm->arch.vmid.vmid = 0
| -+ kvm_riscv_guest_timer_init(kvm) "初始化guest timer"
\ - riscv_cs_get_mult_shift(&gt->nsec_mult, &gt->nsec_shift); "从riscv_clocksource 处获得ns 单位及偏移"
| - gt->time_delta = -get_cycles64();

返回用户空间的fd, 用户空间可以做什么

调用ioctl(KVM_CREATE_VM) 后返回给用户空间fd, 该fd 为 vmfd
具体代码可以参考qemu
accel->kvm->kvm_all.c#kvm_init 函数

1
2
fd = open("/dev/kvm")
s->vmfd = ioctl(KVM_CREATE_VM)

kernel kvm 为该vmfd 安装了 fops 为 kvm_vm_fops

1
2
3
4
5
6
static const struct file_operations kvm_vm_fops = {
.release = kvm_vm_release,
.unlocked_ioctl = kvm_vm_ioctl,
.llseek = noop_llseek,
KVM_COMPAT(kvm_vm_compat_ioctl),
};

下面主要关注 kvm_vm_ioctlkvm_vm_release 的实现

kvm_vm_ioctl

先看下qemu 使用该函数做了哪些操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
* kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem)
* kvm_vm_ioctl(s, KVM_CREATE_VCPU, (void *)vcpu_id);
kvm_vm_ioctl(s, KVM_REGISTER_COALESCED_MMIO, &zone);
kvm_vm_ioctl(s, KVM_UNREGISTER_COALESCED_MMIO, &zone);
kvm_vm_ioctl(s, KVM_CHECK_EXTENSION, extension);
kvm_vm_ioctl(s, KVM_IOEVENTFD, &iofd);
kvm_vm_ioctl(s, KVM_IRQFD, &irqfd);
kvm_vm_ioctl(s, s->irq_set_ioctl, &event);
kvm_vm_ioctl(s, KVM_SET_GSI_ROUTING, s->irq_routes);
kvm_vm_ioctl(s, KVM_SIGNAL_MSI, &msi);
kvm_vm_ioctl(s, KVM_CREATE_IRQCHIP);
kvm_vm_ioctl(s, KVM_ENABLE_CAP, &cap);
kvm_vm_ioctl(s, KVM_HAS_DEVICE_ATTR, &attribute)
kvm_vm_ioctl(s, KVM_GET_DIRTY_LOG, &d);
kvm_vm_ioctl(s, KVM_RESET_DIRTY_RINGS);
kvm_vm_ioctl(s, KVM_CLEAR_DIRTY_LOG, &d);
# VFIO
kvm_vm_ioctl(s, KVM_CREATE_DEVICE, &create_dev);

kvm_vm_release

该函数为 close(vmfd) 时调用, 主要用途是释放虚拟机的资源, 调用了close(vmfd) 后认为该虚拟机被释放了.

ioctl KVM_SET_USER_MEMORY_REGION

为虚拟机映射内存

虚拟化下的内存访问

虚拟化情况下,内存的访问会分为两个StageHypervisor通过Stage 2来控制虚拟机的内存视图,控制虚拟机是否可以访问某块物理内存,进而达到隔离的目的;

  • Stage 1VA(Virtual Address)->IPA(Intermediate Physical Address),Host的操作系统控制Stage 1的转换;
  • Stage 2IPA(Intermediate Physical Address)->PA(Physical Address),Hypervisor控制Stage 2的转换;
    猛一看上边两个图,好像明白了啥,仔细一想,啥也不明白
    先说几个概念
    GVA - guest virtual address
    GPA - guest physical address
    HVA - host virtual address
    HPA - host physical address

stage1 中 Guest OS中的虚拟地址到物理地址的映射,就是典型的常规操作
stage2 涉及到两个地方:
GPA->HVA
HVA->HPA

GPA->HVA

在kvm中处理GPA->HVA的映射由ioctl(KVM_SET_USER_MEMORY_REGION) 命令完成
kvm 中注册 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进程地址空间中的一段区域;

对应用户态代码的这一部分

1
2
3
4
5
6
7
8
9
int kfd = open("/root/image.bin", O_RDONLY); //use virt baremental bin
read(kfd, ram, 0x5000);
struct kvm_userspace_memory_region mem = {
.slot = 0,
.guest_phys_addr = 0,
.memory_size = 0x5000,
.userspace_addr = (unsigned long) ram,
};
ret = ioctl(vmfd, KVM_SET_USER_MEMORY_REGION, &mem);

这个地方我们把baremental 的riscv bin 写到 ram中去了, 构建了kvm_userspace_memory_region, ram指向 这块区域的 userspace_addr, 将这块内存区域传给了kernel kvm.

在qemu中最重要的两个设置

  • mem.guest_phys_addr,这里设置成slot->start_addr,表示的是虚拟机的物理地址 GPA
  • mem.userspace_addr设置为slot->ram,表示的是虚拟对应的QEMU进程的虚拟地址。QEMU的 VA
    最后调用虚拟机所属的ioctl(KVM_SET_USER_MEMORY_REGION)来设置虚拟机的物理地址与QEMU虚拟地址的映射关系。

这样设置之后,虚拟机对物理地址的访问其实就是对QEMU这里虚拟地址的访问

简单分析下kvm是怎么处理的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-+ case KVM_SET_USER_MEMORY_REGION
\ - copy_from_user(&kvm_userspace_mem, argp, sizeof(kvm_userspace_mem))); "拷贝到内核空间"
| -+ kvm_vm_ioctl_set_memory_region(kvm, &kvm_userspace_mem)
\ -+ kvm_set_memory_region(kvm, mem)
\ -+ __kvm_set_memory_region(kvm, mem)
"Allocate some memory and give it an address in the guest physical address space."
\ - as_id = mem->slot >> 16;
| - base_gfn = (mem->guest_phys_addr >> PAGE_SHIFT);
| - npages = (mem->memory_size >> PAGE_SHIFT);
| - new = kzalloc(sizeof(*new), GFP_KERNEL_ACCOUNT);
"Allocate a slot that will persist in the memslot"
| - new->as_id = as_id;
| - new->id = id;
| - new->base_gfn = base_gfn;
| - new->userspace_addr = mem->userspace_addr;
| -+ kvm_set_memslot(kvm, old, new, change);
\ - kvm_prepare_memory_region(kvm, old, new, change)
"用于处理新的`slot`可能跨越多个用户进程VMA区域的问题,如果为设备区域,还需要将该区域映射到`Guest IPA`中;"
| -+ kvm_create_memslot(kvm, new); "Add the new memslot to the inactive set and activate"
\ - kvm_insert_gfn_node(slots, new);
"这里只列举了 new slot的情况, 将new slot 挂到 slots的gfn_tree红黑树下"
| -+ kvm_commit_memory_region(kvm, old, new, change);
\ - kvm_arch_commit_memory_region(kvm, old, new, change); "arch specific"

对应新加slot的情况, 最关键的操作是调用kvm_insert_gfn_node函数将新slot挂到了struct kvm_memslots slots的红黑树下.
通过kvm_memory_slot 管理了HVA->GPA的映射关系.

1
2
3
4
5
6
7
8
9
10
11
12
13
struct kvm_memory_slot {
struct hlist_node id_node[2];
struct interval_tree_node hva_node[2];
struct rb_node gfn_node[2];
gfn_t base_gfn;
unsigned long npages;
unsigned long *dirty_bitmap;
struct kvm_arch_memory_slot arch;
unsigned long userspace_addr;
u32 flags;
short id;
u16 as_id;
};

hva = gfn_to_hva_memslot_prot(memslot, gfn, &writeable); 通过gfn (GPA的物理页号) 可以查到对应的 HVA的地址.
这里的HVA在qemu的用户态场景下即对应了qemu初始化VM时, 通过KVM_SET_USER_MEMORY_REGION设置mem 时的VA地址, 即userspace_addr

HVA->HPA

用户态程序中分配虚拟地址vma后,实际与物理内存的映射是在page fault时进行的。那么同样的道理,我们可以顺着这个思路去查找是否HVA->HPA的映射也是在异常处理的过程中创建的

当用户态触发kvm_arch_vcpu_ioctl_run时,会让Guest OS去跑在Hypervisor
Guest OS中出现异常退出Host时,此时host kvm的kvm_riscv_vcpu_exit将对退出的原因进行处理;

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
-+ kvm_arch_vcpu_ioctl_run(struct kvm_vcpu *vcpu)
\ - struct kvm_run *run = vcpu->run;
| - vcpu->arch.ran_atleast_once = true; "标记vcpu 运行过"
| -|+ run->exit_reason == KVM_EXIT_MMIO
\ - kvm_riscv_vcpu_mmio_return(vcpu, vcpu->run) "即将退出guest, 退出到host环境, 退出原因是mmio"
| -|+ run->exit_reason == KVM_EXIT_RISCV_SBI
\ - kvm_riscv_vcpu_sbi_return(vcpu, vcpu->run) "即将退出guest, 退出到host环境, 退出原因是sbi调用"
| - vcpu_load(vcpu); "加载各类寄存器的值"
| -+ while(ret > 0)
\ - kvm_riscv_gstage_vmid_update(vcpu); "更新vmid"
| - kvm_riscv_check_vcpu_requests(vcpu);
| - preempt_disable();
| - local_irq_disable();
| - kvm_riscv_vcpu_flush_interrupts(vcpu); "got VCPU interrupts updated asynchronously, update it in hw"
| - kvm_riscv_update_hvip(vcpu);
| - kvm_riscv_vcpu_enter_exit(vcpu); "切换vcpu 虚拟机运行"
| - trap.sepc = vcpu->arch.guest_context.sepc;
| - trap.scause = csr_read(CSR_SCAUSE);
| = trap.stval = csr_read(CSR_STVAL);
| - trap.htval = csr_read(CSR_HTVAL);
| - trap.htinst = csr_read(CSR_HTINST);
| - kvm_riscv_vcpu_sync_interrupts(vcpu);
| - local_irq_enable();
| - preempt_enable();
| -+ ret = kvm_riscv_vcpu_exit(vcpu, run, &trap); "handle guest traps"
\ -+ case EXC_INST_GUEST_PAGE_FAULT | EXC_LOAD_GUEST_PAGE_FAULT | EXC_STORE_GUEST_PAGE_FAULT:
\ -+ ret = gstage_page_fault(vcpu, run, trap);
\ - fault_addr = (trap->htval << 2) | (trap->stval & 0x3); "错误的GPA会记录在htval中"
| - gfn = fault_addr >> PAGE_SHIFT; "查页号"
| - memslot = gfn_to_memslot(vcpu->kvm, gfn); "根据GPA 页号查询 memslot"
| - hva = gfn_to_hva_memslot_prot(memslot, gfn, &writeable); "根据memslot查对应的hva GPA->HVA"
| -|+ hva 不可用 异常为 EXC_LOAD_GUEST_PAGE_FAULT 21 LOAD_GUEST_PAGE_FAULT
<-- \ -+ return emulate_load(vcpu, run, fault_addr, trap->htinst)
| -|+ hva 不可写 且异常为 EXC_STORE_GUEST_PAGE_FAULT 23 STORE_GUEST_PAGE_FAULT
<-- \ -+ return emulate_store(vcpu, run, fault_addr, trap->htinst);
| -|+ kvm_riscv_gstage_map(vcpu, memslot, fault_addr, hva,
(trap->scause == EXC_STORE_GUEST_PAGE_FAULT) ? true : false); "建立GPA->HPA的映射"
\ - vma = find_vma_intersection(current->mm, hva, hva + 1);
"通过current 当前进程描述符查找 hva对应的vma(vma就是一块连续的线性地址空间的抽象),
riscv使用tp寄存器保存当前进程 task struct
此时current指 qemu vcpu线程对应到内核中的进程 task struct, 而hva 是PAGE_FAULT时的某地址"
| - vma_pagesize = 1ULL << vma_pageshift; "前面查vma是为了这里的vma_pagesize"
| - gfn = gpa >> PAGE_SHIFT; "GPA 页号"
| -+ hfn = gfn_to_pfn_prot(kvm, gfn, is_write, &writeable);
"根据gfn 计算 hfn 页号, hfn 代表host物理地址页号"
\ -+ __gfn_to_pfn_memslot(gfn_to_memslot(kvm, gfn), gfn, false, NULL, write_fault, writable, NULL)
\ - __gfn_to_memslot(kvm_memslots(kvm), gfn); "通过gfn 遍历红黑树找到 之前挂入的memslot"
| -+ addr = __gfn_to_hva_many(slot, gfn, NULL, write_fault)
\ -+ __gfn_to_hva_memslot(slot, gfn); "GPA->HVA"
\ - offset = gfn - slot->base_gfn;
| - offset = array_index_nospec(offset, slot->npages);
| - return addr = slot->userspace_addr + offset * PAGE_SIZE; "指到了 slot->userspace_addr"
*| - return hva_to_pfn(addr, atomic, async, write_fault, writable);
"找到host qemu 线程的va 映射的物理页号"
"页表walk, 默认快查: 默认页表项已经存在,直接通过遍历页表得到对应的页框"
"快查不到再慢查: 如果有页表项没有建立,还需要建立页表项,物理页面没有分配就需要分配物理页面。"
| - kvm_set_pfn_dirty(hfn); "可写情况下, 标记dirty"
| - mark_page_dirty(kvm, gfn);
| -+ gstage_map_page(kvm, pcache, gpa, hfn << PAGE_SHIFT, vma_pagesize, false, true);
\ - new_pte = pfn_pte(PFN_DOWN(hpa), PAGE_WRITE_EXEC); "标记pte 页表权限"
| - new_pte = pte_mkdirty(new_pte); "标记pte 页表 dirty"
| -+ gstage_set_pte(kvm, level, pcache, gpa, &new_pte) "设置 gpa 页表walk, 分配设置各级页表"
\ - ... 省略页表配置, 分配设置各级页表
| -+ gstage_remote_tlb_flush(kvm, current_level, addr); "刷新tlb"
\ -+ kvm_riscv_hfence_gvma_vmid_gpa(kvm, -1UL, 0, addr, BIT(order), order);
\ - data.type = KVM_RISCV_HFENCE_GVMA_VMID_GPA;
"设置vcpu->requests 最终由 kvm_riscv_check_vcpu_requests 时
处理 kvm_riscv_hfence_process"
| -+ make_xfence_request(kvm, hbase, hmask, KVM_REQ_HFENCE,
KVM_REQ_HFENCE_GVMA_VMID_ALL, &data);
\ -+ for vcpu in kvm->vcpu_arrays "遍历该guest os的所有vcpu"
\ -+ kvm_make_vcpu_request(vcpu, req, cpus, me);
\ -+ __kvm_make_request(req, vcpu);
\ - set_bit(req & KVM_REQUEST_MASK, (void *)&vcpu->requests);

如果注册了RAM,能获取到正确的HVA,如果是IO内存访问

  • hva没查到且为LOAD_GUEST_PAGE_FAULT 的情况下, 进入 io的读模拟
  • hva 不可用, 且为 STORE_GUEST_PAGE_FAULT , 进入io的写模拟
  • hva 查到了, 但不可写, 且为 STORE_GUEST_PAGE_FAULT, 进入io的写模拟
    hva 查到时, 可读, 异常为 LOAD_GUEST_PAGE_FAULTSTORE_GUEST_PAGE_FAULT hva 可写的场景下, 不会进入io的模拟, 此时即为 stage2 G-stage 缺页异常, 需要为 GPA->HPA 建立映射关系, 前面HVA与GPA 已经建立了映射关系
    qemu 通过KVM_SET_USER_MEMORY_REGION命令为VM 设置了一块内存, 绑定了 host qemu 进程的userspace_addr, 这块空间其实就是HVA, 对应着就有PA, 而GPA->HPA的映射, HPA就是指 userspace_addr的VA对应的PA .

vcpu run

上一章节中已经涉及到vcpu run的代码解读
vcpu 代表了虚拟机的一个虚拟cpu, qemu会为每个vcpu 创建qemu的一个用户态的线程
为 vCPU 分配内存空间。KVM_CREATE_VCPU 时,KVM 为每一个 vCPU 生成对应的文件句柄,对其执行相应的 ioctl 调用,就可以对 vCPU 进行管理
这部分对应精简代码的这一部分:

1
2
3
int vcpufd = ioctl(vmfd, KVM_CREATE_VCPU, 0);
int mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);
struct kvm_run *run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);

首先看一下 KVM_CREATE_VCPU 命令

KVM_CREATE_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
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
-+ case KVM_CREATE_VCPU
\ -+ kvm_vm_ioctl_create_vcpu(kvm, arg); "arg 为vcpu id"
\ - kvm->created_vcpus++;
| -+ kvm_arch_vcpu_precreate(kvm, id); "riscv not implement"
| - kvm_vcpu * vcpu = kmem_cache_zalloc(kvm_vcpu_cache, GFP_KERNEL_ACCOUNT);
"kvm_vcpu_cache 为kvm_init时为创建用于分配kvm_vcpu的slab 高速缓存"
| - page = alloc_page(GFP_KERNEL_ACCOUNT | __GFP_ZERO);
| - vcpu->run = page_address(page); "后面会和qemu 用户空间共享"
| -+ kvm_vcpu_init(vcpu, kvm, id);
\ - vcpu->kvm = kvm;
| - vcpu->vcpu_id = id;
| - vcpu->cpu = -1;
| -+ kvm_arch_vcpu_create(vcpu); "arch specific --> riscv"
\ - struct kvm_vcpu_csr *reset_csr = &vcpu->arch.guest_reset_csr;
"一组复位寄存器 v开头的寄存器 + hvip + scounteren"
| - cntx = &vcpu->arch.guest_reset_context; "reset 复位上下文 通用寄存器+ sepc + sstatus + hstatus"
| - cntx->sstatus = SR_SPP | SR_SPIE;
"Supervisor Previous Privilege 1代表S-mode, 0代表U-mode, sret执行时, SPP为0, 返回U-mode, 为1 返回S-mode"
"SPIE supervisor interrupts were enabled prior to trapping into supervisor mode"
"When a trap is taken into supervisor mode, SPIE is set to SIE, and SIE is set to 0.
When an SRET instruction is executed, SIE is set to SPIE, then SPIE is set to 1."
| - cntx->hstatus = 0;
| - cntx->hstatus |= HSTATUS_VTW;
"Virtual Timeout Wait, 用来解释WFI 指令的, 设置该位后, guest 执行WFI后会产生虚拟指令异常"
| - cntx->hstatus |= HSTATUS_SPVP;
"Supervisor Previous Virtual Privilege 跟SPP 类似, 为0代表HS-mode, 为1代表VS-mode
SRET 返回时, 为1 返回VS-mode"
| - cntx->hstatus |= HSTATUS_SPV; "Supervisor Previous Virtualization mode"
| -+ kvm_riscv_vcpu_timer_init(vcpu);
\ - kvm_vcpu_timer *t = &vcpu->arch.timer;
| -+ hrtimer_init(&t->hrt, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
"initialize a timer to the given clock"
"monotonic time 字面意思是单调时间,实际上它指的是系统启动以后流逝的时间
HRTIMER_MODE_REL: Time value is relative to now"
\ -+ __hrtimer_init(timer, clock_id, mode); "调用系统 timer进行初始化"
| - t->init_done = true;
| -+ t->hrt.function = kvm_riscv_vcpu_hrtimer_expired; "设置timer 定时器到时 回调函数"
\ - kvm_riscv_vcpu_hrtimer_expired(hrtimer *h) "timer 到时会调用 这个函数"
| - - - + kvm_riscv_vcpu_timer_next_event(struct kvm_vcpu *vcpu, u64 ncycles)
"虚线指示代码关系, 不是真实代码流程"
\ - hrtimer_start(&t->hrt, ktime_set(0, delta_ns), HRTIMER_MODE_REL);
"设定timer 定时器, 定时为delta_ns, 定时器到时会调用kvm_riscv_vcpu_hrtimer_expired 函数"
| -+ kvm_riscv_reset_vcpu(vcpu); "vcpu 复位"
\ - kvm_vcpu_csr *csr = &vcpu->arch.guest_csr;
| - kvm_vcpu_csr *reset_csr = &vcpu->arch.guest_reset_csr;
| - kvm_cpu_context *cntx = &vcpu->arch.guest_context;
| - kvm_cpu_context *reset_cntx = &vcpu->arch.guest_reset_context;
| -|+ if vcpu loaded (判断vcpu->cpu != -1) "应该是没加载过"
\ -+ kvm_arch_vcpu_put(vcpu); "如果加载过, 需要释放"
\ - vcpu->cpu = -1
| - kvm_vcpu_csr* csr = &vcpu->arch.guest_csr;
| - csr->vsstatus = csr_read(CSR_VSSTATUS); "读取v开头的寄存器 + hvip 等存到 vcpu->arch.guest_csr 中"
| - ... "省略其他的, 同上"
| - memcpy(csr, reset_csr, sizeof(*csr)); "guest_reset_csr -> guest_csr"
| - memcpy(cntx, reset_cntx, sizeof(*cntx)); "guest_reset_cntx -> guest_context"
| -+ kvm_riscv_vcpu_timer_reset(vcpu);
\ -+ kvm_riscv_vcpu_timer_cancel(&vcpu->arch.timer);
\ - hrtimer_cancel(&t->hrt); "reset 定时器, 取消定时器"
| - memset(vcpu->arch.hfence_queue, 0, sizeof(vcpu->arch.hfence_queue));
| -|+ if vcpu loaded (判断vcpu->cpu != -1) "应该是没加载过, load过需要重新调用下 load, 对应cpu hotplug场景"
\ -+ kvm_arch_vcpu_load(vcpu, smp_processor_id());
\ - kvm_vcpu_csr *csr = &vcpu->arch.guest_csr;
| - csr_write(CSR_VSSTATUS, csr->vsstatus); "从guest_csr 中加载 v 开头的寄存器 hvip"
| - ..."省略其他的"
| -+ kvm_riscv_gstage_update_hgatp(vcpu); "更新hgatp 寄存器"
\ - hgatp = gstage_mode; "在kvm_init 初始化时, 这个设置过, 是sv57x4 或 sv48x4"
| - hgatp |= (vcpu->kvm->arch->vmid.vmid) << HGATP_VMID_SHIFT) & HGATP_VMID_MASK; "设置vmid位"
| - hgatp |= (vcpu->kvm->arch->pgd_phys >> PAGE_SHIFT) & HGATP_PPN; "设置G-stage页表基址"
| - csr_write(CSR_HGATP, hgatp); "写入hgatp 寄存器"
| -+ kvm_riscv_vcpu_timer_restore(vcpu);
\ - kvm_guest_timer *gt = &vcpu->kvm->arch.timer;
| - csr_write(CSR_HTIMEDELTA, gt->time_delta); "更新htimedelta 寄存器"
| - vcpu->cpu = cpu;
| - vcpu->vcpu_idx = atomic_read(&kvm->online_vcpus);
| - xa_insert(&kvm->vcpu_array, vcpu->vcpu_idx, vcpu, GFP_KERNEL_ACCOUNT);
| -+ r = create_vcpu_fd(vcpu);
\ - snprintf(name, sizeof(name), "kvm-vcpu:%d", vcpu->vcpu_id); "创建/dev/kvm-vcpu-x 节点 vcpu fd 文件"
| -+ return anon_inode_getfd(name, &kvm_vcpu_fops, vcpu, O_RDWR | O_CLOEXEC);
"fd 关联的 file file->private_data 指向vcpu"
"绑定了 kvm_vcpu_fops fops, 操作vcpu fd 时, 会导向到 下面的处理函数"
\ - kvm_vcpu_fops.release        = kvm_vcpu_release,
| - .unlocked_ioctl = kvm_vcpu_ioctl,
| - .mmap           = kvm_vcpu_mmap,
| - .llseek         = noop_llseek,
| -+ kvm_arch_vcpu_postcreate(vcpu);
\ -|+ if (vcpu->vcpu_idx != 0)
"vcpu with id 0 is the designated boot cpu. 除vcpu id为0之外的其他vcpu 执行下电处理"
\ -+ kvm_riscv_vcpu_power_off(vcpu);
\ - kvm_make_request(KVM_REQ_SLEEP, vcpu);
| - kvm_vcpu_kick(vcpu);
"Kick a sleeping VCPU, or a guest VCPU in guest mode, into host kernel mode."
| - kvm_create_vcpu_debugfs(vcpu); "为vcpu fd 安装debugfs"

创建vcpu 时, kvm 做了一下关键的操作:

  1. 从kvm_vcpu_cache 高速slab 缓存中分配vcpu 内存, 分配了一个page 给到vcpu->run, 这块空间后续会跟用户态的qemu 共享

  2. 初始化vcpu 结构体, vcpu->kvm 指向VM, vcpu_id 为 KVM_CREATE_VCPU 命令带的id
    vcpu->arch 指向 riscv的 kvm_vcpu_arch结构体
    - guest_context 为 CPU context of Guest VCPU
    guest 运行上下文 包含 通用寄存器 fp + sepc + sstatus + hstatus
    - guest_csr 为 CPU CSR context of Guest VCPU
    guest vcpu 运行状态上下文 包含 v开头的寄存器 + hvip + scouteren
    - guest_reset_context 为 CPU context upon Guest VCPU reset
    guest 复位上下文, 包含 通用寄存器 fp + sepc + sstatus + hstatus
    - guest_reset_csr 为 CPU CSR context upon Guest VCPU reset
    guest vcpu相关 复位寄存器上下文 包含 v开头的寄存器 + hvip + scouteren
    reset_context 是初始化阶段vcpu 比较重要的寄存器, 这里特别进行了设置, vcpu初始化时 guest_context 是从reset_context 复制来的.

    从vcpu sret 操作后, 可以跳转到 VS-mode

  3. timer 初始化, 绑定了定时器到时的处理函数 kvm_riscv_vcpu_hrtimer_expired

  4. vcpu 挂入 kvm->vcpu_array

  5. vcpu 绑定文件 fd, 绑定 fops 为 kvm_vcpu_fops, 之后对vcpufd 的操作都由 kvm_vcpu_fops 接管, 而关联的file->private_data 指向vcpu 结构体

  6. 其他vcpu 默认下电处理, 只有0号vcpu 才是上电状态

KVM_GET_VCPU_MMAP_SIZE mmap(vcpufd)

KVM_GET_VCPU_MMAP_SIZE 命令很简单, 只返回给用户态一个page, 用户态 mmap 一块空间, 注意fd 是vcpufd, 会紧接着调用vcpu fd 绑定的fops 的mmap 操作
关于mmap的简单示例参考 https://xz.aliyun.com/t/2099

1
2
int mmap_size = ioctl(kvmfd, KVM_GET_VCPU_MMAP_SIZE, NULL);
struct kvm_run *run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-+ kvm_run *run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, vcpufd, 0); 
"第一个参数为null, 由系统自动选择地址"
\ -+ kvm_vcpu_mmap(struct file *file, struct vm_area_struct *vma)
"kvm_vcpu_mmap 并没有做实质性的页表映射, 只注册了一个 fault 缺页处理函数"
\ -+ vma->vm_ops = &kvm_vcpu_vm_ops;
\ -+ kvm_vcpu_vm_ops.fault = kvm_vcpu_fault,
\ -+ kvm_vcpu_fault(struct vm_fault *vmf)
"因为前面并没有对页表进行映射, 在访问到vm 时会触发缺页异常, 进入到该函数处理"
\ - kvm_vcpu *vcpu = vmf->vma->vm_file->private_data;
| -|+ if vmf->pgoff == 0
\ -+ page = virt_to_page(vcpu->run);
"找到vcpu->run 虚拟地址对应的物理页 virt_to_page, 虚拟地址转换为物理页地址"
\ - pfn_to_page(virt_to_pfn(vaddr)
"pfn 物理页帧号 / 每个物理上的页,内核给分配了一个描述符来描述:page"
| - vmf->page = page; "该page 应该是返回给用户空间的"

所以上述mmap 操作最终效果:

  • 由于第一个参数为NULL, 所以由系统自动分配一个虚拟地址给 用户态, 为返回值 run的地址
  • 在返回用户态run地址时, 会触发缺页异常, 走到kernel的kvm_vcpu_fault 函数, 这个函数给它找到 内核kvm中保存的vcpu->run 虚拟地址映射的物理页, 将该物理页跟用户态的 run 虚拟地址映射起来, 所以用户态的run 和 内核态的vcpu->run 虚拟地址不同, 但映射的物理页是相同的, 即共享物理页

接下来进主循环, 看下vcpu run起来的逻辑

KVM_VCPU_RUN

用户态代码

1
2
3
4
while(1) {
ret = ioctl(vcpufd, KVM_RUN, NULL);
...
}

关于 kvm_arch_vcpu_ioctl_run 前面已经说过了, 这里还是简单带一下, 与第一次运行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
-+ 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; "标记运行过"
| -+ 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); "vspec 前面正常应该没在加载过, 这里是0"
| - ... "省略其他的v 开头的寄存器和 hvip的处理"
| -+ while(ret > 0)
\ - kvm_riscv_gstage_vmid_update(vcpu); "更新vmid"
| - kvm_riscv_check_vcpu_requests(vcpu);
| - preempt_disable();
| - local_irq_disable();
| - kvm_riscv_vcpu_flush_interrupts(vcpu); "got VCPU interrupts updated asynchronously, update it in hw"
| - kvm_riscv_update_hvip(vcpu);
| -+ kvm_riscv_vcpu_enter_exit(vcpu); "切换vcpu 虚拟机运行"
\ -+ __kvm_riscv_switch_to(&vcpu->arch); "进到 vcpu_switch.S"
\ - REG_S  ra, (KVM_ARCH_HOST_RA)(a0) "保存当前通用寄存器到 vcpu->arch的 host_context 的对应成员中"
| - ... "省略其他的通用寄存器"
| - REG_L  t0, (KVM_ARCH_GUEST_SSTATUS)(a0) "加载vcpu的 guest_context的sstatus 到 t0 寄存器"
| - REG_L  t1, (KVM_ARCH_GUEST_HSTATUS)(a0) "加载vcpu的 guest_context的hstatus 到 t1 寄存器"
| - REG_L  t2, (KVM_ARCH_GUEST_SCOUNTEREN)(a0) "加载vcpu的 guest_context的scounteren 到 t2 寄存器"
| - la t4, __kvm_switch_return
| - REG_L  t5, (KVM_ARCH_GUEST_SEPC)(a0) "加载vcpu的 guest_context的sepc 到 t5 寄存器"
| - csrrw  t0, CSR_SSTATUS, t0 "交换 t0 寄存器 与 sstatus 寄存器, Save Host and Restore Guest SSTATUS"
"guest_context的sstatus 给到 sstatus 寄存器, 而 sstatus 寄存器原本的值给到t0"
| - csrrw  t1, CSR_HSTATUS, t1
| - csrrw  t2, CSR_SCOUNTEREN, t2
| - csrrw  t4, CSR_STVEC, t4 "stvec 寄存器指向了 __kvm_switch_return 地址, 而stvec 寄存器本来的值给到 t4"
| - csrrw  t3, CSR_SSCRATCH, a0 "vcpu->arch 的地址给到了 sscratch, 而sscratch本来的值给到 t3 寄存器"
| - csrw   CSR_SEPC, t5 "guest sepc 给到了 sepc 寄存器"
| - REG_S  t0, (KVM_ARCH_HOST_SSTATUS)(a0) "sstatus 原本值存到 vcpu->arch的host_context的sstatus成员"
| - REG_S  t1, (KVM_ARCH_HOST_HSTATUS)(a0) "hstatus 原本值存到 vcpu->arch的host_context的hstatus成员"
| - REG_S  t2, (KVM_ARCH_HOST_SCOUNTEREN)(a0) "sconteren 原本值存到vcpu->arch的host_context的sconteren成员"
| - REG_S  t3, (KVM_ARCH_HOST_SSCRATCH)(a0) "sscratch 原本值存到 vcpu->arch的host_context的sscratch 成员"
| - REG_S  t4, (KVM_ARCH_HOST_STVEC)(a0) "stvec 原本值存到 vcpu->arch的host_context的stvec 成员里"
| - REG_L  ra, (KVM_ARCH_GUEST_RA)(a0) "vcpu->arch的 guest_context 保存的通用寄存器恢复到寄存器"
| - ... "省略其他的, 通用寄存器都处理"
| - sret "运行到 vs-mode guest os"

上面最主要的流程还是对riscv S-mode 和 hypervisor 模式新增的 h 开头和 v 开头的寄存器的处理.
注意点:

  1. 切换到vcpu 运行时, 把stvec的中断和异常的处理函数地址给指到了 __kvm_switch_return, 即guest os 运行在vcpu上时, 对应的物理cpu 收到中断和异常后跳到的入口是__kvm_switch_return
  2. vcpu->arch 的 host_context guest_context 结构体的处理, 需要保存加载哪些寄存器
  3. 至于vcpu 的 v开头的寄存器, 在前面 kvm_arch_vcpu_load过程会进行处理, 初始时这些寄存器应该都是0
  4. sret的行为绑定了 sstatus 寄存器的 SPP , hstatus 寄存器的 SPVP SPV VTW 这些状态, 这些状态会在create vcpu 时设置好
  5. 切换vcpu过程并未对hideleg 和 hedeleg 做特殊处理, 这一组寄存器在前面kvm VM 虚拟机初始化时设置的物理cpu 唤醒函数register_syscore_ops(&kvm_syscore_ops)中设置的回调hardware_enable_nolock处理的
  6. vcpu 运行的GPA 怎么处理, vcpu 一运行起来肯定会触发 G-stage 缺页中断, 需要翻到 KVM_SET_USER_MEMORY_REGIONGPA->HVA HVA->HPA章节查看, GPA 最终对应到了 qemu 为VM 分配的内存的物理页上.

KVM_VCPU_RUN的其他部分没讲, 它还负责处理异常, 即从vcpu guest 运行环境陷入到 host 中时, 物理cpu会收到异常, 此时的stvec 为 __kvm_switch_return 函数

__kvm_switch_return

简单看下这个函数干了什么, 需要结合上一章的kvm_riscv_vcpu_enter_exit 来看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-+ __kvm_switch_return
\ - csrrw  a0, CSR_SSCRATCH, a0 "交换sscratch 和 a0, sscratch 在kvm_riscv_vcpu_enter_exit 中最终指到了 vcpu->arch 的地址"
| - REG_S  ra, (KVM_ARCH_GUEST_RA)(a0) "该条为将 ra 寄存器存到 vcpu->arch guest_context的ra成员"
| - REG_S  t0, (KVM_ARCH_GUEST_T0)(a0) "将 t0 寄存器存到 vcpu->arch guest_context的t0成员"
| - ... "省略其他通用寄存器的处理, Save Guest GPRs 保存guest_context 通用寄存器, 除了a0之外"
| - REG_L  t1, (KVM_ARCH_HOST_STVEC)(a0) "vcpu->arch host_guest stvec 成员 赋给 t1寄存器"
| - REG_L  t2, (KVM_ARCH_HOST_SSCRATCH)(a0) "vcpu->arch host_guest sscratch 成员 赋给 t2寄存器"
| - REG_L  t3, (KVM_ARCH_HOST_SCOUNTEREN)(a0) "vcpu->arch host_guest scounteren 成员 赋给 t3寄存器"
| - REG_L  t4, (KVM_ARCH_HOST_HSTATUS)(a0) "vcpu->arch host_guest hstatus 成员 赋给 t4寄存器"
| - REG_L  t5, (KVM_ARCH_HOST_SSTATUS)(a0) "vcpu->arch host_guest sstatus 成员 赋给 t5寄存器"
| - csrr   t0, CSR_SEPC "读sepc 寄存器给到 t0"
| - csrrw  t2, CSR_SSCRATCH, t2 "sscratch 寄存器的值给到 t2, vcpu->arch host_context sscratch 成员的值给到sscratch"
| - csrw   CSR_STVEC, t1 "stvec 更新为 vcpu->arch host_guest stvec成员的值, 即被恢复回到 host的 stvec了"
| - csrrw  t3, CSR_SCOUNTEREN, t3 "vcpu->arch host_guest scounteren 成员 赋给 scounteren, 而scouteren 寄存器的原本的值给到 t3"
| - csrrw  t4, CSR_HSTATUS, t4 "hstatus 更新为 vcpu->arch host_guest hstatus 成员的值, 而hstatus 原本的寄存器值给到 t4"
| - csrrw  t5, CSR_SSTATUS, t5 "sstatus 更新为 vcpu->arch host_guest sstatus 成员的值, 而sstatus 原本的寄存器值给到 t5"
| - REG_S  t0, (KVM_ARCH_GUEST_SEPC)(a0) "sepc 寄存器原本的值存到 vcpu->arch guest_context的 sepc 成员中"
| - REG_S  t2, (KVM_ARCH_GUEST_A0)(a0) "sscratch 原本的值存到 vcpu->arch guest_context的 a0 成员中"
| - REG_S  t3, (KVM_ARCH_GUEST_SCOUNTEREN)(a0) "scouteren 寄存器的原本的值 存到 vcpu->arch guest_context的 scouteren 成员中"
| - REG_S  t4, (KVM_ARCH_GUEST_HSTATUS)(a0) "hstatus 原本的寄存器值 存到 vcpu->arch guest_context的 hstatus 成员中"
| - REG_S  t5, (KVM_ARCH_GUEST_SSTATUS)(a0) "而sstatus 原本的寄存器值 存到 vcpu->arch guest_context的 sstatus 成员中"
| - REG_L  ra, (KVM_ARCH_HOST_RA)(a0) "ra 寄存器恢复为 host_context的 ra 成员"
| - ... "省略其他的通用寄存器的恢复 处理, 统一恢复为 vcpu->arch的host_context的 对应成员的值"
| - ret "返回C-code"

这个函数主要是 保存 guest 的通用寄存器 context 上下文, 恢复host的context 上下文
注意点:

  • stvec 的处理, 恢复到了host的stvec, 也就是host os 原本的向量入口
  • sscratch的处理, 恢复到了host的sscratch
  • sstatus的处理, 恢复到了 host的sstatus

最后的ret 命令非常重要, 回到哪了, 应该是 ra寄存器指向的地方.
前面章节中 kvm_arch_vcpu_ioctl_run 函数调度vcpu 运行时, 最后调用了 kvm_riscv_vcpu_enter_exit , 跳转前会更新ra 寄存器为 kvm_riscv_vcpu_enter_exit的下一条指令的地址, 而ra 被保存到了 vcpu->arch 的 host_context的ra成员中, vcpu 中出现了异常或中断后, 需要返回到host进行处理时, 物理cpu 收到中断或异常, 进入了 __kvm_switch_return 函数, 该函数将 ra 从 vcpu->arch 的 host_context的ra 成员中恢复回来了, 所以ret 会跳转到 kvm_riscv_vcpu_enter_exit的下一条指令处继续运行.

vcpu 运行后会处在vs-mode 下, vs-mode下操作的cpu寄存器(s 开头的寄存器) 在 V=0 (host 视角下) 对应的v 开头的寄存器, 也就是说 vs-mode下不能对 hs-mode下的s 开头的寄存器做操作.
结合上面章节和本章的操作, 为什么在交换host 和 guest 上下文时要处理 s 开头的寄存器呢(stvec sstatus sepc sscratch)等?
其实是为了处理 host 与 guest 的转换过程, host下进程调度时 这几个寄存器的状态与 要进行vcpu的调度时的状态是不同的, 所以需要保存恢复

最后看下收到 中断或异常后, 从__kvm_switch_return 返回后继续做了什么, 也就是kvm_arch_vcpu_ioctl_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
-+ r = kvm_arch_vcpu_ioctl_run(vcpu);  
\ - ...
| -+ while(ret > 0)
\ - ...
| -+ __kvm_riscv_switch_to(&vcpu->arch); "进到 vcpu_switch.S"
\ - ...
| - sret "运行到 vs-mode guest os"
| - vcpu->mode = OUTSIDE_GUEST_MODE; "从__kvm_switch_return 处 ret 返回继续运行"
| - vcpu->stat.exits++; "标记退出guest 次数"
| - trap.sepc = vcpu->arch.guest_context.sepc; "这个地方有点啰嗦了, 直接读sepc 应该效果一样的,
因为guest_context的spec 成员的值也是在 __kvm_switch_return 时读的 sepc 寄存器的值"
| - trap.scause = csr_read(CSR_SCAUSE); "物理cpu 收到的 中断或异常, 所以scause 寄存器会更新"
| - trap.stval = csr_read(CSR_STVAL); "出问题的stval 地址, 代表的是host 中出的异常"
| - trap.htval = csr_read(CSR_HTVAL); "出问题的地址可能存到了 htval 中, 代表是guest 中的异常"
| - trap.htinst = csr_read(CSR_HTINST); "出问题时的指令 可能存到了 htinst 中, 代表guest 指令异常"
| -+ kvm_riscv_vcpu_sync_interrupts(vcpu);
\ - kvm_vcpu_csr *csr = &vcpu->arch.guest_csr
| - csr->vsie = csr_read(CSR_VSIE); "更新guest_csr 的vsie 寄存器"
| - hvip = csr_read(CSR_HVIP);
| -|+ if (csr->hvip ^ hvip) & (1UL << IRQ_VS_SOFT) "如果guest_csr的hvip成员与当前的hvip 在 IRQ_VS_SOFT 位不一样"
\ -|+ if (hvip & (1UL << IRQ_VS_SOFT)
\ - vcpu->arch->irqs_pending 设置 IRQ_VS_SOFT
|+ else !(hvip & (1UL << IRQ_VS_SOFT)
\ - vcpu->arch->irqs_pending 清除 IRQ_VS_SOFT
| - local_irq_enable();
| - preempt_enable();
| -+ ret = kvm_riscv_vcpu_exit(vcpu, run, &trap); "处理异常, 前面章节中讲过了, 不再赘述"
| -+ vcpu_put(vcpu); "while 结束, 代表vcpu 状态异常, 释放vcpu"
\ - preempt_disable(); "关闭抢占"
| -+ kvm_arch_vcpu_put(vcpu);
\ - vcpu->cpu = -1;
| - vcpu->arch.guest_csr->vsstatus = csr_read(CSR_VSSTATUS);
| - ..."省略其他v 开头寄存器处理, 同上"
| - preempt_enable(); "开启抢占"

mmio 模拟初探

vcpu 线程堆栈:

1
2
3
4
#0  0x00aaaaaae4cffa84 in kvm_cpu_exec ()
#1 0x00aaaaaae4d00b18 in kvm_vcpu_thread_fn ()
#2 0x00aaaaaae4e4780c in qemu_thread_start ()
#3 0x00ffffffbe313e1c in start_thread () from /home/liguang/program/3rdparty/buildroot-2022.08.1/output/host/riscv64-buildroot-linux-gnu/sysroot/lib64/libc.so.6

在vcpu run时, qemu 会为每个vcpu 创建一个线程, 每个线程里在while循环里调用ioctl(KVM_VCPU_RUN), 而kernel 对应每个vcpu 的KVM_VCPU_RUN ioctl 命令也有一个while 循环, 简单看下这部分.
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
84
85
86
"qemu 为vcpu 创建的线程"
-+ kvm_cpu_exec(CPUState *cpu)
\ -+ while(ret == 0)
\ -+ run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);
--------------------------------------------> 陷入 kernel <-----------------------------------------------
\ -+ kvm_arch_vcpu_ioctl_run(vcpu)
\ -+ while(ret > 0)
\ -+ kvm_riscv_vcpu_enter_exit(struct kvm_vcpu *vcpu)
\ -+ __kvm_riscv_switch_to(&vcpu->arch);
+++++++++ 进入guest os +++++++++++++++++++++++
\ - io读操作 load 一个mmio 地址上的数据 触发21 LOAD_GUEST_PAGE_FAULT
+++++++++ 退出guest os +++++++++++++++++++++++
| - __kvm_switch_return
| - trap.sepc = vcpu->arch.guest_context.sepc;
| - trap.scause = csr_read(CSR_SCAUSE);
| - trap.stval = csr_read(CSR_STVAL);
| - trap.htval = csr_read(CSR_HTVAL);
| - trap.htinst = csr_read(CSR_HTINST);
| -+ ret = kvm_riscv_vcpu_exit(vcpu, run, &trap);
\ -+ switch (trap->scause)
\ -+ case EXC_LOAD_GUEST_PAGE_FAULT:
\ -|+ if vcpu->arch.guest_context.hstatus & HSTATUS_SPV "trap到host前是 virtual 模式"
\ -+ ret = gstage_page_fault(vcpu, run, trap);
\ - fault_addr = (trap->htval << 2) | (trap->stval & 0x3);
| - gfn = fault_addr >> PAGE_SHIFT;
| - memslot = gfn_to_memslot(vcpu->kvm, gfn);
| - hva = gfn_to_hva_memslot_prot(memslot, gfn, &writeable);
| -|+ if hva 无效 && trap->scause == EXC_LOAD_GUEST_PAGE_FAULT
\ -+ ret = emulate_load(vcpu, run, fault_addr, trap->htinst);
\ -|+ if htinst & 0x1
"trapped instruction value is transformed instruction or custom instruction"
\ - insn = htinst | INSN_16BIT_MASK;
| -|+ else if !(hinst & 0x1)
"trapped instruction value is zero or special value"
\ -+ insn = kvm_riscv_vcpu_unpriv_read(vcpu, true, ct->sepc, &utrap);
"Read machine word from Guest memory 从sepc 读异常指令"
| - "Decode length of MMIO and shift "
"一条load 指令 可以是 lw lb lbu lh lhu c_lw c_lwsp 等, 操作数为地址 和 寄存器, 寄存器里保存读回来的数据"
"因为vcpu->arch guest_context 已经包含了所有的通用寄存器, 且切回guest os 时,
会恢复guest_context的通用寄存器回到寄存器中, 因此shift 对应guest_context 通用寄存器的下标就可以了
host 读出正确的值塞给 guest_context中的shift所指的那一个通用寄存器,
调度回guest时 这条load 指令就像自己把地址里的数据读到了一样"
| - update vcpu->arch.mmio_decode
"将上面解析到的 指令 数据的长度, shift 信息存到 vcpu->arch.mmio_decode中"
| - run->mmio.is_write = false;
| - run->mmio.phys_addr = fault_addr;
| - run->mmio.len = len;
| -|+ if !kvm_io_bus_read(vcpu, KVM_MMIO_BUS, fault_addr, len, data_buf)
"Try to handle MMIO access in the kernel kernel先自己处理, 如果kernel 能自己cover, 则自己处理"
"如果kernel 自己读到了数据, 就把数据放到data_buf里"
\ - vcpu->stat.mmio_exit_kernel++;
| - memcpy(run->mmio.data, data_buf, len);
| -+ kvm_riscv_vcpu_mmio_return(vcpu, run);
\ - 根据数据长度, 将数据放回到 shift 对应的vcpu->arch.guest_context的对应寄存器中
| - vcpu->arch.guest_context.sepc += vcpu->arch.mmio_decode.insn_len;
"从host 返回到 guest 中, 调用sret, sret 看的还是sepc 寄存器, 并不是vsepc, 这个很重要"
<---| - return 1
| - vcpu->stat.mmio_exit_user++; "kernel 自己处理不了, 需要返回用户模式进行io 模拟"
| - run->exit_reason = KVM_EXIT_MMIO; "更新exit_reason, 用户空间也能看到"
<========= | - return 0 "退出while"
| - vcpu_put(vcpu); "释放vcpu"
--------------------------------------------< 退出 kernel >-----------------------------------------------
\ -+ switch run->exit_reason: "kernel 中更新了 run->exit_reason 为 KVM_EXIT_MMIO"
\ -|+ case KVM_EXIT_MMIO
\ -+ address_space_rw(&address_space_memory,
run->mmio.phys_addr, attrs, "前面已经讲过 这里的run 跟 kernel kvm 中vcpu->run 共享物理页"
run->mmio.data, run->mmio.len, run->mmio.is_write);
"可以理解为用户态把 fault_addr(物理地址) 处的内容读出来了, 最后将数据塞到了 run->mmio.data 中"
<-loop ---| - ret = 0; "继续while 循环, 下面把循环流程拆开"
| -+ run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);
--------------------------------------------> 陷入 kernel <-----------------------------------------------
\ -+ kvm_arch_vcpu_ioctl_run(vcpu)
\ -|+ if (run->exit_reason == KVM_EXIT_MMIO) "进入这里"
\ -+ ret = kvm_riscv_vcpu_mmio_return(vcpu, vcpu->run);
"用户态已经把 mmio.data给填上了, 这里跟kernel 自己能cover的情况一致,
把mmio.data里的数据填给guest_context对应shift的通用寄存器就可以了"
\ - 根据数据长度, 将数据放回到 shift 对应的vcpu->arch.guest_context的对应寄存器中
| - vcpu->arch.guest_context.sepc += vcpu->arch.mmio_decode.insn_len;
| -+ vcpu_load(vcpu); "重新装载v开头的寄存器, 恢复vcpu状态"
\ - kvm_arch_vcpu_load(struct kvm_vcpu *vcpu, int cpu)
| -+ while (ret > 0)
\ -+ kvm_riscv_vcpu_enter_exit(vcpu);
\ -+ __kvm_riscv_switch_to(&vcpu->arch);
\ - sret "从spec 处返回到 vs-mode的guest"
+++++++++ 进入guest +++++++++++++
| - guest 从io 读取的下一条指令运行

qemu 中的这个address_space_rw 在后面章节中再讲. 这一块涉及的东西是很复杂的.

上面流程中的关键信息:

  1. qemu 为每个vcpu 创建一个线程, 持续while 循环运行 ioctl(vcpufd, KVM_VCPU_RUN)
  2. guest 读取mmio 虚拟地址时, 会触发 21 LOAD_GUEST_PAGE_FAULT
  3. host 收到异常后, 从htinst 读到 guest 触发异常时的指令是什么, 从htinst 不能读到时, 需要调用kvm_riscv_vcpu_unpriv_readguest_context的sepc 处解析出异常指令来. 从htval 中找到 mmio的GPA
  4. host 需要从异常指令中解析出读地址读出来放到哪个寄存器中 (表示为shift 下标), 数据宽度是多少
  5. host kernel 先看下自己能不能把GPA的数据读出来, 可以的话, 把数据读出后放到 vcpu->run->mmio.data 中, 再填到 guest_context shift 对应的通用寄存器中, 再调度到vcpu进入guest时, 会将该寄存器从guest_context 中恢复
  6. host kernel 不能把GPA的数据读出, 则会回到用户态, 让用户态来处理, 用户态把GPA的数据读出后, 存到run->mmio.data 中, 再回到kernel, kernel 再填到 guest_context shift 对应的通用寄存器中, 再调度vcpu 进入guest 时, 将该寄存器从guest_context 中恢复
  7. guest 继续运行 load 完数据的下一条指令, guest 对上述步骤是无感知的

这样处理的弊端时, 每进行一条mmio的读/写 指令, 都需要陷入KVM, 如果kvm处理不了, 还要陷入qemu 用户态, 光是上下文切换加上调度vcpu相关解析指令等的耗时就已经非常可观了, 可见这种方式是非常低效的.

注意点:
host sret 返回到 HS 或 VS 模式, 都是从sepc 处执行, 返回到VS时并没有用到 vsepc 寄存器

Qemu 7.0 KVM 解读

从下面开始分析 qemu kvm 模式针对 virt 机型的初始化流程

启动虚拟机命令

Image是经过objcopy处理的只包含二进制数据的内核代码,它已经不是elf格式了,但这种格式的内核镜像还没有经过压缩. 有一个MS-DOS header
关于kernel image的格式 见 https://www.cnblogs.com/hnrainll/archive/2011/06/10/2077961.html

1
2
3
4
5
6
7
qemu-system-riscv64 -M virt -bios fw_jump.elf -kernel Image \
-append "rootwait root=/dev/vda ro" \
-drive file=rootfs.ext2,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 \
-nographic \
-smp 2 \
-enable-kvm

bios fw_jump.elf 需要运行在 m-mode, 但是riscv 虚拟化, 使用kvm 模式时, 虚拟机只能跑的模式只有VS-mode 和 VU-mode 模式, m-mode 是不能跑的.
从现象上看, 虚拟机启动后, 并没有opensbi的log输出. 与纯软模拟时的现象不同, 纯软模式下, 会有opensbi的log输出.

1
2
3
4
5
6
7
8
9
10
11
12
13
qemu-system-riscv64 -machine help
Supported machines are:
microchip-icicle-kit Microchip PolarFire SoC Icicle Kit
none empty machine
nuclei_n Nuclei RISC-V demosoc on Kit(MCU200T/DDR200T), support Nuclei N/NX class processor
nuclei_u Nuclei RISC-V demosoc on Kit(MCU200T/DDR200T), support Nuclei UX class processor with MMU
plct_machine PLCT RISC-V board
plct_stone PLCT RISC-V Stone board
shakti_c RISC-V Board compatible with Shakti SDK
sifive_e RISC-V Board compatible with SiFive E SDK
sifive_u RISC-V Board compatible with SiFive U SDK
spike RISC-V Spike board (default)
virt RISC-V VirtIO board

这里重点看下-enable-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
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
DEF("enable-kvm", 0, QEMU_OPTION_enable_kvm, \
"-enable-kvm enable KVM full virtualization support\n", QEMU_ARCH_ALL)
DEF("M", HAS_ARG, QEMU_OPTION_M,
" sgx-epc.0.memdev=memid,sgx-epc.0.node=numaid\n",
QEMU_ARCH_ALL)

-----------> softmmu/vl.c# qemu_init
-+ qemu_init(argc, argv)
\ -+ switch(popt->index)
\ -+ case QEMU_OPTION_enable_kvm:
\ - qdict_put_str(machine_opts_dict, "accel", "kvm");
| -+ case QEMU_OPTION_M | QEMU_OPTION_machine
\ -+ keyval_parse_into(machine_opts_dict, optarg, "type", &help, &error_fatal);
| -+ qemu_create_machine(machine_opts_dict);
\ -+ MachineClass *machine_class = select_machine(qdict, &error_fatal);
"调用TYPE_MACHINE 类型的 class 初始化, 进而调用到 virt_machine_class_init"
\ - *optarg = qdict_get_try_str(qdict, "type");
| -+ GSList *machines = object_class_get_list(TYPE_MACHINE, false);
"查找所有注册 parent 为 TYPE_MACHINE 类型的数据, 对所有 type_table 中的类进行类的初始化"
| - machine_class = find_machine(optarg, machines); "optarg 为 virt, 找到对应的机型class"
| - current_machine = MACHINE(object_new_with_class(OBJECT_CLASS(machine_class)));
"调用实例初始化 进而调用到 virt_machine_instance_init"
| -+ qemu_apply_legacy_machine_options(machine_opts_dict);
\ - value = qdict_get_try_str(qdict, "accel");
| - accelerators = g_strdup(value); "kvm"
| -+ configure_accelerators(argv[0]);
\ -+ qemu_opts_foreach(qemu_find_opts("accel"), do_configure_accelerator, &init_failed, &error_fatal)
\ -+ do_configure_accelerator(void *opaque, QemuOpts *opts, Error **errp)
\ - accel = ACCEL(object_new_with_class(OBJECT_CLASS(ac)));
| -+ ret = accel_init_machine(accel, current_machine);
\ -+ ret = acc->init_machine(ms);
\ -+ kvm_init(MachineState *ms) "进入 kvm 的 kvm_init"
| -+ qmp_x_exit_preconfig()
\ -+ qemu_init_board()
\ -+ machine_run_board_init(current_machine);
\ -+ machine_class->init(machine);
\ -+ virt_machine_init(MachineState *machine)
\ - start_addr = memmap[VIRT_DRAM].base = 0x80000000
\ -|+ if kvm_enabled()
\ - machine->firmware = g_strdup("none");
| -+ firmware_end_addr = riscv_find_and_load_firmware(machine, RISCV64_BIOS_BIN, start_addr, NULL);
"kvm 开启后, 不再加载opensbi"
\ - firmware_end_addr = start_addr;
| -|+ if machine->firmware != "none"
\ - firmware_filename = riscv_find_firmware(machine->firmware);
"指定了 -bios 且未指定-enable_kvm 时会加载这里的 bios bin"
| - firmware_end_addr = riscv_load_firmware(firmware_filename, firmware_load_addr, sym_cb);
| - kernel_start_addr = riscv_calc_kernel_start_addr(&s->soc[0], firmware_end_addr); "跟start_addr 2M 对齐"
| -+ kernel_entry = riscv_load_kernel(machine->kernel_filename, kernel_start_addr, NULL);
"kernel bin 加载到 start_addr 处, 最后返回kernel_entry"
\ -+ load_image_targphys_as(kernel_filename, kernel_start_addr, current_machine->ram_size, NULL)
"Image 格式就是普通的bin文件, 只不过有一个MS-DOS的header, 当前解析出的kernel_entry 就是 start_addr 0x80000000"
\ -+ rom_add_file_fixed_as(filename, addr, -1, NULL) "AddressSpace 为 NULL"
\ -+ rom_add_file(filename, NULL, addr, -1, false, NULL, NULL)
"rom_add_file的主要作用是分配一个Rom结构体, 这里指定的mr MemoryRegion 和 AddressSpace 都为空"
\ - rom->addr = addr
| - rom->name = g_strdup(file); "rom->name 为 file_name"
| - rom->rom_size = file_size
| - rom->data 为 file 的数据
| -+ rom_insert(rom); "将新分配的Rom挂到一个链表上"
\ - rom->as = &address_space_memory; "rom->as 知道默认的 address_space_memory上"
| - QTAILQ_INSERT_TAIL(&roms, rom, next); "将rom 挂到 roms 链表上"
| - snprintf(devpath, sizeof(devpath), "/rom@" "%016", addr);
| - add_boot_device_path(bootindex = -1, NULL, devpath);
| -|+ if machine->initrd_filename "带了initd的命令"
\ - hwaddr end = riscv_load_initrd(machine->initrd_filename, machine->ram_size, kernel_entry, &start);

qemu kvm 初始化:

1
2
3
4
5
6
7
#0  kvm_init (ms=0xaaaaaaac1926d0) at ../accel/kvm/kvm-all.c:2314
#1 0x00aaaaaaab644270 in accel_init_machine (accel=0xaaaaaaac1b6c30, ms=0xaaaaaaac1926d0) at ../accel/accel-softmmu.c:39
#2 0x00aaaaaaab36eafc in do_configure_accelerator (opaque=0xffffffc0d2880d, opts=0xaaaaaaac1b4ba0, errp=0xaaaaaaabf74170 <error_fatal>) at ../softmmu/vl.c:2352
#3 0x00aaaaaaabb0b4cc in qemu_opts_foreach (list=0xaaaaaaabe2fc40 <qemu_accel_opts>, func=0xaaaaaaab36e9ac <do_configure_accelerator>, opaque=0xffffffc0d2880d, errp=0xaaaaaaabf74170 <error_fatal>) at ../util/qemu-option.c:1135
#4 0x00aaaaaaab36ee2c in configure_accelerators (progname=0xffffffc0d28e03 "qemu-system-riscv64") at ../softmmu/vl.c:2418
#5 0x00aaaaaaab37240c in qemu_init (argc=17, argv=0xffffffc0d28b68, envp=0xffffffc0d28bf8) at ../softmmu/vl.c:3725
#6 0x00aaaaaaab323bb4 in main (argc=17, argv=0xffffffc0d28b68, envp=0xffffffc0d28bf8) at ../softmmu/main.c:49

为了弄懂上述流程, 有必要先对qemu的QOM 有一个大概的了解, 否则上面代码的调用链是不容易弄清楚的.

QOM介绍

QOM的全称是QEMU Object Model,顾名思义,这是QEMU中对象的一个抽象层.
一般来讲,对象是C++这类面向对象编程语言中的概念。面向对象的思想包括继承、封装与多态,这些思想在大型项目中能够更好地对程序进行组织与设计。Linux内核与QEMU虽然都是C语言的项目,但是都充满了面向对象的思想,QEMU中体现这一思想的就是QOM。QEMU的代码中充满了对象,特别是设备模拟,如网卡、串口、显卡等都是通过对象来抽象的。

QOM用C语言基本上实现了继承、封装、多态特点。

如网卡是一个类,它的父类是一个PCI设备类,这个PCI设备类的父类是设备类,此即继承。QEMU通过QOM可以对QEMU中的各种资源进行抽象管理(如设备模拟中的设备创建配置销毁)。
QOM还用于各种后端组件(如MemoryRegion,Machine等)的抽象,毫不夸张地说,QOM遍布于QEMU代码。这一节会对QOM进行详细介绍,以帮助读者理解QOM,进而更加方便地阅读QEMU代码。

要理解QOM,首先需要理解类型和对象的区别。类型表示种类,对象表示该种类中一个具体的对象。比如QEMU命令行中指定-device edu,id=edu1,-device edu,id=edu2,edu本身是一个种类,创建了edu1和edu2两个对象。QOM整个运作包括3个部分,即类型的注册、类型的初始化以及对象的初始化,3个部分涉及的函数如图所示

type_init是一个宏,并且除了type_init还有其他几个init宏,比如block_init、opts_init、trace_init等,每个宏都表示一类module,均通过module_init按照不同的参数构造出来
各个QOM类型最终通过函数register_module_init注册到了系统,其中function是每个类型都需要实现的初始化函数,type表示是MODULE_INIT_QOM。这里的constructor是编译器属性,编译器会把带有这个属性的函数do_qemu_init_##function放到特殊的段中,带有这个属性的函数会早于main函数执行,也就是说所有的QOM类型注册在main执行之前就已经执行了

register_module_init函数以类型的初始化函数以及所属类型(对QOM类型来说是MODULE_INIT_QOM)构建出一个ModuleEntry,然后插入到对应module所属的链表中,所有module的链表存放在一个init_type_list数组中。

QEMU使用的各个类型在main函数执行之前就统一注册到了init_type_list [MODULE_INIT_QOM]这个链表中。
进入main函数后不久就以MODULE_INIT_QOM为参数调用了函数module_call_init,这个函数执行了init_type_list[MODULE_INIT_QOM]链表上每一个ModuleEntry的init函数。

type_init - 类注册

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
struct TypeInfo
{
const char *name; //The name of the type.
const char *parent; //The name of the parent type.

size_t instance_size; //The size of the object
size_t instance_align; // The required alignment of the object.
void (*instance_init)(Object *obj); //This function is called to initialize an object.
void (*instance_post_init)(Object *obj); // This function is called to finish initialization of an object
void (*instance_finalize)(Object *obj); //This function is called during object destruction

bool abstract; //If this field is true, then the class is considered abstract and cannot be directly instantiated.
size_t class_size; //The size of the class object

void (*class_init)(ObjectClass *klass, void *data); //This function is called after all parent class initialization has occurred
// to allow a class to set its default virtual method pointers.
void (*class_base_init)(ObjectClass *klass, void *data); // This function is called for all base classes after all
  // parent class initialization has occurred, but before the class itself is initialized.
void *class_data; //Data to pass to the @class_init @class_base_init

InterfaceInfo *interfaces; //The list of interfaces associated with this type.
};
struct TypeImpl
{
const char *name; //表示类型名字 比如edu,isa-i8259等
size_t class_size; //表示所属类的大小
size_t instance_size; // 该类所属实例的大小
size_t instance_align;

void (*class_init)(ObjectClass *klass, void *data);
void (*class_base_init)(ObjectClass *klass, void *data); //表示类相关的初始化函数

void *class_data;

// 表示该类所属实例相关的初始化与销毁函数
void (*instance_init)(Object *obj);
void (*instance_post_init)(Object *obj);
void (*instance_finalize)(Object *obj);

bool abstract; //abstract表示类型是否是抽象的,与C++中的abstract类型类似,抽象类型不能直接创建实例,只能创建其子类所属实例;

// parent和parent_type表示父类型的名字和对应的类型信息,parent_type是一个TypeImpl
const char *parent;
TypeImpl *parent_type;

ObjectClass *class; //class是一个指向ObjectClass的指针,保存了该类型的基本信息;

// 类型的接口信息
int num_interfaces;
InterfaceImpl interfaces[MAX_INTERFACES];
};

TypeInfo表示的是类型信息,其中parent成员表示的是父类型的名字,instance_size和instance_init成员表示该类型对应的实例大小以及实例的初始化函数,class_init成员表示该类型的类初始化函数。

type类型 有 TYPE_MACHINE TYPE_ACCEL TYPE_QIO_CHANNEL TYPE_CPU TYPE_SYS_BUS_DEVICE TYPE_INTERFACE TYPE_NAND TYPE_USB_DEVICE 等

type_register_internal函数很简单,type_new函数首先通过一个TypeInfo结构构造出一个TypeImpl,type_table_add则将这个TypeImpl加入到一个哈希表中。这个哈希表的keyTypeImpl的名字,valueTypeImpl本身的值。这一过程完成了从TypeInfo到TypeImpl的转变,并且将其插入到了一个哈希表中。TypeImpl的数据基本上都是从TypeInfo复制过来的,表示的是一个类型的基本信息。在C++中,可以使用class关键字定义一个类型。QEMU使用C语言实现面向对象时也必须保存对象的类型信息,所以在TypeInfo里面指定了类型的基本信息,然后在初始化的时候复制到TypeImpl的哈希表中。

name表示类型名字,比如edu,isa-i8259等;class_size, instance_size表示所属类的大小以及该类所属实例的大小;class_init, class_base_init, class_finalize表示类相关的初始化与销毁函数,这类函数只会在类初始化的时候进行调用;instance_init, instance_post_init, instance_finalize表示该类所属实例相关的初始化与销毁函数;abstract表示类型是否是抽象的,与C++中的abstract类型类似,抽象类型不能直接创建实例,只能创建其子类所属实例;parent和parent_type表示父类型的名字和对应的类型信息,parent_type是一个TypeImpl;class是一个指向ObjectClass的指针,保存了该类型的基本信息;num_interfaces和interfaces描述的是类型的接口信息

  1. __attribute__((constructor))的修饰让type_init在main之前执行,type_init的参数是XXX_register_types函数指针,将函数指针传递到ModuleEntry的init函数指针,最后就是将这个ModuleEntry插入到ModuleTypeList
  2. main函数中的module_call_init(MODULE_INIT_QOM);调用了MODULE_INIT_QOM类型的ModuleTypeList中的所有ModuleEntry中的init()函数,也就是第一步type_init的第一个参数XXX_register_types函数指针
  3. XXX_register_types函数的操作,就是创建TypeImpl的哈希表
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
static const TypeInfo kvm_accel_type = {
.name = TYPE_KVM_ACCEL,
.parent = TYPE_ACCEL, //TYPE 类型为 TYPE_ACCEL
.instance_init = kvm_accel_instance_init,
.class_init = kvm_accel_class_init,
.instance_size = sizeof(KVMState),
};

// XXX_register_types 函数指针 main 函数调用 module_call_init(MODULE_INIT_QOM) 时执行
static void kvm_type_init(void)
{
// 注册 TypeImpl的哈希表
type_register_static(&kvm_accel_type);
}

type_init(kvm_type_init);


static const TypeInfo virt_machine_typeinfo = {
.name = MACHINE_TYPE_NAME("virt"),
.parent = TYPE_MACHINE, //TYPE 类型为 TYPE_MACHINE
.class_init = virt_machine_class_init,
.instance_init = virt_machine_instance_init,
.instance_size = sizeof(RISCVVirtState),
};

// XXX_register_types 函数指针 main 函数调用 module_call_init(MODULE_INIT_QOM) 时执行
static void virt_machine_init_register_types(void)
{
// 注册 TypeImpl的哈希表
type_register_static(&virt_machine_typeinfo);
}

type_init(virt_machine_init_register_types)

类初始化 - class_init

在C++等面向对象的编程语言中,当程序声明一个类型的时候,就已经知道了其类型的信息,比如它的对象大小。但是如果使用C语言来实现面向对象的这些特性,就需要做特殊的处理,对类进行单独的初始化。

程序在main 之前已经通过module_call_init(MODULE_INIT_QOM)在一个哈希链表中保存了所有的类型信息TypeImpl。
接下来就需要对类进行初始化了。类的初始化是通过type_initialize函数完成的,函数的输入是表示类型信息的TypeImpl类型ti。

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
static void type_initialize(TypeImpl *ti) {
if (ti->class) {
return;
}
// 第一件事是设置相关的filed,比如class_size和instance_size,
ti->class_size = type_class_get_size(ti);
ti->instance_size = type_object_get_size(ti);
ti->class = g_malloc0(ti->class_size);

// 第二件事就是初始化所有父类类型,不仅包括实际的类型,也包括接口这种抽象类型。
parent = type_get_parent(ti);
type_initialize(parent);
memcpy(ti->class, parent->class, parent->class_size);
// 遍历parent的接口, 挨个初始化 parent的 接口类型
for (e = parent->class->interfaces; e; e = e->next) {
InterfaceClass *iface = e->data;
ObjectClass *klass = OBJECT_CLASS(iface);
type_initialize_interface(ti, iface->interface_type, klass->type);
}
// 遍历自己的接口, 挨个初始化自己的接口类型
for (i = 0; i < ti->num_interfaces; i++) {
TypeImpl *t = type_get_by_name(ti->interfaces[i].typename);
for (e = ti->class->interfaces; e; e = e->next) {
TypeImpl *target_type = OBJECT_CLASS(e->data)->type;

if (type_is_ancestor(target_type, t)) {
break;
}
}
type_initialize_interface(ti, t, t);
}
// 依次调用所有父类的class_base_init
while (parent) {
if (parent->class_base_init) {
parent->class_base_init(ti->class, ti->class_data);
}
parent = type_get_parent(parent);
}
// 最后调用自己的 class_init 函数
if (ti->class_init) {
ti->class_init(ti->class, ti->class_data);
}
}

static size_t type_class_get_size(TypeImpl *ti)
{
if (ti->class_size) {
return ti->class_size;
}
// 自己没有该field (class_size 为0), 则会从其parent 继承
if (type_has_parent(ti)) {
return type_class_get_size(type_get_parent(ti));
}
return sizeof(ObjectClass);
}

启动虚拟机时指定了Machine(-M)参数, 调用链main->select_machine->object_class_get_list->object_class_foreach->object_class_foreach_tramp->type_initialize, object_class_get_list来得到所有TYPE_MACHINE类型组成的链表
object_class_get_list会调用object_class_foreachobject_class_foreach会对type_table中所有类型调用object_class_foreach_tramp函数,在该函数中会调用type_initialize函数。

最重要的是 object_class_foreach 会对type_table 中的所有类型调用 type_initialize , 注意是所有类型, 而不是单TYPE_MACHINE类会进行初始化
所有通过type_init 注册到type_table 中的类型都会在这个时候调用其 class_init 函数, 包括virt mahine 注册的 virt_machine_class_init 函数也会在这个地方调用.

类的层次结构

QOM通过这种TypeImpl 层次结构的 parent 实现了类似C++中的继承概念
还是以 virt machine 距离, 这个类的类名叫 virt-machine 其父类的类名叫 “machine”, 再往上找 parent 的类名叫 object
MACHINE_TYPE_NAME(“virt”) -> TYPE_MACHINE -> TYPE_OBJECT
TYPE_OBJECT是所有能够初始化实例的最终祖先,类似的,所有interface的祖先都是TYPE_INTERFACE

在类型的初始化函数type_initialize中会调用ti->class=g_malloc0(ti->class_size)语句来分配类型的class结构,这个结构实际上代表了类型的信息。类似于C++定义的一个类,从前面的分析看到ti->class_size为TypeImpl中的值,如果类型本身没有定义就会使用父类型的class_size进行初始化。

MACHINE_TYPE_NAME(“virt”) 并没有class_size, 在type_initialize时, 其class_size为0时, 会从其parent 上继承class_size.

1
2
3
4
5
6
7
8
9
10
11
static const TypeInfo machine_info = {
.name = TYPE_MACHINE,
.parent = TYPE_OBJECT,
.abstract = true,
.class_size = sizeof(MachineClass),
.class_init = machine_class_init,
.class_base_init = machine_class_base_init,
.instance_size = sizeof(MachineState),
.instance_init = machine_initfn,
.instance_finalize = machine_finalize,
};

TypeImpl 类相比TypeInfo类多了 class 的成员. 再回头看下type_initialize 实现, 把parent的class 拷贝给了自己

1
2
3
4
ti->class = g_malloc0(ti->class_size);
ti->type_initialize(parent); // 父类初始化, 调用其class_init 函数
memcpy(ti->class, parent->class, parent->class_size);
ti->class_init(ti->class, ti->class_data);

对于virt_machine 其class 是来自于 parent TYPE_MACHINE的.
TYPE_MACHINE的class_init 为 machine_class_init, parent 会进行一些公共对象的默认配置初始化, 子类的初始化会覆盖掉父类的一些配置.
这一点同C++的类是一样的
每个类中有private public 的 成员和函数, QOM 一样要有这些东西.

1
2
3
4
5
6
7
8
9
10
11
12
13
static void machine_class_init(ObjectClass *oc, void *data)
{
// ObjectClass 到 MachineClass的转换
MachineClass *mc = MACHINE_CLASS(oc);
mc->default_ram_size = 128 * MiB;
mc->rom_file_has_mr = true;
mc->numa_mem_align_shift = 23;
object_class_property_add_str(oc, "kernel", machine_get_kernel, machine_set_kernel);
...
object_class_property_set_description(oc, "memory-backend",
"Set RAM backend"
"Valid value is ID of hostmem based backend");
}

先看下MachineClass

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
struct MachineClass {
/*< private >*/
ObjectClass parent_class;
/*< public >*/

const char *family; /* NULL iff @name identifies a standalone machtype */
char *name;
const char *alias;
const char *desc;
const char *deprecation_reason;

void (*init)(MachineState *state);
void (*reset)(MachineState *state);
void (*wakeup)(MachineState *state);
int (*kvm_type)(MachineState *machine, const char *arg);

BlockInterfaceType block_default_type;
int units_per_default_bus;
int max_cpus;
int min_cpus;
...
HotplugHandler *(*get_hotplug_handler)(MachineState *machine,
DeviceState *dev);
bool (*hotplug_allowed)(MachineState *state, DeviceState *dev,
Error **errp);
CpuInstanceProperties (*cpu_index_to_instance_props)(MachineState *machine,
unsigned cpu_index);
const CPUArchIdList *(*possible_cpu_arch_ids)(MachineState *machine);
int64_t (*get_default_cpu_node_id)(const MachineState *ms, int idx);
ram_addr_t (*fixup_ram_size)(ram_addr_t size);

在看下 virt_machine class 初始化函数 virt_machine_class_init, virt_machine 没有新增成员和方法, 只是覆盖掉父类的一些默认设置

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 void virt_machine_class_init(ObjectClass *oc, void *data)
{
char str[128];
MachineClass *mc = MACHINE_CLASS(oc); //类型转换

mc->desc = "RISC-V VirtIO board";
mc->init = virt_machine_init;
mc->max_cpus = VIRT_CPUS_MAX;
mc->default_cpu_type = TYPE_RISCV_CPU_BASE;
mc->pci_allow_0_address = true;
mc->possible_cpu_arch_ids = riscv_numa_possible_cpu_arch_ids;
mc->cpu_index_to_instance_props = riscv_numa_cpu_index_to_props;
mc->get_default_cpu_node_id = riscv_numa_get_default_cpu_node_id;
mc->numa_mem_supported = true;
mc->default_ram_id = "riscv_virt_board.ram";

machine_class_allow_dynamic_sysbus_dev(mc, TYPE_RAMFB_DEVICE);

object_class_property_add_bool(oc, "aclint", virt_get_aclint,
virt_set_aclint);
object_class_property_set_description(oc, "aclint",
"Set on/off to enable/disable "
"emulating ACLINT devices");
object_class_property_add_str(oc, "aia", virt_get_aia,
virt_set_aia);
object_class_property_set_description(oc, "aia",
"Set type of AIA interrupt "
"conttoller. Valid values are "
"none, aplic, and aplic-imsic.");

object_class_property_add_str(oc, "aia-guests",
virt_get_aia_guests,
virt_set_aia_guests);
sprintf(str, "Set number of guest MMIO pages for AIA IMSIC. Valid value "
"should be between 0 and %d.", VIRT_IRQCHIP_MAX_GUESTS);
object_class_property_set_description(oc, "aia-guests", str);
}

类型的转换由 MACHINE_CLASS宏完成, 最终调用到 object_class_dynamic_cast函数, 动态转换, 类似于C++的dynamic_cast, 父类对象的指针转为子类的对象类型

静态成员是所有的对象共享的,而非静态的每一个对象都有一份, 面向对象中的基本概念,qemu 也实现了静态变量和静态函数
qemu 中 class 初始化对应着静态变量和静态函数的概念, 而instance 初始化对应着非静态的部分.
其中 ObjectClass 对应着静态变量即类对象, 而Object对应着非静态变量, 即所谓的对象变量

对象的构造与初始化

总结一下前面两节的内容,首先是每个类型指定一个TypeInfo注册到系统中,接着在系统运行初始化的时候会把TypeInfo转变成TypeImple放到一个哈希表(type_table)中,这就是类型的注册。系统会对这个哈希表中的每一个类型进行初始化,主要是设置TypeImpl的一些域以及调用类型的class_init函数,这就是类型的初始化。现在系统中已经有了所有类型的信息并且这些类型的初始化函数已经调用了,接着会根据需要(如QEMU命令行指定的参数)创建对应的实例对象

从上文可以看出,可以把QOM的对象构造分成3部分

  • 第一部分是类型的构造,通过TypeInfo构造一个TypeImpl的哈希表,这是在main之前完成的;
  • 第二部分是类型的初始化,这是在main中进行的,这两部分都是全局的,也就是只要编译进去的QOM对象都会调用, 初始化类(静态变量), 基类为ObjectClass
  • 第三部分是类对象的构造,这是构造具体的对象实例,初始化非静态变量(基类为Object), 只有在命令行指定了对应的设备时,才会创建对象。

还是以virt_machine的TypeInfo 来分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static const TypeInfo virt_machine_typeinfo = {
.name = MACHINE_TYPE_NAME("virt"),
.parent = TYPE_MACHINE, //TYPE 类型为 TYPE_MACHINE
.class_init = virt_machine_class_init,
.instance_init = virt_machine_instance_init,
.instance_size = sizeof(RISCVVirtState),
};
// parent
static const TypeInfo machine_info = {
.name = TYPE_MACHINE,
.parent = TYPE_OBJECT,
.abstract = true,
.class_size = sizeof(MachineClass),
.class_init = machine_class_init,
.class_base_init = machine_class_base_init,
.instance_size = sizeof(MachineState),
.instance_init = machine_initfn,
.instance_finalize = machine_finalize,
};

现在只是构造出了对象,并且调用了类初始化函数,但是RISCVVirtState里面的数据内容并没有填充,这个时候的virt_machine状态并不是可用的

调用链
object_new->object_new_with_type->object_initialize_with_type->object_init_with_type

object_new通过传进来的typename参数找到对应的TypeImpl,再调用object_new_with_type,该函数首先调用type_initialize确保类型已经经过初始化,然后分配type->instance_size作为大小分配对象的实际空间,接着调用object_initialize_with_type对对象进行初始化。

object_initialize_with_type的主要工作是对object_init_with_type和object_post_init_with_type进行调用,前者通过递归调用所有父类型的对象初始化函数和自身对象的初始化函数,后者调用TypeImpl的instance_post_init回调成员完成对象初始化之后的工作。
简单的说就是先递归调父类的 instance_init, 再调自己的instance_init, 再调自己的instance_post_init, 再依次调父类的instane_post_init

实例化是对某个特定的class 对象而言的, 在找到class 后, 进行实例化, 类似于C++ 的 new class 操作

对于virt_machine 来说, 先调父类的 machine_initfn, 因对象层级只有 MACHINE_TYPE_NAME("virt") -> TYPE_MACHINE -> TYPE_OBJECT, 所以parent层级调到TYPE_MACHINE 层级就可以了, 这里比较简单, 因为virt_machine 和其父类没有instance_post_init , 所以先调 machine_initfn 再调virt_machine_instance_init 就完事了

这里同样涉及到两个类型, RISCVVirtState -> MachineState -> Object (对象变量, 非静态部分)

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
struct RISCVVirtState {
/*< private >*/
MachineState parent;

/*< public >*/
RISCVHartArrayState soc[VIRT_SOCKETS_MAX];
DeviceState *irqchip[VIRT_SOCKETS_MAX];
PFlashCFI01 *flash[2];
FWCfgState *fw_cfg;

int fdt_size;
bool have_aclint;
RISCVVirtAIAType aia_type;
int aia_guests;
};
// parent
struct MachineState {
/*< private >*/
Object parent_obj;

/*< public >*/

void *fdt;
char *dtb;
char *dumpdtb;
int phandle_start;
char *dt_compatible;
bool dump_guest_core;
bool mem_merge;
bool usb;
bool usb_disabled;
char *firmware;
bool iommu;
bool suppress_vmdesc;
bool enable_graphics;
ConfidentialGuestSupport *cgs;
char *ram_memdev_id;
/*
* convenience alias to ram_memdev_id backend memory region
* or to numa container memory region
*/
MemoryRegion *ram;
DeviceMemoryState *device_memory;

ram_addr_t ram_size;
ram_addr_t maxram_size;
uint64_t ram_slots;
const char *boot_order;
const char *boot_once;
char *kernel_filename;
char *kernel_cmdline;
char *initrd_filename;
const char *cpu_type;
AccelState *accelerator;
CPUArchIdList *possible_cpus;
CpuTopology smp;
struct NVDIMMState *nvdimms_state;
struct NumaState *numa_state;
};

backtrace:

1
2
3
4
5
6
7
8
9
#0  machine_initfn (obj=0xaaaaaab0b756d0) at ../hw/core/machine.c:898
#1 0x00aaaaaab02d0b90 in object_init_with_type (obj=0xaaaaaab0b756d0, ti=0xaaaaaab097e9d0) at ../qom/object.c:377
#2 0x00aaaaaab02d0b74 in object_init_with_type (obj=0xaaaaaab0b756d0, ti=0xaaaaaab0998900) at ../qom/object.c:373
#3 0x00aaaaaab02d1238 in object_initialize_with_type (obj=0xaaaaaab0b756d0, size=3672, type=0xaaaaaab0998900) at ../qom/object.c:519
#4 0x00aaaaaab02d1a50 in object_new_with_type (type=0xaaaaaab0998900) at ../qom/object.c:734
#5 0x00aaaaaab02d1a98 in object_new_with_class (klass=0xaaaaaab0b6a060) at ../qom/object.c:742
#6 0x00aaaaaaafd51050 in qemu_create_machine (qdict=0xaaaaaab09b38d0) at ../softmmu/vl.c:2151
#7 0x00aaaaaaafd55380 in qemu_init (argc=17, argv=0xffffffced2ab68, envp=0xffffffced2abf8) at ../softmmu/vl.c:3708
#8 0x00aaaaaaafd06bb4 in main (argc=17, argv=0xffffffced2ab68, envp=0xffffffced2abf8) at ../softmmu/main.c:49

这里 obj 怎么解出 MachineState 和 MachineClass 指针的?

obj->class 指向 MachineClass
而 obj 本身就是instance.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void machine_initfn(Object *obj)
{
MachineState *ms = MACHINE(obj); // obj本身
MachineClass *mc = MACHINE_GET_CLASS(obj); //obj->class

container_get(obj, "/peripheral");
container_get(obj, "/peripheral-anon");

ms->dump_guest_core = true;
ms->mem_merge = true;
ms->enable_graphics = true;
ms->kernel_cmdline = g_strdup("");
...
/* default to mc->default_cpus */
ms->smp.cpus = mc->default_cpus;
ms->smp.max_cpus = mc->default_cpus;
ms->smp.sockets = 1;
ms->smp.dies = 1;
ms->smp.clusters = 1;
ms->smp.cores = 1;
ms->smp.threads = 1;
}

属性

QOM实现了类似于C++的基于类的多态,一个对象按照继承体系可以是Object、MachineState、RISCVVirtState等。
在QOM中为了便于对对象进行管理,还给类以及对象增加了属性, 即property 也是划分为 static 和 Non-staic 的:

  • 类属性存在于ObjectClassproperties域中,这个域是在类型初始化函数type_initialize中构造的, 属于静态部分, 同类所有对象实例之间共享
  • 对象属性存放在Objectproperties域中,这个域是在对象的初始化函数object_initialize_with_type中构造的, 属于非静态部分, 同类的不同对象实例之间不共享
    两者都是一个哈希表,存着属性名字到ObjectProperty的映射。
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
struct Object
{
/* private: */
ObjectClass *class;
ObjectFree *free;
GHashTable *properties;
uint32_t ref;
Object *parent;
};
struct ObjectProperty
{
char *name; //name表示名字
char *type; //表示属性的类型,如有的属性是字符串,有的是bool类型,有的是link等其他更复杂的类型
char *description;
// get、set、resolve等回调函数则是对属性进行操作的函数
ObjectPropertyAccessor *get;
ObjectPropertyAccessor *set;
ObjectPropertyResolve *resolve;
ObjectPropertyRelease *release;
ObjectPropertyInit *init;
// opaque指向一个具体的属性,如BoolProperty等。
void *opaque;
QObject *defval;
};
typedef struct BoolProperty
{
bool (*get)(Object *, Error **);
void (*set)(Object *, bool, Error **);
} BoolProperty;

static void object_initialize_with_type(Object *obj, size_t size, TypeImpl *type)
{
type_initialize(type);
memset(obj, 0, type->instance_size);
obj->class = type->class;
object_ref(obj);
object_class_property_init_all(obj);
obj->properties = g_hash_table_new_full(g_str_hash, g_str_equal, NULL, object_property_free);
object_init_with_type(obj, type);
object_post_init_with_type(obj, type);
}
static void object_class_property_init_all(Object *obj)
{
ObjectPropertyIterator iter;
ObjectProperty *prop; //属性挂入hash表

object_class_property_iter_init(&iter, object_get_class(obj));
while ((prop = object_property_iter_next(&iter))) {
if (prop->init) {
prop->init(obj, prop);
}
}
}

每一种具体的属性都会有一个结构体来描述它。比如下面的LinkProperty表示link类型的属性,StringProperty表示字符串类型的属性,BoolProperty表示bool类型的属性。
属性的添加分为类属性的添加对象属性的添加,以对象属性为例,它的属性添加是通过object_property_add接口完成的。
BoolProperty 属于对象属性

object_property_add函数首先调用object_property_find来确认所插入的属性是否已经存在,确保不会添加重复的属性,接着分配一个ObjectProperty结构并使用参数进行初始化,然后调用g_hash_table_insert插入到对象的properties域中。

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
ObjectProperty *
object_class_property_add_bool(ObjectClass *klass, const char *name,
bool (*get)(Object *, Error **),
void (*set)(Object *, bool, Error **))
{
BoolProperty *prop = g_malloc0(sizeof(*prop));
prop->get = get;
prop->set = set;
return object_class_property_add(klass, name, "bool",
// property_get_bool 会调用prop->get, 这里有点啰嗦了, 最终还是调的传参的get
get ? property_get_bool : NULL,
set ? property_set_bool : NULL,
NULL,
prop);
}
ObjectProperty *
object_class_property_add(ObjectClass *klass,
const char *name,
const char *type,
ObjectPropertyAccessor *get,
ObjectPropertyAccessor *set,
ObjectPropertyRelease *release,
void *opaque)
{
ObjectProperty *prop;
assert(!object_class_property_find(klass, name)); // 调用find 查 class 中是否已经有对应属性
prop = g_malloc0(sizeof(*prop));
prop->name = g_strdup(name);
prop->type = g_strdup(type);
prop->get = get;
prop->set = set;
prop->release = release;
prop->opaque = opaque;
g_hash_table_insert(klass->properties, prop->name, prop);
return prop;
}

每一种属性类型都有自己的set函数 get 函数, 如bool类型的为 property_get_bool property_set_bool,

来看下edu设备,在qdev_device_add函数的后面,会调用以下代码

1
object_property_set_bool(OBJECT(dev), "realized", true, errp);

edu设备实例化时并未添加realized属性的过程,那么这是在哪里实现的呢?

edu 设备的层级
TYPE_EDU_DEVICE -> TYPE_PCI_DEVICE -> TYPE_DEVICE -> TYPE_OBJECT

可以从其父对象的class_int 和 instance_init 中找到

1
2
3
static void device_class_init(ObjectClass *class, void *data) {
object_class_property_add_bool(class, "realized", device_get_realized, device_set_realized);
}

object_property_add_bool(“realized”) 时设置的 set 回调 函数为device_set_realized, 所以调用 object_property_set_bool 实际是调用的 set 回调函数 device_set_realized

调用 object_property_set_bool(name)函数时, 调用的是 object_property_add_bool(name, get, set) 设置的 set 回调

其调用了DeviceClass的realize函数, 对PCI设备而言,其类型初始化函数为pci_device_class_init,在该函数中设置了其DeviceClass的realize为qdev_realize函数, 最终调用到 pci_qdev_realize 函数.

1
2
3
4
5
6
7
8
static void pci_device_class_init(ObjectClass *klass, void *data)
{
DeviceClass *k = DEVICE_CLASS(klass);
k->realize = pci_qdev_realize;
k->unrealize = pci_qdev_unrealize;
k->bus_type = TYPE_PCI_BUS;
device_class_set_props(k, pci_props);
}

由上可见, 分析qemu的类的层级是第一时间要做的事, 不然即使能找到具体设备的代码, 但是后面实例化包括class 初始化的时候全是函数指针, 如果不从父类溯源, 根本找不到函数指针具体的指向.

设置设备realized属性的过程叫作设备的具现化, 只有已经具现化的设备才能被使用

bool属性是比较简单的属性,这里再对两个特殊的属性进行简单的介绍,即child属性和link属性。

child属性

child属性表示对象之间的从属关系,父对象的child属性指向子对象,child属性的添加函数为object_property_add_child

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
ObjectProperty *
object_property_try_add(Object *obj, const char *name, const char *type,
ObjectPropertyAccessor *get,
ObjectPropertyAccessor *set,
ObjectPropertyRelease *release,
void *opaque, Error **errp)
{
ObjectProperty *prop;
size_t name_len = strlen(name);
// 支持递归添加
if (name_len >= 3 && !memcmp(name + name_len - 3, "[*]", 4)) {
int i;
ObjectProperty *ret = NULL;
char *name_no_array = g_strdup(name);
name_no_array[name_len - 3] = '\0';
for (i = 0; i < INT16_MAX; ++i) {
char *full_name = g_strdup_printf("%s[%d]", name_no_array, i);
ret = object_property_try_add(obj, full_name, type, get, set,
release, opaque, NULL);
if (ret) {
break;
}
}
return ret;
}
prop = g_malloc0(sizeof(*prop));
prop->name = g_strdup(name);
prop->type = g_strdup(type); //type 为 "child<name>"
prop->get = get; //get 为 object_get_child_property
prop->set = set; // set 为 null
prop->release = release;
prop->opaque = opaque; //opaque 为 child
g_hash_table_insert(obj->properties, prop->name, prop);
return prop;
}
ObjectProperty *
object_property_try_add_child(Object *obj, const char *name,
Object *child, Error **errp)
{
g_autofree char *type = NULL;
ObjectProperty *op;
assert(!child->parent);
type = g_strdup_printf("child<%s>", object_get_typename(child));
op = object_property_try_add(obj, name, type, object_get_child_property,
NULL, object_finalize_child_property,
child, errp);
op->resolve = object_resolve_child_property;
object_ref(child);
child->parent = obj;
return op;
}

首先根据参数中的name(一般是子对象的名字)创建一个child<name>,构造出一个新的名字,然后用这个名字作为父对象的属性名字,将子对象添加到父对象的属性链表中,存放在ObjectProperty的opaque中。 通过对name 进行解析, 支持递归添加

link属性

link属性表示一种连接关系,表示一种设备引用了另一种设备,添加link属性的函数为object_property_add_link
这个函数将会添加obj对象的link<type>属性, 在OBJ_PROP_LINK_DIRECT 模式下 prop->target 指向了另一个设备, 而没有指定 OBJ_PROP_LINK_DIRECT 时, 由prop->targetp 指向了另一个设备
参考 https://martins3.github.io/qemu/qom.html#link

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
typedef struct {
union {
Object **targetp;
Object *target; /* if OBJ_PROP_LINK_DIRECT, when holding the pointer */
ptrdiff_t offset; /* if OBJ_PROP_LINK_CLASS */
};
void (*check)(const Object *, const char *, Object *, Error **);
ObjectPropertyLinkFlags flags;
} LinkProperty;

static ObjectProperty *
object_add_link_prop(Object *obj, const char *name,
const char *type, void *ptr,
void (*check)(const Object *, const char *,
Object *, Error **),
ObjectPropertyLinkFlags flags)
{
LinkProperty *prop = g_malloc(sizeof(*prop));
g_autofree char *full_type = NULL;
ObjectProperty *op;
// 重点是 target 和 targetp 这两个字段, 指向target
if (flags & OBJ_PROP_LINK_DIRECT) {
prop->target = ptr;
} else {
prop->targetp = ptr;
}
prop->check = check;
prop->flags = flags;

full_type = g_strdup_printf("link<%s>", type);
op = object_property_add(obj, name, full_type,
object_get_link_property,
check ? object_set_link_property : NULL,
object_release_link_property,
prop);
op->resolve = object_resolve_link_property;
return op;
}

QOM composition tree

property 中间不仅仅可以存储 str / int 之类基本类型,还可以用于存储 Object 。 通过 link 和 child 类型的 property 可以构建出来 QOM tree
在 QEMU monitor 中使用 info qom-tree 可以查看 QOM tree

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
(qemu) info qom-tree
/machine (virt-machine)
/fw_cfg (fw_cfg_mem)
/fwcfg.ctl[0] (memory-region)
/fwcfg.data[0] (memory-region)
/fwcfg.dma[0] (memory-region)
/peripheral (container)
/peripheral-anon (container)
/device[0] (virtio-blk-device)
/soc0 (riscv.hart_array)
/harts[0] (rv64-riscv-cpu)
/unnamed-gpio-in[0] (irq)
...
/harts[1] (rv64-riscv-cpu)
/unnamed-gpio-in[0] (irq)
...
/unattached (container)
/device[0] (riscv.sifive.plic)
/riscv.sifive.plic[0] (memory-region)
/unnamed-gpio-in[0] (irq)
...
/device[10] (gpex-pcihost)
/gpex_ioport[0] (memory-region)
/pcie-mmio-high[0] (memory-region)
/pcie-mmio[0] (memory-region)
/pcie.0 (PCIE)
/device[7] (virtio-mmio)
/virtio-mmio-bus.5 (virtio-mmio-bus)
/virtio-mmio[0] (memory-region)
/io[0] (memory-region)
/riscv_virt_board.mrom[0] (memory-region)
/sysbus (System)
/system[0] (memory-region)
/virt.flash0 (cfi.pflash01)
/virt.flash0[0] (memory-region)
/virt.flash1 (cfi.pflash01)
/virt.flash1[0] (memory-region)

然后就可以通过路径直接获取到一个 object 了,例如:

1
MemoryRegion *ram = (MemoryRegion *) object_resolve_path("/machine/unattached/system[0]", NULL);

路径解析的一般过程为:

  • object_resolve_path_type
    • object_resolve_abs_path
      • object_resolve_path_component
        • object_property_find
        • ObjectProperty::resolve 也就是 object_resolve_link_property 或者 object_resolve_child_property

在 QEMU 中 link 作用还可以和 object_property_add_str 类似,只是将 string 替换为 * object

  • 通过 object_property_add_link 创建 property

rom 初始化

QEMU会在qdev_machine_creation_done函数中调用rom_check_and_register_reset,后者的主要工作是将rom_reset挂到reset_handlers链表上,当虚拟机重置时会调用该链表上的每一个函数, 即虚拟机重置时会调用 rom_reset 函数

前面讲到将kernel 镜像读出了放到了rom->data 上, 并将rom 挂到了 roms 链表上
而当虚拟机重置时, 调用rom_reset, rom_rest 会遍历roms上的所有rom, 将rom->data 拷贝到所属的 MemoryRegion 空间

1
2
3
4
5
6
7
8
9
10
11
12
13
-+ rom_reset()
\ -+ QTAILQ_FOREACH(rom, &roms, next)
\ -|+ if rom->mr
\ - void *host = memory_region_get_ram_ptr(rom->mr);
| - memcpy(host, rom->data, rom->datasize); "将rom->data 拷贝到 rom对应的memory_region上"
| - memset(host + rom->datasize, 0, rom->romsize - rom->datasize); "memory_region 中剩余的部分写0"
| -|+ else if !rom->mr "前面kernel 镜像对应的rom->mr 为空, 满足条件走这里"
\ - address_space_write_rom(rom->as, rom->addr, MEMTXATTRS_UNSPECIFIED, rom->data, rom->datasize);
"将rom->data 拷贝到初始的 address_space_memory 对应的MemoryRegion中"
| - address_space_set(rom->as, rom->addr + rom->datasize, 0, "将初始的address_space_memory MemoryRegion 剩余的部分写0"
rom->romsize - rom->datasize, MEMTXATTRS_UNSPECIFIED);
| - cpu_flush_icache_range(rom->addr, rom->datasize);

QEMU 中有两个全局的静态 AddressSpace

1
2
static AddressSpace address_space_memory; // 内存地址空间
static AddressSpace address_space_io; // I/O 地址空间

在 QEMU 的exec.c中也定义了两个静态的 MemoryRegion 指针变量

1
2
static MemoryRegion *system_memory; // 内存 MemoryRegion,对应 address_space_memory
static MemoryRegion *system_io; // I/O MemoryRegion,对应 address_space_io

root域分别指向之后会提到的两个 MemoryRegion 类型变量:system_memorysystem_io
可以看到 kernel的镜像正是被拷贝到全局的 address_space_memory 对应的 MemoryRegion 中了.
MemoryRegion 表示在 Guest Memory Layout 中的一段内存区域,它是联系 GPA 和 RAMBlocks(描述真实内存)之间的桥梁
MemoryRegion 用来描述一段逻辑层面上的内存区域,而记录实际分配的内存地址信息的结构体则是 RAMBlock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct RAMBlock {
struct rcu_head rcu;
struct MemoryRegion *mr;
uint8_t *host; // HVA
uint8_t *colo_cache;
ram_addr_t offset; //GPA
ram_addr_t used_length;
ram_addr_t max_length;
void (*resized)(const char*, uint64_t length, void *host);
uint32_t flags;
char idstr[256];
QLIST_ENTRY(RAMBlock) next; //指向下一个RAMBlock
...
};

该结构表示的是虚拟机中的一块内存条,里面记录了该内存条的一些基本信息,如所属的mr、该文件对应的fd(如果有文件作为后端)、系统的页面大小page_size、已经使用的大小used_length等。RMABlock的offset成员表示该内存条在虚拟机整个内存中的偏移,qemu_ram_alloc_internal会初始化部分成员。所有的RAMBlock会通过next域连接到一个链表中,链表头是ram_list.blocks全局变量。

该结构的初始化是在 qemu_ram_alloc_internal 中完成的
virt.flash0 初始化过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#0  qemu_ram_alloc_internal (size=33554432, max_size=33554432, resized=0x0, host=0x0, ram_flags=0, mr=0xaaaaaad80f78a0, errp=0xfffffff4a8f290) at ../softmmu/physmem.c:2149
#1 0x00aaaaaad72b1368 in qemu_ram_alloc (size=33554432, ram_flags=0, mr=0xaaaaaad80f78a0, errp=0xfffffff4a8f290) at ../softmmu/physmem.c:2189
#2 0x00aaaaaad72a2afc in memory_region_init_rom_device_nomigrate (mr=0xaaaaaad80f78a0, owner=0xaaaaaad80f74f0, ops=0xaaaaaad785b1a0 <pflash_cfi01_ops>, opaque=0xaaaaaad80f74f0, name=0xaaaaaad80bfe70 "virt.flash1", size=33554432, errp=0xfffffff4a8f2f8) at ../softmmu/memory.c:1719
#3 0x00aaaaaad72a8b90 in memory_region_init_rom_device (mr=0xaaaaaad80f78a0, owner=0xaaaaaad80f74f0, ops=0xaaaaaad785b1a0 <pflash_cfi01_ops>, opaque=0xaaaaaad80f74f0, name=0xaaaaaad80bfe70 "virt.flash1", size=33554432, errp=0xfffffff4a8f3c0) at ../softmmu/memory.c:3516
#4 0x00aaaaaad6f45aac in pflash_cfi01_realize (dev=0xaaaaaad80f74f0, errp=0xfffffff4a8f3c0) at ../hw/block/pflash_cfi01.c:821
#5 0x00aaaaaad742b97c in device_set_realized (obj=0xaaaaaad80f74f0, value=true, errp=0xfffffff4a8f4b8) at ../hw/core/qdev.c:531
#6 0x00aaaaaad7437b64 in property_set_bool (obj=0xaaaaaad80f74f0, v=0xaaaaaad8115410, name=0xaaaaaad77d4f18 "realized", opaque=0xaaaaaad7b1ab60, errp=0xfffffff4a8f4b8) at ../qom/object.c:2273
#7 0x00aaaaaad74353f0 in object_property_set (obj=0xaaaaaad80f74f0, name=0xaaaaaad77d4f18 "realized", v=0xaaaaaad8115410, errp=0xfffffff4a8f4b8) at ../qom/object.c:1408
#8 0x00aaaaaad743a88c in object_property_set_qobject (obj=0xaaaaaad80f74f0, name=0xaaaaaad77d4f18 "realized", value=0xaaaaaad8115350, errp=0xaaaaaad7ab9170 <error_fatal>) at ../qom/qom-qobject.c:28
#9 0x00aaaaaad74357ec in object_property_set_bool (obj=0xaaaaaad80f74f0, name=0xaaaaaad77d4f18 "realized", value=true, errp=0xaaaaaad7ab9170 <error_fatal>) at ../qom/object.c:1477
#10 0x00aaaaaad742b0e0 in qdev_realize (dev=0xaaaaaad80f74f0, bus=0xaaaaaad7cdf0d0, errp=0xaaaaaad7ab9170 <error_fatal>) at ../hw/core/qdev.c:333
#11 0x00aaaaaad742b124 in qdev_realize_and_unref (dev=0xaaaaaad80f74f0, bus=0xaaaaaad7cdf0d0, errp=0xaaaaaad7ab9170 <error_fatal>) at ../hw/core/qdev.c:340
#12 0x00aaaaaad6f67c58 in sysbus_realize_and_unref (dev=0xaaaaaad80f74f0, errp=0xaaaaaad7ab9170 <error_fatal>) at ../hw/core/sysbus.c:261
#13 0x00aaaaaad719be64 in virt_flash_map1 (flash=0xaaaaaad80f74f0, base=570425344, size=33554432, sysmem=0xaaaaaad7c48c00) at ../hw/riscv/virt.c:145
#14 0x00aaaaaad719bf28 in virt_flash_map (s=0xaaaaaad7cd76d0, sysmem=0xaaaaaad7c48c00) at ../hw/riscv/virt.c:160
#15 0x00aaaaaad71a1ff4 in virt_machine_init (machine=0xaaaaaad7cd76d0) at ../hw/riscv/virt.c:1419
#16 0x00aaaaaad6f634d8 in machine_run_board_init (machine=0xaaaaaad7cd76d0) at ../hw/core/machine.c:1189
#17 0x00aaaaaad6eb4930 in qemu_init_board () at ../softmmu/vl.c:2656
#18 0x00aaaaaad6eb4c28 in qmp_x_exit_preconfig (errp=0xaaaaaad7ab9170 <error_fatal>) at ../softmmu/vl.c:2746
#19 0x00aaaaaad6eb7534 in qemu_init (argc=17, argv=0xfffffff4a8fb68, envp=0xfffffff4a8fbf8) at ../softmmu/vl.c:3776
#20 0x00aaaaaad6e68bb4 in main (argc=17, argv=0xfffffff4a8fb68, envp=0xfffffff4a8fbf8) at ../softmmu/main.c:49
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-+ qemu_ram_alloc_internal(ram_addr_t size, ram_addr_t max_size, void (*resized)(const char*, uint64_t length, void *host), 
void *host, uint32_t ram_flags, MemoryRegion *mr, Error **errp)
\ - new_block = g_malloc0(sizeof(*new_block));
| - new_block->mr = mr;
| - new_block->host = host;
| -+ ram_block_add(new_block, &local_err);
\ - old_ram_size = last_ram_page(); "表示未添加新的new_block之前的RAM大小"
| - new_ram_size = MAX(old_ram_size, (new_block->offset + new_block->max_length) >> TARGET_PAGE_BITS);
"表示添加了new_block之后的RAM大小,这两个值的单位都是页"
| -+ new_block->host = qemu_anon_ram_alloc(new_block->max_length, &new_block->mr->align, shared, noreserve);
"由于是新建RAM,new_block->host是NULL, 由qemu_anon_ram_alloc 分配,
所以host表示的是虚拟机物理内存 GPA 对应的QEMU进程地址空间的虚拟内存 HVA"
\ -+ void *ptr = qemu_ram_mmap(-1, size, align, qemu_map_flags, 0); "使用mmap 分配"
\ - mmap(0, total, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0)
| - memory_try_enable_merging(new_block->host, new_block->max_length);
| - cpu_physical_memory_set_dirty_range(new_block->offset, new_block->used_length, DIRTY_CLIENTS_ALL);
"设置新加的RAMBlock的内存区段为脏页"
| - qemu_madvise(new_block->host, new_block->max_length, QEMU_MADV_HUGEPAGE);
"会调用madvise对操作系统提供一些指示, 在大页存在的情况下会使用大页"
| - ram_block_notify_add(new_block->host, new_block->used_length, new_block->max_length);
""
1
2
3
4
5
6
7
[    0.000000] Virtual kernel memory layout:
[ 0.000000] fixmap : 0xffffffcefee00000 - 0xffffffceff000000 (2048 kB)
[ 0.000000] pci io : 0xffffffceff000000 - 0xffffffcf00000000 ( 16 MB)
[ 0.000000] vmemmap : 0xffffffcf00000000 - 0xffffffcfffffffff (4095 MB)
[ 0.000000] vmalloc : 0xffffffd000000000 - 0xffffffdfffffffff (65535 MB)
[ 0.000000] lowmem : 0xffffffe000000000 - 0xffffffe008000000 ( 128 MB)
[ 0.000000] kernel : 0xffffffff80000000 - 0xffffffffffffffff (2047 MB)

QEMU 虚拟机内存初始化

main函数中会调用cpu_exec_init_all进行一些初始化工作, 其中两个函数的调用与内存相关,即io_mem_init和memory_map_init。第一个函数比较简单,就是创建若干个包含所有地址空间的MemoryRegion,如io_mem_rom和io_mem_unassigned。第二个函数memory_map_init则是一个重要函数

1
2
3
4
5
6
7
-+ memory_map_init
\ - system_memory = g_malloc(sizeof(*system_memory));
| - memory_region_init(system_memory, NULL, "system", UINT64_MAX);
| - address_space_init(&address_space_memory, system_memory, "memory"); "虚拟机的内存地址空间, 关联root system_memory"
| - system_io = g_malloc(sizeof(*system_io));
| - memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io", 65536); ""
| - address_space_init(&address_space_io, system_io, "I/O"); "I/O地址空间, 关联root system_io"
  • Address肯定会指向一段内存,这个由 AddressSpace 的 root 域表示。root 域是个 MemoryRegion 结构体,它关联一段逻辑内存
  • MemoryRegion
    • 可以是一个容器,用来管理内存,本身没有内存,比如 system_memory;
    • 可以是一个实体,它有自己的内存,比如 pc.ram;
    • 可以是一个别名,本身没有内存,但它指向实体内存的一部分,比如 ram-below-4g 和 ram-above-4g
      一个实体 MemoryRegion 和其它 MemoryRegion 的不同在于,它包含一段真正的物理内存,这个内存由 MemoryRegion 的 ram_block 域表示,ram_block 是个 RAMBlock 结构体,表示一段真正可用的内存,说它可用是因为它由 qemu 进程向内核通过 mmap 映射得到,对这段内存的访问和普通应用程序申请的内存没有什么两样。

qemu 开始分配虚机内存流程开始之前,要从命令行解析出用户配置的虚机内存大小,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ram_addr_t ram_size;	/* 全局变量 */
main
/* 解析到命令行参数为 -m */
case QEMU_OPTION_m:
/* 从全局变量vm_config_groups中找到内存的选项定义qemu_mem_opts
* 将命令行参数optarg解析后放到qemu_mem_opts中 */
opts = qemu_opts_parse_noisily(qemu_find_opts("memory"), optarg, true);
/* 根据内存参数设置内存大小*/
set_memory_options
/* 找到存放的内存参数 */
opts = qemu_find_opts_singleton("memory")
/* 解析出内存大小 */
sz = qemu_opt_get_size(opts, "size", ram_size)
/* 将内存大小保存到全局变量ram_size中 */
ram_size = sz
/* 将内存大小存放到machine的ram_size中 */
current_machine->ram_size = ram_size
qemu_init_board
create_default_memdev(current_machine, mem_path);
object_property_set_int(obj, "size", ms->ram_size, &error_fatal);
1
2
3
4
5
6
7
8
9
10
#0  qemu_ram_alloc (size=1073741824, ram_flags=0, mr=0xaaaaaae7fb1c60, errp=0xffffffcba366c0) at ../softmmu/physmem.c:2188
#1 0x00aaaaaae762d37c in memory_region_init_ram_flags_nomigrate (mr=0xaaaaaae7fb1c60, owner=0xaaaaaae7fb1c00, name=0xaaaaaae8075b50 "riscv_virt_board.ram", size=1073741824, ram_flags=0, errp=0xffffffcba36720) at ../softmmu/memory.c:1563
#2 0x00aaaaaae72464d4 in ram_backend_memory_alloc (backend=0xaaaaaae7fb1c00, errp=0xffffffcba36720) at ../backends/hostmem-ram.c:33
#3 0x00aaaaaae724751c in host_memory_backend_memory_complete (uc=0xaaaaaae7fb1c00, errp=0xffffffcba36778) at ../backends/hostmem.c:336
#4 0x00aaaaaae77c46f8 in user_creatable_complete (uc=0xaaaaaae7fb1c00, errp=0xaaaaaae7e44170 <error_fatal>) at ../qom/object_interfaces.c:27
#5 0x00aaaaaae723f074 in create_default_memdev (ms=0xaaaaaae8062b00, path=0x0) at ../softmmu/vl.c:2452
#6 0x00aaaaaae723f904 in qemu_init_board () at ../softmmu/vl.c:2649
#7 0x00aaaaaae723fc28 in qmp_x_exit_preconfig (errp=0xaaaaaae7e44170 <error_fatal>) at ../softmmu/vl.c:2746
#8 0x00aaaaaae7242534 in qemu_init (argc=19, argv=0xffffffcba36b48, envp=0xffffffcba36be8) at ../softmmu/vl.c:3776
#9 0x00aaaaaae71f3bb4 in main (argc=19, argv=0xffffffcba36b48, envp=0xffffffcba36be8) at ../softmmu/main.c:49

内存布局的提交

为了让二级地址翻译正常工作, 还需要将虚拟机的内存布局通知到kvm, 并且每次变化都需要通知kvm 进行修改.
这个过程是通过MemoryListener来实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct MemoryListener {
void (*begin)(MemoryListener *listener);
void (*commit)(MemoryListener *listener); //用来执行内存变更所需的函数
void (*region_add)(MemoryListener *listener, MemoryRegionSection *section); //在添加region的时候被调用
void (*region_del)(MemoryListener *listener, MemoryRegionSection *section);
... // log_xxx函数跟脏页机制的开启和同步有关系
void (*eventfd_add)(MemoryListener *listener, MemoryRegionSection *section,
bool match_data, uint64_t data, EventNotifier *e);

void (*eventfd_del)(MemoryListener *listener, MemoryRegionSection *section,
bool match_data, uint64_t data, EventNotifier *e);

unsigned priority; //priority用来表示优先级,优先级低的在添加时会优先被调用,在删除的时候则会最后被调用
const char *name;
AddressSpace *address_space; //表示监听器对应的地址空间
QTAILQ_ENTRY(MemoryListener) link; //用来将各个MemoryListener连接起来,它们连接到一个全局变量memory_listeners上
QTAILQ_ENTRY(MemoryListener) link_as; //同一个地址空间的MemoryListener通过link_as连接起来
};
typedef struct KVMMemoryListener {
MemoryListener listener;
KVMSlot *slots;
int as_id;
} KVMMemoryListener;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void kvm_memory_listener_register(KVMState *s, KVMMemoryListener *kml,
AddressSpace *as, int as_id, const char *name)
{
kml->slots = g_new0(KVMSlot, s->nr_slots); // 分配s->nr_slots个KVMSlot结构, 该结构表示的是KVM内存槽,也就是对于KVM来说,虚拟机有多少段内存,该值是在kvm_init中通过KVM_CAP_NR_MEMSLOTS这个KVM设备所属的ioctl得到的
kml->as_id = as_id;

for (i = 0; i < s->nr_slots; i++) {
kml->slots[i].slot = i;
}
// 初始化kml的listener回调函数
kml->listener.region_add = kvm_region_add;
kml->listener.region_del = kvm_region_del;
kml->listener.log_start = kvm_log_start;
kml->listener.log_stop = kvm_log_stop;
kml->listener.priority = 10;
kml->listener.name = name;
// ... 省略dirty相关
memory_listener_register(&kml->listener, as);
...
}

进行内存更新时有很多节点,比如

  • 新创建一个AddressSpace时
  • 调用memory_region_add_subregion将一个MemoryRegion添加到另一个MemoryRegion的subregions中时
  • 调用memory_region_set_readonly更改一段内存的属性
  • 利用memory_region_set_enabled将一个MemoryRegion设置为使能或者非使能时
    总之一句话,当修改了虚拟机的内存布局或者属性时,就需要通知到各个listener,这个过程叫作commit,通过函数memory_region_transaction_commit实现

前面创建了system_memory, 在virt_machine_init中, 最终将actual RAM 加到了 system_memory下

1
memory_region_add_subregion(system_memory, memmap[VIRT_DRAM].base, machine->ram);
1
2
3
4
5
6
7
8
9
10
#0  memory_region_transaction_commit () at ../softmmu/memory.c:1092
#1 0x00aaaaaae7630648 in memory_region_update_container_subregions (subregion=0xaaaaaae7fb1c60) at ../softmmu/memory.c:2541
#2 0x00aaaaaae7630708 in memory_region_add_subregion_common (mr=0xaaaaaae805fae0, offset=2147483648, subregion=0xaaaaaae7fb1c60) at ../softmmu/memory.c:2556
#3 0x00aaaaaae7630750 in memory_region_add_subregion (mr=0xaaaaaae805fae0, offset=2147483648, subregion=0xaaaaaae7fb1c60) at ../softmmu/memory.c:2564
#4 0x00aaaaaae752cacc in virt_machine_init (machine=0xaaaaaae8062b00) at ../hw/riscv/virt.c:1296
#5 0x00aaaaaae72ee4d8 in machine_run_board_init (machine=0xaaaaaae8062b00) at ../hw/core/machine.c:1189
#6 0x00aaaaaae723f930 in qemu_init_board () at ../softmmu/vl.c:2656
#7 0x00aaaaaae723fc28 in qmp_x_exit_preconfig (errp=0xaaaaaae7e44170 <error_fatal>) at ../softmmu/vl.c:2746
#8 0x00aaaaaae7242534 in qemu_init (argc=19, argv=0xffffffcba36b48, envp=0xffffffcba36be8) at ../softmmu/vl.c:3776
#9 0x00aaaaaae71f3bb4 in main (argc=19, argv=0xffffffcba36b48, envp=0xffffffcba36be8) at ../softmmu/main.c:49

简单回顾下:

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
-+ qemu_init_board
\ - MachineClass *machine_class = MACHINE_GET_CLASS(current_machine);
| -+ create_default_memdev(current_machine(ms), mem_path); "mem_path为空"
\ - obj = object_new(TYPE_MEMORY_BACKEND_RAM);
"新创建了一个类为"TYPE_MEMORY_BACKEND_RAM"的object, 其父类是"TYPE_MEMORY_BACKEND""
| - object_property_set_int(obj, "size", ms->ram_size, &error_fatal); "添加size到obj的属性中, ram_size 为启动qemu 命令行中带的内存大小"
| - object_property_add_child(object_get_objects_root(), mc->default_ram_id, obj);
"将obj以 riscv_virt_board.ram 的名字 添加到 container/objects/ 下"
| -+ user_creatable_complete(USER_CREATABLE(obj), &error_fatal);
"创建MemoryRegion, mmap ram_size 大小的内存, 请看前面的调用栈"
\ -+ host_memory_backend_memory_complete(obj)
\ -+ memory_region_class ->alloc(obj)
\ -+ ram_backend_memory_alloc(obj)
\ -+ memory_region_init_ram_flags_nomigrate(mr, obj)
\ - memory_region_init(mr, owner, name, size); "obj为owner , 是这块mr (memory_region的所有者)"
| - mr->ram_block = qemu_ram_alloc(size, ram_flags, mr, &err); "为mr 初始化ramblock"
| - object_property_set_str(OBJECT(ms), "memory-backend", mc->default_ram_id, &error_fatal);
"为 current_machine 添加 "memory-backend" 的 string, 为 riscv_virt_board.ram"
-+ machine_run_board_init(machine)
\ - MachineClass *machine_class = MACHINE_GET_CLASS(machine);
| - o = object_resolve_path_type(machine->ram_memdev_id, TYPE_MEMORY_BACKEND, NULL);
"从root下找到 container下找到类为 "TYPE_MEMORY_BACKEND" 的 object"
| -+ machine->ram = machine_consume_memdev(machine, MEMORY_BACKEND(o));
\ -+ MemoryRegion *ret = host_memory_backend_get_memory(backend);
\ - return backend->mr "返回MemoryRegion, 这块memory_region 指向qemu_init_board 建的 初始内存"
| -+ machine_class->init(machine);
\ -+ virt_machine_init(machine(MachineState))
\ -+ memory_region_add_subregion(system_memory, memmap[VIRT_DRAM].base, machine->ram);
"GPA为0x80000000 开头, machine->ram 为初始mmap 内存, 映射了命令行提供的 ram_size 大小, system_memory为root的内存节点"
\ ---+ memory_region_transaction_commit()
\ -+ address_space_set_flatview(AddressSpace)
\ ---+ kvm_set_phys_mem()
\ -+ kvm_set_user_memory_region(KVMSlot *slot)
\ - mem.slot = slot->slot | (kml->as_id << 16);
| - mem.guest_phys_addr = slot->start_addr;
| - mem.userspace_addr = (unsigned long)slot->ram;
| - mem.memory_size = slot->memory_size;
| - kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem);

整个设置初始内存布局涉及的点非常碎, 最后将其加到了system_memory 即 root MemoryRegion的 subregions中
最后我们重点看下 memory_region_transaction_commit 这个函数

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
void memory_region_transaction_commit(void)
{
AddressSpace *as;
--memory_region_transaction_depth;
if (!memory_region_transaction_depth) {
// 对于内存更新来说,memory_region_update_pending会被设置为true
if (memory_region_update_pending) {
flatviews_reset();
// 通过宏MEMORY_LISTENER_CALL_GLOBAL调用每个memory listener的begin函数,
// 这个时候各个memory listener可以在begin函数中做一些初始化的工作
MEMORY_LISTENER_CALL_GLOBAL(begin, Forward);

QTAILQ_FOREACH(as, &address_spaces, address_spaces_link) {
// 接着对每个address_spaces链表上的每个AddressSpace调用 address_space_set_flatview 函数
// 这个函数用来更新AddressSpace的内存视图,这个过程中可能会涉及memory listener的添加删除等回调函数
address_space_set_flatview(as);
address_space_update_ioeventfds(as);
}
memory_region_update_pending = false;
ioeventfd_update_pending = false;
// 最后通过宏MEMORY_LISTENER_CALL_GLOBAL对全局链表memory_listeners上的每一个注册的MemoryListener调用commit回调函数
MEMORY_LISTENER_CALL_GLOBAL(commit, Forward);
}
}
}

上述过程中比较重要的调用为 address_space_set_flatview 内存平坦化函数
再来看一个栈, 可见最终这个函数会调用到设置kvm KVM_SET_USER_MEMORY 跟kernel kvm 内核模块交互设置最终的memory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#0  kvm_set_user_memory_region (kml=0xaaaaaae80883b0, slot=0xffffff91ec3010, new=true) at ../accel/kvm/kvm-all.c:359
#1 0x00aaaaaae77a1dec in kvm_set_phys_mem (kml=0xaaaaaae80883b0, section=0xffffffcba364d0, add=true) at ../accel/kvm/kvm-all.c:1436
#2 0x00aaaaaae77a2074 in kvm_region_add (listener=0xaaaaaae80883b0, section=0xffffffcba364d0) at ../accel/kvm/kvm-all.c:1503
#3 0x00aaaaaae762b7f8 in address_space_update_topology_pass (as=0xaaaaaae7e26c08 <address_space_memory>, old_view=0xaaaaaae80b8420, new_view=0xaaaaaae80d10d0, adding=true) at ../softmmu/memory.c:975
#4 0x00aaaaaae762bb64 in address_space_set_flatview (as=0xaaaaaae7e26c08 <address_space_memory>) at ../softmmu/memory.c:1051
#5 0x00aaaaaae762bdb0 in memory_region_transaction_commit () at ../softmmu/memory.c:1103
#6 0x00aaaaaae7630648 in memory_region_update_container_subregions (subregion=0xaaaaaae7fb1c60) at ../softmmu/memory.c:2541
#7 0x00aaaaaae7630708 in memory_region_add_subregion_common (mr=0xaaaaaae805fae0, offset=2147483648, subregion=0xaaaaaae7fb1c60) at ../softmmu/memory.c:2556
#8 0x00aaaaaae7630750 in memory_region_add_subregion (mr=0xaaaaaae805fae0, offset=2147483648, subregion=0xaaaaaae7fb1c60) at ../softmmu/memory.c:2564
#9 0x00aaaaaae752cacc in virt_machine_init (machine=0xaaaaaae8062b00) at ../hw/riscv/virt.c:1296
#10 0x00aaaaaae72ee4d8 in machine_run_board_init (machine=0xaaaaaae8062b00) at ../hw/core/machine.c:1189
#11 0x00aaaaaae723f930 in qemu_init_board () at ../softmmu/vl.c:2656
#12 0x00aaaaaae723fc28 in qmp_x_exit_preconfig (errp=0xaaaaaae7e44170 <error_fatal>) at ../softmmu/vl.c:2746
#13 0x00aaaaaae7242534 in qemu_init (argc=19, argv=0xffffffcba36b48, envp=0xffffffcba36be8) at ../softmmu/vl.c:3776
#14 0x00aaaaaae71f3bb4 in main (argc=19, argv=0xffffffcba36b48, envp=0xffffffcba36be8) at ../softmmu/main.c:49

虚拟机内存平坦化过程

KVM的ioctl(KVM_SET_USER_MEMORY_REGION)接口用来设置QEMU虚拟地址与虚拟机物理地址的对应关系,这是“平坦”的,但是QEMU是以AddressSpace根MemoryRegion为起始的树状结构表示虚拟机的内存。虚拟机内存的平坦化过程指的是将AddressSpace root MemoryRegion表示的虚拟机内存地址空间转变成一个平坦的线性地址空间。每一段线性空间的属性和其所属的MemoryRegion都一致,每一段线性空间与虚拟机的物理地址空间都相互关联。
虚拟机内存的平坦化以AddressSpace为单位,也就是以AddressSpace的根MemoryRegion为起点,将其表示内存拓扑的无环图结构变成平坦模式。
AddressSpace结构体中有一个类型为FlatView的current_map成员用来表示该AddressSpace对应的平坦视角,MemoryRegion展开之后的内存拓扑由FlatRange表示。每一个FlatRange表示一段AddressSapce中的一段空间,FlatView的成员中nr表示FlatRange的个数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
struct FlatView {
struct rcu_head rcu;
unsigned ref;
FlatRange *ranges;
unsigned nr; // nr表示FlatRange的个数
unsigned nr_allocated; //nr_allocated表示已经分配的FlatRange个数
struct AddressSpaceDispatch *dispatch;
MemoryRegion *root;
};
// 每一个FlatRange表示一段AddressSapce中的一段空间
struct FlatRange {
MemoryRegion *mr; // mr表示对应的MemoryRegion
hwaddr offset_in_region; // offset_in_region表示该FlatRange在MemoryRegion的偏移
AddrRange addr; //地址
uint8_t dirty_log_mask;
bool romd_mode;
bool readonly;
bool nonvolatile;
};


MemoryRegion平坦化之后
MemoryRegion平坦化之后

内存的分派

内存分派表的构建

QEMU内存的分派指的是,当给定一个AddressSpace和一个地址时,要能够快速地找出其所在的MemoryRegionSection,从而找到对应的MemoryRegion.
与内存分派相关的数据结构是AddressSpaceDispatch,AddressSpace结构体中的dispatch成员为AddressSpaceDispatch,记录了该AddressSpace中的分派信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
struct AddressSpaceDispatch {
MemoryRegionSection *mru_section; //缓存,保存最近一次找到的MemoryRegonSection
PhysPageEntry phys_map; // 指向第一级页表, 类似于satp
PhysPageMap map; // "页表" 就存放在AddressSpaceDispatch的map成员中
};
typedef struct PhysPageMap {
unsigned sections_nb; // 表示sections所指向的动态数组中的有效个数
unsigned sections_nb_alloc; // 表示Nodes总共分配的个数
unsigned nodes_nb; //
unsigned nodes_nb_alloc;
Node *nodes; // 中间节点,类似于"页表项"
MemoryRegionSection *sections; //所有MemoryRegionSection的数组,类似于寻址过程中的物理页面
} PhysPageMap;

typedef PhysPageEntry Node[P_L2_SIZE]; //512个页表项

struct PhysPageEntry {
/* How many bits skip to next level (in units of L2_SIZE). 0 for a leaf. */
uint32_t skip : 6;
/* index into phys_sections (!skip) or phys_map_nodes (skip) */
uint32_t ptr : 26; //在非叶子节点的情况会索引nodes中的项,然后要查找的地址本身的一些位会在一个Node中作为索引,找到隶属的PhysPageEntry,类似于MMU的寻址过程,最后一个PhysPageEntry的ptr存放着一个值用来索引sections数组,这样最终得到MemoryRegionSection
};

看下调用栈, 简单分析下哪些函数会调用到 generate_memory_topology , 在这个函数中会新建flatview, 并对flatview 创建 address_space_dispatch
dispatch 对象存到 flatview的 dispatch 成员中.

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
-+ FlatView *generate_memory_topology(MemoryRegion *mr)
\ -+ FlatView *view = flatview_new(mr);
| -+ view->dispatch = address_space_dispatch_new(view);
| -+ for i in view->nr "遍历view->ranges"
\ - MemoryRegionSection mrs = section_from_flat_range(&view->ranges[i], view);
"每个FlatRange 对应一个 MemoryRegionSection, 根据FlatRange 构造MemorySection对象"
| -+ flatview_add_to_dispatch(view, &mrs); "讲MemoryRegionSection对象添加到FlatView对象中"
\ -|+ if 地址没有page对齐
\ - register_subpage(fv, &now);
| -|+ else if 地址page 对齐
\ -+ register_multipage(fv, &now);
\ - AddressSpaceDispatch *d = flatview_to_dispatch(fv); "fv->dispatch"
| - hwaddr start_addr = section->offset_within_address_space;
"start_addr 表示 MemoryRegionSection对象在AddressSpace中的起始地址"
| - section_index = phys_section_add(&d->map, section);
"dispatch->map->sections中增加当前section,返回在其中的索引"
| - num_pages = int128_get64(int128_rshift(section->size, TARGET_PAGE_BITS));
"该MemoryRegionSection对象的页数"
| -+ phys_page_set(d, start_addr >> TARGET_PAGE_BITS, num_pages, section_index);
\ - phys_map_node_reserve(&d->map, 3 * P_L2_LEVELS);
| -+ phys_page_set_level(&d->map, &d->phys_map, &index, &nb, leaf, P_L2_LEVELS - 1);
\ - phys_page_set_level(map, lp, index, nb, leaf, level - 1);
"类似于页表的创建, 递归调用phys_page_set_level"
| -+ address_space_dispatch_compact(view->dispatch); "页表简化"
"PhysPageEntry中有一个skip域,表示需要前进的页表数目,从之前的页表构造过程可以看到,非终结节点的skip都是1,表示去查看下一级页表。
如果pt5中只有一项,那么pt1到pt5也只有一项,所以可以直接pt1---->index。这样,skip就可以设置为5,表示越过4级页表"


最后一级页表的index是PhysPageMap中sections数组中的索引,虚拟机这一层物理地址到MemoryRegionSection的“页表”就建立完成了

看下下面两个调用栈, 能调用到generate_memory_topology 有两条路径:

  1. address_space_init 为root memory_region 初始化
  2. memory_region_add_subregion 添加新的memory_region 到 root memory_region的subregion 中时
1
2
3
4
5
6
7
8
9
10
#0  flatview_new (mr_root=0x0) at ../softmmu/memory.c:259
#1 0x00aaaaaae046b5d8 in generate_memory_topology (mr=0x0) at ../softmmu/memory.c:733
#2 0x00aaaaaae046c8f4 in flatviews_init () at ../softmmu/memory.c:995
#3 0x00aaaaaae046cc00 in address_space_update_topology (as=0xaaaaaae0c67c08 <address_space_memory>) at ../softmmu/memory.c:1075
#4 0x00aaaaaae0473230 in address_space_init (as=0xaaaaaae0c67c08 <address_space_memory>, root=0xaaaaaae0ea0ae0, name=0xaaaaaae0961768 "memory") at ../softmmu/memory.c:3012
#5 0x00aaaaaae047e528 in memory_map_init () at ../softmmu/physmem.c:2669
#6 0x00aaaaaae047f8bc in cpu_exec_init_all () at ../softmmu/physmem.c:3127
#7 0x00aaaaaae007f13c in qemu_create_machine (qdict=0xaaaaaae0ce18d0) at ../softmmu/vl.c:2167
#8 0x00aaaaaae0083380 in qemu_init (argc=19, argv=0xffffffdbd02b48, envp=0xffffffdbd02be8) at ../softmmu/vl.c:3708
#9 0x00aaaaaae0034bb4 in main (argc=19, argv=0xffffffdbd02b48, envp=0xffffffdbd02be8) at ../softmmu/main.c:49
1
2
3
4
5
6
7
8
9
10
11
12
13
#0  flatview_new (mr_root=0xaaaaaae0ea0ae0) at ../softmmu/memory.c:259
#1 0x00aaaaaae046b5d8 in generate_memory_topology (mr=0xaaaaaae0ea0ae0) at ../softmmu/memory.c:733
#2 0x00aaaaaae046ca10 in flatviews_reset () at ../softmmu/memory.c:1022
#3 0x00aaaaaae046cd4c in memory_region_transaction_commit () at ../softmmu/memory.c:1098
#4 0x00aaaaaae0471648 in memory_region_update_container_subregions (subregion=0xaaaaaae0df2c60) at ../softmmu/memory.c:2541
#5 0x00aaaaaae0471708 in memory_region_add_subregion_common (mr=0xaaaaaae0ea0ae0, offset=2147483648, subregion=0xaaaaaae0df2c60) at ../softmmu/memory.c:2556
#6 0x00aaaaaae0471750 in memory_region_add_subregion (mr=0xaaaaaae0ea0ae0, offset=2147483648, subregion=0xaaaaaae0df2c60) at ../softmmu/memory.c:2564
#7 0x00aaaaaae036dacc in virt_machine_init (machine=0xaaaaaae0ea3b00) at ../hw/riscv/virt.c:1296
#8 0x00aaaaaae012f4d8 in machine_run_board_init (machine=0xaaaaaae0ea3b00) at ../hw/core/machine.c:1189
#9 0x00aaaaaae0080930 in qemu_init_board () at ../softmmu/vl.c:2656
#10 0x00aaaaaae0080c28 in qmp_x_exit_preconfig (errp=0xaaaaaae0c85170 <error_fatal>) at ../softmmu/vl.c:2746
#11 0x00aaaaaae0083534 in qemu_init (argc=19, argv=0xffffffdbd02b48, envp=0xffffffdbd02be8) at ../softmmu/vl.c:3776
#12 0x00aaaaaae0034bb4 in main (argc=19, argv=0xffffffdbd02b48, envp=0xffffffdbd02be8) at ../softmmu/main.c:49

回过头来我们再看下MMIO 陷入过程

MMIO qemu 处理

前面讲kvm时, 最后落到了qemu 用户态处理 MMIO

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
\ -|+ case KVM_EXIT_MMIO
\ -+ address_space_rw(&address_space_memory,
run->mmio.phys_addr, attrs, "前面已经讲过 这里的run 跟 kernel kvm 中vcpu->run 共享物理页"
run->mmio.data, run->mmio.len, run->mmio.is_write);
\ -+ address_space_read_full(as, addr, attrs, buf, len); "is_write 为false, 此处为读"
\ - fv = address_space_to_flatview(as); "address_space_memory 为root as, 将其平坦化"
| -+ result = flatview_read(fv, addr, attrs, buf, len);
\ -+ mr = flatview_translate(fv, addr, &addr1, &l, false, attrs); "从GPA 找到对应的MemoryRegion"
\ -+ section = address_space_translate_internal(flatview_to_dispatch(fv), addr, xlat, plen_out, is_mmio);
\ - section = address_space_lookup_region(d, addr, resolve_subpage);
"查找addr在d这个AddressSpaceDispatch中对应的MemoryRegionSection"
"首先读取缓存d->mru_section,如果不存在一个dummy section或者是addr不在section内,则调用phys_page_find找到section。
可以看到phys_page_find函数for循环从一级页表开始,首先使用d->phys_map的ptr作为索引寻址node,
然后使用虚拟机的页帧号来找到在Node的页表项,因为存在页表可能跳过的情况,所以i需要减掉忽略的页表。
当到最后一级或者skip为0时,PhysPageEntry的ptr是实际的MemoryRegionSection在sections中的索引"
| - mr = section.mr;
\ -+ flatview_read_continue(fv, addr, attrs, buf, len, addr1, l, mr);
\ - prepare_mmio_access(mr)
| - l = memory_access_size(mr, l, addr1);
| -+ result |= memory_region_dispatch_read(mr, addr1, &val, size_memop(l), attrs);
\ ---+ memory_region_dispatch_read1(mr, addr, pval, size, attrs);
\ -|+ if mr->ops->read
\ -+ access_with_adjusted_size(addr, pval, size, mr->ops->impl.min_access_size, mr->ops->impl.max_access_size,
memory_region_read_accessor,
mr, attrs);
\ - tmp = mr->ops->read(mr->opaque, addr, size);
| - memory_region_shift_read_access(value, shift, mask, tmp);
| -|+ else if !(mr->ops->read)
\ -+ access_with_adjusted_size(addr, pval, size,
mr->ops->impl.min_access_size,
mr->ops->impl.max_access_size,
memory_region_read_with_attrs_accessor,
mr, attrs);
\ - r = mr->ops->read_with_attrs(mr->opaque, addr, &tmp, size, attrs);
| - memory_region_shift_read_access(value, shift, mask, tmp);

调用过程仍然非常复杂, 需要从root 的address_space_memory 转换为flatview, 从flatview的dispatch 分派表进行页表遍历, 最终找到GPA所在的MemoryRegionSection, 最后定位到其MemoryRegion.
最后最重要的读函数是由 MemoryRegion 定义的回调.

这里我们举一个案例, 以plic mmio 为例, 看一下mmio 整体的结构

PLIC mmio 初始化

首先qemu 命令行 info mtree 看下内存布局

1
2
3
4
address-space: memory
000000000c000000-000000000c5fffff (prio 0, i/o): riscv.sifive.plic
memory-region: system
000000000c000000-000000000c5fffff (prio 0, i/o): riscv.sifive.plic

其是挂在address_space_memory下的, 而不是io 空间下.

接着看下 sifive plic mmio 注册的过程

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
static const MemoryRegionOps sifive_plic_ops = {
.read = sifive_plic_read,
.write = sifive_plic_write,
.endianness = DEVICE_LITTLE_ENDIAN,
.valid = {
.min_access_size = 4,
.max_access_size = 4
}
};

-+ static void virt_machine_init(MachineState *machine)
\ - RISCVVirtState *s = RISCV_VIRT_MACHINE(machine);
| -+ s->irqchip[i] = virt_create_plic(memmap, i, base_hartid, hart_count);
"memmap [VIRT_PLIC] =        {  0xc000000, VIRT_PLIC_SIZE(VIRT_CPUS_MAX * 2) },"
\ -+ sifive_plic_create(memmap[VIRT_PLIC].base + socket * memmap[VIRT_PLIC].size,
plic_hart_config, hart_count, base_hartid,...)
\ -+ DeviceState *dev = qdev_new(TYPE_SIFIVE_PLIC); "创建 riscv.sifive.plic 设备"
"父类继承关系: TYPE_SIFIVE_PLIC->TYPE_SYS_BUS_DEVICE->TYPE_DEVICE->TYPE_OBJECT"
\ -+ sifive_plic_class_init
\ - DeviceClass *dc = DEVICE_CLASS(klass);
| -+ dc->realize = sifive_plic_realize;
\ - SiFivePLICState *s = SIFIVE_PLIC(dev);
| -+ memory_region_init_io(&s->mmio, OBJECT(dev), &sifive_plic_ops, s, TYPE_SIFIVE_PLIC, s->aperture_size);
"初始化MemoryRegion, 注册mr->ops 为 sifive_plic_ops"
\ - object_initialize(mr, sizeof(*mr), TYPE_MEMORY_REGION);
| -+ memory_region_do_init(mr, owner, name, size);
\ - mr->size = int128_make64(size);
| - mr->name = g_strdup(name);
| - mr->owner = owner; "owner 为 SiFivePLICState"
| - mr->ram_block = NULL; "ramblock 为 NULL"
| - sysbus_init_mmio(SYS_BUS_DEVICE(dev), &s->mmio);
| -+ sysbus_mmio_map(SYS_BUS_DEVICE(dev), 0, addr);
\ -+ sysbus_mmio_map_common(dev, 0, addr, false, 0);
\ - memory_region_add_subregion(get_system_memory(), addr, dev->mmio[0].memory);
"最终加到 system_memory root 下, "

过程同内存一样, 同样需要建MemoryRegion, 讲memory 挂到 address_space_memory root 地址下, 物理地址GPA 为 0xc000000, 注册的mr->ops 为 sifive_plic_ops, 注册了读写的函数, 最终将其平坦化
与内存不同的是:

  • 不需要mmap, MemoryRegion初始化时, 其ramblock 是空的.
  • 不需要向kvm注册, 因为需要陷入qemu的mmio 处理, 没有二级页表映射
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void kvm_set_phys_mem(KVMMemoryListener *kml,
MemoryRegionSection *section, bool add)
{
if (!memory_region_is_ram(mr)) { //不是ram的情况下
if (writeable || !kvm_readonly_mem_allowed) {
return; // 可写的, 直接退出
} else if (!mr->romd_mode) {
add = false;
}
}
if (!add) {
goto out;
}
err = kvm_set_user_memory_region(kml, mem, true);
}

可以通过qemu的命令行查看所有的AddressSpace信息和MemoryRegion信息, 关于AddressSpace 与 MemoryRegion的关系, 请看 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
info mtree
address-space: memory
0000000000000000-ffffffffffffffff (prio 0, i/o): system
0000000000001000-000000000000ffff (prio 0, rom): riscv_virt_board.mrom
0000000000100000-0000000000100fff (prio 0, i/o): riscv.sifive.test
0000000000101000-0000000000101023 (prio 0, i/o): goldfish_rtc
0000000002000000-0000000002003fff (prio 0, i/o): riscv.aclint.swi
0000000002004000-000000000200bfff (prio 0, i/o): riscv.aclint.mtimer
0000000003000000-000000000300ffff (prio 0, i/o): gpex_ioport_window
0000000003000000-000000000300ffff (prio 0, i/o): gpex_ioport
0000000003000000-000000000300001f (prio 1, i/o): virtio-pci
000000000c000000-000000000c5fffff (prio 0, i/o): riscv.sifive.plic
0000000010000000-0000000010000007 (prio 0, i/o): serial
0000000010001000-00000000100011ff (prio 0, i/o): virtio-mmio
0000000010002000-00000000100021ff (prio 0, i/o): virtio-mmio
0000000010003000-00000000100031ff (prio 0, i/o): virtio-mmio
0000000010004000-00000000100041ff (prio 0, i/o): virtio-mmio
0000000010005000-00000000100051ff (prio 0, i/o): virtio-mmio
0000000010006000-00000000100061ff (prio 0, i/o): virtio-mmio
0000000010007000-00000000100071ff (prio 0, i/o): virtio-mmio
0000000010008000-00000000100081ff (prio 0, i/o): virtio-mmio
0000000010100000-0000000010100007 (prio 0, i/o): fwcfg.data
0000000010100008-0000000010100009 (prio 0, i/o): fwcfg.ctl
0000000010100010-0000000010100017 (prio 0, i/o): fwcfg.dma
0000000020000000-0000000021ffffff (prio 0, romd): virt.flash0
0000000022000000-0000000023ffffff (prio 0, romd): virt.flash1
0000000030000000-000000003fffffff (prio 0, i/o): alias pcie-ecam @pcie-mmcfg-mmio 0000000000000000-000000000fffffff
0000000040000000-000000007fffffff (prio 0, i/o): alias pcie-mmio @gpex_mmio_window 0000000040000000-000000007fffffff
0000000080000000-00000000ffffffff (prio 0, ram): riscv_virt_board.ram
0000000400000000-00000007ffffffff (prio 0, i/o): alias pcie-mmio-high @gpex_mmio_window 0000000400000000-00000007ffffffff
address-space: virtio-rng-pci
0000000000000000-ffffffffffffffff (prio 0, i/o): bus master container
0000000000000000-ffffffffffffffff (prio 0, i/o): alias bus master @system 0000000000000000-ffffffffffffffff

address-space: gpex-root
0000000000000000-ffffffffffffffff (prio 0, i/o): bus master container

memory-region: system
0000000000000000-ffffffffffffffff (prio 0, i/o): system
0000000000001000-000000000000ffff (prio 0, rom): riscv_virt_board.mrom
0000000000100000-0000000000100fff (prio 0, i/o): riscv.sifive.test
0000000000101000-0000000000101023 (prio 0, i/o): goldfish_rtc
0000000002000000-0000000002003fff (prio 0, i/o): riscv.aclint.swi
0000000002004000-000000000200bfff (prio 0, i/o): riscv.aclint.mtimer
0000000003000000-000000000300ffff (prio 0, i/o): gpex_ioport_window
0000000003000000-000000000300ffff (prio 0, i/o): gpex_ioport
0000000003000000-000000000300001f (prio 1, i/o): virtio-pci
000000000c000000-000000000c5fffff (prio 0, i/o): riscv.sifive.plic
0000000010000000-0000000010000007 (prio 0, i/o): serial
0000000010001000-00000000100011ff (prio 0, i/o): virtio-mmio
0000000010002000-00000000100021ff (prio 0, i/o): virtio-mmio
0000000010003000-00000000100031ff (prio 0, i/o): virtio-mmio
0000000010004000-00000000100041ff (prio 0, i/o): virtio-mmio
0000000010005000-00000000100051ff (prio 0, i/o): virtio-mmio
0000000010006000-00000000100061ff (prio 0, i/o): virtio-mmio
0000000010007000-00000000100071ff (prio 0, i/o): virtio-mmio
0000000010008000-00000000100081ff (prio 0, i/o): virtio-mmio
0000000010100000-0000000010100007 (prio 0, i/o): fwcfg.data
0000000010100008-0000000010100009 (prio 0, i/o): fwcfg.ctl
0000000010100010-0000000010100017 (prio 0, i/o): fwcfg.dma
0000000020000000-0000000021ffffff (prio 0, romd): virt.flash0
0000000022000000-0000000023ffffff (prio 0, romd): virt.flash1
0000000030000000-000000003fffffff (prio 0, i/o): alias pcie-ecam @pcie-mmcfg-mmio 0000000000000000-000000000fffffff
0000000040000000-000000007fffffff (prio 0, i/o): alias pcie-mmio @gpex_mmio_window 0000000040000000-000000007fffffff
0000000080000000-00000000ffffffff (prio 0, ram): riscv_virt_board.ram
0000000400000000-00000007ffffffff (prio 0, i/o): alias pcie-mmio-high @gpex_mmio_window 0000000400000000-00000007ffffffff

memory-region: pcie-mmcfg-mmio
0000000000000000-000000001fffffff (prio 0, i/o): pcie-mmcfg-mmio

检查虚拟机的能力

在虚拟机创建完后, qemu会检查内核kvm 的能力, 必备的能力在kvm_required_capabiliteskvm_arch_required_capabilities 这两个list下
riscv arch 并没有实现 kvm_arch_required_capabilities, 所以这里只关注 kvm_required_capabilites
通过kvm_vm_ioctl(s, KVM_CHECK_EXTENSION, extension); 和kvm 交互查询

1
2
3
KVM_CAP_USER_MEMORY
KVM_CAP_DESTROY_MEMORY_REGION_WORKS
KVM_CAP_JOIN_MEMORY_REGIONS_WORKS

``
这里会进入到 kvm 内核中的代码, riscv 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
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
static long kvm_vm_ioctl_check_extension_generic(struct kvm *kvm, long arg)
{
switch (arg) {
case KVM_CAP_USER_MEMORY:
case KVM_CAP_DESTROY_MEMORY_REGION_WORKS:
case KVM_CAP_JOIN_MEMORY_REGIONS_WORKS:
case KVM_CAP_INTERNAL_ERROR_DATA:
#ifdef CONFIG_HAVE_KVM_MSI
case KVM_CAP_SIGNAL_MSI:
#endif
#ifdef CONFIG_HAVE_KVM_IRQFD
case KVM_CAP_IRQFD:
case KVM_CAP_IRQFD_RESAMPLE:
#endif
case KVM_CAP_IOEVENTFD_ANY_LENGTH:
case KVM_CAP_CHECK_EXTENSION_VM:
case KVM_CAP_ENABLE_CAP_VM:
case KVM_CAP_HALT_POLL:
return 1;
#ifdef CONFIG_KVM_MMIO
case KVM_CAP_COALESCED_MMIO:
return KVM_COALESCED_MMIO_PAGE_OFFSET;
case KVM_CAP_COALESCED_PIO:
return 1;
#endif
#ifdef CONFIG_KVM_GENERIC_DIRTYLOG_READ_PROTECT
case KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2:
return KVM_DIRTY_LOG_MANUAL_CAPS;
#endif
#ifdef CONFIG_HAVE_KVM_IRQ_ROUTING
case KVM_CAP_IRQ_ROUTING:
return KVM_MAX_IRQ_ROUTES;
#endif
#if KVM_ADDRESS_SPACE_NUM > 1
case KVM_CAP_MULTI_ADDRESS_SPACE:
return KVM_ADDRESS_SPACE_NUM;
#endif
case KVM_CAP_NR_MEMSLOTS:
return KVM_USER_MEM_SLOTS;
case KVM_CAP_DIRTY_LOG_RING:
#ifdef CONFIG_HAVE_KVM_DIRTY_RING
return KVM_DIRTY_RING_MAX_ENTRIES * sizeof(struct kvm_dirty_gfn);
#else
return 0;
#endif
case KVM_CAP_BINARY_STATS_FD:
case KVM_CAP_SYSTEM_EVENT_DATA:
return 1;
default:
break;
}
return kvm_vm_ioctl_check_extension(kvm, arg);
}
int kvm_vm_ioctl_check_extension(struct kvm *kvm, long ext)
{
int r;

switch (ext) {
case KVM_CAP_IOEVENTFD:
case KVM_CAP_DEVICE_CTRL:
case KVM_CAP_USER_MEMORY:
case KVM_CAP_SYNC_MMU:
case KVM_CAP_DESTROY_MEMORY_REGION_WORKS:
case KVM_CAP_ONE_REG:
case KVM_CAP_READONLY_MEM:
case KVM_CAP_MP_STATE:
case KVM_CAP_IMMEDIATE_EXIT:
r = 1;
break;
case KVM_CAP_NR_VCPUS:
r = min_t(unsigned int, num_online_cpus(), KVM_MAX_VCPUS);
break;
case KVM_CAP_MAX_VCPUS:
r = KVM_MAX_VCPUS;
break;
case KVM_CAP_NR_MEMSLOTS:
r = KVM_USER_MEM_SLOTS;
break;
case KVM_CAP_VM_GPA_BITS:
r = kvm_riscv_gstage_gpa_bits();
break;
default:
r = 0;
break;
}
return r;
}

qemu中还检查了下面的feature 能力, 下面给出了当前riscv kernel 的支持能力

1
2
3
4
5
6
7
8
9
10
11
12
13
14
s->coalesced_mmio = kvm_check_extension(s, KVM_CAP_COALESCED_MMIO) // 不支持
s->coalesced_pio = s->coalesced_mmio && kvm_check_extension(s, KVM_CAP_COALESCED_PIO); // 不支持
kvm_vm_check_extension(s, KVM_CAP_DIRTY_LOG_RING) // 不支持 KVM dirty ring if supported, otherwise fall back to dirty logging mode
kvm_check_extension(s, KVM_CAP_MANUAL_DIRTY_LOG_PROTECT2) // 可选
s->max_nested_state_len = kvm_check_extension(s, KVM_CAP_NESTED_STATE); //不支持
kvm_direct_msi_allowed = (kvm_check_extension(s, KVM_CAP_SIGNAL_MSI) > 0); //可选
s->intx_set_mask = kvm_check_extension(s, KVM_CAP_PCI_2_3); //不支持
kvm_check_extension(s, KVM_CAP_IRQ_INJECT_STATUS) // 不支持
kvm_readonly_mem_allowed = (kvm_check_extension(s, KVM_CAP_READONLY_MEM) > 0);
kvm_eventfds_allowed = (kvm_check_extension(s, KVM_CAP_IOEVENTFD) > 0);
kvm_irqfds_allowed = (kvm_check_extension(s, KVM_CAP_IRQFD) > 0); //可选
kvm_resamplefds_allowed = (kvm_check_extension(s, KVM_CAP_IRQFD_RESAMPLE) > 0); //可选
kvm_vm_attributes_allowed = (kvm_check_extension(s, KVM_CAP_VM_ATTRIBUTES) > 0); // 不支持
kvm_ioeventfd_any_length_allowed = (kvm_check_extension(s, KVM_CAP_IOEVENTFD_ANY_LENGTH) > 0);

QEMU 内存虚拟化

QEMU是以AddressSpace根MemoryRegion为起始的树状结构表示虚拟机的内存。
虚拟机内存的平坦化过程指的是将AddressSpace根MemoryRegion表示的虚拟机内存地址空间转变成一个平坦的线性地址空间。
虚拟机内存的平坦化以AddressSpace为单位,也就是以AddressSpace的根MemoryRegion为起点,将其表示内存拓扑的无环图结构变成平坦模式。表示虚拟机平坦内存的数据结构是FlatView,相关函数是generate_memory_topology

影子页表及硬件辅助的EPT页表

在CPU没有支持EPT(Extended Page Table)之前,是通过所谓的影子页表来实现这个功能的。在该方案中,影子页表直接实现GVA到HPA的转换,虚拟化软件(KVM)为虚拟机中的每一个进程保存一个页表,虚拟机中的进程也有自己的页表,但是这个页表是可读的,前一个页表就称为这个页表的影子页表,这样虚拟机的页表就会导致VM Exit,然后KVM会处理该请求,然后更新影子页表。

一般来讲,EPT使用的是IA-32e的分页模式,即使48位物理地址,总共分为四级页表,每级页表使用9位物理地址定位,最后12位表示在一个页(4KB)内的偏移

如果虚拟机内部发生缺页异常,则虚拟机会自己修好自己的页表,如果发生EPT异常,则产生EPT异常退出,虚拟机会退出到KVM,构建好对应的EPT页表。如果虚拟机和宿主机都是64位的操作系统,那整个寻址过程中会发生非常多的内存访问,一般情况下,不考虑大页等因素,进行一次内存访问需要24次页表内存的访问(每次GPA->HPA为5次,4个guest表查找4×5+4)。

需要对QEMU中几个与内存相关的数据结构进行介绍。首先是AddressSpace结构体,用来表示一个虚拟机或者虚拟CPU能够访问的所有物理地址。QEMU中的AddressSpace表示的是一段地址空间,整个系统可以有一个全局的地址空间,CPU可以有自己的地址空间视角,设备也可以有自己的地址空间视角

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct AddressSpace {
/* private: */
struct rcu_head rcu;
char *name;
MemoryRegion *root;

/* Accessed via RCU. */
struct FlatView *current_map;

int ioeventfd_nb;
struct MemoryRegionIoeventfd *ioeventfds;
QTAILQ_HEAD(, MemoryListener) listeners;
QTAILQ_ENTRY(AddressSpace) address_spaces_link;
};

root表示AddressSpace对应的一个根MemoryRegion;current_map表示该地址空间是一个平坦模式下的视图。
QEMU的其他子系统可以注册地址空间变更的事件,所有注册的信息都通过listeners连接起来
所有的AddressSpace通过address_spaces_link这个node连接起来,链表头是address_spaces。
在QEMU的HMP中输入info mtree,可以看到所有的AddressSpace

第一个是系统全局的AddressSpace,表示虚拟机能够访问的所有地址,I/O表示系统下I/O端口的地址空间,cpu-memory-x表示CPU视角下的地址空间,i440FX和PIIX3是设备视角的地址空间

内存管理中另一个结构是MemoryRegion,它表示的是虚拟机的一段内存区域。MemoryRegion是内存模拟中的核心结构,整个内存的模拟都是通过MemoryRegion构成的无环图完成的
图的叶子节点是实际分配给虚拟机的物理内存或者是MMIO,中间的节点则表示内存总线,内存控制器是其他MemoryRegion的别名

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
struct MemoryRegion {
Object parent_obj;
/* The following fields should fit in a cache line */
bool romd_mode;
bool ram;
bool subpage;
bool readonly; /* For RAM regions */
bool nonvolatile;
bool rom_device;
bool flush_coalesced_mmio;
uint8_t dirty_log_mask;
bool is_iommu;
RAMBlock *ram_block; //ram_block表示实际分配的物理内存
Object *owner;
const MemoryRegionOps *ops; //ops里面是一组回调函数,在对MemoryRegion进行操作时会被调用,如MMIO的读写请求
void *opaque;
MemoryRegion *container; // container表示该MemoryRegion所处的上一级MemoryRegion
int mapped_via_alias;
Int128 size;
hwaddr addr; // addr表示该MemoryRegion所在的虚拟机的物理地址
void (*destructor)(MemoryRegion *mr);
uint64_t align;
bool terminates; // terminates用来指示是否是叶子节点
bool ram_device;
bool enabled;
bool warning_printed;
uint8_t vga_logging_count;
MemoryRegion *alias;
hwaddr alias_offset;
int32_t priority; // priority用来指示MemoryRegion的优先级
QTAILQ_HEAD(, MemoryRegion) subregions; // subregions将该MemoryRegion所属的子MemoryRegion连接起来
QTAILQ_ENTRY(MemoryRegion) subregions_link; //subregions_link则用来连接同一个父MemoryRegion下的相同兄弟
QTAILQ_HEAD(, CoalescedMemoryRange) coalesced;
const char *name;
unsigned ioeventfd_nb;
MemoryRegionIoeventfd *ioeventfds;
RamDiscardManager *rdm; /* Only for RAM */
};

常见的MemoryRegion有如下几类:

  • RAM:host上一段实际分配给虚拟机作为物理内存的虚拟内存
  • MMIO:guest的一段内存,但是在宿主机上没有对应的虚拟内存,而是截获对这个区域的访问,调用对应读写函数用在设备模拟中
  • ROM:与RAM类似,只是该类型内存只有只读属性,无法写入
  • ROM device:其在读方面类似RAM,能够直接读取,而在写方面类似MMIO,写入会调用对应的写回调函数
  • container:包含若干个MemoryRegion,每一个Region在这个container的偏移都不一样。container主要用来将多个MemoryRegion合并成一个,如PCI的MemoryRegion就会包括RAM和MMIO。一般来说,container中的region不会重合,但是有的时候也有例外
  • alias:region的另一个部分,可以使一个region被分成几个不连续的部分

通常情况下,MemoryRegion并不会重叠,当解析一个地址时,只会落入一个MemoryRegion;而有时让MemoryRegion重合也比较有用。但是当MemoryRegion重合时,需要有一种机制决定到底让哪一个对虚拟机可见,这就是MemoryRegion结构体中priority的作用。

memory_region_add_subregion_overlap函数用来将一个MemoryRegion添加到一个container中去,允许其重复,当重复的时候,谁的priority大谁就能被虚拟机看见。当然,如果两个MemoryRegion并不完全重合,那优先级低的还是会有一部分能够被虚拟机看见。

例如,下图中有一个container类型的region A(包含从0到0x8000的地址空间)以及B和C两个subregion。其中,B也是一个container,地址空间从0x2000到0x6000,优先级为2,C是一个MMIO类型的,从0到0x6000,优先级是1。B包含两个subregion,分别为D和E,D的地址空间从0x2000到0x3000,E的地址空间从0x4000到0x5000。

当给定一个AddressSpace中的一个地址时,其根据如下原则查找对应的MemoryRegion。注意,所有root的子MemoryRegion会根据优先级对地址进行匹配,从优先级高的开始。
1)从AddressSapce中找到根root,找到所有的subregion,如果地址不在region中则放弃考虑region。
2)如果地址在一个叶子MemoryRegion(RAM或者MMIO)中,查找结束,返回该Region。
3)如果子MemoryRegion是一个容器,则在该容器中递归调用该算法。
4)如果MemoryRegion是一个alias,则查找从对应的实际region开始。
5)如果在一个容器或者alias region的查找中并没有找到一个匹配项,且container本身有自己的MMIO或者RAM,那么返回这个容器本身,否则根据下一个优先级查找。
6)如果没有找到匹配的MemoryRegion节点,则结束查找。

中断虚拟化

中断线与中断向量是两个容易混淆的概念。

  • 中断线是硬件的概念,如设备连接哪一条中断线,拉高或拉低中断线的电平。
  • 中断向量则是操作系统的概念,CPU在接收到中断后还会接收到中断向量号,并且会使用该中断向量号作为索引在IDT表中寻找中断处理例程
    在pin-based的中断传递机制中,设备直接向中断线发送信号,中断控制器会把中断线号转换成中断向量号发送给CPU

中断模拟

虚拟机的中断控制器是通过VMM创建的,VMM可以利用虚拟机的中断控制器向其注入中断
中断处理的两条路径:

  • 当cpu处在VMM下时,外部来的中断是直接通过宿主机的物理中断控制器处理的, QEMU-KVM必须模拟这些中断控制器设备
  • CPU在处在guest 模式时, cpu能够将中断信息告诉虚拟机,从而让虚拟机开始对应的中断处理。

kvm 在进入虚拟机前会调用 kvm_riscv_vcpu_flush_interrupts 检查pending的request, 如果vcpu->arch.irqs_pending_maskvcpu->arch.irqs_pending不为空, 会通过hvip向虚拟机注入中断 (kvm_riscv_update_hvip)

PLIC 中断模拟

中断控制器能够全部在KVM中模拟,也能够全部在QEMU中,还能够部分在QEMU中模拟、部分在KVM中模拟,通过参数kernel-irqchip=x可以控制中断芯片由谁模拟
由于在KVM中进行中断模拟的性能更高,所以通常都会在KVM中模拟, 这里只说kvm模拟的情况
通常 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 riscv 模拟的中断控制器是怎样的呢
还是回到sifive plic 中, 这里我们可以以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=0xaaaaaae4353040, irq=9, level=0) at ../target/riscv/cpu.c:695
#1 0x00aaaaaae3a81e90 in qemu_set_irq (irq=0xaaaaaae43456d0, level=0) at ../hw/core/irq.c:45
#2 0x00aaaaaae395ac3c in sifive_plic_update (plic=0xaaaaaae437ff00) at ../hw/intc/sifive_plic.c:121
#3 0x00aaaaaae395b89c in sifive_plic_irq_request (opaque=0xaaaaaae437ff00, irq=10, level=0) at ../hw/intc/sifive_plic.c:316
#4 0x00aaaaaae3a81e90 in qemu_set_irq (irq=0xaaaaaae4381b20, level=0) at ../hw/core/irq.c:45
#5 0x00aaaaaae35a154c in qemu_irq_lower (irq=0xaaaaaae4381b20) at /home/liguang/program/3rdparty/buildroot-2022.08.1/output/build/qemu-7.0.0/include/hw/irq.h:17
#6 0x00aaaaaae35a1c98 in serial_update_irq (s=0xaaaaaae46d9030) at ../hw/char/serial.c:144
#7 0x00aaaaaae35a28bc in serial_ioport_write (opaque=0xaaaaaae46d9030, addr=0, val=48, size=1) at ../hw/char/serial.c:357
#8 0x00aaaaaae35a3f34 in serial_mm_write (opaque=0xaaaaaae46d8d10, addr=0, value=48, size=1) at ../hw/char/serial.c:1009
#9 0x00aaaaaae38f1760 in memory_region_write_accessor (mr=0xaaaaaae46d91a0, addr=0, value=0xffffff47ffd008, size=1, shift=0, mask=255, attrs=...) at ../softmmu/memory.c:492
#10 0x00aaaaaae38f1aa4 in access_with_adjusted_size (addr=0, value=0xffffff47ffd008, size=1, access_size_min=1, access_size_max=8, access_fn=0xaaaaaae38f164c <memory_region_write_accessor>, mr=0xaaaaaae46d91a0, attrs=...) at ../softmmu/memory.c:554
#11 0x00aaaaaae38f51a0 in memory_region_dispatch_write (mr=0xaaaaaae46d91a0, addr=0, data=48, op=MO_8, attrs=...) at ../softmmu/memory.c:1514
#12 0x00aaaaaae3905acc in flatview_write_continue (fv=0xaaaaaae4743c50, addr=268435456, attrs=..., ptr=0xffffff8c19c028, len=1, addr1=0, l=1, mr=0xaaaaaae46d91a0) at ../softmmu/physmem.c:2814
#13 0x00aaaaaae3905c90 in flatview_write (fv=0xaaaaaae4743c50, addr=268435456, attrs=..., buf=0xffffff8c19c028, len=1) at ../softmmu/physmem.c:2856
#14 0x00aaaaaae3906190 in address_space_write (as=0xaaaaaae40eec08 <address_space_memory>, addr=268435456, attrs=..., buf=0xffffff8c19c028, len=1) at ../softmmu/physmem.c:2952
#15 0x00aaaaaae3906234 in address_space_rw (as=0xaaaaaae40eec08 <address_space_memory>, addr=268435456, attrs=..., buf=0xffffff8c19c028, len=1, is_write=true) at ../softmmu/physmem.c:2962
#16 0x00aaaaaae3a6e100 in kvm_cpu_exec (cpu=0xaaaaaae4353040) at ../accel/kvm/kvm-all.c:2929
#17 0x00aaaaaae3a6fe6c in kvm_vcpu_thread_fn (arg=0xaaaaaae4353040) at ../accel/kvm/kvm-accel-ops.c:49
#18 0x00aaaaaae3c98b4c in qemu_thread_start (args=0xaaaaaae4373a10) at ../util/qemu-thread-posix.c:556
#19 0x00ffffff8ce36e1c in start_thread () from /home/liguang/program/3rdparty/buildroot-2022.08.1/output/host/riscv64-buildroot-linux-gnu/sysroot/lib64/libc.so.6
#20 0x00ffffff8ce90c58 in __thread_start () from /home/liguang/program/3rdparty/buildroot-2022.08.1/output/host/riscv64-buildroot-linux-gnu/sysroot/lib64/libc.so.6

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

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
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."

从上面的过程分析调用链
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 进而处理中断

中断handler 路由: sifive_plic_irq_request->riscv_cpu_set_irq

GPIO_IN通过qdev_init_gpio_in初始化
void qdev_init_gpio_in(DeviceState *dev, qemu_irq_handler handler, int n)

GPIO_OUT初始化函数为sysbus_init_irq
`void sysbus_init_irq(SysBusDevice *dev, qemu_irq *p)

qemu使用sysbus_connect_irqGPIO_OUTGPIO_IN关联
void sysbus_connect_irq(SysBusDevice *dev, int n, qemu_irq irq)
把dev中的第n个gpio_out和irq关联, 实际就是把irq保存为第n个gpio_out的值

这里涉及到的一个重点函数对:
object_property_add_link(Object *obj, const char *name, const char *type, Object **targetp) object_property_set_link(Object *obj, const char *name, Object *value)add_link 的时候填了一个*targetp(targetp为指针), set_link会把存targetp的地址的内容替换为新的value 这个地方比较绕, 可以看一下object_set_link_property的实现可以大概清楚 最重要的是这一句 *targetp = new_target;`

设备虚拟化

设备创建

设备的创建有两种方式:

  • 第一种是跟随主板初始化一起创建,典型的设备包括南北桥、一些传统的ISA设备、默认的显卡设备等,这类设备是通过qdev_create函数创建的;
  • 第二类是在命令行通过-device或者在QEMU monitor中通过device_add添加的,这类设备通过qdev_device_add函数创建。

两种创建方式本质上都调用了object_new创建对应设备的QOM对象,然后进行具现化。首先来分析第一种方式。

原生默认设备的创建是通过函数qdev_create完成
所有使用qdev_create创建设备并且Bus参数指定为NULL的设备,都会挂到系统总线上,所有挂到该总线的设备类型都是TYPE_SYS_BUS_DEVICE或者是TYPE_SYS_BUS_DEVICE的子类型

设备的树形结构

QEMU以树形结构模拟了虚拟机系统的设备和总线,树的起点是系统总线。系统总线是通过sysbus_get_default函数创建的
QEMU设备模拟中,以系统总线为起点,根据总线->设备->…总线->设备的关系,形成了一个设备树。通过info qtree可以看到这个结构

1
2
3
4
5
6
7
-+ qemu_create_machine
\ -+ object_property_add_child(container_get(OBJECT(current_machine), "/unattached"),
"sysbus", OBJECT(sysbus_get_default()));
\ -+ sysbus_get_default()
\ -+ main_system_bus_create(); "创建main总线, 为总线设置父节点,将sysbus添加到/machine/unattached 规范路径下"
\ -+ qbus_init(main_system_bus, system_bus_info.instance_size, TYPE_SYSTEM_BUS, NULL, "main-system-bus");
"TYPE_SYSTEM_BUS->TYPE_BUS->TYPE_OBJECT"

先看下 info qtree信息:

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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
info qtree
bus: main-system-bus
type System
dev: cfi.pflash01, id ""
drive = ""
num-blocks = 128 (0x80)
sector-length = 262144 (0x40000)
width = 4 (0x4)
device-width = 2 (0x2)
max-device-width = 2 (0x2)
big-endian = false
secure = false
id0 = 137 (0x89)
id1 = 24 (0x18)
id2 = 0 (0x0)
id3 = 0 (0x0)
name = "virt.flash0"
old-multiple-chip-handling = false
mmio ffffffffffffffff/0000000002000000
// 省略 virt.flash1 , 结构同上
dev: goldfish_rtc, id ""
gpio-out "sysbus-irq" 1
mmio 0000000000101000/0000000000000024
dev: serial-mm, id ""
gpio-out "sysbus-irq" 1
regshift = 0 (0x0)
endianness = 2 (0x2)
mmio ffffffffffffffff/0000000000000008
dev: gpex-pcihost, id ""
gpio-out "sysbus-irq" 4
allow-unmapped-accesses = true
x-config-reg-migration-enabled = true
bypass-iommu = false
mmio ffffffffffffffff/0000000020000000
mmio ffffffffffffffff/ffffffffffffffff
mmio 0000000003000000/0000000000010000
bus: pcie.0
type PCIE
dev: gpex-root, id ""
addr = 00.0
romfile = ""
romsize = 4294967295 (0xffffffff)
rombar = 1 (0x1)
multifunction = false
x-pcie-lnksta-dllla = true
x-pcie-extcap-init = true
failover_pair_id = ""
acpi-index = 0 (0x0)
class Host bridge, addr 00:00.0, pci id 1b36:0008 (sub 1af4:1100)
dev: virtio-mmio, id ""
gpio-out "sysbus-irq" 1
format_transport_address = true
force-legacy = true
ioeventfd = true
mmio 0000000010008000/0000000000000200
bus: virtio-mmio-bus.7
type virtio-mmio-bus
dev: virtio-blk-device, id ""
drive = "hd0"
backend_defaults = "auto"
logical_block_size = 512 (512 B)
physical_block_size = 512 (512 B)
min_io_size = 0 (0 B)
opt_io_size = 0 (0 B)
discard_granularity = 4294967295 (4 GiB)
write-cache = "auto"
share-rw = false
rerror = "auto"
werror = "auto"
cyls = 121 (0x79)
heads = 16 (0x10)
secs = 63 (0x3f)
lcyls = 0 (0x0)
lheads = 0 (0x0)
lsecs = 0 (0x0)
serial = ""
config-wce = true
scsi = false
request-merging = true
num-queues = 1 (0x1)
queue-size = 256 (0x100)
seg-max-adjust = true
discard = true
report-discard-granularity = true
write-zeroes = true
max-discard-sectors = 4194303 (0x3fffff)
max-write-zeroes-sectors = 4194303 (0x3fffff)
x-enable-wce-if-config-wce = true
indirect_desc = true
event_idx = true
notify_on_empty = true
any_layout = false
iommu_platform = false
packed = false
use-started = true
use-disabled-flag = true
x-disable-legacy-check = false
dev: virtio-mmio, id ""
gpio-out "sysbus-irq" 1
format_transport_address = true
force-legacy = true
ioeventfd = true
mmio 0000000010007000/0000000000000200
bus: virtio-mmio-bus.6
type virtio-mmio-bus
// 省略 1-5 个 virtio-mmio 设备, 结构同上同下
dev: virtio-mmio, id ""
gpio-out "sysbus-irq" 1
format_transport_address = true
force-legacy = true
ioeventfd = true
mmio 0000000010001000/0000000000000200
bus: virtio-mmio-bus.0
type virtio-mmio-bus
dev: riscv.sifive.test, id ""
mmio 0000000000100000/0000000000001000
dev: fw_cfg_mem, id ""
data_width = 8 (0x8)
dma_enabled = true
x-file-slots = 32 (0x20)
acpi-mr-restore = true
mmio 0000000010100008/0000000000000002
mmio 0000000010100000/0000000000000008
mmio 0000000010100010/0000000000000008
dev: riscv.sifive.plic, id ""
gpio-in "" 53
gpio-out "" 4
hart-config = "S,S"
hartid-base = 0 (0x0)
num-sources = 53 (0x35)
num-priorities = 7 (0x7)
priority-base = 4 (0x4)
pending-base = 4096 (0x1000)
enable-base = 8192 (0x2000)
enable-stride = 128 (0x80)
context-base = 2097152 (0x200000)
context-stride = 4096 (0x1000)
aperture-size = 6291456 (0x600000)
mmio 000000000c000000/0000000000600000
dev: riscv.hart_array, id ""
num-harts = 2 (0x2)
hartid-base = 0 (0x0)
cpu-type = "rv64-riscv-cpu"
resetvec = 4096 (0x1000)

从上述信息中可以看到 总线->设备->总线->设备 的结构
virtio-blk-device 设备距离, 其拓扑是 bus: main-system-bus -> dev: virtio-mmio -> bus: virtio-mmio-bus.7 -> dev: virtio-blk-device
main-system-bus 为root 总线, virtio-mmio-bus.7 为子总线. 一共有8个virtio_mmio设备, 每个设备下面都挂着一条子总线为 virtio-mmio-bus.[i]
子总线上面再挂设备
这里的结构上看, 并没有pci总线, 这是因为启动qemu的命令行中并没有添加pci设备, 只加了一个virtio-blk-device作为存储

再来总结下 sysbus 的用法:

1
2
3
4
5
6
7
8
9
10
void sysbus_init_mmio(SysBusDevice *dev, MemoryRegion *memory); //初始化 sysbus 上的设备的 mmio 内存空间  
MemoryRegion *sysbus_mmio_get_region(SysBusDevice *dev, int n) //获取 sysbus 上设备的第 n 个内存空间
void sysbus_init_irq(SysBusDevice *dev, qemu_irq *p) //初始化 irq 输出引脚
void sysbus_pass_irq(SysBusDevice *dev, SysBusDevice *target);
void sysbus_connect_irq(SysBusDevice *dev, int n, qemu_irq irq) //设置 irq 输出引脚
void sysbus_mmio_map(SysBusDevice *dev, int n, hwaddr addr) //设置 mmio 地址, n 为第 n 个 MemoryRegion
void sysbus_mmio_map_overlap(SysBusDevice *dev, int n, hwaddr addr, int priority) //设置 mmio 地址
void sysbus_add_io(SysBusDevice *dev, hwaddr addr, MemoryRegion *mem); // 给 sysbus 增加 io port 地址空间
MemoryRegion *sysbus_address_space(SysBusDevice *dev) //返回 sysbus 的地址空间
static inline DeviceState *sysbus_create_simple(const char *name, hwaddr addr, qemu_irq irq) //在 sysbus 上创建设备

SYSTEM_BUS 层次结构

类型继承: TYPE_SYSTEM_BUS->TYPE_BUS->TYPE_OBJECT
类继承: BusClass (没有子类, 完全使用父类的, 只覆盖了 print_dev get_fw_dev_path)
类实例继承: BusState (没有子类实例对象, 使用父类的)

初始化函数:
类: bus_class_init system_bus_class_init
实例: qbus_initfn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct BusClass {
ObjectClass parent_class;
void (*print_dev)(Monitor *mon, DeviceState *dev, int indent); //用于 monitor 监控调试使用
char *(*get_dev_path)(DeviceState *dev); //用户获取总线上设备的路径
char *(*get_fw_dev_path)(DeviceState *dev); //用于支持 of 规范
void (*reset)(BusState *bus); //用于重置 bus
BusRealize realize; //用于 bus 的初始化
BusUnrealize unrealize; //用于析构
int max_dev; //总线上最大支持的设备数
int automatic_ids; //用于给子设备命名
};
struct BusState {
Object obj; //为总线的实例,obj 为基类
DeviceState *parent; // 表示总线的父设备,一般为总线桥设备。
char *name; //总线名称
HotplugHandler *hotplug_handler; // 热插拔处理接口
int max_index; // 当前设备最大索引
bool realized; // 是否初始化完成
QTAILQ_HEAD(ChildrenHead, BusChild) children; //用于存放总线上的子设备
QLIST_ENTRY(BusState) sibling; //用于挂载到父节点上。
};

再来看下支持挂在 sysbus 下的设备是啥样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static const TypeInfo sysbus_device_type_info = {
.name = TYPE_SYS_BUS_DEVICE,
.parent = TYPE_DEVICE, // SysBusDeviceClass 首先属于一个设备,所以父类为 DeviceClass
.instance_size = sizeof(SysBusDevice),
.abstract = true,
.class_size = sizeof(SysBusDeviceClass),
.class_init = sysbus_device_class_init,
};
struct SysBusDevice {
DeviceState parent_obj;
int num_mmio; //mmio 区域的个数
struct {
hwaddr addr; //gpa
MemoryRegion *memory; //对应的MemoryRegion
} mmio[QDEV_MAX_MMIO];
int num_pio; //pio相关, 因为riscv 不支持pio, 这里不用看了
uint32_t pio[QDEV_MAX_PIO];
};

类型继承: TYPE_SYS_BUS_DEVICE -> TYPE_DEVICE -> TYPE_OBJECT
类继承: SysBusDeviceClass -> DeviceClass
对象实例继承: SysBusDevice -> DeviceState

下面看一个具体的system-bus 挂的设备, 这里以virtio-mmio 举例:

virtio-mmio

类型继承: TYPE_VIRTIO_MMIO -> TYPE_SYS_BUS_DEVICE -> TYPE_DEVICE -> TYPE_OBJECT
类继承: SysBusDeviceClass -> DeviceClass (没有添加子类, 用的父类的)
对象实例继承: VirtIOMMIOProxy -> SysBusDevice -> DeviceState

类初始化函数: device_class_init - sysbus_device_class_init - virtio_mmio_class_init
实例初始化函数: device_initfn
具现化函数: virtio_mmio_realizefn

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
struct DeviceState {
Object parent_obj;
char *id;
char *canonical_path;
bool realized;
bool pending_deleted_event;
int64_t pending_deleted_expires_ms;
QDict *opts;
int hotplugged;
bool allow_unplug_during_migration;
BusState *parent_bus;
QLIST_HEAD(, NamedGPIOList) gpios;
QLIST_HEAD(, NamedClockList) clocks;
QLIST_HEAD(, BusState) child_bus;
int num_child_bus;
int instance_id_alias;
int alias_required_for_version;
ResettableState reset;
};
static const TypeInfo device_type_info = {
.name = TYPE_DEVICE,
.parent = TYPE_OBJECT,
.instance_size = sizeof(DeviceState),
.instance_init = device_initfn,
.instance_post_init = device_post_init,
.instance_finalize = device_finalize,
.class_base_init = device_class_base_init,
.class_init = device_class_init,
.abstract = true,
.class_size = sizeof(DeviceClass),
.interfaces = (InterfaceInfo[]) {
{ TYPE_VMSTATE_IF },
{ TYPE_RESETTABLE_INTERFACE },
{ }
}
};
struct SysBusDevice {
DeviceState parent_obj;
int num_mmio;
struct {
hwaddr addr;
MemoryRegion *memory;
} mmio[QDEV_MAX_MMIO];
int num_pio;
uint32_t pio[QDEV_MAX_PIO];
};
static const TypeInfo sysbus_device_type_info = {
.name = TYPE_SYS_BUS_DEVICE,
.parent = TYPE_DEVICE,
.instance_size = sizeof(SysBusDevice),
.abstract = true,
.class_size = sizeof(SysBusDeviceClass),
.class_init = sysbus_device_class_init,
};
struct VirtIOMMIOProxy {
/* Generic */
SysBusDevice parent_obj;
MemoryRegion iomem;
qemu_irq irq;
bool legacy;
uint32_t flags;
/* Guest accessible state needing migration and reset */
uint32_t host_features_sel;
uint32_t guest_features_sel;
uint32_t guest_page_shift;
/* virtio-bus */
VirtioBusState bus; //每个virtio-mmio 有一个子总线 virtio-bus
bool format_transport_address;
/* Fields only used for non-legacy (v2) devices */
uint32_t guest_features[2];
VirtIOMMIOQueue vqs[VIRTIO_QUEUE_MAX];
};
static const TypeInfo virtio_mmio_info = {
.name = TYPE_VIRTIO_MMIO,
.parent = TYPE_SYS_BUS_DEVICE,
.instance_size = sizeof(VirtIOMMIOProxy),
.class_init = virtio_mmio_class_init,
};

先看下这个设备创建的过程:
即在 system-bus 上挂上该设备的过程:
还记得前面 system-bus的几个比较重要的函数吗, 这里是用到了

1
static inline DeviceState *sysbus_create_simple(const char *name, hwaddr addr, qemu_irq irq) //在 sysbus 上创建设备

简单分析下 virtio-mmio 总线设备的创建过程:

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
-+ virt_machine_init(MachineState *machine)
\ - s->irqchip[0] = virt_create_plic(memmap, i, base_hartid, hart_count);
| - virtio_irqchip = s->irqchip[0];
| -+ for i in VIRTIO_COUNT:
\ -+ sysbus_create_simple("virtio-mmio",
memmap[VIRT_VIRTIO].base + i * memmap[VIRT_VIRTIO].size, "GPA地址: 0x10001000,        0x1000"
qdev_get_gpio_in(DEVICE(virtio_irqchip), VIRTIO_IRQ + i));
\ - irq = qdev_get_gpio_in(DEVICE(virtio_irqchip) "找sifive_plic的irq 输入irq 即: gpio_list->in[VIRTIO_IRQ+i]"
| -+ dev = qdev_new(name); "创建设备, 实例化, 调用 device_initfn"
| -+ sysbus_realize_and_unref(s, &error_fatal); "具现化, 调用 virtio_mmio_realizefn"
\ -+ qdev_realize_and_unref(DEVICE(dev), sysbus_get_default(), errp);
\ -+ qdev_realize(dev, bus, errp);
\ -+ qdev_set_parent_bus(dev, bus, errp)
\ - dev->parent_bus = bus;
| -+ bus_add_child(bus, dev);
\ - BusChild *kid = g_malloc0(sizeof(*kid));
| - kid->child = child;
| - kid->index = bus->max_index++; "children+1"
| - bus->num_children++;
| - object_property_add_link(OBJECT(bus), "child[%(kid->index)]", object_get_typename(OBJECT(child)),
(Object **)&kid->child, NULL, 0); "添加对BusChild的引用关系"
| -+ object_property_set_bool(OBJECT(dev), "realized", true, errp);
\ ---+ virtio_mmio_realizefn(DeviceState *d)
\ - VirtIOMMIOProxy *proxy = VIRTIO_MMIO(d);
| - SysBusDevice *sbd = SYS_BUS_DEVICE(d);
| -+ qbus_init(&proxy->bus, sizeof(proxy->bus), TYPE_VIRTIO_MMIO_BUS, d, NULL);
"新建了一个bus, 这个地方即对应前面info qtree中看到的virtio-mmio-bus.[i]"
| - sysbus_init_irq(sbd, &proxy->irq);
"初始化 设备gpio_out, 定好地址 VirtIOMMIOProxy的irq的地址"
| -+ sysbus_mmio_map(s, 0, addr); "设置 mmio 地址"
\ - memory_region_add_subregion(get_system_memory(), addr, dev->mmio[n].memory);
| - sysbus_connect_irq(s, n, irq);
"绑定irq, 设备的gpio_out连中断控制器的gpio_in 即 sifive_plic的 gpio_list->in[VIRTIO_IRQ+i]
即 VirtIOMMIOProxy实例的 irq的地址 指向了 sifive_plic的 gpio_list->in[VIRTIO_IRQ+i]"

上述过程主要做了这么几件事:

  • virtio-mmio 设备实例化, 具现化
  • virtio-mmio 设备挂到了总线上
  • 设置了mmio 地址
  • 设备输出irq 连到了中断控制器 sifive_plic的 gpio_list->in[VIRTIO_IRQ+i] pin
  • 新建了一条virtio-mmio-bus.[i] 子总线

那子总线上的virtio-blk-device是什么时候创建的呢?

virtio-mmio-bus 分析

类型继承: TYPE_VIRTIO_MMIO_BUS -> TYPE_VIRTIO_BUS -> TYPE_BUS -> TYPE_OBJECT
类继承: VirtioBusClass -> BusClass
对象实例继承: VirtioBusState -> BusState (没有添加子类, 用的父类的)

类初始化函数: bus_class_init - virtio_bus_class_init - virtio_mmio_bus_class_init
实例初始化函数: qbus_initfn

这里没有特别要注意的, 只需要关注其增加的字段的函数指针, 即 VirtioBusClass 和 VirtioBusState 增加的内容

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
struct VirtioBusState {
BusState parent_obj;
bool ioeventfd_started;
int ioeventfd_grabbed;
};
struct VirtioBusClass {
/* This is what a VirtioBus must implement */
BusClass parent;
void (*notify)(DeviceState *d, uint16_t vector);
void (*save_config)(DeviceState *d, QEMUFile *f);
void (*save_queue)(DeviceState *d, int n, QEMUFile *f);
void (*save_extra_state)(DeviceState *d, QEMUFile *f);
int (*load_config)(DeviceState *d, QEMUFile *f);
int (*load_queue)(DeviceState *d, int n, QEMUFile *f);
int (*load_done)(DeviceState *d, QEMUFile *f);
int (*load_extra_state)(DeviceState *d, QEMUFile *f);
bool (*has_extra_state)(DeviceState *d);
bool (*query_guest_notifiers)(DeviceState *d);
int (*set_guest_notifiers)(DeviceState *d, int nvqs, bool assign);
int (*set_host_notifier_mr)(DeviceState *d, int n,
MemoryRegion *mr, bool assign);
void (*vmstate_change)(DeviceState *d, bool running);
void (*pre_plugged)(DeviceState *d, Error **errp);
void (*device_plugged)(DeviceState *d, Error **errp);
void (*device_unplugged)(DeviceState *d);
int (*query_nvectors)(DeviceState *d);
bool (*ioeventfd_enabled)(DeviceState *d);
int (*ioeventfd_assign)(DeviceState *d, EventNotifier *notifier,
int n, bool assign);
bool (*queue_enabled)(DeviceState *d, int n);
bool has_variable_vring_alignment;
AddressSpace *(*get_dma_as)(DeviceState *d);
bool (*iommu_enabled)(DeviceState *d);
};

virt bus 支持virtio queue, vhost 架构, 所以会多出一些字段来
上面的部分有需要再分析, 现在重点看下 virtio-blk-device 怎么加到 这个子总线上的

virtio-blk-device

类型继承: TYPE_VIRTIO_BLK -> TYPE_VIRTIO_DEVICE -> TYPE_DEVICE -> TYPE_OBJECT
类继承: VirtioDeviceClass -> DeviceClass
对象实例继承: VirtIOBlock -> VirtIODevice -> DeviceState

初始化函数:
类: virtio_blk_class_init -> virtio_device_class_init -> device_class_init
实例: virtio_blk_instance_init -> device_initfn
具现化: virtio_blk_device_realize

再看一下命令:

1
2
-drive file=rootfs.ext2,format=raw,id=hd0 \
-device virtio-blk-device,drive=hd0 \
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
DEF("device", HAS_ARG, QEMU_OPTION_device,
-+ case QEMU_OPTION_device:
\ - QObject *obj = qobject_from_json(optarg, &error_fatal);
| - QTAILQ_INSERT_TAIL(&device_opts, opt, next);
-+ qemu_create_cli_devices
\ -+ for opt in device_opts
\ -+ dev = qdev_device_add_from_qdict(opt->opts, true, &error_fatal);
\ - DeviceClass* dc = qdev_get_device_class(&driver, errp); "从object中找到virtio-blk-device 类"
| - path = qdict_get_try_str(opts, "bus"); "先从命令行提供的bus中找"
| -|+ if path != NULL "找到了, 也就是命令行有指定bus的情况"
\ - bus = qbus_find(path, errp);
| - match object_dynamic_cast(OBJECT(bus), dc->bus_type) "命令行指定的path 需要匹配bus_type, 否则返回出错"
"这里匹配了 bus_type, 对于virtio-blk-device,
它的bus_type 是在父类初始化函数中 virtio_device_class_init 赋值为 TYPE_VIRTIO_BUS 的"
| -|+ else if path == NULL "命令行没有指定 bus"
\ - bus = qbus_find_recursive(sysbus_get_default(), NULL, dc->bus_type);
"从root 总线即 main-system-bus 遍历子总线, 找match 设备 bus_type的bus"
| -+ dev = qdev_new(driver); "创建设备, 实例化"
\ -+ virtio_blk_instance_init
| -+ qdev_realize(DEVICE(dev), bus, errp) "设备具现化"
\ -+ qdev_set_parent_bus(dev, bus, errp)
\ - dev->parent_bus = bus;
| -+ bus_add_child(bus, dev);
\ - BusChild *kid = g_malloc0(sizeof(*kid));
| - kid->child = child;
| - kid->index = bus->max_index++; "children+1"
| - bus->num_children++;
| - object_property_add_link(OBJECT(bus), "child[%(kid->index)]", object_get_typename(OBJECT(child)),
(Object **)&kid->child, NULL, 0); "添加对BusChild的引用关系"
| -+ object_property_set_bool(OBJECT(dev), "realized", true, errp)
\ ---+ virtio_blk_device_realize(dev)

设备发现

总线-驱动-设备 模型
Linux中, 所有的设备通过总线连接,
当总线类型注册后,/sys/bus/目录下,创建了一个新的目录virtio,在该目录下同时创建了两个文件夹为devices和drivers。表示创建virtio总线,总线支持设备与驱动devices和drivers目录。

linux kernel 中的调用:
8次 virtio_mmio_probe 调用, 说明有8个virtio_mmio 设备

1
2
3
4
5
6
7
8
9
10
11
12
13
#0  virtio_mmio_probe (pdev=0xff600000016b7c00) at ../drivers/virtio/virtio_mmio.c:601
#1 0xffffffff803c7c9a in platform_probe (_dev=0xff600000016b7c10) at ../drivers/base/platform.c:1400
#2 0xffffffff803c5cda in call_driver_probe (drv=0xffffffff8129ab00 <virtio_mmio_driver+40>, dev=0xff600000016b7c10) at ../drivers/base/dd.c:555
#3 really_probe (dev=dev@entry=0xff600000016b7c10, drv=drv@entry=0xffffffff8129ab00 <virtio_mmio_driver+40>) at ../drivers/base/dd.c:634
#4 0xffffffff803c5ea2 in __driver_probe_device (drv=drv@entry=0xffffffff8129ab00 <virtio_mmio_driver+40>, dev=dev@entry=0xff600000016b7c10) at ../drivers/base/dd.c:764
#5 0xffffffff803c5efe in driver_probe_device (drv=drv@entry=0xffffffff8129ab00 <virtio_mmio_driver+40>, dev=dev@entry=0xff600000016b7c10) at ../drivers/base/dd.c:794
#6 0xffffffff803c64f8 in __driver_attach (dev=0xff600000016b7c10, data=0xffffffff8129ab00 <virtio_mmio_driver+40>) at ../drivers/base/dd.c:1176
#7 0xffffffff803c419e in bus_for_each_dev (bus=<optimized out>, start=start@entry=0x0, data=data@entry=0xffffffff8129ab00 <virtio_mmio_driver+40>, fn=fn@entry=0xffffffff803c642c <__driver_attach>) at ../drivers/base/bus.c:301
#8 0xffffffff803c58f0 in driver_attach (drv=drv@entry=0xffffffff8129ab00 <virtio_mmio_driver+40>) at ../drivers/base/dd.c:1193
#9 0xffffffff803c5248 in bus_add_driver (drv=drv@entry=0xffffffff8129ab00 <virtio_mmio_driver+40>) at ../drivers/base/bus.c:618
#10 0xffffffff803c6c30 in driver_register (drv=drv@entry=0xffffffff8129ab00 <virtio_mmio_driver+40>) at ../drivers/base/driver.c:246
#11 0xffffffff803c79f0 in __platform_driver_register (drv=drv@entry=0xffffffff8129aad8 <virtio_mmio_driver>, owner=owner@entry=0x0) at ../drivers/base/platform.c:867
#12 0xffffffff8081c83a in virtio_mmio_init () at ../drivers/virtio/virtio_mmio.c:833

对于virtio_mmio 设备, 在前面章节中分析是挂载 main_system_bus 上的, 而这里调用的 __platform_driver_register 探测的设备, 是否可以理解为linux 重点的platform 总线就是 main_system_bus呢

virtio_mmio_probe 最后会调用 register_virtio_device(&vm_dev->vdev); 注册设备, 并且对设备进行状态设置,VIRTIO_CONFIG_S_ACKNOWLEDGE 表示是发现了设备,这里注册设备到virtio_bus,这里会触发vritio_bus的probe函数。

这里查询了每个virtio_mmio 设备的 base (GPA) 地址, 查询版本号, version, id.device 信息, 特别的在查到id.device (base + VIRTIO_MMIO_DEVICE_ID) 时, 如果device 为 0, 表明这个virtio_mmio 设备的子总线上没有挂设备, probe 就到此为止了, 不会再走后面的 register_virtio_device 流程.
对应前面章节里 只有一个virtio_mmio上挂了virtio-blk-device, 其他7个都是空的, 这里正好能对应上.

对应qemu 中陷入virtio_mmio GPA查询, 返回的是设备初始化时的device_id, 这个字段对于virtio-blk-device 来说, 正是在其具现化virtio_blk_device_realize 时设置的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    case VIRTIO_MMIO_DEVICE_ID:
return vdev->device_id;
-+ virtio_blk_device_realize
\ -+ virtio_init(vdev, "virtio-blk", VIRTIO_ID_BLOCK, s->config_size);
\ - vdev->device_id = device_id; "VIRTIO_ID_BLOCK=2 为 device_id"

" device_id 列表:"
#define VIRTIO_ID_NET 1 /* virtio net */
#define VIRTIO_ID_BLOCK 2 /* virtio block */
#define VIRTIO_ID_CONSOLE 3 /* virtio console */
#define VIRTIO_ID_RNG 4 /* virtio rng */
#define VIRTIO_ID_BALLOON 5 /* virtio balloon */
#define VIRTIO_ID_IOMEM 6 /* virtio ioMemory */
#define VIRTIO_ID_RPMSG 7 /* virtio remote processor messaging */
#define VIRTIO_ID_SCSI 8 /* virtio scsi */
#define VIRTIO_ID_9P 9 /* 9p virtio console */
#define VIRTIO_ID_MAC80211_WLAN 10 /* virtio WLAN MAC */
#define VIRTIO_ID_RPROC_SERIAL 11 /* virtio remoteproc serial link */
#define VIRTIO_ID_CAIF 12 /* Virtio caif */
#define VIRTIO_ID_MEMORY_BALLOON 13 /* virtio memory balloon */
#define VIRTIO_ID_GPU 16 /* virtio GPU */
...

再细看下 register_virtio_device, 这个函数把加的device的bus 设置成了 virtio_bus, 对照前面章节的 virtio_mmio_bus 总线父类型为 TYPE_VIRTIO_BUS, 正好对应上. linux kernel 这边没有再细分, 只加了一条virtio_bus 出来, 这个总线上挂所有的 virtio 设备.
register_virtio_device的意思就是将从其他总线上发现的设备, 如果这个设备下还挂着子总线, 子总线上还挂着设备, 就将设备添加到 virtio_bus.

这个函数有两个地方调用, 一个是virtio_mmio 设备virtio_mmio_probe 时, 另一个是 pci 设备virtio_pci_probe时, 往qemu中找正好发现了 TYPE_VIRTIO_PCI_BUS 其父类为 TYPE_VIRTIO_BUS
说明还有一条子总线为 TYPE_VIRTIO_PCI_BUS

新加几个设备看一下:

1
2
3
-device virtio-blk-device,drive=hd0 \
-device virtio-net-device,netdev=net0 \
-device virtio-rng-pci \

这里我们看一个新的qtree信息:

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
dev: gpex-pcihost, id ""
gpio-out "sysbus-irq" 4
allow-unmapped-accesses = true
x-config-reg-migration-enabled = true
bypass-iommu = false
mmio ffffffffffffffff/0000000020000000
mmio ffffffffffffffff/ffffffffffffffff
mmio 0000000003000000/0000000000010000
bus: pcie.0
type PCIE
dev: virtio-rng-pci, id ""
disable-legacy = "off"
aer = false
addr = 01.0
romfile = ""
romsize = 4294967295 (0xffffffff)
rombar = 1 (0x1)
...
bar 0: i/o at 0x0 [0x1f]
bar 4: mem at 0x400000000 [0x400003fff]
bus: virtio-bus
type virtio-pci-bus
dev: virtio-rng-device, id ""
max-bytes = 9223372036854775807 (0x7fffffffffffffff)
...
...
dev: virtio-mmio, id ""
gpio-out "sysbus-irq" 1
format_transport_address = true
force-legacy = true
ioeventfd = false
mmio 0000000010008000/0000000000000200
bus: virtio-mmio-bus.7
type virtio-mmio-bus
dev: virtio-blk-device, id ""
drive = "hd0"
backend_defaults = "auto"
logical_block_size = 512 (512 B)
physical_block_size = 512 (512 B)
...
dev: virtio-mmio, id ""
gpio-out "sysbus-irq" 1
format_transport_address = true
force-legacy = true
ioeventfd = false
mmio 0000000010007000/0000000000000200
bus: virtio-mmio-bus.6
type virtio-mmio-bus
dev: virtio-net-device, id ""
csum = true
...

没带pci的设备, 最终都是挂在了 virtio-mmio 子总线上
而带了pci的设备, 挂在了 virtio-pci-bus 子总线上
dev: gpex-pcihost -> bus: pcie.0 -> dev: virtio-rng-pci -> bus: virtio-pci-bus-> dev: virtio-rng-device
可以看出virtio-pci的层次要比 virtio-mmio的复杂很多, 这里先不对pci的展开
接下来看一下virtio_mmio_bus 上挂的设备的注册过程:

在对应驱动初始化时, 函数register_virtio_driver(&virtio_driver)就是对bus链表的每一个virtio设备进行探测,遍历注册的驱动是否支持该设备,如果支持,调用驱动probe函数,完成启用该virtio设备

1
2
3
4
5
6
7
8
9
10
11
12
13
#0  virtblk_probe (vdev=0xff6000000160ec28) at ../drivers/block/virtio_blk.c:885
#1 0xffffffff80381f40 in virtio_dev_probe (_d=0xff6000000160ec60) at ../drivers/virtio/virtio.c:305
#2 0xffffffff803c5cda in call_driver_probe (drv=0xffffffff812a4588 <virtio_blk>, dev=0xff6000000160ec60) at ../drivers/base/dd.c:555
#3 really_probe (dev=dev@entry=0xff6000000160ec60, drv=drv@entry=0xffffffff812a4588 <virtio_blk>) at ../drivers/base/dd.c:634
#4 0xffffffff803c5ea2 in __driver_probe_device (drv=drv@entry=0xffffffff812a4588 <virtio_blk>, dev=dev@entry=0xff6000000160ec60) at ../drivers/base/dd.c:764
#5 0xffffffff803c5efe in driver_probe_device (drv=drv@entry=0xffffffff812a4588 <virtio_blk>, dev=dev@entry=0xff6000000160ec60) at ../drivers/base/dd.c:794
#6 0xffffffff803c64f8 in __driver_attach (dev=0xff6000000160ec60, data=0xffffffff812a4588 <virtio_blk>) at ../drivers/base/dd.c:1176
#7 0xffffffff803c419e in bus_for_each_dev (bus=<optimized out>, start=start@entry=0x0, data=data@entry=0xffffffff812a4588 <virtio_blk>, fn=fn@entry=0xffffffff803c642c <__driver_attach>) at ../drivers/base/bus.c:301
#8 0xffffffff803c58f0 in driver_attach (drv=drv@entry=0xffffffff812a4588 <virtio_blk>) at ../drivers/base/dd.c:1193
#9 0xffffffff803c5248 in bus_add_driver (drv=drv@entry=0xffffffff812a4588 <virtio_blk>) at ../drivers/base/bus.c:618
#10 0xffffffff803c6c30 in driver_register (drv=drv@entry=0xffffffff812a4588 <virtio_blk>) at ../drivers/base/driver.c:246
#11 0xffffffff803818aa in register_virtio_driver (driver=driver@entry=0xffffffff812a4588 <virtio_blk>) at ../drivers/virtio/virtio.c:357
#12 0xffffffff8081fb44 in virtio_blk_init () at ../drivers/block/virtio_blk.c:1212

那设备如果是后加到总线上, 是怎样的机制呢
当设备添加到链表时,会触发总线的match函数

但是qemu mmio子总线和pci子总线都不支持hot-plug, 不清楚是否是qemu配置的问题, 从qemu代码上看, 确实没有设置bus的hotplug hanlder

1
2
3
4
5
6
(qemu) device_add virtio-rng-pci
Error: Bus 'pcie.0' does not support hotplugging
(qemu) device_add virtio-net-device
Error: Bus 'virtio-mmio-bus.6' does not support hotplugging
(qemu) device_add virtio-blk-device,drive=hd1
Error: Bus 'virtio-mmio-bus.6' does not support hotplugging

但大概猜一下, 应该是qemu 命令行中输入device_add <device> 后, qemu 进程中会按之前的步骤新建设备出来, 绑到对应总线上, 设备具现化后应该会调用notify 类似的函数, 以中断的方式给到 kvm, kvm再路由给guest, guest 知道总线发生变化了, 会重新枚举总线上的设备, 然后调用register_virtio_device 往virtio_bus 上新加设备, 最重要的是调用device_add 函数, 该函数会初始化新设备的状态, dev sys下的文件更新, 调用bus_probe_device 最终调用到Bus的probe函数(virtio_dev_probe)和设备的 probe(如virtblk_probe) 函数, 完成设备的使能.

PCI 设备

每一个PCI设备在系统中的位置由总线号(Bus Number),设备号(Device Number)以及功能号(Function Number)唯一确定。有的设备可能有多个功能,从逻辑上来说是单独的设备。可以在PCI总线上挂一个桥设备,之后在该桥上再挂一个PCI总线或者其他总线

virtio设备首先需要创建一个PCI设备,叫作virtio PCI代理设备,这个代理设备挂到PCI总线上,接着virtio代理设备再创建一条virtio总线,这样virtio设备就可以挂到这条总线上了。

virtio PCI代理的父设备是一个PCI设备,类型为VirtioPCIClass,实例为VirtIOPCIProxy, QEMU中定义了所有virtio设备的PCI代理设备,如virtio balloon PCI设备、virtio scsi PCI设备、virito crypto PCI设备

PCI设备有自己独立的地址空间,叫作PCI地址空间,也就是说从设备角度看到的地址跟CPU角度看到的地址本质上不在一个地址空间,这种隔离就是由图中的HOST-PCI主桥完成的。

CPU需要通过主桥才能访问PCI设备,而PCI设备也需要通过主桥才能访问主存储器。主桥的一个重要作用就是将处理器访问的存储器地址转换为PCI总线地址。
每个PCI设备都有一个配置空间,该空间至少有256字节,其中前面64个字节是标准化的,每个设备都是这个格式,后面的数据则由设备决定。

PCI配置空间:

  • Vendor ID、Device ID、Class Code用来表明设备的身份,有的时候还会设置Subsystem Vendor ID和Subsystem Device ID。
  • 6个Base Address表示的是PCI设备的IO地址空间,虚拟机可以通过这些地址空间对设备进行读写配置控制
  • 还可能有一个ROM的BAR。
  • 有两个与中断设置有关的值
    • IRQ Line表示设备使用哪一个中断号, IRQ Line表示的是用哪一根线
    • IRQ Pin表示的是PCI设备使用哪一条引脚连接中断控制器,这里的IRQ Pin即用来表示这个引脚编号

在PCI设备的模拟中,不需要关注与电气相关的部分,只需要关注与操作系统的接口部分。设备与操作系统的接口主要包括PCI设备的配置空间以及PCI设备的寄存器基址。所有PCI设备的基类都是TYPE_PCI_DEVICE,所有PCI设备在初始化时都会分配一块PCI配置空间保存在PCIDevice的config中,do_pci_register_device函数会初始化PCI配置空间的基本数据。在具体设备进行具现化的函数中会初始化PCI配置空间的一些其他数据,还会分配设备需要的其他资源。具体设备的具现化函数囊括在用于调用具体PCI设备类的具现化函数中

kernel 枚举PCI 设备

pci controller 设备挂在platform 总线下, pci controller 设备加载驱动, 调用到 pci_host_common_probe , pci controller 设备下挂着pci总线, pci 总线下挂着pci 设备.
pci 设备枚举发生在 pci controller 设备加载驱动时.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#0  pci_setup_device (dev=dev@entry=0xff600000019fc000) at ../drivers/pci/probe.c:1818
#1 0xffffffff8034738e in pci_scan_device (devfn=<optimized out>, bus=0xff60000002206000) at ../drivers/pci/probe.c:2418
#2 pci_scan_single_device (bus=bus@entry=0xff60000002206000, devfn=devfn@entry=0) at ../drivers/pci/probe.c:2572
#3 0xffffffff80347424 in pci_scan_slot (bus=bus@entry=0xff60000002206000, devfn=devfn@entry=0) at ../drivers/pci/probe.c:2652
#4 0xffffffff8034837a in pci_scan_child_bus_extend (bus=0xff60000002206000, available_buses=available_buses@entry=0) at ../drivers/pci/probe.c:2869
#5 0xffffffff803486d4 in pci_scan_child_bus (bus=<optimized out>) at ../drivers/pci/probe.c:2999
#6 pci_scan_root_bus_bridge (bridge=bridge@entry=0xff60000002283400) at ../drivers/pci/probe.c:3177
#7 0xffffffff8034874c in pci_host_probe (bridge=bridge@entry=0xff60000002283400) at ../drivers/pci/probe.c:3057
#8 0xffffffff8036429a in pci_host_common_probe (pdev=<optimized out>) at ../drivers/pci/controller/pci-host-common.c:84
#9 0xffffffff803c7c9a in platform_probe (_dev=0xff600000016b5c10) at ../drivers/base/platform.c:1400
#10 0xffffffff803c5cda in call_driver_probe (drv=0xffffffff812983b0 <gen_pci_driver+40>, dev=0xff600000016b5c10) at ../drivers/base/dd.c:555
#11 really_probe (dev=dev@entry=0xff600000016b5c10, drv=drv@entry=0xffffffff812983b0 <gen_pci_driver+40>) at ../drivers/base/dd.c:634
#12 0xffffffff803c5ea2 in __driver_probe_device (drv=drv@entry=0xffffffff812983b0 <gen_pci_driver+40>, dev=dev@entry=0xff600000016b5c10) at ../drivers/base/dd.c:764
#13 0xffffffff803c5efe in driver_probe_device (drv=drv@entry=0xffffffff812983b0 <gen_pci_driver+40>, dev=dev@entry=0xff600000016b5c10) at ../drivers/base/dd.c:794
#14 0xffffffff803c64f8 in __driver_attach (dev=0xff600000016b5c10, data=0xffffffff812983b0 <gen_pci_driver+40>) at ../drivers/base/dd.c:1176
#15 0xffffffff803c419e in bus_for_each_dev (bus=<optimized out>, start=start@entry=0x0, data=data@entry=0xffffffff812983b0 <gen_pci_driver+40>, fn=fn@entry=0xffffffff803c642c <__driver_attach>) at ../drivers/base/bus.c:301

virtio-balloon-pci 分析

VirtIOBalloonPCI是virtio balloon PCI代理设备的实例对象,其包括两个部分:

  1. 一个是VirtIOPCIProxy,这个是virtio PCI代理设备的通用结构,里面存放了具体virtio PCI代理设备的相关成员
  2. 一个是VirtIOBalloon,这个结构里面存放的是virtio balloon设备的相关数据,其第一个成员是VirtIODevice,也就是virtio公共设备的实例对象
    VirtIOBalloon剩下的成员是与virtio balloon设备相关的数据。

这里以virtio balloon设备为例分析virtio设备的初始化过程。创建virtio balloon时只需要创建其PCI代理设备(即TYPE_VIRTIO_BALLOON_PCI)即可,在命令行指定-device virtio-balloon-pci,先来看实例化函数。

层次关系

pci 设备的层级经过了一层封装, 在类型注册时, 会调用 virtio_pci_types_register 函数, 会注册两个类型
对应于virtio-balloon-pci 而言, 会注册 virtio-balloon-pci-base 类型和 virtio-balloon-pci 类型

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
typedef struct VirtioPCIDeviceTypeInfo {
const char *base_name;
const char *generic_name;
const char *transitional_name;
const char *non_transitional_name;
const char *parent;
size_t instance_size;
size_t class_size;
void (*instance_init)(Object *obj);
void (*class_init)(ObjectClass *klass, void *data);
InterfaceInfo *interfaces;
} VirtioPCIDeviceTypeInfo;
void virtio_pci_types_register(const VirtioPCIDeviceTypeInfo *t)
{
char *base_name = NULL;
TypeInfo base_type_info = {
.name = t->base_name,
.parent = t->parent ? t->parent : TYPE_VIRTIO_PCI,
.instance_size = t->instance_size,
.instance_init = t->instance_init,
.class_size = t->class_size,
.abstract = true,
.interfaces = t->interfaces,
};
TypeInfo generic_type_info = {
.name = t->generic_name,
.parent = base_type_info.name,
.class_init = virtio_pci_generic_class_init,
.interfaces = (InterfaceInfo[]) {
{ INTERFACE_PCIE_DEVICE },
{ INTERFACE_CONVENTIONAL_PCI_DEVICE },
{ }
},
};
base_type_info.class_init = virtio_pci_base_class_init;
base_type_info.class_data = (void *)t;
type_register(&base_type_info); // 注册base_type_info
if (generic_type_info.name) {
type_register(&generic_type_info); // 注册 generic_type_info
}
}

static void virtio_pci_base_class_init(ObjectClass *klass, void *data)
{
const VirtioPCIDeviceTypeInfo *t = data;
if (t->class_init) {
t->class_init(klass, NULL);
}
}

#define TYPE_VIRTIO_BALLOON_PCI "virtio-balloon-pci-base"
static const VirtioPCIDeviceTypeInfo virtio_balloon_pci_info = {
.base_name = TYPE_VIRTIO_BALLOON_PCI,
.generic_name = "virtio-balloon-pci",
.transitional_name = "virtio-balloon-pci-transitional",
.non_transitional_name = "virtio-balloon-pci-non-transitional",
.instance_size = sizeof(VirtIOBalloonPCI),
.instance_init = virtio_balloon_pci_instance_init,
.class_init = virtio_balloon_pci_class_init,
};

两个类初始化函数 virtio_balloon_pci_class_init virtio_pci_generic_class_init
实例初始化函数: virtio_balloon_pci_instance_init

balloon-pci:
类型继承: TYPE_VIRTIO_BALLOON_PCI -> TYPE_VIRTIO_PCI -> TYPE_PCI_DEVICE -> TYPE_DEVICE
类继承: VirtioPCIClass -> PCIDeviceClass -> DeviceClass
实例继承: VirtIOBalloonPCI -> VirtIOPCIProxy -> PCIDevice -> DeviceState
具现化函数 pci_qdev_realize pci_default_realize

类初始化函数: virtio_pci_class_init - pci_device_class_init

1
2
3
4
5
6
7
8
-+ virtio_balloon_pci_instance_init(Object *obj)
\ - VirtIOBalloonPCI *dev = VIRTIO_BALLOON_PCI(obj);
| -+ virtio_instance_init_common(obj, &dev->vdev, sizeof(dev->vdev), TYPE_VIRTIO_BALLOON);
"将VirtIOBalloonPCI结构体的vdev成员地址作为 proxy_obj 以及TYPE_VIRTIO_BALLOON作为参数传递给该函数"
\ - object_initialize_child_with_props(proxy_obj, "virtio-backend", vdev, "初始化TYPE_VIRTIO_BALLOON的实例对象VirtIOBalloon"
vdev_size, TYPE_VIRTIO_BALLOON, &error_abort, NULL);
"然后添加一些属性。由此可见,virtio设备在实例创建过程中并没有做很多事情,大部分的工作是在设备的具现化过程中做的。"
| - qdev_alias_all_properties(vdev, proxy_obj); "添加一些属性"
balloon-pci-device 具现化函数

再来看下具现化函数

在介绍virtio balloon设备的具现化之前,先来回顾一下设备具现化调用的函数,QEMU在main函数中会对所有-device的参数进行具现化,设备的具现化函数都会调用device_set_realized函数,在该函数中会调用设备类的realize函数。

  • 最开始调用的是DeviceClass的realize函数,这个回调的默认函数是device_realize,
  • 当然,如果继承自DeviceClass的类可以重写这个函数,如PCIDeviceClass类就在其类初始化函数pci_device_class_init中将DeviceClass->realize重写为pci_qdev_realize,对于PCIDeviceClass本身来说,其PCIDeviceClass->realize可设置为pci_default_realize,
  • 后面继承PCIDeviceClass的类可以在自己的类初始化函数中设置realize函数。

通常来说父类的realize函数会调用子类的realize函数,如DeviceClass->realize(pci_qdev_realize)会调用PCIDeviceClass->realize回调,PCIDeviceClass->realize回调可以调用子类型的realize函数, 但是这两条语句改变了这个顺序。这里dc->realize成了virtio_pci_dc_realize,所以这个函数会最先执行,然后将原来的dc->realize(pci_qdev_realize)保存到VirtioPCIClass->parent_dc_realize函数中。

通常在设备具现化过程中子类型的realize函数需要先做某些事情的时候会使用这种方法。

1
2
3
4
5
6
-+ virtio_pci_class_init(ObjectClass *klass)
\ - VirtioPCIClass *vpciklass = VIRTIO_PCI_CLASS(klass); "子类"
| - DeviceClass *dc = DEVICE_CLASS(klass); "父类"
| -+ device_class_set_parent_realize(dc, virtio_pci_dc_realize, &vpciklass->parent_dc_realize);
\ - *parent_realize = dc->realize; "子类的换成了父类的"
| - dc->realize = virtio_pci_dc_realize; "父类的realize 换成了子类的"

所以设置virtio PCI代理设备的realized属性时,device_set_realized函数中会首先调用DeviceClass->realize,也就是这里的virtio_pci_dc_realizevirtio_pci_dc_realize函数中会调用VirtioPCIClass->parent_dc_realize函数,也就是这里的pci_qdev_realize。在pci_qdev_realize会调用PCIDeviceClass的realize函数,也就是这里的virtio_pci_realize。在这个函数的最后会调用VirtioPCIClass的realize函数,也就是这里的virtio_balloon_pci_realize

调用链:
virtio_pci_dc_realize -> pci_qdev_realize -> virtio_pci_realize -> virtio_balloon_pci_realize

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
-+ virtio_balloon_pci_instance_init(Object* obj)
\ - VirtIOBalloonPCI *dev = VIRTIO_BALLOON_PCI(obj);
*| - virtio_instance_init_common(obj, &dev->vdev, sizeof(dev->vdev), TYPE_VIRTIO_BALLOON);
"VirtIOBalloonPCI 是代理设备, 其vdev上挂着真实设备, 类型为 TYPE_VIRTIO_BALLOON, 是在实例化时做的"
-+ virtio_pci_dc_realize(DeviceState *qdev)
\ - VirtioPCIClass *vpciklass = VIRTIO_PCI_GET_CLASS(qdev);
| - VirtIOPCIProxy *proxy = VIRTIO_PCI(qdev);
| - PCIDevice *pci_dev = &proxy->pci_dev;
| -|+ if (!(proxy->flags & VIRTIO_PCI_FLAG_DISABLE_PCIE) && virtio_pci_modern(proxy)
\ - pci_dev->cap_present |= QEMU_PCI_CAP_EXPRESS; "PCIE"
| -+ vpciklass->parent_dc_realize(qdev, errp);
\ -+ pci_qdev_realize(qdev)
\ - PCIDevice *pci_dev = (PCIDevice *)qdev;
| - pci_dev = do_pci_register_device(pci_dev, object_get_typename(OBJECT(qdev)), pci_dev->devfn, errp);
"将virtioPCI代理设备注册到PCI总线上"
| - PCIDeviceClass *pc = PCI_DEVICE_GET_CLASS(pci_dev);
| -+ pc->realize(pci_dev, &local_err);
\ -+ virtio_pci_realize(pci_dev)
\ - VirtIOPCIProxy *proxy = VIRTIO_PCI(pci_dev);
| - proxy->legacy_io_bar_idx = 0;
"virtio pci bar layout used by default.
region 0   --  virtio legacy io bar
region 1   --  msi-x bar
region 2   --  virtio modern io bar (off by default)
region 4+5 --  virtio modern memory (64bit) bar"
| - proxy->msix_bar_idx = 1;
| - ... "初始化VirtIOPCIProxy设备的多个BAR数据,设置了这些BAR的索引号,
其中legacy I/O地址为0,msi-x地址为1,modern IO地址为2,modern MMIO地址为4"
| - memory_region_init(&proxy->modern_bar, OBJECT(proxy), "初始化MemoryRegion, 存在 VirtIOPCIProxy的modern_bar成员"
"virtio-pci", pow2ceil(proxy->notify.offset + proxy->notify.size));
| -+ virtio_pci_bus_new(&proxy->bus, sizeof(proxy->bus), proxy); "添加pci bus 子总线"
\ - char virtio_bus_name[] = "virtio-bus";
| - qbus_init(bus, bus_size, TYPE_VIRTIO_PCI_BUS, qdev, virtio_bus_name); "创建 virtio-pci-bus类型的子总线"
| - VirtioPCIClass *k = VIRTIO_PCI_GET_CLASS(pci_dev);
| -+ k->realize(proxy, errp);
\ -+ virtio_balloon_pci_realize(proxy_dev)
\ - VirtIOBalloonPCI *dev = VIRTIO_BALLOON_PCI(vpci_dev);
"VirtIOPCIProxy类型的设备转换为VirtIOBalloonPCI , 父类转子类"
| - DeviceState *vdev = DEVICE(&dev->vdev);
"VirtIOBalloonPCI是代理, 代理上挂着设备, 类型为TYPE_VIRTIO_BALLOON, 这个是在VirtIOBalloonPCI 实例化时做的"
| -+ qdev_realize(vdev, BUS(&vpci_dev->bus), errp);
\ - qdev_set_parent_bus(vdev, bus, errp)
"设置virtio balloon设备的总线为VirtIOPCIProxy设备中的bus成员,也就是把这个virtio balloon设备挂到了virtio-bus 子总线上"
| -+ object_property_set_bool(OBJECT(vdev), "realized", true, errp); "设备具现化"
\ -+ virtio_device_realize(vdev) "TYPE_VIRTIO_BALLOON virtio-balloon 设备具现化"
"virtio_device_realize函数其实也是一个通用函数,是类型为TYPE_VIRTIO_DEVICE抽象设备的具现化函数"
\ - VirtioDeviceClass *vdc = VIRTIO_DEVICE_GET_CLASS(dev);
"这个函数首先得到virtio设备所属的类,然后调用具体类的realize函数"
"对于virtio balloon设备来说是virtio_balloon_device_realize"
| -+ vdc->realize(dev, &err);
\ -+ virtio_balloon_device_realize(dev) "实现TYPE_VIRTIO_BALLOON_DEVICE的具现化"
\ -+ virtio_init(vdev, "virtio-balloon", VIRTIO_ID_BALLOON, virtio_balloon_config_size(s));
"virtio_init初始化virtio设备的公共部分,
virtio_init的工作是初始化所有virtio设备的基类TYPE_VIRTIO_DEVICE的实例VirtIODevice结构体"
\ - vdev->vq = g_new0(VirtQueue, VIRTIO_QUEUE_MAX); "表示的是该设备的virtio queue"
| -+ for vq in vdev->vq[VIRTIO_QUEUE_MAX]
\ - vdev->vq[i].vdev = vdev; ... "分配了VIRTIO_QUEUE_MAX个queue并且进行了初始化"
| - vdev->vmstate = qdev_add_vm_change_state_handler(DEVICE(vdev), virtio_vmstate_change, vdev);
| - vdev->queue_sel = 0;
| - vdev->config_len = config_size; "config_len和config表示该virtio设备配置空间的长度和数据存放区域"
| - vdev->config = g_malloc0(config_size);
| - ...
| - s->ivq = virtio_add_queue(vdev, 128, virtio_balloon_handle_output);
| - s->dvq = virtio_add_queue(vdev, 128, virtio_balloon_handle_output);
| - s->svq = virtio_add_queue(vdev, 128, virtio_balloon_receive_stats);
"调用virtio_add_queue函数创建了3个virtqueue, virtqueue是virtio设备的重要组成部分,
用来与虚拟机中的操作系统进行数据传输。virtio_add_queue是virtio框架中用来添加virtqueue的接口,
3个参数分别表示要添加的virtio设备、virtqueue的大小以及处理函数。"
| -+ virtio_bus_device_plugged(vdev, &err); "将virtio设备插到virtio总线上"
\ - DeviceState *qdev = DEVICE(vdev); BusState *qbus = BUS(qdev_get_parent_bus(qdev));
| - VirtioBusState *bus = VIRTIO_BUS(qbus); VirtioBusClass *klass = VIRTIO_BUS_GET_CLASS(bus);
| -+ klass->device_plugged(qbus->parent, &local_err);
\ -+ virtio_pci_device_plugged(qbus->parent)
\ - VirtIOPCIProxy *proxy = VIRTIO_PCI(d);
| - modern = virtio_pci_modern(proxy); "check if modern device ?"
| -+ if modern
\ - pci_set_word(config + PCI_VENDOR_ID, PCI_VENDOR_ID_REDHAT_QUMRANET);
| - pci_set_word(config + PCI_DEVICE_ID, 0x1040 + virtio_bus_get_vdev_id(bus));
"设置vendor id 和 device id, 对于virtio balloon 0x1040+VIRTIO_ID_BALLOON(5)"
| -+ virtio_pci_modern_regions_init(proxy, vdev->name);
"接下来是将virtio设备的寄存器配置信息作为PCIcapability写入到配置空间中
初始化5个MemoryRegion,
virtio-pci-common、virtio-pci-isr、virtio-pci-device、virtio-pci-notify
这些MemoryRegion的相关信息存放在VirtIOPCIProxy结构中的几个VirtIOPCIRegion类型的成员中。"
\ - MemoryRegionOps common_ops = {.read = virtio_pci_common_read,
.write = virtio_pci_common_write, }
| - memory_region_init_io(&proxy->common.mr, OBJECT(proxy), &common_ops,
proxy, name->str, proxy->common.size);
"注册pci代理的common区域的MemoryRegion, 对应的读写回调为common_ops结构,
mmio陷入读写的时候会调用其读写函数 virtio_pci_common_read virtio_pci_common_write"
| - "省略其他三个"
| - virtio_pci_modern_mem_region_map(proxy, &proxy->common, &cap);
| - virtio_pci_modern_mem_region_map(proxy, &proxy->isr, &cap);
| - virtio_pci_modern_mem_region_map(proxy, &proxy->device, &cap);
| -+ virtio_pci_modern_mem_region_map(proxy, &proxy->notify, &notify.cap);
\ -+ virtio_pci_modern_region_map(proxy, region, cap, &proxy->modern_bar,
proxy->modern_mem_bar_idx);
\ - memory_region_add_subregion(mr=&proxy->modern_bar, region->offset, &region->mr);
"将VirtIOPCIRegion的mr成员virtio-pci-***作为子MemoryRegion加入到VirtIOPCIProxy的modern_bar,
虚拟机内部写virtio PCI proxy的MMIO时会落入这几个virtio设备的MemoryRegion的回调函数"
| - virtio_pci_add_mem_cap(proxy, cap);
"将这些寄存器信息加入到virtio PCI代理设备的pci capability上去"
| - proxy->config_cap = virtio_pci_add_mem_cap(proxy, &cfg.cap);
"virtio_pci_add_mem_cap将这些寄存器信息加入到virtio PCI代理设备的pci capability上去"
| - pci_register_bar(&proxy->pci_dev, proxy->modern_mem_bar_idx,
PCI_BASE_ADDRESS_SPACE_MEMORY | PCI_BASE_ADDRESS_MEM_PREFETCH |
PCI_BASE_ADDRESS_MEM_TYPE_64, &proxy->modern_bar);
"pci_register_bar将VirtIOPCIProxy的modern_bar这个MemoryRegion注册到系统中"
| - proxy->pci_dev.config_write = virtio_write_config;
| - proxy->pci_dev.config_read = virtio_read_config;
"将Virt IOPCIProxy设备PCI配置空间的读写函数分别设置成virtio_write_config和virtio_read_config"
| - vdev->listener.commit = virtio_memory_listener_commit;
| - memory_listener_register(&vdev->listener, vdev->dma_as); "注册MemoryRegion 的listener"

virtio_pci_realize还初始化了多个VirtIOPCIRegion,如VirtIOPCIProxy的common、isr、device、notify等成员。
VirtIOPCIRegion保存了VirtIOPCIProxy设备modern MMIO的相关信息,如VirtIOPCIProxy的modern MMIO中:

  • 最开始区域是common区域,其大小为0x1000,
  • 接着是isr区域,大小也是0x1000,
  • 依次类推到notify区域。
    VirtIOPCIRegion用来表示virtio设备的配置空间信息,后面会单独介绍。
    VirtIOPCIProxy的modern MMIO对应的MemoryRegion存放在VirtIOPCIProxy的modern_bar成员中
    virtio_pci_realize会调用virtio_pci_bus_new创建virtio-bus,挂载到当前的virtio PCI代理设备下面。

virtio_add_queue函数从VirtIODevice的vq数组成员中找到还未被使用的一个queue,一个virtqueue使用VirtQueue结构体表示,这里对VirtQueue的成员进行初始化,包括这个queue的大小以及align信息等,最重要的是设置VirtQueue的handle_output成员,这是一个函数指针,在收到虚拟机发过来的IO请求时会调用存放在handle_output中的回调函数, 另外这个函数设置了使用queue的vring.num, 这个设置会导致virtio_memory_listener_commit 内存提交时调用了address_space_cache_init 为vring 映射了内存, 这个地方对应的AddressSpace 为 root address_space_memory, 虚拟机创建时, 已经根据设定的总的内存大小map了这片区域, address_space_cache_init 的工作就是为vring_desc建立GPA->HVA的映射关系.

1
vdev->dma_as = &address_space_memory;

virtio设备挂载到virtio总线上的行为。这个过程是在virtio_device_realize函数中通过调用virtio_bus_device_plugged函数完成的,这个函数的作用就是将virtio设备插入virtio总线上去。virtio_bus_device_plugged主要是调用了VirtioBusClass类型的device_plugged回调函数,而该回调函数在virtio_pci_bus_class_init被初始化成了virtio_pci_device_plugged

pci capability用来表明设备的功能,virtio会把多个MemoryRegion作为VirtIOPCIProxy设备MMIO对应MemoryRegion的子MemoryRegion,这几个MemoryRegion的信息会作为capbility写入到virtioPCI代理这个PCI设备的配置空间, 这些capability的头结构用virtio_pci_cap表示

1
2
3
4
5
6
7
8
9
10
11
struct virtio_pci_cap {
uint8_t cap_vndr; //用来表示capability的id,除了一些标准的capability外,如果是设备自定义的(如这里的virtio设备)
uint8_t cap_next; // cap_next指向下一个capability在PCI配置空间的偏移
uint8_t cap_len; /* Generic PCI field: capability length */
uint8_t cfg_type; /* Identifies the structure. */
uint8_t bar; // bar表示这个capability使用哪个bar
uint8_t id; /* Multiple capabilities of the same type */
uint8_t padding[2]; /* Pad to full dword. */
uint32_t offset; // offset表示这个capability代表的MemoryRegion在virtioPCI代理设备的bar中从哪里开始
uint32_t length; /* Length of the structure, in bytes. */
};

virtio_pci_cap用来描述在virtioPCI代理设备modern MMIO中的一段地址空间。virtio驱动可以通过这些capability信息将对应的地址映射到内核虚拟地址空间中,然后方便地访问。PCI配置空间与cap以及MMIO的关系可以用图表示。

virtio_pci_device_plugged接下来调用virtio_pci_modern_mem_region_map函数,调用virtio_pci_modern_region_map,这个函数完成两个功能:

  • 第一个是将VirtIOPCIRegion的mr成员virtio-pci-***作为子MemoryRegion加入到VirtIOPCIProxy的modern_bar成员中去,所以当在虚拟机内部写virtio PCI proxy的MMIO时会落入这几个virtio设备的MemoryRegion的回调函数;
  • 第二个是调用virtio_pci_add_mem_cap将这些寄存器信息加入到virtio PCI代理设备的pci capability上去。

virtio_pci_device_plugged 函数接着调用 pci_register_bar 将 VirtIOPCIProxy 的 modern_bar 这个 MemoryRegion 注册到系统中。
virtio_pci_device_plugged 还会将 Virt IOPCIProxy 设备 PCI 配置空间的读写函数分别设置成 virtio_write_config 和 virtio_read_config。

virtio-pci 驱动加载

每一个virtio设备都有一个对应的virtio PCI代理设备,本节来分析虚拟机内部操作系统是如何加载virtioPCI代理设备和virtio设备驱动以及如何与virtio设备通信的。
由于virtioPCI代理设备的存在,PCI进行扫描的时候会扫描到这个设备,并且会调用相应驱动的probe函数,virtio_pci_driver及其probe回调函数定义如下

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
-+ virtio_pci_probe(struct pci_dev *pci_dev,)
\ - vp_dev = kzalloc(sizeof(struct virtio_pci_device), GFP_KERNEL); "表示一个virtio PCI代理设备"
| - vp_dev->pci_dev = pci_dev;
| - pci_set_drvdata(pci_dev, vp_dev); "将vp_dev设置为该pci_dev的私有结构"
| - rc = pci_enable_device(pci_dev); "调用pci_enable_device使能该PCI设备"
| -+ virtio_pci_modern_probe(vp_dev);
"调用virtio_pci_legacy_probe或者virtio_pci_modern_probe来初始化该PCI设备对应的virtio设备, 下面只考虑modern的"
\ - struct virtio_pci_modern_device *mdev = &vp_dev->mdev;
| -+ vp_modern_probe(mdev)
\ - struct pci_dev *pci_dev = mdev->pci_dev;
| - mdev->id.device = pci_dev->device - 0x1040; "设置device_id, 就是前面设备具现化时设置的0x1040+VIRTIO_ID_BALLOON(5)"
| - mdev->id.vendor = pci_dev->subsystem_vendor; "设置vendor_id"
| - common = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_COMMON_CFG,
IORESOURCE_IO | IORESOURCE_MEM, &mdev->modern_bars);
"发现virtio PCI代理设备的pci capability, 这也是在virtio_pci_device_plugged写入到virtio PCI代理设备的配置空间中的
virtio_pci_find_capability找到所属的PCI BAR,写入到virtio_pci_device的modern_bars成员中"
| - isr = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_ISR_CFG, IORESOURCE_IO | IORESOURCE_MEM, &mdev->modern_bars);
| - notify = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_NOTIFY_CFG,
IORESOURCE_IO | IORESOURCE_MEM, &mdev->modern_bars);
| - device = virtio_pci_find_capability(pci_dev, VIRTIO_PCI_CAP_DEVICE_CFG,
IORESOURCE_IO | IORESOURCE_MEM, &mdev->modern_bars);
| - pci_request_selected_regions(pci_dev, mdev->modern_bars, "virtio-pci-modern");
"pci_request_selected_regions就将virtio PCI代理设备的BAR地址空间保留起来了"
| - mdev->common = vp_modern_map_capability(mdev, common, sizeof(struct virtio_pci_common_cfg), 4,
0, sizeof(struct virtio_pci_common_cfg), NULL, NULL);
"映射了virtio_pci_common_cfg的数据到内核中,这样,后续就可以直接通过 mdev->common 内存地址空间来访问common这个capability了"
| - mdev->isr = vp_modern_map_capability(mdev, isr, sizeof(u8), 1, 0, 1, NULL, NULL);
| - mdev->notify_base = vp_modern_map_capability(mdev, notify, 2, 2, 0, notify_length, &mdev->notify_len, &mdev->notify_pa);
| - mdev->device = vp_modern_map_capability(mdev, device, 0, 4, 0, PAGE_SIZE, &mdev->device_len, NULL);
"调用 vp_modern_map_capability 将对应的capability在PCI代理设备中的BAR空间映射到内核地址空间"
"实际上就将virtio PCI代理设备的BAR映射到虚拟机内核地址空间了,后续直接访问这些地址即可实现对virtio PCI代理设备的配置和控制。"
| - vp_dev->vdev.config = &virtio_pci_config_ops;
"如果有device这个capability,则设置为virtio_pci_config_ops, 设置回调函数 vp_get vp_set 等"
| - vp_dev->config_vector = vp_config_vector; "设置virtio_pci_device的几个回调函数, vp_config_vector 与中断有关"
| - vp_dev->setup_vq = setup_vq; "setup_vq用来配置virtio设备virt queue, del_vq用来删除virt queue"
| - vp_dev->del_vq = del_vq;
| - pci_set_master(pci_dev);
| - rc = register_virtio_device(&vp_dev->vdev); "将一个virtio device注册到系统中"

register_virtio_device函数的作用前面在介绍virtio-mmio子总线上的设备时已经说过, 在balloon的驱动初始化时, 最终调用到Bus的probe函数和设备的probe函数,也就是virtio_dev_probe和virtballoon_probe函数。

virtio pci 设备驱动的初始化

在介绍virtio设备驱动的初始化之前,首先介绍virtio配置的函数集合变量virtio_pci_config_ops。virtio_pci_modern_probe函数中virtio_pci_config_ops变量被赋值给了virtio_device结构体的config成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static const struct virtio_config_ops virtio_pci_config_ops = {
.get = vp_get,
.set = vp_set,
.get_status = vp_get_status,
.set_status = vp_set_status,
.reset = vp_reset,
.find_vqs = vp_find_vqs,
.del_vqs = vp_del_vqs,
.synchronize_cbs = vp_synchronize_vectors,
.get_features = vp_get_features,
.finalize_features = vp_finalize_features,
.bus_name = vp_bus_name,
.set_vq_affinity = vp_set_vq_affinity,
.get_vq_affinity = vp_get_vq_affinity,
};

virtio_pci_config_ops结构体中的成员函数通常是代理virtioPCI代理设备的I/O操作,包括读写virtio PCI代理设备的PIO和MMIO,如get_status和set_status成员对应的vp_get_statusvp_set_status函数

1
2
3
4
5
6
7
8
9
10
static u8 vp_get_status(struct virtio_device *vdev)
{
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
return vp_modern_get_status(&vp_dev->mdev);
}
u8 vp_modern_get_status(struct virtio_pci_modern_device *mdev)
{
struct virtio_pci_common_cfg __iomem *cfg = mdev->common;
return vp_ioread8(&cfg->device_status);
}

可见这两个函数读取地址是vp_dev->common->device_status, vp_dev->common对应的是virtio PCI代理设备第四个BAR表示的地址中的一段空间,其指向的数据表示如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct virtio_pci_common_cfg {
/* About the whole device. */
__le32 device_feature_select; /* read-write */
__le32 device_feature; /* read-only */
__le32 guest_feature_select; /* read-write */
__le32 guest_feature; /* read-write */
__le16 msix_config; /* read-write */
__le16 num_queues; /* read-only */
__u8 device_status; /* read-write */ // offset 20字节处
__u8 config_generation; /* read-only */

/* About a specific virtqueue. */
__le16 queue_select; /* read-write */
__le16 queue_size; /* read-write, power of 2. */
__le16 queue_msix_vector; /* read-write */
__le16 queue_enable; /* read-write */
__le16 queue_notify_off; /* read-only */
__le32 queue_desc_lo; /* read-write */
__le32 queue_desc_hi; /* read-write */
__le32 queue_avail_lo; /* read-write */
__le32 queue_avail_hi; /* read-write */
__le32 queue_used_lo; /* read-write */
__le32 queue_used_hi; /* read-write */
};

上面代码中的每一个成员都表示一个virtio PCI代理设备modern MMIO地址空间中对应的值,读写这些成员都会陷入到QEMU中
比如设置或者获取设备状态的device_status成员,其地址从该结构体开始偏移20处,所以读写这个地址的时候会陷入到QEMU中,并且地址是virtio设备的common MemoryRegion偏移20字节处,该MemoryRegion对应的回调操作结构是common_ops, 对用qemu的这段代码:

1
2
3
4
5
6
7
8
9
10
static const MemoryRegionOps common_ops = {
.read = virtio_pci_common_read,
.write = virtio_pci_common_write,
.impl = {
.min_access_size = 1,
.max_access_size = 4,
},
.endianness = DEVICE_LITTLE_ENDIAN,
};
memory_region_init_io(&proxy->common.mr, OBJECT(proxy), &common_ops, proxy, name->str, proxy->common.size);

下面以virtio balloon设备的初始化过程为例分析virtio设备的初始化过程

virtio balloon设备的初始化

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
-+ virtballoon_probe(struct virtio_device *vdev)
\ - virtio_balloon *vb; "virtio balloon设备用virtio_ballon表示"
| - vdev->priv = vb = kzalloc(sizeof(*vb), GFP_KERNEL); "virtio_device的priv也会保存 virtio_balloon 地址"
| -+ init_vqs(vb); "初始化virtqueue和vring"
\ - vq_callback_t *callbacks[VIRTIO_BALLOON_VQ_MAX];
| - callbacks[VIRTIO_BALLOON_VQ_STATS] = stats_request; "判断VIRTIO_BALLOON_F_STATS_VQ特性是否存在, 这个是存在的"
| - callbacks[VIRTIO_BALLOON_VQ_REPORTING] = balloon_ack; "判断 VIRTIO_BALLOON_F_REPORTING 特性是否存在, 存在的话赋值"
| - callbacks[VIRTIO_BALLOON_VQ_INFLATE] = balloon_ack;
| - callbacks[VIRTIO_BALLOON_VQ_DEFLATE] = balloon_ack;
| -+ virtio_find_vqs(vb->vdev, VIRTIO_BALLOON_VQ_MAX, vqs, callbacks, names, NULL)
\ -+ vdev->config->find_vqs(vdev, nvqs, vqs, callbacks, names, NULL, desc)
\ -+ vp_modern_find_vqs(vdev, nvqs, vqs, callbacks, names, NULL, desc)
\ -+ vp_find_vqs(vdev, nvqs, vqs, callbacks, names, ctx, desc)
\ -| vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, true, ctx, desc);
| -| vp_find_vqs_msix(vdev, nvqs, vqs, callbacks, names, false, ctx, desc);
| -+ vp_find_vqs_intx(vdev, nvqs, vqs, callbacks, names, ctx) "每个vq 一个vector"
\ - vp_dev->vqs = kcalloc(nvqs, sizeof(*vp_dev->vqs), GFP_KERNEL)
| - request_irq(vp_dev->pci_dev->irq, vp_interrupt, IRQF_SHARED,
dev_name(&vdev->dev), vp_dev) "申请中断, 中断处理函数为vp_interrupt"
| -+ qs[i] = vp_setup_vq(vdev, queue_idx++, callbacks[i], names[i], ctx ? ctx[i] : false, VIRTIO_MSI_NO_VECTOR);
"对每一个virtqueue调用vp_setup_vq来初始化virtqueue"
\ -+ vq = vp_dev->setup_vq(vp_dev, info, index, callback, name, ctx, msix_vec);
"调用virtio_pci_device的setup_vq回调函数"
\ -+ vp_modern_get_num_queues(mdev)
\ - vp_ioread16(&mdev->common->num_queues) "读写这些地址会导致陷入到QEMU中的virtio_pci_common_read/write函数"
| - num = vp_modern_get_queue_size(mdev, index)
| -+ !num | vp_modern_get_queue_enable(mdev, index)
\ - vp_iowrite16(index, &mdev->common->queue_select);
"queue_select 置为index, 对应的寄存器地址为VIRTIO_PCI_COMMON_Q_SELECT"
"读写这些地址会导致陷入到QEMU中的virtio_pci_common_read/write函数"
| - vp_ioread16(&mdev->common->queue_enable);
| -+ vq = vring_create_virtqueue(index, num, SMP_CACHE_BYTES, &vp_dev->vdev, true, true, ctx,
vp_notify, callback, name);
\ -+ vring_create_virtqueue_split(index, num, vring_align, vdev, weak_barriers, may_reduce_num,
context, notify, callback, name)
\ - queue = vring_alloc_queue(vdev, vring_size(num, vring_align), &dma_addr,
GFP_KERNEL | __GFP_NOWARN | __GFP_ZERO)
"分配空间大小由vring_size(num, vring_align)计算得来"
"分配virtqueue的页面, 分配virtqueue的页面,本质上就是分配vring的descriptor table、available ring
和used ring 3个部分,这个3个部分是在连续的物理地址空间中,“queue”保存了分配空间的虚拟地址。"
| -+ vring_init(&vring, num, queue, vring_align);
\ - vr->num = num; "vr = vring"
| - vr->desc = queue;
| - vr->avail = (struct vring_avail *)((char *)queue + num * sizeof(struct vring_desc));
| - vr->used = (void *)(((&vr->avail->ring[num] + sizeof(__virtio16) + align-1) & ~(align - 1));
"设置vring中队列大小(vring->num)、descriptor table(vring->desc)
avail ring(vring->avail)和used ring(vring->used)的地址。"
| -+ vq = __vring_new_virtqueue(index, vring, vdev, weak_barriers, context, notify, callback, name);
"创建一个vring_virtqueue结构, notify 表示virtio驱动用来通知virtio设备的函数,
callback表示virtio设备使用了descriptor table之后virtio驱动会调用的函数
还多分配了num个void*指针,这是用来在调用使用通知时传递的所谓token"
\ - list_add_tail(&vq->vq.list, &vdev->vqs);
"把virtqueue挂到virtio_device的vqs链表上,这样就可以通过virtio_device快速找到所有队列"
| -+ vp_modern_set_queue_size(mdev, index, virtqueue_get_vring_size(vq));
\ - vp_iowrite16(index, &mdev->common->queue_select);
| - vp_iowrite16(size, &mdev->common->queue_size);
| - vp_modern_queue_address(mdev, index,
virtqueue_get_desc_addr(vq), virtqueue_get_avail_addr(vq), virtqueue_get_used_addr(vq));
"这个步骤会把队列大小,队列的descriptor table、avail ring和used ring的物理地址写入到相应的寄存器中"
| -+ vq->priv = (void __force *)vp_modern_map_vq_notify(mdev, index, NULL);
\ - return mdev->notify_base + index * mdev->notify_offset_multiplier "这个值为4"
"notify操作的地址为 notify_base +virt_queue的index * 4"
\ ---+ vp_notify(vq)
\ - iowrite16(vq->index, (void __iomem *)vq->priv);
"virtio驱动调用vp_notify通知qemu的virtio设备"
\ ---+ virtio_pci_notify_write(void *opaque, hwaddr addr, uint64_t val, unsigned size) "QEMU"
\ - queue_index = addr / virtio_pci_queue_mem_mult(proxy);
| - virtio_queue_notify(vdev, queue_index);
| -+ vp_modern_set_queue_enable(&vp_dev->mdev, [for vq in vdev->vqs]->index, true);
"激活队列"
\ - vp_iowrite16(index, &mdev->common->queue_select);
| - vp_iowrite16(enable, &mdev->common->queue_enable);
| - vb->inflate_vq = vqs[VIRTIO_BALLOON_VQ_INFLATE];
| - vb->deflate_vq = vqs[VIRTIO_BALLOON_VQ_DEFLATE];
| - vb->reporting_vq = vqs[VIRTIO_BALLOON_VQ_REPORTING];
| - vb->free_page_vq = vqs[VIRTIO_BALLOON_VQ_FREE_PAGE];
| - virtio_device_ready(vdev); "设置一个DRIVER_OK的特性位"

virtio设备与驱动的通信

virtqueue是virtio驱动与virtio设备进行通信的方式, 先介绍virtqueue以及vring的相关概念
每个virtio设备可能会有一个或多个virtqueue,如virtio balloon有3个virtqueue,单队列的网卡有两个virtqueue。
每个virtqueue包括三个部分,即descriptor table、available ring以及used ring

  • descriptor table中的每一项用来描述一段缓冲区(Buffer),包括缓冲区的物理地址(GPA)和长度,descriptor table的项数表示virtqueue的大小
  • available ring中每一项的值表示当前可用descriptor table中的index,由虚拟机内部virtio驱动设置,由QEMU侧的virtio设备读取
  • used ring中每一项的值表示已经使用过的descriptor table中的index,由virtio设备设置,virtio驱动读取
vring
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
struct vring {
unsigned int num; //这个vring的vring_desc的个数
// descriptor table、available ring、used ring的起始地址
vring_desc_t *desc;
vring_avail_t *avail;
vring_used_t *used;
};
struct vring_desc {
__virtio64 addr; //GPA
__virtio32 len;
__virtio16 flags; // VRING_DESC_F_NEXT,表示这段I/O包括下一个连续的vring_desc;VRING_DESC_F_WRITE表示这段是只写的(对设备而言);VRING_DESC_F_INDIRECT表示这段I/O由不连续的vring_desc构成
__virtio16 next; //链表 next
};
struct vring_avail {
__virtio16 flags; //一般不用
__virtio16 idx; //idx表示下一次virtio驱动应该写ring数组的哪一个
__virtio16 ring[]; //ring表示一个索引数组,其大小为可用virtqueue的大小,其中存放的是vring_desc的索引
};
struct vring_used {
__virtio16 flags; //一般不用
__virtio16 idx;
vring_used_elem_t ring[]; //ring表示一个索引数组,其大小为已经用的virtqueue的大小
};
struct vring_used_elem {
__virtio32 id; //表示使用了descriptor table中的索引
__virtio32 len; //用这个vring_desc时, virtio设备总共写了多少数据
};

注意,在vring_avail最后有一个used_event,在vring_used最后有一个avail_event,used_event和avail_event都占两个字节,这两个字段与virtio设备的VIRTIO_RING_F_EVENT_IDX特性有关

如果VIRTIO_RING_F_EVENT_IDX开启

  • virtio驱动可以抑制virtio设备发送中断的次数
    • 当设备向used ring写入一个descriptor index的时候,写入的uesd ring的索引到达或者超过used_event时才发送中断,由于vring_used->idx表示的是下一次要用的used ring索引,所以当vring_used->idx>=used_event+1的时候发送中断;
  • virtio设备可以抑制virtio驱动发送通知的次数
    • 当驱动向available ring写入一个descriptor index的时候,写入的avail ring的索引到达或者超过avail_event时才通知QEMU侧的virtio设备,由于vring_avail->idx表示的是下一次要使用的avail ring的索引,所以当vring_avail->idx>=avail_event+1的时候才发送通知。

vring的3个部分被放在连续的两个或多个页上,整个vring的大小计算是通过vring_size完成的

1
2
3
4
5
6
static inline unsigned vring_size(unsigned int num, unsigned long align)
{
return ((sizeof(struct vring_desc) * num + sizeof(__virtio16) * (3 + num)
+ align - 1) & ~(align - 1))
+ sizeof(__virtio16) * 3 + sizeof(struct vring_used_elem) * num;
}

vring_size的参数num是virtqueue的大小,也就是vring_desc的大小,align表示对齐,virtio驱动初始化调用的setup_vq函数中会调用vring_create_virtqueue_split分配vring的空间,其大小通过vring_pci_size决定,在其中可以看到align为SMP_CACHE_BYTES(64)。vring的第一部分是num个vring_desc;第二部分是(3+num)个双字节,num是vring_avail中的ring数组所占的空间,3表示的是vring_avail中的flags、idx成员以及used_event这3个双字节,这两个部分是紧挨着放的;接着是padding的长度,最后在下一个页对齐地址计算used ring所占的空间。3个双字节表示的vring_used的flags、idx成员以及avail_event。

virtio驱动向virtio设备传递数据的步骤如下

  1. 填充一个或多个descriptor table中的一项
  2. 更新available ring的数据,使得idx指向available ring的ring数组中刚刚添加的descriptor table的位置
  3. 发送通知给virtio设备

virtio设备处理virtio驱动的数据请求过程如下

  1. 根据available ring中的信息,把请求数据从descriptor table中取下来
  2. 处理具体的请求
  3. 更新used ring的数据,使得idx指向used ring的ring数组中刚刚添加的descriptor table的信息以及长度。
  4. 发送一个中断给virtio驱动

在分析具体的vring通信机制前,需要介绍两个结构,第一个是virtio驱动中的vring_virtqueue,第二个是QEMU中的VirtQueue,两者分别是virto驱动层和设备层的virtqueue表示,在上述virtio驱动和设备进行数据交互的过程中有重要作用

vring_virtqueue
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
struct vring_virtqueue {
struct virtqueue vq;

/* Is this a packed ring? */
bool packed_ring;

/* Is DMA API used? */
bool use_dma_api;

/* Can we use weak barriers? */
bool weak_barriers;
bool broken; //表示对端(virtio设备端)已经不是正常状态
bool indirect; //示对端支持间接descriptor
bool event; //event表示对端是否使用了avail event index特性
unsigned int free_head; //表示descriptor table第一个可用项
unsigned int num_added; //表示上一次通知对端之后增加的avaial ring的个数
u16 last_used_idx; //表示virtio驱动最后看到的used index,这个值记录驱动这一层的used ring的index

/* Hint for event idx: already triggered no need to disable. */
bool event_triggered;

union {
/* Available for split ring */
struct {
/* Actual memory layout for this queue. */
struct vring vring;
u16 avail_flags_shadow; //最后一次写入到avail->flags中的值
u16 avail_idx_shadow; //最后一次写入avail->idx的值

/* Per-descriptor state. */
struct vring_desc_state_split *desc_state;
struct vring_desc_extra *desc_extra;

/* DMA address and size information */
dma_addr_t queue_dma_addr;
size_t queue_size_in_bytes;
} split;
};
bool (*notify)(struct virtqueue *vq); //通知对端的回调函数
/* DMA, allocation, and size information */
bool we_own_ring;
};

向descriptor table添加请求数据的函数是virtqueue_add

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
-+ int virtqueue_add(struct virtqueue *_vq, "表示要添加数据的virtqueue"
struct scatterlist *sgs[], "数据请求放在一组sgs的scatterlist数组中"
unsigned int total_sg, "total_sg表示该数组的大小"
unsigned int out_sgs, "out_sgs表示驱动写入的数据 占的sg的个数"
unsigned int in_sgs, "in_sgs表示驱动提供给设备的空间,即设备写这个空间 占的sg的个数"
void *data, "data就是本次请求的上下文,通常用来存放具体的virtio设备,如virtio_balloon结构"
void *ctx,
gfp_t gfp) "指示分配空间的标志"
\ -+ virtqueue_add_split(_vq, sgs, total_sg, out_sgs, in_sgs, data, ctx, gfp)
\ - struct vring_virtqueue *vq = to_vvq(_vq);
| -|+ if vq->broken "检查vq 是否损坏"
<- \ - return -EIO;
| - head = vq->free_head; "获取可以使用的descriptor index保存在head中"
| - desc = vq->split.vring.desc; i = head; "desc保存descriptor table首地址,i表示当前可用的vring_desc"
| - descs_used = total_sg; "表示将要使用的descriptor个数"
| -|+ if vq->vq.num_free < descs_used "要使用的descs_used要比当前空闲的descriptor的数目小才行"
<- \ - return -ENOSPC;
| -+ for sg in sgs[total out_sgs]
"每个out_sgs里的sg 构造一个 vring_desc vring_desc的next是指向下一个的索引,所以这里的vring_desc都是紧挨着的"
\ - dma_addr_t addr = vring_map_one_sg(vq, sg, DMA_TO_DEVICE);
| - prev = i;
| - i = virtqueue_add_desc_split(_vq, desc, i, addr, sg->length, VRING_DESC_F_NEXT, indirect);
| -+ for sg in sgs[total in_sgs]
"紧挨着out_sg, 每个in_sgs 里的sg 构造一个vring_desc"
\ - dma_addr_t addr = vring_map_one_sg(vq, sg, DMA_FROM_DEVICE);
| - prev = i;
| - i = virtqueue_add_desc_split(_vq, desc, i, addr, sg->length, VRING_DESC_F_NEXT | VRING_DESC_F_WRITE, indirect);
"多加了一个 VRING_DESC_F_WRITE flag, 对端看到就知道开始了in数据"
| - desc[prev].flags &= cpu_to_virtio16(_vq->vdev, ~VRING_DESC_F_NEXT);
"对于最后一个vring_desc还需要清除其VRING_DESC_F_NEXT,这样对端设备才会知道当前的数据请求vring_desc已结束"
| - vq->vq.num_free -= descs_used; "更新空闲的descriptor的数目"
| - vq->free_head = i; "更新可以使用的descriptor index"
| - avail = vq->split.avail_idx_shadow & (vq->split.vring.num - 1);
"更新available ring了。需要更新的avail ring数组项索引是从vq->avail_idx_shadow中获取的
vq->avail_idx_shadow保存下一次需要使用的avail ring的index,每添加一次数据,vq->avail_idx_shadow就会递增1"
| - vq->split.vring.avail->ring[avail] = cpu_to_virtio16(_vq->vdev, head);
"就将本次请求的第一个vring_desc的索引写入到对应的avail->ring[avail]中去"
| - vq->split.avail_idx_shadow++;
| - vq->split.vring.avail->idx = cpu_to_virtio16(_vq->vdev, vq->split.avail_idx_shadow);
| - vq->num_added++;

假设添加了三个vring_desc

驱动在添加请求数据到virtqueue之后,会调用virtqueue_kick函数来通知对端设备
在调用通知之前调用virtqueue_kick_prepare来判断是否需要通知,如果需要通知就调用virtqueue_notify函数,virtqueue_notify调用vring_virtqueue结构中的notify回调函数,也就是vp_notify向对端发送一个I/O请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-+ virtqueue_kick(struct virtqueue *vq)
\ -|+ if virtqueue_kick_prepare(vq)
\ -+ virtqueue_kick_prepare(vq)
\ -+ virtqueue_kick_prepare_split(_vq);
\ - old = vq->split.avail_idx_shadow - vq->num_added; "上一次的avail index为old"
| - new = vq->split.avail_idx_shadow; "本次新增了new-old项available ring"
| -|+ if vq->event
\ -+ needs_kick = vring_need_event(virtio16_to_cpu(_vq->vdev, vring_avail_event(&vq->split.vring)), new, old);
\ - return (__u16)(new_idx - event_idx - 1) < (__u16)(new_idx - old);
"本次新增了new-old项available ring,只有当event设置成old和new-1(包括)之间的值时,vring_need_event才会返回true,否则返回false"
"event_idx (vring_avail_event(&vq->split.vring))) 在old和new-1之间,返回true"
| -|+ else if !(vq->event) "不使用event index特性"
\ - needs_kick = !(vq->split.vring.used->flags & cpu_to_virtio16(_vq->vdev, VRING_USED_F_NO_NOTIFY));
"通过vq->vring.used->flags标志判断是否需要通知对端。
如果该标志设置了VRING_USED_F_NO_NOTIFY,则needs_kick为false,不通知对端,否则通知"
| - return virtqueue_notify(vq); "通知qemu 设备端"
设备端 QEMU中表示virtqueue的数据结构VirtQueue

接下来分析virtio设备层的数据结构与中断通知。首先介绍QEMU中表示virtqueue的数据结构VirtQueue。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
struct VirtQueue
{
VRing vring; //表示该队列对应的VRing
VirtQueueElement *used_elems;
uint16_t last_avail_idx; //下一个要从avail ring中取数据的索引
bool last_avail_wrap_counter;
uint16_t shadow_avail_idx; //表示最近一次从avail ring中读取的index
bool shadow_avail_wrap_counter;
uint16_t used_idx; //本次要使用的used ring的index
bool used_wrap_counter;
uint16_t signalled_used; //上一次通知驱动侧时的used vring index
bool signalled_used_valid; //表示signalled_used是否有效
bool notification; //是否需要进行通知驱动端
uint16_t queue_index; //表示队列的索引
unsigned int inuse; //表示队列中正在处理的请求个数
uint16_t vector; //中断向量
VirtIOHandleOutput handle_output; //具体的virtio设备提供的用来处理驱动端请求的函数
VirtIODevice *vdev; //vdev指向对应的virtio设备
EventNotifier guest_notifier;
EventNotifier host_notifier; //guest_notifier和host_notifier通常和irqfd和ioeventfd一起使用
bool host_notifier_enabled;
QLIST_ENTRY(VirtQueue) node; //链表node将同一个设备的VirtQueue链接起来
};

在上一节已经介绍到virtio驱动发送MMIO请求给notify MemoeryRegion,QEMU这边会调用到对应的函数virtio_queue_notify,在该函数中会调用VirtQueue中的handle_aio_output或者handle_output函数
handle_output从virt queue的out vring中取下数据,处理并更新used ring,发送中断通知驱动,所以virtio设备实现的handle_output都会有一个固定的模式
下面通过virtio_balloon_handle_output来讲解这几个函数

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
-+ virtio_balloon_handle_output(VirtIODevice *vdev, VirtQueue *vq)
\ -+ while true
\ -+ VirtQueueElement *elem = virtqueue_pop(vq, sizeof(VirtQueueElement));
"第一个参数是VirtQueue,表示相应的队列,第二个参数是一个长度,用来记录从VirtQueue上获取的数据"
"virtio框架提供了一个标准的结构VirtQueueElement, 但是如果设备有特殊需求也可以在VirtQueueElement的基础上扩展这个结构。
需要注意的是自定义的扩展结构需要将VirtQueueElement放在设备自定义结构的第一个成员"
\ -|+ if virtio_device_disabled(vq->vdev)
\ - vdev->disabled || vdev->broken "判断设备是否损坏, 是否禁用"
<- | - return NULL
| -+ virtqueue_split_pop(vq, sz);
\ -|+ if virtio_queue_empty_rcu(vq) "virtqueue上面是不是有数据"
<- \ - done
| -|+ if (vq->inuse >= vq->vring.num) "请求数据超过了vring大小"
<- \ - done
| - virtqueue_get_head(vq, vq->last_avail_idx++, &head) "调用virtqueue_get_head函数获取当前使用的descriptor ring的索引,
该索引是通过读取avail ring数组的第last_avail_idx项获得的,用head保存"
| - i = head;
| - vring_set_avail_event(vq, vq->last_avail_idx); "直接把下一次要读取的avail ring index设置成了event index的值,
所以如果virtio本身不操作event index,
那么virtio驱动在下一次填写avail ring的时候必然就会写入avail ring[last_avail_idx]中,
进而会通知后端设备。"
| -+ caches = vring_get_region_caches(vq);
\ - vq->vring.caches
| - desc_cache = &caches->desc;
| - vring_split_desc_read(vdev, &desc, desc_cache, i); "读取descriptor的时候首先调用vring_desc_read读取第一个descriptor"
| -|+ do while
"循环处理desc 链表"
\ -|+ if (desc.flags & VRING_DESC_F_WRITE)
\ - map_ok = virtqueue_map_desc(vdev, &in_num, addr + out_num, iov + out_num,
VIRTQUEUE_MAX_SIZE - out_num, true, desc.addr, desc.len);
"在循环中会将从descriptor table中读取的由VRingDesc表示的虚拟机物理地址映射到QEMU进程的虚拟地址,
并且把该虚拟地址和长度保存I/O vector中,映射的过程是通过virtqueue_map_desc函数完成的"
"iov也是按照VRingDesc的组织来赋值的,即所有的out数据放在iov数组前面,in放在后面,out_num表示out VRingDesc的个数,
in_num表示in VRingDesc的个数。virtqueue_map_desc还会把每个VRingDesc表示的虚拟机物理地址保存在addr数组中"
| -|+ else if !(desc.flags & VRING_DESC_F_WRITE)
\ - map_ok = virtqueue_map_desc(vdev, &out_num, addr, iov, VIRTQUEUE_MAX_SIZE, false, desc.addr, desc.len);
| - rc = virtqueue_split_read_next_desc(vdev, &desc, desc_cache, max, &i);
"在映射完当前读取的VRingDesc之后就会继续调用virtqueue_read_next_desc读取该VRingDesc之后的下一个VRingDesc"
"virtqueue_read_next_desc函数会检查当前的VRingDesc的flags是否有VRING_DESC_F_NEXT标志,
如果有就会用VRingDesc->next作为索引去读取下一个VRingDesc,然后做映射。
最后一个VRingDesc没有设置VRING_DESC_F_NEXT,所以该循环会在读取完所有的VRingDesc之后结束"
<-| end while "virtqueue_pop至此已经获取了所有I/O请求数据的信息"
| - elem = virtqueue_alloc_element(sz, out_num, in_num); "分配VirtQueueElement数据结构,将这些I/O信息组合起来"
"virtqueue_pop分配好了VirtQueueElement以及后面存放数据的空间之后,紧接着就把addr数组和iov数组中存放的数据赋值到了刚刚分配的空间中"
| -+ for i in out_num
\ - elem->out_addr[i] = addr[i];
| - elem->out_sg[i] = iov[i];
| -+ for i in in_num
\ - elem->in_addr[i] = addr[out_num + i];
| - elem->in_sg[i] = iov[out_num + i];
| - vq->inuse++; "限制设备侧处理驱动请求的速率"
| - ... "取出所有的I/O请求数据之后,virtio设备会对这些数据进行处理,每个设备有自己的数据格式"
| -+ virtqueue_push(vq, elem, 0); "处理完数据之后,会调用virtqueue_push将相关处理的信息反馈到虚拟机中的virtio驱动"
"第二个参数是在virtqueue_pop中分配的VirtQueueElement,len表示的是本次virtio设备侧消耗了多少数据"
\ -+ virtqueue_fill(vq, elem, len, 0); "构造一个VRingUsedElem,并将该数据写入到used ring表示的数组中"
\ -+ virtqueue_split_fill(vq, elem, len, idx); "VRingUsedElem 和 virtio驱动中的avail 每个 ring 元素 vring_used_elem 结构体一致"
\ - idx = (idx + vq->used_idx) % vq->vring.num;
| - uelem.id = elem->index;
| - uelem.len = len;
| - vring_used_write(vq, &uelem, idx); "这个地方需要刷qemu va的cache, 保证GPA 数据写入了"
| -+ virtqueue_flush(vq, 1); "更新used ring的idx成员为最新"
\ -+ virtqueue_split_flush(VirtQueue *vq, count=1)
\ - old = vq->used_idx;
| - new = old + count;
| - vring_used_idx_set(vq, new);
| -+ virtio_notify(vdev, vq);
\ -+ virtio_irq(VirtQueue *vq)
\ -+ if virtio_should_notify(vdev, vq) "判断是否需要通知对端驱动,这个判断过程与驱动端过程类似"
\ - virtio_set_isr(vq->vdev, 0x1);
| -+ virtio_notify_vector(vq->vdev, vq->vector);
"最终会调用到VirtioBusClass的notify成员函数. VirtioBusClass的notify成员函数,
它在virtio_pci_bus_class_init被设置成了virtio_pci_notify"
\ -+ k->notify(qbus->parent, vector);
\ -+ virtio_pci_notify(DeviceState* d, vector)
\ - VirtIODevice *vdev = virtio_bus_get_device(&proxy->bus);
| - pci_set_irq(&proxy->pci_dev, qatomic_read(&vdev->isr) & 1);
"最终调用 sifive_plic 中断控制器 通过kvm 向guest 发送中断, 使用的是 pcie_irqchip = s->irqchip[i];"

VirtQueueElement结构中的成员是对称的,保存了in和out的个数、虚拟机物理地址以及映射过来的I/O vector,加上VirtQueueElement本身用来保存元数据所需的大小,VirtQueueElement总共会包括5个部分的数据信息

虚拟机收到数据返回中断

虚拟机接收到中断之后,再来分析一下虚拟机virtio驱动的代码路径。首先,virtio驱动在初始化时调用的vp_request_intx函数里面会调用request_irq申请中断资源,并把中断函数设置为vp_interrupt。所以当virtio设备发送中断时,虚拟机中会接收到这个中断并调用vp_interrupt函数

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
-+ vp_interrupt(int irq, void *opaque)
\ - isr = ioread8(vp_dev->isr);
| -+ vp_vring_interrupt(irq, opaque);
\ -+ for vq in vdev->virtqueues "设备的virtqueues 共享中断, 所以这个地方需要遍历"
\ -+ vring_interrupt(irq, info->vq)
\ -|+ if !(more_used(vq)) "判断该队列的used ring的idx是否在上一次的基础上增加了,如果没有,说明中断不是当前virtqueue触发的"
<-\ - return IRQ_NONE;
| -+ vq->vq.callback(&vq->vq); "调用virtio具体设备驱动的callback函数, 是在 vring_create_virtqueue 阶段注册的 callback"
"对于balloon 设备, callback 有两个 balloon_ack 和 stats_request "
\ -+ balloon_ack(vq)
\ -+ wake_up(&vb->acked);
-----------------------------------guest os kernel 线程 相当于进程---------------------------
... wait_event(vb->acked, virtqueue_get_buf(vq, &len)); ... "wakeup vb->acked 导致进程被唤醒"
\ -+ virtqueue_get_buf(vq, &len) "回收virtqueue_add使用的descriptor table中的项"
\ -+ virtqueue_get_buf_ctx(_vq, len, NULL);
\ -+ virtqueue_get_buf_ctx_split(_vq, len, ctx)
\ - last_used = (vq->last_used_idx & (vq->split.vring.num - 1));
"vq->last_used_idx 保存了本次要使用的used ring的index"
| - i = virtio32_to_cpu(_vq->vdev, vq->split.vring.used->ring[last_used].id);
| - *len = virtio32_to_cpu(_vq->vdev, vq->split.vring.used->ring[last_used].len);
"从used ring中取出记录对端设备使用的vringdesc的索引i和长度len"
| - detach_buf_split(vq, i, ctx);
"将vringdesc从i开始的几个vringdesc回收起来,
这里的回收包括增加vring_virtqueue成员virtqueue的num_free成员以及修改vring_virtqueue的free_head成员"
| - vq->last_used_idx++;
| - ... "省略设置 avail event index 相关"

virtqueue_get_buf函数最后会根据设备是否设置了VRING_AVAIL_F_NO_INTERRUPT来决定是否设置avail event index为下一个last_used_index。
如果没有设置VRING_AVAIL_F_NO_INTERRUPT,那么每一次设备消耗数据之后都会发送中断,这一点跟驱动通知设备类似,
如果设备有VIRTIO_RING_F_EVENT_IDX特性,那么设备端会设置used event index,每一次驱动写vringdesc都会发送通知到设备。

从上面virtio驱动和virtio设备使用vringdesc的分析可以看到,驱动中的virtqueue->avail_idx_shadow和设备中的VirtQueue->last_avail_idx保存了avail ring中下一个使用的索引,驱动侧virtqueue->last_used_idx和设备侧VirtQueue->used_idx则保存了used ring中下一个使用的索引,驱动侧和设备侧各自保存自己的两个vring的索引值,实现同步。

设备模拟后端

虚拟机中的大部分设备都是需要与外界通信的,比如需要访问真实的网络、读取虚拟机的磁盘数据等,虚拟机在模拟这种设备的过程中需要访问到实际的物理设备。虚拟机通过两个部分完成这类模拟

  1. 前端设备: 前端主要负责与虚拟机打交道,管理虚拟机的I/O, 比如虚拟网卡
  2. 后端设备: 逻辑上属于宿主机, 后端主要与宿主机打交道,将虚拟机的I/O请求转换为宿主机上的I/O请求, 比如网卡对应的tap 后端设备

在各种设备模拟技术中,网络设备是发展最快的,本章随后的章节将介绍网络设备从传统模拟到virtio,再到vhost、vhost-user的发展过程。本节主要介绍全虚拟化下的网卡模拟

网卡模拟介绍

tap设备的使用

首先介绍一下QEMU使用tap作为网络后端的方法。
第一步要创建一个tap设备tap0,并且把它加到网桥br0中,同时将主机的一个物理网卡加到br0

1
2
3
4
5
6
7
8
sudo apt-get install bridge-utils
sudo brctl addbr br0
sudo brctl addif br0 ens33 "将ens33设备加入到网桥"
sudo ifconfig ens33
sudo dhclient br0
sudo ip tuntap add dev tap0 mode tap
sudo brctl addif br0 tap0 "将tap设备加入到网桥"
sudo ip link set dev tap0 up "将tap 设备激活"

启动虚拟机

1
-net nic,model=virtio, netdev=foo -netdev tap, ifname=tap0, script=no, downscript=no, id=foo

nic表示的是创建前端虚拟机网卡
model表示创建的网卡类型(如e1000、rtl8139、pcnet、virtio等)
netdev表示的是其所使用的后端设备的id
-netdev定义一个网卡后端设备,这里tap表示使用tap设备作为后端
ifname表示tap设备的名字
script表示虚拟机在打开tap网卡的时候需要执行的脚本;downscript表示虚拟机在关闭的时候需要执行的脚本,这里都设置为no,表示不需要执行额外的操作
管理软件(如libvirt)可以方便地利用这两个参数项来设置虚拟机创建和启动的一些操作,比如前面是手动创建的tap设备并且加入网桥,在管理软件中,则可以把这些操作放到启动脚本中,当虚拟机关闭的时候可以删除tap设备。

tap设备原理

tun/tap是Linux下的虚拟网卡设备,能够被用户态的进程用来发送和接收数据包,但是与实际网卡的数据来自网卡的链路层不同,tun/tap数据的接收和发送方都是来自用户进程或者内核

tap 设备是一个二层设备, 当向tap设备写入数据时,对tap设备而言就类似于网卡收到了来自网络的数据包,当对tap进行read的时候,对tap设备而言就类似于网卡进行发包。

tun和tap设备都是通过Linux内核中的tun驱动创建的,tun驱动在初始化的时候会注册一个misc设备,其路径为“/dev/net/tun”,用来作为向用户态导出的接口,所有对tun/tap设备的操作都必须首先打开“/dev/net/tun”得到一个fd,然后对fd进行相应的操作

应用程序可以调用ioctl(TUNSETIFF)来创建一个设备,如果在参数中不提供设备的名字,那么内核会自己生成一个名字,参数中还需要包括要创建的设备模式,也就是指明是tun设备还是tap设备。进程创建好tap设备之后就可以用了,但是这样的设备会随着创建设备进程的退出而销毁,为了将tap一直保持在系统中,需要调用额外的ioctl(TUNSETPERSIST)来将设备持久化。

当其他进程要使用该tap设备时,也会调用ioctl(TUNSETIFF),由于这个时候设备已经存在了,所以该进程会连接(attach)到该tap上去。

  • 当向tap设备写入数据时,Linux内核源码文件drivers/net/tun.c中会调用tun_get_user,并且最终调用到netif_rx_ni函数,也就是进入了一个收包流程,这个时候其他应用程序可以读取tap网卡上的数据。
  • 当读tap设备时,drivers/net/tun.c中会调用tun_do_read函数,进而调用tun_put_user将数据复制到用户态进程。

tap的创建是通过ip命令创建的,它创建了一个持久化的tap0设备,并将这个tap设备的名字传给QEMU
QEMU打开tun设备,将自己attach到这个tap设备上去。
命令行初始化时将宿主机的网卡ens33和tap0都加入到了网桥br0上,这样br0将接管ens33和tap0的收发包过程
当虚拟机内部发包并且最终通过tap0发包时,会被br0接管,然后br0将包发送到ens33网卡,ens33将虚拟机的数据包转发出去。
当br0网桥接收到数据包时,会判断该包是否属于tap0,如果属于则会路由到tap0上去,这样QEMU中打开tap设备的fd会产生事件,使得QEMU来处理这个收包,tap设备会将该包发送到QEMU对应的前端设备(也就是虚拟机的网卡)上。由此也可以看出,QEMU向tap设备发包对应的是其收包过程,QEMU设备向tap设备收包对应的其实是tap设备的发包过程。

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
-+ net_init_tap
\ -+ fd = net_tap_init(tap, &vnet_hdr, i >= 1 ? "no" : script, ifname, sizeof ifname, queues > 1, errp);
\ -+ tap_open(ifname, ifname_sz, vnet_hdr, vnet_hdr_required, mq_required, errp));
\ - fd = open(PATH_NET_TUN, O_RDWR)
| - ioctl(fd, TUNGETFEATURES, &features) "调用ioctl(TUNGETFEATURES)得到tun驱动特性"
| - pstrcpy(ifr.ifr_name, IFNAMSIZ, ifname); "使用tap设备名字和模式初始化一个ifreq"
| - ifr.ifr_flags |= IFF_MULTI_QUEUE
| - ret = ioctl(fd, TUNSETIFF, (void *) &ifr); "QEMU进程attach到该tap设备上"
| - fcntl(fd, F_SETFL, O_NONBLOCK); "调用fcntl将该tap设备设置为非阻塞模式"
| -+ net_init_tap_one(tap, peer, "tap", name, ifname, i >= 1 ? "no" : script,
i >= 1 ? "no" : downscript, vhostfdname, vnet_hdr, fd, &err);
\ -+ TAPState *s = net_tap_fd_init(peer, model, name, fd, vnet_hdr);
"net_tap_fd_init完成之后,net_init_tap也基本完成了,QEMU的主循环已经开始监听tap设备fd的可读事件了。"
\ -+ nc = qemu_new_net_client(&net_tap_info, peer, model, name); "创建一个新的NetClientState"
\ - qemu_net_client_setup(nc, info, peer, model, name, qemu_net_client_destructor, true);
"将这个NetClientState加到net_clients链表上,方便前端网卡查找"
"peer表示的是对应的后端网络设备的NetClientState"
| - TAPState* s = DO_UPCAST(TAPState, nc, nc); "将NetClientState 转换为 TAPState,然后对其进行初始化"
| - s->fd = fd; "将TAPState 的fd 指向 tap设备的fd"
| -+ tap_read_poll(s, true);
\ -+ tap_update_fd_handler(s)
\ - qemu_set_fd_handler(s->fd, s->read_poll && s->enabled ? tap_send : NULL,
s->write_poll && s->enabled ? tap_writable : NULL, s);
"将tap设备的fd加入到iohandler_ctx所在的AioContext中"
"tap_send会在tap设备有数据可读的时候调用,这个时候表示有新的数据发送到了tap设备,tap设备会将其发送到虚拟机的网卡,这对应tap设备的收包"
"tap_writable会在tap设备有数据可写的时候调用,只有当调用writev向tap设备发包不成功的时候才会调用tap_write_poll来设置可写时间,
所以理论上正常情况下tap_writable函数是不会被调用的。"
| - tap_set_sndbuf(s->fd, tap, &err); "设置tap设备的发包空间大小"

tap设备的工作主要就是创建一个NetClientState结构并放在net_clients链表上

前端网卡设备的创建

NICInfo结构中的netdev保存了tap网卡的NetClientState结构。值得注意的是,此时网卡并没有创建和初始化,所以tap设备的peer也没有指向,接下来就分析虚拟网卡的初始化以及其如何与tap设备网卡联系在一起。

以e1000网卡为例,其realize函数是pci_e1000_realize,在其中会调用qemu_new_nic去创建新的网卡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-+ pci_e1000_realize(PCIDevice *pci_dev)
\ - e1000_mmio_setup(d);
| - pci_register_bar(pci_dev, 0, PCI_BASE_ADDRESS_SPACE_MEMORY, &d->mmio);
| - pci_register_bar(pci_dev, 1, PCI_BASE_ADDRESS_SPACE_IO, &d->io);
| -+ d->nic = qemu_new_nic(&net_e1000_info, &d->conf, object_get_typename(OBJECT(d)), dev->id, d);
"创建新的网卡"
\ - NetClientState **peers = conf->peers.ncs; "peers变量是来自虚拟网卡的对象成员NICConf"
| - int i, queues = MAX(1, conf->peers.queues); "后端设备的队列个数queues"
| - nic = g_malloc0(info->size + sizeof(NetClientState) * queues); "分配NICState结构以及queues个NetClientState结构"
| - nic->ncs = (void *)nic + info->size; "nic->ncs指向NICState的末尾结构,也就是NetClientState的开始位置"
| -+ for i in queues:
\ -+ qemu_net_client_setup(&nic->ncs[i], info, peers[i], model, name, NULL, true);
"ncs 表示的是虚拟网卡的NetClientState"
"peer表示的是对应的后端网络设备的NetClientState"
\ - nc->peer = peer; peer->peer = nc; "后端网络设备的peer设置成前端网络端点对应的NetClientState"
| - QTAILQ_INSERT_TAIL(&net_clients, nc, next); "将nc加入到net_clients"
| - nc->incoming_queue = qemu_new_net_queue(qemu_deliver_packet_iov, nc);
"给nc分配一个incoming_queue, 并设置queue的分发函数为qemu_deliver_packet_iov"
| - nic->ncs[i].queue_index = i;

最重要的前端虚拟网卡NICStateNetClientState与后端TAP设备的NetClientState相互建立了联系,各自的peer成员指向了对方。
前面在net_init_tap函数中,针对tap设备的每个队列都会调用net_init_tap_one,继而调用net_tap_fd_init,在该函数中会调用qemu_new_net_client来创建网络端点

虚拟网卡 NICState的NetClientState 用来连接前后端网卡

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct NetClientState {
NetClientInfo *info; // 表示网卡的基本信息,主要是网卡的一些注册信息,在调用qemu_new_nic时第一个参数就是NetClientInfo, info的size可以指定大小,info中还包括一些回调函数,其中最重要的是receive回调,该函数用来进行网络端点的收包
int link_down; // 表示当前网卡状态是否down
QTAILQ_ENTRY(NetClientState) next; // 表示所有的NetClientInfo结构都链接在net_clients上,用next域连接
NetClientState *peer; // peer也是NetClientState结构,用来表示对端的网络端点,连接前后端网卡
NetQueue *incoming_queue; // 对应网卡的接受队列,所有的网络包都会被挂到该成员结构的packets链表上
char *model;
char *name;
char info_str[256]; // 表示的都是网卡的基本信息
unsigned receive_disabled : 1; //表示是否禁止收包
NetClientDestructor *destructor; //网卡被删除时执行的函数
unsigned int queue_index; // 实际的虚拟网卡用结构NICState表示,里面有一个ncs的NetClientState数组,这里的queue_index表示就是当前NetClientState在这个ncs中的索引。
...
QTAILQ_HEAD(, NetFilterState) filters; //filters链表上挂有NetFilterState,这个结构用来在网卡进行包路由的时候进行过滤,作用类似于防火墙。
};
虚拟机网卡发包流程

虚拟机内部操作系统中的程序进行发包时,经过了内核的网络协议栈之后到达网络设备的驱动,驱动程序会将要发送的数据的基本信息写入网卡的寄存器地址并触发网卡将数据包发送出去,这些操作通常都是通过MMIO或者PIO完成的, QEMU截获这些请求之后,将对应的数据写入网卡的状态中,然后调用发包函数将网络数据包发送出去

以e1000网卡为例,其发包函数是e1000_send_packet

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
-+ e1000_send_packet(E1000State *s, const uint8_t *buf, int size)
"第一个参数表示前端虚拟网卡,第二个参数是要发送的数据,第三个参数表示的是buf的大小"
\ -|+ if s->phy_reg[PHY_CTRL] & MII_CR_LOOPBACK
\ -+ qemu_receive_packet(nc, buf, size);
| -|+ else if !(s->phy_reg[PHY_CTRL] & MII_CR_LOOPBACK)
\ -+ qemu_send_packet(nc, buf, size); "发包"
\ -+ qemu_send_packet_async_with_flags(sender, QEMU_NET_PACKET_FLAG_NONE, buf, size, cb=NULL);
"sender 前端网卡的端点的 NetClientState"
\ - ret = filter_receive(sender, NET_FILTER_DIRECTION_TX, sender, flags, buf, size, sent_cb);
"前端网卡端点发送, 判断能否发送"
| - ret = filter_receive(sender->peer, NET_FILTER_DIRECTION_RX, sender, flags, buf, size, sent_cb);
"后端网卡端点接收, 判断后端tap 设备能否接收"
| - queue = sender->peer->incoming_queue; "后端设备端点的incoming_queue并赋值到queue"
| -+ qemu_net_queue_send(queue, sender, flags, buf, size, sent_cb=NULL);
\ -|+ if (queue->delivering || !qemu_can_send_packet(sender))
"如果此时这个后端网络的queue正在进行发包或者是当前网卡状态不允许发包,则将当前数据包加到queue的packets链表就返回。"
\ - qemu_net_queue_append(queue, sender, flags, data, size, sent_cb);
<-- | - return 0
| -|+ else
\ -+ qemu_net_queue_deliver(queue, sender, flags, data, size); "调用qemu_net_queue_deliver进行发包"
\ -+ queue->deliver(sender, flags, &iov, 1, queue->opaque);
"data和size表示的数据转换为一个iovec, 最终会调用到创建incoming_queue时涉及的回调函数qemu_deliver_packet_iov"
\ -+ qemu_deliver_packet_iov(NetClientState *sender,
unsigned flags,
const struct iovec *iov,
int iovcnt,
void *opaque)
\ - NetClientState *nc = opaque;
"opaque中得到当前queue对应的NetClientState,在这个例子中是TAP设备的NetClientState"
| -+ ret = nc->info->receive_iov(nc, iov, iovcnt); "调用NetClientState中NetClientInfo中的recieve_iov回调函数"
\ -+ tap_receive_iov(NetClientState *nc, const struct iovec *iov, int iovcnt)
\ -+ tap_write_packet(s, iovp, iovcnt);
\ - len = writev(s->fd, iov, iovcnt); "调用writev将数据发送到tap设备, 注意s->fd 前面赋值时说过"
"由于tap设备已经桥接到了br0网桥,所以这个时候网桥会把这个包进行转发,转到ens33物理网卡之后,ens33会把这个包发送出去。"

网络数据包就从前端网卡的NetClientState找到对应的后端网卡NetClientState以及它的收包队列,将数据包发送出去了。

虚拟机网卡接收数据包

虚拟机数据包的接收是发送的逆过程。网桥收到数据包后会进行转发,如果目的地址是tap设备,那就会把数据转发到tap网卡,这个时候QEMU中打开的tap设备fd就会有数据可读事件,QEMU主线程会返回,并且会调用到tap_send。

1
2
3
4
5
6
7
8
9
10
11
    qemu_set_fd_handler(s->fd,
s->read_poll && s->enabled ? tap_send : NULL, "read_poll 注册的是 tap_send函数"
s->write_poll && s->enabled ? tap_writable : NULL,
s);
-+ tap_send(void *opaque)
\ -+ while true
\ - size = tap_read_packet(s->fd, s->buf, sizeof(s->buf)); "从fd 接数据"
| -+ size = qemu_send_packet_async(&s->nc, buf, size, tap_send_completed);
\ -+ qemu_send_packet_async_with_flags(sender, QEMU_NET_PACKET_FLAG_NONE, buf, size, sent_cb=tap_send_completed);
"这个地方的send变成了后端TAP设备, 它会找到NetClientState的peer端,也就是前端网卡的incoming_queue,然后进行数据包的发送。这最终会调用到前端网卡注册NetClientInfo时涉及的receive_iov函数,对于e1000来说该回调函数是e1000_receive_iov,定义在net_e1000_info中,
这里不对具体的e1000收包进行赘述,简单来说就是把数据放到e1000指定的各个描述符中,然后发送一个中断提醒内核收包,在其中调用了qemu_set_irq进行中断注入,这样虚拟机中的操作系统的e1000网卡驱动就能够将数据取走了。"

ioeventfd 和 irqfd

对于设备模拟,虚拟机通过触发VM Exit退出到KVM,接着由KVM或者QEMU完成I/O模拟等操作。
当KVM或QEMU完成了I/O请求或者有其他事件需要通知虚拟机时,则通过注入中断的方式让虚拟机得到事件通知。

从I/O请求的分发路径来看,每次虚拟机内部写设备的MMIO的时候,都会导致陷入到KVM,然后分发到QEMU,QEMU中还会进行一轮分发,这个过程比较低效,因此ioeventfd方案就应运而生了。

eventfd原理

eventfd本质上是一个系统调用,创建一个事件通知fd,在内核内部创建一个eventfd对象,可以用来实现进程之间的等待/通知机制,内核也可以利用eventfd来通知用户态进程事件

ioeventfd

存在这样一种情况,即I/O请求本身只是作为一个通知事件,这个事件本身可能是通知KVM或者QEMU完成另一个具体的I/O,这种情况下没有必要像普通I/O一样等待数据完全写完,而是只需要完成一个简单的通知。
如果这种I/O请求也使用之前同步的方式完成,很明显会增加不必要的路径。
ioeventfd就是对这种通知I/O进行的优化,用户层程序(如QEMU)可以为虚拟机特定的地址关联一个eventfd,并对该eventfd进行事件监听,然后调用ioctl(KVM_IOEVENTFD)向KVM注册这段地址
当虚拟机内部因为I/O发生VM Exit时,KVM可以判断其地址是否有对应的eventfd,如果有就直接调用eventfd_signal发送信号到对应的fd,这样,QEMU就能够从其事件监听循环返回,进而进行处理

KVM注册ioeventfd

kernel 需要开启CONFIG_HAVE_KVM_EVENTFD 选项

对于KVM_IOEVENTFD,KVM将参数从用户进程复制到内核之后,会调用kvm_ioeventfd

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
virt/kvm/eventfd.c
-+ kvm_ioeventfd(struct kvm *kvm, struct kvm_ioeventfd *args)
\ -+ kvm_assign_ioeventfd(kvm, args);
\ - ..."对用户态参数进行合法性检查"
| - bus_idx = ioeventfd_bus_from_flags(args->flags);
| -+ ret = kvm_assign_ioeventfd_idx(kvm, bus_idx, args);
\ -+ eventfd = eventfd_ctx_fdget(args->fd); "从eventfd得到其对应的内核态表示eventfd"
| - _ioeventfd* p = kzalloc(sizeof(*p), GFP_KERNEL_ACCOUNT);
"分配并初始化 _ioeventfd 结构"
| - p->addr    = args->addr; "addr表示eventfd关联的地址"
| - p->bus_idx = bus_idx; "bus_idx根据PIO或者MMIO设置为KVM_PIO_BUS或者KVM_MMIO_BUS, 后面创建一个内核态设备会用到"
| - p->length = args->len; "eventfd关联的长度"
| - p->eventfd = eventfd; "eventfd表示用户态的event fd对应内核态结构体eventfd_ctx"
| - p->datamatch = args->datamatch; or p->wildcard = true "根据KVM_IOEVENTFD_FLAG_DATAMATCH flags 确定是完全匹配还是模糊匹配"
| -+ kvm_iodevice_init(&p->dev, &ioeventfd_ops); "注册fd 回调函数"
\ - dev->ops = ops; ".write = ioeventfd_write, .destructor = ioeventfd_destructor,"
| -+ kvm_io_bus_register_dev(kvm, bus_idx, p->addr, p->length, &p->dev); "将该I/O设备注册到该虚拟机上"
\ - kvm_io_bus* new_bus = kmalloc(struct_size(bus, range, bus->dev_count + 1), GFP_KERNEL_ACCOUNT);
| - old_bus = kvm_get_bus(kvm, bus_idx); "根据bus_idx 找到原先的 kvm_io_bus 结构"
| - memcpy(new_bus, bus, sizeof(*bus) + i * sizeof(struct kvm_io_range));
| - kvm_io_range range = {.addr=addr, .len=len, .dev=dev} "将参数给到 kvm_io_range 结构"
| - new_bus->range[i] = range; "i 为 bus 上所有dev range 的排序, 即根据range->addr 的大小从小到大对range进行排序"
| - kvm->buses[bus_idx] = new_bus "更新 kvm->buses"
| - list_add_tail(&p->list, &kvm->ioeventfds); "最后将新增加的ioeventfd加到kvm->ioevnetfds链表上"
| - kvm_get_bus(kvm, bus_idx)->ioeventfd_count++; "更新该地址对应的Bus上的ioeventfd_count"
ioeventfd下的地址分派

当虚拟机访问向注册了ioeventfd的地址写数据时,与所有I/O操作一样,会产生VM Exit, 陷入到vmm 后, kvm 在处理EXC_LOAD_GUEST_PAGE_FAULTEXC_STORE_GUEST_PAGE_FAULT 异常时, 关联到前面的kvm mmio 章节的介绍, 会先由kernel 自己处理异常指令, 自己处理不了时, 再给到qemu 用户态
这个场景下, 就属于kernel 自己能处理的情况

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
-+ kvm_riscv_vcpu_exit(vcpu, run, trap)
\ -+ switch (trap->scause)
\ -|+ case EXC_LOAD_GUEST_PAGE_FAULT or EXC_STORE_GUEST_PAGE_FAULT
\ -+ gstage_page_fault(vcpu, run, trap)
\ -+ switch (trap->scause)
\ -|+ case EXC_LOAD_GUEST_PAGE_FAULT:
\ - return emulate_load(vcpu, run, fault_addr, trap->htinst);
| -|+ case EXC_STORE_GUEST_PAGE_FAULT:
\ -+ return emulate_store(vcpu, run, fault_addr,trap->htinst);
\ -+ if !(kvm_io_bus_write(vcpu, KVM_MMIO_BUS, fault_addr, len, run->mmio.data))
"kernel 自己处理不了来自guest 异常指令时, 才会给到 qemu 用户态处理"
\ - kvm_riscv_vcpu_mmio_return(vcpu, run);
-+ kvm_io_bus_write(vcpu, KVM_MMIO_BUS, fault_addr, len, run->mmio.data)
\ - bus = srcu_dereference(vcpu->kvm->buses[bus_idx], &vcpu->kvm->srcu);
"bus_idx 为 KVM_MMIO_BUS, 从kvm->buses 上根据bus_idx 找到bus, riscv 上只有mmio 这一条bus"
| - kvm_io_range range = {.addr=fault_addr, .len=len}
| - __kvm_io_bus_write(vcpu, bus, &range, val);
| - "根据range->addr 在bus的range[] 数组中找 当前传入的addr len 落在哪个range 里了, 找到对应的index 记为idx"
| -+ kvm_iodevice_write(vcpu, bus->range[idx].dev, range->addr, range->len, val)
\ -+ dev->ops->write ? dev->ops->write(vcpu, dev, addr, l, v) "调用注册的回调函数, 这里为ioeventfd_write"
\ -+ ioeventfd_write(vcpu, dev, addr, len, val)
\ - ioeventfd_in_range(p, addr, len, val)
"判断addr len 是否在该range 范围内, 会判断val 是否等于 p->datamatch, 如果是模糊匹配, 直接会pass"
| - eventfd_signal(p->eventfd, 1);
"通知用户态线程, 来活了, 这里并没有切换到用户态, 没有上下文切换, 而是给用户态线程发了一个通知, 这里通知完就返回了"
"QEMU将该eventfd关联到一个事件,并且将eventfd加入到QEMU的主循环中监听,那么此时QEMU的poll也会返回,用来处理这个事件"
| - return "进而重新vcpu run循环, 该线程再次进入到guest"

这里与一般的mmio 的处理流程有差别的地方就是对guest 来说, 这是一个异步调用, guest 在读写某数据时, 并不会等这个数据读写完, 而是直接再次进入到guest中了.

qemu 的另一个用户态线程在读写完后, 需要发中断通知guest, 这个地址上的数据读写完了, guest 接收到中断后再处理剩下的事情.

qemu需要提前确定哪种类型的地址可以是这种异步的形式, 否则与guest行为不一致的话就乱套. 从qemu侧看只有半虚拟化的部分virtio 设备才会用到该特性, 如支持iothread的virtio-blk 和 vhost 会用到

qemu ioeventfd的调用链

这个地方调用链比较长, 先摘出几个地方来

1
2
3
virtio_bus_start_ioeventfd -> VirtioDeviceClass* vdc->start_ioeventfd -> virtio_device_start_ioeventfd_impl -> virtio_bus_set_host_notifier -> VirtioBusClass *k -> ioeventfd_assign -> virtio_mmio_bus virtio_mmio_ioeventfd_assign -> memory_region_add_eventfd -> memory_region_transaction_commit -> address_space_update_ioeventfds -> address_space_add_del_ioeventfds ->             MEMORY_LISTENER_CALL(as, eventfd_add, Reverse, &section, fd->match_data, fd->data, fd->e);
KVMState *s ->memory_listener.listener.eventfd_add = kvm_mem_ioeventfd_add
kvm_mem_ioeventfd_add

分叉起点函数为 virtio_bus_set_host_notifier, 这个函数调用了mmio-bus 或pci-bus的 ioeventfd_assign 函数回调, 这个回调函数调用了memory_region_add_eventfd 该函数设置了 ioeventfd_update_pending 为 true后, 最后才会调 address_space_update_ioeventfds 这个函数 最终跟kvm 交互设置ioeventfd

如vhost 设置vq 对应的eventfd 的函数入口为 vhost_dev_enable_notifiers -> virtio_bus_set_host_notifier

irqfd

ioeventfd是虚拟机内部操作系统通知KVM/QEMU的一种快捷通道, 与之类似,irqfd是KVM/QEMU通知虚拟机内部操作系统的快捷通道。irqfd将一个eventfd与一个全局的中断号联系起来,当向这个eventfd发送信号时,就会导致对应的中断注入到虚拟机中

irqfd也是基于eventfd的,使用irqfd之前需要先初始化一个EventNotifier对象, 接着调用kvm_irqchip_add_irqfd_notifier_gsi,该函数调用kvm_irqchip_assign_irqfd向KVM发送ioctl(KVM_IRQFD)

kvm_irqchip_assign_irqfd函数准备一个kvm_irqfd结构,该结构中fd表示eventfd的fd,gsi表示对应的全局中断号

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
static int kvm_irqchip_assign_irqfd(KVMState *s, EventNotifier *event, EventNotifier *resample, int virq, bool assign)
{
int fd = event_notifier_get_fd(event);
int rfd = resample ? event_notifier_get_fd(resample) : -1;

struct kvm_irqfd irqfd = {
.fd = fd,
.gsi = virq, // 全局中断号
.flags = assign ? 0 : KVM_IRQFD_FLAG_DEASSIGN, //KVM_IRQFD_FLAG_DEASSIGN表示解除event fd与irq的关联
};

if (rfd != -1) {
assert(assign);
if (kvm_irqchip_is_split()) {
kvm_resample_fd_insert(virq, resample);
} else {
irqfd.flags |= KVM_IRQFD_FLAG_RESAMPLE; //KVM_IRQFD_FLAG_RESAMPLE用于水平触发的中断
irqfd.resamplefd = rfd;
}
}
if (!kvm_irqfds_enabled()) {
return -ENOSYS;
}
return kvm_vm_ioctl(s, KVM_IRQFD, &irqfd);
}

kernel 需要开启 CONFIG_HAVE_KVM_IRQFD 选项, 可选需要开启 CONFIG_HAVE_KVM_IRQ_ROUTING 选项, 如果没实现kvm_arch_set_irq_inatomic函数和kvm_set_routing_entry函数, 则不支持irqfd的中断路由, 在这个版本上并未发现riscv 支持irqfd的中断路由

KVM接收到ioctl(KVM_IRQFD)请求之后,把参数从用户态复制到内核态,然后执行kvm_irqfd,该函数主要根据flags来判断是创建还是解除eventfd与中断号的关联,以创建为例,会调用kvm_irqfd_assign。

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
-+ kvm_irqfd_assign(struct kvm *kvm, struct kvm_irqfd *args)
\ - irqfd = kzalloc(sizeof(*irqfd), GFP_KERNEL_ACCOUNT);
| - irqfd->kvm = kvm;
| - irqfd->gsi = args->gsi;
| - INIT_WORK(&irqfd->inject, irqfd_inject); "初始化 work_struct"
| - INIT_WORK(&irqfd->shutdown, irqfd_shutdown);
| - f = fdget(args->fd);
| - eventfd = eventfd_ctx_fileget(f.file); "关联kernel 中的eventfd 结构"
| - irqfd->eventfd = eventfd;
| - init_waitqueue_func_entry(&irqfd->wait, irqfd_wakeup);
| -+ init_poll_funcptr(&irqfd->pt, irqfd_ptable_queue_proc);
"以irqfd->pt调用eventfd的poll函数, 也就是eventfd_poll,在该函数中会调用poll_wait,进而调用到pt->_qproc 即 irqfd_ptable_queue_proc"
\ -+ irqfd_ptable_queue_proc(file, wqh, pt)
\ - add_wait_queue_priority(wqh, &irqfd->wait);
"将irqfd->wait这个对象加入到了eventfd的wqh队列中。这样
当有其他进程或者内核对eventfd进行write时,就会导致eventfd的wqh等待队列上的对象函数得到执行,也就是irqfd_wakeup会被执行"
| - list_add_tail(&irqfd->list, &kvm->irqfds.items); "入队"
| - events = vfs_poll(f.file, &irqfd->pt);
| -|+ if (events & EPOLLIN)
\ -+ schedule_work(&irqfd->inject); "只考虑有数据,即POLLIN的情形, 调用inject 回调"
\ -+ irqfd_inject(struct work_struct *work)
\ - struct kvm_kernel_irqfd *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 注册的, 这个kernel版本riscv 并未实现该函数"
| -+ while i--
\ - irq_set[i].set(&irq_set[i], kvm, irq_source_id, level, line_status); "会挨个调用每个中断路由项上的set方法触发中断"
-+ irqfd_wakeup(wait_queue_entry_t *wait, unsigned mode, int sync, void *key)
\ - eventfd_ctx_do_read(irqfd->eventfd, &cnt);
| - irq = irqfd->irq_entry;
| -|+ if kvm_arch_set_irq_inatomic(&irq, kvm, KVM_USERSPACE_IRQ_SOURCE_ID, 1, false) == -EWOULDBLOCK)
"先调用arch的实现, 将irq 注入到 虚拟机中, 如果这个地方处理不了, 再调用irqfd_inject 处理 , 在riscv 未实现"
\ - schedule_work(&irqfd->inject); "所以只能走到这里"

从当前版本的信息上看, riscv kvm 并不支持irqfd特性

vhost

回顾virtio 的原理,可以发现,QEMU的收发包是通过在用户态访问tap设备完成的,这一过程会涉及虚拟机内核、宿主机KVM模块、QEMU、宿主机网络协议栈中的多次转换,路径依然显得比较长,会带来性能上的损失。vhost就是针对virtio的优化,将virtio的后端从宿主机应用层的QEMU放到了宿主机的内核,这样虚拟机陷入KVM之后会直接在宿主机内核中进行收发包,不需要经过QEMU宿主机用户态,可以显著提高性能

以网卡为例, 对vhost net的原理进行分析

virtio net和vhost-net网卡的基本原理

左边是传统virtio网卡的原理图,在传统virtio中,虚拟机内部的virtio驱动作为前端,负责将虚拟机内的I/O请求封装到vring descriptor中,然后通过写MMIO的方式通知QEMU中的virtio后端设备,QEMU侧则会将这些I/O请求发送到tap设备,然后通过网桥发送到真实的网卡上

与传统virtio网卡模拟相同,vhost方案中也是由虚拟机中的virtio驱动将I/O请求封装到vringdesc中的,不过与传统virtio不同的是,vhost是由宿主机内核中的vhost模块作为virtio的后端,vhost在接收到来自虚拟机的通知之后会直接在宿主机内核中与tap设备通信,从而完成网络数据包的收发。

要想使用vhost,只需要在启动虚拟机的命令参数中设置tap设备的参数vhost为on,即-netdev tap,vhost=on即可。vhost-net的I/O路径中,除了QEMU、KVM外,还有另一个模块参与,即宿主机 kernel的 vhost-net模块。

vhost的初始化

virtio协议不管是实现在QEMU中还是vhost中都需要初始化相关数据结构,在vhost情况下,这些初始化工作都是由QEMU委托vhost完成的。为此,QEMU需要初始化并保存vhost的一些基本信息。

在前面介绍的net_init_tap_one中有vhost net的初始化代码

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
-+ net_init_tap_one(const NetdevTapOptions *tap, NetClientState *peer,
const char *model, const char *name,
const char *ifname, const char *script,
const char *downscript, const char *vhostfdname,
int vnet_hdr, int fd, Error **errp)
\ - vhostfd = open("/dev/vhost-net", O_RDWR); "由于并没有指定vhostfdname,所以会直接打开“/dev/vhost-net”设备,得到一个fd"
| - qemu_set_nonblock(vhostfd); "设置为非阻塞模式"
| - options.backend_type = VHOST_BACKEND_TYPE_KERNEL; "backend_type设置为VHOST_BACKEND_TYPE_KERNEL。"
| - options.net_backend = &s->nc;
| - options.opaque = (void *)(uintptr_t)vhostfd; "fd保存在options的opaque成员中"
| - TAPState *s = net_tap_fd_init(peer, model, name, fd, vnet_hdr);
"QEMU使用这个fd来与内核模块vhost-net进行通信,"
| -+ s->vhost_net = vhost_net_init(&options); "进行vhost net的初始化"
\ - struct vhost_net *net = g_new0(struct vhost_net, 1);
| - net->nc = options->net_backend;
| - net->dev.vqs = net->vqs;
| - r = vhost_net_get_fd(options->net_backend);
| - net->backend = r; "backend成员保存了tap设备对应的fd"
| -+ r = vhost_dev_init(&net->dev, options->opaque, options->backend_type, options->busyloop_timeout, );
"初始化vhost_net的vhost_dev类型的dev成员, 即vhost_dev结构"
\ -+ r = vhost_set_backend_type(hdev, backend_type); "该函数通过vhost后端类型设置vhost_dev的vhost_ops成员"
\ - dev->vhost_ops = &kernel_ops; "设置为 kernel_ops, 里面封装了很多的回调函数"
| -+ r = hdev->vhost_ops->vhost_backend_init(hdev, opaque, errp);
"前面初始化了vhost_ops为kernel_ops, 此处调用kernel_ops的 vhost_kernel_init 回调"
\ - dev->opaque = opaque; "设置vhost_dev的opaque为“/dev/net-vhost”的fd"
| -+ r = hdev->vhost_ops->vhost_set_owner(hdev); "vhost_set_owner回调函数会在内核中创建一个内核线程"
\ -+ vhost_kernel_set_owner(vhost_dev)
\ - vhost_kernel_call(vhost_dev, VHOST_SET_OWNER, NULL);
| -+ for i in hdev->nvqs
\ -+ r = vhost_virtqueue_init(hdev, hdev->vqs + i, hdev->vq_index + i);
"初始化virtqueue,vhost中一个virtqueue用vhost_virtqueue表示, 里面只存了一些基本的信息"
"在vhost-net中,QEMU其实只是作为一个控制面,真正与虚拟机中的virtio前端驱动交互的是vhost-net宿主机内核模块,
所以不需要像VirtQueue结构体中那样保存很多数据"
\ - vhost_vq_index = dev->vhost_ops->vhost_get_vq_index(dev, hdev->vq_index + i);
| - int r = event_notifier_init(&vq->masked_notifier, 0); "初始化一个eventfd,保存在vhost_virtqueue的masked_notifier成员中"
| - file.fd = event_notifier_get_wfd(&vq->masked_notifier); ""
| -+ r = dev->vhost_ops->vhost_set_vring_call(dev, &file);
"该参数中包含了当前virtqueue的序号和对应的eventfd,该回调函数会触发一个ioctl(VHOST_SET_VRING_CALL),
将参数传递到内核,这样vhost-net模块就能够使用该eventfd来通知虚拟机其avail vring已经被使用,也就是virtio的后端通知前端。"
\ -+ vhost_kernel_set_vring_call(dev, file)
\ - vhost_kernel_call(dev, VHOST_SET_VRING_CALL, file);
| - vq->dev = dev;
| - hdev->memory_listener = {.name = "vhost", .begin = vhost_begin, .commit = vhost_commit,
.region_add = vhost_region_addnop, .eventfd_add = vhost_eventfd_add, .eventfd_del = vhost_eventfd_del,}
| - memory_listener_register(&hdev->memory_listener, &address_space_memory);
"初始化vhost_dev的memory_listener, 然后将其注册到address_space_memory上"
"这个listener的一个重要用途是控制热迁移中的脏页记录,在不使用vhost的时候,虚拟机访问的内存都能够通过EPT进行标脏处理,
但是在使用vhost-net之后,vring中指示的内存也会被vhost给标脏,因此必须有方法去记录这些脏页,
保证以后在热迁移的时候能够使用。所以这里注册了内存变更的监听函数。"
| - hdev->iommu_listener = {.name = "vhost-iommu",
.region_add = vhost_iommu_region_add, .region_del = vhost_iommu_region_del, };

前面介绍设备模拟时已经介绍过TAPState及其第一个成员NetClientState,它们表示的是一个网络端点,与传统网卡(如e1000)或者virtio网卡完全由QEMU完成模拟不同,vhost网卡的模拟需要与宿主机的vhost模块联系,所以TAPState中需要有一个结构来联系宿主机vhost模块,这是通过TAPState中的vhost_net成员来完成的

vhost_net结构体的第一个成员dev用来表示一个vhost的设备对象,其成员是表示与vhost本身相关的(如virtqueue的指针、特性)且与内核交互的一组函数

目前有两种vhost后端:

  • 一种是vhost-net,在宿主机内核态实现的virtio后端,其VhostOps由kernel_ops表示;
  • 另一个是vhost-user,是在用户态实现的virtio后端,这是通过用户态驱动直接与设备进行交互实现的,其VhostOps是user_ops。

这里以kernel_ops为例分析VhostOps结构,其定义如下,成员基本都是回调函数。

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
const VhostOps kernel_ops = {
.backend_type = VHOST_BACKEND_TYPE_KERNEL,
.vhost_backend_init = vhost_kernel_init,
.vhost_backend_cleanup = vhost_kernel_cleanup,
.vhost_backend_memslots_limit = vhost_kernel_memslots_limit,
.vhost_net_set_backend = vhost_kernel_net_set_backend,
.vhost_scsi_set_endpoint = vhost_kernel_scsi_set_endpoint,
.vhost_scsi_clear_endpoint = vhost_kernel_scsi_clear_endpoint,
.vhost_scsi_get_abi_version = vhost_kernel_scsi_get_abi_version,
.vhost_set_log_base = vhost_kernel_set_log_base,
.vhost_set_mem_table = vhost_kernel_set_mem_table,
.vhost_set_vring_addr = vhost_kernel_set_vring_addr,
.vhost_set_vring_endian = vhost_kernel_set_vring_endian,
.vhost_set_vring_num = vhost_kernel_set_vring_num,
.vhost_set_vring_base = vhost_kernel_set_vring_base,
.vhost_get_vring_base = vhost_kernel_get_vring_base,
.vhost_set_vring_kick = vhost_kernel_set_vring_kick,
.vhost_set_vring_call = vhost_kernel_set_vring_call,
.vhost_set_vring_busyloop_timeout =
vhost_kernel_set_vring_busyloop_timeout,
.vhost_set_features = vhost_kernel_set_features,
.vhost_get_features = vhost_kernel_get_features,
.vhost_set_backend_cap = vhost_kernel_set_backend_cap,
.vhost_set_owner = vhost_kernel_set_owner,
.vhost_reset_device = vhost_kernel_reset_device,
.vhost_get_vq_index = vhost_kernel_get_vq_index,
#ifdef CONFIG_VHOST_VSOCK
.vhost_vsock_set_guest_cid = vhost_kernel_vsock_set_guest_cid,
.vhost_vsock_set_running = vhost_kernel_vsock_set_running,
#endif /* CONFIG_VHOST_VSOCK */
.vhost_set_iotlb_callback = vhost_kernel_set_iotlb_callback,
.vhost_send_device_iotlb_msg = vhost_kernel_send_device_iotlb_msg,
};

其中的大部分函数都会直接在/dev/vhost-net所在的fd上调用ioctl,用来配置vhost-net中virtio后端
vhost_kernel_set_vring_base为例,它调用了vhost_kernel_call,并且提供了相关的ioctl请求与参数,进而实现与内核vhost-net模块的沟通

1
2
3
4
static int vhost_kernel_set_vring_base(struct vhost_dev *dev, struct vhost_vring_state *ring)
{
return vhost_kernel_call(dev, VHOST_SET_VRING_BASE, ring);
}

vhost net内核网络模块

vhost-net作为一个misc设备驱动存在于内核中,所在目录是drivers/vhost/net.c,其驱动模块的初始化函数调用misc_register注册一个misc设备vhost_net_misc,后者的file_operations为vhost_net_fops

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static const struct file_operations vhost_net_fops = {
.owner = THIS_MODULE,
.release = vhost_net_release,
.read_iter = vhost_net_chr_read_iter,
.write_iter = vhost_net_chr_write_iter,
.poll = vhost_net_chr_poll,
.unlocked_ioctl = vhost_net_ioctl, // 重要
.compat_ioctl = compat_ptr_ioctl,
.open = vhost_net_open, // 重要
.llseek = noop_llseek,
};
static struct miscdevice vhost_net_misc = {
.minor = VHOST_NET_MINOR,
.name = "vhost-net",
.fops = &vhost_net_fops,
};
static int vhost_net_init(void)
{
if (experimental_zcopytx)
vhost_net_enable_zcopy(VHOST_NET_VQ_TX);
return misc_register(&vhost_net_misc);
}
module_init(vhost_net_init);

vhost_net_fops中最重要的两个回调函数是vhost_net_open和vhost_net_ioctl

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
-+ vhost_net_open(struct inode *inode, struct file *f)
\ - struct vhost_net *n = kvmalloc(sizeof *n, GFP_KERNEL | __GFP_RETRY_MAYFAIL); "vhost_net 结构为dev的私有数据"
| - vhost_virtqueue **vqs = kmalloc_array(VHOST_NET_VQ_MAX, sizeof(*vqs), GFP_KERNEL);
| - queue = kmalloc_array(VHOST_NET_BATCH, sizeof(void *), GFP_KERNEL);
| - n->vqs[VHOST_NET_VQ_RX].rxq.queue = queue; "rx"
| - xdp = kmalloc_array(VHOST_NET_BATCH, sizeof(*xdp), GFP_KERNEL);
| - n->vqs[VHOST_NET_VQ_TX].xdp = xdp; "tx"
| - dev = &n->dev;
| - n->vqs[VHOST_NET_VQ_TX].vq.handle_kick = handle_tx_kick;
| - n->vqs[VHOST_NET_VQ_RX].vq.handle_kick = handle_rx_kick;
| -+ vhost_dev_init(dev, vqs, VHOST_NET_VQ_MAX, UIO_MAXIOV + VHOST_NET_BATCH, VHOST_NET_PKT_WEIGHT, VHOST_NET_WEIGHT, true, NULL);
"初始化vhost_net的vhost_dev成员"
\ - dev->vqs = vqs;
| - ...
| - init_llist_head(&dev->work_list);
| -+ for i in dev->nvqs
\ -|+ if vq->handle_kick
\ - vhost_poll_init(&vq->poll, vq->handle_kick, EPOLLIN, dev) "设置vq->poll的 work的fn为 handle_tx_kick 和 handle_rx_kick"
| -+ vhost_poll_init(n->poll + VHOST_NET_VQ_TX, handle_tx_net, EPOLLOUT, dev); "设置网络发包回调函数"
\ - init_waitqueue_func_entry(&poll->wait, vhost_poll_wakeup); "设置poll的wait等待队列成员的唤醒函数为vhost_poll_wakeup"
| - init_poll_funcptr(&poll->table, vhost_poll_func); "设置table的_qproc函数为vhost_poll_func"
"该函数把wait队列加到vhost的poll_table结构成员table中,最后初始化vhost_poll的work函数为fn, 即vhost_poll_wakeup"
| - vhost_work_init(&poll->work, fn); "fn为 handle_tx_net "
| - vhost_poll_init(n->poll + VHOST_NET_VQ_RX, handle_rx_net, EPOLLIN, dev); "设置网络收包回调函数"
| - f->private_data = n; "应用程序可以通过fd找到file,进而找到vhost_net、vhost_dev、vhost_virtqueue等结构"

用户态的程序每次打开/dev/net/vhost-net都会导致vhost_net_open的执行,该函数的主要作用是分配并初始化一个vhost_net结构及其成员vhost_dev和收发包队列vhost_virtqueue成员,同时将vhost_net设置为该open打开的file结构的私有成员

接下来分析vhost_net_ioctl函数,该函数是QEMU与vhost通信的通道,vhost-net设备导出到用户态空间的ioctl分为3类:

  • 第一类ioct1与整个vhost本身相关,如VHOST_NET_SET_BACKEND用来得到QEMU传递下来的tap设备fd,使得vhost-net能够直接与tap设备通信。VHOST_GET_FEATURES用来返回内核侧vhost-net的特性,这类ioctl通常在vhost_net_ioctl中直接处理;
  • 第二类ioctl与vhost创建的vhost设备相关,如VHOST_SET_MEM_TABLE用来得到QEMU传递下来的虚拟机物理地址QEMU虚拟地址的布局信息,VHOST_SET_LOG_BASE和VHOST_SET_LOG_FD通常与虚拟机热迁移的脏页记录有关,这一类ioctl通常在vhost_dev_ioctl函数中处理;
  • 第三类ioctl与vring相关,如VHOST_SET_VRING_NUM用来设置vring的大小, VHOST_SET_VRING_ADDR用来设置vring的地址,VHOST_SET_VRING_KICK和VHOST_SET_VRING_CALL用来设置两个eventfd,这一类ioctl通常在vhost_vring_ioctl函数中处理

vhost-net ioctl 分析

VHOST_SET_OWNER 分析

首先分析ioctl(VHOST_SET_OWNER)的对应处理函数vhost_net_set_owner,该函数的主要目的是把一个打开的vhost-net fd与一个进程关联起来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-+ vhost_net_set_owner(struct vhost_net *n)
\ -|+ if (vhost_dev_has_owner(&n->dev) "判断当前的vhost_net是否已经与进程绑定"
\ - return
| - vhost_net_set_ubuf_info(n) "与零拷贝(zero copy)有关"
| -+ vhost_dev_set_owner(&n->dev); "绑定进程"
\ -+ vhost_attach_mm(dev);
\ - dev->mm = get_task_mm(current); "将当前进程的mm_struct 成员赋值给 dev的mm_struct 成员"
| - dev->kcov_handle = current->kcov_handle;
| -+ worker = kthread_create(vhost_worker, dev, "vhost-%d", current->pid); "创建一个内核线程, 线程函数为vhost_worker"
\ ---+ vhost_worker(void *data)
\ - kthread_use_mm(dev->mm); "make the calling kthread operate on an address space"
| -+ while 1
\ - node = llist_del_all(&dev->work_list); "从vhost_dev的work_list循环取下一个vhost_work"
| -| if !node schedule() "如果没有任务,则该线程会进入睡眠状态。"
| - node = llist_reverse_order(node);
| - llist_for_each_entry_safe(work, work_next, node, node)
| - __set_current_state(TASK_RUNNING);
| - work->fn(work);
| - dev->worker = worker; "将该线程的task_struct赋值到vhost_dev的worker成员中"
| - wake_up_process(worker); "调用其对应的函数"
| - vhost_attach_cgroups(dev); "跟cgroups有关,这里暂时不考虑"
| - vhost_dev_alloc_iovecs(dev); "分配virtqueue的相关空间"
VHOST_SET_MEM_TABLE 分析

下面分析与vhost net设备相关的ioctl(VHOST_SET_MEM_TABLE)。将用户态的vhost_memory类型的参数复制到vhost_dev的memory成员中

vhost_memory用来表示虚拟机的内存布局信息,vhost_memory_region是一个可变长数组,其中的每一项数组元素保存了虚拟机的物理地址与QEMU虚拟地址之间的关系

1
2
3
4
5
6
7
8
9
10
11
struct vhost_memory {
__u32 nregions; "nregions表示 vhost_memory_region 变长数组 数组的大小"
__u32 padding; ""
struct vhost_memory_region regions[0];
};
struct vhost_memory_region {
__u64 guest_phys_addr;
__u64 memory_size; /* bytes */
__u64 userspace_addr;
__u64 flags_padding; /* No flags are currently specified. */
};

QEMU通过ioctl(VHOST_SET_MEM_TABLE)将虚拟机的内存布局信息告诉vhost-net,vhost-net模块在进行相关的virtio后端操作处理虚拟机物理地址时能够找到对应的QEMU所在的虚拟地址,从而读写相关数据。

1
2
3
4
5
6
7
8
9
10
11
12
-+ vhost_set_memory(d, argp);
\ - copy_from_user(&mem, m, size))
| - newmem = kvzalloc(struct_size(newmem, regions, mem.nregions), GFP_KERNEL);
| - memcpy(newmem, &mem, size);
| - newumem = vhost_iotlb_alloc(max_iotlb_entries, VHOST_IOTLB_FLAG_RETIRE); "add a new vhost IOTLB"
| -+ for (region = newmem->regions; region < newmem->regions + mem.nregions; region++)
\ - vhost_iotlb_add_range(newumem,
region->guest_phys_addr,
region->guest_phys_addr +
region->memory_size - 1,
region->userspace_addr,
VHOST_MAP_RW)

接下来分析与virtqueue的vring设置相关的ioctl处理,这一类的ioctl都是通过vhost_vring_ioctl函数处理的。这里可以看到有很多与QEMU类似的功能,如VHOST_SET_VRING_NUM用来设置virtqueue的vring大小,VHOST_SET_VRING_ADDR用来设置vring所在地址等。

这里介绍两个非常重要的ioctl
第一个是VHOST_SET_VRING_KICK,用来告诉vhost-net模块前端virtio驱动发送通知的时候触发的eventfd,vhost-net在处理该ioctl的时候首先得到该eventfd对应的file结构,然后将其赋值给vq->kickQEMU会针对虚拟机中virtio驱动进行通知的寄存器地址(MMIO)与该eventfd设置一个ioeventfd,这样当虚拟机中virtio驱动写这个地址的时候就会触发该eventfd,从而直接在vq->kick的file上产生信号
在vhost_vring_ioctl函数最后调用vhost_poll_start时,在vq->kick上面进行poll。
**当fd有信号之后会唤醒eventfd等待队列上的对象,这里会执行vhost_poll_wakeup函数,该函数把work挂到vhost_dev的work_list中,然后唤醒vhost_dev的work线程,也就是在绑定用户态进程时创建的线程,该线程执行对应的函数handle_tx_kick 或 handle_rx_kick,进行发包或者收包。
用户态vring 上每个vq 上回注册一个eventfd, 每个vq 调用一次 ioctl(VHOST_SET_VRING_KICK) 绑定vhost 线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-+ vhost_vring_ioctl(struct vhost_dev *d, unsigned int ioctl, void __user *argp)
\ -+ switch(ioctl)
\ -+ case VHOST_SET_VRING_KICK
\ - copy_from_user(&f, argp, sizeof f)
| - eventfp = eventfd_fget(f.fd);
| - pollstop = (filep = vq->kick) != NULL;
| - pollstart = (vq->kick = eventfp) != NULL; "vq->kick 赋值为 eventfp 即 赋值为eventfd, 且 pollstart 设置为 true"
| -|+ if (pollstop && vq->handle_kick)
\ - vhost_poll_stop(&vq->poll); "停掉先前的vq->kick, 如果先前有值的话"
| -|+ if (pollstart && vq->handle_kick)
\ -+ r = vhost_poll_start(&vq->poll, vq->kick); "在vq->kick上面进行poll"
\ ---+ vhost_poll_wakeup(wait_queue_entry_t *wait, unsigned mode, int sync, key) "当eventfd 有信号时"
\ - vhost_work *work = &poll->work;
| -+ vhost_poll_queue(poll);
\ -+ vhost_work_queue(poll->dev, &poll->work);
\ - llist_add(&work->node, &dev->work_list);
| - wake_up_process(dev->worker); "唤醒work线程, 该线程执行对应的函数handle_tx_kick 或 handle_rx_kick"

第二个重要的ioctl是VHOST_SET_VRING_CALL,与VHOST_SET_VRING_KICK类似,VHOST_SET_VRING_CALL用来设置一个eventfd,这个eventfd用来完成vhost-net后端到虚拟机virtio前端的中断通知。其基本原理是,QEMU首先将virtio网卡使用的中断与一个eventfd联系起来,创建一个irqfd,并通过KVM的ioctl将irqfd信息传递到KVM模块,然后通过设备/dev/vhost-net的ioctl(VHOST_SET_VRING_CALL)将该eventfd传递到vhost-net模块。当vhost-net virtio后端使用了avail ring之后,就可以直接向该eventfd发送信号,从而让KVM注入一个中断。

vhost net的启动

从上面的分析可以知道,tap后端设备在进行初始化的时候如果开启了vhost,则会在TAPState中保存一个vhost_net结构体,vhost_net中有vhost_device表示与内核态vhost的一个连接。本节分析前端的virtio网卡如何与其建立联系。

当虚拟机的virtio net驱动准备好之后会改变网卡的status,最终在QEMU调用到virtio_set_status函数,在这个函数的调用链中会调用到virtio_net_vhost_status

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
-+ virtio_net_vhost_status(VirtIONet *n, uint8_t status)
\ - NetClientState *nc = qemu_get_queue(n->nic);
| -+ if !get_vhost_net(nc->peer) "用后端设备的NetClientState作为参数调用get_vhost_net函数"
\ - return "如果没有使用vhost,则该成员为空,表示没有使用vhost,函数直接返回"
| -+ if virtio_net_started(n, status) && !nc->peer->link_down) == !!n->vhost_started) "判断vhost net是否启动"
\ - return "如果已经启动了, 直接return"
| -+ if !n->vhost_started "没启动时"
\ -+ for i in queue_pairs
\ - NetClientState *qnc = qemu_get_subqueue(n->nic, i);
| - qemu_net_queue_purge(qnc->peer->incoming_queue, qnc); "将前端网卡队列上的数据包丢掉"
| - qemu_net_queue_purge(qnc->incoming_queue, qnc->peer); "将后端网卡队列上的数据丢掉"
| - n->vhost_started = 1;
| -+ r = vhost_net_start(vdev, n->nic->ncs, queue_pairs, cvq) "让虚拟机的收发包通过vhost-net完成,
vdev是virtio网卡, ncs是virtio对应的网络后端tap设备, queue_pairs 表示的是网卡队列"
\ -+ r = k->set_guest_notifiers(qbus->parent, total_notifiers, true);
"调用virtio总线类的回调函数set_guest_notifiers, 为virtio_pci_set_guest_notifiers, 用来设置从宿主机到虚拟机通知的eventfd的设置"
"virtio_pci_set_guest_notifiers通过virtio_pci_set_guest_notifier初始化VirtQueue的类型为EventNotifier的成员guest_notifier,
然后在kvm_virtio_pci_vector_use函数中将该guest_notifier与virtio设备的中断关联起来,
在内核中构成一个irqfd,最后在将guest_notifier对应的fd通过ioctl(VHOST_SET_VRING_CALL)通知到vhost-net模块
vhost-net模块可以直接设置该eventfd,从而让虚拟机接收到中断"
| - r = vhost_net_start_one(get_vhost_net(peer), dev); "开启virtio网卡队列"
| - vhost_set_vring_enable(peer, peer->vring_enable)
"通过EventNotifier的成员host_notifier来初始化VirtQueue的类型。
vhost_dev_start调用相关ioctl初始化vhost-net模块中相应的virt queue结构,
这些ioctl包括VHOST_SET_MEM_TABLE(用于向vhost传递虚拟机物理地址与QEMU虚拟地址的布局关系)、
VHOST_SET_VRING_BASE(将vring descriptor描述符地址告诉vhost)、
VHOST_SET_VRING_KICK(将VirtQueue的host_notifier成员对应的fd传递给vhost)、
VHOST_SET_VRING_CALL(将VirtQueue的guest_notifier成员对应的fd传递给vhost)等"

vhost net的收发包

在介绍vhost-net情况下的收发包之前,需要讨论另一个vhost-net的ioctl,即VHOST_NET_SET_BACKEND,这个ioctl用来设置vhost-net对应的后端设备
virtio网卡是网络的前端设备,tap是后端设备,使用vhost之后,与虚拟机有关的网络收发包都是在内核态进行的,所以需要QEMU告诉vhost tap设备的fd,由vhost保存对应的sock结构。ioctl(VHOST_NET_SET_BACKEND)即用于完成这项任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-+ vhost_net_set_backend(struct vhost_net *n, unsigned index, int fd)
\ - vhost_virtqueue * vq = &n->vqs[index].vq; "得到收包队列的vhost_virtqueue"
| - vhost_net_virtqueue * nvq = &n->vqs[index];
| - sock = get_socket(fd); "从tap设备的fd得到所属的socket结构sock"
| - oldsock = vhost_vq_get_backend(vq);
| -|+ if (sock != oldsock) "如果当前的sock与已经保存oldsock的不一样,说明改变了后端设备, 最开始的时候oldsock是空"
\ - ubufs = vhost_net_ubuf_alloc(vq, sock && vhost_sock_zcopy(sock)); "zero copy 相关"
| - vhost_net_disable_vq(n, vq); "如果当前的sock与已经保存oldsock的不一样,说明改变了后端设备, 将当前正在poll的fd取消"
| -+ vhost_vq_set_backend(vq, sock)
\ - vq->private_data = sock; "vq的私有数据赋值为sock"
| -+ r = vhost_net_enable_vq(n, vq); "启动对tap设备fd的poll"
\ -+ sock = vhost_vq_get_backend(vq);
\ - return vq->private_data;
| - vhost_poll_start(poll, sock->file);
"这里可以看到vhost对tap设备的file进行poll,这样当tap设备接收到数据时就会调用在初始化时指定的handle_rx_net,
进而调用handle_rx,handle_rx将接收到的数据放入到队列的vring中"

接下来分析vhost-net情况下的发包
虚拟机需要发送网络数据包时,会填充好需要发送的数据到vring descriptor,然后写MMIO通知对端,也就是vhost-net,由于kick fd是一个ioeventfd,所以会导致kick fd上产生信号
从前面vhost初始化poll相关结构体的函数vhost_poll_init可知,kick fd上有信号时会调用函数vhost_poll_wakeup,该函数调用的vhost_poll_queue会调用vhost_work_queue,这个函数会将一个vhost_work挂在vhost_dev的work_list成员上,然后唤醒vhost_dev的worker成员。这个worker是在vhost_dev_set_owner函数中创建的内核线程的task_struct,该内核线程的线程函数是vhost_worker,vhost_work即是从vhost_dev->work_list取出的。然后调用其fn回调函数,对于发包队列来说这个回调函数是handle_tx_kick。handle_tx_kick调用handle_tx进行实际的发包工作,handle_tx的细节这里不再讨论,整个过程就是从vring上面取出网络数据,然后调用tap设备对应sock的sendmsg函数将数据发送出去。

与发包函数handle_tx对应的收包函数是handle_rx。handle_rx调用tap设备对应sock的recvmsg函数接收网络数据包,并将数据放到vring descritpor上,然后调用vhost_add_used_and_signal_n通知虚拟机,其中进行通知的函数是vhost_signal,该函数直接在vhost_virtqueue->call_ctx上触发一个信号,由于这个fd已经对应了一个虚拟机中断,所以KVM在该信号被触发之后会向虚拟机注入一个中断,虚拟机virtio驱动可以在中断函数中处理收包。

1
2
3
4
5
void vhost_signal(struct vhost_dev *dev, struct vhost_virtqueue *vq)
{
if (vq->call_ctx.ctx && vhost_notify(dev, vq))
eventfd_signal(vq->call_ctx.ctx, 1);
}

设备直通与VFIO

iommu 主要功能

Iommu 的主要功能为设备 dma 时刻能够访问机器的物理内存区,同时保证安全性。

在没有 Iommu 的时候,设备通过 dma 可以访问到机器的全部的地址空间。

  1. 这种机制下如果将设备的驱动放在用户态,那么如何保护机器物理内存区对于用户态驱动框架设计带来挑战。当出现了 iommu 以后,iommu 通过控制每个设备 dma 地址到实际物理地址的映射转换,使得在一定的内核驱动框架下,用户态驱动能能够完全操作某个设备 dma 和中断成为可能。

  2. 如果将这个物理设备通过透传的方式进入到虚拟机里,虚拟机的设备驱动配置设备的 dma 后,hypervisor 必须在透传设备 dma 访问时刻,对 dma 访问进行截获,将其中 dma 访问的虚拟机物理地址,转换为 hypervisor 为虚拟机分配的物理地址,也就是需要将虚拟机透传设备 dma 访问做 GPA(虚拟机物理地址)—->HPA(物理机物理地址)。这部分截获对虚拟机 dma 来说带来切换到 hypervisor 开销,hypervisor 转换地址开销。

当引入了 iommu 以后,这部分开销由 iommu 硬件承担,所有 hypervisor 工作就更加简单,只需要将透传设备 Iommu dma 地址映射表使用 vpaddr—>ppaddr 地址转换表即可(这部分表在 hypervisor 里配置在 ept 中)

  1. 方便了老式 32 位 pci 硬件在 64 位机器上的使用。只需要在 iommu 地址映射表上配置 32bitpci 设备 dma 地址 –>64 位机器物理地址即可。

  2. 方便了主机 os 配置设备 dma 工作,因为 dma 要求使用连续的地址空间进行读写,有了 iommu 的存在 os 就可以为设备配置连续的 dma 地址而真正对应的非连续的物理地址

VFIO基本思想与原理

设备直通就是将物理设备直接挂到虚拟机,虚拟机通过直接与设备交互来获得较好的性能。传统的透传设备到QEMU/KVM虚拟机的方法为PCI passthrough,这种老的设备直通方式需要KVM完成大量的工作,如与IOMMU交互、注册中断处理函数等。显然这种方法会让KVM过多地与设备打交道,扮演一个设备驱动的角色,这种方案不够通用灵活,所以后来有了VFIO(Virtual Function I/O)

VFIO是一个用户态驱动框架,它利用硬件层面的I/O虚拟化技术,如Intel的VT-d和AMD的AMD-Vi,将设备直通给虚拟机. 设备驱动与设备进行交互需要访问设备的很多资源,如PCI设备的配置空间、BAR地址空间、设备中断等,所有这些资源都是在内核态进行分配和访问的。虚拟化环境下,把设备直通给虚拟机之后,QEMU需要接管所有虚拟机对设备资源的访问

VFIO的基本思想包括两个部分

  • 第一个是将物理设备的各种资源分解,并将获取这些资源的接口向上导出到用户空间,
    • QEMU等应用层软件可以利用这些接口获取硬件的所有资源,包括设备的配置空间、BAR空间和中断。
  • 第二部分就是聚合,也就是将从硬件设备得到的各种资源聚合起来,对虚拟化展示一个完整的设备接口,这种聚合是在用户空间完成的
    • 以QEMU为例,它从硬件设备分解各种资源之后,会重新聚合成一个虚拟机设备挂到虚拟机上,QEMU还会调用KVM的接口将这些资源与虚拟机联系起来,使得虚拟机内部完全对VFIO的存在无感知,虚拟机内部的操作系统能够透明地与直通设备进行交互,也能够正常处理直通设备的中断请求。

在非虚拟化环境中,大部分情况下都是通过设备驱动访问硬件外设的,对于设备来说,其访问的内存地址空间可以是整个机器的,外设的中断也统一纳入操作系统的中断处理框架。但是在虚拟化环境下,当把设备直通给虚拟机之后,有两个难点需要解决,一个是设备DMA使用的地址,另一个是由于虚拟机内部在指定设备DMA地址的时候能够随意指定地址,所以需要有一种机制来对设备的DMA地址访问进行隔离。

IOMMU的主要功能是DMA Remapping。如果设备的DMA访问没有隔离,该设备就能够访问物理机上的所有地址空间,为了保证安全性,IOMMU会对设备的DMA地址再进行一层转换,使得设备的DMA能够访问的地址仅限于宿主机分配的一定内存中, 这里的Domain可以理解为一个虚拟机。

当虚拟机让设备进行DMA时,指定的是GPA地址,在经过DMA Remapping之后,该GPA地址会被转换成QEMU/KVM为其分配的物理地址。这一点与右边CPU进行访问时EPT的作用是一样的。

与MMU类似,DMA Remapping也需要建立类似页表这样的结构来完成DMA的地址转换


Root Table总共255项,每一项表示一条总线,Root Table中的每一项用来指向一个Context Table。Context Table中的每一项都记录有该设备对应的Domain信息,这些信息里面就有地址转换页表。这样通过DMA remapping就能够将设备访问的虚拟机地址转换成宿主机分配给虚拟机的物理地址。当然,Root Table、Context Table等都需要宿主机通过iommu驱动的编程接口去构造。

IOMMU还会有Interrupt Remapping,其原理也是通过IOMMU对所有的中断请求做一个重定向,从而将直通设备内中断正确地分派到虚拟机

VFIO 框架

  • VFIO Interface作为接口层,用来向应用层导出一系列接口,QEMU等用户程序可以通过相应的ioctl对VFIO进行交互
  • iommu driver是物理硬件IOMMU的驱动实现,如Intel和AMD的IOMMU, arm的smmu.
  • pci_bus driver是物理PCI设备的驱动程序
  • kernel 初始化时枚举 (pci_register_host_bridge) PCI设备的时候vfio_iommu是对底层iommu driver的封装,用来向上提供IOMMU的功能,如DMA Remapping以及Interrupt Remapping。
  • vfio_pci是对设备驱动的封装,用来向用户进程提供访问设备驱动的功能,如配置空间和模拟BAR。

VFIO的重要功能之一是对各个设备进行分区,但是即使有IOMMU的存在,想要以单个设备作为隔离粒度有时也做不到。所以,VFIO设备直通中有3个重要的概念,即container、group和device

group是IOMMU能够进行DMA隔离的最小单元,一个group内可能只有一个device,也可能有多个device,这取决于物理平台上硬件的IOMMU拓扑结构。设备直通的时候一个group里面的设备必须都直通给一个虚拟机。不能让一个group里的多个device分别从属于2个不同的VM,也不允许部分device在宿主机上而另一部分被分配到虚拟机里,因为这样一个虚拟机中的device可以利用DMA攻击获取另外一个虚拟机里的数据,无法做到物理上的DMA隔离。

device指的是要操作的硬件设备,不过这里的“设备”需要从IOMMU拓扑的角度去理解。如果该设备是一个硬件拓扑上独立的设备,那么它自己就构成一个IOMMU group。如果这里是一个multi-function设备,那么它和其他的function一起组成一个IOMMU group,因为多个function设备在物理硬件上是互联的,它们可以互相访问数据,所以必须放到一个group里隔离起来。

container是由多个group组成的,虽然group是VFIO的最小隔离单元,但是有的时候并不是最好的分割粒度。如多个group可能会共享一组页表,通过将多个group组成一个container可以提高系统的性能,也能够方便用户。一般来讲,每个进程/虚拟机可以作为一个container。

VFIO使用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
$ readlink /sys/bus/pci/devices/0000:01.10.0/iommu_group "找到这个设备的VFIO group, 这是由内核生成的"
../../../../kernel/iommu_groups/15
$ cd /sys/bus/pci/devices/0000:01.10.0/iommu_group
$ ls devices "查看group 里的设备, 这个group只有一个设备"
0000.01:10.0
$ echo 0000.01.10.0 > /sys/bus/pci/devices/0000.01.10.0/driver/unbind "将设备与驱动程序解绑"
$ lspci -n -s 01.10.0 "找到设备的vendor id 和 device id"
01.10.0 0200: 8086:10ca
$ echo "8086 10ca" > /sys/bus/pci/drivers/vfio-pci/new_id "将设备绑定到vfio-pci驱动, 会有一个新的设备节点/dev/vfio/15 被创建, 这个节点表示
直通设备所属的group文件, 用户态程序可以通过该节点操作直通设备的group"
$ chown qemu:qemu /dev/vfio/15 "修改这个设备节点的属性"
$ ulimit -l 2621440 "设置能够锁定的内存为 虚拟机内存 + 一些IO 空间"
$ qemu-system-x86_64 -m 2048 -hda -vga std -enable-kvm -device vfio-pci,host=01.10.0,id=net0 "启动qemu, 加入vfio设备"

VFIO 接口

与KVM的dev/vm/vcpu接口类似,VFIO的接口也分为3类,分别是container、group、device。本节对VFIO的接口进行简单介绍

第一类接口是container层面的,通过打开“/dev/vfio/vfio”设备可以获得一个新的container,可以用在container上的ioctl包括如下几个

  • VFIO_GET_API_VERSION:用来报告VFIO API的版本
  • VFIO_CHECK_EXTENSION:用来检测是否支持特定的扩展,如支持哪个IOMMU
  • VFIO_SET_IOMMU:用来指定IOMMU的类型,指定的IOMMU必须是通过VFIO_CHECK_EXTENSION确认驱动支持的
  • VFIO_IOMMU_GET_INFO:用来得到IOMMU的一些信息,这个ioctl只针对Type1的IOMMU。
  • VFIO_IOMMU_MAP_DMA:用来指定设备端看到的 IO 地址到进程的虚拟地址之间的映射,类似于 KVM 中的 KVM_SET_USER_MEMORY_REGION 指定虚拟机物理地址到进程虚拟地址之间的映射。

这里IOMMU的类型指定的不同架构的IOMMU实现不一样,能够向上提供的功能也不一样,所以会有不同类型的IOMMU,如内核针对Intel VT-d和AMD-Vi的IOMMU就叫作Type1 IOMMU。

第二类接口是group层面的,通过打开/dev/vfio/<groupid>可以得到一个group,group层面的ioctl包括如下几个。

  • VFIO_GROUP_GET_STATUS:用来得到指定group的状态信息,如是否可用、是否设置了container。
  • VFIO_GROUP_SET_CONTAINER:用来设置container和group之间的管理,多个group可以属于单个container。
  • VFIO_GROUP_GET_DEVICE_FD:用来返回一个新的文件描述符fd来描述具体设备,用户态进程可以通过该fd获取文件的诸多信息。

第三类接口是设备层面的,其 fd 是通过 VFIO_GROUP_GET_DEVICE_FD 接口返回的,device 层面的 ioctl 包括如下几个。

  • VFIO_DEVICE_GET_REGION_INFO:用来得到设备的指定Region的数据,需要注意的是,这里的region不单单指BAR,还包括ROM空间、PCI配置空间等。
  • VFIO_DEVICE_GET_IRQ_INFO:得到设备的中断信息。
  • VFIO_DEVICE_RESET:重置设备

下面介绍这些接口的使用方法。

  1. 创建container,并判断其是否支持Type1类型的IOMMU,设置类型为VFIO_TYPE1_IOMMU。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    s->container = open("/dev/vfio/vfio", O_RDWR);
    if (s->container == -1) {
    error_setg_errno(errp, errno, "Failed to open /dev/vfio/vfio");
    return -errno;
    }
    if (ioctl(s->container, VFIO_GET_API_VERSION) != VFIO_API_VERSION) {
    error_setg(errp, "Invalid VFIO version");
    ret = -EINVAL;
    goto fail_container;
    }
    if (!ioctl(s->container, VFIO_CHECK_EXTENSION, VFIO_TYPE1_IOMMU)) {
    error_setg_errno(errp, errno, "VFIO IOMMU Type1 is not supported");
    ret = -EINVAL;
    goto fail_container;
    }
  2. 打开group,得到该group的信息并设置container。

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
s->group = open(group_file, O_RDWR);
if (s->group == -1) {
error_setg_errno(errp, errno, "Failed to open VFIO group file: %s",
group_file);
g_free(group_file);
ret = -errno;
goto fail_container;
}
g_free(group_file);

/* Test the group is viable and available */
if (ioctl(s->group, VFIO_GROUP_GET_STATUS, &group_status)) {
error_setg_errno(errp, errno, "Failed to get VFIO group status");
ret = -errno;
goto fail;
}

if (!(group_status.flags & VFIO_GROUP_FLAGS_VIABLE)) {
error_setg(errp, "VFIO group is not viable");
ret = -EINVAL;
goto fail;
}

/* Add the group to the container */
if (ioctl(s->group, VFIO_GROUP_SET_CONTAINER, &s->container)) {
error_setg_errno(errp, errno, "Failed to add group to VFIO container");
ret = -errno;
goto fail;
}

/* Enable the IOMMU model we want */
if (ioctl(s->container, VFIO_SET_IOMMU, VFIO_TYPE1_IOMMU)) {
error_setg_errno(errp, errno, "Failed to set VFIO IOMMU type");
ret = -errno;
goto fail;
}
  1. 设置 DMA mapping,这里指定将设备视角下从0开始的1MB 映射到了进程地址空间内从 dma_map.vaddr 开始的 1MB。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int qemu_vfio_do_mapping(QEMUVFIOState *s, void *host, size_t size,
uint64_t iova, Error **errp)
{
struct vfio_iommu_type1_dma_map dma_map = {
.argsz = sizeof(dma_map),
.flags = VFIO_DMA_MAP_FLAG_READ | VFIO_DMA_MAP_FLAG_WRITE,
.iova = iova,
.vaddr = (uintptr_t)host,
.size = size,
};

if (ioctl(s->container, VFIO_IOMMU_MAP_DMA, &dma_map)) {
error_setg_errno(errp, errno, "VFIO_MAP_DMA failed");
return -errno;
}
return 0;
}
  1. 得到直通设备描述符,获取其各个region信息和irq信息, 设置eventfd。
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
s->device = ioctl(s->group, VFIO_GROUP_GET_DEVICE_FD, device);
if (ioctl(s->device, VFIO_DEVICE_GET_INFO, &device_info)) {
error_setg_errno(errp, errno, "Failed to get device info");
ret = -errno;
goto fail;
}
s->config_region_info = (struct vfio_region_info) {
.index = VFIO_PCI_CONFIG_REGION_INDEX,
.argsz = sizeof(struct vfio_region_info),
};
if (ioctl(s->device, VFIO_DEVICE_GET_REGION_INFO, &s->config_region_info)) {
error_setg_errno(errp, errno, "Failed to get config region info");
ret = -errno;
goto fail;
}
if (ioctl(s->device, VFIO_DEVICE_GET_IRQ_INFO, &irq_info)) {
error_setg_errno(errp, errno, "Failed to get device interrupt info");
return -errno;
}
if (!(irq_info.flags & VFIO_IRQ_INFO_EVENTFD)) {
error_setg(errp, "Device interrupt doesn't support eventfd");
return -EINVAL;
}
*irq_set = (struct vfio_irq_set) {
.argsz = irq_set_size,
.flags = VFIO_IRQ_SET_DATA_EVENTFD | VFIO_IRQ_SET_ACTION_TRIGGER,
.index = irq_info.index,
.start = 0,
.count = 1,
};

*(int *)&irq_set->data = event_notifier_get_fd(e);
r = ioctl(s->device, VFIO_DEVICE_SET_IRQS, irq_set);
  1. 重置设备,这样直通设备就连接到虚拟机内的操作系统中了
    1
    ioctl(device, VFIO_DEVICE_RESET)

VFIO相关内核模块分析

vfio-pci 驱动分析

设备绑定vfio-pci 驱动

从上面的步骤上可以看出, 设备需要先与原来的驱动解绑, 并重新与vfio-pci 驱动绑定
vfio-pci 驱动初始化函数为 vfio_pci_init, 该函数中注册了一个名为 vfio_pci_driver 的 PCI 驱动。vfio_pci_driver 的 probe 函数为 vfio_pci_probe,当设备与 vfio-pci 驱动绑定时会调用该函数。

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
-+ vfio_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
\ - vfio_pci_core_device *vdev = kzalloc(sizeof(*vdev), GFP_KERNEL);
"分配一个vfio_pci_core_device结构赋值到vdev中"
| -+ vfio_pci_core_init_device(vdev, pdev, &vfio_pci_ops);
\ -+ vfio_init_group_dev(&vdev->vdev, &pdev->dev, vfio_pci_ops);
\ - pdev->dev->ops = vfio_pci_ops; "vfio_pci_ops 保存了vfio_device结构体的操作回调函数"
| - vdev->pdev = pdev; "vdev->pdev 指向 PCI 物理设备指针"
| -+ dev_set_drvdata(&pdev->dev, vdev);
\ - dev->driver_data = vdev; "vdev 赋值到pdev->dev->driver_data 中"
| -+ vfio_pci_core_register_device(vdev);
\ - vfio_assign_device_set(&vdev->vdev, vdev) "pdev->bus 为 root bus 时"
| - ret = vfio_pci_vf_init(vdev);
| - ret = vfio_pci_vga_init(vdev);
| - vfio_pci_set_power_state(vdev, PCI_D0);
| -+ ret = vfio_register_group_dev(&vdev->vdev); "vdev->vdev 表示 vfio_pci_core_device 成员 vfio_device "
\ -+ __vfio_register_dev(device, vfio_group_find_or_alloc(device->dev));
\ -+ vfio_group_find_or_alloc(device->dev)
\ - iommu_group = iommu_group_get(dev); "iommu_group表示iommu驱动层的group,
系统在设备初始化的时候会为每一个PCI设备设置其对应的group,
保存在表示设备的device结构体的iommu_group成员中"
| -|+ if (!iommu_group)
<- \ - return -EINVAL
| - !iommu_capable(dev->bus, IOMMU_CAP_CACHE_COHERENCY)
| - group = vfio_group_get_from_iommu(iommu_group); "根据iommu层的group生成一个vfio层的group"
| -|+ if (!group)
\ -+ group = vfio_create_group(iommu_group, VFIO_IOMMU); "没找到的话生成一个"
"vfio_group只会在第一个设备进行直通的时候创建,所以这里vfio_group_get_from_iommu可能返回空,
也可能返回一个已经创建好的vfio_group。如果返回空则还需要调用vfio_create_group创建vfio_group。"
\ -+ group = vfio_group_alloc(iommu_group, type);
\ - vfio_group *group = kzalloc(sizeof(*group), GFP_KERNEL);
| - group->dev.devt = MKDEV(MAJOR(vfio.group_devt), minor);
"分配一个minor, 并给group_dev"
| - group->dev.class = vfio.class;
| - cdev_init(&group->cdev, &vfio_group_fops); "vfio_group的回调函数"
"这个设备在VFS下的操作接口存放在vfio_group_fops结构体中的各个回调函数成员中"
| - INIT_LIST_HEAD(&group->device_list);
| - group->iommu_group = iommu_group;
| - group->type = VFIO_IOMMU;
| - dev_set_name(&group->dev, "%s%d", "", iommu_group_id(iommu_group));
"创建一个设备,这个设备就是“/dev/vfio/$group_id”,其中$group_id表示group的数字,
vfio_group的成员dev保存了这个设备,用户态程序通过该设备控制vfio_group"
| - cdev_device_add(&group->cdev, &group->dev);
| - device->group = group; "vfio_device只能属于一个vfio_group"
| - list_add(&device->group_next, &group->device_list);
"挂入group->device_list, device_list将属于该vfio_group的vfio_device链接起来"
| - group->dev_counter++;

vfio_group与vfio_device结构
一个vfio_device,绑定到一个vfio_group上,如果vfio_group还没有创建,则也会创建一个vfio_group。

vfio_group的device_list是一个链表,将属于该vfio_group的vfio_device链接起来,vfio_group的container成员存放了该vfio_group连接到的container。所有的vfio_group通过vfio_next成员连接到全局变量vfio.group_list上。vfio_group的iommu_group指向iommu层,表示一个group的iommu_group结构体。

vfio_device用来表示VFIO层面的一个设备。其中,dev成员表示物理设备;ops表示存放其操作接口回调函数,该ops被设置成了vfio_pci_ops;group表示所属的vfio_group,group_next会用来链接同一个vfio_group中的设备;

vfio_group会在/dev/vfio下面创建设备提供给用户态访问
VFIO设备需要用户在vfio group的fd上(打开/dev/vfio/$groupid返回的fd)调用ioctl(VFIO_GROUP_GET_DEVICE_FD),内核在处理这个请求时会为vfio_device创建一个file并关联到一个fd上,这个file的操作接口回调函数保存在vfio_device_fops中,file的私有结构会设置为vfio_device,这个ioctl返回的设备fd即可被用户态用来与vfio设备通信。

vfio-pci接口

vfio-pci驱动作为桥梁,是VFIO模块与PCI设备驱动之间沟通的途径,其向上提供VFIO的接口,向下控制PCI物理设备的行为
如设置或者获取PCI的配置空间、寄存器信息、中断信息等。
用户态程序通常首先通过group fd的ioctl(VFIO_GROUP_GET_DEVICE_FD)接口获取一个VFIO设备的fd,然后在设备fd上对设备进行控制,VFIO驱动模块调用vfio_group_get_device_fd处理该接口请求。

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
-+ vfio_group_get_device_fd(struct vfio_group *group, char *buf) "参数buf保存了用户态进程指定的物理设备地址 bdf"
\ - device = vfio_device_get_from_name(group, buf);
"函数根据该地址从vfio_group找到对应的vfio_device赋值到device变量中"
| - fdno = get_unused_fd_flags(O_CLOEXEC); "获得一个空闲fd"
| -+ filep = vfio_device_open(device);
\ -+ vfio_device_assign_container(device);
\ - vfio_group *group = device->group;
| -|+ if (!group->container || !group->container->iommu_driver)
<-\ - return -EINVAL "group 必须有container, 且group container 必须有iommu_driver"
| -|+ if group->type == VFIO_NO_IOMMU && !capable(CAP_SYS_RAWIO)
<-\ - return -EPERM; "group 为 VFIO_NO_IOMMU时 必须具有CAP_SYS_RAWIO 能力"
| - device->open_count++;
| - device->kvm = device->group->kvm;
| -+ device->ops->open_device(device) "vfio_pci_ops.open_device = vfio_pci_open_device 回调"
\ -+ vfio_pci_open_device(vfio_device *core_vdev)
\ - vfio_pci_core_device *vdev = container_of(core_vdev, struct vfio_pci_core_device, vdev);
| -+ vfio_pci_core_enable(vdev);
\ - pci_enable_device(pdev); "调用pci_enable_device将设备使能,
每个PCI设备的驱动都需要调用pci_enable_device,vfio-pci驱动作为PCI物理驱动的接管者也需要调用该函数"
| - vfio_config_init(vdev); "根据物理设备的配置信息生成vfio_pci_device的配置信息,
如vfio_pci_device结构体的pci_config_map保存物理设备的配置空间数据,
rbar数组保存物理设备的7个BAR数据"
| - "最后根据VFIO自己的情况对PCI物理设备的配置空间做一些调整"
| - filep = anon_inode_getfile("[vfio-device]", &vfio_device_fops, device, O_RDWR);
"获得一个文件结构并设置该文件结构的操作接口为vfio_device_fops"
| - fd_install(fdno, filep); "将该空闲fd和文件结构关联起来"

ioctl(VFIO_GROUP_GET_DEVICE_FD)接口返回vfio_device的fd之后,用户态可以通过这个fd控制VFIO设备,内核对应的操作函数存放在vfio_device_fops结构体中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static const struct file_operations vfio_device_fops = {
.owner = THIS_MODULE,
.release = vfio_device_fops_release,
.read = vfio_device_fops_read,
.write = vfio_device_fops_write,
.unlocked_ioctl = vfio_device_fops_unl_ioctl,
.compat_ioctl = compat_ptr_ioctl,
.mmap = vfio_device_fops_mmap,
};
static ssize_t vfio_device_fops_write(struct file *filep,
const char __user *buf,
size_t count, loff_t *ppos)
{
struct vfio_device *device = filep->private_data;

if (unlikely(!device->ops->write))
return -EINVAL;

return device->ops->write(device, buf, count, ppos); // 实际还是调用 vfio_pci_ops 的函数
}

从 vfio_device_fops 结构体定义以及 vfio_device_fops_write 函数的定义可以看出,VFIO 设备向用户态导出的接口只是作为一个代理,调用了 vfio_device 的 ops 成员中的对应函数。从之前的分析可知,vfio_device 的 ops 为 vfio_pci_ops,本质上用户态调用的 VFIO 设备接口实际会调用 VFIO PCI 设备的接口。

1
2
3
4
5
6
7
8
9
10
11
12
static const struct vfio_device_ops vfio_pci_ops = {
.name = "vfio-pci",
.open_device = vfio_pci_open_device,
.close_device = vfio_pci_core_close_device,
.ioctl = vfio_pci_core_ioctl,
.device_feature = vfio_pci_core_ioctl_feature,
.read = vfio_pci_core_read,
.write = vfio_pci_core_write,
.mmap = vfio_pci_core_mmap,
.request = vfio_pci_core_request,
.match = vfio_pci_core_match,
};

vfio_pci_ops处理ioctl的函数是vfio_pci_core_ioctl,接下来分析几个重要的ioctl。

上一节描述的 VFIO 编程中的第四步涉及了 VFIO 设备的接口 ioctl(VFIO_DEVICE_GET_INFO)ioctl(VFIO_DEVICE_GET_REGION_INFO)ioctl(VFIO_DEVICE_GET_IRQ_INFO)

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
if (cmd == VFIO_DEVICE_GET_INFO) {
struct vfio_device_info info;
struct vfio_info_cap caps = { .buf = NULL, .size = 0 };
unsigned long capsz;
int ret;

minsz = offsetofend(struct vfio_device_info, num_irqs);
capsz = offsetofend(struct vfio_iommu_type1_info, cap_offset);

if (copy_from_user(&info, (void __user *)arg, minsz))
return -EFAULT;

if (info.argsz >= capsz) {
minsz = capsz;
info.cap_offset = 0;
}

info.flags = VFIO_DEVICE_FLAGS_PCI; //flags成员表示设备的一些特性

if (vdev->reset_works)
info.flags |= VFIO_DEVICE_FLAGS_RESET;

info.num_regions = VFIO_PCI_NUM_REGIONS + vdev->num_regions; //num_regions表示设备的内存区域个数
info.num_irqs = VFIO_PCI_NUM_IRQS; //num_irqs表示设备支持中断类型个数。

ret = vfio_pci_info_zdev_add_caps(vdev, &caps);

if (caps.size) {
info.flags |= VFIO_DEVICE_FLAGS_CAPS;
if (info.argsz < sizeof(info) + caps.size) {
info.argsz = sizeof(info) + caps.size;
} else {
vfio_info_cap_shift(&caps, sizeof(info));
copy_to_user((void __user *)arg + sizeof(info), caps.buf, caps.size))
info.cap_offset = sizeof(info);
}
kfree(caps.buf);
}

return copy_to_user((void __user *)arg, &info, minsz) ? -EFAULT : 0;

ioctl(VFIO_DEVICE_GET_REGION_INFO)接口用来返回VFIO设备的各个内存区域信息,内存区域信息用vfio_region_info结构体表示。

1
2
3
4
5
6
7
8
9
10
11
12
struct vfio_region_info {
__u32 argsz; //argsz表示参数的大小,是输入参数
__u32 flags; //flags表明该内存区域允许的操作,是输出参数
#define VFIO_REGION_INFO_FLAG_READ (1 << 0) /* Region supports read */
#define VFIO_REGION_INFO_FLAG_WRITE (1 << 1) /* Region supports write */
#define VFIO_REGION_INFO_FLAG_MMAP (1 << 2) /* Region supports mmap */
#define VFIO_REGION_INFO_FLAG_CAPS (1 << 3) /* Info supports caps */
__u32 index; //index表示ioctl调用时的内存区域索引,是输入参数
__u32 cap_offset; //输入参数, capbility info 的偏移
__u64 size; // size表示region的大小,是输出参数
__u64 offset; //offset表示内存区域在VFIO设备文件对应的偏移,是输出参数。
};

VFIO设备的fd实现了类似普通文件的访问功能,可以在fd上使用write/read等系统调用,需要访问哪个内存空间就提供对应的vfio_region_info的offset作为参数

用户态获取 PCI 配置空间的信息之后,可以使用 write/read 系统调用读写该区域,这需要使用这里的 offset 作为读写的位置。VFIO 设备的读写函数分别是 vfio_pci_write 和 vfio_pci_read,这两个函数最终都调用了函数 vfio_pci_rw。

1
2
3
4
5
6
7
8
9
10
11
-+ vfio_pci_rw(vfio_pci_core_device *vdev, char __user *buf, size_t count, loff_t *ppos, bool iswrite)
\ - index = VFIO_PCI_OFFSET_TO_INDEX(*ppos);
"根据VFIO_PCI_OFFSET_TO_INDEX宏得到用户态访问的内存区域的索引,然后根据这个索引再去做具体的访问"
| -+ swith index
\ -+ case VFIO_PCI_CONFIG_REGION_INDEX:
\ -+ vfio_pci_config_rw(vdev, buf, count, ppos, iswrite);
\ -+ vfio_config_do_rw(vdev, buf, count, &pos, iswrite);
\ - copy_from_user(&val, buf, count)
| - cap_id = vdev->pci_config_map[*ppos]; "从map信息里找到 offset 对应的cap_id"
| - "根据cap_id选择 perm 回调"
| - perm->writefn(vdev, *ppos, count, perm, offset, val);
vfio驱动分析

VFIO模块的初始化函数vfio_init会注册一个misc设备vfio_dev。
从定义中可以看到,该misc设备的文件操作接口保存在vfio_fops中,并且会创建一个设备节点/dev/vfio/vfio,该设备即用来与用户态通信,与/dev/kvm类似。

vfio iommu driver是VFIO模块与iommu driver模块的中间层。

/dev/vfio/vfio设备的文件操作接口为vfio_fops,其open回调函数为vfio_fops_open。

1
2
3
4
5
6
7
8
9
static struct vfio {
struct class *class;
struct list_head iommu_drivers_list; // 所有的vfio iommu driver都会链接到iommu_drivers_list成员链表上
struct mutex iommu_drivers_lock;
struct list_head group_list; //所有的vfio_group都会链接到group_list成员上。
struct mutex group_lock;
struct ida group_ida;
dev_t group_devt;
} vfio;

每一个用户态进程在打开/dev/vfio/vfio时内核就会为其分配一个vfio_container结构体作为该进程所有VFIO设备的载体。

1
2
3
4
5
6
7
8
static int vfio_fops_open(struct inode *inode, struct file *filep)
{
struct vfio_container *container;
container = kzalloc(sizeof(*container), GFP_KERNEL); // 打开/dev/vfio/vfio 时会创建container
INIT_LIST_HEAD(&container->group_list);
filep->private_data = container; // file的私有数据保存了container
return 0;
}

group附加到container

通过打开/dev/vfio/vfio,可以获得一个container的fd (file 私有数据保存了container),通过打开/dev/vfio/$groupid,可以获得一个group的fd,group提供了ioctl(VFIO_GROUP_SET_CONTAINER)接口来将group附加到container上去,内核调用vfio_group_set_container函数处理这个接口请求

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
-+ vfio_group_set_container(group, container_fd);
\ - fd f = fdget(container_fd);
| - container = f.file->private_data; "从file 私有数据取得container"
| -|+ if group->type == VFIO_IOMMU
\ -+ iommu_group_claim_dma_owner(group->iommu_group, f.file); "dma "
\ - __iommu_group_alloc_blocking_domain(group)
| - __iommu_group_set_domain(group, group->blocking_domain);
| - group->owner = f.file; "file 设置为其owner"
| - driver = container->iommu_driver;
"这个地方driver 是qemu 通过 ioctl(VFIO_SET_IOMMU) 设置的, 为 VFIO_TYPE1_IOMMU 类型,
kernel 需要打开该驱动配置 CONFIG_VFIO_IOMMU_TYPE1=y"
| -+ driver->ops->attach_group(container->iommu_data, group->iommu_group, VFIO_IOMMU);
\ -+ vfio_iommu_type1_attach_group(iommu_data, iommu_group *iommu_group, VFIO_IOMMU))
\ - vfio_iommu_find_iommu_group(iommu, iommu_group)) "必须能找到才行"
| - group->iommu_group = iommu_group;
| - iommu_group_for_each_dev(iommu_group, &bus, vfio_bus_type);
"遍历group->devices, 根据device->bus 类型填充bus 参数"
| - vfio_domain *domain = kzalloc(sizeof(*domain), GFP_KERNEL);
| -+ domain->domain = iommu_domain_alloc(bus); "分配一个domain"
\ ---+ domain->ops = bus->iommu_ops->default_domain_ops;
| -+ iommu_attach_group(domain->domain, group->iommu_group);
\ -+ __iommu_attach_group(domain, group);
\ -+ __iommu_group_for_each_dev(group, domain, iommu_group_do_attach_device);
"遍历group->devices, 调用iommu_group_do_attach_device(dev, domain)"
\ -+ iommu_group_do_attach_device(dev, domain)
\ -+ __iommu_attach_device(domain, dev);
\ - domain->ops->attach_dev(domain, dev); "这个跟iommu 硬件有关, 下面会详细分析"
| - INIT_LIST_HEAD(&domain->group_list);
| - list_add(&group->next, &domain->group_list); "将group 挂到domain的group_list链表下"
| - list_add(&domain->next, &iommu->domain_list); "将domain 挂到 iommu domain_list 链表下"

调用了attach_group回调函数之后, group下面的所有设备信息都会写入到IOMMU硬件context表中

重点关注下跟硬件相关的操作

  1. domain->ops = bus->iommu_ops->default_domain_ops;
  2. domain->ops->attach_dev(domain, dev);

首先iommu 是一个硬件设备, 设备就需要挂到总线上.
设备驱动加载时, 会调用相应驱动的probe 函数
以arm 举例: 其iommu 设备为smmu, 挂到了platform 总线上, 会注册 arm_smmu_ops 到 platform bus 的iommu_ops 成员上

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
static struct iommu_ops arm_smmu_ops = {
.capable = arm_smmu_capable,
.domain_alloc = arm_smmu_domain_alloc,
.probe_device = arm_smmu_probe_device,
.release_device = arm_smmu_release_device,
.probe_finalize = arm_smmu_probe_finalize,
.device_group = arm_smmu_device_group,
.of_xlate = arm_smmu_of_xlate,
.get_resv_regions = arm_smmu_get_resv_regions,
.put_resv_regions = generic_iommu_put_resv_regions,
.def_domain_type = arm_smmu_def_domain_type,
.pgsize_bitmap = -1UL, /* Restricted during device attach */
.owner = THIS_MODULE,
.default_domain_ops = &(const struct iommu_domain_ops) {
.attach_dev = arm_smmu_attach_dev,
.map_pages = arm_smmu_map_pages,
.unmap_pages = arm_smmu_unmap_pages,
.flush_iotlb_all = arm_smmu_flush_iotlb_all,
.iotlb_sync = arm_smmu_iotlb_sync,
.iova_to_phys = arm_smmu_iova_to_phys,
.enable_nesting = arm_smmu_enable_nesting,
.set_pgtable_quirks = arm_smmu_set_pgtable_quirks,
.free = arm_smmu_domain_free,
}
};
-+ arm_smmu_device_probe(struct platform_device *pdev)
\ -+ iommu_device_register(&smmu->iommu, &arm_smmu_ops, dev);
\ - iommu->ops = ops; "先创建了一个iommu_device 结构, ops 注册为 arm_smmu_ops"
| - list_add_tail(&iommu->list, &iommu_device_list); "挂到 iommu_device_list 链表上"
| -+ arm_smmu_bus_init(&arm_smmu_ops)
\ -+ bus_set_iommu(&platform_bus_type, arm_smmu_ops)
\ - bus->iommu_ops = arm_smmu_ops; "platform 总线, 设置其 iommu_ops 成员为 arm_smmu_ops"

对于arm 来说, domain->ops->attach_dev(domain, dev) 最终会调用到arm smmu 驱动的 arm_smmu_attach_dev 函数
最终通过arm_smmu_init_domain_context(struct iommu_domain *domain, struct arm_smmu_device *smmu, struct device *dev)函数设置了smmu 硬件的context, 关联了设备信息.

vfio iommu 驱动

vfio iommu驱动是VFIO接口和底层iommu驱动之间通信的桥梁,它向上接收来自VFIO的接口请求,向下利用iommu驱动完成DMA重定向功能。下面以vfio iommu type1驱动为例简单分析一下vfio iommu驱动的功能。

每一种vfio iommu驱动都用vfio_iommu_driver结构体表示, 这个结构体ops成员向vfio层导出了一系列的接口函数
vfio iommu type1的驱动初始化函数为 vfio_iommu_type1_init,该函数仅仅调用 vfio_register_iommu_driver 向 vfio 驱动注册一个操作接口为 vfio_iommu_driver_ops_type1的 vfio_iommu_driver。vfio_register_iommu_driver 函数把新创建的这个 vfio_iommu_driver 挂到全局变量 vfio 的 iommu_drivers_list 链表上

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
vfio_register_iommu_driver(&vfio_iommu_driver_ops_type1);
int vfio_register_iommu_driver(const struct vfio_iommu_driver_ops *ops)
{
struct vfio_iommu_driver *driver;
driver = kzalloc(sizeof(*driver), GFP_KERNEL);
driver->ops = ops;
list_add(&driver->vfio_next, &vfio.iommu_drivers_list);
return 0;
}
static const struct vfio_iommu_driver_ops vfio_iommu_driver_ops_type1 = {
.name = "vfio-iommu-type1",
.owner = THIS_MODULE,
.open = vfio_iommu_type1_open,
.release = vfio_iommu_type1_release,
.ioctl = vfio_iommu_type1_ioctl,
.attach_group = vfio_iommu_type1_attach_group,
.detach_group = vfio_iommu_type1_detach_group,
.pin_pages = vfio_iommu_type1_pin_pages,
.unpin_pages = vfio_iommu_type1_unpin_pages,
.register_notifier = vfio_iommu_type1_register_notifier,
.unregister_notifier = vfio_iommu_type1_unregister_notifier,
.dma_rw = vfio_iommu_type1_dma_rw,
.group_iommu_domain = vfio_iommu_type1_group_iommu_domain,
.notify = vfio_iommu_type1_notify,
};

首先分析open回调函数。
每一个container都会打开一个vfio iommu driver

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
static void *vfio_iommu_type1_open(unsigned long arg)
{
struct vfio_iommu *iommu;
iommu = kzalloc(sizeof(*iommu), GFP_KERNEL); //分配了一个vfio_iommu结构赋值到iommu,然后进行一些初始化
switch (arg) {
case VFIO_TYPE1_IOMMU:
break;
case VFIO_TYPE1_NESTING_IOMMU:
iommu->nesting = true;
fallthrough;
case VFIO_TYPE1v2_IOMMU:
iommu->v2 = true;
break;
INIT_LIST_HEAD(&iommu->domain_list); //domain_list成员用来链接所有的vfio_domain
INIT_LIST_HEAD(&iommu->iova_list);
iommu->dma_list = RB_ROOT; // dma_list用来表示该container中DMA重定向的映射表,也就是GPA到HPA的转换
iommu->dma_avail = dma_entry_limit;
iommu->container_open = true;
BLOCKING_INIT_NOTIFIER_HEAD(&iommu->notifier);
init_waitqueue_head(&iommu->vaddr_wait);
iommu->pgsize_bitmap = PAGE_MASK;
INIT_LIST_HEAD(&iommu->emulated_iommu_groups);

return iommu;
}

vfio_ioctl_set_iommu的参数arg是用户态进程指定的vfio iommu驱动类型,如VFIO_TYPE1_IOMMUVFIO_TYPE1v2_IOMMU等。vfio_ioctl_set_iomm函数遍历vfio.iommu_drivers_list链表,调用每一个vfio_iommu_driver的回调函数,如果vfio_iommu_driver的操作函数ioctl(VFIO_CHECK_EXTENSION)返回值小于等于零则继续找下一个,如果返回值大于零则表明支持用户指定的vfio iommu驱动类型。接着调用vfio_iommu_driver的操作open函数返回一个结构体,对vfioiommu driver typ1来说,这个结构体是vfio_iommu,调用__vfio_container_attach_groups将container上的所有group都附加到该vfio iommu驱动上。最后设置container的iommu_driver成员为vfio_iommu_driver,iommu_data成员为具体vfio iommu驱动返回的结构体。

通过在container的fd上调用ioctl(VFIO_IOMMU_MAP_DMA)可以创建一个设备I/O地址(IOVA)到宿主机物理地址的映射,这样设备在进行DMA操作时使用的地址都是IOVA,会经过IOMMU的DMA重映射进行地址转换将IOVA转换成宿主机物理地址。container上的所有设备都要直通给虚拟机,这些设备使用同一份IOVA到宿主机物理地址的映射表。container_fd的ioctl处理函数是vfio_fops_unl_ioctl

  • vfio_iommu可以认为是和container概念相对应的iommu数据结构,在虚拟化场景下每个虚拟机的物理地址空间映射到一个vfio_iommu上。
  • vfio_group可以认为是和group概念对应的iommu数据结构,它指向一个iommu_group对象,记录了着iommu_group的信息。
  • vfio_domain这个概念尤其需要注意,这里绝不能把它理解成一个虚拟机domain,它是一个与DRHD(即IOMMU硬件)相关的概念, 它的出现就是为了应对多IOMMU硬件的场景,我们知道在大规格服务器上可能会有多个IOMMU硬件,不同的IOMMU硬件有可能存在差异, 例如IOMMU 0支持IOMMU_CACHE而IOMMU 1不支持IOMMU_CACHE(当然这种情况少见,大部分平台上硬件功能是具备一致性的),这时候我们不能直接将分别属于不同IOMMU硬件管理的设备直接加入到一个container中, 因为它们的IOMMU页表SNP bit是不一致的。 因此,一种合理的解决办法就是把一个container划分多个vfio_domain,当然在大多数情况下我们只需要一个vfio_domain就足够了。 处在同一个vfio_domain中的设备共享IOMMU页表区域,不同的vfio_domain的页表属性又可以不一致,这样我们就可以支持跨IOMMU硬件的设备直通的混合场景。
vfio_fops_unl_ioctl VFIO_IOMMU_MAP_DMA

从vfio_fops_unl_ioctl函数中可以看到,VFIO模块本身只处理3个ioctl,即VFIO_GET_API_VERSIONVFIO_CHECK_EXTENSION以及VFIO_SET_IOMMU。对于其他ioctl,该函数只是调用了container中保存的iommu_driver成员对应的ioctl函数,并且以container的iommu_data(如vfio_iommu)作为参数

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
...
    default:
driver = container->iommu_driver;
data = container->iommu_data;

if (driver) /* passthrough all unrecognized ioctls */
ret = driver->ops->ioctl(data, cmd, arg);
"对于vfio iommu type1驱动来说,它的ioctl函数是 vfio_iommu_type1_ioctl"

struct vfio_iommu_type1_dma_map {
__u32 argsz;
__u32 flags;
#define VFIO_DMA_MAP_FLAG_READ (1 << 0) /* readable from device */
#define VFIO_DMA_MAP_FLAG_WRITE (1 << 1) /* writable from device */
#define VFIO_DMA_MAP_FLAG_VADDR (1 << 2)
__u64 vaddr; /* vaddr表示用户态进程的虚拟地址 */
__u64 iova; /* iova表示设备使用的I/O地址 */
__u64 size; /* size表示其大小 */
};

struct vfio_dma {
struct rb_node node; "node用来将vfio_dma结构链接到以vfio_iommu的dma_list成员为根的二叉树中"
dma_addr_t iova; "iova表示设备I/O地址"
unsigned long vaddr; /* Process virtual addr */
size_t size; /* Map size (bytes) */
int prot; /* IOMMU_READ/WRITE */
bool iommu_mapped;
bool lock_cap; /* capable(CAP_IPC_LOCK) */
bool vaddr_invalid;
struct task_struct *task;
struct rb_root pfn_list; /* Ex-user pinned pfn list */
unsigned long *bitmap;
};

-+ vfio_iommu_type1_ioctl
\ -+ case VFIO_IOMMU_MAP_DMA "需要指定一个vfio_iommu_type1_dma_map类型的参"
\ -+ vfio_iommu_type1_map_dma(iommu, arg);
\ - copy_from_user(vfio_iommu_type1_dma_map &map, (void __user *)arg, minsz))
| -+ vfio_dma_do_map(iommu, &map);
\ - vfio_dma * dma = vfio_find_dma(iommu, iova, size);
"使用vfio_dma结构体表示一段虚拟地址到设备I/O地址的映射关系"
| -+ vfio_pin_map_dma(iommu, dma, size);
"在一个while循环中完成映射工作。vfio_pin_pages_remote函数将几个连续的物理内存页面锁在内存中,
vfio_iommu_map将锁住的内存与指定的设备I/O地址进行映射, 会调用IOMMU硬件层的iommu_map完成实际的映射工作"
\ ---+ ops->map_pages(domain, iova, paddr, pgsize, count, prot, gfp, mapped)
\ -+ arm_smmu_map_pages(...) "以arm 为例, 调用到 arm_smmu_map_pages"

QEMU VFIO 设备具现化过程

vfio_realize是VFIO设备的具现函数

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
-+ vfio_realize(PCIDevice *pdev, Error **errp)
\ - vdev->vbasedev.sysfsdev = g_strdup_printf("/sys/bus/pci/devices/%04x:%02x:%02x.%01x",
vdev->host.domain, vdev->host.bus, vdev->host.slot, vdev->host.function);
| - vdev->vbasedev.name = g_path_get_basename(vdev->vbasedev.sysfsdev);
| - vdev->vbasedev.ops = &vfio_pci_ops;
| - tmp = g_strdup_printf("%s/iommu_group", vdev->vbasedev.sysfsdev);
| - readlink(tmp, group_path, sizeof(group_path))
| - group_name = basename(group_path);
| - sscanf(group_name, "%d", &groupid)
"通过/sys/bus/pci/devices/%04x:%02x:%02x.%01x/iommu_group readlink后获取 group的真实路径, 路径最后为groupid"
| -+ group = vfio_get_group(groupid, pci_device_iommu_address_space(pdev), errp);
"vfio_get_group的作用就是创建container以及将直通设备所属的group添加到container中"
\ - qemu_open("/dev/vfio/$groupid") "打开/dev/vfio/$groupid设备"
| -+ vfio_connect_container(group, pci_device_iommu_address_space(pdev))
\ - space = vfio_get_address_space(as);
| - qemu_open("/dev/vfio/vfio")
| -+ vfio_init_container(container, groupfd) "创建一个container"
\ - iommu_type = vfio_get_iommu_type(container, errp);
| - ioctl(group_fd, VFIO_GROUP_SET_CONTAINER, &container->fd) "设置成group的container"
| - ioctl(container->fd, VFIO_SET_IOMMU, iommu_type) "设置iommu_type, 一般为 vfio_iommu_type1 "
| - vfio_kvm_device_add_group(group);
"kvm_vm_ioctl(kvm_state, KVM_CREATE_DEVICE), 创建KVM VFIO device"
| - container->listener = vfio_memory_listener;
| - memory_listener_register(&container->listener, container->space->as);
"注册一个名为vfio_memory_listener的内存监听器,
用来监听虚拟机内存的状态改变,并完成直通设备的DMA地址到虚拟机地址的映射"
| -+ vfio_get_device(group, vdev->vbasedev.name, &vdev->vbasedev) "得到直通设备的fd"
\ - fd = ioctl(group->fd, VFIO_GROUP_GET_DEVICE_FD, name);
| - ioctl(fd, VFIO_DEVICE_GET_INFO, &dev_info);
| - vbasedev->fd = fd;
| - vbasedev->group = group;
| - vbasedev->num_irqs = dev_info.num_irqs; "ioctl(VFIO_DEVICE_GET_INFO)返回设备的中断和内存区域个数
分别保存在vbasedev->num_irqs和vbasedev->num_regions中"
| - vbasedev->num_regions = dev_info.num_regions;
| - QLIST_INSERT_HEAD(&group->device_list, vbasedev, next);
| -+ vfio_populate_device(vdev, &err);
\ -+ for (i = 0; i < (VFIO_PCI_ROM_REGION_INDEX = 6); i++)
"对6个BAR依次调用vfio_region_setup函数"
\ - char *name = g_strdup_printf("%s BAR %d", vbasedev->name, i);
| -+ vfio_region_setup(OBJECT(vdev), vbasedev, &vdev->bars[i].region, i, name);
\ - vfio_get_region_info(vbasedev, index, &info);
| - region = &vdev->bars[i].region;
| - region->size = info->size;
| - region->fd_offset = info->offset;
| - region->mem = g_new0(MemoryRegion, 1);
| - memory_region_init_io(region->mem, obj, &vfio_region_ops, region, name, region->size);
"利用直通设备的BAR建立起VFIO虚拟设备的BAR"
| -+ vfio_get_region_info(vbasedev, VFIO_PCI_CONFIG_REGION_INDEX, &reg_info);
"VFIO_PCI_CONFIG_REGION_INDEX是PCI配置空间索引,
不同于BAR内存区域,PCI配置空间值需要获取其大小以及在设备fd描述的文件中的偏移"
| - vdev->config_size = reg_info->size;
| - vdev->config_offset = reg_info->offset;
| - ioctl(vdev->vbasedev.fd, VFIO_DEVICE_GET_IRQ_INFO, &irq_info);
"来获取索引为VFIO_PCI_ERR_IRQ_INDEX的中断信息,通常内核会填充irq_info.count为1,表示支持一个设备错误通知,
当硬件设备发生不可恢复的错误时会通知QEMU"
| - pread(vdev->vbasedev.fd, vdev->pdev.config, MIN(pci_config_size(&vdev->pdev), vdev->config_size),
vdev->config_offset); "获取一份直通设备的配置空间, 放到pdev.config"
| - vdev->emulated_config_bits = g_malloc0(vdev->config_size);
"当虚拟机内部访问配置空间时,如果emulated_config_bits中设置了对应地址的字节,就会直接访问pdev.config中的值"
| - vfio_add_emulated_word(vdev, PCI_DEVICE_ID, vdev->device_id, ~0)
| - vfio_add_emulated_word() ... "接着调用vfio_add_emulated_word写入
设备的配置空间(vdev->pdev.config)及控制数据(vdev->pdev.wmask和vdev->emulated_config_bits),
对一系列配置空间进行微调,使之能够向虚拟机呈现出完整的PCI设备的模样"
| -+ vfio_pci_size_rom(vdev); "处理设备的rom, 主要是为有rom的直通设备创建一个MemoryRegion,
并将这个MemoryRegion注册成虚拟设备的rom。"
| -+ vfio_bars_prepare(vdev); "获取各个BAR的类型、大小等基本信息,"
| -+ vfio_bars_register(vdev);
\ -+ for (i = 0; i < PCI_ROM_SLOT; i++)
\ -+ vfio_bar_register(vdev, i);
\ - VFIOBAR *bar = &vdev->bars[i];
| - bar->mr = g_new0(MemoryRegion, 1);
| - name = g_strdup_printf("%s base BAR %d", vdev->vbasedev.name, nr);
| - memory_region_init_io(bar->mr, OBJECT(vdev), NULL, NULL, name, bar->size);
| - memory_region_add_subregion(bar->mr, 0, bar->region.mem);
| -+ vfio_region_mmap(&bar->region)
"将直通设备的BAR地址空间映射到QEMU中, 保存在VFIORegion.mmaps的mmap成员"
\ -+ for (i = 0; i < bar->region->nr_mmaps; i++)
\ - region->mmaps[i].mmap = mmap(NULL, region->mmaps[i].size, prot,
MAP_SHARED, region->vbasedev->fd, "region绑定的fd, 调用到内核的 vfio_pci_core_mmap"
region->fd_offset + region->mmaps[i].offset);
"会调用到 内核 vfio_pci_core_mmap 函数,
vfio_pci_core_mmap函数中会调用到pci_resource_len,pci_request_selected_regions、
pci_iomap等函数将直通设备对应的MMIO空间映射到内核中,然后再通过内核映射到用户空间中。"
| - memory_region_init_ram_device_ptr(&region->mmaps[i].mem, memory_region_owner(region->mem),
name, region->mmaps[i].size, region->mmaps[i].mmap);
"MemoryRegion 标记为 ram类型"
| -+ memory_region_add_subregion(region->mem, region->mmaps[i].offset, &region->mmaps[i].mem);
"将 region->mmaps[i].mem 加到 bar->region->mem 下"
\ ---+ kvm_set_user_memory_region() "触发 ram类型的 memory_listener 的 region_add 回调,
最终调用kvm_set_user_memory_region, ioctl(KVM_SET_USER_MEMORY_REGION) 建立GPA->HVA->HPA 映射关系"
| - pci_register_bar(&vdev->pdev, nr, bar->type, bar->mr); "为虚拟设备注册BAR"
| -+ vfio_add_capabilities(vdev, errp); "根据直通设备的PCI能力为虚拟机设备添加功能"
| -+ pci_device_set_intx_routing_notifier(&vdev->pdev, vfio_intx_routing_notifier);
| - vfio_register_req_notifier(vdev); "完成虚拟设备的中断初始化。虚拟设备的中断设置和触发"

设备I/O地址空间模拟

vfio_populate_device中会对直通设备的每个BAR调用vfio_region_setup,在调用次函数的时候,第三个参数为&vdev->bars[i].region。虚拟设备的所有BAR信息存放在虚拟设备结构体VFIOPCIDevice的bars数组成员中,其类型为VFIOBAR。VFIOBAR中有一个重要的成员region,类型为VFIORegion,其中存放了虚拟设备的BAR信息,定义如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef struct VFIOBAR {
VFIORegion region;
MemoryRegion *mr;
size_t size;
uint8_t type;
bool ioport;
bool mem64;
QLIST_HEAD(, VFIOQuirk) quirks;
} VFIOBAR;
typedef struct VFIORegion {
struct VFIODevice *vbasedev; // 指向虚拟设备
off_t fd_offset; /* offset of region within device fd */
MemoryRegion *mem;
size_t size; // bar 的基本信息
uint32_t flags; /* VFIO region flags (rd/wr/mmap) */
uint32_t nr_mmaps; //mmap 映射元素的个数
VFIOMmap *mmaps; // mmap 放映射元素的容器
uint8_t nr; /* cache the region number for debug */
} VFIORegion;

vfio_region_setup中,vfio_get_region_info函数会在直通设备fd上调用ioctl(VFIO_DEVICE_GET_REGION_INFO)获取设备BAR内存区域的基本信息,然后复制到VFIORegion中。
从直通设备获取BAR之后,vfio_region_setup会为BAR创建一个MemoryRegion结构体,并使用BAR的信息进行初始化,最后分配VFIORegion的mmaps空间以及初始相关成员。

vfio_region_setup 中调用了 vdev->bars[i].region->mem

1
memory_region_init_io(region->mem, obj, &vfio_region_ops, region, name, region->size)

而vfio_bar_register 中调用了 &vdev->bars[i]->mr

1
2
memory_region_init_io(bar->mr, OBJECT(vdev), NULL, NULL, name, bar->size)
memory_region_add_subregion(bar->mr, 0, bar->region.mem);

一个bar为什么需要两个RemoryRegion?
VFIORegion 的 MemoryRegion 是容器, 是为了挂 region->mmaps[i].mem 子MemoryRegion 用的

下面是一个整体数据结构的框图

PCI设备的BAR空间不是直接呈现给guest的,这是为什么呢?我们知道,PCI设备的I/O地址空间是通过PCI BAR空间报告给操作系统的。那么有两种方式来:

  1. 将设备的PCI BAR空间直接报告给guest,并通过VMCS的I/O bitmap和EPT使guest访问PCI设备的PIO和MMIO都不需要VM-Exit,这样guest操作系统的驱动程序就可以直接访问设备的I/O地址空间。
  2. 建立转换表(重映射)来报告PCI BAR空间给guest,当guest访问PIO或者MMIO时候VMM负责截获并转发I/O请求到设备的I/O地址空间。

上面两种方法中,方法1 是高效和简单的,但实际上却无法运用,原因很简单:
设备的PCI BAR空间在host上由host kernel 分配的,当这个设备呈现给虚拟机后guest kernel又会对其PCI BAR空间进行分配,这样一来两者之间就产生了冲突。 在操作系统看来,这样就发生了资源冲突,很可能停用这个设备,甚至还会造成更加严重的后果。 实际上,由于操作系统修改设备PCI BAR空间的权利,我们应该阻止guest来修改真实设备的PCI BAR地址以防止造成host上PCI设备的BAR空间冲突。

我们知道PCI直通设备的PIO和MMIO访问方式是通过BAR空间给guest操作系统报告的
例如,下面这张PCI网卡
BAR0的起始地址为0xef100000,大小为256k,non-prefetchable表明一般为设备Register区域,
BAR2的地址为 e000,大小为128字节,为设备的PIO区域。

1
2
3
4
5
6
7
8
04:00.0 Ethernet controller: Qualcomm Atheros Killer E2500 Gigabit Ethernet Controller (rev 10)
Subsystem: Gigabyte Technology Co., Ltd Device e000
Control: I/O+ Mem+ BusMaster+ SpecCycle- MemWINV- VGASnoop- ParErr- Stepping- SERR- FastB2B- DisINTx-
Status: Cap+ 66MHz- UDF- FastB2B- ParErr- DEVSEL=fast >TAbort- <TAbort- <MAbort- >SERR- <PERR- INTx+
Latency: 0, Cache Line Size: 64 bytes
Interrupt: pin A routed to IRQ 17
Region 0: Memory at ef100000 (64-bit, non-prefetchable) [size=256K]
Region 2: I/O ports at e000 [size=128]

QEMU在查询到物理设备的PCI BAR空间信息后,会通过vfio_region_mmap对BAR空间进行映射,这样KVM就会为MMIO建立EPT页表,虚拟机后续访问BAR空间就可以不用发生VM-Exit 如此一来就变得很高效。同时还会调用pci_register_bar来注册BAR空间信息, PCI BAR空间重映射是如何完成的呢?

vfio_region_mmap函数中,我们摘取最关键部分进行简单分析。 VFIO内核中提供了一个名为vfio_pci_core_mmap的函数,该函数中调用了remap_pfn_range,将物理设备的PCI BAR重新映射到了进程的虚拟地址空间。 这样进程中,对该区域进行mmap后,拿到虚拟地址后就可以直接通过读写该段地址来访问物理设备的BAR空间。 在该函数中,将该段重映射虚拟地址空间作为RAM Device分配给guest,在memory_region_init_ram_device_ptr函数中会将该段内容标志为RAM, 如此一来KVM会调用kvm_set_user_memory_region为该段内存区域建立GPA到HPA的EPT映射关系,这样一来Guest内部通过MMIO访问设备BAR空间就不用发生VM-Exit。 当QEMU将该段虚拟地址空间分配给guest后,kernel初始化时枚举PCI设备的时候,会将该段内存区域映射到guest的物理地址空间,guest驱动再将该段区域映射到 guest内核虚拟地址空间通过访问该段区域来驱动设备。

guest 访问 直通设备pci bar的GPA时, 发生guest load/store fault时, kvm处理该中断, 需要在host kernel中先找到hva, hva如果不可访问, 会缺页中断, 此时该hva的缺页中断会进入到 vfio_pci_mmap_fault 中, 该函数中对hva进行重新映射, 映射到了该vfio device的物理pci bar mmio 区域的物理地址上, 最后kvm 进行GPA->HPA的页表映射, guest os 再次访问该GPA时就不会再发生guest load/store fault了.

VFIO 中断处理

在guest 未运行时, 即当前cpu运行在VMM 时, 分给guest的直通设备来了中断应该怎样处理?

通过VFIO将PCI设备直通给虚拟机之后,vfio-pci驱动会接管该PCI设备的中断,所以vfio-pci会为设备注册中断处理函数,该中断处理函数需要把中断注入到虚拟机中。
物理机接收到直通设备中断的时候,既可以在内核直接处理注入虚拟机中,也可以交给QEMU处理。
这里只讲内核处理的情况

初始化过程中,QEMU在VFIO虚拟设备的fd上调用ioctl(VFIO_DEVICE_SET_IRQS)设置一个eventfd,
初始化过程中,QEMU还会在虚拟机的fd上调用ioctl(KVM_IRQFD)将前述eventfd与VFIO虚拟设备的中断号联系起来
当vfio-pci驱动接收到直通设备的中断时就会向这个eventfd发送信号, 当eventfd上有信号时kvm 则向虚拟机注入中断。
这样即完成了物理设备触发中断、虚拟机接收中断的流程。

x86 vfio 中断处理

![](attachments/Pasted image 20221206105932.png)

以MSI中断为例,当虚机内部为透传设备配置中断时,会触发vm_exit,这时会调用qemu中vfio_pci_write_config->vfio_msi_enable,在vfio_msi_enable中,主要做了两件事:

  • vfio_add_kvm_msi_virq向KVM中注入监听事件,当虚机写入MSI配置空间时,可以获取到MSI message(包括addredd和data,data中包含了设备在虚机内部使用的中断号),在向KVM注入事件时需要传入虚机内部使用的中断号,这个中断号在Posted Interrupt时会使用。
  • vfio_enable_vectors利用vfio提供的VFIO_DEVICE_SET_IRQS命令为透传设备申请真正的中断,因为中断相关的配置都不允许虚机直接访问,必须借助VFIO提供的IO接口实现。此时,会在物理机内申请中断,中断服务程序在VFIO内实现,服务程序主要激活步骤1中的监听事件,然后再调用Posted Interrupt。

当透传设备产生中断时,vfio_msihandler ISR执行,该函数不做实际的服务程序处理,仅仅通过eventfd_signal激活irqfd_inject,然后最终调用deliver_posted_interrupt向虚机注入中断,中断号即为虚机配置透传设备时的中断号。

DMA重定向

DMA remaping的意思就是设置设备端的内存视图到QEMU进程虚拟地址之间的映射, 这是由函数vfio_listener_region_add完成的

每当内存的拓扑逻辑改变时,都会调用注册在AddressSpace上的所有MemoryListener,这里如果是添加MemoryRegion,会调用到vfio_memory_listenervfio_listener_region_add
vfio_memory_listener 是在vfio_connect_container 阶段设置的

vfio_listener_region_add 的处理流程:

  1. 该函数首先判断MemoryRegionSection是否需要建立映射,调用vfio_listener_skipped_section完成
  2. 只有虚拟机实际的物理内存,也就是对应在QEMU中分配有实际虚拟地址空间的MemoryRegion,才进行DMA Remapping。
  3. 接着算出iova,实际上就是该MemoryRegionSection在AddressSpace中的起始位置,也就是虚拟机物理内存的地址。
  4. 然后算出vaddr,也就是QEMU分配的虚拟机物理内存的虚拟地址,
  5. 最后调用vfio_dma_map,在该函数中调用ioctl(VFIO_IOMMU_MAP_DMA)完成iova到vaddr的映射。

DMA remapping

在设备直通(Device Passthough)的虚拟化场景下,直通设备在工作的时候同样要使用DMA技术来访问虚拟机的主存以提升IO性能。
那么问题来了,直接分配给某个特定的虚拟机的,我们必须要保证直通设备DMA的安全性:

  • 一个VM的直通设备不能通过DMA访问到其他VM的内存,
  • 同时也不能直接访问Host的内存,
    否则会造成极其严重的后果。
    因此,必须对直通设备进行“DMA隔离”和“DMA地址翻译”,
  • 隔离将直通设备的DMA访问限制在其所在VM的物理地址空间内保证不发生访问越界,
  • 地址翻译则保证了直通设备的DMA能够被正确重定向到虚拟机的物理地址空间内。
    为什么直通设备会存在DMA访问的安全性问题呢?原因也很简单:
    由于直通设备进行DMA操作的时候guest驱动直接使用gpa来访问内存的,这就导致如果不加以隔离和地址翻译必然会访问到其他VM的物理内存或者破坏Host内存,因此必须有一套机制能够将gpa转换为对应的hpa这样直通设备的DMA操作才能够顺利完成。

DMA Remapping的引入就是为了解决直通设备DMA隔离和DMA地址翻译的问题
DMA Remapping的硬件能力主要是由IOMMU来提供,通过引入根Context Entry和IOMMU Domain Page Table等机制来实现直通设备隔离和DMA地址转换的目的。那么具体是怎样实现的呢?下面将对其进行介绍。

根据DMA Request是否包含地址空间标志(address-space-identifier)我们将DMA Request分为2类:

  • Requests without address-space-identifier: 不含地址空间标志的DMA Request,这种一般是endpoint devices的普通请求,请求内容仅包含请求的类型(read/write/atomics),DMA请求的address/size以及请求设备的标志符等。
  • Requests with address-space-identifier: 包含地址空间描述标志的DMA Request,此类请求需要包含额外信息以提供目标进程的地址空间标志符(PASID),以及Execute-Requested (ER) flag和 Privileged-mode-Requested 等细节信息。
    为了简单,通常称上面两类DMA请求简称为:Requests-without-PASID和Requests-with-PASID。本节我们只讨论Requests-without-PASID

首先要明确的是DMA Isolation是以Domain为单位进行隔离的,在虚拟化环境下可以认为每个VM的地址空间为一个Domain,直通给这个VM的设备只能访问这个VM的地址空间这就称之为“隔离”。
根据软件的使用模型不同,直通设备的DMA Address Space可能是某个VM的 GPA 或某个进程的虚拟地址空间(由分配给进程的PASID定义)或是由软件定义的一段抽象的IO Virtual Address space (IOVA),总之DMA Remapping就是要能够将设备发起的DMA Request进行DMA Translation重映射到对应的HPA上。下面的图描述了DMA Translation的原理,这和MMU将虚拟地址翻译成物理地址的过程非常的类似。

Host平台上可能会存在一个或者多个DMA Remapping硬件单元,而每个硬件单元支持在它管理的设备范围内的所有设备的DMA Remapping

在服务器上可能集成多个DMA Remapping 硬件单元, 每个硬件单元负责管理挂载到它所在的PCIe Root Port下所有设备的DMA请求。操作系统来初始化和管理这些硬件设备。

为了实现DMA隔离,我们需要对直通设备进行标志,而这是通过PCIe的Request ID来完成的。根据PCIe的SPEC,每个PCIe设备的请求都包含了PCI Bus/Device/Function信息,通过BDF号我们可以唯一确定一个PCIe设备。

同时为了能够记录直通设备和每个Domain的关系,VT-d引入了root-entry/context-entry的概念,通过查询root-entry/context-entry表就可以获得直通设备和Domain之间的映射关系。


Root-table 是一个4K 页,共包含了256项 root-entry,分别覆盖了 PCI 的 Bus0-255,每个 root-entry 占16-Byte,记录了当前 PCI Bus 上的设备映射关系,通过 PCI Bus Number 进行索引。 Root-table 的基地址存放在 Root Table Address Register 当中。Root-entry 中记录的关键信息有:

  • Present Flag:代表着该Bus号对应的Root-Entry是否呈现,CTP域是否初始化;
  • Context-table pointer (CTP):CTP记录了当前Bus号对应点Context Table的地址。

同样每个context-table也是一个4K页,记录一个特定的PCI设备和它被分配的Domain的映射关系,即对应Domain的DMA地址翻译结构信息的地址。 每个root-entry包含了该Bus号对应的context-table指针,指向一个context-table,而每张context-table包又含256个context-entry, 其中每个entry对应了一个Device Function号所确认的设备的信息。通过2级表项的查询我们就能够获得指定PCI被分配的Domain的地址翻译结构信息。Context-entry中记录的信息有:

  • Present Flag:表示该设备对应的context-entry是否被初始化,如果当前平台上没有该设备Preset域为0,索引到该设备的请求也会被block掉。
  • Translation Type:表示哪种请求将被允许;
  • Address Width:表示该设备被分配的Domain的地址宽度;
  • Second-level Page-table Pointer:二阶页表指针提供了DMA地址翻译结构的HPA地址(这里仅针对Requests-without-PASID而言);
  • Domain Identifier: Domain标志符表示当前设备的被分配到的Domain的标志,硬件会利用此域来标记context-entry cache,这里有点类似VPID的意思;
  • Fault Processing Disable Flag:此域表示是否需要选择性的disable此entry相关的remapping faults reporting。

因为多个设备有可能被分配到同一个Domain,这时只需要将其中每个设备context-entry项的 Second-level Page-table Pointer 设置为对同一个Domain的引用, 并将Domain ID赋值为同一个Domian的就行了。

DMA隔离和地址翻译

iommu中引入root-table和context-table的目的比较明显,这些额外的table的存在就是为了记录每个直通设备和其被分配的Domain之间的映射关系。
有了这个映射关系后,DMA隔离的实现就变得非常简单。 IOMMU硬件会截获直通设备发出的请求,然后根据其Request ID查表找到对应的Address Translation Structure即该Domain的IOMMU页表基地址, 这样一来该设备的DMA地址翻译就只会按这个Domain的IOMMU页表的方式进行翻译,翻译后的HPA必然落在此Domain的地址空间内(这个过程由IOMMU硬件中自动完成), 而不会访问到其他Domain的地址空间,这样就达到了DMA隔离的目的。

DMA地址翻译的过程和虚拟地址翻译的过程是完全一致的,唯一不同的地方在于

  • MMU地址翻译是将进程的虚拟地址(HVA)翻译成物理地址(HPA),
  • 而IOMMU地址翻译则是将虚拟机物理地址空间内的GPA翻译成HPA。
    IOMMU页表和MMU页表一样,都采用了多级页表的方式来进行翻译。例如,对于一个48bit的GPA地址空间的Domain而言,其IOMMU Page Table共分4级,每一级都是一个4KB页含有512个8-Byte的目录项。和MMU页表一样,IOMMU页表页支持2M/1G大页内存,同时硬件上还提供了IO-TLB来缓存最近翻译过的地址来提升地址翻译的速度。