15158846557 在线咨询 在线咨询
15158846557 在线咨询
所在位置: 首页 > 营销资讯 > 网站运营 > 从kvmtools学习虚拟化一 基础介绍

从kvmtools学习虚拟化一 基础介绍

时间:2023-07-12 18:48:01 | 来源:网站运营

时间:2023-07-12 18:48:01 来源:网站运营

从kvmtools学习虚拟化一 基础介绍:

前言

qemu可谓是linux中虚拟化的集大成者, 但是qemu庞大的体量对于我这样的初学者很不友好, 后有幸看到一本很不错的书: <<深度探索Linux系统虚拟化原理与实现>>, 其中使用一个轻量级的虚拟机kvmtool作为讲解的案例, 通俗易懂.

在看完kvmtools了解虚拟化原理之后再去看qemu, 相信许多问题都会迎刃而解. 该书以kvmtool为案例, 本系列文章则反过来从kvmtool出发, 去践行学到的知识, 也算是自己的读书笔记.

虚拟机基础

内核实际上也就是一段程序, 虚拟机的目标就是运行内核, 那么如何实现这个目标呢?

我们先打一个比方, 如果有一个人想要住在海边, 但是买不起房子, 那么该怎么办呢? 除了努力赚钱外, 他还可以把窗户换成屏幕, 不停播放海的画面, 播放海浪的声音, 时间一久就真的好像住在海边一样.

我们把操作系统的内核看做是这个人, 虚拟机就是窗户上的屏幕, 内核想要运行在物理机上, 当内核想要访问键盘时, 虚拟机就会在屏幕上播放键盘相关信息来哄骗内核, 这就是虚拟机的基本思想.

为了实现上述过程, 常用的是陷入和模拟(Trap and Emulate)模型. 我们知道CPU有用户态和内核态两种, CPU指令也分配特权指令和非特权指令. 在陷入和模拟模型中, 虚拟机程序(VMM)运行用户态, 内核也运行在用户态中, 内核中的非特权指令不需要干预, CPU直接运行即可. 对于特权指令, 由于CPU处于用户态, 会触发虚拟机异常, 陷入VMM中, VMM代理虚拟机完成系统资源的访问, 这就是模拟的过程. 这样所有的硬件资源都被VMM控制, 不会破坏宿主机的环境.

上述过程是纯软件实现的虚拟化, 但并不是所有的特权指令都需要模拟, 也不是所有需要模拟的指令都是特权指令. 单纯的依靠软件来虚拟化十分困难. 为此Intel提出了硬件的解决方案VT-x, 为CPU增加了虚拟机扩展, 简称为VMX.

当CPU开启VMX支持, CPU就有了两种运行模拟时: VMX Root ModeVMX non-Root Mode, 每种模式都支持ring0~ring3. 宿主机(Host)的内核和程序运行在VMX Root Mode, 这与最普通的模式一样. 虚拟机(Guest)运行在VMX non Root Mode. 由于VMX的支持, 现在不需要特权压缩, 虚拟机内核可以运行在VMX non Root Mode ring 0中. 虚拟机中的程序可以运行在VMX non Root ring3中.

开启VMX后, CPU会支持VMX指令集, 用于在Host与Guest之间进行切换. 正如进程之间切换需要任务状态段来保存CPU状态一样. 在切入切出虚拟机时, CPU使用VMCS(Virtual Machine Control data Structures)结构来描述相关信息, VMCS包含切入虚拟机前宿主机的寄存器状态, 切入虚拟机后虚拟机的寄存器状态, 以及中断等控制信息.

CPU的状态转换如下图.




image.png



为了便于使用, linux中基于虚拟化指令集实现了KVM(Kernel-based Virtual Machine)模块, 该模块只负责CPU, 内存, 中断等最核心的虚拟化, 并通过linux的文件系统向用户导出接口. 至于相关外设, 如硬盘, pci设备等的虚拟化, 由于种类繁多, 代码量大, 因此由kvmtool这类虚拟机管理软件(VMM)来负责. 由此我们可以看到kvmtool基本架构如下




image.png



kvmtool是一个托管KVM虚拟机(后文称之为guest)的轻量级工具. 作为单纯的虚拟化工具, 只支持与宿主机(后文称之为Host)相同架构的Guest, 有一个特例: 32位的guest仍可以运行在64位的host中.

kvmtool的目标是提供一个简单的, 轻量的 KVM host工具. 可用于启动不依赖BIOS的Linux内核的虚拟机镜像, 并且只实现了最基础的设备模拟. 如果想要涉足虚拟化的话, kvmtool很适合入门. 他只有5K多行的纯C代码.

