没有IOMMU的DMA操作-swiotlb(转)

链接 :没有IOMMU的DMA操作_布道师Peter的博客-CSDN博客

(62条消息) Linux x86-64 IOMMU详解(二)——SWIOTLB(软件IOMMU)_C is My Best Friend的博客-CSDN博客_swiotlb

SWIOTLB概述

IOMMU的核心功能就是,实现在low buffer和high buffer之间的sync,也就是内存内容的复制操作。

在这里插入图片描述

读者可能会想,内存的复制,在内核中,不就是调用memcpy()函数来实现的吗?没错,这就是本文要介绍的IOMMU的软件实现方式——SWIOTLB。之所以说是软件实现,是因为sync操作在底层正是调用memcpy()函数,这完全是软件实现的。

SWIOTLB的作用在于,使得寻址能力较低、无法直接寻址到内核所分配的DMA buffer的那些设备,也能够进行DMA操作。记住这句话——它将贯穿全文。由此,我们对本文开头图片稍作修改,制作了一个SWIOTLB的实现版本。

在这里插入图片描述
在目前主流的Linux操作系统中,SWIOTLB发挥作用的场合并不多见。这主要是由于以下原因:

现代的外部设备,通常都是32位或64位设备。64位设备毫无疑问可以直接寻址整个物理内存空间;而32位设备能够直接寻址的范围也达到了4G。如果操作系统运行内存不大于4G,则所有内存都可以被这些设备直接寻址到,此时设备的DMA操作,就无需SWIOTLB的辅助。
相比硬件IOMMU,SWIOTLB存在memcpy()操作,需要CPU的参与,降低了效率,这是软件实现的固有弊端。
后面的文章将会提到,如果启动参数中同时启用SWIOTLB和硬件IOMMU(即Intel IOMMU),那么当Linux系统启动完成后,SWIOTLB将会被禁用,而仅保留硬件IOMMU。
 

====================================================================

我们知道DMA通常需要访问连续的物理内存,除非设备支持iommu,当设备不支持iommu的话可以用以下方式:

在内核启动时为设备保留内存

将MMU内嵌到设备中,如GPU

这里GPU MMU的方式算是个例外,不在本篇文章讨论范围内。

我们知道DMA映射有两种方式,一种是一致性映射 dma_alloc_coherent,一种是流式映射 dma_map_single (dma_map_sg可以映射多个dma buffer)。

一致性映射 dma_alloc_coherent
dma_alloc_coherent会调用dma_alloc_attrs:

dma_alloc_coherent->dma_alloc_attrs->swiotlb_alloc->(ma_direct_alloc/swiotlb_alloc_buffer)->swiotlb_tbl_map_single

static inline void *dma_alloc_attrs(struct device *dev, size_t size,
           dma_addr_t *dma_handle, gfp_t flag,
           unsigned long attrs)
{
 const struct dma_map_ops *ops = get_dma_ops(dev);
 void *cpu_addr;
 
 BUG_ON(!ops);
 
 if (dma_alloc_from_dev_coherent(dev, size, dma_handle, &cpu_addr))
  return cpu_addr;
 
 if (!arch_dma_alloc_attrs(&dev, &flag))
  return NULL;
 if (!ops->alloc)
  return NULL;
 
 cpu_addr = ops->alloc(dev, size, dma_handle, flag, attrs);
 debug_dma_alloc_coherent(dev, size, *dma_handle, cpu_addr);
 return cpu_addr;
}


ops->alloc对应的回调有两个注册,分别是swiotlb和iommu:

static struct dma_map_ops swiotlb_dma_ops = {
 .alloc = __dma_alloc, //dma_alloc_attrs
 .free = __dma_free,
 .mmap = __swiotlb_mmap,
 .get_sgtable = __swiotlb_get_sgtable,
 .map_page = __swiotlb_map_page, //dma_map_single
 .unmap_page = __swiotlb_unmap_page,
 .map_sg = __swiotlb_map_sg_attrs, //dma_map_sg
 .unmap_sg = __swiotlb_unmap_sg_attrs,
 .sync_single_for_cpu = __swiotlb_sync_single_for_cpu,
 .sync_single_for_device = __swiotlb_sync_single_for_device,
 .sync_sg_for_cpu = __swiotlb_sync_sg_for_cpu,
 .sync_sg_for_device = __swiotlb_sync_sg_for_device,
 .dma_supported = __swiotlb_dma_supported,
 .mapping_error = __swiotlb_dma_mapping_error,
};
 
