virtio-mmio实现分析
——lvyilong316
前面的文章我们介绍过virtio over mmio的背景和原理,当前virtio-mmio被大量用于轻量级安全容器当中,那么本章我们就重点分析一下virtio-mmio的实现。我们以linux 4.9位例分析。在内核代码中有一个文件:linux-4.9.162\drivers\virtio\virtio_mmio.c,其中就是virtio-mmio。我们分以下三部分介绍:驱动的注册,设备的创建,设备和驱动的绑定。
驱动的注册
首先,我们看virtio-mmio这个内核模块的初始化部分,如下
-
/* Platform driver */
-
-
static struct of_device_id virtio_mmio_match[] = {
-
{ .compatible = "virtio,mmio", },
-
{},
-
};
-
MODULE_DEVICE_TABLE(of, virtio_mmio_match);
-
-
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);
-
}
可以看到,模块定义了一个virtio-mmio的驱动,这个驱动是platform类型的。这里要说明两点:
1. platform是一个伪总线也可以称之为虚拟总线,platform是用来解决如下问题:真实世界的 Linux 设备和驱动程序通常需要挂在总线上,对于挂在PCI、USB、I2C、SPI上的设备来说自然不是问题,但是在嵌入式系统中,SOC系统中集成了独立的外设控制器,SOC内存空间的外设不包含在这个class bus中。基于此背景,Linux 发明了一种称为平台总线的虚拟总线。如SOC 系统中集成的独立外部单元(I2C、LCD、SPI、RTC 等)被视为平台设备。 从Linux2.6开始引入一套新的驱动管理和注册机制:platform_device和platform_driver。大多数Linux都可以使用这种机制,设备用platform_Device表示;驱动程序注册到 platform_Driver。关于platform的更详细介绍可以参考网上相关资料,http://hulc.xyz/2021/11/23/platform_device%E5%92%8Cplatform_driver%E5%8C%B9%E9%85%8D%E8%BF%87%E7%A8%8B%E5%88%86%E6%9E%90/ 这里不再过多介绍;
2. 我们通常说的virtio-mmio是一个驱动,不是一个总线,virtio-mmio是和virtio-pci类似的,是一个驱动,不一样的是virtio-pci对应的总线是pci,而virtio-mmio对应的总线可不是mmio,而是platform,这里内核并没有创建一个mmio总线,虽然设备通信是通过mmio,但并没有这样一个bus,如下图所示。
在执行 platform_driver_register后,便将virtio-mmio驱动注册到了platform bus,如下图所示:
设备的创建
virtio-mmio对应的设备本质上就是一块内存,那么这个内存设备如果创建呢?在virtio-mmio.c的{BANNED}{BANNED}最佳佳前面注释给出了方式:
-
/*
-
* Virtio memory mapped device driver
-
*
-
* Copyright 2011-2014, ARM Ltd.
-
*
-
* This module allows virtio devices to be used over a virtual, memory mapped
-
* platform device.
-
*
-
* The guest device(s) may be instantiated in one of three equivalent ways:
-
*
-
* 1. Static platform device in board's code, eg.:
-
*
-
* 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,
-
* },
-
* }
-
* };
-
*
-
* 2. Device Tree node, eg.:
-
*
-
* virtio_block@1e000 {
-
* compatible = "virtio,mmio";
-
* reg = <0x1e000 0x100>;
-
* interrupts = <42>;
-
* }
-
*
-
* 3. Kernel module (or command line) parameter. Can be used more than once -
-
* one device will be created for each one. Syntax:
-
*
-
* [virtio_mmio.]device=<size>@<baseaddr>:<irq>[:<id>]
-
* where:
-
* <size> := size (can use standard suffixes like K, M or G)
-
* <baseaddr> := physical base address
-
* <irq> := interrupt number (as passed to request_irq())
-
* <id> := (optional) platform device id
-
* eg.:
-
* virtio_mmio.device=0x100@0x100b0000:48 \
-
* virtio_mmio.device=1K@0x1001e000:74
-
*
-
*/
总结就是可以使用三种方式创建:
1. 在内核代码中静态创建platform_device,然后调用platform_device_register;
2. 通过DTS设备树文件指定创建;
3. 通过内核启动参数 command line或模块加载参数指定创建;
而现实中{BANNED}{BANNED}最佳佳常用的就是通过内核启动 command line创建,如下图就是通过command line创建了多个virtio-mmio设备。
以其中标红的一个设备为例,8K表示设备配置空间的大小,@后面表示设备配置空间对应的mmio内存其实地址,再后面表示irq。而解析内核cmdline和创建设备是通过以下函数进行的:
-
static const struct kernel_param_ops vm_cmdline_param_ops = {
-
.set = vm_cmdline_set,
-
.get = vm_cmdline_get,
-
};
-
-
device_param_cb(device, &vm_cmdline_param_ops, NULL, S_IRUSR);
通过device_param_cb注册virtio-mmio的kernel_param_ops中的set函数进行cmdline解析和设备创建,具体就是vm_cmdline_set。
-
static int vm_cmdline_set(const char *device,
-
const struct kernel_param *kp)
-
{
-
int err;
-
struct resource resources[2] = {};
-
char *str;
-
long long int base, size;
-
unsigned int irq;
-
int processed, consumed = 0;
-
struct platform_device *pdev;
-
-
/* Consume "size" part of the command line parameter */
-
size = memparse(device, &str);
-
/* 解析command line中的以virtio-mmio开头的参数 */
-
/* Get "@:[:]" chunks */
-
processed = sscanf(str, "@%lli:%u%n:%d%n",
-
&base, &irq, &consumed,
-
&vm_cmdline_id, &consumed);
-
-
/*
-
* sscanf() must processes at least 2 chunks; also there
-
* must be no extra characters after the last chunk, so
-
* str[consumed] must be '\0'
-
*/
-
if (processed < 2 || str[consumed])
-
return -EINVAL;
-
-
resources[0].flags = IORESOURCE_MEM;
-
resources[0].start = base;
-
resources[0].end = base + size - 1;
-
-
resources[1].flags = IORESOURCE_IRQ;
-
resources[1].start = resources[1].end = irq;
-
-
/* 所有virtio-mmio有一个共同的virtio-mmio-cmdline父设备,如果没有创建则需要创建 */
-
if (!vm_cmdline_parent_registered) {
-
err = device_register(&vm_cmdline_parent);
-
if (err) {
-
pr_err("Failed to register parent device!\n");
-
return err;
-
}
-
vm_cmdline_parent_registered = 1;
-
}
-
-
pr_info("Registering device virtio-mmio.%d at 0x%llx-0x%llx, IRQ %d.\n",
-
vm_cmdline_id,
-
(unsigned long long)resources[0].start,
-
(unsigned long long)resources[0].end,
-
(int)resources[1].start);
-
-
pdev = platform_device_register_resndata(&vm_cmdline_parent,
-
"virtio-mmio", vm_cmdline_id++,
-
resources, ARRAY_SIZE(resources), NULL, 0);
-
if (IS_ERR(pdev))
-
return PTR_ERR(pdev);
-
-
return 0;
-
}
这里通过解析cmdline,创建platform_device设备(注意virtio-mmio设备本质是一个platform_device),并且初始化其对应的BAR空间和IRQ。需要注意的是这里创建了一个名为virtio-mmio-cmdline的父设备,为了方便管理,所有通过comdline创建的virtio-mmio设备都有一个共同的父设备virtio-mmio-cmdline,如下图所示。
{BANNED}{BANNED}最佳佳后就是通过platform_device_register_resndata将设备挂在platform bus上(把device所在的bus设置为platform_bus_type),而这将会触发调用platform bus调用其match函数(platform_match)找到对应的驱动,即virtio-mmio,然后调用驱动的probe函数进行设备初始化,这里就是virtio_mmio_probe,进而完成设备和驱动的绑定。
设备和驱动的绑定
根据上文接收virtio-mmio创建的platform设备,{BANNED}{BANNED}最佳佳终是通过virtio_mmio_probe进行驱动绑定的。
-
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.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);
-
if (vm_dev->vdev.id.device == 0) {
-
/*
-
* virtio-mmio device with an ID 0 is a (dummy) placeholder
-
* with no function. End probing now with no error reported.
-
*/
-
return -ENODEV;
-
}
-
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));
-
if (rc)
-
dev_warn(&pdev->dev, "Failed to enable 64-bit or 32-bit DMA. Trying to continue, but this might not work.\n");
-
-
platform_set_drvdata(pdev, vm_dev);
-
-
return register_virtio_device(&vm_dev->vdev);
-
}
其核心是创建struct virtio_mmio_device,并对其mmio中的设备配置检查合法性,{BANNED}{BANNED}最佳佳后调用 register_virtio_device。virtio_mmio_device设备结构如下,
-
struct virtio_mmio_device {
-
struct virtio_device vdev;
-
struct platform_device *pdev;
-
-
void __iomem *base;
-
unsigned long version;
-
-
/* a list of queues so we can dispatch IRQs */
-
spinlock_t lock;
-
struct list_head virtqueues;
-
};
包含virtio_device和platform_device两部分,这里virtio_mmio_probe是对platform_device部分进行初始化,{BANNED}{BANNED}最佳佳后调用的register_virtio_device则是对virtio_device的初始化,相关过程的类比virtio-pci会发现是一样的,区别是pci部分的设备初始化换成了platform_device的初始化。初始化platform_device的关键是设置了设备操作函数virtio_mmio_config_ops。
-
static const struct virtio_config_ops virtio_mmio_config_ops = {
-
.get = vm_get,
-
.set = vm_set,
-
.generation = vm_generation,
-
.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,
-
};
和pci设备读取pci配置空间或BAR空间不同的时这里是读取mmio空间,其layout是按照virtio mmio设置的,如get_features的实现如下:
-
static u64 vm_get_features(struct virtio_device *vdev)
-
{
-
struct virtio_mmio_device *vm_dev = to_virtio_mmio_device(vdev);
-
u64 features;
-
-
writel(1, vm_dev->base + VIRTIO_MMIO_DEVICE_FEATURES_SEL);
-
features = readl(vm_dev->base + VIRTIO_MMIO_DEVICE_FEATURES);
-
features <<= 32;
-
-
writel(0, vm_dev->base + VIRTIO_MMIO_DEVICE_FEATURES_SEL);
-
features |= readl(vm_dev->base + VIRTIO_MMIO_DEVICE_FEATURES);
-
-
return features;
-
}
驱动绑定后其设备驱动关系如下图所示。
virtio-mmio热插设备的支持
virtio-mmio之前我们介绍过是不支持热插拔的,需要通过类似cmdline静态初始化。不过现在qemu社区也在做热插拔的支持探索,其中一个思路是:利用GED,在热插入的时候向内核发送中断,内核会在收到中断后执行ged设计好的aml代码段,在每个mmio slot 检测是否有新的硬件,有的话调用virtio_mmio_probe函数就可以完成热插拔。这里Generic Event Device(GED)和ACPI Machine Lanague(AML)都是ACPI中的概念,详细过程不再展开,
小结
总之,virtio-mmio就是利用linux platform虚拟bus,借助mmio机制实现的一个驱动,整体过程类比virtio-pci就非常清晰了,这里核心是设备的创建的理解。由于不再是一个真实的pci设备,而是一块mmio内存,需要有地方指定这块内存,一般通常使用内核cmdline进行静态指定。
阅读(1713) | 评论(0) | 转发(0) |