操作系统面试题汇总🙌巩固你的计算机基础✨

391 阅读22分钟

我并没有系统的学习过计算机操作系统的课程,之前因为准备面试,发现大厂会比较喜欢考这一方面的问题,所以根据一些朋友面经的题目,再查找一些好的文章将其汇总了一下。其中像进程与线程的区别,同步与异步,阻塞、非阻塞,死锁了解这些概念我认为对加深前端某些知识的理解还是很有帮助的。对于想冲击大厂的同学来说像磁盘寻道进程之间的通信方式这些问题也是经常会考察的。

一、进程和线程的区别是什么?

网上关于这个问题有很多的解答,也有很多生动的例子,但这里给出我自己比较认可的一种回答,仅供参考。


本质上来说,进程与线程都是CPU工作时间段的描述,也就是运行中程序指令的描述

背景知识:


为了解释上面这句话,我们首先需要了解一些关于计算机的基础知识。

  1. 对于CPU来说,其执行任务的速度是非常快的。也就是说即使有多个任务要执行,但对于CPU来说每个任务也是轮流执行的,由于执行的速度太快,以至于我们认为其是同时在处理多个任务的。

  2. CPU在执行某段程序,或者任务之前需要将所有相关资源准备好。这些任务都处于就绪队列,然后等待操作系统的调度算法,选出某个任务去给CPU执行,之后PC指针指向该代码的开始,由CPU取指令然后一个个执行,在CPU执行某个任务的时候对应所需的资源会加载在RAM中。

  3. 这里CPU执行每一个任务的过程其实就可以理解为一个个进程,而所需准备的资源就可以理解为进程执行过程中的所需要的上下文环境,当某个进程执行完毕,或者分配给他的CPU的时间段用完了,需要先保存当前进程的上下文环境,等待CPU的下次执行。


那么多个进程是怎么样有序的执行的?首先加载一个进程的上下文环境,执行完毕之后保存其上下文环境,之后加载下一个进程的上下文环境,执行完毕,保存上下文环境。。。如此重复。

说了这些我想大家应该对进程的本质有了自己的理解了,为了描述计算机在切换上下文之间CPU执行程序的时间段我们有了进程和线程这两个名词

插入一点不相关的:名词是对客观事物的指代,形容词是对客观事物的描述。


之后的线程就很好理解了,线程是相比于进程更小CPU执行时间段,我们假设CPU要执行一个程序A也就是处理一个进程,而这个程序由a,b,c三部分组成。大家可以看下面这个例子来理解。

我们可以想象打开Chrome浏览器相当于打开了一个进程,而如果我们需要显示一个页面需要Chrome浏览器解析JS代码的线程,发送请求数据的线程,生成位图数据的线程等,他们以某种顺序轮流工作最终生成了我们要显示的页面。


执行过程就是首先会加载A的上下文环境,然后CPU执行a部分,之后执行b,之后执行c,然后保存A的上下文环境。这里在切换线程的上下文环境时所消耗时间远远小于切换进程上下文所需要的时间,所以CPU的利用率大大提高。

全局变量我们在程序几乎任何地方都可以使用,相当于所有的线程共享当前进程的上下文环境,也就是当前进程的地址空间

注意这里描述的进程线程概念和实际代码中所说的进程线程是有区别的。编程语言中的定义方式仅仅是语言的实现方式,是对进程线程概念的物化。


什么是单线程与多线程?


CPU的执行时间段,从宏观与微观上可以分为进程与线程,而线程又可以分为单线程多线程

  • 单线程:这个很好理解,我们的的Javascript是单线程的,也就是说浏览器在解析JS代码时只有一个线程在工作的,如果发现某地方有错误就会停止,后续无法执行。这个有错误后续无法执行的过程也称之为阻塞
  • 多线程:多个线程按照某种顺序轮流执行,多线程在上面的例子中,已经可以很好的说明了,但是我们要注意的是多线程的程序看起来像多个线程同时运行,其实对于CPU来说他们只是按照某种顺序轮流执行罢了。


简单来说就是在一个进程中,同一时间只能做一件事就是单线程,可以做多件事就是多线程。

什么是并发与并行?

  • 并发:并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。

我们想象有多个用户访问服务器,如果将一个用户的请求处理完才能处理下一个用户的请求,那也太过于浪费时间了,所以实际上我们好像是可以同时访问一个网站,但是对于被访问网站的服务器来说其还是一个个处理不同客户端的请求的,只不过其处理请求的速度太快让我们误以为多个用户是同时访问网站的。


  • 并行:同一时刻,多个指令,在不同的处理机上运行。比如我们一边听歌一边写代码,播放器进程和VScode进程就是并行的。


进程与线程两者区别是什么?


说了上面这些再回答这个问题,还是希望大家从理解的角度来记忆,从而在面试时给面试官一个满意的答案。

  1. 首先进程与线程本质都是CPU工作的时间段的描述,这句话可以得50分
  2. 进程是资源分配的基本单位,线程是程序执行的基本单位
  3. 进程拥有独立的地址空间,多进程程序一个进程崩溃了不影响其他进程。多线程程序共享当前进程的地址空间,一个线程崩溃了可能导致整个进程崩溃
  4. 线程之间通信很容易,本身就共享同一块内存,只要指针指向就可以了,而进程之间通信就比较麻烦,这里面试官可能会问进程之间如何通信等,文章后面有回答


其实多线程程序从宏观上来看,是让一个程序在运行时有多个功能可以同时执行,但本质上对于CPU来说还是离不开切换当前线程的上下文(当然切换线程的上下文比较容易),按某种顺序执行不同的线程等。

二、什么是同步与异步?


同步和异步本质上关心的是消息的通信机制 (synchronous communication/ asynchronous communication)。

同步:如果你发起一个调用,在没有得到结果之前这个调用是没有返回值的,如果一旦有了结果,该调用就有了返回值。也就是说我们需要主动的等待调用的结果

异步:异步正好相反,如果我们发起了一个调用,这个调用就立刻返回了,所以没有返回结果。也就是说当一个调用被发起后,我们是不会立刻得到结果的,而是等待被调用者通过状态、通知来通知调用者或者通过回调函数来处理这个调用很像Promsie)。

举个例子:你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。
而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。


三、什么是阻塞与非阻塞?


阻塞和非阻塞关注的是程序在等待调用结果(消息返回值)时的状态

阻塞调用是指当前调用在没有得到返回结果之前,线程会被挂起。无法执行其他调用,只有返回结果之后才可以。

非阻塞调用指在没有返回结果之前,该线程还可以满足其他调用,不会被阻塞

举个例子:你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。

在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。

总结一下就是,同步与异步主要关注消息的通信机制,而阻塞与非阻塞主要关注的是等待程序调用结果时的状态

同步与异步主要关心的是消息通知时的方式,如果调用立刻就有返回值那就是同步,如果调用时不是立刻有返回值而是有返回值时通过某种方式通知我们那就是异步。

阻塞与非阻塞主要关心的是等待通知时的状态,如果等待通知时不能做其他的事就是阻塞,而等待通知与做其他事无影响那就是非阻塞。

四、进程之间的通信方式有哪些,他们的优缺点有哪些呢?


上面已经说过,每一个进程都有自己的用户地址空间,进程中是无法获取到其他进程的全局变量或数据的,所以如果进程之间要进行通信必须通过“第三者”也就是内核,在内核中开辟一块内存缓冲区,进程A将数据拷贝到内核缓冲区之中,进程B要使用的话就必须去内核缓冲区中读取,这样就完成了进程之间的相互通信,内核提供的这种机制就称之为进程间通信IPC,InterProcess Communication)。



管道:


管道分为两种,一种是匿名管道,一种是具名管道。在Linux中使用“|”来表示管道,有兴趣的同学可以去看看它的用法。

管道的实质是一个内核缓冲区,进程以先进先出的方式从缓冲区存取数据,管道一端的进程顺序的将数据写入缓冲区,另一端的进程则顺序的读出数据,我们可以将其想象成一个队列。


匿名管道

  • 其是半全双工的,数据只能单向流动,如果双方都需要通信就需要建立两个管道
  • 其只能用在亲戚进程之间,父子进程,兄弟进程
  • 数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
  • 注意通信时的数据还是在内存中的

具名管道

  • 解决了匿名管道只能在亲戚进程之间通信的问题
  • 管道的名字存放在系统磁盘,内容存放在内核中


管道的优缺点

优点很明确,在Linux系统中使用很简单,存在阻塞机制,如果可以确保数据被取走

缺点是内核的缓冲区是有限的,并且也正是由于存在阻塞机制a进程给b进程传递数据,b进程读取数据后a进程才可以返回。所以不适合频繁通信的情况。

信号:

信号是软件层次上对中断机制的一种模拟,是一种异步通信方式,信号可以在用户空间进程和内核之间直接交互,内核可以利用信号来通知用户空间的进程发生了哪些系统事件。

  • 信号是Linux系统中常用的一种进程间的通信方式,可以在任何时刻向进程进行通信,而不需要知道进程的状态
  • 如果当前进程并未处于执行状态,那么该信号会保存在内核中,当进程进行执行时传递给进程
  • 如果该进程将信号设置为阻塞,那么该信号将会被延时传递,直到阻塞取消

也就是说信号主要是在进程与内核直接进行传递,可以用于控制进程终止退出等,信号事件主要有两个来源

  • 硬件来源:用户按键输入Ctrl+C退出、硬件异常如无效的存储访问等。
  • 软件终止:终止进程信号、其他进程调用kill函数、软件异常产生信号


Linux中的信号有:

(1)SIGHUP:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。 (2)SIGINT:程序终止信号。程序运行过程中,按Ctrl+C键将产生该信号。 (3)SIGQUIT:程序退出信号。程序运行过程中,按Ctrl+\\键将产生该信号。 (4)SIGBUS和SIGSEGV:进程访问非法地址。 (5)SIGFPE:运算中出现致命错误,如除零操作、数据溢出等。 (6)SIGKILL:用户终止进程执行信号。shell下执行kill -9发送该信号。 (7)SIGTERM:结束进程信号。shell下执行kill 进程pid发送该信号。 (8)SIGALRM:定时器信号。 (9)SIGCLD:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。


消息队列:


我们知道对于管道是存在阻塞机制的,如果一端要传递数据那么直到另一端接收数据后进程才可以返回,那么可不可以让进程将数据放在某个内存之后就立刻返回呢?

答案就是消息队列的通信模式,A进程将数据放在消息队列后就返回了,如果之后B进程需要就去消息队列中取。这个有点类似缓存。

当然这个方式解决了管道只能单向传输,存在阻塞机制的问题,但是如果通信的数据过大,在反复通信的情况下,系统消耗还是比较严重。

注意:匿名管道是存在于内存文件,中具名管道存在于系统磁盘中,而消息队列是存在于内核中的。

共享内存:


既然消息队列还是存在拷贝大量数据时效率不高的问题,那么如果两个进程可以共享一块内存那这个问题不就解决了?

其实系统分配给进程的不是真实的物理内存,而是地址空间,我们让多个进程共享一块地址空间就可以让进程间通信达到最快的速度,但每个进程依然拥有自己独立的地址空间,只有一部分是共享的。

信号量:


我们知道JS设计成单线程本质上是为了如果存在多个线程同时修改DOM,那么页面中的DOM究竟是处于什么状态?

信号量的目的本质上也是为了解决如果多个线程共享内存,一个线程在使用内存的时候让其他线程不再使用,防止同时修改出现问题。

信号量本质上就是计数器,原理就是假设初始信号量为1,如果A进程访问内存的时候将信号量置为0,其他内存要访问的时候看见信号量为0就不再访问了。

Socket:


如果上面说的进程之间的通信是在同一个计算机之中的话,那么Socket套接字便是为处于不同计算机之间进程通信提供了一种方式。


 


我们可以看见其处于应用层与传输层之间,是应用层与传输层之间的桥梁。


差不多就是这些了,虽然也就只有几个但是我还是查看了许多的文章,希望可以简单好理解的总结出来,很多太复杂的我也不愿去记了,毕竟这部分日常使用的也比较少。

五、请说说磁盘调度(寻道)算法?

先来看看磁盘结构:



常见的机械磁盘是上图左边的样子,中间圆的部分是磁盘的盘片,一般会有多个盘片,每个盘面都有自己的磁头。右边的图就是一个盘片的结构,盘片中的每一层分为多个磁道,每个磁道分多个扇区,每个扇区是 512 字节。那么,多个具有相同编号的磁道形成一个圆柱,称之为磁盘的柱面,如上图里中间的样子。

磁盘调度算法的目的很简单,就是为了提高磁盘的访问性能,一般是通过优化磁盘的访问请求顺序来做到的。

寻道的时间是磁盘访问最耗时的部分,如果请求顺序优化的得当,必然可以节省一些不必要的寻道时间,从而提高磁盘的访问性能。

接下来主要介绍一下几种寻道算法:

  • 先来先服务算法
  • 最短寻道时间优先算法
  • 扫描算法算法
  • 循环扫描算法
  • LOOK 与 C-LOOK 算法


先来先服务算法 FCFS:






根据进程访问磁盘的先后顺序来进行调度。

优点:简单,公平每一个进程的请求都能依次得到处理,不会出现某一进程的请求长期得不到满足的情况。

缺点:由于此算法未对寻道进行优化,导致平均寻道时间较长。

最短时间优先算法 SSTF:


优先处理距当前磁头距离最近的磁道上的进程请求。

优点:在性能上要优于先来先服务算法,可以使每次寻道时间最短。

缺点:如果在动态的请求情况下,可能会出现饥饿问题,导致磁头一直在一个小范围移动,有些进程的请求永远无法得到处理。

扫描算法(电梯算法) SCAN:


因为最短时间优先算法会造成饥饿问题,也就是有些磁路可能访问不到,导致磁头总在一个小范围移动,所以我们让磁头一直沿一个方向移动直到到达这个方向上最后一个磁道再更换方向,因为这样寻道的过程十分类似于电梯,所以又称为电梯算法。

