LDD-Memory Mapping and DMA

Memory Management in Linux

本章内容可以分为以下三部分:

  1. mmap系统调用的实现,mmap可以将设备的内存直接映射到用户进程的地址空间内,并不是所有的设备都支持mmap系统调用,但是有些情况下映射设备的内存能够带来显著的性能提升
  2. 通过get_user_pages将用户空间的内存映射到内核,从而能够访问用户空间的内存
  3. DMA I/O操作,外设可以直接访问系统内存

当然,上述内容都需要对Linux的内存管理有深入的理解,因此我们先从内存管理子系统开始。

Address Type

Linux采用虚拟内存系统,能够将程序的内存映射到设备的内存。
Linux系统使用了多种地址类型,每种地址类型都有自己的含义,但是内核的代码对这些地址类型的区分并不明显:

  1. User virtual address:用户虚拟地址,指用户程序能够看到的地址空间,64位系统下,用户程序的地址空间通常小于0x8000 0000 0000(参见Documentation/x86/x86_64/mm.txt)
  2. Physical address:处理器和系统内存间使用的地址
  3. Bus address:外设总线和内存间使用的地址,通常和处理器使用的物理地址相同;一些结构提供了I/O memory management unit(IOMMU),能够将总线映射到主存
  4. Kernel logical address:内核的地址空间,映射主存,通常和物理地址只相差一个偏移量
  5. Kernel virtual address:内核虚拟地址和内核逻辑地址类似,都将物理地址映射为内核地址;但是内核虚拟地址不想逻辑地址是线性映射。所有的逻辑地址都是虚拟地址,反之不成立

Physical Addresses and Pages

物理内存分成页,大小为PAGE_SIZE,通常为4KB。

High and Low Memory

32位系统中,内核将4GB的地址空间的低3GB分给用户程序,高1GB分给内核进程,而内核也需要将物理内存映射到自己的地址空间内才能访问,而内核地址空间的一部分还需要分出来运行内核代码,于是x86 Linux系统最多只能安装小于1GB的物理内存。
为了在32位系统下支持更多的内存,处理器厂商添加了地址扩展特性(PAE),于是很多32位的处理器能够寻址超过4GB的物理内存;但是能够直接映射到线性地址的内存仍然受到限制——只有内存的最低一部分(Low Memory)有线性地址,另一部分没有(High Memory)。因此在访问高内存前,内核需要显式的为其建立映射关系——大部分内核数据结构必须放在低内存,高内存一般用来服务用户进程。

  • Low Memory:其逻辑地址存在于内核空间
  • High Memory:其逻辑地址不存在,因为超过内核虚拟地址空间可寻址的范围

在i386系统,低内存和高内存通常以1GB为界限,尽管可以通过内核配置选项修改——这个值仅仅供内核来分割地址空间,和硬件没有关系。

The Memory Map and Struct Page

由于历史原因,内核代码中的逻辑地址指物理内存的页,但是高内存的存在带来了问题——高内存没有逻辑地址。于是内核代码开始用struct page类型的指针来管理内存,这个结构体包含内核所需要的物理内存信息,其中一些信息如下:

  • atomic_t count:引用该页框的计数,减少至0时将其添加到空闲列表
  • void *virtual:内核的虚拟地址,未映射为NULL。低内存的页框通常被映射,高内存的页框通常没有映射——并非所有的结构都会有这个域,只有内核的虚拟地址无法轻易计算才会有;page_address宏可以得到这个值
  • unsigned long flags:指明页框的状态,PG_locked指页框固定在内存中,PG_reserved组织内存管理系统操作该页框

内核可能维护所有物理页框的一个或多个列表,叫做mem_map。在NUMA系统中,这个列表可能有多个,因此最好不要直接引用这个列表。
struct page相关的函数有很多,但是根据函数名可以得知其功能:

struct page *virt_to_page(void *kaddr);
struct page *pfn_to_page(int pfn);
void *page_address(struct page *page);  
/* 如果存在的话,返回内核虚拟地址 */

#include <linux/highmem.h>
void *kmap(struct page *page);
void kunmap(struct page *page);
/* kmap会返回页框的内核虚拟地址。对于低内存,直接返回其逻辑地址;
   对于高内存,在内核的地址空间建立映射,并返回。这种映射的数量有
   限,而且无法建立映射时,可能会休眠。*/

