操作系统八股文
第 1 章 操作系统概述
第 2 章 进程管理
线程与进程的区别⭐
- 定义
- 进程是操作系统资源分配的基本单位,是一个独立运行的程序实例。每个进程有独立的内存空间、全局变量和系统资源。
- 线程是进程中的一个执行单元,是CPU调度的基本单位。多个线程共享同一个进程的内存和资源。
- 调度。线程是独立调度的基本单位,进程是拥有资源的基本单位。在同一进程中,线程的切换不会引起进程切换。在不同进程中进行线程切换,如从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。
- 拥有资源。进程都是拥有资源的基本单位,而线程不拥有系统资源,但线程可以访问其所属进程的系统资源。
- 并发性。在引入线程的操作系统中,不仅进程之间可以并发执行,而且多个线程之间也可以并发执行。
- 系统开销。进程创建、撤销和切换,所付出的开销远大于线程。
- 地址空间。进程的地址空间之间互相独立,同一进程的各线程间共享进程的地址空间。
- 通信方面。进程间需要用PV操作、共享存储、消息传递和管道通信等方式进行通信,而线程间可以直接读/写进程数据段(如全局变量)来进行通信。
进程、线程和协程的区别⭐
1. 定义
- 进程:操作系统资源分配的基本单位,是一个独立运行的程序实例。每个进程有独立的内存空间、全局变量和系统资源。
- 线程:是进程中的一个执行单元,是CPU调度的基本单位。多个线程共享同一个进程的内存和资源。
- 协程:是一种用户态的轻量级线程,由程序自身控制调度。协程可以在一个线程内实现多任务切换,而不依赖操作系统内核。
2. 资源分配
- 进程:每个进程拥有独立的地址空间,系统为其分配资源(如内存、文件句柄)。进程之间资源完全隔离。
- 线程:同一进程中的线程共享进程的地址空间和资源(如全局变量、文件描述符等)。
- 协程:协程是基于线程或进程的用户态执行单元,不直接由操作系统分配资源,仅共享其所在线程/进程的资源。
3. 调度方式
- 进程:进程的调度是由操作系统的内核完成的。操作系统根据一定的调度算法(如先来先服务、时间片轮转、优先级调度等)来分配 CPU 时间给不同的进程。
- 线程:线程的调度同样主要由操作系统内核来完成。在内核的线程调度器的管理下,根据线程的优先级、等待状态等因素来分配 CPU 时间。
- 协程:由程序自身调度(用户态切换),无需操作系统介入,调度开销最小。协程通过主动让出控制权(如
yield
或await
)实现切换。
4. 上下文切换开销
- 进程:进程切换需要保存和恢复独立的内存空间和资源,开销最大。
- 线程:线程切换比进程轻量,但需要保存和恢复线程的寄存器状态,仍需内核态支持,开销中等。
- 协程:协程切换发生在用户态,不涉及内核调度,仅保存与恢复寄存器、堆栈等状态,开销最小。
5. 通信方式
- 进程:进程之间是完全独立的,每个进程拥有自己的内存空间。需要通过操作系统内核来实现通信,开销较大,但安全性高。常见的进程通信方式有共享内存、消息队列和管道等。
- 线程:线程共享同一进程的内存和资源,通信比进程间简单。由于共享资源,容易产生竞争,需借助锁和信号量等同步机制。
- 协程:协程之间共享线程的资源,因此通信更高效,不涉及操作系统内核。通常使用队列、Channel、事件等方式进行通信。
6. 并发与并行
- 进程:可实现真正的并行,多个进程可以运行在不同的CPU核心上。
- 线程:可实现并行(在多核CPU上),但因为共享内存资源,容易出现竞争问题。
- 协程:协程本质上是并发(通过时间片或事件循环模拟),不一定是真正的并行。需要配合多线程或多进程来利用多核CPU。
7. 使用场景
- 进程:适用于需要高隔离性的任务(如多用户服务、分布式系统)。如:Web服务器中每个请求由一个独立的进程处理。
- 线程:适用于需要高效并行处理的任务(如图像处理、并行计算)。如:多线程下载、并行文件处理。
- 协程:适用于高并发但对CPU密集度要求低的任务(如I/O密集型任务)。如:网络爬虫、异步Web服务器(如Python的
asyncio
)。
僵尸进程和孤儿进程⭐
1. 僵尸进程(Zombie Process)
定义:
僵尸进程是指已经终止运行,但其退出状态尚未被父进程回收的进程。这类进程的进程描述符(PID)仍然保留在系统进程表中,但不再占用内存或CPU资源。
产生原因:
当子进程终止时,会向父进程发送 SIGCHLD
信号,并保留其退出状态(如返回值)。父进程需要通过 wait()
或 waitpid()
系统调用获取该状态。若父进程未调用这些函数,子进程的退出信息会一直保留,导致其成为僵尸进程。
- 潜在风险:大量僵尸进程可能耗尽 PID,导致无法创建新进程。
- 解决方法: 父进程主动调用
wait()
或waitpid()
:回收子进程退出状态。
2. 孤儿进程(Orphan Process)
定义:
孤儿进程是指父进程已终止,但子进程仍在运行的进程。此时,孤儿进程会被 init 进程(PID 为 1)收养,并由 init 负责在其终止时回收资源。
产生原因:
父进程可能因崩溃、错误或主动退出而终止,导致子进程失去父进程。
特点:
- 被 init 进程接管:PPID(父进程ID)变为 1,init 进程会正确回收孤儿进程的资源。
- 常见场景:后台守护进程(daemon)常有意成为孤儿进程。
解决方法:
通常无需手动处理,init 进程会自动回收孤儿进程。但在编程中,可通过以下方式避免意外孤儿进程:
- 父进程等待子进程结束:使用
wait()
或waitpid()
。 - 使用双 fork 技术:创建守护进程时,通过两次
fork()
确保子进程脱离原会话。
进程间通信(IPC)的方式⭐
进程间通信(Inter-Process Communication,IPC)是指在不同进程之间传播或交换信息。
- 管道(Pipe)
- 匿名管道:是一种半双工的通信方式,数据只能单向流动,通常用于具有亲缘关系的进程之间,如父子进程。它在内存中开辟一块缓冲区实现通信,由系统自动分配和管理,通信效率相对较低。
- 命名管道:可以在不相关的进程之间进行通信,提供了一个路径名与之关联,以FIFO(First In First Out)的方式进行数据传输,有自己的文件系统节点,可以通过文件系统的接口进行访问。
-
信号(Signal)
信号是一种比较简单的进程间通信机制,用于通知进程发生了某种特定事件,如进程终止、键盘中断等。信号可以由系统、其他进程或进程自身发送,进程接收到信号后,会根据信号的类型和进程的设置执行相应的操作,如执行默认动作、忽略信号或执行自定义的信号处理函数。 -
消息队列(Message Queue)
消息队列是一种以消息为单位进行数据传输的通信方式,进程可以向消息队列中发送消息,也可以从消息队列中读取消息。消息队列提供了一种异步通信的机制,发送方和接收方不需要同时运行,消息会在队列中等待被接收。消息队列具有一定的存储容量,不同的消息可以根据类型或优先级进行区分。 -
共享内存(Shared Memory)
共享内存允许不同进程访问同一块物理内存空间,是一种高效的进程间通信方式。多个进程可以直接读写共享内存中的数据,无需进行数据复制,从而提高了通信效率。但是,由于多个进程可能同时访问共享内存,因此需要使用信号量等同步机制来保证数据的一致性和完整性。 -
信号量(Semaphore)
信号量主要用于实现进程之间的同步和互斥,它本质上是一个计数器,用于控制多个进程对共享资源的访问。信号量的值表示当前可用的共享资源数量,当进程需要访问共享资源时,会先检查信号量的值,如果大于0,则可以访问,并将信号量的值减1;如果等于0,则进程需要等待,直到信号量的值大于0。 -
套接字(Socket)
套接字是一种网络通信机制,也可以用于本地进程间通信。它提供了一种通用的、面向连接或无连接的通信方式,允许不同主机或同一主机上的不同进程之间进行数据传输。 -
文件(File)
同进程通过对同一文件进行读写操作来实现信息传递。一个进程将数据写入文件,其他进程从文件中读取数据,以此达到进程间通信的目的。 -
内存映射(Memory-Mapped File)
将文件的内容映射到进程的虚拟地址空间中,使得进程可以像访问内存一样直接访问文件内容,而不需要通过传统的文件 I/O 系统调用。这样多个进程可以通过映射同一个文件到各自的地址空间,实现对同一块数据区域的共享访问,从而达到通信的目的。 -
RPC(Remote Procedure Call,远程过程调用)
它是一种通信协议,允许程序调用另一个地址空间的过程或函数,而不需要了解底层的网络细节和远程系统的具体实现。它将网络通信和远程调用进行了封装,使得开发者可以像调用本地函数一样调用远程服务,实现了进程间的跨网络通信。
线程间同步方式(线程安全的实现方式)⭐⭐
线程安全是指在多线程环境下,程序能够正确地执行,不会出现数据竞争、不一致等问题。
线程间的同步是确保多个线程在访问共享资源时能够正确协作、避免数据竞争和状态不一致的关键机制。
1. 互斥锁(Mutex)
- 原理:通过锁的获取(Lock)与释放(Unlock)实现独占访问共享资源。同一时刻只有一个线程能持有锁。当一个线程获得互斥锁后,其他线程如果试图获取该锁,就会被阻塞,直到持有锁的线程释放锁。
2. 读写锁(Read-Write Lock)
- 原理:读写锁允许多个线程同时进行读操作,但在写操作时会独占资源。当有线程进行写操作时,其他线程的读和写操作都会被阻塞;当只有读操作时,多个线程可以同时进行。这种锁适用于读多写少的场景。
3. 信号量(Semaphore)
- 原理:信号量维护一个计数器,表示可用资源数量。它有两个基本操作:
P
操作(等待)和V
操作(释放)。P
操作会将信号量的值减 1,如果信号量的值小于 0,则线程会被阻塞;V
操作会将信号量的值加 1,如果有线程在等待,则唤醒其中一个线程。
4. 条件变量(Condition Variables)
- 允许线程在特定条件下等待或唤醒其他线程,需与互斥锁配合使用。
5. 原子操作(Atomic Operations)
- 原理:原子操作通过硬件指令保证单条操作的不可分割性,在多线程环境下不会被其他线程中断。
6. 无锁编程(Lock-Free)
- 原理:通过CAS(Compare-And-Swap)等原子指令实现同步,无需传统锁。
- 无锁数据结构是通过无锁编程实现的,不使用传统的锁机制,而是通过原子操作和内存屏障来实现线程安全的数据结构。
- 面试题:使用C++实现无锁队列、无锁栈。
7. 屏障(Barrier)
- 原理:多个线程在屏障处等待,直到所有线程到达后才继续执行。
8. 自旋锁(Spinlock)
- 原理:线程在获取锁时循环检查锁状态(忙等待),而非阻塞休眠。
- 使用场景:锁持有时间极短且线程不想让出 CPU(如内核中断处理)。
锁的使用场景⭐
- 共享数据的并发读写
当多个线程/进程需要访问同一块内存或数据结构时,需通过锁保证操作的原子性。 - 线程间协作与条件同步
当线程需要等待特定条件满足后再继续执行时,结合锁和条件变量实现同步,如:生产者-消费者队列。 - 资源池管理
管理有限资源(如数据库连接池、线程池)时,需通过锁分配资源。 - 单例模式初始化
确保全局单例对象在多线程环境下只初始化一次。 - 数据库事务隔离
在数据库操作中,通过锁实现事务隔离级别(如行锁、表锁)。 - 文件或硬件设备访问
多个进程/线程操作同一文件或硬件设备时,需通过文件锁协调访问。 - GUI主线程更新
在图形界面编程中,UI控件通常只能在主线程修改,需通过锁同步后台线程的更新请求。 - 分布式系统协调
在分布式系统中,通过分布式锁(如Redis、ZooKeeper)协调多个节点对共享资源的访问。
介绍死锁,如何解除⭐
1. 死锁的定义
死锁(Deadlock) 是多个进程(或线程)因竞争资源而陷入互相等待的状态,若无外力干预,这些进程将永久阻塞。
2. 死锁的四个必要条件
死锁发生需同时满足以下条件:
- 互斥(Mutual Exclusion):资源同一时间只能被一个进程占用。
- 请求并保持(Hold and Wait):进程持有资源的同时请求其他资源。
- 不可剥夺(No Preemption):资源只能由持有者主动释放,不可强制剥夺。
- 循环等待(Circular Wait):多个进程形成资源请求的环形依赖链。
3. 解除死锁的通用方法
(1) 死锁预防
通过破坏死锁的四个必要条件之一,避免死锁发生:
- 破坏互斥:允许资源共享(如只读文件),但多数资源无法共享。
- 破坏占有且等待:进程必须一次性申请所有所需资源(资源利用率低)。
- 破坏不可抢占:强制剥夺资源(需设计资源恢复机制,复杂度高)。
- 破坏循环等待:按固定顺序申请资源(如统一资源编号,按升序请求)。
(2) 死锁避免
在资源分配时动态判断是否安全,常用算法:
- 银行家算法(Banker’s Algorithm):模拟分配资源后系统是否处于安全状态(所有进程能按顺序完成)。若不安全,则拒绝分配请求。
(3) 死锁检测与恢复
允许死锁发生,但定期检测并解除:
- 检测方法:构建资源分配图,检测是否存在环路。
- 恢复方法:
- 终止进程:强制终止一个或多个进程(如优先级最低或剩余时间最长的进程)。
- 资源剥夺:从某些进程中剥夺资源分配给其他进程(需处理进程回滚)。
- 进程回退法:让一个或多个死锁进程回退到足以避免死锁的地步。
第 3 章 内存管理
第 4 章 文件管理
第 5 章 I/O管理
第 6 章 网络系统
1. 零拷贝技术
2. I/O 多路复用:select/poll/epoll
3. 高性能网络模式:Reactor 和 Proactor
4. 一致性哈希
1.背景
在分布式系统中,服务器节点的动态增减是常见需求。传统哈希算法(如取模哈希 key % N
,N为节点数)存在两个核心问题:
- 节点变动时数据迁移量大:当节点数N变化时,几乎所有键的映射关系都会改变,导致大量数据重新分布(如缓存失效)。
- 负载不均衡:节点直接按哈希值分配时,若数据分布不均,易出现“热点节点”。
一致性哈希的目标:在节点动态变化时,尽可能减少数据迁移,并实现负载均衡。
```接下来阅读:https://developer.aliyun.com/article/1082388``