kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

编译qemu-kvm和安装qemu-kvm

http://smilejay.com/2012/06/qemu-kvm_compilation_installation/

3.4 编译和安装qemu-kvm

除了在内核空间的KVM模块之外,在用户空间需要QEMU[注6]来模拟所需要CPU和设备模型以及用于启动客户机进程,这样才有了一个完整的KVM运行环境。而qemu-kvm是为了针对KVM专门做了修改和优化的QEMU分支[注7],在本书写作的2012年,qemu-kvm分支里面的小部分特性还没有完全合并进入到qemu的主干代码之中,所以本书中采用qemu-kvm来讲解。

在编译和安装了KVM并且启动到编译的内核之后,下面来看一下qemu-kvm的编译和安装。

3.4.1 下载qemu-kvm源代码

目前的QEMU项目针对KVM的代码分支qemu-kvm也是由Redhat公司的Gleb Natapov和Marcelo Tosatti作维护者(Maintainer),代码也是托管在kernel.org上。

qemu-kvm开发代码仓库的网页连接为:http://git.kernel.org/?p=virt/kvm/qemu-kvm.git

其中,可以看到有如下3个URL连接可供下载开发中的最新qemu-kvm的代码仓库。 git://git.kernel.org/pub/scm/virt/kvm/qemu-kvm.git
http://git.kernel.org/pub/scm/virt/kvm/qemu-kvm.git
https://git.kernel.org/pub/scm/virt/kvm/qemu-kvm.git

可以根据自己实际需要选择3个中任一个用git clone命令下载即可,它们是完全一样的。

另外,可以到sourceforge.net的如下链接中根据需要下载qemu-kvm各个发布版本的代码压缩包(作者建议使用最新的正式发布版本,因为它的功能更多,同时也比较稳定)。

http://sourceforge.net/projects/kvm/files/qemu-kvm/

在本节讲解编译时,以下载开发中的最新的qemu-kvm.git为例,获取其代码仓库过程如下:

1
2
3
4
5
6
7
8
9
10
[root@jay-linux kvm_demo]# git clone\ git://git.kernel.org/pub/scm/virt/kvm/qemu-kvm.git qemu-kvm.git
Initialized empty Git repository in /root/kvm_demo/qemu-kvm.git/.git/
remote: Counting objects: 145222, done.
remote: Compressing objects: 100% (35825/35825), done.
remote: Total 145222 (delta 114656), reused 137663 (delta 107444)
Receiving objects: 100% (145222/145222), 40.83 MiB | 10.33 MiB/s, done.
Resolving deltas: 100% (114656/114656), done.
[root@jay-linux kvm_demo]# cd qemu-kvm.git
[root@jay-linux kvm.git]# pwd
/root/kvm_demo/qemu-kvm.git
3.4.2 配置和编译qemu-kvm

qemu-kvm的配置并不复杂,通常情况下,可以直接运行代码仓库中configure文件进行配置即可。当然,如果对其配置并不熟悉,可以运行“./configure –help”命令查看配置的一些选项及其帮助信息。

显示配置的帮助手册信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[root@jay-linux qemu-kvm.git]# ./configure --help
Usage: configure [options]
Options: [defaults in brackets after descriptions]
 
Standard options:
--help                   print this message
--prefix=PREFIX          install in PREFIX [/usr/local]
--interp-prefix=PREFIX   where to find shared libraries, etc.
use %M for cpu name [/usr/gnemul/qemu-%M]
--target-list=LIST       set target list (default: build everything)
Available targets: i386-softmmu x86_64-softmmu
<!- 此处省略百余行帮助信息的输出 ->
--disable-guest-agent    disable building of the QEMU Guest Agent
--enable-guest-agent     enable building of the QEMU Guest Agent
--with-coroutine=BACKEND coroutine backend. Supported options:
gthread, ucontext, sigaltstack, windows

NOTE: The object files are built at the place where configure is launched

执行configure文件进行配置的过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@jay-linux qemu-kvm.git]# ./configure
Install prefix    /usr/local
BIOS directory    /usr/local/share/qemu
binary directory  /usr/local/bin
library directory /usr/local/lib
include directory /usr/local/include
config directory  /usr/local/etc
Manual directory  /usr/local/share/man
ELF interp prefix /usr/gnemul/qemu-%M
Source path       /root/kvm_demo/qemu-kvm.git
C compiler        gcc
Host C compiler   gcc
<!– 此处省略数十行 –>
VNC support       yes     #通常需要通过VNC连接到客户机中
<!– 此处省略十余行 –>
KVM support       yes     #这是对KVM的支持
TCG interpreter   no
KVM device assig. yes    #这是对KVM中VT-d功能的支持
<!– 此处省略十余行 –>
OpenGL support    yes
libiscsi support  no
build guest agent yes
coroutine backend ucontext

需要注意的是,上面命令行输出的KVM相关的选项需要是配置为yes,另外,一般VNC的支持也是配置为yes的(因为通常需要用VNC连接到客户机中)。

【2013.05.13 updated】 在configure时,可能遇到“glib-2.12 required to compile QEMU”的错误,是需要安装glib2和glib2-dev软件包,在RHEL上的安装命令为“yum install glib2 glib2-devel”,在Ubuntu上安装的过程为“apt-get install libglib2.0 libglib2.0-dev”。

经过配置之后,进行编译就很简单了,直接执行make即可进行编译,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[root@jay-linux qemu-kvm.git]# make -j 20
GEN   config-host.h
GEN   trace.h
GEN   qemu-options.def
GEN   qmp-commands.h
GEN   qapi-types.h
GEN   qapi-visit.h
GEN   tests/test-qapi-types.h
GEN   tests/test-qapi-visit.h
GEN   tests/test-qmp-commands.h
CC    libcacard/cac.o
CC    libcacard/event.o
<!– 此处省略数百行的编译时输出信息 –>
CC    x86_64-softmmu/target-i386/cpu.o
CC    x86_64-softmmu/target-i386/machine.o
CC    x86_64-softmmu/target-i386/arch_memory_mapping.o
CC    x86_64-softmmu/target-i386/arch_dump.o
CC    x86_64-softmmu/target-i386/kvm.o
CC    x86_64-softmmu/target-i386/hyperv.o
LINK  x86_64-softmmu/qemu-system-x86_64

可以看到,最后有编译生成qemu-system-x86_64文件,它就是我们常用的qemu-kvm的命令行工具(在多数Linux发行版中自带的qemu-kvm软件包的命令行是qemu-kvm,只是名字不同而已)。

3.4.2 安装qemu-kvm

编译完成之后,运行“make install”命令即可安装qemu-kvm,其过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
[root@jay-linux qemu-kvm.git]# make install | tee make-install.log
install -d -m 0755 “/usr/local/share/qemu”
install -d -m 0755 “/usr/local/etc/qemu”
install -c -m 0644 /root/kvm_demo/qemu-kvm.git/sysconfigs/target/target-x86_64.conf “/usr/local/etc/qemu”
install -c -m 0644 /root/kvm_demo/qemu-kvm.git/sysconfigs/target/cpus-x86_64.conf “/usr/local/share/qemu”
install -d -m 0755 “/usr/local/bin”
install -c -m 0755  vscclient qemu-ga qemu-nbd qemu-img qemu-io  “/usr/local/bin”
install -d -m 0755 “/usr/local/libexec”
<!– 此处省略数行的安装时输出信息 –>
make[1]: Entering directory `/root/kvm_demo/qemu-kvm.git/x86_64-softmmu’
install -m 755 qemu-system-x86_64 “/usr/local/bin”
strip “/usr/local/bin/qemu-system-x86_64″
make[1]: Leaving directory `/root/kvm_demo/qemu-kvm.git/x86_64-softmmu’

qemu-kvm的安装过程的主要任务有这几个:创建qemu的一些目录,拷贝一些配置文件到相应的目录下,拷贝一些firmware文件(如:sgabios.bin, kvmvapic.bin)到目录下以便qemu-kvm的命令行启动时可以找到对应的固件提供给客户机使用,拷贝keymaps到相应的目录下以便在客户机中支持各种所需键盘类型,拷贝qemu-system-x86_64、qemu-img等可执行程序到对应的目录下。下面的一些命令行检查了qemu-kvm被安装了之后的系统状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@jay-linux qemu-kvm.git]# which qemu-system-x86_64
/usr/local/bin/qemu-system-x86_64
[root@jay-linux qemu-kvm.git]# which qemu-img
/usr/local/bin/qemu-img
[root@jay-linux qemu-kvm.git]# ls /usr/local/share/qemu/
bamboo.dtb        mpc8544ds.dtb     petalogix-ml605.dtb       pxe-pcnet.rom    slof.bin            vgabios-vmware.bin
bios.bin          multiboot.bin     petalogix-s3adsp1800.dtb  pxe-rtl8139.rom  spapr-rtas.bin
cpus-x86_64.conf  openbios-ppc      ppc_rom.bin               pxe-virtio.rom   vgabios.bin
keymaps           openbios-sparc32  pxe-e1000.rom             qemu-icon.bmp    vgabios-cirrus.bin
kvmvapic.bin      openbios-sparc64  pxe-eepro100.rom          s390-zipl.rom    vgabios-qxl.bin
linuxboot.bin     palcode-clipper   pxe-ne2k_pci.rom          sgabios.bin      vgabios-stdvga.bin
[root@jay-linux qemu-kvm.git]# ls /usr/local/share/qemu/keymaps/
ar    common  de     en-gb  es  fi  fr     fr-ca  hr  is  ja  lv  modifiers  nl-be  pl  pt-br  sl  th
bepo  da      de-ch  en-us  et  fo  fr-be  fr-ch  hu  it  lt  mk  nl         no     pt  ru     sv  tr

由于qemu-kvm是用户空间的程序,安装之后不用重启系统,直接用qemu-system-x86_64、qemu-img这样的命令行工具即可使用qemu-kvm了。

KVM源代码分析4:内存虚拟化

http://www.oenhan.com/kvm-src-4-mem

在虚拟机的创建与运行中pc_init_pci负责在qemu中初始化虚拟机,内存初始化也是在这里完成的,还是一步步从qemu说起,在vl.c的main函数中有ram_size参数,由qemu入参标识QEMU_OPTION_m设定,顾名思义就是虚拟机内存的大小,通过machine->init一步步传递给pc_init1函数。在这里分出了above_4g_mem_size和below_4g_mem_size,即高低端内存(也不一定是32bit机器..),然后开始初始化内存,即pc_memory_init,内存通过memory_region_init_ram下面的qemu_ram_alloc分配,使用qemu_ram_alloc_from_ptr。

插播qemu对内存条的模拟管理,是通过RAMBlock和ram_list管理的,RAMBlock就是每次申请的内存池,ram_list则是RAMBlock的链表,他们结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct RAMBlock {
	//对应宿主的内存地址
	uint8_t *host;
	//block在ramlist中的偏移
	ram_addr_t offset;
	//block长度
	ram_addr_t length;
	uint32_t flags;
	//block名字
	char idstr[256];
	QLIST_ENTRY(RAMBlock) next;
#if defined(__linux__) && !defined(TARGET_S390X)
	int fd;
#endif
} RAMBlock;

typedef struct RAMList {
	//看代码理解就是list的head,但是不知道为啥叫dirty...
	uint8_t *phys_dirty;
	QLIST_HEAD(ram, RAMBlock) blocks;
} RAMList;

下面再回到qemu_ram_alloc_from_ptr函数,使用find_ram_offset赋值给new block的offset,find_ram_offset具体工作模型已经在KVM源代码分析2:虚拟机的创建与运行中提到了,不赘述。然后是一串判断,在kvm_enabled的情况下使用new_block->host = kvm_vmalloc(size),最终内存是qemu_vmalloc分配的,使用qemu_memalign干活。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void *qemu_memalign(size_t alignment, size_t size)
{
	void *ptr;
	//使用posix进行内存针对页大小对齐
#if defined(_POSIX_C_SOURCE) && !defined(__sun__)
	int ret;
	ret = posix_memalign(&ptr, alignment, size);
	if (ret != 0) {
		fprintf(stderr, "Failed to allocate %zu B: %s\n",
				size, strerror(ret));
		abort();
	}
#elif defined(CONFIG_BSD)
	ptr = qemu_oom_check(valloc(size));
#else
	//所谓检查oom就是看memalign对应malloc申请内存是否成功
	ptr = qemu_oom_check(memalign(alignment, size));
#endif
	trace_qemu_memalign(alignment, size, ptr);
	return ptr;
}

以上qemu_vmalloc进行内存申请就结束了。在qemu_ram_alloc_from_ptr函数末尾则是将block添加到链表,realloc整个ramlist,用memset初始化整个ramblock,madvise对内存使用限定。 然后一层层的退回到pc_memory_init函数。

此时pc.ram已经分配完成,ram_addr已经拿到了分配的内存地址,MemoryRegion ram初始化完成。下面则是对已有的ram进行分段,即ram-below-4g和ram-above-4g,也就是高端内存和低端内存。用memory_region_init_alias初始化子MemoryRegion,然后将memory_region_add_subregion添加关联起来,memory_region_add_subregion具体细节“KVM源码分析2”中已经说了,参考对照着看吧,中间很多映射代码过程也只是qemu遗留的软件实现,没看到具体存在的意义,直接看到kvm_set_user_memory_region函数,内核真正需要kvm_vm_ioctl传递过去的参数是什么, struct kvm_userspace_memory_region mem而已,也就是

1
2
3
4
5
6
7
struct kvm_userspace_memory_region {
	__u32 slot;
	__u32 flags;
	__u64 guest_phys_addr;
	__u64 memory_size;    /* bytes */
	__u64 userspace_addr; /* start of the userspace allocated memory */
};

kvm_vm_ioctl进入到内核是在KVM_SET_USER_MEMORY_REGION参数中,即执行kvm_vm_ioctl_set_memory_region,然后一直向下,到__kvm_set_memory_region函数,check_memory_region_flags检查mem->flags是否合法,而当前flag也就使用了两位,KVM_MEM_LOG_DIRTY_PAGES和KVM_MEM_READONLY,从qemu传递过来只能是KVM_MEM_LOG_DIRTY_PAGES,下面是对mem中各参数的合规检查,(mem->memory_size & (PAGE_SIZE – 1))要求以页为单位,(mem->guest_phys_addr & (PAGE_SIZE – 1))要求guest_phys_addr页对齐,而((mem->userspace_addr & (PAGE_SIZE – 1)) || !access_ok(VERIFY_WRITE,(void __user *)(unsigned long)mem->userspace_addr,mem->memory_size))则保证host的线性地址页对齐而且该地址域有写权限。

id_to_memslot则是根据qemu的内存槽号得到kvm结构下的内存槽号,转换关系来自id_to_index数组,那映射关系怎么来的,映射关系是一一对应的,在kvm_create_vm虚拟机创建过程中,kvm_init_memslots_id初始化对应关系,即slots->id_to_index[i] = slots->memslots[i].id = i,当前映射是没有意义的,估计是为了后续扩展而存在的。

扩充了new的kvm_memory_slot,下面直接在代码中注释更方便:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//映射内存有大小,不是删除内存条
if (npages) {
	//内存槽号没有虚拟内存条,意味内存新创建
	if (!old.npages)
		change = KVM_MR_CREATE;
	else { /* Modify an existing slot. */
		//修改已存在的内存修改标志或者平移映射地址
		//下面是不能处理的状态(内存条大小不能变,物理地址不能变,不能修改只读)
		if ((mem->userspace_addr != old.userspace_addr) ||
			(npages != old.npages) ||
			((new.flags ^ old.flags) & KVM_MEM_READONLY))
			goto out;
		//guest地址不同,内存条平移
		if (base_gfn != old.base_gfn)
			change = KVM_MR_MOVE;
		else if (new.flags != old.flags)
			//修改属性
			change = KVM_MR_FLAGS_ONLY;
		else { /* Nothing to change. */
			r = 0;
			goto out;
		}
	}
} else if (old.npages) {
	//申请插入的内存为0,而内存槽上有内存,意味删除
	change = KVM_MR_DELETE;
} else /* Modify a non-existent slot: disallowed. */
	goto out;

另外看kvm_mr_change就知道memslot的变动值了:

1
2
3
4
5
6
enum kvm_mr_change {
	KVM_MR_CREATE,
	KVM_MR_DELETE,
	KVM_MR_MOVE,
	KVM_MR_FLAGS_ONLY,
};

在往下是一段检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if ((change == KVM_MR_CREATE) || (change == KVM_MR_MOVE)) {
	/* Check for overlaps */
	r = -EEXIST;
	kvm_for_each_memslot(slot, kvm->memslots) {
		if ((slot->id >= KVM_USER_MEM_SLOTS) ||
			//下面排除掉准备操作的内存条,在KVM_MR_MOVE中是有交集的
			(slot->id == mem->slot))
			continue;
		//下面就是当前已有的slot与new在guest线性区间上有交集
		if (!((base_gfn + npages <= slot->base_gfn) ||
			  (base_gfn >= slot->base_gfn + slot->npages)))
			goto out;
			//out错误码就是EEXIST
	}
}

如果是新插入内存条,代码则走入kvm_arch_create_memslot函数,里面主要是一个循环,KVM_NR_PAGE_SIZES是分页的级数,此处是3,第一次循环,lpages = gfn_to_index(slot->base_gfn + npages – 1,slot->base_gfn, level) + 1,lpages就是一级页表所需要的page数,大致是npages>>09,然后为slot->arch.rmap[i]申请了内存空间,此处可以猜想,rmap就是一级页表了,继续看,lpages约为npages>>19,此处又多为lpage_info申请了同等空间,然后对lpage_info初始化赋值,现在看不到lpage_info的具体作用,看到后再补上。整体上看kvm_arch_create_memslot做了一个3级的软件页表。

如果有脏页,并且脏页位图为空,则分配脏页位图, kvm_create_dirty_bitmap实际就是”页数/8″.

1
2
3
4
if ((new.flags & KVM_MEM_LOG_DIRTY_PAGES) && !new.dirty_bitmap) {
	if (kvm_create_dirty_bitmap(&new) < 0)
		goto out_free;
}

当内存条的改变是KVM_MR_DELETE或者KVM_MR_MOVE,先申请一个slots,把kvm->memslots暂存到这里,首先通过id_to_memslot获取准备插入的内存条对应到kvm的插槽是slot,无论删除还是移动,将其先标记为KVM_MEMSLOT_INVALID,然后是install_new_memslots,其实就是更新了一下slots->generation的值,

KVM源代码分析3:CPU虚拟化

http://www.oenhan.com/kvm-src-3-cpu

在虚拟机的创建与运行章节里面笼统的介绍了KVM在qemu中的创建和运行,基本的qemu代码流程已经梳理清楚,后续主要写一些硬件虚拟化的原理和代码流程,主要写原理和qemu控制KVM运行的的ioctl接口,后续对内核代码的梳理也从这些接口下手。

1.VT-x 技术

Intel处理器支持的虚拟化技术即是VT-x,之所以CPU支持硬件虚拟化是因为软件虚拟化的效率太低。

处理器虚拟化的本质是分时共享,主要体现在状态恢复和资源隔离,实际上每个VM对于VMM看就是一个task么,之前Intel处理器在虚拟化上没有提供默认的硬件支持,传统 x86 处理器有4个特权级,Linux使用了0,3级别,0即内核,3即用户态,(更多参考CPU的运行环、特权级与保护)而在虚拟化架构上,虚拟机监控器的运行级别需要内核态特权级,而CPU特权级被传统OS占用,所以Intel设计了VT-x,提出了VMX模式,即VMX root operation 和 VMX non-root operation,虚拟机监控器运行在VMX root operation,虚拟机运行在VMX non-root operation。每个模式下都有相对应的0~3特权级。

为什么引入这两种特殊模式,在传统x86的系统中,CPU有不同的特权级,是为了划分不同的权限指令,某些指令只能由系统软件操作,称为特权指令,这些指令只能在最高特权级上才能正确执行,反之则会触发异常,处理器会陷入到最高特权级,由系统软件处理。还有一种需要操作特权资源(如访问中断寄存器)的指令,称为敏感指令。OS运行在特权级上,屏蔽掉用户态直接执行的特权指令,达到控制所有的硬件资源目的;而在虚拟化环境中,VMM控制所有所有硬件资源,VM中的OS只能占用一部分资源,OS执行的很多特权指令是不能真正对硬件生效的,所以原特权级下有了root模式,OS指令不需要修改就可以正常执行在特权级上,但这个特权级的所有敏感指令都会传递到root模式处理,这样达到了VMM的目的。

在KVM源代码分析1:基本工作原理章节中也说了kvm分3个模式,对应到VT-x 中即是客户模式对应vmx非root模式,内核模式对应VMX root模式下的0特权级,用户模式对应vmx root模式下的3特权级。

如下图

在非根模式下敏感指令引发的陷入称为VM-Exit,VM-Exit发生后,CPU从非根模式切换到根模式;对应的,VM-Entry则是从根模式到非根模式,通常意味着调用VM进入运行态。VMLAUCH/VMRESUME命令则是用来发起VM-Entry。

2.VMCS

VMCS保存虚拟机的相关CPU状态,每个VCPU都有一个VMCS(内存的),每个物理CPU都有VMCS对应的寄存器(物理的),当CPU发生VM-Entry时,CPU则从VCPU指定的内存中读取VMCS加载到物理CPU上执行,当发生VM-Exit时,CPU则将当前的CPU状态保存到VCPU指定的内存中,即VMCS,以备下次VMRESUME。

VMLAUCH指VM的第一次VM-Entry,VMRESUME则是VMLAUCH之后后续的VM-Entry。VMCS下有一些控制域:

VM-execution controls Determines what operations cause VM exits CR0, CR3, CR4, Exceptions, IO Ports, Interrupts, Pin Events, etc
Guest-state area Saved on VM exits,Reloaded on VM entry EIP, ESP, EFLAGS, IDTR, Segment Regs, Exit info, etc
Host-state area Loaded on VM exits CR3, EIP set to monitor entry point, EFLAGS hardcoded, etc
VM-exit controls Determines which state to save, load, how to transition Example: MSR save-load list
VM-entry controls Determines which state to load, how to transition Including injecting events (interrupts, exceptions) on entry

关于具体控制域的细节,还是翻Intel手册吧。

3.VM-Entry/VM-Exit

VM-Entry是从根模式切换到非根模式,即VMM切换到guest上,这个状态由VMM发起,发起之前先保存VMM中的关键寄存器内容到VMCS中,然后进入到VM-Entry,VM-Entry附带参数主要有3个:1.guest是否处于64bit模式,2.MSR VM-Entry控制,3.注入事件。1应该只在VMLAUCH有意义,3更多是在VMRESUME,而VMM发起VM-Entry更多是因为3,2主要用来每次更新MSR。

VM-Exit是CPU从非根模式切换到根模式,从guest切换到VMM的操作,VM-Exit触发的原因就很多了,执行敏感指令,发生中断,模拟特权资源等。

运行在非根模式下的敏感指令一般分为3个方面:

1.行为没有变化的,也就是说该指令能够正确执行。

2.行为有变化的,直接产生VM-Exit。

3.行为有变化的,但是是否产生VM-Exit受到VM-Execution控制域控制。

主要说一下”受到VM-Execution控制域控制”的敏感指令,这个就是针对性的硬件优化了,一般是1.产生VM-Exit;2.不产生VM-Exit,同时调用优化函数完成功能。典型的有“RDTSC指令”。除了大部分是优化性能的,还有一小部分是直接VM-Exit执行指令结果是异常的,或者说在虚拟化场景下是不适用的,典型的就是TSC offset了。

VM-Exit发生时退出的相关信息,如退出原因、触发中断等,这些内容保存在VM-Exit信息域中。

4.KVM_CREATE_VM

创建VM就写这里吧,kvm_dev_ioctl_create_vm函数是主干,在kvm_create_vm中,主要有两个函数,kvm_arch_init_vm和hardware_enable_all,需要注意,但是更先一步的是KVM结构体,下面的struct是精简后的版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct kvm {
	struct mm_struct *mm; /* userspace tied to this vm */
	struct kvm_memslots *memslots;  /*qemu模拟的内存条模型*/
	struct kvm_vcpu *vcpus[KVM_MAX_VCPUS]; /* 模拟的CPU */
	atomic_t online_vcpus;
	int last_boosted_vcpu;
	struct list_head vm_list;  //HOST上VM管理链表,
	struct kvm_io_bus *buses[KVM_NR_BUSES];
	struct kvm_vm_stat stat;
	struct kvm_arch arch; //这个是host的arch的一些参数
	atomic_t users_count;
 
	long tlbs_dirty;
	struct list_head devices;
};

kvm_arch_init_vm基本没有特别动作,初始化了KVM->arch,以及更新了kvmclock函数,这个另外再说。

而hardware_enable_all,针对于每个CPU执行“on_each_cpu(hardware_enable_nolock, NULL, 1)”,在hardware_enable_nolock中先把cpus_hardware_enabled置位,进入到kvm_arch_hardware_enable中,有hardware_enable和TSC初始化规则,主要看hardware_enable,crash_enable_local_vmclear清理位图,判断MSR_IA32_FEATURE_CONTROL寄存器是否满足虚拟环境,不满足则将条件写入到寄存器内,CR4将X86_CR4_VMXE置位,另外还有kvm_cpu_vmxon打开VMX操作模式,外层包了vmm_exclusive的判断,它是kvm_intel.ko的外置参数,默认唯一,可以让用户强制不使用VMM硬件支持。

5.KVM_CREATE_VCPU

kvm_vm_ioctl_create_vcpu主要有三部分,kvm_arch_vcpu_create,kvm_arch_vcpu_setup和kvm_arch_vcpu_postcreate,重点自然是kvm_arch_vcpu_create。老样子,在这之前先看一下VCPU的结构体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
struct kvm_vcpu {
	struct kvm *kvm;  //归属的KVM
#ifdef CONFIG_PREEMPT_NOTIFIERS
	struct preempt_notifier preempt_notifier;
#endif
	int cpu;
	int vcpu_id;
	int srcu_idx;
	int mode;
	unsigned long requests;
	unsigned long guest_debug;
 
	struct mutex mutex;
	struct kvm_run *run;  //运行时的状态
 
	int fpu_active;
	int guest_fpu_loaded, guest_xcr0_loaded;
	wait_queue_head_t wq; //队列
	struct pid *pid;
	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_cur_fragment;
	int mmio_nr_fragments;
	struct kvm_mmio_fragment mmio_fragments[KVM_MAX_MMIO_FRAGMENTS];
#endif
 
#ifdef CONFIG_KVM_ASYNC_PF
	struct {
		u32 queued;
		struct list_head queue;
		struct list_head done;
		spinlock_t lock;
	} async_pf;
#endif
 
#ifdef CONFIG_HAVE_KVM_CPU_RELAX_INTERCEPT
	/*
	 * Cpu relax intercept or pause loop exit optimization
	 * in_spin_loop: set when a vcpu does a pause loop exit
	 *  or cpu relax intercepted.
	 * dy_eligible: indicates whether vcpu is eligible for directed yield.
	 */
	struct {
		bool in_spin_loop;
		bool dy_eligible;
	} spin_loop;
#endif
	bool preempted;
	struct kvm_vcpu_arch arch;  //当前VCPU虚拟的架构,默认介绍X86
};

借着看kvm_arch_vcpu_create,它借助kvm_x86_ops->vcpu_create即vmx_create_vcpu完成任务,vmx是X86硬件虚拟化层,从代码看,qemu用户态是一层,kernel 中KVM通用代码是一层,类似kvm_x86_ops是一层,针对各个不同硬件架构,而vcpu_vmx则是具体架构的虚拟化方案一层。首先是kvm_vcpu_init初始化,主要是填充结构体,可以注意的是vcpu->run分派了一页内存,下面有kvm_arch_vcpu_init负责填充x86 CPU结构体,下面就是kvm_vcpu_arch:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
struct kvm_vcpu_arch {
	/*
	 * 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;
	//类似这些寄存器就是就是用来缓存真正的CPU值的
	unsigned long cr0;
	unsigned long cr0_guest_owned_bits;
	unsigned long cr2;
	unsigned long cr3;
	unsigned long cr4;
	unsigned long cr4_guest_owned_bits;
	unsigned long cr8;
	u32 hflags;
	u64 efer;
	u64 apic_base;
	struct kvm_lapic *apic;    /* kernel irqchip context */
	unsigned long apic_attention;
	int32_t apic_arb_prio;
	int mp_state;
	u64 ia32_misc_enable_msr;
	bool tpr_access_reporting;
	u64 ia32_xss;
 
