admin 管理员组

文章数量: 1086019


2024年3月21日发(作者:大数据架构师需要掌握知识)

KVM虚拟机源代码分析

1, KVM结构及工作原理

1.1 KVM结构

KVM基本结构有两部分组成。一个是KVM Driver ,已经成为Linux 内核的一个模

块。负责虚拟机的创建,虚拟内存的分配,虚拟CPU寄存器的读写以及虚拟CPU的运行

等。另外一个是稍微修改过的Qemu,用于模拟PC硬件的用户空间组件,提供I/O设备

模型以及访问外设的途径。

用户模式

客户模式

内核模式

Qemu

LibKvm

Linux用户

模式

KVM fd

/dev/kvm

ioctl

Kvm-vm fd Kvm-vcpu fd

Linux内核

VMM(Linux 内核+ KVM Driver)

模式

图1 KVM基本结构

KVM基本结构如图1所示。其中KVM加入到标准的Linux内核中,被组织成Linux

中标准的字符设备(/dev/kvm)。Qemu通KVM提供的LibKvm应用程序接口,通过ioctl

系统调用创建和运行虚拟机。KVM Driver使得整个Linux成为一个虚拟机监控器。并且

在原有的Linux两种执行模式(内核模式和用户模式)的基础上,新增加了客户模式,客户

模式拥有自己的内核模式和用户模式。在虚拟机运行下,三种模式的分工如下:

客户模式:执行非I/O的客户代码。虚拟机运行在客户模式下。

内核模式:实现到客户模式的切换。处理因为I/O或者其它指令引起的从客户模式的

退出。KVM Driver工作在这种模式下。

用户模式:代表客户执行I/O指令Qemu运行在这种模式下。

在KVM模型中,每一个Guest OS 都作为一个标准的Linux进程,可以使用Linux

的进程管理指令管理。

在图1中./dev/kvm在内核中创建的标准字符设备,通过ioctl系统调用来访问内核

虚拟机,进行虚拟机的创建和初始化;kvm_vm fd是创建的指向特定虚拟机实例的文件描

述符,通过这个文件描述符对特定虚拟机进行访问控制;kvm_vcpu fd指向为虚拟机创建

的虚拟处理器的文件描述符,通过该描述符使用ioctl系统调用设置和调度虚拟处理器的运

行。

1.2 KVM工作原理

KVM的基本工作原理:用户模式的Qemu利用接口libkvm通过ioctl系统调用进入

内核模式。KVM Driver为虚拟机创建虚拟内存和虚拟CPU后执行VMLAUCH指令进入

客户模式。装载Guest OS执行。如果Guest OS发生外部中断或者影子页表缺页之类的

事件,暂停Guest OS的执行,退出客户模式进行一些必要的处理。然后重新进入客户模

式,执行客户代码。如果发生I/O事件或者信号队列中有信号到达,就会进入用户模式处

理。KVM采用全虚拟化技术。客户机不用修改就可以运行。

用户模式

Qemu

内核模式KVM

Driver

客户模式Guest

OS

开始

进入客户模式执行客户代码

执行IOCTL系统调

处理退出

处理I/OYes

I/O?

NO

信号到

VMX Root

Operation

VMX Non Root

Operation

图2 KVM 工作基本原理

2 ,相关技术-处理器管理和硬件辅助虚拟化技术

Intel 在2006年发布了硬件虚拟化技术。其中支持X86体系结构的称为Intel VT-x

技术。ADM称为SVM技术。

VT-x引入了一种新的处理器操作,叫做VMX(Virtual Machine Extension),提供

了两种处理器的工作环境。VMCS结构实现两种环境之间的切换。VM Entry使虚拟机进

去客户模式,VM Exit使虚拟机退出客户模式。

2.1 KVM中Guest OS的调度执行

VMM调度Guest OS执行时,Qemu通过ioctl系统调用进入内核模式,在KVM

Driver中通过get_cpu获得当前物理CPU的引用。之后将Guest状态从VMCS中读出。

并装入物理CPU中。执行VMLAUCH指令使得物理处理器进入非根操作环境,运行客户

代码。

当Guest OS执行一些特权指令或者外部事件时,比如I/O访问,对控制寄存器的操

作,MSR的读写数据包到达等。都会导致物理CPU发生VMExit,停止运行Guest OS。

将Guest OS保存到VMCS中,Host状态装入物理处理器中,处理器进入根操作环境,

KVM取得控制权,通过读取VMCS中VM_EXIT_REASON字段得到引起VM Exit的原因。

从而调用kvm_exit_handler处理函数。如果由于I/O获得信号到达,则退出到用户模式

的Qemu处理。处理完毕后,重新进入客户模式运行虚拟CPU。如果是因为外部中断,

则在Lib KVM中做一些必要的处理,重新进入客户模式执行客户代码。

2.2 KVM中内存管理

KVM使用影子页表实现客户物理地址到主机物理地址的转换。初始为空,随着虚拟机

页访问实效的增加,影子页表被逐渐建立起来,并随着客户机页表的更新而更新。在KVM

中提供了一个哈希列表和哈希函数,以客户机页表项中的虚拟页号和该页表项所在页表的

级别作为键值,通过该键值查询,如不为空,则表示该对应的影子页表项中的物理页号已

经存在并且所指向的影子页表已经生成。如为空,则需新生成一张影子页表,KVM将获取

指向该影子页表的主机物理页号填充到相应的影子页表项的内容中,同时以客户机页表虚

拟页号和表所在的级别生成键值,在代表该键值的哈希桶中填入主机物理页号,以备查询。

但是一旦Guest OS中出现进程切换,会把整个影子页表全部删除重建,而刚被删掉的页

表可能很快又被客户机使用,如果只更新相应的影子页表的表项,旧的影子页表就可以重

用。因此在KVM中采用将影子页表中对应主机物理页的客户虚拟页写保护并且维护一张

影子页表的逆向映射表,即从主机物理地址到客户虚拟地址之间的转换表,这样VM对页

表或页目录的修改就可以触发一个缺页异常,从而被KVM捕获,对客户页表或页目录项

的修改就可以同样作用于影子页表,通过这种方式实现影子页表与客户机页表保持同步。

2.3 KVM中设备管理

