操作系统八股

对操作系统有一个更加全面的认识,操作系统八股文专项突破!!!


什么是操作系统(Operating System,OS)

操作系统是计算机系统中管理硬件和软件资源的中间层系统,位于用户程序与计算机硬件之间,起到桥梁的作用。

  • 作用
    • 屏蔽硬件细节,简化用户操作
    • 管理资源(CPU、内存、设备、文件等)
    • 为用户和程序提供统一、简洁、抽象的接口
  • 常见操作系统举例
    • Windows
    • Linux
    • macOS
    • Android、iOS(移动设备)

操作系统的主要功能

操作系统有以下几个核心功能模块,每一项都至关重要:

1. 进程管理

  • 进程是程序的运行实例,是资源分配的基本单位。
  • 操作系统负责:
    • 创建与终止进程
    • 分配和回收进程所需资源(如CPU时间、内存)
    • 进行进程调度(决定哪个进程先运行)
    • 维护进程状态与进程控制块(PCB)

2. 内存管理

  • 任务:
    • 分配内存给正在运行的程序
    • 跟踪内存使用情况,防止越界访问
    • 在进程终止时回收所占内存
  • 支持虚拟内存技术,提供比实际内存更大的地址空间

3. 文件管理

  • 文件是操作系统中用于存储数据的抽象形式
  • 操作系统提供:
    • 创建、删除、读写文件的接口
    • 文件目录结构管理(如树状目录)
    • 权限管理(谁可以访问什么文件)
    • 持久化存储支持(通常在磁盘上)

4. 设备管理

  • 控制和协调硬件设备的使用
  • 通过设备驱动程序(Driver)来实现对设备的操作
  • 管理内容包括:
    • 输入设备:键盘、鼠标、扫描仪等
    • 输出设备:显示器、打印机、音箱等
    • 存储设备:硬盘、U盘、光盘等
  • 任务包括缓冲、排队、错误处理等

5. 用户接口管理(可选扩展)

  • 为用户提供操作计算机的接口,包括:
    • 命令行界面(CLI):如 Linux shell
    • 图形用户界面(GUI):如 Windows 桌面

什么是内核(Kernel)

📌 定义:

内核是操作系统中最核心的部分,它是一个控制计算机硬件和软件资源的程序,负责操作系统的基本功能。

📌 特点:

  • 操作系统的心脏,管理所有系统资源
  • 运行在内核态(Kernel Mode),拥有最高权限
  • 用户程序不能直接操作内核,必须通过系统调用间接请求服务

📌 内核的核心功能包括:

  • 进程调度与管理
  • 内存管理
  • 文件系统操作
  • 设备驱动程序接口
  • 系统调用接口

什么是用户态(User Mode)和内核态(Kernel Mode)

📌 用户态与内核态的基本区别:

属性 用户态(User Mode) 内核态(Kernel Mode)
权限等级
可访问范围 只能访问用户空间 可以访问内核空间和所有硬件资源
应用程序行为 执行普通计算,如变量运算、函数调用等 执行操作系统代码,如调度、内存分配等
稳定性 出错仅影响当前进程 出错可能导致整个系统崩溃
示例 用户运行的应用程序(如浏览器、编辑器等) 系统调用、驱动程序执行过程

📌 为什么需要划分用户态和内核态?

  • 系统安全性:防止普通程序随意操作内核或硬件
  • 系统稳定性:用户程序崩溃不会影响整个操作系统
  • 资源隔离与保护:防止非法访问其他进程或系统资源

📌 状态切换:从用户态到内核态

  1. 系统调用(System Call) 当用户程序需要操作硬件或请求操作系统服务(如读文件、申请内存),必须通过系统调用切换到内核态。
  2. 中断/异常处理 例如键盘输入、定时器中断、除零异常,也会使 CPU 进入内核态处理。
  3. 完成任务后,系统返回用户态,继续执行用户程序。

📝 举例说明:

1
2
3
// C语言中read()函数
char buffer[100];
read(0, buffer, 100); // 用户态执行,调用read()进入内核态

📌 图示辅助(文字版):

1
2
3
4
5
6
7
+--------------------+           +---------------------+
| 用户空间(User Space)| <-----+-----> 系统调用接口 |
+--------------------+ +---------------------+
↓ ↓
+--------------------+ +---------------------+
| 内核空间(Kernel) | <-----+-----> 内核态处理功能 |
+--------------------+ +---------------------+

