操作系统面试题
其实操作系统各方面都很重要,但从我看过的校招面试文章来说,主要考察的方面分为:进程(线程)、内存(虚拟内存),学习操作系统方面的知识可以看看《现代操作系统》这本书,其中的许多思想和经典算法都能在其他技术和日常应用中看到类似的使用
阅读原文
本文章大量参考开源项目和技术博客,具体引用在最下方
操作系统基础
什么是操作系统?
操作系统其实本质上也是一个运行在计算机上的软件,主要用来管理计算机系统上的资源(软件资源、硬件资源),其屏蔽了硬件的复杂性,用户的硬件的操作由操作系统实际管理,操作系统核心的部分叫做内核 (Kernel),系统的内存管理,硬件设备的管理,文件系统的管理以及应用程序的管理都是由内核负责,比起操作系统,内核更适合称作软硬件的桥梁。人们常认为 Linux 是由 Linus 开发,但其实像 Linux 这样庞大的工作只凭一个人是万万无法完成的,Linus 实际开发的就是 Linux 内核
什么是系统调用?
这里先不聊系统调用,先谈谈什么内核态,内核态又称为管态(和管理员权限不是一个东西),当程序运行在 0 级特权级上时,就可以称之为运行在内核态。因为这是最高特权级,大部分用户面对的程序都是运行在用户态,用户态态的级别则是 3,运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。当我们在系统中执行一个程序时,大部分时间是运行在用户态下的,在其需要操作系统帮助完成某些它没有权力和能力完成的工作时就会切换到内核态,其中最常见的用户态切换到内核态的场景(系统调用、异常、外围设备中断)就是系统调用:用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,凡是与系统态级别的资源有关的操作(文件管理、进程控制、内存管理等),都必须通过系统调用方式向操作系统提出服务请求,并由操作系统代为完成
进程与线程
简单说一说进程和线程以及它们的区别?
- 进程是具有一定功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源调度和分配的一个独立单位
- 线程是进程的实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位
- 一个进程可以有多个线程,多个线程也可以并发执行
- 线程和进程最大的不同在于基本上各进程是独立的,而各线程则不一定,因为同一进程中的线程极有可能会相互影响。线程执行开销小,但不利于资源的管理和保护;而进程正相反
进程的状态?
- 创建状态 (new) :进程正在被创建,尚未到就绪状态
- 就绪状态 (ready) :进程已处于准备运行状态,即进程获得了除了处理器之外的一切所需资源,一旦得到处理器资源(处理器分配的时间片)即可运行
- 运行状态 (running) :进程正在处理器上上运行(单核 CPU 下任意时刻只有一个进程处于运行状态)
- 阻塞状态 (waiting) :又称为等待状态,进程正在等待某一事件而暂停运行如等待某资源为可用或等待 IO 操作完成。即使处理器空闲,该进程也不能运行
- 结束状态 (terminated) :进程正在从系统中消失。可能是进程正常结束或其他原因中断退出运行
进程的调度算法
进程的执行顺序直接表现在 CPU 的利用率之上,可以采用不同调度算法进行顺序的调控
- 先到先服务 (FCFS) 调度算法:从就绪队列中选择一个最先进入该队列的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度
- 短作业优先 (SJF) 的调度算法:从就绪队列中选出一个估计运行时间最短的进程为之分配资源,使它立即执行并一直执行到完成或发生某事件而被阻塞放弃占用 CPU 时再重新调度
- 时间片轮转调度算法:时间片轮转调度是一种最古老,最简单,最公平且使用最广的算法,又称 RR(Round robin) 调度。每个进程被分配一个时间段,称作它的时间片,即该进程允许运行的时间
- 优先级调度:为每个流程分配优先级,首先执行具有最高优先级的进程,依此类推,具有相同优先级的进程以 FCFS 方式执行。可以根据内存要求,时间要求或任何其他资源要求来确定优先级
- 多级反馈队列调度算法:前面介绍的几种进程调度的算法都有一定的局限性。如短进程优先的调度算法,仅照顾了短进程而忽略了长进程。多级反馈队列调度算法既能使高优先级的作业得到响应又能使短作业迅速完成。因而它是目前被公认的一种较好的进程调度算法。
进程的通信方式?
主要分为:管道、系统 IPC(包括消息队列、信号量、共享存储)、SOCKET
管道主要分为:普通管道 PIPE 、流管道(s_pipe)、命名管道(name_pipe)
- 管道是一种半双工的通信方式,数据只能单项流动,并且只能在具有亲缘关系的进程间流动,进程的亲缘关系通常是父子进程
- 命名管道也是半双工的通信方式,它允许无亲缘关系的进程间进行通信
- 信号量是一个计数器,用来控制多个进程对资源的访问,它通常作为一种锁机制
- 消息队列是消息的链表,存放在内核中并由消息队列标识符标识
- 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
- 共享内存就是映射一段能被其它进程访问的内存,这段共享内存由一个进程创建,但是多个进程可以访问
- 套接字主要用于在客户端和服务器之间通过网络进行通信
线程同步的方式?
面对两个或多个共享关键资源的线程的并发执行,应该同步线程以避免关键的资源使用冲突
- 互斥量 (Mutex):采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的 synchronized 关键词和各种 Lock 都是这种机制
- 信号量 (Semphares):它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量
- 事件 (Event):通过通知操作的方式 (Wait/Notify) 来保持多线程同步,还可以方便的实现多线程优先级的比较操作
什么是死锁?死锁产生的条件?
在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是两个或多个进程无限期的阻塞、相互等待的一种状态,双方都不主动,像极了爱情~~
死锁产生的四个条件(有一个条件不成立,则不会产生死锁):
- 互斥条件:一个资源一次只能被一个进程使用
- 请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放
- 不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺
- 循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系
死锁的处理基本策略和常用方法
解决死锁的基本方法如下:
预防死锁、避免死锁、检测死锁、解除死锁
解决四多的常用策略如下:
鸵鸟策略、预防策略、避免策略、检测与解除死锁
内存管理
内存管理主要负责内存的分配与回收(malloc 函数:申请内存,free 函数:释放内存),另外地址转换也就是将逻辑地址转换成相应的物理地址等功能也是操作系统内存管理做的事情
内存管理有哪几种方式?
简单分为连续分配管理方式和非连续分配管理方式这两种。连续分配管理方式是指为一个用户程序分配一个连续的内存空间,如块式管理。同样地,非连续分配管理方式允许一个程序使用的内存分布在离散或者说不相邻的内存中,常见的如页式管理和段式管理
- 块式管理:将内存分为多个大小相同的块,每个块只服务于进程,缺点是进程需要的内存过小时,会产生过多的内存碎片
- 页式管理:将内存分为多个页,但划分力度更大,通过页表来映射逻辑地址和物理地址
- 段式管理:页式管理中每页是没有实际意义的,而段使用比页更小且具有实际意义的方式进行划分,每个段定义了一组逻辑信息,可以体现当前段存储的是代码还是数据,地址信息使用段表管理
- 段页式管理:段页式管理机制结合了段式管理和页式管理的优点。把主存先分成若干段,每个段又分成若干页,其中段与段之间以及段的内部的页都是离散的
分页和分段的异同
- 共同点:
- 目的都是为了提高内存利用率,减少内存碎片
- 页和段都是离散存储的,每个页和段中的内存是连续的
- 区别:
- 页的大小是固定的,由操作系统决定;而段的大小不固定,取决于当前运行的程序
- 分页仅仅是为了满足操作系统内存管理的需求,而段是逻辑信息的单位,在程序中可以体现为代码段,数据段
逻辑(虚拟)地址和物理地址
直接将物理地址显示给开发者使用可能会带来一些问题,所以我们实际开发是与逻辑地址打交道,指针存储的值或者 Java 对内存中数据的引用使用的都是逻辑地址,而物理地址可以具体到内存地址寄存器中,是内存单元的真正地址,而地址转换就是对这两者互相映射的操作
为什么要有虚拟地址
程序直接访问和操作的都是物理内存时,可能会有意无意的破坏操作系统或程序,在同时运行两个或多个程序时,对同一内存地址进行读写,会产生程序错乱等问题,而使用虚拟内存不仅可以解决这些问题,还有如下优势
- 程序可以使用一系列相邻的虚拟地址来访问物理内存中不相邻的大内存缓冲区
- 程序可以使用一系列虚拟地址来访问大于可用物理内存的内存缓冲区。当物理内存的供应量变小时,内存管理器会将物理内存页(通常大小为 4KB)保存到磁盘文件。数据或代码页会根据需要在物理内存与磁盘之间移动 虚拟内存
- 不同进程使用的虚拟地址彼此隔离,一个进程中的代码无法更改正在由另一进程或操作系统使用的物理内存
CPU 寻址
CPU 中的内存管理单元 (MMU) 完成虚拟内存到实际物理内存之间的转换,只有通过 CPU 寻址,程序才能访问到真正的内存地址
介绍快表和多级页表
快表和多级页表的存在主要解决地址转换中的时间和空间问题,我们希望,虚拟地址到物理地址转换的过程要快、大量虚拟地址存储时空间尽量要小
快表: 可以将其看作页表的缓存,其中包括页表中的一部分,存储结构与页表大致相同。读写内存数据时 CPU 要访问两次主存,有了快表,有时只要访问一次高速缓冲存储器,一次主存
- 根据虚拟地址中的页号查快表
- 如果该页在快表中,直接从快表中读取相应的物理地址
- 如果该页不在快表中,就访问内存中的页表,再从页表中得到物理地址,同时将页表中的该映射表项添加到快表中
- 当快表填满后,又要登记新页时,就按照一定的淘汰策略淘汰掉快表中的一个页
多级页表: 为了避免把全部页表一直放在内存中占用过多空间,特别是那些根本就不需要的页表就不需要保留在内存中,将页表进行分级,可以采用将不常用的地址映射项放在二级页表中,二级页表可以不存放在内存,而是在使用的时候调入
什么是缓冲区溢出?有什么危害?其原因是什么?
缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上
危害有以下两点:
- 程序崩溃,导致拒绝服务
- 跳转并且执行一段恶意代码
造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入
什么是虚拟内存
常见的认知是开辟一块磁盘空间服务于内存,让用户产生内存变大的错觉,主要是因为其定义了一个连续的虚拟地址空间,并且把内存扩展到硬盘空间,这样程序在使用连续的虚拟地址时可以在内存不足的情况下利用内存与硬盘中的内容的切换完成“扩容”
局部性原理
不管是缓存还是虚拟内存,怎样确定哪些数据会被使用是一个重要的问题,而解决这个问题可以利用局部性原理进行实现
- 时间局部性:如果程序中的某条指令一旦执行,不久以后该指令可能再次执行,因为程序中有着大量的循环操作
- 空间局部性:一旦程序访问了某个存储单元,在不久之后,其附近的存储单元也将被访问,指令通常是顺序存放、顺序执行的,数据也一般是以向量、数组、表等形式簇聚存储的
虚拟存储的实现
主要有请求分页、请求分段、请求段页式
其都建立在相应的存储方式之上,且提供了请求调用和存储单位置换的功能,内存在程序开始运行时仅调用当前运行所需要的资源,按照其存储单位进行存储,如果在运行过程中产生需执行的指令或访问的数据尚未在内存(缺页中断)时,会将由处理器通知操作系统按照对应的页面置换算法将相应的资源按存储单位调入到主存,同时操作系统也可以将暂时不用的资源置换到外存中,这也就是为什么程序明明在内存中运行,固态硬盘的运行速度却要比机械磁盘快的原因
通过上述实现过程,可以看到需要一些前置条件
- 内外存有一定的容量确保实现的空间条件
- 缺页中断提供虚拟存储的触发条件
- 置换算法保证其高效实用
常见的置换算法
简单的内外存资源切换是不需要进行置换的,但在程序运行一段时间之后,难免会导致内存已满的情况,这时就需要考虑在从外存调入资源时将内存中的哪些资源腾出去(当然置换算法的使用不仅仅是在内存已满的情况下,这里只是提供一个场景)
- OPT(Optimal 最佳页面置换算法):所选择的被淘汰页面将是以后永不使用的,或者是在最长时间内不再被访问的页面,这样可以保证获得最低的缺页率。但由于人们目前无法预知进程在内存下的若千页面中哪个是未来最长时间内不再被访问的,因而该算法无法实现。一般作为衡量其他置换算法的方法
- FIFO(First In First Out 先进先出页面置换算法): 总是淘汰最先进入内存的页面,即选择在内存中驻留时间最久的页面进行淘汰
- LRU (Least Recently Used 最近最久未使用页面置换算法):赋予每个页面一个访问字段,用来记录这个页面自上次被访问以来所经历的时间 T,当须淘汰一个页面时,选择现有页面中其 T 值最大的,即最近最久未使用的页面予以淘汰
- LFU (Least Frequently Used 最少使用页面置换算法): 该置换算法选择在之前时期使用最少的页面作为淘汰页
参考文章:
https://www.zhihu.com/collection/665771028
https://snailclimb.gitee.io/javaguide/#/docs/cs-basics/operating-system/basis?id=_23-%e8%bf%9b%e7%a8%8b%e9%97%b4%e7%9a%84%e9%80%9a%e4%bf%a1%e6%96%b9%e5%bc%8f
https://www.cnblogs.com/gizing/p/10925286.html