操作系统面经

521 阅读20分钟

1、进程、线程、协程的区别和联系

进程是拥有资源是基本单位,运行一个程序时会创建一个或者多个进程,线程是系统调度的基本单位,线程不拥有系统资源,多个线程共享隶属进程的系统资源,因为不占用系统资源,所以线程比进程更加轻量,线程切换的速度比进程更快。协程是线程内部调度的基本单位,处于用户态,由用户控制上下文切换,不需要陷入内核态就能执行与操作,比线程更加轻量,但协程本身并不能直接被CPU调度,需要通过一个调度器利用线程的资源去执行。

  • 线程创建销毁只需要销毁PC、状态码、通用寄存器、线程栈、栈指针;
  • 进程创建销毁需要重新分配及销毁整个的任务结构。

2、一个进程可以创建多少线程,和什么有关?

和用户态的虚拟空间大小及创建线程消耗的内存空间有关。

  • 如果是32位系统,用户态虚拟空间大小是3G如果创建一个线程消耗10M那么大约能创建300个。
  • 如果是64位系统,用户态虚拟空间内存大小是128T理论上不会受虚拟内存大小的限制,而会受系统的参数或性能限制。

3、外中断和异常有什么区别?

外中断是CPU执行指令以外的事件引起的,如IO中断、时钟中断、控制台中断等; 而异常是由CPU执行指令的内部事件引起的,如非法操作码、地址越界、算术溢出等。

4、进程线程模型

  • 多线程模型:表示的是一个进程内部有多个线程,所有的线程共享该进程下的内存空间,进程中系统资源如堆空间会被所有的线程共享。
  • 多进程模型:每一个进程都是资源调度的基本单位。运行一个可执行程序会创建一个或者多个进程。

5、进程调度算法你了解多少

  • 先来先服务算法。按请求的顺序进行调度,有利于长作业,不利于短作业;因为短作业需要等待前面的长作业执行完才能得到调度,而长作业需要执行很长时间,所以短作业需要等待很久才能被调度。
  • 短作业优先:非抢占式调度算法,按进程的估计运行时间由小至大进行调度,利于短作业,不利于长作业。因为若一直有短作业到来,长作业会处于饿死的状态,一直得不到调度。
  • 最短剩余时间:抢占式调度算法,每次都调度最短剩余运行时间的进程。可以说是抢占式的短作业优先算法。
  • 时间片轮转算法:将所有进程放在一个队列中,每次调度时,都为队首进程分配一个时间片,这个时间片执行完后,将进程放入队列的队尾。这个算法效率和时间片的大小有很大关系,如果时间片过小,会导致进程切换得太频繁,在进程切换上就会花过多时间。而如果时间片过长,那么实时性就得不到保证。
  • 优先级调度算法:为每个进程分配一个优先级,按进程的优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
  • 多级反馈队列:一个长进程使用时间片轮转算法时,可能要执行100个时间片。多级队列是为这种需要执行多个时间片的进程考虑的,它设置了多个队列,每个队列时间片大小不同,如:2,4,6,8...。进程在第一个队列未执行完,就会被移到下一个队列执行。

6、Linux下进程、线程间的通信方式?

6.1 进程通信

  • 管道:管道是一种半双工的通信方式,数据只能单向流动,遵循先进先出的原则。管道分为无名管道和有名管道。

    • 无名管道:只能用于有亲缘关系的进程之间的使用。进程的亲缘关系通常是指父子进程关系。
    • 有名管道:允许在没有亲缘关系的进程之间使用。
  • 消息队列:消息队列是有消息的链表,存放在内核中并由消息队列标识符标识,它克服了信号传递信息少、管道只能承载无格式字符流以及缓冲区大小受限等缺点。

  • 共享内存+信号量:映射一段能够被其它进程所访问的内存,它往往和信号量同时使用,以实现进程间的同步和通信。

  • 套接字:一般用于不同机器间进程通信,在本地也可作为两个进程通信的方式。

  • 信号:用于通知和接受进程某个事件已经发生,以让其执行对应的逻辑。

6.1、线程间通信

在操作系统中,一个进程对应多个线程,多个线程共享隶属进程的系统资源,所以一个进程内的线程是无需通信的,因为其操作的资源是相同的,但是需要同步互斥机制保证数据的一致性。而不同进程的线程间通信是通过进程通信完成的。所以线程间的通信的目的是为了保证数据同步。

  • 信号
  • 信号量
  • 锁机制+条件变量

7、Linux下同步机制?

  • 信号量:可用于进程同步,也可用于线程同步。
  • 互斥锁 + 条件变量:只能用于线程同步。

8、 如果系统中具有快表后,那么地址的转换过程变成什么样了?

  1. 首先,CPU给出逻辑地址,由某个硬件算出页号、页内偏移量,将页号与快表中的页号进行比较。
  2. 匹配成功:则直接从中取出该页对应的内存块号,再将内存块号与页内偏移量拼接得到物理地址,最后访问物理地址对应的内存空间,此时一次就可命中。
  3. 匹配失败:说明快表中无备份,则需要去内存页表中去查询,此时需要查询两次才能命中。 由于查询快表的速度比查询页表的速度快很多,因此只要快表命中,就可以节省很多时间。 因为局部性原理,–般来说快表的命中率可以达到90%以上。

9、内存交换和覆盖有什么区别?

交换技术主要是用于不同进程间的,而覆盖则用于同一程序或进程中。

10、动态分区分配算法有哪几种?

  1. 首次适应算法:每次都从低地址开始查找,找到满足大小的第一个空闲分区进行分配。
  2. 最佳适应算法:由于动态分区分配是一种连续分配方式,为各进程分配的空间必须是连续的一整片区域。因此为了保证当“大进程”到来时能有连续的大片空间,可以尽可能多地留下大片的空闲区,即,优先使用更小的空闲区。它的做法是将空闲分区按容量递增次序链接,每次按顺序查找,找到第一个满足大小的空闲分区。
  3. 最大适应算法:为了解决最佳适应算法的问题—即留下太多难以利用的小碎片,可以在每次分配时优先使用最大的连续空闲区,这样分配后剩余的空闲区就不会太小,更方便使用。空闲分区按容量递减次序链接。每次分配内存时顺序查找空闲分区链(或空闲分区表),找到大小能满足要求的第一个空闲分区。
  4. 邻近适应算法:首次适应算法每次都从地址空间进行查找,这可能导致低地址空间出现很多连续的碎片空间,而每次查找时都要经过,这增大了系统开销。邻近适应算法是每次都从上一次查找结束的位置开始检索,以解决该问题。

11、虚拟技术你了解吗?

虚拟技术是把一个物理实体转换为多个逻辑实体。包括时分复用技术和空分复用技术。

  • 多个进程在处理机上并发的执行就用到了时分复用技术。每个进程执行一个时间片后就快速的切换执行下一个进程,让用户觉得好像所有程序在同时执行。
  • 虚拟内存技术就用到了空分复用技术。它将物理内存抽象为地址空间,每个进程都有自己的地址空间,地址空间的页不需要全部存储在物理内存中,当使用到一个没有物理内存的页时,执行页面置换算法,将该页置换到内存中。

12、进程状态的切换你知道多少?

包括就绪状态、运行状态、阻塞状态。

  • 就绪状态:当一个进程被创建,其资源也分配完毕,那么就会进入就绪态等待处理机调度。
  • 就绪->运行:处理机按调度算法进行调度,当该进程被调度就会进入到运行态。
  • 运行->阻塞:进程被调度时,请求的资源被其它进程占用,进程就会进入阻塞态。
  • 阻塞->就绪:请求的资源被其它进程释放,拥有了资源就会进入就绪态,继续等待处理机调度。
  • 运行->就绪:当前CPU分配的时间片用完,就会转为就绪状态,等待下一次调度。

13、通过例子讲解逻辑地址转换为物理地址的基本过程

首先,根据逻辑地址计算页号和页内偏移量 根据逻辑地址A中的页号和页表寄存器中的页表始址,得到内存块号,结合页内偏移量得到物理地址。 例如:页面大小L为1K字节,页号2对应的内存块号b=8,将逻辑地址A=2500转换为物理地址。

  • 根据A得到页号=A//L=2=b,页内偏移量=A%L=2500%1024=452。那么物理地址=bL+W=81024+500

image.png

14、怎么实现进程同步

在多个进程并发执行去访问共享的内存时,为了避免出现数据不一致或死锁等问题。

临界区:多个进程通过共享内存去通信时,这片共享的内存空间就是临界区,如果同一时刻有多个进程访问临界区资源的话,可能导致丢失修改等问题。为了防止这类问题的出现,进程间需要互斥的访问数据,互斥是指同一时刻只允许一个进程访问临界区资源。此外,多个进程间因为合作产生的一些直接制约关系,使得进程有一定的先后执行关系。为了保证同步与互斥的访问临界区资源,通常通过信号量的方式去实现,信号量(Semaphore)是一个整型变量,可以对其执行 down 和 up 操作,也就是常见的 P 和 V 操作。

  • down:当一个进程访问临界区资源时,首先检查信号量是不是大于0,如果不是,则说明此时有其它进程正在访问临界区,进程就等待其被释放。如果是,down执行-1操作,该进程开始访问临界区。
  • up:当进程访问进程结束后,信号量执行+1操作,相当于释放锁。

使用信号量实现生产者-消费者问题

问题描述:使用一个缓冲区存放物品,只有缓冲区没满,生产者才能放入物品,只有缓冲区不为空,消费者才能拿走物品。

需要设置三个信号量来实现:

  1. empty:初始为缓冲区容量,生产者进程使用,当empty大于0时,生产者才能向缓冲区放入物品。
  2. full:初始为0,消费者进程使用,当full大于0时,消费者才能从缓存队列拿物品。
  3. mutex:锁初始为1,缓冲区是临界资源,各进程需要互斥的访问,该变量实现锁的逻辑,只有mutex大于0,才能访问缓冲区。
const N = 5

var queue chan int

var mutex int = 1
var empty int = N
var full int = 0

func down(signal *int) {
   for *signal <= 0 {
   }
   *signal--
}
func up(signal *int) {
   *signal++
}
func producer() {
   item := 0
   for {
      fmt.Println("开始生产...")
      down(&empty)
      down(&mutex)
      queue <- item
      fmt.Printf("生产者生产了物品:%d\n", item)
      item++
      up(&mutex)
      up(&full)
   }
}

func consumer() {
   for {
      fmt.Println("开始消费...")
      down(&full)
      down(&mutex)
      item := <-queue
      fmt.Printf("消费者拿走了物品:%d\n", item)
      up(&mutex)
      up(&empty)
   }
}

func main() {
   queue = make(chan int, N)
   for i := 0; i < N; i++ {
      go producer()
   }
   for i := 0; i < N; i++ {
      go consumer()
   }
   time.Sleep(time.Second * 5)
}

15、操作系统在对内存进行管理的时候需要做些什么?

  • 操作系统要负责内存空间的分配和回收
  • 操作系统需要使用某种技术,如空分复用技术从逻辑上扩充内存空间
  • 操作系统需要提高逻辑转换的功能,负责将逻辑地址转为物理地址。
  • 操作系统需要提高内存保护的功能,保证各进程在各自的存储空间运行,互不干扰

16、虚拟内存的目的是什么?

虚拟内存是为了将物理内存扩充为更大的逻辑内存,从而让程序获得更大的可用内存。它的做法是,将内存抽象成地址空间。每个程序都拥有自己的地址空间,这个地址空间被分为很多页,这些页被映射到物理内存,它不需要连续的物理内存,也不需要所有页都必须在内存中,当程序引用不在内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。

17、说一下你理解中的内存?他有什么作用呢?

内存是用来存放数据的硬件,程序执行前需要放到内存中才能被CPU处理。

18、操作系统经典问题之哲学家进餐问题

五个哲学家围着一张圆桌,每个哲学家面前放着食物。哲学家的生活有两种交替活动:吃饭以及思考。当一个哲学家吃饭时,需要先拿起自己左右两边的两根筷子,并且一次只能拿起一根筷子。

所以为了防止死锁,可以:

  • 让哲学家只有能同时拿起两根筷子的时候才能拿起筷子
  • 只有邻居没进餐时,他才能进餐

19、操作系统经典问题之读者-写者问题

允许多个进程同时对数据进行读操作,但是不允许读和写以及写和写操作同时发生。

20、介绍一下几种典型的锁?

  • 读写锁(进程锁):允许多个进程同时读取,但不允许多个进程同时写、有进程在写时,不允许进程读。
  • 互斥锁(进线程锁):抢锁失败会主动放弃CPU资源,进入睡眠状态,至到锁的状态改变时,再唤醒进程。为了实现状态改变时唤醒进程或者线程,需要把锁交给操作系统管理。
  • 条件变量(进线程锁):条件变量是线程同步的机制,他往往和互斥锁一起使用。当条件不满足时,线程往往解开相应的互斥锁阻塞线程然后等待条件发生变化,一旦其它的某个线程改变了条件变量,他将通知相应条件变量唤醒一个或者多个正在被此条件变量阻塞的线程。
  • 自旋锁(进线程锁):如果进线程无法获取锁,进线程不会立即放弃CPU时间片,而是一直循环尝试获得锁,直到获取为止。如果别的线程长期占用,那么自选就是在浪费CPU做无用功,但自旋效率较高,一般用于加锁时间短的场景。

21、逻辑地址和物理地址

编译时只需要知道进程相对地址,当进程被调度时,通过地址转换机构,计算进程的绝对地址,这个相对地址就是逻辑地址,绝对地址就是物理地址。

22、内存的覆盖是什么?有什么特点?

因为程序执行的时候尤其是大程序,并不是要一次性访问全部的数据和代码段,为了提高内存的利用率,所以就有了内存覆盖技术,它将内存分为固定区和覆盖区,固定区存储了常常需要访问的代码段和数据段,覆盖区存储了局部访问的数据和代码段,当有不在内存中的代码和数据需要被访问时,它们会被系统放入覆盖区,替换原有的段。

