Dynamic DMA mapping Guide

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的地址空间就可以了。

Dynamic DMA mapping Guide

在枚举过程中,内核会获取到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。

这意味着当上面三个函数返回失败后,我们可以做如下几个事情:

  1. 如果可能的话,使用其他的mask掩码
  2. 如果可能的话,在数据传输时,使用非DMA方式
  3. 忽略这个设备,并且不初始化它(不使用这个设备了)

当出现第2点和第3点的时候,建议使用KERN_WARNING级别的打印来输出一条消息。这样以来,当用户使用我们的设备时发现有问题或者设备不能被检测到时,可以通过内核打印来找到原因。

对于标准的32bit寻址能力的设备,可以使用如下的代码进行设置:

1
2
3
4
if (dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
dev_warn(dev, "mydev: No suitable DMA available.\n");
goto ignore_this_device;
}

而对于具有64bit寻址能力的设备来讲,我们一般首先会尝试设置64bit的寻址能力,但是如果返回失败的话,则会尝试32bit的寻址能力。通常情况下,之所以会设置64bit寻址能力失败,可能仅仅是因为32bit的寻址能力相对于64bit的寻址能力更加高效。比如,在Sparc64中,PCI SAC寻址比DAC寻址更快。

以具有64bit寻址能力的设备设置streaming DMA能力为例,可以使用如下代码:

1
2
3
4
5
6
7
8
9
int using_dac;
if (!dma_set_mask(dev, DMA_BIT_MASK(64))) {
using_dac = 1;
} else if (!dma_set_mask(dev, DMA_BIT_MASK(32))) {
using_dac = 0;
} else {
dev_warn(dev, "mydev: No suitable DMA available.\n");
goto ignore_this_device;
}

如果也要设置consistent DMA能力的话,则使用如下代码:

1
2
3
4
5
6
7
8
9
10
11
int using_dac, consistent_using_dac;
if (!dma_set_mask_and_coherent(dev, DMA_BIT_MSAK(64))) {
using_dac = 1;
consistent_using_dac = 1;
} else if (!dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32))) {
using_dac = 0;
consistent_using_dac = 0;
} else {
dev_warn(dev, "mydev: No suitable DMA available.\n");
goto ignore_this_device;
}

coherent DMA的掩码与streaming DMA的mask相等或者要小一些。极少数的设备只支持consistent分配,那么我们就必须使用检查dma_set_coherent_mask()的返回值。

最后,如果设备支持24bit的寻址空间的话,我们需要这样做:

1
2
3
4
if (!dma_set_mask(dev, DMA_BIT_MASK(24))) {
dev_warn(dev, "mydev: 24-bit DMA addressing not available.\n");
goto ignore_this_device;
}

当dma_set_mask()或者dma_set_mask_and_coherent()成功(返回值为0)的话,内核会将我们提供的mask信息保存下来,并在后续做DMA mapping的时候使用。

还有一种比较特殊的场景需要考虑,比如说设备支持多个功能(如一个声卡支持播放和录音功能),不同的功能有不同的DMA寻址限制。这时我们希望在probe的时候能够针对不同功能设置合适的mask掩码。但实际上,只有最后一次调用的dma_set_mask()才生效。下面是一份伪代码来描述这种场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#define PLAYBACK_ADDDRESS_BITS	DMA_BIT_MASK(32)
#define RECORD_ADDRESS_BITS DMA_BIT_MASK(24)
...
if (!dma_set_mask(dev, PLAYBACK_ADDRESS_BITS)) {
card->playback_enabled = 1;
} else {
card->playback_enabled = 0;
dev_warn(dev, "%s: Playback disabled due to DMA limiatations.\n", card->name);
}

if (!dma_set_mask(dev, RECORD_ADDRESS_BITS)) {
card->record_enabled = 1;
} else {
card->record_enabled = 0;
dev_warn(dev, "%s: Record disabled due to DMA limiations.\n", card->name)
}

上面例子使用的声卡设备如果是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
2
3
desc->word0 = address;
wmb();
desc->word1 = DESC_VALID;

内存屏障之前的指令一定会先于内存屏障之后的指令执行。因此上面的这段代码保证了在使能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
2
dma_addr_t dma_handle;
cpu_addr = dma_alloc_coherent(dev, size, &dma_handle, gfp);

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
2
struct dma_pool *pool;
poll = dma_pool_create(name, dev, size, align, boundary);

参数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
2
3
4
5
6
7
8
9
10
11
12
13
14
struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
void *addr = buffer->ptr;
size_t size = buffer->len;

dma_handle = dma_map_single(dev, addr, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling;
}

下面是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
struct page *page = buffer->page;
size_t size = buffer->len;

dma_handle = dma_map_page(dev, page, offset, size, direction);
if (dma_mapping_error(dev, dma_handle)) {
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling;
}
……
dma_unmap_page(dev, dma_handle, size, direction);

这里面,offset指的是页内的偏移。同样的,要使用dma_mapping_error来检查是否映射出错。使用完DMA映射后,调用dma_unmap_page来进行资源释放。

如果使用scatterlists,可以使用如下代码来进行DMA映射:

1
2
3
4
5
6
7
int i, count = dma_map_sg(dev, sglist, nents, direction);
struct scatterlist, *sg;

for_each_sg(sglist, sg, count, i) {
hw_address[i] = sg_dma_address(sg);
hw_len[i] = sg_dma_len(sg);
}

其中,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
my_card_setup_receive_buffer(struct my_card *cp, char *buffer, int len)
{
dma_addr_t mapping;
mapping = dma_map_single(cp->dev, buffer, len, DMA_FROM_DEVICE);
if (dma_mapping_error(cp->dev, mapping)){
/*
* reduce current DMA mapping usage,
* delay and try again later or
* reset driver.
*/
goto map_error_handling;
}
cp->rx_buf = buffer;
cp->rx_len = len;
cp->rx_dma = mapping;

give_rx_buf_to_card(cp);
}
……
my_card_interrupt_handler(int irq, void *devid, struct pt_regs *regs)
{
struct my_card *cp = devid;
……
if (read_card_status(cp) == RX_BUF_TRANSFERRED) {
struct my_card_header *hp;
/* Examine the header to see if we wish
* to accept the data. But synchronize
* the DMA transfer with the CPU first
* so that we see updated contents.
*/
dma_sync_single_for_cpu(&cp->dev,
cp->rx_dma,
cp->rx_len,
DMA_FROM_DEVICE);
/* Now it is safe to examine the buffer. */
hp = (struct my_card header *)cp->rx_buf;
if (header_is_ok(hp)) {
dma_unmap_single(&cp->dev, cp->rx_dma, cp->rx_len, DMA_FROM_DEVICE);
pass_to_upper_layers(cp->buf);
make_and_setup_new_rx_buf(cp);
} else {
/* CPU should not write to
* DMA_FROM_DEVICE-mapped area,
* so dma_sync_single_for_device() is
* not needed here. It would be required
* for DMA_BIDRECTIONAL mapping if
* the memory was modified.
*/
give_rx_buf_to_card(cp);
}
}
}

再次提醒,驱动不应该再使用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
2
3
4
5
struct ring_state {
struct sk_buff *skb;
dma_addr_t mapping;
__u32 len;
}

变为:

1
2
3
4
5
struct ring_state {
struct sk_buffer *skb;
DEFINE_DMA_UNMAP_ADDR(mapping);
DEFINE_DMA_UNMAP_LEN(len);
}
  • 使用函数来对mapping和len的字段进行赋值:
1
2
dma_unmap_addr_set(ringqp, mapping, FOO);
dma_unmap_len_set(ringqp, len, BAR);
  • 使用dma_unmap_{addr, len}()进行资源释放:
1
2
3
4
dma_unmap_single(dev,
dma_unmap_addr(ringp, mapping),
dma_unmap_len(ringp, len),
DMA_FROM_DEVICE);

平台相关的一些主题

  • 如果平台支持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)

上一篇:Nginx核心知识100讲学习笔记(陶辉)Nginx架构基础(四)


下一篇:swftools中的pdf2swf转换Error overflow ID 65535 解决办法