一个机器只有一套I/o地址和设备。设备的管理和访问是操作系统中的突出问题、同

样也是虚拟机实现的难题,另外还要提供虚拟设备供各个VM使用。在KVM中通过移植

Qemu中的设备模型(Device Model)进行设备的管理和访问。操作系统中,软件使用可编

程I/O(PIO)和内存映射I/O(MMIO)与硬件交互。而且硬件可以发出中断请求,由操

作系统处理。在有虚拟机的情况下,虚拟机必须要捕获并且模拟PIO和MMIO的请求,

模拟虚拟硬件中断。

捕获PIO:由硬件直接提供。当VM发出PIO指令时,导致VM Exit然后硬件会将

VM Exit原因及对应的指令写入VMCS控制结构中,这样KVM就会模拟PIO指令。MMIO

捕获:对MMIO页的访问导致缺页异常,被KVM捕获,通过X86模拟器模拟执行MMIO

指令。KVM中的I/O虚拟化都是用户空间的Qemu实现的。所有PIO和MMIO的访问

都是被转发到Qemu的。Qemu模拟硬件设备提供给虚拟机使用。KVM通过异步通知机

制以及I/O指令的模拟来完成设备访问,这些通知包括:虚拟中断请求,信号驱动机制以

及VM间的通信。

以虚拟机接收数据包来说明虚拟机和设备的交互。

图3 I/O分析

(1)当数据包到达主机的物理网卡后,调用物理网卡的驱动程序,在其中利用Linux内

核中的软件网桥,实现数据的转发。

(2)在软件网挢这一层,会判断数据包是发往那个设备的,同时调用网桥的发送函数,

向对应的端口发送数据包。

(3)若数据包是发往虚拟机的,则要通过tap设备进行转发,tap设备由两部分组成,

网络设备和字符设备。网络设备负责接收和发送数据包,字符设备负责将数据包往内核空

间和用户空间进行转发。Tap网络部分收到数据包后,将其设备文件符置位,同时向正在

运行VM的进程发出I/O可用信号,引起VM Exit,停止VM运行,进入根操作状态。KVM

根据KVM_EXIT_REASON判断原因,模拟I/O指令的执行,将中断注入到VM的中断向

量表中。

(4)返回用户模式的Qemu中,执行设备模型。返回到kvm_main loop中,执行Kvm

—main—loop—wait,之后进入main_loop wait中,在这个函数里收集对应设备的设备

文件描述符的状态,此时tap设备文件描述符的状态同样被集到fd set。

(5)Kvm main—loop不停地循环,通过select系统调用判断哪螋文件描述符的状态

发生变化,相应的调用对应的处理函数。对予tap来说,就会通过Qemu—send_packet

将数据发往rtl8139一do—receiver,在这个函数中完成相当于硬件RTL8139网卡的逻

辑操作。KVM通过模拟I/O指令操作虚拟RTL8139将数据拷贝到用户地址空间,放在

相应的I/O地址。用户模式处理完毕后返回内核模式,而后进入客户模式,VM被再次执

行,继续收发数据包。

3,KVM 源代码分析-虚拟机创建和运行流程代码分析

3.1 KVM创建和运行虚拟机流程

KVM虚拟机创建和运行虚拟机分为用户态和核心态两个部分,用户态主要提供应用程

序接口,为虚拟机创建虚拟机上下文环境,在libkvm中提供访问内核字符设备/dev/kvm

的接口;内核态为添加到内核中的字符设备/dev/kvm,模块加载进内核后即可进行接口用

户空间调用创建虚拟机。在创建虚拟机过程中,kvm字符设备主要为客户机创建kvm数

据机构,创建该虚拟机的虚拟机文件描述符及其相应的数据结构以及创建虚拟处理器及其

相应的数据结构。Kvm创建虚拟机的流程如图4所示。

首先申明一个kvm_context_t 变量用以描述用户态虚拟机上下文信息,然后调用

kvm_init()函数初始化虚拟机上下文信息;函数kvm_create()创建虚拟机实例,该函数通

过ioctl系统调用创建虚拟机相关的内核数据结构并且返回虚拟机文件描述符给用户态

kvm_context_t数据结构;创建完内核虚拟机数据结构后,再创建内核pit以及mmio等

基本外设模拟设备,然后调用kvm_create_vcpu()函数来创建虚拟处理器,

kvm_create_vcpu()函数通过ioctl()系统调用向由vm_fd文件描述符指向的虚拟文件调用

创建虚拟处理器,并将虚拟处理器的文件描述符返回给用户态程序,用以以后的调度使用;

创建完虚拟处理器后,由用户态的QEMU程序申请客户机用户空间,用以加载和运行客户

机代码;为了使得客户虚拟机正确执行,必须要在内核中为客户机建立正确的内存映射关

系,即影子页表信息。因此,申请客户机内存地址空间后,调用函数

kvm_create_phys_mem()创建客户机内存映射关系,该函数主要通过ioctl系统调用向

vm_fd指向的虚拟文件调用设置内核数据结构中客户机内存域相关信息,主要建立影子页

表信息;当创建好虚拟处理器和影子页表后,即可读取客户机到指定分配的空间中,然后

调度虚拟处理器运行。调度虚拟机的函数为kvm_run(),该函数通过ioctl系统调用调用由

虚拟处理器文件描述符指向的虚拟文件调度处理函数kvm_run()调度虚拟处理器的执行,

该系统调用将虚拟处理器vcpu信息加载到物理处理器中,通过vm_entry执行进入客户

机执行。在客户机正常运行期间kvm_run()函数不返回,只有发生以下两种情况时,函数

返回:1,发生了I/O事件,如客户机发出读写I/O的指令;2,产生了客户机和内核KVM

都无法处理的异常。I/O事件处理完毕后,通过重新调用KVM_RUN()函数继续调度客户机

的执行。

用户态内核态

声明虚拟机上下文

Kvm_context_t

调用kvm_init()初始化 虚

拟机上下文kvm_context_t

调用kvm_create()函数创建

虚拟机

返回虚拟机文件描述符

vm_fd

调用

ioctl(fd,KVM_CREATE_VM

,0)来创建内核虚拟机相

关数据结构

调用

ioctl(kvm_vmfd,KVM_CRE

ATE_IRQCHIP)来创建内核

irqchip数据结构

创建pit,mmio,以及irqchip

Ioctl 系统调用

调用kvm_create_vcpu创建

虚拟处理器

返回虚拟处理器的文件描

述符信息

调用

ioctl(kvm_vmfd,KVM_CRE

ATE_VCPU,slot)来创建内

核虚拟处理器信息

申请虚拟机用户空间内存块

调用kvm_create_phys_mem

创建虚拟机内核内存,建立

基本的影子页表

调用

ioctl(kvm_vmfd,KVM_SET

_USER_MEMORY_REGION,**

mem)来创建内核虚拟机影

子页表信息

调用kvm_run()函数运行虚

拟处理器,调度客户机的执

调用

ioctl(fd,KVM_RUN,0)来

调度虚拟处理器被加载到

物理处理器上执行

图 4 KVM虚拟机创建流程

3.2 虚拟机创建和运行主要函数分析

1,函数kvm_init():该函数在用户态创建一个虚拟机上下文,用以在用户态保存基本

的虚拟机信息,这个函数是创建虚拟机第一个需要调用的函数,函数返回一个

kvm_context_t结构体。该函数原型为:

Kvm_context_t kvm_init(struct kvm_callbacks *callbacks,void *opaque);

参数:callbacks为结构体kvm_callbacks变量,该结构体包含指向函数的一组指针,

用于在客户机执行过程中因为I/O事件退出到用户态的时候处理的回调函数(后面会分析)。

参数opaque一般未使用。

函数执行基本过程:打开字符设备dev/kvm,申请虚拟机上下文变量kvm_context_t

空间,初始化上下文的基本信息:设置fd文件描述符指向/dev/kvm、禁用虚拟机文件描

述符vm_fd(-1)

设置I/O事件回调函数结构体,设置IRQ和PIT的标志位以及内存页面记录的标志位。

主要相关数据结构:

虚拟机上下文structkvm_context_t,用户态数据结构,用以描述虚拟机实例的用户

态上下文信息。

Struct kvm_context: 该结构体用于表示一个虚拟机上下文。主要包含的数据域为:

Int fd :指向内核标准字符设备/dev/kvm的文件描述符。

Int vm_fd:指向所创建的内核虚拟机数据结构相关文件的文件描述符。

Int vcpu_fd[MAX_VCPUS]:指向虚拟机所有的虚拟处理器的文件描述符数组。

Struct kvm_run *run[MAX_VCPUS]:指向虚拟机运行环境上下文的指针数组。

Struct kvm_callbacks *call_backs: 回调函数结构体指针,该结构体用于处理用户态

I/O事件。

Void *opaque:指针(还未弄清楚)

Int dirty_page_log_all:设置是否记录脏页面的标志。

Int no_ira_creation: 用于设置是否再kernel里设置irq芯片。

Int_irqchip_in_kernel: 内核中irqchip的状态

Int irqchip_inject_ioctl:用于拦截中断的iotcl系统调用

Int no_pit_creation: 设置是否再内核中设置陷阱

int pit_in_kernel:PIT状态

int coalesced_mmio:kernel中mmio

struct kvm_irq_routing *irq_routes:KVM中中断处理的路由结构指针。

int nr_allocated_irq_routes:分配给该虚拟机的中断路由数目。

int max_used_gsi:使用的最大的gsi(?)。

用户态I/O处理函数结构体struct kvm_callbacks,该结构体指向一组函数,主要用

于处理客户机因为I/O事件退出而执行的过程。

struct kvm_callbacks :该结构体用于在用户态中处理I/O事件,在KVM中调用

KVM_QEMU实现。主要包含的数据域为:

int (*inb)(void *opaque, uint16_t addr, uint8_t *data):用于模拟客户机执行8位

的inb指令。

int (*inw)(void *opaque, uint16_t addr, uint16_t *data):用于模拟客户机执行16

位的inw指令。

int (*inl)(void *opaque, uint16_t addr, uint32_t *data):用于模拟客户机执行32

位的inl指令。

int (*outb)(void *opaque, uint16_t addr, uint8_t data):用于模拟客户机执行8位

的outb指令。

int (*outw)(void *opaque, uint16_t addr, uint16_t data):用于模拟客户机执行16

位的outw指令。

int (*outl)(void *opaque, uint16_t addr, uint32_t data):用于模拟客户机执行32

位的outl指令。

int (*mmio_read)(void *opaque, uint64_t addr, uint8_t *data,int len):用于模拟

客户机执行mmio读指令。

int (*mmio_write)(void *opaque, uint64_t addr, uint8_t *data,int len):用于模拟

客户机执行mmio写指令。

int (*debug)(void *opaque, void *env, struct kvm_debug_exit_arch *arch_info):

用户客户机调试的回调函数。

int (*halt)(void *opaque, int vcpu):用于客户机执行halt指令的响应。

int (*shutdown)(void *opaque, void *env):用于客户机执行shutdown指令的响

应。

int (*io_window)(void *opaque):用于获得客户机io_windows。

int (*try_push_interrupts)(void *opaque):用于注入中断的回调函数。

void (*push_nmi)(void *opaque):用于注入nmi中断的函数。

void (*post_kvm_run)(void *opaque, void *env);用户得到kvm运行状态函数。

int (*pre_kvm_run)(void *opaque, void *env);用于获得kvm之前运行状态的函数

int (*tpr_access)(void *opaque, int vcpu, uint64_t rip, int is_write);获得tpr访问

处理函数

int (*powerpc_dcr_read)(int vcpu, uint32_t dcrn, uint32_t *data);用于powerpc

的dcr读操作

nt (*powerpc_dcr_write)(int vcpu, uint32_t dcrn, uint32_t data);用于powerpc的

dcr写操作

int (*s390_handle_intercept)(kvm_context_t context, int vcpu,struct kvm_run

*run);用于s390的中断处理。

int (*s390_handle_reset)(kvm_context_t context, int vcpu,struct kvm_run *run);

用于s390的重设处理。

}

当客户机执行I/O事件或者停机操作等事件时,KVM会交给用户态的QEMU模拟外

部I/O事件,调用这个结构体指向的相关的函数进行处理。

Struct kvm_run: 用于KVM运行时一些的一些状态信息。主要包含的数据域为:

__u8 request_interrupt_window;

__u8 padding1[7];

__u32 exit_reason;

__u8 ready_for_interrupt_injection;

__u8 if_flag;

__u8 padding2[2];

/* in (pre_kvm_run), out (post_kvm_run) */

__u64 cr8;

__u64 apic_base;

union {

/* KVM_EXIT_UNKNOWN */

struct {

__u64 hardware_exit_reason; 记录退出原因

} hw;

/* KVM_EXIT_FAIL_ENTRY */ 客户机执行过程中执行VM_ENTRY失败。

struct {

__u64 hardware_entry_failure_reason;

} fail_entry;

/* KVM_EXIT_EXCEPTION */ 客户机因为异常退出

struct {

__u32 exception;

__u32 error_code;

} ex;

/* KVM_EXIT_IO */ 客户机因为IO事件退出。

struct kvm_io {

#define KVM_EXIT_IO_IN 0

#define KVM_EXIT_IO_OUT 1

__u8 direction;

__u8 size; /* bytes */

__u16 port;

__u32 count;

__u64 data_offset; /* relative to kvm_run start */

} io;

struct {

struct kvm_debug_exit_arch arch;

} debug;

/* KVM_EXIT_MMIO */ 客户机因为MMIO退出

struct {

__u64 phys_addr;

__u8 data[8];

__u32 len;

__u8 is_write;

} mmio;

/* KVM_EXIT_HYPERCALL */ 客户机退出的超调用参数。

struct {

__u64 nr;

__u64 args[6];

__u64 ret;

__u32 longmode;

__u32 pad;

} hypercall;

/* KVM_EXIT_TPR_ACCESS */ 客户机退出访问TPR参数

struct {

__u64 rip;

__u32 is_write;

__u32 pad;

} tpr_access;

/* KVM_EXIT_S390_SIEIC */ 和S390相关数据

struct {

__u8 icptcode;

__u64 mask; /* psw upper half */

__u64 addr; /* psw lower half */

__u16 ipa;

__u32 ipb;

} s390_sieic;

/* KVM_EXIT_S390_RESET */

#define KVM_S390_RESET_POR 1

#define KVM_S390_RESET_CLEAR 2

#define KVM_S390_RESET_SUBSYSTEM 4

#define KVM_S390_RESET_CPU_INIT 8

#define KVM_S390_RESET_IPL

__u64 s390_reset_flags;

/* KVM_EXIT_DCR */

struct {

__u32 dcrn;

__u32 data;

__u8 is_write;

} dcr;

/* Fix the size of the union. */

char padding[256];

16

2, 函数kvm_create():该函数主要用于创建一个虚拟机内核环境。该函数原型为:

int kvm_create(kvm_context_t kvm,unsigned long phys_mem_bytes, void

**phys_mem);

参数:kvm_context_t 表示传递的用户态虚拟机上下文环境,phys_mem_bytes表

示需要创建的物理内存的大小,phys_mem表示创建虚拟机的首地址。这个函数首先调用

kvm_create_vm()分配IRQ并且初始化为0,设置vcpu[0]的值为-1,即不允许调度虚拟机

执行。然后调用ioctl系统调用ioctl(fd,KVM_CREATE_VM,0)来创建虚拟机内核数据结构

struct kvm。

3,系统调用函数ioctl(fd,KVM_CREATE_VM,0),用于在内核中创建和虚拟机相关的数

据结构。该函数原型为:

Static long kvm_dev_ioctl(struct file *filp,unsigned int ioctl, unsigned long

arg);其中ioctl表示命令。这个函数调用kvm_dev_ioctl_create_vm()创建虚拟机实例内核

相关数据结构。该函数首先通过内核中kvm_create_vm()函数创建内核中kvm上下文

struct kvm,然后通过函数

Anno_inode_getfd(“kvm_vm”,&kvm_vm_fops,kvm,0)返回该虚拟机的文件描述

符,返回给用户调用函数,由2中描述的函数赋值给用户态虚拟机上下文变量中的虚拟机

描述符kvm_vm_fd。

4,内核创建虚拟机kvm对象后,接着调用kvm_arch_create函数用于创建一些体系

结构相关的信息,主要包括kvm_init_tss、kvm_create_pit以及kvm_init_coalsced_mmio

等信息。然后调用kvm_create_phys_mem创建物理内存,函数kvm_create_irqchip用

于创建内核irq信息,通过系统调用ioctl(kvm->vm_fd,KVM_CREATE_IRQCHIP)。

5,函数kvm_create_vcpu():用于创建虚拟处理器。该函数原型为:

int kvm_create_vcpu(kvm_context_t kvm, int slot);

参数:kvm表示对应用户态虚拟机上下文,slot表示需要创建的虚拟处理器的个数。

该函数通过ioctl系统调用ioctl(kvm->vm_fd,KVM_CREATE_VCPU,slot)创建属于

该虚拟机的虚拟处理器。该系统调用函数:

Static init kvm_vm_ioctl_create_vcpu(struct *kvm, n) 参数kvm为内核虚拟机实例

数据结构,n为创建的虚拟CPU的数目。

6,函数kvm_create_phys_mem()用于创建虚拟机内存空间,该函数原型:

Void * kvm_create_phys_mem(kvm_context_t kvm,unsigned long

phys_start,unsigned len,int log,int writable);

参数:kvm 表示用户态虚拟机上下文信息,phys_start为分配给该虚拟机的物理起始

地址,len表示内存大小,log表示是否记录脏页面,writable表示该段内存对应的页表是

否可写。

该函数首先申请一个结构体kvm_userspace_memory_region 然后通过系统调用

KVM_SET_USER_MEMORY_REGION来设置内核中对应的内存的属性。该系统调用函数

原型:

Ioctl(int kvm->vm_fd,KVM_SET_USER_MEMORY_REGION,&memory);

参数:第一个参数vm_fd为指向内核虚拟机实例对象的文件描述符,第二个参数

KVM_SET_USER_MEMORY_REGION为系统调用命令参数,表示该系统调用为创建内核

客户机映射,即影子页表。第三个参数memory表示指向该虚拟机的内存空间地址。系统

调用首先通过参数memory通过函数copy_from_user从用户空间复制

struct_user_momory_region 变量,然后通过kvm_vm_ioctl_set_memory_region函数

设置内核中对应的内存域。该函数原型:

Int kvm_vm_ioctl_set_memory_region(struct *kvm,struct

kvm_usersapce_memory_region *mem,int user_alloc);该函数再调用函数

kvm_set_memory_resgion()设置影子页表。当这一切都准备完毕后,调用kvm_run()函

数即可调度执行虚拟处理器。

7,函数kvm_run():用于调度运行虚拟处理器。该函数原型为:

Int kvm_run(kvm_context_t kvm,int vcpu, void *env) 该函数首先得到vcpu的描

述符,然后调用系统调用ioctl(fd,kvm_run,0)调度运行虚拟处理器。Kvm_run函数在正常

运行情况下并不返回,除非发生以下事件之一:一是发生了I/O事件,I/O事件由用户态

的QEMU处理;一个是发生了客户机和KVM都无法处理的异常事件。KVM_RUN()中返

回截获的事件,主要是I/O以及停机等事件。

4, KVM客户机异常处理机制和代码流程

KVM保证客户机正确执行的基本手段就是当客户机执行I/O指令或者其它特权指令

时,引发处理器异常,从而陷入到根操作模式,由KVM Driver模拟执行,可以说,虚拟

化保证客户机正确执行的基本手段就是异常处理机制。由于KVM采取了硬件辅助虚拟化

技术,因此,和异常处理机制相关的一个重要的数据结构就是虚拟机控制结构VMCS。

VMCS控制结构分为三个部分,一个是版本信息,一个是中止标识符,最后一个是

VMCS数据域。VMCS数据域包含了六类信息:客户机状态域,宿主机状态域,VM-Entry

控制域,VM-Execution控制域VM-Exit控制域以及VM-Exit信息域。其中VM-Execution

控制域可以设置一些可选的标志位使得客户机可以引发一定的异常的指令。宿主机状态域,

则保持了基本的寄存器信息,其中CS:RIP指向KVM中异常处理程序的地址。是客户机异

常处理的总入口,而异常处理程序则根据VM-Exit信息域来判断客户机异常的根本原因,

选择正确的处理逻辑来进行处理。

vmx.c文件是和Intel VT-x体系结构相关的代码文件,用于处理内核态相关的硬件逻

辑代码。在VCPU初始化中(vmx_vcpu_create),将kvm中的对应的异常退出处理函数赋

值给CS:EIP中,在客户机运行过程中,产生客户机异常时,CPU根据VMCS中的客户机

状态域装载CS:EIP的值,从而退出到内核执行异常处理。在KVM内核中,异常处理函数

的总入口为:

static int vmx_handle_exit(struct kvm_run *kvm_run, struct kvm_vcpu *vcpu);

参数:kvm_run,当前虚拟机实例的运行状态信息,vcpu,对应的虚拟cpu。

这个函数首先从客户机VM-Exit信息域中读取exit_reason字段信息,然后进行一些

必要的处理后,调用对应于函数指针数组中对应退出原因字段的处理函数进行处理。函数

指针数组定义信息为:

static int (*kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu,

struct kvm_run *kvm_run) = {

[EXIT_REASON_EXCEPTION_NMI] = handle_exception,

[EXIT_REASON_EXTERNAL_INTERRUPT] = handle_external_interrupt,

[EXIT_REASON_TRIPLE_FAULT] = handle_triple_fault,

[EXIT_REASON_NMI_WINDOW] = handle_nmi_window,

[EXIT_REASON_IO_INSTRUCTION] = handle_io,

[EXIT_REASON_CR_ACCESS] = handle_cr,

[EXIT_REASON_DR_ACCESS] = handle_dr,

[EXIT_REASON_CPUID] = handle_cpuid,

[EXIT_REASON_MSR_READ] = handle_rdmsr,

[EXIT_REASON_MSR_WRITE] = handle_wrmsr,

[EXIT_REASON_PENDING_INTERRUPT] = handle_interrupt_window,

[EXIT_REASON_HLT] = handle_halt,

[EXIT_REASON_INVLPG] = handle_invlpg,

[EXIT_REASON_VMCALL] = handle_vmcall,

[EXIT_REASON_TPR_BELOW_THRESHOLD] = handle_tpr_below_threshold,

[EXIT_REASON_APIC_ACCESS] = handle_apic_access,

[EXIT_REASON_WBINVD] = handle_wbinvd,

[EXIT_REASON_TASK_SWITCH] = handle_task_switch,

[EXIT_REASON_EPT_VIOLATION] = handle_ept_violation,

};

这是一组指针数组,用于处理客户机引发异常时候,根据对应的退出字段选择处理函

数进行处理。例如EXIT_REASON_EXCEPTION_NMI对应的handle_exception处理函数

用于处理NMI引脚异常,而EXIT_REASON_EPT_VIOLATION对应的

handle_ept_violation处理函数用于处理缺页异常。

5, KVM 客户机影子页表机制和缺页处理机制及其代码流程

5.1 KVM影子页表基本原理和缺页处理流程

KVM给客户机呈现了一个从0开始的连续的物理地址空间,成为客户机物理地址空

间,在宿主机中,只是由KVM分配给客户机的宿主机物理内存的一部分。客户机中为应

用程序建立的客户机页表,只是实现由客户机虚拟地址到客户机物理地址的转换

(GVA-GPA),为了正确的访问内存,需要进行二次转换,即客户机物理地址到宿主机物

理地址的转换(GPA-HPA),两个地址转换,前者由客户机负责完成,后者由VMM负责

完成。为了实现客户机物理地址到主机物理地址的地址翻译,VMM为每个虚拟机动态维

护了一张从客户机物理地址到宿主机物理地址的映射关系表。客户机OS所维护的页表只

是负责传统的客户机虚拟地址到客户机物理地址的转换。如果MMU直接装在客户机操作

系统所维护的页表进行内存访问,硬件则无法争取的实现地址翻译。

影子页表是一个有效的解决办法。一份影子页表与一份客户机操作系统的页表相对应,

作用是实现从客户虚拟机地址到宿主机物理地址的直接翻译。这样,客户机所能看到和操

作的都是虚拟MMU,而真正载入到屋里MMU的是影子页表。如图5所示。

虚拟内存空间

虚拟地址

客户机操作系统内页表

客户机物理内存空间

客户机物理地址

影子页表

虚实物理地址翻译表

宿主机物理内存空间

宿主机物理地址

图5-影子页表

由于客户机维护的页表真正体现在影子页表上,因此,客户机对客户机页表的操作实

质上反映到影子页表中,并且由VMM控制,因此,影子页表中,对于页表页的访问权限

是只读的。一旦客户机对客户机页表进行修改,则产生页面异常,由VMM处理。

影子页表的结构:以x86的两级页表为例。客户机物理帧号GFN和宿主机物理帧号

MFN是一一对应的,而宿主机必须要另外负责创建一个页用于CR3寄存器指向的影子页

表SMFN。通常通过hash表实现MFN和SMFN之间的计算。影子页表的建立和修改过

程交错在客户机对客户机页表的创建过程中。

影子页表的缺页异常处理:当发生缺页异常时,产生异常条件并被VMM捕获,让VMM

发现客户机操作系统尚未给客户机虚拟机地址分配客户机物理页,那么VMM首先将缺页

异常传递给客户机操作系统,由客户机操作系统为这个客户机虚拟地址分配客户机物理页,

由于客户机操作系统分配客户机物理页需要修改其页表,因为又被VMM捕获,VMM更

新影子页表中相应的目录和页表项,增加这个客户机虚拟地址到分配的宿主页的映射。如

果产生异常时,VMM发现客户机操作系统已经分配给了相应的客户机物理页,只是写权

限造成写异常,则VMM更新影子页表中的页目录和页表项,重定向客户机虚拟地址页到

宿主机页的映射。客户机缺页异常处理逻辑流程如图6所示。

客户机产生缺页异常

客户机本身引起异

VMM将异常注入到客户机,

由Guest OS 自身处理

影子页表缺页异常

根据客户机页表建立相应

影子页目录和页表结构

根据客户页表得到与之对

应的客户机物理地址

根据虚实物理地址对照表

得到宿主机物理地址

将宿主机物理地址填充到

影子页表中

同步客户机页表和影子页

表访问修改位,语义同步

图6 – 缺页异常处理流程

5.2 KVM中缺页异常的处理代码流程

由4中说明的异常处理逻辑中,vmx_handle_exit函数用于处理客户机异常的总入口

程序,KVM根据退出域中的退出原因字段,调用函数指针数据中对应的处理函数进行处理,

在缺页异常中,处理函数为handle_ept_violation。该函数调用kvm_mmu_page_fault

函数进行缺页异常处理。

6, KVM中主要数据结构以及相互关系

6.1 用户态关键数据机构

<>kvm_context

-int fd

-int vm_fd;

-int vcpu_fd[MAX_VCPUS]

-struct kvm_run *run[MAX_VCPUS]

-struct kvm_callbacks *callbacks

-void *opaque

-int dirty_pages_log_all

-int no_irqchip_creation

-int irqchip_in_kernel

-int irqchip_inject_ioctl

-int no_pit_creation

-int pit_in_kernel

-int coalesced_mmio

-struct kvm_irq_routing *irq_routes

-int nr_allocated_irq_routes

-int max_used_gsi

<>

kvm_run

+__u8 request_interrupt_window

+__u8 padding1[7]

+__u32 exit_reason

+__u8 ready_for_interrupt_injection

+__u8 if_flag

+__u8 padding2[2]

+__u64 cr8

+_u64 apic_base

+union anno

<>

kvm_irq_routing

+__u32 nr

+__u32 flags

+struct kvm_irq_routing_entry entries[0]

<>

kvm_callbacks

+int (*inb)(void *opaque, uint16_t addr, uint8_t *data)

+int (*inw)(void *opaque, uint16_t addr, uint16_t *data)

+int (*inl)(void *opaque, uint16_t addr, uint32_t *data)

+int (*outb)(void *opaque, uint16_t addr, uint8_t data)

+int (*outw)(void *opaque, uint16_t addr, uint16_t data)

+int (*outl)(void *opaque, uint16_t addr, uint32_t data)

+int (*mmio_read)(void *opaque, uint64_t addr, uint8_t *data,int len)

+int (*mmio_write)(void *opaque, uint64_t addr, uint8_t *data,int len)

+int (*debug)(void *opaque, void *env, struct kvm_debug_exit_arch *arch_info)

+int (*halt)(void *opaque, int vcpu)

+int (*shutdown)(void *opaque, void *env)

+int (*io_window)(void *opaque)

+int (*try_push_interrupts)(void *opaque)

+void (*push_nmi)(void *opaque)

+void (*post_kvm_run)(void *opaque, void *env)

+int (*pre_kvm_run)(void *opaque, void *env)

+int (*tpr_access)(void *opaque, int vcpu, uint64_t rip, int is_write)

+int (*powerpc_dcr_read)(int vcpu, uint32_t dcrn, uint32_t *data)

+int (*powerpc_dcr_write)(int vcpu, uint32_t dcrn, uint32_t data)

+int (*s390_handle_intercept)(kvm_context_t context, int vcpu,struct kvm_run *run)

+int (*s390_handle_reset)(kvm_context_t context, int vcpu,struct kvm_run *run)

<>

kvm_irq_routing_entry

+__u32 gsi

+__u32 type

+__u32 flags

+__u32 pad

+union u

<>u

-struct kvm_irq_routing_irqchip irqchip

-struct kvm_irq_routing_msi msi

-__u32 pad[8]

<>

kvm_debug_exit_arch

+__u32 exception

+__u32 pad

+__u64 pc

+__u64 dr6

+__u64 dr7

<>

kvm_irq_routing_irqchip

+__u32 irqchip

+__u32 pin

<>

kvm_irq_routing_msi

+__u32 address_lo

+__u32 address_hi

+__u32 data

+__u32 pad

KVM用户态重要数据结构

KVM-CONTEXT-T为虚拟机上下文

KVM-run为虚拟机运行状态

Kvm-callbacks为虚拟机产生I/O事件处理的回调函数

Kvm-irq-routing 为中断处理相关数据结构

6.2 内核态关键数据机构

<>kvm

+struct mutex lock

+spinlock_t mmu_lock

+struct rw_semaphore slots_lock

+struct mm_struct *mm

+int nmemslots

+struct kvm_memory_slot memslots[KVM_MEMORY_SLOTS +KVM_PRIVATE_MEM_SLOTS]

+struct kvm_vcpu *vcpus[KVM_MAX_VCPUS]

+struct list_head vm_list

+struct kvm_io_bus mmio_bus

+struct kvm_io_bus pio_bus

+struct kvm_vm_stat stat

+struct kvm_arch arch

+atomic_t users_count

+struct kvm_coalesced_mmio_dev *coalesced_mmio_dev

+struct kvm_coalesced_mmio_ring *coalesced_mmio_ring

+struct list_head irq_routing

+struct hlist_head mask_notifier_list

+struct mmu_notifier mmu_notifier

+unsigned long mmu_notifier_seq

+long mmu_notifier_count

<>

kvm_memory_slot

+gfn_t base_gfn

+unsigned long npages

+unsigned long flags

+unsigned long *rmap

+unsigned long *dirty_bitmap

+struct *lpage_info

+unsigned long userspace_addr

+int user_alloc

<>kvm_vcpu

+struct kvm *kvm

+struct preempt_notifier preempt_notifier

+int vcpu_id

+struct mutex mutex

+int cpu

+struct kvm_run *run

+int guest_mode

+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

<>

kvm_io_bus

+int dev_count

+#define NR_IOBUS_DEVS 6

+struct kvm_io_device *devs[NR_IOBUS_DEVS]

<>

lpage_info

+unsigned long rmap_pde

+int write_count

<>

kvm_arch

+int naliases

+struct kvm_mem_alias aliases[KVM_ALIAS_SLOTS]

+unsigned int n_free_mmu_pages

+unsigned int n_requested_mmu_pages;

+unsigned int n_alloc_mmu_pages

+struct hlist_head mmu_page_hash[KVM_NUM_MMU_PAGES]

+struct list_head active_mmu_pages

+struct list_head assigned_dev_head

+struct list_head oos_global_pages

+struct iommu_domain *iommu_domain

+struct kvm_pic *vpic

+struct kvm_ioapic *vioapic

+struct kvm_pit *vpit

+struct hlist_head irq_ack_notifier_list

+int vapics_in_nmi_mode

+unsigned int tss_addr

+struct page *apic_access_page

+gpa_t wall_clock

+struct page *ept_identity_pagetable

+bool ept_identity_pagetable_done

+unsigned long irq_sources_bitmap

+unsigned long irq_states[KVM_IOAPIC_NUM_PINS]

+u64 vm_init_tsc

<>

kvm_ioapic

+u64 base_address

+u32 ioregsel

+u32 id

+u32 irr

+u32 pad

+union kvm_ioapic_redirect_entry redirtbl[IOAPIC_NUM_PINS]

+struct kvm_io_device dev

+struct kvm *kvm

+void (*ack_notifier)(void *opaque, int irq)

<>

kvm_vm_stat(x86)

+u32 mmu_shadow_zapped;

+u32 mmu_pte_write

+u32 mmu_pte_updated

+u32 mmu_pde_zapped

+u32 mmu_flooded

+u32 mmu_recycled

+u32 mmu_cache_miss

+u32 mmu_unsync

+u32 mmu_unsync_global

+u32 remote_tlb_flush

+u32 lpages

<>

kvm_mem_alias

+gfn_t base_gfn

+unsigned long npages

+gfn_t target_gfn

<>

kvm_coalesced_mmio_dev

+struct kvm_io_device dev

+struct kvm *kvm

+int nb_zones

+struct kvm_coalesced_mmio_zone zone[KVM_COALESCED_MMIO_ZONE_MAX]

<>

kvm_coalesced_mmio_ring

+__u32 first, last

+struct kvm_coalesced_mmio coalesced_mmio[0]

KVM 结构体定义

<>

kvm_vcpu

+struct kvm *kvm

+struct preempt_notifier preempt_notifier

+int vcpu_id

+struct mutex mutex

+int cpu

+struct kvm_run *run

+int guest_mode

+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

+int mmio_needed;

+int mmio_read_completed

+int mmio_is_write

+int mmio_size

+unsigned char mmio_data[8]

+gpa_t mmio_phys_addr

+struct kvm_vcpu_arch arch

<>

preempt_notifier

+struct list_head link

+struct task_struct *tsk

+struct preempt_ops *ops

<>

kvm_run

+__u8 request_interrupt_window

+__u8 padding1[7]

+__u32 exit_reason

+__u8 ready_for_interrupt_injection

+__u8 if_flag

+__u8 padding2[2]

+__u64 cr8

+_u64 apic_base

+union anno

kvm_vcpu_stat

-u32 pf_fixed

-u32 pf_guest

-u32 tlb_flush

-u32 invlpg

-u32 exits

-u32 io_exits

-u32 mmio_exits

-u32 signal_exits

-u32 irq_window_exits

-u32 nmi_window_exits

-u32 halt_exits

-u32 halt_wakeup

-u32 request_irq_exits

-u32 request_nmi_exits

-u32 irq_exits;

-u32 host_state_reload

-u32 efer_reload

-u32 fpu_reload

-u32 insn_emulation

-u32 insn_emulation_fail

-u32 hypercalls

-u32 irq_injections

-u32 nmi_injections

<>kvm_vcpu_arch

preempt_ops

-void (*sched_in)(struct preempt_notifier *notifier, int cpu)

-void (*sched_out)(struct preempt_notifier *notifier,struct task_struct *next)

VCPU 结构体定义

<>

kvm_vcpu_arch

+u64 host_tsc

+int interrupt_window_open

+unsigned long irq_summary

+DECLARE_BITMAP(irq_pending, KVM_NR_INTERRUPTS)

+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]