优点:这种寻道算法的性能比较好,且不会出现饥饿问题。

缺点:中间的磁道会比较占便宜,中间部分磁道的响应频率会比较高,也就是每个磁道响应的频率并不均匀。

循环扫描算法 CSCAN:

因为扫描算法会造成每个磁道响应的处理频率并不均匀,所以我们有了循环扫描算法,让磁头一直向一个方向移动,直到这个方向上的所有磁道响应处理完到达这个方向上最后一个磁道,我们快速再让磁头移动到另一方的边缘移动的过程中不会处理其他的请求,之后再次向同一方向移动。也就是说其只在解决某一方向上的请求。

循环扫描算法解决了解决了扫描算法对每个磁道上的响应处理不均匀的问题,是比较完美的一种算法。


LOOK与C-LOOK:


Look:


LOOK与C-LOOK是针对扫描算法与循环扫描每次需要到达某一方向上最后一个磁道才返回的问题,优化后只要处理完某一方向上最后一个请求就更改方向,而不需要到达最后一个磁道了。


C-LOOK:



六、谈谈你对死锁的理解

死锁的定义:


在一个集合中,每一个进程都在等待,这个集合中其他进程才能引发的事件(资源),那么这组进程就是死锁的。

简单理解就是假如A进程运行中需要获取a资源,但是a资源被B进程所占用了,那么A进程就无法继续执行下去且A进程已经获取的资源b无法释放,就一直卡在那里,过了一会B进程也需要获取b资源了由于A进程卡在那里两者都无法继续运行下去,这就发生了死锁。

所以死锁的本质就是进程之间由于资源的竞争(也可以理解为资源获取顺序的不当)而造成彼此之间都无法继续运行的情况。

注意:上面所说的资源可以是很多东西如:锁,网络连接,通知事件,磁盘等。

死锁发生的必要条件:

  • 互斥条件:同一时间,同一资源只可以被一个进程所占用,当该资源被占用时其他进程便不可以使用
  • 请求和保持条件:当一个进程运行时需要获取的资源,被另一进程所占用时,这个进程将一直处于请求状态,当前所占用的资源也无法被释放
  • 不可剥夺条件:当一进程使用某一资源时,该资源便不可以被其他进程所使用,直到该资源被释放
  • 环路等待条件:在发生死锁的进程组内,一个进程将会占有另一进程所需要的资源从而形成一个等待闭环

如何预防以及避免死锁:


1.合理的规划资源使用顺序

既然死锁的发生多数是因为资源分配时间不合理而造成的那么我们就制定更加合理的资源使用顺序从而尽可能的避免死锁的发生。

比如原先进程A需要先使用资源a然后再使用资源b,而B进程需要先使用资源c再使用资源b,那么极有可能发生死锁的情况,我们可以让进程B进程先使用资源b使用完释放后再去使用资源c,这样来尽可能的避免死锁的发生。

上面只是举例说明一下可以通过合理的资源分配来尽可能的避免死锁的发生。


2.设置最大资源占用时间

当某一进程需要请求使用某一资源而该资源被其他进程所占用时,其会一直保持当前已经获取资源的占用而处于等待状态,从而可能造成其他进程也无法对资源的使用,从而造成死锁现象。

既然死锁可能由于资源被某一进程长时间占用所导致那么我们就设置该资源的最大占用时间,一但超过时间便主动释放。


3.预先分配所需资源

当某一进程运行时再去获取其需要的资源,如果该资源正处于被其他进程所占用状态那么很有可能造成死锁情况,所以当进程运行前我们可以提前分配给该进程运行时所有所需要的资源,这样也可以尽可能避免死锁。

但是这样会导致该资源的利用率降低,某些资源并不是在进程运行时一直被需要。



4.银行家算法

银行家善于计算🤣,所以银行家算法就是对当前所有进程正在使用的资源,以及将要使用的资源,以及剩余可利用的资源进行计算从而分析出当前状态是否安全,确保资源充足不会发生死锁。

检测死锁:


我们可以使用一些算法来定时检测在系统中是否有死锁发生,如检测当前内存资源的利用率等。

解除死锁:


当检测到死锁发生后我们便需要解决死锁,最直接的方式就是重启电脑,但是作为一名程序员我们应该更专业一些。

  • 终止相关进程:检测到发生死锁的进程撤销或者挂起它,强制其释放内存
  • 剥夺资源:强行将进程所占用的资源剥夺出来,解除死锁
  • 进程回退:将死锁进程回退到未出问题之前,不过比较难以实现



七、参考:


知乎 小林coding 大厂面试爱问的「调度算法」,20 张图一举拿下