文章目录
前言
本篇文章由浅入深的跟随操作系统的发展时间线来简要的介绍操作系统中对于内存的管理,文章中理论居多,讲解的都是操作系统内存管理的重点部分。
提示:以下是本篇文章正文内容,下面案例可供参考
一、无存储器抽象
最简单的内存管理就是啥也不管理,任由其发展。将这种思想转换到操作系统里面来说就是无存储器抽象,每一个程序都是直接访问物理内存,呈现给程序员的内存模型就是物理内存模型,进而程序员能够使用的内存大小也是受限于物理内存的大小的,内存地址从0开始到内存地址的最大值处。因此在这种思想下,不可能会有两个应用程序同时在内存中运行。
我们知道,操作系统是需要在常驻在内存中运行的,因此,结合应用程序,内存中分布可以如下的几种方式:

程序在内存中通过如上的方式组织起来,使得通常同一时刻只能有一个进程在运行。操作系统通过将程序从磁盘文件复制到内存中来使得程序开始运行,当程序运行结束后,操作系统再将新的程序装载到内存中运行,新的程序会覆盖掉内存中的其他程序。
运行多个程序
通过上述的定义可以看出是无法同时运行多个程序的,但是计算机科学家们通过特殊的方式使得同时运行多个程序称为可能。
在早期的机器中,内存被划分为2KB大小的区域块,每块区域被分配一个4位的保护健,保护健存储在CPU的**特殊寄存器(SFR)**中。这种情况下,一个内存为1MB的机器需要512个这样的4位寄存器,PSW(Program Status Word, 程序状态字) 中有一个4位码。一个运行中的进程如果访问键与其 PSW 中保存的码不同,硬件会捕获这种情况。因为只有操作系统可以修改保护键,这样就可以防止进程之间、用户进程和操作系统之间的干扰。
这种解决方式的一个缺陷如下所示:假设有两个程序,每个大小各位16KB

可以看到,当两个程序同时装载进内存时在内存中的布局如上图所示,对于第一个程序来说,执行过程中没有任何问题,但是对于第二个程序来说,当执行到JMP 28指令时,会跳转到程序一的内存地址,从而导致出现错误。
上述问题的核心是引用了绝对物理地址,故在处理上述问题的时候采用了如下的方式:静态重定位(static relocation)
当一程序被加载到16384地址时,常数16384被加到每一个程序地址上(所以JMP 28会变为JMP 16412)。
二、存储器抽象:地址空间
地址空间(address space),也是计算机科学中的一个重要的抽象,类似于进程抽象了CPU来运行程序,地址空间抽象了内存来提供给程序使用。地址空间是进程可以用来寻址内存的地址集。每一个进程都有自己私有的地址空间,独立于其他进程的地址空间,但是也可以通过某种方式来共享进程之间的地址空间。
基址寄存器和变址寄存器
对于上一节讲过的多个进程同时运行的静态重定位来说,结合地址空间提出了动态重定位的技术,即通过一种简单的方式将每一个进程的地址空间映射到物理内存的不同区域(注意此时的技术方向仍然是将进程完整的装载进内存空间中)。
对于动态重定位的经典实现方法是给每个CPU配置两个特殊的硬件寄存器,通常叫做基址寄存器和变址寄存器。结合如下的图示来讲解程序在此种动态重定位的方式下的装载流程:

- 程序会装载到内存中的连续位置并且在装载期间无需重定位。
- 程序运行时,程序的起始物理地址装载到基址寄存器中,程序的长度则装载到变址寄存器中。
- 在上图中,第一个程序运行时,装载到这些硬件寄存器中的基址和变址寄存器的值分别是 0 和 16384。当第二个程序运行时,这些值分别是 16384 和 16384。如果第三个 16 KB 的程序直接装载到第二个程序的地址之上并且运行,这时基址寄存器和变址寄存器的值会是 32768 和 16384。
- 基址寄存器:存储数据内存的起始位置
- 变址寄存器:存储应用程序的长度。
使用基址寄存器和变址寄存器是给每个进程提供私有地址空间的一种非常好的方法,因为每个内存地址在送到内存之前,都会先加上基址寄存器的内容。唯一的缺点就是每次访问内存时,都会进行 ADD
和 CMP
运算。CMP 指令可以执行的很快,但是加法就会相对慢一些,除非使用特殊的加法电路,否则加法因进位传播时间而变慢。
交换技术
如果计算机的物理内存足够大,可以用来容纳所有的进程,那么上述的方法是可以使用的。但是实际上计算机的物理内存是有限的,而且程序大小的增长速度也是非常快的。因此,上述的方案也只能解燃眉之急,无法做到通用。
针对上述的内存不足的问题,提出了以下的两种解决方式:
- 最简单的一种方式就是交换(swapping)技术,即把一个进程完整的调入内存,然后再内存中运行一段时间,再把它放回磁盘。空闲进程会存储在磁盘中,所以这些进程在没有运行时不会占用太多内存。
- 另外一种策略叫做虚拟内存(virtual memory),虚拟内存技术能够允许应用程序部分的运行在内存中。
对于交换技术来说,会在内存中创建很多空闲区(hole),内存会把所有的空闲区尽可能向下移动合并为一个大的空闲区,这项技术称为内存紧缩(memory compaction)。一般不会使用该技术,因为该技术使用起来会消耗大量的CPU时间。
有一个值得注意的问题是,当进程被创建或者换入内存时应该为它分配多大的内存。如果进程被创建后它的大小是固定的并且不再改变,那么分配策略就比较简单:操作系统会准确的按其需要的大小进行分配。但是如果进程的data segment能够自动增长,例如,通过动态分配堆中的内存,肯定会出现问题。所以针对内存会自动增长的区域,会有以下几种处理方式:
- 如果一个进程与空闲区相邻,那么可把该空闲区分配给进程以供其增大。
- 如果进程相邻的是另一个进程,就会有两种处理方式:要么把需要增长的进程移动到一个内存中空闲区足够大的区域,要么把一个或多个进程交换出去,已变成生成一个大的空闲区。
- 如果一个进程在内存中不能增长,而且磁盘上的交换区也满了,那么这个进程只有挂起一些空闲空间(或者可以结束该进程)
- 在换入或移动进程时为它分配一些额外的内存。
空闲区管理
在进行内存分配的时候,操作系统必须对其进行管理,大致上来说,有以下两种方式:
- 位图(bitmap)
- 空闲列表(free lists)
使用位图的存储管理
使用位图方法时,内存可能被划分为从小到几个字或大到几千字节的分配单元。每个分配单元对应于位图中的一位,0 表示空闲, 1 表示占用(或者相反)。一块内存区域和其对应的位图如下

图 a 表示一段有 5 个进程和 3 个空闲区的内存,刻度为内存分配单元,阴影区表示空闲(在位图中用 0 表示);图 b 表示对应的位图;图 c 表示用链表表示同样的信息
使用链表进行内存管理
维护一个记录已分配内存段和空闲内存段的链表,段会包含进程或者是两个进程的空闲区域。可用上面的图 c 来表示内存的使用情况。链表中的每一项都可以代表一个 空闲区(H) 或者是进程§的起始标志,长度和下一个链表项的位置。
在这个例子中,段链表(segment list)是按照地址排序的。这种方式的优点是,当进程终止或被交换时,更新列表很简单。一个终止进程通常有两个邻居(除了内存的顶部和底部外)。相邻的可能是进程也可能是空闲区,它们有四种组合方式。

在链表的方式下,有以下几种分配算法:
- 最简单的分配算法是首次适配(first fit),内存管理器会沿着段列表进行扫描,直到找个一个足够大的空闲区为止。除非空闲区大小和要分配的空间大小一样,否则将空闲区分为两部分,一部分供进程使用;一部分生成新的空闲区。
- 首次适配的一个小的变体是下次适配(next fit)。它和首次匹配的工作方式相同,只有一个不同之处那就是下次适配在每次找到合适的空闲区时就会记录当时的位置,以便下次寻找空闲区时从上次结束的地方开始搜索,而不是像首次匹配算法那样每次都会从头开始搜索。
- 最佳适配(best fit)。最佳适配会从头到尾寻找整个链表,找出能够容纳进程的最小空闲区。最佳适配算法会试图找出最接近实际需要的空闲区,以最好的匹配请求和可用空闲区,而不是先一次拆分一个以后可能会用到的大的空闲区。此算法与首次适配和下次适配相比,浪费更多的内存,因为他会产生大量无用的小缓冲区。
三、虚拟内存
尽管基址寄存器和变址寄存器用来创建地址空间的抽象,但是这种方式的实现都是将整个进程的地址空间都放在内存中来执行。但是随着软件规模的增大,运行的程序往往大到内存无法容纳。
针对于以上的问题,提出了虚拟内存(virtual memory)的概念。虚拟内存的基本思想是,每个程序都有自己的地址空间,这个地址空间被划分为多个称为页面(page)的块。每一页都是连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,硬件会立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令(缺页中断)。
注意:虚拟内存在本质上是用来创建一个地址空间的抽象,虚拟内存的实现本质上是将虚拟地址空间分解成页,并将每一项映射到物理内存的某个页框。
分页
一般使用虚拟内存的系统都会使用一种称为分页(paging)的技术。程序生成的地址被称为虚拟地址(virtual addresses),在使用虚拟内存时,虚拟地址不会直接发到内存总线上,相反会使用**MMU(memory management unit)**内存管理单元把虚拟地址映射为物理内存地址,如下所示:

下面展示了映射是如何工作的:

页表给出了虚拟地址与物理地址之间的映射关系。
存在映射的页映射方法
虚拟地址空间由固定大小的单元组成,这种固定大小的单元称为 页(pages)。而相对的,物理内存中也有固定大小的物理单元,称为页框(page frames)。页和页框的大小一样。在上面这个例子中,页的大小为 4KB ,但是实际的使用过程中页的大小范围可能是 512 字节 - 1G 字节的大小。对应于 64 KB 的虚拟地址空间和 32 KB 的物理内存,可得到 16 个虚拟页面和 8 个页框。RAM 和磁盘之间的交换总是以整个页为单元进行交换的。
当程序访问地址0时,会将虚拟地址 0 送到 MMU。MMU 看到虚拟地址落在页面 0 (0 - 4095),根据其映射结果,这一页面对应的页框 2 (8192 - 12287),因此 MMU 把地址变换为 8192 ,并把地址 8192 送到总线上。内存对 MMU 一无所知,它只看到一个对 8192 地址的读写请求并执行它。MMU 从而有效的把所有虚拟地址 0 - 4095 映射到了 8192 - 12287 的物理地址。
不存在映射的页映射方法
当程序访问一个未映射的页面,CPU 会陷入(trap)到操作系统中。这个陷入称为缺页中断(page fault) 或者是缺页异常。操作系统会选择一个很少使用的页并判断是否是脏页,如果是脏页则需要写回到磁盘中。随后把需要访问的页面读到刚才回收的页框中,修改映射关系,然后重新启动引起陷入的指令。
页表
对于每一个进程来说,都存在一个独立的页表,存在于进程的地址空间中,在进程运行的时候会有内核调入到内存中。下面我们通过分析MMU的内部构造来了解页表的具体细节,下面看一个虚拟地址的例子:

虚拟地址 8196 (二进制 0010000000000100)用上面的页表映射图所示的 MMU 映射机制进行映射,输入的 16 位虚拟地址被分为 4 位的页号和 12 位的偏移量。4 位的页号可以表示 16 个页面,12 位的偏移可以为一页内的全部 4096 个字节。
可用页号作为页表(page table) 的索引,以得出对应于该虚拟页面的页框号。如果在/不在位则是 0 ,则引起一个操作系统陷入。如果该位是 1,则将在页表中查到的页框号复制到输出寄存器的高 3 位中,再加上输入虚拟地址中的低 12 位偏移量。如此就构成了 15 位的物理地址。输出寄存器的内容随即被作为物理地址送到总线。
总结如下:虚拟地址被分为虚拟页号(高位部分)和偏移量(低位部分)。例如,对于 16 位地址和 4 KB 的页面大小,高 4 位可以指定 16 个虚拟页面中的一页,而低 12 位接着确定了所选页面中的偏移量(0-4095)。
虚拟页号可作为页表的索引用来找到虚拟页中的内容。由页表项可以找到页框号(如果有的话)。然后把页框号拼接到偏移量的高位端,以替换掉虚拟页号,形成物理地址。

页表的结构
如下所示记录了页表中常见的一些字段

- 页表项中最重要的字段就是页框号(Page frame number)。毕竟,页表到页框最重要的一步操作就是要把此值映射过去。
- 在/不在位,如果此位上的值是 1,那么页表项是有效的并且能够被使用。如果此值是 0 的话,则表示该页表项对应的虚拟页面不在内存中,访问该页面会引起一个缺页异常(page fault)。
- **保护位(Protection)**告诉我们哪一种访问是允许的,最简单的表示形式是这个域只有一位,0 表示可读可写,1 表示的是只读。
- 修改位(Modified) 和 访问位(Referenced) 会跟踪页面的使用情况。当一个页面被写入时,硬件会自动的设置修改位。修改位在页面重新分配页框时很有用。如果一个页面已经被修改过(即它是 脏 的),则必须把它写回磁盘。如果一个页面没有被修改过(即它是 干净的),那么重新分配时这个页框会被直接丢弃,因为磁盘上的副本仍然是有效的。这个位有时也叫做 脏位(dirty bit),因为它反映了页面的状态。
- 访问位(Referenced) 在页面被访问时被设置,不管是读还是写。这个值能够帮助操作系统在发生缺页中断时选择要淘汰的页。不再使用的页要比正在使用的页更适合被淘汰。这个位在后面要讨论的页面置换算法中作用很大。
- 禁止该页面被高速缓存,这个功能对于映射到设备寄存器还是内存中起到了关键作用。通过这一位可以禁用高速缓存。具有独立的 I/O 空间而不是用内存映射 I/O 的机器来说,并不需要这一位。
加速分页的过程
在理解了虚拟内存和分页的基础上,我们需要考虑以下两种问题:
- 虚拟地址到物理地址的映射速度必须要快
- 如果虚拟地址空间足够大,那么页表也会足够大
在启动一个进程时,操作系统会把保存在内存中的进程页表读副本放入寄存器中。所以,在进程的运行过程中,不必再为页表而访问内存。使用这种方法的优势是简单而且映射过程中不需要访问内存。缺点是页表太大时,代价高昂,而且每次上下文切换的时候都必须装载整个页表,这样会造成性能的降低。鉴于此,我们讨论一下加速分页机制和处理大的虚拟地址空间的实现方案。
转换检测缓冲区
大多数程序总是对少量页面进行多次访问,而不是对大量页面进行少量访问 – 局部性原理。
基于这种设想,提出了一种方案,即从硬件方面来解决这个问题,为计算机设置一个小型的硬件设备,能够将虚拟地址直接映射到物理地址,而不必再访问页表。这种设备被称为转换检测缓冲区(Translation Lookaside Buffer, TLB),有时又被称为相联存储器(associate memory) 。
TLB 通常位于 MMU 中,包含少量的表项,每个表项都记录了页面的相关信息,除了虚拟页号外,其他表项都和页表是一一对应的

TLB 其实就是一种内存缓存,用于减少访问内存所需要的时间,它就是 MMU 的一部分,TLB 会将虚拟地址到物理地址的转换存储起来,通常可以称为地址翻译缓存(address-translation cache)。
TLB的工作流程如下所示:
- 当一个 MMU 中的虚拟地址需要进行转换时,硬件首先检查虚拟页号与 TLB 中所有表项进行并行匹配,判断虚拟页是否在 TLB 中
- 如果找到了有效匹配项,并且要进行的访问操作没有违反保护位的话,则将页框号直接从 TLB 中取出而不用再直接访问页表。
- 如果虚拟页在 TLB 中但是违反了保护位的权限的话(比如只允许读但是是一个写指令),则会生成一个保护错误(protection fault) 返回。
- 如果 MMU 检测到没有有效的匹配项,就会进行正常的页表查找,然后从 TLB 中逐出一个表项然后把从页表中找到的项放在 TLB 中。当一个表项被从 TLB 中清除出,将修改位复制到内存中页表项,除了访问位之外,其他位保持不变。当页表项从页表装入 TLB 中时,所有的值都来自于内存。
针对大内存的页表
如果虚拟地址空间足够大,那么页表也会足够大,以下提供了几种方式处理巨大的虚拟地址空间。
多级页表
第一种方案是使用多级页表(multi),下面是一个例子

32 位的虚拟地址被划分为 10 位的 PT1 域,10 位的 PT2 域,还有 12 位的 Offset 域。因为偏移量是 12 位,所以页面大小是 4KB,公有 2^20 次方个页面。
引入多级页表的原因是避免把全部页表一直保存在内存中。不需要的页表就不应该保留。
多级页表是一种分页方案,它由两个或多个层次的分页表组成,也称为分层分页。级别1(level 1)页面表的条目是指向级别 2(level 2) 页面表的指针,级别2页面表的条目是指向级别 3(level 3) 页面表的指针,依此类推。最后一级页表存储的是实际的信息。
下面是一个二级页表的工作过程:

在最左边是顶级页表,它有 1024 个表项,对应于 10 位的 PT1 域。当一个虚拟地址被送到 MMU 时,MMU 首先提取 PT1 域并把该值作为访问顶级页表的索引。因为整个 4 GB (即 32 位)虚拟地址已经按 4 KB 大小分块,所以顶级页表中的 1024 个表项的每一个都表示 4M 的块地址范围。
由索引顶级页表得到的表项中含有二级页表的地址或页框号。顶级页表的表项 0 指向程序正文的页表,表项 1 指向含有数据的页表,表项 1023 指向堆栈的页表,其他的项(用阴影表示)表示没有使用(没有使用的就不需要在内存中存在二级页表,这样可以大大的节省内存空间)。现在把 PT2 域作为访问选定的二级页表的索引,以便找到虚拟页面的对应页框号。
四、MMU、TLB是否需要知道进程信息
问题的引入,当新的进程运行的时候,CPU是如何知道运行的进程已经改变了的?解决此问题的方式是借助于CPU寄存器中的CR3寄存器。
在多处理系统中,每个CPU都有自己的TLB,这叫做该CPU的本地TLB。与硬件高速缓存相反,TLB中的对应项不必同步,这是因为运行在现有CPU上的进程可以使用同一线性地址与不同的物理地址发生联系。当CPU的CR3控制寄存器被修改时,硬件自动使本地TLB中的所有项都无效,这是因为新的一组页表被启用而TLB指向的是旧数据。
每个进程都有自己的页表,页表的起始地址放在进程的进程控制块(PCB)中,当某进程运行时,将其页表的起始地址放在页表寄存器(CR3)中。单CPU系统中只能有一个进程处于执行状态,因此一个页表寄存器可供系统中所有的进程交替使用。
CR3中含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base address Register)
操作系统和MMU是这样配合的:
- 操作系统在初始化或分配、释放内存时会执行一些指令在物理内存中填写页表,设置CR3寄存器,MMU通过CR3寄存器获得页表在物理内存中的位置。
- 设置好之后,CPU每次执行访问内存的指令都会自动引发MMU做查表和地址转换操作,地址转换操作由硬件自动完成,不需要用指令控制MMU去做。
总结:通过上述分析,其实MMU、TLB是需要知道进程的具体信息的,进程切换之后TLB的内容都会无效,它只缓存当前运行进程的地址转换,当进程切换时,CR3寄存器内容修改,TLB中的内容自动失效 。MMU也是通过CR3寄存器中的页目录表物理内存基地址去找到当前正在运行进程的页表,所以MMU也不太需要具体的进程信息。
页面置换算法
下面只是列出常见的页面置换算法:
- 最近未使用页面置换
- 先进先出页面置换
- 最近最少使用页面置换
- 时钟页面置换
- 第二次机会页面置换
- 工作集页面置换
总结
本篇文章从无存储器抽象入手,依次讨论了地址空间、虚拟内存等计算机中的抽象概念。详细的叙述了在虚拟内存技术的条件下,虚拟内存是如何转换为物理地址的,以及在转换的过程中使用的一些方法(页表,MMU、TLB等)。同时也讨论了在虚拟内存的技术下可能会遇到的问题(虚拟地址到物理地址的转换速度、降低页表的大小等)。最后简单的介绍了一下常见的页面置换算法。