+u64 shadow_efer

+u64 apic_base

+struct kvm_lapic *apic

+int32_t apic_arb_prio

+int mp_state

+int sipi_vector

+u64 ia32_misc_enable_msr

+bool tpr_access_reporting;

+struct kvm_mmu mmu

+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

kvm_vcpu_stat

+int last_pt_write_count

+u64 *last_pte_updated

-u32 pf_fixed

+gfn_t last_pte_gfn

-u32 pf_guest

+struct update_pte

-u32 tlb_flush

+struct i387_fxsave_struct host_fx_image

-u32 invlpg

+struct i387_fxsave_struct guest_fx_image

-u32 exits

+gva_t mmio_fault_cr2

-u32 io_exits

+struct kvm_pio_request pio

-u32 mmio_exits

+void *pio_data

-u32 signal_exits

+struct kvm_queued_exception exception

-u32 irq_window_exits

+struct kvm_queued_interrupt interrupt

-u32 nmi_window_exits

+struct rmode

-u32 halt_exits

+int halt_request

-u32 halt_wakeup

+int cpuid_nent

-u32 request_irq_exits

+struct kvm_cpuid_entry2 cpuid_entries[KVM_MAX_CPUID_ENTRIES]

-u32 request_nmi_exits

+struct x86_emulate_ctxt emulate_ctxt