23、内存交换是什么?

内存空间紧张时,系统将内存中某些进程暂时换出至外存,将已经具备允许条件的某些进程换入内存中运行。

24、什么时候会进行内存的交换?

内存交换通常发生在许多进程运行内存吃紧时,当有许多进程缺页,就将部分进程换出,将准备好运行的进程换入,若缺页率明显下降,就停止换出。

25、终端退出,终端运行的进程会怎样

终端在退出时会发送SIGHUP给对应的bash进程,bash进程收到这个信号后首先将它发给session下面的进程,如果程序没有对SIGHUP信号做特殊处理,那么进程就会随着终端关闭而退出。

26、什么是快表,你知道多少关于快表的知识?

快表又称联想寄存器,是一种访问速度比内存快很多的高速缓存存储器,用来存储当前访问的若干页表项,以加快地址变换的过程,与此对应的,内存中的页表称为慢表。

27、地址变换中,有快表和没快表,有什么区别?

有快表时,可能只需要访问一次内存就可命中,而无快表则需要访问两次内存。地址转换首先在快表中查询有无页号的备份,

  • 如果找到,则转换为物理地址,直接访问物理内存空间。此时需要访问一次内存。
  • 没找到,则需要去内存中查找页号,再转换为物理内存地址,进行范围,此时需要访问二次内存。 无快表时,直接去内存中查找页号,找到后转换为物理地址进行访问。此时需要访问二次内存。 注意:有快表时,访问快表时是在高速缓存中进行的,速度非常快,这次访问所花时间与访问内存花费时间相比可以忽略不计。

28、死锁

  1. 死锁产生的必要条件
    • 互斥:进程互斥的访问某一临界资源。
    • 不可剥脱:不能剥脱进程拥有的资源。
    • 请求和保持:进程请求某一资源时,失败要持续的保持请求,不会放弃自己所拥有的资源。
    • 循环等待:各个进程循环的请求对方所拥有的资源。
  2. 解决办法
    • 鸵鸟策略:忽视死锁。因为解决死锁问题代价高,当发生死锁的概率很小或者发生了影响也不大,可以采用该策略。
    • 死锁检测与恢复:不阻止死锁的发生,而是用检测办法去检测死锁,再解除死锁。例如,深度优先遍历进程执行的有向图,每次遍历都对进程进行标记,当遍历到已经标记过的进程时,就说明发生了死锁。怎么恢复和解除死锁?可以通过打破死锁的四个必要条件去解除死锁,例如杀死环路中的某一个进程、剥夺导致环路发生的进程资源。
    • 死锁预防:破坏死锁的四个必要条件去预防。破坏互斥条件:设置一个代理去访问资源,进程访问该资源时通过该代理实现。破坏不可剥夺条件:允许资源的抢占。破坏请求和保持等待:进程执行前就准备好所有所需要的资源。破坏循环等待条件:该资源编号,按编号顺序来请求资源。
    • 死锁避免:使用银行家算法。使用多个表记录当前资源状态和进程资源状态,包括最大资源表,当前可用资源表,当前执行进程未来会用到的资源表。如果找不到一条路径,让进程全部执行完成,那么就认为该进程是不安全的,会发生死锁,就避免进入这种状态。

29、页面置换算法

  • 最佳置换算法:将未来最长时间不会被访问的页面换出。这是一个理想化的方法,无法实现。
  • 先进先出置换算法:将最先进入的页面置换出去。
  • 最近最长时间未使用置换算法:将最长时间未使用的页面置换出去。
  • 时钟置换算法:使用标志位记录页面的访问情况,每次淘汰标志位为0的页面,即最近未被访问过的页面。当所有的页面标志位都是1,则全部置零,以确保存在能被置换出去的页面。
  • 改进型时钟算法:如果某个内存页面数据被修改过,那么置换的时候需要执行IO操作将其写回外存,所有淘汰时,应该优先淘汰未必修改且最近未被使用的内存页。因此改进型时钟算法添加了一个修改标志位。

30、为什么分段式存储管理有外部碎片而无内部碎片?为什么固定分区分配有内部碎片而不会有外部碎片?

  • 分段式内存管理是按需分配,所以不会有内部碎片,但是可能会导致许多碎小的外部碎片难以被利用。
  • 固定分区分配是按固定大小分配内存,所以该固定大小的内存不一定会完全被利用而导致内部碎片。而无外部碎片。

31、服务器的高并发解决方案

  • 使用集群和分布式架构,将压力叫平均的分配给多台服务器。
  • 使用缓存技术,如redis,提高单台服务器性能。
  • 将动态资源和静态资源分离。

参考资料

操作系统 | 阿秀的学习笔记 (interviewguide.cn)