#include <linux/highmem.h>
#include <asm/kmap_types.h>
void *kmap_atomic(struct page *page, enum km_type type);
void kunmap_atomic(void *addr, enum km_type type);
/*  kmap_atomic是kmap的高性能形式,每种体系结构都会维持一小部分用
   来进行原子的kmap的slot(槽),调用者通过type参数指明所需的槽类型。
   驱动程序只会用到KM_USER0和KM_USER1(用户空间直接调用的代码),
   KM_IRQ0和KM_IRQ1(中断处理函数)。调用atomic函数时不能休眠,内核
   不能阻止两个函数尝试使用同一个槽。 */

Page Tables

处理器通过页表将虚拟地址转化为物理地址,页表通常为树形结构,还有一些标志位。设备驱动可能需要操作页表,不过在2.6内核,驱动程序已经不需要直接对页表进行操作。

Virtual Memory Areas

虚拟内存区域(VMA)是管理不同进程地址空间的内核数据结构:包括一片连续的权限位一样的虚拟内存区域(并由同样地对象支持,backed up by the same object)。进程的内存空间至少包含以下部分:

  • 程序的可执行代码区(text)
  • 多个数据区,包括 初始化过的数据区和未初始化的数据区(bss),还有程序栈
  • 活动的内存的映射区(One area for each active memory mapping)

进程的内存空间可以根据PID获取:/proc/<pid>/maps,/proc/self总是指向当前的进程。
/proc/<pid>/maps中的内容和sturct vm_area_struct结构体的成员相对应:

  • start end:内存区域的起始虚拟地址
  • perm:指明读、写、执行权限的掩码,最后一个字符p指私有,s指共享
  • offset:内存区域在其映射到的文件的起始位置
  • major minor:保存被映射的文件的设备的主从设备号;对于设备,主从设备号指保存设备文件的磁盘分区,而不是设备自己的主从设备号
  • inode:被映射的文件的inode号
  • image:被映射的文件名,通常为可执行镜像

用户进程调用mmap函数将设备的内存映射到自己的地址空间时,系统会创建一个VMA来代表这个映射,支持mmap功能的设备驱动需要完成VMA的初始化。
struct vm_area_struct中的一些和驱动程序相关的成员为:

  • unsigned long vm_start, vm_end
  • struct file *vm_file:和该区域相关的文件
  • unsigned long vm_pgoff:对齐到页,映射到这片区域的第一个页在文件中的位置
  • unsigned long vm_flags:指明区域类型,VM_IO指MMIO,防止用于进程核心转储;VM_RESERVED防止被交换
  • struct vm_operations_struct *vm_ops:内核可能用来对区域进行操作,其存在指明内存区域是一个内核对象,就像struct file
  • void *vm_private_data:驱动可用来保存自己的信息

struct vm_operation_struct中的以下操作即可满足进程的需要:

  • void (*open)(struct vm_area_struct *vma):VMA有新的引用时调用(fork),VMA第一次创建时通过mmap函数,不会调用此函数
  • void (*close)(struct vm_area_struct *vma):内存区域销毁时,内核调用此函数,VMA没有引用计数,因此打开和关闭必须相对应
  • struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type): 进程尝试访问VMA内的一个页面,但是不再内存中,会调用此函数。函数在将请求的页面读入内存后会将对应的页面指针返回。如果没有定义此函数,内核分配一个空页面
  • int (*populate)(struct vm_area_struct *vm, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock):在访问前将其挤出内存(prefault)

The Process Memory Map

每个进程都有一个struct mm_struct成员,包含进程的所有虚拟内存区域,页表,以及其他内存管理所需的信息,还有一个信号量(mmap_sem)和一个自选锁(page_table_lock)。

The mmap Device Operation

