DMA那些事儿

在VIP群里,大家讨论热烈,完全可以看得出来大家都Linux技术和嵌入式技术等渴望和热诚,欢迎大家加入笨叔的VIP私密群,一起研究技术,一起奔跑,一起成长。

关于DMA

有朋友问有关DMA的相关问题,那么大家在编写驱动的时候需要额外小心敬慎。因为DMA的问题主要会影响cache的一致性。那linux内核里面提供了不少的API来帮助大家实现DMA的操作。

我们知道DMA使用的物理内存,那么在内核中主要有两类的接口来分配物理内存

  • __get_free_page()和alloc_page()来分配以页为单位的物理内存
  • kmalloc() 和 kmem_cache_alloc()来分配以字节为单位的物理内存

上述两个接口分配的内存都可以作为DMA buffer的原材料。但是我们系统分配的物理内存都是cachable的,也就是打开cache功能的。另外DMA engine在做DMA传输的时候是不需要CPU参与的,所以从这个角度来看,同一个cache line有可能CPU和DMA engine都能访问到,那就会导致cache line被破坏了,那如何去保证DMA操作的时候,cache这个捣蛋鬼不来捣乱呢?

一个简单的想法就是,我们在做DMA传输的时候,这段buffer我把cache给关闭了,这个就是kernel实现的一致性的DMA buffer mapping的(英文叫做:Consistent DMA mappings)。这里consistent的意思是synchronize或者conherent的意思,CPU和DMA这两个哥们都能同时看到数据的改变,不需要软件做额外的一些flush动作。核心函数是:

dma_addr_t dma_handle;

cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);


这个函数返回两个值,其中cpu_addr是虚拟地址,CPU可以通过这个地址来访问这段buffer,另外一个dma_handle物理地址,可以传递给DMA engine。

这里分配的大小以 PAGE_SIZE为单位。

另外这个函数会调用alloc_page()来分配物理页面,所以不要在中断上下文中使用该API

大家可以想一下一致性的DMA有什么缺点?很明显的一个缺点就是cache一直都是关闭的,所以性能就会很低下。比如DMA传输完成之后,CPU去把这个DMA buffer的数据取过来,这时候cache关闭的,CPU去读写就变得很慢。那有没有办法可以保证DMA的传输的一致性,又能提高性能呢?特别是CPU去访问这段DMA buffer的性能呢?

方法是有,那就是streaming的DMA,streaming DAM有的书上翻译成流式DMA,从字面上不好理解。其实上大家可以把DMA 传输分解一下,比如:

设备的FIFO -> DMA buffer。

这个就是代码里说的DMA_FROM_DEVICE。数据流向是从设备到物理内存,注意DMA buffer是在主存储器,即DDR里,此时从CPU角度来看,CPU的任务是读取来自设备的数据,也就是我们常说的“ DMA读”。

DMA buffer -> 设备的FIFO。

这个就是代码说的DMA_TO_DEVICE。数据流向是从主存取器DDR到设备,注意DMA buffer是在主存储器,即DDR里,此时从CPU角度来看,CPU的任务是把数据写入设备,也就是我们常说的“ DMA写”。

(DMA读或者写这个说法 不是很严谨,只是大家口头禅都这么说,代码里的DMA_FROM_DEVICE和DMA_TO_DEVICE这两个宏比较严谨一些)

那很多人就疑惑了,那DMA读和DMA写,怎么和cache的操作连续在一起呢?究竟DMA读是要invalid cache还是flush cache呢?我们来看一下这个图。

从图里看到,CPU需要进行DMA写操作,也就是把内存中的buffer A写入到设备的FIFO A里面,那么有可能cache里面的数据还没有完全写入到内存的buffer A中,那这时候启动DMA的话,最终传递到设备FIFO A的数据其实不是CPU想写的,因为还有一部分数据早 潜伏在cache A中没有 sync到内存里。这个场景有点类似,我们拷贝内存到U盘,马上拔出,然后发现U盘没有东西。

