Memory Mapping and DMA [LDD3 15]
Table of Contents
15.1. Memory Management in Linux
15.1.2. Physical Addresses and Pages
15.1.4. The Memory Map and Struct Page
15.1.6.1 The vm_area_struct structure
15.1.7. The Process Memory Map
15.2. The mmap Device Operation
15.2.2. A Simple Implementation
15.2.4. Mapping Memory with nopage
15.2.5. Remapping Specific I/O Regions
15.2.6.1 Remapping RAM with the nopage method
15.2.7. Remapping Kernel Virtual Addresses
15.4.1. Overview of a DMA Data Transfer
15.4.2. Allocating the DMA Buffer
15.4.2.1 Do-it-yourself allocation
15.4.4.1 Dealing with difficult hardware
15.4.4.3 Setting up coherent DMA mappings
15.4.4.5 Setting up streaming DMA mappings
15.4.4.6 Single-page streaming mappings
15.4.4.7 Scatter/gather mappings
15.4.4.8 PCI double-address cycle mappings
15.4.4.9 A simple PCI DMA example
15.4.5.1 Registering DMA usage
15.4.5.2 Talking to the DMA controller
本章的内容分为三节:
1, mmap的实现,也就是把device memory map到user process里,这样可以提高performance。
2, kernel driver如何访问user space的page。
3, DMA,也就是device直接访问system memory。
15.1. Memory Management in Linux
15.1.1. Address Types
Linux kernel中有很多的地址类型,主要分为两类:虚拟地址,物理地址。在用户程序中看到的地址都是虚拟地址,和硬件使用的物理地址不同,虚拟地址并不直接对应物理地址,需要中转,采用虚拟地址的机制,可以让程序使用比物理地址多得多的内存。
kernel中的地址类型其实还需要细分,虚拟地址包括几种类型如下:
User virtual addresses
user space programmer看到的地址就是用户态虚拟地址,一般是32bit或者64bit,取决于当前的硬件架构,每个进程都有自己的虚拟地址空间。
Physical addresses
在处理器和系统内存之间,使用的就是物理地址。物理地址也分为32bit或者64bit,即便32bit系统,在某些条件下也能使用很大的物理内存。
Bus addresses
在外设总线和物理内存之间,使用的就是bus address。通常和CPU使用的物理地址相同,但是如果有IOMMU,就不一样了。IOMMU会把物理地址做一个map,拿到的地址就是bus address,device可以通过这个bus address做DMA。
Kernel logical addresses
这个就是kernel自己的normal地址空间,一般会把物理内存map成normal地址空间,并且可以当作物理地址空间来使用,某些架构上,kernel的逻辑地址和物理地址差一个offset,逻辑地址通常使用硬件相关的native pointer,指针能访问多少memory,逻辑地址空间就能支持多少memory,通常这个pointer是一个unsigned long或者void *类型,因此如果在32bit系统上,就可能访问不了大的内存地址。kmalloc返回的就是逻辑地址。
Kernel virtual addresses
kernel虚拟地址和逻辑地址有些类似,都是最终map到物理地址,区别在于虚拟地址对应的物理地址可能不是连续的,不是一一映射。逻辑地址是虚拟地址的子集,即所有的逻辑地址都是虚拟地址,但不是所有的虚拟地址都是逻辑地址。例如,vmalloc和kmap返回的就是虚拟地址,它对应的物理内存可能不是连续的,kmalloc返回的地址就是逻辑地址,它分出来的page都是连续的。虚拟地址通常使用指针变量来存储。

如果你有一个逻辑地址,可以通过__pa()获取到它对应的物理地址;物理地址也可以通过__va()获取它的逻辑地址,但只限于low memory page,不能用于high-memory page。
在kernel的函数中,不同的接口可能需要不同的地址类型,这个需要自己特别注意。
15.1.2. Physical Addresses and Pages
kernel中的物理内存,都是按照page来管理,每一个page都是PAGE_SIZE这么大,PAGE_SIZE具体的值取决与硬件架构,一般是4096 byte。无论虚拟地址(逻辑地址?)还是物理地址,地址的组成都分成了两个部分:PFN(page frame number)和offset,假设先的PAGE_SIZE是4096byte,那么低12bit就是作为page中的offset,如果把这低12bit向右移出去,得到的值就是PFN。获取pfn的操作在kernel中很常见,具体移出去多少bit,取决于PAGE_SHIFT这个值。
15.1.3. High and Low Memory
kernel的虚拟地址和逻辑地址的区别在大容量物理内存的32位机器上笔记明显,理论上讲,32位机器上可以寻址4G的地址空间,但是因为kernel设置虚拟地址的方式,实际值比4G要小。
按照默认设置,32位的机器上,4G的地址空间划分为user space和kernel space,user space占用了3G的虚拟地址空间,kernel space占用1G的虚拟地址空间。在kernel 1G的地址空间里,除了kernel的code、data等占用的空间之外,能够map出来使用的地址空间不到1G。kernel里,如果没有对应的虚拟地址,kernel是无法访问这段memory的,因此kernel实际能够访问的内存实际上是1G减掉kernel code等自己占用的部分,也就是不到1G。如果是大容量的内存,就会导致很多的物理地址不能map到kernel的地址空间,从而不能使用。后来CPU中添加了feature,添加了内存扩展特性,从而使得CPU可以访问到超过4G的内存。但是kernel中的逻辑地址仍然有这个限制,只能map优先的物理内存,超过这部分的memory称为high memory,而kernel map过能直接访问的memory就是low memory,也就是在kernel中直接就存在逻辑地址。这里对high、low memory又做了定义:
Low memroy
kernel中可以使用的物理内存,这部分内存在kernel中有逻辑地址相对应,被称为low memory。
High memory
这部分内存在kernel中不存在直接能够访问的逻辑地址,因为地址范围超过了kernel的虚拟地址空间反问,被称为high memory。
kernel中low memory和hight memory的分界线在1G memory以下的某个位置。
15.1.4. The Memory Map and Struct Page
因为历史原因,kernel一直使用逻辑地址来访问物理内存中的page,因此对于hight memory,这种访问就有问题了,因为high memory在kernel中没有能直接访问的逻辑地址。因此kernel中对page的访问越来越多的使用struct page这个结构体,这个结构体中的成员有:
atomic_t count;
page的reference counter,如果变成了0,就被放入free page list。
void *virtual;
如果这个page被map过,记录的就是map后得到的虚拟地址,否则就是NULL。low memory通常都是被map过的,high memory通常没有被map。这个成员在有些架构上没有,因为他们有更好的计算虚拟地址的方法。
unsigned long flags;
用于描述page的属性和状态,比如PG_locked,说明page在memory中已经被lock;PG_reserved,说明page已经被reserve,kernel的内存管理不应该再touch这个page。
kernel中使用struct page的数组来管理物理内存,有些系统上只有一个数组mem_map,有些架构的系统上,比如NUMA,因为有大量不连续的物理内存,那就可能需要多个page 数组来管理这些内存。幸运的是,driver只需要使用struct page即可,不需要关心这个page从哪里来的。kernel提供了一些函数,可以方便的根据page获取virtual address:
#include <linux/mm.h>
#include <linux/highmem.h>
#include <asm/kmap_types.h>
//根据逻辑地址获取对应的page结构体,如果kaddr是从vmalloc或者high memory过来的,这个不能用。
struct page *virt_to_page(void *kaddr);
//根据pfn,获取它对应的page 结构体。
struct page *pfn_to_page(int pfn);
//返回这个page对应的kernel虚拟地址。如果是high memory,需要事先map过才行。一般不用,而是用kmap。
void *page_address(struct page *page);
#include <linux/highmem.h>
//kmap返回这个page对应的虚拟地址,如果是low memory,直接就是逻辑地址
//如果是high memory并且没有map过,kernel就会给它在专用的space里做一次map,然后返回虚拟地址。
//kmap对同一个page有reference counter,所以要和kunmap配对调用。
void *kmap(struct page *page);
void kunmap(struct page *page);
//是kmap的高性能版本,有些架构上会reserver一些专用的slots(PTE),给atomic用。
//参数type用来表明需要哪个slot,driver能够使用的slot一般是KM_USER0和KM_USR1(如果是在user space的系统调用),以及KM_IRQ0和KM_IRQ1(interrupt handler)。
//调用atomic的driver code必须是atomic的,不能sleep。
void *kmap_atomic(struct page *page, enum km_type type);
void kunmap_atomic(void *addr, enum km_type type);
15.1.5. Page Tables
kernel中既然使用了虚拟地址,必然存在某种机制,可以通过虚拟地址得到物理地址,这个机制就是page table,page table也许是多级数据结构来实现,并且包含一些对应的flag。device driver的很多操作可能都会涉及page table,但是kernel已经做了封装,driver不需要和page table直接打交道,这里也不再赘述。
15.1.6. Virtual Memory Areas
virtual memory area (VMA)是kernel中的结构体,用于区分和管理进程地址空间中的不同区域。一个VMA中代表了一类有相同访问权限或者底层对应了同一个object的连续虚拟地址空间,大概类似于segment的概念。一个进程地址空间中通常包含以下几个部分:
1, 存储程序代码的区域。
2, 存放数据的区域,通常有多个,比如已经初始化的变量数据,还有未初始化的数据变量,以及程序的stack等。
3, 活动中的memory mapping的一个区域。
通过/proc/<pid/maps>可以看到这个进程虚拟地址空间的状态,读取maps,打印出来的东西有几个部分:
start-end perm offset major:minor inode image
看个例子:
# cat /proc/1/maps look at init
08048000-0804e000 r-xp 00000000 03:01 64652 /sbin/init text
0804e000-0804f000 rw-p 00006000 03:01 64652 /sbin/init data
0804f000-08053000 rwxp 00000000 00:00 0 zero-mapped BSS
40000000-40015000 r-xp 00000000 03:01 96278 /lib/ld-2.3.2.so text
40015000-40016000 rw-p 00014000 03:01 96278 /lib/ld-2.3.2.so data
40016000-40017000 rw-p 00000000 00:00 0 BSS for ld.so
42000000-4212e000 r-xp 00000000 03:01 80290 /lib/tls/libc-2.3.2.so text
4212e000-42131000 rw-p 0012e000 03:01 80290 /lib/tls/libc-2.3.2.so data
42131000-42133000 rw-p 00000000 00:00 0 BSS for libc
bffff000-c0000000 rwxp 00000000 00:00 0 Stack segment
ffffe000-fffff000 ---p 00000000 00:00 0 vsyscall page
# rsh wolf cat /proc/self/maps #### x86-64 (trimmed)
00400000-00405000 r-xp 00000000 03:01 1596291 /bin/cat text
00504000-00505000 rw-p 00004000 03:01 1596291 /bin/cat data
00505000-00526000 rwxp 00505000 00:00 0 bss
3252200000-3252214000 r-xp 00000000 03:01 1237890 /lib64/ld-2.3.3.so
3252300000-3252301000 r--p 00100000 03:01 1237890 /lib64/ld-2.3.3.so
3252301000-3252302000 rw-p 00101000 03:01 1237890 /lib64/ld-2.3.3.so
7fbfffe000-7fc0000000 rw-p 7fbfffe000 00:00 0 stack
ffffffffff600000-ffffffffffe00000 ---p 00000000 00:00 0 vsyscall
除了image,其他每一列在kernel的struct vm_area_struct中都有对应的成员变量(除了image name):
start
end
这个memory area的start和end虚拟地址。
perm
这个memory area对应的读写或者执行权限,表示进程针对虚拟地址对应的page所能做的操作。最后的字符要么是p,表示private,要么是s,表示shared。
offset
表示当前的VMA,在这个被map的file中的起始offset,offset为0意味着VMA的start对应了file的start。
major
minor
代表了device的major/minor number,这个device就是使用了这个被map的file的device。
inode
被map的文件对应的inode。
image
被map的文件的文件名,通常是可执行文件。
15.1.6.1 The vm_area_struct structure
当用户态进程调用了mmap去map device memory的时候,kernel就会给它创建一个新的vm_area_struct结构体。底层的device driver需要实现mmap,mmap的功能其实就是帮助kernel初始化这个VMA。
下面我们就看一下vm_area_struct这个结构体里的成员变量,其中有些可能会被device driver用到。要注意的是vm_area_struct里有一些成员变量kernel用来存储VMA的list或者树形结构,因此这个结构体不能在device driver中创建,而是由kernel自己创建。其中,比较重要的member有:
unsigned long vm_start;
unsigned long vm_end;
这个VMA的start和end,也就是/proc/pid/maps里看到的start和end。
struct file *vm_file;
如果area有关联的 file,指向它。
unsigned long vm_pgoff;
被map的file的offset,按照page来算的,不是byte。当file后者device memory被map的时候,这个就是第一个被map的page的位置。
unsigned long vm_flags;
用来描述这个vma的flags。最重要的两个flag是VM_IO和VM_RESERVED。VM_IO表示这个vma是用来做I/O的memroy map,在做core dump的时候会跳过这个vma。VM_RESERVED告诉kernel不要把vma swap出去,在大部分device的map中都会设置这个flag。
struct vm_operations_struct *vm_ops;
kernel用来操作这个vma一系列函数。说明vma在kernel中也是类似于struct file的object(对应一系列的callback,类似于面向对象)。
void *vm_private_data;
driver用来存储自己的私有数据。
下面是vm_operations_struct中的callback:
void (*open)(struct vm_area_struct *vma);
当这个vma有新的reference时(比如fork),kernel会调用实现这个VMA的subsystem的open callback,用来对vma做一些初始化。如果这个vma第一次create是通过mmap产生的,open就不会被调用,而是去调用driver实现的mmap。
void (*close)(struct vm_area_struct *vma);
当vma被destroy的时候,kernel会调用vma的close callback。要注意的是,vma本身没有记reference count,process只会调用一次open和close。
struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);
当进程试图访问一个在有效的vma里面,但是当前不在memory里的page时,kernel会调用nopage这个callback。如果这个page是被swap到了别的存储设备,nopage会把这个page再swap进来,并返回struct page的指针。如果nopage callback是NULL,kernel会分配一个空的page。
int (*populate)(struct vm_area_struct *vm, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);
当user 访问memory,kernel可以提前发生fault,driver一般不实现。(没太理解)
The Process Memory Map
kernel中的每一个进程,个别kernel thread除外,都会有一个结构体struct mm_struct,这个mm_struct中记录了很多和memory相关的数据结构,比如virtual memory area list,page tables,以及其他的一些bitmask,mmap的semaphore,page table的spinlock。这个mm_struct会记在task里面,因此在kernel中,可以通过current->mm来访问。这个mm_struct是可以share的,比如线程就会和别的线程共享进程地址空间。
15.1.7. The Process Memory Map
kernel 内存管理的最后一个是process memory map数据结构,这个数据结构把进程相关的其他数据结构都组合到一起,系统中的每个进程(除了kernel自己的一些helper thread)都有一个struct mm_struct数据结构,这个数据结构里记录了进程的vma,page tables,以及一些其他的bit mask,一个semaphore(mmap_sema),一个spinlock(page_table_lock)。mm_struct的指针就在task里面(也就current里,current->mm),driver可以通过current->mm来访问这个struct mm_struct,需要注意的是,mm_struct这个东西可以在多个process之间share,线程共享进程的虚拟地址空间就是通过这个做到的。
15.2. The mmap Device Operation
通过mmap,user space可以直接访问device的memory。书上以Xorg为例,获取它的maps:
cat /proc/731/maps
000a0000-000c0000 rwxs 000a0000 03:01 282652 /dev/mem
000f0000-00100000 r-xs 000f0000 03:01 282652 /dev/mem
00400000-005c0000 r-xp 00000000 03:01 1366927 /usr/X11R6/bin/Xorg
006bf000-006f7000 rw-p 001bf000 03:01 1366927 /usr/X11R6/bin/Xorg
2a95828000-2a958a8000 rw-s fcc00000 03:01 282652 /dev/mem
2a958a8000-2a9d8a8000 rw-s e8000000 03:01 282652 /dev/mem
...
有个/dev/mem,Xorg map了四次,第一个offset是0xa0000,其实是ISA DMA memory之后的一段,是在ISA hole中的一段标准的video RAM。后面的两个/dev/mem,offset非常大,地址已经超过了RAM的地址,这是从video adapter直接map出来的device memory。在/proc/iomem中也能看到这段memory:
000a0000-000bffff : Video RAM area
000c0000-000ccfff : Video ROM
000d1000-000d1fff : Adapter ROM
000f0000-000fffff : System ROM
d7f00000-f7efffff : PCI Bus #01
e8000000-efffffff : 0000:01:00.0
fc700000-fccfffff : PCI Bus #01
fcc00000-fcc0ffff : 0000:01:00.0
所谓的map device,其实就是将user space的一段virtual address对应到device的memory,当user space通过这个虚拟地址读写时,就是在读写device的memory。对于X这种类型的应用程序而言,直接能够访问video card的memory无疑可以获得较高的performance。
并不是所有的device都能支持mmap,比如串口设备或者其他流设备;另一个限制是mmap的memory size是以page为单位,kernel管理虚拟地址也是按照page为单位,这样的话mmap的内存大小就是page size的整数倍,而且对应物理地址的起始地址也应该是page size的整数倍。
这种限制对于driver来说并不是问题,因为底层硬件的工作方式对上层的driver来说是不透明的,也就说driver应该知道硬件的一些要求,比如page alignment等等。
mmap的好处,除了上面看到的Xorg这个例子,还有一个例子是PCI设备,PCI设备可以把自己的register space通过mmap的方式暴露给user space,从而在user space就可以直接操作PCI的寄存器,比ioctl的方式要方便和快捷。
mmap这个callback位于file_operations里面,当mmap系统调用发生的时候被kernel调用。但是drive要实现的这个mmap callback函数原型和user mode相比有较大不同,因为kernel在调用driver的mmap之前自己要做一些事情。
用户态的函数原型:mmap (caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset)
driver要实现的mmap:int (*mmap) (struct file *filp, struct vm_area_struct *vma);
单看这个driver实现的callback,filp就是被open的file pointer,vma就是kernel为这次mmap分配的虚拟地址空间,可见kernel在调用mmap之前已经做了很多事情,driver需要做的就是为这个虚拟地址创建的page table entry,并且在必要的时候更新新的vma->vm_ops。
kernel提供了两种更新page table的方式:remap_pfn_range,可以一次性做完一个range的page table;或者通过之前提到过的nopage这个callback一个一个去做。下面分别看这两种方式。
15.2.1. Using remap_pfn_range
使用remap_pfn_range,可以一次性把一个range的physical memory和虚拟地址绑定,driver可以使用两个接口中的一个:
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);
这两个函数都是成功返回0,出错返回error value。下面看一下他们的参数:
vma
和device的物理内存对应的vma,后续就把其中的虚拟地址和device的物理内存绑定。
virt_addr
用户态虚拟地址首地址,也就是remap后用户态虚拟地址的开始位置,范围是virt_addr到virt_addr+size。
pfn
要被remap的物理内存的page frame number,其实就是物理地址。这个值就存储在vma->vm_pgoff,所以driver调用这个函数的时候直接用这个值就好。被map出去的物理内存的范围是(pfn << PAGE_SHIFT)到(pfn << PAGE_SHIFT) + size。
size
被map的内存大小,以byte为单位。
prot
这个vma的权限保护位,记录在vma->vm_page_prot,driver调用这个函数时用这个值就好。
remap_pfn_range和io_remap_page_range这两个函数是类似的,他们需要使用的大部分参数都在vma里,直接拿来用就可以了。为什么有两个函数实现同样的功能?如果按照标准用法,remap_pfn_range适用于物理内存在system RAM上的场景;io_remap_page_range适用于物理内存在I/O region的场景;然而在绝大部分架构上,这两个函数做的事情是一样的,除了在SPARC架构上。如果需要更好的移植性,则应该按照标准的做法,根据自己的场景选择合适的接口。
这里还讨论了另外一个问题,就是cache,一般来说,访问device memory的时候CPU不会做cache,这些设置应该由BIOS在开机的时候处理好。不过仍然可以通过vma的权限保护位来控制这一行为,只是这种方式成否凑效是跟CPU的实现相关的。
15.2.2. A Simple Implementation
如果driver需要一个简单的,线性映射的device memory给用户态程序访问,这里有一个简单的例子:
static int simple_remap_mmap(struct file *filp, struct vm_area_struct *vma)
{
if (remap_pfn_range(vma, vma->vm_start, vm->vm_pgoff,
vma->vm_end - vma->vm_start,vma->vm_page_prot))
return -EAGAIN;
vma->vm_ops = &simple_remap_vm_ops;
simple_vma_open(vma);
return 0;
}
可以看到,这里实现的mmap callback直接调用了remap_pfn_range,用的参数都是直接从vma里取。
15.2.3. Adding VMA Operations
vma中记录了一个struct vm_operations_struct,这个里面存储的是针对这个vma可以做的操作,比如open/close等,通常driver只需要实现open/close即可。open这个callback被调用的时机是,当进程fork了一个子进程,或者这个VMA增加了一个新的reference counter;当VMA被close的时候,close函数会被调用。open和close是由kernel调用的,并且已经做了很多事情,driver之所以可以实现这个callback,是让driver有机会在open/close的时候做一些自己的事情。这里是一个简单的例子:
void simple_vma_open(struct vm_area_struct *vma)
{
printk(KERN_NOTICE "Simple VMA open, virt %lx, phys %lx\n",
vma->vm_start, vma->vm_pgoff << PAGE_SHIFT);
}
void simple_vma_close(struct vm_area_struct *vma)
{
printk(KERN_NOTICE "Simple VMA close.\n");
}
static struct vm_operations_struct simple_remap_vm_ops = {
.open = simple_vma_open,
.close = simple_vma_close,
};
vma->vm_ops = &simple_remap_vm_ops;
//显示调用open,触发open函数的调用。
simple_vma_open(vma);
上面的例子中,driver实现了自己的open和close vma ops,并设给了vma,然后主动调用了一次open来触发open callback调用。
15.2.4. Mapping Memory with nopage
通常remap_pfn_range能够满足driver的绝大部分需求,然而一小部分的需求还是无法满足,因此kernel提供了另外一种方式来map物理内存——nopage callback。
在mremap这个系统调用中就会用到nopage callback,user space在此之前已经map过了meomry,通过mremap可以增大或者减小map的size,如果是减小,kernel会自己把多出来的page处理掉,不会通知driver;如果是要增大map的size,那就需要通过nopage这个callback通知driver,让driver再多map一些page出来。因此,如果driver需要支持mremap,就必须实现nopage这个callback。原型:
struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);
当用户态程序访问了VMA中的一个page,但是这个page又没有在memory中时,driver的nopage函数就会被调用,参数address就是引起page fault的用户态虚拟地址,并且round down到page的整数倍address。nopage这个函数最终找到这个page,并且返回struct page指针,在返回之前,需要对这个page进行reference count加1操作,通过这个接口来完成:
get_page(struct page *pageptr);
当unmap的时候,再把这个page的ref减掉。kernel对page的管理需要使用它的reference counter,只要conter不为0,就说明有人再使用这个page,一旦reference counter变为0,就说没有人再使用这个page,它就会被kernel放到free list中去。当一个VMA被unmap时,它里面包含的所有page的reference couter都会被减1,如果driver之前没有处理好page的reference counter,这里就会出现问题。参数type如果不为NULL,记录的就是page fault的类型,对于device driver来说,这个值拿到的一直是VM_FAULT_MINOR。
看一个nopage的例子,当使用nopage的方式时,mmap要做的事情就很少了:
static int simple_nopage_mmap(struct file *filp, struct vm_area_struct *vma)
{
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
if (offset >= _ _pa(high_memory) || (filp->f_flags & O_SYNC))
vma->vm_flags |= VM_IO;
vma->vm_flags |= VM_RESERVED;
vma->vm_ops = &simple_nopage_vm_ops;
simple_vma_open(vma);
return 0;
}
mmap里做的主要事情就是设置了新的vm_ops,nopage callback会把对应的page map出来,然后返回page的指针:
struct page *simple_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type)
{
struct page *pageptr;
unsigned long offset = vma->vm_pgoff << PAGE_SHIFT;
unsigned long physaddr = address - vma->vm_start + offset;
unsigned long pageframe = physaddr >> PAGE_SHIFT;
if (!pfn_valid(pageframe))
return NOPAGE_SIGBUS;
pageptr = pfn_to_page(pageframe);
get_page(pageptr);
if (type)
*type = VM_FAULT_MINOR;
return pageptr;
}
这里simple_vma_nopage只是操作main memory,vma中已经记好了对应的physical address,也就是pfn,我们nopage这里只需要把pfn转换成对应的page结构体指针,然后加上page的引用计数,就可以return了。注意,这里需要对送过来的pfn做invalid检查——pfn_valid,如果超出了memory range,直接返回NOPAGE_SIGBUS。
如果因为某些原因(比如请求的地址超出了device的memory region),不能返回一个正常的page指针,就返回NOPAGE_SIGBUS,如果memory不够,也可以返回NOPAGE_OOM。
注意,上面的例子可以用于ISA memory region,但是不适用于PCI设备的memory region,PCI memory map的地址在system memory以上,在system memory map的地址里没有对应的entry,因此没有page结构体与他对应,这种情况下,必须使用remap_pfn_range。
如果driver没有实现nopage这个callback,kernel在remap的时候返回一个fill 0的page出去,而且是copy-on-write的page,也就说如果用户态只是读,就会发现读到的都是0;如果写,就会分配一个page出来,然后写进去,这种机制经常可执行程序中的BSS使用。
15.2.5. Remapping Specific I/O Regions
很多时候driver只是需要把外设的一部分memory range map出去,而不是整个device memory,可以通过指定offset的方式来实现。这里有一个例子,device memory的起始地址是simple_region_start(page align),要map的内存大小是simple_region_size。
unsigned long off = vma->vm_pgoff << PAGE_SHIFT;
unsigned long physical = simple_region_start + off;
unsigned long vsize = vma->vm_end - vma->vm_start;
unsigned long psize = simple_region_size - off;
if (vsize > psize)
return -EINVAL; /* spans too high */
remap_pfn_range(vma, vma_>vm_start, physical, vsize, vma->vm_page_prot);
除了计算offset,这个code还引入了一个check,如果发现user space想要map的内存大小超过了设备的物理内存,就返回EINVAL。在上面的code中,psize表示从指定的off开始,剩余的device memory大小,vsize是user space请求map的内存大小,如果user space请求的内存大小超过了device memory可用的,请求就会被拒绝。
要 注意的是,如果user space调用mremap来扩展它的map address space,就有可能导致它请求的size超过了device可用的,如果driver没有定义nopage函数,driver甚至不会知道这种事情的发生,user space的map会拿到一个zero的page,不过这种行为可能不是driver期望看到的。一个最简单的方式,是实现一个简单的nopage callback:
struct page *simple_nopage(struct vm_area_struct *vma,
unsigned long address, int *type);
{ return NOPAGE_SIGBUS; /* send a SIGBUS */}
nopage这个callback,只有在user space访问了有效VMA里的虚拟地址,并且kernel发现虚拟地址没有对应真正的page table entry才会被调用。如果我们之前调用了remap_pfn_range来map整个device memory,那就不可能发生这种情况,因此只需要简单的返回NOPAGE_SIGBUS就足够了,因为这种访问一定是不正常的访问。当然,正常的driver应该检查导致page fault的地址是否在device的region范围之内,如果是,应该做remap。再次强调,nopage这个callback不适用于PCI设备。
15.2.6. Remapping RAM
使用remap_pfn_range来map物理内存有一个限制——它只允许map被reserve的物理内存或者系统物理内存之外的内存,而从kernel直接取到的page不能被remap,比如通过get_free_page获取到的page就不允许通过remap_pfn_range map做remap,如果driver这么做了,结果就是user space的vma map到了一个zero的page,从user space看,读写都能工作,只是不是期望的行为。对于大多数device driver而言,不会碰到这个问题,因为device driver只会map自己device的memory,不会map system的RAM。不过kernel也可以remap system的RAM,但是只能使用nopage这种方式。
15.2.6.1 Remapping RAM with the nopage method
直接看例子就明白了,就是利用virt_to_page通过kmalloc获取到的逻辑地址拿到它对应的page结构体。
int scullp_mmap(struct file *filp, struct vm_area_struct *vma)
{
struct inode *inode = filp->f_dentry->d_inode;
/* refuse to map if order is not 0 */
if (scullp_devices[iminor(inode)].order)
return -ENODEV;
/* don't do anything here: "nopage" will fill the holes */
vma->vm_ops = &scullp_vm_ops;
vma->vm_flags |= VM_RESERVED;
vma->vm_private_data = filp->private_data;
scullp_vma_open(vma);
return 0;
}
void scullp_vma_open(struct vm_area_struct *vma)
{
struct scullp_dev *dev = vma->vm_private_data;
dev->vmas++;
}
void scullp_vma_close(struct vm_area_struct *vma)
{
struct scullp_dev *dev = vma->vm_private_data;
dev->vmas--;
}
//大部分工作都是nopage这个callback在做。
struct page *scullp_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type)
{
unsigned long offset;
struct scullp_dev *ptr, *dev = vma->vm_private_data;
struct page *page = NOPAGE_SIGBUS;
void *pageptr = NULL; /* default to "missing" */
down(&dev->sem);
offset = (address - vma->vm_start) + (vma->vm_pgoff << PAGE_SHIFT);
if (offset >= dev->size)
goto out; /* out of range */
/*
* Now retrieve the scullp device from the list,then the page.
* If the device has holes, the process receives a SIGBUS when
* accessing the hole.
*/
offset >>= PAGE_SHIFT; /* offset is a number of pages */
for (ptr = dev; ptr && offset >= dev->qset;) {
ptr = ptr->next;
offset -= dev->qset;
}
if (ptr && ptr->data)
pageptr = ptr->data[offset];
if (!pageptr)
goto out; /* hole or end-of-file */
page = virt_to_page(pageptr);
/* got it, now increment the count */
get_page(page);
if (type)
*type = VM_FAULT_MINOR;
out:
up(&dev->sem);
return page;
}
15.2.7. Remapping Kernel Virtual Addresses
通过vmalloc分出来的虚拟内存也可以remap给user 用,通过vmalloc_to_page来实现。
/*
* After scullv lookup, "page" is now the address of the page
* needed by the current process. Since it's a vmalloc address,
* turn it into a struct page.
*/
page = vmalloc_to_page(pageptr);
/* got it, now increment the count */
get_page(page);
if (type)
*type = VM_FAULT_MINOR;
out:
up(&dev->sem);
return page;
虽然可以把vmalloc拿到的虚拟地址remap给user space用,但是ioremap这个函数返回的虚拟地址不可以再remap给user space用,而应该直接使用remap_pfn_range。
15.3. Performing Direct I/O
系统中大部分的I/O操作都会有buffer,使用buffer的好处在于用户态程序可以会device分隔开,用户态程序读写内存不会被device block住,从而获得更好的性能。但是在某些情况下,直接操作device memory,不经过buffer可以获得更高的performance,尤其是传输的数据比较多比较大的时候。
有些情况下使用direct I/O,返回会适得其反,因为配置direct I/O,需要处理fault,以及user page,这部分工作需要开销,结果就是还不如使用buffer的I/O更好。比如说用户态程序再把自己的I/O buffer的内存通过direct I/O写到device memory之前,这个I/O buffer是不能再被使用的,因此这个写就必须是同步的操作,直到data全部写到device才会返回,每次写都会block用户态程序,performance也会变差。
按照LDD3的说法,char device driver一般没有必要实现direct I/O操作。
kernel提供的direct 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);
tsk
指向执行I/O的task,目的是告诉kernel,当发生了page fault时,是负责处理。这个参数一般设置为current。
mm
指向虚拟内存管理的数据结构,其中包含了当前操作需要用到的VMA。一般设置为current->mm。
start
len
start是user page的start address(page align),len是内存大小(page number)。
write
force
如果write不是0,说明需要往user page里写(对user space来说要做读操作),force是告诉get_user_page在操作的时候override掉指定page的保护位,driver一般不设置force。
pages
vmas
这两个是output参数,如果get_user_page操作成功,pages里就是获取到的struct page list,vmas指向关联的vma list,这两个都是至少len长度的两个数组类型,以容纳len个数据结构。
get_user_pages是底层内存管理接口,在调用get_user_pages之前,要求driver获取mm的seamphore,例子如下:
down_read(¤t->mm->mmap_sem);
result = get_user_pages(current, current->mm, ...);
up_read(¤t->mm->mmap_sem);
返回值是成功map的page的个数,有可能会被要求的小,但肯定不是0。函数返回以后,kernel就拿到了一个page array指针,这个指针指向了user space的buffer list,这个page数组指针不能在kernel中直接访问,需要先做kmap或者kmap_atomic,拿到虚拟地址以后才能访问。很多device对于direct IO操作,最后都是用来做DMA,device的DMA很多支持scatter/gather list,通过这个user space的buffer list可以很容易的组合成scatter/gather,然后做DMA使用,后面用到的时候再说。
在direct I/O完成以后,也就是driver使用完了这些user page,需要driver自己release。如果driver修改了这些page,在release之前要告诉kernel,kernel就会把修改的内容写回到memroy里面。怎么告诉kernel呢?调用SetPageDirty:
<linux/page-flags.h>
void SetPageDirty(struct page *page);
//driver调用:先check page是否在memory map的reserve 部分,如果不在才设为dirty。
if (! PageReserved(page))
SetPageDirty(page);
//无论user page是否被修改,在driver使用以后,必须调用这个release:
void page_cache_release(struct page *page);
15.3.1. Asynchronous I/O
device driver中用的不多,所以这里也不讲了。
15.4. Direct Memory Access
Direct memory access,也就是DMA,指的是外设不通过CPU,直接读写主内存。不经过CPU,可以省掉很多的延迟,从而显著提高performance。
15.4.1. Overview of a DMA Data Transfer
在讲编程细节之前,先看一下DMA是如何发生的,以DMA的read为例。
DMA的读有两个触发条件:上层的application需要从设备读取数据;也就是设备需要向memory中写数据,一般是从hardware来了数据等。在第一个场景下,DMA的执行过程为:
1. 当用户态进程调用了read时,driver首选分配一个DMA buffer,再通知hardware,让它把数据放入这个DMA buffer,与此同时,发起read的process被sleep,等待硬件写数据完成;
2. hardware向DMA buffer中写数据,完成以后,产生中断;
3. interrupt handler拿到了数据,识别并处理中断,唤醒sleep的process,告诉它数据准备好了。
第二个场景下,是异步DMA read的方式,没有用户态进程调用read,硬件产生的新数据只能放入driver自己的DMA buffer,等以后用户态进程调用了read时,再把这些数据送给用户态进程。DMA的执行过程为:
1. 硬件产生中断,通知有新的数据产生;
2. interrupt hanlder处理中断,分配一个DMA buffer,告诉硬件往DMA buffer里写数据;
3. 硬件把数据写入指定的DMA buffer,写完以后产生中断;
4. interrupt handler 分发新的数据,并唤醒任何可能等待的process。
这种异步的DMA在网卡驱动中较为常见,driver内部使用circular buffer(DMA ring buffer)来管理,每次有新的数据到达,就会放到DMA ring buffer里,然后执行interrupt handler,把数据给kernel之后然后就会添加一个新的DMA ring buffer。
以上讲的DMA都是中断驱动,虽然也可以通过polling的方式做DMA,但是这种同步的DMA方式可能显著降低performance,因此not make sense。DMA buffer是一些特殊的buffer,专门用来做DMA,许多device driver在初始化的时候把DMA buffer分配出来使用,直到shutdown才会释放。
15.4.2. Allocating the DMA Buffer
使用DMA buffer有一个问题要注意,如果buffer的大小超过一个page,那么这些pages必须是连续的物理内存,因为ISA或者PCI总线使用的是物理内存。另一个要注意的问题是DMA buffer的分配,可以在bootup的时候,也可以在runtime的时候,对于module driver来说,只能是runtime的时候分DMA buffer,分配buffer的时候要从合适的zone里分配,因为不是所有zone里的物理内存都可以做DMA,比如high memory可能在某些架构或者外设上做DMA使用。
现代绝大多数外设都支持32bit寻址,因此从kernel分配的normal memory都是可以做DMA用的,不过某些PCI设备可能没有完全按照PCI的标准来做,所以不能支持32bit寻址,要看具体的设备;像ISA设备,也只能支持24bit寻址。对于有这种限制的外设,在使用kmalloc或者get_free_pages来分配memory时,应当在gfp mask中设置GFP_DMA,这样kernel就会从外设能够寻址的24bit地址空间内分配内存。不过,你也可以使用kernel提供的DMA layer来workaround这个地址空间受限的问题。
15.4.2.1 Do-it-yourself allocation
driver可以从kernel的memory management中分配内存,也可以通过reserve的方式来分配。随着kernel的运行,内存的碎片会增多,如果module driver分配大量连续内存,就存在失败的可能。为了避免这个问题,driver可以在系统bootup的时候reserve memory出来,通过在启动参数中设置mem=255M,可以把从255M开始的物理内存reserve出来,driver如果需要使用就map出来:
dmabuf = ioremap (0xFF00000 /* 255M */, 0x100000 /* 1M */);
总之,分配大量连续的物理内存做DMA,并不是一个好主意,如果有这个需求,考虑一下device是否支持scatter/gather,如果是,应该分配离散的page,然后组合到一起作为外设的DMA buffer,使用scatter/gather是最好的方式。
15.4.3. Bus Addresses
使用DMA的device driver,driver内部使用虚拟地址,而外设则直接使用物理地址。然而这种描述并不精确,外设使用的地址是总线地址,并不是物理地址,尽管ISA和PCI使用的总线地址和物理地址相同,但是在某些平台并不是这样。有些总线接口是通过bridge circuitry相连接,I/O地址会被map到不同的物理地址,而有些系统上可以把离散的page map成连续的地址给外设bus。
为了解决上述的问题,kernel在最底层提供了一些具有移植性的函数,不过这些函数不推荐使用,因为只在某些具有简单I/O架构的系统上可以使用:
unsigned long virt_to_bus(volatile void *address);
void *bus_to_virt(unsigned long address);
上面的函数只是在kernel的逻辑地址和总线地址之间做了转换,如果中间涉及到IOMMU或者bounce buffer,这种方式就无法工作,正确的方式是使用kernel提供的DMA layer。
15.4.4. The Generic DMA Layer
笼统的说,DMA就是分配buffer,把buffer的bus地址告诉外设,外设通过buffer的bus地址读写RAM。虽然逻辑看上去很简单,但是要想写出正确,安全访问DMA的driver有很大难度:不同的系统对cache一致性的处理方式不同,driver如果不考虑cache,就会导致memory corrupt;不同的硬件对DMA的支持方式不一样的,有的简单一些,有的非常复杂;不是所有的系统都能够在所有的memory上做DMA。比较幸运的是,kernel提供了DMA layer,DMA layer与架构和总线都保持独立,可以帮助driver实现DMA。这些DMA layer的接口要使用struct device,这个struct device就是kernel的设备模型中,最底层的device表示,driver可以在bus相关的device中找到这个底层的device,比如在pci_device中或者usb_device中的dev成员,这个device就是DMA layer需要使用的。
15.4.4.1 Dealing with difficult hardware
再使用DMA之前,要做的第一步就要确认外设能支持多少bit的地址寻址,这关系到如何使用DMA,如果使用的DMA buffer地址超过了外设能够寻址的范围,那就无法做DMA。默认情况下,kernel认为外设都可以在32bit地址空间内做DMA,如果外设因为某些限制不能访问全部的32bit地址空间,driver需要告诉kernel外设的访问能力:
int dma_set_mask(struct device *dev, u64 mask);
如果外设支持24bit地址的DMA,mask就设置为0xffffff。如果返回非零值,说明可以在24bit地址内做DMA,否则就不可以,例子:
if (dma_set_mask (dev, 0xffffff))
card->use_dma = 1;
else {
card->use_dma = 0;
/* We'll have to live without DMA */
printk (KERN_WARN, "mydev: DMA not supported\n");
}
因为kernel默认外设支持32bit寻址,如果device支持32bit,就没必要调用这个函数。
15.4.4.2 DMA mappings
DMA mapping,就是分配一个DMA buffer并且生成一个外设可以访问的地址。虽然可以通过virt_to_bus获取buffer的地址,但是不推荐这样做,第一个原因是很多的硬件平台有IOMMU,针对bus存在map register集合,IOMMU可以把离散的page变成连续的bus 地址,这样外设可以访问scatter,virt_to_bus在有IOMMU的这种情况下就不能使用。
并不是所有的架构上都有IOMMU,很x86上就没有(现在有了),写得好的driver不需要关心底层硬件中是否存在IOMMU设备。
由于kernel无法直接访问high memory,也没有high memory的page,所以如果通过high memory做DMA,需要借助bounce buffer,bounce buffer是device能够直接访问的memory,通过bounce buffer的中转(copy to/from),device可以间接实现high memory的DMA。使用bounce buffer无疑会导致performance降低,但是这也没得选。
DMA中另外一个重要的问题是cache coherency,因为做DMA的memory,CPU和Device都有可能去访问,就要做好cache一致性。DMA layer中引入了一个新的变量类型——dma_addr_t,用来表示做DMA的bus address,dma_addr_t唯一会被使用到的地方就是把它传递给其他的DMA layer routine,以及给外设,CPU如果直接访问这个bus address会有不可预料的错误。
在PCI core中区分了两类DMA mapping,依据是DMA buffer生命周期的长短:
1. Coherent DMA mappings
这种DMA map得到的buffer生命周期比较长,一般和driver自己的生命周期保持一致,coherent DMA buffer可以同时被CPU和device访问,而且会在coherent 的memory里面。coherent DMA buffer在kernel中比较少。
2. Streaming DMA mappings
这种方式得到的DMA buffer一般只是给单个操作使用,因此生命周期比较短。这个DMA buffer同一时间只能给一个人用,比如CPU或者外设。kernel中推荐优先使用streaming map的DMA buffer,原因有两个:
a,IOMMU针对bus有mapping register,每一个DMA map都会占用其中的一个或几个register。coherent dma mapping的生命周期很长,因此占用的map register就很久,即便可能没有用到。
b,很多架构对streaming dma可以做优化,coherent就很难优化。
这两个map的接口也是不同的。
15.4.4.3 Setting up coherent DMA mappings
先看coherent的,通过调用dma_alloc_coherent可以获得一个conherent map的DMA buffer:
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, int flag);
void dma_free_coherent(struct device *dev, size_t size,
void *vaddr, dma_addr_t dma_handle);
dma_alloc_coherent函数所做到事情有两个:分配dma buffer,分配DMA bus address。前两个参数是底层device,和要分配的DMA buffer的大小。返回值有两个,函数返回值void *是dma buffer的虚拟地址,可以被driver直接使用,参数里的dma_handle就是buffer的dma_addr_t,也就是dma bus address,flag就是gfp的flag,因为需要分配page,所以需要指定gfp mask,如果是普通的分配就是GFP_KERNEL,如果是atomic context,就设置为GFP_ATOMIC。当dma buffer不再使用的时候调用dma_free_coherent。
15.4.4.4 DMA pools
DMA pool可以用来分配size比较小的coherent DMA buffer。通过dma_alloc_coherent分配的dma buffer都是一个page以上,如果只需要size比较小的memory做DMA,可以使用DMA pool。
和之前的kmem cache用法类似,在使用DMA pool之前要创建DMA pool:
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 addr);
dma_pool_create里,name是DMA pool的名字,方便debug;dev是底层device;size是从DMA pool中分配的DMA buffer的size;align是从DMA pool中分配的DMA buffer需要做的hardware align;allocation代表了不能跨越的boundary,如果不是0,就是限制分配的DMA buffer不要跨越allocation这个boundary,比如allocation是4096,就是说从DMA pool中分配的DMA buffer不能跨4096.
如果不再要DMA pool,就调用dma_pool_destroy把它destroy掉。在调用dma_pool_destroy之前,要保证DMA buffer都已经释放掉。
从DMA pool中分配DMA buffer,要调用dma_pool_alloc这个接口。pool代表之前dma_pool_create拿到的DMA pool;mem_flags是gfp mask;如果函数成功执行,就会返回固定大小的DMA buffer,大小就是dma_pool_create的时候指定的size。
如果DMA buffer不再使用,就调用dma_pool_free把它释放掉。
15.4.4.5 Setting up streaming DMA mappings
和coherent DMA mapping相比,streaming DMA mapping接口更加复杂,原因是多方便的:streaming map支持对已经分配的page进行DMA map,而且要处理没有被选中的address,在某些架构上,还支持scatter/gather。
在使用streaming DMA map之前,要告诉kernel这个DMA buffer的数据流向(enum dma_data_direction):
DMA_TO_DEVICE 写到device里面去(来自user space的write操作)
DMA_FROM_DEVICE 从device里面读出来(来自user space的read操作)
DMA_BIDIRECTIONAL 读写同时有
DMA_NONE debug用,如果操作了NONE的DMA,kernel会报panic
虽然可以一直设置DMA_BIDIRECTIONAL,但是performance不如正确的设置direction。
如果只是map/unmap单个buffer,可以使用接口:
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 dma_addr, size_t size,
enum dma_data_direction direction);
dma_map_single中,返回值是bus address,如果出错,返回0. 如果DMA做完了,就要调用dma_unmap_single,其中的size和direction必须和dma_map_single使用的一模一样。
有一些原则需要注意:
1. DMA buffer一定按照当时map用的direction来使用
2. 一旦map成功,这个buffer就归device所有,CPU不能再使用,只有做了unmap之后,DMA buffer才又归CPU所有,可以操作。
3. 如果DMA buffer正在做DMA,一定不能unmap。
关于2,可能会比较奇怪为什么DMA buffer在同一时间只能被device或者CPU访问,原因主要有两个:
a,如果需要map dma buffer,kernel要保证DMA buffer对应的CPU cache全部写到了memory,因此此时可能会有一次CPU cache的flush,driver在flush之后写到buffer的内容就没机会flush到memory里面去了。
b,第二情况和第一种有些类似。有些memory不能直接用来做DMA,某些架构上会通过bounce buffer的方式间接实现DMA的访问,比如当前是TO_DEVICE的,那么bounce buffer的操作会在map调用的时候把original buffer里的内容copy到bounce buffer中去,如果map了以后,driver还在操作original buffer,那么这部分操作是没有同步到bounce buffer中去的。考虑到bounce buffer的存在,就知道为什么不要随便设置DMA 的方向为同时读写了,因此会有两次bounce bufffer的copy。
如果driver(CPU)需要在不unmap的情况下使用DMA buffer,可以通过这个接口:
//首先通过这个接口获取DMA buffer给CPU用(在CPU开始使用之前)
void dma_sync_single_for_cpu(struct device *dev, dma_handle_t bus_addr,
size_t size, enum dma_data_direction direction);
//用完之后把DMA buffer返还给device(在device开始使用之前),一旦调用,CPU就不能再访问DMA buffer了
void dma_sync_single_for_device(struct device *dev, dma_handle_t bus_addr,
size_t size, enum dma_data_direction direction);
15.4.4.6 Single-page streaming mappings
如果你已经有了一个page指针,想把它用来做DMA,可以通过下面的接口:
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);
offset/size可以用来控制只对page中的一部分memory做map,但是不推荐这么做,因为有可能导致无法对齐cache line。
15.4.4.7 Scatter/gather mappings
scatter/gather是特殊类型的streaming DMA mapping,针对同时要map多个bufffer的情况。当然你可以采用每次map一个buffer的方式,但是kernel提供了更方便的办法。很多device都支持通过scatter list的方式一次DMA读写多个buffer,好处之一就是可以利用bus的DMA map registers,这样可以使得不连续的物理内存在外设看起来是连续的。要注意的是,除了scatter list的第一个和最后一个entry,其他entry的size需要和page size长度一样,否则不能使用scatter。(why?)
如果需要使用bounce buffer,就会把scatter list copy到一个大的buffer上去,因为反正要做copy。。
使用scatter/gather list的第一步就是创建scatter/gather的数组,并且填充必要的成员。scatter数据结构是架构相关的,但是都会包含如下三个成员:
struct page *page; 指向scatter/gather用到的buffer
unsigned int length; //buffer的length
unsigned int offset; //在page中的offset
driver需要填充scatterlist结构体里的这三个成员,然后调用:
int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents,
enum dma_data_direction direction)
nents表示scatter数组的entry个数,返回值是map成功的DMA buffer的个数,可能小于nents。dma_map_sg函数中,会对scatterlist中的每一个page计算它的bus address,同时会合并相邻的page。如果当前的机器上有IOMMU,dma_map_sg也会设置IOMMU的mapping register,结果就是获得了一个在外设看起来连续的内存区域。
因为map成功以后,每一个buffer的地址和length可能都会发生变化,因此driver需要读取scatterlist中每一个buffer的bus地址和length,这些都记录在scatterlist entry里面,但是在结构体里的位置是不同的,为了移植性,kernel提供了接口:
//返回scatterlist entry的bus地址
dma_addr_t sg_dma_address(struct scatterlist *sg);
//返回scatterlist entry的length
unsigned int sg_dma_len(struct scatterlist *sg);
在map完成以后,需要unmap:
void dma_unmap_sg(struct device *dev, struct scatterlist *list, int nents, enum dma_data_direction direction);
unmap的时候这个nents是map的时候传递给dma_map_sg的nents,不是map返回给你的值。
同样的,如果在map以后,CPU需要访问,就需要通过如下的接口先获取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);
15.4.4.8 PCI double-address cycle mappings
通常情况下,DMA layer都是support 32bit地址寻址,不过也支持64bit寻址——double-address cycle (DAC),普通的DMA layer并不支持DAC。如果某个外设支持非常大的high memory DMA,那就使用DAC,不过DAC只适用于PCI bus,别的不适用。
如果driver要支持DAC,首先应当设置这个mask:
int pci_dac_set_dma_mask(struct pci_dev *pdev, u64 mask);
如果函数返回0,说明device driver可以使用DAC。通过下面的接口来创建mapping:
dma64_addr_t pci_dac_page_to_dma(struct pci_dev *pdev, struct page *page,
unsigned long offset, int direction);
可以看到map的时候,buffer用struct page指针表示,因此DAC的map每次只能是一个page,而且必须是high memory或者没有被任何人使用的memory(不知道是不是这个意思);direction和DMA layer类似:PCI_DMA_TODEVICE, PCI_DMA_FROMDEVICE, 和 PCI_DMA_BIDIRECTIONAL。
pci_dac_page_to_dma没有占用external resource,使用完以后不需要释放。不过仍然遵守owner只有一个的原则,所以CPU访问前后要处理所有权的问题:
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);
15.4.4.9 A simple PCI DMA example
这里就是一个简单的PCI DMA的例子:
int dad_transfer(struct dad_dev *dev, int write, void *buffer,
size_t count)
{
dma_addr_t bus_addr;
/* Map the buffer for 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;
/* Set up the device */
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));
/* Start the operation */
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;
/* Make sure it's really our device interrupting */
/* Unmap the DMA buffer */
dma_unmap_single(dev->pci_dev->dev, dev->dma_addr,
dev->dma_size, dev->dma_dir);
/* Only now is it safe to access the buffer, copy to user, etc. */
...
}
15.4.5. DMA for ISA Devices
略过
浙公网安备 33010602011771号