内存映射是现代Unix系统最有趣的特点之一,对于驱动,实现内存映射能够帮助用户程序直接访问设备内存。
映射设备的内存意味着将一段用户地址空间和设备内存相关联,当程序访问分配的地址范围时,起始是在访问设备。
但是对于串口和其他的面相数据流的设备,mmap没有意义;而且mmap只能对齐到PAGE_SIZE——内核只能在页表的基础上管理虚拟地址,映射的区域在物理内存上也必须以页为单位(因为物理地址和虚拟地址只相差一个偏移量?)。
mmapfile_operations结构体的一部分,进行mmap系统调用时会调用这个函数。调用mmap函数时,内核会进行大量的工作,因此系统调用的形式和文件操作的形式差别很大。
系统调用mmap定义如下:
mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset)
文件操作定义如下:
int (*mmap)(struct file *filp, struct vm_area_struct *vma);
vma包含访问设备所用的虚拟地址范围,驱动程序只需要为vma内的地址范围正确建立页表,如果可能的话给vma->vm_ops赋新值,其他的工作都由内核完成。
建立页表的方式有两种:通过函数remap_pfn_range一次性建立所有的页表,或者通过vm_area_structnopage函数每次为一个页面建立页表。

Using remap_pfn_range

要映射一段地址范围内的物理地址,通过remap_pfn_rangeio_remap_page_range函数:

int remap_pfn_range(struct vm_area_struct *vma,
                unsigned long virt_addr, unsigned long pfn, 
                unsigned long size, pgprot_t prot);
int io_remap_page_range(struct vm_area_struct *vma,
                unsigned long virt_addr, unsigned long phys_addr,
                unsigned long size, pgprot_t prot);

vma保存物理地址映射后的内存区域;virt_addr为映射开始的用户空间虚拟地址,函数为virt_addrvirt_addr+size间的地址范围建立页表;pfn指虚拟地址对应起始的物理页框号,函数会影响pfn<<PAGE_SHIFTpfn<<PAGE_SHIFT+size间的物理地址;size指映射的区域的大小,字节计;prot指VMA的权限保护位。
在映射设备的内存时,要注意cache的影响。

Mapping Memory with nopage

虽然remap_pfn_range函数能满足大部分需求,但是VMA的nopage函数更加灵活。例如可以调整映射区域边界的系统调用mremap,如果VMA减小,内核会在不告诉驱动的情况下悄悄地将不需要的页面冲刷出去;如果VMA增大,内核在为新的页面建立页表时就会调用nopage函数。定义如下:
struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);
address是发生缺页的地址,对齐到页,nopage函数只需计算出address对应的页面的指针,增加页的引用计数get_page(struct page *pageptr);
nopage函数还需要将缺页的类型保存在type参数内,如果type不是NULL的话。对于驱动程序而言,type的值只能是VM_FAULT_MINOR
如果VMA的nopage函数为NULL,内核缺页处理函数会将0页面映射到中断的虚拟地址。0页面是一个写时复制页,读的结果为0,用来映射BSS段。如果进程对0页面进行写操作,最终会修改自己页面的拷贝。也就是说,如果一个进程通过mremap扩展内存,而驱动程序又没有实现nopage函数,进程会以0x0000 0000 内存结束(unsupported reference to 0x0000 0000?),而不是段错误。

Remapping Specific I/O Regions

/dev/mem将所有的物理地址(主存和设备)都映射到了用户空间,但驱动程序有时只想映射外设的一部分地址范围,这时可以先计算出需要映射的物理地址对应的页框号,然后通过函数remap_pfn_range进行映射。

Remapping RAM

remap_pfn_range函数有一个限制,只能映射保留的页面和大于物理内存的物理地址(因为物理内存都和内核逻辑地址相对应?)。在Linux中,标记为保留的物理地址不会被内存管理系统操作;在PC上,640KB-1MB的内存是保留的,因为保存着内核代码。保留的页面常驻于内存当中,才能安全的映射到用户空间,这样才能维持系统稳定。
remap_pfn_range不会允许映射传统的地址,包括通过get_free_page得到的页面,他会将这些地址映射到0页面。

Remapping RAM with the nopage method

要将计算机的主存进行映射,可以通过nopage函数实现,但是还要实现设备的openclose函数,以及正确调整页面的引用计数。

Remapping Kernel Virtual Address

驱动程序还可以将内核的虚拟地址通过mmap映射到用户空间,只有映射到内核页表的虚拟地址才是真正的内核虚拟地址,比如vmalloc函数的返回值。vmalloc函数每次只会申请一个页面,因为但页面的申请远比多页面的申请更容易成功。通过vmalloc申请到的页面需要通过vmalloc_to_page转化为struct page类型的指针。
但是,通过ioremap得到的地址不能通过同样地方式映射到用户空间——ioremap返回的地址十分特殊,不能像普通的内核虚拟地址那样操作,需要用remap_pfn_range将I/O的内存区域映射到用户空间。

Performing Direct I/O

通常情况I/O的操作都会通过内核进行缓冲,既能够将用户程序和设备隔离,也能带来显著的性能提升。但是如果要传输大量的数据,将数据从用户空间拷贝到内核空间,然后再由内核发送到设备,会影响传输的效率。
实现直接I/O的内核函数是get_user_pages,声明在<linux/mm.h>中:

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);
  • tsk:进行I/O的进程,用来告知内核建立缓冲区时处理缺页的进程,通常为current
  • mm:包含映射的地址空间的内存管理结构,通常为current->mm
  • start,len:start是用户空间缓冲区的起始地址,页对齐;len是缓冲区的大小,以页计
  • write,force:如果write非零,映射的页可以写;force非零告知get_usr_pages用传入的访问权限位覆盖原来的权限位
  • pages,vmas:输出参数,成功的话pages包含指向用户缓冲区的struct page指针,vmas包含相关的VMA

函数的返回值为映射的页面数,可能返回少于请求的页面数。而且调用get_user_pages函数前需要请求用户空间的读写信号量。
函数成功返回后,调用者会得到一个指向用户空间的缓冲区的struct page类型的数组,要直接对缓冲区操作,内核代码需要通过kmapkmap_atomic将其转化为内核的虚拟地址。
直接I/O完成后,必须将缓冲区占用的用户页释放,在释放前一定要通知内核对这些内存页所做的更改,否则内核会将这些页视为干净的,在交换设备上发现匹配的拷贝后,直接将其释放,而不将这些数据写回到硬盘中,导致数据丢失。
因此,对于修改过的内存页,通过void SetPageDirty(struct page *page);将其设为脏的;很多代码在调用这个函数前,还会先判断内存页是否是保留页,不会被内存换出PageReserved(page)
无论页面是否被修改,都必须从页面的高速缓存中释放,否则会永远驻留,在设置脏位后调用void page_cache_release(struct page *page);

Asynchronous I/O

异步I/O使得用户程序能够在等待I/O操作的过程中执行其他操作。块设备和网络设备的驱动总是完全异步的,只有字符设备才需要显式的异步I/O。
实现异步I/O的驱动需要包含头文件<linux/aio.h>,实现三个文件操作方法:

ssize_t (*aio_read)(struct kiocb *iocb, char *buffer, 
              size_t count, loff_t offset);
ssize_t (*aio_write)(struct kiocb *iocb, const char *buffer,
              size_t count, loff_t offset);
int (*aio_fsync)(struct kiocb *iocb, int datasync);

aio_fsync函数和文件系统相关,aio_readaio_write函数的偏移量通过值直接传入,因为异步操作不会改变文件的位置,函数还需要iocb的参数,指明I/O控制块,I/O control block。
aio_readaio_write函数发起一个读写操作,在返回之前可能不会完成;如果操作能够立即完成,需要返回传输的字节数,或者错误码。
内核可能会创建同步的IOCB,这些是必须同步执行的异步操作。同步的操作在IOCB中会有标记,可以通过int is_sync_kiocb(struct kiocb *iocb);来查询,如果返回非零值,驱动程序必须同步执行操作。
如果驱动能够初始化一个操作,必须记住和操作相关的所有所需信息,然后返回-EIOCBQUEUED给调用者,表明操作尚未完成,最终的状态需要后续通知。
当“后续”到来时,驱动必须通知内核操作已经完成,通过int aio_complete(struct kiocb *iocb, long res, long res2);完成。res是操作的完成状态码,res2是返回用户空间的状态码,通常为0。一旦调用aio_complete函数,就不能再修改IOCB和用户缓冲区。

Direct Memory Access

DMA是允许外设直接和主存进行I/O数据传输而不需要处理器的硬件机制,可以显著提升设备的吞吐量,因为省去了许多计算的开销。

Overview of a DMA Data Transfer

DMA传输可能有两种触发方式:软件请求数据(例如read函数)或者硬件异步的将数据传输到系统。第一种方式涉及到的操作如下:

  1. 进程调用read函数,驱动分配一个DMA缓冲区,命令硬件将数据传输到缓冲区中,进程被设为休眠状态
  2. 硬件将数据写入DMA缓冲区中,完成时产生一个中断
  3. 中断处理函数获取到输入数据,ACK中断,唤醒休眠的进程

