Linux DMA子系统(1):DMA Engine体系结构

 

目录

1. 前言

2. DMA简介

2.1 DMA概念 

2.2 DMA原理和实现 

2.3 DMA传输过程

2.4 DMA传输需要的参数

2.4.1 transfer size

2.4.2 transfer width

2.4.3 burst size

2.4.4 DMA传输方式

3. Linux DMA子系统体系结构

4. DMA控制器与CPU怎样分时使用内存

4.1 停止CPU访问内存

 4.2 周期挪用

4.3 DMA与CPU交替访问内存

5. DMA 导致的问题

5.1 关于流式 DMA 和一致性 DMA 的区别

6. 参考文章


1. 前言

最近在学习Linux DMA相关的内容,会从DMA控制器驱动(provider,提供DMA服务)和使用DMA控制器提供的服务的外设驱动(consumer)这两个方面,来分析介绍DMA Engine的体系结构。

2. DMA简介

2.1 DMA概念 

DMA(direct memory access),即不经过CPU,直接访问内存。因为不管是memory和memory之间还是memory和设备之间进行数据搬运,对CPU来说都是枯燥乏味的,且非常浪费CPU的时间,造成CPU无法及时处理一些实时的事件。因此,为了解放CPU,可以让CPU在搬运数据的这段时间可以去做更有意义的事情,工程师们就设计出来一种专门用来搬运数据的器件,即DMA控制器,让它来协助CPU进行数据搬运工作,下图是DMA的硬件示意图。

2.2 DMA原理和实现 

DMA 的原理:即CPU将需要迁移的数据的位置告诉给DMA,包括源地址,目的地址以及需要迁移的长度,然后启动DMA设备,DMA设备收到命令之后,就去完成相应的操作,最后通过中断反馈给CPU。

DMA的实现:在实现DMA传输时,是DMA控制器掌控着总线,也就是说,这里会有一个控制权转让的问题,我们当然知道,计算机中权限最大的就是CPU,这个DMA暂时掌管的总线控制权当前也是CPU赋予的,在DMA完成传输之后,会通过中断通知CPU收回总线控制权。

2.3 DMA传输过程

DMA传输的过程:一个完整的DMA传输过程必须经过DMA请求、DMA响应、DMA传输、DMA结束这四个阶段。

DMA 请求:CPU对DMA控制器初始化,并向I/O接口发出操作命令,I/O接口提出DMA请求。
DMA 响应:DMA控制器对DMA请求判别优先级以及屏蔽位,向总线裁决逻辑提出总线请求,当CPU执行完成当前的总线周期之后即可释放总线控制权。此时,总线裁决逻辑输出总线应答,表示DMA已经就绪,通过 DMA 控制器通知I/O接口开始DMA传输。
DMA 传输:在DMA控制器的引导下,在存储器和外设之间进行数据传送,在传送过程中不需要CPU的参与。
DMA 结束:当完成既定操作之后,DMA控制器释放总线控制权,并向I/O接口发出结束信号,当I/O接口收到结束信号之后,一方面停止I/O设备的工作,另一方面向CPU提出中断请求,使CPU从不介入状态解脱,并执行一段检查本次DMA传输操作正确性的代码。最后带着本次操作的结果以及状态继续执行原来的程序。

2.4 DMA传输需要的参数

2.4.1 transfer size

在最简单的DMA传输中,只需要为DMA controller提供一个参数,即transfer size它就可以进行数据传输了:在每一个时钟周期,DMA controller将1byte的数据从一个buffer搬到另一个buffer,直到搬完transfer size个bytes即可停止。

2.4.2 transfer width

不过在实际应用场景中,只有一个transfer size参数是不够的,因为有些设备可能需要在一个时钟周期中,传输指定bit的数据,例如:

memory之间传输数据的时候,希望能以总线的最大宽度为单位(32-bit、64-bit等),以提升数据传输的效率。

而在音频设备中,需要每次写入精确的16-bit或者24-bit的数据;等等。

因此,为了满足这些多样的需求,我们需要为DMA controller提供一个额外的参数,即transfer width。

/**
 * enum dma_slave_buswidth - defines bus width of the DMA slave
 * device, source or target buses
 */
enum dma_slave_buswidth {
	DMA_SLAVE_BUSWIDTH_UNDEFINED = 0,
	DMA_SLAVE_BUSWIDTH_1_BYTE = 1,
	DMA_SLAVE_BUSWIDTH_2_BYTES = 2,
	DMA_SLAVE_BUSWIDTH_3_BYTES = 3,
	DMA_SLAVE_BUSWIDTH_4_BYTES = 4,
	DMA_SLAVE_BUSWIDTH_8_BYTES = 8,
	DMA_SLAVE_BUSWIDTH_16_BYTES = 16,
	DMA_SLAVE_BUSWIDTH_32_BYTES = 32,
	DMA_SLAVE_BUSWIDTH_64_BYTES = 64,
};

2.4.3 burst size

另外,当传输的源或者目的地是memory的时候,为了提高效率,DMA controller不愿意每一次传输都访问memory,而是在内部开一个buffer,将数据缓存在自己buffer中:

