‘‘Programs expand to fill the memory available to hold them.’’ —— Parkinson’sLaw
为什么提出分层存储器体系(memory hierarchy)的概念?
因为任何一个存储器都达不到“高速度,大容量,低价格”的目标,只能采用分层结构,形成存储体系。
操作系统中管理分层存储器体系的部分称为存储管理器(memory manager).
内存管理的概念:操作系统对内存的划分和动态分配。
链接:
抽象:程序员必备的能力
最简单的存储器抽象就是根本没有抽象,如:早期大型计算机(20世纪60年代前)、小型计算机(20世纪70年代前)和个人计算机(20世纪80年代前)。
每一个程序都直接访问物理内存。
因此,存储器模型就是简单的物理内存:从0到某个上限的地址集合,每一个地址对应一个可容纳一定数目二进制位的存储单元,通常是8个。而且要想在内存中同时运行两个程序是不可能的。
按以上的方式组织系统时,通常同一时刻只能有一个进程在运行。
而在没有存储器抽象的系统中实现并行的一种方法是使用多线程来编程,因为一个进程中的所有线程间可以共享同一内存映像。但是由于人们通常希望能够在同一时间运行没有关联的程序,而这正是线程抽象所不能提供的;而且一个没有存储器抽象的系统也不大可能具有线程抽象的功能,因此该想法并没有被广泛使用。
如何在不使用存储器抽象的情况下运行多个程序?
1.只要在某一个时间内存中只有一个程序,那么就不会发生冲突。(操作系统将当前内存中所有内容保存到磁盘文件中,然后把下一个程序读入到内存中在运行即可.)
2.将内存划分,并给每个单位分配一个4位的保护键(存储在CPU的特殊寄存器中),一个运行中的进程无法访问保护键与其PSW码不同的内存。
缺陷:两个程序都引用了绝对物理地址,会造成冲突。而我们希望每个程序都使用一套私有的本地地址来进行内存寻址。
解决:静态重定位(当一个程序被装载到地址16384时,将常数16384加到每个程序的地址上。)
问题:不通用;减慢装载速度;难以区分哪些内存字中有(可重定位的)地址(即地址or常数).
物理地址暴露给进程带来的问题:
1.用户程序易破坏操作系统
2.很难同时运行多个程序
要使多个应用程序同时处于内存中并且不互相影响,需要解决两个问题:保护和重定位。
这就需要创造一个新的存储器抽象:地址空间(一个进程可用于寻址内存的一套地址集合,e.g.电话号码、以“.com”结尾的网络域名的集合)。每个进程有自己一个地址空间,并且它独立于其他进程的地址空间。
如何给每个进程提供私有的地址空间?
方法:使用基址寄存器和界限寄存器(动态重定位)
程序装载到内存中连续的空闲位置且装载期间无须重定位。当一个进程运行时,程序的起始物理地址装载到基址寄存器,程序长度装到界限寄存器。每次一个进程访问内存时,CPU会在把地址发到内存总线前,自动把基址值加到进程发出的地址值上,并检查访问的地址是否超过界限。
缺点:每次访问内存都要进行加法(慢)和比较运算(快)。
如何处理内存超载?
方法一:交换(swapping)技术
把一个进程完整调入内存中运行一段时间,之后存回磁盘。空闲进程主要存储在磁盘上。
内存紧缩(memory compaction):交换在内存中产生了多个空闲区(空洞,hole),通过把所有的进程尽可能向下移动,合并成一大块。
缺点:要耗费大量的CPU时间。
第一种——如果进程的数据段可以增长,相邻的是一个空闲区,就可把该空闲区分配给进程供其增大;但相邻的是另一个进程,那么要么把需要增长的进程移到内存中一个足够大的区域中去,要么把一个或多个进程交换出去生成一个足够大的空闲区。若一个进程在内存中不能增长,磁盘上的交换区也满了,那该进程只能挂起(或结束)。
第二种——如果大部分进程在运行时都要增长,可以在换入或移动进程时为它分配一些额外内存(来减少因内存区域不够而引起的进程交换和移动所产生的开销)。但当进程被换出到磁盘上时,应只交换进程实际上使用的内存中的内容。
第三种——如果进程有两个可增长的段,如数据段和堆栈段,那么堆栈段向下增长,数据段向上增长,两者之间的内存供两者使用。如果也用完了,同第一种,进程必须移动到足够大的空闲区中(或结束)。
方法二:虚拟内存(virtual memory)
使程序在只有一部分被调入内存的情况下运行。
空闲内存管理
在动态分配内存时,操作系统必须对其进行管理,有两种方法跟踪内存使用情况:位图和空闲区链表。
(1)使用位图的存储管理
内存被划分为分配单元,每个分配单元对应位图中一位,0->空闲,1->占用(或相反)。
分配单元越小,位图越大。
内存的大小和分配单元的大小决定位图的大小。
(2)使用链表的存储管理
链表的结点包括:空闲区(H)(or进程§的指示标志)、起始地址、长度、指向下一结点的指针。
上图中,a)更新链表需要把P替换成H
b)&c)两个结点被合并为一个,链表删除一个结点
d)三个结点被合并为一个,链表删除两个结点
由于进程表中表示终止进程的结点中通常含有指向对应其段链表结点的指针,因此使用双链表可能更方便,便于合并。
动态分配内存算法
首次适配算法(first fit):按地址从小到大为序,分配第一个符合条件的分区。
下次适配算法(next fit):每次找到合适的空闲区后记录当前位置,下次从这里开始找。
最佳适配算法(best fit):按空间从小到大为序,找整个链表,找容纳进程的最小的空闲区。
最差适配算法(worst fit):按空间从小到大为序,找最大的可用空闲区。
快速适配算法(quick fit):单独维护常用大小的空闲区链表。但在一个进程终止或被换出时,寻找它的相邻块并查看是否可以合并的过程比较费时;若不合并,内存会分裂出大量无法利用的小空闲区。
e.g.在一个交换系统中,按内存地址排列的空闲区大小是:10KB、4KB、20KB、18KB、7KB、9KB、12KB和15KB。对于连续的段请求:a)12KB;b) 10KB;c)9KB。使用首次适配算法,将找出哪个空闲区?使用最佳适配、最差适配、下次适配算法呢?
答:
首次适配: 20KB,10KB,18KB;
最佳适配: 12KB,10KB,9KB;
最差适配: 20KB,18KB,15KB;
下次适配: 20KB,18KB,9KB。
背景:1.管理软件的膨胀 2.需要运行的程序往往大到内存无法容纳
方法:
交换技术——磁盘传输率低,需要好几秒才能换出或换入一个1GB的程序。
覆盖(overlay,把程序分割成许多片段)——费时而且易于出错。
虚拟内存(virtual memory)
基本思想:a.每个程序拥有自己的地址空间,这个空间被分割成多个块,每一块称作一页or页面(page)。b.每一页有连续的地址范围。c.这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。d1.当程序引用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。d2.当程序引用到一部分不在物理内存中的地址空间时,由OS负责将缺失的部分装入物理内存并重新执行失败的指令。
本质:创造一个新的抽象概念——地址空间,这个概念是对物理内存的抽象,类似于进程是对CPU的抽象。是对基址寄存器和界限寄存器的一种综合。
实现:很适合在多道程序设计系统中使用,将虚拟地址空间分解成页,并将每一页映射到物理内存的某个页框或暂时解除映射。
p.s.用户程序的地址空间被划分为若干固定大小的区域,称为“页”。相应地,内存空间分成若干个物理块,页和块的大小相等。可将用户程序的任一页放在内存的任一块中,实现了离散分配,由一个页表来维护它们之间的映射关系。
由程序产生的地址称为虚拟地址(virtual address),它们构成了一个虚拟地址空间(virtual address space)。
在没有虚拟内存的计算机上,系统直接将虚拟地址送到内存总线上,读写操作使用具有同样地址的物理内存字;
在使有虚拟内存的计算机上,虚拟地址被送到内存管理单元(MMU)而不是内存总线,接着MMU把虚拟地址映射为物理内存地址。
下图说明了这种映射如何工作。
虚拟地址空间按照固定大小划分成被称为页面(page)的若干单元,在物理内存中对应的单元称为页框(page frame),二者通常是一样的,在上图中都是4KB。
e.g.使用以上页表,给出下面每个虚拟地址对应的物理地址:a) 20 b) 4100 c) 8300
a) 因为虚拟地址20(在虚拟页面0中)被映射到到物理页框2中,所以物理地址是:(20-0)+8192=8212.
b) 因为虚拟地址4100(在虚拟页面1中)被映射到到物理页框1中,所以物理地址是:(4100-4096)+4096=4100.
c) 因为虚拟地址8300(在虚拟页面2中)被映射到到物理页框6中,所以物理地址是:(8300-8192)+24576=24684.
缺页中断or缺页错误(page fault):虚拟页面对应的物理地址不存在(要访问的页不在主存),于是使CPU陷入到OS.
OS找到一个很少使用的页框且把它的内容写入磁盘,随后把需要访问的页面读到刚才回收的页框中,修改映射关系,然后重新启动引起陷进的指令。
MMU内部结构:
虚拟页号可用作页表(page table)的索引,以找到该虚拟页面对应的页表项,由页表项找到页框号(如果存在),然后将页框号复制到输出寄存器的的高位端,再加上输入虚拟地址中的低位偏移量,以此构成物理地址。输出寄存器的内容随即被作为物理地址送到内存总线。
输入的16位虚拟地址被分为4位页号(高位部分)和12位偏移量(低位部分)。4位的页号可以表示16个页面,12位偏移量可以为一页内的全部4096个字节编址。(使用其他数也可以)
目的:把虚拟页面映射为页框
p.s. 页表是一个函数,参数是虚拟页号,结果是物理页框号。
虚拟地址=虚拟页号(高位部分)+偏移量(低位部分)
对于20000这个十进制虚拟地址,分别使用4KB页面和8KB页面计算虚拟页号和偏移量。
答:20000 --> 01001110 00100000
对于4KB页面,由于0~4K为 0~4095 ,2^12=4096;所以 page number = 0100,offset = 1110 00100000,即(page,offset)对是(4, 3616)
对于8KB页面,由于4~8K为 4096~8191 ,2^13=8192;所以 page number = 010,offset = 01110 00100000,即(page,offset)对是(2, 3616)
页表项的结构
不同机器的页表项存储的信息大致相同,不同计算机的页表项大小可能不一样,通常为32位。
若某个页面不在内存中,用于保存该页面的磁盘地址不是页表的组成部分。
两个主要问题
一、虚拟地址到物理地址的映射必须非常快
二、如果虚拟地址空间很大,页表也会很大
解决方案
怎样处理巨大的虚拟地址空间?
1.一级页表
由(a)可知,offset(偏移量)是12位,所以页面大小=2^12 B=4KB,【因此页内地址要用12位表示,剩余20位表示页号】共有2^20个页面;相应的,在一个进程的页表中,会有2 ^20个页表项。若页表项长度为4B,所以一个页表需要2 ^20 * 4B = 2 ^22B。若一个页框(内存块)大小为4B,所以需要2 ^22/2 ^12 = 2 ^10个页框存储该页表。而页表的是连续存储的,因为根据页号查询页表的方法:K号页对应的页表项的位置 = 页表起始地址 + K * 4B(页表项长度)。
这和之前将进程划分成一个个页面以此不用连续的存放在内存中的目标相悖;而且,根据局部性原理可知,进程在一段时间内只需要访问某几个页面就可以正常运行了,因此也没有必要让整个页面都常驻内存。
2.两级页表
将长长的页表进行分组,使每个页面中刚好可以放下一个分组。
e.g. 假设一个机器有38位虚拟地址和32位的物理地址。若采用二级页表,页面大小为16KB,每个页表项为4B,应该对第一级和第二级页表域分配多少位?
分别分配12位。页面大小=16KB=2^14B,offset=14;因为每个页表项长度为4B,每个页面可容纳2 ^12个页表项。因此每2 ^12个连续的页表项为一组,每组刚好占一个页面,需要12个位来索引一个页面。38-12-14=12 bits
当发生缺页中断时,OS必须在内存中选择一个页面将其换出内存,再调入另一个页面。如果要换出的页面在内存驻留期间已经被修改过,就必须把它写回磁盘以更新该页面在磁盘上的副本;如果没有被修改过,直接覆盖即可。
最优页面置换算法(TheOptimal Page Replacement Algorithm)
应该置换标记最大的页面;但该算法不可能实现,OS无法预知各个页面下一次将在什么时候被访问。
最近未使用页面置换算法(NRU)
系统为每一页面设置了两个状态位:访问位(R位),修改位(M位)
选择在最近一段时间内未使用过的一页并置换。
先进先出页面置换算法(FIFO)
选择在内存中驻留时间最长的页并置换它。
实现:页面链表法(最早进入的页面放在表头,最新进入的页面放在表尾。)
该算法还会出现奇特的Belady现象
第二次机会页面置换算法(SCR)
按照先进先出算法选择某一页面,检查其访问位R,如果为0,则置换该页;如果为1,则给第二次机会,并将访问位置为0。
时钟页面置换算法(Clock)
需要用到一个访问位,当一个页面被访问时,将访问为置为 1。
首先,将内存中的所有页面保存在一个环形链表中,当缺页中断发生时,检查当前指针所指向页面的R位,如果R位为 0,就将该页面换出;否则将该页的访问位设置为 0,给该页面第二次的机会,移动指针继续检查。
链接:Clock
CLOCK
最近最少使用页面置换算法(LRU)
选择最后一次访问时间距离当前时间最长的一页并置换,即置换未使用时间最长的一页。
实现:在内存中维护一个所有页面的链表→费时、开销大
最不常用算法(NFU)
将每个页面与一个软件计数器相关联,计数器的初始值为0。每次时钟中断时,由OS扫描内存中的所有页面,将每个页面的R位(0/1)加到它的计数器上。这个计数器大体上跟踪了各个页面被访问的频繁程度。当发生缺页中断时,则置换计数器值最小的页面。
缺点:从来不忘记任何事情
工作集页面置换算法
请求调页(demand paging):页面在需要时被调入,而不是预先装入。
预先调页(prepaging):在进程运行前预先装入其工作集页面。
当前实际运行时间:进程从它开始执行到当前所实际使用CPU时间总数。
局部性访问:在进程运行的任何阶段,它都只访问较少的一部分页面。
颠簸(denning):每执行几条指令程序就发生一次缺页中断。
工作集:根据程序的局部性原理,一般情况下,进程在一段时间内总是集中访问一些页面,这些页面称为活跃页面;如果分配给一个进程的物理页面数太少了,使该进程所需的活跃页面不能全部装入内存,则进程在运行过程中将频繁发生中断。
工作集是随着时间变化的
清除策略
设计分页守护进程,大多数时候它是睡眠,但定期被唤醒以检查内存的状态。当需要使用一个已置换出的页框时,如果该页框还没有被覆盖,将其从空闲页框缓冲池中移出即可恢复该页面。
使用一个双指针时钟实现清除策略。前指针由分页守护进程控制:当它指向一个“脏”页面时,就把该页面写回磁盘,前指针向前移动;当它指向一个干净页面时,仅仅向前移动指针。后指针用于页面置换,与标准时钟算法一样。
缺页中断处理
用户程序地址空间按进程自身的逻辑关系划分为若干段,内存空间被动态的划分为若干个长度不相同的区域(可变分区)。以段为单位分配内存,每一段在内存中占据连续空间,各段之间可以不连续存放。
段页式:
用户程序地址空间:段式;内存空间:页式;分配单位:页