	/*
	 * Paging state of the vcpu
	 *
	 * If the vcpu runs in guest mode with two level paging this still saves
	 * the paging mode of the l1 guest. This context is always used to
	 * handle faults.
	 */
	struct kvm_mmu mmu; //内存管理,更多的是附带了直接操作函数
 
	/*
	 * Paging state of an L2 guest (used for nested npt)
	 *
	 * This context will save all necessary information to walk page tables
	 * of the an L2 guest. This context is only initialized for page table
	 * walking and not for faulting since we never handle l2 page faults on
	 * the host.
	 */
	struct kvm_mmu nested_mmu;
 
	/*
	 * Pointer to the mmu context currently used for
	 * gva_to_gpa translations.
	 */
	struct kvm_mmu *walk_mmu;
 
	struct kvm_mmu_memory_cache mmu_pte_list_desc_cache;
	struct kvm_mmu_memory_cache mmu_page_cache;
	struct kvm_mmu_memory_cache mmu_page_header_cache;
 
	struct fpu guest_fpu;
	u64 xcr0;
	u64 guest_supported_xcr0;
	u32 guest_xstate_size;
 
	struct kvm_pio_request pio;
	void *pio_data;
 
	u8 event_exit_inst_len;
 
	struct kvm_queued_exception {
		bool pending;
		bool has_error_code;
		bool reinject;
		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];
 
	int maxphyaddr;
 
	/* emulate context */
	//下面是KVM的软件模拟模式,也就是没有vmx的情况,估计也没人用这一套
	struct x86_emulate_ctxt emulate_ctxt;
	bool emulate_regs_need_sync_to_vcpu;
	bool emulate_regs_need_sync_from_vcpu;
	int (*complete_userspace_io)(struct kvm_vcpu *vcpu);
 
	gpa_t time;
	struct pvclock_vcpu_time_info hv_clock;
	unsigned int hw_tsc_khz;
	struct gfn_to_hva_cache pv_time;
	bool pv_time_enabled;
	/* set guest stopped flag in pvclock flags field */
	bool pvclock_set_guest_stopped_request;
 
	struct {
		u64 msr_val;
		u64 last_steal;
		u64 accum_steal;
		struct gfn_to_hva_cache stime;
		struct kvm_steal_time steal;
	} st;
 
	u64 last_guest_tsc;
	u64 last_host_tsc;
	u64 tsc_offset_adjustment;
	u64 this_tsc_nsec;
	u64 this_tsc_write;
	u64 this_tsc_generation;
	bool tsc_catchup;
	bool tsc_always_catchup;
	s8 virtual_tsc_shift;
	u32 virtual_tsc_mult;
	u32 virtual_tsc_khz;
	s64 ia32_tsc_adjust_msr;
 
	atomic_t nmi_queued;  /* unprocessed asynchronous NMIs */
	unsigned nmi_pending; /* NMI queued after currently running handler */
	bool nmi_injected;    /* Trying to inject an NMI this entry */
 
	struct mtrr_state_type mtrr_state;
	u64 pat;
 
	unsigned switch_db_regs;
	unsigned long db[KVM_NR_DB_REGS];
	unsigned long dr6;
	unsigned long dr7;
	unsigned long eff_db[KVM_NR_DB_REGS];
	unsigned long guest_debug_dr7;
 
	u64 mcg_cap;
	u64 mcg_status;
	u64 mcg_ctl;
	u64 *mce_banks;
 
	/* Cache MMIO info */
	u64 mmio_gva;
	unsigned access;
	gfn_t mmio_gfn;
	u64 mmio_gen;
 
	struct kvm_pmu pmu;
 
	/* used for guest single stepping over the given code position */
	unsigned long singlestep_rip;
 
	/* fields used by HYPER-V emulation */
	u64 hv_vapic;
 
	cpumask_var_t wbinvd_dirty_mask;
 
	unsigned long last_retry_eip;
	unsigned long last_retry_addr;
 
	struct {
		bool halted;
		gfn_t gfns[roundup_pow_of_two(ASYNC_PF_PER_VCPU)];
		struct gfn_to_hva_cache data;
		u64 msr_val;
		u32 id;
		bool send_user_only;
	} apf;
 
	/* OSVW MSRs (AMD only) */
	struct {
		u64 length;
		u64 status;
	} osvw;
 
	struct {
		u64 msr_val;
		struct gfn_to_hva_cache data;
	} pv_eoi;
 
	/*
	 * Indicate whether the access faults on its page table in guest
	 * which is set when fix page fault and used to detect unhandeable
	 * instruction.
	 */
	bool write_fault_to_shadow_pgtable;
 
	/* set at EPT violation at this point */
	unsigned long exit_qualification;
 
