📄 (ldd) ch13-mmap和dma(转载).txt
字号:
(LDD) Ch13-MMAP和DMA(转载)
第十三章 MMAP和DMA
这一章介绍Linux内存管理和内存映射的奥秘。同时讲述设备驱动程序是如何使用“直接
内存访问”(DMA)的。尽管你可能反对,认为DMA更属于硬件处理而不是软件接口,但
我觉得与硬件控制比起来,它与内存管理更相关。
这一章比较高级;大多数驱动程序的作者并不需要太深入到系统内部。不过理解内存如
何工作可以帮助你在设计驱动程序时有效地利用系统的能力。
Linux中的内存管理
这一节不是描述操作系统中内存管理的理论,而是关注于这个理论在Linux实现中的主要
这一节不是描述操作系统中内存管理的理论,而是关注于这个理论在Linux实现中的主要
特征。本节主要提供一些信息,跳过它不会影响您理解后面一些更面向实现的主题。
页表
当一个程序查一个虚地址时,处理器将地址分成一些位域(bit field)。每个位域被用
来索引一个称做页表的数组,以获得要么下一个表的地址,要么是存有这个虚地址的物
理页的地址。
为了进行虚地址到物理地址的映射,Linux核心管理三级页表。开始这也许会显得有些奇
怪。正如大多数PC程序员所知道的,x86硬件只实现了两级页表。事实上,大多数Linux
支持的32位处理器实现两级,但不管怎样核心实现了三级。
在处理器无关的实现中使用三级,使得Linux可以同时支持两级和三级(如Alpha)的处
理器,而不必用大量的#ifdef语句把代码搅得一团糟。这种“保守编码”方式并不会给
核心在两级处理器上运行时带来额外的开销,因为实际上,编译器已经把没用的一级优
化掉了。
但是让我们看一会儿实现换页的数据结构。为了跟上讨论,你应该记住大多数用作内存
管理的数据都采用unsigned long的内部表示,因为它们所表示的地址不会再被复引用。
下述几条总结了Linux的三级实现,由图13-1示意:
l 一个“页目录(Page Directory,PGD)”是顶级页表。PGD是由pgd_t项所组成的数
组,每一项指向一个二级页表。每个进程都有它自己的页目录,你可以认为页目录是个
页对齐的pgd_t数组。
l 二级表被称做“中级页目录(Page Mid_level Directory)”或PMD。 PMD是一个
页对齐的pmd_t数组。每个pmd_t是个指向三级页表的指针。两级的处理器,如x86和spar
c_4c,没有物理PMD;它们将PMD声明为只有一个元素的数组,这个元素的值就是PMD本身
——马上我们将会看到C语言是如何处理这种情况以及编译器是如何把这一级优化掉的。
l 再下一级被简单地称为“页表(Page Table)”。同样地,它也是一个页对齐的数
组,每一项被称为“页表项(Page Table Entry)”。核心使用pte_t类型表示每一项。
pte_t包含数据页的物理地址。
上面提到的类型都在<asm/page.h>中定义,每个与换页相关的源文件都必须包含它。
核心在一般程序执行时并不需要为页表查寻操心,因为这是有硬件完成的。不过,核心
必须将事情组织好,硬件才能正常工作。它必须构造页表,并在处理器报告一个页面错
时(即当处理器需要的虚地址不在内存中时)查找页表,。
下面的符号被用来访问页表。<asm/page.h>和<asm/pgtable.h>必须被包含以使它们可以
被访问。
(Figure 13.1 Linux的三级页表)
PTRS_PER_PGD
PTRS_PER_PMD
PTRS_PER_PTE
每个页表的大小。两级处理器置PTRS_PER_PMD为1,以避免处理中级。
unsigned long pgd_bal(pgd_t pgd)
unsigned long pmd_val(pmd_t pmd)
unsigned long pte_val(pte_t pte)
这三个宏被用来从有类型数据项中获取无符号长整数值。这些宏通过在源码中使用严格
的数据类型有助于减小计算开销。
pgd_t *pgd_offset(struct mm_struct *mm,unsigned long address)
pmd_t *pmd_offset(pgd_t *dir,unsigned long address)
pmd_t *pmd_offset(pgd_t *dir,unsigned long address)
pte_t *pte_offset(pmd_t *dir,unsigned long address)
这些线入函数是用于获取与address相关联的pgd,pmd和pte项。页表查询从一个指向结
构mm_struct的指针开始。与当前进程内存映射相关联的指针是current->mm。指向核心
空间的指针由init_mm描述,它没有被引出到模块,因为它们不需要它。两级处理器定义
pmd_offset(dir,add)为(pmd_t* )dir,这样就把pmd折合在pgd上。扫描页表的函数总是
被声明为inline,而且编译器优化掉所有pmd查找。
unsigned long pte_page(pte_t pte)
这个函数从页表项中抽取物理页的地址。使用pte_val(pte)并不可行,因为微处理器使
用pte的低位存贮页的额外信息。这些位不是实际地址的一部分,而且需要使用pte_page
从页表中、抽取实际地址。
pte_present(pte_t pte)
这个宏返回布尔值表明数据页当前是否在内存中。这是访问pte低位的几个函数中最常用
的一个——这些低位被pte_page丢弃。有趣的是注意到不论物理页是否在内存中,页表
始终在(在当前的Linux实现中)。这简化了核心代码,因为pgd_offset及其它类似函数
从不失败;另一方面,即使一个有零“驻留存贮大小”的进程也在实际RAM中保留它的页
表。
表。
仅仅看看这些列出的函数不足以使你对Linux的内存管理算法熟悉起来;实际的内存管理
要复杂的多,而且还要处理其它一些繁杂的事,如高速缓存一致性。不过,上面列出的
函数足以给你一个关于页面管理实现的初步印象;你可以从核心源码的include/asm和mm
子树中得到更好的信息。
虚拟内存区域
尽管换页位于内存管理的最低层,你在能有效地使用计算机资源之前还需要一些别的知
识。核心需要一种更高级的机制处理进程看到它的内存方式。这种机制在Linux中以“虚
拟内存区域的方式实现,我称之为“区域”或“VMA”。
一个区域是在一个进程的虚存中的一个同质区间,一个具有同样许可标志的地址的连续
范围。它与“段”的概念松散对应,尽管最好还是将其描述为“具有自己属性的内存对
象”。一个进程的内存映象由下面组成:一个程序代码(正文)区域;一个数据、BSS(
未初始化的数据)和栈区域;以及每个活动的内存映射的区域。一个进程的内存区域可
以通过查看/proc/pid/maps看到。/proc/self是/proc/pid的特殊情况,它总是指向当前
进程,做为一个例子,下面是三个不同的内存映象,我在#字号后面加了一些短的注释:
(代码271)
每一行的域为:
每一行的域为:
start_end perm offset major:minor inode
perm代表一个位掩码包括读、写和执行许可;它表示对属于这个区域的页,允许进程做
什么。这个域的最后一个字符要么是p表示私有的,要么是s表示共享的。
/proc/*/maps的每个域对应着结构vm_area_struct的一个域,我们将在下面描述这个结
构。
实现mmap的方法的驱动程序需要填充在映射设备的进程地址空间中的一个VMA结构。因此
,驱动程序的作者对VMA应该有个最起码的理解以便使用它们。
让我们看一下结构vm_area_struct(在<linux/mm.h>)中最重要的几个域。这些域可能
在设备驱动程序的mmap实现中被用到。注意核心维护VMA的列表和树以优化区域查找,vm
_area_struct的几个域被用来维护这个组织。VMA不能按照驱动程序的意愿被产生,不然
结构将会崩溃。VMA的几个主要域如下:
unsigned long vm_start
unsigned long vm_end
一个VMA描述的虚地址介于vma->vm_start和vma->vm_end之间。这两个域是/pro/*/maps
一个VMA描述的虚地址介于vma->vm_start和vma->vm_end之间。这两个域是/pro/*/maps
中显示的最先两个域。
struct inode *vm_inode
如果这个区域与一个inode相关联(如一个磁盘文件或一个设备节点),这个域是指向这
个inode的指针。不然,它为NULL。
unsigned long vm_offset
inode中这个区域的偏移量。当一个文件或设备被映射时,这是映射到这个区域的第一个
字节的文件的位置(filp->f_ops)。
struct vm_operations_struct *vm_ops
vma->vm_ops说明这个内存区域是一个核心“对象”,就象我们在本书中一直在用的结构
file。这个区域声明在其内容上操作的“方法”,这个域就是用来列出这些方法。
和结构vm_area_struct一样,vm_operations_struct在<linux/mm.h>中定义;它包括了
列在下面的操作。这些操作是处理进程内存需要的所有操作,它们以被声明的顺序列出
。列出的原型是2.0的,与1.2.13的区别在每一项中都有描述。在本章的后面,这些函数
中的部分会被实现,那时会更完全地加以描述。
void(*open)(struct vm_area_struct *vma);
在核心生成一个VMA后,它就把它打开。当一个区域被复制时,孩子从父亲那里继承它的
操作,就区域用vm->open打开。例如,当fork将存在进程的区域复制到新的进程时,vm_
ops->open被调用以打开所有的映象。另一方面,只要mmap执行,区域在file->f_ops->m
map被调用前被产生,此时不调用vm_ops->open。
void(*close)(struct vm_area_struct *vma);
当一个区域被销毁时,核心调用它的close操作。注意VMA没有相关的使用计数;区域只
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -