SEE MIPS RUN 第六章 内存管理与TLB
Posted freshui
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SEE MIPS RUN 第六章 内存管理与TLB相关的知识,希望对你有一定的参考价值。
内存管理与TLB
我们倾向于直接从最底层引入本书中的大部分主题进行探讨,对于一本关注计算机底层体系结构的书而言,这似乎是自然而然的。然而,为了说清楚内存管理硬件,我们得从MIPS R2000所寻求实现的unix风格的虚拟存储系统开始讲起。本章的后面我们还会讨论一下相同的硬件如何在其他环境下工作。
早期的MIPS CPU定位于支持运行在UNIX工作站与服务器上的应用程序,因此内存管理硬件被构想为一个最小化的能帮助BSD UNIX——一个经过完善设计并拥有充分多虚拟存储需求的操作系统的典型——提供内存管理功能的硬件。很明显的是,这些设计者们十分熟悉DEC VAX小型机,并且在从这种体系结构中获取了众多思路的同时,也摒弃了许多复杂设计。尤其是许多VAX使用微代码来解决的问题,在MIPS中被交由软件处理。
本章中我们将从MIPS的设计起点开始,面对着一个unix类型的操作系统以及它的虚存系统的众多需求。我们将会展示一下MIPS的硬件是如何满足这些需求的。结尾时,我们会讨论一下在不能像通常一样使用内存管理硬件的嵌入式系统中,您可以采取的几种使用方式。
内存地址转译硬件(下面我们将称其MMU,全称为memory management unit)有几类不同用途:
n 重定位(Relocation):程序的函数方法和预先声明的数据地址均在编译期间决定,MMU允许程序在任何物理地址运行。
n 为程序分配内存:MMU可以从物理内存里许多零散的页中创建连续的程序空间,使我们能从一个充满固定大小页面的池里分配内存。如果我们不停分配释放大小不一的内存块,就会碰上内存碎片问题:我们不得不止步于一个布满“小孤岛”的内存空间,无法满足对较大块内存的申请要求,哪怕此时所有的空闲空间之和是足够的。
n 隐藏和保护:用户级程序只能访问kuseg内存区域(较低的程序地址)内的数据。这类程序只能在操作系统所许可的内存区域中取得数据。
此外,每一页可以独立的指定为可写权限或者写保护权限;操作系统甚至可以停止一个意外的写覆盖代码空间的应用程序。
n 扩展地址空间: 有些CPU不能直接访问它们拥有的全部物理空间。尽管MIPS I 系列CPU是真正的32位体系结构,它们却布局了地址映射,使得未被映射的地址空间窗口kseg0和kseg1(它们不依赖MMU进行地址转换)落在了物理内存的开头的512M内。如果你要访问更高地址,则必须通过MMU。
n 内存映射对程序的适应化:在MMU的帮助下,你的程序能够去使用适合它的地址。同一段程序的许多份拷贝可能会同时运行在一个庞大的操作系统里,令它们去使用相同的程序地址变得更容易。
n 调页能力:程序可以好像已经得到它们所申请分配的所有资源一样正常的运行,而操作系统实际上只分配给它们当前所需的资源。访问未分配空间的程序会导致一个交由操作系统处理的异常(exception),操作系统此时才在这块内存中装入适当数据并令应用程序继续运行。
UNIX内存管理工作的本质是为了能运行众多不同的任务(即multitasking——多进程),并且每个任务各自拥有自己的内存空间。如果这个工作圆满完成,那么各任务的命运将彼此独立开来(操作系统自身也因此得以保护):一个任务自身崩溃或者错误的做某些事不会影响整个系统。显然,对一个使用分布终端来运行学生们程序的大学而言,这是一个很有用的特性;然而不仅如此,甚至是要求最严格的商业系统环境也需要能够在运行的同时支持实验软件或原型软件一并进行调试和测试。
MMU并不仅仅为了建立巨大而完备的虚拟存储系统,小的嵌入式程序也能在重定位和更有效的内存分配里受益。如果能把应用程序观念上的地址映射到任何可获得的物理地址,系统在不同时刻运行不同程序就会更加容易。
多进程和隔离不同进程地址空间一直都在向更小的计算机上移植,目前在个人电脑以及英特网服务器端都已经十分普通。
嵌入式应用中常常会明确的运用多进程机制,但几乎没有多少嵌入式操作系统使用隔离的地址空间。或许这归咎于这种机制在嵌入式CPU以及它们上面的操作系统上用处不大并且带来不稳定性,因而显得不那么重要。
MIPS这种如此之必要以致于导致在1986年时工作站CPU变的廉价起来的简单机制,或许也可以被证实跟90年代后期嵌入式系统的兴起有一定关系。甚至是很小的应用,也被迅速增长的代码大小所困扰,需要使用所有已知的手段来控制软件的复杂度;这种由MIPS首创的灵活的基于软件的方法看来能提供任何所需空间。仅仅几年前,CPU的厂商们在定位嵌入式市场时还很难确定MMU是否值得包括进去;然而到1997年,微软推出的无法在没有内存管理硬件的环境下运行的Windows/CE,已被视为针对嵌入式所面对的各种困难的一个成功解决方案。
6.1 大型计算机上的内存管理
或许从一个类似unix系统的内存管理系统的整个工作开始讨论是最容易的(选择unix作为研究是因为:尽管它体积庞大,却比PC上的操作系统简单的多)。在图6-1中展示了其典型特征。
6.1.1 基本的进程空间布局和保护
图6-1中最宽的分隔线是在低半部分——标明“用户程序可访问”的那部分——以及剩余部分之间的。程序的空间中的用户可访问部分就是我们在2.8节所描述的通常在MIPS内存映射中称为“kuseg”的部分。所有的高位地址内存都保留给操作系统。从操作系统的角度来看,地址低半部分是一个用户程序可以随心所欲使用的安全的“沙盒”(sandbox)。假如程序运行错误并且毁坏了自己所有的数据,其他程序并不用担心受到影响。
从应用程序的角度来看,这块区域可以随意用来创建独享的复杂数据结构来继续自己的工作。
在用户区域内部,也就是在“沙盒”的内部,操作系统给有需求的程序提供更多的栈空间(由于栈在暗中向下增长)。同时也提供一个系统调用,用来从一个以“预先声明数据区域”(declared data)最高地址为起始地址并且不断增长的地址空间——人们称之为“堆”(heap)——当中来获取更多数据空间。“堆”用来实现诸如malloc()这样的用于给应用程序提供大块额外内存的库函数。
用来构建堆和栈的内存块应该小到足以使系统节约内存,但同时也必须大到可以避免过多的系统调用或者访存异常的产生。不过,在每一次系统调用或访存异常时,操作系统会有机会监督应用程序的内存消耗。操作系统可以增强限制以确保应用程序不会获取过多的内存以致于威胁到系统中关键的运行活动。
在unix类型的系统中,进程在操作系统内核中拥有自己的识别符;为了确保应用程序只能做它们被允许的事情,绝大多数内核服务以特殊子函数的形式(即系统调用)提供出来,应用程序调用时也必须遵从一定的特殊规定。
图6.1
操作系统自己的代码和数据显然不能被用户空间的程序访问到。在某些系统里,这是通过把它们完全安置于一个隔离的地址空间内来完成的;在MIPS上运行的操作系统则与用户程序共享同一地址空间,当CPU运行在用户级权限下时,访问内核的空间是非法的,将会导致一个异常产生。
需要注意的是,尽管每个进程的用户空间会映射到专属于本进程的物理存储空间上去,操作系统的空间却往往是共享的。大部分的操作系统代码以及资源在所有进程看来位于相同的地址——操作系统内核内部是一个多线程单地址空间的——而每个进程的用户地址空间则位于专属自己的隔离空间。应用程序发出的系统调用在内核里的运行过程是完全可信赖的,而应用程序则根本无须被信任。
用户空间的有效部分是被分开的,栈位于空间顶端,而代码和静态编译的数据位于底部。这就使得栈可以向下增长(这是隐式的,由于程序的运行中函数参数的累积)而数据空间能够向上增长(这是显式的,由于程序调用了分配内存的库函数)。操作系统能够为栈或数据空间分配更多的内存并映射到合适的地址上去。
请注意,为了使程序能够使用庞大的数据空间,通常会让栈从用户空间所允许的最高地址开始向下增长。地址转换方案中必须要妥善应对这种在大跨度的范围内使用地址空间(在被使用空间中有一个巨大空洞)的地址映射特征。
实时系统中为了寻求效率和更多的共享函数,机制更加复杂化。大多数系统把应用程序的代码映射为“只读”(read-only),这意味着这些代码可以被许多进程安全的进行共享——许多进程运行同一应用程序的情况也很常见。
许多系统不仅仅共享整个应用程序,还可以共享通过库调用来访问的程序段(共享库)。目前我们还是先暂不讨论这所引发的另外一大堆问题吧。
6.1.2 把进程空间映射到物理内存
支持这个模型需要什么机制呢?
MIPS体系结构或多或少地要求程序(不管是应用程序还是内核方法)的地址空间在编译连接期间固定下来。这意味着应用程序在构建时不可能明确使用不同的地址——在我们希望运行同一应用程序的不同拷贝时也是如此。因此,当程序运行时,它的地址会映射到一个在程序装入时就已经由操作系统所固定下来的物理地址上。
尽管在进程上下文切换时更新所有的地址映射信息是可行的,但其效率相当之低。替代办法是:我们给每个进程一个编号(在unix里称作“进程ID”,但更准确的叫法应该是“地址空间ID”或者简称为“ASID”)。每个进程里的任何地址都在暗中被进程的ASID扩展后产生一个唯一的待转换地址。ASID需要在进程被新调度执行时装入CPU的一个寄存器,使得硬件可以来使用它。
映射机制还令操作系统能够把用户空间的不同部分区分对待:应用程序的某些空间(一般是代码部分)被映射为只读,而其它某些部分则可以暂不映射并且对其访问能引起“陷入”(trapped),这意味着一个胡乱运行的程序可以更早被停住。
进程地址空间中的内核部分通常是被所有进程共享的,而且这一部分绝大多数内容映射为常驻的操作系统代码和数据。由于这些代码连接为在这些地址运行,不需要灵活的映射机制,大部分MIPS上运行的内核会把它们的绝大部分代码置于这块在这一体系结构里具有固定映射的地址空间中。
6.1.3页映射最佳
为了映射地址人们尝试过很多特殊办法,通常使用“基地址/范围”二元组来保证地址的正确性。然而如果以提供给程序恰好其所需大小的内存的方式来进行内存映射,尽管这很明显为应用程序提供了最优服务,却会迅速导致可用内存变为零散的具有难以使用的大小的内存碎块。所有的实际系统都对内存以页(page,一种固定大小的内存块)进行映射。页通常是2的幂次大小,4K大小得到了压倒性优势的使用频率。
在4K的情况下,一个CPU地址可以简单的映射为这样:
“页内地址”(图中Address within page部分)的几位不需要转译,因此内存管理器件只需要去处理地址高位的转译,即把通常称作“虚页号”(图中Virtual page number,简称VPN)的部分转译为实际物理地址的高位(即physical frame number,或简称PFN,没人能想得起来为啥不叫PPN)。
6.1.4 我们真正想要的
映射机制必须使一个程序能断言某个地址在其自己的进程空间或地址空间内,并且能够高效的将其转换为真实的物理地址以访问内存。
一个好主意是使用一个含有整个空间内所有页的入口(entry)的表(即页表),每个入口包含这个页的正确物理地址。这很明显是个相当大的数据结构,因而不得不存放于主存之中。不过这带来两个严重问题。
第一,每次取出或存入数据我们都需要访问两次内存,就性能而言这显然是没什么好指望的。大概您已预见到这样一个答案:我们可以使用一个快存(cache)来存储这些入口,仅仅在我们未命中快存时才去访问常驻内存的表。由于每个快存的入口覆盖了4KB的内存空间,我们似乎可以就这样得到一块未命中率出奇低而自身又相当小的快存。(现在要介绍一下,快存很稀少而且有时被称作“查找缓存”(lookaside buffers),因此内存转译快存就被称作“转译查找缓存”(translation lookaside buffers)或简称为TLB;这个缩写更加常用)。
第二个问题是页表的大小;对一个划分为4KB大小页面的32位应用程序的空间,将产生100万个入口,这会占去将近4MB的内存。我们有必要找些办法让这个表小一点,否则就剩不下多少内存给程序使用了。
我们将在这样一个前提下讨论不同的解决方案:真实运行中的程序会在地址空间中留有巨大的地址空洞,如果我们能有一种方法,避免用物理内存来存储表中的这些空洞,情况看上去会好的多。
现在我们有解决办法了,本质上,这来自于DEC在VAX小型机上所使用的内存转译系统,它给绝大多数的并发体系结构带来了深远的影响。图6-2中对其做了概括。
图6.2
硬件的处理过程大致上是这样的:
n 一个虚地址被分割为两部分,低半部分地址位(通常为12位)不经转译直接通过,因此转译结果总是落在一个页内(通常4KB大小)。
n 高半部分地址位,也就是VPN,会在前面拼接上当前运行进程的ASID以形成一个独一无二的页首地址。
n 我们在TLB中查找是否有一个本页的转译项在里面。如果有,那么我们将得到对应的物理地址的高位,最终得到可用地址。TLB是一个做特殊用途的存储器件,可以运用各种有效的方法来匹配地址。它可以参考一个全局的标志位来在查找某些入口时忽略ASID位,因此这些TLB入口可以用来映射所有进程中的某一段共享的虚地址空间。
类似的,VPN也可以在存储的时候使用某些掩码位,使VPN中某些位在匹配过程中被排除在外,这使得TLB入口能够映射更大范围的虚地址。
在某些MIPS MMU中这两种特殊机制均被采纳。
n 通常在PFN中还存储了一些额外的位信息(flags)以用于控制哪些访问可以被允许——最明显的,允许读操作而不允许写操作。我们会在6.2节中讨论MIPS体系结构的标志位。
n 如果在TLB中入口匹配失败,那么系统必须定位或者分配一个适当的入口(使用常驻内存页表的相应信息)并将其装入TLB,然后再次进行一次转译过程
在VAX小型机中,这个过程是被微代码(microcode)所控制的,对程序员而言整个过程完全是自动进行的。
6.1.5 MIPS如此设计的起源
为了在尽可能少使用硬件的前提下提供一套与VAX相同的功能,MIPS的设计者们需要找些好办法。由微代码控制的TLB重装入(refill)是不能接受的,因此他们勇敢的迈出了一步:把这个工作交给软件来完成。
这意味着除了有一个寄存器用来存放当前的ASID,MMU器件仅仅是个TLB而已,也就是一个简单的高速、定长的转译表。系统软件可以(通常也就是如此)把TLB作为一个快存来面向常驻内存的页表,然而TLB硬件本身并不能把自己当作快存来使用,而只能这样:当某一个地址无法进行转译时,TLB会触发一个特殊的异常(TLB重装入异常)来引发调用软件程序。不过,TLB的细节设计和相应的控制寄存器上都作了十分周密的考虑,以帮助软件更加富有效率的运行。
6.2 MIPS TLB的特点
MIPS TLB通常都是在芯片上实现的:即使在快存命中的情形下,内存转译也这一步仍然必须进行,因此在机器上这是一个十分重要的“关键路径”(critical path)。这意味着它必须很小,尤其在早期那个年代,因此,把它的规模控制的很小就十分明智。
基本上这是块全相连的存储单元。每个入口都是一块拥有键值(key)域和数据域的关联存储器;当你提供某个键值后,由硬件来进行匹配并给出匹配成功的入口内的数据。通常全相连存储器效率很高但在硬件上过于浪费,而MIPS系列的TLB含有32到64个入口不等;这种规模的存储量在芯片设计中是相对容易处理的。
R4000风格的CPU至今都在使用这样一种TLB:每个入口内容被扩大为2倍以容纳2个各自映射独立物理页的连续的VPN。这种成对的入口仅增添了很少的硬件逻辑但却加倍了TLB可以装入的映射页,避免了对TLB的设计进行大幅度的调整。
您可以看到为何被称为“全相连”,这强调了所有的键值实际上是并行对输入值进行比较的。
图6.3
TLB的入口如图6-3中所示(您可以在后面的6.5节中找到其详细的编程信息)。TLB的键值包含了以下内容:
n VPN:虚地址的高位(即虚页地址)。在双入口TLBMIPS 内存执行预防