1. Virtio-MMIO 设备概述
在虚拟化技术中,virtio 是一种半虚拟化标准协议,它定义了虚拟机与宿主机之间高效通信的机制。传统上,virtio 设备大多基于 PCI 总线实现,称为 virtio-pci 设备。然而,virtio 规范还支持另外两种总线实现方式:virtio over MMIO(内存映射 I/O)和 virtio over channel I/O。
1.1 为什么需要 MMIO 实现
virtio 本质上是一个协议标准,其实现并不依赖于特定的总线协议。CPU 与外设之间的通知机制以及外设访问内存的方式,实际上由连接 CPU 与外设的总线协议决定。在虚拟化场景中,主要采用两种总线协议:
-
PCI 总线协议:采用这种协议的 virtio 设备称为 virtio-pci 设备。它的优势在于:
- 通用性强,可以支持设备热插拔(利用 PCI 总线的热插拔机制)
- 可以应用于真实物理外设,虚拟机无法区分设备是虚拟还是物理的
- 缺点是实现复杂,涉及 PCI 主桥模拟、配置空间和中断分配等复杂逻辑
-
MMIO 总线协议:采用内存映射 I/O 方式实现的 virtio 设备称为 virtio-mmio 设备。它的特点是:
- 专为虚拟机设计,是一种轻量级的虚拟总线机制
- 支持快速设备发现,但无法用于真实物理外设
- 实现简单,适合轻量级虚拟化场景(如 AWS 的 Firecracker)
提示:MMIO 实现之所以简单,是因为它绕过了复杂的 PCI 枚举过程,直接通过内存映射方式访问设备寄存器,特别适合对启动速度要求高的轻量级虚拟化场景。
2. Virtio-MMIO 与 Virtio-PCI 的关键差异
2.1 设备发现机制
virtio-mmio 设备无法像 PCI 设备那样支持动态发现,guest OS 需要预先知道设备使用的具体资源:
- 静态资源配置:必须预先知道 MMIO 设备使用的内存地址范围和中断号
- 配置方式:
- 通过内核启动命令行参数:每个 mmio 设备通过虚拟机内核启动时的命令行参数上报资源信息
- 通过设备树(DTS)方式:静态定义设备资源,如下示例:
dts复制virtio-block@130000 {
compatible = "virtio,mmio";
reg = <0x130000 0x200>; // 内存映射区域
interrupts = <42>; // 使用的中断号
}
2.2 寄存器布局
virtio-mmio 设备需要实现一组寄存器来存放设备配置信息,类似于 PCI 设备的配置空间。关键寄存器包括:
| 寄存器偏移 | 名称 | 描述 | 必需值 |
|---|---|---|---|
| 0x000 | MagicValue | 设备标识 | 0x74726976 ("virt") |
| 0x004 | Version | 设备版本 | 0x1 或 0x2 |
| 0x008 | DeviceID | 设备类型ID | 如 0x1(网络设备) |
| 0x00c | VendorID | 厂商ID | - |
| 0x010 | HostFeatures | 主机支持的特性 | - |
| 0x014 | HostFeaturesSel | 主机特性选择 | - |
| 0x020 | GuestFeatures | Guest启用的特性 | - |
| 0x024 | GuestFeaturesSel | Guest特性选择 | - |
| 0x028 | GuestPageSize | 页大小(仅v1) | - |
| 0x030 | QueueSel | 队列选择 | - |
| 0x034 | QueueNumMax | 队列最大支持项数 | - |
| 0x038 | QueueNum | 队列实际项数 | - |
| 0x044 | QueueReady | 队列就绪标志(v2) | - |
| 0x050 | QueueNotify | 队列通知 | - |
| 0x060 | InterruptStatus | 中断状态 | - |
| 0x064 | InterruptACK | 中断确认 | - |
| 0x070 | Status | 设备状态 | - |
注意:驱动必须首先检查 MagicValue 和 Version 寄存器的值是否正确,才能继续后续的设备初始化和配置。
3. Virtio-MMIO 的实现架构
3.1 设备创建方式
virtio-mmio 设备可以通过三种方式创建:
- 内核静态创建:在内核代码中定义 platform_device 结构并注册
c复制static struct platform_device v2m_virtio_device = {
.name = "virtio-mmio",
.id = -1,
.num_resources = 2,
.resource = (struct resource []) {
{
.start = 0x1001e000,
.end = 0x1001e0ff,
.flags = IORESOURCE_MEM,
}, {
.start = 42 + 32,
.end = 42 + 32,
.flags = IORESOURCE_IRQ,
},
}
};
- 设备树定义:通过 DTS 文件静态描述设备资源
dts复制virtio_block@1e000 {
compatible = "virtio,mmio";
reg = <0x1e000 0x100>;
interrupts = <42>;
}
- 命令行参数:通过内核启动参数动态创建
code复制virtio_mmio.device=4k@0x12003400:12
参数格式:[virtio_mmio.]device=<size>@<baseaddr>:<irq>[:<id>]
3.2 驱动注册流程
virtio-mmio 实际上是一个 platform 驱动,而不是一种独立的总线类型。驱动注册主要流程:
c复制static struct platform_driver virtio_mmio_driver = {
.probe = virtio_mmio_probe,
.remove = virtio_mmio_remove,
.driver = {
.name = "virtio-mmio",
.of_match_table = virtio_mmio_match,
.acpi_match_table = ACPI_PTR(virtio_mmio_acpi_match),
},
};
static int __init virtio_mmio_init(void)
{
return platform_driver_register(&virtio_mmio_driver);
}
注册后,可以在 /sys/bus/platform/drivers/ 下看到 virtio-mmio 驱动。
4. Virtio-MMIO 核心实现解析
4.1 设备数据结构
c复制struct virtio_mmio_device {
struct virtio_device vdev; // 基础virtio设备
struct platform_device *pdev; // 关联的platform设备
void __iomem *base; // MMIO寄存器基地址
unsigned long version; // 设备版本(1或2)
/* 用于管理virtqueue的链表和锁 */
spinlock_t lock;
struct list_head virtqueues;
};
4.2 设备探测流程
virtio_mmio_probe() 是驱动初始化的核心函数,主要步骤:
- 获取并映射 MMIO 区域
- 检查 MagicValue 和 Version 寄存器
- 读取 DeviceID 和 VendorID
- 配置 DMA 掩码
- 注册 virtio 设备
c复制static int virtio_mmio_probe(struct platform_device *pdev)
{
/* 获取资源并映射寄存器 */
mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
vm_dev->base = devm_ioremap(&pdev->dev, mem->start, resource_size(mem));
/* 验证设备标识 */
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;
}
/* 检查设备版本 */
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;
}
/* 读取设备ID和厂商ID */
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);
/* 配置DMA掩码 */
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));
/* ... */
} else {
rc = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
}
/* 注册virtio设备 */
rc = register_virtio_device(&vm_dev->vdev);
}
4.3 配置操作集
virtio-mmio 实现了 virtio_config_ops 结构体中的各种操作:
c复制static const struct virtio_config_ops virtio_mmio_config_ops = {
.get = vm_get,
.set = vm_set,
.get_status = vm_get_status,
.set_status = vm_set_status,
.reset = vm_reset,
.find_vqs = vm_find_vqs,
.del_vqs = vm_del_vqs,
.get_features = vm_get_features,
.finalize_features = vm_finalize_features,
.bus_name = vm_bus_name,
};
4.3.1 配置空间访问
c复制static void vm_get(struct virtio_device *vdev, unsigned offset,
void *buf, unsigned len)
{
struct virtio_mmio_device *vm_dev = to_virtio_mmio_device(vdev);
void __iomem *base = vm_dev->base + VIRTIO_MMIO_CONFIG;
switch (len) {
case 1:
b = readb(base + offset);
memcpy(buf, &b, sizeof b);
break;
case 2:
w = cpu_to_le16(readw(base + offset));
memcpy(buf, &w, sizeof w);
break;
/* ... */
}
}
4.3.2 状态管理
c复制static u8 vm_get_status(struct virtio_device *vdev)
{
struct virtio_mmio_device *vm_dev = to_virtio_mmio_device(vdev);
return readl(vm_dev->base + VIRTIO_MMIO_STATUS) & 0xff;
}
static void vm_set_status(struct virtio_device *vdev, u8 status)
{
/* 不能设置状态为0 */
BUG_ON(status == 0);
writel(status, vm_dev->base + VIRTIO_MMIO_STATUS);
}
static void vm_reset(struct virtio_device *vdev)
{
/* 写0表示复位设备 */
writel(0, vm_dev->base + VIRTIO_MMIO_STATUS);
}
4.4 Virtqueue 管理
4.4.1 Virtqueue 创建
vm_find_vqs() 是创建 virtqueue 的入口函数:
c复制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)
{
/* 申请中断 */
irq = platform_get_irq(vm_dev->pdev, 0);
err = request_irq(irq, vm_interrupt, IRQF_SHARED,
dev_name(&vdev->dev), vm_dev);
/* 为每个队列调用vm_setup_vq */
for (i = 0; i < nvqs; ++i) {
vqs[i] = vm_setup_vq(vdev, queue_idx++, callbacks[i], names[i],
ctx ? ctx[i] : false);
/* ... */
}
}
vm_setup_vq() 负责单个队列的初始化:
c复制static struct virtqueue *vm_setup_vq(struct virtio_device *vdev, unsigned index,
void (*callback)(struct virtqueue *vq),
const char *name, bool ctx)
{
/* 选择当前队列 */
writel(index, vm_dev->base + VIRTIO_MMIO_QUEUE_SEL);
/* 检查队列是否已设置 */
if (readl(vm_dev->base + (vm_dev->version == 1 ?
VIRTIO_MMIO_QUEUE_PFN : VIRTIO_MMIO_QUEUE_READY))) {
return ERR_PTR(-ENOENT);
}
/* 获取队列最大长度 */
num = readl(vm_dev->base + VIRTIO_MMIO_QUEUE_NUM_MAX);
/* 创建vring */
vq = vring_create_virtqueue(index, num, VIRTIO_MMIO_VRING_ALIGN, vdev,
true, true, ctx, vm_notify, callback, name);
/* 激活队列 */
writel(virtqueue_get_vring_size(vq), vm_dev->base + VIRTIO_MMIO_QUEUE_NUM);
if (vm_dev->version == 1) {
/* v1使用32位PFN */
u64 q_pfn = virtqueue_get_desc_addr(vq) >> PAGE_SHIFT;
writel(q_pfn, vm_dev->base + VIRTIO_MMIO_QUEUE_PFN);
} else {
/* v2分别设置描述符、可用和已用环地址 */
addr = virtqueue_get_desc_addr(vq);
writel((u32)addr, vm_dev->base + VIRTIO_MMIO_QUEUE_DESC_LOW);
writel((u32)(addr >> 32), vm_dev->base + VIRTIO_MMIO_QUEUE_DESC_HIGH);
/* ... 设置可用和已用环 ... */
writel(1, vm_dev->base + VIRTIO_MMIO_QUEUE_READY);
}
/* 将队列添加到设备队列列表 */
spin_lock_irqsave(&vm_dev->lock, flags);
list_add(&info->node, &vm_dev->virtqueues);
spin_unlock_irqrestore(&vm_dev->lock, flags);
return vq;
}
4.4.2 中断处理
c复制static irqreturn_t vm_interrupt(int irq, void *opaque)
{
/* 读取并确认中断状态 */
status = readl(vm_dev->base + VIRTIO_MMIO_INTERRUPT_STATUS);
writel(status, vm_dev->base + VIRTIO_MMIO_INTERRUPT_ACK);
/* 配置变更中断 */
if (unlikely(status & VIRTIO_MMIO_INT_CONFIG)) {
virtio_config_changed(&vm_dev->vdev);
ret = IRQ_HANDLED;
}
/* 队列中断 */
if (likely(status & VIRTIO_MMIO_INT_VRING)) {
spin_lock_irqsave(&vm_dev->lock, flags);
list_for_each_entry(info, &vm_dev->virtqueues, node)
ret |= vring_interrupt(irq, info->vq);
spin_unlock_irqrestore(&vm_dev->lock, flags);
}
return ret;
}
4.4.3 队列通知
c复制static bool vm_notify(struct virtqueue *vq)
{
struct virtio_mmio_device *vm_dev = to_virtio_mmio_device(vq->vdev);
/* 写入队列索引到通知寄存器 */
writel(vq->index, vm_dev->base + VIRTIO_MMIO_QUEUE_NOTIFY);
return true;
}
4.4.4 队列释放
c复制static void vm_del_vq(struct virtqueue *vq)
{
/* 从队列列表中移除 */
spin_lock_irqsave(&vm_dev->lock, flags);
list_del(&info->node);
spin_unlock_irqrestore(&vm_dev->lock, flags);
/* 停用队列 */
writel(index, vm_dev->base + VIRTIO_MMIO_QUEUE_SEL);
if (vm_dev->version == 1) {
writel(0, vm_dev->base + VIRTIO_MMIO_QUEUE_PFN);
} else {
writel(0, vm_dev->base + VIRTIO_MMIO_QUEUE_READY);
}
/* 释放vring和相关资源 */
vring_del_virtqueue(vq);
kfree(info);
}
5. 实践经验与性能考量
5.1 版本选择建议
virtio-mmio 有两个版本实现:
-
版本1:
- 使用单个32位PFN(页帧号)指向队列
- 限制:队列内存必须位于低4GB地址空间
- 需要设置GUEST_PAGE_SIZE
-
版本2:
- 使用64位地址分别指向描述符表、可用环和已用环
- 支持更高的内存地址
- 增加了QUEUE_READY寄存器明确指示队列状态
建议:新实现应优先使用版本2,它解决了版本1的地址空间限制问题,并且状态控制更加明确。
5.2 性能优化技巧
-
批处理通知:对于高吞吐量场景,可以实现批处理通知机制,减少MMIO写入次数。
-
中断合并:配置合适的中断合并参数,避免中断风暴。
-
队列大小:根据实际负载调整队列大小(vring项数),太大会浪费内存,太小会增加通知开销。
-
DMA优化:确保vring和描述符使用的内存是DMA友好的,避免不必要的缓存同步。
5.3 常见问题排查
-
设备未识别:
- 检查MagicValue和Version寄存器是否正确
- 确认MMIO区域映射正确
- 验证中断号配置是否正确
-
队列无法工作:
- 检查QUEUE_READY/PFN寄存器是否已正确设置
- 确认vring地址是否正确对齐
- 验证通知机制是否正常工作
-
性能低下:
- 检查是否启用了IOMMU或SMMU,可能导致额外的地址转换开销
- 确认是否使用了适当的中断亲和性设置
- 检查NUMA配置,确保设备和CPU在同一个NUMA节点
6. 实际应用场景
virtio-mmio 特别适合以下场景:
-
轻量级虚拟化:如容器运行时(Firecracker)等需要快速启动的环境。
-
嵌入式虚拟化:资源受限的设备,不需要完整的PCI模拟。
-
特定硬件加速:为特定加速器提供简单的virtio接口。
-
测试和原型开发:快速实现virtio设备原型,无需复杂的PCI模拟。
在AWS Firecracker等轻量级VMM中,virtio-mmio是默认的设备模拟方式,因为它提供了足够的功能同时保持了极简的设计和快速的启动时间。
