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))) { 
                        using_dac = 1; 
                } else if (!dma_set_mask(dev, DMA_BIT_MASK(32))) { 
                        using_dac = 0; 
                } else { 
                        printk(KERN_WARNING 
                                "mydev: No suitable DMA available.\n"); 
                        goto ignore_this_device; 
                }

如果一个设备可以使用64位一致性内存,那么代码如下:

int using_dac, consistent_using_dac;

                if (!dma_set_mask(dev, DMA_BIT_MASK(64))) { 
                        using_dac = 1; 
                        consistent_using_dac = 1; 
                        dma_set_coherent_mask(dev, DMA_BIT_MASK(64)); 
                } else if (!dma_set_mask(dev, DMA_BIT_MASK(32))) { 
                        using_dac = 0;
                        consistent_using_dac = 0;
                        dma_set_coherent_mask(dev, DMA_BIT_MASK(32)); 
                } else { 
                        printk(KERN_WARNING 
                                "mydev: No suitable DMA available.\n"); 
                        goto ignore_this_device; 
                }

最后,如果你的设备只有低24位寻址能力,那么代码可能如下:

                if (dma_set_mask(dev, DMA_BIT_MASK(24))) { 
                        printk(KERN_WARNING 
                                "mydev: 24-bit DMA addressing not available.\n"); 
                        goto ignore_this_device; 
                }

当调用dma_set_mask()成功时,会返回零。内核会保存输入的掩码值。以后再做DMA映射时就会用到该掩码信息。

有一个特殊情况我们需要在这里提及一下。如果设备支持多重功能(比如声卡支持播放和录音功能),不同的功能有不同的DMA寻址寻址限制,你可能想探测每个掩码然后选出一个机器可以处理的值。最后调用dma_set_mask()会成为最特别的掩码值,这点很重要。

下面给出伪代码来展示如何处理该问题。

#define PLAYBACK_ADDRESS_BITS DMA_BIT_MASK(32)
                #define RECORD_ADDRESS_BITS DMA_BIT_MASK(24)

                struct my_sound_card *card;
                struct device *dev;

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

DMA映射类型

有两种DMA映射类型。一种为一致性DMA映射,另一种为流式DMA映射。

一致性DMA映射通常在驱动初始化时就完成映射,驱动退出时取消映射。硬件应该保证外设及处理器可以并发访问数据,在没有显性软件刷缓存的情况下,双方都可以看到数据的变换。

可以将“一致性”认为“同步的”。

当前默认返回一致性内存为低32位总线地址,但是考虑到未来的兼容性,你应该设置一致性掩码,尽管默认值对于你的驱动来说是可以正常工作的。

使用一致性映射的好的例子如下:

  • 网卡DMA环描述符;

  • SCSI适配器邮箱命令数据结构;

  • 当超过主内存时的设备固件代码;

这些例子的共性是当处理器存储数据到内存中,设备立即可见,反之亦然。一致性映射可以保证这点。

重要点:一致性DMA内存并不包括使用适当的内存屏障。处理器可能会重新排序存入一致性内存中的数据。比如,设备需要看到一个描述符的第一个字先得到更新了,然后再看到第二个字得到更新,这个顺序非常重要。你应该像下面这样做来保证在所有平台上都可以正常工作:

desc->word0 = address;
wmb();
desc->word1 = DESC_VALID;

DMA流映射通常映射一次DMA传输,传输完后取消映射(除非使用下面所述的dma_sync_*),硬件可以优化访问顺序。

这里的“流”指的是“异步的”或“在一致性区域外的”。

使用流映射好的例子有:

  • 设备传输/接受网络缓冲区;

  • SCSI设备的读写文件系统缓冲区;

使用流映射的接口用于实现硬件支持的性能优化。到此为止,当使用这种映射时,你需要明确知道你为什么使用它。

使用DMA一致性映射

申请和映射大型(以页为单位)一致性DMA缓冲区,你应该:

dma_addr_t dma_handle;
cpu_addr = 
dma_alloc_coherent(dev, size, &dma_handle, gfp);

这里device是struct device *。如果使用GFP_ATOMIC标志,则可以在中断上下文调用。

size是你想申请的缓冲区的大小,单位为bytes。

该函数会在内存中申请一块缓冲区,就像调用__get_free_pages(将size替换成页的阶数)一样。如果你的驱动希望获取小于一个页面的缓冲区,可以调用dma_pool接口,下面有详细描述。

DMA一致性映射接口默认会返回一个32位的DMA地址。即使设备申明(通过DMA掩码)它有超过32位的寻址能力,如果一致性DMA掩码显性的通过调用dma_set_coherent_mask()来设置,那么一致性分配会返回一个大于32位DMA地址。对于dma_pool接口也一样。

dma_alloc_coherent返回两个值:一个处理可以使用的虚拟地址和一个设备卡可以使用的dma_handle。

处理器的返回地址及DMA总线地址是最小的页的幂次方对齐,返回的缓冲区的大小会大于或者等于申请的size。比如你申请的缓存区小于或者等于64KB,那么返回的缓冲区大小不会超过64KB。

如果想取消映射和释放该DMA缓冲区,可以调用下面的函数:

dma_free_coherent(dev, size, cpu_addr, dma_handle);

如果你的驱动需要很多小块内存缓冲区,你可以写一些本地代码将dma_alloc_coherent申请的页分成小块,或者可以调用dma_pool 接口函数。dma_pool有点像kmem_cache,但它使用的是dma_alloc_coherent,而不是__get_free_pages。同样地,它也知道通用硬件的对齐限制,比如队列头需要N字节对齐。

创建一个dma_pool:

struct dma_pool *pool;
pool = 
dma_pool_create(name, dev, size, align, alloc);

name为内存池的名字(就像struct kmem_cache name一样)。dev及size就如dma_alloc_coherent()参数一样。align为设备硬件需要的对齐大小(单位为字节,必须为2的幂次方)。如果设备没有边界限制,可以设置该参数为0。如果设置为4096,则表示从内存池分配的内存不能超过4K字节的边界(这个时候最好直接使用dma_alloc_coherent)。

从DMA内存池中申请内存:

cpu_addr = dma_pool_alloc(pool, flags, &dma_handle);

使用SLAB_KERNEL表示可以阻塞申请内存(不能在中断上下文及持有SMP锁),SLAB_ATOMIC表示无阻塞申请内存。和dma_alloc_coherent一样,它会返回两个值,cpu_addr 及 dma_handle。

释放内存给dma_pool:

dma_pool_free(pool, cpu_addr, dma_handle);

参数pool为传递给dma_pool_alloc()的pool,参数vaddr及addr为dma_pool_alloc()的返回值。

内存池析构函数用于释放内存池的资源:

dma_pool_destroy(pool);

这个函数在可睡眠上下文调用。请确认在调用此函数时,所有从该内存池申请的内存必须都要归还给内存池。 

DMA方向

本文提到的DMA方向为一个整型值,如下所示:

DMA_BIDIRECTIONAL
DMA_TO_DEVICE
DMA_FROM_DEVICE
DMA_NONE

如果你清楚DMA方向的话,应该提供一个准确值。

DMA_TO_DEVICE    数据从内存传输到设备。
DMA_FROM_DEVICE    数据从设备传输到内存。
DMA_BIDIRECTIONAL    不清楚传输方向则可用该类型。
DMA_NONE    仅用于调试目的

只有流映射才需要定义方向。一致性内存映射隐性的设置为DMA_BIDIRECTIONAL。

SCSI子系统通过成员'sc_data_direction'来告诉你使用的方向。

网络驱动就更加简单了。对于传输数据,用方向DMA_TO_DEVICE来map/unmap缓冲区。对于接收数据,用方向DMA_FROM_DEVICE来map/unmap缓冲区。

使用DMA流映射

DMA流映射函数可以在中断上下文使用。有两种map/ummap。一种是map/unmap单个内存区域,另一只是map/unmap一个scatterlist。

如何映射单个内存区域:

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);

取消映射

dma_unmap_single(dev, dma_handle, size, direction);

应该在DMA传输完成时调用dma_unmap_single。比如在中断处理函数中告诉你DMA传输已经完成。

使用像单一映射使用cpu指针做参数有一个劣势,即不能引用高端内存。因此有一对与dma_{map,unmap}_single相同的接口函数。这些函数用于处理页/偏移量而不是cpu指针。特别的:

struct device *dev = &my_dev->dev;
dma_addr_t dma_handle;
struct page *page = buffer->page;
unsigned long offset = buffer->offset;
size_t size = buffer->len;
dma_handle = 
dma_map_page(dev, page, offset, size, direction);
...
dma_unmap_page(dev, dma_handle, size, direction);

其中offset为页内偏移量。

如果使用scatterlists,则可以将若干个区域合并成一个区域用于映射:

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的条目数量。