由于KVM-base虚拟机是主流, 因此我们主要关注于kvmtool中设备模拟的部分, 至于KVM部分, 只会讲一下api的使用和背后的原理. 接下来就通过kvmtool来探索linux虚拟化的世界吧

环境搭建

Parallel Desktop虚拟机中安装Ubuntu 20.04, 因为低版本的ubuntu在kvm嵌套虚拟化时会出错, 先安装kvm:

sudo apt-get install qemu-kvmkvm-ok切换到kvmtools的主目录, 直接make会报错

hw/i8042.c: In function kbd_io:hw/i8042.c:153:19: error: value may be used uninitialized in this function [-Werror=maybe-uninitialized] state.write_cmd = val; ~~~~~~~~~~~~~~~~^~~~~hw/i8042.c:298:5: note: value was declared here u8 value; ^~~~~cc1: all warnings being treated as errorskbd_io函数中为变量value设置一个初始值,

static void kbd_io(struct kvm_cpu *vcpu, u64 addr, u8 *data, u32 len, u8 is_write, void *ptr){ u8 value = 0 ; //set default value if (is_write) value = ioport__read8(data); switch (addr) { case I8042_COMMAND_REG: if (is_write) kbd_write_command(vcpu->kvm, value); else value = kbd_read_status(); break; ....为了调试方便, 在Makefile中把CFLAGS的-O2修改为-O0, 表示编译时不进行优化. 然后make即可

CFLAGS += $(CPPFLAGS) $(DEFINES) -I$(KVM_INCLUDE) -I$(ARCH_INCLUDE) -O0 -fno-strict-aliasing -g启虚拟机的命令如下, 其中rootfs.cpio为编译busybox打包得到的文件镜像, bzImage为linux内核编译之后得到镜像

./lkvm run / --initrd ./rootfs.cpio / --kernel ./bzImage

入口点: main()

main()在去掉argv[0]之后会调用handle_command()根据kvm_commands中支持的命令去解析参数

int main(int argc, char* argv[]){ ...; return handle_kvm_command(argc - 1, &argv[1]); //从第二个参数开始解析, 省略"lkvm"}static int handle_kvm_command(int argc, char** argv){ //进行参数解析 return handle_command(kvm_commands, argc, (const char**)&argv[0]);}kvm_commands是一个struct cmd_struct组成的数组, 所有支持的命令如下

struct cmd_struct { const char* cmd; //命令名字 int (*fn)(int, const char**, const char*); //执行命令 void (*help)(void); //获取命令帮助 int option; //选项};struct cmd_struct kvm_commands[] = { { "pause", kvm_cmd_pause, kvm_pause_help, 0 }, //暂停虚拟机的运行 { "resume", kvm_cmd_resume, kvm_resume_help, 0 }, //恢复虚拟机的运行 { "debug", kvm_cmd_debug, kvm_debug_help, 0 }, //从一个正在运行的虚拟机输出调试信息 { "balloon", kvm_cmd_balloon, kvm_balloon_help, 0 }, //扩张或者搜索virtio balloon { "list", kvm_cmd_list, kvm_list_help, 0 }, //输出正在host上运行的虚拟机 { "version", kvm_cmd_version, NULL, 0 }, { "--version", kvm_cmd_version, NULL, 0 }, { "stop", kvm_cmd_stop, kvm_stop_help, 0 }, //停止一个运行的虚拟机实例 { "stat", kvm_cmd_stat, kvm_stat_help, 0 }, //输出正在运行的虚拟机状态 { "help", kvm_cmd_help, NULL, 0 }, { "setup", kvm_cmd_setup, kvm_setup_help, 0 }, //设置一个新的虚拟机 { "run", kvm_cmd_run, kvm_run_help, 0 }, //启动一个虚拟机 { "sandbox", kvm_cmd_sandbox, kvm_run_help, 0 }, //在虚拟机沙盒中运行命令 { NULL, NULL, NULL, 0 },};handle_command()解析命令的过程如下.

struct cmd_struct* kvm_get_command(struct cmd_struct* command, const char* cmd){ struct cmd_struct* p = command; //遍历所有可用命令, 返回cmd对应的struct cmd_struct while (p->cmd) { if (!strcmp(p->cmd, cmd)) return p; p++; } return NULL;}int handle_command(struct cmd_struct* command, int argc, const char** argv){ struct cmd_struct* p; const char* prefix = NULL; int ret = 0; ...; //根据argv[0]找到对应的struct cmd_struct p = kvm_get_command(command, argv[0]); //从&argv[1]开始作为参数, 调用命令对应的执行函数 ret = p->fn(argc - 1, &argv[1], prefix); return ret;}我们以run命令为例子, 根据kvm_commands可知, 接下来会进入bulitin-run.c中的kvm_cmd_run()

处理run命令: kvm_cmd_run()

kvm_cmd_run()如下

int kvm_cmd_run(int argc, const char** argv, const char* prefix){ int ret = -EFAULT; struct kvm* kvm; //根据参数初始化一个kvm对象 kvm = kvm_cmd_run_init(argc, argv); //执行一个虚拟机 ret = kvm_cmd_run_work(kvm); //虚拟机退出 kvm_cmd_run_exit(kvm, ret); return ret;}

虚拟机初始化: kvm_cmd_run_init()

kvm_cmd_run_init()分为三部分

  1. 调用kvm__new()申请一个kvm对象
  2. 然后通过宏BUILD_OPTIONS生成所有可用的命令选项, 然后调用parse_options()解析参数 ,然后进行一些参数的处理
  3. 通过init_list__init()调用所有初始化函数来初始化此kvm对象
static struct kvm* kvm_cmd_run_init(int argc, const char** argv){ static char default_name[20]; unsigned int nr_online_cpus; struct kvm* kvm = kvm__new(); //申请一个kvm对象 nr_online_cpus = sysconf(_SC_NPROCESSORS_ONLN); //获取Host中有多少CPU while (argc != 0) { BUILD_OPTIONS(options, &kvm->cfg, kvm); //初始化options为run命令的参数 //根据option解析argv参数 argc = parse_options(argc, argv, options, run_usage, PARSE_OPT_STOP_AT_NON_OPTION | PARSE_OPT_KEEP_DASHDASH); ...; } kvm_run_validate_cfg(kvm); //检查kvm中的配置 //cpu个数默认为nr_online_cpus if (kvm->cfg.nrcpus == 0) kvm->cfg.nrcpus = nr_online_cpus; //没设置内存大小,就根据cpu个数计算 if (!kvm->cfg.ram_size) kvm->cfg.ram_size = get_ram_size(kvm->cfg.nrcpus); ...; //处理其余默认配置 //遍历初始化函数表, 以kvm为参数调用其中的所有函数 if (init_list__init(kvm) < 0) die("Initialisation failed");}

申请kvm对象: kvm__new()

新建一个kvm对象的过程如下.

struct kvm* kvm__new(void){ struct kvm* kvm = calloc(1, sizeof(*kvm)); //分配内存 if (!kvm) return ERR_PTR(-ENOMEM); mutex_init(&kvm->mem_banks_lock); //初始化互斥锁 kvm->sys_fd = -1; kvm->vm_fd = -1; return kvm;}一个struct kvm就表示一台虚拟机, 定义如下, 其中字段比较多, 我们会在后面一点点介绍

struct kvm { struct kvm_arch arch; // Guest的架构 struct kvm_config cfg; // Guest配置相关信息 int sys_fd; /* 通过"/dev/kvm"打开kvm模块的fd */ int vm_fd; /* 通过ioctl(sys_fd, KVM_CREATE_VM, ...)创建一个虚拟机时KVM返回的fd */ timer_t timerid; /* Posix timer for interrupts */ int nrcpus; /* 运行多少个CPU */ struct kvm_cpu** cpus; // CPU对象数组 u32 mem_slots; /* 内存插槽 for KVM_SET_USER_MEMORY_REGION */ u64 ram_size; // 内存大小 void* ram_start; // Host中为Guest的内存分配的地址 u64 ram_pagesize; // 内存页大小 struct mutex mem_banks_lock; struct list_head mem_banks; // Guest可能有多个内存区域, 每一个内存区域用struct kvm_mem_bank表示 bool nmi_disabled; bool msix_needs_devid; const char* vmlinux; //执行内核程序 struct disk_image** disks; //指向磁盘镜像 int nr_disks; //有多少个磁盘 int vm_state; //虚拟机状态#ifdef KVM_BRLOCK_DEBUG pthread_rwlock_t brlock_sem;#endif};

参数解析:parse_options()

BUILD_OPTIONS会生成一个struct option组成的数组, 下面只以-k为例子

//name: 命令的选项数组. //cfg: 指向struct kvm_config对象, 保存解析出来的配置//kvm: 指向struct kvm对象#define BUILD_OPTIONS(name, cfg, kvm) / struct option name[] = { / OPT_STRING('k', "kernel", &(cfg)->kernel_filename, "kernel", / "Kernel to boot in virtual machine"), / ... }其中每一个选项struct option定义如下

struct option { enum parse_opt_type type; //保存选项的类型, 最后一个参数必须设置为OPTION_END int short_name; //选项的短名, 只包含一个char, '/0'表示没有 const char* long_name; //选项的长名 void* value; //指向写入这个选项对应值的地方 const char* argh; const char* help; void* ptr; /* 选项标志的掩码: - PARSE_OPT_OPTARG: 这个选项是可选的 - PARSE_OPT_NOARG: 这个选项没有参数 - PARSE_OPT_NONEG: 这个选项不能是负的 - PARSE_OPT_HIDDEN: 展示默认用法中会跳过这个选项, 展示更长的用法时出现 */ int flags; parse_opt_cb* callback; //对于OPTION_CALLBACK, 指向需要调用的回调函数 intptr_t defval; //对于可选选项, 该值作为默认值写入(*->value)中. 对于有回调的选项则随意使用};OPT_STRING负责初始化一个option

#define OPT_STRING(s, l, v, a, h) / { / .type = OPTION_STRING, / .short_name = (s), / .long_name = (l), / .value = check_vtype(v, const char**), (a), / .help = (h) / }--kernel为例子, 宏展开时会生成OPT_STRING('k', "kernel", &(&kvm->cfg)->kernel_filename, "kernel", "..."), 在函数parse_options()解析argv的过程中就会把内核文件名写入&(&kvm->cfg)->kernel_filename中. 其余参数类似处理

本例中配置处理完毕后kvm->cfg的主要内容如下




image.png



初始化链表: init_list__init()

kvmtool用struct init_item表示一个初始化函数, 每个初始化函数都有一个优先级, 优先级相同的初始化函数通过struct hlist_node n组织为一个双链表, 数组init_lists中保存各优先级函数对应的链表头.

#define PRIORITY_LISTS 10static struct hlist_head init_lists[PRIORITY_LISTS]; //根据优先级排序的初始化函数//表示一个要初始化的函数struct init_item { struct hlist_node n; // 用作非循环双链表中的指针 const char* fn_name; //函数名 int (*init)(struct kvm*); //初始化函数};kvmtool定义了如下宏, 用于注册各个优先级的初始化函数

#define core_init(cb) __init_list_add(cb, 0) //核心初始化: 为Guest申请内存和CPU#define base_init(cb) __init_list_add(cb, 2) //基础初始化: 初始化CPU, 建立PCI总线#define dev_base_init(cb) __init_list_add(cb, 4) //基础设备初始化: 建立中断机制#define dev_init(cb) __init_list_add(cb, 5) //设备初始化: 建立键盘, 鼠标等具体设备//cb: 回调函数, l: 优先级, 该宏会生成一个构造函数, 在进入main之前就被调用, 从而完成init_lists的设置#define __init_list_add(cb, l) / static void __attribute__((constructor)) __init__##cb(void) / { / static char name[] = #cb; / static struct init_item t; / init_list_add(&t, cb, l, name); / }//将init_item初始化并添加到初始化链表中int init_list_add(struct init_item* t, int (*init)(struct kvm*), int priority, const char* name){ t->init = init; t->fn_name = name; hlist_add_head(&t->n, &init_lists[priority]); return 0;}函数init_list__init()会根据优先级调用链表中所有的初始化函数

int init_list__init(struct kvm* kvm){ unsigned int i; int r = 0; struct init_item* t; for (i = 0; i < ARRAY_SIZE(init_lists); i++) //从低到高遍历所有的优先级 hlist_for_each_entry(t, &init_lists[i], n) //遍历此优先级链表中所有的函数 { r = t->init(kvm); //调用该初始化函数 if (r < 0) { pr_warning("Failed init: %s/n", t->fn_name); goto fail; } }fail: return r;}一些典型的注册的初始化函数如下, 在接下来的文章中, 我们看一下这些初始化函数到底做了什么

core_init(kvm__init); //注册为核心初始化函数base_init(kvm_cpu__init);dev_base_init(pci__init);dev_base_init(irq__init);

关键词:基础,学习,虚拟

74
73
25
news

版权所有© 亿企邦 1997-2025 保留一切法律许可权利。

为了最佳展示效果,本站不支持IE9及以下版本的浏览器,建议您使用谷歌Chrome浏览器。 点击下载Chrome浏览器
关闭