📄 (ldd) ch13-mmap和dma(转载).txt
字号:
被打开和关闭一次。
void(*unmap)(struct vm_area_struct *vma,unsigned long addr,size_t len);
核心调用这个方法取消一个区域的部分或全部映射。如果整个区域的映射被取消,核心
在vm_ops->unmap返回后立即调用vm_ops->close。
void (*protect)(struct vm_area_struct *vma,unsigned long,size_t,unsigned
int
new prot);
当前未被使用。许可(保护)位的处理并不依赖于区域本身。
int(*sync)(struct vm_area_struct *vma,unsigned long,size_t,unsigned int
flags);
这个方法被msync系统调用以将一个脏的内存区段保存到存贮介质上。如果成功则返回值
为0 ,如果有错,则返回一个负数。核心版本1.2让这个方法返回void,因为这个函数不
被认为会失败。
void(*advise)(struct vm_area_struct *vma,unsigned long,size_t,unsigned int
advise);
当前未被使用。
unsigned long(*nopage)(struct vm_area_struct *vma,unsigned long
address,int
write_access);
当一进程试图访问属于另一个有效VMA的某页,而该页当前不在内存时,nopage方法就会
被调用,如果它为相关区域定义。这个方法返回该页的(物理)地址。如果这个方法不
为这个区域所定义,核心会分配一个空页。通常,驱动程序并不实现nopage,因为被一
个驱动程序映射的区段往往被完全映射到系统物理地址。核心版本1.2的nopage具有一个
不同的原型和不同的含义。第三个参数write_access被当做“不共享”——一个非零值
意味着该页必须被当前进程所有,而零则表示共享是可能的。
unsigned long(*wppage)(struct vm_area_struct *vma,unsigned long
address,unsigned long page);
这个方法处理“写保护”页面错,但目前不被使用。核心处理所有不调用区域特定的回
调函数却往一个被保护的页面上写的企图。写保护被用来实现“写时拷贝(copy_on_wri
te)”。一个私有的页可以被不同进程所共享,直到其中一个进程试图写它时。当这种
情况发生时,页面被克隆,进程向自己的页拷贝上写。如果整个区域被称为只读,会有
一SIGSEGV信息被发送给进程,写时拷贝就未能完成。
int (*swapout)(struct vm_area_struct *vma,unsigned long offset,pte_t
*page_table);
这个方法被用来从交换空间取得一页。参数offset是相对区域而言(与上面swapout一样
),而entry是页面的当前pte——如果swapout在这一项中保存了一些信息,那么现在就
可以用这些信息来取得该页。
一般说来,驱动程序并不需要去实现swapout或swapin,因为驱动程序通常映射I/O内存
,而不是常规内存。I/O页是一些象访问内存一样访问的物理地址,但被映射到设备硬件
而不是RAM上。I/O内存区段或者被标记为“保留”,或者居于物理内存之上,因此它们
从不被换出—交换I/O内存没什么实际意义。
内存映象
内存映象
在Linux中还有与内存管理相关的第三个数据结构。VMA和页表组织虚拟地址空间,而物
理地址空间则由内存映象概括。
核心需要物理内存当前使用情况的一个描述。由于内存可以被看作是页面数组,因此这
个信息也可以组织为一个数组。如果你需要其页面的信息,你就用其物理地址去访问内
存映象。下面就是核心代码用来访问内存映象的一些符号:
typedef struct {/*…*/} mem_map_t
extern mem_map_t mem_map[];
映象本身是mem_map_t的一个数组。系统中的每个物理页,包括核心代码和核心数据,都
在mem_map中有一项。
PAGE_OFFSET
这个宏表示由物理地址映射到的核心地址空间中的虚地址。PAGE_OFFSET在任何用到“物
理”地址的地方都必须要考虑。核心认为的物理地址实际上是一个虚拟地址,从实际物
理地址偏移PAGE_OFFSET——这个实际物理地址是在CPU外的电气地址线使用。在Linux2.
0.x中, PAGE_OFFSET在PC上都是零,在大多数其它平台上都不是零。2.1.0版修改了PC
上的实现,所以它现在也使用偏移映射。如果考虑到核心代码,将物理空间映射到高的
上的实现,所以它现在也使用偏移映射。如果考虑到核心代码,将物理空间映射到高的
虚拟地址有一些好处,但这已经超出了本书的范围。
int MAP_NR(addr)
当程序需要访问一个内存映象时,MAP_NR返回在与addr关联的mem_map数组中的索引。参
数addr可以是unsigned long,也可以是一个指针。因为这个宏被几个关键的内存管理函
数使用多次,所以它不进行addr的有效性检查;调用代码在必要的时候必须自己进行检
查。
((nr<<PAGE_SHIFT)+PAGE_OFFSET)
没有标准化的函数或者宏可以将一个映象号转译为一个物理地址。如果你需要MAP_NR的
逆函数,这个语句可以使用。
内存映象是用来为每个内存页维护一些低级信息。在核心开发过程中,内存映象结构的
准确定义变过几次,你不必了解细节,因为驱动程序不期望查看映象内部。
不过,如果你对了解页面管理的内部感兴趣的话,头文件<linux/mm.h>含有一大段注释
解释mem_map_t域的含义。
mmap设备操作
内存映象是现代Unix系统中最有趣的特征之一。至于驱动程序,内存映射可以提供用户
程序对设备内存的直接访问。
例如,一个简单ISA抓图器将图象数据保存在它自己的内存中,或者在640KB-1KB地址范
围,或者在“ISA洞”(指14MB-16MB之间的范围参见第8章“硬件管理”中“访问设备板
子上的内存”一节)中。
将图象数据复制到常规(并且更快)RAM中是不定期抓图的合适的方法,但如果用户程序
需要经常性地访问当前图象,使用mmap方法将更合适。
映射一个设备的意思是使用户空间的一段地址空间关联到设备内存上。当程序读写指定
的地址范围时,它实际上是在访问设备。
正如你所怀疑的,并不是每个设备都适合mmap概念;例如,对于串口或其它面向流的设
备来说它的确没有意义。mmap的另一个限制是映射是以PAGE_SIZE为单位的。核心只能在
页表一级处置虚地址,因此,被映射的区域必须是PAGE_SIZE的整数倍,而且居于页对齐
的物理内存。核心通过使一个区段稍微大一点儿的办法解决了页面粒度问题。对齐的问
题通过使用vma->vm_offset来处理,但这对于驱动程序并不可行——映射一个设备简化
为访问物理页,它必须是页对齐的。
这些限制对驱动程序来说并不是很大的问题,因为不管怎样,访问设备的程序是设备相
关的。它知道如何使得被映射的内存区段有意义,因此页对齐不是一个问题。当你ISA板
关的。它知道如何使得被映射的内存区段有意义,因此页对齐不是一个问题。当你ISA板
子插到一个Alpha机器上时,有一个更大的限制,因为ISA内存是以8位、16位或32位项的
散布集合被访问的,没有从ISA地址到Alpha地址的直接映射。在这种情况下,你根本不
能使用mmap。不能进行ISA地址到Alpha地址的直接映射归因于两种系统数据传送规范的
不兼容。Alpha只能进行32位和64位的内存访问,而ISA只能进行8位和16位的传送,没有
办法透明地从一个协议映射到另一个。结果是你根本不能对插在Alpha计算机的ISA板子
使用mmap。
当可行的时候,使用mmap有一些好处。例如,一个类似于X服务器的程序从显存中传送大
量的数据;把图形显示映射到用户空间与lseek/write实现相比,显著地改善了吞吐率。
另一个例子是程序控制PCI设备。大多数PCI外围设备都将它们的控制寄存映射到内存地
址上,一个请求应用更喜欢能直接访问寄存器,而不是反复调用ioctl来完成任务。
mmap方法是file_oprations结构的一部分,在mmap系统调用被发出时调用。在调用实际
方法之前,核心用mmap完成了很多工作,因此,这个方法的原型与系统调用很不一样。
这与其它调用如ioctl和select不同,它们在被调用之前核心并不做太多的工作。
系统调用如下声明(在mmap(2)手册中有描述):
mmap(caddr_t,size_t len,int prot,int flags,int fd,off_t offset)
另一方面,文件操作如下声明:
int (*mmap)(struct inode*inode,struct file*filp,struct vm_area_struct
*vma);
方法中inode和filp参数与第三章“字符设备驱动程序”中介绍的一样。vma会有用以访
问设备的虚拟地址范围的信息。这样,驱动程序只需为这个地址范围构造合适的页表:
如果需要,用一组新的操作代替vma->vm_ops。
一个简单的实现
设备驱动程序的大多数mmap实现对居于周边设备上的某些I/O内存进行线性的映射。/dev
/mem和/dev/audio都是这类重映射的例子。下面的代码来自drivers/char/mem.c,显示
了在一个被称为simple(Simple Implementation Mapping Pages with Little
Enthusiasm)的典型模块中这个任务是如何完成的:
(代码277)
很清楚,操作的核心由remap_page_range完成,它被引出到模块化的驱动程序,因为它
做了大多数映射需要做的工作。
维护使用计数
上面给出的实现的主要问题在于驱动程序没有维护一个与被映射区域的连接。这对/dev/
上面给出的实现的主要问题在于驱动程序没有维护一个与被映射区域的连接。这对/dev/
mem来说并不是个问题,它是核心的一个完整的部分,但对于模块来说必须有一个办法来
保持它的使用计数是最新的。一个程序可以对文件描述符调用close,并仍然访问内存映
射的区段。然而,如果关闭文件描述符导致模块的使用计数降为零,那么模块可能被卸
载,即使它们仍被通过mmap使用着。
试图关于这个问题警告模块的使用者是不充分的解决办法,因为可能使用kerneld装载和
卸载你的模块。这个守护进程在模块的使用计数降为零时自动地去除它们,你当然不能
警告kerneld去留神mmap。
这个问题的解决办法是用跟踪使用计数的操作取代缺省的vma->vm.ops。代码相当简单—
—用于模块化的/dev/mem的一个完全的mmap实现如下所示:
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -