内存管理
# 内存管理
操作系统中管理分层存储器体系的部分称为存储管理器(memory manager)。它的任务是有效地管理内存,即记录哪些内存是正在使用的,哪些内存是空闲的;在进程需要时为其分配内存,在进程使用完后释放内存。
# 无存储器抽象
最简单的存储器抽象就是根本没有抽象。即直接访问物理内存。
运行多个程序:操作系统只需要把当前内存中所有内容保存到磁盘文件中,然后把下一个程序读入内存中再运行即可。即在某一个时间内存中只有一个程序。
# 一种存储器抽象:地址空间
把物理地址暴露给进程会带来下面几个严重问题:
- 用户程序可以直接寻址内存的每个字节,它们就很容易地破坏操作系统。
- 使用这种模型,需要同时运行多个程序是很困难的。
我们希望每个程序都使用一套私有的本地地址来进行内存寻址。
# 地址空间的概念
要使多个应用程序同时处于内存中并且不互相影响,需要解决两个问题:保护和重定位。
地址空间:地址空间是一个进程可用于寻址内存的一套地址集合。每个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的地址空间(除了在一些特殊情况下进程需要共享它们的地址空间外)。
比较难的是给每个程序一个自己的独有的地址空间,使得一个程序中地址28所对应的物理地址与另一个程序中的地址28所对应的物理地址不同。所使用的经典办法是给每个CPU配置两个特殊硬件寄存器:基址寄存器与界限寄存器。
# 交换技术
当内存的容量小于程序的大小时,就不可以把进程一直保存在内存中。
有两种处理内存超载的通用方法:
- 交换(swapping)技术:即把一个进程完整调入内存,使该进程运行一段时间,然后把它存回磁盘。
- 虚拟内存(virtual memory):该策略甚至能使程序在只有一个部分被调入内存的情况下运行。
交换技术是以进程为单位,若进程所需内存大于系统内存 ,则此进程无法进行。而虚拟存储是以页或段为单位,是把进程再分为页或段对内存进行分化,若进程所需内存大于系统内存,进程也可以运行,因为该进程的一部分可换到外存上。
交换技术的优点:
- 空闲进程主要存储在磁盘上,所以当它们不运行时就不会占用内存。
交换技术的缺点:
- 需要将整个进程存回磁盘或者从磁盘调入内存,耗时较大。
- 交换在内存中会产生多个空闲区(hole,也称为空洞)。
- 当程序大于内存大小时,不能将程序调入内存,因此该程序不能运行。
# 空闲内存管理
两种跟踪内存使用情况的方法(linux还用到伙伴分配器和slab分配器):
- 位图
- 空闲区链表
# 使用位图的管理存储器
使用位图方法时,内存可能被划分成小到几千字或大到几千字节的分配单元。每个分配单元对应于位图中的一位,0表示空闲,1表示占用(或者相反)。
内存的大小和分配单元的大小决定了位图的大小。分配单元越小,位图越大。
# 使用链表的存储管理
维护一个记录已分配内存段和空闲段的链表。其中链表中的一个结点或者包含一个进程,或者是两个进程间的一块空闲区。链表中的每一个结点都包含以下域:空闲区(H)或进程(P)的指示标志、起始地址、长度和指向下一结点的指针。
当按照地址顺序在链表中存放进程和空闲区时,有几种算法可以用来为创建的进程分配内存:
- 首次适配(first fit)算法:存储管理器沿着段链表进行搜索,直到找到了一个足够大的空闲区,除非空闲区大小和要分配的空间大小正好一样,否则将该空闲区分为两部分,一部分供进程使用,另一个部分形成新的空闲区。速度很快,因为尽可能找到一个足够大的空闲区。
- 下次适配(next fit)算法:工作方式与首次适配相同,不同点是每次找到合适的空闲区时都记录当时的位置,以便在下次寻址空闲区时从上次结束的地方开始搜索,而不是从头开始。性能略低于首次适配算法。
- 最佳适配(best fit)算法:搜索整个链表(从开始到结束),找到能够容纳进程的最小的空闲区。最佳适配每次都要搜索整个链表,比首次适配慢,同时比首次适配算法或下次适配算法浪费更多内存,因为它会产生大量无用的小空闲区。
- 最差适配(worst fit)算法:可以避免最佳适配分裂很多非常小的空闲区的问题。最差适配算法总是分配最大的可用空闲区,使新的空闲区比较大从而可以继续使用。
如果为进程和空闲区维护各自独立的链表,以上四种算法的速度都能够得到提高。这种分配速度提高的代价是增加复杂度和内存释放速度变慢,因为必须将一个回收的段从进程链表中删除并插入空闲区链表。
快速适配(quick fit)算法:它为那些常用大小的空闲区维护单独的链表。例如,有一个n项的表,该表的第一项是指向大小为4KB的空闲区链表表头的指针,第二项是指向大小为8KB的空闲区链表表头指针,第三项是指向大小为12KB的空闲区链表表头的指针,以此类推。缺点是会分裂大量的小空闲区,合并空闲区非常耗时。
# 虚拟内存
虚拟内存(virtual memory)的基本思想是:每个程序拥有自己的地址空间,这个空间被分割成多个块,每一块称作一页或页面(page)。每一页有连续的地址范围。这些也被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序用到一部分在物理内存中的地址空间时,由硬件立刻执行必要的映射。当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令(装入物理内存的这一系列过程称为缺页中断或缺页错误)。
虚拟内存时对基址寄存器和界限寄存器的一种综合。
# 分页
大部分虚拟内存系统中都使用一种称为分页(paging)的技术。
在任何一台计算机上,程序引用一组内存地址。当程序执行指令:
MOV REG, 1000
时,它把地址为1000的内存单元的内容复制到REG中。地址可以通过索引、基址寄存器、段寄存器或其他方式产生。
由程序产生的这些地址称为虚拟地址(virtual address),它们构成了一个虚拟地址空间(virtual address)。在没有虚拟内存的计算机上,系统直接将虚拟地址送到内存总线上,读写操作使用具有同样地址的物理内存字;而在使用虚拟内存的情况下,虚拟地址不是被直接送到内存总线上,而是被送到内存管理单元(Memory Management Unit,MMU),MMU
把虚拟地址映射为物理内存地址。
虚拟地址空间按照固定大小划分成页面(page)的若干单元。在物理内存中对应的单元称为页框(page frame)。页面和页框的大小通常是一样的。
在实际的硬件中,用一个**“在/不在”**位(present/absent bit)记录页面在内存中的实际存在情况。
缺页中断或缺页错误:当程序访问了一个未映射的页面,例如指令MOV REG, 32780
时。MMU
注意到该页面没有被映射,于是使CPU
陷入到操作系统,这个陷阱称为缺页中断或缺页错误(page fault)。操作系统找到一个很少使用的页框且把它的内容写入磁盘(如果它不在磁盘上)。随后把需要访问的页面读到刚才回收的页框中,修改映射关系,然后重新启动引起陷阱的指令。
可用页号作为页表(page table)的索引,以得出对应于该虚拟页面的页框号。如果在/不在位是0,则将引起一个操作系统陷阱。
# 页表
虚拟地址到物理地址的映射可以概况如下:虚拟地址被分成虚拟页号(高位部分)和偏移量(低位部分)两部分。例如,对于16位地址和4KB
的页面大小,高4位可以指定16个虚拟页面中的一页,而低12位接着确定了所选页面中的字节偏移量(0~4095)。
虚拟页号可用作页表的索引,以找到该虚拟页面对应的页表项。由页表项可以找到页框号(如果有的话)。然后把页框号拼接到偏移量的高位端,以替代掉虚拟页号,形成送往内存的物理地址。
即:虚拟地址(虚拟页号+偏移量)-> 虚拟页号 -> 页表项 -> 页框号(拼接到偏移量的高位端) -> 物理地址。
页表的目的是把虚拟页面映射为页框。从数学角度说,页表是一个函数,它的参数是虚拟页号,结果是物理页框号。通过这个函数可以把虚拟地址中的虚拟页面域替换成页框域,从而形成物理地址。
页表项的结构
- 页框号:最重要的域。
- “在/不在”位:如果是0,则表示该表项对应的虚拟页面现在不在内存中,访问该页面会引起一个缺页中断。
- 保护(protection)位:保护位指出了一个页允许什么类型的访问。例如0表示读/写,1表示只读。
- 修改(modified)位:如果一个页面被修改过(即它是“脏”的),则必须把它写回磁盘。如果一个页面没有被修改过(即它是“干净”的),则只简单地把它丢弃就可以了,因为它在磁盘上的副本仍然是有效的。
- 访问(referenced)位:它的值被用来帮助操作系统在发生缺页中断时选择要淘汰的页面。不再使用的页面要比正在使用的页面更适合淘汰。
- 高速缓存禁止位:对那些映射到设备寄存器而不是常规内存的页面而言是非常重要的。可以禁止高速缓存,保证硬件是不断地从设备中读取数据而不是访问一个旧的被高速缓存的副本。
# 加速分页过程
在任何分页系统中,都需要考虑两个主要问题:
- 虚拟地址到物理地址的映射必须非常快。
- 如果虚拟地址空间很大,页表也会很大。
# 转换检测缓冲区
大多数程序总是对少量的页面进行多次的访问,而不是相反。因此,只有很少的页表项被反复读取,而其他的页表项很少被访问。
在计算机上设置一个小型的硬件设备,将虚拟地址直接映射到物理地址,而不必再访问页表。这种设备称为转换检测缓冲区(Translation Lookaside Buffer, TLB),有时又称为相联存储器(associate memory)或快表。它通常在MMU
中,包含少量的表项。
# 针对大内存的页表
# 多级页表
引入多级页表的原因是避免全部页表一直保存在内存中。特别是那些从不需要的页表就不应该保留。
由索引顶级页表得到的表项中包含有二级页表的地址或页框号。顶级页表的表项0指向程序正文的页表,表项1指向数据的页表,表项1023指向堆栈的页表,其他的页表项(用阴影表示的)未用。
# 倒排页表
针对页式调度层级不断增长的另一种解决方案是倒排页表(inverted page table)。在这种设计中,实际内存中的每个页框对应一个表项,而不是每个虚拟页面对应一个表项。
# 页面置换算法
当发生缺页中断时,操作系统必须在内存中选择一个页面将其换出内存,以便为即将调入的页面腾出空间。如果要换出的页面在内存驻留期间已经被修改过,就必须把它写回磁盘以便更新该页面在磁盘上的副本;如果该页面没有被修改过(如一个包含查询正文的页面),那么它在磁盘上的副本已经是最新的,不需要回写。直接用调入的页面覆盖被淘汰的页面即可。
页面置换算法中都存在的一个问题:当需要从内存中换出某个页面时,它是否只能是缺页进程自己的页面?这个要换出的页面是否可以属于另一个进程?
# 最优页面置换算法
最优页面置换算法每次只会置换标记最大(最少被使用的)的页面,但它是无法实现的一种算法。
# 最近未使用页面置换算法
系统为每一页面设置了两个状态为。当页面被访问(读或写)时设置R
位;当页面被写入(即修改)时设置M
位。这些位包含在每个页表项中。
当发生缺页中断时,操作系统检查所有的页面并根据它们当前的R
位和M
位的值,把它们分为4类:
- 第0类:没有被访问,没有被修改。
- 第1类:没有被访问,已被修改。(第3类的页面在它的
R
位被时钟中断清零而不清除R
位后形成) - 第2类:已被访问,没有被修改。
- 第3类:已被访问,已被修改。
时钟中断不断清除M
位是因为在决定一个页面是否需要写回磁盘时将用到这个信息。
NRU(Not Recently Used,最近未使用)算法随机地从类编号最小的非空类中挑选一个页面淘汰,这个算法隐含的意思是,在最近一个时钟滴答中(大约是20ms)淘汰一个没有被访问的已修改页面要比淘汰一个被频繁使用的“干净”页面好。NRU的主要优点是易于理解和能够有效地被实现,但是性能不是最好的。
# 先进先出页面置换算法
另一个开销较小的页面置换算法是FIFO(First-In First-Out,先进先出)算法。
由操作系统维护一个所有当前在内存中的页面的链表,最新进入的页面在表尾,最早进入的页面放在表头。当发生缺页中断时,淘汰表头的页面并把新调入的页面加到表尾。
缺点是可能会淘汰常用的页面。
# 第二次机会页面置换算法
第二次机会(second chance)算法:FIFO算法可能会把常用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:检查最老页面的R
位,如果R
位是0,那么这个页面既老又没有被使用,可以立刻置换掉;如果是1,就将R
位清0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续搜索。
第二次机会算法就是寻找一个在最近的时钟间隔内没有被访问过的页面。如果所有页面都被访问过,该算法就简化为纯粹的FIFO算法。
缺点是经常要移动页面,既降低了效率又不是很有必要。
# 时钟页面置换算法
一个比第二次机会页面置换算法更好的办法是把所有的页面都保存在一个类似钟面的环形链表中,一个表针指向最老的页面。
当发生缺页字段时,算法首先检查表针指向的页面,如果它的R
位是0就淘汰该页面,并把新的页面插入这个位置,然后把表针前移一个位置;如果R
位是1就清楚R
位并把表针前移一个位置。重复这个过程直到找到了一个R
位为0的页面为止。
# 最近最少使用页面置换算法
已经很久没有使用的页面很有可能在未来较长的一段时间内仍然不会被使用。这个思想提示了一个可实现的算法:在缺页中断发生时,置换未使用时间最长的页面。这个策略称为LRU(Least Recently Used,最近最少使用)页面置换算法。
LRU
在理论上可以实现,但是代价很高。为了完全实现LRU
,需要在内存中维护一个所有页面的链表,最近最多使用的页面在表头,最近最少使用的页面在表尾。困难的是在每次访问内存时都必须更新整个链表。一般使用特定的硬件实现。
# 用软件模拟LRU
NRU(Not Frequently Used,最不常用)算法:将每个页面与一个软件计数器相关联,计数器的初值为0。每次时钟中断时,由操作系统扫描内存中所有的页面,将每个页面的R
位(它的值是0或1)加到它的计数器上。这个计数器大体上跟踪了各个页面被访问的频繁程度。发生缺页中断时,则置换计数器值最小的页面。
NFU
需要做一个小小的修改就能使它很好地模拟LRU
。其修改分为两部分:
- 首先,在
R
位被加进之前先将计数器右移一位; - 其次,将
R
位加到计数器最左端的位而不是最右端的位。
修改以后的算法称为老化(aging)算法。如图,假设在第一个时钟滴答后,页面0~5的R
位值分别是1、0、1、0、1、1(页面0位1,页面1位0,页面2位1,以此类推)。换句话说,在是时钟滴答0到时钟滴答1期间,访问了页0、2、4、5,他们的R
位设置为1,而其他页面的R
为仍是1。
发生页面中断时,将置换计数器值最小的页面。如果一个页面在4个时钟滴答中都没有被访问过,那么它的计数器最前面应该有4个连续的0。
该算法与LRU
有两个区别:
- 在图3-17e中的页面3和页面5,它们都连续两个时钟滴答都没有被访问过了,而在两个时钟滴答前的时钟滴答被访问过。根据
LRU
,如果必须置换一个,会随机挑选一个置换。但对于老化算法,会选择置换页面3,因为页面5在更往前的两个时钟滴答中也被访问过而页面3没有。 - 第二个区别是老化算法的计数器只有有限位(本例为8位),这就限制了其对以往页面的记录。如果两个页面的计数器都是0,只能在两个页面中随机选一个进行置换。实际上有可能其中一个页面上次被访问是在9个时钟滴答之前,另一个页面时在1000个时钟滴答之前。在实践中,如果时钟滴答是20ms,8位一般是够用的。
# 工作集页面置换算法
一个进程当前正在使用的页面的集合称为它的工作集。
如果整个工作集都被装进到内存中,那么进程在运行下一运行阶段之前就不会产生很多缺页中断。若内存太小而无法容纳下整个工作集,那么进程的运行过程中会产生大量的缺页中断。若每执行几条指令程序就发生一次缺页中断,那么就称整个程序发生了颠簸。
不少分页系统都会设法跟踪进程的工作集,以确保在让进程运行以前,它的工作集就已在内存中了。该方法称为工作集模型,其目的在于大大减少缺页中断率。在进程运行前预先装入其工作集也称为预先调页。
在任一时刻t
,都存在一个集合,它包含所有最近k
次内存访问所访问过的页面,这个集合w(k, t)
就是工作集。
当缺页中断发生后,需要扫描整个页表才能确定被淘汰的页面,因此基本工作集算法是比较费时的。
# 工作集时钟页面置换算法
有一种改进工作集费时缺点的算法,它基于时钟算法,并且使用了工作集信息,称为WSClock(工作集时钟)算法。具有实现简单,性能较好的优点。
与时钟算法一样,所需的数据结构是一个以页框为元素的循环表。最初,该表是空的。当装入第一个页面后,把它加到表中。随着更多的页面的加入,它们形成了一个环。每个表项包含来自基本工作集算法的上次使用时间,以及R
位(图中已标明)和M
位(图中未标明)。
工作原理:与时钟算法一样,每次缺页中断时,首先检查指针指向的页面。如果R
位被置为1,该页面在当前时钟滴答中就被使用过,那么该页面就不适合被淘汰。然后把该页面的R
位置为0,指针指向下一个页面,并重复该算法。
当指针指向的页面在R=0
时,参见图3-20c。如果页面的生存时间大于τ并且该页面是干净的,它就不在工作集中,可以置换该页面。如果该页面被修改过,就不能立即申请页框,因为这个页面在磁盘上没有有效的副本。为了避免由于调度写磁盘操作引起的进程切换,指针继续向前走,算法继续对下一个页面进行操作。
原则上,所有页面都有可能因为磁盘I/O在某个时钟周期被调度。为了降低磁盘阻塞,需要设置一个限制,即最大只允许写回n个页面。一旦达到该限制,就不允许调度新的写操作。
# 页面置换小结
算法 | 注释 |
---|---|
最优算法 | 不可实现,但可用作基准 |
NRU(最近未使用)算法 | LRU的很粗糙的近似 |
FIFO(先进先出)算法 | 可能抛弃重要页面 |
第二次机会算法 | 比FIFO有较大的改善 |
时钟算法 | 现实的 |
LRU(最近最少使用)算法 | 很优秀,但很难实现 |
NFU(最不经常使用)算法 | LRU的相对粗略近似 |
老化算法 | 非常近似LRU的有效算法 |
工作集算法 | 实现起来开销大 |
工作集时钟算法 | 好的有效算法 |
最好的两种算法是老化算法和工作集时钟算法,他们分别基于LRU
和工作集。他们都具有良好的页面调度性能,可以有效地实现。在实际应用中,这两种算法可能是最重要的。
# 分页系统中的设计问题
# 局部分配策略与全局分配策略
页面置换的一个主要问题:怎样在互相竞争的可运行进程之间分配内存?
如图所示,三个进程A、B、C构成了可运行进程的集合。假如A发生了缺页中断,页面置换算法在寻找最近最少使用的页面时是只考虑分配给A的6个页面呢?还是考虑所有在内存中的页面?
- 图3-22b的算法被称为局部(local)页面置换算法:只考虑分配给A的页面,生存时间值最小的页面是A5,于是得到图3-22b所示的状态。
- 图3-22c的算法被称为全局(global)页面置换算法:淘汰内存中生存时间最小的页面,而不管它属于哪个进程。
全局算法在通常情况下工作得比局部算法好,当工作集的大小随进程运行时间发生变化时这种现象更加明显。若使用局部算法,即使有大量的空闲页框时,工作集的增长也会导致颠簸。如果工作集缩小了,局部算法又会浪费内存。
管理内存动态分配的一种方法是使用PFF(Page Fault Frequency,缺页中断率)算法。它指出了何时增加或减少分配给一个进程的页面,但却完全没有说明在发生缺页中断时应该替换掉哪一个页面,它仅仅控制分配集的大小。
测量缺页中断率的方法:计算每秒的缺页中断数,可能也会将过去数秒的情况做连续平均。
# 负载控制
一旦所有进程的组合工作集超出了内存容量,就可能发生颠簸。唯一现实的解决方案就是暂时从内存中去掉一些进程。
减少竞争内存的进程数的一个好方法是将一部分进程交换到磁盘,并释放它们所占有的所有页面。即使是使用分页,交换也是需要的,只是现在交换是用来减少对内存潜在的需求,而不是回收它们的页面。
在决定交换出哪个进程时不光要考虑进程大小和分页率,还要考虑它的特性(CPU密集型还是I/O密集型)以及其他进程的特性。
# 页面大小
页面大小时操作系统可以选择的一个参数。
要确定最佳的页面大小需要在几个互相矛盾的因素之间进程权衡。
选择小页面的两个因素(理由):
- 平均情况下,最后一个页面中有一半是空的。多余的空间会被浪费,这种浪费称为内部碎片(internal fragementation)。在内存中有n个段、页面大小为p字节时,会有
np/2
字节被内部碎片浪费。从这方面考虑,使用小页面更好。 - 加入一个程序,它分成8个阶段顺利执行,每阶段需要4KB内存,如果页面大小是32KB,那就必须始终给程序分配32KB内存。如果页面大小是16KB,它就只需要16KB。如果页面大小是4KB或更小,那么在任何时刻它只需要4KB内存。总的来说,大尺寸页面比小尺寸页面浪费了更多内存。
另一方面,页面小意味着程序需要更多的页面,这又意味着需要更大的页表。相同的内存,不同大小的页面,小页面的耗时会比大页面更多。
此外,小页面能够更充分地利用TLB空间。为了进行必要的平衡,操作系统有时会为系统中的不同部分使用不同的页面大小。例如,内核使用大页面,而用户进程则使用小页面。
# 分离的指令空间和数据空间
为指令(程序正文)和数据设置分离的地址空间,分别称为I空间和D空间。每个地址空间都从0开始到某个最大值,比较有代表性的是
在使用这种设计的计算机中,两种地址空间都可以进程分页,而且互相独立。它们分别有自己的页表,分别完成虚拟页面到物理页框的映射。
# 共享页面
在大型多道程序设计系统中,几个不同的用户同时运行同一个程序是很常见的。显然,由于避免了在内存中有一个页面的两份副本,共享页面效率更高。并不是所有的页面都适合共享。特别地,那些只读的页面(诸如程序文本)可以共享,但是数据页面则不能共享。
只要两个共享内存的进程都仅仅是读数据,而不做更改,它们可以一直共享内存页面而不赋复制。但只要有一个进程更新了一点数据,就会触发只读保护,并引发操作系统陷阱。然后会生成一个该页的副本,这样每个进程都有自己的专用副本。两个复制都是可以读写的,随后对任何一个副本的写操作都不会再引发陷阱。这种策略意味着那些从来不会执行写操作的页面(包括所有程序页面)是不需要复制的,只有实际修改的数据页面需要复制。这种方法称为写时复制,它通过减少复制而提高了性能。
# 共享库
现代操作系统中,有很多大型库被众多进程使用,例如处理浏览文件以便打开文件的对话框的库和多个图形库。把所有的这些库静态地与磁盘上的每一个可执行程序绑定在一起,就会使它们变得更加庞大。
一个更加通用的技术是使用共享库(在Windows中称作DDL或动态链接库)。
共享库的思想:当一个程序和共享库(与静态库有些许区别)链接时,链接器没有加载被调佣的函数,而是加载了一小段能够在运行时绑定被调用函数的存根例程(stud routine)。依赖于操作系统和配置信息,共享库或者程序一起被装载,或者在其他所包含函数第一次被调用时被装载。当然,如果其他程序已经装载了某个共享库,就没有必要再次装载它了——这正是关键所在。当一个共享库被装载和使用时,整个库并不是被一次性的读入内存。而是根据需要,以页面为单位装载的,因此没有被调用到的函数是不会被装载到内存中的。
优点:
- 可以使可执行文件更小、节省内存空间。
- 如果共享库中的一个函数因为修正了一个bug被更新了,那么并不需要重新编译调用了这个函数的程序。旧的二进制文件依然可以支持工作。
# 内存映射文件
共享库实际上是内存映射文件(memory-mapped file)的一种特例。内存映射文件的思想是:进程可以通过发起一个系统调用,将一个文件映射到其他虚拟地址空间的一部分。
如果两个或两个以上的进程同时映射了同一个文件,它们就可以通过共享内存来通信。
# 清除策略
为保证有足够的空闲页框,很多分页系统有一个称为分页守护进程(paging daemon)的后台进程,它在大多数时候睡眠,但定期被唤醒以检查内存的状态。如果空闲页框过少,分页守护进程通过预定的页面置换算法选择页面换出内存。如果这些页面装入内存后被修改过,则将它们写回磁盘。
# 虚拟内存接口
分布式共享内存。允许网络上的多个进程共享一个页面集合。
# 有关实现的问题
# 与分页有关的工作
操作系统要在下面的四段时间里做与分页相关的工作:
- 进程创建时。确定程序和数据初始大小。
- 进程执行时。分配磁盘交换区空间,用程序正文和数据对换区进程初始化。
- 缺页中断时。
- 进程终止时。释放进程的页表、页面和页面在硬盘上所占用的空间。
# 缺页中断处理
缺页中断发生时的事件顺序:
- 硬件陷入内核,在堆栈中保存程序计数器。
- 启动一个汇编代码例程保存通用寄存器和其他易失的信息,以免被操作系统破坏。
- 当操作系统发现一个缺页中断时,尝试发现需要哪个虚拟页面。
- 一旦知道了发生缺页中断的虚拟地址,操作系统检查这个地址是否有效,并检查存取与保护是否一致。如果不一致,向进程发出一个信号或杀掉该进程。如果地址有效且没有保护错误发生,系统则检查是否有空闲页框。如果没有空闲页框,执行页面置换算法寻找一个页面来淘汰。
- 如果选择的页框“脏”了,安排该页面写回磁盘,并发生一次上下文切换,挂起产生缺页中断的进程,让其他进程运行直至磁盘传输结束。无论如何,该页框被标记为忙,以免因为其他原因而被其他进程占用。
- 一旦页框“干净”后(无论是立刻还是在写回磁盘后),操作系统查找所需页面在磁盘上的地址,通过磁盘操作将其装入。该页面正在被装入时,产生缺页中断的进程仍然被挂起,并且如果有其他可运行的用户进程,则选择另一个用户进程运行。
- 当磁盘中断发生时,表明该页已经被装入,页表已经更新可以反映它的位置,页框也被标记为正常状态。
- 恢复发生缺页中断指令以前的状态,程序计数器重新指向这条指令。
- 调度引发缺页中断的进程,操作系统返回调用它的汇编语言例程。
- 该例程恢复寄存器和其他状态信息,返回到用户空间继续执行,就好像缺页中断没有发生过一样。
# 分段
分页虚拟内存都是一维的,虚拟地址从0到最大地址,一个地址接着另一个地址,难以管理长度经常变动的数据结构。对许多问题来说,有两个或多个独立的地址空间可能比只有一个要好得多。
段(segment)地址空间:在机器上提供多个互相独立的地址空间。每个段由一个从0到最大的线性地址序列构成。每个段的长度可以是0到某个允许的最大值之间的任何一个值。不同的段的长度可以不同,并且通常情况下也都不相同。段的长度在运行期间可以动态改变。
段是一个逻辑实体。一个段可能包括一个过程、一个数组、一个堆栈、一组数值变量,但一般它不会同时包含多种不同类型的内容。
优点:
- 简化对长度经常变动的数据结构的管理。
- 每个程序都位于一个独立的段中并且起始地址是0,如果一个程序分为多个段,重新编译一个段程序(程序的一部分)不需要编程整个程序。
- 有助于在几个进程之间共享过程和数据。
- 可以为不同的段设置不同的权限。
分页与分段的比较
考查点 | 分页 | 分段 |
---|---|---|
需要程序员连接正在使用这种技术吗? | 否 | 段 |
存在多少线性地址空间? | 1 | 许多 |
整个地址空间可以超出物理存储器的大小吗? | 是 | 是 |
过程和数据可以被区分并分别被保护吗? | 否 | 是 |
其大小浮动的表可以很容易提供吗? | 否 | 是 |
用户间过程的共享方便吗? | 否 | 是 |
为什么发明这种技术? | 为了得到大的线性地址空间而不必购买更大的物理存储器 | 为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。 |
# 纯分段的实现
分段和分页的实现本质上是不同的:页面是定长的而段不是。
在系统运行一段时间后内存被划分为许多块,一些块包含着段,一些则成了空闲区,这种现象称为棋盘碎片或外部碎片(external fragmentation)。空闲区的存在使内存被浪费了,可以通过内存紧缩来解决。