├── README.md ├── SUMMARY.md ├── address_space ├── 00-as.md ├── 01-initialization.md ├── 02-MemoryRegion.md ├── 03-AddressSpace1.md ├── 04-FlatView.md ├── 05-RAMBlock.md ├── 06-AddressSpace2.md ├── 07-witness.md └── 08-commit_mr.md ├── apic ├── 00-advance_interrupt_controller.md ├── 01-qemu_emulate.md ├── 02-qemu_kernel_emulate.md └── 03-apicv.md ├── cpu ├── 00-vcpu.md ├── 01-type_cpu.md └── 02-x86_cpu.md ├── device_model ├── 00-devices.md ├── 01-type_register.md ├── 02-register_objectclass.md ├── 03-objectclass_instance.md ├── 04-DeviceClass_instance.md ├── 05-device_oo_model.md ├── 06-interface.md ├── 07-class_obj_interface.md └── pc_dimm │ ├── 00-an_example.md │ ├── 01-pc_dimm_class.md │ ├── 02-pc_dimm_instance.md │ ├── 03-plug.md │ ├── 04-dimm_acpi.md │ └── 05-nvdimm.md ├── fw_cfg ├── 00-qmeu_bios_guest.md ├── 01-spec.md ├── 02-linux_guest.md └── 03-seabios.md ├── lm ├── 00-lm.md ├── 01-migrate_command_line.md ├── 02-infrastructure.md ├── 03-vmsd.md ├── 04-ram_migration.md └── 05-postcopy.md ├── machine ├── 00-mt.md ├── 01-machine_type.md └── 02-pc_machine.md └── memory_backend ├── 00-memory_backend.md ├── 01-class_hierarchy.md └── 02-init_flow.md /README.md: -------------------------------------------------------------------------------- 1 | # understanding qemu 2 | 3 | A place to write down my understanding of qemu. 4 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | * [设备模型](device_model/00-devices.md) 4 | * [设备类型注册](device_model/01-type_register.md) 5 | * [设备类型初始化](device_model/02-register_objectclass.md) 6 | * [设备实例化](device_model/03-objectclass_instance.md) 7 | * [DeviceClass实例化细节](device_model/04-DeviceClass_instance.md) 8 | * [面向对象的设备模型](device_model/05-device_oo_model.md) 9 | * [接口](device_model/06-interface.md) 10 | * [类型、对象和接口之间的转换](device_model/07-class_obj_interface.md) 11 | * [PCDIMM](device_model/pc_dimm/00-an_example.md) 12 | * [PCDIMM类型](device_model/pc_dimm/01-pc_dimm_class.md) 13 | * [PCDIMM实例](device_model/pc_dimm/02-pc_dimm_instance.md) 14 | * [插入系统](device_model/pc_dimm/03-plug.md) 15 | * [创建ACPI表](device_model/pc_dimm/04-dimm_acpi.md) 16 | * [NVDIMM](device_model/pc_dimm/05-nvdimm.md) 17 | * [地址空间](address_space/00-as.md) 18 | * [从初始化开始](address_space/01-initialization.md) 19 | * [MemoryRegion](address_space/02-MemoryRegion.md) 20 | * [AddressSpace Part1](address_space/03-AddressSpace1.md) 21 | * [FlatView](address_space/04-FlatView.md) 22 | * [RAMBlock](address_space/05-RAMBlock.md) 23 | * [AddressSpace Part2](address_space/06-AddressSpace2.md) 24 | * [眼见为实](address_space/07-witness.md) 25 | * [添加MemoryRegion](address_space/08-commit_mr.md) 26 | * [APIC](apic/00-advance_interrupt_controller.md) 27 | * [纯Qemu模拟](apic/01-qemu_emulate.md) 28 | * [Qemu/kernel混合模拟](apic/02-qemu_kernel_emulate.md) 29 | * [APICV](apic/03-apicv.md) 30 | * [Live Migration](lm/00-lm.md) 31 | * [从用法说起](lm/01-migrate_command_line.md) 32 | * [整体架构](lm/02-infrastructure.md) 33 | * [VMStateDescription](lm/03-vmsd.md) 34 | * [内存热迁移](lm/04-ram_migration.md) 35 | * [postcopy](lm/05-postcopy.md) 36 | * [FW_CFG](fw_cfg/00-qmeu_bios_guest.md) 37 | * [规范解读](fw_cfg/01-spec.md) 38 | * [Linux Guest](fw_cfg/02-linux_guest.md) 39 | * [SeaBios](fw_cfg/03-seabios.md) 40 | * [Machine](machine/00-mt.md) 41 | * [MachineType](machine/01-machine_type.md) 42 | * [PCMachine](machine/02-pc_machine.md) 43 | * [CPU](cpu/00-vcpu.md) 44 | * [TYPE_CPU](cpu/01-type_cpu.md) 45 | * [X86_CPU](cpu/02-x86_cpu.md) 46 | * [MemoryBackend](memory_backend/00-memory_backend.md) 47 | * [MemoryBackend类层次结构](memory_backend/01-class_hierarchy.md) 48 | * [MemoryBackend初始化流程](memory_backend/02-init_flow.md) 49 | -------------------------------------------------------------------------------- /address_space/00-as.md: -------------------------------------------------------------------------------- 1 | 地址空间(address space)是qemu内存虚拟化的重要组成部分。不过这个模拟起来还真实有点复杂,也涉及到多个在qemu中关键的数据结构。 2 | 3 | 让我们一点点探索把。 4 | 5 | # 从初始化开始 6 | 7 | 地址空间比较抽象,那我们就从初始化的地方开始吧。 8 | 9 | [从初始化开始](/address_space/01-initialization.md) 10 | 11 | # MemoryRegion 12 | 13 | 初始化时首先遇到的一个数据结构是MemoryRegion,那就以这个为切入点看看地址空间的模拟方式。 14 | 15 | [MemoryRegion](/address_space/02-MemoryRegion.md) 16 | 17 | # AddressSpace Part1 18 | 19 | 初始化时第二个遇到的数据结构就是AddressSpace了。可以说这是整个qemu地址空间模拟的中枢。 20 | 21 | 也正是因为这个是中枢,所以一次估计讲不完,咱就先讲一节。 22 | 23 | [AddressSpace Part1](/address_space/03-AddressSpace1.md) 24 | 25 | # FlatView 26 | 27 | 随着细节的展开,又遇到了一个和AddressSpace相关的数据结构 -- FlatView。 28 | 29 | [FlatView](/address_space/04-FlatView.md) 30 | 31 | # RAMBlock 32 | 33 | 前几个数据结构都是用来描述地址空间的,现在就来看看虚拟的地址空间和真实的内存之间的对应关系 -- RAMBlock。 34 | 35 | [RAMBlock](/address_space/05-RAMBlock.md) 36 | 37 | # AddressSpace Part2 38 | 39 | 讲完了FlatView和RAMBlock,这下可以回来补充AddressSpace的内容了。可以看看有了这两者后地址空间的样子。 40 | 41 | [AddressSpace Part2](/address_space/06-AddressSpace2.md) 42 | 43 | # 眼见为实 44 | 45 | 是时候亲眼看一看地址空间中这些数据结构的样子了。 46 | 47 | [眼见为实](/address_space/07-witness.md) 48 | 49 | # 添加MemoryRegion 50 | 51 | 现在我们对重要数据结构已经有了基本的概念,是时候动态得看看添加一个MemoryRegion时都发生了些什么了。 52 | 53 | [添加MemoryRegion](/address_space/08-commit_mr.md) 54 | -------------------------------------------------------------------------------- /address_space/01-initialization.md: -------------------------------------------------------------------------------- 1 | 刚开始看这部分代码的时候是一脸懵逼的,感觉哪里都连接着哪里不知道从哪里入手。既然如此,那还是从初始化的流程上开始看吧。 2 | 3 | ``` 4 | main() 5 | cpu_exec_init_all() 6 | io_mem_init() 7 | { 8 | memory_region_init_io(&io_mem_rom, NULL, &readonly_mem_ops, NULL, NULL, UINT64_MAX); 9 | memory_region_init_io(&io_mem_unassigned, NULL, &unassigned_mem_ops, NULL, NULL, UINT64_MAX); 10 | memory_region_init_io(&io_mem_notdirty, NULL, ¬dirty_mem_ops, NULL, NULL, UINT64_MAX); 11 | memory_region_clear_global_locking(&io_mem_notdirty); 12 | memory_region_init_io(&io_mem_watch, NULL, &watch_mem_ops, NULL, NULL, UINT64_MAX); 13 | } 14 | memory_map_init() 15 | { 16 | memory_region_init(system_memory, NULL, "system", UINT64_MAX); 17 | address_space_init(&address_space_memory, system_memory, "memory"); 18 | memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io", 65536); 19 | address_space_init(&address_space_io, system_io, "I/O"); 20 | } 21 | ``` 22 | 23 | 这部分代码是在main函数中开始的部分执行的。其中主要使用了两个函数: 24 | 25 | * memory_region_init 26 | * address_space_init 27 | 28 | 既然如此,那我们就挨个了解一下。 29 | -------------------------------------------------------------------------------- /address_space/02-MemoryRegion.md: -------------------------------------------------------------------------------- 1 | MemoryRegion是qemu中管理内存空间的一个重要数据结构,这里我们就花点时间观察一下它。 2 | 3 | # 初始化 -- memory_region_init 4 | 5 | 首先想到的是如何初始化这个数据结构,其中有一个重要的函数入口:memory_region_init。不过如果你打开仔细观察后,这个函数调用了另外两个函数进行初始化: 6 | 7 | * memory_region_initfn 8 | * memory_region_do_init 9 | 10 | 后者比较明显,而前者则隐藏在TYPE_MEMORY_REGION类型的实例化函数中。 11 | 12 | 但是总的来讲也没有做什么太多工作,无非是初始化了一些重要的成员: 13 | 14 | * name 15 | * ops 16 | * size 17 | 18 | 用一个简单的图来展示一下: 19 | 20 | ``` 21 | struct MemoryRegion 22 | +------------------------+ 23 | |name | 24 | | (const char *) | 25 | +------------------------+ 26 | |ops | 27 | | (MemoryRegionOps *) | 28 | +------------------------+ 29 | |addr | 30 | | (hwaddr) | 31 | |size | 32 | | (Int128) | 33 | +------------------------+ 34 | |subregions | 35 | | QTAILQ_HEAD() | 36 | +------------------------+ 37 | ``` 38 | 39 | # 一棵树 -- memory_region_add_subregion 40 | 41 | 细心的朋友在上面这张图中可能已经发现了,在这个数据结构的末尾我列出了另一个重要的成员变量subregions。从字面上就看出一个MemoryRegion可带有多个subregions。 42 | 43 | 这个过程是通过memory_region_add_subregion()函数实现的,这个过程很简单,运行后的结果如下: 44 | 45 | ``` 46 | struct MemoryRegion 47 | +------------------------+ 48 | |name | 49 | | (const char *) | 50 | +------------------------+ 51 | |addr | 52 | | (hwaddr) | 53 | |size | 54 | | (Int128) | 55 | +------------------------+ 56 | |subregions | 57 | | QTAILQ_HEAD() | 58 | +------------------------+ 59 | | 60 | | 61 | ----+-------------------+---------------------+---- 62 | | | 63 | | | 64 | | | 65 | 66 | struct MemoryRegion struct MemoryRegion 67 | +------------------------+ +------------------------+ 68 | |name | |name | 69 | | (const char *) | | (const char *) | 70 | +------------------------+ +------------------------+ 71 | |addr | |addr | 72 | | (hwaddr) | | (hwaddr) | 73 | |size | |size | 74 | | (Int128) | | (Int128) | 75 | +------------------------+ +------------------------+ 76 | |subregions | |subregions | 77 | | QTAILQ_HEAD() | | QTAILQ_HEAD() | 78 | +------------------------+ +------------------------+ 79 | ``` 80 | 81 | 想起了熟悉的pci树,好久没摸pci了,感慨一下。 82 | 83 | # 内存模拟 -- address_space_rw 84 | 85 | 好了,该说到重点的重点了。MemoryRegion的存在是为了模拟内存空间,那究竟是怎么做的呢? 86 | 87 | 还记得刚才说MemoryRegion中有一个成员叫ops么?就是它了。当然最后进行读写的方法有很多,这里只讲其中之一: 88 | 89 | * address_space_rw 90 | 91 | ``` 92 | address_space_rw() 93 | ... 94 | memory_region_dispatch_read() 95 | ... 96 | mr->ops->read() 97 | mr->ops->read_with_attrs() 98 | ``` 99 | 100 | 中间需要跳掉一些东西,因为这些数据结构暂时还没有讲到。这里主要是展示MemoryRegion的根本作用是虚拟机和主机之间地址空间的信息交换。 101 | -------------------------------------------------------------------------------- /address_space/03-AddressSpace1.md: -------------------------------------------------------------------------------- 1 | AddressSpace,看名字就知道很牛。其中也囊括、关联了很多数据结构。这次我们主要讲讲它和MemoryRegion之间的关系。 2 | 3 | # 初始化 -- address_space_init 4 | 5 | 这个初始化函数倒是不长,不过包含了很多重要的内容,这里我们只说两个: 6 | 7 | * QTAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link); 8 | * as->root = root; 9 | 10 | # 不止一个的地址空间 11 | 12 | 从上面第一条看,所有的地址空间都会链接在一个链表上。这就说明了在qemu中不止有一个地址空间,而不是我最开始想象的一个虚拟机公用一个地址空间。 13 | 14 | 在初始化函数中我们看到两个地址空间: address_space_memory, address_space_io。所以此时这个链表长成这样: 15 | 16 | ``` 17 | address_space(global) 18 | +-------------------------------+ 19 | |tqh_last | 20 | |tqh_first | 21 | +-+-----------------------------+ 22 | | 23 | | address_space_memory address_space_io 24 | | (AddressSpace) (AddressSpace) 25 | | +------------------------+ +------------------------+ 26 | +-->|address_spaces_link | ----->|address_spaces_link | 27 | | | | | 28 | +------------------------+ +------------------------+ 29 | ``` 30 | 31 | # AddressSpace和MemoryRegion 32 | 33 | 这个root参数是一个MemoryRegion类型,所以我们得到了AddressSpace和一个MemoryRegion对应。 34 | 35 | 慢着,我们刚才看到了什么?MemoryRegion可以是一颗树是不是?那实际上我们得到的是: 36 | 37 | > AddressSpace和一颗MemoryRegion树对应 38 | 39 | 做了一个简单的图示意: 40 | 41 | ``` 42 | AddressSpace 43 | +-------------------------+ 44 | |name | 45 | | (char *) | 46 | | | 47 | | | MemoryRegion(system_memory/system_io) 48 | +-------------------------+ +------------------------+ 49 | |root | |name | 50 | | (MemoryRegion *) | -------->| (const char *) | 51 | +-------------------------+ +------------------------+ 52 | |addr | 53 | | (hwaddr) | 54 | |size | 55 | | (Int128) | 56 | +------------------------+ 57 | |subregions | 58 | | QTAILQ_HEAD() | 59 | +------------------------+ 60 | | 61 | | 62 | | 63 | | 64 | ----+-------------------+---------------------+---- 65 | | | 66 | | | 67 | | | 68 | 69 | struct MemoryRegion struct MemoryRegion 70 | +------------------------+ +------------------------+ 71 | |name | |name | 72 | | (const char *) | | (const char *) | 73 | +------------------------+ +------------------------+ 74 | |addr | |addr | 75 | | (hwaddr) | | (hwaddr) | 76 | |size | |size | 77 | | (Int128) | | (Int128) | 78 | +------------------------+ +------------------------+ 79 | |subregions | |subregions | 80 | | QTAILQ_HEAD() | | QTAILQ_HEAD() | 81 | +------------------------+ +------------------------+ 82 | ``` 83 | 84 | # 隐藏内容 85 | 86 | 地址空间就这么简单么?再看一眼初始化函数,发现其中还有两个子函数并没有打开。其中包含的奥秘让我们接着打开。 87 | -------------------------------------------------------------------------------- /address_space/04-FlatView.md: -------------------------------------------------------------------------------- 1 | FlatView,从字面上看意思就是 扁平视图。那是谁的偏平视图呢? 2 | 3 | 从大了说是地址空间的偏平视图,但是仔细想想它是对应MemoryRegion的。因为MemoryRegion是一颗树,那相应的就可以有一个偏平空间。 4 | 5 | 说到这儿,其实已经没有什么花头了。不就是一个偏平视图么?但是为了充数,还是要展开讲讲的。突然想到了《三体》世界中的低纬展开,这个MemoryRegion到FlatView的过程还真有点像。 6 | 7 | 这个过程在qemu中分成了两步: 8 | 9 | * generate_memory_topology() 10 | * address_space_set_flatview() 11 | 12 | 后者是将AddressSpace和FlatView进行关联。 13 | 14 | 而前者的工作又分成两部分: 15 | 16 | * render_memory_region 17 | * flatview_add_to_dispatch 18 | 19 | # render_memory_region 20 | 21 | 这个函数的作用就是将树状的MemoryRegion展开成一维的FlatView。 22 | 23 | 因为确实没啥多说的,还是用张图来表示AddressSpace,MemoryRegion和FlatView之间的关系。 24 | 25 | ``` 26 | AddressSpace 27 | +-------------------------+ 28 | |name | 29 | | (char *) | FlatView (An array of FlatRange) 30 | +-------------------------+ +----------------------+ 31 | |current_map | -------->|nr | 32 | | (FlatView *) | |nr_allocated | 33 | +-------------------------+ | (unsigned) | FlatRange FlatRange 34 | | | +----------------------+ 35 | | | |ranges | ------> +---------------------+---------------------+ 36 | | | | (FlatRange *) | |offset_in_region |offset_in_region | 37 | | | +----------------------+ | (hwaddr) | (hwaddr) | 38 | | | +---------------------+---------------------+ 39 | | | |addr(AddrRange) |addr(AddrRange) | 40 | | | | +----------------| +----------------+ 41 | | | | |start (Int128) | |start (Int128) | 42 | | | | |size (Int128) | |size (Int128) | 43 | | | +----+----------------+----+----------------+ 44 | | | |mr |mr | 45 | | | | (MemoryRegion *) | (MemoryRegion *) | 46 | | | +---------------------+---------------------+ 47 | | | 48 | | | 49 | | | 50 | | | MemoryRegion(system_memory/system_io) 51 | +-------------------------+ +----------------------+ 52 | |root | | | root of a MemoryRegion 53 | | (MemoryRegion *) | -------->| | tree 54 | +-------------------------+ +----------------------+ 55 | ``` 56 | 57 | 在AddressSpace中,root和current_map分别指向了树状的地址空间和对应的一维展开。 58 | 59 | 至于为什么要使用这样两种数据结构,暂时还不知道这样做的好处。等我想明白了再回来解释。 60 | 61 | # flatview_add_to_dispatch 62 | 63 | 这个函数的主要任务就是构建AddressSpaceDispatch这个结构了。让人吃惊的是,这是一个庞然大物。 64 | 65 | 恐怕大家是要缩小了才能看清楚的吧。 66 | 67 | ``` 68 | AddressSpaceDispatch 69 | +-------------------------+ 70 | |as | 71 | | (AddressSpace *) | 72 | +-------------------------+ 73 | |mru_section | 74 | | (MemoryRegionSection*)| 75 | | | 76 | | | 77 | | | 78 | | | 79 | | | 80 | +-------------------------+ 81 | |map(PhysPageMap) | MemoryRegionSection[] 82 | | +---------------------+ +---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+ 83 | | |sections |-------->|mr = io_mem_unassigned |mr = io_mem_notdirty |mr = io_mem_rom |mr = io_mem_watch |mr = one mr in tree |mr = subpage_t->iomem | 84 | | | MemoryRegionSection*| | (MemoryRegion *) | (MemoryRegion *) | (MemoryRegion *) | (MemoryRegion *) | (MemoryRegion *) | (MemoryRegion *) | 85 | | | | | | | | | | | 86 | | +---------------------+ +---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+ 87 | | |sections_nb | |fv |fv |fv |fv |fv |fv | 88 | | |sections_nb_alloc | | (FlatView *) | (FlatView *) | (FlatView *) | (FlatView *) | (FlatView *) | (FlatView *) | 89 | | | (unsigned) | +---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+ 90 | | +---------------------+ |size (Int128) |size (Int128) |size (Int128) |size (Int128) |size (Int128) |size (Int128) | 91 | | | | +---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+ 92 | | | | |offset_within_region |offset_within_region |offset_within_region |offset_within_region |offset_within_region |offset_within_region | 93 | | | | | (hwaddr) | (hwaddr) | (hwaddr) | (hwaddr) | (hwaddr) | (hwaddr) | 94 | | | | |offset_within_address_space|offset_within_address_space|offset_within_address_space|offset_within_address_space|offset_within_address_space|offset_within_address_space| 95 | | | | | (hwaddr) GPA | (hwaddr) GPA | (hwaddr) GPA | (hwaddr) GPA | (hwaddr) | (hwaddr) | 96 | | | | +---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+---------------------------+ 97 | | | | ^ 98 | | | | nodes[1] | 99 | | | | +---->+------------------+ | 100 | | | | | |u32 skip:6 | = 0 | 101 | | | | | |u32 ptr:26 | = 4 -----------------------------------------+ 102 | | | | P_L2_LEVELS = 6 | +------------------+ 103 | | | | nodes[0] = PhysPageEntry[P_L2_SIZE = 2^9] | | | 104 | | +---------------------+ +------------------+ | | ... | 105 | | |nodes | ------->|u32 skip:6 | = 1 | | | 106 | | | (Node *) | |u32 ptr:26 | = 1 -------------+ +------------------+ 107 | | +---------------------+ +------------------+ |u32 skip:6 | = 0 108 | | |nodes_nb | | | |u32 ptr:26 | = PHYS_SECTION_UNASSIGNED 109 | | |nodes_nb_alloc | | ... | +------------------+ 110 | | | (unsigned) | | | 111 | | +---------------------+ +------------------+ 112 | | | | |u32 skip:6 | = 1 113 | | | | |u32 ptr:26 | = PHYS_MAP_NODE_NIL nodes[2] 114 | | | | +------------------+ +---->+------------------+ 115 | | | | |u32 skip:6 | = 1 | |u32 skip:6 | 116 | | | | |u32 ptr:26 | = 2 -------------+ |u32 ptr:26 | 117 | | | | +------------------+ +------------------+ 118 | | | | ^ | | 119 | | | | | | ... | 120 | | | | | | | 121 | +---+---------------------+ | +------------------+ 122 | |phys_map(PhysPageEntry) | | |u32 skip:6 | 123 | | +---------------------+ | |u32 ptr:26 | 124 | | |u32 skip:6 | = 1 | +------------------+ 125 | | |u32 ptr:26 | = 0 --------+ 126 | +---+---------------------+ 127 | ``` 128 | 129 | 简单来说 130 | 131 | * phys_map 像是CR3 132 | * nodes 是一个用链表存储了的页表 133 | * sections 是nodes的叶子ptr指向的内容,其中包含了MemoryRegion 134 | 135 | 希望这么解释能够在一定程度上帮助理解。 136 | -------------------------------------------------------------------------------- /address_space/05-RAMBlock.md: -------------------------------------------------------------------------------- 1 | 对于一个地址空间,我们有了树状的MemoryRegion,有了一维的FlatView,但是还没有讲真正对应的内存在哪里。 2 | 3 | 在qemu中,这部分的工作交给了RAMBlock。 4 | 5 | # 虚拟机内存的分配流程 6 | 7 | 按照惯例,我们来看一眼一台虚拟机是如何获得对应的内存的。这个过程有点长,在这里只列出关键的部分。 8 | 9 | ``` 10 | pc_memory_init() 11 | memory_region_allocate_system_memory() 12 | allocate_system_memory_nonnuma() 13 | memory_region_init_ram_nomigrate() 14 | memory_region_init_ram_shared_nomigrate() 15 | { 16 | mr->ram = true; 17 | mr->destructor = memory_region_destructor_ram; 18 | mr->ram_block = qemu_ram_alloc(size, share, mr, errp); 19 | } 20 | ``` 21 | 22 | 埋得很深,最终还是找到。主要工作还是和MemoryRegion相关,设置了其中几个关键的成员: 23 | 24 | * ram: 表示有内存对应 25 | * destructor: 释放时的操作 26 | * ram_block: 这个就是本节要讲的RAMBlock了 27 | 28 | # ram_list按照空间大小排序的链表 29 | 30 | 其实到这里已经没啥好多说的了,RAMBlock数据结构就是描述虚拟机在主机上对应的内存空间的。不过呢,在qemu上还用了一个链表把他们连起来。这说明qemu上可以分配不止一个RAMBlock。而且在链表上,他们是按照空间大小排序的。 31 | 32 | 这部分可以看ram_block_add()函数的注释。 33 | 34 | ``` 35 | /* Keep the list sorted from biggest to smallest block. Unlike QTAILQ, 36 | * QLIST (which has an RCU-friendly variant) does not have insertion at 37 | * tail, so save the last element in last_block. 38 | */ 39 | RAMBLOCK_FOREACH(block) { 40 | last_block = block; 41 | if (block->max_length < new_block->max_length) { 42 | break; 43 | } 44 | } 45 | ``` 46 | 47 | 用一张图来让大家增加一些直观印象。 48 | 49 | ``` 50 | ram_list (RAMList) 51 | +------------------------------+ 52 | |dirty_memory[] | 53 | | (unsigned long *) | 54 | +------------------------------+ 55 | |blocks | 56 | | QLIST_HEAD | 57 | +------------------------------+ 58 | | 59 | | RAMBlock RAMBlock 60 | | +---------------------------+ +---------------------------+ 61 | +---> |next | -------------> |next | 62 | | QLIST_ENTRY(RAMBlock) | | QLIST_ENTRY(RAMBlock) | 63 | +---------------------------+ +---------------------------+ 64 | |offset | |offset | 65 | |used_length | |used_length | 66 | |max_length | |max_length | 67 | | (ram_addr_t) | | (ram_addr_t) | 68 | +---------------------------+ +---------------------------+ 69 | ``` 70 | 71 | # 地址对应关系 72 | 73 | 有了RAMBlock后,其中关键的一点就是GPA(Guest Physical Address)是如何和HVP(Host Virtual Address)的映射关系就建立了。 74 | 75 | 我们用一张图来解释。 76 | 77 | ``` 78 | RAMBlock RAMBlock 79 | +---------------------------+ +---------------------------+ 80 | |next | -----------------------------> |next | 81 | | QLIST_ENTRY(RAMBlock) | | QLIST_ENTRY(RAMBlock) | 82 | +---------------------------+ +---------------------------+ 83 | |offset | |offset | 84 | |used_length | |used_length | 85 | |max_length | |max_length | 86 | | (ram_addr_t) | | (ram_addr_t) | 87 | +---------------------------+ +---------------------------+ 88 | |host | virtual address of a ram |host | 89 | | (uint8_t *) | in host (mmap) | (uint8_t *) | 90 | +---------------------------+ +---------------------------+ 91 | |mr | |mr | 92 | | (struct MemoryRegion *)| | (struct MemoryRegion *)| 93 | +---------------------------+ +---------------------------+ 94 | | | 95 | | | 96 | | | 97 | | struct MemoryRegion | struct MemoryRegion 98 | +-->+------------------------+ +-->+------------------------+ 99 | |name | |name | 100 | | (const char *) | | (const char *) | 101 | +------------------------+ +------------------------+ 102 | |addr | physical address in guest |addr | 103 | | (hwaddr) | (offset in RAMBlock) | (hwaddr) | 104 | |size | |size | 105 | | (Int128) | | (Int128) | 106 | +------------------------+ +------------------------+ 107 | ``` 108 | 109 | GPA -> HVA 的映射由MemoryRegion->addr到RAMBlock->host完成。 110 | 111 | 是不是有种明心见性的感觉~ 112 | -------------------------------------------------------------------------------- /address_space/06-AddressSpace2.md: -------------------------------------------------------------------------------- 1 | 现在我们知道了AddressSpace, MemoryRegion, FlatView, RAMBlock。那就来看看这几个角色在一起的样子。 2 | 3 | ``` 4 | AddressSpace 5 | +-------------------------+ 6 | |name | 7 | | (char *) | FlatView (An array of FlatRange) 8 | +-------------------------+ +----------------------+ 9 | |current_map | -------->|nr | 10 | | (FlatView *) | |nr_allocated | 11 | +-------------------------+ | (unsigned) | FlatRange FlatRange 12 | | | +----------------------+ 13 | | | |ranges | ------> +---------------------+---------------------+ 14 | | | | (FlatRange *) | |offset_in_region |offset_in_region | 15 | | | +----------------------+ | (hwaddr) | (hwaddr) | 16 | | | +---------------------+---------------------+ 17 | | | |addr(AddrRange) |addr(AddrRange) | 18 | | | | +----------------| +----------------+ 19 | | | | |start (Int128) | |start (Int128) | 20 | | | | |size (Int128) | |size (Int128) | 21 | | | +----+----------------+----+----------------+ 22 | | | |mr |mr | 23 | | | | (MemoryRegion *) | (MemoryRegion *) | 24 | | | +---------------------+---------------------+ 25 | | | 26 | | | 27 | | | 28 | | | MemoryRegion(system_memory/system_io) 29 | +-------------------------+ +------------------------+ 30 | |root | |name | root of a MemoryRegion 31 | | (MemoryRegion *) | -------->| (const char *) | tree 32 | +-------------------------+ +------------------------+ 33 | |addr | 34 | | (hwaddr) | 35 | |size | 36 | | (Int128) | 37 | +------------------------+ 38 | |subregions | 39 | | QTAILQ_HEAD() | 40 | +------------------------+ 41 | | 42 | | 43 | ----+-------------------+---------------------+---- 44 | | | 45 | | | 46 | | | 47 | 48 | struct MemoryRegion struct MemoryRegion 49 | +------------------------+ +------------------------+ 50 | |name | |name | 51 | | (const char *) | | (const char *) | 52 | +------------------------+ +------------------------+ 53 | |addr | GPA |addr | 54 | | (hwaddr) | | (hwaddr) | 55 | |size | |size | 56 | | (Int128) | | (Int128) | 57 | +------------------------+ +------------------------+ 58 | |ram_block | |ram_block | 59 | | (RAMBlock *) | | (RAMBlock *) | 60 | +------------------------+ +------------------------+ 61 | | 62 | | 63 | | 64 | | 65 | v 66 | RAMBlock 67 | +---------------------------+ 68 | |next | 69 | | QLIST_ENTRY(RAMBlock) | 70 | +---------------------------+ 71 | |offset | 72 | |used_length | 73 | |max_length | 74 | | (ram_addr_t) | 75 | +---------------------------+ 76 | |host | HVA 77 | | (uint8_t *) | 78 | +---------------------------+ 79 | |mr | 80 | | (struct MemoryRegion *)| 81 | +---------------------------+ 82 | 83 | ``` 84 | -------------------------------------------------------------------------------- /address_space/07-witness.md: -------------------------------------------------------------------------------- 1 | > 纸上得来终觉浅,绝知此事须躬行 2 | 3 | 讲了这么多概念是时候实际看一看这些数据结构的样子了。这次我们借助程序员的老朋友gdb来帮助我们近距离观察qemu的地址空间。 4 | 5 | # 修改.gdbinit 6 | 7 | 在qemu源码目录下有一个文件.gdbinit, 这是默认gdb启动时加载的脚本。添加如下内容: 8 | 9 | ``` 10 | file x86_64-softmmu/qemu-system-x86_64 11 | set args -nographic 12 | source gdb-script 13 | b pc_memory_init 14 | r 15 | ``` 16 | 17 | 修改后,在源码目录下直接运行gdb就可以执行这些命令。其含义是: 18 | 19 | * 加载指定可执行文件 20 | * 设定可执行文件的参数 21 | * 加载一个脚本[gdb-script][1] 22 | * 设置断点在pc_memory_init 23 | * 运行 24 | 25 | 在源码目录下敲入gdb开始我们的探索。 26 | 27 | 注:请自行编译qemu源码。 28 | 29 | # 查看帮助 30 | 31 | 脚本中主要的函数有三个,分别带有简单的帮助,可以先看一下。 32 | 33 | ``` 34 | (gdb) help dump_address_spaces 35 | Dump a AddressSpace: dump_address_spaces 0|1 36 | 37 | Example: 38 | 39 | dump_address_spaces 0 40 | dump_address_spaces 1 41 | (gdb) help dump_memory_region 42 | Dump a MemoryRegion: dump_memory_region SYM|ADDRESS 43 | 44 | Example: 45 | 46 | dump_memory_region system_memory 47 | dump_memory_region 0x5555565036a0 48 | (gdb) help dump_flatview 49 | Dump a FlatView: dump_flatview ADDRESS 50 | 51 | Example: 52 | 53 | dump_memory_region 0x555556675be0 54 | ``` 55 | 56 | 这三个函数分别用来显示AddressSpace, MemoryRegion和FlatView。 57 | 58 | # 显示AddressSpace 59 | 60 | 首先来看看当前qemu中AddressSpace的状况。 61 | 62 | ``` 63 | (gdb) dump_address_spaces 0 64 | AddressSpace : memory(0x5555565036a0) 65 | Root MR : 0x55555661e300 66 | FlatView : 0x555556675be0 67 | AddressSpace : I/O(0x555556503640) 68 | Root MR : 0x555556616800 69 | FlatView : 0x555556691f60 70 | AddressSpace : cpu-memory-0(0x5555566756e0) 71 | Root MR : 0x55555661e300 72 | FlatView : 0x555556675be0 73 | AddressSpace : cpu-smm-0(0x5555566758c0) 74 | Root MR : 0x555556590000 75 | FlatView : 0x555556675be0 76 | ``` 77 | 78 | 这个命令可以带两个参数,0或1。 79 | 80 | * 0: 显示地址空间的根MemoryRegion和FlatView 81 | * 1: 显示地址空间的MemoryRegion树 82 | 83 | # 显示MemoryRegion 84 | 85 | 接着我们可以查看MemoryRegion的情况。 86 | 87 | ``` 88 | (gdb) dump_memory_region 0x55555661e300 89 | Dump MemoryRegion:system 90 | [0000000000000000-10000000000000000]:system 91 | [00000000fee00000-000000000fef00000]:apic-msi 92 | ``` 93 | 94 | 这个函数可以接受两种参数,变量名或地址。 95 | 96 | 在这个例子中,我们传入的是上一步地址空间中得到的一个根MemoryRegion的地址。 97 | 98 | # 显示FlatView 99 | 100 | 看过了MemoryRegion,我们还以看其对应的FlatView。 101 | 102 | ``` 103 | (gdb) dump_flatview 0x555556675be0 104 | [000000000fee00000-000000000fef00000], offset_in_region 0000000000000000 105 | ``` 106 | 107 | 这个就是刚才那个根MemoryRegion的一维展开了。 108 | 109 | 怎么样,这样是不是更清楚了? 110 | 111 | 最近发现在qemu monitor中已经有类似的功能了。直接输入info mtree,就可以获得内存树结构。 112 | 113 | [1]: https://gist.github.com/RichardWeiYang/123ce27f686165dca9a27278384d1081 114 | -------------------------------------------------------------------------------- /address_space/08-commit_mr.md: -------------------------------------------------------------------------------- 1 | 本章我们将以添加MemoryRegion为线索,动态得考察各个数据结构及其之间的变化。 2 | 3 | 那添加MemoryRegion的线索是谁呢? 就是这个 4 | 5 | > memory_region_transaction_commit() 6 | 7 | 那就以此开始。 8 | 9 | # 精简的call flow 10 | 11 | 让我们先从全局观察这个函数,获得一个全局的概念。 12 | 13 | ``` 14 | memory_region_transaction_commit(), update topology or ioeventfds 15 | flatviews_reset() 16 | flatviews_init() 17 | flat_views = g_hash_table_new_full() 18 | empty_view = generate_memory_topology(NULL); 19 | generate_memory_topology() 20 | MEMORY_LISTENER_CALL_GLOBAL(begin, Forward) 21 | address_space_set_flatview() 22 | address_space_update_topology_pass(false) 23 | address_space_update_topology_pass(true) 24 | address_space_update_ioeventfds() 25 | address_space_add_del_ioeventfds() 26 | MEMORY_LISTENER_CALL_GLOBAL(commit, Forward) 27 | ``` 28 | 29 | 从上面的精简版来看,添加一个MemoryRegion需要做如下几件事: 30 | 31 | * flatviews_reset: 重构所有AddressSpace的flatview 32 | * MEMORY_LISTENER_CALL_GLOBAL(begin, Forward) 33 | * address_space_set_flatview: 根据变化添加删除region 34 | * address_space_update_ioeventfds: 根据变化添加删除eventfd 35 | * MEMORY_LISTENER_CALL_GLOBAL(commit, Forward) 36 | 37 | 所以总的来讲也就这五个步骤,其中第一个步骤就是将MemoryRegion转换为FlatView,而其余的四个步骤都是根据FlatView的变化做出相应的调整。 38 | 39 | # MemoryListener 40 | 41 | 为了辅助地址空间的动态变化,还需要了解一个数据结构 MemoryListener。 42 | 43 | ``` 44 | MemoryListerner 45 | +---------------------------+ 46 | |begin | 47 | |commit | 48 | +---------------------------+ 49 | |region_add | 50 | |region_del | 51 | +---------------------------+ 52 | |eventfd_add | 53 | |eventfd_del | 54 | +---------------------------+ 55 | |log_start | 56 | |log_stop | 57 | +---------------------------+ 58 | ``` 59 | 60 | 有经验的朋友到这估计猜出来了,这个就是一个回调函数的集合,用在必要的时间点调用到这些钩子。 61 | 62 | 那在什么地方去调用这些回调函数呢?最主要的一共有两个宏定义 63 | 64 | * MEMORY_LISTENER_CALL_GLOBAL 65 | * MEMORY_LISTENER_CALL 66 | 67 | 这两个宏定义长得非常像,只有一点点的差别。那就是调用的MemoryListener的链表不同。 68 | 69 | 在memory_listener_register注册MemoryListener的时候,会注册到两个地方 70 | 71 | * memory_listeners 72 | * as->listeners 73 | 74 | 也就是一个是全局的链表,一个是AddressSpace自身的链表。 75 | 76 | 所以我们可以总结一下添加MemoryRegion时需要做的操作 77 | 78 | * 根据新的MemoryRegion树生成FlatView 79 | * 比较新旧FlatView调用MemoryListener进行修改 80 | 81 | # 以KVM为例 82 | 83 | 最后我们以KVM为例,看看kvm是如何将客户机的内存通知给内核模块的。 84 | 85 | 既然知道了上述的流程概述,那么就知道重点是看kvm注册的MemoryListener的回调函数是什么。 86 | 87 | 查看代码,发现有两个重要的成员是: 88 | 89 | ``` 90 | kml->listener.region_add = kvm_region_add; 91 | kml->listener.region_del = kvm_region_del; 92 | ``` 93 | 94 | 那我们以region_add为例,看看添加一个MemoryRegion时做的工作。 95 | 96 | ``` 97 | kvm_region_add 98 | kvm_set_phys_mem 99 | kvm_set_user_memory_region 100 | kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem); 101 | ``` 102 | 103 | 所以最后是调用了KVM_SET_USER_MEMORY_REGION这个ioctl将qemu用户态的内存传给了kvm内核模块。 104 | -------------------------------------------------------------------------------- /apic/00-advance_interrupt_controller.md: -------------------------------------------------------------------------------- 1 | 说实话对中断知之甚少,一方面用的少,另一方面对中断的硬件部分也了解的少。 2 | 3 | 借这个机会,通过对虚拟硬件的了解学习一下中断控制器。 4 | 5 | 随着技术的发展,现在有至少两种中断虚拟化的方式。 6 | 7 | * [纯Qemu模拟][1] 8 | 9 | * [Qemu/kernel混合模拟][2] 10 | 11 | 在第二种模拟的方式上,硬件又进一步做了辅助模拟 12 | 13 | * [APICV][3] 14 | 15 | [1]: /apic/01-qemu_emulate.md 16 | [2]: /apic/02-qemu_kernel_emulate.md 17 | [3]: /apic/03-apicv.md 18 | -------------------------------------------------------------------------------- /apic/01-qemu_emulate.md: -------------------------------------------------------------------------------- 1 | 虽然说纯软件模拟的方式在大部分的情况下已经被替代了,但是对软件模拟方式的理解能够加深对虚拟化的理解,并且对软硬件进化的过程有个概念。 2 | 3 | # 继承关系 4 | 5 | ``` 6 | TYPE_OBJECT 7 | +-----------------------------+ 8 | |class_init | = object_class_init 9 | | | 10 | |instance_size | = sizeof(Object) 11 | +-----------------------------+ 12 | 13 | 14 | TYPE_DEVICE 15 | +-----------------------------+ 16 | |class_size | = sizeof(DeviceClass) 17 | |class_init | = device_class_init 18 | | | 19 | |instance_size | = sizeof(Object) 20 | |instance_init | = device_initfn 21 | |instance_finalize | = device_finalize 22 | | | 23 | |realize | = apic_common_realize 24 | +-----------------------------+ 25 | 26 | APICCommonClass TYPE_APIC_COMMON "apic-common" 27 | +-----------------------------+ 28 | |class_size | = sizeof(APICCommonClass) 29 | |class_init | = apic_common_class_init 30 | | | 31 | |instance_size | = sizeof(APICCommonState) 32 | |instance_init | = apic_common_initfn 33 | | | 34 | +-----------------------------+ 35 | 36 | APICCommonClass TYPE_APIC "apic" 37 | +-----------------------------+ 38 | |class_init | = apic_class_init 39 | | | 40 | |instance_size | = sizeof(APICCommonState) 41 | | | 42 | |realize | = apic_realize 43 | +-----------------------------+ 44 | ``` 45 | 46 | 这个继承层次也挺长的了,不过还好,没有cpu那个长。而且总的来说也还算蛮清晰的。 47 | 48 | # 初始化 49 | 50 | APIC的初始化和CPU还是有很大关联的,因为在硬件上他们两个人就是在一起的。所以qemu中APIC的创建也是随着CPU一起创建的。 51 | 52 | 为了说明问题,我们还是以x86 cpu为例。这个创建的过程就在x86_cpu_realizefn函数里。相关内容参见[x86 cpu][1] 53 | 54 | 接着我们就打开这个函数,看看究竟这个是怎么玩的。 55 | 56 | ``` 57 | x86_cpu_realizefn 58 | x86_cpu_apic_create 59 | apic_common_initfn 60 | set apic->id with cpu->apic_id 61 | x86_cpu_apic_realize 62 | object_property_set_bool(cpu->apic_state, realized) 63 | apic_common_realize 64 | apic_realize 65 | // only one memory region installed 66 | memory_region_add_subregion_overlap(&apic->io_memory) 67 | ``` 68 | 69 | # 实现 70 | 71 | 了解了初始化的流程,接下来我们看看APIC是怎么模拟的。 72 | 73 | 先来看看APICCommonState这个数据结构 74 | 75 | ``` 76 | APICCommonState 77 | +-----------------------------+ 78 | |io_memory | 79 | | ops | = apic_io_ops 80 | | | 81 | |apicbase | = APIC_DEFAULT_ADDRESS 82 | | | 83 | |isr/irr/tmr | 84 | |lvt | 85 | |sipi_vector | 86 | | | 87 | |cpu | 88 | | (X86CPU *) | 89 | | +------------------------+ 90 | | |interrupt_request | 91 | | | | 92 | +----+------------------------+ 93 | ``` 94 | 95 | 首先是这个io_memory,这个就是在初始化时注册的内存空间。有意思的是不管有多少cpu,这个空间只有一个。而其中关键的就是对应的apic_io_ops了,具体的模拟手段都隐藏在这个操作中。 96 | 97 | 其次是apicbase,这就是系统默认的APIC访问的地址了。这没啥说的,就是按照手册来。 98 | 99 | 剩下的就是APIC的一些寄存器了,每次读写都会对这些寄存器访问。 100 | 101 | # 发送中断 102 | 103 | 那现在我们就来看看对发送中断的模拟。 104 | 105 | 我们以IPI为例,在SDM中10.6小节描述了如何发送IPI。简单来说就是通过写ICR(Interrupt Command Register)来达到目的。 106 | 107 | 对应的在代码中,写APIC最后要走到apic_mem_write中0x30选项。 108 | 109 | ``` 110 | case 0x30: 111 | s->icr[0] = val; 112 | apic_deliver(dev, (s->icr[1] >> 24) & 0xff, (s->icr[0] >> 11) & 1, 113 | (s->icr[0] >> 8) & 7, (s->icr[0] & 0xff), 114 | (s->icr[0] >> 15) & 1); 115 | ``` 116 | 117 | 你看,是不是和手册上说的一样。写入时设置了ICR? 118 | 119 | 实际上msi的中断也是在apic_mem_write中执行的。貌似这两段内存空间是重合的?没搞懂,不过代码注释好像是这么写的。 120 | 121 | ``` 122 | apic_mem_write 123 | apic_send_msi() 124 | apic_deliver_irq 125 | apic_bus_deliver 126 | apic_deliver 127 | apic_startup 128 | cpu_interrupt = generic_handle_interrupt 129 | qemu_cpu_kick_thread 130 | pthread_kill(cpu->thread->thread, SIG_IPI) 131 | apic_bus_deliver 132 | cpu_interrupt 133 | apic_set_irq 134 | apic_update_irq 135 | cpu_interrupt 136 | ``` 137 | 138 | 从这里看,所有的路径基本都会走到cpu_interrupt函数。而这个函数的工作是设置s->interrupt_request并发送一个信号给vcpu thread。 139 | 140 | # 处理中断 141 | 142 | 处理中断和vcpu thread有很大关系。这里我们只看没有kvm介入时tcg的情况 qemu_tcg_rr_cpu_thread_fn。 143 | 144 | ``` 145 | tcg_cpu_exec 146 | cpu_exec 147 | cpu_handle_interrupt 148 | cpu_handle_exception() 149 | cc->do_interrupt = x86_cpu_do_interrupt 150 | do_interrupt_all 151 | do_interrupt_protected 152 | cpu_handle_interrupt() 153 | cc->cpu_exec_interrupt = x86_cpu_exec_interrupt 154 | ``` 155 | 156 | 当vcpu thread起来后,每个周期中会去检测有没有异常和中断。当检测到有的时候,则模拟中断的处理。 157 | 158 | 这个代码可是老复杂了,感觉能写这个代码的人简直是神。 159 | 160 | 不过从上面的分析中可以看出,软件模拟的情况下中断处理会有两个问题: 161 | 162 | * 中断处理是在vcpu thread处理的间歇执行的,而并没有强制打断vcpu thread的正常执行。所以这个中断将会有较大的延时。 163 | * 如果持续有中断好像会一直处理中断,所以中断太过频繁,也会导致系统无法继续。 164 | 165 | 以上是对用户态软件模拟的LAPIC的理解,希望是正确的。 166 | 167 | [1]: /cpu/02-x86_cpu.md 168 | -------------------------------------------------------------------------------- /apic/02-qemu_kernel_emulate.md: -------------------------------------------------------------------------------- 1 | 研究之后发现,除了qemu模拟apic的操作之外,还有一种模拟方式是qemu/kernel协同模拟。这个确实搞的有点绕。 2 | 3 | 让我们对这种方式进行一下探索,并看看他们之间有什么区别。 4 | 5 | # 继承关系 6 | 7 | ``` 8 | TYPE_OBJECT 9 | +-----------------------------+ 10 | |class_init | = object_class_init 11 | | | 12 | |instance_size | = sizeof(Object) 13 | +-----------------------------+ 14 | 15 | 16 | TYPE_DEVICE 17 | +-----------------------------+ 18 | |class_size | = sizeof(DeviceClass) 19 | |class_init | = device_class_init 20 | | | 21 | |instance_size | = sizeof(Object) 22 | |instance_init | = device_initfn 23 | |instance_finalize | = device_finalize 24 | | | 25 | |realize | = apic_common_realize 26 | +-----------------------------+ 27 | 28 | APICCommonClass TYPE_APIC_COMMON "apic-common" 29 | +-----------------------------+ 30 | |class_size | = sizeof(APICCommonClass) 31 | |class_init | = apic_common_class_init 32 | | | 33 | |instance_size | = sizeof(APICCommonState) 34 | |instance_init | = apic_common_initfn 35 | | | 36 | +-----------------------------+ 37 | 38 | APICCommonClass TYPE_APIC "kvm-apic" 39 | +-----------------------------+ 40 | |class_init | = kvm_apic_class_init 41 | | | 42 | |instance_size | = sizeof(APICCommonState) 43 | | | 44 | |realize | = kvm_apic_realize 45 | +-----------------------------+ 46 | ``` 47 | 48 | 从继承关系上看,这个混合模拟的设备类型和纯qemu模拟的设备类型只在最后一个类型上有所差别。 49 | 50 | 那究竟差在哪里呢?对了,就是这个class_init函数做的操作不同。这个函数主要设置了APICCommonClass中的回调函数,其中就包括了realize函数。 51 | 52 | 好了,这里先卖一个关子,我们一会儿再过来看具体差别。 53 | 54 | # 初始化 55 | 56 | 作为混合模拟,所以apic设备在内核和qemu中各有一部分。 57 | 58 | ## qemu部分 59 | 60 | 总的来说,初始化的大致流程和qemu模拟的相似。因为从硬件角度上看,apic是和cpu绑定的。所以模拟硬件的生成也是要再vcpu创建时生成。 61 | 62 | 但是因为有kernel的参与,所以又略有不同。 63 | 64 | ``` 65 | x86_cpu_realizefn 66 | x86_cpu_apic_create 67 | apic_common_initfn 68 | set apic->id with cpu->apic_id 69 | qemu_init_vcpu 70 | qemu_kvm_start_vcpu 71 | qemu_kvm_cpu_thread_fn 72 | kvm_init_vcpu 73 | kvm_get_vcpu 74 | kvm_vm_ioctl(s, KVM_CREATE_VCPU, (void *)vcpu_id) 75 | x86_cpu_apic_realize 76 | object_property_set_bool(cpu->apic_state, realized) 77 | apic_common_realize 78 | kvm_apic_realize 79 | // only one memory region installed 80 | memory_region_add_subregion_overlap(&apic->io_memory) 81 | ``` 82 | 83 | 感觉这里实际上创建了两个模拟的apic设备: 84 | 85 | * acpi_common_initfn: 这里在Qemu中创建了APICCommonState结构体 86 | * KVM_CREATE_VCPU: 这里到内核里创建了自己的apic 87 | 88 | 不知道是不是可以改进一点点? 89 | 90 | 接着是注册MemoryRegion,这个过程和qemu模拟的设备是一样的,只是注册的函数不同。这样看来,对guest这个接口是一样的。 91 | 92 | ``` 93 | APICCommonState 94 | +-----------------------------+ 95 | |io_memory | 96 | | ops | = kvm_apic_io_ops 97 | | | 98 | |apicbase | = APIC_DEFAULT_ADDRESS 99 | | | 100 | |isr/irr/tmr | 101 | |lvt | 102 | |sipi_vector | 103 | | | 104 | |cpu | 105 | | (X86CPU *) | 106 | | +------------------------+ 107 | | |interrupt_request | 108 | | | | 109 | +----+------------------------+ 110 | ``` 111 | 112 | 因为继承关系上和qemu模拟的设备一样,所以实现上也是非常类似。其中有区别的一点是对应注册的函数改成了kvm_apic_io_ops。 113 | 114 | ## kernel部分 115 | 116 | 因为有一(大)部分apic的模拟在内核中,所以在使用前也需要在内核中初始化好。 117 | 118 | 还记得上面那一段qemu中的初始化部分么?其中qemu_init_vcpu部分停在了kvm_vm_ioctl(KVM_CREATE_VCPU)这。从字面上看这里是创建vcpu,但是要记住vcpu和lapic是绑定在一起的。所以这里就是进入kernel初始化的入口。 119 | 120 | ``` 121 | vmx_create_vcpu 122 | kvm_vcpu_init 123 | kvm_arch_vcpu_init 124 | kvm_create_lapic 125 | ``` 126 | 127 | 可以看出,从函数调用的关系上这个还是相对简洁的。不过对应的数据结构就有点长了,毕竟这次大部分工作要在内核中完成。 128 | 129 | ``` 130 | vcpu 131 | +----------------------------------+ 132 | |arch | 133 | | (struct kvm_vcpu_arch) | 134 | | +------------------------------+ 135 | | |apic_arb_prio | 136 | | | | 137 | | |apic --+ | 138 | +---+------------------------------+ 139 | | 140 | | 141 | kvm_lapic v 142 | +----------------------------------+ 143 | |vcpu | 144 | | (struct kvm_vcpu*) | 145 | +----------------------------------+ 146 | |dev | 147 | | (struct kvm_io_device) | 148 | | +-----------------------------+ 149 | | |ops | = apic_mmio_ops 150 | | | (kvm_io_device_ops*) | 151 | +----+-----------------------------+ 152 | |base_address | = MSR_IA32_APICBASE_ENABLE 153 | | (unsigned long) | 154 | +----------------------------------+ 155 | |lapic_timer | 156 | | (struct kvm_timer) | 157 | | +-----------------------------+ 158 | | |pending | 159 | | |period | 160 | | |timer | = apic_timer_fn 161 | | | | 162 | +----+-----------------------------+ 163 | |divide_count | 164 | | (u32) | 165 | |isr_count | 166 | | (s16) | 167 | |highest_isr_cache | 168 | | (int) | 169 | |pending_events | 170 | | (unsigned long) | 171 | |sipi_vector | 172 | | (unsigned int) | 173 | +----------------------------------+ 174 | |sw_enabled | 175 | |irr_pending | 176 | |lvt0_in_nmi_mode | 177 | | (bool) | 178 | +----------------------------------+ 179 | |vapic_addr | 180 | | (gpa_t) | 181 | | | 182 | |regs | = address to one page 183 | | (void*) | 184 | +----------------------------------+ 185 | ``` 186 | 187 | 在这个结构中有两点值得注意: 188 | 189 | * 对应了一个内存空间操作的回调函数: apic_mmio_ops 190 | * 分配了一页空间作为apic包含的寄存器:regs 191 | 192 | # 发送中断 193 | 194 | 接下来我们来看看虚拟环境下如何向一个apic发送中断,或者应该讲如何做到操作一个apic来发送中断的。 195 | 196 | 又因为我们在内核和qemu中分别模拟了apic设备,所以在这里也是有两种情况。 197 | 198 | 就我的理解,原理或者说起因都是一样的。那就是都去写apic的寄存器。在qemu和kernel中分别对应了两个内存处理的ops 199 | 200 | * kvm_apic_io_ops 201 | * apic_mmio_ops 202 | 203 | ## qemu部分 204 | 205 | 当guest通过kvm_apic_io_ops写寄存器时就会触发对应apic中断的流程。 206 | 207 | ``` 208 | kvm_apic_mem_write() 209 | kvm_send_msi() 210 | kvm_irqchip_send_msi(kvm_state, *msg) 211 | kvm_vm_ioctl(s, KVM_SIGNAL_MSI, &msi) 212 | route = kvm_lookup_msi_route(s, msg) 213 | kvm_set_irq(s, route->kroute.gsi, 1) 214 | kvm_vm_ioctl(s, s->irq_set_ioctl, $event) KVM_IRQ_LINE/_STATUS 215 | ``` 216 | 217 | 这个流程就相对简单,qemu直接通过ioctl把中断传递给了内核,让内核来进行处理。 218 | 219 | 再仔细和apic_mem_write做对比,kvm_apic_mem_write的流程简直简单到爆。那说明原来对apic内部寄存器操作的动作根本不会出来? 220 | 221 | 那意思是说通过qemu模拟的apic发送中断的只有ioapic了么? 222 | 223 | ## kernel部分 224 | 225 | 到了内核部分就有点意思了。因为内核部分从来源上又分成两个部分。 226 | 227 | * 从qemu通过ioctl发送中断 228 | * 在内核中通过直接写lapic发送中断 229 | 230 | 因为第二中情况最后会调用到第一种情况,所以下面先展示在内核中如何接收到guest的一次lapic的请求并发送中断的。 231 | 232 | ``` 233 | vcpu_mmio_write 234 | kvm_iodevice_write(vcpu, vcpu->arch.apic->dev) 235 | apic_mmio_in_range() 236 | apic_mmio_write() 237 | kvm_lapic_reg_write() 238 | APIC_ID 239 | APIC_TASKPRI 240 | APIC_EOI 241 | APIC_LDR 242 | APIC_DFR 243 | APIC_SPIV 244 | APIC_ICR 245 | apic_send_ipi() 246 | kvm_irq_delivery_to_apic() 247 | ``` 248 | 249 | 内核在接收到这个mmio写操作时,如果判断这个是在apic的范围内,那么就会调用到我们之前注册的apic_mmio_ops的write函数apic_mmio_write()。 250 | 251 | 其中会向其他apic发送ipi中断的操作最后调用到了**kvm_irq_delivery_to_apic**。记住这个函数,一会儿我们还会看到。 252 | 253 | 接下来看内核中在收到了来自qemu的中断操作请求是如何响应的。 254 | 255 | ``` 256 | kvm_send_userspace_msi(kvm, &msi) 257 | kvm_set_msi(KVM_USERSPACE_IRQ_SOURCE_ID) 258 | kvm_msi_route_invalid() 259 | kvm_set_msi_irq() 260 | kvm_irq_delivery_to_apic() 261 | ``` 262 | 263 | 瞧,这里最后一个函数也是**kvm_irq_delivery_to_apic**,真是殊途同归啊。 264 | 265 | 既然我们现在找到了这个共同的函数,那就来看看接下来内核是怎么处理的。 266 | 267 | ``` 268 | kvm_irq_delivery_to_apic() 269 | kvm_irq_delivery_to_apic_fast() 270 | kvm_vector_to_index() 271 | kvm_get_vcpu() 272 | kvm_apic_set_irq() 273 | __apic_accept_irq() 274 | APIC_DM_LOWEST 275 | vcpu->arch.apic_arb_prio++ 276 | APIC_DM_FIXED 277 | kvm_lapic_set_vector 278 | kvm_lapic_clear_vector 279 | if (vcpu->arch.apicv_active) 280 | kvm_x86_ops->deliver_posted_interrupt(vcpu, vector); 281 | else 282 | kvm_lapic_set_irr 283 | kvm_make_request(KVM_REQ_EVENT, vcpu) 284 | kvm_vcpu_kick() 285 | APIC_DM_REMRD 286 | kvm_make_request(KVM_REQ_EVENT, vcpu) 287 | kvm_vcpu_kick() 288 | APIC_DM_SMI 289 | kvm_make_request(KVM_REQ_EVENT, vcpu) 290 | kvm_vcpu_kick() 291 | APIC_DM_NMI 292 | kvm_inject_nmi(vcpu) 293 | kvm_vcpu_kick(vcpu) 294 | APIC_DM_INIT 295 | apic->pending_events = (1UL << KVM_APIC_INIT); 296 | kvm_make_request(KVM_REQ_EVENT, vcpu) 297 | kvm_vcpu_kick() 298 | APIC_DM_STARTUP 299 | apic->sipi_vector = vector; 300 | kvm_make_request(KVM_REQ_EVENT, vcpu) 301 | kvm_vcpu_kick() 302 | APIC_DM_EXTINT 303 | ``` 304 | 305 | 终于看到最后了,这个的奥秘就隐藏在了__apic_accept_irq()中。针对不同的情况做不同的处理,但是大部分情况都做了这么两件事: 306 | 307 | * kvm_make_request(KVM_REQ_EVENT, vcpu) 308 | * kvm_vcpu_kick() 309 | 310 | 也就是设置了标示后,唤醒vcpu来处理中断。完美~ 311 | 312 | # 接收中断 313 | 314 | 这个时候vcpu被唤醒去处理中断,按照我的理解其实没有直接处理中断的函数,而是在vcpu再次进入guest时检测KVM_REQ_EVENT事件。 315 | 316 | 话不多书,直接看流程 317 | 318 | ``` 319 | vcpu_enter_guest() 320 | kvm_check_request(KVM_REQ_EVENT, vcpu) 321 | kvm_apic_accept_events() 322 | inject_pending_event(vcpu, req_int_win) 323 | kvm_x86_ops->set_irq(vcpu) = vmx_inject_irq() 324 | vmcs_write32(VM_ENTRY_INTR_INFO_FIELD, intr) 325 | ``` 326 | 327 | 最后的最后,通过写vmcs中的VM_ENTRY_INTR_INFO_FIELD字段通知到guest。 328 | 329 | 该字段的详细信息在SDM vol3, 24.8.3 VM-Entry Controls for Event Injection中。 330 | 331 | 其中就包含了 332 | 333 | * 中断向量号 334 | * 中断类型 335 | 336 | 好了,我想到这里整个完整的流程就算是理清楚了。 337 | -------------------------------------------------------------------------------- /apic/03-apicv.md: -------------------------------------------------------------------------------- 1 | 在qemu/kernel混合模拟的基础上,为了进一步优化中断模拟,硬件提出了APICV。 2 | 3 | 在上面一小节混合模拟的流程中,其实也会涉及到apicv的代码,但没有仔细考察其作用。现在我们把它拿出来,仔细考察。 4 | 5 | # 硬件知识 6 | 7 | 既然是硬件的辅助功能,那自然是先学习下硬件都提供了些什么。这部分的内容主要在SDM Chapter 29 APIC VIRTUALIZATION AND VIRTUAL INTERRUPTS。 8 | 9 | ## 相关的VM-execution controls 10 | 11 | 24.6.8 Controls for APIC Virtualization 12 | 13 | * **APIC-access address (64 bits)**. This field contains the physical address of the 4-KByte APIC-access page 14 | * **Virtual-APIC address (64 bits)**. This field contains the physical address of the 4-KByte virtual-APIC page 15 | 16 | Certain VM-execution controls enable the processor to virtualize certain accesses to the APIC-access page without a VM exit. In general, this virtualization causes these accesses to be made to the virtual-APIC page instead of the APIC-access page. 17 | 18 | * **TPR threshold (32 bits)**. Bits 3:0 of this field determine the threshold below which bits 7:4 of VTPR (see Section 29.1.1) cannot fall. 19 | * **EOI-exit bitmap (4 fields; 64 bits each)**. These fields are supported only on processors that support the 1- setting of the “virtual-interrupt delivery” VM-execution control. They are used to determine which virtualized writes to the APIC’s EOI register cause VM exits 20 | 21 | * **Posted-interrupt notification vector (16 bits)**. Its low 8 bits contain the interrupt vector that is used to notify a logical processor that virtual interrupts have been posted. See Section 29.6 for more information on the use of this field. 22 | * **Posted-interrupt descriptor address (64 bits)**. 23 | 24 | 24.4.2 Guest Non-Register State 25 | 26 | * **Guest interrupt status (16 bits)** This field is supported only on processors that support the 1-setting of the “virtual-interrupt delivery” VM-execution control. 27 | 28 | - **Requesting virtual interrupt (RVI)(low byte)** 29 | - **Servicing virtual interrupt (SVI)(high byte)** 30 | 31 | 29 APIC VIRTUALIZATION AND VIRTUAL INTERRUPTS 32 | 33 | The following are the VM-execution controls relevant to APIC virtualization and virtual interrupts (see Section 24.6 for information about the locations of these controls): 34 | * **Virtual-interrupt delivery**. This controls enables the evaluation and delivery of pending virtual interrupts (Section 29.2). It also enables the emulation of writes (memory-mapped or MSR-based, as enabled) to the APIC registers that control interrupt prioritization. 35 | * **Use TPR shadow**. This control enables emulation of accesses to the APIC’s task-priority register (TPR) via CR8 (Section 29.3) and, if enabled, via the memory-mapped or MSR-based interfaces. 36 | * **Virtualize APIC accesses**. This control enables virtualization of memory-mapped accesses to the APIC (Section 29.4) by causing VM exits on accesses to a VMM-specified APIC-access page. Some of the other controls, if set, may cause some of these accesses to be emulated rather than causing VM exits. 37 | * **Virtualize x2APIC mode**. This control enables virtualization of MSR-based accesses to the APIC (Section 29.5). 38 | * **APIC-register virtualization**. This control allows memory-mapped and MSR-based reads of most APIC registers (as enabled) by satisfying them from the virtual-APIC page. It directs memory-mapped writes to the APIC-access page to the virtual-APIC page, following them by VM exits for VMM emulation. 39 | * **Process posted interrupts**. This control allows software to post virtual interrupts in a data structure and send a notification to another logical processor; upon receipt of the notification, the target processor will process the posted interrupts by copying them into the virtual-APIC page (Section 29.6). 40 | 41 | ## Virtual APIC Page 42 | 43 | 29.1.1 Virtualized APIC Registers 44 | 45 | * **Virtual task-priority register (VTPR)**: the 32-bit field located at offset 080H on the virtual-APIC page. 46 | * **Virtual processor-priority register (VPPR)**: the 32-bit field located at offset 0A0H on the virtual-APIC page. 47 | * **Virtual end-of-interrupt register (VEOI)**: the 32-bit field located at offset 0B0H on the virtual-APIC page. 48 | * **Virtual interrupt-service register (VISR)** 49 | * **Virtual interrupt-request register (VIRR)** 50 | * **Virtual interrupt-command register (VICR_LO)**: the 32-bit field located at offset 300H on the virtual-APIC page 51 | * **Virtual interrupt-command register (VICR_HI)**: the 32-bit field located at offset 310H on the virtual-APIC page. 52 | 53 | ## 图解相关寄存器 54 | 55 | ``` 56 | vmcs 57 | +----------------------------------+ 58 | |guest state area | 59 | | +------------------------------+ 60 | | |guest non-register state | 61 | | | +--------------------------+ 62 | | | |Guest interrupt status | 63 | | | | +----------------------+ 64 | | | | |Requesting virtual | 65 | | | | | interrupt (RVI). | 66 | | | | +----------------------+ 67 | | | | |Servicing virtual | 68 | | | | | interrupt (RVI). | 69 | | | | | | 70 | | | +---+----------------------+ 71 | | | | 72 | | +------------------------------+ 73 | | | 74 | |vm-execution control | 75 | | +------------------------------+ 76 | | |APIC-access address | 77 | | | | 78 | | | | 4K Virtual-APIC page 79 | | |Virtual-APIC address ----|-------->+-------------------------+ 80 | | | | 080H|Virtual task-priority | 81 | | | | | register (VTPR) | 82 | | | | 0A0H|Vrtl processor-priority | 83 | | | | | register (VPPR) | 84 | | | | 0B0H|Virtual end-of-interrupt | 85 | | | | | register (VEOI) | 86 | | | | |Virtual interrupt-service| 87 | | | | | register (VISR) | 88 | | | | |Virtual interrupt-request| 89 | | | | | register (VIRR) | 90 | | | | 300H|Virtual interrupt-command| 91 | | | | | register(VICR_LO)| 92 | | | | 310H|Virtual interrupt-command| 93 | | | | | register(VICR_HO)| 94 | | | | | | 95 | | | | +-------------------------+ 96 | | | | 97 | | | | 98 | | |TPR threshold | 99 | | |EOI-exit bitmap | 100 | | |Posted-interrupt notification | 101 | | | vector | 102 | | | | 103 | | | | 64 byte descriptor 104 | | | | 511 255 0 105 | | |Posted-interrupt descriptor |--->+----------------+----------------+ 106 | | | address | | | | 107 | | | | | | | 108 | | | | +----------------+----------------+ 109 | | |Pin-Based VM-Execution Ctrls | 110 | | | +-------------------------+ 111 | | | |Process posted interrupts| 112 | | | | | 113 | | | +-------------------------+ 114 | | | | 115 | | |Primary Processor-Based | 116 | | | VM-Execution Controls | 117 | | | +-------------------------+ 118 | | | |Interrupt window exiting | 119 | | | |Use TPR shadow | 120 | | | | | 121 | | | +-------------------------+ 122 | | | | 123 | | |Secondary Processor-Based | 124 | | | VM-Execution Controls | 125 | | | +-------------------------+ 126 | | | |Virtualize APIC access | 127 | | | |Virtualize x2APIC mode | 128 | | | |APIC-register virtual | 129 | | | |Virtual-intr delivery | 130 | | | | | 131 | | | | | 132 | | | +-------------------------+ 133 | | | | 134 | | +------------------------------+ 135 | | | 136 | +----------------------------------+ 137 | ``` 138 | 139 | ## 虚拟APIC状态 140 | 141 | 29.1 VIRTUAL APIC STATE 142 | 143 | 硬件虚拟化通过**Virtualize APIC accesses**来指定一块4k内存,用于模拟APIC寄存器的访问和管理中断。 144 | 145 | 如果没有理解错的话,这个页面在vmx_vcpu_reset函数中设置。 146 | 147 | ``` 148 | vmx_vcpu_reset 149 | vmcs_write64(VIRTUAL_APIC_PAGE_ADDR, __pa(vcpu->arch.apic->regs)); 150 | ``` 151 | 152 | ## 虚拟中断 153 | 154 | 29.2 EVALUATION AND DELIVERY OF VIRTUAL INTERRUPTS 155 | 156 | 虚拟中断包含了中断检测和中断发送。 157 | 158 | 对virtual-APIC page的操作会触发虚拟中断的检测,如果这个检测得到了一个虚拟中断,则会向guest发送一个中断且不导致guest退出。 159 | 160 | ### 虚拟中断检测 161 | 162 | 一下几种情况将触发虚拟中断的检测: 163 | 164 | * VM entry(Section 26.3.2.5) 165 | * TPR virtualization(Section 29.1.2) 166 | * EOI virtualization(Section 29.1.4) 167 | * self-IPI virtualization(Section 29.1.5) 168 | * posted-interrupt processing(Section 29.6) 169 | 170 | 检测的伪代码如下: 171 | 172 | ``` 173 | IF “interrupt-window exiting” is 0 AND 174 | RVI[7:4] > VPPR[7:4] (see Section 29.1.1 for definition of VPPR) 175 | THEN recognize a pending virtual interrupt; 176 | ELSE 177 | do not recognize a pending virtual interrupt; 178 | FI; 179 | ``` 180 | 181 | ### 虚拟中断发送 182 | 183 | 虚拟中断发送会改变虚拟机中断状态(RVI/SVI),并且在vmx non-root 模式下发送一个中断,从而不导致虚拟机退出。 184 | 185 | 中断发送的伪代码如下: 186 | 187 | ``` 188 | Vector ← RVI; VISR[Vector] ← 1; 189 | SVI ← Vector; 190 | VPPR ← Vector & F0H; VIRR[Vector] ← 0; 191 | IF any bits set in VIRR 192 | THEN RVI ← highest index of bit set in VIRR 193 | ELSE RVI ← 0; FI; 194 | deliver interrupt with Vector through IDT; 195 | cease recognition of any pending virtual interrupt; 196 | ``` 197 | 198 | 看过了硬件提供的能力,我们来看看软件上是如何借助硬件的。 199 | 200 | # 相关软件 201 | 202 | ## 检测APICV能力 203 | 204 | 在kvm代码中首先需要检测硬件是否支持apicv的功能,并作标示。 205 | 206 | ``` 207 | vmx_init 208 | kvm_init 209 | kvm_arch_hardware_setup 210 | hardware_setup 211 | if (!cpu_has_vmx_apicv()) { 212 | enable_apicv = 0; 213 | kvm_x86_ops->sync_pir_to_irr = NULL; 214 | } 215 | ``` 216 | 217 | 可以看到在启动kvm模块的时候就需要检测apicv是否存在并标示enable_apicv。 218 | 219 | 那这个cpu_has_vmx_apicv又做了啥? 220 | 221 | ``` 222 | static inline bool cpu_has_vmx_apicv(void) 223 | { 224 | return cpu_has_vmx_apic_register_virt() && 225 | cpu_has_vmx_virtual_intr_delivery() && 226 | cpu_has_vmx_posted_intr(); 227 | } 228 | ``` 229 | 230 | 这个就和上文列出了硬件提供的内容匹配了。 231 | 232 | ## 何处使用 233 | 234 | 检测好了硬件的特性,现在就要看在什么地方使用了。当然,我们并没有直接使用enable_apicv这个值,而是把它赋值给了vcpu。 235 | 236 | ``` 237 | kvm_arch_vcpu_init 238 | vcpu->arch.apicv_active = kvm_x86_ops->get_enable_apicv(vcpu); 239 | return enable_apicv; 240 | ``` 241 | 242 | 聪明的朋友一定已经想到,接下来就是找apicv_active这个值会在哪里判断,判断的地方就是使用apicv功能的地方了。 243 | 244 | 245 | # Posted Interrupt 246 | 247 | Posted Interrupt作为一个比较重要的功能,我们单独拿出来研究。 248 | 249 | ## 概念 250 | 251 | Posted-interrupt processing is a feature by which a processor processes the virtual interrupts by recording them as pending on the virtual-APIC page. 252 | 253 | If the “external-interrupt exiting” VM-execution control is 1, any unmasked external interrupt causes a VM exit (see Section 25.2). If the “process posted interrupts” VM-execution control is also 1, this behavior is changed and the processor handles an external interrupt as follows. 254 | 255 | 1. The local APIC is acknowledged; this provides the processor core with an interrupt vector, called here the physical vector. 256 | 2. If the physical vector equals the posted-interrupt notification vector, the logical processor continues to the next step. Otherwise, a VM exit occurs as it would normally due to an external interrupt; the vector is saved in the VM-exit interruption-information field. **物理中断向量号等于 posted-interrupt notification vector才继续。** 257 | 3. The processor clears the outstanding-notification bit in the posted-interrupt descriptor. This is done atomically so as to leave the remainder of the descriptor unmodified (e.g., with a locked AND operation). **清ON位。** 258 | 4. The processor writes zero to the EOI register in the local APIC; this dismisses the interrupt with the posted- interrupt notification vector from the local APIC.**清EOI。** 259 | 5. The logical processor performs a logical-OR of PIR into VIRR and clears PIR. No other agent can read or write a PIR bit (or group of bits) between the time it is read (to determine what to OR into VIRR) and when it is cleared. **PIR->VIRR** 260 | 6. The logical processor sets RVI to be the maximum of the old value of RVI and the highest index of all bits that were set in PIR; if no bit was set in PIR, RVI is left unmodified. **计算得到RVI** 261 | 7. The logical processor evaluates pending virtual interrupts as described in Section 29.2.1. 262 | 263 | 简单来说就是原来运行是的虚拟机需要退出来处理中断,现在不退出了,宿主机上用一个特殊的中断将真正的中断注入到虚拟机。真正注入到虚拟机的中断号记录在 **PIR** (Posted Interrupt Requests)。 264 | 265 | ## 软件中断处理的代码 266 | 267 | 在之前的代码分析中我们也看到过,不过没有着重讲解。在发送中断的内核部分中,我们看到 268 | 269 | ``` 270 | kvm_irq_delivery_to_apic() 271 | kvm_irq_delivery_to_apic_fast() 272 | kvm_vector_to_index() 273 | kvm_get_vcpu() 274 | kvm_apic_set_irq() 275 | __apic_accept_irq() 276 | ... 277 | APIC_DM_FIXED 278 | kvm_lapic_set_vector 279 | kvm_lapic_clear_vector 280 | if (vcpu->arch.apicv_active) 281 | kvm_x86_ops->deliver_posted_interrupt(vcpu, vector); 282 | else 283 | kvm_lapic_set_irr 284 | kvm_make_request(KVM_REQ_EVENT, vcpu) 285 | kvm_vcpu_kick() 286 | ... 287 | ``` 288 | 289 | 对于APIC_DM_FIXED中断类型,如果apicv_active为真,则会采用Posted interrupt方式。 290 | 291 | 这个函数的工作在注释中写得很清楚了。我向着重讲的是pi_test_and_set_pir将真正的中断向量号写在了pir中。 292 | 293 | ``` 294 | /* 295 | * Send interrupt to vcpu via posted interrupt way. 296 | * 1. If target vcpu is running(non-root mode), send posted interrupt 297 | * notification to vcpu and hardware will sync PIR to vIRR atomically. 298 | * 2. If target vcpu isn't running(root mode), kick it to pick up the 299 | * interrupt from PIR in next vmentry. 300 | */ 301 | static void vmx_deliver_posted_interrupt(struct kvm_vcpu *vcpu, int vector) 302 | { 303 | struct vcpu_vmx *vmx = to_vmx(vcpu); 304 | int r; 305 | 306 | ... 307 | 308 | if (pi_test_and_set_pir(vector, &vmx->pi_desc)) 309 | return; 310 | 311 | /* If a previous notification has sent the IPI, nothing to do. */ 312 | if (pi_test_and_set_on(&vmx->pi_desc)) 313 | return; 314 | 315 | if (!kvm_vcpu_trigger_posted_interrupt(vcpu, false)) 316 | kvm_vcpu_kick(vcpu); 317 | } 318 | ``` 319 | 320 | 进一步打开kvm_vcpu_trigger_posted_interrupt,发生了什么呢?实际上是向目标vcpu所在的物理cpu上发送了vector为POSTED_INTR_VECTOR的一个中断。当然这个中断就叫Posted Interrupt。 321 | 322 | ``` 323 | static inline bool kvm_vcpu_trigger_posted_interrupt(struct kvm_vcpu *vcpu, 324 | bool nested) 325 | { 326 | #ifdef CONFIG_SMP 327 | int pi_vec = nested ? POSTED_INTR_NESTED_VECTOR : POSTED_INTR_VECTOR; 328 | 329 | if (vcpu->mode == IN_GUEST_MODE) { 330 | /* 331 | * The vector of interrupt to be delivered to vcpu had 332 | * been set in PIR before this function. 333 | * 334 | * Following cases will be reached in this block, and 335 | * we always send a notification event in all cases as 336 | * explained below. 337 | * 338 | * Case 1: vcpu keeps in non-root mode. Sending a 339 | * notification event posts the interrupt to vcpu. 340 | * 341 | * Case 2: vcpu exits to root mode and is still 342 | * runnable. PIR will be synced to vIRR before the 343 | * next vcpu entry. Sending a notification event in 344 | * this case has no effect, as vcpu is not in root 345 | * mode. 346 | * 347 | * Case 3: vcpu exits to root mode and is blocked. 348 | * vcpu_block() has already synced PIR to vIRR and 349 | * never blocks vcpu if vIRR is not cleared. Therefore, 350 | * a blocked vcpu here does not wait for any requested 351 | * interrupts in PIR, and sending a notification event 352 | * which has no effect is safe here. 353 | */ 354 | 355 | apic->send_IPI_mask(get_cpu_mask(vcpu->cpu), pi_vec); 356 | return true; 357 | } 358 | #endif 359 | return false; 360 | } 361 | ``` 362 | 363 | ## 硬件中断处理的代码 364 | 365 | 在上面的代码中我们可以看到,如果vcpu在guest状态下才会通过post interrupt发送中断。否则还是走kvm_vcpu_kick。 366 | 367 | 但是对于硬件中断来说,中断发生时直接进入中断处理函数,而不会去判断vcpu状态。这要怎么处理呢? 368 | 369 | 暂时我能看到是当vcpu处于block状态时,会更换notification vector。也就是由另一个中断函数来响应这个事件。 370 | 371 | ``` 372 | vcpu_block() 373 | pre_block = vmx_pre_block 374 | pi_pre_block 375 | new.nv = POSTED_INTR_WAKEUP_VECTOR 376 | kvm_vcpu_block() 377 | post_block = vmx_post_block 378 | pi_post_block = __pi_post_block 379 | new.nv = POSTED_INTR_VECTOR 380 | ``` 381 | 382 | 而这个中断处理函数的内容是: 383 | 384 | ``` 385 | static void wakeup_handler(void) 386 | { 387 | struct kvm_vcpu *vcpu; 388 | int cpu = smp_processor_id(); 389 | 390 | spin_lock(&per_cpu(blocked_vcpu_on_cpu_lock, cpu)); 391 | list_for_each_entry(vcpu, &per_cpu(blocked_vcpu_on_cpu, cpu), 392 | blocked_vcpu_list) { 393 | struct pi_desc *pi_desc = vcpu_to_pi_desc(vcpu); 394 | 395 | if (pi_test_on(pi_desc) == 1) 396 | kvm_vcpu_kick(vcpu); 397 | } 398 | spin_unlock(&per_cpu(blocked_vcpu_on_cpu_lock, cpu)); 399 | } 400 | ``` 401 | 402 | 暂时还不是特别理解,待我以后好好研究。 403 | -------------------------------------------------------------------------------- /cpu/00-vcpu.md: -------------------------------------------------------------------------------- 1 | 每个计算机都有CPU,虚拟机也不例外。那这次我们就来研究一下虚拟CPU是怎么处理的。 2 | 3 | 还是按照类型层次来研究,首先有一个共同的父类 4 | 5 | [TYPE_CPU][1] 6 | 7 | 接着有不同类型的CPU。如 8 | 9 | [X86_CPU][2] 10 | 11 | [1]: /cpu/01-type_cpu.md 12 | [2]: /cpu/02-x86_cpu.md 13 | -------------------------------------------------------------------------------- /cpu/01-type_cpu.md: -------------------------------------------------------------------------------- 1 | Qemu中所有CPU类型都有一个父类TYPE_CPU。所以研究这个结构是开展后续研究的基础。 2 | 3 | # 继承关系 4 | 5 | 按照老规矩,我们还是先看一下类型的继承层次关系。 6 | 7 | ``` 8 | TYPE_OBJECT 9 | +-------------------------------+ 10 | |class_init | = object_class_init 11 | | | 12 | |instance_size | = sizeof(Object) 13 | +-------------------------------+ 14 | 15 | 16 | TYPE_DEVICE 17 | +-------------------------------+ 18 | |class_size | = sizeof(DeviceClass) 19 | |class_init | = device_class_init 20 | | | 21 | |instance_size | = sizeof(Object) 22 | |instance_init | = device_initfn 23 | |instance_finalize | = device_finalize 24 | | | 25 | |realize | = cpu_common_realizefn 26 | +-------------------------------+ 27 | 28 | 29 | TYPE_CPU 30 | +-------------------------------+ 31 | |class_size | = sizeof(CPUClass) 32 | |class_init | = cpu_class_init 33 | | | 34 | |instance_size | = sizeof(CPUState) 35 | |instance_init | = cpu_common_initfn 36 | |instance_finalize | = cpu_common_finalize 37 | +-------------------------------+ 38 | ``` 39 | 40 | 可以看到,TYPE_CPU是TYPE_DEVICE的子类,也就是CPU类型的初始化遵从[Device类型初始化的方法][1]。 41 | 42 | 大家可以看到realize函数也被我着重标出了。 43 | 44 | # 初始化 45 | 46 | 针对TYPE_CPU,我们能看到的初始化过程就是普通的类型初始化的流程,以及Device类型realize时所做的工作。 47 | 48 | 具体的工作流程将根据不同的CPU类型而有所不同,还需要按照不同的CPU和Machine类型来处理。 49 | 50 | [1]: /device_model/04-DeviceClass_instance.md 51 | -------------------------------------------------------------------------------- /cpu/02-x86_cpu.md: -------------------------------------------------------------------------------- 1 | 正如上一节所说,Qemu中对不同类型的CPU做了具体的模拟。而且根据不同的CPU类型和Machine类型其初始化过程又有些不同。 2 | 3 | 这里我们就来看看X86_CPU。 4 | 5 | # 继承关系 6 | 7 | ``` 8 | TYPE_DEVICE 9 | +-------------------------------+ 10 | |class_size | = sizeof(DeviceClass) 11 | |class_init | = device_class_init 12 | | | 13 | |instance_size | = sizeof(Object) 14 | |instance_init | = device_initfn 15 | |instance_finalize | = device_finalize 16 | | | 17 | |realize | = x86_cpu_realizefn 18 | +-------------------------------+ 19 | 20 | 21 | TYPE_CPU 22 | +-------------------------------+ 23 | |class_size | = sizeof(CPUClass) 24 | |class_init | = cpu_class_init 25 | | | 26 | |instance_size | = sizeof(CPUState) 27 | |instance_init | = cpu_common_initfn 28 | |instance_finalize | = cpu_common_finalize 29 | +-------------------------------+ 30 | 31 | 32 | 33 | TYPE_X86_CPU 34 | +-------------------------------+ 35 | |abstract | = true 36 | |class_size | = sizeof(X86CPUClass) 37 | |class_init | = x86_cpu_common_class_init 38 | | | 39 | |instance_size | = sizeof(X86CPU) 40 | |instance_init | = x86_cpu_initfn 41 | | | 42 | |parent_realize | = cpu_common_realizefn 43 | +-------------------------------+ 44 | 45 | 46 | base-x86_64-cpu qemu64-x86_64-cpu host-x86_64-cpu 47 | +-------------------------------+ +-------------------------------+ +-------------------------------+ 48 | |class_size | |class_size | |class_size | 49 | |class_init | = x86_cpu_base_class_init |class_init | = x86_cpu_cpudef_class_init |class_init | = host_x86_cpu_class_init 50 | | | | | | | 51 | |instance_size | |instance_size | |instance_size | 52 | |instance_init | |instance_init | |instance_init | 53 | +-------------------------------+ +-------------------------------+ +-------------------------------+ 54 | ``` 55 | 56 | 这个继承关系略有点让人烦 57 | 58 | * 首先大家都有一个TYPE_X86_CPU的父类 59 | * 这个父类下又可以生出好多子类 60 | * 貌似现在默认使用的是qemu64-x86_64-cpu 61 | 62 | # 类型定义 63 | 64 | 首先我们要来说说CPU类型定义,不仅使用了常见的方式,还用了一个函数来注册CPU类型。 65 | 66 | ``` 67 | static void x86_cpu_register_types(void) 68 | { 69 | int i; 70 | 71 | type_register_static(&x86_cpu_type_info); 72 | for (i = 0; i < ARRAY_SIZE(builtin_x86_defs); i++) { 73 | x86_register_cpudef_type(&builtin_x86_defs[i]); 74 | } 75 | type_register_static(&max_x86_cpu_type_info); 76 | type_register_static(&x86_base_cpu_type_info); 77 | #if defined(CONFIG_KVM) || defined(CONFIG_HVF) 78 | type_register_static(&host_x86_cpu_type_info); 79 | #endif 80 | } 81 | ``` 82 | 83 | 是不是比之前的有点丑?丑的还没有展开呢。 84 | 85 | x86_register_cpudef_type()会对builtin_x86_defs数组一次进行注册,里面标明了具体cpu的型号和对应能支持的功能。那里面简直就是。。。当然看着看着也就习惯了。 86 | 87 | ## 指定CPU类型 88 | 89 | 既然定义了这么多的CPU类型,那就可以在启动qemu的时候指定才对。方法如下: 90 | 91 | ``` 92 | -cpu MODEL 93 | ``` 94 | 95 | 这里的MODEL就是你可以指定的类型了。具体qemu支持哪些类型,可以通过命令**qemu -cpu help**来查看。 96 | 97 | 故事讲到这里就结束本来也是可以的,不过我打算再深入看一下代码中是哪里解析命令行的CPU类型并且找到我们注册的这些类型的。 98 | 99 | 说起来也不难,就是在main函数中调用了parse_cpu_option这个函数。 100 | 101 | ``` 102 | parse_cpu_optioin(), parse option "cpu" 103 | model_pieces = g_strsplit(cpu_option, ",", 2) 104 | oc = cpu_class_by_name(CPU_RESOLVING_TYPE, model_pieces) 105 | cpu_type = object_class_get_name(oc); 106 | cc->class_by_name(cpu_model) = x86_cpu_class_by_name 107 | cc->parse_features(cpu_type, ) = x86_cpu_parse_featurestr 108 | ``` 109 | 110 | 看明白了,套路就是这么简单。 111 | 112 | 最终得到的这个类型将被赋值到current_machine->cpu_type等待这后续初始化过程中被使用。 113 | 114 | 除此之外,还解析了是否有增加或者减少的feature。比如命令行中如下: 115 | 116 | ``` 117 | -cpu host,+movdir64b,-movdiri 118 | ``` 119 | 120 | 这部分将在初始化cpu的时候用到,这里知道就行了。 121 | 122 | # 初始化 123 | 124 | ## 从创建Machine开始 125 | 126 | CPU和Machine看来还是非常紧密联系在一起的。这点我们从创建cpu的流程中可以看出。 127 | 128 | 在PCMachine上,cpu初始化的一个简单流程如下: 129 | 130 | ``` 131 | pc_init1/pc_q35_init 132 | x86_cpus_init 133 | x86_cpu_new() 134 | object_new(MACHINE(x86ms)->cpu_type) 135 | ``` 136 | 137 | 看到了熟悉的cpu_type没有?对了,这个就是之前解析cpu类型时得到的值。由此证明了命令行传入的类型会用来创建cpu。 138 | 139 | ## X86 CPU的初始化细节 140 | 141 | CPU的初始化涉及到很多内容,比如支持什么特性,创建vcpu线程,如何接收信号等。在这里我们只是宏观得展开,具体细节有待专门章节讲解。 142 | 143 | ``` 144 | object_new(MACHINE(x86ms)->cpu_type) 145 | cpu_common_initfn 146 | cpu_exec_initfn() 147 | x86_cpu_initfn 148 | x86_cpu_register_feature_bit_props() 149 | x86_cpu_load_model(xcc->model) 150 | x86_cpu_realizefn 151 | x86_cpu_expand_features(), expand cpuid 152 | plus_features, passed from qemu command line 153 | minus_features, passed from qemu command line 154 | x86_cpu_filter_features() 155 | cpu_exec_realizefn() 156 | cpu_list_add() 157 | vmstate_register() 158 | mce_init() 159 | qemu_init_vcpu 160 | qemu_kvm_start_vcpu->qemu_kvm_cpu_thread_fn 161 | x86_cpu_apic_realize() 162 | object_property_set_bool(OBJECT(cpu->apic_state), true, "realized",) 163 | Map APIC MMIO area 164 | xcc->parent_realize = cpu_common_realizefn 165 | ``` 166 | 167 | 作为DEVICE类型的子类,初始化流程自然包含了两个方面: 168 | 169 | * instance_init的层次调用 170 | * realize函数的调用 171 | 172 | 仔细看图的朋友估计已经发现了,初始化过程中的第二点,也就是类型中的realize函数发生了微妙的变化。 173 | 174 | * Device类型的realize变成了 x86_cpu_realizefn 175 | * TYPE_X86_CPU的realize变成了 cpu_common_realizefn 176 | 177 | 这个偷梁换柱的工作是 x86_cpu_common_class_init 干的。 178 | 179 | 所以当一个cpu要实例化的时候,调用的realize函数会走到x86_cpu_realizefn,而不是通常的cpu_common_realizefn。 180 | 181 | 在整个realize过程中,主要完成了几件事: 182 | 183 | * 整理了feature,按照当前的了解就是哪些cpuid是支持的 184 | * 启动了vcpu线程 185 | * realize了apic 186 | 187 | 因为每个cpu对应一个线程,每个cpu上也有自己的apic,所以每个vcpu都会对应去做这样的操作。 188 | -------------------------------------------------------------------------------- /device_model/00-devices.md: -------------------------------------------------------------------------------- 1 | qemu作为一个虚拟机的软件,其重要功能之一就是模拟设备。说实话,这个设备模拟的模型还挺复杂的。 2 | 3 | 看过好几次都没有记清楚,这次重新梳理一遍,并记录在此。 4 | 5 | # 设备类型注册 6 | 7 | qemu中模拟的每一种设备都在代码中对应了一个类型,这个类型在使用之前需要注册到系统中。这样的好处是后续增添设备的流程变得简单化了。 8 | 9 | 这一节就来看看设备类型注册的流程。 10 | 11 | [设备类型注册][1] 12 | 13 | # 设备类型初始化 14 | 15 | 设备类型注册后,在需要使用之前得初始化该类型,并生成对应得ObjectClass对象。 16 | 17 | [设备类型初始化][2] 18 | 19 | # 设备实例化 20 | 21 | 接着就是实例化设备类型,也就是真的生成一个设备给虚拟机使用。 22 | 23 | [设备实例化][3] 24 | 25 | # DeviceClass实例化细节 26 | 27 | 对于qemu中一个"device"设备,除了实例化中instance_init函数之外,还隐藏了很多实现的细节。 28 | 29 | [DeviceClass实例化细节][4] 30 | 31 | # 面向对象的设备模型 32 | 33 | 在整理了一遍设备类型和实例的初始化过程后,发现qemu的整个设备模型是完整的面向对象模型。 34 | 35 | 小生斗胆在这里总结一下整个面向对象的模型架构 36 | 37 | [面向对象的设备模型][5] 38 | 39 | # 接口 40 | 41 | 随着系统的复杂,设备模型中又提出了接口的概念。没怎么用过java,也不知道概念是不是类似。 42 | 43 | 原本不想看这个部分,谁想到代码中使用到接口的地方还挺多。所以只好硬着头皮看了一遍。 44 | 45 | [接口][6] 46 | 47 | # 类型、对象和接口之间的转换 48 | 49 | 在设备模型中我们涉及了三个概念: 50 | 51 | * 类型 52 | * 对象 53 | * 接口 54 | 55 | 三者之间相互独立又互有关联,在代码中我们也通常会在这几个成员之间转换。 56 | 57 | 我们单独列出一节总结一下他们之间转换的故事。 58 | 59 | [类型、对象和接口之间的转换][7] 60 | 61 | # PCDIMM设备 62 | 63 | 最后我们以PCDIMM设备为例,详细剖析一下该设备初始化并加入系统的过程。 64 | 65 | [PCDIMM][8] 66 | 67 | [1]: /device_model/01-type_register.md 68 | [2]: /device_model/02-register_objectclass.md 69 | [3]: /device_model/03-objectclass_instance.md 70 | [4]: /device_model/04-DeviceClass_instance.md 71 | [5]: /device_model/05-device_oo_model.md 72 | [6]: /device_model/06-interface.md 73 | [7]: /device_model/07-class_obj_interface.md 74 | [8]: /device_model/pc_dimm/00-an_example.md 75 | -------------------------------------------------------------------------------- /device_model/01-type_register.md: -------------------------------------------------------------------------------- 1 | 本小节主要讲清楚type_table这个hash table的由来。 2 | 3 | 为了比较清楚的解释这个流程,在本节中以e1000这种设备为例。其代码主要集中在hw/net/e1000.c这个文件中。 4 | 5 | # TypeInfo定义设备 6 | 7 | qemu中注册的每个设备都由一个TypeInfo类型来定义。这个定义的内容不太多,我就直接上代码了。 8 | 9 | ``` 10 | struct TypeInfo 11 | { 12 | const char *name; 13 | const char *parent; 14 | 15 | size_t instance_size; 16 | void (*instance_init)(Object *obj); 17 | void (*instance_post_init)(Object *obj); 18 | void (*instance_finalize)(Object *obj); 19 | 20 | bool abstract; 21 | size_t class_size; 22 | 23 | void (*class_init)(ObjectClass *klass, void *data); 24 | void (*class_base_init)(ObjectClass *klass, void *data); 25 | void (*class_finalize)(ObjectClass *klass, void *data); 26 | void *class_data; 27 | 28 | InterfaceInfo *interfaces; 29 | }; 30 | ``` 31 | 32 | 那对于一个e1000设备,这个类型是什么样子的呢? 33 | 34 | ``` 35 | #define TYPE_E1000_BASE "e1000-base" 36 | 37 | static const TypeInfo e1000_base_info = { 38 | .name = TYPE_E1000_BASE, 39 | .parent = TYPE_PCI_DEVICE, 40 | .instance_size = sizeof(E1000State), 41 | .instance_init = e1000_instance_init, 42 | .class_size = sizeof(E1000BaseClass), 43 | .abstract = true, 44 | .interfaces = (InterfaceInfo[]) { 45 | { INTERFACE_CONVENTIONAL_PCI_DEVICE }, 46 | { }, 47 | }, 48 | }; 49 | ``` 50 | 51 | 暂时我们不关系其他内容,只看到定义时赋值的头两个值是: 52 | 53 | * name: 本设备类型的名称 54 | * parent:父设备类型的名称 55 | 56 | 这里可以看出在qemu设备模型中使用**名称作为类型的唯一标识**的,并且还存在了父子关系(这点我们在后面再讲)。 57 | 58 | # 类型注册函数type_register() 59 | 60 | 定义了设备类型后,需要做的是注册这个类型。这样qemu才知道现在可以支持这种设备了。 61 | 62 | 注册的函数就是type_register(),做的工作也很简单: 63 | 64 | * 通过type_new()生成一个TypeInfo对应的TypeImpl类型 65 | * 并以name为关键字添加到名为type_table的一个hash table中 66 | 67 | 假如我们用一个图来描述,大概可以画成这样。 68 | 69 | ``` 70 | type_table(GHashTable) ; this is a hash table with name as the key 71 | +-----------------------+ 72 | | | 73 | +-----------------------+ 74 | | 75 | v 76 | +--------------------------+ +----------------------------+ 77 | |TypeImpl* | <--- type_new() | TypeInfo | 78 | | name | | name | 79 | | parent | | parent | 80 | | | | | 81 | | class_size | | class_size | 82 | | class_init | | class_init | 83 | | class_base_init | | class_base_init | 84 | | class_finalize | | class_finalize | 85 | | class_data | | class_data | 86 | | | | | 87 | | instance_size | | instance_size | 88 | | instance_init | | instance_init | 89 | | instance_post_init | | instance_post_init | 90 | | instance_finalize | | instance_finalize | 91 | | | | | 92 | | abstract | | abstract | 93 | | interfaces | | interfaces | 94 | | num_interfaces | | num_interfaces | 95 | +--------------------------+ +----------------------------+ 96 | | 97 | v 98 | +--------------------------+ +----------------------------+ 99 | |TypeImpl* | <--- type_new() | TypeInfo | 100 | | TYPE_MACHINE | | TYPE_MACHINE | 101 | +--------------------------+ +----------------------------+ 102 | ``` 103 | 104 | 可以看到,几乎所有的成员两者是一致的。这里所做的工作就是把内容复制了一遍。 105 | 106 | 为啥不直接用呢?不懂。 107 | 108 | # 何时注册设备类型 109 | 110 | 不过这些都不难,着重我想说的是type_register调用的时机。而这个过程又分了两步走: 111 | 112 | * “**注册**”设备注册函数 113 | * “**执行**”设备类型注册 114 | 115 | 这个东西是有点绕,那就以e1000为例来看。 116 | 117 | ## 注册设备注册函数 118 | 119 | 在e1000的实现中,我们可以看到如下的过程: 120 | 121 | ``` 122 | static void e1000_register_types(void) 123 | { 124 | ... 125 | type_register(&type_info); 126 | } 127 | 128 | type_init(e1000_register_types) 129 | ``` 130 | 131 | 神奇的地方就在这个type_init(),使用了一个我以前不知道的gcc方法__attribute__((constructor))。 132 | 133 | 来看看函数的定义先: 134 | 135 | ``` 136 | #define type_init(function) module_init(function, MODULE_INIT_QOM) 137 | 138 | #define module_init(function, type) \ 139 | static void __attribute__((constructor)) do_qemu_init_ ## function(void) \ 140 | { \ 141 | register_module_init(function, type); \ 142 | } 143 | ``` 144 | 145 | 因为使用了__attribute__((constructor))修饰,所以这个函数将会在main函数执行前被执行。那这个register_module_init()函数又干了啥呢? 146 | 147 | ``` 148 | void register_module_init(void (*fn)(void), module_init_type type) 149 | { 150 | ModuleEntry *e; 151 | ModuleTypeList *l; 152 | 153 | e = g_malloc0(sizeof(*e)); 154 | e->init = fn; 155 | e->type = type; 156 | 157 | l = find_type(type); 158 | 159 | QTAILQ_INSERT_TAIL(l, e, node); 160 | } 161 | ``` 162 | 163 | 所以说qemu有时候有点蛋疼。又整了一个链表,把设备类型注册函数添加到里面。在这个e1000的例子中就是e1000_register_types这个函数。 164 | 165 | 细心的朋友可能还注意到了,这个链表还分了类型。对type_init()而言,这个类型是**MODULE_INIT_QOM**。记住这个,后面我们将会用到。 166 | 167 | 来看一下这个注册函数的链表: init_type_list[MODULE_INIT_MAX] 168 | 169 | ``` 170 | init_type_list[MODULE_INIT_MAX] 171 | +-----------------------------+ 172 | |MODULE_INIT_BLOCK | 173 | | | 174 | | | 175 | +-----------------------------+ 176 | |MODULE_INIT_OPTS | 177 | | | 178 | | | 179 | +-----------------------------+ +-----------------------+ +-----------------------+ 180 | |MODULE_INIT_QOM | ---->| |---->| | 181 | | | |e1000_register_types | |pc_dimm_register_types | 182 | | | | --> type_register() | | --> type_register() | 183 | +-----------------------------+ +-----------------------+ +-----------------------+ 184 | |MODULE_INIT_TRACE | 185 | | | 186 | | | 187 | +-----------------------------+ 188 | ``` 189 | 190 | 这样就形成了一个注册函数的数组,不同的类型各自添加到这个数组中。 191 | 192 | ## 执行设备类型注册 193 | 194 | 刚才忙活了一堆,其实这个类型注册函数e1000_register_types还没有被执行到。那究竟什么时候执行真正的类型注册呢? 195 | 196 | 让偶来揭示谜团: 197 | 198 | ``` 199 | main() 200 | module_call_init(MODULE_INIT_QOM); 201 | { 202 | ModuleTypeList *l; 203 | ModuleEntry *e; 204 | 205 | l = find_type(type); 206 | 207 | QTAILQ_FOREACH(e, l, node) { 208 | e->init(); 209 | } 210 | } 211 | ``` 212 | 213 | 看到上一节中的MODULE_INIT_QOM了没有?其作用就是找到MODULE_INIT_QOM对应的链表,执行其中的init函数。也就是我们刚通过register_module_init添加进去的e1000_register_types了。 214 | 215 | 到这,终于算是把type_table这个hash table的由来说清楚了。 216 | -------------------------------------------------------------------------------- /device_model/02-register_objectclass.md: -------------------------------------------------------------------------------- 1 | # 一一对应 2 | 3 | 注册类型TypeImpl之后就需要初始化该类型,如果打开TypeImpl这个结构体,可以看到其中有个叫class的成员。初始化其实就是初始化的它。 4 | 5 | ``` 6 | TypeImpl ObjectClass 7 | +----------------------+ +----------------------+ 8 | |class |<------------->|type | 9 | | (ObjectClass*) | | (TypeImpl *) | 10 | +----------------------+ +----------------------+ 11 | ``` 12 | 13 | 进行初始化的具体函数是 type_initialize()。 14 | 15 | 摘取其中主要的流程如下: 16 | 17 | ``` 18 | type_initialize() 19 | ... 20 | parent = type_get_parent(ti); 21 | if (parent) { 22 | type_initialize(parent); 23 | ... 24 | for (i = 0; i < ti->num_interfaces; i++) { 25 | TypeImpl *t = type_get_by_name(ti->interfaces[i].typename); 26 | ... 27 | type_initialize_interface(ti, t, t); 28 | } 29 | } 30 | 31 | ... 32 | 33 | while (parent) { 34 | if (parent->class_base_init) { 35 | parent->class_base_init(ti->class, ti->class_data); 36 | } 37 | parent = type_get_parent(parent); 38 | } 39 | 40 | if (ti->class_init) { 41 | ti->class_init(ti->class, ti->class_data); 42 | } 43 | ``` 44 | 45 | 其中主要做了几件事: 46 | 47 | * 设置类型的大小并创建该类型 48 | * 设置类型实例的大小,为实例化设备做准备 49 | * 初始化父设备类型,如果没有的话 50 | * 初始化接口类型 51 | * 调用父设备类型的class_base_init初始化自己 52 | * 调用class_init初始化自己 53 | 54 | 在这里看不出什么具体的东西,因为每种设备类型将执行不同的初始化函数。 55 | 56 | 但是有一点可以看出的是,qemu设备类型有一个树形结构。或者说是一种面向对象的编程模型,需要初始化父类后,再初始化自己。 57 | 58 | # 接口类 59 | 60 | 在看了一段时间代码后,发现一个类型还能定义其相关的接口类型。 61 | 62 | 比如我们看e1000设备的定义可以看到在最后有定义一个interfaces成员。 63 | 64 | ``` 65 | #define TYPE_E1000_BASE "e1000-base" 66 | 67 | static const TypeInfo e1000_base_info = { 68 | .name = TYPE_E1000_BASE, 69 | .parent = TYPE_PCI_DEVICE, 70 | ... 71 | .interfaces = (InterfaceInfo[]) { 72 | { INTERFACE_CONVENTIONAL_PCI_DEVICE }, 73 | { }, 74 | }, 75 | }; 76 | ``` 77 | 78 | 那么我们来看看这个成员如何初始化,如何使用。 79 | 80 | 在 type_initialize()函数中有一段 81 | 82 | ``` 83 | for (i = 0; i < ti->num_interfaces; i++) { 84 | TypeImpl *t = type_get_by_name(ti->interfaces[i].typename); 85 | ... 86 | type_initialize_interface(ti, t, t); 87 | } 88 | ``` 89 | 90 | 这个就是在初始化ti这个类型的接口类。好在这个函数type_initialize_interface()不长,我们直接打开看看。 91 | 92 | ``` 93 | static void type_initialize_interface(TypeImpl *ti, TypeImpl *interface_type, 94 | TypeImpl *parent_type) 95 | { 96 | InterfaceClass *new_iface; 97 | TypeInfo info = { }; 98 | TypeImpl *iface_impl; 99 | 100 | info.parent = parent_type->name; 101 | info.name = g_strdup_printf("%s::%s", ti->name, interface_type->name); 102 | info.abstract = true; 103 | 104 | iface_impl = type_new(&info); 105 | iface_impl->parent_type = parent_type; 106 | type_initialize(iface_impl); 107 | g_free((char *)info.name); 108 | 109 | new_iface = (InterfaceClass *)iface_impl->class; 110 | new_iface->concrete_class = ti->class; 111 | new_iface->interface_type = interface_type; 112 | 113 | ti->class->interfaces = g_slist_append(ti->class->interfaces, 114 | iface_impl->class); 115 | } 116 | ``` 117 | 118 | 说实话,这个东西有点绕。因为在这个过程中又注册了一个新的类型并做了初始化。 119 | 120 | ``` 121 | TypeImpl 122 | +------------------------------------+ 123 | |name | 124 | | "conventional-pci-device" | 125 | |class (struct InterfaceClass) | 126 | | | 127 | +------------------------------------+ 128 | ^ 129 | | 130 | | 131 | TypeImpl TypeImpl | 132 | +------------------------------------+ 133 | +--------------------------+ |parent ---+ | 134 | |name | |name | 135 | | "e1000-base" | | "e1000::conventional-pci-device" | 136 | |class (E1000BaseClass) | +----------->|class (struct InterfaceClass) | 137 | | ^ interfaces -----|-----+ | concrete_class ----+ | 138 | | | | +------------------------------------+ 139 | +--------------------------+ | 140 | | | 141 | +---------------------------------------------------------------------+ 142 | ``` 143 | 144 | 最后的结果我尝试用图来展示。 145 | 146 | * 创建了一个INTERFACE_CONVENTIONAL_PCI_DEVICE类型的子类,也是一个接口类型 147 | * E1000BaseClass中的interfaces会指向新建的接口类 148 | * 而接口类中的concrete_class会指向E1000BaseClass 149 | -------------------------------------------------------------------------------- /device_model/03-objectclass_instance.md: -------------------------------------------------------------------------------- 1 | 有了设备类型,接着就要实例化设备了。这个理解起来就好像我们可以在一台机器上安装多个同种类的网卡。 2 | 3 | 实例化完成后会产生如下的对应关系: 4 | 5 | ``` 6 | ObjectClass Object 7 | +---------------+ +----------------------+ 8 | | | <------------|class | 9 | | | | (ObjectClass*) | 10 | +---------------+ +----------------------+ 11 | ``` 12 | 13 | 这个过程由object_initialize()->object_initialize_with_type()实现。其过程实在有点简单: 14 | 15 | * 建立obj和ObjectClass之间的关联 16 | * 递归调用父类型的instance_init 17 | * 调用自己的instance_init 18 | 19 | 怎么样,是不是有点超级简单了? 20 | 21 | > 太单纯了! 22 | -------------------------------------------------------------------------------- /device_model/04-DeviceClass_instance.md: -------------------------------------------------------------------------------- 1 | 上节描述的设备模型抽象了的实例化流程,正是因为是一个抽象的实例化过程,虽然看着很简单,但也失去了很多真实场景下丰富的细节。 2 | 3 | 那现在我们来看看DeviceClass的实例化。因为很多具体的设备的父类就是这个DeviceClass,所以对这个类型实例化过程的了解对理解Qemu中大部分设备的初始化有很重要的帮助。 4 | 5 | ``` 6 | +--------------------------+ +----------------------+ 7 | | | | | 8 | | ObjectClass | <-------------------------| Object | 9 | | class_init | | instance_init | 10 | | | |(object_instance_init)| 11 | +--------------------------+ +----------------------+ 12 | | | 13 | | | 14 | | | 15 | v v 16 | +--------------------------+ +----------------------+ 17 | | | | | 18 | | DeviceClass | <--------------------- | DeviceState | 19 | | class_init | | instance_init | 20 | | (device_class_init) | | (device_initfn) | 21 | | | | | 22 | | realize | overwrite by child class | | 23 | | unrealize | | | 24 | +--------------------------+ +----------------------+ 25 | ``` 26 | 27 | 这张图显示了DeviceClass相关的父类和对应的对象之间的关系。而我们现在关注的就是它的实例化函数**device_initfn**。 28 | 29 | # device_initfn 30 | 31 | 这个函数并不长,看着也很简单。 32 | 33 | ``` 34 | static void device_initfn(Object *obj) 35 | { 36 | DeviceState *dev = DEVICE(obj); 37 | ObjectClass *class; 38 | Property *prop; 39 | 40 | if (qdev_hotplug) { 41 | dev->hotplugged = 1; 42 | qdev_hot_added = true; 43 | } 44 | 45 | dev->instance_id_alias = -1; 46 | dev->realized = false; 47 | 48 | object_property_add_bool(obj, "realized", 49 | device_get_realized, device_set_realized, NULL); 50 | object_property_add_bool(obj, "hotpluggable", 51 | device_get_hotpluggable, NULL, NULL); 52 | object_property_add_bool(obj, "hotplugged", 53 | device_get_hotplugged, NULL, 54 | &error_abort); 55 | 56 | class = object_get_class(OBJECT(dev)); 57 | do { 58 | for (prop = DEVICE_CLASS(class)->props; prop && prop->name; prop++) { 59 | qdev_property_add_legacy(dev, prop, &error_abort); 60 | qdev_property_add_static(dev, prop, &error_abort); 61 | } 62 | class = object_class_get_parent(class); 63 | } while (class != object_class_by_name(TYPE_DEVICE)); 64 | 65 | object_property_add_link(OBJECT(dev), "parent_bus", TYPE_BUS, 66 | (Object **)&dev->parent_bus, NULL, 0, 67 | &error_abort); 68 | QLIST_INIT(&dev->gpios); 69 | } 70 | ``` 71 | 72 | 就我所知,这个函数主要做了两件事: 73 | 74 | * 给设备设置了三个共有的属性: realized, hotpluggable, hotplugged 75 | * 根据每个类型定义时的props字段,设置各自的属性 76 | 77 | # 设备属性的设置 78 | 79 | 先来讲讲这个属性的设置是设置的什么。 80 | 81 | 当我们运行Qemu的时候,通常会在命令行写上一串参数来表示虚拟机的硬件配置。或者我们通过Qemu monitor添加设备时输入的硬件参数。 82 | 83 | 比如: 84 | 85 | ``` 86 | object_add memory-backend-ram,id=ram0,size=1G 87 | device_add pc-dimm,id=dimm0,memdev=ram0,node=0 88 | ``` 89 | 90 | 我们就看pc-dimm设备,其中有参数memdev=ram0。 91 | 92 | 从命令行上,这个参数的作用其实是关联pc-dimm和memory-backend-ram。那在代码中是如何做的呢? 93 | 94 | 先来看pc-dimm设备的属性props定义: 95 | 96 | ``` 97 | static Property pc_dimm_properties[] = { 98 | ... 99 | DEFINE_PROP_LINK(PC_DIMM_MEMDEV_PROP, PCDIMMDevice, hostmem, 100 | TYPE_MEMORY_BACKEND, HostMemoryBackend *), 101 | ... 102 | }; 103 | ``` 104 | 105 | 再回过去看device_initfn中那个对props的循环。在这里就关联了pc-dimm和memory-backend。 106 | 107 | # realized属性的功效 108 | 109 | 那接着看看这个属性的被设置时都发生了些什么。 110 | 111 | ``` 112 | static void device_set_realized(Object *obj, bool value, Error **errp) 113 | { 114 | DeviceState *dev = DEVICE(obj); 115 | DeviceClass *dc = DEVICE_GET_CLASS(dev); 116 | 117 | ... 118 | 119 | if (dc->realize) { 120 | dc->realize(dev, &local_err); 121 | } 122 | 123 | ... 124 | 125 | } 126 | ``` 127 | 128 | 从截取的代码片段中可以看到,当属性被设置时会调用DeviceClass中的realize函数。而这个realized函数就是那个被隐藏了的实例化套路。而很多设备初始化的细节都隐藏在realized函数中。 129 | 130 | **注**:你再往细了看,device_set_realized函数本身也隐藏着很多实现的细节,这里我们就不展开了。 131 | 132 | 到这里我想大家一定会有两个问题。 133 | 134 | * 这个属性是什么时候设置的 135 | * 这个realize函数长什么样 136 | 137 | 那我们兵分两路,先来看看这个属性是什么时候设置的。 138 | 139 | ## 谁动了我的realized属性 140 | 141 | 这个东西还真不好找,而且可能存在多个设置属性的路径,我尝试用下面一个代码片段解释其中一个路径。 142 | 143 | ``` 144 | main() 145 | qemu_opts_foreach(qemu_find_opts("device"),device_init_func, NULL, NULL) 146 | qdev_device_add() 147 | dev = DEVICE(object_new(driver)); 148 | object_property_set_bool(OBJECT(dev), true, "realized", &err); 149 | ``` 150 | 151 | 这下明确了,当我们生成一个设备对象(object)后,就会调用方法来设置它的realized属性。 152 | 153 | 也就是我们会**人为**得去触发这个事件。 154 | 155 | ## 一个真实的realize函数 156 | 157 | 既然刚才讲到了pc-dimm,那我们就来看看pc-dimm的realize。 158 | 159 | 通常这个函数在class_init中设置,所以我们看到在pc_dimm_class_init中realize被设置成了pc_dimm_realize。 160 | 161 | 打开这个函数pc_dimm_realize可以看到它会进一步调用子类PCDIMMDeviceClass的realize。 162 | 163 | 这样就形成了一个类似面向对象的初始化过程。 164 | 165 | ``` 166 | +--------------------------+ 167 | | | 168 | | DeviceClass | 169 | | realize | pc_dimm_realize() 170 | +--------------------------+ 171 | | 172 | | 173 | | 174 | v 175 | +--------------------------+ 176 | | | 177 | | PCDIMMDeviceClass | 178 | | realize | who? leave it a question here :-) 179 | +--------------------------+ 180 | ``` 181 | -------------------------------------------------------------------------------- /device_model/05-device_oo_model.md: -------------------------------------------------------------------------------- 1 | 书接上回,说到device设备在设置了realized属性后会调用DeviceClass的realize函数进行初始化。那这个函数究竟是什么的? 2 | 3 | 要说清楚这件事,还得找一个具体的设备来说才能够看得清。 4 | 5 | # qemu中的函数重载 6 | 7 | DeviceClass类型中的realize成员在device_class_init()初始化函数中并没有被设置。这个函数的具体对应是在DeviceClass的子类型的类型初始化函数中设置的。 8 | 9 | 比如PCIDeviceClass就是DeviceClass的一个子类型。在它的类型初始化函数pci_device_class_init中可以看到下面这段代码: 10 | 11 | ``` 12 | k->realize = pci_qdev_realize; 13 | ``` 14 | 15 | 所以在qemu的设备模型中其实采用了面向对象的编程方法。 16 | 17 | # 面向对象的设备模型 18 | 19 | 讲了这么多,恐怕大家也听晕了。正所谓耳听为虚,眼见为实。我们来画一个图,看看e1000这个设备面向对象模型的样子。 20 | 21 | ``` 22 | +--------------------------+ +----------------------+ 23 | | | | | 24 | | ObjectClass | <-------------------------| Object | 25 | | class_init | | instance_init | 26 | | | |(object_instance_init)| 27 | +--------------------------+ +----------------------+ 28 | | | 29 | | | 30 | | | 31 | v v 32 | +--------------------------+ +----------------------+ 33 | | | | | 34 | | DeviceClass | <--------------------- | DeviceState | 35 | | class_init | | instance_init | 36 | | (device_class_init) | | (device_initfn) | 37 | | | | | 38 | | realize | overwrite by child class | | 39 | | unrealize | | | 40 | +--------------------------+ +----------------------+ 41 | | | 42 | | | 43 | | | 44 | v v 45 | +--------------------------+ +----------------------+ 46 | | | | | 47 | | PCIDeviceClass | <--------------------- | PCIDevice | 48 | | class_init | | instance_init | 49 | | (pci_device_class_init)| | (NULL) | 50 | | realize | | | 51 | | (pci_qdev_realize) | call PCIDevice->realize | | 52 | | unrealize | | | 53 | | (pci_qdev_unrealize) | | | 54 | +--------------------------+ +----------------------+ 55 | | | 56 | | | 57 | | | 58 | v v 59 | +--------------------------+ +----------------------+ 60 | | | | | 61 | | E1000BaseClass | <-------------------- | E1000State | 62 | | class_init | | instance_init | 63 | | (e1000_class_init) | | (e1000_instance_init)| 64 | | realize | | | 65 | | (pci_e1000_realize) | | | 66 | | unrealize | | | 67 | | | | | 68 | +--------------------------+ +----------------------+ 69 | ``` 70 | 71 | 在这张图中体现了这么几点: 72 | 73 | * 某个类型可以是另一个类型的子类型 74 | * 某个设备实例包含了父类型的实例 75 | * 实例和类型之间一一对应 76 | * 类型的成员可以被子类重写 77 | 78 | 到这里,感觉终于可以说对qemu的设备模型有了那么一点点的了解。 79 | -------------------------------------------------------------------------------- /device_model/06-interface.md: -------------------------------------------------------------------------------- 1 | 在一些设备类型定义中我们可以看到interfaces一栏。 2 | 3 | ``` 4 | static TypeInfo pc_dimm_info = { 5 | .name = TYPE_PC_DIMM, 6 | .parent = TYPE_DEVICE, 7 | ... 8 | .interfaces = (InterfaceInfo[]) { 9 | { TYPE_MEMORY_DEVICE }, 10 | { } 11 | }, 12 | }; 13 | ``` 14 | 15 | 那这一栏是什么意思呢?让我们来进一步打开看看。 16 | 17 | # Interface也是一种类型 18 | 19 | 进一步看,可以发现在Qemu模型中接口也是一种类型,并且也有父子关系。 20 | 21 | ``` 22 | static TypeInfo interface_info = { 23 | .name = TYPE_INTERFACE, 24 | .class_size = sizeof(InterfaceClass), 25 | .abstract = true, 26 | }; 27 | 28 | static const TypeInfo memory_device_info = { 29 | .name = TYPE_MEMORY_DEVICE, 30 | .parent = TYPE_INTERFACE, 31 | .class_size = sizeof(MemoryDeviceClass), 32 | }; 33 | ``` 34 | 35 | 但是和普通类型不同,他们只有对应的Class,而没有object。 36 | 37 | 我们尝试把这两个类型画成图,或许能看得更清楚一些。 38 | 39 | ``` 40 | +---------------------------------------------------------------+ 41 | | | 42 | v | 43 | TypeInfo TypeImpl* InterfaceClass | 44 | +---------------------+ +----------------------+ +-->+------------------------+ | 45 | |name | type_new()---> |name | | |parent_class | | 46 | | TYPE_INTERFACE | | TYPE_INTERFACE | | | (ObjectClass) | | 47 | +---------------------+ |class ---|----+ | type ----|------+ 48 | | (ObjectClass*) | |concrete_class | 49 | +----------------------+ | (ObjectClass*) | 50 | |interface_type | 51 | | (TypeImpl*) | 52 | +------------------------+ 53 | | 54 | | 55 | | 56 | v 57 | +---------------------------------------------------------------+ 58 | | MemoryDeviceClass | 59 | v +--------------------------+ | 60 | TypeInfo TypeImpl* |InterfaceClass | | 61 | +---------------------+ +----------------------+ +--> | +------------------------+ | 62 | |name | type_new()---> |name | | | |parent_class | | 63 | | TYPE_MEMORY_DEVICE| | TYPE_MEMORY_DEVICE | | | | (ObjectClass) | | 64 | +---------------------+ |class ---|----+ | | type ----|---+ 65 | | (ObjectClass*) | | |concrete_class | 66 | +----------------------+ | | (ObjectClass*) | 67 | | |interface_type | 68 | | | (TypeImpl*) | 69 | | +------------------------+ 70 | |get_addr | 71 | |set_addr | 72 | |... | 73 | | | 74 | +--------------------------+ 75 | ``` 76 | 77 | # 接口类型附属于设备类型 78 | 79 | 接口类型是一个很有意思的类型,刚才我们看到了TYPE_MEMORY_DEVICE会对应有一个MemoryDeviceClass。 80 | 81 | 对于普通的设备类型,那整个系统中就只会有这么一个对应的Class,但是接口类型则不是。 82 | 83 | 还是从代码上来看: 84 | 85 | ``` 86 | static void type_initialize(TypeImpl *ti) 87 | { 88 | ... 89 | for (i = 0; i < ti->num_interfaces; i++) { 90 | TypeImpl *t = type_get_by_name(ti->interfaces[i].typename); 91 | for (e = ti->class->interfaces; e; e = e->next) { 92 | TypeImpl *target_type = OBJECT_CLASS(e->data)->type; 93 | 94 | if (type_is_ancestor(target_type, t)) { 95 | break; 96 | } 97 | } 98 | 99 | if (e) { 100 | continue; 101 | } 102 | 103 | type_initialize_interface(ti, t, t); 104 | } 105 | ... 106 | } 107 | ``` 108 | 109 | 对于定义了interfaces成员的类型,在初始化中将通过type_initialize_interface来创建自己的接口类型。 110 | 而这个类型不仅在系统中注册,还会添加到class->interfaces链表上。 111 | 112 | ``` 113 | +----------------------------------------------------------------+-----+ 114 | | | | 115 | | MemoryDeviceClass | | 116 | v +--------------------------+ | | 117 | TypeImpl* |InterfaceClass | | | 118 | +----------------------+ +--> | +------------------------+ | | 119 | |name | | | |parent_class | | | 120 | | TYPE_MEMORY_DEVICE | | | | (ObjectClass) | | | 121 | |class ---|----+ | | type ----|----+ | 122 | | (ObjectClass*) | | |interface_type | | 123 | +----------------------+ | | (TypeImpl*) | | 124 | ^ | |concrete_class | | 125 | | | | (ObjectClass*) | | 126 | | | +------------------------+ | 127 | | |get_addr | | 128 | | |set_addr | | 129 | | |... | | 130 | | | | | 131 | | +--------------------------+ | 132 | | | 133 | | | 134 | | | 135 | TypeImpl +----------------------------------------------------------------+ | 136 | +--------------------------+ | | | | 137 | |name | | | MemoryDeviceClass | | 138 | | TYPE_PC_DIMM | v | +--------------------------+ | | 139 | +--------------------------+ TypeImpl* | |InterfaceClass | | | 140 | ^ +----------------------+ +--> | +------------------------+ | | 141 | | |name | | | |parent_class | | | 142 | PCDIMMDeviceClass | TYPE_PC_DIMM:: | | | | (ObjectClass) | | | 143 | +--------------------------+ | TYPE_MEMORY_DEVICE | | | | type ----|----+ | 144 | |parent_class | +----->|class ---|----+ | |interface_type ----|-----------+ 145 | | (ObjectClass) | | | (ObjectClass*) | | | (TypeImpl*) | 146 | | interfaces ----|------+ +----------------------+ | |concrete_class ----|----+ 147 | | | | | (ObjectClass*) | | 148 | +--------------------------+ | +------------------------+ | 149 | ^ |get_addr | | 150 | | |set_addr | | 151 | | |... | | 152 | | | | | 153 | | +--------------------------+ | 154 | | | 155 | | | 156 | +---------------------------------------------------------------------------------------------------------+ 157 | ``` 158 | 159 | 希望这个图能够对理解接口有一点点帮助。 160 | 161 | # 设置接口类型的“虚函数” 162 | 163 | 接口类型重要的成员就是它的虚函数了,那谁在什么时候设置呢? 164 | 165 | 因为接口类型是从属于设备类型的,所以虚函数的设置在设备类型初始化函数中设置。 166 | 167 | 对于pc-dimm设备,这个函数就是pc_dimm_class_init中了。 168 | 169 | ``` 170 | DeviceClass *dc = DEVICE_CLASS(oc); 171 | PCDIMMDeviceClass *ddc = PC_DIMM_CLASS(oc); 172 | MemoryDeviceClass mdc = MEMORY_DEVICE_CLASS(oc); 173 | mdc->get_addr = pc_dimm_md_get_addr; 174 | mdc->set_addr = pc_dimm_md_set_addr; 175 | ``` 176 | 177 | # 从设备类型到接口类型 178 | 179 | 在上面这段代码中有一个有意思的地方就是怎么从设备类型PC_DIMM_CLASS转换到接口类型MEMORY_DEVICE_CLASS的 180 | 181 | 让我们来看一眼: 182 | 183 | ``` 184 | #define MEMORY_DEVICE_CLASS(klass) \ 185 | OBJECT_CLASS_CHECK(MemoryDeviceClass, (klass), TYPE_MEMORY_DEVICE) 186 | 187 | #define OBJECT_CLASS_CHECK(class_type, class, name) \ 188 | ((class_type *)object_class_dynamic_cast_assert(OBJECT_CLASS(class), (name), \ 189 | __FILE__, __LINE__, __func__)) 190 | ``` 191 | 192 | 经过这两个宏,最后会落到函数object_class_dynamic_cast()。 193 | 194 | 而这个函数就是沿着klass->interfaces链表查找名字为TYPE_MEMORY_DEVICE的类型。如果找到了唯一的,那就范围它。 195 | 196 | 怎么样,现在是不是能看清楚这个函数的意义,以及设备类到接口类的转换了? 197 | -------------------------------------------------------------------------------- /device_model/07-class_obj_interface.md: -------------------------------------------------------------------------------- 1 | 因为用c语言实现了一个类面向对象的设备模型,在类型和实例之间的转换都需要手工实现。所以在代码中有几个重要的宏定义来完成这样的工作。 2 | 3 | # obj -> class 4 | 5 | 对应的obj找到它的class。 6 | 7 | ``` 8 | #define OBJECT_GET_CLASS(class, obj, name) \ 9 | OBJECT_CLASS_CHECK(class, object_get_class(OBJECT(obj)), name) 10 | ``` 11 | 12 | obj就是我们传入的对象实例, object_get_class就是获得obj->class这个成员,也就是对应的class。 13 | class就是这个对象对应的class的类型,name呢就是这个class的名字。 14 | 对于没有intefaces的类型来说,这就是一个强制类型转换。也就是得到了obj对应的类型。 15 | 16 | # ObjectClass -> 子Class 17 | 18 | 在上面的图中我们也看到,类型之间也有着父子关系。比如PCIDeviceClass就是DeviceClass的子类。 19 | 20 | 所以有时候在代码中我们需要从父类获得具体的子类。这个工作交给了: 21 | 22 | ``` 23 | #define OBJECT_CLASS_CHECK(class_type, class, name) \ 24 | ((class_type *)object_class_dynamic_cast_assert(OBJECT_CLASS(class), (name), \ 25 | __FILE__, __LINE__, __func__)) 26 | ``` 27 | 28 | 这个宏其实就是上面的那个宏,如果没有interfaces成员这也是一个强制类型转换。把class类型转换成class_type。 29 | 30 | 但是因为Qemu自身的面向对象的模型,子类和父类的基地址相同所以能够通过强制转换来得到子类。 31 | 32 | 其实这么看,反过来转换也是可以的。 33 | 34 | # Object -> 子Object 35 | 36 | 同上面的类型一样,object也有父子关系。所以当代码中需要从父对象转换到一个具体的子对象时就要用: 37 | 38 | ``` 39 | #define OBJECT_CHECK(type, obj, name) \ 40 | ((type *)object_dynamic_cast_assert(OBJECT(obj), (name), \ 41 | __FILE__, __LINE__, __func__)) 42 | ``` 43 | 44 | 对obj来讲就没有类型Class这么复杂了,在生产环境中纯粹就是强制类型转换,把obj类型转换到type类型。 45 | 因为按照Qemu的面向对象模型,父子obj的基地址是一样的。 46 | 47 | 48 | # Class -> InterfaceClass 49 | 50 | 这个转换和Class到子Class的方式一样,也是 51 | 52 | ``` 53 | #define OBJECT_CLASS_CHECK(class_type, class, name) \ 54 | ((class_type *)object_class_dynamic_cast_assert(OBJECT_CLASS(class), (name), \ 55 | __FILE__, __LINE__, __func__)) 56 | ``` 57 | 58 | 但是因为这个Class有interfaces成员,所以会在interfaces链表上查找名字为name的接口。 59 | 60 | 如果有则返回对应name的接口。 61 | 62 | # Object -> Interface 63 | 64 | 从上面的转换可以看出,如果想要从一个obj得到interface可以分成两步: 65 | 66 | * obj -> class 67 | * class -> interface 68 | 69 | 但是因为当Class有interface成员,且传入的name就是接口类型的名字时,obj可以直接转换成接口。 70 | 71 | 也就是用OBJECT_CLASS_CHECK可以直接转换得到。 72 | 73 | # obj -> 虚obj 74 | 75 | 所以虚obj就是obj对象对应的Class类型的接口类型的对象。这么说还真有点绕,估计我自己过段时间来看也得花点时间回忆回忆。 76 | 77 | 之所以我叫它虚obj,是因为接口类型本身是没有对应的对象的。所以这个东西还挺神奇,没想到代码里还会这么用。 78 | 79 | 大家也一定觉得不信,那先举个例子: 80 | 81 | ``` 82 | #define MEMORY_DEVICE(obj) \ 83 | INTERFACE_CHECK(MemoryDeviceState, (obj), TYPE_MEMORY_DEVICE) 84 | 85 | typedef struct MemoryDeviceState MemoryDeviceState; 86 | ``` 87 | 88 | 这个MemoryDeviceState就定义了这么一个类型,没有任何的成员。而它对应的Class类型是TYPE_MEMORY_DEVICE的接口类型。 89 | 90 | 而在代码中通常的用法还是要将这个虚obj再转换到接口类型。通常用法如下: 91 | 92 | ``` 93 | const MemoryDeviceState *md = MEMORY_DEVICE(obj); 94 | const MemoryDeviceClass *mdc = MEMORY_DEVICE_GET_CLASS(obj); 95 | ``` 96 | 97 | 这个obj就是一个真实的obj对象,而md则是对应的虚obj。其实他们俩的指是一样的,都指向了同一个地址。 98 | 99 | 还是觉得很神奇,为什么代码要这么写呢?就不能直接传obj么? 100 | 101 | 我能想到的一个作用就是在debug选项打开时能够检测类型,而不至于传错参数。 102 | -------------------------------------------------------------------------------- /device_model/pc_dimm/00-an_example.md: -------------------------------------------------------------------------------- 1 | 在总体上有了概念之后,我们就来看看实际的例子。比如 PCDIMM。 2 | 3 | 按照之前总结的顺序,分成几个步骤来看: 4 | 5 | [PCDIMM类型][1] 6 | 7 | [PCDIMM实例][2] 8 | 9 | [插入系统][3] 10 | 11 | [创建ACPI表][4] 12 | 13 | 最后再说一下PCDIMM的子类型NVDIMM的虚拟化。 14 | 15 | [NVDIMM][5] 16 | 17 | [1]: /device_model/pc_dimm/01-pc_dimm_class.md 18 | [2]: /device_model/pc_dimm/02-pc_dimm_instance.md 19 | [3]: /device_model/pc_dimm/03-plug.md 20 | [4]: /device_model/pc_dimm/04-dimm_acpi.md 21 | [5]: /device_model/pc_dimm/05-nvdimm.md 22 | -------------------------------------------------------------------------------- /device_model/pc_dimm/01-pc_dimm_class.md: -------------------------------------------------------------------------------- 1 | # 类型注册 2 | 3 | 类型看着比较简单。 4 | 5 | ``` 6 | static TypeInfo pc_dimm_info = { 7 | .name = TYPE_PC_DIMM, 8 | .parent = TYPE_DEVICE, 9 | .instance_size = sizeof(PCDIMMDevice), 10 | .instance_init = pc_dimm_init, 11 | .class_init = pc_dimm_class_init, 12 | .class_size = sizeof(PCDIMMDeviceClass), 13 | .interfaces = (InterfaceInfo[]) { 14 | { TYPE_MEMORY_DEVICE }, 15 | { } 16 | }, 17 | }; 18 | ``` 19 | 20 | # 类型初始化 21 | 22 | 初始化也比较简单。 23 | 24 | ``` 25 | static void pc_dimm_class_init(ObjectClass *oc, void *data) 26 | { 27 | DeviceClass *dc = DEVICE_CLASS(oc); 28 | PCDIMMDeviceClass *ddc = PC_DIMM_CLASS(oc); 29 | MemoryDeviceClass *mdc = MEMORY_DEVICE_CLASS(oc); 30 | 31 | dc->realize = pc_dimm_realize; 32 | dc->unrealize = pc_dimm_unrealize; 33 | dc->props = pc_dimm_properties; 34 | dc->desc = "DIMM memory module"; 35 | 36 | ddc->get_memory_region = pc_dimm_get_memory_region; 37 | ddc->get_vmstate_memory_region = pc_dimm_get_memory_region; 38 | 39 | mdc->get_addr = pc_dimm_md_get_addr; 40 | /* for a dimm plugged_size == region_size */ 41 | mdc->get_plugged_size = pc_dimm_md_get_region_size; 42 | mdc->get_region_size = pc_dimm_md_get_region_size; 43 | mdc->fill_device_info = pc_dimm_md_fill_device_info; 44 | } 45 | ``` 46 | 47 | 其重要着重的一点是设置了props。我们将在实例化过程中看到这个参数的作用。 48 | -------------------------------------------------------------------------------- /device_model/pc_dimm/02-pc_dimm_instance.md: -------------------------------------------------------------------------------- 1 | # 实例化 2 | 3 | 有了类型就可以实例化一个设备了。一台机器默认情况下就又pc-dimm设备,但是我们也可以通过参数人为添加。这样我们可以进一步了解设备注册和生成的步骤。 4 | 5 | 比如我们可以通过qemu monitor输入一下命令进行内存热插: 6 | 7 | > object_add memory-backend-ram,id=ram0,size=1G 8 | > device_add pc-dimm,id=dimm0,memdev=ram0,node0 9 | 10 | 其中第二行命令就是添加pc-dimm设备的,而第一行命令是表示真实使用的是什么内存。 11 | 12 | 既然如此在实例化的过程中有一个重要的步骤就是找到第一个object并关联他们。 13 | 14 | # devic_initfn 15 | 16 | 因为PCDIMM是一个设备类型,所以实例化PCDIMM之前需要调用这个函数。 17 | 18 | 其中重要的一个步骤就是设置属性。 19 | 20 | ``` 21 | do { 22 | for (prop = DEVICE_CLASS(class)->props; prop && prop->name; prop++) { 23 | qdev_property_add_legacy(dev, prop, &error_abort); 24 | qdev_property_add_static(dev, prop, &error_abort); 25 | } 26 | class = object_class_get_parent(class); 27 | } while (class != object_class_by_name(TYPE_DEVICE)); 28 | ``` 29 | 30 | 还记得在PCDIMM类型初始化时设置的props么?在这里就用到了。 31 | 32 | ``` 33 | static Property pc_dimm_properties[] = { 34 | ... 35 | DEFINE_PROP_LINK(PC_DIMM_MEMDEV_PROP, PCDIMMDevice, hostmem, 36 | TYPE_MEMORY_BACKEND, HostMemoryBackend *), 37 | DEFINE_PROP_END_OF_LIST(), 38 | }; 39 | ``` 40 | 41 | 属性中的这个成员memdev,就是用来关联前后端的。至于具体怎么操作,实在是有点复杂。这里略过。 42 | 43 | # pc_dimm_realize 44 | 45 | 那这个具体类型的初始化函数干了什么呢? 46 | 47 | 仔细一看,这是个空架子。主要就是判断我们的前后端有没有关联好,如果没有则失败。 48 | -------------------------------------------------------------------------------- /device_model/pc_dimm/03-plug.md: -------------------------------------------------------------------------------- 1 | # 插入系统 2 | 3 | 本来以为设备实例化就算完事了,结果还没完。 4 | 5 | 这还要从device_set_realized开始看。 6 | 7 | ``` 8 | device_set_realized() 9 | if (hotplug_ctrl) { 10 | hotplug_handler_pre_plug(hotplug_ctrl, dev, &local_err); 11 | } 12 | 13 | if (dc->realize) { 14 | dc->realize(dev, &local_err); 15 | } 16 | 17 | if (hotplug_ctrl) { 18 | hotplug_handler_plug(hotplug_ctrl, dev, &local_err); 19 | } 20 | ``` 21 | 22 | 从上面简化的框架来看,除了设备实例化本身,初始化时在实例化前后分别做了一些准备和善后: 23 | 24 | * hotplug_handler_pre_plug -> pc_memory_pre_plug 25 | * hotplug_handler_plug -> pc_memory_plug 26 | 27 | # pc_memory_pre_plug 28 | 29 | 这个函数在实际插入设备前做一些检测工作: 30 | 31 | * 检测系统是否支持热插拔:有没有acpi,是不是enable了 32 | * 计算出该插入到哪里,是不是有空间可以个插入 33 | 34 | 计算出合适的位置后,讲这个值保存在PCDIMMDevice.addr字段中。 35 | 这个addr就是该内存条在虚拟机中的物理地址。该地址addr的计算在函数memory_device_get_free_addr()中实现。 36 | 37 | # pc_memory_plug 38 | 39 | 对于普通的dimm设备,插入工作也分成两个步骤: 40 | 41 | * 添加MemoryRegion -> pc_dimm_plug 42 | * 添加acpi -> piix4_device_plug_cb 43 | 44 | 前者将dimm设备的内存注册到系统中,这个通过memory_region_add_subregion来实现。 45 | 46 | 后者做的工作主要是在真的hotplug的情况下发送一个acpi的事件,这样虚拟机的内核才能触发内存热插的工作。 47 | 假如没有理解错,这个acpi事件会通过中断告知虚拟机。 48 | -------------------------------------------------------------------------------- /device_model/pc_dimm/04-dimm_acpi.md: -------------------------------------------------------------------------------- 1 | 在x86带有acpi支持的系统上,如果去要加入内存numa的支持,除了上述的步骤之外好要添加相应的acpi表。这样才能在内核中获取相关的numa信息。 2 | 3 | # 内核需要什么acpi信息 4 | 5 | 在看Qemu中干了什么之前我们先看看内核中是需要什么acpi的信息。 6 | 7 | ``` 8 | int __init acpi_numa_init(void) 9 | { 10 | int cnt = 0; 11 | 12 | if (acpi_disabled) 13 | return -EINVAL; 14 | 15 | /* 16 | * Should not limit number with cpu num that is from NR_CPUS or nr_cpus= 17 | * SRAT cpu entries could have different order with that in MADT. 18 | * So go over all cpu entries in SRAT to get apicid to node mapping. 19 | */ 20 | 21 | /* SRAT: System Resource Affinity Table */ 22 | if (!acpi_table_parse(ACPI_SIG_SRAT, acpi_parse_srat)) { 23 | struct acpi_subtable_proc srat_proc[3]; 24 | 25 | ... 26 | cnt = acpi_table_parse_srat(ACPI_SRAT_TYPE_MEMORY_AFFINITY, 27 | acpi_parse_memory_affinity, 0); 28 | } 29 | 30 | /* SLIT: System Locality Information Table */ 31 | acpi_table_parse(ACPI_SIG_SLIT, acpi_parse_slit); 32 | 33 | ... 34 | return 0; 35 | } 36 | ``` 37 | 38 | 简化后的代码如上,主要就是解析了两张表: 39 | 40 | * SRAT: System Resource Affinity Table 41 | * SLIT: System Locality Information Table 42 | 43 | 你要问我这是啥我也不懂,反正就是他们俩没错了~ 44 | 45 | # Qemu中模拟ACPI表 46 | 47 | 这个工作在下面的函数中执行。 48 | 49 | ``` 50 | acpi_build 51 | if (pcms->numa_nodes) { 52 | acpi_add_table(table_offsets, tables_blob); 53 | build_srat(tables_blob, tables->linker, machine); 54 | if (have_numa_distance) { 55 | acpi_add_table(table_offsets, tables_blob); 56 | build_slit(tables_blob, tables->linker); 57 | } 58 | } 59 | ``` 60 | 61 | 你看,是不是qemu也是创建了两张表? 62 | 63 | * 至于这个acpi_build是什么时候调用的,请看下一节。 64 | 65 | 那这个表是不是就是内核中解析的表呢?我们来看看两个数据结构是不是能够对上就好。 66 | 67 | Qemu中有 AcpiSratMemoryAffinity 68 | 69 | ``` 70 | struct AcpiSratMemoryAffinity { 71 | ACPI_SUB_HEADER_DEF 72 | uint32_t proximity; 73 | uint16_t reserved1; 74 | uint64_t base_addr; 75 | uint64_t range_length; 76 | uint32_t reserved2; 77 | uint32_t flags; 78 | uint32_t reserved3[2]; 79 | } QEMU_PACKED; 80 | ``` 81 | 82 | 而内核中有 acpi_srat_mem_affinity 83 | 84 | ``` 85 | struct acpi_srat_mem_affinity { 86 | struct acpi_subtable_header header; 87 | u32 proximity_domain; 88 | u16 reserved; /* Reserved, must be zero */ 89 | u64 base_address; 90 | u64 length; 91 | u32 reserved1; 92 | u32 flags; 93 | u64 reserved2; /* Reserved, must be zero */ 94 | }; 95 | ``` 96 | 97 | 这两个正好一摸一样。这下放心了。 98 | 99 | # 附带一个ACPI System Description Tables 100 | 101 | 多次需要对ACPI Table进行操作,总是要去找什么表在什么地方。很费时间,干脆做个总结。没有单独开一章节,就先放在这里了。 102 | 103 | 下图的内容在ACPI SPEC 5.2 ACPI System Description Tables中可以找到细节。 104 | 105 | ``` 106 | RSDP 107 | +--------------+ RSDT / XSDT 108 | |RSDT Addr ---|-------+------>+--------------+ 109 | | | | |Head | 110 | +--------------+ | | | 111 | |XSDT Addr ---|-------+ | | 112 | | | | | 113 | +--------------+ Entry +--------------+ FADT 114 | |FADT Addr ---|---------->+--------------+ 115 | | | |Head | 116 | +--------------+ | | 117 | | | | | 118 | | | | | 119 | +--------------+ +--------------+ 120 | |MADT Addr | |FACS Addr | 121 | | | | | 122 | +--------------+ +--------------+ 123 | |MCFG Addr | |DSDT Addr | 124 | | | | | 125 | +--------------+ +--------------+ 126 | | | |X_DSDT Addr | 127 | | | | | 128 | +--------------+ +--------------+ 129 | |SSDT Addr | 130 | | | 131 | +--------------+ 132 | ``` 133 | -------------------------------------------------------------------------------- /device_model/pc_dimm/05-nvdimm.md: -------------------------------------------------------------------------------- 1 | NVDIMM是PCDIMM的子类型,所以整体的流程基本差不多,着重讲一下几个不同之处。 2 | 3 | # 全局 4 | 5 | 我们先来看一下全局上,一个nvdimm设备初始化并加入系统有哪些步骤。 6 | 7 | ``` 8 | main() 9 | qemu_opts_foreach(qemu_find_opts("device"), device_init_func, NULL, NULL) 10 | ... 11 | device_set_realized() 12 | hotplug_handler_pre_plug 13 | ... 14 | memory_device_pre_plug 15 | nvdimm_prepare_memory_region (1) 16 | 17 | if (dc->realize) { 18 | dc->realize(dev, &local_err); 19 | } 20 | 21 | hotplug_handler_plug 22 | ... 23 | nvdimm_plug 24 | nvdimm_build_fit_buffer (2) 25 | qemu_run_machine_init_done_notifiers() 26 | pc_machine_done 27 | acpi_setup 28 | acpi_build 29 | nvdimm_build_acpi (3) 30 | nvdimm_build_ssdt 31 | nvdimm_build_nfit 32 | 33 | acpi_add_rom_blob(build_state, tables.table_data, 34 | "etc/acpi/tables", 0x200000); 35 | acpi_add_rom_blob(build_state, tables.linker->cmd_blob, 36 | "etc/table-loader", 0); 37 | ``` 38 | 39 | 从上面的图中可以看到NVDIMM设备初始化和PCDIMM设备类似,也是有这么几个步骤 40 | 41 | * 插入准备 42 | * 实例化 43 | * 插入善后 44 | * 添加ACPI表 45 | 46 | 其中大部分的工作和PCDIMM一样,只有标注了1,2,3的三个地方需要额外的工作。 47 | 48 | 分别是: 49 | 50 | 1. 添加nvdimm label区域 51 | 2. 创建NFIT表的内容 52 | 3. 创建SSDT和NFIT并加入acpi 53 | 54 | # 分配label区域 55 | 56 | 按照当前的实现,使用nvdimm设备时,在分配空间的最后挖出一块做label。这个工作在nvdimm_prepare_memory_region中完成。 57 | 58 | 这块label区域在虚拟机内核中通过DSM方法来操作,后面我们会看到DSM方法是如何虚拟化的。 59 | 60 | # 填写nfit信息 61 | 62 | nfit表是用来描述nvdimm设备上地址空间组织形式的,所以这部分需要在计算好了dimm地址后进行,有nvdimm_build_fit_buffer完成。 63 | 64 | 展开这个函数我们可以看到 65 | 66 | ``` 67 | for (; device_list; device_list = device_list->next) { 68 | DeviceState *dev = device_list->data; 69 | 70 | /* build System Physical Address Range Structure. */ 71 | nvdimm_build_structure_spa(structures, dev); 72 | 73 | /* 74 | * build Memory Device to System Physical Address Range Mapping 75 | * Structure. 76 | */ 77 | nvdimm_build_structure_memdev(structures, dev); 78 | 79 | /* build NVDIMM Control Region Structure. */ 80 | nvdimm_build_structure_dcr(structures, dev); 81 | } 82 | ``` 83 | 84 | 也就是分别构建了 SPA, MEMDEV和DCR三个信息。而这些信息就在内核函数add_table中处理。 85 | 86 | 不看具体内容,我们只看add_table中类型的定义: 87 | 88 | ``` 89 | enum acpi_nfit_type { 90 | ACPI_NFIT_TYPE_SYSTEM_ADDRESS = 0, 91 | ACPI_NFIT_TYPE_MEMORY_MAP = 1, 92 | ACPI_NFIT_TYPE_INTERLEAVE = 2, 93 | ACPI_NFIT_TYPE_SMBIOS = 3, 94 | ACPI_NFIT_TYPE_CONTROL_REGION = 4, 95 | ACPI_NFIT_TYPE_DATA_REGION = 5, 96 | ACPI_NFIT_TYPE_FLUSH_ADDRESS = 6, 97 | ACPI_NFIT_TYPE_CAPABILITIES = 7, 98 | ACPI_NFIT_TYPE_RESERVED = 8 /* 8 and greater are reserved */ 99 | }; 100 | ``` 101 | 102 | 上面三个类型对应了这个枚举类型的0,1,4。 103 | 104 | # 构建acpi 105 | 106 | 最终nvdimm的信息要填写到acpi中,一共有两张表:SSDT和NFIT。这部分工作在nvdimm_build_acpi中完成。 107 | 108 | 其中NFIT表在nvdimm_build_nfit函数中完成,其实没有太多内容。就是将上面创建好的表内容拷贝过来。 109 | 110 | 而SSDT这张表就讲究多了,这里面涉及了acpi的一些概念。 111 | 112 | ## ACPI Table 113 | 114 | 在进入代码之前,还是要先了解一下acpi的一些概念。这里并不是系统性介绍acpi,只是补充一些代码阅读中涉及的概念。 115 | 116 | ACPI(Advanced Configuration and Power Management Interface),简单理解就是用来描述硬件配置的。 117 | 118 | 为了做好管理,acpi当然提供了很多复杂的功能,其中我们关注的就是acpi table。而这些表的作用比较直观,那就是按照一定的规范来描述硬件设备的属性。 119 | 120 | 规范中定义了很多表,而这些表按照我的理解可以分成两类: 121 | 122 | * DSDT & SSDT 123 | * 其他 124 | 125 | DSDT & SSDT 虽然也叫表,但是他们使用AML语言编写的,可以被解析执行的。也就是这两张表是可编程的。 126 | 而其他的表则是静态的,按照标准写好的数据结构。 127 | 128 | ## 查看Table 129 | 130 | 在linux上可以很方便得查看acpi table。因为他们都在sysfs上。 131 | 132 | > /sys/firmware/acpi/tables/ 133 | 134 | 文件的内容还需要使用工具查看。 135 | 136 | ``` 137 | # acpidump -b 138 | ``` 139 | 140 | 这个命令将会把所有的acpi table的二进制形式导出到.dat文件,并保存在当前目录下。 141 | 142 | 比如我们就看nvdimm中创建的NFIT表,那么再执行 143 | 144 | ``` 145 | # iasl NFIT.dat 146 | ``` 147 | 148 | 就能解析成可阅读的文件了。在我的虚拟机上显示如下,而且正好就是spa, memdev, dcr三个部分。 149 | 150 | ``` 151 | [000h 0000 4] Signature : "NFIT" [NVDIMM Firmware Interface Table] 152 | [004h 0004 4] Table Length : 000000E0 153 | [008h 0008 1] Revision : 01 154 | [009h 0009 1] Checksum : BF 155 | [00Ah 0010 6] Oem ID : "BOCHS " 156 | [010h 0016 8] Oem Table ID : "BXPCNFIT" 157 | [018h 0024 4] Oem Revision : 00000001 158 | [01Ch 0028 4] Asl Compiler ID : "BXPC" 159 | [020h 0032 4] Asl Compiler Revision : 00000001 160 | 161 | [024h 0036 4] Reserved : 00000000 162 | 163 | [028h 0040 2] Subtable Type : 0000 [System Physical Address Range] 164 | [02Ah 0042 2] Length : 0038 165 | 166 | [02Ch 0044 2] Range Index : 0002 167 | [02Eh 0046 2] Flags (decoded below) : 0003 168 | Add/Online Operation Only : 1 169 | Proximity Domain Valid : 1 170 | [030h 0048 4] Reserved : 00000000 171 | [034h 0052 4] Proximity Domain : 00000000 172 | [038h 0056 16] Address Range GUID : 66F0D379-B4F3-4074-AC43-0D3318B78CDB 173 | [048h 0072 8] Address Range Base : 00000001C0000000 174 | [050h 0080 8] Address Range Length : 0000000278000000 175 | [058h 0088 8] Memory Map Attribute : 0000000000008008 176 | 177 | [060h 0096 2] Subtable Type : 0001 [Memory Range Map] 178 | [062h 0098 2] Length : 0030 179 | 180 | [064h 0100 4] Device Handle : 00000001 181 | [068h 0104 2] Physical Id : 0000 182 | [06Ah 0106 2] Region Id : 0000 183 | [06Ch 0108 2] Range Index : 0002 184 | [06Eh 0110 2] Control Region Index : 0003 185 | [070h 0112 8] Region Size : 0000000278000000 186 | [078h 0120 8] Region Offset : 0000000000000000 187 | [080h 0128 8] Address Region Base : 0000000000000000 188 | [088h 0136 2] Interleave Index : 0000 189 | [08Ah 0138 2] Interleave Ways : 0001 190 | [08Ch 0140 2] Flags : 0000 191 | Save to device failed : 0 192 | Restore from device failed : 0 193 | Platform flush failed : 0 194 | Device not armed : 0 195 | Health events observed : 0 196 | Health events enabled : 0 197 | Mapping failed : 0 198 | [08Eh 0142 2] Reserved : 0000 199 | 200 | [090h 0144 2] Subtable Type : 0004 [NVDIMM Control Region] 201 | [092h 0146 2] Length : 0050 202 | 203 | [094h 0148 2] Region Index : 0003 204 | [096h 0150 2] Vendor Id : 8086 205 | [098h 0152 2] Device Id : 0001 206 | [09Ah 0154 2] Revision Id : 0001 207 | [09Ch 0156 2] Subsystem Vendor Id : 0000 208 | [09Eh 0158 2] Subsystem Device Id : 0000 209 | [0A0h 0160 2] Subsystem Revision Id : 0000 210 | [0A2h 0162 1] Valid Fields : 00 211 | [0A3h 0163 1] Manufacturing Location : 00 212 | [0A4h 0164 2] Manufacturing Date : 0000 213 | [0A6h 0166 2] Reserved : 0000 214 | [0A8h 0168 4] Serial Number : 00123456 215 | [0ACh 0172 2] Code : 0301 216 | [0AEh 0174 2] Window Count : 0000 217 | [0B0h 0176 8] Window Size : 0000000000000000 218 | [0B8h 0184 8] Command Offset : 0000000000000000 219 | [0C0h 0192 8] Command Size : 0000000000000000 220 | [0C8h 0200 8] Status Offset : 0000000000000000 221 | [0D0h 0208 8] Status Size : 0000000000000000 222 | [0D8h 0216 2] Flags : 0000 223 | Windows buffered : 0 224 | [0DAh 0218 6] Reserved1 : 000000000000 225 | ``` 226 | 227 | ## AML & ASL 228 | 229 | 开始我们也说了,acpi table分成两类。刚才的facs属于第二类,也就是静态的数据结构。 230 | 231 | 而对于第一种表,他们则复杂得多,是用AML编写的。 232 | 233 | * AML: ACPI Machine Language 234 | * ASL: ACPI source language 235 | 236 | 所以前者相当于机器语言,而后者相当于汇编。 237 | 238 | 解析这两张表的方式和刚才的一样也是通过iasl命令,但是样子就完全不一样了。我们就直接拿nvdimm构造ssdt来看。 239 | 240 | [NVDIMM SSDT][1] 241 | 242 | 刚开始看确实有点头大,不过慢慢就好了。我稍微来解释一下。 243 | 244 | **定义函数** 245 | 246 | ``` 247 | Method (NCAL, 5, Serialized) 248 | ``` 249 | 250 | 所以看到这个表中定义了好些函数: _DSM, RFIT, _FIT 。 251 | 252 | 值得注意的是以_ 开头的函数是在规范中定义的,而其他的则是可以自己添加的。 253 | 254 | **定义变量** 255 | 256 | 如果说函数定义还比较好理解,那么变量的定义就有点不那么直观了。 257 | 258 | 最简单的方式是赋值: 259 | 260 | ``` 261 | Local6 = MEMA 262 | ``` 263 | 264 | 当然这个变量Local6是自定义的,只要直接使用就好。 265 | 266 | 另外一种就是比较特殊的,我称之为**指针方式**。因为看着和c语言的指针访问很像。而且这种方式需要做两步。 267 | 268 | * 定义Region 269 | * 定义Field 270 | 271 | 先定义出一块空间,里面包含了这块空间对应的起始地址和长度。 272 | 273 | ``` 274 | OperationRegion (NPIO, SystemIO, 0x0A18, 0x04) 275 | ``` 276 | 277 | 然后定义这块空间的field,这点上看和c语言的结构体很像。 278 | 279 | ``` 280 | Field (NPIO, DWordAcc, NoLock, Preserve) 281 | { 282 | NTFI, 32 283 | } 284 | ``` 285 | 286 | 这样相当于得到了一个指针变量NFIT,这个地址是指向0x0A18的4个字节。 287 | 288 | 最后访问这个区域就是直接用赋值语句: 289 | 290 | ``` 291 | NTFI = Local6 292 | ``` 293 | 294 | 就相当于往NTFI这个指针写了Local6的值。 295 | 296 | ## 模拟nvdimm的_DSM 297 | 298 | 好了,绕了这么大一圈,终于可以回到正题看看nvdimm的_DSM是如何模拟的。 299 | 300 | 所谓模拟,也就是在guest执行_DSM方法时截获该操作,由qemu进行模拟。 301 | 302 | ### guest内核 303 | 304 | 所以正式开始前,我们还得看一眼内核中执行_DSM时做了点什么。 305 | 306 | 下面的代码截取自acpi_evaluate_dsm() 307 | 308 | ``` 309 | params[0].type = ACPI_TYPE_BUFFER; 310 | params[0].buffer.length = 16; 311 | params[0].buffer.pointer = (u8 *)guid; 312 | params[1].type = ACPI_TYPE_INTEGER; 313 | params[1].integer.value = rev; 314 | params[2].type = ACPI_TYPE_INTEGER; 315 | params[2].integer.value = func; 316 | if (argv4) { 317 | params[3] = *argv4; 318 | } else { 319 | params[3].type = ACPI_TYPE_PACKAGE; 320 | params[3].package.count = 0; 321 | params[3].package.elements = NULL; 322 | } 323 | 324 | ret = acpi_evaluate_object(handle, "_DSM", &input, &buf); 325 | ``` 326 | 327 | 别的不管,主要看一共传进来了四个参数。比如有版本号和函数。这里先记着,留着后面再来看。 328 | 329 | ### AML 330 | 331 | 回到之前看得acpi表总的来说,SSDT表在地址空间上构建了这么两段: 332 | 333 | ``` 334 | 0xA18 MEMA 335 | +------------------+--+------------------+------+-------------+ 336 | | | | | | | 337 | +------------------+--+------------------+------+-------------+ 338 | / \ 339 | / \ 340 | +--------------+ 341 | |HDLE | 342 | |REVS | 343 | |FUNC | 344 | |FARG | 345 | +--------------+ 346 | ``` 347 | 348 | 其目的就是创建了一块共享内存,用来在guest和qemu之间传递模拟_DSM的参数和返回值。 349 | 350 | 比如我们可以在aml文件中看到 351 | 352 | ``` 353 | REVS = Arg1 354 | FUNC = Arg2 355 | ``` 356 | 357 | 而其中Arg1就是内核传递下来的参数rev,Arg2是内核传递参数的func。 358 | 359 | ### Qemu 360 | 361 | 当AML执行了 NFIT = Local6 的时候,接下去的操作就被Qemu截获了。而这个Local6参数的值是什么呢? 362 | 363 | > Local6 = MEMA 364 | 365 | 所以,实际上就是告诉了Qemu一块共享内存的空间。这下是不是明白了? 366 | 367 | 那Qemu截获后是如何操作?这还要看nvdimm_dsm_ops了。因为是写操作,所以只看write方法。 368 | 369 | 其中有一个变量很引人注目:NvdimmDsmIn。 370 | 371 | ``` 372 | NvdimmDsmIn *in; 373 | 374 | in->revision = le32_to_cpu(in->revision); 375 | in->function = le32_to_cpu(in->function); 376 | in->handle = le32_to_cpu(in->handle); 377 | ``` 378 | 379 | 而这个变量类型定义为: 380 | 381 | ``` 382 | struct NvdimmDsmIn { 383 | uint32_t handle; 384 | uint32_t revision; 385 | uint32_t function; 386 | /* the remaining size in the page is used by arg3. */ 387 | union { 388 | uint8_t arg3[4084]; 389 | }; 390 | } QEMU_PACKED; 391 | ``` 392 | 393 | 这下是不是和AML中共享内存的结构对应起来了? 394 | 395 | 好了,再详细的细节就和具体的函数实现相关了。留给大家自己去探索把~ 396 | 397 | # 最后的疑问 398 | 399 | 如果大家仔细看,比较qemu中aml的代码和guest中SSDT生成的文件,其中还有一个问题没有解决。 400 | 401 | 那就是 Local6 = MEMA 的这个MEMA的地址是如何得到的。 402 | 403 | 这个就牵扯到了另外两个超纲的知识点,将在[FW_CFG][2]章节中解开这个谜团。 404 | 405 | [1]: https://gist.github.com/RichardWeiYang/aea8e71f5c9ff71499d19e77eb8a777e 406 | [2]: /fw_cfg/00-qmeu_bios_guest.md 407 | -------------------------------------------------------------------------------- /fw_cfg/00-qmeu_bios_guest.md: -------------------------------------------------------------------------------- 1 | FW_CFG 提供了guest一种获得额外信息的方式,好像我也很难讲清楚,直接上原文 2 | 3 | > This hardware interface allows the guest to retrieve various data items 4 | (blobs) that can influence how the firmware configures itself, or may 5 | contain tables to be installed for the guest OS. Examples include device 6 | boot order, ACPI and SMBIOS tables, virtual machine UUID, SMP and NUMA 7 | information, kernel/initrd images for direct (Linux) kernel booting, etc. 8 | 9 | 在Qemu代码中有对FW_CFG的[规范][1],大家可以自行前往。 10 | 11 | 这一章,我们就来解读一下这个东西的作用,以及它的用户们。 12 | 13 | 所以首先我们来解读一下这个规范,看看这个东西的样子以及工作的原理。 14 | 15 | [规范解读][2] 16 | 17 | 接着我们来看看都有谁,会怎么用这个FW_CFG。 18 | 19 | 现在我能看到的分别有两个使用者: 20 | 21 | * [linux 虚拟机][3] 22 | 23 | * [SeaBios][4] 24 | 25 | [1]: https://github.com/qemu/qemu/blob/master/docs/specs/fw_cfg.txt 26 | [2]: /fw_cfg/01-spec.md 27 | [3]: /fw_cfg/02-linux_guest.md 28 | [4]: /fw_cfg/03-seabios.md 29 | -------------------------------------------------------------------------------- /fw_cfg/01-spec.md: -------------------------------------------------------------------------------- 1 | 看完了代码才发现有这个[规范文档][1]。不过也好,正好印证一下我对代码的理解。 2 | 3 | 按照我的理解,讲清楚两个东西就可以理解这个规范: 4 | 5 | * 接口 6 | * 数据结构 7 | 8 | # 接口 9 | 10 | 在文档的开头就描述了两个东西: 11 | 12 | * Selector (Control) Register 13 | * Data Register 14 | 15 | 说白了就是有两个寄存器/通道,分别对应了控制面和数据面。 16 | 17 | 扯多了,直白的解释就是: 18 | 19 | > 用户使用时,先通过Selector Register选定需要访问的单元,再通过Data Register来获取实际的数据。 20 | 21 | 好了,接口这部分其实就这么些,没多大花头。 22 | 23 | 在规范中还定义了这两个接口的地址。 24 | 25 | ``` 26 | === x86, x86_64 Register Locations === 27 | 28 | Selector Register IOport: 0x510 29 | Data Register IOport: 0x511 30 | DMA Address IOport: 0x514 31 | 32 | === ARM Register Locations === 33 | 34 | Selector Register address: Base + 8 (2 bytes) 35 | Data Register address: Base + 0 (8 bytes) 36 | DMA Address address: Base + 16 (8 bytes) 37 | ``` 38 | 39 | 我猜大家到这里一定感觉啥都不知道,不着急,等看到数据结构就清楚了。 40 | 41 | # 数据结构 42 | 43 | 好了,这里要上一个巨大的数据结构了。嗯,其实呢和某些结构相比也不算大,主要是这里面有数组,所以显得大了。 44 | 45 | ``` 46 | FWCfgIoState 47 | +------------------------------------------+ 48 | |machine_ready | 49 | | notify | = fw_cfg_machine_ready 50 | +------------------------------------------+ 51 | |dma_iomem | 52 | | (MemoryRegion) | 53 | | +--------------------------------------+ 54 | | |addr | = FW_CFG_IO_BASE + 4(0x514) 55 | | |name | = "fwcfg.dma" 56 | | |ops | fw_cfg_dma_mem_ops 57 | | |opaque | FWCfgState itself 58 | | |size | = 8 59 | +---+--------------------------------------+ 60 | |comb_iomem | 61 | | (MemoryRegion) | 62 | | +--------------------------------------+ 63 | | |addr | = FW_CFG_IO_BASE(0x510) 64 | | |name | = "fwcfg" 65 | | |ops | fw_cfg_comb_mem_ops 66 | | |opaque | FWCfgState itself 67 | | |size | = 0x02 68 | +---+--------------------------------------+ 69 | |cur_entry | fw_cfg_select(key) 70 | |cur_offset | 71 | | (uint) | 72 | +------------------------------------------+ 73 | |file_slots | 74 | | (uint16) | 75 | +------------------------------------------+ 76 | |files | 77 | | (FWCfgFiles) | 78 | | +--------------------------------------+ 79 | | |count | 80 | | | (uint32_t) | 81 | | +--------------------------------------+ 82 | | |f[] | etc/acpi/tables etc/table-loader 83 | | | (FWCfgFile) | ------------------------------------------------------------------------------------------->+------------------------------+----------------------->+------------------------------+ 84 | | | +----------------------------------+ |name | |name | 85 | | | |name | | (char [FW_CFG_MAX_FILE_PATH])| | (char [FW_CFG_MAX_FILE_PATH])| 86 | | | | (char [FW_CFG_MAX_FILE_PATH]) | |size | |size | 87 | | | |size | |select | |select | 88 | | | |select | |reserved | |reserved | 89 | | | |reserved | +------------------------------+ +------------------------------+ 90 | +---+---+----------------------------------+ ^ ^ 91 | |entry_order | [FW_CFG_FILE_FIRST + file_slots] | | 92 | | (int*) | | | 93 | +------------------------------------------+ | | 94 | |entries[0] | [FW_CFG_FILE_FIRST + file_slots] | | 95 | | (FWCfgEntry*) | | | 96 | | | | | 97 | | | v v 98 | | | [FW_CFG_SIGNATURE] [FW_CFG_FILE_DIR] [FW_CFG_FILE_FIRST+1] [FW_CFG_FILE_FIRST+2] 99 | | | +-------------------+ +-----------------+ +------------------+ +------------------+ 100 | | | |data | = "QEMU" |data | = s->files |data | = AcpiBuildTables.table_data |data | = AcpiBuildTables.linker->cmd_blob 101 | | | | (uint8_t *) | | (uint8_t *) | | (uint8_t *) | | (uint8_t *) | 102 | | | |len | = 4 |len | = size of s->files |len | = sizeof (table_data) |len | = sizeof (cmd_blob) 103 | | | | (uint32_t) | . . . | (uint32_t) | | (uint32_t) | | (uint32_t) | 104 | | | |callback_opaque | = NULL |callback_opaque | = NULL |callback_opaque | = AcpiBuildState |callback_opaque | = AcpiBuildState 105 | | | | (void*) | | (void*) | | (void*) | | (void*) | 106 | | | |select_cb | = NULL |select_cb | = NULL |select_cb | = acpi_build_update |select_cb | = acpi_build_update 107 | | | |write_cb | = NULL |write_cb | = NULL |write_cb | = NULL |write_cb | = NULL 108 | | | +-------------------+ +-----------------+ +------------------+ +------------------+ 109 | | | 110 | | | [FW_CFG_SIGNATURE...FW_CFG_FILE_DIR] static key [FW_CFG_FILE_FIRST... ] related to f[] 111 | | | 112 | | | 113 | +------------------------------------------+ 114 | |entries[1] | [FW_CFG_FILE_FIRST + file_slots] 115 | | (FWCfgEntry*) | 116 | | +--------------------------------------+ 117 | | |data | 118 | | | (uint8_t *) | 119 | | |callback_opaque | 120 | | | (void*) | 121 | | |select_cb | 122 | | |write_cb | 123 | +---+--------------------------------------+ 124 | ``` 125 | 126 | 让偶来稍做解释,估计你可以看懂一些: 127 | 128 | * comb_iomem: 这个就是规范中定义的接口。看这个地址FW_CFG_IO_BASE就是0x510 129 | * entries: 这是整个数据的核心。fw_cfg用编号(key)作为关键字访问各种配置。而每个配置都有一个FWCfgEntry数据结构表示,并保存在entries数组上。 130 | * files: fw_cfg的配置可以是简单的数据类型,如int, double。也可以是一个文件。所以在entries上可以看到分成了两端。从FW_CFG_FILE_FIRST开始保存的是文件信息。而且都有一个对应的FWCfgFile记录文件信息,并保存在files数组上。 131 | * cur_entry/offset: 因为接口简单,在每次访问配置之前先要指定访问的是哪个entry,且每次读写的位移取决于上一次的操作。所以这两个分别保存了当前的entry和偏移。 132 | 133 | 好了,其实展开就这么点内容了。除了长得丑,别的也没啥了。 134 | 135 | # 相关代码 136 | 137 | 大的概念讲完了,还是要落到实际代码。 138 | 139 | 先看看这个高级货在哪里创建的。 140 | 141 | ``` 142 | pc_memory_init 143 | bochs_bios_init 144 | fw_cfg_init_io_dma 145 | qdev_create(NULL, TYPE_FW_CFG_IO); 146 | qdev_init_nofail(dev); 147 | fw_cfg_io_realize 148 | fw_cfg_common_realize 149 | rom_set_fw(); 150 | ``` 151 | 152 | 创建完了之后,分别有两大类添加配置的方式: 153 | 154 | * fw_cfg_add_bytes() 155 | * fw_cfg_add_file_callback() 156 | 157 | 前者就是添加普通配置的,而后者就是添加带有文件的配置的。 158 | 159 | [1]: https://github.com/qemu/qemu/blob/master/docs/specs/fw_cfg.txt 160 | -------------------------------------------------------------------------------- /fw_cfg/02-linux_guest.md: -------------------------------------------------------------------------------- 1 | FW_CFG的一个用户就是linux中的qemu_fw_cfg模块了。 2 | 3 | 这个模块很简单,只有一个文件drivers/firmware/qemu_fw_cfg.c。其主要工作就是读取fw_cfg中的entry,并给每个entry创建一个sysfs。这样最终用户就可以通过sysfs来获取相关信息了。 4 | 5 | # sysfs的结构 6 | 7 | 先来看一下模块加载后sysfs的样子。 8 | 9 | 一共有两个子目录: 10 | 11 | * 按照id区分的 12 | * 按照名字区分的 13 | 14 | 每个文件下面有 15 | 16 | * name 17 | * size 18 | * raw 19 | 20 | 前两个文件的含义比较明显,raw就是直接能读到fw的内容。 21 | 22 | ``` 23 | /sys/firmware/qemu_fw_cfg/ 24 | | 25 | +--rev 26 | | 27 | +--by_id 28 | | | 29 | | +-- 0 30 | | | | 31 | | | +--name 32 | | | | 33 | | | +--size 34 | | | | 35 | | | +--raw 36 | | | 37 | | +-- 1 38 | | | 39 | | +--name 40 | | | 41 | | +--size 42 | | | 43 | | +--raw 44 | | 45 | +--by_name 46 | ``` 47 | 48 | 好了,我能想到的基本就这些了。也有可能我没有看到再上层的用户是如何利用sysfs来操作fw的。 49 | -------------------------------------------------------------------------------- /fw_cfg/03-seabios.md: -------------------------------------------------------------------------------- 1 | 第二个用户是seabios,就现在了解,seabios所做的工作比linux上的qemu_fw_cfg做的工作要多些。 2 | 3 | # 基础工作 4 | 5 | 所谓基础工作就是和linux qemu_fw_cfg模块所做的工作类似 6 | 7 | > 读取fw_cfg相关的内容 8 | 9 | 说白了就是按照规范把文件的读写接口给实现了。 10 | 11 | 这些实现都在文件 src/fw/paravirt.c中。 12 | 13 | * qemu_cfg_select 14 | * qemu_cfg_read_file 15 | * qemu_cfg_write_file 16 | 17 | 等。 18 | 19 | # linker/loader 20 | 21 | 接下来要讲的这个东西就有点意思了,说实话研究fw_cfg的目的就是想了解一下这个东西。 22 | 23 | 一切还得从qemu说起。 24 | 25 | ## Qemu的BIOSLinker 26 | 27 | 在Qemu的代码中有一个神奇的结构名字叫BIOSLinker。而且貌似它有一个作用是能够动态填写某些地址。 28 | 29 | 比如在[nvdimm][1]小节中留下的最后疑问,MEMA地址的动态绑定好像就是这个家伙完成的。 30 | 31 | 那究竟是怎么绑定的呢? 开始我们的探索之旅吧~ 32 | 33 | 先来看看这个结构的样子。 34 | 35 | ``` 36 | AcpiBuildTables 37 | +-------------------------------------------+ 38 | |table_data | 39 | | (GArray*) | 40 | | +-------------------------------------+ ------------ facs 41 | | |AcpiFacsDescriptorRev1 | 42 | | | signature | = "FACS" 43 | | | | 64bytes 44 | | | | 45 | | | | 46 | | +-------------------------------------+ ------------ dsdt 47 | | |AcpiTableHeader | 48 | | | signature | = "DSDT" 49 | | | | 50 | | | | 51 | | | | 52 | | | | 53 | | +-------------------------------------+ ------------ 54 | | | 55 | +-------------------------------------------+ 56 | |linker | 57 | | (BIOSLinker*) | 58 | | +-------------------------------------+ 59 | | |cmd_blob | list of BiosLinkerLoaderEntry 60 | | | (GArray*) | 61 | | | +--------------------------------+ 62 | | | |command | = BIOS_LINKER_LOADER_COMMAND_ALLOCATE 63 | | | | alloc.file | = "etc/acpi/tables" 64 | | | | alloc.align | 65 | | | | alloc.zone | 66 | | | | | 67 | | | +--------------------------------+ 68 | | | |command | = BIOS_LINKER_LOADER_COMMAND_ALLOCATE 69 | | | | alloc.file | = "etc/acpi/nvdimm-mem" 70 | | | | alloc.align | 71 | | | | alloc.zone | 72 | | | | | 73 | | | +--------------------------------+ 74 | | | |command | = BIOS_LINKER_LOADER_COMMAND_ADD_CHECKSUM 75 | | | | file | = "etc/acpi/tables" 76 | | | | offset | 77 | | | | start_offset | 78 | | | | | 79 | | | +--------------------------------+ 80 | | | |command | = BIOS_LINKER_LOADER_COMMAND_ADD_POINTER 81 | | | | dest_file | = "etc/acpi/tables" 82 | | | | src_file | = "etc/acpi/nvdimm-mem" 83 | | | | offset | = mem_addr_offset 84 | | | | size | = 4 85 | | | +--------------------------------+ 86 | | | | 87 | | |file_list | list of BiosLinkerFileEntry 88 | | | (GArray*) | 89 | | | +--------------------------------+ 90 | | | |name | = "etc/acpi/tables" 91 | | | |bolb | = table_data 92 | | | +--------------------------------+ 93 | | | |name | = "etc/acpi/nvdimm-mem" 94 | | | |bolb | = AcpiNVDIMMState.dsm_mem 95 | | | +--------------------------------+ 96 | | | | | 97 | | | | | 98 | +-----+----+--------------------------------+ 99 | ``` 100 | 101 | 在这里我把AcpiBuildTables这个结构也列出,以便看清结构中和acpi table的对应关系。 102 | 103 | 这个结构展开其中就两个重要的部分: 104 | 105 | * cmd_blob 106 | * file_list 107 | 108 | file_list是一个“文件”列表,其实是这个结构以文件名字的方式组织了对应的内存。 109 | cmd_blob则是定义了“命令”列表。 110 | 111 | 怎么样,看到命令是不是感觉有点意思了? 112 | 113 | 定义命令的结构体叫做BiosLinkerLoaderEntry,它太大了我们就不在这里展开。不过我们可以看看一共有几种命令。 114 | 115 | ``` 116 | enum { 117 | BIOS_LINKER_LOADER_COMMAND_ALLOCATE = 0x1, 118 | BIOS_LINKER_LOADER_COMMAND_ADD_POINTER = 0x2, 119 | BIOS_LINKER_LOADER_COMMAND_ADD_CHECKSUM = 0x3, 120 | BIOS_LINKER_LOADER_COMMAND_WRITE_POINTER = 0x4, 121 | }; 122 | ``` 123 | 124 | 也就四种,不算太多。 125 | 126 | ## Seabios的romfile_loader_entry_s 127 | 128 | Seabios中好像没有找到BIOSLinker对应的结构,但是能找到BiosLinkerLoaderEntry对应的是romfile_loader_entry_s。 129 | 130 | 而且可以看到Seabios中定义的命令类型是: 131 | 132 | ``` 133 | enum { 134 | ROMFILE_LOADER_COMMAND_ALLOCATE = 0x1, 135 | ROMFILE_LOADER_COMMAND_ADD_POINTER = 0x2, 136 | ROMFILE_LOADER_COMMAND_ADD_CHECKSUM = 0x3, 137 | ROMFILE_LOADER_COMMAND_WRITE_POINTER = 0x4, 138 | }; 139 | ``` 140 | 141 | 这样正好好qemu中的命令对上。 142 | 143 | 好了,到了这里我想你基本已经知道他们之间的关系以及是如何运作的了。 144 | 145 | 最后我只用提示一点,那就是[nvdimm][1]中那个存疑的MEMA地址是通过COMMAND_ADD_POINTER来实现的。 146 | 147 | [1]: /device_model/pc_dimm/05-nvdimm.md 148 | -------------------------------------------------------------------------------- /lm/00-lm.md: -------------------------------------------------------------------------------- 1 | 热迁移是一个很有技术含量的话题,也是在实际使用中会常常运动的特性。最近做了一些学习,记录在此。 2 | 3 | 在研究代码之前,我们先来看看要做一次热迁移是如何操作的。 4 | 5 | [从用法说起][1] 6 | 7 | 接下来我们开始从代码层面研究: 8 | 9 | [整体架构][2] 10 | 11 | 在上文整体架构篇中限于篇幅,我们跳过了一个非常重要的结构。所以干脆把VMStateDescription单独拿出来讲: 12 | 13 | [VMStateDescription][3] 14 | 15 | 了解了总体结构后,就该研究具体的设备是如何迁移的了。在众多设备中,内存是关键的内容之一,所以第一个要研究的就是内存了。 16 | 17 | [内存热迁移][4] 18 | 19 | 内存迁移中运用了很多有意思的技术,大部分在[内存热迁移][4]中描述了。但其中有一个非常重要而且有意思的技术叫postcopy。正是因为其特殊性,且不仅适用于内存,故单列一节。 20 | 21 | [postcopy][5] 22 | 23 | [1]:/lm/01-migrate_command_line.md 24 | [2]:/lm/02-infrastructure.md 25 | [3]:/lm/03-vmsd.md 26 | [4]:/lm/04-ram_migration.md 27 | [5]:/lm/05-postcopy.md 28 | -------------------------------------------------------------------------------- /lm/01-migrate_command_line.md: -------------------------------------------------------------------------------- 1 | 我们通过例子来看一下,要做一次热迁移需要如何操作。 2 | 3 | # tcp传输 4 | 5 | **源端** 6 | 7 | ``` 8 | sudo qemu-system-x86_64 -m 4G,slots=4,maxmem=128G -smp 4,maxcpus=16 --enable-kvm \ 9 | -drive file=fedora29.img,format=raw -nographic 10 | ``` 11 | 12 | 源端的qemu命令和平时的没有什么两样。 13 | 14 | **目的端** 15 | 16 | ``` 17 | sudo qemu-system-x86_64 -m 4G,slots=4,maxmem=128G -smp 4,maxcpus=16 --enable-kvm \ 18 | -drive file=fedora29.img,format=raw -nographic \ 19 | -incoming tcp:0:4444 20 | ``` 21 | 22 | 目的端要保证和源端的命令行是一样的,且要加上**"-incoming tcp:0:4444"** 23 | 24 | **开始迁移** 25 | 26 | 此时可以在源端的monitor中执行下面的命令开始迁移 27 | 28 | ``` 29 | migrate -d tcp:0:4444 30 | ``` 31 | 32 | # exec传输 33 | 34 | 这种严格来说不能叫热迁移,因为虚拟机有停顿。不过因为从命令行的形式上看类似,就放在这里。 35 | 36 | **源端** 37 | 38 | ``` 39 | sudo qemu-system-x86_64 -m 4G,slots=4,maxmem=128G -smp 4,maxcpus=16 --enable-kvm \ 40 | -drive file=fedora29.img,format=raw -nographic 41 | ``` 42 | 43 | 虚拟机的启动是一样的,但是源端启动后就需要在monitor中执行 44 | 45 | ``` 46 | stop 47 | migrate "exec cat > /backup_file" 48 | quit 49 | ``` 50 | 51 | 可以看到,源端的虚拟机不仅停止了,还退出了。 52 | 53 | **目的端** 54 | 55 | ``` 56 | sudo qemu-system-x86_64 -m 4G,slots=4,maxmem=128G -smp 4,maxcpus=16 --enable-kvm \ 57 | -drive file=fedora29.img,format=raw -nographic \ 58 | -incoming "exec:cat < /backup_file" 59 | ``` 60 | 61 | 运行之后在monitor中执行 62 | 63 | ``` 64 | cont 65 | ``` 66 | 67 | 更多用法可以参考[Migration](https://www.linux-kvm.org/page/Migration) 68 | -------------------------------------------------------------------------------- /lm/02-infrastructure.md: -------------------------------------------------------------------------------- 1 | 现在就让我们来看看迁移这件事的总体架构吧。 2 | 3 | # 发送端 4 | 5 | ## 从migrate命令开始 6 | 7 | 通过上一小节的例子,我们可以看到迁移可以通过在monitor中执行命令开始。既然如此,那我们就从这里开始。 8 | 9 | 话说要讲清楚monitor中命令执行的机制,还真是要花费一些事件。通过一些学习,发现迁移的入口函数是hmp_migrate。下面就是本人总结的从hmp_migrate开始到迁移主函数的流程。 10 | 11 | ``` 12 | hmp_migrate(), invoked from handle_hmp_command() 13 | qmp_migrate() 14 | migrate_get_current(), global MigrationState 15 | migrate_prepare() 16 | migrate_init() 17 | 18 | tcp_start_outgoing_migration() 19 | socket_start_outgoing_migration() 20 | unix_start_outgoing_migration() 21 | socket_start_outgoing_migration() 22 | socket_outgoing_migration 23 | migration_channel_connect(s, sioc, hostname, err) 24 | exec_start_outgoing_migration() 25 | migration_channel_connect(s, ioc, NULL, NULL) 26 | fd_start_outgoing_migration() 27 | migration_channel_connect(s, ioc, NULL, NULL) 28 | migrate_fd_connect(s, NULL) 29 | rdma_start_outgoing_migration() 30 | migrate_fd_connect(s, NULL) 31 | migration_thread() 32 | ``` 33 | 34 | 可以看到,迁移的接口有 35 | 36 | * tcp 37 | * unix 38 | * exec 39 | * fd 40 | * rdma 41 | 42 | 但是万变不离其宗,最后都启动了migration_thread这个线程处理。 43 | 44 | ## 迁移主函数 migration_thread 45 | 46 | 所以最关键的就是这个迁移的主函数migration_thread。那我们把这个函数也打开。 47 | 48 | ``` 49 | migration_thread() 50 | 51 | qemu_savevm_state_header 52 | qemu_put_be32(f, QEMU_VM_FILE_MAGIC); 53 | qemu_put_be32(f, QEMU_VM_FILE_VERSION); 54 | qemu_put_be32(f, QEMU_VM_CONFIGURATION); 55 | vmstate_save_state(f, &vmstate_configuration, &savevm_state, 0); 56 | qemu_savevm_send_open_return_path(s->to_dst_file); 57 | qemu_savevm_send_ping(s->to_dst_file, 1); 58 | qemu_savevm_command_send(f, MIG_CMD_PING, , (uint8_t *)&buf) 59 | 60 | ; iterate savevm_state and call save_setup 61 | qemu_savevm_state_setup(s->to_dst_file); 62 | save_section_header(f, se, QEMU_VM_SECTION_START) 63 | se->ops->save_setup(f, se->opaque) 64 | save_section_footer(f, se) 65 | precopy_notify(PRECOPY_NOTIFY_SETUP, &local_err) 66 | 67 | migrate_set_state(&s->state, MIGRATION_STATUS_SETUP, MIGRATION_STATUS_ACTIVE); 68 | migration_iteration_run 69 | 70 | ; iterate savevm_state and call save_live_pending 71 | qemu_savevm_state_pending(pend_pre/compat/post) 72 | se->ops->save_live_pending() 73 | 74 | ; iterate savevm_state and call save_live_iterate 75 | qemu_savevm_state_iterate() 76 | save_section_header(f, se, QEMU_VM_SECTION_PART) 77 | se->ops->save_live_iterate(f, se->opaque) 78 | save_section_footer(f, se) 79 | migration_completion() 80 | qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL); 81 | vm_stop_force_state(RUN_STATE_FINISH_MIGRATE) 82 | 83 | qemu_savevm_state_complete_precopy(s->to_dst_file, false, inactivate); 84 | ; iterate savevm_state and call save_live_complete_precopy 85 | cpu_synchronize_all_states(); 86 | save_section_header(f, se, QEMU_VM_SECTION_END); 87 | se->ops->save_live_complete_precopy(f, se->opaque) 88 | save_section_footer(f, se); 89 | 90 | ; iterate savevm_state and call vmstate_save 91 | save_section_header(f, se, QEMU_VM_SECTION_FULL); 92 | vmstate_save(f, se, vmdesc) 93 | save_section_footer(f, se); 94 | 95 | migration_detect_error 96 | migration_update_counters 97 | migration_iteration_finish 98 | ``` 99 | 100 | 虽然这个函数很长,不过整体的结构还算清晰。大致可以分成这么几个阶段: 101 | 102 | * 发送header 103 | * 建立迁移的准备 104 | * 迭代传输 105 | * 完成迁移 106 | 107 | 其中主要就是通过几个不同的se->ops来实现的。 108 | 109 | # 接收端 110 | 111 | ## 从incoming开始 112 | 113 | 接收端在运行时需要加上-incoming选项,所以我们也从incoming开始。 114 | 115 | ``` 116 | qemu_start_incoming_migration() 117 | deferred_incoming_migration() 118 | tcp_start_incoming_migration() 119 | socket_start_incoming_migration() 120 | rdma_start_incoming_migration() 121 | rdma_accept_incoming_migration() 122 | migration_fd_process_incoming() 123 | migration_incoming_setup() 124 | migration_incoming_process() 125 | exec_start_incoming_migration() 126 | exec_accept_incoming_migration() 127 | migration_channel_process_incoming() 128 | unix_start_incoming_migration() 129 | socket_start_incoming_migration() 130 | socket_accept_incoming_migration() 131 | migration_channel_process_incoming() 132 | fd_start_incoming_migration() 133 | fd_accept_incoming_migration() 134 | migration_channel_process_incoming() 135 | migration_tls_channel_process_incoming() 136 | migration_ioc_process_incoming() 137 | migration_incoming_process() 138 | process_incoming_migration_co() 139 | qemu_loadvm_state() 140 | ``` 141 | 142 | 看着要比发送端麻烦些,不过还好找到了各种方式最终都执行到qemu_loadvm_state()。 143 | 144 | ## qemu_loadvm_state 145 | 146 | ``` 147 | qemu_loadvm_state() 148 | qemu_get_be32, QEMU_VM_FILE_MAGIC 149 | qemu_get_be32, QEMU_VM_FILE_VERSION 150 | qemu_loadvm_state_setup 151 | se->ops->load_setup 152 | vmstate_load_state(f, &vmstate_configuration, &savevm_state, 0) 153 | cpu_synchronize_all_pre_loadvm 154 | cpu_synchronize_pre_loadvm(cpu) 155 | 156 | qemu_loadvm_state_main 157 | section_type = qemu_get_byte(f) 158 | QEMU_VM_SECTION_START | QEMU_VM_SECTION_FULL 159 | qemu_loadvm_section_start_full 160 | section_id = qemu_get_be32 161 | vmstate_load 162 | QEMU_VM_SECTION_PART | QEMU_VM_SECTION_END 163 | qemu_loadvm_section_part_end 164 | section_id = qemu_get_be32 165 | vmstate_load 166 | QEMU_VM_COMMAND 167 | loadvm_process_command 168 | QEMU_VM_EOF 169 | 170 | qemu_loadvm_state_cleanup 171 | se->ops->load_cleanup 172 | cpu_synchronize_all_post_init 173 | cpu_synchronize_post_init(cpu); 174 | ``` 175 | 176 | 接收的过程相对发送要“简单”,主要的工作都隐藏在了section的三种情况中。 177 | 178 | * QEMU_VM_SECTION_START | QEMU_VM_SECTION_FULL 179 | * QEMU_VM_SECTION_PART | QEMU_VM_SECTION_END 180 | * QEMU_VM_COMMAND 181 | 182 | 第一种代表了setup阶段和最后vmstate_save阶段。 183 | 第二种代表了iteration的中间阶段和最后一次完成。 184 | 第三种还没有仔细看。 185 | 186 | 但是至少从前两种情况看,大家都走到了vmstate_load。不错。 187 | 188 | # SaveStateEntry 189 | 190 | 迁移过程中起到关键作用的数据结构名字是SaveStateEntry,也就是代码中的se。 191 | 192 | ``` 193 | SaveState(savevm_state) 194 | +--------------------------------------+ 195 | |global_section_id | 196 | | (int) | 197 | |name | 198 | | (char*) | 199 | |len | 200 | |target_page_bits | 201 | |caps_count | 202 | | (uint32_t) | 203 | +--------------------------------------+ 204 | |capabilities | 205 | | (MigrationCapability*) | 206 | +--------------------------------------+ 207 | |handlers | 208 | | (list of SaveStateEntry) | 209 | +--------------------------------------+ 210 | | 211 | | 212 | ------------+-----+----------------------------------+-------------------------------------------+--- 213 | | | | 214 | | | | 215 | SaveStateEntry SaveStateEntry SaveStateEntry 216 | +-----------------------------+ +-----------------------------+ +------------------------------------+ 217 | |idstr | |idstr | |idstr | 218 | | = "block" | | = "ram" | | = "dirty-bitmap" | 219 | |ops | |ops | |ops | 220 | | = savevm_block_handlers | | = savevm_ram_handlers | | = savevm_dirty_bitmap_handlers | 221 | |opaque | |opaque | |opaque | 222 | | = block_mig_state | | = ram_state | | = dirty_bitmap_mig_state | 223 | |vmsd | |vmsd | |vmsd | 224 | | = NULL | | = NULL | | = NULL | 225 | +-----------------------------+ +-----------------------------+ +------------------------------------+ 226 | ``` 227 | 228 | 所有的SaveStateEntry结构都链接在全局链表savevm_state上。上图列举了几个比较重要的SaveStateEntry。比如名字叫ram的就是管理RAMBlock的。而且有意思的是,这几个的vmsd都是空。 229 | 230 | # 隐藏的重点 231 | 232 | 在上述migrate_thread的流程中,有一个隐藏的函数vmstate_save。如果你把这个函数打开,那又将是一番新的天地。今天就先到这里把。 233 | -------------------------------------------------------------------------------- /lm/03-vmsd.md: -------------------------------------------------------------------------------- 1 | 在迁移的最后阶段,对每个SaveStateEntry的vmsd会跳用函数vmstate_save。在上一节中,我们跳过了这部分的讲解,在这节中我们补上。 2 | 3 | # VMStateDescription是SaveStateEntry的一部分 4 | 5 | VMStateDescription并不是单独存在,而是SaveStateEntry的一部分。但是并非所有的SaveStateEntry结构都有vmsd。比如上一小节看到的“ram”,”block“这几个SaveStateEntry就没有。 6 | 7 | 正因为如此,vmsd的出现会伴随这SaveStateEntry。通畅我们见到创建两者的函数是vmstate_register_with_alias_id。比如在函数apic_common_realize中,每个apic设备就会有一个SaveStateEntry且它的vmsd就是vmstate_apic_common。 8 | 9 | # 发送流程 vmstate_save 10 | 11 | 接下来我们了解一下上一小节中我们略过的函数。 12 | 13 | ``` 14 | vmstate_save(f, se, vmdesc) 15 | if (!se->vmsd) { 16 | vmstate_save_old_style(f, se, vmdesc); 17 | 18 | vmstate_save_state(f, se->vmsd, se->opaque, vmdesc) 19 | vmsd->pre_save(se->opaque) 20 | 21 | ; iterate on fields 22 | ; iterate on elements of this field 23 | vmsd_desc_field_start 24 | vmstate_save_state(f, field->vmsd, curr_elem, vmdesc_loop) 25 | vmstate_save_state_v(f, field->vmsd, curr_elem, vmdesc_loop, field->struct_version_id) 26 | field->info->put(f, curr_elem, size, field, vmdesc_loop) 27 | vmsd_desc_field_end 28 | 29 | ; go into subsection 30 | vmstate_subsection_save() 31 | vmstate_save_state(f, vmsdsub, se->opaque, vmdesc) 32 | 33 | vmsd->post_save(se->opaque) 34 | ``` 35 | 36 | 当然这个调用图中我们跳过了没有vmsd的情况。先着重观察有vmsd时的操作。 37 | 38 | 其实上面的结构不是很清楚,让我再多说两句: 39 | 40 | * vmsd有多个field 41 | * field有多个element 42 | * field类型可以嵌套 43 | 44 | # 接收流程 vmstate_load 45 | 46 | 这个函数是vmstate_save的另一半。 47 | 48 | ``` 49 | vmstate_load(f, se) 50 | if (!se->vmsd) 51 | se->ops->load_state() 52 | vmstate_load_state(f, se->vmsd, se->opaque, se->load_version_id) 53 | vmsd->pre_load(se->opaque) 54 | 55 | ; iterate on fields 56 | ; iterate on elements of this field 57 | vmstate_load_state(f, field->vmsd, curr_elem, field->vmsd->version_id) 58 | vmstate_load_state(f, field->vmsd, curr_elem, field->struct_version_id) 59 | field->info->get(f, curr_elem, size, field) 60 | 61 | ; go into subsection 62 | vmstate_subsection_save() 63 | vmstate_save_state(f, vmsdsub, se->opaque, vmdesc) 64 | 65 | vmsd->pre_save(se->opaque) 66 | ``` 67 | 68 | 可谓是珠联璧合。 69 | 70 | # 结构体 71 | 72 | 是时候展示一下结构体的庐山真面目了。 73 | 74 | ``` 75 | +--------------------------------------+ 76 | |vmsd | 77 | | (VMStateDescription *) | 78 | | +--------------------------------+ 79 | | |name | 80 | | | (char *) | 81 | | +--------------------------------+ 82 | | |version_id | 83 | | |minimum_version_id | 84 | | |minimum_version_id_old | 85 | | | (int) | 86 | | +--------------------------------+ 87 | | |unmigratable | 88 | | | (int) | 89 | | +--------------------------------+ 90 | | |priority | 91 | | | (MigrationPriority) | 92 | | +--------------------------------+ 93 | | |load_state_old | 94 | | |pre_load | 95 | | |post_load | 96 | | |pre_save | 97 | | |post_save | 98 | | |needed | 99 | | | bool (*)() | 100 | | +--------------------------------+ 101 | | |fields | 102 | | | (VMStateField *) | 103 | | | +---------------------------+ 104 | | | |flags | POINTER, ARRARY, V/STRUCT, VARRAY_INT32, BUFFER, ARRAY_OF_POINTER 105 | | | | (VMStateFlags) | VARRAY_UINT8/16/32, VBUFFER, MULTIPLY/_ELEMENTS, MUST_EXIST, ALLOC 106 | | | | | 107 | | | |name | 108 | | | |offset | 109 | | | |start | 110 | | | | | 111 | | | |size | 112 | | | |size_offset | 113 | | | | | 114 | | | |num | 115 | | | |num_offset | 116 | | | | | 117 | | | |version_id | 118 | | | |struct_version_id | 119 | | | |field_exists | 120 | | | | | 121 | | | |vmsd | 122 | | | | (VMStateDescription*) | 123 | | | | | 124 | | | |info | 125 | | | | (VMStateInfo *) | 126 | | | | +----------------------+ 127 | | | | |name | 128 | | | | |get | 129 | | | | |put | 130 | | +----+----+----------------------+ 131 | | |subsections | 132 | | | (VMStateDescription **) | 133 | +-----+--------------------------------+ 134 | ``` 135 | 136 | 是不是很长,感觉一头雾水? 我也觉得是,那就再来一张凸显其中某些关联的图。 137 | 138 | ``` 139 | SaveStateEntry 140 | +--------------------------------------+ 141 | |opaque | 142 | | (void *) ----|--->+-+----+----+----+----+----+----+----+----+----+------------------+ 143 | | | | |elem|elem|elem|elem|elem|elem|elem|elem|elem| | 144 | | | +-+----+----+----+----+----+----+----+----+----+------------------+ 145 | | | ^ ^ 146 | | | | | 147 | | | | | 148 | | | | field | field 149 | +--------------------------------------+ +----------------+ +----------------+ 150 | |vmsd | | | 151 | | (VMStateDescription *) ----|--->+--------------------------------+ +--------------------------------+ 152 | +--------------------------------------+ |fields | | |fields | | 153 | |subsections | |(VMStateField *) | | |(VMStateField *) | | 154 | | (VMStateDescription **) | | +---------------------------+ | +---------------------------+ 155 | +--------------------------------------+ | |name | | | |name | | 156 | | |offset ----+ | | |offset ----+ | 157 | | |start | | |start | 158 | | | | | | | 159 | | |size | => size | |size | => size 160 | | |size_offset | | |size_offset | 161 | | | | | | | 162 | | |num | => n_elems | |num | => n_elems 163 | | |num_offset | | |num_offset | 164 | | | | | | | 165 | | |version_id | | |version_id | 166 | | |struct_version_id | | |struct_version_id | 167 | | |field_exists | | |field_exists | 168 | | | | | | | 169 | | |vmsd | | |vmsd | 170 | | | (VMStateDescription*) | | | (VMStateDescription*) | 171 | | | | | | | 172 | | |info | | |info | 173 | | | (VMStateInfo *) | | | (VMStateInfo *) | 174 | | | +----------------------+ | | +----------------------+ 175 | | | |name | | | |name | 176 | | | |get | | | |get | 177 | | | |put | | | |put | 178 | +----+----+----------------------+ +----+----+----------------------+ 179 | ``` 180 | 181 | 这样希望能够突出几点: 182 | 183 | * vmsd包含了fields的数组,每个元素都是VMStateField结构 184 | * 每个field又包含了多个elements,其个数由num/num_offset决定,其大小由size/size_offset决定 185 | * elements的起始位置由offset决定,在opaque指针指向的空间 186 | 187 | # 如何定义 188 | 189 | 对结构体有了大致了概念后,我们就可以来看看代码中我们是如何定义的。下面这个是apic设备的vmsd。 190 | 191 | ``` 192 | static const VMStateDescription vmstate_apic_common = { 193 | .name = "apic", 194 | .version_id = 3, 195 | .minimum_version_id = 3, 196 | .minimum_version_id_old = 1, 197 | .load_state_old = apic_load_old, 198 | .pre_load = apic_pre_load, 199 | .pre_save = apic_dispatch_pre_save, 200 | .post_load = apic_dispatch_post_load, 201 | .fields = (VMStateField[]) { 202 | VMSTATE_UINT32(apicbase, APICCommonState), 203 | VMSTATE_UINT8(id, APICCommonState), 204 | VMSTATE_UINT8(arb_id, APICCommonState), 205 | VMSTATE_UINT8(tpr, APICCommonState), 206 | VMSTATE_UINT32(spurious_vec, APICCommonState), 207 | VMSTATE_UINT8(log_dest, APICCommonState), 208 | VMSTATE_UINT8(dest_mode, APICCommonState), 209 | VMSTATE_UINT32_ARRAY(isr, APICCommonState, 8), 210 | VMSTATE_UINT32_ARRAY(tmr, APICCommonState, 8), 211 | VMSTATE_UINT32_ARRAY(irr, APICCommonState, 8), 212 | VMSTATE_UINT32_ARRAY(lvt, APICCommonState, APIC_LVT_NB), 213 | VMSTATE_UINT32(esr, APICCommonState), 214 | VMSTATE_UINT32_ARRAY(icr, APICCommonState, 2), 215 | VMSTATE_UINT32(divide_conf, APICCommonState), 216 | VMSTATE_INT32(count_shift, APICCommonState), 217 | VMSTATE_UINT32(initial_count, APICCommonState), 218 | VMSTATE_INT64(initial_count_load_time, APICCommonState), 219 | VMSTATE_INT64(next_time, APICCommonState), 220 | VMSTATE_INT64(timer_expiry, 221 | APICCommonState), /* open-coded timer state */ 222 | VMSTATE_END_OF_LIST() 223 | }, 224 | .subsections = (const VMStateDescription*[]) { 225 | &vmstate_apic_common_sipi, 226 | NULL 227 | } 228 | }; 229 | ``` 230 | 231 | 可以看到,它的fields上有多个部分,而且还包含了一个subsection。其他的成员都一眼看穿,唯独fields成员的定义隐藏在VMSTATE_宏定义里。那就让我们打开这个宏定义看看吧。 232 | 233 | ## VMSTATE_UINT8 234 | 235 | ``` 236 | VMSTATE_UINT8(id, APICCommonState) 237 | 238 | #define VMSTATE_UINT8(_f, _s) \ 239 | VMSTATE_UINT8_V(_f, _s, 0) 240 | 241 | VMSTATE_UINT8_V(id, APICCommonState, 0) 242 | 243 | #define VMSTATE_UINT8_V(_f, _s, _v) \ 244 | VMSTATE_SINGLE(_f, _s, _v, vmstate_info_uint8, uint8_t) 245 | 246 | VMSTATE_SINGLE(id, APICCommonState, 0, vmstate_info_uint8, uint8_t) 247 | 248 | #define VMSTATE_SINGLE(_field, _state, _version, _info, _type) \ 249 | VMSTATE_SINGLE_TEST(_field, _state, NULL, _version, _info, _type) 250 | 251 | VMSTATE_SINGLE_TEST(id, APICCommonState, NULL, 0, vmstate_info_uint8, uint8_t) 252 | 253 | #define VMSTATE_SINGLE_TEST(_field, _state, _test, _version, _info, _type) { \ 254 | .name = (stringify(_field)), \ 255 | .version_id = (_version), \ 256 | .field_exists = (_test), \ 257 | .size = sizeof(_type), \ 258 | .info = &(_info), \ 259 | .flags = VMS_SINGLE, \ 260 | .offset = vmstate_offset_value(_state, _field, _type), \ 261 | } 262 | 263 | { 264 | .name = "id", 265 | .version_id = 0 266 | .field_exists = NULL, 267 | .size = 1, 268 | .info = &vmstate_info_uint8, 269 | .flags = VMS_SINGLE, 270 | .offset = offsetof(APICCommonState, id), 271 | } 272 | ``` 273 | 274 | ## VMSTATE_UINT32_ARRAY 275 | 276 | ``` 277 | VMSTATE_UINT32_ARRAY(tmr, APICCommonState, 8) 278 | 279 | #define VMSTATE_UINT32_ARRAY(_f, _s, _n) \ 280 | VMSTATE_UINT32_ARRAY_V(_f, _s, _n, 0) 281 | 282 | VMSTATE_UINT32_ARRAY_V(tmr, APICCommonState, 8, 0) 283 | 284 | #define VMSTATE_UINT32_ARRAY_V(_f, _s, _n, _v) \ 285 | VMSTATE_ARRAY(_f, _s, _n, _v, vmstate_info_uint32, uint32_t) 286 | 287 | VMSTATE_ARRAY(tmr, APICCommonState, 8, 0, vmstate_info_uint32, uint32_t) 288 | 289 | #define VMSTATE_ARRAY(_field, _state, _num, _version, _info, _type) {\ 290 | .name = (stringify(_field)), \ 291 | .version_id = (_version), \ 292 | .num = (_num), \ 293 | .info = &(_info), \ 294 | .size = sizeof(_type), \ 295 | .flags = VMS_ARRAY, \ 296 | .offset = vmstate_offset_array(_state, _field, _type, _num), \ 297 | } 298 | 299 | { 300 | .name = "tmr", 301 | .version_id = 0, 302 | .num = 8, 303 | .info = &vmstate_info_uint32, 304 | .size = 4, 305 | .flags = VMS_ARRAY, 306 | .offset = offsetof(APICCommonState, tmr), 307 | } 308 | ``` 309 | -------------------------------------------------------------------------------- /lm/04-ram_migration.md: -------------------------------------------------------------------------------- 1 | 内存是整个虚拟机中重要的部分,也是迁移过程中迁移量最大的部分。 2 | 3 | 首先我们将从迁移框架的流程上看看整个过程都有哪些函数参与其中,了解内存迁移的流程结构。 4 | 5 | 接着我们要从数据结构层面看看我们在这些流程中究竟是要处理哪些数据。 6 | 7 | # 内存迁移流程 8 | 9 | ## 发送流程 10 | 11 | 要说流程,还是要先回到总体架构那一节中的总体流程。 12 | 13 | ``` 14 | migration_thread() 15 | 16 | qemu_savevm_state_header 17 | qemu_put_be32(f, QEMU_VM_FILE_MAGIC); 18 | qemu_put_be32(f, QEMU_VM_FILE_VERSION); 19 | qemu_put_be32(f, QEMU_VM_CONFIGURATION); 20 | vmstate_save_state(f, &vmstate_configuration, &savevm_state, 0); 21 | qemu_savevm_send_open_return_path(s->to_dst_file); 22 | qemu_savevm_send_ping(s->to_dst_file, 1); 23 | qemu_savevm_command_send(f, MIG_CMD_PING, , (uint8_t *)&buf) 24 | 25 | ; iterate savevm_state and call save_setup 26 | qemu_savevm_state_setup(s->to_dst_file); 27 | save_section_header(f, se, QEMU_VM_SECTION_START) 28 | se->ops->save_setup(f, se->opaque) 29 | save_section_footer(f, se) 30 | precopy_notify(PRECOPY_NOTIFY_SETUP, &local_err) 31 | 32 | migrate_set_state(&s->state, MIGRATION_STATUS_SETUP, MIGRATION_STATUS_ACTIVE); 33 | migration_iteration_run 34 | 35 | ; iterate savevm_state and call save_live_pending 36 | qemu_savevm_state_pending(pend_pre/compat/post) 37 | se->ops->save_live_pending() 38 | 39 | ; iterate savevm_state and call save_live_iterate 40 | qemu_savevm_state_iterate() 41 | save_section_header(f, se, QEMU_VM_SECTION_PART) 42 | se->ops->save_live_iterate(f, se->opaque) 43 | save_section_footer(f, se) 44 | migration_completion() 45 | qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL); 46 | vm_stop_force_state(RUN_STATE_FINISH_MIGRATE) 47 | 48 | qemu_savevm_state_complete_precopy(s->to_dst_file, false, inactivate); 49 | ; iterate savevm_state and call save_live_complete_precopy 50 | cpu_synchronize_all_states(); 51 | save_section_header(f, se, QEMU_VM_SECTION_END); 52 | se->ops->save_live_complete_precopy(f, se->opaque) 53 | save_section_footer(f, se); 54 | 55 | ; iterate savevm_state and call vmstate_save 56 | save_section_header(f, se, QEMU_VM_SECTION_FULL); 57 | vmstate_save(f, se, vmdesc) 58 | save_section_footer(f, se); 59 | 60 | migration_detect_error 61 | migration_update_counters 62 | migration_iteration_finish 63 | ``` 64 | 65 | 在这个基础上我们进一步抽取和简化,能看到整个过程中主要是这么几个回调函数在起作用。 66 | 67 | * se->ops->save_setup 68 | * se->ops->save_live_pending 69 | * se->ops->save_live_iterate 70 | * se->ops->save_live_complete_precopy 71 | 72 | 那对应到内存,这个se就是savevm_ram_handlers,其对应的函数们就是 73 | 74 | * ram_save_setup 75 | * ram_save_pending 76 | * ram_save_iterate 77 | * ram_save_complete 78 | 79 | 虽然里面做了很多事儿,也有很多细节上的优化,不过总的流程可以总结成两句话: 80 | 81 | * 用bitmap跟踪脏页 82 | * 将脏页传送到对端 83 | 84 | ## 接收流程 85 | 86 | 看完了发送,还得来看看接收。同样,看具体内存之前,先看一看总体架构。 87 | 88 | ``` 89 | qemu_loadvm_state() 90 | qemu_get_be32, QEMU_VM_FILE_MAGIC 91 | qemu_get_be32, QEMU_VM_FILE_VERSION 92 | qemu_loadvm_state_setup 93 | se->ops->load_setup 94 | vmstate_load_state(f, &vmstate_configuration, &savevm_state, 0) 95 | cpu_synchronize_all_pre_loadvm 96 | cpu_synchronize_pre_loadvm(cpu) 97 | 98 | qemu_loadvm_state_main 99 | section_type = qemu_get_byte(f) 100 | QEMU_VM_SECTION_START | QEMU_VM_SECTION_FULL 101 | qemu_loadvm_section_start_full 102 | section_id = qemu_get_be32 103 | vmstate_load 104 | QEMU_VM_SECTION_PART | QEMU_VM_SECTION_END 105 | qemu_loadvm_section_part_end 106 | section_id = qemu_get_be32 107 | vmstate_load 108 | QEMU_VM_COMMAND 109 | loadvm_process_command 110 | QEMU_VM_EOF 111 | 112 | qemu_loadvm_state_cleanup 113 | se->ops->load_cleanup 114 | cpu_synchronize_all_post_init 115 | cpu_synchronize_post_init(cpu); 116 | ``` 117 | 118 | 进一步打开,我们可以看到有这么几个重要的函数。 119 | 120 | * se->ops->load_setup 121 | * se->ops->load_state 122 | 123 | 对应到内存,分别是 124 | 125 | * ram_load_setup 126 | * ram_load 127 | 128 | # 发送接收对应关系 129 | 130 | ``` 131 | source destination 132 | 133 | +------------------------+ +-------------------------+ 134 | | | | | 135 | SETUP | ram_save_setup | | ram_load_setup | 136 | | | | | 137 | +------------------------+ +-------------------------+ 138 | 139 | sync dirty bit to Setup RAMBlock->receivedmap 140 | RAMBlock->bmap 141 | 142 | 143 | +------------------------+ +-------------------------+ 144 | | | | | 145 | ITER | ram_save_pending | | ram_load | 146 | | ram_save_iterate | | | 147 | | | | | 148 | +------------------------+ +-------------------------+ 149 | 150 | sync dirty bit Receive page 151 | and send page 152 | 153 | 154 | +------------------------+ +-------------------------+ 155 | | | | | 156 | COMP | ram_save_pending | | ram_load | 157 | | ram_save_complete | | | 158 | | | | | 159 | +------------------------+ +-------------------------+ 160 | 161 | sync dirty bit Receive page 162 | and send page 163 | ``` 164 | 165 | # 脏页同步 166 | 167 | 在整个内存迁移的过程中脏页同步是重中之重了,这一小节我们来看看qemu是如何获得这一段时间中的虚拟机脏页的。 168 | 169 | ## 代码流程 170 | 171 | qemu发起获得脏页的地方有几处,比如每次迭代开始,或者最后要结束的时候。这个动作都通过统一的函数migration_bitmap_sync()。 172 | 173 | ``` 174 | migration_bitmap_sync 175 | memory_global_dirty_log_sync (1) 176 | memory_region_sync_dirty_bitmap(NULL); 177 | listener->log_sync(listener, &mrs) -> kvm_log_sync 178 | kvm_physical_sync_dirty_bitmap 179 | migration_bitmap_sync_range; called on each RAMBlock 180 | cpu_physical_memory_sync_dirty_bitmap (2) 181 | ``` 182 | 183 | 其中主要工作分成两步: 184 | 185 | * 通过KVM_GET_DIRTY_LOG获得脏页到kvm_dirty_log.dirty_bitmap,并复制到ram_list.dirty_memory 186 | * 再将ram_list.dirty_memory的脏页拷贝到RAMBlock->bmap 187 | 188 | 至于KVM_GET_DIRTY_LOG是怎么得到的脏页这个要看kvm的代码,其中一部分的功劳在vmx_flush_pml_buffer()。 189 | 190 | ## 相关数据结构 191 | 192 | 在内存迁移过程中重要的数据结构就是跟踪脏页的bitmap了。 193 | 194 | 其中一共用到了两个bitmap: 195 | 196 | * RAMBlock->bmap 197 | * ram_list.dirty_memory[DIRTY_MEMORY_MIGRATION] 198 | 199 | 如果要画一个图来解释的画,那么这个可能会像一点。 200 | 201 | ``` 202 | ram_list.dirty_memory[] 203 | +----------------------+---------------+--------------+----------------+ 204 | | | | | | 205 | +----------------------+---------------+--------------+----------------+ 206 | ^ ^ ^ ^ 207 | | | | | 208 | RAMBlock.bmap RAMBlock.bmap 209 | +----------------------+ +--------------+ 210 | | | | | 211 | +----------------------+ +--------------+ 212 | ``` 213 | 214 | ram_list.dirty_memroy[]是一整个虚拟机地址空间的bitmap。虽然虚拟机的地址空间可能有空洞,但是这个bitmap是连续的。 215 | RAMBlock.bmap是每个RAMBlock表示的地址空间的bitmap。 216 | 217 | 所以同步的时候分成两步: 218 | 219 | * 虚拟机对内存发生读写时会更新ram_list.dirty_memroy[] 220 | * 每次内存迁移迭代开始,将ram_list.dirty_memory[]更新到RAMBlock.bmap 221 | 222 | 这样两个bitmap各司其职可以同步进行。 223 | 224 | # 进化 225 | 226 | 理解了总体的流程和重要的数据结构,我们来看看内存迁移部分是如何进化的。如何从最开始的找到脏页迁移脏页,变成现在这么复杂的逻辑的。 227 | 228 | ## 零页 229 | 230 | 迁移的时候,如果知道这个页面的内容都是0,那么从信息的角度看,其实信息量很小。那是不是有更好的方法去传送这些信息呢? 231 | 232 | 函数save_zero_page_to_file(),就做了这件事。如果这个页面是零页,我们只传送一些标示符号。 233 | 234 | ## 压缩 235 | 236 | 这个想法也比较直接,就是在传送前先压缩一下。这个工作交给了do_compress_ram_page()。 237 | 238 | ## 多线程 239 | 240 | 这也是一个比较直观的想法,一个人干活慢,那就多叫几个人。多线程的设置在compress_threads_save_setup()。 241 | 242 | ## Free Page 243 | 244 | 这个想法相对来说就比较隐晦了。意思是系统中总有部分的内存没有使用是free的,那么这些内存在最开始的时候就没有必要传送。(默认第一遍是都传送的。) 245 | 246 | 当然要做到这点,需要在虚拟机中有一个内应。当前这个内应是virtio-balloon。在启动虚拟机时需要加上参数。 247 | 248 | ``` 249 | -object iothread,id=iothread1 --device virtio-balloon,free-page-hint=true,iothread=iothread1 250 | ``` 251 | 252 | ## multifd 253 | 254 | 接下来这个东西略微有点复杂。也不知道为什么名字叫multifd,好像很有来头的样子。那就先来看看其中重要的数据结构。 255 | 256 | 第一个叫multifd_send_state。这个结构在函数multifd_save_setup中初始化。样子大概长这样。 257 | 258 | ``` 259 | multifd_send_state 260 | +-------------------------------+ 261 | |packet_num | global 262 | | (uint64_t) | 263 | |sem_sync | 264 | |channels_ready | 265 | | (QemuSemaphore) | 266 | | | 267 | |pages | 268 | | (MultiFDPages_t*) | 269 | | | 270 | |params | [migrate_multifd_channels()] each channel has one MultiFDSendParams 271 | | (MultiFDSendParams*) | 272 | | | MultiFDSendParams MultiFDSendParams MultiFDSendParams 273 | +-------------------------------+ +-------------------+ +-------------------+ +-------------------+ 274 | |name | |name | |name | 275 | | multifdsend_0 | | multifdsend_1 | | multifdsend_2 | 276 | |pages | |pages | |pages | 277 | | (MultiFDPages_t*) | | (MultiFDPages_t*) | | (MultiFDPages_t*) | 278 | |packet | |packet | |packet | 279 | | (MultiFDPacket_t*)| | (MultiFDPacket_t*)| | (MultiFDPacket_t*)| 280 | | | | | | | 281 | | | | | | | 282 | +-------------------+ +-------------------+ +-------------------+ 283 | ``` 284 | 285 | 几点说明: 286 | 287 | * multifd是一个多线程的结构,一共有migrate_multifd_channels个线程,这个可以通过参数设置 288 | * 所以也很好理解的是对应有migrate_multifd_channels()个params,每个线程人手一个不要打架 289 | * 每个params中重要的两个成员是pages/packet,packet相当于控制信息,pages就是数据 290 | 291 | 接着我们来看看page和packet的样子。 292 | 293 | ``` 294 | pages 295 | (MultiFDPages_t*) 296 | +----------------------+ 297 | |packet_num | global 298 | | (uint64_t) | 299 | |block | 300 | | (RAMBlock*) | 301 | |allocated | 302 | |used | 303 | | (uint32_t) | 304 | |offset | [allocated] 305 | | (ram_addr_t) | 306 | |iov | [allocated] 307 | | (struct iovec*) | 308 | | +------------------+ 309 | | |iov_base | = block->host + offset 310 | | |iov_len | = TARGET_PAGE_SIZE 311 | | +------------------+ 312 | | | 313 | +----------------------+ 314 | ``` 315 | 316 | 看到这个大家或许能够明白一点,page其实是一个iov的集合。一共分配有allocated个iov的结构,用于发送数据。不过要注意的一点是,发送的所有数据都属于同一个RAMBlock。 317 | 318 | ``` 319 | packet 320 | (MultiFDPacket_t*) 321 | +----------------------+ 322 | |magic | = MULTIFD_MAGIC 323 | |version | = MULTIFD_VERSION 324 | |flags | = params->flags 325 | | | 326 | |pages_alloc | = page_count 327 | |pages_used | = pages->used 328 | |next_packet_size | = params->next_packet_size 329 | |packet_num | = params->packet_num 330 | | | 331 | |offset[] | [page_count] 332 | | (ram_addr_t) | 333 | +----------------------+ 334 | ``` 335 | 336 | 这个相当于每次发送时的元数据了。而且因为iov的传送只有base/len,所以这里还要传送在RAMBlock中的offset。 337 | 338 | ## xbzrle 339 | 340 | 这个东西也很高端,全称叫 [XOR Based Zero run lenght encoding][1]. 其中使用到的编码是[LEB128][2]. 341 | 342 | 这个东西很有意思,说起来其实也简单。 343 | 344 | **不直接传送每个页的内容,而是传送每个页的差值** 345 | 346 | 怎么样,是不是很有意思?好了,这个思想的所有内容就这么写,其他的就剩下具体的实现细节了。 347 | 348 | [1]: https://github.com/qemu/qemu/blob/master/docs/xbzrle.txt 349 | [2]: https://en.wikipedia.org/wiki/LEB128 350 | -------------------------------------------------------------------------------- /lm/05-postcopy.md: -------------------------------------------------------------------------------- 1 | postcopy对我来说一直是一个神秘的东西,这次终于有时间仔细研究一下。 2 | 3 | > 所谓postcopy直接来说就是在虚拟机启动后再迁移 4 | 5 | 正常情况下我们是先把虚拟机的信息拷贝到目的端,包括内存、设备,然后在目的端启动虚拟机。而postcopy引入了一个非常有意思的想法,就是先把虚拟机启动起来,然后再来迁移需要的信息。当然这里最重要的就是内存了。 6 | 7 | 本小节也是以内存为主线,其余支持postcopy的部分暂且不表。(主要是因为没看) 8 | 9 | # 从用法开始 10 | 11 | 在探讨细节前,我们先看看是怎么使用postcopy的。总体过程和普通迁移类似,有一点有意思的区别是在启动迁移后,再启动postcopy。 12 | 13 | 以在monitor中执行迁移为例,需要执行的命令是: 14 | 15 | ``` 16 | migrate_set_capability postcopy-ram on # both source and destination 17 | migrate_set_capability postcopy-blocktime on # both source and destination 18 | migrate -d tcp:0:4444 19 | migrate_start_postcopy # after first round of sync 20 | ``` 21 | 22 | 其中第一二条命令需要在目的和源端都执行。然后需要在启动迁移后,再开启postcopy。其中建议在运行了一轮迁移后执行。 23 | 24 | # 启动postcopy 25 | 26 | 执行migrate_start_postcopy命令,作用是将某个变量置为true。 27 | 28 | ``` 29 | hmp_migrate_start_postcopy 30 | qmp_migrate_start_postcopy 31 | atomic_set(&s->start_postcopy, true); 32 | ``` 33 | 34 | 而这个变量设置后,就会在migration_iteration_run判断。当达到条件则会开启postcopy。听着真是一点也不神奇。 35 | 36 | # postcopy中的交互 37 | 38 | 神奇的事情这个时候才开始发生 -- postcopy_start()函数隐藏了大部分的秘密。 39 | 40 | 先来一个简化的代码流程: 41 | 42 | ``` 43 | postcopy_start(), a little similar with migration_completion() 44 | migrate_set_state(MIGRATION_STATUS_ACTIVE, MIGRATION_STATUS_POSTCOPY_ACTIVE) 45 | qemu_system_wakeup_request(QEMU_WAKEUP_REASON_OTHER, NULL); 46 | global_state_store(); 47 | vm_stop_force_state(RUN_STATE_FINISH_MIGRATE); 48 | migration_maybe_pause(ms, &cur_state, MIGRATION_STATUS_POSTCOPY_ACTIVE); 49 | bdrv_inactivate_all(); 50 | qemu_savevm_state_complete_precopy(ms->to_dst_file, true, false); 51 | ram_postcopy_send_discard_bitmap() 52 | migration_bitmap_sync(), should be the last sync 53 | postcopy_chunk_hostpages(), Deal with TPS != HPS 54 | postcopy_chunk_hostpages_pass(ms, true, block, pds); 55 | postcopy_chunk_hostpages_pass(ms, false, block, pds); 56 | postcopy_each_ram_send_discard(), tell destination to discard page 57 | postcopy_discard_send_init() 58 | postcopy_send_discard_bm_ram() 59 | postcopy_discard_send_finish() 60 | qemu_savevm_send_postcopy_listen(fb); let destination in Listen State 61 | qemu_savevm_command_send(f, MIG_CMD_POSTCOPY_LISTEN), destination will start postcopy_ram_listen_thread 62 | qemu_savevm_state_complete_precopy(fb, false, false); 63 | qemu_savevm_send_postcopy_run(fb); 64 | qemu_savevm_command_send(f, MIG_CMD_POSTCOPY_RUN, 0, NULL); 65 | qemu_savevm_send_packaged(ms->to_dst_file, bioc->data, bioc->usage), MIG_CMD_PACKAGED 66 | ram_postcopy_migrated_memory_release(ms), release mem. 67 | ``` 68 | 69 | 貌似还是有点长,稍微总结一下: 70 | 71 | * 进入后虚拟机停掉了 72 | * 通知目的地那些内从空间是要丢弃的 73 | * 发送命令LISTNE/RUN给目的地 74 | 75 | 上面的流程看着已经有点麻烦了,但是这还只是迁移过程中的源端。目的端是如何和源端进行交互的呢?下面用一个小小的流程图解释一下。 76 | 77 | ``` 78 | Source Destination 79 | 80 | 81 | migration_thread qemu_loadvm_state_main 82 | loadvm_process_command 83 | ADVISE loadvm_postcopy_handle_advise 84 | check userfaultfd 85 | check page size match 86 | 87 | | | 88 | | | 89 | v v 90 | 91 | migration_iteration_run 92 | qemu_savevm_state_iterate() 93 | or 94 | postcopy_start 95 | ram_postcopy_send_discard_bitmap 96 | DISCARD loadvm_postcopy_ram_handle_discard 97 | NOHUGEPAGE 98 | clear receivedmap 99 | unmap 100 | 101 | | | 102 | | | 103 | v v 104 | 105 | qemu_savevm_send_postcopy_listen(fb) 106 | LISTEN loadvm_postcopy_handle_listen 107 | setup userfaultfd 108 | postcopy_ram_fault_thread 109 | ram_block_enable_notify 110 | postcopy_ram_listen_thread 111 | qemu_loadvm_state_main 112 | 113 | | | 114 | | | 115 | v v 116 | 117 | qemu_savevm_send_packaged(fb) 118 | PACKAGED loadvm_handle_cmd_packaged 119 | qemu_loadvm_state_main 120 | loadvm_process_command 121 | 122 | | | 123 | | | 124 | v v 125 | 126 | qemu_savevm_send_postcopy_run(fb) 127 | RUN loadvm_postcopy_handle_run 128 | loadvm_postcopy_handle_run_bh 129 | vm_start 130 | return LOADVM_QUIT 131 | 132 | ``` 133 | 134 | 目的和源端的交互是通过几个命令来实现的。 135 | 136 | * DISCARD : 通知目的端需要放弃的内存空间 137 | * LISTEN : 开启一个新的监听线程 138 | * PACKAGED : 这个命令有点意思,它将一坨东西打包发送 139 | * RUN : 告诉目的端可以运行了 140 | 141 | 当目的端收到RUN命令,主线程就会拉起虚拟机执行了。而在LISTEN命令下开启的postcopy_ram_listen_thread就会肩负其接收后续内存的责任。 142 | 143 | 好玩的是在目的端还用了一个超级简化的状态机来记录postcopy过程的状态变化:**incoming_postcopy_state(PostcopyState)** 144 | 145 | ``` 146 | +---------------------------+ 147 | |POSTCOPY_INCOMING_NONE | 148 | |POSTCOPY_INCOMING_ADVISE | ----------------+ 149 | |POSTCOPY_INCOMING_DISCARD | | 150 | |POSTCOPY_INCOMING_LISTENING| --+ +-- advise 151 | | | +-- running | 152 | |POSTCOPY_INCOMING_RUNNING | --+-------------+ 153 | |POSTCOPY_INCOMING_END | 154 | +---------------------------+ 155 | ``` 156 | 157 | 这个状态迁移实在太简单了,变化如下: 158 | 159 | ``` 160 | NONE 161 | 162 | | 163 | | MIG_CMD_POSTCOPY_ADVISE 164 | | 165 | 166 | ADVISE 167 | 168 | | \ 169 | | \ MIG_CMD_POSTCOPY_RAM_DISCARD 170 | | \ 171 | | DISCARD 172 | | / 173 | | / MIG_CMD_POSTCOPY_LISTEN 174 | | / 175 | 176 | LISTEN 177 | 178 | | 179 | | MIG_CMD_POSTCOPY_RUN 180 | | 181 | 182 | RUNNING 183 | 184 | | 185 | | 186 | | 187 | 188 | END 189 | ``` 190 | 191 | # userfaultfd 192 | 193 | 重点在上面都描述完了,这里只提一下这里利用到的一个内核特性userfaultfd。 194 | 195 | 也即是我们需要通过这个接口来: 196 | 197 | * 告诉虚拟机哪个内存被丢弃 198 | * 当新内存来到后,填入指定位置 199 | 200 | 好了,基本讲完了,以后想到新的再来补充。 201 | -------------------------------------------------------------------------------- /machine/00-mt.md: -------------------------------------------------------------------------------- 1 | 在Qemu中一台虚拟机由Machine表示,对应的设备模型是 2 | 3 | [MachineType][1] 4 | 5 | 不过这个类型是一个抽象结构,需要有对应的实际类型才能初始化。 6 | 7 | 对应我们常用的x86机器,对应的类型是 8 | 9 | [PCMachineType][2] 10 | 11 | [1]: /machine/01-machine_type.md 12 | [2]: /machine/02-pc_machine.md 13 | -------------------------------------------------------------------------------- /machine/01-machine_type.md: -------------------------------------------------------------------------------- 1 | MachineType是qemu中所有虚拟机类型的父类。虽然这是一个抽象类,但是对这个类的了解也能帮助我们对后续子类的理解有帮助。 2 | 3 | # 继承关系 4 | 5 | 先来看一下MachineType类本身的继承关系 6 | 7 | ``` 8 | TYPE_OBJECT 9 | +-------------------------------+ 10 | |class_init | = object_class_init 11 | | | 12 | |instance_size | = sizeof(Object) 13 | +-------------------------------+ 14 | 15 | 16 | TYPE_MACHINE 17 | +-------------------------------+ 18 | |class_size | = sizeof(MachineClass) 19 | |class_init | = machine_class_init 20 | |class_base_init | = machine_class_base_init 21 | | | 22 | |instance_size | = sizeof(MachineState) 23 | |instance_init | = machine_initfn 24 | |instance_finalize | = machine_finalize 25 | +-------------------------------+ 26 | ``` 27 | 28 | 基本的东西我想大家都一目了然了,值得注意的是MachineType继承于Object,而不是Device。所以之前的realize一套在这里就不适用了。 29 | 30 | # 类型选择 31 | 32 | 在Qemu中包含着多种MachineType类型,也就是这个类型有多个子类。那在进程启动时就需要选择指定类型,让我们来看一看。 33 | 34 | 这个过程在main函数中。 35 | 36 | ``` 37 | main 38 | select_machine() 39 | find_default_machine() 40 | machine_parse() 41 | ``` 42 | 43 | 这几个函数很简单,我想大家看一眼也就明白了。 44 | 45 | # 创建Machine 46 | 47 | 刚才说了,MachineType的父类是Object,而不是Device。所以创建时和普通设备有很大的不同。 48 | 49 | 具体的细节散落在main函数中,而且涉及到了不同MachineType类型的不同处理,这里就不一一展开了。(其实我不懂) 50 | 51 | 在这里我们指出几个重要的点: 52 | 53 | ``` 54 | main 55 | current_machine = 56 | MACHINE(object_new(object_class_get_name(OBJECT_CLASS(machine_class)))); 57 | machine_run_board_init(current_machine); 58 | machine_class->init(machine); 59 | ``` 60 | 61 | 第一句是创建出了一个虚拟机 current_machine。 62 | 第二句是调用init函数。 63 | -------------------------------------------------------------------------------- /machine/02-pc_machine.md: -------------------------------------------------------------------------------- 1 | 相对MachineType, PCMachine就要复杂得多了。 2 | 3 | 还是按照之前的顺序,先来看继承关系。 4 | 5 | # 继承关系 6 | 7 | ``` 8 | TYPE_OBJECT 9 | +-------------------------------+ 10 | |abstract | = true 11 | |class_init | = object_class_init 12 | | | 13 | |instance_size | = sizeof(Object) 14 | +-------------------------------+ 15 | 16 | 17 | TYPE_MACHINE 18 | +-------------------------------+ 19 | |abstract | = true 20 | |class_size | = sizeof(MachineClass) 21 | |class_init | = machine_class_init 22 | |class_base_init | = machine_class_base_init 23 | | | 24 | |instance_size | = sizeof(MachineState) 25 | |instance_init | = machine_initfn 26 | |instance_finalize | = machine_finalize 27 | +-------------------------------+ 28 | 29 | 30 | TYPE_PC_MACHINE 31 | +-------------------------------+ 32 | |abstract | = true 33 | |class_size | = sizeof(PCMachineClass) 34 | |class_init | = pc_machine_class_init 35 | |class_base_init | = NULL 36 | | | 37 | |instance_size | = sizeof(PCMachineState) 38 | |instance_init | = pc_machine_initfn 39 | |instance_finalize | = NULL 40 | +-------------------------------+ 41 | 42 | 43 | pc_i440x_4.0-machine 44 | +-------------------------------+ 45 | |abstract | = true 46 | |class_init | = pc_machine_v4_0_class_init 47 | | | -> pc_i440fx_4_0_machine_options 48 | | | 49 | |instance_size | = sizeof(PCMachineState) 50 | |instance_init | = pc_machine_initfn 51 | |instance_finalize | = NULL 52 | | | 53 | |init | = pc_init_v4_0 54 | | | -> pc_init1 55 | +-------------------------------+ 56 | ``` 57 | 58 | 所以我们看到PCMachine只是一个抽象父类,这正的虚拟机是它的子类。 59 | 60 | 而这些子类都由一个宏来定义 61 | 62 | ``` 63 | #define DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn) \ 64 | static void pc_machine_##suffix##_class_init(ObjectClass *oc, void *data) \ 65 | { \ 66 | MachineClass *mc = MACHINE_CLASS(oc); \ 67 | optsfn(mc); \ 68 | mc->init = initfn; \ 69 | } \ 70 | static const TypeInfo pc_machine_type_##suffix = { \ 71 | .name = namestr TYPE_MACHINE_SUFFIX, \ 72 | .parent = TYPE_PC_MACHINE, \ 73 | .class_init = pc_machine_##suffix##_class_init, \ 74 | }; \ 75 | static void pc_machine_init_##suffix(void) \ 76 | { \ 77 | type_register(&pc_machine_type_##suffix); \ 78 | } \ 79 | type_init(pc_machine_init_##suffix) 80 | ``` 81 | 82 | 其中重要的就是将mc->init赋值。也就是在machine_run_board_init()函数中调用的部分。 83 | 84 | # 初始化 85 | 86 | 初始化的工作很大一部分由mc->init函数完成,对于piix机器,这个工作就交给了pc_init1函数。 87 | 88 | 具体的我就不展开了,留到需要的时候再做讲解。 89 | -------------------------------------------------------------------------------- /memory_backend/00-memory_backend.md: -------------------------------------------------------------------------------- 1 | MemoryBackend是虚拟内存的后端,和前段设备pc-dimm一起组成完整的虚拟内存。 2 | 3 | 比如我们在做内存热插拔时的命令行: 4 | 5 | ``` 6 | object_add memory-backend-ram,id=ram0,size=1G 7 | device_add pc-dimm,id=dimm0,memdev=ram0,node=0 8 | ``` 9 | 10 | 其中第一行创建的就是MemoryBackend。 11 | 12 | 本章主要讲述MemoryBackend设备的 13 | 14 | [类层次结构](/memory_backend/01-class_hierarchy.md) 15 | [初始化流程](memory_backend/02-init_flow.md) 16 | -------------------------------------------------------------------------------- /memory_backend/01-class_hierarchy.md: -------------------------------------------------------------------------------- 1 | 作为一个设备类型,MemoryBackend自然也有自己的类层次结构。 2 | 3 | 经过这么多代码的洗礼,我就不多说什么了,直接上类继承关系图。 4 | 5 | ``` 6 | +------------------+ +----------------------+ 7 | | | | | 8 | | ObjectClass | ------------------------------ | Object | 9 | | class_init | | | 10 | | | | | 11 | +------------------+ +----------------------+ 12 | | | 13 | | | 14 | | | 15 | v v 16 | +--------------------------+ +----------------------+ 17 | | | | | 18 | |HostMemoryBackendClass | ------------------------------------ | HostMemoryBackend | 19 | | class_init | host_memory_backend_class_init | instance_init | host_memory_backend_init 20 | | | | | 21 | | +-----------------------+ | | 22 | | |UserCreatableClass | | | 23 | | | complete | host_memory_backend_memory_complete | | 24 | +--+-----------------------+ +---+------------------+ 25 | | | 26 | | | 27 | | | 28 | | | 29 | | TYPE_MEMORY_BACKEND_RAM | 30 | | +-----------------------+ | +----------------------+ 31 | | | | | | | 32 | +--- |HostMemoryBackendClass | +--- |HostMemoryBackend | 33 | | | class_init | ram_backend_class_init | | instance_init | 34 | | | bc->alloc | ram_backend_memory_alloc | | | 35 | | +-----------------------+ | +----------------------+ 36 | | | 37 | | TYPE_MEMORY_BACKEND_FILE | 38 | | +-----------------------+ | +----------------------+ 39 | | | | | | | 40 | +--- |HostMemoryBackendClass | +--- |HostMemoryBackendFile | 41 | | | class_init | file_backend_class_init | | instance_init | 42 | | | bc->alloc | file_backend_memory_alloc | | | 43 | | +-----------------------+ | +----------------------+ 44 | | | 45 | | TYPE_MEMORY_BACKEND_RAM | 46 | | +-----------------------+ | +----------------------+ 47 | | | | | | | 48 | +--- |HostMemoryBackendClass | +--- |HostMemoryBackendMemfd| 49 | | class_init | memfd_backend_class_init | instance_init | memfd_backend_instance_init 50 | | bc->alloc | memfd_backend_memory_alloc | | 51 | +-----------------------+ +----------------------+ 52 | 53 | ``` 54 | 55 | 可以看到,现在的MemoryBackend一共有三种具体的实现 ram, file, memfd。他们大同小异,主要的区别就在于bc->alloc函数的实现不同。 56 | 57 | 那这个bc->alloc究竟是如何起作用的,请看下节[初始化流程][/memory_backend/02-init_flow.md] 58 | -------------------------------------------------------------------------------- /memory_backend/02-init_flow.md: -------------------------------------------------------------------------------- 1 | 在上一小节的类层次结构中我们可以看到,MemoryBackend类没有继承自TYPE_DEVICE,而是有一个接口类UserCreatableClass。所以这个对象的初始化流程和其他的类型又略有不同。 2 | 3 | 这里列举在命令行中添加MemoryBackend的情况,这一切都从main函数开始。 4 | 5 | ``` 6 | qemu_opts_foreach(qemu_find_opts("object"), 7 | user_creatable_add_opts_foreach, 8 | object_create_delayed, &error_fatal); 9 | user_creatable_add_opts 10 | user_creatable_add_type(type, id, pdict, v, errp); 11 | object_new("memory-backend-file") 12 | host_memory_backend_init 13 | object_property_set(obj, v, e->key, &local_err); 14 | user_creatable_complete(USER_CREATABLE(obj), &local_err); 15 | ucc->complete() host_memory_backend_memory_complete 16 | bc->alloc() [ram|file|memfd]_backend_memory_alloc 17 | ``` 18 | 19 | 可以看出,在main函数中对每一个object命令行参数都会执行上述操作。查询到指定的类型后就会执行user_creatable_add_type。 20 | 21 | 并且其中特殊的是因为这是一个UserCreatableClass类型,还会调用user_creatable_complete做进一步的操作。 22 | 23 | 而其中的ucc->complete和bc->alloc就是MemoryBackend类型需要做的特殊操作了。 24 | 25 | 好了,我觉得已经讲得够多了。具体的细节大家可以在代码中找到。我只再多说一点,对于每个MemoryBackend都会有一个RAMBlock产生~ 26 | --------------------------------------------------------------------------------