1.DMA通道
DMA(Direct Memory Access)通道建立在设备和RAM之间,DMAC(DMA Controler)与设备I/O控制器相互作用共同实现数据传送。
在PC中,DMA控制器位于主板上负责管理I/O总线的南桥上。典型的PC架构的数据通道示意图如下:
DMAC(DMA Controller)一旦被CPU激活,就可以自行传送数据。在实现DMA传输时,由DMA控制器直接掌管总线,因此,存在着一个总线控制权转移问题。在DMA传输前,CPU要把总线控制权交给DMA控制器。在DMA传输后,DMAC发出一个中断请求,将总线控制权回交给CPU。DMA控制器获得总线控制权后,CPU即刻挂起或只执行内部操作,由DMA控制器输出读写命令,直接控制RAM与I/O接口进行DMA传输。
需要明确的是,I/O设备内部一般自带缓存,即通常所说的设备内存。从数据传输的源宿角度分析,DMA通道的两端分别是RAM和设备内存。设备内存一般选用快速低功耗的SRAM材质,例如AR9331交换芯片PCU单元中有4KB的Tx FIFO和2KB的Rx FIFO,“The GE0 and GE1 support 2K transmit FIFO and 2K receive FIFO.”
使用DMAC最多的是磁盘驱动器和其他需要一次传送大量字节的慢速设备,例如PCI网卡(NIC)。
2.Linux中的DMA层
DMA操作的核心是DMA内存映射,包括一致性DMA映射、流式DMA映射和分散/聚集映射。以下是Linux内核DMA层的大体框架图:
从图中可以看出,Linux内核中的DMA层为设备驱动程序提供标准的DMA映射接口,例如一致性映射类型的dma_alloc_coherent和流式映射类型的dma_map_single。这些接口屏蔽了不同平台之间的差异,为设备驱动程序提供了良好的可移植性。
3.DMA描述符
SoC datasheet通常会提供DMA rx/tx的descriptor address和trigger control寄存器。在嵌入式软件开发中,DMA描述符数组是个很重要的概念。
DMA描述符数组(DMA Descriptor Array/Ring/Chain)是一个形如unsigned long* hw_desc[DESC_NUM]的指针数组,每个指针(hw_desc[i])指向一个描述符。这个描述符是由硬件定义的,其数据结构一般由datasheet或sdk定义。
3.1 硬件描述符(h/w descriptor)
硬件描述符通常包含以下五个部分:
<1>控制位(empty flag/own bit):descriptor empty/owned by DMA or not,该位域描述的是descriptor对CPU/DMA的有效性。empty_flag和own_bit是站在不同角度对控制状态的同功描述,empty表示descriptor上尚无数据(包)。对于RX,descriptor empty(owned by DMA)表示亟待DMA搬运数据进来挂到该descriptor的DMA缓存上;对于TX,descriptor empty(not owned by DMA)表示亟待CPU往descriptor上挂载待发送的数据包。下文主要基于DMAown_bit分析控制位,读者可自行做等价转换。
<2>数据包地址(data buffer address):该指针指向DMA传输时的源端或目的端内存区,有的地方称之为DMA缓存。DMA缓存是数据包的终极归宿,即cluster的数据区(mBlk::mBlkHdr.mData inVxWorks, sk_buff.data in Linux)。
<3>数据包长度(packet length):rx/tx数据包的有效长度。
<4>环尾标(wrap bit):wrap bit of the last descriptor,该位域标记最后一个描述符,用于判断溢出(rx overflow)。站在“环”的角度分析,也可以理解为按照数组索引的回环点。
<5>环链结(next pointer):该指针指向下一个描述符。尽管分配的DMA描述符数组已经是线性存储,但是硬件总是习惯按照地址来查找下一个描述符。软件则更习惯在RX ISR中使用数组索引来遍历描述符环上待收割(reap)的数据包。
有的地方称之为BD(Buffer Descriptor),由于是hardware specific,故开发者一般无需修改h/w descriptor数据结构。
DMA描述符数组hw_desc[]的地址是DMA映射的虚拟地址,它是描述符环的基地址,需要配置到SoC芯片的相关DMA寄存器中,例如AR9331Datasheet中的DMARXDESCR(Pointer to Rx Descrpitor)和DMATXDESCR_Q0(Descriptor Address for Queue 0 Tx)。很显然,这个数组需要分配或转换到非缓存的区段(例如MIPS中的kseg1段)。但每个描述符所指向的DMA缓存(desc buffer)通常是分配在缓存的区段(例如MIPS中的kseg0段),使用cache主要是出于访存性能考虑。
3.2 软件描述符(s/w descriptor)
硬件描述符(h/w descriptor)更多的关注分组(例如以太网帧)的传输,而缺乏对数据包或数据链的软件组织层次关怀。数据包在网络协议栈各层之间流动时,软件层面需要维系完整的数据链信息,包括横向的包内分片(fragment)和纵向的多包链化(chain),以便进行链接跟踪(conntrack)。以网卡为例,NIC的ISR中首先会创建一个buf(mBlk/mbuf in VxWorksor or sk_buff in Linux),分组的内容将被封装到这个buf结构体中,进而调用相应函数(END_RCV_RTN_CALL(END_OBJ*,M_BLK_ID)in VxWorks or netif_rx(sk_buff*)in Linux)将数据包推送到网络子系统(TCP/IP协议栈)中的高层代码中。
数据包以mBlk或sk_buff的形式在协议栈之间流动,因此在软件层面,往往根据信息组织的完整性需要对h/w descriptor进行适当地扩展。通常,s/w descriptor是h/w descriptor的container。除了数据包的组织扩展以外,也可以根据需要增加一些描述信息和维护信息,例如可添加用于跟踪tx timeout的timestamp。
4.DMA流程
4.1 DMA挂环及启动
我们知道读写操作存在差异性,rx是不定期发生的,故rx descriptor ring上需要预挂cluster随时待命;tx往往是由我们主动发起的,驱动的send调用中将数据包cluster挂到tx descriptor ring上。
设备驱动程序在初始时,需要将RX环上的每个descriptor的控制位手工置1(owned by DMA,empty and not available for CPU to handle),意即当前对DMA有效(available for DMA),CPU虚位以待DMA从设备传送数据进来。初始化TX环上的每个descriptor的控制位手工置0(not owned by DMA,empty and available for CPU to fill),意即当前对CPU有效(available for CPU),DMA虚位以待CPU将待发数据包挂载到TX环上。
一般来说,网络设备(MAC)驱动层配置就绪之后,就可以通过配置DMA RX trigger control寄存器启用DMA RX通道。对于TX流程,往往在数据包准备好且挂接到TX环上后配置DMA TX trigger control寄存器启动DMA TX。
4.2 DMA环的大小
rx环上的每个描述符(descriptor)指向一个缓存实体(buffer),因此环的大小在一定程度上决定了收包的能力。rx环太小会造成频繁的rx overflow,势必会影响吞吐量性能。tx环的size将决定我们能往其上挂多少个待发数据包(packet buffer to be sent),tx环太小也会影响吞吐量性能。但是rx/tx环并不是越大越好,需要结合内存余量来配置,而且要和CPU的处理能力适配,综合权衡才能获得最佳的性能(performance balance)。Windows PC的网络适配器高级选项里面的接收缓冲区和发送缓冲区(传输缓冲区)的数量一般就是指RX/TX环的大小。
4.3 DMA过程控制
(1)在向内传输(rx)时,设备收到数据包后(或设备RX FIFO满时)将通知DMAC,DMA开始将数据从设备内存传送到预先挂在RX环上的DMA缓冲区上。此时,一方面,DMAC将对应描述符的控制位自动置0(not owned by DMA,not empty and available for CPU to handle),意即等待CPU进行消化处理;另一方面,DMAC将向CPU发起中断请求。在RX ISR中,对RX环进行扫描,其中控制位为0的描述符上的DMA缓冲区对应的数据包将被采摘下来封装成mBlk/sk_buff,并将由END_OBJ/net_device ISR推送到TCP/IP协议栈队列,等待网络子系统的一系列处理。
通常,在RX ISR中需要从netPool/skbPool中重新分配一个新的mBlk/sk_buff,并将其补挂(参考下文的流式映射)到刚才buffer被采摘走的descriptor上(需要将控制位手工置1),如此则可以保证RX描述符环总是虚位以待。被采摘走的mBlk/sk_buff的生命周期将由网络子系统控制,释放或用来构造tx包。
当协议栈处理包流程(中断处理下半部)过长时,将会导致netPool/skbPool吃紧;另一方面,中断响应(中断上半部)不及时将会引发rx overflow。在某些SoC芯片中,rx overflow可能会将DMA暂停一段时间,以避免频繁中断。以下表述摘自AR9331 Datasheet:
This bit(DMARXSTATUS::RXOVERFLOW) is set when the DMA controller reads a set empty flag(not available for DMA) in the descriptor it is processing.The DMA controller clears this bit(DMARXCTRL::RXENABLE) when it encounters an RX overflow or bus error state.
(2)本地协议栈对rx进来的数据包进行反馈(ACK)或转发(Forward)时,需要将数据包挂载到tx环上空闲的描述符上,空闲的标准是控制位为0(not owned by DMA)。挂上tx环后,我们需要将描述符控制位手工置1(owned by DMA,not empty and available for DMA to xmit),表示TX环上的该描述符上挂载着待发的数据包。紧接着,启动DMA TX trigger,DMAC将在抢到总线后,将控制位为1的描述符上的数据包传送(拷贝)到设备内存(TX FIFO)中。
我们总是假设DMAC和设备I/O控制器之间能够很好地同步工作,故通常较少提及TX OVERFLOW事件。DMAC将成功传送到设备内存的数据包对应的描述符的控制位自动置0(not owned by DMA,empty and available for CPU to reuse/refill),并向CPU发出中断请求。设备驱动程序一般在TX ISR或下一次的send中,扫描TX环上控制位为0的描述符环,并释放回收(reap)挂在上面的数据包缓存。
5.DMA与cache的一致性问题
5.1 cache一致性问题
下图展示了CPU、cache、RAM和Device之间的数据流动。
中间CPU和内存直接交互的为在kseg1地址空间的操作;上下经过Cache的为kseg0地址空间的操作(指针对不适用MMU的情况)。
由于数据区域对于进程来说是可读写的,而指令数据对于进程来说是只读的,故程序源码编译后分为“程序指令”和“程序数据”两种段,这种分区虚存有利于权限管理。当系统中运行着多个进程的实例副本时,由于指令是一样的,所以内存中只需要保存一份程序的指令部分。现代处理器的L1 Cache设计时基本上采用与程序分段思想呼应的哈佛结构,即将指令和数据分离,分别叫做I-Cache、D-Cache,各自有独立的读写端口(I-Cache只读,不需要写端口)。哈佛结构有利于提高程序的局部性,具体来说能够提高CPU对缓存的命中率。
如果RAM(Memory)和Device之间的一次数据交换改变了RAM中DMA缓存区的内容,假设在这个案例里恰好cache中缓存了DMA缓冲区对应RAM中一段内存块,如果没有一种机制确保cache中的内容被新的DMA缓冲区数据更新(或者无效),则cache和它对应的RAM中的一段内存块在内容上出现了不一致性。若此时CPU试图去获取Device传到RAM的DMA缓冲区中的数据,它将直接从cache获得数据,这些数据显然不是所期望的,因为cache对应的RAM中的数据已经被更新了。
单就cache一致性问题,不同体系架构有不同策略。有些在硬件层面予以保证(比如x86平台);有些则没有硬件支持而需要软件的参与(比如MIPS、ARM平台),此时设备驱动程序员需要在软件层面解决cache与DMA不一致性问题。
5.2 一致性DMA映射
前面提到过,出于访存性能考虑,在内核协议栈中流窜的数据包对应的DMA缓存区段最好开启cache,通常分配在缓存的区段(例如MIPS中的kseg0段)。
在x86平台上,硬件处理cache的一致性,所以一致性的DMA映射的建立只需要保证能获得一组所需大小的连续的物理内存页帧即可。在MIPS或ARM品台上,dma_alloc_coherent()首先分配一组连续的物理页用作后续DMA操作的缓冲区,然后在软件层面将该段物理地址空间重新映射到非缓存的虚拟地址空间,具体来说在页目录和页表项中关闭了这段映射区间上的cache功能,使得cache的一致性问题不再成为问题。因为关闭了cache,失去了高速缓存功能,所以一致性映射在性能上打了折扣。
DMA描述符本身很适合使用一致性映射,一般映射到非缓存的区段(例如MIPS中的kseg1段)。
在实际驱动程序中,一致性映射的缓冲区都是由驱动程序自身在初始化阶段分配,其生命周期可以一直延续到该驱动程序模块从内系统中移除。但在某些情况下,一致性映射也会遇到无法克服的困难,这主要是指驱动程序中使用的DMA缓冲区并非由驱动程序分配,而是来自其他模块(典型的如网络设备驱动程序中数据包传输的skb->data所指向的缓冲区),此时需要使用另一种DMA映射方式:流式DMA映射。
5.3 流式DMA映射
在流式DMA映射场合,DMA传输通道所使用的缓冲区往往不是由当前驱动程序自身分配的,而且往往每次DMA传输都会重新建立一个流式映射的缓冲区。此外,由于无法确定外部模块传入的DMA缓冲区的映射情况,所以设备驱动程序必须小心地处理可能会出现的cache一致性问题。
(1)在向内传输(rx)时,DMA设备将数据写入内存后,DMAC将向CPU发出中断请求,在RX ISR中使用该内存之前,需要先InvalidateD-Cache(sync_single_for_cpu)使cache无效重填(refill),此时CPU通过高速缓存cache获得的才是最新的数据。
(2)在向外传输(tx)时,一种可能的情形是CPU构造的本地协议栈反馈包还在D-Cache中,故在send调用中需要先Flush D-Cache(sync_single_for_device)将数据写回(write back)到内存,使DMA缓存更新为最新鲜的待发送数据再启动DMA TX trigger。
需要注意的是,在某些平台上,比如ARM,CPU的读/写用的是不同的cache(读用的是cache,写则用的是write buffer),所以建立流式DMA映射需要指明数据在DMA通道中的流向,以便由内核决定是操作cache还是write buffer。
5.4 分散/聚集DMA映射(scatter/gather map)
到目前为止,对DMA操作时缓冲区的映射问题的讨论仅限于单个缓冲区,接下来将讨论另一种类型的DMA映射——分散/聚集映射。
分散/聚集映射通过将虚拟地址上分散的多个DMA缓冲区通过一个类型为struct scatterlist的数组或链表组织起来,然后通过一次的DMA传输操作在主存RAM和设备之间传输数据。可以类比WinSock中WSA系列API提供的Scatter/GatherI/O特性。
上图显示了主存中三个分散的物理页面与设备之间进行的一次DMA传输时分散/聚集映射示意。其中单个物理页面与设备之间可以看做是一个单一的流式映射,每个这样的单一映射在内核中有数据结构strcut scatterlist来表示。
如果从CPU的角度来看这种分散/聚集映射,它对应的需求时三块数据(分别存放在三段分散的虚拟地址空间中)需要和设备进行交互(发送或者接收),通过建立struct scatterlist类型的数组/链表在一次DMA传输中完成所有的数据传输。这样,通过减少重复的DMA传送请求来提高效率。
通过上面的讨论可知,分散/聚集映射本质上是通过一次DMA操作把主存中分散的数据块在主存与设备之间进行传输,对于其中的每个数据块内核都会建立对应的一个流式DMA映射。但是对于MIPS、ARM平台而言,还是需要通过软件来保证cache的一致性问题。
参考:
《MIPS体系结构透视》
10.3<可见缓存的问题>
10<内存映射和DMA>
12.3.3<数据包的发送>
12.3.6<数据包的接收>
《精通Linux设备驱动程序开发》
10.4 DMA
10.5.2 数据传输
15 网络接口卡
15 内存映射和 DMA
11.2.6驱动DMA工作
《Blackfin DMA》《ADI blackfin DMA Descriptor》
《ARM DMA Channel Number Register》
《Linux内核分析 – 网络[二]:B44网卡驱动接收报文》
《e1000中DMA传输的问题》《网络数据包收发流程(三):e1000网卡和DMA》
《Linux内核开发之内存与I/O访问》《Linux驱动修炼之道-DMA框架源码分析》
《Linux DMA驱动开发(S3C2410)》《Linux DMA驱动分析(S3C6410)》《Linux DMA驱动构架分析(S3C2440)》