信号量机制
由于双标志检查法中检查和上锁这两个操作无法一气呵成,从而导致了多个进程可能同时进入临界区的问题,且之前学习的所有进程同步的解决方案都无法遵循让权等待原则,因此Dijkstra提出了信号量机制算法,其是一个卓有成效的可以实现进程互斥和同步的方案
其会设置一个信号量,这个信号量有两种,一种是整数,另一种是复杂的记录型变量,其表示系统中某种资源的数量,该算法还会通过wait()和signal()这一对原语实现信号量的增减,调用该原语时需要传入该信号量,这一对原语又称为PV操作,做题时常把这两个操作称为P(S)【wait(S)】、V(S)【signal(S)】
信号量代表资源还有多少,进程使用资源前要执行P(S)、使用后会执行V(S)
在整型信号量中,wait原语实现的操作是如果信号量不大于0,说明已经没有该资源可以供给进程使用了,那么就会将该进程阻塞,反之则使用该进程,会将信号量减一,而在V(S)中,会将信号量+1,说明进程已经使用完该资源,所以可用资源的数量+1
这种方式由于检查和上锁是一气呵成的,所以能避免并发、异步导致的问题,这时有长得帅的同学要问了,假如一个进程他执行P(S)的时候卡死在while循环了,同时原子操作又是不可中断的,那这个进程不是一直卡死在那了?这其实是个问题,但是他就是能不卡死在这里,这是因为当原子操作无法被执行的时候,是会将状态恢复到原子状态执行前的,所以能避免一直卡死的问题
整型信号量机制存在的问题就是不满足让权等待,是会发生忙等的
最后值得一提的是,P函数会令信号量减一,而V函数会令信号量加一
为了解决整型信号量的缺陷,提出了记录型信号量,简单来说就是定义一个数据结构,该数据结构内部存有剩余资源数的int值和一个等待队列
其中P(S)会将其中的信号量减一,代表该进程要使用这个资源,然后判断资源数是否小于0,若是则说明没有资源可用,此时阻塞当前进程,而V(S)会将信号量+1,代表已经释放了资源,接着判断信号量是否不大于0,若是则说明还有进程想要使用该资源,则唤醒进程
下面是例子,可以看到最开始两台打印机给P0和P1服务,value值从2变为了0,而P2和P3则暂时无法使用资源则导致value值为-2,代表有两个进程等待使用该资源,且这两个进程会被阻塞之后加入等待队列,当其他进程使用资源完毕释放资源后发现还有进程想要使用该资源会唤醒等待队列上的进程使用该资源
最后我们来看看总结
注意如果考试中出现了P(S)或V(S)的操作,除非特别说明,否则默认S为记录型信号量
实现进程互斥、同步、前驱关系
上面我们科普了信号量机制,然后我们来用信号量机制来实现进程的互斥、同步和前驱关系
其实现互斥关系就很简单了,因为之前我们都已经讲过了,就是指定一个互斥信号量,然后进入临界区之前调用P(S),退出临界区前用V(S)即可
值得一提是我们最好还是学习一下如何自己定义信号量,如果题目没特别说明,我们也可以把信号量写成semaphore mutex=1这样的形式
进程同步的要求是要让各个并发的进程有序地推进,简单来说就是要让代码按照我们的犹豫顺序执行
我们要实现进程同步,就要在希望先执行的代码后执行V(S),在希望后执行的代码前加上P(S),比如在我们下图中的例子里,若是P1先执行,则那么执行到V(S)之后会将信号量+1,假设此时立刻执行到P2,那么在P(S)执行后也不会被阻塞,可以正常继续往下执行,反之如果P2先执行,则进程会因为执行P(S)立刻阻塞,直到执行了P1的代码V(S)之后,才会被唤醒继续执行
而要实现前驱也非常简单,就是将每条线都加入一个变量,然后在对应方向的前操作后加入V(),后操作前加入P()
最后我们来看看总结
生产-消费者问题
系统中有一组生产者和消费者进程,共享一个初始值为0,大小为n的缓冲区,条件是只有缓冲区不满,生产者这才能将产品放入缓冲区中且只有当缓冲区不空时消费者才能从中取出产品,否则必须等待,最后一个条件是缓冲区是临界资源,各个进程必须互斥地进行访问
这里的三个条件其实就告诉我们三个条件
- 缓冲区没满时生产者进程才可以进行生产
- 缓冲区没空时消费者进程才可以进程消费
- 生产者和消费者进程是互斥的
要实现缓冲区有内容时消费者进程才能消费,就要使用PV操作,刚开始缓冲区的数据量为0,所以full的值为0,然后消费者取出数据前限制性V操作取出后执行P操作,这也是上一节我们讲过的前V后P原则,反之亦然
最后要设置一个互斥信号量,那肯定是1了,这个不用多说
最后我们在生产者的进程要执行将产品放入缓冲区的代码前先执行P(empty)之后执行V(full),前者指的是消耗一个空闲缓冲区,令空闲缓冲区的值-1,而后者指的是增加一个产品,由于还需要进程互斥,因此执行将产品放入缓冲区的代码前还需要执行P(mutex),执行完后还要执行V(mutex),这两个代码的执行的作用其实就是加锁解锁
对另一个消费者进场来说也是同理,这里就不谈了
值得一提的是,生产者和消费者中的P(mutex)和P(empty)这两个代码是不可以反过来的,否则会导致死锁,但是V的两个函数是可以的,因为这里没有持有锁而是释放锁,改变顺序没有影响。要是在记不住,就记得反正加锁的P代码执行肯定是要放到最里面那一层的就完了
另外一个是消费者的使用产品代码逻辑上是可以放到PV之间的,但这样会导致进程每次要执行的代码变成,影响效率,所以这里要放到外面去
最后我们来看看总结
多生产-多消费者问题
接着我们来讲多个生产者和消费者的问题,我们这里题目自己看,总之我们根据题目容易分析出从缓冲区的大小为1且有两个生产者和消费者进程
我们这里首先设置一个互斥关系的信号量,然后我们根据图中关系,我们容易知道还需要两个用于表示盘子中对于水果的变量来联系者四个进程,我们这里分别设置为apple和orange,他们初始值都为0,这是当然的,最开始盘子里肯定是啥也没有
根据前V后P的原则,我们可以得到我们的进程间的PV关系图如下
然后我们可以看到,父亲进程每次将水果放入盘子前都检查盘子的信号量是否已经为0,也就是是否已经放入了一个水果导致盘子满了,若是则阻塞,反之则放入苹果并将苹果的数量+1,此时plate变量为由于执行了P(V)函数已经导致其值为0了,这样就告知其他进程这个盘子已经满了,比如妈妈进程就会发现盘子已满,则会阻塞
然后女儿进程我那个里面取出苹果时首先调用P检查盘子里是否有苹果,若有则调用P函数取出苹果,搞定之后利用V函数将盘子的信号量恢复为1,代表盘子还可以装一个水果,儿子进程同理
我们上面的代码还是使用了互斥信号量来保证线程互斥,但其实在我们这个例子里即使不设置专门的互斥变量也不会出现多个进程同时访问盘子的情况
这是因为我们这里的缓冲区大小为1,且三个同步信号量最多只有一个是1,这个1就正好可以承担作为同步信号量的作用,所以我们可以不设置同步信号量
当然,如果说各个进程里的数量不为1,那我们还是要设置同步信号量的,其实最好就还是好好设置同步信号量比较好,反正写上去了肯定不会错
最后我们值得一提的是,我们分析一个问题时最好从事件的角度来考虑而不是从进程的角度来考虑,如果用后者分析,会搞得凭空多出许多变量,徒增复杂度,用前者分析会好很多
吸烟者问题
题目自己看,这个问题其实本质来说是一个生产者对应多个消费者的问题
分析该问题时,我们会发现消费者一次需要两个材料可以进行消费,而生产者每次又会供给两种材料,因此我们可以将两种材料设置为一个组合,这样就可以理解为生产者每次提供一种材料,只不过材料一共有三种而已,那么桌子容量毫无疑问此时也是1了
根据前V后P的原则,我们可以得到消费者和生产者的关系如下图,这里要注意题目中说明了当消费者消费完之后会提示生产者提供下一组材料,那么三个消费者和生产者之间还会通过一个信号量存在联系,同样也是根据前V后P原则构建关系
由于同题目中要求生产者以轮询方式为消费者提供服务,因此我们这里还会构建i变量来实现轮询,三个组合对于三个变量,生产者每次调用对于的V函数时将对于变量+1,然后调用P函数将自身线程阻塞,等待消费者唤醒
消费者每次调用P函数从桌上取出自己想要的组合,然后执行了动作之后调用V函数唤醒生产者进程
我们这题最重要的还是生产者要生成多种产品的话,那么各个V操作应该要放在各自对于的“事件”也就是V函数的调用之后的位置,这点一定要记住
读写者问题
读写者问题是比较麻烦的一种,我们先来看看题目要求
首先我们分析里面的关系会发现,写-读,写写进程都是要求互斥的,其他则不要求
那么最简单的方法当然是建立一个信号量用于在写文件和读文件的都加锁,这样首先实现了写文件和读文件进程之间的互斥
但是这样就还实现读文件不互斥,为了实现读文件互斥,我们可以再增加一个变量用于记录当前有几个读进程正在访问文件,增加if条件只让第一个读进程加锁,其他读进程不加锁,当最后一个读进程释放时解锁,这样第一个以后的读进程就会跳过加锁过程直接执行读操作,这样读进程之间就不互斥了
但是这样还是有问题,因为可能会出现并发时两个读进程都认为自己满足if条件从而加锁导致第二个读进程阻塞的情况,解决该问题我们可以再加上一个同步信号量加到读进程加锁和读进程解锁的过程中来保证不会出现读进程并发阻塞问题,注意不可以随便改变这个同步变量加锁解锁的位置,不然可能会导致各种预期外的问题
最后这里还存在的问题是如果读进程一直进来,那么就会出现写进程饥饿的问题,我们可以在设置一个同步信号量来解决该问题,在写文件前先加入该同步锁,然后在读文件的加锁代码上也加入该同步锁,这样在各种情况下都可以实现写优先,保证写进程不发生饥饿现象
不过严格来说这个实现的并不是写优先,而是相对公平的先来先服务原则,所以该算法也称读写公平法
到此为止这题算法解决了,已经可以完美实现了
这一题我们要学习的内容就是如果我们想要某一个进程之间不互斥的同时其他进程相互互斥,就可以选择添加计数器的方式来实现这个需求,当然,我们还需要认真体会写进程饥饿这个问题的解决方式,这个地方是精髓
哲学家就餐问题
来看看哲学家就餐问题,这题的关键在于如果不合理设置代码,则会出现死锁现象
我们首先将五个筷子设置为五个信号量,用数组来设置,哲学家用0-4编号,哲学家左边的筷子编号为i,右边的则为(i+1)%5,这样可以做到当数组到达边界时重置为0
我们可以简单设置哲学家要拿左右筷子的P函数,吃饭完之后进行V函数,这样的设置非常简单合理,但是这里存在一个五个哲学家每次都总是同时拿起筷子的问题,这样会导致线程一直阻塞
解决这种问题有两种方法,第一种是最多允许四个哲学家同时进餐,这样保证就算一起拿筷子也肯定有一个进程是满足的,第二种方式是要求奇数号哲学家先拿左边的筷子,这种方式保证各个进程拿筷子时首先和旁边的进程先竞争,失败则直接阻塞,这样保证进程会被满足,前者的实现方式可以新建一个变量用于记录此时有几个进程正在获取筷子,后者则更是简单,用if条件判断即可
第三种方式是再新增一个互斥变量,每次保证只允许一个哲学家在获取筷子,这个方法也可以,就是会降低并发量
最后我们来看看总结
管程
之前我们介绍的通过信号量机制来实现进程同步、互斥存在的问是程序编写复杂困难而且容易出错,因此引入了管程这种高级同步机制
管程是一个特殊的软件模块,其内部存有局部对共享数据结构的说明以及用于对数据结构进行调用的函数,且有设置初始值的语句和管程名,这个其实和我们Java中的类这个概念非常像,甚至要访问管程内的共享数据都只能通过调用管程的函数来访问,注意管程每次只允许一个进程在管程内执行内部过程
比方说我们下面的代码就是实现管程的代码,注意我们这里的代码是伪代码,不是真的就这样的,方便理解而已
可以看到里面提供了insert和remove方法,这样我们可以直接调用这个管程来实现生产者消费者进程的功能,内部自己会帮我们解决同步问题,可以看到这里在生产者进程中判断自己是否是第一个生产者,若是则唤醒消费者线程,这样做的目的是第一个生产者生产了内容就让消费者去消费,消费者则判断自己是不是最后一位消费者进程,若是则说明只要消费者进程正常执行则产品必定会被消耗完,因此会唤醒生产者进程生产产品
引入管程的目的就是为了更加方便得实现进程互斥和同步,程序员只能通过管程提供的特定入口才能访问共享数据且管程每次只开放一个入口,这意味着管程每次只能让一个进程或线程访问共享数据,且可以在管程中设置条件变量以及等待/唤醒的操作来解决同步问题
Java中也是有管程的对应函数的,最经典的就是synchronized,其是用管程模型实现的同步函数
最后我们来看看总结
死锁
之前我们讲过的哲学家进餐问题中就会发生死锁
死锁指的是在并发环境下各进程因竞争资源而造成的一种互相等待对方手里的资源导致各个进程都阻塞无法向前推进的现象,发生死锁时如果没有外力干涉,那么发生死锁的进程都无法向前推进
死锁、饥饿、死循环这是三个概念,反正,死锁和饥饿都有可能是操作系统的问题,但是死循环肯定是程序的问题了,同时注意发生死锁至少要有两个进程或以上,而饥饿则可能只有一个进程发生
产生死锁必须同时满足四个条件,分别是不剥夺条件,也就是进程的资源不能被其他进程强行剥夺,请求和保持条件,指的是进程持续持有了一个资源的同时还会请求新的资源,循环等待条件,也就是存在一个资源的循环等待链,也就是各个进程同时向另一个进程请求资源但是对方都不给形成的等待链
但是要注意发生死锁时一定会有循环等待条件,但是反之则不一定,除非保证进程请求的同类资源数只为1
发生死锁可能是因为在下面这三个场景下,分别是对系统资源有竞争、进程的请求或推进顺序非法、信号量的使用不当,总之就是对不可剥夺的资源的不合理分配就可能会导致死锁
对死锁的处理策略无非是三种,分别是预防、避免和检测和解除,第一个方法是破坏死锁四个必要条件中的至少一个,第二个是使用某种方法阻止系统进入不安全状态,比如使用银行家算法,最后是允许死锁的发生,但是操作系统会检测出死锁并采取措施解除死锁
最后我们来看看总结
预防死锁
预防死锁可以打破构成死锁需要的必要条件,首先是打破互斥条件,可使用框架将独占设备在逻辑上改造成共享设备这样来打破互斥条件
不过这个方法缺点很多,首先是并不是所有资源都可以改造的,其次是很多地方为了系统安全还必须要保持共享资源的互斥性,可使用的地方比较少
第二种打打破不剥夺条件,有两种方法,第一种是当某个进程请求新的资源得不到满足时就释放此前持有的资源,第二种是当让某个进程可以剥夺其他进程的资源,这种方式一般要结合各个进程的优先级,比如说使用剥夺调度方式,高优先级的进程允许剥夺低优先级的进程的资源
这种方式的问题是实现起来比较复杂而且会降低系统吞吐量甚至可能会导致饥饿,一般多用于易保存和易恢复的状态的资源,比如CPU
破坏请求和保持条件的方法比较简单,可以让进程一次就必须要获得它所有需要的资源,否则就不运行,一旦运行那除非这个进程完成了不然资源全部归运行进程所有,这个方法实现起来简单,但是缺点是资源利用率很低而且可能会导致进程饥饿
破坏循环等待条件的方法可以使用顺序资源分配法,规定每个进程必须按照编号递增的方式请求资源,编号资源只可以申请更大编号的资源,不可以反向申请更小的资源,这样做可以保证总是有进程可以获得所需要的资源并执行
这个方法的缺点是代码写起来太麻烦而且会导致资源浪费
最后我们来看看总结
避免死锁
避免死锁使用的是银行家算法,我们先来说说银行家算法吧,先来看下图的题目
我们可以看到我们构建了三个序列,分别是最大需求已经借走和还会借的序列,同时如果我们答应了某些借钱的需求之后发现我们手里的现金不能满足另外两个公司的可能请求,就说明此时借出的钱会令我们的系统进入不安全状态,那么这个序列就称为不安全序列,反之如果借出去之后剩余的钱仍然有可以满足的借钱公司,就说明此时我们最差都还能满足一个公司,那么此时的借钱请求就不会让我们的系统进入不安全状态,这个序列就称为安全序列,同时对于已经借完了所有钱的公司,我们认为他会立即归还所有钱,所以我们可以将其归还的钱作为本金计入下一次借钱的判断中
注意,系统处于安全状态一定不会发生死锁,但不是说处于不安全状态就一定会发生死锁
这个算法还是Dijkstra提出,没错看,还是他!这个算法用于避免死锁,核心逻辑是当进程提出请求时判断此次分配是否会令系统进入不安全状态,若是则拒绝该请求,反之则接收
但是我们系统中的资源是有多种类型的,并不是上面例子里的只有钱这一种类型,我们解决的方式是用数组来代表各个进程所需要的不同资源的数量
运行逻辑就是执行顺序循环判断,假设接受了请求之后下一个请求是否还能找到能接受的请求,若是则说明接收该请求安全,则接收该请求,反之则说明不安全,拒绝该请求,这样不断运行知道所有进程都运行完毕
判断接受该请求之后是否还有下一个可以执行的请求的这种算法被称为安全性算法
不过实际做题的时候我们可以简单点来,直接把两个及以上能接受的请求都接受,然后进行推演,这样重复直接把题目解出来,主打的就是一个速度
下面是找不到任何一个安全序列的情况,此时说明系统处于不安全状态,可能发生死锁
假设系统中有n个进程和m中资源,那么银行家算法首先会让每个进程在运行前生命对各种资源的最大需求数并组成一个二维数组,分别有最大需求、已分配、最多还需这三列,同时对还可分配和进程所需要这两个内容也分配了一个数组
首先对于进程提出的请求,首先判断该请求是否大于预定最多还需要的请求的资源,若大于则报错,反之则判断该请求是否大于可使用资源,若大于则说明没有足够的资源分配给改进程,该进程必须等待,反之则尝试将资源分配给该进程,修改对于变量后执行安全性算法,若算法通过则说明分配资源给进程后仍处于安全状态,则正式分配,反之则说明分配资源给该进程后会处于不安全状态,则拒绝分配
最后我们来看看总结
检测和解除死锁
当系统处于不安全状态时很可能会发生死锁,此时需要提供死锁检测算法和死锁解除算法,前者用于检测系统中是否发生了死锁,后者用于解除系统中发生的死锁
死锁检测算法简单来说就是有两种结点,一种是进程节点,每个进程节点对于系统中的一个进程,另一种是资源节点,每个资源节点对于一个系统资源,而一类资源有多少个就反映在经常节点图中有多少个点,还有两个边,一种是分配边,由资源节点指向进程节点,每一条边说明该资源节点分配了一个资源给该进程,另一个是请求边,由资源节点指向分配节点,每一条边说明该进程向对于的资源发送了资源请求
根据上面所说的内容我们可以构建出对于的进程和资源的请求图,接着我们就按照请求边的方式查看哪个进程可以被满足,可以被满足的边就消除掉,消除掉之后则说明进程执行完成,接着继续消除其他进程节点的边,如果不能消除所有边,说明此时发生了死锁,最终还连着边的进程就是发生死锁的进程
在实际检测死锁的算法中,首先要找到既不阻塞又不是孤点的进程(不是孤点指的是至少有一条有向边和该进程相连),然后直接消除其所有边,按照这个逻辑一直消除,如果最终没法消除所有边,则说明发生了死锁
解除死锁的主要方法有资源剥夺法,也就是将死锁进程挂起并抢占其资源,但是要防止被挂起的进程出现饥饿,撤销进程法又称终止进程法,就跟这个方法说的一样,直接终止死锁进程,但是这个方法可能会付出比较大的代价,比如一个进程已经执行很久而且马上就要执行完了你给他终止了那特么又要执行一遍,那真的是纯折磨,第三个是进程回退法,就是让一个或多个死锁进程回退到足以避免死锁的地步,但这个要求系统记录进程的历史信息和设置还原点,实现起来也不简单
最后我们来看看总结
内存的基础知识
内存可以存放数据,而程序执行前要先存放到内存中才能被CPU处理,这样做是为了缓和CPU和硬盘之间的速度矛盾
内存地址从0开始,每个地址对应一个存储单元,这个存储单元如果按字节编址则每个存储单元大小为1字节,如果是按字编址则单元大小为一个字,一个字有多少个比特位就要看这个系统是怎么样的了,有的是16,有的可能是其他数
一台电脑有4GB内存指的是有4G内存的同时这4G的数量指定的内容是B,也就是一个字节,所以按照转换公式那么4GB内存就是有4*2^30==2^32个字节,就需要32位二进制位才能表示这台电脑的内存
一般来说,我们写入的代码会在编译之后形成可执行文件,内存有一个个指令,执行该文件会将文件加载至执行文件中的指令,文件中的指令一般来说第一个参数表示这个指令是什么指令,后面的参数就是参数,比如在数据传送指令里,第一个参数表示这个指令是数据传输指令,后面两个参数代表要将后者物理地址上的参数移动到前者的物理地址上,可以看到编译之后的指令内容都是一堆二进制内容,也就是机器能看懂的二进制语言
这里值得一提的是,我们这里的指令内容是随便写的,为了方便理解而已,并不是说实际在内存中就是这么运行的,我们这里同时还默认让进程从地址#0开始连续存放
注意我们实际在内存中使用使用的地址其实是逻辑地址,是相对于要加载的进程的起始地址的地址,这样听着就晕了,但是其实很简单,看例子就行
比如在下面的例子中,我们进程中设置要内存中物理地址为79中的存储单元写入数据,但是由于我们的进程是从物理地址100开始加载的,此时如果往79中设置数据显然是不合理的,我们预期肯定是往179里存放数据的,那么此时79就是绝对地址,而179就是相对地址
一个可执行文件也是一个装入模块,在Windows系统中的装入模块是exe文件,对装入模块进行装入有三种方式
第一种是绝对装入,就是在编译时提前知道程序将要放到内存中的哪个位置并将对应的物理代码进行转换,这种方式的问题是可用性太差,只适合于单道程序环境
第二种是静态重定位方法,又称为可重定位装入,其逻辑是在装入时将起始物理地址加上逻辑地址得到相对地址并加载到内存中,可以理解为是装入时对地址进行的重定位使其不对发生错误,这种方法的特点是当一个进程进入内存时就必须分配器要求的全部内存空间且在运行期间不能再移动,也不能申请新的内存空间
动态重定位又称动态运行时装入,其需要重定位寄存器的支持,后者记录进程的内存中断的进程地址,当对于的程序真正要执行时会通过重定位寄存器记录的数据将进行重定位,然后执行对应的指令
这种方式有很多好处,包括但不限于允许程序在内存中发生移动、允许将程序分配到不连续的存储区中、允许程序运行前只装入其部分代码、允许程序段的共享
程序员写的代码文件首先会被编译为一个个目标模块,然后会被链接为一个装入模块,也就是一个可执行文件,最后装入到内存中执行
这个链接也有三种方式,第一种是静态链接,是指在程序运行之前先将各目标模块及其所需的库函数连接成一个完整的可执行文件,之后不再拆开,而装入时动态加载指的是各个目标模块只有在装入内存时才会被组装成一个整体,采用边装入边链接的链接方式,第三种是运行时动态链接,其逻辑是只有程序执行需要改目标模块时才对将其加入内存并链接,具有便于修改和更新、便于实现对目标模块的共享的优点
最后我们来看看总结
内存管理概念
内存管理指的是操作系统作为系统资源的管理者要对内存进行管理,其首先要做的是负责内存空间的分配与回收,不过这个内容比较复杂,我们这里先不详谈
其次是需要提供技术从逻辑上对内存空间进行扩充,第三个是需要提供程序中逻辑地址与物理地址的转换功能
从逻辑地址到物理地址的转换操作性系统提供了三种方式,其中可重定位装入和动态运行时装入是现代操作系统使用得比较多的方式
最后操作系统需要提供内存保护功能,保证各进程在各自的存储空间里运行,互不干扰
提供内存保护功能的方法有两种,第一种方式是在CPU中设置一对上下限寄存器,存放进程的上下限地址,进程的指令要访问地址前要经过上下限寄存器的检查,如果越界就不予通过
第二种方式是采用重定位寄存器(又称基址寄存器)和界地址寄存器(又称限长寄存器),前者存放进程的骑士物理地址,后者存放进程的最大逻辑地址,当进程想要访问某个地址时,界地址寄存器会判断该逻辑地址是否越界,若没越界则交由给重定位寄存器构建实际的物理地址进行访问
这里注意逻辑地址指的是一开始设置到进程中的要执行的地址,而实际物理地址则是真正要执行的地址,同时这里由于采用了重定位寄存器,则说明了其装入方式必然是动态重定位
最后我们来看看总结
覆盖与交换
早起的计算机内存很小,因此引入了覆盖技术来解决程序大小超过物理内存总和的问题
覆盖技术的思想是将程序分为多个段,令常用的段常驻内存,而不常用的段则在需要时调入内存,因此内存中分为一个固定区和多个覆盖区,前者存放常驻内存段且调入后就不再调出,后者存放不常用的段且需要时再调入内存,不需要时调入内存
比如在下面的例子只有除于根的A才在固定区,第二层第三层的全部在不同的覆盖区中,这样就可以在逻辑上增加内存空间
这种方法的缺点是必须由程序员声明覆盖结构,对用户不透明的同时还增加了编程负担,现在都已经不用了
现在常用的方式是交换技术,也就是当内存空间紧张时,会将内存中的进程挂起,也就是暂时换出外存,等内存空间充裕了再将进程换回来
注意这里使用的技术是中级调度,也就是决定那个处于挂起状态的进程重新调入内存,低级调度是进程调度,高级调度是作业调度,同时进程中的PCB是一直存放到内存中的,因为调用挂起状态的进程回内存时需要找到其位置,而其对于的PCB就记录了其位置,因此PCB只存在于内存中的
暂时换出外存等待的进程可以分为挂起状态,挂起状态又可以进一步分为就绪挂起和阻塞挂起这两种状态,这个知识第二章就提过了,我们这里复习复习
在具有对换功能的操作系统中,通常将磁盘分为文件区和对换区两部分,前者主要追求存储空间的利用率,采用离散分配方式,后者则是用于存放换出的进程谁,追求换入换出速度,采用连续分配方式,反正对换区的I/O速度比文件区的更快
一般来说当内存吃紧时操作系统就会将进程换出了,一般来说是优先换出优先级低的进程,同时为了防止低优先级的进程饥饿,有的操作系统还会考虑进程的等待时间
最后我们来看看总结
连续分配管理方式
接着我们来讲内存空间的分配与回收,先来讲其下的分配方式,第一个是连续分配管理方式,指的是操作系统必须给用户进程分配一个连续的内存空间
在连续分配管理方式中又分为三种方法, 分别是单一连续分配、固定分区分配、动态分区分配
单一连续分配方式中,内存被分为系统区和用户区,前者常用存放操作系统相关数据,后者存放用户进程相关数据,在这种分配方式中只能有一道用户程序,用户程序独占整个用户区空间,其优先是实现简单无外部碎片,缺点是有内部碎片且只能用于单用户单任务的操作系统中,存储器的利用率非常低
第二种是固定分区分配方式,这种分配方式有分区大小相等和不等的两种分配方式,前者是将内存分为分区大小相等的各个期间,而后者是将内存分为分区大小不等的多个区间,前者缺乏灵活性,不过在某些场景下非常实用,后者增加了灵活性
为了记录不同分区的位置及其大小,因此该分配方式会建立一个分区说明表,每个表对应一个分区,通常来说包括分区号、大小、起始地址以及分配状态,这个数据的标识方式可以用数组也可以用链表,当用户程序要装入内存时会由操作系统内核程序根据用户程序大小检索该表找到一个能满足大小且未分配的分区将之分配给改程序,然后修改状态为已分配
这个分配方式是的优点是实现简单且无外部碎片,缺点是可能会出现所有分区都无法装载用户程序的情况且会产生内部碎片,内存利用率也不咋地
第三种方式是动态分区分配,又称可变分区分配,这种分配方式不会预先划分内存分区,而是在进程装入内存时根据进程的大小动态建立分区
一般可以用链表或者是数组两种结构来记录内存的使用情况,注意链表是双向链表啊,这里前者又被称为空闲分区链,后者又被称为空闲分区表
当多个空闲分区都能满足需求时,其会根据动态分区分配算法来选择,我们下一会介绍四种动态分区分配算法,这里先不谈
当分配空间时,如果分配的空间在对应的分区号内,就将对于的分区号的分区大小和起始位置做改动即可
如果说分配的大小正好让一个分区大小为0,那么直接将该列的数据去掉
然后将回收的情况,回收一个进程之后看该进程是否有相连的空闲分区记录,若有则直接往该分区记录上做修改
这个和上面的情况是一样的
如果说回收之后上下都有分区记录,则直接将两个分区记录合并为一个
如果说回收之后前后都没有空闲的分区,则新增一条空闲分区记录
动态分区分配没有内部碎片,但是会有外部碎片,内部碎片指的是分配给某进程的内存区域中有些部分没有用上,而外部碎片指的是内存中的某些空闲分区由于太小而无法被利用,外部碎片可以通过紧凑(又称拼凑)技术来解决,这个技术其实就是将各个进程往上顶置,这样保证内存腾出一段连续的空间,使用这个技术当然也要对内存记录表做相应处理
最后我们来看看总结,注意回收内存分区可能遇到多种情况,不说到底就是相邻的空间分区要合并
动态分区分配算法
动态分区分配算法有四种,分别是首次适应算法、最佳适应算法、最坏适应算法和邻近适应算法
首次适应算法指的是每次从低地址开始查找,找到第一个能满足大小的空闲分区就进行分配,这个算法要求空闲分区总是以地址递增的次序排列
最佳适应算法的是每次分配时都优先使用最小的空闲区,其要求空闲分区总是以容量递增次序链接(注意这里是指的是链表要是这种次序,并不是说在实际的操作系统内存中要是这样的次序),该算法的缺点是会产生非常多的外部碎片
最坏适应书案发又称最大适应算法,其要求空闲分区按照容量递减次序链接,每次分配时优先使用最大的空闲分区,该算法的缺点是会导致大进程到达时就没有内存分区可用
邻近适应算法指的是每次查找时都从上一次查找的结束为止开始找,找到合适的空间大小之后就分配,该算法保留有最佳适应算法的优点,也就是会将高地址部分的大分区保留下来,但是也可能会导致大分区分裂成小分区,也就是还保留了最大适应算法的缺点
这四个算法介绍下来,其实最好的反而是第一个介绍的首次适应算法
最后我们来看看总结
基本分页存储管理概念
接着我们来介绍非连续分配管理方式,这里一共有三种管理方式,分别是基本分页存储管理、基本分段存储管理和段页式存储管理
这里非现需分配指的是为用户进程分配的可以是一些分散的内存空间
将内存分为一个个大小相等的分区的话,每个分区就是一个页框,也称为内存块等,每个页框有一个编号,这就是页框号,页框号从0开始,同时进程的逻辑地址空间也分内与页框大小相等的一个个部分,这里的每个部分称为页或页面,每个页面也有唯一编号,这个编号称为页号,页号也是从0开始,操作系统以页框为单位为各个进程分配内存空间,进程的每个页面都会放入到内存的页框中,也就是说进程的页面和内存的页框是一一对应的,注意各个页面不必连续存放,可以放到不相邻的页框中
操作系统会为每个进程建立一张页表用于记录页面和页框的关系,其通常存放于PCB中,由页号和块号组成,页号对于页面,块号对于页框
假如我们知道了系统的物理内存和页面大小,我们要怎么求每个页表项至少要有多少字节?首先内存块大小==页面大小,这个我们上面提过了,那么首先能有内存块大小为4KB==2^12B,注意4GB的内存总共有2^32B,那么最终就有2^32/2^12=2^20个内存块,那么内存块号的范围就是0~2^20-1,那么至少要用20个bit,也就是比特来表示,但是用于题目要求的是字节,所以说每个页表项要有3个字节来表示块号
注意页表记录的只是内存块号而不是内存块的起始地址,如果我们要计算第J号内存块的起始地址,那么其公式为J*内存块大小
进程在内存连续存放时通过寄存器就可以实现逻辑地址到物理地址的转换
但在不连续分配时就不能用上面的方法了,不连续分配时有一个特点就是虽然进程的各个页面在内存里是离散存放的,但是其页面中的指令却是连续存放的,那么我们这时如果有逻辑地址,我们首先确定逻辑地址对于的页号,假定其为P,然后我们找到P号页面在内存中的起始地址,然后找到逻辑地址的页内偏移量,也就是我们所要找的指令离第一行的指令的距离,最后我们的逻辑地址对于的物理地址就为其对于的页面在内存中的起始地址+页内偏移量
那么欧美要如何确定页号和页内偏移量呢?这个其实很简单,页号就是逻辑地址/页面长度并取整,而页内偏移量就是逻辑地址和页面长度取余
在计算机内部地址使用二进制表示的,此时如果页面大小正好是2的整数幂则计算机的运算速率会得到提升,这还因为只要页面大小是2的整数倍,那么在二进制中每个页面的大小就位2^k,单位为B,其中二进制末尾K位为页内偏移量,其余部分则表示页号
同时在这种情况下要查出某个内存块的起始地址可以直接使用公式页面在内存中存放的起始地址+页内偏移量==对于内存块的起始地址
最后我们来看看这个二进制这一小部分的总结
在分页存储管理下的逻辑地址结构下,有页号和页内偏移量两部分,也就是我们上面讲过的二进制表示逻辑地址,其中如果有K位页内偏移量,说明该系统中的一个页面大小为2^K个内存单元,如果有M位页号则说明在该系统中一个进程最多允许2^M个页面
当然,有些题目的页面大小就是不是2的整数次幂,那就没办法了,此时就只能用原始的计算方法了
最后我们来看看总结
基本地址变换机构
接着我们来细讲上面的从逻辑地址到物理地址的转换是怎么转换的
假设我们要求逻辑地址A的物理地址,那么根据逻辑地址我们可以推导出其对于的页号和页内偏移量,通常来说系统中有页表寄存器PTR,其用于存放页表在内存中的起始地址F和页表长度M,一开始这两个变量是放在PCB中的,但是当进程被调度时,操作系统内核就会将这两个变量放到页表寄存器中,页表长度M和页号是有对于关系的,因此页号首先和页表长度M做比对判断是否越界,若是则报错,反之则查询页表,找到页号对应的页表项,然后用内存块号和页内偏移量得到物理地址
注意页面大小是2的整数次幂,同时页号P应严格小于M才不算越界,假设最终的物理地址为E,那么E=b*L+W,其中b为页表项内容,L为页面大小,W为页内偏移量,其中b同时也为内存块号
下面这题中要注意分析题目的隐藏条件,题目就包括了系统按字节寻址,页内偏移量占10位,因为2^B==1KB,首先我们计算页号P,由于页面大小为1K字节==1024B,所以页号P=2500/1024==2,页内偏移量W则是相同的公式取余,结果为452,由于页号2对于的内存块号为8,那么根据公式E=b*L+W可以计算出结果为8644
注意每个页表项的长度是相同的,而页号是隐含的,只要我们根据公式自己求的,各个页表项是按顺序连续地存放在内存中的,如果页表在内存中存放的起始位置为X,则M号页对应的页表项存放在内存的地址为X+3M,不过我们这里的条件是一个页面为3KB的情况下,实际情况中为了方便查询,会令一个页面占据4字节,此时内存地址的公式则为X+4M,这个记住就行了,反正理论上用3字节表示一个页面,但实际里常用4字节来表示
最后我们来看看总结
具有快表的地址变换机构
快表是基本地址变换结构的改进版本,引入快表可以有效提高地址转换的效率
快表又称联想寄存器,简称TLB,是一种访问速度比内存快很多的高速缓存,用于存放访问页表项的副本,与其对应,内存中的页表常称为慢表
一般来说,外存是最慢的,内存其次,高速缓存和寄存器分别位居第二和第一,一般这两者都集成在CPU内部,但是我们这里的快表不是集成在CPU内部的啊,这点要记住,同时越快的部分总是越贵
其执行逻辑其实就跟缓存差不多,反正根据页号查内存块号时先从快表查,查不到就从慢表查,从慢表查到了时候要同步信息到快表上
这里我们要注意,有的系统支持快表和慢表同时查找,这样的话我们计算时间时要将查询快表所用的时间给去掉
之所以这样做是因为在支持快表和慢表同时查询的系统中快表和慢表同时查询,如果快表不命中那么慢表也会经过同样的时间,不过其实这里可以看到如果查快表的话,那么慢表也查询了同样的时间,按说是要加上去来算的,不过我们这里就忽略这个误差了
之所以快表可以如此有效地提高系统的查询效率是因为局部性原理,局部原理分为时间局部性和空间局部性,前者指的是如果执行了程序中的某条指令,那么不久后该指令很可能再次执行,后者指的是一旦程序访问了某个存储单元那么在不久之后其附近的存储单元也很可能被访问,由于局部性原理,因此当地址变换时,很可能连续查到的都是同一个页表项
最后我们来看看总结,注意这里TLB和普通Cache的区别是前者只有页表项的副本,而后者可能会有其他各种数据的副本
两级页表
单纯使用一级页表来联立进程中的页和内存中的内存块是有问题的,第一个问题就是页表必须连续存放,当页表很大时,需要占用多个连续的页框
下面页表项指的是页表里的每一横列,而页表项长度指的是一横列的数据大小
第二个问题是,我们没有必要让整个页表常驻内存,因为进程在一段时间内可能只需要访问某几个特定页面就可以了
为了解决这两个问题,我们提出了两级页表的概念,简单来说就是将页表再次进行分组,使得内个内存块刚好可以放入一个分组,这些分组在内存块中是可以离散分布的,因此还要给离散分配的页表再建立一个页表,这个页表称为页目录表或是外层页表又或是顶层页表
这里有个问题,就是我不懂为什么其能确定每个页面可以存放1K个页表项,但反正他说是就是吧,以后上课了再问老师
那么我们可以将之前的页表进行细分,再细分成1023个页表,每个页表的大小是1023,块号是不断往前的,但是每个分组的页号是从0开始到1023的
那么实际其结构如下图所示,我们可以看到首先有一个一级页号,这个其实就是页目录表包括页目录号和内存块号,用户根据页目录表找到自己对于的二级页表,然后根据二级页表找到对于的内存块号,接着再根据内存块号来访问内存中的内存块
在二进制表示中,也会拆为三部分,第一部分是一级页号,由31位到22位,一共10位,最大值是1024,也就是说顶层页表只有1024个长度,而二级页号也是如此,这也说明二级页表最大也只有1024个长度,而页内偏移量同样如此
这里值得一提的是,页目录表和页表都是存在于内存中的
解决第二个问题也很简单,就是在需要访问对于的页面是再把页面调入内存,这里使用到的是虚拟存储技术,我们后面会将,在页面项中再加一个标志位用于表示该页面是否已经调入内存就完了
注意如果采用多级页表机制,那么各级页表的大小不能超过一个页面,比如说下题中我们首先求出页面大小为2^12位,那么页号是40-12=28位,已知页表项为4B,因此每个页面可以存放2^12/4=2^10个页表项,但是这里一共有四十位逻辑地址,因此我们最起码要给其分三级,这样才能满足要求
最后多级页表的访存次数在没有快表机制的问题下N级页表访问一个逻辑地址需要N+1次访存
基本分段存储管理
分段存储管理和分页最大的区别就是分段离散分配时所分配的地址空间的基本单位和分页的不同
按照程序自身的逻辑关系将进程划分为若干个段IE每个段都有段名,每个段从0开始编址就是分段存储,分段存储管理中的内存分配规则是以段为单位进行分配,每个段在内存中占据连续空间,但各段之间不可以相邻
段是按逻辑功能划分的,用户编程更加方便且程序的可读性更高
分段系统的逻辑地址由段号和段内地址组成,前者的位数决定了每个进程最多可以分成几个段,后者的地址位数决定了每个段的最大长度是多少,段名可以由程序员自己写,不过在程序里段名会被翻译成段号同时对应的单元还会作为不同的单元命名,比如单元这样的
当然,进程里的段和内存里的段需要记录映射关系,因此段表就应运而生,段表中有多个段表项,每个段表项中包括段号、段长和基址(起始地址),注意各个段表项的长度是相同的,都是占用48位,也就是6B,所以如果段表存放的起始地址为M,那么第K号段对于的段表项存放的地址就是M+K*6
而在内存中这些段存放的位置则是分开的且是离散的
CPU执行执行指令时需要将逻辑地址转换为物理地址,因此机器指令中的逻辑地址使用二进制表示,这里也跟分页表示差不多,从一个位作为分界线左边是表示段号的,而右边是表示段内地址的
其执行过程其实也跟页表的差不多,先判断段号是否越界,但是要注意段表的长度是从1开始的,没越界就继续处理,查询段表之后找到段表项的存放地址为段表开始地址F+段号S*段表项长度,不过这里找到段表项之后还会检查段内地址是否超过了段长,这个是分页存储中不会做的,没超过就计算得到物理地址并访问目标内存单元
这里同样有段表寄存器,存放段表起始地址F和段表长度M,而段表起始长度和段表长度的数据存在于PCB中,只有需要使用的时候才会被调出
页和段最大的不同在于,前者的地址空间是唯一的且是对用户不可见的信息的物理单位,而后者的地址空间是二维的,对用户是可见的信息的逻辑单位
分段比分页共容易实现信息的共享和保护,需要让进程共享一个段就只需要让两个进程的段表项指向同一个段即可,一般来说不能被修改的代码称为纯代码或可重入代码,纯代码是可以共享的
分段也比分页共容易实现信息的共享和保护,比如下图中可以看到分段如果要共享或保护,可以直接对一个段保护或者对一个段进行允许或不允许其他进程访问的标记,但是在分页中由于每一块页表项都是相同大小的,这样就会出现一个页表项有允许共享和不允许共享的部分,这就难搞了
最后值得一提的单级页表的访问一个逻辑地址需要两次访问,分段也是,同时分段也存在和分页系统类似的快表结构
最后我们来看看总结
段页式管理
由于分页管理和分段管理都有其缺点,因此产生了段页式管理来中和两者的缺点并取其优点,经典折中了反正
段页式管理的运行原理是先将进程按逻辑模块分段,再将各段分页,然后将内存空间分为大小相等的内存块之后将分页的各页面装入到各内存块中
段页式管理的逻辑地址由段号、页号、页内偏移量组成,段号的位数决定了每个进程最多可以分为几个段,页号位数决定了每个段最大有多少页,而页内偏移量决定了页面和内存块大小是多少,这里要注意分段是用户是可见的,但是分页对用户是不可见的,由于经过了分段和分页,因此段页式管理的地址结构是二维的
比如在下面的图里我们可以看到进程中的页首先被分段,接着形成段表,在段页式管理中的段表中的一个段表项包括段号、页表长度和页表存放块号,众所周知,分段的内容还要继续分页的,所以这里直接存放的是页表长度和页表存放块号,后者对于的页表中的页表项,其页表中存有页号和内存块号,找到对应的内存块号之后根据公式计算出物理地址之后访问该地址即可
我们也可以从其过程中看到,首先其根据段号判断是否越界,没越界就访问段表,然后检查页号是否越界,没越界就访问页表,之后访问对于的内存块号然后根据页内偏移量正式访问目标内存单元
这里在没有快表的前提下,访问到真实数据一共需要三次访存,同时这个管理方式也是支持快表的
最后我们来看看总结
虚拟内存基本概念
虚拟存储技术也是用于扩充内存空间的技术之一,和覆盖技术、交换技术属于同一种类型
传统的存储管理方式具有作业必须一次性全部转入内存后才能开始运行导致并发度下降和大作业无法运行以及作业一旦被装入内存就会一直驻留的缺点,为了解决这些缺点,因此出现了虚拟存储技术
虚拟存储技术的原理是局部性原理,这个我们上面提过了,直接看图吧
虚拟存储技术实现的需求简单来说就是先将程序中会使用的部分装入内存,用不到的留在外存,当要访问的信息不在内存时,由操作系统负责将所需信息从外存调入内存,同时如果内存空间不够,则由操作系统负责将内存中暂时不用的信息换出到外存
虚拟内存具有多次性、对换性和虚拟性,注意虚拟指的是使得用户感受到的内存容量远大于实际的容量,但实际物理内存没变大,只是在逻辑上扩充了而已
虚拟内存技术是建立在离散分配的基础上的,因此衍生出了请求分页/分段/段页式存储管理三种方式,这三种方式无非是多了请求调页和页面置换功能
最后我们来看看总结
请求分页管理
请求分页存储管理和基本分页存储的主要区别是前者当所访问的信息不在内存时,会由操作系统将所需的信息从外存调入内存且当内存空间不够时,会将内存中暂时用不到的信息换出到外存,其为了实现上面的功能还提供了缺页中断机构
请求分页存储管理中的页表有时也称请求页表,其页表项除了有页号和内存块号外,还有状态位(是否已经调入内存)、访问字段(记录被访问过几次或上次访问的时间)、修改位(页面调入内存后是否被修改过)、外存地址(页面在外存中的存放地址)
假设要请求的页面不在内存时,此时会触发缺页中断,操作系统的中断处理程序会处理中断,此时缺页的进程会阻塞并放入阻塞队列,直到调页完成后才会被唤醒,如果此时内存中有空闲的内存块,那么就会给将外存中的页面调入装入到该块,如果你没有,那么就会使用置换算法选择一个页面将其淘汰然后将外存中的页面写到该内存块中,此时如果该页面在内存中被修改过,那么就要将其写回外存
当然,注意无论是从空内存块中装入还是淘汰一个页面后装入,请求页表都是要做对应修改的
缺页中断属于内中断,属于可修复的故障,注意一条指令执行期间可能会产生多次缺页中断
请求分页存储管理相当于基本分页存储管理器新增了请求调页、页面置换以及修改请求页表中新增的表项
执行过程没什么值得提的,只是有一点要注意,那就是如果某个页面被换出外存,那么对于快表中的相应表项也要删除
书里也是给出了请求分页管理的执行流程图,我们这里要注意的以下几点
- 只有写指令才需要修改“修改位”且一般来说只需要修改快表中的数据
- 缺页中断处理也仍然需要保留CPU现场
- 换出/换入操作页面都需要I/O操作,如果其太频繁,会造成较大的开销
- 页面调入内存后需要修改慢表,同时也需要将表项复制到快表中
最后我们来看看总结
页面置换算法
用于请求分页管理的页面置换算法一共有五种,好的页面置换算法应该追求更少的缺页率
最佳置换算法,简称OPT,简单来说就是每次置换时选择以后或者是最长时间内不再被访问的页面进行替换,其能保证最低的缺页率,比如下图中,第一次要进行替换式发生在2中,其经过对比发现7这个内存块是最后被访问的,因此选择替换7这个内存块
缺页率的计算方式就是全部页面的访问次数比上发生缺页的次数
OPT是算法是最好的算法,但是其要求提前知道页面的访问序列,实际情况下肯定不可能知道的,所以这个算法是无法实现的,仅能当做理想情况来进行对比
先入先出指定算法简称FIFO算法,其逻辑是每次选择最早进入内存的页面作为淘汰的页面,其会将调入内存的页面根据调入的先后顺序排成一个队列,需要换出页面时就选出队头页面,每次添加页面时页面增加到队尾
FIFO算法的优点是实现简单,但是其算法性能差,而且存在贝拉迪(Belady)异常,这个异常就是当进程分配的物理块数增加,缺页率反而会增加的现象
最近最久未使用置换算法简称LRU,其逻辑是每次淘汰的页面是最近未使用的页面,其实现方法是给每个页面的页表项用访问字段记录页面自上次被访问以来经历的时间t,当期需要淘汰一个页面时就选择页面中t值最大的进行淘汰
当然我们解题时就逆向检查此时内存中的页面号最早出现的那个页号就是要淘汰的页面
这个算法的性能是最接近OPT算法的,但是实现该算法需要专门的硬件支持,实现困难且开销大
上面介绍的算法都有其缺点,而我们经常使用的比较折中的算法是时钟置换算法,简称CLOCK算法,或最近未用算法NRU,其有两种实现,我们先来说简单的实现方式
其简单的实现方式是将内存的页面都通过链接指针连接成一个循环队列同时往页表中的页表项中增加一个访问位,当为1时表示最近访问过,为0时表示最近没访问过,当某一页被访问时起访问位就置为1,当需要淘汰一个页面时检查循环队列中的页面,当找到页面中页表项的访问位为0的就淘汰,同时将所有访问位为1的页表项置为0,当第一轮扫描找不到就进行第二轮扫描,第二轮肯定就能找到了,所以简单的CLOCK算法选择一个淘汰页面最多会经过两轮扫描
事实上如果被淘汰的页面只有被修改过时才需要写回外存,那么我们可以优先淘汰没有修改过的页面,这样就能尽可能避免IO操作,提高效率,这个就是改进型CLOCK算法的最基本思路
要实现该算法需要往页表项中添加一个修改位,当其值为1表示被修改过,反之则是没修改过,同样也是将所有可能被置换的页面排成一个循环队列,第一轮淘汰最近没被访问且没修改的页面,也就是访问位为0,修改位为0的页面,后面用(0,0)表示,第二轮淘汰最近没访问过,但修改过的页面(0,1)同时会将所有扫描过的页面的访问位设置为0,第三轮淘汰最近访问过但是没修改的(0,0)页面,第四轮淘汰最近访问过且修改过的(0,1)页面
改进型CLOCK置换算法选择一个淘汰页面最多会经过四轮扫描
最后我们来看看总结
分配策略、抖动、工作集
本节介绍下图所示的内容
驻留集指的是请求分页存储管理中给进程分配的物理块集合,在采取虚拟存储技术的系统中驻留集大小一般小于进程的总大小,如果其太小,会导致缺页频繁,反之则会导致多道程序的并发度下降,所以压迫给驻留集选择一个合适的大小
驻留集有固定分配和可变分配的两种分配方式,前者操作系统会给每个进程分配一组固定数目的物理块,在运行期间不再改变,后者则是先给进程分配一定数目的物理块,可根据情况做适当增减,其大小可见
还存在局部置换和全局置换两种方式,前者发生缺页时只能选进程自己的物理块进行置换,而后者可以将操作系统中保留的空闲物理块或是别的进程持有的物理块分配给缺页进场
通过固定/可变分配和局部/全局置换可以构成三种驻留集配置方式,之所以是三种,是因为全局置换不能配和固定分配,以为前者要求一个进程拥有的物理块数必然会改变,而后者要求必不能改变,自相矛盾了
驻留集的固定分配局部置换方式指的是系统为每个进程分配一定数量的物理块且在整个运行期间都不能改变,若进程在运行时间发生缺页,则只能从该进程的内存中的页面选出一页来换,这个方式非常不灵活,即使程序员可以自己配置大小,但是配置的大小往往也是不合理的
可变分配全局置换和上一条配置的不同在于只要某进程发生缺页都将获得新的物理块,这个物理块可能是自己的,也可能是其他进程的物理块,而被选中用于增添到其他进程中的进程物理块会减少,因此缺页率会增加
而可变分配局部置换指的是刚开始给每个进程分配一定数量的地址块,当某个进程发生缺页时允许从自己或其他进程的物理块中选出一个换入,如果一个进程在运行频繁缺页,那么系统会给该进程适当多分配物理块,如果缺页率特别低则会适当减少其物理块
反正记住可变分配全局置换的特点是只要缺页就给分配新的物理块,而可变分配局部置换要根据发生缺页的频率来动态地增加或减少进程的物理块
调入页面的策略有两种第一种是预调页策略,指的是调入一个页面时会同时调入若干个与其相邻的页面,这个方法成功率只有一半左右,主要用于进程的首次调用,第二种是请求调页策略,指的是进程在运行期间发现缺页时才将所缺页页面调入内存,这种策略由于每次只调入一页,因此IO开销比较大
众所周知,磁盘分为对换区和文件区,其中前者采用连续分配方式后者采用离散分配方式同时前者的访存速度远大于后者,因此如果系统在拥有足够的对换区空间的情况下会将文件区要执行的数据全部复制到对换区中,数据的修改也在对换区中,这样保证速度,反之则会将不修改的数据直接调入到内存中,而需要修改的数据则在内存和对换区之中转移,最后一种方式是UNIX方式,运行之前进程有关的数据全部放到文件区中,要使用从文件区调入,如果要对已经加载的页面进行换入换出则是在内存和对换区中转移进行的
刚刚换出的页面马上又要换入内存或刚换入的页面马上又要换出外存的频繁页面调度行为称为抖动,产生抖动的主要原因是分配给进程的物理块不够,为了研究应该给每个进程分配多少个物理块,因此提出了工作集的概念
工作集指的是在某段时间间隔里进程实际访问页面的集合,工作集的大小可能小于窗口尺寸,实际应用中操作系统统计工作集大小,根据其大小给进程分配内存块,驻留集的大小不可小于工作集大小,否则将会造成进程运行过程中的频繁缺页
最后我们来看看总结
内存映射文件
内存映射文件时操作系统向上层程序员提供的系统调用功能,其可以方便程序员访问文件数据且方便多个进程共享同一个文件
文件存储在硬盘中时分块存储的,而传统的文件访问方式需要程序员调用多个命令才能实现打开查看修改等功能,太麻烦了
而有了内存映射文件之后,程序员就无需关心太多这些繁杂命令了,大部分内容可以由操作系统自己完成,同时进程的虚拟空间对应着实际的文件空间,不过一开始保存的是指针而不是实际的内容,如果说后续用户要访问实际的内容,那么其会将文件空间里的内容转移到虚拟地址空间里,方便用户查看修改
多个进程可以映射同一个文件以实现共享,当一个进程修改文件时,另一个进程也可以感知到
最后来看看
文件管理概述
文件是一组有意义的信息/数据集合,操作系统需要给向上的应用程序/用户或裸机提供向上和向下的文件相关接口
文件具有文件名、标识符(操作系统内部用于区分各个文件的一种内部名称)、类型、位置、大小等信息,这里重点要提的是保护信息,保护信息指的是对文件进行保护的控制信息,比如说文件是否允许非管理员用户读写等
文件内部的数据分为无结构文件和有结构文件(数据库表内的数据),前者是由一些二进制或字符流组成,又称流式文件,后者由一组相似的记录组成,又称记录式文件,其中的每一条内容是一个记录,而记录中的每个内容是多个数据项组成的,数据项是文件系统中最基本的数据单位
在有结构文件中,各个记录如何存放是文件管理要解决的问题
文件管理中的目录是属性结构,目录中可以存放目录和文件,目录也是一种特殊的有结构文件
操作系统要向上提供的接口最简单的如创建文件的create函数,读文件的read函数、删除文件的delete函数和写文件的write函数等
向上提供的各个基本操作可以组合起来完成更复杂的操作,当然操作系统在此还需要做额外处理
外存与内存一样是由一个个存储单元组成的,每个存储单元可以存储一定量的数据并且对于一个物理地址,外存会分一个个块/磁盘块/物理块,每个磁盘块的大小是相等的且一般是包含2的整数次幂个地址,文件的逻辑地址同样可以分为逻辑块号和快内地址,操作系统同样需要将逻辑地址转为外存的物理地址(物理块号、块内地址),块内地址的位数取决于磁盘块的大小
注意操作系统是以磁盘块为单位分配空间的,因此即使一个文件大小远不足一个一个块的大小仍然会占用同样大小的磁盘块
操作系统可以将文件数据放在连续的几个磁盘块中,也可以放在离散的几个磁盘块中并记录器先后顺序,当然这是后面要探讨的内容
操作系统还需要提供文件共享和文件保护的内容
最后我们来看看总结