-u32 irq_exits;

+gpa_t time;

-u32 host_state_reload

+struct pvclock_vcpu_time_info hv_clock

-u32 efer_reload

+unsigned int hv_clock_tsc_khz

-u32 fpu_reload

+unsigned int time_offset

-u32 insn_emulation

+struct page *time_page

-u32 insn_emulation_fail

+bool nmi_pending

-u32 hypercalls

+bool nmi_injected

-u32 irq_injections

+bool nmi_window_open;

-u32 nmi_injections

+struct mtrr_state_type mtrr_state

+u32 pat

+int switch_db_regs

+unsigned long host_db[KVM_NR_DB_REGS]

+unsigned long host_dr6

+unsigned long host_dr7

+unsigned long db[KVM_NR_DB_REGS]

+unsigned long dr6

+unsigned long dr7

+unsigned long eff_db[KVM_NR_DB_REGS]

kvm_vcpu_arch结构

<>

kvm_vcpu_arch

<>

kvm_lapic

+unsigned long base_address

+struct kvm_io_device dev

+struct kvm_timer lapic_timer

+u32 divide_count

+struct kvm_vcpu *vcpu;

+struct page *regs_page

+void *regs

+gpa_t vapic_addr

+struct page *vapic_page

<>

kvm_pv_mmu_op_buffer

