1.1 虚拟化
什么是虚拟化?按一般的感觉,虚拟化就是在一台物理机器上,可同时运行多个操作系统,而这些操作系统彼此之间不能感知。本质上说,虚拟化是通过空间上的分割,时间上的分时和模拟,将物理机上面的一份资源抽象成多份。虚拟机(Virtual Machine)是由虚拟化层提供的独立的虚拟计算机系统,每个虚拟机都拥有自己的虚拟硬件(CPU,内存,IO设备)。通过虚拟化层的模拟,虚拟机在上层软件看来,就是一个真实的物理设备。这个虚拟化层一般称为虚拟机监控器(virtual machine monitor)。
而学术界对虚拟化做了严格的定义。Popek在1974的论文提出了VM的三个特征:
q 等价:即应用程序在VMM之上的VM运行,除了时间因素外,其余都和在物理硬件上的执行行为相同。
q 资源控制:VMM对物理资源具有完全控制,而VM中的程序(包括操作系统)不得直接访问设备。
q 高性能:在虚拟环境中执行的应用程序,绝大多数指令要能够直接在物理机器上执行,只有少量指令需要VMM的模拟。这一条就过滤了模拟器(boches),是不能称为VM的。
1.1.1 CPU虚拟化
VMM对机器硬件资源的虚拟化过程中,CPU虚拟化是最为重要的一个环节。只有保证虚拟机的指令能够被正确地虚拟执行,各个虚拟机之间不互相影响,即指令的执行结果不改变其它虚拟化的状态,才能够保证整个虚拟化环境的正确和有效。CPU虚拟化是为每个虚拟化提供一个或者多个虚拟CPU(virtual CPU)。多个VCPU分时复用物理CPU。VMM必须为多个VCPU合理分配时间片并维护所有VCPU的状态,当一个VCPU的时间片用完需要切换时,要保存当前VCPU的状态,将被调度的VCPU的状态载入物理CPU。所以CPU虚拟化需要解决两个问题,一是VCPU的正确运行,二是VCPU的调度。
那么如何实现VCPU的正确运行?
简而言之,处理器呈现给软件的接口就是一堆指令(指令集)和一堆寄存器(包括用于通用运算的寄存器和用于控制处理器行为的状态和控制寄存器)。而IO设备呈现软件的接口也是一堆的状态和控制寄存器。这些都是系统的资源,其中影响处理器和设备状态和行为的寄存器称为关键资源或者特权资源。如x86处理器的CR0~CR4寄存器。可以读写系统关键资源的指令叫做敏感指令,如x86的lgdt/sgdt/lidt/sidt/in/out等等。
现代的大多数CPU都划分为两种或者多种状态。某些指令只能在最高级别的状态下才能正确执行,在其它级别执行是禁止的。操作系统内核的代码通常运行在最高级别的状态下。根据运行级别的不同,CPU指令可以分为两类:
q 特权指令:只能在最高级别状态下执行。在低级别状态执行后产生Trap。
q 非特权指令:可以在各个级别的状态下执行。
CPU虚拟化通常采用的是“特权解除”和陷入-模拟(trap-and-emulation)”技术。特权解除是指为了实现VMM对虚拟机的控制,降低Guest OS运行的特权级别,而将VMM运行在最高特权级的技术。Guest OS的大部分指令仍可以在硬件上直接运行,只有当Guest OS执行到特权指令时,才会陷入到最高特权级的VMM模拟执行。这样就实现了陷入-模拟执行。对照前面论文提出的VM三个条件,“特权解除”和“陷入-模拟”实现了VMM对资源的完全控制和高性能要求。
“陷入-模拟”要求敏感指令都是特权指令,都不能在低级别状态执行,可惜X86体系不能满足这个要求,在X86指令集中有十几条敏感指令不是特权指令,这就造成X86虚拟化的复杂性。比如,x86指令sgdt/sidt/sldt可以在用户态读取特权寄存器GDTR/IDTR/LDTR的值,smsw可以读取当前机器的状态,还有call ,jmp,int n,pop,push等指令,都会影响计算机系统的虚拟化。
1)x86的全虚拟化
VMware代表的全虚拟化要在运行时检测敏感指令,捕捉到敏感指令后模拟敏感指令的执行。
2)X86的半虚拟化
以 xen为代表。基本思想是修改Guest OS的代码,把敏感指令的操作,替换为对VMM的超级调用(hypercall)。
3)硬件辅助的虚拟化
为解决X86虚拟化的困难,intel推出了硬件辅助虚拟化技术。基本思想是引入新的处理器运行模式和新的指令。Guest OS运行在受控模式,原来的一些敏感指令可以直接在虚拟机环境执行。
同时VT技术的新指令可以用硬件完成“陷入-模拟”模式切换时上下文的保存和恢复,这样大大提高了“陷入-模拟”上下文切换的效率(此时已经不是原来意义的陷入-模拟,这里借用了陷入-模拟的概念)。
1.1.2 内存虚拟化
VMM掌控了所有的系统资源。内存自然也在VMM的管理和控制之下。但是Guest OS本身也有内存的管理机制,所以在虚拟化的场景下,就比通常系统的多了一层映射。通常的计算机系统,经过MMU转换后的地址就是真实的物理地址,而在虚拟化的情况下,这个地址还需要经过VMM的一次映射,才能得到真正的物理地址。列出虚拟化情况需要转换的地址:
GVA:虚拟地址,指VM里面进程所使用的内存地址。
GPA:物理地址。这个地址是伪物理地址,是经过MMU映射的,虚拟机上面看到的物理地址。
HVA:宿主机的虚拟地址。
HPA:宿主机物理地址。是真实的物理地址。
但在实际应用中,如果VM的每个地址访问都需要VMM的参与,都需要经过一次软件转换,代价是不可接受的。为了实现虚拟地址到机器地址的高效转换,常用的有两种转换方法:一种是半虚拟化采用的MMU虚拟化,另外一种是全虚拟化采用的影子页表。
1) MMU虚拟化
MMU半虚拟化,是指利用物理的MMU,在虚拟机里面一次完成从虚拟地址到机器地址的映射。为达到这个目的,VMM是需要保存一张转换表(从虚拟机物理地址到机器地址),经过转换表的处理,VM里面的页表中的地址不再是虚拟机的物理地址,而是真实的机器地址。
为实现MMU虚拟化,必须对Guest OS的内核做修改,在每个涉及到内存分配和页表操作的地方,都要改为VMM提供的超级调用。VMM通过超级调用,查找转换表,对页表项做修改,载入真实的机器地址。
对于VM来说,它可以知道真正的机器地址,因此为了保护各个VM之间的隔离,VMM在对页表进行地址转换前,会对页表中的每一个页表项检查,确保页表项只映射了属于该VM的机器页面,而且不能包含对页面的可写映射。
2) 影子页表
全虚拟化的情况下,Guest OS必须是透明的,也就是说不能修改Guest OS的系统内核。这样需要改Guest OS系统内核的MMU虚拟化就不能应用了,必须使用影子页表的技术。影子页表是为Guest OS的每个页表都维护一个影子页表,并将合成后的映射关系(从虚拟地址到机器地址)写入到影子页表,而Guest OS的页表内容则保持不变。然后,VMM将影子页表交给MMU进行地址转换。
在利用影子页表的情况下,Geust OS所见到的页表内容没任何变化,影子页表的分配和维护完全由VMM控制。但是,使用影子页表的开销是很大的,一方面是时间开销,一方面是空间开销。
首先是时间开销,Guest OS构造页表的时候不会通知VMM(对比MMU虚拟化是通过超级调用通知VMM),VMM必须要等到Guest OS发生缺页,通过分析缺页原因,再为其补全影子页表。此过程中VMM需要通过模拟MMU遍历Guest OS的页表,才能获得Guest OS维护的地址映射关系(虚拟地址到VM的物理地址),这种间接手段的效率比半虚拟化的效率低很多。
其次是空间开销。VMM需要支持多个VM同时运行。以linux而言,Guest OS里面每个用户进程
都有自己的页表,因此影子页表的空间开销会随着进程数量的增多而迅速增大。而且Guest OS的进程数量对于VMM来说是不可控的。
1.1.3 IO虚拟化
设备对软件来说,就是一堆的寄存器(io端口)和IO内存,以及中断和DMA。而设备虚拟化的过程,就是模拟设备的这些寄存器和内存,然后截获Guest OS里面对IO端口和寄存器的访问,然后通过软件的方式来模拟真实的硬件。
1)全虚拟化的IO虚拟化
在全虚拟化,因为不修改Guest OS的内核,Guest OS保存了IO设备的原生驱动。但是VMM处理设备的方式会根据VMM位置的不同而有所不同。例如,全虚拟化最有代表性的VMware ESX和VMWare Workstattion,由于VMM实现模式不同,采用的设备虚拟化方式也不同。
在VMware ESX中,VMM直接运行在物理硬件之上,直接操作硬件设备,而Guest OS看到的则是一组统一的虚拟IO设备。Guest OS对这些虚拟设备的每一个IO操作都会陷入VMM 中,由VMM对IO指令进行解析并映射到实际的物理设备,然后直接控制硬件完成。
而VMWare WorkStation采用了不同的方式。VMM实际上运行在一个传统的操作系统之上,这类VMM无法获得对硬件资源的完全控制,因此采用软件模拟的方式来模拟IO设备。Guest OS的IO操作会被VMM捕获,并转发给宿主机(host OS)的一个用户态进程,该进程通过对宿主机操作系统的系统调用来模拟设备的行为。
模拟IO虚拟化方式的最大开销在于处理器模式的切换:包括从Guest OS到VMM的切换,以及从内核态的VMM到用户态的IO模拟进程之间的切换。
2)半虚拟化的IO虚拟化
在半虚拟化的情况下,修改Guest OS的内核,将原生的设备驱动从Guest OS移出,放到一个特殊的设备虚拟机中(对xen来说,就是Dom0了),其余虚拟机中的IO请求都由设备虚拟机处理。而在Guest OS内部,为每个虚拟设备安装一个特殊的驱动程序,由该驱动程序负责IO请求的传递,设备虚拟机经过VMM授权,解析收到的请求并映射到实际物理设备,最后交给设备的原生驱动来完成IO。实际上在这种情况下,Guest OS的驱动是消息代理的作用,把io事件转换为消息,发送给设备虚拟机处理。
第2章 KVM 虚拟化
2.1 kvm技术基础
KVM(kernel-based virtual machine)的名字,基于kernel的虚拟机,已经很准确的说出了kvm的设计思路:也就是依赖linux内核,完全利用linux内核来实现cpu的调度,内存管理的功能。而另一个开源虚拟机xen,则自己开发了一套底层操作系统功能。从vcpu调度到内存管理一应俱全。虽然xen这个系统也是基于linux的,但是发展路线不同,和目前linux内核相比,已经面目全非了。这就是kvm受到开源组织的欢迎,而xen一直被排斥的根源。
虽然说早期的kvm是全虚拟化,而xen是半虚拟化,但发展到今天,xen支持全虚拟化,而kvm也早就有了半虚拟化的patch。技术上可以互相渗透,而软件架构一旦确定了,反而难改。不能因为xen是半虚拟化,就认为linux内核排斥半虚拟化的方案。实际上,另一个进了内核的开源虚拟机Lguest,它就是一个半虚拟化的方案。当然,现在linux内核本身都推出了半虚拟化架构,做半虚拟化也没以前那么繁琐了。
另一个趋势是基于硬件的虚拟化成为主流。早期x86虚拟化的低性能让人印象深刻,所以在intel推出硬件辅助虚拟化之后,虚拟化方案全面向硬件辅助靠拢。而kvm,Lguest这些比较新的方案,则彻底不支持软件的方案,而把硬件辅助当作了设计的根基。
从软件架构上来说,kvm提供了两个内核模块,使用kvm的io_ctl接口可以管理vcpu和内存,为vcpu注入中断和提供时钟信号,而kvm本身没有提供设备的模拟。设备模拟需要应用层软件Qemu来实现。这种架构保证了kvm避免了繁琐的设备模拟和设备驱动部分(内核中80%以上的代码就是驱动部分)。
总结一下kvm软件的架构特点:
q Kvm本身只提供两个内核模块。Kvm实现了vcpu和内存的管理。
q Qemu控制逻辑,负责创建虚拟机,创建vcpu。
2.2 Kvm管理接口
Qemu和kvm关系很深,甚至可以认为双方本来是一个软件,Qemu是应用层的控制部分,而kvm是内核执行部分。软件复用能达到如此天衣无缝的地步,是一件很神奇的事情,也说明kvm设计时候的思路之巧。
所以分析kvm,必须首先从Qemu的代码分析入手。为了避免繁琐,引入太多知识点,而混杂不清。所以把Qemu的代码做简化处理。
代码清单2-1 Qemu启动代码
s->fd = qemu_open("/dev/kvm", O_RDWR);
ret = kvm_ioctl(s, KVM_GET_API_VERSION, 0);
s->vmfd = kvm_ioctl(s, KVM_CREATE_VM, 0);
...............................
ret = kvm_vm_ioctl(s, KVM_CREATE_VCPU, env->cpu_index);
.............................
env->kvm_fd = ret;
run_ret = kvm_vcpu_ioctl(env, KVM_RUN, 0);
可以看到,kvm提供了一个设备/dev/kvm,对kvm的控制要通过这个设备提供的io_ctl接口实现。这是linux内核提供服务的最通用方式,不再赘述。
而kvm提供了三种概念,分别通过不同的io_ctl接口来控制。
q kvm:代表kvm模块本身,用来管理kvm版本信息,创建一个vm。
q vm:代表一个虚拟机。通过vm的io_ctl接口,可以为虚拟机创建vcpu,设置内存区间,创建中断控制芯片,分配中断等等。
q vcpu:代表一个vcpu。通过vcpu的io_ctl接口,可以启动或者暂停vcpu,设置vcpu的寄存器,为vcpu注入中断等等。
Qemu的使用方式,首先是打开/dev/kvm设备,通过KVM_CREATE_VM创建一个虚拟机对象,然后通过KVM_CREATE_VCPU为虚拟机创建vcpu对象,最后通过KVM_RUN设置vcpu运行起来。因为是简化的代码,中断芯片的模拟,内存的模拟,寄存器的设置等等都已经省略了。
2.3 VT技术和vmcs结构
前文讲到kvm是基于硬件辅助虚拟化来实现的。这个硬件辅助的虚拟化技术,在不同的cpu架构中有不同的实现。在x86的平台下,intel实现了VT技术,而另一家x86芯片厂家AMD也推出了自己的虚拟化技术AMD-V。反映到代码上,intel技术的代码都在/arch/x86/kvm目录里面的vmx.c文件,而AMD的实现代码在相同目录的svm.c文件中。
回顾一下虚拟化技术的实现,经典的虚拟化使用了陷入-模拟的模式,而硬件辅助虚拟化引入了根模式(root operation)和非根模式(none-root operation),每种模式都有ring0-3的四级特权级别。所以,在硬件辅助虚拟化中,陷入的概念实际上被VM-EXIT操作取代了,它代表从非根模式退出到根模式,而从根模式切换到非根模式是VM-Entry操作。
2.3.1 需要具备的硬件知识
做系统软件的必须和硬件打交道,这就必须深入cpu架构和设备的架构。但是intel的架构浩大繁杂,说明文档多达上千页,深入了解着实有难度,另外一种趋势是软硬件的分离已经进行了多年,而系统软件的作者多半是软件人员,而非硬件人员。作为软件人员,了解必备的硬件知识是需要的,也是理解代码和架构的基础。同时,在操作系统软件的理解中,分清软件部分的工作和硬件部分的工作是必备条件,这也是操作系统软件中最让人困惑的部分。
对于虚拟化的vt技术而言,它的软件部分基本体现在vmcs结构中(virtual machine control block)。主要通过vmcs结构来控制vcpu的运转。
q Vmcs是个不超过4K的内存块。
q Vmcs通过下列的指令控制,vmclear:清空vmcs结构,vmread:读取vmcs数据,vmwrite:数据写入vmcs
q 通过VMPTR指针指向vmcs结构,该指针包含vmcs的物理地址。
Vmcs包含的信息可以分为六个部分。
q Guest state area:虚拟机状态域,保存非根模式的vcpu运行状态。当VM-Exit发生,vcpu的运行状态要写入这个区域,当VM-Entry发生时,cpu会把这个区域保存的信息加载到自身,从而进入非根模式。这个过程是硬件自动完成的。保存是自动的,加载也是自动的,软件只需要修改这个区域的信息就可以控制cpu的运转。
q Host state area:宿主机状态域,保存根模式下cpu的运行状态。只在vm-exit时需要将状态
q VM-Execution control filelds:包括page fault控制,I/O位图地址,CR3目标控制,异常位图,pin-based运行控制(异步事件),processor-based运行控制(同步事件)。这个域可以设置那些指令触发VM-Exit。触发VM-Exit的指令分为无条件指令和有条件指令,这里设置的是有条件指令。
q VM-entry contorl filelds:包括vm-entry控制,vm-entry MSR控制,VM-Entry插入的事件。MSR是cpu的模式寄存器,设置cpu的工作环境和标识cpu的工作状态。
q VM-exit control filelds:包括VM-Exit控制,VM-Exit MSR控制。
q VM退出信息:这个域保存VM-Exit退出时的信息,并且描述原因。
有了vmcs结构后,对虚拟机的控制就是读写vmcs结构。后面对vcpu设置中断,检查状态实际上都是在读写vmcs结构。在vmx.h文件给出了intel定义的vmcs结构的内容。
2.4 cpu虚拟化
2.4.1 Vcpu数据结构
struct kvm_vcpu {
struct kvm *kvm;
#ifdef CONFIG_PREEMPT_NOTIFIERS
struct preempt_notifier preempt_notifier;
#endif
int vcpu_id;
struct mutex mutex;
int cpu;
struct kvm_run *run;
unsigned long requests;
unsigned long guest_debug;
int fpu_active;
int guest_fpu_loaded;
wait_queue_head_t wq;
int sigset_active;
sigset_t sigset;
struct kvm_vcpu_stat stat;
#ifdef CONFIG_HAS_IOMEM
int mmio_needed;
int mmio_read_completed;
int mmio_is_write;
int mmio_size;
unsigned char mmio_data[8];
gpa_t mmio_phys_addr;
#endif
struct kvm_vcpu_arch arch;
};
这个结构定义了vcpu的通用结构,其中重点是kvm_vcpu_arch,这个是和具体cpu型号有关的信息。
struct kvm_vcpu_arch {
u64 host_tsc;
/*
* rip and regs accesses must go through
* kvm_{register,rip}_{read,write} functions.
*/
unsigned long regs[NR_VCPU_REGS];
u32 regs_avail;
u32 regs_dirty;
unsigned long cr0;
unsigned long cr2;
unsigned long cr3;
unsigned long cr4;
unsigned long cr8;
u32 hflags;
u64 pdptrs[4]; /* pae */
u64 shadow_efer;
u64 apic_base;
struct kvm_lapic *apic; /* kernel irqchip context */
int32_t apic_arb_prio;
int mp_state;
int sipi_vector;
u64 ia32_misc_enable_msr;
bool tpr_access_reporting;
struct kvm_mmu mmu;
/* only needed in kvm_pv_mmu_op() path, but it's hot so
* put it here to avoid allocation */
struct kvm_pv_mmu_op_buffer mmu_op_buffer;
struct kvm_mmu_memory_cache mmu_pte_chain_cache;
struct kvm_mmu_memory_cache mmu_rmap_desc_cache;
struct kvm_mmu_memory_cache mmu_page_cache;
struct kvm_mmu_memory_cache mmu_page_header_cache;
gfn_t last_pt_write_gfn;
int last_pt_write_count;
u64 *last_pte_updated;
gfn_t last_pte_gfn;
struct {
gfn_t gfn; /* presumed gfn during guest pte update */
pfn_t pfn; /* pfn corresponding to that gfn */
unsigned long mmu_seq;
} update_pte;
struct i387_fxsave_struct host_fx_image;
struct i387_fxsave_struct guest_fx_image;
gva_t mmio_fault_cr2;
struct kvm_pio_request pio;
void *pio_data;
u8 event_exit_inst_len;
struct kvm_queued_exception {
bool pending;
bool has_error_code;
u8 nr;
u32 error_code;
} exception;
struct kvm_queued_interrupt {
bool pending;
bool soft;
u8 nr;
} interrupt;
int halt_request; /* real mode on Intel only */
int cpuid_nent;
struct kvm_cpuid_entry2 cpuid_entries[KVM_MAX_CPUID_ENTRIES];
/* emulate context */
struct x86_emulate_ctxt emulate_ctxt;
gpa_t time;
struct pvclock_vcpu_time_info hv_clock;
unsigned int hv_clock_tsc_khz;
unsigned int time_offset;
struct page *time_page;
bool singlestep; /* guest is single stepped by KVM */
bool nmi_pending;
bool nmi_injected;
struct mtrr_state_type mtrr_state;
u32 pat;
int switch_db_regs;
unsigned long db[KVM_NR_DB_REGS];
unsigned long dr6;
unsigned long dr7;
unsigned long eff_db[KVM_NR_DB_REGS];
u64 mcg_cap;
u64 mcg_status;
u64 mcg_ctl;
u64 *mce_banks;
};
q 有寄存器信息,cr0,cr2,cr3等。
q 有内存mmu的信息,
q 有中断控制芯片的信息kvm_lapic
q 有io请求信息kvm_pio_request
q 有vcpu的中断信息interrupt
2.4.2 vcpu创建
首先是Qemu创建VM,从代码分析一下:
代码清单2-2 V
static int kvm_dev_ioctl_create_vm(void)
{
int fd;
struct kvm *kvm;
kvm = kvm_create_vm();
if (IS_ERR(kvm))
return PTR_ERR(kvm);
/*生成kvm-vm控制文件*/
fd = anon_inode_getfd("kvm-vm", &kvm_vm_fops, kvm, 0);
if (fd < 0)
kvm_put_kvm(kvm);
return fd;
}
调用了函数kvm_create_vm,然后是创建一个文件,这个文件作用是提供对vm的io_ctl控制。
代码清单2-3 V
static struct kvm *kvm_create_vm(void)
{
struct kvm *kvm = kvm_arch_create_vm();
/*设置kvm的mm结构为当前进程的mm,然后引用计数加一*/
kvm->mm = current->mm;
atomic_inc(&kvm->mm->mm_count);
spin_lock_init(&kvm->mmu_lock);
spin_lock_init(&kvm->requests_lock);
kvm_io_bus_init(&kvm->pio_bus);
kvm_eventfd_init(kvm);
mutex_init(&kvm->lock);
mutex_init(&kvm->irq_lock);
kvm_io_bus_init(&kvm->mmio_bus);
init_rwsem(&kvm->slots_lock);
atomic_set(&kvm->users_count, 1);
spin_lock(&kvm_lock);
/*把kvm链表加入总链表*/
list_add(&kvm->vm_list, &vm_list);
spin_unlock(&kvm_lock);
return kvm;
}
可以看到,这个函数首先是申请一个kvm结构。然后执行初始化工作。
初始化第一步是把kvm的mm结构设置为当前进程的mm。我们知道,mm结构反应了整个进程的内存使用情况,也包括进程使用的页目录信息。
然后是初始化io bus和eventfd。这两者和设备io有关。
最后把kvm加入到一个全局链表头。通过这个链表头,可以遍历所有的vm虚拟机。
创建VM之后,就是创建VCPU。
代码清单2-4 V
static int kvm_vm_ioctl_create_vcpu(struct kvm *kvm, u32 id)
{
int r;
struct kvm_vcpu *vcpu, *v;
/*调用相关cpu的vcpu_create*/
vcpu = kvm_arch_vcpu_create(kvm, id);
if (IS_ERR(vcpu))
return PTR_ERR(vcpu);
preempt_notifier_init(&vcpu->preempt_notifier, &kvm_preempt_ops);
/*调用相关cpu的vcpu_setup*/
r = kvm_arch_vcpu_setup(vcpu);
if (r)
return r;
/*判断是否达到最大cpu个数*/
mutex_lock(&kvm->lock);
if (atomic_read(&kvm->online_vcpus) == KVM_MAX_VCPUS) {
r = -EINVAL;
goto vcpu_destroy;
}
/*判断该vcpu是否已经存在*/
kvm_for_each_vcpu(r, v, kvm)
if (v->vcpu_id == id) {
r = -EEXIST;
goto vcpu_destroy;
}
/*生成kvm-vcpu控制文件*/
/* Now it's all set up, let userspace reach it */
kvm_get_kvm(kvm);
r = create_vcpu_fd(vcpu);
if (r < 0) {
kvm_put_kvm(kvm);
goto vcpu_destroy;
}
kvm->vcpus[atomic_read(&kvm->online_vcpus)] = vcpu;
smp_wmb();
atomic_inc(&kvm->online_vcpus);
mutex_unlock(&kvm->lock);
return r;
vcpu_destroy:
mutex_unlock(&kvm->lock);
kvm_arch_vcpu_destroy(vcpu);
return r;
}
从代码可见,分别调用相关cpu提供的vcpu_create和vcpu_setup来完成vcpu创建。
Intel的vt技术和amd的svm技术所提供的vcpu调用各自不同。我们集中在intel的vt技术,
而省略AMD的SVM。
代码清单2-5 vmx_create_vcpu
static struct kvm_vcpu *vmx_create_vcpu(struct kvm *kvm, unsigned int id)
{
int err;
/*申请一个vmx结构*/
struct vcpu_vmx *vmx = kmem_cache_zalloc(kvm_vcpu_cache, GFP_KERNEL);
int cpu;
.......................................
err = kvm_vcpu_init(&vmx->vcpu, kvm, id);
/*申请guest的msrs,host的msrs*/
vmx->guest_msrs = kmalloc(PAGE_SIZE, GFP_KERNEL);
vmx->host_msrs = kmalloc(PAGE_SIZE, GFP_KERNEL);
/*申请一个vmcs结构*/
vmx->vmcs = alloc_vmcs();
vmcs_clear(vmx->vmcs);
cpu = get_cpu();
vmx_vcpu_load(&vmx->vcpu, cpu);
/*设置vcpu为实模式,设置各种寄存器*/
err = vmx_vcpu_setup(vmx);
vmx_vcpu_put(&vmx->vcpu);
put_cpu();
if (vm_need_virtualize_apic_accesses(kvm))
if (alloc_apic_access_page(kvm) != 0)
goto free_vmcs;
return &vmx->vcpu;
}
首先申请一个vcpu_vmx结构,然后初始化vcpu_vmx包含的mmu,仿真断芯片等等成员。
MSR寄存器是cpu模式寄存器,所以要分别为guest 和host申请页面,这个页面要保存MSR寄存器的信息。然后申请一个vmcs结构。然后调用vmx_vcpu_setup设置vcpu工作在实模式。
代码清单2-6 vmx_vcpu_setup
static int vmx_vcpu_setup(struct vcpu_vmx *vmx)
{u32 host_sysenter_cs, msr_low, msr_high;
u32 junk;
u64 host_pat, tsc_this, tsc_base;
unsigned long a;
struct descriptor_table dt;
int i;
unsigned long kvm_vmx_return;
u32 exec_control;
/* Control */
vmcs_write32(PIN_BASED_VM_EXEC_CONTROL,
vmcs_config.pin_based_exec_ctrl);
exec_control = vmcs_config.cpu_based_exec_ctrl;
/*如果不支持EPT,有条件退出指令要增加*/
if (!enable_ept)
exec_control |= CPU_BASED_CR3_STORE_EXITING |
CPU_BASED_CR3_LOAD_EXITING |
CPU_BASED_INVLPG_EXITING;
vmcs_write32(CPU_BASED_VM_EXEC_CONTROL, exec_control);
if (cpu_has_secondary_exec_ctrls()) {
exec_control = vmcs_config.cpu_based_2nd_exec_ctrl;
if (!vm_need_virtualize_apic_accesses(vmx->vcpu.kvm))
exec_control &=
~SECONDARY_EXEC_VIRTUALIZE_APIC_ACCESSES;
if (vmx->vpid == 0)
exec_control &= ~SECONDARY_EXEC_ENABLE_VPID;
if (!enable_ept)
exec_control &= ~SECONDARY_EXEC_ENABLE_EPT;
if (!enable_unrestricted_guest)
exec_control &= ~SECONDARY_EXEC_UNRESTRICTED_GUEST;
vmcs_write32(SECONDARY_VM_EXEC_CONTROL, exec_control);
}
vmcs_write32(PAGE_FAULT_ERROR_CODE_MASK, !!bypass_guest_pf);
vmcs_write32(PAGE_FAULT_ERROR_CODE_MATCH, !!bypass_guest_pf);
vmcs_write32(CR3_TARGET_COUNT, 0); /* 22.2.1 */
vmcs_writel(HOST_CR0, read_cr0()); /* 22.2.3 */
vmcs_writel(HOST_CR4, read_cr4()); /* 22.2.3, 22.2.5 */
vmcs_writel(HOST_CR3, read_cr3()); /* 22.2.3 FIXME: shadow tables */
vmcs_write16(HOST_CS_SELECTOR, __KERNEL_CS); /* 22.2.4 */
vmcs_write16(HOST_DS_SELECTOR, __KERNEL_DS); /* 22.2.4 */
vmcs_write16(HOST_ES_SELECTOR, __KERNEL_DS); /* 22.2.4 */
vmcs_write16(HOST_FS_SELECTOR, kvm_read_fs()); /* 22.2.4 */
vmcs_write16(HOST_GS_SELECTOR, kvm_read_gs()); /* 22.2.4 */
vmcs_write16(HOST_SS_SELECTOR, __KERNEL_DS); /* 22.2.4 */
vmcs_writel(HOST_FS_BASE, 0); /* 22.2.4 */
vmcs_writel(HOST_GS_BASE, 0); /* 22.2.4 */
vmcs_write16(HOST_TR_SELECTOR, GDT_ENTRY_TSS*8); /* 22.2.4 */
kvm_get_idt(&dt);
vmcs_writel(HOST_IDTR_BASE, dt.base); /* 22.2.4 */
asm("mov $.Lkvm_vmx_return, %0" : "=r"(kvm_vmx_return));
vmcs_writel(HOST_RIP, kvm_vmx_return); /* 22.2.5 */
vmcs_write32(VM_EXIT_MSR_STORE_COUNT, 0);
vmcs_write32(VM_EXIT_MSR_LOAD_COUNT, 0);
vmcs_write32(VM_ENTRY_MSR_LOAD_COUNT, 0);
rdmsr(MSR_IA32_SYSENTER_CS, host_sysenter_cs, junk);
vmcs_write32(HOST_IA32_SYSENTER_CS, host_sysenter_cs);
rdmsrl(MSR_IA32_SYSENTER_ESP, a);
vmcs_writel(HOST_IA32_SYSENTER_ESP, a); /* 22.2.3 */
rdmsrl(MSR_IA32_SYSENTER_EIP, a);
vmcs_writel(HOST_IA32_SYSENTER_EIP, a); /* 22.2.3 */
if (vmcs_config.vmexit_ctrl & VM_EXIT_LOAD_IA32_PAT) {
rdmsr(MSR_IA32_CR_PAT, msr_low, msr_high);
host_pat = msr_low | ((u64) msr_high << 32);
vmcs_write64(HOST_IA32_PAT, host_pat);
}
if (vmcs_config.vmentry_ctrl & VM_ENTRY_LOAD_IA32_PAT) {
rdmsr(MSR_IA32_CR_PAT, msr_low, msr_high);
host_pat = msr_low | ((u64) msr_high << 32);
/* Write the default value follow host pat */
vmcs_write64(GUEST_IA32_PAT, host_pat);
/* Keep arch.pat sync with GUEST_IA32_PAT */
vmx->vcpu.arch.pat = host_pat;
}
/*保存host的MSR值*/
for (i = 0; i < NR_VMX_MSR; ++i) {
u32 index = vmx_msr_index[i];
u32 data_low, data_high;
u64 data;
int j = vmx->nmsrs;
if (rdmsr_safe(index, &data_low, &data_high) < 0)
continue;
if (wrmsr_safe(index, data_low, data_high) < 0)
continue;
data = data_low | ((u64)data_high << 32);
vmx->host_msrs[j].index = index;
vmx->host_msrs[j].reserved = 0;
vmx->host_msrs[j].data = data;
vmx->guest_msrs[j] = vmx->host_msrs[j];
++vmx->nmsrs;
}
vmcs_write32(VM_EXIT_CONTROLS, vmcs_config.vmexit_ctrl);
/* 22.2.1, 20.8.1 */
vmcs_write32(VM_ENTRY_CONTROLS, vmcs_config.vmentry_ctrl);
vmcs_writel(CR0_GUEST_HOST_MASK, ~0UL);
vmcs_writel(CR4_GUEST_HOST_MASK, KVM_GUEST_CR4_MASK);
tsc_base = vmx->vcpu.kvm->arch.vm_init_tsc;
rdtscll(tsc_this);
if (tsc_this < vmx->vcpu.kvm->arch.vm_init_tsc)
tsc_base = tsc_this;
guest_write_tsc(0, tsc_base);
return 0;
}
这个函数要写一堆的寄存器和控制信息,信息很多。所以只重点分析其中的几个地方:
当cpu不支持EPT扩展技术时候,有条件退出vm的指令要增加。这些指令是cr3 store和cr3 load,要把这个新内容写入cpu_based控制里面。(cpu_based控制是vmcs结构的一部分)。
然后是写cr0,cr3寄存器以及cs,ds以及es等段选择寄存器。
之后,要保存host的MSR寄存器的值到前面分配的guest_msrs页面。
2.4.3 Vcpu运行
推动vcpu运行,让虚拟机开始运行,主要在__vcpu_run函数执行。
代码清单2-7 V
static int __vcpu_run(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run)
{
int r;
..................................
down_read(&vcpu->kvm->slots_lock);
vapic_enter(vcpu);
r = 1;
while (r > 0) {
/*vcpu进入guest模式*/
if (vcpu->arch.mp_state == KVM_MP_STATE_RUNNABLE)
r = vcpu_enter_guest(vcpu, kvm_run);
else {
up_read(&vcpu->kvm->slots_lock);
kvm_vcpu_block(vcpu);
down_read(&vcpu->kvm->slots_lock);
if (test_and_clear_bit(KVM_REQ_UNHALT, &vcpu->requests))
{
switch(vcpu->arch.mp_state) {
case KVM_MP_STATE_HALTED:
vcpu->arch.mp_state =
KVM_MP_STATE_RUNNABLE;
case KVM_MP_STATE_RUNNABLE:
break;
case KVM_MP_STATE_SIPI_RECEIVED:
default:
r = -EINTR;
break;
}
}
}
..............................
clear_bit(KVM_REQ_PENDING_TIMER, &vcpu->requests);
/*检查是否有阻塞的时钟timer*/
if (kvm_cpu_has_pending_timer(vcpu))
kvm_inject_pending_timer_irqs(vcpu);
/*检查是否有用户空间的中断注入*/
if (dm_request_for_irq_injection(vcpu, kvm_run)) {
r = -EINTR;
kvm_run->exit_reason = KVM_EXIT_INTR;
++vcpu->stat.request_irq_exits;
}
/*是否有阻塞的signal*/
if (signal_pending(current)) {
r = -EINTR;
kvm_run->exit_reason = KVM_EXIT_INTR;
++vcpu->stat.signal_exits;
}
/*执行一个调度*/
if (need_resched()) {
up_read(&vcpu->kvm->slots_lock);
kvm_resched(vcpu);
down_read(&vcpu->kvm->slots_lock);
}
}
up_read(&vcpu->kvm->slots_lock);
post_kvm_run_save(vcpu, kvm_run);
vapic_exit(vcpu);
return r;
}
这里理解的关键是vcpu_enter_guest进入了Guest,然后一直是vcpu在运行,当退出这个函数的时候,虚拟机已经执行了VM-Exit指令,也就是说,已经退出了虚拟机,进入根模式了。
退出之后,要检查退出的原因。如果有时钟中断发生,则插入一个时钟中断,如果是用户空间的中断发生,则退出原因要填写为KVM_EXIT_INTR。
注意一点的是,对于导致退出的事件,vcpu_enter_guest函数里面已经处理了一部分,处理的是虚拟机本身运行导致退出的事件。比如虚拟机内部写磁盘导致退出,就在vcpu_enter_guest里面处理(只是写了退出的原因,并没有真正处理)。Kvm是如何知道退出的原因的?这个就是vmcs结构的作用了,vmcs结构里面有VM-Exit的信息。
退出VM之后,如果内核没有完成处理,那么要退出内核到QEMU进程。然后是QEMU进程要处理。后面io处理一节可以看到QEMU的处理过程。
代码清单2-8 vcpu_enter_guest
static int vcpu_enter_guest(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run)
{
int r;
bool req_int_win = !irqchip_in_kernel(vcpu->kvm) &&
kvm_run->request_interrupt_window;
/*装载mmu*/
r = kvm_mmu_reload(vcpu);
kvm_x86_ops->prepare_guest_switch(vcpu);
kvm_load_guest_fpu(vcpu);
/*注入阻塞的事件,中断,异常和nmi等*/
inject_pending_event(vcpu, kvm_run);
if (kvm_lapic_enabled(vcpu)) {
update_cr8_intercept(vcpu);
kvm_lapic_sync_to_vapic(vcpu);
}
/*计算进入guest的时间*/
kvm_guest_enter();
kvm_x86_ops->run(vcpu, kvm_run);
/*
* We must have an instruction between local_irq_enable() and
* kvm_guest_exit(), so the timer interrupt isn't delayed by
* the interrupt shadow. The stat.exits increment will do nicely.
* But we need to prevent reordering, hence this barrier():
*/
/*计算退出的时间*/
kvm_guest_exit();
................................/*退出之前,设置各种参数*/
r = kvm_x86_ops->handle_exit(kvm_run, vcpu);
out:
return r;
}
首先要装载mmu,然后注入事件,像中断,异常什么的。然后调用cpu架构相关的run函数,这个函数里面有一堆汇编写的语句,用来进入虚拟机以及指定从虚拟机退出的执行地址。最后调用cpu的handle_exit,用来从vmcs读取退出的信息。
将注入中断的函数简化一下。
代码清单2-9 V
static void vmx_inject_irq(struct kvm_vcpu *vcpu)
{
int irq = vcpu->arch.interrupt.nr;
..........................
intr = irq | INTR_INFO_VALID_MASK;
...............................
vmcs_write32(VM_ENTRY_INTR_INFO_FIELD, intr);
}
可以看到,实际上注入中断就是写vmcs里面的VM_ENTRY_INTR_INFO_FIELD这个域。然后在cpu的run函数里面设置cpu进入非根模式,vcpu会自动检查vmcs结构,然后注入中断,这是硬件自动完成的工作。而处理中断,就是Guest os内核所完成的工作了。
2.4.4 调度
kvm只是个内核模块,虚拟机实际上是运行在QEMU的进程上下文中。所以vcpu的调度实际上直接使用了linux自身的调度机制。也就是linux自身的进程调度机制。
QEMU可以设置每个vcpu都运作在一个线程中。
代码清单2-10 qemu_kvm_start_vcpu
static void qemu_kvm_start_vcpu(CPUState *env)
{
env->thread = qemu_mallocz(sizeof(QemuThread));
env->halt_cond = qemu_mallocz(sizeof(QemuCond));
qemu_cond_init(env->halt_cond);
qemu_thread_create(env->thread, qemu_kvm_cpu_thread_fn, env);
.................................................
}
从Qemu的代码,看到Qemu启动了一个kvm_cpu_thread线程。这个线程是循环调用
kvm_cpu_exec函数。
代码清单2-11 kvm_cpu_exec
int kvm_cpu_exec(CPUState *env)
{
struct kvm_run *run = env->kvm_run;
int ret, run_ret;
do {
...............................
run_ret = kvm_vcpu_ioctl(env, KVM_RUN, 0);
......................................
/*处理退出的事件*/
switch (run->exit_reason) {
case KVM_EXIT_IO:
DPRINTF("handle_io\n");
kvm_handle_io(run->io.port,
(uint8_t *)run + run->io.data_offset,
run->io.direction,
run->io.size,
run->io.count);
ret = 0;
break;
case KVM_EXIT_MMIO:
DPRINTF("handle_mmio\n");
cpu_physical_memory_rw(run->mmio.phys_addr,
run->mmio.data,
run->mmio.len,
run->mmio.is_write);
ret = 0;
break;
case KVM_EXIT_IRQ_WINDOW_OPEN:
DPRINTF("irq_window_open\n");
ret = EXCP_INTERRUPT;
break;
case KVM_EXIT_SHUTDOWN:
DPRINTF("shutdown\n");
qemu_system_reset_request();
ret = EXCP_INTERRUPT;
break;
case KVM_EXIT_UNKNOWN:
fprintf(stderr, "KVM: unknown exit, hardware reason %" PRIx64 "\n",
(uint64_t)run->hw.hardware_exit_reason);
ret = -1;
break;
case KVM_EXIT_INTERNAL_ERROR:
ret = kvm_handle_internal_error(env, run);
break;
default:
DPRINTF("kvm_arch_handle_exit\n");
ret = kvm_arch_handle_exit(env, run);
break;
}
} while (ret == 0);
..............................
env->exit_request = 0;
cpu_single_env = NULL;
return ret;
}
这个函数就是调用了前面分析过的KVM_RUN。回顾一下前面的分析,KVM_RUN就进入了虚拟机,如果从虚拟化退出到这里,那么Qemu要处理退出的事件。这些事件,可能是因为io引起的KVM_EXIT_IO,也可能是内部错误引起的KVM_EXIT_INTERNAL_ERROR。如果事件没有被完善处理,那么要停止虚拟机。
2.4.5 中断
如何向vcpu注入中断?是通过向VMCS表写入中断数据来实现。
在真实的物理环境,中断是由中断控制芯片来触发的,虚拟化的kvm环境就必须通过软件模拟一个中断控制芯片,这个是通过KVM_CREATE_IRQCHIP来实现的。
然后,如果Qemu想注入一个中断,就通过KVM_IRQ_LINE实现。这个所谓中断控制芯片只是在内存中存在的结构,kvm通过软件方式模拟了中断的机制。
KVM_CREATE_IRQCHIP实际上调用了kvm_create_pic这个函数。
代码清单2-12 kvm_create_pic
struct kvm_pic *kvm_create_pic(struct kvm *kvm)
{
struct kvm_pic *s;
int ret;
s = kzalloc(sizeof(struct kvm_pic), GFP_KERNEL);
if (!s)
return NULL;
spin_lock_init(&s->lock);
s->kvm = kvm;
s->pics[0].elcr_mask = 0xf8;
s->pics[1].elcr_mask = 0xde;
s->irq_request = pic_irq_request;
s->irq_request_opaque = kvm;
s->pics[0].pics_state = s;
s->pics[1].pics_state = s;
/*
* Initialize PIO device
*/
kvm_iodevice_init(&s->dev, &picdev_ops);
ret = kvm_io_bus_register_dev(kvm, &kvm->pio_bus, &s->dev);
if (ret < 0) {
kfree(s);
return NULL;
}
return s;
}
可以看到,这个函数很简单,其实就是申请了一个kvm_pic的结构。然后指定irq_request指针为pic_irq_request。
而KVM_IRQ_LINE实际上调用的是kvm_set_irq,分析一下它是如何注入中断的。
代码清单2-13 kvm_set_irq
int kvm_set_irq(struct kvm *kvm, int irq_source_id, int irq, int level)
{
struct kvm_kernel_irq_routing_entry *e;
unsigned long *irq_state, sig_level;
int ret = -1;
...................................................
/* Not possible to detect if the guest uses the PIC or the
* IOAPIC. So set the bit in both. The guest will ignore
* writes to the unused one.
*/
list_for_each_entry(e, &kvm->irq_routing, link)
if (e->gsi == irq) {
int r = e->set(e, kvm, sig_level);
if (r < 0)
continue;
ret = r + ((ret < 0) ? 0 : ret);
}
return ret;
}
从英文解释可以看到,因为不可能判断Guest使用的是PIC还是APIC,所以为每一个中断路由都设置中断。
这里解释一下,PIC就是传统的中断控制器8259,x86体系最初使用的中断控制器。后来,又推出了APIC,也就是高级中断控制器。APIC为多核架构做了更多设计。
这里的这个set函数,其实就是kvm_pic_set_irq。
代码清单2-14 V
int kvm_pic_set_irq(void *opaque, int irq, int level)
{ struct kvm_pic *s = opaque;
............................
if (irq >= 0 && irq < PIC_NUM_PINS) {
ret = pic_set_irq1(&s->pics[irq >> 3], irq & 7, level);
pic_update_irq(s);
}
............................................
}
可以看到,前面申请的kvm_pic结构作为参数被引入。然后设置irq到这个结构的pic成员。
代码清单2-15 pic_update_irq
static void pic_update_irq(struct kvm_pic *s)
{
int irq2, irq;
irq2 = pic_get_irq(&s->pics[1]);
if (irq2 >= 0) {
/*
* if irq request by slave pic, signal master PIC
*/
pic_set_irq1(&s->pics[0], 2, 1);
pic_set_irq1(&s->pics[0], 2, 0);
}
irq = pic_get_irq(&s->pics[0]);
if (irq >= 0)
s->irq_request(s->irq_request_opaque, 1);
else
s->irq_request(s->irq_request_opaque, 0);
}
此时调用irq_request,就是初始化中断芯片时候绑定的函数pic_irq_request。
代码清单2-16 pic_irq_request
static void pic_irq_request(void *opaque, int level)
{
struct kvm *kvm = opaque;
struct kvm_vcpu *vcpu = kvm->bsp_vcpu;
struct kvm_pic *s = pic_irqchip(kvm);
int irq = pic_get_irq(&s->pics[0]);
/*设置中断*/
s->output = level;
if (vcpu && level && (s->pics[0].isr_ack & (1 << irq))) {
s->pics[0].isr_ack &= ~(1 << irq);
kvm_vcpu_kick(vcpu);
}
}
这个函数很简单,就是设置中断控制芯片的output,然后调用kvm_vcpu_kick。
kvm_vcpu_kick这个地方很容易混淆。
等VM-exit退出后,就接上了前文分析过的部分。Vcpu再次进入虚拟机的时候,通过inject_pengding_event检查中断。这里面就查出来通过KVM_IRQ_LINE注入的中断,然后后面就是写vmcs结构了,已经分析过了。
2.5 vcpu的内存虚拟化
在kmv初始化的时候,要检查是否支持vt里面的EPT扩展技术。如果支持,enable_ept这个变量置为1,然后设置tdp_enabled为1。Tdp就是两维页表的意思,也就是EPT技术。
为陈述方便,给出kvm中下列名字的定义:
q GPA:guest机物理地址
q GVA:guest机虚拟地址
q HVA:host机虚拟地址
q HPA:host机物理地址
2.5.1 虚拟机页表初始化
在vcpu初始化的时候,要调用init_kvm_mmu来设置不同的内存虚拟化方式。
代码清单2-17 init_kvm_mmu
static int init_kvm_mmu(struct kvm_vcpu *vcpu)
{
vcpu->arch.update_pte.pfn = bad_pfn;
if (tdp_enabled)
return init_kvm_tdp_mmu(vcpu);
else
return init_kvm_softmmu(vcpu);
}
设置两种方式,一种是支持EPT的方式,一种是soft mmu,也就是影子页表的方式。
代码清单2-18 V
static int init_kvm_softmmu(struct kvm_vcpu *vcpu)
{
int r;
/*无分页模式的设置*/
if (!is_paging(vcpu))
r = nonpaging_init_context(vcpu);
else if (is_long_mode(vcpu)) /*64位cpu的设置*/
r = paging64_init_context(vcpu);
else if (is_pae(vcpu))/*32位cpu的设置*/
r = paging32E_init_context(vcpu);
else
r = paging32_init_context(vcpu);
vcpu->arch.mmu.base_role.glevels = vcpu->arch.mmu.root_level;
return r;
}
这个函数为多种模式的cpu设置了不同的虚拟化处理函数。选择32位非PAE模式的cpu进行分析。
代码清单2-19 V
static int paging32_init_context(struct kvm_vcpu *vcpu)
{
struct kvm_mmu *context = &vcpu->arch.mmu;
reset_rsvds_bits_mask(vcpu, PT32_ROOT_LEVEL);
context->new_cr3 = paging_new_cr3;
context->page_fault = paging32_page_fault;
context->gva_to_gpa = paging32_gva_to_gpa;
context->free = paging_free;
context->prefetch_page = paging32_prefetch_page;
context->sync_page = paging32_sync_page;
context->invlpg = paging32_invlpg;
context->root_level = PT32_ROOT_LEVEL;
context->shadow_root_level = PT32E_ROOT_LEVEL;
/*页表根地址设为无效*/
context->root_hpa = INVALID_PAGE;
return 0;
}
这个函数要设置一堆函数指针。其中paging32_page_fault等函数直接找是找不到的。这是内核代码经常用的一个技巧(好像别的代码很少见到这种用法)。真正定义在paging_tmpl.h这个文件。通过FNAME这个宏根据不同的cpu平台定义了各自的函数。比如paging32_page_fault实际上就是FNAME(page_fault)这个函数。
我们知道,linux为不同的cpu提供不同的页表层级。64位cpu使用了四级页表。这里指定页表是两级,也就是PT32_ROOT_LEVEL,同时设定页表根地址为无效。此时页表尚未分配。
何时去分配vcpu的页表哪?是在vcpu_enter_guest的开始位置,通过调用kvm_mmu_reload实现。
代码清单2-20 kvm_mmu_reload
static inline int kvm_mmu_reload(struct kvm_vcpu *vcpu)
{ /*页表根地址不是无效的,则退出,不用分配。*/
if (likely(vcpu->arch.mmu.root_hpa != INVALID_PAGE))
return 0;
return kvm_mmu_load(vcpu);
}
首先检查页表根地址是否无效,如果无效,则调用kvm_mmu_load。
代码清单2-21 V
int kvm_mmu_load(struct kvm_vcpu *vcpu)
{
int r;
r = mmu_alloc_roots(vcpu);
/*同步页表*/
mmu_sync_roots(vcpu);
/* set_cr3() should ensure TLB has been flushed */
kvm_x86_ops->set_cr3(vcpu, vcpu->arch.mmu.root_hpa);
....................
}
mmu_alloc_roots这个函数要申请内存,作为根页表使用,同时root_hpa指向根页表的物理地址。然后可以看到,vcpu中cr3寄存器的地址要指向这个根页表的物理地址。
2.5.2 虚拟机物理地址
我们已经分析过,kvm的虚拟机实际上运行在Qemu的进程上下文中。于是,虚拟机的物理内存实际上是Qemu进程的虚拟地址。Kvm要把虚拟机的物理内存分成几个slot。这是因为,对计算机系统来说,物理地址是不连续的,除了bios和显存要编入内存地址,设备的内存也可能映射到内存了,所以内存实际上是分为一段段的。
Qemu通过KVM_SET_USER_MEMORY_REGION来为虚拟机设置内存。
代码清单2-22 kvm_set_memory_region
int __kvm_set_memory_region(struct kvm *kvm,
struct kvm_userspace_memory_region *mem,
int user_alloc)
{
int r;
gfn_t base_gfn;
unsigned long npages;
unsigned long i;
struct kvm_memory_slot *memslot;
struct kvm_memory_slot old, new;
r = -EINVAL;
/*找到现在的memslot*/
memslot = &kvm->memslots[mem->slot];
base_gfn = mem->guest_phys_addr >> PAGE_SHIFT;
npages = mem->memory_size >> PAGE_SHIFT;
new = old = *memslot;
/*new是新的slots,old保持老的数值不变*/
new.base_gfn = base_gfn;
new.npages = npages;
new.flags = mem->flags;
new.user_alloc = user_alloc;
/*用户已经分配了内存,slot的用户空间地址就等于用户分配的地址*/
if (user_alloc)
new.userspace_addr = mem->userspace_addr;
spin_lock(&kvm->mmu_lock);
if (mem->slot >= kvm->nmemslots)
kvm->nmemslots = mem->slot + 1;
*memslot = new;
spin_unlock(&kvm->mmu_lock);
kvm_free_physmem_slot(&old, npages ? &new : NULL);
return 0;
}
这个函数大幅简化了。看代码时候,要注意对内存地址页的检查和内存overlap的检查部分。经过简化之后,代码很清晰了。就是创建一个新的memslot,代替原来的memslot。一个内存slot,最重要部分是指定了vm的物理地址,同时指定了Qemu分配的用户地址,前面一个地址是GPA,后面一个地址是HVA。可见,一个memslot就是建立了GPA到HVA的映射关系。
2.5.3 内存虚拟化过程
这里,有必要描述一下内存虚拟化的过程:
VM要访问GVA 0,那么首先查询VM的页表得到PTE(页表项),通过PTE将GVA 0映射到物理地址GPA 0.
GPA 0此时不存在,发生页缺失。
KVM接管。
从memslot,可以知道GPA对应的其实是HVA x,然后从HVA x,可以查找得到HPA y,然后将HPA y这个映射写入到PTE。
VM再次存取GVA 0,这是从页表项已经可以查到HPA y了,内存可正常访问。
首先,从page_fault处理开始。从前文的分析,知道VM里面的异常产生VM-Exit,然后由各自cpu提供的处理函数处理。对intel的vt技术,就是handle_exception这个函数。
代码清单2-23 V
static int handle_exception(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run)
{
/*读vmcs,获得VM-exit的信息*/
intr_info = vmcs_read32(VM_EXIT_INTR_INFO);
/*发现是page_fault引起*/
if (is_page_fault(intr_info)) {
/* EPT won't cause page fault directly */
/*如果支持EPT,不会因为page_fault退出,所以是bug*/
if (enable_ept)
BUG();
/*读cr2寄存器的值*/
cr2 = vmcs_readl(EXIT_QUALIFICATION);
trace_kvm_page_fault(cr2, error_code);
if (kvm_event_needs_reinjection(vcpu))
kvm_mmu_unprotect_page_virt(vcpu, cr2);
return kvm_mmu_page_fault(vcpu, cr2, error_code);
}
return 0;
}
从这个函数,可以看到对vmcs的使用。通过读vmcs的域,可以获得退出vm的原因。如果是page_fault引起,则调用kvm_mmu_page_fault去处理。
代码清单2-24 kvm_mmu_page_fault
int kvm_mmu_page_fault(struct kvm_vcpu *vcpu, gva_t cr2, u32 error_code)
{
int r;
enum emulation_result er;
/*调用mmu的page_fault*/
r = vcpu->arch.mmu.page_fault(vcpu, cr2, error_code);
if (r < 0)
goto out;
if (!r) {
r = 1;
goto out;
}
/*模拟指令*/
er = emulate_instruction(vcpu, vcpu->run, cr2, error_code, 0);
..................................
}
这里调用了MMU的page_fault处理函数。这个函数就是前面初始化时候设置的paging32_page_fault。也就是通过FNAME宏展开的FNAME(page_fault)。
代码清单2-25 page_fault
static int FNAME(page_fault)(struct kvm_vcpu *vcpu, gva_t addr,
u32 error_code)
{
/*查guest页表,物理地址是否存在 */
r = FNAME(walk_addr)(&walker, vcpu, addr, write_fault, user_fault,
fetch_fault);
/*页还没映射,交Guest OS处理 */
if (!r) {
pgprintk("%s: guest page fault\n", __func__);
inject_page_fault(vcpu, addr, walker.error_code);
vcpu->arch.last_pt_write_count = 0; /* reset fork detector */
return 0;
}
if (walker.level >= PT_DIRECTORY_LEVEL) {
level = min(walker.level, mapping_level(vcpu, walker.gfn));
walker.gfn = walker.gfn & ~(KVM_PAGES_PER_HPAGE(level) - 1);
}
/*通过gfn找pfn*/
pfn = gfn_to_pfn(vcpu->kvm, walker.gfn);
/* mmio ,如果是mmio,是io访问,不是内存,返回*/
if (is_error_pfn(pfn)) {
pgprintk("gfn %lx is mmio\n", walker.gfn);
kvm_release_pfn_clean(pfn);
return 1;
}
/*写入HVA到页表*/
sptep = FNAME(fetch)(vcpu, addr, &walker, user_fault, write_fault,
level, &write_pt, pfn);
.............................
}
对照前面的分析,比较容易理解这个函数了。首先是查guest机的页表,如果从GVA到GPA的映射都没建立,那么返回,让Guest OS做这个工作。
然后,如果映射已经建立,GPA存在,那么从Guest的页面号,查找Host的页面号。如何执行这个查找?从memslot可以知道user space首地址,就可以把物理地址GPA转为HVA,通过HVA就可以查到HPA,然后找到所在页的页号。
最后,写HVA到页表里面。页表在那里?回顾一下前面kvm_mmu_load的过程,页表是host申请的。通过页表搜索,就可以找到要写入的页表项。
2.6 IO虚拟化
IO虚拟化有两种方案,一种是半虚拟化方案,一种是全虚拟化方案。全虚拟化方案不需要该Guest的代码,那么Guest里面的io操作最终都变成io指令。在前面的分析中,其实已经涉及了io虚拟化的流程。在VM-exit的时候,前文分析过page fault导致的退出。那么io指令,同样会导致VM-exit退出,然后kvm会把io交给Qemu进程处理。
而半虚拟化方案,基本都是把io变成了消息处理,从guest机器发消息出来,然后由host机器处理。此时,在guest机器的驱动都被接管,已经不能被称为驱动(因为已经不再处理io指令,不和具体设备打交道),称为消息代理更合适。
2.6.1 Vmm对io的处理
当guest因为执行io执行退出后,由handle_io函数处理。
代码清单2-26 V
static int handle_io(struct kvm_vcpu *vcpu, struct kvm_run *kvm_run)
{
++vcpu->stat.io_exits;
exit_qualification = vmcs_readl(EXIT_QUALIFICATION);
...................................
size = (exit_qualification & 7) + 1;
in = (exit_qualification & 8) != 0;
port = exit_qualification >> 16;
.................................................
return kvm_emulate_pio(vcpu, kvm_run, in, size, port);
}
要从vmcs读退出的信息,然后调用kvm_emulate_pio处理。
代码清单2-27 V
int kvm_emulate_pio(struct kvm_vcpu *vcpu, struct kvm_run *run, int in,
int size, unsigned port)
{
unsigned long val;
/*要赋值退出的种种参数*/
vcpu->run->exit_reason = KVM_EXIT_IO;
vcpu->run->io.direction = in ? KVM_EXIT_IO_IN : KVM_EXIT_IO_OUT;
vcpu->run->io.size = vcpu->arch.pio.size = size;
vcpu->run->io.data_offset = KVM_PIO_PAGE_OFFSET * PAGE_SIZE;
vcpu->run->io.count = vcpu->arch.pio.count = vcpu->arch.pio.cur_count = 1;
vcpu->run->io.port = vcpu->arch.pio.port = port;
vcpu->arch.pio.in = in;
vcpu->arch.pio.string = 0;
vcpu->arch.pio.down = 0;
vcpu->arch.pio.rep = 0;
.................................
/*内核能不能处理?*/
if (!kernel_pio(vcpu, vcpu->arch.pio_data)) {
complete_pio(vcpu);
return 1;
}
return 0;
}
这里要为io处理赋值各种参数,然后看内核能否处理这个io,如果内核能处理,就不用Qemu进程处理,否则退出内核态,返回用户态。从前文的分析中,我们知道返回是到Qemu的线程上下文中。实际上就是kvm_handle_io这个函数里面。
2.6.2 虚拟化io流程
用户态的Qemu如何处理io指令?首先,每种设备都需要注册自己的io指令处理函数到Qemu。
这是通过register_ioport_write和register_ioport_read是实现的。
代码清单2-28 register_ioport_read
int register_ioport_read(pio_addr_t start, int length, int size,
IOPortReadFunc *func, void *opaque)
{
int i, bsize;
/*把处理函数写入ioport_read_table这个全局数据*/
for(i = start; i < start + length; i += size) {
ioport_read_table[bsize][i] = func;
if (ioport_opaque[i] != NULL && ioport_opaque[i] != opaque)
hw_error("register_ioport_read: invalid opaque for address 0x%x",
i);
ioport_opaque[i] = opaque;
}
return 0;
}
通过这个函数,实际上把io指令处理函数登记到一个全局的数组。每种支持的设备都登记在这个数组中。
再分析kvm_handle_io的流程。
代码清单2-29 V
static void kvm_handle_io(uint16_t port, void *data, int direction, int size,
uint32_t count)
{
.............................
for (i = 0; i < count; i++) {
if (direction == KVM_EXIT_IO_IN) {
switch (size) {
case 1:
stb_p(ptr, cpu_inb(port));
break;
}
ptr += size;
}
}
对于退出原因是KVM_EXIT_IO_IN的情况,调用cpu_inb处理。Cpu_inb是个封装函数,它的作用就是调用ioport_read.
代码清单2-30 ioport_read
static uint32_t ioport_read(int index, uint32_t address)
{
static IOPortReadFunc * const default_func[3] = {
default_ioport_readb,
default_ioport_readw,
default_ioport_readl
};
/*从全局数组读入处理函数*/
IOPortReadFunc *func = ioport_read_table[index][address];
if (!func)
func = default_func[index];
return func(ioport_opaque[address], address);
}
这里代码很清晰,就是从登记io指令函数的数组中读出处理函数,然后调用每种设备所登记的指令处理函数处理,完成io。
各种设备都有自己的处理函数,所以Qemu需要支持各种不同的设备,Qemu的复杂性也体现在这里。
第3章 CPU虚拟化
3.1 xen基本机制和提供的服务
为了实现半虚拟化的目标,VMM必须提供一系列的机制。讨论一下这些机制需要实现的功能:
q 计算机系统启动的时候,需要读BIOS获得机器的内存,硬盘参数等物理信息。在虚拟化的情况下,BIOS是不存在的。所以VMM需要模拟这部分的功能。
q VMM运行在保护模式,而Guest OS也运行在保护模式,需要提供保护模式下的信息共享机制。
q VMM运作在最高优先级(0级),而Guest OS运行在低优先级。这意味着虚拟机的内核不能执行某些特权指令,VMM必须提供执行这些特权指令的接口。
q VMM要通知事件到VM,需要机制实现这种事件机制。
q Linux系统进程之间有通信机制。而虚拟机之间也需要一种安全高效的通信机制。
为实现这些功能,xen提供了一系列的机制来完成这些功能。
3.1.1 启动信息页
启动信息页包含了内核启动所需要的信息。启动信息页是一个start_info的数据结构,定义在/xen/include/public/xen.h文件。启动信息页包括了分配给domain的内存页面数,xen store通信页表的机器页号,保存共享信息页的物理地址等等。
3.1.2 共享信息页
启动信息页在domain启动或者恢复时候才发挥作用,而共享信息页在整个系统运行的过程中都发挥作用。共享信息页的结构shared_info同样在/xen/include/public/xen.h文件中定义。
共享信息页主要是与VCPU和虚拟机状态相关的信息,包括VCPU状态信息,时钟信息和虚拟中断状态信息。共享信息页能够被xen和Guest OS访问,因此可以用来在xen和Guest OS之间共享信息。
3.1.3 超级调用
超级调用为Guest OS提供了实现特权指令的机制。在linux系统中,内核提供了系统调用功能,这是通过软中断指令(int 80H)实现。超级调用也是通过软中断实现的, 它使用了0x82这个软中断调用号。
3.1.4 事件通道
事件通道提供了xen和domain之间的事件通知机制。虚拟机的中断也是通过事件通道方式来实现。事件通道在xen使用非常广泛,domain之间通信,虚拟处理器间中断都是通过事件通道来实现的。
3.1.5 授权表
授权表提供了domain间的共享内存机制。和共享内存不同的是,必须经过共享内存所有者domain的授权才有权访问。这也是授权表名称的由来。
3.1.6 Xen store和xen bus
Xen store类似windows里面的注册表。Xen store存储了各个VM的配置信息,前后端设备的信息,虚拟机状态等等。Xen store是一种高级通信机制,它是基于低级通信机制共享页面和事件通道来实现的。Xen store提供了更高级的操作,它提供了一个具有层次结构的目录,类似linux里面的树形目录。通过xen store可以列出目录,读写值,写入值等等。
Xen bus可以看做是一条虚拟的总线。<object data="data:application/x-silverlight-2," 它是对具体物理总线的模拟,在后面章节将详细讨论xenbus。
3.2 虚拟化数据结构
前文讲到VMM通过VCPU来保证VM之间的隔离,同时通过VCPU来调度虚拟机。VMM定义了一些数据结构来完成这些任务,其中最重要的有四个数据结构。
q Vcpu结构:保存vcpu
q Arch_vcpu结构:
q Vcpu_guest_context:
q Vcpu_info:
3.2.1 VCPU数据结构
VCPU结构保存了vcpu的基本信息,同时有成员指针指向arch_vcpu结构。Vcpu的基本信息包括cpu ID,vcpu调度相关信息,vcpu状态信息等。
代码清单2-1 VCPU结构
<object data="data:application/x-silverlight-2," struct vcpu
{
int vcpu_id;
int processor;
vcpu_info_t *vcpu_info;
struct domain *domain;
struct vcpu *next_in_list;
uint64_t periodic_period;
uint64_t periodic_last_event;
struct timer periodic_timer;
struct timer singleshot_timer;
struct timer poll_timer; /* timeout for SCHEDOP_poll */
void *sched_priv; /* scheduler-specific data */
struct vcpu_runstate_info runstate;
/* Has the FPU been initialised? */
bool_t fpu_initialised;
/* Has the FPU been used since it was last saved? */
bool_t fpu_dirtied;
/* Is this VCPU polling any event channels (SCHEDOP_poll)? */
bool_t is_polling;
/* Initialization completed for this VCPU? */
bool_t is_initialised;
/* Currently running on a CPU? */
bool_t is_running;
/* NMI callback pending for this VCPU? */
bool_t nmi_pending;
/* Avoid NMI reentry by allowing NMIs to be masked for short periods. */
bool_t nmi_masked;
/* Require shutdown to be deferred for some asynchronous operation? */
bool_t defer_shutdown;
/* VCPU is paused following shutdown request (d->is_shutting_down)? */
bool_t paused_for_shutdown;
unsigned long pause_flags;
atomic_t pause_count;
u16 virq_to_evtchn[NR_VIRQS];
/* Bitmask of CPUs on which this VCPU may run. */
cpumask_t cpu_affinity;
unsigned long nmi_addr; /* NMI callback address. */
/* Bitmask of CPUs which are holding onto this VCPU's state. */
cpumask_t vcpu_dirty_cpumask;
struct arch_vcpu arch;
};
type="application/x-silverlight-2"
每一个domain可以拥有多个VCPU,这些VCPU通过成员next_in_list组成了一个单向链表。通过这个链表,可以遍历一个domain内的所有VCPU。
而runstate这个成员则是保存VCPU的状态以及在各个状态运行的时间。
代码清单2-2 vcpu运行状态
<object data="data:application/x-silverlight-2," struct vcpu_runstate_info {
/* VCPU's current state (RUNSTATE_*). */
int state;
/* When was current state entered (system time, ns)? */
uint64_t state_entry_time;
/*
* Time spent in each RUNSTATE_* (ns). The sum of these times is
* guaranteed not to drift from system time.
*/
uint64_t time[4];
};type="application/x-silverlight-2"
可以看到,time变量是个四个成员的数组,说明vcpu有四种状态。这四种状态分别是运行态,可运行态,阻塞态和离线态。运行态是vcpu处于运行中,而可运行态说明vcpu已经具备运行的条件,但是还没有分配物理cpu。阻塞态则说明vcpu还需要等待某些资源才能运行。
3.2.2 arch_vcpu
Arch_vcpu结构是跟物理cpu有关的结构,它保存的信息和物理cpu的架构有关。通常包括和vcpu调度有关的函数指针,堆栈信息和上下文切换相关的信息。每种cpu架构都有各自不同的arch_vcpu结构,下文展示x86结构的arch_vcpu结构。
代码清单2-3 Arch_vcpu
<object data="data:application/x-silverlight-2," struct arch_vcpu
{
/* Needs 16-byte aligment for FXSAVE/FXRSTOR. */
struct vcpu_guest_context guest_context
__attribute__((__aligned__(16)));
struct pae_l3_cache pae_l3_cache;
unsigned long flags; /* TF_ */
void (*schedule_tail) (struct vcpu *);
void (*ctxt_switch_from) (struct vcpu *);
void (*ctxt_switch_to) (struct vcpu *);
/* Bounce information for propagating an exception to guest OS. */
struct trap_bounce trap_bounce;
/* I/O-port access bitmap. */
XEN_GUEST_HANDLE(uint8_t) iobmp; /* Guest kernel virtual address of the bitmap. */
int iobmp_limit; /* Number of ports represented in the bitmap. */
int iopl; /* Current IOPL for this VCPU. */
struct desc_struct int80_desc;
/* Virtual Machine Extensions */
struct hvm_vcpu hvm_vcpu;
l1_pgentry_t *perdomain_ptes;
pagetable_t guest_table; /* (MFN) guest notion of cr3 */
/* guest_table holds a ref to the page, and also a type-count unless
* shadow refcounts are in use */
pagetable_t shadow_table[4]; /* (MFN) shadow(s) of guest */
pagetable_t monitor_table; /* (MFN) hypervisor PT (for HVM) */
unsigned long cr3; /* (MA) value to install in HW CR3 */
/* Current LDT details. */
unsigned long shadow_ldt_mapcnt;
struct paging_vcpu paging;
} __cacheline_aligned;type="application/x-silverlight-2"
这里的guest_context变量保存的就是cpu切换时候的寄存器信息和GDT,LDT等描述符信息。
而函数指针ctxt_switch_from 和ctxt_switch_to是要在vcpu切换的时候调用,对于xen的半虚拟化和全虚拟化来说,它们的实现也是各自不同的。
shadow_table则保存了和影子页表有关的信息。
3.2.3 vcpu_guest_context
代码清单2-4 vcpu_guest_context
struct vcpu_guest_context {
/* FPU registers come first so they can be aligned for FXSAVE/FXRSTOR. */
struct { char x[512]; } fpu_ctxt; /* User-level FPU registers */
#define VGCF_I387_VALID (1<<0)
#define VGCF_IN_KERNEL (1<<2)
#define _VGCF_i387_valid 0
#define VGCF_i387_valid (1<<_VGCF_i387_valid)
#define _VGCF_in_kernel 2
#define VGCF_in_kernel (1<<_VGCF_in_kernel)
#define _VGCF_failsafe_disables_events 3
#define VGCF_failsafe_disables_events (1<<_VGCF_failsafe_disables_events)
#define _VGCF_syscall_disables_events 4
#define VGCF_syscall_disables_events (1<<_VGCF_syscall_disables_events)
#define _VGCF_online 5
#define VGCF_online (1<<_VGCF_online)
unsigned long flags; /* VGCF_* flags */
struct cpu_user_regs user_regs; /* User-level CPU registers */
struct trap_info trap_ctxt[256]; /* Virtual IDT */
unsigned long ldt_base, ldt_ents; /* LDT (linear address, # ents) */
unsigned long gdt_frames[16], gdt_ents; /* GDT (machine frames, # ents) */
unsigned long kernel_ss, kernel_sp; /* Virtual TSS (only SS1/SP1) */
/* NB. User pagetable on x86/64 is placed in ctrlreg[1]. */
unsigned long ctrlreg[8]; /* CR0-CR7 (control registers) */
unsigned long debugreg[8]; /* DB0-DB7 (debug registers) */
#ifdef __i386__
unsigned long event_callback_cs; /* CS:EIP of event callback */
unsigned long event_callback_eip;
unsigned long failsafe_callback_cs; /* CS:EIP of failsafe callback */
unsigned long failsafe_callback_eip;
#else
unsigned long event_callback_eip;
unsigned long failsafe_callback_eip;
#ifdef __XEN__
union {
unsigned long syscall_callback_eip;
struct {
unsigned int event_callback_cs; /* compat CS of event cb */
unsigned int failsafe_callback_cs; /* compat CS of failsafe cb */
};
};
#else
unsigned long syscall_callback_eip;
#endif
#endif
unsigned long vm_assist; /* VMASST_TYPE_* bitmap */
#ifdef __x86_64__
/* Segment base addresses. */
uint64_t fs_base;
uint64_t gs_base_kernel;
uint64_t gs_base_user;
#endif
};
struct vcpu_guest_context {
/* FPU registers come first so they can be aligned for FXSAVE/FXRSTOR. */
struct { char x[512]; } fpu_ctxt; /* User-level FPU registers */
#define VGCF_I387_VALID (1<<0)
#define VGCF_IN_KERNEL (1<<2)
#define _VGCF_i387_valid 0
#define VGCF_i387_valid (1<<_VGCF_i387_valid)
#define _VGCF_in_kernel 2
#define VGCF_in_kernel (1<<_VGCF_in_kernel)
#define _VGCF_failsafe_disables_events 3
#define VGCF_failsafe_disables_events (1<<_VGCF_failsafe_disables_events)
#define _VGCF_syscall_disables_events 4
#define VGCF_syscall_disables_events (1<<_VGCF_syscall_disables_events)
#define _VGCF_online 5
#define VGCF_online (1<<_VGCF_online)
unsigned long flags; /* VGCF_* flags */
struct cpu_user_regs user_regs; /* User-level CPU registers */
struct trap_info trap_ctxt[256]; /* Virtual IDT */
unsigned long ldt_base, ldt_ents; /* LDT (linear address, # ents) */
unsigned long gdt_frames[16], gdt_ents; /* GDT (machine frames, # ents) */
unsigned long kernel_ss, kernel_sp; /* Virtual TSS (only SS1/SP1) */
/* NB. User pagetable on x86/64 is placed in ctrlreg[1]. */
unsigned long ctrlreg[8]; /* CR0-CR7 (control registers) */
unsigned long debugreg[8]; /* DB0-DB7 (debug registers) */
#ifdef __i386__
unsigned long event_callback_cs; /* CS:EIP of event callback */
unsigned long event_callback_eip;
unsigned long failsafe_callback_cs; /* CS:EIP of failsafe callback */
unsigned long failsafe_callback_eip;
#else
unsigned long event_callback_eip;
unsigned long failsafe_callback_eip;
#ifdef __XEN__
union {
unsigned long syscall_callback_eip;
struct {
unsigned int event_callback_cs; /* compat CS of event cb */
unsigned int failsafe_callback_cs; /* compat CS of failsafe cb */
};
};
#else
unsigned long syscall_callback_eip;
#endif
#endif
unsigned long vm_assist; /* VMASST_TYPE_* bitmap */
#ifdef __x86_64__
/* Segment base addresses. */
uint64_t fs_base;
uint64_t gs_base_kernel;
uint64_t gs_base_user;
#endif
struct vcpu_guest_context {
/* FPU registers come first so they can be aligned for FXSAVE/FXRSTOR. */
struct { char x[512]; } fpu_ctxt; /* User-level FPU registers */
#define VGCF_I387_VALID (1<<0)
#define VGCF_IN_KERNEL (1<<2)
#define _VGCF_i387_valid 0
#define VGCF_i387_valid (1<<_VGCF_i387_valid)
#define _VGCF_in_kernel 2
#define VGCF_in_kernel (1<<_VGCF_in_kernel)
#define _VGCF_failsafe_disables_events 3
#define VGCF_failsafe_disables_events (1<<_VGCF_failsafe_disables_events)
#define _VGCF_syscall_disables_events 4
#define VGCF_syscall_disables_events (1<<_VGCF_syscall_disables_events)
#define _VGCF_online 5
#define VGCF_online (1<<_VGCF_online)
unsigned long flags; /* VGCF_* flags */
struct cpu_user_regs user_regs; /* User-level CPU registers */
struct trap_info trap_ctxt[256]; /* Virtual IDT */
unsigned long ldt_base, ldt_ents; /* LDT (linear address, # ents) */
unsigned long gdt_frames[16], gdt_ents; /* GDT (machine frames, # ents) */
unsigned long kernel_ss, kernel_sp; /* Virtual TSS (only SS1/SP1) */
/* NB. User pagetable on x86/64 is placed in ctrlreg[1]. */
unsigned long ctrlreg[8]; /* CR0-CR7 (control registers) */
unsigned long debugreg[8]; /* DB0-DB7 (debug registers) */
#ifdef __i386__
unsigned long event_callback_cs; /* CS:EIP of event callback */
unsigned long event_callback_eip;
unsigned long failsafe_callback_cs; /* CS:EIP of failsafe callback */
unsigned long failsafe_callback_eip;
#else
unsigned long event_callback_eip;
unsigned long failsafe_callback_eip;
#ifdef __XEN__
union {
unsigned long syscall_callback_eip;
struct {
unsigned int event_callback_cs; /* compat CS of event cb */
unsigned int failsafe_callback_cs; /* compat CS of failsafe cb */
};
};
#else
unsigned long syscall_callback_eip;
#endif
#endif
unsigned long vm_assist; /* VMASST_TYPE_* bitmap */
#ifdef __x86_64__
/* Segment base addresses. */
uint64_t fs_base;
uint64_t gs_base_kernel;
uint64_t gs_base_user;
#endif
struct vcpu_guest_context {
/* FPU registers come first so they can be aligned for FXSAVE/FXRSTOR. */
struct { char x[512]; } fpu_ctxt; /* User-level FPU registers */
#define VGCF_I387_VALID (1<<0)
#define VGCF_IN_KERNEL (1<<2)
#define _VGCF_i387_valid 0
#define VGCF_i387_valid (1<<_VGCF_i387_valid)
#define _VGCF_in_kernel 2
#define VGCF_in_kernel (1<<_VGCF_in_kernel)
#define _VGCF_failsafe_disables_events 3
#define VGCF_failsafe_disables_events (1<<_VGCF_failsafe_disables_events)
#define _VGCF_syscall_disables_events 4
#define VGCF_syscall_disables_events (1<<_VGCF_syscall_disables_events)
#define _VGCF_online 5
#define VGCF_online (1<<_VGCF_online)
unsigned long flags; /* VGCF_* flags */
struct cpu_user_regs user_regs; /* User-level CPU registers */
struct trap_info trap_ctxt[256]; /* Virtual IDT */
unsigned long ldt_base, ldt_ents; /* LDT (linear address, # ents) */
unsigned long gdt_frames[16], gdt_ents; /* GDT (machine frames, # ents) */
unsigned long kernel_ss, kernel_sp; /* Virtual TSS (only SS1/SP1) */
/* NB. User pagetable on x86/64 is placed in ctrlreg[1]. */
unsigned long ctrlreg[8]; /* CR0-CR7 (control registers) */
unsigned long debugreg[8]; /* DB0-DB7 (debug registers) */
#ifdef __i386__
unsigned long event_callback_cs; /* CS:EIP of event callback */
unsigned long event_callback_eip;
unsigned long failsafe_callback_cs; /* CS:EIP of failsafe callback */
unsigned long failsafe_callback_eip;
#else
unsigned long event_callback_eip;
unsigned long failsafe_callback_eip;
#ifdef __XEN__
union {
unsigned long syscall_callback_eip;
struct {
unsigned int event_callback_cs; /* compat CS of event cb */
unsigned int failsafe_callback_cs; /* compat CS of failsafe cb */
};
};
#else
unsigned long syscall_callback_eip;
#endif
#endif
unsigned long vm_assist; /* VMASST_TYPE_* bitmap */
#ifdef __x86_64__
/* Segment base addresses. */
uint64_t fs_base;
uint64_t gs_base_kernel;
uint64_t gs_base_user;
#endif
struct vcpu_guest_context {
/* FPU registers come first so they can be aligned for FXSAVE/FXRSTOR. */
struct { char x[512]; } fpu_ctxt; /* User-level FPU registers */
#define VGCF_I387_VALID (1<<0)
#define VGCF_IN_KERNEL (1<<2)
#define _VGCF_i387_valid 0
#define VGCF_i387_valid (1<<_VGCF_i387_valid)
#define _VGCF_in_kernel 2
#define VGCF_in_kernel (1<<_VGCF_in_kernel)
#define _VGCF_failsafe_disables_events 3
#define VGCF_failsafe_disables_events (1<<_VGCF_failsafe_disables_events)
#define _VGCF_syscall_disables_events 4
#define VGCF_syscall_disables_events (1<<_VGCF_syscall_disables_events)
#define _VGCF_online 5
#define VGCF_online (1<<_VGCF_online)
unsigned long flags; /* VGCF_* flags */
struct cpu_user_regs user_regs; /* User-level CPU registers */
struct trap_info trap_ctxt[256]; /* Virtual IDT */
unsigned long ldt_base, ldt_ents; /* LDT (linear address, # ents) */
unsigned long gdt_frames[16], gdt_ents; /* GDT (machine frames, # ents) */
unsigned long kernel_ss, kernel_sp; /* Virtual TSS (only SS1/SP1) */
/* NB. User pagetable on x86/64 is placed in ctrlreg[1]. */
unsigned long ctrlreg[8]; /* CR0-CR7 (control registers) */
unsigned long debugreg[8]; /* DB0-DB7 (debug registers) */
#ifdef __i386__
unsigned long event_callback_cs; /* CS:EIP of event callback */
unsigned long event_callback_eip;
unsigned long failsafe_callback_cs; /* CS:EIP of failsafe callback */
unsigned long failsafe_callback_eip;
#else
unsigned long event_callback_eip;
unsigned long failsafe_callback_eip;
#ifdef __XEN__
union {
unsigned long syscall_callback_eip;
struct {
unsigned int event_callback_cs; /* compat CS of event cb */
unsigned int failsafe_callback_cs; /* compat CS of failsafe cb */
};
};
#else
unsigned long syscall_callback_eip;
#endif
#endif
unsigned long vm_assist; /* VMASST_TYPE_* bitmap */
#ifdef __x86_64__
/* Segment base addresses. */
uint64_t fs_base;
uint64_t gs_base_kernel;
uint64_t gs_base_user;
#endif
};
可以看到,这个结构体保存了cr0~cr7寄存器的地址,返回现场的eip指令地址,以及GDT,LDT和TSS的数值。
3.2.4 vcpu_info
代码清单2-5 Vcpu_info
struct vcpu_info {
/*
* 'evtchn_upcall_pending' is written non-zero by Xen to indicate
* a pending notification for a particular VCPU. It is then cleared
* by the guest OS /before/ checking for pending work, thus avoiding
* a set-and-check race. Note that the mask is only accessed by Xen
* on the CPU that is currently hosting the VCPU. This means that the
* pending and mask flags can be updated by the guest without special
* synchronisation (i.e., no need for the x86 LOCK prefix).
* This may seem suboptimal because if the pending flag is set by
* a different CPU then an IPI may be scheduled even when the mask
* is set. However, note:
* 1. The task of 'interrupt holdoff' is covered by the per-event-
* channel mask bits. A 'noisy' event that is continually being
* triggered can be masked at source at this very precise
* granularity.
* 2. The main purpose of the per-VCPU mask is therefore to restrict
* reentrant execution: whether for concurrency control, or to
* prevent unbounded stack usage. Whatever the purpose, we expect
* that the mask will be asserted only for short periods at a time,
* and so the likelihood of a 'spurious' IPI is suitably small.
* The mask is read before making an event upcall to the guest: a
* non-zero mask therefore guarantees that the VCPU will not receive
* an upcall activation. The mask is cleared when the VCPU requests
* to block: this avoids wakeup-waiting races.
*/
uint8_t evtchn_upcall_pending;
uint8_t evtchn_upcall_mask;
unsigned long evtchn_pending_sel;
struct arch_vcpu_info arch;
struct vcpu_time_info time;
}; /* 64 bytes (x86) */
struct vcpu_info {
uint8_t evtchn_upcall_pending;
uint8_t evtchn_upcall_mask;
unsigned long evtchn_pending_sel;
struct arch_vcpu_info arch;
struct vcpu_time_info time;
}; /* 64 bytes (x86) */
Vcpu_info位于共享信息页,因此可以被Guest OS所访问。它包括event_chan的信息和系统时间信息。
3.3 vcpu创建和调度
Vcpu和domain具有密不可分的关系。创建domain的时候,也要同时为domain分配vcpu。
每个vcpu,都要通过调度器来调度。首先分析vcpu的创建和初始化,然后分析调度器如何来调度vcpu。
3.3.1 Vcpu的创建和初始化
在xen的初始化阶段,就要通过init_idle_domain创建一个domain,同时为它分配vcpu。从这里开始分析:
代码清单2-6 init_idle_domain
static void __init init_idle_domain(void)
{
struct domain *idle_domain;
/* Domain creation requires that scheduler structures are initialised. */
scheduler_init();
idle_domain = domain_create(IDLE_DOMAIN_ID, 0, 0);
if ( (idle_domain == NULL) || (alloc_vcpu(idle_domain, 0, 0) == NULL) )
BUG();
set_current(idle_domain->vcpu[0]);
idle_vcpu[0] = this_cpu(curr_vcpu) = current;
setup_idle_pagetable();
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
scheduler_init()是初始化一个缺省的调度器。用来对vcpu进行调度。
然后创建一个 domain结构,通过alloc_vcpu为domain分配一个vcpu。这里创建的domain是一个idle domain。Idle domain和idle进程有点像,都是用来填补物理cpu的空闲,如果cpu找不到合适的进程投入运行,那就运行idle 进程,而在xen里面,如果没合适的vcpu运行,就运行idle domain的idle vcpu。
最后通过setup_idle_pagetable设置空闲页表。这个和内存的虚拟化有关系,在后面分析。
代码清单2-7 Alloc_vcpu
struct vcpu *alloc_vcpu(
struct domain *d, unsigned int vcpu_id, unsigned int cpu_id)
{
struct vcpu *v;
BUG_ON(d->vcpu[vcpu_id] != NULL);
/*分配一个vcpu结构*/
if ( (v = alloc_vcpu_struct()) == NULL )
return NULL;
/*设置vcpu的domain*/
v->domain = d;
v->vcpu_id = vcpu_id;
/*设置状态,如果是idle domain,状态设置为运行态,否则设置为离线态*/
v->runstate.state = is_idle_vcpu(v) ? RUNSTATE_running : RUNSTATE_offline;
/*取当前时间*/
v->runstate.state_entry_time = NOW();
/*为非idle domain,设置共享信息页*/
if ( !is_idle_domain(d) )
{
set_bit(_VPF_down, &v->pause_flags);
v->vcpu_info = shared_info_addr(d, vcpu_info[vcpu_id]);
}
/*初始化vcpu的调度信息*/
if ( sched_init_vcpu(v, cpu_id) != 0 )
{
free_vcpu_struct(v);
return NULL;
}
/*初始化vcpu的结构信息*/
if ( vcpu_initialise(v) != 0 )
{
sched_destroy_vcpu(v);
free_vcpu_struct(v);
return NULL;
}
/*将vcpu加入domain的vcpu链表*/
d->vcpu[vcpu_id] = v;
if ( vcpu_id != 0 )
d->vcpu[v->vcpu_id-1]->next_in_list = v;
/* Must be called after making new vcpu visible to for_each_vcpu(). */
vcpu_check_shutdown(v);
return v;
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
创建vcpu之后,要把vcpu和具体的物理处理器绑定,投入调度。因为是一个idle domain,所以立即投入运行。这个工作是通过sched_init_vcpu来完成的。
代码清单2-8 sched_init_vcpu
int sched_init_vcpu(struct vcpu *v, unsigned int processor)
{
struct domain *d = v->domain;
/*
* Initialize processor and affinity settings. The idler, and potentially
* domain-0 VCPUs, are pinned onto their respective physical CPUs.
*/
/*设置vcpu的处理器*/
v->processor = processor;
if ( is_idle_domain(d) || ((d->domain_id == 0) && opt_dom0_vcpus_pin) )
v->cpu_affinity = cpumask_of_cpu(processor);
else
cpus_setall(v->cpu_affinity);
/* Initialise the per-vcpu timers. */
init_timer(&v->periodic_timer, vcpu_periodic_timer_fn,
v, v->processor);
init_timer(&v->singleshot_timer, vcpu_singleshot_timer_fn,
v, v->processor);
init_timer(&v->poll_timer, poll_timer_fn,
v, v->processor);
/*如英文注解,idle domain立即进入运行*/
/* Idle VCPUs are scheduled immediately. */
if ( is_idle_domain(d) )
{
per_cpu(schedule_data, v->processor).curr = v;
per_cpu(schedule_data, v->processor).idle = v;
v->is_running = 1;
}
TRACE_2D(TRC_SCHED_DOM_ADD, v->domain->domain_id, v->vcpu_id);
/*启动调度器*/
return SCHED_OP(init_vcpu, v);
}
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
periodic_timer和singleshot_timer这个计时器对Guest OS的运行具有重大意义。periodic_timer是周期计时器,它用来触发时间中断,控制Guest OS时间的更新,而singleshot_timer是单次计时器,用来Guest OS完成某些时间相关的任务。这两个计时器在后面将继续分析。
最后是通过SCHED_OP来调用调度器的初始化函数。
3.3.2 调度器和vcpu调度
上文的scheduler_init在设置一个缺省调度器的同时,也设置了一个调度计时器,通过调度计时器来执行vcpu的调度。
代码清单2-9
void __init scheduler_init(void)
{
int i;
/*注册一个软中断*/
open_softirq(SCHEDULE_SOFTIRQ, schedule);
/*为每个cpu,启动一个调度计时器*/
for_each_cpu ( i )
{
spin_lock_init(&per_cpu(schedule_data, i).schedule_lock);
init_timer(&per_cpu(schedule_data, i).s_timer, s_timer_fn, NULL, i);
}
/*设置缺省的调度器*/
for ( i = 0; schedulers[i] != NULL; i++ )
{
ops = *schedulers[i];
if ( strcmp(ops.opt_name, opt_sched) == 0 )
break;
}
if ( schedulers[i] == NULL )
printk("Could not find scheduler: %s\n", opt_sched);
printk("Using scheduler: %s (%s)\n", ops.name, ops.opt_name);
SCHED_OP(init);
}type="application/x-silverlight-2"
首先是注册SCHEDULE_SOFTIRQ软中断。这个软中断的处理函数schedule就是vcpu的调度函数。这个函数要检查当前domain是否能继续运行,如果不能,就要找到一个新的domain,并切换到新domain的上下文。
调度计时器s_timer的作用很简单,就是触发一个SCHEDULE_SOFTIRQ的软中断,从而引起调度动作。
而opt_sched缺省设置为credit调度器,通过这个调度器设置的策略决定那个domain可以运行。
代码清单2-10
static void s_timer_fn(void *unused)
{
raise_softirq(SCHEDULE_SOFTIRQ);
perfc_incr(sched_irq);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
可以看到,调度计时器s_timer的回调函数很简单,就是触发一个软中断,引起系统调度。
调度器scheduler封装了调度算法,它对外提供了一系列的函数指针,调度函数(也就是schedule)通过调度器提供的调度算法实现调度。分析一下调度器的结构定义:
代码清单2-11 虚
struct scheduler {
char *name; /* full name for this scheduler */
char *opt_name; /* option name for this scheduler */
unsigned int sched_id; /* ID for this scheduler */
void (*init) (void);
int (*init_domain) (struct domain *);
void (*destroy_domain) (struct domain *);
int (*init_vcpu) (struct vcpu *);
void (*destroy_vcpu) (struct vcpu *);
void (*sleep) (struct vcpu *);
void (*wake) (struct vcpu *);
struct task_slice (*do_schedule) (s_time_t);
int (*pick_cpu) (struct vcpu *);
int (*adjust) (struct domain *,
struct xen_domctl_scheduler_op *);
void (*dump_settings) (void);
void (*dump_cpu_state) (int);
};<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
这个结构里面,do_schedule是最重要的,它执行真正的调度算法。而sleep函数用来调度vcpu睡眠而wack用来唤醒vcpu。
将schedule函数进行简化来分析调度的过程。
代码清单2-12
static void schedule(void)
{
/* get policy-specific decision on scheduling... */
next_slice = ops.do_schedule(now);
next = next_slice.task;
......................
if ( unlikely(prev == next) )
{
spin_unlock_irq(&sd->schedule_lock);
return continue_running(prev);
}
.....................
vcpu_runstate_change(next, RUNSTATE_running, now);
context_switch(prev, next);
}type="application/x-silverlight-2"
调用调度器的do_schedule函数来找到下一个运行的vcpu,如果下一个vcpu等于当前的vcpu,说明不需要切换,那么调用continue_running继续运行当前的vcpu,否则,要切换next vcpu的状态,然后调用context_switch执行vcpu的切换。
Credit调度器是这个版本xen设置的默认调度器。在credit调度算法中,每个cpu管理一个本地可运行的vcpu队列,该队列根据vcpu的优先级进行排序。每个vcpu的优先级可以用两种状态:over和under。这两种状态表示该vcpu是否已经透支了它应该分配到的cpu资源。状态over表示它占用的cpu资源超过了资源平均值,而under表示低于这个值。Vcpu的运行将消耗它的cpu额度。每隔一段时间,由结算程序重新计算每个vcpu消耗了或者获得了多少额度。当计算额度为负值时,将其优先级改为over,当额度积累为正数时,将优先级改为under。每计算一次,运行队列要重排一次。当一个vcpu被放入运行队列时,将它插入相同优先级队列vcpu的后面。
调度时,调度程序优先服务当前状态为under的vcpu。当运行的vcpu时间片用完或者被阻塞时,排在运行队列头的vcpu将被调度运行。若此时该cpu运行队列中没有优先级为under的vcpu,将从其它cpu的运行队列中寻找一个under的vcpu。这一策略保证了domain能够共享整个物理主机的资源,也保证了所有物理cpu的负载均衡。
3.3.3 控制vcpu的超级调用
用户和Guest OS都需要控制vcpu的运行,比如用户想暂停domain。Xen提供了超级调用__HYPERVISOR_vcpu_op来完成这个工作。
3.4 中断和异常
xen要管理所有的系统资源,所以它要负责接收所有的中断,然后决定那些由VM处理,那些由xen本身处理。因为xen已经处理了物理中断,所以发给VM的中断已经是虚拟化之后的中断,称为虚拟中断。
3.4.1 xen物理中断的处理
中断的设置是__start_xen函数里面通过init_IRQ实现。
代码清单2-13 xen
void __init init_IRQ(void)
{
int i;
init_bsp_APIC();
/*初始化8259芯片*/
init_8259A(0);
/*初始化irq_desc数组*/
for ( i = 0; i < NR_IRQS; i++ )
{
irq_desc[i].status = IRQ_DISABLED;
irq_desc[i].handler = &no_irq_type;
irq_desc[i].action = NULL;
irq_desc[i].depth = 1;
spin_lock_init(&irq_desc[i].lock);
set_intr_gate(i, interrupt[i]);
}
/*设置前16个向量的*/
for ( i = 0; i < 16; i++ )
{
vector_irq[LEGACY_VECTOR(i)] = i;
irq_desc[LEGACY_VECTOR(i)].handler = &i8259A_irq_type;
}
apic_intr_init();
/*初始化8259芯片*/
/* Set the clock to HZ Hz */
#define CLOCK_TICK_RATE 1193180 /* crystal freq (Hz) */
#define LATCH (((CLOCK_TICK_RATE)+(HZ/2))/HZ)
outb_p(0x34, PIT_MODE); /* binary, mode 2, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff, PIT_CH0); /* LSB */
outb(LATCH >> 8, PIT_CH0); /* MSB */
setup_irq(2, &cascade);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
Irq_desx是个256个成员的全局数组,每个成员代表一个中断。set_intr_gate(i, interrupt[i])这句是设置中断的通用处理函数。Interrupt这个变量的定义非常繁琐,其目的是定义256个中断的处理函数为一个通用的处理函数,就是common_interrupt这个处理函数,所有的中断都由这个处理函数来处理。
设置为中断处理函数后,初始化8259芯片,此时起就可以接收中断了。
代码清单2-14 xen
#define BUILD_COMMON_IRQ() \
__asm__( \
"\n" __ALIGN_STR"\n" \
"common_interrupt:\n\t" \
STR(FIXUP_RING0_GUEST_STACK) \
STR(SAVE_ALL(a)) \
"movl %esp,%eax\n\t" \
"pushl %eax\n\t" \
"call " STR(do_IRQ) "\n\t" \
"addl $4,%esp\n\t" \
"jmp ret_from_intr\n");<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
common_interrupt作用是保存未保存的寄存器(有的寄存器是硬件自动保存的),然后调用do_IRQ处理中断,最后调用中断返回函数ret_from_intr。
代码清单2-15 do_IRQ
asmlinkage void do_IRQ(struct cpu_user_regs *regs)
{
unsigned int vector = regs->entry_vector;
irq_desc_t *desc = &irq_desc[vector];
struct irqaction *action;
perfc_incr(irqs);
/*ack,先回应中断*/
spin_lock(&desc->lock);
desc->handler->ack(vector);
/*如果是IRQ_GUEST类型的中断,在这里处理*/
if ( likely(desc->status & IRQ_GUEST) )
{
__do_IRQ_guest(vector);
spin_unlock(&desc->lock);
return;
}
/*设置中断状态PENDING,阻止另一个相同中断进来*/
desc->status &= ~IRQ_REPLAY;
desc->status |= IRQ_PENDING;
/*
* Since we set PENDING, if another processor is handling a different
* instance of this same irq, the other processor will take care of it.
*/
if ( desc->status & (IRQ_DISABLED | IRQ_INPROGRESS) )
goto out;
desc->status |= IRQ_INPROGRESS;
action = desc->action;
while ( desc->status & IRQ_PENDING )
{
desc->status &= ~IRQ_PENDING;
irq_enter();
spin_unlock_irq(&desc->lock);
action->handler(vector_to_irq(vector), action->dev_id, regs);
spin_lock_irq(&desc->lock);
irq_exit();
}
desc->status &= ~IRQ_INPROGRESS;
out:
desc->handler->end(vector);
spin_unlock(&desc->lock);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
Do_IRQ要根据中断的类型进行不同的处理。如果是IRQ_GUEST类型的中断,说明是由Guest OS处理的中断,要送给VM处理。如果是xen处理的中断,那么调用注册进来的handler函数处理。处理时候要设置中断状态,避免同一个中断再次进入。
__do_IRQ_guest函数要将真实的物理中断,转为虚拟中断,然后发送到绑定该中断的虚拟机。
现在问题是,xen自身需要处理那些中断?实际上,xen只处理两个物理中断,一个是时钟中断,一个是串口中断。串口中断在__start_xen的早期设置,而时钟中断通过early_time_init设置。
代码清单2-16 xen
void __init early_time_init(void)
{
/*设置tsc高精度计时器*/
u64 tmp = calibrate_boot_tsc();
set_time_scale(&per_cpu(cpu_time, 0).tsc_scale, tmp);
do_div(tmp, 1000);
cpu_khz = (unsigned long)tmp;
printk("Detected %lu.%03lu MHz processor.\n",
cpu_khz / 1000, cpu_khz % 1000);
/*注册时钟中断*/
setup_irq(0, &irq0);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
early_time_init也在xen的启动函数__start_xen中调用,它把中断处理函数timer_interrupt注册到系统。
3.4.2 虚拟中断的处理
虚拟中断要从两方面分析。一个方面是xen是如何发送虚拟中断的,一个方面是Guest OS是如何注册中断处理函数,然后接收xen发送过来的虚拟中断。
代码清单2-17 __do_IRQ_guest
static void __do_IRQ_guest(int vector)
{
unsigned int irq = vector_to_irq(vector);
irq_desc_t *desc = &irq_desc[vector];
irq_guest_action_t *action = (irq_guest_action_t *)desc->action;
struct domain *d;
int i, sp;
struct pending_eoi *peoi = this_cpu(pending_eoi);
if ( unlikely(action->nr_guests == 0) )
{
/* An interrupt may slip through while freeing an ACKTYPE_EOI irq. */
ASSERT(action->ack_type == ACKTYPE_EOI);
ASSERT(desc->status & IRQ_DISABLED);
desc->handler->end(vector);
return;
}
if ( action->ack_type == ACKTYPE_EOI )
{
sp = pending_eoi_sp(peoi);
ASSERT((sp == 0) || (peoi[sp-1].vector < vector));
ASSERT(sp < (NR_VECTORS-1));
peoi[sp].vector = vector;
peoi[sp].ready = 0;
pending_eoi_sp(peoi) = sp+1;
cpu_set(smp_processor_id(), action->cpu_eoi_map);
}
for ( i = 0; i < action->nr_guests; i++ )
{
d = action->guest[i];
if ( (action->ack_type != ACKTYPE_NONE) &&
!test_and_set_bit(irq, d->pirq_mask) )
action->in_flight++;
send_guest_pirq(d, irq);
}
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
__do_IRQ_guest是把虚拟中断pirq发送给所有注册了该中断的虚拟机。
代码清单2-18 xen
void send_guest_pirq(struct domain *d, int pirq)
{
/*找到中断对应的端口号。端口是事件通道使用的*/
int port = d->pirq_to_evtchn[pirq];
struct evtchn *chn;
ASSERT(port != 0);
chn = evtchn_from_port(d, port);
/*设置vcpu的pending位*/
evtchn_set_pending(d->vcpu[chn->notify_vcpu_id], port);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
虚拟中断pirq是通过前文介绍的事件通道实现的。Xen为domian中所有的中断都分配了一个事件通道号,它们的对应关系保存在pirq_to_evtchn中。
在xen通过send_guest_pirq向Guest OS发送事件通知后,Guest OS会调用函数evtcha_do_upcall处理。
代码清单2-19 evtchn_do_upcall
asmlinkage void evtchn_do_upcall(struct pt_regs *regs)
{
unsigned long l1, l2;
unsigned int l1i, l2i, port, count;
int irq, cpu = smp_processor_id();
/*取共享信息页。*/
shared_info_t *s = HYPERVISOR_shared_info;
vcpu_info_t *vcpu_info = &s->vcpu_info[cpu];
do {
/* Avoid a callback storm when we reenable delivery. */
vcpu_info->evtchn_upcall_pending = 0;
/* Nested invocations bail immediately. */
if (unlikely(per_cpu(upcall_count, cpu)++))
return;
#ifndef CONFIG_X86 /* No need for a barrier -- XCHG is a barrier on x86. */
/* Clear master flag /before/ clearing selector flag. */
rmb();
#endif
l1 = xchg(&vcpu_info->evtchn_pending_sel, 0);
while (l1 != 0) {
l1i = __ffs(l1);
l1 &= ~(1UL << l1i);
while ((l2 = active_evtchns(cpu, s, l1i)) != 0) {
l2i = __ffs(l2);
port = (l1i * BITS_PER_LONG) + l2i;
if ((irq = evtchn_to_irq[port]) != -1)
do_IRQ(irq, regs);
else {
exit_idle();
evtchn_device_upcall(port);
}
}
}
/* If there were nested callbacks then we have more to do. */
count = per_cpu(upcall_count, cpu);
per_cpu(upcall_count, cpu) = 0;
} while (unlikely(count != 1));
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
evtcha_do_upcall最终是调用do_irq来处理中断。Linux内核标准的中断处理和xen内部的中断处理类似,都是调用common_interrupt来做通用的中断处理,而VM里面由于物理中断被xen接管了,实际上VM处理的是事件通道模拟的虚拟中断。
3.5 时间
时间是整个计算机系统运行的重要概念。VM和 xen的调度,都需要依赖时间来执行。
3.5.1 时间初始化
在xen的启动期间,通过两个重要函数来初始化时间功能。一个是early_time_init,另一个是init_xen_time。early_time_init在中断一节分析过,它的作用是注册中断的处理函数timer_interrupt。
代码清单2-20 xen
void timer_interrupt(int irq, void *dev_id, struct cpu_user_regs *regs)
{
ASSERT(local_irq_is_enabled());
/* Update jiffies counter. */
(*(volatile unsigned long *)&jiffies)++;
/* Rough hack to allow accurate timers to sort-of-work with no APIC. */
if ( !cpu_has_apic )
raise_softirq(TIMER_SOFTIRQ);
if ( using_pit )
pit_overflow();
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
在时钟中断里面,每来一个时钟中断,都把jiffies变量加一,这样jeffies变量表示从开机以来的时间值。
代码清单2-21 xen
/* Late init function (after all CPUs are booted). */
int __init init_xen_time(void)
{ /*获得cmos的时间*/
wc_sec = get_cmos_time();
local_irq_disable();
/*初始化一个cpu 计时器*/
init_percpu_time();
stime_platform_stamp = 0;
init_platform_timer();
local_irq_enable();
return 0;
}type="application/x-silverlight-2"
init_xen_time首先要获得cmos的当前系统时间,保存在wc_sec全局变量。然后要为cpu设置一个计时器。这个计时器作用每经过一个时间段EPOCH(定义为1000ms),就刷新系统时间。把这个计时器的处理函数进行简化处理。
代码清单2-22 xen
static void local_time_calibration(void *unused)
{
............................................
/* Record new timestamp information. */
t->tsc_scale.mul_frac = calibration_mul_frac;
t->tsc_scale.shift = tsc_shift;
t->local_tsc_stamp = curr_tsc;
t->stime_local_stamp = curr_local_stime;
t->stime_master_stamp = curr_master_stime;
update_vcpu_system_time(current);
out:
set_timer(&t->calibration_timer, NOW() + EPOCH);
if ( smp_processor_id() == 0 )
platform_time_calibration();
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
获得当前的时间后,通过update_vcpu_system_time来更新vcpu的系统时间。
每个domain都有自己的共享信息页,共享信息页包含了domain启动时候的初始系统时间。
这个初始系统时间是在domain 创建时候,通过update_domain_wallclock_time函数来设置。
3.5.2 虚拟机时间
总结一下前文,虚拟机创建时候会初始化一个初始系统时间,保存在共享信息页里面。而定时器会不断刷新vcpu结构里面的vcpu_time_info信息。
虚拟机通过调用do_settimeofday来更新系统时间。
代码清单2-23 xen
int do_settimeofday(struct timespec *tv)
{
time_t sec;
s64 nsec;
unsigned int cpu;
struct shadow_time_info *shadow;
struct xen_platform_op op;
if ((unsigned long)tv->tv_nsec >= NSEC_PER_SEC)
return -EINVAL;
cpu = get_cpu();
shadow = &per_cpu(shadow_time, cpu);
write_seqlock_irq(&xtime_lock);
/*
* Ensure we don't get blocked for a long time so that our time delta
* overflows. If that were to happen then our shadow time values would
* be stale, so we can retry with fresh ones.
*/
for (;;) {
nsec = tv->tv_nsec - get_nsec_offset(shadow);
/*更新系统时间*/
if (time_values_up_to_date(cpu))
break;
get_time_values_from_xen(cpu);
}
sec = tv->tv_sec;
__normalize_time(&sec, &nsec);
/*判断是否dom0,只有dom0可以更新时间*/
if (is_initial_xendomain() && !independent_wallclock) {
op.cmd = XENPF_settime;
op.u.settime.secs = sec;
op.u.settime.nsecs = nsec;
op.u.settime.system_time = shadow->system_timestamp;
HYPERVISOR_platform_op(&op);
update_wallclock();
} else if (independent_wallclock) {
nsec -= shadow->system_timestamp;
__normalize_time(&sec, &nsec);
__update_wallclock(sec, nsec);
}
write_sequnlock_irq(&xtime_lock);
put_cpu();
clock_was_set();
return 0;
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
可以看到,函数调用了超级调用HYPERVISOR_platform_op来设置时间。这个超级调用实际上是设置了全局变量wc_sec的值,也就是系统启动时间的值,然后要为每一个domain都刷新它的系统启动时间。
3.5.3 虚拟机的时钟中断
在vcpu的创建时,就创建了一个周期计时器,这个计时器每10ms发送一个虚拟时钟中断VIRQ_TIMER给虚拟机。这个虚拟时钟中断同样是通过事件通道的方式发送到Guest OS。
代码清单2-24 xen
static void vcpu_periodic_timer_fn(void *data)
{
struct vcpu *v = data;
vcpu_periodic_timer_work(v);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
代码清单2-25 xen
static void vcpu_periodic_timer_work(struct vcpu *v)
{
s_time_t now = NOW();
uint64_t periodic_next_event;
ASSERT(!active_timer(&v->periodic_timer));
if ( v->periodic_period == 0 )
return;
/*判断当前时间是否已经超过了预设的定时器时间*/
periodic_next_event = v->periodic_last_event + v->periodic_period;
if ( now > periodic_next_event )
{
/*送timer事件到虚拟机*/
send_timer_event(v);
v->periodic_last_event = now;
periodic_next_event = now + v->periodic_period;
}
/*再次启动定时器*/
v->periodic_timer.cpu = smp_processor_id();
set_timer(&v->periodic_timer, periodic_next_event);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
代码清单2-26 xen
void send_timer_event(struct vcpu *v)
{
/*送VIRQ_TIMER到Guest os*/
send_guest_vcpu_virq(v, VIRQ_TIMER);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
第4章 io设备虚拟化
Io设备的虚拟化,不可避免要涉及到设备的概念和隐藏在设备背后的总线。理解了这两个概念,就比较容易理解io设备的虚拟化。
4.1 设备,总线和驱动
设备是计算机系统中一个重要概念。通常的显卡网卡声卡等设备,都是先插入计算机系统的PCI总线插槽(早期还有ISA,MCA总线等。现在PC领域基本PCI总线统一了),然后安装驱动,之后应用程序可以通过文件系统打开和读写设备文件。这个过程可以从三个层面理解:设备本身的特性,总线和操作系统对设备的处理,驱动层次。
以PCI设备为例,一个PCI设备,本身就包含一个配置表。配置表包含设备制造商填充的厂商信息,设备属性等等的通用配置信息。此外,设备厂商还应该提供设备的控制寄存器信息,通过这些控制寄存器,系统可以设置设备的状态,控制设备的运行,或者从设备获得状态信息。另外设备还可能配备了内存(也有的设备可能没有),系统可以读写设备的内存。用图3-1来解释设备的基本信息:
图3-1 设备配置表的信息
如上图,设备本身有一些配置信息。配置信息里面的设备内存基址,指示了设备内存的地址和长度,而设备寄存器基址,则指向了设备的寄存器地址和长度。该设备有两个寄存器,一个输入寄存器,一个输出寄存器。当输入寄存器写入数值后,可以从输出寄存器读到另一个数值。
设备寄存器基址,这个概念有点难理解。实际上,可以看做是一个地址,对这个地址写指令,就可以控制设备。所以,设备寄存器其实就是设备的控制接口。这个接口必须要映射到计算机系统的io空间,这样内核就可以访问设备了。
4.1.1 io端口和io内存
不同的处理器对io访问有不同的处理方式。对X86系统来说,专门提供了特别的指令来访问设备寄存器。所有这些设备寄存器占据了65536个8位的空间。这个空间称为计算机的IO端口空间。
对上文的例子设备来说,需要把设备的寄存器基址纳入到系统的IO端口空间里面,然后驱动就可以通过系统提供的特别指令来访问设备的寄存器。假设设备厂商提供的寄存器基址是0x1c00,长度是8个字节。那么有两种情况,一种是这个0x1c00地址和别的设备没有冲突,可以直接使用,那么操作系统内核就记录设备的寄存器基址为0x1c00,驱动通过X86系统提供的io指令访问0x1c00 io地址,或者叫0x1c00 io端口,就可以设置设备输入寄存器的内容。访问地址0x1c04,就可以读到设备输出寄存器的内容。
另外一种情况是其它设备也使用了0x1c00这个io地址。那么操作系统内核就需要寻找一个合适的寄存器基址,然后更新设备的寄存器基址,并记录到内核的设备信息里面。驱动使用x86的io指令,访问这个更新的地址,就可以设置设备输入寄存器的内容了。
通过设备的io端口访问设备寄存器来控制设备,这就是设备驱动的功能。设备厂商会提供设备寄存器的详细内容,这也是驱动开发者所必须关注的。而发现设备,扫描设备信息,为设备提供合适的io地址空间,这是内核的总线部分要处理的事情。后文将继续分析。
设备的内存处理过程差不多一样。内核同样要读取设备内存基址,然后找到合适的内存空间,把设备的内存映射到内存空间。然后驱动就可以标准的内存接口访问设备的内存了。
4.1.2 总线
设备的配置信息提供了设备寄存器基址和设备内存基址。因此首先要读到这两个寄存器的内容,也就是设备寄存器基址和长度以及设备内存基址和长度,然后操作系统才能安排合适的io端口和io内存。
但是如何去读设备的配置信息?X86系统使用的PCI总线对这个问题的解决方法是:保留了8个字节的io端口地址,就是0xCF8~0xCFF。要访问设备的某个配置信息,先往0xCF8地址写入目标地址信息,然后通过0xCFC地址读数据,就可以获得这个配置信息。这里的写和读,都使用的是x86所特有的io指令。
写入0xCF8的目标地址信息,是一种包括了总线号,设备号,功能号和配置寄存器地址的综合信息。
1) 总线对设备的扫描和管理
计算机系统的设备是如何被发现的?对PCI总线上的设备来说,这是总线扫描所得到的结果(非PCI总线各有各自的扫描方式)。每个PCI设备有一个总线号,一个设备号,一个功能号标识。Pci规范允许一个系统最多拥有256条总线,每条总线最多可以带32个设备,每个设备可以是最多8个功能的多功能板。Pci扫描就是对单条总线的地址范围进行扫描。根据前面关于pci配置信息的知识,如果某个地址存在设备,那么在0xCF8写入目标地址信息,就可以从0xCFC读到设备的信息。包括设备的io端口和io内存,设备的中断号和DMA信息。对每个读到的pci设备,都要为它创建设备对象。
2) 总线对驱动和设备的管理
当设备插入计算机系统时,可以找到自己的驱动开始工作。而升级了设备驱动,也能找到适合的设备,自动产生设备。这些功能其实就是pci总线结构完成的。通过代码分析来了解一下:
代码清单3-1 pci_register_driver
int __pci_register_driver(struct pci_driver *drv, struct module *owner)
{
int error;
/*设置驱动的总线类型和参数*/
/* initialize common driver fields */
drv->driver.name = drv->name;
drv->driver.bus = &pci_bus_type;
drv->driver.owner = owner;
drv->driver.kobj.ktype = &pci_driver_kobj_type;
spin_lock_init(&drv->dynids.lock);
INIT_LIST_HEAD(&drv->dynids.list);
/* register with core */
error = driver_register(&drv->driver);
if (!error)
error = pci_create_newid_file(drv);
return error;
} <object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
注册一个pci驱动,其实就是调用了__pci_register_driver函数。首先设置了驱动的总线类型为pci,然后调用driver_register登记。
代码清单3-2 driver_register
int driver_register(struct device_driver * drv)
{
klist_init(&drv->klist_devices, klist_devices_get, klist_devices_put);
init_completion(&drv->unloaded);
return bus_add_driver(drv);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
实际是调用了bus_add_driver。bus_add_driver中真正起作用的是driver_attach这个函数。
代码清单3-3 设
void driver_attach(struct device_driver * drv)
{
bus_for_each_dev(drv->bus, NULL, drv, __driver_attach);
} <object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
这里就清楚了,driver_attach实际上把总线上面的所有设备都遍历了一遍,通过__driver_attach函数判断驱动和设备是否匹配。而__driver_attach实际是调用了driver_probe_device来检查设备和驱动的匹配关系。
代码清单3-4 设
int driver_probe_device(struct device_driver * drv, struct device * dev)
{
int ret = 0;
/*总线定义了match函数,通过match函数判断是否匹配*/
if (drv->bus->match && !drv->bus->match(dev, drv))
goto Done;
pr_debug("%s: Matched Device %s with Driver %s\n",
drv->bus->name, dev->bus_id, drv->name);
dev->driver = drv;
/*匹配,调用probe函数*/
if (dev->bus->probe) {
ret = dev->bus->probe(dev);
if (ret) {
dev->driver = NULL;
goto ProbeFailed;
}
} else if (drv->probe) {
ret = drv->probe(dev);
if (ret) {
dev->driver = NULL;
goto ProbeFailed;
}
}
device_bind_driver(dev);
ret = 1;
pr_debug("%s: Bound Device %s to Driver %s\n",
drv->bus->name, dev->bus_id, drv->name);
goto Done;
Done:
return ret;
} <object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
判断驱动和设备是否匹配,首先是通过注册的match函数判断。通常情况,匹配是通过驱动里面包含的id表和扫描设备发现的id比较,如果相同,则说明驱动和设备是适合的。当匹配通过后,调用驱动里面的probe函数。这个函数往往用来继续对设备做进一步的检测工作。在后面块设备的驱动可以看到具体的用法。
4.1.3 设备中断
Cpu虚拟化一节已经讨论了设备中断的处理。
4.2 虚拟化的设备驱动和总线
半虚拟化结构的xen提出了虚拟设备的架构。Xen的虚拟设备架构采用前后端分离的设备驱动结构。虚拟设备驱动包含两个部分:domU中的前段设备驱动(fronted)和dom0中的后端设备驱动。后端设备驱动可以访问真实的硬件设备。
前端设备驱动从Guest OS接收io请求,然后将io请求转发到后端,而后端接收到前端转发的设备请求后,检查请求是否合法,然后通过本地的设备驱动访问真实的硬件设备。Io完成后,后端设备驱动通知前端设备驱动已经准备就绪,然后前端驱动向Guest OS报告io操作完成。
在Xen的半虚拟化架构中,同样需要一种机制来发现设备,连接设备和驱动,自动匹配设备和驱动。而且和linux不同的是,因为前端设备和后端设备是联动的关系,当某一方设备变动的时候,还必须通知另一方设备的变动情况。为了完成这个工作,xen提供了一条虚拟总线xenbus来管理所有的虚拟设备和驱动。Xen系统的所有虚拟设备都要注册到xenbus。Pci总线也是作为一个设备注册到xenbus ,通过注册的pci总线,执行扫描动作可以产生所有的pci设备。而所有的虚拟驱动也都要注册到xenbus,从而可以自动完成虚拟设备和驱动的匹配。
4.2.1 Pci前端注册和扫描
代码清单3-5 pcifront_init
static int __init pcifront_init(void)
{
if (!is_running_on_xen())
return -ENODEV;
return xenbus_register_frontend(&xenbus_pcifront_driver);
}<object data="data:application/x-silverlight-2,"
Pcifront作为一个前端驱动注册到xenbus。注册驱动后,会自动调用驱动的probe函数,完成初始化后,把设备状态改为初始完成状态。通过xenbus,pci后端检测到前端变化,将信息同步后,将状态改为connected。前端此时要调用pcifront_try_connect完成扫描。
代码清单3-6 设
static int pcifront_try_connect(struct pcifront_device *pdev)
{
int err = -EFAULT;
int i, num_roots, len;
char str[64];
unsigned int domain, bus;
spin_lock(&pdev->dev_lock);
/* Only connect once */
if (xenbus_read_driver_state(pdev->xdev->nodename) !=
XenbusStateInitialised)
goto out;
/*检查状态,保证只连接一次*/
err = pcifront_connect(pdev);
if (err) {
xenbus_dev_fatal(pdev->xdev, err,
"Error connecting PCI Frontend");
goto out;
}
err = xenbus_scanf(XBT_NIL, pdev->xdev->otherend,
"root_num", "%d", &num_roots);
if (err == -ENOENT) {
xenbus_dev_error(pdev->xdev, err,
"No PCI Roots found, trying 0000:00");
err = pcifront_scan_root(pdev, 0, 0);
num_roots = 0;
} else if (err != 1) {
if (err == 0)
err = -EINVAL;
xenbus_dev_fatal(pdev->xdev, err,
"Error reading number of PCI roots");
goto out;
}
for (i = 0; i < num_roots; i++) {
len = snprintf(str, sizeof(str), "root-%d", i);
if (unlikely(len >= (sizeof(str) - 1))) {
err = -ENOMEM;
goto out;
}
/*获得pci的域号和总线号*/
err = xenbus_scanf(XBT_NIL, pdev->xdev->otherend, str,
"%x:%x", &domain, &bus);
if (err != 2) {
if (err >= 0)
err = -EINVAL;
xenbus_dev_fatal(pdev->xdev, err,
"Error reading PCI root %d", i);
goto out;
}
/*扫描pci总线*/
err = pcifront_scan_root(pdev, domain, bus);
if (err) {
xenbus_dev_fatal(pdev->xdev, err,
"Error scanning PCI root %04x:%02x",
domain, bus);
goto out;
}
}
err = xenbus_switch_state(pdev->xdev, XenbusStateConnected);
if (err)
goto out;
out:
spin_unlock(&pdev->dev_lock);
return err;
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
获得后端设置的pci域号和pci总线号后,调用pcifront_scan_root来扫描设备。
<object data="data:application/x-silverlight-2,"
代码清单3-7 pcifront_scan_root
int pcifront_scan_root(struct pcifront_device *pdev,
unsigned int domain, unsigned int bus)
{
struct pci_bus *b;
struct pcifront_sd *sd = NULL;
struct pci_bus_entry *bus_entry = NULL;
int err = 0;
bus_entry = kmalloc(sizeof(*bus_entry), GFP_KERNEL);
sd = kmalloc(sizeof(*sd), GFP_KERNEL);
if (!bus_entry || !sd) {
err = -ENOMEM;
goto err_out;
}
pcifront_init_sd(sd, domain, pdev);
b = pci_scan_bus_parented(&pdev->xdev->dev, bus,
&pcifront_bus_ops, sd);
if (!b) {
dev_err(&pdev->xdev->dev,
"Error creating PCI Frontend Bus!\n");
err = -ENOMEM;
goto err_out;
}
bus_entry->bus = b;
list_add(&bus_entry->list, &pdev->root_buses);
/* Claim resources before going "live" with our devices */
pci_walk_bus(b, pcifront_claim_resource, pdev);
pci_bus_add_devices(b);
return 0;
err_out:
kfree(bus_entry);
kfree(sd);
return err;
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
pci_scan_bus_parented就是对pci总线的扫描,扫描之后,产生的pci设备就连接到总线的链表头,从而通过总线可以遍历pci设备。而pci_walk_bus要把设备的资源(io端口,io内存)全都注册到系统内。
4.3 虚拟化的块设备
硬盘等块设备采用了前后端的设备驱动架构。对原生的驱动比较,采用半虚拟化之后的驱动分成了两部分。在domU里面运行的是前端驱动,而dom0里面运行后端驱动。
后端驱动和前端一一对应。当前端驱动状态改变的时候,就会触发事件,通知后端驱动。当后端驱动完成事务处理后,通过改变状态,就可以触发事件,通知前端驱动。所以前端驱动和后端驱动是互相呼应,共同完成io请求。
4.3.1 块设备的前端驱动
前端驱动定义了一个xenbus的驱动类型。如清单所示:
代码清单3-8 blkfront
static struct xenbus_driver blkfront = {
.name = "vbd",
.owner = THIS_MODULE,
.ids = blkfront_ids,
.probe = blkfront_probe,
.remove = blkfront_remove,
.resume = blkfront_resume,
.otherend_changed = backend_changed,
}
根据前面总线的分析,这个驱动要注册到xenbus总线。如果和xenbus扫描出来的设备能匹配,那么要调用驱动提供的probe函数,做进一步的初始化。
代码清单3-9 虚拟驱动例子
static int blkfront_probe(struct xenbus_device *dev,
const struct xenbus_device_id *id)
{
int err, vdevice, i;
struct blkfront_info *info;
/* FIXME: Use dynamic device id if this is not set. */
err = xenbus_scanf(XBT_NIL, dev->nodename,
"virtual-device", "%i", &vdevice);
if (err != 1) {
xenbus_dev_fatal(dev, err, "reading virtual-device");
return err;
}
info = kzalloc(sizeof(*info), GFP_KERNEL);
if (!info) {
xenbus_dev_fatal(dev, -ENOMEM, "allocating info structure");
return -ENOMEM;
}
info->xbdev = dev;
info->vdevice = vdevice;
info->connected = BLKIF_STATE_DISCONNECTED;
INIT_WORK(&info->work, blkif_restart_queue, (void *)info);
for (i = 0; i < BLK_RING_SIZE; i++)
info->shadow[i].req.id = i+1;
info->shadow[BLK_RING_SIZE-1].req.id = 0x0fffffff;
/* Front end dir is a number, which is used as the id. */
info->handle = simple_strtoul(strrchr(dev->nodename,'/')+1, NULL, 0);
dev->dev.driver_data = info;
/*连接后端*/
err = talk_to_backend(dev, info);
if (err) {
kfree(info);
dev->dev.driver_data = NULL;
return err;
}
return 0;
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
这个函数主要是申请一个blkfront_info类型的数据结构。这个数据结构是个容器,既包括块设备有关的请求队列和gendisk数据结构,也包括和虚拟化有关的ring结构,和grantbable结构。
初始化之后,调用talk_to_backend来和后端设备连接。
代码清单3-10 虚拟驱动例子
static int talk_to_backend(struct xenbus_device *dev,
struct blkfront_info *info)
{
const char *message = NULL;
struct xenbus_transaction xbt;
int err;
/*ring是前端和后端通讯的一种方式*/
/* Create shared ring, alloc event channel. */
err = setup_blkring(dev, info);
if (err)
goto out;
again:
/*启动一个xenbus事务*/
err = xenbus_transaction_start(&xbt);
if (err) {
xenbus_dev_fatal(dev, err, "starting transaction");
goto destroy_blkring;
}
/*输出信息,在后端可以看到输出的信息*/
err = xenbus_printf(xbt, dev->nodename,
"ring-ref","%u", info->ring_ref);
if (err) {
message = "writing ring-ref";
goto abort_transaction;
}
err = xenbus_printf(xbt, dev->nodename, "event-channel", "%u",
irq_to_evtchn_port(info->irq));
if (err) {
message = "writing event-channel";
goto abort_transaction;
}
err = xenbus_printf(xbt, dev->nodename, "protocol", "%s",
XEN_IO_PROTO_ABI_NATIVE);
if (err) {
message = "writing protocol";
goto abort_transaction;
}
err = xenbus_transaction_end(xbt, 0);
if (err) {
if (err == -EAGAIN)
goto again;
xenbus_dev_fatal(dev, err, "completing transaction");
goto destroy_blkring;
}
/*改变前端状态为初始化完成*/
xenbus_switch_state(dev, XenbusStateInitialised);
return 0;
abort_transaction:
xenbus_transaction_end(xbt, 1);
if (message)
xenbus_dev_fatal(dev, err, "%s", message);
destroy_blkring:
blkif_free(info, 0);
out:
return err;
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
talk_to_backend主要是建立和后端的通讯机制,也就是xenbus_transaction。这种通讯机制是基于xen提供的机制xen store实现的。xenbus_transaction实现了一种类似文件系统的读写接口。建立通讯机制后,就可以调用xenbus_printf写入信息,而后端驱动就可以读取这些信息。
然后前端驱动把状态改为“初始化完成”,等待后端驱动的回应。
从虚拟化的逻辑来说,此时控制逻辑转入后端驱动。当后端驱动完成需要的处理,将状态改为“connected"后,前端驱动将调用connect函数,完成虚拟化块设备的整个初始化工作。后端掌管的是物理设备,只有后端才能看到设备的真正信息,所以后端要完成设备的物理信息。比如块设备的扇区数目,扇区大小等等。
代码清单3-11 connect
static void connect(struct blkfront_info *info)
{
unsigned long long sectors;
unsigned long sector_size;
unsigned int binfo;
int err;
............................................
/**获得设备的物理信息,扇区,扇区大小等等/
err = xenbus_gather(XBT_NIL, info->xbdev->otherend,
"sectors", "%Lu", §ors,
"info", "%u", &binfo,
"sector-size", "%lu", §or_size,
NULL);
err = xenbus_gather(XBT_NIL, info->xbdev->otherend,
"feature-barrier", "%lu", &info->feature_barrier,
NULL);
err = xlvbd_add(sectors, info->vdevice, binfo, sector_size, info);
/*切换状态为connected*/
(void)xenbus_switch_state(info->xbdev, XenbusStateConnected);
/* Kick pending requests. */
spin_lock_irq(&blkif_io_lock);
info->connected = BLKIF_STATE_CONNECTED;
kick_pending_request_queues(info);
spin_unlock_irq(&blkif_io_lock);
/*把创建的磁盘设备加入到系统*/
add_disk(info->gd);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
Connect做的事情是根据后端写入的物理信息,来创建磁盘设备,设置设备的参数。最重要的是,要截获io请求,转入虚拟化的处理过程。
Linux的文件读写,是以请求队列的形式发送给块设备。每个块设备都有自己的请求队列处理函数。如果改写这个函数,就可以截获linux的io请求。
xlvbd_add要调用虚拟块设备的核心函数来完成块设备创建,初始化的工作。虚拟块设备的函数中,最重要的是xlvbd_alloc_gendisk。
代码清单3-12 虚拟驱动例子
xlvbd_alloc_gendisk(int minor, blkif_sector_t capacity, int vdevice,
u16 vdisk_info, u16 sector_size,
struct blkfront_info *info)
{
...............................
/*分配一个gendisk数据结构*/
gd = alloc_disk(nr_minors);
................................................
/*设置gd的主设备号,私有数据和磁盘容量*/
gd->major = mi->major;
gd->first_minor = minor;
gd->fops = &xlvbd_block_fops;
gd->private_data = info;
gd->driverfs_dev = &(info->xbdev->dev);
set_capacity(gd, capacity);
/*注册虚拟块设备的io队列处理函数*/
if (xlvbd_init_blk_queue(gd, sector_size)) {
del_gendisk(gd);
goto out;
}
if (vdisk_info & VDISK_CDROM)
gd->flags |= GENHD_FL_CD;
return 0;
}
<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
这里首先是申请一个gendisk设备数据结构,然后设置设备参数。注意,这里的参数是后端驱动写入的。
xlvbd_init_blk_queue的作用就是注册虚拟块设备的请求队列处理函数,这个函数是do_blkif_request。所有发送到块设备的io都首先经过这个函数。
do_blkif_request这个函数要把所有接收的io请求都转发到后端驱动。这里就不分析了。
4.3.2 块设备后端驱动
对后端驱动的分析,重点在后端对前端驱动的呼应和配合。首先分析后端驱动对前端状态改变的处理函数。
代码清单3-13 虚拟驱动例子
static void frontend_changed(struct xenbus_device *dev,
enum xenbus_state frontend_state)
{
struct backend_info *be = dev->dev.driver_data;
int err;
DPRINTK("%s", xenbus_strstate(frontend_state));
switch (frontend_state) {
case XenbusStateInitialising:
break;
case XenbusStateInitialised:
case XenbusStateConnected:
..........................
err = connect_ring(be);
update_blkif_status(be->blkif);
break;
case XenbusStateClosing:
blkif_disconnect(be->blkif);
xenbus_switch_state(dev, XenbusStateClosing);
break;
}
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
简化后端的状态处理函数,当前端状态变为“初始化完成”,后端要调用connect_ring来连接前端定义的ring,然后update_blkif_status来写入设备信息和更新状态。
代码清单3-14 虚拟驱动例子
static void update_blkif_status(blkif_t *blkif)
{
int err;
char name[TASK_COMM_LEN];
.........................................
/* Attempt to connect: exit if we fail to. */
connect(blkif->be);
err = blkback_name(blkif, name);
blkif->xenblkd = kthread_run(blkif_schedule, blkif, name);
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
实际调用的是connect来写入设备信息。
代码清单3-15 connect
static void connect(struct backend_info *be)
{
struct xenbus_transaction xbt;
int err;
struct xenbus_device *dev = be->dev;
/*写入块设备扇区*/
err = xenbus_printf(xbt, dev->nodename, "sectors", "%llu",
vbd_size(&be->blkif->vbd));
/* FIXME: use a typename instead */
err = xenbus_printf(xbt, dev->nodename, "info", "%u",
vbd_info(&be->blkif->vbd));
/*写入扇区大小*/
err = xenbus_printf(xbt, dev->nodename, "sector-size", "%lu",
vbd_secsize(&be->blkif->vbd));
/*设置状态为connected*/
err = xenbus_switch_state(dev, XenbusStateConnected);
return;
}<object data="data:application/x-silverlight-2," type="application/x-silverlight-2"
后端要写入块设备的扇区数目和扇区大小,以及node的名字。在前面块设备前端驱动的分析中,要通过connect函数来读入扇区数目和扇区大小来完成块设备初始化。读入的信息就是在这是由后端驱动写入的。
本节的分析只涉及块设备的创建和初始化。至于块设备对文件io的处理,以及前端和后端联动处理io的过程,读者可以自行分析一下。