mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2026-05-11 04:04:34 +08:00
mod
This commit is contained in:
286
极客时间专栏/趣谈Linux操作系统/核心原理篇:第九部分 虚拟化/49 | 虚拟机:如何成立子公司,让公司变集团?.md
Normal file
286
极客时间专栏/趣谈Linux操作系统/核心原理篇:第九部分 虚拟化/49 | 虚拟机:如何成立子公司,让公司变集团?.md
Normal file
@@ -0,0 +1,286 @@
|
||||
<audio id="audio" title="49 | 虚拟机:如何成立子公司,让公司变集团?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/fb/ea/fb3977764c78de633ef1a1f5df9342ea.mp3"></audio>
|
||||
|
||||
我们前面所有章节涉及的Linux操作系统原理,都是在一台Linux服务器上工作的。在前面的原理阐述中,我们一直把Linux当作一家外包公司的老板来看待。想要管理这么复杂、这么大的一个公司,需要配备咱们前面讲过的所有机制。
|
||||
|
||||
Linux很强大,Linux服务器也随之变得越来越强大了。无论是计算、网络、存储,都越来越牛。例如,内存动不动就是百G内存,网络设备一个端口的带宽就能有几十G甚至上百G,存储在数据中心至少是PB级别的(一个P是1024个T,一个T是1024个G)。
|
||||
|
||||
公司大有大的好处,自然也有大的毛病,也就是咱们常见的“大公司病”——**不灵活**。这里面的不灵活,有下面这几种,我列一下,你看看你是不是都见过。
|
||||
|
||||
- **资源大小不灵活**:有时候我们不需要这么大规格的机器,可能只想尝试一下某些新业务,申请个4核8G的服务器试一下,但是不可能采购这么小规格的机器。无论每个项目需要多大规格的机器,公司统一采购就限制几种,全部是上面那种大规格的。
|
||||
- **资源申请不灵活**:规格定死就定死吧,可是每次申请机器都要重新采购,周期很长。
|
||||
- **资源复用不灵活**:反正我需要的资源不多,和别人共享一台机器吧,这样不同的进程可能会产生冲突,例如socket的端口冲突。另外就是别人用过的机器,不知道上面做过哪些操作,有很多的历史包袱,如果重新安装则代价太大。
|
||||
|
||||
这些是不是和咱们在大公司里面遇到的问题很像?按说,大事情流程严谨没问题,很多小事情也要被拖累走整个流程,而且很容易出现资源冲突,每天跨部门的协调很累人,历史包袱严重,创新没有办法轻装上阵。
|
||||
|
||||
很多公司处理这种问题采取的策略是成立独立的子公司,独立决策,独立运营,往往用于创新型的项目。
|
||||
|
||||
Linux也采取了这样的手段,就是在物理机上面创建虚拟机。每个虚拟机有自己单独的操作系统、灵活的规格,一个命令就能启动起来。每次创建都是新的操作系统,很好地解决了上面不灵活的问题。
|
||||
|
||||
但是要使用虚拟机,还有一些问题需要解决一下。
|
||||
|
||||
我们知道,操作系统上的程序分为两种,一种是用户态的程序,例如Word、Excel等,一种是内核态的程序,例如内核代码、驱动程序等。
|
||||
|
||||
为了区分内核态和用户态,CPU专门设置四个特权等级0、1、2、3来做这个事情。
|
||||
|
||||
当时写Linux内核的时候,估计大牛们还不知道将来虚拟机会大放异彩。大牛们想,一共两级特权,一个内核态,一个用户态,却有四个等级,好奢侈、好富裕,于是就敞开了用。内核态运行在第0等级,用户态运行在第3等级,占了两头,中间的都不用,太不会过日子了。
|
||||
|
||||
大牛们在写Linux内核的时候,如果用户态程序做事情,就将扳手掰到第3等级,一旦要申请使用更多的资源,就需要申请将扳手掰到第0等级,内核才能在高权限访问这些资源,申请完资源,返回到用户态,扳手再掰回去。
|
||||
|
||||
这个程序一直非常顺利地运行着,直到虚拟机出现了。
|
||||
|
||||
## 三种虚拟化方式
|
||||
|
||||
如果你安装VirtualBox桌面版,你可以用这个虚拟化软件创建虚拟机,在虚拟机里面安装一个Linux,外面的操作系统也可以是Linux。VirtualBox这个虚拟化软件,和你的Excel一样,都是在你的任务栏里面并排放着,是一个普通的应用。
|
||||
|
||||
当你进入虚拟机的时候,虚拟机里面的Excel也是一个普通的应用。
|
||||
|
||||
这个时候麻烦的事情出现了,当你设身处地地站在虚拟机的内核角度,去思考一下人生,你就会出现困惑了,会想,我到底是啥?
|
||||
|
||||
在硬件上的操作系统来看,我是一个普通的应用,只能运行在用户态。可是大牛们“生“我的时候,我的每一行代码都告诉我,我是个内核啊,应该运行在内核态。当虚拟机里面的Excel要访问网络的时候,向我请求,我的代码就要努力地去操作网卡。尽管我努力,但是我做不到啊,我没有权限!
|
||||
|
||||
我分裂了……
|
||||
|
||||
怎么办呢?虚拟化层,也就是Virtualbox会帮你解决这个问题,它有三种虚拟化的方式。
|
||||
|
||||
我们先来看第一种方式,**完全虚拟化**(Full virtualization)。其实说白了,这是一种“骗人”的方式。虚拟化软件会模拟假的CPU、内存、网络、硬盘给到我,让我自我感觉良好,感觉自己终于又像个内核了。
|
||||
|
||||
但是,真正的工作模式其实是下面这样的。
|
||||
|
||||
>
|
||||
<p>虚拟机内核说:我要在CPU上跑一个指令!<br>
|
||||
虚拟化软件说:没问题,你是内核嘛,可以跑!<br>
|
||||
虚拟化软件转过头去找物理机内核说:报告,我管理的虚拟机里面的一个要执行一个CPU指令,帮忙来一小段时间空闲的CPU时间,让我代它跑个指令。<br>
|
||||
物理机内核说:你等着,另一个跑着呢。(过了一会儿)它跑完了,该你了。<br>
|
||||
虚拟化软件说:我代它跑,终于跑完了,出来结果了。<br>
|
||||
虚拟化软件转头给虚拟机内核说:哥们儿,跑完了,结果是这个。我说你是内核吧,绝对有权限,没问题,下次跑指令找我啊!<br>
|
||||
虚拟机内核说:看来我真的是内核呢,可是,哥,好像这点儿指令跑得有点慢啊!<br>
|
||||
虚拟化软件说:这就不错啦,好几个排着队跑呢!</p>
|
||||
|
||||
|
||||
内存的申请模式是下面这样的。
|
||||
|
||||
>
|
||||
<p>虚拟机内核说:我启动需要4G内存,我好分给我上面的应用。<br>
|
||||
虚拟化软件说:没问题,才4G,你是内核嘛,我马上申请好。<br>
|
||||
虚拟化软件转头给物理机内核说:报告,我启动了一个虚拟机,需要4G内存,给我4个房间呗。<br>
|
||||
物理机内核:怎么又一个虚拟机啊!好吧,给你90、91、92、93四个房间。<br>
|
||||
虚拟化软件转头给虚拟机内核说:哥们,内存有了,0、1、2、3这个四个房间都是你的。你看,你是内核嘛,独占资源,从0编号的就是你的。<br>
|
||||
虚拟机内核说:看来我真的是内核啊,能从头开始用。那好,我就在房间2的第三个柜子里面放个东西吧!<br>
|
||||
虚拟化软件说:要放东西啊,没问题。但是,它心里想:我查查看,这个虚拟机是90号房间开头的,它要在房间2放东西,那就相当于在房间92放东西。<br>
|
||||
虚拟化软件转头给物理机内核说:报告,我上面的虚拟机要在92号房间的第三个柜子里面放个东西。</p>
|
||||
|
||||
|
||||
好了,说完了CPU和内存的例子,网络和硬盘就不细说了,情况也是类似的,都是虚拟化软件模拟一个给虚拟机内核看的,其实啥事儿都需要虚拟化软件转一遍。
|
||||
|
||||
这种方式一个坏处就是,慢,而且往往慢到不能忍受。
|
||||
|
||||
于是,虚拟化软件想,我能不能不当传话筒,要让虚拟机内核正视自己的身份。别说你是内核,你还真喘上了。你不是物理机,你是虚拟机!
|
||||
|
||||
但是,怎么解决权限等级的问题呢?于是,Intel的VT-x和AMD的AMD-V从硬件层面帮上了忙。当初谁让你们这些写内核的大牛用等级这么奢侈,用完了0,就是3,也不省着点儿用,没办法,只好另起炉灶弄一个新的标志位,表示当前是在虚拟机状态下,还是在真正的物理机内核下。
|
||||
|
||||
对于虚拟机内核来讲,只要将标志位设为虚拟机状态,我们就可以直接在CPU上执行大部分的指令,不需要虚拟化软件在中间转述,除非遇到特别敏感的指令,才需要将标志位设为物理机内核态运行,这样大大提高了效率。
|
||||
|
||||
所以,安装虚拟机的时候,我们务必要将物理CPU的这个标志位打开。想知道是否打开,对于Intel,你可以查看grep “vmx” /proc/cpuinfo;对于AMD,你可以查看grep “svm” /proc/cpuinfo
|
||||
|
||||
这叫作**硬件辅助虚拟化**(Hardware-Assisted Virtualization)。
|
||||
|
||||
另外就是访问网络或者硬盘的时候,为了取得更高的性能,也需要让虚拟机内核加载特殊的驱动,也是让虚拟机内核从代码层面就重新定位自己的身份,不能像访问物理机一样访问网络或者硬盘,而是用一种特殊的方式。
|
||||
|
||||
我知道我不是物理机内核,我知道我是虚拟机,我没那么高的权限,我很可能和很多虚拟机共享物理资源,所以我要学会排队,我写硬盘其实写的是一个物理机上的文件,那我的写文件的缓存方式是不是可以变一下。我发送网络包,根本就不是发给真正的网络设备,而是给虚拟的设备,我可不可以直接在内存里面拷贝给它,等等等等。
|
||||
|
||||
一旦我知道我不是物理机内核,痛定思痛,只好重新认识自己,反而能找出很多方式来优化我的资源访问。
|
||||
|
||||
这叫作**半虚拟化**(Paravirtualization)。
|
||||
|
||||
对于桌面虚拟化软件,我们多采用VirtualBox,如果使用服务器的虚拟化软件,则有另外的选型。
|
||||
|
||||
服务器上的虚拟化软件,多使用qemu,其中关键字emu,全称是emulator,模拟器。所以,单纯使用qemu,采用的是完全虚拟化的模式。
|
||||
|
||||
qemu向Guest OS模拟CPU,也模拟其他的硬件,GuestOS认为自己和硬件直接打交道,其实是同qemu模拟出来的硬件打交道,qemu会将这些指令转译给真正的硬件。由于所有的指令都要从qemu里面过一手,因而性能就会比较差。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/05/fa/058be86de5a43a782392aff3cb8a1ffa.png" alt="">
|
||||
|
||||
按照上面的介绍,完全虚拟化是非常慢的,所以要使用硬件辅助虚拟化技术Intel-VT,AMD-V,所以需要CPU硬件开启这个标志位,一般在BIOS里面设置。
|
||||
|
||||
当确认开始了标志位之后,通过KVM,GuestOS的CPU指令不用经过Qemu转译,直接运行,大大提高了速度。
|
||||
|
||||
所以,KVM在内核里面需要有一个模块,来设置当前CPU是Guest OS在用,还是Host OS在用。
|
||||
|
||||
下面,我们来查看内核模块中是否含有kvm, lsmod | grep kvm。
|
||||
|
||||
KVM内核模块通过/dev/kvm暴露接口,用户态程序可以通过ioctl来访问这个接口。例如,你可以通过下面的流程编写程序。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f5/62/f5ee1a44d7c4890e411c2520507ddc62.png" alt="">
|
||||
|
||||
Qemu将KVM整合进来,将有关CPU指令的部分交由内核模块来做,就是qemu-kvm (qemu-system-XXX)。
|
||||
|
||||
qemu和kvm整合之后,CPU的性能问题解决了。另外Qemu还会模拟其他的硬件,如网络和硬盘。同样,全虚拟化的方式也会影响这些设备的性能。
|
||||
|
||||
于是,qemu采取半虚拟化的方式,让Guest OS加载特殊的驱动来做这件事情。
|
||||
|
||||
例如,网络需要加载virtio_net,存储需要加载virtio_blk,Guest需要安装这些半虚拟化驱动,GuestOS知道自己是虚拟机,所以数据会直接发送给半虚拟化设备,经过特殊处理(例如排队、缓存、批量处理等性能优化方式),最终发送给真正的硬件。这在一定程度上提高了性能。
|
||||
|
||||
至此,整个关系如下图所示。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/f7/22/f748fd6b6b84fa90a1044a92443c3522.png" alt="">
|
||||
|
||||
## 创建虚拟机
|
||||
|
||||
了解了qemu-kvm的工作原理之后,下面我们来看一下,如何使用qemu-kvm创建一个能够上网的虚拟机。
|
||||
|
||||
如果使用VirtualBox创建过虚拟机,通过界面点点就能创建一个能够上网的虚拟机。如果使用qemu-kvm,就没有这么简单了。一切都得自己来做,不过这个过程可以了解KVM虚拟机的创建原理。
|
||||
|
||||
首先,我们要给虚拟机起一个名字,在KVM里面就是-name ubuntutest。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/93/a8/93d2df627d6dfb09cf1c13fee16629a8.png" alt="">
|
||||
|
||||
设置一个内存大小,在KVM里面就是-m 1024。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/db/76/db13672936d08b2c2c28115f16937876.png" alt="">
|
||||
|
||||
创建一个虚拟硬盘,对于VirtualBox是VDI格式,对于KVM则不同。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/13/dd/136d17009a7481cbd472036e4ee83ddd.png" alt="">
|
||||
|
||||
硬盘有两种格式,一个是动态分配,也即开始创建的时候,看起来很大,其实占用的空间很少,真实有多少数据,才真的占用多少空间。一个是固定大小,一开始就占用指定的大小。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c2/66/c201a260cb24b2f39d6ba0ac7b548a66.png" alt="">
|
||||
|
||||
比如,我这台电脑,硬盘的大小为8G。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/9e/6d/9ed4e7712a6fe9f43dae5d1c1ecb9e6d.png" alt="">
|
||||
|
||||
在KVM中,创建一个虚拟机镜像,大小为8G,其中qcow2格式为动态分配,raw格式为固定大小。
|
||||
|
||||
```
|
||||
qemu-img create -f qcow2 ubuntutest.img 8G
|
||||
|
||||
```
|
||||
|
||||
我们将Ubuntu的ISO挂载为光盘,在KVM里面-cdrom [ubuntu-xxx-server-amd64.iso](http://ubuntu-xxx-server-amd64.iso)。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/09/b0/091d934004d10550a87b4d635546d5b0.png" alt="">
|
||||
|
||||
创建一个网络,有时候会选择桥接网络,有时候会选择NAT网络,这个在KVM里面只有自己配置了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6d/5b/6d8733d73192bd1b114df03f01c6275b.png" alt="">
|
||||
|
||||
接下来Virtualbox就会有一个界面,可以看到安装的整个过程,在KVM里面,我们用VNC来做。参数为-vnc :19
|
||||
|
||||
于是,我们也可以创建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,我们也能看到安装的过程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/6a/1e/6afde27eb9bf29b566c47bf60fd56f1e.png" alt="">
|
||||
|
||||
按照普通安装Ubuntu的流程安装好Ubuntu,然后shutdown -h now,关闭虚拟机。
|
||||
|
||||
接下来,我们可以对KVM创建桥接网络了。这个要模拟virtualbox的桥接网络模式。
|
||||
|
||||
如果在桌面虚拟化软件上选择桥接网络,在你的笔记本电脑上,就会形成下面的结构。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2b/47/2b49867c473162d4706553e8cbb5f247.png" alt="">
|
||||
|
||||
每个虚拟机都会有虚拟网卡,在你的笔记本电脑上,会发现多了几个网卡,其实是虚拟交换机。这个虚拟交换机将虚拟机连接在一起。在桥接模式下,物理网卡也连接到这个虚拟交换机上。物理网卡在桌面虚拟化软件的“界面名称”那里选定。
|
||||
|
||||
如果使用桥接网络,当你登录虚拟机里看IP地址时会发现,你的虚拟机的地址和你的笔记本电脑的地址,以及你旁边的同事的电脑的网段是一个网段。这是为什么呢?这其实相当于将物理机和虚拟机放在同一个网桥上,相当于这个网桥上有三台机器,是一个网段的,全部打平了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/78/47/7899a96aaa0b91c165f867d3ec42e947.png" alt="">
|
||||
|
||||
在数据中心里面,采取的也是类似的技术,连接方式如下图所示,只不过是Linux在每台机器上都创建网桥br0,虚拟机的网卡都连到br0上,物理网卡也连到br0上,所有的br0都通过物理网卡连接到物理交换机上。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/da/a7/da83bb01b7ed63ac0062b5cc835099a7.png" alt="">
|
||||
|
||||
同样我们换一个角度看待这个拓扑图。同样是将网络打平,虚拟机会和物理网络具有相同的网段,就相当于两个虚拟交换机、一个物理交换机,一共三个交换机连在一起。两组四个虚拟机和两台物理机都是在一个二层网络里面的。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8e/c6/8e471a287e0181f1b7af56b60b84adc6.png" alt="">
|
||||
|
||||
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。
|
||||
|
||||
在这里,请记住qemu-system-x86_64的启动命令,这里面有CPU虚拟化KVM,有内存虚拟化、硬盘虚拟化、网络虚拟化。接下来的章节,我们会看内核是如何进行虚拟化的。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
今天我们讲了虚拟化的基本原理,并且手动创建一个可以上网的虚拟机。请记住下面这一点,非常重要,理解虚拟机启动的参数就是理解虚拟化技术的入口。学会创建虚拟机,在后面做内核相关实验的时候就会非常方便。
|
||||
|
||||
具体到知识点上,这一节你需要需要记住下面的这些知识点:
|
||||
|
||||
- 虚拟化的本质是用qemu的软件模拟硬件,但是模拟方式比较慢,需要加速;
|
||||
- 虚拟化主要模拟CPU、内存、网络、存储,分别有不同的加速办法;
|
||||
- CPU和内存主要使用硬件辅助虚拟化进行加速,需要配备特殊的硬件才能工作;
|
||||
- 网络和存储主要使用特殊的半虚拟化驱动加速,需要加载特殊的驱动程序。
|
||||
|
||||
## 课堂练习
|
||||
|
||||
请你务必自己使用qemu,按照上面我写的步骤创建一台虚拟机。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
@@ -0,0 +1,541 @@
|
||||
<audio id="audio" title="50 | 计算虚拟化之CPU(上):如何复用集团的人力资源?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/86/8d/864b26555d79e4635efeac6b4580bb8d.mp3"></audio>
|
||||
|
||||
上一节,我们讲了一下虚拟化的基本原理,以及qemu、kvm之间的关系。这一节,我们就来看一下,用户态的qemu和内核态的kvm如何一起协作,来创建虚拟机,实现CPU和内存虚拟化。
|
||||
|
||||
这里是上一节我们讲的qemu启动时候的命令。
|
||||
|
||||
```
|
||||
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
|
||||
|
||||
```
|
||||
|
||||
接下来,我们在[这里下载](https://www.qemu.org/)qemu的代码。qemu的main函数在vl.c下面。这是一个非常非常长的函数,我们来慢慢地解析它。
|
||||
|
||||
## 1.初始化所有的Module
|
||||
|
||||
第一步,初始化所有的Module,调用下面的函数。
|
||||
|
||||
```
|
||||
module_call_init(MODULE_INIT_QOM);
|
||||
|
||||
```
|
||||
|
||||
上一节我们讲过,qemu作为中间人其实挺累的,对上面的虚拟机需要模拟各种各样的外部设备。当虚拟机真的要使用物理资源的时候,对下面的物理机上的资源要进行请求,所以它的工作模式有点儿类似操作系统对接驱动。驱动要符合一定的格式,才能算操作系统的一个模块。同理,qemu为了模拟各种各样的设备,也需要管理各种各样的模块,这些模块也需要符合一定的格式。
|
||||
|
||||
定义一个qemu模块会调用type_init。例如,kvm的模块要在accel/kvm/kvm-all.c文件里面实现。在这个文件里面,有一行下面的代码:
|
||||
|
||||
```
|
||||
type_init(kvm_type_init);
|
||||
|
||||
#define type_init(function) module_init(function, MODULE_INIT_QOM)
|
||||
|
||||
#define module_init(function, type) \
|
||||
static void __attribute__((constructor)) do_qemu_init_ ## function(void) \
|
||||
{ \
|
||||
register_module_init(function, type); \
|
||||
}
|
||||
|
||||
void register_module_init(void (*fn)(void), module_init_type type)
|
||||
{
|
||||
ModuleEntry *e;
|
||||
ModuleTypeList *l;
|
||||
|
||||
e = g_malloc0(sizeof(*e));
|
||||
e->init = fn;
|
||||
e->type = type;
|
||||
|
||||
l = find_type(type);
|
||||
|
||||
QTAILQ_INSERT_TAIL(l, e, node);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
从代码里面的定义我们可以看出来,type_init后面的参数是一个函数,调用type_init就相当于调用module_init,在这里函数就是kvm_type_init,类型就是MODULE_INIT_QOM。是不是感觉和驱动有点儿像?
|
||||
|
||||
module_init最终要调用register_module_init。属于MODULE_INIT_QOM这种类型的,有一个Module列表ModuleTypeList,列表里面是一项一项的ModuleEntry。KVM就是其中一项,并且会初始化每一项的init函数为参数表示的函数fn,也即KVM这个module的init函数就是kvm_type_init。
|
||||
|
||||
当然,MODULE_INIT_QOM这种类型会有很多很多的module,从后面的代码我们可以看到,所有调用type_init的地方都注册了一个MODULE_INIT_QOM类型的Module。
|
||||
|
||||
了解了Module的注册机制,我们继续回到main函数中module_call_init的调用。
|
||||
|
||||
```
|
||||
void module_call_init(module_init_type type)
|
||||
{
|
||||
ModuleTypeList *l;
|
||||
ModuleEntry *e;
|
||||
l = find_type(type);
|
||||
QTAILQ_FOREACH(e, l, node) {
|
||||
e->init();
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在module_call_init中,我们会找到MODULE_INIT_QOM这种类型对应的ModuleTypeList,找出列表中所有的ModuleEntry,然后调用每个ModuleEntry的init函数。这里需要注意的是,在module_call_init调用的这一步,所有Module的init函数都已经被调用过了。
|
||||
|
||||
后面我们会看到很多的Module,当你看到它们的时候,你需要意识到,它的init函数在这里也被调用过了。这里我们还是以对于kvm这个module为例子,看看它的init函数都做了哪些事情。你会发现,其实它调用的是kvm_type_init。
|
||||
|
||||
```
|
||||
static void kvm_type_init(void)
|
||||
{
|
||||
type_register_static(&kvm_accel_type);
|
||||
}
|
||||
|
||||
TypeImpl *type_register_static(const TypeInfo *info)
|
||||
{
|
||||
return type_register(info);
|
||||
}
|
||||
|
||||
TypeImpl *type_register(const TypeInfo *info)
|
||||
{
|
||||
assert(info->parent);
|
||||
return type_register_internal(info);
|
||||
}
|
||||
|
||||
static TypeImpl *type_register_internal(const TypeInfo *info)
|
||||
{
|
||||
TypeImpl *ti;
|
||||
ti = type_new(info);
|
||||
|
||||
type_table_add(ti);
|
||||
return ti;
|
||||
}
|
||||
|
||||
static TypeImpl *type_new(const TypeInfo *info)
|
||||
{
|
||||
TypeImpl *ti = g_malloc0(sizeof(*ti));
|
||||
int i;
|
||||
|
||||
if (type_table_lookup(info->name) != NULL) {
|
||||
}
|
||||
|
||||
ti->name = g_strdup(info->name);
|
||||
ti->parent = g_strdup(info->parent);
|
||||
|
||||
ti->class_size = info->class_size;
|
||||
ti->instance_size = info->instance_size;
|
||||
|
||||
ti->class_init = info->class_init;
|
||||
ti->class_base_init = info->class_base_init;
|
||||
ti->class_data = info->class_data;
|
||||
|
||||
ti->instance_init = info->instance_init;
|
||||
ti->instance_post_init = info->instance_post_init;
|
||||
ti->instance_finalize = info->instance_finalize;
|
||||
|
||||
ti->abstract = info->abstract;
|
||||
|
||||
for (i = 0; info->interfaces && info->interfaces[i].type; i++) {
|
||||
ti->interfaces[i].typename = g_strdup(info->interfaces[i].type);
|
||||
}
|
||||
ti->num_interfaces = i;
|
||||
|
||||
return ti;
|
||||
}
|
||||
|
||||
static void type_table_add(TypeImpl *ti)
|
||||
{
|
||||
assert(!enumerating_types);
|
||||
g_hash_table_insert(type_table_get(), (void *)ti->name, ti);
|
||||
}
|
||||
|
||||
static GHashTable *type_table_get(void)
|
||||
{
|
||||
static GHashTable *type_table;
|
||||
|
||||
if (type_table == NULL) {
|
||||
type_table = g_hash_table_new(g_str_hash, g_str_equal);
|
||||
}
|
||||
|
||||
return type_table;
|
||||
}
|
||||
|
||||
static const TypeInfo kvm_accel_type = {
|
||||
.name = TYPE_KVM_ACCEL,
|
||||
.parent = TYPE_ACCEL,
|
||||
.class_init = kvm_accel_class_init,
|
||||
.instance_size = sizeof(KVMState),
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
每一个Module既然要模拟某种设备,那应该定义一种类型TypeImpl来表示这些设备,这其实是一种面向对象编程的思路,只不过这里用的是纯C语言的实现,所以需要变相实现一下类和对象。
|
||||
|
||||
kvm_type_init会注册kvm_accel_type,定义上面的代码,我们可以认为这样动态定义了一个类。这个类的名字是TYPE_KVM_ACCEL,这个类有父类TYPE_ACCEL,这个类的初始化应该调用函数kvm_accel_class_init(看,这里已经直接叫类class了)。如果用这个类声明一个对象,对象的大小应该是instance_size。是不是有点儿Java语言反射的意思,根据一些名称的定义,一个类就定义好了。
|
||||
|
||||
这里的调用链为:kvm_type_init->type_register_static->type_register->type_register_internal。
|
||||
|
||||
在type_register_internal中,我们会根据kvm_accel_type这个TypeInfo,创建一个TypeImpl来表示这个新注册的类,也就是说,TypeImpl才是我们想要声明的那个class。在qemu里面,有一个全局的哈希表type_table,用来存放所有定义的类。在type_new里面,我们先从全局表里面根据名字找这个类。如果找到,说明这个类曾经被注册过,就报错;如果没有找到,说明这是一个新的类,则将TypeInfo里面信息填到TypeImpl里面。type_table_add会将这个类注册到全局的表里面。到这里,我们注意,class_init还没有被调用,也即这个类现在还处于纸面的状态。
|
||||
|
||||
这点更加像Java的反射机制了。在Java里面,对于一个类,首先我们写代码的时候要写一个class xxx的定义,编译好就放在.class文件中,这也是出于纸面的状态。然后,Java会有一个Class对象,用于读取和表示这个纸面上的class xxx,可以生成真正的对象。
|
||||
|
||||
相同的过程在后面的代码中我们也可以看到,class_init会生成XXXClass,就相当于Java里面的Class对象,TypeImpl还会有一个instance_init函数,相当于构造函数,用于根据XXXClass生成Object,这就相当于Java反射里面最终创建的对象。和构造函数对应的还有instance_finalize,相当于析构函数。
|
||||
|
||||
这一套反射机制放在qom文件夹下面,全称QEMU Object Model,也即用C实现了一套面向对象的反射机制。
|
||||
|
||||
说完了初始化Module,我们还回到main函数接着分析。
|
||||
|
||||
## 2.解析qemu的命令行
|
||||
|
||||
第二步我们就要开始解析qemu的命令行了。qemu的命令行解析,就是下面这样一长串。还记得咱们自己写过一个解析命令行参数的程序吗?这里的opts是差不多的意思。
|
||||
|
||||
```
|
||||
qemu_add_opts(&qemu_drive_opts);
|
||||
qemu_add_opts(&qemu_chardev_opts);
|
||||
qemu_add_opts(&qemu_device_opts);
|
||||
qemu_add_opts(&qemu_netdev_opts);
|
||||
qemu_add_opts(&qemu_nic_opts);
|
||||
qemu_add_opts(&qemu_net_opts);
|
||||
qemu_add_opts(&qemu_rtc_opts);
|
||||
qemu_add_opts(&qemu_machine_opts);
|
||||
qemu_add_opts(&qemu_accel_opts);
|
||||
qemu_add_opts(&qemu_mem_opts);
|
||||
qemu_add_opts(&qemu_smp_opts);
|
||||
qemu_add_opts(&qemu_boot_opts);
|
||||
qemu_add_opts(&qemu_name_opts);
|
||||
qemu_add_opts(&qemu_numa_opts);
|
||||
|
||||
```
|
||||
|
||||
为什么有这么多的opts呢?这是因为,我们上一节给的参数都是简单的参数,实际运行中创建的kvm参数会复杂N倍。这里我们贴一个开源云平台软件OpenStack创建出来的KVM的参数,如下所示。不要被吓坏,你不需要全部看懂,只需要看懂一部分就行了。具体我来给你解析。
|
||||
|
||||
```
|
||||
qemu-system-x86_64
|
||||
-enable-kvm
|
||||
-name instance-00000024
|
||||
-machine pc-i440fx-trusty,accel=kvm,usb=off
|
||||
-cpu SandyBridge,+erms,+smep,+fsgsbase,+pdpe1gb,+rdrand,+f16c,+osxsave,+dca,+pcid,+pdcm,+xtpr,+tm2,+est,+smx,+vmx,+ds_cpl,+monitor,+dtes64,+pbe,+tm,+ht,+ss,+acpi,+ds,+vme
|
||||
-m 2048
|
||||
-smp 1,sockets=1,cores=1,threads=1
|
||||
......
|
||||
-rtc base=utc,driftfix=slew
|
||||
-drive file=/var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/disk,if=none,id=drive-virtio-disk0,format=qcow2,cache=none
|
||||
-device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x4,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=1
|
||||
-netdev tap,fd=32,id=hostnet0,vhost=on,vhostfd=37
|
||||
-device virtio-net-pci,netdev=hostnet0,id=net0,mac=fa:16:3e:d1:2d:99,bus=pci.0,addr=0x3
|
||||
-chardev file,id=charserial0,path=/var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/console.log
|
||||
-vnc 0.0.0.0:12
|
||||
-device cirrus-vga,id=video0,bus=pci.0,addr=0x2
|
||||
|
||||
```
|
||||
|
||||
<li>
|
||||
-enable-kvm:表示启用硬件辅助虚拟化。
|
||||
</li>
|
||||
<li>
|
||||
-name instance-00000024:表示虚拟机的名称。
|
||||
</li>
|
||||
<li>
|
||||
<p>-machine pc-i440fx-trusty,accel=kvm,usb=off:machine是什么呢?其实就是计算机体系结构。不知道什么是体系结构的话,可以订阅极客时间的另一个专栏《深入浅出计算机组成原理》。<br>
|
||||
qemu会模拟多种体系结构,常用的有普通PC机,也即x86的32位或者64位的体系结构、Mac电脑PowerPC的体系结构、Sun的体系结构、MIPS的体系结构,精简指令集。如果使用KVM hardware-assisted virtualization,也即BIOS中VD-T是打开的,则参数中accel=kvm。如果不使用hardware-assisted virtualization,用的是纯模拟,则有参数accel = tcg,-no-kvm。</p>
|
||||
</li>
|
||||
<li>
|
||||
-cpu SandyBridge,+erms,+smep,+fsgsbase,+pdpe1gb,+rdrand,+f16c,+osxsave,+dca,+pcid,+pdcm,+xtpr,+tm2,+est,+smx,+vmx,+ds_cpl,+monitor,+dtes64,+pbe,+tm,+ht,+ss,+acpi,+ds,+vme:表示设置CPU,SandyBridge是Intel处理器,后面的加号都是添加的CPU的参数,这些参数会显示在/proc/cpuinfo里面。
|
||||
</li>
|
||||
<li>
|
||||
-m 2048:表示内存。
|
||||
</li>
|
||||
<li>
|
||||
<p>-smp 1,sockets=1,cores=1,threads=1:SMP我们解析过,叫对称多处理器,和NUMA对应。qemu仿真了一个具有1个vcpu,一个socket,一个core,一个threads的处理器。<br>
|
||||
socket、core、threads是什么概念呢?socket就是主板上插cpu的槽的数目,也即常说的“路”,core就是我们平时说的“核”,即双核、4核等。thread就是每个core的硬件线程数,即超线程。举个具体的例子,某个服务器是:2路4核超线程(一般默认为2个线程),通过cat /proc/cpuinfo,我们看到的是2**4**2=16个processor,很多人也习惯成为16核了。</p>
|
||||
</li>
|
||||
<li>
|
||||
-rtc base=utc,driftfix=slew:表示系统时间由参数-rtc指定。
|
||||
</li>
|
||||
<li>
|
||||
-device cirrus-vga,id=video0,bus=pci.0,addr=0x2:表示显示器用参数-vga设置,默认为cirrus,它模拟了CL-GD5446PCI VGA card。
|
||||
</li>
|
||||
<li>
|
||||
有关网卡,使用-net参数和-device。
|
||||
</li>
|
||||
<li>
|
||||
从HOST角度:-netdev tap,fd=32,id=hostnet0,vhost=on,vhostfd=37。
|
||||
</li>
|
||||
<li>
|
||||
从GUEST角度:-device virtio-net-pci,netdev=hostnet0,id=net0,mac=fa:16:3e:d1:2d:99,bus=pci.0,addr=0x3。
|
||||
</li>
|
||||
<li>
|
||||
有关硬盘,使用-hda -hdb,或者使用-drive和-device。
|
||||
</li>
|
||||
<li>
|
||||
从HOST角度:-drive file=/var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/disk,if=none,id=drive-virtio-disk0,format=qcow2,cache=none
|
||||
</li>
|
||||
<li>
|
||||
从GUEST角度:-device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x4,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=1
|
||||
</li>
|
||||
<li>
|
||||
-vnc 0.0.0.0:12:设置VNC。
|
||||
</li>
|
||||
|
||||
在main函数中,接下来的for循环和大量的switch case语句,就是对于这些参数的解析,我们不一一解析,后面真的用到这些参数的时候,我们再仔细看。
|
||||
|
||||
## 3.初始化machine
|
||||
|
||||
回到main函数,接下来是初始化machine。
|
||||
|
||||
```
|
||||
machine_class = select_machine();
|
||||
current_machine = MACHINE(object_new(object_class_get_name(
|
||||
OBJECT_CLASS(machine_class))));
|
||||
|
||||
```
|
||||
|
||||
这里面的machine_class是什么呢?这还得从machine参数说起。
|
||||
|
||||
```
|
||||
-machine pc-i440fx-trusty,accel=kvm,usb=off
|
||||
|
||||
```
|
||||
|
||||
这里的pc-i440fx是x86机器默认的体系结构。在hw/i386/pc_piix.c中,它定义了对应的machine_class。
|
||||
|
||||
```
|
||||
DEFINE_I440FX_MACHINE(v4_0, "pc-i440fx-4.0", NULL,
|
||||
pc_i440fx_4_0_machine_options);
|
||||
|
||||
#define DEFINE_I440FX_MACHINE(suffix, name, compatfn, optionfn) \
|
||||
static void pc_init_##suffix(MachineState *machine) \
|
||||
{ \
|
||||
......
|
||||
pc_init1(machine, TYPE_I440FX_PCI_HOST_BRIDGE, \
|
||||
TYPE_I440FX_PCI_DEVICE); \
|
||||
} \
|
||||
DEFINE_PC_MACHINE(suffix, name, pc_init_##suffix, optionfn)
|
||||
|
||||
|
||||
#define DEFINE_PC_MACHINE(suffix, namestr, initfn, optsfn) \
|
||||
static void pc_machine_##suffix##_class_init(ObjectClass *oc, void *data
|
||||
) \
|
||||
{ \
|
||||
MachineClass *mc = MACHINE_CLASS(oc); \
|
||||
optsfn(mc); \
|
||||
mc->init = initfn; \
|
||||
} \
|
||||
static const TypeInfo pc_machine_type_##suffix = { \
|
||||
.name = namestr TYPE_MACHINE_SUFFIX, \
|
||||
.parent = TYPE_PC_MACHINE, \
|
||||
.class_init = pc_machine_##suffix##_class_init, \
|
||||
}; \
|
||||
static void pc_machine_init_##suffix(void) \
|
||||
{ \
|
||||
type_register(&pc_machine_type_##suffix); \
|
||||
} \
|
||||
type_init(pc_machine_init_##suffix)
|
||||
|
||||
```
|
||||
|
||||
为了定义machine_class,这里有一系列的宏定义。入口是DEFINE_I440FX_MACHINE。这个宏有几个参数,v4_0是后缀,"pc-i440fx-4.0"是名字,pc_i440fx_4_0_machine_options是一个函数,用于定义machine_class相关的选项。这个函数定义如下:
|
||||
|
||||
```
|
||||
static void pc_i440fx_4_0_machine_options(MachineClass *m)
|
||||
{
|
||||
pc_i440fx_machine_options(m);
|
||||
m->alias = "pc";
|
||||
m->is_default = 1;
|
||||
}
|
||||
|
||||
static void pc_i440fx_machine_options(MachineClass *m)
|
||||
{
|
||||
PCMachineClass *pcmc = PC_MACHINE_CLASS(m);
|
||||
pcmc->default_nic_model = "e1000";
|
||||
|
||||
m->family = "pc_piix";
|
||||
m->desc = "Standard PC (i440FX + PIIX, 1996)";
|
||||
m->default_machine_opts = "firmware=bios-256k.bin";
|
||||
m->default_display = "std";
|
||||
machine_class_allow_dynamic_sysbus_dev(m, TYPE_RAMFB_DEVICE);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们先不看pc_i440fx_4_0_machine_options,先来看DEFINE_I440FX_MACHINE。
|
||||
|
||||
这里面定义了一个pc_init_##suffix,也就是pc_init_v4_0。这里面转而调用pc_init1。注意这里这个函数只是定义了一下,没有被调用。
|
||||
|
||||
接下来,DEFINE_I440FX_MACHINE里面又定义了DEFINE_PC_MACHINE。它有四个参数,除了DEFINE_I440FX_MACHINE传进来的三个参数以外,多了一个initfn,也即初始化函数,指向刚才定义的pc_init_##suffix。
|
||||
|
||||
在DEFINE_PC_MACHINE中,我们定义了一个函数pc_machine_##suffix##**class_init。从函数的名字class_init可以看出,这是machine_class从纸面上的class初始化为Class对象的方法。在这个函数里面,我们可以看到,它创建了一个MachineClass对象,这个就是Class对象。MachineClass对象的init函数指向上面定义的pc_init**##suffix,说明这个函数是machine这种类型初始化的一个函数,后面会被调用。
|
||||
|
||||
接着,我们看DEFINE_PC_MACHINE。它定义了一个pc_machine_type_##suffix的TypeInfo。这是用于生成纸面上的class的原材料,果真后面调用了type_init。
|
||||
|
||||
看到了type_init,我们应该能够想到,既然它定义了一个纸面上的class,那上面的那句module_call_init,会和我们上面解析的type_init是一样的,在全局的表里面注册了一个全局的名字是"pc-i440fx-4.0"的纸面上的class,也即TypeImpl。
|
||||
|
||||
现在全局表中有这个纸面上的class了。我们回到select_machine。
|
||||
|
||||
```
|
||||
static MachineClass *select_machine(void)
|
||||
{
|
||||
MachineClass *machine_class = find_default_machine();
|
||||
const char *optarg;
|
||||
QemuOpts *opts;
|
||||
......
|
||||
opts = qemu_get_machine_opts();
|
||||
qemu_opts_loc_restore(opts);
|
||||
|
||||
optarg = qemu_opt_get(opts, "type");
|
||||
if (optarg) {
|
||||
machine_class = machine_parse(optarg);
|
||||
}
|
||||
......
|
||||
return machine_class;
|
||||
}
|
||||
|
||||
MachineClass *find_default_machine(void)
|
||||
{
|
||||
GSList *el, *machines = object_class_get_list(TYPE_MACHINE, false);
|
||||
MachineClass *mc = NULL;
|
||||
for (el = machines; el; el = el->next) {
|
||||
MachineClass *temp = el->data;
|
||||
if (temp->is_default) {
|
||||
mc = temp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
g_slist_free(machines);
|
||||
return mc;
|
||||
}
|
||||
|
||||
static MachineClass *machine_parse(const char *name)
|
||||
{
|
||||
MachineClass *mc = NULL;
|
||||
GSList *el, *machines = object_class_get_list(TYPE_MACHINE, false);
|
||||
|
||||
if (name) {
|
||||
mc = find_machine(name);
|
||||
}
|
||||
if (mc) {
|
||||
g_slist_free(machines);
|
||||
return mc;
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在select_machine中,有两种方式可以生成MachineClass。一种方式是find_default_machine,找一个默认的;另一种方式是machine_parse,通过解析参数生成MachineClass。无论哪种方式,都会调用object_class_get_list获得一个MachineClass的列表,然后在里面找。object_class_get_list定义如下:
|
||||
|
||||
```
|
||||
GSList *object_class_get_list(const char *implements_type,
|
||||
bool include_abstract)
|
||||
{
|
||||
GSList *list = NULL;
|
||||
|
||||
object_class_foreach(object_class_get_list_tramp,
|
||||
implements_type, include_abstract, &list);
|
||||
return list;
|
||||
}
|
||||
|
||||
void object_class_foreach(void (*fn)(ObjectClass *klass, void *opaque), const char *implements_type, bool include_abstract,
|
||||
void *opaque)
|
||||
{
|
||||
OCFData data = { fn, implements_type, include_abstract, opaque };
|
||||
|
||||
enumerating_types = true;
|
||||
g_hash_table_foreach(type_table_get(), object_class_foreach_tramp, &data);
|
||||
enumerating_types = false;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在全局表type_table_get()中,对于每一项TypeImpl,我们都执行object_class_foreach_tramp。
|
||||
|
||||
```
|
||||
static void object_class_foreach_tramp(gpointer key, gpointer value,
|
||||
gpointer opaque)
|
||||
{
|
||||
OCFData *data = opaque;
|
||||
TypeImpl *type = value;
|
||||
ObjectClass *k;
|
||||
|
||||
type_initialize(type);
|
||||
k = type->class;
|
||||
......
|
||||
data->fn(k, data->opaque);
|
||||
}
|
||||
|
||||
static void type_initialize(TypeImpl *ti)
|
||||
{
|
||||
TypeImpl *parent;
|
||||
......
|
||||
ti->class_size = type_class_get_size(ti);
|
||||
ti->instance_size = type_object_get_size(ti);
|
||||
if (ti->instance_size == 0) {
|
||||
ti->abstract = true;
|
||||
}
|
||||
......
|
||||
ti->class = g_malloc0(ti->class_size);
|
||||
......
|
||||
ti->class->type = ti;
|
||||
|
||||
while (parent) {
|
||||
if (parent->class_base_init) {
|
||||
parent->class_base_init(ti->class, ti->class_data);
|
||||
}
|
||||
parent = type_get_parent(parent);
|
||||
}
|
||||
|
||||
if (ti->class_init) {
|
||||
ti->class_init(ti->class, ti->class_data);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在object_class_foreach_tramp中,会调用将type_initialize,这里面会调用class_init将纸面上的class也即TypeImpl变为ObjectClass,ObjectClass是所有Class类的祖先,MachineClass是它的子类。
|
||||
|
||||
因为在machine的命令行里面,我们指定了名字为"pc-i440fx-4.0",就肯定能够找到我们注册过了的TypeImpl,并调用它的class_init函数。
|
||||
|
||||
因而pc_machine_##suffix##**class_init会被调用,在这里面,pc_i440fx_machine_options才真正被调用初始化MachineClass,并且将MachineClass的init函数设置为pc_init**##suffix。也即,当select_machine执行完毕后,就有一个MachineClass了。
|
||||
|
||||
接着,我们回到object_new。这就很好理解了,MachineClass是一个Class类,接下来应该通过它生成一个Instance,也即对象,这就是object_new的作用。
|
||||
|
||||
```
|
||||
Object *object_new(const char *typename)
|
||||
{
|
||||
TypeImpl *ti = type_get_by_name(typename);
|
||||
|
||||
return object_new_with_type(ti);
|
||||
}
|
||||
|
||||
static Object *object_new_with_type(Type type)
|
||||
{
|
||||
Object *obj;
|
||||
type_initialize(type);
|
||||
obj = g_malloc(type->instance_size);
|
||||
object_initialize_with_type(obj, type->instance_size, type);
|
||||
obj->free = g_free;
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
object_new中,TypeImpl的instance_init会被调用,创建一个对象。current_machine就是这个对象,它的类型是MachineState。
|
||||
|
||||
至此,绕了这么大一圈,有关体系结构的对象才创建完毕,接下来很多的设备的初始化,包括CPU和内存的初始化,都是围绕着体系结构的对象来的,后面我们会常常看到current_machine。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
这一节,我们学到,虚拟机对于设备的模拟是一件非常复杂的事情,需要用复杂的参数模拟各种各样的设备。为了能够适配这些设备,qemu定义了自己的模块管理机制,只有了解了这种机制,后面看每一种设备的虚拟化的时候,才有一个整体的思路。
|
||||
|
||||
这里的MachineClass是我们遇到的第一个,我们需要掌握它里面各种定义之间的关系。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/07/30/078dc698ef1b3df93ee9569e55ea2f30.png" alt="">
|
||||
|
||||
每个模块都会有一个定义TypeInfo,会通过type_init变为全局的TypeImpl。TypeInfo以及生成的TypeImpl有以下成员:
|
||||
|
||||
- name表示当前类型的名称
|
||||
- parent表示父类的名称
|
||||
- class_init用于将TypeImpl初始化为MachineClass
|
||||
- instance_init用于将MachineClass初始化为MachineState
|
||||
|
||||
所以,以后遇到任何一个类型的时候,将父类和子类之间的关系,以及对应的初始化函数都要看好,这样就一目了然了。
|
||||
|
||||
## 课堂练习
|
||||
|
||||
你可能会问,这么复杂的qemu命令,我是怎么找到的,当然不是我一个字一个字打的,这是著名的云平台管理软件OpenStack创建虚拟机的时候自动生成的命令行。所以,给你留一道课堂练习题,请你看一下OpenStack的基本原理,看它是通过什么工具来管理如此复杂的命令行的。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解,也欢迎可以收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
|
||||
@@ -0,0 +1,887 @@
|
||||
<audio id="audio" title="51 | 计算虚拟化之CPU(下):如何复用集团的人力资源?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/6e/f6/6e9eb376b3f8ef84f8af74b699194cf6.mp3"></audio>
|
||||
|
||||
上一节qemu初始化的main函数,我们解析了一个开头,得到了表示体系结构的MachineClass以及MachineState。
|
||||
|
||||
## 4.初始化块设备
|
||||
|
||||
我们接着回到main函数,接下来初始化的是块设备,调用的是configure_blockdev。这里我们需要重点关注上面参数中的硬盘,不过我们放在存储虚拟化那一节再解析。
|
||||
|
||||
```
|
||||
configure_blockdev(&bdo_queue, machine_class, snapshot);
|
||||
|
||||
```
|
||||
|
||||
## 5.初始化计算虚拟化的加速模式
|
||||
|
||||
接下来初始化的是计算虚拟化的加速模式,也即要不要使用KVM。根据参数中的配置是启用KVM。这里调用的是configure_accelerator。
|
||||
|
||||
```
|
||||
configure_accelerator(current_machine, argv[0]);
|
||||
|
||||
void configure_accelerator(MachineState *ms, const char *progname)
|
||||
{
|
||||
const char *accel;
|
||||
char **accel_list, **tmp;
|
||||
int ret;
|
||||
bool accel_initialised = false;
|
||||
bool init_failed = false;
|
||||
AccelClass *acc = NULL;
|
||||
|
||||
accel = qemu_opt_get(qemu_get_machine_opts(), "accel");
|
||||
accel = "kvm";
|
||||
accel_list = g_strsplit(accel, ":", 0);
|
||||
|
||||
for (tmp = accel_list; !accel_initialised && tmp && *tmp; tmp++) {
|
||||
acc = accel_find(*tmp);
|
||||
ret = accel_init_machine(acc, ms);
|
||||
}
|
||||
}
|
||||
|
||||
static AccelClass *accel_find(const char *opt_name)
|
||||
{
|
||||
char *class_name = g_strdup_printf(ACCEL_CLASS_NAME("%s"), opt_name);
|
||||
AccelClass *ac = ACCEL_CLASS(object_class_by_name(class_name));
|
||||
g_free(class_name);
|
||||
return ac;
|
||||
}
|
||||
|
||||
static int accel_init_machine(AccelClass *acc, MachineState *ms)
|
||||
{
|
||||
ObjectClass *oc = OBJECT_CLASS(acc);
|
||||
const char *cname = object_class_get_name(oc);
|
||||
AccelState *accel = ACCEL(object_new(cname));
|
||||
int ret;
|
||||
ms->accelerator = accel;
|
||||
*(acc->allowed) = true;
|
||||
ret = acc->init_machine(ms);
|
||||
return ret;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在configure_accelerator中,我们看命令行参数里面的accel,发现是kvm,则调用accel_find根据名字,得到相应的纸面上的class,并初始化为Class类。
|
||||
|
||||
MachineClass是计算机体系结构的Class类,同理,AccelClass就是加速器的Class类,然后调用accel_init_machine,通过object_new,将AccelClass这个Class类实例化为AccelState,类似对于体系结构的实例是MachineState。
|
||||
|
||||
在accel_find中,我们会根据名字kvm,找到纸面上的class,也即kvm_accel_type,然后调用type_initialize,里面调用kvm_accel_type的class_init方法,也即kvm_accel_class_init。
|
||||
|
||||
```
|
||||
static void kvm_accel_class_init(ObjectClass *oc, void *data)
|
||||
{
|
||||
AccelClass *ac = ACCEL_CLASS(oc);
|
||||
ac->name = "KVM";
|
||||
ac->init_machine = kvm_init;
|
||||
ac->allowed = &kvm_allowed;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在kvm_accel_class_init中,我们创建AccelClass,将init_machine设置为kvm_init。在accel_init_machine中其实就调用了这个init_machine函数,也即调用kvm_init方法。
|
||||
|
||||
```
|
||||
static int kvm_init(MachineState *ms)
|
||||
{
|
||||
MachineClass *mc = MACHINE_GET_CLASS(ms);
|
||||
int soft_vcpus_limit, hard_vcpus_limit;
|
||||
KVMState *s;
|
||||
const KVMCapabilityInfo *missing_cap;
|
||||
int ret;
|
||||
int type = 0;
|
||||
const char *kvm_type;
|
||||
|
||||
s = KVM_STATE(ms->accelerator);
|
||||
s->fd = qemu_open("/dev/kvm", O_RDWR);
|
||||
ret = kvm_ioctl(s, KVM_GET_API_VERSION, 0);
|
||||
......
|
||||
do {
|
||||
ret = kvm_ioctl(s, KVM_CREATE_VM, type);
|
||||
} while (ret == -EINTR);
|
||||
......
|
||||
s->vmfd = ret;
|
||||
|
||||
/* check the vcpu limits */
|
||||
soft_vcpus_limit = kvm_recommended_vcpus(s);
|
||||
hard_vcpus_limit = kvm_max_vcpus(s);
|
||||
......
|
||||
ret = kvm_arch_init(ms, s);
|
||||
if (ret < 0) {
|
||||
goto err;
|
||||
}
|
||||
|
||||
if (machine_kernel_irqchip_allowed(ms)) {
|
||||
kvm_irqchip_create(ms, s);
|
||||
}
|
||||
......
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
这里面的操作就从用户态到内核态的KVM了。就像前面原理讲过的一样,用户态使用内核态KVM的能力,需要打开一个文件/dev/kvm,这是一个字符设备文件,打开一个字符设备文件的过程我们讲过,这里不再赘述。
|
||||
|
||||
```
|
||||
static struct miscdevice kvm_dev = {
|
||||
KVM_MINOR,
|
||||
"kvm",
|
||||
&kvm_chardev_ops,
|
||||
};
|
||||
|
||||
static struct file_operations kvm_chardev_ops = {
|
||||
.unlocked_ioctl = kvm_dev_ioctl,
|
||||
.compat_ioctl = kvm_dev_ioctl,
|
||||
.llseek = noop_llseek,
|
||||
};
|
||||
|
||||
|
||||
```
|
||||
|
||||
KVM这个字符设备文件定义了一个字符设备文件的操作函数kvm_chardev_ops,这里面只定义了ioctl的操作。
|
||||
|
||||
接下来,用户态就通过ioctl系统调用,调用到kvm_dev_ioctl这个函数。这个过程我们在[字符设备](https://time.geekbang.org/column/article/100068)那一节也讲了。
|
||||
|
||||
```
|
||||
static long kvm_dev_ioctl(struct file *filp,
|
||||
unsigned int ioctl, unsigned long arg)
|
||||
{
|
||||
long r = -EINVAL;
|
||||
|
||||
switch (ioctl) {
|
||||
case KVM_GET_API_VERSION:
|
||||
r = KVM_API_VERSION;
|
||||
break;
|
||||
case KVM_CREATE_VM:
|
||||
r = kvm_dev_ioctl_create_vm(arg);
|
||||
break;
|
||||
case KVM_CHECK_EXTENSION:
|
||||
r = kvm_vm_ioctl_check_extension_generic(NULL, arg);
|
||||
break;
|
||||
case KVM_GET_VCPU_MMAP_SIZE:
|
||||
r = PAGE_SIZE; /* struct kvm_run */
|
||||
break;
|
||||
......
|
||||
}
|
||||
out:
|
||||
return r;
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
我们可以看到,在用户态qemu中,调用KVM_GET_API_VERSION查看版本号,内核就有相应的分支,返回版本号,如果能够匹配上,则调用KVM_CREATE_VM创建虚拟机。
|
||||
|
||||
创建虚拟机,需要调用kvm_dev_ioctl_create_vm。
|
||||
|
||||
```
|
||||
static int kvm_dev_ioctl_create_vm(unsigned long type)
|
||||
{
|
||||
int r;
|
||||
struct kvm *kvm;
|
||||
struct file *file;
|
||||
|
||||
kvm = kvm_create_vm(type);
|
||||
......
|
||||
r = get_unused_fd_flags(O_CLOEXEC);
|
||||
......
|
||||
file = anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR);
|
||||
......
|
||||
fd_install(r, file);
|
||||
return r;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在kvm_dev_ioctl_create_vm中,首先调用kvm_create_vm创建一个struct kvm结构。这个结构在内核里面代表一个虚拟机。
|
||||
|
||||
从下面结构的定义里,我们可以看到,这里面有vcpu,有mm_struct结构。这个结构本来用来管理进程的内存的。虚拟机也是一个进程,所以虚拟机的用户进程空间也是用它来表示。虚拟机里面的操作系统以及应用的进程空间不归它管。
|
||||
|
||||
在kvm_dev_ioctl_create_vm中,第二件事情就是创建一个文件描述符,和struct file关联起来,这个struct file的file_operations会被设置为kvm_vm_fops。
|
||||
|
||||
```
|
||||
struct kvm {
|
||||
struct mm_struct *mm; /* userspace tied to this vm */
|
||||
struct kvm_memslots __rcu *memslots[KVM_ADDRESS_SPACE_NUM];
|
||||
struct kvm_vcpu *vcpus[KVM_MAX_VCPUS];
|
||||
atomic_t online_vcpus;
|
||||
int created_vcpus;
|
||||
int last_boosted_vcpu;
|
||||
struct list_head vm_list;
|
||||
struct mutex lock;
|
||||
struct kvm_io_bus __rcu *buses[KVM_NR_BUSES];
|
||||
......
|
||||
struct kvm_vm_stat stat;
|
||||
struct kvm_arch arch;
|
||||
refcount_t users_count;
|
||||
......
|
||||
long tlbs_dirty;
|
||||
struct list_head devices;
|
||||
pid_t userspace_pid;
|
||||
};
|
||||
|
||||
static struct file_operations kvm_vm_fops = {
|
||||
.release = kvm_vm_release,
|
||||
.unlocked_ioctl = kvm_vm_ioctl,
|
||||
.llseek = noop_llseek,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
kvm_dev_ioctl_create_vm结束之后,对于一台虚拟机而言,只是在内核中有一个数据结构,对于相应的资源还没有分配,所以我们还需要接着看。
|
||||
|
||||
## 6.初始化网络设备
|
||||
|
||||
接下来,调用net_init_clients进行网络设备的初始化。我们可以解析net参数,也会在net_init_clients中解析netdev参数。这属于网络虚拟化的部分,我们先暂时放一下。
|
||||
|
||||
```
|
||||
int net_init_clients(Error **errp)
|
||||
{
|
||||
QTAILQ_INIT(&net_clients);
|
||||
if (qemu_opts_foreach(qemu_find_opts("netdev"),
|
||||
net_init_netdev, NULL, errp)) {
|
||||
return -1;
|
||||
}
|
||||
if (qemu_opts_foreach(qemu_find_opts("nic"), net_param_nic, NULL, errp)) {
|
||||
return -1;
|
||||
}
|
||||
if (qemu_opts_foreach(qemu_find_opts("net"), net_init_client, NULL, errp)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 7.CPU虚拟化
|
||||
|
||||
接下来,我们要调用machine_run_board_init。这里面调用了MachineClass的init函数。盼啊盼才到了它,这才调用了pc_init1。
|
||||
|
||||
```
|
||||
void machine_run_board_init(MachineState *machine)
|
||||
{
|
||||
MachineClass *machine_class = MACHINE_GET_CLASS(machine);
|
||||
numa_complete_configuration(machine);
|
||||
if (nb_numa_nodes) {
|
||||
machine_numa_finish_cpu_init(machine);
|
||||
}
|
||||
......
|
||||
machine_class->init(machine);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在pc_init1里面,我们重点关注两件重要的事情,一个的CPU的虚拟化,主要调用pc_cpus_init;另外就是内存的虚拟化,主要调用pc_memory_init。这一节我们重点关注CPU的虚拟化,下一节,我们来看内存的虚拟化。
|
||||
|
||||
```
|
||||
void pc_cpus_init(PCMachineState *pcms)
|
||||
{
|
||||
......
|
||||
for (i = 0; i < smp_cpus; i++) {
|
||||
pc_new_cpu(possible_cpus->cpus[i].type, possible_cpus->cpus[i].arch_id, &error_fatal);
|
||||
}
|
||||
}
|
||||
|
||||
static void pc_new_cpu(const char *typename, int64_t apic_id, Error **errp)
|
||||
{
|
||||
Object *cpu = NULL;
|
||||
cpu = object_new(typename);
|
||||
object_property_set_uint(cpu, apic_id, "apic-id", &local_err);
|
||||
object_property_set_bool(cpu, true, "realized", &local_err);//调用 object_property_add_bool的时候,设置了用 device_set_realized 来设置
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在pc_cpus_init中,对于每一个CPU,都调用pc_new_cpu,在这里,我们又看到了object_new,这又是一个从TypeImpl到Class类再到对象的一个过程。
|
||||
|
||||
这个时候,我们就要看CPU的类是怎么组织的了。
|
||||
|
||||
在上面的参数里面,CPU的配置是这样的:
|
||||
|
||||
```
|
||||
-cpu SandyBridge,+erms,+smep,+fsgsbase,+pdpe1gb,+rdrand,+f16c,+osxsave,+dca,+pcid,+pdcm,+xtpr,+tm2,+est,+smx,+vmx,+ds_cpl,+monitor,+dtes64,+pbe,+tm,+ht,+ss,+acpi,+ds,+vme
|
||||
|
||||
```
|
||||
|
||||
在这里我们知道,SandyBridge是CPU的一种类型。在hw/i386/pc.c中,我们能看到这种CPU的定义。
|
||||
|
||||
```
|
||||
{ "SandyBridge" "-" TYPE_X86_CPU, "min-xlevel", "0x8000000a" }
|
||||
|
||||
```
|
||||
|
||||
接下来,我们就来看"SandyBridge",也即TYPE_X86_CPU这种CPU的类,是一个什么样的结构。
|
||||
|
||||
```
|
||||
static const TypeInfo device_type_info = {
|
||||
.name = TYPE_DEVICE,
|
||||
.parent = TYPE_OBJECT,
|
||||
.instance_size = sizeof(DeviceState),
|
||||
.instance_init = device_initfn,
|
||||
.instance_post_init = device_post_init,
|
||||
.instance_finalize = device_finalize,
|
||||
.class_base_init = device_class_base_init,
|
||||
.class_init = device_class_init,
|
||||
.abstract = true,
|
||||
.class_size = sizeof(DeviceClass),
|
||||
};
|
||||
|
||||
static const TypeInfo cpu_type_info = {
|
||||
.name = TYPE_CPU,
|
||||
.parent = TYPE_DEVICE,
|
||||
.instance_size = sizeof(CPUState),
|
||||
.instance_init = cpu_common_initfn,
|
||||
.instance_finalize = cpu_common_finalize,
|
||||
.abstract = true,
|
||||
.class_size = sizeof(CPUClass),
|
||||
.class_init = cpu_class_init,
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
CPU这种类的定义是有多层继承关系的。TYPE_X86_CPU的父类是TYPE_CPU,TYPE_CPU的父类是TYPE_DEVICE,TYPE_DEVICE的父类是TYPE_OBJECT。到头了。
|
||||
|
||||
这里面每一层都有class_init,用于从TypeImpl生产xxxClass,也有instance_init将xxxClass初始化为实例。
|
||||
|
||||
在TYPE_X86_CPU这一层的class_init中,也即x86_cpu_common_class_init中,设置了DeviceClass的realize函数为x86_cpu_realizefn。这个函数很重要,马上就能用到。
|
||||
|
||||
```
|
||||
static void x86_cpu_common_class_init(ObjectClass *oc, void *data)
|
||||
{
|
||||
X86CPUClass *xcc = X86_CPU_CLASS(oc);
|
||||
CPUClass *cc = CPU_CLASS(oc);
|
||||
DeviceClass *dc = DEVICE_CLASS(oc);
|
||||
|
||||
device_class_set_parent_realize(dc, x86_cpu_realizefn,
|
||||
&xcc->parent_realize);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在TYPE_DEVICE这一层的instance_init函数device_initfn,会为这个设备添加一个属性"realized",要设置这个属性,需要用函数device_set_realized。
|
||||
|
||||
```
|
||||
static void device_initfn(Object *obj)
|
||||
{
|
||||
DeviceState *dev = DEVICE(obj);
|
||||
ObjectClass *class;
|
||||
Property *prop;
|
||||
dev->realized = false;
|
||||
object_property_add_bool(obj, "realized",
|
||||
device_get_realized, device_set_realized, NULL);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们回到pc_new_cpu函数,这里面就是通过object_property_set_bool设置这个属性为true,所以device_set_realized函数会被调用。
|
||||
|
||||
在device_set_realized中,DeviceClass的realize函数x86_cpu_realizefn会被调用。这里面qemu_init_vcpu会调用qemu_kvm_start_vcpu。
|
||||
|
||||
```
|
||||
static void qemu_kvm_start_vcpu(CPUState *cpu)
|
||||
{
|
||||
char thread_name[VCPU_THREAD_NAME_SIZE];
|
||||
cpu->thread = g_malloc0(sizeof(QemuThread));
|
||||
cpu->halt_cond = g_malloc0(sizeof(QemuCond));
|
||||
qemu_cond_init(cpu->halt_cond);
|
||||
qemu_thread_create(cpu->thread, thread_name, qemu_kvm_cpu_thread_fn, cpu, QEMU_THREAD_JOINABLE);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这里面,为这个vcpu创建一个线程,也即虚拟机里面的一个vcpu对应物理机上的一个线程,然后这个线程被调度到某个物理CPU上。
|
||||
|
||||
我们来看这个vcpu的线程执行函数。
|
||||
|
||||
```
|
||||
static void *qemu_kvm_cpu_thread_fn(void *arg)
|
||||
{
|
||||
CPUState *cpu = arg;
|
||||
int r;
|
||||
|
||||
rcu_register_thread();
|
||||
|
||||
qemu_mutex_lock_iothread();
|
||||
qemu_thread_get_self(cpu->thread);
|
||||
cpu->thread_id = qemu_get_thread_id();
|
||||
cpu->can_do_io = 1;
|
||||
current_cpu = cpu;
|
||||
|
||||
r = kvm_init_vcpu(cpu);
|
||||
kvm_init_cpu_signals(cpu);
|
||||
|
||||
/* signal CPU creation */
|
||||
cpu->created = true;
|
||||
qemu_cond_signal(&qemu_cpu_cond);
|
||||
|
||||
do {
|
||||
if (cpu_can_run(cpu)) {
|
||||
r = kvm_cpu_exec(cpu);
|
||||
}
|
||||
qemu_wait_io_event(cpu);
|
||||
} while (!cpu->unplug || cpu_can_run(cpu));
|
||||
|
||||
qemu_kvm_destroy_vcpu(cpu);
|
||||
cpu->created = false;
|
||||
qemu_cond_signal(&qemu_cpu_cond);
|
||||
qemu_mutex_unlock_iothread();
|
||||
rcu_unregister_thread();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在qemu_kvm_cpu_thread_fn中,先是kvm_init_vcpu初始化这个vcpu。
|
||||
|
||||
```
|
||||
int kvm_init_vcpu(CPUState *cpu)
|
||||
{
|
||||
KVMState *s = kvm_state;
|
||||
long mmap_size;
|
||||
int ret;
|
||||
......
|
||||
ret = kvm_get_vcpu(s, kvm_arch_vcpu_id(cpu));
|
||||
......
|
||||
cpu->kvm_fd = ret;
|
||||
cpu->kvm_state = s;
|
||||
cpu->vcpu_dirty = true;
|
||||
|
||||
mmap_size = kvm_ioctl(s, KVM_GET_VCPU_MMAP_SIZE, 0);
|
||||
......
|
||||
cpu->kvm_run = mmap(NULL, mmap_size, PROT_READ | PROT_WRITE, MAP_SHARED, cpu->kvm_fd, 0);
|
||||
......
|
||||
ret = kvm_arch_init_vcpu(cpu);
|
||||
err:
|
||||
return ret;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在kvm_get_vcpu中,我们会调用kvm_vm_ioctl(s, KVM_CREATE_VCPU, (void *)vcpu_id),在内核里面创建一个vcpu。在上面创建KVM_CREATE_VM的时候,我们已经创建了一个struct file,它的file_operations被设置为kvm_vm_fops,这个内核文件也是可以响应ioctl的。
|
||||
|
||||
如果我们切换到内核KVM,在kvm_vm_ioctl函数中,有对于KVM_CREATE_VCPU的处理,调用的是kvm_vm_ioctl_create_vcpu。
|
||||
|
||||
```
|
||||
static long kvm_vm_ioctl(struct file *filp,
|
||||
unsigned int ioctl, unsigned long arg)
|
||||
{
|
||||
struct kvm *kvm = filp->private_data;
|
||||
void __user *argp = (void __user *)arg;
|
||||
int r;
|
||||
switch (ioctl) {
|
||||
case KVM_CREATE_VCPU:
|
||||
r = kvm_vm_ioctl_create_vcpu(kvm, arg);
|
||||
break;
|
||||
case KVM_SET_USER_MEMORY_REGION: {
|
||||
struct kvm_userspace_memory_region kvm_userspace_mem;
|
||||
if (copy_from_user(&kvm_userspace_mem, argp,
|
||||
sizeof(kvm_userspace_mem)))
|
||||
goto out;
|
||||
r = kvm_vm_ioctl_set_memory_region(kvm, &kvm_userspace_mem);
|
||||
break;
|
||||
}
|
||||
......
|
||||
case KVM_CREATE_DEVICE: {
|
||||
struct kvm_create_device cd;
|
||||
if (copy_from_user(&cd, argp, sizeof(cd)))
|
||||
goto out;
|
||||
r = kvm_ioctl_create_device(kvm, &cd);
|
||||
if (copy_to_user(argp, &cd, sizeof(cd)))
|
||||
goto out;
|
||||
break;
|
||||
}
|
||||
case KVM_CHECK_EXTENSION:
|
||||
r = kvm_vm_ioctl_check_extension_generic(kvm, arg);
|
||||
break;
|
||||
default:
|
||||
r = kvm_arch_vm_ioctl(filp, ioctl, arg);
|
||||
}
|
||||
out:
|
||||
return r;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在kvm_vm_ioctl_create_vcpu中,kvm_arch_vcpu_create调用kvm_x86_ops的vcpu_create函数来创建CPU。
|
||||
|
||||
```
|
||||
static int kvm_vm_ioctl_create_vcpu(struct kvm *kvm, u32 id)
|
||||
{
|
||||
int r;
|
||||
struct kvm_vcpu *vcpu;
|
||||
kvm->created_vcpus++;
|
||||
......
|
||||
vcpu = kvm_arch_vcpu_create(kvm, id);
|
||||
preempt_notifier_init(&vcpu->preempt_notifier, &kvm_preempt_ops);
|
||||
r = kvm_arch_vcpu_setup(vcpu);
|
||||
......
|
||||
/* Now it's all set up, let userspace reach it */
|
||||
kvm_get_kvm(kvm);
|
||||
r = create_vcpu_fd(vcpu);
|
||||
kvm->vcpus[atomic_read(&kvm->online_vcpus)] = vcpu;
|
||||
......
|
||||
}
|
||||
|
||||
struct kvm_vcpu *kvm_arch_vcpu_create(struct kvm *kvm,
|
||||
unsigned int id)
|
||||
{
|
||||
struct kvm_vcpu *vcpu;
|
||||
vcpu = kvm_x86_ops->vcpu_create(kvm, id);
|
||||
return vcpu;
|
||||
}
|
||||
|
||||
static int create_vcpu_fd(struct kvm_vcpu *vcpu)
|
||||
{
|
||||
return anon_inode_getfd("kvm-vcpu", &kvm_vcpu_fops, vcpu, O_RDWR | O_CLOEXEC);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
然后,create_vcpu_fd又创建了一个struct file,它的file_operations指向kvm_vcpu_fops。从这里可以看出,KVM的内核模块是一个文件,可以通过ioctl进行操作。基于这个内核模块创建的VM也是一个文件,也可以通过ioctl进行操作。在这个VM上创建的vcpu同样是一个文件,同样可以通过ioctl进行操作。
|
||||
|
||||
我们回过头来看,kvm_x86_ops的vcpu_create函数。kvm_x86_ops对于不同的硬件加速虚拟化指向不同的结构,如果是vmx,则指向vmx_x86_ops;如果是svm,则指向svm_x86_ops。我们这里看vmx_x86_ops。这个结构很长,里面有非常多的操作,我们用一个看一个。
|
||||
|
||||
```
|
||||
static struct kvm_x86_ops vmx_x86_ops __ro_after_init = {
|
||||
......
|
||||
.vcpu_create = vmx_create_vcpu,
|
||||
......
|
||||
}
|
||||
|
||||
static struct kvm_vcpu *vmx_create_vcpu(struct kvm *kvm, unsigned int id)
|
||||
{
|
||||
int err;
|
||||
struct vcpu_vmx *vmx = kmem_cache_zalloc(kvm_vcpu_cache, GFP_KERNEL);
|
||||
int cpu;
|
||||
vmx->vpid = allocate_vpid();
|
||||
err = kvm_vcpu_init(&vmx->vcpu, kvm, id);
|
||||
vmx->guest_msrs = kmalloc(PAGE_SIZE, GFP_KERNEL);
|
||||
vmx->loaded_vmcs = &vmx->vmcs01;
|
||||
vmx->loaded_vmcs->vmcs = alloc_vmcs();
|
||||
vmx->loaded_vmcs->shadow_vmcs = NULL;
|
||||
loaded_vmcs_init(vmx->loaded_vmcs);
|
||||
|
||||
cpu = get_cpu();
|
||||
vmx_vcpu_load(&vmx->vcpu, cpu);
|
||||
vmx->vcpu.cpu = cpu;
|
||||
err = vmx_vcpu_setup(vmx);
|
||||
vmx_vcpu_put(&vmx->vcpu);
|
||||
put_cpu();
|
||||
|
||||
if (enable_ept) {
|
||||
if (!kvm->arch.ept_identity_map_addr)
|
||||
kvm->arch.ept_identity_map_addr =
|
||||
VMX_EPT_IDENTITY_PAGETABLE_ADDR;
|
||||
err = init_rmode_identity_map(kvm);
|
||||
}
|
||||
|
||||
return &vmx->vcpu;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
vmx_create_vcpu创建用于表示vcpu的结构struct vcpu_vmx,并填写里面的内容。例如guest_msrs,咱们在讲系统调用的时候提过msr寄存器,虚拟机也需要有这样的寄存器。
|
||||
|
||||
enable_ept是和内存虚拟化相关的,EPT全称Extended Page Table,顾名思义,是优化内存虚拟化的,这个功能我们放到内存的那一节讲。
|
||||
|
||||
最最重要的就是loaded_vmcs了。VMCS是什么呢?它的全称是Virtual Machine Control Structure。它是来干什么呢?
|
||||
|
||||
前面咱们讲进程调度的时候讲过,为了支持进程在CPU上的切换,CPU硬件要求有一个TSS结构,用于保存进程运行时的所有寄存器的状态,进程切换的时候,需要根据TSS恢复寄存器。
|
||||
|
||||
虚拟机也是一个进程,也需要切换,而且切换更加的复杂,可能是两个虚拟机之间切换,也可能是虚拟机切换给内核,虚拟机因为里面还有另一个操作系统,要保存的信息比普通的进程多得多。那就需要有一个结构来保存虚拟机运行的上下文,VMCS就是是Intel实现CPU虚拟化,记录vCPU状态的一个关键数据结构。
|
||||
|
||||
VMCS数据结构主要包含以下信息。
|
||||
|
||||
- Guest-state area,即vCPU的状态信息,包括vCPU的基本运行环境,例如寄存器等。
|
||||
- Host-state area,是物理CPU的状态信息。物理CPU和vCPU之间也会来回切换,所以,VMCS中既要记录vCPU的状态,也要记录物理CPU的状态。
|
||||
- VM-execution control fields,对vCPU的运行行为进行控制。例如,发生中断怎么办,是否使用EPT(Extended Page Table)功能等。
|
||||
|
||||
接下来,对于VMCS,有两个重要的操作。
|
||||
|
||||
VM-Entry,我们称为从根模式切换到非根模式,也即切换到guest上,这个时候CPU上运行的是虚拟机。VM-Exit我们称为CPU从非根模式切换到根模式,也即从guest切换到宿主机。例如,当要执行一些虚拟机没有权限的敏感指令时。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1e/dc/1ec7600be619221dfac03e6ade67f7dc.png" alt="">
|
||||
|
||||
为了维护这两个动作,VMCS里面还有几项内容:
|
||||
|
||||
- VM-exit control fields,对VM Exit的行为进行控制。比如,VM Exit的时候对vCPU来说需要保存哪些MSR寄存器,对于主机CPU来说需要恢复哪些MSR寄存器。
|
||||
- VM-entry control fields,对VM Entry的行为进行控制。比如,需要保存和恢复哪些MSR寄存器等。
|
||||
- VM-exit information fields,记录下发生VM Exit发生的原因及一些必要的信息,方便对VM Exit事件进行处理。
|
||||
|
||||
至此,内核准备完毕。
|
||||
|
||||
我们再回到qemu的kvm_init_vcpu函数,这里面除了创建内核中的vcpu结构之外,还通过mmap将内核的vcpu结构,映射到qemu中CPUState的kvm_run中,为什么能用mmap呢,上面咱们不是说过了吗,vcpu也是一个文件。
|
||||
|
||||
我们再回到这个vcpu的线程函数qemu_kvm_cpu_thread_fn,他在执行kvm_init_vcpu创建vcpu之后,接下来是一个do-while循环,也即一直运行,并且通过调用kvm_cpu_exec,运行这个虚拟机。
|
||||
|
||||
```
|
||||
int kvm_cpu_exec(CPUState *cpu)
|
||||
{
|
||||
struct kvm_run *run = cpu->kvm_run;
|
||||
int ret, run_ret;
|
||||
......
|
||||
do {
|
||||
......
|
||||
run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);
|
||||
......
|
||||
switch (run->exit_reason) {
|
||||
case KVM_EXIT_IO:
|
||||
kvm_handle_io(run->io.port, attrs,
|
||||
(uint8_t *)run + run->io.data_offset,
|
||||
run->io.direction,
|
||||
run->io.size,
|
||||
run->io.count);
|
||||
break;
|
||||
case KVM_EXIT_IRQ_WINDOW_OPEN:
|
||||
ret = EXCP_INTERRUPT;
|
||||
break;
|
||||
case KVM_EXIT_SHUTDOWN:
|
||||
qemu_system_reset_request(SHUTDOWN_CAUSE_GUEST_RESET);
|
||||
ret = EXCP_INTERRUPT;
|
||||
break;
|
||||
case KVM_EXIT_UNKNOWN:
|
||||
fprintf(stderr, "KVM: unknown exit, hardware reason %" PRIx64 "\n",(uint64_t)run->hw.hardware_exit_reason);
|
||||
ret = -1;
|
||||
break;
|
||||
case KVM_EXIT_INTERNAL_ERROR:
|
||||
ret = kvm_handle_internal_error(cpu, run);
|
||||
break;
|
||||
......
|
||||
}
|
||||
} while (ret == 0);
|
||||
......
|
||||
return ret;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在kvm_cpu_exec中,我们能看到一个循环,在循环中,kvm_vcpu_ioctl(KVM_RUN)运行这个虚拟机,这个时候CPU进入VM-Entry,也即进入客户机模式。
|
||||
|
||||
如果一直是客户机的操作系统占用这个CPU,则会一直停留在这一行运行,一旦这个调用返回了,就说明CPU进入VM-Exit退出客户机模式,将CPU交还给宿主机。在循环中,我们会对退出的原因exit_reason进行分析处理,因为有了I/O,还有了中断等,做相应的处理。处理完毕之后,再次循环,再次通过VM-Entry,进入客户机模式。如此循环,直到虚拟机正常或者异常退出。
|
||||
|
||||
我们来看kvm_vcpu_ioctl(KVM_RUN)在内核做了哪些事情。
|
||||
|
||||
上面我们也讲了,vcpu在内核也是一个文件,也是通过ioctl进行用户态和内核态通信的,在内核中,调用的是kvm_vcpu_ioctl。
|
||||
|
||||
```
|
||||
static long kvm_vcpu_ioctl(struct file *filp,
|
||||
unsigned int ioctl, unsigned long arg)
|
||||
{
|
||||
struct kvm_vcpu *vcpu = filp->private_data;
|
||||
void __user *argp = (void __user *)arg;
|
||||
int r;
|
||||
struct kvm_fpu *fpu = NULL;
|
||||
struct kvm_sregs *kvm_sregs = NULL;
|
||||
......
|
||||
r = vcpu_load(vcpu);
|
||||
switch (ioctl) {
|
||||
case KVM_RUN: {
|
||||
struct pid *oldpid;
|
||||
r = kvm_arch_vcpu_ioctl_run(vcpu, vcpu->run);
|
||||
break;
|
||||
}
|
||||
case KVM_GET_REGS: {
|
||||
struct kvm_regs *kvm_regs;
|
||||
kvm_regs = kzalloc(sizeof(struct kvm_regs), GFP_KERNEL);
|
||||
r = kvm_arch_vcpu_ioctl_get_regs(vcpu, kvm_regs);
|
||||
if (copy_to_user(argp, kvm_regs, sizeof(struct kvm_regs)))
|
||||
goto out_free1;
|
||||
break;
|
||||
}
|
||||
case KVM_SET_REGS: {
|
||||
struct kvm_regs *kvm_regs;
|
||||
kvm_regs = memdup_user(argp, sizeof(*kvm_regs));
|
||||
r = kvm_arch_vcpu_ioctl_set_regs(vcpu, kvm_regs);
|
||||
break;
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
kvm_arch_vcpu_ioctl_run会调用vcpu_run,这里面也是一个无限循环。
|
||||
|
||||
```
|
||||
static int vcpu_run(struct kvm_vcpu *vcpu)
|
||||
{
|
||||
int r;
|
||||
struct kvm *kvm = vcpu->kvm;
|
||||
|
||||
for (;;) {
|
||||
if (kvm_vcpu_running(vcpu)) {
|
||||
r = vcpu_enter_guest(vcpu);
|
||||
} else {
|
||||
r = vcpu_block(kvm, vcpu);
|
||||
}
|
||||
....
|
||||
if (signal_pending(current)) {
|
||||
r = -EINTR;
|
||||
vcpu->run->exit_reason = KVM_EXIT_INTR;
|
||||
++vcpu->stat.signal_exits;
|
||||
break;
|
||||
}
|
||||
if (need_resched()) {
|
||||
cond_resched();
|
||||
}
|
||||
}
|
||||
......
|
||||
return r;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这个循环中,除了调用vcpu_enter_guest进入客户机模式运行之外,还有对于信号的响应signal_pending,也即一台虚拟机是可以被kill掉的,还有对于调度的响应,这台虚拟机可以被从当前的物理CPU上赶下来,换成别的虚拟机或者其他进程。
|
||||
|
||||
我们这里重点看vcpu_enter_guest。
|
||||
|
||||
```
|
||||
static int vcpu_enter_guest(struct kvm_vcpu *vcpu)
|
||||
{
|
||||
r = kvm_mmu_reload(vcpu);
|
||||
vcpu->mode = IN_GUEST_MODE;
|
||||
kvm_load_guest_xcr0(vcpu);
|
||||
......
|
||||
guest_enter_irqoff();
|
||||
kvm_x86_ops->run(vcpu);
|
||||
vcpu->mode = OUTSIDE_GUEST_MODE;
|
||||
......
|
||||
kvm_put_guest_xcr0(vcpu);
|
||||
kvm_x86_ops->handle_external_intr(vcpu);
|
||||
++vcpu->stat.exits;
|
||||
guest_exit_irqoff();
|
||||
r = kvm_x86_ops->handle_exit(vcpu);
|
||||
return r;
|
||||
......
|
||||
}
|
||||
|
||||
static struct kvm_x86_ops vmx_x86_ops __ro_after_init = {
|
||||
......
|
||||
.run = vmx_vcpu_run,
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在vcpu_enter_guest中,我们会调用vmx_x86_ops 的vmx_vcpu_run函数,进入客户机模式。
|
||||
|
||||
```
|
||||
static void __noclone vmx_vcpu_run(struct kvm_vcpu *vcpu)
|
||||
{
|
||||
struct vcpu_vmx *vmx = to_vmx(vcpu);
|
||||
unsigned long debugctlmsr, cr3, cr4;
|
||||
......
|
||||
cr3 = __get_current_cr3_fast();
|
||||
......
|
||||
cr4 = cr4_read_shadow();
|
||||
......
|
||||
vmx->__launched = vmx->loaded_vmcs->launched;
|
||||
asm(
|
||||
/* Store host registers */
|
||||
"push %%" _ASM_DX "; push %%" _ASM_BP ";"
|
||||
"push %%" _ASM_CX " \n\t" /* placeholder for guest rcx */
|
||||
"push %%" _ASM_CX " \n\t"
|
||||
......
|
||||
/* Load guest registers. Don't clobber flags. */
|
||||
"mov %c[rax](%0), %%" _ASM_AX " \n\t"
|
||||
"mov %c[rbx](%0), %%" _ASM_BX " \n\t"
|
||||
"mov %c[rdx](%0), %%" _ASM_DX " \n\t"
|
||||
"mov %c[rsi](%0), %%" _ASM_SI " \n\t"
|
||||
"mov %c[rdi](%0), %%" _ASM_DI " \n\t"
|
||||
"mov %c[rbp](%0), %%" _ASM_BP " \n\t"
|
||||
#ifdef CONFIG_X86_64
|
||||
"mov %c[r8](%0), %%r8 \n\t"
|
||||
"mov %c[r9](%0), %%r9 \n\t"
|
||||
"mov %c[r10](%0), %%r10 \n\t"
|
||||
"mov %c[r11](%0), %%r11 \n\t"
|
||||
"mov %c[r12](%0), %%r12 \n\t"
|
||||
"mov %c[r13](%0), %%r13 \n\t"
|
||||
"mov %c[r14](%0), %%r14 \n\t"
|
||||
"mov %c[r15](%0), %%r15 \n\t"
|
||||
#endif
|
||||
"mov %c[rcx](%0), %%" _ASM_CX " \n\t" /* kills %0 (ecx) */
|
||||
|
||||
/* Enter guest mode */
|
||||
"jne 1f \n\t"
|
||||
__ex(ASM_VMX_VMLAUNCH) "\n\t"
|
||||
"jmp 2f \n\t"
|
||||
"1: " __ex(ASM_VMX_VMRESUME) "\n\t"
|
||||
"2: "
|
||||
/* Save guest registers, load host registers, keep flags */
|
||||
"mov %0, %c[wordsize](%%" _ASM_SP ") \n\t"
|
||||
"pop %0 \n\t"
|
||||
"mov %%" _ASM_AX ", %c[rax](%0) \n\t"
|
||||
"mov %%" _ASM_BX ", %c[rbx](%0) \n\t"
|
||||
__ASM_SIZE(pop) " %c[rcx](%0) \n\t"
|
||||
"mov %%" _ASM_DX ", %c[rdx](%0) \n\t"
|
||||
"mov %%" _ASM_SI ", %c[rsi](%0) \n\t"
|
||||
"mov %%" _ASM_DI ", %c[rdi](%0) \n\t"
|
||||
"mov %%" _ASM_BP ", %c[rbp](%0) \n\t"
|
||||
#ifdef CONFIG_X86_64
|
||||
"mov %%r8, %c[r8](%0) \n\t"
|
||||
"mov %%r9, %c[r9](%0) \n\t"
|
||||
"mov %%r10, %c[r10](%0) \n\t"
|
||||
"mov %%r11, %c[r11](%0) \n\t"
|
||||
"mov %%r12, %c[r12](%0) \n\t"
|
||||
"mov %%r13, %c[r13](%0) \n\t"
|
||||
"mov %%r14, %c[r14](%0) \n\t"
|
||||
"mov %%r15, %c[r15](%0) \n\t"
|
||||
#endif
|
||||
"mov %%cr2, %%" _ASM_AX " \n\t"
|
||||
"mov %%" _ASM_AX ", %c[cr2](%0) \n\t"
|
||||
|
||||
"pop %%" _ASM_BP "; pop %%" _ASM_DX " \n\t"
|
||||
"setbe %c[fail](%0) \n\t"
|
||||
".pushsection .rodata \n\t"
|
||||
".global vmx_return \n\t"
|
||||
"vmx_return: " _ASM_PTR " 2b \n\t"
|
||||
......
|
||||
);
|
||||
......
|
||||
vmx->loaded_vmcs->launched = 1;
|
||||
vmx->exit_reason = vmcs_read32(VM_EXIT_REASON);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在vmx_vcpu_run中,出现了汇编语言的代码,比较难看懂,但是没有关系呀,里面有注释呀,我们可以沿着注释来看。
|
||||
|
||||
- 首先是Store host registers,要从宿主机模式变为客户机模式了,所以原来宿主机运行时候的寄存器要保存下来。
|
||||
- 接下来是Load guest registers,将原来客户机运行的时候的寄存器加载进来。
|
||||
- 接下来是Enter guest mode,调用ASM_VMX_VMLAUNCH进入客户机模型运行,或者ASM_VMX_VMRESUME恢复客户机模型运行。
|
||||
- 如果客户机因为某种原因退出,Save guest registers, load host registers,也即保存客户机运行的时候的寄存器,就加载宿主机运行的时候的寄存器。
|
||||
- 最后将exit_reason保存在vmx结构中。
|
||||
|
||||
至此,CPU虚拟化就解析完了。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
CPU的虚拟化过程还是很复杂的,我画了一张图总结了一下。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/c4/67/c43639f7024848aa3e828bcfc10ca467.png" alt="">
|
||||
|
||||
- 首先,我们要定义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进行处理。
|
||||
|
||||
## 课堂练习
|
||||
|
||||
在咱们上面操作KVM的过程中,出现了好几次文件系统。不愧是“Linux中一切皆文件”。那你能否整理一下这些文件系统之间的关系呢?
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解,也欢迎收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
|
||||
843
极客时间专栏/趣谈Linux操作系统/核心原理篇:第九部分 虚拟化/52 | 计算虚拟化之内存:如何建立独立的办公室?.md
Normal file
843
极客时间专栏/趣谈Linux操作系统/核心原理篇:第九部分 虚拟化/52 | 计算虚拟化之内存:如何建立独立的办公室?.md
Normal file
@@ -0,0 +1,843 @@
|
||||
<audio id="audio" title="52 | 计算虚拟化之内存:如何建立独立的办公室?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/e0/29/e09a498824c7c5dc63a27ed518a2f929.mp3"></audio>
|
||||
|
||||
上一节,我们解析了计算虚拟化之CPU。可以看到,CPU的虚拟化是用户态的qemu和内核态的KVM共同配合完成的。它们二者通过ioctl进行通信。对于内存管理来讲,也是需要这两者配合完成的。
|
||||
|
||||
咱们在内存管理的时候讲过,操作系统给每个进程分配的内存都是虚拟内存,需要通过页表映射,变成物理内存进行访问。当有了虚拟机之后,情况会变得更加复杂。因为虚拟机对于物理机来讲是一个进程,但是虚拟机里面也有内核,也有虚拟机里面跑的进程。所以有了虚拟机,内存就变成了四类:
|
||||
|
||||
- **虚拟机里面的虚拟内存**(Guest OS Virtual Memory,GVA),这是虚拟机里面的进程看到的内存空间;
|
||||
- **虚拟机里面的物理内存**(Guest OS Physical Memory,GPA),这是虚拟机里面的操作系统看到的内存,它认为这是物理内存;
|
||||
- **物理机的虚拟内存**(Host Virtual Memory,HVA),这是物理机上的qemu进程看到的内存空间;
|
||||
- **物理机的物理内存**(Host Physical Memory,HPA),这是物理机上的操作系统看到的内存。
|
||||
|
||||
咱们内存管理那一章讲的两大内容,一个是内存管理,它变得非常复杂;另一个是内存映射,具体来说就是,从GVA到GPA,到HVA,再到HPA,这样几经转手,计算机的性能就会变得很差。当然,虚拟化技术成熟的今天,有了一些优化的手段,具体怎么优化呢?我们这一节就来一一解析。
|
||||
|
||||
## 内存管理
|
||||
|
||||
我们先来看内存管理的部分。
|
||||
|
||||
由于CPU和内存是紧密结合的,因而内存虚拟化的初始化过程,和CPU虚拟化的初始化是一起完成的。
|
||||
|
||||
上一节说CPU虚拟化初始化的时候,我们会调用kvm_init函数,这里面打开了"/dev/kvm"这个字符文件,并且通过ioctl调用到内核kvm的KVM_CREATE_VM操作,除了这些CPU相关的调用,接下来还有内存相关的。我们来看看。
|
||||
|
||||
```
|
||||
static int kvm_init(MachineState *ms)
|
||||
{
|
||||
MachineClass *mc = MACHINE_GET_CLASS(ms);
|
||||
......
|
||||
kvm_memory_listener_register(s, &s->memory_listener,
|
||||
&address_space_memory, 0);
|
||||
memory_listener_register(&kvm_io_listener,
|
||||
&address_space_io);
|
||||
......
|
||||
}
|
||||
|
||||
AddressSpace address_space_io;
|
||||
AddressSpace address_space_memory;
|
||||
|
||||
```
|
||||
|
||||
这里面有两个地址空间AddressSpace,一个是系统内存的地址空间address_space_memory,一个用于I/O的地址空间address_space_io。这里我们重点看address_space_memory。
|
||||
|
||||
```
|
||||
struct AddressSpace {
|
||||
/* All fields are private. */
|
||||
struct rcu_head rcu;
|
||||
char *name;
|
||||
MemoryRegion *root;
|
||||
|
||||
/* Accessed via RCU. */
|
||||
struct FlatView *current_map;
|
||||
|
||||
int ioeventfd_nb;
|
||||
struct MemoryRegionIoeventfd *ioeventfds;
|
||||
QTAILQ_HEAD(, MemoryListener) listeners;
|
||||
QTAILQ_ENTRY(AddressSpace) address_spaces_link;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
对于一个地址空间,会有多个内存区域MemoryRegion组成树形结构。这里面,root是这棵树的根。另外,还有一个MemoryListener链表,当内存区域发生变化的时候,需要做一些动作,使得用户态和内核态能够协同,就是由这些MemoryListener完成的。
|
||||
|
||||
在kvm_init这个时候,还没有内存区域加入进来,root还是空的,但是我们可以先注册MemoryListener,这里注册的是KVMMemoryListener。
|
||||
|
||||
```
|
||||
void kvm_memory_listener_register(KVMState *s, KVMMemoryListener *kml,
|
||||
AddressSpace *as, int as_id)
|
||||
{
|
||||
int i;
|
||||
|
||||
kml->slots = g_malloc0(s->nr_slots * sizeof(KVMSlot));
|
||||
kml->as_id = as_id;
|
||||
|
||||
for (i = 0; i < s->nr_slots; i++) {
|
||||
kml->slots[i].slot = i;
|
||||
}
|
||||
|
||||
kml->listener.region_add = kvm_region_add;
|
||||
kml->listener.region_del = kvm_region_del;
|
||||
kml->listener.priority = 10;
|
||||
|
||||
memory_listener_register(&kml->listener, as);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这个KVMMemoryListener中是这样配置的:当添加一个MemoryRegion的时候,region_add会被调用,这个我们后面会用到。
|
||||
|
||||
接下来,在qemu启动的main函数中,我们会调用cpu_exec_init_all->memory_map_init.
|
||||
|
||||
```
|
||||
static void memory_map_init(void)
|
||||
{
|
||||
system_memory = g_malloc(sizeof(*system_memory));
|
||||
|
||||
memory_region_init(system_memory, NULL, "system", UINT64_MAX);
|
||||
address_space_init(&address_space_memory, system_memory, "memory");
|
||||
|
||||
system_io = g_malloc(sizeof(*system_io));
|
||||
memory_region_init_io(system_io, NULL, &unassigned_io_ops, NULL, "io",
|
||||
65536);
|
||||
address_space_init(&address_space_io, system_io, "I/O");
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在这里,对于系统内存区域system_memory和用于I/O的内存区域system_io,我们都进行了初始化,并且关联到了相应的地址空间AddressSpace。
|
||||
|
||||
```
|
||||
void address_space_init(AddressSpace *as, MemoryRegion *root, const char *name)
|
||||
{
|
||||
memory_region_ref(root);
|
||||
as->root = root;
|
||||
as->current_map = NULL;
|
||||
as->ioeventfd_nb = 0;
|
||||
as->ioeventfds = NULL;
|
||||
QTAILQ_INIT(&as->listeners);
|
||||
QTAILQ_INSERT_TAIL(&address_spaces, as, address_spaces_link);
|
||||
as->name = g_strdup(name ? name : "anonymous");
|
||||
address_space_update_topology(as);
|
||||
address_space_update_ioeventfds(as);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
对于系统内存地址空间address_space_memory,我们需要把它里面内存区域的根root设置为system_memory。
|
||||
|
||||
另外,在这里,我们还调用了address_space_update_topology。
|
||||
|
||||
```
|
||||
static void address_space_update_topology(AddressSpace *as)
|
||||
{
|
||||
MemoryRegion *physmr = memory_region_get_flatview_root(as->root);
|
||||
|
||||
flatviews_init();
|
||||
if (!g_hash_table_lookup(flat_views, physmr)) {
|
||||
generate_memory_topology(physmr);
|
||||
}
|
||||
address_space_set_flatview(as);
|
||||
}
|
||||
|
||||
static void address_space_set_flatview(AddressSpace *as)
|
||||
{
|
||||
FlatView *old_view = address_space_to_flatview(as);
|
||||
MemoryRegion *physmr = memory_region_get_flatview_root(as->root);
|
||||
FlatView *new_view = g_hash_table_lookup(flat_views, physmr);
|
||||
|
||||
if (old_view == new_view) {
|
||||
return;
|
||||
}
|
||||
......
|
||||
if (!QTAILQ_EMPTY(&as->listeners)) {
|
||||
FlatView tmpview = { .nr = 0 }, *old_view2 = old_view;
|
||||
|
||||
if (!old_view2) {
|
||||
old_view2 = &tmpview;
|
||||
}
|
||||
address_space_update_topology_pass(as, old_view2, new_view, false);
|
||||
address_space_update_topology_pass(as, old_view2, new_view, true);
|
||||
}
|
||||
|
||||
/* Writes are protected by the BQL. */
|
||||
atomic_rcu_set(&as->current_map, new_view);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里面会生成AddressSpace的flatview。flatview是什么意思呢?
|
||||
|
||||
我们可以看到,在AddressSpace里面,除了树形结构的MemoryRegion之外,还有一个flatview结构,其实这个结构就是把这样一个树形的内存结构变成平的内存结构。因为树形内存结构比较容易管理,但是平的内存结构,比较方便和内核里面通信,来请求物理内存。虽然操作系统内核里面也是用树形结构来表示内存区域的,但是用户态向内核申请内存的时候,会按照平的、连续的模式进行申请。这里,qemu在用户态,所以要做这样一个转换。
|
||||
|
||||
在address_space_set_flatview中,我们将老的flatview和新的flatview进行比较。如果不同,说明内存结构发生了变化,会调用address_space_update_topology_pass->MEMORY_LISTENER_UPDATE_REGION->MEMORY_LISTENER_CALL。
|
||||
|
||||
这里面调用所有的listener。但是,这个逻辑这里不会执行的。这是因为这里内存处于初始化的阶段,全局的flat_views里面肯定找不到。因而generate_memory_topology第一次生成了FlatView,然后才调用了address_space_set_flatview。这里面,老的flatview和新的flatview一定是一样的。
|
||||
|
||||
但是,请你记住这个逻辑,到这里我们还没解析qemu有关内存的参数,所以这里添加的MemoryRegion虽然是一个根,但是是空的,是为了管理使用的,后面真的添加内存的时候,这个逻辑还会调用到。
|
||||
|
||||
我们再回到qemu启动的main函数中。接下来的初始化过程会调用pc_init1。在这里面,对于CPU虚拟化,我们会调用pc_cpus_init。这个我们在上一节已经讲过了。另外,pc_init1还会调用pc_memory_init,进行内存的虚拟化,我们这里解析这一部分。
|
||||
|
||||
```
|
||||
void pc_memory_init(PCMachineState *pcms,
|
||||
MemoryRegion *system_memory,
|
||||
MemoryRegion *rom_memory,
|
||||
MemoryRegion **ram_memory)
|
||||
{
|
||||
int linux_boot, i;
|
||||
MemoryRegion *ram, *option_rom_mr;
|
||||
MemoryRegion *ram_below_4g, *ram_above_4g;
|
||||
FWCfgState *fw_cfg;
|
||||
MachineState *machine = MACHINE(pcms);
|
||||
PCMachineClass *pcmc = PC_MACHINE_GET_CLASS(pcms);
|
||||
......
|
||||
/* Allocate RAM. We allocate it as a single memory region and use
|
||||
* aliases to address portions of it, mostly for backwards compatibility with older qemus that used qemu_ram_alloc().
|
||||
*/
|
||||
ram = g_malloc(sizeof(*ram));
|
||||
memory_region_allocate_system_memory(ram, NULL, "pc.ram",
|
||||
machine->ram_size);
|
||||
*ram_memory = ram;
|
||||
ram_below_4g = g_malloc(sizeof(*ram_below_4g));
|
||||
memory_region_init_alias(ram_below_4g, NULL, "ram-below-4g", ram,
|
||||
0, pcms->below_4g_mem_size);
|
||||
memory_region_add_subregion(system_memory, 0, ram_below_4g);
|
||||
e820_add_entry(0, pcms->below_4g_mem_size, E820_RAM);
|
||||
if (pcms->above_4g_mem_size > 0) {
|
||||
ram_above_4g = g_malloc(sizeof(*ram_above_4g));
|
||||
memory_region_init_alias(ram_above_4g, NULL, "ram-above-4g", ram, pcms->below_4g_mem_size, pcms->above_4g_mem_size);
|
||||
memory_region_add_subregion(system_memory, 0x100000000ULL,
|
||||
ram_above_4g);
|
||||
e820_add_entry(0x100000000ULL, pcms->above_4g_mem_size, E820_RAM);
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在pc_memory_init中,我们已经知道了虚拟机要申请的内存ram_size,于是通过memory_region_allocate_system_memory来申请内存。
|
||||
|
||||
接下来的调用链为:memory_region_allocate_system_memory->allocate_system_memory_nonnuma->memory_region_init_ram_nomigrate->memory_region_init_ram_shared_nomigrate。
|
||||
|
||||
```
|
||||
void memory_region_init_ram_shared_nomigrate(MemoryRegion *mr,
|
||||
Object *owner,
|
||||
const char *name,
|
||||
uint64_t size,
|
||||
bool share,
|
||||
Error **errp)
|
||||
{
|
||||
Error *err = NULL;
|
||||
memory_region_init(mr, owner, name, size);
|
||||
mr->ram = true;
|
||||
mr->terminates = true;
|
||||
mr->destructor = memory_region_destructor_ram;
|
||||
mr->ram_block = qemu_ram_alloc(size, share, mr, &err);
|
||||
......
|
||||
}
|
||||
|
||||
static
|
||||
RAMBlock *qemu_ram_alloc_internal(ram_addr_t size, ram_addr_t max_size, void (*resized)(const char*,uint64_t length,void *host),void *host, bool resizeable, bool share,MemoryRegion *mr, Error **errp)
|
||||
{
|
||||
RAMBlock *new_block;
|
||||
size = HOST_PAGE_ALIGN(size);
|
||||
max_size = HOST_PAGE_ALIGN(max_size);
|
||||
new_block = g_malloc0(sizeof(*new_block));
|
||||
new_block->mr = mr;
|
||||
new_block->resized = resized;
|
||||
new_block->used_length = size;
|
||||
new_block->max_length = max_size;
|
||||
new_block->fd = -1;
|
||||
new_block->page_size = getpagesize();
|
||||
new_block->host = host;
|
||||
......
|
||||
ram_block_add(new_block, &local_err, share);
|
||||
return new_block;
|
||||
}
|
||||
|
||||
static void ram_block_add(RAMBlock *new_block, Error **errp, bool shared)
|
||||
{
|
||||
RAMBlock *block;
|
||||
RAMBlock *last_block = NULL;
|
||||
ram_addr_t old_ram_size, new_ram_size;
|
||||
Error *err = NULL;
|
||||
old_ram_size = last_ram_page();
|
||||
new_block->offset = find_ram_offset(new_block->max_length);
|
||||
if (!new_block->host) {
|
||||
new_block->host = phys_mem_alloc(new_block->max_length, &new_block->mr->align, shared);
|
||||
......
|
||||
}
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里面,我们会调用qemu_ram_alloc,创建一个RAMBlock用来表示内存块。这里面调用ram_block_add->phys_mem_alloc。phys_mem_alloc是一个函数指针,指向函数qemu_anon_ram_alloc,这里面调用qemu_ram_mmap,在qemu_ram_mmap中调用mmap分配内存。
|
||||
|
||||
```
|
||||
static void *(*phys_mem_alloc)(size_t size, uint64_t *align, bool shared) = qemu_anon_ram_alloc;
|
||||
|
||||
void *qemu_anon_ram_alloc(size_t size, uint64_t *alignment, bool shared)
|
||||
{
|
||||
size_t align = QEMU_VMALLOC_ALIGN;
|
||||
void *ptr = qemu_ram_mmap(-1, size, align, shared);
|
||||
......
|
||||
if (alignment) {
|
||||
*alignment = align;
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void *qemu_ram_mmap(int fd, size_t size, size_t align, bool shared)
|
||||
{
|
||||
int flags;
|
||||
int guardfd;
|
||||
size_t offset;
|
||||
size_t pagesize;
|
||||
size_t total;
|
||||
void *guardptr;
|
||||
void *ptr;
|
||||
......
|
||||
total = size + align;
|
||||
guardfd = -1;
|
||||
pagesize = getpagesize();
|
||||
flags = MAP_PRIVATE | MAP_ANONYMOUS;
|
||||
guardptr = mmap(0, total, PROT_NONE, flags, guardfd, 0);
|
||||
......
|
||||
flags = MAP_FIXED;
|
||||
flags |= fd == -1 ? MAP_ANONYMOUS : 0;
|
||||
flags |= shared ? MAP_SHARED : MAP_PRIVATE;
|
||||
offset = QEMU_ALIGN_UP((uintptr_t)guardptr, align) - (uintptr_t)guardptr;
|
||||
ptr = mmap(guardptr + offset, size, PROT_READ | PROT_WRITE, flags, fd, 0);
|
||||
......
|
||||
return ptr;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们回到pc_memory_init,通过memory_region_allocate_system_memory申请到内存以后,为了兼容过去的版本,我们分成两个MemoryRegion进行管理,一个是ram_below_4g,一个是ram_above_4g。对于这两个MemoryRegion,我们都会初始化一个alias,也即别名,意思是说,两个MemoryRegion其实都指向memory_region_allocate_system_memory分配的内存,只不过分成两个部分,起两个别名指向不同的区域。
|
||||
|
||||
这两部分MemoryRegion都会调用memory_region_add_subregion,将这两部分作为子的内存区域添加到system_memory这棵树上。
|
||||
|
||||
接下来的调用链为:memory_region_add_subregion->memory_region_add_subregion_common->memory_region_update_container_subregions。
|
||||
|
||||
```
|
||||
static void memory_region_update_container_subregions(MemoryRegion *subregion)
|
||||
{
|
||||
MemoryRegion *mr = subregion->container;
|
||||
MemoryRegion *other;
|
||||
|
||||
memory_region_transaction_begin();
|
||||
|
||||
memory_region_ref(subregion);
|
||||
QTAILQ_FOREACH(other, &mr->subregions, subregions_link) {
|
||||
if (subregion->priority >= other->priority) {
|
||||
QTAILQ_INSERT_BEFORE(other, subregion, subregions_link);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
QTAILQ_INSERT_TAIL(&mr->subregions, subregion, subregions_link);
|
||||
done:
|
||||
memory_region_update_pending |= mr->enabled && subregion->enabled;
|
||||
memory_region_transaction_commit();
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在memory_region_update_container_subregions中,我们会将子区域放到链表中,然后调用memory_region_transaction_commit。在这里面,我们会调用address_space_set_flatview。因为内存区域变了,flatview也会变,就像上面分析过的一样,listener会被调用。
|
||||
|
||||
因为添加了一个MemoryRegion,region_add也即kvm_region_add。
|
||||
|
||||
```
|
||||
static void kvm_region_add(MemoryListener *listener,
|
||||
MemoryRegionSection *section)
|
||||
{
|
||||
KVMMemoryListener *kml = container_of(listener, KVMMemoryListener, listener);
|
||||
kvm_set_phys_mem(kml, section, true);
|
||||
}
|
||||
|
||||
static void kvm_set_phys_mem(KVMMemoryListener *kml,
|
||||
MemoryRegionSection *section, bool add)
|
||||
{
|
||||
KVMSlot *mem;
|
||||
int err;
|
||||
MemoryRegion *mr = section->mr;
|
||||
bool writeable = !mr->readonly && !mr->rom_device;
|
||||
hwaddr start_addr, size;
|
||||
void *ram;
|
||||
......
|
||||
size = kvm_align_section(section, &start_addr);
|
||||
......
|
||||
/* use aligned delta to align the ram address */
|
||||
ram = memory_region_get_ram_ptr(mr) + section->offset_within_region + (start_addr - section->offset_within_address_space);
|
||||
......
|
||||
/* register the new slot */
|
||||
mem = kvm_alloc_slot(kml);
|
||||
mem->memory_size = size;
|
||||
mem->start_addr = start_addr;
|
||||
mem->ram = ram;
|
||||
mem->flags = kvm_mem_flags(mr);
|
||||
|
||||
err = kvm_set_user_memory_region(kml, mem, true);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
kvm_region_add调用的是kvm_set_phys_mem,这里面分配一个用于放这块内存的KVMSlot结构,就像一个内存条一样,当然这是在用户态模拟出来的内存条,放在KVMState结构里面。这个结构是我们上一节创建虚拟机的时候创建的。
|
||||
|
||||
接下来,kvm_set_user_memory_region就会将用户态模拟出来的内存条,和内核中的KVM模块关联起来。
|
||||
|
||||
```
|
||||
static int kvm_set_user_memory_region(KVMMemoryListener *kml, KVMSlot *slot, bool new)
|
||||
{
|
||||
KVMState *s = kvm_state;
|
||||
struct kvm_userspace_memory_region mem;
|
||||
int ret;
|
||||
|
||||
mem.slot = slot->slot | (kml->as_id << 16);
|
||||
mem.guest_phys_addr = slot->start_addr;
|
||||
mem.userspace_addr = (unsigned long)slot->ram;
|
||||
mem.flags = slot->flags;
|
||||
......
|
||||
mem.memory_size = slot->memory_size;
|
||||
ret = kvm_vm_ioctl(s, KVM_SET_USER_MEMORY_REGION, &mem);
|
||||
slot->old_flags = mem.flags;
|
||||
......
|
||||
return ret;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
终于,在这里,我们又看到了可以和内核通信的kvm_vm_ioctl。我们来看内核收到KVM_SET_USER_MEMORY_REGION会做哪些事情。
|
||||
|
||||
```
|
||||
static long kvm_vm_ioctl(struct file *filp,
|
||||
unsigned int ioctl, unsigned long arg)
|
||||
{
|
||||
struct kvm *kvm = filp->private_data;
|
||||
void __user *argp = (void __user *)arg;
|
||||
switch (ioctl) {
|
||||
case KVM_SET_USER_MEMORY_REGION: {
|
||||
struct kvm_userspace_memory_region kvm_userspace_mem;
|
||||
if (copy_from_user(&kvm_userspace_mem, argp,
|
||||
sizeof(kvm_userspace_mem)))
|
||||
goto out;
|
||||
r = kvm_vm_ioctl_set_memory_region(kvm, &kvm_userspace_mem);
|
||||
break;
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
接下来的调用链为:kvm_vm_ioctl_set_memory_region->kvm_set_memory_region->__kvm_set_memory_region。
|
||||
|
||||
```
|
||||
int __kvm_set_memory_region(struct kvm *kvm,
|
||||
const struct kvm_userspace_memory_region *mem)
|
||||
{
|
||||
int r;
|
||||
gfn_t base_gfn;
|
||||
unsigned long npages;
|
||||
struct kvm_memory_slot *slot;
|
||||
struct kvm_memory_slot old, new;
|
||||
struct kvm_memslots *slots = NULL, *old_memslots;
|
||||
int as_id, id;
|
||||
enum kvm_mr_change change;
|
||||
......
|
||||
as_id = mem->slot >> 16;
|
||||
id = (u16)mem->slot;
|
||||
|
||||
slot = id_to_memslot(__kvm_memslots(kvm, as_id), id);
|
||||
base_gfn = mem->guest_phys_addr >> PAGE_SHIFT;
|
||||
npages = mem->memory_size >> PAGE_SHIFT;
|
||||
......
|
||||
new = old = *slot;
|
||||
|
||||
new.id = id;
|
||||
new.base_gfn = base_gfn;
|
||||
new.npages = npages;
|
||||
new.flags = mem->flags;
|
||||
......
|
||||
if (change == KVM_MR_CREATE) {
|
||||
new.userspace_addr = mem->userspace_addr;
|
||||
|
||||
if (kvm_arch_create_memslot(kvm, &new, npages))
|
||||
goto out_free;
|
||||
}
|
||||
......
|
||||
slots = kvzalloc(sizeof(struct kvm_memslots), GFP_KERNEL);
|
||||
memcpy(slots, __kvm_memslots(kvm, as_id), sizeof(struct kvm_memslots));
|
||||
......
|
||||
r = kvm_arch_prepare_memory_region(kvm, &new, mem, change);
|
||||
|
||||
update_memslots(slots, &new);
|
||||
old_memslots = install_new_memslots(kvm, as_id, slots);
|
||||
|
||||
kvm_arch_commit_memory_region(kvm, mem, &old, &new, change);
|
||||
return 0;
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在用户态每个KVMState有多个KVMSlot,在内核里面,同样每个struct kvm也有多个struct kvm_memory_slot,两者是对应起来的。
|
||||
|
||||
```
|
||||
//用户态
|
||||
struct KVMState
|
||||
{
|
||||
......
|
||||
int nr_slots;
|
||||
......
|
||||
KVMMemoryListener memory_listener;
|
||||
......
|
||||
};
|
||||
|
||||
typedef struct KVMMemoryListener {
|
||||
MemoryListener listener;
|
||||
KVMSlot *slots;
|
||||
int as_id;
|
||||
} KVMMemoryListener
|
||||
|
||||
typedef struct KVMSlot
|
||||
{
|
||||
hwaddr start_addr;
|
||||
ram_addr_t memory_size;
|
||||
void *ram;
|
||||
int slot;
|
||||
int flags;
|
||||
int old_flags;
|
||||
} KVMSlot;
|
||||
|
||||
//内核态
|
||||
struct kvm {
|
||||
spinlock_t mmu_lock;
|
||||
struct mutex slots_lock;
|
||||
struct mm_struct *mm; /* userspace tied to this vm */
|
||||
struct kvm_memslots __rcu *memslots[KVM_ADDRESS_SPACE_NUM];
|
||||
......
|
||||
}
|
||||
|
||||
struct kvm_memslots {
|
||||
u64 generation;
|
||||
struct kvm_memory_slot memslots[KVM_MEM_SLOTS_NUM];
|
||||
/* The mapping table from slot id to the index in memslots[]. */
|
||||
short id_to_index[KVM_MEM_SLOTS_NUM];
|
||||
atomic_t lru_slot;
|
||||
int used_slots;
|
||||
};
|
||||
|
||||
struct kvm_memory_slot {
|
||||
gfn_t base_gfn;//根据guest_phys_addr计算
|
||||
unsigned long npages;
|
||||
unsigned long *dirty_bitmap;
|
||||
struct kvm_arch_memory_slot arch;
|
||||
unsigned long userspace_addr;
|
||||
u32 flags;
|
||||
short id;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
并且,id_to_memslot函数可以根据用户态的slot号得到内核态的slot结构。
|
||||
|
||||
如果传进来的参数是KVM_MR_CREATE,表示要创建一个新的内存条,就会调用kvm_arch_create_memslot来创建kvm_memory_slot的成员kvm_arch_memory_slot。
|
||||
|
||||
接下来就是创建kvm_memslots结构,填充这个结构,然后通过install_new_memslots将这个新的内存条,添加到struct kvm结构中。
|
||||
|
||||
至此,用户态的内存结构和内核态的内存结构算是对应了起来。
|
||||
|
||||
## 页面分配和映射
|
||||
|
||||
上面对于内存的管理,还只是停留在元数据的管理。对于内存的分配与映射,我们还没有涉及,接下来,我们就来看看,页面是如何进行分配和映射的。
|
||||
|
||||
上面咱们说了,内存映射对于虚拟机来讲是一件非常麻烦的事情,从GVA到GPA到HVA到HPA,性能很差,为了解决这个问题,有两种主要的思路。
|
||||
|
||||
### 影子页表
|
||||
|
||||
第一种方式就是软件的方式,**影子页表** (Shadow Page Table)。
|
||||
|
||||
按照咱们在内存管理那一节讲的,内存映射要通过页表来管理,页表地址应该放在cr3寄存器里面。本来的过程是,客户机要通过cr3找到客户机的页表,实现从GVA到GPA的转换,然后在宿主机上,要通过cr3找到宿主机的页表,实现从HVA到HPA的转换。
|
||||
|
||||
为了实现客户机虚拟地址空间到宿主机物理地址空间的直接映射。客户机中每个进程都有自己的虚拟地址空间,所以KVM需要为客户机中的每个进程页表都要维护一套相应的影子页表。
|
||||
|
||||
在客户机访问内存时,使用的不是客户机的原来的页表,而是这个页表对应的影子页表,从而实现了从客户机虚拟地址到宿主机物理地址的直接转换。而且,在TLB和CPU 缓存上缓存的是来自影子页表中客户机虚拟地址和宿主机物理地址之间的映射,也因此提高了缓存的效率。
|
||||
|
||||
但是影子页表的引入也意味着 KVM 需要为每个客户机的每个进程的页表都要维护一套相应的影子页表,内存占用比较大,而且客户机页表和和影子页表也需要进行实时同步。
|
||||
|
||||
### 扩展页表
|
||||
|
||||
于是就有了第二种方式,就是硬件的方式,Intel的EPT(Extent Page Table,扩展页表)技术。
|
||||
|
||||
EPT在原有客户机页表对客户机虚拟地址到客户机物理地址映射的基础上,又引入了 EPT页表来实现客户机物理地址到宿主机物理地址的另一次映射。客户机运行时,客户机页表被载入 CR3,而EPT页表被载入专门的EPT 页表指针寄存器 EPTP。
|
||||
|
||||
有了EPT,在客户机物理地址到宿主机物理地址转换的过程中,缺页会产生EPT 缺页异常。KVM首先根据引起异常的客户机物理地址,映射到对应的宿主机虚拟地址,然后为此虚拟地址分配新的物理页,最后 KVM 再更新 EPT 页表,建立起引起异常的客户机物理地址到宿主机物理地址之间的映射。
|
||||
|
||||
KVM 只需为每个客户机维护一套 EPT 页表,也大大减少了内存的开销。
|
||||
|
||||
这里,我们重点看第二种方式。因为使用了EPT之后,客户机里面的页表映射,也即从GVA到GPA的转换,还是用传统的方式,和在内存管理那一章讲的没有什么区别。而EPT重点帮我们解决的就是从GPA到HPA的转换问题。因为要经过两次页表,所以EPT又称为tdp(two dimentional paging)。
|
||||
|
||||
EPT的页表结构也是分为四层,EPT Pointer (EPTP)指向PML4的首地址。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/02/30/02e4740398bc3685f366351260ae7230.jpg" alt="">
|
||||
|
||||
管理物理页面的Page结构和咱们讲内存管理那一章是一样的。EPT页表也需要存放在一个页中,这些页要用kvm_mmu_page这个结构来管理。
|
||||
|
||||
当一个虚拟机运行,进入客户机模式的时候,我们上一节解析过,它会调用vcpu_enter_guest函数,这里面会调用kvm_mmu_reload->kvm_mmu_load。
|
||||
|
||||
```
|
||||
int kvm_mmu_load(struct kvm_vcpu *vcpu)
|
||||
{
|
||||
......
|
||||
r = mmu_topup_memory_caches(vcpu);
|
||||
r = mmu_alloc_roots(vcpu);
|
||||
kvm_mmu_sync_roots(vcpu);
|
||||
/* set_cr3() should ensure TLB has been flushed */
|
||||
vcpu->arch.mmu.set_cr3(vcpu, vcpu->arch.mmu.root_hpa);
|
||||
......
|
||||
}
|
||||
|
||||
static int mmu_alloc_roots(struct kvm_vcpu *vcpu)
|
||||
{
|
||||
if (vcpu->arch.mmu.direct_map)
|
||||
return mmu_alloc_direct_roots(vcpu);
|
||||
else
|
||||
return mmu_alloc_shadow_roots(vcpu);
|
||||
}
|
||||
|
||||
static int mmu_alloc_direct_roots(struct kvm_vcpu *vcpu)
|
||||
{
|
||||
struct kvm_mmu_page *sp;
|
||||
unsigned i;
|
||||
|
||||
if (vcpu->arch.mmu.shadow_root_level == PT64_ROOT_LEVEL) {
|
||||
spin_lock(&vcpu->kvm->mmu_lock);
|
||||
make_mmu_pages_available(vcpu);
|
||||
sp = kvm_mmu_get_page(vcpu, 0, 0, PT64_ROOT_LEVEL, 1, ACC_ALL);
|
||||
++sp->root_count;
|
||||
spin_unlock(&vcpu->kvm->mmu_lock);
|
||||
vcpu->arch.mmu.root_hpa = __pa(sp->spt);
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里构建的是页表的根部,也即顶级页表,并且设置cr3来刷新TLB。mmu_alloc_roots会调用mmu_alloc_direct_roots,因为我们用的是EPT模式,而非影子表。在mmu_alloc_direct_roots中,kvm_mmu_get_page会分配一个kvm_mmu_page,来存放顶级页表项。
|
||||
|
||||
接下来,当虚拟机真的要访问内存的时候,会发现有的页表没有建立,有的物理页没有分配,这都会触发缺页异常,在KVM里面会发送VM-Exit,从客户机模式转换为宿主机模式,来修复这个缺失的页表或者物理页。
|
||||
|
||||
```
|
||||
static int (*const kvm_vmx_exit_handlers[])(struct kvm_vcpu *vcpu) = {
|
||||
[EXIT_REASON_EXCEPTION_NMI] = handle_exception,
|
||||
[EXIT_REASON_EXTERNAL_INTERRUPT] = handle_external_interrupt,
|
||||
[EXIT_REASON_IO_INSTRUCTION] = handle_io,
|
||||
......
|
||||
[EXIT_REASON_EPT_VIOLATION] = handle_ept_violation,
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
咱们前面讲过,虚拟机退出客户机模式有很多种原因,例如接收到中断、接收到I/O等,EPT的缺页异常也是一种类型,我们称为EXIT_REASON_EPT_VIOLATION,对应的处理函数是handle_ept_violation。
|
||||
|
||||
```
|
||||
static int handle_ept_violation(struct kvm_vcpu *vcpu)
|
||||
{
|
||||
gpa_t gpa;
|
||||
......
|
||||
gpa = vmcs_read64(GUEST_PHYSICAL_ADDRESS);
|
||||
......
|
||||
vcpu->arch.gpa_available = true;
|
||||
vcpu->arch.exit_qualification = exit_qualification;
|
||||
|
||||
return kvm_mmu_page_fault(vcpu, gpa, error_code, NULL, 0);
|
||||
}
|
||||
|
||||
int kvm_mmu_page_fault(struct kvm_vcpu *vcpu, gva_t cr2, u64 error_code,
|
||||
void *insn, int insn_len)
|
||||
{
|
||||
......
|
||||
r = vcpu->arch.mmu.page_fault(vcpu, cr2, lower_32_bits(error_code),false);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在handle_ept_violation里面,我们从VMCS中得到没有解析成功的GPA,也即客户机的物理地址,然后调用kvm_mmu_page_fault,看为什么解析不成功。kvm_mmu_page_fault会调用page_fault函数,其实是tdp_page_fault函数。tdp的意思就是EPT,前面我们解释过了。
|
||||
|
||||
```
|
||||
static int tdp_page_fault(struct kvm_vcpu *vcpu, gva_t gpa, u32 error_code, bool prefault)
|
||||
{
|
||||
kvm_pfn_t pfn;
|
||||
int r;
|
||||
int level;
|
||||
bool force_pt_level;
|
||||
gfn_t gfn = gpa >> PAGE_SHIFT;
|
||||
unsigned long mmu_seq;
|
||||
int write = error_code & PFERR_WRITE_MASK;
|
||||
bool map_writable;
|
||||
|
||||
r = mmu_topup_memory_caches(vcpu);
|
||||
level = mapping_level(vcpu, gfn, &force_pt_level);
|
||||
......
|
||||
if (try_async_pf(vcpu, prefault, gfn, gpa, &pfn, write, &map_writable))
|
||||
return 0;
|
||||
|
||||
if (handle_abnormal_pfn(vcpu, 0, gfn, pfn, ACC_ALL, &r))
|
||||
return r;
|
||||
|
||||
make_mmu_pages_available(vcpu);
|
||||
r = __direct_map(vcpu, write, map_writable, level, gfn, pfn, prefault);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
既然没有映射,就应该加上映射,tdp_page_fault就是干这个事情的。
|
||||
|
||||
在tdp_page_fault这个函数开头,我们通过gpa,也即客户机的物理地址得到客户机的页号gfn。接下来,我们要通过调用try_async_pf得到宿主机的物理地址对应的页号,也即真正的物理页的页号,然后通过__direct_map将两者关联起来。
|
||||
|
||||
```
|
||||
static bool try_async_pf(struct kvm_vcpu *vcpu, bool prefault, gfn_t gfn, gva_t gva, kvm_pfn_t *pfn, bool write, bool *writable)
|
||||
{
|
||||
struct kvm_memory_slot *slot;
|
||||
bool async;
|
||||
|
||||
slot = kvm_vcpu_gfn_to_memslot(vcpu, gfn);
|
||||
async = false;
|
||||
*pfn = __gfn_to_pfn_memslot(slot, gfn, false, &async, write, writable);
|
||||
if (!async)
|
||||
return false; /* *pfn has correct page already */
|
||||
|
||||
if (!prefault && kvm_can_do_async_pf(vcpu)) {
|
||||
if (kvm_find_async_pf_gfn(vcpu, gfn)) {
|
||||
kvm_make_request(KVM_REQ_APF_HALT, vcpu);
|
||||
return true;
|
||||
} else if (kvm_arch_setup_async_pf(vcpu, gva, gfn))
|
||||
return true;
|
||||
}
|
||||
*pfn = __gfn_to_pfn_memslot(slot, gfn, false, NULL, write, writable);
|
||||
return false;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在try_async_pf中,要想得到pfn,也即物理页的页号,会先通过kvm_vcpu_gfn_to_memslot,根据客户机的物理地址对应的页号找到内存条,然后调用__gfn_to_pfn_memslot,根据内存条找到pfn。
|
||||
|
||||
```
|
||||
kvm_pfn_t __gfn_to_pfn_memslot(struct kvm_memory_slot *slot, gfn_t gfn,bool atomic, bool *async, bool write_fault,bool *writable)
|
||||
{
|
||||
unsigned long addr = __gfn_to_hva_many(slot, gfn, NULL, write_fault);
|
||||
......
|
||||
return hva_to_pfn(addr, atomic, async, write_fault,
|
||||
writable);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在__gfn_to_pfn_memslot中,我们会调用__gfn_to_hva_many,从客户机物理地址对应的页号,得到宿主机虚拟地址hva,然后从宿主机虚拟地址到宿主机物理地址,调用的是hva_to_pfn。
|
||||
|
||||
hva_to_pfn会调用hva_to_pfn_slow。
|
||||
|
||||
```
|
||||
static int hva_to_pfn_slow(unsigned long addr, bool *async, bool write_fault,
|
||||
bool *writable, kvm_pfn_t *pfn)
|
||||
{
|
||||
struct page *page[1];
|
||||
int npages = 0;
|
||||
......
|
||||
if (async) {
|
||||
npages = get_user_page_nowait(addr, write_fault, page);
|
||||
} else {
|
||||
......
|
||||
npages = get_user_pages_unlocked(addr, 1, page, flags);
|
||||
}
|
||||
......
|
||||
*pfn = page_to_pfn(page[0]);
|
||||
return npages;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在hva_to_pfn_slow中,我们要先调用get_user_page_nowait,得到一个物理页面,然后再调用page_to_pfn将物理页面转换成为物理页号。
|
||||
|
||||
无论是哪一种get_user_pages_XXX,最终都会调用__get_user_pages函数。这里面会调用faultin_page,在faultin_page中我们会调用handle_mm_fault。看到这个是不是很熟悉?这就是咱们内存管理那一章讲的缺页异常的逻辑,分配一个物理内存。
|
||||
|
||||
至此,try_async_pf得到了物理页面,并且转换为对应的物理页号。
|
||||
|
||||
接下来,__direct_map会关联客户机物理页号和宿主机物理页号。
|
||||
|
||||
```
|
||||
static int __direct_map(struct kvm_vcpu *vcpu, int write, int map_writable,
|
||||
int level, gfn_t gfn, kvm_pfn_t pfn, bool prefault)
|
||||
{
|
||||
struct kvm_shadow_walk_iterator iterator;
|
||||
struct kvm_mmu_page *sp;
|
||||
int emulate = 0;
|
||||
gfn_t pseudo_gfn;
|
||||
|
||||
if (!VALID_PAGE(vcpu->arch.mmu.root_hpa))
|
||||
return 0;
|
||||
|
||||
for_each_shadow_entry(vcpu, (u64)gfn << PAGE_SHIFT, iterator) {
|
||||
if (iterator.level == level) {
|
||||
emulate = mmu_set_spte(vcpu, iterator.sptep, ACC_ALL,
|
||||
write, level, gfn, pfn, prefault,
|
||||
map_writable);
|
||||
direct_pte_prefetch(vcpu, iterator.sptep);
|
||||
++vcpu->stat.pf_fixed;
|
||||
break;
|
||||
}
|
||||
|
||||
drop_large_spte(vcpu, iterator.sptep);
|
||||
if (!is_shadow_present_pte(*iterator.sptep)) {
|
||||
u64 base_addr = iterator.addr;
|
||||
|
||||
base_addr &= PT64_LVL_ADDR_MASK(iterator.level);
|
||||
pseudo_gfn = base_addr >> PAGE_SHIFT;
|
||||
sp = kvm_mmu_get_page(vcpu, pseudo_gfn, iterator.addr,
|
||||
iterator.level - 1, 1, ACC_ALL);
|
||||
|
||||
link_shadow_page(vcpu, iterator.sptep, sp);
|
||||
}
|
||||
}
|
||||
return emulate;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
__direct_map首先判断页表的根是否存在,当然存在,我们刚才初始化了。
|
||||
|
||||
接下来是for_each_shadow_entry一个循环。每一个循环中,先是会判断需要映射的level,是否正是当前循环的这个iterator.level。如果是,则说明是叶子节点,直接映射真正的物理页面pfn,然后退出。接着是非叶子节点的情形,判断如果这一项指向的页表项不存在,就要建立页表项,通过kvm_mmu_get_page得到保存页表项的页面,然后将这一项指向下一级的页表页面。
|
||||
|
||||
至此,内存映射就结束了。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
我们这里来总结一下,虚拟机的内存管理也是需要用户态的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页表映射。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/01/9b/0186c533b7ef706df880dfd775c2449b.jpg" alt="">
|
||||
|
||||
## 课堂练习
|
||||
|
||||
这一节,影子页表我们没有深入去讲,你能自己研究一下,它是如何实现的吗?
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解,也欢迎收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
|
||||
@@ -0,0 +1,440 @@
|
||||
<audio id="audio" title="53 | 存储虚拟化(上):如何建立自己保管的单独档案库?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/0c/b5/0cea069cfe1898557329f1b2e780f1b5.mp3"></audio>
|
||||
|
||||
前面几节,我们讲了CPU和内存的虚拟化。我们知道,完全虚拟化是很慢的,而通过内核的KVM技术和EPT技术,加速虚拟机对于物理CPU和内存的使用,我们称为硬件辅助虚拟化。
|
||||
|
||||
对于一台虚拟机而言,除了要虚拟化CPU和内存,存储和网络也需要虚拟化,存储和网络都属于外部设备,这些外部设备应该如何虚拟化呢?
|
||||
|
||||
当然一种方式还是完全虚拟化。比如,有什么样的硬盘设备或者网卡设备,我们就用qemu模拟一个一模一样的软件的硬盘和网卡设备,这样在虚拟机里面的操作系统看来,使用这些设备和使用物理设备是一样的。当然缺点就是,qemu模拟的设备又是一个翻译官的角色。虽然这个时候虚拟机里面的操作系统,意识不到自己是运行在虚拟机里面的,但是这种每个指令都翻译的方式,实在是太慢了。
|
||||
|
||||
另外一种方式就是,虚拟机里面的操作系统不是一个通用的操作系统,它知道自己是运行在虚拟机里面的,使用的硬盘设备和网络设备都是虚拟的,应该加载特殊的驱动才能运行。这些特殊的驱动往往要通过虚拟机里面和外面配合工作的模式,来加速对于物理存储和网络设备的使用。
|
||||
|
||||
## virtio的基本原理
|
||||
|
||||
在虚拟化技术的早期,不同的虚拟化技术会针对不同硬盘设备和网络设备实现不同的驱动,虚拟机里面的操作系统也要根据不同的虚拟化技术和物理存储和网络设备,选择加载不同的驱动。但是,由于硬盘设备和网络设备太多了,驱动纷繁复杂。
|
||||
|
||||
后来慢慢就形成了一定的标准,这就是**virtio**,就是**虚拟化I/O设备**的意思。virtio负责对于虚拟机提供统一的接口。也就是说,在虚拟机里面的操作系统加载的驱动,以后都统一加载virtio就可以了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1e/33/1e13ffd5ac846c52739291cb489d0233.png" alt="">
|
||||
|
||||
在虚拟机外,我们可以实现不同的virtio的后端,来适配不同的物理硬件设备。那virtio到底长什么样子呢?我们一起来看一看。
|
||||
|
||||
virtio的架构可以分为四层。
|
||||
|
||||
- 首先,在虚拟机里面的virtio前端,针对不同类型的设备有不同的**驱动程序**,但是接口都是统一的。例如,硬盘就是virtio_blk,网络就是virtio_net。
|
||||
- 其次,在宿主机的qemu里面,实现virtio后端的逻辑,主要就是**操作硬件的设备**。例如通过写一个物理机硬盘上的文件来完成虚拟机写入硬盘的操作。再如向内核协议栈发送一个网络包完成虚拟机对于网络的操作。
|
||||
- 在virtio的前端和后端之间,有一个通信层,里面包含**virtio层**和**virtio-ring层**。virtio这一层实现的是虚拟队列接口,算是前后端通信的桥梁。而virtio-ring则是该桥梁的具体实现。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/2e/f3/2e9ef612f7b80ec9fcd91e200f4946f3.png" alt="">
|
||||
|
||||
virtio使用virtqueue进行前端和后端的高速通信。不同类型的设备队列数目不同。virtio-net使用两个队列,一个用于接收,另一个用于发送;而 virtio-blk仅使用一个队列。
|
||||
|
||||
如果客户机要向宿主机发送数据,客户机会将数据的buffer添加到virtqueue中,然后通过写入寄存器通知宿主机。这样宿主机就可以从virtqueue 中收到的buffer里面的数据。
|
||||
|
||||
了解了virtio的基本原理,接下来,我们以硬盘写入为例,具体看一下存储虚拟化的过程。
|
||||
|
||||
## 初始化阶段的存储虚拟化
|
||||
|
||||
和咱们在学习CPU的时候看到的一样,Virtio Block Device也是一种类。它的继承关系如下:
|
||||
|
||||
```
|
||||
static const TypeInfo device_type_info = {
|
||||
.name = TYPE_DEVICE,
|
||||
.parent = TYPE_OBJECT,
|
||||
.instance_size = sizeof(DeviceState),
|
||||
.instance_init = device_initfn,
|
||||
.instance_post_init = device_post_init,
|
||||
.instance_finalize = device_finalize,
|
||||
.class_base_init = device_class_base_init,
|
||||
.class_init = device_class_init,
|
||||
.abstract = true,
|
||||
.class_size = sizeof(DeviceClass),
|
||||
};
|
||||
|
||||
static const TypeInfo virtio_device_info = {
|
||||
.name = TYPE_VIRTIO_DEVICE,
|
||||
.parent = TYPE_DEVICE,
|
||||
.instance_size = sizeof(VirtIODevice),
|
||||
.class_init = virtio_device_class_init,
|
||||
.instance_finalize = virtio_device_instance_finalize,
|
||||
.abstract = true,
|
||||
.class_size = sizeof(VirtioDeviceClass),
|
||||
};
|
||||
|
||||
static const TypeInfo virtio_blk_info = {
|
||||
.name = TYPE_VIRTIO_BLK,
|
||||
.parent = TYPE_VIRTIO_DEVICE,
|
||||
.instance_size = sizeof(VirtIOBlock),
|
||||
.instance_init = virtio_blk_instance_init,
|
||||
.class_init = virtio_blk_class_init,
|
||||
};
|
||||
|
||||
static void virtio_register_types(void)
|
||||
{
|
||||
type_register_static(&virtio_blk_info);
|
||||
}
|
||||
|
||||
type_init(virtio_register_types)
|
||||
|
||||
```
|
||||
|
||||
Virtio Block Device这种类的定义是有多层继承关系的。TYPE_VIRTIO_BLK的父类是TYPE_VIRTIO_DEVICE,TYPE_VIRTIO_DEVICE的父类是TYPE_DEVICE,TYPE_DEVICE的父类是TYPE_OBJECT。到头了。
|
||||
|
||||
type_init用于注册这种类。这里面每一层都有class_init,用于从TypeImpl生产xxxClass。还有instance_init,可以将xxxClass初始化为实例。
|
||||
|
||||
在TYPE_VIRTIO_BLK层的class_init函数virtio_blk_class_init中,定义了DeviceClass的realize函数为virtio_blk_device_realize,这一点在[CPU](https://time.geekbang.org/column/article/109335)那一节也有类似的结构。
|
||||
|
||||
```
|
||||
static void virtio_blk_device_realize(DeviceState *dev, Error **errp)
|
||||
{
|
||||
VirtIODevice *vdev = VIRTIO_DEVICE(dev);
|
||||
VirtIOBlock *s = VIRTIO_BLK(dev);
|
||||
VirtIOBlkConf *conf = &s->conf;
|
||||
......
|
||||
blkconf_blocksizes(&conf->conf);
|
||||
virtio_blk_set_config_size(s, s->host_features);
|
||||
virtio_init(vdev, "virtio-blk", VIRTIO_ID_BLOCK, s->config_size);
|
||||
s->blk = conf->conf.blk;
|
||||
s->rq = NULL;
|
||||
s->sector_mask = (s->conf.conf.logical_block_size / BDRV_SECTOR_SIZE) - 1;
|
||||
for (i = 0; i < conf->num_queues; i++) {
|
||||
virtio_add_queue(vdev, conf->queue_size, virtio_blk_handle_output);
|
||||
}
|
||||
virtio_blk_data_plane_create(vdev, conf, &s->dataplane, &err);
|
||||
s->change = qemu_add_vm_change_state_handler(virtio_blk_dma_restart_cb, s);
|
||||
blk_set_dev_ops(s->blk, &virtio_block_ops, s);
|
||||
blk_set_guest_block_size(s->blk, s->conf.conf.logical_block_size);
|
||||
blk_iostatus_enable(s->blk);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在virtio_blk_device_realize函数中,我们先是通过virtio_init初始化VirtIODevice结构。
|
||||
|
||||
```
|
||||
void virtio_init(VirtIODevice *vdev, const char *name,
|
||||
uint16_t device_id, size_t config_size)
|
||||
{
|
||||
BusState *qbus = qdev_get_parent_bus(DEVICE(vdev));
|
||||
VirtioBusClass *k = VIRTIO_BUS_GET_CLASS(qbus);
|
||||
int i;
|
||||
int nvectors = k->query_nvectors ? k->query_nvectors(qbus->parent) : 0;
|
||||
|
||||
if (nvectors) {
|
||||
vdev->vector_queues =
|
||||
g_malloc0(sizeof(*vdev->vector_queues) * nvectors);
|
||||
}
|
||||
vdev->device_id = device_id;
|
||||
vdev->status = 0;
|
||||
atomic_set(&vdev->isr, 0);
|
||||
vdev->queue_sel = 0;
|
||||
vdev->config_vector = VIRTIO_NO_VECTOR;
|
||||
vdev->vq = g_malloc0(sizeof(VirtQueue) * VIRTIO_QUEUE_MAX);
|
||||
vdev->vm_running = runstate_is_running();
|
||||
vdev->broken = false;
|
||||
for (i = 0; i < VIRTIO_QUEUE_MAX; i++) {
|
||||
vdev->vq[i].vector = VIRTIO_NO_VECTOR;
|
||||
vdev->vq[i].vdev = vdev;
|
||||
vdev->vq[i].queue_index = i;
|
||||
}
|
||||
vdev->name = name;
|
||||
vdev->config_len = config_size;
|
||||
if (vdev->config_len) {
|
||||
vdev->config = g_malloc0(config_size);
|
||||
} else {
|
||||
vdev->config = NULL;
|
||||
}
|
||||
vdev->vmstate = qemu_add_vm_change_state_handler(virtio_vmstate_change,
|
||||
vdev);
|
||||
vdev->device_endian = virtio_default_endian();
|
||||
vdev->use_guest_notifier_mask = true;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
从virtio_init中可以看出,VirtIODevice结构里面有一个VirtQueue数组,这就是virtio前端和后端互相传数据的队列,最多VIRTIO_QUEUE_MAX个。
|
||||
|
||||
我们回到virtio_blk_device_realize函数。接下来,根据配置的队列数目num_queues,对于每个队列都调用virtio_add_queue来初始化队列。
|
||||
|
||||
```
|
||||
VirtQueue *virtio_add_queue(VirtIODevice *vdev, int queue_size,
|
||||
VirtIOHandleOutput handle_output)
|
||||
{
|
||||
int i;
|
||||
vdev->vq[i].vring.num = queue_size;
|
||||
vdev->vq[i].vring.num_default = queue_size;
|
||||
vdev->vq[i].vring.align = VIRTIO_PCI_VRING_ALIGN;
|
||||
vdev->vq[i].handle_output = handle_output;
|
||||
vdev->vq[i].handle_aio_output = NULL;
|
||||
|
||||
return &vdev->vq[i];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在每个VirtQueue中,都有一个vring,用来维护这个队列里面的数据;另外还有一个函数virtio_blk_handle_output,用于处理数据写入,这个函数我们后面会用到。
|
||||
|
||||
至此,VirtIODevice,VirtQueue,vring之间的关系如下图所示。这是在qemu里面的对应关系,请你记好,后面我们还能看到类似的结构。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e1/6d/e18dae0a5951392c4a8e8630e53a616d.jpg" alt="">
|
||||
|
||||
## qemu启动过程中的存储虚拟化
|
||||
|
||||
初始化过程解析完毕以后,我们接下来从qemu的启动过程看起。
|
||||
|
||||
对于硬盘的虚拟化,qemu的启动参数里面有关的是下面两行:
|
||||
|
||||
```
|
||||
-drive file=/var/lib/nova/instances/1f8e6f7e-5a70-4780-89c1-464dc0e7f308/disk,if=none,id=drive-virtio-disk0,format=qcow2,cache=none
|
||||
-device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x4,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=1
|
||||
|
||||
```
|
||||
|
||||
其中,第一行指定了宿主机硬盘上的一个文件,文件的格式是qcow2,这个格式我们这里不准备解析它,你只要明白,对于宿主机上的一个文件,可以被qemu模拟称为客户机上的一块硬盘就可以了。
|
||||
|
||||
而第二行说明了,使用的驱动是virtio-blk驱动。
|
||||
|
||||
```
|
||||
configure_blockdev(&bdo_queue, machine_class, snapshot);
|
||||
|
||||
```
|
||||
|
||||
在qemu启动的main函数里面,初始化块设备,是通过configure_blockdev调用开始的。
|
||||
|
||||
```
|
||||
static void configure_blockdev(BlockdevOptionsQueue *bdo_queue, MachineClass *machine_class, int snapshot)
|
||||
{
|
||||
......
|
||||
if (qemu_opts_foreach(qemu_find_opts("drive"), drive_init_func,
|
||||
&machine_class->block_default_type, &error_fatal)) {
|
||||
.....
|
||||
}
|
||||
}
|
||||
|
||||
static int drive_init_func(void *opaque, QemuOpts *opts, Error **errp)
|
||||
{
|
||||
BlockInterfaceType *block_default_type = opaque;
|
||||
return drive_new(opts, *block_default_type, errp) == NULL;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在configure_blockdev中,我们能看到对于drive这个参数的解析,并且初始化这个设备要调用drive_init_func函数,这里面会调用drive_new创建一个设备。
|
||||
|
||||
```
|
||||
DriveInfo *drive_new(QemuOpts *all_opts, BlockInterfaceType block_default_type, Error **errp)
|
||||
{
|
||||
const char *value;
|
||||
BlockBackend *blk;
|
||||
DriveInfo *dinfo = NULL;
|
||||
QDict *bs_opts;
|
||||
QemuOpts *legacy_opts;
|
||||
DriveMediaType media = MEDIA_DISK;
|
||||
BlockInterfaceType type;
|
||||
int max_devs, bus_id, unit_id, index;
|
||||
const char *werror, *rerror;
|
||||
bool read_only = false;
|
||||
bool copy_on_read;
|
||||
const char *filename;
|
||||
Error *local_err = NULL;
|
||||
int i;
|
||||
......
|
||||
legacy_opts = qemu_opts_create(&qemu_legacy_drive_opts, NULL, 0,
|
||||
&error_abort);
|
||||
......
|
||||
/* Add virtio block device */
|
||||
if (type == IF_VIRTIO) {
|
||||
QemuOpts *devopts;
|
||||
devopts = qemu_opts_create(qemu_find_opts("device"), NULL, 0,
|
||||
&error_abort);
|
||||
qemu_opt_set(devopts, "driver", "virtio-blk-pci", &error_abort);
|
||||
qemu_opt_set(devopts, "drive", qdict_get_str(bs_opts, "id"),
|
||||
&error_abort);
|
||||
}
|
||||
|
||||
filename = qemu_opt_get(legacy_opts, "file");
|
||||
......
|
||||
/* Actual block device init: Functionality shared with blockdev-add */
|
||||
blk = blockdev_init(filename, bs_opts, &local_err);
|
||||
......
|
||||
/* Create legacy DriveInfo */
|
||||
dinfo = g_malloc0(sizeof(*dinfo));
|
||||
dinfo->opts = all_opts;
|
||||
|
||||
dinfo->type = type;
|
||||
dinfo->bus = bus_id;
|
||||
dinfo->unit = unit_id;
|
||||
|
||||
blk_set_legacy_dinfo(blk, dinfo);
|
||||
|
||||
switch(type) {
|
||||
case IF_IDE:
|
||||
case IF_SCSI:
|
||||
case IF_XEN:
|
||||
case IF_NONE:
|
||||
dinfo->media_cd = media == MEDIA_CDROM;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在drive_new里面,会解析qemu的启动参数。对于virtio来讲,会解析device参数,把driver设置为virtio-blk-pci;还会解析file参数,就是指向那个宿主机上的文件。
|
||||
|
||||
接下来,drive_new会调用blockdev_init,根据参数进行初始化,最后会创建一个DriveInfo来管理这个设备。
|
||||
|
||||
我们重点来看blockdev_init。在这里面,我们发现,如果file不为空,则应该调用blk_new_open打开宿主机上的硬盘文件,返回的结果是BlockBackend,对应我们上面讲原理的时候的virtio的后端。
|
||||
|
||||
```
|
||||
BlockBackend *blk_new_open(const char *filename, const char *reference,
|
||||
QDict *options, int flags, Error **errp)
|
||||
{
|
||||
BlockBackend *blk;
|
||||
BlockDriverState *bs;
|
||||
uint64_t perm = 0;
|
||||
......
|
||||
blk = blk_new(perm, BLK_PERM_ALL);
|
||||
bs = bdrv_open(filename, reference, options, flags, errp);
|
||||
blk->root = bdrv_root_attach_child(bs, "root", &child_root,
|
||||
perm, BLK_PERM_ALL, blk, errp);
|
||||
return blk;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
接下来的调用链为:bdrv_open->bdrv_open_inherit->bdrv_open_common.
|
||||
|
||||
```
|
||||
static int bdrv_open_common(BlockDriverState *bs, BlockBackend *file,
|
||||
QDict *options, Error **errp)
|
||||
{
|
||||
int ret, open_flags;
|
||||
const char *filename;
|
||||
const char *driver_name = NULL;
|
||||
const char *node_name = NULL;
|
||||
const char *discard;
|
||||
QemuOpts *opts;
|
||||
BlockDriver *drv;
|
||||
Error *local_err = NULL;
|
||||
......
|
||||
drv = bdrv_find_format(driver_name);
|
||||
......
|
||||
ret = bdrv_open_driver(bs, drv, node_name, options, open_flags, errp);
|
||||
......
|
||||
}
|
||||
|
||||
static int bdrv_open_driver(BlockDriverState *bs, BlockDriver *drv,
|
||||
const char *node_name, QDict *options,
|
||||
int open_flags, Error **errp)
|
||||
{
|
||||
......
|
||||
bs->drv = drv;
|
||||
bs->read_only = !(bs->open_flags & BDRV_O_RDWR);
|
||||
bs->opaque = g_malloc0(drv->instance_size);
|
||||
|
||||
if (drv->bdrv_open) {
|
||||
ret = drv->bdrv_open(bs, options, open_flags, &local_err);
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在bdrv_open_common中,根据硬盘文件的格式,得到BlockDriver。因为虚拟机的硬盘文件格式有很多种,qcow2是一种,raw是一种,vmdk是一种,各有优缺点,启动虚拟机的时候,可以自由选择。
|
||||
|
||||
对于不同的格式,打开的方式不一样,我们拿qcow2来解析。它的BlockDriver定义如下:
|
||||
|
||||
```
|
||||
BlockDriver bdrv_qcow2 = {
|
||||
.format_name = "qcow2",
|
||||
.instance_size = sizeof(BDRVQcow2State),
|
||||
.bdrv_probe = qcow2_probe,
|
||||
.bdrv_open = qcow2_open,
|
||||
.bdrv_close = qcow2_close,
|
||||
......
|
||||
.bdrv_snapshot_create = qcow2_snapshot_create,
|
||||
.bdrv_snapshot_goto = qcow2_snapshot_goto,
|
||||
.bdrv_snapshot_delete = qcow2_snapshot_delete,
|
||||
.bdrv_snapshot_list = qcow2_snapshot_list,
|
||||
.bdrv_snapshot_load_tmp = qcow2_snapshot_load_tmp,
|
||||
.bdrv_measure = qcow2_measure,
|
||||
.bdrv_get_info = qcow2_get_info,
|
||||
.bdrv_get_specific_info = qcow2_get_specific_info,
|
||||
|
||||
.bdrv_save_vmstate = qcow2_save_vmstate,
|
||||
.bdrv_load_vmstate = qcow2_load_vmstate,
|
||||
|
||||
.supports_backing = true,
|
||||
.bdrv_change_backing_file = qcow2_change_backing_file,
|
||||
|
||||
.bdrv_refresh_limits = qcow2_refresh_limits,
|
||||
......
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
根据上面的定义,对于qcow2来讲,bdrv_open调用的是qcow2_open。
|
||||
|
||||
```
|
||||
static int qcow2_open(BlockDriverState *bs, QDict *options, int flags,
|
||||
Error **errp)
|
||||
{
|
||||
BDRVQcow2State *s = bs->opaque;
|
||||
QCow2OpenCo qoc = {
|
||||
.bs = bs,
|
||||
.options = options,
|
||||
.flags = flags,
|
||||
.errp = errp,
|
||||
.ret = -EINPROGRESS
|
||||
};
|
||||
|
||||
bs->file = bdrv_open_child(NULL, options, "file", bs, &child_file,
|
||||
false, errp);
|
||||
qemu_coroutine_enter(qemu_coroutine_create(qcow2_open_entry, &qoc));
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在qcow2_open中,我们会通过qemu_coroutine_enter进入一个协程coroutine。什么叫协程呢?我们可以简单地将它理解为用户态自己实现的线程。
|
||||
|
||||
前面咱们讲线程的时候说过,如果一个程序想实现并发,可以创建多个线程,但是线程是一个内核的概念,创建的每一个线程内核都能看到,内核的调度也是以线程为单位的。这对于普通的进程没有什么问题,但是对于qemu这种虚拟机,如果在用户态和内核态切换来切换去,由于还涉及虚拟机的状态,代价比较大。
|
||||
|
||||
但是,qemu的设备也是需要多线程能力的,怎么办呢?我们就在用户态实现一个类似线程的东西,也就是协程,用于实现并发,并且不被内核看到,调度全部在用户态完成。
|
||||
|
||||
从后面的读写过程可以看出,协程在后端经常使用。这里打开一个qcow2文件就是使用一个协程,创建一个协程和创建一个线程很像,也需要指定一个函数来执行,qcow2_open_entry就是协程的函数。
|
||||
|
||||
```
|
||||
static void coroutine_fn qcow2_open_entry(void *opaque)
|
||||
{
|
||||
QCow2OpenCo *qoc = opaque;
|
||||
BDRVQcow2State *s = qoc->bs->opaque;
|
||||
|
||||
qemu_co_mutex_lock(&s->lock);
|
||||
qoc->ret = qcow2_do_open(qoc->bs, qoc->options, qoc->flags, qoc->errp);
|
||||
qemu_co_mutex_unlock(&s->lock);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们可以看到,qcow2_open_entry函数前面有一个coroutine_fn,说明它是一个协程函数。在qcow2_do_open中,qcow2_do_open根据qcow2的格式打开硬盘文件。这个格式[官网](https://github.com/qemu/qemu/blob/master/docs/interop/qcow2.txt)就有,我们这里就不花篇幅解析了。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
我们这里来总结一下,存储虚拟化的过程分为前端、后端和中间的队列。
|
||||
|
||||
- 前端有前端的块设备驱动Front-end driver,在客户机的内核里面,它符合普通设备驱动的格式,对外通过VFS暴露文件系统接口给客户机里面的应用。这一部分这一节我们没有讲,放在下一节解析。
|
||||
- 后端有后端的设备驱动Back-end driver,在宿主机的qemu进程中,当收到客户机的写入请求的时候,调用文件系统的write函数,写入宿主机的VFS文件系统,最终写到物理硬盘设备上的qcow2文件。
|
||||
- 中间的队列用于前端和后端之间传输数据,在前端的设备驱动和后端的设备驱动,都有类似的数据结构virt-queue来管理这些队列,这一部分这一节我们也没有讲,也放到下一节解析。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1f/4b/1f0c3043a11d6ea1a802f7d0f3b0b34b.jpg" alt="">
|
||||
|
||||
## 课堂练习
|
||||
|
||||
对于qemu-kvm来讲,qcow2是一种常见的文件格式。它有精妙的格式设计,从而适应虚拟化的场景,请你研究一下这个文件格式。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解,也欢迎收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
|
||||
@@ -0,0 +1,925 @@
|
||||
<audio id="audio" title="54 | 存储虚拟化(下):如何建立自己保管的单独档案库?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/96/9a/96ae957ba80acda102c44d661ea0be9a.mp3"></audio>
|
||||
|
||||
上一节,我们讲了qemu启动过程中的存储虚拟化。好了,现在qemu启动了,硬盘设备文件已经打开了。那如果我们要往虚拟机的一个进程写入一个文件,该怎么做呢?最终这个文件又是如何落到宿主机上的硬盘文件的呢?这一节,我们一起来看一看。
|
||||
|
||||
## 前端设备驱动virtio_blk
|
||||
|
||||
虚拟机里面的进程写入一个文件,当然要通过文件系统。整个过程和咱们在[文件系统](https://time.geekbang.org/column/article/97876)那一节讲的过程没有区别。只是到了设备驱动层,我们看到的就不是普通的硬盘驱动了,而是virtio的驱动。
|
||||
|
||||
virtio的驱动程序代码在Linux操作系统的源代码里面,文件名叫drivers/block/virtio_blk.c。
|
||||
|
||||
```
|
||||
static int __init init(void)
|
||||
{
|
||||
int error;
|
||||
virtblk_wq = alloc_workqueue("virtio-blk", 0, 0);
|
||||
major = register_blkdev(0, "virtblk");
|
||||
error = register_virtio_driver(&virtio_blk);
|
||||
......
|
||||
}
|
||||
|
||||
module_init(init);
|
||||
module_exit(fini);
|
||||
|
||||
MODULE_DEVICE_TABLE(virtio, id_table);
|
||||
MODULE_DESCRIPTION("Virtio block driver");
|
||||
MODULE_LICENSE("GPL");
|
||||
|
||||
static struct virtio_driver virtio_blk = {
|
||||
......
|
||||
.driver.name = KBUILD_MODNAME,
|
||||
.driver.owner = THIS_MODULE,
|
||||
.id_table = id_table,
|
||||
.probe = virtblk_probe,
|
||||
.remove = virtblk_remove,
|
||||
......
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
前面我们介绍过设备驱动程序,从这里的代码中,我们能看到非常熟悉的结构。它会创建一个workqueue,注册一个块设备,并获得一个主设备号,然后注册一个驱动函数virtio_blk。
|
||||
|
||||
当一个设备驱动作为一个内核模块被初始化的时候,probe函数会被调用,因而我们来看一下virtblk_probe。
|
||||
|
||||
```
|
||||
static int virtblk_probe(struct virtio_device *vdev)
|
||||
{
|
||||
struct virtio_blk *vblk;
|
||||
struct request_queue *q;
|
||||
......
|
||||
vdev->priv = vblk = kmalloc(sizeof(*vblk), GFP_KERNEL);
|
||||
vblk->vdev = vdev;
|
||||
vblk->sg_elems = sg_elems;
|
||||
INIT_WORK(&vblk->config_work, virtblk_config_changed_work);
|
||||
......
|
||||
err = init_vq(vblk);
|
||||
......
|
||||
vblk->disk = alloc_disk(1 << PART_BITS);
|
||||
memset(&vblk->tag_set, 0, sizeof(vblk->tag_set));
|
||||
vblk->tag_set.ops = &virtio_mq_ops;
|
||||
vblk->tag_set.queue_depth = virtblk_queue_depth;
|
||||
vblk->tag_set.numa_node = NUMA_NO_NODE;
|
||||
vblk->tag_set.flags = BLK_MQ_F_SHOULD_MERGE;
|
||||
vblk->tag_set.cmd_size =
|
||||
sizeof(struct virtblk_req) +
|
||||
sizeof(struct scatterlist) * sg_elems;
|
||||
vblk->tag_set.driver_data = vblk;
|
||||
vblk->tag_set.nr_hw_queues = vblk->num_vqs;
|
||||
err = blk_mq_alloc_tag_set(&vblk->tag_set);
|
||||
......
|
||||
q = blk_mq_init_queue(&vblk->tag_set);
|
||||
vblk->disk->queue = q;
|
||||
q->queuedata = vblk;
|
||||
virtblk_name_format("vd", index, vblk->disk->disk_name, DISK_NAME_LEN);
|
||||
vblk->disk->major = major;
|
||||
vblk->disk->first_minor = index_to_minor(index);
|
||||
vblk->disk->private_data = vblk;
|
||||
vblk->disk->fops = &virtblk_fops;
|
||||
vblk->disk->flags |= GENHD_FL_EXT_DEVT;
|
||||
vblk->index = index;
|
||||
......
|
||||
device_add_disk(&vdev->dev, vblk->disk);
|
||||
err = device_create_file(disk_to_dev(vblk->disk), &dev_attr_serial);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在virtblk_probe中,我们首先看到的是struct request_queue,这是每一个块设备都有的一个队列。还记得吗?它有两个函数,一个是make_request_fn函数,用于生成request;另一个是request_fn函数,用于处理request。
|
||||
|
||||
这个request_queue的初始化过程在blk_mq_init_queue中。它会调用blk_mq_init_allocated_queue->blk_queue_make_request。在这里面,我们可以将make_request_fn函数设置为blk_mq_make_request,也就是说,一旦上层有写入请求,我们就通过blk_mq_make_request这个函数,将请求放入request_queue队列中。
|
||||
|
||||
另外,在virtblk_probe中,我们会初始化一个gendisk。前面我们也讲了,每一个块设备都有这样一个结构。
|
||||
|
||||
在virtblk_probe中,还有一件重要的事情就是,init_vq会来初始化virtqueue。
|
||||
|
||||
```
|
||||
static int init_vq(struct virtio_blk *vblk)
|
||||
{
|
||||
int err;
|
||||
int i;
|
||||
vq_callback_t **callbacks;
|
||||
const char **names;
|
||||
struct virtqueue **vqs;
|
||||
unsigned short num_vqs;
|
||||
struct virtio_device *vdev = vblk->vdev;
|
||||
......
|
||||
vblk->vqs = kmalloc_array(num_vqs, sizeof(*vblk->vqs), GFP_KERNEL);
|
||||
names = kmalloc_array(num_vqs, sizeof(*names), GFP_KERNEL);
|
||||
callbacks = kmalloc_array(num_vqs, sizeof(*callbacks), GFP_KERNEL);
|
||||
vqs = kmalloc_array(num_vqs, sizeof(*vqs), GFP_KERNEL);
|
||||
......
|
||||
for (i = 0; i < num_vqs; i++) {
|
||||
callbacks[i] = virtblk_done;
|
||||
names[i] = vblk->vqs[i].name;
|
||||
}
|
||||
|
||||
/* Discover virtqueues and write information to configuration. */
|
||||
err = virtio_find_vqs(vdev, num_vqs, vqs, callbacks, names, &desc);
|
||||
|
||||
for (i = 0; i < num_vqs; i++) {
|
||||
vblk->vqs[i].vq = vqs[i];
|
||||
}
|
||||
vblk->num_vqs = num_vqs;
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
按照上面的原理来说,virtqueue是一个介于客户机前端和qemu后端的一个结构,用于在这两端之间传递数据。这里建立的struct virtqueue是客户机前端对于队列的管理的数据结构,在客户机的linux内核中通过kmalloc_array进行分配。
|
||||
|
||||
而队列的实体需要通过函数virtio_find_vqs查找或者生成,所以这里我们还把callback函数指定为virtblk_done。当buffer使用发生变化的时候,我们需要调用这个callback函数进行通知。
|
||||
|
||||
```
|
||||
static inline
|
||||
int virtio_find_vqs(struct virtio_device *vdev, unsigned nvqs,
|
||||
struct virtqueue *vqs[], vq_callback_t *callbacks[],
|
||||
const char * const names[],
|
||||
struct irq_affinity *desc)
|
||||
{
|
||||
return vdev->config->find_vqs(vdev, nvqs, vqs, callbacks, names, NULL, desc);
|
||||
}
|
||||
|
||||
static const struct virtio_config_ops virtio_pci_config_ops = {
|
||||
.get = vp_get,
|
||||
.set = vp_set,
|
||||
.generation = vp_generation,
|
||||
.get_status = vp_get_status,
|
||||
.set_status = vp_set_status,
|
||||
.reset = vp_reset,
|
||||
.find_vqs = vp_modern_find_vqs,
|
||||
.del_vqs = vp_del_vqs,
|
||||
.get_features = vp_get_features,
|
||||
.finalize_features = vp_finalize_features,
|
||||
.bus_name = vp_bus_name,
|
||||
.set_vq_affinity = vp_set_vq_affinity,
|
||||
.get_vq_affinity = vp_get_vq_affinity,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
根据virtio_config_ops的定义,virtio_find_vqs会调用vp_modern_find_vqs。
|
||||
|
||||
```
|
||||
static int vp_modern_find_vqs(struct virtio_device *vdev, unsigned nvqs,
|
||||
struct virtqueue *vqs[],
|
||||
vq_callback_t *callbacks[],
|
||||
const char * const names[], const bool *ctx,
|
||||
struct irq_affinity *desc)
|
||||
{
|
||||
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
|
||||
struct virtqueue *vq;
|
||||
int rc = vp_find_vqs(vdev, nvqs, vqs, callbacks, names, ctx, desc);
|
||||
/* Select and activate all queues. Has to be done last: once we do
|
||||
* this, there's no way to go back except reset.
|
||||
*/
|
||||
list_for_each_entry(vq, &vdev->vqs, list) {
|
||||
vp_iowrite16(vq->index, &vp_dev->common->queue_select);
|
||||
vp_iowrite16(1, &vp_dev->common->queue_enable);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在vp_modern_find_vqs中,vp_find_vqs会调用vp_find_vqs_intx。
|
||||
|
||||
```
|
||||
static int vp_find_vqs_intx(struct virtio_device *vdev, unsigned nvqs,
|
||||
struct virtqueue *vqs[], vq_callback_t *callbacks[],
|
||||
const char * const names[], const bool *ctx)
|
||||
{
|
||||
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
|
||||
int i, err;
|
||||
|
||||
vp_dev->vqs = kcalloc(nvqs, sizeof(*vp_dev->vqs), GFP_KERNEL);
|
||||
err = request_irq(vp_dev->pci_dev->irq, vp_interrupt, IRQF_SHARED,
|
||||
dev_name(&vdev->dev), vp_dev);
|
||||
vp_dev->intx_enabled = 1;
|
||||
vp_dev->per_vq_vectors = false;
|
||||
for (i = 0; i < nvqs; ++i) {
|
||||
vqs[i] = vp_setup_vq(vdev, i, callbacks[i], names[i],
|
||||
ctx ? ctx[i] : false,
|
||||
VIRTIO_MSI_NO_VECTOR);
|
||||
......
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在vp_find_vqs_intx中,我们通过request_irq注册一个中断处理函数vp_interrupt,当设备的配置信息发生改变,会产生一个中断,当设备向队列中写入信息时,也会产生一个中断,我们称为vq中断,中断处理函数需要调用相应的队列的回调函数。
|
||||
|
||||
然后,我们根据队列的数目,依次调用vp_setup_vq,完成virtqueue、vring的分配和初始化。
|
||||
|
||||
```
|
||||
static struct virtqueue *vp_setup_vq(struct virtio_device *vdev, unsigned index,
|
||||
void (*callback)(struct virtqueue *vq),
|
||||
const char *name,
|
||||
bool ctx,
|
||||
u16 msix_vec)
|
||||
{
|
||||
struct virtio_pci_device *vp_dev = to_vp_device(vdev);
|
||||
struct virtio_pci_vq_info *info = kmalloc(sizeof *info, GFP_KERNEL);
|
||||
struct virtqueue *vq;
|
||||
unsigned long flags;
|
||||
......
|
||||
vq = vp_dev->setup_vq(vp_dev, info, index, callback, name, ctx,
|
||||
msix_vec);
|
||||
info->vq = vq;
|
||||
if (callback) {
|
||||
spin_lock_irqsave(&vp_dev->lock, flags);
|
||||
list_add(&info->node, &vp_dev->virtqueues);
|
||||
spin_unlock_irqrestore(&vp_dev->lock, flags);
|
||||
} else {
|
||||
INIT_LIST_HEAD(&info->node);
|
||||
}
|
||||
vp_dev->vqs[index] = info;
|
||||
return vq;
|
||||
}
|
||||
|
||||
static struct virtqueue *setup_vq(struct virtio_pci_device *vp_dev,
|
||||
struct virtio_pci_vq_info *info,
|
||||
unsigned index,
|
||||
void (*callback)(struct virtqueue *vq),
|
||||
const char *name,
|
||||
bool ctx,
|
||||
u16 msix_vec)
|
||||
{
|
||||
struct virtio_pci_common_cfg __iomem *cfg = vp_dev->common;
|
||||
struct virtqueue *vq;
|
||||
u16 num, off;
|
||||
int err;
|
||||
|
||||
/* Select the queue we're interested in */
|
||||
vp_iowrite16(index, &cfg->queue_select);
|
||||
|
||||
/* Check if queue is either not available or already active. */
|
||||
num = vp_ioread16(&cfg->queue_size);
|
||||
|
||||
/* get offset of notification word for this vq */
|
||||
off = vp_ioread16(&cfg->queue_notify_off);
|
||||
|
||||
info->msix_vector = msix_vec;
|
||||
|
||||
/* create the vring */
|
||||
vq = vring_create_virtqueue(index, num,
|
||||
SMP_CACHE_BYTES, &vp_dev->vdev,
|
||||
true, true, ctx,
|
||||
vp_notify, callback, name);
|
||||
/* activate the queue */
|
||||
vp_iowrite16(virtqueue_get_vring_size(vq), &cfg->queue_size);
|
||||
vp_iowrite64_twopart(virtqueue_get_desc_addr(vq),
|
||||
&cfg->queue_desc_lo, &cfg->queue_desc_hi);
|
||||
vp_iowrite64_twopart(virtqueue_get_avail_addr(vq),
|
||||
&cfg->queue_avail_lo, &cfg->queue_avail_hi);
|
||||
vp_iowrite64_twopart(virtqueue_get_used_addr(vq),
|
||||
&cfg->queue_used_lo, &cfg->queue_used_hi);
|
||||
......
|
||||
return vq;
|
||||
}
|
||||
|
||||
struct virtqueue *vring_create_virtqueue(
|
||||
unsigned int index,
|
||||
unsigned int num,
|
||||
unsigned int vring_align,
|
||||
struct virtio_device *vdev,
|
||||
bool weak_barriers,
|
||||
bool may_reduce_num,
|
||||
bool context,
|
||||
bool (*notify)(struct virtqueue *),
|
||||
void (*callback)(struct virtqueue *),
|
||||
const char *name)
|
||||
{
|
||||
struct virtqueue *vq;
|
||||
void *queue = NULL;
|
||||
dma_addr_t dma_addr;
|
||||
size_t queue_size_in_bytes;
|
||||
struct vring vring;
|
||||
|
||||
/* TODO: allocate each queue chunk individually */
|
||||
for (; num && vring_size(num, vring_align) > PAGE_SIZE; num /= 2) {
|
||||
queue = vring_alloc_queue(vdev, vring_size(num, vring_align),
|
||||
&dma_addr,
|
||||
GFP_KERNEL|__GFP_NOWARN|__GFP_ZERO);
|
||||
if (queue)
|
||||
break;
|
||||
}
|
||||
|
||||
if (!queue) {
|
||||
/* Try to get a single page. You are my only hope! */
|
||||
queue = vring_alloc_queue(vdev, vring_size(num, vring_align),
|
||||
&dma_addr, GFP_KERNEL|__GFP_ZERO);
|
||||
}
|
||||
|
||||
queue_size_in_bytes = vring_size(num, vring_align);
|
||||
vring_init(&vring, num, queue, vring_align);
|
||||
|
||||
vq = __vring_new_virtqueue(index, vring, vdev, weak_barriers, context, notify, callback, name);
|
||||
|
||||
to_vvq(vq)->queue_dma_addr = dma_addr;
|
||||
to_vvq(vq)->queue_size_in_bytes = queue_size_in_bytes;
|
||||
to_vvq(vq)->we_own_ring = true;
|
||||
|
||||
return vq;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在vring_create_virtqueue中,我们会调用vring_alloc_queue,来创建队列所需要的内存空间,然后调用vring_init初始化结构struct vring,来管理队列的内存空间,调用__vring_new_virtqueue,来创建struct vring_virtqueue。
|
||||
|
||||
这个结构的一开始,是struct virtqueue,它也是struct virtqueue的一个扩展,紧接着后面就是struct vring。
|
||||
|
||||
```
|
||||
struct vring_virtqueue {
|
||||
struct virtqueue vq;
|
||||
|
||||
/* Actual memory layout for this queue */
|
||||
struct vring vring;
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
至此我们发现,虚拟机里面的virtio的前端是这样的结构:struct virtio_device里面有一个struct vring_virtqueue,在struct vring_virtqueue里面有一个struct vring。
|
||||
|
||||
## 中间virtio队列的管理
|
||||
|
||||
还记不记得我们上面讲qemu初始化的时候,virtio的后端有数据结构VirtIODevice,VirtQueue和vring一模一样,前端和后端对应起来,都应该指向刚才创建的那一段内存。
|
||||
|
||||
现在的问题是,我们刚才分配的内存在客户机的内核里面,如何告知qemu来访问这段内存呢?
|
||||
|
||||
别忘了,qemu模拟出来的virtio block device只是一个PCI设备。对于客户机来讲,这是一个外部设备,我们可以通过给外部设备发送指令的方式告知外部设备,这就是代码中vp_iowrite16的作用。它会调用专门给外部设备发送指令的函数iowrite,告诉外部的PCI设备。
|
||||
|
||||
告知的有三个地址virtqueue_get_desc_addr、virtqueue_get_avail_addr,virtqueue_get_used_addr。从客户机角度来看,这里面的地址都是物理地址,也即GPA(Guest Physical Address)。因为只有物理地址才是客户机和qemu程序都认可的地址,本来客户机的物理内存也是qemu模拟出来的。
|
||||
|
||||
在qemu中,对PCI总线添加一个设备的时候,我们会调用virtio_pci_device_plugged。
|
||||
|
||||
```
|
||||
static void virtio_pci_device_plugged(DeviceState *d, Error **errp)
|
||||
{
|
||||
VirtIOPCIProxy *proxy = VIRTIO_PCI(d);
|
||||
......
|
||||
memory_region_init_io(&proxy->bar, OBJECT(proxy),
|
||||
&virtio_pci_config_ops,
|
||||
proxy, "virtio-pci", size);
|
||||
......
|
||||
}
|
||||
|
||||
static const MemoryRegionOps virtio_pci_config_ops = {
|
||||
.read = virtio_pci_config_read,
|
||||
.write = virtio_pci_config_write,
|
||||
.impl = {
|
||||
.min_access_size = 1,
|
||||
.max_access_size = 4,
|
||||
},
|
||||
.endianness = DEVICE_LITTLE_ENDIAN,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
在这里面,对于这个加载的设备进行I/O操作,会映射到读写某一块内存空间,对应的操作为virtio_pci_config_ops,也即写入这块内存空间,这就相当于对于这个PCI设备进行某种配置。
|
||||
|
||||
对PCI设备进行配置的时候,会有这样的调用链:virtio_pci_config_write->virtio_ioport_write->virtio_queue_set_addr。设置virtio的queue的地址是一项很重要的操作。
|
||||
|
||||
```
|
||||
void virtio_queue_set_addr(VirtIODevice *vdev, int n, hwaddr addr)
|
||||
{
|
||||
vdev->vq[n].vring.desc = addr;
|
||||
virtio_queue_update_rings(vdev, n);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
从这里我们可以看出,qemu后端的VirtIODevice的VirtQueue的vring的地址,被设置成了刚才给队列分配的内存的GPA。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/25/d0/2572f8b1e75b9eaab6560866fcb31fd0.jpg" alt="">
|
||||
|
||||
接着,我们来看一下这个队列的格式。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/49/db/49414d5acc81933b66410bbba102b0db.jpg" alt="">
|
||||
|
||||
```
|
||||
/* Virtio ring descriptors: 16 bytes. These can chain together via "next". */
|
||||
struct vring_desc {
|
||||
/* Address (guest-physical). */
|
||||
__virtio64 addr;
|
||||
/* Length. */
|
||||
__virtio32 len;
|
||||
/* The flags as indicated above. */
|
||||
__virtio16 flags;
|
||||
/* We chain unused descriptors via this, too */
|
||||
__virtio16 next;
|
||||
};
|
||||
|
||||
struct vring_avail {
|
||||
__virtio16 flags;
|
||||
__virtio16 idx;
|
||||
__virtio16 ring[];
|
||||
};
|
||||
|
||||
/* u32 is used here for ids for padding reasons. */
|
||||
struct vring_used_elem {
|
||||
/* Index of start of used descriptor chain. */
|
||||
__virtio32 id;
|
||||
/* Total length of the descriptor chain which was used (written to) */
|
||||
__virtio32 len;
|
||||
};
|
||||
|
||||
struct vring_used {
|
||||
__virtio16 flags;
|
||||
__virtio16 idx;
|
||||
struct vring_used_elem ring[];
|
||||
};
|
||||
|
||||
struct vring {
|
||||
unsigned int num;
|
||||
|
||||
struct vring_desc *desc;
|
||||
|
||||
struct vring_avail *avail;
|
||||
|
||||
struct vring_used *used;
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
vring包含三个成员:
|
||||
|
||||
- vring_desc指向分配的内存块,用于存放客户机和qemu之间传输的数据。
|
||||
- avail->ring[]是发送端维护的环形队列,指向需要接收端处理的vring_desc。
|
||||
- used->ring[]是接收端维护的环形队列,指向自己已经处理过了的vring_desc。
|
||||
|
||||
## 数据写入的流程
|
||||
|
||||
接下来,我们来看,真的写入一个数据的时候,会发生什么。
|
||||
|
||||
按照上面virtio驱动初始化的时候的逻辑,blk_mq_make_request会被调用。这个函数比较复杂,会分成多个分支,但是最终都会调用到request_queue的virtio_mq_ops的queue_rq函数。
|
||||
|
||||
```
|
||||
struct request_queue *q = rq->q;
|
||||
q->mq_ops->queue_rq(hctx, &bd);
|
||||
|
||||
static const struct blk_mq_ops virtio_mq_ops = {
|
||||
.queue_rq = virtio_queue_rq,
|
||||
.complete = virtblk_request_done,
|
||||
.init_request = virtblk_init_request,
|
||||
.map_queues = virtblk_map_queues,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
根据virtio_mq_ops的定义,我们现在要调用virtio_queue_rq。
|
||||
|
||||
```
|
||||
static blk_status_t virtio_queue_rq(struct blk_mq_hw_ctx *hctx,
|
||||
const struct blk_mq_queue_data *bd)
|
||||
{
|
||||
struct virtio_blk *vblk = hctx->queue->queuedata;
|
||||
struct request *req = bd->rq;
|
||||
struct virtblk_req *vbr = blk_mq_rq_to_pdu(req);
|
||||
......
|
||||
err = virtblk_add_req(vblk->vqs[qid].vq, vbr, vbr->sg, num);
|
||||
......
|
||||
if (notify)
|
||||
virtqueue_notify(vblk->vqs[qid].vq);
|
||||
return BLK_STS_OK;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在virtio_queue_rq中,我们会将请求写入的数据,通过virtblk_add_req放入struct virtqueue。
|
||||
|
||||
因此,接下来的调用链为:virtblk_add_req->virtqueue_add_sgs->virtqueue_add。
|
||||
|
||||
```
|
||||
static inline int virtqueue_add(struct virtqueue *_vq,
|
||||
struct scatterlist *sgs[],
|
||||
unsigned int total_sg,
|
||||
unsigned int out_sgs,
|
||||
unsigned int in_sgs,
|
||||
void *data,
|
||||
void *ctx,
|
||||
gfp_t gfp)
|
||||
{
|
||||
struct vring_virtqueue *vq = to_vvq(_vq);
|
||||
struct scatterlist *sg;
|
||||
struct vring_desc *desc;
|
||||
unsigned int i, n, avail, descs_used, uninitialized_var(prev), err_idx;
|
||||
int head;
|
||||
bool indirect;
|
||||
......
|
||||
head = vq->free_head;
|
||||
|
||||
indirect = false;
|
||||
desc = vq->vring.desc;
|
||||
i = head;
|
||||
descs_used = total_sg;
|
||||
|
||||
for (n = 0; n < out_sgs; n++) {
|
||||
for (sg = sgs[n]; sg; sg = sg_next(sg)) {
|
||||
dma_addr_t addr = vring_map_one_sg(vq, sg, DMA_TO_DEVICE);
|
||||
......
|
||||
desc[i].flags = cpu_to_virtio16(_vq->vdev, VRING_DESC_F_NEXT);
|
||||
desc[i].addr = cpu_to_virtio64(_vq->vdev, addr);
|
||||
desc[i].len = cpu_to_virtio32(_vq->vdev, sg->length);
|
||||
prev = i;
|
||||
i = virtio16_to_cpu(_vq->vdev, desc[i].next);
|
||||
}
|
||||
}
|
||||
|
||||
/* Last one doesn't continue. */
|
||||
desc[prev].flags &= cpu_to_virtio16(_vq->vdev, ~VRING_DESC_F_NEXT);
|
||||
|
||||
/* We're using some buffers from the free list. */
|
||||
vq->vq.num_free -= descs_used;
|
||||
|
||||
/* Update free pointer */
|
||||
vq->free_head = i;
|
||||
|
||||
/* Store token and indirect buffer state. */
|
||||
vq->desc_state[head].data = data;
|
||||
|
||||
/* Put entry in available array (but don't update avail->idx until they do sync). */
|
||||
avail = vq->avail_idx_shadow & (vq->vring.num - 1);
|
||||
vq->vring.avail->ring[avail] = cpu_to_virtio16(_vq->vdev, head);
|
||||
|
||||
/* Descriptors and available array need to be set before we expose the new available array entries. */
|
||||
virtio_wmb(vq->weak_barriers);
|
||||
vq->avail_idx_shadow++;
|
||||
vq->vring.avail->idx = cpu_to_virtio16(_vq->vdev, vq->avail_idx_shadow);
|
||||
vq->num_added++;
|
||||
......
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在virtqueue_add函数中,我们能看到,free_head指向的整个内存块空闲链表的起始位置,用head变量记住这个起始位置。
|
||||
|
||||
接下来,i也指向这个起始位置,然后是一个for循环,将数据放到内存块里面,放的过程中,next不断指向下一个空闲位置,这样空闲的内存块被不断的占用。等所有的写入都结束了,i就会指向这次存放的内存块的下一个空闲位置,然后free_head就指向i,因为前面的都填满了。
|
||||
|
||||
至此,从head到i之间的内存块,就是这次写入的全部数据。
|
||||
|
||||
于是,在vring的avail变量中,在ring[]数组中分配新的一项,在avail的位置,avail的计算是avail_idx_shadow & (vq->vring.num - 1),其中,avail_idx_shadow是上一次的avail的位置。这里如果超过了ring[]数组的下标,则重新跳到起始位置,就说明是一个环。这次分配的新的avail的位置就存放新写入的从head到i之间的内存块。然后是avail_idx_shadow++,这说明这一块内存可以被接收方读取了。
|
||||
|
||||
接下来,我们回到virtio_queue_rq,调用virtqueue_notify通知接收方。而virtqueue_notify会调用vp_notify。
|
||||
|
||||
```
|
||||
bool vp_notify(struct virtqueue *vq)
|
||||
{
|
||||
/* we write the queue's selector into the notification register to
|
||||
* signal the other end */
|
||||
iowrite16(vq->index, (void __iomem *)vq->priv);
|
||||
return true;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
然后,我们写入一个I/O会触发VM exit。我们在解析CPU的时候看到过这个逻辑。
|
||||
|
||||
```
|
||||
int kvm_cpu_exec(CPUState *cpu)
|
||||
{
|
||||
struct kvm_run *run = cpu->kvm_run;
|
||||
int ret, run_ret;
|
||||
......
|
||||
run_ret = kvm_vcpu_ioctl(cpu, KVM_RUN, 0);
|
||||
......
|
||||
switch (run->exit_reason) {
|
||||
case KVM_EXIT_IO:
|
||||
DPRINTF("handle_io\n");
|
||||
/* Called outside BQL */
|
||||
kvm_handle_io(run->io.port, attrs,
|
||||
(uint8_t *)run + run->io.data_offset,
|
||||
run->io.direction,
|
||||
run->io.size,
|
||||
run->io.count);
|
||||
ret = 0;
|
||||
break;
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这次写入的也是一个I/O的内存空间,同样会触发virtio_ioport_write,这次会调用virtio_queue_notify。
|
||||
|
||||
```
|
||||
void virtio_queue_notify(VirtIODevice *vdev, int n)
|
||||
{
|
||||
VirtQueue *vq = &vdev->vq[n];
|
||||
......
|
||||
if (vq->handle_aio_output) {
|
||||
event_notifier_set(&vq->host_notifier);
|
||||
} else if (vq->handle_output) {
|
||||
vq->handle_output(vdev, vq);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
virtio_queue_notify会调用VirtQueue的handle_output函数,前面我们已经设置过这个函数了,是virtio_blk_handle_output。
|
||||
|
||||
接下来的调用链为:virtio_blk_handle_output->virtio_blk_handle_output_do->virtio_blk_handle_vq。
|
||||
|
||||
```
|
||||
bool virtio_blk_handle_vq(VirtIOBlock *s, VirtQueue *vq)
|
||||
{
|
||||
VirtIOBlockReq *req;
|
||||
MultiReqBuffer mrb = {};
|
||||
bool progress = false;
|
||||
......
|
||||
do {
|
||||
virtio_queue_set_notification(vq, 0);
|
||||
|
||||
while ((req = virtio_blk_get_request(s, vq))) {
|
||||
progress = true;
|
||||
if (virtio_blk_handle_request(req, &mrb)) {
|
||||
virtqueue_detach_element(req->vq, &req->elem, 0);
|
||||
virtio_blk_free_request(req);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
virtio_queue_set_notification(vq, 1);
|
||||
} while (!virtio_queue_empty(vq));
|
||||
|
||||
if (mrb.num_reqs) {
|
||||
virtio_blk_submit_multireq(s->blk, &mrb);
|
||||
}
|
||||
......
|
||||
return progress;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在virtio_blk_handle_vq中,有一个while循环,在循环中调用函数virtio_blk_get_request从vq中取出请求,然后调用virtio_blk_handle_request处理从vq中取出的请求。
|
||||
|
||||
我们先来看virtio_blk_get_request。
|
||||
|
||||
```
|
||||
static VirtIOBlockReq *virtio_blk_get_request(VirtIOBlock *s, VirtQueue *vq)
|
||||
{
|
||||
VirtIOBlockReq *req = virtqueue_pop(vq, sizeof(VirtIOBlockReq));
|
||||
|
||||
if (req) {
|
||||
virtio_blk_init_request(s, vq, req);
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
void *virtqueue_pop(VirtQueue *vq, size_t sz)
|
||||
{
|
||||
unsigned int i, head, max;
|
||||
VRingMemoryRegionCaches *caches;
|
||||
MemoryRegionCache *desc_cache;
|
||||
int64_t len;
|
||||
VirtIODevice *vdev = vq->vdev;
|
||||
VirtQueueElement *elem = NULL;
|
||||
unsigned out_num, in_num, elem_entries;
|
||||
hwaddr addr[VIRTQUEUE_MAX_SIZE];
|
||||
struct iovec iov[VIRTQUEUE_MAX_SIZE];
|
||||
VRingDesc desc;
|
||||
int rc;
|
||||
......
|
||||
/* When we start there are none of either input nor output. */
|
||||
out_num = in_num = elem_entries = 0;
|
||||
|
||||
max = vq->vring.num;
|
||||
|
||||
i = head;
|
||||
|
||||
caches = vring_get_region_caches(vq);
|
||||
desc_cache = &caches->desc;
|
||||
vring_desc_read(vdev, &desc, desc_cache, i);
|
||||
......
|
||||
/* Collect all the descriptors */
|
||||
do {
|
||||
bool map_ok;
|
||||
|
||||
if (desc.flags & VRING_DESC_F_WRITE) {
|
||||
map_ok = virtqueue_map_desc(vdev, &in_num, addr + out_num,
|
||||
iov + out_num,
|
||||
VIRTQUEUE_MAX_SIZE - out_num, true,
|
||||
desc.addr, desc.len);
|
||||
} else {
|
||||
map_ok = virtqueue_map_desc(vdev, &out_num, addr, iov,
|
||||
VIRTQUEUE_MAX_SIZE, false,
|
||||
desc.addr, desc.len);
|
||||
}
|
||||
......
|
||||
rc = virtqueue_read_next_desc(vdev, &desc, desc_cache, max, &i);
|
||||
} while (rc == VIRTQUEUE_READ_DESC_MORE);
|
||||
......
|
||||
/* Now copy what we have collected and mapped */
|
||||
elem = virtqueue_alloc_element(sz, out_num, in_num);
|
||||
elem->index = head;
|
||||
for (i = 0; i < out_num; i++) {
|
||||
elem->out_addr[i] = addr[i];
|
||||
elem->out_sg[i] = iov[i];
|
||||
}
|
||||
for (i = 0; i < in_num; i++) {
|
||||
elem->in_addr[i] = addr[out_num + i];
|
||||
elem->in_sg[i] = iov[out_num + i];
|
||||
}
|
||||
|
||||
vq->inuse++;
|
||||
......
|
||||
return elem;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们可以看到,virtio_blk_get_request会调用virtqueue_pop。在这里面,我们能看到对于vring的操作,也即从这里面将客户机里面写入的数据读取出来,放到VirtIOBlockReq结构中。
|
||||
|
||||
接下来,我们就要调用virtio_blk_handle_request处理这些数据。所以接下来的调用链为:virtio_blk_handle_request->virtio_blk_submit_multireq->submit_requests。
|
||||
|
||||
```
|
||||
static inline void submit_requests(BlockBackend *blk, MultiReqBuffer *mrb,int start, int num_reqs, int niov)
|
||||
{
|
||||
QEMUIOVector *qiov = &mrb->reqs[start]->qiov;
|
||||
int64_t sector_num = mrb->reqs[start]->sector_num;
|
||||
bool is_write = mrb->is_write;
|
||||
|
||||
if (num_reqs > 1) {
|
||||
int i;
|
||||
struct iovec *tmp_iov = qiov->iov;
|
||||
int tmp_niov = qiov->niov;
|
||||
qemu_iovec_init(qiov, niov);
|
||||
|
||||
for (i = 0; i < tmp_niov; i++) {
|
||||
qemu_iovec_add(qiov, tmp_iov[i].iov_base, tmp_iov[i].iov_len);
|
||||
}
|
||||
|
||||
for (i = start + 1; i < start + num_reqs; i++) {
|
||||
qemu_iovec_concat(qiov, &mrb->reqs[i]->qiov, 0,
|
||||
mrb->reqs[i]->qiov.size);
|
||||
mrb->reqs[i - 1]->mr_next = mrb->reqs[i];
|
||||
}
|
||||
|
||||
block_acct_merge_done(blk_get_stats(blk),
|
||||
is_write ? BLOCK_ACCT_WRITE : BLOCK_ACCT_READ,
|
||||
num_reqs - 1);
|
||||
}
|
||||
|
||||
if (is_write) {
|
||||
blk_aio_pwritev(blk, sector_num << BDRV_SECTOR_BITS, qiov, 0,
|
||||
virtio_blk_rw_complete, mrb->reqs[start]);
|
||||
} else {
|
||||
blk_aio_preadv(blk, sector_num << BDRV_SECTOR_BITS, qiov, 0,
|
||||
virtio_blk_rw_complete, mrb->reqs[start]);
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在submit_requests中,我们看到了BlockBackend。这是在qemu启动的时候,打开qcow2文件的时候生成的,现在我们可以用它来写入文件了,调用的是blk_aio_pwritev。
|
||||
|
||||
```
|
||||
BlockAIOCB *blk_aio_pwritev(BlockBackend *blk, int64_t offset,
|
||||
QEMUIOVector *qiov, BdrvRequestFlags flags,
|
||||
BlockCompletionFunc *cb, void *opaque)
|
||||
{
|
||||
return blk_aio_prwv(blk, offset, qiov->size, qiov,
|
||||
blk_aio_write_entry, flags, cb, opaque);
|
||||
}
|
||||
|
||||
static BlockAIOCB *blk_aio_prwv(BlockBackend *blk, int64_t offset, int bytes,
|
||||
void *iobuf, CoroutineEntry co_entry,
|
||||
BdrvRequestFlags flags,
|
||||
BlockCompletionFunc *cb, void *opaque)
|
||||
{
|
||||
BlkAioEmAIOCB *acb;
|
||||
Coroutine *co;
|
||||
acb = blk_aio_get(&blk_aio_em_aiocb_info, blk, cb, opaque);
|
||||
acb->rwco = (BlkRwCo) {
|
||||
.blk = blk,
|
||||
.offset = offset,
|
||||
.iobuf = iobuf,
|
||||
.flags = flags,
|
||||
.ret = NOT_DONE,
|
||||
};
|
||||
acb->bytes = bytes;
|
||||
acb->has_returned = false;
|
||||
|
||||
co = qemu_coroutine_create(co_entry, acb);
|
||||
bdrv_coroutine_enter(blk_bs(blk), co);
|
||||
|
||||
acb->has_returned = true;
|
||||
return &acb->common;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在blk_aio_pwritev中,我们看到,又是创建了一个协程来进行写入。写入完毕之后调用virtio_blk_rw_complete->virtio_blk_req_complete。
|
||||
|
||||
```
|
||||
static void virtio_blk_req_complete(VirtIOBlockReq *req, unsigned char status)
|
||||
{
|
||||
VirtIOBlock *s = req->dev;
|
||||
VirtIODevice *vdev = VIRTIO_DEVICE(s);
|
||||
|
||||
trace_virtio_blk_req_complete(vdev, req, status);
|
||||
|
||||
stb_p(&req->in->status, status);
|
||||
virtqueue_push(req->vq, &req->elem, req->in_len);
|
||||
virtio_notify(vdev, req->vq);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在virtio_blk_req_complete中,我们先是调用virtqueue_push,更新vring中used变量,表示这部分已经写入完毕,空间可以回收利用了。但是,这部分的改变仅仅改变了qemu后端的vring,我们还需要通知客户机中virtio前端的vring的值,因而要调用virtio_notify。virtio_notify会调用virtio_irq发送一个中断。
|
||||
|
||||
还记得咱们前面注册过一个中断处理函数vp_interrupt吗?它就是干这个事情的。
|
||||
|
||||
```
|
||||
static irqreturn_t vp_interrupt(int irq, void *opaque)
|
||||
{
|
||||
struct virtio_pci_device *vp_dev = opaque;
|
||||
u8 isr;
|
||||
|
||||
/* reading the ISR has the effect of also clearing it so it's very
|
||||
* important to save off the value. */
|
||||
isr = ioread8(vp_dev->isr);
|
||||
|
||||
/* Configuration change? Tell driver if it wants to know. */
|
||||
if (isr & VIRTIO_PCI_ISR_CONFIG)
|
||||
vp_config_changed(irq, opaque);
|
||||
|
||||
return vp_vring_interrupt(irq, opaque);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
就像前面说的一样vp_interrupt这个中断处理函数,一是处理配置变化,二是处理I/O结束。第二种的调用链为:vp_interrupt->vp_vring_interrupt->vring_interrupt。
|
||||
|
||||
```
|
||||
irqreturn_t vring_interrupt(int irq, void *_vq)
|
||||
{
|
||||
struct vring_virtqueue *vq = to_vvq(_vq);
|
||||
......
|
||||
if (vq->vq.callback)
|
||||
vq->vq.callback(&vq->vq);
|
||||
|
||||
return IRQ_HANDLED;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在vring_interrupt中,我们会调用callback函数,这个也是在前面注册过的,是virtblk_done。
|
||||
|
||||
接下来的调用链为:virtblk_done->virtqueue_get_buf->virtqueue_get_buf_ctx。
|
||||
|
||||
```
|
||||
void *virtqueue_get_buf_ctx(struct virtqueue *_vq, unsigned int *len,
|
||||
void **ctx)
|
||||
{
|
||||
struct vring_virtqueue *vq = to_vvq(_vq);
|
||||
void *ret;
|
||||
unsigned int i;
|
||||
u16 last_used;
|
||||
......
|
||||
last_used = (vq->last_used_idx & (vq->vring.num - 1));
|
||||
i = virtio32_to_cpu(_vq->vdev, vq->vring.used->ring[last_used].id);
|
||||
*len = virtio32_to_cpu(_vq->vdev, vq->vring.used->ring[last_used].len);
|
||||
......
|
||||
/* detach_buf clears data, so grab it now. */
|
||||
ret = vq->desc_state[i].data;
|
||||
detach_buf(vq, i, ctx);
|
||||
vq->last_used_idx++;
|
||||
......
|
||||
return ret;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在virtqueue_get_buf_ctx中,我们可以看到,virtio前端的vring中的last_used_idx加一,说明这块数据qemu后端已经消费完毕。我们可以通过detach_buf将其放入空闲队列中,留给以后的写入请求使用。
|
||||
|
||||
至此,整个存储虚拟化的写入流程才全部完成。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
下面我们来总结一下存储虚拟化的场景下,整个写入的过程。
|
||||
|
||||
- 在虚拟机里面,应用层调用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会收到这个通知。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/79/0c/79ad143a3149ea36bc80219940d7d00c.jpg" alt="">
|
||||
|
||||
## 课堂练习
|
||||
|
||||
请你沿着代码,仔细分析并牢记virtqueue的结构以及写入和读取方式。这个结构在下面的网络传输过程中,还要起大作用。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解,也欢迎收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
|
||||
927
极客时间专栏/趣谈Linux操作系统/核心原理篇:第九部分 虚拟化/55 | 网络虚拟化:如何成立独立的合作部?.md
Normal file
927
极客时间专栏/趣谈Linux操作系统/核心原理篇:第九部分 虚拟化/55 | 网络虚拟化:如何成立独立的合作部?.md
Normal file
@@ -0,0 +1,927 @@
|
||||
<audio id="audio" title="55 | 网络虚拟化:如何成立独立的合作部?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/17/dc/17dd6491d3f574bceea1b2d7ce0029dc.mp3"></audio>
|
||||
|
||||
上一节,我们讲了存储虚拟化,这一节我们来讲网络虚拟化。
|
||||
|
||||
网络虚拟化有和存储虚拟化类似的地方,例如,它们都是基于virtio的,因而我们在看网络虚拟化的过程中,会看到和存储虚拟化很像的数据结构和原理。但是,网络虚拟化也有自己的特殊性。例如,存储虚拟化是将宿主机上的文件作为客户机上的硬盘,而网络虚拟化需要依赖于内核协议栈进行网络包的封装与解封装。那怎么实现客户机和宿主机之间的互通呢?我们就一起来看一看。
|
||||
|
||||
## 解析初始化过程
|
||||
|
||||
我们还是从Virtio Network Device这个设备的初始化讲起。
|
||||
|
||||
```
|
||||
static const TypeInfo device_type_info = {
|
||||
.name = TYPE_DEVICE,
|
||||
.parent = TYPE_OBJECT,
|
||||
.instance_size = sizeof(DeviceState),
|
||||
.instance_init = device_initfn,
|
||||
.instance_post_init = device_post_init,
|
||||
.instance_finalize = device_finalize,
|
||||
.class_base_init = device_class_base_init,
|
||||
.class_init = device_class_init,
|
||||
.abstract = true,
|
||||
.class_size = sizeof(DeviceClass),
|
||||
};
|
||||
|
||||
static const TypeInfo virtio_device_info = {
|
||||
.name = TYPE_VIRTIO_DEVICE,
|
||||
.parent = TYPE_DEVICE,
|
||||
.instance_size = sizeof(VirtIODevice),
|
||||
.class_init = virtio_device_class_init,
|
||||
.instance_finalize = virtio_device_instance_finalize,
|
||||
.abstract = true,
|
||||
.class_size = sizeof(VirtioDeviceClass),
|
||||
};
|
||||
|
||||
static const TypeInfo virtio_net_info = {
|
||||
.name = TYPE_VIRTIO_NET,
|
||||
.parent = TYPE_VIRTIO_DEVICE,
|
||||
.instance_size = sizeof(VirtIONet),
|
||||
.instance_init = virtio_net_instance_init,
|
||||
.class_init = virtio_net_class_init,
|
||||
};
|
||||
|
||||
static void virtio_register_types(void)
|
||||
{
|
||||
type_register_static(&virtio_net_info);
|
||||
}
|
||||
|
||||
type_init(virtio_register_types)
|
||||
|
||||
```
|
||||
|
||||
Virtio Network Device这种类的定义是有多层继承关系的,TYPE_VIRTIO_NET的父类是TYPE_VIRTIO_DEVICE,TYPE_VIRTIO_DEVICE的父类是TYPE_DEVICE,TYPE_DEVICE的父类是TYPE_OBJECT,继承关系到头了。
|
||||
|
||||
type_init用于注册这种类。这里面每一层都有class_init,用于从TypeImpl生成xxxClass,也有instance_init,会将xxxClass初始化为实例。
|
||||
|
||||
TYPE_VIRTIO_NET层的class_init函数virtio_net_class_init,定义了DeviceClass的realize函数为virtio_net_device_realize,这一点和存储块设备是一样的。
|
||||
|
||||
```
|
||||
static void virtio_net_device_realize(DeviceState *dev, Error **errp)
|
||||
{
|
||||
VirtIODevice *vdev = VIRTIO_DEVICE(dev);
|
||||
VirtIONet *n = VIRTIO_NET(dev);
|
||||
NetClientState *nc;
|
||||
int i;
|
||||
......
|
||||
virtio_init(vdev, "virtio-net", VIRTIO_ID_NET, n->config_size);
|
||||
|
||||
/*
|
||||
* We set a lower limit on RX queue size to what it always was.
|
||||
* Guests that want a smaller ring can always resize it without
|
||||
* help from us (using virtio 1 and up).
|
||||
*/
|
||||
if (n->net_conf.rx_queue_size < VIRTIO_NET_RX_QUEUE_MIN_SIZE ||
|
||||
n->net_conf.rx_queue_size > VIRTQUEUE_MAX_SIZE ||
|
||||
!is_power_of_2(n->net_conf.rx_queue_size)) {
|
||||
......
|
||||
return;
|
||||
}
|
||||
|
||||
if (n->net_conf.tx_queue_size < VIRTIO_NET_TX_QUEUE_MIN_SIZE ||
|
||||
n->net_conf.tx_queue_size > VIRTQUEUE_MAX_SIZE ||
|
||||
!is_power_of_2(n->net_conf.tx_queue_size)) {
|
||||
......
|
||||
return;
|
||||
}
|
||||
|
||||
n->max_queues = MAX(n->nic_conf.peers.queues, 1);
|
||||
if (n->max_queues * 2 + 1 > VIRTIO_QUEUE_MAX) {
|
||||
......
|
||||
return;
|
||||
}
|
||||
n->vqs = g_malloc0(sizeof(VirtIONetQueue) * n->max_queues);
|
||||
n->curr_queues = 1;
|
||||
......
|
||||
n->net_conf.tx_queue_size = MIN(virtio_net_max_tx_queue_size(n),
|
||||
n->net_conf.tx_queue_size);
|
||||
|
||||
for (i = 0; i < n->max_queues; i++) {
|
||||
virtio_net_add_queue(n, i);
|
||||
}
|
||||
|
||||
n->ctrl_vq = virtio_add_queue(vdev, 64, virtio_net_handle_ctrl);
|
||||
qemu_macaddr_default_if_unset(&n->nic_conf.macaddr);
|
||||
memcpy(&n->mac[0], &n->nic_conf.macaddr, sizeof(n->mac));
|
||||
n->status = VIRTIO_NET_S_LINK_UP;
|
||||
|
||||
if (n->netclient_type) {
|
||||
n->nic = qemu_new_nic(&net_virtio_info, &n->nic_conf,
|
||||
n->netclient_type, n->netclient_name, n);
|
||||
} else {
|
||||
n->nic = qemu_new_nic(&net_virtio_info, &n->nic_conf,
|
||||
object_get_typename(OBJECT(dev)), dev->id, n);
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里面创建了一个VirtIODevice,这一点和存储虚拟化也是一样的。virtio_init用来初始化这个设备。VirtIODevice结构里面有一个VirtQueue数组,这就是virtio前端和后端互相传数据的队列,最多有VIRTIO_QUEUE_MAX个。
|
||||
|
||||
刚才我们说的都是一样的地方,其实也有不一样的地方,我们下面来看。
|
||||
|
||||
你会发现,这里面有这样的语句n->max_queues * 2 + 1 > VIRTIO_QUEUE_MAX。为什么要乘以2呢?这是因为,对于网络设备来讲,应该分发送队列和接收队列两个方向,所以乘以2。
|
||||
|
||||
接下来,我们调用virtio_net_add_queue来初始化队列,可以看出来,这里面就有发送tx_vq和接收rx_vq两个队列。
|
||||
|
||||
```
|
||||
typedef struct VirtIONetQueue {
|
||||
VirtQueue *rx_vq;
|
||||
VirtQueue *tx_vq;
|
||||
QEMUTimer *tx_timer;
|
||||
QEMUBH *tx_bh;
|
||||
uint32_t tx_waiting;
|
||||
struct {
|
||||
VirtQueueElement *elem;
|
||||
} async_tx;
|
||||
struct VirtIONet *n;
|
||||
} VirtIONetQueue;
|
||||
|
||||
static void virtio_net_add_queue(VirtIONet *n, int index)
|
||||
{
|
||||
VirtIODevice *vdev = VIRTIO_DEVICE(n);
|
||||
|
||||
n->vqs[index].rx_vq = virtio_add_queue(vdev, n->net_conf.rx_queue_size, virtio_net_handle_rx);
|
||||
|
||||
......
|
||||
|
||||
n->vqs[index].tx_vq = virtio_add_queue(vdev, n->net_conf.tx_queue_size, virtio_net_handle_tx_bh);
|
||||
n->vqs[index].tx_bh = qemu_bh_new(virtio_net_tx_bh, &n->vqs[index]);
|
||||
n->vqs[index].n = n;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
每个VirtQueue中,都有一个vring用来维护这个队列里面的数据;另外还有函数virtio_net_handle_rx用于处理网络包的接收;函数virtio_net_handle_tx_bh用于网络包的发送,这个函数我们后面会用到。
|
||||
|
||||
```
|
||||
NICState *qemu_new_nic(NetClientInfo *info,
|
||||
NICConf *conf,
|
||||
const char *model,
|
||||
const char *name,
|
||||
void *opaque)
|
||||
{
|
||||
NetClientState **peers = conf->peers.ncs;
|
||||
NICState *nic;
|
||||
int i, queues = MAX(1, conf->peers.queues);
|
||||
......
|
||||
nic = g_malloc0(info->size + sizeof(NetClientState) * queues);
|
||||
nic->ncs = (void *)nic + info->size;
|
||||
nic->conf = conf;
|
||||
nic->opaque = opaque;
|
||||
|
||||
for (i = 0; i < queues; i++) {
|
||||
qemu_net_client_setup(&nic->ncs[i], info, peers[i], model, name, NULL);
|
||||
nic->ncs[i].queue_index = i;
|
||||
}
|
||||
|
||||
return nic;
|
||||
}
|
||||
|
||||
static void qemu_net_client_setup(NetClientState *nc,
|
||||
NetClientInfo *info,
|
||||
NetClientState *peer,
|
||||
const char *model,
|
||||
const char *name,
|
||||
NetClientDestructor *destructor)
|
||||
{
|
||||
nc->info = info;
|
||||
nc->model = g_strdup(model);
|
||||
if (name) {
|
||||
nc->name = g_strdup(name);
|
||||
} else {
|
||||
nc->name = assign_name(nc, model);
|
||||
}
|
||||
|
||||
QTAILQ_INSERT_TAIL(&net_clients, nc, next);
|
||||
|
||||
nc->incoming_queue = qemu_new_net_queue(qemu_deliver_packet_iov, nc);
|
||||
nc->destructor = destructor;
|
||||
QTAILQ_INIT(&nc->filters);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
接下来,qemu_new_nic会创建一个虚拟机里面的网卡。
|
||||
|
||||
## qemu的启动过程中的网络虚拟化
|
||||
|
||||
初始化过程解析完毕以后,我们接下来从qemu的启动过程看起。
|
||||
|
||||
对于网卡的虚拟化,qemu的启动参数里面有关的是下面两行:
|
||||
|
||||
```
|
||||
-netdev tap,fd=32,id=hostnet0,vhost=on,vhostfd=37
|
||||
-device virtio-net-pci,netdev=hostnet0,id=net0,mac=fa:16:3e:d1:2d:99,bus=pci.0,addr=0x3
|
||||
|
||||
```
|
||||
|
||||
qemu的main函数会调用net_init_clients进行网络设备的初始化,可以解析net参数,也可以在net_init_clients中解析netdev参数。
|
||||
|
||||
```
|
||||
int net_init_clients(Error **errp)
|
||||
{
|
||||
QTAILQ_INIT(&net_clients);
|
||||
if (qemu_opts_foreach(qemu_find_opts("netdev"),
|
||||
net_init_netdev, NULL, errp)) {
|
||||
return -1;
|
||||
}
|
||||
if (qemu_opts_foreach(qemu_find_opts("nic"), net_param_nic, NULL, errp)) {
|
||||
return -1;
|
||||
}
|
||||
if (qemu_opts_foreach(qemu_find_opts("net"), net_init_client, NULL, errp)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
net_init_clients会解析参数。上面的参数netdev会调用net_init_netdev->net_client_init->net_client_init1。
|
||||
|
||||
net_client_init1会根据不同的driver类型,调用不同的初始化函数。
|
||||
|
||||
```
|
||||
static int (* const net_client_init_fun[NET_CLIENT_DRIVER__MAX])(
|
||||
const Netdev *netdev,
|
||||
const char *name,
|
||||
NetClientState *peer, Error **errp) = {
|
||||
[NET_CLIENT_DRIVER_NIC] = net_init_nic,
|
||||
[NET_CLIENT_DRIVER_TAP] = net_init_tap,
|
||||
[NET_CLIENT_DRIVER_SOCKET] = net_init_socket,
|
||||
[NET_CLIENT_DRIVER_HUBPORT] = net_init_hubport,
|
||||
......
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
由于我们配置的driver的类型是tap,因而这里会调用net_init_tap->net_tap_init->tap_open。
|
||||
|
||||
```
|
||||
#define PATH_NET_TUN "/dev/net/tun"
|
||||
|
||||
int tap_open(char *ifname, int ifname_size, int *vnet_hdr,
|
||||
int vnet_hdr_required, int mq_required, Error **errp)
|
||||
{
|
||||
struct ifreq ifr;
|
||||
int fd, ret;
|
||||
int len = sizeof(struct virtio_net_hdr);
|
||||
unsigned int features;
|
||||
|
||||
TFR(fd = open(PATH_NET_TUN, O_RDWR));
|
||||
memset(&ifr, 0, sizeof(ifr));
|
||||
ifr.ifr_flags = IFF_TAP | IFF_NO_PI;
|
||||
|
||||
if (ioctl(fd, TUNGETFEATURES, &features) == -1) {
|
||||
features = 0;
|
||||
}
|
||||
|
||||
if (features & IFF_ONE_QUEUE) {
|
||||
ifr.ifr_flags |= IFF_ONE_QUEUE;
|
||||
}
|
||||
|
||||
if (*vnet_hdr) {
|
||||
if (features & IFF_VNET_HDR) {
|
||||
*vnet_hdr = 1;
|
||||
ifr.ifr_flags |= IFF_VNET_HDR;
|
||||
} else {
|
||||
*vnet_hdr = 0;
|
||||
}
|
||||
ioctl(fd, TUNSETVNETHDRSZ, &len);
|
||||
}
|
||||
......
|
||||
ret = ioctl(fd, TUNSETIFF, (void *) &ifr);
|
||||
......
|
||||
fcntl(fd, F_SETFL, O_NONBLOCK);
|
||||
return fd;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在tap_open中,我们打开一个文件"/dev/net/tun",然后通过ioctl操作这个文件。这是Linux内核的一项机制,和KVM机制很像。其实这就是一种通过打开这个字符设备文件,然后通过ioctl操作这个文件和内核打交道,来使用内核的能力。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/24/d3/243e93913b18c3ab00be5676bef334d3.png" alt="">
|
||||
|
||||
为什么需要使用内核的机制呢?因为网络包需要从虚拟机里面发送到虚拟机外面,发送到宿主机上的时候,必须是一个正常的网络包才能被转发。要形成一个网络包,我们那就需要经过复杂的协议栈,协议栈的复杂咱们在[发送网络包](https://time.geekbang.org/column/article/106490)那一节讲过了。
|
||||
|
||||
客户机会将网络包发送给qemu。qemu自己没有网络协议栈,现去实现一个也不可能,太复杂了。于是,它就要借助内核的力量。
|
||||
|
||||
qemu会将客户机发送给它的网络包,然后转换成为文件流,写入"/dev/net/tun"字符设备。就像写一个文件一样。内核中TUN/TAP字符设备驱动会收到这个写入的文件流,然后交给TUN/TAP的虚拟网卡驱动。这个驱动会将文件流再次转成网络包,交给TCP/IP栈,最终从虚拟TAP网卡tap0发出来,成为标准的网络包。后面我们会看到这个过程。
|
||||
|
||||
现在我们到内核里面,看一看打开"/dev/net/tun"字符设备后,内核会发生什么事情。内核的实现在drivers/net/tun.c文件中。这是一个字符设备驱动程序,应该符合字符设备的格式。
|
||||
|
||||
```
|
||||
module_init(tun_init);
|
||||
module_exit(tun_cleanup);
|
||||
MODULE_DESCRIPTION(DRV_DESCRIPTION);
|
||||
MODULE_AUTHOR(DRV_COPYRIGHT);
|
||||
MODULE_LICENSE("GPL");
|
||||
MODULE_ALIAS_MISCDEV(TUN_MINOR);
|
||||
MODULE_ALIAS("devname:net/tun");
|
||||
|
||||
static int __init tun_init(void)
|
||||
{
|
||||
......
|
||||
ret = rtnl_link_register(&tun_link_ops);
|
||||
......
|
||||
ret = misc_register(&tun_miscdev);
|
||||
......
|
||||
ret = register_netdevice_notifier(&tun_notifier_block);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里面注册了一个tun_miscdev字符设备,从它的定义可以看出,这就是"/dev/net/tun"字符设备。
|
||||
|
||||
```
|
||||
static struct miscdevice tun_miscdev = {
|
||||
.minor = TUN_MINOR,
|
||||
.name = "tun",
|
||||
.nodename = "net/tun",
|
||||
.fops = &tun_fops,
|
||||
};
|
||||
|
||||
static const struct file_operations tun_fops = {
|
||||
.owner = THIS_MODULE,
|
||||
.llseek = no_llseek,
|
||||
.read_iter = tun_chr_read_iter,
|
||||
.write_iter = tun_chr_write_iter,
|
||||
.poll = tun_chr_poll,
|
||||
.unlocked_ioctl = tun_chr_ioctl,
|
||||
.open = tun_chr_open,
|
||||
.release = tun_chr_close,
|
||||
.fasync = tun_chr_fasync,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
qemu的tap_open函数会打开这个字符设备PATH_NET_TUN。打开字符设备的过程我们不再重复。我就说一下,到了驱动这一层,调用的是tun_chr_open。
|
||||
|
||||
```
|
||||
static int tun_chr_open(struct inode *inode, struct file * file)
|
||||
{
|
||||
struct tun_file *tfile;
|
||||
tfile = (struct tun_file *)sk_alloc(net, AF_UNSPEC, GFP_KERNEL,
|
||||
&tun_proto, 0);
|
||||
RCU_INIT_POINTER(tfile->tun, NULL);
|
||||
tfile->flags = 0;
|
||||
tfile->ifindex = 0;
|
||||
|
||||
init_waitqueue_head(&tfile->wq.wait);
|
||||
RCU_INIT_POINTER(tfile->socket.wq, &tfile->wq);
|
||||
|
||||
tfile->socket.file = file;
|
||||
tfile->socket.ops = &tun_socket_ops;
|
||||
|
||||
sock_init_data(&tfile->socket, &tfile->sk);
|
||||
|
||||
tfile->sk.sk_write_space = tun_sock_write_space;
|
||||
tfile->sk.sk_sndbuf = INT_MAX;
|
||||
|
||||
file->private_data = tfile;
|
||||
INIT_LIST_HEAD(&tfile->next);
|
||||
|
||||
sock_set_flag(&tfile->sk, SOCK_ZEROCOPY);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在tun_chr_open的参数里面,有一个struct file,这是代表什么文件呢?它代表的就是打开的字符设备文件"/dev/net/tun",因而往这个字符设备文件中写数据,就会通过这个struct file写入。这个struct file里面的file_operations,按照字符设备打开的规则,指向的就是tun_fops。
|
||||
|
||||
另外,我们还需要在tun_chr_open创建了一个结构struct tun_file,并且将struct file的private_data指向它。
|
||||
|
||||
```
|
||||
/* A tun_file connects an open character device to a tuntap netdevice. It
|
||||
* also contains all socket related structures
|
||||
* to serve as one transmit queue for tuntap device.
|
||||
*/
|
||||
struct tun_file {
|
||||
struct sock sk;
|
||||
struct socket socket;
|
||||
struct socket_wq wq;
|
||||
struct tun_struct __rcu *tun;
|
||||
struct fasync_struct *fasync;
|
||||
/* only used for fasnyc */
|
||||
unsigned int flags;
|
||||
union {
|
||||
u16 queue_index;
|
||||
unsigned int ifindex;
|
||||
};
|
||||
struct list_head next;
|
||||
struct tun_struct *detached;
|
||||
struct skb_array tx_array;
|
||||
};
|
||||
|
||||
struct tun_struct {
|
||||
struct tun_file __rcu *tfiles[MAX_TAP_QUEUES];
|
||||
unsigned int numqueues;
|
||||
unsigned int flags;
|
||||
kuid_t owner;
|
||||
kgid_t group;
|
||||
|
||||
struct net_device *dev;
|
||||
netdev_features_t set_features;
|
||||
int align;
|
||||
int vnet_hdr_sz;
|
||||
int sndbuf;
|
||||
struct tap_filter txflt;
|
||||
struct sock_fprog fprog;
|
||||
/* protected by rtnl lock */
|
||||
bool filter_attached;
|
||||
spinlock_t lock;
|
||||
struct hlist_head flows[TUN_NUM_FLOW_ENTRIES];
|
||||
struct timer_list flow_gc_timer;
|
||||
unsigned long ageing_time;
|
||||
unsigned int numdisabled;
|
||||
struct list_head disabled;
|
||||
void *security;
|
||||
u32 flow_count;
|
||||
u32 rx_batched;
|
||||
struct tun_pcpu_stats __percpu *pcpu_stats;
|
||||
};
|
||||
|
||||
static const struct proto_ops tun_socket_ops = {
|
||||
.peek_len = tun_peek_len,
|
||||
.sendmsg = tun_sendmsg,
|
||||
.recvmsg = tun_recvmsg,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
在struct tun_file中,有一个成员struct tun_struct,它里面有一个struct net_device,这个用来表示宿主机上的tuntap网络设备。在struct tun_file中,还有struct socket和struct sock,因为要用到内核的网络协议栈,所以就需要这两个结构,这在[网络协议](https://time.geekbang.org/column/article/105338)那一节已经分析过了。
|
||||
|
||||
所以,按照struct tun_file的注释说的,这是一个很重要的数据结构。"/dev/net/tun"对应的struct file的private_data指向它,因而可以接收qemu发过来的数据。除此之外,它还可以通过struct sock来操作内核协议栈,然后将网络包从宿主机上的tuntap网络设备发出去,宿主机上的tuntap网络设备对应的struct net_device也归它管。
|
||||
|
||||
在qemu的tap_open函数中,打开这个字符设备文件之后,接下来要做的事情是,通过ioctl来设置宿主机的网卡TUNSETIFF。
|
||||
|
||||
接下来,ioctl到了内核里面,会调用tun_chr_ioctl。
|
||||
|
||||
```
|
||||
static long __tun_chr_ioctl(struct file *file, unsigned int cmd,
|
||||
unsigned long arg, int ifreq_len)
|
||||
{
|
||||
struct tun_file *tfile = file->private_data;
|
||||
struct tun_struct *tun;
|
||||
void __user* argp = (void __user*)arg;
|
||||
struct ifreq ifr;
|
||||
kuid_t owner;
|
||||
kgid_t group;
|
||||
int sndbuf;
|
||||
int vnet_hdr_sz;
|
||||
unsigned int ifindex;
|
||||
int le;
|
||||
int ret;
|
||||
|
||||
if (cmd == TUNSETIFF || cmd == TUNSETQUEUE || _IOC_TYPE(cmd) == SOCK_IOC_TYPE) {
|
||||
if (copy_from_user(&ifr, argp, ifreq_len))
|
||||
return -EFAULT;
|
||||
}
|
||||
......
|
||||
tun = __tun_get(tfile);
|
||||
if (cmd == TUNSETIFF) {
|
||||
ifr.ifr_name[IFNAMSIZ-1] = '\0';
|
||||
ret = tun_set_iff(sock_net(&tfile->sk), file, &ifr);
|
||||
......
|
||||
if (copy_to_user(argp, &ifr, ifreq_len))
|
||||
ret = -EFAULT;
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在__tun_chr_ioctl中,我们首先通过copy_from_user把配置从用户态拷贝到内核态,调用tun_set_iff设置tuntap网络设备,然后调用copy_to_user将配置结果返回。
|
||||
|
||||
```
|
||||
static int tun_set_iff(struct net *net, struct file *file, struct ifreq *ifr)
|
||||
{
|
||||
struct tun_struct *tun;
|
||||
struct tun_file *tfile = file->private_data;
|
||||
struct net_device *dev;
|
||||
......
|
||||
char *name;
|
||||
unsigned long flags = 0;
|
||||
int queues = ifr->ifr_flags & IFF_MULTI_QUEUE ?
|
||||
MAX_TAP_QUEUES : 1;
|
||||
|
||||
if (ifr->ifr_flags & IFF_TUN) {
|
||||
/* TUN device */
|
||||
flags |= IFF_TUN;
|
||||
name = "tun%d";
|
||||
} else if (ifr->ifr_flags & IFF_TAP) {
|
||||
/* TAP device */
|
||||
flags |= IFF_TAP;
|
||||
name = "tap%d";
|
||||
} else
|
||||
return -EINVAL;
|
||||
|
||||
if (*ifr->ifr_name)
|
||||
name = ifr->ifr_name;
|
||||
|
||||
dev = alloc_netdev_mqs(sizeof(struct tun_struct), name,
|
||||
NET_NAME_UNKNOWN, tun_setup, queues,
|
||||
queues);
|
||||
|
||||
err = dev_get_valid_name(net, dev, name);
|
||||
dev_net_set(dev, net);
|
||||
dev->rtnl_link_ops = &tun_link_ops;
|
||||
dev->ifindex = tfile->ifindex;
|
||||
dev->sysfs_groups[0] = &tun_attr_group;
|
||||
|
||||
tun = netdev_priv(dev);
|
||||
tun->dev = dev;
|
||||
tun->flags = flags;
|
||||
tun->txflt.count = 0;
|
||||
tun->vnet_hdr_sz = sizeof(struct virtio_net_hdr);
|
||||
|
||||
tun->align = NET_SKB_PAD;
|
||||
tun->filter_attached = false;
|
||||
tun->sndbuf = tfile->socket.sk->sk_sndbuf;
|
||||
tun->rx_batched = 0;
|
||||
|
||||
tun_net_init(dev);
|
||||
tun_flow_init(tun);
|
||||
|
||||
err = tun_attach(tun, file, false);
|
||||
err = register_netdevice(tun->dev);
|
||||
|
||||
netif_carrier_on(tun->dev);
|
||||
|
||||
if (netif_running(tun->dev))
|
||||
netif_tx_wake_all_queues(tun->dev);
|
||||
|
||||
strcpy(ifr->ifr_name, tun->dev->name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
tun_set_iff创建了struct tun_struct和struct net_device,并且将这个tuntap网络设备通过register_netdevice注册到内核中。这样,我们就能在宿主机上通过ip addr看到这个网卡了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/98/fd/9826223c7375bec19bd13588f3875ffd.png" alt="">
|
||||
|
||||
至此宿主机上的内核的数据结构也完成了。
|
||||
|
||||
## 关联前端设备驱动和后端设备驱动
|
||||
|
||||
下面,我们来解析在客户机中发送一个网络包的时候,会发生哪些事情。
|
||||
|
||||
虚拟机里面的进程发送一个网络包,通过文件系统和Socket调用网络协议栈,到达网络设备层。只不过这个不是普通的网络设备,而是virtio_net的驱动。
|
||||
|
||||
virtio_net的驱动程序代码在Linux操作系统的源代码里面,文件名为drivers/net/virtio_net.c。
|
||||
|
||||
```
|
||||
static __init int virtio_net_driver_init(void)
|
||||
{
|
||||
ret = register_virtio_driver(&virtio_net_driver);
|
||||
......
|
||||
}
|
||||
module_init(virtio_net_driver_init);
|
||||
module_exit(virtio_net_driver_exit);
|
||||
|
||||
MODULE_DEVICE_TABLE(virtio, id_table);
|
||||
MODULE_DESCRIPTION("Virtio network driver");
|
||||
MODULE_LICENSE("GPL");
|
||||
|
||||
static struct virtio_driver virtio_net_driver = {
|
||||
.driver.name = KBUILD_MODNAME,
|
||||
.driver.owner = THIS_MODULE,
|
||||
.id_table = id_table,
|
||||
.validate = virtnet_validate,
|
||||
.probe = virtnet_probe,
|
||||
.remove = virtnet_remove,
|
||||
.config_changed = virtnet_config_changed,
|
||||
......
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
在virtio_net的驱动程序的初始化代码中,我们需要注册一个驱动函数virtio_net_driver。
|
||||
|
||||
当一个设备驱动作为一个内核模块被初始化的时候,probe函数会被调用,因而我们来看一下virtnet_probe。
|
||||
|
||||
```
|
||||
static int virtnet_probe(struct virtio_device *vdev)
|
||||
{
|
||||
int i, err;
|
||||
struct net_device *dev;
|
||||
struct virtnet_info *vi;
|
||||
u16 max_queue_pairs;
|
||||
int mtu;
|
||||
|
||||
/* Allocate ourselves a network device with room for our info */
|
||||
dev = alloc_etherdev_mq(sizeof(struct virtnet_info), max_queue_pairs);
|
||||
|
||||
/* Set up network device as normal. */
|
||||
dev->priv_flags |= IFF_UNICAST_FLT | IFF_LIVE_ADDR_CHANGE;
|
||||
dev->netdev_ops = &virtnet_netdev;
|
||||
dev->features = NETIF_F_HIGHDMA;
|
||||
|
||||
dev->ethtool_ops = &virtnet_ethtool_ops;
|
||||
SET_NETDEV_DEV(dev, &vdev->dev);
|
||||
......
|
||||
/* MTU range: 68 - 65535 */
|
||||
dev->min_mtu = MIN_MTU;
|
||||
dev->max_mtu = MAX_MTU;
|
||||
|
||||
/* Set up our device-specific information */
|
||||
vi = netdev_priv(dev);
|
||||
vi->dev = dev;
|
||||
vi->vdev = vdev;
|
||||
vdev->priv = vi;
|
||||
vi->stats = alloc_percpu(struct virtnet_stats);
|
||||
INIT_WORK(&vi->config_work, virtnet_config_changed_work);
|
||||
......
|
||||
vi->max_queue_pairs = max_queue_pairs;
|
||||
|
||||
/* Allocate/initialize the rx/tx queues, and invoke find_vqs */
|
||||
err = init_vqs(vi);
|
||||
netif_set_real_num_tx_queues(dev, vi->curr_queue_pairs);
|
||||
netif_set_real_num_rx_queues(dev, vi->curr_queue_pairs);
|
||||
|
||||
virtnet_init_settings(dev);
|
||||
|
||||
err = register_netdev(dev);
|
||||
virtio_device_ready(vdev);
|
||||
virtnet_set_queues(vi, vi->curr_queue_pairs);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在virtnet_probe中,会创建struct net_device,并且通过register_netdev注册这个网络设备,这样在客户机里面,就能看到这个网卡了。
|
||||
|
||||
在virtnet_probe中,还有一件重要的事情就是,init_vqs会初始化发送和接收的virtqueue。
|
||||
|
||||
```
|
||||
static int init_vqs(struct virtnet_info *vi)
|
||||
{
|
||||
int ret;
|
||||
|
||||
/* Allocate send & receive queues */
|
||||
ret = virtnet_alloc_queues(vi);
|
||||
ret = virtnet_find_vqs(vi);
|
||||
......
|
||||
get_online_cpus();
|
||||
virtnet_set_affinity(vi);
|
||||
put_online_cpus();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int virtnet_alloc_queues(struct virtnet_info *vi)
|
||||
{
|
||||
int i;
|
||||
|
||||
vi->sq = kzalloc(sizeof(*vi->sq) * vi->max_queue_pairs, GFP_KERNEL);
|
||||
vi->rq = kzalloc(sizeof(*vi->rq) * vi->max_queue_pairs, GFP_KERNEL);
|
||||
|
||||
INIT_DELAYED_WORK(&vi->refill, refill_work);
|
||||
for (i = 0; i < vi->max_queue_pairs; i++) {
|
||||
vi->rq[i].pages = NULL;
|
||||
netif_napi_add(vi->dev, &vi->rq[i].napi, virtnet_poll,
|
||||
napi_weight);
|
||||
netif_tx_napi_add(vi->dev, &vi->sq[i].napi, virtnet_poll_tx,
|
||||
napi_tx ? napi_weight : 0);
|
||||
|
||||
sg_init_table(vi->rq[i].sg, ARRAY_SIZE(vi->rq[i].sg));
|
||||
ewma_pkt_len_init(&vi->rq[i].mrg_avg_pkt_len);
|
||||
sg_init_table(vi->sq[i].sg, ARRAY_SIZE(vi->sq[i].sg));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
按照上一节的virtio原理,virtqueue是一个介于客户机前端和qemu后端的一个结构,用于在这两端之间传递数据,对于网络设备来讲有发送和接收两个方向的队列。这里建立的struct virtqueue是客户机前端对于队列的管理的数据结构。
|
||||
|
||||
队列的实体需要通过函数virtnet_find_vqs查找或者生成,这里还会指定接收队列的callback函数为skb_recv_done,发送队列的callback函数为skb_xmit_done。那当buffer使用发生变化的时候,我们可以调用这个callback函数进行通知。
|
||||
|
||||
```
|
||||
static int virtnet_find_vqs(struct virtnet_info *vi)
|
||||
{
|
||||
vq_callback_t **callbacks;
|
||||
struct virtqueue **vqs;
|
||||
int ret = -ENOMEM;
|
||||
int i, total_vqs;
|
||||
const char **names;
|
||||
|
||||
/* Allocate space for find_vqs parameters */
|
||||
vqs = kzalloc(total_vqs * sizeof(*vqs), GFP_KERNEL);
|
||||
callbacks = kmalloc(total_vqs * sizeof(*callbacks), GFP_KERNEL);
|
||||
names = kmalloc(total_vqs * sizeof(*names), GFP_KERNEL);
|
||||
|
||||
/* Allocate/initialize parameters for send/receive virtqueues */
|
||||
for (i = 0; i < vi->max_queue_pairs; i++) {
|
||||
callbacks[rxq2vq(i)] = skb_recv_done;
|
||||
callbacks[txq2vq(i)] = skb_xmit_done;
|
||||
names[rxq2vq(i)] = vi->rq[i].name;
|
||||
names[txq2vq(i)] = vi->sq[i].name;
|
||||
}
|
||||
|
||||
ret = vi->vdev->config->find_vqs(vi->vdev, total_vqs, vqs, callbacks, names, ctx, NULL);
|
||||
......
|
||||
for (i = 0; i < vi->max_queue_pairs; i++) {
|
||||
vi->rq[i].vq = vqs[rxq2vq(i)];
|
||||
vi->rq[i].min_buf_len = mergeable_min_buf_len(vi, vi->rq[i].vq);
|
||||
vi->sq[i].vq = vqs[txq2vq(i)];
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这里的find_vqs是在struct virtnet_info里的struct virtio_device里的struct virtio_config_ops *config里面定义的。
|
||||
|
||||
根据virtio_config_ops的定义,find_vqs会调用vp_modern_find_vqs,到这一步和块设备是一样的了。
|
||||
|
||||
在vp_modern_find_vqs中,vp_find_vqs会调用vp_find_vqs_intx。在vp_find_vqs_intx中,通过request_irq注册一个中断处理函数vp_interrupt。当设备向队列中写入信息时,会产生一个中断,也就是vq中断。中断处理函数需要调用相应的队列的回调函数,然后根据队列的数目,依次调用vp_setup_vq完成virtqueue、vring的分配和初始化。
|
||||
|
||||
同样,这些数据结构会和virtio后端的VirtIODevice、VirtQueue、vring对应起来,都应该指向刚才创建的那一段内存。
|
||||
|
||||
客户机同样会通过调用专门给外部设备发送指令的函数iowrite告诉外部的pci设备,这些共享内存的地址。
|
||||
|
||||
至此前端设备驱动和后端设备驱动之间的两个收发队列就关联好了,这两个队列的格式和块设备是一样的。
|
||||
|
||||
## 发送网络包过程
|
||||
|
||||
接下来,我们来看当真的发送一个网络包的时候,会发生什么。
|
||||
|
||||
当网络包经过客户机的协议栈到达virtio_net驱动的时候,按照net_device_ops的定义,start_xmit会被调用。
|
||||
|
||||
```
|
||||
static const struct net_device_ops virtnet_netdev = {
|
||||
.ndo_open = virtnet_open,
|
||||
.ndo_stop = virtnet_close,
|
||||
.ndo_start_xmit = start_xmit,
|
||||
.ndo_validate_addr = eth_validate_addr,
|
||||
.ndo_set_mac_address = virtnet_set_mac_address,
|
||||
.ndo_set_rx_mode = virtnet_set_rx_mode,
|
||||
.ndo_get_stats64 = virtnet_stats,
|
||||
.ndo_vlan_rx_add_vid = virtnet_vlan_rx_add_vid,
|
||||
.ndo_vlan_rx_kill_vid = virtnet_vlan_rx_kill_vid,
|
||||
.ndo_xdp = virtnet_xdp,
|
||||
.ndo_features_check = passthru_features_check,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
接下来的调用链为:start_xmit->xmit_skb-> virtqueue_add_outbuf->virtqueue_add,将网络包放入队列中,并调用virtqueue_notify通知接收方。
|
||||
|
||||
```
|
||||
static netdev_tx_t start_xmit(struct sk_buff *skb, struct net_device *dev)
|
||||
{
|
||||
struct virtnet_info *vi = netdev_priv(dev);
|
||||
int qnum = skb_get_queue_mapping(skb);
|
||||
struct send_queue *sq = &vi->sq[qnum];
|
||||
int err;
|
||||
struct netdev_queue *txq = netdev_get_tx_queue(dev, qnum);
|
||||
bool kick = !skb->xmit_more;
|
||||
bool use_napi = sq->napi.weight;
|
||||
......
|
||||
/* Try to transmit */
|
||||
err = xmit_skb(sq, skb);
|
||||
......
|
||||
if (kick || netif_xmit_stopped(txq))
|
||||
virtqueue_kick(sq->vq);
|
||||
return NETDEV_TX_OK;
|
||||
}
|
||||
|
||||
bool virtqueue_kick(struct virtqueue *vq)
|
||||
{
|
||||
if (virtqueue_kick_prepare(vq))
|
||||
return virtqueue_notify(vq);
|
||||
return true;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
写入一个I/O会使得qemu触发VM exit,这个逻辑我们在解析CPU的时候看到过。
|
||||
|
||||
接下来,我们那会调用VirtQueue的handle_output函数。前面我们已经设置过这个函数了,其实就是virtio_net_handle_tx_bh。
|
||||
|
||||
```
|
||||
static void virtio_net_handle_tx_bh(VirtIODevice *vdev, VirtQueue *vq)
|
||||
{
|
||||
VirtIONet *n = VIRTIO_NET(vdev);
|
||||
VirtIONetQueue *q = &n->vqs[vq2q(virtio_get_queue_index(vq))];
|
||||
|
||||
q->tx_waiting = 1;
|
||||
|
||||
virtio_queue_set_notification(vq, 0);
|
||||
qemu_bh_schedule(q->tx_bh);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
virtio_net_handle_tx_bh调用了qemu_bh_schedule,而在virtio_net_add_queue中调用qemu_bh_new,并把函数设置为virtio_net_tx_bh。
|
||||
|
||||
virtio_net_tx_bh函数调用发送函数virtio_net_flush_tx。
|
||||
|
||||
```
|
||||
static int32_t virtio_net_flush_tx(VirtIONetQueue *q)
|
||||
{
|
||||
VirtIONet *n = q->n;
|
||||
VirtIODevice *vdev = VIRTIO_DEVICE(n);
|
||||
VirtQueueElement *elem;
|
||||
int32_t num_packets = 0;
|
||||
int queue_index = vq2q(virtio_get_queue_index(q->tx_vq));
|
||||
|
||||
for (;;) {
|
||||
ssize_t ret;
|
||||
unsigned int out_num;
|
||||
struct iovec sg[VIRTQUEUE_MAX_SIZE], sg2[VIRTQUEUE_MAX_SIZE + 1], *out_sg;
|
||||
struct virtio_net_hdr_mrg_rxbuf mhdr;
|
||||
|
||||
elem = virtqueue_pop(q->tx_vq, sizeof(VirtQueueElement));
|
||||
out_num = elem->out_num;
|
||||
out_sg = elem->out_sg;
|
||||
......
|
||||
ret = qemu_sendv_packet_async(qemu_get_subqueue(n->nic, queue_index),out_sg, out_num, virtio_net_tx_complete);
|
||||
}
|
||||
......
|
||||
return num_packets;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
virtio_net_flush_tx会调用virtqueue_pop。这里面,我们能看到对于vring的操作,也即从这里面将客户机里面写入的数据读取出来。
|
||||
|
||||
然后,我们调用qemu_sendv_packet_async发送网络包。接下来的调用链为:qemu_sendv_packet_async->qemu_net_queue_send_iov->qemu_net_queue_flush->qemu_net_queue_deliver。
|
||||
|
||||
在qemu_net_queue_deliver中,我们会调用NetQueue的deliver函数。前面qemu_new_net_queue会把deliver函数设置为qemu_deliver_packet_iov。它会调用nc->info->receive_iov。
|
||||
|
||||
```
|
||||
static NetClientInfo net_tap_info = {
|
||||
.type = NET_CLIENT_DRIVER_TAP,
|
||||
.size = sizeof(TAPState),
|
||||
.receive = tap_receive,
|
||||
.receive_raw = tap_receive_raw,
|
||||
.receive_iov = tap_receive_iov,
|
||||
.poll = tap_poll,
|
||||
.cleanup = tap_cleanup,
|
||||
.has_ufo = tap_has_ufo,
|
||||
.has_vnet_hdr = tap_has_vnet_hdr,
|
||||
.has_vnet_hdr_len = tap_has_vnet_hdr_len,
|
||||
.using_vnet_hdr = tap_using_vnet_hdr,
|
||||
.set_offload = tap_set_offload,
|
||||
.set_vnet_hdr_len = tap_set_vnet_hdr_len,
|
||||
.set_vnet_le = tap_set_vnet_le,
|
||||
.set_vnet_be = tap_set_vnet_be,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
根据net_tap_info的定义调用的是tap_receive_iov。他会调用tap_write_packet->writev写入这个字符设备。
|
||||
|
||||
在内核的字符设备驱动中,tun_chr_write_iter会被调用。
|
||||
|
||||
```
|
||||
static ssize_t tun_chr_write_iter(struct kiocb *iocb, struct iov_iter *from)
|
||||
{
|
||||
struct file *file = iocb->ki_filp;
|
||||
struct tun_struct *tun = tun_get(file);
|
||||
struct tun_file *tfile = file->private_data;
|
||||
ssize_t result;
|
||||
|
||||
result = tun_get_user(tun, tfile, NULL, from,
|
||||
file->f_flags & O_NONBLOCK, false);
|
||||
|
||||
tun_put(tun);
|
||||
return result;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
当我们使用writev()系统调用向tun/tap设备的字符设备文件写入数据时,tun_chr_write函数将被调用。它会使用tun_get_user,从用户区接收数据,将数据存入skb中,然后调用关键的函数netif_rx_ni(skb) ,将skb送给tcp/ip协议栈处理,最终完成虚拟网卡的数据接收。
|
||||
|
||||
至此,从虚拟机内部到宿主机的网络传输过程才算结束。
|
||||
|
||||
## 总结时刻
|
||||
|
||||
最后,我们把网络虚拟化场景下网络包的发送过程总结一下。
|
||||
|
||||
- 在虚拟机里面的用户态,应用程序通过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虚拟网卡,完成从虚拟机里面到宿主机的整个发送过程。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/e3/44/e329505cfcd367612f8ae47054ec8e44.jpg" alt="">
|
||||
|
||||
## 课堂练习
|
||||
|
||||
这一节我们解析的是发送过程,请你根据类似的思路,解析一下接收过程。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解,也欢迎收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">
|
||||
Reference in New Issue
Block a user