内存映射(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或者用户缓冲区了。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值