本章内容分为三个部分:
- 第一部分讲述了mmap系统调用的实现过程。将设备内存直接映射到用户进程的地址空间,尽管不是所有设备都需要,但是能显著的提高设备性能。
- 如何跨越边界直接访问用户空间的内存页,一些相关的驱动程序需要这种能力。在很多情况下,内核执行了该种映射,而无需驱动程序的参与。
- 直接内存访问(DMA)I/O操作,它使得外设具有直接访问系统内存的能力。
一、Linux的内存管理
关注Linux内存管理实现的主要特性,而非讲述操作系统中内存管理的理论。
1.1 地址类型
Linux是一个虚拟内存系统,意味着用户程序所使用的地址与硬件使用的物理地址是不同的。
虚拟内存是一个简介层,系统中运行的程序可以分配比物理内存更多的内存。甚至单独进程都拥有比系统物理内存更多的虚拟地址空间。
在任何情况下使用何种类型的地址,内核代码并未明确加以区分,因此程序对此要仔细处理。
- 用户虚拟地址:这是在用户空间程序所能看到的常规地址。用户地址或者32位的,或者是64位的
- 物理地址:该地址在处理器和系统内存之家使用。
- 总线地址:该地址在外围总线和内存之间使用。通常他们与处理器使用的物理地址相同,但这么做并不是必须的。一些计算机提供I/O内存管理(MMU),实现总线和主内存之间的重新映射。
- 但使用DMA时,MMU变成了一个额外的操作。
- 内核逻辑地址:内核逻辑地址组成了内核的常规地址空间。kmalloc返回的就是内核逻辑地址
- 内核虚拟地址:内核虚拟地址和内核逻辑地址,都将内核空间的地址映射到物理地址上。内核虚拟地址与物理地址的映射不是一一对应的。
如果有一个逻辑地址,宏__pa()(在<asm/page.h>中定义)返回其对应的物理地址,
使用宏__va()也能将物理地址逆向映射到逻辑地址,但这只对低端内存页有效。
1.2 物理地址和页
物理地址被分散成离散的单元,称之为页。系统对内存的操作都是基于单个页的。
每个页的大小随体系架构的不同而不同,大多数系统使用4095个字节。常量PAGE_SIZE(在<asm/page.h>中定义)给出了在任何指定体系架构下的大小。
1.3 高端与低端内存
大量的32位系统中,系统的寻址空间不能大于4GB。内核在(x86中)将4GB的虚拟地址空间分割成用户空间和内核空间。
典型的分配是1GB内核空间,3GB的用户空间。内核对任何内存的访问,都需要映射至虚拟地址空间内核部分的大小,再减去内核代码自身所占用的空间。
低端内存:在于内核空间上的逻辑地址内存。
高端内存:那些不存在逻辑地址的内存,它们处于内核虚拟地址之上。
1.4 内存映射和页结构
内核使用逻辑地址来引用物理内存中的页。支持高端内存后,在高端内存中无法使用逻辑地址。内核处理内存的函数趋向于使用指向page结构的指针(在<linux/mm.h>中)。
/* * Each physical page in the system has a struct page associated with * it to keep track of whatever it is we are using the page for at the * moment. Note that we have no way to track which tasks are using * a page, though if it is a pagecache page, rmap structures can tell us * who is mapping it. */ struct page { unsigned long flags; /* Atomic flags, some possibly * updated asynchronously */ atomic_t _count; /* Usage count, see below. */ union { /* * Count of ptes mapped in * mms, to show when page is * mapped & limit reverse map * searches. * * Used also for tail pages * refcounting instead of * _count. Tail pages cannot * be mapped and keeping the * tail page _count zero at * all times guarantees * get_page_unless_zero() will * never succeed on tail * pages. */ atomic_t _mapcount; struct { /* SLUB */ u16 inuse; u16 objects; }; }; union { struct { unsigned long private; /* Mapping-private opaque data: * usually used for buffer_heads * if PagePrivate set; used for * swp_entry_t if PageSwapCache; * indicates order in the buddy * system if PG_buddy is set. */ struct address_space *mapping; /* If low bit clear, points to * inode address_space, or NULL. * If page mapped as anonymous * memory, low bit is set, and * it points to anon_vma object: * see PAGE_MAPPING_ANON below. */ }; #if USE_SPLIT_PTLOCKS spinlock_t ptl; #endif struct kmem_cache *slab; /* SLUB: Pointer to slab */ struct page *first_page; /* Compound tail pages */ }; union { pgoff_t index; /* Our offset within mapping. */ void *freelist; /* SLUB: freelist req. slab lock */ }; struct list_head lru; /* Pageout list, eg. active_list * protected by zone->lru_lock ! */ /* * On machines where all RAM is mapped into kernel address space, * we can simply calculate the virtual address. On machines with * highmem some memory is mapped into kernel virtual memory * dynamically, so we need a place to store that address. * Note that this field could be 16 bits on x86 ... ;) * * Architectures with slow multiplication can define * WANT_PAGE_VIRTUAL in asm/page.h */ #if defined(WANT_PAGE_VIRTUAL) void *virtual; /* Kernel virtual address (NULL if not kmapped, ie. highmem) */ #endif /* WANT_PAGE_VIRTUAL */ #ifdef CONFIG_WANT_PAGE_DEBUG_FLAGS unsigned long debug_flags; /* Use atomic bitops on this */ #endif #ifdef CONFIG_KMEMCHECK /* * kmemcheck wants to track the status of each byte in a page; this * is a pointer to such a status block. NULL if not tracked. */ void *shadow; #endif };
atomic_t count; 对该页的访问计数,当计数值为0时,该页将返回给空闲链表
void *virtual; 如果页面被映射,指向内核虚拟地址。未被映射则是NULL
unsigned long flags; 描述页状态的一系列标志。PG_locked表示内存中的页已经被锁住,而PG_reserved表示禁止内存管理系统访问该页
有一些函数和宏用来在page结构指针与虚拟地址之间进行转换。
struct page *virt_to_page(void *kaddr); 在<asm/page.h>中定义,负责将内核逻辑地址转换为相应的page结构体指针。 犹豫需要一个逻辑地址,因此不能操作vmalloc生成的地址以及高端内存。 struct page *pfn_to_page(int pfn); 针对给定的页帧号,返回page结构指针。使用pfn_valid确认页帧号的合理性 void *page_address(struct page *page); 如果地址存在的话,返回页的内核虚拟地址。对于高端内存来说,只有当内存页被映射后该地址才存在。
该函数定义在<linux/mm.h>中。大多数情况下,使用kmap而不是page_address
kmap相关的函数:
#include <linux/highmem.h> void *kmap(struct page *page); void kunmap(struct page *page); kmap为系统中的页返回内核虚拟地址。对于低端内存页来说,它只返回页的逻辑地址; 对于高端内存,kmap在抓弄的内核地址空间创建特殊的映射。 kmap的映射数量是有限的,不能映射过长的时间。
kmap_atomic相关函数:
#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的高性能版本,以原子的处理 type参数:对驱动程序有意义的只有KM_USER0和KM_USER1。(KM_IRQ0和KM_IRQ1中断)
1.5 页表
处理器必须使用某种机制,将虚拟地址转换为相应的物理地址,这种机制被称为页表。
1.6 虚拟内存区
用于管理进程地址空间中不同区域的内核数据结构。
进程内存映射(至少)包含下面这些区域:
- 程序的可执行代码(通常称为text)区域
- 多个数据区,其中包含初始化数据、非初始化数据(BSS)以及程序堆栈。
- 与每个活动的内存映射对应的区域
查看/proc/<pid>/maps (其中pid要替换为具体的进程ID)文件就能了解进程的内存区域
/proc/self 是一个特殊的文件,始终指向当前进程。
cat /proc/<pid>/maps的结果与vm_area_struct结构中的一个成员相对应。
start
end 该内存区域的起始处和结束处的虚拟地址
perm 内存区域的读、写和执行权限的位掩码。
offset 表示内存区域在映射文件中的起始位置。
major
minor 拥有映射文件的设备的主设备号和次设备号
inode 被映射文件的索引节点号
image 被映射额文件(通常是一个可执行映象)的名称
1.7 vm_area_struct结构
vma的主要成员如下所示,在头文件<linux/mm.h>中定义:
/* * This struct defines a memory VMM memory area. There is one of these * per VM-area/task. A VM area is any part of the process virtual memory * space that has a special rule for the page-fault handlers (ie a shared * library, the executable area etc). */ struct vm_area_struct { struct mm_struct * vm_mm; /* The address space we belong to. */ unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_end; /* The first byte after our end address within vm_mm. */ /* linked list of VM areas per task, sorted by address */ struct vm_area_struct *vm_next, *vm_prev; pgprot_t vm_page_prot; /* Access permissions of this VMA. */ unsigned long vm_flags; /* Flags, see mm.h. */ struct rb_node vm_rb; /* * For areas with an address space and backing store, * linkage into the address_space->i_mmap prio tree, or * linkage to the list of like vmas hanging off its node, or * linkage of vma in the address_space->i_mmap_nonlinear list. */ union { struct { struct list_head list; void *parent; /* aligns with prio_tree_node parent */ struct vm_area_struct *head; } vm_set; struct raw_prio_tree_node prio_tree_node; } shared; /* * A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma * list, after a COW of one of the file pages. A MAP_SHARED vma * can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack * or brk vma (with NULL file) can only be in an anon_vma list. */ struct list_head anon_vma_chain; /* Serialized by mmap_sem & * page_table_lock */ struct anon_vma *anon_vma; /* Serialized by page_table_lock */ /* Function pointers to deal with this struct. */ const struct vm_operations_struct *vm_ops; /* Information about our backing store: */ unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE units, *not* PAGE_CACHE_SIZE */ struct file * vm_file; /* File we map to (can be NULL). */ void * vm_private_data; /* was vm_pte (shared mem) */ #ifndef CONFIG_MMU struct vm_region *vm_region; /* NOMMU mapping region */ #endif #ifdef CONFIG_NUMA struct mempolicy *vm_policy; /* NUMA policy for the VMA */ #endif };
unsigned long vm_start;
unsigned long vm_end; 该VMA所覆盖的虚拟地址范围。这是/proc/*/maps中最前面的两个成员
struct file *vm_file; 指向与该区域(如果存在的话)相关联的file结构指针
unsigned long vm_pgoff; 以页为单位,文件中该区域的偏移量。当映射一个文件或者设备时,它是该区域中被映射的第一页在文件中的位置
unsigned long vm_flags; 描述该区域的一套标志。驱动程序相关的是VM_IO和VM_RESERVED
struct vm_operations_struct *vm_ops; 内核能调用的一套函数,用来对该内存区进行操作。
void *vm_private_data; 驱动程序用来保存自身信息的成员
/* * These are the virtual MM functions - opening of an area, closing and * unmapping it (needed to keep files on disk up-to-date etc), pointer * to the functions called when a no-page or a wp-page exception occurs. */ struct vm_operations_struct { void (*open)(struct vm_area_struct * area); void (*close)(struct vm_area_struct * area); int (*fault)(struct vm_area_struct *vma, struct vm_fault *vmf); /* notification that a previously read-only page is about to become * writable, if an error is returned it will cause a SIGBUS */ int (*page_mkwrite)(struct vm_area_struct *vma, struct vm_fault *vmf); /* called by access_process_vm when get_user_pages() fails, typically * for use by special VMAs that can switch between memory and hardware */ int (*access)(struct vm_area_struct *vma, unsigned long addr, void *buf, int len, int write); #ifdef CONFIG_NUMA /* * set_policy() op must add a reference to any non-NULL @new mempolicy * to hold the policy upon return. Caller should pass NULL @new to * remove a policy and fall back to surrounding context--i.e. do not * install a MPOL_DEFAULT policy, nor the task or system default * mempolicy. */ int (*set_policy)(struct vm_area_struct *vma, struct mempolicy *new); /* * get_policy() op must add reference [mpol_get()] to any policy at * (vma,addr) marked as MPOL_SHARED. The shared policy infrastructure * in mm/mempolicy.c will do this automatically. * get_policy() must NOT add a ref if the policy at (vma,addr) is not * marked as MPOL_SHARED. vma policies are protected by the mmap_sem. * If no [shared/vma] mempolicy exists at the addr, get_policy() op * must return NULL--i.e., do not "fallback" to task or system default * policy. */ struct mempolicy *(*get_policy)(struct vm_area_struct *vma, unsigned long addr); int (*migrate)(struct vm_area_struct *vma, const nodemask_t *from, const nodemask_t *to, unsigned long flags); #endif };
1.8 内存映射处理
每个进程都拥有一个struct mm_struct 结构(在<linux/sched.h>中定义) ,其中包括虚拟内存区域链表、页表以及其他大量内存管理信息。
还包括信号灯(mmap_sem)和一个自旋锁(page_table_lock)。多数驱动要访问时,使用current->mm
二、mmap设备操作
mmap方法是file_operations结构的一部分,并且执行mmap系统调用时将调用该方法。
这个方法和系统调用的mmap有很大的不同,原型如下:
mmap(caddr_t addr, size_t len, int port, int flags, int fd, off_t offset); int (*mmap)(struct file *filp, struct vm_area_struct *vma); vma包含了用于访问设备的虚拟地址的信息
有两种建立页表的方法:
- 使用remap_pfn_range函数一次全部建立
- 通过nopage VMA方法每次建立一个页表
2.1 使用remap_pfn_range
int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t port); 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:重新映射时的起始用户虚拟地址。 pfn:与物理内存对应的页帧号,虚拟内存将要被映射到该物理内存上。 size:以字节为单位,被重新映射的区域大小 prot:新VMA要求的保护属性
2.2 一个简单的实现
如果驱动程序要将设备内存线性地映射到用户地址空间中,程序员基本上就只需要调用remap_pfn_range函数。例子:
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; }
2.3 为VMA添加操作
vma_area_struct 包含了一系列针对VMA的操作,当fork进程或者创建一个新的对VMA引用时,随时都会调用open函数。
void simple_vma_open(struct vm_area_struct *vma) { printk(KERN_NOTICE "Simple VMA open, virt %lx, phys %lx ", vma->vm_start, vma->vm_pgoff << PAGE_SHIFT); } void simple_vma_close(struct vm_area_struct *vma) { printk(KERN_NOTICE "Simple VMA close. "); } static struct vm_operations_struct simple_remap_vm_ops = { .open = simple_vma_open, .close = simple_vma_close, };
使用nopage映射内存
尽管remap_page_range在许多情况下工作良好,但是并不能适应大多数的情况。
如果VMA映射尺寸变小或变大时,使用nopage更适合,不需要做额外的操作。
struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type); 当用户要访问VMA中的页,而该页又不再内存中时,将调用相关的nopage函数 address:包含了引起错误的虚拟地址,被向下圆整到页的开始位置。 nopage:函数必须定位并返回指向用户所需要页的page结构指针。 get_page(struct page *pageptr); 该函数调用get_page宏,用来增加返回的内存页的使用计数
如果使用了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函数小心的每次重新映射一页,并且返回它的page结构指针。
重映射的步骤非常简单:需要的地址定位并返回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; }
只是简单的映射了主内存,并增加了计数引用。需要的步骤是:
计算物理地址,然后通过右移PAGE_SHIFT位,将它转换成页帧号。
pfn_valid确保地址的合理性,超过范围返回NOPAGE_SIGBUS
重映射特定的I/O区域
所有例子都是对/dev/mem的再次实现,一个典型的驱动程序只映射与其外围设备相关的一小段地址,而不是映射全部地址。
下面代码揭示了驱动程序如何对起始于物理地址simple_region_start、大小为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 = simpel_region_size - off; if(vsize > psize) return -EINVAL; /* 跨度过大 */ remap_pfn_range(vma, vma->vm_start, physical, visze, vma->vm_page_prot);
为防止扩展映射最简单的办法是实现一个简单的nopage方法,它会产生一个总线信号传递给故障进程。
struct page *simple_nopage(struct vm_area_struct *vma, unsigned long address, int *type) { return NOPAGE_SIGBUS; /* 发送SIGBUS */ }
重新映射RAM
remap_pfn_range函数的一个限制是:它只能访问保留页和超出物理内存的物理地址。
在Linux中,在内存映射时,物理地址页被标记为“保留的,表示内存管理对其不起作用”。
使用nopage方法重映射RAM
struct page *scullp_vma_nopage(struct vm_area_struct *vma, unsigned long address, int *type) { unsigned long offset; struct scull_dev *ptr, *dev = vma->vm_private_data; struct page *page = NOPAGE_SIGBUS; void *pageptr = NULL; /* 默认值是没有 */ down(&dev->sem); offset = (address - vma->vm_start) + (vma->vm_pgoff << PAGE_SHIFT); if(offset >= dev->size) goto out; /* 超出范围 */ offset >= PAGE_SHIFT; /* offset是页号 */ 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; /* 空白区或者文件末尾 */ page = virt_to_page(pageptr); /* 获得该值,现在可以增加计数了 */ get_page(page); if(type) *type = VM_FAULT_MINOR; out: up(&dev->sem); return page; }
重新映射内核虚拟地址
page = vmalloc_to_page(pageptr); get_page(page); if(type) *type = VM_FAULT_MINOR; out: up(&dev->sem); return page;
三、执行直接I/O访问
无论如何,在字符设别中执行直接I/O是不可行的,也是有害的。只有确定执行设置缓冲I/O的开销特别巨大,才使用直接I/O。请注意块设备和网络设备根本不用担心实现直接I/O的问题。
在这两种情况中,内核中高层代码设置和使用了直接I/O,而驱动程序级的代码升值不需要知道已经执行了直接I/O
2.6中实现直接I/O的关键是名为get_user_pages的函数,它定义在<linux/mm.h>中,原型如下:
int get_user_page(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的任务指针,主要目的是告诉内核,当设置缓冲区时,谁负责解决页错误的问题。 mm:知悉将描述被映射地址空间的内存你管理结构的指针。 start len:start是用户空间缓冲区的地址,len是页内的缓冲区长度 write force:如果write非零,对映射的页有写权限(意味着用户空间执行了读操作) pages vmas:输出参数。如果调用成功,pages中包含一个描述用户空间缓冲区page结构的指针列表,vmas包含了相应VMA的指针。
get_user_pages函数是一个底层内存管理函数,使用了比较复杂的接口。
它还需要在调用前,将mmap为获得地址空间的读取者/写入者信号量设置为读模式。因此有:
down_read(&cuurent->mm->mmap_sem); result = get_user_pages(current, current->mm, ...); up_read(¤t->mm->mmap_sem);
result返回一个实际被调用的页数,它可能比请求的数量少,但大于0
1、调用成功后,就有了一个用户空间缓冲区的页数组,它将被锁在内存中。
2、为了能直接操作缓冲区,内核空间的代码必须用kmap或者kmap_atomic函数将每个page结构指针转换成内核虚拟地址。
3、使用直接I/O通常使用DMA操作,因此驱动程序要从page结构指针数组中创建一个分散/聚合链表。
4、一旦直接操作I/O完成,必须是释放用户内存页。如果改变了页的内容,必须通知内核,确保内核认为它是“干净”的
void SetPageDirty(struct page *page);
宏在头文件<linux/page-flags.h>中。使用例子如下:
if(!PageReserved(page)) SetPageDirty(page);
不管页是否被改变,他们都必须从也缓存中释放,否则他们会永远存在那里,函数:
void page_cache_release(struct page *page);
异步I/O
2.6的新特性是异步I/O,异步I/O允许用户空间初始化操作,但不必等待它们完成,这样执行异步I/O时,应用程序可以进行其他的操作。
字符设备驱动吗程序需要清楚地表示需要异步I/O的支持。如果有恰当的理由需要在同一时刻执行多余一个的I/O操作,则字符设备将会从异步I/O中受益。
支持异步I/O的驱动程序应该包括<linux/aio.h>,有三个用于实现异步I/O的file_operations方法:
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_write与常用的read和write函数非常相似,也有一些不同。 其中一个不同是:offset参数是一个值,异步操作从不改变文件的位置,因此没有必要向它传递指针。
异步I/O的例子:
static ssize_t scullp_aio_read(struct kiocb *iocb, char *buf, size_t count, loff_t pos) { return scullp_defer_op(0, iocb, buf, count, pos); } static ssize_t scullp_aio_write(struct kiocb *iocb, const char *buf, size_t count, loff_t pos) { return scullp_defer_op(1, iocb, (char *)buf, count, pos); } struct async_work{ struct kiocb *iocb; int result; struct work_struct work; } static int scullp_defer_op(int write, struct kiocb *iocb, char *buf, size_t count, loff_t pos) { struct async_work *stuff; int result; /* 虽然可以访问缓冲区,但现在要进行拷贝操作 */ if(write) result = scullp_write(iocb->ki_filp, buf, count, &pos); else result = scullp_read(iocb->ki_filp, buf, count, &pos); /* 如果这是一个同步的IOCB,则现在反悔状态值 */ if(is_sync_kiocb(iocb)) return result; /* 否则把完成操作向后推迟几毫秒 */ stuff = kmalloc(sizeof(*stuff), GFP_KERNEL); if(stuff == NULL) return result; /* 没有可用内存了,使之完成 */ stuff->iocb = iocb; stuff->result = result; INIT_WORK(&stuff->work, scullp_do_deferred_op, stuff); schedule_delayed_work(&stuff->work, HZ/100); return -EIOCBQUEUED; } static void scullp_do_defered_op(void *p) { struct async_work *stuff = (struct async_work *)p; aio_complete(stuff->iocb, stuff->result, 0); kfree(stuff); }
四、直接访问内存
直接访问内存,或者DMA。是内存中的高级部分。
DMA数据传输概览
第一种情况中,所需要的步骤概括如下:
1.当进程调用read,驱动程序函数分配一个DMA缓冲区,并让硬件将数据传输到这个缓冲区中,进程处于睡眠状态
2.硬件将数据写入到DMA缓冲区中,当写入完毕,产生一个中断
3.中断处理程序获得输入的数据,应答中断,并且唤醒进程,该进程现在即可读取数据。
分配DMA缓冲区
使用DMA缓冲区的主要问题是:当大于一页时,他们必须占据连续的物理页,这是因为设备使用ISA或者PCI系统总线传输数据,而这两种方式使用的都是物理地址。
DIY分配
get_free_pages函数可以分配多达几M字节的内存,但是对较大数量的请求,甚至是远少于129KB的请求也通常会失败。
在引导时,我们可以通过内核传递"mem=参数"的办法保留顶部的RAM。比如系统有256MB内存,参数"mem=255M"将使内核不能使用顶部的1M字节。
dmabuf = ioremap(0xFF00000 /* 255M */, 0x100000 /* 1M */);
总线地址
使用DMA的设备驱动后曾虚将于连接到总线接口上的硬件通信,硬件使用的是物理地址,而程序代码使用的是虚拟地址。
不推荐使用这些函数,在<ams/io.h>中定义的函数:
unsigned long virt_to_bus(volatile void *address); void *bus_to_virt(unsigned long address);
通用DMA层
DMA操作最终会分配缓冲区,并将总线地址传递给设备。一个可移植的驱动程序要求对所有体系架构都能安全而正确的执行DMA操作,编写这样一个驱动程序的难度超出了一般人的想象。
幸运的是内核提供了一个与总线-----体系架构无关的DMA层,他会隐藏大多数问题。强烈建议使用该层编写。
涉及到的device结构指针,需要包含文件<linux/dma-mapping.h>
处理复杂的硬件
在执行DMA之前,必须确定给定设备是否有能力执行该操作。
默认情况下,内核假设设备都能在32为地址上执行DMA。如果不是这样应该调用下面的函数通知内核:
int dma_set_mask(struct device *dev, u64 mask);
因此一个受限的24位DMA操作应该为:
if(dma_set_mask(dev, 0xffffff)) card->use_dma = 1; else { card->use_dma = 0; printk(KERN_WARN, "mydev: DMA not supported "); }
如何设备支持常见的23位DMA操作,则没有必要调用dma_set_mask。
DMA映射
一个DMA映射是 要分配的DMA缓冲区 与 为该缓冲区生成的、设备可访问地址的组合。
DMA映射必须解决缓存一致性的问题。如果设备改变了主内存找那个的区域,则任何覆盖该区域的处理器缓存都将无效。
否则处理器将使用不正确的主内存映射,从而产生不正确的数据。
DMA映射建立一个新的结构类型------dma_addr_t表示总线地址。
dma_addr_t类型的变量对驱动程序是不透明的,唯一允许的操作是将他们传递给DMA支持例程以及设备本身。
PCI代码区分两种类型的DMA映射:
1.一致性DMA映射
2.流式DM映射
建立一致性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中 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地址、总线地址等参数
DMA池
DMA池是一个生成小型、一致性DMA映射的机制。调用dma_alloc_coherent函数获得最小单位为页的映射。
如果需要再小,就需要DMA池了。在头文件<linux/dmapool.h>中定义了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
用完DMA池后,需要调用函数释放:
void dma_pool_destroy(struct dma_pool *pool); 销毁之前必须分返回所有分配的内存 void *dma_pool_alloc(struct dma_pool *pool, int mem_flags, dma_addr_t *handle); mem_flags:通常设置为GFP_分配标志 返回的DMA地址是内核虚拟地址,并作为总线地址保存在handle中 使用下面函数返回不需要的缓冲区 void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr);
建立流式DMA映射
只有一个缓冲区要被传输的时候,使用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_signle(strcut device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction direction); size和direction采纳数必须与映射缓冲区的参数像匹配
驱动程序需要不经过撤销映射就访问流式DMA缓冲区的内容,内核提供如下调用:
void dma_sync_signle_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); 处理器在调用该函数后,不能再访问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(strcut device *dev, dma_addr_t dma_address, size_t size, enum dma_data_direction direction);
分散/聚集映射
映射分散表的第一步是建立并填充一个描述被传送缓冲区的scatterlist结构的数组。该结构在头文件<linux/scatterlist.h>中描述
struct page *page; 与在scatter/gather操作中用到缓冲区响应的page结构指针
unsigned int length;
unsigned int offset; 在页内缓冲区的长度和偏移量
PCI双重地址周期映射
一个简单的PCI DMA例子
ISA设备的DMA
注册DMA
与DMA控制器通信