这种实现可以很方便将几个连续的sglist条目合并成一个(如果DMA映射是以页为单位,任何连续的sglist条目可以合并成一个,事实上在这种特性在一些没有分散/聚集能力或者处理分散/聚集能力很弱的情况下,有巨大的优势)。返回真正映射的sg条目数量。返回零表示失败。

然后你就可以循环多次(可能小于nents次)使用sg_dma_address() 及sg_dma_len()来获取sg的物理地址及长度。

取消scatterlist映射:

dma_unmap_sg(dev, sglist, nents, direction);

确认此时DMA活动已经完成。

注意:<nents>是传入的参数,不一定是实际返回条目的数值。

每次调用dma_map_{single,sg}都应该相应的调用dma_unmap_{single,sg},因为总线地址空间是共享资源(尽管有些端口的映射到每一条总线上,因此很少设备会在同一个总线地址空间上产生冲突),你可以通过占用所有的总线地址来让系统变得不稳定。

如果你想多次使用DMA流映射,同时在DMA传输过程中访问数据,此时需要合适地同步数据缓冲区,这样可以让处理器及外设可以看到最新的更新和正确的DMA缓冲区数据。

所以第一步,调用dma_map_{single,sg}做一个流映射,在DMA传输完成后适当地调用:

dma_sync_single_for_cpu(dev, dma_handle, size, direction);

或者

dma_sync_sg_for_cpu(dev, sglist, nents, direction);

第二步,你希望将缓冲区传给硬件前,让设备再次获得DMA缓冲区,完成CPU对数据的访问,可以调用

dma_sync_single_for_device(dev, dma_handle, size, direction);

或者

dma_sync_sg_for_device(dev, sglist, nents, direction);

在最后一次DMA传输后调用dma_unmap_{single,sg}。如果你在调用dma_map_*和dma_unmap_*之间并不打算访问数据,根本无需调用dma_sync_*。

下面的伪代码展示了你需要使用dma_sync_*() 的情境。

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);

        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->rx_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_BIDIRECTIONAL mapping if
                        * the memory was modified.
                        */ 
                        give_rx_buf_to_card(cp); 
                } 
        } 
}

驱动程序不应该再用virt_to_bus或者bus_to_virt。有些驱动程序不得不做一些改动,因为动态DMA映射中再也没有与bus_to_virt一样的东西了。因为dma_alloc_coherent, dma_pool_alloc 及 dma_map_single 会返回的DMA地址(如果平台支持动态DMA映射,dma_map_sg将它们存在scatterlist中),你应该将这个DMA地址保存在驱动数据结构或者设备寄存器中。

所有的驱动程序都应该毫无例外的使用这些接口。virt_to_bus() 及 bus_to_virt()完成过时了,未来有计划将它们侧地移除。

错误处理

DMA寻址空间有时在某些体系结构中会受限制,可以通过下面方式来检查内存分配是否失败:

  • 检查dma_alloc_coherent是否返回NULL或dma_map_sg返回0.

  • 通过dma_mapping_error()来检查 dma_map_single 及 dma_map_page的dma_addr_t返回值:

dma_addr_t dma_handle;
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.
        */ 
}

平台相关的问题

如果你只是些一个linux驱动而不需要为内核维护一个体系结构你可以跳过本章。

1)数据结构scatterlist的条件

不要发明新的体系结构相关的scatterlist;使用<asm-generic/scatterlist.h>就可以了。如果体系结构支持IOMMU(包括软件IOMMU),你就需要使能CONFIG_NEED_SG_DMA_LENGTH。

2)ARCH_DMA_MINALIGN

体系结构必须确保kmalloc申请的缓冲区对DMA来说是安全的。驱动及子系统都依赖于此。如果一个体系结构并非是一致性DMA(比如硬件不能保证在处理器缓存中的数据和主内存中的数据时刻保持一致),则必须要设置ARCH_DMA_MINALIGN来确保kmalloc缓冲区不会和其他缓冲区共享一个缓存行。实例请看arch/arm/include/asm/cache.h。

请注意ARCH_DMA_MINALIGN和DMA内存对齐限制相关。你无需担心体系结构数据对齐限制。

3)支持多种类型的IOMMU

如果你的体系结构需要支持多种IOMMU,那么你可以使用 include/linux/asm-generic/dma-mapping-common.h。这是一个支持多种IOMMU的DMA API的库。很多体系结构(x86, powerpc, sh, alpha, ia64, microblaze and sparc)都使用它。选择一个看看如何正确使用该库。如果你想在单一系统中支持多种类型的IOMMU,基于x86 及 powec的实例可以给你帮助。

参考文献

[1] documentation/DMA-API-HOWTO.txt

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