memory是源的时候,一次从memory读出一批数据,保存在自己的buffer中,然后再一点点(以时钟为节拍),传输到目的地;

memory是目的地的时候,先将源的数据传输到自己的buffer中,当累计一定量的数据之后,再一次性的写入memory。

这种场景下,DMA控制器内部可缓存的数据量的大小,称作burst size。

2.4.4 DMA传输方式

DMA传输方式分为两种,即block DMA方式和scatter-gather DMA方式。

在DMA传输数据的过程中,要求源物理地址和目标物理地址必须是连续的。但是在某些计算机体系中,如IA架构,连续的存储器地址在物理上不一定是连续的,所以DMA传输要分成多次完成。

如果在传输完一块物理上连续的数据后引起一次中断,然后再由主机进行下一块物理上连续的数据传输,那么这种方式就为block DMA方式。

Scatter-gather DMA使用一个链表描述物理上不连续的存储空间,然后把链表首地址告诉DMA master。DMA master在传输完一块物理连续的数据后,不用发起中断,而是根据链表来传输下一块物理上连续的数据,直到传输完毕后再发起一次中断。

很显然,scatter-gather DMA方式比block DMA方式效率高。

3. Linux DMA子系统体系结构

Linux DMA体系结构主要分为三个部分:DMA provider、DMA consumer以及DMA核心层(DMA Engine)。

DMA provider:即DMA控制器驱动。

DMA consumer:主要是用于消费(申请)DMA请求,对于SPI、I2C等控制器驱动,会去申请DMA通道并使能DMA传输;

DMA核心层(DMA Engine):提供公共接口函数。

4. DMA控制器与CPU怎样分时使用内存

外围设备可以通过DMA控制器直接访问内存,与此同时,CPU可以继续执行程序逻辑,通常采用三种方法实现DMA控制机与CPU分时使用内存,分别是:停止CPU访问内存、周期挪用以及DMA与CPU交替访问内存。

4.1 停止CPU访问内存

当外围设备要求传送一批数据时,由DMA控制器发一个停止信号给CPU,要求CPU放弃对地址总线、数据总线和有关控制总线的使用权,DMA控制器获得总线控制权之后,开始进行数据传输,在一批数据传输完毕之后,DMA控制器通知CPU可以继续使用内存,并把总线控制权交还给CPU。下图就是这样的一个传输图,很显然,在这种 DMA 传输过程中,CPU基本处于不工作状态或者保持状态。

优点:控制简单,他是用于数据传输率很高的设备进行成组的传输。

缺点:在DMA控制器访问内存阶段,内存效能没有充分发挥,相当一部分的内存周期是空闲的,这是因为外围设备传送两个数据之间的间隔一般大于内存存储间隔,即使是再告诉的I/O存储设备也是如此(就是内存读写速率比SSD等都快)。

 4.2 周期挪用

当I/O设备没有DMA请求时,CPU按照程序要求访问内存;一旦I/O设备有 DMA 请求,则由I/O设备挪用一个或者几个内存周期,这种传送方式如下图所示:

I/O设备要求DMA传送时可能遇到两种情况:

第一种:此时CPU不需要访问内存,例如,CPU正在执行乘法指令,由于乘法指令执行时间长,此时I/O访问与CPU访问之间没有冲突,则I/O设备挪用一两个内存周期是对CPU执行没有任何的影响。

第二种:I/O设备要求访问内存时,CPU也需要访问内存,这就产生了访问内存冲突,在这种情况下I/O设备优先访问,因为I/O访问有时间要求,前一个I/O数据必须在下一个访问请求来到之前存储完毕。显然,在这种情况下,I/O设备挪用一两个内存周期,意味着CPU延缓了对指令的执行,或者更为明确的讲,在CPU执行访问指令的过程中插入了 DMA 请求。周期挪用内存的方式与停止CPU访问内存的方式对比,这种方式,既满足了I/O数据的传送,也发挥了CPU和内存的效率,是一种广泛采用的方法,但是I/O设备每一周期挪用都有申请总线控制权,建立总线控制权和归还总线控制权的过程,所以不适合传输小的数据。这种方式比较适用于I/O设备读写周期大于内存访问周期的情况。

4.3 DMA与CPU交替访问内存

如果CPU的工作周期比内存的存储周期长很多,此时采用交替访问内存的方法可以使DMA传送和CPU同时发挥最高效率。这种传送方式的时间图如下:

此图是DMA和CPU交替访问内存的消息时间图,假设CPU的工作周期为1.2us,内存的存储周期小于0.6us,那么一个CPU周期可以分为C1和C2两个分周期,其中C1专供DMA控制器访问内存,C2专供CPU访问内存。这种方式不需要总线使用权的申请、建立和归还的过程,总线的使用权是通过C1和C2分时机制决定的。CPU和DMA控制器各自有自己的访问地址寄存器、数据寄存器和读写信号控制寄存器。在C1周期中,如果DMA控制器有访问请求,那么可以将地址和数据信号发送到总线上。在C2周期内,如果CPU有访问内存的请求,同样将请求的地址和数据信号发送到总线上。事实上,这是用C1、C2控制的一个多路转换器,这种总线控制权的转移几乎不需要什么时间,所以对于DMA来讲是很高效率的。

