JasonWang's Blog

虚拟化之三virtio的实现原理

字数统计: 3.6k阅读时长: 16 min
2023/09/15

在讲虚拟化的第二篇QNX系统时,提到在QNX中设备的虚拟化有直通(pass-through)、半虚拟化(para-virtulization)、全虚拟化(full-virtualization)等几种形式,而像串口、网络设备、块设备等通常都是基于半虚拟化的形式实现的。在半虚拟化的实现方案中,virtio是最常见的一种。简单来说,virtio是虚拟化设备的中间抽象层,为设备的虚拟化提供了一个统一的框架与接口,增加了跨平台时代码的复用性。

下图是两种虚拟化方案:全虚拟化与半虚拟化的框架简图。对全虚拟化方案而已,虚拟机完全不知道自己运行在一个虚拟化平台之上,hypervisor为虚拟机提供了一个完全模拟的环境,虚拟机对资源与设备的访问都需要通过异常的陷入(trap)指令来完成,这在一定程度上降低了资源访问的效率;而对半虚拟化方案而言,虚拟机与宿主机共同来完成虚拟化,虚拟机是完全知道自己运行在一个虚拟化的环境中,相对而言,半虚拟化的效率更高。

full-para virtualization

virtio正是半虚拟化技术中实现设备虚拟化的一种被广泛使用的方案,并被非营利性标准化组织OASIS进行了标准化,详细的标准文档可以参考Virtio V1.2virtio将设备驱动的实现分离为前端(front-end)与后端(back-end)两个部分:

  • 前端驱动(front-end driver): 虚拟机系统中的一个设备驱动模块,负责接收来自虚拟机用户的请求,将其发送给宿主机上的后端驱动
  • 后端驱动(back-end driver): 运行在宿主机上的驱动模块,接收到来自虚拟机上的请求后,将其转换成物理设备上的操作

driver abstractions with virtio

QNX系统,提供了常见的如网络设备、串口设备以及块设备等多种后端虚拟化实现, 可以很好的与Linux下的virtio框架进行衔接,例如要在QNX上给Linux的虚拟机提供一张虚拟网卡,只需要在linux-lv.config中如下配置即可:

1
2
3

vdev vdev-virtio-net.so loc 0x1b018000 intr gic:45 peer /dev/vdevpeer/vp_lv mac aa:aa:aa:aa:aa:b1 bind-mode 0660 name agl_to_host

Linux虚拟机上打开内核配置CONFIG_VIRTIO_NET就可以枚举到对应的虚拟网卡, 其mac地址为aa:aa:aa:aa:aa:b1,中断号为45。接下来我们就以虚拟网卡virtio-net为例说明virtio具体的实现原理。

virtio的原理

QNX中的virtio驱动分为两个部分: 前端设备驱动(front-end driver)与后端驱动(back-end driver),前端驱动负责将虚拟机中的请求发送给后端驱动,后端驱动负责与前端驱动交互并将其请求发送给物理设备,像磁盘、网络、I2C、串口、fastRPC(用于与DSP交互)等设备都是通过virtio的方式来实现的,而其他的如音视频、显示、图形(Graphics)、Camera等传输数据比较大的模块也是采用了类似的前端与后端框架(高通称之为HAB(Hypervisor ABstraction))。virtio为虚拟化驱动的实现提供了一个标准接口,从而解耦了宿主机与虚拟机之间的驱动开发, 提高了系统的开发效率。以QNX中的网络驱动为例:

  • 虚拟机有虚拟网卡驱动,与后端驱动通过一个内存映射的区域进行数据交互
  • 后端驱动接收到虚拟机的数据后通过一个桥接口与QNX宿主机上的物理网卡驱动进行交互

QNX虚拟网卡驱动框架

接下来,我们从设备的识别与发现的过程来看下虚拟网卡中的具体实现。

设备的识别与初始化

virtio设备的发现一般有两种方式,一种是基于PCI虚拟总线进行设备枚举,一种是基于内存映射的方式。在QNX上,很多设备如虚拟网卡,虚拟磁盘设备等都是基于内存映射(MMIO)的方式来实现的。具体可以参考:

