📄 (ldd) ch13-mmap和dma(转载).txt
字号:
(代码278)
这个代码依赖于一个事实,即核心在调用f_op->mmap之前将新产生区域中的vm_ops域初
始化为NULL。为安全起见以防止在将来的核心发生什么改变,给出的代码检查了指针的
当前值。
给出的实现利用了一个概念,即open(vma)和close(vma)都是缺省实现的一个补充。
驱动程序的方法不须复制打开和关闭的内存区域的标准代码;驱动程序只是实现额外的
管理。
有趣的是注意到,VMA的swapin和swapout方法以另外的方式工作——驱动程序定义的vm_
ops->swap*不是添加而是用完全不同的东西取代了缺省实现。
支持mremap系统调用
mremap系统调用被应用程序用来改变映射区段的边界地址。如果驱动程序希望能支持mre
map,以前的实现就不能正确地工作,因为驱动程序没有办法知道映射的区域已经改变了
。
Linux的mremap实现不提醒驱动程序关于映射区域的改变。实际上,它到是通过unmap方
法在区域减小时提醒驱动程序,但在区域变大时没有回调发出。
将减小告诉驱动程序隐含的基本思想法是驱动程序(或是将常规文件映射到内存的文件
系统)需要知道区段什么时候被取消映射了,从而采取适应的动作,如将页面刷新到磁
盘上。另一方面,映射区域的增大对驱动程序来说意义不大。除非调用mremap的程序访
问新的虚地址。在实际情况中,映射从未使用的区段是很常见的(如未使用过的某些程
序代码段)。因此,Linux核心在映射区段增大时并不告诉驱动程序,因为nopage方法将
会照管这些页。如果它们确实被访问了。
换句话说,当映射区段增大时,驱动程序未被提醒是因为nopage后来会这样做;从而不
必在需要前使用内存。这个优化主要是针对常规文件的,它们使用真正的RAM进行映射。
因此,如果你想支持mremap系统调用,就必须实现nopage。不过,一旦有了nopage,你
可选择广泛地使用它,从而避免从fops->mmap调用remap_page_range;这在下一个代码
段中给出。在这个mmap的实现中,设备方法只取代了vma->vm_fops。nopage方法负责一
次重映射一个页并返回其地址。
一个支持mremap(为节省空间,不支持使用计数)的/dev/mem实现如下所示:
(代码279)
(代码280)
如果nopage方法被留为NULL,处理页面错的核心代码就将零页映射到出错虚地址。零页
是一个写时 贝页,被当作零来读,可以用来映射BSS段。因此,如果一个进程通过调用
mremap扩展一个映射区段,并且驱动程序没有实现nopage,你最终会得到一些零页,而
不是段错。
注意,给出的实现远远不是最优的;如果内存方法能绕过remap_page_range而直接返回
物理地址会更好。不幸的是,这个技术的正确实现牵涉到一些细节,只能在本章晚些时
候搞清楚。而且上面给出的实现在核心1.2中并不能工作,因为nopage的原型在版本1.2
和2.0之间做了修改。在本节中我不打算管1.2核心。
重映射特定的I/O区段
到目前为止,我们所看到的所有例子都是/dev/mem的再次实现;它们将物理地址重映射
到用户空间——或者至少这是它们认为它们所做的。然而,典型的驱动程序只想映射应
用于它的外围设备的小地址区间,并非所有内存。
为了能为一个特定的驱动程序自定义/dev/mem的实现,我们需要进一步来研究一下remap
_page_range的内部。这个函数的完整原型是:
int remap_page_range(unsigned long virt_add,unsigned long phy_add,unsigned
long size, pgprot_t prot);
这个函数的返回值通常为零或为一个负的错误代码。让我们看看它的参数的确切含义。
unsigned long virt_add
重映射开始处的虚拟地址。这个函数为虚地址空间virt_add和virt_add+size之间的范围
构造页表。
unsigned long phys_add
虚拟地址应该映射到的物理地址。这个地址在上面提到的意义下是“物理的”这个函数
虚拟地址应该映射到的物理地址。这个地址在上面提到的意义下是“物理的”这个函数
影响phys_add到phys_add+size之间的物理地址。
unsigned long size
被重映射的区域的大小,以字节为单位。
pgprot_t prot
为新页所请求的“保护”。驱动程序不必修改保护,而且在vma->vma_page_prot中找到
的参数可以不加改变地使用。如果你好奇,你可以在<Linux/mm.h>中找到更多的信息。
为了向用户空间映射整个内存区间的一个子集,驱动程序需要处理偏移量。下面几行为
映射了从物理地址simple_region_start开始的simple_region_size字节大小的区段的驱
动程序完成了这项工作:
(代码281)
除了计算偏移量,上面的代码还为错误条件引入了两个检查。第一个检查拒绝将一个在
物理空间未对齐的位置映射到用户空间。由于只有完整的页能被重映射,因此映射的区
段只能偏移页面大小的整数倍。ENXIO是这种情况下通常返回的错误代码,它被展开为“
无此设备或地址”。
第二个检查在程序试图映射多于目标设备I/O区段可获得内存的空间时报告一个错误。代
码中psize是在偏移被确定后剩下的物理I/O大小,vsize是请求的虚存大小;这个函数拒
绝映射超出允许内存范围的地址。
注意,如果进程调用mremap,它便可以扩展其映射。一个“非常炫耀”的驱动程序可能
希望阻止这个发生;达到目的的唯一办法是实现一个vma->nopage方法。下面是这个方法
的最简单的实现:
unsigned long simple_pedantic_nopage(struct vm_area_struct *vma,unsigned
long address, int write_access);
{return 0;} /*发送一个SIGBUS*/
如果nopage方法返回0而不是一个有效的物理地址,一个SIGBUS(总线错)被发送到当前
进程(即发生页面错的进程)。如果驱动程序没有实现nopage,进程在请求的虚地址处
得到一个零页;这通常可以接受,因为mremap是个非常少用的系统调用,而且将零页映
射到用户空间也没有安全问题。
重映射RAM
在Linux中,物理地址的一页被标记在内存映象中是“保留的”,表明不被内存管理系统
使用。例如在PC上,640KB到1MB之间的部分被称为“保留的”,它被用来存放核心代码
使用。例如在PC上,640KB到1MB之间的部分被称为“保留的”,它被用来存放核心代码
。
remap_page_range的一个有趣的限制是,它只能给予对保留的页和物理内存之上的物理
地址的访问。保留页被锁在内存中,是仅有的能安全映射到用户空间的页;这个限制是
系统稳定性的基本要求*。
因此,remap_page_range不允许你重映射常规地址——包括你通过调用get_free_page所
获得的那些。不过,这个函数做了所有一个硬件驱动程序希望它做的,因为它可以重映
射高PCI缓冲和ISA内存——包括第1兆内存和15MB处ISA洞,如果在第八章“1M以上的ISA
内存”中提到的改变发生了的话。另一方面,当对非保留的页使用remap_page_range时
,缺省的nopage处理程序映射被访问的虚地址处的零页。
这个行为可以通过运行mapper看到。mapper是在O’Reilly的FTP站点上提供的文件中mis
c_programs里的一个示例程序。它是个可以快速测试mmap系统调用的简单工具。mapper
根据命令行选项映射一个文件中的只读部分,并把映射的区段输出到标准输出上。例如
,下面这个交互过程表明/dev/mem不映射位于64KB地址处的物理页(本例中的宿主机是
个PC,但在别的平台上结果应该是一样的):
(代码283)
remap_page_range对处理RAM的无能为力说明象scullp这样的设备不能简单地实现mmap,
因为它的设备内存是常规RAM,而不是I/O内存。
因为它的设备内存是常规RAM,而不是I/O内存。
有两个办法可以绕过remap_page_range对RAM的不可用性。一个是“糟糕”的办法,另一
个是干净的。
使用预定位
糟糕的办法要为你想映射到用户空间的页在mem_map[MAP_NR(page)]中置PG_reserved位
。这样就预定了这些页,而一旦预定了,remap_page_range就可以按期望工作了。设置
标志的代码很短很容易,但我不想在这儿给出来,因为另一个方法更有趣。不用说,不
释放页面之前,预定的位必须被清除。
有两个原因说明为什么这是个好办法。第一,被标为预定的页永远不会被内存管理所动
。核心在数据结构初始化之前系统引导时确定它们,因此不能用在任何其它用途上。而
另一方面,通过get_free_page,vmalloc或其它一些方式分配的页都是由内存子系统处理
的。即使2.0核心在你运行时预定额外的页并不崩溃,这样做可能在将来会产生问题。因
而是不鼓励的。不过你可以尝试这种快且脏的技术看看它是任何工作的。
预定页不是个好办法的第二个原因是被预定的页不被算做是整个系统内存的一部分,有
的用户在系统RAM发生变化时可能会很在意——用户经常留意空闲内存数量,而总的内存
量一般总和空闲内存一道显示。
实现nopage方法
实现nopage方法
将实际RAM映射到用户空间的一个较好的办法是用vm_ops->nopage来一次处理一个页面错
。作为scullp模块一部分的一个示例实现在第七章“把握内存”中介绍过。
scullp是面向页的字符设备。因为它是面向页的,所以可以在它的内存中实现mmap。实
现内存映射的代码用了一些以前在“Linux的内存管理”中介绍过的概念。
在查看代码之前,让我们看一下影响scullp中mmap实现的设计选择。
l 设备为模块更新使用计数
在卸载模块时为了避免发生问题,内存区域的open和close方法被实现去跟踪模块的使用
。
l 设备为页更新使用计数
这是为保证系统稳定是一个严格要求;不能更新这个计数将导致系统崩溃。每个页有其
自己的使用计数;当它降为零时,该页被插入到空闲页表。当一个活动映象被破坏掉时
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -