基于web技术的操作系统安装器的设计

什么是基于web技术的安装器(web-based installer)?

传统的Linux操作系统安装需要启动一个LiveOS,然后在LiveOS中运行一个本地安装程序,如Fedora下的Anaconda. LiveOS除了让用户在安装操作系统之前能预先体验之外,也为安装器提供了运行环境。这对于桌面操作系统已然足够,因为PC、笔记本电脑自带终端设备——键盘、显示器、鼠标。然而,如果要给一台服务器安装操作系统则稍微复杂了一点,因为服务器通常没有这些终端设备。这就需要利用网络和VNC将服务器端的图像传送出来。这样做的缺点在于,网络负载很大,在网络条件不好的环境下会带给用户非常差的安装体验。另外,本地安装器也会依赖很多的图形软件包,不仅会增加ISO的大小,也会带来很多版权及法律上的工作量。

基于web技术的安装器则利用web开发技术——HTML5,Javascript,CSS,web server等,让用户可以通过浏览器直接安装操作系统。目前,”KVM for IBM z Systems”已经采用这种安装方式。

基于web技术安装器的优势

  • 依赖更少的软件包
  • 非常小的网络负载,网页加载后只需要通过Ajax来与服务器通信
  • 可通过浏览器跨平台访问
  • 对于服务器及集群安装非常方便
  • 易于与其他软件集成,提供RESTful API
  • 满足可访问性,方便残障人士使用

使用web安装器的安装流程

使用web安装器来安装操作系统的流程非常简单,只需要如下几个步骤:

  1. 加载ISO
  2. 用https://{IP_OF_SERVER}作为URL来访问安装器
  3. 按照安装器的引导完成安装
  4. 点击安装器的重启按钮重启服务器,安装完成

Web安装器实现的功能

  • 国际化及多语言支持,选择安装语言与系统语言
  • 版权声明
  • 磁盘列表及选择安装磁盘
  • 添加SCSI磁盘
  • 添加DASD磁盘
  • Swap分区加密
  • 自动分区
  • 手动分区
  • 分区操作列表
  • 激活网卡
  • IPv4配置
  • IPv6配置
  • 主机名及搜索域配置
  • NTP配置
  • Kdump配置
  • 时区设置
  • Root密码设置
  • 添加、删除用户
  • 配置总结列表
  • 无人值守安装
  • 安装日志下载

Web安装器的架构设计

Web安装器分为前端和后端两部分实现:前端负责UI展示及用户交互,后端负责给前端提供RESTful API并根据前端的API调用来存储用户配置数据,执行分区、安装、配置目标系统等操作。

Web安装器由4个HTML页面组成:

  • 欢迎页:介绍操作系统,提供选择安装语言的下拉框,点击下一步可进入到版权声明页
  • 版权声明页:显示版权文件,在用户同意后可跳转到配置页面
  • 配置页:引导用户进行系统配置
  • 安装页:展示安装进度,安装完成后可点击重启按钮重启系统

UI是基于HTML5、CSS3及Javascript等网页开发技术,并利用如下工具:

  • jQuery:一个快速、小巧且功能丰富的js库,可用来操作DOM,处理事件及Ajax请求
  • Bootstrap:最流行的前端开发框架之一,多用于开发响应式、移动优先的web项目
  • Bootstrap-select: jQuery 插件,利用Bootstrap,但提供了功能更加丰富的下拉选择框控件
  • Jquery Validation :jQuery插件,用来检验表单的合法性

后端由以下几个模块组成:

  • CherryPy:一个轻量级的python web发布器
  • Model:存储用户的配置数据
  • RESTful API:为前端提供API接口
  • 子功能模块:提供安装器的各个子功能

QEMU3 – 使用ceph来存储QEMU镜像

ceph简介

Ceph是一个PB级别的分布式软件定义存储系统,为用户提供了块存储、对象存储以及符合POSIX标准的文件系统接口。目前,Ceph已经成为Openstack最受欢迎的后端存储系统。下图为ceph的架构图。

ceph架构图

RADOS本身是一个对象存储系统,实现了ceph的核心功能。Librados是ceph提供给各种编程语言的接口。RADOSGW,RBD,CEPH FS分别为用户提供了对象存储、块存储及文件系统的功能。Ceph集群及客户端的安装配置请参考Ceph官方文档

使用Ceph来存储QEMU镜像

QEMU会假定ceph配置文件存放在默认位置/etc/ceph/$cluster.conf,也会使用client.admin作为默认的ceph用户。如果要指定其他的配置文件或者用户,可以在ceph RBD的选项中添加conf=/home/ceph.conf或者id=admin选项。qemu-img使用ceph块存储RBD时,需要使用下面的格式:

qemu-img {command} [options] rbd:{pool-name}/{image-name}[@snapshot-name][:option1=value1][:option2=value2…] 

例如:

qemu-img {command} [options] rbd:glance-pool/maipo:id=glance:conf=/etc/ceph/ceph.conf 

创建一个镜像

可以使用qemu-img命令在ceph集群中创建一个虚拟机镜像。需要指定rbd, pool,以及镜像名。

qemu-img create -f raw rbd:{pool-name}/{image-name} {size} 

例如:

[root@ltczhp20 ~]# qemu-img create -f raw rbd:rbd/vmdisk1 4G

Formatting 'rbd:rbd/vmdisk1', fmt=raw size=4294967296
[root@ltczhp20 ~]# rbd ls
vmdisk1

qemu-img通常会指定RBD存储的镜像格式是RAW,这样可以减少其他格式带来的性能开销,也会防止虚拟机热迁移时缓存带来的问题。

调整镜像的大小

要调整镜像大小,必须指定rbd,pool name,以及镜像名。

qemu-img resize rbd:{pool-name}/{image-name} {size} 

例如:

[root@ltczhp20 ~]# qemu-img resize -f raw rbd:rbd/vmdisk1 2G

Image resized.
[root@ltczhp20 ~]# rbd ls
vmdisk1
[root@ltczhp20 ~]# rbd info vmdisk1
rbd image 'vmdisk1':
    size 2048 MB in 512 objects
    order 22 (4096 kB objects)
    block_name_prefix: rbd_data.fa802ae8944a
    format: 2
    features: layering, exclusive-lock, object-map, fast-diff, deep-flatten
    flags:

如果不指定镜像格式(-f raw),qemu会给出警告信息:

[root@ltczhp20 ~]# qemu-img resize rbd:rbd/vmdisk1 4G
WARNING: Image format was not specified for 'rbd:rbd/vmdisk1' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
Image resized.

获取镜像信息

获取镜像信息同样需要指定rbd,pool name以及镜像名:

qemu-img info rbd:{pool-name}/{image-name} 

例如:

[root@ltczhp20 ~]# qemu-img info rbd:rbd/vmdisk1

image: rbd:rbd/vmdisk1
file format: raw
virtual size: 4.0G (4294967296 bytes)
disk size: unavailable
cluster_size: 4194304

使用qemu命令运行虚拟机

从QEMU0.15后,虚拟机使用ceph块设备就不需要使用rbd map命令将RBD镜像映射到本地了,QEMU可以通过librados直接访问一个虚拟块设备。这样避免了额外的上下文切换,也充分利用了RBD的缓存功能。

在运行虚拟机之前,我们可以把一个已经存在的虚拟机镜像转化为ceph RBD存储,然后直接从RBD启动虚拟机。

qemu-img convert -c -f fmt -O out_fmt -o options  fname out_fname 

例如:

[root@ltczhp20 ~]# qemu-img convert -f qcow2 -O raw /srv/fedora24/fedora24.qcow2 rbd:rbd/fedora

然后使用qemu命令运行虚拟机。

[root@ltczhp20 ~]# qemu-system-s390x -nographic -enable-kvm -m 4G -drive format=raw,file=rbd:rbd/fedora

RBD缓存会极大的提高虚拟机的性能。QEMU1.2之后,cache选项可以直接控制librbd:

[root@ltczhp20 ~]# qemu-system-s390x -nographic -enable-kvm -m 4G -drive format=raw,file=rbd:rbd/fedora,cache=writeback

在QEMU1.2之前,如果要使用RBD缓存,需要额外添加rbd_cache=true选项:

[root@ltczhp20 ~]# qemu-system-s390x -nographic -enable-kvm -m 4G -drive format=raw,file=rbd:rbd/fedora,cache=writeback,rbd_cache=true

如果指定了rbd_cache=true,一定要指定cache=writeback,否则QEMU不会给librbd发送flush请求,RBD之上的文件系统可能会被破坏。

