⑶ 在cmdCreate主程序中有一个特别重要的函数:virDomainCreateXML(),这个函数的最初原型是: virDomainPtr virDomainCreateXML (virConnectPtr conn,const char*xmlDesc,unsigned int flags),这个函数是基于一个指定的XML文件来创建一个虚拟机,其中conn表示一个指向hypervisor的连接,xmlDesc表示一个 XML文件,flags表示命令选项的标志。
2.2 通过libvirt创建虚拟机的关键API
通过分析2.1中的virsh源码我们可以看出,使用libvirt进行虚拟机创建要调用两个关键的API-- virFileReadAll和virDomainCreateXML,下面分别进行说明。
2.2.1 virFileReadAll
该函数原型为intvirFileReadAll(const char *path, int maxlen, char **buf),功能是将参数“path”指定路径的文件内容读到一个缓冲区中,并将缓冲区地址记录在参数“*buf”中,而参数“maxlen”指定文件的最大长度。利用该API,我们可以将xml配置文件都到一个缓冲区中,以方便接下来的使用。
2.2.2virDomainCreateXML
该函数原型为virDomainPtr virDomainCreateXML (virConnectPtrconn, const char * xmlDesc, unsigned int flags),功能是根据参数“xmlDesc”定义的配置方式创建一个域并返回该域的指针。参数“conn”是指向虚拟机管理器的指针,而通过设置不同的“flags”标志,可以使创建的域具有不同的属性。
三. 利用libvirt库编写自己的虚拟机创建程序
Virsh命令用来创建虚拟机的命令是:virsh create,这个命令主要是从给定的XML文件生成客户端并启动客户端。
下面用一个测试例子来说明如何通过virsh命令来创建虚拟机的。
具体的操作实践步骤是:
- 首先需要创建虚拟硬盘,为了放置操作系统的地方,命令是:kvm-img create
701.img10G,也就是创建一个大小为10G的虚拟硬盘。
2. 编写一个xml文件,这个文件里面包含启动操作系统的一些特征,比如:内存容量,操作系统位置,虚拟硬盘位置等等,其实有很多的字段,可以简写一个xml 文件,如果有些字段没有定义,那么系统就会默认,下面给出一个xml文件,命名为701.xml,程序为:
<domain type='qemu'>
<name>linux10.0421</name>
<uuid></uuid>
<memory>512000</memory>
<currentMemory>512000</currentMemory>
<vcpu>1</vcpu>
<os>
<type arch='i686' machine='pc'>hvm</type>
<boot dev='cdrom'/>
<boot dev='hd'/>
</os>
<devices>
<emulator>/usr/bin/qemu-system-x86_64</emulator>
<disk type='file' device='cdrom'>
<source file='/usr/src/ubuntu-10.04-desktop-i386.iso'/>
<target dev='hdc'/>
<readonly/>
</disk>
<disk type='file' device='disk'>
<sourcefile='/var/lib/libvirt/images/701.img'/>
<target dev='hda'/>
</disk>
<graphics type='vnc' port='5901'listen='127.0.0.1'/>
</devices>
</domain>
3. 接着编写一个c文件,名称为701.c这个文件主要实现的功能就是调用这个xml文件来创建并启动虚拟机。这个c程序代码为:
#include<stdio.h>
#include<stdlib.h>
#include<memory.h>
#include<libvirt/libvirt.h>
const char *from=NULL;
static virConnectPtr conn=NULL;
#define VIRSH_MAX_XML_FILE 10*1024*1024
void closeConn()
{
if(conn!=NULL)
virConnectClose(conn);
}
int cmdCreate()
{
virDomainPtr dom;
char *buffer;
unsigned int flags=VIR_DOMAIN_NONE;
conn=virConnectOpen("qemu:///system");
if(conn==NULL)
{
fprintf(stderr,"failed to connect tohypervisor/n");
closeConn();
return 0;
}
if(virFileReadAll(from,VIRSH_MAX_XML_FILE,&buffer)<0)
return 0;
dom=virDomainCreateXML(conn,buffer,flags);
memset(buffer,0,sizeof(buffer));
if(dom!=NULL){
fprintf(stdout,"Domain %screated from %s\n",virDomainGetName(dom),from);
virDomainFree(dom);
}
else{
fprintf(stdout,"Failed to createdomain from %s",from);
}
}
int main(int argc,char *argv[])
{
if(argc<2){
fprintf(stdout,"there are too fewparameters,should has two more parameters!");
}
from=*++argv;
cmdCreate();
return 0;
}
4. 在命令窗口中先执行gcc -lvirt -o 701 701.c ,然后执行./701 701.xml,就可以看到这个虚拟机被创建并启动起来了。
四.KVM内核如何实现底层虚拟机创建功能
4.1 KVM虚拟机创建和运行虚拟机的流程
开源的Lbvirt库实现了很多的虚拟化API,这些API的实现还是要靠底层的KVM内核的实现,下面重点讲讲KVM内核中是如何实现虚拟机创建和运行功能的操作系统层的实现。
KVM虚拟机创建和运行虚拟机分为用户态和核心态两个部分,用户态主要提供应用程序接口,为虚拟机创建虚拟机上下文环境,在libkvm中提供访问内核字符设备/dev/kvm的接口;内核态为添加到内核中的字符设备/dev/kvm,模块加载进内核后,即可进行接口用户空间调用创建虚拟机。在创建虚拟机过程中,kvm字符设备主要为客户机创建kvm数据结构,创建该虚拟机的虚拟机文件描述符及其相应的数据结构以及创建虚拟机处理器及其相应的数据结构。 kvm创建虚拟机的流程如下图:
根据上图就可以大致知道虚拟机创建和运行的流程了。首先申明一个kvm_context_t变量用以描述用户态虚拟机上下文信息,然后调用 kvm_init()函数初始化虚拟机上下文信息;函数kvm_create()创建虚拟机实例,该函数通过ioctl系统调用创建虚拟机相关的内核数据结构并且返回文件描述符给用户态kvm_context_t数据结构;创建完内核虚拟机数据结构后,再创建内核pit以及mmio等外设模拟设备,然后调用kvm_create_vcpu()函数来创建虚拟处理器,kvm_create_vcpu()函数通过系统调用向由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()函数继续调度客户机的执行。
4.2 KVM虚拟机创建和运行虚拟机的主要函数分析以及流程
1.函数kvm_init():该函数在用户态创建一个虚拟机上下文,用以在用户态保存基本的虚拟机信息,这个函数是创建虚拟机的第一个需要调用的函数,函数返回一个kvm_context_t结构体。该函数原型的实现在libkvm.c中,该函数原型是:
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的标志位以及内存页面记录的标志位。
用户态数据结构kvm_context_t用以描述虚拟机实例的用户态上下文信息。在kvm_common.h文件里面有kvm_context的结构体定义。
structkvm_context {
/// Filedescriptor to /dev/kvm
int fd;
int vm_fd;
int vcpu_fd[MAX_VCPUS];
struct kvm_run *run[MAX_VCPUS];
/// Callbacks that KVM uses to emulatevarious unvirtualizable functionality
struct kvm_callbacks *callbacks;
void *opaque;
/// A pointer to the memory used as thephysical memory for the guest
void *physical_memory;
/// is dirty pages logging enabled for allregions or not
int dirty_pages_log_all;
/// memory regions parameters
struct kvm_memory_regionmem_regions[KVM_MAX_NUM_MEM_REGIONS];
/// do not create in-kernel irqchip if set
int no_irqchip_creation;
/// in-kernel irqchip status
int irqchip_in_kernel;
};
各个数据域的解释为:
int fd :指向内核标准字符设备/dev/kvm的文件描述符。
int vm_fd:指向所创建的内核虚拟机数据结构相关文件的文件描述符。
intvcpu_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的状态
structkvm_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_taddr, uint8_t *data,int len):用于模拟客户机执行mmio读指令。
int (*mmio_write)(void *opaque, uint64_taddr, 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_tdcrn, uint32_t *data);用于powerpc的dcr读操作
nt (*powerpc_dcr_write)(int vcpu, uint32_tdcrn, uint32_t data);用于powerpc的dcr写操作
int (*s390_handle_intercept)(kvm_context_tcontext, int vcpu,struct kvm_run *run);用于s390的中断处理。
int (*s390_handle_reset)(kvm_context_tcontext, 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 {
__u64hardware_entry_failure_reason;
} fail_entry;
/* KVM_EXIT_EXCEPTION */ 客户机因为异常退出
struct {
__u32exception;
__u32error_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_runstart */
} 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 {
__u64rip;
__u32is_write;
__u32pad;
} 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 16
__u64 s390_reset_flags;
/* KVM_EXIT_DCR */
struct {
__u32dcrn;
__u32data;
__u8 is_write;
} dcr;
/* Fix the size of the union. */
char padding[256];
2. 函数kvm_create():该函数主要用于创建一个虚拟机内核环境。该函数原型为:
int kvm_create(kvm_context_t kvm,unsignedlong 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 intioctl, unsignedlong 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, intslot);
参数: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_tkvm,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以及停机等事件。