代码分析文章《KVM虚拟机代码揭秘——QEMU代码结构分析》、《KVM虚拟机代码揭秘——中断虚拟化》、《KVM虚拟机代码揭秘——设备IO虚拟化》、《KVM虚拟机代码揭秘——QEMU的PCI总线与设备(上)》、《KVM虚拟机代码揭秘——QEMU的PCI总线与设备(下)》。先从大的方面分析代码结构,然后分中断、IO、PCI总线与设备详细介绍。
关于TCG的解释:TCG(Tiny Code Generator),QEMU的官方解释在http://wiki.qemu-project.org/documentation/TCG。
TCG的作用就是将Target的指令通过TCG前端转换成TCG ops,进而通过TCG后端转换成Host上运行的指令。
需要将QEMU移植到一个新CPU上运行,需要关注TCG后端。需要基于QEMU模拟一个新CPU,需要关注TCG前端。
在根目录生成,参照Makefile可知有如下文件组成:
qemu-img$(EXESUF): qemu-img.o $(block-obj-y) $(crypto-obj-y) $(io-obj-y) $(qom-obj-y) $(COMMON_LDADDS)qemu-nbd$(EXESUF): qemu-nbd.o $(block-obj-y) $(crypto-obj-y) $(io-obj-y) $(qom-obj-y) $(COMMON_LDADDS)qemu-io$(EXESUF): qemu-io.o $(block-obj-y) $(crypto-obj-y) $(io-obj-y) $(qom-obj-y) $(COMMON_LDADDS)
由于target比较多,编译也费时。可以指定便以特定的target:
qemu-system-x86_64的入口定义在vl.c的main中:
QEMU的main函数定义在vl.c中,是执行程序的起点,主要功能是建立一个虚拟的硬件环境。
.├── audio├── backends├── block├── bsd-user├── chardev├── configure├── contrib├── crypto 加密解密算法等。├── docs├── dtc├── fpu├── fsdev├── hw 所有硬件设备,包括总线、串口、网卡、鼠标等等。通过设备模块串在一起。├── include├── io├── linux-headers├── linux-user├── Makefile├── migration├── nbd├── net├── pc-bios├── pixman├── po├── qapi├── qga├── qobject├── qom├── README├── replay├── roms├── scripts├── stubs├── target 不同架构的对应目录,将客户CPU架构的TBs转化成TCG中间代码,这个就是TCG前半部分。├── tcg 这部分是使用TCG代码生成主机的代码,不同架构对应不同子目录。整个生成主机代码的过程即TCG后半部分。├── tests├── trace├── trace-events├── trace-events-all├── ui├── util├── VERSION├── vl.c main函数,程序执行起点。最主要的模拟循环,虚拟机环境初始化和CPU的执行。├── x86_64-softmmu https://www.cnblogs.com/arnoldlu/p/configure配置生成的目录
QEMU是一个模拟器,它能够动态模拟特定架构CPU指令,QEMU模拟的架构叫目标架构;运行QEMU的系统架构叫主机架构。
QEMU中有一个模块叫微型代码生成器,将目标代码翻译成主机代码。
运行在虚拟CPU上的代码叫做客户机代码,QEMU主要功能就是不断提取客户机代码并且转化成主机代码。
整个翻译分成两部分:将目标代码(TB)转化成TCG中间代码,然后再将中间代码转化成主机代码。
当新的代码从TB(Translation Block)中生成以后,将会保存到一个cache中,因为很多相同的TB会被反复的进行操作,所以这样类似于内存的cache,能够提高使用效率。而cache的刷新使用LRU算法。
由tb_gen_code调用,将客户机代码转换成主机代码。gen_intermediate_code之前是客户及代码,tcg_gen_code之后是主机代码,两者之间是TCG中间代码。
KVM中断虚拟化主要依赖于VT-x技术,VT-x主要提供了两种中断事件机制,分别是中断退出和中断注入。
中断退出:指虚拟机发生中断时,主动式的客户机发生VM-Exit,这样能够在主机中实现对客户机中断的注入。
中断注入:是指将中断写入VMCS对应的中断信息位,来实现中断的注入,当中断完成后通过读取中断的返回信息来分析中断是否正确。
中断注入的标志性函数kvm_set_irq,是中断注入的最开始。
第一个参数s,传递设置IRQ需要的vmfd句柄,以及IRQ的ioctl类型。
第二、三参数,是IRQ中断号,以及触发类型。
int kvm_set_irq(KVMState *s, int irq, int level){ struct kvm_irq_level event; int ret;
assert(kvm_async_interrupts_enabled());
event.level = level; event.irq = irq; ret = kvm_vm_ioctl(s, s->irq_set_ioctl, &event); 将irq_set_ioctl和具体IRQ信息写入vmfd。 if (ret < 0) { perror("kvm_set_irq"); abort(); }
return (s->irq_set_ioctl == KVM_IRQ_LINE) ? 1 : event.status;}
上面的ioctl对应内核中的kvm_vm_ioctl,内核首先case到KVM_IRQ_LINE。然后解析
static long kvm_vm_ioctl(struct file *filp, unsigned int ioctl, unsigned long arg){…#ifdef __KVM_HAVE_IRQ_LINE case KVM_IRQ_LINE_STATUS: case KVM_IRQ_LINE: { struct kvm_irq_level irq_event;
r = -EFAULT; if (copy_from_user(&irq_event, argp, sizeof(irq_event))) goto out;
r = kvm_vm_ioctl_irq_line(kvm, &irq_event, ioctl == KVM_IRQ_LINE_STATUS); if (r) goto out;
r = -EFAULT; if (ioctl == KVM_IRQ_LINE_STATUS) { if (copy_to_user(argp, &irq_event, sizeof(irq_event))) goto out; }
r = 0; break; }#endif
…}KVM中断路由(何为?)
从上可以看出针对不同类型的ROUTING方式和IRQCHIP,跳转到对应的中断注入函数。IRQCHIP类型的中断路由有PIC和IOAPIC;还有MSI和SINT类型。
PIC全称 Programmable Interrupt Controller,通常是指Intel 8259A双片级联构成的最多支持15个interrupts的中断控制系统。
APIC全称Advanced Programmable Interrupt Controller,APIC是为了多核平台而设计的。它由两个部分组成IOAPIC和LAPIC,其中IOAPIC通常位于南桥中用于处理桥上的设备所产生的各种中断,LAPIC则是每个CPU都会有一个。IOAPIC通过APICBUS(现在都是通过FSB/QPI)将中断信息分 派给每颗CPU的LAPIC,CPU上的LAPIC能够智能的决定是否接受系统总线上传递过来的中断信息,而且它还可以处理Local端中断的 pending、nesting、masking,以及IOAPIC于Local CPU的交互处理。
设置好虚拟中断控制器之后,在KVM_RUN退出以后,就开始遍历虚拟中断控制器,如果发现中断,就将中断写入中断信息位.
inject_pending_event在进入Guest之前被调用。
KVM虚拟机设备模拟实在QEMU中实现的,而KVM实现的实质上只是IO的拦截。真正的虚拟设备IO地址注册实在QEMU代码里面实现的。
QEMU中,初始化硬件设备的时候需要注册IO空间,有两种方法:
- PIO(Port IO) 端口IO
- MIO(Memory IO) 内存映射IO
PS:发觉这里面介绍的代码和最新的4.x已经很大差异,所以略过。
QEMU在初始化硬件的时候,最开始的函数就是pc_init1。在这个函数里面会相继的初始化CPU、中断控制器、ISA总线,然后就要判断是否需要支持PCI。如果支持则调用i440fx_init初始化PCI总线。
static void pc_init1(MachineState *machine, const char *host_type, const char *pci_type){…if (pcmc->pci_enabled) { pci_bus = i440fx_init(host_type, pci_type, &i440fx_state, &piix3_devfn, &isa_bus, pcms->gsi, system_memory, system_io, machine->ram_size, pcms->below_4g_mem_size, pcms->above_4g_mem_size, pci_memory, ram_memory); pcms->bus = pci_bus;} else { pci_bus = NULL; i440fx_state = NULL; isa_bus = isa_bus_new(NULL, get_system_memory(), system_io, &error_abort); no_hpet = 1;}…}
i440fx_init函数主要参数就是之前初始化好的ISA总线以及中断控制器,返回值就是PCI总线,之后我们就可以将设备统统挂载在这个上面。
在QEMU中,所有的设备包括总线,桥,一般设备都对应一个设备结构,通过register函数将所有的设备链接起来,就像Linux的模块一样,在QEMU启动的时候会初始化所有的QEMU设备,而对于PCI设备来说,QEMU在初始化以后还会进行一次RESET,将所有的PCI bar上的地址清空,然后进行统一分配。
QEMU(x86)里面的PCI的默认PCI设都是挂载主总线上的,貌似没有看到PCI-PCI桥,而桥的作用一般也就是连接两个总线,然后进行终端和IO的映射。
一般的PCI设备其实和桥很像,甚至更简单,关键区分桥和一般设备的地方就是class属性和bar地址。
struct PCIDevice表示了PCI设备的信息。
pci_register_bat主要给bar分配IO地址。
void pci_register_bar(PCIDevice *pci_dev, int region_num, uint8_t type, MemoryRegion *memory){ PCIIORegion *r; uint32_t addr; uint64_t wmask; pcibus_t size = memory_region_size(memory);
assert(region_num >= 0); assert(region_num < PCI_NUM_REGIONS); if (size & (size-1)) { fprintf(stderr, "ERROR: PCI region size must be pow2 " "type=0x%x, size=0x%"FMT_PCIBUS" ", type, size); exit(1); }
r = &pci_dev->io_regions[region_num]; r->addr = PCI_BAR_UNMAPPED; r->size = size; r->type = type; r->memory = memory; r->address_space = type & PCI_base_ADDRESS_SPACE_IO ? pci_dev->bus->address_space_io : pci_dev->bus->address_space_mem;
wmask = ~(size - 1); if (region_num == PCI_ROM_SLOT) { wmask |= PCI_ROM_ADDRESS_ENABLE; }
addr = pci_bar(pci_dev, region_num); pci_set_long(pci_dev->config + addr, type);
if (!(r->type & PCI_base_ADDRESS_SPACE_IO) && r->type & PCI_base_ADDRESS_MEM_TYPE_64) { pci_set_quad(pci_dev->wmask + addr, wmask); pci_set_quad(pci_dev->cmask + addr, ~0ULL); } else { pci_set_long(pci_dev->wmask + addr, wmask & 0xffffffff); pci_set_long(pci_dev->cmask + addr, 0xffffffff); }}