7、进程的调度算法有哪些?

调度算法是指:调度程序是内核的重要组成部分,决定这下一个要运行的进程。那么根据系统的资源分配策略所规定的资源分配算法。常用的调度算法有:先来先服务调度算法、时间片轮转调度法、短作业优先调度算法、最短剩余时间优先、高响应比优先调度算法、优先级调度算法等等。

  • 先来先服务调度算法

先来先服务让我们想起了队列的先进先出特性,每一次的调度都从队列中选择最先进入队列的投入运行。

【操作系统系列】2 高频操作系统面试题解答2_调度算法

  • 时间片轮转调度算法

先来理解轮转,假设当前进程A、B、C、D,按照进程到达的时间排序,而且每个进行都有着同样大小的时间片。如果这个进程在当前的时间片运行结束,啥事儿没有,直接将进程从队列移除完事儿。如果进程在这个时间片跑完都没有结束,进程变为等待状态,放在进程尾部直到所有进程执行完毕。

为什么进程要切换,切换无外乎是时间片够用或者不够用。如果时间片够用,那么进程可以运行到结束,结束后删除启动新的时间片。如果时间片不够用,对不起,暂时只能完成一部分任务(变为等待状态),过后再等待 CPU 的调度。网上开源的代码太多,怎么实现,大家可以参照加深影响。

  • 短作业优先调度算法

短作业优先调度算法,从名称可以清晰的知道「短作业」意味着执行时间比较短,「优先」代表执行顺序。结合就是"短者吃香"。那么多短吃香?进程不可能都短,也有需要执行时间比较长的进程怎么办?一直等待,直到饿死麦?而且有些进程比较紧急,能够得到先执行?这些都是此算法所出现的问题,然后出现下面的一些算法

  • 最短剩余时间优先调度算法

最短剩余时间是针对最短进程优先增加了抢占机制的版本。在这种情况下,进程调度总是选择预期剩余时间最短的进程。当一个进程加入到就绪队列时,他可能比当前运行的进程具有更短的剩余时间,因此只要新进程就绪,调度程序就能可能抢占当前正在运行的进程。像最短进程优先一样,调度程序正在执行选择函数是必须有关于处理时间的估计,并且存在长进程饥饿的危险。

  • 高响应比优先调度算法

什么是高响应比,有响应之前应该会有请求,相当于是请求+响应+优先,算是一种综合的调度算法。也就是它结合了短作业优先,先来先服务以及长作业的一些特性。ok,那么这三种是如何体现出来的

首先来说短作业优先。等待时间我们假设相等,服务时间很短,这样的话短作业就会有更高的优先权。

再来看先来先服务。假设服务时间相同,先来的自然等待时间较长,优先级越高。
上面说长作业很可能因为等待时间过长,容易饿死。在这里不会,仿佛像医生的这个职业,工作越久资历越老,优先级越来越高,越来越吃香

  • 优先级调度算法

优先级调度算法每次从后备作业队列中选择优先级最髙的一个或几个作业,将它们调入内存,分配必要的资源,创建进程并放入就绪队列。在进程调度中,优先级调度算法每次从就绪队列中选择优先级最高的进程,将处理机分配给它,使之投入运行。

8、什么是死锁?

死锁,顾名思义就是导致线程卡死的锁冲突,例如下面的这种情况

【操作系统系列】2 高频操作系统面试题解答2_死锁_02

线程 1 已经成功拿到了互斥量 1 ,正在申请互斥量 2 ,而同时在另一个 CPU 上,线程 2 已经拿到了互

斥量 2 ,正在申请互斥量 1 。彼此占有对方正在申请的互斥量,结局就是谁也没办法拿到想要的互斥
量,于是死锁就发生了。

【操作系统系列】2 高频操作系统面试题解答2_死锁_03

稍微复杂一点的情况

【操作系统系列】2 高频操作系统面试题解答2_死锁_04

存在多个互斥量的情况下,避免死锁最简单的方法就是总是按照一定的先后顺序申请这些互斥
量。还是以刚才的例子为例,如果每个线程都按照先申请互斥量 1 ,再申请互斥量 2 的顺序执行,死锁
就不会发生。有些互斥量有明显的层级关系,但是也有一些互斥量原本就没有特定的层级关系,不过
没有关系,可以人为干预,让所有的线程必须遵循同样的顺序来申请互斥量

9、产生死锁的原因?

由于系统中存在一些不可剥夺资源,而当两个或两个以上进程占有自身资源,并请求对方资源时,会导致每个进程都无法向前推进,这就是死锁

  • 竞争资源

例如:系统中只有一台打印机,可供进程 A 使用,假定 A 已占用了打印机,若 B 继续要求打印机打印将被阻塞。
系统中的资源可以分为两类:

  1. 可剥夺资源:是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺, CPU 和主存均属于可剥夺性资源;
  2. 不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
  • 进程推进顺序不当