使用ceph RBD的快照功能

创建一个镜像快照sp0:

[root@ltczhp20 ~]# qemu-img snapshot -l rbd:rbd/fedora
[root@ltczhp20 ~]# qemu-img snapshot -c sp0 rbd:rbd/fedora
WARNING: Image format was not specified for 'rbd:rbd/fedora' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
[root@ltczhp20 ~]# qemu-img snapshot -l rbd:rbd/fedora
Snapshot list:
ID        TAG                 VM SIZE                DATE       VM CLOCK
sp0       sp0                     20G 1970-01-01 01:00:00   00:00:00.000

启动虚拟机,创建文件/root/hello.txt并写入字符串”hello world”,然后关闭虚拟机。

[root@ltczhp20 ~]# qemu-system-s390x -nographic -enable-kvm -m 4G -drive format=raw,file=rbd:rbd/fedora

In VM:

[root@localhost ~]# echo "hello world" >> /root/hello.txt
[root@localhost ~]# cat /root/hello.txt
hello world

[root@localhost ~]# halt

将虚拟机回滚到快照sp0,然后检查是否存在/root/hello.txt文件,如果不存在则说明快照已经成功回滚。

[root@ltczhp20 ~]# qemu-img snapshot -a sp0 rbd:rbd/fedora

[root@ltczhp20 ~]# qemu-system-s390x -nographic -enable-kvm -m 4G -drive format=raw,file=rbd:rbd/fedora

In VM:

[root@localhost ~]# ls /root/hello.txt
ls: cannot access '/root/hello.txt': No such file or directory

[root@localhost ~]# halt

删除快照:

[root@ltczhp20 ~]# rbd snap ls rbd/fedora
SNAPID NAME     SIZE
    22 sp0  20480 MB
[root@ltczhp20 ~]# qemu-img snapshot -d sp0 rbd:rbd/fedora
WARNING: Image format was not specified for 'rbd:rbd/fedora' and probing guessed raw.
         Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
         Specify the 'raw' format explicitly to remove the restrictions.
[root@ltczhp20 ~]# rbd snap ls rbd/fedora
[root@ltczhp20 ~]# qemu-img snapshot -l rbd:rbd/fedora

阅读原文

QEMU 2: 参数解析

#一、使用gdb分析QEMU代码#

使用gdb不仅可以很好地调试代码,也可以利用它来动态地分析代码。使用gdb调试QEMU需要做一些准备工作:

1, 编译QEMU时需要在执行configure脚本时的参数中加入–enable-debug。

2, 从QEMU官方网站上下载一个精简的镜像——linux-0.2.img。linux-0.2.img只有8MB大小,启动后包含一些常用的shell命令,用于QEMU的测试。

$wget http://wiki.qemu.org/download/linux-0.2.img.bz2
$bzip2 -d ./linux-0.2.img.bz2

3, 启动gdb调试QEMU:

gdb --args qemu-system-x86_64 -enable-kvm -m 4096 -smp 4 linux-0.2.img

-smp指定处理器个数。

#二、参数解析用到的数据结构#

QEMU系统模拟的主函数位于vl.c文件,无论是qemu-system-x86_64还是qemu-system-ppc64,都是从vl.c中的main函数开始执行。下面先介绍main函数涉及到的一些数据结构。

###QEMU链表

QEMU的链表在include/qemu/queue.h文件中定义,分为四种类型:

  • 单链表(singly-linked list):单链表适用于大的数据集,并且很少有删除节点或者移动节点的操作,也适用于实现后进先出的队列。
  • 链表(list):即双向链表,除了头节点之外每个节点都会同时指向前一个节点和后一个节点。
  • 简单队列(simple queue):简单队列类似于单链表,只是多了一个指向链表尾的一个表头,插入节点的时候不仅可以像单链表那样将其插入表头或者某节点之后,还可以插入到链表尾。
  • 尾队列(tail queue):类似于简单队列,但节点之间是双向指向的。

这里不一一介绍各种链表的用法,只通过NotifierList的定义来说明QEMU链表(list)的用法。在main函数的开头定义的DisplayState结构体使用到了NotifiereList,NotifierList就用到了链表。

a. 表头及节点的定义

定义表头需要用到QLIST_HEAD,定义如下:

 86 #define QLIST_HEAD(name, type)                                          \
 87 struct name {                                                           \
 88         struct type *lh_first;  /* first element */                     \
 89 }

NotifierList就采用了QLIST_HEAD来定义表头:

 27 typedef struct NotifierList
 28 {
 29     QLIST_HEAD(, Notifier) notifiers;
 30 } NotifierList;

定义节点需要用到QLIST_ENTRY,定义如下:

 94 #define QLIST_ENTRY(type)                                               \
 95 struct {                                                                \
 96         struct type *le_next;   /* next element */                      \
 97         struct type **le_prev;  /* address of previous next element */  \
 98 }

Notifier的节点定义如下:

 21 struct Notifier
 22 {
 23     void (*notify)(Notifier *notifier, void *data);
 24     QLIST_ENTRY(Notifier) node;
 25 };

b. 初始化表头

初始化表头用到QLIST_INIT:

103 #define QLIST_INIT(head) do {                                           \
104         (head)->lh_first = NULL;                                        \
105 } while (/*CONSTCOND*/0)

初始化NotifierList就可以这样进行:

 19 void notifier_list_init(NotifierList *list)
 20 {
 21     QLIST_INIT(&list->notifiers);
 22 }

c. 在表头插入节点

将节点插入到表头使用QLIST_INSERT_HEAD:

122 #define QLIST_INSERT_HEAD(head, elm, field) do {                        \
123         if (((elm)->field.le_next = (head)->lh_first) != NULL)          \
124                 (head)->lh_first->field.le_prev = &(elm)->field.le_next;\
125         (head)->lh_first = (elm);                                       \
126         (elm)->field.le_prev = &(head)->lh_first;                       \
127 } while (/*CONSTCOND*/0)

插入Notifier到NotifierList:

 24 void notifier_list_add(NotifierList *list, Notifier *notifier)
 25 {
 26     QLIST_INSERT_HEAD(&list->notifiers, notifier, node);
 27 }

d. 遍历节点

遍历节点使用QLIST_FOREACH或者QLIST_FOREACH_SAFE,QLIST_FOREACH_SAFE是为了防止遍历过程中删除了节点,从而导致le_next被释放掉,中断了遍历。

147 #define QLIST_FOREACH(var, head, field)                                 \
148         for ((var) = ((head)->lh_first);                                \
149                 (var);                                                  \
150                 (var) = ((var)->field.le_next))
151 
152 #define QLIST_FOREACH_SAFE(var, head, field, next_var)                  \
153         for ((var) = ((head)->lh_first);                                \
154                 (var) && ((next_var) = ((var)->field.le_next), 1);      \
155                 (var) = (next_var))

NotifierList在执行所有的回调函数时就用到了QLIST_FOREACH_SAFE:

 34 void notifier_list_notify(NotifierList *list, void *data)
 35 {
 36     Notifier *notifier, *next;
 37 
 38     QLIST_FOREACH_SAFE(notifier, &list->notifiers, node, next) {
 39         notifier->notify(notifier, data);
 40     }
 41 }

###Error和QError

为了方便的处理错误信息,QEMU定义了Error和QError两个数据结构。Error在qobject/qerror.c中定义:

101 struct Error
102 {
103     char *msg;
104     ErrorClass err_class;
105 };

包含了错误消息字符串和枚举类型的错误类别。错误类别有下面几个:

 139 typedef enum ErrorClass
 140 {
 141     ERROR_CLASS_GENERIC_ERROR = 0,
 142     ERROR_CLASS_COMMAND_NOT_FOUND = 1,
 143     ERROR_CLASS_DEVICE_ENCRYPTED = 2,
 144     ERROR_CLASS_DEVICE_NOT_ACTIVE = 3,
 145     ERROR_CLASS_DEVICE_NOT_FOUND = 4,
 146     ERROR_CLASS_K_V_M_MISSING_CAP = 5,
 147     ERROR_CLASS_MAX = 6,
 148 } ErrorClass;

QEMU在util/error.c中定义了几个函数来对Error进行操作:

error_set  //根据给定的ErrorClass以及格式化字符串来给Error分配空间并赋值
error_set_errno  //除了error_set的功能外,将指定errno的错误信息追加到格式化字符串的后面
error_copy  //复制Error           
error_is_set   //判断Error是否已经分配并设置
error_get_class  //获取Error的ErrorClass      
error_get_pretty  //获取Error的msg
error_free  //释放Error及msg的空间

另外,QEMU定义了QError来处理更为细致的错误信息:

 22 typedef struct QError { 
 23     QObject_HEAD;
 24     Location loc;
 25     char *err_msg;
 26     ErrorClass err_class;
 27 } QError;

QError可以通过一系列的宏来给err_msg及err_class赋值:

 39 #define QERR_ADD_CLIENT_FAILED \
 40     ERROR_CLASS_GENERIC_ERROR, "Could not add client"
 41 
 42 #define QERR_AMBIGUOUS_PATH \
 43     ERROR_CLASS_GENERIC_ERROR, "Path '%s' does not uniquely identify an object"
 44 
 45 #define QERR_BAD_BUS_FOR_DEVICE \
 46     ERROR_CLASS_GENERIC_ERROR, "Device '%s' can't go on a %s bus"
 47 
 48 #define QERR_BASE_NOT_FOUND \
 49     ERROR_CLASS_GENERIC_ERROR, "Base '%s' not found"
...

Location记录了出错的位置,定义如下:

 20 typedef struct Location {
 21     /* all members are private to qemu-error.c */
 22     enum { LOC_NONE, LOC_CMDLINE, LOC_FILE } kind;
 23     int num;
 24     const void *ptr;
 25     struct Location *prev;
 26 } Location;

###GMainLoop

QEMU使用glib中的GMainLoop来实现IO多路复用,关于GMainLoop可以参考博客GMainLoop的实现原理和代码模型。由于GMainLoop并非QEMU本身的代码,本文就不重复赘述。

#三、QEMUOption、QemuOpt及QEMU参数解析

QEMU定义了QEMUOption来表示执行qemu-system-x86_64等命令时用到的选项。在vl.c中定义如下:

2123 typedef struct QEMUOption {
2124     const char *name;   //选项名,如 -device, name的值就是device
2125     int flags;  //标志位,表示选项是否带参数,可以是0,或者HAS_ARG(值为0x0001)
2126     int index;  //枚举类型的值,如-device,该值就是QEMU_OPTION_device
2127     uint32_t arch_mask;  //  选项支持架构的掩码
2128 } QEMUOption;

vl.c中维护了一个QEMUOption数组qemu_options来存储所有可用的选项,并利用qemu-options-wrapper.h和qemu-options.def来给该数组赋值。赋值语句如下:

2130 static const QEMUOption qemu_options[] = {
2131     { "h", 0, QEMU_OPTION_h, QEMU_ARCH_ALL },
2132 #define QEMU_OPTIONS_GENERATE_OPTIONS
2133 #include "qemu-options-wrapper.h"
2134     { NULL },
2135 };

#define QEMU_OPTIONS_GENERATE_OPTIONS选择qemu-options-wrapper.h的操作,qemu-options-wrapper.h可以进行三种操作:

QEMU_OPTIONS_GENERATE_ENUM: 利用qemu-options.def生成一个枚举值列表,就是上面提到的QEMU_OPTION_device等
QEMU_OPTIONS_GENERATE_HELP: 利用qemu-options.def生成帮助信息并输出到标准输出
QEMU_OPTIONS_GENERATE_OPTIONS: 利用qemu-options.def生成一组选项列表

可以通过下面的方法来展开qemu-options-wrapper.h来查看上述操作的结果,以生成选项为例。

  1. 在qemu-options-wrapper.h第一行写入#define QEMU_OPTIONS_GENERATE_OPTIONS.
  2. 执行命令gcc -E -o options.txt qemu-options-wrapper.h
  3. 查看文件options.txt即可

给qemu_options数组赋值后,QEMU就有了一个所有可用选项的集合。之后在vl.c中main函数的一个for循环根据这个集合开始解析命令行。for循环的框架大致如下:

  1     for(;;) {
  2         if (optind >= argc)
  3             break;
  4         if (argv[optind][0] != '-') {
  5         hda_opts = drive_add(IF_DEFAULT, 0, argv[optind++], HD_OPTS);
  6         } else {
  7             const QEMUOption *popt;
  8 
  9             popt = lookup_opt(argc, argv, &optarg, &optind);
 10             if (!(popt->arch_mask & arch_type)) {
 11                 printf("Option %s not supported for this target\n", popt->name);
 12                 exit(1);
 13             }
 14             switch(popt->index) {
 15             case QEMU_OPTION_M:
 16             ......
 17             case QEMU_OPTION_hda:
 18             ......  
 19             case QEMU_OPTION_watchdog:
 20             ......
 21             default:
 22                 os_parse_cmd_args(popt->index, optarg);
 23             }   
 24         }
 25     }

QEMU会把argv中以’-‘开头的字符串当作选项,然后调用lookup_opt函数到qemu_options数组中查找该选项,如果查找到的选项中flags的值是HAS_ARG,lookup_opt也会将参数字符串赋值给optarg。找到选项和参数之后,QEMU便根据选项中的index枚举值来执行不同的分支。

对于一些开关性质的选项,分支执行时仅仅是把相关的标志位赋值而已,如:

3712             case QEMU_OPTION_old_param:
3713                 old_param = 1;
3714                 break;

也有一些选项没有子选项,分支执行时就直接把optarg的值交给相关变量:

3822             case QEMU_OPTION_qtest:
3823                 qtest_chrdev = optarg;
3824                 break;

对于那些拥有子选项的选项,如”-drive if=none,id=DRIVE-ID”,QEMU的处理会更为复杂一些。它会调用qemu_opts_parse来解析子选项,如realtime选项的解析:

3852             case QEMU_OPTION_realtime:
3853                 opts = qemu_opts_parse(qemu_find_opts("realtime"), optarg, 0);
3854                 if (!opts) {
3855                     exit(1);
3856                 }
3857                 configure_realtime(opts);
3858                 break;

对子选项的解析涉及到4个数据结构:QemuOpt, QemuDesc, QemuOpts, QemuOptsList. 它们的关系如下图所示:

Qemu选项数据结构关系图

QemuOpt存储子选项,每个QemuOpt有一个QemuOptDesc来描述该子选项名字、类型、及帮助信息。两个结构体定义如下:

 32 struct QemuOpt {
 33     const char   *name;  //子选项的名字
 34     const char   *str;  //字符串值
 35 
 36     const QemuOptDesc *desc;  
 37     union {
 38         bool boolean;  //布尔值
 39         uint64_t uint;  //数字或者大小
 40     } value; 
 41 
 42     QemuOpts     *opts;  
 43     QTAILQ_ENTRY(QemuOpt) next;
 44 };

 95 typedef struct QemuOptDesc {
 96     const char *name;
 97     enum QemuOptType type;
 98     const char *help;
 99 } QemuOptDesc;

子选项的类型可以是:

 88 enum QemuOptType {
 89     QEMU_OPT_STRING = 0,   // 字符串
 90     QEMU_OPT_BOOL,       // 取值可以是on或者off
 91     QEMU_OPT_NUMBER,     // 数字
 92     QEMU_OPT_SIZE,       // 大小,可以有K, M, G, T等后缀
 93 };

QEMU维护了一个QemuOptsList*的数组,在util/qemu-config.c中定义:

10 static QemuOptsList *vm_config_groups[32];

在main函数中由qemu_add_opts将各种QemuOptsList写入到数组中:

2944     qemu_add_opts(&qemu_drive_opts);
2945     qemu_add_opts(&qemu_chardev_opts);
2946     qemu_add_opts(&qemu_device_opts); 
2947     qemu_add_opts(&qemu_netdev_opts);
2948     qemu_add_opts(&qemu_net_opts);
2949     qemu_add_opts(&qemu_rtc_opts);
2950     qemu_add_opts(&qemu_global_opts);
2951     qemu_add_opts(&qemu_mon_opts);
2952     qemu_add_opts(&qemu_trace_opts);
2953     qemu_add_opts(&qemu_option_rom_opts);
2954     qemu_add_opts(&qemu_machine_opts);
2955     qemu_add_opts(&qemu_smp_opts);
2956     qemu_add_opts(&qemu_boot_opts);
2957     qemu_add_opts(&qemu_sandbox_opts);
2958     qemu_add_opts(&qemu_add_fd_opts);
2959     qemu_add_opts(&qemu_object_opts);
2960     qemu_add_opts(&qemu_tpmdev_opts);
2961     qemu_add_opts(&qemu_realtime_opts);
2962     qemu_add_opts(&qemu_msg_opts);

每个QemuOptsList存储了大选项所支持的所有小选项,如-realtime大选项QemuOptsList的定义:

 507 static QemuOptsList qemu_realtime_opts = {
 508     .name = "realtime",
 509     .head = QTAILQ_HEAD_INITIALIZER(qemu_realtime_opts.head),
 510     .desc = {
 511         {
 512             .name = "mlock",
 513             .type = QEMU_OPT_BOOL,
 514         },
 515         { /* end of list */ }
 516     },
 517 };

-realtime只支持1个子选项,且值为bool类型,即只能是on或者off。

在调用qemu_opts_parse解析子选项之前,QEMU会调用qemu_find_opts(“realtime”),把QemuOptsList *从qemu_add_opts中找出来,和optarg一起传递给qemu_opts_parse去解析。QEMU可能会多次使用同一个大选项来指定多个相同的设备,在这种情况下,需要用id来区分。QemuOpts结构体就表示同一id下所有的子选项,定义如下:

 46 struct QemuOpts {
 47     char *id;
 48     QemuOptsList *list;
 49     Location loc;
 50     QTAILQ_HEAD(QemuOptHead, QemuOpt) head;
 51     QTAILQ_ENTRY(QemuOpts) next;
 52 };

其中list是同一个大选项下不同id的QemuOpts链表。

QEMU 1: 使用QEMU创建虚拟机

一、QEMU简介#

QEMU是一款开源的模拟器及虚拟机监管器(Virtual Machine Monitor, VMM)。QEMU主要提供两种功能给用户使用。一是作为用户态模拟器,利用动态代码翻译机制来执行不同于主机架构的代码。二是作为虚拟机监管器,模拟全系统,利用其他VMM(Xen, KVM, etc)来使用硬件提供的虚拟化支持,创建接近于主机性能的虚拟机。

用户可以通过不同Linux发行版所带有的软件包管理器来安装QEMU。如在Debian系列的发行版上可以使用下面的命令来安装:

sudo apt-get install qemu

或者在红帽系列的发行版上使用如下命令安装:

sudo yum install qemu -y

除此之外,也可以选择从源码安装。

##获取QEMU源码##

可以从QEMU官网上下载QEMU源码的tar包,以命令行下载2.0版本的QEMU为例:

$wget http://wiki.qemu-project.org/download/qemu-2.0.0.tar.bz2
$tar xjvf qemu-2.0.0.tar.bz2

如果需要参与到QEMU的开发中,最好使用Git获取源码:

$git clone git://git.qemu-project.org/qemu.git

##编译及安装##

获取源码后,可以根据需求来配置和编译QEMU。

$cd qemu-2.0.0 //如果使用的是git下载的源码,执行cd qemu
$./configure --enable-kvm --enable-debug --enable-vnc --enable-werror  --target-list="x86_64-softmmu"
$make -j8
$sudo make install

configure脚本用于生成Makefile,其选项可以用./configure --help查看。这里使用到的选项含义如下:

--enable-kvm:编译KVM模块,使QEMU可以利用KVM来访问硬件提供的虚拟化服务。
--enable-vnc:启用VNC。
--enalbe-werror:编译时,将所有的警告当作错误处理。
--target-list:选择目标机器的架构。默认是将所有的架构都编译,但为了更快的完成编译,指定需要的架构即可。

#二、基本原理#

QEMU作为系统模拟器时,会模拟出一台能够独立运行操作系统的虚拟机。如下图所示,每个虚拟机对应主机(Host)中的一个QEMU进程,而虚拟机的vCPU对应QEMU进程的一个线程。

QEMU结构图

系统虚拟化最主要是虚拟出CPU、内存及I/O设备。虚拟出的CPU称之为vCPU,QEMU为了提升效率,借用KVM、XEN等虚拟化技术,直接利用硬件对虚拟化的支持,在主机上安全地运行虚拟机代码(需要硬件支持)。虚拟机vCPU调用KVM的接口来执行任务的流程如下(代码源自QEMU开发者Stefan的技术博客):

open("/dev/kvm")
ioctl(KVM_CREATE_VM)
ioctl(KVM_CREATE_VCPU)
for (;;) {
     ioctl(KVM_RUN)
     switch (exit_reason) {
     case KVM_EXIT_IO:  /* ... */
     case KVM_EXIT_HLT: /* ... */
     }
}

