现代操作系统(第3章 内存管理)
分层存储器体系(memory hierarchy)
操作系统中管理分层存储器体系的部分称为存储管理器(memory manager)。它的任务是有效地管理内存(即记录哪些内存是正在使用的,哪些内存是空闲的);在进程需要时为其分配内存,在进程使用完后释放内存。
进程中所使用的内存为堆和栈。
3.1 无存储器抽象
最简单的存储器抽象就是根本没有抽象。每一个程序都直接访问物理内存。
即使没有存储器抽象,同时运行多个程序也是可能的——操作系统只需要把当前内存中所有内容保存到磁盘文件中,然后把下一个程序读入到内存中再运行即可(只要再某个时间内存中只有一个程序就不会发生冲突)。
不使用存储器抽象的缺陷:重定位问题
两个程序都引用了绝对物理地址,因此在地址16384处的指令JMP28会使程序跳转到地址28处执行ADD指令,而不是16384+28的16412处。
因此,我们希望每个程序都使用一套私有的本地地址来进行内存寻址。
IBM 360对上述问题的补救方案是静态重定位技术——当一个程序被装载到地址16384时,常数16384被加到每一个程序地址上。(有局限性,并且会减慢装载速度)
总之,把物理地址暴露给进程会带来下面几个严重问题:
- 操作系统容易被破坏
- 无法同时运行多个程序。
3.2 一种存储器抽象:地址空间
要使多个应用程序同时处于内存中并且不相互影响,需要解决的两个问题:保护和重定位。
3.2.1 地址空间的概念
就像进程的概念创造了一类抽象的CPU以运行程序一样,地址空间为程序创造了一种抽象的内存。
地址空间是一个进程可用于寻址内存的一套地址集合。每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间。
基址寄存器与界限寄存器
给每个程序一个自己独有的地址空间有一个简单的解决办法是使用动态重定位,简单地把每个进程的地址空间映射到物理内存的不同部分。
此时,需要给每个CPU配置两个特殊硬件寄存器——基址寄存器和界限寄存器。
原理:程序的起始物理地址装载到基址寄存器中,程序的长度装载到界限寄存器中。
每次一个进程访问内存,取一条指令,读或写一个数据字,CPU硬件会在把地址发送到内存总线之前,自动把基址值加到进程发出的地址值上。同时,它检查程序提供的地址是否等于或大于界限寄存器里的值。如果访问的地址超过了界限,会产生错误并中止访问。
缺点:每次访问内存都需要进行加法和比较运算。其中加法运算由于进位传递时间的问题,在没有使用特殊电路的情况下会显得很慢。
3.2.2 交换技术
两种处理内存超载的通用方法:交换(swapping)和虚拟内存(virtual memory)。
交换系统的操作将空闲的进程存回磁盘,使其不再占用内存。
交换在内存中产生了多个空闲区(hole, 也称为空洞),通过把所有的进程尽可能向下移动,有可能将这些小的空闲区合成一大块——内存紧缩(memory compaction)。通常不进行这个操作,因为耗费大量CPU时间。
3.2.3 空闲内存管理
1.使用位图的存储管理
内存被划分成小到几个字或大到几千字节的分配单元。每个分配单元对应于位图中的一位,0表示空闲,1表示占用(或者相反)。
分配单元越小,位图越大。
即使只有4个字节大小的分配单元,32位的内存也只需要位图中的1位。所以位图只占用了1/32的内存。
若进程的大小不是分配单元的整数倍,那么在最后一个分配单元中就会有一定数量的内存被浪费。
缺陷:在决定把一个占\(k\)个分配单元的进程调入内存时,存储管理器必须搜索位图,在位图中找出有\(k\)个连续0的串。查找位图中指定长度的连续0串是耗时的操作。
2.使用链表的存储管理
另一种方法:维护一个记录已分配内存段和空闲内存段的链表。其中链表中的一个结点或者包含一个进程,或者是两个进程间的一块空闲区。链表中的每一个结点都包含以下域:空闲区(H)或进程(P)的指示标志、起始地址、长度和指向下一结点的指针。
进程表中表示终止进程的结点中通常含有指向对应于其段链表结点的指针,因此段链表使用双向链表会更方便——更易于找到上一个结点,并检查是否可以合并。
有以下几种算法可以用来位创建的进程分配内存:
首次适配FF(first fit):沿段链表进行搜索,直到找到一个足够大的空闲区,除非空闲区大小和要分配的空间大小正好一样,否则将该空闲区分为两部分,一部分共进程使用,另一部分形成新的空闲区。
下次适配NF(next fit):与FF类似,但每次找到合适的空闲区时都记录当时的位置,以便在下次寻找空闲区时从上次结束的地方开始搜索。(性能略低于FF)
最佳适配BF(best fit):搜索整个链表,找出能够容纳进程的最小的空闲区。(会分裂出很多非常小的空闲区)
最差适配WF(worst fit):搜索整个链表,找出最大的可用空闲区,使新的空闲区比较大从而可以继续使用。(性能较差)
快速适配QF(quick fit):为常用大小的空闲区维护单独的链表。
如果为进程和空闲区维护各自独立的链表,那么前四个算法的速度都能得到提高(可以集中精力只检查空闲区)。此时若按照大小对空闲区链表进行排序,则BF的速度会更快。(但和QF一样,需要费时进行合并,否则会分裂出大量的进程无法利用的小空闲区)
3.3 虚拟内存
虚拟内存(virtual memory)的基本思想是:每个程序拥有自己的地址空间,这个空间被分割成多个块,每一块称作一页或页面。每一页有连续的地址范围。这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。
当程序引用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。
当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。
3.3.1 分页
大部分虚拟内存系统中都使用一种称为分页(paging)的技术。
由程序产生的地址称为虚拟地址(virtual address),它们构成了一个虚拟地址空间(virtual address space)。
此时,虚拟地址不是被直接送到内存总线上,而是被送到内存管理单元(Memory Management Unit, MMU),MMU把虚拟地址映射为物理内存地址。
虚拟地址空间按照固定大小划分成被称为页面(page)的若干单元。在物理内存中对应的单元称为页框(page frame)
缺页中断或缺页错误(page fault):该页面没有被映射,使CPU陷入到操作系统。(图中页表中为absent bit(0)所在的部分)
上述例子给出虚拟地址的二进制表示:0010/000000000100(前半部分4位为虚拟页号,后半部分12位为页内偏移量)。(这样切割的原因是页面大小:\(4KB = 2^{12}B\))
以页号作为页表(page table)的索引,以得出对应于该虚拟页面的页框号。如果Present/Absent bit位为0,则将引起上述操作系统陷阱。如果该位为1,则在页表中查到的页框号复制到输出寄存器的高3位中,再加上输入虚拟地址中的低12位偏移量,得到最终的物理地址。
3.3.2 页表
虚拟页号可用作页表的索引,以找到该虚拟页面对应的页表项。由页表项可以找到页框号。然后把页框号拼接到偏移量的高位端,以替换掉虚拟页号,形成送往内存的物理地址。
数学上,页表就是一个函数。
页表项的结构
保护(protection)位指出一个页允许什么类型的访问。读/写/执行
修改(modified)位指出页面是否被修改。
访问(referenced)位在该页面被访问时设置,帮助操作系统在发生缺页中断时选择要被淘汰的页面。
禁止该页面被高速缓存(caching disabled)。
3.3.3 加速分页过程
-
虚拟地址到物理地址的映射必须非常快
-
如果虚拟地址空间很大,页表也会很大。
1. 转换检测缓冲区
大多数程序总是对少量的页面进行多次的访问,而不是相反。
为计算机设置一个小型的硬件设备,将虚拟地址直接映射到物理地址,而不必再访问页表。这种设备称为转换检测缓冲区(Translation Lookaside Buffer, TLB),有时又称为相联存储器(associate memory)或快表。(通常在MMU中)
将一个虚拟地址放入MMU中进行转换时,硬件首先通过将该虚拟页号与TLB中所有表项同时(即并行)进行匹配,判断虚拟页面是否在其中。
如果发现了一个有效的匹配并且要进行的访问操作并不违反保护位,则将页框号直接从TLB中取出,而不必再访问页表。
如果虚拟页号确实再TLB中,但指令试图在一个只读页面上进行写操作,则会产生一个保护错误,就像对页表进行非法访问一样。
如果MMU检测到没有有效的匹配项,就会进行正常的页表查询。并从TLB中淘汰一个表项,用新找到的页表项代替它。
2. 软件TLB管理
许多现代的RISC机器,几乎所有的页面管理都是在软件中实现的。此时,TLB表项被操作系统显式地装载。
软失效、硬失效
页表遍历
次要缺页错误、严重缺页错误、段错误
3.3.4 针对大内存的页表
怎样处理巨大的虚拟地址空间
1. 多级页表
32位的虚拟地址被划分为10位的PT1域、10位的PT2域和12位的Offset(偏移量)域。因为偏移量是12位,所以页面大小为4KB,共有\(2^{20}\)个页面。
2. 倒排页表
针对页式调度层级不断增长的另一种解决方案是倒排页表(inverted page table)。
3.4 页面置换算法
当发生缺页中断时,操作系统必须在内存中选择一个页面将其换出内存,以便为即将调入的页面腾出空间。
如果要换出的页面在内存驻留期间已经被修改过,就必须把它写回磁盘以更新该页面在磁盘上的副本。
如果该页面没有被修改过,那么它在磁盘上的副本已经是最新的,不需要回写。直接用调入的页面覆盖被淘汰的页面即可。
此时,虽然可以随机地选择一个页面来置换,但是如果每次都选择不常使用的页面会提升系统的性能。如果一个被频繁使用的页面被置换出内存,很可能它在短时间内又要被调入内存,这会带来不必要的开销。
3.4.1 最优页面置换算法(Optimal Page Replacement, OPR)
将每个页面用在该页面首次被访问前所要执行的指令数标记,置换标记最大的页面。(保留最近被使用的)
无法实现,当缺页中断发生时,操作系统无法知道各个页面下一次将在什么时候被访问。
3.4.2 最近未使用页面置换算法(Not Recently Used, NRU)
用页表项中的访问位和修改位构造一个简单的页面置换算法:当启动一个进程时,它的所有页面的两个位都由操作系统设置成0,访问位被定期地(比如在每次时钟中断时)清零,以区别最近没有被访问的页面和被访问的页面。
分为4类:
- 第0类:没有被访问,没有被修改。
- 第1类:没有被访问,已被修改。
- 第2类:已被访问,没有被修改。
- 第3类:已被访问,已被修改。
算法随机地从类编号最小的非空类中挑选一个页面淘汰。
3.4.3 先进先出页面置换算法
另一种开销较小的页面置换算法是先进先出(First-In First-Out, FIFO)页面置换算法。(淘汰停留时间最长的)
缺陷:很可能将从始至终频繁被访问的页面替换掉。
3.4.4 第二次机会页面置换算法
对FIFO算法进行改进:检查最老页面的R位。如果R位是0,则立即置换;如果R位是1,则将R置0,并将该页面放到链表的尾端。
缺陷:经常在链表中移动页面,效率低。
3.4.5 时钟页面置换算法
为了克服第二次机会算法的缺陷,把所有的页面都保存在一个类似钟面的环形链表中,一个表指针指向最老的页面,这样在链表中移动页面的操作就被简化为将表指针移动一位。
3.4.6 最近最少使用页面置换算法
最近最少使用页面置换算法(Least Recently Used, LRU):在缺页中断发生时,置换未使用时间最长的页面。
需要在内存中维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾。每次访问内存都必须更新整个链表。
可以通过特殊硬件实现LRU。比如一个64位计数器C,在每条指令执行完后自动加1,存在页表项中,发生缺页中断时只需检查所有页表项中计数器的值即可。
最优算法的一个很好的近似。理论上可以实现,但代价很高。
3.4.7 用软件模拟LRU
LRU算法需求的硬件只有少量计算机拥有,因此需要一个能用软件实现的解决方案。
最不常用(Not Frequently Used, NFU)算法:将每个页面与一个软件计数器相关联,每次时钟中断时,由操作系统将每个页面的R位加到计数器上。发生缺页中断时,置换计数器值最小的页面。
缺陷:从来不忘记任何事情——第一次扫描中计数器的值很高的页面进入第二次扫描时,其值仍然会很高,尽管它可能未被使用过。
优化:首先在R位被加进之前先将计数器右移一位;其次将R位加到计数器最左端的位而不是最右端的位。
修改以后的算法称为老化(aging)算法。
3.4.8 工作集页面置换算法
略
3.4.9 工作集时钟页面置换算法
略
3.4.10 页面置换算法小结
3.5 分页系统中的设计问题
3.5.1 局部分配策略与全局分配策略
怎样在相互竞争的可运行进程之间分配内存。
全局算法在通常情况下工作得比局部算法好。
有一大类页面置换算法,缺页中断率都会随着分配的页面的增加而降低。
管理内存动态分配的一种方法时使用缺页中断率(Page Fault Frequency, PFF)算法。
测量缺页中断率的方法是直截了当的:计算每秒的缺页中断数,可能也会将过去数秒的情况做连续平均。一个简单的方法是将当前这一秒的值加到当前的连续平均值上然后除以2。
3.5.4 分离的指令空间和数据空间
3.5.5 共享页面
3.5.6 共享库
3.7 分段
需要提供能令程序员不用管理表扩张和收缩的方法——在机器上提高多个互相独立的称为段(segment)的地址空间。
分页与分段的比较:
3.7.1 纯分段的实现
棋盘型碎片或外部碎片(external fragmentation)。(对应分页的内部碎片,二者都是空闲区)