+void *ptr

+unsigned len

+unsigned processed

+char buf[512] __aligned(sizeof(long))

<>

kvm_mmu

+void (*new_cr3)(struct kvm_vcpu *vcpu)

+int (*page_fault)(struct kvm_vcpu *vcpu, gva_t gva, u32 err)

+void (*free)(struct kvm_vcpu *vcpu)

+gpa_t (*gva_to_gpa)(struct kvm_vcpu *vcpu, gva_t gva)

+void (*prefetch_page)(struct kvm_vcpu *vcpu,struct kvm_mmu_page *page)

+int (*sync_page)(struct kvm_vcpu *vcpu struct kvm_mmu_page *sp)

+void (*invlpg)(struct kvm_vcpu *vcpu, gva_t gva)

+hpa_t root_hpa;

+int root_level

+int shadow_root_level

+union kvm_mmu_page_role base_role

+u64 *pae_root

+u64 rsvd_bits_mask[2][4]

<>

kvm_queued_exception

+bool pending

+bool has_error_code

+u8 nr

+u32 error_code

<>

kvm_pio_request

+unsigned long count

+int cur_count

+gva_t guest_gva

+int in

+int port

+int size

+int string

+int down

+int rep

kvm_mmu_memory_cache

-int nobjs

-void *objects[KVM_NR_MEM_OBJS]

