本章内容分为三个部分:

  • 第一部分讲述了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的更多相关文章

  1. 《Linux Device Drivers》第十五章 内存映射和DMA——note

    简单介绍 很多类型的驱动程序编程都须要了解一些虚拟内存子系统怎样工作的知识 当遇到更为复杂.性能要求更为苛刻的子系统时,本章所讨论的内容迟早都要用到 本章的内容分成三个部分 讲述mmap系统调用的实现 ...

  2. Windows核心编程 第十七章 -内存映射文件(上)

    第1 7章 内存映射文件 对文件进行操作几乎是所有应用程序都必须进行的,并且这常常是人们争论的一个问题.应用程序究竟是应该打开文件,读取文件并关闭文件,还是打开文件,然后使用一种缓冲算法,从文件的各个 ...

  3. 内存映射与DMA

    1.mmap系统调用的实现过程,该系统调用直接将设备内存映射到用户进程的地址空间. 2.用户空间内存如何映射到内核中(get_user_pages). 3.直接内存访问(DMA),他使得外设具有直接访 ...

  4. 《windows核心编程》 17章 内存映射文件

    内存映射文件主要用于以下三种情况: 系统使用内存映射文件载入并运行exe和dll,这大量节省了页交换文件的空间以及应用程序的启动时间 开发人员可以使用内存映射文件来访问磁盘上的数据文件.这使得我们可以 ...

  5. Windows核心编程 第十七章 -内存映射文件(下)

    17.3 使用内存映射文件 若要使用内存映射文件,必须执行下列操作步骤: 1) 创建或打开一个文件内核对象,该对象用于标识磁盘上你想用作内存映射文件的文件. 2) 创建一个文件映射内核对象,告诉系统该 ...

  6. 第17章 内存映射文件(3)_稀疏文件(Sparse File)

    17.8 稀疏调拨的内存映射文件 17.8.1 稀疏文件简介 (1)稀疏文件(Sparse File):指的是文件中出现大量的0数据,这些数据对我们用处不大,但是却一样的占用空间.NTFS文件系统对此 ...

  7. 内存映射MMAP和DMA【转】

    转自:http://blog.csdn.net/zhoudengqing/article/details/41654293 版权声明:本文为博主原创文章,未经博主允许不得转载. 这一章介绍Linux内 ...

  8. Linux就这个范儿 第15章 七种武器 linux 同步IO: sync、fsync与fdatasync Linux中的内存大页面huge page/large page David Cutler Linux读写内存数据的三种方式

    Linux就这个范儿 第15章 七种武器  linux 同步IO: sync.fsync与fdatasync   Linux中的内存大页面huge page/large page  David Cut ...

  9. ASM:《X86汇编语言-从实模式到保护模式》第15章:任务切换

    15章其实应该是和14章相辅相成的(感觉应该是作者觉得14章内容太多了然后切出来了一点).任务切换和14章的某些概念是分不开的. ★PART1:任务门与任务切换的方法 1. 任务管理程序 14章的时候 ...

随机推荐

  1. (转)linux nc命令使用详解

    linux nc命令使用详解 原文:https://www.2cto.com/os/201306/220971.html 功能说明:功能强大的网络工具 语 法:nc [-hlnruz][-g<网 ...

  2. 阶段1 语言基础+高级_1-3-Java语言高级_04-集合_02 泛型_6_泛型通配符

    泛型通配符是一个问号 也是代表不确定的意思 换成Object两个都报错了. 泛型是没有继承概念的,所以上面写Object就会报错.这里应问号 可以代表位置类型 it.next会自动用Object接收 ...

  3. jmeter之分布式压测

    很多性能大牛说一台机器的压测其实不准确,于是搜索网上的分布式压测练习了一番 目录 1.环境准备 2.控制机和压测机配置 3.执行分布式压测 1.环境准备 1.1准备一台windows作为控制机(mas ...

  4. MyBatis Generator 生成的example 使用 and or 简单混合查询

    MyBatis Generator 生成的example 使用 and or 简单混合查询 参考博客:https://www.cnblogs.com/kangping/p/6001519.html 简 ...

  5. Log4Net使用详解(续)

    转:http://blog.csdn.net/zhoufoxcn/article/details/6029021 说明自从上次在2008年在博客上发表过有关log4net的用法介绍文章之后(网址:ht ...

  6. STL 配接器(adapters)

    定义 配接器(adapters):将一个class的接口,转换为另一个class的接口,使得原来不能一起使用相互兼容的classes,可以一起协同工作. 配接器是一种设计模式. STL中提供的各种配接 ...

  7. [Python3 练习] 004 水仙花数

    题目:水仙花数 (1) 描述 水仙花数各位的数字的立方之和等于自身 如 153 为水仙花数,因为 153 = 1^3 + 5^3 + 3^3 (2) 要求 找到所有的三位数的水仙花数 (3) 程序 # ...

  8. hdu6357 Hills And Valleys (最长不下降子序列)

    题目传送门 题意: 给你0~9的字符串,问你翻转哪个区间后使得其最长不下降子序列长度最长 思路: 因为字符是0~9,所以我们可以定义一个b数组来枚举L,R, 去和原来的字符串去求最长公共子序列长度,不 ...

  9. 除了a链接跳转,还有其他的跳转方式

    一.直接在要跳转部分加上onclick事件 1.加入onclick事件: <div onclick="window.open('http://baidu.com','_blank')& ...

  10. VINS 估计器之外参初始化

    为何初始化外参 当外参完全不知道的时候,VINS也可以在线对其进行估计(rotation),先在processImage内进行初步估计,然后在后续优化时,会在optimize函数中再次优化. 如何初始 ...