第十章、虚拟存储器
一个系统中的进程是与其他进程共享CPU和主存资源。存储器很容易被破坏,如果某个进程不小心写了另一个进程使用的存储器,那么进程可能以某种完全和程序无关的令人迷惑的方式失败。为了更加有效地管理存储器并且少出错,现代系统提供了一种对主存的抽象概念,叫做虚拟存储器(是硬件异常、硬件地址翻译、主存、磁盘文件盒内核软件的完美交互,它为每个进程提供了一个大的一致的、私有地址空间)保护了每个进程的地址空间不被其他进程破坏。它成功的一个主要原因是因为它是沉默地,自动地工作的,不需要应用程序员的任何干涉。
计算机系统的主存被组织成一个由M个连续的字节大小的单元组成的数组。每个字节都有一个唯一的物理地址(PA)。
当CPU执行加载指令时,它会产生一个有效的物理地址,通过存储器总线,把它传递给主存。
根据虚拟寻址,CPU通过生成一个虚拟地址(VA)来访问主存,这个虚拟地址在被送到存储器之前先转换成适当的物理地址。将一个虚拟地址转换为物理地址的任务叫做地址翻译。
地址空间是一个非负整数地址的有序集合。如果地址空间中的整数是连续的,那么我们说它是一个线性地址空间。
一个地址空间的大小是由表示最大地址所需要的位数来描述的。
一个系统换由一个物理地址空间,它与系统中物理存储器的M个字节相对应
地址空间很重要的,因为它清楚地区分了数据对象(字节)和它们的属性(地址)。一旦我们认识了这种区别,那么我们可以允许每个数据对象有多个独立的地址,,其中每个地址都选自一个不同的地址空间,这就是虚拟存储器的基本思想,主存中的每一个字节都有一个选自虚拟地址空间的虚拟地址,和一个选自物理地址空间的物理地址。
物理存储器被分割为物理页,大小也为P字节(物理页也被称为页帧)
在任意时刻,虚拟页面的集合都有三个不相交的子集;1、未分配的 2、缓存的 3、未缓存的
DRAM比 SRAM要慢大约10倍,而磁盘要比DRAM慢大约100000多倍。
页表:虚拟存储器系统必须有某种方法来判定一个虚拟页是否存放在DRAM中的某个地方,如果是,系统换必须确定这个虚拟页存放在哪个物理页中,如果不命中,系统必须判断这个虚拟页存放在磁盘的哪个位置,在物理存储器中选择一个牺牲页,并将虚拟页从磁盘拷贝到DRAM中,替换这个牺牲页。
缺页:在虚拟存储器的习惯说法中,DRAM缓存不命中称为缺页。
了解虚拟存储器概念后,我们会认为不命中处罚大,我们会担心页面调度会破坏程序性能,实际上,虚拟存储器工作的相当好,主要归功于局部性。
独立的地址空间允许每个进程为它的存储器映像使用相同的基本格式,而不管代码和数据实际存放在物理存储器的何处。
一般而言,每个进程都有自己私有的代码、数据、堆以及栈区域,是不和其他进程共享的。但是独立地址空间为操作系统提供了一个管理用户进程和操作系统自身之间共享的一致机制。
虚拟存储器为向用户进程提供了一个简单的分配额外存储器的机制。(当一个运行在用户进程中的程序要求额外的堆空间时,操作系统分配一个适当数字(例如K)个连续的虚拟存储器页面,并且把他们映射到物理存储器中任意位置的K个任意的物理页面。)
虚拟存储器也使加载执行文件和已共享目标文件到存储器中变的容易。
有趣的一点是,加载器从不真正到底从磁盘中拷贝任何数据到存储器中。当每个页面第一次被引用时,虚拟存储器系统将自动并按需地把数据从磁盘上调入到存储器。
映射一个连续虚拟页面的集合到任意一个文件中的任意一个位置的概念叫做存储器映射。
虚拟存储器作为存储器保护的工具:任何现代计算机系统必须为操作系统提供手段来控制对存储器系统的访问,不允许用户进程修改它的只读文本段,而且不允许它读或修改任何内核中的代码和数据结构。不允许它读或者写其他进程的私有存储器,而且不允许它修改任何与其它进程共享的虚拟页面,除非所有的共享者都显示地允许它这么做。
用来压缩页表的常用方法是使用层次结构的页表。
分配器的要求和目标:1、处理任意请求序列。2、立即响应请求。3、只使用堆。4、对齐块(对齐要求)5、不修改已分配的块
造成堆利用率很低的主要原因是一种称为碎片的现象。分为内部碎片和外部碎片。当有未使用的存储器但不能用来满足分配请求时,就发生这种现象。
内部碎片:是在一个已分配块比有效载荷大时发生的,很多原因都可能造成这个问题。
外部碎片是当空闲存储器合计起来足够满足一个分配请求,但是没有一个单独的空闲块足够大可以来处理这个请求时发生的。
外部碎片比内部碎片的量化要困难的多,外部碎片还不可能测量。因为它不仅取决于以前请求的模式和分配器的实现方式,还取决于将来请求的模式。
防止分配的块:当一个应用请求一个K字节的块时,非配器搜索空间链表,查找一个足够大,可以放置所请求块的空闲块。分配器执行这种方式是由放置策略确定的。
首次适配的有点是它趋向于将大的空闲块保留在链表的后面,缺点是它趋向于在靠近链表起始处留下小空闲的“碎片”,这就增加了对较大块的搜索时间。
分割空闲块:一旦分配器找到一个匹配的空闲块,它就必须做另一个策略决定,那就是分配这个空闲块多少空间,一个选择用整个空闲块,虽然这种方法简单快捷,但是容易造成内部碎片。如果放置策略趋向于产生好的匹配,那么额外的内部碎片也是可以接受的。
如果分配器不能为请求块找到合适的空闲块,怎么办呢?一个选择是通过合并那些在存储器中物理上相邻的空闲块来创建一些更大的空闲块,然而这样还是不能生成一个足够大的块,或者空闲块已经最大程度地合并了,那么分配器就会向内核请求额外的堆存储器。
合并空间块:当分配器释放一个已分配块时,可能有其他空闲块与这个新释放的空闲块相邻,这些相邻的空闲块可能引起假碎片,这里有许多空闲块被分割成晓得,无法使用的空闲块。
使用双向链表,释放一个块的时间可以是线性的,也可能是个常数。一种方法是先进后出的顺序维护链表,另一种是按照地址顺序来维护链表,其中表中每一块的地址都小于它祖先的地址。
一个使用单向空闲块链接表的分配器需要与空闲块数量成线性关系的时间来分配块。一种流行的减少分配时间的方法,通常称为分离存储。
一般的思路是将所有可能的块大小分成一些等价类,也叫做大小类。
分配器维护着一个空闲链表数组,每个大小类一个空闲链表,按照大小的升序排列。当分配器需要一个大小为n的块时,它就搜索相应的空闲链表。如果它找不到合适的的块与之匹配,它就搜索下一个链表,以此类推。
使用简单分离存储,每个大小类的空闲链表包含大小相等的块,每个块的大小就是这个大小类中最大元素的大小。
未能释放已分配的块是一种常见的变成错误。
垃圾收集器是一种动态存储分配器,它自动释放程序不再需要的已分配块,这些块被称为垃圾。自动回收堆存储的过程叫做垃圾收集。
对于C程序员来说,管理和使用虚拟存储器可能是个困难的、容易出错的任务。与存储器有关的错误都很严重,因为他们在时间和空间上,都是在距错误源一段距离之后才表现出来。
间接应用坏指针:在进程的虚拟地址空间中有较大的洞,没有映射到任何别的意义的数据,如果我们试图间接引用一个指针指向这些洞的指针,那么操作系统就会以段异常终止我们的程序。
如果一个程序不检查输入串的大小就写入栈中的目标缓冲区,那么这个程序就会有缓冲区溢出错误。
一种常见的错误是假设指向对象的指针和它们所指向的对象是相同大小的。
误解指针运算:常见的错误是忘记了指针的算术操作是以它们指向的对象的大小为单位来进行的。而这种大小单位并不一定是字节。
引用不存在的变量:没有经验的c程序员不理解栈的规则,有时会引用不再合法的本地变量。
一个相似的错误就是引用了已经被释放了的堆块中的数据。
引起存储器泄露:存储器泄漏是缓慢、隐形的杀手,当程序员不小心忘记释放已分配的块。而在堆里创建了垃圾时,会发生这种问题。
即使虚拟存储器是由系统自动提供的,它也是一种有限的存储器资源,应用程序必须精明地管理它。管理虚拟存储器可能包含一些微妙的时间和空间的平衡,在C程序中很容易犯与存储器有关的错误。这些错误很麻烦,这是导致JAVA产生的一个重要原因,JAVA取消了取变量地址的能力,完全控制了动态存储分配器,从而严格控制了对虚拟存储器的访问。
虚拟存储器是对主存的一个抽象,支持虚拟存储器的处理器通过使用一种叫做虚拟寻址的间接形式来引用主存。处理器产生一个虚拟地址,在被发送到主存之前,这个地址被翻译成物理地址。从虚拟地址空间到物理地址空间的地址翻译要求硬件和软件紧密合作,专门的硬件通过使用页表来翻译虚拟地址,而页表的内容是由操作系统提供的。
虚拟存储器提供了三个重要功能1、它在主存中自动缓存最近使用的存放磁盘上的虚拟地址空间的内容。虚拟存储器缓存中的块叫做页。对磁盘上页的引用会触发缺页,如果有必要将写回被驱逐的页。2、虚拟存储器简化了存储器管理,进而又简化了链接、在进程间共享数据,进程的存储器分配、以及程序加载。3、虚拟存储器通过每条页表条目中加入保护位。从而简化了存储器保护。
地址翻译的过程必须和系统中任意硬件缓存的操作集成在一起。
现在存储器通过将虚拟存储器组块和磁盘上的文件组块关联起来,来初始化虚拟存储器组块。这个过程称为存储器映射。