第二种方式在异步使用DMA时会用到,例如数据采集设备会将数据发送到系统,计时没有进程读取。这种情况驱动需要维护一个缓冲区,以便后续的读操作能够得到所有积累的数据,涉及到的操作如下:

  1. 硬件产生中断,表明新数据的到来
  2. 中断处理函数分配一个缓冲区,告知硬件传输数据传输的地址
  3. 外设将数据写入缓冲区,在完成时产生另一个中断
  4. 中断处理函数唤醒相关的进程,并分发新数据

异步的方式经常在网卡中用到,这些网卡在内存中有一个循环缓冲区(DMA ring buffer),和内核共享;新到来的数据包放在环中下一个空闲的缓冲区中,然后产生一个中断。驱动将数据包发送给内核,并在环中放置一个新的DMA缓冲区。
通常情况下DMA缓冲区会在初始化时分配给驱动,因此上述的分配缓冲区其实指的是获取之前分配的缓冲区。

Allocating the DMA Buffer

如果DMA缓冲区大于一个PAGE,必须在物理地址上连续,因为设备通过ISA或者PCI总线传输数据,二者通过物理地址进行操作(SBus不同)。
尽管DMA缓冲区可以在系统启动或者运行时分配,模块只能在运行时分配自己的缓冲区。驱动程序在分配DMA缓冲区时必须注意内存的类型,不是所有的内存区域都可以用来进行DMA传输——在某些系统中,高内存不适合用来DMA传输,因为超出了一些设备的寻址范围。比如一些PCI设备只能工作在32位地址下,ISA设备只能寻址24位。
对于这些设备,在分配DMA缓冲区时要指明GFP_DMA标志给函数get_free_pages,分配24位地址以下的内存,也可以通过通用的DMA层来分配缓冲区。

虽然get_free_pages可以申请多达数MB的内存,但是这种分配可能会失败,即使请求的内存远少于128KB,因为系统的内存支离破碎。
如果要分配大量连续的内存给DMA缓冲区,可以在系统启动时指明mem=SIZE参数,将内核可用的内存限定在SIZE之内,然后在驱动中将预留的内存分配给DMA缓冲区。
还可以通过指明GFP_NOFAIL标志,但是会给内存管理系统带来严重的负担,有一定的风险锁死系统(这个标志可能会强制内存管理系统进行页面的迁移,以便拼凑出满足要求的内存空间)。

Bus Addresses

采用DMA设备的驱动需要通过总线和硬件打交道,支持DMA的硬件使用总线地址。尽管ISA和PCI总线地址就是PC上的物理地址,但一些总线通过桥接将I/O地址映射到不同的物理地址。
内核提供了将总线地址转化为虚拟地址的函数,但是只有在很简单的I/O结构的系统中才能正常工作。

unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);

正确的地址转换应该通过通用DMA层实现。

The Generic DMA Layer

DMA操作最终归结于分配一个缓冲区,然后将总线地址传送给设备。DMA传输可能带来cache一致性的问题,如果不能正确处理,会损坏内存的数据。幸运的是,内核提供了独立于总线和架构的DMA层,帮助处理这些问题。

内核假定设备能够对任何32位地址进行DMA传输,否则需要调用int dma_set_mask(struct device *dev, u64 mask);告知内核。mask指明限制的位数,比如24位值为0xFF FFFF。如果可以,返回非零值。
如果设备支持32位DMA操作,不需要调用dma_set_mask函数。

DMA映射指分配DMA缓冲区,并为其产生一个设备可访问的地址。
分配缓冲区时,可能会需要建立跳板缓冲区(bounce buffer)。跳板缓冲区用来帮助在超过外设可寻址的地址范围进行DMA操作——数据通过跳板缓冲区传送到寻址范围外的地址,会降低数据传输的效率。
DMA映射还必须考虑cache一致性的问题。处理器会将经常访问的内存数据放在一个高速缓存中,以获得显著的性能提升。如果内存中的数据发生变化,处理器需要将cache中对应的数据无效,以免发生错误。因此,如果设备通过DMA读取内存中的数据,必须现将cache中对应的数据刷出。通用DMA层有大量代码保证cache一致性,但是必须遵守一些规则。
DMA映射用dma_addr_t来代表总线地址,驱动程序不能使用,只能将其传送给支持DMA的例程和设备。如果CPU直接使用dma_addr_t,可能出现意外的问题。
PCI代码根据DMA缓冲区存在的时间将DMA映射分为两种:

  • Coherent DMA mapping:一致性的DMA映射。存在于驱动的整个生命周期中,必须同时可被CPU和外设使用,因此必须存在于cache一致的内存中,建立和使用的代价较高
  • Streaming DMA mapping:流式DMA映射。通常用来进行单个操作,一些架构在使用流式映射时会有显著的性能提升,但是和访问其的方式有关。内核开发者推荐尽可能的使用流式映射。例如在支持寄存器映射的系统中,每个DMA映射可能使用总线上的一个或多个寄存器,由于一致性DMA映射存在于驱动的整个生命周期,会长时间占用这些寄存器。还有在某些硬件上,流式映射能够以一致性映射不可得的方法进行加速。

