⭐ 欢迎来到虫虫下载站! | 📦 资源下载 📁 资源专辑 ℹ️ 关于我们
⭐ 虫虫下载站

📄 (ldd) ch13-mmap和dma(转载).txt

📁 献给ARM初学者
💻 TXT
📖 第 1 页 / 共 4 页
字号:
(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 + -