vault backup: 2026-07-03 23:22:28

This commit is contained in:
2026-07-03 23:22:28 +08:00
parent 8197fd341e
commit 3aac80fb26
41 changed files with 11906 additions and 10335 deletions

View File

@@ -1,353 +1,353 @@
# 第02讲Linux操作系统目录结构与文件操作 # 第02讲Linux操作系统目录结构与文件操作
> **本节目标**理解Linux系统的起源与设计哲学掌握目录组织方式和常用文件操作命令为后续系统编程打下基础 > **本节目标**理解Linux系统的起源与设计哲学掌握目录组织方式和常用文件操作命令为后续系统编程打下基础
## 前置知识 ## 前置知识
- [[01_系统运行机制]] -- 对操作系统的基本认识 - [[01_系统运行机制]] -- 对操作系统的基本认识
- 计算机基本操作能力 - 计算机基本操作能力
--- ---
## 一、UNIX/Linux系统简介 ## 一、UNIX/Linux系统简介
### 1.1 UNIX的诞生 ### 1.1 UNIX的诞生
1969年贝尔实验室Bell Labs**Ken Thompson****Dennis Ritchie** 在一台废弃的PDP-7小型机上开发了UNIX操作系统。此后Ritchie还创造了C语言并用C语言重写了UNIX使UNIX成为第一个用高级语言编写的操作系统极大地提高了可移植性。 1969年贝尔实验室Bell Labs**Ken Thompson****Dennis Ritchie** 在一台废弃的PDP-7小型机上开发了UNIX操作系统。此后Ritchie还创造了C语言并用C语言重写了UNIX使UNIX成为第一个用高级语言编写的操作系统极大地提高了可移植性。
```mermaid ```mermaid
graph LR graph LR
A["1969 Ken Thompson A["1969 Ken Thompson
Dennis Ritchie"] -->|在PDP-7上开发| B[UNIX初版] Dennis Ritchie"] -->|在PDP-7上开发| B[UNIX初版]
B -->|用C语言重写| C[UNIX V6/V7] B -->|用C语言重写| C[UNIX V6/V7]
C -->|研究方向| D[BSD UNIX] C -->|研究方向| D[BSD UNIX]
C -->|商业方向| E[System V] C -->|商业方向| E[System V]
D --> F[FreeBSD / NetBSD] D --> F[FreeBSD / NetBSD]
E --> G[AIX / Solaris / HP-UX] E --> G[AIX / Solaris / HP-UX]
style A fill:#fff3e0 style A fill:#fff3e0
style C fill:#e1f5fe style C fill:#e1f5fe
``` ```
### 1.2 UNIX两大流派 ### 1.2 UNIX两大流派
| 流派 | 代表 | 特点 | | 流派 | 代表 | 特点 |
|------|------|------| |------|------|------|
| **BSD UNIX**(研究版) | BSD 4.x | 由加州大学伯克利分校维护,偏重学术研究 | | **BSD UNIX**(研究版) | BSD 4.x | 由加州大学伯克利分校维护,偏重学术研究 |
| **System V**(商业版) | SVR4 | 由AT&T维护偏重商业应用 | | **System V**(商业版) | SVR4 | 由AT&T维护偏重商业应用 |
### 1.3 主要商业UNIX版本 ### 1.3 主要商业UNIX版本
| 版本 | 厂商 | 典型硬件平台 | | 版本 | 厂商 | 典型硬件平台 |
|------|------|-------------| |------|------|-------------|
| **AIX** | IBM | Power/PowerPC | | **AIX** | IBM | Power/PowerPC |
| **Solaris** | Sun Microsystems | SPARC / x86 | | **Solaris** | Sun Microsystems | SPARC / x86 |
| **HP-UX** | HP | PA-RISC / Itanium | | **HP-UX** | HP | PA-RISC / Itanium |
| **IRIX** | SGI | MIPS | | **IRIX** | SGI | MIPS |
--- ---
## 二、Linux系统 ## 二、Linux系统
### 2.1 Linux的诞生 ### 2.1 Linux的诞生
1991年芬兰赫尔辛基大学的学生 **林纳斯·托瓦兹Linus Torvalds** 开发了Linux内核。最初这只是个人项目但由于他将内核源码以GPLGNU General Public License协议发布全球开发者得以自由修改和分发Linux迅速成长为最重要的开源操作系统。 1991年芬兰赫尔辛基大学的学生 **林纳斯·托瓦兹Linus Torvalds** 开发了Linux内核。最初这只是个人项目但由于他将内核源码以GPLGNU General Public License协议发布全球开发者得以自由修改和分发Linux迅速成长为最重要的开源操作系统。
### 2.2 内核版本号 ### 2.2 内核版本号
Linux内核版本号的格式为 **`r.x.y`** Linux内核版本号的格式为 **`r.x.y`**
- **r**(主版本号):内核有重大变更时递增 - **r**(主版本号):内核有重大变更时递增
- **x**(次版本号):偶数表示稳定版,奇数表示开发版 - **x**(次版本号):偶数表示稳定版,奇数表示开发版
- **y**修订版本号bug修复和小改动 - **y**修订版本号bug修复和小改动
常见内核版本线2.4 -> 2.6 -> 3.2 -> 4.6.4 -> 5.x -> 6.x 常见内核版本线2.4 -> 2.6 -> 3.2 -> 4.6.4 -> 5.x -> 6.x
> **注意**从3.0开始,主版本号不再有"奇偶"含义,版本号只是简单的递增计数器。 > **注意**从3.0开始,主版本号不再有"奇偶"含义,版本号只是简单的递增计数器。
### 2.3 Linux发行版 ### 2.3 Linux发行版
内核本身只是操作系统的核心部分。**发行版Distribution** 将内核与各种工具、桌面环境、软件包管理器等打包在一起,形成完整的操作系统。 内核本身只是操作系统的核心部分。**发行版Distribution** 将内核与各种工具、桌面环境、软件包管理器等打包在一起,形成完整的操作系统。
```mermaid ```mermaid
graph TB graph TB
A[Linux内核] --> B[GNU工具集] A[Linux内核] --> B[GNU工具集]
B --> C[发行版] B --> C[发行版]
C --> D[RHEL] C --> D[RHEL]
C --> E[Fedora] C --> E[Fedora]
C --> F[Ubuntu] C --> F[Ubuntu]
C --> G[CentOS] C --> G[CentOS]
C --> H[中标麒麟] C --> H[中标麒麟]
style A fill:#ffcdd2 style A fill:#ffcdd2
style C fill:#e1f5fe style C fill:#e1f5fe
``` ```
| 发行版 | 特点 | 典型用途 | | 发行版 | 特点 | 典型用途 |
|--------|------|---------| |--------|------|---------|
| **RHEL**Red Hat Enterprise Linux | 商业支持,稳定 | 企业服务器 | | **RHEL**Red Hat Enterprise Linux | 商业支持,稳定 | 企业服务器 |
| **Fedora** | 技术前沿,更新快 | 桌面 / 开发 | | **Fedora** | 技术前沿,更新快 | 桌面 / 开发 |
| **Ubuntu** | 用户友好,社区活跃 | 桌面 / 云服务器 | | **Ubuntu** | 用户友好,社区活跃 | 桌面 / 云服务器 |
| **CentOS** | RHEL的免费克隆版 | 企业服务器 | | **CentOS** | RHEL的免费克隆版 | 企业服务器 |
| **中标麒麟** | 国产化,政府认证 | 国内政府 / 国防 | | **中标麒麟** | 国产化,政府认证 | 国内政府 / 国防 |
--- ---
## 三、Linux目录结构 ## 三、Linux目录结构
### 3.1 核心概念:一切皆文件 ### 3.1 核心概念:一切皆文件
与Windows不同Linux **没有盘符**C:、D:等)的概念。整个文件系统从根目录 **`/`** 开始,形成一棵倒置的树。 与Windows不同Linux **没有盘符**C:、D:等)的概念。整个文件系统从根目录 **`/`** 开始,形成一棵倒置的树。
```mermaid ```mermaid
graph TB graph TB
ROOT["/ (根目录)"] --> BIN["/bin ROOT["/ (根目录)"] --> BIN["/bin
基本命令"] 基本命令"]
ROOT --> SBIN["/sbin ROOT --> SBIN["/sbin
系统管理命令"] 系统管理命令"]
ROOT --> ETC["/etc ROOT --> ETC["/etc
配置文件"] 配置文件"]
ROOT --> HOME["/home ROOT --> HOME["/home
用户家目录"] 用户家目录"]
ROOT --> VAR["/var ROOT --> VAR["/var
可变数据"] 可变数据"]
ROOT --> USR["/usr ROOT --> USR["/usr
用户程序"] 用户程序"]
ROOT --> TMP["/tmp ROOT --> TMP["/tmp
临时文件"] 临时文件"]
ROOT --> DEV["/dev ROOT --> DEV["/dev
设备文件"] 设备文件"]
ROOT --> PROC["/proc ROOT --> PROC["/proc
进程信息"] 进程信息"]
ROOT --> BOOT["/boot ROOT --> BOOT["/boot
内核与启动"] 内核与启动"]
ROOT --> LIB["/lib ROOT --> LIB["/lib
共享库"] 共享库"]
ROOT --> MNT["/mnt ROOT --> MNT["/mnt
挂载点"] 挂载点"]
ROOT --> OPT["/opt ROOT --> OPT["/opt
第三方软件"] 第三方软件"]
ROOT --> ROOT2["/root ROOT --> ROOT2["/root
root用户家目录"] root用户家目录"]
style ROOT fill:#ffcdd2 style ROOT fill:#ffcdd2
style ETC fill:#fff3e0 style ETC fill:#fff3e0
style HOME fill:#e8f5e9 style HOME fill:#e8f5e9
style PROC fill:#e1f5fe style PROC fill:#e1f5fe
style DEV fill:#f3e5f5 style DEV fill:#f3e5f5
``` ```
### 3.2 重要目录详解 ### 3.2 重要目录详解
| 目录 | 全称 | 作用 | 举例 | | 目录 | 全称 | 作用 | 举例 |
|------|------|------|------| |------|------|------|------|
| `/bin` | Binaries | 基本用户命令 | `ls`, `cp`, `cat` | | `/bin` | Binaries | 基本用户命令 | `ls`, `cp`, `cat` |
| `/sbin` | System Binaries | 系统管理命令 | `fdisk`, `ifconfig` | | `/sbin` | System Binaries | 系统管理命令 | `fdisk`, `ifconfig` |
| `/etc` | Editable Text Config | 系统配置文件 | `/etc/passwd`, `/etc/fstab` | | `/etc` | Editable Text Config | 系统配置文件 | `/etc/passwd`, `/etc/fstab` |
| `/home` | Home | 普通用户家目录 | `/home/zhangsan` | | `/home` | Home | 普通用户家目录 | `/home/zhangsan` |
| `/root` | Root Home | root用户的家目录 | -- | | `/root` | Root Home | root用户的家目录 | -- |
| `/var` | Variable | 可变数据(日志、缓存等) | `/var/log/messages` | | `/var` | Variable | 可变数据(日志、缓存等) | `/var/log/messages` |
| `/usr` | Unix System Resources | 用户程序和数据 | `/usr/bin`, `/usr/lib` | | `/usr` | Unix System Resources | 用户程序和数据 | `/usr/bin`, `/usr/lib` |
| `/tmp` | Temporary | 临时文件,重启后可能清除 | -- | | `/tmp` | Temporary | 临时文件,重启后可能清除 | -- |
| `/dev` | Device | 设备文件 | `/dev/sda`(硬盘) | | `/dev` | Device | 设备文件 | `/dev/sda`(硬盘) |
| `/proc` | Process | 虚拟文件系统,内核运行信息 | `/proc/cpuinfo` | | `/proc` | Process | 虚拟文件系统,内核运行信息 | `/proc/cpuinfo` |
| `/boot` | Boot | 启动相关文件(内核镜像等) | `vmlinuz-xxx` | | `/boot` | Boot | 启动相关文件(内核镜像等) | `vmlinuz-xxx` |
| `/lib` | Library | 共享库文件 | `libc.so.6` | | `/lib` | Library | 共享库文件 | `libc.so.6` |
| `/mnt` | Mount | 临时挂载点 | U盘、网络存储 | | `/mnt` | Mount | 临时挂载点 | U盘、网络存储 |
### 3.3 /proc文件系统 ### 3.3 /proc文件系统
`/proc` 是一个 **虚拟文件系统** -- 它不占用磁盘空间,而是内核在内存中动态生成的。通过读取 `/proc` 下的文件,可以实时查看系统和进程信息。 `/proc` 是一个 **虚拟文件系统** -- 它不占用磁盘空间,而是内核在内存中动态生成的。通过读取 `/proc` 下的文件,可以实时查看系统和进程信息。
```bash ```bash
# 查看CPU信息 # 查看CPU信息
cat /proc/cpuinfo cat /proc/cpuinfo
# 查看内存信息 # 查看内存信息
cat /proc/meminfo cat /proc/meminfo
# 查看当前进程信息PID=1的进程 # 查看当前进程信息PID=1的进程
cat /proc/1/status cat /proc/1/status
``` ```
--- ---
## 四、目录操作命令 ## 四、目录操作命令
### 4.1 特殊目录符号 ### 4.1 特殊目录符号
| 符号 | 含义 | 示例 | | 符号 | 含义 | 示例 |
|------|------|------| |------|------|------|
| `.` | 当前目录 | `./program`(运行当前目录下的程序) | | `.` | 当前目录 | `./program`(运行当前目录下的程序) |
| `..` | 上一级目录 | `cd ..`(回到上级目录) | | `..` | 上一级目录 | `cd ..`(回到上级目录) |
| `~` | 当前用户的家目录 | `cd ~`(回到家目录) | | `~` | 当前用户的家目录 | `cd ~`(回到家目录) |
| `-` | 上一次所在的目录 | `cd -`(在两个目录间来回切换) | | `-` | 上一次所在的目录 | `cd -`(在两个目录间来回切换) |
### 4.2 常用目录操作命令 ### 4.2 常用目录操作命令
```bash ```bash
# pwd - 显示当前工作目录Print Working Directory # pwd - 显示当前工作目录Print Working Directory
pwd pwd
# 输出:/home/zhangsan # 输出:/home/zhangsan
# cd - 切换目录Change Directory # cd - 切换目录Change Directory
cd /usr/local # 绝对路径切换 cd /usr/local # 绝对路径切换
cd .. # 回到上级目录 cd .. # 回到上级目录
cd ~ # 回到家目录(等同于 cd $HOME cd ~ # 回到家目录(等同于 cd $HOME
cd - # 回到上一次的目录 cd - # 回到上一次的目录
# mkdir - 创建目录Make Directory # mkdir - 创建目录Make Directory
mkdir mydir # 创建单个目录 mkdir mydir # 创建单个目录
mkdir -p project/src/main # 递归创建多级目录 mkdir -p project/src/main # 递归创建多级目录
# rmdir - 删除空目录Remove Directory # rmdir - 删除空目录Remove Directory
rmdir mydir # 只能删除空目录 rmdir mydir # 只能删除空目录
# rm - 删除文件或目录 # rm - 删除文件或目录
rm file.txt # 删除文件 rm file.txt # 删除文件
rm -r mydir # 递归删除目录及其内容 rm -r mydir # 递归删除目录及其内容
rm -ri mydir # 递归删除,每个文件都询问确认 rm -ri mydir # 递归删除,每个文件都询问确认
``` ```
--- ---
## 五、文件操作命令 ## 五、文件操作命令
### 5.1 查看文件内容 ### 5.1 查看文件内容
```bash ```bash
# ls - 列出目录内容List # ls - 列出目录内容List
ls # 列出当前目录下的文件 ls # 列出当前目录下的文件
ls -l # 长格式显示(含权限、大小、时间) ls -l # 长格式显示(含权限、大小、时间)
ls -a # 显示隐藏文件(以 . 开头的文件) ls -a # 显示隐藏文件(以 . 开头的文件)
ls -lh # 长格式 + 人类可读的文件大小 ls -lh # 长格式 + 人类可读的文件大小
# cat - 连接并显示文件内容 # cat - 连接并显示文件内容
cat file.txt # 显示文件全部内容 cat file.txt # 显示文件全部内容
cat -n file.txt # 显示时带行号 cat -n file.txt # 显示时带行号
# more / less - 分页查看 # more / less - 分页查看
more file.txt # 空格翻页q 退出 more file.txt # 空格翻页q 退出
less file.txt # 更强大的分页器,支持搜索 less file.txt # 更强大的分页器,支持搜索
``` ```
### 5.2 复制、移动和重命名 ### 5.2 复制、移动和重命名
```bash ```bash
# cp - 复制文件或目录Copy # cp - 复制文件或目录Copy
cp file1.txt file2.txt # 复制文件 cp file1.txt file2.txt # 复制文件
cp -r dir1/ dir2/ # 递归复制目录 cp -r dir1/ dir2/ # 递归复制目录
cp -i file1.txt file2.txt # 覆盖前询问 cp -i file1.txt file2.txt # 覆盖前询问
# mv - 移动/重命名Move # mv - 移动/重命名Move
mv file.txt /tmp/ # 移动文件到 /tmp mv file.txt /tmp/ # 移动文件到 /tmp
mv oldname.txt newname.txt # 重命名 mv oldname.txt newname.txt # 重命名
``` ```
### 5.3 文件权限管理 ### 5.3 文件权限管理
每个文件都有三组权限,分别针对三类用户: 每个文件都有三组权限,分别针对三类用户:
| 权限位 | 含义 | 对文件的效果 | 对目录的效果 | | 权限位 | 含义 | 对文件的效果 | 对目录的效果 |
|--------|------|-------------|-------------| |--------|------|-------------|-------------|
| **r**(读) | Read | 可以查看文件内容 | 可以列出目录内容 | | **r**(读) | Read | 可以查看文件内容 | 可以列出目录内容 |
| **w**(写) | Write | 可以修改文件内容 | 可以在目录中创建/删除文件 | | **w**(写) | Write | 可以修改文件内容 | 可以在目录中创建/删除文件 |
| **x**(执行) | Execute | 可以运行文件 | 可以进入目录(`cd` | | **x**(执行) | Execute | 可以运行文件 | 可以进入目录(`cd` |
三类用户: 三类用户:
- **u**user/所有者):文件的拥有者 - **u**user/所有者):文件的拥有者
- **g**group/所属组):与文件同组的用户 - **g**group/所属组):与文件同组的用户
- **o**others/其他人):其余所有用户 - **o**others/其他人):其余所有用户
```bash ```bash
# chmod - 修改文件权限Change Mode # chmod - 修改文件权限Change Mode
chmod u+x program.sh # 给所有者添加执行权限 chmod u+x program.sh # 给所有者添加执行权限
chmod g-w file.txt # 去掉组的写权限 chmod g-w file.txt # 去掉组的写权限
chmod 755 program.sh # 数字表示法rwxr-xr-x chmod 755 program.sh # 数字表示法rwxr-xr-x
chmod 644 data.txt # 数字表示法rw-r--r-- chmod 644 data.txt # 数字表示法rw-r--r--
# chown - 修改文件所有者Change Owner # chown - 修改文件所有者Change Owner
chown zhangsan file.txt # 修改所有者 chown zhangsan file.txt # 修改所有者
chown zhangsan:staff file.txt # 同时修改所有者和所属组 chown zhangsan:staff file.txt # 同时修改所有者和所属组
chown -R zhangsan:staff mydir/ # 递归修改目录 chown -R zhangsan:staff mydir/ # 递归修改目录
``` ```
**权限的数字表示法** **权限的数字表示法**
| 数字 | 二进制 | 权限 | | 数字 | 二进制 | 权限 |
|------|--------|------| |------|--------|------|
| 7 | 111 | rwx | | 7 | 111 | rwx |
| 6 | 110 | rw- | | 6 | 110 | rw- |
| 5 | 101 | r-x | | 5 | 101 | r-x |
| 4 | 100 | r-- | | 4 | 100 | r-- |
| 0 | 000 | --- | | 0 | 000 | --- |
因此 `chmod 755` 等价于 `rwxr-xr-x`:所有者可读可写可执行,组和其他人可读可执行。 因此 `chmod 755` 等价于 `rwxr-xr-x`:所有者可读可写可执行,组和其他人可读可执行。
--- ---
## 六、绝对路径与相对路径 ## 六、绝对路径与相对路径
### 6.1 概念对比 ### 6.1 概念对比
| 路径类型 | 起点 | 示例 | 特点 | | 路径类型 | 起点 | 示例 | 特点 |
|----------|------|------|------| |----------|------|------|------|
| **绝对路径** | 根目录 `/` | `/home/zhangsan/file.txt` | 完整路径,从根开始,与当前位置无关 | | **绝对路径** | 根目录 `/` | `/home/zhangsan/file.txt` | 完整路径,从根开始,与当前位置无关 |
| **相对路径** | 当前目录 | `./file.txt``../dir/file.txt` | 相对于当前位置,更短更灵活 | | **相对路径** | 当前目录 | `./file.txt``../dir/file.txt` | 相对于当前位置,更短更灵活 |
### 6.2 示例 ### 6.2 示例
假设当前目录为 `/home/zhangsan/project` 假设当前目录为 `/home/zhangsan/project`
```bash ```bash
# 绝对路径方式 # 绝对路径方式
cat /home/zhangsan/project/src/main.c cat /home/zhangsan/project/src/main.c
# 相对路径方式(效果相同) # 相对路径方式(效果相同)
cat ./src/main.c cat ./src/main.c
cat src/main.c cat src/main.c
# 使用 .. 回到上级 # 使用 .. 回到上级
cat ../README.md # 访问上级目录中的 README.md cat ../README.md # 访问上级目录中的 README.md
``` ```
```mermaid ```mermaid
graph TB graph TB
HOME["/home"] --> ZS["zhangsan"] HOME["/home"] --> ZS["zhangsan"]
ZS --> PROJ["project"] ZS --> PROJ["project"]
PROJ --> SRC["src"] PROJ --> SRC["src"]
SRC --> MAIN["main.c"] SRC --> MAIN["main.c"]
ZS --> README["README.md"] ZS --> README["README.md"]
PROJ -.->|"cd .."| ZS PROJ -.->|"cd .."| ZS
PROJ -.->|"cd ./src"| SRC PROJ -.->|"cd ./src"| SRC
PROJ -.->|"cat ../README.md"| README PROJ -.->|"cat ../README.md"| README
style PROJ fill:#fff3e0 style PROJ fill:#fff3e0
style MAIN fill:#e8f5e9 style MAIN fill:#e8f5e9
``` ```
--- ---
## 七、知识关联 ## 七、知识关联
- Linux的"一切皆文件"思想在 [[04_文件IO编程]] 中会深入学习 - Linux的"一切皆文件"思想在 [[04_文件IO编程]] 中会深入学习
- 文件权限在 [[04_文件IO编程]] 中用 `open()` 系统调用的 `mode` 参数体现 - 文件权限在 [[04_文件IO编程]] 中用 `open()` 系统调用的 `mode` 参数体现
- `/proc` 文件系统在 [[05_进程控制]] 中用于查看进程状态 - `/proc` 文件系统在 [[05_进程控制]] 中用于查看进程状态
- 后续C语言编程需要在Linux环境下进行参见 [[03_C语言编程基础]] - 后续C语言编程需要在Linux环境下进行参见 [[03_C语言编程基础]]
--- ---
## 八、思考题 ## 八、思考题
1. **为什么Linux没有盘符** 这种设计有什么优势? 1. **为什么Linux没有盘符** 这种设计有什么优势?
2. **/proc 为什么是"虚拟"文件系统?** 它与 /home 这样的目录有什么本质区别? 2. **/proc 为什么是"虚拟"文件系统?** 它与 /home 这样的目录有什么本质区别?
3. **`rm -rf /` 会发生什么?** 为什么root用户执行这个命令非常危险 3. **`rm -rf /` 会发生什么?** 为什么root用户执行这个命令非常危险
4. **`chmod 755``chmod 644` 分别适用于什么场景?**(提示:可执行文件 vs 数据文件) 4. **`chmod 755``chmod 644` 分别适用于什么场景?**(提示:可执行文件 vs 数据文件)
--- ---
## 九、扩展阅读 ## 九、扩展阅读
- 《鸟哥的Linux私房菜》第5-6章Linux文件权限与目录配置 - 《鸟哥的Linux私房菜》第5-6章Linux文件权限与目录配置
- 《UNIX环境高级编程》第1-2章UNIX基础知识 - 《UNIX环境高级编程》第1-2章UNIX基础知识
- Linux文件系统层次标准FHShttps://pathname.com/fhs/ - Linux文件系统层次标准FHShttps://pathname.com/fhs/

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,315 +1,315 @@
# 第06讲进程控制进阶 # 第06讲进程控制进阶
> 🎯 **本节目标**:深入理解 fork/exec 的工作原理,掌握 Shell 的实现机制 > 🎯 **本节目标**:深入理解 fork/exec 的工作原理,掌握 Shell 的实现机制
## 📋 前置知识 ## 📋 前置知识
- [[06_进程控制]] — fork、exec、wait 的基本概念 - [[06_进程控制]] — fork、exec、wait 的基本概念
--- ---
## 🤔 为什么需要这个? ## 🤔 为什么需要这个?
你每天都在用 Shell命令行但你有没有想过 你每天都在用 Shell命令行但你有没有想过
- Shell 是怎么执行你的命令的? - Shell 是怎么执行你的命令的?
- 为什么输入 `ls` 就能列出文件? - 为什么输入 `ls` 就能列出文件?
- 后台运行 `&` 是怎么实现的? - 后台运行 `&` 是怎么实现的?
理解这些,需要深入掌握 fork 和 exec 的配合机制。 理解这些,需要深入掌握 fork 和 exec 的配合机制。
--- ---
## 📖 核心概念 ## 📖 核心概念
### 1. Shell 的工作原理 ### 1. Shell 的工作原理
```mermaid ```mermaid
graph TD graph TD
A[用户输入命令] --> B[Shell 解析命令] A[用户输入命令] --> B[Shell 解析命令]
B --> C[Shell 调用 fork] B --> C[Shell 调用 fork]
C --> D[子进程调用 execvp] C --> D[子进程调用 execvp]
D --> E[执行命令程序] D --> E[执行命令程序]
E --> F[子进程结束] E --> F[子进程结束]
F --> G[Shell 调用 waitpid] F --> G[Shell 调用 waitpid]
G --> A G --> A
style A fill:#e1f5fe style A fill:#e1f5fe
style D fill:#e8f5e9 style D fill:#e8f5e9
``` ```
**Shell 的核心逻辑** **Shell 的核心逻辑**
```c ```c
while (1) { while (1) {
printf("%% "); // 打印提示符 printf("%% "); // 打印提示符
fgets(cmdline); // 读取命令 fgets(cmdline); // 读取命令
if (feof(stdin)) exit(0); if (feof(stdin)) exit(0);
pid = fork(); // 创建子进程 pid = fork(); // 创建子进程
if (pid == 0) { // 子进程 if (pid == 0) { // 子进程
execvp(argv[0], argv); // 执行命令 execvp(argv[0], argv); // 执行命令
exit(1); // exec 失败 exit(1); // exec 失败
} }
if (!background) // 前台运行 if (!background) // 前台运行
waitpid(pid); // 等待子进程结束 waitpid(pid); // 等待子进程结束
} }
``` ```
### 2. fork() 的实现细节 ### 2. fork() 的实现细节
```mermaid ```mermaid
graph TD graph TD
A[父进程调用 fork] --> B[内核复制父进程的 PCB] A[父进程调用 fork] --> B[内核复制父进程的 PCB]
B --> C[复制页表(写时复制)] B --> C[复制页表(写时复制)]
C --> D[设置子进程的 PID] C --> D[设置子进程的 PID]
D --> E[父子进程各返回一次] D --> E[父子进程各返回一次]
style A fill:#e1f5fe style A fill:#e1f5fe
style B fill:#fff3e0 style B fill:#fff3e0
``` ```
**写时复制Copy-on-Write** **写时复制Copy-on-Write**
- fork() 时不立即复制物理内存 - fork() 时不立即复制物理内存
- 父子进程共享同一份物理页面 - 父子进程共享同一份物理页面
- 只有当某一方尝试写入时,才复制该页面 - 只有当某一方尝试写入时,才复制该页面
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant 父进程 participant 父进程
participant 子进程 participant 子进程
participant 内存 participant 内存
父进程->>内存: fork() 父进程->>内存: fork()
Note over 内存: 父子共享物理页面 Note over 内存: 父子共享物理页面
子进程->>内存: 尝试写入 子进程->>内存: 尝试写入
Note over 内存: 触发写时复制 Note over 内存: 触发写时复制
Note over 内存: 复制该页面给子进程 Note over 内存: 复制该页面给子进程
子进程->>内存: 写入新页面 子进程->>内存: 写入新页面
``` ```
### 3. exec() 的工作流程 ### 3. exec() 的工作流程
```mermaid ```mermaid
graph TD graph TD
A[调用 execvp] --> B[查找可执行文件] A[调用 execvp] --> B[查找可执行文件]
B --> C[释放旧的地址空间] B --> C[释放旧的地址空间]
C --> D[加载新的代码段] C --> D[加载新的代码段]
D --> E[加载新的数据段] D --> E[加载新的数据段]
E --> F[设置新的栈] E --> F[设置新的栈]
F --> G[跳转到新程序入口] F --> G[跳转到新程序入口]
style A fill:#e1f5fe style A fill:#e1f5fe
style G fill:#e8f5e9 style G fill:#e8f5e9
``` ```
**exec 的关键特性** **exec 的关键特性**
- **不创建新进程**:只是替换当前进程的内容 - **不创建新进程**:只是替换当前进程的内容
- **PID 不变**:进程还是原来的进程 - **PID 不变**:进程还是原来的进程
- **文件描述符保留**:打开的文件不会自动关闭(除非设置了 close-on-exec - **文件描述符保留**:打开的文件不会自动关闭(除非设置了 close-on-exec
### 4. 进程组与会话 ### 4. 进程组与会话
```mermaid ```mermaid
graph TD graph TD
A[会话 Session] --> B[前台进程组] A[会话 Session] --> B[前台进程组]
A --> C[后台进程组1] A --> C[后台进程组1]
A --> D[后台进程组2] A --> D[后台进程组2]
B --> B1[Shell] B --> B1[Shell]
B --> B2[当前命令] B --> B2[当前命令]
style A fill:#ffcdd2 style A fill:#ffcdd2
style B fill:#e8f5e9 style B fill:#e8f5e9
style C fill:#e1f5fe style C fill:#e1f5fe
style D fill:#e1f5fe style D fill:#e1f5fe
``` ```
**进程组**:一组相关进程的集合 **进程组**:一组相关进程的集合
- 用于信号的批量发送 - 用于信号的批量发送
- 用于作业控制(前台/后台切换) - 用于作业控制(前台/后台切换)
**会话**:一个用户登录期间的所有进程 **会话**:一个用户登录期间的所有进程
- 一个终端对应一个会话 - 一个终端对应一个会话
- 会话有一个控制终端 - 会话有一个控制终端
### 5. 守护进程 ### 5. 守护进程
守护进程是在后台运行的特殊进程,没有控制终端: 守护进程是在后台运行的特殊进程,没有控制终端:
```mermaid ```mermaid
graph TD graph TD
A[创建子进程] --> B[父进程退出] A[创建子进程] --> B[父进程退出]
B --> C[创建新会话] B --> C[创建新会话]
C --> D[改变工作目录] C --> D[改变工作目录]
D --> E[关闭文件描述符] D --> E[关闭文件描述符]
E --> F[重定向 stdin/stdout/stderr] E --> F[重定向 stdin/stdout/stderr]
F --> G[进入主循环] F --> G[进入主循环]
style A fill:#e1f5fe style A fill:#e1f5fe
style G fill:#e8f5e9 style G fill:#e8f5e9
``` ```
--- ---
## 💻 动手实践 ## 💻 动手实践
### 示例1实现简单的 Shell ### 示例1实现简单的 Shell
```c ```c
// myshell.c - 简单的 Shell 实现 // myshell.c - 简单的 Shell 实现
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h> #include <unistd.h>
int main(void) { int main(void) {
int ret, i, k, len, pid; int ret, i, k, len, pid;
char cmd[100]; // 命令串最多100个字符 char cmd[100]; // 命令串最多100个字符
char *arg[20]; // 参数数组最多20个参数 char *arg[20]; // 参数数组最多20个参数
printf("%% "); // 打印提示符 printf("%% "); // 打印提示符
fgets(cmd, 100, stdin); // 从标准输入读取一行命令 fgets(cmd, 100, stdin); // 从标准输入读取一行命令
// 将命令串中的空格替换成'\0',并将各参数提取出来 // 将命令串中的空格替换成'\0',并将各参数提取出来
// 例如 cmd[100]="ls -l -a\0",替换后变为 cmd="ls\0-l\0-a\0" // 例如 cmd[100]="ls -l -a\0",替换后变为 cmd="ls\0-l\0-a\0"
len = strlen(cmd); len = strlen(cmd);
cmd[len - 1] = '\0'; // 去掉 fgets 加在串尾的换行符 cmd[len - 1] = '\0'; // 去掉 fgets 加在串尾的换行符
for (i = 0; i < len - 1; i++) for (i = 0; i < len - 1; i++)
if (cmd[i] == ' ') cmd[i] = '\0'; if (cmd[i] == ' ') cmd[i] = '\0';
// 准备参数数组 arg // 准备参数数组 arg
// arg[0]=cmd, arg[1]=cmd+3, arg[2]=cmd+6, arg[3]=NULL // arg[0]=cmd, arg[1]=cmd+3, arg[2]=cmd+6, arg[3]=NULL
arg[0] = cmd; arg[0] = cmd;
k = 1; k = 1;
for (i = 1; i < len - 1; i++) { for (i = 1; i < len - 1; i++) {
if (cmd[i] == '\0') { if (cmd[i] == '\0') {
arg[k] = cmd + i + 1; arg[k] = cmd + i + 1;
k++; k++;
} }
} }
arg[k] = NULL; arg[k] = NULL;
pid = fork(); pid = fork();
if (pid == 0) { if (pid == 0) {
ret = execvp(arg[0], arg); // 子进程执行命令 ret = execvp(arg[0], arg); // 子进程执行命令
if (ret == -1) { if (ret == -1) {
perror("exec error"); perror("exec error");
exit(1); exit(1);
} }
} else { } else {
wait(-1); // 父进程等待子进程结束 wait(-1); // 父进程等待子进程结束
} }
} }
``` ```
**编译运行** **编译运行**
```bash ```bash
gcc -o myshell myshell.c gcc -o myshell myshell.c
./myshell ./myshell
% ls -l % ls -l
``` ```
### 示例2后台运行 ### 示例2后台运行
```c ```c
// shellex.c - 支持后台运行的 Shell // shellex.c - 支持后台运行的 Shell
#include "wrapper.h" #include "wrapper.h"
#define MAXARGS 128 #define MAXARGS 128
int parseline(char *buf, char **argv) { int parseline(char *buf, char **argv) {
char *delim; char *delim;
int argc; int argc;
int bg; // 后台作业标志 int bg; // 后台作业标志
buf[strlen(buf) - 1] = ' '; // 用空格替换末尾换行 buf[strlen(buf) - 1] = ' '; // 用空格替换末尾换行
while (*buf && (*buf == ' ')) while (*buf && (*buf == ' '))
buf++; buf++;
argc = 0; argc = 0;
while ((delim = strchr(buf, ' '))) { while ((delim = strchr(buf, ' '))) {
argv[argc++] = buf; argv[argc++] = buf;
*delim = '\0'; *delim = '\0';
buf = delim + 1; buf = delim + 1;
while (*buf && (*buf == ' ')) while (*buf && (*buf == ' '))
buf++; buf++;
} }
argv[argc] = NULL; argv[argc] = NULL;
if (argc == 0) return 1; if (argc == 0) return 1;
// 检查是否应该在后台执行 // 检查是否应该在后台执行
if ((bg = (*argv[argc - 1] == '&')) != 0) if ((bg = (*argv[argc - 1] == '&')) != 0)
argv[--argc] = NULL; argv[--argc] = NULL;
return bg; return bg;
} }
void execute(char *cmdline) { void execute(char *cmdline) {
char *argv[MAXARGS]; char *argv[MAXARGS];
char buf[MAXLINE]; char buf[MAXLINE];
int bg; int bg;
pid_t pid; pid_t pid;
strcpy(buf, cmdline); strcpy(buf, cmdline);
bg = parseline(buf, argv); bg = parseline(buf, argv);
if (argv[0] == NULL) return; // 忽略空行 if (argv[0] == NULL) return; // 忽略空行
if ((pid = fork()) == 0) { // 子进程 if ((pid = fork()) == 0) { // 子进程
if (execvp(argv[0], argv) < 0) { if (execvp(argv[0], argv) < 0) {
printf("%s: Command not found.\n", argv[0]); printf("%s: Command not found.\n", argv[0]);
exit(0); exit(0);
} }
} }
if (!bg) { // 前台运行 if (!bg) { // 前台运行
int status; int status;
if (waitpid(pid, &status, 0) < 0) if (waitpid(pid, &status, 0) < 0)
perror("waitpid error"); perror("waitpid error");
} else { // 后台运行 } else { // 后台运行
printf("%d %s", pid, cmdline); printf("%d %s", pid, cmdline);
} }
} }
int main() { int main() {
char cmdline[MAXLINE]; char cmdline[MAXLINE];
while (1) { while (1) {
printf("%% "); printf("%% ");
fgets(cmdline, MAXLINE, stdin); fgets(cmdline, MAXLINE, stdin);
if (feof(stdin)) exit(0); if (feof(stdin)) exit(0);
execute(cmdline); execute(cmdline);
} }
} }
``` ```
**编译运行** **编译运行**
```bash ```bash
gcc -o shellex shellex.c -L. -lwrapper gcc -o shellex shellex.c -L. -lwrapper
./shellex ./shellex
% sleep 10 & # 后台运行 % sleep 10 & # 后台运行
% ps # 查看进程 % ps # 查看进程
``` ```
--- ---
## 🔗 知识关联 ## 🔗 知识关联
- Shell 的 I/O 重定向在 [[04_文件IO编程]] 中的 dup2 有详细讲解 - Shell 的 I/O 重定向在 [[04_文件IO编程]] 中的 dup2 有详细讲解
- 守护进程在 [[09_网络编程]] 中会实际使用 - 守护进程在 [[09_网络编程]] 中会实际使用
- 进程组在 [[11_处理机调度]] 中用于作业控制 - 进程组在 [[11_处理机调度]] 中用于作业控制
--- ---
## 📝 思考题 ## 📝 思考题
1. **为什么 exec() 后文件描述符还保留?** 什么时候需要关闭它们? 1. **为什么 exec() 后文件描述符还保留?** 什么时候需要关闭它们?
2. **写时复制的优势是什么?** 如果 fork() 时立即复制所有内存会怎样? 2. **写时复制的优势是什么?** 如果 fork() 时立即复制所有内存会怎样?
3. **Shell 是怎么实现管道的?** 例如 `ls | grep .c` 的执行过程是什么? 3. **Shell 是怎么实现管道的?** 例如 `ls | grep .c` 的执行过程是什么?
--- ---
## 📚 扩展阅读 ## 📚 扩展阅读
- 《UNIX环境高级编程》第8章进程控制 - 《UNIX环境高级编程》第8章进程控制
- 《深入理解计算机系统》第8章异常控制流 - 《深入理解计算机系统》第8章异常控制流
- [Bash 源码](https://git.savannah.gnu.org/cgit/bash.git/) - [Bash 源码](https://git.savannah.gnu.org/cgit/bash.git/)

File diff suppressed because it is too large Load Diff

View File

@@ -1,406 +1,406 @@
# 第08讲进程间通信 # 第08讲进程间通信
> 🎯 **本节目标**:掌握管道、消息队列、共享内存等进程间通信方式 > 🎯 **本节目标**:掌握管道、消息队列、共享内存等进程间通信方式
## 📋 前置知识 ## 📋 前置知识
- [[06_进程控制]] — 进程的基本概念 - [[06_进程控制]] — 进程的基本概念
- [[06_进程控制_深入]] — fork 和 exec 的工作原理 - [[06_进程控制_深入]] — fork 和 exec 的工作原理
--- ---
## 🤔 为什么需要这个? ## 🤔 为什么需要这个?
进程之间是相互隔离的,每个进程有自己的地址空间。但有时候进程需要协作: 进程之间是相互隔离的,每个进程有自己的地址空间。但有时候进程需要协作:
- Shell 需要将 `ls` 的输出传给 `grep` - Shell 需要将 `ls` 的输出传给 `grep`
- 浏览器需要与下载管理器通信 - 浏览器需要与下载管理器通信
- 数据库需要与应用程序交互 - 数据库需要与应用程序交互
**进程间通信IPC** 就是解决这个问题的。 **进程间通信IPC** 就是解决这个问题的。
**生活比喻** **生活比喻**
- **管道** = 对讲机(单向通信) - **管道** = 对讲机(单向通信)
- **消息队列** = 邮箱(异步通信) - **消息队列** = 邮箱(异步通信)
- **共享内存** = 共享白板(最快的通信方式) - **共享内存** = 共享白板(最快的通信方式)
--- ---
## 📖 核心概念 ## 📖 核心概念
### 1. IPC 方式概览 ### 1. IPC 方式概览
```mermaid ```mermaid
graph TD graph TD
A[进程间通信 IPC] --> B[管道 Pipe] A[进程间通信 IPC] --> B[管道 Pipe]
A --> C[消息队列 Message Queue] A --> C[消息队列 Message Queue]
A --> D[共享内存 Shared Memory] A --> D[共享内存 Shared Memory]
A --> E[信号 Signal] A --> E[信号 Signal]
A --> F[信号量 Semaphore] A --> F[信号量 Semaphore]
A --> G[套接字 Socket] A --> G[套接字 Socket]
B --> B1[单向通信] B --> B1[单向通信]
C --> C1[异步通信] C --> C1[异步通信]
D --> D1[最快] D --> D1[最快]
E --> E1[异步通知] E --> E1[异步通知]
F --> F1[同步控制] F --> F1[同步控制]
G --> G1[网络通信] G --> G1[网络通信]
style A fill:#e1f5fe style A fill:#e1f5fe
style D fill:#e8f5e9 style D fill:#e8f5e9
``` ```
**对比** **对比**
| 方式 | 优点 | 缺点 | 适用场景 | | 方式 | 优点 | 缺点 | 适用场景 |
|------|------|------|----------| |------|------|------|----------|
| 管道 | 简单、易用 | 单向、只能父子进程 | Shell 命令组合 | | 管道 | 简单、易用 | 单向、只能父子进程 | Shell 命令组合 |
| 消息队列 | 异步、可按类型读取 | 有大小限制 | 任务分发 | | 消息队列 | 异步、可按类型读取 | 有大小限制 | 任务分发 |
| 共享内存 | 最快 | 需要同步机制 | 大量数据交换 | | 共享内存 | 最快 | 需要同步机制 | 大量数据交换 |
| 信号 | 异步通知 | 只能传递信号编号 | 事件通知 | | 信号 | 异步通知 | 只能传递信号编号 | 事件通知 |
| 信号量 | 同步控制 | 不能传递数据 | 互斥、同步 | | 信号量 | 同步控制 | 不能传递数据 | 互斥、同步 |
| 套接字 | 跨网络 | 开销大 | 网络通信 | | 套接字 | 跨网络 | 开销大 | 网络通信 |
### 2. 管道Pipe ### 2. 管道Pipe
管道是最古老的 IPC 方式,用于有亲缘关系的进程之间: 管道是最古老的 IPC 方式,用于有亲缘关系的进程之间:
```mermaid ```mermaid
graph LR graph LR
A[写端 fd[1]] -->|数据流| B[读端 fd[0]] A[写端 fd[1]] -->|数据流| B[读端 fd[0]]
style A fill:#e8f5e9 style A fill:#e8f5e9
style B fill:#e1f5fe style B fill:#e1f5fe
``` ```
**特点** **特点**
- **单向**:只能从一端写,另一端读 - **单向**:只能从一端写,另一端读
- **有亲缘关系**:通常用于父子进程 - **有亲缘关系**:通常用于父子进程
- **自带同步**:读端空时阻塞,写端满时阻塞 - **自带同步**:读端空时阻塞,写端满时阻塞
### 3. 命名管道FIFO ### 3. 命名管道FIFO
命名管道让没有亲缘关系的进程也能通信: 命名管道让没有亲缘关系的进程也能通信:
```mermaid ```mermaid
graph LR graph LR
A[进程1] -->|写入| B[/tmp/my_fifo] A[进程1] -->|写入| B[/tmp/my_fifo]
B -->|读取| C[进程2] B -->|读取| C[进程2]
style B fill:#fff3e0 style B fill:#fff3e0
``` ```
**特点** **特点**
- 有文件名,存在于文件系统中 - 有文件名,存在于文件系统中
- 任意进程都可以打开 - 任意进程都可以打开
- 使用方法与普通文件相同 - 使用方法与普通文件相同
### 4. 消息队列 ### 4. 消息队列
消息队列是一种异步通信方式: 消息队列是一种异步通信方式:
```mermaid ```mermaid
graph LR graph LR
A[发送方 msgsnd] -->|消息| B[消息队列] A[发送方 msgsnd] -->|消息| B[消息队列]
B -->|消息| C[接收方 msgrcv] B -->|消息| C[接收方 msgrcv]
style B fill:#fff3e0 style B fill:#fff3e0
``` ```
**消息结构** **消息结构**
```c ```c
struct msgbuf { struct msgbuf {
long mtype; // 消息类型 long mtype; // 消息类型
char mtext[512]; // 消息内容 char mtext[512]; // 消息内容
}; };
``` ```
**优势** **优势**
- 可以按类型读取消息 - 可以按类型读取消息
- 异步通信,不需要同步 - 异步通信,不需要同步
- 可以设置优先级 - 可以设置优先级
### 5. 共享内存 ### 5. 共享内存
共享内存是最快的 IPC 方式: 共享内存是最快的 IPC 方式:
```mermaid ```mermaid
graph TD graph TD
A[进程1] -->|读写| B[共享内存区域] A[进程1] -->|读写| B[共享内存区域]
C[进程2] -->|读写| B C[进程2] -->|读写| B
style B fill:#e8f5e9 style B fill:#e8f5e9
``` ```
**工作流程** **工作流程**
1. 创建共享内存段 1. 创建共享内存段
2. 将共享内存映射到进程地址空间 2. 将共享内存映射到进程地址空间
3. 直接读写共享内存 3. 直接读写共享内存
4. 使用完毕后分离 4. 使用完毕后分离
**注意**:共享内存本身不提供同步机制,需要配合信号量使用。 **注意**:共享内存本身不提供同步机制,需要配合信号量使用。
--- ---
## 💻 动手实践 ## 💻 动手实践
### 示例1使用管道通信 ### 示例1使用管道通信
```c ```c
// pipe1.c - 管道通信示例 // pipe1.c - 管道通信示例
#include "wrapper.h" #include "wrapper.h"
int main() { int main() {
int count; int count;
int fds[2]; // fds[0]=读端, fds[1]=写端 int fds[2]; // fds[0]=读端, fds[1]=写端
const char some_data[] = "1234567890"; const char some_data[] = "1234567890";
char buffer[BUFSIZ + 1]; char buffer[BUFSIZ + 1];
memset(buffer, '\0', sizeof(buffer)); memset(buffer, '\0', sizeof(buffer));
// 创建管道 // 创建管道
pipe(fds); pipe(fds);
// 向管道写入数据 // 向管道写入数据
count = Write(fds[1], (void *)some_data, strlen(some_data)); count = Write(fds[1], (void *)some_data, strlen(some_data));
printf("Wrote %d bytes\n", count); printf("Wrote %d bytes\n", count);
// 从管道读取数据 // 从管道读取数据
count = Read(fds[0], (void *)buffer, BUFSIZ); count = Read(fds[0], (void *)buffer, BUFSIZ);
printf("Read %d bytes: %s\n", count, buffer); printf("Read %d bytes: %s\n", count, buffer);
exit(EXIT_SUCCESS); exit(EXIT_SUCCESS);
} }
``` ```
**编译运行** **编译运行**
```bash ```bash
gcc -o pipe1 pipe1.c -L. -lwrapper gcc -o pipe1 pipe1.c -L. -lwrapper
./pipe1 ./pipe1
``` ```
**预期输出** **预期输出**
``` ```
Wrote 10 bytes Wrote 10 bytes
Read 10 bytes: 1234567890 Read 10 bytes: 1234567890
``` ```
### 示例2创建命名管道 ### 示例2创建命名管道
```c ```c
// fifo1.c - 创建命名管道 // fifo1.c - 创建命名管道
#include <unistd.h> #include <unistd.h>
#include <stdlib.h> #include <stdlib.h>
#include <stdio.h> #include <stdio.h>
#include <sys/types.h> #include <sys/types.h>
#include <sys/stat.h> #include <sys/stat.h>
int main() { int main() {
int res = mkfifo("/tmp/my_fifo", 0777); int res = mkfifo("/tmp/my_fifo", 0777);
if (res == 0) if (res == 0)
printf("FIFO created\n"); printf("FIFO created\n");
exit(EXIT_SUCCESS); exit(EXIT_SUCCESS);
} }
``` ```
**编译运行** **编译运行**
```bash ```bash
gcc -o fifo1 fifo1.c gcc -o fifo1 fifo1.c
./fifo1 ./fifo1
ls -l /tmp/my_fifo ls -l /tmp/my_fifo
``` ```
**使用命名管道** **使用命名管道**
```bash ```bash
# 终端1写入数据 # 终端1写入数据
echo "Hello FIFO" > /tmp/my_fifo echo "Hello FIFO" > /tmp/my_fifo
# 终端2读取数据 # 终端2读取数据
cat /tmp/my_fifo cat /tmp/my_fifo
``` ```
### 示例3共享内存通信 ### 示例3共享内存通信
```c ```c
// shmwrite.c - 写入共享内存 // shmwrite.c - 写入共享内存
#include "wrapper.h" #include "wrapper.h"
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
int shmid; int shmid;
key_t key; key_t key;
void *shmptr; void *shmptr;
if (argc <= 1) { if (argc <= 1) {
fprintf(stderr, "请以 ./shmwrite <key> <message> 形式运行\n"); fprintf(stderr, "请以 ./shmwrite <key> <message> 形式运行\n");
exit(2); exit(2);
} }
// 将参数转换成十六进制数作为 key // 将参数转换成十六进制数作为 key
sscanf(argv[1], "%x", &key); sscanf(argv[1], "%x", &key);
// 创建共享内存 // 创建共享内存
shmid = Shmget(key, 4096, IPC_CREAT | 0644); shmid = Shmget(key, 4096, IPC_CREAT | 0644);
// 将共享内存映射到进程地址空间 // 将共享内存映射到进程地址空间
shmptr = Shmat(shmid, 0, 0); shmptr = Shmat(shmid, 0, 0);
// 写入数据 // 写入数据
memcpy(shmptr, argv[2], strlen(argv[2]) + 1); memcpy(shmptr, argv[2], strlen(argv[2]) + 1);
// 分离共享内存 // 分离共享内存
Shmdt(shmptr); Shmdt(shmptr);
exit(0); exit(0);
} }
``` ```
```c ```c
// shmread.c - 读取共享内存 // shmread.c - 读取共享内存
#include "wrapper.h" #include "wrapper.h"
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
int shmid; int shmid;
key_t key; key_t key;
void *shmptr; void *shmptr;
if (argc <= 1) { if (argc <= 1) {
fprintf(stderr, "请以 ./shmread <key> 形式运行\n"); fprintf(stderr, "请以 ./shmread <key> 形式运行\n");
exit(2); exit(2);
} }
sscanf(argv[1], "%x", &key); sscanf(argv[1], "%x", &key);
// 获取已存在的共享内存 // 获取已存在的共享内存
shmid = Shmget(key, 4096, IPC_CREAT | 0644); shmid = Shmget(key, 4096, IPC_CREAT | 0644);
// 映射共享内存 // 映射共享内存
shmptr = Shmat(shmid, 0, 0); shmptr = Shmat(shmid, 0, 0);
// 读取数据 // 读取数据
printf("%s\n", (char *)shmptr); printf("%s\n", (char *)shmptr);
// 分离共享内存 // 分离共享内存
Shmdt(shmptr); Shmdt(shmptr);
exit(0); exit(0);
} }
``` ```
**编译运行** **编译运行**
```bash ```bash
gcc -o shmwrite shmwrite.c -L. -lwrapper gcc -o shmwrite shmwrite.c -L. -lwrapper
gcc -o shmread shmread.c -L. -lwrapper gcc -o shmread shmread.c -L. -lwrapper
# 写入数据 # 写入数据
./shmwrite 0x12345678 "Hello Shared Memory!" ./shmwrite 0x12345678 "Hello Shared Memory!"
# 读取数据 # 读取数据
./shmread 0x12345678 ./shmread 0x12345678
``` ```
**预期输出** **预期输出**
``` ```
Hello Shared Memory! Hello Shared Memory!
``` ```
### 示例4消息队列通信 ### 示例4消息队列通信
```c ```c
// msgsnd1.c - 发送消息 // msgsnd1.c - 发送消息
#include "wrapper.h" #include "wrapper.h"
typedef struct MESSAGE { typedef struct MESSAGE {
int mtype; int mtype;
char mtext[512]; char mtext[512];
} mymsg, *pmymsg; } mymsg, *pmymsg;
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
int msqid; int msqid;
key_t key; key_t key;
mymsg msginfo; mymsg msginfo;
if (argc != 3) { if (argc != 3) {
fprintf(stderr, "使用方法: msgsnd1 <key> <message>\n"); fprintf(stderr, "使用方法: msgsnd1 <key> <message>\n");
exit(2); exit(2);
} }
sscanf(argv[1], "%x", &key); sscanf(argv[1], "%x", &key);
// 获取消息队列 // 获取消息队列
msqid = Msgget(key, 0644); msqid = Msgget(key, 0644);
// 设置消息类型和内容 // 设置消息类型和内容
msginfo.mtype = 1; msginfo.mtype = 1;
memcpy(&msginfo.mtext, argv[2], strlen(argv[2]) + 1); memcpy(&msginfo.mtext, argv[2], strlen(argv[2]) + 1);
// 发送消息 // 发送消息
Msgsnd(msqid, (pmymsg)&msginfo, strlen(msginfo.mtext) + 1, 0); Msgsnd(msqid, (pmymsg)&msginfo, strlen(msginfo.mtext) + 1, 0);
printf("you send a message \"%s\" to msq %d\n", argv[1], msqid); printf("you send a message \"%s\" to msq %d\n", argv[1], msqid);
return 0; return 0;
} }
``` ```
```c ```c
// msgrcv1.c - 接收消息 // msgrcv1.c - 接收消息
#include "wrapper.h" #include "wrapper.h"
typedef struct MESSAGE { typedef struct MESSAGE {
int mtype; int mtype;
char mtext[512]; char mtext[512];
} mymsg, *pmymsg; } mymsg, *pmymsg;
int main(int argc, char *argv[]) { int main(int argc, char *argv[]) {
int msqid; int msqid;
key_t key; key_t key;
mymsg msginfo; mymsg msginfo;
if (argc != 2) { if (argc != 2) {
fprintf(stderr, "使用方法: msgrcv1 <key>\n"); fprintf(stderr, "使用方法: msgrcv1 <key>\n");
exit(2); exit(2);
} }
sscanf(argv[1], "%x", &key); sscanf(argv[1], "%x", &key);
// 获取消息队列 // 获取消息队列
msqid = Msgget(key, 0644); msqid = Msgget(key, 0644);
// 接收消息类型为1 // 接收消息类型为1
msgrcv(msqid, (pmymsg)&msginfo, 512, 1, 0); msgrcv(msqid, (pmymsg)&msginfo, 512, 1, 0);
printf("%s\n", msginfo.mtext); printf("%s\n", msginfo.mtext);
return 0; return 0;
} }
``` ```
**编译运行** **编译运行**
```bash ```bash
gcc -o msgsnd1 msgsnd1.c -L. -lwrapper gcc -o msgsnd1 msgsnd1.c -L. -lwrapper
gcc -o msgrcv1 msgrcv1.c -L. -lwrapper gcc -o msgrcv1 msgrcv1.c -L. -lwrapper
# 发送消息 # 发送消息
./msgsnd1 0x12345678 "Hello Message Queue!" ./msgsnd1 0x12345678 "Hello Message Queue!"
# 接收消息 # 接收消息
./msgrcv1 0x12345678 ./msgrcv1 0x12345678
``` ```
**预期输出** **预期输出**
``` ```
Hello Message Queue! Hello Message Queue!
``` ```
--- ---
## 🔗 知识关联 ## 🔗 知识关联
- 管道在 Shell 中广泛使用,详见 [[06_进程控制_深入]] - 管道在 Shell 中广泛使用,详见 [[06_进程控制_深入]]
- 共享内存的同步需要信号量,详见 [[07_多线程编程]] - 共享内存的同步需要信号量,详见 [[07_多线程编程]]
- 套接字是网络通信的基础,详见 [[09_网络编程]] - 套接字是网络通信的基础,详见 [[09_网络编程]]
--- ---
## 📝 思考题 ## 📝 思考题
1. **管道的局限性**:为什么管道只能用于有亲缘关系的进程? 1. **管道的局限性**:为什么管道只能用于有亲缘关系的进程?
2. **共享内存的速度优势**:为什么共享内存比管道快? 2. **共享内存的速度优势**:为什么共享内存比管道快?
3. **消息队列 vs 管道**:在什么场景下消息队列比管道更合适? 3. **消息队列 vs 管道**:在什么场景下消息队列比管道更合适?
--- ---
## 📚 扩展阅读 ## 📚 扩展阅读
- 《UNIX环境高级编程》第15章进程间通信 - 《UNIX环境高级编程》第15章进程间通信
- 《深入理解计算机系统》第10章系统级I/O - 《深入理解计算机系统》第10章系统级I/O
- [Linux IPC 编程](https://www.tldp.org/LDP/tlk/ipc/ipc.html) - [Linux IPC 编程](https://www.tldp.org/LDP/tlk/ipc/ipc.html)

View File

@@ -1,296 +1,296 @@
# 第09讲网络编程 # 第09讲网络编程
> 🎯 **本节目标**:掌握 Socket 编程,理解客户端-服务器模型 > 🎯 **本节目标**:掌握 Socket 编程,理解客户端-服务器模型
## 📋 前置知识 ## 📋 前置知识
- [[04_文件IO编程]] — 文件描述符的概念 - [[04_文件IO编程]] — 文件描述符的概念
- [[06_进程控制]] — 进程创建 - [[06_进程控制]] — 进程创建
--- ---
## 🤔 为什么需要这个? ## 🤔 为什么需要这个?
你每天都在使用网络:浏览网页、发送消息、观看视频。但你有没有想过: 你每天都在使用网络:浏览网页、发送消息、观看视频。但你有没有想过:
- 浏览器是怎么从服务器获取网页的? - 浏览器是怎么从服务器获取网页的?
- 两台电脑之间是怎么通信的? - 两台电脑之间是怎么通信的?
**网络编程**就是让你能够编写这样的程序。 **网络编程**就是让你能够编写这样的程序。
**生活比喻** **生活比喻**
- **Socket** = 电话插座 - **Socket** = 电话插座
- **服务器** = 客服中心(等待来电) - **服务器** = 客服中心(等待来电)
- **客户端** = 拨打电话的用户 - **客户端** = 拨打电话的用户
- **端口** = 分机号 - **端口** = 分机号
--- ---
## 📖 核心概念 ## 📖 核心概念
### 1. 客户端-服务器模型 ### 1. 客户端-服务器模型
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant 客户端 participant 客户端
participant 服务器 participant 服务器
服务器->>服务器: socket() 创建套接字 服务器->>服务器: socket() 创建套接字
服务器->>服务器: bind() 绑定地址 服务器->>服务器: bind() 绑定地址
服务器->>服务器: listen() 监听连接 服务器->>服务器: listen() 监听连接
服务器->>服务器: accept() 等待连接 服务器->>服务器: accept() 等待连接
客户端->>客户端: socket() 创建套接字 客户端->>客户端: socket() 创建套接字
客户端->>服务器: connect() 发起连接 客户端->>服务器: connect() 发起连接
服务器->>客户端: 连接建立 服务器->>客户端: 连接建立
客户端->>服务器: send() 发送数据 客户端->>服务器: send() 发送数据
服务器->>客户端: recv() 接收数据 服务器->>客户端: recv() 接收数据
服务器->>客户端: send() 发送响应 服务器->>客户端: send() 发送响应
客户端->>服务器: recv() 接收响应 客户端->>服务器: recv() 接收响应
客户端->>客户端: close() 关闭连接 客户端->>客户端: close() 关闭连接
服务器->>服务器: close() 关闭连接 服务器->>服务器: close() 关闭连接
``` ```
### 2. Socket 编程流程 ### 2. Socket 编程流程
```mermaid ```mermaid
graph TD graph TD
subgraph 服务器端 subgraph 服务器端
A1[socket] --> A2[bind] A1[socket] --> A2[bind]
A2 --> A3[listen] A2 --> A3[listen]
A3 --> A4[accept] A3 --> A4[accept]
A4 --> A5[read/write] A4 --> A5[read/write]
A5 --> A6[close] A5 --> A6[close]
end end
subgraph 客户端 subgraph 客户端
B1[socket] --> B2[connect] B1[socket] --> B2[connect]
B2 --> B3[write/read] B2 --> B3[write/read]
B3 --> B4[close] B3 --> B4[close]
end end
style A1 fill:#e8f5e9 style A1 fill:#e8f5e9
style B1 fill:#e1f5fe style B1 fill:#e1f5fe
``` ```
**核心函数** **核心函数**
| 函数 | 作用 | 服务器/客户端 | | 函数 | 作用 | 服务器/客户端 |
|------|------|:---:| |------|------|:---:|
| `socket()` | 创建套接字 | 都需要 | | `socket()` | 创建套接字 | 都需要 |
| `bind()` | 绑定地址和端口 | 服务器 | | `bind()` | 绑定地址和端口 | 服务器 |
| `listen()` | 开始监听 | 服务器 | | `listen()` | 开始监听 | 服务器 |
| `accept()` | 接受连接 | 服务器 | | `accept()` | 接受连接 | 服务器 |
| `connect()` | 发起连接 | 客户端 | | `connect()` | 发起连接 | 客户端 |
| `send()` | 发送数据 | 都需要 | | `send()` | 发送数据 | 都需要 |
| `recv()` | 接收数据 | 都需要 | | `recv()` | 接收数据 | 都需要 |
| `close()` | 关闭连接 | 都需要 | | `close()` | 关闭连接 | 都需要 |
### 3. IP 地址与端口 ### 3. IP 地址与端口
```mermaid ```mermaid
graph LR graph LR
A[计算机] -->|IP地址| B[192.168.1.100] A[计算机] -->|IP地址| B[192.168.1.100]
A -->|端口| C[:80] A -->|端口| C[:80]
B --> D[唯一标识一台电脑] B --> D[唯一标识一台电脑]
C --> E[唯一标识一个服务] C --> E[唯一标识一个服务]
style B fill:#e1f5fe style B fill:#e1f5fe
style C fill:#e8f5e9 style C fill:#e8f5e9
``` ```
**常见端口** **常见端口**
| 端口 | 服务 | 说明 | | 端口 | 服务 | 说明 |
|:---:|------|------| |:---:|------|------|
| 22 | SSH | 远程登录 | | 22 | SSH | 远程登录 |
| 80 | HTTP | 网页浏览 | | 80 | HTTP | 网页浏览 |
| 443 | HTTPS | 安全网页 | | 443 | HTTPS | 安全网页 |
| 3306 | MySQL | 数据库 | | 3306 | MySQL | 数据库 |
| 8080 | HTTP备用 | 开发常用 | | 8080 | HTTP备用 | 开发常用 |
### 4. 字节序 ### 4. 字节序
不同 CPU 存储多字节数据的方式不同: 不同 CPU 存储多字节数据的方式不同:
```mermaid ```mermaid
graph LR graph LR
A[0x12345678] --> B[大端序 Big-endian] A[0x12345678] --> B[大端序 Big-endian]
A --> C[小端序 Little-endian] A --> C[小端序 Little-endian]
B --> B1[12 34 56 78] B --> B1[12 34 56 78]
C --> C1[78 56 34 12] C --> C1[78 56 34 12]
style B fill:#e1f5fe style B fill:#e1f5fe
style C fill:#e8f5e9 style C fill:#e8f5e9
``` ```
**网络字节序**大端序Big-endian **网络字节序**大端序Big-endian
**转换函数** **转换函数**
```c ```c
htonl() // host to network long htonl() // host to network long
htons() // host to network short htons() // host to network short
ntohl() // network to host long ntohl() // network to host long
ntohs() // network to host short ntohs() // network to host short
``` ```
### 5. DNS 域名解析 ### 5. DNS 域名解析
```mermaid ```mermaid
graph LR graph LR
A[www.example.com] -->|DNS查询| B[DNS服务器] A[www.example.com] -->|DNS查询| B[DNS服务器]
B -->|返回IP| C[93.184.216.34] B -->|返回IP| C[93.184.216.34]
C -->|连接| D[Web服务器] C -->|连接| D[Web服务器]
style A fill:#e1f5fe style A fill:#e1f5fe
style C fill:#e8f5e9 style C fill:#e8f5e9
``` ```
--- ---
## 💻 动手实践 ## 💻 动手实践
### 示例1查询主机信息 ### 示例1查询主机信息
```c ```c
// hostinfo.c - 查询主机信息 // hostinfo.c - 查询主机信息
#include "wrapper.h" #include "wrapper.h"
int main(int argc, char **argv) { int main(int argc, char **argv) {
char **pp; char **pp;
struct in_addr addr; struct in_addr addr;
struct hostent *hostp; struct hostent *hostp;
if (argc != 2) { if (argc != 2) {
fprintf(stderr, "usage: %s <domain name or dotted-decimal>\n", argv[0]); fprintf(stderr, "usage: %s <domain name or dotted-decimal>\n", argv[0]);
exit(0); exit(0);
} }
// 判断是域名还是IP地址 // 判断是域名还是IP地址
if (inet_aton(argv[1], &addr) != 0) if (inet_aton(argv[1], &addr) != 0)
hostp = Gethostbyaddr((const char *)&addr, sizeof(addr), AF_INET); hostp = Gethostbyaddr((const char *)&addr, sizeof(addr), AF_INET);
else else
hostp = Gethostbyname(argv[1]); hostp = Gethostbyname(argv[1]);
// 打印主机信息 // 打印主机信息
printf("official hostname: %s\n", hostp->h_name); printf("official hostname: %s\n", hostp->h_name);
for (pp = hostp->h_aliases; *pp != NULL; pp++) for (pp = hostp->h_aliases; *pp != NULL; pp++)
printf("alias: %s\n", *pp); printf("alias: %s\n", *pp);
for (pp = hostp->h_addr_list; *pp != NULL; pp++) { for (pp = hostp->h_addr_list; *pp != NULL; pp++) {
addr.s_addr = *((unsigned int *)*pp); addr.s_addr = *((unsigned int *)*pp);
printf("address: %s\n", inet_ntoa(addr)); printf("address: %s\n", inet_ntoa(addr));
} }
exit(0); exit(0);
} }
``` ```
**编译运行** **编译运行**
```bash ```bash
gcc -o hostinfo hostinfo.c -L. -lwrapper gcc -o hostinfo hostinfo.c -L. -lwrapper
./hostinfo www.baidu.com ./hostinfo www.baidu.com
``` ```
**预期输出** **预期输出**
``` ```
official hostname: www.a.shifen.com official hostname: www.a.shifen.com
alias: www.baidu.com alias: www.baidu.com
address: 110.242.68.66 address: 110.242.68.66
address: 110.242.68.3 address: 110.242.68.3
``` ```
### 示例2TCP 服务器(大小写转换) ### 示例2TCP 服务器(大小写转换)
```c ```c
// toggle.c - TCP 服务器 // toggle.c - TCP 服务器
#include "wrapper.h" #include "wrapper.h"
void toggle(int conn_sock) { void toggle(int conn_sock) {
size_t n; size_t n;
int i; int i;
char buf[MAXLINE]; char buf[MAXLINE];
while ((n = recv(conn_sock, buf, MAXLINE, 0)) > 0) { while ((n = recv(conn_sock, buf, MAXLINE, 0)) > 0) {
printf("toggle server received %d bytes\n", n); printf("toggle server received %d bytes\n", n);
// 转换大小写 // 转换大小写
for (i = 0; i < n; i++) for (i = 0; i < n; i++)
if (isupper(buf[i])) if (isupper(buf[i]))
buf[i] = tolower(buf[i]); buf[i] = tolower(buf[i]);
else if (islower(buf[i])) else if (islower(buf[i]))
buf[i] = toupper(buf[i]); buf[i] = toupper(buf[i]);
send(conn_sock, buf, n, 0); send(conn_sock, buf, n, 0);
} }
} }
``` ```
### 示例3TCP 客户端 ### 示例3TCP 客户端
```c ```c
// togglec.c - TCP 客户端 // togglec.c - TCP 客户端
#include "wrapper.h" #include "wrapper.h"
int main(int argc, char **argv) { int main(int argc, char **argv) {
int client_sock, port; int client_sock, port;
char *host, buf[MAXLINE]; char *host, buf[MAXLINE];
rio_t rio; rio_t rio;
if (argc != 3) { if (argc != 3) {
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]); fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
exit(1); exit(1);
} }
host = argv[1]; host = argv[1];
port = atoi(argv[2]); port = atoi(argv[2]);
// 连接服务器 // 连接服务器
client_sock = open_client_sock(host, port); client_sock = open_client_sock(host, port);
// 从标准输入读取,发送到服务器,接收响应 // 从标准输入读取,发送到服务器,接收响应
while (fgets(buf, MAXLINE, stdin) != NULL) { while (fgets(buf, MAXLINE, stdin) != NULL) {
send(client_sock, buf, strlen(buf), 0); send(client_sock, buf, strlen(buf), 0);
recv(client_sock, buf, MAXLINE, 0); recv(client_sock, buf, MAXLINE, 0);
fputs(buf, stdout); fputs(buf, stdout);
} }
close(client_sock); close(client_sock);
exit(0); exit(0);
} }
``` ```
**编译运行** **编译运行**
```bash ```bash
# 编译 # 编译
gcc -o toggle toggle.c -L. -lwrapper gcc -o toggle toggle.c -L. -lwrapper
gcc -o togglec togglec.c -L. -lwrapper gcc -o togglec togglec.c -L. -lwrapper
# 终端1启动服务器 # 终端1启动服务器
./toggle 8080 ./toggle 8080
# 终端2启动客户端 # 终端2启动客户端
./togglec localhost 8080 ./togglec localhost 8080
Hello World # 输入 Hello World # 输入
HELLO WORLD # 输出(大小写转换) HELLO WORLD # 输出(大小写转换)
``` ```
--- ---
## 🔗 知识关联 ## 🔗 知识关联
- Socket 是文件描述符,详见 [[04_文件IO编程]] - Socket 是文件描述符,详见 [[04_文件IO编程]]
- 并发服务器在 [[10_并发服务器]] 中有详细讲解 - 并发服务器在 [[10_并发服务器]] 中有详细讲解
- 网络编程在 [[实验05_Linux网络通信编程]] 中有实践练习 - 网络编程在 [[实验05_Linux网络通信编程]] 中有实践练习
--- ---
## 📝 思考题 ## 📝 思考题
1. **TCP vs UDP**:什么时候用 TCP什么时候用 UDP 1. **TCP vs UDP**:什么时候用 TCP什么时候用 UDP
2. **为什么需要字节序转换?** 如果不转换会怎样? 2. **为什么需要字节序转换?** 如果不转换会怎样?
3. **服务器为什么需要 bind()** 客户端为什么不需要? 3. **服务器为什么需要 bind()** 客户端为什么不需要?
--- ---
## 📚 扩展阅读 ## 📚 扩展阅读
- 《UNIX网络编程》第1卷套接字联网API - 《UNIX网络编程》第1卷套接字联网API
- [Beej's Guide to Network Programming](https://beej.us/guide/bgnet/) - [Beej's Guide to Network Programming](https://beej.us/guide/bgnet/)
- [Socket 编程详解](https://www.cs.rpi.edu/~moorthy/Courses/os98/Pggrams/socket.html) - [Socket 编程详解](https://www.cs.rpi.edu/~moorthy/Courses/os98/Pggrams/socket.html)

View File

@@ -1,395 +1,395 @@
# 第10讲并发网络服务器 # 第10讲并发网络服务器
> 🎯 **本节目标**掌握多进程、多线程、I/O 多路复用三种并发服务器模型 > 🎯 **本节目标**掌握多进程、多线程、I/O 多路复用三种并发服务器模型
## 📋 前置知识 ## 📋 前置知识
- [[09_网络编程]] — Socket 编程基础 - [[09_网络编程]] — Socket 编程基础
- [[06_进程控制]] — 进程创建 - [[06_进程控制]] — 进程创建
- [[07_多线程编程]] — 线程创建 - [[07_多线程编程]] — 线程创建
--- ---
## 🤔 为什么需要这个? ## 🤔 为什么需要这个?
上一讲的服务器一次只能服务一个客户端。如果有 100 个用户同时访问网站,第 100 个用户必须等前 99 个都处理完才能得到响应。 上一讲的服务器一次只能服务一个客户端。如果有 100 个用户同时访问网站,第 100 个用户必须等前 99 个都处理完才能得到响应。
**并发服务器**就是解决这个问题的——让服务器能够同时服务多个客户端。 **并发服务器**就是解决这个问题的——让服务器能够同时服务多个客户端。
**生活比喻** **生活比喻**
- **迭代服务器** = 一个服务员一次只服务一桌客人 - **迭代服务器** = 一个服务员一次只服务一桌客人
- **多进程服务器** = 每来一桌客人就招一个新服务员 - **多进程服务器** = 每来一桌客人就招一个新服务员
- **多线程服务器** = 一个服务员同时照看多桌客人 - **多线程服务器** = 一个服务员同时照看多桌客人
- **I/O 多路复用** = 服务员轮流查看哪桌客人需要服务 - **I/O 多路复用** = 服务员轮流查看哪桌客人需要服务
--- ---
## 📖 核心概念 ## 📖 核心概念
### 1. 三种并发模型 ### 1. 三种并发模型
```mermaid ```mermaid
graph TD graph TD
A[并发服务器模型] --> B[多进程模型] A[并发服务器模型] --> B[多进程模型]
A --> C[多线程模型] A --> C[多线程模型]
A --> D[I/O 多路复用] A --> D[I/O 多路复用]
B --> B1[每连接一个进程] B --> B1[每连接一个进程]
B --> B2[进程间隔离] B --> B2[进程间隔离]
B --> B3[开销大] B --> B3[开销大]
C --> C1[每连接一个线程] C --> C1[每连接一个线程]
C --> C2[共享内存] C --> C2[共享内存]
C --> C3[开销较小] C --> C3[开销较小]
D --> D1[单线程处理多连接] D --> D1[单线程处理多连接]
D --> D2[select/poll/epoll] D --> D2[select/poll/epoll]
D --> D3[开销最小] D --> D3[开销最小]
style B fill:#ffcdd2 style B fill:#ffcdd2
style C fill:#fff3e0 style C fill:#fff3e0
style D fill:#e8f5e9 style D fill:#e8f5e9
``` ```
**对比** **对比**
| 模型 | 优点 | 缺点 | 适用场景 | | 模型 | 优点 | 缺点 | 适用场景 |
|------|------|------|----------| |------|------|------|----------|
| 多进程 | 隔离性好 | 开销大 | 连接数少 | | 多进程 | 隔离性好 | 开销大 | 连接数少 |
| 多线程 | 开销较小 | 需要同步 | 连接数中等 | | 多线程 | 开销较小 | 需要同步 | 连接数中等 |
| I/O 多路复用 | 开销最小 | 编程复杂 | 连接数多 | | I/O 多路复用 | 开销最小 | 编程复杂 | 连接数多 |
### 2. 多进程模型 ### 2. 多进程模型
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant 主进程 participant 主进程
participant 子进程1 participant 子进程1
participant 子进程2 participant 子进程2
participant 客户端 participant 客户端
主进程->>主进程: accept() 等待连接 主进程->>主进程: accept() 等待连接
客户端->>主进程: 连接请求 客户端->>主进程: 连接请求
主进程->>子进程1: fork() 创建子进程 主进程->>子进程1: fork() 创建子进程
子进程1->>客户端: 处理请求 子进程1->>客户端: 处理请求
主进程->>主进程: 继续 accept() 主进程->>主进程: 继续 accept()
客户端->>主进程: 新连接请求 客户端->>主进程: 新连接请求
主进程->>子进程2: fork() 创建子进程 主进程->>子进程2: fork() 创建子进程
子进程2->>客户端: 处理请求 子进程2->>客户端: 处理请求
``` ```
**特点** **特点**
- 每个连接一个独立进程 - 每个连接一个独立进程
- 进程间完全隔离 - 进程间完全隔离
- 进程创建和销毁开销大 - 进程创建和销毁开销大
### 3. 多线程模型 ### 3. 多线程模型
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant 主线程 participant 主线程
participant 工作线程1 participant 工作线程1
participant 工作线程2 participant 工作线程2
participant 客户端 participant 客户端
主线程->>主线程: accept() 等待连接 主线程->>主线程: accept() 等待连接
客户端->>主线程: 连接请求 客户端->>主线程: 连接请求
主线程->>工作线程1: pthread_create() 主线程->>工作线程1: pthread_create()
工作线程1->>客户端: 处理请求 工作线程1->>客户端: 处理请求
主线程->>主线程: 继续 accept() 主线程->>主线程: 继续 accept()
客户端->>主线程: 新连接请求 客户端->>主线程: 新连接请求
主线程->>工作线程2: pthread_create() 主线程->>工作线程2: pthread_create()
工作线程2->>客户端: 处理请求 工作线程2->>客户端: 处理请求
``` ```
**特点** **特点**
- 每个连接一个线程 - 每个连接一个线程
- 线程共享进程资源 - 线程共享进程资源
- 需要注意线程安全 - 需要注意线程安全
### 4. I/O 多路复用select ### 4. I/O 多路复用select
```mermaid ```mermaid
graph TD graph TD
A[主循环] --> B[select 监听所有 fd] A[主循环] --> B[select 监听所有 fd]
B --> C{哪个 fd 就绪?} B --> C{哪个 fd 就绪?}
C -->|监听套接字| D[accept 新连接] C -->|监听套接字| D[accept 新连接]
C -->|客户端套接字| E[处理请求] C -->|客户端套接字| E[处理请求]
D --> A D --> A
E --> A E --> A
style B fill:#fff3e0 style B fill:#fff3e0
``` ```
**select 的工作原理** **select 的工作原理**
1. 将所有需要监听的文件描述符放入集合 1. 将所有需要监听的文件描述符放入集合
2. 调用 `select()` 等待任意一个就绪 2. 调用 `select()` 等待任意一个就绪
3. 遍历集合,处理就绪的描述符 3. 遍历集合,处理就绪的描述符
4. 重复步骤 1 4. 重复步骤 1
**fd_set 操作** **fd_set 操作**
```c ```c
fd_set read_set; fd_set read_set;
FD_ZERO(&read_set); // 清空集合 FD_ZERO(&read_set); // 清空集合
FD_SET(fd1, &read_set); // 添加 fd1 FD_SET(fd1, &read_set); // 添加 fd1
FD_SET(fd2, &read_set); // 添加 fd2 FD_SET(fd2, &read_set); // 添加 fd2
select(maxfd+1, &read_set, NULL, NULL, NULL); // 等待 select(maxfd+1, &read_set, NULL, NULL, NULL); // 等待
if (FD_ISSET(fd1, &read_set)) // 检查 fd1 是否就绪 if (FD_ISSET(fd1, &read_set)) // 检查 fd1 是否就绪
// 处理 fd1 // 处理 fd1
``` ```
### 5. 线程池模型 ### 5. 线程池模型
```mermaid ```mermaid
graph TD graph TD
A[主线程] -->|accept| B[任务队列] A[主线程] -->|accept| B[任务队列]
B --> C[工作线程1] B --> C[工作线程1]
B --> D[工作线程2] B --> D[工作线程2]
B --> E[工作线程3] B --> E[工作线程3]
C -->|处理完毕| B C -->|处理完毕| B
D -->|处理完毕| B D -->|处理完毕| B
E -->|处理完毕| B E -->|处理完毕| B
style B fill:#fff3e0 style B fill:#fff3e0
``` ```
**优势** **优势**
- 避免频繁创建销毁线程 - 避免频繁创建销毁线程
- 控制并发数量 - 控制并发数量
- 提高资源利用率 - 提高资源利用率
--- ---
## 💻 动手实践 ## 💻 动手实践
### 示例1多线程并发服务器 ### 示例1多线程并发服务器
```c ```c
// togglest.c - 多线程并发服务器 // togglest.c - 多线程并发服务器
#include "wrapper.h" #include "wrapper.h"
void toggle(int conn_sock); void toggle(int conn_sock);
void *serve_client(void *vargp); void *serve_client(void *vargp);
int main(int argc, char **argv) { int main(int argc, char **argv) {
int listen_sock, conn_sock, port, *conn_sock_p; int listen_sock, conn_sock, port, *conn_sock_p;
struct sockaddr_in clientaddr; struct sockaddr_in clientaddr;
struct hostent *hp; struct hostent *hp;
char *haddrp; char *haddrp;
socklen_t clientlen = sizeof(struct sockaddr_in); socklen_t clientlen = sizeof(struct sockaddr_in);
pthread_t tid; pthread_t tid;
if (argc != 2) { if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]); fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1); exit(1);
} }
port = atoi(argv[1]); port = atoi(argv[1]);
listen_sock = open_listen_sock(port); listen_sock = open_listen_sock(port);
while (1) { while (1) {
conn_sock_p = malloc(sizeof(int)); conn_sock_p = malloc(sizeof(int));
*conn_sock_p = accept(listen_sock, (SA *)&clientaddr, &clientlen); *conn_sock_p = accept(listen_sock, (SA *)&clientaddr, &clientlen);
// 获取客户端信息 // 获取客户端信息
hp = Gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr, hp = Gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr,
sizeof(clientaddr.sin_addr.s_addr), AF_INET); sizeof(clientaddr.sin_addr.s_addr), AF_INET);
haddrp = inet_ntoa(clientaddr.sin_addr); haddrp = inet_ntoa(clientaddr.sin_addr);
printf("server connected to %s (%s)\n", hp->h_name, haddrp); printf("server connected to %s (%s)\n", hp->h_name, haddrp);
// 创建新线程处理客户端 // 创建新线程处理客户端
pthread_create(&tid, NULL, serve_client, conn_sock_p); pthread_create(&tid, NULL, serve_client, conn_sock_p);
} }
} }
void *serve_client(void *vargp) { void *serve_client(void *vargp) {
int conn_sock = *((int *)vargp); int conn_sock = *((int *)vargp);
pthread_detach(pthread_self()); // 分离线程 pthread_detach(pthread_self()); // 分离线程
free(vargp); free(vargp);
toggle(conn_sock); toggle(conn_sock);
close(conn_sock); close(conn_sock);
return NULL; return NULL;
} }
``` ```
**编译运行** **编译运行**
```bash ```bash
gcc -o togglest togglest.c -L. -lwrapper -lpthread gcc -o togglest togglest.c -L. -lwrapper -lpthread
# 终端1启动服务器 # 终端1启动服务器
./togglest 8080 ./togglest 8080
# 终端2-4启动多个客户端 # 终端2-4启动多个客户端
./togglec localhost 8080 ./togglec localhost 8080
``` ```
### 示例2I/O 多路复用服务器 ### 示例2I/O 多路复用服务器
```c ```c
// toggless1.c - select 多路复用服务器 // toggless1.c - select 多路复用服务器
#include "wrapper.h" #include "wrapper.h"
void toggle(int conn_sock); void toggle(int conn_sock);
void read_input(void); void read_input(void);
int main(int argc, char **argv) { int main(int argc, char **argv) {
int listen_sock, conn_sock, port; int listen_sock, conn_sock, port;
socklen_t clientlen = sizeof(struct sockaddr_in); socklen_t clientlen = sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr; struct sockaddr_in clientaddr;
fd_set read_set, ready_set; fd_set read_set, ready_set;
if (argc != 2) { if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]); fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1); exit(1);
} }
port = atoi(argv[1]); port = atoi(argv[1]);
listen_sock = open_listen_sock(port); listen_sock = open_listen_sock(port);
// 初始化 fd_set // 初始化 fd_set
FD_ZERO(&read_set); FD_ZERO(&read_set);
FD_SET(STDIN_FILENO, &read_set); // 监听标准输入 FD_SET(STDIN_FILENO, &read_set); // 监听标准输入
FD_SET(listen_sock, &read_set); // 监听套接字 FD_SET(listen_sock, &read_set); // 监听套接字
while (1) { while (1) {
ready_set = read_set; ready_set = read_set;
select(listen_sock + 1, &ready_set, NULL, NULL, NULL); select(listen_sock + 1, &ready_set, NULL, NULL, NULL);
// 检查标准输入 // 检查标准输入
if (FD_ISSET(STDIN_FILENO, &ready_set)) if (FD_ISSET(STDIN_FILENO, &ready_set))
read_input(); read_input();
// 检查新连接 // 检查新连接
if (FD_ISSET(listen_sock, &ready_set)) { if (FD_ISSET(listen_sock, &ready_set)) {
conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen); conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen);
toggle(conn_sock); toggle(conn_sock);
close(conn_sock); close(conn_sock);
} }
} }
} }
void read_input(void) { void read_input(void) {
char buf[MAXLINE]; char buf[MAXLINE];
if (!fgets(buf, MAXLINE, stdin)) if (!fgets(buf, MAXLINE, stdin))
exit(0); exit(0);
printf("%s", buf); printf("%s", buf);
} }
``` ```
### 示例3连接池服务器 ### 示例3连接池服务器
```c ```c
// toggless2.c - 连接池服务器 // toggless2.c - 连接池服务器
#include "wrapper.h" #include "wrapper.h"
typedef struct { typedef struct {
int maxfd; int maxfd;
fd_set read_set; fd_set read_set;
fd_set ready_set; fd_set ready_set;
int nready; int nready;
int maxi; int maxi;
int client_sock[FD_SETSIZE]; int client_sock[FD_SETSIZE];
} sock_pool; } sock_pool;
void init_sock_pool(int listen_sock, sock_pool *pool); void init_sock_pool(int listen_sock, sock_pool *pool);
void add_sock(int conn_sock, sock_pool *pool); void add_sock(int conn_sock, sock_pool *pool);
void serve_clients(sock_pool *pool); void serve_clients(sock_pool *pool);
int main(int argc, char **argv) { int main(int argc, char **argv) {
int listen_sock, conn_sock, port; int listen_sock, conn_sock, port;
socklen_t clientlen = sizeof(struct sockaddr_in); socklen_t clientlen = sizeof(struct sockaddr_in);
struct sockaddr_in clientaddr; struct sockaddr_in clientaddr;
static sock_pool pool; static sock_pool pool;
if (argc != 2) { if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]); fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1); exit(1);
} }
port = atoi(argv[1]); port = atoi(argv[1]);
listen_sock = open_listen_sock(port); listen_sock = open_listen_sock(port);
init_sock_pool(listen_sock, &pool); init_sock_pool(listen_sock, &pool);
while (1) { while (1) {
pool.ready_set = pool.read_set; pool.ready_set = pool.read_set;
pool.nready = select(pool.maxfd + 1, &pool.ready_set, NULL, NULL, NULL); pool.nready = select(pool.maxfd + 1, &pool.ready_set, NULL, NULL, NULL);
// 新连接 // 新连接
if (FD_ISSET(listen_sock, &pool.ready_set)) { if (FD_ISSET(listen_sock, &pool.ready_set)) {
conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen); conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen);
add_sock(conn_sock, &pool); add_sock(conn_sock, &pool);
} }
// 处理客户端请求 // 处理客户端请求
serve_clients(&pool); serve_clients(&pool);
} }
} }
void init_sock_pool(int listen_sock, sock_pool *p) { void init_sock_pool(int listen_sock, sock_pool *p) {
int i; int i;
p->maxi = -1; p->maxi = -1;
for (i = 0; i < FD_SETSIZE; i++) for (i = 0; i < FD_SETSIZE; i++)
p->client_sock[i] = -1; p->client_sock[i] = -1;
p->maxfd = listen_sock; p->maxfd = listen_sock;
FD_ZERO(&p->read_set); FD_ZERO(&p->read_set);
FD_SET(listen_sock, &p->read_set); FD_SET(listen_sock, &p->read_set);
} }
void add_sock(int conn_sock, sock_pool *p) { void add_sock(int conn_sock, sock_pool *p) {
int i; int i;
p->nready--; p->nready--;
for (i = 0; i < FD_SETSIZE; i++) for (i = 0; i < FD_SETSIZE; i++)
if (p->client_sock[i] < 0) { if (p->client_sock[i] < 0) {
p->client_sock[i] = conn_sock; p->client_sock[i] = conn_sock;
FD_SET(conn_sock, &p->read_set); FD_SET(conn_sock, &p->read_set);
if (conn_sock > p->maxfd) if (conn_sock > p->maxfd)
p->maxfd = conn_sock; p->maxfd = conn_sock;
if (i > p->maxi) if (i > p->maxi)
p->maxi = i; p->maxi = i;
break; break;
} }
if (i == FD_SETSIZE) if (i == FD_SETSIZE)
perror("add_sock error: Too many clients"); perror("add_sock error: Too many clients");
} }
void serve_clients(sock_pool *p) { void serve_clients(sock_pool *p) {
int i, conn_sock, n; int i, conn_sock, n;
char buf[MAXLINE]; char buf[MAXLINE];
for (i = 0; (i <= p->maxi) && (p->nready > 0); i++) { for (i = 0; (i <= p->maxi) && (p->nready > 0); i++) {
conn_sock = p->client_sock[i]; conn_sock = p->client_sock[i];
if ((conn_sock > 0) && (FD_ISSET(conn_sock, &p->ready_set))) { if ((conn_sock > 0) && (FD_ISSET(conn_sock, &p->ready_set))) {
p->nready--; p->nready--;
if ((n = recv(conn_sock, buf, MAXLINE, 0)) != 0) { if ((n = recv(conn_sock, buf, MAXLINE, 0)) != 0) {
printf("Server received %d bytes on fd %d\n", n, conn_sock); printf("Server received %d bytes on fd %d\n", n, conn_sock);
send(conn_sock, buf, n, 0); send(conn_sock, buf, n, 0);
} else { } else {
close(conn_sock); close(conn_sock);
FD_CLR(conn_sock, &p->read_set); FD_CLR(conn_sock, &p->read_set);
p->client_sock[i] = -1; p->client_sock[i] = -1;
} }
} }
} }
} }
``` ```
--- ---
## 🔗 知识关联 ## 🔗 知识关联
- 多进程模型在 [[06_进程控制]] 中有详细讲解 - 多进程模型在 [[06_进程控制]] 中有详细讲解
- 多线程模型在 [[07_多线程编程]] 中有详细讲解 - 多线程模型在 [[07_多线程编程]] 中有详细讲解
- I/O 多路复用在 [[17_IO系统]] 中有更深入的讨论 - I/O 多路复用在 [[17_IO系统]] 中有更深入的讨论
--- ---
## 📝 思考题 ## 📝 思考题
1. **为什么 select 有 FD_SETSIZE 限制?** 如何突破这个限制? 1. **为什么 select 有 FD_SETSIZE 限制?** 如何突破这个限制?
2. **多进程 vs 多线程**:在什么情况下多进程比多线程更合适? 2. **多进程 vs 多线程**:在什么情况下多进程比多线程更合适?
3. **epoll 的优势**:为什么 Linux 推荐使用 epoll 而不是 select 3. **epoll 的优势**:为什么 Linux 推荐使用 epoll 而不是 select
--- ---
## 📚 扩展阅读 ## 📚 扩展阅读
- 《UNIX网络编程》第1卷第6章、第16章 - 《UNIX网络编程》第1卷第6章、第16章
- [epoll 详解](https://man7.org/linux/man-pages/man7/epoll.7.html) - [epoll 详解](https://man7.org/linux/man-pages/man7/epoll.7.html)
- [高性能网络编程](https://www.zhihu.com/question/28594409) - [高性能网络编程](https://www.zhihu.com/question/28594409)

File diff suppressed because it is too large Load Diff

View File

@@ -1,376 +1,376 @@
# 第12讲死锁 # 第12讲死锁
> 🎯 **本节目标**:理解死锁的概念,掌握死锁的预防、避免和检测方法 > 🎯 **本节目标**:理解死锁的概念,掌握死锁的预防、避免和检测方法
## 📋 前置知识 ## 📋 前置知识
- [[07_多线程编程]] — 互斥锁和信号量 - [[07_多线程编程]] — 互斥锁和信号量
- [[11_处理机调度]] — 调度的基本概念 - [[11_处理机调度]] — 调度的基本概念
--- ---
## 🤔 为什么需要这个? ## 🤔 为什么需要这个?
想象这样一个场景: 想象这样一个场景:
- 线程 A 持有锁 1等待锁 2 - 线程 A 持有锁 1等待锁 2
- 线程 B 持有锁 2等待锁 1 - 线程 B 持有锁 2等待锁 1
- 两个线程互相等待,永远无法继续 - 两个线程互相等待,永远无法继续
这就是**死锁**——多个进程互相等待对方释放资源,导致所有进程都无法继续执行。 这就是**死锁**——多个进程互相等待对方释放资源,导致所有进程都无法继续执行。
**生活比喻** **生活比喻**
- **死锁** = 两个人在狭窄的走廊相遇,谁都不肯让路,结果谁也过不去 - **死锁** = 两个人在狭窄的走廊相遇,谁都不肯让路,结果谁也过不去
--- ---
## 📖 核心概念 ## 📖 核心概念
### 1. 死锁的四个必要条件 ### 1. 死锁的四个必要条件
```mermaid ```mermaid
graph TD graph TD
A[死锁四个条件] --> B[互斥条件] A[死锁四个条件] --> B[互斥条件]
A --> C[持有并等待] A --> C[持有并等待]
A --> D[不可抢占] A --> D[不可抢占]
A --> E[循环等待] A --> E[循环等待]
B --> B1[资源一次只能被一个进程使用] B --> B1[资源一次只能被一个进程使用]
C --> C1[进程持有资源的同时请求新资源] C --> C1[进程持有资源的同时请求新资源]
D --> D1[资源不能被强制剥夺] D --> D1[资源不能被强制剥夺]
E --> E1[存在进程的循环等待链] E --> E1[存在进程的循环等待链]
style A fill:#ffcdd2 style A fill:#ffcdd2
``` ```
**必须同时满足这四个条件才会发生死锁** **必须同时满足这四个条件才会发生死锁**
### 2. 资源分配图 ### 2. 资源分配图
```mermaid ```mermaid
graph LR graph LR
subgraph 进程 subgraph 进程
P1[P1] P1[P1]
P2[P2] P2[P2]
end end
subgraph 资源 subgraph 资源
R1[R1] R1[R1]
R2[R2] R2[R2]
end end
R1 -->|分配| P1 R1 -->|分配| P1
R2 -->|分配| P2 R2 -->|分配| P2
P1 -->|请求| R2 P1 -->|请求| R2
P2 -->|请求| R1 P2 -->|请求| R1
style P1 fill:#e1f5fe style P1 fill:#e1f5fe
style P2 fill:#e1f5fe style P2 fill:#e1f5fe
style R1 fill:#e8f5e9 style R1 fill:#e8f5e9
style R2 fill:#e8f5e9 style R2 fill:#e8f5e9
``` ```
**图例** **图例**
- 方框表示资源,圆圈表示进程 - 方框表示资源,圆圈表示进程
- 资源→进程:已分配 - 资源→进程:已分配
- 进程→资源:请求中 - 进程→资源:请求中
### 3. 死锁的处理策略 ### 3. 死锁的处理策略
```mermaid ```mermaid
graph TD graph TD
A[死锁处理策略] --> B[死锁预防] A[死锁处理策略] --> B[死锁预防]
A --> C[死锁避免] A --> C[死锁避免]
A --> D[死锁检测] A --> D[死锁检测]
A --> E[死锁恢复] A --> E[死锁恢复]
B --> B1[破坏四个条件之一] B --> B1[破坏四个条件之一]
C --> C1[银行家算法] C --> C1[银行家算法]
D --> D1[资源分配图] D --> D1[资源分配图]
E --> E1[终止进程] E --> E1[终止进程]
style B fill:#e8f5e9 style B fill:#e8f5e9
style C fill:#e1f5fe style C fill:#e1f5fe
style D fill:#fff3e0 style D fill:#fff3e0
style E fill:#ffcdd2 style E fill:#ffcdd2
``` ```
### 4. 死锁预防 ### 4. 死锁预防
破坏死锁的四个必要条件之一: 破坏死锁的四个必要条件之一:
| 条件 | 破坏方法 | 代价 | | 条件 | 破坏方法 | 代价 |
|------|----------|------| |------|----------|------|
| 互斥 | 使用可共享资源 | 不总是可行 | | 互斥 | 使用可共享资源 | 不总是可行 |
| 持有并等待 | 一次性申请所有资源 | 资源浪费 | | 持有并等待 | 一次性申请所有资源 | 资源浪费 |
| 不可抢占 | 允许抢占资源 | 实现复杂 | | 不可抢占 | 允许抢占资源 | 实现复杂 |
| 循环等待 | 按顺序申请资源 | 限制灵活性 | | 循环等待 | 按顺序申请资源 | 限制灵活性 |
**资源有序分配法** **资源有序分配法**
```c ```c
// 规定所有进程必须按编号顺序申请资源 // 规定所有进程必须按编号顺序申请资源
// 例如先申请锁1再申请锁2 // 例如先申请锁1再申请锁2
pthread_mutex_lock(&mutex1); // 正确 pthread_mutex_lock(&mutex1); // 正确
pthread_mutex_lock(&mutex2); pthread_mutex_lock(&mutex2);
// 而不是 // 而不是
pthread_mutex_lock(&mutex2); // 可能导致死锁 pthread_mutex_lock(&mutex2); // 可能导致死锁
pthread_mutex_lock(&mutex1); pthread_mutex_lock(&mutex1);
``` ```
### 5. 银行家算法 ### 5. 银行家算法
```mermaid ```mermaid
graph TD graph TD
A[银行家算法] --> B[检查请求是否安全] A[银行家算法] --> B[检查请求是否安全]
B --> C{安全?} B --> C{安全?}
C -->|是| D[分配资源] C -->|是| D[分配资源]
C -->|否| E[等待] C -->|否| E[等待]
D --> F[更新数据结构] D --> F[更新数据结构]
E --> G[阻塞进程] E --> G[阻塞进程]
style C fill:#fff3e0 style C fill:#fff3e0
style D fill:#e8f5e9 style D fill:#e8f5e9
style E fill:#ffcdd2 style E fill:#ffcdd2
``` ```
**安全状态**:存在一个安全序列,使得所有进程都能顺利完成 **安全状态**:存在一个安全序列,使得所有进程都能顺利完成
**数据结构** **数据结构**
- `Available[]`:可用资源向量 - `Available[]`:可用资源向量
- `Max[][]`:最大需求矩阵 - `Max[][]`:最大需求矩阵
- `Allocation[][]`:已分配矩阵 - `Allocation[][]`:已分配矩阵
- `Need[][]`:还需要的资源矩阵 - `Need[][]`:还需要的资源矩阵
**算法步骤** **算法步骤**
1. 检查请求是否超过需要 1. 检查请求是否超过需要
2. 检查请求是否超过可用资源 2. 检查请求是否超过可用资源
3. 尝试分配,检查是否安全 3. 尝试分配,检查是否安全
4. 如果安全,正式分配;否则等待 4. 如果安全,正式分配;否则等待
### 6. 死锁检测 ### 6. 死锁检测
```mermaid ```mermaid
graph TD graph TD
A[构建资源分配图] --> B[寻找环路] A[构建资源分配图] --> B[寻找环路]
B --> C{有环路?} B --> C{有环路?}
C -->|是| D[可能存在死锁] C -->|是| D[可能存在死锁]
C -->|否| E[无死锁] C -->|否| E[无死锁]
D --> F[进一步分析] D --> F[进一步分析]
style C fill:#fff3e0 style C fill:#fff3e0
style D fill:#ffcdd2 style D fill:#ffcdd2
style E fill:#e8f5e9 style E fill:#e8f5e9
``` ```
**检测算法** **检测算法**
1. 构建资源分配图 1. 构建资源分配图
2. 使用深度优先搜索寻找环路 2. 使用深度优先搜索寻找环路
3. 如果存在环路,可能存在死锁 3. 如果存在环路,可能存在死锁
### 7. 死锁恢复 ### 7. 死锁恢复
```mermaid ```mermaid
graph TD graph TD
A[检测到死锁] --> B[选择终止进程] A[检测到死锁] --> B[选择终止进程]
B --> C[回滚操作] B --> C[回滚操作]
C --> D[释放资源] C --> D[释放资源]
D --> E[唤醒等待进程] D --> E[唤醒等待进程]
style A fill:#ffcdd2 style A fill:#ffcdd2
style E fill:#e8f5e9 style E fill:#e8f5e9
``` ```
**恢复方法** **恢复方法**
- **终止所有死锁进程**:简单但代价大 - **终止所有死锁进程**:简单但代价大
- **逐个终止进程**:直到死锁解除 - **逐个终止进程**:直到死锁解除
- **资源抢占**:强制剥夺资源 - **资源抢占**:强制剥夺资源
--- ---
## 💻 动手实践 ## 💻 动手实践
### 示例1死锁演示 ### 示例1死锁演示
```c ```c
// deadlock_demo.c - 死锁演示 // deadlock_demo.c - 死锁演示
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <pthread.h> #include <pthread.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void *thread1(void *arg) { void *thread1(void *arg) {
pthread_mutex_lock(&mutex1); // 获取锁1 pthread_mutex_lock(&mutex1); // 获取锁1
printf("Thread 1: 持有锁1等待锁2...\n"); printf("Thread 1: 持有锁1等待锁2...\n");
sleep(1); // 等待让线程2获取锁2 sleep(1); // 等待让线程2获取锁2
pthread_mutex_lock(&mutex2); // 等待锁2死锁 pthread_mutex_lock(&mutex2); // 等待锁2死锁
printf("Thread 1: 获取到锁2\n"); printf("Thread 1: 获取到锁2\n");
pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex1);
return NULL; return NULL;
} }
void *thread2(void *arg) { void *thread2(void *arg) {
pthread_mutex_lock(&mutex2); // 获取锁2 pthread_mutex_lock(&mutex2); // 获取锁2
printf("Thread 2: 持有锁2等待锁1...\n"); printf("Thread 2: 持有锁2等待锁1...\n");
sleep(1); // 等待让线程1获取锁1 sleep(1); // 等待让线程1获取锁1
pthread_mutex_lock(&mutex1); // 等待锁1死锁 pthread_mutex_lock(&mutex1); // 等待锁1死锁
printf("Thread 2: 获取到锁1\n"); printf("Thread 2: 获取到锁1\n");
pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex1);
pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex2);
return NULL; return NULL;
} }
int main() { int main() {
pthread_t tid1, tid2; pthread_t tid1, tid2;
pthread_create(&tid1, NULL, thread1, NULL); pthread_create(&tid1, NULL, thread1, NULL);
pthread_create(&tid2, NULL, thread2, NULL); pthread_create(&tid2, NULL, thread2, NULL);
pthread_join(tid1, NULL); pthread_join(tid1, NULL);
pthread_join(tid2, NULL); pthread_join(tid2, NULL);
printf("程序正常结束(如果这里能打印说明没有死锁)\n"); printf("程序正常结束(如果这里能打印说明没有死锁)\n");
return 0; return 0;
} }
``` ```
**编译运行** **编译运行**
```bash ```bash
gcc -o deadlock deadlock_demo.c -lpthread gcc -o deadlock deadlock_demo.c -lpthread
./deadlock ./deadlock
``` ```
**预期结果**:程序会卡住,无法正常结束(死锁) **预期结果**:程序会卡住,无法正常结束(死锁)
### 示例2避免死锁 ### 示例2避免死锁
```c ```c
// no_deadlock.c - 避免死锁(按顺序获取锁) // no_deadlock.c - 避免死锁(按顺序获取锁)
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <pthread.h> #include <pthread.h>
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER; pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
void *thread1(void *arg) { void *thread1(void *arg) {
pthread_mutex_lock(&mutex1); // 先获取锁1 pthread_mutex_lock(&mutex1); // 先获取锁1
printf("Thread 1: 持有锁1\n"); printf("Thread 1: 持有锁1\n");
sleep(1); sleep(1);
pthread_mutex_lock(&mutex2); // 再获取锁2 pthread_mutex_lock(&mutex2); // 再获取锁2
printf("Thread 1: 持有锁1和锁2\n"); printf("Thread 1: 持有锁1和锁2\n");
pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex1);
return NULL; return NULL;
} }
void *thread2(void *arg) { void *thread2(void *arg) {
pthread_mutex_lock(&mutex1); // 也先获取锁1顺序一致 pthread_mutex_lock(&mutex1); // 也先获取锁1顺序一致
printf("Thread 2: 持有锁1\n"); printf("Thread 2: 持有锁1\n");
sleep(1); sleep(1);
pthread_mutex_lock(&mutex2); // 再获取锁2 pthread_mutex_lock(&mutex2); // 再获取锁2
printf("Thread 2: 持有锁1和锁2\n"); printf("Thread 2: 持有锁1和锁2\n");
pthread_mutex_unlock(&mutex2); pthread_mutex_unlock(&mutex2);
pthread_mutex_unlock(&mutex1); pthread_mutex_unlock(&mutex1);
return NULL; return NULL;
} }
int main() { int main() {
pthread_t tid1, tid2; pthread_t tid1, tid2;
pthread_create(&tid1, NULL, thread1, NULL); pthread_create(&tid1, NULL, thread1, NULL);
pthread_create(&tid2, NULL, thread2, NULL); pthread_create(&tid2, NULL, thread2, NULL);
pthread_join(tid1, NULL); pthread_join(tid1, NULL);
pthread_join(tid2, NULL); pthread_join(tid2, NULL);
printf("程序正常结束\n"); printf("程序正常结束\n");
return 0; return 0;
} }
``` ```
### 示例3银行家算法模拟 ### 示例3银行家算法模拟
```python ```python
# banker.py - 银行家算法模拟 # banker.py - 银行家算法模拟
def is_safe(available, max_need, allocation): def is_safe(available, max_need, allocation):
"""检查系统是否处于安全状态""" """检查系统是否处于安全状态"""
n = len(allocation) # 进程数 n = len(allocation) # 进程数
m = len(available) # 资源类型数 m = len(available) # 资源类型数
# 计算 Need 矩阵 # 计算 Need 矩阵
need = [[max_need[i][j] - allocation[i][j] for j in range(m)] for i in range(n)] need = [[max_need[i][j] - allocation[i][j] for j in range(m)] for i in range(n)]
# 初始化工作向量和完成标志 # 初始化工作向量和完成标志
work = available.copy() work = available.copy()
finish = [False] * n finish = [False] * n
safe_seq = [] safe_seq = []
while len(safe_seq) < n: while len(safe_seq) < n:
found = False found = False
for i in range(n): for i in range(n):
if not finish[i]: if not finish[i]:
# 检查是否可以分配 # 检查是否可以分配
if all(need[i][j] <= work[j] for j in range(m)): if all(need[i][j] <= work[j] for j in range(m)):
# 模拟分配 # 模拟分配
for j in range(m): for j in range(m):
work[j] += allocation[i][j] work[j] += allocation[i][j]
finish[i] = True finish[i] = True
safe_seq.append(i) safe_seq.append(i)
found = True found = True
if not found: if not found:
return False, [] # 不安全 return False, [] # 不安全
return True, safe_seq return True, safe_seq
# 测试数据 # 测试数据
available = [3, 3, 2] available = [3, 3, 2]
max_need = [ max_need = [
[7, 5, 3], [7, 5, 3],
[3, 2, 2], [3, 2, 2],
[9, 0, 2], [9, 0, 2],
[2, 2, 2], [2, 2, 2],
[4, 3, 3] [4, 3, 3]
] ]
allocation = [ allocation = [
[0, 1, 0], [0, 1, 0],
[2, 0, 0], [2, 0, 0],
[3, 0, 2], [3, 0, 2],
[2, 1, 1], [2, 1, 1],
[0, 0, 2] [0, 0, 2]
] ]
safe, seq = is_safe(available, max_need, allocation) safe, seq = is_safe(available, max_need, allocation)
if safe: if safe:
print(f"系统安全,安全序列: {seq}") print(f"系统安全,安全序列: {seq}")
else: else:
print("系统不安全") print("系统不安全")
``` ```
--- ---
## 🔗 知识关联 ## 🔗 知识关联
- 互斥锁在 [[07_多线程编程]] 中有详细讲解 - 互斥锁在 [[07_多线程编程]] 中有详细讲解
- 资源分配在 [[05_磁盘空间管理]] 中有类似概念 - 资源分配在 [[05_磁盘空间管理]] 中有类似概念
- 银行家算法在 [[14_分页存储管理]] 中的页面置换有类似思想 - 银行家算法在 [[14_分页存储管理]] 中的页面置换有类似思想
--- ---
## 📝 思考题 ## 📝 思考题
1. **为什么需要同时满足四个条件?** 如果只有三个条件满足会怎样? 1. **为什么需要同时满足四个条件?** 如果只有三个条件满足会怎样?
2. **银行家算法的局限性**:为什么它在实际系统中很少使用? 2. **银行家算法的局限性**:为什么它在实际系统中很少使用?
3. **鸵鸟策略**:为什么有些系统选择忽略死锁问题? 3. **鸵鸟策略**:为什么有些系统选择忽略死锁问题?
--- ---
## 📚 扩展阅读 ## 📚 扩展阅读
- 《操作系统概念》第7章死锁 - 《操作系统概念》第7章死锁
- 《现代操作系统》第6章死锁 - 《现代操作系统》第6章死锁
- [死锁检测算法](https://www.geeksforgeeks.org/deadlock-detection-algorithm/) - [死锁检测算法](https://www.geeksforgeeks.org/deadlock-detection-algorithm/)

View File

@@ -1,303 +1,303 @@
# 13. 存储管理基础 # 13. 存储管理基础
> **课程**: 操作系统 - 存储器管理 > **课程**: 操作系统 - 存储器管理
> **核心内容**: 存储器层次结构、存储管理功能、程序编译链接与装入、地址空间 > **核心内容**: 存储器层次结构、存储管理功能、程序编译链接与装入、地址空间
--- ---
## 一、存储器层次结构 ## 一、存储器层次结构
存储器按照速度和容量形成层次结构越靠近CPU速度越快但容量越小、价格越高。 存储器按照速度和容量形成层次结构越靠近CPU速度越快但容量越小、价格越高。
``` ```
速度递增 ↑ 容量递减 ↑ 价格递增 ↑ 速度递增 ↑ 容量递减 ↑ 价格递增 ↑
┌─────────────────────────┐ ┌─────────────────────────┐
│ 寄存器(Register) │ ← 最快,纳秒级,几十~几百字节 │ 寄存器(Register) │ ← 最快,纳秒级,几十~几百字节
├─────────────────────────┤ ├─────────────────────────┤
│ 高速缓存(Cache) │ ← L1/L2/L3几MB │ 高速缓存(Cache) │ ← L1/L2/L3几MB
├─────────────────────────┤ ├─────────────────────────┤
│ 主存(内存/DRAM) │ ← 几GB~几百GB百纳秒级 │ 主存(内存/DRAM) │ ← 几GB~几百GB百纳秒级
├─────────────────────────┤ ├─────────────────────────┤
│ Flash/SSD │ ← 固态存储 │ Flash/SSD │ ← 固态存储
├─────────────────────────┤ ├─────────────────────────┤
│ 磁盘缓存(Disk Cache) │ │ 磁盘缓存(Disk Cache) │
├─────────────────────────┤ ├─────────────────────────┤
│ 固定磁盘(HDD/SSD) │ ← 几百GB~几TB毫秒级 │ 固定磁盘(HDD/SSD) │ ← 几百GB~几TB毫秒级
├─────────────────────────┤ ├─────────────────────────┤
│ 可移动存储(U盘/光盘/磁带) │ ← 最慢,容量可很大 │ 可移动存储(U盘/光盘/磁带) │ ← 最慢,容量可很大
└─────────────────────────┘ └─────────────────────────┘
``` ```
**设计原则**: 利用**局部性原理**,将频繁访问的数据放在高速层次,较少访问的数据放在低速大容量层次。 **设计原则**: 利用**局部性原理**,将频繁访问的数据放在高速层次,较少访问的数据放在低速大容量层次。
--- ---
## 二、存储管理的功能 ## 二、存储管理的功能
操作系统存储管理需要实现以下五大核心功能: 操作系统存储管理需要实现以下五大核心功能:
| 功能 | 说明 | | 功能 | 说明 |
|------|------| |------|------|
| **内存分配与回收** | 为进程分配所需内存,进程结束后回收内存 | | **内存分配与回收** | 为进程分配所需内存,进程结束后回收内存 |
| **地址转换** | 将程序中的逻辑地址转换为物理地址([[14_分页存储管理\|分页]]、[[15_段式存储管理\|分段]] | | **地址转换** | 将程序中的逻辑地址转换为物理地址([[14_分页存储管理\|分页]]、[[15_段式存储管理\|分段]] |
| **内存共享** | 多个进程共享同一段代码(如共享库) | | **内存共享** | 多个进程共享同一段代码(如共享库) |
| **内存保护** | 防止进程越界访问其他进程或内核的内存区域 | | **内存保护** | 防止进程越界访问其他进程或内核的内存区域 |
| **内存扩充** | 通过虚拟存储技术,使程序可用空间大于实际物理内存([[16_虚拟存储器\|虚拟存储器]] | | **内存扩充** | 通过虚拟存储技术,使程序可用空间大于实际物理内存([[16_虚拟存储器\|虚拟存储器]] |
--- ---
## 三、程序的编译与链接过程 ## 三、程序的编译与链接过程
一个C语言源文件从编写到可执行经历以下阶段 一个C语言源文件从编写到可执行经历以下阶段
```mermaid ```mermaid
flowchart LR flowchart LR
A["hello.c<br/>源文件"] -->|预处理| B["hello.i<br/>预处理后"] A["hello.c<br/>源文件"] -->|预处理| B["hello.i<br/>预处理后"]
B -->|编译| C["hello.s<br/>汇编文件"] B -->|编译| C["hello.s<br/>汇编文件"]
C -->|汇编| D["hello.o<br/>目标文件(可重定位)"] C -->|汇编| D["hello.o<br/>目标文件(可重定位)"]
D -->|链接| E["hello / a.out<br/>可执行文件"] D -->|链接| E["hello / a.out<br/>可执行文件"]
E -->|装入| F["内存中运行的进程"] E -->|装入| F["内存中运行的进程"]
style A fill:#e1f5fe style A fill:#e1f5fe
style C fill:#fff3e0 style C fill:#fff3e0
style D fill:#fce4ec style D fill:#fce4ec
style E fill:#e8f5e9 style E fill:#e8f5e9
style F fill:#f3e5f5 style F fill:#f3e5f5
``` ```
### 各阶段说明 ### 各阶段说明
| 阶段 | 输入 | 输出 | 工具 | 说明 | | 阶段 | 输入 | 输出 | 工具 | 说明 |
|------|------|------|------|------| |------|------|------|------|------|
| **预处理** | `.c` | `.i` | cpp | 展开宏、头文件、条件编译 | | **预处理** | `.c` | `.i` | cpp | 展开宏、头文件、条件编译 |
| **编译** | `.i` | `.s` | cc1 | 翻译为汇编语言 | | **编译** | `.i` | `.s` | cc1 | 翻译为汇编语言 |
| **汇编** | `.s` | `.o` | as | 翻译为机器指令(可重定位目标文件) | | **汇编** | `.s` | `.o` | as | 翻译为机器指令(可重定位目标文件) |
| **链接** | `.o` | 可执行文件 | ld | 合并节段、解析符号引用、重定位 | | **链接** | `.o` | 可执行文件 | ld | 合并节段、解析符号引用、重定位 |
| **装入** | 可执行文件 | 进程 | 加载器 | 将程序载入内存并创建进程 | | **装入** | 可执行文件 | 进程 | 加载器 | 将程序载入内存并创建进程 |
### 查看目标文件的节段 ### 查看目标文件的节段
使用 `objdump -h` 可以查看可执行文件或目标文件中的节段(Section)信息: 使用 `objdump -h` 可以查看可执行文件或目标文件中的节段(Section)信息:
```bash ```bash
gcc -c hello.c # 编译为可重定位目标文件 gcc -c hello.c # 编译为可重定位目标文件
objdump -h hello.o # 查看节段头信息 objdump -h hello.o # 查看节段头信息
gcc hello.c -o hello # 链接为可执行文件 gcc hello.c -o hello # 链接为可执行文件
objdump -h hello # 查看可执行文件的节段 objdump -h hello # 查看可执行文件的节段
``` ```
--- ---
## 四、可执行文件结构 ## 四、可执行文件结构
可执行文件在内存中从低地址到高地址的典型布局如下: 可执行文件在内存中从低地址到高地址的典型布局如下:
``` ```
高地址 ┌──────────────────┐ 高地址 ┌──────────────────┐
│ 命令行参数 │ │ 命令行参数 │
│ 和环境变量 │ │ 和环境变量 │
├──────────────────┤ ├──────────────────┤
│ 栈(Stack) │ ← 局部变量、函数调用帧,向下增长 ↓ │ 栈(Stack) │ ← 局部变量、函数调用帧,向下增长 ↓
│ ↓ │ │ ↓ │
│ │ │ │
│ ↑ │ │ ↑ │
│ 堆(Heap) │ ← malloc/new动态分配向上增长 ↑ │ 堆(Heap) │ ← malloc/new动态分配向上增长 ↑
├──────────────────┤ ├──────────────────┤
│ .bss 段 │ ← 未初始化的全局/静态变量(不占文件空间) │ .bss 段 │ ← 未初始化的全局/静态变量(不占文件空间)
├──────────────────┤ ├──────────────────┤
│ .data 段 │ ← 已初始化的全局变量和静态变量 │ .data 段 │ ← 已初始化的全局变量和静态变量
├──────────────────┤ ├──────────────────┤
│ .rodata 段 │ ← 只读数据(如字符串常量) │ .rodata 段 │ ← 只读数据(如字符串常量)
├──────────────────┤ ├──────────────────┤
│ .text 段 │ ← 可执行代码(机器指令) │ .text 段 │ ← 可执行代码(机器指令)
低地址 └──────────────────┘ 低地址 └──────────────────┘
``` ```
| 段名 | 内容 | 是否可写 | 说明 | | 段名 | 内容 | 是否可写 | 说明 |
|------|------|---------|------| |------|------|---------|------|
| `.text` | 机器指令代码 | 只读/只执行 | 程序的可执行代码 | | `.text` | 机器指令代码 | 只读/只执行 | 程序的可执行代码 |
| `.rodata` | 只读数据 | 只读 | 字符串常量、`const`变量 | | `.rodata` | 只读数据 | 只读 | 字符串常量、`const`变量 |
| `.data` | 已初始化全局变量 | 可读写 | 有初始值的全局和静态变量 | | `.data` | 已初始化全局变量 | 可读写 | 有初始值的全局和静态变量 |
| `.bss` | 未初始化全局变量 | 可读写 | 不占文件空间,装入时清零 | | `.bss` | 未初始化全局变量 | 可读写 | 不占文件空间,装入时清零 |
| 堆(Heap) | 动态分配内存 | 可读写 | `malloc`/`new` 分配 | | 堆(Heap) | 动态分配内存 | 可读写 | `malloc`/`new` 分配 |
| 栈(Stack) | 函数调用帧 | 可读写 | 局部变量、返回地址等 | | 栈(Stack) | 函数调用帧 | 可读写 | 局部变量、返回地址等 |
--- ---
## 五、逻辑地址与物理地址 ## 五、逻辑地址与物理地址
### 核心概念 ### 核心概念
| 术语 | 别名 | 说明 | | 术语 | 别名 | 说明 |
|------|------|------| |------|------|------|
| **逻辑地址** | 虚拟地址(VA)、相对地址 | 程序中使用的地址从0开始编址 | | **逻辑地址** | 虚拟地址(VA)、相对地址 | 程序中使用的地址从0开始编址 |
| **物理地址** | PA、绝对地址 | 内存硬件中的实际地址 | | **物理地址** | PA、绝对地址 | 内存硬件中的实际地址 |
> **关键**: 程序中的地址(逻辑地址)**不等于**内存中的实际地址(物理地址)。地址转换由硬件([[01_系统运行机制#四、存储器管理硬件——MMU|MMU]])和操作系统配合完成。 > **关键**: 程序中的地址(逻辑地址)**不等于**内存中的实际地址(物理地址)。地址转换由硬件([[01_系统运行机制#四、存储器管理硬件——MMU|MMU]])和操作系统配合完成。
### 逻辑地址空间与物理地址空间 ### 逻辑地址空间与物理地址空间
```mermaid ```mermaid
flowchart TB flowchart TB
subgraph VA["逻辑地址空间 (虚拟)"] subgraph VA["逻辑地址空间 (虚拟)"]
direction TB direction TB
V0["地址 0"] V0["地址 0"]
V1["..."] V1["..."]
V2["地址 2^v - 1"] V2["地址 2^v - 1"]
end end
subgraph PA["物理地址空间 (实际)"] subgraph PA["物理地址空间 (实际)"]
direction TB direction TB
P0["地址 0"] P0["地址 0"]
P1["..."] P1["..."]
P2["地址 2^p - 1"] P2["地址 2^p - 1"]
end end
VA -->|"地址映射<br/>(页表/段表)"| PA VA -->|"地址映射<br/>(页表/段表)"| PA
style VA fill:#e3f2fd,stroke:#1976d2 style VA fill:#e3f2fd,stroke:#1976d2
style PA fill:#fff8e1,stroke:#f9a825 style PA fill:#fff8e1,stroke:#f9a825
``` ```
- **虚拟地址空间**: 大小为 $2^v$ 字节,其中 $v$ 是虚拟地址的位数 - **虚拟地址空间**: 大小为 $2^v$ 字节,其中 $v$ 是虚拟地址的位数
- **物理地址空间**: 大小为 $2^p$ 字节,其中 $p$ 是物理地址的位数 - **物理地址空间**: 大小为 $2^p$ 字节,其中 $p$ 是物理地址的位数
- 通常 $v \geq p$(虚拟地址空间可以大于物理内存),这就是[[16_虚拟存储器|虚拟存储器]]的基础 - 通常 $v \geq p$(虚拟地址空间可以大于物理内存),这就是[[16_虚拟存储器|虚拟存储器]]的基础
### 引入逻辑地址的好处 ### 引入逻辑地址的好处
1. **进程隔离**: 每个进程拥有独立的虚拟地址空间,互不干扰 1. **进程隔离**: 每个进程拥有独立的虚拟地址空间,互不干扰
2. **提高内存利用率**: 可以使用[[16_虚拟存储器|虚拟存储器]]技术 2. **提高内存利用率**: 可以使用[[16_虚拟存储器|虚拟存储器]]技术
3. **内存保护**: 通过地址转换实现访问权限控制 3. **内存保护**: 通过地址转换实现访问权限控制
--- ---
## 六、内核空间与用户空间 ## 六、内核空间与用户空间
操作系统将每个进程的虚拟地址空间划分为**用户空间**和**内核空间**两部分: 操作系统将每个进程的虚拟地址空间划分为**用户空间**和**内核空间**两部分:
```mermaid ```mermaid
block-beta block-beta
columns 1 columns 1
block:linux["Linux 进程地址空间 (4GB)"] block:linux["Linux 进程地址空间 (4GB)"]
columns 1 columns 1
block:kernel_linux["内核空间 (高 1GB)"] block:kernel_linux["内核空间 (高 1GB)"]
k1["内核代码、数据、内核栈等"] k1["内核代码、数据、内核栈等"]
end end
block:user_linux["用户空间 (低 3GB)"] block:user_linux["用户空间 (低 3GB)"]
u1["栈 ↓"] u1["栈 ↓"]
u2["..."] u2["..."]
u3["堆 ↑"] u3["堆 ↑"]
u4[".bss / .data / .rodata / .text"] u4[".bss / .data / .rodata / .text"]
end end
end end
block:win["Windows 进程地址空间 (4GB)"] block:win["Windows 进程地址空间 (4GB)"]
columns 1 columns 1
block:kernel_win["内核空间 (高 2GB)"] block:kernel_win["内核空间 (高 2GB)"]
k2["内核代码、驱动等"] k2["内核代码、驱动等"]
end end
block:user_win["用户空间 (低 2GB)"] block:user_win["用户空间 (低 2GB)"]
u5["用户程序空间"] u5["用户程序空间"]
end end
end end
``` ```
| 操作系统 | 用户空间 | 内核空间 | 说明 | | 操作系统 | 用户空间 | 内核空间 | 说明 |
|---------|---------|---------|------| |---------|---------|---------|------|
| **Linux (32位)** | 0 ~ 3GB (低3GB) | 3GB ~ 4GB (高1GB) | 通过`PAGE_OFFSET`划分 | | **Linux (32位)** | 0 ~ 3GB (低3GB) | 3GB ~ 4GB (高1GB) | 通过`PAGE_OFFSET`划分 |
| **Windows (32位)** | 0 ~ 2GB (低2GB) | 2GB ~ 4GB (高2GB) | 可通过`/3GB`启动参数调整为3:1 | | **Windows (32位)** | 0 ~ 2GB (低2GB) | 2GB ~ 4GB (高2GB) | 可通过`/3GB`启动参数调整为3:1 |
**重要规则**: 用户态程序**不能**直接访问内核空间的地址,否则触发保护异常。内核态代码可以访问整个地址空间。 **重要规则**: 用户态程序**不能**直接访问内核空间的地址,否则触发保护异常。内核态代码可以访问整个地址空间。
--- ---
## 七、MMU 与内存保护 ## 七、MMU 与内存保护
**MMU (Memory Management Unit)** 是CPU中负责地址转换和内存保护的硬件单元。 **MMU (Memory Management Unit)** 是CPU中负责地址转换和内存保护的硬件单元。
### 地址转换流程 ### 地址转换流程
```mermaid ```mermaid
flowchart LR flowchart LR
CPU["CPU 发出<br/>虚拟地址(VA)"] --> MMU["MMU<br/>地址转换"] CPU["CPU 发出<br/>虚拟地址(VA)"] --> MMU["MMU<br/>地址转换"]
MMU --> PA["物理地址(PA)"] MMU --> PA["物理地址(PA)"]
PA --> MEM["访问内存"] PA --> MEM["访问内存"]
style MMU fill:#ffcdd2,stroke:#c62828 style MMU fill:#ffcdd2,stroke:#c62828
``` ```
### 页表保护机制 ### 页表保护机制
MMU通过页表中的**U/S位**和CPU的**模式位**配合实现内存保护: MMU通过页表中的**U/S位**和CPU的**模式位**配合实现内存保护:
| 页表U/S位 | 含义 | | 页表U/S位 | 含义 |
|-----------|------| |-----------|------|
| **U=0 (Supervisor)** | 内核态页面,只有内核可以访问 | | **U=0 (Supervisor)** | 内核态页面,只有内核可以访问 |
| **U=1 (User)** | 用户态页面,用户态和内核态均可访问 | | **U=1 (User)** | 用户态页面,用户态和内核态均可访问 |
| CPU模式 | 可访问的页面 | | CPU模式 | 可访问的页面 |
|---------|-------------| |---------|-------------|
| **用户模式 (User mode)** | 只能访问 U=1 的页面 | | **用户模式 (User mode)** | 只能访问 U=1 的页面 |
| **内核模式 (Kernel mode)** | 可以访问 U=0 和 U=1 的页面 | | **内核模式 (Kernel mode)** | 可以访问 U=0 和 U=1 的页面 |
当用户态程序试图访问 U=0 的内核页面时MMU会产生**保护异常(段错误/Segmentation Fault**,终止该进程。 当用户态程序试图访问 U=0 的内核页面时MMU会产生**保护异常(段错误/Segmentation Fault**,终止该进程。
--- ---
## 八、地址转换的主要方式 ## 八、地址转换的主要方式
操作系统实现地址转换有多种方式,各有特点: 操作系统实现地址转换有多种方式,各有特点:
| 方式 | 原理 | 优点 | 缺点 | | 方式 | 原理 | 优点 | 缺点 |
|------|------|------|------| |------|------|------|------|
| **重定位寄存器(基址寄存器)** | PA = VA + 基址值 | 简单 | 程序必须连续存放 | | **重定位寄存器(基址寄存器)** | PA = VA + 基址值 | 简单 | 程序必须连续存放 |
| **静态重定位** | 装入时一次性修改所有地址 | 无需硬件支持 | 装入后不能移动 | | **静态重定位** | 装入时一次性修改所有地址 | 无需硬件支持 | 装入后不能移动 |
| **动态重定位** | 执行时通过MMU实时转换 | 灵活,支持移动 | 需要硬件支持 | | **动态重定位** | 执行时通过MMU实时转换 | 灵活,支持移动 | 需要硬件支持 |
| **[[14_分页存储管理\|分页]]** | 按页划分,通过页表映射 | 消除外部碎片 | 有内部碎片、页表开销 | | **[[14_分页存储管理\|分页]]** | 按页划分,通过页表映射 | 消除外部碎片 | 有内部碎片、页表开销 |
| **[[15_段式存储管理\|分段]]** | 按逻辑段划分,通过段表映射 | 符合程序逻辑 | 外部碎片问题 | | **[[15_段式存储管理\|分段]]** | 按逻辑段划分,通过段表映射 | 符合程序逻辑 | 外部碎片问题 |
--- ---
## 九、小结 ## 九、小结
```mermaid ```mermaid
mindmap mindmap
root((存储管理基础)) root((存储管理基础))
存储层次 存储层次
寄存器 寄存器
Cache Cache
主存 主存
Flash Flash
磁盘 磁盘
管理功能 管理功能
分配回收 分配回收
地址转换 地址转换
内存共享 内存共享
内存保护 内存保护
内存扩充 内存扩充
程序装入 程序装入
编译链接 编译链接
可执行文件结构 可执行文件结构
逻辑地址vs物理地址 逻辑地址vs物理地址
地址空间 地址空间
虚拟地址空间 虚拟地址空间
物理地址空间 物理地址空间
内核空间/用户空间 内核空间/用户空间
MMU保护 MMU保护
页表U/S位 页表U/S位
CPU模式位 CPU模式位
``` ```
--- ---
## 关联笔记 ## 关联笔记
- [[01_系统运行机制]] — CPU工作模式用户态/内核态与MMU硬件基础 - [[01_系统运行机制]] — CPU工作模式用户态/内核态与MMU硬件基础
- [[14_分页存储管理]] — 分页式地址转换的详细实现 - [[14_分页存储管理]] — 分页式地址转换的详细实现
- [[15_段式存储管理]] — 分段式地址转换 - [[15_段式存储管理]] — 分段式地址转换
- [[16_虚拟存储器]] — 虚拟存储器的实现原理 - [[16_虚拟存储器]] — 虚拟存储器的实现原理
--- ---
**上一讲**: [[01_系统运行机制]] **上一讲**: [[01_系统运行机制]]
**下一讲**: [[14_分页存储管理]] **下一讲**: [[14_分页存储管理]]

File diff suppressed because it is too large Load Diff

View File

@@ -1,398 +1,398 @@
# 15. 段式存储管理 # 15. 段式存储管理
> **课程**: 操作系统 - 存储器管理 > **课程**: 操作系统 - 存储器管理
> **核心内容**: 分段引入原因、分段思想、地址结构、段表、地址变换、段页式存储管理 > **核心内容**: 分段引入原因、分段思想、地址结构、段表、地址变换、段页式存储管理
--- ---
## 前置知识 ## 前置知识
- [[13_存储管理基础]] — 存储器层次结构、逻辑地址与物理地址 - [[13_存储管理基础]] — 存储器层次结构、逻辑地址与物理地址
- [[14_分页存储管理]] — 分页思想、页表、地址变换 - [[14_分页存储管理]] — 分页思想、页表、地址变换
--- ---
## 一、为什么需要分段 ## 一、为什么需要分段
[[14_分页存储管理|分页]]虽然解决了碎片问题,但在以下场景中存在不足: [[14_分页存储管理|分页]]虽然解决了碎片问题,但在以下场景中存在不足:
### 1. 信息共享不方便 ### 1. 信息共享不方便
分页按固定大小划分,不考虑程序的逻辑结构。如果要共享一段代码(如共享库函数),该代码可能跨越多个页面,其中某些页面还包含不需要共享的数据,导致共享粒度过粗。 分页按固定大小划分,不考虑程序的逻辑结构。如果要共享一段代码(如共享库函数),该代码可能跨越多个页面,其中某些页面还包含不需要共享的数据,导致共享粒度过粗。
``` ```
分页视角(按固定大小切分,不考虑逻辑含义): 分页视角(按固定大小切分,不考虑逻辑含义):
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ 代码1 │ │ 代码2+ │ │ 数据 │ ← 一个逻辑模块跨了3个页 │ 代码1 │ │ 代码2+ │ │ 数据 │ ← 一个逻辑模块跨了3个页
│ │ │ 常量 │ │ │ 共享时会把不相关的内容也共享了 │ │ │ 常量 │ │ │ 共享时会把不相关的内容也共享了
└────────┘ └────────┘ └────────┘ └────────┘ └────────┘ └────────┘
``` ```
### 2. 动态链接问题 ### 2. 动态链接问题
动态链接需要在运行时将目标模块装入内存并链接。分页系统中,模块的装入和链接以页为单位,不够灵活。 动态链接需要在运行时将目标模块装入内存并链接。分页系统中,模块的装入和链接以页为单位,不够灵活。
### 3. 程序员视角需求 ### 3. 程序员视角需求
程序员编写程序时,自然地将代码划分为**代码段、数据段、堆栈段**等逻辑模块。分页完全打乱了这种逻辑结构。 程序员编写程序时,自然地将代码划分为**代码段、数据段、堆栈段**等逻辑模块。分页完全打乱了这种逻辑结构。
> **核心需求**: 地址空间的划分应该按照**逻辑意义**进行,而非固定大小。这就是分段的思想。 > **核心需求**: 地址空间的划分应该按照**逻辑意义**进行,而非固定大小。这就是分段的思想。
--- ---
## 二、分段的基本思想 ## 二、分段的基本思想
分段按照程序的**逻辑结构**将地址空间划分为若干个段: 分段按照程序的**逻辑结构**将地址空间划分为若干个段:
- 每个段有独立的**段名**和**段长** - 每个段有独立的**段名**和**段长**
- 每个段在内存中**连续存放** - 每个段在内存中**连续存放**
- 不同段之间**不需要连续** - 不同段之间**不需要连续**
```mermaid ```mermaid
flowchart TB flowchart TB
subgraph VA["进程虚拟地址空间"] subgraph VA["进程虚拟地址空间"]
direction TB direction TB
S0["主程序段 (段0)"] S0["主程序段 (段0)"]
S1["子程序段 (段1)"] S1["子程序段 (段1)"]
S2["数据段 (段2)"] S2["数据段 (段2)"]
S3["栈段 (段3)"] S3["栈段 (段3)"]
end end
subgraph PM["物理内存"] subgraph PM["物理内存"]
direction TB direction TB
M0["区域A"] M0["区域A"]
M1["区域B"] M1["区域B"]
M2["区域C"] M2["区域C"]
M3["区域D"] M3["区域D"]
M4["空闲"] M4["空闲"]
M5["区域E"] M5["区域E"]
end end
S0 -->|"段表映射"| M5 S0 -->|"段表映射"| M5
S1 -->|"段表映射"| M0 S1 -->|"段表映射"| M0
S2 -->|"段表映射"| M2 S2 -->|"段表映射"| M2
S3 -->|"段表映射"| M3 S3 -->|"段表映射"| M3
style VA fill:#e3f2fd,stroke:#1976d2 style VA fill:#e3f2fd,stroke:#1976d2
style PM fill:#fff8e1,stroke:#f9a825 style PM fill:#fff8e1,stroke:#f9a825
``` ```
### 分段 vs 分页 ### 分段 vs 分页
| 对比项 | 分页 | 分段 | | 对比项 | 分页 | 分段 |
|--------|------|------| |--------|------|------|
| **划分依据** | 固定大小(物理需要) | 逻辑意义(程序员视角) | | **划分依据** | 固定大小(物理需要) | 逻辑意义(程序员视角) |
| **段/页大小** | 所有页等大 | 各段长度不同 | | **段/页大小** | 所有页等大 | 各段长度不同 |
| **地址空间** | 一维VA直接计算VPN和偏移 | **二维**(段号 + 段内偏移) | | **地址空间** | 一维VA直接计算VPN和偏移 | **二维**(段号 + 段内偏移) |
| **碎片类型** | 内碎片(页内浪费) | 外碎片(段间空闲区) | | **碎片类型** | 内碎片(页内浪费) | 外碎片(段间空闲区) |
| **共享** | 以页为粒度,较粗 | 以段为粒度,符合逻辑 | | **共享** | 以页为粒度,较粗 | 以段为粒度,符合逻辑 |
| **动态增长** | 不方便 | 方便(如堆、栈段可动态扩展) | | **动态增长** | 不方便 | 方便(如堆、栈段可动态扩展) |
--- ---
## 三、地址结构 ## 三、地址结构
分段系统的逻辑地址是**二维**的,由两部分组成: 分段系统的逻辑地址是**二维**的,由两部分组成:
``` ```
逻辑地址 = (段号 S, 段内地址 d) 逻辑地址 = (段号 S, 段内地址 d)
``` ```
``` ```
逻辑地址表示: 逻辑地址表示:
┌──────────────┬────────────────────┐ ┌──────────────┬────────────────────┐
│ 段号 S │ 段内地址 d │ │ 段号 S │ 段内地址 d │
│ (高位部分) │ (低位部分) │ │ (高位部分) │ (低位部分) │
└──────────────┴────────────────────┘ └──────────────┴────────────────────┘
注意: 不同于分页的VPN|VPO可以统一计算 注意: 不同于分页的VPN|VPO可以统一计算
分段中 d 的取值范围取决于段长,各段不同。 分段中 d 的取值范围取决于段长,各段不同。
``` ```
**地址表示示例**: **地址表示示例**:
- 分页地址 `(0x1A8F)` → 可直接算出 VPN=6, VPO=0x28F - 分页地址 `(0x1A8F)` → 可直接算出 VPN=6, VPO=0x28F
- 分段地址 `(2, 0x100)` → 段号=2偏移=0x100需要查段表才知道该段的基址和长度 - 分段地址 `(2, 0x100)` → 段号=2偏移=0x100需要查段表才知道该段的基址和长度
--- ---
## 四、段表 ## 四、段表
### 段表结构 ### 段表结构
每个进程拥有一张**段表**,段表以**段号**为索引,每项包含: 每个进程拥有一张**段表**,段表以**段号**为索引,每项包含:
| 字段 | 含义 | | 字段 | 含义 |
|------|------| |------|------|
| **段号** | 段的编号(隐含在索引中) | | **段号** | 段的编号(隐含在索引中) |
| **段长 (Limit)** | 该段的长度(字节数),用于越界检查 | | **段长 (Limit)** | 该段的长度(字节数),用于越界检查 |
| **段基址 (Base)** | 该段在物理内存中的起始地址 | | **段基址 (Base)** | 该段在物理内存中的起始地址 |
``` ```
段表示例: 段表示例:
┌──────┬────────────┬────────────────┐ ┌──────┬────────────┬────────────────┐
│ 段号 │ 段长(Limit) │ 基址(Base) │ │ 段号 │ 段长(Limit) │ 基址(Base) │
├──────┼────────────┼────────────────┤ ├──────┼────────────┼────────────────┤
│ 0 │ 0x2000 │ 0x4000 │ ← 主程序: 在内存0x4000处, 长8KB │ 0 │ 0x2000 │ 0x4000 │ ← 主程序: 在内存0x4000处, 长8KB
│ 1 │ 0x1000 │ 0x8000 │ ← 子程序: 在内存0x8000处, 长4KB │ 1 │ 0x1000 │ 0x8000 │ ← 子程序: 在内存0x8000处, 长4KB
│ 2 │ 0x3000 │ 0xB000 │ ← 数据段: 在内存0xB000处, 长12KB │ 2 │ 0x3000 │ 0xB000 │ ← 数据段: 在内存0xB000处, 长12KB
│ 3 │ 0x1800 │ 0x2000 │ ← 栈段: 在内存0x2000处, 长6KB │ 3 │ 0x1800 │ 0x2000 │ ← 栈段: 在内存0x2000处, 长6KB
└──────┴────────────┴────────────────┘ └──────┴────────────┴────────────────┘
``` ```
段表基址寄存器 **STBR** 指向段表在内存中的起始地址。 段表基址寄存器 **STBR** 指向段表在内存中的起始地址。
--- ---
## 五、地址变换过程 ## 五、地址变换过程
### 变换流程 ### 变换流程
```mermaid ```mermaid
flowchart TD flowchart TD
A["逻辑地址 (S, d)"] --> B["用STBR找到段表基址"] A["逻辑地址 (S, d)"] --> B["用STBR找到段表基址"]
B --> C["定位段表项: 段表基址 + S × 段表项大小"] B --> C["定位段表项: 段表基址 + S × 段表项大小"]
C --> D{"d < Limit ?"} C --> D{"d < Limit ?"}
D -->|"是"| E["物理地址 PA = Base + d"] D -->|"是"| E["物理地址 PA = Base + d"]
D -->|"否"| F["**越界中断**<br/>(Segmentation Fault)"] D -->|"否"| F["**越界中断**<br/>(Segmentation Fault)"]
E --> G["访问物理内存"] E --> G["访问物理内存"]
style F fill:#ffcdd2,stroke:#c62828 style F fill:#ffcdd2,stroke:#c62828
style D fill:#fff3e0,stroke:#e65100 style D fill:#fff3e0,stroke:#e65100
``` ```
### 详细步骤 ### 详细步骤
1. **提取段号和偏移**: 从逻辑地址中分离出段号 S 和段内地址 d 1. **提取段号和偏移**: 从逻辑地址中分离出段号 S 和段内地址 d
2. **查段表**: 用 STBR + S 定位到第 S 个段表项 2. **查段表**: 用 STBR + S 定位到第 S 个段表项
3. **越界检查**: 比较 d 与 Limit 3. **越界检查**: 比较 d 与 Limit
- 若 d >= Limit触发**越界中断**Segmentation Fault - 若 d >= Limit触发**越界中断**Segmentation Fault
- 若 d < Limit继续 - 若 d < Limit继续
4. **计算物理地址**: PA = Base + d 4. **计算物理地址**: PA = Base + d
5. **访问内存**: 用物理地址访问实际内存 5. **访问内存**: 用物理地址访问实际内存
### 地址变换计算示例 ### 地址变换计算示例
> **例题**: 某分段系统,段表如下。求以下逻辑地址对应的物理地址: > **例题**: 某分段系统,段表如下。求以下逻辑地址对应的物理地址:
> - (0, 0x1500) > - (0, 0x1500)
> - (1, 0x2000) > - (1, 0x2000)
> - (2, 0x0800) > - (2, 0x0800)
**解题过程**: **解题过程**:
| 逻辑地址 | S | d | Limit | 比较 | Base | PA = Base + d | 结果 | | 逻辑地址 | S | d | Limit | 比较 | Base | PA = Base + d | 结果 |
|---------|---|---|-------|------|------|--------------|------| |---------|---|---|-------|------|------|--------------|------|
| (0, 0x1500) | 0 | 0x1500 | 0x2000 | 0x1500 < 0x2000 ✓ | 0x4000 | 0x4000 + 0x1500 | **0x5500** | | (0, 0x1500) | 0 | 0x1500 | 0x2000 | 0x1500 < 0x2000 ✓ | 0x4000 | 0x4000 + 0x1500 | **0x5500** |
| (1, 0x2000) | 1 | 0x2000 | 0x1000 | 0x2000 >= 0x1000 ✗ | — | — | **越界中断** | | (1, 0x2000) | 1 | 0x2000 | 0x1000 | 0x2000 >= 0x1000 ✗ | — | — | **越界中断** |
| (2, 0x0800) | 2 | 0x0800 | 0x3000 | 0x0800 < 0x3000 ✓ | 0xB000 | 0xB000 + 0x0800 | **0xB800** | | (2, 0x0800) | 2 | 0x0800 | 0x3000 | 0x0800 < 0x3000 ✓ | 0xB000 | 0xB000 + 0x0800 | **0xB800** |
--- ---
## 六、分段的优缺点 ## 六、分段的优缺点
### 优点 ### 优点
| 优点 | 说明 | | 优点 | 说明 |
|------|------| |------|------|
| **符合程序逻辑** | 按代码、数据、栈等逻辑单元组织,便于理解和管理 | | **符合程序逻辑** | 按代码、数据、栈等逻辑单元组织,便于理解和管理 |
| **便于共享** | 以段为单位共享,粒度合理(如共享整个代码段) | | **便于共享** | 以段为单位共享,粒度合理(如共享整个代码段) |
| **便于动态链接** | 可以按段为单位进行动态链接和装入 | | **便于动态链接** | 可以按段为单位进行动态链接和装入 |
| **支持动态增长** | 堆、栈等段可以独立扩展,不影响其他段 | | **支持动态增长** | 堆、栈等段可以独立扩展,不影响其他段 |
| **保护自然** | 每段可设置独立的访问权限(代码段只读、数据段可读写等) | | **保护自然** | 每段可设置独立的访问权限(代码段只读、数据段可读写等) |
### 缺点 ### 缺点
| 缺点 | 说明 | | 缺点 | 说明 |
|------|------| |------|------|
| **外碎片** | 段长不等,内存分配/回收后产生不连续的空闲区 | | **外碎片** | 段长不等,内存分配/回收后产生不连续的空闲区 |
| **段长限制** | 每段最大长度受地址结构限制 | | **段长限制** | 每段最大长度受地址结构限制 |
| **内存紧缩开销** | 消除外碎片需要移动段(类似动态分区的紧凑操作) | | **内存紧缩开销** | 消除外碎片需要移动段(类似动态分区的紧凑操作) |
--- ---
## 七、段页式存储管理 ## 七、段页式存储管理
### 基本思想 ### 基本思想
**段页式** = 分段 + 分页,兼具两者的优点: **段页式** = 分段 + 分页,兼具两者的优点:
- 先按**逻辑结构分段**(保留分段的优点:共享、保护、逻辑清晰) - 先按**逻辑结构分段**(保留分段的优点:共享、保护、逻辑清晰)
- 再将每段**按固定大小分页**(保留分页的优点:消除外碎片) - 再将每段**按固定大小分页**(保留分页的优点:消除外碎片)
```mermaid ```mermaid
flowchart TB flowchart TB
subgraph VA["虚拟地址空间"] subgraph VA["虚拟地址空间"]
direction TB direction TB
S0["段0 (主程序)"] S0["段0 (主程序)"]
S1["段1 (子程序)"] S1["段1 (子程序)"]
S2["段2 (数据)"] S2["段2 (数据)"]
end end
subgraph S0P["段0 内部分页"] subgraph S0P["段0 内部分页"]
direction TB direction TB
P0["页0"] P0["页0"]
P1["页1"] P1["页1"]
P2["页2"] P2["页2"]
end end
subgraph PM["物理内存"] subgraph PM["物理内存"]
direction TB direction TB
F0["页框0"] F0["页框0"]
F1["页框1"] F1["页框1"]
F2["页框2"] F2["页框2"]
F3["页框3"] F3["页框3"]
end end
VA --> S0P VA --> S0P
P0 --> F2 P0 --> F2
P1 --> F0 P1 --> F0
P2 --> F3 P2 --> F3
style VA fill:#e3f2fd,stroke:#1976d2 style VA fill:#e3f2fd,stroke:#1976d2
style S0P fill:#f3e5f5,stroke:#7b1fa2 style S0P fill:#f3e5f5,stroke:#7b1fa2
style PM fill:#fff8e1,stroke:#f9a825 style PM fill:#fff8e1,stroke:#f9a825
``` ```
### 段页式地址结构 ### 段页式地址结构
逻辑地址由**三部分**组成: 逻辑地址由**三部分**组成:
``` ```
段页式逻辑地址: 段页式逻辑地址:
┌──────────────┬──────────────┬──────────────┐ ┌──────────────┬──────────────┬──────────────┐
│ 段号 S │ 页号 P │ 页内偏移 d │ │ 段号 S │ 页号 P │ 页内偏移 d │
└──────────────┴──────────────┴──────────────┘ └──────────────┴──────────────┴──────────────┘
``` ```
### 段页式地址变换 ### 段页式地址变换
```mermaid ```mermaid
flowchart TD flowchart TD
A["逻辑地址 (S, P, d)"] --> B["1. 用STBR找到段表"] A["逻辑地址 (S, P, d)"] --> B["1. 用STBR找到段表"]
B --> C["2. 用段号S查段表<br/>得到该段的页表基址和段长"] B --> C["2. 用段号S查段表<br/>得到该段的页表基址和段长"]
C --> D{"P < 该段页数?"} C --> D{"P < 该段页数?"}
D -->|"是"| E["3. 用页号P查页表<br/>得到物理页框号 PPN"] D -->|"是"| E["3. 用页号P查页表<br/>得到物理页框号 PPN"]
D -->|"否"| F["越界中断"] D -->|"否"| F["越界中断"]
E --> G["4. 物理地址 = PPN × 页大小 + d"] E --> G["4. 物理地址 = PPN × 页大小 + d"]
G --> H["访问物理内存"] G --> H["访问物理内存"]
style F fill:#ffcdd2,stroke:#c62828 style F fill:#ffcdd2,stroke:#c62828
``` ```
### 段页式地址变换步骤 ### 段页式地址变换步骤
1. **查段表**: 用 STBR + S 找到第 S 个段表项,得到该段的**页表基址** 1. **查段表**: 用 STBR + S 找到第 S 个段表项,得到该段的**页表基址**
2. **查页表**: 用页表基址 + P 找到第 P 个页表项,得到 **PPN** 2. **查页表**: 用页表基址 + P 找到第 P 个页表项,得到 **PPN**
3. **拼接地址**: PA = PPN × 页大小 + d 3. **拼接地址**: PA = PPN × 页大小 + d
> **注意**: 段页式需要 **3 次内存访问**(段表 + 页表 + 数据比纯分页多一次。TLB 的作用更加关键。 > **注意**: 段页式需要 **3 次内存访问**(段表 + 页表 + 数据比纯分页多一次。TLB 的作用更加关键。
### 段页式地址变换计算示例 ### 段页式地址变换计算示例
> **例题**: 某段页式系统,页面大小 4KB。段表如下求逻辑地址 (1, 2, 0x100) 的物理地址。 > **例题**: 某段页式系统,页面大小 4KB。段表如下求逻辑地址 (1, 2, 0x100) 的物理地址。
**段表**: **段表**:
| 段号 | 页表基址 | 段长(页数) | | 段号 | 页表基址 | 段长(页数) |
|------|---------|-----------| |------|---------|-----------|
| 0 | 0x8000 | 4 | | 0 | 0x8000 | 4 |
| 1 | 0xA000 | 6 | | 1 | 0xA000 | 6 |
| 2 | 0xC000 | 3 | | 2 | 0xC000 | 3 |
**段1的页表** (基址 0xA000): **段1的页表** (基址 0xA000):
| 页号 | PPN | | 页号 | PPN |
|------|-----| |------|-----|
| 0 | 5 | | 0 | 5 |
| 1 | 2 | | 1 | 2 |
| 2 | 8 | | 2 | 8 |
| 3 | 1 | | 3 | 1 |
| 4 | 7 | | 4 | 7 |
| 5 | 3 | | 5 | 3 |
**解题过程**: **解题过程**:
1. S=1, P=2, d=0x100 1. S=1, P=2, d=0x100
2. 查段表段1的页表基址 = 0xA000段长 = 6 页 2. 查段表段1的页表基址 = 0xA000段长 = 6 页
3. P=2 < 6合法 3. P=2 < 6合法
4. 查段1的页表P=2 对应 PPN=**8** 4. 查段1的页表P=2 对应 PPN=**8**
5. PA = 8 × 4096 + 0x100 = 0x8000 + 0x100 = **0x8100** 5. PA = 8 × 4096 + 0x100 = 0x8000 + 0x100 = **0x8100**
--- ---
## 八、三种存储管理方式对比 ## 八、三种存储管理方式对比
| 对比项 | 纯分页 | 纯分段 | 段页式 | | 对比项 | 纯分页 | 纯分段 | 段页式 |
|--------|--------|--------|--------| |--------|--------|--------|--------|
| **划分依据** | 固定大小 | 逻辑结构 | 先逻辑后固定大小 | | **划分依据** | 固定大小 | 逻辑结构 | 先逻辑后固定大小 |
| **地址维度** | 一维 | 二维 | 三维 | | **地址维度** | 一维 | 二维 | 三维 |
| **碎片** | 内碎片 | 外碎片 | 内碎片 | | **碎片** | 内碎片 | 外碎片 | 内碎片 |
| **共享** | 以页为粒度 | 以段为粒度 | 以段为粒度 | | **共享** | 以页为粒度 | 以段为粒度 | 以段为粒度 |
| **内存访问次数** | 2次页表+数据) | 2次段表+数据) | 3次段表+页表+数据) | | **内存访问次数** | 2次页表+数据) | 2次段表+数据) | 3次段表+页表+数据) |
| **代表系统** | Linux | 早期Multics | Intel x8632位保护模式 | | **代表系统** | Linux | 早期Multics | Intel x8632位保护模式 |
--- ---
## 九、小结 ## 九、小结
```mermaid ```mermaid
mindmap mindmap
root((段式存储管理)) root((段式存储管理))
分段引入 分段引入
信息共享不便 信息共享不便
动态链接需求 动态链接需求
逻辑结构需求 逻辑结构需求
分段思想 分段思想
按逻辑分段 按逻辑分段
代码段/数据段/栈段 代码段/数据段/栈段
各段独立地址空间 各段独立地址空间
地址结构 地址结构
二维: 段号+偏移 二维: 段号+偏移
不同于分页的一维 不同于分页的一维
段表 段表
段号/段长/基址 段号/段长/基址
越界检查 越界检查
STBR寄存器 STBR寄存器
地址变换 地址变换
查段表→越界检查→基址+偏移 查段表→越界检查→基址+偏移
PA = Base + d PA = Base + d
段页式 段页式
先分段再分页 先分段再分页
三维地址: 段号+页号+偏移 三维地址: 段号+页号+偏移
兼具两者优点 兼具两者优点
``` ```
--- ---
## 思考题 ## 思考题
1. **概念理解**: 分段和分页的根本区别是什么?为什么说分段的地址空间是二维的? 1. **概念理解**: 分段和分页的根本区别是什么?为什么说分段的地址空间是二维的?
2. **地址变换**: 某分段系统有 4 个段,段表如下。求物理地址或判断是否越界: 2. **地址变换**: 某分段系统有 4 个段,段表如下。求物理地址或判断是否越界:
- (0, 0x800) → ? - (0, 0x800) → ?
- (2, 0x5000) → ? - (2, 0x5000) → ?
- (3, 0x2000) → ? - (3, 0x2000) → ?
| 段号 | 段长 | 基址 | | 段号 | 段长 | 基址 |
|------|------|------| |------|------|------|
| 0 | 0x1000 | 0x5000 | | 0 | 0x1000 | 0x5000 |
| 1 | 0x2000 | 0x8000 | | 1 | 0x2000 | 0x8000 |
| 2 | 0x4000 | 0xA000 | | 2 | 0x4000 | 0xA000 |
| 3 | 0x3000 | 0x2000 | | 3 | 0x3000 | 0x2000 |
3. **段页式**: 为什么段页式需要 3 次内存访问TLB 如何缓解这个问题? 3. **段页式**: 为什么段页式需要 3 次内存访问TLB 如何缓解这个问题?
4. **对比分析**: 在什么场景下分段优于分页?在什么场景下分页优于分段? 4. **对比分析**: 在什么场景下分段优于分页?在什么场景下分页优于分段?
--- ---
## 关联笔记 ## 关联笔记
- [[13_存储管理基础]] — 存储器层次结构与地址空间基础 - [[13_存储管理基础]] — 存储器层次结构与地址空间基础
- [[14_分页存储管理]] — 分页思想、页表与地址变换 - [[14_分页存储管理]] — 分页思想、页表与地址变换
- [[16_虚拟存储器]] — 基于分段的虚拟存储器实现 - [[16_虚拟存储器]] — 基于分段的虚拟存储器实现
--- ---
**上一讲**: [[14_分页存储管理]] **上一讲**: [[14_分页存储管理]]
**下一讲**: [[16_虚拟存储器]] **下一讲**: [[16_虚拟存储器]]

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,257 @@
# 2026年操作系统期末预测卷
## 考试信息
- **满分**100分
- **时间**120分钟
- **题型**7道大题与历年试卷格式一致
---
## 题目一进程控制与进程树分析15分
### 题目
阅读以下程序代码,回答问题:
```c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid;
int status;
printf("A: pid=%d\n", getpid());
pid = fork();
if (pid == 0) {
printf("B: pid=%d, ppid=%d\n", getpid(), getppid());
pid = fork();
if (pid == 0) {
printf("C: pid=%d, ppid=%d\n", getpid(), getppid());
} else {
wait(&status);
printf("D: pid=%d\n", getpid());
}
} else {
printf("E: pid=%d\n", getpid());
pid = fork();
if (pid == 0) {
printf("F: pid=%d, ppid=%d\n", getpid(), getppid());
} else {
wait(&status);
wait(&status);
printf("G: pid=%d\n", getpid());
}
}
return 0;
}
```
**问题**
1. 6分画出完整的进程树标注每个进程执行的输出语句用字母A-G表示
2. 4分该程序共有多少个进程包括初始进程请列出所有进程的创建顺序
3. 3分输出语句D和G的执行顺序是否一定为什么
4. 2分如果将所有`wait(&status)`调用删除,进程输出结果会发生什么变化?
---
## 题目二磁盘调度算法计算15分
### 题目
某磁盘有200个磁道编号0-199磁盘请求队列中的磁道号依次为
**98, 183, 37, 122, 14, 124, 65, 67**
当前磁头位于磁道**53**,向磁道号**增大**的方向移动。
**问题**
1. 8分分别使用以下磁盘调度算法计算磁头移动的总磁道数
- FCFS先来先服务
- SSTF最短寻道时间优先
- SCAN电梯算法
- C-SCAN循环扫描算法
2. 4分在上述四种算法中哪种算法的磁头移动总磁道数最少哪种最多简要说明原因。
3. 3分如果磁头初始方向改为向磁道号减小的方向移动SCAN算法的结果会如何变化请计算新的磁头移动总磁道数。
---
## 题目三虚拟内存与页面置换15分
### 题目
某系统采用请求分页存储管理页面大小为4KB分配给某进程的物理页框数为**4**。该进程的页面访问序列为:
**1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5**
**问题**
1. 9分分别使用以下页面置换算法计算缺页次数和缺页率
- OPT最优算法
- FIFO先进先出算法
- LRU最近最久未使用算法
2. 3分如果将分配的物理页框数增加到5使用FIFO算法缺页次数是否会减少请验证并解释是否出现了Belady异常。
3. 3分假设页面大小改为8KB其他条件不变该进程的虚拟地址空间大小为32KB请问
- 进程共有多少个虚拟页?
- 页表需要多少个页表项?
- 虚拟地址`0x3A5F`对应的页号和页内偏移分别是多少?
---
## 题目四文件系统与inode计算15分
### 题目
某Unix文件系统采用Ext2结构文件系统的块大小为**4KB**,磁盘块号占**4字节**inode中包含**15个地址项**12个直接索引、1个一次间接、1个二次间接、1个三次间接
**问题**
1. 6分计算
- 每个磁盘块可以存放多少个磁盘块号?
- 一个inode支持的最大文件大小是多少请列出计算过程
2. 4分假设某文件大小为260KB请问
- 该文件占用多少个数据块?
- inode中的哪些地址项会被使用
- 是否需要使用间接索引块?如果需要,使用几个?
3. 5分假设要读取该文件偏移量为100KB处开始的4KB数据请描述完整的地址转换过程
- 如何确定使用inode中的哪个地址项
- 如何计算数据在磁盘上的具体位置?
- 需要访问几次磁盘?
---
## 题目五CPU调度算法计算15分
### 题目
某单处理器系统中有以下进程,按到达时间排序:
| 进程 | 到达时间 | 运行时间 |
|------|----------|----------|
| P1 | 0 | 8 |
| P2 | 1 | 4 |
| P3 | 2 | 9 |
| P4 | 3 | 5 |
| P5 | 4 | 2 |
**问题**
1. 9分分别使用以下调度算法计算各进程的完成时间、周转时间和带权周转时间并求平均周转时间和平均带权周转时间
- FCFS先来先服务
- SJF非抢占最短作业优先
- HRRF最高响应比优先
2. 3分在上述三种算法中
- 哪种算法的平均周转时间最短?
- 哪种算法可能产生"饥饿"现象?为什么?
3. 3分如果采用时间片轮转调度RR时间片大小为3请画出甘特图并计算P1和P2的周转时间。
---
## 题目六I/O控制方式与磁盘访问15分
### 题目
1. 6分比较以下四种I/O控制方式的特点填写下表
| 特性 | 程序直接控制 | 中断驱动I/O | DMA | 通道 |
|------|-------------|-------------|-----|------|
| CPU介入频率 | | | | |
| 数据传输单位 | | | | |
| CPU与I/O并行性 | | | | |
| 硬件复杂度 | | | | |
2. 4分某磁盘转速为7200 RPM平均寻道时间为8ms每个磁道500个扇区每个扇区512字节。
- 计算平均旋转延迟
- 计算读取一个扇区的平均访问时间
- 如果要连续读取100个扇区在同一磁道上总访问时间是多少
3. 5分某系统使用SPOOLing技术管理打印机
- 画出SPOOLing系统的组成结构图
- 解释SPOOLing如何将独占设备改造为共享设备
- 说明SPOOLing与缓冲技术的区别
---
## 题目七段页式存储管理与地址转换10分
### 题目
某系统采用段页式存储管理,地址结构如下:
- 段号8位
- 页号8位
- 页内偏移16位
系统参数:
- 页大小64KB
- 段表基址寄存器指向的段表如下:
| 段号 | 段长(页数) | 页表始址 |
|------|-------------|----------|
| 0 | 4 | 1000 |
| 1 | 6 | 2000 |
| 2 | 2 | 3000 |
页表(部分)如下:
| 页号 | 物理块号 |
|------|----------|
| 0 | 50 |
| 1 | 51 |
| 2 | 52 |
| 3 | 53 |
**问题**
1. 4分逻辑地址`0x00018000`对应的段号、页号、页内偏移分别是多少?该地址对应的物理地址是多少?(写出计算过程)
2. 3分逻辑地址`0x01028000`是否合法?为什么?如果合法,计算其物理地址。
3. 3分与纯分页和纯分段相比段页式存储管理有哪些优点和缺点
---
## 参考答案要点
### 题目一参考答案
1. 进程树A→(B, E)B→(C, D)E→(F, G)
2. 共7个进程创建顺序A→B→E→C→F→D→G
3. D和G的执行顺序不一定取决于哪个子进程先完成
4. 删除wait后输出顺序不确定可能出现孤儿进程
### 题目二参考答案
1. FCFS: 640道SSTF: 236道SCAN: 299道C-SCAN: 356道
2. SSTF最少贪心选择最近FCFS最多无优化
3. 反向SCAN53→37→14→0→65→67→98→122→124→183总移动359道
### 题目三参考答案
1. OPT: 8次(66.7%)FIFO: 10次(83.3%)LRU: 10次(83.3%)
2. 页框数=5时FIFO缺页仍为10次未出现Belady异常
3. 虚拟页数=4页表项=4地址0x3A5F页号=3偏移=0x2A5F
### 题目四参考答案
1. 每块1024个块号最大文件约16GB+
2. 65个数据块使用直接索引12个+一次间接索引1个不需要二次间接
3. 偏移100KB=第25块使用直接索引项2需2次磁盘访问
### 题目五参考答案
1. FCFS平均周转7.8SJF平均周转6.2HRRF平均周转7.0
2. SJF最短SJF可能饥饿长作业持续等待
3. RR甘特图0-3(P1),3-6(P2),6-8(P5),8-11(P1),11-14(P3),14-17(P4),17-20(P1),20-23(P3),23-25(P3)
P1周转=23P2周转=6
### 题目六参考答案
1. 表格填写(见知识点总结)
2. 旋转延迟4.17ms扇区访问12.21ms连续100扇区约12.5ms
3. SPOOLing由输入/输出井和预输入/缓输出程序组成
### 题目七参考答案
1. 段号=0页号=1偏移=0x8000物理地址=51×64KB+0x8000
2. 合法段号1页号2物理地址=52×64KB+0x8000
3. 优点:分段便于共享/保护+分页解决外部碎片缺点地址转换复杂需3次访存

View File

@@ -0,0 +1,688 @@
# 📖 操作系统复习与教学笔记
> **目标**:不仅能应付考试,更能真正理解操作系统的工作原理
> **定位**:这份笔记不是简单的知识点罗列,而是教你"怎么学、怎么记、怎么做题"
---
## 目录
1. [复习策略与时间规划](#一复习策略与时间规划)
2. [四大必考计算题 · 手把手教学](#二四大必考计算题--手把手教学)
3. [核心概念 · 快速记忆法](#三核心概念--快速记忆法)
4. [易混淆概念辨析](#四易混淆概念辨析)
5. [代码题考点精讲](#五代码题考点精讲)
6. [问答题高频考点](#六问答题高频考点)
7. [考前速查表](#七考前速查表)
---
## 一、复习策略与时间规划
### 1.1 按优先级分三层
| 层级 | 内容 | 建议投入时间 | 策略 |
|------|------|-------------|------|
| 🔴 **保命层** | 磁盘调度、页面置换、fork进程树、inode计算 | 60% | 必须能**独立默写**解题步骤 |
| 🟡 **进阶层** | CPU调度、I/O控制方式、段页式地址转换、死锁 | 30% | 理解原理 + 做2-3道题 |
| 🟢 **基础层** | 概念题、代码优化、工作集模型 | 10% | 多看几遍,有印象即可 |
### 1.2 复习三步法
```
第一步:看本笔记 → 建立知识框架和记忆锚点
第二步:做预测卷 → 实战演练,暴露薄弱点
第三步:复盘错题 → 针对性地回顾对应章节
```
### 1.3 考试时间分配建议120分钟
| 题型 | 建议用时 | 策略 |
|------|---------|------|
| 计算题(磁盘/页面置换/CPU调度/inode | 50-60min | 先做!这是拿分大头 |
| 代码分析题fork进程树 | 15-20min | 画图要清晰规范 |
| 简答/概念题 | 25-30min | 分点作答,关键词给分 |
| 地址转换题 | 10-15min | 按步骤写,不要跳步 |
| 检查 | 5-10min | 检查计算题的数字 |
---
## 二、四大必考计算题 · 手把手教学
### 2.1 磁盘调度算法计算(★★★★★ 每年必考)
#### 本质理解
磁盘调度就是在解决"磁头怎么走最省时间"的问题。可以把磁头想象成电梯,磁道就是楼层号。
#### 各算法的"人设"
| 算法 | 一句话人设 | 行为特征 |
|------|-----------|----------|
| **FCFS** | "先来后到"的耿直 boy | 来一个处理一个,不做任何优化 |
| **SSTF** | "贪心"的近视眼 | 每次都选最近的,但可能让远的永远等不到 |
| **SCAN** | "电梯"本梯 | 一直走到头再回头 |
| **C-SCAN** | 单向电梯 | 只往上走,到顶后直接回到底部重来 |
| **LOOK** | 智能电梯 | 走到最后一个请求就回头,不到边界 |
| **C-LOOK** | 智能单向电梯 | 只往上到最高请求,然后直接回最低请求 |
#### 📐 解题模板(以最难的 C-SCAN 为例)
```
题目磁道0-199磁头在53向增大方向移动
请求序列98, 183, 37, 122, 14, 124, 65, 67
Step 1: 将请求排序
排序14, 37, 65, 67, 98, 122, 124, 183
Step 2: 确定方向并依次服务
C-SCAN规则向增大方向走 → 到199边界→ 回到0 → 继续向增大方向
路径53 → 65 → 67 → 98 → 122 → 124 → 183 → 199到边界→ 0 → 14 → 37
移动距离12 + 2 + 31 + 24 + 2 + 59 + 16 + 199 + 14 + 23
Step 3: 求和
= 12+2+31+24+2+59+16+199+14+23 = 382道
```
#### ⚠️ 常见错误
| 错误 | 正确做法 |
|------|---------|
| SCAN/C-SCAN 忘记走到边界199或0 | **必须**走到边界,除非题目说用 LOOK |
| SSTF 选择了相等距离的两个磁道 | 通常选编号小的(题目可能会有说明) |
| 方向搞反 | 题目说"向增大方向"就从53往上走 |
| C-SCAN 往回时逐个服务 | C-SCAN 回头时**不服务**,到底后才重新开始 |
#### 💡 速算技巧
SSTF 的路径一定是最短的贪心嘛FCFS 通常是最长的。检查时如果 SSTF 比别的算法还长,肯定算错了。
---
### 2.2 页面置换算法(★★★★★ 每年必考)
#### 本质理解
内存满了要换掉哪个页面OPT 是"预言家"LRU 是"记仇本"FIFO 是"排队机"。
#### 各算法速记
| 算法 | 口诀 | 关键特征 |
|------|------|---------|
| **OPT** | "看未来,谁最晚用就换谁" | 理论最优,无法实现(但考试能算) |
| **FIFO** | "先来先走,排队淘汰" | 可能 Belady 异常(页框变多反而缺页更多) |
| **LRU** | "看过去,谁最久没用就换谁" | 无 Belady 异常,硬件开销大 |
| **Clock** | "转圈查,引用位=0就换" | LRU 的简化版 |
#### 📐 解题模板(以 LRU页框数=3 为例)
```
访问序列7, 0, 1, 2, 0, 3, 0, 4, 2, 3, 0, 3, 2, 1, 2, 0, 1, 7, 0, 1
画表格法:
-------------------------------------------------------------
访问 页框1 页框2 页框3 是否缺页 说明
-------------------------------------------------------------
7 7 - - ✓缺页 空,直接放入
0 7 0 - ✓缺页 空,直接放入
1 7 0 1 ✓缺页 空,直接放入
2 7 0 2 ✓缺页 换掉最久未用的11离上次最远
0 7 0 2 ✗命中 0已存在更新它的使用时间
3 3 0 2 ✓缺页 换掉最久未用的77离上次最远
0 3 0 2 ✗命中 0已存在
4 3 0 4 ✓缺页 换掉最久未用的22离上次最远
...以此类推
```
#### ⚠️ OPT 和 LRU 的区别(最容易混淆)
```
序列1, 2, 3, 4, 1, 2, 5, 1, 2, 3, 4, 5页框数=4
OPT 在访问5时
当前页框1,2,3,4
看未来1还要用第5位2还要用第6位3还要用第10位4还要用第11位
最晚使用的是4第11位才用所以换4
LRU 在访问5时
当前页框1,2,3,4
看过去1最近在位置4用过2在位置5用过3在位置2用过4在位置3用过
最久未用的是3位置2之后就没用过所以换3
```
> **关键口诀**OPT 看**未来**LRU 看**过去**。OPT 是"预言"LRU 是"历史"。
#### 💡 Belady 异常
**只在 FIFO 中出现**。增加页框数反而缺页更多。LRU 和 OPT 都不会有这个问题。
---
### 2.3 fork 进程树分析(★★★★★ 每年必考)
#### 本质理解
`fork()` 就像细胞分裂 — 调用一次,变成两个进程,从同一行代码继续执行。
#### 黄金法则
```
法则1fork() 一次,返回两次
- 父进程返回值 = 子进程的 PID
- 子进程返回值 = 0
法则2子进程从 fork() 的下一行开始执行(不是从 main 开头)
法则3fork() 嵌套时,每个进程都要独立分析它的执行路径
法则4父进程总数 = 2^nn为fork调用次数无嵌套时
```
#### 📐 解题模板
```
程序:
fork() // fork1
if (pid == 0) {
fork() // fork2
}
fork() // fork3
Step 1: 画出进程树
P(初始)
╲ ← fork1
P(子1) P(父)
╲ ╲ ← fork2只有子1执行
P(孙1) P(子1续) P(父)
| | | ← fork3所有进程都执行
P(曾孙1) P(孙2) P(子2)
Step 2: 统计进程总数
2³ = 8个进程3次fork最外层没有if限制
Step 3: 分析每个进程的输出
按执行路径逐条跟踪...
```
#### ⚠️ 常见陷阱
| 陷阱 | 说明 |
|------|------|
| `if (pid == 0)` 内的 fork | 只有子进程执行,父进程跳过 |
| `wait(NULL)` | 父进程等待**任意**子进程结束 |
| 嵌套的 `wait` | 每个 `wait` 只能回收一个子进程 |
| 没有 wait | 子进程变成僵尸进程Zombie |
#### 💡 wait 的个数
父进程有几个子进程,就应该有几个 `wait()`,否则会产生僵尸进程。
---
### 2.4 Ext2 inode 最大文件计算(★★★★★ 每年必考)
#### 本质理解
inode 就像文件系统的"目录索引卡"有15个格子放地址。前12个直接指向数据块后3个是间接指向指向一个"指针块",指针块再指向数据块)。
#### 计算公式速记
```
块大小 = 4KB = 4096 字节
指针大小 = 4 字节
每块指针数 = 4096 / 4 = 1024 个
最大文件:
直接12 × 4KB = 48KB
一次间接1024 × 4KB = 4MB
二次间接1024² × 4KB = 4GB
三次间接1024³ × 4KB = 4TB
──────────────────────────────────
总计48KB + 4MB + 4GB + 4TB ≈ 4.004 TB
```
> **注意**:如果块大小是 1KB、指针 4B则每块 256 个指针,结果约 16GB+(你的知识点总结里那个)。
#### 📐 解题模板:给定文件大小,问需要哪些地址项
```
题目:块大小=4KB指针=4B文件=260KB
Step 1: 计算文件占多少数据块
260KB / 4KB = 65 块
Step 2: 检查直接索引能装多少
直接索引 = 12 块 → 只能装 48KB不够
Step 3: 一次间接能装多少
一次间接 = 1024 块 → 1024 × 4KB = 4MB
12 + 1024 = 1036 块65 ≤ 1036所以只需要一次间接
结论使用直接索引12个 + 一次间接1个其中用到 65-12=53 个指针)
```
#### ⚠️ 易错点
- 问"需要几次磁盘访问":直接索引=1次一次间接=2次先读指针块再读数据块二次间接=3次...
- 在计算最大文件时,**不要忘了加直接索引的12块**
---
## 三、核心概念 · 快速记忆法
### 3.1 操作系统四大特征
```
口诀:共、享、虚、异 → "恭喜虚拟"
- 共(并发):宏观并行,微观串行
- 享(共享):资源共享
- 虚(虚拟):物理实体变为逻辑多个
- 异(异步):进程走走停停
```
### 3.2 CPU 双模式
```
用户态 →→→ 中断/异常/系统调用 →→→ 内核态
内核态 →→→ PSW切换 →→→ 用户态
记忆:用户态进内核态叫"陷入"trap内核态回用户态叫"恢复"
```
### 3.3 中断分类速记
```
中断(Interrupt)
├── 硬中断(外部,异步)
│ ├── I/O中断键盘敲一下
│ └── 时钟中断OS拿回控制权的关键
└── 软中断/异常(内部,同步)
├── 故障Fault缺页异常可恢复
├── 陷阱Trap系统调用 int 0x80
└── 终止Abort除零错误不可恢复
```
> **必考提问**OS 如何获得 CPU 控制权?→ **时钟中断!** 时间片到了时钟中断触发OS 重新调度。
### 3.4 ELF 内存布局
```
从低地址到高地址:
┌─────────────────┐
│ .text 代码段 │ ← 只读,存放指令
│ .rodata 只读数据 │ ← 字符串常量等
│ .data 初始化数据 │ ← 已初始化全局变量
│ .bss 未初始化数据 │ ← 不占磁盘!只占内存
├─────────────────┤
│ heap↑ │ ← malloc 分配
├─────────────────┤
│ ↓ │
│ stack │ ← 局部变量、函数调用
└─────────────────┘
```
**记忆 trick**:堆向上长(像树往上),栈向下长(像水往下流)。
### 3.5 死锁四个必要条件
```
口诀:互、请、不、循 → "互请不循"(互相请求,不能循环)
1. 互斥Mutual exclusion
2. 请求和保持Hold and wait
3. 不可抢占No preemption
4. 循环等待Circular wait
破坏任意一个 → 死锁预防
```
### 3.6 页面置换算法特征速记
```
OPT → 看未来,最晚用的 → 不可能实现(除非能预知未来)
LRU → 看过去,最久没用 → 硬件开销大
FIFO → 看时间,最早进入 → 会有 Belady 异常
Clock→ 看引用位,转圈查 → 近似 LRU
LFU → 看次数,最少使用 → 可能被"热数据"刷掉
```
---
## 四、易混淆概念辨析
### 4.1 并发 vs 并行
| | 并发 | 并行 |
|--|------|------|
| 含义 | 同时**处理**多个任务 | 同时**执行**多个任务 |
| 核心 | 宏观上同时,微观上交替 | 真正的同一时刻都在运行 |
| 硬件 | 单核 CPU | 多核 CPU |
| 类比 | 一个人交替做几件事 | 几个人同时做各自的事 |
### 4.2 进程 vs 线程
| | 进程 | 线程 |
|--|------|------|
| 资源拥有 | 独立地址空间 | 共享进程资源 |
| 切换开销 | 大(需切换页表等) | 小(只需切换栈) |
| 通信方式 | IPC管道、共享内存等 | 直接读写共享数据 |
| 独立性 | 一个崩溃不影响其他 | 一个线程崩溃→整个进程崩溃 |
### 4.3 死锁 vs 饥饿
| | 死锁 | 饥饿 |
|--|------|------|
| 状态 | 多个进程**互相等待**,都无法前进 | 一个进程**一直等不到**资源 |
| 是否可以解除 | 必须外部干预(终止进程/抢占资源) | 其他进程释放资源后可自动解除 |
| 典型场景 | 银行家算法中不安全状态 | SJF 中长作业一直得不到 CPU |
### 4.4 静态链接 vs 动态链接
| | 静态链接 | 动态链接 |
|--|---------|---------|
| 时机 | 编译时 | 运行时 |
| 文件大小 | 大(包含所有库代码) | 小(运行时加载) |
| 更新库 | 需重新编译 | 直接替换库文件 |
| 依赖 | 无外部依赖 | 需要 .so/.dll 文件存在 |
### 4.5 全局置换 vs 局部置换
| | 全局置换 | 局部置换 |
|--|---------|---------|
| 范围 | 从所有进程中选页面换出 | 只从当前进程的页面中选 |
| 公平性 | 进程可能互相影响 | 每个进程独立管理 |
| 抖动控制 | 难控制 | 容易控制 |
### 4.6 固定分区 vs 动态分区 vs 分页
| | 固定分区 | 动态分区 | 分页 |
|--|---------|---------|------|
| 内部碎片 | ✓ | ✗ | ✓(最后一页) |
| 外部碎片 | ✗ | ✓ | ✗ |
| 管理复杂度 | 低 | 中 | 高 |
---
## 五、代码题考点精讲
### 5.1 fork 代码分析
#### 经典模式 1单次 fork
```c
pid_t pid = fork();
if (pid == 0) {
// 子进程代码
} else {
// 父进程代码pid 是子进程的 PID
}
```
**输出特点**
- 子进程和父进程的代码都会执行
- 子进程从 fork() 返回处开始执行(不是从 main 开始)
- "A\n" 只有一个进程输出,"B\n" 和 "C\n" 各输出一次(因为 fork 后两个进程都继续)
#### 经典模式 2嵌套 fork
```c
pid_t pid1 = fork(); // fork1
if (pid1 == 0) {
pid_t pid2 = fork(); // fork2 — 只在子进程中执行
if (pid2 == 0) {
// 孙进程
}
}
```
**进程树分析**
```
P初始
/ \ ← fork1
P(子1) P(父)
/ \ ← fork2仅在子1中执行
P(孙) P(父)
```
#### 经典模式 3fork + wait
```c
for (int i = 0; i < 3; i++) {
pid_t pid = fork();
if (pid == 0) {
// 子进程
exit(0);
}
}
// 父进程需要回收所有子进程
while (wait(NULL) > 0);
```
**重点**:循环中的 fork子进程 exit 后,父进程用 `wait` 回收。总计 `2³ = 8` 个进程。
### 5.2 生产者消费者(多线程同步)
```c
sem_t mutex, empty, full;
void *producer(void *arg) {
// 生产数据...
sem_wait(&empty); // 等空位
sem_wait(&mutex); // 加锁
// 放入缓冲区
sem_post(&mutex); // 解锁
sem_post(&full); // 通知消费者
}
void *consumer(void *arg) {
sem_wait(&full); // 等数据
sem_wait(&mutex); // 加锁
// 取出数据
sem_post(&mutex); // 解锁
sem_post(&empty); // 通知生产者
}
```
**⚠️ 注意顺序**P(empty) 必须在 P(mutex) 之前!否则可能死锁。
——如果先锁 mutex 再等 empty而缓冲区满了生产者拿不到 empty消费者进不来临界区拿数据死锁。
### 5.3 Shell 基本框架
```c
while (1) {
printf("$ ");
fgets(cmdline, MAXLINE, stdin);
pid_t pid = fork();
if (pid == 0) {
// 子进程执行命令
execvp(argv[0], argv);
exit(0);
}
// 父进程等待
wait(NULL);
}
```
### 5.4 守护进程创建5步法
```c
// 第1步fork + 父进程退出
pid_t pid = fork();
if (pid > 0) exit(0);
// 第2步创建新会话
setsid();
// 第3步再 fork 一次,防止重新获得控制终端
pid = fork();
if (pid > 0) exit(0);
// 第4步改变工作目录
chdir("/");
// 第5步重设文件权限掩码
umask(0);
```
**问**:为什么要 fork 两次?
**答**:第一次 fork 后 setsid() 创建新会话,但会话首进程可能重新获得控制终端。第二次 fork 的子进程不是会话首进程,**永远无法**获得控制终端。
---
## 六、问答题高频考点
### 6.1 什么是操作系统?它的四大特征是什么?
**答题模板**
1. 操作系统是管理计算机硬件和软件资源的系统软件
2. 四大特征:并发、共享、虚拟、异步(各用一句话解释)
### 6.2 中断在操作系统中的作用?
**核心要点**
- 中断是实现**并发执行**的基础
- **时钟中断**是操作系统**获得 CPU 控制权**的关键机制
- 中断使 CPU 能够**响应外部事件**I/O 完成、硬件故障等)
- 系统调用通过**软中断trap**实现用户态→内核态切换
### 6.3 比较几种 I/O 控制方式
| 方式 | CPU介入 | 传输单位 | 并行性 | 硬件 |
|------|---------|---------|--------|------|
| 程序直接控制 | 全程忙等 | 字 | 无 | 最简单 |
| 中断驱动 | 每个字中断一次 | 字 | 有 | 较简单 |
| DMA | 每块中断一次 | 块 | 高 | 较复杂 |
| 通道 | 程序级 | 块/组 | 极高 | 最复杂 |
**记忆线索**从上到下CPU 越来越省事,硬件越来越复杂,传输效率越来越高。
### 6.4 什么是抖动Thrashing如何解决
**定义**分配的页框数太少导致频繁缺页CPU 大部分时间花在页面置换上,利用率急剧下降。
**原因**:工作集 > 分配的页框数
**解决方案**
1. 增加分配页框数(工作集模型)
2. 采用局部置换(不让进程互相影响)
3. 降低多道程序度
### 6.5 SPOOLing 如何将独占设备改造为共享设备?
**原理**
```
输入井
用户进程1 ╲ 预输入程序
用户进程2 ╲── 磁盘存储 ─╱ (把磁盘当"大缓冲区"
用户进程3 (输入/输出井) ╲ 缓输出程序
输出井
```
**核心思想**:用磁盘(共享设备)做缓冲,把独占设备(打印机)的逻辑使用变成对磁盘文件的读写。用户觉得打印机是自己的,实际上是 SPOOLing 系统在排队管理。
### 6.6 段式 vs 分页 vs 段页式
| | 分段 | 分页 | 段页式 |
|--|------|------|--------|
| 出发点 | 用户视角,逻辑模块 | 系统视角,物理管理 | 兼具两者优点 |
| 大小 | 可变(由逻辑决定) | 固定4KB等 | 段可变,段内页固定 |
| 碎片 | 外部碎片 | 内部碎片(最后一页) | 无外部碎片 |
| 共享/保护 | 方便 | 不方便 | 方便 |
| 地址结构 | 段号+偏移 | 页号+偏移 | 段号+页号+偏移 |
| 访存次数 | 2次 | 2次有TLB可减 | 3次 |
### 6.7 优先级反转
**场景**高优先级进程H等待低优先级进程L持有的资源而中等优先级进程M抢占了 L 的 CPU导致 H 被 M 间接阻塞。
**解决方案**:优先级继承 — L 持有的资源被 H 需要时L 临时继承 H 的优先级,运行完释放资源后再降回去。
---
## 七、考前速查表
### 7.1 公式速查
| 公式 | 适用场景 |
|------|---------|
| 周转时间 = 完成时间 - 到达时间 | CPU 调度 |
| 带权周转时间 = 周转时间 / 运行时间 | CPU 调度 |
| 响应比 = 1 + 等待时间 / 运行时间 | HRRF 调度 |
| EAT = λ + t + (1-a)·t | TLB 有效访问时间 |
| 每块指针数 = 块大小 / 指针大小 | inode 计算 |
| 加速比 S = T串行 / T并行 | 并行计算 |
| Amdahl: S = 1 / ((1-f) + f/P) | 并行计算加速比上限 |
| 磁盘访问时间 = 寻道 + 旋转延迟 + 传输 | I/O 性能 |
| 平均旋转延迟 = 1/2 × 转一圈时间 | 磁盘性能 |
### 7.2 数值速查
| 场景 | 典型值 |
|------|--------|
| 页大小 | 4KB |
| 块大小Ext2 | 1KB 或 4KB |
| 指针大小(地址项) | 4B |
| 时间片RR | 几十 ms 级别 |
| 7200RPM 旋转延迟 | 4.17ms(半圈) |
### 7.3 算法时间复杂度速查
| 算法/机制 | 数据结构 | 复杂度 |
|-----------|---------|--------|
| FCFS 调度 | 队列 | O(1) |
| SJF 调度 | 最小堆 | O(log n) |
| CFSLinux | 红黑树 | O(log n) |
| 页表查找(无 TLB | 数组 | O(1) |
| 多级页表 | 树 | O(级数) |
| 倒排页表 | 哈希表 | O(1)平均 |
### 7.4 Linux 常用命令速查
```bash
chmod 755 file # rwxr-xr-x (7=4+2+1, 5=4+1)
chmod u+x file # 给所有者加执行权限
ps -ef # 查看所有进程
top # 动态查看进程
kill -9 PID # 强制终止进程SIGKILL
grep pattern file # 搜索文件内容
strace ./prog # 跟踪系统调用
gdb ./prog # 调试程序
```
---
## 附录:考前最后一天做什么
### 8小时冲刺计划
```
第1小时复习四大计算题模板本笔记 2.1-2.4
第2小时做预测卷的计算题部分
第3小时复习进程树分析和代码题第五部分
第4小时做预测卷的代码分析题
第5小时复习概念辨析和问答题第三、四、六部分
第6小时做预测卷的问答题
第7小时错题回顾 + 速查表记忆
第8小时放松早点休息
```
### 考场检查清单
- ✅ 计算题先做(分值高、确定性大)
- ✅ 画进程树时标注字母/输出
- ✅ 缺页次数计算时,初始空页框**算**缺页
- ✅ SCAN/C-SCAN 注意方向
- ✅ CPU 调度画甘特图辅助
- ✅ 地址转换要写出每一步的数值
- ✅ 简答题分点作答,关键词突出
---
> **最后的话**:操作系统不是死记硬背的科目,理解"为什么"比记住"是什么"重要得多。
> 遇到问题时,想想"如果是 Linux它会怎么做"——这是最好的学习方式。
>
> 祝考试顺利!🎯

View File

@@ -0,0 +1,626 @@
# 操作系统期末知识点总结与考点分析
## 一、历年试卷考点频率统计
| 考点 | 20-21A | 21-22A | 2023A | 2025A | 频率 |
|------|--------|--------|-------|-------|------|
| 磁盘调度算法 | ✓ | ✓ | ✓ | ✓ | ★★★★★ |
| 虚拟内存页面置换 | ✓ | ✓ | ✓ | ✓ | ★★★★★ |
| 进程控制(fork) | ✓ | ✓ | ✓ | ✓ | ★★★★★ |
| CPU调度算法 | ✓ | - | ✓ | ✓ | ★★★★ |
| 文件系统(Ext2/inode) | - | ✓ | ✓ | ✓ | ★★★★ |
| I/O控制方式 | ✓ | - | - | ✓ | ★★★ |
| 段页式地址转换 | ✓ | ✓ | - | - | ★★★ |
| 银行家算法 | ✓ | - | - | - | ★★ |
| 工作集模型 | ✓ | ✓ | ✓ | - | ★★★ |
| 代码优化 | - | ✓ | ✓ | - | ★★ |
| 系统运行机制 | - | - | ✓ | - | ★★ |
| 多级反馈队列 | - | - | ✓ | - | ★★ |
---
## 二、各章核心知识点总结
### 第1章操作系统概述
**考点OS基本概念、发展历史、结构**
- **四大特征**:并发、共享、虚拟、异步
- **OS结构类型**
- 单体结构Linux所有服务在内核态运行性能好但耦合度高
- 分层结构:按层次组织,便于调试但层间通信开销大
- 微内核结构:内核只保留最基本功能,其余在用户态运行,可靠性高但性能差
- 虚拟机结构在硬件上运行多个OS实例
---
### 第2章系统运行机制
**考点中断、MMU、CPU双模式、系统调用**
- **中断机制**
- 硬中断:外部设备产生,异步
- 软中断异常CPU内部产生同步除零错误、缺页异常、系统调用
- **时钟中断**OS获得CPU控制权的关键机制
- **MMU地址转换**
- 逻辑地址 → 物理地址的硬件支持
- U/S位用户态/内核态标识,实现内存保护
- **CPU双模式**
- 用户态(目态)→ 内核态(管态):通过中断/异常/系统调用
- 内核态 → 用户态通过PSW程序状态字切换
- **系统调用流程**
```
用户程序 → 系统调用号放入EAX → int $0x80 → 内核态
→ 查系统调用表 → 执行服务程序 → 返回用户态
```
---
### 第3章Linux基础
**考点:目录结构、文件权限、/proc文件系统**
- **目录结构**/bin, /etc, /home, /proc, /dev, /tmp
- **文件权限**chmod 数字法r=4, w=2, x=1
- **/proc文件系统**:虚拟文件系统,提供进程和内核信息
---
### 第4章C语言开发基础
**考点编译流程、ELF内存布局、调试**
- **编译流程**:预处理 → 编译 → 汇编 → 链接
- **ELF内存布局**(从低到高):
```
.text代码段
.rodata只读数据
.data已初始化全局变量
.bss未初始化全局变量不占磁盘空间
heap向上增长 ↗)
stack向下增长 ↘)
```
- **关键区别**.bss不占磁盘空间但占内存空间运行时分配
---
### 第5-6章文件I/O
**考点UNIX IO、文件描述符共享、mmap、重定向**
- **UNIX IO函数**open/read/write/close/lseek
- **文件描述符表共享**
```
进程fd表 → 文件表(引用计数) → v-node表共享
fork后父子进程共享文件表引用计数+1
```
- **mmap内存映射**将文件映射到进程地址空间实现高效I/O
- **dup2重定向**实现I/O重定向的标准方法
- **stdio vs UNIX IO缓冲**
- stdio有用户缓冲区全缓冲/行缓冲/无缓冲)
- UNIX IO无用户缓冲区
---
### 第7章磁盘空间管理 ★★★★★必考
**考点分配方式、Ext2文件系统、inode混合索引**
- **三种分配方式**
- 连续分配:支持顺序/随机访问,有外部碎片
- 链接分配无外部碎片只支持顺序访问FAT是改进
- 索引分配:支持直接/间接访问inode是典型实现
- **FAT文件系统**FAT12/16/32链接分配的改进版本
- **NTFS**基于MFT主文件表B+树结构
- **Ext2文件系统**(重点):
```
超级块 → 块组描述符 → 块位图 → inode位图 → inode表 → 数据块
```
- **inode结构**15个地址项
- 0-11直接索引12个块
- 12一次间接索引256个块假设块大小1KB指针4字节
- 13二次间接索引256×256个块
- 14三次间接索引256³个块
- **最大文件计算**
```
块大小=1KB指针=4B则每块256个指针
直接12块
一次间接256块
二次间接256×256=65536块
三次间接256³=16777216块
总计12+256+65536+16777216 ≈ 16GB+
```
- **HDFS**分布式文件系统NameNode+DataNode架构
- **空闲空间管理**:位图法、成组链接法
- **RAID**
- RAID0条带化无冗余
- RAID1镜像100%冗余
- RAID3位交叉+专用校验盘
- RAID5块交叉+分布式校验
---
### 第8章进程控制 ★★★★★必考
**考点fork/exec/wait/exit、僵尸进程、shell实现**
- **fork()**
- 调用一次返回两次父进程返回子进程PID子进程返回0
- **COW写时复制**:父子进程共享物理页,写时才复制
- fork后文件描述符共享引用计数+1
- **exec系列函数**:替换进程映像,不创建新进程
- **wait/waitpid**
- 阻塞等待子进程状态变化
- 回收子进程资源,防止僵尸进程
- **exit/_exit**
- exit执行清理刷新缓冲区、调用atexit处理函数
- _exit直接退出不执行清理
- **僵尸进程**
- 子进程exit但父进程未wait
- 解决方法父进程调用wait/waitpid或SIGCHLD信号处理
- **shell实现**
```c
// 基本框架
while (1) {
读取命令行
解析命令
if (内置命令) 直接执行
else {
pid = fork()
if (pid == 0) execvp(...) // 子进程执行
else wait(NULL) // 父进程等待
}
}
```
- **守护进程daemon创建5步**
1. fork()创建子进程父进程exit
2. setsid()创建新会话
3. fork()再次fork父进程exit防止重新获得控制终端
4. chdir("/")改变工作目录
5. umask(0)重设文件权限掩码
---
### 第9章多线程 ★★★★
**考点pthread API、竞态条件、互斥锁、信号量、生产者消费者**
- **pthread API**
```c
pthread_create() // 创建线程
pthread_join() // 等待线程结束
pthread_exit() // 退出线程
pthread_detach() // 分离线程
```
- **竞态条件Race Condition**
- 多线程并发访问共享数据,结果取决于执行顺序
- 解决:互斥锁、信号量
- **互斥锁Mutex**
```c
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&mutex); // 加锁
// 临界区
pthread_mutex_unlock(&mutex); // 解锁
```
- **信号量Semaphore**
- P操作waitS--若S<0则阻塞
- V操作signalS++,若有等待者则唤醒
- **实现同步和互斥**
- **生产者消费者模型**
```
信号量:
mutex = 1 // 互斥访问缓冲区
empty = N // 空闲缓冲区数量
full = 0 // 满缓冲区数量
生产者: 消费者:
P(empty) P(full)
P(mutex) P(mutex)
放入产品 取出产品
V(mutex) V(mutex)
V(full) V(empty)
```
- **并行计算**
- 加速比 S = T_serial / T_parallel
- 效率 E = S / PP为处理器数量
- Amdahl定律S = 1 / ((1-f) + f/P)
---
### 第10章进程间通信IPC
**考点:管道、消息队列、共享内存、信号**
- **管道**
- 匿名管道:只能用于父子进程,单向通信
- 命名管道FIFO可用于无亲缘关系进程
- **消息队列**:内核中的链表,按消息类型读取
- **共享内存**最快的IPC方式需要同步机制保护
- **信号**异步通知机制SIGINT/SIGTERM/SIGCHLD等
---
### 第11章网络编程
**考点Socket API、TCP客户端服务器模型**
- **Socket API**
```c
// 服务器
socket() → bind() → listen() → accept() → read/write → close()
// 客户端
socket() → connect() → read/write → close()
```
- **字节序转换**htonl/htons/ntohl/ntohs
---
### 第12章并发服务器
**考点:多进程/多线程模型、I/O多路复用、线程池**
- **多进程模型**fork per request简单但开销大
- **多线程模型**pthread_create per request开销较小
- **I/O多路复用select**
```c
fd_set read_set;
FD_ZERO(&read_set);
FD_SET(fd, &read_set);
select(maxfd+1, &read_set, NULL, NULL, NULL);
```
- **线程池**:预先创建线程,减少创建/销毁开销
---
### 第13章CPU调度 ★★★★
**考点调度算法计算、优先级反转、CFS**
- **三级调度**:高级调度(作业调度)、中级调度(内存调度)、低级调度(进程调度)
- **调度算法**
- **FCFS**(先来先服务):简单,对长作业有利
- **SJF/SRTF**(最短作业优先/最短剩余时间优先):平均等待时间最短,但可能饥饿
- **HRRF**(最高响应比优先):响应比 = 1 + 等待时间/运行时间
- **RR**时间片轮转时间片太大退化为FCFS太小上下文切换开销大
- **MFQ**(多级反馈队列):综合了多种算法优点
- **优先级调度**:可能导致低优先级饥饿
- **优先级反转**:高优先级进程等待低优先级进程持有的资源
- 解决方案:优先级继承协议
- **EDF**(最早截止时间优先):实时调度
- **LLF**(最低松弛度优先):松弛度 = 截止时间 - 剩余执行时间
- **CFS**完全公平调度Linux默认
- vruntime虚拟运行时间
- 红黑树组织运行队列
- **调度公式**
```
周转时间 = 完成时间 - 到达时间
带权周转时间 = 周转时间 / 运行时间
平均周转时间 = Σ周转时间 / n
```
---
### 第14章死锁 ★★★
**考点:必要条件、资源分配图、银行家算法**
- **四个必要条件**
1. 互斥条件
2. 请求和保持条件
3. 不可抢占条件
4. 循环等待条件
- **资源分配图**:检测死锁的图形化方法
- **银行家算法**
```
Available: 系统可用资源向量
Max: 进程最大需求矩阵
Allocation: 已分配矩阵
Need = Max - Allocation: 还需要矩阵
安全性检查:
1. Work = Available, Finish = false
2. 找一个 Need ≤ Work 且 Finish=false 的进程
3. Work += Allocation, Finish = true
4. 重复直到所有 Finish=true安全或找不到不安全
```
- **死锁处理**
- 预防:破坏四个必要条件之一
- 避免:银行家算法
- 检测:资源分配图
- 恢夺:终止进程或资源抢占
---
### 第15章内存管理 ★★★★
**考点地址转换、页表、TLB、多级页表、段页式**
- **逻辑地址 vs 物理地址**
- **分页管理**
- 虚拟页号(VPN) + 页内偏移(VPO)
- 物理帧号(PPN) + 帧内偏移(PPO)
- 页表项有效位、脏位、引用位、R/W位、U/S位
- **缺页处理**
```
访问页表 → 有效位=0 → 缺页异常
→ 选择牺牲页(若修改过则写回磁盘)
→ 从磁盘调入新页
→ 更新页表 → 重新执行指令
```
- **TLB快表**
```
有效访问时间 EAT = λ + t + (1-a)·t
λ: TLB访问时间
t: 内存访问时间
a: TLB命中率
```
- **多级页表**:减少页表占用的连续内存
- 二级页表VPN分为两部分第一级索引页目录第二级索引页表
- **倒排页表**:按物理帧组织,减少内存占用但查找慢
- **段页式**:三维地址(段号 + 页号 + 页内偏移)
---
### 第16章虚拟内存 ★★★★★必考
**考点:页面置换算法、工作集、抖动**
- **请求调页**:访问时才调入页面
- **局部性原理**
- 时间局部性:最近访问的数据可能再次访问
- 空间局部性:相邻地址可能被访问
- **页面置换算法**(重点中的重点):
- **OPT**(最优算法):替换将来最长时间不使用的页面,理论最优但不可实现
- **FIFO**(先进先出):可能产生**Belady异常**(增加页框数反而增加缺页次数)
- **LRU**最近最久未使用替换最长时间未使用的页面无Belady异常
- **Clock算法**LRU的近似检查引用位
- **LFU**(最不经常使用):替换访问次数最少的页面
- **抖动Thrashing**
- 分配的页框数太少,频繁缺页
- 工作集 > 分配的页框数
- **工作集模型**
```
工作集 W(t, Δ) = 在时刻t前Δ时间窗口内访问的页面集合
Δ: 工作集窗口大小
```
- 若 W(t,Δ) > 分配页框数 → 抖动
- **内存分配策略**
- 固定分配 vs 可变分配
- 全局置换 vs 局部置换
---
### 第17章I/O系统 ★★★
**考点I/O控制方式、缓冲、磁盘调度**
- **四种I/O控制方式**
1. **程序直接控制**CPU忙等效率最低
2. **中断驱动**CPU不必忙等但每次传输一个字
3. **DMA**:直接内存访问,以块为单位传输
4. **通道**独立的I/O处理器执行通道程序
- **I/O软件层次**
```
用户层I/O软件
设备独立性软件
设备驱动程序
中断处理程序
硬件
```
- **缓冲**
- 单缓冲、双缓冲、循环缓冲、缓冲池
- **SPOOLing**:假脱机技术,将独占设备改为共享设备
- **磁盘调度算法**(必考):
- **FCFS**:简单但移动距离长
- **SSTF**(最短寻道时间优先):可能饥饿
- **SCAN**(电梯算法):双向扫描到边界
- **C-SCAN**:单向扫描,返回时快速移动
- **LOOK/C-LOOK**:改进版,不到达边界
- **磁盘访问时间**
```
访问时间 = 寻道时间 + 旋转延迟 + 传输时间
```
---
### 第18章代码优化
**考点CPE、代码移动、消除别名、循环展开**
- **CPE**(每元素周期数):衡量循环性能
- **优化技术**
1. **代码移动**:将循环不变量移到循环外
2. **消除过程调用**:用内联代码替代函数调用
3. **减少内存引用**:使用局部变量累积,最后写回
4. **消除指针别名**使用restrict关键字或引入局部变量
5. **循环展开**:减少循环控制开销
6. **SIMD**:单指令多数据流
- **指针别名问题**
```c
void twiddle1(int *xp, int *yp) {
*xp += *yp;
*xp += *yp;
}
// 若xp==yp结果是4倍否则是2倍
// 引入局部变量可消除别名影响
```
---
## 三、高频计算题型总结
### 1. 磁盘调度计算(每年必考)
**题型**:给定磁道请求序列和磁头初始位置,计算各算法的磁头移动总道数
**解题模板**
```
请求序列98,183,37,122,14,124,65,67
初始位置53
FCFS: 53→98→183→37→122→14→124→65→67
SSTF: 53→65→67→37→14→98→122→124→183
SCAN: 53→37→14→0→65→67→98→122→124→183
C-SCAN: 53→65→67→98→122→124→183→0→14→37
```
---
### 2. 虚拟内存页面置换(每年必考)
**题型**:给定页面访问序列和页框数,计算各算法的缺页次数
**解题模板**
```
访问序列7,0,1,2,0,3,0,4,2,3,0,3,2,1,2,0,1,7,0,1
页框数3
OPT: 替换将来最长时间不用的
FIFO: 替换最早进入的
LRU: 替换最长时间未使用的
```
---
### 3. fork进程树分析每年必考
**题型**分析fork()调用序列,画出进程树
**关键规则**
- fork()创建子进程子进程从fork处继续执行
- 进程总数 = 2^nn为fork调用次数假设无嵌套
- 嵌套fork需仔细跟踪每个进程的执行路径
---
### 4. CPU调度计算
**题型**:给定进程到达时间和运行时间,计算各算法的周转时间
**公式**
```
周转时间 = 完成时间 - 到达时间
带权周转时间 = 周转时间 / 运行时间
```
---
### 5. Ext2 inode最大文件计算
**题型**给定块大小和指针大小计算单个inode支持的最大文件
**公式**
```
每块指针数 = 块大小 / 指针大小
直接索引12块
一次间接:块指针数
二次间接:块指针数²
三次间接:块指针数³
```
---
### 6. TLB有效访问时间计算
**题型**给定TLB访问时间、内存访问时间、TLB命中率计算EAT
**公式**
```
EAT = λ + t + (1-a)·t
```
---
### 7. 银行家算法
**题型**:给定资源分配状态,判断是否安全
**步骤**
1. 计算Need矩阵 = Max - Allocation
2. 执行安全性检查算法
3. 判断是否存在安全序列
---
## 四、2026年考试预测
### 必考题型(概率>90%
1. 磁盘调度算法计算SSTF/SCAN/C-SCAN
2. 虚拟内存页面置换OPT/FIFO/LRU
3. fork进程树分析
4. 文件系统inode计算或Ext2结构分析
### 高概率题型概率60-90%
5. CPU调度算法HRRF或SJF
6. I/O控制方式比较
7. 段页式地址转换
### 可能题型概率30-60%
8. 银行家算法安全性检查
9. 代码优化(别名问题、循环展开)
10. 工作集模型与抖动分析
11. 多线程同步(生产者消费者)
---
## 五、复习建议
1. **重点掌握计算题**磁盘调度、页面置换、CPU调度、inode计算必考
2. **理解fork机制**:画进程树是每年必考题型
3. **掌握基本概念**I/O控制方式、死锁条件、虚拟内存原理
4. **练习画图**:进程树、资源分配图、地址转换过程
5. **熟悉公式**EAT、CPI、周转时间、响应比等

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,290 +1,290 @@
# 第8章 包图 # 第8章 包图
> **考试重要度**:★★ > **考试重要度**:★★
> **核心内容**:包的概念与可见性、包之间的依赖与泛化关系、四大设计原则 > **核心内容**:包的概念与可见性、包之间的依赖与泛化关系、四大设计原则
--- ---
## 📢 从文件夹理解包图 ## 📢 从文件夹理解包图
💬 你电脑上肯定有文件夹(目录),用来把相关文件放在一起。 💬 你电脑上肯定有文件夹(目录),用来把相关文件放在一起。
在UML中**包**就是"文件夹"——把关系密切的类、接口、组件等放在一起。 在UML中**包**就是"文件夹"——把关系密切的类、接口、组件等放在一起。
``` ```
┌─────────────────┐ ┌─────────────────┐
│ 包名 │ │ 包名 │
│ ┌───────────┐ │ │ ┌───────────┐ │
│ │ + 公开类 │ │ ← 被import后外部可用 │ │ + 公开类 │ │ ← 被import后外部可用
│ │ # 受保护 │ │ ← 只有子包可用 │ │ # 受保护 │ │ ← 只有子包可用
│ │ - 私有类 │ │ ← 只有包内部可用 │ │ - 私有类 │ │ ← 只有包内部可用
│ └───────────┘ │ │ └───────────┘ │
└─────────────────┘ └─────────────────┘
``` ```
--- ---
## 💡 一、包的可见性 ## 💡 一、包的可见性
跟类的可见性一样,包中的元素也有三种可见性: 跟类的可见性一样,包中的元素也有三种可见性:
| 可见性 | 符号 | 含义 | | 可见性 | 符号 | 含义 |
|--------|------|------| |--------|------|------|
| **公有** | `+` | 任何导入此包的包都可以用 | | **公有** | `+` | 任何导入此包的包都可以用 |
| **受保护** | `#` | 只有子包可以用 | | **受保护** | `#` | 只有子包可以用 |
| **私有** | `-` | 只有包内部可以用 | | **私有** | `-` | 只有包内部可以用 |
--- ---
## 💡 二、包之间的关系 ## 💡 二、包之间的关系
### 1. 依赖关系 ### 1. 依赖关系
> 包A中的某个类依赖于包B中的某个类 → 包A依赖于包B。 > 包A中的某个类依赖于包B中的某个类 → 包A依赖于包B。
⚠️ **注意**:包之间的依赖关系**没有传递性**A依赖BB依赖C不等于A依赖C。 ⚠️ **注意**:包之间的依赖关系**没有传递性**A依赖BB依赖C不等于A依赖C。
### 2. 泛化关系 ### 2. 泛化关系
> 子包继承父包中可见性为public和protected的元素。 > 子包继承父包中可见性为public和protected的元素。
### 📌 包图各元素的画法速查 ### 📌 包图各元素的画法速查
> 这一节把包图里所有的画图元素(包本身、依赖、泛化)用字符画列出来。 > 这一节把包图里所有的画图元素(包本身、依赖、泛化)用字符画列出来。
#### ① 包的两种画法 #### ① 包的两种画法
**标准文件夹画法**(最常用): **标准文件夹画法**(最常用):
``` ```
┌──────────────┐ ┌──────────────┐
│ UI层 │ ← 上方有"小标签"(像文件夹的舌头) │ UI层 │ ← 上方有"小标签"(像文件夹的舌头)
└──────────────┘ └──────────────┘
``` ```
字符画表示(小标签在左上角): 字符画表示(小标签在左上角):
``` ```
┌────────────┐ ┌────────────┐
│┌─┐ │ │┌─┐ │
││ │ │ ││ │ │
│└─┘─────────┤ │└─┘─────────┤
│ UI层 │ │ UI层 │
│ │ │ │
│ ┌─────────┐ │ │ ┌─────────┐ │
│ │ 登录页 │ │ ← 包内可以嵌套子包或类 │ │ 登录页 │ │ ← 包内可以嵌套子包或类
│ └─────────┘ │ │ └─────────┘ │
└─────────────┘ └─────────────┘
``` ```
**简化画法**(只用矩形): **简化画法**(只用矩形):
``` ```
┌──────────────┐ ┌──────────────┐
│ UI层 │ │ UI层 │
│ ┌────────┐ │ │ ┌────────┐ │
│ │ 登录页 │ │ │ │ 登录页 │ │
│ └────────┘ │ │ └────────┘ │
│ ┌────────┐ │ │ ┌────────┐ │
│ │ 主页面 │ │ │ │ 主页面 │ │
│ └────────┘ │ │ └────────┘ │
└──────────────┘ └──────────────┘
``` ```
#### ② 包的嵌套 #### ② 包的嵌套
``` ```
┌──────────────────────┐ ┌──────────────────────┐
│ 电子商务系统 │ │ 电子商务系统 │
│ ┌──────────┐ │ │ ┌──────────┐ │
│ │ 用户模块 │ │ │ │ 用户模块 │ │
│ │ ┌──────┐ │ │ │ │ ┌──────┐ │ │
│ │ │登录类│ │ │ │ │ │登录类│ │ │
│ │ └──────┘ │ │ │ │ └──────┘ │ │
│ │ ┌──────┐ │ │ │ │ ┌──────┐ │ │
│ │ │注册类│ │ │ │ │ │注册类│ │ │
│ │ └──────┘ │ │ │ │ └──────┘ │ │
│ └──────────┘ │ │ └──────────┘ │
│ ┌──────────┐ │ │ ┌──────────┐ │
│ │ 订单模块 │ │ │ │ 订单模块 │ │
│ └──────────┘ │ │ └──────────┘ │
└──────────────────────┘ └──────────────────────┘
``` ```
#### ③ 包的可见性符号 #### ③ 包的可见性符号
``` ```
┌────────────────┐ ┌────────────────┐
│ ┌─┐ │ │ ┌─┐ │
│ │+│ 公开类 │ ← + 公有:任何外部包都可用 │ │+│ 公开类 │ ← + 公有:任何外部包都可用
│ └─┘───────────┤ │ └─┘───────────┤
│ ┌─┐ │ │ ┌─┐ │
│ │#│ 受保护类 │ ← # 受保护:只有子包可用 │ │#│ 受保护类 │ ← # 受保护:只有子包可用
│ └─┘───────────┤ │ └─┘───────────┤
│ ┌─┐ │ │ ┌─┐ │
│ │-│ 私有类 │ ← - 私有:只有包内部可用 │ │-│ 私有类 │ ← - 私有:只有包内部可用
│ └─┘───────────┤ │ └─┘───────────┤
│ │ │ │
└────────────────┘ └────────────────┘
``` ```
#### ④ 包之间的依赖关系 —— 虚线 + 开放箭头 #### ④ 包之间的依赖关系 —— 虚线 + 开放箭头
> 一个包中的类**引用**另一个包中的类 → 包之间形成依赖。 > 一个包中的类**引用**另一个包中的类 → 包之间形成依赖。
``` ```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ UI层 │ │ 业务层 │ │ UI层 │ │ 业务层 │
│ │ │ │ │ │ │ │
│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │
│ │登录窗口 │ │ │ │登录服务 │ │ │ │登录窗口 │ │ │ │登录服务 │ │
│ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │
│ │ │ │ │ │ │ │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
│ ▲ │ ▲
└ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┘ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─┘
虚线 + 开放箭头 虚线 + 开放箭头
依赖方向UI层 依赖 业务层) 依赖方向UI层 依赖 业务层)
``` ```
⚠️ **依赖方向**:箭头从"使用者"指向"被使用者"(类似类图里的依赖)。 ⚠️ **依赖方向**:箭头从"使用者"指向"被使用者"(类似类图里的依赖)。
#### ⑤ 包之间的泛化关系 —— 实线 + 空心三角 #### ⑤ 包之间的泛化关系 —— 实线 + 空心三角
> 子包继承父包的内容。子包继承父包中**public 和 protected** 的元素。 > 子包继承父包的内容。子包继承父包中**public 和 protected** 的元素。
``` ```
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 业务层 │ │ 通用层 │ │ 业务层 │ │ 通用层 │
│ (子包) │ ─ ─ ─ ─ ─ ─ ─ ▷ │ (父包) │ │ (子包) │ ─ ─ ─ ─ ─ ─ ─ ▷ │ (父包) │
│ │ 空心三角 │ │ │ │ 空心三角 │ │
│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │
│ │订单处理 │ │ │ │日志工具 │ │ │ │订单处理 │ │ │ │日志工具 │ │
│ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │
│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │
│ │支付处理 │ │ │ │通用工具 │ │ │ │支付处理 │ │ │ │通用工具 │ │
│ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
``` ```
⚠️ **泛化方向**:空心三角**指向父包**(与类图泛化一致)。 ⚠️ **泛化方向**:空心三角**指向父包**(与类图泛化一致)。
#### ⑥ 完整包图示例 #### ⑥ 完整包图示例
``` ```
┌────────────────┐ ┌────────────────┐
│ 表现层 │ │ 表现层 │
│ ┌────────┐ │ │ ┌────────┐ │
│ │登录页 │ │ │ │登录页 │ │
│ └────────┘ │ │ └────────┘ │
│ ┌────────┐ │ │ ┌────────┐ │
│ │主窗体 │ │ │ │主窗体 │ │
│ └────────┘ │ │ └────────┘ │
└───────┬────────┘ └───────┬────────┘
│ 依赖 │ 依赖
╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌
┌────────────────┐ ▷ ┌────────────────┐ ┌────────────────┐ ▷ ┌────────────────┐
│ 业务层 │ ─ ─ ─ ─ ─ ─ ─ ─ ─│ 通用层 │ │ 业务层 │ ─ ─ ─ ─ ─ ─ ─ ─ ─│ 通用层 │
│ │ 泛化 │ │ │ │ 泛化 │ │
│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │
│ │登录管理 │ │ │ │日志工具 │ │ │ │登录管理 │ │ │ │日志工具 │ │
│ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │
│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │
│ │订单管理 │ │ │ │数据校验 │ │ │ │订单管理 │ │ │ │数据校验 │ │
│ └────────┘ │ │ └────────┘ │ │ └────────┘ │ │ └────────┘ │
└───────┬────────┘ └────────────────┘ └───────┬────────┘ └────────────────┘
│ 依赖 │ 依赖
╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌ ╌
┌────────────────┐ ┌────────────────┐
│ 数据层 │ │ 数据层 │
│ ┌────────┐ │ │ ┌────────┐ │
│ │用户DAO │ │ │ │用户DAO │ │
│ └────────┘ │ │ └────────┘ │
│ ┌────────┐ │ │ ┌────────┐ │
│ │订单DAO │ │ │ │订单DAO │ │
│ └────────┘ │ │ └────────┘ │
└────────────────┘ └────────────────┘
``` ```
🔑 **一句话记忆口诀**:包用文件夹画,标签是包名;包间依赖是虚线开放箭头(指向被依赖),包间泛化是实线空心三角(指向父包);包可以嵌套,元素分+/-可见性。 🔑 **一句话记忆口诀**:包用文件夹画,标签是包名;包间依赖是虚线开放箭头(指向被依赖),包间泛化是实线空心三角(指向父包);包可以嵌套,元素分+/-可见性。
--- ---
## 💡 三、包的四大设计原则 ## 💡 三、包的四大设计原则
这些原则告诉你**怎么把类分到不同的包里** 这些原则告诉你**怎么把类分到不同的包里**
### 1. 重用等价原则REP ### 1. 重用等价原则REP
> 把可以一起复用的类放在一个包中。包 = 可重用的单元。 > 把可以一起复用的类放在一个包中。包 = 可重用的单元。
💬 就像一个工具包——扳手和螺丝刀放在一起,因为它们总是一起用。 💬 就像一个工具包——扳手和螺丝刀放在一起,因为它们总是一起用。
### 2. 共同闭包原则CCP ### 2. 共同闭包原则CCP
> 把需要同时修改的类放在一个包中。 > 把需要同时修改的类放在一个包中。
💬 修改了类A就必须修改类B那它们应该在一个包里——改一个包就够了。 💬 修改了类A就必须修改类B那它们应该在一个包里——改一个包就够了。
### 3. 共同重用原则CRP ### 3. 共同重用原则CRP
> 不会一起用的类不要放在同一个包里。 > 不会一起用的类不要放在同一个包里。
💬 别把厨房用品和修车工具放一起——要用厨房用品的人不需要修车工具。 💬 别把厨房用品和修车工具放一起——要用厨房用品的人不需要修车工具。
### 4. 非循环依赖原则ADP ### 4. 非循环依赖原则ADP
> 包之间的依赖关系**不能形成环**。 > 包之间的依赖关系**不能形成环**。
💬 A→B→C→A 这样的循环依赖会严重妨碍复用。破解方法:提取共同接口或拆分包。 💬 A→B→C→A 这样的循环依赖会严重妨碍复用。破解方法:提取共同接口或拆分包。
### 🔑 核心目标:高内聚、低耦合 ### 🔑 核心目标:高内聚、低耦合
| 原则 | 作用 | | 原则 | 作用 |
|------|------| |------|------|
| **高内聚** | 包内的类关系紧密REP + CCP | | **高内聚** | 包内的类关系紧密REP + CCP |
| **低耦合** | 包之间的依赖尽量少CRP + ADP | | **低耦合** | 包之间的依赖尽量少CRP + ADP |
--- ---
## ✍️ 边学边练 ## ✍️ 边学边练
**题目**:判断以下做法是否合理。 **题目**:判断以下做法是否合理。
1. 把订单处理类和用户登录类放在同一个包 1. 把订单处理类和用户登录类放在同一个包
2. A包依赖B包B包依赖C包C包依赖A包 2. A包依赖B包B包依赖C包C包依赖A包
3. 把所有工具类放在一个util包中让其他包依赖它 3. 把所有工具类放在一个util包中让其他包依赖它
4. 把用户界面类、数据库访问类、业务逻辑类放在同一个包 4. 把用户界面类、数据库访问类、业务逻辑类放在同一个包
**答案:** **答案:**
1. ❌ 不合理 — 违反CRP不会一起使用的类不要放一起。订单处理的人不需要登录功能。 1. ❌ 不合理 — 违反CRP不会一起使用的类不要放一起。订单处理的人不需要登录功能。
2. ❌ 不合理 — 违反ADP循环依赖 2. ❌ 不合理 — 违反ADP循环依赖
3. ✅ 合理 — 符合REP工具类作为可重用单元 3. ✅ 合理 — 符合REP工具类作为可重用单元
4. ❌ 不合理 — 违反CCP和CRP。不同层次的类界面/数据/逻辑)修改原因不同,应该分开。 4. ❌ 不合理 — 违反CCP和CRP。不同层次的类界面/数据/逻辑)修改原因不同,应该分开。
⚠️ **实用建议**通常按层次分包UI层、业务层、数据层而不是按功能分包。 ⚠️ **实用建议**通常按层次分包UI层、业务层、数据层而不是按功能分包。
--- ---
## 📝 章末自测 ## 📝 章末自测
**1. 填空题** **1. 填空题**
- 包的四大设计原则缩写是:( ___ ___ )、( ___ ___ )、( ___ ___ )、( ___ ___ - 包的四大设计原则缩写是:( ___ ___ )、( ___ ___ )、( ___ ___ )、( ___ ___
- ADP原则要求包之间的依赖不能形成 ___ ___ - ADP原则要求包之间的依赖不能形成 ___ ___
- 包中元素的三种可见性是:公有( ___ ___ )、受保护( ___ ___ )、私有( ___ ___ ) - 包中元素的三种可见性是:公有( ___ ___ )、受保护( ___ ___ )、私有( ___ ___ )
**2. 简答题** **2. 简答题**
- 简述REP和CRP之间的矛盾是什么 - 简述REP和CRP之间的矛盾是什么
- 包图和组件图的关系是什么? - 包图和组件图的关系是什么?
**答案:** **答案:**
**填空题**REP、CCP、CRP、ADP循环+、#、- **填空题**REP、CCP、CRP、ADP循环+、#、-
**简答题** **简答题**
- REP说要把可一起复用的类放在一个包这可能导致一个包很大包含所有可能用到的类CRP说不会一起用的类要分开这可能导致很多小包。两者需要权衡。 - REP说要把可一起复用的类放在一个包这可能导致一个包很大包含所有可能用到的类CRP说不会一起用的类要分开这可能导致很多小包。两者需要权衡。
- 包存放组件,组件包含类。包相当于文件夹,组件相当于文件。部署时:类→组件→包→结点。 - 包存放组件,组件包含类。包相当于文件夹,组件相当于文件。部署时:类→组件→包→结点。
--- ---
> 🔗 上一篇:[第7章 组件图与部署图](第07章-组件图与部署图.md) | 下一篇:[第9章 数据建模](第09章-数据建模.md) > 🔗 上一篇:[第7章 组件图与部署图](第07章-组件图与部署图.md) | 下一篇:[第9章 数据建模](第09章-数据建模.md)

View File

@@ -1,141 +1,141 @@
# 第9章 数据建模 # 第9章 数据建模
> **考试重要度**:★★★★ > **考试重要度**:★★★★
> **核心内容**:对象模型→数据模型的转换规则、多重性映射、泛化关系映射、三大范式 > **核心内容**:对象模型→数据模型的转换规则、多重性映射、泛化关系映射、三大范式
--- ---
## 📢 从类图到数据库表 ## 📢 从类图到数据库表
💬 前面用类图设计好了系统结构,但数据最终要存到数据库里。**数据建模**就是回答:怎么把"类"变成"数据库表" 💬 前面用类图设计好了系统结构,但数据最终要存到数据库里。**数据建模**就是回答:怎么把"类"变成"数据库表"
--- ---
## 💡 一、数据库设计的三个阶段 ## 💡 一、数据库设计的三个阶段
| 阶段 | 任务 | 产物 | | 阶段 | 任务 | 产物 |
|------|------|------| |------|------|------|
| **概念设计** | 把用户需求统一到一个逻辑结构中 | E-R图或UML类图 | | **概念设计** | 把用户需求统一到一个逻辑结构中 | E-R图或UML类图 |
| **逻辑设计** | 转换为具体DBMS支持的数据模型 | 关系模式(表结构) | | **逻辑设计** | 转换为具体DBMS支持的数据模型 | 关系模式(表结构) |
| **物理设计** | 选择存储方式、索引策略等 | 在特定数据库上实现 | | **物理设计** | 选择存储方式、索引策略等 | 在特定数据库上实现 |
💬 UML类图可以替代E-R图——类图不仅能描述数据还能描述行为触发器和存储过程 💬 UML类图可以替代E-R图——类图不仅能描述数据还能描述行为触发器和存储过程
### 关键概念速查 ### 关键概念速查
| 概念 | 说明 | | 概念 | 说明 |
|------|------| |------|------|
| **主键** | 唯一标识一条记录的字段 | | **主键** | 唯一标识一条记录的字段 |
| **外键** | 引用另一个表主键的字段 | | **外键** | 引用另一个表主键的字段 |
| **模式** | 所有表及表间关系的集合 | | **模式** | 所有表及表间关系的集合 |
| **索引** | 提高查询速度的数据结构 | | **索引** | 提高查询速度的数据结构 |
| **触发器** | 满足条件时自动执行的操作 | | **触发器** | 满足条件时自动执行的操作 |
| **存储过程** | 预编译的SQL语句集合 | | **存储过程** | 预编译的SQL语句集合 |
--- ---
## 💡 二、对象模型 → 数据模型转换规则 ## 💡 二、对象模型 → 数据模型转换规则
### 基本转换规则 ### 基本转换规则
``` ```
类 → 表 类 → 表
属性 → 字段(列) 属性 → 字段(列)
操作 → 触发器/存储过程 操作 → 触发器/存储过程
类间关系 → 表间关系(或单独的表) 类间关系 → 表间关系(或单独的表)
``` ```
### 多重性在数据模型中的映射(重点!) ### 多重性在数据模型中的映射(重点!)
| 多重性 | 映射方法 | | 多重性 | 映射方法 |
|--------|----------| |--------|----------|
| **1 对 0..\*** | 在多的一端加外键(指向一的一端的主键) | | **1 对 0..\*** | 在多的一端加外键(指向一的一端的主键) |
| **0..1 对 1** | 在0..1一端加外键指向1的一端的主键 | | **0..1 对 1** | 在0..1一端加外键指向1的一端的主键 |
| **0..\* 对 0..\*** | 新建一个关联表,两端主键作为外键 | | **0..\* 对 0..\*** | 新建一个关联表,两端主键作为外键 |
> 例:客户(Customer) 1 —— 0..* 订单(Order) > 例:客户(Customer) 1 —— 0..* 订单(Order)
> >
> → Order表加外键 `customer_id` 指向 Customer表的主键。 > → Order表加外键 `customer_id` 指向 Customer表的主键。
> 例:学生(Student) * —— * 课程(Course) > 例:学生(Student) * —— * 课程(Course)
> >
> → 新建选课表(Enrollment),包含 `student_id` 和 `course_id` 两个外键。 > → 新建选课表(Enrollment),包含 `student_id` 和 `course_id` 两个外键。
### 泛化关系在数据模型中的映射(三种方案) ### 泛化关系在数据模型中的映射(三种方案)
| 方案 | 做法 | 优点 | 缺点 | | 方案 | 做法 | 优点 | 缺点 |
|------|------|------|------| |------|------|------|------|
| **方案一** | 父类和每个子类各建一个表 | 结构清晰 | 表多查询要JOIN | | **方案一** | 父类和每个子类各建一个表 | 结构清晰 | 表多查询要JOIN |
| **方案二** | 不建父类表,父类属性放在每个子类表中 | 查询快 | 数据冗余 | | **方案二** | 不建父类表,父类属性放在每个子类表中 | 查询快 | 数据冗余 |
| **方案三** | 不建子类表,所有子类属性放在父类表中 | 表少 | 字段很多,有空值 | | **方案三** | 不建子类表,所有子类属性放在父类表中 | 表少 | 字段很多,有空值 |
💬 **方案一像"分家"**——各过各的,但联系起来需要花时间。 💬 **方案一像"分家"**——各过各的,但联系起来需要花时间。
💬 **方案二像"合住但分灶"**——各自独立但有些东西重复了。 💬 **方案二像"合住但分灶"**——各自独立但有些东西重复了。
💬 **方案三像"大家庭"**——都在一个屋檐下,但人多事杂。 💬 **方案三像"大家庭"**——都在一个屋檐下,但人多事杂。
--- ---
## 💡 三、三大数据库范式 ## 💡 三、三大数据库范式
| 范式 | 要求 | 解决的问题 | | 范式 | 要求 | 解决的问题 |
|------|------|------------| |------|------|------------|
| **1NF** | 每个字段值都是**不可再分**的原子值 | 消除重复列 | | **1NF** | 每个字段值都是**不可再分**的原子值 | 消除重复列 |
| **2NF** | 满足1NF + 非主键字段**完全依赖**主键 | 消除部分依赖 | | **2NF** | 满足1NF + 非主键字段**完全依赖**主键 | 消除部分依赖 |
| **3NF** | 满足2NF + 非主键字段**不传递依赖**主键 | 消除传递依赖 | | **3NF** | 满足2NF + 非主键字段**不传递依赖**主键 | 消除传递依赖 |
💬 **1NF**:别在一个格子里塞多个值。 💬 **1NF**:别在一个格子里塞多个值。
💬 **2NF**:每个字段只跟主键有关,别跟主键的一部分有关。 💬 **2NF**:每个字段只跟主键有关,别跟主键的一部分有关。
💬 **3NF**:非主键字段之间不要相互依赖。 💬 **3NF**:非主键字段之间不要相互依赖。
--- ---
## ✍️ 边学边练 ## ✍️ 边学边练
**题目**以下表是否符合3NF如果不符合说明原因并修正。 **题目**以下表是否符合3NF如果不符合说明原因并修正。
``` ```
订单表(Order) 订单表(Order)
订单ID (主键) 订单ID (主键)
客户ID 客户ID
客户姓名 客户姓名
客户电话 客户电话
商品ID 商品ID
商品名称 商品名称
数量 数量
下单日期 下单日期
``` ```
**答案:** **答案:**
不符合3NF。存在传递依赖 不符合3NF。存在传递依赖
- 客户姓名、客户电话 → 依赖客户ID不是直接依赖订单ID - 客户姓名、客户电话 → 依赖客户ID不是直接依赖订单ID
- 商品名称 → 依赖商品ID不是直接依赖订单ID - 商品名称 → 依赖商品ID不是直接依赖订单ID
**修正方案**(拆分成三个表): **修正方案**(拆分成三个表):
- 订单表订单ID、客户ID、商品ID、数量、下单日期 - 订单表订单ID、客户ID、商品ID、数量、下单日期
- 客户表客户ID、客户姓名、客户电话 - 客户表客户ID、客户姓名、客户电话
- 商品表商品ID、商品名称 - 商品表商品ID、商品名称
这样每个非主键字段都直接依赖于所在表的主键满足3NF。 这样每个非主键字段都直接依赖于所在表的主键满足3NF。
--- ---
## 📝 章末自测 ## 📝 章末自测
**1. 填空题** **1. 填空题**
- 1对多关联的映射方法是 ___ ___ )的一端加外键指向( ___ ___ )的一端 - 1对多关联的映射方法是 ___ ___ )的一端加外键指向( ___ ___ )的一端
- 多对多关联的映射方法是:新建一个( ___ ___ ),两边的主键作为外键 - 多对多关联的映射方法是:新建一个( ___ ___ ),两边的主键作为外键
- 3NF要求非主键字段之间没有 ___ ___ )依赖 - 3NF要求非主键字段之间没有 ___ ___ )依赖
**2. 简答题** **2. 简答题**
- 简述泛化关系映射的三种方案及其适用场景。 - 简述泛化关系映射的三种方案及其适用场景。
- 类图中的操作在数据模型中对应什么? - 类图中的操作在数据模型中对应什么?
**答案:** **答案:**
**填空题**:多、一;关联表;传递 **填空题**:多、一;关联表;传递
**简答题** **简答题**
- 方案一(每个类一个表):适合查询灵活、结构变化少的系统。方案二(父子属性合并到子表):适合子类差异大、按子类查询多的场景。方案三(所有属性合并到父表):适合子类差异小、字段不多的场景。 - 方案一(每个类一个表):适合查询灵活、结构变化少的系统。方案二(父子属性合并到子表):适合子类差异大、按子类查询多的场景。方案三(所有属性合并到父表):适合子类差异小、字段不多的场景。
- 类图中的操作对应数据库中的**触发器和存储过程**。触发器是满足条件自动执行的代码存储过程是预编译的SQL程序。 - 类图中的操作对应数据库中的**触发器和存储过程**。触发器是满足条件自动执行的代码存储过程是预编译的SQL程序。
--- ---
> 🔗 上一篇:[第8章 包图](第08章-包图.md) | 下一篇:[第11章 RUP统一过程](第11章-RUP统一过程.md) > 🔗 上一篇:[第8章 包图](第08章-包图.md) | 下一篇:[第11章 RUP统一过程](第11章-RUP统一过程.md)

View File

@@ -1,164 +1,164 @@
# 第11章 Rational统一过程RUP # 第11章 Rational统一过程RUP
> **考试重要度**:★★★ > **考试重要度**:★★★
> **核心内容**RUP的三大特点、四个阶段、九个核心工作流、六大最佳实践 > **核心内容**RUP的三大特点、四个阶段、九个核心工作流、六大最佳实践
--- ---
## 📢 软件开发不只是写代码 ## 📢 软件开发不只是写代码
💬 开发软件就像盖一座大楼——你不能上来就搬砖,得先画图纸、打地基、搭框架、装修…… 💬 开发软件就像盖一座大楼——你不能上来就搬砖,得先画图纸、打地基、搭框架、装修……
**RUP** 就是一套完整的"盖楼流程指南",告诉你:什么人、在什么时候、做什么事、怎么做。 **RUP** 就是一套完整的"盖楼流程指南",告诉你:什么人、在什么时候、做什么事、怎么做。
--- ---
## 💡 一、RUP是什么 ## 💡 一、RUP是什么
> **RUP (Rational Unified Process)** = 一套基于UML的软件开发过程框架。 > **RUP (Rational Unified Process)** = 一套基于UML的软件开发过程框架。
🔑 **核心公式**RUP = **用例驱动** + **以体系结构为中心** + **迭代和增量开发** 🔑 **核心公式**RUP = **用例驱动** + **以体系结构为中心** + **迭代和增量开发**
### 三大特点 ### 三大特点
| 特点 | 含义 | | 特点 | 含义 |
|------|------| |------|------|
| **用例驱动** | 从用例出发,贯穿需求→设计→实现→测试全过程 | | **用例驱动** | 从用例出发,贯穿需求→设计→实现→测试全过程 |
| **以体系结构为中心** | 先搭建系统骨架,再逐步填充细节 | | **以体系结构为中心** | 先搭建系统骨架,再逐步填充细节 |
| **迭代和增量开发** | 不是一口气做完,而是一轮一轮地完善 | | **迭代和增量开发** | 不是一口气做完,而是一轮一轮地完善 |
--- ---
## 💡 二、RUP的二维结构 ## 💡 二、RUP的二维结构
``` ```
┌─────────────────────────────────────┐ ┌─────────────────────────────────────┐
纵轴 │ 工作流 │ 纵轴 │ 工作流 │
(做 │ 业务建模 需求 分析设计 实现 测试 部署 │ (做 │ 业务建模 需求 分析设计 实现 测试 部署 │
什么) │ 配置管理 项目管理 环境 │ 什么) │ 配置管理 项目管理 环境 │
├─────────────────────────────────────┤ ├─────────────────────────────────────┤
横轴 │ 初始 → 细化 → 构造 → 移交 │ 横轴 │ 初始 → 细化 → 构造 → 移交 │
(什么 │ ↑ ↑ ↑ ↑ │ (什么 │ ↑ ↑ ↑ ↑ │
时候) │ 里程碑 里程碑 里程碑 里程碑 │ 时候) │ 里程碑 里程碑 里程碑 里程碑 │
└─────────────────────────────────────┘ └─────────────────────────────────────┘
``` ```
--- ---
## 💡 三、四个阶段(横轴) ## 💡 三、四个阶段(横轴)
| 阶段 | 核心任务 | 里程碑 | | 阶段 | 核心任务 | 里程碑 |
|------|----------|--------| |------|----------|--------|
| **初始 (Inception)** | 定义项目范围,评估可行性 | 生命周期目标里程碑 | | **初始 (Inception)** | 定义项目范围,评估可行性 | 生命周期目标里程碑 |
| **细化 (Elaboration)** | 设计体系结构,制定计划 | 生命周期体系结构里程碑 | | **细化 (Elaboration)** | 设计体系结构,制定计划 | 生命周期体系结构里程碑 |
| **构造 (Construction)** | 大规模开发,完成所有功能 | 初始操作能力里程碑 | | **构造 (Construction)** | 大规模开发,完成所有功能 | 初始操作能力里程碑 |
| **移交 (Transition)** | 交付用户,培训,部署 | 产品发布里程碑 | | **移交 (Transition)** | 交付用户,培训,部署 | 产品发布里程碑 |
💬 **比喻**:初始=立项审批,细化=画施工图,构造=主体施工,移交=交房验收。 💬 **比喻**:初始=立项审批,细化=画施工图,构造=主体施工,移交=交房验收。
--- ---
## 💡 四、九个核心工作流(纵轴) ## 💡 四、九个核心工作流(纵轴)
### 六个过程工作流 ### 六个过程工作流
| 工作流 | 做什么 | 主要产物 | | 工作流 | 做什么 | 主要产物 |
|--------|--------|----------| |--------|--------|----------|
| **业务建模** | 理解业务现状 | 业务用例模型、业务对象模型 | | **业务建模** | 理解业务现状 | 业务用例模型、业务对象模型 |
| **需求** | 定义系统功能 | 用例模型、SRS | | **需求** | 定义系统功能 | 用例模型、SRS |
| **分析与设计** | 设计系统结构 | 分析模型、设计模型、数据模型 | | **分析与设计** | 设计系统结构 | 分析模型、设计模型、数据模型 |
| **实现** | 编写代码 | 源代码、可执行系统 | | **实现** | 编写代码 | 源代码、可执行系统 |
| **测试** | 验证系统质量 | 测试计划、测试报告 | | **测试** | 验证系统质量 | 测试计划、测试报告 |
| **部署** | 交付使用 | 安装包、用户手册、培训资料 | | **部署** | 交付使用 | 安装包、用户手册、培训资料 |
### 三个支持工作流 ### 三个支持工作流
| 工作流 | 做什么 | | 工作流 | 做什么 |
|--------|--------| |--------|--------|
| **配置与变更管理** | 控制制品版本和变更 | | **配置与变更管理** | 控制制品版本和变更 |
| **项目管理** | 计划、分配、监控项目 | | **项目管理** | 计划、分配、监控项目 |
| **环境** | 提供开发工具和过程支持 | | **环境** | 提供开发工具和过程支持 |
--- ---
## 💡 五、4W概念 ## 💡 五、4W概念
RUP回答了软件开发的四个基本问题 RUP回答了软件开发的四个基本问题
| 维度 | 问题 | RUP概念 | | 维度 | 问题 | RUP概念 |
|------|------|---------| |------|------|---------|
| **Who** | 谁来做? | 角色Role | | **Who** | 谁来做? | 角色Role |
| **How** | 怎么做? | 活动Activity | | **How** | 怎么做? | 活动Activity |
| **What** | 产出什么? | 制品Artifact | | **What** | 产出什么? | 制品Artifact |
| **When** | 什么时候做? | 工作流Workflow | | **When** | 什么时候做? | 工作流Workflow |
--- ---
## 💡 六、六大最佳实践 ## 💡 六、六大最佳实践
🔑 **这是考试的常考点!** 🔑 **这是考试的常考点!**
| 最佳实践 | 含义 | | 最佳实践 | 含义 |
|----------|------| |----------|------|
| **① 迭代式开发** | 不是一次做完,而是多轮迭代,每轮都有一个可运行版本 | | **① 迭代式开发** | 不是一次做完,而是多轮迭代,每轮都有一个可运行版本 |
| **② 管理需求** | 用用例来组织和管理需求,控制变更 | | **② 管理需求** | 用用例来组织和管理需求,控制变更 |
| **③ 使用基于构件的体系结构** | 把系统设计成可替换的构件 | | **③ 使用基于构件的体系结构** | 把系统设计成可替换的构件 |
| **④ 可视化软件建模** | 用UML图来描述系统结构和行为 | | **④ 可视化软件建模** | 用UML图来描述系统结构和行为 |
| **⑤ 验证软件质量** | 把质量评估嵌入整个过程,不是事后检查 | | **⑤ 验证软件质量** | 把质量评估嵌入整个过程,不是事后检查 |
| **⑥ 控制软件变更** | 通过配置管理跟踪和控制每个修改 | | **⑥ 控制软件变更** | 通过配置管理跟踪和控制每个修改 |
--- ---
## 💡 七、迭代 vs 瀑布 ## 💡 七、迭代 vs 瀑布
| 维度 | 瀑布模型 | RUP迭代 | | 维度 | 瀑布模型 | RUP迭代 |
|------|----------|---------| |------|----------|---------|
| **节奏** | 需求→设计→编码→测试,一次性 | 多轮迭代,每轮都走完整流程 | | **节奏** | 需求→设计→编码→测试,一次性 | 多轮迭代,每轮都走完整流程 |
| **风险** | 到最后才发现问题 | 早期就能发现和解决风险 | | **风险** | 到最后才发现问题 | 早期就能发现和解决风险 |
| **变更** | 不欢迎变更 | 每轮迭代可以调整方向 | | **变更** | 不欢迎变更 | 每轮迭代可以调整方向 |
| **产出** | 最后才有可运行版本 | 每轮都有可运行版本 | | **产出** | 最后才有可运行版本 | 每轮都有可运行版本 |
--- ---
## ✍️ 边学边练 ## ✍️ 边学边练
**题目**判断以下描述对应RUP的哪个阶段或工作流。 **题目**判断以下描述对应RUP的哪个阶段或工作流。
1. 项目经理制定本次迭代的计划 1. 项目经理制定本次迭代的计划
2. 测试人员编写测试用例并执行 2. 测试人员编写测试用例并执行
3. 确定系统的技术架构是B/S还是C/S 3. 确定系统的技术架构是B/S还是C/S
4. 系统上线,培训用户使用 4. 系统上线,培训用户使用
5. 绘制用例图,确定系统功能范围 5. 绘制用例图,确定系统功能范围
**答案:** **答案:**
1. **项目管理**(支持工作流)| 可在任何阶段 1. **项目管理**(支持工作流)| 可在任何阶段
2. **测试**(过程工作流)| 主要在构造阶段 2. **测试**(过程工作流)| 主要在构造阶段
3. **细化阶段** — 设计体系结构 3. **细化阶段** — 设计体系结构
4. **移交阶段** — 交付给用户 4. **移交阶段** — 交付给用户
5. **初始阶段****需求工作流** — 定义范围和功能 5. **初始阶段****需求工作流** — 定义范围和功能
--- ---
## 📝 章末自测 ## 📝 章末自测
**1. 填空题** **1. 填空题**
- RUP的三大特点是 ___ ___ )、( ___ ___ )、( ___ ___ - RUP的三大特点是 ___ ___ )、( ___ ___ )、( ___ ___
- RUP的四个阶段依次是 ___ ___ )、( ___ ___ )、( ___ ___ )、( ___ ___ - RUP的四个阶段依次是 ___ ___ )、( ___ ___ )、( ___ ___ )、( ___ ___
- 九个核心工作流中6个是 ___ ___ 工作流3个是 ___ ___ )工作流 - 九个核心工作流中6个是 ___ ___ 工作流3个是 ___ ___ )工作流
**2. 简答题** **2. 简答题**
- 简述RUP六大最佳实践。 - 简述RUP六大最佳实践。
- 迭代和增量开发相比瀑布模型有什么优势? - 迭代和增量开发相比瀑布模型有什么优势?
**答案:** **答案:**
**填空题** **填空题**
- 用例驱动、以体系结构为中心、迭代和增量开发 - 用例驱动、以体系结构为中心、迭代和增量开发
- 初始、细化、构造、移交 - 初始、细化、构造、移交
- 过程、支持 - 过程、支持
**简答题** **简答题**
- ①迭代式开发 ②管理需求 ③使用基于构件的体系结构 ④可视化软件建模 ⑤验证软件质量 ⑥控制软件变更 - ①迭代式开发 ②管理需求 ③使用基于构件的体系结构 ④可视化软件建模 ⑤验证软件质量 ⑥控制软件变更
- 迭代开发可以早期发现风险、更容易容纳需求变更、每轮都有可运行的版本(增强信心)、可以根据反馈及时调整方向。 - 迭代开发可以早期发现风险、更容易容纳需求变更、每轮都有可运行的版本(增强信心)、可以根据反馈及时调整方向。
--- ---
> 🔗 上一篇:[第9章 数据建模](第09章-数据建模.md) > 🔗 上一篇:[第9章 数据建模](第09章-数据建模.md)

View File

@@ -1,189 +1,189 @@
# 期中考试复习 # 期中考试复习
> **考试**2022-2023学年第1学期《软件需求分析与设计》期中考试 > **考试**2022-2023学年第1学期《软件需求分析与设计》期中考试
> **满分**100分 | 选择题30分 + 系统分析题70分 > **满分**100分 | 选择题30分 + 系统分析题70分
> **系统场景**:在线酒店订房系统 > **系统场景**:在线酒店订房系统
--- ---
## 📋 试卷结构一览 ## 📋 试卷结构一览
| 题型 | 题量 | 分值 | 考点 | | 题型 | 题量 | 分值 | 考点 |
|------|------|------|------| |------|------|------|------|
| 选择题 | 15题 | 30分2分/题) | UML基本概念、关系类型、面向对象特性 | | 选择题 | 15题 | 30分2分/题) | UML基本概念、关系类型、面向对象特性 |
| 系统分析题 | 7题 | 70分10分/题) | 7种UML图的实际绘制 | | 系统分析题 | 7题 | 70分10分/题) | 7种UML图的实际绘制 |
--- ---
## 🎯 七道大题逐题详解 ## 🎯 七道大题逐题详解
### 第1题用例图10分 ### 第1题用例图10分
> 画出在线订房系统的用例图。 > 画出在线订房系统的用例图。
**答案要点** **答案要点**
**参与者3个**:客户、管理员、身份信息平台 **参与者3个**:客户、管理员、身份信息平台
**用例10个** **用例10个**
- 客户相关:找回密码、注册、身份认证、登录、查询房间、下订单、在线支付 - 客户相关:找回密码、注册、身份认证、登录、查询房间、下订单、在线支付
- 管理员相关:登录、房间上线、修改房间信息、房间下线 - 管理员相关:登录、房间上线、修改房间信息、房间下线
**关键关系** **关键关系**
- **包含 `<<include>>`**:注册 → 身份认证;下订单 → 查询房间;在线支付 → 下订单 - **包含 `<<include>>`**:注册 → 身份认证;下订单 → 查询房间;在线支付 → 下订单
- **关联**:客户↔找回密码、注册、登录、查询房间、下订单;管理员↔登录、房间上线、修改、下线 - **关联**:客户↔找回密码、注册、登录、查询房间、下订单;管理员↔登录、房间上线、修改、下线
- **身份认证 → 身份信息平台**(外部系统交互) - **身份认证 → 身份信息平台**(外部系统交互)
⚠️ **易错**:包含关系箭头从基本用例指向包含用例! ⚠️ **易错**:包含关系箭头从基本用例指向包含用例!
--- ---
### 第2题"找回密码"用例描述10分 ### 第2题"找回密码"用例描述10分
> 写出"找回密码"的完整用例描述。 > 写出"找回密码"的完整用例描述。
**参考答案** **参考答案**
| 项目 | 内容 | | 项目 | 内容 |
|------|------| |------|------|
| **用例名** | 找回密码 | | **用例名** | 找回密码 |
| **参与者** | 客户 | | **参与者** | 客户 |
| **前置条件** | 客户已注册 | | **前置条件** | 客户已注册 |
| **基本流程** | ①输入手机号 → ②获取短信验证码 → ③输入验证码 → ④输入新密码(两次) → ⑤两次一致则密码修改成功 | | **基本流程** | ①输入手机号 → ②获取短信验证码 → ③输入验证码 → ④输入新密码(两次) → ⑤两次一致则密码修改成功 |
| **备选流程** | 若两次新密码不一致,提示重新输入 | | **备选流程** | 若两次新密码不一致,提示重新输入 |
| **后置条件** | 密码修改成功,用户可用新密码登录 | | **后置条件** | 密码修改成功,用户可用新密码登录 |
--- ---
### 第3题实体类类图10分 ### 第3题实体类类图10分
> 画出系统的实体类类图,标注多重性。 > 画出系统的实体类类图,标注多重性。
**答案要点** **答案要点**
**实体类**:客户(Customer)、房间(Room)、订单(Order)、管理员(Admin) **实体类**:客户(Customer)、房间(Room)、订单(Order)、管理员(Admin)
**关系及多重性** **关系及多重性**
``` ```
客户 1 ────── 0..* 订单 (一个客户可以有多个订单) 客户 1 ────── 0..* 订单 (一个客户可以有多个订单)
房间 1 ────── 0..* 订单 (一个房间可以被多次预订) 房间 1 ────── 0..* 订单 (一个房间可以被多次预订)
管理员 0..2 ── 0..* 订单 (管理员处理订单) 管理员 0..2 ── 0..* 订单 (管理员处理订单)
``` ```
--- ---
### 第4题"找回密码"的基于协作的类图10分 ### 第4题"找回密码"的基于协作的类图10分
> 使用边界类、控制类、实体类画出协作类图。 > 使用边界类、控制类、实体类画出协作类图。
**答案要点** **答案要点**
| 版型 | 类名 | 职责 | | 版型 | 类名 | 职责 |
|------|------|------| |------|------|------|
| <<boundary>> | RetrievePasswordBoundary | 找回密码界面,与用户交互 | | <<boundary>> | RetrievePasswordBoundary | 找回密码界面,与用户交互 |
| <<control>> | RetrievePasswordControl | 协调找回密码的业务逻辑 | | <<control>> | RetrievePasswordControl | 协调找回密码的业务逻辑 |
| <<interface>> | SMSInterface | 短信接口(外部) | | <<interface>> | SMSInterface | 短信接口(外部) |
| <<entity>> | UserInfo | 用户信息(持久化) | | <<entity>> | UserInfo | 用户信息(持久化) |
**依赖关系** **依赖关系**
``` ```
Boundary → Control → SMSInterface Boundary → Control → SMSInterface
→ UserInfo → UserInfo
``` ```
--- ---
### 第5题"找回密码"的顺序图10分 ### 第5题"找回密码"的顺序图10分
> 画出"找回密码"的顺序图,标注消息编号。 > 画出"找回密码"的顺序图,标注消息编号。
**答案要点** **答案要点**
**对象**:用户、:RetrievePasswordBoundary、:RetrievePasswordControl、:UserInfo、:SMSInterface **对象**:用户、:RetrievePasswordBoundary、:RetrievePasswordControl、:UserInfo、:SMSInterface
**消息序列(核心流程)** **消息序列(核心流程)**
``` ```
1: 用户 → 边界类 : 找回密码() 1: 用户 → 边界类 : 找回密码()
1.1: 用户 → 边界类 : 输入手机号 1.1: 用户 → 边界类 : 输入手机号
1.1.1: 边界 → 控制 : 获取短信(手机号) 1.1.1: 边界 → 控制 : 获取短信(手机号)
1.1.1.1: 控制 → SMS : 获取短信(手机号) 1.1.1.1: 控制 → SMS : 获取短信(手机号)
2: 用户 → 边界类 : 输入验证码 2: 用户 → 边界类 : 输入验证码
2.1: 边界 → 控制 : 验证(验证码) 2.1: 边界 → 控制 : 验证(验证码)
2.1.1: 控制 → UserInfo : 验证(验证码) 2.1.1: 控制 → UserInfo : 验证(验证码)
3: 用户 → 边界类 : 输入(新密码1, 新密码2) 3: 用户 → 边界类 : 输入(新密码1, 新密码2)
3.1: 边界 → 控制 : 验证一致性(p1, p2) 3.1: 边界 → 控制 : 验证一致性(p1, p2)
3.1.1: [一致] 控制 → UserInfo : 写入新密码 3.1.1: [一致] 控制 → UserInfo : 写入新密码
``` ```
🔑 **编号规则**`1``1.1``1.1.1` 表示嵌套调用,`1.1.1` 必须在 `1.1` 完成之前完成。 🔑 **编号规则**`1``1.1``1.1.1` 表示嵌套调用,`1.1.1` 必须在 `1.1` 完成之前完成。
--- ---
### 第6题"订单"的状态图10分 ### 第6题"订单"的状态图10分
> 画出实体类"订单"的状态图。 > 画出实体类"订单"的状态图。
**答案要点** **答案要点**
``` ```
初始 → 待支付 → 已支付 → 已入住 → 完成 初始 → 待支付 → 已支付 → 已入住 → 完成
↓ ↓ ↓ ↓
取消 失效 取消 失效
``` ```
| 转移 | 触发事件 | | 转移 | 触发事件 |
|------|----------| |------|----------|
| 初始 → 待支付 | 下单 | | 初始 → 待支付 | 下单 |
| 待支付 → 取消 | 未支付 | | 待支付 → 取消 | 未支付 |
| 待支付 → 已支付 | 成功支付 | | 待支付 → 已支付 | 成功支付 |
| 已支付 → 失效 | 超时 | | 已支付 → 失效 | 超时 |
| 已支付 → 已入住 | 入住 | | 已支付 → 已入住 | 入住 |
| 已入住 → 完成 | 退房 | | 已入住 → 完成 | 退房 |
--- ---
### 第7题"找回密码"的带泳道活动图10分 ### 第7题"找回密码"的带泳道活动图10分
> 画出"找回密码"的带泳道活动图。 > 画出"找回密码"的带泳道活动图。
**答案要点** **答案要点**
**泳道5条**:用户 | 找回密码边界类 | 找回密码控制类 | 短信接口 | 用户信息 **泳道5条**:用户 | 找回密码边界类 | 找回密码控制类 | 短信接口 | 用户信息
**活动流程** **活动流程**
``` ```
用户:开始 → 找回密码 → 输入手机号 用户:开始 → 找回密码 → 输入手机号
控制类 → 短信接口:获取短信验证码 → 返回验证码 控制类 → 短信接口:获取短信验证码 → 返回验证码
用户:输入验证码 → 输入2次新密码 用户:输入验证码 → 输入2次新密码
控制类:验证两次密码 控制类:验证两次密码
├─ [不一致] → 结束 ├─ [不一致] → 结束
└─ [一致] → 用户信息:写入新密码 → 结束 └─ [一致] → 用户信息:写入新密码 → 结束
``` ```
--- ---
## 📊 七道题的分值分布与重点 ## 📊 七道题的分值分布与重点
| 题号 | 考什么 | 难度 | 必会程度 | | 题号 | 考什么 | 难度 | 必会程度 |
|------|--------|------|----------| |------|--------|------|----------|
| 1 | 用例图 | ⭐⭐ | ★★★★★ | | 1 | 用例图 | ⭐⭐ | ★★★★★ |
| 2 | 用例描述 | ⭐ | ★★★★★ | | 2 | 用例描述 | ⭐ | ★★★★★ |
| 3 | 类图(实体类) | ⭐⭐ | ★★★★★ | | 3 | 类图(实体类) | ⭐⭐ | ★★★★★ |
| 4 | 协作类图MVC | ⭐⭐⭐ | ★★★★ | | 4 | 协作类图MVC | ⭐⭐⭐ | ★★★★ |
| 5 | 顺序图(消息编号) | ⭐⭐⭐ | ★★★★ | | 5 | 顺序图(消息编号) | ⭐⭐⭐ | ★★★★ |
| 6 | 状态图 | ⭐⭐ | ★★★★★ | | 6 | 状态图 | ⭐⭐ | ★★★★★ |
| 7 | 活动图(带泳道) | ⭐⭐⭐ | ★★★★ | | 7 | 活动图(带泳道) | ⭐⭐⭐ | ★★★★ |
--- ---
## 🔑 考前必记要点 ## 🔑 考前必记要点
1. **包含关系 vs 扩展关系**:包含=必然,扩展=条件。箭头方向不同! 1. **包含关系 vs 扩展关系**:包含=必然,扩展=条件。箭头方向不同!
2. **多重性**`1` `0..1` `0..*` `1..*` 的含义和标注位置 2. **多重性**`1` `0..1` `0..*` `1..*` 的含义和标注位置
3. **边界类/控制类/实体类**View/Controller/Model 对应关系 3. **边界类/控制类/实体类**View/Controller/Model 对应关系
4. **消息编号层级**`1.1.1` 表示嵌套调用深度 4. **消息编号层级**`1.1.1` 表示嵌套调用深度
5. **状态图**:别忘了初始状态(实心圆)和终态(圆圈+实心圆) 5. **状态图**:别忘了初始状态(实心圆)和终态(圆圈+实心圆)
6. **活动图泳道**:根据参与者来划分,每道一个责任区 6. **活动图泳道**:根据参与者来划分,每道一个责任区

View File

@@ -1,101 +1,101 @@
# UML九图速查手册 # UML九图速查手册
> **用途**考前快速回顾9种UML图的核心特征 > **用途**考前快速回顾9种UML图的核心特征
--- ---
## 📊 九种UML图一句话定位 ## 📊 九种UML图一句话定位
| 想表达什么? | 用什么图? | | 想表达什么? | 用什么图? |
|--------------|-----------| |--------------|-----------|
| 系统能干什么? | **用例图** | | 系统能干什么? | **用例图** |
| 系统里有哪些类?它们什么关系? | **类图** | | 系统里有哪些类?它们什么关系? | **类图** |
| 某一刻对象的具体情况? | **对象图** | | 某一刻对象的具体情况? | **对象图** |
| 对象之间怎么按时间顺序交互? | **顺序图** | | 对象之间怎么按时间顺序交互? | **顺序图** |
| 对象之间怎么在空间上协作? | **协作图** | | 对象之间怎么在空间上协作? | **协作图** |
| 一个对象的状态怎么变化? | **状态图** | | 一个对象的状态怎么变化? | **状态图** |
| 一个业务流程步骤怎么走? | **活动图** | | 一个业务流程步骤怎么走? | **活动图** |
| 代码文件之间怎么依赖? | **组件图** | | 代码文件之间怎么依赖? | **组件图** |
| 软件部署在哪些硬件上? | **部署图** | | 软件部署在哪些硬件上? | **部署图** |
--- ---
## 🔗 九图分类 ## 🔗 九图分类
``` ```
UML图 UML图
├── 功能模型:用例图 ├── 功能模型:用例图
├── 静态模型(结构):类图、对象图、包图 ├── 静态模型(结构):类图、对象图、包图
├── 动态模型(行为):顺序图、协作图、状态图、活动图 ├── 动态模型(行为):顺序图、协作图、状态图、活动图
└── 物理模型(实现):组件图、部署图 └── 物理模型(实现):组件图、部署图
``` ```
--- ---
## 📋 九图完整对比表 ## 📋 九图完整对比表
| 图类型 | 分类 | 描述几个对象? | 强调 | 核心元素 | | 图类型 | 分类 | 描述几个对象? | 强调 | 核心元素 |
|--------|------|:---:|------|----------| |--------|------|:---:|------|----------|
| **用例图** | 功能 | 多 | 系统功能全景 | 参与者、用例、系统边界 | | **用例图** | 功能 | 多 | 系统功能全景 | 参与者、用例、系统边界 |
| **类图** | 静态结构 | 多 | 类的定义和关系 | 类、属性、操作、关系 | | **类图** | 静态结构 | 多 | 类的定义和关系 | 类、属性、操作、关系 |
| **对象图** | 静态结构 | 多 | 某一时刻的快照 | 对象、属性值、链 | | **对象图** | 静态结构 | 多 | 某一时刻的快照 | 对象、属性值、链 |
| **包图** | 静态结构 | 多 | 类的分组组织 | 包、依赖、泛化 | | **包图** | 静态结构 | 多 | 类的分组组织 | 包、依赖、泛化 |
| **顺序图** | 动态交互 | 多 | 消息的时间顺序 | 对象、生命线、控制焦点、消息 | | **顺序图** | 动态交互 | 多 | 消息的时间顺序 | 对象、生命线、控制焦点、消息 |
| **协作图** | 动态交互 | 多 | 对象的空间位置 | 对象、链、消息(带序号) | | **协作图** | 动态交互 | 多 | 对象的空间位置 | 对象、链、消息(带序号) |
| **状态图** | 动态行为 | **1个** | 状态如何变迁 | 状态、转移、事件 | | **状态图** | 动态行为 | **1个** | 状态如何变迁 | 状态、转移、事件 |
| **活动图** | 动态行为 | 多 | 流程步骤 | 活动、分支、泳道、分叉 | | **活动图** | 动态行为 | 多 | 流程步骤 | 活动、分支、泳道、分叉 |
| **组件图** | 物理实现 | 多 | 代码文件组织 | 组件、接口、依赖 | | **组件图** | 物理实现 | 多 | 代码文件组织 | 组件、接口、依赖 |
| **部署图** | 物理实现 | 多 | 硬件拓扑 | 结点、组件、连接 | | **部署图** | 物理实现 | 多 | 硬件拓扑 | 结点、组件、连接 |
--- ---
## 🎨 各图关键语法速查 ## 🎨 各图关键语法速查
### 用例图 ### 用例图
- 参与者 = 火柴人 🧑 - 参与者 = 火柴人 🧑
- 用例 = 椭圆 ○ - 用例 = 椭圆 ○
- 系统边界 = 方框 □ - 系统边界 = 方框 □
- 关系:包含 `<<include>>` (必然)、扩展 `<<extend>>` (条件) - 关系:包含 `<<include>>` (必然)、扩展 `<<extend>>` (条件)
### 类图 ### 类图
- 类 = 三格矩形(类名|属性|操作) - 类 = 三格矩形(类名|属性|操作)
- 可见性:`+`公有 `-`私有 `#`受保护 - 可见性:`+`公有 `-`私有 `#`受保护
- 六大关系:关联(实线)、聚合(空心菱形)、组合(实心菱形)、依赖(虚线)、泛化(空心三角)、实现(虚线空心三角) - 六大关系:关联(实线)、聚合(空心菱形)、组合(实心菱形)、依赖(虚线)、泛化(空心三角)、实现(虚线空心三角)
### 顺序图 ### 顺序图
- 对象 + 生命线(虚线) + 控制焦点(矩形条) + 消息(箭头) - 对象 + 生命线(虚线) + 控制焦点(矩形条) + 消息(箭头)
- 调用消息 = 实心箭头,异步消息 = 开放箭头 - 调用消息 = 实心箭头,异步消息 = 开放箭头
- 嵌套1 → 1.1 → 1.1.1 - 嵌套1 → 1.1 → 1.1.1
### 协作图 ### 协作图
- 对象 + 链(实线) + 消息(带序号) - 对象 + 链(实线) + 消息(带序号)
- 必须标注消息序号!没有时间轴 - 必须标注消息序号!没有时间轴
### 状态图 ### 状态图
- 状态 = 圆角矩形,转移 = 箭头 - 状态 = 圆角矩形,转移 = 箭头
- 事件/ [条件] / 动作 - 事件/ [条件] / 动作
- 初始状态 ● → 终态 ⊙ - 初始状态 ● → 终态 ⊙
### 活动图 ### 活动图
- 活动 = 圆角矩形 - 活动 = 圆角矩形
- 分支 = 菱形 ◇ - 分支 = 菱形 ◇
- 分叉/汇合 = 粗横线 - 分叉/汇合 = 粗横线
- 泳道 = 纵向分区 - 泳道 = 纵向分区
### 组件图 ### 组件图
- 组件 = 带<<component>>的矩形 - 组件 = 带<<component>>的矩形
- 关系主要是依赖(虚线箭头) - 关系主要是依赖(虚线箭头)
### 部署图 ### 部署图
- 结点 = 3D立方体 - 结点 = 3D立方体
- 通信路径 = 实线 - 通信路径 = 实线
--- ---
## 📐 MVC版型速查 ## 📐 MVC版型速查
| 版型 | 图标 | 职责 | 例子 | | 版型 | 图标 | 职责 | 例子 |
|------|------|------|------| |------|------|------|------|
| <<boundary>> 边界类 | 带竖线的圆 | 与外界交互 | 登录页面、购物车页面 | | <<boundary>> 边界类 | 带竖线的圆 | 与外界交互 | 登录页面、购物车页面 |
| <<control>> 控制类 | 带箭头的圆 | 协调逻辑 | 订单处理、支付验证 | | <<control>> 控制类 | 带箭头的圆 | 协调逻辑 | 订单处理、支付验证 |
| <<entity>> 实体类 | 带横线的圆 | 持久化数据 | 用户、订单、商品 | | <<entity>> 实体类 | 带横线的圆 | 持久化数据 | 用户、订单、商品 |

View File

@@ -1,110 +1,110 @@
# 设计原则与术语速查 # 设计原则与术语速查
> **用途**:考前快速回顾关键设计原则和术语 > **用途**:考前快速回顾关键设计原则和术语
--- ---
## 🔷 SOLID五大原则 ## 🔷 SOLID五大原则
| 原则 | 全称 | 含义 | | 原则 | 全称 | 含义 |
|------|------|------| |------|------|------|
| **S** | 单一职责原则 | 一个类只负责一件事 | | **S** | 单一职责原则 | 一个类只负责一件事 |
| **O** | 开闭原则 | 对扩展开放,对修改关闭 | | **O** | 开闭原则 | 对扩展开放,对修改关闭 |
| **L** | 里氏替换原则 | 子类可以替换父类而不出问题 | | **L** | 里氏替换原则 | 子类可以替换父类而不出问题 |
| **I** | 接口隔离原则 | 接口要小而精,不要大而全 | | **I** | 接口隔离原则 | 接口要小而精,不要大而全 |
| **D** | 依赖倒置原则 | 依赖抽象(接口),不依赖具体实现 | | **D** | 依赖倒置原则 | 依赖抽象(接口),不依赖具体实现 |
--- ---
## 🔷 包设计四大原则 ## 🔷 包设计四大原则
| 缩写 | 原则 | 含义 | | 缩写 | 原则 | 含义 |
|------|------|------| |------|------|------|
| **REP** | 重用等价原则 | 可一起复用的类放一个包 | | **REP** | 重用等价原则 | 可一起复用的类放一个包 |
| **CCP** | 共同闭包原则 | 需要同时修改的类放一个包 | | **CCP** | 共同闭包原则 | 需要同时修改的类放一个包 |
| **CRP** | 共同重用原则 | 不会一起用的类分开放 | | **CRP** | 共同重用原则 | 不会一起用的类分开放 |
| **ADP** | 非循环依赖原则 | 包之间的依赖不能形成环 | | **ADP** | 非循环依赖原则 | 包之间的依赖不能形成环 |
--- ---
## 🔷 其他重要原则 ## 🔷 其他重要原则
| 原则 | 含义 | | 原则 | 含义 |
|------|------| |------|------|
| **高内聚低耦合** | 包内关系紧密(内聚),包间关系稀疏(耦合) | | **高内聚低耦合** | 包内关系紧密(内聚),包间关系稀疏(耦合) |
| **迪米特法则** | 最少知识原则——只跟"朋友"说话 | | **迪米特法则** | 最少知识原则——只跟"朋友"说话 |
| **组合优于继承** | 能用组合实现的效果,别用继承(更灵活) | | **组合优于继承** | 能用组合实现的效果,别用继承(更灵活) |
--- ---
## 🔷 数据库三大范式 ## 🔷 数据库三大范式
| 范式 | 要求 | | 范式 | 要求 |
|------|------| |------|------|
| **1NF** | 字段值不可再分(原子性) | | **1NF** | 字段值不可再分(原子性) |
| **2NF** | 非主键字段完全依赖主键(消除部分依赖) | | **2NF** | 非主键字段完全依赖主键(消除部分依赖) |
| **3NF** | 非主键字段不传递依赖主键(消除传递依赖) | | **3NF** | 非主键字段不传递依赖主键(消除传递依赖) |
--- ---
## 🔷 对象模型→数据模型映射速查 ## 🔷 对象模型→数据模型映射速查
| 对象模型 | 数据模型 | | 对象模型 | 数据模型 |
|----------|----------| |----------|----------|
| 类 | 表 | | 类 | 表 |
| 属性 | 字段(列) | | 属性 | 字段(列) |
| 操作 | 触发器 + 存储过程 | | 操作 | 触发器 + 存储过程 |
| 1对0..*关联 | 多的一端加外键 | | 1对0..*关联 | 多的一端加外键 |
| *对*关联 | 新建关联表 | | *对*关联 | 新建关联表 |
| 泛化关系 | 三种方案(父子各建表/只建子表/只建父表) | | 泛化关系 | 三种方案(父子各建表/只建子表/只建父表) |
--- ---
## 🔷 需求工程术语速查 ## 🔷 需求工程术语速查
| 术语 | 定义 | | 术语 | 定义 |
|------|------| |------|------|
| **软件需求** | 用户对系统在功能、行为、性能、约束方面的期望 | | **软件需求** | 用户对系统在功能、行为、性能、约束方面的期望 |
| **业务需求** | 组织的高层次目标 | | **业务需求** | 组织的高层次目标 |
| **用户需求** | 用户使用系统完成的任务 | | **用户需求** | 用户使用系统完成的任务 |
| **功能需求** | 系统必须实现的功能 | | **功能需求** | 系统必须实现的功能 |
| **非功能需求** | 系统的质量属性和约束 | | **非功能需求** | 系统的质量属性和约束 |
| **SRS** | 软件需求规格说明书 | | **SRS** | 软件需求规格说明书 |
| **需求基线** | 评审通过、双方承诺的需求版本 | | **需求基线** | 评审通过、双方承诺的需求版本 |
| **需求跟踪矩阵(RTM)** | 记录需求与设计、代码、测试对应关系的表 | | **需求跟踪矩阵(RTM)** | 记录需求与设计、代码、测试对应关系的表 |
| **需求变更控制** | 变更申请→审批→修改→重新确认的流程 | | **需求变更控制** | 变更申请→审批→修改→重新确认的流程 |
--- ---
## 🔷 面向对象核心概念 ## 🔷 面向对象核心概念
| 概念 | 含义 | | 概念 | 含义 |
|------|------| |------|------|
| **封装** | 隐藏内部实现,只暴露接口 | | **封装** | 隐藏内部实现,只暴露接口 |
| **继承** | 子类自动拥有父类的属性和方法 | | **继承** | 子类自动拥有父类的属性和方法 |
| **多态** | 同一操作在不同对象上有不同行为 | | **多态** | 同一操作在不同对象上有不同行为 |
| **抽象** | 提取共性,忽略细节 | | **抽象** | 提取共性,忽略细节 |
--- ---
## 🔷 关联关系多重性速记 ## 🔷 关联关系多重性速记
| 符号 | 含义 | | 符号 | 含义 |
|------|------| |------|------|
| `1` | 恰好1个 | | `1` | 恰好1个 |
| `0..1` | 0个或1个 | | `0..1` | 0个或1个 |
| `*``0..*` | 0个或多个 | | `*``0..*` | 0个或多个 |
| `1..*` | 至少1个 | | `1..*` | 至少1个 |
| `n` | 恰好n个 | | `n` | 恰好n个 |
--- ---
## 🔷 常见设计模式速查 ## 🔷 常见设计模式速查
| 模式 | 类型 | 解决什么问题 | | 模式 | 类型 | 解决什么问题 |
|------|------|-------------| |------|------|-------------|
| **策略模式** | 行为型 | 同一功能有多种算法实现,可动态切换 | | **策略模式** | 行为型 | 同一功能有多种算法实现,可动态切换 |
| **工厂方法** | 创建型 | 创建对象时不指定具体类 | | **工厂方法** | 创建型 | 创建对象时不指定具体类 |
| **组合模式** | 结构型 | 统一处理整体和部分(如文件系统目录和文件) | | **组合模式** | 结构型 | 统一处理整体和部分(如文件系统目录和文件) |
| **DAO模式** | 数据访问 | 分离数据访问逻辑和业务逻辑 | | **DAO模式** | 数据访问 | 分离数据访问逻辑和业务逻辑 |
| **MVC模式** | 架构型 | 分离界面(View)、逻辑(Controller)、数据(Model) | | **MVC模式** | 架构型 | 分离界面(View)、逻辑(Controller)、数据(Model) |