存储管理学习笔记
重新复习《现代操作系统》对存储管理这一章做一下简单的归纳和总结
操作系统中管理分层存储器体系的部分称为存储管理器
1.无存储器抽象
早期的计算机都没有存储器抽象,每一个程序都直接访问物理内存,即我指令指明用哪个地址,就用了对应的物理地址
在这种情况下,要想在内存中同时运行两个程序是不可能的,因为第二个程序在运行时会立即擦除掉在相同位置放置的第一个程序的数据
这个时期,存储器模型就是物理内存,在只有一个操作系统和一个用户进程的情况下,在当时发展出了3种改进的模式
值得一提的是,第三种方案用于早期的个人计算机中,在ROM中的系统部分称为BIOS
按上面的方式组织系统时,通常同一个时刻只能有一个进程在运行,一旦用户键入一个命令,操作系统就把需要的程序从磁盘复制到内存中执行;当进程运行结束后,操作系统在用户终端显示提示符并等待新的命令;当收到新的命令后,他把新的程序装入内存,覆盖前一个程序。
但是,如何在不使用内存抽象的情况下运行多道程序呢?
我们可以这样做,先把当前内存中的所有内容保存到磁盘文件中,然后把下一个程序读入内存再运行即可,只要在某一个时间内存中只有一个程序,就不会发生冲突(我感觉有点像宏观上的并行)
这样看起来很美好,但实际存在一个致命的bug,即比如在程序A中我让小明去商店买个东西,然后在程序B中我让小红去商店找老板要钱,小明家和小红家在不同地方(即内存中的地址不同),商店只是一个统称,但程序不知道,小红在要钱时又跑到小明去的商店去要,这样一来,程序自然就崩了,为解决这个问题,我们使用 静态重定位 的技术来修正它,即当运行到程序B时,我把小红家的地址加到每一个小红将要跳转的地方去(如上面就变为小红家那里对应的商店),这样一来小红就不会走错路了。
2.一种存储器的抽象:地址空间
地址空间是内存的一种抽象,它是一个进程可用于寻址内存的一套地址集合。每一个进程都有一个自己的地址空间,并且这个地址空间独立于其他进程的进程空间。(除了在一些特殊情况下进程需要共享他们的地址空间)
下面思考一个问题,如何使每个程序都拥有自己的地址空间。即让我程序A中的地址28所对应的物理地址 与我程序B中的地址28所对应的物理地址不同,如果能这样就实现了每个程序有着自己的地址空间,我可以在自己的地址空间里为所欲为,丝毫不担心最终的物理地址会与别人冲突。
为了解决这个问题可以使用一种简单的动态重定位技术,他所做的是简单的把每个进程的地址空间映射到物理内存的不同部分。(我觉得 映射 才是重点)
intel以前所采用的经典办法是给每个cpu配置两个特殊的寄存器:基址寄存器(用来存程序物理的首地址) 与界限寄存器(用来存程序长度)
这样在程序加载时,我直接先在内存中找个空闲地方把程序一一放进去就行了,地址啥的先不用管(这个行为导致内存会不够,最终引出交换技术与虚拟内存技术)
当一个进程运行时,程序的起始物理地址加载到基址寄存器中,程序的长度加载到界限寄存器中,这样由于首址知道,长度也知道,我就不用再担心地址混淆的问题了
每次一个进程访问内存,取一条指令,读或写一个数据字,cpu硬件会在把地址发送到内存总线前,自动把基址值加到进程发出的地址值上。同时,它还会检查程序提供的地址是否等于或大于界限寄存器里的值,如果访问的地址超过了界限,就会产生错误并终止访问。
但是使用这个技术也有缺点,就是每次访问内存前都要进行加法和比较运算,特别是加法,影响整体的运行速度。
3.交换技术
由于物理内存的大小是有限的,而在一个操作系统中一般都会启动多个进程,每个进程都能轻易占据个几十或上百兆的内存,这样一来,若所有进程都先一直保存在内存中则会需要巨大的内存,这样是不行的。内存大还可以,若内存本来就小,这样一来,内存便超载了
如果内存超载了,那该怎么办?
通用的两种解决办法便是 交换技术 与虚拟内存技术
交换技术 便是把一个进程完整调入内存,使该进程运行一段时间,然后把它存回磁盘。空闲进程主要存储在磁盘上,所以当他们不运行时就不会占用内存
需要注意的是,这种交换在内存中会产生多个空闲区(hole)通过把所有的进程尽可能的向下移动,有可能将这些小的空闲区合成一大块,这种技术称为内存紧缩,但是这个操作通常不进行,因为他要消耗大量的cpu时间。
所以,如何管理这些空闲内存?也成了一个问题
一般而言,有两种方式跟踪内存使用情况:位图 和 空闲链表
a.使用位图的存储管理
使用位图方法时,内存可能被划分成小到几个字或大到几千字节的分配单元,每个分配单元对应位图中的一位,0表示空闲,1表示占用
因为内存的大小和分配单元的大小决定了位图的大小,所以它提供了一种简单的利用一块固定大小的内存区就能对内存进行记录的方法。
这种方法的缺点是,再决定把一个占有k个分配单元的进程调入内存中时,存储管理器必须搜索位图,在位图中找出k个连续0的串。而查找位图中指定长度的连续0串是耗时的工作。
b.使用链表的内存管理
即维护一个记录已分配内存段和空闲内存段的链表,其中链表中的一个结点或者包含一个进程,或者是两个进程间的一个空的空闲区,上面那个图3-6c所示的段链表就可以来表示3-6a所示的内存布局。链表中的每一个节点都包含以下域:空闲区(H)或进程(P)的指示标志,起始地址,长度和指向下一节点的指针。
因为进程表中表示终止进程的结点通常含有指向对应于其段链表结点的指针,因此段链表使用双链表可能比单链表更方便。
当按照地址顺序在链表中存放进程和空闲区时,有几种算法可为创建的进程分配内存,这里假设存储管理器知道为进程分配多大的内存。
- 首次适配算法(first fit)
- 下次适配算法(next fit)
- 最佳适配算法(best fit)
- 最差适配算法(worst fit)
- 快速适配算法(quick fit)
4.虚拟内存
(随着计算机的发展,软件变得越来越大,单纯的交换技术虽然有用,但是对于这些占内存越来越大的软件,将进程不断从内存换入换出的读写时间变得很长,所以由此引发了虚拟内存的概念)
虚拟内存的基本思想是 每个程序都拥有自己的地址空间,这个空间被分割成多个块,每一块可称为一页或是页面。每一页都有连续的地址范围,这些页被映射到物理内存,但并不是所有的页都必须在内存中才能运行程序。当程序引用到一部分在物理内存中的地址空间时,由硬件立即执行必要的映射;而当程序引用到一部分不在物理内存中的地址空间时,由操作系统负责将缺失的部分装入物理内存并重新执行失败的指令。
(实际上,我个人觉得虚拟内存与交换技术的思想是类似的,交换技术是不断把有用的进程与空闲进程从内存到磁盘之间的交换,交换的主体是进程,也就是程序,但是现在单个程序已经很大了,交换程序变得缓慢,于是,我们便会想到能不能把单个程序再分成多个片段,每次把需要用的片段依次调入内存,不用便不调,内存不够便覆盖掉之前的已经用过的片段的位置。实际上,早期大家也这样做过,但是把程序分成多个片段这个工作并不好做,程序员表示苦不堪言,于是我们让计算机来干这个工作,原先的分割程序变成分割程序对应的空间,这样就简单多了,这便就是虚拟内存)
a.分页
大多数虚拟内存系统使用一种分页技术
如,对于指令 MOV REG 1000
由程序产生的这些地址被称为 虚拟地址,他们构成一个虚拟地址空间(virtual address space)
若没有虚拟内存,计算机会将上述指令的地址直接送入内存总线上,对同样地址的物理内存进行读写操作
若有了虚拟内存,计算机会先将该虚拟地址送入 内存管理单元(memory management unit ,MMU),MMU再把虚拟地址映射位物理内存地址。
虚拟地址空间按照固定大小划分为页面(page)的若干单元,在物理内存中对应的单元称为 页框(page frame),页面和页框的大小通常是一样的。而RAM和磁盘之间的交换总是以整个页面为单元进行的
我们以书上的图为例
这个例子中页面大小为4k,
对于语句 MOV REG 0 ;首先将虚拟地址0送入MMU,MMU看到此虚拟地址落在页面0(即在0~4095的范围内),而页面0对应的映射为页框2(8192~12287),便把虚拟地址0转换为物理地址8192,再送入总线。
可以看到,16个虚拟页面可以映射到8个物理页框中的任意一个,但实际上在用的页面也只有8个,若访问的页面没有映射的页框那该怎么办?如上图中带X的页面
当MMU发现页面没有对应的页框时,会使PU陷入到操作系统,这个陷阱称为缺页中断(page fault)。此时操作系统会找到一个很少使用的页框,把他原本的内容写入磁盘,随后把需要访问的页面读到刚才回收的页框中,并修改映射关系,然后重新启动引起陷阱的指令。
MMU如何完成映射操作,如图,以16个4KB页面为例,输入16位虚拟地址,输出15位物理地址,其中低12位为偏移量,高四位为页号,第四位标记是否映射(即为 在与不在 位,为1表示在,为0表示不在,引发缺页中断)
如对于虚拟地址8196,其二进制为0010000000000100,其中高4位0010为页号2,后面的12位为偏移量,可见4位页号可表示16个页面,而12位偏移量可表示一页内4096个字节编址。
b.页表
页表的目的是把虚拟页面映射为页框,虚拟页号可用作页表的索引以找到该虚拟页面对应的页表项,而由页表项可以找到页框号(如果有的话),然后把页框号拼接到偏移量的高位端,以替代掉虚拟页号,形成送往内存的物理地址。
给出页表项的结构
其中,保护位指出一个页允许访问什么类型的访问,简单点的只有1位,0表示读/写,1表示只读,还有3位的,可读,可写,可执行。
修改位:也称脏位,表示这个页面是否修改过,如果被修改过则必须写会磁盘,若未修改过且磁盘存在副本则直接丢弃
访问位:在该页被写或读时,标记该位,当发生缺页中断时,优先选择未标记访问的页面
高速缓存禁止位:禁止该页面被高速缓存。对映射到设备寄存器而不是常规寄存器非常重要,保证硬件是不断从设备中读数据而不是从缓存中
使用虚拟内存后,确实比原来的交换技术要好些,但伴随虚拟内存本身而来的也有些问题,如下
⑴虚拟地址到物理地址映射必须非常快
⑵如果虚拟地址空间很大,页表也会很大
现象:大多数程序总是对少量的页面进行多次的访问
a.加速分页过程
对于问题1,我们基于大多数程序总是对少量的页面进行多次访问这个特点,为计算机设置一个小的硬件设备,将虚拟地址直接映射为物理地址。而不再访问页表。这种设备叫做转换检测缓冲区(Translation Lookaside Buffer,TLB),有时也叫作相联存储器。(我觉得这个方法就跟计算机上的hosts文件一样,将常用域名地址直接存在电脑上,省去了DNS解析的时间)
它通常位于MMU中,包含少量的表项
MMU使用 TLB时进行并发匹配,若不在则选择一个表项清除,从内存中找到缺的页表覆盖
TLB软失效:不在TLB而在内存中,这时候只需要更新TLB
TLB硬失效:不在TLB也不再内存,需要磁盘I/O找到该页表,与软失效处理时间相差百万倍
(此处tlb硬失效与缺页中断 的关系我有点不懂,看上去两者是一样的。但查了查发现又没有人把这两个一起说的;另外找到以一篇有关TLB刷新的博客
http://blog.csdn.net/dog250/article/details/5303549)
b.对于问题2,再引入快表TLB加快虚拟地址到物理地址的转换后,对于巨大的虚拟地址空间的处理,常有2种办法
(1).多级页表
如图,32位的虚拟地址被划分为10位的PT1域、10位PT2域和12位偏移量
在实际访问时,mmu首先用PT1作为索引访问顶级页表得到表项a,然后再利用PT2作为索引访问刚找到的2级页表a得到表项b,表项b含有对应页面的页框号,如果该页面不在内存中,页表项中的在与不在位为0,引起缺页中断;如果该页面在内存中,从二级页表中得到的页框号将与偏移量结合形成物理地址并被放到总线并送入内存中
(2).倒排页表
在实际内存中每一个页框有一个表项,表象记录哪一个(进程,虚拟页面)对定位于该页框
为了增强从虚拟地址到物理地址,需要使用TLB
TLB失效时搜索方法: 用虚拟地址建立一张散列表,当前所有在内存中的具有相同散列值的虚拟页面被链接到一起
5.页面置换算法
当发生缺页中断时,操作系统必须在内存中选择一个页面将其换出内存,以便为即将调入的页面腾出空间。如果页面在内存驻留期间修改过,就必须把它写会磁盘;如果没有修改过,则直接丢弃(因为磁盘上的副本和当前一致,不需要写回)
a.最优页面置换算法
每个页面都将该页面首次被访问前所需要执行的指令数目做标记,置换时选择标记最大的页面(也就是找到最久不用的页面)
(理想算法,无法实现)
b.最近未使用页面置换算法NRU (Not Recently Use)
用R位和M位在一个简单的页面置换算法:当启动一个进程时,它的所有页面这两位都是0,R位定期地清零(如每次时钟中断时),当页面进行修改时M位置1,被访问时R位置1,则共有下面四类情况:
R M
0,没有被访问,也没有被修改 0 0
1,没有被访问,被修改 0 1
2,被访问,没有修改 1 0
3,被访问,被修改 1 1
NRU算法随机的从编号最小的非空类中挑选一个页面淘汰
c.先进先出页面置换算法FIFO
操作系统维护一个所有当前在内存中的页面的链表,最新进入的页面放在表尾,最久进入的页面在表头。当发生缺页中断时从表头淘汰一个页面,并把新载入的页面加到表尾
d.第二次机会页面置换算法
基于FIFO算法,检查老页面的R位,如果R位是0,那么这个页面既老又没有使用,可以立即置换出去;如果R是1,就将R置为0,并把该页面放到链表尾端,修改装入时间像刚装入的一样,然后继续搜索。
e.时钟页面置换算法
由于第二次机会算法,经常在链表中移动页面,效率不是很高。更好的办法是:把所有的页面都保存在一个类似钟面的环形链表中,一个表指针指向最老的页面。
f.最近最少使用页面置换算法(Least Recently Used)
LRU:在缺页中断发生时,置换未使用时间最长的页面。
硬件实现:
①硬件有一个64位计数器,每条指令执行完后自增1,每个页表需有一个能容纳这个计数器值的域。每次访问内存后,将当前计数器值保存到页面的页表项中。一旦发生缺页中断,操作系统检查所有页表项中计数器的值,找到值最小的一个页面,这个页面就是最近最少使用的页面。
②在一个有n个页框的机器中,LRU硬件可以维持一个初始值为0的n∗n位的矩阵。当访问到页框k时,将k行的位都置为1,k列都置为0。在任何时刻二进制数值最小的行就是最近最少使用的。
软件实现: (Not Frequently Used,最不常用算法)
①NFU:将每个页面与一个软件计数器相关联,计数器的初值为0。每次时钟中断时,由操作系统扫描内存中所有页面,将每个页面的R位加到计数器上。缺页中断时选择计数器值最小的页面。
②老化:基于NFU,在R位被加进来之前将计数器右移一位;其次将R位加到计数器最左端。
老化算法有以下缺陷:
1.无法区分在一个时钟滴答内,那个页面被访问的先后顺序。
2.计数器位数只有有限位,当两个页面的计数器值一样时只能随机选择一个。
往往,时钟嘀嗒如果是20ms,8位一般够用
g.工作集页面置换算法
- 请求调页:页面在需求时被调入,而不是预先装入
- 局部性访问行为:进程运行的任意阶段,它都只访问较少的一部分页面
- 工作集:一个进程当前正在使用的页面集合
- 颠簸:若每执行几条指令程序就发生一次缺页中断,那么就称为这个程序发生了颠簸
- 工作集模型:设法跟踪工作集,以确保让进程运行以前,它的工作集就已在内存中了,目的在于大大减少缺页中断率
- 预先调页:在进程运行前预先装入工作集页面
- 当前实际运行时间:一个进程从它开始执行到当前所使用CPU时间总数通常称为当前实际运行时间。
- 生存时间:当前实际运行时间 减去 上次使用时间
基本思路,找出一个不在工作集中的页面并淘汰它。
假定由硬件记录R位和M位,每个时钟滴答中有一个定期的时钟中断会用软件方法清除R位,当发生缺页中断时:
扫描每个表
1. 若R位为1,把当前实际时间写进页表项的“上次使用时间”域中,继续扫描。
2.若R位为0,且生存时间大于t,则说明不在工作集中置换该页面,扫描继续进行更新剩余表项
3.若R位为0,且生存时间小于等于t,则记住生存时间最长的页面
4.若扫描结束后,R位都是0,则置换所记录生存时间最长的页面;若所有页面都是1,则随机选一个页面置换,最好为干净的。
h.工作集时钟页面置换算法
类似于时钟算法,有一个循环链表和一个指针,每个页表项有R和M位
当发生缺页中断时:
1.从当前指针开始,判断R值时候为1,若为1,则更新上次使用时间,并将R位置为0,继续搜索
2. 若R为0,且生存时间大于t,W位为0,则置换出该页
3. 若R为0,生存时间大于t,W位为1,写回该页面,指针继续前进;为了降低磁盘阻塞,只允许最大写回n个页面
4. 若扫描一圈回到起点,则有两种可能:
4.1 调度过写操作,置换第一个写完成的页面(由于磁盘驱动,未必是第一个调度的页面)。
4.2 未调度过写操作,随机置换一个干净的页面,若没干净页面,则选当前页面写回磁盘,干净后置换
页面置换算法总结:
6.分页系统设计时遇到的问题
(1)局部分配与全局分配
上面主要讲了缺页中断与页面置换算法,但基本是对应着一个进程来讲的,下面思考这样一个问题,若同时存在多个进程,怎样在他们的相互竞争之中来实现内存分配?
例如当进程A发生缺页中断时,到底是从分配给A自己的页面中寻找最近最少使用的页面?还是该从整个内存中中找到最近最少使用的页面并把他分配给A(要知道,此页面可能同时正被其他进程所占着)前一种分法叫做局部页面置换算法,后一种算法叫做全局页面置换算法。
局部分配算法可以有效地为每个进程分配固定的内存片段,全局分配算法可以在运行进程之间动态的分配页框,因此分配给各个进程的页框数是随时间变化的。全局分配策略更为适宜。
管理内存动态分配的一种方法是使用PFF(Page Fault Frequency,缺页中断率)算法,它指出了何时增加或减少分配给一个进程的页面数,但他并么有指明在发生缺页中断时该替换谁,它仅仅只能控制分配集的大小
A :分配页框太少,缺页率过高
B :分配的页框太多
A与B之间为合理的缺页中断数
对于上面的一大类算法,缺页中断率都会随着分配的页面的增加而降低。
(2).负载控制
为了减少竞争内存的进程数,将一部分进程交换到磁盘,并释放他们所有的页面
在交换时,不仅要考虑进程的大小、分页率也要考虑他的特性(如I/O密集或CPU密集)
(3).页面大小
大页面:可能会有内部碎片(最后一个页面将近一半是空的)
小页面:需要更大的页表,页表的占用和内存磁盘交换时间都需要更多
最有页面大小公式:p=sqrt(2se) (根号下)
s:进程平均大小
e:每个页表项所需要的字节
p:页面大小
(4).分离指令空间和数据空间
(5).共享页面
每个进程在它的进程表中有两个指针:一个指向I空间页表,另一个指向D空间页表
两个进程共享页面时,各自有各自的页表,但都指向相同的页面(只读的)
写时复制:当某进程需要对共享页表进行修改时,触发只读保护,陷入内核,复制一份该副本,且该副本可写
(6).共享库(win中称为dll)
任何在目标文件中被调用了但是没有被定义的函数称为未定义外部函数
当一个共享库被装载和使用时,是以页面为单位装载的,因此没有被调用的函数是不会被装在到内存中的
编译共享库时,需要用一个特殊的编译选项告知编译器,不要产生使用绝对地址的指令,只能使用相对地址。只是用相对偏移量的代码被称作位置无关代码
(7).内存映射文件
进程可以通过发起一个系统调用,将一个文件映射到其虚拟地址空间的一部分。
(8).清除策略
分页守护进程:后台进程,大多数时候睡眠,但定期被唤醒检查内存的状态,若果空闲页框过少,分页守护进程通过预定的页面置换算法选择页面置换出内存
策略之一双指针时钟:前指针搜索脏页面写回磁盘,后指针正常进行页面置换
(9).虚拟内存接口
某些高级操作系统,程序员可以对内存映射进行控制,并通过非常规的方法增强程序行为。允许程序员对内存映射进行控制的一个原因是为了允许两个或多个进程共享同一部分内存,而页面共享也可以用来实现高新能的消息传递系统。
7.有关实现的问题
(1).操作系统在下面4段时间内做与分页有关的工作
进程创建时,进程执行时,缺页中断时,进程终止时
(2).缺页中断处理时发生的事件顺序
1.硬件陷入内核,在堆栈中保存程序计数器。
2.启动一个汇编例程保存通用寄存器和其他易失信息,这个例程将操作系统作为函数来调用
3.找出缺少的虚拟页面,通常硬件寄存器包含了这一信息,若没有的话,操作系统必须检索程序计数器,取出这条指令,用软件分析这条指令。
4.检查虚拟地址是否有效,并检查存取与保护是否一致,若不一致则发出信号或杀死进程。若果地址有效并且没有保护错误发生,系统检查是否还有空闲页框。如果没有则用页面置换算法淘汰一个页面。
5.若果选择的页框脏了,安排该页写回磁盘,并发生上下文切换,挂起该缺页中断的进程,让出资源直到传输结束。而该页框被标记为忙,不允许其他进程占用。
6.一旦页框干净,操作系统查找所需页面在磁盘上的地址,发起磁盘操作将其装入。同时,挂起该缺页中断进程。
7.当磁盘中断发生时,表明已装入,页表已经更新,页框也标记为正常状态。
8.恢复发生缺页中断指令以前的状态,程序计数器重新指向这条指令。
9.调度引发缺页中断进程,操作系统返回调用它的汇编语言例程。
10.该例程恢复寄存器和其他状态信息,返回用户空间继续执行,好像没发生过缺页中断一样。
(3).指令备份
通过一个隐藏的内部寄存器。在每条指令执行之前,把程序计数器的内容复制到该寄存器上。这些机器可能会有第二个寄存器,用来提供哪些寄存器已经自动增加或者自动减少,以及增减数量的信息
(4).锁定内存页面
两种方法:
1.钉住:可以锁住正在做I/O操作的内存页面以保证它不会被移出内存。
2.在内核缓冲区中完成所有的I/O操作,然后再将数据复制到用户页面
(5).后备存储
在磁盘上分配页面空间的最简单的算法是在磁盘上设置特殊的交换分区,甚至文件系统划分一块独立的磁盘
- 将整个进程映像复制到交换区,以便随时可将所需内容装入
- 将整个进程装入内存,并在需要时换出
(6).策略与机制分离
控制复杂度的一种重要方法就是把策略从机制中分离出来
基于Mach的分离方法
一个底层的MMU处理程序
一个作为内核一部分的缺页中断处理程序
一个运行在用户空间的外部页面调度程序
页面置换算法的位置:外部页面调度程序或者内核中
放在外部需要某种机制把R、M位信息传递给外部页面调度程序
这样分离主要优势是更多的模块化代码和更好的适应性。主要缺点是多次交叉“用户-内核”边界引起的额外开销,以及系统模块间消息传递的额外开销
8.分段
先来思考这样的问题,既然已经有了分页机制,为什么还要引入分段?分段与不分段的区别又在哪里?
先来看这个例子
对于一个程序,在编译器进行编译的过程中,会建立许多表,其中可能包括:
(1)源程序正文
(2)符号表,包含变量的名字和属性
(3)包含用到的所有整型量和浮点常量的表
(4).语法分析树,包含语法分析的结果
(5).编译器内部过程调用使用的堆栈
前4个表会在编译过程中动态增长,最后一个表会以不可预计的方式增长或缩小,对于这种在不断变化的空间,我们总不能人为地实时调用内存空间来满足它的需求,那样程序员会累死的,于是我们就希望能不能有这样一种空间,不同类型的元素放入不同的空间,这种空间地址从0开始而且能在程序运行时动态变化,没错,他就叫做段。
每个段由一个从0到最大值之间的任何一个值构成,不同的段长度可以不同,段的长度在运行期间可以动态改变。
但是要注意,段是一个逻辑实体,为什么要抽象出一个他,还不是为了我们人类在使用时方便,逻辑上易理解,好操作。
一般段不会同时包含多种不同类型的内容,故不同的段可以有不同种类的保护
a.纯分段的实现
要注意,分页和分段的实现本质上是不同的,页面是定长的而段不是。所以当段淘汰与生成新段后,段与段之间的空闲区会形成外部碎片。这可以通过内存紧缩来解决
b.分段与分页的结合
(1)在MULTICS系统中(第一个实现段页结合的系统)
将每一个段都看成一个虚拟内存,按照前面讲的分页的方法对段进行分页
每个程序都有一个段表,每个段对应一个描述符,一个段描述符包含了该段是否在内存中的标志,只要该段的任何一部分在内存中这个段就被认为在内存中,并且他的页表也会在内存中。如果一个段在内存中,他的描述符将包含一个18位的指向他的页表的指针。段在辅助存储器中的地址不在段描述符中,而是在缺段处理程序使用的另一个表中。
在该系统中,一个地址由两部分构成:段和段内地址 。段内地址又进一步分为页号和页内的字。
段内地址
地址 = 段号:(页号,页内偏移量)
基本步骤为:根据段号找段描述符,根据段描述符判断段的页表在或不在内存中,若在检查所请求虚拟页面的页表项,若页面不在内存,则产生缺页中断,若在,就从页表项中取出这个页面在内存中的起始地址,将起始地址加上偏移量得到要访问的字的物理地址
(2)在Intel Pentium系统中(这一部分的深入可参考我的 一个操作系统的实现的学习笔记:)
该系统中虚拟内存的核心是两张表:GDT(全局描述符表) 和LDT(局部描述符表)
每个程序都有自己的LDT,同一计算机上的所有程序共享一张GDT表
LDT描述局部于每个程序的段,包括代码,数据,堆栈等;GDT描述系统段,包括操作系统本身
为了访问一个段,程序必须把这个段的选择子(selector)装入机器的6个段寄存器中的任一个,在运行过程中,cs寄存器保存代码段的选择子,DS寄存器保存数据段的选择子,其他选择子不太重要,每个选择子是16位数的
选择子中的一位指出这个段是局部的还是全局的,还有2位和保护有关,其他13位是LDT或GDT的表项编号
基本步骤为:首先根据第2位选择LDT或是GDT,随后选择子被复制进一个内部被擦除寄存器中并且他的低3位被清0;最后LDT或GDT表的地址被加到它上面,得出一个指向描述符的指针。
微程序知道我们要使用哪个段处理器后,他就能从内部寄存器中找到这个对应与这个选择子的完整的描述符
硬件随后根据段长度域检查偏移量是否超出了段的结尾,如果是,发生一次陷阱。
假如段在内存中并且偏移量也在范围内,处理器接着把描述符中32位的基址和偏移量相加形成线性地址
如果禁止分页,线性地址就被解释为物理地址并被送往存储器用于读写操作;如果允许分页,线性地址将通过页表映射到物理地址。