LDD3 第15章 内存映射和DMA

2022-10-15,,

本章内容分为三个部分:

第一部分讲述了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
};

struct page

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

vm_area_struct

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

vm_operations_struct

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

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\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,
};

使用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 ;
}

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

scullp_vma_nopage

重新映射内核虚拟地址

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_page

get_user_pages函数是一个底层内存管理函数,使用了比较复杂的接口。

它还需要在调用前,将mmap为获得地址空间的读取者/写入者信号量设置为读模式。因此有:

down_read(&cuurent->mm->mmap_sem);
result = get_user_pages(current, current->mm, ...);
up_read(&current->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

异步I/O的例子:

static ssize_t scullp_aio_read(struct kiocb *iocb, char *buf, size_t count,
loff_t pos)
{
return scullp_defer_op(, 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(, 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/);
return -EIOCBQUEUED;
} static void scullp_do_defered_op(void *p)
{
struct async_work *stuff = (struct async_work *)p;
aio_complete(stuff->iocb, stuff->result, );
kfree(stuff);
}

异步I/O例子

四、直接访问内存

直接访问内存,或者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 = ;
else {
card->use_dma = ;
printk(KERN_WARN, "mydev: DMA not supported\n");
}

如何设备支持常见的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控制器通信

LDD3 第15章 内存映射和DMA的相关教程结束。

《LDD3 第15章 内存映射和DMA.doc》

下载本文的Word格式文档,以方便收藏与打印。