QEMU发起ioctrl来调用KVM接口,KVM则利用硬件扩展直接将虚拟机代码运行于主机之上,一旦vCPU需要操作设备寄存器,vCPU将会停止并退回到QEMU,QEMU去模拟出操作结果。

虚拟机内存会被映射到QEMU的进程地址空间,在启动时分配。在虚拟机看来,QEMU所分配的主机上的虚拟地址空间为虚拟机的物理地址空间。

QEMU在主机用户态模拟虚拟机的硬件设备,vCPU对硬件的操作结果会在用户态进行模拟,如虚拟机需要将数据写入硬盘,实际结果是将数据写入到了主机中的一个镜像文件中。

#三、创建及使用虚拟机#

##命令行创建及启动虚拟机##

成功安装QEMU之后便可创建自己的虚拟机。具体步骤如下:

1, 使用qemu-img创建虚拟机镜像。虚拟机镜像用来模拟虚拟机的硬盘,在启动虚拟机之前需要创建镜像文件。

[kelvin@kelvin tmp]$ qemu-img create -f qcow2 fedora.img 10G
Formatting 'fedora.img', fmt=qcow2 size=10737418240 encryption=off cluster_size=65536 lazy_refcounts=off 
[kelvin@kelvin tmp]$ ls
fedora.img

-f选项用于指定镜像的格式,qcow2格式是QEMU最常用的镜像格式,采用写时复制技术来优化性能。fedora.img是镜像文件的名字,10G是镜像文件大小。镜像文件创建完成后,可使用qemu-system-x86来启动x86架构的虚拟机:

qemu-system-x86_64 fedora.img

此时会弹出一个窗口来作为虚拟机的显示器,显示内容如下:

QEMU虚拟机显示器输出

因为fedora.img中并未给虚拟机安装操作系统,所以会提示“No bootable device”,无可启动设备。

2, 准备操作系统镜像。

可以从不同Linux发行版的官方网站上获取安装镜像,以fedora20为例:

[kelvin@kelvin tmp]$ wget http://ftp6.sjtu.edu.cn/fedora/linux/releases/20/Live/x86_64/Fedora-Live-Desktop-x86_64-20-1.iso

3, 检查KVM是否可用。

QEMU使用KVM来提升虚拟机性能,如果不启用KVM会导致性能损失。要使用KVM,首先要检查硬件是否有虚拟化支持:

[kelvin@kelvin ~]$ grep -E 'vmx|svm' /proc/cpuinfo

如果有输出则表示硬件有虚拟化支持。其次要检查kvm模块是否已经加载:

[kelvin@kelvin ~]$ lsmod | grep kvm
kvm_intel             142999  0 
kvm                   444314  1 kvm_intel

如果kvm_intel/kvm_amd、kvm模块被显示出来,则kvm模块已经加载。最后要确保qemu在编译的时候使能了KVM,即在执行configure脚本的时候加入了–enable-kvm选项。

4, 启动虚拟机安装操作系统。

执行下面的命令启动带有cdrom的虚拟机:

[kelvin@kelvin tmp]$ qemu-system-x86_64 -m 2048 -enable-kvm fedora.img -cdrom ./Fedora-Live-Desktop-x86_64-20-1.iso

-m 指定虚拟机内存大小,默认单位是MB, -enable-kvm使用KVM进行加速,-cdrom添加fedora的安装镜像。可在弹出的窗口中操作虚拟机,安装操作系统,安装完成后重起虚拟机便会从硬盘(fedora.img)启动。之后再启动虚拟机只需要执行:

[kelvin@kelvin tmp]$ qemu-system-x86_64 -m 2048 -enable-kvm fedora.img

即可。

##图形界面创建及启动虚拟机##

命令行启动虚拟机比较繁琐,适合开发者,但对于普通用户来说,采用图形界面管理虚拟机则更为方便。采用图形界面管理QEMU虚拟机需要安装virt-manager,红帽系列的发行版只需要执行命令:

$sudo yum install virt-manager -y

安装完成后用root用户启动virt-manager:

$su -
#virt-manager

启动后的界面如下图所示:

virt-manager界面

点击左上角电脑图标即可创建虚拟机。按照步骤操作即可完成对虚拟机的创建。