[转载]Linux内存管理 —— DMA和一致性缓存

adtxl
2023-02-27 / 0 评论 / 604 阅读 / 正在检测是否收录...

版权声明:本文为CSDN博主「落尘纷扰」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jasonchen_gbd/article/details/79462064

1. 出现内存不一致的原因

CPU写内存的时候有两种方式:

  1. write through: CPU直接写内存,不经过cache。
  2. write back: CPU只写到cache中。cache的硬件使用LRU算法将cache里面的内容替换到内存。通常是这种方式。

DMA可以完成从内存到外设直接进行数据搬移。但DMA不能访问CPU的cache,CPU在读内存的时候,如果cache命中则只是在cache去读,而不是从内存读,写内存的时候,也可能实际上没有写到内存,而只是直接写到了cache。

image.png

这样一来,如果DMA从将数据从外设写到内存,CPU中cache中的数据(如果有的话)就是旧数据了,这时CPU在读内存的时候命中cache了,就是读到了旧数据;CPU写数据到内存时,如果只是先写到了cache,则内存里的数据就是旧数据了。这两种情况(两个方向)都存在cache一致性问题。例如,网卡发包的时候,CPU将数据写到cache,而网卡的DMA从内存里去读数据,就发送了错误的数据。

imageba64d8cc868cdb12.png

2. 如何解决一致性问题

主要靠两类APIs:

2.1 一致性DMA缓存

DMA需要的内存由内核去申请,内核可能需要对这段内存重新做一遍映射,特点是映射的时候标记这些页是不带cache的,这个特性也是存放在页表里面的。

上面说“可能”需要重新做映射,如果内核在highmem映射区申请内存并将这个地址通过vmap映射到vmalloc区域,则需要修改相应页表项并将页面设置为非cache的,而如果内核从lowmem申请内存,我们知道这部分是已经线性映射好了,因此不需要修改页表,只需修改相应页表项为非cache即可。

相关的接口就是dma_alloc_coherent()和dma_free_coherent()。dma_alloc_coherent()会传一个device结构体指明给哪个设备申请一致性DMA内存,它会产生两个地址,一个是给CPU看的,一个是给DMA看的。CPU需要通过返回的虚拟地址来访问这段内存,才是非cache的。至于dma_alloc_coherent()的内部实现可以不关注,它是和体系结构如何实现非cache(如mips的kseg1)相关,也可能与硬件特性(如是否支持CMA)相关。

还有一个接口dma_cache_sync(),可以手动去做cache同步,上面说dma_alloc_coherent()分配的是uncached内存,但有时给DMA用的内存是其他模块已经分配好的,例如协议栈发包时,最终要把skb的地址和长度交给DMA,除了将skb地址转换为物理地址外,还要将CPU cache写回(因为cache里可能是新的,内存里是旧的)。

贴出一种实现:

void dma_cache_sync(struct device *dev, void *vaddr, size_t size,
            enum dma_data_direction direction)
{
    void *addr;

    addr = __in_29bit_mode() ?
           (void *)CAC_ADDR((unsigned long)vaddr) : vaddr;

    switch (direction) {
    case DMA_FROM_DEVICE:       /* invalidate only */
        __flush_invalidate_region(addr, size);
        break;
    case DMA_TO_DEVICE:     /* writeback only */
        __flush_wback_region(addr, size);
        break;
    case DMA_BIDIRECTIONAL:     /* writeback and invalidate */
        __flush_purge_region(addr, size);
        break;
    default:
        BUG();
    }
}

调用这个函数的时刻就是上面描述的情况:因为内存是可cache的,因此在DMA读内存(内存到设备方向)时,由于cache中可能有新的数据,因此要先将cache中的数据写回到内存;在DMA写内存(设备到内存方向)时,cache中可能还有数据没有写回,为了防止cache数据覆盖DMA要写的内容,要先将cache无效。注意这个函数的vaddr参数接收的是虚拟地址。

例如在发包时将协议栈的skb放进ring buffer之前,要做一次DMA_TO_DEVICE的flush。对应的,在收包后为ring buffer中已被使用的skb数据buffer重新分配内存后,要做一次DMA_FROM_DEVICE的flush(invalidate的时候要注意cache align)。

2.2 流式DMA映射(DMA Streaming Mapping)

相关接口为 dma_map_sg(), dma_unmap_sg(),dma_map_single(),dma_unmap_single()。

一致性缓存的方式是内核专门申请好一块内存给DMA用。而有时驱动并没这样做,而是让DMA引擎直接在上层传下来的内存里做事情。例如从协议栈里发下来的一个包,想通过网卡发送出去。

但是协议栈并不知道这个包要往哪里走,因此分配内存的时候并没有特殊对待,这个包所在的内存通常都是可以cache的。

这时,内存在给DMA使用之前,就要调用一次dma_map_sg()或dma_map_single(),取决于你的DMA引擎是否支持聚集散列(DMA scatter-gather),支持就用dma_map_sg(),不支持就用dma_map_single()。DMA用完之后要调用对应的unmap接口。

由于协议栈下来的包的数据有可能还在cache里面,调用dma_map_single()后,CPU就会做一次cache的flush,将cache的数据刷到内存,这样DMA去读内存就读到新的数据了。

注意,在map的时候要指定一个参数,来指明数据的方向是从外设到内存还是从内存到外设:

从内存到外设:CPU会做cache的flush操作,将cache中新的数据刷到内存。
从外设到内存:CPU将cache置无效,这样CPU读的时候不命中,就会从内存去读新的数据。

还要注意,这几个接口都是一次性的,每次操作数据都要调用一次map和unmap。并且在map期间,CPU不能去操作这段内存,因此如果CPU去写,就又不一致了。
同样的,dma_map_sg()和dma_map_single()的后端实现也都是和硬件特性相关。

2.3 其他方式

上面说的是常规DMA,有些SoC可以用硬件做CPU和外设的cache coherence,例如在SoC中集成了叫做“Cache Coherent interconnect”的硬件,它可以做到让DMA踏到CPU的cache或者帮忙做cache的刷新。这样的话,dma_alloc_coherent()申请的内存就没必要是非cache的了。

1

评论 (0)

取消