Setting up coherent DMA mappings

驱动程序通过dma_alloc_coherent创建一致性DMA映射void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, int flag);。函数会分配并映射DMA缓冲区,将缓冲区的内核虚拟地址返回,同时保存在dma_handle中,flag指明内存分配的方式,通常为GFP_KERNEL或者GFP_ATOMIC(运行在原子上下文中)。
缓冲区不再使用时,即模块卸载阶段,通过void dma_free_coherent(struct device *dev, size_t size, void *vaddr, dma_addr_t dma_handle);将缓冲区返还系统。

DMA pools

DMA池是用来分配小的一致性DMA映射的机制,dma_alloc_coherent分配的映射最小为一个页,要分配小于一个页的DMA缓冲区,需要通过DMA池。
DMA池在使用前需要先创建struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t allocation);。name是池的名字,size是pool可分配的缓冲区的个数,align是分配的缓冲区的大小,allocation如果非零,指明分配不能超过的边界。
DMA池使用完毕后需要释放void dma_pool_destroy(struct dma_pool *pool);,获取和释放DMA缓冲区:

void *dma_pool_alloc(struct dma_pool *pool, int mem_flags,
              dma_addr_t *handle);
void dma_pool_free(struct dma_pool *pool, void *addr,
              dma_addr_t addr);

Setting up streaming DMA mappings

流式DMA映射比一致性映射的结构更复杂:流式映射会用到已经由驱动分配的缓冲区,需要处理不是由自己选择的地址;在某些架构,流式映射会使用多个不连续的分散/聚集(sactter/gather)缓冲区。
创建流式映射时,必须告知内核数据传输的方向:

  • DMA_TO_DEVICE,DMA_FROM_DEVICE:见名知义
  • DMA_BIDIRECTIONAL:双向传输
  • DMA_NONE:调试需求,会引起内核panic

似乎选择双向传输总是没错的,但是在某些架构上会影响性能。
如果只需要传输一个单独的缓冲区,可以采用以下函数:

dma_addr_t dma_map_single(struct device *dev, void *buffer, 
        size_t size, enum dma_data_direction directrion);
void dma_unmap_single(struct device *dev, dma_addr_t dma_addr,
        size_t size, enum dma_data_direction direction);

流式DMA映射的使用要遵守以下规则:

  • 只能按照建立映射时指定的方向传输数据
  • 缓冲区建立后,属于设备所有,而不是处理器。在缓冲区被解除映射之前,不能修改缓冲区中的内容;只有调用unmap函数后才能修改缓冲区的内容。这条规则意味着正在写入设备的缓冲区在包含所有要写入的数据前不能被映射。
  • DMA处于活跃状态时缓冲区不能解除映射

对于要映射的内存区域超出设备的寻址范围的情况,有些架构通过跳板缓冲区来实现映射。如果一个映射的数据传输方向为DMA_TO_DEVICE,同时又需要跳板缓冲区,原缓冲区的数据会首先拷贝到跳板缓冲区,然后再拷贝到设备中。很明显,如果原缓冲区的数据在拷贝到跳板缓冲区后发生了改变,并不会影响到最终发送给设备的数据。类似的,对于DMA_FROM_DEVICE的映射,跳板缓冲区的数据在调用dma_unmap_single函数后才会拷贝到原缓冲区。(书中还说跳板缓冲区是保证传输方向正确的原因之一:DMA_BIDIRECTIONAL的跳板缓冲区中的内容会在操作的前后都进行拷贝,导致性能下降——从这个说法中可以推断出跳板缓冲区的数据拷贝操作只和设置的DMA数据传输方向有关)。
驱动程序要访问DMA缓冲区的数据,要通过函数void dma_sync_single_for_cpu(struct deivce *dev, dma_handle_t bus_addr, size_t size, enmu 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);

Single-page streaming mappings

有时可能建立的缓冲区的大小刚好为一个页面,可以通过下列函数:

dma_addr_t dma_map_page(struct device *dev, struct page *page,
                unsigned 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);

offsetsize参数可以用来映射一个页面内的部分空间,建议不要映射部分页面,如果映射的内存只覆盖了部分高速缓存行(cache line),可能带来cache一致性的问题。

Scatter/gather mappings

分散/集中映射是一种特殊的流式DMA映射。假设现在有多个缓冲区都需要向设备发送数据,或者从设备接收数据,可以分别将这些缓冲区一一映射,然后分别进行传输操作。
许多设备支持scatterlist,包括一系列的数组指针和长度信息,将所有的数据通过一次DMA操作传输。在某些系统中,物理连续的页面能够被设备视为一个单独的连续的数组,当scatterlist的表项大小都为一个页面时(除了第一项和最后一项),可以将多个操作通过一个DMA完成,提高传输效率。
在使用跳板缓冲区时,将scatterlist中的多个操作整合到一个缓冲区中,也能够带来性能提升。
scatterlist定义在asm/scatterlist.h中,通常包括:

  • struct page *page;
    scatter/gather操作中使用的缓冲区所在的页
  • unsigned int length;
  • unsigned int offset;
    缓冲区的长度及其在页面内的偏移量

要映射scatter/gather DMA操作,驱动需要将每个缓冲区的pagelengthoffset参数设置好,然后调用int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction direction);函数,nents是scatterlist中的表项数,返回值是要传输的缓冲区的数量,可能小于nents
对于scatterlist中的每一个缓冲区,dma_map_sg都会判断其正确的总线地址,将内存上连续的缓冲区组合。如果系统中有I/O内存管理单元,dma_map_sg还会设置管理单元的映射寄存器,使得设备有一定可能性传输单个连续的缓冲区。
驱动程序需要将每一个dma_map_sg返回的缓冲区进行传输,struct scatterlist表项中包含每个缓冲区的总线地址和长度。可以通过下列函数获得:

dma_addr_t sg_dma_address(struct scatterlist *sg);
unsigned int sg_dma_len(struct scatterlist *sg);

当然,两个函数返回的结果可能和传入的结果不一致。
传输完成后,通过void dma_unmap_sg(struct device *dev, strcut scatterlist *list, int nents, enum dma_data_direction direction);解除scatter/gather映射。nents必须和调用dma_map_sg函数时传入的参数一致,而不是dma_map_sg函数的返回值。
scatter/gather映射也是流式DMA映射,因此必须遵守同样地规则;如果要访问已经映射的scatterlist,必须先进行同步操作:

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

PCI double-address cycle mappings

PCI总线支持64位地址,即double-address cycle(DAC)。通用DMA层不支持这种模式,因为只是PCI总线才有这种特性。而且DAC实现存在一些问题,还会带来性能的下降。但是,如果驱动程序需要访问很大的地址空间来建立缓冲区,可以使用PCI总线的DAC特性。
要使用DAC,必须包含linux/pci.h,设置DMA地址的掩码int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask);,成功返回0。DAC映射使用的地址为dma64_addr_t类型,通过函数dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev, struct page *page, unsigned long offset, int direction);建立映射。
从函数的参数可以看出,DAC映射接收的参数类型为struct page,意味着要映射的内存地址位于高内存中——如果不是,就没有使用DAC映射的必要;而且每次必须映射单独一个页面。
DAC映射不需要外部资源,使用后不需要显式释放,但是对缓冲区进行操作时需要向流式映射一样首先获得缓冲区的所有权:

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

DMA for ISA Devices

ISA总线支持两种DMA传输:本地DMA和ISA总线DMA。本地DMA使用主板上的DMA控制器控制ISA总线上的信号进行传输,ISA总线DMA完全由外设自己进行传输,已经很少使用,从驱动的角度来看和PCI设备的DMA很想,内核代码中有一个实例,1542 SCSI控制器,drivers/scsi/aha1542.c
对于本地DMA,数据传输包括以下三部分:

  • The 8237 DMA controller(DMAC)
    控制器包含DMA传输的方向,内存地址,传输的长度等信息,还有一个计数器保存正在进行的传输的状态信息。控制器收到DMA请求信号时,控制总线的信号线来进行传输。
  • The peripheral device
    设备准备好传输数据后,激活DMA请求信号;传输完成后通常会产生中断。
  • The device driver
    提供传输方向、总线地址、传输的长度信息给DMA控制器,通知外设准备好传输的数据,在传输完成后处理产生的中断

PC的DMAC通常有4个通道,每个通道都有自己的寄存器。较新的PC包含两个DMAC,主控制器和处理器直接相连,从控制器连接到主控制器的第0个通道。从整体上看,从控制器的0-3通道和主控制器的5-7通道可用,主控制器的通道4不可用,用来串联从控制器。每一个DMA传输的大小,保存在控制器中,是一个代表总线cycle数的16位数。因此,从控制器的最大传输长度为64KB(每个cycle传输8bit),主控制器的最大传输长度为128KB(每个cycle传输16bit数据)。
DMAC是系统资源,因此内核提供了DMA注册方法来请求和释放DMA通道,以及一些函数来配置DMA控制器中的通道信息。

Registering DMA usage

和I/O端口类似,DMA通道注册函数定义在asm/dma.h,申请和释放函数如下:

int request_dma(unsigned int channel, const char *name);
void free_dma(unsigned int channel);

channel参数是一个在0和MAX_DMA_CHANNELS之间的值,name用来标识设备,会出现在/proc/dma文件中,能够被用户程序读取。请求成功返回0,否则返回错误码。
和I/O端口以及中断线一样,建议在open时请求,不要在模块初始化时请求,以免其他程序无法使用。
还建议在请求中断线以后再请求DMA通道,在释放中断线以前释放DMA通道,以免发生死锁(DMA通道需要在传输完成后通过中断告知系统)。

Talking to the DMA controller

注册DMA后线,驱动的主要任务就是配置DMAC,内核向驱动提供了所需的函数。DMAC是共享资源,为了防止多个CPU同时使用一个DMAC,DMAC有自旋锁,dma_spin_lock。内核提供了获取锁的函数unsigned long claim_dma_lock();和释放锁的函数void release_dma_lock(unsigned long flags);。获取函数会阻塞当前处理器的中断,因此返回值是一组描述之前中断状态的标志位,需要传递给释放函数。
在执行下列操作时,必须持有自旋锁。但是在进行真正的I/O时,不能持有自旋锁;驱动程序持有自旋锁时,不能休眠。
驱动程序必须设置好控制器传输的地址,长度和方向。asm/dma.h中声明了下列函数来快速设置DMAC:

  • void set_dma_mode(unsigned int channel, char mode);
    设置DMA传输的方向,DMA_MODE_READ时从设备读取数据,DMA_MODE_WRITE是向设备写入数据,DMA_MODE_CASCADE是从控制器和主控制器相连的方式。
  • void set_dma_addr(unsigned int channel, unsigned int addr);
    设置DMA缓冲区的地址,函数将addr的低24位保存在控制器中,addr必须是一个总线地址。
  • void set_dma_count(unsigned int channel, unsigned int count);
    设置传输的字节数。

内核还提供了函数来设置DMA设备(这些操作也必须在持有锁的条件下进行):

  • void disable_dma(unsigned int channel);
    关闭控制器内的一个DMA通道,控制器在正确配置前应该关闭通道,防止不正确的操作。
  • void enable_dma(unsigned int channel);
    通知控制此DMA通道包含有效的数据。
  • int get_dma_residue(unsigned int channel);
    驱动有时候需要判断通道内的DMA传输是否完成,这个函数可以返回需要传输的字节数,如果传输已经完成返回0。
  • void clear_dma_ff(unsigned int channel);
    清除DMA触发器,DMA触发器用来控制16位寄存器的访问。寄存器通过两次连续的8位操作访问,触发器清空时,可以访问低8位;触发器设置时,可以访问高8位。触发器在传输8位后自动翻转,因此在访问DMA寄存器时,必须进行清除,将其置为确定的状态。

猜你喜欢

转载自www.cnblogs.com/adera/p/10118044.html
DMA