static struct dma_map_ops iommu_dma_ops = {
 .alloc = __iommu_alloc_attrs,
 .free = __iommu_free_attrs,
 .mmap = __iommu_mmap_attrs,
 .get_sgtable = __iommu_get_sgtable,
 .map_page = __iommu_map_page,
 .unmap_page = __iommu_unmap_page,
 .map_sg = __iommu_map_sg_attrs,
 .unmap_sg = __iommu_unmap_sg_attrs,
 .sync_single_for_cpu = __iommu_sync_single_for_cpu,
 .sync_single_for_device = __iommu_sync_single_for_device,
 .sync_sg_for_cpu = __iommu_sync_sg_for_cpu,
 .sync_sg_for_device = __iommu_sync_sg_for_device,
 .map_resource = iommu_dma_map_resource,
 .unmap_resource = iommu_dma_unmap_resource,
 .mapping_error = iommu_dma_mapping_error,
};


非iommu的话即调用__dma_alloc:

static void *__dma_alloc(struct device *dev, size_t size,
    dma_addr_t *dma_handle, gfp_t flags,
    unsigned long attrs)
{
  ......
 size = PAGE_ALIGN(size);
 
 if (!coherent && !gfpflags_allow_blocking(flags)) {
    ......
  //coherent_pool
  void *addr = __alloc_from_pool(size, &page, flags); 
 
  if (addr)
   *dma_handle = phys_to_dma(dev, page_to_phys(page));
 
  return addr;
 }
 
 //cma or buddy or swiotlb
 ptr = __dma_alloc_coherent(dev, size, dma_handle, flags, attrs);
 if (!ptr)
  goto no_mem;
 ......
 return coherent_ptr;
}


其中__alloc_from_pool用来分配 coherent_pool 的内存,__dma_alloc_coherent用来分配 cma 或者 buddy 或者 swiotlb的内存。(实际方案中一般通过memblock的方式划分coherent pool,cma,swiotlb这三种reserved mem)

其分配流程如下图所示:

                                from nxp community by eric chen

相关解释:

为了下面流式映射更好的理解,这里再详细讲下 swiotlb 的分配过程。

dma_map_single->dma_map_single_attrs->(ops->map_page)->__swiotlb_map_page-> swiotlb_map_page-> map_single->swiotlb_tbl_map_single ...

static phys_addr_t
map_single(struct device *hwdev, phys_addr_t phys, size_t size,
    enum dma_data_direction dir, unsigned long attrs)
{
 dma_addr_t start_dma_addr;
 
 if (swiotlb_force == SWIOTLB_NO_FORCE) {
  dev_warn_ratelimited(hwdev, "Cannot do DMA to address %pa\n",
         &phys);
  return SWIOTLB_MAP_ERROR;
 }
 
 start_dma_addr = swiotlb_phys_to_dma(hwdev, io_tlb_start);
 return swiotlb_tbl_map_single(hwdev, start_dma_addr, phys, size,
          dir, attrs);
}
 
phys_addr_t swiotlb_tbl_map_single(struct device *hwdev,
       dma_addr_t tbl_dma_addr,
       phys_addr_t orig_addr, size_t size,
       enum dma_data_direction dir,
       unsigned long attrs)
{
 
  if (io_tlb_list[index] >= nslots) {
   int count = 0;
 
   for (i = index; i < (int) (index + nslots); i++)
    io_tlb_list[i] = 0;
   for (i = index - 1; (OFFSET(i, IO_TLB_SEGSIZE) != IO_TLB_SEGSIZE - 1) && io_tlb_list[i]; i--)
    io_tlb_list[i] = ++count;
   tlb_addr = io_tlb_start + (index << IO_TLB_SHIFT);
 
   /*
    * Update the indices to avoid searching in the next
    * round.
    */
   io_tlb_index = ((index + nslots) < io_tlb_nslabs
     ? (index + nslots) : 0);
 
   goto found;
  }
    ......
    return tlb_addr;
}


申请bounce buffer并且返回虚拟地址,出去再转为dma地址

系统启动的时候就做好了slots和swiotlb内存的映射,这里根据slot可以返回其地址。

至此,dma_alloc_coherent的分配流程就完成了。我们可以看出虽然申请api都是dma_alloc_coherent函数,但是后台的实现有很多种,并且和是否是dma zone也没什么必然关系,本质上只是一块0x0000_0000到0xFFFF_FFFF范围内的连续内存。

流式映射 dma_map_single
因为DMA受32位访问的限制,所以只能访问0x0000_0000到0xFFFF_FFFF地址空间的内存,再加上DMA需要访问连续的物理内存,故coherent pool,cma,buddy,swiotlb必须保证在0x0000_0000~0xFFFF_FFFF以内的连续物理空间。 这些没毛病。

但是如果一个64位系统的话,CPU访问内存完全是可以大于0xFFFF_FFFF范围的。比如一个内存的基地址是0x80000000,内存大小是4G,则内存的物理地址范围是0x8000_0000~0x18000_0000。由于DMA寻址范围为0x0000_0000~0xFFFF_FFFF,如果CPU把数据放在0x10000_0000~0x18000_0000这段空间,DMA就无法访问了。

怎么解决上面的问题?

此时swiotlb就登上了历史舞台。


                                        from nxp community by eric chen

swiotlb做的工作如上图所示,主要通过map_single从swiotlb里找到一块buffer叫做Bounce Buffer,然后把CPU访问的Data Buffer与Bounce Buffer映射起来,最后通过swiotlb_bounce把这两个buffer中的数据做个同步(memcpy)。

下面我们通过代码把上面的过程梳理一下。

物理页映射
dma_map_single->dma_map_single_attrs->(ops->map_page)->__swiotlb_map_page-> swiotlb_map_page-> map_single->swiotlb_tbl_map_single ...

dma_addr_t swiotlb_map_page(struct device *dev, struct page *page,
       unsigned long offset, size_t size,
       enum dma_data_direction dir,
       unsigned long attrs)
{
 //根据页号获取物理地址,进而获得DMA地址
 phys_addr_t map, phys = page_to_phys(page) + offset;
 dma_addr_t dev_addr = phys_to_dma(dev, phys);
 
 BUG_ON(dir == DMA_NONE);
 /*
  * If the address happens to be in the device's DMA window,
  * we can safely return the device addr and not worry about bounce
  * buffering it.
  */
 //判断DMA的寻址能力是否能够覆盖上一步得到的物理地址,如果能的话,直接返回物理地址,否则采用swiotlb机制分配内存。
 if (dma_capable(dev, dev_addr, size) && swiotlb_force != SWIOTLB_FORCE)
  return dev_addr;
 
 trace_swiotlb_bounced(dev, dev_addr, size, swiotlb_force);
 
 /* Oh well, have to allocate and map a bounce buffer. */
 //用swiotlb机制分配内存
 map = map_single(dev, phys, size, dir, attrs);
 //判断调用swiotlb机制分配的内存物理地址是否在DMA寻址能力范围内,如果在的话直接返回,否则直接返回备用地址
 if (map == SWIOTLB_MAP_ERROR) {
  swiotlb_full(dev, size, dir, 1);
  return swiotlb_phys_to_dma(dev, io_tlb_overflow_buffer);
 }
 
 dev_addr = swiotlb_phys_to_dma(dev, map);
 
 /* Ensure that the address returned is DMA'ble */
 if (dma_capable(dev, dev_addr, size))
  return dev_addr;
 
 attrs |= DMA_ATTR_SKIP_CPU_SYNC;
 swiotlb_tbl_unmap_single(dev, map, size, dir, attrs);
 
 return swiotlb_phys_to_dma(dev, io_tlb_overflow_buffer);
}


CPU访问的内存转为DMA地址

判断DMA的寻址能力是否能够覆盖上一步得到的地址,如果能的话,直接返回地址,否则采用swiotlb机制分配内存。

通过map_single用swiotlb机制分配内存,详情见上面

至此,CPU对应的Data Buffer和DMA对应的Bounce Buffer就映射起来了

数据同步

dma_sync_single_for_device

dma_sync_single_for_device
    _swiotlb_sync_single_for_device
        swiotlb_sync_single_for_device
          swiotlb_sync_single(..., SYNC_FOR_DEVICE)
            swiotlb_tbl_sync_single
              swiotlb_bounce(..., DMA_TO_DEVICE)
                memcpy(vaddr, buffer + offset, sz) //将数据从Data Buffer处拷贝到Bounce Buffer
        __dma_map_area
            ENTRY(__dma_map_area)
              cmp     w2, #DMA_FROM_DEVICE
              b.eq    __dma_inv_area //invalid就是使cache中内容无效,下次使用时需要从内存中重新读取
              b       __dma_clean_area //把cache中内容刷到内存中
            ENDPIPROC(__dma_map_area)

dma_sync_single_for_cpu

dma_sync_single_for_cpu
    __swiotlb_sync_single_for_cpu
        __dma_unmap_area
            ENTRY(__dma_unmap_area)
              cmp     w2, #DMA_TO_DEVICE
              b.ne    __dma_inv_area //invalid就是使cache中内容无效,下次使用时需要从内存中重新读取
              ret
            ENDPIPROC(__dma_unmap_area)
        swiotlb_sync_single_for_cpu
          swiotlb_sync_single(..., SYNC_FOR_CPU)
            swiotlb_tbl_sync_single
              swiotlb_bounce(..., DMA_FROM_DEVICE)
                memcpy(buffer + offset, vaddr, sz) //将数据从Bounce Buffer处拷贝到Data Buffer

STMMAC网卡驱动执行sync操作的dump_stack:

[  480.591373] ===swiotlb_bounce orig_addr 2351cfb042 to tlb_addr dcc1e000  size 60 dir 2 
[  480.599367] CPU: 0 PID: 0 Comm: swapper/0 Not tainted 4.19.0-fix-full-10+ #61
[  480.606488] Hardware name: PHYTIUM LTD D2000/D2000, BIOS  
[  480.611959] Call trace:
[  480.614394]  dump_backtrace+0x0/0x1b8
[  480.618043]  show_stack+0x24/0x30
[  480.621345]  dump_stack+0x90/0xb4
[  480.624647]  swiotlb_bounce+0x48/0xa8
[  480.628296]  swiotlb_tbl_sync_single+0xa0/0xd0
[  480.632727]  swiotlb_sync_single+0xa8/0x108
[  480.636897]  swiotlb_sync_single_for_cpu+0x40/0x50
[  480.641674]  __swiotlb_sync_single_for_cpu+0x68/0x78
[  480.646631]  stmmac_rx+0x790/0xa30 [stmmac]
[  480.650807]  stmmac_napi_poll+0xdc/0x120 [stmmac]
[  480.655498]  net_rx_action+0x180/0x410
[  480.659234]  __do_softirq+0x11c/0x30c
[  480.662883]  irq_exit+0x108/0x120
[  480.666185]  __handle_domain_irq+0x6c/0xc0
[  480.670268]  gic_handle_irq+0x80/0x190
[  480.674003]  el1_irq+0xb0/0x140
[  480.677131]  arch_cpu_idle+0x30/0x1b8
[  480.680781]  do_idle+0x1dc/0x2a8
[  480.683996]  cpu_startup_entry+0x2c/0x30
[  480.687906]  rest_init+0xb8/0xc8
[  480.691121]  start_kernel+0x4a8/0x4d4
 

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

古井无波 2024

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值