Chinaunix首页 | 论坛 | 博客
  • 博客访问: 3569442
  • 博文数量: 205
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 7385
  • 用 户 组: 普通用户
  • 注册时间: 2013-01-23 18:56
个人简介

将晦涩难懂的技术讲的通俗易懂

文章分类

全部博文(205)

文章存档

2024年(8)

2023年(9)

2022年(4)

2021年(12)

2020年(8)

2019年(18)

2018年(19)

2017年(9)

2016年(26)

2015年(18)

2014年(54)

2013年(20)

分类: LINUX

2023-08-19 21:29:54

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这个内核模块的初始化部分,如下


点击(此处)折叠或打开

  1. /* Platform driver */

  2. static struct of_device_id virtio_mmio_match[] = {
  3.     { .compatible = "virtio,mmio", },
  4.     {},
  5. };
  6. MODULE_DEVICE_TABLE(of, virtio_mmio_match);

  7. static struct platform_driver virtio_mmio_driver = {
  8.     .probe        = virtio_mmio_probe,
  9.     .remove        = virtio_mmio_remove,
  10.     .driver        = {
  11.         .name    = "virtio-mmio",
  12.         .of_match_table    = virtio_mmio_match,
  13.         .acpi_match_table = ACPI_PTR(virtio_mmio_acpi_match),
  14.     },
  15. };

  16. static int __init virtio_mmio_init(void)
  17. {
  18.     return platform_driver_register(&virtio_mmio_driver);
  19. }


   可以看到,模块定义了一个virtio-mmio的驱动,这个驱动是platform类型的。这里要说明两点:

1. platform是一个伪总线也可以称之为虚拟总线,platform是用来解决如下问题:真实世界的 Linux 设备和驱动程序通常需要挂在总线上,对于挂在PCIUSBI2CSPI上的设备来说自然不是问题,但是在嵌入式系统中,SOC系统中集成了独立的外设控制器,SOC内存空间的外设不包含在这个class bus中。基于此背景,Linux 发明了一种称为平台总线的虚拟总线。如SOC 系统中集成的独立外部单元(I2CLCDSPIRTC 等)被视为平台设备。 从Linux2.6开始引入一套新的驱动管理和注册机制:platform_deviceplatform_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}最佳佳前面注释给出了方式:


点击(此处)折叠或打开

  1. /*
  2.  * Virtio memory mapped device driver
  3.  *
  4.  * Copyright 2011-2014, ARM Ltd.
  5.  *
  6.  * This module allows virtio devices to be used over a virtual, memory mapped
  7.  * platform device.
  8.  *
  9.  * The guest device(s) may be instantiated in one of three equivalent ways:
  10.  *
  11.  * 1. Static platform device in board's code, eg.:
  12.  *
  13.  *    static struct platform_device v2m_virtio_device = {
  14.  *        .name = "virtio-mmio",
  15.  *        .id = -1,
  16.  *        .num_resources = 2,
  17.  *        .resource = (struct resource []) {
  18.  *            {
  19.  *                .start = 0x1001e000,
  20.  *                .end = 0x1001e0ff,
  21.  *                .flags = IORESOURCE_MEM,
  22.  *            }, {
  23.  *                .start = 42 + 32,
  24.  *                .end = 42 + 32,
  25.  *                .flags = IORESOURCE_IRQ,
  26.  *            },
  27.  *        }
  28.  *    };
  29.  *
  30.  * 2. Device Tree node, eg.:
  31.  *
  32.  *        virtio_block@1e000 {
  33.  *            compatible = "virtio,mmio";
  34.  *            reg = <0x1e000 0x100>;
  35.  *            interrupts = <42>;
  36.  *        }
  37.  *
  38.  * 3. Kernel module (or command line) parameter. Can be used more than once -
  39.  * one device will be created for each one. Syntax:
  40.  *
  41.  *        [virtio_mmio.]device=<size>@<baseaddr>:<irq>[:<id>]
  42.  * where:
  43.  *        <size> := size (can use standard suffixes like K, M or G)
  44.  *        <baseaddr> := physical base address
  45.  *        <irq> := interrupt number (as passed to request_irq())
  46.  *        <id> := (optional) platform device id
  47.  * eg.:
  48.  *        virtio_mmio.device=0x100@0x100b0000:48 \
  49.  *                virtio_mmio.device=1K@0x1001e000:74
  50.  *
  51.  */


