DMA实际上是赋予了设备在CPU的控制下,对memory进行读写访问的的能力。所谓的“CPU的控制”,指的是控制路径,CPU/软件当然要对DMA的地址、长度进行设置,对不同的设备的DMA空间进行隔离等;而实际的DMA动作,则是by pass CPU的。
谈到DMA,不可避免的会涉及到不同的地址转换,这对理解Linux下面的DMA是十分重要的。总共有三类地址:虚拟地址,物理地址以及总线地址。
内核通常使用虚拟地址,比如像kmallc(),vmalloc()和类似的接口返回的地址都是void *类型的虚拟地址。
虚拟内存系统,比如TLB,页表等,会将虚拟地址转换成CPU的物理地址。物理地址的类型一般为phy_addr_t或者resource_size_t。外设的寄存器,内核实际上是把它们当成物理地址来进行管理,这些地址可以在/proc/iomem中被访问。这些物理地址不能直接被驱动程序使用,必须使用ioremap()来将这些地址映射为虚拟地址之后,才能被驱动所使用。这也就是为什么我们的驱动程序中,总是会看到设备的寄存器地址空间被ioremap后,才能被正确访问。
对于IO设备来讲,它们使用的地址通常被称为总线地址(bus address)。如果设备的寄存器在MMIO地址空间,或者它使用DMA对memory进行读写访问,这个过程中设备所使用的地址其实就是总线地址。在一些硬件架构中,总线地址和CPU的物理地址是相同的,但是并不是所有的都这样。IOMMU和host bridge可以在总线地址和物理地址之间进行任意的映射。
从设备的角度来讲,DMA使用总线地址空间或者总线地址空间的一个子集。比如说,虽然系统支持64-bit的地址空间,但是经过IOMMU,设备可能仅仅使用32-bit的地址空间就可以了。
在枚举过程中,内核会获取到IO设备、它们的MMIO空间以及所挂载的桥设备。例如,一个PCI设备有BAR空间,内核从BAR空间中拿到总线地址(A),并且将它转换成CPU物理地址(B)。地址(B)被存储在struct resource结构中,并且通过/proc/iomem暴露出来。当驱动probe设备的时候,通常会用ioremap()来讲物理地址(B)映射成虚拟地址(C)。此时,就可以通过类似ioread32(C)来访问到设备在总线地址(A)上的寄存器。
驱动程序同样的,可以使用kmalloc()和类似的接口,来分配一个buffer。接口返回的地址实际上是虚拟地址,如虚拟地址(X)。虚拟内存系统将X映射到物理地址(Y)。驱动可以使用虚拟地址(X)来访问这个buffer,但是设备不能使用这个地址,因为DMA不会经过CPU的虚拟内存系统。
在一些简单的系统中,设备可以直接向屋里地址Y进行DMA访问。但是在其他的系统中,一般需要一种硬件,比如IOMMU,建立DMA地址(总线地址)和物理地址的映射关系。比如,将地址(Z)转换成地址(Y)。dma_map_single()接口其实就是做了这么一个事情:传入了虚拟地址X,然后设置IOMMU映射,然后返回了总线地址(DMA地址)Z。映射之后,驱动就可以告诉设备使用地址(Z)进行DMA,IOMMU会将对这个地址的DMA操作映射到实际的RAM中的地址Y上。
Linux系统也能支持动态DMA映射,驱动只需要在地址实际使用之前进行mapping,在使用之后进行unmap即可。
DMA相关的API
DMA相关的API与底层的架构无关,因为Linux已经替我们做好了HAL层。所以我们使用DMA API的使用,不应该使用总线相关的API,比如使用dma_map_(),而非pci_map_()接口。
在我们的驱动程序里,应该包含头文件linux/dma-mapping.h,这个头文件提供了dma_addr_t的定义。dma_addr_t可以提供对任何平台的dma地址的支持。
内存的DMA可用性
哪些内存可以被用作DMA?有一些不成文的规则。
使用页面分配函数(比如__get_gree_page*())或者通用内存分配函数(比如kmalloc()、kmem_cache_alloc())分配的地址一般是可以来用作DMA地址的。
而使用vmallc()函数分配的地址最好不要用作DMA,因为vmalloc分配出来的地址在物理地址上不一定连续,进行DMA的时候可能需要遍历页表去拿到物理地址,而将这些物理地址转成虚拟地址的时候,又需要使用到__va()类似的函数。
所以,我们一般不能使用内核镜像地址(比如data/text/bss段),或者模块镜像地址、栈地址来进行DMA,这些地址可能被映射到物理内存上的任意位置。即使我们要使用这些种类的地址来进行DMA,我们也需要确保I/O buffer是cacheline对齐的。否则,就很容易在DMA-incoherent cache上出现cache一致性的问题。
我们也不能使用kmap()返回的地址来进行DMA,原因与vmalloc()类似。
块I/O和网络设备的buffer怎么分配的呢?实际上,块I/O和网络子系统会保证它们使用的地址是可以进行DMA的。
DMA地址的限制
设备对于DMA地址空间一般都有一定的限制,比如说我们的设备的寻址能力只有24bit,那么我们一定要将限制通知到内核。
默认情况下,内核认为设备的寻址空间可以达到32bit。对于有64bit寻址能力的设备来讲,我们需要告知内核调大这个能力。而对于不足32bit寻址能力的设备来讲,需要告诉内核降低这个能力。
需要特别注意的一点是:PCI-X规范要求PCI-X设备要能够支持64-bit的寻址(DAC)的数据传输。并且某些平台(SGI SN2)也要求当IO总线是PCI-X模式时,必须要支持64bit的consistent分配。
正确的操作应该是:我们必须在设备的probe函数中向内核查询机器的DMA控制器能否正常支持当前设备的DMA寻址限制。即使设备支持默认的设置,我们最好也在probe函数中这么做。起码说明我们考虑到了这个事情。
通过调用dma_set_mask_and_coherent()可以完成这种能力通知:函数原型为
1
|
int dma_set_mask_and_coherent(struct device *dev, u64 mask);
|
这个函数可以同时通知streaming和coherent DMA的寻址能力。如果有特殊的需求的话,也可以使用下面两个单独的查询函数:
设置streaming DMA的能力:
1
|
int dma_set_mask(struct device *dev, u64 mask);
|
设置consistent DMA的能力:
1
|
int dma_set_coherent_mask(struct device *dev, u64 mask);
|
在这些函数中,dev指向设备所对应的struct device结构体,mask是一个bit域的mask值,用来描述设备支持哪些bit位宽的寻址能力。如果这个函数返回了0,则表示设备能够在当前的机器上正常的DMA。通常情况下,设备的struct device结构体会嵌入在设备的总线相关的struct device结构体中。比如,&pdev->dev是一个指向我们的设备,我们的设备是挂载到PCI上,而pdev则是指向了我们设备的PCI struct device结构体。
如果返回值不是0,那么表明我们设备在这个平台上不能正常的完成DMA操作,如果强行做的话,会导致不可预期的结果。要么我们使用不同的mask掩码,要么不要使用DMA。
这意味着当上面三个函数返回失败后,我们可以做如下几个事情:
- 如果可能的话,使用其他的mask掩码
- 如果可能的话,在数据传输时,使用非DMA方式
- 忽略这个设备,并且不初始化它(不使用这个设备了)
当出现第2点和第3点的时候,建议使用KERN_WARNING级别的打印来输出一条消息。这样以来,当用户使用我们的设备时发现有问题或者设备不能被检测到时,可以通过内核打印来找到原因。
对于标准的32bit寻址能力的设备,可以使用如下的代码进行设置:
1
|
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
|
而对于具有64bit寻址能力的设备来讲,我们一般首先会尝试设置64bit的寻址能力,但是如果返回失败的话,则会尝试32bit的寻址能力。通常情况下,之所以会设置64bit寻址能力失败,可能仅仅是因为32bit的寻址能力相对于64bit的寻址能力更加高效。比如,在Sparc64中,PCI SAC寻址比DAC寻址更快。
以具有64bit寻址能力的设备设置streaming DMA能力为例,可以使用如下代码:
1
|
int using_dac;
|
如果也要设置consistent DMA能力的话,则使用如下代码:
1
|
int using_dac, consistent_using_dac;
|
coherent DMA的掩码与streaming DMA的mask相等或者要小一些。极少数的设备只支持consistent分配,那么我们就必须使用检查dma_set_coherent_mask()的返回值。
最后,如果设备支持24bit的寻址空间的话,我们需要这样做:
1
|
if (!dma_set_mask(dev, DMA_BIT_MASK(24))) {
|
当dma_set_mask()或者dma_set_mask_and_coherent()成功(返回值为0)的话,内核会将我们提供的mask信息保存下来,并在后续做DMA mapping的时候使用。
还有一种比较特殊的场景需要考虑,比如说设备支持多个功能(如一个声卡支持播放和录音功能),不同的功能有不同的DMA寻址限制。这时我们希望在probe的时候能够针对不同功能设置合适的mask掩码。但实际上,只有最后一次调用的dma_set_mask()才生效。下面是一份伪代码来描述这种场景:
1
|
#define PLAYBACK_ADDDRESS_BITS DMA_BIT_MASK(32)
|
上面例子使用的声卡设备如果是PCI设备的话,其实就像是ISA标准残留下来的“垃圾”,因为这种设备还保持着ISA标准所具有的16M的DMA寻址空间限制。
两种类型的DMA映射
一般有两种类型的DMA映射:
一致性DMA映射(consistent DMA)
这种类型的DMA通常是在驱动初始化的时候进行映射。在驱动卸载时候进行unmap。硬件应该保证设备和CPU可以并发的访问DMA数据。不需要显示的进行数据flush,任何一方都可以看到另外一方写入的最新数据。consistent DMA也可以理解成synchronous或者coherent。当前默认会将consistent DMA申请的内存放在低32bit寻址的空间内,但是为了以后的兼容性,即使当前的默认值对于我们的设备也ok,我们写驱动程序的时候,还是应该显示地进行一致性DMA mask的设置。
经常使用consistent映射的一些场景包括:
- 网卡设备的DMA ring环描述符
- SCSI适配器的mailbox的命令数据结构
- 设备主存放不下的设备固件微码
上面描述的这三种应用场景都要求CPU对于DMA内存的修改都可以立刻被设备看到,设备对于DMA内存的修改也可以被CPU立刻看到。consisitent DMA映射可以保证这一点。
特别需要注意的一点是,一致性DMA不能提供相关的内存屏障。现代CPU一般都可能将指令打乱以提高指令执行的性能,而这些被打乱的指令里面一旦有访问一致性DMA的store/load等指令时,情况就会变得很微妙。比如说有一个描述符,word0中的内容必须先被更新,word1的内容一定要后被更新。那么我们必须写类似于下面的代码:
1
|
desc->word0 = address;
|
内存屏障之前的指令一定会先于内存屏障之后的指令执行。因此上面的这段代码保证了在使能desc之前,就将地址放入word0中。值得一提的是,wmb是一个与平台无关的内存屏障,这样就保证了代码在任何平台上都能得到正确的结果。
在一些平台上,我们可能需要刷新CPU的写buffer或者PCI桥的写buffer。通常的做法是在写操作之后加一个读操作。
流式DMA映射
流式DMA映射一般用在一次DMA传送过程中,使用完即可unmap(除非使用了dma_sync_*)。硬件可以对这种DMA映射的访问进行顺序访问的优化。
可以将“streaming”理解成“异步的”(asynchronous)或者“非一致性的”(outside the coherency domain)。
经常使用流式DMA的一些场景包括
- 网络设备的发送/接收buffer;
- SCSI设备的文件系统的读写buffer。
流式DMA映射的接口设计,允许具体的实现可以进行任何硬件设备所允许的优化。最后,使用这类映射时,我们需要对自己想要什么样的结果十分清楚。
任何一种类型的DMA映射都不会因为底层的总线类型而产生一些字节对齐的限制,也许某些设备本身会有类似的限制。
同时,在具有cache功能的平台上,当DMA buffer没有和其他数据产生冲突(在同一个cache line)时,这个时候的DMA性能是最高的。
使用一致性DMA映射
如果想要分配和映射大(PAGE_SIZE或者更大的)的一致性DMA区域,我们应该使用如下代码:
1
|
dma_addr_t dma_handle;
|
dev是一个struct device *类型的变量,这个函数可以在中断上下文中被调用,只要使用了GFP_ATOMIC标志。size是我们要分配的区域大小,单位是Byte。
该函数会在RAM中分配相应的空间,它表现和__get_free_pages()的行为很相似(参数有些区别,比如size表示字节数,而get_free_pages则是指数次幂)。如果设备想分配的区域小于1个page,使用dma_poll相关的接口会更好一些。
一致性DMA映射的接口,当dev非空时,默认情况下会返回一个32bit寻址范围内的DMA地址。当设备显式地调用dma_set_coherent_mask来设置DMA掩码的时候,这个接口对一致性DMA映射的函数分配才会返回大于32bit寻址范围外的地址。dma_pool的接口也是类似的。
dma_alloc_coherent()会返回两个值:通过CPU可以访问的虚拟地址以及可以传递给硬件的dma_handler。这两个地址还有一个特点:他们都被对齐到PAGE_SIZE的2的指数次幂。
可以使用如下接口进行unmap操作:
1
|
dma_free_coherent(dev, size, cpu_addr, dma_handle);
|
接口中dev、size和之前的入参一致,而cpu_addr、dma_handle是alloc函数的返回值。这个函数也可以在中断上下文中被调用。
如果我们需要少量的内存的话,我们可以自己写代码来利用dma_alloc_coherent()返回的地址作为内存池,也可以使用dma_pool接口来做这样的事情。dma_poll类似于kmem_cache,不同的是dma_pool利用的是dma_alloc_coherentl来进行内存分配,而kmem_cache利用的是__get_free_page进行内存分配。此外,dma_pool也会考虑到一些硬件对齐上的限制。
可以使用如下接口来创建一个dma_pool:
1
|
struct dma_pool *pool;
|
参数name就是一个名字咯,dev和size与上面都一样。设备的对齐的硬件限制就可以通过align来告诉内核。align必须是2的整数次幂,以byte为单位。如果设备没有对齐的限制,则可以传入0;如果传入4096,则是告诉内核,该pool中的地址不能跨越4K Byte边界,但是在这种场景直接使用dma_alloc_coherent会更好一些。
申请好pool之后,可以使用
1
|
cpu_addr = dma_pool_alloc(pool, flags, & dma_handle);
|
来完成DMA内存申请。flags可以使用GFP_KERNEL(如果允许阻塞)、GFP_ATOMIC等。类似于dma_alloc_coherent(),本函数返回2个值:cpu_addr和dma_handle。
如果要释放从dma_pool中申请的内存,可以使用:
1
|
dma_pool_free(pool, cpu_addr, dma_handle);
|
pool同dma_pool_alloc中传入的pool一致,cpu_addr和dma_handle是dma_pool_alloc返回的对应值。这个函数可以在中断上下文中被调用。
如果要注销dma_pool,可以使用函数:
1
|
dma_pool_destroy(pool);
|
需要在调用这个函数之前,保证从pool中申请的内存都已经被释放完成。这个函数不能在中断上下文中被调用。
DMA方向
部分的DMA接口函数中,有参数来描述DMA的方向(这个参数在后面会看到),参数是一个整数,并且只能够取如下几个值:
- DMA_BIDIRECTIONAL
- DMA_TO_DEVICE
- DMA_FROM_DEVICE
- DMA_NONE
如果调用函数的时候知道DMA的方向,那么就需要向函数调用提供准确的值。
DMA_TO_DEVICE表示数据传输从主存到设备,而DMA_FROM_DEVICE表示数据传输从设备到主存。
我们需要准确地提供这个值。如果我们实在不知道或者无法确定,那么可以使用DMA_BIDIRECTIONAL。这个参数表示DMA传输可以是任何一个方向。平台可以保证我们这样做且能够正常工作,但是是以性能为代价的。
DMA_NONE是用来调试的。在知道准确方向之前,我们可以将对应的结构体中的值保存为DMA_NONE。一旦因此程序fail的话,可以捕捉到这部分的异常逻辑并进行修复。
这个值还有一个作用是可以用来debug(这个与上一段中的debug不同)。某些硬件平台上,,可以对DMA映射的地址是否可写进行标注,类似于用户地址空间中的页保护。因此,恰当的标注和DMA方向不匹配时,硬件可以捕获这个错误然后上报,进行更深度的DMA保护。
streaming映射需要指定一个方向,而一致性DMA映射不需要指定方向,一致性DMA映射隐含着DMA_BIDIRECTIONAL属性。
在SCSI子系统中,使用SCSI命令中的sc_data_direction来表示DMA的实际方向。
对于网络设备驱动来讲,就更简单了。发送报文时,使用DMA_TO_DEVICE方向来进行map/unmap。而接收报文时,使用DMA_FROM_DEVICE来进行map/unmap。
使用流式DMA映射
流式DMA映射函数可以在中断上下文中被调用,它有大类map/unmap函数族,一类是映射单个DMA区域,另外一个则是映射DMA地址链。
下面是一个映射单个DMA region的示例:
1
|
struct device *dev = &my_dev->dev;
|
下面是unmap的示例
1
|
dma_unmap_single(dev, dma_handle, size, direction);
|
调用dma_map_single()后,需要使用dma_mapping_error()来进行检测。这样做是为了保证映射的代码不依赖于某个平台的具体DMA实现。不检测的话,会带来预期意外的问题。dma_map_page()也是一样的。
DMA完成后,要使用dma_unmap_single()来释放,比如在通知host DMA已经完成的中断中来调用这个函数去释放相应的DMA区域。
使用这类的DMA映射具有一个缺点:我们不能引用到HIGHMEM的内存。为了访问HIGHMEM的内存,可以使用另外一组直接操作page/offset的DMA map/unmap函数。下面是使用这组函数的示例:
1
|
struct device *dev = &my_dev->dev;
|
这里面,offset指的是页内的偏移。同样的,要使用dma_mapping_error来检查是否映射出错。使用完DMA映射后,调用dma_unmap_page来进行资源释放。
如果使用scatterlists,可以使用如下代码来进行DMA映射:
1
|
int i, count = dma_map_sg(dev, sglist, nents, direction);
|
其中,nents是sglist中地址的个数。
dma_map_sg函数的具体实现,可以将多个sglist项映射成1个DMA映射地址,并且返回映射后的实际地址个数,如果出错的话,则返回0。这对于不能处理scatter-gather或者能够处理的scatter-gather中地址项数有限的硬件来说,是十分有帮助的。
如上suoshu,返回的count一定小于等于nents的值。
如果要释放scatterlist的DMA映射,只需要调用:
1
|
dma_unmap_sg(dev, sglist, nents, direction);
|
需要注意的是,dma_unmap_sg传入的参数nents,与dma_map_sg传入的nents值需要相同,而非dma_map_sg返回的实际映射出来的DMA地址个数。
另外,用完DMA映射后,一定要进行unmap。否则会造成资源泄露。
在使用流式DMA映射地址时,一定要注意数据的同步,以保证CPU或者设备能够看到最新的、正确的数据。处理不好的话,很容易引起数据不一致的问题。
一般的操作流程为:使用dma_map{single, sg}()进行DMA映射,然后每次DMA传输完成后,使用
1
|
dma_sync_single_for_cpu(dev, dma_handle, size, direction);
|
或者
1
|
dma_sync_sg_for_cpu(dev, sglist, nents, direction);
|
来进行一次同步,以便让CPU获取最新的数据。
然后,如果想让设备获得DMA区域,在将权限交给硬件之前,调用如下的API:
1
|
dma_sync_single_for_device(dev, dma_handle, size, direction);
|
或者
1
|
dma_sync_sg_for_device(dev, sglist, nents, direction);
|
最后传输完成后,调用相应的unmap API进行DMA资源的释放。如果在整个传输过程中,我们压根不访问数据,就不需要调用dma_sync_*()相关的API。
下面是使用了dma_sync_*()接口的一份伪代码示例:
1
|
my_card_setup_receive_buffer(struct my_card *cp, char *buffer, int len)
|
再次提醒,驱动不应该再使用virt_to_bus()和bus_to_virt()接口了。linux主线后续将会移除这两类的接口
错误处理
DMA资源是有限的,当发生错误时,一定要进行相应的异常处理:
- 查看dma_alloc_coherent()是否返回了NULL或者dma_map_sg返回了0
- 使用dma_mapping_error()来检查dma_map_single()和dma_map_page()返回的dma_addr_t是否有效
多次申请DMA,但是在中间某次申请失败的话,前面成功申请的都需要去释放。
在网卡设备中,发送时申请DMA地址失败后,在ndo_start_xmit的hook函数中,需要调用dev_kfree_skb()进行socket资源的释放并且返回NETDEV_TO_OK。
而对于SCSI设备,则需要返回SCSI_MLQUEUE_HOST_BUSY。在以后的某些时刻,SCSI子系统会再次向驱动下发命令。
优化为了unmap而多出来的状态空间开销
在许多平台上,dma_unmap_(single, page) 实际上什么都不执行,仅相当于一个nop指令。跟踪映射的地址和长度是十分消耗空间的。为了避免在驱动代码中多了很多if define的语句。可以使用如下的方式来减少空间。
- 使用DEFINE_DMA_UNMAP_{ADDR, LEN}来进行结构体的定义
1
|
struct ring_state {
|
变为:
1
|
struct ring_state {
|
- 使用函数来对mapping和len的字段进行赋值:
1
|
dma_unmap_addr_set(ringqp, mapping, FOO);
|
- 使用dma_unmap_{addr, len}()进行资源释放:
1
|
dma_unmap_single(dev,
|
平台相关的一些主题
- 如果平台支持IOMMU(包括软件的IOMMU),则内核编译时,需要是能CONFIG_NEED_SG_DMA_LENGTH
- ARCH_DMA_MINALIGN
架构必须保证kmalloc申请的buffer是DMA安全的。驱动和子系统都依赖于这一点。如果某个平台不是完全DMA一致(比如硬件不能保证在CPU cache中的数据和主存中的数据一致),ARCH_DMA_MINALIGN必须被设置上,以便时内存分配器能够保证kmalloc分配的buffer没有共享cacheline。可以查看一个例子:arch/arm/include/asm/cache.h
需要注意的是,ARCH_DMA_MINALIGN是关于DMA内存对齐的约束。我们不需要关心普通数据的对齐一致性问题。
(Done)