49 虚拟机
为什么需要虚拟机?
linux服务器越来越强大,但是有时候并需要很大的服务器。

三种虚拟化方式
完全虚拟化 
虚拟化软件会模拟出假的CPU、内存、网络、硬盘等资源,实际资源的获取需要虚拟化软件向物理机申请。虚拟化软件执行转手工作,所以会比较慢

硬件辅助虚拟化
借助Intel-VT,AMD-V等,并整合kvm技术,设置虚拟机状态,虚拟机内核可以在CPU上执行大部分的指令,不需要虚拟化软件在中间转述,除非遇到特别敏感的指令,才需要将标志位设为物理机内核态运行,这样大大提高了效率。

半虚拟化 
知道自己是虚拟机,尝试优化各种资源访问。那我发送网络包,根本就不是发给真正的网络设备,而是给虚拟的设备,我可不可以直接在内存里面拷贝给它。

虚拟化实现方式
如桌面虚拟化软件,VirtualBox;服务器上的虚拟化软件,qemu;
qemu向GuestOS模拟CPU等资源,GuestOS认为自己和硬件直接打交道,其实是同qemu模拟出来的硬件打交道,qemu会将这些指令转译给真正的硬件。
如果是这样,由于所有的指令都要从 qemu 里面过一手,因而性能就会比较差。所以qemu需要结合别的技术一起使用来提升性能,如:
1)使用硬件辅助虚拟化技术 Intel-VT,AMD-V,整合kvm,将 CPU 指令的部分交由虚拟机内核模块来做,提升cpu性能。
2)qemu 采取半虚拟化的方式,让 Guest OS 加载特殊的驱动解决访问其他的硬件慢的问题,如网络和硬盘。
如,网络需要加载 virtio_net,存储需要加载 virtio_blk,Guest需要安装这些半虚拟化驱动,GuestOS 知道自己是虚拟机,所以数据会直接发送给半虚拟化设备,经过特殊处理(例如排队、缓存、批量处理等性能优化方式),最终发送给真正的硬件。这在一定程度上提高了性能。

kvm的理解
为了提高虚拟机软件的性能,在主操作系统中通过内核模块开一个洞,通过这个洞将虚拟机中的操作直接映射到物理硬件上,从而提高虚拟机中运行的操作系统的性能。
如下图:


参考:https://blog.csdn.net/sunylat/article/details/53837938

创建虚拟机
使用qemu-kvm创建一个虚拟机,主要参数如下:
-name ubuntutest 指定虚拟机的名字
-m 1024 指定内存大小为1024
qemu-img create -f qcow2 ubuntutest.img 8G 指定硬盘大小和格式,这里是创建一个虚拟机镜像,大小为 8G,其中 qcow2 格式为动态分配,raw 格式为固定大小。硬盘有两种格式,一个是动态分配,也即开始创建的时候,看起来很大,其实占用的空间很少,真实有多少数据,才真的占用多少空间。一个是固定大小,一开始就占用指定的大小。
-cdrom ubuntu-xxx-server-amd64.iso 将 Ubuntu 的 ISO 挂载为光盘
-vnc:19 采用vnc方式在kvm中创建一个网络,有时候会选择桥接网络,有时候会选择 NAT 网络

所以创建kvm虚拟机的指令如下:
qemu-system-x86_64 -enable-kvm -name ubuntutest -m 2048 -hda ubuntutest.img -cdrom ubuntu-14.04-server-amd64.iso -boot d -vnc :19

然后连接vnc,查看安装过程。按照普通安装 Ubuntu 的流程安装好 Ubuntu,然后 shutdown -h now,关闭虚拟机。

对 KVM 创建桥接网络
一般的虚拟机桥接网络如下图:

Linux 在每台机器上都创建网桥 br0(即虚拟交换机),虚拟机的网卡都连到 br0 上,物理网卡也连到 br0 上,所有的 br0 都通过物理网卡连接到物理交换机上。

qemu-kvm创建上面桥接网络的过程如下:
1. 在 Host 机器上创建 bridge br0。
brctl addbr br0
2. 将 br0 设为 up。ip link set br0 up
3. 创建 tap device。tunctl -b
4. 将 tap0 设为 up。
ip link set tap0 up
5 将 tap0 加入到 br0 上。brctl addif br0 tap0
6. 启动虚拟机, 虚拟机连接 tap0、tap0 连接 br0。
qemu-system-x86_64 -enable-kvm -name ubuntutest -m 2048 -hda ubuntutest.qcow2 -vnc :19 -net nic,model=virtio -nettap,ifname=tap0,script=no,downscript=no
7. 虚拟机启动后,网卡没有配置,所以无法连接外网,先给 br0 设置一个 ip。
ifconfig br0 192.168.57.1/24
8.VNC 连上虚拟机,给网卡设置地址,重启虚拟机,可 ping 通 br0。
9. 要想访问外网,在 Host 上设置 NAT,并且 enable ip forwarding,可以 ping 通外网网关。
# sysctl -p
net.ipv4.ip_forward = 1
sudo iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
10. 如果 DNS 没配错,可以进行 apt-get update。

50-51 虚拟化之cpu
qemu代码解析
1 初始化所有的module
qemu 作为中间层,对虚拟机需要模拟各种各样的外部设备,且能通过这些设备访问物理机资源。
以kvm为例:
会调用type_init,kvm模块函数就是 kvm_type_init,类型就是MODULE_INIT_QOM。最终调用register_module_init注册type,在ModuleTypeList中增加kvm项,且这个 module 的 init 函数就是 kvm_type_init

2 执行初始化
调用module_call_init(MODULE_INIT_QOM);
会找到 MODULE_INIT_QOM 这种类型对应的 ModuleTypeList,找出列表中所有的 ModuleEntry,然后调用每个 ModuleEntry 的 init 函数。

3 实际初始化machine,反射机制,过程如下图:
如图。

4 初始化块设备
调用的是 configure_blockdev。

5 初始化计算虚拟化的加速模式
启用 KVM。这里调用的是 configure_accelerator。名字是kvm,然后调用accel_find,根据名字,得到AccelClass,Class 类实例化为 AccelState。
上面过程中会调用kvm_init方法,里面的操作就从用户态到内核态的 KVM 了。就像前面原理讲过的一样,用户态使用内核态 KVM 的能力,需要打开一个文件 /dev/kvm,这是一个字符设备文件。
KVM 这个字符设备文件定义了一个字符设备文件的操作函数 kvm_chardev_ops,这里面只定义了 ioctl 的操作。接下来,用户态就通过 ioctl 系统调用,调用到 kvm_dev_ioctl 这个函数。
在用户态 qemu 中,调用 KVM_GET_API_VERSION 查看版本号,内核就有相应的分支,返回版本号,如果能够匹配上,则调用 KVM_CREATE_VM 创建虚拟机。
创建虚拟机,需要调用 kvm_dev_ioctl_create_vm,具体过程如下:
1)kvm_create_vm 创建一个 struct kvm 结构。这个结构在内核里面代表一个虚拟机。
2)第二件事情就是创建一个文件描述符,和 struct file 关联起来,这个 struct file 的 file_operations 会被设置为 kvm_vm_fops。
说明:对于一台虚拟机而言,只是在内核中有一个数据结构,对于相应的资源还没有分配,所以我们还需要接着看。

6 初始化网络设备
调用 net_init_clients 进行网络设备的初始化。

7 cpu虚拟化
要调用 machine_run_board_init,里面调用了 MachineClass 的 init 函数,然后调用了pc_init1。重点做了两件事情:
1)CPU 的虚拟化,主要调用 pc_cpus_init;
2)内存的虚拟化,主要调用 pc_memory_init。
这里主要看下pc_cpus_init中对cpu的虚拟化:
每一个 CPU,都调用 pc_new_cpu。
假设cpu的类型是SandyBridge,那么cpu的定义如下
{ "SandyBridge" "-" TYPE_X86_CPU, "min-xlevel", "0x8000000a" }
CPU 这种类的定义是有多层继承关系的。TYPE_X86_CPU 的父类是 TYPE_CPU,TYPE_CPU 的父类是 TYPE_DEVICE,TYPE_DEVICE 的父类是 TYPE_OBJECT。

TYPE_X86_CPU的定义如下:
static const TypeInfo x86_cpu_type_info = {
    .name = TYPE_X86_CPU,
    .parent = TYPE_CPU,
    .instance_size = sizeof(X86CPU),
    .instance_init = x86_cpu_initfn,
    .abstract = true,
    .class_size = sizeof(X86CPUClass),
    .class_init = x86_cpu_common_class_init,
};
每一层都有 class_init,用于从 TypeImpl 生产 xxxClass,也有 instance_init 将 xxxClass 初始化为实例。

cpu虚拟化过程总结:
首先,我们要定义 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 里面,创建一个结构 struct vcpu_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 进行处理。
 

52 虚拟化之内存
虚拟机的内存管理也需要用户态的qemu和内核态的KVM共同完成,二者通过ioctl进行通信。
同时需要借助硬件的EPT技术加速内存的映射。

有了虚拟机,内存就变成了四类:
虚拟机里面的虚拟内存(Guest OS Virtual Memory,GVA),这是虚拟机里面的进程看到的内存空间;
虚拟机里面的物理内存(Guest OS Physical Memory,GPA),这是虚拟机里面的操作系统看到的内存,它认为这是物理内存;
物理机的虚拟内存(Host Virtual Memory,HVA),这是物理机上的 qemu 进程看到的内存空间;
物理机的物理内存(Host Physical Memory,HPA),这是物理机上的操作系统看到的内存。

总结:
虚拟机虚拟内存空间:
在用户态 qemu 中,有一个结构 AddressSpace address_space_memory 来表示虚拟机的系统内存,这个内存可能包含多个内存区域 struct MemoryRegion,组成树形结构,指向由 mmap 分配的虚拟内存。
用户态 qemu 中,对于虚拟机有一个结构 struct KVMState 表示这个虚拟机,这个结构会指向一个数组的 struct KVMSlot 表示这个虚拟机的多个内存条,KVMSlot 中有一个 void *ram 指针指向 mmap 分配的那块虚拟内存。

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

虚拟机物理内存的调用:
虚拟机的虚拟内存AddressSpace 结构中,有一个 struct KVMMemoryListener,当有新的内存区域添加的时候,会被通知调用 kvm_region_add 来通知内核。
kvm_region_add 是通过 ioctl 来通知内核 KVM 的,会给内核 KVM 发送一个 KVM_SET_USER_MEMORY_REGION 消息,表示用户态 qemu 添加了一个内存区域,内核 KVM 也应该添加一个相应的内存区域。

虚拟机的物理内存空间里面的页面映射到宿主机物理页面的时机:
只有当虚拟机的内存被访问的时候,也即 mmap 分配的虚拟内存空间被访问的时候,先查看 EPT 页表,是否已经映射过,如果已经映射过,则经过四级页表映射,就能访问到物理页面。
如果没有映射过,则虚拟机会通过 VM-Exit 指令回到宿主机模式,通过 handle_ept_violation 补充页表映射。先是通过 handle_mm_fault 为虚拟机的物理内存空间分配真正的物理页面,然后通过 __direct_map 添加 EPT 页表映射。


页面分配和映射:
内存映射对于虚拟机来讲是一件非常麻烦的事情,从 GVA 到 GPA 到 HVA 到 HPA,性能很差,为了解决这个问题,有两种主要的思路。
1 影子页表
本来的过程是,客户机要通过 cr3 找到客户机的页表,实现从 GVA 到 GPA 的转换,然后在宿主机上,要通过 cr3 找到宿主机的页表,实现从 HVA 到 HPA 的转换。
所以有一种方式是,客户机中每个进程都有自己的虚拟地址空间,所以 KVM 需要为客户机中的每个进程页表都要维护一套相应的影子页表,实现客户机虚拟地址空间到宿主机物理地址空间的直接映射。
但是影子页表的引入也意味着 KVM 需要为每个客户机的每个进程的页表都要维护一套相应的影子页表,内存占用比较大,而且客户机页表和和影子页表也需要进行实时同步。

2 扩展页表
硬件方式,即intel的EPT(Extent Page Table)技术。
GVA到GPA还是传统方式,EPT页表实现客户机物理地址到宿主机物理地址的另一次映射。
有了 EPT,在客户机物理地址到宿主机物理地址转换的过程中,缺页会产生 EPT 缺页异常。KVM 首先根据引起异常的客户机物理地址,映射到对应的宿主机虚拟地址,然后为此虚拟地址分配新的物理页,最后 KVM 再更新 EPT 页表,建立起引起异常的客户机物理地址到宿主机物理地址之间的映射。
KVM 只需为每个客户机维护一套 EPT 页表,也大大减少了内存的开销。
 

53-54 存储虚拟化
半虚拟下,系统知道硬盘设备和网络设备都是虚拟的,应该加载特殊的驱动才能运行。这些特殊的驱动往往要通过虚拟机里面和外面配合工作的模式,来加速对于物理存储和网络设备的使用。

前端有前端的块设备驱动Front-enddriver,在客户机的内核里面,它符合普通设备驱动的格式,对外通过 VFS 暴露文件系统接口给客户机里面的应用。
后端有后端的设备驱动 Back-end driver,在宿主机的qemu进程中,当收到客户机的写入请求的时候,调用文件系统的 write 函数,写入宿主机的 VFS 文件系统,最终写到物理硬盘设备上的 qcow2 文件。
中间的队列用于前端和后端之间传输数据,在前端的设备驱动和后端的设备驱动,都有类似的数据结构 virt-queue 来管理这些队列。

