vault backup: 2026-06-13 23:46:22
This commit is contained in:
527
操作系统/00_操作系统概述/00_操作系统概述.md
Normal file
527
操作系统/00_操作系统概述/00_操作系统概述.md
Normal file
@@ -0,0 +1,527 @@
|
|||||||
|
# 第00讲 操作系统概述
|
||||||
|
|
||||||
|
> [!info] 课程信息
|
||||||
|
> **课程**:操作系统(专业基础课,必修)
|
||||||
|
> **教材**:《计算机操作系统》(第4版),汤小丹 等,西安电子科技大学出版社
|
||||||
|
> **学时**:4 学时(第1章 绪论)
|
||||||
|
> **先修课程**:C语言、数据结构、计算机组成原理
|
||||||
|
> **考核方式**:平时成绩 40% + 闭卷笔试 60%
|
||||||
|
> **课程目标**:理解 OS 基本原理 → 掌握系统编程 → 培养系统级思维
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、操作系统的概念
|
||||||
|
|
||||||
|
### 1.1 什么是操作系统
|
||||||
|
|
||||||
|
操作系统(Operating System, OS)是**管理计算机硬件与软件资源的系统软件**,是用户与计算机硬件之间的接口。
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────┐
|
||||||
|
│ 应用程序层 │ ← 用户直接使用
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ ★ 操作系统层 ★ │ ← 管理者与中介者
|
||||||
|
├─────────────────────────────────────┤
|
||||||
|
│ 硬件层 │ ← CPU、内存、设备
|
||||||
|
└─────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!tip] 三种看待 OS 的视角
|
||||||
|
> - **用户视角**:OS 是一台"虚拟机"——隐藏硬件复杂性,提供易用的接口
|
||||||
|
> - **系统视角**:OS 是一个"资源管理者"——分配 CPU、内存、I/O 设备等
|
||||||
|
> - **软件视角**:OS 是一个"运行平台"——提供系统调用和运行环境
|
||||||
|
|
||||||
|
### 1.2 操作系统的目标
|
||||||
|
|
||||||
|
| 目标 | 含义 | 典型手段 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **方便性** | 降低用户使用计算机的难度 | GUI、命令行、系统调用 |
|
||||||
|
| **有效性** | 提高系统资源的利用率 | 调度算法、虚拟内存、缓冲技术 |
|
||||||
|
| **可扩充性** | 便于扩充新功能 | 模块化、微内核架构 |
|
||||||
|
| **开放性** | 遵循标准,兼容互连 | POSIX 标准、ABI 兼容 |
|
||||||
|
|
||||||
|
### 1.3 操作系统的作用
|
||||||
|
|
||||||
|
**作用一:用户与硬件之间的接口**
|
||||||
|
|
||||||
|
```
|
||||||
|
用户 / 应用程序
|
||||||
|
│
|
||||||
|
│ 系统调用 (System Call)
|
||||||
|
│ 命令 (Shell)
|
||||||
|
│ GUI (图形界面)
|
||||||
|
▼
|
||||||
|
┌─────────────┐
|
||||||
|
│ 操作系统 │
|
||||||
|
└─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**作用二:计算机资源的管理者**
|
||||||
|
|
||||||
|
OS 负责管理四大类资源:
|
||||||
|
|
||||||
|
- **处理机(CPU)**:决定哪个进程使用 CPU、使用多长时间 → [[11_处理机调度]]
|
||||||
|
- **存储器(内存)**:为程序分配和回收内存空间 → [[13_存储管理基础]]、[[14_分页存储管理]]
|
||||||
|
- **I/O 设备**:管理外部设备的分配与回收 → 设备管理
|
||||||
|
- **文件(信息)**:管理磁盘上的数据存储与访问 → [[09_文件系统]]
|
||||||
|
|
||||||
|
**作用三:扩充机器(虚拟机)**
|
||||||
|
|
||||||
|
OS 在裸机之上叠加多层软件,将物理资源转化为功能更强、使用更方便的逻辑资源。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、操作系统的发展历程
|
||||||
|
|
||||||
|
> [!note] 发展驱动力
|
||||||
|
> OS 的每一次变革,根本动力都是**提高资源利用率**和**方便用户使用**。
|
||||||
|
|
||||||
|
### 2.1 发展时间线
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
timeline
|
||||||
|
title 操作系统发展历程
|
||||||
|
1940s-1950s : 手工操作阶段
|
||||||
|
: 无操作系统
|
||||||
|
: 人工操作纸带/卡片
|
||||||
|
1950s中后期 : 单道批处理系统
|
||||||
|
: 监督程序出现
|
||||||
|
: 自动按批处理作业
|
||||||
|
1960s前期 : 多道批处理系统
|
||||||
|
: 多道程序并发执行
|
||||||
|
: 资源利用率大幅提升
|
||||||
|
1960s中后期 : 分时系统
|
||||||
|
: 人机交互成为可能
|
||||||
|
: CTSS, MULTICS, UNIX
|
||||||
|
1970s-1980s : 实时系统
|
||||||
|
: 实时控制/实时信息处理
|
||||||
|
: 响应时间有严格保证
|
||||||
|
1980s-1990s : 网络操作系统
|
||||||
|
: 网络资源共享
|
||||||
|
: C/S 模式
|
||||||
|
1990s-至今 : 分布式操作系统
|
||||||
|
: 多机协同, 透明性
|
||||||
|
: 云计算, 容器化
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 各阶段详解
|
||||||
|
|
||||||
|
#### (1)手工操作阶段(1940s-1950s)
|
||||||
|
|
||||||
|
- **特点**:无操作系统,用户独占全机
|
||||||
|
- **工作方式**:程序员通过纸带/卡片将程序输入计算机,手工控制运行
|
||||||
|
- **问题**:
|
||||||
|
- 用户独占资源,CPU 利用率极低
|
||||||
|
- 人工操作时间远大于计算时间
|
||||||
|
- 上机需要预约机时
|
||||||
|
|
||||||
|
#### (2)单道批处理系统(1950s中后期)
|
||||||
|
|
||||||
|
- **核心思想**:引入**监督程序(Monitor)**,自动依次处理一批作业
|
||||||
|
- **特征**:作业成批输入,自动顺序执行,无交互能力
|
||||||
|
- **改进**:减少了人工干预时间
|
||||||
|
- **不足**:内存中始终只有一个程序运行,I/O 等待时 CPU 空闲
|
||||||
|
|
||||||
|
```
|
||||||
|
作业1 → [输入] → [计算] → [输出]
|
||||||
|
作业2 → [输入] → [计算] → [输出]
|
||||||
|
作业3 → ...
|
||||||
|
CPU: ████░░░░░░████████░░░░░░████████ ← CPU 仍大量空闲
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!warning] 单道批处理的瓶颈
|
||||||
|
> 当某作业进行 I/O 操作时,CPU 处于空闲等待状态,系统资源利用率仍然不高。
|
||||||
|
|
||||||
|
#### (3)多道批处理系统(1960s前期)
|
||||||
|
|
||||||
|
- **核心思想**:内存中同时存放多道程序,利用 CPU 空闲时间切换到其他程序运行
|
||||||
|
- **关键特征**:
|
||||||
|
- **多道性**:内存中有多道程序并发执行
|
||||||
|
- **宏观并行**:多道程序同时在系统中运行
|
||||||
|
- **微观串行**:任一时刻 CPU 只执行一道程序
|
||||||
|
|
||||||
|
> [!important] 多道批处理的意义
|
||||||
|
> 多道程序设计技术是 OS 发展史上的**里程碑**。它引入了程序并发执行的概念,极大地提高了 CPU 和资源利用率。这也是后续 [[06_进程控制|进程管理]] 和 [[11_处理机调度|调度算法]] 的基础。
|
||||||
|
|
||||||
|
- **缺点**:无交互能力(用户提交作业后无法干预)、平均周转时间长
|
||||||
|
|
||||||
|
#### (4)分时系统(1960s中后期)
|
||||||
|
|
||||||
|
- **核心思想**:多个用户通过终端同时使用一台计算机,每人获得"独占"的错觉
|
||||||
|
- **关键技术**:**时间片轮转** —— 每个用户/程序轮流使用一小段 CPU 时间
|
||||||
|
- **特征**:
|
||||||
|
- **多路性**:多个用户同时使用
|
||||||
|
- **独立性**:各用户互不干扰
|
||||||
|
- **交互性**:用户可实时与系统对话
|
||||||
|
- **及时性**:响应时间通常在数秒内
|
||||||
|
|
||||||
|
> [!example] 典型分时系统
|
||||||
|
> - **CTSS**(1961,MIT):最早的分时系统之一
|
||||||
|
> - **MULTICS**(1964,MIT/Bell Labs/GE):影响深远但过于复杂
|
||||||
|
> - **UNIX**(1969,Ken Thompson & Dennis Ritchie):从 MULTICS 理念简化而来,成为现代 OS 的基石
|
||||||
|
|
||||||
|
#### (5)实时系统(1970s-1980s)
|
||||||
|
|
||||||
|
- **定义**:系统能**及时响应外部事件请求**,在规定时间内完成处理
|
||||||
|
- **两种类型**:
|
||||||
|
|
||||||
|
| 类型 | 应用场景 | 示例 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 实时控制系统 | 工业过程控制、军事 | 飞行控制、导弹制导 |
|
||||||
|
| 实时信息处理 | 信息查询与处理 | 飞机订票系统、银行交易 |
|
||||||
|
|
||||||
|
- **与分时系统的区别**:实时系统强调**确定性**(硬实时)或**高概率满足时限**(软实时)
|
||||||
|
|
||||||
|
#### (6)网络操作系统(1980s-1990s)
|
||||||
|
|
||||||
|
- **目标**:实现网络中各计算机之间的资源共享与通信
|
||||||
|
- **特征**:基于 C/S 模式,提供文件共享、打印服务、网络通信等
|
||||||
|
- **典型系统**:Novell NetWare、Windows NT Server
|
||||||
|
|
||||||
|
#### (7)分布式操作系统(1990s-至今)
|
||||||
|
|
||||||
|
- **目标**:多台计算机协同工作,对外表现为一个统一的系统
|
||||||
|
- **关键特征**:
|
||||||
|
- **分布性**:多台自治计算机协作
|
||||||
|
- **透明性**:用户感知不到资源的物理分布
|
||||||
|
- **共享性**:资源全局共享
|
||||||
|
|
||||||
|
> [!tip] 分布式系统的演进
|
||||||
|
> 从集群计算 → 网格计算 → **云计算**(IaaS/PaaS/SaaS)→ 容器编排(Kubernetes),分布式系统的理念在不断深化。现代云计算本质上是分布式操作系统的延伸。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、操作系统的基本功能
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph OS["操作系统"]
|
||||||
|
PM["处理机管理
|
||||||
|
Process Management"]
|
||||||
|
SM["存储器管理
|
||||||
|
Memory Management"]
|
||||||
|
DM["设备管理
|
||||||
|
Device Management"]
|
||||||
|
FM["文件管理
|
||||||
|
File Management"]
|
||||||
|
UI["用户接口
|
||||||
|
User Interface"]
|
||||||
|
end
|
||||||
|
|
||||||
|
PM --> CPU["CPU 分配与回收"]
|
||||||
|
PM --> PROC["进程创建/撤销"]
|
||||||
|
PM --> SCHED["进程调度"]
|
||||||
|
PM --> SYNC["同步与通信"]
|
||||||
|
PM --> DEAD["死锁处理"]
|
||||||
|
|
||||||
|
SM --> ALLOC["内存分配与回收"]
|
||||||
|
SM --> PROTECT["内存保护"]
|
||||||
|
SM --> ADDR["地址映射"]
|
||||||
|
SM --> VMEM["虚拟内存"]
|
||||||
|
|
||||||
|
DM --> BUFFER["缓冲管理"]
|
||||||
|
DM --> DEV_ALLOC["设备分配与回收"]
|
||||||
|
DM --> DRIVER["设备驱动"]
|
||||||
|
|
||||||
|
FM --> DIR["目录管理"]
|
||||||
|
FM --> DISK["磁盘空间管理"]
|
||||||
|
FM --> RW["文件读写"]
|
||||||
|
|
||||||
|
UI --> CMD["命令接口"]
|
||||||
|
GUI --> GUI_INT["图形接口"]
|
||||||
|
UI --> SYSCALL["程序接口(系统调用)"]
|
||||||
|
|
||||||
|
style OS fill:#1a1a2e,stroke:#e94560,color:#fff
|
||||||
|
style PM fill:#16213e,stroke:#0f3460,color:#fff
|
||||||
|
style SM fill:#16213e,stroke:#0f3460,color:#fff
|
||||||
|
style DM fill:#16213e,stroke:#0f3460,color:#fff
|
||||||
|
style FM fill:#16213e,stroke:#0f3460,color:#fff
|
||||||
|
style UI fill:#16213e,stroke:#0f3460,color:#fff
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 处理机管理(进程管理)
|
||||||
|
|
||||||
|
处理机管理是 OS 的核心功能,负责对 **CPU 资源** 进行管理和调度。
|
||||||
|
|
||||||
|
主要任务:
|
||||||
|
|
||||||
|
1. **进程控制**:创建、撤销进程,控制进程状态转换 → [[06_进程控制]]
|
||||||
|
2. **进程调度**:按照一定策略从就绪队列中选择进程投入运行 → [[11_处理机调度]]
|
||||||
|
3. **进程同步与通信**:协调并发进程之间的关系,解决竞争与协作问题 → [[08_进程间通信]]
|
||||||
|
4. **死锁处理**:预防、避免、检测和解除死锁 → [[12_死锁]]
|
||||||
|
|
||||||
|
> [!info] 进程 vs 程序
|
||||||
|
> - **程序**:静态的代码和数据(存储在磁盘上的文件)
|
||||||
|
> - **进程**:程序的一次执行过程,是动态的、有生命周期的
|
||||||
|
> - 一个程序可以对应多个进程(例如多次打开同一个浏览器)
|
||||||
|
|
||||||
|
### 3.2 存储器管理
|
||||||
|
|
||||||
|
存储器管理负责为程序分配内存空间,并确保各程序互不干扰。
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **内存分配与回收** | 按策略为进程分配内存,进程结束时回收 |
|
||||||
|
| **内存保护** | 确保每个进程只能访问自己的地址空间 |
|
||||||
|
| **地址映射** | 将逻辑地址转换为物理地址(需要 [[01_系统运行机制|MMU]] 的支持) |
|
||||||
|
| **虚拟内存** | 利用磁盘扩充内存容量,使程序不受物理内存限制 |
|
||||||
|
|
||||||
|
相关笔记:[[13_存储管理基础]] → [[14_分页存储管理]] → [[15_段式存储管理]] → [[16_虚拟存储器]]
|
||||||
|
|
||||||
|
### 3.3 设备管理
|
||||||
|
|
||||||
|
设备管理负责管理各类 I/O 设备,完成用户提出的 I/O 请求。
|
||||||
|
|
||||||
|
- **缓冲管理**:在 CPU 与 I/O 设备之间设置缓冲区,缓解速度差异
|
||||||
|
- **设备分配与回收**:按策略为进程分配设备
|
||||||
|
- **设备驱动**:控制设备硬件完成实际的输入输出操作
|
||||||
|
|
||||||
|
> [!tip] I/O 控制方式的演进
|
||||||
|
> 程序直接控制 → 中断驱动 → DMA → 通道。每种方式在 CPU 参与度和效率上逐步优化。详见 [[01_系统运行机制]] 中的中断机制。
|
||||||
|
|
||||||
|
### 3.4 文件管理
|
||||||
|
|
||||||
|
文件管理负责管理外存上的文件,为用户提供"按名存取"的能力。
|
||||||
|
|
||||||
|
- **文件存储空间管理**:管理磁盘空间的分配与回收 → [[05_磁盘空间管理]]
|
||||||
|
- **目录管理**:通过目录结构组织文件,支持文件的按名查找
|
||||||
|
- **文件读写与保护**:控制文件的读、写、执行权限 → [[09_文件系统]]
|
||||||
|
|
||||||
|
### 3.5 用户接口(用户与 OS 的通信手段)
|
||||||
|
|
||||||
|
| 接口类型 | 形式 | 说明 |
|
||||||
|
|----------|------|------|
|
||||||
|
| **命令接口** | Shell 命令 | 用户在终端输入命令(如 `ls`, `gcc`) |
|
||||||
|
| **图形接口** | GUI 窗口操作 | 通过鼠标点击操作(如 Windows 桌面) |
|
||||||
|
| **程序接口** | 系统调用 (System Call) | 应用程序通过 API 请求 OS 服务 |
|
||||||
|
|
||||||
|
> [!important] 系统调用
|
||||||
|
> 系统调用是应用程序与操作系统之间的**唯一合法入口**。当程序需要访问硬件资源(如读文件、分配内存、创建进程)时,必须通过系统调用陷入内核态执行。这是 [[01_系统运行机制]] 的核心内容。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、操作系统的结构
|
||||||
|
|
||||||
|
> [!note] 为什么要关注 OS 结构?
|
||||||
|
> 操作系统是一个极其复杂的大型软件。良好的结构设计能够提高系统的**可靠性、可维护性和可扩展性**。
|
||||||
|
|
||||||
|
### 4.1 整体式结构(Monolithic Structure)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────┐
|
||||||
|
│ 用户程序 │
|
||||||
|
├──────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 操作系统内核 │
|
||||||
|
│ ┌────┬────┬────┬────┬────┐ │
|
||||||
|
│ │进程│内存│设备│文件│调度│ │
|
||||||
|
│ │管理│管理│管理│管理│算法│ │
|
||||||
|
│ └────┴────┴────┴────┴────┘ │
|
||||||
|
│ 所有模块在同一层,任意调用 │
|
||||||
|
│ │
|
||||||
|
├──────────────────────────────────┤
|
||||||
|
│ 硬件 │
|
||||||
|
└──────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **特点**:所有功能模块组织在一起,模块之间可以相互调用
|
||||||
|
- **优点**:结构简单、效率高(模块间直接函数调用)
|
||||||
|
- **缺点**:模块间耦合度高,一处错误可能导致整个系统崩溃;难以调试和维护
|
||||||
|
- **典型代表**:早期 UNIX
|
||||||
|
|
||||||
|
### 4.2 层次式结构(Layered Structure)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ 第 N 层:用户接口 │
|
||||||
|
├─────────────────────┤
|
||||||
|
│ 第 N-1 层:文件管理 │
|
||||||
|
├─────────────────────┤
|
||||||
|
│ 第 N-2 层:设备管理 │
|
||||||
|
├─────────────────────┤
|
||||||
|
│ 第 N-3 层:内存管理 │
|
||||||
|
├─────────────────────┤
|
||||||
|
│ 第 N-4 层:进程管理 │
|
||||||
|
├─────────────────────┤
|
||||||
|
│ 第 0 层:硬件抽象 │
|
||||||
|
└─────────────────────┘
|
||||||
|
每一层只调用下一层的服务
|
||||||
|
```
|
||||||
|
|
||||||
|
- **特点**:将 OS 划分为若干层,每层只能调用紧邻的下层
|
||||||
|
- **优点**:结构清晰、便于调试(逐层验证)、错误隔离
|
||||||
|
- **缺点**:层间调用带来性能开销;层次划分困难
|
||||||
|
- **典型代表**:THE 系统(Dijkstra, 1968)、Windows NT 内核
|
||||||
|
|
||||||
|
### 4.3 微内核结构(Microkernel)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────┐
|
||||||
|
│ 用户态 │
|
||||||
|
│ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │
|
||||||
|
│ │文件 │ │网络 │ │设备 │ │用户 │ │
|
||||||
|
│ │服务 │ │服务 │ │驱动 │ │应用 │ │
|
||||||
|
│ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ ┌──┴────────┴────────┴────────┴──┐ │
|
||||||
|
│ │ 消息传递 (IPC) │ │
|
||||||
|
│ └──────────────┬─────────────────┘ │
|
||||||
|
├─────────────────┼────────────────────┤
|
||||||
|
│ 内核态 │ │
|
||||||
|
│ ┌──────────────┴────────────────┐ │
|
||||||
|
│ │ 微内核 │ │
|
||||||
|
│ │ · 进程调度 │ │
|
||||||
|
│ │ · 内存管理(基本) │ │
|
||||||
|
│ │ · 进程间通信 (IPC) │ │
|
||||||
|
│ └───────────────────────────────┘ │
|
||||||
|
├──────────────────────────────────────┤
|
||||||
|
│ 硬件 │
|
||||||
|
└──────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **核心思想**:将 OS 核心功能精简到最小(微内核),其余功能以**用户态服务进程**运行
|
||||||
|
- **优点**:
|
||||||
|
- 内核小而稳定,可靠性高
|
||||||
|
- 服务进程崩溃不影响内核
|
||||||
|
- 易于扩展新服务
|
||||||
|
- **缺点**:用户态与内核态之间的消息传递带来性能开销
|
||||||
|
- **典型代表**:Mach、QNX、MINIX、L4、Windows NT(混合结构)
|
||||||
|
|
||||||
|
> [!example] 现实中的微内核
|
||||||
|
> - **QNX**:用于汽车电子、航空航天等安全关键领域
|
||||||
|
> - **Google Fuchsia**:基于 Zircon 微内核
|
||||||
|
> - **鸿蒙 OS**:采用微内核架构,用于 IoT 和移动设备
|
||||||
|
|
||||||
|
### 4.4 虚拟机结构(Virtual Machine)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────┐ ┌──────┐ ┌──────┐
|
||||||
|
│ VM 1 │ │ VM 2 │ │ VM 3 │ ← 各 VM 运行独立的 OS
|
||||||
|
│Linux │ │Win │ │macOS │
|
||||||
|
├──────┤ ├──────┤ ├──────┤
|
||||||
|
│Guest │ │Guest │ │Guest │ ← 客户机 OS
|
||||||
|
│ OS │ │ OS │ │ OS │
|
||||||
|
└──┬───┘ └──┬───┘ └──┬───┘
|
||||||
|
│ │ │
|
||||||
|
┌──┴────────┴────────┴──┐
|
||||||
|
│ 虚拟机监控器 (VMM/Hypervisor) │ ← 硬件虚拟化
|
||||||
|
├───────────────────────┤
|
||||||
|
│ 硬件 │
|
||||||
|
└───────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **核心思想**:在硬件之上运行**虚拟机监控器(VMM/Hypervisor)**,将物理机器虚拟为多台逻辑机器
|
||||||
|
- **优点**:
|
||||||
|
- 完全隔离,安全性最高
|
||||||
|
- 可以在同一硬件上运行不同 OS
|
||||||
|
- 便于系统迁移和测试
|
||||||
|
- **缺点**:虚拟化带来性能损耗
|
||||||
|
- **典型代表**:VMware、KVM、Xen、Hyper-V
|
||||||
|
|
||||||
|
> [!tip] 容器化 vs 虚拟机
|
||||||
|
> - **虚拟机**:虚拟化整个硬件,每个 VM 运行完整的 OS
|
||||||
|
> - **容器**(Docker):共享宿主 OS 内核,只隔离用户空间,更轻量
|
||||||
|
> - 容器可以看作是操作系统层面虚拟化的产物
|
||||||
|
|
||||||
|
### 4.5 四种结构对比
|
||||||
|
|
||||||
|
| 结构 | 耦合度 | 性能 | 可靠性 | 可扩展性 | 代表 |
|
||||||
|
|------|--------|------|--------|----------|------|
|
||||||
|
| 整体式 | 高 | 高 | 低 | 差 | 早期 UNIX |
|
||||||
|
| 层次式 | 中 | 中 | 中 | 中 | THE, Windows NT |
|
||||||
|
| 微内核 | 低 | 较低 | 高 | 好 | QNX, L4, Mach |
|
||||||
|
| 虚拟机 | 最低 | 较低 | 最高 | 最好 | VMware, KVM |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、操作系统的特征
|
||||||
|
|
||||||
|
> [!important] 四大基本特征
|
||||||
|
> 理解这四个特征是学习后续所有章节的前提。
|
||||||
|
|
||||||
|
### 5.1 并发(Concurrence)
|
||||||
|
|
||||||
|
- **并发**:多个事件在同一时间间隔内发生(宏观同时,微观交替)
|
||||||
|
- **并行**(Parallelism):多个事件在同一时刻同时发生(需要多核/多处理器)
|
||||||
|
|
||||||
|
> [!tip] 并发 vs 并行
|
||||||
|
> - 单核 CPU:只能实现**并发**(通过时间片轮转)
|
||||||
|
> - 多核 CPU:可以实现**并行**(每个核心执行一个进程)
|
||||||
|
> - 并发是 OS 最基本、最重要的特征
|
||||||
|
|
||||||
|
### 5.2 共享(Sharing)
|
||||||
|
|
||||||
|
- **互斥共享**:资源一次只允许一个进程使用(如打印机)
|
||||||
|
- **同时共享**:资源可被多个进程同时访问(如磁盘文件、只读代码段)
|
||||||
|
|
||||||
|
并发和共享是 OS 的两个最基本的特征,它们**互为存在条件**。
|
||||||
|
|
||||||
|
### 5.3 虚拟(Virtual)
|
||||||
|
|
||||||
|
- 通过某种技术将一个物理实体变为多个逻辑实体
|
||||||
|
- **CPU 虚拟化**:时分复用 → 每个进程感觉自己独占 CPU
|
||||||
|
- **内存虚拟化**:空分复用 → 虚拟内存技术让程序使用比实际更大的地址空间
|
||||||
|
|
||||||
|
### 5.4 异步(Asynchronism)
|
||||||
|
|
||||||
|
- 进程的执行以**不可预知的速度**向前推进
|
||||||
|
- 同一程序、同一数据,多次运行的结果可能不同(取决于调度和资源竞争)
|
||||||
|
- OS 需要保证:只要运行环境相同,程序的**最终结果**一致
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、本讲小结
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A["操作系统概述"] --> B["概念"]
|
||||||
|
A --> C["发展历程"]
|
||||||
|
A --> D["基本功能"]
|
||||||
|
A --> E["系统结构"]
|
||||||
|
A --> F["基本特征"]
|
||||||
|
|
||||||
|
B --> B1["定义:系统软件"]
|
||||||
|
B --> B2["目标:方便·有效·可扩·开放"]
|
||||||
|
B --> B3["作用:接口·管理·扩充"]
|
||||||
|
|
||||||
|
C --> C1["手工 → 批处理"]
|
||||||
|
C --> C2["分时 → 实时"]
|
||||||
|
C --> C3["网络 → 分布式"]
|
||||||
|
|
||||||
|
D --> D1["处理机管理"]
|
||||||
|
D --> D2["存储器管理"]
|
||||||
|
D --> D3["设备管理"]
|
||||||
|
D --> D4["文件管理"]
|
||||||
|
D --> D5["用户接口"]
|
||||||
|
|
||||||
|
E --> E1["整体式 / 层次式"]
|
||||||
|
E --> E2["微内核 / 虚拟机"]
|
||||||
|
|
||||||
|
F --> F1["并发·共享·虚拟·异步"]
|
||||||
|
|
||||||
|
style A fill:#e94560,stroke:#333,color:#fff
|
||||||
|
style B fill:#0f3460,stroke:#333,color:#fff
|
||||||
|
style C fill:#0f3460,stroke:#333,color:#fff
|
||||||
|
style D fill:#0f3460,stroke:#333,color:#fff
|
||||||
|
style E fill:#0f3460,stroke:#333,color:#fff
|
||||||
|
style F fill:#0f3460,stroke:#333,color:#fff
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、相关笔记
|
||||||
|
|
||||||
|
| 主题 | 链接 |
|
||||||
|
|------|------|
|
||||||
|
| 系统运行机制(中断、MMU、系统调用) | [[01_系统运行机制]] |
|
||||||
|
| 进程控制(fork、exec、wait、signal) | [[06_进程控制]] |
|
||||||
|
| 进程间通信 | [[08_进程间通信]] |
|
||||||
|
| 处理机调度 | [[11_处理机调度]] |
|
||||||
|
| 死锁 | [[12_死锁]] |
|
||||||
|
| 存储管理基础 | [[13_存储管理基础]] |
|
||||||
|
| 分页存储管理 | [[14_分页存储管理]] |
|
||||||
|
| 段式存储管理 | [[15_段式存储管理]] |
|
||||||
|
| 虚拟存储器 | [[16_虚拟存储器]] |
|
||||||
|
| 磁盘空间管理 | [[05_磁盘空间管理]] |
|
||||||
|
| 文件系统 | [[09_文件系统]] |
|
||||||
|
| 课程总导航 | [[00_课程导航]] |
|
||||||
163
操作系统/00_课程导航.md
Normal file
163
操作系统/00_课程导航.md
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# 🗺️ 操作系统课程导航
|
||||||
|
|
||||||
|
> **课程**:操作系统 | **对象**:软件工程 3、4 班 | **主讲**:徐钦桂
|
||||||
|
> **教材**:汤小丹《计算机操作系统》、《操作系统概念》、《Linux编程》
|
||||||
|
> **资料来源**:`F:\操作系统\` 目录下的课件(.ppt/.pptx)、实验指导书(.doc)、源代码
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 课程内容总览
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
mindmap
|
||||||
|
root((操作系统))
|
||||||
|
基础篇
|
||||||
|
系统运行机制(中断/MMU/CPU模式/系统调用)
|
||||||
|
Linux简介与使用(目录/文件/命令)
|
||||||
|
Linux C编程(gdb/make/编译链接)
|
||||||
|
文件与存储
|
||||||
|
文件IO(UNIX IO/mmap/重定向)
|
||||||
|
磁盘空间管理(FAT/NTFS/Ext2/RAID)
|
||||||
|
存储管理(分页/分段/段页式)
|
||||||
|
虚拟存储器(请求分页/页面置换/工作集)
|
||||||
|
进程与线程
|
||||||
|
进程控制(fork/exec/wait/signal)
|
||||||
|
多线程(互斥/同步/生产者消费者)
|
||||||
|
进程间通信(管道/消息队列/共享内存)
|
||||||
|
处理机调度(FCFS/SJF/RR/MFQ/CFS)
|
||||||
|
死锁(预防/避免/检测/银行家算法)
|
||||||
|
网络与并发
|
||||||
|
网络编程(socket/客户端服务器)
|
||||||
|
并发服务器(多进程/多线程/预线程化)
|
||||||
|
系统底层
|
||||||
|
程序代码优化
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛤️ 推荐学习路线
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A["第01讲 系统运行机制
|
||||||
|
中断·MMU·系统调用"] --> B["第02讲 Linux基础
|
||||||
|
目录·文件·命令"]
|
||||||
|
B --> C["第03讲 C编程基础
|
||||||
|
gdb·make·编译"]
|
||||||
|
C --> D["第04讲 文件IO
|
||||||
|
open·read·write·mmap"]
|
||||||
|
D --> E["实验01 IO编程"]
|
||||||
|
C --> F["第05讲 进程控制
|
||||||
|
fork·exec·signal"]
|
||||||
|
F --> G["实验02 进程控制"]
|
||||||
|
F --> H["第06讲 多线程
|
||||||
|
mutex·sem·生产者消费者"]
|
||||||
|
H --> I["实验03 多线程"]
|
||||||
|
F --> J["第07讲 进程间通信
|
||||||
|
pipe·shm·msg"]
|
||||||
|
J --> K["实验04 IPC"]
|
||||||
|
D --> L["第08讲 网络编程
|
||||||
|
socket·toggle"]
|
||||||
|
L --> M["实验05 网络通信"]
|
||||||
|
L --> N["第09讲 并发服务器
|
||||||
|
多进程·多线程·预线程化"]
|
||||||
|
N --> O["实验06 并发服务器"]
|
||||||
|
A --> P["第11讲 处理机调度
|
||||||
|
FCFS·SJF·RR·MFQ·CFS"]
|
||||||
|
P --> Q["第12讲 死锁
|
||||||
|
银行家算法·检测·解除"]
|
||||||
|
D --> R["第10讲 磁盘管理
|
||||||
|
FAT·Ext2·RAID"]
|
||||||
|
D --> S["第13讲 存储管理
|
||||||
|
分页·分段·TLB"]
|
||||||
|
S --> T["第14讲 虚拟存储器
|
||||||
|
请求分页·置换算法"]
|
||||||
|
C --> U["第15讲 代码优化"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 各讲笔记
|
||||||
|
|
||||||
|
### 基础篇
|
||||||
|
| 讲次 | 主题 | 核心内容 | 课件 |
|
||||||
|
|------|------|----------|------|
|
||||||
|
| [[01_系统运行机制]] | 系统运行机制 | 中断机制、MMU地址变换、CPU双模式、系统调用 | 第01讲_系统运行机制.ppt |
|
||||||
|
| [[02_Linux基础]] | Linux简介与使用 | UNIX/Linux历史、目录结构、文件操作、权限 | 第02讲_Linux简介与使用.pptx |
|
||||||
|
| [[03_C语言编程基础]] | Linux C编程 | 编译链接过程、gdb调试、Makefile、wrapper库 | 第03讲/Linux C编程 |
|
||||||
|
|
||||||
|
### 文件与IO
|
||||||
|
| 讲次 | 主题 | 核心内容 | 课件 |
|
||||||
|
|------|------|----------|------|
|
||||||
|
| [[04_文件IO编程]] | 文件IO编程 | open/read/write/close、mmap、dup2、文件共享 | 第04讲/实验01 |
|
||||||
|
| [[05_磁盘空间管理]] | 磁盘空间管理 | 连续/链接/索引组织、FAT12/16/32、NTFS、Ext2、RAID | 第04讲_磁盘空间管理.ppt |
|
||||||
|
|
||||||
|
### 进程与线程
|
||||||
|
| 讲次 | 主题 | 核心内容 | 课件 |
|
||||||
|
|------|------|----------|------|
|
||||||
|
| [[06_进程控制]] | 进程控制 | fork/exec/wait/exit、信号机制、shell实现 | 第05讲/实验02 |
|
||||||
|
| [[07_多线程编程]] | 多线程编程 | pthread、互斥锁、信号量、生产者消费者、并行计算 | 第06讲/实验03 |
|
||||||
|
| [[08_进程间通信]] | 进程间通信 | 管道、FIFO、消息队列、共享内存、IPC信号量 | 第07讲/实验04 |
|
||||||
|
|
||||||
|
### 网络与并发
|
||||||
|
| 讲次 | 主题 | 核心内容 | 课件 |
|
||||||
|
|------|------|----------|------|
|
||||||
|
| [[09_网络编程基础]] | 网络编程 | socket API、gethostbyname、客户端服务器模型 | 第08讲/实验05 |
|
||||||
|
| [[10_并发服务器]] | 并发网络服务器 | 多进程/多线程/预线程化服务器、sbuf缓冲区 | 第09讲/实验06 |
|
||||||
|
|
||||||
|
### 系统管理
|
||||||
|
| 讲次 | 主题 | 核心内容 | 课件 |
|
||||||
|
|------|------|----------|------|
|
||||||
|
| [[11_处理机调度]] | 处理机调度 | FCFS/SJF/HRRF/RR/优先级/MFQ、Linux CFS调度 | 第10讲_处理机调度.ppt |
|
||||||
|
| [[12_死锁]] | 死锁 | 四个必要条件、预防/避免/检测/解除、银行家算法 | 第11讲_死锁.ppt |
|
||||||
|
|
||||||
|
### 存储管理
|
||||||
|
| 讲次 | 主题 | 核心内容 | 课件 |
|
||||||
|
|------|------|----------|------|
|
||||||
|
| [[13_存储管理基础]] | 存储管理概念与程序装入 | 逻辑/物理地址、地址空间、装入链接、内核/用户空间 | 第12讲_存储管理概念与程序装入.ppt |
|
||||||
|
| [[14_分页存储管理]] | 分页存储管理 | 页表、地址变换、TLB快表、多级页表、倒转页表 | 第12讲_分页式存储管理.ppt |
|
||||||
|
| [[15_段式存储管理]] | 段式存储管理 | 分段原理、段表、段页式存储管理 | 第12讲_段式存储管理.ppt |
|
||||||
|
| [[16_虚拟存储器]] | 虚拟存储器 | 局部性原理、请求分页、页面置换算法、工作集、抖动 | 第12讲_虚拟存储器.ppt |
|
||||||
|
|
||||||
|
### 系统底层
|
||||||
|
| 讲次 | 主题 | 核心内容 | 课件 |
|
||||||
|
|------|------|----------|------|
|
||||||
|
| [[17_程序代码优化]] | 程序代码优化 | 编译优化、循环优化、缓存友好编程 | 第14讲_程序代码优化.ppt |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔬 实验指南
|
||||||
|
|
||||||
|
| 实验 | 主题 | 核心任务 | 涉及章节 |
|
||||||
|
|------|------|----------|----------|
|
||||||
|
| [[实验01_IO编程]] | Linux IO编程 | student.txt处理、结构体IO、测时、词频统计 | 第04讲 |
|
||||||
|
| [[实验02_进程控制]] | 进程控制编程 | 进程树、shell实现、daemon监控、信号管理 | 第06讲 |
|
||||||
|
| [[实验03_多线程编程]] | 多线程编程 | 3线程打印、信号量互斥、生产者消费者、并行求和 | 第07讲 |
|
||||||
|
| [[实验04_进程间通信]] | 进程间通信 | 管道通信、消息队列CS、共享内存+信号量同步 | 第08讲 |
|
||||||
|
| [[实验05_网络通信]] | 网络通信编程 | toggle测试、weblet服务器、文件下载、远程shell | 第09讲 |
|
||||||
|
| [[实验06_并发服务器]] | 并发网络应用 | 多进程/多线程/预线程化weblet、web代理 | 第10讲 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📎 附录
|
||||||
|
|
||||||
|
- [[附录A_Wrapper库参考]] — 课程提供的C语言包装库(wrapper.h/libwrapper.a)
|
||||||
|
- [[附录B_术语表]] — 操作系统核心术语速查
|
||||||
|
- [[附录C_学习路径指南]] — 根据基础定制学习方案
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📂 源代码索引
|
||||||
|
|
||||||
|
课程源代码位于 `F:\操作系统\` 下:
|
||||||
|
|
||||||
|
| 目录 | 内容 | 文件数 |
|
||||||
|
|------|------|--------|
|
||||||
|
| `chap3/` | C语言基础(hello.c, sum.c, gdbuse.c, cmdpar.c等) | ~10 |
|
||||||
|
| `chap4/` | 文件IO(cpfile.c, fcopy1.c, lseek1.c, mmap1.c, dup2.c等) | ~15 |
|
||||||
|
| `chap5/` | 进程控制(fork1-3.c, exec1.c, waitpid1.c, signal1.c, shellex.c等) | ~20 |
|
||||||
|
| `chap6/` | 多线程(pthread1.c, threadrace.c, mutex1.c, norace.c等) | ~15 |
|
||||||
|
| `chap7/` | IPC(pipe1.c, fifo1.c, shmread.c, msgsnd1.c等) | ~20 |
|
||||||
|
| `chap8/` | 网络编程(hostinfo.c, toggle.c, togglec.c等) | ~15 |
|
||||||
|
| `chap9/` | 并发服务器(toggless1-2.c, togglest.c等) | ~15 |
|
||||||
|
| `lib/` | wrapper库(wrapper.h, libwrapper.a) | 2 |
|
||||||
696
操作系统/01_系统运行机制/01_系统运行机制.md
Normal file
696
操作系统/01_系统运行机制/01_系统运行机制.md
Normal file
@@ -0,0 +1,696 @@
|
|||||||
|
# 第1.5章 系统运行机制
|
||||||
|
|
||||||
|
> [!abstract] 学习目标
|
||||||
|
> 1. 理解多进程并发运行的场景及其对硬件支持的需求
|
||||||
|
> 2. 掌握中断机制的工作原理,理解时钟中断对操作系统的核心作用
|
||||||
|
> 3. 理解 MMU(存储管理部件)如何实现进程隔离与地址转换
|
||||||
|
> 4. 掌握 CPU 双模式工作原理(内核模式 vs 用户模式)及特权指令的划分
|
||||||
|
> 5. 理解系统调用的完整流程(从用户程序到内核服务)
|
||||||
|
> 6. 理解广义中断的分类(外部中断、异常)
|
||||||
|
> 7. 能够综合运用上述机制,解释操作系统如何实现多任务并发执行
|
||||||
|
|
||||||
|
> [!info] 前置知识
|
||||||
|
> - 计算机组成原理:CPU、内存、寄存器的基本概念
|
||||||
|
> - 二进制与地址的基本概念
|
||||||
|
> - 进程的基本概念(参见 [[06_进程控制]])
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、为什么需要系统运行机制?
|
||||||
|
|
||||||
|
### 1.1 多进程并发运行场景
|
||||||
|
|
||||||
|
在现代操作系统中,多个进程需要**并发**(交替)执行。考虑两个进程 A 和 B:
|
||||||
|
|
||||||
|
```
|
||||||
|
进程 A 的代码: 进程 B 的代码:
|
||||||
|
A:1 B:1
|
||||||
|
A:2 B:2
|
||||||
|
A:3 B:3
|
||||||
|
A:4 B:4
|
||||||
|
A:5 B:5
|
||||||
|
```
|
||||||
|
|
||||||
|
实际在 CPU 上的执行顺序可能是:
|
||||||
|
|
||||||
|
```
|
||||||
|
时间线 → A:1 B:1 A:2 B:2 A:3 B:3 B:4 A:4 A:5 B:5
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!question] 核心问题
|
||||||
|
> 这种交替执行不会自然发生!CPU 只是一个顺序执行指令的"傻瓜机器",它不会自动在 A 和 B 之间切换。要实现多进程并发运行,**必须有硬件和操作系统的支持**。
|
||||||
|
|
||||||
|
实现多进程并发运行需要以下关键机制:
|
||||||
|
|
||||||
|
| 机制 | 作用 |
|
||||||
|
|------|------|
|
||||||
|
| [[#二、中断机制]] | 让 CPU 能够响应外部事件,实现进程切换的"触发器" |
|
||||||
|
| [[#三、MMU 存储管理部件]] | 实现进程间的地址隔离,防止互相干扰 |
|
||||||
|
| [[#四、CPU 工作模式]] | 区分特权操作,保护操作系统内核不被用户程序破坏 |
|
||||||
|
| [[#五、系统调用机制]] | 为用户程序提供受控的内核服务访问途径 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、中断机制
|
||||||
|
|
||||||
|
### 2.1 什么是中断?
|
||||||
|
|
||||||
|
**中断**(Interrupt)是指 CPU 在执行程序的过程中,收到来自外部设备或内部异常的信号,暂停当前程序的执行,转而去处理该事件,处理完毕后再返回原程序继续执行的过程。
|
||||||
|
|
||||||
|
> [!tip] 生活类比
|
||||||
|
> 中断就像你在看书时手机响了(外部中断),你先记住看到哪一页(保存断点),然后去接电话(处理中断),接完电话后回到那一页继续看(恢复执行)。
|
||||||
|
|
||||||
|
### 2.2 中断处理流程
|
||||||
|
|
||||||
|
CPU 处理一次中断的完整流程如下:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["CPU 正在执行用户程序"] --> B{"收到中断请求?"}
|
||||||
|
B -- "否" --> A
|
||||||
|
B -- "是" --> C["关中断(防止嵌套中断)"]
|
||||||
|
C --> D["保存断点<br>(PC、PSW 压入内核栈)"]
|
||||||
|
D --> E["保存通用寄存器<br>(保存进程现场)"]
|
||||||
|
E --> F["识别中断源<br>(查询中断向量表)"]
|
||||||
|
F --> G["跳转到中断服务程序<br>(PC ← 中断向量地址)"]
|
||||||
|
G --> H["执行中断服务程序<br>(处理具体的中断事件)"]
|
||||||
|
H --> I["恢复通用寄存器<br>(恢复进程现场)"]
|
||||||
|
I --> J["恢复断点<br>(PC、PSW 出栈)"]
|
||||||
|
J --> K["开中断"]
|
||||||
|
K --> L["继续执行用户程序"]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style C fill:#ffcdd2
|
||||||
|
style D fill:#ffcdd2
|
||||||
|
style G fill:#fff3e0
|
||||||
|
style H fill:#fff3e0
|
||||||
|
style J fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键步骤详解**:
|
||||||
|
|
||||||
|
1. **关中断**:CPU 响应中断后立即关闭中断信号接收,防止在处理一个中断时又被另一个中断打断(中断嵌套)
|
||||||
|
2. **保存断点**:将程序计数器(PC)和程序状态字(PSW)压入内核栈,记录"回来后从哪里继续执行"
|
||||||
|
3. **保存现场**:将通用寄存器的内容保存到进程的 PCB(进程控制块)中
|
||||||
|
4. **识别中断源**:通过中断向量表(Interrupt Vector Table)确定是哪个设备发出的中断
|
||||||
|
5. **执行中断服务程序**:跳转到对应的中断处理函数,完成实际的处理工作
|
||||||
|
6. **恢复现场和断点**:从 PCB 恢复寄存器,从栈中弹出 PC 和 PSW
|
||||||
|
7. **开中断**:重新允许 CPU 接收中断信号
|
||||||
|
|
||||||
|
### 2.3 时钟中断 — 操作系统的"心跳"
|
||||||
|
|
||||||
|
> [!important] 时钟中断的核心地位
|
||||||
|
> **时钟中断**(Timer Interrupt)是由实时时钟(RTC,Real-Time Clock)硬件周期性产生的中断,是操作系统实现进程管理的最关键机制。
|
||||||
|
|
||||||
|
时钟中断的工作原理:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant RTC as 实时时钟 (RTC)
|
||||||
|
participant CPU
|
||||||
|
participant OS as 操作系统
|
||||||
|
participant PA as 进程 A
|
||||||
|
participant PB as 进程 B
|
||||||
|
|
||||||
|
PA->>CPU: 正在执行进程 A
|
||||||
|
RTC->>CPU: 时钟中断(每 10ms)
|
||||||
|
CPU->>OS: 保存 A 的现场,进入内核
|
||||||
|
OS->>OS: 检查:A 的时间片用完了吗?
|
||||||
|
OS->>CPU: 切换到进程 B
|
||||||
|
CPU->>PB: 恢复 B 的现场,执行进程 B
|
||||||
|
RTC->>CPU: 时钟中断(每 10ms)
|
||||||
|
CPU->>OS: 保存 B 的现场,进入内核
|
||||||
|
OS->>CPU: 切换到进程 A
|
||||||
|
CPU->>PA: 恢复 A 的现场,执行进程 A
|
||||||
|
```
|
||||||
|
|
||||||
|
**时钟中断频率**:通常为 **10ms 一次**(即每秒 100 次),也称为 **时钟滴答**(Clock Tick)。
|
||||||
|
|
||||||
|
> [!warning] 没有时钟中断会怎样?
|
||||||
|
> 如果没有时钟中断,CPU 一旦开始执行某个进程,就永远不会主动让出 CPU。操作系统将无法实现"分时",也就无法让多个进程看起来在"同时"运行。时钟中断是操作系统实现**抢占式调度**的基础。
|
||||||
|
|
||||||
|
### 2.4 中断对操作系统的意义
|
||||||
|
|
||||||
|
中断机制对操作系统具有根本性的意义:
|
||||||
|
|
||||||
|
1. **实现进程并发执行**:时钟中断迫使 CPU 周期性地将控制权交还给操作系统,操作系统可以决定切换到哪个进程
|
||||||
|
2. **实现 I/O 管理**:设备完成 I/O 操作后通过中断通知 CPU,CPU 可以将等待该 I/O 的进程唤醒
|
||||||
|
3. **实现系统调用**:用户程序通过软中断指令(如 `int $0x80`)请求内核服务
|
||||||
|
4. **异常处理**:CPU 内部发生的异常(如除零、缺页)通过中断机制通知操作系统处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、MMU(存储管理部件)
|
||||||
|
|
||||||
|
### 3.1 为什么需要 MMU?
|
||||||
|
|
||||||
|
> [!question] 核心问题
|
||||||
|
> 如果没有地址隔离,进程 A 可以直接读写进程 B 的内存空间,甚至可以修改操作系统内核的代码!这是绝对不允许的。
|
||||||
|
|
||||||
|
**没有 MMU 时**:程序中使用的地址 = 物理地址
|
||||||
|
|
||||||
|
```
|
||||||
|
进程 A: MOV [0x1000], 1 → 直接写物理地址 0x1000
|
||||||
|
进程 B: MOV [0x1000], 2 → 也写物理地址 0x1000 !冲突!
|
||||||
|
```
|
||||||
|
|
||||||
|
**有 MMU 时**:程序中使用的地址是**逻辑地址**(虚拟地址),经过 MMU 转换后才是**物理地址**
|
||||||
|
|
||||||
|
```
|
||||||
|
进程 A: MOV [0x1000], 1 → MMU 转换 → 物理地址 0x5000
|
||||||
|
进程 B: MOV [0x1000], 2 → MMU 转换 → 物理地址 0x8000 ✓ 不冲突
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 MMU 地址转换原理
|
||||||
|
|
||||||
|
MMU 通过**地址变换函数**将不同进程的逻辑地址映射到不同的物理地址区域:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph 进程A["进程 A"]
|
||||||
|
LA["逻辑地址空间<br>0x0000 ~ 0xFFFF"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph MMU_Block["MMU(存储管理部件)"]
|
||||||
|
TR1["地址变换函数 tr1()"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph PA_Phys["物理内存"]
|
||||||
|
PA_M["区域 1<br>物理地址 0x5000 ~ 0x14FFF"]
|
||||||
|
end
|
||||||
|
|
||||||
|
LA -->|"逻辑地址 0x1000"| TR1
|
||||||
|
TR1 -->|"物理地址 0x6000"| PA_M
|
||||||
|
|
||||||
|
style LA fill:#e1f5fe
|
||||||
|
style TR1 fill:#fff3e0
|
||||||
|
style PA_M fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph 进程B["进程 B"]
|
||||||
|
LB["逻辑地址空间<br>0x0000 ~ 0xFFFF"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph MMU_Block2["MMU(存储管理部件)"]
|
||||||
|
TR2["地址变换函数 tr2()"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph PB_Phys["物理内存"]
|
||||||
|
PB_M["区域 2<br>物理地址 0x8000 ~ 0x17FFF"]
|
||||||
|
end
|
||||||
|
|
||||||
|
LB -->|"逻辑地址 0x1000"| TR2
|
||||||
|
TR2 -->|"物理地址 0x9000"| PB_M
|
||||||
|
|
||||||
|
style LB fill:#e1f5fe
|
||||||
|
style TR2 fill:#fff3e0
|
||||||
|
style PB_M fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键概念**:
|
||||||
|
|
||||||
|
| 术语 | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| **逻辑地址**(虚拟地址) | 程序中使用的地址,每个进程从 0 开始编址 |
|
||||||
|
| **物理地址** | 实际内存硬件上的地址,全局唯一 |
|
||||||
|
| **地址变换函数** | MMU 中的映射规则,每个进程有独立的变换函数 |
|
||||||
|
|
||||||
|
> [!tip] 进程隔离的本质
|
||||||
|
> 每个进程都有自己独立的**逻辑地址空间**,通过 MMU 映射到物理内存中互不重叠的区域。进程 A 无法访问进程 B 的物理内存区域,从而实现了**进程隔离**。
|
||||||
|
|
||||||
|
### 3.3 页表与 MMU 保护机制
|
||||||
|
|
||||||
|
在现代操作系统中,MMU 通常通过**页表**(Page Table)实现地址转换。页表中除了地址映射信息外,还包含保护位:
|
||||||
|
|
||||||
|
**页表项中的 U/S 位**:
|
||||||
|
|
||||||
|
| U/S 位值 | 含义 | 访问权限 |
|
||||||
|
|----------|------|----------|
|
||||||
|
| 0 | Supervisor(内核页) | 仅内核模式可访问 |
|
||||||
|
| 1 | User(用户页) | 内核模式和用户模式均可访问 |
|
||||||
|
|
||||||
|
**CPU 标志寄存器中的 IOPL 位**:
|
||||||
|
|
||||||
|
- `IOPL = 0` 或 `1`:用户模式(受限)
|
||||||
|
- `IOPL = 3`:内核模式(拥有全部 I/O 权限)
|
||||||
|
|
||||||
|
**保护机制的工作方式**:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["CPU 执行指令,访问逻辑地址"] --> B["MMU 查页表"]
|
||||||
|
B --> C{"当前 CPU 模式?"}
|
||||||
|
C -- "用户模式" --> D{"页表 U/S 位?"}
|
||||||
|
D -- "U/S = 1(用户页)" --> E["允许访问 ✓"]
|
||||||
|
D -- "U/S = 0(内核页)" --> F["触发保护异常 ✗<br>终止进程"]
|
||||||
|
C -- "内核模式" --> G["允许访问所有页面 ✓"]
|
||||||
|
|
||||||
|
style E fill:#c8e6c9
|
||||||
|
style F fill:#ffcdd2
|
||||||
|
style G fill:#c8e6c9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 Linux 地址空间划分
|
||||||
|
|
||||||
|
操作系统将每个进程的逻辑地址空间划分为**用户空间**和**内核空间**:
|
||||||
|
|
||||||
|
| 操作系统 | 用户空间 | 内核空间 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| **32 位 Linux** | 低 3GB(0x00000000 ~ 0xBFFFFFFF) | 高 1GB(0xC0000000 ~ 0xFFFFFFFF) |
|
||||||
|
| **32 位 Windows** | 低 2GB(0x00000000 ~ 0x7FFFFFFF) | 高 2GB(0x80000000 ~ 0xFFFFFFFF) |
|
||||||
|
|
||||||
|
> [!info] 为什么内核空间在高地址?
|
||||||
|
> 将内核空间放在高地址是一种约定。在 Linux 中,所有进程共享同一个内核空间映射(内核只有一份),但每个进程的用户空间是独立的。当 CPU 处于用户模式时,访问内核空间会触发保护异常。
|
||||||
|
|
||||||
|
> [!tip] 相关链接
|
||||||
|
> 关于存储管理的更多内容,参见 [[13_存储管理基础]] 和 [[14_分页存储管理]]。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、CPU 工作模式
|
||||||
|
|
||||||
|
### 4.1 双模式设计
|
||||||
|
|
||||||
|
现代 CPU 设计了两种工作模式,用于区分"谁可以执行什么指令":
|
||||||
|
|
||||||
|
| 模式 | 别名 | 权限 | 使用者 |
|
||||||
|
|------|------|------|--------|
|
||||||
|
| **内核模式** | 系统态、核心态、管态 | 可执行所有指令,访问所有内存 | 操作系统内核 |
|
||||||
|
| **用户模式** | 用户态、目态 | 只能执行非特权指令,不能访问内核空间 | 用户程序 |
|
||||||
|
|
||||||
|
> [!warning] 为什么需要两种模式?
|
||||||
|
> 如果所有程序都运行在内核模式,任何一个用户程序都可以:修改操作系统代码、直接操作硬件、访问其他进程的内存。这将导致系统完全不安全。双模式设计是操作系统**自我保护**的基础。
|
||||||
|
|
||||||
|
### 4.2 特权指令 vs 非特权指令
|
||||||
|
|
||||||
|
| 分类 | 特点 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| **特权指令** | 只能在内核模式下执行 | I/O 指令、停机指令(HLT)、特殊寄存器访问(如修改页表基址寄存器) |
|
||||||
|
| **非特权指令** | 在任何模式下均可执行 | 算术逻辑运算(ADD、SUB)、寄存器操作(MOV)、内存访问(普通读写) |
|
||||||
|
|
||||||
|
> [!warning] 用户模式执行特权指令会怎样?
|
||||||
|
> 如果 CPU 处于用户模式时执行了特权指令,CPU 会立即触发一个**保护异常**(Protection Exception),操作系统会终止该进程(通常表现为 "Segmentation Fault")。
|
||||||
|
|
||||||
|
### 4.3 不同架构的模式标识
|
||||||
|
|
||||||
|
CPU 通过标志寄存器中的特定位来标识当前工作模式:
|
||||||
|
|
||||||
|
**x86 架构**:使用 FLAGS 寄存器中的 **IOPL**(I/O Privilege Level)字段
|
||||||
|
|
||||||
|
```
|
||||||
|
FLAGS 寄存器:
|
||||||
|
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
|
||||||
|
│ ... │ IOPL│ IOPL│ ... │ IF │ ... │ ZF │ CF │
|
||||||
|
│ │ (13)│ (12)│ │ (9) │ │ (6) │ (0) │
|
||||||
|
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
||||||
|
├───IOPL───┤
|
||||||
|
00 = Ring 0(内核模式)
|
||||||
|
11 = Ring 3(用户模式)
|
||||||
|
```
|
||||||
|
|
||||||
|
**ARM 架构**:使用 CPSR(Current Program Status Register)中的 **M[4:0]** 位
|
||||||
|
|
||||||
|
```
|
||||||
|
CPSR 寄存器:
|
||||||
|
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
|
||||||
|
│ ... │ M4 │ M3 │ M2 │ M1 │ M0 │ ... │ │
|
||||||
|
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
|
||||||
|
├─── M[4:0] ───┤
|
||||||
|
10000 = User(用户模式)
|
||||||
|
10011 = Supervisor(内核模式)
|
||||||
|
10111 = Abort
|
||||||
|
11011 = Undefined
|
||||||
|
10010 = IRQ
|
||||||
|
10001 = FIQ
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 CPU 模式切换
|
||||||
|
|
||||||
|
CPU 模式切换发生在以下场景:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph 用户模式区["用户模式(User Mode)"]
|
||||||
|
A["执行用户程序"]
|
||||||
|
B["执行非特权指令"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 内核模式区["内核模式(Kernel Mode)"]
|
||||||
|
C["处理中断/异常"]
|
||||||
|
D["执行系统调用"]
|
||||||
|
E["执行特权指令"]
|
||||||
|
end
|
||||||
|
|
||||||
|
A -->|"中断/异常发生<br>(时钟中断、I/O中断、缺页等)"| C
|
||||||
|
A -->|"系统调用<br>(int $0x80 / SWI)"| D
|
||||||
|
C -->|"中断返回<br>(IRET)"| A
|
||||||
|
D -->|"系统调用返回<br>(IRET)"| A
|
||||||
|
E -->|"设置 IOPL/M[4:0]"| A
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style B fill:#e1f5fe
|
||||||
|
style C fill:#fff3e0
|
||||||
|
style D fill:#fff3e0
|
||||||
|
style E fill:#fff3e0
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户模式 → 内核模式**(两种途径):
|
||||||
|
|
||||||
|
1. **中断/异常**:硬件自动切换
|
||||||
|
- 时钟中断、I/O 中断
|
||||||
|
- 缺页异常、除零异常
|
||||||
|
- 系统调用(软中断)
|
||||||
|
|
||||||
|
2. **系统调用**:用户程序主动发起
|
||||||
|
- x86:执行 `int $0x80` 指令
|
||||||
|
- ARM:执行 `SWI` 指令
|
||||||
|
|
||||||
|
**内核模式 → 用户模式**:
|
||||||
|
|
||||||
|
- 执行中断返回指令(x86: `IRET`),恢复用户模式的状态
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、系统调用机制
|
||||||
|
|
||||||
|
### 5.1 什么是系统调用?
|
||||||
|
|
||||||
|
**系统调用**(System Call)是操作系统提供给用户程序的**唯一合法途径**,用于请求内核提供的服务。
|
||||||
|
|
||||||
|
> [!tip] 生活类比
|
||||||
|
> 系统调用就像银行柜台:你(用户程序)不能自己进入金库(内核空间)取钱,但你可以通过柜台窗口(系统调用接口)告诉工作人员(内核)你要做什么,工作人员帮你完成后再把结果交给你。
|
||||||
|
|
||||||
|
### 5.2 系统调用的必要性
|
||||||
|
|
||||||
|
用户程序**不能直接调用**内核函数,原因:
|
||||||
|
|
||||||
|
1. **地址隔离**:内核函数位于内核空间,用户模式无法访问
|
||||||
|
2. **权限保护**:内核函数可能执行特权操作(如 I/O),用户模式无权执行
|
||||||
|
3. **安全控制**:内核需要验证参数的合法性,防止恶意调用
|
||||||
|
|
||||||
|
### 5.3 系统调用的完整流程
|
||||||
|
|
||||||
|
以 `printf("Hello")` 为例,完整调用链如下:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["用户程序<br>printf(#quot;Hello#quot;)"] --> B["C 库函数<br>write(1, #quot;Hello#quot;, 5)"]
|
||||||
|
B --> C["系统调用包装函数<br>将参数写入寄存器<br>EAX=4 (sys_write 号)<br>EBX=1 (文件描述符)<br>ECX=缓冲区地址<br>EDX=5 (长度)"]
|
||||||
|
C --> D["软中断指令<br>int $0x80"]
|
||||||
|
D --> E["CPU 模式切换<br>用户模式 → 内核模式<br>保存断点和现场"]
|
||||||
|
E --> F["查询系统调用表<br>根据 EAX=4 找到<br>sys_write 函数地址"]
|
||||||
|
F --> G["执行内核函数<br>sys_write()<br>实际完成屏幕输出"]
|
||||||
|
G --> H["系统调用返回<br>恢复现场<br>内核模式 → 用户模式"]
|
||||||
|
H --> I["返回用户程序<br>继续执行"]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style B fill:#e1f5fe
|
||||||
|
style C fill:#e1f5fe
|
||||||
|
style D fill:#ffcdd2
|
||||||
|
style E fill:#fff3e0
|
||||||
|
style F fill:#fff3e0
|
||||||
|
style G fill:#fff3e0
|
||||||
|
style H fill:#ffcdd2
|
||||||
|
style I fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 系统调用的参数传递方式
|
||||||
|
|
||||||
|
用户程序通过**寄存器**将参数传递给内核:
|
||||||
|
|
||||||
|
**x86 架构的系统调用参数寄存器**:
|
||||||
|
|
||||||
|
| 寄存器 | 用途 |
|
||||||
|
|--------|------|
|
||||||
|
| **EAX** | 系统调用号(标识调用哪个内核函数) |
|
||||||
|
| **EBX** | 第 1 个参数 |
|
||||||
|
| **ECX** | 第 2 个参数 |
|
||||||
|
| **EDX** | 第 3 个参数 |
|
||||||
|
| **ESI** | 第 4 个参数 |
|
||||||
|
| **EDI** | 第 5 个参数 |
|
||||||
|
| **EBP** | 第 6 个参数 |
|
||||||
|
|
||||||
|
> [!info] `int $0x80` 指令的作用
|
||||||
|
> `int $0x80` 是 x86 的**软中断指令**(Software Interrupt)。执行该指令时,CPU 会:
|
||||||
|
> 1. 自动将 FLAGS、CS、IP 压入内核栈(保存断点)
|
||||||
|
> 2. 将 CPU 模式切换为内核模式
|
||||||
|
> 3. 从中断向量表的第 0x80 号表项取出中断服务程序入口地址
|
||||||
|
> 4. 跳转到该地址开始执行(即系统调用处理函数 `system_call`)
|
||||||
|
|
||||||
|
### 5.5 系统调用的底层实现
|
||||||
|
|
||||||
|
```asm
|
||||||
|
; x86 Linux 系统调用的底层实现示意
|
||||||
|
|
||||||
|
; 用户程序部分(C 库包装)
|
||||||
|
mov eax, 4 ; 系统调用号 = 4 (sys_write)
|
||||||
|
mov ebx, 1 ; 文件描述符 = 1 (stdout)
|
||||||
|
mov ecx, msg ; 缓冲区地址
|
||||||
|
mov edx, 5 ; 字节数
|
||||||
|
int 0x80 ; 触发软中断,进入内核
|
||||||
|
|
||||||
|
; 内核部分(system_call)
|
||||||
|
system_call:
|
||||||
|
; 1. 保存所有寄存器到内核栈
|
||||||
|
push eax
|
||||||
|
push ebx
|
||||||
|
push ecx
|
||||||
|
push edx
|
||||||
|
; ...
|
||||||
|
|
||||||
|
; 2. 根据 EAX 的值查系统调用表
|
||||||
|
call [sys_call_table + eax*4]
|
||||||
|
|
||||||
|
; 3. 返回值放在 EAX 中
|
||||||
|
mov [esp + 恢复位置], eax
|
||||||
|
|
||||||
|
; 4. 恢复寄存器,返回用户模式
|
||||||
|
pop edx
|
||||||
|
pop ecx
|
||||||
|
pop ebx
|
||||||
|
pop eax
|
||||||
|
iret
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.6 常见的系统调用
|
||||||
|
|
||||||
|
| 系统调用号 | 函数名 | 功能 |
|
||||||
|
|-----------|--------|------|
|
||||||
|
| 1 | `sys_exit` | 终止进程 |
|
||||||
|
| 2 | `sys_fork` | 创建子进程 |
|
||||||
|
| 3 | `sys_read` | 读文件 |
|
||||||
|
| 4 | `sys_write` | 写文件 |
|
||||||
|
| 5 | `sys_open` | 打开文件 |
|
||||||
|
| 6 | `sys_close` | 关闭文件 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、异常(广义中断)
|
||||||
|
|
||||||
|
### 6.1 广义中断的分类
|
||||||
|
|
||||||
|
在操作系统中,**广义的中断**包括两大类:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["广义中断"] --> B["外部中断<br>(硬件中断)"]
|
||||||
|
A --> C["异常<br>(内部中断)"]
|
||||||
|
|
||||||
|
B --> B1["I/O 中断<br>键盘、鼠标、网卡等设备发出"]
|
||||||
|
B --> B2["时钟中断<br>RTC 周期性发出"]
|
||||||
|
|
||||||
|
C --> C1["系统调用<br>用户执行 int $0x80 / SWI"]
|
||||||
|
C --> C2["缺页异常<br>访问的页面不在内存中"]
|
||||||
|
C --> C3["断点指令<br>调试器设置的断点"]
|
||||||
|
C --> C4["算术溢出<br>除零错误等"]
|
||||||
|
|
||||||
|
style A fill:#fff3e0
|
||||||
|
style B fill:#e1f5fe
|
||||||
|
style C fill:#ffcdd2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 外部中断 vs 异常
|
||||||
|
|
||||||
|
| 特征 | 外部中断 | 异常 |
|
||||||
|
|------|----------|------|
|
||||||
|
| **来源** | CPU 外部设备 | CPU 内部执行指令时产生 |
|
||||||
|
| **发生时机** | 与当前指令无关,随机发生 | 与当前指令直接相关 |
|
||||||
|
| **是否可屏蔽** | 可屏蔽中断(INTR)可被 IF 位屏蔽 | 不可屏蔽 |
|
||||||
|
| **典型例子** | I/O 中断、时钟中断 | 缺页、除零、系统调用 |
|
||||||
|
|
||||||
|
### 6.3 常见异常类型
|
||||||
|
|
||||||
|
| 异常类型 | 触发条件 | 处理方式 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| **系统调用** | 执行 `int $0x80` 或 `SWI` | 查系统调用表,执行对应内核函数 |
|
||||||
|
| **缺页异常** | 访问的虚拟页面不在物理内存中 | 从磁盘调入页面(参见 [[14_分页存储管理]]) |
|
||||||
|
| **断点指令** | 调试器在代码中设置断点 | 暂停程序,将控制权交给调试器 |
|
||||||
|
| **算术溢出** | 除零、溢出等运算错误 | 终止进程或通知进程处理 |
|
||||||
|
| **保护异常** | 用户模式执行特权指令或访问内核空间 | 终止进程(Segmentation Fault) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、多任务并发执行的综合场景
|
||||||
|
|
||||||
|
### 7.1 操作系统如何实现多任务并发
|
||||||
|
|
||||||
|
操作系统通过**中断驱动**的方式实现多任务并发执行。以下是几种典型的触发场景:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["CPU 正在执行进程 P1"] --> B{"发生了什么?"}
|
||||||
|
|
||||||
|
B -->|"时钟中断(每 10ms)"| C["进入内核<br>调度器决定是否切换进程"]
|
||||||
|
B -->|"系统调用"| D["进入内核<br>执行内核函数"]
|
||||||
|
B -->|"I/O 请求"| E["进程 P1 等待 I/O<br>CPU 分配给其他进程"]
|
||||||
|
B -->|"进程结束"| F["进程 P1 终止<br>CPU 分配给其他进程"]
|
||||||
|
|
||||||
|
C --> G{"调度器决策"}
|
||||||
|
G -->|"继续执行 P1"| A
|
||||||
|
G -->|"切换到 P2"| H["保存 P1 现场<br>恢复 P2 现场<br>执行进程 P2"]
|
||||||
|
|
||||||
|
D --> I{"系统调用是否阻塞?"}
|
||||||
|
I -->|"不阻塞"| A
|
||||||
|
I -->|"需要等待"| E
|
||||||
|
|
||||||
|
E --> J["P1 进入等待队列<br>CPU 执行进程 P2"]
|
||||||
|
F --> K["回收 P1 资源<br>CPU 执行就绪队列中的进程"]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style C fill:#fff3e0
|
||||||
|
style D fill:#fff3e0
|
||||||
|
style E fill:#ffcdd2
|
||||||
|
style F fill:#ffcdd2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 四种典型场景详解
|
||||||
|
|
||||||
|
**场景一:时钟中断触发进程切换**
|
||||||
|
|
||||||
|
```
|
||||||
|
时间线:
|
||||||
|
P1 执行 → [时钟中断] → OS 调度 → P2 执行 → [时钟中断] → OS 调度 → P1 执行
|
||||||
|
↑ 10ms ↑ 10ms
|
||||||
|
```
|
||||||
|
|
||||||
|
**场景二:系统调用触发内核执行**
|
||||||
|
|
||||||
|
```
|
||||||
|
P1 执行 printf → [int $0x80] → 内核执行 sys_write → [iret] → P1 继续执行
|
||||||
|
```
|
||||||
|
|
||||||
|
**场景三:I/O 等待导致 CPU 重分配**
|
||||||
|
|
||||||
|
```
|
||||||
|
P1 请求读磁盘 → P1 进入等待队列 → CPU 执行 P2 → 磁盘中断 → P1 就绪 → P1 继续执行
|
||||||
|
```
|
||||||
|
|
||||||
|
**场景四:进程结束释放 CPU**
|
||||||
|
|
||||||
|
```
|
||||||
|
P1 执行 exit() → P1 终止 → OS 回收资源 → CPU 执行就绪队列中的 P2
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!important] 中断是操作系统的"发动机"
|
||||||
|
> 可以说,**没有中断就没有操作系统**。中断机制是操作系统实现进程管理、I/O 管理、内存管理等所有核心功能的基础。时钟中断让 OS 能够"定期检查"系统状态,I/O 中断让 OS 能够响应设备事件,系统调用让 OS 能够为用户程序提供服务。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、机制之间的关系
|
||||||
|
|
||||||
|
理解了各个机制后,需要看到它们之间的协同关系:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
subgraph 硬件支持
|
||||||
|
CLK["时钟中断<br>(RTC 硬件)"]
|
||||||
|
MMU_HW["MMU 硬件<br>(地址转换)"]
|
||||||
|
CPU_MODE["CPU 双模式<br>(IOPL/M[4:0])"]
|
||||||
|
INT_HW["中断控制器<br>(PIC/APIC)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph OS核心功能
|
||||||
|
PROC["进程管理<br>(调度、切换)"]
|
||||||
|
MEM["内存管理<br>(地址隔离)"]
|
||||||
|
PROT["系统保护<br>(内核安全)"]
|
||||||
|
IO["I/O 管理<br>(设备驱动)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
CLK -->|"触发进程切换"| PROC
|
||||||
|
MMU_HW -->|"地址转换 + 保护"| MEM
|
||||||
|
CPU_MODE -->|"特权级隔离"| PROT
|
||||||
|
INT_HW -->|"设备事件通知"| IO
|
||||||
|
|
||||||
|
PROC -->|"选择下一个进程"| MEM
|
||||||
|
MEM -->|"提供地址空间"| PROC
|
||||||
|
PROT -->|"限制用户操作"| PROC
|
||||||
|
|
||||||
|
style CLK fill:#ffcdd2
|
||||||
|
style MMU_HW fill:#ffcdd2
|
||||||
|
style CPU_MODE fill:#ffcdd2
|
||||||
|
style INT_HW fill:#ffcdd2
|
||||||
|
style PROC fill:#e1f5fe
|
||||||
|
style MEM fill:#e1f5fe
|
||||||
|
style PROT fill:#e1f5fe
|
||||||
|
style IO fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!tip] 核心要点
|
||||||
|
> 操作系统的四大运行机制(中断、MMU、CPU 模式、系统调用)不是孤立的,而是相互配合:
|
||||||
|
> - **中断**提供了"进入内核"的途径
|
||||||
|
> - **CPU 模式**确保了"谁能做什么"
|
||||||
|
> - **MMU** 保证了"谁的内存谁用"
|
||||||
|
> - **系统调用**提供了"受控的内核访问"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 思考与练习
|
||||||
|
|
||||||
|
以下是 PPT 中给出的复习思考题:
|
||||||
|
|
||||||
|
1. **什么是逻辑地址、物理地址?什么是内核空间、用户空间?**
|
||||||
|
- 提示:逻辑地址是程序中使用的地址,物理地址是内存硬件的实际地址;内核空间是操作系统运行的区域,用户空间是应用程序运行的区域
|
||||||
|
|
||||||
|
2. **MMU 的功能是什么,如何实现进程隔离?**
|
||||||
|
- 提示:MMU 负责逻辑地址到物理地址的转换;每个进程有独立的地址变换函数,映射到不同的物理区域
|
||||||
|
|
||||||
|
3. **特权指令与非特权指令的特点?**
|
||||||
|
- 提示:特权指令只能在内核模式执行(如 I/O 指令、停机指令),非特权指令在任何模式都可执行(如算术运算)
|
||||||
|
|
||||||
|
4. **两种 CPU 工作模式的特点?**
|
||||||
|
- 提示:内核模式可执行所有指令、访问所有内存;用户模式只能执行非特权指令、不能访问内核空间
|
||||||
|
|
||||||
|
5. **如何利用 MMU 与工作模式防止用户程序破坏 OS?**
|
||||||
|
- 提示:MMU 通过页表 U/S 位将内核页标记为仅内核模式可访问;CPU 在用户模式执行特权指令会触发保护异常
|
||||||
|
|
||||||
|
6. **CPU 模式切换方法?**
|
||||||
|
- 提示:用户→内核:中断/异常/系统调用(硬件自动切换);内核→用户:执行 IRET 指令
|
||||||
|
|
||||||
|
7. **系统调用的作用、原理和实现过程?**
|
||||||
|
- 提示:作用是让安全地访问内核服务;原理是通过软中断指令触发模式切换;实现过程是参数通过寄存器传递,查系统调用表找到对应函数执行
|
||||||
|
|
||||||
|
8. **时钟中断对操作系统的作用?**
|
||||||
|
- 提示:时钟中断是操作系统实现进程并发执行的基础,它迫使 CPU 周期性地将控制权交还给 OS,使 OS 能够进行进程调度
|
||||||
|
|
||||||
|
9. **中断对操作系统的意义?**
|
||||||
|
- 提示:中断是操作系统的"发动机",是实现进程管理、I/O 管理、系统调用等所有核心功能的基础
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关笔记
|
||||||
|
|
||||||
|
- [[02_Linux基础]] — Linux 系统的基本使用
|
||||||
|
- [[06_进程控制]] — 进程的创建、终止与管理
|
||||||
|
- [[11_处理机调度]] — 进程调度算法(时钟中断的实际应用)
|
||||||
|
- [[13_存储管理基础]] — 存储管理的基本概念
|
||||||
|
- [[14_分页存储管理]] — 分页存储与 MMU 的详细实现
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 参考资料
|
||||||
|
|
||||||
|
- 《操作系统概念》(Operating System Concepts)第1章、第2章
|
||||||
|
- 《深入理解计算机系统》(CSAPP)第1章、第9章
|
||||||
|
- 《现代操作系统》(Modern Operating Systems)第2章
|
||||||
353
操作系统/02_Linux基础/02_Linux基础.md
Normal file
353
操作系统/02_Linux基础/02_Linux基础.md
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
# 第02讲:Linux操作系统目录结构与文件操作
|
||||||
|
|
||||||
|
> **本节目标**:理解Linux系统的起源与设计哲学,掌握目录组织方式和常用文件操作命令,为后续系统编程打下基础
|
||||||
|
|
||||||
|
## 前置知识
|
||||||
|
- [[01_系统运行机制]] -- 对操作系统的基本认识
|
||||||
|
- 计算机基本操作能力
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、UNIX/Linux系统简介
|
||||||
|
|
||||||
|
### 1.1 UNIX的诞生
|
||||||
|
|
||||||
|
1969年,贝尔实验室(Bell Labs)的 **Ken Thompson** 和 **Dennis Ritchie** 在一台废弃的PDP-7小型机上开发了UNIX操作系统。此后Ritchie还创造了C语言,并用C语言重写了UNIX,使UNIX成为第一个用高级语言编写的操作系统,极大地提高了可移植性。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A["1969 Ken Thompson
|
||||||
|
Dennis Ritchie"] -->|在PDP-7上开发| B[UNIX初版]
|
||||||
|
B -->|用C语言重写| C[UNIX V6/V7]
|
||||||
|
C -->|研究方向| D[BSD UNIX]
|
||||||
|
C -->|商业方向| E[System V]
|
||||||
|
D --> F[FreeBSD / NetBSD]
|
||||||
|
E --> G[AIX / Solaris / HP-UX]
|
||||||
|
|
||||||
|
style A fill:#fff3e0
|
||||||
|
style C fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 UNIX两大流派
|
||||||
|
|
||||||
|
| 流派 | 代表 | 特点 |
|
||||||
|
|------|------|------|
|
||||||
|
| **BSD UNIX**(研究版) | BSD 4.x | 由加州大学伯克利分校维护,偏重学术研究 |
|
||||||
|
| **System V**(商业版) | SVR4 | 由AT&T维护,偏重商业应用 |
|
||||||
|
|
||||||
|
### 1.3 主要商业UNIX版本
|
||||||
|
|
||||||
|
| 版本 | 厂商 | 典型硬件平台 |
|
||||||
|
|------|------|-------------|
|
||||||
|
| **AIX** | IBM | Power/PowerPC |
|
||||||
|
| **Solaris** | Sun Microsystems | SPARC / x86 |
|
||||||
|
| **HP-UX** | HP | PA-RISC / Itanium |
|
||||||
|
| **IRIX** | SGI | MIPS |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、Linux系统
|
||||||
|
|
||||||
|
### 2.1 Linux的诞生
|
||||||
|
|
||||||
|
1991年,芬兰赫尔辛基大学的学生 **林纳斯·托瓦兹(Linus Torvalds)** 开发了Linux内核。最初这只是个人项目,但由于他将内核源码以GPL(GNU General Public License)协议发布,全球开发者得以自由修改和分发,Linux迅速成长为最重要的开源操作系统。
|
||||||
|
|
||||||
|
### 2.2 内核版本号
|
||||||
|
|
||||||
|
Linux内核版本号的格式为 **`r.x.y`**:
|
||||||
|
|
||||||
|
- **r**(主版本号):内核有重大变更时递增
|
||||||
|
- **x**(次版本号):偶数表示稳定版,奇数表示开发版
|
||||||
|
- **y**(修订版本号):bug修复和小改动
|
||||||
|
|
||||||
|
常见内核版本线:2.4 -> 2.6 -> 3.2 -> 4.6.4 -> 5.x -> 6.x
|
||||||
|
|
||||||
|
> **注意**:从3.0开始,主版本号不再有"奇偶"含义,版本号只是简单的递增计数器。
|
||||||
|
|
||||||
|
### 2.3 Linux发行版
|
||||||
|
|
||||||
|
内核本身只是操作系统的核心部分。**发行版(Distribution)** 将内核与各种工具、桌面环境、软件包管理器等打包在一起,形成完整的操作系统。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
A[Linux内核] --> B[GNU工具集]
|
||||||
|
B --> C[发行版]
|
||||||
|
C --> D[RHEL]
|
||||||
|
C --> E[Fedora]
|
||||||
|
C --> F[Ubuntu]
|
||||||
|
C --> G[CentOS]
|
||||||
|
C --> H[中标麒麟]
|
||||||
|
|
||||||
|
style A fill:#ffcdd2
|
||||||
|
style C fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
| 发行版 | 特点 | 典型用途 |
|
||||||
|
|--------|------|---------|
|
||||||
|
| **RHEL**(Red Hat Enterprise Linux) | 商业支持,稳定 | 企业服务器 |
|
||||||
|
| **Fedora** | 技术前沿,更新快 | 桌面 / 开发 |
|
||||||
|
| **Ubuntu** | 用户友好,社区活跃 | 桌面 / 云服务器 |
|
||||||
|
| **CentOS** | RHEL的免费克隆版 | 企业服务器 |
|
||||||
|
| **中标麒麟** | 国产化,政府认证 | 国内政府 / 国防 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、Linux目录结构
|
||||||
|
|
||||||
|
### 3.1 核心概念:一切皆文件
|
||||||
|
|
||||||
|
与Windows不同,Linux **没有盘符**(C:、D:等)的概念。整个文件系统从根目录 **`/`** 开始,形成一棵倒置的树。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
ROOT["/ (根目录)"] --> BIN["/bin
|
||||||
|
基本命令"]
|
||||||
|
ROOT --> SBIN["/sbin
|
||||||
|
系统管理命令"]
|
||||||
|
ROOT --> ETC["/etc
|
||||||
|
配置文件"]
|
||||||
|
ROOT --> HOME["/home
|
||||||
|
用户家目录"]
|
||||||
|
ROOT --> VAR["/var
|
||||||
|
可变数据"]
|
||||||
|
ROOT --> USR["/usr
|
||||||
|
用户程序"]
|
||||||
|
ROOT --> TMP["/tmp
|
||||||
|
临时文件"]
|
||||||
|
ROOT --> DEV["/dev
|
||||||
|
设备文件"]
|
||||||
|
ROOT --> PROC["/proc
|
||||||
|
进程信息"]
|
||||||
|
ROOT --> BOOT["/boot
|
||||||
|
内核与启动"]
|
||||||
|
ROOT --> LIB["/lib
|
||||||
|
共享库"]
|
||||||
|
ROOT --> MNT["/mnt
|
||||||
|
挂载点"]
|
||||||
|
ROOT --> OPT["/opt
|
||||||
|
第三方软件"]
|
||||||
|
ROOT --> ROOT2["/root
|
||||||
|
root用户家目录"]
|
||||||
|
|
||||||
|
style ROOT fill:#ffcdd2
|
||||||
|
style ETC fill:#fff3e0
|
||||||
|
style HOME fill:#e8f5e9
|
||||||
|
style PROC fill:#e1f5fe
|
||||||
|
style DEV fill:#f3e5f5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 重要目录详解
|
||||||
|
|
||||||
|
| 目录 | 全称 | 作用 | 举例 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `/bin` | Binaries | 基本用户命令 | `ls`, `cp`, `cat` |
|
||||||
|
| `/sbin` | System Binaries | 系统管理命令 | `fdisk`, `ifconfig` |
|
||||||
|
| `/etc` | Editable Text Config | 系统配置文件 | `/etc/passwd`, `/etc/fstab` |
|
||||||
|
| `/home` | Home | 普通用户家目录 | `/home/zhangsan` |
|
||||||
|
| `/root` | Root Home | root用户的家目录 | -- |
|
||||||
|
| `/var` | Variable | 可变数据(日志、缓存等) | `/var/log/messages` |
|
||||||
|
| `/usr` | Unix System Resources | 用户程序和数据 | `/usr/bin`, `/usr/lib` |
|
||||||
|
| `/tmp` | Temporary | 临时文件,重启后可能清除 | -- |
|
||||||
|
| `/dev` | Device | 设备文件 | `/dev/sda`(硬盘) |
|
||||||
|
| `/proc` | Process | 虚拟文件系统,内核运行信息 | `/proc/cpuinfo` |
|
||||||
|
| `/boot` | Boot | 启动相关文件(内核镜像等) | `vmlinuz-xxx` |
|
||||||
|
| `/lib` | Library | 共享库文件 | `libc.so.6` |
|
||||||
|
| `/mnt` | Mount | 临时挂载点 | U盘、网络存储 |
|
||||||
|
|
||||||
|
### 3.3 /proc文件系统
|
||||||
|
|
||||||
|
`/proc` 是一个 **虚拟文件系统** -- 它不占用磁盘空间,而是内核在内存中动态生成的。通过读取 `/proc` 下的文件,可以实时查看系统和进程信息。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看CPU信息
|
||||||
|
cat /proc/cpuinfo
|
||||||
|
|
||||||
|
# 查看内存信息
|
||||||
|
cat /proc/meminfo
|
||||||
|
|
||||||
|
# 查看当前进程信息(PID=1的进程)
|
||||||
|
cat /proc/1/status
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、目录操作命令
|
||||||
|
|
||||||
|
### 4.1 特殊目录符号
|
||||||
|
|
||||||
|
| 符号 | 含义 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `.` | 当前目录 | `./program`(运行当前目录下的程序) |
|
||||||
|
| `..` | 上一级目录 | `cd ..`(回到上级目录) |
|
||||||
|
| `~` | 当前用户的家目录 | `cd ~`(回到家目录) |
|
||||||
|
| `-` | 上一次所在的目录 | `cd -`(在两个目录间来回切换) |
|
||||||
|
|
||||||
|
### 4.2 常用目录操作命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# pwd - 显示当前工作目录(Print Working Directory)
|
||||||
|
pwd
|
||||||
|
# 输出:/home/zhangsan
|
||||||
|
|
||||||
|
# cd - 切换目录(Change Directory)
|
||||||
|
cd /usr/local # 绝对路径切换
|
||||||
|
cd .. # 回到上级目录
|
||||||
|
cd ~ # 回到家目录(等同于 cd $HOME)
|
||||||
|
cd - # 回到上一次的目录
|
||||||
|
|
||||||
|
# mkdir - 创建目录(Make Directory)
|
||||||
|
mkdir mydir # 创建单个目录
|
||||||
|
mkdir -p project/src/main # 递归创建多级目录
|
||||||
|
|
||||||
|
# rmdir - 删除空目录(Remove Directory)
|
||||||
|
rmdir mydir # 只能删除空目录
|
||||||
|
|
||||||
|
# rm - 删除文件或目录
|
||||||
|
rm file.txt # 删除文件
|
||||||
|
rm -r mydir # 递归删除目录及其内容
|
||||||
|
rm -ri mydir # 递归删除,每个文件都询问确认
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、文件操作命令
|
||||||
|
|
||||||
|
### 5.1 查看文件内容
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ls - 列出目录内容(List)
|
||||||
|
ls # 列出当前目录下的文件
|
||||||
|
ls -l # 长格式显示(含权限、大小、时间)
|
||||||
|
ls -a # 显示隐藏文件(以 . 开头的文件)
|
||||||
|
ls -lh # 长格式 + 人类可读的文件大小
|
||||||
|
|
||||||
|
# cat - 连接并显示文件内容
|
||||||
|
cat file.txt # 显示文件全部内容
|
||||||
|
cat -n file.txt # 显示时带行号
|
||||||
|
|
||||||
|
# more / less - 分页查看
|
||||||
|
more file.txt # 空格翻页,q 退出
|
||||||
|
less file.txt # 更强大的分页器,支持搜索
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 复制、移动和重命名
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# cp - 复制文件或目录(Copy)
|
||||||
|
cp file1.txt file2.txt # 复制文件
|
||||||
|
cp -r dir1/ dir2/ # 递归复制目录
|
||||||
|
cp -i file1.txt file2.txt # 覆盖前询问
|
||||||
|
|
||||||
|
# mv - 移动/重命名(Move)
|
||||||
|
mv file.txt /tmp/ # 移动文件到 /tmp
|
||||||
|
mv oldname.txt newname.txt # 重命名
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 文件权限管理
|
||||||
|
|
||||||
|
每个文件都有三组权限,分别针对三类用户:
|
||||||
|
|
||||||
|
| 权限位 | 含义 | 对文件的效果 | 对目录的效果 |
|
||||||
|
|--------|------|-------------|-------------|
|
||||||
|
| **r**(读) | Read | 可以查看文件内容 | 可以列出目录内容 |
|
||||||
|
| **w**(写) | Write | 可以修改文件内容 | 可以在目录中创建/删除文件 |
|
||||||
|
| **x**(执行) | Execute | 可以运行文件 | 可以进入目录(`cd`) |
|
||||||
|
|
||||||
|
三类用户:
|
||||||
|
- **u**(user/所有者):文件的拥有者
|
||||||
|
- **g**(group/所属组):与文件同组的用户
|
||||||
|
- **o**(others/其他人):其余所有用户
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# chmod - 修改文件权限(Change Mode)
|
||||||
|
chmod u+x program.sh # 给所有者添加执行权限
|
||||||
|
chmod g-w file.txt # 去掉组的写权限
|
||||||
|
chmod 755 program.sh # 数字表示法:rwxr-xr-x
|
||||||
|
chmod 644 data.txt # 数字表示法:rw-r--r--
|
||||||
|
|
||||||
|
# chown - 修改文件所有者(Change Owner)
|
||||||
|
chown zhangsan file.txt # 修改所有者
|
||||||
|
chown zhangsan:staff file.txt # 同时修改所有者和所属组
|
||||||
|
chown -R zhangsan:staff mydir/ # 递归修改目录
|
||||||
|
```
|
||||||
|
|
||||||
|
**权限的数字表示法**:
|
||||||
|
|
||||||
|
| 数字 | 二进制 | 权限 |
|
||||||
|
|------|--------|------|
|
||||||
|
| 7 | 111 | rwx |
|
||||||
|
| 6 | 110 | rw- |
|
||||||
|
| 5 | 101 | r-x |
|
||||||
|
| 4 | 100 | r-- |
|
||||||
|
| 0 | 000 | --- |
|
||||||
|
|
||||||
|
因此 `chmod 755` 等价于 `rwxr-xr-x`:所有者可读可写可执行,组和其他人可读可执行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、绝对路径与相对路径
|
||||||
|
|
||||||
|
### 6.1 概念对比
|
||||||
|
|
||||||
|
| 路径类型 | 起点 | 示例 | 特点 |
|
||||||
|
|----------|------|------|------|
|
||||||
|
| **绝对路径** | 根目录 `/` | `/home/zhangsan/file.txt` | 完整路径,从根开始,与当前位置无关 |
|
||||||
|
| **相对路径** | 当前目录 | `./file.txt` 或 `../dir/file.txt` | 相对于当前位置,更短更灵活 |
|
||||||
|
|
||||||
|
### 6.2 示例
|
||||||
|
|
||||||
|
假设当前目录为 `/home/zhangsan/project`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 绝对路径方式
|
||||||
|
cat /home/zhangsan/project/src/main.c
|
||||||
|
|
||||||
|
# 相对路径方式(效果相同)
|
||||||
|
cat ./src/main.c
|
||||||
|
cat src/main.c
|
||||||
|
|
||||||
|
# 使用 .. 回到上级
|
||||||
|
cat ../README.md # 访问上级目录中的 README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
HOME["/home"] --> ZS["zhangsan"]
|
||||||
|
ZS --> PROJ["project"]
|
||||||
|
PROJ --> SRC["src"]
|
||||||
|
SRC --> MAIN["main.c"]
|
||||||
|
ZS --> README["README.md"]
|
||||||
|
|
||||||
|
PROJ -.->|"cd .."| ZS
|
||||||
|
PROJ -.->|"cd ./src"| SRC
|
||||||
|
PROJ -.->|"cat ../README.md"| README
|
||||||
|
|
||||||
|
style PROJ fill:#fff3e0
|
||||||
|
style MAIN fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、知识关联
|
||||||
|
|
||||||
|
- Linux的"一切皆文件"思想在 [[04_文件IO编程]] 中会深入学习
|
||||||
|
- 文件权限在 [[04_文件IO编程]] 中用 `open()` 系统调用的 `mode` 参数体现
|
||||||
|
- `/proc` 文件系统在 [[05_进程控制]] 中用于查看进程状态
|
||||||
|
- 后续C语言编程需要在Linux环境下进行,参见 [[03_C语言编程基础]]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、思考题
|
||||||
|
|
||||||
|
1. **为什么Linux没有盘符?** 这种设计有什么优势?
|
||||||
|
2. **/proc 为什么是"虚拟"文件系统?** 它与 /home 这样的目录有什么本质区别?
|
||||||
|
3. **`rm -rf /` 会发生什么?** 为什么root用户执行这个命令非常危险?
|
||||||
|
4. **`chmod 755` 和 `chmod 644` 分别适用于什么场景?**(提示:可执行文件 vs 数据文件)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、扩展阅读
|
||||||
|
|
||||||
|
- 《鸟哥的Linux私房菜》第5-6章:Linux文件权限与目录配置
|
||||||
|
- 《UNIX环境高级编程》第1-2章:UNIX基础知识
|
||||||
|
- Linux文件系统层次标准(FHS):https://pathname.com/fhs/
|
||||||
631
操作系统/03_C语言编程基础/03_C语言编程基础.md
Normal file
631
操作系统/03_C语言编程基础/03_C语言编程基础.md
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
# 第03讲:C语言编程基础 -- Linux环境下的编译、调试与程序结构
|
||||||
|
|
||||||
|
> **本节目标**:掌握Linux环境下C语言程序的编译链接过程、可执行程序的内存结构、gdb调试方法和Makefile编写,为后续操作系统编程打下基础
|
||||||
|
|
||||||
|
## 前置知识
|
||||||
|
- [[02_Linux基础]] -- Linux基本命令和文件操作
|
||||||
|
- C语言基础语法
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、Linux环境下C语言程序编译链接过程
|
||||||
|
|
||||||
|
### 1.1 编译的四个阶段
|
||||||
|
|
||||||
|
在Linux下,一个C源文件要经过四个阶段才能变成可执行程序:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A["hello.c
|
||||||
|
源代码"] -->|预处理
|
||||||
|
gcc -E| B["hello.i
|
||||||
|
预处理后的源代码"]
|
||||||
|
B -->|编译
|
||||||
|
gcc -S| C["hello.s
|
||||||
|
汇编代码"]
|
||||||
|
C -->|汇编
|
||||||
|
gcc -c| D["hello.o
|
||||||
|
目标代码(机器码)"]
|
||||||
|
D -->|链接
|
||||||
|
gcc| E["hello
|
||||||
|
可执行程序"]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style B fill:#fff3e0
|
||||||
|
style C fill:#f3e5f5
|
||||||
|
style D fill:#ffcdd2
|
||||||
|
style E fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
| 阶段 | 输入 | 输出 | gcc选项 | 主要工作 |
|
||||||
|
|------|------|------|---------|---------|
|
||||||
|
| **预处理** | `.c` | `.i` | `-E` | 展开宏 `#define`、包含头文件 `#include`、条件编译 |
|
||||||
|
| **编译** | `.i` | `.s` | `-S` | 将C代码翻译为汇编代码,进行语法分析和优化 |
|
||||||
|
| **汇编** | `.s` | `.o` | `-c` | 将汇编代码翻译为机器码(目标文件) |
|
||||||
|
| **链接** | `.o` | 可执行文件 | (默认) | 合并库函数、解析符号引用、生成最终可执行文件 |
|
||||||
|
|
||||||
|
### 1.2 实际操作演示
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 分步编译 hello.c
|
||||||
|
gcc -E hello.c -o hello.i # 第1步:预处理
|
||||||
|
gcc -S hello.i -o hello.s # 第2步:编译(生成汇编)
|
||||||
|
gcc -c hello.s -o hello.o # 第3步:汇编(生成目标文件)
|
||||||
|
gcc hello.o -o hello # 第4步:链接(生成可执行文件)
|
||||||
|
|
||||||
|
# 一步完成(等价于上面四步)
|
||||||
|
gcc hello.c -o hello
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 各阶段产物的特点
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看预处理结果(宏被展开,头文件被包含)
|
||||||
|
gcc -E hello.c -o hello.i
|
||||||
|
wc -l hello.c hello.i # hello.i 会比 hello.c 长很多
|
||||||
|
|
||||||
|
# 查看汇编代码
|
||||||
|
gcc -S hello.c -o hello.s
|
||||||
|
cat hello.s # 可以看到汇编指令
|
||||||
|
|
||||||
|
# 查看目标文件内容
|
||||||
|
gcc -c hello.c -o hello.o
|
||||||
|
file hello.o # 显示文件类型:ELF 64-bit relocatable
|
||||||
|
nm hello.o # 查看符号表
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 链接的作用
|
||||||
|
|
||||||
|
链接器主要完成以下工作:
|
||||||
|
1. **符号解析**:将函数调用和变量引用与它们的定义关联起来
|
||||||
|
2. **地址重定位**:为代码和数据分配最终的内存地址
|
||||||
|
3. **库合并**:将程序使用的库函数(如 `printf`)合并进来
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "链接前"
|
||||||
|
A["main.o
|
||||||
|
调用 printf()"]
|
||||||
|
B["sum.o
|
||||||
|
定义 sum()"]
|
||||||
|
C["libc.a
|
||||||
|
定义 printf()"]
|
||||||
|
end
|
||||||
|
subgraph "链接后"
|
||||||
|
D["可执行文件
|
||||||
|
所有代码和数据合并"]
|
||||||
|
end
|
||||||
|
A --> D
|
||||||
|
B --> D
|
||||||
|
C --> D
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style B fill:#fff3e0
|
||||||
|
style C fill:#f3e5f5
|
||||||
|
style D fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、Linux可执行程序结构
|
||||||
|
|
||||||
|
### 2.1 ELF文件格式
|
||||||
|
|
||||||
|
Linux下的可执行文件采用 **ELF(Executable and Linkable Format)** 格式。一个可执行文件在内存中由多个段(section/segment)组成:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "高地址"
|
||||||
|
STACK["栈 (Stack)
|
||||||
|
局部变量、函数调用帧
|
||||||
|
向下增长 ↓"]
|
||||||
|
end
|
||||||
|
subgraph ""
|
||||||
|
FREE["空闲区域
|
||||||
|
(向中间增长)"]
|
||||||
|
end
|
||||||
|
subgraph ""
|
||||||
|
HEAP["堆 (Heap)
|
||||||
|
malloc/free 动态分配
|
||||||
|
向上增长 ↑"]
|
||||||
|
end
|
||||||
|
subgraph ""
|
||||||
|
BSS[".bss 段
|
||||||
|
未初始化的全局变量
|
||||||
|
(运行时初始化为0)"]
|
||||||
|
end
|
||||||
|
subgraph ""
|
||||||
|
DATA[".data 段
|
||||||
|
已初始化的全局变量"]
|
||||||
|
end
|
||||||
|
subgraph ""
|
||||||
|
RODATA[".rodata 段
|
||||||
|
只读数据(如字符串常量)"]
|
||||||
|
end
|
||||||
|
subgraph "低地址"
|
||||||
|
TEXT[".text 段
|
||||||
|
程序代码(机器指令)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
STACK --- FREE
|
||||||
|
FREE --- HEAP
|
||||||
|
HEAP --- BSS
|
||||||
|
BSS --- DATA
|
||||||
|
DATA --- RODATA
|
||||||
|
RODATA --- TEXT
|
||||||
|
|
||||||
|
style TEXT fill:#e1f5fe
|
||||||
|
style RODATA fill:#fff3e0
|
||||||
|
style DATA fill:#e8f5e9
|
||||||
|
style BSS fill:#f3e5f5
|
||||||
|
style HEAP fill:#ffcdd2
|
||||||
|
style STACK fill:#c8e6c9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 各段详解
|
||||||
|
|
||||||
|
| 段名 | 内容 | 特点 |
|
||||||
|
|------|------|------|
|
||||||
|
| **.text**(代码段) | 程序的机器指令 | 只读、可执行 |
|
||||||
|
| **.rodata**(只读数据段) | 字符串常量、`const` 修饰的全局变量 | 只读 |
|
||||||
|
| **.data**(数据段) | 已初始化的全局变量和静态变量 | 可读可写 |
|
||||||
|
| **.bss** | 未初始化的全局变量和静态变量 | 运行时由系统初始化为0,不占磁盘空间 |
|
||||||
|
| **堆(Heap)** | `malloc()` / `calloc()` 动态分配的内存 | 由程序员手动管理,向上增长 |
|
||||||
|
| **栈(Stack)** | 局部变量、函数参数、返回地址 | 由编译器自动管理,向下增长 |
|
||||||
|
|
||||||
|
### 2.3 用代码验证
|
||||||
|
|
||||||
|
```c
|
||||||
|
// memory_layout.c - 验证程序内存布局
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
int global_init = 100; // .data 段(已初始化全局变量)
|
||||||
|
int global_uninit; // .bss 段(未初始化全局变量)
|
||||||
|
const int READONLY = 42; // .rodata 段(只读数据)
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int local_var = 10; // 栈(局部变量)
|
||||||
|
int *heap_var = malloc(sizeof(int)); // 堆(动态分配)
|
||||||
|
|
||||||
|
printf("代码段地址 (main): %p\n", main);
|
||||||
|
printf("只读数据段地址: %p\n", &READONLY);
|
||||||
|
printf("数据段地址 (global): %p\n", &global_init);
|
||||||
|
printf("BSS段地址 (uninit): %p\n", &global_uninit);
|
||||||
|
printf("堆地址 (malloc): %p\n", heap_var);
|
||||||
|
printf("栈地址 (local): %p\n", &local_var);
|
||||||
|
|
||||||
|
free(heap_var);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gcc memory_layout.c -o memory_layout
|
||||||
|
./memory_layout
|
||||||
|
```
|
||||||
|
|
||||||
|
预期输出中,地址从低到高排列为:`.text` < `.rodata` < `.data` < `.bss` < 堆 < 栈。
|
||||||
|
|
||||||
|
### 2.4 用 readelf 和 objdump 查看
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看ELF文件的段信息
|
||||||
|
readelf -S hello
|
||||||
|
|
||||||
|
# 反汇编 .text 段
|
||||||
|
objdump -d hello
|
||||||
|
|
||||||
|
# 查看符号表
|
||||||
|
nm hello
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、gdb调试
|
||||||
|
|
||||||
|
### 3.1 编译时添加调试信息
|
||||||
|
|
||||||
|
使用 `-g` 选项编译,gdb才能将机器指令与源代码行号对应:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gcc -g gdbuse.c -o gdbuse
|
||||||
|
gdb ./gdbuse
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 gdb常用命令
|
||||||
|
|
||||||
|
| 命令 | 缩写 | 作用 | 示例 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `run` | `r` | 运行程序 | `run` |
|
||||||
|
| `break` | `b` | 设置断点 | `b main` 或 `b gdbuse.c:10` |
|
||||||
|
| `next` | `n` | 单步执行(不进入函数) | `n` |
|
||||||
|
| `step` | `s` | 单步执行(进入函数) | `s` |
|
||||||
|
| `continue` | `c` | 继续运行到下一个断点 | `c` |
|
||||||
|
| `print` | `p` | 打印变量值 | `p count` |
|
||||||
|
| `backtrace` | `bt` | 查看调用栈 | `bt` |
|
||||||
|
| `list` | `l` | 查看源代码 | `l` |
|
||||||
|
| `info locals` | -- | 查看所有局部变量 | `info locals` |
|
||||||
|
| `watch` | -- | 监视变量变化 | `watch count` |
|
||||||
|
| `quit` | `q` | 退出gdb | `q` |
|
||||||
|
|
||||||
|
### 3.3 调试实例:gdbuse.c 中的bug
|
||||||
|
|
||||||
|
课程提供的 `gdbuse.c` 包含一个典型的bug,适合用来练习gdb调试:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// gdbuse.c - 包含bug的程序
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
char c = 't';
|
||||||
|
char s[100];
|
||||||
|
int i;
|
||||||
|
int count = 0;
|
||||||
|
strcpy(s, "abcdefghijklmopqrstuvstuxyz0123456789");
|
||||||
|
for (i = 0; i < strlen(s); i++)
|
||||||
|
if (s[i] = c) // BUG: 应该是 == 而不是 =
|
||||||
|
count++;
|
||||||
|
printf("count = %d\n", count);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**调试步骤**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 编译(加 -g 选项)
|
||||||
|
gcc -g gdbuse.c -o gdbuse
|
||||||
|
|
||||||
|
# 2. 启动gdb
|
||||||
|
gdb ./gdbuse
|
||||||
|
|
||||||
|
# 3. 在 main 函数设置断点
|
||||||
|
(gdb) b main
|
||||||
|
|
||||||
|
# 4. 运行程序
|
||||||
|
(gdb) run
|
||||||
|
|
||||||
|
# 5. 单步执行,观察变量
|
||||||
|
(gdb) n # 执行到 for 循环
|
||||||
|
(gdb) p s[i] # 打印当前字符
|
||||||
|
(gdb) p c # 打印目标字符
|
||||||
|
(gdb) p count # 打印计数器
|
||||||
|
|
||||||
|
# 6. 发现问题:s[i] = c 是赋值,不是比较
|
||||||
|
# 应该写成 s[i] == c
|
||||||
|
```
|
||||||
|
|
||||||
|
**bug分析**:`if (s[i] = c)` 中使用了赋值运算符 `=` 而非比较运算符 `==`,导致每次循环都把 `c` 赋值给 `s[i]`,且条件永远为真(`c` 的值 `'t'` 非零),最终 `count` 等于字符串长度。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、Makefile
|
||||||
|
|
||||||
|
### 4.1 为什么需要Makefile
|
||||||
|
|
||||||
|
当项目包含多个源文件时,手动逐个编译非常麻烦。Makefile 可以:
|
||||||
|
- 自动判断哪些文件需要重新编译
|
||||||
|
- 并行编译,加快速度
|
||||||
|
- 统一管理编译选项
|
||||||
|
|
||||||
|
### 4.2 基本规则
|
||||||
|
|
||||||
|
Makefile 的每条规则由三部分组成:
|
||||||
|
|
||||||
|
```
|
||||||
|
目标: 依赖
|
||||||
|
命令(必须以Tab开头)
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A["target
|
||||||
|
目标"] --> B["dependencies
|
||||||
|
依赖"]
|
||||||
|
B --> C["recipe
|
||||||
|
命令"]
|
||||||
|
|
||||||
|
style A fill:#ffcdd2
|
||||||
|
style B fill:#fff3e0
|
||||||
|
style C fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 Makefile实例
|
||||||
|
|
||||||
|
以课程中的 `sum.c` 多文件项目为例(`sum.c` + `main.c` + `calc.h`):
|
||||||
|
|
||||||
|
```makefile
|
||||||
|
# Makefile - 多文件编译示例
|
||||||
|
CC = gcc
|
||||||
|
CFLAGS = -Wall -g
|
||||||
|
LDFLAGS = -L. -lwrapper
|
||||||
|
|
||||||
|
# 目标文件
|
||||||
|
TARGET = myprogram
|
||||||
|
OBJS = main.o sum.o
|
||||||
|
|
||||||
|
# 默认目标
|
||||||
|
all: $(TARGET)
|
||||||
|
|
||||||
|
# 链接规则
|
||||||
|
$(TARGET): $(OBJS)
|
||||||
|
$(CC) $(OBJS) -o $(TARGET) $(LDFLAGS)
|
||||||
|
|
||||||
|
# 通用编译规则
|
||||||
|
%.o: %.c calc.h
|
||||||
|
$(CC) $(CFLAGS) -c $< -o $@
|
||||||
|
|
||||||
|
# 清理
|
||||||
|
clean:
|
||||||
|
rm -f $(OBJS) $(TARGET)
|
||||||
|
```
|
||||||
|
|
||||||
|
**自动变量说明**:
|
||||||
|
|
||||||
|
| 变量 | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| `$@` | 当前目标文件名 |
|
||||||
|
| `$<` | 第一个依赖文件名 |
|
||||||
|
| `$^` | 所有依赖文件名 |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用Makefile
|
||||||
|
make # 默认编译
|
||||||
|
make clean # 清理编译产物
|
||||||
|
make -j4 # 4个线程并行编译
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 Makefile的执行逻辑
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A["make 命令"] --> B{检查 all 目标}
|
||||||
|
B --> C{myprogram 存在且最新?}
|
||||||
|
C -->|是| D["无需编译"]
|
||||||
|
C -->|否| E{main.o 需要更新?}
|
||||||
|
E -->|是| F["gcc -c main.c -o main.o"]
|
||||||
|
E -->|否| G{sum.o 需要更新?}
|
||||||
|
F --> G
|
||||||
|
G -->|是| H["gcc -c sum.c -o sum.o"]
|
||||||
|
G -->|否| I["gcc main.o sum.o -o myprogram"]
|
||||||
|
H --> I
|
||||||
|
|
||||||
|
style A fill:#ffcdd2
|
||||||
|
style D fill:#e8f5e9
|
||||||
|
style I fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、Wrapper库
|
||||||
|
|
||||||
|
### 5.1 什么是Wrapper库
|
||||||
|
|
||||||
|
课程提供的 **Wrapper库**(`libwrapper.a`)封装了常用的系统调用,在原始系统调用的基础上增加了 **错误检查** 功能。如果系统调用失败,Wrapper函数会自动打印错误信息并终止程序。
|
||||||
|
|
||||||
|
### 5.2 设计哲学
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph "不使用Wrapper"
|
||||||
|
A[程序员] -->|"手动检查返回值"| B[系统调用]
|
||||||
|
B -->|"可能忘记检查"| C[隐患:错误被忽略]
|
||||||
|
end
|
||||||
|
subgraph "使用Wrapper"
|
||||||
|
D[程序员] -->|"直接调用"| E[Wrapper函数]
|
||||||
|
E -->|"自动检查"| F[系统调用]
|
||||||
|
F -->|"失败时"| G[打印错误并退出]
|
||||||
|
end
|
||||||
|
|
||||||
|
style C fill:#ffcdd2
|
||||||
|
style G fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 使用示例
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 不使用 Wrapper(容易忘记检查)
|
||||||
|
int fd = open("file.txt", O_RDONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
perror("open");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 Wrapper(自动检查,代码更简洁)
|
||||||
|
int fd = Open("file.txt", O_RDONLY, 0); // 注意:大写开头
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.4 常用Wrapper函数对照
|
||||||
|
|
||||||
|
| 系统调用 | Wrapper函数 | 包含头文件 |
|
||||||
|
|----------|-------------|-----------|
|
||||||
|
| `fork()` | `Fork()` | `wrapper.h` |
|
||||||
|
| `execve()` | `Execve()` | `wrapper.h` |
|
||||||
|
| `wait()` | `Wait()` | `wrapper.h` |
|
||||||
|
| `open()` | `Open()` | `wrapper.h` |
|
||||||
|
| `read()` | `Read()` | `wrapper.h` |
|
||||||
|
| `write()` | `Write()` | `wrapper.h` |
|
||||||
|
| `close()` | `Close()` | `wrapper.h` |
|
||||||
|
| `malloc()` | `Malloc()` | `wrapper.h` |
|
||||||
|
| `free()` | `Free()` | `wrapper.h` |
|
||||||
|
| `pthread_create()` | `Pthread_create()` | `wrapper.h` |
|
||||||
|
|
||||||
|
### 5.5 编译时链接Wrapper库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编译时链接 libwrapper.a
|
||||||
|
gcc -o myprogram myprogram.c -L. -lwrapper
|
||||||
|
|
||||||
|
# 或者在 Makefile 中
|
||||||
|
LDFLAGS = -L. -lwrapper
|
||||||
|
```
|
||||||
|
|
||||||
|
> **注意**:Wrapper函数名是对应系统调用的首字母大写形式。详见 [[附录A_Wrapper库参考]]。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、命令行参数
|
||||||
|
|
||||||
|
### 6.1 argc 和 argv
|
||||||
|
|
||||||
|
C程序的 `main` 函数可以接收命令行参数:
|
||||||
|
|
||||||
|
```c
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
```
|
||||||
|
|
||||||
|
- **argc**(argument count):参数个数,包括程序名本身
|
||||||
|
- **argv**(argument vector):参数字符串数组,`argv[0]` 是程序名
|
||||||
|
|
||||||
|
### 6.2 实例:cmdpar.c
|
||||||
|
|
||||||
|
```c
|
||||||
|
// cmdpar.c - 命令行参数处理
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
int i;
|
||||||
|
printf("参数个数(含程序名): %d\n", argc);
|
||||||
|
for (i = 0; i < argc; i++)
|
||||||
|
printf("argv[%d] = %s\n", i, argv[i]);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gcc cmdpar.c -o cmdpar
|
||||||
|
./cmdpar hello world 123
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期输出**:
|
||||||
|
```
|
||||||
|
参数个数(含程序名): 4
|
||||||
|
argv[0] = ./cmdpar
|
||||||
|
argv[1] = hello
|
||||||
|
argv[2] = world
|
||||||
|
argv[3] = 123
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 参数传递原理
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "命令行"
|
||||||
|
CMD["./cmdpar hello world 123"]
|
||||||
|
end
|
||||||
|
subgraph "操作系统解析"
|
||||||
|
ARG0["argv[0] = \"./cmdpar\""]
|
||||||
|
ARG1["argv[1] = \"hello\""]
|
||||||
|
ARG2["argv[2] = \"world\""]
|
||||||
|
ARG3["argv[3] = \"123\""]
|
||||||
|
ARGC["argc = 4"]
|
||||||
|
end
|
||||||
|
CMD --> ARG0
|
||||||
|
CMD --> ARG1
|
||||||
|
CMD --> ARG2
|
||||||
|
CMD --> ARG3
|
||||||
|
CMD --> ARGC
|
||||||
|
|
||||||
|
style CMD fill:#fff3e0
|
||||||
|
style ARGC fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、基本C编程
|
||||||
|
|
||||||
|
### 7.1 hello.c
|
||||||
|
|
||||||
|
```c
|
||||||
|
// hello.c - 最简单的C程序
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
printf("hello World\n");
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gcc hello.c -o hello
|
||||||
|
./hello
|
||||||
|
# 输出:hello World
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 sum.c -- 多文件项目
|
||||||
|
|
||||||
|
**calc.h**(头文件):
|
||||||
|
```c
|
||||||
|
// calc.h - 函数声明
|
||||||
|
#ifndef CALC_H
|
||||||
|
#define CALC_H
|
||||||
|
|
||||||
|
double aver(double, double);
|
||||||
|
double sum(double, double);
|
||||||
|
|
||||||
|
#endif
|
||||||
|
```
|
||||||
|
|
||||||
|
**sum.c**(实现文件):
|
||||||
|
```c
|
||||||
|
// sum.c - 求和函数实现
|
||||||
|
#include "calc.h"
|
||||||
|
|
||||||
|
double sum(double num1, double num2) {
|
||||||
|
return (num1 + num2);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**main.c**(主文件):
|
||||||
|
```c
|
||||||
|
// main.c - 主函数
|
||||||
|
#include <stdio.h>
|
||||||
|
#include "calc.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
double a = 10.0, b = 20.0;
|
||||||
|
printf("Sum = %.2f\n", sum(a, b));
|
||||||
|
printf("Average = %.2f\n", aver(a, b));
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 分步编译
|
||||||
|
gcc -c sum.c -o sum.o
|
||||||
|
gcc -c main.c -o main.o
|
||||||
|
gcc sum.o main.o -o calc
|
||||||
|
./calc
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、知识关联
|
||||||
|
|
||||||
|
- 编译链接过程在 [[01_系统运行机制]] 中理解程序如何被加载到内存
|
||||||
|
- 可执行程序的内存布局(堆、栈)在 [[05_进程控制]] 中与进程地址空间对应
|
||||||
|
- gdb调试在实验中会大量使用
|
||||||
|
- Makefile在 [[09_并发网络服务器]] 的多文件项目中会用到
|
||||||
|
- Wrapper库贯穿整个课程,详见 [[附录A_Wrapper库参考]]
|
||||||
|
- 文件I/O编程是 [[04_文件IO编程]] 的核心内容
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、思考题
|
||||||
|
|
||||||
|
1. **预处理阶段做了什么?** `#include <stdio.h>` 在预处理后变成了什么?
|
||||||
|
2. **为什么需要链接?** 如果没有链接器,编程会变成什么样?
|
||||||
|
3. **.bss段为什么不在文件中占空间?** 系统如何保证未初始化全局变量为0?
|
||||||
|
4. **栈和堆的增长方向为什么相反?** 这种设计有什么好处?
|
||||||
|
5. **Wrapper库的价值是什么?** 为什么课程不直接使用系统调用?
|
||||||
|
6. **`if (a = 1)` 和 `if (a == 1)` 的区别?** 如何用gdb发现这类bug?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、扩展阅读
|
||||||
|
|
||||||
|
- 《深入理解计算机系统》第7章:链接
|
||||||
|
- 《UNIX环境高级编程》第1章:UNIX基础知识
|
||||||
|
- GCC官方文档:https://gcc.gnu.org/onlinedocs/
|
||||||
|
- GDB官方教程:https://www.sourceware.org/gdb/documentation/
|
||||||
619
操作系统/04_文件IO编程/04_文件IO编程.md
Normal file
619
操作系统/04_文件IO编程/04_文件IO编程.md
Normal file
@@ -0,0 +1,619 @@
|
|||||||
|
# 第04讲:文件IO编程
|
||||||
|
|
||||||
|
> 🎯 **本节目标**:掌握UNIX文件IO系统调用的使用方法,理解文件描述符、文件共享、mmap内存映射和I/O重定向机制
|
||||||
|
|
||||||
|
## 📋 前置知识
|
||||||
|
- [[03_C语言编程基础]] — C语言编译链接、gdb调试、Makefile
|
||||||
|
- [[01_系统运行机制]] — 系统调用的概念
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤔 为什么需要这个?
|
||||||
|
|
||||||
|
程序要读写文件、处理数据,必须通过操作系统的I/O接口。UNIX提供了一套简洁而强大的系统调用(open/read/write/close/lseek),几乎所有的Linux程序都建立在这套接口之上。Shell的重定向机制、数据库的存储引擎、Web服务器的文件传输,底层都依赖这些原语。
|
||||||
|
|
||||||
|
**生活比喻**:
|
||||||
|
- 文件描述符就像**取餐号牌**:你去餐厅点餐(open),服务员给你一个号牌(fd),之后你用号牌取餐(read/write),吃完归还号牌(close)
|
||||||
|
- lseek就像**唱片针头移动**:可以跳到唱片的任意位置开始播放
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 核心概念
|
||||||
|
|
||||||
|
### 1. UNIX IO函数
|
||||||
|
|
||||||
|
Linux将所有I/O设备都视为**文件**,统一通过文件描述符进行操作。内核为每个进程维护一个**文件描述符表**,从0开始编号。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph 进程文件描述符表
|
||||||
|
fd0[0 - stdin 标准输入]
|
||||||
|
fd1[1 - stdout 标准输出]
|
||||||
|
fd2[2 - stderr 标准错误]
|
||||||
|
fd3[3 - 普通文件]
|
||||||
|
fd4[4 - 普通文件]
|
||||||
|
end
|
||||||
|
|
||||||
|
style fd0 fill:#e1f5fe
|
||||||
|
style fd1 fill:#e8f5e9
|
||||||
|
style fd2 fill:#fff3e0
|
||||||
|
```
|
||||||
|
|
||||||
|
#### open() - 打开/创建文件
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <fcntl.h>
|
||||||
|
int open(const char *pathname, int flags, mode_t mode);
|
||||||
|
// 返回:成功返回文件描述符(>=0),失败返回-1
|
||||||
|
```
|
||||||
|
|
||||||
|
**flags参数**(可用 `|` 组合):
|
||||||
|
|
||||||
|
| 标志 | 含义 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `O_RDONLY` | 只读 | 三个互斥的访问模式之一 |
|
||||||
|
| `O_WRONLY` | 只写 | 三个互斥的访问模式之一 |
|
||||||
|
| `O_RDWR` | 读写 | 三个互斥的访问模式之一 |
|
||||||
|
| `O_CREAT` | 若文件不存在则创建 | 需要指定mode参数 |
|
||||||
|
| `O_TRUNC` | 截断文件为0 | 清空已有内容 |
|
||||||
|
| `O_APPEND` | 追加模式 | 每次写操作前定位到文件末尾 |
|
||||||
|
|
||||||
|
**mode参数**(创建文件时的权限):
|
||||||
|
|
||||||
|
| 八进制 | 含义 |
|
||||||
|
|--------|------|
|
||||||
|
| `0666` | 所有者/组/其他均可读写 |
|
||||||
|
| `0777` | 所有者/组/其他可读写执行 |
|
||||||
|
| `0644` | 所有者读写,组和其他只读 |
|
||||||
|
|
||||||
|
#### close() - 关闭文件
|
||||||
|
|
||||||
|
```c
|
||||||
|
int close(int fd);
|
||||||
|
// 关闭文件描述符,释放内核资源
|
||||||
|
```
|
||||||
|
|
||||||
|
#### read() / write() - 读写文件
|
||||||
|
|
||||||
|
```c
|
||||||
|
ssize_t read(int fd, void *buf, size_t count);
|
||||||
|
// 返回:实际读取的字节数,0表示EOF,-1表示错误
|
||||||
|
|
||||||
|
ssize_t write(int fd, const void *buf, size_t count);
|
||||||
|
// 返回:实际写入的字节数,-1表示错误
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 文件描述符分配规则
|
||||||
|
|
||||||
|
新打开的文件描述符总是取**当前未使用的最小值**。标准输入(0)、标准输出(1)、标准错误(2)默认被占用,因此普通文件通常从3开始。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A["open('f1')"] --> B["返回 fd=3"]
|
||||||
|
B --> C["open('f2')"]
|
||||||
|
C --> D["返回 fd=4"]
|
||||||
|
D --> E["open('f3')"]
|
||||||
|
E --> F["返回 fd=5"]
|
||||||
|
F --> G["close(fd=3)"]
|
||||||
|
G --> H["open('f4')"]
|
||||||
|
H --> I["返回 fd=3(最小可用)"]
|
||||||
|
|
||||||
|
style B fill:#e8f5e9
|
||||||
|
style D fill:#e8f5e9
|
||||||
|
style F fill:#e8f5e9
|
||||||
|
style I fill:#fff3e0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 文件共享
|
||||||
|
|
||||||
|
当多个进程打开同一个文件时,内核通过三层数据结构实现共享:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph 进程A的描述符表
|
||||||
|
A1["fd=3"] --> FT1
|
||||||
|
A2["fd=4"] --> FT2
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 进程B的描述符表
|
||||||
|
B1["fd=3"] --> FT1
|
||||||
|
B2["fd=5"] --> FT3
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 文件表 全局
|
||||||
|
FT1["文件表项1
|
||||||
|
文件偏移=100
|
||||||
|
引用计数=2"]
|
||||||
|
FT2["文件表项2
|
||||||
|
文件偏移=0
|
||||||
|
引用计数=1"]
|
||||||
|
FT3["文件表项3
|
||||||
|
文件偏移=50
|
||||||
|
引用计数=1"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph v-node表
|
||||||
|
V1["v-node
|
||||||
|
文件大小=1024
|
||||||
|
inode信息"]
|
||||||
|
end
|
||||||
|
|
||||||
|
FT1 --> V1
|
||||||
|
FT2 --> V1
|
||||||
|
FT3 --> V1
|
||||||
|
|
||||||
|
style FT1 fill:#ffcdd2
|
||||||
|
style V1 fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- **描述符表**:每个进程独立,每个打开的文件描述符对应一个表项
|
||||||
|
- **文件表**:所有进程共享,记录当前文件偏移量和引用计数
|
||||||
|
- **v-node表**:所有进程共享,存储文件元数据(大小、类型、inode等)
|
||||||
|
|
||||||
|
**同进程多次打开同一文件**:每次open创建新的文件表项(独立偏移量),但指向同一个v-node。
|
||||||
|
|
||||||
|
**不同进程打开同一文件**:每个进程各自创建独立的文件表项,共享同一个v-node。
|
||||||
|
|
||||||
|
### 3. lseek定位
|
||||||
|
|
||||||
|
`lseek` 修改文件的当前偏移量,实现随机访问:
|
||||||
|
|
||||||
|
```c
|
||||||
|
off_t lseek(int fd, off_t offset, int whence);
|
||||||
|
// whence: SEEK_SET / SEEK_CUR / SEEK_END
|
||||||
|
// 返回:新的文件偏移量,-1表示错误
|
||||||
|
```
|
||||||
|
|
||||||
|
| whence | 含义 | 计算方式 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| `SEEK_SET` | 从文件开头 | 新偏移 = offset |
|
||||||
|
| `SEEK_CUR` | 从当前位置 | 新偏移 = 当前偏移 + offset |
|
||||||
|
| `SEEK_END` | 从文件末尾 | 新偏移 = 文件大小 + offset |
|
||||||
|
|
||||||
|
**利用lseek获取文件大小**:
|
||||||
|
```c
|
||||||
|
off_t size = lseek(fd, 0, SEEK_END);
|
||||||
|
```
|
||||||
|
|
||||||
|
**利用lseek创建空洞文件**:
|
||||||
|
```c
|
||||||
|
lseek(fd, 1024*1024, SEEK_SET); // 跳到1MB位置
|
||||||
|
write(fd, "X", 1); // 写1字节
|
||||||
|
// 文件大小为 1MB+1,中间全是空洞('\0')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. mmap内存映射
|
||||||
|
|
||||||
|
`mmap` 将文件直接映射到进程的虚拟地址空间,之后可以像访问内存一样读写文件,无需read/write系统调用。
|
||||||
|
|
||||||
|
```c
|
||||||
|
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
|
||||||
|
// addr: 建议映射地址(通常传NULL由内核选择)
|
||||||
|
// length: 映射长度
|
||||||
|
// prot: 保护标志 PROT_READ | PROT_WRITE | PROT_EXEC
|
||||||
|
// flags: MAP_PRIVATE(私有副本)| MAP_SHARED(共享映射)
|
||||||
|
// fd: 文件描述符
|
||||||
|
// offset: 文件中的起始偏移
|
||||||
|
// 返回:映射区域的起始地址,失败返回MAP_FAILED
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph 进程虚拟地址空间
|
||||||
|
A["映射区域"]
|
||||||
|
end
|
||||||
|
subgraph 内核页缓存
|
||||||
|
B["文件数据页1"]
|
||||||
|
C["文件数据页2"]
|
||||||
|
D["文件数据页3"]
|
||||||
|
end
|
||||||
|
|
||||||
|
A -->|"缺页时加载"| B
|
||||||
|
A -->|"缺页时加载"| C
|
||||||
|
A -->|"缺页时加载"| D
|
||||||
|
|
||||||
|
style A fill:#e8f5e9
|
||||||
|
style B fill:#e1f5fe
|
||||||
|
style C fill:#e1f5fe
|
||||||
|
style D fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用流程**:
|
||||||
|
1. `open()` 打开文件
|
||||||
|
2. `fstat()` 获取文件大小
|
||||||
|
3. `mmap()` 映射到内存
|
||||||
|
4. 通过指针直接读写
|
||||||
|
5. `munmap()` 解除映射
|
||||||
|
6. `close()` 关闭文件
|
||||||
|
|
||||||
|
**MAP_PRIVATE vs MAP_SHARED**:
|
||||||
|
|
||||||
|
| 特性 | MAP_PRIVATE | MAP_SHARED |
|
||||||
|
|------|-------------|------------|
|
||||||
|
| 写入效果 | 写时复制,不影响原文件 | 直接修改原文件 |
|
||||||
|
| 使用场景 | 只读浏览文件 | 需要修改文件 |
|
||||||
|
| 进程间共享 | 不共享 | 多进程共享同一映射 |
|
||||||
|
|
||||||
|
### 5. dup2重定向
|
||||||
|
|
||||||
|
文件描述符复制是实现I/O重定向的核心机制。Shell的 `>`、`<`、`|` 管道都依赖于此。
|
||||||
|
|
||||||
|
#### dup() - 复制文件描述符
|
||||||
|
|
||||||
|
```c
|
||||||
|
int dup(int oldfd);
|
||||||
|
// 返回:新的文件描述符(取当前最小可用值)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### dup2() - 原子复制并重定向
|
||||||
|
|
||||||
|
```c
|
||||||
|
int dup2(int oldfd, int newfd);
|
||||||
|
// 先关闭newfd,再将oldfd复制到newfd
|
||||||
|
// 返回:newfd,失败返回-1
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant P as 进程
|
||||||
|
participant K as 内核
|
||||||
|
|
||||||
|
Note over P: save_fd = dup(STDOUT_FILENO)
|
||||||
|
P->>K: 复制fd=1
|
||||||
|
K-->>P: save_fd=3(保存stdout)
|
||||||
|
|
||||||
|
Note over P: dup2(fd, STDOUT_FILENO)
|
||||||
|
P->>K: 关闭fd=1,复制fd到1
|
||||||
|
K-->>P: fd=1现在指向文件
|
||||||
|
|
||||||
|
Note over P: write(STDOUT_FILENO, ...)
|
||||||
|
P->>K: 写入fd=1(文件)
|
||||||
|
K-->>P: 数据写入文件
|
||||||
|
|
||||||
|
Note over P: dup2(save_fd, STDOUT_FILENO)
|
||||||
|
P->>K: 关闭fd=1,复制save_fd到1
|
||||||
|
K-->>P: fd=1恢复为终端
|
||||||
|
|
||||||
|
Note over P: write(STDOUT_FILENO, ...)
|
||||||
|
P->>K: 写入fd=1(终端)
|
||||||
|
K-->>P: 数据显示在终端
|
||||||
|
```
|
||||||
|
|
||||||
|
**dup() vs dup2()**:
|
||||||
|
|
||||||
|
| 特性 | dup() | dup2() |
|
||||||
|
|------|-------|--------|
|
||||||
|
| 目标fd | 自动取最小可用值 | 指定目标fd |
|
||||||
|
| 关闭目标 | 不关闭 | 自动关闭目标fd |
|
||||||
|
| 原子性 | — | 原子操作,不会竞争 |
|
||||||
|
| 典型用途 | 简单复制 | Shell重定向 |
|
||||||
|
|
||||||
|
### 6. 标准IO vs UNIX IO
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph 标准IO
|
||||||
|
A["fopen/fread/fwrite/fclose"]
|
||||||
|
B["用户缓冲区
|
||||||
|
减少系统调用次数"]
|
||||||
|
end
|
||||||
|
subgraph UNIX IO
|
||||||
|
C["open/read/write/close"]
|
||||||
|
D["无用户缓冲区
|
||||||
|
每次操作都是系统调用"]
|
||||||
|
end
|
||||||
|
|
||||||
|
A --> B
|
||||||
|
C --> D
|
||||||
|
B -->|"底层调用"| D
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style C fill:#fff3e0
|
||||||
|
```
|
||||||
|
|
||||||
|
| 特性 | 标准IO (stdio) | UNIX IO |
|
||||||
|
|------|---------------|---------|
|
||||||
|
| 头文件 | `<stdio.h>` | `<unistd.h>`, `<fcntl.h>` |
|
||||||
|
| 缓冲 | 用户空间缓冲(默认行缓冲/全缓冲) | 无用户缓冲 |
|
||||||
|
| 性能 | 频繁小IO时快(减少系统调用) | 大块IO时相当 |
|
||||||
|
| 可移植性 | 高(ANSI C标准) | 低(POSIX,Linux/UNIX特有) |
|
||||||
|
| 功能 | 格式化printf/scanf | 更底层控制(如mmap) |
|
||||||
|
|
||||||
|
**缓冲模式**:
|
||||||
|
- **无缓冲**:stderr,立即输出
|
||||||
|
- **行缓冲**:stdout连接终端时,遇到换行符刷新
|
||||||
|
- **全缓冲**:普通文件读写,缓冲区满时刷新
|
||||||
|
|
||||||
|
### 7. struct IO - 结构体二进制读写
|
||||||
|
|
||||||
|
在操作系统实验中,经常需要将结构体数据以二进制格式写入文件并读回:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 写入结构体数组
|
||||||
|
struct student {
|
||||||
|
int id;
|
||||||
|
char name[20];
|
||||||
|
float score;
|
||||||
|
};
|
||||||
|
struct student stu[100];
|
||||||
|
// ... 填充数据 ...
|
||||||
|
|
||||||
|
int fd = open("student.dat", O_WRONLY|O_CREAT|O_TRUNC, 0666);
|
||||||
|
write(fd, stu, sizeof(struct student) * 100);
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
// 读回结构体数组
|
||||||
|
fd = open("student.dat", O_RDONLY);
|
||||||
|
read(fd, stu, sizeof(struct student) * 100);
|
||||||
|
close(fd);
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意事项**:
|
||||||
|
- 结构体可能存在**内存对齐**填充,文件中的字节布局与内存一致
|
||||||
|
- 跨平台时需注意字节序(大端/小端)
|
||||||
|
- 标准IO的fread/fwrite也可以进行结构体二进制读写
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 动手实践
|
||||||
|
|
||||||
|
### 示例1:逐字节文件拷贝(fcopy1.c)
|
||||||
|
|
||||||
|
最基础的文件拷贝,逐字节读写,理解read/write的基本用法:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// fcopy1.c - 逐字节文件拷贝
|
||||||
|
#include "wrapper.h"
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
char c;
|
||||||
|
int in, out;
|
||||||
|
in = Open("file.in", O_RDONLY, 0); // 以只读方式打开源文件
|
||||||
|
out = Open("file.out", O_WRONLY|O_CREAT, 0666); // 以写方式创建目标文件
|
||||||
|
while (Read(in, &c, 1) == 1) // 每次读1字节
|
||||||
|
Write(out, &c, 1); // 每次写1字节
|
||||||
|
Close(in); // 关闭源文件
|
||||||
|
Close(out); // 关闭目标文件
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
gcc -o fcopy1 fcopy1.c -L. -lwrapper
|
||||||
|
echo "Hello, UNIX IO!" > file.in
|
||||||
|
./fcopy1
|
||||||
|
cat file.out # 输出: Hello, UNIX IO!
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- `Open`、`Read`、`Write`、`Close` 是wrapper.h提供的带错误检查的包装函数
|
||||||
|
- `O_CREAT` 标志需要提供第三个参数(文件权限)
|
||||||
|
- 逐字节拷贝效率很低,实际应用应使用缓冲区
|
||||||
|
|
||||||
|
### 示例2:块缓冲文件拷贝(fcopy2.c)
|
||||||
|
|
||||||
|
改用1024字节的缓冲区,大幅提升性能:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// fcopy2.c - 块缓冲文件拷贝
|
||||||
|
#include "wrapper.h"
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
char block[1024]; // 1024字节缓冲区
|
||||||
|
int in, out;
|
||||||
|
int nread;
|
||||||
|
in = Open("file.in", O_RDONLY, 0);
|
||||||
|
out = Open("file.out", O_WRONLY|O_CREAT, 0666);
|
||||||
|
while ((nread = Read(in, block, sizeof(block))) > 0) // 每次读最多1024字节
|
||||||
|
Write(out, block, nread); // 写入实际读到的字节数
|
||||||
|
Close(in);
|
||||||
|
Close(out);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- `Read` 返回实际读取的字节数(可能小于请求值)
|
||||||
|
- 最后一次读取可能不满1024字节,需要用 `nread` 控制写入量
|
||||||
|
- 磁盘文件通常4KB对齐,使用4096字节缓冲区效率更高
|
||||||
|
|
||||||
|
### 示例3:lseek随机访问(lseek1.c)
|
||||||
|
|
||||||
|
演示文件随机读写:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// lseek1.c - 使用lseek进行随机访问
|
||||||
|
#include "wrapper.h"
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
char s1[6], s2[6];
|
||||||
|
int fd;
|
||||||
|
fd = Open("infile", O_RDWR, 0);
|
||||||
|
lseek(fd, 10, SEEK_SET); // 定位到文件开头偏移10字节处
|
||||||
|
Read(fd, s1, 5); // 读取5个字节
|
||||||
|
s1[5] = '\0';
|
||||||
|
printf("Read string: %s\n", s1); // 打印读到的内容
|
||||||
|
|
||||||
|
strcpy(s2, "12345");
|
||||||
|
lseek(fd, -5, SEEK_CUR); // 从当前位置回退5字节
|
||||||
|
Write(fd, s2, 5); // 覆写5字节
|
||||||
|
close(fd);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**执行过程图示**:
|
||||||
|
```
|
||||||
|
文件内容: ABCDEFGHIJ0123456789...
|
||||||
|
0123456789(偏移)
|
||||||
|
|
||||||
|
第1步: lseek(fd, 10, SEEK_SET) → 偏移=10
|
||||||
|
第2步: Read(fd, s1, 5) → s1="01234",偏移=15
|
||||||
|
第3步: lseek(fd, -5, SEEK_CUR) → 偏移=10
|
||||||
|
第4步: Write(fd, s2, 5) → 写入"12345",偏移=15
|
||||||
|
|
||||||
|
结果: ABCDEFGHIJ123456789...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例4:mmap内存映射(mmap1.c)
|
||||||
|
|
||||||
|
将整个文件映射到内存,通过指针直接读取:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// mmap1.c - 内存映射文件读取
|
||||||
|
#include "wrapper.h"
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
int fd = open("test.file", 0); // 打开文件
|
||||||
|
struct stat statbuf;
|
||||||
|
char *start;
|
||||||
|
char buf[2] = {0};
|
||||||
|
int ret = 0;
|
||||||
|
fstat(fd, &statbuf); // 获取文件大小
|
||||||
|
start = mmap(NULL, statbuf.st_size, // 映射整个文件
|
||||||
|
PROT_READ, MAP_PRIVATE, fd, 0);
|
||||||
|
do {
|
||||||
|
*buf = start[ret++]; // 像访问数组一样读取
|
||||||
|
} while(ret < statbuf.st_size);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
gcc -o mmap1 mmap1.c -L. -lwrapper
|
||||||
|
./mmap1
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- `fstat()` 获取文件大小,确定映射长度
|
||||||
|
- `PROT_READ` 表示只读映射,写入会触发段错误
|
||||||
|
- `MAP_PRIVATE` 表示私有映射(写时复制),修改不会影响原文件
|
||||||
|
- 访问映射区域时触发缺页中断,内核按需加载文件数据
|
||||||
|
|
||||||
|
### 示例5:dup2实现I/O重定向(dup2.c)
|
||||||
|
|
||||||
|
演示如何将stdout重定向到文件,再恢复:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// dup2.c - dup2实现I/O重定向
|
||||||
|
#include "wrapper.h"
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
int fd, save_fd;
|
||||||
|
char msg[] = "This is a test\n";
|
||||||
|
fd = Open("somefile", O_RDWR|O_CREAT, S_IRUSR|S_IWUSR);
|
||||||
|
save_fd = dup(STDOUT_FILENO); // 保存原始stdout(fd=1)到新fd
|
||||||
|
dup2(fd, STDOUT_FILENO); // 将stdout重定向到文件
|
||||||
|
Close(fd); // 关闭原fd(不影响已复制的fd=1)
|
||||||
|
Write(STDOUT_FILENO, msg, strlen(msg)); // 写入文件
|
||||||
|
dup2(save_fd, STDOUT_FILENO); // 恢复stdout为终端
|
||||||
|
Write(STDOUT_FILENO, msg, strlen(msg)); // 写入终端
|
||||||
|
Close(save_fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期输出**:
|
||||||
|
```
|
||||||
|
终端显示: This is a test
|
||||||
|
somefile内容: This is a test
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- `dup(STDOUT_FILENO)` 保存原始stdout的副本
|
||||||
|
- `dup2(fd, STDOUT_FILENO)` 原子地关闭fd=1并复制fd到1
|
||||||
|
- 恢复时再次用 `dup2` 将保存的副本写回fd=1
|
||||||
|
- Shell实现 `>` 重定向的原理与此相同
|
||||||
|
|
||||||
|
### 示例6:标准IO与UNIX IO对比
|
||||||
|
|
||||||
|
通过对比 `read1.c` 和 `fread1.c` 理解缓冲差异:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// read1.c - UNIX IO(无缓冲,每次read都是系统调用)
|
||||||
|
#include "wrapper.h"
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
int fd = open("test.file", O_RDONLY);
|
||||||
|
char buf[2] = {0};
|
||||||
|
int ret = 0;
|
||||||
|
do {
|
||||||
|
ret = read(fd, buf, 1); // 每读1字节都触发一次系统调用
|
||||||
|
} while(ret);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```c
|
||||||
|
// fread1.c - 标准IO(有用户缓冲区,减少系统调用次数)
|
||||||
|
#include "wrapper.h"
|
||||||
|
void main()
|
||||||
|
{
|
||||||
|
FILE *pf = fopen("test.file", "r");
|
||||||
|
char buf[2] = {0};
|
||||||
|
int ret = 0;
|
||||||
|
do {
|
||||||
|
ret = fread(buf, 1, 1, pf); // 用户缓冲区存在时不需要系统调用
|
||||||
|
} while(ret);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**性能对比**:读取同一个100KB的文件:
|
||||||
|
- `read1.c`:约102,400次系统调用
|
||||||
|
- `fread1.c`:约128次系统调用(默认缓冲区8KB)
|
||||||
|
|
||||||
|
### 示例7:文件描述符分配测试(fdtest1.c)
|
||||||
|
|
||||||
|
```c
|
||||||
|
// fdtest1.c - 观察文件描述符的分配顺序
|
||||||
|
#include "wrapper.h"
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
int fd1, fd2, fd3;
|
||||||
|
fd1 = Open("f1", O_RDWR|O_CREAT, 0777);
|
||||||
|
fd2 = Open("f2", O_RDWR|O_CREAT, 0777);
|
||||||
|
fd3 = Open("f3", O_RDWR|O_CREAT, 0777);
|
||||||
|
printf("fd1=%d fd2=%d fd3=%d\n", fd1, fd2, fd3);
|
||||||
|
Close(fd1);
|
||||||
|
Close(fd2);
|
||||||
|
Close(fd3);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期输出**:
|
||||||
|
```
|
||||||
|
fd1=3 fd2=4 fd3=5
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:0/1/2被stdin/stdout/stderr占用,新文件从3开始分配。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 知识关联
|
||||||
|
- 文件描述符的系统调用通过 [[01_系统运行机制]] 中的陷入指令进入内核态执行
|
||||||
|
- 编译链接这些程序需要 [[03_C语言编程基础]] 中的gcc、Makefile知识
|
||||||
|
- 文件的物理存储方式由 [[05_磁盘空间管理]] 中的FAT/NTFS/Ext2决定
|
||||||
|
- 实验要求见 [[实验01_IO编程]]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 思考题
|
||||||
|
|
||||||
|
1. **概念理解题**:文件描述符、文件表、v-node表三者的关系是什么?同一个文件被同一个进程打开两次和被两个不同进程各打开一次,有什么区别?
|
||||||
|
2. **代码分析题**:以下代码的输出是什么?为什么?
|
||||||
|
```c
|
||||||
|
fd = Open("test", O_RDWR|O_CREAT|O_TRUNC, 0666);
|
||||||
|
Write(fd, "hello", 5);
|
||||||
|
lseek(fd, 0, SEEK_SET);
|
||||||
|
Write(fd, "world", 5);
|
||||||
|
```
|
||||||
|
3. **应用题**:如何用dup2实现命令 `ls | grep .c` 的管道功能?需要哪些系统调用?
|
||||||
|
4. **性能题**:为什么fcopy2.c比fcopy1.c快得多?如果将缓冲区从1024改为4096,性能会如何变化?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 扩展阅读
|
||||||
|
- 《深入理解计算机系统》第10章:系统级I/O
|
||||||
|
- 《UNIX环境高级编程》第3章:文件I/O
|
||||||
|
- 《Linux编程》第4章相关源码:`实例源代码/chap4/`
|
||||||
595
操作系统/05_磁盘空间管理/05_磁盘空间管理.md
Normal file
595
操作系统/05_磁盘空间管理/05_磁盘空间管理.md
Normal file
@@ -0,0 +1,595 @@
|
|||||||
|
# 第05讲:磁盘空间管理
|
||||||
|
|
||||||
|
> 🎯 **本节目标**:理解磁盘空间的外存组织方式,掌握FAT/NTFS/Ext2文件系统的结构,理解空闲空间管理和磁盘容错机制
|
||||||
|
|
||||||
|
## 📋 前置知识
|
||||||
|
- [[04_文件IO编程]] — 文件读写的基本概念
|
||||||
|
- [[01_系统运行机制]] — 存储层次结构
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤔 为什么需要这个?
|
||||||
|
|
||||||
|
当你保存一个4GB的电影文件时,操作系统需要决定:把文件存在磁盘的哪些位置?如何记录哪些空间已用、哪些空闲?如果某个磁盘块坏了怎么办?不同的组织方式会直接影响文件的读写速度和磁盘空间利用率。
|
||||||
|
|
||||||
|
**生活比喻**:
|
||||||
|
- **连续分配**就像电影院的连续座位:一整排坐在一起,找人很快,但如果有零散空位就浪费了
|
||||||
|
- **链接分配**就像寻宝游戏:每个地点告诉你下一个地点在哪,灵活但不能直接跳到第N个
|
||||||
|
- **索引分配**就像书的目录:通过目录直接找到对应页码,高效且灵活
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 核心概念
|
||||||
|
|
||||||
|
### 1. 外存组织方式
|
||||||
|
|
||||||
|
文件在磁盘上的存放方式有三种基本策略:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[外存组织方式] --> B[连续组织
|
||||||
|
顺序文件]
|
||||||
|
A --> C[链接组织
|
||||||
|
隐式链接/FAT]
|
||||||
|
A --> D[索引组织
|
||||||
|
单级/多级/混合]
|
||||||
|
|
||||||
|
B --> B1["优点:顺序和随机访问都快"]
|
||||||
|
B --> B2["缺点:外部碎片,不能动态增长"]
|
||||||
|
C --> C1["优点:消除了外部碎片"]
|
||||||
|
C --> C2["缺点:不能高效随机访问"]
|
||||||
|
D --> D1["优点:支持随机访问,无外部碎片"]
|
||||||
|
D --> D2["缺点:索引块有开销"]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 连续组织(顺序文件)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[文件目录] -->|"起始块号=2, 长度=3"| B[磁盘块2]
|
||||||
|
B --> C[磁盘块3]
|
||||||
|
C --> D[磁盘块4]
|
||||||
|
|
||||||
|
style B fill:#e8f5e9
|
||||||
|
style C fill:#e8f5e9
|
||||||
|
style D fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
- 文件占用一组**连续的磁盘块**
|
||||||
|
- 目录项只需记录:起始块号 + 长度
|
||||||
|
- 支持顺序访问和随机访问(直接计算偏移)
|
||||||
|
- 问题:外部碎片严重,文件不能动态增长
|
||||||
|
|
||||||
|
#### 链接组织
|
||||||
|
|
||||||
|
**隐式链接**:每个磁盘块末尾存储指向下一个块的指针。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[目录] -->|"起始块号=2"| B[块2] -->|"指针→5"| C[块5] -->|"指针→8"| D[块8] -->|"指针→EOF"| E[结束]
|
||||||
|
|
||||||
|
style B fill:#e1f5fe
|
||||||
|
style C fill:#e1f5fe
|
||||||
|
style D fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
- 优点:消除了外部碎片,文件可以动态增长
|
||||||
|
- 缺点:只能顺序访问,指针占用存储空间,可靠性差(一个指针损坏后续全部丢失)
|
||||||
|
|
||||||
|
**显式链接(FAT)**:将所有块的链接指针集中存放在一张**文件分配表(FAT)**中。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph 文件分配表FAT
|
||||||
|
F0["0: —"]
|
||||||
|
F1["1: —"]
|
||||||
|
F2["2: 5"]
|
||||||
|
F3["3: —"]
|
||||||
|
F4["4: —"]
|
||||||
|
F5["5: 8"]
|
||||||
|
F6["6: —"]
|
||||||
|
F7["7: —"]
|
||||||
|
F8["8: EOF"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 磁盘块
|
||||||
|
D2[块2]
|
||||||
|
D5[块5]
|
||||||
|
D8[块8]
|
||||||
|
end
|
||||||
|
|
||||||
|
D2 -.->|"FAT[2]=5"| D5
|
||||||
|
D5 -.->|"FAT[5]=8"| D8
|
||||||
|
D8 -.->|"FAT[8]=EOF"| STOP[结束]
|
||||||
|
|
||||||
|
style F2 fill:#ffcdd2
|
||||||
|
style F5 fill:#ffcdd2
|
||||||
|
style F8 fill:#ffcdd2
|
||||||
|
```
|
||||||
|
|
||||||
|
- FAT常驻内存,随机访问时只需查表,不需要读磁盘
|
||||||
|
- 比隐式链接更可靠,但也更占内存
|
||||||
|
|
||||||
|
#### 索引组织
|
||||||
|
|
||||||
|
为每个文件建立一个**索引块**,集中存储所有数据块的块号:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[文件目录] -->|"索引块号=10"| B[索引块]
|
||||||
|
B -->|"指针0"| C[块2]
|
||||||
|
B -->|"指针1"| D[块5]
|
||||||
|
B -->|"指针2"| E[块8]
|
||||||
|
B -->|"指针3"| F[块12]
|
||||||
|
|
||||||
|
style B fill:#fff3e0
|
||||||
|
style C fill:#e1f5fe
|
||||||
|
style D fill:#e1f5fe
|
||||||
|
style E fill:#e1f5fe
|
||||||
|
style F fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
- 支持随机访问:访问第N个数据块,直接查索引块的第N个指针
|
||||||
|
- 无外部碎片
|
||||||
|
- 缺点:索引块本身占用空间,大文件需要多级索引
|
||||||
|
|
||||||
|
**三种方式对比**:
|
||||||
|
|
||||||
|
| 特性 | 连续组织 | 链接组织 | 索引组织 |
|
||||||
|
|------|----------|----------|----------|
|
||||||
|
| 顺序访问 | 快 | 快 | 快 |
|
||||||
|
| 随机访问 | 快(直接计算) | 慢(需遍历链) | 快(查索引表) |
|
||||||
|
| 外部碎片 | 严重 | 无 | 无 |
|
||||||
|
| 文件增长 | 困难 | 容易 | 容易 |
|
||||||
|
| 可靠性 | 高 | 低(指针损坏) | 高 |
|
||||||
|
|
||||||
|
### 2. FAT文件系统
|
||||||
|
|
||||||
|
FAT(File Allocation Table)是Windows早期广泛使用的文件系统。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph FAT磁盘布局
|
||||||
|
A[引导扇区] --> B[FAT1]
|
||||||
|
B --> C[FAT2 备份]
|
||||||
|
C --> D[根目录区]
|
||||||
|
D --> E[数据区]
|
||||||
|
end
|
||||||
|
|
||||||
|
style A fill:#ffcdd2
|
||||||
|
style B fill:#fff3e0
|
||||||
|
style C fill:#fff3e0
|
||||||
|
style D fill:#e1f5fe
|
||||||
|
style E fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**簇(Cluster)**:FAT文件系统的最小分配单位,由若干连续扇区组成。
|
||||||
|
|
||||||
|
| FAT版本 | 最大分区 | 簇大小 | FAT表项位数 |
|
||||||
|
|---------|---------|--------|-------------|
|
||||||
|
| FAT12 | 2MB | 512B~4KB | 12位 |
|
||||||
|
| FAT16 | 2GB | 2KB~32KB | 16位 |
|
||||||
|
| FAT32 | 2TB | 4KB~32KB | 32位(实际28位) |
|
||||||
|
|
||||||
|
**FAT表结构**:每个簇在FAT表中占一个表项,表项内容的含义:
|
||||||
|
|
||||||
|
| 表项值 | 含义 |
|
||||||
|
|--------|------|
|
||||||
|
| 0 | 空闲簇 |
|
||||||
|
| 2~N | 下一个簇的簇号 |
|
||||||
|
| FF8H~FFFH (FAT12) | 文件结束标记 |
|
||||||
|
| FFF8H~FFFFH (FAT16) | 文件结束标记 |
|
||||||
|
| 0FFFFFF8H~0FFFFFFFH (FAT32) | 文件结束标记 |
|
||||||
|
| 其他特殊值 | 坏簇标记 |
|
||||||
|
|
||||||
|
**FAT16的问题**:每个分区最多65536个簇,簇最小2KB时最大分区为2GB。小文件也会浪费一个簇的空间(簇内碎片)。
|
||||||
|
|
||||||
|
### 3. NTFS文件系统
|
||||||
|
|
||||||
|
NTFS(New Technology File System)是Windows NT系列的现代文件系统。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph NTFS磁盘布局
|
||||||
|
A[引导扇区
|
||||||
|
MBR/分区表] --> B[主控文件表MFT]
|
||||||
|
B --> C[MFT副本]
|
||||||
|
C --> D[系统文件区]
|
||||||
|
D --> E[用户数据区]
|
||||||
|
end
|
||||||
|
|
||||||
|
style A fill:#ffcdd2
|
||||||
|
style B fill:#fff3e0
|
||||||
|
style C fill:#fff3e0
|
||||||
|
style D fill:#e1f5fe
|
||||||
|
style E fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键特性**:
|
||||||
|
|
||||||
|
| 特性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 64位磁盘地址 | 支持超大分区(理论2^64字节) |
|
||||||
|
| MFT主控文件表 | 核心数据结构,每个文件/目录对应一条MFT记录 |
|
||||||
|
| LCN逻辑簇号 | 从分区开头算起的绝对簇号 |
|
||||||
|
| VCN虚拟簇号 | 文件内部的相对簇号(从0开始) |
|
||||||
|
| 日志文件 | 记录文件操作日志,崩溃后可恢复一致性 |
|
||||||
|
| 文件加密 | 支持EFS加密文件系统 |
|
||||||
|
| 文件压缩 | 支持透明压缩 |
|
||||||
|
| 硬链接 | 多个文件名指向同一个MFT记录 |
|
||||||
|
|
||||||
|
**MFT结构**:每条MFT记录通常1KB,包含多个属性(文件名、时间戳、数据内容等)。小文件的数据直接存储在MFT记录中(驻留属性),无需额外数据块。
|
||||||
|
|
||||||
|
### 4. Ext2文件系统
|
||||||
|
|
||||||
|
Ext2是Linux的经典文件系统,采用**块组**组织磁盘空间。
|
||||||
|
|
||||||
|
#### 磁盘总体布局
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph "Ext2磁盘布局"
|
||||||
|
A["引导块
|
||||||
|
Block 0"] --> B["块组0"]
|
||||||
|
B --> C["块组1"]
|
||||||
|
C --> D["块组2"]
|
||||||
|
D --> E["..."]
|
||||||
|
end
|
||||||
|
|
||||||
|
style A fill:#ffcdd2
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 块组内部结构
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "单个块组的结构"
|
||||||
|
A["超级块 Super Block
|
||||||
|
文件系统全局信息"] --> B["块组描述符表
|
||||||
|
所有块组的描述信息"]
|
||||||
|
B --> C["数据块位图
|
||||||
|
记录哪些块已用"]
|
||||||
|
C --> D["inode位图
|
||||||
|
记录哪些inode已用"]
|
||||||
|
D --> E["inode表
|
||||||
|
所有inode的数组"]
|
||||||
|
E --> F["数据块
|
||||||
|
存放文件内容"]
|
||||||
|
end
|
||||||
|
|
||||||
|
style A fill:#ffcdd2
|
||||||
|
style B fill:#ffcdd2
|
||||||
|
style C fill:#fff3e0
|
||||||
|
style D fill:#fff3e0
|
||||||
|
style E fill:#e1f5fe
|
||||||
|
style F fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**超级块**(Super Block):存储整个文件系统的元数据
|
||||||
|
- 块大小、inode总数、块总数、空闲块数、空闲inode数
|
||||||
|
- 挂载计数、上次检查时间等
|
||||||
|
|
||||||
|
**块组描述符**(Group Descriptor):描述每个块组的状态
|
||||||
|
- 本块组的块位图位置、inode位图位置、inode表位置、空闲块数等
|
||||||
|
|
||||||
|
**块位图**(Block Bitmap):每个bit对应一个数据块,1=已用,0=空闲
|
||||||
|
|
||||||
|
**inode位图**(inode Bitmap):每个bit对应一个inode,1=已用,0=空闲
|
||||||
|
|
||||||
|
#### inode结构(128字节)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "inode 128字节"
|
||||||
|
A["模式/权限 2B"] --> B["所有者ID 2B"]
|
||||||
|
B --> C["文件大小 4B"]
|
||||||
|
C --> D["时间戳 12B
|
||||||
|
访问/修改/创建"]
|
||||||
|
D --> E["链接计数 2B"]
|
||||||
|
E --> F["数据块指针 60B"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "15个地址项(60字节)"
|
||||||
|
G["直接块指针 0~9
|
||||||
|
10个 × 4B"]
|
||||||
|
H["一次间接指针
|
||||||
|
1个 × 4B"]
|
||||||
|
I["二次间接指针
|
||||||
|
1个 × 4B"]
|
||||||
|
J["三次间接指针
|
||||||
|
1个 × 4B"]
|
||||||
|
end
|
||||||
|
|
||||||
|
F --> G
|
||||||
|
F --> H
|
||||||
|
F --> I
|
||||||
|
F --> J
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style G fill:#e8f5e9
|
||||||
|
style H fill:#fff3e0
|
||||||
|
style I fill:#ffcdd2
|
||||||
|
style J fill:#fce4ec
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 混合索引容量计算(重点)
|
||||||
|
|
||||||
|
设盘块大小为**4KB**,盘块号占**4字节**,inode有**13个地址项**(10直接 + 1一次间接 + 1二次间接 + 1三次间接):
|
||||||
|
|
||||||
|
每个盘块可存放盘块号个数:
|
||||||
|
$$4096 \div 4 = 1024 \text{ 个指针}$$
|
||||||
|
|
||||||
|
| 索引级别 | 计算过程 | 容量 |
|
||||||
|
|----------|----------|------|
|
||||||
|
| 直接块(10个) | 10 x 4KB | **40KB** |
|
||||||
|
| 一次间接 | 1024 x 4KB | **4MB** |
|
||||||
|
| 二次间接 | 1024 x 1024 x 4KB | **4GB** |
|
||||||
|
| 三次间接 | 1024 x 1024 x 1024 x 4KB | **4TB** |
|
||||||
|
| **总计** | | **约4TB** |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "混合索引寻址过程"
|
||||||
|
A["inode"] -->|"直接块指针0~9"| B["10个数据块
|
||||||
|
共40KB"]
|
||||||
|
A -->|"一次间接指针"| C["索引块1
|
||||||
|
1024个指针"]
|
||||||
|
C -->|"指针0~1023"| D["1024个数据块
|
||||||
|
共4MB"]
|
||||||
|
A -->|"二次间接指针"| E["二级索引块
|
||||||
|
1024个指针"]
|
||||||
|
E -->|"指针0~1023"| F["1024个一级索引块"]
|
||||||
|
F -->|"每个指向1024个数据块"| G["1024x1024个数据块
|
||||||
|
共4GB"]
|
||||||
|
A -->|"三次间接指针"| H["三级索引块"]
|
||||||
|
H --> I["1024个二级索引块"]
|
||||||
|
I --> J["1024x1024个一级索引块"]
|
||||||
|
J --> K["1024^3个数据块
|
||||||
|
共4TB"]
|
||||||
|
end
|
||||||
|
|
||||||
|
style A fill:#ffcdd2
|
||||||
|
style B fill:#e8f5e9
|
||||||
|
style D fill:#e8f5e9
|
||||||
|
style G fill:#e8f5e9
|
||||||
|
style K fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**寻址示例**:假设要读取文件的第15000个字节(盘块大小4KB):
|
||||||
|
- 逻辑块号 = 15000 / 4096 = 3(第4个块,索引为3)
|
||||||
|
- 块内偏移 = 15000 % 4096 = 2660
|
||||||
|
- 因为索引3 < 10,所以使用直接块指针[3]找到数据块
|
||||||
|
- 在该数据块的偏移2660处读取数据
|
||||||
|
|
||||||
|
#### 目录项 ext2_dir_entry_2
|
||||||
|
|
||||||
|
目录在Ext2中也是文件,内容是一系列目录项:
|
||||||
|
|
||||||
|
| 字段 | 大小 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| inode号 | 4字节 | 指向的inode编号 |
|
||||||
|
| rec_len | 2字节 | 本目录项总长度 |
|
||||||
|
| name_len | 1字节 | 文件名长度 |
|
||||||
|
| file_type | 1字节 | 文件类型(普通文件/目录/符号链接等) |
|
||||||
|
| name | 变长 | 文件名(不超过255字节) |
|
||||||
|
|
||||||
|
### 5. HDFS
|
||||||
|
|
||||||
|
HDFS(Hadoop Distributed File System)是大数据领域的分布式文件系统。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph HDFS架构
|
||||||
|
NN["名称节点 NameNode
|
||||||
|
元数据管理
|
||||||
|
文件→块映射
|
||||||
|
块→数据节点映射"]
|
||||||
|
DN1["数据节点 DataNode1
|
||||||
|
块1, 块3"]
|
||||||
|
DN2["数据节点 DataNode2
|
||||||
|
块2, 块4"]
|
||||||
|
DN3["数据节点 DataNode3
|
||||||
|
块1副本, 块5"]
|
||||||
|
end
|
||||||
|
|
||||||
|
NN -->|"心跳/块报告"| DN1
|
||||||
|
NN -->|"心跳/块报告"| DN2
|
||||||
|
NN -->|"心跳/块报告"| DN3
|
||||||
|
DN1 -->|"数据流"| CLIENT["客户端"]
|
||||||
|
|
||||||
|
style NN fill:#ffcdd2
|
||||||
|
style DN1 fill:#e1f5fe
|
||||||
|
style DN2 fill:#e1f5fe
|
||||||
|
style DN3 fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- 文件被分割为固定大小的块(默认128MB),分布在多个数据节点上
|
||||||
|
- 每个块默认3副本,分布在不同机架
|
||||||
|
- 名称节点管理元数据(内存中),数据节点存储实际数据
|
||||||
|
- 适合大文件顺序读取,不适合小文件和随机写入
|
||||||
|
|
||||||
|
### 6. 空闲空间管理
|
||||||
|
|
||||||
|
#### 空闲表法
|
||||||
|
|
||||||
|
用一张表记录所有空闲区的起始块号和长度:
|
||||||
|
|
||||||
|
| 起始块号 | 空闲块数 |
|
||||||
|
|----------|----------|
|
||||||
|
| 2 | 3 |
|
||||||
|
| 10 | 5 |
|
||||||
|
| 20 | 2 |
|
||||||
|
|
||||||
|
适用于连续分配方式,适合少量空闲区的情况。
|
||||||
|
|
||||||
|
#### 空闲链表法
|
||||||
|
|
||||||
|
将所有空闲磁盘块用指针链接成一个链表。分配时从链头取,回收时插入链尾。缺点是指针占用空间,分配效率低。
|
||||||
|
|
||||||
|
#### 位示图法
|
||||||
|
|
||||||
|
用一个**位图(bitmap)**记录每个磁盘块的使用状态,1=已用,0=空闲(或反之)。
|
||||||
|
|
||||||
|
```
|
||||||
|
位示图示例(每行16位):
|
||||||
|
第0行: 1 1 1 1 0 0 0 0 0 0 0 0 0 0 0 0 → 块0~3已用
|
||||||
|
第1行: 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 → 块16~31空闲
|
||||||
|
第2行: 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 → 块32~33已用
|
||||||
|
```
|
||||||
|
|
||||||
|
**地址转换公式**:
|
||||||
|
|
||||||
|
设每行有 **j** 位(通常为字长,如16位、32位),则第 **n** 个磁盘块对应位示图中:
|
||||||
|
|
||||||
|
$$\text{行号 } i = n \div j$$
|
||||||
|
$$\text{列号 } k = n \mod j$$
|
||||||
|
|
||||||
|
即:
|
||||||
|
$$n = j \times i + k$$
|
||||||
|
|
||||||
|
**经典公式**(每行16位时):$n = 16 \times i + j$
|
||||||
|
|
||||||
|
**位示图法示例**:设磁盘共200个块,每行16位,位示图需要 200/16 = 13 行。
|
||||||
|
|
||||||
|
要找到第75个空闲块:$i = 75 \div 16 = 4$,$j = 75 \mod 16 = 11$,即第4行第11列。
|
||||||
|
|
||||||
|
#### 成组链接法(UNIX)
|
||||||
|
|
||||||
|
UNIX采用**成组链接法**管理空闲磁盘块,是空闲表法和空闲链表法的结合:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
SB["超级块
|
||||||
|
栈:存放当前组的空闲块号
|
||||||
|
栈顶指针"] -->|"指向"| G1["空闲块组1
|
||||||
|
栈底块存储下一组的信息"]
|
||||||
|
G1 -->|"下一组指针"| G2["空闲块组2"]
|
||||||
|
G2 -->|"下一组指针"| G3["空闲块组3"]
|
||||||
|
G3 -->|"下一组指针"| G4["更多组..."]
|
||||||
|
|
||||||
|
style SB fill:#ffcdd2
|
||||||
|
style G1 fill:#e8f5e9
|
||||||
|
style G2 fill:#e8f5e9
|
||||||
|
style G3 fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**分配过程**:
|
||||||
|
1. 从超级块的栈中弹出一个空闲块号
|
||||||
|
2. 如果栈中只剩一个元素(它是下一组的指针),先将该组信息读入超级块,再弹出
|
||||||
|
3. 更新超级块
|
||||||
|
|
||||||
|
**回收过程**:
|
||||||
|
1. 将回收的块号压入超级块的栈中
|
||||||
|
2. 如果栈已满,将超级块中的栈信息写入回收块(成为新组),清空栈,将回收块号作为唯一元素
|
||||||
|
|
||||||
|
**优点**:分配和回收只需读写超级块(内存中),效率极高。
|
||||||
|
|
||||||
|
### 7. 磁盘IO优化
|
||||||
|
|
||||||
|
| 优化技术 | 原理 |
|
||||||
|
|----------|------|
|
||||||
|
| 磁盘高速缓存 | 在内存中开辟缓冲区缓存磁盘块,减少磁盘访问 |
|
||||||
|
| 提前读 | 顺序读取时,预先把后续块读入缓存 |
|
||||||
|
| 延迟写 | 先写入缓存,延迟到合适时机再写入磁盘 |
|
||||||
|
| 虚拟盘 | 用内存模拟磁盘(RAM Disk),速度极快但断电丢失 |
|
||||||
|
|
||||||
|
### 8. RAID
|
||||||
|
|
||||||
|
RAID(Redundant Array of Independent Disks)通过多块磁盘组合提高性能和可靠性。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph RAID0 条带化
|
||||||
|
A0["数据块1"] --> S0[磁盘0]
|
||||||
|
A1["数据块2"] --> S1[磁盘1]
|
||||||
|
A2["数据块3"] --> S0
|
||||||
|
A3["数据块4"] --> S1
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph RAID1 镜像
|
||||||
|
B0["数据"] --> M0[磁盘0]
|
||||||
|
B0 --> M1[磁盘1 镜像]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph RAID5 分布式校验
|
||||||
|
C0["数据A"] --> R0[磁盘0]
|
||||||
|
C1["数据B"] --> R1[磁盘1]
|
||||||
|
C2["P校验"] --> R2[磁盘2]
|
||||||
|
C3["数据C"] --> R3[磁盘3]
|
||||||
|
end
|
||||||
|
|
||||||
|
style S0 fill:#e1f5fe
|
||||||
|
style S1 fill:#e1f5fe
|
||||||
|
style M0 fill:#e8f5e9
|
||||||
|
style M1 fill:#fff3e0
|
||||||
|
style R0 fill:#e1f5fe
|
||||||
|
style R1 fill:#e1f5fe
|
||||||
|
style R2 fill:#ffcdd2
|
||||||
|
style R3 fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
| RAID级别 | 原理 | 冗余 | 最少磁盘 | 利用率 | 特点 |
|
||||||
|
|----------|------|------|----------|--------|------|
|
||||||
|
| RAID0 | 数据条带化分布 | 无 | 2 | 100% | 高性能,无容错 |
|
||||||
|
| RAID1 | 数据完全镜像 | 100% | 2 | 50% | 高可靠,成本高 |
|
||||||
|
| RAID3 | 位交叉+专用校验盘 | 1块校验盘 | 3 | (N-1)/N | 校验盘成瓶颈 |
|
||||||
|
| RAID5 | 块交叉+分布式校验 | 分布式校验 | 3 | (N-1)/N | 性能与可靠性平衡 |
|
||||||
|
|
||||||
|
### 9. 磁盘容错
|
||||||
|
|
||||||
|
| 容错级别 | 技术 | 内容 |
|
||||||
|
|----------|------|------|
|
||||||
|
| SFT-I | 一级容错 | 双份目录和FAT表、热修复重定向(写入坏块时重定向到备用块) |
|
||||||
|
| SFT-II | 二级容错 | 磁盘镜像(同一控制器两个磁盘)、磁盘双工(不同控制器两个磁盘) |
|
||||||
|
| 集群容错 | 三级容错 | 多台服务器组成集群,一台故障其他接管 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 动手实践
|
||||||
|
|
||||||
|
### 查看文件系统信息
|
||||||
|
```bash
|
||||||
|
# 查看磁盘使用情况
|
||||||
|
df -h
|
||||||
|
|
||||||
|
# 查看inode使用情况
|
||||||
|
df -i
|
||||||
|
|
||||||
|
# 查看文件系统类型和块大小
|
||||||
|
tune2fs -l /dev/sda1 | grep -E "Block size|Inode count"
|
||||||
|
|
||||||
|
# 查看文件的块分配
|
||||||
|
stat filename
|
||||||
|
|
||||||
|
# 查看文件的inode号
|
||||||
|
ls -i filename
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 知识关联
|
||||||
|
- 文件IO的read/write最终需要将数据写入 [[04_文件IO编程]] 中的物理磁盘
|
||||||
|
- 磁盘调度算法在 [[17_IO系统]] 中有详细讲解
|
||||||
|
- 文件系统是 [[13_存储管理基础]] 中存储管理的重要组成部分
|
||||||
|
- 分页存储管理的思想与Ext2的块分配有相似之处,见 [[14_分页存储管理]]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 思考题
|
||||||
|
|
||||||
|
1. **FAT表计算**:一个FAT16分区,每簇4KB,最多能管理多大的分区?为什么?
|
||||||
|
2. **Ext2容量计算**:如果盘块大小为1KB(而非4KB),盘块号占4字节,那么混合索引支持的最大文件是多少?(提示:每个间接块只能放256个指针)
|
||||||
|
3. **inode寻址**:给定盘块大小4KB,盘块号4字节,要读取文件偏移5GB处的数据,需要经过几级间接索引?
|
||||||
|
4. **成组链接法**:如果超级块栈最多容纳100个空闲块号,那么分配第101个空闲块时会发生什么?
|
||||||
|
5. **RAID选择**:一个视频监控系统需要大容量存储、持续写入、偶尔丢失可接受,应选择RAID几?为什么?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 扩展阅读
|
||||||
|
- 《计算机操作系统》(汤小丹)第5章:文件管理
|
||||||
|
- 《操作系统概念》第11-12章:文件系统接口与实现
|
||||||
|
- [ext2文件系统详解](https://www.nongnu.org/ext2-intro/)
|
||||||
|
- [RAID级别详解](https://www.techtarget.com/searchstorage/definition/RAID)
|
||||||
868
操作系统/06_进程控制/06_进程控制.md
Normal file
868
操作系统/06_进程控制/06_进程控制.md
Normal file
@@ -0,0 +1,868 @@
|
|||||||
|
# 第06讲:进程控制
|
||||||
|
|
||||||
|
> **本节目标**:理解进程的概念与生命周期,掌握 `fork()`、`exec()`、`wait()`、`exit()` 等进程控制系统调用,理解信号机制与僵尸进程,最终能实现一个简易 shell 和 daemon 进程。
|
||||||
|
|
||||||
|
## 前置知识
|
||||||
|
|
||||||
|
- [[03_C语言编程基础]] -- C 语言指针、数组、字符串操作
|
||||||
|
- [[07_多线程编程]] -- 线程与进程的对比
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、进程概念
|
||||||
|
|
||||||
|
### 1.1 什么是进程
|
||||||
|
|
||||||
|
**进程(Process)** 是程序的一次执行实例。程序是静态的代码文件,进程是动态的执行过程。当操作系统将程序加载到内存并开始执行时,就创建了一个进程。
|
||||||
|
|
||||||
|
每个进程拥有独立的地址空间,包含以下组成部分:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
P[进程] --> T[代码段 Text]
|
||||||
|
P --> D[数据段 Data]
|
||||||
|
P --> H[堆 Heap]
|
||||||
|
P --> S[栈 Stack]
|
||||||
|
P --> PCB[PCB 进程控制块]
|
||||||
|
|
||||||
|
T -->|存放| T1[程序指令]
|
||||||
|
D -->|存放| D1[全局变量/静态变量]
|
||||||
|
H -->|动态分配| H1[malloc/free]
|
||||||
|
S -->|自动管理| S1[局部变量/函数调用]
|
||||||
|
PCB -->|记录| PCB1[PID/状态/寄存器/文件描述符]
|
||||||
|
|
||||||
|
style P fill:#fff3e0
|
||||||
|
style PCB fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 进程 vs 程序
|
||||||
|
|
||||||
|
| 对比维度 | 程序(Program) | 进程(Process) |
|
||||||
|
|----------|-----------------|-----------------|
|
||||||
|
| 本质 | 静态的代码文件 | 动态的执行过程 |
|
||||||
|
| 存储位置 | 硬盘 | 内存 |
|
||||||
|
| 生命周期 | 长期保存 | 有创建、运行、终止 |
|
||||||
|
| 对应关系 | 一个程序可对应多个进程 | 一个进程只对应一个程序 |
|
||||||
|
| 包含内容 | 代码 + 数据 | 代码 + 数据 + 堆栈 + PCB |
|
||||||
|
|
||||||
|
### 1.3 进程状态
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> 创建: fork()
|
||||||
|
创建 --> 就绪: 分配资源
|
||||||
|
就绪 --> 运行: 调度器选中
|
||||||
|
运行 --> 就绪: 时间片用完
|
||||||
|
运行 --> 阻塞: 等待I/O
|
||||||
|
阻塞 --> 就绪: I/O完成
|
||||||
|
运行 --> 终止: exit()
|
||||||
|
终止 --> [*]
|
||||||
|
|
||||||
|
就绪: Ready
|
||||||
|
运行: Running
|
||||||
|
阻塞: Blocked/Waiting
|
||||||
|
终止: Terminated/Zombie
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 进程标识
|
||||||
|
|
||||||
|
每个进程都有唯一的 **PID**(Process ID)。常用函数:
|
||||||
|
|
||||||
|
```c
|
||||||
|
pid_t getpid(); // 获取当前进程的 PID
|
||||||
|
pid_t getppid(); // 获取父进程的 PID
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、fork() -- 创建子进程
|
||||||
|
|
||||||
|
### 2.1 基本原理
|
||||||
|
|
||||||
|
`fork()` 是 Linux 中创建新进程的唯一方式。调用 `fork()` 后,操作系统会创建一个与父进程几乎完全相同的子进程。子进程获得父进程的代码段、数据段、堆栈的**副本**。
|
||||||
|
|
||||||
|
**关键特性**:`fork()` 被调用一次,但**返回两次**:
|
||||||
|
- 父进程中返回子进程的 PID(正整数)
|
||||||
|
- 子进程中返回 0
|
||||||
|
- 如果出错返回 -1
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A["父进程调用 fork()"] --> B{操作系统创建子进程}
|
||||||
|
B --> C["父进程:返回子进程 PID(>0)"]
|
||||||
|
B --> D["子进程:返回 0"]
|
||||||
|
C --> E["父子进程各自独立执行"]
|
||||||
|
D --> E
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style C fill:#fff3e0
|
||||||
|
style D fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 fork 基础示例
|
||||||
|
|
||||||
|
参考 `实例源代码/chap5/fork1.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "wrapper.h"
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
pid_t pid;
|
||||||
|
int x = 1;
|
||||||
|
|
||||||
|
pid = fork();
|
||||||
|
if (pid == 0) { // 子进程执行这段代码
|
||||||
|
x = x + 1;
|
||||||
|
printf("child: x=%d\n", x); // 输出 child: x=2
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pid > 0) { // 父进程执行这段代码
|
||||||
|
x = x - 1;
|
||||||
|
printf("parent: x=%d\n", x); // 输出 parent: x=0
|
||||||
|
}
|
||||||
|
sleep(10); // 让父子进程都执行完代码
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键理解**:
|
||||||
|
- `fork()` 后,父子进程拥有**独立的变量副本**
|
||||||
|
- 子进程修改 `x` 不影响父进程的 `x`,反之亦然
|
||||||
|
- 父子进程的执行顺序是不确定的,取决于调度器
|
||||||
|
|
||||||
|
### 2.3 多次 fork 的进程数计算
|
||||||
|
|
||||||
|
参考 `实例源代码/chap5/fork3.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
fork(); // 第1次:2个进程
|
||||||
|
fork(); // 第2次:4个进程
|
||||||
|
fork(); // 第3次:8个进程
|
||||||
|
printf("hello \n");
|
||||||
|
sleep(10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**进程树分析**:n 次 `fork()` 会产生 2^n 个进程。上面的代码执行 3 次 `fork()`,最终产生 2^3 = 8 个进程,每个进程打印一次 "hello"。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
P0["P0 (原始进程)"] -->|第1次fork| P1["P1"]
|
||||||
|
P0 -->|继续| P0a["P0"]
|
||||||
|
P0a -->|第2次fork| P2["P2"]
|
||||||
|
P0a -->|继续| P0b["P0"]
|
||||||
|
P1 -->|第2次fork| P3["P3"]
|
||||||
|
P1 -->|继续| P1a["P1"]
|
||||||
|
P0b -->|第3次fork| P4["P4"]
|
||||||
|
P1a -->|第3次fork| P5["P5"]
|
||||||
|
P2 -->|第3次fork| P6["P6"]
|
||||||
|
P3 -->|第3次fork| P7["P7"]
|
||||||
|
|
||||||
|
style P0 fill:#ffcdd2
|
||||||
|
style P1 fill:#e1f5fe
|
||||||
|
style P2 fill:#e1f5fe
|
||||||
|
style P3 fill:#e1f5fe
|
||||||
|
style P4 fill:#e8f5e9
|
||||||
|
style P5 fill:#e8f5e9
|
||||||
|
style P6 fill:#e8f5e9
|
||||||
|
style P7 fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 fork 的复杂示例
|
||||||
|
|
||||||
|
参考 `实例源代码/chap5/fork2.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
int pid;
|
||||||
|
pid = fork(); // 创建 P1
|
||||||
|
pid = fork(); // P0 和 P1 各创建一个子进程
|
||||||
|
if (pid > 0) fork(); // pid>0 的进程再创建一个子进程
|
||||||
|
printf("hello \n");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**分析**:需要追踪每个进程的 `pid` 值来确定哪些进程会执行第三次 `fork()`。最终打印 6 个 "hello"。
|
||||||
|
|
||||||
|
### 2.5 fork 的注意事项
|
||||||
|
|
||||||
|
| 要点 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 返回值判断 | 必须用 `if-else` 分别处理父子进程 |
|
||||||
|
| 资源复制 | 子进程获得父进程的副本(写时复制 COW 优化) |
|
||||||
|
| 执行顺序 | 父子进程执行顺序不确定 |
|
||||||
|
| 文件描述符 | 子进程继承父进程打开的文件描述符 |
|
||||||
|
| 必须 exit | 子进程处理完后必须调用 `exit()` 终止 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、exec 族函数 -- 替换进程映像
|
||||||
|
|
||||||
|
### 3.1 基本原理
|
||||||
|
|
||||||
|
`exec` 族函数用一个新的程序**替换**当前进程的代码段、数据段和堆栈。进程的 PID 不变,但执行的代码完全改变。`exec` 调用成功后**不会返回**,只有出错时才返回 -1。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A["当前进程<br/>PID=1234<br/>执行 myprogram"] -->|execvp("ps", args)| B["同一进程<br/>PID=1234<br/>执行 ps 命令"]
|
||||||
|
B --> C["原来的代码段<br/>被完全替换"]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style B fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 exec 族函数对比
|
||||||
|
|
||||||
|
| 函数 | 参数形式 | 路径搜索 | 环境变量 |
|
||||||
|
|------|----------|----------|----------|
|
||||||
|
| `execl(path, arg0, ..., NULL)` | 列表 | 完整路径 | 继承父进程 |
|
||||||
|
| `execlp(file, arg0, ..., NULL)` | 列表 | 搜索 PATH | 继承父进程 |
|
||||||
|
| `execle(path, arg0, ..., NULL, envp)` | 列表 | 完整路径 | 自定义 |
|
||||||
|
| `execv(path, argv)` | 数组 | 完整路径 | 继承父进程 |
|
||||||
|
| `execvp(file, argv)` | 数组 | 搜索 PATH | 继承父进程 |
|
||||||
|
|
||||||
|
**记忆技巧**:
|
||||||
|
- `l` = list(参数列表),`v` = vector(参数数组)
|
||||||
|
- `p` = path(搜索 PATH 环境变量),`e` = environment(自定义环境变量)
|
||||||
|
|
||||||
|
### 3.3 exec 示例
|
||||||
|
|
||||||
|
参考 `实例源代码/chap5/exec1.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "wrapper.h"
|
||||||
|
int main(void)
|
||||||
|
{
|
||||||
|
char *arg[] = {"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};
|
||||||
|
execvp("ps", arg); // 用 ps 命令替换当前进程
|
||||||
|
perror("exec ps"); // 如果 execvp 返回,说明出错
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**运行结果**:程序执行后变成了 `ps` 命令,显示当前系统的进程信息。
|
||||||
|
|
||||||
|
### 3.4 fork + exec 组合
|
||||||
|
|
||||||
|
在实际应用中,`fork()` 和 `exec()` 通常配合使用:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant 父进程
|
||||||
|
participant 子进程
|
||||||
|
participant 新程序
|
||||||
|
|
||||||
|
父进程->>子进程: fork()
|
||||||
|
Note over 子进程: 子进程是父进程的副本
|
||||||
|
子进程->>新程序: execvp("ls", args)
|
||||||
|
Note over 新程序: 子进程的代码被 ls 替换
|
||||||
|
新程序-->>父进程: 执行完毕
|
||||||
|
父进程->>父进程: wait() 回收子进程
|
||||||
|
```
|
||||||
|
|
||||||
|
这种模式是 shell 执行命令的核心机制:先 `fork` 创建子进程,再在子进程中 `exec` 执行新程序。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、wait/waitpid -- 等待子进程
|
||||||
|
|
||||||
|
### 4.1 为什么需要 wait
|
||||||
|
|
||||||
|
父进程创建子进程后,需要等待子进程结束并回收其资源。如果不调用 `wait`,子进程终止后会变成**僵尸进程**(详见第六节)。
|
||||||
|
|
||||||
|
### 4.2 wait 与 waitpid
|
||||||
|
|
||||||
|
| 函数 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `wait(&status)` | 等待任意一个子进程结束 |
|
||||||
|
| `waitpid(pid, &status, options)` | 可指定等待的子进程 |
|
||||||
|
| `WIFEXITED(status)` | 判断子进程是否正常退出 |
|
||||||
|
| `WEXITSTATUS(status)` | 获取子进程的退出状态 |
|
||||||
|
|
||||||
|
**waitpid 的 pid 参数**:
|
||||||
|
- `pid > 0`:等待指定 PID 的子进程
|
||||||
|
- `pid = -1`:等待任意子进程(等同于 `wait`)
|
||||||
|
- `pid = 0`:等待同一进程组的任意子进程
|
||||||
|
- `pid < -1`:等待进程组 ID 为 |pid| 的任意子进程
|
||||||
|
|
||||||
|
### 4.3 waitpid 示例
|
||||||
|
|
||||||
|
参考 `实例源代码/chap5/waitpid1.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "wrapper.h"
|
||||||
|
#define N 2
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
int status, i;
|
||||||
|
pid_t pid;
|
||||||
|
|
||||||
|
// 父进程创建 N 个子进程
|
||||||
|
for (i = 0; i < 2; i++)
|
||||||
|
if ((pid = fork()) == 0) // 子进程
|
||||||
|
exit(100 + i); // 以不同状态退出
|
||||||
|
|
||||||
|
// 父进程按任意顺序等待所有子进程
|
||||||
|
while ((pid = waitpid(-1, &status, 0)) > 0) {
|
||||||
|
if (WIFEXITED(status))
|
||||||
|
printf("child %d terminated normally with exit status=%d\n",
|
||||||
|
pid, WEXITSTATUS(status));
|
||||||
|
else
|
||||||
|
printf("child %d terminated abnormally\n", pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 所有子进程已结束,waitpid 返回 -1,errno 为 ECHILD
|
||||||
|
if (errno != ECHILD)
|
||||||
|
perror("waitpid error");
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出示例**:
|
||||||
|
```
|
||||||
|
child 1235 terminated normally with exit status=100
|
||||||
|
child 1236 terminated normally with exit status=101
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、exit/_exit -- 进程终止
|
||||||
|
|
||||||
|
### 5.1 两种终止方式
|
||||||
|
|
||||||
|
| 函数 | 头文件 | 行为 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `exit(status)` | `<stdlib.h>` | 执行清理工作(刷新缓冲区、调用 atexit 注册的函数),然后终止 |
|
||||||
|
| `_exit(status)` | `<unistd.h>` | 立即终止,不执行任何清理 |
|
||||||
|
|
||||||
|
### 5.2 exit 的清理过程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A["调用 exit(status)"] --> B["执行 atexit() 注册的清理函数"]
|
||||||
|
B --> C["刷新 stdio 缓冲区<br/>(fclose 所有打开的流)"]
|
||||||
|
C --> D["调用 _exit(status)"]
|
||||||
|
D --> E["内核回收进程资源"]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style E fill:#ffcdd2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.3 退出状态
|
||||||
|
|
||||||
|
参考 `实例源代码/chap5/exitstatus.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdlib.h>
|
||||||
|
int main() { exit(100); }
|
||||||
|
```
|
||||||
|
|
||||||
|
父进程通过 `waitpid` 的 `WEXITSTATUS(status)` 宏获取子进程的退出状态值(0-255)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、僵尸进程
|
||||||
|
|
||||||
|
### 6.1 什么是僵尸进程
|
||||||
|
|
||||||
|
当子进程终止后,如果父进程没有调用 `wait()` 或 `waitpid()` 回收子进程的退出状态,子进程的进程控制块(PCB)仍然保留在系统中,成为**僵尸进程(Zombie Process)**。
|
||||||
|
|
||||||
|
### 6.2 僵尸进程的产生
|
||||||
|
|
||||||
|
参考 `实例源代码/chap5/zombie.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
pid_t pid;
|
||||||
|
if ((pid = fork()) < 0) {
|
||||||
|
perror("fork failed");
|
||||||
|
exit(1);
|
||||||
|
} else if (pid == 0) {
|
||||||
|
printf("child...\n"); // 子进程打印后退出
|
||||||
|
} else {
|
||||||
|
printf("parent...\n");
|
||||||
|
while (1); // 父进程不调用 wait(),子进程成为僵尸
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 僵尸进程的生命周期
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant 父进程
|
||||||
|
participant 子进程
|
||||||
|
participant 操作系统内核
|
||||||
|
|
||||||
|
父进程->>子进程: fork() 创建子进程
|
||||||
|
子进程->>子进程: 执行任务
|
||||||
|
子进程->>操作系统内核: exit() 终止
|
||||||
|
操作系统内核->>父进程: 发送 SIGCHLD 信号
|
||||||
|
Note over 父进程: 父进程没有调用 wait()
|
||||||
|
Note over 操作系统内核: 子进程的 PCB 无法释放
|
||||||
|
Note over 操作系统内核: 子进程成为僵尸进程
|
||||||
|
Note over 操作系统内核: 僵尸进程占用 PID 资源
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 僵尸进程的危害与解决
|
||||||
|
|
||||||
|
| 危害 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 占用 PID | 僵尸进程的 PID 无法被复用 |
|
||||||
|
| 资源泄漏 | 虽然不占内存,但 PCB 信息占用内核资源 |
|
||||||
|
| 累积效应 | 大量僵尸进程可耗尽 PID 资源 |
|
||||||
|
|
||||||
|
**解决方法**:
|
||||||
|
1. 父进程调用 `wait()` 或 `waitpid()` 回收子进程
|
||||||
|
2. 注册 `SIGCHLD` 信号处理函数,在其中调用 `waitpid()`
|
||||||
|
3. 父进程先退出,让 init 进程(PID=1)自动回收孤儿进程
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、信号机制
|
||||||
|
|
||||||
|
### 7.1 信号的概念
|
||||||
|
|
||||||
|
信号是操作系统通知进程发生了某种事件的一种**异步通信机制**。进程可以注册信号处理函数来响应特定信号。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
SRC["信号来源"] --> SIG["信号"]
|
||||||
|
SIG --> PROC["目标进程"]
|
||||||
|
|
||||||
|
SRC --> S1["用户输入 Ctrl+C"]
|
||||||
|
SRC --> S2["kill 命令"]
|
||||||
|
SRC --> S3["子进程终止"]
|
||||||
|
SRC --> S4["定时器超时"]
|
||||||
|
SRC --> S5["非法内存访问"]
|
||||||
|
|
||||||
|
PROC --> H1["执行信号处理函数"]
|
||||||
|
PROC --> H2["忽略信号"]
|
||||||
|
PROC --> H3["执行默认动作"]
|
||||||
|
|
||||||
|
style SIG fill:#fff3e0
|
||||||
|
style SRC fill:#e1f5fe
|
||||||
|
style PROC fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 常见信号
|
||||||
|
|
||||||
|
| 信号 | 编号 | 默认动作 | 说明 |
|
||||||
|
|------|:----:|----------|------|
|
||||||
|
| `SIGINT` | 2 | 终止 | 用户按 Ctrl+C |
|
||||||
|
| `SIGKILL` | 9 | 终止 | 强制终止(**不可捕获**) |
|
||||||
|
| `SIGTERM` | 15 | 终止 | 请求终止(可捕获) |
|
||||||
|
| `SIGCHLD` | 17 | 忽略 | 子进程状态改变 |
|
||||||
|
| `SIGALRM` | 14 | 终止 | 定时器超时 |
|
||||||
|
| `SIGSEGV` | 11 | 终止+core | 段错误 |
|
||||||
|
| `SIGSTOP` | 19 | 停止 | 暂停进程(**不可捕获**) |
|
||||||
|
| `SIGCONT` | 18 | 继续 | 恢复被暂停的进程 |
|
||||||
|
|
||||||
|
### 7.3 signal() 注册信号处理函数
|
||||||
|
|
||||||
|
参考 `实例源代码/chap5/signal1.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
void handler1(int sig)
|
||||||
|
{
|
||||||
|
pid_t pid;
|
||||||
|
if ((pid = waitpid(-1, NULL, 0)) < 0)
|
||||||
|
perror("waitpid error");
|
||||||
|
printf("Handler reaped child %d\n", (int)pid);
|
||||||
|
sleep(2);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
int i, n;
|
||||||
|
char buf[MAXBUF];
|
||||||
|
|
||||||
|
// 注册 SIGCHLD 信号处理函数
|
||||||
|
if (signal(SIGCHLD, handler1) == SIG_ERR)
|
||||||
|
perror("signal error");
|
||||||
|
|
||||||
|
// 父进程创建 3 个子进程
|
||||||
|
for (i = 0; i < 3; i++) {
|
||||||
|
if (fork() == 0) {
|
||||||
|
printf("Hello from child %d\n", (int)getpid());
|
||||||
|
sleep(1);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 父进程等待终端输入
|
||||||
|
if ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0)
|
||||||
|
perror("read");
|
||||||
|
|
||||||
|
printf("Parent processing input\n");
|
||||||
|
while (1);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**信号处理流程**:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant 父进程
|
||||||
|
participant 子进程1
|
||||||
|
participant 子进程2
|
||||||
|
participant 子进程3
|
||||||
|
participant 信号处理函数
|
||||||
|
|
||||||
|
父进程->>信号处理函数: signal(SIGCHLD, handler1)
|
||||||
|
父进程->>子进程1: fork()
|
||||||
|
父进程->>子进程2: fork()
|
||||||
|
父进程->>子进程3: fork()
|
||||||
|
|
||||||
|
子进程1->>父进程: exit() 触发 SIGCHLD
|
||||||
|
父进程->>信号处理函数: 调用 handler1()
|
||||||
|
信号处理函数->>信号处理函数: waitpid() 回收子进程
|
||||||
|
|
||||||
|
子进程2->>父进程: exit() 触发 SIGCHLD
|
||||||
|
父进程->>信号处理函数: 调用 handler1()
|
||||||
|
信号处理函数->>信号处理函数: waitpid() 回收子进程
|
||||||
|
|
||||||
|
子进程3->>父进程: exit() 触发 SIGCHLD
|
||||||
|
父进程->>信号处理函数: 调用 handler1()
|
||||||
|
信号处理函数->>信号处理函数: waitpid() 回收子进程
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.4 SIGALRM 定时器
|
||||||
|
|
||||||
|
参考 `实例源代码/chap5/alarm.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
void handler(int sig)
|
||||||
|
{
|
||||||
|
static int beeps = 0;
|
||||||
|
printf("BEEP\n");
|
||||||
|
if (++beeps < 5)
|
||||||
|
alarm(1); // 1秒后再次触发 SIGALRM
|
||||||
|
else {
|
||||||
|
printf("BOOM!\n");
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
signal(SIGALRM, handler); // 注册 SIGALRM 处理函数
|
||||||
|
alarm(1); // 1秒后触发第一次 SIGALRM
|
||||||
|
while (1); // 等待信号
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出**:每隔 1 秒打印一次 "BEEP",第 5 次后打印 "BOOM!" 并退出。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、Shell 的实现
|
||||||
|
|
||||||
|
### 8.1 Shell 的工作原理
|
||||||
|
|
||||||
|
Shell 是一个命令解释器,其核心逻辑可以用一个循环概括:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A["打印提示符 %"] --> B["读取用户输入命令"]
|
||||||
|
B --> C{"命令是否为空?"}
|
||||||
|
C -->|是| A
|
||||||
|
C -->|否| D{"是否为内置命令?"}
|
||||||
|
D -->|是 exit| E["退出 shell"]
|
||||||
|
D -->|是其他| F["执行内置命令"]
|
||||||
|
F --> A
|
||||||
|
D -->|否| G["fork() 创建子进程"]
|
||||||
|
G --> H["子进程: execvp() 执行命令"]
|
||||||
|
G --> I["父进程: waitpid() 等待子进程"]
|
||||||
|
H --> A
|
||||||
|
I --> A
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style E fill:#ffcdd2
|
||||||
|
style G fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.2 shellex.c 核心实现
|
||||||
|
|
||||||
|
参考 `实例源代码/chap5/shellex.c`,这是一个完整的简易 shell 实现:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "wrapper.h"
|
||||||
|
#define MAXARGS 128
|
||||||
|
|
||||||
|
// 解析命令行,返回是否为后台作业
|
||||||
|
int parseline(char *buf, char **argv)
|
||||||
|
{
|
||||||
|
char *delim;
|
||||||
|
int argc;
|
||||||
|
int bg;
|
||||||
|
|
||||||
|
buf[strlen(buf)-1] = ' '; // 用空格替换末尾换行
|
||||||
|
while (*buf && (*buf == ' ')) // 跳过前导空格
|
||||||
|
buf++;
|
||||||
|
|
||||||
|
argc = 0;
|
||||||
|
while ((delim = strchr(buf, ' '))) {
|
||||||
|
argv[argc++] = buf;
|
||||||
|
*delim = '\0';
|
||||||
|
buf = delim + 1;
|
||||||
|
while (*buf && (*buf == ' '))
|
||||||
|
buf++;
|
||||||
|
}
|
||||||
|
argv[argc] = NULL;
|
||||||
|
|
||||||
|
if (argc == 0) return 1;
|
||||||
|
|
||||||
|
// 检查是否应在后台执行(最后一个参数为 &)
|
||||||
|
if ((bg = (*argv[argc-1] == '&')) != 0)
|
||||||
|
argv[--argc] = NULL;
|
||||||
|
return bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 执行命令
|
||||||
|
void execute(char *cmdline)
|
||||||
|
{
|
||||||
|
char *argv[MAXARGS];
|
||||||
|
char buf[MAXLINE];
|
||||||
|
int bg;
|
||||||
|
pid_t pid;
|
||||||
|
|
||||||
|
strcpy(buf, cmdline);
|
||||||
|
bg = parseline(buf, argv);
|
||||||
|
if (argv[0] == NULL) return;
|
||||||
|
|
||||||
|
if (!builtin_command(argv)) {
|
||||||
|
if ((pid = fork()) == 0) { // 子进程
|
||||||
|
if (execvp(argv[0], argv) < 0) {
|
||||||
|
printf("%s: Command not found.\n", argv[0]);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bg) { // 前台执行
|
||||||
|
int status;
|
||||||
|
if (waitpid(pid, &status, 0) < 0)
|
||||||
|
perror("waitpid error");
|
||||||
|
}
|
||||||
|
else // 后台执行
|
||||||
|
printf("%d %s", pid, cmdline);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断内置命令(如 exit)
|
||||||
|
int builtin_command(char **argv)
|
||||||
|
{
|
||||||
|
if (!strcmp(argv[0], "exit"))
|
||||||
|
exit(0);
|
||||||
|
if (!strcmp(argv[0], "&"))
|
||||||
|
return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
char cmdline[MAXLINE];
|
||||||
|
while (1) {
|
||||||
|
printf("%% "); // 打印提示符
|
||||||
|
fgets(cmdline, MAXLINE, stdin); // 读取命令
|
||||||
|
if (feof(stdin)) exit(0);
|
||||||
|
execute(cmdline); // 执行命令
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 Shell 执行流程图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant 用户
|
||||||
|
participant Shell主进程
|
||||||
|
participant 子进程
|
||||||
|
participant 操作系统
|
||||||
|
|
||||||
|
loop Shell 主循环
|
||||||
|
用户->>Shell主进程: 输入 "ls -l"
|
||||||
|
Shell主进程->>Shell主进程: parseline() 解析命令
|
||||||
|
Shell主进程->>子进程: fork() 创建子进程
|
||||||
|
子进程->>操作系统: execvp("ls", ["ls", "-l", NULL])
|
||||||
|
Note over 操作系统: ls 程序替换子进程
|
||||||
|
操作系统-->>子进程: ls 执行完毕
|
||||||
|
子进程->>Shell主进程: exit()
|
||||||
|
Shell主进程->>Shell主进程: waitpid() 回收子进程
|
||||||
|
Shell主进程-->>用户: 显示结果,打印提示符
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.4 前台进程与后台进程
|
||||||
|
|
||||||
|
- **前台进程**:Shell 调用 `waitpid()` 等待子进程结束后才继续接受输入
|
||||||
|
- **后台进程**:Shell 不等待,直接打印子进程 PID 并继续接受输入(命令末尾加 `&`)
|
||||||
|
|
||||||
|
### 8.5 管道与重定向
|
||||||
|
|
||||||
|
实验中的 `task52.c` 实现了管道和重定向功能:
|
||||||
|
|
||||||
|
**输出重定向**:使用 `dup2()` 将标准输出重定向到文件
|
||||||
|
```c
|
||||||
|
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
dup2(fd, STDOUT_FILENO); // 将 STDOUT 重定向到文件
|
||||||
|
close(fd);
|
||||||
|
execvp(args[0], args); // 执行命令,输出写入文件
|
||||||
|
```
|
||||||
|
|
||||||
|
**管道**:使用 `pipe()` 创建管道,连接两个进程的输入输出
|
||||||
|
```c
|
||||||
|
int pipefd[2];
|
||||||
|
pipe(pipefd); // pipefd[0]=读端, pipefd[1]=写端
|
||||||
|
|
||||||
|
// 子进程1: 写入管道
|
||||||
|
dup2(pipefd[1], STDOUT_FILENO);
|
||||||
|
close(pipefd[0]);
|
||||||
|
execvp(cmd1_args[0], cmd1_args);
|
||||||
|
|
||||||
|
// 子进程2: 从管道读取
|
||||||
|
dup2(pipefd[0], STDIN_FILENO);
|
||||||
|
close(pipefd[1]);
|
||||||
|
execvp(cmd2_args[0], cmd2_args);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、Daemon 进程
|
||||||
|
|
||||||
|
### 9.1 什么是 Daemon
|
||||||
|
|
||||||
|
Daemon(守护进程)是在后台运行的特殊进程,通常在系统启动时创建,一直运行到系统关闭。常见的 daemon 有:`sshd`(SSH 服务)、`httpd`(Web 服务)、`crond`(定时任务)等。
|
||||||
|
|
||||||
|
### 9.2 Daemon 的特点
|
||||||
|
|
||||||
|
- 没有控制终端(不与任何终端关联)
|
||||||
|
- 在后台运行
|
||||||
|
- 父进程是 init(PID=1)
|
||||||
|
- 通常以 `d` 结尾命名
|
||||||
|
|
||||||
|
### 9.3 创建 Daemon 的五步法
|
||||||
|
|
||||||
|
参考 `实例源代码/chap5/daemon.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
int init_daemon(void)
|
||||||
|
{
|
||||||
|
pid_t pid;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
// 第1步:fork() + 父进程退出,脱离终端控制
|
||||||
|
pid = fork();
|
||||||
|
if (pid == -1) return -1;
|
||||||
|
else if (pid != 0) exit(EXIT_SUCCESS);
|
||||||
|
|
||||||
|
// 第2步:setsid() 创建新会话,成为会话首进程
|
||||||
|
if (setsid() == -1) return -1;
|
||||||
|
|
||||||
|
// 第3步:(可选) 再次 fork(),确保不会重新获得控制终端
|
||||||
|
// daemon.c 中省略了这一步,但 task53.c 中包含
|
||||||
|
|
||||||
|
// 第4步:chdir("/") 避免占用可卸载的文件系统
|
||||||
|
if (chdir("/") == -1) return -1;
|
||||||
|
|
||||||
|
// 第5步:关闭所有打开的文件描述符,重定向 0/1/2 到 /dev/null
|
||||||
|
for (i = 0; i < NR_OPEN; i++) close(i);
|
||||||
|
open("/dev/null", O_RDWR); // stdin -> /dev/null
|
||||||
|
dup(0); // stdout -> /dev/null
|
||||||
|
dup(0); // stderr -> /dev/null
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**五步法流程图**:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A["第1步: fork() + 父进程 exit()"] --> B["子进程成为孤儿进程<br/>被 init 收养"]
|
||||||
|
B --> C["第2步: setsid()"]
|
||||||
|
C --> D["创建新会话<br/>成为会话首进程<br/>脱离控制终端"]
|
||||||
|
D --> E["第3步: fork() + 父进程 exit()"]
|
||||||
|
E --> F["确保不会重新获得<br/>控制终端"]
|
||||||
|
F --> G["第4步: chdir('/')"]
|
||||||
|
G --> H["避免占用可卸载的<br/>文件系统"]
|
||||||
|
H --> I["第5步: 关闭 0/1/2<br/>重定向到 /dev/null"]
|
||||||
|
I --> J["Daemon 创建完成<br/>在后台运行"]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style J fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.4 Daemon 文件监控实例
|
||||||
|
|
||||||
|
实验中的 `task53.c` 实现了一个 daemon 文件监控程序:
|
||||||
|
1. 创建守护进程
|
||||||
|
2. 每隔 5 分钟读取目标文件内容,计算 hash 值
|
||||||
|
3. 与上一次的 hash 值对比
|
||||||
|
4. 如果文件被篡改,将事件记录到日志文件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、实验任务概览
|
||||||
|
|
||||||
|
本讲对应 [[实验02_进程控制]],包含以下任务:
|
||||||
|
|
||||||
|
| 任务 | 文件 | 内容 | 核心知识点 |
|
||||||
|
|------|------|------|------------|
|
||||||
|
| 任务一 | task51.c | 创建进程族亲结构(p1 -> p11, p12 -> p121, p122) | fork() + wait() + 进程树 |
|
||||||
|
| 任务二 | task52.c | 简单 shell 实现(命令解析、重定向、管道) | fork() + exec() + dup2() + pipe() |
|
||||||
|
| 任务三 | task53.c | daemon 文件监控(hash 值检测篡改) | daemon 五步法 + 文件 I/O |
|
||||||
|
| 任务四 | task54.c | 信号机制管理子进程(create/kill/ps/exit) | signal() + kill() + SIGCHLD |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 知识关联
|
||||||
|
|
||||||
|
- 进程状态转换在处理机调度中有更详细的讨论,参见相关课件
|
||||||
|
- 信号机制与进程间通信密切相关,参见 [[08_进程间通信]]
|
||||||
|
- Shell 的并发版本(多进程并发服务器)在网络编程中会深入讨论
|
||||||
|
- 线程可以看作轻量级进程,参见 [[07_多线程编程]]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 思考题
|
||||||
|
|
||||||
|
1. **fork 返回值的设计**:为什么 `fork()` 要返回两次而不是只返回一次?如果子进程不知道自己的 PID,可以用什么方式获取?
|
||||||
|
2. **exec 的不可逆性**:为什么 `exec()` 成功后不返回?如果 `exec()` 执行失败会怎样?
|
||||||
|
3. **僵尸进程的危害**:如果一个服务器程序不断创建子进程但从不 `wait`,最终会怎样?如何用 `SIGCHLD` 信号处理函数避免僵尸进程?
|
||||||
|
4. **shell 的本质**:shell 执行 `ls -l` 命令时,为什么需要 `fork()` + `exec()` 两个步骤?只用 `exec()` 行不行?
|
||||||
|
5. **daemon 的双重 fork**:为什么 daemon 创建通常要 `fork()` 两次?只 `fork()` 一次有什么问题?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 扩展阅读
|
||||||
|
|
||||||
|
- 《UNIX环境高级编程》第8章:进程控制
|
||||||
|
- 《深入理解计算机系统》第8章:异常控制流
|
||||||
|
- 《Linux/UNIX系统编程手册》第24-27章:进程的创建、终止、监控
|
||||||
315
操作系统/06_进程控制/06_进程控制_深入.md
Normal file
315
操作系统/06_进程控制/06_进程控制_深入.md
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
# 第06讲:进程控制(进阶)
|
||||||
|
|
||||||
|
> 🎯 **本节目标**:深入理解 fork/exec 的工作原理,掌握 Shell 的实现机制
|
||||||
|
|
||||||
|
## 📋 前置知识
|
||||||
|
- [[06_进程控制]] — fork、exec、wait 的基本概念
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤔 为什么需要这个?
|
||||||
|
|
||||||
|
你每天都在用 Shell(命令行),但你有没有想过:
|
||||||
|
- Shell 是怎么执行你的命令的?
|
||||||
|
- 为什么输入 `ls` 就能列出文件?
|
||||||
|
- 后台运行 `&` 是怎么实现的?
|
||||||
|
|
||||||
|
理解这些,需要深入掌握 fork 和 exec 的配合机制。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 核心概念
|
||||||
|
|
||||||
|
### 1. Shell 的工作原理
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[用户输入命令] --> B[Shell 解析命令]
|
||||||
|
B --> C[Shell 调用 fork]
|
||||||
|
C --> D[子进程调用 execvp]
|
||||||
|
D --> E[执行命令程序]
|
||||||
|
E --> F[子进程结束]
|
||||||
|
F --> G[Shell 调用 waitpid]
|
||||||
|
G --> A
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style D fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**Shell 的核心逻辑**:
|
||||||
|
```c
|
||||||
|
while (1) {
|
||||||
|
printf("%% "); // 打印提示符
|
||||||
|
fgets(cmdline); // 读取命令
|
||||||
|
if (feof(stdin)) exit(0);
|
||||||
|
|
||||||
|
pid = fork(); // 创建子进程
|
||||||
|
if (pid == 0) { // 子进程
|
||||||
|
execvp(argv[0], argv); // 执行命令
|
||||||
|
exit(1); // exec 失败
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!background) // 前台运行
|
||||||
|
waitpid(pid); // 等待子进程结束
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. fork() 的实现细节
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[父进程调用 fork] --> B[内核复制父进程的 PCB]
|
||||||
|
B --> C[复制页表(写时复制)]
|
||||||
|
C --> D[设置子进程的 PID]
|
||||||
|
D --> E[父子进程各返回一次]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style B fill:#fff3e0
|
||||||
|
```
|
||||||
|
|
||||||
|
**写时复制(Copy-on-Write)**:
|
||||||
|
- fork() 时不立即复制物理内存
|
||||||
|
- 父子进程共享同一份物理页面
|
||||||
|
- 只有当某一方尝试写入时,才复制该页面
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant 父进程
|
||||||
|
participant 子进程
|
||||||
|
participant 内存
|
||||||
|
|
||||||
|
父进程->>内存: fork()
|
||||||
|
Note over 内存: 父子共享物理页面
|
||||||
|
子进程->>内存: 尝试写入
|
||||||
|
Note over 内存: 触发写时复制
|
||||||
|
Note over 内存: 复制该页面给子进程
|
||||||
|
子进程->>内存: 写入新页面
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. exec() 的工作流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[调用 execvp] --> B[查找可执行文件]
|
||||||
|
B --> C[释放旧的地址空间]
|
||||||
|
C --> D[加载新的代码段]
|
||||||
|
D --> E[加载新的数据段]
|
||||||
|
E --> F[设置新的栈]
|
||||||
|
F --> G[跳转到新程序入口]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style G fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**exec 的关键特性**:
|
||||||
|
- **不创建新进程**:只是替换当前进程的内容
|
||||||
|
- **PID 不变**:进程还是原来的进程
|
||||||
|
- **文件描述符保留**:打开的文件不会自动关闭(除非设置了 close-on-exec)
|
||||||
|
|
||||||
|
### 4. 进程组与会话
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[会话 Session] --> B[前台进程组]
|
||||||
|
A --> C[后台进程组1]
|
||||||
|
A --> D[后台进程组2]
|
||||||
|
|
||||||
|
B --> B1[Shell]
|
||||||
|
B --> B2[当前命令]
|
||||||
|
|
||||||
|
style A fill:#ffcdd2
|
||||||
|
style B fill:#e8f5e9
|
||||||
|
style C fill:#e1f5fe
|
||||||
|
style D fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
**进程组**:一组相关进程的集合
|
||||||
|
- 用于信号的批量发送
|
||||||
|
- 用于作业控制(前台/后台切换)
|
||||||
|
|
||||||
|
**会话**:一个用户登录期间的所有进程
|
||||||
|
- 一个终端对应一个会话
|
||||||
|
- 会话有一个控制终端
|
||||||
|
|
||||||
|
### 5. 守护进程
|
||||||
|
|
||||||
|
守护进程是在后台运行的特殊进程,没有控制终端:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[创建子进程] --> B[父进程退出]
|
||||||
|
B --> C[创建新会话]
|
||||||
|
C --> D[改变工作目录]
|
||||||
|
D --> E[关闭文件描述符]
|
||||||
|
E --> F[重定向 stdin/stdout/stderr]
|
||||||
|
F --> G[进入主循环]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style G fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 动手实践
|
||||||
|
|
||||||
|
### 示例1:实现简单的 Shell
|
||||||
|
|
||||||
|
```c
|
||||||
|
// myshell.c - 简单的 Shell 实现
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
int ret, i, k, len, pid;
|
||||||
|
char cmd[100]; // 命令串,最多100个字符
|
||||||
|
char *arg[20]; // 参数数组,最多20个参数
|
||||||
|
|
||||||
|
printf("%% "); // 打印提示符
|
||||||
|
|
||||||
|
fgets(cmd, 100, stdin); // 从标准输入读取一行命令
|
||||||
|
|
||||||
|
// 将命令串中的空格替换成'\0',并将各参数提取出来
|
||||||
|
// 例如 cmd[100]="ls -l -a\0",替换后变为 cmd="ls\0-l\0-a\0"
|
||||||
|
len = strlen(cmd);
|
||||||
|
cmd[len - 1] = '\0'; // 去掉 fgets 加在串尾的换行符
|
||||||
|
|
||||||
|
for (i = 0; i < len - 1; i++)
|
||||||
|
if (cmd[i] == ' ') cmd[i] = '\0';
|
||||||
|
|
||||||
|
// 准备参数数组 arg
|
||||||
|
// arg[0]=cmd, arg[1]=cmd+3, arg[2]=cmd+6, arg[3]=NULL
|
||||||
|
arg[0] = cmd;
|
||||||
|
k = 1;
|
||||||
|
for (i = 1; i < len - 1; i++) {
|
||||||
|
if (cmd[i] == '\0') {
|
||||||
|
arg[k] = cmd + i + 1;
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
arg[k] = NULL;
|
||||||
|
|
||||||
|
pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
ret = execvp(arg[0], arg); // 子进程执行命令
|
||||||
|
if (ret == -1) {
|
||||||
|
perror("exec error");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
wait(-1); // 父进程等待子进程结束
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
gcc -o myshell myshell.c
|
||||||
|
./myshell
|
||||||
|
% ls -l
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例2:后台运行
|
||||||
|
|
||||||
|
```c
|
||||||
|
// shellex.c - 支持后台运行的 Shell
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
#define MAXARGS 128
|
||||||
|
|
||||||
|
int parseline(char *buf, char **argv) {
|
||||||
|
char *delim;
|
||||||
|
int argc;
|
||||||
|
int bg; // 后台作业标志
|
||||||
|
|
||||||
|
buf[strlen(buf) - 1] = ' '; // 用空格替换末尾换行
|
||||||
|
while (*buf && (*buf == ' '))
|
||||||
|
buf++;
|
||||||
|
|
||||||
|
argc = 0;
|
||||||
|
while ((delim = strchr(buf, ' '))) {
|
||||||
|
argv[argc++] = buf;
|
||||||
|
*delim = '\0';
|
||||||
|
buf = delim + 1;
|
||||||
|
while (*buf && (*buf == ' '))
|
||||||
|
buf++;
|
||||||
|
}
|
||||||
|
argv[argc] = NULL;
|
||||||
|
|
||||||
|
if (argc == 0) return 1;
|
||||||
|
|
||||||
|
// 检查是否应该在后台执行
|
||||||
|
if ((bg = (*argv[argc - 1] == '&')) != 0)
|
||||||
|
argv[--argc] = NULL;
|
||||||
|
|
||||||
|
return bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
void execute(char *cmdline) {
|
||||||
|
char *argv[MAXARGS];
|
||||||
|
char buf[MAXLINE];
|
||||||
|
int bg;
|
||||||
|
pid_t pid;
|
||||||
|
|
||||||
|
strcpy(buf, cmdline);
|
||||||
|
bg = parseline(buf, argv);
|
||||||
|
|
||||||
|
if (argv[0] == NULL) return; // 忽略空行
|
||||||
|
|
||||||
|
if ((pid = fork()) == 0) { // 子进程
|
||||||
|
if (execvp(argv[0], argv) < 0) {
|
||||||
|
printf("%s: Command not found.\n", argv[0]);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bg) { // 前台运行
|
||||||
|
int status;
|
||||||
|
if (waitpid(pid, &status, 0) < 0)
|
||||||
|
perror("waitpid error");
|
||||||
|
} else { // 后台运行
|
||||||
|
printf("%d %s", pid, cmdline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
char cmdline[MAXLINE];
|
||||||
|
while (1) {
|
||||||
|
printf("%% ");
|
||||||
|
fgets(cmdline, MAXLINE, stdin);
|
||||||
|
if (feof(stdin)) exit(0);
|
||||||
|
execute(cmdline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
gcc -o shellex shellex.c -L. -lwrapper
|
||||||
|
./shellex
|
||||||
|
% sleep 10 & # 后台运行
|
||||||
|
% ps # 查看进程
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 知识关联
|
||||||
|
- Shell 的 I/O 重定向在 [[04_文件IO编程]] 中的 dup2 有详细讲解
|
||||||
|
- 守护进程在 [[09_网络编程]] 中会实际使用
|
||||||
|
- 进程组在 [[11_处理机调度]] 中用于作业控制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 思考题
|
||||||
|
|
||||||
|
1. **为什么 exec() 后文件描述符还保留?** 什么时候需要关闭它们?
|
||||||
|
2. **写时复制的优势是什么?** 如果 fork() 时立即复制所有内存会怎样?
|
||||||
|
3. **Shell 是怎么实现管道的?** 例如 `ls | grep .c` 的执行过程是什么?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 扩展阅读
|
||||||
|
- 《UNIX环境高级编程》第8章:进程控制
|
||||||
|
- 《深入理解计算机系统》第8章:异常控制流
|
||||||
|
- [Bash 源码](https://git.savannah.gnu.org/cgit/bash.git/)
|
||||||
840
操作系统/07_多线程编程/07_多线程编程.md
Normal file
840
操作系统/07_多线程编程/07_多线程编程.md
Normal file
@@ -0,0 +1,840 @@
|
|||||||
|
# 第07讲:多线程编程
|
||||||
|
|
||||||
|
> **本节目标**:理解线程的概念与优势,掌握 POSIX 线程 API(pthread),理解竞态条件的成因与解决方案(互斥锁、信号量),能够实现并行计算和生产者-消费者模型。
|
||||||
|
|
||||||
|
## 前置知识
|
||||||
|
|
||||||
|
- [[06_进程控制]] -- 进程的概念与创建
|
||||||
|
- [[08_进程间通信]] -- 进程间通信方式(与线程通信对比)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、线程概念
|
||||||
|
|
||||||
|
### 1.1 什么是线程
|
||||||
|
|
||||||
|
**线程(Thread)** 是进程内的一个执行单元。一个进程可以包含多个线程,这些线程共享进程的地址空间,但每个线程拥有自己独立的栈和寄存器状态。
|
||||||
|
|
||||||
|
**生活比喻**:
|
||||||
|
- **进程** = 一家公司(独立的办公室、财务系统、人员编制)
|
||||||
|
- **线程** = 公司里的员工(共享办公室和财务系统,但各有各的工作任务)
|
||||||
|
|
||||||
|
### 1.2 进程 vs 线程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph "多进程模型"
|
||||||
|
P1["进程1"] --> M1["独立的地址空间"]
|
||||||
|
P2["进程2"] --> M2["独立的地址空间"]
|
||||||
|
M1 -.->|"IPC 通信<br/>(管道/共享内存)"| M2
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "多线程模型"
|
||||||
|
T1["线程1"] --> SM["共享的地址空间"]
|
||||||
|
T2["线程2"] --> SM
|
||||||
|
T3["线程3"] --> SM
|
||||||
|
SM --> S1["代码段 (共享)"]
|
||||||
|
SM --> S2["数据段 (共享)"]
|
||||||
|
SM --> S3["堆 (共享)"]
|
||||||
|
SM --> S4["栈 (各自独立)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
style P1 fill:#e1f5fe
|
||||||
|
style P2 fill:#e1f5fe
|
||||||
|
style T1 fill:#e8f5e9
|
||||||
|
style T2 fill:#e8f5e9
|
||||||
|
style T3 fill:#e8f5e9
|
||||||
|
style SM fill:#fff3e0
|
||||||
|
```
|
||||||
|
|
||||||
|
| 对比维度 | 进程 | 线程 |
|
||||||
|
|----------|------|------|
|
||||||
|
| 地址空间 | 独立(互不可见) | 共享(可见彼此的变量) |
|
||||||
|
| 创建开销 | 大(复制页表、地址空间) | 小(只创建栈和寄存器) |
|
||||||
|
| 切换开销 | 大(切换页表、刷新 TLB) | 小(只切换栈指针和寄存器) |
|
||||||
|
| 通信方式 | 需要 IPC 机制(管道、共享内存等) | 直接读写共享变量 |
|
||||||
|
| 安全性 | 高(地址空间隔离) | 低(需要同步机制保护) |
|
||||||
|
| 典型应用 | 独立的程序(如浏览器和音乐播放器) | 同一程序内的并发任务 |
|
||||||
|
|
||||||
|
### 1.3 线程的共享与私有
|
||||||
|
|
||||||
|
| 资源 | 共享/私有 | 说明 |
|
||||||
|
|------|-----------|------|
|
||||||
|
| 代码段 | 共享 | 所有线程执行相同的代码 |
|
||||||
|
| 数据段(全局变量) | 共享 | 所有线程可见同一份全局变量 |
|
||||||
|
| 堆(malloc 分配的内存) | 共享 | 任一线程分配的内存,其他线程可访问 |
|
||||||
|
| 栈 | **私有** | 每个线程有自己的栈,存放局部变量 |
|
||||||
|
| 寄存器状态 | **私有** | 每个线程有自己的程序计数器、栈指针等 |
|
||||||
|
| 信号处理 | 共享 | 信号发送给进程,任意线程可处理 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、POSIX 线程 API
|
||||||
|
|
||||||
|
### 2.1 核心函数
|
||||||
|
|
||||||
|
| 函数 | 作用 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `pthread_create()` | 创建新线程 | 类似 `fork()`,但开销小得多 |
|
||||||
|
| `pthread_join()` | 等待线程结束 | 类似 `wait()`,阻塞调用线程 |
|
||||||
|
| `pthread_exit()` | 终止当前线程 | 类似 `exit()`,但只终止当前线程 |
|
||||||
|
| `pthread_self()` | 获取当前线程 ID | 类似 `getpid()` |
|
||||||
|
| `pthread_detach()` | 分离线程 | 线程结束后自动释放资源 |
|
||||||
|
| `pthread_cancel()` | 取消线程 | 请求终止另一个线程 |
|
||||||
|
|
||||||
|
### 2.2 pthread_create 详解
|
||||||
|
|
||||||
|
```c
|
||||||
|
int pthread_create(
|
||||||
|
pthread_t *tid, // 输出参数:新线程的 ID
|
||||||
|
const pthread_attr_t *attr, // 线程属性(通常传 NULL)
|
||||||
|
void *(*start_routine)(void*), // 线程函数(新线程从这里开始执行)
|
||||||
|
void *arg // 传递给线程函数的参数
|
||||||
|
);
|
||||||
|
// 返回值:成功返回 0,失败返回错误码
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A["主线程调用<br/>pthread_create()"] --> B["操作系统创建新线程"]
|
||||||
|
B --> C["新线程执行<br/>start_routine(arg)"]
|
||||||
|
C --> D["线程函数返回"]
|
||||||
|
D --> E["主线程调用<br/>pthread_join() 回收"]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style C fill:#e8f5e9
|
||||||
|
style E fill:#fff3e0
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 线程创建示例
|
||||||
|
|
||||||
|
参考 `实例源代码/chap6/pthread1.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "wrapper.h"
|
||||||
|
void *peertask(void *vargp);
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
pthread_t tid;
|
||||||
|
int i;
|
||||||
|
|
||||||
|
pthread_create(&tid, NULL, peertask, NULL); // 创建新线程
|
||||||
|
|
||||||
|
for (i = 0; i < 2; i++) {
|
||||||
|
printf("main thread looped %d times\n", i);
|
||||||
|
usleep(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_join(tid, NULL); // 等待新线程结束
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void *peertask(void *vargp) // 线程函数
|
||||||
|
{
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < 4; i++) {
|
||||||
|
printf("peer thread looped %d times\n", i);
|
||||||
|
usleep(10);
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出示例**(顺序不确定,因为主线程和新线程并发执行):
|
||||||
|
```
|
||||||
|
main thread looped 0 times
|
||||||
|
peer thread looped 0 times
|
||||||
|
main thread looped 1 times
|
||||||
|
peer thread looped 1 times
|
||||||
|
peer thread looped 2 times
|
||||||
|
peer thread looped 3 times
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- `pthread_create` 创建新线程后,两个线程**并发执行**
|
||||||
|
- `pthread_join` 阻塞主线程,直到新线程结束
|
||||||
|
- 输出顺序取决于调度器,每次运行可能不同
|
||||||
|
- 编译时需要加 `-lpthread` 链接 pthread 库
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、线程安全问题 -- 竞态条件
|
||||||
|
|
||||||
|
### 3.1 什么是竞态条件
|
||||||
|
|
||||||
|
**竞态条件(Race Condition)** 是指多个线程同时访问和修改共享变量,最终结果取决于线程执行的精确时序,导致程序行为不可预测。
|
||||||
|
|
||||||
|
### 3.2 竞态条件演示
|
||||||
|
|
||||||
|
参考 `实例源代码/chap6/threadrace.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "wrapper.h"
|
||||||
|
#define N 4
|
||||||
|
|
||||||
|
void *thread(void *vargp);
|
||||||
|
|
||||||
|
int main()
|
||||||
|
{
|
||||||
|
pthread_t tid[N];
|
||||||
|
int i;
|
||||||
|
|
||||||
|
for (i = 0; i < N; i++)
|
||||||
|
pthread_create(&tid[i], NULL, thread, &i); // 传递 i 的地址
|
||||||
|
|
||||||
|
for (i = 0; i < N; i++)
|
||||||
|
pthread_join(tid[i], NULL);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void *thread(void *vargp)
|
||||||
|
{
|
||||||
|
usleep(1);
|
||||||
|
int myid = *((int *)vargp); // 读取线程 ID
|
||||||
|
printf("Hello from thread %d\n", myid);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**问题分析**:所有线程共享同一个变量 `i` 的地址。当线程读取 `*vargp` 时,`i` 的值可能已经被主线程的循环修改了。
|
||||||
|
|
||||||
|
**可能出现的输出**:
|
||||||
|
```
|
||||||
|
Hello from thread 2
|
||||||
|
Hello from thread 2
|
||||||
|
Hello from thread 3
|
||||||
|
Hello from thread 3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.3 竞态条件的时序分析
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant 主线程
|
||||||
|
participant 线程0
|
||||||
|
participant 线程1
|
||||||
|
participant 线程2
|
||||||
|
participant 线程3
|
||||||
|
|
||||||
|
主线程->>线程0: pthread_create(&i)
|
||||||
|
主线程->>主线程: i++ (i=1)
|
||||||
|
主线程->>线程1: pthread_create(&i)
|
||||||
|
主线程->>主线程: i++ (i=2)
|
||||||
|
主线程->>线程2: pthread_create(&i)
|
||||||
|
主线程->>主线程: i++ (i=3)
|
||||||
|
主线程->>线程3: pthread_create(&i)
|
||||||
|
|
||||||
|
Note over 线程0: 读取 *vargp,但 i 已经是 3
|
||||||
|
Note over 线程1: 读取 *vargp,i 也是 3
|
||||||
|
Note over 线程2: 读取 *vargp,i 也是 3
|
||||||
|
Note over 线程3: 读取 *vargp,i 是 3
|
||||||
|
|
||||||
|
线程0-->>主线程: Hello from thread 3 (错误!)
|
||||||
|
线程1-->>主线程: Hello from thread 3 (错误!)
|
||||||
|
线程2-->>主线程: Hello from thread 3 (错误!)
|
||||||
|
线程3-->>主线程: Hello from thread 3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.4 经典竞态:badcount.c
|
||||||
|
|
||||||
|
参考 `实例源代码/chap6/badcount.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "wrapper.h"
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
void *increase(void *arg) {
|
||||||
|
unsigned int i, niters = (unsigned int)arg;
|
||||||
|
for (i = 0; i < niters; i++)
|
||||||
|
count++; // 非原子操作!
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *decrease(void *arg) {
|
||||||
|
unsigned int i, niters = (unsigned int)arg;
|
||||||
|
for (i = 0; i < niters; i++)
|
||||||
|
count--; // 非原子操作!
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
unsigned int niters = atoll(argv[1]);
|
||||||
|
pthread_t tid1, tid2;
|
||||||
|
pthread_create(&tid1, NULL, increase, (void*)niters);
|
||||||
|
pthread_create(&tid2, NULL, decrease, (void*)niters);
|
||||||
|
pthread_join(tid1, NULL);
|
||||||
|
pthread_join(tid2, NULL);
|
||||||
|
|
||||||
|
if (count != 0)
|
||||||
|
printf("Error! cnt=%d\n", count); // 期望为0,但实际可能不为0
|
||||||
|
else
|
||||||
|
printf("Correct cnt=%d\n", count);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**为什么 `count++` 和 `count--` 不是原子操作?**
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph "count++ 的三条指令"
|
||||||
|
A1["LOAD count -> 寄存器"] --> A2["寄存器 = 寄存器 + 1"]
|
||||||
|
A2 --> A3["STORE 寄存器 -> count"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "count-- 的三条指令"
|
||||||
|
B1["LOAD count -> 寄存器"] --> B2["寄存器 = 寄存器 - 1"]
|
||||||
|
B2 --> B3["STORE 寄存器 -> count"]
|
||||||
|
end
|
||||||
|
|
||||||
|
style A1 fill:#e1f5fe
|
||||||
|
style A2 fill:#fff3e0
|
||||||
|
style A3 fill:#ffcdd2
|
||||||
|
style B1 fill:#e1f5fe
|
||||||
|
style B2 fill:#fff3e0
|
||||||
|
style B3 fill:#ffcdd2
|
||||||
|
```
|
||||||
|
|
||||||
|
当两个线程的 LOAD/STORE 指令交错执行时,就会出现丢失更新的问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、互斥锁(Mutex)
|
||||||
|
|
||||||
|
### 4.1 互斥锁的原理
|
||||||
|
|
||||||
|
互斥锁(Mutex)保证同一时刻只有一个线程可以进入**临界区**(访问共享资源的代码段)。其他试图获取已锁定的互斥锁的线程会被阻塞,直到锁被释放。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant 线程1
|
||||||
|
participant 互斥锁
|
||||||
|
participant 线程2
|
||||||
|
participant 共享变量
|
||||||
|
|
||||||
|
线程1->>互斥锁: pthread_mutex_lock()
|
||||||
|
Note over 互斥锁: 锁状态:已锁定
|
||||||
|
线程1->>共享变量: 读取 count (0)
|
||||||
|
|
||||||
|
线程2->>互斥锁: pthread_mutex_lock()
|
||||||
|
Note over 互斥锁: 锁已被占用,线程2 阻塞
|
||||||
|
|
||||||
|
线程1->>共享变量: count = count + 1 = 1
|
||||||
|
线程1->>互斥锁: pthread_mutex_unlock()
|
||||||
|
Note over 互斥锁: 锁状态:已解锁
|
||||||
|
互斥锁-->>线程2: 被唤醒,获得锁
|
||||||
|
|
||||||
|
线程2->>共享变量: 读取 count (1)
|
||||||
|
线程2->>共享变量: count = count - 1 = 0
|
||||||
|
线程2->>互斥锁: pthread_mutex_unlock()
|
||||||
|
|
||||||
|
Note over 共享变量: count = 0 (正确!)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 互斥锁 API
|
||||||
|
|
||||||
|
| 函数 | 作用 |
|
||||||
|
|------|------|
|
||||||
|
| `pthread_mutex_init()` | 初始化互斥锁 |
|
||||||
|
| `pthread_mutex_lock()` | 加锁(阻塞等待) |
|
||||||
|
| `pthread_mutex_trylock()` | 尝试加锁(非阻塞) |
|
||||||
|
| `pthread_mutex_unlock()` | 解锁 |
|
||||||
|
| `pthread_mutex_destroy()` | 销毁互斥锁 |
|
||||||
|
|
||||||
|
**初始化方式**:
|
||||||
|
```c
|
||||||
|
// 方式1:静态初始化
|
||||||
|
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
|
// 方式2:动态初始化
|
||||||
|
pthread_mutex_t mutex;
|
||||||
|
pthread_mutex_init(&mutex, NULL);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 互斥锁修复竞态
|
||||||
|
|
||||||
|
参考 `实例源代码/chap6/mutex1.c`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// mutex1.c - 使用互斥锁保护共享变量
|
||||||
|
#include "wrapper.h"
|
||||||
|
int count = 0;
|
||||||
|
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
|
void *thread_fun(void *no) {
|
||||||
|
pthread_mutex_lock(&mutex); // 加锁 -- 进入临界区
|
||||||
|
count++;
|
||||||
|
printf("thread %d, count = %d\n", (int)no, count);
|
||||||
|
pthread_mutex_unlock(&mutex); // 解锁 -- 离开临界区
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
pthread_t thread_id1, thread_id2;
|
||||||
|
Pthread_create(&thread_id1, NULL, thread_fun, (void*)1);
|
||||||
|
Pthread_create(&thread_id2, NULL, thread_fun, (void*)2);
|
||||||
|
Pthread_join(thread_id1, NULL);
|
||||||
|
Pthread_join(thread_id2, NULL);
|
||||||
|
Pthread_exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**输出**(保证正确):
|
||||||
|
```
|
||||||
|
thread 1, count = 1
|
||||||
|
thread 2, count = 2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 另一种修复方式:避免共享
|
||||||
|
|
||||||
|
参考 `实例源代码/chap6/norace.c`,通过为每个线程分配独立的内存来避免共享:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// norace.c - 使用动态分配避免竞态
|
||||||
|
for (i = 0; i < N; i++) {
|
||||||
|
ptr = malloc(sizeof(int)); // 为每个线程分配独立的内存
|
||||||
|
*ptr = i;
|
||||||
|
pthread_create(&tid[i], NULL, thread, ptr);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**原理**:每个线程拥有自己独立的 `int` 变量,不再共享 `i`,从根本上消除了竞态条件。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、信号量(Semaphore)
|
||||||
|
|
||||||
|
### 5.1 信号量的概念
|
||||||
|
|
||||||
|
信号量是一种比互斥锁更通用的同步机制,由 Dijkstra 提出。信号量是一个非负整数计数器,支持两个原子操作:
|
||||||
|
|
||||||
|
- **P 操作**(`sem_wait`):计数器减 1,如果计数器 < 0 则阻塞
|
||||||
|
- **V 操作**(`sem_post`):计数器加 1,如果有等待者则唤醒
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph "信号量 S"
|
||||||
|
S["S = 计数器"]
|
||||||
|
end
|
||||||
|
|
||||||
|
P["sem_wait (P操作)<br/>S--"] -->|S >= 0| OK["允许通过"]
|
||||||
|
P -->|S < 0| BLOCK["阻塞等待"]
|
||||||
|
|
||||||
|
V["sem_post (V操作)<br/>S++"] --> WAKE["唤醒等待者"]
|
||||||
|
|
||||||
|
style S fill:#fff3e0
|
||||||
|
style OK fill:#e8f5e9
|
||||||
|
style BLOCK fill:#ffcdd2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 信号量 API
|
||||||
|
|
||||||
|
| 函数 | 作用 |
|
||||||
|
|------|------|
|
||||||
|
| `sem_init(&sem, pshared, value)` | 初始化信号量,value 为初始值 |
|
||||||
|
| `sem_wait(&sem)` | P 操作:等待信号量(S--,若 S<0 则阻塞) |
|
||||||
|
| `sem_post(&sem)` | V 操作:释放信号量(S++,唤醒等待者) |
|
||||||
|
| `sem_getvalue(&sem, &val)` | 获取信号量当前值 |
|
||||||
|
| `sem_destroy(&sem)` | 销毁信号量 |
|
||||||
|
|
||||||
|
### 5.3 互斥锁 vs 信号量
|
||||||
|
|
||||||
|
| 特性 | 互斥锁 (Mutex) | 信号量 (Semaphore) |
|
||||||
|
|------|----------------|-------------------|
|
||||||
|
| 计数范围 | 0 或 1(二值) | 0 到 N(计数) |
|
||||||
|
| 用途 | 互斥(保护临界区) | 同步(协调执行顺序) |
|
||||||
|
| 加锁/解锁者 | 谁加锁谁解锁 | 一个线程 sem_wait,另一个 sem_post |
|
||||||
|
| 典型场景 | 保护共享变量 | 生产者-消费者、资源池 |
|
||||||
|
|
||||||
|
### 5.4 生产者-消费者问题
|
||||||
|
|
||||||
|
生产者-消费者是经典的同步问题:生产者向缓冲区放入数据,消费者从缓冲区取出数据。需要解决:
|
||||||
|
- 缓冲区满时生产者必须等待
|
||||||
|
- 缓冲区空时消费者必须等待
|
||||||
|
- 同一时刻只有一个线程访问缓冲区
|
||||||
|
|
||||||
|
**使用信号量解决**:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <semaphore.h>
|
||||||
|
|
||||||
|
#define N 10 // 缓冲区大小
|
||||||
|
int buf[N];
|
||||||
|
int in = 0, out = 0;
|
||||||
|
|
||||||
|
sem_t mutex; // 互斥信号量,初值 1
|
||||||
|
sem_t empty; // 空槽位数,初值 N
|
||||||
|
sem_t full; // 满槽位数,初值 0
|
||||||
|
|
||||||
|
void *producer(void *arg) {
|
||||||
|
while (1) {
|
||||||
|
int item = produce_item();
|
||||||
|
sem_wait(&empty); // 等待空槽位 (empty--)
|
||||||
|
sem_wait(&mutex); // 进入临界区
|
||||||
|
buf[in] = item;
|
||||||
|
in = (in + 1) % N;
|
||||||
|
sem_post(&mutex); // 离开临界区
|
||||||
|
sem_post(&full); // 增加满槽位 (full++)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void *consumer(void *arg) {
|
||||||
|
while (1) {
|
||||||
|
sem_wait(&full); // 等待满槽位 (full--)
|
||||||
|
sem_wait(&mutex); // 进入临界区
|
||||||
|
int item = buf[out];
|
||||||
|
out = (out + 1) % N;
|
||||||
|
sem_post(&mutex); // 离开临界区
|
||||||
|
sem_post(&empty); // 增加空槽位 (empty++)
|
||||||
|
consume_item(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**生产者-消费者流程图**:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "生产者"
|
||||||
|
P1["生产数据"] --> P2["sem_wait(empty)<br/>等待空槽位"]
|
||||||
|
P2 --> P3["sem_wait(mutex)<br/>进入临界区"]
|
||||||
|
P3 --> P4["放入缓冲区"]
|
||||||
|
P4 --> P5["sem_post(mutex)<br/>离开临界区"]
|
||||||
|
P5 --> P6["sem_post(full)<br/>通知消费者"]
|
||||||
|
P6 --> P1
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "消费者"
|
||||||
|
C1["sem_wait(full)<br/>等待满槽位"] --> C2["sem_wait(mutex)<br/>进入临界区"]
|
||||||
|
C2 --> C3["取出数据"]
|
||||||
|
C3 --> C4["sem_post(mutex)<br/>离开临界区"]
|
||||||
|
C4 --> C5["sem_post(empty)<br/>通知生产者"]
|
||||||
|
C5 --> C6["处理数据"]
|
||||||
|
C6 --> C1
|
||||||
|
end
|
||||||
|
|
||||||
|
P6 -.->|"full++ 唤醒"| C1
|
||||||
|
C5 -.->|"empty++ 唤醒"| P2
|
||||||
|
|
||||||
|
style P1 fill:#e1f5fe
|
||||||
|
style C6 fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.5 实验任务:修复 badcount.c
|
||||||
|
|
||||||
|
实验中的 `task62.c` 要求使用信号量修复 `badcount.c` 的竞态条件:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <semaphore.h>
|
||||||
|
sem_t mutex;
|
||||||
|
sem_init(&mutex, 0, 1); // 互斥信号量,初值 1
|
||||||
|
|
||||||
|
void *increase(void *arg) {
|
||||||
|
for (i = 0; i < niters; i++) {
|
||||||
|
sem_wait(&mutex); // P 操作
|
||||||
|
count++;
|
||||||
|
sem_post(&mutex); // V 操作
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void *decrease(void *arg) {
|
||||||
|
for (i = 0; i < niters; i++) {
|
||||||
|
sem_wait(&mutex); // P 操作
|
||||||
|
count--;
|
||||||
|
sem_post(&mutex); // V 操作
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、并行计算
|
||||||
|
|
||||||
|
### 6.1 并行求和问题
|
||||||
|
|
||||||
|
参考 `实例源代码/chap6/psum64.c`,将一个大数组的求和任务分配给多个线程并行计算:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "wrapper.h"
|
||||||
|
#define MAXTHREADS 32
|
||||||
|
|
||||||
|
unsigned long long psum[MAXTHREADS]; // 每个线程的部分和
|
||||||
|
unsigned long long nelems_per_thread; // 每个线程处理的元素数
|
||||||
|
|
||||||
|
void *sum(void *vargp) {
|
||||||
|
int myid = *((int *)vargp);
|
||||||
|
unsigned long long start = myid * nelems_per_thread;
|
||||||
|
unsigned long long end = start + nelems_per_thread;
|
||||||
|
unsigned long long i, lsum = 0;
|
||||||
|
|
||||||
|
for (i = start; i < end; i++)
|
||||||
|
lsum += i;
|
||||||
|
|
||||||
|
psum[myid] = lsum; // 将部分和存入全局数组
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
unsigned long long i, nelems, nthreads, result = 0;
|
||||||
|
pthread_t tid[MAXTHREADS];
|
||||||
|
int myid[MAXTHREADS];
|
||||||
|
|
||||||
|
nthreads = atoi(argv[1]);
|
||||||
|
nelems = (1L << atoi(argv[2])); // 2^log_nelems
|
||||||
|
nelems_per_thread = nelems / nthreads;
|
||||||
|
|
||||||
|
// 创建线程
|
||||||
|
for (i = 0; i < nthreads; i++) {
|
||||||
|
myid[i] = i;
|
||||||
|
pthread_create(&tid[i], NULL, sum, &myid[i]);
|
||||||
|
}
|
||||||
|
for (i = 0; i < nthreads; i++)
|
||||||
|
pthread_join(tid[i], NULL);
|
||||||
|
|
||||||
|
// 汇总部分和
|
||||||
|
for (i = 0; i < nthreads; i++)
|
||||||
|
result += psum[i];
|
||||||
|
|
||||||
|
// 验证结果
|
||||||
|
if (result == (nelems * (nelems - 1) / 2))
|
||||||
|
printf("Correct Result=%lld\n", result);
|
||||||
|
else
|
||||||
|
printf("Error: result=%lld\n", result);
|
||||||
|
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**并行求和示意图**:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "数据划分"
|
||||||
|
DATA["0, 1, 2, 3, 4, 5, 6, 7, ..."]
|
||||||
|
DATA --> T0["线程0: 0 ~ N/p-1"]
|
||||||
|
DATA --> T1["线程1: N/p ~ 2N/p-1"]
|
||||||
|
DATA --> T2["线程2: 2N/p ~ 3N/p-1"]
|
||||||
|
DATA --> T3["线程3: 3N/p ~ N-1"]
|
||||||
|
end
|
||||||
|
|
||||||
|
T0 --> S0["psum[0]"]
|
||||||
|
T1 --> S1["psum[1]"]
|
||||||
|
T2 --> S2["psum[2]"]
|
||||||
|
T3 --> S3["psum[3]"]
|
||||||
|
|
||||||
|
S0 --> SUM["result = psum[0]+psum[1]+psum[2]+psum[3]"]
|
||||||
|
S1 --> SUM
|
||||||
|
S2 --> SUM
|
||||||
|
S3 --> SUM
|
||||||
|
|
||||||
|
style DATA fill:#e1f5fe
|
||||||
|
style SUM fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 加速比与效率
|
||||||
|
|
||||||
|
**加速比(Speedup)**:S_p = T_1 / T_p
|
||||||
|
- T_1:单线程执行时间
|
||||||
|
- T_p:p 个线程并行执行时间
|
||||||
|
- 理想加速比 S_p = p(线性加速)
|
||||||
|
|
||||||
|
**效率(Efficiency)**:E_p = S_p / p = T_1 / (p * T_p)
|
||||||
|
- 理想效率 E_p = 1(100%)
|
||||||
|
- 实际效率 < 1,因为存在线程创建、同步、通信等开销
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph "加速比曲线"
|
||||||
|
X["线程数 p"] --> Y["加速比 Sp"]
|
||||||
|
IDEAL["理想: Sp=p"] -.-> Y
|
||||||
|
ACTUAL["实际: Sp<p"] --> Y
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 实验任务:测量加速比
|
||||||
|
|
||||||
|
实验中的 `task64.c` 要求:
|
||||||
|
1. 修改 `psum64.c`,添加计时功能
|
||||||
|
2. 分别用 1、2、4、8 个线程运行
|
||||||
|
3. 计算每个配置的加速比和效率
|
||||||
|
4. 分析为什么加速比不是线性的
|
||||||
|
|
||||||
|
**预期结果**:
|
||||||
|
|
||||||
|
| 线程数 | 执行时间 | 加速比 | 效率 |
|
||||||
|
|:------:|:--------:|:------:|:----:|
|
||||||
|
| 1 | T1 | 1.0 | 100% |
|
||||||
|
| 2 | T2 | ~1.8 | ~90% |
|
||||||
|
| 4 | T4 | ~3.2 | ~80% |
|
||||||
|
| 8 | T8 | ~5.5 | ~69% |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、fork vs pthread_create 性能对比
|
||||||
|
|
||||||
|
### 7.1 为什么线程更快
|
||||||
|
|
||||||
|
`fork()` 创建进程时需要复制父进程的整个地址空间(虽然有写时复制优化),而 `pthread_create()` 只需要分配一个新的栈和少量元数据。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph "fork() 创建进程"
|
||||||
|
F1["复制页表"] --> F2["复制地址空间<br/>(写时复制)"]
|
||||||
|
F2 --> F3["创建新的 PCB"]
|
||||||
|
F3 --> F4["开销大: ~ms 级"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "pthread_create() 创建线程"
|
||||||
|
T1["分配栈空间"] --> T2["设置线程属性"]
|
||||||
|
T2 --> T3["创建线程元数据"]
|
||||||
|
T3 --> T4["开销小: ~us 级"]
|
||||||
|
end
|
||||||
|
|
||||||
|
style F4 fill:#ffcdd2
|
||||||
|
style T4 fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 实验任务:测量创建时间
|
||||||
|
|
||||||
|
实验中的 `task66.c` 要求分别测量 `fork()` 和 `pthread_create()` 的创建时间:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 测量 fork 创建时间
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &start);
|
||||||
|
for (i = 0; i < N; i++) {
|
||||||
|
if (fork() == 0) exit(0); // 子进程立即退出
|
||||||
|
wait(NULL);
|
||||||
|
}
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &end);
|
||||||
|
|
||||||
|
// 测量 pthread_create 创建时间
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &start);
|
||||||
|
for (i = 0; i < N; i++) {
|
||||||
|
pthread_create(&tid, NULL, dummy, NULL);
|
||||||
|
pthread_join(tid, NULL);
|
||||||
|
}
|
||||||
|
clock_gettime(CLOCK_MONOTONIC, &end);
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期结果**:`pthread_create` 通常比 `fork` 快 10-100 倍。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、动态线程池
|
||||||
|
|
||||||
|
### 8.1 为什么需要线程池
|
||||||
|
|
||||||
|
频繁创建和销毁线程会带来较大开销。线程池预先创建一组线程,当有任务到来时从池中取出一个线程执行,执行完毕后线程归还池中,避免反复创建销毁。
|
||||||
|
|
||||||
|
### 8.2 sbuf_t 带缓冲区的线程池
|
||||||
|
|
||||||
|
实验中的 `task67.c` 实现了一个动态线程池,核心数据结构 `sbuf_t`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
int *buf; // 缓冲区数组
|
||||||
|
int n; // 缓冲区容量
|
||||||
|
int front; // 队首索引
|
||||||
|
int rear; // 队尾索引
|
||||||
|
sem_t mutex; // 互斥访问
|
||||||
|
sem_t slots; // 空槽位信号量
|
||||||
|
sem_t items; // 满槽位信号量
|
||||||
|
} sbuf_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
**动态调整策略**:
|
||||||
|
- **缓冲区满时翻倍**:`buf = realloc(buf, 2 * n)`
|
||||||
|
- **缓冲区空时减半**:`buf = realloc(buf, n / 2)`
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph "线程池架构"
|
||||||
|
MAIN["主线程<br/>接收任务"] -->|"放入缓冲区"| BUF["sbuf_t 缓冲区"]
|
||||||
|
BUF -->|"取出任务"| W1["工作线程1"]
|
||||||
|
BUF -->|"取出任务"| W2["工作线程2"]
|
||||||
|
BUF -->|"取出任务"| W3["工作线程3"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph "动态调整"
|
||||||
|
FULL["缓冲区满"] -->|"realloc"| DOUBLE["容量翻倍"]
|
||||||
|
EMPTY["缓冲区空"] -->|"realloc"| HALF["容量减半"]
|
||||||
|
end
|
||||||
|
|
||||||
|
style MAIN fill:#e1f5fe
|
||||||
|
style BUF fill:#fff3e0
|
||||||
|
style W1 fill:#e8f5e9
|
||||||
|
style W2 fill:#e8f5e9
|
||||||
|
style W3 fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、线程安全的数据结构
|
||||||
|
|
||||||
|
### 9.1 tickets 售票问题
|
||||||
|
|
||||||
|
参考 `实例源代码/chap6/tickets1.c`(有竞态条件)和 `tickets2.c`(使用互斥锁修复):
|
||||||
|
|
||||||
|
```c
|
||||||
|
// tickets2.c - 使用互斥锁保护售票
|
||||||
|
int tickets = 10;
|
||||||
|
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
|
void *counter(void *no) {
|
||||||
|
while (tickets > 0) {
|
||||||
|
pthread_mutex_lock(&mutex); // 加锁
|
||||||
|
if (tickets > 0) { // 双重检查
|
||||||
|
printf("柜台%d 卖出一张票,票号为%d\n", (int)no, tickets);
|
||||||
|
usleep(1);
|
||||||
|
tickets--;
|
||||||
|
}
|
||||||
|
pthread_mutex_unlock(&mutex); // 解锁
|
||||||
|
usleep(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:`while` 条件检查和 `if` 条件检查都需要在锁内进行(双重检查),否则可能出现超卖。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、实验任务概览
|
||||||
|
|
||||||
|
本讲对应 [[实验03_多线程编程]],包含以下任务:
|
||||||
|
|
||||||
|
| 任务 | 文件 | 内容 | 核心知识点 |
|
||||||
|
|------|------|------|------------|
|
||||||
|
| 任务一 | task61.c | 3 个对等线程打印姓名/学号/时间 | pthread_create/join |
|
||||||
|
| 任务二 | task62.c | 用信号量修复 badcount.c 的竞态条件 | sem_wait/sem_post |
|
||||||
|
| 任务三 | task63.c | k 个生产者 + m 个消费者,信号量同步 | 生产者-消费者模型 |
|
||||||
|
| 任务四 | task64.c | psum64.c 并行求和,测量不同线程数的加速比 | 并行计算 + 性能分析 |
|
||||||
|
| 任务五 | task66.c | 测量 fork vs pthread_create 时间 | 进程/线程创建开销对比 |
|
||||||
|
| 任务六 | task67.c | 动态线程池(sbuf_t 缓冲区满翻倍/空减半) | 线程池 + 动态数据结构 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 知识关联
|
||||||
|
|
||||||
|
- 线程是轻量级进程,理解 [[06_进程控制]] 中的 fork 有助于理解线程的优势
|
||||||
|
- 线程间的同步问题在死锁章节中会进一步讨论
|
||||||
|
- 生产者-消费者模型在 [[08_进程间通信]] 中也有管道和共享内存的实现方式
|
||||||
|
- 线程池在并发网络服务器中是核心组件
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 思考题
|
||||||
|
|
||||||
|
1. **互斥锁 vs 信号量**:互斥锁和二值信号量(初值为 1 的信号量)看起来功能相同,它们有什么本质区别?什么时候必须用信号量而不能用互斥锁?
|
||||||
|
2. **死锁的产生**:如果一个线程对已经加锁的互斥锁再次调用 `pthread_mutex_lock()`,会发生什么?如果两个线程分别持有对方需要的锁呢?
|
||||||
|
3. **线程安全的函数**:为什么 `printf()` 是线程安全的,而 `count++` 不是?如何判断一个函数是否线程安全?
|
||||||
|
4. **并行计算的瓶颈**:为什么 p 个线程的加速比通常达不到理想的 p 倍?哪些因素限制了并行加速?
|
||||||
|
5. **fork vs pthread**:在什么场景下应该用多进程而不是多线程?多进程模型有什么优势?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 扩展阅读
|
||||||
|
|
||||||
|
- 《UNIX环境高级编程》第11章:线程
|
||||||
|
- 《深入理解计算机系统》第12章:并发编程
|
||||||
|
- 《POSIX 多线程编程指南》
|
||||||
|
- [Pthreads Tutorial (LLNL)](https://computing.llnl.gov/tutorials/pthreads/)
|
||||||
406
操作系统/08_进程间通信/08_进程间通信.md
Normal file
406
操作系统/08_进程间通信/08_进程间通信.md
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
# 第08讲:进程间通信
|
||||||
|
|
||||||
|
> 🎯 **本节目标**:掌握管道、消息队列、共享内存等进程间通信方式
|
||||||
|
|
||||||
|
## 📋 前置知识
|
||||||
|
- [[06_进程控制]] — 进程的基本概念
|
||||||
|
- [[06_进程控制_深入]] — fork 和 exec 的工作原理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤔 为什么需要这个?
|
||||||
|
|
||||||
|
进程之间是相互隔离的,每个进程有自己的地址空间。但有时候进程需要协作:
|
||||||
|
- Shell 需要将 `ls` 的输出传给 `grep`
|
||||||
|
- 浏览器需要与下载管理器通信
|
||||||
|
- 数据库需要与应用程序交互
|
||||||
|
|
||||||
|
**进程间通信(IPC)** 就是解决这个问题的。
|
||||||
|
|
||||||
|
**生活比喻**:
|
||||||
|
- **管道** = 对讲机(单向通信)
|
||||||
|
- **消息队列** = 邮箱(异步通信)
|
||||||
|
- **共享内存** = 共享白板(最快的通信方式)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 核心概念
|
||||||
|
|
||||||
|
### 1. IPC 方式概览
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[进程间通信 IPC] --> B[管道 Pipe]
|
||||||
|
A --> C[消息队列 Message Queue]
|
||||||
|
A --> D[共享内存 Shared Memory]
|
||||||
|
A --> E[信号 Signal]
|
||||||
|
A --> F[信号量 Semaphore]
|
||||||
|
A --> G[套接字 Socket]
|
||||||
|
|
||||||
|
B --> B1[单向通信]
|
||||||
|
C --> C1[异步通信]
|
||||||
|
D --> D1[最快]
|
||||||
|
E --> E1[异步通知]
|
||||||
|
F --> F1[同步控制]
|
||||||
|
G --> G1[网络通信]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style D fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**对比**:
|
||||||
|
| 方式 | 优点 | 缺点 | 适用场景 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| 管道 | 简单、易用 | 单向、只能父子进程 | Shell 命令组合 |
|
||||||
|
| 消息队列 | 异步、可按类型读取 | 有大小限制 | 任务分发 |
|
||||||
|
| 共享内存 | 最快 | 需要同步机制 | 大量数据交换 |
|
||||||
|
| 信号 | 异步通知 | 只能传递信号编号 | 事件通知 |
|
||||||
|
| 信号量 | 同步控制 | 不能传递数据 | 互斥、同步 |
|
||||||
|
| 套接字 | 跨网络 | 开销大 | 网络通信 |
|
||||||
|
|
||||||
|
### 2. 管道(Pipe)
|
||||||
|
|
||||||
|
管道是最古老的 IPC 方式,用于有亲缘关系的进程之间:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[写端 fd[1]] -->|数据流| B[读端 fd[0]]
|
||||||
|
|
||||||
|
style A fill:#e8f5e9
|
||||||
|
style B fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- **单向**:只能从一端写,另一端读
|
||||||
|
- **有亲缘关系**:通常用于父子进程
|
||||||
|
- **自带同步**:读端空时阻塞,写端满时阻塞
|
||||||
|
|
||||||
|
### 3. 命名管道(FIFO)
|
||||||
|
|
||||||
|
命名管道让没有亲缘关系的进程也能通信:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[进程1] -->|写入| B[/tmp/my_fifo]
|
||||||
|
B -->|读取| C[进程2]
|
||||||
|
|
||||||
|
style B fill:#fff3e0
|
||||||
|
```
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- 有文件名,存在于文件系统中
|
||||||
|
- 任意进程都可以打开
|
||||||
|
- 使用方法与普通文件相同
|
||||||
|
|
||||||
|
### 4. 消息队列
|
||||||
|
|
||||||
|
消息队列是一种异步通信方式:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[发送方 msgsnd] -->|消息| B[消息队列]
|
||||||
|
B -->|消息| C[接收方 msgrcv]
|
||||||
|
|
||||||
|
style B fill:#fff3e0
|
||||||
|
```
|
||||||
|
|
||||||
|
**消息结构**:
|
||||||
|
```c
|
||||||
|
struct msgbuf {
|
||||||
|
long mtype; // 消息类型
|
||||||
|
char mtext[512]; // 消息内容
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 可以按类型读取消息
|
||||||
|
- 异步通信,不需要同步
|
||||||
|
- 可以设置优先级
|
||||||
|
|
||||||
|
### 5. 共享内存
|
||||||
|
|
||||||
|
共享内存是最快的 IPC 方式:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[进程1] -->|读写| B[共享内存区域]
|
||||||
|
C[进程2] -->|读写| B
|
||||||
|
|
||||||
|
style B fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**工作流程**:
|
||||||
|
1. 创建共享内存段
|
||||||
|
2. 将共享内存映射到进程地址空间
|
||||||
|
3. 直接读写共享内存
|
||||||
|
4. 使用完毕后分离
|
||||||
|
|
||||||
|
**注意**:共享内存本身不提供同步机制,需要配合信号量使用。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 动手实践
|
||||||
|
|
||||||
|
### 示例1:使用管道通信
|
||||||
|
|
||||||
|
```c
|
||||||
|
// pipe1.c - 管道通信示例
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int count;
|
||||||
|
int fds[2]; // fds[0]=读端, fds[1]=写端
|
||||||
|
const char some_data[] = "1234567890";
|
||||||
|
char buffer[BUFSIZ + 1];
|
||||||
|
|
||||||
|
memset(buffer, '\0', sizeof(buffer));
|
||||||
|
|
||||||
|
// 创建管道
|
||||||
|
pipe(fds);
|
||||||
|
|
||||||
|
// 向管道写入数据
|
||||||
|
count = Write(fds[1], (void *)some_data, strlen(some_data));
|
||||||
|
printf("Wrote %d bytes\n", count);
|
||||||
|
|
||||||
|
// 从管道读取数据
|
||||||
|
count = Read(fds[0], (void *)buffer, BUFSIZ);
|
||||||
|
printf("Read %d bytes: %s\n", count, buffer);
|
||||||
|
|
||||||
|
exit(EXIT_SUCCESS);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
gcc -o pipe1 pipe1.c -L. -lwrapper
|
||||||
|
./pipe1
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期输出**:
|
||||||
|
```
|
||||||
|
Wrote 10 bytes
|
||||||
|
Read 10 bytes: 1234567890
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例2:创建命名管道
|
||||||
|
|
||||||
|
```c
|
||||||
|
// fifo1.c - 创建命名管道
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int res = mkfifo("/tmp/my_fifo", 0777);
|
||||||
|
if (res == 0)
|
||||||
|
printf("FIFO created\n");
|
||||||
|
exit(EXIT_SUCCESS);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
gcc -o fifo1 fifo1.c
|
||||||
|
./fifo1
|
||||||
|
ls -l /tmp/my_fifo
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用命名管道**:
|
||||||
|
```bash
|
||||||
|
# 终端1:写入数据
|
||||||
|
echo "Hello FIFO" > /tmp/my_fifo
|
||||||
|
|
||||||
|
# 终端2:读取数据
|
||||||
|
cat /tmp/my_fifo
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例3:共享内存通信
|
||||||
|
|
||||||
|
```c
|
||||||
|
// shmwrite.c - 写入共享内存
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
int shmid;
|
||||||
|
key_t key;
|
||||||
|
void *shmptr;
|
||||||
|
|
||||||
|
if (argc <= 1) {
|
||||||
|
fprintf(stderr, "请以 ./shmwrite <key> <message> 形式运行\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将参数转换成十六进制数作为 key
|
||||||
|
sscanf(argv[1], "%x", &key);
|
||||||
|
|
||||||
|
// 创建共享内存
|
||||||
|
shmid = Shmget(key, 4096, IPC_CREAT | 0644);
|
||||||
|
|
||||||
|
// 将共享内存映射到进程地址空间
|
||||||
|
shmptr = Shmat(shmid, 0, 0);
|
||||||
|
|
||||||
|
// 写入数据
|
||||||
|
memcpy(shmptr, argv[2], strlen(argv[2]) + 1);
|
||||||
|
|
||||||
|
// 分离共享内存
|
||||||
|
Shmdt(shmptr);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```c
|
||||||
|
// shmread.c - 读取共享内存
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
int shmid;
|
||||||
|
key_t key;
|
||||||
|
void *shmptr;
|
||||||
|
|
||||||
|
if (argc <= 1) {
|
||||||
|
fprintf(stderr, "请以 ./shmread <key> 形式运行\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
sscanf(argv[1], "%x", &key);
|
||||||
|
|
||||||
|
// 获取已存在的共享内存
|
||||||
|
shmid = Shmget(key, 4096, IPC_CREAT | 0644);
|
||||||
|
|
||||||
|
// 映射共享内存
|
||||||
|
shmptr = Shmat(shmid, 0, 0);
|
||||||
|
|
||||||
|
// 读取数据
|
||||||
|
printf("%s\n", (char *)shmptr);
|
||||||
|
|
||||||
|
// 分离共享内存
|
||||||
|
Shmdt(shmptr);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
gcc -o shmwrite shmwrite.c -L. -lwrapper
|
||||||
|
gcc -o shmread shmread.c -L. -lwrapper
|
||||||
|
|
||||||
|
# 写入数据
|
||||||
|
./shmwrite 0x12345678 "Hello Shared Memory!"
|
||||||
|
|
||||||
|
# 读取数据
|
||||||
|
./shmread 0x12345678
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期输出**:
|
||||||
|
```
|
||||||
|
Hello Shared Memory!
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例4:消息队列通信
|
||||||
|
|
||||||
|
```c
|
||||||
|
// msgsnd1.c - 发送消息
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
typedef struct MESSAGE {
|
||||||
|
int mtype;
|
||||||
|
char mtext[512];
|
||||||
|
} mymsg, *pmymsg;
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
int msqid;
|
||||||
|
key_t key;
|
||||||
|
mymsg msginfo;
|
||||||
|
|
||||||
|
if (argc != 3) {
|
||||||
|
fprintf(stderr, "使用方法: msgsnd1 <key> <message>\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
sscanf(argv[1], "%x", &key);
|
||||||
|
|
||||||
|
// 获取消息队列
|
||||||
|
msqid = Msgget(key, 0644);
|
||||||
|
|
||||||
|
// 设置消息类型和内容
|
||||||
|
msginfo.mtype = 1;
|
||||||
|
memcpy(&msginfo.mtext, argv[2], strlen(argv[2]) + 1);
|
||||||
|
|
||||||
|
// 发送消息
|
||||||
|
Msgsnd(msqid, (pmymsg)&msginfo, strlen(msginfo.mtext) + 1, 0);
|
||||||
|
printf("you send a message \"%s\" to msq %d\n", argv[1], msqid);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```c
|
||||||
|
// msgrcv1.c - 接收消息
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
typedef struct MESSAGE {
|
||||||
|
int mtype;
|
||||||
|
char mtext[512];
|
||||||
|
} mymsg, *pmymsg;
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
int msqid;
|
||||||
|
key_t key;
|
||||||
|
mymsg msginfo;
|
||||||
|
|
||||||
|
if (argc != 2) {
|
||||||
|
fprintf(stderr, "使用方法: msgrcv1 <key>\n");
|
||||||
|
exit(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
sscanf(argv[1], "%x", &key);
|
||||||
|
|
||||||
|
// 获取消息队列
|
||||||
|
msqid = Msgget(key, 0644);
|
||||||
|
|
||||||
|
// 接收消息(类型为1)
|
||||||
|
msgrcv(msqid, (pmymsg)&msginfo, 512, 1, 0);
|
||||||
|
printf("%s\n", msginfo.mtext);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
gcc -o msgsnd1 msgsnd1.c -L. -lwrapper
|
||||||
|
gcc -o msgrcv1 msgrcv1.c -L. -lwrapper
|
||||||
|
|
||||||
|
# 发送消息
|
||||||
|
./msgsnd1 0x12345678 "Hello Message Queue!"
|
||||||
|
|
||||||
|
# 接收消息
|
||||||
|
./msgrcv1 0x12345678
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期输出**:
|
||||||
|
```
|
||||||
|
Hello Message Queue!
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 知识关联
|
||||||
|
- 管道在 Shell 中广泛使用,详见 [[06_进程控制_深入]]
|
||||||
|
- 共享内存的同步需要信号量,详见 [[07_多线程编程]]
|
||||||
|
- 套接字是网络通信的基础,详见 [[09_网络编程]]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 思考题
|
||||||
|
|
||||||
|
1. **管道的局限性**:为什么管道只能用于有亲缘关系的进程?
|
||||||
|
2. **共享内存的速度优势**:为什么共享内存比管道快?
|
||||||
|
3. **消息队列 vs 管道**:在什么场景下消息队列比管道更合适?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 扩展阅读
|
||||||
|
- 《UNIX环境高级编程》第15章:进程间通信
|
||||||
|
- 《深入理解计算机系统》第10章:系统级I/O
|
||||||
|
- [Linux IPC 编程](https://www.tldp.org/LDP/tlk/ipc/ipc.html)
|
||||||
296
操作系统/09_网络编程基础/09_网络编程基础.md
Normal file
296
操作系统/09_网络编程基础/09_网络编程基础.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# 第09讲:网络编程
|
||||||
|
|
||||||
|
> 🎯 **本节目标**:掌握 Socket 编程,理解客户端-服务器模型
|
||||||
|
|
||||||
|
## 📋 前置知识
|
||||||
|
- [[04_文件IO编程]] — 文件描述符的概念
|
||||||
|
- [[06_进程控制]] — 进程创建
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤔 为什么需要这个?
|
||||||
|
|
||||||
|
你每天都在使用网络:浏览网页、发送消息、观看视频。但你有没有想过:
|
||||||
|
- 浏览器是怎么从服务器获取网页的?
|
||||||
|
- 两台电脑之间是怎么通信的?
|
||||||
|
|
||||||
|
**网络编程**就是让你能够编写这样的程序。
|
||||||
|
|
||||||
|
**生活比喻**:
|
||||||
|
- **Socket** = 电话插座
|
||||||
|
- **服务器** = 客服中心(等待来电)
|
||||||
|
- **客户端** = 拨打电话的用户
|
||||||
|
- **端口** = 分机号
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 核心概念
|
||||||
|
|
||||||
|
### 1. 客户端-服务器模型
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant 客户端
|
||||||
|
participant 服务器
|
||||||
|
|
||||||
|
服务器->>服务器: socket() 创建套接字
|
||||||
|
服务器->>服务器: bind() 绑定地址
|
||||||
|
服务器->>服务器: listen() 监听连接
|
||||||
|
服务器->>服务器: accept() 等待连接
|
||||||
|
|
||||||
|
客户端->>客户端: socket() 创建套接字
|
||||||
|
客户端->>服务器: connect() 发起连接
|
||||||
|
|
||||||
|
服务器->>客户端: 连接建立
|
||||||
|
|
||||||
|
客户端->>服务器: send() 发送数据
|
||||||
|
服务器->>客户端: recv() 接收数据
|
||||||
|
服务器->>客户端: send() 发送响应
|
||||||
|
客户端->>服务器: recv() 接收响应
|
||||||
|
|
||||||
|
客户端->>客户端: close() 关闭连接
|
||||||
|
服务器->>服务器: close() 关闭连接
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Socket 编程流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
subgraph 服务器端
|
||||||
|
A1[socket] --> A2[bind]
|
||||||
|
A2 --> A3[listen]
|
||||||
|
A3 --> A4[accept]
|
||||||
|
A4 --> A5[read/write]
|
||||||
|
A5 --> A6[close]
|
||||||
|
end
|
||||||
|
subgraph 客户端
|
||||||
|
B1[socket] --> B2[connect]
|
||||||
|
B2 --> B3[write/read]
|
||||||
|
B3 --> B4[close]
|
||||||
|
end
|
||||||
|
|
||||||
|
style A1 fill:#e8f5e9
|
||||||
|
style B1 fill:#e1f5fe
|
||||||
|
```
|
||||||
|
|
||||||
|
**核心函数**:
|
||||||
|
| 函数 | 作用 | 服务器/客户端 |
|
||||||
|
|------|------|:---:|
|
||||||
|
| `socket()` | 创建套接字 | 都需要 |
|
||||||
|
| `bind()` | 绑定地址和端口 | 服务器 |
|
||||||
|
| `listen()` | 开始监听 | 服务器 |
|
||||||
|
| `accept()` | 接受连接 | 服务器 |
|
||||||
|
| `connect()` | 发起连接 | 客户端 |
|
||||||
|
| `send()` | 发送数据 | 都需要 |
|
||||||
|
| `recv()` | 接收数据 | 都需要 |
|
||||||
|
| `close()` | 关闭连接 | 都需要 |
|
||||||
|
|
||||||
|
### 3. IP 地址与端口
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[计算机] -->|IP地址| B[192.168.1.100]
|
||||||
|
A -->|端口| C[:80]
|
||||||
|
B --> D[唯一标识一台电脑]
|
||||||
|
C --> E[唯一标识一个服务]
|
||||||
|
|
||||||
|
style B fill:#e1f5fe
|
||||||
|
style C fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**常见端口**:
|
||||||
|
| 端口 | 服务 | 说明 |
|
||||||
|
|:---:|------|------|
|
||||||
|
| 22 | SSH | 远程登录 |
|
||||||
|
| 80 | HTTP | 网页浏览 |
|
||||||
|
| 443 | HTTPS | 安全网页 |
|
||||||
|
| 3306 | MySQL | 数据库 |
|
||||||
|
| 8080 | HTTP备用 | 开发常用 |
|
||||||
|
|
||||||
|
### 4. 字节序
|
||||||
|
|
||||||
|
不同 CPU 存储多字节数据的方式不同:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[0x12345678] --> B[大端序 Big-endian]
|
||||||
|
A --> C[小端序 Little-endian]
|
||||||
|
|
||||||
|
B --> B1[12 34 56 78]
|
||||||
|
C --> C1[78 56 34 12]
|
||||||
|
|
||||||
|
style B fill:#e1f5fe
|
||||||
|
style C fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**网络字节序**:大端序(Big-endian)
|
||||||
|
|
||||||
|
**转换函数**:
|
||||||
|
```c
|
||||||
|
htonl() // host to network long
|
||||||
|
htons() // host to network short
|
||||||
|
ntohl() // network to host long
|
||||||
|
ntohs() // network to host short
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. DNS 域名解析
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A[www.example.com] -->|DNS查询| B[DNS服务器]
|
||||||
|
B -->|返回IP| C[93.184.216.34]
|
||||||
|
C -->|连接| D[Web服务器]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style C fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 动手实践
|
||||||
|
|
||||||
|
### 示例1:查询主机信息
|
||||||
|
|
||||||
|
```c
|
||||||
|
// hostinfo.c - 查询主机信息
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
char **pp;
|
||||||
|
struct in_addr addr;
|
||||||
|
struct hostent *hostp;
|
||||||
|
|
||||||
|
if (argc != 2) {
|
||||||
|
fprintf(stderr, "usage: %s <domain name or dotted-decimal>\n", argv[0]);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是域名还是IP地址
|
||||||
|
if (inet_aton(argv[1], &addr) != 0)
|
||||||
|
hostp = Gethostbyaddr((const char *)&addr, sizeof(addr), AF_INET);
|
||||||
|
else
|
||||||
|
hostp = Gethostbyname(argv[1]);
|
||||||
|
|
||||||
|
// 打印主机信息
|
||||||
|
printf("official hostname: %s\n", hostp->h_name);
|
||||||
|
|
||||||
|
for (pp = hostp->h_aliases; *pp != NULL; pp++)
|
||||||
|
printf("alias: %s\n", *pp);
|
||||||
|
|
||||||
|
for (pp = hostp->h_addr_list; *pp != NULL; pp++) {
|
||||||
|
addr.s_addr = *((unsigned int *)*pp);
|
||||||
|
printf("address: %s\n", inet_ntoa(addr));
|
||||||
|
}
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
gcc -o hostinfo hostinfo.c -L. -lwrapper
|
||||||
|
./hostinfo www.baidu.com
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期输出**:
|
||||||
|
```
|
||||||
|
official hostname: www.a.shifen.com
|
||||||
|
alias: www.baidu.com
|
||||||
|
address: 110.242.68.66
|
||||||
|
address: 110.242.68.3
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例2:TCP 服务器(大小写转换)
|
||||||
|
|
||||||
|
```c
|
||||||
|
// toggle.c - TCP 服务器
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
void toggle(int conn_sock) {
|
||||||
|
size_t n;
|
||||||
|
int i;
|
||||||
|
char buf[MAXLINE];
|
||||||
|
|
||||||
|
while ((n = recv(conn_sock, buf, MAXLINE, 0)) > 0) {
|
||||||
|
printf("toggle server received %d bytes\n", n);
|
||||||
|
|
||||||
|
// 转换大小写
|
||||||
|
for (i = 0; i < n; i++)
|
||||||
|
if (isupper(buf[i]))
|
||||||
|
buf[i] = tolower(buf[i]);
|
||||||
|
else if (islower(buf[i]))
|
||||||
|
buf[i] = toupper(buf[i]);
|
||||||
|
|
||||||
|
send(conn_sock, buf, n, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例3:TCP 客户端
|
||||||
|
|
||||||
|
```c
|
||||||
|
// togglec.c - TCP 客户端
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
int client_sock, port;
|
||||||
|
char *host, buf[MAXLINE];
|
||||||
|
rio_t rio;
|
||||||
|
|
||||||
|
if (argc != 3) {
|
||||||
|
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
host = argv[1];
|
||||||
|
port = atoi(argv[2]);
|
||||||
|
|
||||||
|
// 连接服务器
|
||||||
|
client_sock = open_client_sock(host, port);
|
||||||
|
|
||||||
|
// 从标准输入读取,发送到服务器,接收响应
|
||||||
|
while (fgets(buf, MAXLINE, stdin) != NULL) {
|
||||||
|
send(client_sock, buf, strlen(buf), 0);
|
||||||
|
recv(client_sock, buf, MAXLINE, 0);
|
||||||
|
fputs(buf, stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(client_sock);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
# 编译
|
||||||
|
gcc -o toggle toggle.c -L. -lwrapper
|
||||||
|
gcc -o togglec togglec.c -L. -lwrapper
|
||||||
|
|
||||||
|
# 终端1:启动服务器
|
||||||
|
./toggle 8080
|
||||||
|
|
||||||
|
# 终端2:启动客户端
|
||||||
|
./togglec localhost 8080
|
||||||
|
Hello World # 输入
|
||||||
|
HELLO WORLD # 输出(大小写转换)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 知识关联
|
||||||
|
- Socket 是文件描述符,详见 [[04_文件IO编程]]
|
||||||
|
- 并发服务器在 [[10_并发服务器]] 中有详细讲解
|
||||||
|
- 网络编程在 [[实验05_Linux网络通信编程]] 中有实践练习
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 思考题
|
||||||
|
|
||||||
|
1. **TCP vs UDP**:什么时候用 TCP,什么时候用 UDP?
|
||||||
|
2. **为什么需要字节序转换?** 如果不转换会怎样?
|
||||||
|
3. **服务器为什么需要 bind()?** 客户端为什么不需要?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 扩展阅读
|
||||||
|
- 《UNIX网络编程》第1卷:套接字联网API
|
||||||
|
- [Beej's Guide to Network Programming](https://beej.us/guide/bgnet/)
|
||||||
|
- [Socket 编程详解](https://www.cs.rpi.edu/~moorthy/Courses/os98/Pggrams/socket.html)
|
||||||
395
操作系统/10_并发服务器/10_并发服务器.md
Normal file
395
操作系统/10_并发服务器/10_并发服务器.md
Normal file
@@ -0,0 +1,395 @@
|
|||||||
|
# 第10讲:并发网络服务器
|
||||||
|
|
||||||
|
> 🎯 **本节目标**:掌握多进程、多线程、I/O 多路复用三种并发服务器模型
|
||||||
|
|
||||||
|
## 📋 前置知识
|
||||||
|
- [[09_网络编程]] — Socket 编程基础
|
||||||
|
- [[06_进程控制]] — 进程创建
|
||||||
|
- [[07_多线程编程]] — 线程创建
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤔 为什么需要这个?
|
||||||
|
|
||||||
|
上一讲的服务器一次只能服务一个客户端。如果有 100 个用户同时访问网站,第 100 个用户必须等前 99 个都处理完才能得到响应。
|
||||||
|
|
||||||
|
**并发服务器**就是解决这个问题的——让服务器能够同时服务多个客户端。
|
||||||
|
|
||||||
|
**生活比喻**:
|
||||||
|
- **迭代服务器** = 一个服务员一次只服务一桌客人
|
||||||
|
- **多进程服务器** = 每来一桌客人就招一个新服务员
|
||||||
|
- **多线程服务器** = 一个服务员同时照看多桌客人
|
||||||
|
- **I/O 多路复用** = 服务员轮流查看哪桌客人需要服务
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 核心概念
|
||||||
|
|
||||||
|
### 1. 三种并发模型
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[并发服务器模型] --> B[多进程模型]
|
||||||
|
A --> C[多线程模型]
|
||||||
|
A --> D[I/O 多路复用]
|
||||||
|
|
||||||
|
B --> B1[每连接一个进程]
|
||||||
|
B --> B2[进程间隔离]
|
||||||
|
B --> B3[开销大]
|
||||||
|
|
||||||
|
C --> C1[每连接一个线程]
|
||||||
|
C --> C2[共享内存]
|
||||||
|
C --> C3[开销较小]
|
||||||
|
|
||||||
|
D --> D1[单线程处理多连接]
|
||||||
|
D --> D2[select/poll/epoll]
|
||||||
|
D --> D3[开销最小]
|
||||||
|
|
||||||
|
style B fill:#ffcdd2
|
||||||
|
style C fill:#fff3e0
|
||||||
|
style D fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**对比**:
|
||||||
|
| 模型 | 优点 | 缺点 | 适用场景 |
|
||||||
|
|------|------|------|----------|
|
||||||
|
| 多进程 | 隔离性好 | 开销大 | 连接数少 |
|
||||||
|
| 多线程 | 开销较小 | 需要同步 | 连接数中等 |
|
||||||
|
| I/O 多路复用 | 开销最小 | 编程复杂 | 连接数多 |
|
||||||
|
|
||||||
|
### 2. 多进程模型
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant 主进程
|
||||||
|
participant 子进程1
|
||||||
|
participant 子进程2
|
||||||
|
participant 客户端
|
||||||
|
|
||||||
|
主进程->>主进程: accept() 等待连接
|
||||||
|
客户端->>主进程: 连接请求
|
||||||
|
主进程->>子进程1: fork() 创建子进程
|
||||||
|
子进程1->>客户端: 处理请求
|
||||||
|
主进程->>主进程: 继续 accept()
|
||||||
|
|
||||||
|
客户端->>主进程: 新连接请求
|
||||||
|
主进程->>子进程2: fork() 创建子进程
|
||||||
|
子进程2->>客户端: 处理请求
|
||||||
|
```
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- 每个连接一个独立进程
|
||||||
|
- 进程间完全隔离
|
||||||
|
- 进程创建和销毁开销大
|
||||||
|
|
||||||
|
### 3. 多线程模型
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant 主线程
|
||||||
|
participant 工作线程1
|
||||||
|
participant 工作线程2
|
||||||
|
participant 客户端
|
||||||
|
|
||||||
|
主线程->>主线程: accept() 等待连接
|
||||||
|
客户端->>主线程: 连接请求
|
||||||
|
主线程->>工作线程1: pthread_create()
|
||||||
|
工作线程1->>客户端: 处理请求
|
||||||
|
主线程->>主线程: 继续 accept()
|
||||||
|
|
||||||
|
客户端->>主线程: 新连接请求
|
||||||
|
主线程->>工作线程2: pthread_create()
|
||||||
|
工作线程2->>客户端: 处理请求
|
||||||
|
```
|
||||||
|
|
||||||
|
**特点**:
|
||||||
|
- 每个连接一个线程
|
||||||
|
- 线程共享进程资源
|
||||||
|
- 需要注意线程安全
|
||||||
|
|
||||||
|
### 4. I/O 多路复用(select)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[主循环] --> B[select 监听所有 fd]
|
||||||
|
B --> C{哪个 fd 就绪?}
|
||||||
|
C -->|监听套接字| D[accept 新连接]
|
||||||
|
C -->|客户端套接字| E[处理请求]
|
||||||
|
D --> A
|
||||||
|
E --> A
|
||||||
|
|
||||||
|
style B fill:#fff3e0
|
||||||
|
```
|
||||||
|
|
||||||
|
**select 的工作原理**:
|
||||||
|
1. 将所有需要监听的文件描述符放入集合
|
||||||
|
2. 调用 `select()` 等待任意一个就绪
|
||||||
|
3. 遍历集合,处理就绪的描述符
|
||||||
|
4. 重复步骤 1
|
||||||
|
|
||||||
|
**fd_set 操作**:
|
||||||
|
```c
|
||||||
|
fd_set read_set;
|
||||||
|
FD_ZERO(&read_set); // 清空集合
|
||||||
|
FD_SET(fd1, &read_set); // 添加 fd1
|
||||||
|
FD_SET(fd2, &read_set); // 添加 fd2
|
||||||
|
select(maxfd+1, &read_set, NULL, NULL, NULL); // 等待
|
||||||
|
if (FD_ISSET(fd1, &read_set)) // 检查 fd1 是否就绪
|
||||||
|
// 处理 fd1
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 线程池模型
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[主线程] -->|accept| B[任务队列]
|
||||||
|
B --> C[工作线程1]
|
||||||
|
B --> D[工作线程2]
|
||||||
|
B --> E[工作线程3]
|
||||||
|
C -->|处理完毕| B
|
||||||
|
D -->|处理完毕| B
|
||||||
|
E -->|处理完毕| B
|
||||||
|
|
||||||
|
style B fill:#fff3e0
|
||||||
|
```
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 避免频繁创建销毁线程
|
||||||
|
- 控制并发数量
|
||||||
|
- 提高资源利用率
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 动手实践
|
||||||
|
|
||||||
|
### 示例1:多线程并发服务器
|
||||||
|
|
||||||
|
```c
|
||||||
|
// togglest.c - 多线程并发服务器
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
void toggle(int conn_sock);
|
||||||
|
void *serve_client(void *vargp);
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
int listen_sock, conn_sock, port, *conn_sock_p;
|
||||||
|
struct sockaddr_in clientaddr;
|
||||||
|
struct hostent *hp;
|
||||||
|
char *haddrp;
|
||||||
|
socklen_t clientlen = sizeof(struct sockaddr_in);
|
||||||
|
pthread_t tid;
|
||||||
|
|
||||||
|
if (argc != 2) {
|
||||||
|
fprintf(stderr, "usage: %s <port>\n", argv[0]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
port = atoi(argv[1]);
|
||||||
|
|
||||||
|
listen_sock = open_listen_sock(port);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
conn_sock_p = malloc(sizeof(int));
|
||||||
|
*conn_sock_p = accept(listen_sock, (SA *)&clientaddr, &clientlen);
|
||||||
|
|
||||||
|
// 获取客户端信息
|
||||||
|
hp = Gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr,
|
||||||
|
sizeof(clientaddr.sin_addr.s_addr), AF_INET);
|
||||||
|
haddrp = inet_ntoa(clientaddr.sin_addr);
|
||||||
|
printf("server connected to %s (%s)\n", hp->h_name, haddrp);
|
||||||
|
|
||||||
|
// 创建新线程处理客户端
|
||||||
|
pthread_create(&tid, NULL, serve_client, conn_sock_p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void *serve_client(void *vargp) {
|
||||||
|
int conn_sock = *((int *)vargp);
|
||||||
|
pthread_detach(pthread_self()); // 分离线程
|
||||||
|
free(vargp);
|
||||||
|
toggle(conn_sock);
|
||||||
|
close(conn_sock);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
gcc -o togglest togglest.c -L. -lwrapper -lpthread
|
||||||
|
|
||||||
|
# 终端1:启动服务器
|
||||||
|
./togglest 8080
|
||||||
|
|
||||||
|
# 终端2-4:启动多个客户端
|
||||||
|
./togglec localhost 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例2:I/O 多路复用服务器
|
||||||
|
|
||||||
|
```c
|
||||||
|
// toggless1.c - select 多路复用服务器
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
void toggle(int conn_sock);
|
||||||
|
void read_input(void);
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
int listen_sock, conn_sock, port;
|
||||||
|
socklen_t clientlen = sizeof(struct sockaddr_in);
|
||||||
|
struct sockaddr_in clientaddr;
|
||||||
|
fd_set read_set, ready_set;
|
||||||
|
|
||||||
|
if (argc != 2) {
|
||||||
|
fprintf(stderr, "usage: %s <port>\n", argv[0]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
port = atoi(argv[1]);
|
||||||
|
listen_sock = open_listen_sock(port);
|
||||||
|
|
||||||
|
// 初始化 fd_set
|
||||||
|
FD_ZERO(&read_set);
|
||||||
|
FD_SET(STDIN_FILENO, &read_set); // 监听标准输入
|
||||||
|
FD_SET(listen_sock, &read_set); // 监听套接字
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
ready_set = read_set;
|
||||||
|
select(listen_sock + 1, &ready_set, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
// 检查标准输入
|
||||||
|
if (FD_ISSET(STDIN_FILENO, &ready_set))
|
||||||
|
read_input();
|
||||||
|
|
||||||
|
// 检查新连接
|
||||||
|
if (FD_ISSET(listen_sock, &ready_set)) {
|
||||||
|
conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen);
|
||||||
|
toggle(conn_sock);
|
||||||
|
close(conn_sock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void read_input(void) {
|
||||||
|
char buf[MAXLINE];
|
||||||
|
if (!fgets(buf, MAXLINE, stdin))
|
||||||
|
exit(0);
|
||||||
|
printf("%s", buf);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例3:连接池服务器
|
||||||
|
|
||||||
|
```c
|
||||||
|
// toggless2.c - 连接池服务器
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int maxfd;
|
||||||
|
fd_set read_set;
|
||||||
|
fd_set ready_set;
|
||||||
|
int nready;
|
||||||
|
int maxi;
|
||||||
|
int client_sock[FD_SETSIZE];
|
||||||
|
} sock_pool;
|
||||||
|
|
||||||
|
void init_sock_pool(int listen_sock, sock_pool *pool);
|
||||||
|
void add_sock(int conn_sock, sock_pool *pool);
|
||||||
|
void serve_clients(sock_pool *pool);
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
int listen_sock, conn_sock, port;
|
||||||
|
socklen_t clientlen = sizeof(struct sockaddr_in);
|
||||||
|
struct sockaddr_in clientaddr;
|
||||||
|
static sock_pool pool;
|
||||||
|
|
||||||
|
if (argc != 2) {
|
||||||
|
fprintf(stderr, "usage: %s <port>\n", argv[0]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
port = atoi(argv[1]);
|
||||||
|
|
||||||
|
listen_sock = open_listen_sock(port);
|
||||||
|
init_sock_pool(listen_sock, &pool);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
pool.ready_set = pool.read_set;
|
||||||
|
pool.nready = select(pool.maxfd + 1, &pool.ready_set, NULL, NULL, NULL);
|
||||||
|
|
||||||
|
// 新连接
|
||||||
|
if (FD_ISSET(listen_sock, &pool.ready_set)) {
|
||||||
|
conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen);
|
||||||
|
add_sock(conn_sock, &pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理客户端请求
|
||||||
|
serve_clients(&pool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void init_sock_pool(int listen_sock, sock_pool *p) {
|
||||||
|
int i;
|
||||||
|
p->maxi = -1;
|
||||||
|
for (i = 0; i < FD_SETSIZE; i++)
|
||||||
|
p->client_sock[i] = -1;
|
||||||
|
p->maxfd = listen_sock;
|
||||||
|
FD_ZERO(&p->read_set);
|
||||||
|
FD_SET(listen_sock, &p->read_set);
|
||||||
|
}
|
||||||
|
|
||||||
|
void add_sock(int conn_sock, sock_pool *p) {
|
||||||
|
int i;
|
||||||
|
p->nready--;
|
||||||
|
for (i = 0; i < FD_SETSIZE; i++)
|
||||||
|
if (p->client_sock[i] < 0) {
|
||||||
|
p->client_sock[i] = conn_sock;
|
||||||
|
FD_SET(conn_sock, &p->read_set);
|
||||||
|
if (conn_sock > p->maxfd)
|
||||||
|
p->maxfd = conn_sock;
|
||||||
|
if (i > p->maxi)
|
||||||
|
p->maxi = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (i == FD_SETSIZE)
|
||||||
|
perror("add_sock error: Too many clients");
|
||||||
|
}
|
||||||
|
|
||||||
|
void serve_clients(sock_pool *p) {
|
||||||
|
int i, conn_sock, n;
|
||||||
|
char buf[MAXLINE];
|
||||||
|
|
||||||
|
for (i = 0; (i <= p->maxi) && (p->nready > 0); i++) {
|
||||||
|
conn_sock = p->client_sock[i];
|
||||||
|
if ((conn_sock > 0) && (FD_ISSET(conn_sock, &p->ready_set))) {
|
||||||
|
p->nready--;
|
||||||
|
if ((n = recv(conn_sock, buf, MAXLINE, 0)) != 0) {
|
||||||
|
printf("Server received %d bytes on fd %d\n", n, conn_sock);
|
||||||
|
send(conn_sock, buf, n, 0);
|
||||||
|
} else {
|
||||||
|
close(conn_sock);
|
||||||
|
FD_CLR(conn_sock, &p->read_set);
|
||||||
|
p->client_sock[i] = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 知识关联
|
||||||
|
- 多进程模型在 [[06_进程控制]] 中有详细讲解
|
||||||
|
- 多线程模型在 [[07_多线程编程]] 中有详细讲解
|
||||||
|
- I/O 多路复用在 [[17_IO系统]] 中有更深入的讨论
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 思考题
|
||||||
|
|
||||||
|
1. **为什么 select 有 FD_SETSIZE 限制?** 如何突破这个限制?
|
||||||
|
2. **多进程 vs 多线程**:在什么情况下多进程比多线程更合适?
|
||||||
|
3. **epoll 的优势**:为什么 Linux 推荐使用 epoll 而不是 select?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 扩展阅读
|
||||||
|
- 《UNIX网络编程》第1卷:第6章、第16章
|
||||||
|
- [epoll 详解](https://man7.org/linux/man-pages/man7/epoll.7.html)
|
||||||
|
- [高性能网络编程](https://www.zhihu.com/question/28594409)
|
||||||
705
操作系统/11_处理机调度/11_处理机调度.md
Normal file
705
操作系统/11_处理机调度/11_处理机调度.md
Normal file
@@ -0,0 +1,705 @@
|
|||||||
|
# 11 处理机调度
|
||||||
|
|
||||||
|
> **课程**:操作系统
|
||||||
|
> **关联章节**:[[06_进程控制]] | [[12_死锁]]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、处理器调度的概念
|
||||||
|
|
||||||
|
### 1.1 什么是调度
|
||||||
|
|
||||||
|
处理器调度的核心任务是将 **CPU 时间** 公平、合理地分配给系统中的各个进程。其本质是一种**资源分配决策**——决定在某一时刻,哪个进程获得 CPU 的使用权。
|
||||||
|
|
||||||
|
> **类比**:学校排课——教室(CPU)有限,多个班级(进程)都要使用,需要教务处(调度器)制定合理的课表。
|
||||||
|
|
||||||
|
### 1.2 调度的基本原则
|
||||||
|
|
||||||
|
| 原则 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **公平性** | 每个进程都能获得合理的 CPU 时间 |
|
||||||
|
| **高效性** | CPU 利用率尽可能高 |
|
||||||
|
| **响应性** | 交互式进程能快速得到响应 |
|
||||||
|
| **吞吐量** | 单位时间内完成的进程数尽可能多 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、作业(Job)的概念
|
||||||
|
|
||||||
|
### 2.1 作业与作业步
|
||||||
|
|
||||||
|
- **作业(Job)**:用户提交给系统的一项**完整工作**,是系统进行资源分配的基本单位。
|
||||||
|
- **作业步(Job Step)**:作业中若干个**既独立又相互关联**的加工步骤。每个作业步完成一项特定的处理任务。
|
||||||
|
|
||||||
|
```
|
||||||
|
作业示例:编写并运行一个C程序
|
||||||
|
作业步1:编辑(edit)→ 生成源代码文件
|
||||||
|
作业步2:编译(compile)→ 生成目标文件
|
||||||
|
作业步3:链接(link)→ 生成可执行文件
|
||||||
|
作业步4:运行(run)→ 输出结果
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 作业的状态转换
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> 后备 : 用户提交作业
|
||||||
|
后备 --> 运行 : 高级调度(被选中)
|
||||||
|
运行 --> 完成 : 作业执行结束
|
||||||
|
完成 --> [*] : 撤离系统
|
||||||
|
|
||||||
|
state 运行 {
|
||||||
|
[*] --> 就绪 : 进程创建
|
||||||
|
就绪 --> 执行 : 低级调度(被选中)
|
||||||
|
执行 --> 就绪 : 时间片用完
|
||||||
|
执行 --> 阻塞 : 等待I/O
|
||||||
|
阻塞 --> 就绪 : I/O完成
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 作业控制块(JCB)
|
||||||
|
|
||||||
|
作业控制块是作业在系统中存在的唯一标识,包含:
|
||||||
|
|
||||||
|
- 作业名、作业状态
|
||||||
|
- 资源需求(内存大小、外设类型等)
|
||||||
|
- 优先级、提交时间
|
||||||
|
- 作业步信息
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、三级调度体系
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph 外存
|
||||||
|
A[后备队列<br/>作业池]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 内存
|
||||||
|
B[就绪队列]
|
||||||
|
C[阻塞队列]
|
||||||
|
D[CPU 执行]
|
||||||
|
end
|
||||||
|
|
||||||
|
A -->|高级调度<br/>长程调度<br/>作业调度| B
|
||||||
|
B -->|低级调度<br/>短程调度<br/>进程调度| D
|
||||||
|
D -->|时间片用完/等待事件| B
|
||||||
|
D -->|等待I/O| C
|
||||||
|
C -->|I/O完成| B
|
||||||
|
|
||||||
|
B -->|中级调度<br/>交换调度<br/>内存调度| A
|
||||||
|
|
||||||
|
style A fill:#f9f,stroke:#333
|
||||||
|
style D fill:#ff9,stroke:#333
|
||||||
|
```
|
||||||
|
|
||||||
|
| 调度级别 | 别名 | 频率 | 功能 |
|
||||||
|
|----------|------|------|------|
|
||||||
|
| **高级调度** | 作业调度 / 长程调度 | 最低 | 从后备队列选择作业调入内存 |
|
||||||
|
| **中级调度** | 交换调度 / 内存调度 | 中等 | 将暂时不用的进程换出到外存(挂起) |
|
||||||
|
| **低级调度** | 进程调度 / 短程调度 | 最高 | 从就绪队列选择进程分配 CPU |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、调度时机
|
||||||
|
|
||||||
|
### 4.1 何时触发调度
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A{发生什么事件?} --> B[进程结束]
|
||||||
|
A --> C[进程等待事件<br/>如I/O请求]
|
||||||
|
A --> D[时间片用完]
|
||||||
|
A --> E[新进程产生]
|
||||||
|
A --> F[等待的事件已发生<br/>如I/O完成]
|
||||||
|
|
||||||
|
B --> G[执行调度]
|
||||||
|
C --> G
|
||||||
|
D --> G
|
||||||
|
E --> G
|
||||||
|
F --> G
|
||||||
|
|
||||||
|
G --> H[选择下一个<br/>运行的进程]
|
||||||
|
|
||||||
|
style G fill:#ff9,stroke:#333
|
||||||
|
```
|
||||||
|
|
||||||
|
| 调度时机 | 说明 | 触发场景 |
|
||||||
|
|----------|------|----------|
|
||||||
|
| 进程结束 | 运行进程执行完毕 | 正常退出 |
|
||||||
|
| 等待事件 | 进程因 I/O 等阻塞 | 读磁盘、等待键盘输入 |
|
||||||
|
| 时间片用完 | 当前进程的时间配额耗尽 | 时钟中断 |
|
||||||
|
| 新进程产生 | 新进程进入就绪队列 | fork() 创建子进程 |
|
||||||
|
| 等待事件已发生 | 阻塞进程变就绪 | I/O 中断 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、调度方式
|
||||||
|
|
||||||
|
### 5.1 非抢占式调度
|
||||||
|
|
||||||
|
进程**自愿放弃** CPU,系统不会强制剥夺。只有当进程结束、阻塞或主动让出时才调度。
|
||||||
|
|
||||||
|
- **优点**:实现简单,系统开销小
|
||||||
|
- **缺点**:响应时间不可预测,不适合交互式系统
|
||||||
|
|
||||||
|
### 5.2 抢占式调度
|
||||||
|
|
||||||
|
系统**强制剥夺**当前运行进程的 CPU,分配给其他进程。
|
||||||
|
|
||||||
|
- **优点**:响应快,适合分时/实时系统
|
||||||
|
- **缺点**:实现复杂,需要保存/恢复现场,开销较大
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph 非抢占式
|
||||||
|
A[进程A运行] -->|自愿放弃| B[调度]
|
||||||
|
B --> C[进程B运行]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 抢占式
|
||||||
|
D[进程A运行] -->|强制剥夺| E[调度]
|
||||||
|
E --> F[进程B运行]
|
||||||
|
end
|
||||||
|
|
||||||
|
style E fill:#f96,stroke:#333
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、调度算法的设计目标
|
||||||
|
|
||||||
|
| 系统类型 | 主要目标 | 关键指标 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| **批处理系统** | 周转时间短、吞吐量高 | 平均周转时间、吞吐量 |
|
||||||
|
| **分时系统** | 响应时间快、均衡性 | 响应时间、公平性 |
|
||||||
|
| **实时系统** | 满足截止时间、可预测性 | 截止时间满足率 |
|
||||||
|
|
||||||
|
### 关键性能指标
|
||||||
|
|
||||||
|
$$\text{周转时间} = \text{完成时间} - \text{提交时间}$$
|
||||||
|
|
||||||
|
$$\text{等待时间} = \text{周转时间} - \text{运行时间}$$
|
||||||
|
|
||||||
|
$$\text{带权周转时间} = \frac{\text{周转时间}}{\text{运行时间}}$$
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、批处理调度算法
|
||||||
|
|
||||||
|
### 7.1 FCFS — 先来先服务
|
||||||
|
|
||||||
|
**First Come First Served**:按进程到达就绪队列的**先后顺序**依次调度。
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
|
||||||
|
假设三个进程 P1、P2、P3 几乎同时到达:
|
||||||
|
|
||||||
|
| 进程 | 运行时间 |
|
||||||
|
|------|----------|
|
||||||
|
| P1 | 24 |
|
||||||
|
| P2 | 3 |
|
||||||
|
| P3 | 3 |
|
||||||
|
|
||||||
|
**按 P1→P2→P3 顺序执行:**
|
||||||
|
|
||||||
|
```
|
||||||
|
时间轴(Gantt图):
|
||||||
|
|--------P1(24)--------|--P2(3)--|--P3(3)--|
|
||||||
|
0 24 27 30
|
||||||
|
```
|
||||||
|
|
||||||
|
| 进程 | 完成时间 | 周转时间 | 等待时间 |
|
||||||
|
|------|----------|----------|----------|
|
||||||
|
| P1 | 24 | 24 | 0 |
|
||||||
|
| P2 | 27 | 27 | 24 |
|
||||||
|
| P3 | 30 | 30 | 27 |
|
||||||
|
| **平均** | | **27** | **17** |
|
||||||
|
|
||||||
|
**按 P2→P3→P1 顺序执行(短作业在前):**
|
||||||
|
|
||||||
|
```
|
||||||
|
时间轴(Gantt图):
|
||||||
|
|--P2(3)--|--P3(3)--|--------P1(24)--------|
|
||||||
|
0 3 6 30
|
||||||
|
```
|
||||||
|
|
||||||
|
| 进程 | 完成时间 | 周转时间 | 等待时间 |
|
||||||
|
|------|----------|----------|----------|
|
||||||
|
| P2 | 3 | 3 | 0 |
|
||||||
|
| P3 | 6 | 6 | 3 |
|
||||||
|
| P1 | 30 | 30 | 6 |
|
||||||
|
| **平均** | | **13** | **3** |
|
||||||
|
|
||||||
|
> **结论**:FCFS 对短作业非常不利,平均等待时间从 17 降到仅 3(仅调整顺序)。
|
||||||
|
|
||||||
|
### 7.2 SJF — 短作业优先
|
||||||
|
|
||||||
|
**Shortest Job First**:选择**估计运行时间最短**的进程优先执行。
|
||||||
|
|
||||||
|
#### 两种变体
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[SJF 短作业优先] --> B[非抢占式 SJF]
|
||||||
|
A --> C[抢占式 SJF<br/>即 SRTF]
|
||||||
|
|
||||||
|
B --> D[进程开始执行后<br/>不会被中断]
|
||||||
|
C --> E[新进程到达时<br/>比较剩余时间<br/>更短则抢占]
|
||||||
|
|
||||||
|
style C fill:#f96,stroke:#333
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 示例(非抢占式 SJF)
|
||||||
|
|
||||||
|
| 进程 | 到达时间 | 运行时间 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| P1 | 0 | 7 |
|
||||||
|
| P2 | 2 | 4 |
|
||||||
|
| P3 | 4 | 1 |
|
||||||
|
| P4 | 5 | 4 |
|
||||||
|
|
||||||
|
```
|
||||||
|
Gantt图(非抢占SJF):
|
||||||
|
|--P1(7)--|-P3(1)-|---P2(4)---|---P4(4)---|
|
||||||
|
0 7 8 12 16
|
||||||
|
```
|
||||||
|
|
||||||
|
- t=0 时只有 P1,P1 先执行
|
||||||
|
- t=7 时 P2、P3、P4 都已到达,选最短的 P3(运行时间 1)
|
||||||
|
- t=8 时选 P2(运行时间 4)
|
||||||
|
- t=12 时选 P4
|
||||||
|
|
||||||
|
#### 示例(抢占式 SRTF)
|
||||||
|
|
||||||
|
| 进程 | 到达时间 | 运行时间 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| P1 | 0 | 7 |
|
||||||
|
| P2 | 2 | 4 |
|
||||||
|
| P3 | 4 | 1 |
|
||||||
|
| P4 | 5 | 4 |
|
||||||
|
|
||||||
|
```
|
||||||
|
Gantt图(抢占式SRTF):
|
||||||
|
|--P1--|P1|P3|---P2---|---P4---|--P1--|
|
||||||
|
0 2 4 5 9 13 16
|
||||||
|
|
||||||
|
时间线:
|
||||||
|
0-2: P1运行(剩5)
|
||||||
|
2: P2到达(需4) < P1剩余(5), P2抢占
|
||||||
|
2-4: P2运行(剩2)
|
||||||
|
4: P3到达(需1) < P2剩余(2), P3抢占
|
||||||
|
4-5: P3完成
|
||||||
|
5: P4到达(需4) = P2剩余(2), P2先到
|
||||||
|
5-7: P2完成(剩2)
|
||||||
|
7: P4(需4) vs P1(剩5), P4更短
|
||||||
|
7-11: P4完成
|
||||||
|
11-16: P1完成(剩5)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 进程 | 完成时间 | 周转时间 | 等待时间 |
|
||||||
|
|------|----------|----------|----------|
|
||||||
|
| P1 | 16 | 16 | 9 |
|
||||||
|
| P2 | 9 | 7 | 3 |
|
||||||
|
| P3 | 5 | 1 | 0 |
|
||||||
|
| P4 | 13 | 8 | 4 |
|
||||||
|
| **平均** | | **8** | **4** |
|
||||||
|
|
||||||
|
#### SJF 的优缺点
|
||||||
|
|
||||||
|
| 优点 | 缺点 |
|
||||||
|
|------|------|
|
||||||
|
| 平均等待时间最小(理论上最优) | 对长作业不利,可能产生**饥饿** |
|
||||||
|
| 吞吐量高 | 需要预知运行时间(难以精确估计) |
|
||||||
|
|
||||||
|
### 7.3 HRRF — 响应比高优先
|
||||||
|
|
||||||
|
**Highest Response Ratio First**:FCFS 和 SJF 的折衷方案。
|
||||||
|
|
||||||
|
$$\text{响应比} = 1 + \frac{\text{等待时间}}{\text{估计运行时间}}$$
|
||||||
|
|
||||||
|
- 短作业:运行时间小,响应比容易变高 → 兼顾 SJF
|
||||||
|
- 长作业:随着等待时间增长,响应比逐渐增大 → 避免饥饿
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A[选择响应比最高的进程] --> B{响应比 = 1 + 等待时间/运行时间}
|
||||||
|
B --> C[短作业:等待时间相同时<br/>运行时间短 → 响应比高]
|
||||||
|
B --> D[长作业:等待时间越长<br/>响应比越高 → 不会饥饿]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
|
||||||
|
| 进程 | 到达时间 | 运行时间 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| P1 | 0 | 20 |
|
||||||
|
| P2 | 0 | 5 |
|
||||||
|
| P3 | 0 | 10 |
|
||||||
|
|
||||||
|
t=0: 三个进程同时到达,计算响应比:
|
||||||
|
- P1: 1 + 0/20 = 1
|
||||||
|
- P2: 1 + 0/5 = 1
|
||||||
|
- P3: 1 + 0/10 = 1
|
||||||
|
|
||||||
|
全部相同,按 FCFS 选 P1(运行时间 20)。
|
||||||
|
|
||||||
|
t=20: P1 完成,计算剩余进程响应比:
|
||||||
|
- P2: 1 + 20/5 = 5
|
||||||
|
- P3: 1 + 20/10 = 3
|
||||||
|
|
||||||
|
选 P2(响应比 5),P2 执行到 t=25。
|
||||||
|
|
||||||
|
t=25: 选 P3(响应比 1+25/10=3.5),P3 执行到 t=35。
|
||||||
|
|
||||||
|
### 7.4 优先级调度
|
||||||
|
|
||||||
|
**Priority Scheduling**:为每个进程分配优先级,选择优先级最高的进程执行。
|
||||||
|
|
||||||
|
| 分类方式 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| **静态优先级** | 创建时确定,运行期间不变 |
|
||||||
|
| **动态优先级** | 运行期间根据情况调整 |
|
||||||
|
| **抢占式** | 高优先级进程到达时可抢占当前进程 |
|
||||||
|
| **非抢占式** | 等当前进程完成后才重新调度 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、分时调度算法
|
||||||
|
|
||||||
|
### 8.1 RR — 时间片轮转
|
||||||
|
|
||||||
|
**Round Robin**:就绪队列中的进程按**先来先服务**依次执行,每个进程每次最多运行一个**时间片**(quantum)的时间。
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
|
||||||
|
4 个进程,时间片 q = 4:
|
||||||
|
|
||||||
|
| 进程 | 到达时间 | 运行时间 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| P1 | 0 | 24 |
|
||||||
|
| P2 | 0 | 3 |
|
||||||
|
| P3 | 0 | 3 |
|
||||||
|
| P4 | 0 | 6 |
|
||||||
|
|
||||||
|
```
|
||||||
|
Gantt图(时间片q=4):
|
||||||
|
|P1(4)|P2(3)|P3(3)|P4(4)|P1(4)|P4(2)|P1(4)|P1(4)|P1(4)|P1(4)|
|
||||||
|
0 4 7 10 14 18 20 24 28 32 36
|
||||||
|
|
||||||
|
执行顺序:
|
||||||
|
0-4: P1执行(剩20)
|
||||||
|
4-7: P2执行(完成) ← P2只需3 < 时间片4
|
||||||
|
7-10: P3执行(完成) ← P3只需3 < 时间片4
|
||||||
|
10-14: P4执行(剩2)
|
||||||
|
14-18: P1执行(剩16)
|
||||||
|
18-20: P4执行(完成) ← P4剩2 < 时间片4
|
||||||
|
20-24: P1执行(剩12)
|
||||||
|
24-28: P1执行(剩8)
|
||||||
|
28-32: P1执行(剩4)
|
||||||
|
32-36: P1执行(完成)
|
||||||
|
```
|
||||||
|
|
||||||
|
| 进程 | 完成时间 | 周转时间 | 等待时间 |
|
||||||
|
|------|----------|----------|----------|
|
||||||
|
| P1 | 36 | 36 | 12 |
|
||||||
|
| P2 | 7 | 7 | 4 |
|
||||||
|
| P3 | 10 | 10 | 7 |
|
||||||
|
| P4 | 20 | 20 | 14 |
|
||||||
|
| **平均** | | **18.25** | **9.25** |
|
||||||
|
|
||||||
|
#### 时间片大小的影响
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[时间片 q 的选择] --> B[q 太大]
|
||||||
|
A --> C[q 太小]
|
||||||
|
A --> D[q 适中]
|
||||||
|
|
||||||
|
B --> E[退化为 FCFS<br/>响应时间变长]
|
||||||
|
C --> F[进程切换频繁<br/>系统开销过大]
|
||||||
|
D --> G[兼顾响应性和效率<br/>通常 10~100ms]
|
||||||
|
|
||||||
|
style B fill:#f99,stroke:#333
|
||||||
|
style C fill:#f99,stroke:#333
|
||||||
|
style D fill:#9f9,stroke:#333
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 时间片选择示例
|
||||||
|
|
||||||
|
> 10 个进程,进程切换开销为 10ms。
|
||||||
|
>
|
||||||
|
> - 时间片 q = 200ms:每个进程切换一次,总切换时间 = 10×10ms = 100ms
|
||||||
|
> - 开销比 = 100 / (10×200) = **5%** → 可接受
|
||||||
|
> - 时间片 q = 100ms:每个进程切换约2次,总切换时间 = 20×10ms = 200ms
|
||||||
|
> - 开销比 = 200 / (10×200) = **10%** → 开销偏高
|
||||||
|
|
||||||
|
### 8.2 多级反馈队列(MFQ)
|
||||||
|
|
||||||
|
**Multi-level Feedback Queue**:综合多种调度算法优点的经典调度算法。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph MFQ多级反馈队列
|
||||||
|
A[新进程到达] --> B[队列1<br/>优先级最高<br/>时间片最小 如4ms]
|
||||||
|
B -->|时间片用完<br/>未完成| C[队列2<br/>优先级中等<br/>时间片中等 如8ms]
|
||||||
|
C -->|时间片用完<br/>未完成| D[队列3<br/>优先级最低<br/>时间片最大 如16ms]
|
||||||
|
end
|
||||||
|
|
||||||
|
B -->|完成| E[进程结束]
|
||||||
|
C -->|完成| E
|
||||||
|
D -->|完成| E
|
||||||
|
|
||||||
|
B -->|等待I/O| F[阻塞]
|
||||||
|
F -->|I/O完成| B
|
||||||
|
|
||||||
|
style B fill:#9f9,stroke:#333
|
||||||
|
style C fill:#ff9,stroke:#333
|
||||||
|
style D fill:#f99,stroke:#333
|
||||||
|
```
|
||||||
|
|
||||||
|
#### MFQ 的核心规则
|
||||||
|
|
||||||
|
1. **新进程**进入**最高优先级**队列(队列1)
|
||||||
|
2. 进程在当前队列用完时间片 → **降级**到下一优先级队列
|
||||||
|
3. 高优先级队列**空**时,才调度低优先级队列
|
||||||
|
4. 可设置**队列间调度策略**:固定优先级 或 按时间比例分配
|
||||||
|
|
||||||
|
| 特点 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 自适应 | I/O 密集型进程常在高优先级队列(因时间片未用完就阻塞) |
|
||||||
|
| 周转时间短 | CPU 密集型进程最终会降到低优先级队列,用大时间片执行 |
|
||||||
|
| 公平性 | 各类用户(交互型、批处理型)都能得到合理服务 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、实时调度算法
|
||||||
|
|
||||||
|
### 9.1 EDF — 最早截止时间优先
|
||||||
|
|
||||||
|
**Earliest Deadline First**:截止时间越早的进程,优先级越高。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
gantt
|
||||||
|
title EDF 调度示例
|
||||||
|
dateFormat X
|
||||||
|
axisFormat %s
|
||||||
|
|
||||||
|
section 进程
|
||||||
|
P1(截止t=4) :a1, 0, 2
|
||||||
|
P2(截止t=6) :a2, 2, 5
|
||||||
|
P3(截止t=8) :a3, 5, 7
|
||||||
|
```
|
||||||
|
|
||||||
|
| 进程 | 到达时间 | 运行时间 | 截止时间 |
|
||||||
|
|------|----------|----------|----------|
|
||||||
|
| P1 | 0 | 2 | 4 |
|
||||||
|
| P2 | 0 | 3 | 6 |
|
||||||
|
| P3 | 0 | 2 | 8 |
|
||||||
|
|
||||||
|
调度过程:
|
||||||
|
1. t=0:P1(截止4) < P2(截止6) < P3(截止8) → 执行 P1
|
||||||
|
2. t=2:P1 完成,P2(截止6) < P3(截止8) → 执行 P2
|
||||||
|
3. t=5:P2 完成,执行 P3
|
||||||
|
4. t=7:全部完成,均满足截止时间
|
||||||
|
|
||||||
|
### 9.2 LLF — 最低松弛度优先
|
||||||
|
|
||||||
|
**Least Laxity First**:选择**松弛度最小**的进程优先执行。
|
||||||
|
|
||||||
|
$$\text{松弛度} = \text{必须完成时间} - \text{本身运行时间} - \text{当前时间}$$
|
||||||
|
|
||||||
|
> 松弛度越小,说明该进程越"紧迫"。
|
||||||
|
|
||||||
|
#### 示例
|
||||||
|
|
||||||
|
| 进程 | 运行时间 | 截止时间 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| P1 | 2 | 8 |
|
||||||
|
| P2 | 4 | 12 |
|
||||||
|
|
||||||
|
t=0 时刻:
|
||||||
|
- P1 松弛度 = 8 - 2 - 0 = **6**
|
||||||
|
- P2 松弛度 = 12 - 4 - 0 = **8**
|
||||||
|
|
||||||
|
P1 松弛度更小,先执行 P1。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、优先级倒置问题
|
||||||
|
|
||||||
|
### 10.1 问题描述
|
||||||
|
|
||||||
|
**优先级倒置(Priority Inversion)**:高优先级进程被低优先级进程间接阻塞的现象。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant H as 高优先级进程H
|
||||||
|
participant M as 中优先级进程M
|
||||||
|
participant L as 低优先级进程L
|
||||||
|
participant R as 共享资源R
|
||||||
|
|
||||||
|
L->>R: 获得资源R(加锁)
|
||||||
|
H->>R: 请求资源R → 阻塞!
|
||||||
|
Note over H: H必须等待L释放R
|
||||||
|
M->>M: M到达,优先级高于L
|
||||||
|
Note over M: M抢占L执行
|
||||||
|
Note over H: H被M间接阻塞!<br/>优先级倒置发生!
|
||||||
|
M->>M: M执行完毕
|
||||||
|
L->>R: 释放资源R
|
||||||
|
H->>R: 获得资源R,继续执行
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 解决方案——优先级继承
|
||||||
|
|
||||||
|
**Priority Inheritance Protocol**:当高优先级进程因共享资源阻塞时,**临时提升**持有该资源的低优先级进程的优先级,使其尽快完成并释放资源。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant H as 高优先级进程H
|
||||||
|
participant L as 低优先级进程L(提升优先级)
|
||||||
|
participant M as 中优先级进程M
|
||||||
|
participant R as 共享资源R
|
||||||
|
|
||||||
|
L->>R: 获得资源R
|
||||||
|
H->>R: 请求资源R → 阻塞
|
||||||
|
Note over L: L的优先级临时提升为H的优先级
|
||||||
|
Note over M: M到达,但L优先级已提升
|
||||||
|
Note over M: M无法抢占L!
|
||||||
|
L->>R: 释放资源R
|
||||||
|
Note over L: L优先级恢复原值
|
||||||
|
H->>R: 获得资源R,继续执行
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、Linux 调度器
|
||||||
|
|
||||||
|
### 11.1 CFS — 完全公平调度器
|
||||||
|
|
||||||
|
**Completely Fair Scheduler**:Linux 默认调度器,基于**虚拟运行时间(vruntime)**实现公平调度。
|
||||||
|
|
||||||
|
核心思想:**vruntime 最小的进程优先执行**。
|
||||||
|
|
||||||
|
$$\text{vruntime} += \text{实际运行时间} \times \frac{\text{nice 值基准权重}}{\text{该进程权重}}$$
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[CFS调度器] --> B{选择 vruntime<br/>最小的进程}
|
||||||
|
B --> C[红黑树组织<br/>就绪进程]
|
||||||
|
C --> D[最左叶子节点<br/>= vruntime 最小]
|
||||||
|
D --> E[执行该进程]
|
||||||
|
E --> F[更新 vruntime]
|
||||||
|
F --> C
|
||||||
|
|
||||||
|
style D fill:#9f9,stroke:#333
|
||||||
|
```
|
||||||
|
|
||||||
|
#### CFS 特点
|
||||||
|
|
||||||
|
| 特性 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 无固定时间片 | 根据进程权重和系统负载动态计算 |
|
||||||
|
| 红黑树结构 | O(log n) 插入/查找,高效调度 |
|
||||||
|
| 公平性保证 | nice 值越低(优先级越高),vruntime 增长越慢 |
|
||||||
|
| 自动适配 | I/O 密集型进程 vruntime 自然较小,获得更多 CPU |
|
||||||
|
|
||||||
|
### 11.2 调度器类层次
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
A[调度器类优先级] --> B[Stop Task<br/>最高优先级<br/>停止特定CPU]
|
||||||
|
A --> C[Real-Time<br/>实时调度类]
|
||||||
|
A --> D[Fair<br/>CFS 调度类<br/>普通进程]
|
||||||
|
A --> E[Idle Task<br/>最低优先级<br/>空闲任务]
|
||||||
|
|
||||||
|
B --> C --> D --> E
|
||||||
|
|
||||||
|
style B fill:#f66,stroke:#333
|
||||||
|
style C fill:#f96,stroke:#333
|
||||||
|
style D fill:#9f9,stroke:#333
|
||||||
|
style E fill:#99f,stroke:#333
|
||||||
|
```
|
||||||
|
|
||||||
|
**调度顺序**:Stop Task > Real-Time > Fair > Idle Task
|
||||||
|
|
||||||
|
每次调度时,优先从高优先级调度类中选择进程。
|
||||||
|
|
||||||
|
### 11.3 实时进程调度策略
|
||||||
|
|
||||||
|
| 策略 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **SCHED_FIFO** | 先来先服务。实时进程一旦获得 CPU,将一直执行直到主动放弃或被更高优先级进程抢占 |
|
||||||
|
| **SCHED_RR** | 时间片轮转。同优先级的实时进程轮流使用 CPU,时间片用完后回到队尾 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、调度算法对比总结
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph 批处理算法
|
||||||
|
FCFS[FCFS<br/>简单公平]
|
||||||
|
SJF[SJF/SRTF<br/>最优平均等待]
|
||||||
|
HRRF[HRRF<br/>FCFS+SJF折衷]
|
||||||
|
PRIO[优先级调度<br/>灵活控制]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 分时算法
|
||||||
|
RR[RR 轮转<br/>公平响应]
|
||||||
|
MFQ[多级反馈队列<br/>综合最优]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 实时算法
|
||||||
|
EDF[EDF<br/>截止时间优先]
|
||||||
|
LLF[LLF<br/>松弛度优先]
|
||||||
|
end
|
||||||
|
|
||||||
|
FCFS -.->|缺点: 短作业等待久| SJF
|
||||||
|
SJF -.->|缺点: 长作业饥饿| HRRF
|
||||||
|
RR -.->|时间片选择关键| MFQ
|
||||||
|
```
|
||||||
|
|
||||||
|
| 算法 | 类型 | 抢占 | 优点 | 缺点 |
|
||||||
|
|------|------|------|------|------|
|
||||||
|
| FCFS | 批处理 | 否 | 简单公平 | 短作业等待时间长 |
|
||||||
|
| SJF | 批处理 | 否 | 最小平均等待 | 长作业饥饿 |
|
||||||
|
| SRTF | 批处理 | 是 | 更优的平均等待 | 长作业饥饿 |
|
||||||
|
| HRRF | 批处理 | 否 | 兼顾公平 | 需预估运行时间 |
|
||||||
|
| RR | 分时 | 是 | 响应快 | 时间片选择影响大 |
|
||||||
|
| MFQ | 分时 | 是 | 自适应各类进程 | 参数调整复杂 |
|
||||||
|
| EDF | 实时 | 是 | 利用率可达100% | 过载时不确定 |
|
||||||
|
| LLF | 实时 | 是 | 松弛度精确 | 频繁切换开销 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十三、本讲关键公式
|
||||||
|
|
||||||
|
$$\text{周转时间} = \text{完成时间} - \text{提交时间}$$
|
||||||
|
|
||||||
|
$$\text{等待时间} = \text{周转时间} - \text{运行时间}$$
|
||||||
|
|
||||||
|
$$\text{带权周转时间} = \frac{\text{周转时间}}{\text{运行时间}} \geq 1$$
|
||||||
|
|
||||||
|
$$\text{平均周转时间} = \frac{1}{n}\sum_{i=1}^{n}T_i$$
|
||||||
|
|
||||||
|
$$\text{响应比} = 1 + \frac{\text{等待时间}}{\text{估计运行时间}}$$
|
||||||
|
|
||||||
|
$$\text{松弛度} = \text{必须完成时间} - \text{本身运行时间} - \text{当前时间}$$
|
||||||
|
|
||||||
|
$$\text{时间片开销比} = \frac{\text{进程数} \times \text{切换时间}}{\text{进程数} \times \text{时间片}} = \frac{\text{切换时间}}{\text{时间片}}$$
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十四、常见考点
|
||||||
|
|
||||||
|
1. **FCFS/SJF/RR 手工计算**:给定进程到达时间和运行时间,画 Gantt 图,计算平均等待时间和周转时间
|
||||||
|
2. **时间片大小对 RR 的影响**:太大退化为 FCFS,太小切换开销大
|
||||||
|
3. **响应比计算**:HRRF 的每一步调度决策
|
||||||
|
4. **优先级倒置场景分析**及优先级继承原理
|
||||||
|
5. **CFS 核心思想**:vruntime 最小优先
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**上一章**:[[06_进程控制]]
|
||||||
|
**下一章**:[[12_死锁]]
|
||||||
376
操作系统/12_死锁/12_死锁.md
Normal file
376
操作系统/12_死锁/12_死锁.md
Normal file
@@ -0,0 +1,376 @@
|
|||||||
|
# 第12讲:死锁
|
||||||
|
|
||||||
|
> 🎯 **本节目标**:理解死锁的概念,掌握死锁的预防、避免和检测方法
|
||||||
|
|
||||||
|
## 📋 前置知识
|
||||||
|
- [[07_多线程编程]] — 互斥锁和信号量
|
||||||
|
- [[11_处理机调度]] — 调度的基本概念
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤔 为什么需要这个?
|
||||||
|
|
||||||
|
想象这样一个场景:
|
||||||
|
- 线程 A 持有锁 1,等待锁 2
|
||||||
|
- 线程 B 持有锁 2,等待锁 1
|
||||||
|
- 两个线程互相等待,永远无法继续
|
||||||
|
|
||||||
|
这就是**死锁**——多个进程互相等待对方释放资源,导致所有进程都无法继续执行。
|
||||||
|
|
||||||
|
**生活比喻**:
|
||||||
|
- **死锁** = 两个人在狭窄的走廊相遇,谁都不肯让路,结果谁也过不去
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 核心概念
|
||||||
|
|
||||||
|
### 1. 死锁的四个必要条件
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[死锁四个条件] --> B[互斥条件]
|
||||||
|
A --> C[持有并等待]
|
||||||
|
A --> D[不可抢占]
|
||||||
|
A --> E[循环等待]
|
||||||
|
|
||||||
|
B --> B1[资源一次只能被一个进程使用]
|
||||||
|
C --> C1[进程持有资源的同时请求新资源]
|
||||||
|
D --> D1[资源不能被强制剥夺]
|
||||||
|
E --> E1[存在进程的循环等待链]
|
||||||
|
|
||||||
|
style A fill:#ffcdd2
|
||||||
|
```
|
||||||
|
|
||||||
|
**必须同时满足这四个条件才会发生死锁**。
|
||||||
|
|
||||||
|
### 2. 资源分配图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph 进程
|
||||||
|
P1[P1]
|
||||||
|
P2[P2]
|
||||||
|
end
|
||||||
|
subgraph 资源
|
||||||
|
R1[R1]
|
||||||
|
R2[R2]
|
||||||
|
end
|
||||||
|
|
||||||
|
R1 -->|分配| P1
|
||||||
|
R2 -->|分配| P2
|
||||||
|
P1 -->|请求| R2
|
||||||
|
P2 -->|请求| R1
|
||||||
|
|
||||||
|
style P1 fill:#e1f5fe
|
||||||
|
style P2 fill:#e1f5fe
|
||||||
|
style R1 fill:#e8f5e9
|
||||||
|
style R2 fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**图例**:
|
||||||
|
- 方框表示资源,圆圈表示进程
|
||||||
|
- 资源→进程:已分配
|
||||||
|
- 进程→资源:请求中
|
||||||
|
|
||||||
|
### 3. 死锁的处理策略
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[死锁处理策略] --> B[死锁预防]
|
||||||
|
A --> C[死锁避免]
|
||||||
|
A --> D[死锁检测]
|
||||||
|
A --> E[死锁恢复]
|
||||||
|
|
||||||
|
B --> B1[破坏四个条件之一]
|
||||||
|
C --> C1[银行家算法]
|
||||||
|
D --> D1[资源分配图]
|
||||||
|
E --> E1[终止进程]
|
||||||
|
|
||||||
|
style B fill:#e8f5e9
|
||||||
|
style C fill:#e1f5fe
|
||||||
|
style D fill:#fff3e0
|
||||||
|
style E fill:#ffcdd2
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 死锁预防
|
||||||
|
|
||||||
|
破坏死锁的四个必要条件之一:
|
||||||
|
|
||||||
|
| 条件 | 破坏方法 | 代价 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 互斥 | 使用可共享资源 | 不总是可行 |
|
||||||
|
| 持有并等待 | 一次性申请所有资源 | 资源浪费 |
|
||||||
|
| 不可抢占 | 允许抢占资源 | 实现复杂 |
|
||||||
|
| 循环等待 | 按顺序申请资源 | 限制灵活性 |
|
||||||
|
|
||||||
|
**资源有序分配法**:
|
||||||
|
```c
|
||||||
|
// 规定所有进程必须按编号顺序申请资源
|
||||||
|
// 例如:先申请锁1,再申请锁2
|
||||||
|
|
||||||
|
pthread_mutex_lock(&mutex1); // 正确
|
||||||
|
pthread_mutex_lock(&mutex2);
|
||||||
|
|
||||||
|
// 而不是
|
||||||
|
pthread_mutex_lock(&mutex2); // 可能导致死锁
|
||||||
|
pthread_mutex_lock(&mutex1);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. 银行家算法
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[银行家算法] --> B[检查请求是否安全]
|
||||||
|
B --> C{安全?}
|
||||||
|
C -->|是| D[分配资源]
|
||||||
|
C -->|否| E[等待]
|
||||||
|
D --> F[更新数据结构]
|
||||||
|
E --> G[阻塞进程]
|
||||||
|
|
||||||
|
style C fill:#fff3e0
|
||||||
|
style D fill:#e8f5e9
|
||||||
|
style E fill:#ffcdd2
|
||||||
|
```
|
||||||
|
|
||||||
|
**安全状态**:存在一个安全序列,使得所有进程都能顺利完成
|
||||||
|
|
||||||
|
**数据结构**:
|
||||||
|
- `Available[]`:可用资源向量
|
||||||
|
- `Max[][]`:最大需求矩阵
|
||||||
|
- `Allocation[][]`:已分配矩阵
|
||||||
|
- `Need[][]`:还需要的资源矩阵
|
||||||
|
|
||||||
|
**算法步骤**:
|
||||||
|
1. 检查请求是否超过需要
|
||||||
|
2. 检查请求是否超过可用资源
|
||||||
|
3. 尝试分配,检查是否安全
|
||||||
|
4. 如果安全,正式分配;否则等待
|
||||||
|
|
||||||
|
### 6. 死锁检测
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[构建资源分配图] --> B[寻找环路]
|
||||||
|
B --> C{有环路?}
|
||||||
|
C -->|是| D[可能存在死锁]
|
||||||
|
C -->|否| E[无死锁]
|
||||||
|
D --> F[进一步分析]
|
||||||
|
|
||||||
|
style C fill:#fff3e0
|
||||||
|
style D fill:#ffcdd2
|
||||||
|
style E fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**检测算法**:
|
||||||
|
1. 构建资源分配图
|
||||||
|
2. 使用深度优先搜索寻找环路
|
||||||
|
3. 如果存在环路,可能存在死锁
|
||||||
|
|
||||||
|
### 7. 死锁恢复
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[检测到死锁] --> B[选择终止进程]
|
||||||
|
B --> C[回滚操作]
|
||||||
|
C --> D[释放资源]
|
||||||
|
D --> E[唤醒等待进程]
|
||||||
|
|
||||||
|
style A fill:#ffcdd2
|
||||||
|
style E fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
**恢复方法**:
|
||||||
|
- **终止所有死锁进程**:简单但代价大
|
||||||
|
- **逐个终止进程**:直到死锁解除
|
||||||
|
- **资源抢占**:强制剥夺资源
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 动手实践
|
||||||
|
|
||||||
|
### 示例1:死锁演示
|
||||||
|
|
||||||
|
```c
|
||||||
|
// deadlock_demo.c - 死锁演示
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
|
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
|
void *thread1(void *arg) {
|
||||||
|
pthread_mutex_lock(&mutex1); // 获取锁1
|
||||||
|
printf("Thread 1: 持有锁1,等待锁2...\n");
|
||||||
|
sleep(1); // 等待,让线程2获取锁2
|
||||||
|
pthread_mutex_lock(&mutex2); // 等待锁2(死锁!)
|
||||||
|
printf("Thread 1: 获取到锁2\n");
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&mutex2);
|
||||||
|
pthread_mutex_unlock(&mutex1);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *thread2(void *arg) {
|
||||||
|
pthread_mutex_lock(&mutex2); // 获取锁2
|
||||||
|
printf("Thread 2: 持有锁2,等待锁1...\n");
|
||||||
|
sleep(1); // 等待,让线程1获取锁1
|
||||||
|
pthread_mutex_lock(&mutex1); // 等待锁1(死锁!)
|
||||||
|
printf("Thread 2: 获取到锁1\n");
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&mutex1);
|
||||||
|
pthread_mutex_unlock(&mutex2);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
pthread_t tid1, tid2;
|
||||||
|
|
||||||
|
pthread_create(&tid1, NULL, thread1, NULL);
|
||||||
|
pthread_create(&tid2, NULL, thread2, NULL);
|
||||||
|
|
||||||
|
pthread_join(tid1, NULL);
|
||||||
|
pthread_join(tid2, NULL);
|
||||||
|
|
||||||
|
printf("程序正常结束(如果这里能打印说明没有死锁)\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
gcc -o deadlock deadlock_demo.c -lpthread
|
||||||
|
./deadlock
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期结果**:程序会卡住,无法正常结束(死锁)
|
||||||
|
|
||||||
|
### 示例2:避免死锁
|
||||||
|
|
||||||
|
```c
|
||||||
|
// no_deadlock.c - 避免死锁(按顺序获取锁)
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
|
||||||
|
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
|
void *thread1(void *arg) {
|
||||||
|
pthread_mutex_lock(&mutex1); // 先获取锁1
|
||||||
|
printf("Thread 1: 持有锁1\n");
|
||||||
|
sleep(1);
|
||||||
|
pthread_mutex_lock(&mutex2); // 再获取锁2
|
||||||
|
printf("Thread 1: 持有锁1和锁2\n");
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&mutex2);
|
||||||
|
pthread_mutex_unlock(&mutex1);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *thread2(void *arg) {
|
||||||
|
pthread_mutex_lock(&mutex1); // 也先获取锁1(顺序一致)
|
||||||
|
printf("Thread 2: 持有锁1\n");
|
||||||
|
sleep(1);
|
||||||
|
pthread_mutex_lock(&mutex2); // 再获取锁2
|
||||||
|
printf("Thread 2: 持有锁1和锁2\n");
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&mutex2);
|
||||||
|
pthread_mutex_unlock(&mutex1);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
pthread_t tid1, tid2;
|
||||||
|
|
||||||
|
pthread_create(&tid1, NULL, thread1, NULL);
|
||||||
|
pthread_create(&tid2, NULL, thread2, NULL);
|
||||||
|
|
||||||
|
pthread_join(tid1, NULL);
|
||||||
|
pthread_join(tid2, NULL);
|
||||||
|
|
||||||
|
printf("程序正常结束\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例3:银行家算法模拟
|
||||||
|
|
||||||
|
```python
|
||||||
|
# banker.py - 银行家算法模拟
|
||||||
|
def is_safe(available, max_need, allocation):
|
||||||
|
"""检查系统是否处于安全状态"""
|
||||||
|
n = len(allocation) # 进程数
|
||||||
|
m = len(available) # 资源类型数
|
||||||
|
|
||||||
|
# 计算 Need 矩阵
|
||||||
|
need = [[max_need[i][j] - allocation[i][j] for j in range(m)] for i in range(n)]
|
||||||
|
|
||||||
|
# 初始化工作向量和完成标志
|
||||||
|
work = available.copy()
|
||||||
|
finish = [False] * n
|
||||||
|
safe_seq = []
|
||||||
|
|
||||||
|
while len(safe_seq) < n:
|
||||||
|
found = False
|
||||||
|
for i in range(n):
|
||||||
|
if not finish[i]:
|
||||||
|
# 检查是否可以分配
|
||||||
|
if all(need[i][j] <= work[j] for j in range(m)):
|
||||||
|
# 模拟分配
|
||||||
|
for j in range(m):
|
||||||
|
work[j] += allocation[i][j]
|
||||||
|
finish[i] = True
|
||||||
|
safe_seq.append(i)
|
||||||
|
found = True
|
||||||
|
|
||||||
|
if not found:
|
||||||
|
return False, [] # 不安全
|
||||||
|
|
||||||
|
return True, safe_seq
|
||||||
|
|
||||||
|
# 测试数据
|
||||||
|
available = [3, 3, 2]
|
||||||
|
max_need = [
|
||||||
|
[7, 5, 3],
|
||||||
|
[3, 2, 2],
|
||||||
|
[9, 0, 2],
|
||||||
|
[2, 2, 2],
|
||||||
|
[4, 3, 3]
|
||||||
|
]
|
||||||
|
allocation = [
|
||||||
|
[0, 1, 0],
|
||||||
|
[2, 0, 0],
|
||||||
|
[3, 0, 2],
|
||||||
|
[2, 1, 1],
|
||||||
|
[0, 0, 2]
|
||||||
|
]
|
||||||
|
|
||||||
|
safe, seq = is_safe(available, max_need, allocation)
|
||||||
|
if safe:
|
||||||
|
print(f"系统安全,安全序列: {seq}")
|
||||||
|
else:
|
||||||
|
print("系统不安全")
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 知识关联
|
||||||
|
- 互斥锁在 [[07_多线程编程]] 中有详细讲解
|
||||||
|
- 资源分配在 [[05_磁盘空间管理]] 中有类似概念
|
||||||
|
- 银行家算法在 [[14_分页存储管理]] 中的页面置换有类似思想
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 思考题
|
||||||
|
|
||||||
|
1. **为什么需要同时满足四个条件?** 如果只有三个条件满足会怎样?
|
||||||
|
2. **银行家算法的局限性**:为什么它在实际系统中很少使用?
|
||||||
|
3. **鸵鸟策略**:为什么有些系统选择忽略死锁问题?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 扩展阅读
|
||||||
|
- 《操作系统概念》第7章:死锁
|
||||||
|
- 《现代操作系统》第6章:死锁
|
||||||
|
- [死锁检测算法](https://www.geeksforgeeks.org/deadlock-detection-algorithm/)
|
||||||
303
操作系统/13_存储管理基础/13_存储管理基础.md
Normal file
303
操作系统/13_存储管理基础/13_存储管理基础.md
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# 13. 存储管理基础
|
||||||
|
|
||||||
|
> **课程**: 操作系统 - 存储器管理
|
||||||
|
> **核心内容**: 存储器层次结构、存储管理功能、程序编译链接与装入、地址空间
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、存储器层次结构
|
||||||
|
|
||||||
|
存储器按照速度和容量形成层次结构,越靠近CPU速度越快但容量越小、价格越高。
|
||||||
|
|
||||||
|
```
|
||||||
|
速度递增 ↑ 容量递减 ↑ 价格递增 ↑
|
||||||
|
┌─────────────────────────┐
|
||||||
|
│ 寄存器(Register) │ ← 最快,纳秒级,几十~几百字节
|
||||||
|
├─────────────────────────┤
|
||||||
|
│ 高速缓存(Cache) │ ← L1/L2/L3,几MB
|
||||||
|
├─────────────────────────┤
|
||||||
|
│ 主存(内存/DRAM) │ ← 几GB~几百GB,百纳秒级
|
||||||
|
├─────────────────────────┤
|
||||||
|
│ Flash/SSD │ ← 固态存储
|
||||||
|
├─────────────────────────┤
|
||||||
|
│ 磁盘缓存(Disk Cache) │
|
||||||
|
├─────────────────────────┤
|
||||||
|
│ 固定磁盘(HDD/SSD) │ ← 几百GB~几TB,毫秒级
|
||||||
|
├─────────────────────────┤
|
||||||
|
│ 可移动存储(U盘/光盘/磁带) │ ← 最慢,容量可很大
|
||||||
|
└─────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**设计原则**: 利用**局部性原理**,将频繁访问的数据放在高速层次,较少访问的数据放在低速大容量层次。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、存储管理的功能
|
||||||
|
|
||||||
|
操作系统存储管理需要实现以下五大核心功能:
|
||||||
|
|
||||||
|
| 功能 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **内存分配与回收** | 为进程分配所需内存,进程结束后回收内存 |
|
||||||
|
| **地址转换** | 将程序中的逻辑地址转换为物理地址([[14_分页存储管理\|分页]]、[[15_段式存储管理\|分段]]) |
|
||||||
|
| **内存共享** | 多个进程共享同一段代码(如共享库) |
|
||||||
|
| **内存保护** | 防止进程越界访问其他进程或内核的内存区域 |
|
||||||
|
| **内存扩充** | 通过虚拟存储技术,使程序可用空间大于实际物理内存([[16_虚拟存储器\|虚拟存储器]]) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、程序的编译与链接过程
|
||||||
|
|
||||||
|
一个C语言源文件从编写到可执行,经历以下阶段:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
A["hello.c<br/>源文件"] -->|预处理| B["hello.i<br/>预处理后"]
|
||||||
|
B -->|编译| C["hello.s<br/>汇编文件"]
|
||||||
|
C -->|汇编| D["hello.o<br/>目标文件(可重定位)"]
|
||||||
|
D -->|链接| E["hello / a.out<br/>可执行文件"]
|
||||||
|
E -->|装入| F["内存中运行的进程"]
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style C fill:#fff3e0
|
||||||
|
style D fill:#fce4ec
|
||||||
|
style E fill:#e8f5e9
|
||||||
|
style F fill:#f3e5f5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 各阶段说明
|
||||||
|
|
||||||
|
| 阶段 | 输入 | 输出 | 工具 | 说明 |
|
||||||
|
|------|------|------|------|------|
|
||||||
|
| **预处理** | `.c` | `.i` | cpp | 展开宏、头文件、条件编译 |
|
||||||
|
| **编译** | `.i` | `.s` | cc1 | 翻译为汇编语言 |
|
||||||
|
| **汇编** | `.s` | `.o` | as | 翻译为机器指令(可重定位目标文件) |
|
||||||
|
| **链接** | `.o` | 可执行文件 | ld | 合并节段、解析符号引用、重定位 |
|
||||||
|
| **装入** | 可执行文件 | 进程 | 加载器 | 将程序载入内存并创建进程 |
|
||||||
|
|
||||||
|
### 查看目标文件的节段
|
||||||
|
|
||||||
|
使用 `objdump -h` 可以查看可执行文件或目标文件中的节段(Section)信息:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gcc -c hello.c # 编译为可重定位目标文件
|
||||||
|
objdump -h hello.o # 查看节段头信息
|
||||||
|
|
||||||
|
gcc hello.c -o hello # 链接为可执行文件
|
||||||
|
objdump -h hello # 查看可执行文件的节段
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、可执行文件结构
|
||||||
|
|
||||||
|
可执行文件在内存中从低地址到高地址的典型布局如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
高地址 ┌──────────────────┐
|
||||||
|
│ 命令行参数 │
|
||||||
|
│ 和环境变量 │
|
||||||
|
├──────────────────┤
|
||||||
|
│ 栈(Stack) │ ← 局部变量、函数调用帧,向下增长 ↓
|
||||||
|
│ ↓ │
|
||||||
|
│ │
|
||||||
|
│ ↑ │
|
||||||
|
│ 堆(Heap) │ ← malloc/new动态分配,向上增长 ↑
|
||||||
|
├──────────────────┤
|
||||||
|
│ .bss 段 │ ← 未初始化的全局/静态变量(不占文件空间)
|
||||||
|
├──────────────────┤
|
||||||
|
│ .data 段 │ ← 已初始化的全局变量和静态变量
|
||||||
|
├──────────────────┤
|
||||||
|
│ .rodata 段 │ ← 只读数据(如字符串常量)
|
||||||
|
├──────────────────┤
|
||||||
|
│ .text 段 │ ← 可执行代码(机器指令)
|
||||||
|
低地址 └──────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
| 段名 | 内容 | 是否可写 | 说明 |
|
||||||
|
|------|------|---------|------|
|
||||||
|
| `.text` | 机器指令代码 | 只读/只执行 | 程序的可执行代码 |
|
||||||
|
| `.rodata` | 只读数据 | 只读 | 字符串常量、`const`变量 |
|
||||||
|
| `.data` | 已初始化全局变量 | 可读写 | 有初始值的全局和静态变量 |
|
||||||
|
| `.bss` | 未初始化全局变量 | 可读写 | 不占文件空间,装入时清零 |
|
||||||
|
| 堆(Heap) | 动态分配内存 | 可读写 | `malloc`/`new` 分配 |
|
||||||
|
| 栈(Stack) | 函数调用帧 | 可读写 | 局部变量、返回地址等 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、逻辑地址与物理地址
|
||||||
|
|
||||||
|
### 核心概念
|
||||||
|
|
||||||
|
| 术语 | 别名 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **逻辑地址** | 虚拟地址(VA)、相对地址 | 程序中使用的地址,从0开始编址 |
|
||||||
|
| **物理地址** | PA、绝对地址 | 内存硬件中的实际地址 |
|
||||||
|
|
||||||
|
> **关键**: 程序中的地址(逻辑地址)**不等于**内存中的实际地址(物理地址)。地址转换由硬件([[01_系统运行机制#四、存储器管理硬件——MMU|MMU]])和操作系统配合完成。
|
||||||
|
|
||||||
|
### 逻辑地址空间与物理地址空间
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph VA["逻辑地址空间 (虚拟)"]
|
||||||
|
direction TB
|
||||||
|
V0["地址 0"]
|
||||||
|
V1["..."]
|
||||||
|
V2["地址 2^v - 1"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph PA["物理地址空间 (实际)"]
|
||||||
|
direction TB
|
||||||
|
P0["地址 0"]
|
||||||
|
P1["..."]
|
||||||
|
P2["地址 2^p - 1"]
|
||||||
|
end
|
||||||
|
|
||||||
|
VA -->|"地址映射<br/>(页表/段表)"| PA
|
||||||
|
|
||||||
|
style VA fill:#e3f2fd,stroke:#1976d2
|
||||||
|
style PA fill:#fff8e1,stroke:#f9a825
|
||||||
|
```
|
||||||
|
|
||||||
|
- **虚拟地址空间**: 大小为 $2^v$ 字节,其中 $v$ 是虚拟地址的位数
|
||||||
|
- **物理地址空间**: 大小为 $2^p$ 字节,其中 $p$ 是物理地址的位数
|
||||||
|
- 通常 $v \geq p$(虚拟地址空间可以大于物理内存),这就是[[16_虚拟存储器|虚拟存储器]]的基础
|
||||||
|
|
||||||
|
### 引入逻辑地址的好处
|
||||||
|
|
||||||
|
1. **进程隔离**: 每个进程拥有独立的虚拟地址空间,互不干扰
|
||||||
|
2. **提高内存利用率**: 可以使用[[16_虚拟存储器|虚拟存储器]]技术
|
||||||
|
3. **内存保护**: 通过地址转换实现访问权限控制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、内核空间与用户空间
|
||||||
|
|
||||||
|
操作系统将每个进程的虚拟地址空间划分为**用户空间**和**内核空间**两部分:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
block-beta
|
||||||
|
columns 1
|
||||||
|
block:linux["Linux 进程地址空间 (4GB)"]
|
||||||
|
columns 1
|
||||||
|
block:kernel_linux["内核空间 (高 1GB)"]
|
||||||
|
k1["内核代码、数据、内核栈等"]
|
||||||
|
end
|
||||||
|
block:user_linux["用户空间 (低 3GB)"]
|
||||||
|
u1["栈 ↓"]
|
||||||
|
u2["..."]
|
||||||
|
u3["堆 ↑"]
|
||||||
|
u4[".bss / .data / .rodata / .text"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
block:win["Windows 进程地址空间 (4GB)"]
|
||||||
|
columns 1
|
||||||
|
block:kernel_win["内核空间 (高 2GB)"]
|
||||||
|
k2["内核代码、驱动等"]
|
||||||
|
end
|
||||||
|
block:user_win["用户空间 (低 2GB)"]
|
||||||
|
u5["用户程序空间"]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
| 操作系统 | 用户空间 | 内核空间 | 说明 |
|
||||||
|
|---------|---------|---------|------|
|
||||||
|
| **Linux (32位)** | 0 ~ 3GB (低3GB) | 3GB ~ 4GB (高1GB) | 通过`PAGE_OFFSET`划分 |
|
||||||
|
| **Windows (32位)** | 0 ~ 2GB (低2GB) | 2GB ~ 4GB (高2GB) | 可通过`/3GB`启动参数调整为3:1 |
|
||||||
|
|
||||||
|
**重要规则**: 用户态程序**不能**直接访问内核空间的地址,否则触发保护异常。内核态代码可以访问整个地址空间。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、MMU 与内存保护
|
||||||
|
|
||||||
|
**MMU (Memory Management Unit)** 是CPU中负责地址转换和内存保护的硬件单元。
|
||||||
|
|
||||||
|
### 地址转换流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
CPU["CPU 发出<br/>虚拟地址(VA)"] --> MMU["MMU<br/>地址转换"]
|
||||||
|
MMU --> PA["物理地址(PA)"]
|
||||||
|
PA --> MEM["访问内存"]
|
||||||
|
|
||||||
|
style MMU fill:#ffcdd2,stroke:#c62828
|
||||||
|
```
|
||||||
|
|
||||||
|
### 页表保护机制
|
||||||
|
|
||||||
|
MMU通过页表中的**U/S位**和CPU的**模式位**配合实现内存保护:
|
||||||
|
|
||||||
|
| 页表U/S位 | 含义 |
|
||||||
|
|-----------|------|
|
||||||
|
| **U=0 (Supervisor)** | 内核态页面,只有内核可以访问 |
|
||||||
|
| **U=1 (User)** | 用户态页面,用户态和内核态均可访问 |
|
||||||
|
|
||||||
|
| CPU模式 | 可访问的页面 |
|
||||||
|
|---------|-------------|
|
||||||
|
| **用户模式 (User mode)** | 只能访问 U=1 的页面 |
|
||||||
|
| **内核模式 (Kernel mode)** | 可以访问 U=0 和 U=1 的页面 |
|
||||||
|
|
||||||
|
当用户态程序试图访问 U=0 的内核页面时,MMU会产生**保护异常(段错误/Segmentation Fault)**,终止该进程。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、地址转换的主要方式
|
||||||
|
|
||||||
|
操作系统实现地址转换有多种方式,各有特点:
|
||||||
|
|
||||||
|
| 方式 | 原理 | 优点 | 缺点 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| **重定位寄存器(基址寄存器)** | PA = VA + 基址值 | 简单 | 程序必须连续存放 |
|
||||||
|
| **静态重定位** | 装入时一次性修改所有地址 | 无需硬件支持 | 装入后不能移动 |
|
||||||
|
| **动态重定位** | 执行时通过MMU实时转换 | 灵活,支持移动 | 需要硬件支持 |
|
||||||
|
| **[[14_分页存储管理\|分页]]** | 按页划分,通过页表映射 | 消除外部碎片 | 有内部碎片、页表开销 |
|
||||||
|
| **[[15_段式存储管理\|分段]]** | 按逻辑段划分,通过段表映射 | 符合程序逻辑 | 外部碎片问题 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、小结
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
mindmap
|
||||||
|
root((存储管理基础))
|
||||||
|
存储层次
|
||||||
|
寄存器
|
||||||
|
Cache
|
||||||
|
主存
|
||||||
|
Flash
|
||||||
|
磁盘
|
||||||
|
管理功能
|
||||||
|
分配回收
|
||||||
|
地址转换
|
||||||
|
内存共享
|
||||||
|
内存保护
|
||||||
|
内存扩充
|
||||||
|
程序装入
|
||||||
|
编译链接
|
||||||
|
可执行文件结构
|
||||||
|
逻辑地址vs物理地址
|
||||||
|
地址空间
|
||||||
|
虚拟地址空间
|
||||||
|
物理地址空间
|
||||||
|
内核空间/用户空间
|
||||||
|
MMU保护
|
||||||
|
页表U/S位
|
||||||
|
CPU模式位
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关联笔记
|
||||||
|
|
||||||
|
- [[01_系统运行机制]] — CPU工作模式(用户态/内核态)与MMU硬件基础
|
||||||
|
- [[14_分页存储管理]] — 分页式地址转换的详细实现
|
||||||
|
- [[15_段式存储管理]] — 分段式地址转换
|
||||||
|
- [[16_虚拟存储器]] — 虚拟存储器的实现原理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**上一讲**: [[01_系统运行机制]]
|
||||||
|
**下一讲**: [[14_分页存储管理]]
|
||||||
597
操作系统/14_分页存储管理/14_分页存储管理.md
Normal file
597
操作系统/14_分页存储管理/14_分页存储管理.md
Normal file
@@ -0,0 +1,597 @@
|
|||||||
|
# 14. 分页存储管理
|
||||||
|
|
||||||
|
> **课程**: 操作系统 - 存储器管理
|
||||||
|
> **核心内容**: 碎片问题、分页思想、地址结构、页表、地址变换、TLB快表、多级页表、倒转页表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前置知识
|
||||||
|
|
||||||
|
- [[13_存储管理基础]] — 存储器层次结构、逻辑地址与物理地址、MMU
|
||||||
|
- [[01_系统运行机制]] — CPU工作模式与硬件基础
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、碎片问题
|
||||||
|
|
||||||
|
在连续分配方式中,内存中会出现无法被利用的空闲区域,称为**碎片**。
|
||||||
|
|
||||||
|
| 碎片类型 | 出现位置 | 产生原因 | 对应分配方式 |
|
||||||
|
|---------|---------|---------|------------|
|
||||||
|
| **内碎片** (Internal Fragmentation) | 分配区域**内部** | 固定分区大小 > 进程实际需要 | 固定分区分配 |
|
||||||
|
| **外碎片** (External Fragmentation) | 分配区域**外部** | 空闲分区太小,无法满足任何请求 | 动态分区分配 |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph 内碎片示意
|
||||||
|
direction TB
|
||||||
|
A1["┌──────────────┐"]
|
||||||
|
A2["│ 进程实际数据 │"]
|
||||||
|
A3["│──────────────│"]
|
||||||
|
A4["│ 未使用空间 │ ← 内碎片"]
|
||||||
|
A5["└──────────────┘"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 外碎片示意
|
||||||
|
direction TB
|
||||||
|
B1["┌────┐ ┌────┐ ┌────┐ ┌────┐"]
|
||||||
|
B2["│进程A│ │空闲│ │进程B│ │空闲│"]
|
||||||
|
B3["└────┘ └────┘ └────┘ └────┘"]
|
||||||
|
B4[" ↑外碎片 ↑外碎片"]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
> **根本原因**: 无论是固定分区还是动态分区,都要求进程在内存中**连续存放**。要彻底解决碎片问题,必须放弃"连续"要求——这就是**分页**思想的核心出发点。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、分页的基本思想
|
||||||
|
|
||||||
|
分页存储管理将**虚拟地址空间**和**物理内存**都划分为大小相等的小块:
|
||||||
|
|
||||||
|
- 虚拟地址空间的每一块称为**虚拟页 (Virtual Page, VP)**
|
||||||
|
- 物理内存的每一块称为**物理页框 (Page Frame, PF)**
|
||||||
|
|
||||||
|
常见的页面大小为 $2^k$ 字节:512B、1KB、2KB、**4KB**(最常用)。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph VA["虚拟地址空间 (进程视角)"]
|
||||||
|
direction TB
|
||||||
|
VP0["虚拟页 VP0"]
|
||||||
|
VP1["虚拟页 VP1"]
|
||||||
|
VP2["虚拟页 VP2"]
|
||||||
|
VP3["虚拟页 VP3"]
|
||||||
|
VP4["..."]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph PM["物理内存"]
|
||||||
|
direction TB
|
||||||
|
PF0["物理页框 PF0"]
|
||||||
|
PF1["物理页框 PF1"]
|
||||||
|
PF2["物理页框 PF2"]
|
||||||
|
PF3["物理页框 PF3"]
|
||||||
|
PF4["..."]
|
||||||
|
end
|
||||||
|
|
||||||
|
VP0 -->|"页表映射"| PF2
|
||||||
|
VP1 -->|"页表映射"| PF0
|
||||||
|
VP2 -->|"不在内存"| DISK["外存(磁盘)"]
|
||||||
|
VP3 -->|"页表映射"| PF3
|
||||||
|
|
||||||
|
style VA fill:#e3f2fd,stroke:#1976d2
|
||||||
|
style PM fill:#fff8e1,stroke:#f9a825
|
||||||
|
style DISK fill:#fce4ec,stroke:#c62828
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键特性**:
|
||||||
|
- 虚拟页在物理内存中**不需要连续**存放
|
||||||
|
- 每个虚拟页可以映射到任意一个空闲物理页框
|
||||||
|
- 消除了外碎片(但每个页内可能有少量内碎片)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、地址结构
|
||||||
|
|
||||||
|
在分页系统中,虚拟地址被划分为两部分:
|
||||||
|
|
||||||
|
```
|
||||||
|
虚拟地址 VA (v+k 位)
|
||||||
|
┌─────────────────┬─────────────┐
|
||||||
|
│ VPN (v位) │ VPO (k位) │
|
||||||
|
│ 虚拟页号 │ 页内偏移 │
|
||||||
|
└─────────────────┴─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**计算公式**:
|
||||||
|
- 页大小 = $2^k$ 字节
|
||||||
|
- 虚拟页号 VPN = $\lfloor VA / 2^k \rfloor$(即高位部分)
|
||||||
|
- 页内偏移 VPO = $VA \mod 2^k$(即低 $k$ 位)
|
||||||
|
|
||||||
|
物理地址同理:
|
||||||
|
|
||||||
|
```
|
||||||
|
物理地址 PA (p+k 位)
|
||||||
|
┌─────────────────┬─────────────┐
|
||||||
|
│ PPN (p位) │ PPO (k位) │
|
||||||
|
│ 物理页号 │ 页内偏移 │
|
||||||
|
└─────────────────┴─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- 物理页号 PPN 由页表查得
|
||||||
|
- **PPO = VPO**(页内偏移不变,直接复制)
|
||||||
|
|
||||||
|
### 地址计算示例
|
||||||
|
|
||||||
|
> **例题**: 某系统虚拟地址 16 位,页面大小 4KB ($2^{12}$),求虚拟地址 VA = 0x3A6F 对应的 VPN 和 VPO。
|
||||||
|
|
||||||
|
- 页大小 $2^k = 2^{12}$,所以 $k = 12$,$v = 16 - 12 = 4$
|
||||||
|
- VPN = $0x3A6F / 0x1000 = 0x3$(高4位:0011)
|
||||||
|
- VPO = $0x3A6F \mod 0x1000 = 0xA6F$(低12位)
|
||||||
|
|
||||||
|
```
|
||||||
|
VA = 0x3A6F = 0011 1010 0110 1111
|
||||||
|
──── ────────────────
|
||||||
|
VPN VPO
|
||||||
|
(0x3) (0xA6F)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、页表
|
||||||
|
|
||||||
|
页表是实现虚拟页到物理页框映射的核心数据结构。
|
||||||
|
|
||||||
|
### 页表结构
|
||||||
|
|
||||||
|
每个进程拥有**独立的页表**。页表以 VPN 为索引,每个页表项 (PTE) 包含:
|
||||||
|
|
||||||
|
```
|
||||||
|
页表 (以VPN为索引)
|
||||||
|
┌──────┬───────┬──────────────────────────────┐
|
||||||
|
│ VPN │ PPN │ 控制位 │
|
||||||
|
├──────┼───────┼──────────────────────────────┤
|
||||||
|
│ 0 │ 5 │ V=1 D=0 R=1 W=1 U/S=1 │
|
||||||
|
│ 1 │ 2 │ V=1 D=1 R=1 W=1 U/S=1 │
|
||||||
|
│ 2 │ --- │ V=0 (不在内存) │
|
||||||
|
│ 3 │ 8 │ V=1 D=0 R=1 W=0 U/S=1 │
|
||||||
|
│ ... │ ... │ ... │
|
||||||
|
└──────┴───────┴──────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 页表项 (PTE) 各字段
|
||||||
|
|
||||||
|
| 字段 | 含义 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **有效位 (Valid/Present)** | 页面是否在内存中 | V=1:在内存;V=0:不在内存(缺页) |
|
||||||
|
| **修改位 (Dirty)** | 页面是否被写过 | D=1:被修改过,换出时需写回外存 |
|
||||||
|
| **引用位 (Reference)** | 页面是否被访问过 | 用于页面置换算法(如Clock、LRU近似) |
|
||||||
|
| **读/写权限 (R/W)** | 读写保护 | 只读页被写入时触发保护异常 |
|
||||||
|
| **用户/内核 (U/S)** | 访问权限 | U=0:仅内核可访问;U=1:用户可访问 |
|
||||||
|
| **物理页号 (PPN)** | 对应的物理页框号 | 与VPO拼接得到物理地址 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、地址变换过程
|
||||||
|
|
||||||
|
### 基本地址变换流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["CPU发出虚拟地址 VA"] --> B["从VA中提取VPN和VPO"]
|
||||||
|
B --> C{"TLB查找VPN"}
|
||||||
|
C -->|"TLB命中"| D["直接取出PPN"]
|
||||||
|
C -->|"TLB未命中"| E["访问页表<br/>(页表基址寄存器PTBR + VPN × PTE大小)"]
|
||||||
|
E --> F{"页表项有效位?"}
|
||||||
|
F -->|"V=1"| G["取出PPN"]
|
||||||
|
F -->|"V=0"| H["**缺页中断**<br/>操作系统处理"]
|
||||||
|
H --> I["从外存调入页面"]
|
||||||
|
I --> J["更新页表"]
|
||||||
|
J --> G
|
||||||
|
D --> K["物理地址 PA = PPN × 2^k + PPO"]
|
||||||
|
G --> K
|
||||||
|
K --> L["访问物理内存"]
|
||||||
|
|
||||||
|
style H fill:#ffcdd2,stroke:#c62828
|
||||||
|
style C fill:#e8f5e9,stroke:#2e7d32
|
||||||
|
```
|
||||||
|
|
||||||
|
### 详细步骤
|
||||||
|
|
||||||
|
1. **提取地址字段**: 从虚拟地址 VA 中分离出 VPN 和 VPO
|
||||||
|
2. **查TLB**: 用 VPN 在 TLB 中查找(见第六节)
|
||||||
|
3. **查页表**: 若 TLB 未命中,用 **PTBR(页表基址寄存器)+ VPN** 定位页表项
|
||||||
|
4. **检查有效位**:
|
||||||
|
- V=1:取出 PPN,与 PPO 拼接得到物理地址
|
||||||
|
- V=0:触发**缺页中断**,OS 从外存调入页面
|
||||||
|
5. **拼接物理地址**: PA = PPN | PPO(将 PPN 放高位,PPO 放低位)
|
||||||
|
|
||||||
|
### 缺页中断处理流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["发生缺页中断"] --> B["保存CPU现场"]
|
||||||
|
B --> C{"外存中找到该页?"}
|
||||||
|
C -->|"找到"| D{"内存有空闲页框?"}
|
||||||
|
C -->|"未找到"| E["终止进程<br/>(非法访问)"]
|
||||||
|
D -->|"有空闲"| F["从外存读入该页"]
|
||||||
|
D -->|"无空闲"| G["执行页面置换算法<br/>选择牺牲页"]
|
||||||
|
G --> H{"牺牲页Dirty=1?"}
|
||||||
|
H -->|"是"| I["将牺牲页写回外存"]
|
||||||
|
H -->|"否"| J["直接覆盖"]
|
||||||
|
I --> F
|
||||||
|
J --> F
|
||||||
|
F --> K["修改页表<br/>设置V=1, PPN"]
|
||||||
|
K --> L["重新执行被中断的指令"]
|
||||||
|
```
|
||||||
|
|
||||||
|
### 地址变换计算示例
|
||||||
|
|
||||||
|
> **例题**: 某系统页面大小 1KB ($2^{10}$),虚拟地址 14 位。页表如下,求虚拟地址 VA=0x1A8F 对应的物理地址。
|
||||||
|
|
||||||
|
| VPN | PPN | Valid |
|
||||||
|
|-----|-----|-------|
|
||||||
|
| 0 | 3 | 1 |
|
||||||
|
| 1 | 7 | 1 |
|
||||||
|
| 2 | --- | 0 |
|
||||||
|
| 3 | 5 | 1 |
|
||||||
|
| 4 | 2 | 1 |
|
||||||
|
| 5 | 8 | 1 |
|
||||||
|
| 6 | 1 | 1 |
|
||||||
|
|
||||||
|
**解题过程**:
|
||||||
|
|
||||||
|
1. 页面大小 $2^{10}$,所以 $k=10$,$v=14-10=4$
|
||||||
|
2. VA = 0x1A8F = **01 1010 1000 1111** (二进制)
|
||||||
|
- VPN = 高4位 = 0110 = **6**
|
||||||
|
- VPO = 低10位 = 10 1000 1111 = 0x28F
|
||||||
|
3. 查页表:VPN=6 对应 PPN=**1**,Valid=1
|
||||||
|
4. PA = PPN | PPO = 1 × 2^{10} + 0x28F = 0x400 + 0x28F = **0x68F**
|
||||||
|
|
||||||
|
```
|
||||||
|
VA = 0x1A8F: 0110 1010001111
|
||||||
|
VPN=6 VPO=0x28F
|
||||||
|
|
||||||
|
查页表: VPN=6 → PPN=1
|
||||||
|
|
||||||
|
PA = 0001 1010001111 = 0x068F
|
||||||
|
PPN=1 PPO=0x28F
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、TLB 快表
|
||||||
|
|
||||||
|
### TLB 概述
|
||||||
|
|
||||||
|
**TLB (Translation Lookaside Buffer)** 是集成在 MMU 中的高速缓存,存储最近使用的页表项。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
CPU["CPU"] -->|"虚拟地址"| TLB{"TLB<br/>(快表)"}
|
||||||
|
TLB -->|"命中<br/>取出PPN"| PA["物理地址"]
|
||||||
|
TLB -->|"未命中"| PT["查页表<br/>(内存)"]
|
||||||
|
PT -->|"PPN"| PA
|
||||||
|
PT -->|"更新TLB"| TLB
|
||||||
|
|
||||||
|
style TLB fill:#e8f5e9,stroke:#2e7d32
|
||||||
|
style PT fill:#fff3e0,stroke:#e65100
|
||||||
|
```
|
||||||
|
|
||||||
|
### TLB 组织方式
|
||||||
|
|
||||||
|
| 方式 | 说明 | 特点 |
|
||||||
|
|------|------|------|
|
||||||
|
| **全相联** | VPN可以放在TLB的任意位置 | 灵活但查找慢,适合小容量TLB |
|
||||||
|
| **组相联** | VPN映射到固定的组(set),组内任意放置 | 折中方案,最常用 |
|
||||||
|
| **直接映射** | VPN映射到固定的TLB位置 | 最快但冲突多 |
|
||||||
|
|
||||||
|
### TLB 性能分析
|
||||||
|
|
||||||
|
设 TLB 查找时间为 $\lambda$,内存访问时间为 $t$,TLB 命中率为 $a$:
|
||||||
|
|
||||||
|
**无 TLB 时**:每次地址变换需要访问一次页表(内存)+ 一次数据访问(内存)
|
||||||
|
|
||||||
|
$$EAT_{无TLB} = t + t = 2t$$
|
||||||
|
|
||||||
|
**有 TLB 时**:
|
||||||
|
|
||||||
|
$$EAT = a(\lambda + t) + (1-a)(\lambda + t + t) = \lambda + t + (1-a) \cdot t$$
|
||||||
|
|
||||||
|
> **计算示例**: 设 $\lambda = 10$ns, $t = 100$ns, $a = 0.98$(98%命中率)
|
||||||
|
>
|
||||||
|
> $EAT = 10 + 100 + (1 - 0.98) \times 100 = 10 + 100 + 2 = 112$ ns
|
||||||
|
>
|
||||||
|
> 相比无TLB的 $2t = 200$ ns,性能提升了约 **44%**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、多级页表
|
||||||
|
|
||||||
|
### 为什么需要多级页表
|
||||||
|
|
||||||
|
对于 32 位系统,页面大小 4KB:
|
||||||
|
- 虚拟地址空间 = $2^{32}$ = 4GB
|
||||||
|
- 页数 = $2^{32} / 2^{12} = 2^{20}$ = 1M 个页
|
||||||
|
- 每个页表项 4 字节 → 页表大小 = 1M × 4B = **4MB**
|
||||||
|
|
||||||
|
4MB 的页表对于每个进程都太大了!而且页表必须连续存放。
|
||||||
|
|
||||||
|
### 二级页表结构
|
||||||
|
|
||||||
|
**核心思想**: 将页表本身也分页,用"页目录"来索引这些页表页。
|
||||||
|
|
||||||
|
```
|
||||||
|
32位虚拟地址 (二级页表)
|
||||||
|
┌──────────────┬──────────────┬──────────────┐
|
||||||
|
│ 页目录索引(10位)│ 页表索引(10位) │ 页内偏移(12位)│
|
||||||
|
└──────────────┴──────────────┴──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
CR3["CR3<br/>(页目录基址)"] --> PD["页目录<br/>(1024项)"]
|
||||||
|
PD -->|"页目录项"| PT1["页表1<br/>(1024项)"]
|
||||||
|
PD -->|"页目录项"| PT2["页表2<br/>(1024项)"]
|
||||||
|
PD -->|"页目录项"| PT3["页表3<br/>(1024项)"]
|
||||||
|
PT1 -->|"页表项+偏移"| PF1["物理页框"]
|
||||||
|
PT2 -->|"页表项+偏移"| PF2["物理页框"]
|
||||||
|
PT3 -->|"页表项+偏移"| PF3["物理页框"]
|
||||||
|
|
||||||
|
style PD fill:#e3f2fd,stroke:#1976d2
|
||||||
|
style PT1 fill:#fff3e0,stroke:#e65100
|
||||||
|
style PT2 fill:#fff3e0,stroke:#e65100
|
||||||
|
style PT3 fill:#fff3e0,stroke:#e65100
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多级页表的优势
|
||||||
|
|
||||||
|
- 页目录只需常驻内存(4KB),页表页按需创建
|
||||||
|
- 未使用的虚拟地址区域**不需要分配页表页**,节省大量内存
|
||||||
|
- 64 位系统通常使用 **3~5 级页表**(如 Linux 的 4 级页表:PGD→PUD→PMD→PTE→偏移)
|
||||||
|
|
||||||
|
### 二级页表地址变换
|
||||||
|
|
||||||
|
1. 用 CR3 找到页目录基址
|
||||||
|
2. 用**页目录索引**在页目录中找到页表页的物理地址
|
||||||
|
3. 用**页表索引**在页表页中找到 PPN
|
||||||
|
4. PPN 与**页内偏移**拼接得到物理地址
|
||||||
|
|
||||||
|
> **注意**: 二级页表需要 **3 次内存访问**(页目录 + 页表 + 数据),比一级页表多一次。因此 TLB 的作用更加重要。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、倒转页表
|
||||||
|
|
||||||
|
### 基本思想
|
||||||
|
|
||||||
|
传统页表以**虚拟页号**为索引,每个进程一张。倒转页表以**物理页框号**为索引,整个系统一张。
|
||||||
|
|
||||||
|
| 对比项 | 传统页表 | 倒转页表 |
|
||||||
|
|--------|---------|---------|
|
||||||
|
| 索引 | 虚拟页号 (VPN) | 物理页框号 (PFN) |
|
||||||
|
| 表项数 | 虚拟页数(可能很大) | 物理页框数(固定) |
|
||||||
|
| 进程数 | 每进程一张 | 全系统一张 |
|
||||||
|
| 查找方式 | 直接索引 | 需要搜索(或用Hash) |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph 传统页表
|
||||||
|
direction TB
|
||||||
|
T1["VPN 0 → PPN x"]
|
||||||
|
T2["VPN 1 → PPN y"]
|
||||||
|
T3["VPN 2 → PPN z"]
|
||||||
|
T4["..."]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph 倒转页表
|
||||||
|
direction TB
|
||||||
|
I1["PFN 0 ← VPN a, 进程P1"]
|
||||||
|
I2["PFN 1 ← VPN b, 进程P2"]
|
||||||
|
I3["PFN 2 ← VPN c, 进程P1"]
|
||||||
|
I4["..."]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**优点**: 表大小与物理内存成正比,节省空间(尤其在 64 位系统)
|
||||||
|
|
||||||
|
**缺点**: 查找需要搜索整个表(通常用 Hash 加速)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、内存保护
|
||||||
|
|
||||||
|
分页系统中通过以下机制实现内存保护:
|
||||||
|
|
||||||
|
### 1. 越界保护
|
||||||
|
|
||||||
|
- 页表项中的有效位 V=0 表示该页不在内存,访问时触发缺页中断
|
||||||
|
- 非法虚拟地址(超出进程地址空间范围)触发保护异常
|
||||||
|
|
||||||
|
### 2. 标志位保护
|
||||||
|
|
||||||
|
| 标志位 | 保护功能 |
|
||||||
|
|--------|---------|
|
||||||
|
| R/W | 读/写权限控制。只读页被写入时触发异常 |
|
||||||
|
| U/S | 用户/内核权限。用户态访问内核页时触发异常 |
|
||||||
|
| NX (No Execute) | 禁止执行位。数据页被当作代码执行时触发异常 |
|
||||||
|
|
||||||
|
### 3. 键保护
|
||||||
|
|
||||||
|
- 每个物理页框有一个保护键 (Protection Key)
|
||||||
|
- 每个进程有一个键寄存器
|
||||||
|
- 只有键匹配时才允许访问
|
||||||
|
- Intel MPX/MPK 技术支持此机制
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、空闲页面管理
|
||||||
|
|
||||||
|
操作系统需要跟踪物理内存中哪些页框是空闲的:
|
||||||
|
|
||||||
|
### 1. 位示图法 (Bitmap)
|
||||||
|
|
||||||
|
用一个 bit 表示一个物理页框的状态:0=空闲,1=已分配。
|
||||||
|
|
||||||
|
```
|
||||||
|
位示图示例 (假设16个物理页框):
|
||||||
|
位号: 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
|
||||||
|
状态: 1 1 0 1 0 0 1 1 0 1 0 0 1 1 0 1
|
||||||
|
↑ ↑ ↑
|
||||||
|
空闲 空闲 空闲
|
||||||
|
```
|
||||||
|
|
||||||
|
- **优点**: 简单,查找连续空闲块方便
|
||||||
|
- **缺点**: 位示图本身占用内存(物理内存 4GB、页大小 4KB → 位示图 128KB)
|
||||||
|
|
||||||
|
### 2. 链表法
|
||||||
|
|
||||||
|
将所有空闲页框用链表串起来:
|
||||||
|
|
||||||
|
```
|
||||||
|
空闲链表: [PF3] → [PF5] → [PF8] → [PF12] → NULL
|
||||||
|
```
|
||||||
|
|
||||||
|
- **优点**: 实现简单,不额外占用大量空间
|
||||||
|
- **缺点**: 查找连续空闲块需要遍历链表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、Nachos 页式存储管理代码
|
||||||
|
|
||||||
|
Nachos 教学操作系统中实现了基本的分页存储管理。以下是关键代码片段:
|
||||||
|
|
||||||
|
### 核心数据结构
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// 页表项结构 (Machine/translate.h)
|
||||||
|
typedef struct {
|
||||||
|
int virtualPage; // 虚拟页号 (VPN)
|
||||||
|
int physicalPage; // 物理页号 (PPN)
|
||||||
|
bool valid; // 有效位
|
||||||
|
bool readOnly; // 只读标志
|
||||||
|
bool use; // 引用位 (用于LRU等算法)
|
||||||
|
bool dirty; // 修改位
|
||||||
|
} TranslationEntry;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 地址转换核心代码
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Machine/translate.cc - translate()
|
||||||
|
// 将虚拟地址转换为物理地址
|
||||||
|
int Machine::Translate(int virtAddr, int *physAddr, int size, bool writing) {
|
||||||
|
// 1. 计算VPN和偏移
|
||||||
|
int vpn = virtAddr / PageSize;
|
||||||
|
int offset = virtAddr % PageSize;
|
||||||
|
|
||||||
|
// 2. 查找页表
|
||||||
|
TranslationEntry *entry = &pageTable[vpn];
|
||||||
|
|
||||||
|
// 3. 检查有效位
|
||||||
|
if (!entry->valid) {
|
||||||
|
// 缺页处理
|
||||||
|
return PageFaultException;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 检查写权限
|
||||||
|
if (writing && entry->readOnly) {
|
||||||
|
return ReadOnlyException;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 计算物理地址
|
||||||
|
*physAddr = entry->physicalPage * PageSize + offset;
|
||||||
|
|
||||||
|
// 6. 更新引用位和修改位
|
||||||
|
entry->use = true;
|
||||||
|
if (writing) entry->dirty = true;
|
||||||
|
|
||||||
|
return NoException;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 内存分配示例
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// AddrSpace/addrspace.cc - 进程地址空间初始化
|
||||||
|
// 为进程分配物理页框
|
||||||
|
void AddrSpace::InitRegisters() {
|
||||||
|
// 初始化页表
|
||||||
|
pageTable = new TranslationEntry[numPages];
|
||||||
|
for (int i = 0; i < numPages; i++) {
|
||||||
|
pageTable[i].virtualPage = i;
|
||||||
|
pageTable[i].physicalPage = bitMap->Find(); // 位示图分配
|
||||||
|
pageTable[i].valid = true;
|
||||||
|
pageTable[i].readOnly = false;
|
||||||
|
pageTable[i].use = false;
|
||||||
|
pageTable[i].dirty = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、小结
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
mindmap
|
||||||
|
root((分页存储管理))
|
||||||
|
碎片问题
|
||||||
|
内碎片(固定分区)
|
||||||
|
外碎片(动态分区)
|
||||||
|
根因:连续存放
|
||||||
|
分页思想
|
||||||
|
虚拟页VP
|
||||||
|
物理页框PF
|
||||||
|
等大划分
|
||||||
|
地址结构
|
||||||
|
VPN+VPO
|
||||||
|
PPN+PPO
|
||||||
|
页内偏移不变
|
||||||
|
页表
|
||||||
|
VPN→PPN映射
|
||||||
|
有效位/Dirty/Ref
|
||||||
|
每进程独立
|
||||||
|
地址变换
|
||||||
|
PTBR+VPN查页表
|
||||||
|
取PPN拼PPO
|
||||||
|
缺页中断处理
|
||||||
|
TLB快表
|
||||||
|
高速缓存页表项
|
||||||
|
命中率影响EAT
|
||||||
|
多级页表
|
||||||
|
页目录+页表
|
||||||
|
节省内存
|
||||||
|
倒转页表
|
||||||
|
按物理块号索引
|
||||||
|
全系统一张
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 思考题
|
||||||
|
|
||||||
|
1. **概念理解**: 为什么分页能消除外碎片但不能消除内碎片?内碎片平均浪费多少?
|
||||||
|
|
||||||
|
2. **计算题**: 某系统页面大小 4KB,虚拟地址 20 位,物理地址 18 位。
|
||||||
|
- 虚拟地址空间有多少页?
|
||||||
|
- 物理内存最大多少?
|
||||||
|
- 页表至少需要多少项?
|
||||||
|
|
||||||
|
3. **TLB计算**: 设 TLB 查找时间 5ns,内存访问时间 80ns,要求有效访问时间不超过 100ns,TLB 命中率至少为多少?
|
||||||
|
|
||||||
|
4. **多级页表**: 对于 64 位系统,为什么至少需要 3 级页表?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关联笔记
|
||||||
|
|
||||||
|
- [[13_存储管理基础]] — 存储器层次结构与地址空间基础
|
||||||
|
- [[15_段式存储管理]] — 分段式地址转换,段页式结合
|
||||||
|
- [[16_虚拟存储器]] — 基于分页的虚拟存储器实现
|
||||||
|
- [[11_处理机调度]] — 进程调度与缺页处理的关系
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**上一讲**: [[13_存储管理基础]]
|
||||||
|
**下一讲**: [[15_段式存储管理]]
|
||||||
398
操作系统/15_段式存储管理/15_段式存储管理.md
Normal file
398
操作系统/15_段式存储管理/15_段式存储管理.md
Normal file
@@ -0,0 +1,398 @@
|
|||||||
|
# 15. 段式存储管理
|
||||||
|
|
||||||
|
> **课程**: 操作系统 - 存储器管理
|
||||||
|
> **核心内容**: 分段引入原因、分段思想、地址结构、段表、地址变换、段页式存储管理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前置知识
|
||||||
|
|
||||||
|
- [[13_存储管理基础]] — 存储器层次结构、逻辑地址与物理地址
|
||||||
|
- [[14_分页存储管理]] — 分页思想、页表、地址变换
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、为什么需要分段
|
||||||
|
|
||||||
|
[[14_分页存储管理|分页]]虽然解决了碎片问题,但在以下场景中存在不足:
|
||||||
|
|
||||||
|
### 1. 信息共享不方便
|
||||||
|
|
||||||
|
分页按固定大小划分,不考虑程序的逻辑结构。如果要共享一段代码(如共享库函数),该代码可能跨越多个页面,其中某些页面还包含不需要共享的数据,导致共享粒度过粗。
|
||||||
|
|
||||||
|
```
|
||||||
|
分页视角(按固定大小切分,不考虑逻辑含义):
|
||||||
|
┌────────┐ ┌────────┐ ┌────────┐
|
||||||
|
│ 代码1 │ │ 代码2+ │ │ 数据 │ ← 一个逻辑模块跨了3个页
|
||||||
|
│ │ │ 常量 │ │ │ 共享时会把不相关的内容也共享了
|
||||||
|
└────────┘ └────────┘ └────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 动态链接问题
|
||||||
|
|
||||||
|
动态链接需要在运行时将目标模块装入内存并链接。分页系统中,模块的装入和链接以页为单位,不够灵活。
|
||||||
|
|
||||||
|
### 3. 程序员视角需求
|
||||||
|
|
||||||
|
程序员编写程序时,自然地将代码划分为**代码段、数据段、堆栈段**等逻辑模块。分页完全打乱了这种逻辑结构。
|
||||||
|
|
||||||
|
> **核心需求**: 地址空间的划分应该按照**逻辑意义**进行,而非固定大小。这就是分段的思想。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、分段的基本思想
|
||||||
|
|
||||||
|
分段按照程序的**逻辑结构**将地址空间划分为若干个段:
|
||||||
|
|
||||||
|
- 每个段有独立的**段名**和**段长**
|
||||||
|
- 每个段在内存中**连续存放**
|
||||||
|
- 不同段之间**不需要连续**
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph VA["进程虚拟地址空间"]
|
||||||
|
direction TB
|
||||||
|
S0["主程序段 (段0)"]
|
||||||
|
S1["子程序段 (段1)"]
|
||||||
|
S2["数据段 (段2)"]
|
||||||
|
S3["栈段 (段3)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph PM["物理内存"]
|
||||||
|
direction TB
|
||||||
|
M0["区域A"]
|
||||||
|
M1["区域B"]
|
||||||
|
M2["区域C"]
|
||||||
|
M3["区域D"]
|
||||||
|
M4["空闲"]
|
||||||
|
M5["区域E"]
|
||||||
|
end
|
||||||
|
|
||||||
|
S0 -->|"段表映射"| M5
|
||||||
|
S1 -->|"段表映射"| M0
|
||||||
|
S2 -->|"段表映射"| M2
|
||||||
|
S3 -->|"段表映射"| M3
|
||||||
|
|
||||||
|
style VA fill:#e3f2fd,stroke:#1976d2
|
||||||
|
style PM fill:#fff8e1,stroke:#f9a825
|
||||||
|
```
|
||||||
|
|
||||||
|
### 分段 vs 分页
|
||||||
|
|
||||||
|
| 对比项 | 分页 | 分段 |
|
||||||
|
|--------|------|------|
|
||||||
|
| **划分依据** | 固定大小(物理需要) | 逻辑意义(程序员视角) |
|
||||||
|
| **段/页大小** | 所有页等大 | 各段长度不同 |
|
||||||
|
| **地址空间** | 一维(VA直接计算VPN和偏移) | **二维**(段号 + 段内偏移) |
|
||||||
|
| **碎片类型** | 内碎片(页内浪费) | 外碎片(段间空闲区) |
|
||||||
|
| **共享** | 以页为粒度,较粗 | 以段为粒度,符合逻辑 |
|
||||||
|
| **动态增长** | 不方便 | 方便(如堆、栈段可动态扩展) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、地址结构
|
||||||
|
|
||||||
|
分段系统的逻辑地址是**二维**的,由两部分组成:
|
||||||
|
|
||||||
|
```
|
||||||
|
逻辑地址 = (段号 S, 段内地址 d)
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
逻辑地址表示:
|
||||||
|
┌──────────────┬────────────────────┐
|
||||||
|
│ 段号 S │ 段内地址 d │
|
||||||
|
│ (高位部分) │ (低位部分) │
|
||||||
|
└──────────────┴────────────────────┘
|
||||||
|
|
||||||
|
注意: 不同于分页的VPN|VPO可以统一计算,
|
||||||
|
分段中 d 的取值范围取决于段长,各段不同。
|
||||||
|
```
|
||||||
|
|
||||||
|
**地址表示示例**:
|
||||||
|
|
||||||
|
- 分页地址 `(0x1A8F)` → 可直接算出 VPN=6, VPO=0x28F
|
||||||
|
- 分段地址 `(2, 0x100)` → 段号=2,偏移=0x100(需要查段表才知道该段的基址和长度)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、段表
|
||||||
|
|
||||||
|
### 段表结构
|
||||||
|
|
||||||
|
每个进程拥有一张**段表**,段表以**段号**为索引,每项包含:
|
||||||
|
|
||||||
|
| 字段 | 含义 |
|
||||||
|
|------|------|
|
||||||
|
| **段号** | 段的编号(隐含在索引中) |
|
||||||
|
| **段长 (Limit)** | 该段的长度(字节数),用于越界检查 |
|
||||||
|
| **段基址 (Base)** | 该段在物理内存中的起始地址 |
|
||||||
|
|
||||||
|
```
|
||||||
|
段表示例:
|
||||||
|
┌──────┬────────────┬────────────────┐
|
||||||
|
│ 段号 │ 段长(Limit) │ 基址(Base) │
|
||||||
|
├──────┼────────────┼────────────────┤
|
||||||
|
│ 0 │ 0x2000 │ 0x4000 │ ← 主程序: 在内存0x4000处, 长8KB
|
||||||
|
│ 1 │ 0x1000 │ 0x8000 │ ← 子程序: 在内存0x8000处, 长4KB
|
||||||
|
│ 2 │ 0x3000 │ 0xB000 │ ← 数据段: 在内存0xB000处, 长12KB
|
||||||
|
│ 3 │ 0x1800 │ 0x2000 │ ← 栈段: 在内存0x2000处, 长6KB
|
||||||
|
└──────┴────────────┴────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
段表基址寄存器 **STBR** 指向段表在内存中的起始地址。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、地址变换过程
|
||||||
|
|
||||||
|
### 变换流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["逻辑地址 (S, d)"] --> B["用STBR找到段表基址"]
|
||||||
|
B --> C["定位段表项: 段表基址 + S × 段表项大小"]
|
||||||
|
C --> D{"d < Limit ?"}
|
||||||
|
D -->|"是"| E["物理地址 PA = Base + d"]
|
||||||
|
D -->|"否"| F["**越界中断**<br/>(Segmentation Fault)"]
|
||||||
|
E --> G["访问物理内存"]
|
||||||
|
|
||||||
|
style F fill:#ffcdd2,stroke:#c62828
|
||||||
|
style D fill:#fff3e0,stroke:#e65100
|
||||||
|
```
|
||||||
|
|
||||||
|
### 详细步骤
|
||||||
|
|
||||||
|
1. **提取段号和偏移**: 从逻辑地址中分离出段号 S 和段内地址 d
|
||||||
|
2. **查段表**: 用 STBR + S 定位到第 S 个段表项
|
||||||
|
3. **越界检查**: 比较 d 与 Limit
|
||||||
|
- 若 d >= Limit,触发**越界中断**(Segmentation Fault)
|
||||||
|
- 若 d < Limit,继续
|
||||||
|
4. **计算物理地址**: PA = Base + d
|
||||||
|
5. **访问内存**: 用物理地址访问实际内存
|
||||||
|
|
||||||
|
### 地址变换计算示例
|
||||||
|
|
||||||
|
> **例题**: 某分段系统,段表如下。求以下逻辑地址对应的物理地址:
|
||||||
|
> - (0, 0x1500)
|
||||||
|
> - (1, 0x2000)
|
||||||
|
> - (2, 0x0800)
|
||||||
|
|
||||||
|
**解题过程**:
|
||||||
|
|
||||||
|
| 逻辑地址 | S | d | Limit | 比较 | Base | PA = Base + d | 结果 |
|
||||||
|
|---------|---|---|-------|------|------|--------------|------|
|
||||||
|
| (0, 0x1500) | 0 | 0x1500 | 0x2000 | 0x1500 < 0x2000 ✓ | 0x4000 | 0x4000 + 0x1500 | **0x5500** |
|
||||||
|
| (1, 0x2000) | 1 | 0x2000 | 0x1000 | 0x2000 >= 0x1000 ✗ | — | — | **越界中断** |
|
||||||
|
| (2, 0x0800) | 2 | 0x0800 | 0x3000 | 0x0800 < 0x3000 ✓ | 0xB000 | 0xB000 + 0x0800 | **0xB800** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、分段的优缺点
|
||||||
|
|
||||||
|
### 优点
|
||||||
|
|
||||||
|
| 优点 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **符合程序逻辑** | 按代码、数据、栈等逻辑单元组织,便于理解和管理 |
|
||||||
|
| **便于共享** | 以段为单位共享,粒度合理(如共享整个代码段) |
|
||||||
|
| **便于动态链接** | 可以按段为单位进行动态链接和装入 |
|
||||||
|
| **支持动态增长** | 堆、栈等段可以独立扩展,不影响其他段 |
|
||||||
|
| **保护自然** | 每段可设置独立的访问权限(代码段只读、数据段可读写等) |
|
||||||
|
|
||||||
|
### 缺点
|
||||||
|
|
||||||
|
| 缺点 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **外碎片** | 段长不等,内存分配/回收后产生不连续的空闲区 |
|
||||||
|
| **段长限制** | 每段最大长度受地址结构限制 |
|
||||||
|
| **内存紧缩开销** | 消除外碎片需要移动段(类似动态分区的紧凑操作) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、段页式存储管理
|
||||||
|
|
||||||
|
### 基本思想
|
||||||
|
|
||||||
|
**段页式** = 分段 + 分页,兼具两者的优点:
|
||||||
|
|
||||||
|
- 先按**逻辑结构分段**(保留分段的优点:共享、保护、逻辑清晰)
|
||||||
|
- 再将每段**按固定大小分页**(保留分页的优点:消除外碎片)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph VA["虚拟地址空间"]
|
||||||
|
direction TB
|
||||||
|
S0["段0 (主程序)"]
|
||||||
|
S1["段1 (子程序)"]
|
||||||
|
S2["段2 (数据)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph S0P["段0 内部分页"]
|
||||||
|
direction TB
|
||||||
|
P0["页0"]
|
||||||
|
P1["页1"]
|
||||||
|
P2["页2"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph PM["物理内存"]
|
||||||
|
direction TB
|
||||||
|
F0["页框0"]
|
||||||
|
F1["页框1"]
|
||||||
|
F2["页框2"]
|
||||||
|
F3["页框3"]
|
||||||
|
end
|
||||||
|
|
||||||
|
VA --> S0P
|
||||||
|
P0 --> F2
|
||||||
|
P1 --> F0
|
||||||
|
P2 --> F3
|
||||||
|
|
||||||
|
style VA fill:#e3f2fd,stroke:#1976d2
|
||||||
|
style S0P fill:#f3e5f5,stroke:#7b1fa2
|
||||||
|
style PM fill:#fff8e1,stroke:#f9a825
|
||||||
|
```
|
||||||
|
|
||||||
|
### 段页式地址结构
|
||||||
|
|
||||||
|
逻辑地址由**三部分**组成:
|
||||||
|
|
||||||
|
```
|
||||||
|
段页式逻辑地址:
|
||||||
|
┌──────────────┬──────────────┬──────────────┐
|
||||||
|
│ 段号 S │ 页号 P │ 页内偏移 d │
|
||||||
|
└──────────────┴──────────────┴──────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 段页式地址变换
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["逻辑地址 (S, P, d)"] --> B["1. 用STBR找到段表"]
|
||||||
|
B --> C["2. 用段号S查段表<br/>得到该段的页表基址和段长"]
|
||||||
|
C --> D{"P < 该段页数?"}
|
||||||
|
D -->|"是"| E["3. 用页号P查页表<br/>得到物理页框号 PPN"]
|
||||||
|
D -->|"否"| F["越界中断"]
|
||||||
|
E --> G["4. 物理地址 = PPN × 页大小 + d"]
|
||||||
|
G --> H["访问物理内存"]
|
||||||
|
|
||||||
|
style F fill:#ffcdd2,stroke:#c62828
|
||||||
|
```
|
||||||
|
|
||||||
|
### 段页式地址变换步骤
|
||||||
|
|
||||||
|
1. **查段表**: 用 STBR + S 找到第 S 个段表项,得到该段的**页表基址**
|
||||||
|
2. **查页表**: 用页表基址 + P 找到第 P 个页表项,得到 **PPN**
|
||||||
|
3. **拼接地址**: PA = PPN × 页大小 + d
|
||||||
|
|
||||||
|
> **注意**: 段页式需要 **3 次内存访问**(段表 + 页表 + 数据),比纯分页多一次。TLB 的作用更加关键。
|
||||||
|
|
||||||
|
### 段页式地址变换计算示例
|
||||||
|
|
||||||
|
> **例题**: 某段页式系统,页面大小 4KB。段表如下,求逻辑地址 (1, 2, 0x100) 的物理地址。
|
||||||
|
|
||||||
|
**段表**:
|
||||||
|
|
||||||
|
| 段号 | 页表基址 | 段长(页数) |
|
||||||
|
|------|---------|-----------|
|
||||||
|
| 0 | 0x8000 | 4 |
|
||||||
|
| 1 | 0xA000 | 6 |
|
||||||
|
| 2 | 0xC000 | 3 |
|
||||||
|
|
||||||
|
**段1的页表** (基址 0xA000):
|
||||||
|
|
||||||
|
| 页号 | PPN |
|
||||||
|
|------|-----|
|
||||||
|
| 0 | 5 |
|
||||||
|
| 1 | 2 |
|
||||||
|
| 2 | 8 |
|
||||||
|
| 3 | 1 |
|
||||||
|
| 4 | 7 |
|
||||||
|
| 5 | 3 |
|
||||||
|
|
||||||
|
**解题过程**:
|
||||||
|
|
||||||
|
1. S=1, P=2, d=0x100
|
||||||
|
2. 查段表:段1的页表基址 = 0xA000,段长 = 6 页
|
||||||
|
3. P=2 < 6,合法
|
||||||
|
4. 查段1的页表:P=2 对应 PPN=**8**
|
||||||
|
5. PA = 8 × 4096 + 0x100 = 0x8000 + 0x100 = **0x8100**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、三种存储管理方式对比
|
||||||
|
|
||||||
|
| 对比项 | 纯分页 | 纯分段 | 段页式 |
|
||||||
|
|--------|--------|--------|--------|
|
||||||
|
| **划分依据** | 固定大小 | 逻辑结构 | 先逻辑后固定大小 |
|
||||||
|
| **地址维度** | 一维 | 二维 | 三维 |
|
||||||
|
| **碎片** | 内碎片 | 外碎片 | 内碎片 |
|
||||||
|
| **共享** | 以页为粒度 | 以段为粒度 | 以段为粒度 |
|
||||||
|
| **内存访问次数** | 2次(页表+数据) | 2次(段表+数据) | 3次(段表+页表+数据) |
|
||||||
|
| **代表系统** | Linux | 早期Multics | Intel x86(32位保护模式) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、小结
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
mindmap
|
||||||
|
root((段式存储管理))
|
||||||
|
分段引入
|
||||||
|
信息共享不便
|
||||||
|
动态链接需求
|
||||||
|
逻辑结构需求
|
||||||
|
分段思想
|
||||||
|
按逻辑分段
|
||||||
|
代码段/数据段/栈段
|
||||||
|
各段独立地址空间
|
||||||
|
地址结构
|
||||||
|
二维: 段号+偏移
|
||||||
|
不同于分页的一维
|
||||||
|
段表
|
||||||
|
段号/段长/基址
|
||||||
|
越界检查
|
||||||
|
STBR寄存器
|
||||||
|
地址变换
|
||||||
|
查段表→越界检查→基址+偏移
|
||||||
|
PA = Base + d
|
||||||
|
段页式
|
||||||
|
先分段再分页
|
||||||
|
三维地址: 段号+页号+偏移
|
||||||
|
兼具两者优点
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 思考题
|
||||||
|
|
||||||
|
1. **概念理解**: 分段和分页的根本区别是什么?为什么说分段的地址空间是二维的?
|
||||||
|
|
||||||
|
2. **地址变换**: 某分段系统有 4 个段,段表如下。求物理地址或判断是否越界:
|
||||||
|
- (0, 0x800) → ?
|
||||||
|
- (2, 0x5000) → ?
|
||||||
|
- (3, 0x2000) → ?
|
||||||
|
|
||||||
|
| 段号 | 段长 | 基址 |
|
||||||
|
|------|------|------|
|
||||||
|
| 0 | 0x1000 | 0x5000 |
|
||||||
|
| 1 | 0x2000 | 0x8000 |
|
||||||
|
| 2 | 0x4000 | 0xA000 |
|
||||||
|
| 3 | 0x3000 | 0x2000 |
|
||||||
|
|
||||||
|
3. **段页式**: 为什么段页式需要 3 次内存访问?TLB 如何缓解这个问题?
|
||||||
|
|
||||||
|
4. **对比分析**: 在什么场景下分段优于分页?在什么场景下分页优于分段?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关联笔记
|
||||||
|
|
||||||
|
- [[13_存储管理基础]] — 存储器层次结构与地址空间基础
|
||||||
|
- [[14_分页存储管理]] — 分页思想、页表与地址变换
|
||||||
|
- [[16_虚拟存储器]] — 基于分段的虚拟存储器实现
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**上一讲**: [[14_分页存储管理]]
|
||||||
|
**下一讲**: [[16_虚拟存储器]]
|
||||||
606
操作系统/16_虚拟存储器/16_虚拟存储器.md
Normal file
606
操作系统/16_虚拟存储器/16_虚拟存储器.md
Normal file
@@ -0,0 +1,606 @@
|
|||||||
|
# 16. 虚拟存储器
|
||||||
|
|
||||||
|
> **课程**: 操作系统 - 存储器管理
|
||||||
|
> **核心内容**: 虚拟存储器思想、局部性原理、请求分页、页面置换算法、内存分配策略、抖动与工作集
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前置知识
|
||||||
|
|
||||||
|
- [[14_分页存储管理]] — 分页思想、页表、地址变换、缺页中断
|
||||||
|
- [[15_段式存储管理]] — 分段思想、段页式
|
||||||
|
- [[11_处理机调度]] — 进程调度基本概念
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、虚拟存储器的基本思想
|
||||||
|
|
||||||
|
### 问题的提出
|
||||||
|
|
||||||
|
在传统的存储管理中,作业必须**全部装入内存**才能运行。这带来两个问题:
|
||||||
|
1. 作业大小超过物理内存时,无法运行
|
||||||
|
2. 内存中同时驻留多个大程序时,内存不够用
|
||||||
|
|
||||||
|
### 虚拟存储器思想
|
||||||
|
|
||||||
|
**核心**: 程序员在比实际物理内存**大得多**的虚拟地址空间中编写程序,运行时只需将**部分页面**调入内存,其余留在外存。当访问到不在内存的页面时,再从外存调入。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph VA["虚拟地址空间 (很大)"]
|
||||||
|
direction TB
|
||||||
|
V0["页面0 ✓ 在内存"]
|
||||||
|
V1["页面1 ✓ 在内存"]
|
||||||
|
V2["页面2 ✗ 在外存"]
|
||||||
|
V3["页面3 ✓ 在内存"]
|
||||||
|
V4["页面4 ✗ 在外存"]
|
||||||
|
V5["..."]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph MEM["物理内存 (较小)"]
|
||||||
|
direction TB
|
||||||
|
M0["页框0"]
|
||||||
|
M1["页框1"]
|
||||||
|
M2["页框2"]
|
||||||
|
M3["页框3"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph DISK["外存 (磁盘)"]
|
||||||
|
direction TB
|
||||||
|
D0["页面2"]
|
||||||
|
D1["页面4"]
|
||||||
|
D2["..."]
|
||||||
|
end
|
||||||
|
|
||||||
|
V0 --> M0
|
||||||
|
V1 --> M1
|
||||||
|
V3 --> M3
|
||||||
|
V2 -.->|"缺页时调入"| DISK
|
||||||
|
V4 -.->|"缺页时调入"| DISK
|
||||||
|
|
||||||
|
style VA fill:#e3f2fd,stroke:#1976d2
|
||||||
|
style MEM fill:#fff8e1,stroke:#f9a825
|
||||||
|
style DISK fill:#fce4ec,stroke:#c62828
|
||||||
|
```
|
||||||
|
|
||||||
|
### 实现基础
|
||||||
|
|
||||||
|
虚拟存储器的实现依赖两个关键技术:
|
||||||
|
1. **[[14_分页存储管理|分页]]或[[15_段式存储管理|分段]]**: 将程序划分为小块,可以部分装入
|
||||||
|
2. **[[14_分页存储管理#五、地址变换过程|缺页中断]]**: 访问不在内存的页面时,由OS负责调入
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、局部性原理
|
||||||
|
|
||||||
|
虚拟存储器之所以可行,是因为程序运行时具有**局部性**——程序不会均匀地访问所有地址空间。
|
||||||
|
|
||||||
|
### 时间局部性 (Temporal Locality)
|
||||||
|
|
||||||
|
> **含义**: 如果一条指令/数据被访问,那么在不久的将来它很可能**再次被访问**。
|
||||||
|
|
||||||
|
**典型场景**:
|
||||||
|
- 循环中的指令反复执行
|
||||||
|
- 频繁访问的变量
|
||||||
|
- 栈顶数据的反复操作
|
||||||
|
|
||||||
|
### 空间局部性 (Spatial Locality)
|
||||||
|
|
||||||
|
> **含义**: 如果一个存储单元被访问,那么它**附近的单元**也很可能很快被访问。
|
||||||
|
|
||||||
|
**典型场景**:
|
||||||
|
- 顺序执行的指令
|
||||||
|
- 数组的顺序遍历
|
||||||
|
- 结构体成员的连续访问
|
||||||
|
|
||||||
|
### 局部性的实际影响:行优先 vs 列优先
|
||||||
|
|
||||||
|
以二维数组遍历为例,行优先和列优先的性能差异可达 **21.5 倍**:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 二维数组: int a[1024][1024];
|
||||||
|
|
||||||
|
// 行优先遍历 (空间局部性好)
|
||||||
|
for (int i = 0; i < 1024; i++)
|
||||||
|
for (int j = 0; j < 1024; j++)
|
||||||
|
sum += a[i][j]; // 访问顺序: a[0][0], a[0][1], a[0][2], ...
|
||||||
|
// 连续内存访问,充分利用缓存行
|
||||||
|
|
||||||
|
// 列优先遍历 (空间局部性差)
|
||||||
|
for (int j = 0; j < 1024; j++)
|
||||||
|
for (int i = 0; i < 1024; i++)
|
||||||
|
sum += a[i][j]; // 访问顺序: a[0][0], a[1][0], a[2][0], ...
|
||||||
|
// 每次跳过一整行,频繁缺页/缓存失效
|
||||||
|
```
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph RowMajor["行优先遍历"]
|
||||||
|
direction LR
|
||||||
|
R1["a[0][0]"] --> R2["a[0][1]"] --> R3["a[0][2]"] --> R4["..."]
|
||||||
|
style R1 fill:#c8e6c9
|
||||||
|
style R2 fill:#c8e6c9
|
||||||
|
style R3 fill:#c8e6c9
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph ColMajor["列优先遍历"]
|
||||||
|
direction LR
|
||||||
|
C1["a[0][0]"] --> C2["a[1][0]"] --> C3["a[2][0]"] --> C4["..."]
|
||||||
|
style C1 fill:#ffcdd2
|
||||||
|
style C2 fill:#ffcdd2
|
||||||
|
style C3 fill:#ffcdd2
|
||||||
|
end
|
||||||
|
|
||||||
|
RowMajor ---|"性能差异约 21.5 倍"| ColMajor
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、请求分页
|
||||||
|
|
||||||
|
### 基本思想
|
||||||
|
|
||||||
|
**请求分页 (Demand Paging)** 是最常用的虚拟存储器实现方式:
|
||||||
|
- 页面只在**被访问时**才调入内存(而非预装入)
|
||||||
|
- 内存不足时,将某些页面**换出**到外存,腾出空间
|
||||||
|
|
||||||
|
### 页表项扩展
|
||||||
|
|
||||||
|
在[[14_分页存储管理#四、页表|基本分页]]的基础上,请求分页的页表项增加了**状态位**:
|
||||||
|
|
||||||
|
| 字段 | 含义 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| **状态位 (Present/Valid)** | 页面是否在内存 | 1=在内存,0=不在内存 |
|
||||||
|
| **访问位 (Reference/Access)** | 页面是否被访问过 | 用于置换算法判断 |
|
||||||
|
| **修改位 (Dirty)** | 页面是否被写过 | Dirty=1 时换出需写回外存 |
|
||||||
|
| **外存地址** | 页面在外存中的位置 | 用于缺页时调入 |
|
||||||
|
|
||||||
|
### 缺页中断处理
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["CPU访问虚拟地址"] --> B{"页面在内存中?<br/>(检查状态位)"}
|
||||||
|
B -->|"在内存"| C["正常地址变换,访问数据"]
|
||||||
|
B -->|"不在内存"| D["触发**缺页中断**"]
|
||||||
|
D --> E["保存CPU现场"]
|
||||||
|
E --> F{"外存中存在该页?"}
|
||||||
|
F -->|"不存在"| G["终止进程<br/>Segmentation Fault"]
|
||||||
|
F -->|"存在"| H{"内存有空闲页框?"}
|
||||||
|
H -->|"有空闲"| I["从外存读入该页"]
|
||||||
|
H -->|"无空闲"| J["执行**页面置换算法**<br/>选择牺牲页"]
|
||||||
|
J --> K{"牺牲页Dirty=1?"}
|
||||||
|
K -->|"是"| L["将牺牲页写回外存"]
|
||||||
|
K -->|"否"| M["直接覆盖"]
|
||||||
|
L --> I
|
||||||
|
M --> I
|
||||||
|
I --> N["更新页表:<br/>状态位=1, PPN"]
|
||||||
|
N --> O["刷新TLB"]
|
||||||
|
O --> P["重新执行被中断的指令"]
|
||||||
|
|
||||||
|
style D fill:#ffcdd2,stroke:#c62828
|
||||||
|
style J fill:#fff3e0,stroke:#e65100
|
||||||
|
style G fill:#ffcdd2,stroke:#c62828
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、页面置换算法
|
||||||
|
|
||||||
|
当内存中没有空闲页框时,需要选择一个页面换出到外存,为新页面腾出空间。不同的选择策略就是**页面置换算法**。
|
||||||
|
|
||||||
|
> **示例参数**: 物理内存有 **3 个页框**,页面引用串为:
|
||||||
|
> **7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1**
|
||||||
|
|
||||||
|
### 1. OPT (Optimal) 最佳置换算法
|
||||||
|
|
||||||
|
**策略**: 换出**将来最长时间不会被使用**的页面。
|
||||||
|
|
||||||
|
> OPT 是理论算法,因为无法预知未来的页面访问序列。它用于衡量其他算法的上限。
|
||||||
|
|
||||||
|
**执行过程**:
|
||||||
|
|
||||||
|
| 访问序列 | 页框0 | 页框1 | 页框2 | 缺页? | 说明 |
|
||||||
|
|---------|-------|-------|-------|-------|------|
|
||||||
|
| **7** | 7 | - | - | 缺页 | 空闲页框,直接装入 |
|
||||||
|
| **0** | 7 | 0 | - | 缺页 | 空闲页框,直接装入 |
|
||||||
|
| **1** | 7 | 0 | 1 | 缺页 | 空闲页框,直接装入 |
|
||||||
|
| **2** | 7 | 0 | **2** | 缺页 | 换出1(最久后才用) |
|
||||||
|
| **0** | 7 | 0 | 2 | 命中 | 0已在内存 |
|
||||||
|
| **3** | **3** | 0 | 2 | 缺页 | 换出7(最久后才用) |
|
||||||
|
| **0** | 3 | 0 | 2 | 命中 | |
|
||||||
|
| **4** | 3 | 0 | **4** | 缺页 | 换出2(最久后才用) |
|
||||||
|
| **2** | 3 | **2** | 4 | 缺页 | 换出0(最久后才用) |
|
||||||
|
| **3** | 3 | 2 | 4 | 命中 | |
|
||||||
|
| **0** | 3 | 2 | **0** | 缺页 | 换出4(最久后才用) |
|
||||||
|
| **3** | 3 | 2 | 0 | 命中 | |
|
||||||
|
| **2** | 3 | 2 | 0 | 命中 | |
|
||||||
|
| **1** | **1** | 2 | 0 | 缺页 | 换出3(最久后才用) |
|
||||||
|
| **2** | 1 | 2 | 0 | 命中 | |
|
||||||
|
| **0** | 1 | 2 | 0 | 命中 | |
|
||||||
|
| **1** | 1 | 2 | 0 | 命中 | |
|
||||||
|
| **7** | 1 | **7** | 0 | 缺页 | 换出2(最久后才用) |
|
||||||
|
| **0** | 1 | 7 | 0 | 命中 | |
|
||||||
|
| **1** | 1 | 7 | 0 | 命中 | |
|
||||||
|
|
||||||
|
**缺页次数: 9**,缺页率 = 9/20 = **45%**
|
||||||
|
|
||||||
|
### 2. FIFO (First-In First-Out) 先进先出
|
||||||
|
|
||||||
|
**策略**: 换出**最早进入内存**的页面。
|
||||||
|
|
||||||
|
**执行过程**:
|
||||||
|
|
||||||
|
| 访问序列 | 页框0 | 页框1 | 页框2 | 缺页? | 淘汰队列 |
|
||||||
|
|---------|-------|-------|-------|-------|---------|
|
||||||
|
| **7** | 7 | - | - | 缺页 | [7] |
|
||||||
|
| **0** | 7 | 0 | - | 缺页 | [7,0] |
|
||||||
|
| **1** | 7 | 0 | 1 | 缺页 | [7,0,1] |
|
||||||
|
| **2** | **2** | 0 | 1 | 缺页 | [0,1,2] 淘汰7 |
|
||||||
|
| **0** | 2 | 0 | 1 | 命中 | [0,1,2] |
|
||||||
|
| **3** | 2 | **3** | 1 | 缺页 | [1,2,3] 淘汰0 |
|
||||||
|
| **0** | 2 | 3 | **0** | 缺页 | [2,3,0] 淘汰1 |
|
||||||
|
| **4** | **4** | 3 | 0 | 缺页 | [3,0,4] 淘汰2 |
|
||||||
|
| **2** | 4 | **2** | 0 | 缺页 | [0,4,2] 淘汰3 |
|
||||||
|
| **3** | 4 | 2 | **3** | 缺页 | [4,2,3] 淘汰0 |
|
||||||
|
| **0** | **0** | 2 | 3 | 缺页 | [2,3,0] 淘汰4 |
|
||||||
|
| **3** | 0 | 2 | 3 | 命中 | [2,3,0] |
|
||||||
|
| **2** | 0 | 2 | 3 | 命中 | [2,3,0] |
|
||||||
|
| **1** | 0 | **1** | 3 | 缺页 | [3,0,1] 淘汰2 |
|
||||||
|
| **2** | 0 | 1 | **2** | 缺页 | [0,1,2] 淘汰3 |
|
||||||
|
| **0** | 0 | 1 | 2 | 命中 | [0,1,2] |
|
||||||
|
| **1** | 0 | 1 | 2 | 命中 | [0,1,2] |
|
||||||
|
| **7** | **7** | 1 | 2 | 缺页 | [1,2,7] 淘汰0 |
|
||||||
|
| **0** | 7 | **0** | 2 | 缺页 | [2,7,0] 淘汰1 |
|
||||||
|
| **1** | 7 | 0 | **1** | 缺页 | [7,0,1] 淘汰2 |
|
||||||
|
|
||||||
|
**缺页次数: 15**,缺页率 = 15/20 = **75%**
|
||||||
|
|
||||||
|
> **Belady 异常**: FIFO 算法在某些情况下,增加物理页框数反而会导致缺页率**上升**。例如本例中 4 个页框时缺页次数可能比 3 个页框时更多。这是 FIFO 算法的独特缺陷。
|
||||||
|
|
||||||
|
### 3. LRU (Least Recently Used) 最近最久未使用
|
||||||
|
|
||||||
|
**策略**: 换出**最近最长时间没有被访问**的页面。
|
||||||
|
|
||||||
|
**实现方式**:
|
||||||
|
- **栈法**: 用一个栈保存页面号,访问某页时将其移到栈顶。栈底就是最久未使用的页面
|
||||||
|
- **计数器法**: 每个页面记录最后一次访问的时间,淘汰时间值最小的页面
|
||||||
|
|
||||||
|
**执行过程**:
|
||||||
|
|
||||||
|
| 访问序列 | 页框0 | 页框1 | 页框2 | 缺页? | 说明 |
|
||||||
|
|---------|-------|-------|-------|-------|------|
|
||||||
|
| **7** | 7 | - | - | 缺页 | |
|
||||||
|
| **0** | 7 | 0 | - | 缺页 | |
|
||||||
|
| **1** | 7 | 0 | 1 | 缺页 | |
|
||||||
|
| **2** | 2 | 0 | 1 | 缺页 | 淘汰7(最久未用) |
|
||||||
|
| **0** | 2 | 0 | 1 | 命中 | 0被访问,更新时间戳 |
|
||||||
|
| **3** | 2 | 0 | 3 | 缺页 | 淘汰1(最久未用) |
|
||||||
|
| **0** | 2 | 0 | 3 | 命中 | |
|
||||||
|
| **4** | 4 | 0 | 3 | 缺页 | 淘汰2(最久未用) |
|
||||||
|
| **2** | 4 | 0 | 2 | 缺页 | 淘汰3(最久未用) |
|
||||||
|
| **3** | 4 | 3 | 2 | 缺页 | 淘汰0(最久未用) |
|
||||||
|
| **0** | 0 | 3 | 2 | 缺页 | 淘汰4(最久未用) |
|
||||||
|
| **3** | 0 | 3 | 2 | 命中 | |
|
||||||
|
| **2** | 0 | 3 | 2 | 命中 | |
|
||||||
|
| **1** | 0 | 3 | 1 | 缺页 | 淘汰2(最久未用) |
|
||||||
|
| **2** | 0 | 2 | 1 | 缺页 | 淘汰3(最久未用) |
|
||||||
|
| **0** | 0 | 2 | 1 | 命中 | |
|
||||||
|
| **1** | 0 | 2 | 1 | 命中 | |
|
||||||
|
| **7** | 0 | 2 | 7 | 缺页 | 淘汰1(最久未用) |
|
||||||
|
| **0** | 0 | 2 | 7 | 命中 | |
|
||||||
|
| **1** | 1 | 2 | 7 | 缺页 | 淘汰0(最久未用) |
|
||||||
|
|
||||||
|
**缺页次数: 12**,缺页率 = 12/20 = **60%**
|
||||||
|
|
||||||
|
### 4. Clock (时钟 / 近似LRU)
|
||||||
|
|
||||||
|
**策略**: 将所有页面组成一个**循环链表**,每页有一个**访问位 R**。淘汰指针从当前位置开始扫描:
|
||||||
|
- R=1:将 R 置 0,指针前移(给第二次机会)
|
||||||
|
- R=0:淘汰该页
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph Clock["时钟置换算法 (循环链表)"]
|
||||||
|
direction LR
|
||||||
|
P0["页0<br/>R=1"]
|
||||||
|
P1["页1<br/>R=0"]
|
||||||
|
P2["页2<br/>R=1"]
|
||||||
|
P3["页3<br/>R=0"]
|
||||||
|
end
|
||||||
|
|
||||||
|
PTR["淘汰指针"] --> P1
|
||||||
|
|
||||||
|
P0 --> P1
|
||||||
|
P1 --> P2
|
||||||
|
P2 --> P3
|
||||||
|
P3 --> P0
|
||||||
|
|
||||||
|
style P1 fill:#ffcdd2,stroke:#c62828
|
||||||
|
style PTR fill:#fff3e0,stroke:#e65100
|
||||||
|
```
|
||||||
|
|
||||||
|
**执行示例**(简化):
|
||||||
|
- 指针指向页1 (R=0) → 淘汰页1,装入新页,指针前移
|
||||||
|
- 指针指向页2 (R=1) → R置0,指针前移
|
||||||
|
- 指针指向页3 (R=0) → 淘汰页3
|
||||||
|
|
||||||
|
**改进型Clock**: 同时考虑 R 和 D (修改位),优先淘汰 R=0 且 D=0 的页面(不需要写回外存)。
|
||||||
|
|
||||||
|
| 优先级 | R | D | 淘汰代价 |
|
||||||
|
|--------|---|---|---------|
|
||||||
|
| 最高 | 0 | 0 | 未访问未修改,直接覆盖 |
|
||||||
|
| | 0 | 1 | 未访问但已修改,需写回 |
|
||||||
|
| | 1 | 0 | 已访问未修改,再给机会 |
|
||||||
|
| 最低 | 1 | 1 | 已访问已修改,最不想淘汰 |
|
||||||
|
|
||||||
|
### 5. LFU (Least Frequently Used) 最少使用
|
||||||
|
|
||||||
|
**策略**: 换出**访问次数最少**的页面。
|
||||||
|
|
||||||
|
- 用计数器记录每个页面的访问次数
|
||||||
|
- 淘汰计数器值最小的页面
|
||||||
|
- **缺点**: 某页面过去被频繁访问但现在不再使用,仍难以被淘汰
|
||||||
|
- **实现开销**: 需要为每个页面维护计数器
|
||||||
|
|
||||||
|
### 算法对比
|
||||||
|
|
||||||
|
| 算法 | 策略 | 优点 | 缺点 | 是否有Belady异常 |
|
||||||
|
|------|------|------|------|----------------|
|
||||||
|
| **OPT** | 淘汰将来最久不用的 | 缺页率最低(理论最优) | 无法实现 | 无 |
|
||||||
|
| **FIFO** | 淘汰最早进入的 | 实现简单 | 缺页率高,有Belady异常 | **有** |
|
||||||
|
| **LRU** | 淘汰最近最久未用的 | 接近OPT,效果好 | 实现开销大(需时间戳或栈) | 无 |
|
||||||
|
| **Clock** | 给访问位R=0的淘汰 | LRU的近似,开销小 | 效果略逊于LRU | 无 |
|
||||||
|
| **LFU** | 淘汰访问次数最少的 | 考虑历史频率 | 对访问模式变化响应慢 | 无 |
|
||||||
|
|
||||||
|
### 算法效果对比(本例)
|
||||||
|
|
||||||
|
```
|
||||||
|
页面引用串: 7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1 (3个物理块)
|
||||||
|
|
||||||
|
┌──────────┬──────────┬──────────┐
|
||||||
|
│ 算法 │ 缺页次数 │ 缺页率 │
|
||||||
|
├──────────┼──────────┼──────────┤
|
||||||
|
│ OPT │ 9 │ 45% │ ← 理论最优
|
||||||
|
│ LRU │ 12 │ 60% │
|
||||||
|
│ FIFO │ 15 │ 75% │ ← 效果最差
|
||||||
|
└──────────┴──────────┴──────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、内存分配策略
|
||||||
|
|
||||||
|
### 物理页框分配方式
|
||||||
|
|
||||||
|
| 方式 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **平均分配** | 将可用页框平均分配给所有进程 |
|
||||||
|
| **按比例分配** | 按进程大小占总需求的比例分配 |
|
||||||
|
| **考虑优先权** | 高优先级进程分配更多页框 |
|
||||||
|
|
||||||
|
**按比例分配公式**:
|
||||||
|
|
||||||
|
$$a_i = \frac{s_i}{\sum s_j} \times m$$
|
||||||
|
|
||||||
|
其中 $s_i$ 是进程 $i$ 的页面数,$m$ 是可用页框总数。
|
||||||
|
|
||||||
|
### 置换策略
|
||||||
|
|
||||||
|
| 策略 | 说明 | 特点 |
|
||||||
|
|------|------|------|
|
||||||
|
| **固定分配局部置换** | 每个进程固定页框数,只能在自己的页面中置换 | 公平,但可能浪费 |
|
||||||
|
| **可变分配全局置换** | 从全局空闲页框池中分配,可换出任何进程的页面 | 灵活,但可能影响其他进程 |
|
||||||
|
| **可变分配局部置换** | 进程页框数可调,但只能在自己的页面中置换 | 折中方案 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、抖动与工作集
|
||||||
|
|
||||||
|
### 抖动 (Thrashing)
|
||||||
|
|
||||||
|
当进程分配到的页框数**不足以**容纳其工作集时,会频繁发生缺页,CPU 大量时间花在页面置换上,利用率急剧下降——这种现象称为**抖动**。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["进程增多"] --> B["每个进程分到的页框减少"]
|
||||||
|
B --> C["缺页率上升"]
|
||||||
|
C --> D["CPU利用率下降"]
|
||||||
|
D --> E["调度器增加进程数<br/>(以为CPU空闲)"]
|
||||||
|
E --> B
|
||||||
|
|
||||||
|
style C fill:#ffcdd2,stroke:#c62828
|
||||||
|
style D fill:#ffcdd2,stroke:#c62828
|
||||||
|
```
|
||||||
|
|
||||||
|
CPU 利用率随进程数的变化:
|
||||||
|
|
||||||
|
```
|
||||||
|
CPU利用率
|
||||||
|
↑
|
||||||
|
100%│ ╱‾‾‾‾╲
|
||||||
|
│ ╱ ╲
|
||||||
|
│ ╱ ╲ ← 抖动区域
|
||||||
|
│ ╱ ╲
|
||||||
|
│ ╱ ╲
|
||||||
|
│╱ ╲
|
||||||
|
└──────────────────────────→ 进程数
|
||||||
|
↑
|
||||||
|
最佳点
|
||||||
|
```
|
||||||
|
|
||||||
|
### 工作集 (Working Set)
|
||||||
|
|
||||||
|
**工作集**的定义:在最近 $\Delta$ 时间内,进程实际访问的页面集合。
|
||||||
|
|
||||||
|
$$W(t, \Delta) = \text{在时刻 } t-\Delta \text{ 到 } t \text{ 之间访问的页面集合}$$
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
|
||||||
|
页面访问序列: ... 2, 6, 1, 5, 7, 7, 7, 2, ...
|
||||||
|
|
||||||
|
设 $\Delta = 5$(最近 5 次访问),当前访问页面 2:
|
||||||
|
- 最近 5 次访问: {5, 7, 7, 7, 2}
|
||||||
|
- 工作集 = {2, 5, 7},大小 = 3
|
||||||
|
|
||||||
|
**工作集策略**: 为每个进程分配**不少于其工作集大小**的页框数,就可以避免抖动。
|
||||||
|
|
||||||
|
### 工作集模型的应用
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["监控每个进程的工作集"] --> B{"总工作集 ≤ 物理页框数?"}
|
||||||
|
B -->|"是"| C["正常运行,不会抖动"]
|
||||||
|
B -->|"否"| D["可能发生抖动"]
|
||||||
|
D --> E["挂起某些进程<br/>释放页框"]
|
||||||
|
E --> B
|
||||||
|
|
||||||
|
style D fill:#ffcdd2,stroke:#c62828
|
||||||
|
style C fill:#c8e6c9,stroke:#2e7d32
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、请求分段
|
||||||
|
|
||||||
|
### 基本思想
|
||||||
|
|
||||||
|
**请求分段**是虚拟存储器在[[15_段式存储管理|分段]]基础上的实现:
|
||||||
|
- 段不需要全部装入内存
|
||||||
|
- 访问到不在内存的段时,触发**缺段中断**,从外存调入
|
||||||
|
|
||||||
|
### 与请求分页的对比
|
||||||
|
|
||||||
|
| 对比项 | 请求分页 | 请求分段 |
|
||||||
|
|--------|---------|---------|
|
||||||
|
| 调入/调出单位 | 页面(固定大小) | 段(可变大小) |
|
||||||
|
| 碎片 | 内碎片 | 外碎片 |
|
||||||
|
| 共享 | 以页为粒度 | 以段为粒度 |
|
||||||
|
| 中断类型 | 缺页中断 | 缺段中断 |
|
||||||
|
| 实现复杂度 | 较简单 | 较复杂 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、完整计算示例
|
||||||
|
|
||||||
|
### 综合例题
|
||||||
|
|
||||||
|
> 某系统采用请求分页存储管理,页面大小为 4KB,物理内存有 4 个页框。某进程的页面访问序列为:**0, 1, 4, 2, 0, 2, 6, 5, 1, 2, 3, 2, 1, 2, 6, 5, 2, 1, 3, 6**。分别用 FIFO 和 LRU 算法计算缺页次数。
|
||||||
|
|
||||||
|
**FIFO 算法**:
|
||||||
|
|
||||||
|
| 访问 | 页框0 | 页框1 | 页框2 | 页框3 | 缺页? |
|
||||||
|
|------|-------|-------|-------|-------|-------|
|
||||||
|
| 0 | 0 | - | - | - | 缺页 |
|
||||||
|
| 1 | 0 | 1 | - | - | 缺页 |
|
||||||
|
| 4 | 0 | 1 | 4 | - | 缺页 |
|
||||||
|
| 2 | 0 | 1 | 4 | 2 | 缺页 |
|
||||||
|
| 0 | 0 | 1 | 4 | 2 | 命中 |
|
||||||
|
| 2 | 0 | 1 | 4 | 2 | 命中 |
|
||||||
|
| 6 | 6 | 1 | 4 | 2 | 缺页(淘汰0) |
|
||||||
|
| 5 | 6 | 5 | 4 | 2 | 缺页(淘汰1) |
|
||||||
|
| 1 | 6 | 5 | 1 | 2 | 缺页(淘汰4) |
|
||||||
|
| 2 | 6 | 5 | 1 | 2 | 命中 |
|
||||||
|
| 3 | 6 | 5 | 1 | 3 | 缺页(淘汰2) |
|
||||||
|
| 2 | 2 | 5 | 1 | 3 | 缺页(淘汰6) |
|
||||||
|
| 1 | 2 | 5 | 1 | 3 | 命中 |
|
||||||
|
| 2 | 2 | 5 | 1 | 3 | 命中 |
|
||||||
|
| 6 | 2 | 6 | 1 | 3 | 缺页(淘汰5) |
|
||||||
|
| 5 | 2 | 6 | 5 | 3 | 缺页(淘汰1) |
|
||||||
|
| 2 | 2 | 6 | 5 | 3 | 命中 |
|
||||||
|
| 1 | 2 | 6 | 5 | 1 | 缺页(淘汰3) |
|
||||||
|
| 3 | 3 | 6 | 5 | 1 | 缺页(淘汰2) |
|
||||||
|
| 6 | 3 | 6 | 5 | 1 | 命中 |
|
||||||
|
|
||||||
|
**FIFO 缺页次数: 14**
|
||||||
|
|
||||||
|
**LRU 算法**:
|
||||||
|
|
||||||
|
| 访问 | 页框0 | 页框1 | 页框2 | 页框3 | 缺页? |
|
||||||
|
|------|-------|-------|-------|-------|-------|
|
||||||
|
| 0 | 0 | - | - | - | 缺页 |
|
||||||
|
| 1 | 0 | 1 | - | - | 缺页 |
|
||||||
|
| 4 | 0 | 1 | 4 | - | 缺页 |
|
||||||
|
| 2 | 0 | 1 | 4 | 2 | 缺页 |
|
||||||
|
| 0 | 0 | 1 | 4 | 2 | 命中 |
|
||||||
|
| 2 | 0 | 1 | 4 | 2 | 命中 |
|
||||||
|
| 6 | 0 | 1 | 6 | 2 | 缺页(淘汰4) |
|
||||||
|
| 5 | 0 | 5 | 6 | 2 | 缺页(淘汰1) |
|
||||||
|
| 1 | 1 | 5 | 6 | 2 | 缺页(淘汰0) |
|
||||||
|
| 2 | 1 | 5 | 6 | 2 | 命中 |
|
||||||
|
| 3 | 1 | 5 | 3 | 2 | 缺页(淘汰6) |
|
||||||
|
| 2 | 1 | 5 | 3 | 2 | 命中 |
|
||||||
|
| 1 | 1 | 5 | 3 | 2 | 命中 |
|
||||||
|
| 2 | 1 | 5 | 3 | 2 | 命中 |
|
||||||
|
| 6 | 1 | 5 | 3 | 6 | 缺页(淘汰2) |
|
||||||
|
| 5 | 1 | 5 | 3 | 6 | 命中 |
|
||||||
|
| 2 | 2 | 5 | 3 | 6 | 缺页(淘汰1) |
|
||||||
|
| 1 | 2 | 1 | 3 | 6 | 缺页(淘汰5) |
|
||||||
|
| 3 | 2 | 1 | 3 | 6 | 命中 |
|
||||||
|
| 6 | 2 | 1 | 3 | 6 | 命中 |
|
||||||
|
|
||||||
|
**LRU 缺页次数: 12**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、小结
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
mindmap
|
||||||
|
root((虚拟存储器))
|
||||||
|
基本思想
|
||||||
|
虚拟空间>物理内存
|
||||||
|
部分装入
|
||||||
|
按需调入调出
|
||||||
|
局部性原理
|
||||||
|
时间局部性
|
||||||
|
空间局部性
|
||||||
|
行优先vs列优先
|
||||||
|
请求分页
|
||||||
|
页表扩展状态位
|
||||||
|
缺页中断处理
|
||||||
|
页面置换
|
||||||
|
置换算法
|
||||||
|
OPT理论最优
|
||||||
|
FIFO简单但有Belady异常
|
||||||
|
LRU效果好但开销大
|
||||||
|
Clock近似LRU
|
||||||
|
LFU最少使用
|
||||||
|
内存分配
|
||||||
|
平均/按比例/优先权
|
||||||
|
固定/可变分配
|
||||||
|
局部/全局置换
|
||||||
|
抖动与工作集
|
||||||
|
频繁缺页=抖动
|
||||||
|
工作集=近期访问页面集
|
||||||
|
帧数≥工作集大小
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 思考题
|
||||||
|
|
||||||
|
1. **概念理解**: 为什么说虚拟存储器的基础是局部性原理?如果没有局部性,虚拟存储器还能工作吗?
|
||||||
|
|
||||||
|
2. **算法对比**: 对于页面引用串 1,2,3,4,1,2,5,1,2,3,4,5(3个物理块),分别计算 OPT、FIFO、LRU 的缺页次数。是否存在Belady异常?
|
||||||
|
|
||||||
|
3. **工作集**: 某进程页面访问序列为 2,6,1,5,7,7,7,2,5,1,3,1,5,7,2,5,设 $\Delta=5$,求各时刻的工作集大小。
|
||||||
|
|
||||||
|
4. **抖动分析**: 某系统物理内存可容纳 10 个工作集页面。目前有 5 个进程,工作集大小分别为 {3,2,2,1,3}。此时再增加一个工作集大小为 3 的进程,系统是否会发生抖动?
|
||||||
|
|
||||||
|
5. **综合题**: 在请求分页系统中,页面大小 4KB,页表项 4 字节,虚拟地址 32 位,物理地址 32 位。
|
||||||
|
- 一级页表有多大?
|
||||||
|
- 如果采用二级页表,页目录和页表各多大?
|
||||||
|
- 如果 TLB 命中率 95%,TLB 访问 10ns,内存访问 100ns,有效访问时间是多少?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 关联笔记
|
||||||
|
|
||||||
|
- [[14_分页存储管理]] — 分页基础、页表结构、地址变换
|
||||||
|
- [[15_段式存储管理]] — 分段思想、段页式
|
||||||
|
- [[11_处理机调度]] — 进程调度与抖动的关系
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**上一讲**: [[15_段式存储管理]]
|
||||||
|
**目录**: [[00_课程导航]]
|
||||||
937
操作系统/17_IO系统/17_IO系统.md
Normal file
937
操作系统/17_IO系统/17_IO系统.md
Normal file
@@ -0,0 +1,937 @@
|
|||||||
|
# 17 I/O系统
|
||||||
|
|
||||||
|
> **课程**:操作系统
|
||||||
|
> **关联章节**:[[01_系统运行机制]] | [[04_文件IO编程]] | [[05_磁盘空间管理]]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、I/O系统概述
|
||||||
|
|
||||||
|
### 1.1 I/O系统的基本概念
|
||||||
|
|
||||||
|
I/O系统(输入/输出系统)是操作系统中负责管理**外部设备**与**主机(CPU和内存)**之间数据传输的子系统。它是计算机系统中最复杂的组成部分之一。
|
||||||
|
|
||||||
|
> **核心任务**:屏蔽各种I/O设备的硬件差异,为上层提供**统一、简洁**的I/O接口,使用户程序能够方便地使用各种外部设备。
|
||||||
|
|
||||||
|
I/O系统需要解决的关键问题:
|
||||||
|
- 设备的多样性与统一管理
|
||||||
|
- CPU与I/O设备之间的速度差异
|
||||||
|
- 设备的分配与回收
|
||||||
|
- I/O操作的高效执行
|
||||||
|
|
||||||
|
### 1.2 I/O设备分类
|
||||||
|
|
||||||
|
#### 按数据传输单位分类
|
||||||
|
|
||||||
|
| 类型 | 特征 | 典型设备 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **字符设备** | 以字符为单位传输,速率低,不可寻址 | 键盘、鼠标、串口 |
|
||||||
|
| **块设备** | 以数据块为单位传输,速率高,可寻址 | 磁盘、磁带、光盘 |
|
||||||
|
|
||||||
|
#### 按传输速率分类
|
||||||
|
|
||||||
|
| 类型 | 速率范围 | 典型设备 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| **低速设备** | 几字节/s ~ 几百B/s | 鼠标、键盘、调制解调器 |
|
||||||
|
| **中速设备** | 几KB/s ~ 几MB/s | 打印机、扫描仪 |
|
||||||
|
| **高速设备** | 几MB/s ~ 几GB/s | 磁盘、磁带、网络接口 |
|
||||||
|
|
||||||
|
#### 按使用特性分类
|
||||||
|
|
||||||
|
| 类型 | 说明 | 典型设备 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **存储设备** | 用于永久保存信息 | 磁盘、磁带、光盘 |
|
||||||
|
| **输入设备** | 用于向计算机输入信息 | 键盘、鼠标、扫描仪 |
|
||||||
|
| **输出设备** | 用于将计算机信息输出 | 显示器、打印机 |
|
||||||
|
| **交互设备** | 既能输入又能输出 | 触摸屏、网络接口 |
|
||||||
|
|
||||||
|
### 1.3 I/O设备的组成
|
||||||
|
|
||||||
|
I/O设备一般由**机械部分**和**电子部分**组成:
|
||||||
|
|
||||||
|
```
|
||||||
|
I/O设备 = 设备(Device)+ 设备控制器(Device Controller)
|
||||||
|
```
|
||||||
|
|
||||||
|
- **设备(Device)**:执行实际的I/O操作(如磁盘的旋转和读写)
|
||||||
|
- **设备控制器(Device Controller)**:电子部件,负责控制设备的物理操作,是CPU与设备之间的接口
|
||||||
|
|
||||||
|
> **设备控制器的功能**:
|
||||||
|
> 1. 接收和识别CPU发来的命令
|
||||||
|
> 2. 实现CPU与控制器、控制器与设备之间的数据交换
|
||||||
|
> 3. 记录设备的状态供CPU查询
|
||||||
|
> 4. 识别设备地址(端口地址)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、I/O控制方式
|
||||||
|
|
||||||
|
I/O控制方式的发展目标:**减少CPU对I/O过程的干预**,提高CPU与I/O设备的**并行操作**程度。
|
||||||
|
|
||||||
|
### 2.1 程序直接控制方式(Programmed I/O)
|
||||||
|
|
||||||
|
#### 基本思想
|
||||||
|
|
||||||
|
CPU通过不断**轮询(polling)**设备状态寄存器,直接控制I/O操作的全过程。
|
||||||
|
|
||||||
|
#### 工作流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant CPU
|
||||||
|
participant 控制器
|
||||||
|
participant 设备
|
||||||
|
|
||||||
|
CPU->>控制器: 1. 发送I/O命令
|
||||||
|
CPU->>控制器: 2. 读取状态寄存器
|
||||||
|
loop 轮询等待
|
||||||
|
CPU->>控制器: 检查忙/完成位
|
||||||
|
控制器-->>CPU: 返回状态(忙)
|
||||||
|
end
|
||||||
|
控制器-->>CPU: 返回状态(完成)
|
||||||
|
CPU->>控制器: 3. 读取数据寄存器
|
||||||
|
控制器-->>CPU: 数据
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 优缺点
|
||||||
|
|
||||||
|
| 优点 | 缺点 |
|
||||||
|
|------|------|
|
||||||
|
| 实现简单 | CPU利用率极低(忙等待) |
|
||||||
|
| 硬件要求低 | CPU与设备串行工作 |
|
||||||
|
| 易于理解 | 不适用于多设备系统 |
|
||||||
|
|
||||||
|
> [!note] 关键特征
|
||||||
|
> CPU在整个I/O过程中**一直参与**,以**轮询方式**检查设备状态,CPU与设备完全**串行**工作。
|
||||||
|
|
||||||
|
### 2.2 中断驱动方式(Interrupt-Driven I/O)
|
||||||
|
|
||||||
|
#### 基本思想
|
||||||
|
|
||||||
|
CPU发出I/O命令后,**不需要持续轮询**,可以去执行其他进程。当设备完成操作后,通过**中断**通知CPU。
|
||||||
|
|
||||||
|
#### 工作流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant CPU
|
||||||
|
participant 控制器
|
||||||
|
participant 设备
|
||||||
|
|
||||||
|
CPU->>控制器: 1. 发送I/O命令
|
||||||
|
Note over CPU: CPU转去执行其他进程
|
||||||
|
控制器->>设备: 启动设备操作
|
||||||
|
设备->>控制器: 操作完成
|
||||||
|
控制器->>CPU: 2. 发出中断请求
|
||||||
|
Note over CPU: CPU响应中断
|
||||||
|
CPU->>控制器: 3. 读取数据
|
||||||
|
控制器-->>CPU: 数据
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 优缺点
|
||||||
|
|
||||||
|
| 优点 | 缺点 |
|
||||||
|
|------|------|
|
||||||
|
| CPU利用率提高 | 每次传输以字/字节为单位 |
|
||||||
|
| CPU与设备可并行工作 | 频繁中断开销大 |
|
||||||
|
| 支持多设备 | 数据仍需经过CPU中转 |
|
||||||
|
|
||||||
|
> [!note] 与程序直接控制方式的对比
|
||||||
|
> 中断驱动方式中,CPU在**启动I/O后**可执行其他任务,设备完成时通过**中断**通知CPU,实现了CPU与设备的**部分并行**。但每次数据传输都需要CPU干预。
|
||||||
|
|
||||||
|
### 2.3 DMA方式(Direct Memory Access)
|
||||||
|
|
||||||
|
#### 基本思想
|
||||||
|
|
||||||
|
引入**DMA控制器**,在设备与内存之间开辟**直接数据通路**,数据传输不需要经过CPU中转。CPU只需在传输**开始**和**结束**时介入。
|
||||||
|
|
||||||
|
#### DMA控制器的组成
|
||||||
|
|
||||||
|
```
|
||||||
|
DMA控制器寄存器:
|
||||||
|
┌─────────────────────────────────┐
|
||||||
|
│ MAR(内存地址寄存器) │ → 数据在内存中的目标地址
|
||||||
|
│ DC (数据计数器) │ → 剩余要传输的字节数
|
||||||
|
│ DR (数据寄存器) │ → 暂存传输的数据
|
||||||
|
│ CR (命令/状态寄存器) │ → 存放CPU发来的命令和状态
|
||||||
|
└─────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 工作流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant CPU
|
||||||
|
participant DMA
|
||||||
|
participant 内存
|
||||||
|
participant 设备
|
||||||
|
|
||||||
|
CPU->>DMA: 1. 设置MAR、DC、CR(发送命令)
|
||||||
|
Note over CPU: CPU转去执行其他进程
|
||||||
|
DMA->>设备: 启动设备
|
||||||
|
loop 块传输(硬件自动)
|
||||||
|
设备->>DMA: 读入一个字
|
||||||
|
DMA->>内存: 写入内存(MAR)
|
||||||
|
Note over DMA: MAR++, DC--
|
||||||
|
end
|
||||||
|
DMA->>CPU: 2. 传输完成,发出中断
|
||||||
|
CPU->>DMA: 3. 处理完成中断
|
||||||
|
```
|
||||||
|
|
||||||
|
#### DMA与中断驱动的对比
|
||||||
|
|
||||||
|
| 比较项 | 中断驱动 | DMA |
|
||||||
|
|--------|----------|-----|
|
||||||
|
| 数据传输单位 | 字/字节 | **数据块** |
|
||||||
|
| 数据是否经过CPU | **是** | **否**(直接内存访问) |
|
||||||
|
| 中断频率 | 每个字/字节一次 | **每块一次** |
|
||||||
|
| CPU干预程度 | 每次传输都干预 | 仅开始和结束干预 |
|
||||||
|
|
||||||
|
> [!important] DMA的核心优势
|
||||||
|
> 数据在**设备和内存之间**直接传输,不经过CPU,以**块**为单位传输,大大减少了CPU的干预次数。
|
||||||
|
|
||||||
|
### 2.4 通道方式(Channel I/O)
|
||||||
|
|
||||||
|
#### 基本思想
|
||||||
|
|
||||||
|
通道是一种**专用处理器**(I/O处理器),有自己的**指令系统**(通道指令)和**程序**(通道程序)。CPU只需发出一条I/O指令,通道就能**自主完成**整个数据传输过程。
|
||||||
|
|
||||||
|
#### 通道的类型
|
||||||
|
|
||||||
|
| 类型 | 特征 | 传输单位 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **字节多路通道** | 以字节为单位交叉传输多个设备 | 字节 |
|
||||||
|
| **数组选择通道** | 一次只服务一个设备,传输速率高 | 数据块 |
|
||||||
|
| **数组多路通道** | 上述两种的结合,兼顾效率和利用率 | 数据块 |
|
||||||
|
|
||||||
|
#### 工作流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant CPU
|
||||||
|
participant 通道
|
||||||
|
participant 控制器
|
||||||
|
participant 设备
|
||||||
|
|
||||||
|
CPU->>通道: 1. 发送通道程序地址
|
||||||
|
Note over CPU: CPU完全转去执行其他进程
|
||||||
|
通道->>通道: 取通道指令执行
|
||||||
|
通道->>控制器: 控制设备操作
|
||||||
|
loop 通道自主执行
|
||||||
|
通道->>内存: 数据传输(无需CPU干预)
|
||||||
|
end
|
||||||
|
通道->>CPU: 2. 传输完成,发出中断
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 通道与DMA的对比
|
||||||
|
|
||||||
|
| 比较项 | DMA | 通道 |
|
||||||
|
|--------|-----|------|
|
||||||
|
| 控制能力 | 受CPU控制 | **自主执行**通道程序 |
|
||||||
|
| 并行度 | 一个DMA对应一类设备 | 一个通道可控制**多个**设备 |
|
||||||
|
| CPU干预 | 需要CPU设置传输参数 | 仅需CPU启动通道 |
|
||||||
|
| 灵活性 | 较低 | **高**(可编程) |
|
||||||
|
|
||||||
|
> [!tip] I/O控制方式演进总结
|
||||||
|
> | 方式 | CPU干预频率 | 数据单位 | 并行程度 |
|
||||||
|
> |------|-------------|----------|----------|
|
||||||
|
> | 程序直接控制 | 每个字 | 字 | 无并行 |
|
||||||
|
> | 中断驱动 | 每个字 | 字 | 部分并行 |
|
||||||
|
> | DMA | 每个块 | 块 | 较高并行 |
|
||||||
|
> | 通道 | 仅启动/结束 | 一组块 | 高度并行 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、I/O软件层次结构
|
||||||
|
|
||||||
|
### 3.1 层次结构概览
|
||||||
|
|
||||||
|
I/O软件采用**分层设计**思想,每一层利用下层提供的服务,为上层提供更高级的服务。
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
subgraph 用户空间
|
||||||
|
A["用户层I/O软件<br/>(库函数: printf, scanf)"]
|
||||||
|
end
|
||||||
|
subgraph 内核空间
|
||||||
|
B["设备独立性软件<br/>(统一接口、缓冲、分配)"]
|
||||||
|
C["设备驱动程序<br/>(控制具体设备)"]
|
||||||
|
D["中断处理程序<br/>(响应设备中断)"]
|
||||||
|
end
|
||||||
|
subgraph 硬件
|
||||||
|
E["设备控制器 / 设备"]
|
||||||
|
end
|
||||||
|
|
||||||
|
A -->|"系统调用"| B
|
||||||
|
B -->|"调用驱动"| C
|
||||||
|
C -->|"启动I/O"| E
|
||||||
|
E -->|"中断"| D
|
||||||
|
D -->|"唤醒驱动"| C
|
||||||
|
C -->|"唤醒上层"| B
|
||||||
|
B -->|"返回结果"| A
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style B fill:#fff3e0
|
||||||
|
style C fill:#fce4ec
|
||||||
|
style D fill:#f3e5f5
|
||||||
|
style E fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 各层功能详解
|
||||||
|
|
||||||
|
#### 第一层:中断处理程序
|
||||||
|
|
||||||
|
**位置**:最底层,直接与硬件交互
|
||||||
|
**功能**:
|
||||||
|
- 响应设备中断请求
|
||||||
|
- 保存被中断进程的现场
|
||||||
|
- 分析中断原因,转入相应的中断处理
|
||||||
|
- 唤醒等待I/O完成的进程
|
||||||
|
- 恢复现场,返回被中断的进程
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 中断处理程序伪代码
|
||||||
|
void disk_interrupt_handler() {
|
||||||
|
save_context(); // 保存现场
|
||||||
|
wake_up(device_waiter); // 唤醒等待该设备的进程
|
||||||
|
schedule(); // 可能触发进程调度
|
||||||
|
restore_context(); // 恢复现场
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 第二层:设备驱动程序
|
||||||
|
|
||||||
|
**位置**:紧邻中断处理程序之上
|
||||||
|
**功能**:
|
||||||
|
- 将上层的抽象I/O请求转换为对**具体设备控制器**的操作
|
||||||
|
- 设置设备控制器的寄存器
|
||||||
|
- 启动设备进行I/O操作
|
||||||
|
- 处理设备中断,检查操作结果
|
||||||
|
|
||||||
|
> [!note] 设备驱动程序的特点
|
||||||
|
> - 每类设备对应一个**设备驱动程序**
|
||||||
|
> - 是操作系统中**了解设备控制器细节**的唯一模块
|
||||||
|
> - 通常由**设备制造商**提供
|
||||||
|
|
||||||
|
#### 第三层:设备独立性软件
|
||||||
|
|
||||||
|
**位置**:设备驱动程序之上
|
||||||
|
**功能**:
|
||||||
|
- 提供**统一的I/O接口**,屏蔽设备差异
|
||||||
|
- 实现**缓冲管理**(缓冲区的分配与回收)
|
||||||
|
- 实现**设备分配与回收**
|
||||||
|
- 实现**逻辑设备名到物理设备名**的映射
|
||||||
|
- 提供**差错处理**机制
|
||||||
|
- 提供**设备保护**(防止非法访问)
|
||||||
|
|
||||||
|
> **设备独立性**:应用程序使用**逻辑设备名**请求I/O,由系统将其映射到**物理设备**。这样,更换物理设备不影响程序运行。
|
||||||
|
|
||||||
|
```
|
||||||
|
应用程序: read(fd, buf, size)
|
||||||
|
↓ 逻辑设备名
|
||||||
|
设备独立性软件: 查逻辑设备表 → 物理设备
|
||||||
|
↓
|
||||||
|
设备驱动程序: 操作具体设备控制器
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 第四层:用户层I/O软件
|
||||||
|
|
||||||
|
**位置**:最上层,用户空间
|
||||||
|
**功能**:
|
||||||
|
- 提供**库函数**接口(如C语言的 `printf`、`scanf`、`read`、`write`)
|
||||||
|
- 对用户数据进行**格式化**处理
|
||||||
|
- 通过**系统调用**进入内核
|
||||||
|
|
||||||
|
### 3.3 一次完整的I/O操作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户调用 printf("Hello")
|
||||||
|
→ 用户层:格式化数据,调用 write() 系统调用
|
||||||
|
→ 设备独立性软件:查设备表,分配缓冲区,确定物理设备
|
||||||
|
→ 设备驱动程序:向设备控制器发送命令
|
||||||
|
→ 设备控制器:启动设备进行输出
|
||||||
|
→ 设备完成操作,发出中断
|
||||||
|
→ 中断处理程序:唤醒等待进程
|
||||||
|
→ 设备驱动程序:检查操作结果
|
||||||
|
→ 设备独立性软件:释放缓冲区,返回结果
|
||||||
|
→ 用户层:系统调用返回
|
||||||
|
用户程序继续执行
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、缓冲管理
|
||||||
|
|
||||||
|
### 4.1 引入缓冲的原因
|
||||||
|
|
||||||
|
1. **缓和CPU与I/O设备速度不匹配**的矛盾
|
||||||
|
2. **减少对CPU的中断频率**,放宽对中断响应时间的限制
|
||||||
|
3. **提高CPU与I/O设备之间的并行性**
|
||||||
|
|
||||||
|
### 4.2 单缓冲(Single Buffer)
|
||||||
|
|
||||||
|
#### 基本思想
|
||||||
|
|
||||||
|
系统为设备分配**一个**缓冲区。
|
||||||
|
|
||||||
|
#### 工作方式
|
||||||
|
|
||||||
|
处理一块数据的时间 = **max(C, T) + M**
|
||||||
|
- C:CPU处理一块数据的时间
|
||||||
|
- T:设备将一块数据传送到缓冲区的时间
|
||||||
|
- M:缓冲区数据传送到用户区的时间
|
||||||
|
|
||||||
|
```
|
||||||
|
设备 → [缓冲区] → 用户区 → CPU处理
|
||||||
|
```
|
||||||
|
|
||||||
|
> **分析**:当C > T时,CPU处理慢,设备需等待;当T > C时,设备传输慢,CPU需等待。两者不能完全并行。
|
||||||
|
|
||||||
|
### 4.3 双缓冲(Double Buffer)
|
||||||
|
|
||||||
|
#### 基本思想
|
||||||
|
|
||||||
|
系统为设备分配**两个**缓冲区,实现**并行操作**。
|
||||||
|
|
||||||
|
#### 工作方式
|
||||||
|
|
||||||
|
```
|
||||||
|
设备 → [缓冲区1] → 用户区 缓冲区2 ← 设备
|
||||||
|
设备 → [缓冲区2] → 用户区 缓冲区1 ← 设备
|
||||||
|
```
|
||||||
|
|
||||||
|
处理一块数据的时间 = **max(C, T)**
|
||||||
|
|
||||||
|
> [!tip] 双缓冲的优势
|
||||||
|
> 当一个缓冲区的数据传送到用户区时,另一个缓冲区可同时接收设备的新数据,实现了**设备与CPU的并行**。
|
||||||
|
|
||||||
|
### 4.4 循环缓冲(Circular Buffer)
|
||||||
|
|
||||||
|
#### 基本思想
|
||||||
|
|
||||||
|
系统分配**多个**大小相等的缓冲区,组成**循环队列**。
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───┐ ┌───┐ ┌───┐ ┌───┐
|
||||||
|
输入 → │ B1 │ → │ B2 │ → │ B3 │ → │ B4 │ → 输出
|
||||||
|
└───┘ └───┘ └───┘ └───┘
|
||||||
|
↑ ↓
|
||||||
|
└────────────────────────┘
|
||||||
|
循环使用
|
||||||
|
```
|
||||||
|
|
||||||
|
- **输入指针(in)**:指向下一个可放入数据的缓冲区
|
||||||
|
- **输出指针(out)**:指向下一个可取出数据的缓冲区
|
||||||
|
|
||||||
|
> **适用场景**:I/O速度与处理速度差异较大的系统。
|
||||||
|
|
||||||
|
### 4.5 缓冲池(Buffer Pool)
|
||||||
|
|
||||||
|
#### 基本思想
|
||||||
|
|
||||||
|
系统从公用内存中分配**一组**缓冲区,组成缓冲池,供**多个进程共享**使用。
|
||||||
|
|
||||||
|
#### 缓冲区队列
|
||||||
|
|
||||||
|
缓冲池中的缓冲区按用途分为三个队列:
|
||||||
|
|
||||||
|
| 队列 | 用途 |
|
||||||
|
|------|------|
|
||||||
|
| **空缓冲队列(emq)** | 尚未使用的空缓冲区 |
|
||||||
|
| **输入队列(inq)** | 装满输入数据的缓冲区 |
|
||||||
|
| **输出队列(outq)** | 装满输出数据的缓冲区 |
|
||||||
|
|
||||||
|
#### 四种工作缓冲区
|
||||||
|
|
||||||
|
| 缓冲区类型 | 功能 |
|
||||||
|
|------------|------|
|
||||||
|
| **收容输入(hin)** | 从空缓冲队列取缓冲区,接收输入数据,挂入输入队列 |
|
||||||
|
| **提取输入(sin)** | 从输入队列取缓冲区,供CPU提取数据,释放回空缓冲队列 |
|
||||||
|
| **收容输出(hout)** | 从空缓冲队列取缓冲区,接收CPU输出数据,挂入输出队列 |
|
||||||
|
| **提取输出(sout)** | 从输出队列取缓冲区,供设备提取数据,释放回空缓冲队列 |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph 缓冲池
|
||||||
|
空缓冲队列["空缓冲队列 (emq)"]
|
||||||
|
输入队列["输入队列 (inq)"]
|
||||||
|
输出队列["输出队列 (outq)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
输入设备 -->|"收容输入(hin)"| 空缓冲队列
|
||||||
|
空缓冲队列 -->|"收容输入(hin)"| 输入队列
|
||||||
|
输入队列 -->|"提取输入(sin)"| CPU
|
||||||
|
CPU -->|"提取输入(sin)"| 空缓冲队列
|
||||||
|
|
||||||
|
CPU -->|"收容输出(hout)"| 空缓冲队列
|
||||||
|
空缓冲队列 -->|"收容输出(hout)"| 输出队列
|
||||||
|
输出队列 -->|"提取输出(sout)"| 输出设备
|
||||||
|
输出设备 -->|"提取输出(sout)"| 空缓冲队列
|
||||||
|
|
||||||
|
style 空缓冲队列 fill:#e8f5e9
|
||||||
|
style 输入队列 fill:#e1f5fe
|
||||||
|
style 输出队列 fill:#fff3e0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、设备分配与回收
|
||||||
|
|
||||||
|
### 5.1 设备分配中的数据结构
|
||||||
|
|
||||||
|
#### 设备控制表(DCT)
|
||||||
|
|
||||||
|
每个设备一张,记录设备的状态和属性:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 设备控制表 (DCT) │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ 设备标识符 (设备ID) │
|
||||||
|
│ 设备类型 │
|
||||||
|
│ 设备状态:忙/空闲/故障 │
|
||||||
|
│ 指向控制器表的指针 (COCT) │
|
||||||
|
│ 重复执行次数/定时器 │
|
||||||
|
│ 设备队列的队首指针 │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 控制器控制表(COCT)
|
||||||
|
|
||||||
|
每个设备控制器一张:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 控制器控制表 (COCT) │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ 控制器标识符 │
|
||||||
|
│ 控制器状态 │
|
||||||
|
│ 指向通道表的指针 (CHCT) │
|
||||||
|
│ 控制器队列的队首指针 │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 通道控制表(CHCT)
|
||||||
|
|
||||||
|
每个通道一张:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 通道控制表 (CHCT) │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ 通道标识符 │
|
||||||
|
│ 通道状态 │
|
||||||
|
│ 通道连接的控制器列表 │
|
||||||
|
│ 通道队列的队首指针 │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 系统设备表(SDT)
|
||||||
|
|
||||||
|
整个系统一张,记录所有设备的信息:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────┐
|
||||||
|
│ 系统设备表 (SDT) │
|
||||||
|
├─────────────────────────────┤
|
||||||
|
│ 设备类 │
|
||||||
|
│ 设备标识符 │
|
||||||
|
│ DCT指针 │
|
||||||
|
│ 驱动程序入口地址 │
|
||||||
|
└─────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5.2 设备分配原则
|
||||||
|
|
||||||
|
| 考虑因素 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| **固有属性** | 独占设备、共享设备、虚拟设备 |
|
||||||
|
| **安全性** | 避免死锁(参考 [[12_死锁]]) |
|
||||||
|
| **效率** | 充分利用设备,提高系统吞吐量 |
|
||||||
|
|
||||||
|
### 5.3 独占设备、共享设备与SPOOLing
|
||||||
|
|
||||||
|
#### 独占设备
|
||||||
|
|
||||||
|
同一时刻**只能被一个进程**使用(如打印机)。
|
||||||
|
|
||||||
|
#### 共享设备
|
||||||
|
|
||||||
|
可被**多个进程交替**使用(如磁盘)。
|
||||||
|
|
||||||
|
#### SPOOLing技术(假脱机技术)
|
||||||
|
|
||||||
|
将**独占设备**改造为**共享设备**的技术,是虚拟设备的典型实现。
|
||||||
|
|
||||||
|
```
|
||||||
|
输入设备 ──→ 输入井 ──→ 内存 ──→ 输出井 ──→ 输出设备
|
||||||
|
(输入进程) (磁盘) (用户进程) (磁盘) (输出进程)
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!important] SPOOLing系统的核心思想
|
||||||
|
> 在磁盘上开辟**输入井**和**输出井**,用**输入进程**模拟脱机输入,用**输出进程**模拟脱机输出。用户进程不直接操作独占设备,而是与磁盘缓冲区交互,从而实现**独占设备的共享**。
|
||||||
|
|
||||||
|
**经典应用**:共享打印机——多个进程的打印请求先存入输出井,由输出进程依次输出到打印机。
|
||||||
|
|
||||||
|
### 5.4 设备分配流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[进程请求设备] --> B{分配设备}
|
||||||
|
B -->|独占设备| C{设备空闲?}
|
||||||
|
C -->|是| D[分配设备, 修改DCT]
|
||||||
|
C -->|否| E[进程阻塞, 排入设备等待队列]
|
||||||
|
B -->|共享设备| F[直接分配]
|
||||||
|
D --> G{分配控制器}
|
||||||
|
F --> G
|
||||||
|
G --> H{分配通道}
|
||||||
|
H --> I[分配成功, 启动I/O]
|
||||||
|
I --> J[I/O完成]
|
||||||
|
J --> K[回收通道]
|
||||||
|
K --> L[回收控制器]
|
||||||
|
L --> M[回收设备]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、磁盘调度算法
|
||||||
|
|
||||||
|
### 6.1 磁盘结构与性能参数
|
||||||
|
|
||||||
|
#### 磁盘的物理结构
|
||||||
|
|
||||||
|
```
|
||||||
|
柱面 (Cylinder)
|
||||||
|
↓
|
||||||
|
┌───────────────────────┐
|
||||||
|
│ 磁头0 磁头1 磁头2 │ ← 磁头 (Head)
|
||||||
|
│ ───── ───── ───── │
|
||||||
|
│ 磁道0 磁道0 磁道0 │ ← 磁道 (Track)
|
||||||
|
│ 磁道1 磁道1 磁道1 │
|
||||||
|
│ ... ... ... │
|
||||||
|
│ 磁道n 磁道n 磁道n │
|
||||||
|
└───────────────────────┘
|
||||||
|
盘面 盘面 盘面
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 磁盘访问时间组成
|
||||||
|
|
||||||
|
```
|
||||||
|
总访问时间 T = 寻道时间 Ts + 旋转延迟 Tr + 传输时间 Tt
|
||||||
|
```
|
||||||
|
|
||||||
|
| 参数 | 含义 | 典型值 |
|
||||||
|
|------|------|--------|
|
||||||
|
| **寻道时间 Ts** | 磁头移动到目标磁道的时间 | 几ms ~ 几十ms(**最主要**) |
|
||||||
|
| **旋转延迟 Tr** | 等待目标扇区旋转到磁头下的时间 | 转速7200rpm → 平均4.17ms |
|
||||||
|
| **传输时间 Tt** | 数据读写时间 | 较短 |
|
||||||
|
|
||||||
|
> [!important] 优化重点
|
||||||
|
> **寻道时间**在总访问时间中占比最大,因此磁盘调度算法主要优化**寻道时间**。
|
||||||
|
|
||||||
|
### 6.2 常见磁盘调度算法
|
||||||
|
|
||||||
|
#### 示例磁盘请求序列
|
||||||
|
|
||||||
|
假设当前磁头位置在**磁道53**,磁道范围0~199,请求队列:
|
||||||
|
|
||||||
|
```
|
||||||
|
请求队列: 98, 183, 37, 122, 14, 124, 65, 67
|
||||||
|
方向: 磁道号增加方向 (向磁道号大的方向移动)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### FCFS(先来先服务)
|
||||||
|
|
||||||
|
**原则**:按请求到达的先后顺序依次服务。
|
||||||
|
|
||||||
|
```
|
||||||
|
磁头移动顺序: 53 → 98 → 183 → 37 → 122 → 14 → 124 → 65 → 67
|
||||||
|
|
||||||
|
移动距离:
|
||||||
|
|53-98| + |98-183| + |183-37| + |37-122| + |122-14| + |14-124| + |124-65| + |65-67|
|
||||||
|
= 45 + 85 + 146 + 85 + 108 + 110 + 59 + 2
|
||||||
|
= 640 磁道
|
||||||
|
|
||||||
|
平均寻道长度 = 640 / 8 = 80 磁道
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!note] FCFS特点
|
||||||
|
> - **优点**:公平、简单、不会产生饥饿
|
||||||
|
> - **缺点**:寻道距离大,性能差(磁头来回大幅移动)
|
||||||
|
> - **适用**:请求量少或对公平性要求高的场景
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### SSTF(最短寻道时间优先)
|
||||||
|
|
||||||
|
**原则**:选择距离当前磁头位置**最近**的请求优先服务。
|
||||||
|
|
||||||
|
```
|
||||||
|
当前位置: 53
|
||||||
|
请求队列: 98, 183, 37, 122, 14, 124, 65, 67
|
||||||
|
|
||||||
|
Step 1: 当前53, 最近是65(距离12)或37(距离16), 选65
|
||||||
|
Step 2: 当前65, 最近是67(距离2), 选67
|
||||||
|
Step 3: 当前67, 最近是37(距离30)或98(距离31), 选37
|
||||||
|
Step 4: 当前37, 最近是14(距离23), 选14
|
||||||
|
Step 5: 当前14, 最近是98(距离84), 选98
|
||||||
|
Step 6: 当前98, 最近是122(距离24), 选122
|
||||||
|
Step 7: 当前122, 最近是124(距离2), 选124
|
||||||
|
Step 8: 当前124, 最近是183(距离59), 选183
|
||||||
|
|
||||||
|
移动顺序: 53 → 65 → 67 → 37 → 14 → 98 → 122 → 124 → 183
|
||||||
|
|
||||||
|
移动距离:
|
||||||
|
|53-65| + |65-67| + |67-37| + |37-14| + |14-98| + |98-122| + |122-124| + |124-183|
|
||||||
|
= 12 + 2 + 30 + 23 + 84 + 24 + 2 + 59
|
||||||
|
= 236 磁道
|
||||||
|
|
||||||
|
平均寻道长度 = 236 / 8 = 29.5 磁道
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!warning] SSTF的"饥饿"问题
|
||||||
|
> - **优点**:平均寻道时间短
|
||||||
|
> - **缺点**:可能导致某些请求**长期得不到服务**(饥饿),尤其是距离较远的磁道请求
|
||||||
|
> - **原因**:距离远的请求总被距离近的新请求"插队"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### SCAN(电梯算法/扫描算法)
|
||||||
|
|
||||||
|
**原则**:磁头沿**一个方向**移动,依次服务沿途的请求,到达磁道尽头后**反向**移动。
|
||||||
|
|
||||||
|
```
|
||||||
|
当前位置: 53, 方向: 磁道号增加
|
||||||
|
|
||||||
|
磁头移动: 53 → 65 → 67 → 98 → 122 → 124 → 183 → (到达尽头) → 37 → 14
|
||||||
|
|
||||||
|
移动距离:
|
||||||
|
(53→65): 12
|
||||||
|
(65→67): 2
|
||||||
|
(67→98): 31
|
||||||
|
(98→122): 24
|
||||||
|
(122→124): 2
|
||||||
|
(124→183): 59
|
||||||
|
(183→37): 146 ← 到达尽头后反向
|
||||||
|
(37→14): 23
|
||||||
|
|
||||||
|
总距离 = 12 + 2 + 31 + 24 + 2 + 59 + 146 + 23 = 299 磁道
|
||||||
|
|
||||||
|
平均寻道长度 = 299 / 8 = 37.4 磁道
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!note] SCAN特点
|
||||||
|
> - **优点**:寻道性能好,不会产生饥饿
|
||||||
|
> - **缺点**:两侧磁道的访问频率不均匀(中间磁道被更频繁访问)
|
||||||
|
> - **类比**:像电梯一样,先上后下(或先下后上)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### C-SCAN(循环扫描算法)
|
||||||
|
|
||||||
|
**原则**:磁头**单向移动**,到达尽头后**快速返回**起始端(返回途中不服务),继续单向扫描。
|
||||||
|
|
||||||
|
```
|
||||||
|
当前位置: 53, 方向: 磁道号增加
|
||||||
|
|
||||||
|
磁头移动: 53 → 65 → 67 → 98 → 122 → 124 → 183 → (到达尽头)
|
||||||
|
→ 快速返回0 → 14 → 37
|
||||||
|
|
||||||
|
移动距离:
|
||||||
|
(53→65): 12
|
||||||
|
(65→67): 2
|
||||||
|
(67→98): 31
|
||||||
|
(98→122): 24
|
||||||
|
(122→124): 2
|
||||||
|
(124→183): 59
|
||||||
|
(183→0): 183 ← 快速返回
|
||||||
|
(0→14): 14
|
||||||
|
(14→37): 23
|
||||||
|
|
||||||
|
总距离 = 12 + 2 + 31 + 24 + 2 + 59 + 183 + 14 + 23 = 350 磁道
|
||||||
|
|
||||||
|
平均寻道长度 = 350 / 8 = 43.75 磁道
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!tip] C-SCAN vs SCAN
|
||||||
|
> - **C-SCAN** 解决了 SCAN 中**磁道访问频率不均匀**的问题
|
||||||
|
> - 返回过程不服务请求,使各磁道被访问的概率更**均匀**
|
||||||
|
> - 总移动距离可能比SCAN略大
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### LOOK与C-LOOK
|
||||||
|
|
||||||
|
**LOOK**:SCAN的改进,磁头移动到**最后一个请求**就反向(不必到磁道尽头)。
|
||||||
|
**C-LOOK**:C-SCAN的改进,同理不必到磁道尽头。
|
||||||
|
|
||||||
|
```
|
||||||
|
当前位置: 53, 方向: 磁道号增加
|
||||||
|
|
||||||
|
LOOK: 53 → 65 → 67 → 98 → 122 → 124 → 183 → 37 → 14
|
||||||
|
(不必到达磁道199,到183就反向)
|
||||||
|
|
||||||
|
移动距离 = 12 + 2 + 31 + 24 + 2 + 59 + 146 + 23 = 299
|
||||||
|
注意: SCAN和LOOK在此例中结果相同,因为183已是最大请求
|
||||||
|
|
||||||
|
C-LOOK: 53 → 65 → 67 → 98 → 122 → 124 → 183 → 14 → 37
|
||||||
|
(不必返回磁道0,直接跳到最小请求14)
|
||||||
|
|
||||||
|
移动距离 = 12 + 2 + 31 + 24 + 2 + 59 + 169 + 23 = 322
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 磁盘调度算法对比
|
||||||
|
|
||||||
|
| 算法 | 寻道距离 | 公平性 | 饥饿问题 | 特点 |
|
||||||
|
|------|----------|--------|----------|------|
|
||||||
|
| **FCFS** | 640 | 公平 | 无 | 简单,性能差 |
|
||||||
|
| **SSTF** | 236 | 不公平 | **有** | 性能好,可能饥饿 |
|
||||||
|
| **SCAN** | 299 | 较公平 | 无 | 性能好,两侧不均匀 |
|
||||||
|
| **C-SCAN** | 350 | 公平 | 无 | 各磁道均匀访问 |
|
||||||
|
| **LOOK** | 299 | 较公平 | 无 | SCAN的优化版本 |
|
||||||
|
| **C-LOOK** | 322 | 公平 | 无 | C-SCAN的优化版本 |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
subgraph 磁盘调度算法对比
|
||||||
|
A["FCFS<br/>距离: 640"] -->|"改进"| B["SSTF<br/>距离: 236"]
|
||||||
|
B -->|"解决饥饿"| C["SCAN<br/>距离: 299"]
|
||||||
|
C -->|"优化尽头"| D["LOOK<br/>距离: 299"]
|
||||||
|
C -->|"解决不均匀"| E["C-SCAN<br/>距离: 350"]
|
||||||
|
E -->|"优化尽头"| F["C-LOOK<br/>距离: 322"]
|
||||||
|
end
|
||||||
|
|
||||||
|
style A fill:#ffebee
|
||||||
|
style B fill:#fff3e0
|
||||||
|
style C fill:#e8f5e9
|
||||||
|
style D fill:#e1f5fe
|
||||||
|
style E fill:#f3e5f5
|
||||||
|
style F fill:#fce4ec
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、磁盘高速缓存
|
||||||
|
|
||||||
|
### 7.1 基本概念
|
||||||
|
|
||||||
|
**磁盘高速缓存**是在**内存**中为磁盘盘块设置的缓冲区,保存了某些盘块的副本。
|
||||||
|
|
||||||
|
> **工作原理**:当有访问磁盘的请求时,先检查高速缓存中是否有所需盘块的数据。如果有(命中),直接从缓存读取;如果未命中,才启动磁盘读取,并将数据存入缓存。
|
||||||
|
|
||||||
|
```
|
||||||
|
请求访问盘块X
|
||||||
|
→ 查找高速缓存
|
||||||
|
→ 命中(Hit): 直接从缓存读取(速度提高几个数量级)
|
||||||
|
→ 未命中(Miss): 启动磁盘读取 → 数据送入缓存 → 返回给进程
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 数据交付方式
|
||||||
|
|
||||||
|
| 方式 | 说明 | 特点 |
|
||||||
|
|------|------|------|
|
||||||
|
| **数据交付** | 将缓存中的数据**复制**到进程的内存工作区 | 数据量大,有复制开销 |
|
||||||
|
| **指针交付** | 将指向缓存的**指针**交给进程 | 无复制,更高效 |
|
||||||
|
|
||||||
|
### 7.3 置换策略
|
||||||
|
|
||||||
|
| 算法 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| **LRU**(最近最久未使用) | 替换最长时间未被访问的盘块 |
|
||||||
|
| **NRU**(最近未使用) | 替换最近一个周期内未被访问的盘块 |
|
||||||
|
| **LFU**(最少使用) | 替换被访问次数最少的盘块 |
|
||||||
|
|
||||||
|
设计置换算法还需考虑:
|
||||||
|
- **访问频率**:经常被访问的盘块应保留在缓存中
|
||||||
|
- **可预见性**:某些盘块可能很快又被访问(如顺序读取时的预读)
|
||||||
|
- **数据一致性**:确保缓存中的数据与磁盘中的数据一致
|
||||||
|
|
||||||
|
### 7.4 数据一致性保障
|
||||||
|
|
||||||
|
#### 写回策略
|
||||||
|
|
||||||
|
- **立即写回**:修改数据后立即写入磁盘(安全但性能低)
|
||||||
|
- **延迟写回**:修改数据先保留在缓存中,稍后写回磁盘(性能高但有风险)
|
||||||
|
|
||||||
|
#### 周期性写回
|
||||||
|
|
||||||
|
UNIX系统使用 `sync` 系统调用,周期性地将所有已修改的缓存数据强制写回磁盘,防止数据丢失。
|
||||||
|
|
||||||
|
```
|
||||||
|
UNIX update进程: 周期性调用 sync()
|
||||||
|
→ 将所有"脏"缓冲区的数据写回磁盘
|
||||||
|
→ 保障数据一致性
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、综合示例
|
||||||
|
|
||||||
|
### 8.1 磁盘调度算法计算练习
|
||||||
|
|
||||||
|
> [!example] 练习题
|
||||||
|
> 某磁盘有200个磁道(0~199),当前磁头位于**磁道100**,向磁道号**减小**方向移动。请求队列为:`55, 58, 39, 18, 90, 160, 150, 38, 184`。请分别用 FCFS、SSTF、SCAN、C-SCAN 算法计算总寻道距离。
|
||||||
|
|
||||||
|
**FCFS**:
|
||||||
|
```
|
||||||
|
100 → 55 → 58 → 39 → 18 → 90 → 160 → 150 → 38 → 184
|
||||||
|
= 45+3+19+21+72+70+10+112+146 = 498
|
||||||
|
```
|
||||||
|
|
||||||
|
**SSTF**:
|
||||||
|
```
|
||||||
|
100 → 90(10) → 58(32) → 55(3) → 39(16) → 38(1) → 18(20)
|
||||||
|
→ 150(132) → 160(10) → 184(24)
|
||||||
|
= 10+32+3+16+1+20+132+10+24 = 248
|
||||||
|
```
|
||||||
|
|
||||||
|
**SCAN**(向减小方向):
|
||||||
|
```
|
||||||
|
100 → 90 → 58 → 55 → 39 → 38 → 18 → (到达0) → 150 → 160 → 184
|
||||||
|
= 10+32+3+16+1+20+18+150+10+24 = 284
|
||||||
|
```
|
||||||
|
|
||||||
|
**C-SCAN**(向减小方向,单向):
|
||||||
|
```
|
||||||
|
100 → 90 → 58 → 55 → 39 → 38 → 18 → (到达0) → 快速到199
|
||||||
|
→ 184 → 160 → 150
|
||||||
|
= 10+32+3+16+1+20+18+199+15+24+10 = 348
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、I/O系统与其他子系统的关系
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
A["用户程序"] -->|"系统调用"| B["I/O系统"]
|
||||||
|
B --> C["文件系统<br/>(参考 [[04_文件IO编程]])"]
|
||||||
|
B --> D["存储管理<br/>(参考 [[12_存储管理]])"]
|
||||||
|
B --> E["进程管理<br/>(参考 [[01_系统运行机制]])"]
|
||||||
|
C --> F["磁盘管理<br/>(参考 [[05_磁盘空间管理]])"]
|
||||||
|
D -->|"缓冲区分配"| B
|
||||||
|
E -->|"中断处理、进程调度"| B
|
||||||
|
F -->|"磁盘调度"| B
|
||||||
|
|
||||||
|
style A fill:#e1f5fe
|
||||||
|
style B fill:#fff3e0
|
||||||
|
style C fill:#e8f5e9
|
||||||
|
style D fill:#f3e5f5
|
||||||
|
style E fill:#fce4ec
|
||||||
|
style F fill:#ffebee
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!summary] 本章要点回顾
|
||||||
|
> 1. **I/O控制方式**:程序直接控制 → 中断驱动 → DMA → 通道,CPU干预逐步减少
|
||||||
|
> 2. **I/O软件层次**:中断处理程序 → 设备驱动 → 设备独立性软件 → 用户层软件
|
||||||
|
> 3. **缓冲管理**:单缓冲、双缓冲、循环缓冲、缓冲池,解决速度匹配问题
|
||||||
|
> 4. **设备分配**:考虑独占/共享/虚拟设备,SPOOLing将独占改造为共享
|
||||||
|
> 5. **磁盘调度**:FCFS、SSTF(最短寻道)、SCAN(电梯)、C-SCAN(循环扫描)
|
||||||
|
> 6. **磁盘高速缓存**:内存中缓存磁盘数据,配合LRU等置换算法
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
> **参考教材**:汤小丹《计算机操作系统》第4版 第5章
|
||||||
|
> **下一篇**:[[05_磁盘空间管理]]
|
||||||
731
操作系统/18_程序代码优化/18_程序代码优化.md
Normal file
731
操作系统/18_程序代码优化/18_程序代码优化.md
Normal file
@@ -0,0 +1,731 @@
|
|||||||
|
# 18. 程序代码优化
|
||||||
|
|
||||||
|
> **课程**: 操作系统 - 程序代码优化
|
||||||
|
> **核心内容**: 机器无关优化、代码移动、消除不必要的内存引用、优化障碍(指针别名、函数副作用)、性能度量
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 前置知识
|
||||||
|
- [[03_C语言编程基础]] -- C语言编译链接过程、指针与内存模型
|
||||||
|
- [[13_存储管理基础]] -- 存储器层次结构、局部性原理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、代码优化的概念与意义
|
||||||
|
|
||||||
|
### 1.1 为什么需要代码优化
|
||||||
|
|
||||||
|
> [!important] 核心观点
|
||||||
|
> 常数因子也很重要!根据代码编写方式的不同,程序性能可能相差 **10倍** 以上。必须在多个层次上进行优化:算法、数据表示、过程调用和循环。
|
||||||
|
|
||||||
|
代码优化的目标:
|
||||||
|
- 理解程序如何被编译和执行
|
||||||
|
- 学习如何度量程序性能并识别瓶颈
|
||||||
|
- 在不破坏代码模块化和通用性的前提下提升性能
|
||||||
|
|
||||||
|
### 1.2 优化的层次
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph TB
|
||||||
|
A["算法优化<br/>选择更高效的算法"] --> B["数据结构优化<br/>选择合适的内存布局"]
|
||||||
|
B --> C["编译器优化<br/>利用编译器选项"]
|
||||||
|
C --> D["源代码级优化<br/>代码移动、消除冗余"]
|
||||||
|
D --> E["指令级优化<br/>利用底层硬件特性"]
|
||||||
|
|
||||||
|
style A fill:#e8f5e9
|
||||||
|
style B fill:#e1f5fe
|
||||||
|
style C fill:#fff3e0
|
||||||
|
style D fill:#fce4ec
|
||||||
|
style E fill:#f3e5f5
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 编译器优化级别
|
||||||
|
|
||||||
|
| 优化级别 | 说明 | 特点 |
|
||||||
|
|---------|------|------|
|
||||||
|
| `-O0` | 不优化 | 编译最快,调试最方便,性能最差 |
|
||||||
|
| `-O1` | 基本优化 | 消除冗余代码、简单内联,平衡编译速度和性能 |
|
||||||
|
| `-O2` | 推荐优化 | 启用大多数优化,包括循环优化、指令调度等 |
|
||||||
|
| `-O3` | 激进优化 | 包含 `-O2` 所有优化,加上循环展开、SIMD向量化等 |
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 不同优化级别的编译
|
||||||
|
gcc -O0 prog.c -o prog_O0 # 无优化(调试用)
|
||||||
|
gcc -O1 prog.c -o prog_O1 # 基本优化
|
||||||
|
gcc -O2 prog.c -o prog_O2 # 推荐优化
|
||||||
|
gcc -O3 prog.c -o prog_O3 # 激进优化
|
||||||
|
|
||||||
|
# 比较不同优化级别的汇编输出
|
||||||
|
gcc -O0 -S prog.c -o prog_O0.s
|
||||||
|
gcc -O2 -S prog.c -o prog_O2.s
|
||||||
|
diff prog_O0.s prog_O2.s
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!tip] 编译器的局限性
|
||||||
|
> 编译器通常**不会**改善渐近效率(大O复杂度),这取决于程序员选择最优算法。大O节省通常比常数因子更重要,但常数因子也确实重要。编译器在面对"优化障碍"时往往难以进行优化。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、性能度量:CPE(每元素周期数)
|
||||||
|
|
||||||
|
### 2.1 CPE 概念
|
||||||
|
|
||||||
|
CPE(Cycles Per Element)是度量向量或列表操作程序性能的便捷方式:
|
||||||
|
|
||||||
|
$$T = CPE \times n + Overhead$$
|
||||||
|
|
||||||
|
其中 $n$ 是向量长度,$T$ 是总执行时间(时钟周期数)。
|
||||||
|
|
||||||
|
### 2.2 时间尺度
|
||||||
|
|
||||||
|
| 指标 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| 绝对时间 | 通常使用纳秒($10^{-9}$ 秒) |
|
||||||
|
| 时钟周期 | 100 MHz → 10ns/周期;2 GHz → 0.5ns/周期 |
|
||||||
|
|
||||||
|
### 2.3 CPE 度量示例
|
||||||
|
|
||||||
|
```c
|
||||||
|
// psum1: 朴素前缀和
|
||||||
|
void psum1(float a[], float p[], long int n) {
|
||||||
|
long int i;
|
||||||
|
p[0] = a[0];
|
||||||
|
for (i = 0; i < n; i++)
|
||||||
|
p[i] = p[i-1] + a[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// psum2: 循环展开的前缀和
|
||||||
|
void psum2(float a[], float p[], long int n) {
|
||||||
|
long int i;
|
||||||
|
p[0] = a[0];
|
||||||
|
for (i = 1; i < n-1; i += 2) {
|
||||||
|
float mid_val = p[i-1] + a[i];
|
||||||
|
p[i] = mid_val;
|
||||||
|
p[i+1] = mid_val + a[i+1];
|
||||||
|
}
|
||||||
|
if (i < n)
|
||||||
|
p[i] = p[i-1] + a[i];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!note] 性能对比
|
||||||
|
> `psum2` 通过循环展开减少了关键路径上的操作次数,从而降低了 CPE。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、优化实例:向量求和的逐步优化
|
||||||
|
|
||||||
|
### 3.1 向量 ADT 定义
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef int data_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int len;
|
||||||
|
data_t *data;
|
||||||
|
} vec_rec, *vec_ptr;
|
||||||
|
```
|
||||||
|
|
||||||
|
相关操作:
|
||||||
|
- `new_vec(len)` -- 创建指定长度的向量
|
||||||
|
- `vec_length(v)` -- 返回向量长度
|
||||||
|
- `get_vec_start(v)` -- 返回向量数据起始指针
|
||||||
|
- `get_vec_element(v, index, &dest)` -- 获取指定下标的元素(带边界检查)
|
||||||
|
|
||||||
|
### 3.2 combine1:抽象版本(基线)
|
||||||
|
|
||||||
|
```c
|
||||||
|
void combine1(vec_ptr v, data_t *dest) {
|
||||||
|
long int i;
|
||||||
|
*dest = IDENT;
|
||||||
|
for (i = 0; i < vec_length(v); i++) {
|
||||||
|
data_t val;
|
||||||
|
get_vec_element(v, i, &val);
|
||||||
|
*dest = *dest OP val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!warning] 问题分析
|
||||||
|
> 每次循环迭代都调用 `vec_length(v)`,即使其返回值始终不变。这属于**循环不变量**问题。
|
||||||
|
|
||||||
|
### 3.3 combine2:代码移动(Code Motion)
|
||||||
|
|
||||||
|
```c
|
||||||
|
void combine2(vec_ptr v, data_t *dest) {
|
||||||
|
long int i;
|
||||||
|
long int length = vec_length(v); // 移出循环
|
||||||
|
*dest = IDENT;
|
||||||
|
for (i = 0; i < length; i++) {
|
||||||
|
data_t val;
|
||||||
|
get_vec_element(v, i, &val);
|
||||||
|
*dest = *dest OP val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!tip] 代码移动优化
|
||||||
|
> 将 `vec_length()` 调用从循环体内移到循环之前。循环不变量外提(Loop-Invariant Code Motion)是最基本的优化之一。
|
||||||
|
|
||||||
|
### 3.4 combine3:消除过程调用(Reduction in Strength)
|
||||||
|
|
||||||
|
```c
|
||||||
|
void combine3(vec_ptr v, data_t *dest) {
|
||||||
|
long int i;
|
||||||
|
long int length = vec_length(v);
|
||||||
|
data_t *data = get_vec_start(v); // 获取数据指针
|
||||||
|
*dest = IDENT;
|
||||||
|
for (i = 0; i < length; i++) {
|
||||||
|
*dest = *dest OP data[i]; // 直接数组访问
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**优化要点**:避免每次迭代都调用 `get_vec_element()` 函数来获取元素,而是在循环前获取数据指针,循环内直接进行指针引用。
|
||||||
|
|
||||||
|
### 3.5 combine4:消除不必要的内存引用
|
||||||
|
|
||||||
|
```c
|
||||||
|
void combine4(vec_ptr v, data_t *dest) {
|
||||||
|
long int i;
|
||||||
|
long int length = vec_length(v);
|
||||||
|
data_t *data = get_vec_start(v);
|
||||||
|
data_t acc = IDENT; // 使用局部变量累积
|
||||||
|
for (i = 0; i < length; i++)
|
||||||
|
acc = acc OP data[i];
|
||||||
|
*dest = acc; // 最后一次性写回
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**汇编对比**:
|
||||||
|
|
||||||
|
```
|
||||||
|
combine3 的循环体(每次迭代需要3条内存指令):
|
||||||
|
movss (%rbp), %xmm0 # 从 dest 读取累加值
|
||||||
|
mulss (%rax,%rdx,4), %xmm0 # 乘以 data[i]
|
||||||
|
movss %xmm0, (%rbp) # 写回 dest
|
||||||
|
addq $1, %rdx # i++
|
||||||
|
cmpq %rdx, %r12 # 比较 i:limit
|
||||||
|
jg .L498 # 循环跳转
|
||||||
|
|
||||||
|
combine4 的循环体(只有1条内存指令):
|
||||||
|
mulss (%rax,%rdx,4), %xmm0 # acc *= data[i]
|
||||||
|
addq $1, %rdx # i++
|
||||||
|
cmpq %rdx, %rbp # 比较 limit:i
|
||||||
|
jg .L488 # 循环跳转
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!important] 优化效果
|
||||||
|
> - `combine3`:每次迭代需要 **1次读 + 1次写** 内存
|
||||||
|
> - `combine4`:每次迭代只需要 **0次** 额外内存访问(`acc` 在寄存器中)
|
||||||
|
> - 局部变量 `acc` 告诉编译器:不需要在每轮循环都检查内存别名
|
||||||
|
|
||||||
|
### 3.6 优化效果汇总
|
||||||
|
|
||||||
|
| 函数 | 优化方法 | 整数+ CPE | 整数* CPE | 浮点+ CPE |
|
||||||
|
|------|---------|----------|----------|----------|
|
||||||
|
| combine1 | 抽象接口 | 12.00 | 12.00 | 12.00 |
|
||||||
|
| combine2 | 代码移动 | -- | -- | -- |
|
||||||
|
| combine3 | 消除过程调用 | -- | -- | -- |
|
||||||
|
| combine4 | 使用临时变量累积 | **2.00** | **3.00** | **3.00** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、代码移动(Code Motion)详解
|
||||||
|
|
||||||
|
### 4.1 循环不变量外提
|
||||||
|
|
||||||
|
将循环中每次迭代结果相同的计算移到循环之前:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 优化前:strlen 在每次循环都被调用
|
||||||
|
void lower1(char *s) {
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < strlen(s); i++) // O(n^2) 复杂度!
|
||||||
|
if (s[i] >= 'A' && s[i] <= 'Z')
|
||||||
|
s[i] -= ('A' - 'a');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优化后:strlen 只调用一次
|
||||||
|
void lower2(char *s) {
|
||||||
|
int i;
|
||||||
|
int len = strlen(s); // O(n) 复杂度
|
||||||
|
for (i = 0; i < len; i++)
|
||||||
|
if (s[i] >= 'A' && s[i] <= 'Z')
|
||||||
|
s[i] -= ('A' - 'a');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!warning] 为什么编译器不能自动做这个优化?
|
||||||
|
> `strlen` 是一个函数调用,编译器**不知道**它是否有副作用,也不知道字符串长度在循环中是否会改变。编译器必须保守处理。
|
||||||
|
|
||||||
|
### 4.2 strlen 的内部实现
|
||||||
|
|
||||||
|
```c
|
||||||
|
size_t strlen(const char *s) {
|
||||||
|
int length = 0;
|
||||||
|
while (*s != '\0') {
|
||||||
|
s++;
|
||||||
|
length++;
|
||||||
|
}
|
||||||
|
return length;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`strlen` 本身是 $O(n)$ 的。如果在循环中每次迭代都调用它,整体复杂度就变成了 $O(n^2)$。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、优化障碍(Optimization Blockers)
|
||||||
|
|
||||||
|
编译器在进行优化时受到两大根本性约束:
|
||||||
|
1. 编译器在**任何可能的条件下**都不能改变程序的行为
|
||||||
|
2. 当程序员知道的信息比编译器多时,需要程序员主动干预
|
||||||
|
|
||||||
|
### 5.1 指针别名(Memory Aliasing)
|
||||||
|
|
||||||
|
> [!danger] 别名问题
|
||||||
|
> 当两个不同的内存引用指向同一个存储位置时,就产生了别名。C语言允许地址算术运算和直接访问存储结构,因此别名问题非常容易出现。
|
||||||
|
|
||||||
|
```c
|
||||||
|
// twiddle1 和 twiddle2 看似等价,但实际上不一定!
|
||||||
|
void twiddle1(int *xp, int *yp) {
|
||||||
|
*xp += *yp;
|
||||||
|
*xp += *yp;
|
||||||
|
}
|
||||||
|
|
||||||
|
void twiddle2(int *xp, int *yp) {
|
||||||
|
*xp += 2 * *yp;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**当 `xp == yp` 时(别名情况)**:
|
||||||
|
- `twiddle1`:`*xp = *xp + *xp = 2*xp`,然后 `*xp = 2*xp + 2*xp = 4*xp`
|
||||||
|
- `twiddle2`:`*xp = *xp + 2*(*xp) = 3*xp`
|
||||||
|
- 结果不同!编译器不能将 `twiddle1` 优化为 `twiddle2`
|
||||||
|
|
||||||
|
**别名实例演示**:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// v = [2, 3, 5]
|
||||||
|
combine3(v, get_vec_start(v) + 2); // dest 指向 v->data[2]
|
||||||
|
combine4(v, get_vec_start(v) + 2); // dest 指向 v->data[2]
|
||||||
|
```
|
||||||
|
|
||||||
|
| 函数 | 初始 | i=0 | i=1 | i=2 | 最终 |
|
||||||
|
|------|------|-----|-----|-----|------|
|
||||||
|
| combine3 | [2,3,5] | [2,3,1] | [2,3,2] | [2,3,6] | [2,3,36] |
|
||||||
|
| combine4 | [2,3,5] | [2,3,5] | [2,3,5] | [2,3,5] | [2,3,30] |
|
||||||
|
|
||||||
|
> [!tip] 避免别名问题的方法
|
||||||
|
> 养成使用**局部变量**的习惯。在循环中用局部变量累积结果,循环结束后再写回目标地址。这是告诉编译器"不需要检查别名"的方式。
|
||||||
|
|
||||||
|
### 5.2 函数调用的副作用
|
||||||
|
|
||||||
|
```c
|
||||||
|
int f(int);
|
||||||
|
|
||||||
|
// func1:调用 f 4次
|
||||||
|
int func1(int x) {
|
||||||
|
return f(x) + f(x) + f(x) + f(x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// func2:调用 f 1次
|
||||||
|
int func2(int x) {
|
||||||
|
return 4 * f(x);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
看起来 `func1` 可以优化为 `func2`,但当 `f` 有副作用时就不行了:
|
||||||
|
|
||||||
|
```c
|
||||||
|
int counter = 0;
|
||||||
|
int f(int x) {
|
||||||
|
return counter++; // 每次调用返回不同的值!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!warning] 编译器的保守策略
|
||||||
|
> 编译器通常假设函数调用可能有副作用,因此不会轻易将多次函数调用合并。程序员可以:
|
||||||
|
> - 使用 `inline` 关键字建议编译器内联
|
||||||
|
> - 使用 `const`、`pure` 等属性标记无副作用的函数
|
||||||
|
> - 将函数调用结果缓存到局部变量中
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、循环优化技术
|
||||||
|
|
||||||
|
### 6.1 循环展开(Loop Unrolling)
|
||||||
|
|
||||||
|
减少循环控制开销,增加指令级并行性:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 优化前
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
sum += a[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2x 循环展开
|
||||||
|
int i;
|
||||||
|
for (i = 0; i < n - 1; i += 2) {
|
||||||
|
sum += a[i] + a[i+1];
|
||||||
|
}
|
||||||
|
for (; i < n; i++) { // 处理剩余元素
|
||||||
|
sum += a[i];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.2 循环合并(Loop Fusion)
|
||||||
|
|
||||||
|
将多个独立循环合并为一个,改善数据局部性:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 优化前:两次遍历
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
a[i] = b[i] + c[i];
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
d[i] = a[i] * 2;
|
||||||
|
|
||||||
|
// 优化后:一次遍历
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
a[i] = b[i] + c[i];
|
||||||
|
d[i] = a[i] * 2;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.3 循环不变量外提(Loop-Invariant Code Motion)
|
||||||
|
|
||||||
|
将循环中不变的计算移到循环外:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 优化前
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
a[i] = b[i] * (x * y + z); // x*y+z 在循环中不变
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优化后
|
||||||
|
int tmp = x * y + z;
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
a[i] = b[i] * tmp;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6.4 循环优化流程图
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["循环代码"] --> B{"循环体中有<br>不变量?"}
|
||||||
|
B -->|是| C["循环不变量外提"]
|
||||||
|
B -->|否| D{"循环迭代<br>次数少?"}
|
||||||
|
C --> D
|
||||||
|
D -->|是| E["循环展开"]
|
||||||
|
D -->|否| F{"多个循环<br>数据相关?"}
|
||||||
|
E --> F
|
||||||
|
F -->|是| G["循环合并"]
|
||||||
|
F -->|否| H["保持原样"]
|
||||||
|
G --> I["优化后的循环"]
|
||||||
|
H --> I
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 七、内存访问优化与缓存友好性
|
||||||
|
|
||||||
|
### 7.1 空间局部性与时间局部性
|
||||||
|
|
||||||
|
程序的局部性原理(参见 [[13_存储管理基础]])是内存优化的基础:
|
||||||
|
|
||||||
|
- **空间局部性**:访问了某个地址,附近地址很可能也会被访问
|
||||||
|
- **时间局部性**:刚访问过的数据很可能再次被访问
|
||||||
|
|
||||||
|
### 7.2 按行优先 vs 按列遍历
|
||||||
|
|
||||||
|
```c
|
||||||
|
#define M 2048
|
||||||
|
#define N 2048
|
||||||
|
|
||||||
|
// 方式P1:按行遍历(空间局部性好)
|
||||||
|
int sumarrayrows(int a[M][N]) {
|
||||||
|
int i, j, sum = 0;
|
||||||
|
for (i = 0; i < M; i++)
|
||||||
|
for (j = 0; j < N; j++)
|
||||||
|
sum += a[i][j]; // 顺序访问内存
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 方式P2:按列遍历(空间局部性差)
|
||||||
|
int sumarraycols(int a[M][N]) {
|
||||||
|
int i, j, sum = 0;
|
||||||
|
for (j = 0; j < N; j++)
|
||||||
|
for (i = 0; i < M; i++)
|
||||||
|
sum += a[i][j]; // 每次跳过 N 个元素
|
||||||
|
return sum;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!important] 实测性能差距
|
||||||
|
> 在 2GHz Intel Pentium 4 上:
|
||||||
|
> - **P1(按行遍历)**:59,393,288 时钟周期
|
||||||
|
> - **P2(按列遍历)**:1,277,877,876 时钟周期
|
||||||
|
> - P1 比 P2 **快 21.5 倍**!
|
||||||
|
|
||||||
|
### 7.3 局部性分析
|
||||||
|
|
||||||
|
| 数据 | 按行遍历(P1) | 按列遍历(P2) |
|
||||||
|
|------|-------------|-------------|
|
||||||
|
| 数组 `a` | 空间局部性好(顺序访问) | 空间局部性差(每次跳2048个单元) |
|
||||||
|
| 变量 `sum,i,j` | 时间局部性好(循环中反复访问) | 时间局部性好 |
|
||||||
|
| 循环指令 | 空间局部性好 + 时间局部性好 | 空间局部性好 + 时间局部性好 |
|
||||||
|
|
||||||
|
### 7.4 缓存友好的代码编写原则
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
graph LR
|
||||||
|
A["缓存友好原则"] --> B["按行优先遍历多维数组"]
|
||||||
|
A --> C["使用连续内存布局"]
|
||||||
|
A --> D["减少跨步访问"]
|
||||||
|
A --> E["数据结构对齐缓存行"]
|
||||||
|
|
||||||
|
style A fill:#e8f5e9
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 八、函数调用优化
|
||||||
|
|
||||||
|
### 8.1 内联函数(Inline)
|
||||||
|
|
||||||
|
将函数体直接展开到调用处,消除函数调用开销:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 优化前:函数调用有开销
|
||||||
|
int square(int x) {
|
||||||
|
return x * x;
|
||||||
|
}
|
||||||
|
int result = square(5);
|
||||||
|
|
||||||
|
// 使用 inline 关键字
|
||||||
|
static inline int square_inline(int x) {
|
||||||
|
return x * x;
|
||||||
|
}
|
||||||
|
int result = square_inline(5); // 编译器可能直接展开为 5*5
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!note] 编译器的内联决策
|
||||||
|
> 即使不使用 `inline` 关键字,`-O2` 及以上优化级别下,编译器也会自动内联小型函数。但大型函数的内联会增加代码体积,可能导致指令缓存不友好。
|
||||||
|
|
||||||
|
### 8.2 消除过程调用
|
||||||
|
|
||||||
|
如 combine3 的优化所示,将函数调用替换为直接内存访问:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 优化前:每次迭代调用函数
|
||||||
|
for (i = 0; i < length; i++) {
|
||||||
|
data_t val;
|
||||||
|
get_vec_element(v, i, &val); // 函数调用开销
|
||||||
|
*dest = *dest OP val;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优化后:直接指针访问
|
||||||
|
data_t *data = get_vec_start(v);
|
||||||
|
for (i = 0; i < length; i++) {
|
||||||
|
*dest = *dest OP data[i]; // 直接数组访问
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 8.3 尾调用优化
|
||||||
|
|
||||||
|
当函数最后一步是调用另一个函数时,编译器可以复用当前栈帧:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 优化前:递归可能导致栈溢出
|
||||||
|
int factorial(int n) {
|
||||||
|
if (n <= 1) return 1;
|
||||||
|
return n * factorial(n - 1); // 不是尾调用(乘法在递归之后)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尾递归版本
|
||||||
|
int factorial_tail(int n, int acc) {
|
||||||
|
if (n <= 1) return acc;
|
||||||
|
return factorial_tail(n - 1, n * acc); // 尾调用
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 九、SIMD 与底层优化
|
||||||
|
|
||||||
|
### 9.1 SIMD 概念
|
||||||
|
|
||||||
|
SIMD(Single Instruction, Multiple Data)允许一条指令同时处理多个数据元素:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 标量版本:一次处理一个元素
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
c[i] = a[i] + b[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// SSE 向量化(伪代码):一次处理4个float
|
||||||
|
// 编译器在 -O3 下可能自动生成 SIMD 指令
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.2 编译器自动向量化
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看编译器是否进行了向量化
|
||||||
|
gcc -O3 -ftree-vectorize -fopt-info-vec prog.c
|
||||||
|
|
||||||
|
# 明确启用 SIMD 优化
|
||||||
|
gcc -O3 -mavx2 prog.c # 使用 AVX2 指令集
|
||||||
|
gcc -O3 -msse4.2 prog.c # 使用 SSE4.2 指令集
|
||||||
|
```
|
||||||
|
|
||||||
|
### 9.3 数据对齐
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 确保数据对齐以获得最佳 SIMD 性能
|
||||||
|
float *a = aligned_alloc(32, n * sizeof(float)); // 32字节对齐(AVX)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十、优化编译器的局限性
|
||||||
|
|
||||||
|
### 10.1 编译器的基本约束
|
||||||
|
|
||||||
|
> [!important] 根本约束
|
||||||
|
> 编译器在**任何可能的条件下**都不能改变程序的行为。这意味着即使某些行为只在极端情况下发生,编译器也必须保守地保留这些行为。
|
||||||
|
|
||||||
|
### 10.2 编译器难以优化的情况
|
||||||
|
|
||||||
|
| 情况 | 原因 | 程序员对策 |
|
||||||
|
|------|------|-----------|
|
||||||
|
| 指针别名 | 编译器不知道两个指针是否指向同一位置 | 使用局部变量累积结果 |
|
||||||
|
| 函数副作用 | 编译器不知道函数是否有副作用 | 使用 `inline`、`const`、`pure` 属性 |
|
||||||
|
| 数据范围 | 编译器不知道变量的实际取值范围 | 使用更精确的数据类型 |
|
||||||
|
| 循环边界 | 编译器不确定循环次数 | 使用常量或 `restrict` 关键字 |
|
||||||
|
|
||||||
|
### 10.3 `restrict` 关键字
|
||||||
|
|
||||||
|
C99 引入的 `restrict` 告诉编译器指针没有别名:
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 使用 restrict 告诉编译器 src 和 dest 不重叠
|
||||||
|
void copy(int *restrict dest, const int *restrict src, int n) {
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
dest[i] = src[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十一、性能测量与 Profiling 工具
|
||||||
|
|
||||||
|
### 11.1 常用性能分析工具
|
||||||
|
|
||||||
|
| 工具 | 用途 | 命令示例 |
|
||||||
|
|------|------|---------|
|
||||||
|
| `time` | 测量程序总执行时间 | `time ./prog` |
|
||||||
|
| `perf` | Linux 性能计数器分析 | `perf stat ./prog` |
|
||||||
|
| `gprof` | GNU 函数级 profiling | `gcc -pg prog.c && ./a.out && gprof` |
|
||||||
|
| `valgrind/callgrind` | 缓存和分支预测分析 | `valgrind --tool=callgrind ./prog` |
|
||||||
|
| `cachegrind` | 缓存命中率分析 | `valgrind --tool=cachegrind ./prog` |
|
||||||
|
|
||||||
|
### 11.2 使用 perf 进行分析
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 基本性能统计
|
||||||
|
perf stat ./prog
|
||||||
|
|
||||||
|
# 详细缓存分析
|
||||||
|
perf stat -e cache-references,cache-misses ./prog
|
||||||
|
|
||||||
|
# 函数级热点分析
|
||||||
|
perf record ./prog
|
||||||
|
perf report
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.3 使用 gprof 进行分析
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编译时加入 profiling 支持
|
||||||
|
gcc -pg -O2 prog.c -o prog
|
||||||
|
|
||||||
|
# 运行程序(生成 gmon.out)
|
||||||
|
./prog
|
||||||
|
|
||||||
|
# 查看分析结果
|
||||||
|
gprof prog gmon.out > analysis.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 11.4 优化工作流程
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A["编写正确的代码"] --> B["选择合适的算法"]
|
||||||
|
B --> C["使用 profiling 找到瓶颈"]
|
||||||
|
C --> D{"瓶颈在哪里?"}
|
||||||
|
D -->|循环| E["循环优化:展开、外提、合并"]
|
||||||
|
D -->|内存| F["内存优化:局部性、缓存友好"]
|
||||||
|
D -->|函数调用| G["内联、消除过程调用"]
|
||||||
|
D -->|I/O| H["缓冲、批量处理"]
|
||||||
|
E --> I["测量优化效果"]
|
||||||
|
F --> I
|
||||||
|
G --> I
|
||||||
|
H --> I
|
||||||
|
I --> J{"性能满足要求?"}
|
||||||
|
J -->|否| C
|
||||||
|
J -->|是| K["完成"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 十二、优化总结
|
||||||
|
|
||||||
|
### 12.1 机器无关优化的核心策略
|
||||||
|
|
||||||
|
| 优化策略 | 说明 | 对应函数 |
|
||||||
|
|---------|------|---------|
|
||||||
|
| **代码移动** | 将循环不变量移出循环 | combine1 → combine2 |
|
||||||
|
| **消除过程调用** | 用直接内存访问替代函数调用 | combine2 → combine3 |
|
||||||
|
| **消除不必要内存引用** | 使用局部变量/寄存器累积 | combine3 → combine4 |
|
||||||
|
| **循环展开** | 减少循环控制开销 | psum1 → psum2 |
|
||||||
|
|
||||||
|
### 12.2 关键原则
|
||||||
|
|
||||||
|
> [!abstract] 代码优化的核心原则
|
||||||
|
> 1. **先正确,后优化**:保证代码正确性是前提
|
||||||
|
> 2. **度量驱动**:使用 profiling 工具找到真正的瓶颈
|
||||||
|
> 3. **算法优先**:$O(n \log n)$ 的算法比 $O(n^2)$ 的优化更重要
|
||||||
|
> 4. **利用局部性**:按行访问数组,使用连续内存布局
|
||||||
|
> 5. **减少函数调用**:在热循环中避免不必要的函数调用
|
||||||
|
> 6. **使用局部变量**:告诉编译器数据没有别名问题
|
||||||
|
> 7. **信任编译器**:`-O2` 通常足够,除非有明确的性能需求
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 本章术语
|
||||||
|
|
||||||
|
| 术语 | 英文 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 代码移动 | Code Motion | 将循环不变量计算移到循环外 |
|
||||||
|
| CPE | Cycles Per Element | 每元素处理所需的时钟周期数 |
|
||||||
|
| 别名 | Aliasing | 两个不同指针指向同一内存位置 |
|
||||||
|
| 循环展开 | Loop Unrolling | 复制循环体以减少循环控制开销 |
|
||||||
|
| 内联 | Inline | 将函数体展开到调用处 |
|
||||||
|
| SIMD | Single Instruction Multiple Data | 单指令多数据并行 |
|
||||||
|
| 向量化 | Vectorization | 利用 SIMD 指令并行处理数据 |
|
||||||
|
| 代码选择 | Code Selection | 编译器选择合适的机器指令 |
|
||||||
|
| 寄存器分配 | Register Allocation | 将变量分配到 CPU 寄存器 |
|
||||||
|
| `restrict` | Restrict Qualifier | 告诉编译器指针没有别名 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 复习与思考
|
||||||
|
|
||||||
|
1. 为什么 `combine1` 到 `combine4` 的优化能显著降低 CPE?每一步消除了什么开销?
|
||||||
|
2. 当 `xp` 和 `yp` 指向同一地址时,`twiddle1` 和 `twiddle2` 的结果为什么不同?
|
||||||
|
3. 为什么按列遍历二维数组比按行遍历慢很多?这与 [[13_存储管理基础]] 中的局部性原理有什么关系?
|
||||||
|
4. 编译器为什么不能自动将 `func1` 优化为 `func2`?程序员应该如何帮助编译器?
|
||||||
|
5. 在什么情况下应该使用 `-O3` 而不是 `-O2`?有什么潜在的代价?
|
||||||
337
操作系统/实验/实验01_IO编程.md
Normal file
337
操作系统/实验/实验01_IO编程.md
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
# 实验01 Linux I/O 编程
|
||||||
|
|
||||||
|
## 实验目的
|
||||||
|
|
||||||
|
1. 练习 UNIX I/O 函数(`open`、`close`、`read`、`write`、`lseek`)的使用
|
||||||
|
2. 掌握标准 I/O 函数(`fgets`、`fread`、`fwrite`)的操作方式
|
||||||
|
3. 建立 API 开销的概念,理解系统调用与库函数的性能差异
|
||||||
|
4. 熟悉结构体的二进制 I/O 读写方法
|
||||||
|
5. 综合运用文件 I/O 完成文本处理任务
|
||||||
|
|
||||||
|
## 涉及知识点
|
||||||
|
|
||||||
|
- 文件描述符与 `open`/`close`/`read`/`write` 系统调用
|
||||||
|
- 标准 I/O:`fopen`/`fclose`/`fgets`/`fprintf`/`fread`/`fwrite`
|
||||||
|
- 文件打开模式:`O_RDONLY`、`O_WRONLY`、`O_CREAT`、`O_TRUNC`、`O_APPEND`
|
||||||
|
- 结构体与文件 I/O 结合(二进制序列化)
|
||||||
|
- `gettimeofday` 高精度计时
|
||||||
|
- 字符串处理:`strtok`、`strcmp`、`strstr`、`sscanf`、`%[^:]`
|
||||||
|
- 排序算法(词频统计中的字典序排列)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务一:task41.c —— 学生信息文件字段处理
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
1. 创建文件 `student.txt`,写入若干学生记录,每行格式为 `姓名:学号:学院:年龄:性别`
|
||||||
|
2. 从 `student.txt` 中查找所有属于"计算机与网络安全学院"的记录
|
||||||
|
3. 将找到的记录字段顺序调整为 `学号:姓名:性别:年龄:学院`
|
||||||
|
4. 将调整后的记录写入 `csStudent.txt`
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// ---- 第一步:创建并写入 student.txt ----
|
||||||
|
int fd = open("student.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
if (fd < 0) { perror("open student.txt"); exit(1); }
|
||||||
|
|
||||||
|
const char *records[] = {
|
||||||
|
"张三:2023001:计算机与网络安全学院:20:男\n",
|
||||||
|
"李四:2023002:电子信息学院:21:女\n",
|
||||||
|
"王五:2023003:计算机与网络安全学院:22:男\n",
|
||||||
|
"赵六:2023004:数学学院:19:女\n",
|
||||||
|
"钱七:2023005:计算机与网络安全学院:20:男\n",
|
||||||
|
};
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
write(fd, records[i], strlen(records[i]));
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
// ---- 第二步:读取、筛选、重组字段 ----
|
||||||
|
FILE *fin = fopen("student.txt", "r");
|
||||||
|
FILE *fout = fopen("csStudent.txt", "w");
|
||||||
|
char line[256];
|
||||||
|
|
||||||
|
while (fgets(line, sizeof(line), fin) != NULL) {
|
||||||
|
if (strstr(line, "计算机与网络安全学院") != NULL) {
|
||||||
|
char name[64], id[64], college[64], age[16], gender[16];
|
||||||
|
sscanf(line, "%[^:]:%[^:]:%[^:]:%[^:]:%s",
|
||||||
|
name, id, college, age, gender);
|
||||||
|
// 调整字段顺序:学号:姓名:性别:年龄:学院
|
||||||
|
fprintf(fout, "%s:%s:%s:%s:%s\n", id, name, gender, age, college);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fclose(fin);
|
||||||
|
fclose(fout);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `write` 后文件内容为空 | 忘记 `close`,数据还在内核缓冲区 | 写完后务必 `close(fd)` |
|
||||||
|
| 读取中文出现乱码 | 编码不匹配 | 确保源文件为 UTF-8 编码,终端 locale 一致 |
|
||||||
|
| `strtok` 分割结果不对 | 行末换行符干扰 | 分割前先去除 `\n` |
|
||||||
|
| `sscanf` 读取不完整 | 格式字符串匹配错误 | 使用 `%[^:]` 匹配非冒号字符序列 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务二:task42.c —— 结构体二进制文件读写
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
1. 从键盘读入 5 个学生的信息(学号、姓名、语文、数学、英语成绩),存入结构体数组
|
||||||
|
2. 将结构体数组以二进制方式写入文件 `score.dat`(使用 `write` 写入原始字节)
|
||||||
|
3. 从文件中读取第 1、3、5 条记录并显示
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int id;
|
||||||
|
char name[32];
|
||||||
|
float chinese;
|
||||||
|
float math;
|
||||||
|
float english;
|
||||||
|
} Student;
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
Student stu[5];
|
||||||
|
|
||||||
|
// 从键盘读入
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
printf("请输入第%d个学生(学号 姓名 语文 数学 英语): ", i + 1);
|
||||||
|
scanf("%d %s %f %f %f", &stu[i].id, stu[i].name,
|
||||||
|
&stu[i].chinese, &stu[i].math, &stu[i].english);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 二进制写入
|
||||||
|
int fd = open("score.dat", O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
write(fd, stu, sizeof(Student) * 5);
|
||||||
|
close(fd);
|
||||||
|
|
||||||
|
// 读取第 1、3、5 条(下标 0、2、4)
|
||||||
|
fd = open("score.dat", O_RDONLY);
|
||||||
|
Student temp;
|
||||||
|
for (int i = 0; i < 5; i++) {
|
||||||
|
read(fd, &temp, sizeof(Student));
|
||||||
|
if (i == 0 || i == 2 || i == 4) {
|
||||||
|
printf("学号:%d 姓名:%s 语文:%.1f 数学:%.1f 英语:%.1f\n",
|
||||||
|
temp.id, temp.name, temp.chinese, temp.math, temp.english);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 也可用 lseek 精确定位到第 3 条
|
||||||
|
lseek(fd, sizeof(Student) * 2, SEEK_SET);
|
||||||
|
read(fd, &temp, sizeof(Student));
|
||||||
|
printf("第3条: %s\n", temp.name);
|
||||||
|
|
||||||
|
close(fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 读出的数值不对 | 结构体内存对齐(padding) | `sizeof(Student)` 可能大于各字段大小之和,属正常现象 |
|
||||||
|
| `lseek` 定位不准 | 偏移量计算错误 | 偏移量 = `sizeof(Student) * (n - 1)` |
|
||||||
|
| 中文姓名存储异常 | `char name[32]` 对 UTF-8 中文不够 | 增大缓冲区(一个汉字占 3 字节) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务三:task43.c —— API 执行时间测量(选做)
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
1. 分别测量 `read`/`write` 和 `fread`/`fwrite` 在不同数据量下的执行时间
|
||||||
|
2. 对比系统调用与库函数的性能差异
|
||||||
|
3. 绘制或输出性能对比表
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
long time_diff(struct timeval *start, struct timeval *end) {
|
||||||
|
return (end->tv_sec - start->tv_sec) * 1000000L
|
||||||
|
+ (end->tv_usec - start->tv_usec);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
struct timeval start, end;
|
||||||
|
int N = 1000000; // 循环次数
|
||||||
|
char buf[1];
|
||||||
|
|
||||||
|
// 测量 write(逐字节)
|
||||||
|
int fd = open("test.dat", O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
gettimeofday(&start, NULL);
|
||||||
|
for (int i = 0; i < N; i++)
|
||||||
|
write(fd, buf, 1);
|
||||||
|
gettimeofday(&end, NULL);
|
||||||
|
close(fd);
|
||||||
|
printf("write 逐字节: %ld 微秒\n", time_diff(&start, &end));
|
||||||
|
|
||||||
|
// 测量 fwrite(逐字节,带用户缓冲)
|
||||||
|
FILE *fp = fopen("test2.dat", "w");
|
||||||
|
gettimeofday(&start, NULL);
|
||||||
|
for (int i = 0; i < N; i++)
|
||||||
|
fwrite(buf, 1, 1, fp);
|
||||||
|
gettimeofday(&end, NULL);
|
||||||
|
fclose(fp);
|
||||||
|
printf("fwrite 逐字节: %ld 微秒\n", time_diff(&start, &end));
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 测量方案
|
||||||
|
|
||||||
|
| 测量项 | 操作 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `write` | 逐字节写 1MB | 基准:每次陷入内核 |
|
||||||
|
| `read` | 逐字节读 1MB | 基准:每次陷入内核 |
|
||||||
|
| `fwrite` | 逐字节写 1MB | 带用户空间缓冲 |
|
||||||
|
| `fread` | 逐字节读 1MB | 带用户空间缓冲 |
|
||||||
|
| `write` | 块写入(4KB) | 对比块大小影响 |
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 计时结果为 0 | 操作太快,微秒级精度不够 | 增加循环次数到百万级 |
|
||||||
|
| 系统调用比库函数慢很多 | 每次 `read`/`write` 都陷入内核 | 正常现象,体现用户缓冲的价值 |
|
||||||
|
| 结果波动大 | 系统调度干扰 | 多次测量取平均值 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务四:task44.c —— 英文文章词频统计
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
1. 读取一篇英文文章(从文件或标准输入)
|
||||||
|
2. 统计每个单词出现的次数
|
||||||
|
3. 输出格式:`单词:次数`
|
||||||
|
4. 按字典序排列所有单词
|
||||||
|
5. 额外输出出现频度最高的 10 个单词
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <ctype.h>
|
||||||
|
|
||||||
|
#define MAX_WORDS 10000
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char word[64];
|
||||||
|
int count;
|
||||||
|
} WordEntry;
|
||||||
|
|
||||||
|
WordEntry dict[MAX_WORDS];
|
||||||
|
int dict_size = 0;
|
||||||
|
|
||||||
|
// 查找已有单词或插入新单词
|
||||||
|
int find_or_insert(const char *word) {
|
||||||
|
for (int i = 0; i < dict_size; i++) {
|
||||||
|
if (strcmp(dict[i].word, word) == 0) {
|
||||||
|
dict[i].count++;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
strcpy(dict[dict_size].word, word);
|
||||||
|
dict[dict_size].count = 1;
|
||||||
|
return dict_size++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// qsort 比较函数:字典序
|
||||||
|
int cmp_alpha(const void *a, const void *b) {
|
||||||
|
return strcmp(((WordEntry *)a)->word, ((WordEntry *)b)->word);
|
||||||
|
}
|
||||||
|
|
||||||
|
// qsort 比较函数:频度降序
|
||||||
|
int cmp_freq(const void *a, const void *b) {
|
||||||
|
return ((WordEntry *)b)->count - ((WordEntry *)a)->count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
FILE *fp = fopen("article.txt", "r");
|
||||||
|
if (!fp) { perror("fopen"); return 1; }
|
||||||
|
|
||||||
|
char word[64];
|
||||||
|
while (fscanf(fp, "%63s", word) == 1) {
|
||||||
|
// 去除标点,统一小写
|
||||||
|
char clean[64];
|
||||||
|
int j = 0;
|
||||||
|
for (int i = 0; word[i]; i++) {
|
||||||
|
if (isalpha(word[i]))
|
||||||
|
clean[j++] = tolower(word[i]);
|
||||||
|
}
|
||||||
|
clean[j] = '\0';
|
||||||
|
if (j > 0)
|
||||||
|
find_or_insert(clean);
|
||||||
|
}
|
||||||
|
fclose(fp);
|
||||||
|
|
||||||
|
// 按字典序输出
|
||||||
|
qsort(dict, dict_size, sizeof(WordEntry), cmp_alpha);
|
||||||
|
for (int i = 0; i < dict_size; i++)
|
||||||
|
printf("%s:%d\n", dict[i].word, dict[i].count);
|
||||||
|
|
||||||
|
// 按频度降序输出前 10 个
|
||||||
|
qsort(dict, dict_size, sizeof(WordEntry), cmp_freq);
|
||||||
|
printf("\n频度最高的10个单词:\n");
|
||||||
|
for (int i = 0; i < 10 && i < dict_size; i++)
|
||||||
|
printf("%s:%d\n", dict[i].word, dict[i].count);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 单词提取时需过滤标点符号(逗号、句号、引号等)
|
||||||
|
- 不区分大小写(统一转为小写)
|
||||||
|
- 连字符(如 "well-known")可按需决定是否拆分
|
||||||
|
- 文件较大时注意 `MAX_WORDS` 的上限,可改用动态分配
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 单词带着标点 | 没有清理非字母字符 | 用 `isalpha` 逐字符过滤 |
|
||||||
|
| 大小写被当成不同单词 | 未统一大小写 | 提取前用 `tolower` 转换 |
|
||||||
|
| 排序结果不对 | `qsort` 比较函数写错 | 注意比较函数的参数类型转换 |
|
||||||
|
| 数组越界 | 单词数超过 `MAX_WORDS` | 动态扩容(`realloc`)或增大数组 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实验总结
|
||||||
|
|
||||||
|
通过本实验,应掌握以下能力:
|
||||||
|
|
||||||
|
1. 熟练使用底层 I/O(`open`/`read`/`write`)和标准 I/O(`fopen`/`fgets`/`fprintf`)
|
||||||
|
2. 理解文件描述符与 `FILE *` 的区别
|
||||||
|
3. 能用结构体进行二进制文件读写
|
||||||
|
4. 了解系统调用与库函数的性能差异
|
||||||
|
5. 综合运用字符串处理和文件 I/O 解决实际问题
|
||||||
437
操作系统/实验/实验02_进程控制.md
Normal file
437
操作系统/实验/实验02_进程控制.md
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
# 实验02 Linux 进程控制编程
|
||||||
|
|
||||||
|
## 实验目的
|
||||||
|
|
||||||
|
1. 掌握 `fork`、`exec`、`wait`、`exit` 等进程控制系统调用
|
||||||
|
2. 理解进程的创建、执行和终止过程
|
||||||
|
3. 掌握进程族亲关系(父子进程、兄弟进程)
|
||||||
|
4. 学会编写简单的交互式 shell 程序
|
||||||
|
5. 理解守护进程(daemon)的概念和实现方法
|
||||||
|
6. 了解信号机制的基本使用
|
||||||
|
|
||||||
|
## 涉及知识点
|
||||||
|
|
||||||
|
- `fork()` 创建子进程及返回值含义
|
||||||
|
- `exec` 家族函数:`execl`、`execlp`、`execvp`、`execve`
|
||||||
|
- `wait` / `waitpid` 回收子进程,避免僵尸进程
|
||||||
|
- `exit` / `_exit` 终止进程
|
||||||
|
- 进程组与会话(`setsid`)
|
||||||
|
- 信号:`SIGCHLD`、`SIGKILL`、`SIGTERM`、`SIGALRM`
|
||||||
|
- 守护进程(daemon)的创建步骤
|
||||||
|
- 文件重定向:`dup2`
|
||||||
|
- 管道:`pipe`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务一:task51.c -- 进程族亲结构
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
创建如下进程树结构,每个进程打印自己的 PID 和父进程 PPID,最终输出完整的族亲关系:
|
||||||
|
|
||||||
|
```
|
||||||
|
p1
|
||||||
|
+-- p11
|
||||||
|
+-- p12
|
||||||
|
+-- p121
|
||||||
|
+-- p122
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
pid_t pid;
|
||||||
|
|
||||||
|
printf("p1: PID=%d, PPID=%d\n", getpid(), getppid());
|
||||||
|
|
||||||
|
// 创建 p11
|
||||||
|
pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
printf("p11: PID=%d, PPID=%d\n", getpid(), getppid());
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 p12
|
||||||
|
pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
printf("p12: PID=%d, PPID=%d\n", getpid(), getppid());
|
||||||
|
|
||||||
|
// p12 创建 p121
|
||||||
|
pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
printf("p121: PID=%d, PPID=%d\n", getpid(), getppid());
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// p12 创建 p122
|
||||||
|
pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
printf("p122: PID=%d, PPID=%d\n", getpid(), getppid());
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
wait(NULL);
|
||||||
|
wait(NULL);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// p1 等待两个子进程
|
||||||
|
wait(NULL);
|
||||||
|
wait(NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键要点
|
||||||
|
|
||||||
|
- `fork()` 返回值:父进程中返回子进程 PID,子进程中返回 0
|
||||||
|
- `wait(NULL)` 阻塞等待任意一个子进程结束
|
||||||
|
- 子进程必须调用 `exit(0)` 终止,否则会继续执行父进程后续代码
|
||||||
|
- `fork` 后父子进程从同一位置继续执行,注意判断返回值分流
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 输出顺序不确定 | 父子进程并发执行 | 正常现象,可用 `wait` 控制部分顺序 |
|
||||||
|
| 进程数不对 | `fork` 位置错误 | 确保在正确的位置 `fork`,避免重复创建 |
|
||||||
|
| 僵尸进程 | 父进程未 `wait` | 每个 `fork` 后都要有对应的 `wait` |
|
||||||
|
| p11 的 PPID 不是 p1 | 父进程先退出 | 正常现象(孤儿进程被 init 收养),可用 `sleep` 验证 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务二:task52.c -- 交互式 shell
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
实现一个简化版的交互式 shell,具备以下功能:
|
||||||
|
|
||||||
|
1. 显示提示符 `%`,等待用户输入命令
|
||||||
|
2. 解析并执行用户输入的外部命令
|
||||||
|
3. 输入 `exit` 退出 shell
|
||||||
|
4. 支持输出重定向(`>`)
|
||||||
|
5. 支持管道(`|`)
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
|
||||||
|
#define MAX_LINE 1024
|
||||||
|
#define MAX_ARGS 64
|
||||||
|
|
||||||
|
// 解析命令行为参数数组
|
||||||
|
int parse(char *line, char **args) {
|
||||||
|
int argc = 0;
|
||||||
|
char *token = strtok(line, " \t\n");
|
||||||
|
while (token != NULL && argc < MAX_ARGS - 1) {
|
||||||
|
args[argc++] = token;
|
||||||
|
token = strtok(NULL, " \t\n");
|
||||||
|
}
|
||||||
|
args[argc] = NULL;
|
||||||
|
return argc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理输出重定向
|
||||||
|
void handle_redirect(char **args) {
|
||||||
|
for (int i = 0; args[i] != NULL; i++) {
|
||||||
|
if (strcmp(args[i], ">") == 0) {
|
||||||
|
int fd = open(args[i + 1],
|
||||||
|
O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
dup2(fd, STDOUT_FILENO);
|
||||||
|
close(fd);
|
||||||
|
args[i] = NULL;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理管道
|
||||||
|
void handle_pipe(char *line) {
|
||||||
|
char *cmd1 = strtok(line, "|");
|
||||||
|
char *cmd2 = strtok(NULL, "|");
|
||||||
|
|
||||||
|
int pipefd[2];
|
||||||
|
pipe(pipefd);
|
||||||
|
|
||||||
|
pid_t pid1 = fork();
|
||||||
|
if (pid1 == 0) {
|
||||||
|
close(pipefd[0]);
|
||||||
|
dup2(pipefd[1], STDOUT_FILENO);
|
||||||
|
close(pipefd[1]);
|
||||||
|
char *args[MAX_ARGS];
|
||||||
|
parse(cmd1, args);
|
||||||
|
execvp(args[0], args);
|
||||||
|
perror("execvp");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pid_t pid2 = fork();
|
||||||
|
if (pid2 == 0) {
|
||||||
|
close(pipefd[1]);
|
||||||
|
dup2(pipefd[0], STDIN_FILENO);
|
||||||
|
close(pipefd[0]);
|
||||||
|
char *args[MAX_ARGS];
|
||||||
|
parse(cmd2, args);
|
||||||
|
execvp(args[0], args);
|
||||||
|
perror("execvp");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(pipefd[0]);
|
||||||
|
close(pipefd[1]);
|
||||||
|
waitpid(pid1, NULL, 0);
|
||||||
|
waitpid(pid2, NULL, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
char line[MAX_LINE];
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
printf("%% ");
|
||||||
|
fflush(stdout);
|
||||||
|
|
||||||
|
if (fgets(line, sizeof(line), stdin) == NULL) break;
|
||||||
|
if (strncmp(line, "exit", 4) == 0) break;
|
||||||
|
|
||||||
|
if (strchr(line, '|') != NULL) {
|
||||||
|
handle_pipe(line);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
char *args[MAX_ARGS];
|
||||||
|
parse(line, args);
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
handle_redirect(args);
|
||||||
|
execvp(args[0], args);
|
||||||
|
perror("execvp");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
waitpid(pid, NULL, 0);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| shell 卡住不响应 | `fgets` 缓冲问题 | 输入后确保有换行符,提示符后用 `fflush(stdout)` |
|
||||||
|
| 重定向不生效 | `dup2` 调用时机不对 | 必须在 `execvp` 之前调用 `dup2` |
|
||||||
|
| 管道只执行一半 | 文件描述符未关闭 | 父子进程中都要关闭不用的管道端 |
|
||||||
|
| `exit` 无法退出 | 字符串比较逻辑错误 | 用 `strncmp` 精确匹配前 4 个字符 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务三:task53.c -- daemon 文件监控
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
1. 创建守护进程(daemon),后台运行
|
||||||
|
2. 每隔 5 分钟读取 `task53.c` 文件内容,计算 hash 值
|
||||||
|
3. 与上一次的 hash 值对比
|
||||||
|
4. 如果文件被篡改,将事件记录到日志文件
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
// djb2 哈希算法
|
||||||
|
unsigned long compute_hash(const char *filename) {
|
||||||
|
FILE *fp = fopen(filename, "r");
|
||||||
|
if (!fp) return 0;
|
||||||
|
unsigned long hash = 5381;
|
||||||
|
int c;
|
||||||
|
while ((c = fgetc(fp)) != EOF)
|
||||||
|
hash = ((hash << 5) + hash) + c;
|
||||||
|
fclose(fp);
|
||||||
|
return hash;
|
||||||
|
}
|
||||||
|
|
||||||
|
// daemon 创建五步法
|
||||||
|
void daemonize() {
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid < 0) exit(1);
|
||||||
|
if (pid > 0) exit(0);
|
||||||
|
|
||||||
|
setsid();
|
||||||
|
|
||||||
|
pid = fork();
|
||||||
|
if (pid < 0) exit(1);
|
||||||
|
if (pid > 0) exit(0);
|
||||||
|
|
||||||
|
chdir("/");
|
||||||
|
|
||||||
|
close(STDIN_FILENO);
|
||||||
|
close(STDOUT_FILENO);
|
||||||
|
close(STDERR_FILENO);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
daemonize();
|
||||||
|
|
||||||
|
unsigned long last_hash = compute_hash("task53.c");
|
||||||
|
FILE *logfp;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
sleep(300);
|
||||||
|
|
||||||
|
unsigned long current_hash = compute_hash("task53.c");
|
||||||
|
if (current_hash != last_hash) {
|
||||||
|
logfp = fopen("/var/log/task53.log", "a");
|
||||||
|
if (logfp) {
|
||||||
|
time_t now = time(NULL);
|
||||||
|
fprintf(logfp, "[%s] task53.c changed! "
|
||||||
|
"old=%lx, new=%lx\n",
|
||||||
|
ctime(&now), last_hash, current_hash);
|
||||||
|
fclose(logfp);
|
||||||
|
}
|
||||||
|
last_hash = current_hash;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### daemon 创建步骤(五步法)
|
||||||
|
|
||||||
|
```
|
||||||
|
1. fork() + 父进程 exit -- 脱离终端控制
|
||||||
|
2. setsid() -- 创建新会话,成为会话首进程
|
||||||
|
3. fork() + 父进程 exit -- 确保不会重新获得控制终端
|
||||||
|
4. chdir("/") -- 避免占用可卸载的文件系统
|
||||||
|
5. 关闭 0/1/2 -- 释放标准输入输出
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| daemon 无法后台运行 | 没有正确 `fork` 两次 | 严格按照五步法创建 |
|
||||||
|
| 日志文件无输出 | 路径权限问题 | 检查 `/var/log/` 写权限,或改用用户目录 |
|
||||||
|
| 如何终止 daemon | 无交互终端 | `ps aux | grep task53` 找到 PID 后 `kill` |
|
||||||
|
| hash 算法冲突 | 简单 hash 可能碰撞 | 实验中 djb2 即可,正式场景可用 MD5/SHA |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务四:task54.c -- 信号管理子进程(选做)
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
实现一个通过信号管理子进程的程序,支持以下命令:
|
||||||
|
|
||||||
|
- `create`:创建一个子进程
|
||||||
|
- `kill <pid>`:向指定子进程发送 SIGTERM
|
||||||
|
- `ps`:列出所有存活的子进程
|
||||||
|
- `exit`:终止所有子进程并退出
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
#define MAX_CHILDREN 64
|
||||||
|
|
||||||
|
pid_t children[MAX_CHILDREN];
|
||||||
|
int child_count = 0;
|
||||||
|
|
||||||
|
void sigchld_handler(int sig) {
|
||||||
|
pid_t pid;
|
||||||
|
while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
|
||||||
|
for (int i = 0; i < child_count; i++) {
|
||||||
|
if (children[i] == pid) {
|
||||||
|
children[i] = children[--child_count];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
printf("[parent] child %d terminated\n", pid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void child_work() {
|
||||||
|
while (1) sleep(10);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
struct sigaction sa;
|
||||||
|
sa.sa_handler = sigchld_handler;
|
||||||
|
sigemptyset(&sa.sa_mask);
|
||||||
|
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
|
||||||
|
sigaction(SIGCHLD, &sa, NULL);
|
||||||
|
|
||||||
|
char cmd[64];
|
||||||
|
while (1) {
|
||||||
|
printf("cmd> ");
|
||||||
|
fflush(stdout);
|
||||||
|
if (fgets(cmd, sizeof(cmd), stdin) == NULL) break;
|
||||||
|
|
||||||
|
if (strncmp(cmd, "create", 6) == 0) {
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) { child_work(); exit(0); }
|
||||||
|
children[child_count++] = pid;
|
||||||
|
printf("created child PID=%d\n", pid);
|
||||||
|
} else if (strncmp(cmd, "kill", 4) == 0) {
|
||||||
|
pid_t pid;
|
||||||
|
sscanf(cmd + 5, "%d", &pid);
|
||||||
|
kill(pid, SIGTERM);
|
||||||
|
} else if (strncmp(cmd, "ps", 2) == 0) {
|
||||||
|
printf("alive children: ");
|
||||||
|
for (int i = 0; i < child_count; i++)
|
||||||
|
printf("%d ", children[i]);
|
||||||
|
printf("\n");
|
||||||
|
} else if (strncmp(cmd, "exit", 4) == 0) {
|
||||||
|
for (int i = 0; i < child_count; i++)
|
||||||
|
kill(children[i], SIGTERM);
|
||||||
|
sleep(1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 僵尸进程残留 | `SIGCHLD` 未正确处理 | handler 中用 `waitpid(-1, ..., WNOHANG)` 循环回收 |
|
||||||
|
| `ps` 列表不准 | handler 与主程序竞争共享数组 | 使用信号屏蔽(`sigprocmask`)保护临界区 |
|
||||||
|
| 子进程无法终止 | 信号被忽略或屏蔽 | 确保子进程没有屏蔽 `SIGTERM` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实验总结
|
||||||
|
|
||||||
|
通过本实验,应掌握以下能力:
|
||||||
|
|
||||||
|
1. 使用 `fork` 创建进程树,理解父子进程关系
|
||||||
|
2. 使用 `exec` 家族函数执行外部命令
|
||||||
|
3. 使用 `wait`/`waitpid` 回收子进程,避免僵尸进程
|
||||||
|
4. 实现简单的 shell,理解 shell 的工作原理
|
||||||
|
5. 创建守护进程,理解 daemon 的设计模式
|
||||||
|
6. 使用信号机制进行进程间通信
|
||||||
470
操作系统/实验/实验03_多线程编程.md
Normal file
470
操作系统/实验/实验03_多线程编程.md
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
# 实验03 Linux 多线程编程
|
||||||
|
|
||||||
|
## 实验目的
|
||||||
|
|
||||||
|
1. 掌握 POSIX 线程(pthread)的创建、等待和终止
|
||||||
|
2. 理解线程间共享地址空间的特性
|
||||||
|
3. 掌握信号量(semaphore)在线程同步中的使用
|
||||||
|
4. 理解竞态条件(race condition)的成因及修复方法
|
||||||
|
5. 学会使用生产者-消费者模型解决缓冲区同步问题
|
||||||
|
6. 了解并行计算中的加速比概念
|
||||||
|
|
||||||
|
## 涉及知识点
|
||||||
|
|
||||||
|
- `pthread_create` / `pthread_join` / `pthread_detach`
|
||||||
|
- POSIX 信号量:`sem_init` / `sem_wait`(P 操作) / `sem_post`(V 操作)
|
||||||
|
- 互斥锁(mutex)与信号量的区别
|
||||||
|
- 生产者-消费者问题
|
||||||
|
- 竞态条件与临界区保护
|
||||||
|
- 并行求和与加速比测量
|
||||||
|
- `fork` vs `pthread_create` 的开销对比
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务一:task61.c —— 三线程交替打印
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
创建 3 个线程 T1、T2、T3,每个线程执行 5 次 `printf` 打印自己的线程 ID 和当前是第几次打印,每次打印后随机等待 1~5 秒。
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
#define NUM_THREADS 3
|
||||||
|
#define PRINT_COUNT 5
|
||||||
|
|
||||||
|
void *thread_func(void *arg) {
|
||||||
|
int id = *(int *)arg;
|
||||||
|
for (int i = 0; i < PRINT_COUNT; i++) {
|
||||||
|
printf("T%d: 第%d次打印 (PID=%d, TID=%lu)\n",
|
||||||
|
id, i + 1, getpid(), (unsigned long)pthread_self());
|
||||||
|
int wait_time = rand() % 5 + 1; // 1~5 秒
|
||||||
|
sleep(wait_time);
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
pthread_t threads[NUM_THREADS];
|
||||||
|
int ids[NUM_THREADS];
|
||||||
|
|
||||||
|
srand(time(NULL));
|
||||||
|
|
||||||
|
for (int i = 0; i < NUM_THREADS; i++) {
|
||||||
|
ids[i] = i + 1;
|
||||||
|
pthread_create(&threads[i], NULL, thread_func, &ids[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < NUM_THREADS; i++)
|
||||||
|
pthread_join(threads[i], NULL);
|
||||||
|
|
||||||
|
printf("所有线程执行完毕\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编译:gcc -o task61 task61.c -lpthread
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 输出交错混乱 | 多线程并发写 stdout | 正常现象,可用互斥锁保护 `printf` |
|
||||||
|
| 传参错误 | 循环变量地址被覆盖 | 使用数组存储各线程参数,而非循环变量地址 |
|
||||||
|
| 编译报错 undefined reference | 未链接 pthread | 加 `-lpthread` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务二:task62.c —— 用信号量修复竞态条件
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
给定一个存在竞态条件的 `badcount.c` 程序(多线程同时对共享计数器自增),使用信号量修复该问题,使最终结果正确。
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <semaphore.h>
|
||||||
|
|
||||||
|
#define NTHREADS 4
|
||||||
|
#define NITERS 1000000
|
||||||
|
|
||||||
|
volatile long counter = 0; // 共享计数器
|
||||||
|
sem_t mutex; // 信号量(用作互斥锁)
|
||||||
|
|
||||||
|
void *badcount(void *arg) {
|
||||||
|
for (int i = 0; i < NITERS; i++) {
|
||||||
|
sem_wait(&mutex); // P 操作:进入临界区
|
||||||
|
counter++;
|
||||||
|
sem_post(&mutex); // V 操作:离开临界区
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
pthread_t threads[NTHREADS];
|
||||||
|
|
||||||
|
sem_init(&mutex, 0, 1); // 初始值为 1,相当于互斥锁
|
||||||
|
|
||||||
|
for (int i = 0; i < NTHREADS; i++)
|
||||||
|
pthread_create(&threads[i], NULL, badcount, NULL);
|
||||||
|
|
||||||
|
for (int i = 0; i < NTHREADS; i++)
|
||||||
|
pthread_join(threads[i], NULL);
|
||||||
|
|
||||||
|
printf("期望值: %d\n", NTHREADS * NITERS);
|
||||||
|
printf("实际值: %ld\n", counter);
|
||||||
|
printf("差值: %ld\n",
|
||||||
|
(long)(NTHREADS * NITERS) - counter);
|
||||||
|
|
||||||
|
sem_destroy(&mutex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 修复原理
|
||||||
|
|
||||||
|
- 不加信号量时,`counter++` 不是原子操作(读-改-写三步),多线程交错执行导致丢失更新
|
||||||
|
- `sem_wait` / `sem_post` 将 `counter++` 包裹为临界区,保证同一时刻只有一个线程执行
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 修复后差值仍不为 0 | 信号量使用错误 | 确保 `sem_wait`/`sem_post` 配对,且初始值为 1 |
|
||||||
|
| 程序死锁 | `sem_wait` 多次但 `sem_post` 不足 | 检查每个 `sem_wait` 是否有对应的 `sem_post` |
|
||||||
|
| 性能下降严重 | 信号量粒度太大 | 可尝试减小临界区范围 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务三:task63.c —— 生产者-消费者问题
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
实现 k 个生产者线程和 m 个消费者线程,共享一个大小为 N 的环形缓冲区。生产者向缓冲区放入数据,消费者从缓冲区取出数据,使用信号量实现同步。
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <semaphore.h>
|
||||||
|
|
||||||
|
#define N 10 // 缓冲区大小
|
||||||
|
#define K 3 // 生产者数量
|
||||||
|
#define M 2 // 消费者数量
|
||||||
|
#define ITERS 20 // 每个生产者生产数量
|
||||||
|
|
||||||
|
int buffer[N]; // 环形缓冲区
|
||||||
|
int in = 0; // 生产者写入位置
|
||||||
|
int out = 0; // 消费者读取位置
|
||||||
|
|
||||||
|
sem_t mutex; // 互斥访问缓冲区
|
||||||
|
sem_t slots; // 空闲槽位数(初始 N)
|
||||||
|
sem_t items; // 已有物品数(初始 0)
|
||||||
|
|
||||||
|
void *producer(void *arg) {
|
||||||
|
int id = *(int *)arg;
|
||||||
|
for (int i = 0; i < ITERS; i++) {
|
||||||
|
int item = id * 100 + i;
|
||||||
|
|
||||||
|
sem_wait(&slots); // 等待空闲槽位
|
||||||
|
sem_wait(&mutex); // 进入临界区
|
||||||
|
|
||||||
|
buffer[in] = item;
|
||||||
|
printf("生产者%d: 放入 buffer[%d] = %d\n", id, in, item);
|
||||||
|
in = (in + 1) % N;
|
||||||
|
|
||||||
|
sem_post(&mutex); // 离开临界区
|
||||||
|
sem_post(&items); // 增加物品计数
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *consumer(void *arg) {
|
||||||
|
int id = *(int *)arg;
|
||||||
|
int total = (K * ITERS) / M; // 每个消费者消费数量
|
||||||
|
for (int i = 0; i < total; i++) {
|
||||||
|
sem_wait(&items); // 等待物品
|
||||||
|
sem_wait(&mutex); // 进入临界区
|
||||||
|
|
||||||
|
int item = buffer[out];
|
||||||
|
printf("消费者%d: 取出 buffer[%d] = %d\n", id, out, item);
|
||||||
|
out = (out + 1) % N;
|
||||||
|
|
||||||
|
sem_post(&mutex); // 离开临界区
|
||||||
|
sem_post(&slots); // 增加空闲槽位
|
||||||
|
}
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
pthread_t ptid[K], ctid[M];
|
||||||
|
int pids[K], cids[M];
|
||||||
|
|
||||||
|
sem_init(&mutex, 0, 1);
|
||||||
|
sem_init(&slots, 0, N);
|
||||||
|
sem_init(&items, 0, 0);
|
||||||
|
|
||||||
|
for (int i = 0; i < K; i++) {
|
||||||
|
pids[i] = i;
|
||||||
|
pthread_create(&ptid[i], NULL, producer, &pids[i]);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < M; i++) {
|
||||||
|
cids[i] = i;
|
||||||
|
pthread_create(&ctid[i], NULL, consumer, &cids[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < K; i++) pthread_join(ptid[i], NULL);
|
||||||
|
for (int i = 0; i < M; i++) pthread_join(ctid[i], NULL);
|
||||||
|
|
||||||
|
sem_destroy(&mutex);
|
||||||
|
sem_destroy(&slots);
|
||||||
|
sem_destroy(&items);
|
||||||
|
|
||||||
|
printf("所有生产者和消费者完成\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 信号量使用要点
|
||||||
|
|
||||||
|
| 信号量 | 初始值 | 含义 |
|
||||||
|
|--------|--------|------|
|
||||||
|
| `mutex` | 1 | 互斥锁,保护临界区 |
|
||||||
|
| `slots` | N | 空闲槽位数,生产者 P(slots)、消费者 V(slots) |
|
||||||
|
| `items` | 0 | 已有物品数,消费者 P(items)、生产者 V(items) |
|
||||||
|
|
||||||
|
**关键顺序:** P 操作时先 `P(slots/items)` 再 `P(mutex)`,否则可能死锁。
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 死锁 | P 操作顺序不对 | 先 `P(slots)` 再 `P(mutex)` |
|
||||||
|
| 缓冲区越界 | 环形索引计算错误 | 使用 `% N` 取模 |
|
||||||
|
| 生产者/消费者数量不匹配 | 总生产量 != 总消费量 | 合理分配每个线程的生产/消费数量 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务四:task64.c —— 并行求和与加速比
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
实现 `psum64.c`:将长度为 n 的数组分成若干段,每个线程计算一段的部分和,最后汇总得到总和。分别测试 1、2、4、8、16 个线程的执行时间,计算加速比。
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
|
||||||
|
#define MAXN 100000000 // 1 亿元素
|
||||||
|
#define MAX_THREADS 16
|
||||||
|
|
||||||
|
long a[MAXN]; // 全局数组
|
||||||
|
long psum[MAX_THREADS]; // 各线程的部分和
|
||||||
|
int n, num_threads;
|
||||||
|
|
||||||
|
void *sum_thread(void *arg) {
|
||||||
|
int id = *(int *)arg;
|
||||||
|
long chunk = n / num_threads;
|
||||||
|
long start = id * chunk;
|
||||||
|
long end = (id == num_threads - 1) ? n : start + chunk;
|
||||||
|
|
||||||
|
psum[id] = 0;
|
||||||
|
for (long i = start; i < end; i++)
|
||||||
|
psum[id] += a[i];
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
long get_time_us() {
|
||||||
|
struct timeval tv;
|
||||||
|
gettimeofday(&tv, NULL);
|
||||||
|
return tv.tv_sec * 1000000L + tv.tv_usec;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
n = MAXN;
|
||||||
|
for (int i = 0; i < n; i++)
|
||||||
|
a[i] = i + 1; // 初始化数组
|
||||||
|
|
||||||
|
int thread_counts[] = {1, 2, 4, 8, 16};
|
||||||
|
long base_time = 0;
|
||||||
|
|
||||||
|
for (int t = 0; t < 5; t++) {
|
||||||
|
num_threads = thread_counts[t];
|
||||||
|
pthread_t threads[MAX_THREADS];
|
||||||
|
int ids[MAX_THREADS];
|
||||||
|
|
||||||
|
long start = get_time_us();
|
||||||
|
|
||||||
|
for (int i = 0; i < num_threads; i++) {
|
||||||
|
ids[i] = i;
|
||||||
|
pthread_create(&threads[i], NULL, sum_thread, &ids[i]);
|
||||||
|
}
|
||||||
|
for (int i = 0; i < num_threads; i++)
|
||||||
|
pthread_join(threads[i], NULL);
|
||||||
|
|
||||||
|
long total = 0;
|
||||||
|
for (int i = 0; i < num_threads; i++)
|
||||||
|
total += psum[i];
|
||||||
|
|
||||||
|
long elapsed = get_time_us() - start;
|
||||||
|
if (t == 0) base_time = elapsed;
|
||||||
|
|
||||||
|
printf("线程数=%2d, 总和=%ld, 耗时=%ldus, 加速比=%.2f\n",
|
||||||
|
num_threads, total, elapsed,
|
||||||
|
(double)base_time / elapsed);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 加速比不理想 | 线程创建开销、缓存竞争 | 正常现象,线程数超过核心数后收益递减 |
|
||||||
|
| 总和不对 | 分段边界计算错误 | 注意最后一段包含剩余元素 |
|
||||||
|
| 加速比超过线程数 | 计时误差 | 多次测量取平均值 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务五:task66.c —— fork 与 pthread_create 开销对比(选做)
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
分别测量 `fork()` 和 `pthread_create()` 的执行时间,对比进程创建与线程创建的开销差异。
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
|
||||||
|
#define N 10000
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
struct timeval start, end;
|
||||||
|
|
||||||
|
// 测量 fork
|
||||||
|
gettimeofday(&start, NULL);
|
||||||
|
for (int i = 0; i < N; i++) {
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) _exit(0);
|
||||||
|
wait(NULL);
|
||||||
|
}
|
||||||
|
gettimeofday(&end, NULL);
|
||||||
|
printf("fork x%d: %ld us (avg %.1f us)\n", N,
|
||||||
|
end.tv_sec * 1000000L + end.tv_usec
|
||||||
|
- start.tv_sec * 1000000L - start.tv_usec,
|
||||||
|
(double)(end.tv_sec * 1000000L + end.tv_usec
|
||||||
|
- start.tv_sec * 1000000L - start.tv_usec) / N);
|
||||||
|
|
||||||
|
// 测量 pthread_create
|
||||||
|
pthread_t tid;
|
||||||
|
void *dummy(void *a) { return NULL; }
|
||||||
|
|
||||||
|
gettimeofday(&start, NULL);
|
||||||
|
for (int i = 0; i < N; i++) {
|
||||||
|
pthread_create(&tid, NULL, dummy, NULL);
|
||||||
|
pthread_join(tid, NULL);
|
||||||
|
}
|
||||||
|
gettimeofday(&end, NULL);
|
||||||
|
printf("pthread_create x%d: %ld us (avg %.1f us)\n", N,
|
||||||
|
end.tv_sec * 1000000L + end.tv_usec
|
||||||
|
- start.tv_sec * 1000000L - start.tv_usec,
|
||||||
|
(double)(end.tv_sec * 1000000L + end.tv_usec
|
||||||
|
- start.tv_sec * 1000000L - start.tv_usec) / N);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务六:task67.c —— 动态线程池(选做)
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
实现一个动态线程池 `sbuf_t`,当缓冲区满时容量翻倍,当缓冲区空且线程数过多时容量减半。
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
int *buf; // 缓冲区
|
||||||
|
int n; // 容量
|
||||||
|
int front; // 队头
|
||||||
|
int rear; // 队尾
|
||||||
|
sem_t mutex; // 互斥
|
||||||
|
sem_t slots; // 空闲槽位
|
||||||
|
sem_t items; // 已有物品
|
||||||
|
} sbuf_t;
|
||||||
|
|
||||||
|
void sbuf_init(sbuf_t *sp, int n) {
|
||||||
|
sp->buf = malloc(n * sizeof(int));
|
||||||
|
sp->n = n;
|
||||||
|
sp->front = sp->rear = 0;
|
||||||
|
sem_init(&sp->mutex, 0, 1);
|
||||||
|
sem_init(&sp->slots, 0, n);
|
||||||
|
sem_init(&sp->items, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 动态扩容:满时翻倍
|
||||||
|
void sbuf_insert(sbuf_t *sp, int item) {
|
||||||
|
sem_wait(&sp->slots);
|
||||||
|
sem_wait(&sp->mutex);
|
||||||
|
|
||||||
|
sp->buf[sp->rear] = item;
|
||||||
|
sp->rear = (sp->rear + 1) % sp->n;
|
||||||
|
|
||||||
|
// 检查是否需要扩容(简化判断)
|
||||||
|
// 实际实现需要更精细的逻辑
|
||||||
|
|
||||||
|
sem_post(&sp->mutex);
|
||||||
|
sem_post(&sp->items);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sbuf_remove(sbuf_t *sp) {
|
||||||
|
sem_wait(&sp->items);
|
||||||
|
sem_wait(&sp->mutex);
|
||||||
|
|
||||||
|
int item = sp->buf[sp->front];
|
||||||
|
sp->front = (sp->front + 1) % sp->n;
|
||||||
|
|
||||||
|
sem_post(&sp->mutex);
|
||||||
|
sem_post(&sp->slots);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实验总结
|
||||||
|
|
||||||
|
通过本实验,应掌握以下能力:
|
||||||
|
|
||||||
|
1. 使用 `pthread_create`/`pthread_join` 创建和管理线程
|
||||||
|
2. 使用 POSIX 信号量实现线程同步
|
||||||
|
3. 理解竞态条件的成因,学会用信号量/互斥锁保护临界区
|
||||||
|
4. 实现经典的生产者-消费者同步模型
|
||||||
|
5. 理解并行计算中的加速比及其限制因素
|
||||||
|
6. 了解 `fork` 与 `pthread_create` 的开销差异
|
||||||
437
操作系统/实验/实验04_进程间通信.md
Normal file
437
操作系统/实验/实验04_进程间通信.md
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
# 实验04 Linux 进程间通信
|
||||||
|
|
||||||
|
## 实验目的
|
||||||
|
|
||||||
|
1. 掌握管道(pipe)的使用方法
|
||||||
|
2. 掌握 System V 消息队列(message queue)的创建和使用
|
||||||
|
3. 掌握共享内存(shared memory)配合信号量的同步通信
|
||||||
|
4. 了解命名管道(FIFO)在多进程通信中的应用
|
||||||
|
5. 理解不同 IPC 机制的适用场景和性能特点
|
||||||
|
|
||||||
|
## 涉及知识点
|
||||||
|
|
||||||
|
- 匿名管道:`pipe`、父子进程间通信
|
||||||
|
- System V 消息队列:`msgget`、`msgsnd`、`msgrcv`、`msgctl`
|
||||||
|
- System V 共享内存:`shmget`、`shmat`、`shmdt`、`shmctl`
|
||||||
|
- System V 信号量:`semget`、`semop`、`semctl`
|
||||||
|
- 命名管道(FIFO):`mkfifo`
|
||||||
|
- IPC 键值:`ftok`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务一:task71.c —— 父子进程管道通信
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
父进程创建 2 个子进程,通过管道实现父子进程间的通信:
|
||||||
|
|
||||||
|
1. 子进程 1 向管道写入消息
|
||||||
|
2. 子进程 2 从管道读取消息并显示
|
||||||
|
3. 父进程等待所有子进程结束
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int pipefd[2]; // pipefd[0]=读端, pipefd[1]=写端
|
||||||
|
pid_t pid1, pid2;
|
||||||
|
|
||||||
|
if (pipe(pipefd) < 0) {
|
||||||
|
perror("pipe");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子进程 1:写端
|
||||||
|
pid1 = fork();
|
||||||
|
if (pid1 == 0) {
|
||||||
|
close(pipefd[0]); // 关闭读端
|
||||||
|
char *msg = "Hello from child 1!";
|
||||||
|
write(pipefd[1], msg, strlen(msg) + 1);
|
||||||
|
printf("子进程1(PID=%d): 发送消息 -> %s\n", getpid(), msg);
|
||||||
|
close(pipefd[1]);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 子进程 2:读端
|
||||||
|
pid2 = fork();
|
||||||
|
if (pid2 == 0) {
|
||||||
|
close(pipefd[1]); // 关闭写端
|
||||||
|
char buf[256];
|
||||||
|
int n = read(pipefd[0], buf, sizeof(buf));
|
||||||
|
printf("子进程2(PID=%d): 收到消息 -> %s (共%d字节)\n",
|
||||||
|
getpid(), buf, n);
|
||||||
|
close(pipefd[0]);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 父进程:关闭两端,等待子进程
|
||||||
|
close(pipefd[0]);
|
||||||
|
close(pipefd[1]);
|
||||||
|
waitpid(pid1, NULL, 0);
|
||||||
|
waitpid(pid2, NULL, 0);
|
||||||
|
printf("父进程: 所有子进程已结束\n");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关键要点
|
||||||
|
|
||||||
|
- `pipe()` 创建一对文件描述符:`pipefd[0]` 读端、`pipefd[1]` 写端
|
||||||
|
- 不使用的端口必须关闭,否则可能导致读端永远阻塞
|
||||||
|
- 管道是半双工的,数据单向流动
|
||||||
|
- 管道缓冲区大小通常为 64KB(Linux)
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 读端永远阻塞 | 写端未关闭 | 父进程中关闭两端,子进程中关闭不用的端 |
|
||||||
|
| 数据不完整 | `read` 可能短读 | 循环读取直到满足期望字节数 |
|
||||||
|
| 管道破裂 SIGPIPE | 读端已关闭但写端仍在写 | 检查 `write` 返回值,捕获 `SIGPIPE` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务二:task72s.c / task72c.c —— 消息队列客户端/服务器
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
- 服务器(task72s.c):创建消息队列,等待接收客户端消息,处理后回复
|
||||||
|
- 客户端(task72c.c):向消息队列发送请求消息,等待服务器回复
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
**公共头文件定义:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
// msg_def.h
|
||||||
|
#define MSG_KEY 1234
|
||||||
|
#define MSG_TYPE_REQUEST 1
|
||||||
|
#define MSG_TYPE_REPLY 2
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
long mtype; // 消息类型(必须 > 0)
|
||||||
|
char mtext[256]; // 消息内容
|
||||||
|
} MsgBuf;
|
||||||
|
```
|
||||||
|
|
||||||
|
**服务器 task72s.c:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/ipc.h>
|
||||||
|
#include <sys/msg.h>
|
||||||
|
#include "msg_def.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// 创建消息队列
|
||||||
|
int msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
|
||||||
|
if (msqid < 0) { perror("msgget"); exit(1); }
|
||||||
|
printf("服务器: 消息队列已创建 (ID=%d)\n", msqid);
|
||||||
|
|
||||||
|
MsgBuf msg;
|
||||||
|
while (1) {
|
||||||
|
// 掭收类型为 MSG_TYPE_REQUEST 的消息
|
||||||
|
if (msgrcv(msqid, &msg, sizeof(msg.mtext),
|
||||||
|
MSG_TYPE_REQUEST, 0) < 0) {
|
||||||
|
perror("msgrcv");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
printf("服务器: 收到请求 -> %s\n", msg.mtext);
|
||||||
|
|
||||||
|
// 处理请求(简单回显)
|
||||||
|
msg.mtype = MSG_TYPE_REPLY;
|
||||||
|
snprintf(msg.mtext, sizeof(msg.mtext),
|
||||||
|
"服务器回复: 已收到你的消息");
|
||||||
|
msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除消息队列
|
||||||
|
msgctl(msqid, IPC_RMID, NULL);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**客户端 task72c.c:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/ipc.h>
|
||||||
|
#include <sys/msg.h>
|
||||||
|
#include "msg_def.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
int msqid = msgget(MSG_KEY, 0666);
|
||||||
|
if (msqid < 0) { perror("msgget"); exit(1); }
|
||||||
|
|
||||||
|
MsgBuf msg;
|
||||||
|
msg.mtype = MSG_TYPE_REQUEST;
|
||||||
|
printf("请输入消息: ");
|
||||||
|
fgets(msg.mtext, sizeof(msg.mtext), stdin);
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0);
|
||||||
|
|
||||||
|
// 接收回复
|
||||||
|
msgrcv(msqid, &msg, sizeof(msg.mtext), MSG_TYPE_REPLY, 0);
|
||||||
|
printf("客户端: %s\n", msg.mtext);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `msgget` 失败 | 消息队列不存在或权限不足 | 确保服务器先启动,检查权限 |
|
||||||
|
| `msgrcv` 阻塞 | 没有对应类型的消息 | 检查 `mtype` 是否匹配 |
|
||||||
|
| 消息队列残留 | 程序异常退出未删除 | 用 `ipcrm -q <msqid>` 手动删除 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务三:task73.c —— 共享内存 + IPC 信号量同步
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
使用共享内存实现两个进程间的数据传输,配合 System V 信号量实现同步。发送方写入 1~10,接收方依次读取并显示。
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/ipc.h>
|
||||||
|
#include <sys/shm.h>
|
||||||
|
#include <sys/sem.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
|
||||||
|
#define SHM_KEY 5678
|
||||||
|
#define SEM_KEY 9012
|
||||||
|
|
||||||
|
// 共享内存结构
|
||||||
|
typedef struct {
|
||||||
|
int data;
|
||||||
|
int done; // 发送方是否完成
|
||||||
|
} SharedData;
|
||||||
|
|
||||||
|
// semop 辅助函数
|
||||||
|
void sem_op(int semid, int semnum, int op) {
|
||||||
|
struct sembuf sb = {semnum, op, 0};
|
||||||
|
semop(semid, &sb, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// 创建共享内存
|
||||||
|
int shmid = shmget(SHM_KEY, sizeof(SharedData),
|
||||||
|
IPC_CREAT | 0666);
|
||||||
|
SharedData *shm = (SharedData *)shmat(shmid, NULL, 0);
|
||||||
|
|
||||||
|
// 创建信号量:0=可用槽位, 1=已有数据
|
||||||
|
int semid = semget(SEM_KEY, 2, IPC_CREAT | 0666);
|
||||||
|
semctl(semid, 0, SETVAL, 1); // sem[0]=1: 可写
|
||||||
|
semctl(semid, 1, SETVAL, 0); // sem[1]=0: 无数据
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
// 接收方
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
sem_op(semid, 1, -1); // 等待有数据
|
||||||
|
printf("接收: %d\n", shm->data);
|
||||||
|
sem_op(semid, 0, +1); // 释放可写
|
||||||
|
}
|
||||||
|
shmdt(shm);
|
||||||
|
exit(0);
|
||||||
|
} else {
|
||||||
|
// 发送方
|
||||||
|
for (int i = 1; i <= 10; i++) {
|
||||||
|
sem_op(semid, 0, -1); // 等待可写
|
||||||
|
shm->data = i;
|
||||||
|
printf("发送: %d\n", i);
|
||||||
|
sem_op(semid, 1, +1); // 通知有数据
|
||||||
|
}
|
||||||
|
wait(NULL);
|
||||||
|
|
||||||
|
// 清理
|
||||||
|
shmdt(shm);
|
||||||
|
shmctl(shmid, IPC_RMID, NULL);
|
||||||
|
semctl(semid, 0, IPC_RMID);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### IPC 资源管理
|
||||||
|
|
||||||
|
| 操作 | 命令 |
|
||||||
|
|------|------|
|
||||||
|
| 查看共享内存 | `ipcs -m` |
|
||||||
|
| 查看信号量 | `ipcs -s` |
|
||||||
|
| 查看消息队列 | `ipcs -q` |
|
||||||
|
| 删除共享内存 | `ipcrm -m <shmid>` |
|
||||||
|
| 删除信号量 | `ipcrm -s <semid>` |
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 共享内存未清理 | 程序异常退出 | 用 `ipcs` 查看,`ipcrm` 手动删除 |
|
||||||
|
| 数据竞争 | 未使用信号量同步 | 确保发送方写完后才通知接收方读取 |
|
||||||
|
| `shmat` 返回 -1 | 权限不足或 key 冲突 | 检查权限,换用不同 key |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务四:task74s.c / task74c.c —— 多进程并发服务器(选做)
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
使用命名管道(FIFO)实现一个多进程并发服务器:
|
||||||
|
|
||||||
|
- 服务器创建一个公共 FIFO 接收客户端请求
|
||||||
|
- 每个客户端创建自己的私有 FIFO 用于接收回复
|
||||||
|
- 服务器为每个请求 fork 一个子进程处理
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
**服务器 task74s.c:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
|
#define SERVER_FIFO "/tmp/server_fifo"
|
||||||
|
#define CLIENT_FIFO_TEMPLATE "/tmp/client_%d_fifo"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
pid_t client_pid;
|
||||||
|
char message[256];
|
||||||
|
} Request;
|
||||||
|
|
||||||
|
void sigchld_handler(int sig) {
|
||||||
|
while (waitpid(-1, NULL, WNOHANG) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// 创建服务器 FIFO
|
||||||
|
unlink(SERVER_FIFO);
|
||||||
|
mkfifo(SERVER_FIFO, 0666);
|
||||||
|
|
||||||
|
signal(SIGCHLD, sigchld_handler);
|
||||||
|
|
||||||
|
printf("服务器启动,等待连接...\n");
|
||||||
|
|
||||||
|
int server_fd = open(SERVER_FIFO, O_RDONLY);
|
||||||
|
Request req;
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
int n = read(server_fd, &req, sizeof(Request));
|
||||||
|
if (n <= 0) continue;
|
||||||
|
|
||||||
|
pid_t pid = fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
// 子进程处理请求
|
||||||
|
char client_fifo[256];
|
||||||
|
snprintf(client_fifo, sizeof(client_fifo),
|
||||||
|
CLIENT_FIFO_TEMPLATE, req.client_pid);
|
||||||
|
|
||||||
|
int client_fd = open(client_fifo, O_WRONLY);
|
||||||
|
char reply[256];
|
||||||
|
snprintf(reply, sizeof(reply),
|
||||||
|
"服务器已处理: %s", req.message);
|
||||||
|
write(client_fd, reply, strlen(reply) + 1);
|
||||||
|
close(client_fd);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(server_fd);
|
||||||
|
unlink(SERVER_FIFO);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**客户端 task74c.c:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#define SERVER_FIFO "/tmp/server_fifo"
|
||||||
|
#define CLIENT_FIFO_TEMPLATE "/tmp/client_%d_fifo"
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
pid_t client_pid;
|
||||||
|
char message[256];
|
||||||
|
} Request;
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// 创建私有 FIFO
|
||||||
|
char client_fifo[256];
|
||||||
|
snprintf(client_fifo, sizeof(client_fifo),
|
||||||
|
CLIENT_FIFO_TEMPLATE, getpid());
|
||||||
|
unlink(client_fifo);
|
||||||
|
mkfifo(client_fifo, 0666);
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
|
Request req;
|
||||||
|
req.client_pid = getpid();
|
||||||
|
printf("请输入消息: ");
|
||||||
|
fgets(req.message, sizeof(req.message), stdin);
|
||||||
|
|
||||||
|
int server_fd = open(SERVER_FIFO, O_WRONLY);
|
||||||
|
write(server_fd, &req, sizeof(Request));
|
||||||
|
close(server_fd);
|
||||||
|
|
||||||
|
// 接收回复
|
||||||
|
int client_fd = open(client_fifo, O_RDONLY);
|
||||||
|
char reply[256];
|
||||||
|
read(client_fd, reply, sizeof(reply));
|
||||||
|
printf("收到回复: %s\n", reply);
|
||||||
|
|
||||||
|
close(client_fd);
|
||||||
|
unlink(client_fifo);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| FIFO 文件残留 | 程序异常退出 | 启动时 `unlink` 清理旧 FIFO |
|
||||||
|
| `open` 阻塞 | FIFO 另一端未打开 | 确保服务器先启动 |
|
||||||
|
| 多客户端并发时数据混乱 | FIFO 是字节流,无消息边界 | 使用定长结构体或长度前缀协议 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实验总结
|
||||||
|
|
||||||
|
通过本实验,应掌握以下能力:
|
||||||
|
|
||||||
|
1. 使用匿名管道实现父子进程间通信
|
||||||
|
2. 使用消息队列实现任意进程间的消息传递
|
||||||
|
3. 使用共享内存实现高速数据传输,配合信号量同步
|
||||||
|
4. 使用命名管道实现无亲缘关系进程间的通信
|
||||||
|
5. 理解不同 IPC 机制的优缺点和适用场景
|
||||||
423
操作系统/实验/实验05_网络通信.md
Normal file
423
操作系统/实验/实验05_网络通信.md
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
# 实验05 Linux 网络通信编程
|
||||||
|
|
||||||
|
## 实验目的
|
||||||
|
|
||||||
|
1. 掌握 Socket 编程的基本流程(socket/bind/listen/accept/connect)
|
||||||
|
2. 理解 TCP 客户端/服务器模型
|
||||||
|
3. 学会实现文件下载和远程 shell 等网络应用
|
||||||
|
4. 掌握 HTTP 协议的基本交互方式
|
||||||
|
5. 了解静态网页和动态网页的生成原理
|
||||||
|
|
||||||
|
## 涉及知识点
|
||||||
|
|
||||||
|
- Socket 地址结构:`sockaddr_in`、`inet_ntoa`、`htonl`/`htons`
|
||||||
|
- TCP 服务器流程:`socket` -> `bind` -> `listen` -> `accept` -> `read/write`
|
||||||
|
- TCP 客户端流程:`socket` -> `connect` -> `read/write`
|
||||||
|
- HTTP 请求/响应格式
|
||||||
|
- 文件传输与 `send`/`recv`
|
||||||
|
- 进程与网络 I/O 结合(远程 shell)
|
||||||
|
- Wrapper 库辅助函数:`open_listen_sock`、`open_client_sock`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务一:toggle 服务器测试
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
测试课程提供的 toggle 服务器和客户端程序,理解基本的 Socket 通信流程。
|
||||||
|
|
||||||
|
### 操作步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编译
|
||||||
|
gcc -o toggles toggle_server.c -L. -lwrapper
|
||||||
|
gcc -o togglec toggle_client.c -L. -lwrapper
|
||||||
|
|
||||||
|
# 终端 1:启动服务器
|
||||||
|
./toggles 8080
|
||||||
|
|
||||||
|
# 终端 2:启动客户端
|
||||||
|
./togglec localhost 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
### TCP 服务器基本流程
|
||||||
|
|
||||||
|
```
|
||||||
|
socket() -- 创建套接字
|
||||||
|
|
|
||||||
|
bind() -- 绑定地址和端口
|
||||||
|
|
|
||||||
|
listen() -- 监听连接
|
||||||
|
|
|
||||||
|
while(1) {
|
||||||
|
accept() -- 接受客户端连接
|
||||||
|
|
|
||||||
|
read() -- 读取请求
|
||||||
|
|
|
||||||
|
write() -- 发送响应
|
||||||
|
|
|
||||||
|
close() -- 关闭连接
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `bind` 失败 "Address already in use" | 端口被占用 | 等待几分钟或用 `setsockopt` 设置 `SO_REUSEADDR` |
|
||||||
|
| 客户端连接超时 | 服务器未启动或防火墙 | 检查服务器状态和端口是否开放 |
|
||||||
|
| 中文乱码 | 编码不一致 | 统一使用 UTF-8 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务二:weblet 服务器
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
测试课程提供的 weblet 服务器,理解 HTTP 请求处理:
|
||||||
|
|
||||||
|
1. 静态网页服务(返回 HTML 文件)
|
||||||
|
2. 动态网页生成(CGI 方式)
|
||||||
|
|
||||||
|
### 操作步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 编译
|
||||||
|
gcc -o weblet weblet.c -L. -lwrapper
|
||||||
|
|
||||||
|
# 启动 weblet 服务器
|
||||||
|
./weblet 8080
|
||||||
|
|
||||||
|
# 在浏览器访问
|
||||||
|
# http://localhost:8080/index.html
|
||||||
|
# http://localhost:8080/cgi-bin/hello
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP 请求格式
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /index.html HTTP/1.1
|
||||||
|
Host: localhost:8080
|
||||||
|
Connection: close
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP 响应格式
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP/1.1 200 OK
|
||||||
|
Content-Type: text/html
|
||||||
|
Content-Length: 123
|
||||||
|
|
||||||
|
<html>...</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 404 Not Found | 文件路径错误 | 检查 `DocumentRoot` 和请求路径 |
|
||||||
|
| 中文乱码 | Content-Type 缺少 charset | 添加 `Content-Type: text/html; charset=utf-8` |
|
||||||
|
| 浏览器无法访问 | 防火墙或端口未开放 | 关闭防火墙或开放对应端口 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务三:task83s.c / task83c.c —— 文件下载
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
实现一个简单的文件下载服务:
|
||||||
|
|
||||||
|
- 客户端发送文件名请求
|
||||||
|
- 服务器查找文件并返回文件内容
|
||||||
|
- 客户端接收并保存到本地
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
**服务器 task83s.c:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
|
#define MAXLINE 8192
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc != 2) {
|
||||||
|
fprintf(stderr, "用法: %s <端口>\n", argv[0]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int port = atoi(argv[1]);
|
||||||
|
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
|
||||||
|
struct sockaddr_in servaddr;
|
||||||
|
memset(&servaddr, 0, sizeof(servaddr));
|
||||||
|
servaddr.sin_family = AF_INET;
|
||||||
|
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||||
|
servaddr.sin_port = htons(port);
|
||||||
|
|
||||||
|
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
|
||||||
|
listen(listen_fd, 5);
|
||||||
|
|
||||||
|
printf("文件下载服务器启动,端口 %d\n", port);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
struct sockaddr_in cliaddr;
|
||||||
|
socklen_t clien = sizeof(cliaddr);
|
||||||
|
int conn_fd = accept(listen_fd,
|
||||||
|
(struct sockaddr *)&cliaddr, &clien);
|
||||||
|
printf("客户端 %s:%d 已连接\n",
|
||||||
|
inet_ntoa(cliaddr.sin_addr),
|
||||||
|
ntohs(cliaddr.sin_port));
|
||||||
|
|
||||||
|
// 读取文件名
|
||||||
|
char filename[MAXLINE];
|
||||||
|
int n = recv(conn_fd, filename, MAXLINE - 1, 0);
|
||||||
|
filename[n] = '\0';
|
||||||
|
|
||||||
|
// 打开并发送文件
|
||||||
|
int file_fd = open(filename, O_RDONLY);
|
||||||
|
if (file_fd < 0) {
|
||||||
|
send(conn_fd, "FILE_NOT_FOUND", 14, 0);
|
||||||
|
} else {
|
||||||
|
char buf[MAXLINE];
|
||||||
|
while ((n = read(file_fd, buf, MAXLINE)) > 0)
|
||||||
|
send(conn_fd, buf, n, 0);
|
||||||
|
close(file_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(conn_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(listen_fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**客户端 task83c.c:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
|
||||||
|
#define MAXLINE 8192
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc != 4) {
|
||||||
|
fprintf(stderr, "用法: %s <主机> <端口> <文件名>\n", argv[0]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析服务器地址
|
||||||
|
struct hostent *hp = gethostbyname(argv[1]);
|
||||||
|
int port = atoi(argv[2]);
|
||||||
|
|
||||||
|
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
|
||||||
|
struct sockaddr_in servaddr;
|
||||||
|
memset(&servaddr, 0, sizeof(servaddr));
|
||||||
|
servaddr.sin_family = AF_INET;
|
||||||
|
memcpy(&servaddr.sin_addr, hp->h_addr, hp->h_length);
|
||||||
|
servaddr.sin_port = htons(port);
|
||||||
|
|
||||||
|
connect(sock_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
|
||||||
|
|
||||||
|
// 发送文件名
|
||||||
|
send(sock_fd, argv[3], strlen(argv[3]), 0);
|
||||||
|
|
||||||
|
// 接收文件内容
|
||||||
|
char buf[MAXLINE];
|
||||||
|
int n;
|
||||||
|
char save_name[256];
|
||||||
|
snprintf(save_name, sizeof(save_name), "downloaded_%s", argv[3]);
|
||||||
|
int out_fd = open(save_name, O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||||
|
|
||||||
|
while ((n = recv(sock_fd, buf, MAXLINE, 0)) > 0) {
|
||||||
|
write(out_fd, buf, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(out_fd);
|
||||||
|
close(sock_fd);
|
||||||
|
printf("文件已下载为 %s\n", save_name);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 下载的文件不完整 | `send`/`recv` 短传 | 循环发送/接收,检查返回值 |
|
||||||
|
| 文件名含路径 | 安全隐患 | 生产环境应过滤 `..` 等路径遍历 |
|
||||||
|
| 大文件传输失败 | 缓冲区不够大 | 分块传输,每块用 `send` 发送 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务四:task84s.c / task84c.c —— 远程 shell
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
实现一个远程 shell 服务:
|
||||||
|
|
||||||
|
- 客户端发送 shell 命令
|
||||||
|
- 服务器执行命令并返回输出结果
|
||||||
|
- 客户端显示命令输出
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
**服务器 task84s.c:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
|
#define MAXLINE 8192
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc != 2) {
|
||||||
|
fprintf(stderr, "用法: %s <端口>\n", argv[0]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
int port = atoi(argv[1]);
|
||||||
|
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
|
||||||
|
struct sockaddr_in servaddr;
|
||||||
|
memset(&servaddr, 0, sizeof(servaddr));
|
||||||
|
servaddr.sin_family = AF_INET;
|
||||||
|
servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
|
||||||
|
servaddr.sin_port = htons(port);
|
||||||
|
|
||||||
|
int optval = 1;
|
||||||
|
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,
|
||||||
|
&optval, sizeof(optval));
|
||||||
|
|
||||||
|
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
|
||||||
|
listen(listen_fd, 5);
|
||||||
|
printf("远程 shell 服务器启动,端口 %d\n", port);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
struct sockaddr_in cliaddr;
|
||||||
|
socklen_t clien = sizeof(cliaddr);
|
||||||
|
int conn_fd = accept(listen_fd,
|
||||||
|
(struct sockaddr *)&cliaddr, &clien);
|
||||||
|
printf("客户端 %s 已连接\n", inet_ntoa(cliaddr.sin_addr));
|
||||||
|
|
||||||
|
// 用 dup2 将命令输出重定向到 socket
|
||||||
|
if (fork() == 0) {
|
||||||
|
close(listen_fd);
|
||||||
|
dup2(conn_fd, STDOUT_FILENO);
|
||||||
|
dup2(conn_fd, STDERR_FILENO);
|
||||||
|
|
||||||
|
char cmd[MAXLINE];
|
||||||
|
int n;
|
||||||
|
while ((n = recv(conn_fd, cmd, MAXLINE - 1, 0)) > 0) {
|
||||||
|
cmd[n] = '\0';
|
||||||
|
// 去除换行符
|
||||||
|
if (cmd[n - 1] == '\n') cmd[n - 1] = '\0';
|
||||||
|
system(cmd);
|
||||||
|
}
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
close(conn_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
close(listen_fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**客户端 task84c.c:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
|
||||||
|
#define MAXLINE 8192
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc != 3) {
|
||||||
|
fprintf(stderr, "用法: %s <主机> <端口>\n", argv[0]);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct hostent *hp = gethostbyname(argv[1]);
|
||||||
|
int port = atoi(argv[2]);
|
||||||
|
|
||||||
|
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
|
||||||
|
struct sockaddr_in servaddr;
|
||||||
|
memset(&servaddr, 0, sizeof(servaddr));
|
||||||
|
servaddr.sin_family = AF_INET;
|
||||||
|
memcpy(&servaddr.sin_addr, hp->h_addr, hp->h_length);
|
||||||
|
servaddr.sin_port = htons(port);
|
||||||
|
|
||||||
|
connect(sock_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
|
||||||
|
printf("已连接到 %s:%d\n", argv[1], port);
|
||||||
|
|
||||||
|
char cmd[MAXLINE];
|
||||||
|
while (1) {
|
||||||
|
printf("remote> ");
|
||||||
|
fflush(stdout);
|
||||||
|
if (fgets(cmd, sizeof(cmd), stdin) == NULL) break;
|
||||||
|
|
||||||
|
send(sock_fd, cmd, strlen(cmd), 0);
|
||||||
|
|
||||||
|
// 接收输出
|
||||||
|
char buf[MAXLINE];
|
||||||
|
int n;
|
||||||
|
// 简单方式:等待并读取(生产环境需要更复杂的协议)
|
||||||
|
usleep(100000); // 等待服务器执行
|
||||||
|
while ((n = recv(sock_fd, buf, MAXLINE - 1, MSG_DONTWAIT)) > 0) {
|
||||||
|
buf[n] = '\0';
|
||||||
|
printf("%s", buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
close(sock_fd);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 命令输出不完整 | `recv` 时机不确定 | 使用长度前缀协议或特殊结束标记 |
|
||||||
|
| 服务器僵尸进程 | 未处理 `SIGCHLD` | 注册 `SIGCHLD` handler 用 `waitpid` 回收 |
|
||||||
|
| 安全风险 | `system()` 执行任意命令 | 仅用于实验,生产环境需严格限制命令 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实验总结
|
||||||
|
|
||||||
|
通过本实验,应掌握以下能力:
|
||||||
|
|
||||||
|
1. 使用 Socket API 实现 TCP 客户端/服务器
|
||||||
|
2. 理解 HTTP 协议的基本请求/响应格式
|
||||||
|
3. 实现文件下载服务,理解数据传输流程
|
||||||
|
4. 实现远程 shell,理解 I/O 重定向与网络结合
|
||||||
|
5. 掌握 `dup2` 在网络编程中的应用
|
||||||
526
操作系统/实验/实验06_并发服务器.md
Normal file
526
操作系统/实验/实验06_并发服务器.md
Normal file
@@ -0,0 +1,526 @@
|
|||||||
|
# 实验06 并发网络应用编程
|
||||||
|
|
||||||
|
## 实验目的
|
||||||
|
|
||||||
|
1. 理解迭代式服务器与并发式服务器的区别
|
||||||
|
2. 掌握多进程并发服务器的实现
|
||||||
|
3. 掌握多线程并发服务器的实现
|
||||||
|
4. 学会使用预线程化(prethreading)技术提高服务器性能
|
||||||
|
5. 了解 Web 代理服务器的工作原理
|
||||||
|
6. 掌握 I/O 多路复用(select)的基本使用
|
||||||
|
|
||||||
|
## 涉及知识点
|
||||||
|
|
||||||
|
- 迭代式 vs 并发式服务器模型
|
||||||
|
- `fork` 实现多进程并发
|
||||||
|
- `pthread_create` 实现多线程并发
|
||||||
|
- 预线程化线程池(生产者-消费者模型)
|
||||||
|
- `select` I/O 多路复用
|
||||||
|
- 临界区保护与线程安全
|
||||||
|
- 代理服务器的工作原理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务一:测试 togglesp / togglest / togglest_pre
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
分别测试三种并发服务器模型:
|
||||||
|
|
||||||
|
| 模型 | 文件 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 多进程 | `togglesp.c` | 每个连接 fork 一个子进程 |
|
||||||
|
| 多线程 | `togglest.c` | 每个连接创建一个线程 |
|
||||||
|
| 预线程化 | `togglest_pre.c` | 固定线程池 + 任务队列 |
|
||||||
|
|
||||||
|
### 操作步骤
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 分别编译三种服务器
|
||||||
|
gcc -o togglesp togglesp.c -L. -lwrapper
|
||||||
|
gcc -o togglest togglest.c -L. -lwrapper -lpthread
|
||||||
|
gcc -o togglest_pre togglest_pre.c -L. -lwrapper -lpthread
|
||||||
|
|
||||||
|
# 测试多进程服务器
|
||||||
|
./togglesp 8080 &
|
||||||
|
./togglec localhost 8080
|
||||||
|
|
||||||
|
# 测试多线程服务器
|
||||||
|
./togglest 8081 &
|
||||||
|
./togglec localhost 8081
|
||||||
|
|
||||||
|
# 测试预线程化服务器
|
||||||
|
./togglest_pre 8082 &
|
||||||
|
./togglec localhost 8082
|
||||||
|
```
|
||||||
|
|
||||||
|
### 多线程服务器核心代码
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
void toggle(int conn_fd);
|
||||||
|
void *serve_client(void *vargp);
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
int listen_fd, conn_fd, *conn_fd_p;
|
||||||
|
struct sockaddr_in clientaddr;
|
||||||
|
socklen_t clientlen = sizeof(clientaddr);
|
||||||
|
pthread_t tid;
|
||||||
|
|
||||||
|
listen_fd = open_listen_sock(atoi(argv[1]));
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
conn_fd_p = malloc(sizeof(int));
|
||||||
|
*conn_fd_p = Accept(listen_fd,
|
||||||
|
(SA *)&clientaddr, &clientlen);
|
||||||
|
Pthread_create(&tid, NULL, serve_client, conn_fd_p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void *serve_client(void *vargp) {
|
||||||
|
int conn_fd = *((int *)vargp);
|
||||||
|
Pthread_detach(pthread_self());
|
||||||
|
Free(vargp);
|
||||||
|
toggle(conn_fd);
|
||||||
|
Close(conn_fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预线程化服务器核心代码
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
#define NTHREADS 4
|
||||||
|
#define SBUFSIZE 16
|
||||||
|
|
||||||
|
// Sbuf 结构(生产者-消费者缓冲区)
|
||||||
|
typedef struct {
|
||||||
|
int *buf;
|
||||||
|
int n;
|
||||||
|
int front;
|
||||||
|
int rear;
|
||||||
|
sem_t mutex;
|
||||||
|
sem_t slots;
|
||||||
|
sem_t items;
|
||||||
|
} sbuf_t;
|
||||||
|
|
||||||
|
sbuf_t sbuf;
|
||||||
|
|
||||||
|
void sbuf_init(sbuf_t *sp, int n) {
|
||||||
|
sp->buf = Calloc(n, sizeof(int));
|
||||||
|
sp->n = n;
|
||||||
|
sp->front = sp->rear = 0;
|
||||||
|
Sem_init(&sp->mutex, 0, 1);
|
||||||
|
Sem_init(&sp->slots, 0, n);
|
||||||
|
Sem_init(&sp->items, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void sbuf_insert(sbuf_t *sp, int item) {
|
||||||
|
P(&sp->slots);
|
||||||
|
P(&sp->mutex);
|
||||||
|
sp->buf[(sp->rear++) % sp->n] = item;
|
||||||
|
V(&sp->mutex);
|
||||||
|
V(&sp->items);
|
||||||
|
}
|
||||||
|
|
||||||
|
int sbuf_remove(sbuf_t *sp) {
|
||||||
|
P(&sp->items);
|
||||||
|
P(&sp->mutex);
|
||||||
|
int item = sp->buf[(sp->front++) % sp->n];
|
||||||
|
V(&sp->mutex);
|
||||||
|
V(&sp->slots);
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *thread(void *vargp) {
|
||||||
|
Pthread_detach(pthread_self());
|
||||||
|
while (1) {
|
||||||
|
int conn_fd = sbuf_remove(&sbuf);
|
||||||
|
toggle(conn_fd);
|
||||||
|
Close(conn_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
int listen_fd = open_listen_sock(atoi(argv[1]));
|
||||||
|
sbuf_init(&sbuf, SBUFSIZE);
|
||||||
|
|
||||||
|
for (int i = 0; i < NTHREADS; i++)
|
||||||
|
Pthread_create(NULL, NULL, thread, NULL);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
struct sockaddr_in clientaddr;
|
||||||
|
socklen_t clientlen = sizeof(clientaddr);
|
||||||
|
int conn_fd = Accept(listen_fd,
|
||||||
|
(SA *)&clientaddr, &clientlen);
|
||||||
|
sbuf_insert(&sbuf, conn_fd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 三种模型对比
|
||||||
|
|
||||||
|
| 特性 | 多进程 | 多线程 | 预线程化 |
|
||||||
|
|------|--------|--------|----------|
|
||||||
|
| 并发方式 | fork 子进程 | pthread_create | 固定线程池 |
|
||||||
|
| 进程/线程数 | 动态增长 | 动态增长 | 固定 |
|
||||||
|
| 创建开销 | 高 | 中 | 无(已预创建) |
|
||||||
|
| 资源消耗 | 高(独立地址空间) | 低(共享地址空间) | 低 |
|
||||||
|
| 编程复杂度 | 简单 | 中等 | 较高 |
|
||||||
|
| 适用场景 | 连接数少 | 通用 | 高并发 |
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 多进程服务器僵尸进程 | 子进程未回收 | 注册 `SIGCHLD` handler |
|
||||||
|
| 多线程服务器段错误 | 线程间共享变量竞争 | 使用互斥锁保护共享数据 |
|
||||||
|
| 预线程化服务器阻塞 | 缓冲区满 | 增大 SBUFSIZE 或增加线程数 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务二:task92.c —— 多进程 weblet
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
将 weblet 服务器改造为多进程并发模型:每个客户端连接 fork 一个子进程处理。
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
|
void sigchld_handler(int sig) {
|
||||||
|
while (waitpid(-1, NULL, WNOHANG) > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void handle_request(int conn_fd) {
|
||||||
|
// 读取 HTTP 请求
|
||||||
|
char buf[8192];
|
||||||
|
int n = recv(conn_fd, buf, sizeof(buf) - 1, 0);
|
||||||
|
if (n <= 0) { close(conn_fd); return; }
|
||||||
|
buf[n] = '\0';
|
||||||
|
|
||||||
|
// 解析请求行(GET /path HTTP/1.1)
|
||||||
|
char method[16], path[256], version[16];
|
||||||
|
sscanf(buf, "%s %s %s", method, path, version);
|
||||||
|
|
||||||
|
// 处理静态文件请求
|
||||||
|
// ... 打开文件,发送 HTTP 响应头和文件内容 ...
|
||||||
|
|
||||||
|
close(conn_fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc != 2) { exit(1); }
|
||||||
|
|
||||||
|
signal(SIGCHLD, sigchld_handler);
|
||||||
|
|
||||||
|
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
int optval = 1;
|
||||||
|
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,
|
||||||
|
&optval, sizeof(optval));
|
||||||
|
|
||||||
|
struct sockaddr_in servaddr = {
|
||||||
|
.sin_family = AF_INET,
|
||||||
|
.sin_addr.s_addr = htonl(INADDR_ANY),
|
||||||
|
.sin_port = htons(atoi(argv[1]))
|
||||||
|
};
|
||||||
|
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
|
||||||
|
listen(listen_fd, 1024);
|
||||||
|
|
||||||
|
printf("多进程 weblet 启动,端口 %s\n", argv[1]);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
struct sockaddr_in cliaddr;
|
||||||
|
socklen_t clien = sizeof(cliaddr);
|
||||||
|
int conn_fd = accept(listen_fd,
|
||||||
|
(struct sockaddr *)&cliaddr, &clien);
|
||||||
|
|
||||||
|
if (fork() == 0) {
|
||||||
|
close(listen_fd);
|
||||||
|
handle_request(conn_fd);
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
close(conn_fd);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 服务器响应慢 | fork 开销大 | 改用多线程或预线程化 |
|
||||||
|
| 文件描述符泄漏 | 子进程继承了 listen_fd | 子进程中 `close(listen_fd)` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务三:task93.c —— 多线程 weblet
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
将 weblet 服务器改造为多线程并发模型:每个客户端连接创建一个线程处理。
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
|
void *handle_request(void *arg) {
|
||||||
|
int conn_fd = *((int *)arg);
|
||||||
|
free(arg);
|
||||||
|
pthread_detach(pthread_self());
|
||||||
|
|
||||||
|
char buf[8192];
|
||||||
|
int n = recv(conn_fd, buf, sizeof(buf) - 1, 0);
|
||||||
|
if (n <= 0) { close(conn_fd); return NULL; }
|
||||||
|
buf[n] = '\0';
|
||||||
|
|
||||||
|
// 解析并处理 HTTP 请求
|
||||||
|
char method[16], path[256], version[16];
|
||||||
|
sscanf(buf, "%s %s %s", method, path, version);
|
||||||
|
|
||||||
|
// 发送响应...
|
||||||
|
// 注意:多个线程共享 listen_fd,但 conn_fd 是各线程独有的
|
||||||
|
|
||||||
|
close(conn_fd);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
if (argc != 2) { exit(1); }
|
||||||
|
|
||||||
|
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
|
||||||
|
int optval = 1;
|
||||||
|
setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR,
|
||||||
|
&optval, sizeof(optval));
|
||||||
|
|
||||||
|
struct sockaddr_in servaddr = {
|
||||||
|
.sin_family = AF_INET,
|
||||||
|
.sin_addr.s_addr = htonl(INADDR_ANY),
|
||||||
|
.sin_port = htons(atoi(argv[1]))
|
||||||
|
};
|
||||||
|
bind(listen_fd, (struct sockaddr *)&servaddr, sizeof(servaddr));
|
||||||
|
listen(listen_fd, 1024);
|
||||||
|
|
||||||
|
printf("多线程 weblet 启动,端口 %s\n", argv[1]);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
struct sockaddr_in cliaddr;
|
||||||
|
socklen_t clien = sizeof(cliaddr);
|
||||||
|
int *conn_fd = malloc(sizeof(int));
|
||||||
|
*conn_fd = accept(listen_fd,
|
||||||
|
(struct sockaddr *)&cliaddr, &clien);
|
||||||
|
|
||||||
|
pthread_t tid;
|
||||||
|
pthread_create(&tid, NULL, handle_request, conn_fd);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 线程数爆炸 | 每个请求创建新线程 | 改用预线程化或限制最大线程数 |
|
||||||
|
| 线程不安全的函数 | `strtok`、`ctime` 等非线程安全 | 使用 `_r` 后缀的可重入版本 |
|
||||||
|
| 内存泄漏 | 未 `free(arg)` 或未 `pthread_detach` | 确保线程退出前释放资源 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务四:task94.c —— 预线程化 weblet(动态增减线程)
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
实现预线程化 weblet 服务器,使用固定线程池处理请求。支持根据负载动态增减线程数量。
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <semaphore.h>
|
||||||
|
|
||||||
|
#define MIN_THREADS 2
|
||||||
|
#define MAX_THREADS 16
|
||||||
|
#define SBUFSIZE 32
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
int *buf;
|
||||||
|
int n, front, rear;
|
||||||
|
sem_t mutex, slots, items;
|
||||||
|
} sbuf_t;
|
||||||
|
|
||||||
|
sbuf_t sbuf;
|
||||||
|
int current_threads = 0;
|
||||||
|
pthread_mutex_t count_mutex = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
|
||||||
|
void *worker(void *arg) {
|
||||||
|
pthread_detach(pthread_self());
|
||||||
|
pthread_mutex_lock(&count_mutex);
|
||||||
|
current_threads++;
|
||||||
|
pthread_mutex_unlock(&count_mutex);
|
||||||
|
|
||||||
|
while (1) {
|
||||||
|
int conn_fd = sbuf_remove(&sbuf);
|
||||||
|
|
||||||
|
// 处理 HTTP 请求
|
||||||
|
handle_http_request(conn_fd);
|
||||||
|
close(conn_fd);
|
||||||
|
|
||||||
|
// 动态缩减:如果缓冲区长时间为空且线程数过多
|
||||||
|
// 可在此处实现缩减逻辑
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void *manager(void *arg) {
|
||||||
|
// 监控线程:根据负载动态增减工作线程
|
||||||
|
while (1) {
|
||||||
|
sleep(5);
|
||||||
|
|
||||||
|
int pending = ...; // 获取待处理请求数
|
||||||
|
pthread_mutex_lock(&count_mutex);
|
||||||
|
|
||||||
|
if (pending > current_threads && current_threads < MAX_THREADS) {
|
||||||
|
// 扩容
|
||||||
|
pthread_t tid;
|
||||||
|
pthread_create(&tid, NULL, worker, NULL);
|
||||||
|
} else if (pending == 0 && current_threads > MIN_THREADS) {
|
||||||
|
// 缩减(通过向缓冲区插入特殊值 -1 实现)
|
||||||
|
sbuf_insert(&sbuf, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pthread_mutex_unlock(&count_mutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
sbuf_init(&sbuf, SBUFSIZE);
|
||||||
|
|
||||||
|
// 创建初始线程池
|
||||||
|
for (int i = 0; i < MIN_THREADS; i++) {
|
||||||
|
pthread_t tid;
|
||||||
|
pthread_create(&tid, NULL, worker, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建管理线程
|
||||||
|
pthread_t mgr_tid;
|
||||||
|
pthread_create(&mgr_tid, NULL, manager, NULL);
|
||||||
|
|
||||||
|
// 主线程接受连接
|
||||||
|
int listen_fd = open_listen_sock(atoi(argv[1]));
|
||||||
|
while (1) {
|
||||||
|
struct sockaddr_in cliaddr;
|
||||||
|
socklen_t clien = sizeof(cliaddr);
|
||||||
|
int conn_fd = accept(listen_fd,
|
||||||
|
(struct sockaddr *)&cliaddr, &clien);
|
||||||
|
sbuf_insert(&sbuf, conn_fd);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 动态增减策略
|
||||||
|
|
||||||
|
| 条件 | 操作 |
|
||||||
|
|------|------|
|
||||||
|
| 待处理请求数 > 当前线程数 且 < 最大线程数 | 创建新线程 |
|
||||||
|
| 待处理请求数 = 0 且 当前线程数 > 最小线程数 | 终止一个线程 |
|
||||||
|
| 线程数已达上限 | 等待(请求在缓冲区排队) |
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 线程缩减无效 | 线程阻塞在 `sbuf_remove` | 发送特殊值唤醒线程 |
|
||||||
|
| 线程数波动过大 | 扩缩策略过于敏感 | 设置冷却时间和阈值 |
|
||||||
|
| 队列溢出 | SBUFSIZE 太小 | 增大缓冲区或动态扩容 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 任务五:task95.c —— Web 代理服务器(选做)
|
||||||
|
|
||||||
|
### 任务要求
|
||||||
|
|
||||||
|
实现一个 Web 代理服务器:
|
||||||
|
|
||||||
|
1. 客户端连接代理,发送 HTTP 请求
|
||||||
|
2. 代理解析请求中的目标 URL
|
||||||
|
3. 代理向目标服务器发起请求
|
||||||
|
4. 将目标服务器的响应转发给客户端
|
||||||
|
|
||||||
|
### 关键代码提示
|
||||||
|
|
||||||
|
```c
|
||||||
|
void handle_proxy(int client_fd) {
|
||||||
|
char buf[8192];
|
||||||
|
int n = recv(client_fd, buf, sizeof(buf) - 1, 0);
|
||||||
|
if (n <= 0) { close(client_fd); return; }
|
||||||
|
buf[n] = '\0';
|
||||||
|
|
||||||
|
// 解析 HTTP 请求中的 URL
|
||||||
|
char method[16], url[512], version[16];
|
||||||
|
sscanf(buf, "%s %s %s", method, url, version);
|
||||||
|
|
||||||
|
// 解析主机名和端口
|
||||||
|
char host[256];
|
||||||
|
int port = 80;
|
||||||
|
// url 格式: http://host:port/path
|
||||||
|
// 解析 host 和 port ...
|
||||||
|
|
||||||
|
// 连接目标服务器
|
||||||
|
int server_fd = open_client_sock(host, port);
|
||||||
|
|
||||||
|
// 转发请求
|
||||||
|
send(server_fd, buf, n, 0);
|
||||||
|
|
||||||
|
// 转发响应
|
||||||
|
while ((n = recv(server_fd, buf, sizeof(buf), 0)) > 0)
|
||||||
|
send(client_fd, buf, n, 0);
|
||||||
|
|
||||||
|
close(server_fd);
|
||||||
|
close(client_fd);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
| 问题 | 原因 | 解决方法 |
|
||||||
|
|------|------|----------|
|
||||||
|
| HTTPS 站点无法代理 | 代理不支持 CONNECT 方法 | 仅支持 HTTP |
|
||||||
|
| URL 解析错误 | 格式多样 | 仔细处理 `http://`、端口号、路径等 |
|
||||||
|
| 性能差 | 每次请求都新建连接 | 可实现连接池缓存 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 实验总结
|
||||||
|
|
||||||
|
通过本实验,应掌握以下能力:
|
||||||
|
|
||||||
|
1. 区分迭代式和并发式服务器模型
|
||||||
|
2. 使用 `fork` 实现多进程并发服务器
|
||||||
|
3. 使用 `pthread` 实现多线程并发服务器
|
||||||
|
4. 使用预线程化技术构建高性能服务器
|
||||||
|
5. 理解线程池的工作原理和动态管理策略
|
||||||
|
6. 了解 Web 代理服务器的实现方法
|
||||||
225
操作系统/模板/课件笔记模板.md
Normal file
225
操作系统/模板/课件笔记模板.md
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# 第N讲:标题
|
||||||
|
|
||||||
|
> 🎯 **本节目标**:一句话说明学完能做什么
|
||||||
|
|
||||||
|
## 📋 前置知识
|
||||||
|
- [[相关笔记1]] — 需要先掌握的概念
|
||||||
|
- [[相关笔记2]] — 需要先掌握的概念
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤔 为什么需要这个?
|
||||||
|
|
||||||
|
用生活场景或实际问题引出本节主题。让读者明白"为什么要学这个"。
|
||||||
|
|
||||||
|
**生活比喻**:
|
||||||
|
- 用通俗的比喻解释抽象概念
|
||||||
|
- 例如:进程 = 正在做饭的厨师
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📖 核心概念
|
||||||
|
|
||||||
|
### 1. 概念一:xxx
|
||||||
|
|
||||||
|
**通俗解释**:
|
||||||
|
用简单的语言解释这个概念。
|
||||||
|
|
||||||
|
**正式定义**:
|
||||||
|
给出准确的技术定义。
|
||||||
|
|
||||||
|
**图示**:
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[概念] --> B[组成部分1]
|
||||||
|
A --> C[组成部分2]
|
||||||
|
A --> D[组成部分3]
|
||||||
|
```
|
||||||
|
|
||||||
|
**对比表格**:
|
||||||
|
| 特性 | 概念A | 概念B |
|
||||||
|
|------|-------|-------|
|
||||||
|
| 特性1 | 说明 | 说明 |
|
||||||
|
| 特性2 | 说明 | 说明 |
|
||||||
|
|
||||||
|
### 2. 概念二:xxx
|
||||||
|
|
||||||
|
(同上结构)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💻 动手实践
|
||||||
|
|
||||||
|
### 示例1:xxx
|
||||||
|
|
||||||
|
```c
|
||||||
|
// 文件名.c - 简要说明
|
||||||
|
#include "wrapper.h"
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
// 代码示例
|
||||||
|
// 逐行注释说明
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**编译运行**:
|
||||||
|
```bash
|
||||||
|
gcc -o 文件名 文件名.c -L. -lwrapper
|
||||||
|
./文件名
|
||||||
|
```
|
||||||
|
|
||||||
|
**预期输出**:
|
||||||
|
```
|
||||||
|
预期的输出内容
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键点**:
|
||||||
|
- 解释代码的关键部分
|
||||||
|
- 说明为什么这样写
|
||||||
|
|
||||||
|
### 示例2:xxx
|
||||||
|
|
||||||
|
(同上结构)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔗 知识关联
|
||||||
|
- 与 [[相关笔记1]] 的关系
|
||||||
|
- 与 [[相关笔记2]] 的关系
|
||||||
|
- 在操作系统整体中的位置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 思考题
|
||||||
|
|
||||||
|
1. **概念理解题**:用通俗的语言解释xxx概念
|
||||||
|
2. **代码分析题**:分析以下代码的输出
|
||||||
|
3. **应用题**:在什么场景下会用到xxx?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 扩展阅读
|
||||||
|
- 《操作系统概念》第X章:xxx
|
||||||
|
- 《深入理解计算机系统》第X章:xxx
|
||||||
|
- [相关在线资源](https://example.com)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 笔记使用说明
|
||||||
|
|
||||||
|
### 如何使用这个模板
|
||||||
|
|
||||||
|
1. **复制模板**:复制此文件,重命名为 `第XX讲_标题.md`
|
||||||
|
2. **填写内容**:
|
||||||
|
- 修改标题和目标
|
||||||
|
- 添加前置知识链接
|
||||||
|
- 编写"为什么需要这个"
|
||||||
|
- 解释核心概念
|
||||||
|
- 添加代码示例
|
||||||
|
- 添加思考题
|
||||||
|
3. **添加链接**:使用 `[[]]` 语法链接到相关笔记
|
||||||
|
4. **添加图表**:使用 Mermaid 语法绘制流程图、状态图等
|
||||||
|
|
||||||
|
### Mermaid 图表示例
|
||||||
|
|
||||||
|
#### 流程图
|
||||||
|
```mermaid
|
||||||
|
graph TD
|
||||||
|
A[开始] --> B[处理]
|
||||||
|
B --> C[结束]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 时序图
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant A as 进程A
|
||||||
|
participant B as 进程B
|
||||||
|
A->>B: 请求
|
||||||
|
B->>A: 响应
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 状态图
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> 就绪
|
||||||
|
就绪 --> 运行
|
||||||
|
运行 --> 阻塞
|
||||||
|
阻塞 --> 就绪
|
||||||
|
运行 --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 甘特图
|
||||||
|
```mermaid
|
||||||
|
gantt
|
||||||
|
title 任务进度
|
||||||
|
dateFormat YYYY-MM-DD
|
||||||
|
section 任务
|
||||||
|
任务1 :a1, 2024-01-01, 7d
|
||||||
|
任务2 :after a1, 5d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 格式规范
|
||||||
|
|
||||||
|
### 标题层级
|
||||||
|
- `#` — 一级标题(讲次标题)
|
||||||
|
- `##` — 二级标题(主要章节)
|
||||||
|
- `###` — 三级标题(子章节)
|
||||||
|
- `####` — 四级标题(细节)
|
||||||
|
|
||||||
|
### 表情符号使用
|
||||||
|
- 🎯 — 目标
|
||||||
|
- 📋 — 前置知识
|
||||||
|
- 🤔 — 问题引入
|
||||||
|
- 📖 — 核心概念
|
||||||
|
- 💻 — 动手实践
|
||||||
|
- 🔗 — 知识关联
|
||||||
|
- 📝 — 思考题/笔记
|
||||||
|
- 📚 — 扩展阅读
|
||||||
|
- ⚠️ — 注意事项
|
||||||
|
- 💡 — 提示/技巧
|
||||||
|
|
||||||
|
### 代码块
|
||||||
|
```c
|
||||||
|
// 代码示例
|
||||||
|
// 使用 ```c 标记 C 语言代码
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Shell 命令
|
||||||
|
# 使用 ```bash 标记 Shell 命令
|
||||||
|
```
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Python 代码
|
||||||
|
# 使用 ```python 标记 Python 代码
|
||||||
|
```
|
||||||
|
|
||||||
|
### 链接
|
||||||
|
- 内部链接:`[[笔记名称]]`
|
||||||
|
- 外部链接:`[显示文本](URL)`
|
||||||
|
|
||||||
|
### 图片
|
||||||
|
```markdown
|
||||||
|

|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ 检查清单
|
||||||
|
|
||||||
|
完成笔记后,检查以下内容:
|
||||||
|
|
||||||
|
- [ ] 标题清晰,能准确反映内容
|
||||||
|
- [ ] 目标明确,读者知道学完能做什么
|
||||||
|
- [ ] 前置知识完整,读者知道需要先学什么
|
||||||
|
- [ ] 问题引入生动,用生活场景引出主题
|
||||||
|
- [ ] 核心概念清晰,有通俗解释和正式定义
|
||||||
|
- [ ] 图示准确,Mermaid 语法正确
|
||||||
|
- [ ] 代码示例完整,有编译运行命令
|
||||||
|
- [ ] 输出结果正确,有关键点解释
|
||||||
|
- [ ] 知识关联完整,有双向链接
|
||||||
|
- [ ] 思考题有价值,能帮助理解概念
|
||||||
|
- [ ] 扩展阅读有用,有推荐资源
|
||||||
360
操作系统/附录/附录A_Wrapper库参考.md
Normal file
360
操作系统/附录/附录A_Wrapper库参考.md
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
# 附录A Wrapper 库参考
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Wrapper 库是课程提供的 C 语言工具库,封装了课程用到的所有系统调用。每个 Wrapper 函数在内部调用对应的系统调用并检查返回值,出错时自动打印错误信息并终止程序,从而简化实验代码的编写。
|
||||||
|
|
||||||
|
源文件组成:
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `wrapper.h` | 头文件,包含所有函数声明、类型定义和常量 |
|
||||||
|
| `wrapper.c` | Wrapper 函数实现 |
|
||||||
|
| `ptwrapper.c` | POSIX 线程相关 Wrapper 函数 |
|
||||||
|
| `libwrapper.a` | 预编译的静态库 |
|
||||||
|
|
||||||
|
## 编译与使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 方法一:链接静态库
|
||||||
|
gcc -o program program.c -L. -lwrapper
|
||||||
|
|
||||||
|
# 方法二:链接静态库 + 线程支持
|
||||||
|
gcc -o program program.c -L. -lwrapper -lpthread
|
||||||
|
|
||||||
|
# 方法三:自行编译
|
||||||
|
gcc -c wrapper.c ptwrapper.c
|
||||||
|
ar rc libwrapper.a wrapper.o ptwrapper.o
|
||||||
|
gcc -o program program.c -L. -lwrapper -lpthread
|
||||||
|
```
|
||||||
|
|
||||||
|
## 头文件包含的系统头文件
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <setjmp.h>
|
||||||
|
#include <signal.h>
|
||||||
|
#include <sys/time.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <math.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <semaphore.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <netdb.h>
|
||||||
|
#include <netinet/in.h>
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
#include <sys/ipc.h>
|
||||||
|
#include <sys/msg.h>
|
||||||
|
#include <sys/shm.h>
|
||||||
|
#include <sys/sem.h>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常量与类型定义
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct sockaddr SA; // 简化 socket 地址类型转换
|
||||||
|
|
||||||
|
#define MAXLINE 8192 // 最大文本行长度
|
||||||
|
#define MAXBUF 8192 // 最大 I/O 缓冲区大小
|
||||||
|
#define LISTENQ 1024 // listen() 第二参数(连接队列长度)
|
||||||
|
|
||||||
|
#define DEF_MODE S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH
|
||||||
|
#define DEF_UMASK S_IWGRP|S_IWOTH
|
||||||
|
|
||||||
|
// Robust I/O 缓冲区
|
||||||
|
typedef struct {
|
||||||
|
int rio_fd;
|
||||||
|
int rio_cnt;
|
||||||
|
char *rio_bufptr;
|
||||||
|
char rio_buf[8192];
|
||||||
|
} rio_t;
|
||||||
|
|
||||||
|
// System V 信号量联合体
|
||||||
|
union semun {
|
||||||
|
int val;
|
||||||
|
struct semid_ds *buf;
|
||||||
|
unsigned short *array;
|
||||||
|
struct seminfo *__buf;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 进程控制
|
||||||
|
|
||||||
|
| 函数原型 | 对应系统调用 | 说明 |
|
||||||
|
|----------|-------------|------|
|
||||||
|
| `pid_t Fork(void)` | `fork` | 创建子进程 |
|
||||||
|
| `void Execve(const char *f, char *const a[], char *const e[])` | `execve` | 执行程序 |
|
||||||
|
| `pid_t Wait(int *status)` | `wait` | 等待任意子进程 |
|
||||||
|
| `pid_t Waitpid(pid_t pid, int *iptr, int options)` | `waitpid` | 等待指定子进程 |
|
||||||
|
| `void Kill(pid_t pid, int signum)` | `kill` | 发送信号 |
|
||||||
|
| `unsigned int Sleep(unsigned int secs)` | `sleep` | 休眠指定秒数 |
|
||||||
|
| `void Pause(void)` | `pause` | 暂停直到信号到达 |
|
||||||
|
| `unsigned int Alarm(unsigned int seconds)` | `alarm` | 设置定时器 |
|
||||||
|
| `void Setpgid(pid_t pid, pid_t pgid)` | `setpgid` | 设置进程组 |
|
||||||
|
| `pid_t Getpgrp(void)` | `getpgrp` | 获取当前进程组 |
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
pid_t pid = Fork();
|
||||||
|
if (pid == 0) {
|
||||||
|
char *args[] = {"ls", "-la", NULL};
|
||||||
|
Execve("/bin/ls", args, environ);
|
||||||
|
} else {
|
||||||
|
int status;
|
||||||
|
Waitpid(pid, &status, 0);
|
||||||
|
printf("子进程退出状态: %d\n", WEXITSTATUS(status));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 文件 I/O
|
||||||
|
|
||||||
|
| 函数原型 | 对应系统调用 | 说明 |
|
||||||
|
|----------|-------------|------|
|
||||||
|
| `int Open(const char *path, int flags, mode_t mode)` | `open` | 打开/创建文件 |
|
||||||
|
| `ssize_t Read(int fd, void *buf, size_t count)` | `read` | 读取数据 |
|
||||||
|
| `ssize_t Write(int fd, const void *buf, size_t count)` | `write` | 写入数据 |
|
||||||
|
| `off_t Lseek(int fd, off_t offset, int whence)` | `lseek` | 文件偏移定位 |
|
||||||
|
| `void Close(int fd)` | `close` | 关闭文件描述符 |
|
||||||
|
| `int Dup2(int fd1, int fd2)` | `dup2` | 复制文件描述符 |
|
||||||
|
| `int Select(int n, fd_set *r, fd_set *w, fd_set *e, struct timeval *t)` | `select` | I/O 多路复用 |
|
||||||
|
| `void Stat(const char *filename, struct stat *buf)` | `stat` | 获取文件状态 |
|
||||||
|
| `void Fstat(int fd, struct stat *buf)` | `fstat` | 获取文件状态 |
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
int fd = Open("data.txt", O_RDONLY, 0);
|
||||||
|
char buf[MAXLINE];
|
||||||
|
ssize_t n = Read(fd, buf, MAXLINE);
|
||||||
|
Write(STDOUT_FILENO, buf, n);
|
||||||
|
Close(fd);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 信号
|
||||||
|
|
||||||
|
| 函数原型 | 对应系统调用 | 说明 |
|
||||||
|
|----------|-------------|------|
|
||||||
|
| `handler_t *Signal(int signum, handler_t *handler)` | `signal` | 注册信号处理函数 |
|
||||||
|
| `void Sigprocmask(int how, const sigset_t *set, sigset_t *old)` | `sigprocmask` | 修改信号屏蔽字 |
|
||||||
|
| `void Sigemptyset(sigset_t *set)` | `sigemptyset` | 清空信号集 |
|
||||||
|
| `void Sigfillset(sigset_t *set)` | `sigfillset` | 填满信号集 |
|
||||||
|
| `void Sigaddset(sigset_t *set, int signum)` | `sigaddset` | 添加信号到集合 |
|
||||||
|
| `void Sigdelset(sigset_t *set, int signum)` | `sigdelset` | 从集合移除信号 |
|
||||||
|
| `int Sigismember(const sigset_t *set, int signum)` | `sigismember` | 判断信号是否在集合中 |
|
||||||
|
|
||||||
|
**示例:**
|
||||||
|
|
||||||
|
```c
|
||||||
|
void handler(int sig) {
|
||||||
|
printf("收到信号 %d\n", sig);
|
||||||
|
}
|
||||||
|
Signal(SIGINT, handler); // Ctrl+C 时调用 handler
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 标准 I/O
|
||||||
|
|
||||||
|
| 函数原型 | 对应标准库函数 | 说明 |
|
||||||
|
|----------|--------------|------|
|
||||||
|
| `FILE *Fopen(const char *name, const char *mode)` | `fopen` | 打开文件 |
|
||||||
|
| `void Fclose(FILE *fp)` | `fclose` | 关闭文件 |
|
||||||
|
| `char *Fgets(char *ptr, int n, FILE *stream)` | `fgets` | 读取一行 |
|
||||||
|
| `void Fputs(const char *ptr, FILE *stream)` | `fputs` | 写入一行 |
|
||||||
|
| `size_t Fread(void *ptr, size_t size, size_t nmemb, FILE *stream)` | `fread` | 二进制读取 |
|
||||||
|
| `void Fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)` | `fwrite` | 二进制写入 |
|
||||||
|
| `FILE *Fdopen(int fd, const char *type)` | `fdopen` | 文件描述符转 FILE* |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 内存管理
|
||||||
|
|
||||||
|
| 函数原型 | 对应标准库函数 | 说明 |
|
||||||
|
|----------|--------------|------|
|
||||||
|
| `void *Malloc(size_t size)` | `malloc` | 分配内存 |
|
||||||
|
| `void *Realloc(void *ptr, size_t size)` | `realloc` | 重新分配内存 |
|
||||||
|
| `void *Calloc(size_t nmemb, size_t size)` | `calloc` | 分配并清零 |
|
||||||
|
| `void Free(void *ptr)` | `free` | 释放内存 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 内存映射
|
||||||
|
|
||||||
|
| 函数原型 | 对应系统调用 | 说明 |
|
||||||
|
|----------|-------------|------|
|
||||||
|
| `void *Mmap(void *addr, size_t len, int prot, int flags, int fd, off_t offset)` | `mmap` | 创建内存映射 |
|
||||||
|
| `void Munmap(void *start, size_t length)` | `munmap` | 取消内存映射 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Socket 编程
|
||||||
|
|
||||||
|
| 函数原型 | 对应系统调用 | 说明 |
|
||||||
|
|----------|-------------|------|
|
||||||
|
| `int Socket(int domain, int type, int protocol)` | `socket` | 创建套接字 |
|
||||||
|
| `void Setsockopt(int s, int level, int opt, const void *val, int len)` | `setsockopt` | 设置套接字选项 |
|
||||||
|
| `void Bind(int sockfd, struct sockaddr *addr, int addrlen)` | `bind` | 绑定地址 |
|
||||||
|
| `void Listen(int s, int backlog)` | `listen` | 监听连接 |
|
||||||
|
| `int Accept(int s, struct sockaddr *addr, socklen_t *addrlen)` | `accept` | 接受连接 |
|
||||||
|
| `void Connect(int sockfd, struct sockaddr *addr, int addrlen)` | `connect` | 发起连接 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## DNS 解析
|
||||||
|
|
||||||
|
| 函数原型 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| `struct hostent *Gethostbyname(const char *name)` | 按主机名解析 |
|
||||||
|
| `struct hostent *Gethostbyaddr(const char *addr, int len, int type)` | 按地址解析 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 线程控制
|
||||||
|
|
||||||
|
| 函数原型 | 对应系统调用 | 说明 |
|
||||||
|
|----------|-------------|------|
|
||||||
|
| `void Pthread_create(pthread_t *tid, pthread_attr_t *attr, void *(*r)(void *), void *arg)` | `pthread_create` | 创建线程 |
|
||||||
|
| `void Pthread_join(pthread_t tid, void **retval)` | `pthread_join` | 等待线程结束 |
|
||||||
|
| `void Pthread_detach(pthread_t tid)` | `pthread_detach` | 分离线程 |
|
||||||
|
| `void Pthread_cancel(pthread_t tid)` | `pthread_cancel` | 取消线程 |
|
||||||
|
| `void Pthread_exit(void *retval)` | `pthread_exit` | 终止线程 |
|
||||||
|
| `pthread_t Pthread_self(void)` | `pthread_self` | 获取当前线程 ID |
|
||||||
|
| `void Pthread_once(pthread_once_t *once, void (*init)())` | `pthread_once` | 一次性初始化 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## POSIX 信号量
|
||||||
|
|
||||||
|
| 函数原型 | 对应系统调用 | 说明 |
|
||||||
|
|----------|-------------|------|
|
||||||
|
| `void Sem_init(sem_t *sem, int pshared, unsigned int value)` | `sem_init` | 初始化信号量 |
|
||||||
|
| `void P(sem_t *sem)` | `sem_wait` | 等待(P 操作,值减 1) |
|
||||||
|
| `void V(sem_t *sem)` | `sem_post` | 释放(V 操作,值加 1) |
|
||||||
|
|
||||||
|
**示例(生产者-消费者):**
|
||||||
|
|
||||||
|
```c
|
||||||
|
sem_t mutex, slots, items;
|
||||||
|
Sem_init(&mutex, 0, 1);
|
||||||
|
Sem_init(&slots, 0, N);
|
||||||
|
Sem_init(&items, 0, 0);
|
||||||
|
|
||||||
|
// 生产者
|
||||||
|
P(&slots);
|
||||||
|
P(&mutex);
|
||||||
|
// 放入缓冲区
|
||||||
|
V(&mutex);
|
||||||
|
V(&items);
|
||||||
|
|
||||||
|
// 消费者
|
||||||
|
P(&items);
|
||||||
|
P(&mutex);
|
||||||
|
// 从缓冲区取出
|
||||||
|
V(&mutex);
|
||||||
|
V(&slots);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Robust I/O (Rio)
|
||||||
|
|
||||||
|
| 函数原型 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| `ssize_t rio_readn(int fd, void *buf, size_t n)` | 无缓冲读取 n 字节 |
|
||||||
|
| `ssize_t rio_writen(int fd, void *buf, size_t n)` | 无缓冲写入 n 字节 |
|
||||||
|
| `void rio_readinitb(rio_t *rp, int fd)` | 初始化缓冲读取 |
|
||||||
|
| `ssize_t rio_readnb(rio_t *rp, void *buf, size_t n)` | 带缓冲读取 n 字节 |
|
||||||
|
| `ssize_t rio_readlineb(rio_t *rp, void *buf, size_t maxlen)` | 带缓冲读取一行 |
|
||||||
|
|
||||||
|
**Wrapper 版本:** `Rio_readn`、`Rio_writen`、`Rio_readinitb`、`Rio_readnb`、`Rio_readlineb`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 客户端/服务器辅助函数
|
||||||
|
|
||||||
|
| 函数原型 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| `int open_client_sock(char *hostname, int port)` | 创建客户端套接字并连接 |
|
||||||
|
| `int open_listen_sock(int port)` | 创建服务器套接字并监听 |
|
||||||
|
| `int Open_client_sock(char *hostname, int port)` | 同上(带错误检查) |
|
||||||
|
| `int Open_listen_sock(int port)` | 同上(带错误检查) |
|
||||||
|
|
||||||
|
**示例(服务器):**
|
||||||
|
|
||||||
|
```c
|
||||||
|
int listen_fd = Open_listen_sock(8080);
|
||||||
|
struct sockaddr_in clientaddr;
|
||||||
|
socklen_t clientlen = sizeof(clientaddr);
|
||||||
|
int conn_fd = Accept(listen_fd, (SA *)&clientaddr, &clientlen);
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例(客户端):**
|
||||||
|
|
||||||
|
```c
|
||||||
|
int sock_fd = Open_client_sock("localhost", 8080);
|
||||||
|
Send(sock_fd, "Hello", 5, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## System V IPC
|
||||||
|
|
||||||
|
### 消息队列
|
||||||
|
|
||||||
|
| 函数原型 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| `int Msgget(key_t key, int msgflg)` | 创建/获取消息队列 |
|
||||||
|
| `int Msgsnd(int msqid, const void *msg, size_t sz, int msgflg)` | 发送消息 |
|
||||||
|
| `int Msgrcv(int msqid, void *msg, size_t sz, long type, int msgflg)` | 接收消息 |
|
||||||
|
| `int Msgctl(int msqid, int cmd, struct msqid_ds *buf)` | 控制消息队列 |
|
||||||
|
|
||||||
|
### 共享内存
|
||||||
|
|
||||||
|
| 函数原型 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| `int Shmget(key_t key, size_t size, int shmflg)` | 创建/获取共享内存 |
|
||||||
|
| `char *Shmat(int shmid, const void *addr, int shmflg)` | 附加共享内存 |
|
||||||
|
| `int Shmdt(const void *addr)` | 分离共享内存 |
|
||||||
|
| `int Shmctl(int shmid, int cmd, struct shmid_ds *buf)` | 控制共享内存 |
|
||||||
|
|
||||||
|
### System V 信号量
|
||||||
|
|
||||||
|
| 函数原型 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| `int Semget(key_t key, int nsems, int semflg)` | 创建/获取信号量集 |
|
||||||
|
| `int Semctl(int semid, int semnum, int cmd, union semun arg)` | 控制信号量 |
|
||||||
|
| `int Semop(int semid, struct sembuf *sem, int sops)` | 操作信号量 |
|
||||||
|
|
||||||
|
### 命名管道
|
||||||
|
|
||||||
|
| 函数原型 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| `int Mkfifo(const char *pathname, mode_t mode)` | 创建命名管道 (FIFO) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误处理函数
|
||||||
|
|
||||||
|
Wrapper 库内部使用以下错误处理函数,也可在自己的代码中直接调用:
|
||||||
|
|
||||||
|
| 函数 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `void unix_error(char *msg)` | 打印系统调用错误并退出 |
|
||||||
|
| `void posix_error(int code, char *msg)` | 打印 POSIX 错误并退出 |
|
||||||
|
| `void dns_error(char *msg)` | 打印 DNS 错误并退出 |
|
||||||
|
| `void app_error(char *msg)` | 打印应用错误并退出 |
|
||||||
186
操作系统/附录/附录B_术语表.md
Normal file
186
操作系统/附录/附录B_术语表.md
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
# 附录B 操作系统术语表
|
||||||
|
|
||||||
|
> 本表收录课程涉及的核心术语,按中英文对照排列,涵盖进程管理、存储管理、文件系统、网络编程等领域。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 进程与线程
|
||||||
|
|
||||||
|
| 英文术语 | 中文术语 | 简要说明 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| Process | 进程 | 程序的一次执行实例,是资源分配的基本单位 |
|
||||||
|
| Thread | 线程 | 进程内的执行单元,是 CPU 调度的基本单位 |
|
||||||
|
| Process Control Block (PCB) | 进程控制块 | 存储进程状态、寄存器、页表等信息的数据结构 |
|
||||||
|
| Process State | 进程状态 | 就绪、运行、阻塞等状态 |
|
||||||
|
| Ready | 就绪 | 等待 CPU 调度的状态 |
|
||||||
|
| Running | 运行 | 正在 CPU 上执行的状态 |
|
||||||
|
| Blocked / Waiting | 阻塞/等待 | 等待 I/O 或事件的状态 |
|
||||||
|
| Zombie Process | 僵尸进程 | 已终止但父进程未回收的进程 |
|
||||||
|
| Orphan Process | 孤儿进程 | 父进程已终止的进程,被 init 收养 |
|
||||||
|
| Daemon | 守护进程 | 在后台运行的长期服务进程 |
|
||||||
|
| Fork | 派生 | 创建子进程的系统调用 |
|
||||||
|
| Exec | 执行 | 用新程序替换当前进程映像 |
|
||||||
|
| Wait | 等待 | 父进程等待子进程终止 |
|
||||||
|
| Exit | 退出 | 进程终止并释放资源 |
|
||||||
|
| Process Group | 进程组 | 相关进程的集合 |
|
||||||
|
| Session | 会话 | 进程组的集合,关联一个控制终端 |
|
||||||
|
| Context Switch | 上下文切换 | CPU 从一个进程/线程切换到另一个 |
|
||||||
|
|
||||||
|
## 调度
|
||||||
|
|
||||||
|
| 英文术语 | 中文术语 | 简要说明 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| Scheduling | 调度 | 选择下一个运行的进程/线程 |
|
||||||
|
| Scheduler | 调度器 | 执行调度算法的模块 |
|
||||||
|
| CPU Burst | CPU 区间 | 进程使用 CPU 的时间段 |
|
||||||
|
| I/O Burst | I/O 区间 | 进程等待 I/O 的时间段 |
|
||||||
|
| Preemptive Scheduling | 抢占式调度 | 允许强制剥夺 CPU |
|
||||||
|
| Non-preemptive Scheduling | 非抢占式调度 | 进程主动释放 CPU |
|
||||||
|
| Round Robin (RR) | 时间片轮转 | 每个进程分配固定时间片 |
|
||||||
|
| Shortest Job First (SJF) | 最短作业优先 | 选择预计运行时间最短的进程 |
|
||||||
|
| Priority Scheduling | 优先级调度 | 按优先级选择进程 |
|
||||||
|
| Multilevel Feedback Queue | 多级反馈队列 | 多个就绪队列,动态调整优先级 |
|
||||||
|
| Starvation | 饥饿 | 进程长期得不到调度 |
|
||||||
|
| Fairness | 公平性 | 调度算法的公平程度 |
|
||||||
|
|
||||||
|
## 同步与互斥
|
||||||
|
|
||||||
|
| 英文术语 | 中文术语 | 简要说明 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| Synchronization | 同步 | 协调多个进程/线程的执行顺序 |
|
||||||
|
| Mutual Exclusion | 互斥 | 同一时刻只有一个进程进入临界区 |
|
||||||
|
| Critical Section | 临界区 | 访问共享资源的代码段 |
|
||||||
|
| Race Condition | 竞态条件 | 并发访问共享数据导致结果不确定 |
|
||||||
|
| Semaphore | 信号量 | 用于同步和互斥的计数器 |
|
||||||
|
| Mutex | 互斥锁 | 用于互斥的锁机制 |
|
||||||
|
| Lock | 锁 | 保护临界区的机制 |
|
||||||
|
| Deadlock | 死锁 | 多个进程互相等待,永远无法继续 |
|
||||||
|
| Starvation | 饥饿 | 进程长期无法获取资源 |
|
||||||
|
| Busy Waiting | 忙等待 | 循环检查条件,浪费 CPU |
|
||||||
|
| Condition Variable | 条件变量 | 线程间通知条件满足的机制 |
|
||||||
|
| Monitor | 管程 | 高级同步原语,封装共享数据和操作 |
|
||||||
|
| Producer-Consumer | 生产者-消费者 | 经典同步问题 |
|
||||||
|
| Readers-Writers | 读者-写者 | 经典同步问题 |
|
||||||
|
| Dining Philosophers | 哲学家就餐 | 经典同步问题 |
|
||||||
|
|
||||||
|
## 死锁
|
||||||
|
|
||||||
|
| 英文术语 | 中文术语 | 简要说明 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| Deadlock | 死锁 | 多个进程互相等待对方持有的资源 |
|
||||||
|
| Mutual Exclusion | 互斥条件 | 资源不能被共享 |
|
||||||
|
| Hold and Wait | 持有并等待 | 进程持有资源的同时等待新资源 |
|
||||||
|
| No Preemption | 不可剥夺 | 已分配的资源不能被强制收回 |
|
||||||
|
| Circular Wait | 循环等待 | 存在进程的循环等待链 |
|
||||||
|
| Deadlock Prevention | 死锁预防 | 破坏死锁的必要条件 |
|
||||||
|
| Deadlock Avoidance | 死锁避免 | 动态检查避免进入不安全状态 |
|
||||||
|
| Deadlock Detection | 死锁检测 | 检测死锁是否发生 |
|
||||||
|
| Safe State | 安全状态 | 存在安全序列的状态 |
|
||||||
|
| Banker's Algorithm | 银行家算法 | 经典的死锁避免算法 |
|
||||||
|
| Resource Allocation Graph | 资源分配图 | 描述资源分配关系的图 |
|
||||||
|
|
||||||
|
## 存储管理
|
||||||
|
|
||||||
|
| 英文术语 | 中文术语 | 简要说明 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| Memory Management | 存储管理 | 管理物理和虚拟内存 |
|
||||||
|
| Physical Address | 物理地址 | 内存硬件的实际地址 |
|
||||||
|
| Virtual Address | 虚拟地址 | 程序使用的逻辑地址 |
|
||||||
|
| Address Translation | 地址转换 | 虚拟地址到物理地址的映射 |
|
||||||
|
| Page | 页 | 虚拟地址空间的固定大小块 |
|
||||||
|
| Frame | 页框 | 物理内存的固定大小块 |
|
||||||
|
| Page Table | 页表 | 存储页到页框映射的表 |
|
||||||
|
| Translation Lookaside Buffer (TLB) | 转换后备缓冲器 | 页表的高速缓存 |
|
||||||
|
| Page Fault | 缺页 | 访问的页不在物理内存中 |
|
||||||
|
| Page Replacement | 页面替换 | 将页从磁盘调入内存,替换已有页 |
|
||||||
|
| Working Set | 工作集 | 进程当前使用的页面集合 |
|
||||||
|
| Thrashing | 抖动 | 频繁缺页导致性能急剧下降 |
|
||||||
|
| Segmentation | 分段 | 按逻辑单元划分地址空间 |
|
||||||
|
| Segmentation Fault | 段错误 | 访问非法内存地址 |
|
||||||
|
| Memory-Mapped File | 内存映射文件 | 将文件映射到进程地址空间 |
|
||||||
|
|
||||||
|
## 虚拟存储
|
||||||
|
|
||||||
|
| 英文术语 | 中文术语 | 简要说明 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| Virtual Memory | 虚拟存储 | 用磁盘扩展内存的技术 |
|
||||||
|
| Demand Paging | 请求分页 | 按需加载页面 |
|
||||||
|
| Copy-on-Write (COW) | 写时复制 | fork 时共享页面,写入时才复制 |
|
||||||
|
| Least Recently Used (LRU) | 最近最少使用 | 替换最久未使用的页面 |
|
||||||
|
| First-In-First-Out (FIFO) | 先进先出 | 替换最早进入的页面 |
|
||||||
|
| Clock Algorithm | 时钟算法 | LRU 的近似算法 |
|
||||||
|
| Dirty Page | 脏页 | 被修改过的页面 |
|
||||||
|
| Resident Set | 驻留集 | 进程在物理内存中的页面集合 |
|
||||||
|
| Swap | 交换 | 将整个进程在内存和磁盘间移动 |
|
||||||
|
| Swapping | 交换技术 | 在内存和外存间移动进程 |
|
||||||
|
|
||||||
|
## 文件系统
|
||||||
|
|
||||||
|
| 英文术语 | 中文术语 | 简要说明 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| File System | 文件系统 | 管理文件和目录的系统 |
|
||||||
|
| File Descriptor | 文件描述符 | 打开文件的整数标识符 |
|
||||||
|
| Inode | 索引节点 | 存储文件元信息的数据结构 |
|
||||||
|
| Directory | 目录 | 包含文件名和 inode 映射的特殊文件 |
|
||||||
|
| Hard Link | 硬链接 | 指向同一 inode 的多个目录项 |
|
||||||
|
| Symbolic Link (Symlink) | 符号链接 | 包含目标路径的特殊文件 |
|
||||||
|
| Mount | 挂载 | 将文件系统关联到目录树 |
|
||||||
|
| Block | 块 | 磁盘 I/O 的基本单位 |
|
||||||
|
| Superblock | 超级块 | 存储文件系统元信息的块 |
|
||||||
|
| File Allocation Table (FAT) | 文件分配表 | 一种文件系统组织方式 |
|
||||||
|
| Journaling | 日志 | 记录文件系统操作以保证一致性 |
|
||||||
|
| I-node Number | i-node 编号 | inode 的唯一标识 |
|
||||||
|
|
||||||
|
## I/O 系统
|
||||||
|
|
||||||
|
| 英文术语 | 中文术语 | 简要说明 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| System Call | 系统调用 | 用户程序请求内核服务的接口 |
|
||||||
|
| Trap | 陷入 | 从用户态切换到内核态 |
|
||||||
|
| Interrupt | 中断 | 硬件或软件发出的异步事件 |
|
||||||
|
| Device Driver | 设设备驱动 | 控制硬件设备的软件 |
|
||||||
|
| DMA (Direct Memory Access) | 直接内存访问 | 设备直接读写内存,无需 CPU |
|
||||||
|
| Buffer | 缓冲区 | 临时存储数据的区域 |
|
||||||
|
| Spooling | 假脱机 | 将设备输出先写到磁盘 |
|
||||||
|
| Blocking I/O | 阻塞 I/O | I/O 未完成时进程被阻塞 |
|
||||||
|
| Non-blocking I/O | 非阻塞 I/O | I/O 未完成时立即返回 |
|
||||||
|
| I/O Multiplexing | I/O 多路复用 | 用 select/poll/epoll 同时监听多个 I/O |
|
||||||
|
| Asynchronous I/O | 异步 I/O | I/O 完成后通知进程 |
|
||||||
|
|
||||||
|
## 网络编程
|
||||||
|
|
||||||
|
| 英文术语 | 中文术语 | 简要说明 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| Socket | 套接字 | 网络通信的端点 |
|
||||||
|
| TCP (Transmission Control Protocol) | 传输控制协议 | 可靠的、面向连接的传输协议 |
|
||||||
|
| UDP (User Datagram Protocol) | 用户数据报协议 | 不可靠的、无连接的传输协议 |
|
||||||
|
| IP (Internet Protocol) | 网际协议 | 网络层协议 |
|
||||||
|
| Port | 端口 | 进程的网络标识(0~65535) |
|
||||||
|
| Client | 客户端 | 发起连接的一方 |
|
||||||
|
| Server | 服务器 | 接受连接的一方 |
|
||||||
|
| Three-Way Handshake | 三次握手 | TCP 建立连接的过程 |
|
||||||
|
| Four-Way Termination | 四次挥手 | TCP 断开连接的过程 |
|
||||||
|
| Byte Order | 字节序 | 大端序与小端序 |
|
||||||
|
| Network Byte Order | 网络字节序 | 大端序(Big-Endian) |
|
||||||
|
| DNS (Domain Name System) | 域名系统 | 主机名到 IP 地址的映射 |
|
||||||
|
| HTTP (HyperText Transfer Protocol) | 超文本传输协议 | Web 的应用层协议 |
|
||||||
|
| Concurrent Server | 并发服务器 | 同时处理多个客户端连接 |
|
||||||
|
| Iterative Server | 迭代服务器 | 一次处理一个客户端连接 |
|
||||||
|
| Proxy Server | 代理服务器 | 代替客户端向服务器请求 |
|
||||||
|
|
||||||
|
## 并发编程
|
||||||
|
|
||||||
|
| 英文术语 | 中文术语 | 简要说明 |
|
||||||
|
|----------|----------|----------|
|
||||||
|
| Concurrency | 并发 | 多个任务在逻辑上同时推进 |
|
||||||
|
| Parallelism | 并行 | 多个任务在物理上同时执行 |
|
||||||
|
| Thread Pool | 线程池 | 预先创建一组线程,重复使用 |
|
||||||
|
| Prethreading | 预线程化 | 提前创建线程池的技术 |
|
||||||
|
| Task Queue | 任务队列 | 存储待处理任务的队列 |
|
||||||
|
| Work Stealing | 工作窃取 | 空闲线程从其他线程获取任务 |
|
||||||
|
| Scalability | 可扩展性 | 增加资源时性能提升的程度 |
|
||||||
|
| Speedup | 加速比 | 并行执行时间与串行执行时间的比值 |
|
||||||
|
| Amdahl's Law | 阿姆达尔定律 | 并行加速的理论上限 |
|
||||||
|
| Load Balancing | 负载均衡 | 将任务均匀分配到多个处理单元 |
|
||||||
|
| Thread-Safe | 线程安全 | 函数在多线程环境下正确运行 |
|
||||||
|
| Reentrant | 可重入 | 函数可被中断后安全重新进入 |
|
||||||
239
操作系统/附录/附录C_学习路径指南.md
Normal file
239
操作系统/附录/附录C_学习路径指南.md
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
# 附录C 学习路径指南
|
||||||
|
|
||||||
|
> 根据不同背景和目标,定制个性化的操作系统课程学习方案。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 按基础水平定制
|
||||||
|
|
||||||
|
### 方案一:有 C 语言基础,无 Linux 经验
|
||||||
|
|
||||||
|
**推荐路径:**
|
||||||
|
|
||||||
|
```
|
||||||
|
第01讲 Linux 简介与使用(重点:命令行操作、文件系统)
|
||||||
|
|
|
||||||
|
第02讲 Linux 环境 C 编程(重点:GCC 编译、GDB 调试)
|
||||||
|
|
|
||||||
|
第03讲 文件 I/O 编程(重点:open/read/write、标准 I/O)
|
||||||
|
|
|
||||||
|
第04讲 进程控制(重点:fork/exec/wait)
|
||||||
|
|
|
||||||
|
第05讲 进程间通信(重点:管道、消息队列)
|
||||||
|
|
|
||||||
|
第06讲 多线程编程(重点:pthread、信号量)
|
||||||
|
|
|
||||||
|
第07讲 网络编程基础(重点:Socket API、TCP 模型)
|
||||||
|
|
|
||||||
|
第08讲 并发网络服务器(重点:多线程服务器)
|
||||||
|
|
|
||||||
|
第09讲 操作系统原理(结合实验理解理论)
|
||||||
|
```
|
||||||
|
|
||||||
|
**学习建议:**
|
||||||
|
|
||||||
|
- 花 1~2 周熟悉 Linux 命令行,重点掌握 `ls`、`cd`、`mkdir`、`chmod`、`gcc`、`gdb`
|
||||||
|
- 实验一(I/O 编程)是基础中的基础,务必扎实掌握
|
||||||
|
- 每个实验先读懂示例代码,再自己动手修改
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 方案二:有 Linux 经验,C 语言基础一般
|
||||||
|
|
||||||
|
**推荐路径:**
|
||||||
|
|
||||||
|
```
|
||||||
|
第01讲 C 语言回顾(重点:指针、结构体、动态内存)
|
||||||
|
|
|
||||||
|
第02讲 文件 I/O(重点:系统调用 vs 库函数)
|
||||||
|
|
|
||||||
|
第03讲 进程控制(重点:fork 的理解)
|
||||||
|
|
|
||||||
|
第04讲 进程间通信
|
||||||
|
|
|
||||||
|
第05讲 多线程编程(重点:同步机制)
|
||||||
|
|
|
||||||
|
第06讲 网络编程
|
||||||
|
|
|
||||||
|
第07讲 并发服务器
|
||||||
|
|
|
||||||
|
第08讲 操作系统原理
|
||||||
|
```
|
||||||
|
|
||||||
|
**学习建议:**
|
||||||
|
|
||||||
|
- 重点补强 C 语言指针和内存管理
|
||||||
|
- 实验中遇到的 C 语言问题及时查阅资料
|
||||||
|
- 可以跳过 Linux 基础操作部分,直接进入编程实验
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 方案三:有 C 语言 + Linux 经验
|
||||||
|
|
||||||
|
**推荐路径:**
|
||||||
|
|
||||||
|
```
|
||||||
|
快速浏览第01~03讲,直接进入实验
|
||||||
|
|
|
||||||
|
第04讲 进程控制(重点:shell 实现、daemon)
|
||||||
|
|
|
||||||
|
第05讲 进程间通信(重点:共享内存 + 信号量)
|
||||||
|
|
|
||||||
|
第06讲 多线程编程(重点:竞态条件、生产者-消费者)
|
||||||
|
|
|
||||||
|
第07讲 网络编程(重点:HTTP 协议、文件传输)
|
||||||
|
|
|
||||||
|
第08讲 并发服务器(重点:预线程化、I/O 多路复用)
|
||||||
|
|
|
||||||
|
第09讲 操作系统原理(深入理解调度、内存管理)
|
||||||
|
```
|
||||||
|
|
||||||
|
**学习建议:**
|
||||||
|
|
||||||
|
- 可以直接做选做题(task43、task54、task66、task67 等)
|
||||||
|
- 重点理解操作系统原理,将实验经验与理论结合
|
||||||
|
- 尝试优化代码性能,如并行求和的加速比分析
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 按学习目标定制
|
||||||
|
|
||||||
|
### 目标一:通过课程考试
|
||||||
|
|
||||||
|
**重点内容:**
|
||||||
|
|
||||||
|
| 章节 | 考试重点 | 权重 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 进程管理 | fork/exec/wait 的工作原理、僵尸进程 | 高 |
|
||||||
|
| 调度算法 | FCFS、SJF、RR、优先级调度的对比 | 高 |
|
||||||
|
| 同步互斥 | 信号量、互斥锁、生产者-消费者问题 | 高 |
|
||||||
|
| 死锁 | 四个必要条件、银行家算法 | 中 |
|
||||||
|
| 存储管理 | 页式存储、缺页中断、页面置换算法 | 高 |
|
||||||
|
| 文件系统 | inode、目录结构、磁盘调度 | 中 |
|
||||||
|
| 网络编程 | TCP 三次握手/四次挥手、Socket 编程流程 | 中 |
|
||||||
|
|
||||||
|
**复习策略:**
|
||||||
|
|
||||||
|
- 理解概念 > 死记硬背
|
||||||
|
- 能用代码演示的概念(如 fork、信号量)通过实验加深理解
|
||||||
|
- 算法题要会手动模拟执行过程
|
||||||
|
- 整理每个章节的核心知识点和易混淆概念
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 目标二:提升编程能力
|
||||||
|
|
||||||
|
**重点内容:**
|
||||||
|
|
||||||
|
| 实验 | 核心技能 | 难度 |
|
||||||
|
|------|----------|------|
|
||||||
|
| 实验01 I/O 编程 | 文件读写、结构体序列化 | ★★ |
|
||||||
|
| 实验02 进程控制 | fork/exec/wait、shell 实现 | ★★★ |
|
||||||
|
| 实验03 多线程 | pthread、信号量同步 | ★★★★ |
|
||||||
|
| 实验04 进程间通信 | 管道、消息队列、共享内存 | ★★★ |
|
||||||
|
| 实验05 网络通信 | Socket 编程、HTTP 协议 | ★★★ |
|
||||||
|
| 实验06 并发服务器 | 多进程/多线程/预线程化 | ★★★★ |
|
||||||
|
|
||||||
|
**练习建议:**
|
||||||
|
|
||||||
|
- 每个实验至少完整实现一遍
|
||||||
|
- 尝试扩展功能(如给 shell 加上后台执行、给服务器加上日志)
|
||||||
|
- 阅读课程提供的源代码,学习代码风格和错误处理
|
||||||
|
- 做完必做题后挑战选做题
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 目标三:考研准备
|
||||||
|
|
||||||
|
**重点理论知识:**
|
||||||
|
|
||||||
|
| 主题 | 考点 | 对应章节 |
|
||||||
|
|------|------|----------|
|
||||||
|
| 进程与线程 | PCB、进程状态转换、线程模型 | 第04讲 |
|
||||||
|
| 处理机调度 | 调度算法、周转时间计算 | 第04讲 |
|
||||||
|
| 进程同步 | 信号量机制、经典同步问题 | 第06讲 |
|
||||||
|
| 死锁 | 必要条件、预防/避免/检测 | 第04讲 |
|
||||||
|
| 内存管理 | 分页、分段、虚拟内存、页面置换 | 第05讲 |
|
||||||
|
| 文件系统 | 目录结构、磁盘调度算法 | 第05讲 |
|
||||||
|
| I/O 管理 | I/O 控制方式、缓冲技术 | 第03讲 |
|
||||||
|
|
||||||
|
**复习策略:**
|
||||||
|
|
||||||
|
- 理论与实验结合:用 fork 理解进程创建,用信号量理解同步
|
||||||
|
- 做历年真题,重点练习算法模拟题(如银行家算法、页面置换)
|
||||||
|
- 整理易混淆概念对比表(如进程 vs 线程、互斥 vs 同步)
|
||||||
|
- 用思维导图梳理每章知识结构
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 目标四:就业/工程实践
|
||||||
|
|
||||||
|
**重点技能:**
|
||||||
|
|
||||||
|
| 技能 | 对应内容 | 实用性 |
|
||||||
|
|------|----------|--------|
|
||||||
|
| Linux 系统编程 | fork、exec、信号、I/O | 极高 |
|
||||||
|
| 多线程编程 | pthread、锁、条件变量、线程池 | 极高 |
|
||||||
|
| 网络编程 | Socket、TCP/UDP、HTTP | 极高 |
|
||||||
|
| 并发服务器 | 多进程/多线程/事件驱动 | 高 |
|
||||||
|
| 性能优化 | 加速比分析、I/O 优化 | 中 |
|
||||||
|
| 系统调试 | GDB、strace、valgrind | 高 |
|
||||||
|
|
||||||
|
**实践建议:**
|
||||||
|
|
||||||
|
- 所有实验的选做题都值得完成
|
||||||
|
- 尝试用 epoll 替代 select 实现高性能服务器
|
||||||
|
- 学习使用 strace 跟踪系统调用,理解程序行为
|
||||||
|
- 阅读开源项目(如 Nginx、Redis)的并发模型
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 学习节奏建议
|
||||||
|
|
||||||
|
### 16 周学期安排
|
||||||
|
|
||||||
|
| 周次 | 内容 | 任务 |
|
||||||
|
|------|------|------|
|
||||||
|
| 1~2 | Linux 基础 + C 语言环境 | 熟悉命令行、GCC、GDB |
|
||||||
|
| 3~4 | I/O 编程 | 完成实验01(task41~44) |
|
||||||
|
| 5~6 | 进程控制 | 完成实验02(task51~53) |
|
||||||
|
| 7~8 | 进程间通信 | 完成实验04(task71~73) |
|
||||||
|
| 9~10 | 多线程编程 | 完成实验03(task61~64) |
|
||||||
|
| 11~12 | 网络编程 | 完成实验05(task83~84) |
|
||||||
|
| 13~14 | 并发服务器 | 完成实验06(task92~94) |
|
||||||
|
| 15~16 | 复习 + 选做实验 | 查漏补缺、挑战选做题 |
|
||||||
|
|
||||||
|
### 每周学习建议
|
||||||
|
|
||||||
|
- **理论学习(2~3 小时):** 阅读课件、理解概念
|
||||||
|
- **实验编程(3~4 小时):** 完成实验任务
|
||||||
|
- **总结复习(1 小时):** 整理笔记、记录疑问
|
||||||
|
- **总投入:** 每周约 6~8 小时
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见学习误区
|
||||||
|
|
||||||
|
| 误区 | 正确做法 |
|
||||||
|
|------|----------|
|
||||||
|
| 只看不写代码 | 动手敲代码,遇到问题再查资料 |
|
||||||
|
| 直接抄示例代码 | 先理解思路,再自己实现,最后对比优化 |
|
||||||
|
| 忽略错误处理 | 养成检查返回值、使用 perror 的习惯 |
|
||||||
|
| 不理解就死记 | fork 的返回值为什么要分两种?理解了就不容易忘 |
|
||||||
|
| 忽略编译选项 | 理解 `-Wall`、`-g`、`-lpthread` 的作用 |
|
||||||
|
| 不做实验报告 | 写报告的过程就是整理思路的过程 |
|
||||||
|
| 孤立学习各章节 | 注意知识关联:进程->线程->同步->网络->并发 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 推荐参考资料
|
||||||
|
|
||||||
|
| 类型 | 资源 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| 教材 | 《深入理解计算机系统》(CS:APP) | 实验来源,强烈推荐 |
|
||||||
|
| 教材 | 《Operating System Concepts》 | 操作系统经典教材 |
|
||||||
|
| 教材 | 《UNIX 环境高级编程》(APUE) | UNIX 编程权威参考 |
|
||||||
|
| 在线 | CS:APP 官网实验 | CMU 配套实验 |
|
||||||
|
| 工具 | `man` 手册 | `man 2 fork`、`man 3 printf` |
|
||||||
|
| 工具 | `strace` | 跟踪系统调用 |
|
||||||
|
| 工具 | `valgrind` | 内存泄漏检测 |
|
||||||
BIN
软件需求分析/Pasted image 20260610205411.png
Normal file
BIN
软件需求分析/Pasted image 20260610205411.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.0 KiB |
BIN
软件需求分析/Pasted image 20260610205443.png
Normal file
BIN
软件需求分析/Pasted image 20260610205443.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
Reference in New Issue
Block a user