深入了解iommu系列二:iommu工作原理解析之dmaremapping

Linux阅码场 2022-03-04 08:00

序言

上一篇文章主要写了一下iommu硬件架构以及其在驱动层的初始化流程,那这一篇我们会花点时间来详细介绍一下iommu分别在虚拟化和非虚拟化场景下 dma remapping的工作原理

非虚拟化场景

在非虚拟化场景下如果不是显示的在cmdline里面把iommu设置为disabled则一般都会默认enable iommu硬件,另外还有一种常用的方式就是iommu=pt这种方式跟default设置的唯一区别是会不会提前建立static map。为了让大家有个更清晰的认知,我们在内核里面找一段dma操作相关的代码并以此为开端对iommu的dma工作机制进行剖析。

static int e100_xmit_prepare(struct nic *nic, struct cb *cb,
        struct sk_buff *skb)
{
        dma_addr_t dma_addr;
        cb->command = nic->tx_command;

        dma_addr = pci_map_single(nic->pdev,
                                  skb->data, skb->len, PCI_DMA_TODEVICE);
        /* If we can't map the skb, have the upper layer try later */
        if (pci_dma_mapping_error(nic->pdev, dma_addr)) {
                dev_kfree_skb_any(skb);
                skb = NULL;
                return -ENOMEM;
        }
  
  .........
}

上面的代码片断是取自内核e100网卡驱动,核心逻辑就是先把skb的地址做一下dma map,然后让硬件可以直接dma这段数据,其中使用的pci_map_single这个函数最终会调到

static inline dma_addr_t dma_map_single_attrs(struct device *dev, void *ptr,
                                              size_t size,
                                              enum dma_data_direction dir,
                                              unsigned long attrs)
{
        struct dma_map_ops *ops = get_dma_ops(dev);
        dma_addr_t addr;

        kmemcheck_mark_initialized(ptr, size);
        BUG_ON(!valid_dma_direction(dir));
        addr = ops->map_page(dev, virt_to_page(ptr),
                             offset_in_page(ptr), size,
                             dir, attrs);
        debug_dma_map_page(dev, virt_to_page(ptr),
                           offset_in_page(ptr), size,
                           dir, addr, true);
        return addr;
}

如果有iommu硬件的情况下(以intel iommu为例)他最终会走到intel_map_page,下面我们来看一下这个函数的核心逻辑

static dma_addr_t __intel_map_single(struct device *dev, phys_addr_t paddr,
                                     size_t size, int dir, u64 dma_mask)
{
        struct dmar_domain *domain;
        phys_addr_t start_paddr;
        unsigned long iova_pfn;
        int prot = 0;
        int ret;
        struct intel_iommu *iommu;
        unsigned long paddr_pfn = paddr >> PAGE_SHIFT;

        BUG_ON(dir == DMA_NONE);

        if (iommu_no_mapping(dev))
                return paddr;

        domain = get_valid_domain_for_dev(dev);
        if (!domain)
                return 0;

        iommu = domain_get_iommu(domain);
        size = aligned_nrpages(paddr, size);

        iova_pfn = intel_alloc_iova(dev, domain, dma_to_mm_pfn(size), dma_mask);
        if (!iova_pfn)
                goto error;

        /*
         * Check if DMAR supports zero-length reads on write only
         * mappings..
         */
        if (dir == DMA_TO_DEVICE || dir == DMA_BIDIRECTIONAL || \
                        !cap_zlr(iommu->cap))
                prot |= DMA_PTE_READ;
        if (dir == DMA_FROM_DEVICE || dir == DMA_BIDIRECTIONAL)
                prot |= DMA_PTE_WRITE;
        /*
         * paddr - (paddr + size) might be partial page, we should map the whole
         * page.  Note: if two part of one page are separately mapped, we
         * might have two guest_addr mapping to the same host paddr, but this
         * is not a big problem
         */
   ret = domain_pfn_mapping(domain, mm_to_dma_pfn(iova_pfn),
                                 mm_to_dma_pfn(paddr_pfn), size, prot);
        if (ret)
                goto error;

        /* it's a non-present to present mapping. Only flush if caching mode */
        if (cap_caching_mode(iommu->cap))
                iommu_flush_iotlb_psi(iommu, domain,
                                      mm_to_dma_pfn(iova_pfn),
                                      size, 0, 1);
        else
                iommu_flush_write_buffer(iommu);

        start_paddr = (phys_addr_t)iova_pfn << PAGE_SHIFT;
        start_paddr += paddr & ~PAGE_MASK;
        return start_paddr;

error:
        if (iova_pfn)
                free_iova_fast(&domain->iovad, iova_pfn, dma_to_mm_pfn(size));
        pr_err("Device %s request: %zx@%llx dir %d --- failed\n",
                dev_name(dev), size, (unsigned long long)paddr, dir);
        return 0;
}

我们来好好走读一下这个函数,首先会判断是否为iommu_no_mapping,如果是则直接返回paddr也即物理地址。再来看看iommu_no_mapping这个函数的具体逻辑。

static int iommu_no_mapping(struct device *dev)
{
        int found;

        if (iommu_dummy(dev))
                return 1;

        if (!iommu_identity_mapping)
                return 0;

        found = identity_mapping(dev);
        if (found) {
                if (iommu_should_identity_map(dev, 0))
                        return 1;
                else {
                        /*
                         * 32 bit DMA is removed from si_domain and fall back
                         * to non-identity mapping.
                         */
                        dmar_remove_one_dev_info(si_domain, dev);
                        pr_info("32bit %s uses non-identity mapping\n",
                                dev_name(dev));
                        return 0;
                }
        } else {
                /*
                 * In case of a detached 64 bit DMA device from vm, the device
                 * is put into si_domain for identity mapping.
                 */
                if (iommu_should_identity_map(dev, 0)) {
                        int ret;
                        ret = domain_add_dev_info(si_domain, dev);
                        if (!ret) {
                                pr_info("64bit %s uses identity mapping\n",
                                        dev_name(dev));
                                return 1;
                        }
                }
        }

        return 0;
}

从实现来看,首先会判断iommu_identity_mapping是否为空(如果大家读过上一篇文章应该知道当iommu=pt的时候这个变量是不会为空的)如果为空则返回false,这里我先看一下不为空的逻辑。接着函数走到identity_mapping,这个函数的实现具体如下:

static int identity_mapping(struct device *dev)
{
        struct device_domain_info *info;

        if (likely(!iommu_identity_mapping))
                return 0;

        info = dev->archdata.iommu;
        if (info && info != DUMMY_DEVICE_DOMAIN_INFO)
                return (info->domain == si_domain);

        return 0;
}

可以看到函数里面首先判断iommu_identity_mapping是否为空,那么在iommut=pt的情况下这个是不为空的,然后判断设备的domain是否为si_domain,当然这个答案也是肯定的。因此这个函数返回值为true,接着函数走到iommu_should_identity_map(dev, 0),那么这个函数主要的判断如下:

  1. 如果这个设备不是pci设备且这个设备有RMRR,则返回False.
  2. 如果这个设备是pci设备,则下面几种情况会返回False:
  • 这个pci设备有rmrr
  • iommu_identity_mapping 的值不是IDENTMAP_ALL
  • 是pci设备但不是pcie设备,则如果设备不是 root bus 或者说pci设备的种类是pci bridge
  • 是pcie设备且pcie 设备是pcie bridge
  • 如果这个设备是32bit的设备则返回false
  • 如果这个函数返回false则需要从si_domain里面把这个设备的mapping删除掉,如果返回True则直接返回物理地址。所以总结一下在iommu=pt的场景下,由于静态映射的存在所以直接返回paddr。为什么能够直接返回物理地址而不是iova呢?这里我们再详细地介绍一下,我们先来看一下si_domain的初始化:

    static int __init si_domain_init(int hw)
    {
            int nid, ret = 0;

            si_domain = alloc_domain(DOMAIN_FLAG_STATIC_IDENTITY);
            if (!si_domain)
                    return -EFAULT;

            if (md_domain_init(si_domain, DEFAULT_DOMAIN_ADDRESS_WIDTH)) {
                    domain_exit(si_domain);
                    return -EFAULT;
            }

            pr_debug("Identity mapping domain allocated\n");
            //如果hw为true,则直接返回
            if (hw)
                    return 0;

            for_each_online_node(nid) {
                    unsigned long start_pfn, end_pfn;
                    int i;

                    for_each_mem_pfn_range(i, nid, &start_pfn, &end_pfn, NULL) {
                            ret = iommu_domain_identity_map(si_domain,
                                            PFN_PHYS(start_pfn), PFN_PHYS(end_pfn));
                            if (ret)
                                    return ret;
                    }
            }

            return 0;
    }

    首先,hw这个参数输入为hw_pass_through,它指的是iommu硬件上是否支持paas through翻译模式即iova就是真实的物理地址不需要再走一遍从iova转换到hpa的流程。那么从上面的函数实现也能看到如果hw为true则si_domain不会再去做相关内存mapping(关于hw为false的情况后面我们再分析),也就是说如果iommu硬件支持hw且iommu配置了pt则这种场景下硬件的DMA到达iommu之后不需要走页表翻译直接跟memory controller进行交互就可以了。但是大家再仔细想一下iommu硬件是如何知道哪些设备的dma要走页表进行转换,哪些设备的dma不需要进行地址转换呢? 为了更好的解释这个问题,我们先来回忆一下dma的页表是如何确定的

    图1
    从上面这张图我们可以看到一个设备的iova页表首先是通过bus号在root table里面找到相应的root_entry,然后再通过devfn在context table里面找到对应的context_entry,然后才能找到真正的页表。从vt-d的spec来看,contex_entry的format里面有一个标志位(TT)来表明这个设备的DMA是否是paasthroug,具体如下:

    图2

    我们再来看一下对图2中相关字段的详细描述:

    图3
    从图3中可以看到TT这个字段为10b时表示这个dma是不需要进行页表转换的,所以iommu就是通过这个字段来进行区分是否要通过页表进行转换的。但是还有一个问题是这个TT为paasthrough的translation type是什么时候设置的?回答这个问题,我们还是需要回归到内核相关代码当中。目前我们条件是hw为true,且iommu=pt的情况,我们还是回到intel_iommu_init这个函数当中
            if (iommu_identity_mapping) {
                    ret = iommu_prepare_static_identity_mapping(hw_pass_through);
                    if (ret) {
                            pr_crit("Failed to setup IOMMU pass-through\n");
                            goto free_iommu;
                    }
            }

    上面这个函数片断截取自init_dmars这个函数,其核心函数为iommu_prepare_static_identity_mapping,它最终会调到domain_add_dev_info 这个函数并最终走到domain_context_mapping_one

    static int domain_context_mapping_one(struct dmar_domain *domain,
                                          struct intel_iommu *iommu,
                                          u8 bus, u8 devfn)
    {
     ......
     //设置translation type 为 paas through
     if (hw_pass_through && domain_type_is_si(domain))
                    translation = CONTEXT_TT_PASS_THROUGH;
     ......
     //获取这个设备在contex table表里面的地址
     context = iommu_context_addr(iommu, bus, devfn, 1);
        if (!context)
               goto out_unlock;

         ret = 0;
         if (context_present(context))
                goto out_unlock;

          pgd = domain->pgd; //iova页表基址

          context_clear_entry(context);
          context_set_domain_id(context, did);
      
     .......
     //下面函数可以看出如果是在paas through模式下不会设置iova页表地址
     if (translation != CONTEXT_TT_PASS_THROUGH) {
                    for (agaw = domain->agaw; agaw != iommu->agaw; agaw--) {
                            ret = -ENOMEM;
                            pgd = phys_to_virt(dma_pte_addr(pgd));
                            if (!dma_pte_present(pgd))
                                    goto out_unlock;
                    }

                    info = iommu_support_dev_iotlb(domain, iommu, bus, devfn);
                    if (info && info->ats_supported)
                            translation = CONTEXT_TT_DEV_IOTLB;
                    else
                            translation = CONTEXT_TT_MULTI_LEVEL;
        
                    //非paas through模式下需要设置iova页表的基地址
                    context_set_address_root(context, virt_to_phys(pgd));
                    context_set_address_width(context, iommu->agaw);
            } else {
                    /*
                     * In pass through mode, AW must be programmed to
                     * indicate the largest AGAW value supported by
                     * hardware. And ASR is ignored by hardware.
                     */
                    context_set_address_width(context, iommu->msagaw);
            }
      
      // 设置转换类型
            context_set_translation_type(context, translation)

    }

    看完之后大家应该有个比较清晰的理解,上面主要理了一下在iommu=pt,hw为true的情况;如果hw为false的情况又会怎么样呢?具体的逻辑还是要从init_dmars这个函数开始看起,通过分析可以看到因为hw=false也就是说iommu硬件不支持paas through 的translation type,所以必须要是创建页表的,但是因为是静态映射即iova就等于hpa,所以在这种情况下也是可以直接返回paddr的,但是效率肯定是没法跟hw=true相比的。

    聊完iommu=pt的各种情况之后,我们再看一下iommu为默认设置的情况下设备是如何进行dma操作的。还是先要从intel_iommu_init这个函数里面的init_dmars看起,从这相关的逻辑来看区别在于不会提前创建si_domain(即提前做好iova的映射),那它是在什么时候创建的呢?答案是在dma_map的时候而且dma map相关的api返回的iova,如果大家感兴趣可以去仔细读一下__intel_map_single这个函数,部分核心逻辑如下:

            //如果dev没有创建domain则重新创建,同时将其加入到contex table里面
            domain = get_valid_domain_for_dev(dev);
            if (!domain)
                    return 0;

            iommu = domain_get_iommu(domain);
            size = aligned_nrpages(paddr, size);
      //分配iova
            iova_pfn = intel_alloc_iova(dev, domain, dma_to_mm_pfn(size), dma_mask);
            if (!iova_pfn)
                    goto error;

    虚拟化场景

    iommu在虚拟化场景扮演着非常重要的作用,尤其是在设备直通的场景。大家都知道在虚拟机里面是没有iommu的,guest 里面的所有的dma_ops走的都是noiommu(当然之前也有人在推viommu的方案),所以当你在guest里面使用dma_map这种类似的api的时候,其返回的都是gpa。这样问题就来了,你想想如果虚拟机使用的是virtio-net or virtio-blk这种完全模拟的设备还好,因为其本质都还是共享内存还是在mmu层做转换。但是在直通设备的场景下,它就是跟真的外设打交道,而外设是无法直接dma gpa的,它必然要做一层转换。接下来我们就来分析一下虚拟化场景下iommu是如何工作的。

    首先,如果你要在虚拟机场景下做设备直通那我们经常会在kernel cmdline里面做iommu=pt, intel_iommu=on(当然也可以是amd, arm)。然后把vf或者pf从原来的驱动unbind掉,然后bind到vfio_pci,接着在启动虚拟机的时候我们需要对设备在vfio层面进行相关的初始化,然后并对guest所有的内存做vfio_dma_map。iommu=pt这个由于前面已经分析过了,这里我就不再细讲,我们先看一下把vf或者pf从原来的驱动unbind的过程中跟iommu相关的操作有哪些:

    static int device_notifier(struct notifier_block *nb,
                                      unsigned long action, void *data)
    {
            struct device *dev = data;
            struct dmar_domain *domain;

            if (iommu_dummy(dev))
                    return 0;

            if (action != BUS_NOTIFY_REMOVED_DEVICE)
                    return 0;

            domain = find_domain(dev);
            if (!domain)
                    return 0;

            dmar_remove_one_dev_info(domain, dev);
            if (!domain_type_is_vm_or_si(domain) && list_empty(&domain->devices))
                    domain_exit(domain);

            return 0;
    }

    static struct notifier_block device_nb = {
            .notifier_call = device_notifier,
    };

    这个是比较核心的地方,从上面的逻辑可以看出设备从其原来的driver unbind的时候,如果这个设备已经有了相关的dmar_domain比如之前说的在pt的场景下会提前创建好静态mapping si_domain,那么这时候会把设备从si_domain移除。

    然后再看把设备bind到vfio-pci driver的时发生了什么事情呢?最终其会调到vfio_pci_probe这个函数里面,其核心的逻辑具体如下:

    • vfio_iommu_group_get

    这个函数主要是判断这个设备是否属于某个iommu_group,如果没有则报错。

    • vfio_add_group_dev

    这个函数首先从该设备所属的iommu_group找到相应的vfio_group,如果找不到则会为这个iommu_group创建vfio_group;然后再把这个dev跟vfio_device关联起来,同时为这个vfio_dev绑定vfio_device_ops

    static const struct vfio_device_ops vfio_pci_ops = {
            .name           = "vfio-pci",
            .open           = vfio_pci_open,
            .release        = vfio_pci_release,
            .ioctl          = vfio_pci_ioctl,
            .read           = vfio_pci_read,
            .write          = vfio_pci_write,
            .mmap           = vfio_pci_mmap,
            .request        = vfio_pci_request,
    };

    上面我们主要是聊了一下设备直通之前的一些准备工作,下面我们看一下qemu侧直通设备vfio相关的操作,为了让大家更的好理解我们举个例子,假设我们要把pci device 0000:06:0d.0 直通给vm,则看一下相关的信息:

    readlink /sys/bus/pci/devices/0000:06:0d.0/iommu_group ../../../../kernel/iommu_groups/26

    然后我们来看一下qemu侧vfio设备的初始操作函数vfio_realize,相关的逻辑如下:

    • vfio_get_group

    snprintf(path, sizeof(path), "/dev/vfio/%d", groupid); group->fd = qemu_open(path, O_RDWR);

    首先打开这个group,以上面的device为例子则这个groupid为26,这个open函数对应的是ops函数如下:

    static const struct file_operations vfio_group_fops = {
            .owner          = THIS_MODULE,
            .unlocked_ioctl = vfio_group_fops_unl_ioctl,
    #ifdef CONFIG_COMPAT
            .compat_ioctl   = vfio_group_fops_compat_ioctl,
    #endif
            .open           = vfio_group_fops_open,
            .release        = vfio_group_fops_release,
    };

    vfio_group_fops_open函数核心逻辑就是要找到这个设备所属于的vfio_group并把它赋给这个file,那这个vfio_group是在设备bind到vfio_pci的时候创建的。紧接着会判断这个设备的vfio group是否可用,相关逻辑如下:

      if (ioctl(group->fd, VFIO_GROUP_GET_STATUS, &status)) {
            error_setg_errno(errp, errno, "failed to get group %d status", groupid);
            goto close_fd_exit;
        }

        if (!(status.flags & VFIO_GROUP_FLAGS_VIABLE)) {
            error_setg(errp, "group %d is not viable", groupid);
            error_append_hint(errp,
                              "Please ensure all devices within the iommu_group "
                              "are bound to their vfio bus driver.\n");
            goto close_fd_exit;
        }

    通过ioctl调到vfio模块当中,具体的逻辑如下:

       if (vfio_group_viable(group))
                status.flags |= VFIO_GROUP_FLAGS_VIABLE;

        if (group->container)
                 status.flags |= VFIO_GROUP_FLAGS_CONTAINER_SET;

    紧接着会调用vfio_connect_container, 这个container是vfio层面的一个概念,它跟vm的address space相关联,这个函数的具体逻辑如下:

    • 如果container存在,则直接调用VFIO_GROUP_SET_CONTAINER 这个ioctl将这个设备所属的vfio_group跟这个containe关联起来,具体这个ioctl到底做了哪些事情,我们后面再详细说
    • 如果container不存在,则需要先创建出来,具体的创建逻辑如下
    fd = qemu_open("/dev/vfio/vfio", O_RDWR);

    这个container就是这在个时候创建出来的,它在vfio层调用的是vfio_ops的open函数,具体如下:

    static int vfio_fops_open(struct inode *inode, struct file *filep)
    {
            struct vfio_container *container;

            container = kzalloc(sizeof(*container), GFP_KERNEL);
            if (!container)
                    return -ENOMEM;

            INIT_LIST_HEAD(&container->group_list);
            init_rwsem(&container->group_lock);
            kref_init(&container->kref);

            filep->private_data = container;

            return 0;
    }

    接着调用vfio_init_container函数,首先去获取一下iommu_type,目前的逻辑是VFIO_TYPE1v2_IOMMU优先(在driver/vfio/vfio_iommu_type1.c),然后通过VFIO_GROUP_SET_CONTAINER这个ioctl具体做的事情。它最终会调到vfio_group_set_container 这个函数,其核心逻辑如下:

            driver = container->iommu_driver;
            if (driver) {
                    ret = driver->ops->attach_group(container->iommu_data,
                                                    group->iommu_group);
                    if (ret)
                            goto unlock_out;
            }

    注意这个时候由于driver目前还没有设置所以为空,故这个函数主要的作用就是把这个设备所属的vfio_group跟这个container关联起来,然后将这个vfio_group添加到container的group_list。接着调用VFIO_SET_IOMMU这个ioctl为这个container做相关配置,入口函数为vfio_ioctl_set_iommu具体逻辑如下:

    • vfio_iommu_type1_open 创建一个vfio_iommu,并初始化这个vfio_iommu 的domain_list,初始化这个iommu的dma_list为RB_ROOT。

    • __vfio_container_attach_groups

    static int __vfio_container_attach_groups(struct vfio_container *container,
                                              struct vfio_iommu_driver *driver,
                                              void *data)
    {
            struct vfio_group *group;
            int ret = -ENODEV;

            list_for_each_entry(group, &container->group_list, container_next) {
                    ret = driver->ops->attach_group(data, group->iommu_group);
                    if (ret)
                            goto unwind;
            }

            return ret;

    unwind:
            list_for_each_entry_continue_reverse(group, &container->group_list,
                                                 container_next) {
                    driver->ops->detach_group(data, group->iommu_group);
            }

            return ret;
    }

    从实现来看,遍历container->group_list里面的group(这个时候应该只有一个),然后调用attach_group 这个callback函数,其最终调用的函数为vfio_iommu_type1_attach_group ,下面看一下这个函数核心逻辑:

    1. 首先遍历传入的参数1也就是vfio_iommu的domain_list每一个vfio_domain,然后遍历每个vfio_domain->group_list当中的vfio_group,判断该设备所属于的iommu_group已经attach过。
    2. 如果step1里面没有找到,则新建vfio_group和vfio_domain,并把vfio_group->iommu_group指向该设备所属的iommu_group
    3. 判断该设备的iommu_group下的每个设备的bus type是否为vfio bus。
    4. 通过iommu_domain_alloc为新建的vfio_domain创新的iommu_domain,其最终调用的函数为intel_iommu_domain_alloc
    static struct iommu_domain *intel_iommu_domain_alloc(unsigned type)
    {
            struct dmar_domain *dmar_domain;
            struct iommu_domain *domain;

            if (type != IOMMU_DOMAIN_UNMANAGED)
                    return NULL;

            dmar_domain = alloc_domain(DOMAIN_FLAG_VIRTUAL_MACHINE);
            if (!dmar_domain) {
                    pr_err("Can't allocate dmar_domain\n");
                    return NULL;
            }
            if (md_domain_init(dmar_domain, DEFAULT_DOMAIN_ADDRESS_WIDTH)) {
                    pr_err("Domain initialization failed\n");
                    domain_exit(dmar_domain);
                    return NULL;
            }
            domain_update_iommu_cap(dmar_domain);

            domain = &dmar_domain->domain;
            domain->geometry.aperture_start = 0;
            domain->geometry.aperture_end   = __DOMAIN_MAX_ADDR(dmar_domain->gaw);
            domain->geometry.force_aperture = true;

            return domain;
    }

    其核心逻辑为创建dmar_domain,然后初始化iova页表的pgd

    static int md_domain_init(struct dmar_domain *domain, int guest_width)
    {
            int adjust_width;

            init_iova_domain(&domain->iovad, VTD_PAGE_SIZE, IOVA_START_PFN);
            domain_reserve_special_ranges(domain);

            /* calculate AGAW */
            domain->gaw = guest_width;
            adjust_width = guestwidth_to_adjustwidth(guest_width);
            domain->agaw = width_to_agaw(adjust_width);

            domain->iommu_coherency = 0;
            domain->iommu_snooping = 0;
            domain->iommu_superpage = 0;
            domain->max_addr = 0;

            /* always allocate the top pgd */
      //dma进行转换的页表基地址,可以参考图1
            domain->pgd = (struct dma_pte *)alloc_pgtable_page(domain->nid);
            if (!domain->pgd)
                    return -ENOMEM;
            domain_flush_cache(domain, domain->pgd, PAGE_SIZE);
            return 0;
    }

    紧接着调用 iommu_attach_group,将iommu_group下面的设备绑定到新建的iommu_domain上,函数最终会调到intel_iommu_attach_device 这个函数,具体的调用逻辑如下:

    -intel_iommu_attach_device

            -domain_add_dev_info 

                  -dmar_insert_one_dev_info 

                        -domain_attach_iommu 

                         -domain_context_mapping 

                              -domain_context_mapping_one

    主要实现的就是把context table建立起来,并设置相关的context entry的属性等。然后初始化这个vfio_domain的group_list,并把这个vfio_group添加到这个这个group当中。

        INIT_LIST_HEAD(&domain->group_list);
        list_add(&group->next, &domain->group_list)

    接着往下看相关的逻辑

            list_for_each_entry(d, &iommu->domain_list, next) {
                    if (d->domain->ops == domain->domain->ops &&
                        d->prot == domain->prot) {
                            iommu_detach_group(domain->domain, iommu_group);
                            if (!iommu_attach_group(d->domain, iommu_group)) {
                                    list_add(&group->next, &d->group_list);
                                    iommu_domain_free(domain->domain);
                                    kfree(domain);
                                    mutex_unlock(&iommu->lock);
                                    return 0;
                            }

                            ret = iommu_attach_group(domain->domain, iommu_group);
                            if (ret)
                                    goto out_domain;
                    }
            }

    为了更好的理解上面的这段逻辑,先回忆下这几个概念:container, vfio_iommu, vfio_group, vfio_domain。其中container是per vm的也就是说一个vm只会有一个container;vfio_iommu是在container初始化的时候创建的,它也是per vm的;vfio_group跟iommu_group是一一对应的,有多少个iommu_group就应该有多少个vfio_group;关于vfio_domain数量我们需要分析一下上面的代码片段,从实现来看如果新创建的domain的ops和prot(属性IOMMU_CACHE, IOMMU_READ, IOMMU_WRITE)跟已经存在的domain相同则直接把这个iommu_group直接attach到已存在的domain上,也就是说多个iommu_group或者说vfio_group对应一个vfio_domain。接着往下,如果不满足上面所说的条件(或者初次创建iommu domain_list为空的时候)则

     /* replay mappings on new domains */
     ret = vfio_iommu_replay(iommu, domain);
     if (ret)
         goto out_detach;
    // 将新创建的domain加到vfio_iommu的domain_list链表里面
     list_add(&domain->next, &iommu->domain_list);

    vfio_iommu_replay 这个函数的主要逻辑是找到iommu->domain_list 的第一个domain,然后把iommu->dma_list 下的所有的dma区域全部mapping到新创建的domain里面(当然在初始化阶段这两者都是为空的)。之所以是第一个是因为新创建的domain都是插在domain_list的首部,而头部的domain mapping里面包含了其后面的所有domain的dma mapping。核心逻辑走完,回到vfio_ioctl_set_iommu 函数,这里会把找到的driver和新创建的vfio_iommu赋值给container

    container->iommu_driver = driver; //vfio iommu type1
    container->iommu_data = data; // vfio_iommu

    至此,vfio_realize当中跟vm dma相关数据结构初始化有关的工作已经完成,那我们看一下dma 页表的初始化。qemu当中是通过注册memory region listener callback来将vm所有的内存都mapping起来的,具体调用逻辑

    vfio_dma_map(container, iova, int128_get64(llsize),
                           vaddr, section->readonly);

    这里面iova就是gpa, vaddr就是hva,它是通过VFIO_IOMMU_MAP_DMA 这个系统调用来实现的,然后继续追一下这个系统调用的实现

    vfio_dma_do_map 

        - vfio_find_dma // check下 vfio_iommu的dma_list(红黑树)里面是否已经有了这个dma map的iova区域 

         - vfio_link_dma //将要做dma map的iova区域插入到 dma_list。 

        - vfio_pin_pages //为hva分配相对应的物理地址,然后pin该物理页

        - vfio_iommu_map //为iommu->domain_list每个domain做mapping 

                - iommu_map 

                        - intel_iommu_map

                                - domain_pfn_mapping //完成iova到pfn的映射即gpa->hpa

    因为iova是gpa,所以当guest里面使用dma_map api直接返回gpa地址,但是由于页表里面映射的也是iova->hpa,所以这个时候硬件发出的dma操作就能正确进行了。

    总结

    上一篇我们留了几个问题,那么结合上面的分析我们先来回答一下dma相关的问题,而interrupt remapping的问题在下一篇文章中再来解答。

    Q: 在非虚拟化场景下iommu=pt和iommu disabled这两者有什么区别?

    A: 区别在于是否需要走页表转换,那么在pt场景下应用调用dma map 相关api其返回的都是真实的物理地址,如果hw支持passthrough translation则不需要走,如果不支持则需要走只是这种场景下iova就等于hpa。而disable的情况下其返回的是iova且必须要走页表转换

    Q:虚拟机里面是没有iommu的,那dma是如何进行的?

    A:虚拟机里面没有iommu那么其所有使用dma map的地方返回的都是gpa,在直通的场景需要需要借助于iommu dma remapping机制才能正常工作。


    Q:不同intel iommu硬件下的两个pci设备是如何实现直通给同一个vm的?

    A:不同intel_iommu的两个pci设备直通给同一个vm的时候,这两个设备使用的iova页表的基地址是一样的,即同一个dmar_domain的pgd。


    Linux阅码场 专业的Linux技术社区和Linux操作系统学习平台,内容涉及Linux内核,Linux内存管理,Linux进程管理,Linux文件系统和IO,Linux性能调优,Linux设备驱动以及Linux虚拟化和云计算等各方各面.
    评论
    • 书接上回:【2022年终总结】阳光总在风雨后,启航2023-面包板社区  https://mbb.eet-china.com/blog/468701-438244.html 总结2019,松山湖有个欧洲小镇-面包板社区  https://mbb.eet-china.com/blog/468701-413397.html        2025年该是总结下2024年的喜怒哀乐,有个好的开始,才能更好的面对2025年即将
      liweicheng 2025-01-24 23:18 350浏览
    • 嘿,咱来聊聊RISC-V MCU技术哈。 这RISC-V MCU技术呢,简单来说就是基于一个叫RISC-V的指令集架构做出的微控制器技术。RISC-V这个啊,2010年的时候,是加州大学伯克利分校的研究团队弄出来的,目的就是想搞个新的、开放的指令集架构,能跟上现代计算的需要。到了2015年,专门成立了个RISC-V基金会,让这个架构更标准,也更好地推广开了。这几年啊,这个RISC-V的生态系统发展得可快了,好多公司和机构都加入了RISC-V International,还推出了不少RISC-V
      丙丁先生 2025-01-21 12:10 1229浏览
    •  万万没想到!科幻电影中的人形机器人,正在一步步走进我们人类的日常生活中来了。1月17日,乐聚将第100台全尺寸人形机器人交付北汽越野车,再次吹响了人形机器人疯狂进厂打工的号角。无独有尔,银河通用机器人作为一家成立不到两年时间的创业公司,在短短一年多时间内推出革命性的第一代产品Galbot G1,这是一款轮式、双臂、身体可折叠的人形机器人,得到了美团战投、经纬创投、IDG资本等众多投资方的认可。作为一家成立仅仅只有两年多时间的企业,智元机器人也把机器人从梦想带进了现实。2024年8月1
      刘旷 2025-01-21 11:15 995浏览
    • 数字隔离芯片是一种实现电气隔离功能的集成电路,在工业自动化、汽车电子、光伏储能与电力通信等领域的电气系统中发挥着至关重要的作用。其不仅可令高、低压系统之间相互独立,提高低压系统的抗干扰能力,同时还可确保高、低压系统之间的安全交互,使系统稳定工作,并避免操作者遭受来自高压系统的电击伤害。典型数字隔离芯片的简化原理图值得一提的是,数字隔离芯片历经多年发展,其应用范围已十分广泛,凡涉及到在高、低压系统之间进行信号传输的场景中基本都需要应用到此种芯片。那么,电气工程师在进行电路设计时到底该如何评估选择一
      华普微HOPERF 2025-01-20 16:50 187浏览
    • 2024年是很平淡的一年,能保住饭碗就是万幸了,公司业绩不好,跳槽又不敢跳,还有一个原因就是老板对我们这些员工还是很好的,碍于人情也不能在公司困难时去雪上加霜。在工作其间遇到的大问题没有,小问题还是有不少,这里就举一两个来说一下。第一个就是,先看下下面的这个封装,你能猜出它的引脚间距是多少吗?这种排线座比较常规的是0.6mm间距(即排线是0.3mm间距)的,而这个规格也是我们用得最多的,所以我们按惯性思维来看的话,就会认为这个座子就是0.6mm间距的,这样往往就不会去细看规格书了,所以这次的运气
      wuliangu 2025-01-21 00:15 812浏览
    • 不让汽车专美于前,近年来哈雷(Harley-Davidson)和本田(Honda)等大型重型机车大厂的旗下车款皆已陆续配备车载娱乐系统与语音助理,在路上也有越来越多的普通机车车主开始使用安全帽麦克风,在骑车时透过蓝牙连线执行语音搜寻地点导航、音乐播放控制或免持拨打接听电话等各种「机车语音助理」功能。客户背景与面临的挑战以本次分享的客户个案为例,该客户是一个跨国车用语音软件供货商,过往是与车厂合作开发前装车机为主,且有着多年的「汽车语音助理」产品经验。由于客户这次是首度跨足「机车语音助理」产品,因
      百佳泰测试实验室 2025-01-24 17:00 194浏览
    • 随着AI大模型训练和推理对计算能力的需求呈指数级增长,AI数据中心的网络带宽需求大幅提升,推动了高速光模块的发展。光模块作为数据中心和高性能计算系统中的关键器件,主要用于提供高速和大容量的数据传输服务。 光模块提升带宽的方法有两种:1)提高每个通道的比特速率,如直接提升波特率,或者保持波特率不变,使用复杂的调制解调方式(如PAM4);2)增加通道数,如提升并行光纤数量,或采用波分复用(CWDM、LWDM)。按照传输模式,光模块可分为并行和波分两种类型,其中并行方案主要应用在中短距传输场景中成本
      hycsystembella 2025-01-25 17:24 473浏览
    • 飞凌嵌入式基于瑞芯微RK3562系列处理器打造的FET3562J-C全国产核心板,是一款专为工业自动化及消费类电子设备设计的产品,凭借其强大的功能和灵活性,自上市以来得到了各行业客户的广泛关注。本文将详细介绍如何启动并测试RK3562J处理器的MCU,通过实际操作步骤,帮助各位工程师朋友更好地了解这款芯片。1、RK3562J处理器概述RK3562J处理器采用了4*Cortex-A53@1.8GHz+Cortex-M0@200MHz架构。其中,4个Cortex-A53核心作为主要核心,负责处理复杂
      飞凌嵌入式 2025-01-24 11:21 293浏览
    • 项目展示①正面、反面②左侧、右侧项目源码:https://mbb.eet-china.com/download/316656.html前言为什么想到要做这个小玩意呢,作为一个死宅,懒得看手机,但又想要抬头就能看见时间和天气信息,于是就做个这么个小东西,放在示波器上面正好(示波器外壳有个小槽,刚好可以卡住)功能主要有,获取国家气象局的天气信息,还有实时的温湿度,主控采用ESP32,所以后续还可以开放更多奇奇怪怪的功能,比如油价信息、股票信息之类的,反正能联网可操作性就大多了原理图、PCB、面板设计
      小恶魔owo 2025-01-25 22:09 615浏览
    • 临近春节,各方社交及应酬也变得多起来了,甚至一月份就排满了各式约见。有的是关系好的专业朋友的周末“恳谈会”,基本是关于2025年经济预判的话题,以及如何稳定工作等话题;但更多的预约是来自几个客户老板及副总裁们的见面,他们为今年的经济预判与企业发展焦虑而来。在聊天过程中,我发现今年的聊天有个很有意思的“点”,挺多人尤其关心我到底是怎么成长成现在的多领域风格的,还能掌握一些经济趋势的分析能力,到底学过哪些专业、在企业管过哪些具体事情?单单就这个一个月内,我就重复了数次“为什么”,再辅以我上次写的:《
      牛言喵语 2025-01-22 17:10 494浏览
    • 前篇文章中『服务器散热效能不佳有解吗?』提到气冷式的服务器其散热效能对于系统稳定度是非常重要的关键因素,同时也说明了百佳泰对于散热效能能提供的协助与服务。本篇将为您延伸说明我们如何进行评估,同时也会举例在测试过程中发现的问题及改善后的数据。AI服务器的散热架构三大重点:GPU导风罩:尝试不同的GPU导风罩架构,用以集中服务器进风量,加强对GPU的降温效果。GPU托盘:改动GPU托盘架构,验证出风面积大小对GPU散热的影想程度。CPU导风罩:尝试封闭CPU导风罩间隙,集中风流,验证CPU降温效果。
      百佳泰测试实验室 2025-01-24 16:58 189浏览
    •     IPC-2581是基于ODB++标准、结合PCB行业特点而指定的PCB加工文件规范。    IPC-2581旨在替代CAM350格式,成为PCB加工行业的新的工业规范。    有一些免费软件,可以查看(不可修改)IPC-2581数据文件。这些软件典型用途是工艺校核。    1. Vu2581        出品:Downstream     
      电子知识打边炉 2025-01-22 11:12 465浏览
    • 故障现象 一辆2007款日产天籁车,搭载VQ23发动机(气缸编号如图1所示,点火顺序为1-2-3-4-5-6),累计行驶里程约为21万km。车主反映,该车起步加速时偶尔抖动,且行驶中加速无力。 图1 VQ23发动机的气缸编号 故障诊断接车后试车,发动机怠速运转平稳,但只要换挡起步,稍微踩下一点加速踏板,就能感觉到车身明显抖动。用故障检测仪检测,发动机控制模块(ECM)无故障代码存储,且无失火数据流。用虹科Pico汽车示波器测量气缸1点火信号(COP点火信号)和曲轴位置传感器信
      虹科Pico汽车示波器 2025-01-23 10:46 321浏览
    • 高速先生成员--黄刚这不马上就要过年了嘛,高速先生就不打算给大家上难度了,整一篇简单但很实用的文章给大伙瞧瞧好了。相信这个标题一出来,尤其对于PCB设计工程师来说,心就立马凉了半截。他们辛辛苦苦进行PCB的过孔设计,高速先生居然说设计多大的过孔他们不关心!另外估计这时候就跳出很多“挑刺”的粉丝了哈,因为翻看很多以往的文章,高速先生都表达了过孔孔径对高速性能的影响是很大的哦!咋滴,今天居然说孔径不关心了?别,别急哈,听高速先生在这篇文章中娓娓道来。首先还是要对各位设计工程师的设计表示肯定,毕竟像我
      一博科技 2025-01-21 16:17 241浏览
    我要评论
    0
    点击右上角,分享到朋友圈 我知道啦
    请使用浏览器分享功能 我知道啦