QNX上的虚拟设备vdev采用的是内存映射的方式,就是说虚拟设备实际是一个虚拟机的物理地址。QNX在启动vdev后端驱动进程时,会通过gfdt_add_vdev/gfdt_update_node添加设备树到虚拟机上,这样虚拟机会在启动的时候枚举到对应的虚拟设备
可以在/sys/firmware/devicetree/base/vdevs目录中查找虚拟机上对应的设备树节点

virtio采用标准的Linux设备模型,设备与设备驱动之间通过一个名为virtio的虚拟总线进行连接。在虚拟机启动时,会枚举到QNX配置的虚拟设备,并调用virtio_mmio_probe:

  • 首先会通过devm_request_mem_region请求虚拟设备对应的内存区域,用于设备的控制与事件传递
  • 接着会读取设备I/O地址来确认设备的类型(魔数)、virtio的版本号以及设备ID
  • 最后register_virtio_device将该虚拟设cat备添加到系统中,这样设备驱动加载时可以匹配到对应的设备
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74

static int virtio_mmio_probe(struct platform_device *pdev)
{
struct virtio_mmio_device *vm_dev;
struct resource *mem;
unsigned long magic;
int rc;

mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
if (!mem)
return -EINVAL;

if (!devm_request_mem_region(&pdev->dev, mem->start,
resource_size(mem), pdev->name))
return -EBUSY;

vm_dev = devm_kzalloc(&pdev->dev, sizeof(*vm_dev), GFP_KERNEL);
if (!vm_dev)
return -ENOMEM;

vm_dev->vdev.dev.parent = &pdev->dev;
vm_dev->vdev.dev.release = virtio_mmio_release_dev;
vm_dev->vdev.config = &virtio_mmio_config_ops;
vm_dev->pdev = pdev;
INIT_LIST_HEAD(&vm_dev->virtqueues);
spin_lock_init(&vm_dev->lock);

vm_dev->base = devm_ioremap(&pdev->dev, mem->start, resource_size(mem));
if (vm_dev->base == NULL)
return -EFAULT;

/* Check magic value */
magic = readl(vm_dev->base + VIRTIO_MMIO_MAGIC_VALUE);
if (magic != ('v' | 'i' << 8 | 'r' << 16 | 't' << 24)) {
dev_warn(&pdev->dev, "Wrong magic value 0x%08lx!\n", magic);
return -ENODEV;
}

/* Check device version */
vm_dev->version = readl(vm_dev->base + VIRTIO_MMIO_VERSION);
if (vm_dev->version < 1 || vm_dev->version > 2) {
dev_err(&pdev->dev, "Version %ld not supported!\n",
vm_dev->version);
return -ENXIO;
}

vm_dev->vdev.id.device = readl(vm_dev->base + VIRTIO_MMIO_DEVICE_ID);
...
vm_dev->vdev.id.vendor = readl(vm_dev->base + VIRTIO_MMIO_VENDOR_ID);

if (vm_dev->version == 1) {
writel(PAGE_SIZE, vm_dev->base + VIRTIO_MMIO_GUEST_PAGE_SIZE);

rc = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
/*
* In the legacy case, ensure our coherently-allocated virtio
* ring will be at an address expressable as a 32-bit PFN.
*/
if (!rc)
dma_set_coherent_mask(&pdev->dev,
DMA_BIT_MASK(32 + PAGE_SHIFT));
} else {
rc = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
}
if (rc)
rc = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
...
platform_set_drvdata(pdev, vm_dev);

rc = register_virtio_device(&vm_dev->vdev);
...
return rc;
}

register_virtio_device将虚拟设备注册到系统中,对应的总线类型为virtio_bus, 用于设备驱动的匹配与查找;对应的设备名称格式为virtio%u, 比如virtio33/virtio21; 在Linux系统中,我们可以到路径/sys/devices/platform/vdevs中查看当前系统有哪些virtio设备:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

int register_virtio_device(struct virtio_device *dev)
{
int err;

dev->dev.bus = &virtio_bus;
device_initialize(&dev->dev);

/* Assign a unique device index and hence name. */
err = ida_simple_get(&virtio_index_ida, 0, 0, GFP_KERNEL);
if (err < 0)
goto out;

dev->index = err;
dev_set_name(&dev->dev, "virtio%u", dev->index);

spin_lock_init(&dev->config_lock);
dev->config_enabled = false;
dev->config_change_pending = false;

/* We always start by resetting the device, in case a previous
* driver messed it up. This also tests that code path a little. */
dev->config->reset(dev);

/* Acknowledge that we've seen the device. */
virtio_add_status(dev, VIRTIO_CONFIG_S_ACKNOWLEDGE);

INIT_LIST_HEAD(&dev->vqs);

/*
* device_add() causes the bus infrastructure to look for a matching
* driver.
*/
err = device_add(&dev->dev);
...
return err;
}

设备注册完后,虚拟网卡设备驱动程序virtio-net在注册时,会主动匹配与之对应的设备, 如果匹配到则会调用对应驱动的probe函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

static struct virtio_driver virtio_net_driver = {
.feature_table = features,
.feature_table_size = ARRAY_SIZE(features),
.feature_table_legacy = features_legacy,
.feature_table_size_legacy = ARRAY_SIZE(features_legacy),
.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,
#ifdef CONFIG_PM_SLEEP
.freeze = virtnet_freeze,
.restore = virtnet_restore,
#endif
};


static __init int virtio_net_driver_init(void)
{
int ret;

ret = cpuhp_setup_state_multi(CPUHP_AP_ONLINE_DYN, "virtio/net:online",
virtnet_cpu_online,
virtnet_cpu_down_prep);
if (ret < 0)
goto out;
virtionet_online = ret;
ret = cpuhp_setup_state_multi(CPUHP_VIRT_NET_DEAD, "virtio/net:dead",
NULL, virtnet_cpu_dead);
if (ret)
goto err_dead;

ret = register_virtio_driver(&virtio_net_driver);
...
return ret;
}
module_init(virtio_net_driver_init);

虚拟网卡的驱动加载函数virtnet_probe主要做几个事情:

  • 根据宿主机的驱动配置,设置对应的网卡特性,比如是否支持硬件校验计算、TSO、GSO等
  • alloc_etherdev_mq分配协议栈的网络设备对象net_device, 将其注册到内核中register_netdev
  • 通过检查虚拟设备的特性(feature)确认是否打开网络设备某些配置,比如是否支持GSO/TSO
  • init_vqs初始化虚拟网卡的接收与发送队列, 创建对应的virtqueue循环队列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194

static int virtnet_probe(struct virtio_device *vdev)
{
int i, err = -ENOMEM;
struct net_device *dev;
struct virtnet_info *vi;
u16 max_queue_pairs;
int mtu;

/* Find if host supports multiqueue virtio_net device */
err = virtio_cread_feature(vdev, VIRTIO_NET_F_MQ,
struct virtio_net_config,
max_virtqueue_pairs, &max_queue_pairs);

/* We need at least 2 queue's */
if (err || max_queue_pairs < VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MIN ||
max_queue_pairs > VIRTIO_NET_CTRL_MQ_VQ_PAIRS_MAX ||
!virtio_has_feature(vdev, VIRTIO_NET_F_CTRL_VQ))
max_queue_pairs = 1;

/* Allocate ourselves a network device with room for our info */
dev = alloc_etherdev_mq(sizeof(struct virtnet_info), max_queue_pairs);
if (!dev)
return -ENOMEM;

/* 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);

/* Do we support "hardware" checksums? */
if (virtio_has_feature(vdev, VIRTIO_NET_F_CSUM)) {
/* This opens up the world of extra features. */
dev->hw_features |= NETIF_F_HW_CSUM | NETIF_F_SG;
if (csum)
dev->features |= NETIF_F_HW_CSUM | NETIF_F_SG;

if (virtio_has_feature(vdev, VIRTIO_NET_F_GSO)) {
dev->hw_features |= NETIF_F_TSO
| NETIF_F_TSO_ECN | NETIF_F_TSO6;
}
/* Individual feature bits: what can host handle? */
if (virtio_has_feature(vdev, VIRTIO_NET_F_HOST_TSO4))
dev->hw_features |= NETIF_F_TSO;
if (virtio_has_feature(vdev, VIRTIO_NET_F_HOST_TSO6))
dev->hw_features |= NETIF_F_TSO6;
if (virtio_has_feature(vdev, VIRTIO_NET_F_HOST_ECN))
dev->hw_features |= NETIF_F_TSO_ECN;

dev->features |= NETIF_F_GSO_ROBUST;

if (gso)
dev->features |= dev->hw_features & NETIF_F_ALL_TSO;
/* (!csum && gso) case will be fixed by register_netdev() */
}
if (virtio_has_feature(vdev, VIRTIO_NET_F_GUEST_CSUM))
dev->features |= NETIF_F_RXCSUM;
if (virtio_has_feature(vdev, VIRTIO_NET_F_GUEST_TSO4) ||
virtio_has_feature(vdev, VIRTIO_NET_F_GUEST_TSO6))
dev->features |= NETIF_F_GRO_HW;
if (virtio_has_feature(vdev, VIRTIO_NET_F_CTRL_GUEST_OFFLOADS))
dev->hw_features |= NETIF_F_GRO_HW;

dev->vlan_features = dev->features;

/* MTU range: 68 - 65535 */
dev->min_mtu = MIN_MTU;
dev->max_mtu = MAX_MTU;

/* Configuration may specify what MAC to use. Otherwise random. */
if (virtio_has_feature(vdev, VIRTIO_NET_F_MAC))
virtio_cread_bytes(vdev,
offsetof(struct virtio_net_config, mac),
dev->dev_addr, dev->addr_len);
else
eth_hw_addr_random(dev);

/* Set up our device-specific information */
vi = netdev_priv(dev);
vi->dev = dev;
vi->vdev = vdev;
vdev->priv = vi;

INIT_WORK(&vi->config_work, virtnet_config_changed_work);

/* If we can receive ANY GSO packets, we must allocate large ones. */
if (virtio_has_feature(vdev, VIRTIO_NET_F_GUEST_TSO4) ||
virtio_has_feature(vdev, VIRTIO_NET_F_GUEST_TSO6) ||
virtio_has_feature(vdev, VIRTIO_NET_F_GUEST_ECN) ||
virtio_has_feature(vdev, VIRTIO_NET_F_GUEST_UFO))
vi->big_packets = true;

if (virtio_has_feature(vdev, VIRTIO_NET_F_MRG_RXBUF))
vi->mergeable_rx_bufs = true;

if (virtio_has_feature(vdev, VIRTIO_NET_F_MRG_RXBUF) ||
virtio_has_feature(vdev, VIRTIO_F_VERSION_1))
vi->hdr_len = sizeof(struct virtio_net_hdr_mrg_rxbuf);
else
vi->hdr_len = sizeof(struct virtio_net_hdr);

if (virtio_has_feature(vdev, VIRTIO_F_ANY_LAYOUT) ||
virtio_has_feature(vdev, VIRTIO_F_VERSION_1))
vi->any_header_sg = true;

if (virtio_has_feature(vdev, VIRTIO_NET_F_CTRL_VQ))
vi->has_cvq = true;

if (virtio_has_feature(vdev, VIRTIO_NET_F_MTU)) {
mtu = virtio_cread16(vdev,
offsetof(struct virtio_net_config,
mtu));
if (mtu < dev->min_mtu) {
/* Should never trigger: MTU was previously validated
* in virtnet_validate.
*/
dev_err(&vdev->dev,
"device MTU appears to have changed it is now %d < %d",
mtu, dev->min_mtu);
err = -EINVAL;
goto free;
}

dev->mtu = mtu;
dev->max_mtu = mtu;

/* TODO: size buffers correctly in this case. */
if (dev->mtu > ETH_DATA_LEN)
vi->big_packets = true;
}

if (vi->any_header_sg)
dev->needed_headroom = vi->hdr_len;

/* Enable multiqueue by default */
if (num_online_cpus() >= max_queue_pairs)
vi->curr_queue_pairs = max_queue_pairs;
else
vi->curr_queue_pairs = num_online_cpus();
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);

if (virtio_has_feature(vdev, VIRTIO_NET_F_STANDBY)) {
vi->failover = net_failover_create(vi->dev);
if (IS_ERR(vi->failover)) {
err = PTR_ERR(vi->failover);
goto free_vqs;
}
}

err = register_netdev(dev);
...

virtio_device_ready(vdev);

err = virtnet_cpu_notif_add(vi);
...

virtnet_set_queues(vi, vi->curr_queue_pairs);

/* Assume link up if device can't report link status,
otherwise get link status from config. */
netif_carrier_off(dev);
if (virtio_has_feature(vi->vdev, VIRTIO_NET_F_STATUS)) {
schedule_work(&vi->config_work);
} else {
vi->status = VIRTIO_NET_S_LINK_UP;
virtnet_update_settings(vi);
netif_carrier_on(dev);
}

for (i = 0; i < ARRAY_SIZE(guest_offloads); i++)
if (virtio_has_feature(vi->vdev, guest_offloads[i]))
set_bit(guest_offloads[i], &vi->guest_offloads);
vi->guest_offloads_capable = vi->guest_offloads;

pr_debug("virtnet: registered device %s with %d RX and TX vq's\n",
dev->name, max_queue_pairs);

return 0;
...
return err;
}

重点来看看init_vqs,这个函数首先分配用于接发数据的队列,然后调用virtnet_find_vqs来设置每个接发队列的回调,以及通过virtio_mmio的接口配置中断、配置接发缓冲区的内存等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

static int init_vqs(struct virtnet_info *vi)
{
int ret;

/* Allocate send & receive queues */
ret = virtnet_alloc_queues(vi);
if (ret)
goto err;

ret = virtnet_find_vqs(vi);
if (ret)
goto err_free;

get_online_cpus();
virtnet_set_affinity(vi);
put_online_cpus();

return 0;
...
}


virtnet_find_vqs最终会通过vdev->config->find_vqs调用virtio_mmio.c设备的配置操作函数列表virtio_mmio_config_ops->find_vqs, 即vm_find_vqs, 这个函数主要做了如下几个事情:

  • 配置中断request_irq, 这个中断号就是从宿主机QNX端配置的,比如数据发送,数据接收等信号都是通过这个中断发送过来的
  • 为每个接发队列分配足够的环形缓冲区,这个缓冲区的内存位于虚拟机上,分配成功后会通过通过virtio的协议,将对应的缓冲区队列的虚拟地址传递给宿主机QNX端,这样前端驱动就可以与后端进行数据的交互

有关具体的配置细节可以参考vring_create_virtqueue/vring_create_virtqueue这两个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

static int vm_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_mmio_device *vm_dev = to_virtio_mmio_device(vdev);
int irq = platform_get_irq(vm_dev->pdev, 0);
int i, err, queue_idx = 0;

if (irq < 0) {
dev_err(&vdev->dev, "Cannot get IRQ resource\n");
return irq;
}

err = request_irq(irq, vm_interrupt, IRQF_SHARED,
dev_name(&vdev->dev), vm_dev);
if (err)
return err;

if (of_property_read_bool(vm_dev->pdev->dev.of_node, "virtio,wakeup"))
enable_irq_wake(irq);

for (i = 0; i < nvqs; ++i) {
if (!names[i]) {
vqs[i] = NULL;
continue;
}

vqs[i] = vm_setup_vq(vdev, queue_idx++, callbacks[i], names[i],
ctx ? ctx[i] : false);
if (IS_ERR(vqs[i])) {
vm_del_vqs(vdev);
return PTR_ERR(vqs[i]);
}
}

return 0;
}

总结

virtio是虚拟化技术中很常见的一种设备虚拟化方案,相比全模拟的设备虚拟化,virtio效率更高,性能更优;正是因为其在性能上的优势,virtio在QNX, KVM, QEMU等虚拟化方案中都得到了广泛的应用,QNX中的网络, 磁盘等虚拟化设备都是基于该技术方案实现的。目前,virtio的接口已经被OASIS标准化,这样前端的设备驱动开发与后端宿主机的驱动实现了完全的解耦,彼此只需要遵循标准的接口与协议即可相互匹配工作。

这篇文章我们主要以virtio-net虚拟网卡为例来说明virtio的大致原理,感兴趣的同学也可以下载Linux内核代码学习下其他如输入设备、块设备等虚拟设备驱动是如何实现的, 主要代码路径如下:

  • drivers/virtio: virtio的数据传输层代码,包括PCI/MMIO两种设备枚举方式以及vring的实现
  • drivers/net/virtio_net.c: 虚拟网卡设备驱动
  • drivers/block/virtio_blk.c: 块设备驱动

参考资料

原文作者:Jason Wang

更新日期:2023-09-18, 13:30:36

版权声明:本文采用知识共享署名-非商业性使用 4.0 国际许可协议进行许可

CATALOG
  1. 1. virtio的原理
  2. 2. 设备的识别与初始化
  3. 3. 总结
  4. 4. 参考资料