📄 (ldd) ch13-mmap和dma(转载).htm
字号:
<DIV align=center><A href="mailto:joyfire@sina.com"><FONT
color=#ffffff>联系</FONT></A></DIV></TD></TR></TBODY></TABLE></TD></TR></TBODY></TABLE>
<TABLE borderColor=#666666 cellPadding=2 width="90%" align=center border=2>
<TBODY>
<TR>
<TD bgColor=#000000>
<P align=center><A href="http://joyfire.net/lsdp/index.htm"><FONT
color=#ffffff size=2>目录页</FONT></A> | <A
href="http://joyfire.net/lsdp/15.htm"><FONT color=#ffffff
size=2>上一页</FONT></A> | <A href="http://joyfire.net/lsdp/17.htm"><FONT
color=#ffffff size=2>下一页</FONT></A></P>
<P align=center><FONT face=黑体 color=#ffffff size=6>(LDD) Ch13-MMAP和DMA(转载)
</FONT></P><SPAN style="LINE-HEIGHT: 1; LETTER-SPACING: 0pt"><FONT
color=#ffffff size=3>
<P>发信人: Altmayer (alt), 信区: GNULinux<BR>标 题: (LDD) Ch13-MMAP和DMA(转载)<BR>发信站: 饮水思源 (2001年12月13日08:57:55 星期四), 站内信件<BR> <BR>【 以下文字转载自 <FONT
color=#00ff00>UNIXpost </FONT>讨论区 】<BR>【 原文由<FONT
color=#00ff00> altmayer.bbs@bbs.nju.edu.cn,</FONT> 所发表 】<BR> <BR>【 以下文字转载自 <FONT
color=#00ff00>altmayer </FONT>的信箱 】<BR> <BR>第十三章 MMAP和DMA<BR> <BR> <BR> <BR>这一章介绍Linux内存管理和内存映射的奥秘。同时讲述设备驱动程序是如何使用“直接<BR>内存访问”(DMA)的。尽管你可能反对,认为DMA更属于硬件处理而不是软件接口,但<BR>我觉得与硬件控制比起来,它与内存管理更相关。<BR> <BR>这一章比较高级;大多数驱动程序的作者并不需要太深入到系统内部。不过理解内存如<BR>何工作可以帮助你在设计驱动程序时有效地利用系统的能力。<BR> <BR>Linux中的内存管理<BR> <BR>这一节不是描述操作系统中内存管理的理论,而是关注于这个理论在Linux实现中的主要<BR></P></FONT><FONT
color=#ffffff size=3>
<P>这一节不是描述操作系统中内存管理的理论,而是关注于这个理论在Linux实现中的主要<BR>特征。本节主要提供一些信息,跳过它不会影响您理解后面一些更面向实现的主题。<BR> <BR>页表<BR> <BR>当一个程序查一个虚地址时,处理器将地址分成一些位域(bit field)。每个位域被用<BR>来索引一个称做页表的数组,以获得要么下一个表的地址,要么是存有这个虚地址的物<BR>理页的地址。<BR> <BR>为了进行虚地址到物理地址的映射,Linux核心管理三级页表。开始这也许会显得有些奇<BR>怪。正如大多数PC程序员所知道的,x86硬件只实现了两级页表。事实上,大多数Linux<BR>支持的32位处理器实现两级,但不管怎样核心实现了三级。<BR> <BR>在处理器无关的实现中使用三级,使得Linux可以同时支持两级和三级(如Alpha)的处<BR>理器,而不必用大量的#ifdef语句把代码搅得一团糟。这种“保守编码”方式并不会给<BR>核心在两级处理器上运行时带来额外的开销,因为实际上,编译器已经把没用的一级优<BR>化掉了。<BR> <BR>但是让我们看一会儿实现换页的数据结构。为了跟上讨论,你应该记住大多数用作内存<BR>管理的数据都采用unsigned long的内部表示,因为它们所表示的地址不会再被复引用。<BR> <BR>下述几条总结了Linux的三级实现,由图13-1示意:<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>l 一个“页目录(Page Directory,PGD)”是顶级页表。PGD是由pgd_t项所组成的数<BR>组,每一项指向一个二级页表。每个进程都有它自己的页目录,你可以认为页目录是个<BR>页对齐的pgd_t数组。<BR> <BR>l 二级表被称做“中级页目录(Page Mid_level Directory)”或PMD。 PMD是一个<BR>页对齐的pmd_t数组。每个pmd_t是个指向三级页表的指针。两级的处理器,如x86和spar<BR>c_4c,没有物理PMD;它们将PMD声明为只有一个元素的数组,这个元素的值就是PMD本身<BR>——马上我们将会看到C语言是如何处理这种情况以及编译器是如何把这一级优化掉的。<BR> <BR>l 再下一级被简单地称为“页表(Page Table)”。同样地,它也是一个页对齐的数<BR>组,每一项被称为“页表项(Page Table Entry)”。核心使用pte_t类型表示每一项。<BR>pte_t包含数据页的物理地址。<BR> <BR>上面提到的类型都在<asm/page.h>中定义,每个与换页相关的源文件都必须包含它。<BR> <BR>核心在一般程序执行时并不需要为页表查寻操心,因为这是有硬件完成的。不过,核心<BR>必须将事情组织好,硬件才能正常工作。它必须构造页表,并在处理器报告一个页面错<BR>时(即当处理器需要的虚地址不在内存中时)查找页表,。<BR> <BR>下面的符号被用来访问页表。<asm/page.h>和<asm/pgtable.h>必须被包含以使它们可以<BR>被访问。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>(Figure 13.1 Linux的三级页表)<BR> <BR>PTRS_PER_PGD<BR> <BR>PTRS_PER_PMD<BR> <BR>PTRS_PER_PTE<BR> <BR>每个页表的大小。两级处理器置PTRS_PER_PMD为1,以避免处理中级。<BR> <BR>unsigned long pgd_bal(pgd_t pgd)<BR> <BR>unsigned long pmd_val(pmd_t pmd)<BR> <BR>unsigned long pte_val(pte_t pte)<BR> <BR>这三个宏被用来从有类型数据项中获取无符号长整数值。这些宏通过在源码中使用严格<BR>的数据类型有助于减小计算开销。<BR> <BR>pgd_t *pgd_offset(struct mm_struct *mm,unsigned long address)<BR> <BR>pmd_t *pmd_offset(pgd_t *dir,unsigned long address)<BR></P></FONT><FONT
color=#ffffff size=3>
<P>pmd_t *pmd_offset(pgd_t *dir,unsigned long address)<BR> <BR>pte_t *pte_offset(pmd_t *dir,unsigned long address)<BR> <BR>这些线入函数是用于获取与address相关联的pgd,pmd和pte项。页表查询从一个指向结<BR>构mm_struct的指针开始。与当前进程内存映射相关联的指针是current->mm。指向核心<BR>空间的指针由init_mm描述,它没有被引出到模块,因为它们不需要它。两级处理器定义<BR>pmd_offset(dir,add)为(pmd_t* )dir,这样就把pmd折合在pgd上。扫描页表的函数总是<BR>被声明为inline,而且编译器优化掉所有pmd查找。<BR> <BR>unsigned long pte_page(pte_t pte)<BR> <BR>这个函数从页表项中抽取物理页的地址。使用pte_val(pte)并不可行,因为微处理器使<BR>用pte的低位存贮页的额外信息。这些位不是实际地址的一部分,而且需要使用pte_page<BR>从页表中、抽取实际地址。<BR> <BR>pte_present(pte_t pte)<BR> <BR>这个宏返回布尔值表明数据页当前是否在内存中。这是访问pte低位的几个函数中最常用<BR>的一个——这些低位被pte_page丢弃。有趣的是注意到不论物理页是否在内存中,页表<BR>始终在(在当前的Linux实现中)。这简化了核心代码,因为pgd_offset及其它类似函数<BR>从不失败;另一方面,即使一个有零“驻留存贮大小”的进程也在实际RAM中保留它的页<BR>表。<BR></P></FONT><FONT
color=#ffffff size=3>
<P>表。<BR> <BR>仅仅看看这些列出的函数不足以使你对Linux的内存管理算法熟悉起来;实际的内存管理<BR>要复杂的多,而且还要处理其它一些繁杂的事,如高速缓存一致性。不过,上面列出的<BR>函数足以给你一个关于页面管理实现的初步印象;你可以从核心源码的include/asm和mm<BR>子树中得到更好的信息。<BR> <BR>虚拟内存区域<BR> <BR>尽管换页位于内存管理的最低层,你在能有效地使用计算机资源之前还需要一些别的知<BR>识。核心需要一种更高级的机制处理进程看到它的内存方式。这种机制在Linux中以“虚<BR>拟内存区域的方式实现,我称之为“区域”或“VMA”。<BR> <BR>一个区域是在一个进程的虚存中的一个同质区间,一个具有同样许可标志的地址的连续<BR>范围。它与“段”的概念松散对应,尽管最好还是将其描述为“具有自己属性的内存对<BR>象”。一个进程的内存映象由下面组成:一个程序代码(正文)区域;一个数据、BSS(<BR>未初始化的数据)和栈区域;以及每个活动的内存映射的区域。一个进程的内存区域可<BR>以通过查看/proc/pid/maps看到。/proc/self是/proc/pid的特殊情况,它总是指向当前<BR>进程,做为一个例子,下面是三个不同的内存映象,我在#字号后面加了一些短的注释:<BR> <BR>(代码271)<BR> <BR>每一行的域为:<BR></P></FONT><FONT
color=#ffffff size=3>
<P>每一行的域为:<BR> <BR>start_end perm offset major:minor inode<BR> <BR>perm代表一个位掩码包括读、写和执行许可;它表示对属于这个区域的页,允许进程做<BR>什么。这个域的最后一个字符要么是p表示私有的,要么是s表示共享的。<BR> <BR>/proc/*/maps的每个域对应着结构vm_area_struct的一个域,我们将在下面描述这个结<BR>构。<BR> <BR>实现mmap的方法的驱动程序需要填充在映射设备的进程地址空间中的一个VMA结构。因此<BR>,驱动程序的作者对VMA应该有个最起码的理解以便使用它们。<BR> <BR>让我们看一下结构vm_area_struct(在<linux/mm.h>)中最重要的几个域。这些域可能<BR>在设备驱动程序的mmap实现中被用到。注意核心维护VMA的列表和树以优化区域查找,vm<BR>_area_struct的几个域被用来维护这个组织。VMA不能按照驱动程序的意愿被产生,不然<BR>结构将会崩溃。VMA的几个主要域如下:<BR> <BR>unsigned long vm_start<BR> <BR>unsigned long vm_end<BR> <BR>一个VMA描述的虚地址介于vma->vm_start和vma->vm_end之间。这两个域是/pro/*/maps<BR></P></FONT><FONT
color=#ffffff size=3>
<P>一个VMA描述的虚地址介于vma->vm_start和vma->vm_end之间。这两个域是/pro/*/maps<BR>中显示的最先两个域。<BR> <BR>struct inode *vm_inode<BR> <BR>如果这个区域与一个inode相关联(如一个磁盘文件或一个设备节点),这个域是指向这<BR>个inode的指针。不然,它为NULL。<BR> <BR>unsigned long vm_offset<BR> <BR>inode中这个区域的偏移量。当一个文件或设备被映射时,这是映射到这个区域的第一个<BR>字节的文件的位置(filp->f_ops)。<BR> <BR>struct vm_operations_struct *vm_ops<BR> <BR>vma->vm_ops说明这个内存区域是一个核心“对象”,就象我们在本书中一直在用的结构<BR>file。这个区域声明在其内容上操作的“方法”,这个域就是用来列出这些方法。<BR> <BR>和结构vm_area_struct一样,vm_operations_struct在<linux/mm.h>中定义;它包括了<BR>列在下面的操作。这些操作是处理进程内存需要的所有操作,它们以被声明的顺序列出<BR>。列出的原型是2.0的,与1.2.13的区别在每一项中都有描述。在本章的后面,这些函数<BR>中的部分会被实现,那时会更完全地加以描述。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>void(*open)(struct vm_area_struct *vma);<BR> <BR>在核心生成一个VMA后,它就把它打开。当一个区域被复制时,孩子从父亲那里继承它的<BR>操作,就区域用vm->open打开。例如,当fork将存在进程的区域复制到新的进程时,vm_<BR>ops->open被调用以打开所有的映象。另一方面,只要mmap执行,区域在file->f_ops->m<BR>map被调用前被产生,此时不调用vm_ops->open。<BR> <BR>void(*close)(struct vm_area_struct *vma);<BR> <BR>当一个区域被销毁时,核心调用它的close操作。注意VMA没有相关的使用计数;区域只<BR>被打开和关闭一次。<BR> <BR>void(*unmap)(struct vm_area_struct *vma,unsigned long addr,size_t len);<BR> <BR>核心调用这个方法取消一个区域的部分或全部映射。如果整个区域的映射被取消,核心<BR>在vm_ops->unmap返回后立即调用vm_ops->close。<BR> <BR>void (*protect)(struct vm_area_struct *vma,unsigned long,size_t,unsigned int<BR>new prot);<BR> <BR>当前未被使用。许可(保护)位的处理并不依赖于区域本身。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>int(*sync)(struct vm_area_struct *vma,unsigned long,size_t,unsigned int<BR>flags);<BR> <BR>这个方法被msync系统调用以将一个脏的内存区段保存到存贮介质上。如果成功则返回值<BR>为0 ,如果有错,则返回一个负数。核心版本1.2让这个方法返回void,因为这个函数不<BR>被认为会失败。<BR> <BR>void(*advise)(struct vm_area_struct *vma,unsigned long,size_t,unsigned int<BR>advise);<BR> <BR>当前未被使用。<BR> <BR>unsigned long(*nopage)(struct vm_area_struct *vma,unsigned long address,int<BR>write_access);<BR> <BR>当一进程试图访问属于另一个有效VMA的某页,而该页当前不在内存时,nopage方法就会<BR>被调用,如果它为相关区域定义。这个方法返回该页的(物理)地址。如果这个方法不<BR>为这个区域所定义,核心会分配一个空页。通常,驱动程序并不实现nopage,因为被一<BR>个驱动程序映射的区段往往被完全映射到系统物理地址。核心版本1.2的nopage具有一个<BR>不同的原型和不同的含义。第三个参数write_access被当做“不共享”——一个非零值<BR>意味着该页必须被当前进程所有,而零则表示共享是可能的。<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>unsigned long(*wppage)(struct vm_area_struct *vma,unsigned long<BR>address,unsigned long page);<BR> <BR>这个方法处理“写保护”页面错,但目前不被使用。核心处理所有不调用区域特定的回<BR>调函数却往一个被保护的页面上写的企图。写保护被用来实现“写时拷贝(copy_on_wri<BR>te)”。一个私有的页可以被不同进程所共享,直到其中一个进程试图写它时。当这种<BR>情况发生时,页面被克隆,进程向自己的页拷贝上写。如果整个区域被称为只读,会有<BR>一SIGSEGV信息被发送给进程,写时拷贝就未能完成。<BR> <BR>int (*swapout)(struct vm_area_struct *vma,unsigned long offset,pte_t<BR>*page_table);<BR> <BR>这个方法被用来从交换空间取得一页。参数offset是相对区域而言(与上面swapout一样<BR>),而entry是页面的当前pte——如果swapout在这一项中保存了一些信息,那么现在就<BR>可以用这些信息来取得该页。<BR> <BR>一般说来,驱动程序并不需要去实现swapout或swapin,因为驱动程序通常映射I/O内存<BR>,而不是常规内存。I/O页是一些象访问内存一样访问的物理地址,但被映射到设备硬件<BR>而不是RAM上。I/O内存区段或者被标记为“保留”,或者居于物理内存之上,因此它们<BR>从不被换出—交换I/O内存没什么实际意义。<BR> <BR>内存映象<BR></P></FONT><FONT
color=#ffffff size=3>
<P>内存映象<BR> <BR>在Linux中还有与内存管理相关的第三个数据结构。VMA和页表组织虚拟地址空间,而物<BR>理地址空间则由内存映象概括。<BR> <BR>核心需要物理内存当前使用情况的一个描述。由于内存可以被看作是页面数组,因此这<BR>个信息也可以组织为一个数组。如果你需要其页面的信息,你就用其物理地址去访问内<BR>存映象。下面就是核心代码用来访问内存映象的一些符号:<BR> <BR>typedef struct {/*…*/} mem_map_t<BR> <BR>extern mem_map_t mem_map[];<BR> <BR>映象本身是mem_map_t的一个数组。系统中的每个物理页,包括核心代码和核心数据,都<BR>在mem_map中有一项。<BR> <BR>PAGE_OFFSET<BR> <BR>这个宏表示由物理地址映射到的核心地址空间中的虚地址。PAGE_OFFSET在任何用到“物<BR>理”地址的地方都必须要考虑。核心认为的物理地址实际上是一个虚拟地址,从实际物<BR>理地址偏移PAGE_OFFSET——这个实际物理地址是在CPU外的电气地址线使用。在Linux2.<BR>0.x中, PAGE_OFFSET在PC上都是零,在大多数其它平台上都不是零。2.1.0版修改了PC<BR>上的实现,所以它现在也使用偏移映射。如果考虑到核心代码,将物理空间映射到高的<BR></P></FONT><FONT
color=#ffffff size=3>
<P>上的实现,所以它现在也使用偏移映射。如果考虑到核心代码,将物理空间映射到高的<BR>虚拟地址有一些好处,但这已经超出了本书的范围。<BR> <BR>int MAP_NR(addr)<BR> <BR>当程序需要访问一个内存映象时,MAP_NR返回在与addr关联的mem_map数组中的索引。参<BR>数addr可以是unsigned long,也可以是一个指针。因为这个宏被几个关键的内存管理函<BR>数使用多次,所以它不进行addr的有效性检查;调用代码在必要的时候必须自己进行检<BR>查。<BR> <BR>((nr<<PAGE_SHIFT)+PAGE_OFFSET)<BR> <BR>没有标准化的函数或者宏可以将一个映象号转译为一个物理地址。如果你需要MAP_NR的<BR>逆函数,这个语句可以使用。<BR> <BR>内存映象是用来为每个内存页维护一些低级信息。在核心开发过程中,内存映象结构的<BR>准确定义变过几次,你不必了解细节,因为驱动程序不期望查看映象内部。<BR> <BR>不过,如果你对了解页面管理的内部感兴趣的话,头文件<linux/mm.h>含有一大段注释<BR>解释mem_map_t域的含义。<BR> <BR>mmap设备操作<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>内存映象是现代Unix系统中最有趣的特征之一。至于驱动程序,内存映射可以提供用户<BR>程序对设备内存的直接访问。<BR> <BR>例如,一个简单ISA抓图器将图象数据保存在它自己的内存中,或者在640KB-1KB地址范<BR>围,或者在“ISA洞”(指14MB-16MB之间的范围参见第8章“硬件管理”中“访问设备板<BR>子上的内存”一节)中。<BR> <BR>将图象数据复制到常规(并且更快)RAM中是不定期抓图的合适的方法,但如果用户程序<BR>需要经常性地访问当前图象,使用mmap方法将更合适。<BR> <BR>映射一个设备的意思是使用户空间的一段地址空间关联到设备内存上。当程序读写指定<BR>的地址范围时,它实际上是在访问设备。<BR> <BR>正如你所怀疑的,并不是每个设备都适合mmap概念;例如,对于串口或其它面向流的设<BR>备来说它的确没有意义。mmap的另一个限制是映射是以PAGE_SIZE为单位的。核心只能在<BR>页表一级处置虚地址,因此,被映射的区域必须是PAGE_SIZE的整数倍,而且居于页对齐<BR>的物理内存。核心通过使一个区段稍微大一点儿的办法解决了页面粒度问题。对齐的问<BR>题通过使用vma->vm_offset来处理,但这对于驱动程序并不可行——映射一个设备简化<BR>为访问物理页,它必须是页对齐的。<BR> <BR>这些限制对驱动程序来说并不是很大的问题,因为不管怎样,访问设备的程序是设备相<BR>关的。它知道如何使得被映射的内存区段有意义,因此页对齐不是一个问题。当你ISA板<BR></P></FONT><FONT
color=#ffffff size=3>
<P>关的。它知道如何使得被映射的内存区段有意义,因此页对齐不是一个问题。当你ISA板<BR>子插到一个Alpha机器上时,有一个更大的限制,因为ISA内存是以8位、16位或32位项的<BR>散布集合被访问的,没有从ISA地址到Alpha地址的直接映射。在这种情况下,你根本不<BR>能使用mmap。不能进行ISA地址到Alpha地址的直接映射归因于两种系统数据传送规范的<BR>不兼容。Alpha只能进行32位和64位的内存访问,而ISA只能进行8位和16位的传送,没有<BR>办法透明地从一个协议映射到另一个。结果是你根本不能对插在Alpha计算机的ISA板子<BR>使用mmap。<BR> <BR>当可行的时候,使用mmap有一些好处。例如,一个类似于X服务器的程序从显存中传送大<BR>量的数据;把图形显示映射到用户空间与lseek/write实现相比,显著地改善了吞吐率。<BR>另一个例子是程序控制PCI设备。大多数PCI外围设备都将它们的控制寄存映射到内存地<BR>址上,一个请求应用更喜欢能直接访问寄存器,而不是反复调用ioctl来完成任务。<BR> <BR>mmap方法是file_oprations结构的一部分,在mmap系统调用被发出时调用。在调用实际<BR>方法之前,核心用mmap完成了很多工作,因此,这个方法的原型与系统调用很不一样。<BR>这与其它调用如ioctl和select不同,它们在被调用之前核心并不做太多的工作。<BR> <BR>系统调用如下声明(在mmap(2)手册中有描述):<BR> <BR>mmap(caddr_t,size_t len,int prot,int flags,int fd,off_t offset)<BR> <BR>另一方面,文件操作如下声明:<BR> <BR></P></FONT><FONT
color=#ffffff size=3>
<P><BR>int (*mmap)(struct inode*inode,struct file*filp,struct vm_area_struct<BR>*vma);<BR> <BR>方法中inode和filp参数与第三章“字符设备驱动程序”中介绍的一样。vma会有用以访<BR>问设备的虚拟地址范围的信息。这样,驱动程序只需为这个地址范围构造合适的页表:<BR>如果需要,用一组新的操作代替vma->vm_ops。<BR> <BR>一个简单的实现<BR> <BR>设备驱动程序的大多数mmap实现对居于周边设备上的某些I/O内存进行线性的映射。/dev<BR>/mem和/dev/audio都是这类重映射的例子。下面的代码来自drivers/char/mem.c,显示<BR>了在一个被称为simple(Simple Implementation Mapping Pages with Little<BR>Enthusiasm)的典型模块中这个任务是如何完成的:<BR> <BR>(代码277)<BR> <BR>很清楚,操作的核心由remap_page_range完成,它被引出到模块化的驱动程序,因为它<BR>做了大多数映射需要做的工作。<BR> <BR>维护使用计数<BR> <BR>上面给出的实现的主要问题在于驱动程序没有维护一个与被映射区域的连接。这对/dev/<BR></P></FONT><FONT
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -