From 224c3dc574a24ae20188d1bef835e844160e75db Mon Sep 17 00:00:00 2001 From: ChoChoX Date: Sat, 13 Jun 2026 23:46:22 +0800 Subject: [PATCH] vault backup: 2026-06-13 23:46:22 --- 操作系统/00_操作系统概述/00_操作系统概述.md | 527 ++++++++++ 操作系统/00_课程导航.md | 163 +++ 操作系统/01_系统运行机制/01_系统运行机制.md | 696 +++++++++++++ 操作系统/02_Linux基础/02_Linux基础.md | 353 +++++++ 操作系统/03_C语言编程基础/03_C语言编程基础.md | 631 ++++++++++++ 操作系统/04_文件IO编程/04_文件IO编程.md | 619 ++++++++++++ 操作系统/05_磁盘空间管理/05_磁盘空间管理.md | 595 +++++++++++ 操作系统/06_进程控制/06_进程控制.md | 868 ++++++++++++++++ 操作系统/06_进程控制/06_进程控制_深入.md | 315 ++++++ 操作系统/07_多线程编程/07_多线程编程.md | 840 ++++++++++++++++ 操作系统/08_进程间通信/08_进程间通信.md | 406 ++++++++ 操作系统/09_网络编程基础/09_网络编程基础.md | 296 ++++++ 操作系统/10_并发服务器/10_并发服务器.md | 395 ++++++++ 操作系统/11_处理机调度/11_处理机调度.md | 705 +++++++++++++ 操作系统/12_死锁/12_死锁.md | 376 +++++++ 操作系统/13_存储管理基础/13_存储管理基础.md | 303 ++++++ 操作系统/14_分页存储管理/14_分页存储管理.md | 597 +++++++++++ 操作系统/15_段式存储管理/15_段式存储管理.md | 398 ++++++++ 操作系统/16_虚拟存储器/16_虚拟存储器.md | 606 +++++++++++ 操作系统/17_IO系统/17_IO系统.md | 937 ++++++++++++++++++ 操作系统/18_程序代码优化/18_程序代码优化.md | 731 ++++++++++++++ 操作系统/实验/实验01_IO编程.md | 337 +++++++ 操作系统/实验/实验02_进程控制.md | 437 ++++++++ 操作系统/实验/实验03_多线程编程.md | 470 +++++++++ 操作系统/实验/实验04_进程间通信.md | 437 ++++++++ 操作系统/实验/实验05_网络通信.md | 423 ++++++++ 操作系统/实验/实验06_并发服务器.md | 526 ++++++++++ 操作系统/模板/课件笔记模板.md | 225 +++++ 操作系统/附录/附录A_Wrapper库参考.md | 360 +++++++ 操作系统/附录/附录B_术语表.md | 186 ++++ 操作系统/附录/附录C_学习路径指南.md | 239 +++++ 软件需求分析/Pasted image 20260610205411.png | Bin 0 -> 6113 bytes 软件需求分析/Pasted image 20260610205443.png | Bin 0 -> 6385 bytes 33 files changed, 14997 insertions(+) create mode 100644 操作系统/00_操作系统概述/00_操作系统概述.md create mode 100644 操作系统/00_课程导航.md create mode 100644 操作系统/01_系统运行机制/01_系统运行机制.md create mode 100644 操作系统/02_Linux基础/02_Linux基础.md create mode 100644 操作系统/03_C语言编程基础/03_C语言编程基础.md create mode 100644 操作系统/04_文件IO编程/04_文件IO编程.md create mode 100644 操作系统/05_磁盘空间管理/05_磁盘空间管理.md create mode 100644 操作系统/06_进程控制/06_进程控制.md create mode 100644 操作系统/06_进程控制/06_进程控制_深入.md create mode 100644 操作系统/07_多线程编程/07_多线程编程.md create mode 100644 操作系统/08_进程间通信/08_进程间通信.md create mode 100644 操作系统/09_网络编程基础/09_网络编程基础.md create mode 100644 操作系统/10_并发服务器/10_并发服务器.md create mode 100644 操作系统/11_处理机调度/11_处理机调度.md create mode 100644 操作系统/12_死锁/12_死锁.md create mode 100644 操作系统/13_存储管理基础/13_存储管理基础.md create mode 100644 操作系统/14_分页存储管理/14_分页存储管理.md create mode 100644 操作系统/15_段式存储管理/15_段式存储管理.md create mode 100644 操作系统/16_虚拟存储器/16_虚拟存储器.md create mode 100644 操作系统/17_IO系统/17_IO系统.md create mode 100644 操作系统/18_程序代码优化/18_程序代码优化.md create mode 100644 操作系统/实验/实验01_IO编程.md create mode 100644 操作系统/实验/实验02_进程控制.md create mode 100644 操作系统/实验/实验03_多线程编程.md create mode 100644 操作系统/实验/实验04_进程间通信.md create mode 100644 操作系统/实验/实验05_网络通信.md create mode 100644 操作系统/实验/实验06_并发服务器.md create mode 100644 操作系统/模板/课件笔记模板.md create mode 100644 操作系统/附录/附录A_Wrapper库参考.md create mode 100644 操作系统/附录/附录B_术语表.md create mode 100644 操作系统/附录/附录C_学习路径指南.md create mode 100644 软件需求分析/Pasted image 20260610205411.png create mode 100644 软件需求分析/Pasted image 20260610205443.png diff --git a/操作系统/00_操作系统概述/00_操作系统概述.md b/操作系统/00_操作系统概述/00_操作系统概述.md new file mode 100644 index 0000000..dc71677 --- /dev/null +++ b/操作系统/00_操作系统概述/00_操作系统概述.md @@ -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_课程导航]] | diff --git a/操作系统/00_课程导航.md b/操作系统/00_课程导航.md new file mode 100644 index 0000000..f065a3d --- /dev/null +++ b/操作系统/00_课程导航.md @@ -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 | diff --git a/操作系统/01_系统运行机制/01_系统运行机制.md b/操作系统/01_系统运行机制/01_系统运行机制.md new file mode 100644 index 0000000..637c160 --- /dev/null +++ b/操作系统/01_系统运行机制/01_系统运行机制.md @@ -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["保存断点
(PC、PSW 压入内核栈)"] + D --> E["保存通用寄存器
(保存进程现场)"] + E --> F["识别中断源
(查询中断向量表)"] + F --> G["跳转到中断服务程序
(PC ← 中断向量地址)"] + G --> H["执行中断服务程序
(处理具体的中断事件)"] + H --> I["恢复通用寄存器
(恢复进程现场)"] + I --> J["恢复断点
(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["逻辑地址空间
0x0000 ~ 0xFFFF"] + end + + subgraph MMU_Block["MMU(存储管理部件)"] + TR1["地址变换函数 tr1()"] + end + + subgraph PA_Phys["物理内存"] + PA_M["区域 1
物理地址 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["逻辑地址空间
0x0000 ~ 0xFFFF"] + end + + subgraph MMU_Block2["MMU(存储管理部件)"] + TR2["地址变换函数 tr2()"] + end + + subgraph PB_Phys["物理内存"] + PB_M["区域 2
物理地址 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["触发保护异常 ✗
终止进程"] + 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 -->|"中断/异常发生
(时钟中断、I/O中断、缺页等)"| C + A -->|"系统调用
(int $0x80 / SWI)"| D + C -->|"中断返回
(IRET)"| A + D -->|"系统调用返回
(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["用户程序
printf(#quot;Hello#quot;)"] --> B["C 库函数
write(1, #quot;Hello#quot;, 5)"] + B --> C["系统调用包装函数
将参数写入寄存器
EAX=4 (sys_write 号)
EBX=1 (文件描述符)
ECX=缓冲区地址
EDX=5 (长度)"] + C --> D["软中断指令
int $0x80"] + D --> E["CPU 模式切换
用户模式 → 内核模式
保存断点和现场"] + E --> F["查询系统调用表
根据 EAX=4 找到
sys_write 函数地址"] + F --> G["执行内核函数
sys_write()
实际完成屏幕输出"] + G --> H["系统调用返回
恢复现场
内核模式 → 用户模式"] + H --> I["返回用户程序
继续执行"] + + 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["外部中断
(硬件中断)"] + A --> C["异常
(内部中断)"] + + B --> B1["I/O 中断
键盘、鼠标、网卡等设备发出"] + B --> B2["时钟中断
RTC 周期性发出"] + + C --> C1["系统调用
用户执行 int $0x80 / SWI"] + C --> C2["缺页异常
访问的页面不在内存中"] + C --> C3["断点指令
调试器设置的断点"] + C --> C4["算术溢出
除零错误等"] + + 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["进入内核
调度器决定是否切换进程"] + B -->|"系统调用"| D["进入内核
执行内核函数"] + B -->|"I/O 请求"| E["进程 P1 等待 I/O
CPU 分配给其他进程"] + B -->|"进程结束"| F["进程 P1 终止
CPU 分配给其他进程"] + + C --> G{"调度器决策"} + G -->|"继续执行 P1"| A + G -->|"切换到 P2"| H["保存 P1 现场
恢复 P2 现场
执行进程 P2"] + + D --> I{"系统调用是否阻塞?"} + I -->|"不阻塞"| A + I -->|"需要等待"| E + + E --> J["P1 进入等待队列
CPU 执行进程 P2"] + F --> K["回收 P1 资源
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["时钟中断
(RTC 硬件)"] + MMU_HW["MMU 硬件
(地址转换)"] + CPU_MODE["CPU 双模式
(IOPL/M[4:0])"] + INT_HW["中断控制器
(PIC/APIC)"] + end + + subgraph OS核心功能 + PROC["进程管理
(调度、切换)"] + MEM["内存管理
(地址隔离)"] + PROT["系统保护
(内核安全)"] + IO["I/O 管理
(设备驱动)"] + 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章 diff --git a/操作系统/02_Linux基础/02_Linux基础.md b/操作系统/02_Linux基础/02_Linux基础.md new file mode 100644 index 0000000..52070eb --- /dev/null +++ b/操作系统/02_Linux基础/02_Linux基础.md @@ -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/ diff --git a/操作系统/03_C语言编程基础/03_C语言编程基础.md b/操作系统/03_C语言编程基础/03_C语言编程基础.md new file mode 100644 index 0000000..f0e3daf --- /dev/null +++ b/操作系统/03_C语言编程基础/03_C语言编程基础.md @@ -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 +#include + +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 +#include + +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 +#include + +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 + +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 +#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 ` 在预处理后变成了什么? +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/ diff --git a/操作系统/04_文件IO编程/04_文件IO编程.md b/操作系统/04_文件IO编程/04_文件IO编程.md new file mode 100644 index 0000000..eb73bbe --- /dev/null +++ b/操作系统/04_文件IO编程/04_文件IO编程.md @@ -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 +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 | +|------|---------------|---------| +| 头文件 | `` | ``, `` | +| 缓冲 | 用户空间缓冲(默认行缓冲/全缓冲) | 无用户缓冲 | +| 性能 | 频繁小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/` diff --git a/操作系统/05_磁盘空间管理/05_磁盘空间管理.md b/操作系统/05_磁盘空间管理/05_磁盘空间管理.md new file mode 100644 index 0000000..e5f2df5 --- /dev/null +++ b/操作系统/05_磁盘空间管理/05_磁盘空间管理.md @@ -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) diff --git a/操作系统/06_进程控制/06_进程控制.md b/操作系统/06_进程控制/06_进程控制.md new file mode 100644 index 0000000..3b04f32 --- /dev/null +++ b/操作系统/06_进程控制/06_进程控制.md @@ -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 +#include +#include +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 +#include +#include +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["当前进程
PID=1234
执行 myprogram"] -->|execvp("ps", args)| B["同一进程
PID=1234
执行 ps 命令"] + B --> C["原来的代码段
被完全替换"] + + 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)` | `` | 执行清理工作(刷新缓冲区、调用 atexit 注册的函数),然后终止 | +| `_exit(status)` | `` | 立即终止,不执行任何清理 | + +### 5.2 exit 的清理过程 + +```mermaid +graph TD + A["调用 exit(status)"] --> B["执行 atexit() 注册的清理函数"] + B --> C["刷新 stdio 缓冲区
(fclose 所有打开的流)"] + C --> D["调用 _exit(status)"] + D --> E["内核回收进程资源"] + + style A fill:#e1f5fe + style E fill:#ffcdd2 +``` + +### 5.3 退出状态 + +参考 `实例源代码/chap5/exitstatus.c`: + +```c +#include +int main() { exit(100); } +``` + +父进程通过 `waitpid` 的 `WEXITSTATUS(status)` 宏获取子进程的退出状态值(0-255)。 + +--- + +## 六、僵尸进程 + +### 6.1 什么是僵尸进程 + +当子进程终止后,如果父进程没有调用 `wait()` 或 `waitpid()` 回收子进程的退出状态,子进程的进程控制块(PCB)仍然保留在系统中,成为**僵尸进程(Zombie Process)**。 + +### 6.2 僵尸进程的产生 + +参考 `实例源代码/chap5/zombie.c`: + +```c +#include +#include +#include +#include +#include + +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["子进程成为孤儿进程
被 init 收养"] + B --> C["第2步: setsid()"] + C --> D["创建新会话
成为会话首进程
脱离控制终端"] + D --> E["第3步: fork() + 父进程 exit()"] + E --> F["确保不会重新获得
控制终端"] + F --> G["第4步: chdir('/')"] + G --> H["避免占用可卸载的
文件系统"] + H --> I["第5步: 关闭 0/1/2
重定向到 /dev/null"] + I --> J["Daemon 创建完成
在后台运行"] + + 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章:进程的创建、终止、监控 diff --git a/操作系统/06_进程控制/06_进程控制_深入.md b/操作系统/06_进程控制/06_进程控制_深入.md new file mode 100644 index 0000000..1de2eea --- /dev/null +++ b/操作系统/06_进程控制/06_进程控制_深入.md @@ -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 +#include +#include +#include + +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/) diff --git a/操作系统/07_多线程编程/07_多线程编程.md b/操作系统/07_多线程编程/07_多线程编程.md new file mode 100644 index 0000000..eee58e9 --- /dev/null +++ b/操作系统/07_多线程编程/07_多线程编程.md @@ -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 通信
(管道/共享内存)"| 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["主线程调用
pthread_create()"] --> B["操作系统创建新线程"] + B --> C["新线程执行
start_routine(arg)"] + C --> D["线程函数返回"] + D --> E["主线程调用
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操作)
S--"] -->|S >= 0| OK["允许通过"] + P -->|S < 0| BLOCK["阻塞等待"] + + V["sem_post (V操作)
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 + +#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)
等待空槽位"] + P2 --> P3["sem_wait(mutex)
进入临界区"] + P3 --> P4["放入缓冲区"] + P4 --> P5["sem_post(mutex)
离开临界区"] + P5 --> P6["sem_post(full)
通知消费者"] + P6 --> P1 + end + + subgraph "消费者" + C1["sem_wait(full)
等待满槽位"] --> C2["sem_wait(mutex)
进入临界区"] + C2 --> C3["取出数据"] + C3 --> C4["sem_post(mutex)
离开临界区"] + C4 --> C5["sem_post(empty)
通知生产者"] + 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 +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 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["复制地址空间
(写时复制)"] + 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["主线程
接收任务"] -->|"放入缓冲区"| 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/) diff --git a/操作系统/08_进程间通信/08_进程间通信.md b/操作系统/08_进程间通信/08_进程间通信.md new file mode 100644 index 0000000..0d515a7 --- /dev/null +++ b/操作系统/08_进程间通信/08_进程间通信.md @@ -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 +#include +#include +#include +#include + +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 形式运行\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 形式运行\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 \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 \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) diff --git a/操作系统/09_网络编程基础/09_网络编程基础.md b/操作系统/09_网络编程基础/09_网络编程基础.md new file mode 100644 index 0000000..bc58e14 --- /dev/null +++ b/操作系统/09_网络编程基础/09_网络编程基础.md @@ -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 \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 \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) diff --git a/操作系统/10_并发服务器/10_并发服务器.md b/操作系统/10_并发服务器/10_并发服务器.md new file mode 100644 index 0000000..c7d6136 --- /dev/null +++ b/操作系统/10_并发服务器/10_并发服务器.md @@ -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 \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 \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 \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) diff --git a/操作系统/11_处理机调度/11_处理机调度.md b/操作系统/11_处理机调度/11_处理机调度.md new file mode 100644 index 0000000..ae26484 --- /dev/null +++ b/操作系统/11_处理机调度/11_处理机调度.md @@ -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[后备队列
作业池] + end + + subgraph 内存 + B[就绪队列] + C[阻塞队列] + D[CPU 执行] + end + + A -->|高级调度
长程调度
作业调度| B + B -->|低级调度
短程调度
进程调度| D + D -->|时间片用完/等待事件| B + D -->|等待I/O| C + C -->|I/O完成| B + + B -->|中级调度
交换调度
内存调度| A + + style A fill:#f9f,stroke:#333 + style D fill:#ff9,stroke:#333 +``` + +| 调度级别 | 别名 | 频率 | 功能 | +|----------|------|------|------| +| **高级调度** | 作业调度 / 长程调度 | 最低 | 从后备队列选择作业调入内存 | +| **中级调度** | 交换调度 / 内存调度 | 中等 | 将暂时不用的进程换出到外存(挂起) | +| **低级调度** | 进程调度 / 短程调度 | 最高 | 从就绪队列选择进程分配 CPU | + +--- + +## 四、调度时机 + +### 4.1 何时触发调度 + +```mermaid +flowchart TD + A{发生什么事件?} --> B[进程结束] + A --> C[进程等待事件
如I/O请求] + A --> D[时间片用完] + A --> E[新进程产生] + A --> F[等待的事件已发生
如I/O完成] + + B --> G[执行调度] + C --> G + D --> G + E --> G + F --> G + + G --> H[选择下一个
运行的进程] + + 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
即 SRTF] + + B --> D[进程开始执行后
不会被中断] + C --> E[新进程到达时
比较剩余时间
更短则抢占] + + 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[短作业:等待时间相同时
运行时间短 → 响应比高] + B --> D[长作业:等待时间越长
响应比越高 → 不会饥饿] +``` + +#### 示例 + +| 进程 | 到达时间 | 运行时间 | +|------|----------|----------| +| 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
响应时间变长] + C --> F[进程切换频繁
系统开销过大] + D --> G[兼顾响应性和效率
通常 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
优先级最高
时间片最小 如4ms] + B -->|时间片用完
未完成| C[队列2
优先级中等
时间片中等 如8ms] + C -->|时间片用完
未完成| D[队列3
优先级最低
时间片最大 如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间接阻塞!
优先级倒置发生! + 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
最小的进程} + B --> C[红黑树组织
就绪进程] + C --> D[最左叶子节点
= 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
最高优先级
停止特定CPU] + A --> C[Real-Time
实时调度类] + A --> D[Fair
CFS 调度类
普通进程] + A --> E[Idle Task
最低优先级
空闲任务] + + 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
简单公平] + SJF[SJF/SRTF
最优平均等待] + HRRF[HRRF
FCFS+SJF折衷] + PRIO[优先级调度
灵活控制] + end + + subgraph 分时算法 + RR[RR 轮转
公平响应] + MFQ[多级反馈队列
综合最优] + end + + subgraph 实时算法 + EDF[EDF
截止时间优先] + LLF[LLF
松弛度优先] + 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_死锁]] diff --git a/操作系统/12_死锁/12_死锁.md b/操作系统/12_死锁/12_死锁.md new file mode 100644 index 0000000..a1c74b7 --- /dev/null +++ b/操作系统/12_死锁/12_死锁.md @@ -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 +#include +#include + +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 +#include +#include + +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/) diff --git a/操作系统/13_存储管理基础/13_存储管理基础.md b/操作系统/13_存储管理基础/13_存储管理基础.md new file mode 100644 index 0000000..477ac2b --- /dev/null +++ b/操作系统/13_存储管理基础/13_存储管理基础.md @@ -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
源文件"] -->|预处理| B["hello.i
预处理后"] + B -->|编译| C["hello.s
汇编文件"] + C -->|汇编| D["hello.o
目标文件(可重定位)"] + D -->|链接| E["hello / a.out
可执行文件"] + 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 -->|"地址映射
(页表/段表)"| 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 发出
虚拟地址(VA)"] --> MMU["MMU
地址转换"] + 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_分页存储管理]] diff --git a/操作系统/14_分页存储管理/14_分页存储管理.md b/操作系统/14_分页存储管理/14_分页存储管理.md new file mode 100644 index 0000000..35cbd4e --- /dev/null +++ b/操作系统/14_分页存储管理/14_分页存储管理.md @@ -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["访问页表
(页表基址寄存器PTBR + VPN × PTE大小)"] + E --> F{"页表项有效位?"} + F -->|"V=1"| G["取出PPN"] + F -->|"V=0"| H["**缺页中断**
操作系统处理"] + 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["终止进程
(非法访问)"] + D -->|"有空闲"| F["从外存读入该页"] + D -->|"无空闲"| G["执行页面置换算法
选择牺牲页"] + G --> H{"牺牲页Dirty=1?"} + H -->|"是"| I["将牺牲页写回外存"] + H -->|"否"| J["直接覆盖"] + I --> F + J --> F + F --> K["修改页表
设置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
(快表)"} + TLB -->|"命中
取出PPN"| PA["物理地址"] + TLB -->|"未命中"| PT["查页表
(内存)"] + 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
(页目录基址)"] --> PD["页目录
(1024项)"] + PD -->|"页目录项"| PT1["页表1
(1024项)"] + PD -->|"页目录项"| PT2["页表2
(1024项)"] + PD -->|"页目录项"| PT3["页表3
(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_段式存储管理]] diff --git a/操作系统/15_段式存储管理/15_段式存储管理.md b/操作系统/15_段式存储管理/15_段式存储管理.md new file mode 100644 index 0000000..aed451a --- /dev/null +++ b/操作系统/15_段式存储管理/15_段式存储管理.md @@ -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["**越界中断**
(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查段表
得到该段的页表基址和段长"] + C --> D{"P < 该段页数?"} + D -->|"是"| E["3. 用页号P查页表
得到物理页框号 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_虚拟存储器]] diff --git a/操作系统/16_虚拟存储器/16_虚拟存储器.md b/操作系统/16_虚拟存储器/16_虚拟存储器.md new file mode 100644 index 0000000..f920f45 --- /dev/null +++ b/操作系统/16_虚拟存储器/16_虚拟存储器.md @@ -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{"页面在内存中?
(检查状态位)"} + B -->|"在内存"| C["正常地址变换,访问数据"] + B -->|"不在内存"| D["触发**缺页中断**"] + D --> E["保存CPU现场"] + E --> F{"外存中存在该页?"} + F -->|"不存在"| G["终止进程
Segmentation Fault"] + F -->|"存在"| H{"内存有空闲页框?"} + H -->|"有空闲"| I["从外存读入该页"] + H -->|"无空闲"| J["执行**页面置换算法**
选择牺牲页"] + J --> K{"牺牲页Dirty=1?"} + K -->|"是"| L["将牺牲页写回外存"] + K -->|"否"| M["直接覆盖"] + L --> I + M --> I + I --> N["更新页表:
状态位=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
R=1"] + P1["页1
R=0"] + P2["页2
R=1"] + P3["页3
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["调度器增加进程数
(以为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["挂起某些进程
释放页框"] + 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_课程导航]] diff --git a/操作系统/17_IO系统/17_IO系统.md b/操作系统/17_IO系统/17_IO系统.md new file mode 100644 index 0000000..16f3951 --- /dev/null +++ b/操作系统/17_IO系统/17_IO系统.md @@ -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软件
(库函数: printf, scanf)"] + end + subgraph 内核空间 + B["设备独立性软件
(统一接口、缓冲、分配)"] + C["设备驱动程序
(控制具体设备)"] + D["中断处理程序
(响应设备中断)"] + 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
距离: 640"] -->|"改进"| B["SSTF
距离: 236"] + B -->|"解决饥饿"| C["SCAN
距离: 299"] + C -->|"优化尽头"| D["LOOK
距离: 299"] + C -->|"解决不均匀"| E["C-SCAN
距离: 350"] + E -->|"优化尽头"| F["C-LOOK
距离: 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["文件系统
(参考 [[04_文件IO编程]])"] + B --> D["存储管理
(参考 [[12_存储管理]])"] + B --> E["进程管理
(参考 [[01_系统运行机制]])"] + C --> F["磁盘管理
(参考 [[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_磁盘空间管理]] diff --git a/操作系统/18_程序代码优化/18_程序代码优化.md b/操作系统/18_程序代码优化/18_程序代码优化.md new file mode 100644 index 0000000..d7a5c81 --- /dev/null +++ b/操作系统/18_程序代码优化/18_程序代码优化.md @@ -0,0 +1,731 @@ +# 18. 程序代码优化 + +> **课程**: 操作系统 - 程序代码优化 +> **核心内容**: 机器无关优化、代码移动、消除不必要的内存引用、优化障碍(指针别名、函数副作用)、性能度量 + +--- + +## 前置知识 +- [[03_C语言编程基础]] -- C语言编译链接过程、指针与内存模型 +- [[13_存储管理基础]] -- 存储器层次结构、局部性原理 + +--- + +## 一、代码优化的概念与意义 + +### 1.1 为什么需要代码优化 + +> [!important] 核心观点 +> 常数因子也很重要!根据代码编写方式的不同,程序性能可能相差 **10倍** 以上。必须在多个层次上进行优化:算法、数据表示、过程调用和循环。 + +代码优化的目标: +- 理解程序如何被编译和执行 +- 学习如何度量程序性能并识别瓶颈 +- 在不破坏代码模块化和通用性的前提下提升性能 + +### 1.2 优化的层次 + +```mermaid +graph TB + A["算法优化
选择更高效的算法"] --> B["数据结构优化
选择合适的内存布局"] + B --> C["编译器优化
利用编译器选项"] + C --> D["源代码级优化
代码移动、消除冗余"] + D --> E["指令级优化
利用底层硬件特性"] + + 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{"循环体中有
不变量?"} + B -->|是| C["循环不变量外提"] + B -->|否| D{"循环迭代
次数少?"} + C --> D + D -->|是| E["循环展开"] + D -->|否| F{"多个循环
数据相关?"} + 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`?有什么潜在的代价? diff --git a/操作系统/实验/实验01_IO编程.md b/操作系统/实验/实验01_IO编程.md new file mode 100644 index 0000000..f8f9660 --- /dev/null +++ b/操作系统/实验/实验01_IO编程.md @@ -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 +#include +#include +#include +#include + +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 +#include +#include + +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 +#include +#include +#include +#include + +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 +#include +#include +#include + +#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 解决实际问题 diff --git a/操作系统/实验/实验02_进程控制.md b/操作系统/实验/实验02_进程控制.md new file mode 100644 index 0000000..dde10e4 --- /dev/null +++ b/操作系统/实验/实验02_进程控制.md @@ -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 +#include +#include +#include + +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 +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include + +// 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 `:向指定子进程发送 SIGTERM +- `ps`:列出所有存活的子进程 +- `exit`:终止所有子进程并退出 + +### 关键代码提示 + +```c +#include +#include +#include +#include +#include +#include + +#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. 使用信号机制进行进程间通信 diff --git a/操作系统/实验/实验03_多线程编程.md b/操作系统/实验/实验03_多线程编程.md new file mode 100644 index 0000000..553c221 --- /dev/null +++ b/操作系统/实验/实验03_多线程编程.md @@ -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 +#include +#include +#include +#include + +#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 +#include +#include +#include + +#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 +#include +#include +#include + +#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 +#include +#include +#include + +#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 +#include +#include +#include +#include +#include + +#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` 的开销差异 diff --git a/操作系统/实验/实验04_进程间通信.md b/操作系统/实验/实验04_进程间通信.md new file mode 100644 index 0000000..fea7483 --- /dev/null +++ b/操作系统/实验/实验04_进程间通信.md @@ -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 +#include +#include +#include +#include + +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 +#include +#include +#include +#include +#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 +#include +#include +#include +#include +#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 ` 手动删除 | + +--- + +## 任务三:task73.c —— 共享内存 + IPC 信号量同步 + +### 任务要求 + +使用共享内存实现两个进程间的数据传输,配合 System V 信号量实现同步。发送方写入 1~10,接收方依次读取并显示。 + +### 关键代码提示 + +```c +#include +#include +#include +#include +#include +#include +#include + +#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 ` | +| 删除信号量 | `ipcrm -s ` | + +### 常见问题 + +| 问题 | 原因 | 解决方法 | +|------|------|----------| +| 共享内存未清理 | 程序异常退出 | 用 `ipcs` 查看,`ipcrm` 手动删除 | +| 数据竞争 | 未使用信号量同步 | 确保发送方写完后才通知接收方读取 | +| `shmat` 返回 -1 | 权限不足或 key 冲突 | 检查权限,换用不同 key | + +--- + +## 任务四:task74s.c / task74c.c —— 多进程并发服务器(选做) + +### 任务要求 + +使用命名管道(FIFO)实现一个多进程并发服务器: + +- 服务器创建一个公共 FIFO 接收客户端请求 +- 每个客户端创建自己的私有 FIFO 用于接收回复 +- 服务器为每个请求 fork 一个子进程处理 + +### 关键代码提示 + +**服务器 task74s.c:** + +```c +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include + +#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 机制的优缺点和适用场景 diff --git a/操作系统/实验/实验05_网络通信.md b/操作系统/实验/实验05_网络通信.md new file mode 100644 index 0000000..de38973 --- /dev/null +++ b/操作系统/实验/实验05_网络通信.md @@ -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 + +... +``` + +### 常见问题 + +| 问题 | 原因 | 解决方法 | +|------|------|----------| +| 404 Not Found | 文件路径错误 | 检查 `DocumentRoot` 和请求路径 | +| 中文乱码 | Content-Type 缺少 charset | 添加 `Content-Type: text/html; charset=utf-8` | +| 浏览器无法访问 | 防火墙或端口未开放 | 关闭防火墙或开放对应端口 | + +--- + +## 任务三:task83s.c / task83c.c —— 文件下载 + +### 任务要求 + +实现一个简单的文件下载服务: + +- 客户端发送文件名请求 +- 服务器查找文件并返回文件内容 +- 客户端接收并保存到本地 + +### 关键代码提示 + +**服务器 task83s.c:** + +```c +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include + +#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 +#include +#include +#include +#include +#include +#include + +#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` 在网络编程中的应用 diff --git a/操作系统/实验/实验06_并发服务器.md b/操作系统/实验/实验06_并发服务器.md new file mode 100644 index 0000000..abdecb5 --- /dev/null +++ b/操作系统/实验/实验06_并发服务器.md @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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 +#include +#include +#include +#include +#include +#include +#include + +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 +#include +#include +#include + +#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 代理服务器的实现方法 diff --git a/操作系统/模板/课件笔记模板.md b/操作系统/模板/课件笔记模板.md new file mode 100644 index 0000000..c17d0f7 --- /dev/null +++ b/操作系统/模板/课件笔记模板.md @@ -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 语法正确 +- [ ] 代码示例完整,有编译运行命令 +- [ ] 输出结果正确,有关键点解释 +- [ ] 知识关联完整,有双向链接 +- [ ] 思考题有价值,能帮助理解概念 +- [ ] 扩展阅读有用,有推荐资源 diff --git a/操作系统/附录/附录A_Wrapper库参考.md b/操作系统/附录/附录A_Wrapper库参考.md new file mode 100644 index 0000000..f3a6433 --- /dev/null +++ b/操作系统/附录/附录A_Wrapper库参考.md @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +``` + +## 常量与类型定义 + +```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)` | 打印应用错误并退出 | diff --git a/操作系统/附录/附录B_术语表.md b/操作系统/附录/附录B_术语表.md new file mode 100644 index 0000000..c02a3db --- /dev/null +++ b/操作系统/附录/附录B_术语表.md @@ -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 | 可重入 | 函数可被中断后安全重新进入 | diff --git a/操作系统/附录/附录C_学习路径指南.md b/操作系统/附录/附录C_学习路径指南.md new file mode 100644 index 0000000..3f4547d --- /dev/null +++ b/操作系统/附录/附录C_学习路径指南.md @@ -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` | 内存泄漏检测 | diff --git a/软件需求分析/Pasted image 20260610205411.png b/软件需求分析/Pasted image 20260610205411.png new file mode 100644 index 0000000000000000000000000000000000000000..d7c01ab928fe83ed55523da9977ab971be7aa9c6 GIT binary patch literal 6113 zcmb_gdtB0I+ecfcxvi{bmZvRd%?vF~(?qQ1A<^ct5(N*H2UIjOG(?50)}~J&(?lf= zS`J+)kOwq8QQ6cKH4lJbYUKfqJPYQL7qPareLnB=K6{@(`0xSl-_3nr*Y~=v@AsSZ z-M3Dke!Tr-008i*^HF3`t_E1=_UvQ8 zp_nTk0KntTCF+2U)Z3o}62?DL9`O5J!K(pZfdAdnwwd|G!L=_zM)ZY^0G;@2=GW8Q zQ$v=Yq<9?hGGnA0KD3N8EKX5=bz9^A@jw6WNG`o&Gg$`Tm;I6kp|wc-z2rc8iM1aa z&A_vFG6fahy|2S;u2;!JWo5i?T(-S>P>%okfD!$x_T}rFF}~jsw^8VuJN9e3wJ%bU zTtM747h7V-z5rTe_;B+&@Ge=`7vMdZfW=4a0+eAJy5eg5bG`m_UfOSnj%I#`;P8m| zv5|5y2jpZ%rd^FT4PCr<3sKPchHPg8tMre6oY`-t3~AKiT=I7T8|I2LBEe=EPJ@bI zk>CPtRW$yyv~ZB{o`)ZUx=;?BIjlQK6xA!U@ocp+hAFl~ZlLR)Hf5g3)fq)>*Ozxr zT1KF%sG%rhy9`CuFLV-$T`n&n_#Ydd6Wa=!HLqHqwN&0L*l>$Zq_@-65?cm3KIm8) z(?}0xU6!|HV-H=x5_23Fyp-8JJ}niYXbs?GtP*BEKZO`>5nWyCFe56%HVWU&jt!NR z>OMWmUAtj7``Q*VlKIJH<*|!rZ>{z*nBU)1C-$HS zEK3XOAf{aAE|$5%CNfhesL#n)Gnx1x0@KuMs(v^6h1{s44B}CIs%6gmF11(!QP}v| z%!r2S?nH%;l*flbg;OTzny^ZJ0N~Kp0Qh!Z9Oe|Ysp1<(bGpT;^3h`S$lT3J z?~&(s(i}iCpy>@`esQ|cYj;+LS6aiQ$?w2bT@!G>#rJ90O8)=TGWmD#{b$AZ$F7yr zi+Jwr;TRX_@YHNPB;C*rF6cfb0J#;y%(BwlH3BIb=!QGg zcLQLhP*ZGAyBTF!H^drscOh@j_d%4u1D9)KC2k39_rb_o_8HxJkuinns_Y)`z+z1{ zOEQq&zPCT97NxIzW+g*0`qJBt@x8ze0YvG8H)LL|RPA^0*BWZP1&iK9{$-*v@2)*8Z7l=>n2f7-aLp0M8P(){=u_Fw(In`Rf`%gFm;GCogq{or63Zzo{> z<8Z*nTklZzFmTpX_a`jTl$H!PBcs6M~F*=C%!M%-%|p8DPP;-0$vC^uA9Z|^N77#wqz6UL-o3CK5(4A_KrSN{{+f253F zI-|R^_cH~O6<~?yJgnWBnH8U?Z_)1PjfgXUuJEEq>I7CoXXM+4mhfj#Jjhj2musA1 zyy^y~xx5l4jfXgAcPTZ3^8?zkQNJIB3{Q(A+_r26jo!Te_2LUsTRg$6HfMyK^EHEW zjZ?mI3fGj7CR#kRXR_55G!nb~n6a=C$2%9HkD!db4MlgiFUU$^$w4SR{$NhjF$P$% zQ&ki{G?zdozM8YwF;B9h()0Qpe!;5?!*Bv1z8N@5EhCR_yo3_bpdA~-(wW2&O+U7++9W602r~jEP2$+Ne*RVFBH(i@(;WlNU|VmD$|S9>*09z4B_(e4tYhJ zucTsdrboB0%%76@!@_`igxhr1^-3iD(=KiGw3)hVxJgXU0_2iSdQ$zn!j=U^KqwBv zRM1{LU>+_%!Ng9@4~=;4+(A(ii}JnRxw5IY_Ew#6M~?>e5&|*uqfS_tvCI6}os-le zv9{u-tF-q|*P9CG#@wVu>Z&CltbhsRBP~gDLFX?Pw8RuKdOn#2vINx3gj&a!#7dRXjtjXz(gGN5~vgP@2QAPMngR2xi;(T5+GEqLKSJ(mq zM>iuQKxM&RrdS)0u%kl|I`VuQCg`GeO*iUI$gCu?wQ=!{j5|#M`P|4j(>2cZenDtE z66X?f+4Roz#JQ@7C(1HM%Io3zI4x3WNJQaiU&@kM^>N`hBgKN6DtV8h{{6%=M;2RR zuD6l$h&OF-9}8J>sSB+`EKbgeIa+KfN|ISeklxPQGg#33lB*&@%4Tm)P42PqmnU!; z!bdE@6Mihpi}qV~m<5J;`N<+mLqrI(f}X-Nn3?k$^| zYWvYqi`uIApfQO|$zaOAXumlTHEkkVbpgXjg4?pu9CuM$p?KoCo|mM`Qrkr;ex-Ld zY5wU7w-ONYaxhQz@0Tut2SxQj)Gso06?;h#9wE#7<)kGmjv>GgcjkSUZk?&_{fUlEIT!PD&+xJD+hdTONoH$fs#0GHq^0c&s>!q z$RdRFUt;63Rv>TL9^1cu=%^@#UsQ-85)0QXACGQnz($v>B=bZNyk&_6adYVk_3CtJ ze6Yo?hbs-*38$dxs0 zZcX>=aJJP!6{szZ>rGQ_%;lU_xt$9E$JT>c4dT zpp)TBOXihSPxMbZW&z6&CKnYx{De75E{Qu^Knu>ntQ2CVtMp)vd=P|%mk%aQop>`T0XKJ;%twE+gyd6+KzRRk69JnuNS|0y#@MZnVDp>NK$DNMOPaIyU4*M)T z*W&SwXH@+;?PLA>Zd}uYg`Xqc|11XDUsZ(ERFFHo6<@9t2d~62e$%~)v<{}Q@Lu5S z*oq8&el7PmAM_Tw0f8l~GP|c=Tl^zrDODBbq22A``4f&~it-;nw;56%k*D^%Lxnu!_17P6ktn@j6BTf#X-$jg8()H_I7<yLk=F>nOborTk}A3&n$uX2z<`m(*F;U;#Tj! zmo!1isixSyW2ql89)`O^6_s_nS8eGsb85_1vwtsN&r?1@aRu98f6zOVlrwr)XAqZ% zKID51b9R3F*4VAl_zN9ViDIiKrkk{B7{uko4<7p8WpvwtdbhQH_yn0~RCiIW6WKMp zz+xB+h2DBU_B7!Yw)Xs0(g?pO$?n(o@vs{O(h!L|&Qv6ZTp;&;@@kfl`m8+(8hK`Q(g zUz;+z{N(YXQ#0k%N?r<{Pkj<9fhL!9RZvEww>SySBWD+6z)5etMMzKk1(I~a%G{C- zF_m>R?1489;*4dSs4!gRPHyh0yG`kGEo4{t3buWpmXrnsK{XZWIe~}<&?vsas%t3) z6>HI6mHcWbcBcuAi=VcFu^~dGHfHAELZmhH*G+OG^tw_rY&V^V|F*O>_FPKF-4Q8P zl(zzACv%2$s8Z{$L^8?E08jL%Dsa+Hs(7>OQ;MZ(-zSx_v7zcG6|oCEcD!3zPfN-? ziyS`OuF19(6x#cmm=;Ixu@l~jTD;@ohWBCExy39v`Rd&wv~g_dzn$;!otHZI>^^Y< z@s$Nq{JN>9b92_Llw>VEL7MWAqLun@y1Q!<2j87#kk9Vg!9ue9uyM08M4ddF6U2zZ z4SP0nDzGYA0Zt78-F1+#(Fi#;v@|;;N47q|rTj}B@ICWBXYk-Y2)d)JehMR>k7-Pa ztDIXDi)Vo0{b;_p_70s zPwjC;bXiXy0X}#Q?x<0|g4;qYOGvOnw{C?Z}{3nqmz| z$u*}`W%g3$D&0YP(X!iNiC*qfC#VxjN*kEnr-Cll27_mBRB~{>A$~a#wj*SEb$T+W zZpxn%?`D0^L(y7}iUvVv#BI%#a^aV9`#HBYSb^}VJi>y}8C*e7K2^qX zU!Je&MHn*W*1qjDRkg$VIo)%<&7_QY*tW;A!smj;yd(06{4nR?xzwC!CYfpN-kaxM z9aR|>)>zKM&Q?-oo&ys6%LzY|`+~Nx;qsPdHfWXaL0B=Ll(RHXD}imP!+?5hys zgUP%l5_>$OO&dpOkJ>RtMxdq(z*(3d8Id;PS`S-8C#i4$j9+7#N$4URd96Le%yc1p8me{@_l#c z?9Ywv(6SChex4F*6|dE_S48)FpU=hPX;F3^-}P(=qeIY2LBr-BqzipgXMpsMD7~qA z;W~l|J#9I1>)cdkm#0nEnF2;mvE%}~h>f;jOG71@B-z7vHi}A8^7=Byk_3WQkrGZz zWgS!2qH``@wSjtj-y0V(=jgMhdI*$nL`w=W8Gz4KohlV*^@Vu( z@7=WA``&qGnQovHbBp)`!Y6_WTO_lw_$DknWTxJN_egyD&*>RFnKh%d?bxVhY9qfv z(5x2;vnAC1Wz9t6j6F&^^oHYb*LB!)V8Ns-dKN`co{9Bb<=U zC*m^u2g=Yi9oG*qH*=QI!ACBc;lYcC_0)WK6o&WIrt`;qpX77DKxJs z_O#`DCaCly_maTqIcuvisdP#TjW&qH;80jCnF*G*E$c(Mz04QiL+%3~;UBoUe}}|> z!z2Fh>^eNZh@ZMTo}T88UzhM{^<#NW!5DZJ0tXYc3OW=c|kQ8A~y8+c~+_(TKJo zhH`3bY`JYU_m~yUn9G{E{5~V+^!@t*`a@TnIFq;%KD^5n&EcW&>OhXq=hGQWh3FXe^Awvdn5t@*-(iad)_alZ3Tf+w&;Q4gCu1@o9!Dwpf~H3KqrhO)_`O+JV2m9 z^Tpk$>S%K;VvF9vUJEtl*T5CZuQOXXQ^8Eh&vHsMV9fvSKB*0$Jvs3bpxayj_2F?z zUIvScCu+gjMfG5a{gO8?vKkx{du7TwtP)dwgaS|RC_x6QihM6v9ZmoqaX3L=8L3U~ z8mBNv2{ExZVpWPin>_#M4vQ_*!G-!$;L{X4Vj8`lGJDro@}1YsLzDjSES(hgG$!90 zkHMuwwnpd&!e1VZX&zDO-Nfi9LhbiBZ&kdd3>m!G#lXq=k2CclhvSU>b{;3pK*reA zMV6t+*V$@H9+(?V_MKM`;g1r6Uts5+dYHo$1j(nyQKuQroQ!Vj*yR$R_U_nraAnaY zY+gky&Va)HU~OuU6|b+HO9_VQywk2{)rC50r<2PG?$xezjCT~6jW5zoj)1YMT@z>g z;qs|wdnJ|^6;*=?Cm6z$56o{Nx#I$k!mQ*Tf+M1sfjDQKDXy8!H7tQ6^HqI$?8RZP zHM=Y4b6{?+5HNd~=!%-+7PI}w;My-;!&)Pc6AX+?kpZWqBs>jF(uXK=Zn_7C72i!8 zGY^%7{R0XcUsFdBDr3-ng&D{t zHV4B%5Egv9I87BeE@!HJngl=X7NNE0`)q}0f=l?H>aB68K7F^E8)Mh{Uu80j%06C( zlMsDQkD(aSMan=dU51(b6|3yWQs8>ZGN>77tL)V?Vu0aH4g%yqo+(P$lGgXo)O;HX?xFP}$l9Lujk9%8>eSRImUZ3{}Xp<5jOCjuiW`I}cJxK$^O8>|-Q&H^<@?gwmUjEIN2)mv+5!$#p|36IhFi zv?^%Di(q<;gX3N3}-V*0u~fLqXO6eY*%hH0Eyt zJ&|3mJkPyfCanYUlAyh^O1c-#XPef%<@bwzdLso=>m668dv(g4&8Lu@c04{CDH6O} zSWD~xK%lTp@RNJkYUug4M8N@2)uV3ny;l+vC)z!4Kjm9#?vBzr8H&DfVW1i)`Rh_c zG8BdTtf#kAK=B&OC0QA3S@9Ek{p`h=0JN3W@i`ajivuq{%Nf1quCXfJW=PRJ2dmah z`^P8wnd(>a4Ku5t9CuVxsz!M2LB7=?jE0P|g`Pn@H_;=RL_aesV-pFwGpxy%Z}6f(*Eqiy*DG<47;Wg}2MJ8K6^zdk2k z%jM@BvMBb0q&>F6wf1PJUR+ z&kw~5*rTqd)V6PO2H!wva14~kw3RaBY5J8>I5z4#*}y|_YKnrsV3^x`rYetx zz!@-&Ds(tcRYLCV1H=H*+ubJ@0wY1mHLhjzIYqdGo;dd_IUHv2o&4Hc-}Mt73Kf<& zTT92PltIb(UFlELdw1UXR{D*LW*8Dtw}W)Zx5QNCTM{J<9dtJ#KMzyj`K3SV246o2 zSJUPAK1P*~M8~WBDOJQ*m(8!0->53lMMH!UID{ywfUBwvb6B|lQm`w5AbymlN1I}h zB>Jr3x3BZ28$o(ISLpi2!~(DERP0jJ;kwy1pzyV7uG(Hbg#px7*7~XxfG|;FDnfbW z{A&1nx$BXNU;$B*_M5H;-g+(S=Ab>#vLMdljAppj8o}veNv%0a`-u3lcv?V|8Uh~c3r63uF6vZn{K5bO|f(8$q#VfR&u5a?K zlvhfH#^3vuFGDiK-9a-d8eY!KN!}Y!5pONrTF=3kIVH_vSs2pzmAJL8B(y+##`8T7OrrM*wwftpY zt+L~_i|MELP62byPsV%o$8ok-7#IX46g%L&^QAj4+G`glXal#&H&N2+S=V6c@l>XJ z54D0n(_VYo4MB*+T?zDiE|PM<33B~9LY^-KRHRH{+TN(97Ca(^y@@?G&#ZffVma7W zpRdcs_s(lTH-YXcZr1~W9zKXqwbG?1Bp_nJMa8?|Pm|523*hSP&xww5PiAN_*M?(c zCpIMZy*{hrFZ6w)RZ;s{fTw&bB)G#OshFfcSOz>VYPp9gcc(*k%5QGU+Fnv?KzoqB zQu&=bv|X~()T}pAUZr6n3?0Em7+PB7H>4D}q~qQiLGk@fLao`M2dWXQdWg@a%rG$3 z>IGWqv@vcjxQu&mYIe%^2WJRw?vmLA~NQxVzVsr+xhc+4LaY zj)m_M>&%E4daj>kt+?_9hLVoI<1&hv449omLPTIC*=_y7) zAt`pSkF4pViXzgOI?fx$Mr>M^RKUc!jx~Kg8QTLUmm88oC+RbzU64S(8;2{ei*k6( z;9Gx*6H;5?)YqGVonliIP&kS~h_}#>)$XF(zj!Q)^JIis7o5z}PdvMv?U*HQz8%3| zS|OgT6(qeE@3Q!D+Kn^2G*-4q!=s_OuAgUt-WG{`NQi(~{R~KSW;zkO)LB%CS?kiM z_OMxLJBfku_=SRoj2SVCn5s4Sk#uYazV8*JlQOsiH?-by*|U$A?DGEXr$S~;8yx8* zcF4)~@6Oj|8^W*|lt|YSGyBbd8qz9Qbq1s91T3=MB!%z=Vzrs5l}%+*$9^fV8 z%@df;h?D`N@8$HuXp7Fni!{{Fr&q}F`!G0_XZMFp7pmuZ^FpAUCeatU#q>Jee3ti3 zA<`2d1X&*`PlIuA9*pk$#}83!X~*$5daved6vQx!$kVuaE@Dr0$Z_Fm=tNmtKcS7? zpS9c~)ZVu-&g$DbZh`9SU@PceWX-QEE+r z^m{mO82Rak^aWRAUAKIUbIJqY8uXV0JW1!4i_s1kGjTt0Q&a779%|7Mg$R`^zTUv6@ z9g!H?K^s(JO&Y;JaKv5$c)rT~NK@#%)w&-<%AKEs%6NFqmkf(AJ4JomZ@X{;3$!bs z_kcUWv9~xzoQYu83;J^0T2VHSwe^Q0RO8N`MH5xfygSak&wJw0KhT3u86iwciL4T6MQS{ld`J-LU z__{r)RHV`OU3k%lA)ez*ve^ACvN9Xe#&7iWPP5gvn_M6FvaNDKU7uf$wGoyEk8yrE zE^N0}6#2=GTi)$qRatev>V9%0e}!v#oWDN1buoaF#9d|Y1Y#5MzU(0fpr^8*pLDub zOyVy*4+#eSX0Uha2B2z<4Ja;mxm3i#GxG^HVA$lqRJ9D5t?V{-^L$~S)a3c5fJeVUm2G z(c(aHygwgOhJr?t>xs#I{K7urrEKSMYh#ONII2D?ZKXYuqS>vt)RXOeq2|{mC(W{L zGO=(ACYX4XR8)B*5a=A$7afmew>cA!SuTsMaX{;xvcel81CLIf7k*%&F|*_XTeOSPmx&v@)8Xse{!BnX2ayYJ#JlrKNyrTMSQ=quW;BcGX-A^4VX^SC zqk+#_=z?d*&@mLlJ1cF(;7G;cVP{B&V%t%o7G$c#o+{0;(bh|YcVv;xy|YgpL7xTU z!ZK;9+w*i@9n)n(N{^Lwg2Q-|eHHHTBkDLvc1A(!eSSZIt_>S&sweL9{BT;#RqC`X zxdHyrUKg2L?vHT@iYHi9dxXJ*mlbLi6G_o={#4!Mct@S=>5+1JE(?D-qc5U6#EZl? zbHALnby5Xvn05hz#1TY4ooUECFI2x)e-z4qa&v*U1cJn;&P3*3053U5cym~y98C@z zOO5bjIX$z6e=GpzSL)xwAQzL7+hXG7#N>d#$ zohf^t5s^(Yt_S5GPFJ~EZ}Tfx-@#SEyHACRPdr^AeCQu5qr-7LorT|)8Q2Q*%rF_Q}>f1 zzJ?SVa&wdFN58==z#PEry(^1=vHQC(N3=zTTHIRIfohXBNkLF?-LJs1^v0Sa9-udL z7R*pX`F?Ce%3K;&;1AcuR$9RqvYtlr0BDblZk4Y}ESXEtR&39LhK{@a5kguhYb}vZ zW7Rq-U!?d6YgyCnEH);Ynlb?JW-*|WOY>sGfOgUxn-U(;rNBlvUtwO%&{*weXB@EJ zz^|D-vi^Ndk^_}v0UN=U`=~7L_W%WD%_m?J?_D-(;$$e!|8(9Tt?rMF;?HSmuCYH@ z#=*=)KA}`6h=Dx?+4soiph{-9iYVb`WgWp?7|E_@rd@)R?lL~W;gyFii6_Hk)wurz z=$JD$sNuM4mtvaYIF7%$+?mC5H&#&mR#=`4?Jk3BLW$42-3eUFEqePyhLa>~69HSX zT>7gc{yUuin