首页 > 代码库 > 内存映射(Linux设备驱动程序)

内存映射(Linux设备驱动程序)

第一部分:mmap系统调用直接将设备内存映射到用户进程的地址空间里。
第二部分:跨越边界直接訪问用户空间的内存页。一些相关的驱动程序须要这样的能力,(用户空间内存怎样映射到内核中的方法get_user_pages)
第三部分:直接内存訪问(DMA)I/O操作,使得外设具有直接訪问系统内存的能力。




Linux的内存管理

地址类型
Linux是一个虚拟内存系统,
这意味着用户程序所使用的地址与硬件使用的物理地址是不等同的。


虚拟内存引入了一个间接层。

Linux系统处理多种类型的地址,而每种类型的地址都有自己的语义。


但在何种情况下使用何种类型的地址。内核代码并未明白加以区分。

以下是Linux使用的地址类型列表:
用户虚拟地址: 用户空间程序所能看到的哦常规地址。
物理地址: 在处理器和系统内存之间使用
总线地址: 在外围总线和内存之间使用
内核逻辑地址: 组成了内核的常规地址空间,该地址映射了部分(或者所有)内存,并常常被视为物理地址。
内核虚拟地址: 内核虚拟地址与物理地址的映射不必是线性的和一对一的,而这是逻辑地址空间的特点。



(kmalloc返回的内存就是内核逻辑地址

)


全部的逻辑地址都是内核虚拟地址。但很多内核虚拟地址不是逻辑地址。


vmalloc分配的内存具有一个虚拟地址,kmap函数也返回一个虚拟地址


物理地址和页

高端与低端内存
在内核配置的时候可以改变低端内存和高端内存的界限。通常将该界限设置为小于1GB。


这个界限与早期PC中的640KB限制没有关系,也与硬件无关。
它是由内核设置的。把32位地址空间切割成内核空间与用户空间。



内存映射和页结构
因为历史的关系,内核使用逻辑地址来引用物理内存中的页。

但在高端内存中无法使用逻辑地址,因此内核中处理内存的函数趋向使用指向page结构的指针。

(在<linux/mm.h>中定义)

该数据结构用来保存内核须要知道的全部物理内存信息。
对系统中每一个物理页,都有一个page结构相相应。


内核维护了一个或者多个page结构数据,用来跟踪系统中的物理内存。

在一些系统中,有一个单独的数组称之为mem_map。

有有些函数和宏用来在page结构指针与虚拟地址之间进行转换:
/*负责将内核逻辑地址转换为对应的page结构指针。因为它须要一个逻辑地址。因此不能操作vmalloc生成的地址以及高端内存*/
struct page * virt_to_page(void *kaddr);
/*对于给定的页帧号,返回page结构指针*/
struct page *pfn_to_page(int pfn);
/*返回页的内核虚拟地址。对于高端内存,仅仅有当内存页被映射后该地址才存在。*/
void *page_address(struct page *page);

#include<linux/highmem.h>
/*kmap为系统中的页返回内核虚拟地址*/
void *kmap(struct page *page);
void kunmap(struct page *page);

kmap为系统中的页返回内核虚拟地址。

对于低端内存也来说。它仅仅返回页的逻辑地址;

对于高端内存。kmap在专用的内核地址空间创建特殊的映射。

由kmap创建的映射须要用kunmap释放。




页表
将虚拟地址转换为对应的物理地址。


虚拟内存区(VMA)
虚拟内存区(VMA)用于管理进程地址空间中不同区域的内核数据结构。

一个VAM表示在进程的虚拟内存中的一个同类区域:

拥有相同权限标志位和被相同对象备份的一个连续的虚拟内存地址范围。

“拥有自身属性的内存对象”


进程的内存映射(至少)包括:
程序的可运行代码(text)区域
多个数据区
与每一个活动的内存映射相应的区域


查看/proc/<pid/maps>文件就能了解进程的内存区域。


/proc/self始终指向当前进程。
每行都是用以下的形式表示的:
start-end perm offset major:minor inode image
start-end: 该内存区域的起始处和结束处的虚拟地址
perm: 内存区域的读、写和运行权限的位掩码。描写叙述什么样的进程能訪问。
offset: 表示内存区域在映射文件里的起始位置。
major minor: 拥有映射文件的设备的主设备号和次设备号。

对于设备映射来说,主设备号和次设备号指的是包括设备特殊文件的磁盘分区。该文件由用户而非设备自身打开。
inode: 被映射的文件的索引节点号。
image: 被映射文件的名称(一般是一个可运行映像)。


vm_area_struct结构
当用户空间进程调用mmap,将设备内存映射到它的地址空间时。系统通过创建一个表示该映射的新VMA作为响应。
支持mmap的驱动程序须要帮助进程完毕VMA的初始化。

内核维护了VMA的链表和树型结构,而vm_area_struct中的很多成员都是用来维护这个机构的。
因此驱动程序不能随意创建VMA。或者打破这样的组织结构。




VMA的主要成员:
unsigned long vm_start;
unsigned long vm_end;
struct file *vm_file;
unsigned long vm_pgoff;
unsigned long vm_flags;
struct vm_operations_struct *vm_ops;
void *vm_private_data;


vm_operations_struct结构:
这些操作仅仅是用来处理进程的内存需求。
void (*open)(struct vm_area_struct *vma);
void (*close)(struct vm_area_struct *vma);
/*
当一个进程要訪问属于合法VMA的页,但该页又不在内存中,则为相关区域调用nopage函数。在将物理页从辅助存储器中读入后,该函数返回指向物理页的page结构指针。假设该区域未定义nopage函数。则内核将为其分配一个空页。


*/
struct page *(*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);
/*在用户空间訪问页前。该函数同意内核将这些页预先装入内存。

*/
int (*populate)(struct vm_area_struct *vm, unsigned long address, unsigned long len, pgprot_t prot, unsigned long pgoff, int nonblock);


内存映射处理
在系统中的每一个进程(除了内核空间的一些辅助线程外)都拥有一个struct mm_struct结构(在<linux/sched.h>中定义),当中包括了虚拟内存区域链表、页表以及其它大量内存管理信息,还包括一个信号灯(mmap_sem)和一个自旋锁(page_table_lock)。


在task结构中能找到该结构的指针。


当驱动程序须要訪问它时。经常使用的办法是使用current->mm。
多个进程能够共享内存管理结构,Linux就是用这样的方法实现线程的。


mmap设备操作
在现代Unix系统中,内存映射是最吸引人的特征。
对于驱动程序来说,内存映射能够提供给用户程序直接訪问设备内存的能力。
映射一个设备意味着将用户空间的一段内存与设备内存关联起来。当程序在分配的地址范围内读写时,实际上訪问的就是设备。


但不是全部的设备都能进行mmap抽象的,比方像串口和其它面向流的设备就不能。
mmap的还有一个限制是必须以PAGE_SIZE为单位进行映射。
内核仅仅能在页表一级上对虚拟地址进行管理。因此那些被映射的区域必须是PAGE_SIZE的整数倍。而且在物理内存中的起始地址也要求是PAGE_SIZE的整数倍。


大多数PCI外围设备将它们的控制寄存器映射到内存地址中。




mmap方法是file_operations结构的一部分,而且运行mmap系统调用时将调用该方法。
系统调用有着下面声明:
mmap(caddr_t addr, size_t len, int prot, int flags, int fd, off_t offset);
文件操作声明:
int (*mmap)(struct file *filp, struct vm_area_struct *vma);


为了运行mmap。驱动程序须要为该地址范围建立合适的页表,并将vma->vm_ops替换为一系列的新操作。

有两种建立页表的方法:使用remap_pfn_range函数一次所有建立。或者通过nopage VMA方法每次建立一个页表。


使用remap_pfn_range
remap_pfn_range和io_remap_page_range负责为一段物理地址建立新的页表。
原型:
/*pfn指向实际系统RAM的时候使用*/
int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);
/*phys_addr指向I/O内存时使用*/
int io_remap_page_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long phys_addr, unsigned long size, pgprot_t prot);


使用nopage映射内存(返回page结构指针)
当应用程序要改变一个映射区域所绑定的地址时。会使用mremap系统调用。此时是使用nopage映射的最好的时机。


假设VMA的尺寸变小了,内核将会刷新不必要的页,而不通知驱动程序。
假设VMA的尺寸变大了,当调用nopage时为新页进行设置时,驱动程序终于会发现。
假设要支持mremap系统调用。就必须实现nopage函数。

nopage函数原型:
struct page* (*nopage)(struct vm_area_struct *vma, unsigned long address, int *type);
当用户要訪问VMA中的页,而该页又不在内存中时,将调用相关的nopage函数。
address參数包括了引起错误的虚拟地址。


nopage函数必须定位并返回指向用户所须要页的page结构指针。


该函数还调用get_page宏用来添加返回的内存页的使用计数。

get_page(struct page *pageptr);
内核为每一个内存页都维护了该计数;当计数值为0时,内核将把该页放到空暇列表中。当VMA解除映射时。内核为区域内的每一个内存页降低使用计数。
在设备驱动程序中,type的正确值应该总是VM_FAULT_MINOR。


通常nopage方法返回一个指向page结构的指针。


PCI内存被映射到系统内存最高端之上。因此在系统内存映射中没有这些地址的入口,因此无法返回一个指向page结构的指针。

在这样的情况下。必须使用remap_page_range。

假设nopage函数是NULL。则负责处理页错误的内核代码将把零内存页映射到失效虚拟地址上。
零内存页是一个写拷贝内存页,它=读它时会返回0,它被用于映射BSS段。
假设一个进程调用mremap扩充一个映射区域,而驱动程序没有实现nopage,则进程将终于得到一块全是零的内存,而不会产生段故障错误。




重映射特定的I/O区域
一个典型的驱动程序仅仅映射与其外围设备相关的一小段地址,而不是映射所有地址。



对remap_pfn_range函数的一个限制是:它仅仅能訪问保留页超出物理内存的物理地址



在Linux中。在内存映射时,物理地址页被标记为“保留的”(reserved),表示内存管理对其不起作用。

保留页在内存中被锁住。而且是唯一可安全映射到用户空间的内存页。

这个限制是保证系统稳定性的基本需求。
remap_pfn_range不同意又一次映射常规地址。包含调用get_free_page函数所获得的地址。


它能又一次映射高端PCI缓冲区和ISA内存。


使用nopage方法重映射RAM
将实际的RAM映射到用户空间的方法是:使用vm_ops->nopage一次处理一个页错误。

(又一次映射内核虚拟地址)
一个真正的内核虚拟地址。如vmalloc这种函数返回的地址,是一个映射到内核页表的虚拟地址。




运行直接I/O訪问
对于块设备和网络设备。内核中高层代码设置和使用了直接I/O。而驱动程序级的代码甚至不须要知道已经运行了直接I/O。



在2.6内核中,实现直接I/O的关键是名为get_user_pages的函数。它定义在<linux/mm.h>中。并由下面原型:
int get_user_pages(struct task_struct *tsk, struct mm_struct *mm, unsigned long start, int len, int write, int force, struct page **pages, struct vm_area_struct **vmas);
tsk指向运行I/O的任务指针;
mm指向描写叙述被映射地址空间的内存管理结构的指针;
start是用户空间缓冲区的地址;
len是页内的缓冲区长度。
write非零表示对映射的页有写权限。
force标志告诉get_user_pages函数不要考虑对指定内存页的保护;
pages(输出參数)pages中包括了一个描写叙述用户空间缓冲区page结构的指针列表。
vmas(输出參数)包括了对应VMA的指针。



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

它须要在调用前,将mmap为获得地址空间的读取者/写入者信号量设置为读模式。
如:
down_read(&current->mm->mmap_sem);
result = get_user_pages(current, current->mm,...);
up_read(&current->mm->mmap_sem);
返回的值是实际被映射的页数。
假设调用成功。调用者就会拥有一个指向用户空间缓冲区的页数组,它将被锁在内存中。

为了能直接操作缓冲区,内核空间的代码必须用kmap或者kmap_atimic函数将每一个page结构指针转换成内核虚拟地址。使用直接I/O的设备通常使用DMA操作,因此驱动程序要从page结构指针数组中创建一个分散/聚集链表。
一旦直接I/O操作完毕,就必须释放用户内存页。
在释放前,假设改变了这些页的内容,必须通知内核。

必须使用以下的函数标记出每一个被改变的页:
void SetPageDirty(struct page *page);
(用户空间内存通常不会被标记为保留)
无论页是否被改变,它们都必须从页缓存中释放。
void page_cache_release(struct page *page);


异步I/O
异步I/O同意用户空间初始化操作,但不必等待它们完毕。


块设备和网络设备程序是全然异步操作的。
仅仅有字符设备驱动程序须要清楚地表示须要异步I/O的支持。


异步I/O的实现总是包括直接I/O操作。
有三个用于实现异步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, char *buffer, size_t count, loff_t offset);
int (*aio_fsync)(struct kiocb *iocb, int datasync);
aio_fsync操作仅仅对文件系统有意义。


aio_read和aio_write函数的目的是初始化读和写操作。在这两个函数完毕时,读写操作可能已经完毕,也可能未完毕。



假设支持异步I/O,则必须知道一个事实:内核有时会创建“同步IOCB”。


同步标识会在IOCB中标识,驱动程序应该使用以下的函数进行查询:
int is_sync_kiocb(struct kiocb *iocb);
假设该函数返回非零值,则驱动程序必须运行同步操作。

驱动程序必须通知内核操作已经完毕。
int aio_complete(struct kiocb *iocb, long res, long res2);
一旦调用了aio_complete,就不能再訪问IOCB或者用户缓冲区了。

内存映射(Linux设备驱动程序)