virtio
虚拟化 I/O 设备,虚拟化技术硬盘设备和网络设备的驱动标准,负责对于虚拟机提供统一的接口。
virtio的架构分为四层:
1)在虚拟机里面的 virtio 前端,针对不同类型的设备有不同的驱动程序,但是接口都是统一的,如virtio_blk,virtio_net等。
2)在宿主机的 qemu 里面,实现 virtio 后端的逻辑,主要就是操作硬件的设备。例如通过写一个物理机硬盘上的文件来完成虚拟机写入硬盘的操作。再如向内核协议栈发送一个网络包完成虚拟机对于网络的操作。
3) virtio 的前端和后端之间,有一个通信层,里面包含 virtio 层和 virtio-ring 层。virtio 这一层实现的是虚拟队列接口,算是前后端通信的桥梁。而 virtio-ring 则是该桥梁的具体实现。
如下图:


以硬盘写入为例,具体看下存储虚拟化的过程:
初始化阶段的存储虚拟化
在学习 CPU 的时候看到的一样,Virtio Block Device 也是一种类。
TYPE_VIRTIO_BLK 的父类是 TYPE_VIRTIO_DEVICE,TYPE_VIRTIO_DEVICE 的父类是 TYPE_DEVICE,TYPE_DEVICE 的父类是 TYPE_OBJECT。
先virtio_init 初始化 VirtIODevice 结构,结构里面有一个 VirtQueue 数组,即virtio 前端和后端互相传数据的队列。
每个队列都调用 virtio_add_queue 来初始化队列。
每个 VirtQueue 中,都有一个 vring,用来维护这个队列里面的数据;另外还有一个函数 virtio_blk_handle_output,用于处理数据写入。

qemu启动过程中的存储虚拟化
qemu的启动参数包括阿
1)宿主机硬盘上的一个文件,文件的格式是qcow2,可以被qemu模拟成为客户机上的一块硬盘,BlockBackend。也对应virtio的后端。
2)驱动是 virtio-blk 驱动。

虚拟机里面的进程如何写入一个文件?
虚拟机里面的进程写入一个文件,当然要通过文件系统。但是设备驱动层不再是普通的硬盘驱动了,而是virtio的驱动。
virtio的驱动程序是drivers/block/virtio_blk.c,它会创建一个 workqueue,注册一个块设备,并获得一个主设备号,然后注册一个驱动函数 virtio_blk。

当一个设备驱动作为一个内核模块被初始化的时候,probe 函数会被调用,这里是virtblk_probe.
1)为virtio_blk定义struct request_queue,拥有make_request_fn函数,用于生成request;另一个是 request_fn 函数,用于处理 request。
2)初始化一个 gendisk
3)init_vq 会来初始化 virtqueu,客户机前端对于队列的管理的数据结构,在客户机的 linux 内核中通过 kmalloc_array 进行分配。t同时会:
a.指定队列的callback函数为virtblk_done.
b.注册一个中断处理函数 vp_interrupt,当设备的配置信息发生改变,会产生一个中断,当设备向队列中写入信息时,也会产生一个中断,我们称为 vq 中断,中断处理函数需要调用相应的队列的回调函数。
c.根据队列的数目,依次调用 vp_setup_vq,完成 virtqueue、vring 的分配和初始化。

虚拟机里面的 virtio 的前端是这样的结构:struct virtio_device 里面有一个 struct vring_virtqueue,在 struct vring_virtqueue 里面有一个 struct vring。


中间virtio队列的管理
上面分配的内存在客户机的内核里面,如何告知 qemu 来访问这段内存呢?
qemu 模拟出来的 virtio block device 只是一个 PCI 设备,对客户机来说是一个外部设备,通过vp_iowrite16,会调用专门给外部设备发送指令的函数 iowrite,告诉外部的PCI设备。

qemu 后端的 VirtIODevice 的 VirtQueue 的 vring 的地址,被设置成了刚才给队列分配的内存的 GPA。

队列的格式如下图:

vring 包含三个成员:
vring_desc 指向分配的内存块,用于存放客户机和 qemu 之间传输的数据。
avail->ring[]是发送端维护的环形队列,指向需要接收端处理的 vring_desc。
used->ring[]是接收端维护的环形队列,指向自己已经处理过了的 vring_desc。


