操作系统高频面试题
写在前面
🔥我把后端Java面试题做了一个汇总,有兴趣大家可以看看!这里👉
⭐️在反复复习面试题时,我发现不同资料的解释五花八门,容易造成概念混淆。尤其是很多总结性的文章和视频,要么冗长难记,要么过于简略,导致关键知识点含糊不清。
⭐️为了系统梳理知识,我决定撰写一份面试指南,不只是简单汇总,而是融入个人理解,层层拆解复杂概念,构建完整的知识体系。我希望它不仅帮助自己更自信地应对面试,也能为同行提供清晰、实用的参考。
操作系统相关面试题
面试官: 进程和线程的区别(高频)
候选人: 线程和进程的区别主要体现在以下四个方面:
1️⃣ 资源分配: 进程是操作系统分配资源的基本单位,拥有独立的内存空间;线程共享进程资源,多个线程可访问相同的数据。
你说到进程是分配资源的基本单位,那么这个资源指的是什么? 虚拟内存、文件句柄、信号量等资源。
2️⃣ 创建与切换开销: 进程创建和切换成本较高,需要完整的资源分配和回收;线程切换更轻量,仅需保存和恢复私有数据。
3️⃣ 通信与同步: 进程间通信(IPC)依赖管道、消息队列等机制,效率较低;线程共享进程数据,通信更快,但需加锁避免竞争。
4️⃣ 容错与并发: 进程崩溃通常不影响其他进程,适用于高可靠性场景;线程共享进程资源,故单个线程异常可能导致整个进程崩溃。
面试官: 线程比进程高效的原因?(高频)
候选人: 线程相较于进程的优势主要体现在资源共享、创建成本、切换开销和并发效率四个方面。
- 资源共享:进程间通信(IPC)成本较高,而线程共享进程资源,数据访问更快,通信更高效。
- 创建成本:进程创建需分配独立内存,开销大;线程仅需分配栈,创建速度更快。
- 切换开销:进程切换涉及完整上下文切换,线程切换仅涉及寄存器和栈指针,开销更小。
- 并发执行:线程能充分利用多核 CPU 并行执行,提高程序吞吐量,而进程的资源隔离限制了并发效率。
面试官: 进程,线程,协程的区别是什么?(高频)
候选人:
•首先,我们来谈谈进程。进程是操作系统分配资源的基本单位,它拥有自己的独立内存空间和系统资源。每个进程都有独立的堆和栈,不与其他进程共享。进程间通信需要通过特定的机制,如管道、消息队列、信号量等。由于进程拥有独立的内存空间,因此其稳定性和安全性相对较高,但同时上下文切换的开销也较大,因为需要保存和恢复整个进程的状态。
•接下来是线程。线程是进程内的一个执行单元,也是CPU调度的基本单位。与进程不同,线程共享进程的内存空间,包括堆和方法区。线程之间通信更加高效,因为它们可以直接读写共享内存。线程的上下文切换开销较小,因为只需要保存和恢复线程的上下文,而不是整个进程的状态。然而,由于多个线程共享内存空间,因此存在线程安全的问题,需要通过同步和互斥机制来解决。
•最后是协程。协程是一种用户态的轻量级线程,其调度完全由用户程序控制,而不需要内核的参与。协程拥有自己的寄存器上下文和栈,但与其他协程共享堆内存。协程的切换开销非常小,因为只需要保存和恢复协程的上下文,而无需进行内核级的上下文切换。这使得协程在处理大量并发任务时具有非常高的效率。然而,协程需要程序员显式地进行调度和管理,相对于线程和进程来说,其编程模型更为复杂。
面试官: 用户态和内核态是如何切换的?(高频)
候选人:
在操作系统中,用户态用于运行普通应用程序,受限访问系统资源,而内核态具备最高权限,能够直接控制硬件。两者的切换主要通过以下三种方式:
- 系统调用:应用程序请求受限资源(如文件、网络)时,会触发系统调用,使 CPU 进入内核态,执行相应内核函数,完成后返回用户态。
- 异常处理:程序在用户态发生错误(如除零、非法内存访问)时,CPU 进入内核态执行异常处理程序,决定是否恢复或终止进程。
- 硬件中断:当外设完成任务(如磁盘 I/O、网络数据接收)时,触发中断,CPU 切换至内核态处理中断,随后恢复用户态继续执行程序。
面试官: 什么是孤儿进程?什么是僵尸进程?
候选人:
-
孤儿进程
是指父进程在子进程结束之前就已经退出,导致子进程失去了父进程的管理和控制,成为了 “孤儿”。此时,这些子进程会被系统的 init 进程(在 Linux 系统中,进程 ID 为 1)所收养,init 进程会负责回收它们的资源等工作。 -
僵尸进程
是指一个进程已经执行完了它的主要任务,进入了终止状态,但由于某些原因,它的父进程没有调用相应的系统函数(如 wait () 或 waitpid ())来收集它的退出状态信息,导致该进程虽然已经停止运行,但在系统进程表中仍然保留着一个记录,占据着一定的系统资源。Linux 下可以使用 Top 命令查找,
zombie
值表示僵尸进程的数量,为 0 则代表没有僵尸进程。下面这个命令可以定位僵尸进程以及该僵尸进程的父进程:
ps -A -ostat,ppid,pid,cmd |grep -e '^[Zz]'
面试官: 假设两个线程并发读写同一个整型变量,初始值为零,每个线程加 50 次,结果可能是什么?
候选人: 在没有任何同步机制的情况下,两个线程并发对同一个整型变量进行 50 次加 1 操作,最终结果可能是 100,也可能小于 100,最坏的结果是 50,也就是最终的结果可能是在 [50, 100] 。
小于 100 情况的分析,由于对整型变量的 num++
操作不是原子操作,它实际上包含了三个步骤:读取变量的值、将值加 1、将新值写回变量。在多线程环境下,可能会出现线程安全问题。例如,线程 1 和线程 2 同时读取了变量的当前值,然后各自将其加 1,最后都将相同的新值写回变量,这就导致了一次加 1 操作的丢失。这种情况会多次发生,最终结果就会小于 100。
追问: 如何保证2 个线程累加之后达到 100?
第一种方式:原子变量的方法。使用
AtomicInteger
替代普通int
,其incrementAndGet()
方法保证原子性,代码如下:public class AtomicIntegerAddition { private static AtomicInteger num = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { for (int i = 0; i < 50; i++) { num.incrementAndGet(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 50; i++) { num.incrementAndGet(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("最终结果: " + num.get()); } }
第二种方式:通过
synchronized
关键字或ReentrantLock
确保操作的互斥性,代码如下:public class SynchronizedAddition { private static int num = 0; private static final Object lock = new Object(); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { for (int i = 0; i < 50; i++) { synchronized (lock) { num++; } } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 50; i++) { synchronized (lock) { num++; } } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("最终结果: " + num); } }
面试官: 操作系统中进程的调度算法有哪些?
候选人:
-
先到先服务(FCFS)调度算法 : 按照进程到达的先后顺序进行调度,即最早到达的进程先执行,直到完成或阻塞
-
短作业优先(SJF)调度算法 : 优先选择运行时间最短的进程来运行
-
时间⽚轮转调度算法 : 将
CPU
时间划分为时间片(时间量),每个进程在一个时间片内运行,然后切换到下一个进程。
-
多级反馈队列调度算法 :将进程划分为多个队列,每个队列具有不同的优先级,进程在队列之间移动。具有更高优先级的队列的进程会更早执行,而长时间等待的进程会被提升到更高优先级队列。
-
最高优先级调度 : 为每个进程分配一个优先级,优先级较高的进程先执行。这可能导致低优先级进程长时间等待可能引发饥饿问题。
面试官: 进程间的通信方式
候选人: ⼤概有 7 种常⻅的进程间的通信⽅式。
-
管道/匿名管道(Pipes) :⽤于具有亲缘关系的⽗⼦进程间或者兄弟进程之间的通信。
-
有名管道 (Names Pipes) : 匿名管道由于没有名字,只能⽤于亲缘关系的进程间通信。为了克服这个缺点,提出了有名管道。有名管道严格遵循先进先出(first in first out),可以实现本机任意两个进程通信。
-
信号(Signal) :信号是⼀种⽐复杂的通信⽅式,⽤于通知接收进程某个事件已经发⽣;
-
消息队列(Message Queuing) :消息队列是消息的链表,具有特定的格式,存放在内存中并由消息队列标识符标识。管道和消息队列的通信数据都是先进先出的原则。与管道不同的是消息队列存放在内核中,只有在内核重启(即,操作系统重启)或者显示地删除⼀个消息队列时,该消息队列才会被真正的删除。消息队列可以实现消息的随机查询,消息不⼀定要以先进先出的次序读取,也可以按消息的类型读取,⽐ FIFO 更有优势。
-
信号量(Semaphores) :信号量是⼀个计数器,⽤于多进程对共享数据的访问,信号量的意图在于进程间同步。这种通信⽅式主要⽤于解决与同步相关的问题并避免竞争条件。
-
共享内存(Shared memory) :使得多个进程可以访问同⼀块内存空间,不同进程可以及时看到对⽅进程中对共享内存中数据的更新。这种⽅式需要依靠某种同步操作,如互斥锁和信号量等。可以说这是最有⽤的进程间通信⽅式。
-
套接字(Sockets): 此⽅法主要⽤于在客户端和服务器之间通过⽹络进⾏通信。套接字是⽀持 TCP/IP 的⽹络通信的基本操作单元,简单的说就是通信的两⽅的⼀种约定,⽤套接字中的相关函数来完成通信过程。
面试官: 线程间的同步的⽅式有哪些呢?
候选人:
线程同步是两个或多个共享关键资源的线程的并发执行。应该同步线程以避免关键的资源使用冲突。
下面是几种常见的线程同步的方式:
- 互斥锁(Mutex) :采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。比如 Java 中的
synchronized
关键词和各种Lock
都是这种机制。 - 读写锁(Read-Write Lock) :允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
- 信号量(Semaphore) :它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
- 屏障(Barrier) :屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。比如 Java 中的
CyclicBarrier
是这种机制。 - 事件(Event) :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
面试官: 操作系统的内存管理机制了解吗?内存管理有哪⼏种⽅式?
候选人:简单分为连续分配管理⽅式和⾮连续分配管理⽅式这两种。连续分配管理⽅式是指为⼀个⽤户程序分配⼀个连续的内存空间,常⻅的如 块式管理 。同样地,⾮连续分配管理⽅式允许⼀个程序使⽤的内存分布在离散或者说不相邻的内存中,常⻅的如⻚式管理 和 段式管理。
-
块式管理 : 远古时代的计算机操系统的内存管理⽅式。将内存分为⼏个固定⼤⼩的块,每个块中只包含⼀个进程。如果程序运⾏需要内存的话,操作系统就分配给它⼀块,如果程序运⾏只需要很⼩的空间的话,分配的这块内存很⼤⼀部分⼏乎被浪费了,产生一定的内存碎片。
-
⻚式管理 :把主存分为⼤⼩相等且固定的⼀⻚⼀⻚的形式,相对相⽐于块式管理的划分⼒度更⼤,提⾼了内存利⽤率,减少了碎⽚。⻚式管理通过
⻚表
对应逻辑地址和物理地址。 -
段式管理 : ⻚式管理虽然提⾼了内存利⽤率,但是⻚式管理其中的⻚实际并⽆任何实际意义。 段式管理把主存分为⼀段段的 。但是,最重要的是段是有实际意义的,每个段定义了⼀组逻辑信息,例如,有主程序段 MAIN、⼦程序段X、数据段 D 及栈段 S 等。 段式管理通过
段表
对应逻辑地址和物理地址。 -
段⻚式管理机制 。结合了上述两者的优点,先将内存划分为若干个段,每个段又被进一步划分为若干个页。段式管理解决了程序逻辑上不同部分的内存分配问题,而页式管理解决了物理内存的连续性问题。
面试官: 快表和多级⻚表
候选人:
1)快表
为了解决虚拟地址到物理地址的转换速度
,操作系统在 ⻚表⽅案 基础之上引⼊了 快表 来加速虚拟地址到物理地址的转换。我们可以把快表理解为⼀种特殊的⾼速缓冲存储器(Cache),其中的内容是⻚表的⼀部分或者全部内容。作为⻚表的 Cache,它的作⽤与⻚表相似,但是提⾼了访问速率。由于采⽤⻚表做地址转换,读写内存数据时 CPU 要访问两次主存。有了快表,有时只要访问⼀次⾼速缓冲存储器,⼀次主存,这样可加速查找并提⾼指令执⾏速度。
使⽤快表之后的地址转换流程是这样的:
根据虚拟地址中的⻚号查快表
如果该⻚在快表中,直接从快表中读取相应的物理地址;
如果该⻚不在快表中,就访问内存中的⻚表,再从⻚表中得到物理地址,同时将⻚表中的该映射表项添加到快表中;
当快表填满后,⼜要登记新⻚时,就按照⼀定的淘汰策略淘汰掉快表中的⼀个⻚。
2)多级⻚表
引⼊多级⻚表的主要⽬的是为了避免把全部⻚表⼀直放在内存中占⽤过多空间
,特别是那些根本就不需要的⻚表就不需要保留在内存中。多级⻚表属于时间换空间的典型场景。
面试官: 分⻚机制和分段机制有哪些共同点和区别呢?
候选人:
- 共同点 :
-
分⻚机制和分段机制都是为了提⾼内存利⽤率,少内存碎⽚。
-
⻚和段都是离散存储的,所以两者都是离散分配内存的⽅式。但是,每个⻚和段中的内存是连续的。
- 区别 :
-
⻚的⼤⼩是固定的,由操作系统决定;⽽段的⼤⼩不固定,取决于我们当前运⾏的程序。
-
分页对用户不可见,分段对用户可见
-
分页的地址空间是一维的,分段的地址空间是二维的
面试官: 逻辑(虚拟)地址和物理地址
候选人: 我们编程⼀般只有可能和逻辑地址打交道,⽐如在 C 语⾔中,指针⾥⾯存储的数值就可以理解成为内存⾥的⼀个地址,这个地址也就是我们说的逻辑地址,逻辑地址由操作系统决定。物理地址指的是真实物理内存中地址,更具体⼀点来说就是内存地址寄存器中的地址。物理地址是内存单元真正的地址。
为什么要有虚拟地址空间呢?
没有虚拟地址空间的时候,程序都是直接访问和操作的都是物理内存 。但是这样有什么问题呢?
⽤户程序可以访问任意内存,寻址内存的每个字节,这样就很容易(有意或者⽆意)破坏操作系统,造成操作系统崩溃。
想要同时运⾏多个程序特别困难,⽐如你想同时运⾏⼀个微信和⼀个 QQ ⾳乐都不⾏。为什么呢?举个简单的例⼦:微信在运⾏的时候给内存地址 1xxx 赋值后,QQ ⾳乐也同样给内存地址 1xxx 赋值,那么 QQ ⾳乐对内存的赋值就会覆盖微信之前所赋的值,这就造成了微信这个程序就会崩溃
总结来说:如果直接把物理地址暴露出来的话会带来严重问题,⽐如可能对操作系统造成伤害以及给同时运⾏多个程序造成困难。
面试官: 什么是虚拟内存(Virtual Memory)?
候选人: 虚拟内存(Virtual Memory) 是计算机系统内存管理非常重要的一个技术,本质上来说它只是逻辑存在的,是一个假想出来的内存空间,主要作用是作为进程访问主存(物理内存)的桥梁并简化内存管理。
总结来说,虚拟内存主要提供了下面这些能力:
- 进程隔离:每个进程拥有独立的虚拟地址空间,避免进程间干扰,提高系统稳定性。
- 提升内存利用率:仅加载当前需要的部分数据或代码,减少物理内存占用。
- 简化内存管理:程序员无需直接操作物理内存,而是通过虚拟地址空间访问,提高开发效率。
- 共享物理内存:多个进程可共享公共库,避免重复加载,节省内存资源。
- 增强安全性:控制进程对物理内存的访问权限,防止非法访问。
- 扩展可用内存:利用磁盘作为扩展,提供比物理内存更大的可用空间(但可能降低访问速度)。
面试官: 说⼀下⻚⾯置换算法的作⽤?常⻅的⻚⾯置换算法有哪些?
候选人: 地址映射过程中,若在⻚⾯中发现所要访问的⻚⾯不在内存中,则发⽣缺⻚中断 。
缺⻚中断 就是要访问的⻚不在主存,需要操作系统将其调⼊主存后再进⾏访问。 在这个时候,被内存映射的⽂件实际上成了⼀个分⻚交换⽂件。
当发⽣缺⻚中断时,如果当前内存中并没有空闲的⻚⾯,操作系统就必须在内存选择⼀个⻚⾯将其移出内存,以便为即将调⼊的⻚⾯让出空间。⽤来选择淘汰哪⼀⻚的规则叫做⻚⾯置换算法,我们可以把⻚⾯置换算法看成是淘汰⻚⾯的规则。
-
OPT ⻚⾯置换算法(最佳⻚⾯置换算法) :最佳(OPT)置换算法所选择的被淘汰⻚⾯将是以后永不使⽤的,或者是在最⻓时间内不再被访问的⻚⾯,这样可以保证获得最低的缺⻚率。但由于⼈们⽬前⽆法预知进程在内存下的若千⻚⾯中哪个是未来最⻓时间内不再被访问的,因⽽该算法⽆法实现。⼀般作为衡量其他置换算法的⽅法。
-
FIFO(First In First Out)⻚⾯置换算法(先进先出⻚⾯置换算法) : 总是淘汰最先进⼊内存的⻚⾯,即选择在内存中驻留时间最久的⻚⾯进⾏淘汰。
-
LRU(Least Currently Used)⻚⾯置换算法(最近最久未使⽤⻚⾯置换算法) :LRU算法赋予每个⻚⾯⼀个访问字段,⽤来记录⼀个⻚⾯⾃上次被访问以来所经历的时间 T,当须淘汰⼀个⻚⾯时,选择现有⻚⾯中其 T 值最⼤的,即最近最久未使⽤的⻚⾯予以淘汰。
-
LFU(Least Frequently Used)⻚⾯置换算法(最少使⽤⻚⾯置换算法) : 该置换算法选择在之前时期使⽤最少的⻚⾯作为淘汰⻚。
面试官: 解释一下进程同步和互斥,以及解决这些问题的办法
候选人:
进程同步
是指多个并发执行的进程之间协调和管理它们的执行顺序,以确保它们按照一定的顺序或时间间隔执行。
进程互斥
指的是在某一时刻只允许一个进程访问某个共享资源。当一个进程正在使用共享资源时,其他进程不能同时访问该资源。
解决进程同步和互斥的问题有很多种方法,其中一种常见的方法是使用信号量和 PV 操作。信号量是一种特殊的变量,它表示系统中某种资源的数量或者状态。PV 操作是一种对信号量进行增加或者减少的操作,它们可以用来控制进程之间的同步或者互斥。
除此之外,下面的方法也可以解决进程同步和互斥问题:
- 临界区(Critical Section): 将可能引发互斥问题的代码段称为临界区。为了实现互斥,每个进程在进入临界区前必须获取一个锁,退出临界区后释放该锁。这确保同一时间只有一个进程可以进入临界区。
- 互斥锁(Mutex): 互斥锁是一种同步机制,用于实现互斥。每个共享资源都关联一个互斥锁进程在访问该资源前需要先获取互斥锁,使用完后释放锁。只有获得锁的进程才能访问共享资源。
面试官: 介绍一下几种典型的锁
候选人:
两个基础的锁:
- 互斥锁:互斥锁是一种最常见的锁类型,用于实现互斥访问共享资源。在任何时刻,只有一个线程可以持有互斥锁,其他线程必须等待直到锁被释放。这确保了同一时间只有一个线程能够访问被保护的资源。
- 自旋锁:自旋锁是一种基于忙等待的锁,即线程在尝试获取锁时会不断轮询,直到锁被释放。
其他的锁都是基于这两个锁的:
- 读写锁:允许多个线程同时读共享资源,只允许一个线程进行写操作。分为读(共享)和写(排他)两种状态。
- 悲观锁:认为多线程同时修改共享资源的概率比较高,所以访问共享资源时候要上锁
- 乐观锁:先不管,修改了共享资源再说,如果出现同时修改的情况,再放弃本次操作。.
面试官:死锁产生的条件是什么?(高频)
候选人:一个线程需要同时获取多把锁,这时就容易发生死锁。
死锁必须具备以下四个条件:
-
互斥条件: 该资源任意⼀个时刻只由⼀个线程占⽤。
-
请求与保持条件: ⼀个进程因请求资源⽽阻塞时,对已获得的资源保持不放。
-
不剥夺条件: 线程已获得的资源在末使⽤完之前不能被其他线程强⾏剥夺,只有⾃⼰使⽤完毕后才释放资源。
-
循环等待条件: 若⼲进程之间形成⼀种头尾相接的循环等待资源关系。
面试官:如何避免线程死锁?(高频)
候选人: 我上⾯说了产⽣死锁的四个必要条件,为了避免死锁,我们只要破坏产⽣死锁的四个条件中的其中⼀个就可以了。现在我们来挨个分析⼀下:
-
破坏互斥条件 :这个条件我们没有办法破坏,因为我们⽤锁本来就是想让他们互斥的(临界资源需要互斥访问)。
-
破坏请求与保持条件 :⼀次性申请所有的资源。
-
破坏不剥夺条件 :占⽤部分资源的线程进⼀步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
-
破坏循环等待条件 :靠按序申请资源来预防。按某⼀顺序申请资源,释放资源则反序释放。破坏循环等待条件。
那什么是资源有序分配法呢? 线程A和线程 B获取资源的顺序要一样,当线程A是先尝试获取资源 A, 然后尝试获取资源 B 的时候,线程B同样也是先尝试获取资源 A,然后尝试获取资源 B。也就是说,线程 A和线程B总是以相同的顺序申请自己想要的资源。
面试官:如何进行死锁诊断?(高频)
候选人:这个也很容易,我们只需要通过jdk自动的工具就能搞定。
我们可以先通过jps
来查看当前java程序运行的进程id,然后通过jstack
来查看这个进程id,就能展示出来死锁的问题,并且,可以定位代码的具体行号范围,我们再去找到对应的代码进行排查就行了。
拓展:
jps
:输出JVM中运行的进程状态信息。
jstack
:查看java进程内线程的堆栈信息。