总结就是可以使用三种方式创建:

1. 在内核代码中静态创建platform_device,然后调用platform_device_register

2. 通过DTS设备树文件指定创建;

3. 通过内核启动参数 command line或模块加载参数指定创建;

而现实中{BANNED}{BANNED}最佳佳常用的就是通过内核启动 command line创建,如下图就是通过command line创建了多个virtio-mmio设备。

 

以其中标红的一个设备为例,8K表示设备配置空间的大小,@后面表示设备配置空间对应的mmio内存其实地址,再后面表示irq。而解析内核cmdline和创建设备是通过以下函数进行的:

点击(此处)折叠或打开

  1. static const struct kernel_param_ops vm_cmdline_param_ops = {
  2.     .set = vm_cmdline_set,
  3.     .get = vm_cmdline_get,
  4. };

  5. device_param_cb(device, &vm_cmdline_param_ops, NULL, S_IRUSR);


通过device_param_cb注册virtio-mmiokernel_param_ops中的set函数进行cmdline解析和设备创建,具体就是vm_cmdline_set


点击(此处)折叠或打开

  1. static int vm_cmdline_set(const char *device,
  2.         const struct kernel_param *kp)
  3. {
  4.     int err;
  5.     struct resource resources[2] = {};
  6.     char *str;
  7.     long long int base, size;
  8.     unsigned int irq;
  9.     int processed, consumed = 0;
  10.     struct platform_device *pdev;

  11.     /* Consume "size" part of the command line parameter */
  12.     size = memparse(device, &str);
  13.     /* 解析command line中的以virtio-mmio开头的参数 */
  14.     /* Get "@:[:]" chunks */
  15.     processed = sscanf(str, "@%lli:%u%n:%d%n",
  16.             &base, &irq, &consumed,
  17.             &vm_cmdline_id, &consumed);

  18.     /*
  19.      * sscanf() must processes at least 2 chunks; also there
  20.      * must be no extra characters after the last chunk, so
  21.      * str[consumed] must be '\0'
  22.      */
  23.     if (processed < 2 || str[consumed])
  24.         return -EINVAL;

  25.     resources[0].flags = IORESOURCE_MEM;
  26.     resources[0].start = base;
  27.     resources[0].end = base + size - 1;

  28.     resources[1].flags = IORESOURCE_IRQ;
  29.     resources[1].start = resources[1].end = irq;

  30.     /* 所有virtio-mmio有一个共同的virtio-mmio-cmdline父设备,如果没有创建则需要创建 */
  31.     if (!vm_cmdline_parent_registered) {
  32.         err = device_register(&vm_cmdline_parent);
  33.         if (err) {
  34.             pr_err("Failed to register parent device!\n");
  35.             return err;
  36.         }
  37.         vm_cmdline_parent_registered = 1;
  38.     }

  39.     pr_info("Registering device virtio-mmio.%d at 0x%llx-0x%llx, IRQ %d.\n",
  40.          vm_cmdline_id,
  41.          (unsigned long long)resources[0].start,
  42.          (unsigned long long)resources[0].end,
  43.          (int)resources[1].start);

  44.     pdev = platform_device_register_resndata(&vm_cmdline_parent,
  45.             "virtio-mmio", vm_cmdline_id++,
  46.             resources, ARRAY_SIZE(resources), NULL, 0);
  47.     if (IS_ERR(pdev))
  48.         return PTR_ERR(pdev);

  49.     return 0;
  50. }


  这里通过解析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进行驱动绑定的。


点击(此处)折叠或打开

  1. static int virtio_mmio_probe(struct platform_device *pdev)
  2. {
  3.     struct virtio_mmio_device *vm_dev;
  4.     struct resource *mem;
  5.     unsigned long magic;
  6.     int rc;

  7.     mem = platform_get_resource(pdev, IORESOURCE_MEM, 0);
  8.     if (!mem)
  9.         return -EINVAL;

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

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

  16.     vm_dev->vdev.dev.parent = &pdev->dev;
  17.     vm_dev->vdev.config = &virtio_mmio_config_ops;
  18.     vm_dev->pdev = pdev;
  19.     INIT_LIST_HEAD(&vm_dev->virtqueues);
  20.     spin_lock_init(&vm_dev->lock);

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

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

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

  37.     vm_dev->vdev.id.device = readl(vm_dev->base + VIRTIO_MMIO_DEVICE_ID);
  38.     if (vm_dev->vdev.id.device == 0) {
  39.         /*
  40.          * virtio-mmio device with an ID 0 is a (dummy) placeholder
  41.          * with no function. End probing now with no error reported.
  42.          */
  43.         return -ENODEV;
  44.     }
  45.     vm_dev->vdev.id.vendor = readl(vm_dev->base + VIRTIO_MMIO_VENDOR_ID);

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

  48.         rc = dma_set_mask(&pdev->dev, DMA_BIT_MASK(64));
  49.         /*
  50.          * In the legacy case, ensure our coherently-allocated virtio
  51.          * ring will be at an address expressable as a 32-bit PFN.
  52.          */
  53.         if (!rc)
  54.             dma_set_coherent_mask(&pdev->dev,
  55.                      DMA_BIT_MASK(32 + PAGE_SHIFT));
  56.     } else {
  57.         rc = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64));
  58.     }
  59.     if (rc)
  60.         rc = dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32));
  61.     if (rc)
  62.         dev_warn(&pdev->dev, "Failed to enable 64-bit or 32-bit DMA. Trying to continue, but this might not work.\n");

  63.     platform_set_drvdata(pdev, vm_dev);

  64.     return register_virtio_device(&vm_dev->vdev);
  65. }


   其核心是创建struct virtio_mmio_device,并对其mmio中的设备配置检查合法性,{BANNED}{BANNED}最佳佳后调用 register_virtio_devicevirtio_mmio_device设备结构如下,


点击(此处)折叠或打开

  1. struct virtio_mmio_device {
  2.     struct virtio_device vdev;
  3.     struct platform_device *pdev;

  4.     void __iomem *base;
  5.     unsigned long version;

  6.     /* a list of queues so we can dispatch IRQs */
  7.     spinlock_t lock;
  8.     struct list_head virtqueues;
  9. };


包含virtio_deviceplatform_device两部分,这里virtio_mmio_probe是对platform_device部分进行初始化,{BANNED}{BANNED}最佳佳后调用的register_virtio_device则是对virtio_device的初始化,相关过程的类比virtio-pci会发现是一样的,区别是pci部分的设备初始化换成了platform_device的初始化。初始化platform_device的关键是设置了设备操作函数virtio_mmio_config_ops


点击(此处)折叠或打开

  1. static const struct virtio_config_ops virtio_mmio_config_ops = {
  2.     .get        = vm_get,
  3.     .set        = vm_set,
  4.     .generation    = vm_generation,
  5.     .get_status    = vm_get_status,
  6.     .set_status    = vm_set_status,
  7.     .reset        = vm_reset,
  8.     .find_vqs    = vm_find_vqs,
  9.     .del_vqs    = vm_del_vqs,
  10.     .get_features    = vm_get_features,
  11.     .finalize_features = vm_finalize_features,
  12.     .bus_name    = vm_bus_name,
  13. };


pci设备读取pci配置空间或BAR空间不同的时这里是读取mmio空间,其layout是按照virtio mmio设置的,如get_features的实现如下:


点击(此处)折叠或打开

  1. static u64 vm_get_features(struct virtio_device *vdev)
  2. {
  3.     struct virtio_mmio_device *vm_dev = to_virtio_mmio_device(vdev);
  4.     u64 features;

  5.     writel(1, vm_dev->base + VIRTIO_MMIO_DEVICE_FEATURES_SEL);
  6.     features = readl(vm_dev->base + VIRTIO_MMIO_DEVICE_FEATURES);
  7.     features <<= 32;

  8.     writel(0, vm_dev->base + VIRTIO_MMIO_DEVICE_FEATURES_SEL);
  9.     features |= readl(vm_dev->base + VIRTIO_MMIO_DEVICE_FEATURES);

  10.     return features;
  11. }


    驱动绑定后其设备驱动关系如下图所示。

 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) |
给主人留下些什么吧!~~