数据写入的流程
1 调用blk_mq_make_request,最终都会调用到 request_queue 的 virtio_mq_ops 的 virtio_queue_rq 函数。
2 在 virtio_queue_rq 中,我们会将请求写入的数据,通过 virtblk_add_req 放入 struct virtqueue。
3 virtio_queue_rq调用 virtqueue_notify 通知接收方。而 virtqueue_notify 会调用 vp_notify
4 写入一个 I/O 会触发qemu的 VM exit,qemu进入宿主机状态,会触发 virtio_ioport_write,这次会调用 virtio_queue_notify,最终调用virtio_blk_handle_vq:
循环中调用函数 virtio_blk_get_request 从 vq 中取出请求,然后调用 virtio_blk_handle_request 处理从 vq 中取出的请求。
5 virtio_blk_handle_request最终会调用submit_requests,会调用qemu启动的时生成的BlockBackend写入文件
6 写入完毕后调用virtio_blk_req_complete,qemu后端调用 virtqueue_push表示空间可以回收利用了。调用 virtio_notify更新客户机中 virtio 前端的 vring 的值,virtio_notify 会调用 virtio_irq 发送一个中断,virtio 前端的 vring得知某块数据qemu 后端已经消费完毕,将其放入空闲队列。

存储虚拟化的场景下,整个写入的过程:
在虚拟机里面,应用层调用 write 系统调用写入文件。
write 系统调用进入虚拟机里面的内核,经过 VFS,通用块设备层,I/O 调度层,到达块设备驱动。
虚拟机里面的块设备驱动是 virtio_blk,它和通用的块设备驱动一样,有一个 request  queue,另外有一个函数 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 会收到这个通知。

55 网络虚拟化
Virtio Network Device 这个设备的初始化
TYPE_VIRTIO_NET 的父类是 TYPE_VIRTIO_DEVICE,TYPE_VIRTIO_DEVICE 的父类是 TYPE_DEVICE,TYPE_DEVICE 的父类是 TYPE_OBJECT。
创建了一个 VirtIODevice,virtio_init 用来初始化这个设备。VirtIODevice 结构里面有一个 VirtQueue 数组,这就是 virtio 前端和后端互相传数据的队列。
每个 VirtQueue 中,都有一个 vring 用来维护这个队列里面的数据;另外还有函数 virtio_net_handle_rx 用于处理网络包的接收,函数 virtio_net_handle_tx_bh 用于网络包的发送。
qemu_new_nic 会创建一个虚拟机里面的网卡。

qemu的启动过程中的网络虚拟化
调用 net_init_clients 进行网络设备的初始化。
会根据不同的 driver 类型,调用不同的初始化函数。假设类型是tap,因而这里会调用 net_init_tap->net_tap_init->tap_open。
tap_open 中,我们打开一个文件"/dev/net/tun",然后通过 ioctl 操作这个文件。即通过打开这个字符设备文件,然后通过 ioctl 操作这个文件和内核打交道,来使用内核的能力。

网络包需要从虚拟机里面发送到虚拟机外面,发送到宿主机上的时候,必须是一个正常的网络包才能被转发:
所以qemu 会将客户机发送给它的网络包,然后转换成为文件流,写入"/dev/net/tun"字符设备。就像写一个文件一样。内核中 TUN/TAP 字符设备驱动会收到这个写入的文件流,然后交给 TUN/TAP 的虚拟网卡驱动。这个驱动会将文件流再次转成网络包,交给 TCP/IP 栈,最终从虚拟 TAP 网卡 tap0 发出来,成为标准的网络包。

如下图:


关联前端设备驱动和后端设备驱动
在客户机中发送一个网络包的时候,会发生哪些事情呢?
虚拟机里面的进程发送一个网络包,通过文件系统和 Socket 调用网络协议栈,到达网络设备层。只不过这个不是普通的网络设备,而是 virtio_net 的驱动。
virtio_net初始化时会注册一个驱动函数virtio_net_driver。

virtio_net的驱动作为一个内核模块被初始化的时候,会调用virtnet_probe:
virtnet_probe 中,会创建 struct net_device,并且通过 register_netdev 注册这个网络设备,这样在客户机里面,就能看到这个网卡了。
virtnet_probe 中,还有一件重要的事情就是,init_vqs 会初始化发送和接收的 virtqueue(有发送和接收两个队列)。


发送网络包的过程
网络包经过客户机的协议栈到达 virtio_net 驱动的时候,按照 net_device_ops 的定义,start_xmit 会被调用。最终调用virtqueue_add,将网络包放入队列中,并调用 virtqueue_notify 通知接收方。
qemu接收到,触发 VM exit,计入宿主机模式。
调用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 虚拟网卡,完成从虚拟机里面到宿主机的整个发送过程。

网络虚拟化场景下网络包的发送过程总结:
在虚拟机里面的用户态,应用程序通过 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 虚拟网卡,完成从虚拟机里面到宿主机的整个发送过程。

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