《趣谈Linux》总结十:虚拟化

37 虚拟化

Linux越来越强大了,例如,内存动不动就是百G内存,网络设备一个端口的带宽就能有几十G甚至上百G,存储在数据中心至少是PB级别的(一个P是1024个T,一个T是1024个G)。
这自然有了一些不灵活的地方:

· 资源大小不灵活:有时候我们不需要这么大规格的机器,可能只想尝试一下某些新业务,申请个4核8G的
服务器试一下,但是不可能采购这么小规格的机器。
无论每个项目需要多大规格的机器,公司统一采购就限制几种,全部是上面那种大规格的。

· 资源申请不灵活:规格定死就定死吧,可是每次申请机器都要重新采购,周期很长。

· 资源复用不灵活:反正我需要的资源不多,和别人共享一台机器吧,这样不同的进程可能会产生冲突,例
如socket的端口冲突。另外就是别人用过的机器,不知道上面做过哪些操作,有很多的历史包袱,如果重
新安装则代价太大。

Linux采取了在物理机上面创建虚拟机的手段来解决这个问题。
每个虚拟机有自己单独的操作系统、灵活的规格,一个命令就能启动起来。每次创建都是新的操作系统。

三种虚拟化方式:完全虚拟化、硬件辅助虚拟化、半虚拟化

KVM在内核里面需要有一个模块,来设置当前CPU是Guest OS在用,还是Host OS在用。

qemu-kvm工作原理如图:采用半虚拟化(drivers)+硬件辅助虚拟化(/dev/kvm)
在这里插入图片描述

  • 总结

虚拟化的本质是用qemu的软件模拟硬件,但是模拟方式比较慢,需要加速;

虚拟化主要模拟CPU、内存、网络、存储,分别有不同的加速办法;

CPU和内存主要使用硬件辅助虚拟化进行加速,需要配备特殊的硬件才能工作;

网络和存储主要使用特殊的半虚拟化驱动加速,需要加载特殊的驱动程序。

38 虚拟化组件

用户态的 qemu 和内核态的 kvm 如何一起协作,来创建虚拟机,实现 CPU和内存虚拟化?

qemu 为了模拟各种各样的设备,也需要管理各种各样的模块,这些模块也需要符合一定的格式。

  • main函数流程

1 初始化所有的Module

2 解析 qemu 的命令行

3 初始化machine:计算机体系结构,得到MachineClass和MachineState

虚拟机对于设备的模拟是一件非常复杂的事情,需要用复杂的参数模拟各种各样的设备。为了能够适配这些设备,qemu 定义了自己的模块管理机制;
每个模块都会有一个定义 TypeInfo,会通过 type_init 变为全局的 TypeImpl。
TypeInfo以及生成的 TypeImpl 有以下成员:

name 表示当前类型的名称
parent 表示父类的名称
class_init 用于将 TypeImpl 初始化为 MachineClass
instance_init 用于将 MachineClass 初始化为 MachineState

MachineClass:
在这里插入图片描述
4 初始化块设备

5 初始化计算虚拟化的加速模式

6 初始化网络设备

7 CPU虚拟化

在MachineClass的init函数里调用,内存也是这里此函数里进行虚拟化的

客户机和虚拟机管理系统的交互:
在这里插入图片描述
首先定义 CPU 的 TypeInfo 和 TypeImpl、继承关系,并且声明它的类初始化函数。

在 qemu 的 main 函数中调用 MachineClass 的 init 函数,这个函数既会初始化 CPU,也会初始化内存。

CPU 初始化的时候,会调用 pc_new_cpu 创建一个虚拟 CPU,它会调用 CPU 这个类的初始化函数。

每一个虚拟 CPU 会调用 qemu_thread_create 创建一个线程,线程的执行函数为qemu_kvm_cpu_thread_fn。

在虚拟 CPU 对应的线程执行函数中,先是调用kvm_vm_ioctl(KVM_CREATE_VCPU),在内核的 KVM 里面,创建一个结构 structvcpu_vmx,表示这个虚拟 CPU。
在这个结构里面,有一个 VMCS,用于保存当前虚拟机 CPU 的运行时的状态,用于状态切换。

在虚拟 CPU 对应的线程执行函数中,接着调用 kvm_vcpu_ioctl(KVM_RUN),在内核的 KVM 里面运行这个虚拟机 CPU。

运行的方式:保存宿主机的寄存器,加载客户机的寄存器,然后调用 __ex(ASM_VMX_VMLAUNCH)或者__ex(ASM_VMX_VMRESUME),进入客户机模式运行。
一旦退出客户机模式,就会保存客户机寄存器,加载宿主机寄存器,进入宿主机模式运行,并且会记录退出虚拟机模式的原因;大部分的原因是等待 I/O,因而宿主机调用 kvm_handle_io 进行处理。

8 CPU虚拟化后,就开始虚拟化内存,两者在同一个函数内

CPU 的虚拟化是用户态的 qemu 和内核态的 KVM 共同配合完成的。它们二者通过 ioctl 进行通信。
对于内存管理来讲,也是需要这两者配合完成的。

有了虚拟机,内存就变成了四类:

虚拟机里面的虚拟内存(Guest OS Virtual Memory,GVA),这是虚拟机里面的进程看到的内存空间;

虚拟机里面的物理内存(Guest OS Physical Memory,GPA),这是虚拟机里面的操作系统看到的内存,它认为这是物理内存;

物理机的虚拟内存(Host Virtual Memory,HVA),这是物理机上的 qemu 进程看到的内存空间;

物理机的物理内存(Host Physical Memory,HPA),这是物理机上的操作系统看到的内存。

内存映射使用硬件的方式,即Intel 的 EPT(Extent Page Table,扩展页表):EPT 在原有客户机页表对客户机虚拟地址到客户机物理地址映射的基础上,又引入了 EPT页表来实现客户机物理地址到宿主机物理地址的另一次映射。
客户机运行时,客户机页表被载入 CR3,而 EPT 页表被载入专门的 EPT 页表指针寄存器 EPTP。
有了 EPT,在客户机物理地址到宿主机物理地址转换的过程中,缺页会产生 EPT 缺页异常。
KVM 首先根据引起异常的客户机物理地址,映射到对应的宿主机虚拟地址,然后为此虚拟地址分配新的物理页,最后 KVM 再更新 EPT 页表,建立起引起异常的客户机物理地址到宿主机物理地址之间的映射。

总结:

虚拟机的内存管理也是需要用户态的 qemu 和内核态的 KVM 共同完成。

为了加速内存映射,需要借助硬件的 EPT 技术。

在用户态 qemu 中,有一个结构 AddressSpace address_space_memory 来表示虚拟机的系统内存,这个内存可能包含多个内存区域 struct MemoryRegion,组成树形结构,指向由 mmap 分配的虚拟内存。

在 AddressSpace 结构中,有一个 struct KVMMemoryListener,当有新的内存区域添加的时候,会被通知调用 kvm_region_add 来通知内核。

在用户态 qemu 中,对于虚拟机有一个结构 struct KVMState 表示这个虚拟机,这个结构会指向一个数组的 struct KVMSlot 表示这个虚拟机的多个内存条,KVMSlot 中有一个void *ram 指针指向 mmap 分配的那块虚拟内存。

kvm_region_add 是通过 ioctl 来通知内核 KVM 的,会给内核 KVM 发送一个
KVM_SET_USER_MEMORY_REGION 消息,表示用户态 qemu 添加了一个内存区域,内核 KVM 也应该添加一个相应的内存区域。

和用户态 qemu 对应的内核 KVM,对于虚拟机有一个结构 struct kvm 表示这个虚拟机,这个结构会指向一个数组的 struct kvm_memory_slot 表示这个虚拟机的多个内存条,kvm_memory_slot 中有起始页号,页面数目,表示这个虚拟机的物理内存空间。

虚拟机的物理内存空间里面的页面当然不是一开始就映射到物理页面的,只有当虚拟机的内存被访问的时候,也即 mmap 分配的虚拟内存空间被访问的时候,先查看 EPT 页表,是否已经映射过,如果已经映射过,则经过四级页表映射,就能访问到物理页面。

如果没有映射过,则虚拟机会通过 VM-Exit 指令回到宿主机模式,通过handle_ept_violation 补充页表映射。先是通过 handle_mm_fault 为虚拟机的物理内存空间分配真正的物理页面,然后通过 __direct_map 添加 EPT 页表映射。
在这里插入图片描述
接下来是存储虚拟化:

使用virtio:
在这里插入图片描述
前后端驱动使用一个队列接口来通信:
在这里插入图片描述
对于宿主机上的一个文件,可以被 qemu 模拟称为客户机上的一块硬盘

如果一个程序想实现并发,可以创建多个线程,但是线程是一个内核的概念,创建的每一个线程内核都能看到,内核的调度也是以线程为单位的。
这对于普通的进程没有什么问题,但是对于 qemu 这种虚拟机,如果在用户态和内核态切换来切换去,由于还涉及虚拟机的状态,代价比较大。
但是,qemu 的设备也是需要多线程能力的,怎么办呢?
在用户态实现协程,用于实现并发,并且不被内核看到,调度全部在用户态完成。

存储虚拟化的过程分为前端、后端和中间的队列:

前端有前端的块设备驱动 Front-end driver,在客户机的内核里面,它符合普通设备驱动的格式,对外通过 VFS 暴露文件系统接口给客户机里面的应用。

后端有后端的设备驱动 Back-end driver,在宿主机的 qemu 进程中,当收到客户机的写入请求的时候,调用文件系统的 write 函数,写入宿主机的 VFS 文件系统,最终写到物理硬盘设备上的 qcow2 文件。

中间的队列用于前端和后端之间传输数据,在前端的设备驱动和后端的设备驱动,都有类似的数据结构 virt-queue 来管理这些队列,
在这里插入图片描述
qemu 启动了,硬盘设备文件已经打开了。那如果要往虚拟机的一个进程写入一个文件,该怎么做呢?最终这个
文件又是如何落到宿主机上的硬盘文件的呢?
存储虚拟化的场景下,整个写入的过程:

在虚拟机里面,应用层调用 write 系统调用写入文件。

write 系统调用进入虚拟机里面的内核,经过 VFS,通用块设备层,I/O 调度层,到达块设备驱动。

虚拟机里面的块设备驱动是 virtio_blk,它和通用的块设备驱动一样,有一个 requestqueue,另外有一个函数 make_request_fn 会被设置为 blk_mq_make_request,这个函数用于将请求放入队列。

虚拟机里面的块设备驱动是 virtio_blk 会注册一个中断处理函数 vp_interrupt。当qemu 写入完成之后,它会通知虚拟机里面的块设备驱动。

blk_mq_make_request 最终调用 virtqueue_add,将请求添加到传输队列 virtqueue中,然后调用 virtqueue_notify 通知 qemu。

在 qemu 中,本来虚拟机正处于 KVM_RUN 的状态,也即处于客户机状态。

qemu 收到通知后,通过 VM exit 指令退出客户机状态,进入宿主机状态,根据退出原因,得知有 I/O 需要处理。

qemu 调用 virtio_blk_handle_output,最终调用 virtio_blk_handle_vq。

virtio_blk_handle_vq 里面有一个循环,在循环中,virtio_blk_get_request 函数从传输队列中拿出请求,然后调用 virtio_blk_handle_request 处理请求。

virtio_blk_handle_request 会调用 blk_aio_pwritev,通过 BlockBackend 驱动写入qcow2 文件。

写入完毕之后,virtio_blk_req_complete 会调用 virtio_notify 通知虚拟机里面的驱动。
数据写入完成,刚才注册的中断处理函数 vp_interrupt 会收到这个通知。
在这里插入图片描述
在这里插入图片描述

9 网络虚拟化

网络虚拟化有和存储虚拟化类似的地方,例如,它们都是基于 virtio 的
但是,网络虚拟化也有自己的特殊性。例如,存储虚拟化是将宿主机上的文件作为客户机上的硬盘,而网络虚拟化需要依赖于内核协议栈进行网络包的封装与解封装。

过程:

在虚拟机里面的用户态,应用程序通过 write 系统调用写入 socket。

写入的内容经过 VFS 层,内核协议栈,到达虚拟机里面的内核的网络设备驱动,也即virtio_net。

virtio_net 网络设备有一个操作结构 struct net_device_ops,里面定义了发送一个网络包调用的函数为 start_xmit。

在 virtio_net 的前端驱动和 qemu 中的后端驱动之间,有两个队列 virtqueue,一个用于发送,一个用于接收。然后,我们需要在 start_xmit 中调用 virtqueue_add,将网络包放入发送队列,然后调用 virtqueue_notify 通知 qemu。

qemu 本来处于 KVM_RUN 的状态,收到通知后,通过 VM exit 指令退出客户机模式,进入宿主机模式。发送网络包的时候,virtio_net_handle_tx_bh 函数会被调用。

接下来是一个 for 循环,我们需要在循环中调用 virtqueue_pop,从传输队列中获取要发送的数据,然后调用 qemu_sendv_packet_async 进行发送。

qemu 会调用 writev 向字符设备文件写入,进入宿主机的内核。

在宿主机内核中字符设备文件的 file_operations 里面的 write_iter 会被调用,也即会调用 tun_chr_write_iter。

在 tun_chr_write_iter 函数中,tun_get_user 将要发送的网络包从 qemu 拷贝到宿主机内核里面来,然后调用 netif_rx_ni 开始调用宿主机内核协议栈进行处理。

宿主机内核协议栈处理完毕之后,会发送给 tap 虚拟网卡,完成从虚拟机里面到宿主机的整个发送过程。
在这里插入图片描述
在这里插入图片描述
核心是通过队列+通知机制

发布了235 篇原创文章 · 获赞 264 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/qq_41594698/article/details/103156337