	/* pv related host specific info */
	struct {
		bool pv_unhalted;
	} pv;
};

整个arch结构真是长,很适合凑篇幅,很多结构其他过程涉及到的再提吧,反正我也不知道。

kvm_arch_vcpu_init初始化了x86在虚拟化底层的实现函数,首先是pv和emulate_ctxt,这些不支持VMX下的模拟虚拟化,尤其是vcpu->arch.emulate_ctxt.ops = &emulate_ops,emulate_ops初始化虚拟化模拟的对象函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static struct x86_emulate_ops emulate_ops = {
	.read_std            = kvm_read_guest_virt_system,
	.write_std           = kvm_write_guest_virt_system,
	.fetch               = kvm_fetch_guest_virt,
	.read_emulated       = emulator_read_emulated,
	.write_emulated      = emulator_write_emulated,
	.cmpxchg_emulated    = emulator_cmpxchg_emulated,
	.invlpg              = emulator_invlpg,
	.pio_in_emulated     = emulator_pio_in_emulated,
	.pio_out_emulated    = emulator_pio_out_emulated,
	.get_segment         = emulator_get_segment,
	.set_segment         = emulator_set_segment,
	.get_cached_segment_base = emulator_get_cached_segment_base,
	.get_gdt             = emulator_get_gdt,
	.get_idt         = emulator_get_idt,
	.set_gdt             = emulator_set_gdt,
	.set_idt         = emulator_set_idt,
	.get_cr              = emulator_get_cr,
	.set_cr              = emulator_set_cr,
	.cpl                 = emulator_get_cpl,
	.get_dr              = emulator_get_dr,
	.set_dr              = emulator_set_dr,
	.set_msr             = emulator_set_msr,
	.get_msr             = emulator_get_msr,
	.halt                = emulator_halt,
	.wbinvd              = emulator_wbinvd,
	.fix_hypercall       = emulator_fix_hypercall,
	.get_fpu             = emulator_get_fpu,
	.put_fpu             = emulator_put_fpu,
	.intercept           = emulator_intercept,
	.get_cpuid           = emulator_get_cpuid,
};

x86_emulate_ops函数看看就好,实际上也很少有人放弃vmx直接软件模拟。后面又有mp_state,给pio_data分配了一个page,kvm_set_tsc_khz设置TSC,kvm_mmu_create则是初始化MMU的函数,里面的函数都是地址转换的重点,在内存虚拟化重点提到。kvm_create_lapic初始化lapic,初始化mce_banks结构,还有pv_time,xcr0,xstat,pmu等,类似x86硬件结构上需要存在的,OS底层需要看到的硬件名称都要有对应的软件结构。

回到vmx_create_vcpu,vmx的guest_msrs分配得到一个page,后面是vmcs的分配,vmx->loaded_vmcs->vmcs = alloc_vmcs(),alloc_vmcs为当前cpu执行alloc_vmcs_cpu,alloc_vmcs_cpu中alloc_pages_exact_node分配给vmcs,alloc_pages_exact_node调用__alloc_pages实现,原来以为vmcs占用了一个page,但此处从伙伴系统申请了2vmcs_config.order页,此处vmcs_config在setup_vmcs_config中初始化,vmcs_conf->order = get_order(vmcs_config.size),而vmcs_conf->size = vmx_msr_high & 0x1fff,又rdmsr(MSR_IA32_VMX_BASIC, vmx_msr_low, vmx_msr_high),此处size由于与0x1fff与运算,大小必然小于4k,order则为0,然来绕去还是一个page大小。这么做估计是为了兼容vmcs_config中的size计算。

下面根据vmm_exclusive进行kvm_cpu_vmxon,进入vmx模式,初始化loaded_vmcs,然后用kvm_cpu_vmxoff退出vmx模式。

vmx_vcpu_load加载VCPU的信息,切换到指定cpu,进入到vmx模式,将loaded_vmcs的vmcs和当前cpu的vmcs绑定到一起。vmx_vcpu_setup则是初始化vmcs内容,主要是赋值计算,下面的vmx_vcpu_put则是vmx_vcpu_load的反运算。下面还有一些apic,nested,pml就不说了。

vmx_create_vcpu结束就直接回到kvm_vm_ioctl_create_vcpu函数,下面是kvm_arch_vcpu_setup,整个就一条线到kvm_arch_vcpu_load函数,主要有kvm_x86_ops->vcpu_load(vcpu, cpu)和tsc处理,vcpu_load就是vmx_vcpu_load,刚说了,就是进入vcpu模式下准备工作。

kvm_arch_vcpu_setup后面是create_vcpu_fd为proc创建控制fd,让qemu使用。kvm_arch_vcpu_postcreate则是马后炮般,重新vcpu_load,写msr,tsc。

如此整个vcpu就创建完成了。

6.KVM_RUN

KVM run涉及内容也不少,先写完内存虚拟化之后再开篇专门写RUN流程。