用户态与内核态是如何切换的?

📌 一、切换的基本概念

在现代操作系统中,为了保护系统资源确保稳定性与安全性,用户程序不能直接操作硬件资源或内核数据结构,必须通过系统调用(System Call)进入内核态。

✅ 切换方式本质上是:触发一次特权级转换,由 CPU 控制权限从用户模式(Ring 3)切换到内核模式(Ring 0)。


📌 二、切换的触发条件

用户态切换到内核态,通常有以下几种情况:

触发条件 描述说明
系统调用 用户程序请求 OS 服务,如 read()write()
中断 外部设备(如键盘、鼠标)产生中断
异常 程序运行出错,如除以零、非法内存访问等

📌 三、系统调用过程(详细步骤)

以 Linux 为例,用户程序调用 read() 函数读取文件的过程:

➤ 步骤 1:用户程序调用库函数(如 C 的 read()

  • 位于用户空间,调用的是标准 C 库中的封装函数。

➤ 步骤 2:发起系统调用指令(如 x86 架构中的 int 0x80syscall

  • CPU 进入内核态(特权级提升),切换到内核栈,跳转到内核空间执行。

➤ 步骤 3:操作系统内核根据系统调用号查找服务例程(如 sys_read

  • 执行内核代码,访问文件系统、硬件设备、内存等。

➤ 步骤 4:内核执行完毕后,通过 ret_from_syscall 等机制返回

  • CPU 恢复原来的用户态上下文,返回结果给用户程序。

📌 四、从内核态返回用户态

  • 当系统调用完成,或中断处理完成后:
    • 内核将结果保存到用户进程可访问的位置;
    • 恢复原来的用户态寄存器值、程序计数器等;
    • CPU 将控制权交还用户程序,回到用户态继续执行。

📌 五、图示流程(文字版)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[用户态]
|
| 1. 调用系统调用,如 read()

[切换到内核态] —— 通过 syscall 指令触发

[内核态]
| 2. 操作系统查找对应系统调用(如 sys_read)
| 3. 执行相应服务(访问文件、分配资源)

[切换回用户态]
| 4. 返回调用者,并传回结果

[用户态继续执行]

📌 六、常见系统调用示例(Linux)

功能分类 系统调用
文件操作 open(), read(), write(), close()
进程控制 fork(), exec(), exit(), wait()
内存管理 brk(), mmap(), munmap()
设备管理 ioctl(), read(), write()

并发(Concurrency)与并行(Parallelism)

📌 并发(Concurrency)

定义:

  • 并发是指在一段时间内多个任务“交替”进行。
  • 在单核 CPU 上,通过时间片轮转快速切换任务,实现“多个任务同时执行”的假象。

关键点:

  • 同一时间点,只能执行一个任务
  • 多个任务“你上完我上”,表现为“同时进行”,但实际是轮流执行
  • 实现方式:上下文切换 + 时间片分配

举例:

你一个人炒两个菜,一会炒A菜,一会炒B菜,交替操作,虽然只有一个锅,但你操作得快,看起来像在“同时炒”。


📌 并行(Parallelism)

定义:

  • 并行是指在同一时刻多个任务真正地同时进行。
  • 必须依赖多核/多CPU 资源,每个核/处理器负责一个任务。

关键点:

  • 同一时间点,多个任务同时执行
  • 物理上的并行执行,不依赖时间片切换。

举例:

你和朋友各炒一个菜,用两个锅、两个灶台,同一时刻炒两个菜,这是真正的“同时炒”。


📌 对比总结:

特性 并发(Concurrency) 并行(Parallelism)
是否同时运行 看起来是,实质轮流运行(单核) 真正地同时运行(多核)
处理器需求 单核即可 多核或多 CPU
关注点 如何调度多个任务 如何提高多任务执行效率
示例 单人轮流做两件事 两人同时做两件事

什么是进程上下文切换(Process Context Switch)

📌 定义:

进程上下文切换是指操作系统在多任务环境下,将 CPU 从一个进程切换到另一个进程的过程。

  • 由于 CPU 一次只能处理一个进程(单核),因此需要定时切换,让多个进程“共享” CPU。
  • 每次切换时,OS 都要保存和恢复进程的状态信息,这一过程叫做“上下文切换”。

📌 上下文(Context)是什么?

“上下文”指的是进程在执行过程中的所有状态信息,包括:

  • 程序计数器(PC)
  • CPU 寄存器
  • 堆栈指针
  • 内存映射信息
  • 文件描述符等

📌 上下文切换的步骤(详细)

  1. 保存当前进程的上下文
    • 操作系统记录当前进程的寄存器值、程序计数器、内存状态等信息,并存入 PCB(进程控制块)中。
  2. 选择下一个要运行的进程
    • 调度器根据调度算法(如时间片轮转、优先级等)选出下一个进程。
  3. 恢复新进程的上下文
    • 将下一个进程的 PCB 中保存的上下文信息加载到 CPU 中。
  4. 切换完成,开始执行新进程

📌 上下文切换的代价

  • 开销较大:涉及寄存器存取、缓存失效(cache miss)、TLB刷新等
  • 频繁切换会降低效率
  • 为此,操作系统需要合理调度、控制切换频率

📌 举例说明

假设有两个进程 A 和 B 在执行:

1
2
3
4
5
6
7
8
时间片 1:
→ CPU 执行进程 A
→ 到时间片末,触发中断
→ 保存进程 A 的状态(上下文)

时间片 2:
→ 加载进程 B 的上下文
→ CPU 执行进程 B

📌 图示流程(文字版)

1
2
3
4
5
6
7
8
9
[进程 A 运行中]
↓ 中断/调度
[保存 A 的上下文 → PCB]

[选择进程 B]

[加载 B 的上下文 ← PCB]

[CPU 运行进程 B]

进程的状态(Process States)

一个进程在运行过程中,通常会经历以下几种状态:

📌 主要的三种状态

状态名 英文名 含义说明
运行(运行中) Running 正在使用 CPU 执行指令,系统中同时最多只有一个进程处于运行状态(在单核 CPU 情况下)
就绪 Ready 进程已准备好运行,但因 CPU 被占用而暂时不能执行,等待调度
阻塞(等待) Blocked / Waiting 进程因等待某事件(如 I/O 完成)而暂停执行,即使此时有 CPU 可用也无法执行

📌 其他两种补充状态(形成五状态模型)

状态名 英文名 含义说明
创建 New 进程正在被系统创建,还未进入就绪队列
结束 Exit / Terminated 进程完成或被终止,系统正在清理它占用的资源

📌 状态转换图

img

僵尸进程(Zombie Process)

📌 定义

  • 僵尸进程是已经终止但其进程描述信息(如 PID、状态码)仍保存在进程表中的进程。
  • 通常发生在子进程结束但父进程未回收子进程资源的情况下。

📌 产生原因

  • 每个子进程终止时,会向其父进程发送 SIGCHLD 信号,并留下一个退出状态。
  • 父进程应该通过 wait()waitpid() 系统调用回收子进程资源(即读取退出状态)。
  • 如果父进程没有处理这个信号,子进程的资源(PCB)就不会释放,从而形成僵尸进程。

📌 僵尸进程的危害

  • 僵尸进程不会继续占用 CPU 和内存,但会占用进程表项(进程号 PID)。
  • 如果大量僵尸进程长期存在,会导致系统不能创建新进程(进程表项耗尽)。

📌 如何避免或清理僵尸进程

  1. 父进程调用 wait()waitpid()
    • 正确方式是让父进程在适当时机主动回收子进程资源。
  2. 使用信号处理函数处理 SIGCHLD
    • 在父进程中注册信号处理函数自动处理子进程退出事件。
  3. 让子进程由 init 进程接管
    • 如果父进程异常退出,孤儿进程会被 init(PID=1)接管,init 会自动清理。

📌 图示说明(文字描述)

1
2
3
4
5
6
7
[子进程运行中]

[子进程执行完毕 → 进入僵尸状态]

[父进程未调用 wait() → 子进程描述符残留]

[形成僵尸进程(Zombie)]

孤儿进程(Orphan Process)

📌 定义

  • 孤儿进程是指其父进程已经终止(退出),而子进程仍在继续运行的进程。
  • 在 Unix/Linux 系统中,如果一个进程变成孤儿,系统会自动将它的父进程指向 init 进程(PID=1)。
  • init 进程会负责回收孤儿进程结束后的资源,防止它们变成僵尸进程。

📌 示例说明

1
2
3
4
5
6
7
父进程 A
├── 子进程 B

→ A 提前退出,而 B 还在执行
→ B 成为“孤儿进程”
→ 被 init 进程(PID=1)收养
→ 当 B 退出时,init 自动 wait(),完成资源回收

📌 与僵尸进程的区别对比

特性 孤儿进程 僵尸进程
定义 父进程终止,子进程仍在运行 子进程终止,父进程未回收其资源
状态 仍在运行 已终止,但进程表项未清理
是否会被清理 会,由 init 进程自动收养和清理 不会自动清理,需父进程主动调用 wait()
是否有危害 无危害 如果积累过多,会耗尽系统进程表资源

📌 为什么孤儿进程不会造成危害?

  • 孤儿进程的退出状态会被 init 进程及时回收;
  • init 是系统第一个进程,负责托管系统中失去父进程的孤立子进程;
  • 不会造成僵尸进程堆积,也不会浪费系统资源。

📌 延伸知识:孤儿进程的创建过程(举例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main() {
pid_t pid = fork();
if (pid > 0) {
// 父进程先退出
printf("Parent process exited\n");
exit(0);
} else {
// 子进程延迟运行,成为孤儿进程
sleep(10);
printf("Child process (orphan) running, adopted by init.\n");
}
return 0;
}

运行效果:

  • 父进程立即退出;
  • 子进程延迟运行,成为孤儿,被 init 接管。

📌 总结口诀

**“父亡子存是孤儿,init 养育不成灾。”


进程调度算法(Process Scheduling Algorithms)

进程调度算法决定了哪个进程可以在何时使用 CPU,是多道程序设计的核心机制之一,目标包括提高 CPU 利用率、提高系统吞吐量、减少平均等待时间等。


① 先来先服务(FCFS,First-Come First-Served)

  • 原理:按进程到达就绪队列的时间顺序进行调度。
  • 特点
    • 简单、易于实现;
    • 非抢占式;
    • 容易导致“长进程堵塞短进程”(即“饥饿”现象)。

📌 举例:进程 A(需要 10s),B(需要 2s),A 先到,B 需等 A 执行完再执行。


② 短作业优先(SJF,Shortest Job First)

  • 原理:选择估计执行时间最短的进程优先执行。
  • 特点
    • 最小化平均等待时间;
    • 非抢占式(也有抢占式变种,见 SRTF);
    • 需要准确预测执行时间,现实中难以实现;
    • 容易导致长作业饿死

③ 优先级调度(Priority Scheduling)

  • 原理:根据进程的优先级调度,优先级高者先运行。
  • 实现方式
    • 非抢占式:当前进程运行完才切换;
    • 抢占式:新到高优先级进程可中断当前执行。
  • 特点
    • 灵活;
    • 如果不处理“优先级反转”,低优先级进程可能长期得不到执行;
    • 可配合老化技术动态调整优先级避免饿死。

④ 时间片轮转(Round Robin, RR)

  • 原理:给每个进程分配一个固定的时间片,时间片耗尽就换下一个进程。
  • 特点
    • 抢占式调度;
    • 公平性好,适合交互式系统;
    • 时间片大小影响性能:
      • 太小 ⇒ 频繁切换、开销大;
      • 太大 ⇒ 类似 FCFS,响应时间长。

⑤ 最短剩余时间优先(SRTF, Shortest Remaining Time First)

  • 原理:抢占式的 SJF。选择剩余执行时间最短的进程运行。
  • 特点
    • 每次新进程进入,都会检查是否比当前进程短;
    • 平均等待时间最小;
    • 预测执行时间困难;
    • 容易导致长作业饿死。

⑥ 多级反馈队列(Multilevel Feedback Queue)

  • 原理:结合时间片轮转和优先级调度思想:
    • 设置多个就绪队列(如 Q1、Q2、Q3),每个队列时间片长度逐级变长
    • 新进程从高优先级队列开始运行
    • 若没在该队列完成 ⇒ 下降到下一队列
    • 高优先级队列有更高调度权。
  • 特点
    • 动态调整进程优先级,适应进程类型;
    • 避免长作业频繁切换;
    • 实现复杂。

📊 总结对比表

调度算法 抢占性 是否需要估算执行时间 优点 缺点
FCFS 简单,易于实现 可能导致短作业等待时间过长
SJF 平均等待时间最短 难以准确预估时间,易造成长作业饥饿
优先级调度 可抢占 灵活、可调整策略 可能发生优先级反转与进程饿死
时间片轮转 公平、适合交互系统 时间片设定不当会影响性能
SRTF 最短平均等待时间 同样难以估算时间,饿死风险高
多级反馈队列 动态适应不同进程,效率高 实现复杂

进程间通信(InterProcess Communication, IPC)

一、IPC 的六种主要方式

通信方式 是否支持无亲缘关系 数据流向 通信效率 特点或用途
管道(Pipe) 匿名管道:否命名管道:是 单向(默认) 较低 简单实现、适用于父子进程通信
信号(Signal) 单向通知 较低 通知机制,适用于异步事件处理
消息队列 双向可控 中等 有类型、有序、可靠,适合结构化消息传递
共享内存 双向 最快 直接共享内存,需同步机制配合
信号量(Semaphore) 无直接数据 较高 用于同步与互斥,保护共享资源
套接字(Socket) 是(跨主机) 双向 较高 支持网络通信,适用于分布式系统

二、各通信方式详细说明

1. 管道 Pipe

  • 📌 本质:内核中的一段缓存区。
  • 📥 写入端 → 管道 → 读取端(FIFO)。
  • 分为:
    • 匿名管道pipe() 系统调用,只支持父子进程通信。
    • 命名管道(FIFO)mkfifo(),支持无亲缘关系进程通信。
  • ⚠️ 缺点:只支持单向通信,效率低,不能用于频繁数据交换。

✅ 示例代码(匿名管道):

1
2
3
4
5
int pipefd[2];
pipe(pipefd); // 创建管道
fork(); // 创建子进程
// 父进程关闭读端,写数据 pipefd[1]
// 子进程关闭写端,读数据 pipefd[0]

2. 信号 Signal

  • 📌 是一种“事件通知机制”,用来告诉进程“发生了某个事件”。
  • ⏰ 属于异步通信
  • 常用信号:
    • SIGINT:Ctrl+C 产生,终止程序。
    • SIGTERM:请求终止进程(可被捕获)。
    • SIGKILL:强制终止进程(不可被阻塞/捕获)。
    • SIGHUP:终端挂断。
  • 🧠 类似“手机短消息”,不适合传输数据,仅适合通知。

3. 消息队列 Message Queue

  • 📌 是内核维护的消息链表,进程可以向队列中发送/接收消息。
  • 🧷 特点:
    • 支持消息分类。
    • 有消息长度上限。
    • 用户态和内核态之间有数据拷贝(性能影响)。
  • 📦 优点:
    • 支持无亲缘关系进程通信。
    • 安全性高、可靠性好。

4. 共享内存 Shared Memory

  • 📌 允许多个进程访问同一段物理内存。
  • ⚡ 是最快的通信方式,因为不需要内核中转。
  • 📌 系统调用:shmget()shmat()shmdt()
  • ⚠️ 缺点:需要额外的同步机制(如信号量)以避免竞争条件和数据错乱。

5. 信号量 Semaphore

  • 📌 是一种“进程同步与互斥”的控制手段。
  • 🧠 本质是一个整型计数器。
  • 常用操作:
    • P()(wait):尝试获取资源,计数器减1。
    • V()(signal):释放资源,计数器加1。
  • ✅ 主要用于:控制临界区、实现互斥锁、资源数量管理
  • 💡 类比:交通红绿灯。

6. 套接字 Socket

  • 📌 是网络通信的端点。
  • 可用于:
    • 同一主机内进程通信(UNIX socket)。
    • 不同主机进程通信(TCP/IP socket)。
  • 🛜 通常用于客户端-服务器(C/S)模式通信
  • 💡 类似 Java 的 Socket 类,支持双向、可靠连接。

三、总结对比图(记忆推荐)

类别 是否跨主机 是否亲缘限制 适合传递数据量 是否双向 通信速度 用途示意
匿名管道 父子进程传一句话
命名管道 两个程序用文件通信
信号 是(仅通知) 异常处理/中断通知
消息队列 中等 中等 顺序传数据、分类型
共享内存 最快 大量数据共享(图像)
信号量 控制共享内存访问
套接字 中等 网络程序/分布式系统