这种传送方式又称为“透明DMA”方式,其由来是由于DMA传送对于CPU来讲是透明的,没有任何感觉。在透明DMA方式下CPU不需要停止主程序的运行,也不进入等待状态,是一种很高效率的工作方式,当然,相对应的硬件设计就会更加复杂。

以上是传统意义的DMA:是一种完全由硬件执行I/O交换的工作方式。这种方式中,DMA控制器和CPU完全接管对总线的控制,数据交换不经过CPU,而直接在内存和I/O设备之间进行。DMA工作时,由DMA控制器向内存发起地址和控制信号,进行地址修改,对传输的数据进行统计,并以中断的形式通知CPU数据传输完成。

5. DMA 导致的问题

DMA不仅仅只会带来效率的提升,同样,他也会带来一些问题,最明显的就是缓存一致性问题。想象一下,现代的CPU都是自带一级缓存、二级缓存甚至是三级缓存,当CPU访问内存某个地址时,暂时先将新的值写入到缓存中,但是没有更新外部内存的数据,假设此时发生了DMA请求且操作的就是这一块在缓存中更新了而外部内存没有更新的内存地址,这样DMA读到的就是非最新数据;相同的,如果外部设备通过DMA将新值写入到内存中,但是CPU访问得到的确实缓存中的数据,这样也会导致拿到的不是最新的数据。

为了能够正确进行DMA操作,必须进行必要的Cache操作,Cache的操作主要分为invalidate(作废)和writeback(回写),有时候也会二者一同使用。如果DMA使用了Cache,那么Cache一致性问题是必须要考虑的,解决的最简单的办法就是禁止DMA目标地址范围的Cache功能,但是这样会牺牲掉一定的性能。因此,在DMA是否使用cache的问题上,可以根据DMA缓冲区的期望保留时间长短来决策。DMA被区分为了:一致性DMA映射和流式DMA映射。

一致性DMA映射:他申请的缓存区之后会被以非缓存的形式映射,一致性映射具有很长的生命周期,在这段时间内占用映射寄存器,即使不再使用也不会释放,一般情况下,一致性DMA的生命周期会被设计为驱动的生命周期(也就是在init里面注册,在exit里面释放)。

流式DMA映射:他的实现比较复杂,表现特征为使用周期很短,他的实现中会主动保持缓存的一致性。在使用方法上,流式 DMA 还需要指定内核数据的流向,不然会导致不可预期的后果。不过很多的现代处理能能够自己来保证CPU和DMA控制器之间的cache一致性问题,比如ARM的ACP功能,这样像dma_map_single函数只是返回物理地址,而dma_unmap_single则什么都不做,这样极大的提升了系统性能。

5.1 关于流式 DMA 和一致性 DMA 的区别

在一致性DMA映射中,它采用的是系统预留的一段DMA内存用于DMA操作,这一段内核在系统启动阶段就已经预留完毕,比如arm64平台会在dts文件中写明系统预留的DMA内存段位于何处,并且会被标志为用于DMA一致性内存申请,如果你有关注DMA的一致性映射操作API就会发现,一致性DMA不会去使用别的地方申请的内存,他都是通过dma_alloc_coherent自我申请内存,然后驱动自己填充数据最后被提交给DMA控制器。

流式DMA中,他可以是随意的内存交给DMA进行处理,你不需要从系统预留的DMA位置进行内存申请,任何普通的kmalloc申请的内存都能交给DMA控制器进行操作。

上面的两点是他们二者一个很重要的区别,我不知道为何在其他的文章中没有被写到过,那么以此衍生开的就是这二者是如何做到缓存一致性的:一致性 DMA 要做到缓存一致性很简单,在DMA内存申请的过程中,首先进行一个ioremap_nocache的映射,然后调用函数dma_cache_wback_inv 保证缓存已经刷新到位之后,后面使用这一段内存时不存在一二级缓存;而流式DMA则比较复杂,他不能直接禁止缓存,因为流式DMA可以使用系统中的任意地址范围的地址,CPU总不能将系统所有的地址空间都禁止缓存,这不科学,那么为了实现缓存一致性,流式DMA需要不断的对缓存进行失效操作,告诉CPU这一段缓存是不可信的,必须从内存中重新获取。说到这里,我想起之前的有篇文章提到过说他们二者的区别就是一个是禁止缓存,一个是刷缓存,但是他没有讲清楚,到底谁是刷缓存,谁是禁止缓存呢?现在从上面的理解来看,很明显,一致性DMA就是直接将缓存禁止,而流式DMA则是将缓存失效刷新。

6. 参考文章

\Documentation\dmaengine\provider.txt

DMA 与 scatterlist 技术简介 – 字节岛技术分享

Linux DMA Engine framework(1) - DMA Introduction_Hacker_Albert的博客-CSDN博客

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值