01 - 操作系统概述¶
⏰ 建议学习时间:1.5 小时 🎯 难度等级:⭐⭐ 📋 前置知识:计算机组成原理基础、C/Python 编程基础
📋 本章目录¶
一、操作系统的定义¶
1.1 什么是操作系统?¶
操作系统(Operating System, OS)是管理计算机硬件和软件资源的系统软件,是计算机系统中最基本的系统软件。它可以从两个角度来理解:
角度一:资源管理者(Resource Manager)
计算机系统拥有多种资源:CPU 时间、内存空间、磁盘空间、I/O 设备等。当多个程序同时运行时,操作系统负责合理分配和管理这些资源,确保每个程序都能获得所需的资源,同时防止资源冲突和浪费。
┌─────────────┐
│ 用户程序 │
│ App1 App2 .. │
└──────┬───────┘
│ 资源请求
┌──────▼───────┐
│ 操作系统 │ ← 资源管理者
│ (OS Kernel) │
└──────┬───────┘
│ 管理和分配
┌────────┬───┴───┬────────┐
▼ ▼ ▼ ▼
CPU 内存 磁盘 I/O设备
角度二:抽象机器(Extended Machine / Virtual Machine)
操作系统对底层硬件进行抽象和封装,向上层应用程序提供简洁、统一的接口。程序员不需要知道磁盘的具体物理结构,只需要调用 open()、read()、write() 等系统调用就能操作文件。
没有操作系统时:
程序员需要直接控制磁盘控制器的寄存器
→ 设置磁头位置、柱面号、扇区号
→ 发送读写命令、等待中断
→ 处理错误和重试
→ 极其复杂!
有操作系统后:
程序员只需要:
fd = open("data.txt", O_RDONLY);
read(fd, buffer, size);
close(fd);
→ 简单、安全、高效!
操作系统的标准定义:
操作系统是一种运行在内核态(Kernel Mode)的软件,它管理计算机的硬件资源,为应用程序提供运行环境,并向用户和程序员提供访问硬件的接口。
1.2 操作系统的地位¶
操作系统在计算机软件层次结构中处于核心位置:
┌─────────────────────────────────┐
│ 用户(User) │ ← 最上层
├─────────────────────────────────┤
│ 应用程序(Applications) │
│ 浏览器、Office、游戏... │
├─────────────────────────────────┤
│ 系统工具(System Utilities) │
│ 编译器、Shell、包管理器... │
├─────────────────────────────────┤
│ ★ 操作系统(OS Kernel)★ │ ← 核心位置
├─────────────────────────────────┤
│ 硬件(Hardware) │ ← 最底层
│ CPU、内存、磁盘、网卡... │
└─────────────────────────────────┘
1.3 操作系统与其他软件的区别¶
| 特征 | 操作系统 | 应用软件 |
|---|---|---|
| 运行模式 | 内核态(Ring 0) | 用户态(Ring 3) |
| 启动时机 | 开机时就启动,常驻内存 | 用户按需启动 |
| 资源访问 | 可以直接访问所有硬件 | 必须通过系统调用间接访问 |
| 生命周期 | 与计算机运行同生共死 | 可以被启动和终止 |
| 唯一性 | 同一时刻只有一个 OS 运行 | 可以同时运行多个应用 |
二、操作系统的发展历史¶
操作系统的发展历程是一部不断追求更高效率和更好抽象的历史。每一代操作系统都是为了解决上一代的不足而诞生的。
2.1 第零代:无操作系统(1940s-1950s)¶
硬件背景:第一代电子管计算机(如 ENIAC)
特点: - 没有操作系统,程序员直接操作硬件 - 使用纸带或插线板输入程序 - 一次只能运行一个程序 - 程序员需要预约机时,亲自到机房操作
问题: - CPU 利用率极低(大部分时间在等待人工操作) - 编程极其困难(机器语言 + 手动接线)
典型工作流程:
1. 程序员预约机器时间(可能是凌晨 3 点)
2. 携带纸带到机房
3. 装载纸带、开机、运行
4. 观察结果、收集输出
5. 关机、离开
→ CPU 可能 90% 的时间在空闲等待!
2.2 第一代:批处理系统(1950s-1960s)¶
2.2.1 单道批处理系统¶
核心思想:用监控程序(Monitor)代替人工操作,自动加载和执行作业。
工作方式: - 操作员将多个作业收集到磁带上,一次性提交给计算机 - 监控程序自动依次加载和执行每个作业 - 作业之间无需人工干预
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 作业 1 │───►│ 作业 2 │───►│ 作业 3 │
│ (计算) │ │ (计算) │ │ (计算) │
└──────────┘ └──────────┘ └──────────┘
顺序执行,无重叠
优点:减少了人工操作时间,提高了吞吐量 缺点: - CPU 在 I/O 时仍然空闲(I/O 速度远慢于 CPU) - 没有交互性,用户提交作业后只能等待结果
2.2.2 多道批处理系统¶
核心思想:多道程序设计(Multiprogramming)——内存中同时存放多个作业,当一个作业因为 I/O 而等待时,CPU 立即切换执行另一个作业。
工作方式:
时间轴:
┌──计算──┐ ┌──计算──┐ ┌──计算──┐
作业A: │████████│ │████████│ │████████│
└────────┘ └────────┘ └────────┘
┌─I/O─┐ ┌──计算──┐ ┌─I/O─┐ ┌──计算──┐
作业B: │▓▓▓▓▓│ │████████│ │▓▓▓▓▓│ │████████│
└─────┘ └────────┘ └─────┘ └────────┘
CPU: │A│ B计算 │ A计算 │ B计算 │ A计算 │
CPU几乎不空闲!
关键技术: - 内存管理:需要将多个作业同时放入内存 - 作业调度:决定哪些作业进入内存 - CPU 调度:决定将 CPU 分配给哪个作业 - 内存保护:防止作业之间互相干扰
优点:CPU 利用率大幅提高,吞吐量增加 缺点: - 仍然没有交互性 - 作业从提交到完成可能要等很长时间 - 难以调试程序
2.3 第二代:分时系统(1960s-1970s)¶
核心思想:将 CPU 时间分成时间片(Time Slice),多个用户通过终端同时使用一台计算机,每个用户感觉自己独占了一台计算机。
代表系统: - CTSS(Compatible Time-Sharing System,MIT,1961) - Multics(MIT + GE + Bell Labs,1964) - UNIX(Ken Thompson & Dennis Ritchie,AT&T Bell Labs,1969)
工作方式:
用户A 用户B 用户C 用户D
│ │ │ │
▼ ▼ ▼ ▼
┌─────────────────────┐
│ 分时操作系统 │
│ │
│ 时间片轮转: │
│ [A][B][C][D][A][B]..│
│ 每个时间片约20ms │
└──────────┬──────────┘
▼
CPU
特点: - 交互性好:用户可以直接与系统交互 - 多用户:支持多个用户同时使用 - 响应时间短:通常在几秒钟内响应 - 资源共享:多个用户共享 CPU、内存、磁盘等资源
分时系统 vs 批处理系统:
| 特征 | 批处理系统 | 分时系统 |
|---|---|---|
| 主要目标 | 吞吐量 | 响应时间 |
| 用户交互 | 无 | 实时交互 |
| CPU 分配 | 一个作业占用直到 I/O | 时间片轮转 |
| 用户数量 | 无直接用户 | 多用户 |
| 典型响应时间 | 小时/天 | 秒 |
2.4 第三代:实时操作系统(1970s-)¶
核心思想:保证在严格的时间约束内完成特定任务。
分类:
实时操作系统(RTOS)
├── 硬实时(Hard Real-Time)
│ └── 必须在截止时间前完成,否则可能造成灾难
│ 例:飞行控制系统、心脏起搏器、ABS刹车系统
│
└── 软实时(Soft Real-Time)
└── 偶尔超过截止时间可以接受,但会降低性能
例:视频播放、音频处理、游戏
代表系统:VxWorks、FreeRTOS、QNX、RT-Linux
关键特性: - 确定性:任务的响应时间可以预测 - 优先级调度:高优先级任务可以抢占低优先级任务 - 最小中断延迟:从中断发生到开始处理的时间尽可能短 - 内核可抢占:内核代码也可以被高优先级任务抢占
2.5 第四代:微内核架构(1980s-1990s)¶
设计动机:传统的宏内核(Monolithic Kernel)将所有功能(进程管理、内存管理、文件系统、设备驱动、网络协议栈...)都放在内核态,导致内核庞大且难以维护。
核心思想:只在内核中保留最基本的功能(进程间通信 IPC、基本调度、内存保护),其他功能作为用户态的服务器进程运行。
宏内核架构:
┌───────────────────────────────┐
│ 用户态(User Mode) │
│ App1 App2 App3 │
├───────────────────────────────┤
│ 内核态(Kernel Mode) │
│ 进程管理 │ 内存管理 │ 文件系统 │
│ 设备驱动 │ 网络 │ 安全 │
│ ...(所有功能都在内核态) │
└───────────────────────────────┘
微内核架构:
┌────────────────────────────────────────────┐
│ 用户态(User Mode) │
│ App1 App2 │ 文件服务 网络服务 驱动程序 │
│ │(以用户态服务器进程运行) │
├────────────────────────────────────────────┤
│ 内核态(Kernel Mode) │
│ IPC │ 调度 │ 内存保护 │
│ (最小化的微内核) │
└────────────────────────────────────────────┘
代表系统:Mach、L4、MINIX 3、QNX
微内核 vs 宏内核:
| 特征 | 宏内核 | 微内核 |
|---|---|---|
| 内核大小 | 大(数百万行代码) | 小(数万行代码) |
| 性能 | 高(函数调用) | 较低(需要 IPC) |
| 可靠性 | 一个模块崩溃可能导致整个系统崩溃 | 服务崩溃不影响内核 |
| 可维护性 | 难以维护和调试 | 模块化,易于维护 |
| 安全性 | 攻击面大 | 攻击面小 |
| 代表系统 | Linux、FreeBSD | QNX、MINIX 3 |
2.6 第五代:现代混合内核(1990s-至今)¶
核心思想:结合宏内核的性能优势和微内核的模块化优势。
代表系统: - Windows NT/10/11:混合内核,核心功能在内核态,部分子系统(如 Win32)在用户态 - macOS/iOS (XNU):Mach 微内核 + BSD 宏内核组件 - Linux:本质是宏内核,但通过可加载内核模块(LKM)实现了模块化 - 鸿蒙 OS:微内核架构,面向 IoT 场景
现代操作系统发展时间线:
1940s 1950s 1960s 1970s 1980s 1990s 2000s 2010s 2020s
│ │ │ │ │ │ │ │ │
│ 无OS │ 批处理 │ 分时 │ RTOS │ 微内核 │ 混合 │ 移动 │ 云+IoT │
│ │ │ UNIX │ │ Mach │ NT │ Android│ 鸿蒙 │
│ │ │ Multics│ │ L4 │ Linux │ iOS │ Fuchsia│
▼───────▼───────▼───────▼───────▼───────▼───────▼───────▼───────▼
2.7 发展历史总结表¶
| 时代 | 系统类型 | 解决的问题 | 关键技术 | 代表系统 |
|---|---|---|---|---|
| 1940s | 无 OS | - | 手动操作 | - |
| 1950s | 单道批处理 | 减少人工干预 | 监控程序 | FMS |
| 1960s | 多道批处理 | 提高CPU利用率 | 多道程序设计 | OS/360 |
| 1960s | 分时系统 | 提供交互性 | 时间片轮转 | UNIX |
| 1970s | 实时系统 | 满足时间约束 | 优先级调度 | VxWorks |
| 1980s | 微内核 | 可靠性/模块化 | IPC + 服务器进程 | Mach |
| 1990s至今 | 混合内核 | 性能 + 模块化 | 宏内核 + 模块化 | Linux/Windows |
三、操作系统的五大功能¶
操作系统的核心功能可以归纳为五大类,每一类管理一种关键资源:
3.1 进程管理(CPU 管理)¶
管理对象:CPU 时间
核心任务: - 进程创建与终止:创建新进程、终止已完成或异常的进程 - 进程调度:在多个就绪进程中,决定哪一个获得 CPU 使用权 - 进程同步与互斥:协调多个进程对共享资源的访问 - 进程通信:提供进程间通信机制(IPC) - 死锁处理:预防、避免、检测和解除死锁
进程管理相关系统调用:
├── fork() // 创建子进程
├── exec() // 加载新程序
├── wait() // 等待子进程结束
├── exit() // 终止进程
├── kill() // 发送信号
├── getpid() // 获取进程 ID
└── pthread_create() // 创建线程
3.2 内存管理¶
管理对象:主存(RAM)
核心任务: - 内存分配与回收:为进程分配内存,进程结束后回收 - 地址翻译:将逻辑地址转换为物理地址 - 内存保护:防止进程访问非法内存区域 - 虚拟内存:通过页面调度扩展可用内存 - 内存共享:允许多个进程共享同一段物理内存
3.3 文件管理¶
管理对象:磁盘上的文件和目录
核心任务: - 文件的创建、删除、读写 - 目录管理:层级目录结构 - 磁盘空间分配:管理空闲磁盘块 - 文件权限控制:access control - 文件系统一致性:防止数据损坏
3.4 I/O 设备管理¶
管理对象:各种 I/O 设备(键盘、屏幕、磁盘、网卡等)
核心任务: - 设备驱动程序管理:为每种设备提供统一的接口 - 缓冲管理:通过缓冲区协调 CPU 和 I/O 设备的速度差异 - 设备分配:将设备分配给请求的进程 - 中断处理:响应来自设备的中断信号
3.5 用户接口¶
提供给用户和程序员的访问方式: - 命令行接口(CLI):Shell(bash, zsh, PowerShell) - 图形用户接口(GUI):桌面环境(GNOME, KDE, Windows Desktop) - 系统调用接口(API):程序通过系统调用访问 OS 服务
用户接口的层次:
┌──────────────┐
│ GUI / CLI │ ← 用户直接使用
├──────────────┤
│ 库函数 │ ← printf() 封装了 write()
│ (libc etc.) │
├──────────────┤
│ 系统调用 │ ← 用户态到内核态的接口
│ (syscall) │
├──────────────┤
│ OS 内核 │
└──────────────┘
3.6 五大功能总结¶
┌──────────────────────────────────────────────┐
│ 操作系统 │
│ │
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌────┐ │
│ │进程 │ │内存 │ │文件 │ │ I/O │ │用户│ │
│ │管理 │ │管理 │ │管理 │ │ 管理 │ │接口│ │
│ │ │ │ │ │ │ │ │ │ │ │
│ │ CPU │ │ RAM │ │ Disk │ │Device│ │CLI │ │
│ │调度 │ │虚拟 │ │文件 │ │驱动 │ │GUI │ │
│ │同步 │ │分页 │ │目录 │ │缓冲 │ │API │ │
│ │通信 │ │保护 │ │权限 │ │中断 │ │ │ │
│ └──────┘ └──────┘ └──────┘ └──────┘ └────┘ │
└──────────────────────────────────────────────┘
四、内核态与用户态¶
4.1 为什么需要区分内核态和用户态?¶
假设没有内核态/用户态的区分,所有程序都可以直接访问硬件:
危险场景:
1. 恶意程序直接读写其他进程的内存 → 窃取密码
2. 有 bug 的程序直接操作磁盘控制器 → 破坏文件系统
3. 普通程序禁用中断 → 导致系统死机
4. 用户程序修改页表 → 绕过所有安全限制
因此,现代 CPU 设计了特权级(Privilege Level)机制,将指令分为特权指令和非特权指令:
- 特权指令:只能在内核态执行(如修改页表、禁用中断、直接 I/O)
- 非特权指令:在两种态下都可以执行(如加减运算、比较、跳转)
4.2 x86 特权级体系¶
x86 处理器定义了 4 个特权级(Protection Ring),用 CPU 状态寄存器 CS 段的低两位表示:
Ring 0(内核态)
┌────────────────┐
│ OS Kernel │ 最高特权
│ 可执行所有指令 │
┌──┴────────────────┴──┐
│ Ring 1 │ 驱动程序
│ (通常未使用) │ (某些OS用)
┌──┴──────────────────────┴──┐
│ Ring 2 │
│ (通常未使用) │
┌──┴────────────────────────────┴──┐
│ Ring 3(用户态) │
│ 用户应用程序 │ 最低特权
│ 只能执行非特权指令 │
└──────────────────────────────────┘
在实际的 Linux/Windows 系统中,通常只使用 Ring 0(内核态)和 Ring 3(用户态)两个级别。
4.3 内核态与用户态的切换¶
CPU 从用户态切换到内核态只有三种途径:
途径一:系统调用(System Call)— 主动切换¶
用户程序通过特殊的 trap 指令(如 x86 的 int 0x80 或 syscall 指令)主动请求内核服务。
系统调用完整流程:
用户态 内核态
│ │
│ 1. 准备参数 │
│ (寄存器/栈) │
│ │
│ 2. 执行 syscall 指令 │
│ ─────────────────────► │
│ trap │ 3. 保存用户态上下文
│ │ (寄存器、PC、SP)
│ │
│ │ 4. 查系统调用表
│ │ 根据系统调用号
│ │ 跳转到对应处理函数
│ │
│ │ 5. 执行系统调用
│ │ (如 read/write)
│ │
│ │ 6. 恢复用户态上下文
│ ◄───────────────────── │
│ 返回(iret) │ 7. 切换回用户态
│ │
│ 8. 获取返回值 │
│ │
系统调用号示例(Linux x86-64):
| 系统调用号 | 名称 | 功能 |
|---|---|---|
| 0 | read | 读文件 |
| 1 | write | 写文件 |
| 2 | open | 打开文件 |
| 3 | close | 关闭文件 |
| 39 | getpid | 获取进程 ID |
| 57 | fork | 创建子进程 |
| 59 | execve | 执行程序 |
| 60 | exit | 终止进程 |
途径二:异常(Exception)— 被动切换¶
程序执行过程中发生了异常情况(如除零、缺页),CPU 自动切换到内核态处理。
途径三:外部中断(Interrupt)— 被动切换¶
外部设备(如定时器、键盘、磁盘)向 CPU 发送中断信号,CPU 暂停当前任务,切换到内核态处理中断。
4.4 系统调用的详细机制¶
以 Linux x86-64 平台的 write() 系统调用为例:
// 用户程序调用 write()
#include <unistd.h>
ssize_t result = write(1, "Hello\n", 6); // 向标准输出写入
// 实际的调用链:
// write() [C库函数, glibc]
// → 将系统调用号 1 放入 rax 寄存器
// → 将参数放入 rdi, rsi, rdx 寄存器
// → 执行 syscall 指令
// → CPU 切换到内核态
// → 根据 rax 中的调用号,在 sys_call_table 中查找
// → 调用 sys_write() 内核函数
// → 内核完成写操作
// → 将返回值放入 rax
// → 执行 sysret/iretq 返回用户态
系统调用的开销:
系统调用的开销包括:
1. 保存/恢复寄存器上下文 ~100 cycles
2. 切换特权级(Ring 3 → Ring 0) ~50 cycles
3. TLB 刷新(如果需要) ~variable
4. 内核代码执行 ~variable
5. 切换回用户态 ~50 cycles
总计:一次系统调用约 200-1000 CPU 周期
对于 3GHz CPU,约 0.1-0.3 微秒
→ 所以应尽量减少系统调用次数(如用缓冲I/O代替直接I/O)
4.5 内核态与用户态的关键区别总结¶
| 比较维度 | 用户态(Ring 3) | 内核态(Ring 0) |
|---|---|---|
| 权限级别 | 最低 | 最高 |
| 可执行指令 | 仅非特权指令 | 所有指令 |
| 内存访问 | 仅用户空间(低地址) | 全部地址空间 |
| 硬件访问 | 不能直接访问 | 可以直接访问 |
| 崩溃影响 | 仅影响当前进程 | 导致系统崩溃(内核恐慌) |
| 切换方式 | 系统调用/异常/中断 | iret/sysret 返回 |
五、中断与异常¶
中断和异常是操作系统响应外部事件和处理内部错误的核心机制。没有中断机制,操作系统将无法实现多任务处理。
5.1 为什么需要中断?¶
假设没有中断机制,CPU 想知道键盘是否有按键,只能不断地去轮询(Polling)键盘控制器:
没有中断 → 轮询方式:
while (true) {
if (keyboard_ready()) { // 不断检查键盘
char c = read_keyboard();
process(c);
}
// CPU 大部分时间在做无用的检查!
}
有中断 → 中断驱动方式:
// CPU 正常执行其他任务
// 当键盘有按键时:
// 1. 键盘控制器发送中断信号给 CPU
// 2. CPU 暂停当前任务
// 3. 跳转到键盘中断处理程序
// 4. 读取按键、处理
// 5. 返回继续执行之前的任务
// → CPU 不浪费时间轮询!
5.2 中断的分类¶
CPU 接收到的事件
├── 中断(Interrupt)— 来自外部,异步
│ ├── 可屏蔽中断(Maskable Interrupt)
│ │ ├── 硬中断(Hardware Interrupt)
│ │ │ ├── 定时器中断(Timer Interrupt)
│ │ │ ├── 键盘中断
│ │ │ ├── 磁盘中断
│ │ │ └── 网卡中断
│ │ └── 软中断(Software Interrupt)
│ │ ├── Linux 的 softirq(下半部处理)
│ │ └── tasklet
│ └── 不可屏蔽中断(NMI, Non-Maskable Interrupt)
│ └── 硬件故障(如内存校验错误)
│
└── 异常(Exception)— CPU 内部,同步
├── Fault(故障)— 可恢复
│ ├── 缺页异常(Page Fault)
│ ├── 段错误的某些情况
│ └── 一般保护异常
├── Trap(陷阱)— 有意触发
│ ├── 系统调用(int 0x80 / syscall)
│ ├── 断点(int 3,调试用)
│ └── 溢出检测(into)
└── Abort(终止)— 不可恢复
├── 硬件错误(如总线错误)
└── 双重故障(Double Fault)
5.3 硬中断与软中断¶
硬中断(Hardware Interrupt)¶
由外部硬件设备触发,通过中断控制器(如 APIC)传递给 CPU。
特点: - 异步:随时可能发生,与 CPU 执行的指令无关 - 可屏蔽:CPU 可以通过设置中断标志位(IF)暂时屏蔽 - 优先级:不同设备的中断有不同优先级
硬中断处理流程:
1. 设备完成操作,向中断控制器发送中断信号
2. 中断控制器判断优先级,向 CPU 的 INTR 引脚发信号
3. CPU 在当前指令执行完后检查 INTR
4. 如果 IF=1(中断未屏蔽),CPU 响应中断:
a. 保存当前上下文(PC、PSW、寄存器)到栈
b. 根据中断号查 IDT(中断描述符表)
c. 获取中断处理程序的入口地址
d. 关中断(防止嵌套)
e. 跳转到中断处理程序执行
5. 中断处理程序执行完毕
6. 恢复上下文,开中断
7. 返回被中断的程序继续执行
软中断(Software Interrupt)¶
在 Linux 中,软中断是中断处理的下半部分(Bottom Half)机制,用于延迟处理不紧急的中断工作。
为什么需要软中断?
问题:硬中断处理时间不能太长(会屏蔽其他中断)
解决:将中断处理分为两部分
上半部(Top Half)—— 硬中断处理
├── 快速执行
├── 屏蔽中断
├── 只做最紧急的工作(如从设备读数据到缓冲区)
└── 标记软中断,告诉下半部有工作要做
下半部(Bottom Half)—— 软中断处理
├── 延后执行
├── 不屏蔽中断
├── 处理不紧急的工作(如网络协议栈处理)
└── 通过 softirq / tasklet / workqueue 实现
网络数据包接收示例:
网卡收到数据包
│
▼ [硬中断 - 上半部]
1. 将数据从网卡 DMA 缓冲区复制到内核缓冲区
2. 标记 NET_RX_SOFTIRQ 软中断
3. 硬中断返回(非常快,约几微秒)
│
▼ [软中断 - 下半部]
4. 处理网络协议栈(IP、TCP/UDP 解析)
5. 将数据放入 Socket 接收缓冲区
6. 唤醒等待数据的用户进程
5.4 异常的三种类型详解¶
Fault(故障)— 可恢复的错误¶
特点: - 发生异常的指令可以被重新执行 - 异常处理后,控制权返回到引起 Fault 的那条指令 - 如果无法恢复,则终止进程
最常见的 Fault:缺页异常(Page Fault)
进程访问一个页面 P
│
├── P 在物理内存中 → 正常访问(无异常)
│
└── P 不在物理内存中 → 触发缺页 Fault
│
▼
缺页异常处理程序:
1. 检查地址是否合法
├── 不合法 → 发送 SIGSEGV(段错误),终止进程
└── 合法 → 继续
2. 在磁盘上找到对应的页面
3. 分配一个空闲物理页框
4. 将页面从磁盘读入物理页框
5. 更新页表
6. 返回,重新执行引起缺页的那条指令
→ 这次成功访问!
Trap(陷阱)— 有意触发¶
特点: - 由程序故意触发,是一种预设的机制 - 异常处理后,控制权返回到 trap 指令的下一条指令 - 最典型的应用:系统调用
系统调用使用 trap 实现:
用户程序:
mov rax, 1 ; 系统调用号 = 1 (write)
mov rdi, 1 ; fd = 1 (stdout)
mov rsi, msg ; 缓冲区地址
mov rdx, len ; 长度
syscall ; ← 触发 trap,切换到内核态
; ← 系统调用返回后执行这里
内核:
1. 保存上下文
2. 根据 rax=1 找到 sys_write
3. 执行 sys_write
4. 恢复上下文
5. 返回到 syscall 的下一条指令
Abort(终止)— 不可恢复的错误¶
特点: - 发生了严重的硬件错误,无法恢复 - 直接终止引起异常的进程(或导致系统崩溃)
常见的 Abort: - 硬件故障(如 CPU 内部错误) - 双重故障(Double Fault):处理一个异常时又发生了另一个异常 - Machine Check Exception:处理器检测到内部错误
5.5 Fault vs Trap vs Abort 对比¶
| 类型 | 触发原因 | 是否可恢复 | 返回位置 | 示例 |
|---|---|---|---|---|
| Fault | 执行指令时出错 | 可能可恢复 | 引起异常的指令 | 缺页、除零、一般保护 |
| Trap | 程序故意触发 | 总是可恢复 | 下一条指令 | 系统调用、断点 |
| Abort | 严重硬件错误 | 不可恢复 | 不返回 | Machine Check、Double Fault |
5.6 中断处理的完整流程¶
┌─────────────┐
│ 中断/异常发生 │
└──────┬──────┘
▼
┌─────────────────┐
│ 1. CPU 完成当前 │
│ 指令的执行 │
└──────┬──────────┘
▼
┌─────────────────┐
│ 2. CPU 自动保存 │
│ PC、PSW 到栈 │
│ (硬件完成) │
└──────┬──────────┘
▼
┌─────────────────┐
│ 3. 根据中断号 │
│ 查 IDT 表 │
│ 获取处理程序 │
│ 入口地址 │
└──────┬──────────┘
▼
┌─────────────────┐
│ 4. 切换到内核态 │
│ (如果在用户态) │
└──────┬──────────┘
▼
┌─────────────────┐
│ 5. 保存更多上下文│
│ (通用寄存器) │
│ (OS 完成) │
└──────┬──────────┘
▼
┌─────────────────┐
│ 6. 执行中断 │
│ 处理程序 │
└──────┬──────────┘
▼
┌─────────────────┐
│ 7. 恢复上下文 │
└──────┬──────────┘
▼
┌─────────────────┐
│ 8. 执行 iret │
│ 返回被中断的 │
│ 程序继续执行 │
└─────────────────┘
六、操作系统分类¶
6.1 按任务数分类¶
| 类型 | 特点 | 示例 |
|---|---|---|
| 单任务 OS | 一次只能运行一个程序 | MS-DOS |
| 多任务 OS | 可以同时运行多个程序(并发) | Windows、Linux、macOS |
6.2 按用户数分类¶
| 类型 | 特点 | 示例 |
|---|---|---|
| 单用户 OS | 一次只允许一个用户使用 | MS-DOS、早期 Windows |
| 多用户 OS | 支持多个用户同时使用 | UNIX、Linux |
6.3 按响应方式分类¶
| 类型 | 特点 | 应用场景 |
|---|---|---|
| 批处理 OS | 依次处理作业,无交互 | 大规模科学计算 |
| 分时 OS | 时间片轮转,多用户交互 | 通用计算 |
| 实时 OS | 严格的时间约束 | 航空航天、工业控制 |
6.4 按体系结构分类¶
| 类型 | 特点 | 代表 |
|---|---|---|
| 宏内核 | 所有功能在内核态 | Linux、FreeBSD |
| 微内核 | 最小化内核,服务在用户态 | QNX、MINIX 3 |
| 混合内核 | 结合两者优点 | Windows NT、macOS |
| 外核(Exokernel) | 最小抽象,让应用直接管理资源 | MIT Exokernel(研究) |
6.5 按应用领域分类¶
| 类型 | 特点 | 代表 |
|---|---|---|
| 桌面 OS | 强调用户体验和兼容性 | Windows、macOS |
| 服务器 OS | 强调稳定性和并发 | Linux Server、Windows Server |
| 嵌入式 OS | 资源受限、实时性 | FreeRTOS、RT-Thread |
| 移动 OS | 触控、省电、应用生态 | Android、iOS |
| 分布式 OS | 多台计算机协同工作 | Plan 9(研究) |
| 云操作系统 | 管理数据中心资源 | OpenStack、K8s(某种意义上) |
七、代码示例:系统调用¶
7.1 Python 中的系统调用¶
"""
系统调用示例 — Python 版
展示常见系统调用的 Python 封装
"""
import os
import sys
# ============ 1. 进程相关系统调用 ============
# getpid() - 获取当前进程 ID
print(f"当前进程 PID: {os.getpid()}")
print(f"父进程 PID: {os.getppid()}")
# fork() - 创建子进程(仅 Unix/Linux/macOS)
# 注意:Windows 不支持 fork
if hasattr(os, 'fork'): # hasattr/getattr/setattr动态操作对象属性
pid = os.fork()
if pid == 0:
# 子进程
print(f"[子进程] PID={os.getpid()}, 父进程PID={os.getppid()}")
os._exit(0) # 子进程退出
else:
# 父进程
print(f"[父进程] PID={os.getpid()}, 创建了子进程PID={pid}")
os.wait() # 等待子进程结束
# ============ 2. 文件相关系统调用 ============
# open() + write() + close() — 低级别文件操作
fd = os.open("test_syscall.txt", os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o644)
os.write(fd, b"Hello from syscall!\n")
os.close(fd)
# read() — 读取文件
fd = os.open("test_syscall.txt", os.O_RDONLY)
data = os.read(fd, 1024)
print(f"读取到: {data.decode()}")
os.close(fd)
# stat() — 获取文件信息
stat_info = os.stat("test_syscall.txt")
print(f"文件大小: {stat_info.st_size} 字节")
print(f"修改时间: {stat_info.st_mtime}")
# unlink() — 删除文件
os.unlink("test_syscall.txt")
# ============ 3. 目录相关系统调用 ============
# getcwd() — 获取当前工作目录
print(f"当前目录: {os.getcwd()}")
# mkdir() + rmdir() — 创建和删除目录
os.mkdir("test_dir")
print("创建了 test_dir 目录")
os.rmdir("test_dir")
print("删除了 test_dir 目录")
print("\n系统调用示例完成!")
7.2 C 语言中的系统调用¶
/**
* 系统调用示例 — C 语言版
* 编译:gcc -o syscall_demo syscall_demo.c
* 运行:./syscall_demo
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <string.h>
int main() {
// ========== 1. 进程相关 ==========
printf("当前进程 PID: %d\n", getpid());
printf("父进程 PID: %d\n", getppid());
pid_t pid = fork();
if (pid < 0) {
perror("fork failed");
exit(1);
} else if (pid == 0) {
// 子进程
printf("[子进程] PID=%d, 父进程=%d\n", getpid(), getppid());
// exec: 用新程序替换当前进程
// execlp("ls", "ls", "-l", NULL);
// 如果exec成功,下面的代码不会执行
exit(0);
} else {
// 父进程
printf("[父进程] PID=%d, 创建了子进程=%d\n", getpid(), pid);
int status;
wait(&status); // 等待子进程结束
if (WIFEXITED(status)) {
printf("子进程正常退出,退出码: %d\n", WEXITSTATUS(status));
}
}
// ========== 2. 文件相关 ==========
// open() — 打开/创建文件
int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open failed");
exit(1);
}
// write() — 写入文件
const char *msg = "Hello, Syscall!\n";
write(fd, msg, strlen(msg));
// close() — 关闭文件
close(fd);
// open() + read() — 读取文件
fd = open("test.txt", O_RDONLY);
char buf[128];
ssize_t n = read(fd, buf, sizeof(buf) - 1);
buf[n] = '\0';
printf("读取到: %s", buf);
close(fd);
// unlink() — 删除文件
unlink("test.txt");
return 0;
}
7.3 使用内联汇编直接执行系统调用(Linux x86-64)¶
/**
* 直接使用汇编指令执行系统调用
* 不通过 C 库(glibc)封装
* 仅适用于 Linux x86-64
*/
#include <stdio.h> // 引入头文件
// 直接通过 syscall 指令执行 write 系统调用
ssize_t my_write(int fd, const void *buf, size_t count) { // 指针:存储变量的内存地址
ssize_t ret;
__asm__ volatile (
"movq $1, %%rax\n" // 系统调用号 1 = write
"movq %1, %%rdi\n" // 参数1: fd
"movq %2, %%rsi\n" // 参数2: buf
"movq %3, %%rdx\n" // 参数3: count
"syscall\n" // 触发系统调用
"movq %%rax, %0\n" // 返回值
: "=r" (ret)
: "r" ((long)fd), "r" (buf), "r" (count)
: "rax", "rdi", "rsi", "rdx", "rcx", "r11", "memory"
);
return ret;
}
// 直接通过 syscall 指令执行 getpid 系统调用
pid_t my_getpid(void) {
pid_t ret;
__asm__ volatile (
"movq $39, %%rax\n" // 系统调用号 39 = getpid
"syscall\n"
"movl %%eax, %0\n"
: "=r" (ret)
:
: "rax", "rcx", "r11"
);
return ret;
}
int main() {
// 使用自定义的系统调用函数
char msg[] = "Hello from raw syscall!\n";
my_write(1, msg, sizeof(msg) - 1);
// 对比两种方式获取 PID
printf("通过 glibc: PID = %d\n", getpid());
printf("通过 raw syscall: PID = %d\n", my_getpid());
return 0;
}
7.4 使用 strace 追踪系统调用¶
# strace 是 Linux 下追踪系统调用的工具
# 查看 ls 命令执行了哪些系统调用
$ strace ls /tmp
# 输出示例:
# execve("/usr/bin/ls", ["ls", "/tmp"], ...) = 0
# brk(NULL) = 0x560a8b12b000
# openat(AT_FDCWD, "/etc/ld.so.cache", ...) = 3
# read(3, "\177ELF...", 832) = 832
# mmap(NULL, 8192, ...) = 0x7f6e9c7fe000
# ...
# openat(AT_FDCWD, "/tmp", ...) = 3
# getdents64(3, ...) = 480
# write(1, "file1.txt file2.txt\n", 21) = 21
# close(3) = 0
# exit_group(0) = ?
# 统计系统调用次数
$ strace -c ls /tmp
# % time seconds usecs/call calls errors syscall
# ------ ----------- ----------- --------- --------- --------
# 25.00 0.000012 6 2 read
# 18.75 0.000009 3 3 openat
# 14.58 0.000007 3 2 1 access
# ...
八、练习题¶
选择题¶
1. 操作系统的核心功能不包括以下哪一项? - A. 进程管理 - B. 内存管理 - C. 编译程序 - D. 文件管理
答案:C。编译程序是应用软件(系统工具),不是操作系统内核的功能。
2. 以下哪种情况会导致 CPU 从用户态切换到内核态? - A. 执行一条加法指令 - B. 调用 printf() 函数(最终执行 write 系统调用) - C. 执行一条赋值语句 - D. 调用自定义的普通函数
答案:B。printf() 最终会调用 write() 系统调用,触发 trap,从用户态切换到内核态。
3. 缺页异常属于以下哪种类型? - A. 硬中断 - B. 软中断 - C. Fault(故障) - D. Abort(终止)
答案:C。缺页异常是一种 Fault,处理后会重新执行引起缺页的那条指令。
4. 微内核与宏内核相比,最主要的优势是: - A. 运行速度更快 - B. 可靠性和模块化更好 - C. 支持更多的系统调用 - D. 可以运行更多的进程
答案:B。微内核的核心优势是可靠性(一个服务崩溃不会导致整个系统崩溃)和模块化(易于维护和扩展)。
5. 在多道批处理系统中引入多道程序设计的主要目的是: - A. 提高用户交互体验 - B. 提高 CPU 利用率 - C. 减小内存使用量 - D. 简化操作系统设计
答案:B。多道程序设计的核心目的是在一个程序等待 I/O 时让另一个程序使用 CPU,从而提高 CPU 利用率。
简答题¶
1. 简述操作系统从批处理到分时系统演变的动机和关键技术变化。
参考答案: 批处理系统虽然通过多道程序设计提高了 CPU 利用率,但没有交互性——用户提交作业后只能等待结果,无法在程序运行过程中与之交互。这对于程序开发和调试极为不便。
分时系统的核心动机是提供用户交互性。关键技术变化是引入了时间片(Time Slice)轮转调度:将 CPU 时间分成固定的小片段(如 20ms),让每个用户轮流使用 CPU。由于切换速度很快,每个用户感觉自己独占了一台计算机。
主要技术变化: - 调度策略:从"运行直到完成或I/O"变为"时间片轮转" - 响应目标:从追求吞吐量变为追求响应时间 - 用户交互:引入终端设备,支持在线交互 - 内存管理:需要更好的内存管理来支持更多同时在线的用户
2. 解释硬中断和软中断的区别,并说明 Linux 为什么要将中断处理分为上半部和下半部。
参考答案:
硬中断由外部硬件设备触发(如定时器、网卡、磁盘),是异步事件;软中断是由内核代码触发的,用于延迟处理不紧急的中断工作。
Linux 将中断处理分为上半部和下半部的原因:
硬中断处理期间通常会屏蔽中断(至少屏蔽同级中断),如果处理时间过长,会导致: 1. 其他中断得不到及时响应 2. 系统响应能力下降 3. 可能丢失中断信号
因此,Linux 采用"上半部 + 下半部"的分离策略: - 上半部(Top Half):在硬中断上下文中执行,只做最紧急的工作(如从设备读取数据到缓冲区),然后标记软中断,快速返回 - 下半部(Bottom Half):通过 softirq、tasklet 或 workqueue 在较低优先级的上下文中执行,处理不紧急的工作(如网络协议栈处理)
这种设计在响应速度和处理完整性之间取得了良好的平衡。
九、自检清单¶
学完本章后,请检查自己是否能回答以下问题:
基础概念¶
- 能用自己的话定义"操作系统",并解释"资源管理者"和"抽象机器"两个角色
- 能列出操作系统的五大功能
- 能说出至少 3 种操作系统分类方式
发展历史¶
- 能按时间顺序描述操作系统的发展历程(批处理→分时→实时→微内核→混合)
- 能解释每一代为什么比上一代好(解决了什么问题)
- 能比较宏内核、微内核、混合内核的优缺点
内核态与用户态¶
- 能解释为什么需要内核态和用户态的区分
- 能说出 CPU 从用户态切换到内核态的三种方式
- 能描述一次系统调用的完整流程(从用户程序调用到返回)
- 知道 x86 的 Ring 0 和 Ring 3
中断与异常¶
- 能画出中断和异常的分类树
- 能区分硬中断、软中断、Fault、Trap、Abort
- 能解释 Linux 为什么要将中断处理分为上半部和下半部
- 能描述中断处理的完整流程
十、面试要点¶
🔥 高频面试题¶
Q1:什么是操作系统?它有哪些功能?¶
回答要点: 1. 定义(一句话):操作系统是管理计算机硬件和软件资源的系统软件 2. 两个身份:资源管理者 + 抽象机器 3. 五大功能:进程管理、内存管理、文件管理、I/O管理、用户接口 4. 补充:运行在内核态,是计算机启动后常驻内存的第一个软件
Q2:用户态和内核态的区别是什么?如何切换?¶
回答要点: 1. 区别:权限不同——内核态可以执行所有指令(包括特权指令),访问所有内存;用户态只能执行非特权指令,只能访问用户空间 2. 切换方式:系统调用(主动)、异常(被动)、外部中断(被动) 3. 切换代价:需要保存/恢复上下文、切换内存映射,约 200-1000 CPU 周期 4. 设计目的:保护系统安全和稳定性
Q3:中断和异常有什么区别?¶
回答要点:
| 中断(Interrupt) | 异常(Exception) | |
|---|---|---|
| 来源 | 外部设备 | CPU 内部 |
| 时机 | 异步(随时发生) | 同步(执行某指令时) |
| 举例 | 定时器中断、键盘中断 | 缺页、除零、系统调用 |
然后补充 Fault/Trap/Abort 的区别: - Fault 可恢复,返回同一条指令(如缺页) - Trap 是故意触发,返回下一条指令(如系统调用) - Abort 不可恢复,终止进程
Q4:系统调用的过程是怎样的?¶
回答要点(按步骤描述): 1. 用户程序将参数放入寄存器 2. 将系统调用号放入 eax/rax 寄存器 3. 执行 syscall/int 0x80 指令,触发 trap 4. CPU 切换到内核态,保存用户上下文 5. 根据系统调用号查表,跳转到对应的内核函数 6. 执行系统调用 7. 将返回值放入 rax 8. 恢复用户上下文,执行 sysret/iretq 返回用户态
Q5:宏内核和微内核的区别?Linux 是哪种?¶
回答要点: - 宏内核:所有 OS 功能在内核态,优点是性能高(函数调用),缺点是一个模块崩溃可能导致系统崩溃 - 微内核:只有最基本的功能在内核态,其他作为用户态服务,优点是可靠(模块隔离),缺点是性能开销(频繁 IPC) - Linux:技术上是宏内核,但通过可加载内核模块(LKM)实现了部分模块化。Linus Torvalds 和 Tanenbaum 在 1992 年有一场著名的论战(Tanenbaum–Torvalds debate),讨论宏内核 vs 微内核的优劣。
📝 面试回答模板¶
"概念 → 原理 → 对比 → 实例"
以 Q2 为例的完整回答:
用户态和内核态是 CPU 的两种运行模式,相当于两个不同的"权限等级"。
原理上,现代 CPU(如 x86)通过 Ring 机制实现特权级控制。Ring 0 是内核态,可以执行所有特权指令(如修改页表、关中断、直接 I/O);Ring 3 是用户态,只能执行非特权指令。如果用户态程序试图执行特权指令,CPU 会产生一个保护异常。
切换方式有三种:系统调用(程序主动请求 OS 服务)、异常(如缺页、除零)、外部中断(如定时器中断触发进程调度)。所有这些都会使 CPU 从用户态进入内核态。
设计目的是保护和隔离。如果所有程序都运行在内核态,一个有 bug 的程序可能直接修改其他进程的内存或破坏文件系统。有了特权级保护,即使用户程序崩溃,也只影响自身进程,不会导致整个系统崩溃。
举个例子,当应用程序需要读取文件时,它调用 read() 函数,glibc 将系统调用号放入 rax 寄存器,执行 syscall 指令触发 trap。CPU 保存当前上下文,切换到内核态,查找系统调用表执行对应的内核函数,完成后恢复上下文返回用户态。
📌 下一章:02-进程与线程 — 深入理解进程的概念、生命周期,以及线程与协程的设计