例如:进程 A 和 进程 B 互相等待对方的数据。

10、死锁产生的必要条件?

互斥

要求各个资源互斥,如果这些资源都是可以共享的,那么多个进程直接共享即可,不会存在等待的尴尬场景

非抢占

要求进程所占有的资源使用完后主动释放即可,其他的进程休想抢占这些资源。原因很简单,如果可以抢占,直接拿就好了,不会进入尴尬的等待场景

要求进程是在占有(holding)至少一个资源的前提下,请求(waiting)新的资源的。由于新的资源被其它进程占有,此时,发出请求的进程就会带着自己占有的资源进入阻塞状态。假设 P1,P2 分别都需要 R1,R2 资源,如果是下面这种方式:

P1:          P2:
request(R1) request(R2)
request(R2) request(R1)
>

如果 P1 请求到了 R1 资源之后,P2 请求到了 R2 资源,那么此后不管是哪个进程再次请求资源,都是在占有资源的前提下请求的,此时就会带着这个资源陷入阻塞状态。P1 和 P2 需要互相等待,发生了死锁。

换一种情况:

P1:          P2:
request(R1) request(R1)
request(R2) request(R2)
>

如果 P1 请求到了 R1 资源,那么 P2 在请求 R1 的时候虽然也会阻塞,但是是在不占有资源的情况下阻塞的,不像之前那样占有 R2。所以,此时 P1 可以正常完成任务并释放 R1,P2 拿到 R1 之后再去执行任务。这种情况就不会发生死锁。

  • 循环等待

要求存在一条进程资源的循环等待链,链中的每一个进程占有的资源同时被另一个进程所请求。

发生死锁时一定有循环等待(因为是死锁的必要条件),但是发生循环等待的时候不一定会发生死锁。这是因为,如果循环等待链中的 P1 和 链外的 P6 都占有某个进程 P2 请求的资源,那么 P2 完全可以选择不等待 P1 释放该资源,而是等待 P6 释放资源。这样就不会发生死锁了。

11、解决死锁的基本方法?

如果我们已经知道死锁形成的必要条件,逐一攻破即可。

  • 破坏互斥

通过与锁完全不同的同步方式CAS,CAS提供原子性支持,实现各种无锁的数据结构,不仅可以避免互斥锁带来的开销也可避免死锁问题。

  • 破坏不抢占

如果一个线程已经获取到了一些锁,那么在这个线程释放锁之前这些锁是不会被强制抢占的。但是为了防止死锁的发生,我们可以选择让线程在获取后续的锁失败时主动放弃自己已经持有的锁并在之后重试整个任务,这样其他等待这些锁的线程就可以继续执行了。这样就完美了吗?当然不

这种方式虽然可以在一定程度上避免死锁,但是如果多个相互存在竞争的线程不断的放弃重启放弃循环,就会出现活锁的问题,此时线程虽然没有因为锁冲突被卡死,但是仍然会因为阻塞时间太长处于重试当中。怎么办?

方案1:给任务重试部分增加随机延迟时间,降低任务冲突的概率

  • 破坏环路等待

在实践的过程中,采用破坏环路等待的方式非常常见,这种技术叫做"锁排序"。很好理解,我们假设现在有个数组A,采用单向访问的方式(从前往后),依次访问并加锁,这样一来,线程只会向前单向等待锁释放,自然也就无法形成一个环路了。

说到这里,我想说死锁不仅仅出现在多线程编程领域,在数据库的访问也是非常的常见,比如我们需要更新数据库的几行数据,就得先获取这些数据的锁,然后通过排序的方式阻止数据层发生死锁。
这样就完美了?当然没有,那会出现什么问题?
这种方案也存在它的缺点,比如在大型系统当中,不同模块直接解耦和隔离得非常彻底,不同模块开发人员不清楚其细节,在这样的情况下就很难做到整个系统层面的全局锁排序了。在这种情况下,我们可以对方案进行扩充,例如Linux在内存映射代码就使用了一种锁分组排序的方式来解决这个问题。锁分组排序首先按模块将锁分为了不同的组,每个组之间定义了严格的加锁顺序,然后再在组内对具体的锁按规则进行排序,这样就保证了全局的加锁顺序一致。在Linux的对应的源码顶部,我们可以看到有非常详尽的注释定义了明确的锁排序规则。

这种解决方案如果规模过大的话即使可以实现也会非常的脆弱,只要有一个加锁操作没有遵守锁排序规则就有可能会引发死锁。不过在像微服务之类解耦比较充分的场景下,只要架构拆分合理,任务模块尽可能小且不会将加锁范围扩大到模块之外,那么锁排序将是一种非常实用和便捷的死锁阻止技术

12、怎么预防死锁?

破坏请求条件:一次性分配所有资源,这样就不会再有请求了;
破坏请保持条件:只要有一个资源得不到分配,也不给这个进程分配其他的资源:
破坏不可剥夺条件:当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源;
破坏环路等待条件:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反。

13、怎么避免死锁?

  • 银行家算法

当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存的资源可以满足它的最大需求量则按当前的申请量分配资源,否则就推迟分配。
当进程在执行中继续申请资源时,先测试该进程已占用的资源数与本次申请资源数之和是否超过了该进程对资源的最大需求量。若超过则拒绝分配资源。若没超过则再测试系统现存的资源能否满足该进程尚需的最大资源量,若满足则按当前的申请量分配资源,否则也要推迟分配。

  • 安全序列

是指系统能按某种进程推进顺序(P1, P2, P3, ..., Pn),为每个进程 Pi 分配其所需要的资源,直至满足每个进程对资源的最大需求,使每个进程都可以顺序地完成。这种推进顺序就叫安全序列【银行家算法的核心就是找到一个安全序列】。

  • 系统安全状态

如果系统能找到一个安全序列,就称系统处于安全状态,否则,就称系统处于不安全状态。

14、怎么解除死锁?

  • 资源剥夺:挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他死锁进程(但应该防止被挂起的进程长时间得不到资源);
  • 撤销进程:强制撤销部分、甚至全部死锁进程并剥夺这些进程的资源(撤销的原则可以按进程优先级和撤销进程代价的高低进行);
  • 进程回退:让一个或多个进程回退到足以避免死锁的地步。进程回退时自愿释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。

15、什么是缓冲区溢出?有什么危害?

官话

缓冲区溢出是指当计算机向缓冲区内填充数据时超过了缓冲区本身的容量,溢出的数据覆盖在合法数据上

举个例子

一个两升的杯子,你如果想导入三升,怎么做?其他一生只好流出去,不是打湿了电脑就是那啥

16、物理地址、逻辑地址、线性地址

  • 物理地址:它是地址转换的最终地址,是内存单元真正的地址。如果采用了分页机制,那么线性地址会通过页目录和页表得方式转换为物理地址。如果没有启用则线性地址即为物理地址
  • 逻辑地址:在编写c语言的时候,通过&操作符可以读取指针变量本省得值,这个值就是逻辑地址。实际上是当前进程得数据段得地址,和真实的物理地址没有关系。只有当在Intel实模式下,逻辑地址==物理地址。我们平时的应用程序都是通过和逻辑地址打交道,至于分页,分段机制对他们而言是透明得。逻辑地址也称作虚拟地址
  • 线性地址:线性地址是逻辑地址到物理地址的中间层。我们编写的代码会存在一个逻辑地址或者是段中得偏移地址,通过相应的计算(加上基地址)生成线性地址。此时如果采用了分页机制,那么吸纳行地址再经过变换即产生物理地址。在Intelk 80386中地址空间容量为4G,各个进程地址空间隔离,意味着每个进程独享4G线性空间。多个进程难免出现进程之间的切换,线性空间随之切换。基于分页机制,对于4GB的线性地址一部分会被映射到物理内存,一部分映射到磁盘作为交换文件,一部分没有映射,通过下面加深一下印象

17、分页与分段的区别?

我们知道计算机的五大组成部分分别为运算器,存储器,存储器 ,输入和输出设备。我们的数据或者指定都需要存放内存然后给 CPU 大哥拿去执行。我们平时写的代码不是直接操作的物理地址,我们所看到的地址实际上叫做虚拟地址,通过相应的转换规则将虚拟地址转换为物理地址。

那么虚拟地址是怎么转换为物理地址的呢?

第一种方式,采用一个映射表代表虚拟地址到物理地址的映射,在计算机中我们叫做页表。页表将内存地址分为页号偏移量,举个例子

我们将高位部分称为内存地址的页号,后面的低位叫做内存地址的偏移量。我们只需要保存虚拟地址内存的页号和物理内存页号之间的映射关系即可。

【操作系统系列】2 高频操作系统面试题解答2_调度算法_05

这样说了,也就是三部曲

  • 虚拟地址-----> 页号+偏移量
  • 通过页表查询出虚拟页号,对应的物理页号
  • 物理页号+偏移量-----> 物理内存地址

【操作系统系列】2 高频操作系统面试题解答2_调度算法_06

这样的方法,在32位的内存地址,页表需要多大的空间?

在一个32位的内存地址空间,页表需要记录2^20个物理页面的映射关系,可以想象为要给数组。那么一个页号是完整的4字节。这样一个页表就是4MB。
再来,我们知道进程有各自的虚拟内存空间,也就是说每个进程都需要一个这样的页表,不管此进程是只有几KB的程序还是需要GB的内存空间都需要这样的页表,用这样的结构保存页面,内存的占用将非常的大,那其他方式是怎么样的呢
多级页表
同样的虚拟内存地址,偏移量部分和上面方式一样,但是我们将页号部分拆分为四段,从高到低分成4级到1级的4个页表索引

【操作系统系列】2 高频操作系统面试题解答2_死锁_07

这样一来,每个进程将有4级页表。通过4级页表的索引找到对应的条目。通过这个条目找到3级页表所在位置,4级的每一个条目可能有多个3级的条目,找到了3级的条目后找到对应3级索引的条目,就这样到达1级页表。1级对应的则为物理页号。最终通过物理页号+偏移量的方式获取物理内存地址。

18 为什么使用多级页表

  • 使用多级页表可以让页表在内存中离散存储。多级页表通过索引就可以定位到具体的项。举个例子,假设当前虚拟地址空间为4G,每个页的大小为4k,如果是一级页表的话,共有2……20个页表项,假设每个页表项需要4B,那么存放所有的页表项需要4M,那么为了随机访问,我们就需要连续的4M内存空间存放所有的页表项。这样一来,随着虚拟地址空间的增大,需要存放页表所需的连续空间也就越来多大。如果使用多级页表,我们只需要一页存放目录项,页表存放在内存其他位置即可,下面有例子进一步讲解
  • 使用多级页表更加节省页表内存。理论上,使用一级页表,需要连续存储空间存放所有项。使用多级页表只需要给实际使用的的那些虚拟地址内存的请求分配内存

举个例子
假设虚拟地址空间为4G,A进程只是用 4M 的内存空间。对于一级页表,我们需要 4M 空间存放这4GB 虚拟地址对应的页表,然后找到进程真正的 4M 内存空间。这样的话,A进程本来只使用 4MB 内存空间,但是为了访问它,我们需要为所有的虚拟地址空间建立页表,岂不是很浪费。对于二级页表而言,使用一个页目录就可定位 4M 的内存,存放一个页目录项需要 4k,还需要一页存放进程使用的 4M,4M=1024*4k,也就相当于 1024 个页表项就可以映射4M的内存空间,那么总共就只需要4k(页表)+4k(页目录)=8k来存放进程需要的 4M 内存空间对应页表和页目录项。这样看来确实剩下不少的内存。

那使用多级页表有啥缺点?

还是有的,咋们使用一级页表的时候,只需要访问两次内存,一次是访问页表项,一次是访问需要读取的一页数据。如果是二级页表,就需要访问三次,第一次访问页目录,第二次访问页表项,第三次访问读取的数据。访问次数的增加以为访问数据所花费的总时间也增加

19、页面置换算法有哪些?

请求调页,也称按需调页,即对不在内存中的“页”,当进程执行时要用时才调入,否则有可能到程序结束时也不会调入。而内存中给页面留的位置是有限的,在内存中以为单位放置页面。为了防止请求调页的过程出现过多的内存页面错误(即需要的页面当前不在内存中,需要从硬盘中读数据,也即需要做页面的替换)而使得程序执行效率下降,我们需要设计一些页面置换算法,页面按照这些算法进行相互替换时,可以尽量达到较低的错误率。常用的页面置换算法如下:

  • 先进先出置换算法(FIFO)

先进先出,即淘汰最早调入的页面。

  • 最佳置换算法(OPT)

选未来最远将使用的页淘汰,是一种最优的方案,可以证明缺页数最小。

  • 最近最久未使用(LRU)算法

即选择最近最久未使用的页面予以淘汰

  • 时钟(Clock)置换算法

时钟置换算法也叫最近未用算法 NRU(Not RecentlyUsed)。该算法为每个页面设置一位访问位,将内存中的所有页面都通过链接指针链成一个循环队列。。

20 书籍/视频学习推荐

书籍

  • Linux内核设计与实现
  • 操作系统导论
  • 现代操作系统
  • 深入理解操作系统

视频

  • B站 ----哈工大李x军老师讲解

https://www.bilibili.com/video/av17036347/

唠嗑

关于操作系统的内容细节非常的多,也将伴随你的大学四年,考研多半会考,找工作对你底层功力的考察也跑不掉,更重要的在了解其他技术的时候你会发现其实在操作系统基本原理中都有类似思想。
另外说说工作上的事儿,最近刚工作两周,咋们小组牛批了,八个人六个小姐姐,相关故事后续慢慢道来,一定带劲儿。
最后,如果你能在这篇文章中有点点收获,请不要吝啬你的在看点赞,这将给与小蓝更多写作的动力,fighting。

周末愉快,每篇进步一点点~~