我们来看一下DMA读的情况,CPU想把设备的FIFO B的数据读到内存buffer B中。那如果在开启DMA传输的时候没有去把内存buffer B的相应的cache invalid的话,那么DMA把数据从FIFO B到了内存Buffer B之后,CPU去读这个内存Buffer B的数据,那么会把之前的 残留在cache line的内容 先读到了CPU,那CPU其实是没有读到最新的FIFO B的数据的。

总结:

DMA写,需要flush cache line。

DMA读,需要invalid cache line。

流式DMA的常用的API有:

dma_handle = dma_map_single(dev, addr, size, direction);

dma_unmap_single(dev, dma_handle, size, direction);

dma_sync_sg_for_cpu()

dma_sync_sg_for_device()

流式DMA映射对于CPU何时可以操作DMA缓冲区有严格的要求,只能等到dma_unmap_single后CPU才可以操作该缓冲区。
究其原因,是因为流式DMA缓冲区是cached,在map时刷了下cache,在设备DMA完成unmap时再刷cache(根据数据流向写回或者无效),来保证了cache数据一致性,在unmap之前CPU操作缓冲区是不能保证数据一致的。因此kernel需要严格保证操作时序。

如果你建立的这个DMA buffer需要多次的来回操作,比如需要在CPU去同步访问这个buffer,以及设备去访问这个buffer。那么可以使用kernel也提供函数dma_sync_single_for_cpu与dma_sync_single_for_device来进行同步操作,而不需要 频繁的去创建和释放DMA buffer。

当然内核除了支持single page的DMA mapping,还支持scatterlists方式的DMA mapping,这个在使用过程中就要复杂一些了。

ARM里面怎么操作cache?

有同学问ARM里面怎么操作cache的?我们支持cache的操作有两种,一个flush,另外一个invalid。这里面每一种都可以flush整个cache,也可以按照range来操作。

我们先看一下代码路径:

dma_map_single() -> (ops->map_page) ->arm_dma_map_page() -> __dma_page_cpu_to_dev() ->outer_inv_range() -> v7_dma_inv_range

我们来看一下这段代码,里面主要是几个mcr操作处理器的操作。

有同学问了,这几个协处理操作啥鸟意思?

ok,笨叔以ARM® Architecture Reference Manual 这文件为例子,看看ARM芯片手册上是怎么描述的。

首先我们翻到B 5.8.1这一章里面,我们看这里有描述CP15这个协处理器的描述,我们知道它是干啥的,有哪些东东。

从图中红色圈出来的可以看到CP15里面的C7主要是做cache管理的,还有地址转换等。

  1. 我们继续去看c7的描述,翻到1765页。

从这张描述C7寄存器的图里看到CRm为c14,opc2为1的是做什么用的,这里有一个寄存器名字,DCCIMVAC,它主要是按照MVA(这里可以理解为虚拟地址或者物理地址)来clean和无效data cache line的。另外opc2为2的寄存器是DCCISW,主要是按照set和way来无效cache line的。

  1. 我们可以在手册中搜索“DCCIMVAC”找到更加详细的描述,比如在B 6.2里面有更加详细的介绍。

更多更有趣的问题

VIP私密群里还有很多很有趣的问题:

请问匿名页面是存在于用户地址空间,page cache是存在于内核空间的,这样子理解对吗?如果是这样子的话,那么如果要统计一个用户进程占据的物理内存时,是否应该要加入page cache这部分消耗的内存?

malloc申请的内存,如果先从highmem分配,到了highmem的min水位,是直接被堵住进行direct reclaim,还是跳到lowmem(假设lowmem内存充足)继续分配?

假如先运行A,在运行B,这个时候A进程cachemiss,B进程cache命中,在回到A,A如果invalid,有可能是invalid B命中的那个cache?

欢迎大家加入笨叔的VIP私密群

你可能感兴趣的