Linux 设备驱动程序(三)(中):https://developer.aliyun.com/article/1597488
(4)通用 DMA 层
DMA 操作最终会分配缓冲区,并将总线地址传递给设备。一个可移植的驱动程序要求对所有体系架构都能安全而正确地执行 DMA 操作,编写这样一个驱动程序的难度超出了一般人的想像。不同的系统对处理缓存一致性上有不同的方法;如果不能正确处理该问题,驱动程序会引起内存冲突。一些系统拥有复杂的总线硬件,使得 DMA 任务或变得简单,或变得困难。并且不是所有的系统都能对全部的内存执行 DMA。幸运的是,内核提供了一个与总线体系架构无关的 DMA 层,它会隐藏大多数问题。强烈建议在编写驱动程序时,为 DMA 操作使用该层。
下面许多函数都需要一个指向 device 结构的指针。该结构是在 Linux 设备模型中用来表示设备底层的。驱动程序通常不直接使用该结构,但是在使用通用 DMA 层时,需要使用它。该结构内部隐藏了描述设备的总线细节。比如可以在 pci_device 结构或者 usb_device 结构的 dev 成员中发现它。
使用下列函数的驱动程序都要包含头文件 <linux/dma-mapping.h>。
(a)处理复杂的硬件
在执行 DMA 之前,第一个必须回答的问题是:是否给定的设备在当前主机上具备执行这些操作的能力。出于很多原因,许多设备受限于它们的寻址范围。默认的情况下,内核假设设备都能在 32 位地址上执行 DMA。如果不是这样,应该调用下面的函数通知内核:
int dma_set_mask(struct device *dev, u64 mask);
该掩码显示与设备能寻址能力对应的位。比如设备受限于 24 位寻址,则 mask 应该是 0x0FFFFFF 。如果使用指定的 mask 时 DMA 能正常工作,则返回非零值。如果 dma_set_mask 返回 0,则对该设备不能使用 DMA。因此,一个受限于 24 位 DMA 操作的驱动程序初始化代码有如下的形式:
if (dma_set_mask (dev, 0xffffff)) card->use_dma = 1; else { card->use_dma = 0; /* 不得不在没有 DMA 情况下操作 */ printk (KERN_WARN, "mydev: DMA not supported\n"); }
再强调一遍,如果设备支持常见的 32 位 DMA 操作,则没有必要调用 dma_set_mask。
(b)DMA 映射
一个 DMA 映射是要分配的 DMA 缓冲区与为该缓冲区生成的、设备可访问地址的组合。我们可以通过对 virt_to_bus 函数的调用获得该地址,但是有许多理由建议不要这么做。第一个理由是具有 IOMMU 的硬件为总线提共了一套映射寄存器。IOMMU 在设备可访问的地址范围内规划了物理内存,使得物理上分散的缓冲区对设备来说变成连续的。对 IOMMU 的运用需要使用到通用 DMA 层,而 virt_to_bus 函数不能完成这个任务。
请注意不是所有的体系架构都有 IOMMU;特别是常见的 x86 平台没有对 IOMMU 的支持。但是,一个正确的驱动程序不需要知道其运行系统上的 I/O 支持硬件。
在某些情况下,为设备设置可用地址需要建立回弹缓冲区。当驱动程序要试图在外围设备不可访问的地址上执行 DMA 时(比如高端内存),将创建回弹缓冲区。然后,必要时会将数据写入或者读出回弹缓冲区。对回弹缓冲区的使用势必会降低系统性能,但有的时候却没有其他可替代的方法。
DMA 映射必须解决缓存一致性的问题。现代处理器在内部的快速缓存器中保存了最近访问的内存区域;没有该缓存器,将得不到期望的性能。如果设备改变了主内存中的区域,则任何覆盖该区域的处理器缓存都将无效;否则处理器将使用不正确的主内存映射,从而产生不正确的数据。与此类似,当设备使用 DMA 从主内存中读取数据时,在处理器缓存中的任何改变也必须立刻得到刷新。这些缓存一致性的问题为系统带来诸多不确定因素,如果程序员不细致谨慎的话,这些错误非常难以查找。一些体系架构在硬件中管理缓存的一致性,但是其他一些体系架构则需要软件的支持。通用 DMA 层端尽全力来保证在所有体系架构中都能正常运行,但是必须看到,正确的行为需要一套规则来保障。
DMA 映射建立了一个新的结构类型 —— dma_addr_t 来表示总线地址。dma_addr_t 类型的变量对驱动程序是不透明的;唯一允许的操作是将它们传递给 DMA 支持例程以及设备本身。作为一个总线地址,如果 CPU 直接使用了 dma_addr_t,将会导致发生不可预期的问题。
根据 DMA 缓冲区期望保留的时间长短,PCI 代码区分两种类型的 DMA 映射:
一致性 DMA 映射
这种类型的映射存在于驱动程序生命周期中。一致性映射的缓冲区必须可同时被 CPU 和外围设备访问(其他类型的映射,如后面将要讨论的类型,在给定时刻只能被一个设备访问)。因此一致性映射必须保存在一致性缓存中。建立和使用一致性映射的开销是很大的。
流式 DMA 映射
通常为单独的操作建立流式映射。当使用流式映射时,一些体系架构可以最大程度地优化性能,但是这些映射也要服从一组更加严格的访问规则。内核开发者建议尽量使用流式映射,然后再考虑一致性映射。这么做有两个原因。第一个原因是在支持映射寄存器的系统中,每个 DMA 映射使用总线上的一个或者多个映射寄存器。一致性映射具有很长的生命周期,因此会在相当长的时间内占用这些寄存器,甚至在不使用它们的时候也不释放所有权。第二个原因是在一些硬件中,流式映射可以被优化,但优化的方法对一致性映射无效。
必须用不同的方法操作这两种映射,下面详细描述操作细节。
(c)建立一致性 DMA 映射
驱动程序可调用 pci_alloc_consistent 函数建立一致性映射:
void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);
该函数处理了缓冲区的分配和映射。前两个参数是 device 结构和所需缓冲区的大小。函数在两处返回 DMA 映射的结果。函数的返回值是缓冲区的内核虚拟地址,可以被驱动程序使用;而与其相关的总线地址,返回时保存在 dma_handle 中。该函数对分配的缓冲区做了一些处理,从而缓冲区可用于 DMA;通常只是通过 get_free_pages 函数分配内存(请注意 size 是以字节为单位的,而不是幕次的值)。flag 参数通常是描述如何分配内存的 GFP_ 值;通常是 GFP_KERNEL 或者是 GFP_ATOMIC(在原子上下文中运行时)。
当不再需要缓冲区时(通常在模块卸载的时候),调用 dma_free_coherent 向系统返回缓冲区:
void dma_free_coherent(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle);
请注意该函数与其他通用 DMA 函数一样,需要提供缓冲区大小、CPU 地址、总线地址等参数。
(d)DMA 池
DMA 池是一个生成小型、一致性 DMA 映射的机制。调用 dma_alloc_coherent 函数获得的映射,可能其最小大小为单个页。如果设备需要的 DMA 区域比这还小,就要使用 DMA 池了。在对内嵌于某个大结构中的小型区域执行 DMA 时,也可以使用 DMA 池。一些不容易察觉的驱动程序一致性缓存错误,往往存在于结构中与小型 DMA 区域相邻的成员中。为了避免这一问题的出现,应该总是显式地为 DMA 操作分配区域,而与其他非 DMA 数据结构的操作分开。
在 <linux/dmapool.h> 中定义了 DMA 池的函数。
DMA 池必须在使用前,调用下面的函数创建:
struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation);
这里,name 是 DMA 池的名字,dev 是 device 结构,size 是从该池中分配的缓冲区的大小,align 是该池分配操作所必须遵守的硬件对齐原则(用字节表示),如果 allocation 不为零、表示内存边界不能超越 allocation。比如传入的 allocation 是 4096,从该池中分配的缓冲区不能跨越 4KB 的界限。
当使用完 DMA 池后,调用下面的函数释放:
void dma_pool_destroy(struct dma_pool *pool);
在销毁前,必须向 DMA 池返回所有分配的内存。
使用 dma_pool_alloc 函数处理分配问题:
void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t *handle);
在这个函数中,mem_flags 通常设置为GFP_ 分配标志。如果一切正常,将分配并返回内存区域(拥有创建 DMA 池时指定的大小)。像 dma_alloc_coherent 函数一样,返回的 DMA 缓冲区的地址是内核虚拟地址,并作为总线地址保存在 handle 中。
使用下面的通数返回不需要的缓冲区:
void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);
(e)建立流式 DMA 映射
由于多种原因,流式映射具有比一致性映射更为复杂的接口。这些映射希望能与已经由驱动程序分配的缓冲区协同工作,因而不得不处理那些不是它们选择的地址。在某些体系架构中,流式映射也能够拥有多个不连续的页和多个 “分散 / 聚集” 缓冲区。出于上面这些原因的考虑,流式映射拥有自己的设置函数。
当建立流式映射时,必须告诉内核数据流动的方向。为此定义了一些符号(dma_data_direction 枚举类型):
DMA_TO_DEVICE
DMA_FROM_DEVICE
这两个符号的作用很明显。如果数据被发送到设备(可能使用 write 系统调用作为响应),应使 用 DMA_TO_DEVICE ;而如果数据被发送到 CPU ,则使用 DMA_FROM_DEVICE。
DMA_BIDIRECTIONAL
如果数据可双向移动,则使用 DMA_BIDIRECTIONAL。
DMA_NONE
提供该符号只是出于调试目的。如果要使用设置了该符号的缓冲区,会导致内核错误。
可能有的读者认为任何时候都使用 DMA_BIDIRECTIONAL 就可以了,但是驱动程序作者不能这么做。在一些体系架构中,可能会为这个选择付出很大的性能代价。
当只有一个缓冲区要被传输的时候,使用 dma_map_single 函数映射它:
dma_addr_t dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction);
返回值是总线地址,可以把它传递给设备;如果执行遇到错误,则返回 NULL。
当传输完毕后,使用 dma_unmap_single 函数删除映射:
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction);
在该函数中,size 和 direction 参数必须与映射缓冲区的参数相匹配。有几条非常重要的原则用于流式 DMA 映射:
缓冲区只能用于这样的传送,即其传送方向匹配于映射时给定的方向值。
一旦缓冲区被映射,它将属于设备,而不是处理器。直到缓冲区被撤销映射前,驱动程序不能以任何方式访问其中的内容。只有当 dma_unmap_single 函数被调用后,驱动程序才能安全访问缓冲区中的内容(还存在一个例外,不久就会看到)。尤其要说明的是,这条规则意味着:在包含了所有要写入的数据之前,不能映射要写入设备的缓冲区。
在 DMA 处于活动期间内,不能撤销对缓冲区映射,否则会严重破坏系统的稳定性。
读者可能提出疑问:为什么在缓冲区被映射后,驱动程序不能访问它? 有两个原因来解释这条规则。第一,当一个缓冲区建立 DMA 映射时,内核必须保证在该缓冲区内的全部数据都被写入了内存。当调用 dma_unmap_single 函数时,很可能有一些数据还在处理器的缓存中,因此必须被显式刷新。在刷新动作后,处理器写入缓冲区的数据对设备是不可见的。
第二,如果要映射的缓冲区位于设备不能访问的内存区段时,该怎么办? 一些体系架构会只产生一个错误,但是其他一些体系架构将创建一个回弹缓冲区。回弹缓冲区是内存中的独立区域,它可被设备访问。如果使用 DMA_TO_DEVICE 方向标志映射缓冲区,并且需要使用回弹缓冲区,则在最初缓冲区中的内容作为映射操作的一部分被拷贝。很明显 , 在拷贝操作后,最初缓冲区内容的改变对设备也是不可见的。同样 DMA_FROM_DEVICE 回弹缓冲区被 dma_unmap_single 函数拷贝回最初的缓冲区中,也就是说,直到拷贝操作完成,来自设备的数据才可用。
顺便说一下,为什么获得正确的传输方向是一个重要的问题,回弹缓冲区就是一个解释。DMA_BIDIRECTIONAL 回弹缓冲区在操作前后都要拷贝数据,这通常会浪费不必要的 CPU 指令周期。
有时候,驱动程序需要不经过撤销映射就访问流式 DMA 缓冲区的内容,为此内核提供了如下调用:
void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction);
应该在处理器访问流式 DMA 缓冲区前调用该函数。一旦调用了该函数,处理器将 “拥有” DMA 缓冲区,并可根据需要对它进行访问。然而在设备访问缓冲区前,应该调用下面的函数将所有权交还给设备:
void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction diredtion);
再次强调,处理器在调用该通数后,不能再访问 DMA 缓冲区了。
(f)单页流式映射
有时候,要为 page 结构指针指向的缓冲区建立映射;这种情况是有可能发生的,比如使用 get_user_pages 映射用户空间缓冲区。使用下面的函数,建立和撤销使用 page 结构指针的流式映射:
dma_addr_t dma_map_page(struct device *dev, struct page *page, unsigmed long offset, size_t size, enum dma_data_direction direction); void dma_unmap_page(struct device *dev, dma_addr_t dma_address, size_t size, enum dma_data_direction direction);
offset 和 size 参数用于映射一页中的一部分。建议尽量避免映射部分内存页,除非明了其中的原理。如果分配的页是缓存流水线的一部分,则映射部分页会引起一致性问题,比如内存冲突,以及产生非常难以调试的代码缺陷等。
(g)分散 / 聚集映射
分散/集聚集映射是一种特殊类型的流式 DMA 映射。假设有几个缓冲区,它们需要与设备双向传输数据。有几种方式能产生这种情形,包括从 readv 或者 writev 系统调用产生,从集群的磁盘 I/O 请求产生,或者从映射的内核 I/O 缓冲区中的页面链表产生。可以简单地依次映射每一个缓冲区并且执行请求的操作,但是一次映射整个缓冲区表还是很有利的。
许多设备都能接受一个指针数组的分散表,以及它的长度,然后在一次 DMA 操作中把它们全部传输走。比如将所有的数据包放在多个数据单元中,“零拷贝” 网络非常容易实现。把分散表作为一个整体的另外一个原因是,充分利用那些在总线硬件中含有映射寄存器系统的优点。在这些系统中,从设备角度上看、物理上不连续的内存页,可以被组装成一个连续数组。这种技术只能用在分散表中的项在长度上等于页面大小的时候(除了第一个和最后一个之外),但是在其工作时,它能够将多个操作转化成单个 DMA 操作,因而能够加速处理工作。
最后,如果必须用到回弹缓冲区,将整个表接合成一个单个缓冲区是很有意义的(因为无论如何它也会被复制)。
所以现在可以确信在某些情况下分散表的映射是值得做的。映射分散表的第一步是建立并填充一个描述被传送缓冲区的 scatterlist 结构的数组。该结构是与体系架构相关的,并且在头文件 <linux/scatterlist.h> 中描述。然而,该结构会始终包含两个成员:
struct page *page;
与在 scatter/gather 操作中用到缓冲区相对应的 page 结构指针。
unsigned int length;
unsigned int offset;
在页内缓神区的长度和偏移量。
为了映射一个分散/聚集 DMA 操作,驱动程序应当为传输的每个缓冲区在 scatterlist 结构对应入口项上设置 page、offset 和 length 成员。然后调用:
int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction)
这里的 nents 是传入的分散表入口的数量。返回值是要传送的 DMA 缓冲区数;它可能会小于 nents。
对在输入分散表中的每一个缓冲区,dma_map_sg 函数返回了指定设备的正确的总线地址。作为任务的一部分,它还把内存中相邻的缓冲区接合起来。如果运行驱动程序的系统拥有一个 I/O 内存管理单元,dma_map_sg 函数会对该单元的映射寄存器编程,如果没有发生什么错误,则从设备角度上看,其能够传输一块连续的缓冲区。然而在调用之前是无法知道传输结果的。
驱动程序应该传输由 dma_map_sg 函数返回的每个缓冲区。总线地址和每个缓冲区的长度被保存在 scatterlist 结构中,但是它们在结构中的位置会随体系架构的不同而不同。使用已经定义的两个宏,可用来编写可移植代码:
dma_addr_t sg_dma_address(struct scatterlist *sg);
从该分散表的入口项中返回总线(DMA)地址。
unsigned int sg_dma_len(struct scatterlist *sg);
返回缓冲区的长度。
再次强调,被传输缓冲区的地址和长度与传递给 dma_map_sg 函数的值是不同的。一旦传输完毕,使用 dma_uпmap_sg 函数解除分散 / 聚集映射:
void dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents, enum dma_data_direction direction);
请注意,nents 一定是先前传递给 dma_map_sg 函数的入口项的数量,而不是函数返回的 DMA 缓冲区的数量。
分散/聚集映射是流式 DMA 映射,因此适用于流式映射的规则也适用于该种映射。如果必须访问映射的分散 / 聚集列表,必须首先对其进行同步:
void dma_sync_sg_for_cpu(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction); void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);
(h)PCI 双重地址周期映射
通常 DMA 支持层使用 32 位总线地址,其为设备的 DMA 掩码所约束。然而 PCI 总线还支持 64 位地址模式 ,既双重地址周期(DAC)。出于多种原因,通用 DMA 层并不支持。该模式,首先这是 PCI 独有的特性。其次,许多 DAC 的实现都是有缺陷的,而且 DAC 也比常用的 32 位 DMA 要慢,会增加性能开销。虽然如此,还是有一些应用程序能正确使用 DAC;如果设备需要使用放在高端内存的大块缓冲区,可以考虑实现 DAC 支持。这种支持只有对 PCI 总线有效,因此必须使用与 PCI 总线相关的例程。
为了使用 DAC,驱动程序必须包含头文件 <linux/pci.h>,还必须设置一个单独的 DMA 掩码:
int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask);
只有该函数返回 0 时,才能使用 DAC 地址。
在 DAC 映射中使用了一个特殊类型(dma64_addr_t)。调 pci_dac_page_to_dma 函数建立一个这样的映射:
dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev, struct page *page, unsigned long offset, int direction);
读者会注意到,可以只使用 page 结构指针(毕竟它们应当保存在高端内存中,否则是毫无意义的)来建立 DAC 映射,而且必须以一次一页的方式创建它们。direction 参数与在通用 DMA 层中使用的 dma_data_direction 枚举类型等价,因此可以取 PCI_DMA_TODEVICE、PCI_DMA_FROMDEVICE 或者 PCI_DMA_BIDIRECTIONAL。
DAC 映射不需要其他另外的资源,因此在使用过后,不需要显式释放它。然而像对待其他流式映射一样对待它是必要的,关于缓冲区所有权的规则也适用于它。有一套用于同步 DMA 缓冲区的函数,其形式如下:
void pci_dac_dma_sync_single_for_cpu(struct pci_dev *pdev, dma64_addr_t dma_addr, size_t len, int direction); void pci_dac_dma_sync_single_for_device(struct pci_dev *pdev, dma64_addr_t dma_addr, size_t len, int direction);
(i)一个简单的 PCI DMA 例子
这里提供了一个 PCI 设备的 DMA 例子源代码,以说明如何使用 DMA 映射。实际 PCI 总线上的 DMA 操作形式,与它所驱动的设备密切相关,因此这个例子不能应用于任何真实设备。但它是一个假定的叫 dad(DMA Acquisition Device,DMA 获取设备)驱动程序的一部分。该设备的驱动动程序要用类似下面的代码定义传输函数:
int dad_transfer(struct dad_dev *dev, int write, void *buffer, size_t count) { dma_addr_t bus_addr; /* 映射 DMA 需要的缓冲区 */ dev->dma_dir = (write ? DMA_TO_DEVICE : DMA_FROM_DEVICE); dev->dma_size = count; bus_addr = dma_map_single(&dev->pci_dev->dev, buffer, count, dev->dma_dir); dev->dma_addr = bus_addr; /* 设置设备 */ writeb(dev->registers.command, DAD_CMD_DISABLEDMA); writeb(dev->registers.command, write ? DAD_CMD_WR : DAD_CMD_RD); writel(dev->registers.addr, cpu_to_le32(bus_addr)); writel(dev->registers.len, cpu_to_le32(count)); /* 开始操作 */ writeb(dev->registers.command, DAD_CMD_ENABLEDMA); return 0; }
该函数映射了准备进行传输的缓冲区并且启动设备操作。另一半工作必须在中断服务例程中完成,它看起来类似下面这样:
void dad_interrupt(int irq, void *dev_id, struct pt_regs *regs) { struct dad_dev *dev = (struct dad_dev *) dev_id; /* 确定该中断确实是从对应的设备发来的 */ /* 释放对 DMA 缓冲区的映射 */ dma_unmap_single(dev->pci_dev->dev, dev->dma_addr, dev->dma_size, dev->dma_dir); /* 只有到现在这个时候,对缓冲区的访问才是安全的,把它拷贝给用户。 */ // ... }
显而易见,这个例子忽略了大量细节,包括用来阻止同时开始多个 DMA 操作的必要步骤。
(5)ISA 设备的 DMA
ISA 总线允许两种 DMA 传输:本地(native) DMA 和 ISA 总线控制 (bus-master) DMA。本地 DMA 使用主板上的标准 DMA 控制器电路来驱动 ISA 总线上的信号线。另一方面, ISA 总线控制 DMA 完全由外围设备控制。后一种 DMA 类型很少被使用,并且也不需要在这里讨论,因为至少从设备角度上看,它与 PCI 设备的 DMA 非常类似。一个 ISA 总线控制 DMA 的例子是 1542 SCSI 控制器,它的驱动程序在内核代码 drivers/scsi/ahal542.c 中。
至于这里所关心的本地 DMA,有三种实体涉及到 ISA 总线上的 DMA 数据传输:
8237 DMA 控制器(DMAC)
控制器保存了有关 DMA 传输的信息,比如方向、内存地址、传输数据量大小等。它还包含了一个跟踪传送状态的计数器。当控制器接收到一个 DMA 请求信号时,它将获得总线控制权并驱动信号线,这样设备就能读写数据了。
外围设备
当设备准备传送数据时,必须激活 DMA 请求信号。DMAC 负责管理实际的传输工作;当控制器选通设备后,硬件设备就可以顺序地读写总线上的数据。当传输结束时,设备通常会产生一个中断。
设备驱动程序
需要驱动程序完成的工作很少,它只是负责提供 DMA 控制器的方向、总线地址、传输量的大小等等。它还与外围设备通信、做好传输数据的准备,当 DMA 传输完毕后,响应中断。
在 PC 中使用的早期 DMA 控制器能够管理四个 “通道” ,每个通道都与一套 DMA 寄存器相关联。四个设备可以同时在控制器中保存它们的 DMA 信息。现在的 PC 包含了两个与 DMAC 等价的设备(注 6):第二控制器(主控制器)连接系统的处理器,第一控制
器(从控制器)与第二控制器的 0 通道相连(注7)。
注 6: 这种电路现在是主板芯片组的一部分,但在几年前,它们是两个独立的 8237 芯片。
注 7: 最初的 PC 只有两个控制器,第二个控制器出现在 286 平台中。但是,第二个控制器被连接为主控制器,这是因为它可以处理 16 位传输,而第一个控制器每次只能传输八位,因此仅用于向后兼容。
通道编号从 0 到 7: 通道 4 在内部用来将从属控制器级联到主控制器上、因此对 ISA 外围设备来说,通道 4 不可用。所以可用的通道是从属控制器(8 位通道)上的 0 ~ 3 和主控制器(16 位通道)上的 5 ~ 7。每次 DMA 传输的大小保存在控制器中,它是一个 16 位的数,表示传递所需要的总线周期数。因此最大传输大小对从属控制器来说是 64KB(因为它在一个周期内传输 8 位),对主控制器来说是 128KB(它使用 16 位传输)。
因为 DMA 控制器是系统资源、因此内核协助处理这一资源。内核使用 DMA 注册表为 DMA 通道提供了请求/释放机制,并且提供了一组函数在 DMA 控制器中配置通道信息。
(a)注册 DMA
读者应该对内核注册表很熱悉了,在 I/O 端口和中断号部分就接触过它。DMA 通道注册表与此非常类似。在包含了头文件 <asm/dma.h> 之后,用下面的函数可以获得和释放对 DMA 通道的所有权:
int request_dma(unsigned int channel, const char *name); void free_dma(unsigned int channel);
channel 参数是 0 到 7 的整数,或者更精确地说,是一个小于 MAX_DMA_CHANNELS 的正整数。在 PC 中,MAX_DMA_CHANNELS 定义为与硬件匹配的 8。name 参数是标识设备的字符串,指定的名字出现在文件 /proc/dma 中,用户程序可以读取该文件。
request_dma 函数返回 0 表示执行成功,返回 -EINVAL 或者 - EBUSY 表示失败。前一个错误码表示所请求的通道超出了范围,后面一个错误码表示通道正为另外一个设备所占用。
建议读者像对待 I/O 端口和中断信号线一样地小心处理 DMA 通道;在 open 操作时请求通道,比在模块初始化函数中请求通道更好一些。延迟请求允许在驱动程序间共享信息;比如声卡和类似的 I/O 接口如果不在同一时间内使用 DMA 通道时,就可以共享同一个 DMA 通道。
这里还建议读者在请求中断信号线之后请求 DMA 通道,并且在中断之前释放通道。这是在请求两种资源时常用的请求顺序;按照这个顺序可以避免可能的死锁。请注意使用 DMA 的每个设备还需要一根 IRQ 线,否则它将无法通知数据已经传输完毕。
在典型应用中,open 函数的代码有类似以下的形式,这段代码使用了假想的 dad 模块。
dad 模块不支持共享 IRQ 信号线,它使用了一个快速中断处理例程。
int dad_open(struct inode *inode, struct file *filp) { struct dad_device *my_device; /* ... */ if ( (error = request_irq(my_device->irq, dad_interrupt, SA_INTERRUPT, "dad", NULL)) ) return error; /* 或者实现阻塞的 open 操作 */ if ( (error = request_dma(my_device->dma, "dad")) ) { free_irq(my_device->irq, NULL); return error; /* 或者实现阻塞的 open 操作 */ } /* ... */ return 0; }
与 open 函数相匹配的 close 函数的实现如下:
void dad_close(struct inode *inode, struct file *filp) { struct dad_device *my_device; /* ... */ free_dma(my_device->dma); free_irq(my_device->irq, NULL); /* ... */ }
下面是一个安装了声卡系统的 /proc/dma 文件:
请注意默认的声卡驱动程序在系统启动时获得了DMA 通道,并且不会释放它。cascade 入口是一个占位符,表示通道 4 对驱动程序不可用,其原因在前面讲述过了。
(b)与 DMA 控制器通信
注册之后,驱动程序的主要任务包括为适当的操作配置 DMA 控制器。该任务不是可有可无的,但幸运的是,内核导出了驱动程序所需要的所有函数。
当 read 或者 write 函数被调用时,或者准备异步传输时,驱动程序都要对 DMA 控制器进行配置。根据驱动程序和其实现配置的策略,这一工作可以在调用 open 函数时,或者响应 ioctl 命令时被执行。这里的代码是被 read 和 write 设备方法调用的典型代码。
这一小节提供了 DMA 控制器内部的概貌,这样,读者就能理解这里所给出的代码。如果想要了解更多信息,可阅读 <asm/dma.h> 文件以及描述 PC 体系架构的硬件手册。特别是在这里不处理与 16 位数据传输相对的 8 位传输问题。如果要为 ISA 设备板卡编写驱动程序,应该仔细阅读该设备的硬件手册,以查找相关信息。
DMA 通道是一个可共享的资源,如果多于一个的处理器要同时对其进行编程,则会产生冲突。因此有一个叫作 dma_spin_lock 的自旋锁保护控制器。驱动程序不能直接处理该自旋锁;但是有两个函数能够对它进行操作:
unsigned long claim_dma_lock();
获得 DMA 自旋锁。该函数也阻塞本地处理器上的中断,因此返回值是描述先前中断状态的一系列标志。在重新开中断时,返回值必须传递给下面的函数以恢复中断状态。
void release_dma_lock(unsigned long flags);
返回 DMA 自旋锁并恢复先前的中断状态。
当使用下面描述的函数时,应该拥有自旋锁。但是在实际的 I/O 中却不应该拥有自旋锁。当拥有自旋锁时,驱动程序不能处于休眠状态。
必须被装入控制器的信息包含三个部分: RAM 的地址、必须被传输的原子项个数(以字节或字为单位)以及传输的方向。为达到这个目的,<asm/dma.h> 定义了下面的函数:
void set_dma_mode(unsigned int channel, char mode);
表明是从设备读入通道(DMA_MODE_READ)还是向设备写入数据(DMA_MODE_WRITE)。还有第三个模式 —— DMA_MODE_CASCADE,它用来释放对总线的控制。级联是将第一控制器连接到第二控制器顶端的方法,但是也可以用于真正的 ISA 总线控制设备。在这里不讨论总线控制。
void set_dma_addr(unsigned int channel, unsigned int addr);
为 DMA 缓冲区分配地址。该函数将 addr 的最低 24 位存储到控制器中。addr 参数必须是总线地址(请参看本章前面的 “总线地址” 部分)。
void set_dma_count(unsigned int channel, unsigned int count);
为传输的字节量赋值。count 参数也可以表示 16 位通道的字节数,此时字节数必须是偶数。
除了这些函数外,当处理 DMA 设备时,还有许多用于管理设备的函数:
void disable_dma(unsigned int channel);
控制器内的 DMA 通道可以被禁用。应该在配置控制器前禁用通道,以防止不正确的操作(否则由于控制器是针对 8 位数据传输编程的,可能会引起冲突,因此前面所介绍的函数都不能原子地执行)。
void enable_dma(unsigned int channel);
该函数告诉控制器,DMA 通道中包含了合法的数据。
int get_dma_residue(unsigned int channel);
驱动程序有时需要知道 DMA 传输是否已经结束。该函数返回还未传输的字节数。如果传输成功,返回值是 0,当控制器正在工作时,返回值并不确定(但不会是 0)。如果使用两个 8 位输入操作来获得一个 16 位的余量,则返回值是不可预测的。
void clear_dma_ff(unsigned int channel)
该函数清除了 DMA 的触发器(flip-flop)。触发器用于控制对 16 位寄存器的访问。我们可以通过两个连续的 8 位操作访问该寄存器,而触发器被用来选择低字节(当其清零时)还是高字节(当其被置位)。当传输完 8 位后,触发器自动反转;程序员必须在访问 DMA 寄存器前清除触发器(将其设置为可知状态)。
使用这些函数,驱动程序可以实现如下函数,为 DMA 传输做准备:
int dad_dma_prepare(int channel, int mode, unsigned int buf, unsigned int count) { unsigned long flags; flags = claim_dma_lock(); disable_dma(channel); clear_dma_ff(channel); set_dma_mode(channel, mode); set_dma_addr(channel, virt_to_bus(buf)); set_dma_count (channel, count); enable_dma(channel); release_dma_lock(flags); return 0; }
接着,下面的代码用来检查是否成功完成 DMA:
int dad_dma_isdone(int channel) { int residue; unsigned long flags = claim_dma_lock(); residue = get_dma_residue(channel); release_dma_lock(flags); return (residue == 0); }
剩下需要做的唯一事情是配置设备板卡。这个与设备相关的任务通常包括读写一些 I/O 端口。不同设备实现的方法差异很大。比如一些设备希望程序员告诉硬件 DMA 缓冲区的大小,而有些时候驱动程序不得不读出固化在设备中的数据。为了完成对板卡的配置,硬件手册是程序员唯一的朋友。
5、快速参考
本章介绍了下面这些与内存操作相关的符号。
(1)介绍材料
#include <linux/mm.h> #include <asm/page.h> // 在这些头文件中定义了大部分与内存管理相关的函数和结构,并给出了原型。 void *__va(unsigned long physaddr); unsigned long __pa(void *kaddr); // 在内核逻辑地址和物理地址之间进行转换的宏。
PAGE_SIZE
PAGE_SHIFT
前者以字节为单位给出在特定硬件中每页的大小,后者表示为获得物理地址,页帧
号需要移动的位数。
struct page // 在系统内存映射中,表示硬件页的结构。 struct page *virt_to_page(void *kaddr); void *page_address(struct page *page); struct page *pfn_to_page(int pfn); // 负责在内核逻辑地址和与其相关的内存映射入口之间进行转换的宏。page_address // 只能对已经显式映射的低端内存或者高端内存进行操作。pfn_to_page 将页帧号转 // 换为与其相关的 page 结构指针。 unsigned long kmap(struct page *page); void kunmap(struct page *page); // kmap 返回映射到指定页的内核虚拟地址,如果需要的话,还创建映射。kunmap 为 // 指定页删除映射。 #include <linux/highmem.h> #include <asm/kmap_types.h> void *kmap_atomic(struct page *page, enum km_type type); void kunmap_atomic(void *addr, enun km_type type); // kmap 的高性能版本;只有原子代码才能拥有映射结果。对驱动程序来说,type 可 // 以是 KM_USERO、KM_USER1、KM_IRQ0 或者是 KM_IRQ1。 struct vm_area_struct; // 描述 VMA 的结构。
(2)mmap 的实现
int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_add, unsigned long pfn, unsigned long size, pgprot_t prot); int io_remap_page_range(struct vm_area_struct *vma, unsigned long virt_add, unsigned long phys_add, unsigned long size, pgprot_t prot); // mmap 的核心函数。它们映射了物理地址中从 pfn 表示的页号开始的 size 个字节, // 到虚拟地址 virt_add 上。相关虚拟地址的保护位在 prot 中指定。如果目标地址 // 是在 I/O 地址空间的话,使用 io_remap_page_range 函数。 struct page *vmalloc_to_page(void *vmaddr); // 将从 vmalloc 函数返回的内核虚拟地址转化为相对应的 page 结构指针。
(3)直接 I/O 的实现
int get_user_pages(struct task_struct *tsk, struct mm_struct *mm, unsigned long start, int len, int write, int force, struct page **pages, struct vm_area_struct **vmas); // 该函数将用户空间缓冲区锁进内存,并返回相应的 page 结构指针。调用者必须拥 // 有 mm->mmap_sem。 SetPageDirty(struct page *page); // 将指定内存页标记为 "已经改动过" ,并在释放该页前将其写入后备存储器的宏。 void page_cache_release(struct page *page); // 从页缓存中释放指定的页。 int is_sync_kiocb(struct kiocb *iocb); // 如果指定的 IOCB 需要同步执行,该宏返回非零值。 int aio_complete(struct kiocb *iocb, long res, long res2); // 表明异步 I/O 操作完成的函数。
(4)直接内存访问
#include <asm/io.h> unsigned long virt_to_bus(volatile void * address); void * bus_to_virt(unsigned long address); // 用来在内核、虚拟地址、总线地址之间进行转换的老版本函数。与外围设备通信时, // 必须使用总线地址。 #include <linux/dma-mapping.h> // 用来定义通用 DMA 函数的头文件。 int dma_set_mask(struct device *dev, u64 mask); // 对于那些不能在全部 32 位寻址的外围设备来说,该函数通知内核可寻址的范围,如 // 果 DMA 可行则返回非零值。 void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *bus_addr, int flag) void dma_free_coherent(struct device *dev, size_t size, void *cpuaddr, dma_handle_t bus_addr); // 为缓冲区分配和释放一致性 DMA 映射,该缓冲区的生命周期与驱动程序相同。 #include <linux/dmapool.h> struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation); void dma_pool_destroy(struct dma_pool *pool); void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t *handle); void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t handle); // 用来创建、销毁和使用 DMA 池的函数,用来管理小型的 DMA 区域。
enum dma_data_direction;
DMA_TO_DEVICE
DMA_FROM_DEVICE
DMA_BIDIRECTIONAL
DMA_NONE
用来告诉流式映射函数的符号,表明数据传输的方向。
dma_addr_t_dma_map_single(struct device *dev, void *buffer, size_t size, enum dma_data_direction direction); void dma_unmap_single(struct device *dev, dma_addr_t bus_addr, size_t size, enum dma_data_direction direction); // 创建和销毁单个流式 DMA 映射。 void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction); void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr, size_t size, enum dma_data_direction direction); // 同步拥有流式映射的缓冲区。在使用流式映射时,如果处理器要访问缓冲区,则必 // 须使用这些函数。 #include <asm/scatterlist.h> struct scatterlist { /* */ }; dma_addr_t_sg_dma_address(struct scatterlist *sg); unsigned int sg_dma_len(struct scatterlist *sg); // scatterlist 结构描述了包含多个缓冲区的 I/O 操作。当实现了分散/聚集操作时, // 宏 sg_dma_address 和 sg_dma_len 用来获得总线地址和缓冲区长度,并将它们传递 // 给设备。 dma_map_sg(struct device *dev, struct scatterlist *list, int nents, enum dma_data_direction direction); dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents, enum dma_data_direction direction); void dma_sync_sg_for_cpu(struct_device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction); void dma_sync_sg_for_device(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction); // dma_map_sg 函数映射了分散 / 聚集操作,dma_unmap_sg 函数负责解除映射。当 // 映射处于活动状态而又必须访问缓冲区时,需要使用 dma_sync_sg_* 函数进行同 // 步。
/proc/dma
包含 DMA 控制器中被分配通道信息快照的文本文件。其中不会显示基于 PCI 的。
DMA,因为它们独立工作,不需要在 DMA 控制器中分配一个通道。
#include <asm/dma.h> // 定义与 DMA 相关的函数和宏的头文件,其中给出了函数原型。如果要使用下面的 // 符号,必须包含该文件。 int request_dma(unsigned int channel, const char *name); void free_dma(unsigned int channel); // 访问 DMA 注册表。 在使用 ISA DMA 通道前必须执行注册。 unsigned long claim_dma_lock(); void release_dma_lock(unsigned long flags); // 获得和释放 DMA 自旋锁,在调用 ISA DMA 函数前必须拥有自旋锁。它们也能禁 // 止和重新打开本地处理器的中断。 void set_dma_mode(unsigned int channel, char mode); void set_dma_addr(unsigned int channel, unsigned int addr); void set_dma_count(unsigned int channel, unsigned int count); // 在 DMA 控制器内对 DMA 信息编程。addr 是总线地址。 void disable_dma(unsigned int. channel); void enable_dma(unsigned int channel); // 在配置时,DMA 通道必须被禁止。这些函数用来改变 DMA 通道状态。 int get_dma_residue(unsigned int channel); // 如果驱动程序需要知道 DMA 传输的进展情况,可以调用这个函数,它将返回未被 // 传输数据的数量。在成功完成 DMA 后,该函数返回 0;当数据正在被传输时,返 // 回值是不可预测的。 void clear_dma_ff(unsigned int channel) // DMA 控制器使用触发器并通过两个 8 位操作传输一个 16 位的值。在把数据传递给 // 控制器前,必须清除触发器。