<>

update_pte

+gfn_t gfn

+pfn_t pfn

+int largepage

+unsigned long mmu_seq

<>

kvm_queued_interrupt

+bool pending

+u8 nr

decode_cache

-u8 twobyte

-u8 b

-u8 lock_prefix

-u8 rep_prefix

-u8 op_bytes

-u8 ad_bytes

-u8 rex_prefix

-struct operand src

-struct operand src2

-struct operand dst

-bool has_seg_override

-u8 seg_override

-unsigned int d

-unsigned long regs[NR_VCPU_REGS]

-unsigned long eip

-u8 modrm

-u8 modrm_mod

-u8 modrm_reg

-u8 modrm_rm

-u8 use_modrm_ea

-bool rip_relative

-unsigned long modrm_ea

-void *modrm_ptr

-unsigned long modrm_val

-struct fetch_cache fetch

<>

x86_emulate_ctxt

+struct kvm_vcpu *vcpu;

+unsigned long eflags

+int mode

+u32 cs_base

+struct decode_cache decode

rmode

-int active

-u8 save_iopl

-struct kvm_save_segment

<>

kvm_save_segment

+u16 selector

+unsigned long base

+u32 limit

+u32 ar

<>

pvclock_vcpu_time_info

+u32 version

+u32 pad0

+u64 tsc_timestamp

+u64 system_time

+u32 tsc_to_system_mul

+s8 tsc_shift

+u8 pad[3]

<>

kvm_cpuid_entry2

+__u32 function

+__u32 index

+__u32 flags

+__u32 eax

+__u32 ebx

+__u32 ecx

+__u32 edx

+__u32 padding[3]

<>

mtrr_state_type

+struct mtrr_var_range var_ranges[MTRR_MAX_VAR_RANGES]

+mtrr_type fixed_ranges[MTRR_NUM_FIXED_RANGES]

+unsigned char enabled

+unsigned char have_fixed

+mtrr_type def_type

<>

kvm_io_device

+void (*read)(struct kvm_io_device *this,gpa_t addr, int len, void *val)

+void (*write)(struct kvm_io_device *this,gpa_t addr,int len,const void *val)

+int (*in_range)(struct kvm_io_device *this, gpa_t addr, int len,int is_write)

+void (*destructor)(struct kvm_io_device *this)

+void *private

<>

kvm_timer

+struct hrtimer timer

+s64 period

+atomic_t pending

+bool reinject

+struct kvm_timer_ops *t_ops

+struct kvm *kvm

+int vcpu_id

<>

kvm_timer_ops

+bool (*is_periodic)(struct kvm_timer *)

Kvm-vcpu-arch 包含数据结构


本文标签: 客户机 页表 内核 函数