virtio半虚拟化概述
virtio 是由IBM提出的对半虚拟化 hypervisor 中的一组通用模拟设备的抽象。它允许 hypervisor 导出一组通用的模拟设备,并通过一个通用的应用编程接口(API)让它们变得可用。右图展示了为什么这很重要。有了半虚拟化 hypervisor 之后,客户操作系统能够实现一组通用的接口,在一组后端驱动程序之后采用特定的设备模拟。
抽象结构
- virtio表示虚拟化IO,用于实现设备半虚拟化,即虚拟机中运行的操作系统需要加载特殊的驱动(e.g. virtio-net)且虚拟机知道自己是虚拟机相较于基于完全模拟的全虚拟化,基于virtio的半虚拟化可以提升设备访问性能
- 运行在虚拟机中的部分称为前端驱动,负责对虚拟机提供统一的接口
- 运行在宿主机中的部分称为后端驱动,负责适配不同的物理硬件设备
IO路径概述
[IO路径](https://www.redhat.com/en/blog/virtio-devices-and-drivers-overview-headjack-and-phone)
- virtio层实现虚拟队列接口,作为前后端通信的桥梁,实现virtio的通用结构和属性定义
- virtio_xx(xx可以是blk,net,console等),以virtio_net为例它拥有两组队列input和output即输入输出分别独占一个队列实现异步IO
- virtio-ring层是虚拟队列的具体实现,它包含了实际的队列即上图的vring,上图中虽然前端和后端中都分别画了一组vring但是实际上他们是一个共享内存环也就是说一个队列前端和后端都可以访问
辅助知识
如何查看网卡队列
[root@iv-ybz88tnky35m56blnrfb tools]# ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX: 0
TX: 0
Other: 0
Combined: 2 //支持的最大队列
Current hardware settings:
RX: 0
TX: 0
Other: 0
Combined: 2 //当前队列数
[root@iv-ybz88tnky35m56blnrfb tools]# ethtool -L eth0 combined 2 //设置网卡队列数
[root@iv-ybz88tnky35m56blnrfb ~]# ethtool -g eth0
Ring parameters for eth0:
Pre-set maximums:
RX: 1024
RX Mini: 0
RX Jumbo: 0
TX: 1024
Current hardware settings:
RX: 1024 //队列长度
RX Mini: 0
RX Jumbo: 0
TX: 1024
通过下面的方式可以查看队列和CPU的绑定关系,首先通过lshw找到网卡对应的virtio号,例如下图可见eth0对应virtio8这个设备:
[root@iv-ybz88tnky35m56blnrfb tools]# lshw |less
*-virtio8
description: Ethernet interface
physical id: 0
bus info: virtio@8
logical name: eth0
serial: 00:16:3e:29:7d:d5
capabilities: ethernet physical
configuration: broadcast=yes driver=virtio_net driverversion=1.0.0 ip=172.16.0.127 link=yes multicast=yes
之后通过/proc/interrupts 找到virtio8设备,可以看到中断号31-35被virtio8这个网卡占用了,config 即31号中断是处理控制面的,input0/output0 是第一组队列,input1/output1 是第二组队列,因为网卡是全双工的所以发送和接收队列是独立的互不影响,我的环境是双核的所以这里可以看到两列:CPU0 CPU1 下面的数字分别代表这个处理器处理对应中断的次数,可见virtio8-input.0和virtio8-input.1的几乎所有中断都是CPU1处理的。
[root@iv-ybz88tnky35m56blnrfb tools]# cat /proc/interrupts
CPU0 CPU1
31: 5 0 PCI-MSI-edge virtio8-config
32: 186 1730608 PCI-MSI-edge virtio8-input.0
33: 26 0 PCI-MSI-edge virtio8-output.0
34: 52 4379678 PCI-MSI-edge virtio8-input.1
35: 69 0 PCI-MSI-edge virtio8-output.1
什么是中断
Linux中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的CPU暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。
- 中断:是一种异步的事件处理机制,可以提高系统的并发处理能力。
- 如何解决中断处理程序执行过长和中断丢失的问题:
Linux 将中断处理过程分成了两个阶段,也就是上半部和下半部。 上半部用来快速处理中断,它在中断禁止模式下运行,主要处理跟硬件紧密相关的或时间敏感的工作。也就是我们常说的硬中断,特点是快速执行。 下半部用来延迟处理上半部未完成的工作,通常以内核线程的方式运行。也就是我们常说的软中断,特点是延迟执行。
- proc 文件系统:是一种内核空间和用户空间进行通信的机制,可以用来查看内核的数据结构,或者用来动态修改内核的配置。
/proc/softirqs 提供了软中断的运行情况; /proc/interrupts 提供了硬中断的运行情况。
- 硬中断:硬中断是由硬件产生的,比如,像磁盘,网卡,键盘,时钟等。每个设备或设备集都有它自己的IRQ(中断请求)。基于IRQ,CPU可以将相应的请求分发到对应的硬件驱动上。硬中断可以直接中断CPU,引起内核中相关的代码被触发。
- 软中断:软中断仅与内核相关,由当前正在运行的进程所产生。 通常,软中断是一些对I/O的请求,这些请求会调用内核中可以调度I/O发生的程序。 软中断并不会直接中断CPU,也只有当前正在运行的代码(或进程)才会产生软中断。这种中断是一种需要内核为正在运行的进程去做一些事情(通常为I/O)的请求。 除了iowait(等待I/O的CPU使用率)升高,软中断(softirq)CPU使用率升高也是最常见的一种性能问题。
中断亲和性
在多处理器系统中,管理员可以设置中断亲和性,允许中断控制器把某个中断转发给哪些处理器,有两种配置方法:
- 写文件”/proc/irq/irq_id/smp_affinity”,参数是位掩码。
- 写文件“/proc/irq/irq_id/smp_affinity_list”,参数是处理器列表
查看32和34号中断,我们可以看到他们的smp_affinity都是2,我们用二进制换算一下: 00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000010 具体的位数就代表几号CPU,这就和前面的CPU处理的中断数对上了,但是为什么还是会有几个中断跑到了CPU0上呢,一般应该是由于系统刚启动时还没有默认做亲和性配置,默认所有设备的中断都会被指向0号CPU:
[root@iv-ybz88tnky35m56blnrfb tools]# cat /proc/irq/32/smp_affinity
2
[root@iv-ybz88tnky35m56blnrfb tools]# cat /proc/irq/34/smp_affinity
2
设置中断亲和性:
[root@iv-ybz88tnky35m56blnrfb tools]# echo 0-1 >/proc/irq/34/smp_affinity_list
[root@iv-ybz88tnky35m56blnrfb tools]# cat /proc/irq/34/smp_affinity_list
0-1
[root@iv-ybz88tnky35m56blnrfb tools]# cat /proc/irq/34/smp_affinity
3
数据交换过程概述
Virtio 使用 virtqueue 队列来实现I/O 机制,vring (共享环)是 virtqueue 的具体实现方式,我们可以把这个共享环理解为一个环形数组,这段环形缓冲是前端和后端共享的,当接收报文时,后端会将收到的报文放置到环形缓冲内,前端按序进行接收。同时前端会将已经接收过的报文进行缓冲释放,以确保后端有足够的free buffer放置报文。那么以上三个索引值的含义分别为:
- last_used_idx:前端收取报文(或者缓冲)到哪里
- used->idx:后端放置报文到哪里
- avail->idx:前端提供free buffer到哪里 由此环形缓冲中根据不同的索引产生了三段buffer,其具体含义为:
- used->idx - last_used_idx:代表有多少pending buffer后端提供了,但是前端还没有取走。
- avail->idx - used->idx:前端提供了多少free buffer给后端使用。
- last_used_idx - avail->idx:整个环形缓冲中有多少buffer是可以被释放为free buffer提供给后端的,这些buffer的内容已经被前端取走了,但是尚未被前端释放为free buffer。 要了解数据交换过程需要先了解如下核心数据结构:
crash> struct vring
struct vring {
unsigned int num;
struct vring_desc *desc;
struct vring_avail *avail;
struct vring_used *used;
}
crash> struct vring_desc
struct vring_desc {
__virtio64 addr;
__virtio32 len;
__virtio16 flags;
__virtio16 next;
}
SIZE: 16
crash> struct vring_avail
struct vring_avail {
__virtio16 flags;
__virtio16 idx;
__virtio16 ring[];
}
SIZE: 4
crash> struct vring_used
struct vring_used {
__virtio16 flags;
__virtio16 idx;
struct vring_used_elem ring[];
}
SIZE: 4
- vring结构实现了对ring buffer共享内存环的管理,vring_desc队列保存了所有真正的数据报文
- vring_avail和vring_used , 在发送报文的时候,前端驱动将报文在desc中的索引放在avail队列中,后端驱动从这个队列里获取报文进行转发,处理完之后将这些报文放入used队列。在接受报文的时候前端驱动将空白的内存块放入avail队列中(当然也只是报文在desc队列中的索引而已),后端接受报文将内容填充后,将这些含数据的报文放入used队列。
struct vring_virtqueue {
vq = {
list = {
next = 0xffff881027e3d800,
prev = 0xffff881026d9b000
},
callback = 0xffffffffa0149450,
name = 0xffff881027e3ee88 "output.0", ->>表明是发送队列
vdev = 0xffff881023776800,
priv = 0xffff8810237d03c0
},
vring = {
num = 256, ->>所有的队列长度
desc = 0xffff881026d9c000, ->> desc队列
avail = 0xffff881026d9d000, ->> avail队列
used = 0xffff881026d9e000 ->> used队列
},
broken = false,
indirect = true,
event = true,
num_free = 0, ->> 队列目前有多少空闲元素了,如果已经为0表明队列已经阻塞,前端将无法发送报文给后端
free_head = 0, ->> 指向下一个空闲的desc元素
num_added = 0, ->>是最近一次操作向队列中添加报文的数量
last_used_idx = 52143, 这是前端记录他看到最新的被后端用过的索引(idx),是前端已经处理到的used队列的idx。前端会把这个值写到avail队列的最后一个元素,这样后端就可以得知前端已经处理到used队列的哪一个元素了。
<> ->> last_avail_idx 前端不会碰,而且前端的virtqueue结构里就没有这个值,这个代表后端已经处理到avail队列的哪个元素了,前端靠这个信息来做限速,后端是把这个值写在used队列的最后一个元素,这样前端就可以读到了。
notify = 0xffffffffa005a350,
queue_index = 1,
data = 0xffff881026d9f078
}
crash> struct vring_avail 0xffff881026d9d000
struct vring_avail {
flags = 0,
idx = 52399, ->> avail队列的下个可用元素的索引
ring = 0xffff881026d9d004 ->> 队列数组
}
crash> struct vring_used
struct vring_used {
__u16 flags;
__u16 idx; ->> used队列的下个可用元素的索引
struct vring_used_elem ring[]; ->> 队列数组
}
Linux live debug
下面介绍一下如何在内存中观察这几个索引:
crash> net
NET_DEVICE NAME IP ADDRESS(ES)
ffff9895fa60a000 lo 127.0.0.1
ffff9895f5ec4000 eth0 192.168.10.171 //获取网络设备net_device地址
crash> struct net_device ffff9895f5ec4000 -o | grep device //从net_device中获取通用设备device地址
struct net_device {
[ffff9895f5ec40c8] struct net_device_stats stats;
[ffff9895f5ec4198] const struct net_device_ops *netdev_ops;
[ffff9895f5ec4268] struct in_device *ip_ptr;
[ffff9895f5ec43d8] void (*destructor)(struct net_device *);
[ffff9895f5ec4408] struct device dev;
[ffff9895f5ec4740] struct phy_device *phydev;
[ffff9895f5ec4880] struct net_device_extended *extended;
crash> struct device ffff9895f5ec4408 | grep parent //获取parent指针
parent = 0xffff9895f5c56810,
parent = 0xffff9895f6588600,
crash> struct virtio_device 0xffff9895f5c56800 -o //parent减去十六进制10就是virtio_device地址
struct virtio_device {
[ffff9895f5c56800] int index;
[ffff9895f5c56804] bool config_enabled;
[ffff9895f5c56805] bool config_change_pending;
[ffff9895f5c56808] spinlock_t config_lock;
[ffff9895f5c5680c] bool failed;
[ffff9895f5c56810] struct device dev;
[ffff9895f5c56ab0] struct virtio_device_id id;
[ffff9895f5c56ab8] const struct virtio_config_ops *config;
[ffff9895f5c56ac0] const struct vringh_config_ops *vringh_config;
[ffff9895f5c56ac8] struct list_head vqs;
[ffff9895f5c56ad8] u64 features;
[ffff9895f5c56ae0] void *priv;
}
SIZE: 744
crash> list ffff9895f5c56ac8
ffff9895f5c56ac8
ffff9895f5fc0000 //input.0
ffff9895f7000000 //output.0 //如果是多队列会递增排列
ffff9895f5f90800 //control
crash>
crash> struct vring_virtqueue ffff9895f7000000
struct vring_virtqueue {
vq = {
list = {
next = 0xffff9895f5f90800,
prev = 0xffff9895f5fc0000
},
callback = 0xffffffffc012b390 <skb_xmit_done>,
name = 0xffff9895f5d8b268 "output.0",
vdev = 0xffff9895f5c56800,
index = 1,
num_free = 4094,
priv = 0xffffa42fc0284010
},
vring = {
num = 4096, //队列长度,由后端决定
desc = 0xffff9895f5fe0000, //找到了我们最终需要的vring index
avail = 0xffff9895f5ff0000,
used = 0xffff9895f5ff3000
},
weak_barriers = true,
broken = false,
indirect = true,
event = false,
free_head = 0,
num_added = 0,
last_used_idx = 36327,
notify = 0xffffffffc00bfb60 <vp_notify>,
we_own_ring = true,
queue_size_in_bytes = 110598,
queue_dma_addr = 905838592,
desc_state = 0xffff9895f7000088
}
crash> struct vring_avail 0xffff9895f5ff0000
struct vring_avail {
flags = 1,
idx = 45526, //单调递增的,需要取模
ring = 0xffff9895f5ff0004
}
支持的常见问题场景
前后端队列的实时状态在很大程度上可以反映非常多的问题,是一种比较好的观察手段例如:(性能问题,网络抖动)
- 单队列网卡不工作导致部分网络不通
- 丢中断
- 队列不工作
- 网络负载高,used_ring回收不及时,导致free队列过少造成前后端丢包
- 中断亲和性配置不合理
- 多队列未启用