日期:2014-05-16  浏览次数:21013 次

linux之DMA动态映射指南

DMA动态映射指南

translated by JHJ(jianghuijun211@gmail.com)

本文通过伪代码指导驱动开发者如何正确使用DMA API。关于API更精确的描述,请参考DMA-API.txt。

大多是64位平台有一些特殊硬件可以将总线地址(DMA地址)转换为物理地址。这个和CPU如何利用页表或TLB将虚拟地址转换成物理地址有点像。这种地址转换是有必要的,就像PCI设备可以在单个寻址周期里在64位物理地址空间寻址到任何一个页面。以前linux上的64位平台需要人为设置系统的最大内存大小,这样virt_to_bus()就可以正在工作了(DMA地址转换页表在系统启动时简单初始化,通过__pa(bus_to_virt()可以将DMA地址转换为物理页地址)。

为了使linux可以使用DMA动态映射,它需要得到驱动的一些协助,也就是说需要考虑DMA地址只有在使用时才被映射,DMA传输后,需要取消映射。

当然下面这些API可以在没有这些硬件限制的情况下正常工作。

请注意这些DMA API可以在任何总线上工作,和体系结构无关。你应该是用DMA API而不是特定总线的DMA API(比pci_dma_*)。

首先,你需要确定在你的驱动程序中

#include <linux/dma-mapping.h>

该文件定义了类型dma_addr_t(),它作为一个从DMA 映射函数返回的(总线)地址,到处都会使用到。

什么样的内存是DMA可用的?

第一件你要知道的事情是什么样的内核内存可以用作DMA映射。关于此有一些非书面的准则,本文试图将它们以文字的方式整理出来。

如果你通过页分配器(比如__get_free_page*())或者通用内存分配器(比如kmalloc() or kmem_cache_alloc())分配内存,那么你可以使用由这些函数返回的内存地址用作DMA传输。

这意味着你不能使用vmalloc()返回的内存地址用作DMA。DMA使用由vmalloc申请的内存是有可能的,但是需要遍历页表来获取物理地址,然后将这些页通过类似__va()这样的函数转换成内核虚拟地址。

这条规则意味着你不能将内核镜像地址(在data/text/bss段),或者模块镜像地址,或者栈地址用于DMA。即使这些物理内存可以用于DMA,你也要确保I/O缓冲区是缓存行对齐的。如果不是这样,你将会看到由于DMA不一致性缓存导致的缓存行共享问题(数据丢失)。比如处理器可能写一个字,而DMA在同一个缓存行写另一个字,他们两中的一个将被修改。

同样的,这意味着你不能使用由kmap()调用返回的地址,理由与vmalloc()一样。

可阻塞I/O或者网络缓冲区又会怎么样呢?可阻塞I/O及网络子系统可以确保它们使用的缓冲区可以用于DMA传输。

DMA寻址限制

你的设备有DMA寻址限制吗?比如你的设备只有低24位寻址能力?如果是的,那么你就需通知内核。

默认情况下,内核假设设备可以在32位地址空间寻址。对于64位设备,设备的寻址空间将大大增加。对于一个有寻址限制的外设,如前面所讨论的,需要减小寻址空间。

特别注意对于PCI设备:PCI-X规格书要求PCI-X设备对于数据交互要支持64位寻址。至少在一个平台上(SGI SN2)需要64位一致性内存分配,这样才可以在IO总线为PCI-X模式下正常工作。

可以通过调用dma_set_mask()来通知内核相关限制:

int dma_set_mask(struct device *dev, u64 mask);

通过调用dma_set_coherent_mask()通知内核一致性内存分配的限制。

int dma_set_coherent_mask(struct device *dev, u64 mask);

这里devi为一个指向设备的指针,掩码显示与设备寻址能力对应的位。如果使用指定的mask时DMA能正常工作,则返回零。通常来说,设备数据结构是内嵌到特定总线的设备结构中的。比如一个指向PCI设备的指针为pdev->dev(pdev指向PCI设备)。

如果返回非零值,则对应设备不能使用DMA。如果强行使用则会出现一些不确定现象。此时你需要用一个不同的掩码,或者不适用DMA。这意味着如果返回失败,你有三个选择:

1)如果可能的话,使用另一个DMA掩码值;
2)如果可能的话使用非DMA模式传输数据;
3)放弃该设备,不要初始化该设备;

因此如果你不想执行第二步或者第三步时,你应该在驱动中打印一个KERN_WARNING的消息。这样的话,当驱动使用者抱怨性能很差时,或者根本检测不到设备时,你可以让他们保存内核信息来找出准确原因。

标准的32位寻址设备会如下做一些事情:

if (dma_set_mask(dev, DMA_BIT_MASK(32))) {
                        printk(KERN_WARNING
                                "mydev: No suitable DMA available.\n");
                        goto ignore_this_device;
                }

另一个常见的场景是拥有64位寻址能力的设备。该方法用于尝试64位寻址,但是会使用32位掩码,这样可以确保不会失败。内核可以在64位掩码中返回失败,不是因为没有64位寻址能力,而是因为32位寻址比64位寻址更加有效率。比如Sparc64 PCI SAC寻址就比DAC寻址更加有效率。

下面的例子告诉你如何处理拥有64位处理能力的设备的流式DMA。

int using_dac;

                if (!dma_set_mask(dev, DMA_BIT_MASK(64))) {
   &nbs