vault backup: 2026-07-03 23:22:28
This commit is contained in:
@@ -1,353 +1,353 @@
|
||||
# 第02讲:Linux操作系统目录结构与文件操作
|
||||
|
||||
> **本节目标**:理解Linux系统的起源与设计哲学,掌握目录组织方式和常用文件操作命令,为后续系统编程打下基础
|
||||
|
||||
## 前置知识
|
||||
- [[01_系统运行机制]] -- 对操作系统的基本认识
|
||||
- 计算机基本操作能力
|
||||
|
||||
---
|
||||
|
||||
## 一、UNIX/Linux系统简介
|
||||
|
||||
### 1.1 UNIX的诞生
|
||||
|
||||
1969年,贝尔实验室(Bell Labs)的 **Ken Thompson** 和 **Dennis Ritchie** 在一台废弃的PDP-7小型机上开发了UNIX操作系统。此后Ritchie还创造了C语言,并用C语言重写了UNIX,使UNIX成为第一个用高级语言编写的操作系统,极大地提高了可移植性。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["1969 Ken Thompson
|
||||
Dennis Ritchie"] -->|在PDP-7上开发| B[UNIX初版]
|
||||
B -->|用C语言重写| C[UNIX V6/V7]
|
||||
C -->|研究方向| D[BSD UNIX]
|
||||
C -->|商业方向| E[System V]
|
||||
D --> F[FreeBSD / NetBSD]
|
||||
E --> G[AIX / Solaris / HP-UX]
|
||||
|
||||
style A fill:#fff3e0
|
||||
style C fill:#e1f5fe
|
||||
```
|
||||
|
||||
### 1.2 UNIX两大流派
|
||||
|
||||
| 流派 | 代表 | 特点 |
|
||||
|------|------|------|
|
||||
| **BSD UNIX**(研究版) | BSD 4.x | 由加州大学伯克利分校维护,偏重学术研究 |
|
||||
| **System V**(商业版) | SVR4 | 由AT&T维护,偏重商业应用 |
|
||||
|
||||
### 1.3 主要商业UNIX版本
|
||||
|
||||
| 版本 | 厂商 | 典型硬件平台 |
|
||||
|------|------|-------------|
|
||||
| **AIX** | IBM | Power/PowerPC |
|
||||
| **Solaris** | Sun Microsystems | SPARC / x86 |
|
||||
| **HP-UX** | HP | PA-RISC / Itanium |
|
||||
| **IRIX** | SGI | MIPS |
|
||||
|
||||
---
|
||||
|
||||
## 二、Linux系统
|
||||
|
||||
### 2.1 Linux的诞生
|
||||
|
||||
1991年,芬兰赫尔辛基大学的学生 **林纳斯·托瓦兹(Linus Torvalds)** 开发了Linux内核。最初这只是个人项目,但由于他将内核源码以GPL(GNU General Public License)协议发布,全球开发者得以自由修改和分发,Linux迅速成长为最重要的开源操作系统。
|
||||
|
||||
### 2.2 内核版本号
|
||||
|
||||
Linux内核版本号的格式为 **`r.x.y`**:
|
||||
|
||||
- **r**(主版本号):内核有重大变更时递增
|
||||
- **x**(次版本号):偶数表示稳定版,奇数表示开发版
|
||||
- **y**(修订版本号):bug修复和小改动
|
||||
|
||||
常见内核版本线:2.4 -> 2.6 -> 3.2 -> 4.6.4 -> 5.x -> 6.x
|
||||
|
||||
> **注意**:从3.0开始,主版本号不再有"奇偶"含义,版本号只是简单的递增计数器。
|
||||
|
||||
### 2.3 Linux发行版
|
||||
|
||||
内核本身只是操作系统的核心部分。**发行版(Distribution)** 将内核与各种工具、桌面环境、软件包管理器等打包在一起,形成完整的操作系统。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[Linux内核] --> B[GNU工具集]
|
||||
B --> C[发行版]
|
||||
C --> D[RHEL]
|
||||
C --> E[Fedora]
|
||||
C --> F[Ubuntu]
|
||||
C --> G[CentOS]
|
||||
C --> H[中标麒麟]
|
||||
|
||||
style A fill:#ffcdd2
|
||||
style C fill:#e1f5fe
|
||||
```
|
||||
|
||||
| 发行版 | 特点 | 典型用途 |
|
||||
|--------|------|---------|
|
||||
| **RHEL**(Red Hat Enterprise Linux) | 商业支持,稳定 | 企业服务器 |
|
||||
| **Fedora** | 技术前沿,更新快 | 桌面 / 开发 |
|
||||
| **Ubuntu** | 用户友好,社区活跃 | 桌面 / 云服务器 |
|
||||
| **CentOS** | RHEL的免费克隆版 | 企业服务器 |
|
||||
| **中标麒麟** | 国产化,政府认证 | 国内政府 / 国防 |
|
||||
|
||||
---
|
||||
|
||||
## 三、Linux目录结构
|
||||
|
||||
### 3.1 核心概念:一切皆文件
|
||||
|
||||
与Windows不同,Linux **没有盘符**(C:、D:等)的概念。整个文件系统从根目录 **`/`** 开始,形成一棵倒置的树。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
ROOT["/ (根目录)"] --> BIN["/bin
|
||||
基本命令"]
|
||||
ROOT --> SBIN["/sbin
|
||||
系统管理命令"]
|
||||
ROOT --> ETC["/etc
|
||||
配置文件"]
|
||||
ROOT --> HOME["/home
|
||||
用户家目录"]
|
||||
ROOT --> VAR["/var
|
||||
可变数据"]
|
||||
ROOT --> USR["/usr
|
||||
用户程序"]
|
||||
ROOT --> TMP["/tmp
|
||||
临时文件"]
|
||||
ROOT --> DEV["/dev
|
||||
设备文件"]
|
||||
ROOT --> PROC["/proc
|
||||
进程信息"]
|
||||
ROOT --> BOOT["/boot
|
||||
内核与启动"]
|
||||
ROOT --> LIB["/lib
|
||||
共享库"]
|
||||
ROOT --> MNT["/mnt
|
||||
挂载点"]
|
||||
ROOT --> OPT["/opt
|
||||
第三方软件"]
|
||||
ROOT --> ROOT2["/root
|
||||
root用户家目录"]
|
||||
|
||||
style ROOT fill:#ffcdd2
|
||||
style ETC fill:#fff3e0
|
||||
style HOME fill:#e8f5e9
|
||||
style PROC fill:#e1f5fe
|
||||
style DEV fill:#f3e5f5
|
||||
```
|
||||
|
||||
### 3.2 重要目录详解
|
||||
|
||||
| 目录 | 全称 | 作用 | 举例 |
|
||||
|------|------|------|------|
|
||||
| `/bin` | Binaries | 基本用户命令 | `ls`, `cp`, `cat` |
|
||||
| `/sbin` | System Binaries | 系统管理命令 | `fdisk`, `ifconfig` |
|
||||
| `/etc` | Editable Text Config | 系统配置文件 | `/etc/passwd`, `/etc/fstab` |
|
||||
| `/home` | Home | 普通用户家目录 | `/home/zhangsan` |
|
||||
| `/root` | Root Home | root用户的家目录 | -- |
|
||||
| `/var` | Variable | 可变数据(日志、缓存等) | `/var/log/messages` |
|
||||
| `/usr` | Unix System Resources | 用户程序和数据 | `/usr/bin`, `/usr/lib` |
|
||||
| `/tmp` | Temporary | 临时文件,重启后可能清除 | -- |
|
||||
| `/dev` | Device | 设备文件 | `/dev/sda`(硬盘) |
|
||||
| `/proc` | Process | 虚拟文件系统,内核运行信息 | `/proc/cpuinfo` |
|
||||
| `/boot` | Boot | 启动相关文件(内核镜像等) | `vmlinuz-xxx` |
|
||||
| `/lib` | Library | 共享库文件 | `libc.so.6` |
|
||||
| `/mnt` | Mount | 临时挂载点 | U盘、网络存储 |
|
||||
|
||||
### 3.3 /proc文件系统
|
||||
|
||||
`/proc` 是一个 **虚拟文件系统** -- 它不占用磁盘空间,而是内核在内存中动态生成的。通过读取 `/proc` 下的文件,可以实时查看系统和进程信息。
|
||||
|
||||
```bash
|
||||
# 查看CPU信息
|
||||
cat /proc/cpuinfo
|
||||
|
||||
# 查看内存信息
|
||||
cat /proc/meminfo
|
||||
|
||||
# 查看当前进程信息(PID=1的进程)
|
||||
cat /proc/1/status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、目录操作命令
|
||||
|
||||
### 4.1 特殊目录符号
|
||||
|
||||
| 符号 | 含义 | 示例 |
|
||||
|------|------|------|
|
||||
| `.` | 当前目录 | `./program`(运行当前目录下的程序) |
|
||||
| `..` | 上一级目录 | `cd ..`(回到上级目录) |
|
||||
| `~` | 当前用户的家目录 | `cd ~`(回到家目录) |
|
||||
| `-` | 上一次所在的目录 | `cd -`(在两个目录间来回切换) |
|
||||
|
||||
### 4.2 常用目录操作命令
|
||||
|
||||
```bash
|
||||
# pwd - 显示当前工作目录(Print Working Directory)
|
||||
pwd
|
||||
# 输出:/home/zhangsan
|
||||
|
||||
# cd - 切换目录(Change Directory)
|
||||
cd /usr/local # 绝对路径切换
|
||||
cd .. # 回到上级目录
|
||||
cd ~ # 回到家目录(等同于 cd $HOME)
|
||||
cd - # 回到上一次的目录
|
||||
|
||||
# mkdir - 创建目录(Make Directory)
|
||||
mkdir mydir # 创建单个目录
|
||||
mkdir -p project/src/main # 递归创建多级目录
|
||||
|
||||
# rmdir - 删除空目录(Remove Directory)
|
||||
rmdir mydir # 只能删除空目录
|
||||
|
||||
# rm - 删除文件或目录
|
||||
rm file.txt # 删除文件
|
||||
rm -r mydir # 递归删除目录及其内容
|
||||
rm -ri mydir # 递归删除,每个文件都询问确认
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、文件操作命令
|
||||
|
||||
### 5.1 查看文件内容
|
||||
|
||||
```bash
|
||||
# ls - 列出目录内容(List)
|
||||
ls # 列出当前目录下的文件
|
||||
ls -l # 长格式显示(含权限、大小、时间)
|
||||
ls -a # 显示隐藏文件(以 . 开头的文件)
|
||||
ls -lh # 长格式 + 人类可读的文件大小
|
||||
|
||||
# cat - 连接并显示文件内容
|
||||
cat file.txt # 显示文件全部内容
|
||||
cat -n file.txt # 显示时带行号
|
||||
|
||||
# more / less - 分页查看
|
||||
more file.txt # 空格翻页,q 退出
|
||||
less file.txt # 更强大的分页器,支持搜索
|
||||
```
|
||||
|
||||
### 5.2 复制、移动和重命名
|
||||
|
||||
```bash
|
||||
# cp - 复制文件或目录(Copy)
|
||||
cp file1.txt file2.txt # 复制文件
|
||||
cp -r dir1/ dir2/ # 递归复制目录
|
||||
cp -i file1.txt file2.txt # 覆盖前询问
|
||||
|
||||
# mv - 移动/重命名(Move)
|
||||
mv file.txt /tmp/ # 移动文件到 /tmp
|
||||
mv oldname.txt newname.txt # 重命名
|
||||
```
|
||||
|
||||
### 5.3 文件权限管理
|
||||
|
||||
每个文件都有三组权限,分别针对三类用户:
|
||||
|
||||
| 权限位 | 含义 | 对文件的效果 | 对目录的效果 |
|
||||
|--------|------|-------------|-------------|
|
||||
| **r**(读) | Read | 可以查看文件内容 | 可以列出目录内容 |
|
||||
| **w**(写) | Write | 可以修改文件内容 | 可以在目录中创建/删除文件 |
|
||||
| **x**(执行) | Execute | 可以运行文件 | 可以进入目录(`cd`) |
|
||||
|
||||
三类用户:
|
||||
- **u**(user/所有者):文件的拥有者
|
||||
- **g**(group/所属组):与文件同组的用户
|
||||
- **o**(others/其他人):其余所有用户
|
||||
|
||||
```bash
|
||||
# chmod - 修改文件权限(Change Mode)
|
||||
chmod u+x program.sh # 给所有者添加执行权限
|
||||
chmod g-w file.txt # 去掉组的写权限
|
||||
chmod 755 program.sh # 数字表示法:rwxr-xr-x
|
||||
chmod 644 data.txt # 数字表示法:rw-r--r--
|
||||
|
||||
# chown - 修改文件所有者(Change Owner)
|
||||
chown zhangsan file.txt # 修改所有者
|
||||
chown zhangsan:staff file.txt # 同时修改所有者和所属组
|
||||
chown -R zhangsan:staff mydir/ # 递归修改目录
|
||||
```
|
||||
|
||||
**权限的数字表示法**:
|
||||
|
||||
| 数字 | 二进制 | 权限 |
|
||||
|------|--------|------|
|
||||
| 7 | 111 | rwx |
|
||||
| 6 | 110 | rw- |
|
||||
| 5 | 101 | r-x |
|
||||
| 4 | 100 | r-- |
|
||||
| 0 | 000 | --- |
|
||||
|
||||
因此 `chmod 755` 等价于 `rwxr-xr-x`:所有者可读可写可执行,组和其他人可读可执行。
|
||||
|
||||
---
|
||||
|
||||
## 六、绝对路径与相对路径
|
||||
|
||||
### 6.1 概念对比
|
||||
|
||||
| 路径类型 | 起点 | 示例 | 特点 |
|
||||
|----------|------|------|------|
|
||||
| **绝对路径** | 根目录 `/` | `/home/zhangsan/file.txt` | 完整路径,从根开始,与当前位置无关 |
|
||||
| **相对路径** | 当前目录 | `./file.txt` 或 `../dir/file.txt` | 相对于当前位置,更短更灵活 |
|
||||
|
||||
### 6.2 示例
|
||||
|
||||
假设当前目录为 `/home/zhangsan/project`:
|
||||
|
||||
```bash
|
||||
# 绝对路径方式
|
||||
cat /home/zhangsan/project/src/main.c
|
||||
|
||||
# 相对路径方式(效果相同)
|
||||
cat ./src/main.c
|
||||
cat src/main.c
|
||||
|
||||
# 使用 .. 回到上级
|
||||
cat ../README.md # 访问上级目录中的 README.md
|
||||
```
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
HOME["/home"] --> ZS["zhangsan"]
|
||||
ZS --> PROJ["project"]
|
||||
PROJ --> SRC["src"]
|
||||
SRC --> MAIN["main.c"]
|
||||
ZS --> README["README.md"]
|
||||
|
||||
PROJ -.->|"cd .."| ZS
|
||||
PROJ -.->|"cd ./src"| SRC
|
||||
PROJ -.->|"cat ../README.md"| README
|
||||
|
||||
style PROJ fill:#fff3e0
|
||||
style MAIN fill:#e8f5e9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、知识关联
|
||||
|
||||
- Linux的"一切皆文件"思想在 [[04_文件IO编程]] 中会深入学习
|
||||
- 文件权限在 [[04_文件IO编程]] 中用 `open()` 系统调用的 `mode` 参数体现
|
||||
- `/proc` 文件系统在 [[05_进程控制]] 中用于查看进程状态
|
||||
- 后续C语言编程需要在Linux环境下进行,参见 [[03_C语言编程基础]]
|
||||
|
||||
---
|
||||
|
||||
## 八、思考题
|
||||
|
||||
1. **为什么Linux没有盘符?** 这种设计有什么优势?
|
||||
2. **/proc 为什么是"虚拟"文件系统?** 它与 /home 这样的目录有什么本质区别?
|
||||
3. **`rm -rf /` 会发生什么?** 为什么root用户执行这个命令非常危险?
|
||||
4. **`chmod 755` 和 `chmod 644` 分别适用于什么场景?**(提示:可执行文件 vs 数据文件)
|
||||
|
||||
---
|
||||
|
||||
## 九、扩展阅读
|
||||
|
||||
- 《鸟哥的Linux私房菜》第5-6章:Linux文件权限与目录配置
|
||||
- 《UNIX环境高级编程》第1-2章:UNIX基础知识
|
||||
- Linux文件系统层次标准(FHS):https://pathname.com/fhs/
|
||||
# 第02讲:Linux操作系统目录结构与文件操作
|
||||
|
||||
> **本节目标**:理解Linux系统的起源与设计哲学,掌握目录组织方式和常用文件操作命令,为后续系统编程打下基础
|
||||
|
||||
## 前置知识
|
||||
- [[01_系统运行机制]] -- 对操作系统的基本认识
|
||||
- 计算机基本操作能力
|
||||
|
||||
---
|
||||
|
||||
## 一、UNIX/Linux系统简介
|
||||
|
||||
### 1.1 UNIX的诞生
|
||||
|
||||
1969年,贝尔实验室(Bell Labs)的 **Ken Thompson** 和 **Dennis Ritchie** 在一台废弃的PDP-7小型机上开发了UNIX操作系统。此后Ritchie还创造了C语言,并用C语言重写了UNIX,使UNIX成为第一个用高级语言编写的操作系统,极大地提高了可移植性。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["1969 Ken Thompson
|
||||
Dennis Ritchie"] -->|在PDP-7上开发| B[UNIX初版]
|
||||
B -->|用C语言重写| C[UNIX V6/V7]
|
||||
C -->|研究方向| D[BSD UNIX]
|
||||
C -->|商业方向| E[System V]
|
||||
D --> F[FreeBSD / NetBSD]
|
||||
E --> G[AIX / Solaris / HP-UX]
|
||||
|
||||
style A fill:#fff3e0
|
||||
style C fill:#e1f5fe
|
||||
```
|
||||
|
||||
### 1.2 UNIX两大流派
|
||||
|
||||
| 流派 | 代表 | 特点 |
|
||||
|------|------|------|
|
||||
| **BSD UNIX**(研究版) | BSD 4.x | 由加州大学伯克利分校维护,偏重学术研究 |
|
||||
| **System V**(商业版) | SVR4 | 由AT&T维护,偏重商业应用 |
|
||||
|
||||
### 1.3 主要商业UNIX版本
|
||||
|
||||
| 版本 | 厂商 | 典型硬件平台 |
|
||||
|------|------|-------------|
|
||||
| **AIX** | IBM | Power/PowerPC |
|
||||
| **Solaris** | Sun Microsystems | SPARC / x86 |
|
||||
| **HP-UX** | HP | PA-RISC / Itanium |
|
||||
| **IRIX** | SGI | MIPS |
|
||||
|
||||
---
|
||||
|
||||
## 二、Linux系统
|
||||
|
||||
### 2.1 Linux的诞生
|
||||
|
||||
1991年,芬兰赫尔辛基大学的学生 **林纳斯·托瓦兹(Linus Torvalds)** 开发了Linux内核。最初这只是个人项目,但由于他将内核源码以GPL(GNU General Public License)协议发布,全球开发者得以自由修改和分发,Linux迅速成长为最重要的开源操作系统。
|
||||
|
||||
### 2.2 内核版本号
|
||||
|
||||
Linux内核版本号的格式为 **`r.x.y`**:
|
||||
|
||||
- **r**(主版本号):内核有重大变更时递增
|
||||
- **x**(次版本号):偶数表示稳定版,奇数表示开发版
|
||||
- **y**(修订版本号):bug修复和小改动
|
||||
|
||||
常见内核版本线:2.4 -> 2.6 -> 3.2 -> 4.6.4 -> 5.x -> 6.x
|
||||
|
||||
> **注意**:从3.0开始,主版本号不再有"奇偶"含义,版本号只是简单的递增计数器。
|
||||
|
||||
### 2.3 Linux发行版
|
||||
|
||||
内核本身只是操作系统的核心部分。**发行版(Distribution)** 将内核与各种工具、桌面环境、软件包管理器等打包在一起,形成完整的操作系统。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
A[Linux内核] --> B[GNU工具集]
|
||||
B --> C[发行版]
|
||||
C --> D[RHEL]
|
||||
C --> E[Fedora]
|
||||
C --> F[Ubuntu]
|
||||
C --> G[CentOS]
|
||||
C --> H[中标麒麟]
|
||||
|
||||
style A fill:#ffcdd2
|
||||
style C fill:#e1f5fe
|
||||
```
|
||||
|
||||
| 发行版 | 特点 | 典型用途 |
|
||||
|--------|------|---------|
|
||||
| **RHEL**(Red Hat Enterprise Linux) | 商业支持,稳定 | 企业服务器 |
|
||||
| **Fedora** | 技术前沿,更新快 | 桌面 / 开发 |
|
||||
| **Ubuntu** | 用户友好,社区活跃 | 桌面 / 云服务器 |
|
||||
| **CentOS** | RHEL的免费克隆版 | 企业服务器 |
|
||||
| **中标麒麟** | 国产化,政府认证 | 国内政府 / 国防 |
|
||||
|
||||
---
|
||||
|
||||
## 三、Linux目录结构
|
||||
|
||||
### 3.1 核心概念:一切皆文件
|
||||
|
||||
与Windows不同,Linux **没有盘符**(C:、D:等)的概念。整个文件系统从根目录 **`/`** 开始,形成一棵倒置的树。
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
ROOT["/ (根目录)"] --> BIN["/bin
|
||||
基本命令"]
|
||||
ROOT --> SBIN["/sbin
|
||||
系统管理命令"]
|
||||
ROOT --> ETC["/etc
|
||||
配置文件"]
|
||||
ROOT --> HOME["/home
|
||||
用户家目录"]
|
||||
ROOT --> VAR["/var
|
||||
可变数据"]
|
||||
ROOT --> USR["/usr
|
||||
用户程序"]
|
||||
ROOT --> TMP["/tmp
|
||||
临时文件"]
|
||||
ROOT --> DEV["/dev
|
||||
设备文件"]
|
||||
ROOT --> PROC["/proc
|
||||
进程信息"]
|
||||
ROOT --> BOOT["/boot
|
||||
内核与启动"]
|
||||
ROOT --> LIB["/lib
|
||||
共享库"]
|
||||
ROOT --> MNT["/mnt
|
||||
挂载点"]
|
||||
ROOT --> OPT["/opt
|
||||
第三方软件"]
|
||||
ROOT --> ROOT2["/root
|
||||
root用户家目录"]
|
||||
|
||||
style ROOT fill:#ffcdd2
|
||||
style ETC fill:#fff3e0
|
||||
style HOME fill:#e8f5e9
|
||||
style PROC fill:#e1f5fe
|
||||
style DEV fill:#f3e5f5
|
||||
```
|
||||
|
||||
### 3.2 重要目录详解
|
||||
|
||||
| 目录 | 全称 | 作用 | 举例 |
|
||||
|------|------|------|------|
|
||||
| `/bin` | Binaries | 基本用户命令 | `ls`, `cp`, `cat` |
|
||||
| `/sbin` | System Binaries | 系统管理命令 | `fdisk`, `ifconfig` |
|
||||
| `/etc` | Editable Text Config | 系统配置文件 | `/etc/passwd`, `/etc/fstab` |
|
||||
| `/home` | Home | 普通用户家目录 | `/home/zhangsan` |
|
||||
| `/root` | Root Home | root用户的家目录 | -- |
|
||||
| `/var` | Variable | 可变数据(日志、缓存等) | `/var/log/messages` |
|
||||
| `/usr` | Unix System Resources | 用户程序和数据 | `/usr/bin`, `/usr/lib` |
|
||||
| `/tmp` | Temporary | 临时文件,重启后可能清除 | -- |
|
||||
| `/dev` | Device | 设备文件 | `/dev/sda`(硬盘) |
|
||||
| `/proc` | Process | 虚拟文件系统,内核运行信息 | `/proc/cpuinfo` |
|
||||
| `/boot` | Boot | 启动相关文件(内核镜像等) | `vmlinuz-xxx` |
|
||||
| `/lib` | Library | 共享库文件 | `libc.so.6` |
|
||||
| `/mnt` | Mount | 临时挂载点 | U盘、网络存储 |
|
||||
|
||||
### 3.3 /proc文件系统
|
||||
|
||||
`/proc` 是一个 **虚拟文件系统** -- 它不占用磁盘空间,而是内核在内存中动态生成的。通过读取 `/proc` 下的文件,可以实时查看系统和进程信息。
|
||||
|
||||
```bash
|
||||
# 查看CPU信息
|
||||
cat /proc/cpuinfo
|
||||
|
||||
# 查看内存信息
|
||||
cat /proc/meminfo
|
||||
|
||||
# 查看当前进程信息(PID=1的进程)
|
||||
cat /proc/1/status
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、目录操作命令
|
||||
|
||||
### 4.1 特殊目录符号
|
||||
|
||||
| 符号 | 含义 | 示例 |
|
||||
|------|------|------|
|
||||
| `.` | 当前目录 | `./program`(运行当前目录下的程序) |
|
||||
| `..` | 上一级目录 | `cd ..`(回到上级目录) |
|
||||
| `~` | 当前用户的家目录 | `cd ~`(回到家目录) |
|
||||
| `-` | 上一次所在的目录 | `cd -`(在两个目录间来回切换) |
|
||||
|
||||
### 4.2 常用目录操作命令
|
||||
|
||||
```bash
|
||||
# pwd - 显示当前工作目录(Print Working Directory)
|
||||
pwd
|
||||
# 输出:/home/zhangsan
|
||||
|
||||
# cd - 切换目录(Change Directory)
|
||||
cd /usr/local # 绝对路径切换
|
||||
cd .. # 回到上级目录
|
||||
cd ~ # 回到家目录(等同于 cd $HOME)
|
||||
cd - # 回到上一次的目录
|
||||
|
||||
# mkdir - 创建目录(Make Directory)
|
||||
mkdir mydir # 创建单个目录
|
||||
mkdir -p project/src/main # 递归创建多级目录
|
||||
|
||||
# rmdir - 删除空目录(Remove Directory)
|
||||
rmdir mydir # 只能删除空目录
|
||||
|
||||
# rm - 删除文件或目录
|
||||
rm file.txt # 删除文件
|
||||
rm -r mydir # 递归删除目录及其内容
|
||||
rm -ri mydir # 递归删除,每个文件都询问确认
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、文件操作命令
|
||||
|
||||
### 5.1 查看文件内容
|
||||
|
||||
```bash
|
||||
# ls - 列出目录内容(List)
|
||||
ls # 列出当前目录下的文件
|
||||
ls -l # 长格式显示(含权限、大小、时间)
|
||||
ls -a # 显示隐藏文件(以 . 开头的文件)
|
||||
ls -lh # 长格式 + 人类可读的文件大小
|
||||
|
||||
# cat - 连接并显示文件内容
|
||||
cat file.txt # 显示文件全部内容
|
||||
cat -n file.txt # 显示时带行号
|
||||
|
||||
# more / less - 分页查看
|
||||
more file.txt # 空格翻页,q 退出
|
||||
less file.txt # 更强大的分页器,支持搜索
|
||||
```
|
||||
|
||||
### 5.2 复制、移动和重命名
|
||||
|
||||
```bash
|
||||
# cp - 复制文件或目录(Copy)
|
||||
cp file1.txt file2.txt # 复制文件
|
||||
cp -r dir1/ dir2/ # 递归复制目录
|
||||
cp -i file1.txt file2.txt # 覆盖前询问
|
||||
|
||||
# mv - 移动/重命名(Move)
|
||||
mv file.txt /tmp/ # 移动文件到 /tmp
|
||||
mv oldname.txt newname.txt # 重命名
|
||||
```
|
||||
|
||||
### 5.3 文件权限管理
|
||||
|
||||
每个文件都有三组权限,分别针对三类用户:
|
||||
|
||||
| 权限位 | 含义 | 对文件的效果 | 对目录的效果 |
|
||||
|--------|------|-------------|-------------|
|
||||
| **r**(读) | Read | 可以查看文件内容 | 可以列出目录内容 |
|
||||
| **w**(写) | Write | 可以修改文件内容 | 可以在目录中创建/删除文件 |
|
||||
| **x**(执行) | Execute | 可以运行文件 | 可以进入目录(`cd`) |
|
||||
|
||||
三类用户:
|
||||
- **u**(user/所有者):文件的拥有者
|
||||
- **g**(group/所属组):与文件同组的用户
|
||||
- **o**(others/其他人):其余所有用户
|
||||
|
||||
```bash
|
||||
# chmod - 修改文件权限(Change Mode)
|
||||
chmod u+x program.sh # 给所有者添加执行权限
|
||||
chmod g-w file.txt # 去掉组的写权限
|
||||
chmod 755 program.sh # 数字表示法:rwxr-xr-x
|
||||
chmod 644 data.txt # 数字表示法:rw-r--r--
|
||||
|
||||
# chown - 修改文件所有者(Change Owner)
|
||||
chown zhangsan file.txt # 修改所有者
|
||||
chown zhangsan:staff file.txt # 同时修改所有者和所属组
|
||||
chown -R zhangsan:staff mydir/ # 递归修改目录
|
||||
```
|
||||
|
||||
**权限的数字表示法**:
|
||||
|
||||
| 数字 | 二进制 | 权限 |
|
||||
|------|--------|------|
|
||||
| 7 | 111 | rwx |
|
||||
| 6 | 110 | rw- |
|
||||
| 5 | 101 | r-x |
|
||||
| 4 | 100 | r-- |
|
||||
| 0 | 000 | --- |
|
||||
|
||||
因此 `chmod 755` 等价于 `rwxr-xr-x`:所有者可读可写可执行,组和其他人可读可执行。
|
||||
|
||||
---
|
||||
|
||||
## 六、绝对路径与相对路径
|
||||
|
||||
### 6.1 概念对比
|
||||
|
||||
| 路径类型 | 起点 | 示例 | 特点 |
|
||||
|----------|------|------|------|
|
||||
| **绝对路径** | 根目录 `/` | `/home/zhangsan/file.txt` | 完整路径,从根开始,与当前位置无关 |
|
||||
| **相对路径** | 当前目录 | `./file.txt` 或 `../dir/file.txt` | 相对于当前位置,更短更灵活 |
|
||||
|
||||
### 6.2 示例
|
||||
|
||||
假设当前目录为 `/home/zhangsan/project`:
|
||||
|
||||
```bash
|
||||
# 绝对路径方式
|
||||
cat /home/zhangsan/project/src/main.c
|
||||
|
||||
# 相对路径方式(效果相同)
|
||||
cat ./src/main.c
|
||||
cat src/main.c
|
||||
|
||||
# 使用 .. 回到上级
|
||||
cat ../README.md # 访问上级目录中的 README.md
|
||||
```
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
HOME["/home"] --> ZS["zhangsan"]
|
||||
ZS --> PROJ["project"]
|
||||
PROJ --> SRC["src"]
|
||||
SRC --> MAIN["main.c"]
|
||||
ZS --> README["README.md"]
|
||||
|
||||
PROJ -.->|"cd .."| ZS
|
||||
PROJ -.->|"cd ./src"| SRC
|
||||
PROJ -.->|"cat ../README.md"| README
|
||||
|
||||
style PROJ fill:#fff3e0
|
||||
style MAIN fill:#e8f5e9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、知识关联
|
||||
|
||||
- Linux的"一切皆文件"思想在 [[04_文件IO编程]] 中会深入学习
|
||||
- 文件权限在 [[04_文件IO编程]] 中用 `open()` 系统调用的 `mode` 参数体现
|
||||
- `/proc` 文件系统在 [[05_进程控制]] 中用于查看进程状态
|
||||
- 后续C语言编程需要在Linux环境下进行,参见 [[03_C语言编程基础]]
|
||||
|
||||
---
|
||||
|
||||
## 八、思考题
|
||||
|
||||
1. **为什么Linux没有盘符?** 这种设计有什么优势?
|
||||
2. **/proc 为什么是"虚拟"文件系统?** 它与 /home 这样的目录有什么本质区别?
|
||||
3. **`rm -rf /` 会发生什么?** 为什么root用户执行这个命令非常危险?
|
||||
4. **`chmod 755` 和 `chmod 644` 分别适用于什么场景?**(提示:可执行文件 vs 数据文件)
|
||||
|
||||
---
|
||||
|
||||
## 九、扩展阅读
|
||||
|
||||
- 《鸟哥的Linux私房菜》第5-6章:Linux文件权限与目录配置
|
||||
- 《UNIX环境高级编程》第1-2章:UNIX基础知识
|
||||
- Linux文件系统层次标准(FHS):https://pathname.com/fhs/
|
||||
|
||||
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
@@ -1,315 +1,315 @@
|
||||
# 第06讲:进程控制(进阶)
|
||||
|
||||
> 🎯 **本节目标**:深入理解 fork/exec 的工作原理,掌握 Shell 的实现机制
|
||||
|
||||
## 📋 前置知识
|
||||
- [[06_进程控制]] — fork、exec、wait 的基本概念
|
||||
|
||||
---
|
||||
|
||||
## 🤔 为什么需要这个?
|
||||
|
||||
你每天都在用 Shell(命令行),但你有没有想过:
|
||||
- Shell 是怎么执行你的命令的?
|
||||
- 为什么输入 `ls` 就能列出文件?
|
||||
- 后台运行 `&` 是怎么实现的?
|
||||
|
||||
理解这些,需要深入掌握 fork 和 exec 的配合机制。
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 1. Shell 的工作原理
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[用户输入命令] --> B[Shell 解析命令]
|
||||
B --> C[Shell 调用 fork]
|
||||
C --> D[子进程调用 execvp]
|
||||
D --> E[执行命令程序]
|
||||
E --> F[子进程结束]
|
||||
F --> G[Shell 调用 waitpid]
|
||||
G --> A
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style D fill:#e8f5e9
|
||||
```
|
||||
|
||||
**Shell 的核心逻辑**:
|
||||
```c
|
||||
while (1) {
|
||||
printf("%% "); // 打印提示符
|
||||
fgets(cmdline); // 读取命令
|
||||
if (feof(stdin)) exit(0);
|
||||
|
||||
pid = fork(); // 创建子进程
|
||||
if (pid == 0) { // 子进程
|
||||
execvp(argv[0], argv); // 执行命令
|
||||
exit(1); // exec 失败
|
||||
}
|
||||
|
||||
if (!background) // 前台运行
|
||||
waitpid(pid); // 等待子进程结束
|
||||
}
|
||||
```
|
||||
|
||||
### 2. fork() 的实现细节
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[父进程调用 fork] --> B[内核复制父进程的 PCB]
|
||||
B --> C[复制页表(写时复制)]
|
||||
C --> D[设置子进程的 PID]
|
||||
D --> E[父子进程各返回一次]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**写时复制(Copy-on-Write)**:
|
||||
- fork() 时不立即复制物理内存
|
||||
- 父子进程共享同一份物理页面
|
||||
- 只有当某一方尝试写入时,才复制该页面
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 父进程
|
||||
participant 子进程
|
||||
participant 内存
|
||||
|
||||
父进程->>内存: fork()
|
||||
Note over 内存: 父子共享物理页面
|
||||
子进程->>内存: 尝试写入
|
||||
Note over 内存: 触发写时复制
|
||||
Note over 内存: 复制该页面给子进程
|
||||
子进程->>内存: 写入新页面
|
||||
```
|
||||
|
||||
### 3. exec() 的工作流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[调用 execvp] --> B[查找可执行文件]
|
||||
B --> C[释放旧的地址空间]
|
||||
C --> D[加载新的代码段]
|
||||
D --> E[加载新的数据段]
|
||||
E --> F[设置新的栈]
|
||||
F --> G[跳转到新程序入口]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style G fill:#e8f5e9
|
||||
```
|
||||
|
||||
**exec 的关键特性**:
|
||||
- **不创建新进程**:只是替换当前进程的内容
|
||||
- **PID 不变**:进程还是原来的进程
|
||||
- **文件描述符保留**:打开的文件不会自动关闭(除非设置了 close-on-exec)
|
||||
|
||||
### 4. 进程组与会话
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[会话 Session] --> B[前台进程组]
|
||||
A --> C[后台进程组1]
|
||||
A --> D[后台进程组2]
|
||||
|
||||
B --> B1[Shell]
|
||||
B --> B2[当前命令]
|
||||
|
||||
style A fill:#ffcdd2
|
||||
style B fill:#e8f5e9
|
||||
style C fill:#e1f5fe
|
||||
style D fill:#e1f5fe
|
||||
```
|
||||
|
||||
**进程组**:一组相关进程的集合
|
||||
- 用于信号的批量发送
|
||||
- 用于作业控制(前台/后台切换)
|
||||
|
||||
**会话**:一个用户登录期间的所有进程
|
||||
- 一个终端对应一个会话
|
||||
- 会话有一个控制终端
|
||||
|
||||
### 5. 守护进程
|
||||
|
||||
守护进程是在后台运行的特殊进程,没有控制终端:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[创建子进程] --> B[父进程退出]
|
||||
B --> C[创建新会话]
|
||||
C --> D[改变工作目录]
|
||||
D --> E[关闭文件描述符]
|
||||
E --> F[重定向 stdin/stdout/stderr]
|
||||
F --> G[进入主循环]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style G fill:#e8f5e9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 动手实践
|
||||
|
||||
### 示例1:实现简单的 Shell
|
||||
|
||||
```c
|
||||
// myshell.c - 简单的 Shell 实现
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(void) {
|
||||
int ret, i, k, len, pid;
|
||||
char cmd[100]; // 命令串,最多100个字符
|
||||
char *arg[20]; // 参数数组,最多20个参数
|
||||
|
||||
printf("%% "); // 打印提示符
|
||||
|
||||
fgets(cmd, 100, stdin); // 从标准输入读取一行命令
|
||||
|
||||
// 将命令串中的空格替换成'\0',并将各参数提取出来
|
||||
// 例如 cmd[100]="ls -l -a\0",替换后变为 cmd="ls\0-l\0-a\0"
|
||||
len = strlen(cmd);
|
||||
cmd[len - 1] = '\0'; // 去掉 fgets 加在串尾的换行符
|
||||
|
||||
for (i = 0; i < len - 1; i++)
|
||||
if (cmd[i] == ' ') cmd[i] = '\0';
|
||||
|
||||
// 准备参数数组 arg
|
||||
// arg[0]=cmd, arg[1]=cmd+3, arg[2]=cmd+6, arg[3]=NULL
|
||||
arg[0] = cmd;
|
||||
k = 1;
|
||||
for (i = 1; i < len - 1; i++) {
|
||||
if (cmd[i] == '\0') {
|
||||
arg[k] = cmd + i + 1;
|
||||
k++;
|
||||
}
|
||||
}
|
||||
arg[k] = NULL;
|
||||
|
||||
pid = fork();
|
||||
if (pid == 0) {
|
||||
ret = execvp(arg[0], arg); // 子进程执行命令
|
||||
if (ret == -1) {
|
||||
perror("exec error");
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
wait(-1); // 父进程等待子进程结束
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o myshell myshell.c
|
||||
./myshell
|
||||
% ls -l
|
||||
```
|
||||
|
||||
### 示例2:后台运行
|
||||
|
||||
```c
|
||||
// shellex.c - 支持后台运行的 Shell
|
||||
#include "wrapper.h"
|
||||
|
||||
#define MAXARGS 128
|
||||
|
||||
int parseline(char *buf, char **argv) {
|
||||
char *delim;
|
||||
int argc;
|
||||
int bg; // 后台作业标志
|
||||
|
||||
buf[strlen(buf) - 1] = ' '; // 用空格替换末尾换行
|
||||
while (*buf && (*buf == ' '))
|
||||
buf++;
|
||||
|
||||
argc = 0;
|
||||
while ((delim = strchr(buf, ' '))) {
|
||||
argv[argc++] = buf;
|
||||
*delim = '\0';
|
||||
buf = delim + 1;
|
||||
while (*buf && (*buf == ' '))
|
||||
buf++;
|
||||
}
|
||||
argv[argc] = NULL;
|
||||
|
||||
if (argc == 0) return 1;
|
||||
|
||||
// 检查是否应该在后台执行
|
||||
if ((bg = (*argv[argc - 1] == '&')) != 0)
|
||||
argv[--argc] = NULL;
|
||||
|
||||
return bg;
|
||||
}
|
||||
|
||||
void execute(char *cmdline) {
|
||||
char *argv[MAXARGS];
|
||||
char buf[MAXLINE];
|
||||
int bg;
|
||||
pid_t pid;
|
||||
|
||||
strcpy(buf, cmdline);
|
||||
bg = parseline(buf, argv);
|
||||
|
||||
if (argv[0] == NULL) return; // 忽略空行
|
||||
|
||||
if ((pid = fork()) == 0) { // 子进程
|
||||
if (execvp(argv[0], argv) < 0) {
|
||||
printf("%s: Command not found.\n", argv[0]);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!bg) { // 前台运行
|
||||
int status;
|
||||
if (waitpid(pid, &status, 0) < 0)
|
||||
perror("waitpid error");
|
||||
} else { // 后台运行
|
||||
printf("%d %s", pid, cmdline);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
char cmdline[MAXLINE];
|
||||
while (1) {
|
||||
printf("%% ");
|
||||
fgets(cmdline, MAXLINE, stdin);
|
||||
if (feof(stdin)) exit(0);
|
||||
execute(cmdline);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o shellex shellex.c -L. -lwrapper
|
||||
./shellex
|
||||
% sleep 10 & # 后台运行
|
||||
% ps # 查看进程
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 知识关联
|
||||
- Shell 的 I/O 重定向在 [[04_文件IO编程]] 中的 dup2 有详细讲解
|
||||
- 守护进程在 [[09_网络编程]] 中会实际使用
|
||||
- 进程组在 [[11_处理机调度]] 中用于作业控制
|
||||
|
||||
---
|
||||
|
||||
## 📝 思考题
|
||||
|
||||
1. **为什么 exec() 后文件描述符还保留?** 什么时候需要关闭它们?
|
||||
2. **写时复制的优势是什么?** 如果 fork() 时立即复制所有内存会怎样?
|
||||
3. **Shell 是怎么实现管道的?** 例如 `ls | grep .c` 的执行过程是什么?
|
||||
|
||||
---
|
||||
|
||||
## 📚 扩展阅读
|
||||
- 《UNIX环境高级编程》第8章:进程控制
|
||||
- 《深入理解计算机系统》第8章:异常控制流
|
||||
- [Bash 源码](https://git.savannah.gnu.org/cgit/bash.git/)
|
||||
# 第06讲:进程控制(进阶)
|
||||
|
||||
> 🎯 **本节目标**:深入理解 fork/exec 的工作原理,掌握 Shell 的实现机制
|
||||
|
||||
## 📋 前置知识
|
||||
- [[06_进程控制]] — fork、exec、wait 的基本概念
|
||||
|
||||
---
|
||||
|
||||
## 🤔 为什么需要这个?
|
||||
|
||||
你每天都在用 Shell(命令行),但你有没有想过:
|
||||
- Shell 是怎么执行你的命令的?
|
||||
- 为什么输入 `ls` 就能列出文件?
|
||||
- 后台运行 `&` 是怎么实现的?
|
||||
|
||||
理解这些,需要深入掌握 fork 和 exec 的配合机制。
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 1. Shell 的工作原理
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[用户输入命令] --> B[Shell 解析命令]
|
||||
B --> C[Shell 调用 fork]
|
||||
C --> D[子进程调用 execvp]
|
||||
D --> E[执行命令程序]
|
||||
E --> F[子进程结束]
|
||||
F --> G[Shell 调用 waitpid]
|
||||
G --> A
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style D fill:#e8f5e9
|
||||
```
|
||||
|
||||
**Shell 的核心逻辑**:
|
||||
```c
|
||||
while (1) {
|
||||
printf("%% "); // 打印提示符
|
||||
fgets(cmdline); // 读取命令
|
||||
if (feof(stdin)) exit(0);
|
||||
|
||||
pid = fork(); // 创建子进程
|
||||
if (pid == 0) { // 子进程
|
||||
execvp(argv[0], argv); // 执行命令
|
||||
exit(1); // exec 失败
|
||||
}
|
||||
|
||||
if (!background) // 前台运行
|
||||
waitpid(pid); // 等待子进程结束
|
||||
}
|
||||
```
|
||||
|
||||
### 2. fork() 的实现细节
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[父进程调用 fork] --> B[内核复制父进程的 PCB]
|
||||
B --> C[复制页表(写时复制)]
|
||||
C --> D[设置子进程的 PID]
|
||||
D --> E[父子进程各返回一次]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**写时复制(Copy-on-Write)**:
|
||||
- fork() 时不立即复制物理内存
|
||||
- 父子进程共享同一份物理页面
|
||||
- 只有当某一方尝试写入时,才复制该页面
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 父进程
|
||||
participant 子进程
|
||||
participant 内存
|
||||
|
||||
父进程->>内存: fork()
|
||||
Note over 内存: 父子共享物理页面
|
||||
子进程->>内存: 尝试写入
|
||||
Note over 内存: 触发写时复制
|
||||
Note over 内存: 复制该页面给子进程
|
||||
子进程->>内存: 写入新页面
|
||||
```
|
||||
|
||||
### 3. exec() 的工作流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[调用 execvp] --> B[查找可执行文件]
|
||||
B --> C[释放旧的地址空间]
|
||||
C --> D[加载新的代码段]
|
||||
D --> E[加载新的数据段]
|
||||
E --> F[设置新的栈]
|
||||
F --> G[跳转到新程序入口]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style G fill:#e8f5e9
|
||||
```
|
||||
|
||||
**exec 的关键特性**:
|
||||
- **不创建新进程**:只是替换当前进程的内容
|
||||
- **PID 不变**:进程还是原来的进程
|
||||
- **文件描述符保留**:打开的文件不会自动关闭(除非设置了 close-on-exec)
|
||||
|
||||
### 4. 进程组与会话
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[会话 Session] --> B[前台进程组]
|
||||
A --> C[后台进程组1]
|
||||
A --> D[后台进程组2]
|
||||
|
||||
B --> B1[Shell]
|
||||
B --> B2[当前命令]
|
||||
|
||||
style A fill:#ffcdd2
|
||||
style B fill:#e8f5e9
|
||||
style C fill:#e1f5fe
|
||||
style D fill:#e1f5fe
|
||||
```
|
||||
|
||||
**进程组**:一组相关进程的集合
|
||||
- 用于信号的批量发送
|
||||
- 用于作业控制(前台/后台切换)
|
||||
|
||||
**会话**:一个用户登录期间的所有进程
|
||||
- 一个终端对应一个会话
|
||||
- 会话有一个控制终端
|
||||
|
||||
### 5. 守护进程
|
||||
|
||||
守护进程是在后台运行的特殊进程,没有控制终端:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[创建子进程] --> B[父进程退出]
|
||||
B --> C[创建新会话]
|
||||
C --> D[改变工作目录]
|
||||
D --> E[关闭文件描述符]
|
||||
E --> F[重定向 stdin/stdout/stderr]
|
||||
F --> G[进入主循环]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style G fill:#e8f5e9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 动手实践
|
||||
|
||||
### 示例1:实现简单的 Shell
|
||||
|
||||
```c
|
||||
// myshell.c - 简单的 Shell 实现
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(void) {
|
||||
int ret, i, k, len, pid;
|
||||
char cmd[100]; // 命令串,最多100个字符
|
||||
char *arg[20]; // 参数数组,最多20个参数
|
||||
|
||||
printf("%% "); // 打印提示符
|
||||
|
||||
fgets(cmd, 100, stdin); // 从标准输入读取一行命令
|
||||
|
||||
// 将命令串中的空格替换成'\0',并将各参数提取出来
|
||||
// 例如 cmd[100]="ls -l -a\0",替换后变为 cmd="ls\0-l\0-a\0"
|
||||
len = strlen(cmd);
|
||||
cmd[len - 1] = '\0'; // 去掉 fgets 加在串尾的换行符
|
||||
|
||||
for (i = 0; i < len - 1; i++)
|
||||
if (cmd[i] == ' ') cmd[i] = '\0';
|
||||
|
||||
// 准备参数数组 arg
|
||||
// arg[0]=cmd, arg[1]=cmd+3, arg[2]=cmd+6, arg[3]=NULL
|
||||
arg[0] = cmd;
|
||||
k = 1;
|
||||
for (i = 1; i < len - 1; i++) {
|
||||
if (cmd[i] == '\0') {
|
||||
arg[k] = cmd + i + 1;
|
||||
k++;
|
||||
}
|
||||
}
|
||||
arg[k] = NULL;
|
||||
|
||||
pid = fork();
|
||||
if (pid == 0) {
|
||||
ret = execvp(arg[0], arg); // 子进程执行命令
|
||||
if (ret == -1) {
|
||||
perror("exec error");
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
wait(-1); // 父进程等待子进程结束
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o myshell myshell.c
|
||||
./myshell
|
||||
% ls -l
|
||||
```
|
||||
|
||||
### 示例2:后台运行
|
||||
|
||||
```c
|
||||
// shellex.c - 支持后台运行的 Shell
|
||||
#include "wrapper.h"
|
||||
|
||||
#define MAXARGS 128
|
||||
|
||||
int parseline(char *buf, char **argv) {
|
||||
char *delim;
|
||||
int argc;
|
||||
int bg; // 后台作业标志
|
||||
|
||||
buf[strlen(buf) - 1] = ' '; // 用空格替换末尾换行
|
||||
while (*buf && (*buf == ' '))
|
||||
buf++;
|
||||
|
||||
argc = 0;
|
||||
while ((delim = strchr(buf, ' '))) {
|
||||
argv[argc++] = buf;
|
||||
*delim = '\0';
|
||||
buf = delim + 1;
|
||||
while (*buf && (*buf == ' '))
|
||||
buf++;
|
||||
}
|
||||
argv[argc] = NULL;
|
||||
|
||||
if (argc == 0) return 1;
|
||||
|
||||
// 检查是否应该在后台执行
|
||||
if ((bg = (*argv[argc - 1] == '&')) != 0)
|
||||
argv[--argc] = NULL;
|
||||
|
||||
return bg;
|
||||
}
|
||||
|
||||
void execute(char *cmdline) {
|
||||
char *argv[MAXARGS];
|
||||
char buf[MAXLINE];
|
||||
int bg;
|
||||
pid_t pid;
|
||||
|
||||
strcpy(buf, cmdline);
|
||||
bg = parseline(buf, argv);
|
||||
|
||||
if (argv[0] == NULL) return; // 忽略空行
|
||||
|
||||
if ((pid = fork()) == 0) { // 子进程
|
||||
if (execvp(argv[0], argv) < 0) {
|
||||
printf("%s: Command not found.\n", argv[0]);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!bg) { // 前台运行
|
||||
int status;
|
||||
if (waitpid(pid, &status, 0) < 0)
|
||||
perror("waitpid error");
|
||||
} else { // 后台运行
|
||||
printf("%d %s", pid, cmdline);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
char cmdline[MAXLINE];
|
||||
while (1) {
|
||||
printf("%% ");
|
||||
fgets(cmdline, MAXLINE, stdin);
|
||||
if (feof(stdin)) exit(0);
|
||||
execute(cmdline);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o shellex shellex.c -L. -lwrapper
|
||||
./shellex
|
||||
% sleep 10 & # 后台运行
|
||||
% ps # 查看进程
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 知识关联
|
||||
- Shell 的 I/O 重定向在 [[04_文件IO编程]] 中的 dup2 有详细讲解
|
||||
- 守护进程在 [[09_网络编程]] 中会实际使用
|
||||
- 进程组在 [[11_处理机调度]] 中用于作业控制
|
||||
|
||||
---
|
||||
|
||||
## 📝 思考题
|
||||
|
||||
1. **为什么 exec() 后文件描述符还保留?** 什么时候需要关闭它们?
|
||||
2. **写时复制的优势是什么?** 如果 fork() 时立即复制所有内存会怎样?
|
||||
3. **Shell 是怎么实现管道的?** 例如 `ls | grep .c` 的执行过程是什么?
|
||||
|
||||
---
|
||||
|
||||
## 📚 扩展阅读
|
||||
- 《UNIX环境高级编程》第8章:进程控制
|
||||
- 《深入理解计算机系统》第8章:异常控制流
|
||||
- [Bash 源码](https://git.savannah.gnu.org/cgit/bash.git/)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,406 +1,406 @@
|
||||
# 第08讲:进程间通信
|
||||
|
||||
> 🎯 **本节目标**:掌握管道、消息队列、共享内存等进程间通信方式
|
||||
|
||||
## 📋 前置知识
|
||||
- [[06_进程控制]] — 进程的基本概念
|
||||
- [[06_进程控制_深入]] — fork 和 exec 的工作原理
|
||||
|
||||
---
|
||||
|
||||
## 🤔 为什么需要这个?
|
||||
|
||||
进程之间是相互隔离的,每个进程有自己的地址空间。但有时候进程需要协作:
|
||||
- Shell 需要将 `ls` 的输出传给 `grep`
|
||||
- 浏览器需要与下载管理器通信
|
||||
- 数据库需要与应用程序交互
|
||||
|
||||
**进程间通信(IPC)** 就是解决这个问题的。
|
||||
|
||||
**生活比喻**:
|
||||
- **管道** = 对讲机(单向通信)
|
||||
- **消息队列** = 邮箱(异步通信)
|
||||
- **共享内存** = 共享白板(最快的通信方式)
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 1. IPC 方式概览
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[进程间通信 IPC] --> B[管道 Pipe]
|
||||
A --> C[消息队列 Message Queue]
|
||||
A --> D[共享内存 Shared Memory]
|
||||
A --> E[信号 Signal]
|
||||
A --> F[信号量 Semaphore]
|
||||
A --> G[套接字 Socket]
|
||||
|
||||
B --> B1[单向通信]
|
||||
C --> C1[异步通信]
|
||||
D --> D1[最快]
|
||||
E --> E1[异步通知]
|
||||
F --> F1[同步控制]
|
||||
G --> G1[网络通信]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style D fill:#e8f5e9
|
||||
```
|
||||
|
||||
**对比**:
|
||||
| 方式 | 优点 | 缺点 | 适用场景 |
|
||||
|------|------|------|----------|
|
||||
| 管道 | 简单、易用 | 单向、只能父子进程 | Shell 命令组合 |
|
||||
| 消息队列 | 异步、可按类型读取 | 有大小限制 | 任务分发 |
|
||||
| 共享内存 | 最快 | 需要同步机制 | 大量数据交换 |
|
||||
| 信号 | 异步通知 | 只能传递信号编号 | 事件通知 |
|
||||
| 信号量 | 同步控制 | 不能传递数据 | 互斥、同步 |
|
||||
| 套接字 | 跨网络 | 开销大 | 网络通信 |
|
||||
|
||||
### 2. 管道(Pipe)
|
||||
|
||||
管道是最古老的 IPC 方式,用于有亲缘关系的进程之间:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[写端 fd[1]] -->|数据流| B[读端 fd[0]]
|
||||
|
||||
style A fill:#e8f5e9
|
||||
style B fill:#e1f5fe
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- **单向**:只能从一端写,另一端读
|
||||
- **有亲缘关系**:通常用于父子进程
|
||||
- **自带同步**:读端空时阻塞,写端满时阻塞
|
||||
|
||||
### 3. 命名管道(FIFO)
|
||||
|
||||
命名管道让没有亲缘关系的进程也能通信:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[进程1] -->|写入| B[/tmp/my_fifo]
|
||||
B -->|读取| C[进程2]
|
||||
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 有文件名,存在于文件系统中
|
||||
- 任意进程都可以打开
|
||||
- 使用方法与普通文件相同
|
||||
|
||||
### 4. 消息队列
|
||||
|
||||
消息队列是一种异步通信方式:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[发送方 msgsnd] -->|消息| B[消息队列]
|
||||
B -->|消息| C[接收方 msgrcv]
|
||||
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**消息结构**:
|
||||
```c
|
||||
struct msgbuf {
|
||||
long mtype; // 消息类型
|
||||
char mtext[512]; // 消息内容
|
||||
};
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- 可以按类型读取消息
|
||||
- 异步通信,不需要同步
|
||||
- 可以设置优先级
|
||||
|
||||
### 5. 共享内存
|
||||
|
||||
共享内存是最快的 IPC 方式:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[进程1] -->|读写| B[共享内存区域]
|
||||
C[进程2] -->|读写| B
|
||||
|
||||
style B fill:#e8f5e9
|
||||
```
|
||||
|
||||
**工作流程**:
|
||||
1. 创建共享内存段
|
||||
2. 将共享内存映射到进程地址空间
|
||||
3. 直接读写共享内存
|
||||
4. 使用完毕后分离
|
||||
|
||||
**注意**:共享内存本身不提供同步机制,需要配合信号量使用。
|
||||
|
||||
---
|
||||
|
||||
## 💻 动手实践
|
||||
|
||||
### 示例1:使用管道通信
|
||||
|
||||
```c
|
||||
// pipe1.c - 管道通信示例
|
||||
#include "wrapper.h"
|
||||
|
||||
int main() {
|
||||
int count;
|
||||
int fds[2]; // fds[0]=读端, fds[1]=写端
|
||||
const char some_data[] = "1234567890";
|
||||
char buffer[BUFSIZ + 1];
|
||||
|
||||
memset(buffer, '\0', sizeof(buffer));
|
||||
|
||||
// 创建管道
|
||||
pipe(fds);
|
||||
|
||||
// 向管道写入数据
|
||||
count = Write(fds[1], (void *)some_data, strlen(some_data));
|
||||
printf("Wrote %d bytes\n", count);
|
||||
|
||||
// 从管道读取数据
|
||||
count = Read(fds[0], (void *)buffer, BUFSIZ);
|
||||
printf("Read %d bytes: %s\n", count, buffer);
|
||||
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o pipe1 pipe1.c -L. -lwrapper
|
||||
./pipe1
|
||||
```
|
||||
|
||||
**预期输出**:
|
||||
```
|
||||
Wrote 10 bytes
|
||||
Read 10 bytes: 1234567890
|
||||
```
|
||||
|
||||
### 示例2:创建命名管道
|
||||
|
||||
```c
|
||||
// fifo1.c - 创建命名管道
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
int main() {
|
||||
int res = mkfifo("/tmp/my_fifo", 0777);
|
||||
if (res == 0)
|
||||
printf("FIFO created\n");
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o fifo1 fifo1.c
|
||||
./fifo1
|
||||
ls -l /tmp/my_fifo
|
||||
```
|
||||
|
||||
**使用命名管道**:
|
||||
```bash
|
||||
# 终端1:写入数据
|
||||
echo "Hello FIFO" > /tmp/my_fifo
|
||||
|
||||
# 终端2:读取数据
|
||||
cat /tmp/my_fifo
|
||||
```
|
||||
|
||||
### 示例3:共享内存通信
|
||||
|
||||
```c
|
||||
// shmwrite.c - 写入共享内存
|
||||
#include "wrapper.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int shmid;
|
||||
key_t key;
|
||||
void *shmptr;
|
||||
|
||||
if (argc <= 1) {
|
||||
fprintf(stderr, "请以 ./shmwrite <key> <message> 形式运行\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// 将参数转换成十六进制数作为 key
|
||||
sscanf(argv[1], "%x", &key);
|
||||
|
||||
// 创建共享内存
|
||||
shmid = Shmget(key, 4096, IPC_CREAT | 0644);
|
||||
|
||||
// 将共享内存映射到进程地址空间
|
||||
shmptr = Shmat(shmid, 0, 0);
|
||||
|
||||
// 写入数据
|
||||
memcpy(shmptr, argv[2], strlen(argv[2]) + 1);
|
||||
|
||||
// 分离共享内存
|
||||
Shmdt(shmptr);
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// shmread.c - 读取共享内存
|
||||
#include "wrapper.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int shmid;
|
||||
key_t key;
|
||||
void *shmptr;
|
||||
|
||||
if (argc <= 1) {
|
||||
fprintf(stderr, "请以 ./shmread <key> 形式运行\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
sscanf(argv[1], "%x", &key);
|
||||
|
||||
// 获取已存在的共享内存
|
||||
shmid = Shmget(key, 4096, IPC_CREAT | 0644);
|
||||
|
||||
// 映射共享内存
|
||||
shmptr = Shmat(shmid, 0, 0);
|
||||
|
||||
// 读取数据
|
||||
printf("%s\n", (char *)shmptr);
|
||||
|
||||
// 分离共享内存
|
||||
Shmdt(shmptr);
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o shmwrite shmwrite.c -L. -lwrapper
|
||||
gcc -o shmread shmread.c -L. -lwrapper
|
||||
|
||||
# 写入数据
|
||||
./shmwrite 0x12345678 "Hello Shared Memory!"
|
||||
|
||||
# 读取数据
|
||||
./shmread 0x12345678
|
||||
```
|
||||
|
||||
**预期输出**:
|
||||
```
|
||||
Hello Shared Memory!
|
||||
```
|
||||
|
||||
### 示例4:消息队列通信
|
||||
|
||||
```c
|
||||
// msgsnd1.c - 发送消息
|
||||
#include "wrapper.h"
|
||||
|
||||
typedef struct MESSAGE {
|
||||
int mtype;
|
||||
char mtext[512];
|
||||
} mymsg, *pmymsg;
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int msqid;
|
||||
key_t key;
|
||||
mymsg msginfo;
|
||||
|
||||
if (argc != 3) {
|
||||
fprintf(stderr, "使用方法: msgsnd1 <key> <message>\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
sscanf(argv[1], "%x", &key);
|
||||
|
||||
// 获取消息队列
|
||||
msqid = Msgget(key, 0644);
|
||||
|
||||
// 设置消息类型和内容
|
||||
msginfo.mtype = 1;
|
||||
memcpy(&msginfo.mtext, argv[2], strlen(argv[2]) + 1);
|
||||
|
||||
// 发送消息
|
||||
Msgsnd(msqid, (pmymsg)&msginfo, strlen(msginfo.mtext) + 1, 0);
|
||||
printf("you send a message \"%s\" to msq %d\n", argv[1], msqid);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// msgrcv1.c - 接收消息
|
||||
#include "wrapper.h"
|
||||
|
||||
typedef struct MESSAGE {
|
||||
int mtype;
|
||||
char mtext[512];
|
||||
} mymsg, *pmymsg;
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int msqid;
|
||||
key_t key;
|
||||
mymsg msginfo;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "使用方法: msgrcv1 <key>\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
sscanf(argv[1], "%x", &key);
|
||||
|
||||
// 获取消息队列
|
||||
msqid = Msgget(key, 0644);
|
||||
|
||||
// 接收消息(类型为1)
|
||||
msgrcv(msqid, (pmymsg)&msginfo, 512, 1, 0);
|
||||
printf("%s\n", msginfo.mtext);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o msgsnd1 msgsnd1.c -L. -lwrapper
|
||||
gcc -o msgrcv1 msgrcv1.c -L. -lwrapper
|
||||
|
||||
# 发送消息
|
||||
./msgsnd1 0x12345678 "Hello Message Queue!"
|
||||
|
||||
# 接收消息
|
||||
./msgrcv1 0x12345678
|
||||
```
|
||||
|
||||
**预期输出**:
|
||||
```
|
||||
Hello Message Queue!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 知识关联
|
||||
- 管道在 Shell 中广泛使用,详见 [[06_进程控制_深入]]
|
||||
- 共享内存的同步需要信号量,详见 [[07_多线程编程]]
|
||||
- 套接字是网络通信的基础,详见 [[09_网络编程]]
|
||||
|
||||
---
|
||||
|
||||
## 📝 思考题
|
||||
|
||||
1. **管道的局限性**:为什么管道只能用于有亲缘关系的进程?
|
||||
2. **共享内存的速度优势**:为什么共享内存比管道快?
|
||||
3. **消息队列 vs 管道**:在什么场景下消息队列比管道更合适?
|
||||
|
||||
---
|
||||
|
||||
## 📚 扩展阅读
|
||||
- 《UNIX环境高级编程》第15章:进程间通信
|
||||
- 《深入理解计算机系统》第10章:系统级I/O
|
||||
- [Linux IPC 编程](https://www.tldp.org/LDP/tlk/ipc/ipc.html)
|
||||
# 第08讲:进程间通信
|
||||
|
||||
> 🎯 **本节目标**:掌握管道、消息队列、共享内存等进程间通信方式
|
||||
|
||||
## 📋 前置知识
|
||||
- [[06_进程控制]] — 进程的基本概念
|
||||
- [[06_进程控制_深入]] — fork 和 exec 的工作原理
|
||||
|
||||
---
|
||||
|
||||
## 🤔 为什么需要这个?
|
||||
|
||||
进程之间是相互隔离的,每个进程有自己的地址空间。但有时候进程需要协作:
|
||||
- Shell 需要将 `ls` 的输出传给 `grep`
|
||||
- 浏览器需要与下载管理器通信
|
||||
- 数据库需要与应用程序交互
|
||||
|
||||
**进程间通信(IPC)** 就是解决这个问题的。
|
||||
|
||||
**生活比喻**:
|
||||
- **管道** = 对讲机(单向通信)
|
||||
- **消息队列** = 邮箱(异步通信)
|
||||
- **共享内存** = 共享白板(最快的通信方式)
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 1. IPC 方式概览
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[进程间通信 IPC] --> B[管道 Pipe]
|
||||
A --> C[消息队列 Message Queue]
|
||||
A --> D[共享内存 Shared Memory]
|
||||
A --> E[信号 Signal]
|
||||
A --> F[信号量 Semaphore]
|
||||
A --> G[套接字 Socket]
|
||||
|
||||
B --> B1[单向通信]
|
||||
C --> C1[异步通信]
|
||||
D --> D1[最快]
|
||||
E --> E1[异步通知]
|
||||
F --> F1[同步控制]
|
||||
G --> G1[网络通信]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style D fill:#e8f5e9
|
||||
```
|
||||
|
||||
**对比**:
|
||||
| 方式 | 优点 | 缺点 | 适用场景 |
|
||||
|------|------|------|----------|
|
||||
| 管道 | 简单、易用 | 单向、只能父子进程 | Shell 命令组合 |
|
||||
| 消息队列 | 异步、可按类型读取 | 有大小限制 | 任务分发 |
|
||||
| 共享内存 | 最快 | 需要同步机制 | 大量数据交换 |
|
||||
| 信号 | 异步通知 | 只能传递信号编号 | 事件通知 |
|
||||
| 信号量 | 同步控制 | 不能传递数据 | 互斥、同步 |
|
||||
| 套接字 | 跨网络 | 开销大 | 网络通信 |
|
||||
|
||||
### 2. 管道(Pipe)
|
||||
|
||||
管道是最古老的 IPC 方式,用于有亲缘关系的进程之间:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[写端 fd[1]] -->|数据流| B[读端 fd[0]]
|
||||
|
||||
style A fill:#e8f5e9
|
||||
style B fill:#e1f5fe
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- **单向**:只能从一端写,另一端读
|
||||
- **有亲缘关系**:通常用于父子进程
|
||||
- **自带同步**:读端空时阻塞,写端满时阻塞
|
||||
|
||||
### 3. 命名管道(FIFO)
|
||||
|
||||
命名管道让没有亲缘关系的进程也能通信:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[进程1] -->|写入| B[/tmp/my_fifo]
|
||||
B -->|读取| C[进程2]
|
||||
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 有文件名,存在于文件系统中
|
||||
- 任意进程都可以打开
|
||||
- 使用方法与普通文件相同
|
||||
|
||||
### 4. 消息队列
|
||||
|
||||
消息队列是一种异步通信方式:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[发送方 msgsnd] -->|消息| B[消息队列]
|
||||
B -->|消息| C[接收方 msgrcv]
|
||||
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**消息结构**:
|
||||
```c
|
||||
struct msgbuf {
|
||||
long mtype; // 消息类型
|
||||
char mtext[512]; // 消息内容
|
||||
};
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- 可以按类型读取消息
|
||||
- 异步通信,不需要同步
|
||||
- 可以设置优先级
|
||||
|
||||
### 5. 共享内存
|
||||
|
||||
共享内存是最快的 IPC 方式:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[进程1] -->|读写| B[共享内存区域]
|
||||
C[进程2] -->|读写| B
|
||||
|
||||
style B fill:#e8f5e9
|
||||
```
|
||||
|
||||
**工作流程**:
|
||||
1. 创建共享内存段
|
||||
2. 将共享内存映射到进程地址空间
|
||||
3. 直接读写共享内存
|
||||
4. 使用完毕后分离
|
||||
|
||||
**注意**:共享内存本身不提供同步机制,需要配合信号量使用。
|
||||
|
||||
---
|
||||
|
||||
## 💻 动手实践
|
||||
|
||||
### 示例1:使用管道通信
|
||||
|
||||
```c
|
||||
// pipe1.c - 管道通信示例
|
||||
#include "wrapper.h"
|
||||
|
||||
int main() {
|
||||
int count;
|
||||
int fds[2]; // fds[0]=读端, fds[1]=写端
|
||||
const char some_data[] = "1234567890";
|
||||
char buffer[BUFSIZ + 1];
|
||||
|
||||
memset(buffer, '\0', sizeof(buffer));
|
||||
|
||||
// 创建管道
|
||||
pipe(fds);
|
||||
|
||||
// 向管道写入数据
|
||||
count = Write(fds[1], (void *)some_data, strlen(some_data));
|
||||
printf("Wrote %d bytes\n", count);
|
||||
|
||||
// 从管道读取数据
|
||||
count = Read(fds[0], (void *)buffer, BUFSIZ);
|
||||
printf("Read %d bytes: %s\n", count, buffer);
|
||||
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o pipe1 pipe1.c -L. -lwrapper
|
||||
./pipe1
|
||||
```
|
||||
|
||||
**预期输出**:
|
||||
```
|
||||
Wrote 10 bytes
|
||||
Read 10 bytes: 1234567890
|
||||
```
|
||||
|
||||
### 示例2:创建命名管道
|
||||
|
||||
```c
|
||||
// fifo1.c - 创建命名管道
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
int main() {
|
||||
int res = mkfifo("/tmp/my_fifo", 0777);
|
||||
if (res == 0)
|
||||
printf("FIFO created\n");
|
||||
exit(EXIT_SUCCESS);
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o fifo1 fifo1.c
|
||||
./fifo1
|
||||
ls -l /tmp/my_fifo
|
||||
```
|
||||
|
||||
**使用命名管道**:
|
||||
```bash
|
||||
# 终端1:写入数据
|
||||
echo "Hello FIFO" > /tmp/my_fifo
|
||||
|
||||
# 终端2:读取数据
|
||||
cat /tmp/my_fifo
|
||||
```
|
||||
|
||||
### 示例3:共享内存通信
|
||||
|
||||
```c
|
||||
// shmwrite.c - 写入共享内存
|
||||
#include "wrapper.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int shmid;
|
||||
key_t key;
|
||||
void *shmptr;
|
||||
|
||||
if (argc <= 1) {
|
||||
fprintf(stderr, "请以 ./shmwrite <key> <message> 形式运行\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
// 将参数转换成十六进制数作为 key
|
||||
sscanf(argv[1], "%x", &key);
|
||||
|
||||
// 创建共享内存
|
||||
shmid = Shmget(key, 4096, IPC_CREAT | 0644);
|
||||
|
||||
// 将共享内存映射到进程地址空间
|
||||
shmptr = Shmat(shmid, 0, 0);
|
||||
|
||||
// 写入数据
|
||||
memcpy(shmptr, argv[2], strlen(argv[2]) + 1);
|
||||
|
||||
// 分离共享内存
|
||||
Shmdt(shmptr);
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// shmread.c - 读取共享内存
|
||||
#include "wrapper.h"
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int shmid;
|
||||
key_t key;
|
||||
void *shmptr;
|
||||
|
||||
if (argc <= 1) {
|
||||
fprintf(stderr, "请以 ./shmread <key> 形式运行\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
sscanf(argv[1], "%x", &key);
|
||||
|
||||
// 获取已存在的共享内存
|
||||
shmid = Shmget(key, 4096, IPC_CREAT | 0644);
|
||||
|
||||
// 映射共享内存
|
||||
shmptr = Shmat(shmid, 0, 0);
|
||||
|
||||
// 读取数据
|
||||
printf("%s\n", (char *)shmptr);
|
||||
|
||||
// 分离共享内存
|
||||
Shmdt(shmptr);
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o shmwrite shmwrite.c -L. -lwrapper
|
||||
gcc -o shmread shmread.c -L. -lwrapper
|
||||
|
||||
# 写入数据
|
||||
./shmwrite 0x12345678 "Hello Shared Memory!"
|
||||
|
||||
# 读取数据
|
||||
./shmread 0x12345678
|
||||
```
|
||||
|
||||
**预期输出**:
|
||||
```
|
||||
Hello Shared Memory!
|
||||
```
|
||||
|
||||
### 示例4:消息队列通信
|
||||
|
||||
```c
|
||||
// msgsnd1.c - 发送消息
|
||||
#include "wrapper.h"
|
||||
|
||||
typedef struct MESSAGE {
|
||||
int mtype;
|
||||
char mtext[512];
|
||||
} mymsg, *pmymsg;
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int msqid;
|
||||
key_t key;
|
||||
mymsg msginfo;
|
||||
|
||||
if (argc != 3) {
|
||||
fprintf(stderr, "使用方法: msgsnd1 <key> <message>\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
sscanf(argv[1], "%x", &key);
|
||||
|
||||
// 获取消息队列
|
||||
msqid = Msgget(key, 0644);
|
||||
|
||||
// 设置消息类型和内容
|
||||
msginfo.mtype = 1;
|
||||
memcpy(&msginfo.mtext, argv[2], strlen(argv[2]) + 1);
|
||||
|
||||
// 发送消息
|
||||
Msgsnd(msqid, (pmymsg)&msginfo, strlen(msginfo.mtext) + 1, 0);
|
||||
printf("you send a message \"%s\" to msq %d\n", argv[1], msqid);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
```c
|
||||
// msgrcv1.c - 接收消息
|
||||
#include "wrapper.h"
|
||||
|
||||
typedef struct MESSAGE {
|
||||
int mtype;
|
||||
char mtext[512];
|
||||
} mymsg, *pmymsg;
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
int msqid;
|
||||
key_t key;
|
||||
mymsg msginfo;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "使用方法: msgrcv1 <key>\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
sscanf(argv[1], "%x", &key);
|
||||
|
||||
// 获取消息队列
|
||||
msqid = Msgget(key, 0644);
|
||||
|
||||
// 接收消息(类型为1)
|
||||
msgrcv(msqid, (pmymsg)&msginfo, 512, 1, 0);
|
||||
printf("%s\n", msginfo.mtext);
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o msgsnd1 msgsnd1.c -L. -lwrapper
|
||||
gcc -o msgrcv1 msgrcv1.c -L. -lwrapper
|
||||
|
||||
# 发送消息
|
||||
./msgsnd1 0x12345678 "Hello Message Queue!"
|
||||
|
||||
# 接收消息
|
||||
./msgrcv1 0x12345678
|
||||
```
|
||||
|
||||
**预期输出**:
|
||||
```
|
||||
Hello Message Queue!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 知识关联
|
||||
- 管道在 Shell 中广泛使用,详见 [[06_进程控制_深入]]
|
||||
- 共享内存的同步需要信号量,详见 [[07_多线程编程]]
|
||||
- 套接字是网络通信的基础,详见 [[09_网络编程]]
|
||||
|
||||
---
|
||||
|
||||
## 📝 思考题
|
||||
|
||||
1. **管道的局限性**:为什么管道只能用于有亲缘关系的进程?
|
||||
2. **共享内存的速度优势**:为什么共享内存比管道快?
|
||||
3. **消息队列 vs 管道**:在什么场景下消息队列比管道更合适?
|
||||
|
||||
---
|
||||
|
||||
## 📚 扩展阅读
|
||||
- 《UNIX环境高级编程》第15章:进程间通信
|
||||
- 《深入理解计算机系统》第10章:系统级I/O
|
||||
- [Linux IPC 编程](https://www.tldp.org/LDP/tlk/ipc/ipc.html)
|
||||
|
||||
@@ -1,296 +1,296 @@
|
||||
# 第09讲:网络编程
|
||||
|
||||
> 🎯 **本节目标**:掌握 Socket 编程,理解客户端-服务器模型
|
||||
|
||||
## 📋 前置知识
|
||||
- [[04_文件IO编程]] — 文件描述符的概念
|
||||
- [[06_进程控制]] — 进程创建
|
||||
|
||||
---
|
||||
|
||||
## 🤔 为什么需要这个?
|
||||
|
||||
你每天都在使用网络:浏览网页、发送消息、观看视频。但你有没有想过:
|
||||
- 浏览器是怎么从服务器获取网页的?
|
||||
- 两台电脑之间是怎么通信的?
|
||||
|
||||
**网络编程**就是让你能够编写这样的程序。
|
||||
|
||||
**生活比喻**:
|
||||
- **Socket** = 电话插座
|
||||
- **服务器** = 客服中心(等待来电)
|
||||
- **客户端** = 拨打电话的用户
|
||||
- **端口** = 分机号
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 1. 客户端-服务器模型
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 客户端
|
||||
participant 服务器
|
||||
|
||||
服务器->>服务器: socket() 创建套接字
|
||||
服务器->>服务器: bind() 绑定地址
|
||||
服务器->>服务器: listen() 监听连接
|
||||
服务器->>服务器: accept() 等待连接
|
||||
|
||||
客户端->>客户端: socket() 创建套接字
|
||||
客户端->>服务器: connect() 发起连接
|
||||
|
||||
服务器->>客户端: 连接建立
|
||||
|
||||
客户端->>服务器: send() 发送数据
|
||||
服务器->>客户端: recv() 接收数据
|
||||
服务器->>客户端: send() 发送响应
|
||||
客户端->>服务器: recv() 接收响应
|
||||
|
||||
客户端->>客户端: close() 关闭连接
|
||||
服务器->>服务器: close() 关闭连接
|
||||
```
|
||||
|
||||
### 2. Socket 编程流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph 服务器端
|
||||
A1[socket] --> A2[bind]
|
||||
A2 --> A3[listen]
|
||||
A3 --> A4[accept]
|
||||
A4 --> A5[read/write]
|
||||
A5 --> A6[close]
|
||||
end
|
||||
subgraph 客户端
|
||||
B1[socket] --> B2[connect]
|
||||
B2 --> B3[write/read]
|
||||
B3 --> B4[close]
|
||||
end
|
||||
|
||||
style A1 fill:#e8f5e9
|
||||
style B1 fill:#e1f5fe
|
||||
```
|
||||
|
||||
**核心函数**:
|
||||
| 函数 | 作用 | 服务器/客户端 |
|
||||
|------|------|:---:|
|
||||
| `socket()` | 创建套接字 | 都需要 |
|
||||
| `bind()` | 绑定地址和端口 | 服务器 |
|
||||
| `listen()` | 开始监听 | 服务器 |
|
||||
| `accept()` | 接受连接 | 服务器 |
|
||||
| `connect()` | 发起连接 | 客户端 |
|
||||
| `send()` | 发送数据 | 都需要 |
|
||||
| `recv()` | 接收数据 | 都需要 |
|
||||
| `close()` | 关闭连接 | 都需要 |
|
||||
|
||||
### 3. IP 地址与端口
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[计算机] -->|IP地址| B[192.168.1.100]
|
||||
A -->|端口| C[:80]
|
||||
B --> D[唯一标识一台电脑]
|
||||
C --> E[唯一标识一个服务]
|
||||
|
||||
style B fill:#e1f5fe
|
||||
style C fill:#e8f5e9
|
||||
```
|
||||
|
||||
**常见端口**:
|
||||
| 端口 | 服务 | 说明 |
|
||||
|:---:|------|------|
|
||||
| 22 | SSH | 远程登录 |
|
||||
| 80 | HTTP | 网页浏览 |
|
||||
| 443 | HTTPS | 安全网页 |
|
||||
| 3306 | MySQL | 数据库 |
|
||||
| 8080 | HTTP备用 | 开发常用 |
|
||||
|
||||
### 4. 字节序
|
||||
|
||||
不同 CPU 存储多字节数据的方式不同:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[0x12345678] --> B[大端序 Big-endian]
|
||||
A --> C[小端序 Little-endian]
|
||||
|
||||
B --> B1[12 34 56 78]
|
||||
C --> C1[78 56 34 12]
|
||||
|
||||
style B fill:#e1f5fe
|
||||
style C fill:#e8f5e9
|
||||
```
|
||||
|
||||
**网络字节序**:大端序(Big-endian)
|
||||
|
||||
**转换函数**:
|
||||
```c
|
||||
htonl() // host to network long
|
||||
htons() // host to network short
|
||||
ntohl() // network to host long
|
||||
ntohs() // network to host short
|
||||
```
|
||||
|
||||
### 5. DNS 域名解析
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[www.example.com] -->|DNS查询| B[DNS服务器]
|
||||
B -->|返回IP| C[93.184.216.34]
|
||||
C -->|连接| D[Web服务器]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style C fill:#e8f5e9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 动手实践
|
||||
|
||||
### 示例1:查询主机信息
|
||||
|
||||
```c
|
||||
// hostinfo.c - 查询主机信息
|
||||
#include "wrapper.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
char **pp;
|
||||
struct in_addr addr;
|
||||
struct hostent *hostp;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s <domain name or dotted-decimal>\n", argv[0]);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// 判断是域名还是IP地址
|
||||
if (inet_aton(argv[1], &addr) != 0)
|
||||
hostp = Gethostbyaddr((const char *)&addr, sizeof(addr), AF_INET);
|
||||
else
|
||||
hostp = Gethostbyname(argv[1]);
|
||||
|
||||
// 打印主机信息
|
||||
printf("official hostname: %s\n", hostp->h_name);
|
||||
|
||||
for (pp = hostp->h_aliases; *pp != NULL; pp++)
|
||||
printf("alias: %s\n", *pp);
|
||||
|
||||
for (pp = hostp->h_addr_list; *pp != NULL; pp++) {
|
||||
addr.s_addr = *((unsigned int *)*pp);
|
||||
printf("address: %s\n", inet_ntoa(addr));
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o hostinfo hostinfo.c -L. -lwrapper
|
||||
./hostinfo www.baidu.com
|
||||
```
|
||||
|
||||
**预期输出**:
|
||||
```
|
||||
official hostname: www.a.shifen.com
|
||||
alias: www.baidu.com
|
||||
address: 110.242.68.66
|
||||
address: 110.242.68.3
|
||||
```
|
||||
|
||||
### 示例2:TCP 服务器(大小写转换)
|
||||
|
||||
```c
|
||||
// toggle.c - TCP 服务器
|
||||
#include "wrapper.h"
|
||||
|
||||
void toggle(int conn_sock) {
|
||||
size_t n;
|
||||
int i;
|
||||
char buf[MAXLINE];
|
||||
|
||||
while ((n = recv(conn_sock, buf, MAXLINE, 0)) > 0) {
|
||||
printf("toggle server received %d bytes\n", n);
|
||||
|
||||
// 转换大小写
|
||||
for (i = 0; i < n; i++)
|
||||
if (isupper(buf[i]))
|
||||
buf[i] = tolower(buf[i]);
|
||||
else if (islower(buf[i]))
|
||||
buf[i] = toupper(buf[i]);
|
||||
|
||||
send(conn_sock, buf, n, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例3:TCP 客户端
|
||||
|
||||
```c
|
||||
// togglec.c - TCP 客户端
|
||||
#include "wrapper.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int client_sock, port;
|
||||
char *host, buf[MAXLINE];
|
||||
rio_t rio;
|
||||
|
||||
if (argc != 3) {
|
||||
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
host = argv[1];
|
||||
port = atoi(argv[2]);
|
||||
|
||||
// 连接服务器
|
||||
client_sock = open_client_sock(host, port);
|
||||
|
||||
// 从标准输入读取,发送到服务器,接收响应
|
||||
while (fgets(buf, MAXLINE, stdin) != NULL) {
|
||||
send(client_sock, buf, strlen(buf), 0);
|
||||
recv(client_sock, buf, MAXLINE, 0);
|
||||
fputs(buf, stdout);
|
||||
}
|
||||
|
||||
close(client_sock);
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
# 编译
|
||||
gcc -o toggle toggle.c -L. -lwrapper
|
||||
gcc -o togglec togglec.c -L. -lwrapper
|
||||
|
||||
# 终端1:启动服务器
|
||||
./toggle 8080
|
||||
|
||||
# 终端2:启动客户端
|
||||
./togglec localhost 8080
|
||||
Hello World # 输入
|
||||
HELLO WORLD # 输出(大小写转换)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 知识关联
|
||||
- Socket 是文件描述符,详见 [[04_文件IO编程]]
|
||||
- 并发服务器在 [[10_并发服务器]] 中有详细讲解
|
||||
- 网络编程在 [[实验05_Linux网络通信编程]] 中有实践练习
|
||||
|
||||
---
|
||||
|
||||
## 📝 思考题
|
||||
|
||||
1. **TCP vs UDP**:什么时候用 TCP,什么时候用 UDP?
|
||||
2. **为什么需要字节序转换?** 如果不转换会怎样?
|
||||
3. **服务器为什么需要 bind()?** 客户端为什么不需要?
|
||||
|
||||
---
|
||||
|
||||
## 📚 扩展阅读
|
||||
- 《UNIX网络编程》第1卷:套接字联网API
|
||||
- [Beej's Guide to Network Programming](https://beej.us/guide/bgnet/)
|
||||
- [Socket 编程详解](https://www.cs.rpi.edu/~moorthy/Courses/os98/Pggrams/socket.html)
|
||||
# 第09讲:网络编程
|
||||
|
||||
> 🎯 **本节目标**:掌握 Socket 编程,理解客户端-服务器模型
|
||||
|
||||
## 📋 前置知识
|
||||
- [[04_文件IO编程]] — 文件描述符的概念
|
||||
- [[06_进程控制]] — 进程创建
|
||||
|
||||
---
|
||||
|
||||
## 🤔 为什么需要这个?
|
||||
|
||||
你每天都在使用网络:浏览网页、发送消息、观看视频。但你有没有想过:
|
||||
- 浏览器是怎么从服务器获取网页的?
|
||||
- 两台电脑之间是怎么通信的?
|
||||
|
||||
**网络编程**就是让你能够编写这样的程序。
|
||||
|
||||
**生活比喻**:
|
||||
- **Socket** = 电话插座
|
||||
- **服务器** = 客服中心(等待来电)
|
||||
- **客户端** = 拨打电话的用户
|
||||
- **端口** = 分机号
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 1. 客户端-服务器模型
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 客户端
|
||||
participant 服务器
|
||||
|
||||
服务器->>服务器: socket() 创建套接字
|
||||
服务器->>服务器: bind() 绑定地址
|
||||
服务器->>服务器: listen() 监听连接
|
||||
服务器->>服务器: accept() 等待连接
|
||||
|
||||
客户端->>客户端: socket() 创建套接字
|
||||
客户端->>服务器: connect() 发起连接
|
||||
|
||||
服务器->>客户端: 连接建立
|
||||
|
||||
客户端->>服务器: send() 发送数据
|
||||
服务器->>客户端: recv() 接收数据
|
||||
服务器->>客户端: send() 发送响应
|
||||
客户端->>服务器: recv() 接收响应
|
||||
|
||||
客户端->>客户端: close() 关闭连接
|
||||
服务器->>服务器: close() 关闭连接
|
||||
```
|
||||
|
||||
### 2. Socket 编程流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
subgraph 服务器端
|
||||
A1[socket] --> A2[bind]
|
||||
A2 --> A3[listen]
|
||||
A3 --> A4[accept]
|
||||
A4 --> A5[read/write]
|
||||
A5 --> A6[close]
|
||||
end
|
||||
subgraph 客户端
|
||||
B1[socket] --> B2[connect]
|
||||
B2 --> B3[write/read]
|
||||
B3 --> B4[close]
|
||||
end
|
||||
|
||||
style A1 fill:#e8f5e9
|
||||
style B1 fill:#e1f5fe
|
||||
```
|
||||
|
||||
**核心函数**:
|
||||
| 函数 | 作用 | 服务器/客户端 |
|
||||
|------|------|:---:|
|
||||
| `socket()` | 创建套接字 | 都需要 |
|
||||
| `bind()` | 绑定地址和端口 | 服务器 |
|
||||
| `listen()` | 开始监听 | 服务器 |
|
||||
| `accept()` | 接受连接 | 服务器 |
|
||||
| `connect()` | 发起连接 | 客户端 |
|
||||
| `send()` | 发送数据 | 都需要 |
|
||||
| `recv()` | 接收数据 | 都需要 |
|
||||
| `close()` | 关闭连接 | 都需要 |
|
||||
|
||||
### 3. IP 地址与端口
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[计算机] -->|IP地址| B[192.168.1.100]
|
||||
A -->|端口| C[:80]
|
||||
B --> D[唯一标识一台电脑]
|
||||
C --> E[唯一标识一个服务]
|
||||
|
||||
style B fill:#e1f5fe
|
||||
style C fill:#e8f5e9
|
||||
```
|
||||
|
||||
**常见端口**:
|
||||
| 端口 | 服务 | 说明 |
|
||||
|:---:|------|------|
|
||||
| 22 | SSH | 远程登录 |
|
||||
| 80 | HTTP | 网页浏览 |
|
||||
| 443 | HTTPS | 安全网页 |
|
||||
| 3306 | MySQL | 数据库 |
|
||||
| 8080 | HTTP备用 | 开发常用 |
|
||||
|
||||
### 4. 字节序
|
||||
|
||||
不同 CPU 存储多字节数据的方式不同:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[0x12345678] --> B[大端序 Big-endian]
|
||||
A --> C[小端序 Little-endian]
|
||||
|
||||
B --> B1[12 34 56 78]
|
||||
C --> C1[78 56 34 12]
|
||||
|
||||
style B fill:#e1f5fe
|
||||
style C fill:#e8f5e9
|
||||
```
|
||||
|
||||
**网络字节序**:大端序(Big-endian)
|
||||
|
||||
**转换函数**:
|
||||
```c
|
||||
htonl() // host to network long
|
||||
htons() // host to network short
|
||||
ntohl() // network to host long
|
||||
ntohs() // network to host short
|
||||
```
|
||||
|
||||
### 5. DNS 域名解析
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A[www.example.com] -->|DNS查询| B[DNS服务器]
|
||||
B -->|返回IP| C[93.184.216.34]
|
||||
C -->|连接| D[Web服务器]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style C fill:#e8f5e9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 动手实践
|
||||
|
||||
### 示例1:查询主机信息
|
||||
|
||||
```c
|
||||
// hostinfo.c - 查询主机信息
|
||||
#include "wrapper.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
char **pp;
|
||||
struct in_addr addr;
|
||||
struct hostent *hostp;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s <domain name or dotted-decimal>\n", argv[0]);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// 判断是域名还是IP地址
|
||||
if (inet_aton(argv[1], &addr) != 0)
|
||||
hostp = Gethostbyaddr((const char *)&addr, sizeof(addr), AF_INET);
|
||||
else
|
||||
hostp = Gethostbyname(argv[1]);
|
||||
|
||||
// 打印主机信息
|
||||
printf("official hostname: %s\n", hostp->h_name);
|
||||
|
||||
for (pp = hostp->h_aliases; *pp != NULL; pp++)
|
||||
printf("alias: %s\n", *pp);
|
||||
|
||||
for (pp = hostp->h_addr_list; *pp != NULL; pp++) {
|
||||
addr.s_addr = *((unsigned int *)*pp);
|
||||
printf("address: %s\n", inet_ntoa(addr));
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o hostinfo hostinfo.c -L. -lwrapper
|
||||
./hostinfo www.baidu.com
|
||||
```
|
||||
|
||||
**预期输出**:
|
||||
```
|
||||
official hostname: www.a.shifen.com
|
||||
alias: www.baidu.com
|
||||
address: 110.242.68.66
|
||||
address: 110.242.68.3
|
||||
```
|
||||
|
||||
### 示例2:TCP 服务器(大小写转换)
|
||||
|
||||
```c
|
||||
// toggle.c - TCP 服务器
|
||||
#include "wrapper.h"
|
||||
|
||||
void toggle(int conn_sock) {
|
||||
size_t n;
|
||||
int i;
|
||||
char buf[MAXLINE];
|
||||
|
||||
while ((n = recv(conn_sock, buf, MAXLINE, 0)) > 0) {
|
||||
printf("toggle server received %d bytes\n", n);
|
||||
|
||||
// 转换大小写
|
||||
for (i = 0; i < n; i++)
|
||||
if (isupper(buf[i]))
|
||||
buf[i] = tolower(buf[i]);
|
||||
else if (islower(buf[i]))
|
||||
buf[i] = toupper(buf[i]);
|
||||
|
||||
send(conn_sock, buf, n, 0);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 示例3:TCP 客户端
|
||||
|
||||
```c
|
||||
// togglec.c - TCP 客户端
|
||||
#include "wrapper.h"
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int client_sock, port;
|
||||
char *host, buf[MAXLINE];
|
||||
rio_t rio;
|
||||
|
||||
if (argc != 3) {
|
||||
fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
host = argv[1];
|
||||
port = atoi(argv[2]);
|
||||
|
||||
// 连接服务器
|
||||
client_sock = open_client_sock(host, port);
|
||||
|
||||
// 从标准输入读取,发送到服务器,接收响应
|
||||
while (fgets(buf, MAXLINE, stdin) != NULL) {
|
||||
send(client_sock, buf, strlen(buf), 0);
|
||||
recv(client_sock, buf, MAXLINE, 0);
|
||||
fputs(buf, stdout);
|
||||
}
|
||||
|
||||
close(client_sock);
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
# 编译
|
||||
gcc -o toggle toggle.c -L. -lwrapper
|
||||
gcc -o togglec togglec.c -L. -lwrapper
|
||||
|
||||
# 终端1:启动服务器
|
||||
./toggle 8080
|
||||
|
||||
# 终端2:启动客户端
|
||||
./togglec localhost 8080
|
||||
Hello World # 输入
|
||||
HELLO WORLD # 输出(大小写转换)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 知识关联
|
||||
- Socket 是文件描述符,详见 [[04_文件IO编程]]
|
||||
- 并发服务器在 [[10_并发服务器]] 中有详细讲解
|
||||
- 网络编程在 [[实验05_Linux网络通信编程]] 中有实践练习
|
||||
|
||||
---
|
||||
|
||||
## 📝 思考题
|
||||
|
||||
1. **TCP vs UDP**:什么时候用 TCP,什么时候用 UDP?
|
||||
2. **为什么需要字节序转换?** 如果不转换会怎样?
|
||||
3. **服务器为什么需要 bind()?** 客户端为什么不需要?
|
||||
|
||||
---
|
||||
|
||||
## 📚 扩展阅读
|
||||
- 《UNIX网络编程》第1卷:套接字联网API
|
||||
- [Beej's Guide to Network Programming](https://beej.us/guide/bgnet/)
|
||||
- [Socket 编程详解](https://www.cs.rpi.edu/~moorthy/Courses/os98/Pggrams/socket.html)
|
||||
|
||||
@@ -1,395 +1,395 @@
|
||||
# 第10讲:并发网络服务器
|
||||
|
||||
> 🎯 **本节目标**:掌握多进程、多线程、I/O 多路复用三种并发服务器模型
|
||||
|
||||
## 📋 前置知识
|
||||
- [[09_网络编程]] — Socket 编程基础
|
||||
- [[06_进程控制]] — 进程创建
|
||||
- [[07_多线程编程]] — 线程创建
|
||||
|
||||
---
|
||||
|
||||
## 🤔 为什么需要这个?
|
||||
|
||||
上一讲的服务器一次只能服务一个客户端。如果有 100 个用户同时访问网站,第 100 个用户必须等前 99 个都处理完才能得到响应。
|
||||
|
||||
**并发服务器**就是解决这个问题的——让服务器能够同时服务多个客户端。
|
||||
|
||||
**生活比喻**:
|
||||
- **迭代服务器** = 一个服务员一次只服务一桌客人
|
||||
- **多进程服务器** = 每来一桌客人就招一个新服务员
|
||||
- **多线程服务器** = 一个服务员同时照看多桌客人
|
||||
- **I/O 多路复用** = 服务员轮流查看哪桌客人需要服务
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 1. 三种并发模型
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[并发服务器模型] --> B[多进程模型]
|
||||
A --> C[多线程模型]
|
||||
A --> D[I/O 多路复用]
|
||||
|
||||
B --> B1[每连接一个进程]
|
||||
B --> B2[进程间隔离]
|
||||
B --> B3[开销大]
|
||||
|
||||
C --> C1[每连接一个线程]
|
||||
C --> C2[共享内存]
|
||||
C --> C3[开销较小]
|
||||
|
||||
D --> D1[单线程处理多连接]
|
||||
D --> D2[select/poll/epoll]
|
||||
D --> D3[开销最小]
|
||||
|
||||
style B fill:#ffcdd2
|
||||
style C fill:#fff3e0
|
||||
style D fill:#e8f5e9
|
||||
```
|
||||
|
||||
**对比**:
|
||||
| 模型 | 优点 | 缺点 | 适用场景 |
|
||||
|------|------|------|----------|
|
||||
| 多进程 | 隔离性好 | 开销大 | 连接数少 |
|
||||
| 多线程 | 开销较小 | 需要同步 | 连接数中等 |
|
||||
| I/O 多路复用 | 开销最小 | 编程复杂 | 连接数多 |
|
||||
|
||||
### 2. 多进程模型
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 主进程
|
||||
participant 子进程1
|
||||
participant 子进程2
|
||||
participant 客户端
|
||||
|
||||
主进程->>主进程: accept() 等待连接
|
||||
客户端->>主进程: 连接请求
|
||||
主进程->>子进程1: fork() 创建子进程
|
||||
子进程1->>客户端: 处理请求
|
||||
主进程->>主进程: 继续 accept()
|
||||
|
||||
客户端->>主进程: 新连接请求
|
||||
主进程->>子进程2: fork() 创建子进程
|
||||
子进程2->>客户端: 处理请求
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 每个连接一个独立进程
|
||||
- 进程间完全隔离
|
||||
- 进程创建和销毁开销大
|
||||
|
||||
### 3. 多线程模型
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 主线程
|
||||
participant 工作线程1
|
||||
participant 工作线程2
|
||||
participant 客户端
|
||||
|
||||
主线程->>主线程: accept() 等待连接
|
||||
客户端->>主线程: 连接请求
|
||||
主线程->>工作线程1: pthread_create()
|
||||
工作线程1->>客户端: 处理请求
|
||||
主线程->>主线程: 继续 accept()
|
||||
|
||||
客户端->>主线程: 新连接请求
|
||||
主线程->>工作线程2: pthread_create()
|
||||
工作线程2->>客户端: 处理请求
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 每个连接一个线程
|
||||
- 线程共享进程资源
|
||||
- 需要注意线程安全
|
||||
|
||||
### 4. I/O 多路复用(select)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[主循环] --> B[select 监听所有 fd]
|
||||
B --> C{哪个 fd 就绪?}
|
||||
C -->|监听套接字| D[accept 新连接]
|
||||
C -->|客户端套接字| E[处理请求]
|
||||
D --> A
|
||||
E --> A
|
||||
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**select 的工作原理**:
|
||||
1. 将所有需要监听的文件描述符放入集合
|
||||
2. 调用 `select()` 等待任意一个就绪
|
||||
3. 遍历集合,处理就绪的描述符
|
||||
4. 重复步骤 1
|
||||
|
||||
**fd_set 操作**:
|
||||
```c
|
||||
fd_set read_set;
|
||||
FD_ZERO(&read_set); // 清空集合
|
||||
FD_SET(fd1, &read_set); // 添加 fd1
|
||||
FD_SET(fd2, &read_set); // 添加 fd2
|
||||
select(maxfd+1, &read_set, NULL, NULL, NULL); // 等待
|
||||
if (FD_ISSET(fd1, &read_set)) // 检查 fd1 是否就绪
|
||||
// 处理 fd1
|
||||
```
|
||||
|
||||
### 5. 线程池模型
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[主线程] -->|accept| B[任务队列]
|
||||
B --> C[工作线程1]
|
||||
B --> D[工作线程2]
|
||||
B --> E[工作线程3]
|
||||
C -->|处理完毕| B
|
||||
D -->|处理完毕| B
|
||||
E -->|处理完毕| B
|
||||
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- 避免频繁创建销毁线程
|
||||
- 控制并发数量
|
||||
- 提高资源利用率
|
||||
|
||||
---
|
||||
|
||||
## 💻 动手实践
|
||||
|
||||
### 示例1:多线程并发服务器
|
||||
|
||||
```c
|
||||
// togglest.c - 多线程并发服务器
|
||||
#include "wrapper.h"
|
||||
|
||||
void toggle(int conn_sock);
|
||||
void *serve_client(void *vargp);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int listen_sock, conn_sock, port, *conn_sock_p;
|
||||
struct sockaddr_in clientaddr;
|
||||
struct hostent *hp;
|
||||
char *haddrp;
|
||||
socklen_t clientlen = sizeof(struct sockaddr_in);
|
||||
pthread_t tid;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s <port>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
port = atoi(argv[1]);
|
||||
|
||||
listen_sock = open_listen_sock(port);
|
||||
|
||||
while (1) {
|
||||
conn_sock_p = malloc(sizeof(int));
|
||||
*conn_sock_p = accept(listen_sock, (SA *)&clientaddr, &clientlen);
|
||||
|
||||
// 获取客户端信息
|
||||
hp = Gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr,
|
||||
sizeof(clientaddr.sin_addr.s_addr), AF_INET);
|
||||
haddrp = inet_ntoa(clientaddr.sin_addr);
|
||||
printf("server connected to %s (%s)\n", hp->h_name, haddrp);
|
||||
|
||||
// 创建新线程处理客户端
|
||||
pthread_create(&tid, NULL, serve_client, conn_sock_p);
|
||||
}
|
||||
}
|
||||
|
||||
void *serve_client(void *vargp) {
|
||||
int conn_sock = *((int *)vargp);
|
||||
pthread_detach(pthread_self()); // 分离线程
|
||||
free(vargp);
|
||||
toggle(conn_sock);
|
||||
close(conn_sock);
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o togglest togglest.c -L. -lwrapper -lpthread
|
||||
|
||||
# 终端1:启动服务器
|
||||
./togglest 8080
|
||||
|
||||
# 终端2-4:启动多个客户端
|
||||
./togglec localhost 8080
|
||||
```
|
||||
|
||||
### 示例2:I/O 多路复用服务器
|
||||
|
||||
```c
|
||||
// toggless1.c - select 多路复用服务器
|
||||
#include "wrapper.h"
|
||||
|
||||
void toggle(int conn_sock);
|
||||
void read_input(void);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int listen_sock, conn_sock, port;
|
||||
socklen_t clientlen = sizeof(struct sockaddr_in);
|
||||
struct sockaddr_in clientaddr;
|
||||
fd_set read_set, ready_set;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s <port>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
port = atoi(argv[1]);
|
||||
listen_sock = open_listen_sock(port);
|
||||
|
||||
// 初始化 fd_set
|
||||
FD_ZERO(&read_set);
|
||||
FD_SET(STDIN_FILENO, &read_set); // 监听标准输入
|
||||
FD_SET(listen_sock, &read_set); // 监听套接字
|
||||
|
||||
while (1) {
|
||||
ready_set = read_set;
|
||||
select(listen_sock + 1, &ready_set, NULL, NULL, NULL);
|
||||
|
||||
// 检查标准输入
|
||||
if (FD_ISSET(STDIN_FILENO, &ready_set))
|
||||
read_input();
|
||||
|
||||
// 检查新连接
|
||||
if (FD_ISSET(listen_sock, &ready_set)) {
|
||||
conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen);
|
||||
toggle(conn_sock);
|
||||
close(conn_sock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void read_input(void) {
|
||||
char buf[MAXLINE];
|
||||
if (!fgets(buf, MAXLINE, stdin))
|
||||
exit(0);
|
||||
printf("%s", buf);
|
||||
}
|
||||
```
|
||||
|
||||
### 示例3:连接池服务器
|
||||
|
||||
```c
|
||||
// toggless2.c - 连接池服务器
|
||||
#include "wrapper.h"
|
||||
|
||||
typedef struct {
|
||||
int maxfd;
|
||||
fd_set read_set;
|
||||
fd_set ready_set;
|
||||
int nready;
|
||||
int maxi;
|
||||
int client_sock[FD_SETSIZE];
|
||||
} sock_pool;
|
||||
|
||||
void init_sock_pool(int listen_sock, sock_pool *pool);
|
||||
void add_sock(int conn_sock, sock_pool *pool);
|
||||
void serve_clients(sock_pool *pool);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int listen_sock, conn_sock, port;
|
||||
socklen_t clientlen = sizeof(struct sockaddr_in);
|
||||
struct sockaddr_in clientaddr;
|
||||
static sock_pool pool;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s <port>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
port = atoi(argv[1]);
|
||||
|
||||
listen_sock = open_listen_sock(port);
|
||||
init_sock_pool(listen_sock, &pool);
|
||||
|
||||
while (1) {
|
||||
pool.ready_set = pool.read_set;
|
||||
pool.nready = select(pool.maxfd + 1, &pool.ready_set, NULL, NULL, NULL);
|
||||
|
||||
// 新连接
|
||||
if (FD_ISSET(listen_sock, &pool.ready_set)) {
|
||||
conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen);
|
||||
add_sock(conn_sock, &pool);
|
||||
}
|
||||
|
||||
// 处理客户端请求
|
||||
serve_clients(&pool);
|
||||
}
|
||||
}
|
||||
|
||||
void init_sock_pool(int listen_sock, sock_pool *p) {
|
||||
int i;
|
||||
p->maxi = -1;
|
||||
for (i = 0; i < FD_SETSIZE; i++)
|
||||
p->client_sock[i] = -1;
|
||||
p->maxfd = listen_sock;
|
||||
FD_ZERO(&p->read_set);
|
||||
FD_SET(listen_sock, &p->read_set);
|
||||
}
|
||||
|
||||
void add_sock(int conn_sock, sock_pool *p) {
|
||||
int i;
|
||||
p->nready--;
|
||||
for (i = 0; i < FD_SETSIZE; i++)
|
||||
if (p->client_sock[i] < 0) {
|
||||
p->client_sock[i] = conn_sock;
|
||||
FD_SET(conn_sock, &p->read_set);
|
||||
if (conn_sock > p->maxfd)
|
||||
p->maxfd = conn_sock;
|
||||
if (i > p->maxi)
|
||||
p->maxi = i;
|
||||
break;
|
||||
}
|
||||
if (i == FD_SETSIZE)
|
||||
perror("add_sock error: Too many clients");
|
||||
}
|
||||
|
||||
void serve_clients(sock_pool *p) {
|
||||
int i, conn_sock, n;
|
||||
char buf[MAXLINE];
|
||||
|
||||
for (i = 0; (i <= p->maxi) && (p->nready > 0); i++) {
|
||||
conn_sock = p->client_sock[i];
|
||||
if ((conn_sock > 0) && (FD_ISSET(conn_sock, &p->ready_set))) {
|
||||
p->nready--;
|
||||
if ((n = recv(conn_sock, buf, MAXLINE, 0)) != 0) {
|
||||
printf("Server received %d bytes on fd %d\n", n, conn_sock);
|
||||
send(conn_sock, buf, n, 0);
|
||||
} else {
|
||||
close(conn_sock);
|
||||
FD_CLR(conn_sock, &p->read_set);
|
||||
p->client_sock[i] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 知识关联
|
||||
- 多进程模型在 [[06_进程控制]] 中有详细讲解
|
||||
- 多线程模型在 [[07_多线程编程]] 中有详细讲解
|
||||
- I/O 多路复用在 [[17_IO系统]] 中有更深入的讨论
|
||||
|
||||
---
|
||||
|
||||
## 📝 思考题
|
||||
|
||||
1. **为什么 select 有 FD_SETSIZE 限制?** 如何突破这个限制?
|
||||
2. **多进程 vs 多线程**:在什么情况下多进程比多线程更合适?
|
||||
3. **epoll 的优势**:为什么 Linux 推荐使用 epoll 而不是 select?
|
||||
|
||||
---
|
||||
|
||||
## 📚 扩展阅读
|
||||
- 《UNIX网络编程》第1卷:第6章、第16章
|
||||
- [epoll 详解](https://man7.org/linux/man-pages/man7/epoll.7.html)
|
||||
- [高性能网络编程](https://www.zhihu.com/question/28594409)
|
||||
# 第10讲:并发网络服务器
|
||||
|
||||
> 🎯 **本节目标**:掌握多进程、多线程、I/O 多路复用三种并发服务器模型
|
||||
|
||||
## 📋 前置知识
|
||||
- [[09_网络编程]] — Socket 编程基础
|
||||
- [[06_进程控制]] — 进程创建
|
||||
- [[07_多线程编程]] — 线程创建
|
||||
|
||||
---
|
||||
|
||||
## 🤔 为什么需要这个?
|
||||
|
||||
上一讲的服务器一次只能服务一个客户端。如果有 100 个用户同时访问网站,第 100 个用户必须等前 99 个都处理完才能得到响应。
|
||||
|
||||
**并发服务器**就是解决这个问题的——让服务器能够同时服务多个客户端。
|
||||
|
||||
**生活比喻**:
|
||||
- **迭代服务器** = 一个服务员一次只服务一桌客人
|
||||
- **多进程服务器** = 每来一桌客人就招一个新服务员
|
||||
- **多线程服务器** = 一个服务员同时照看多桌客人
|
||||
- **I/O 多路复用** = 服务员轮流查看哪桌客人需要服务
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 1. 三种并发模型
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[并发服务器模型] --> B[多进程模型]
|
||||
A --> C[多线程模型]
|
||||
A --> D[I/O 多路复用]
|
||||
|
||||
B --> B1[每连接一个进程]
|
||||
B --> B2[进程间隔离]
|
||||
B --> B3[开销大]
|
||||
|
||||
C --> C1[每连接一个线程]
|
||||
C --> C2[共享内存]
|
||||
C --> C3[开销较小]
|
||||
|
||||
D --> D1[单线程处理多连接]
|
||||
D --> D2[select/poll/epoll]
|
||||
D --> D3[开销最小]
|
||||
|
||||
style B fill:#ffcdd2
|
||||
style C fill:#fff3e0
|
||||
style D fill:#e8f5e9
|
||||
```
|
||||
|
||||
**对比**:
|
||||
| 模型 | 优点 | 缺点 | 适用场景 |
|
||||
|------|------|------|----------|
|
||||
| 多进程 | 隔离性好 | 开销大 | 连接数少 |
|
||||
| 多线程 | 开销较小 | 需要同步 | 连接数中等 |
|
||||
| I/O 多路复用 | 开销最小 | 编程复杂 | 连接数多 |
|
||||
|
||||
### 2. 多进程模型
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 主进程
|
||||
participant 子进程1
|
||||
participant 子进程2
|
||||
participant 客户端
|
||||
|
||||
主进程->>主进程: accept() 等待连接
|
||||
客户端->>主进程: 连接请求
|
||||
主进程->>子进程1: fork() 创建子进程
|
||||
子进程1->>客户端: 处理请求
|
||||
主进程->>主进程: 继续 accept()
|
||||
|
||||
客户端->>主进程: 新连接请求
|
||||
主进程->>子进程2: fork() 创建子进程
|
||||
子进程2->>客户端: 处理请求
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 每个连接一个独立进程
|
||||
- 进程间完全隔离
|
||||
- 进程创建和销毁开销大
|
||||
|
||||
### 3. 多线程模型
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 主线程
|
||||
participant 工作线程1
|
||||
participant 工作线程2
|
||||
participant 客户端
|
||||
|
||||
主线程->>主线程: accept() 等待连接
|
||||
客户端->>主线程: 连接请求
|
||||
主线程->>工作线程1: pthread_create()
|
||||
工作线程1->>客户端: 处理请求
|
||||
主线程->>主线程: 继续 accept()
|
||||
|
||||
客户端->>主线程: 新连接请求
|
||||
主线程->>工作线程2: pthread_create()
|
||||
工作线程2->>客户端: 处理请求
|
||||
```
|
||||
|
||||
**特点**:
|
||||
- 每个连接一个线程
|
||||
- 线程共享进程资源
|
||||
- 需要注意线程安全
|
||||
|
||||
### 4. I/O 多路复用(select)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[主循环] --> B[select 监听所有 fd]
|
||||
B --> C{哪个 fd 就绪?}
|
||||
C -->|监听套接字| D[accept 新连接]
|
||||
C -->|客户端套接字| E[处理请求]
|
||||
D --> A
|
||||
E --> A
|
||||
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**select 的工作原理**:
|
||||
1. 将所有需要监听的文件描述符放入集合
|
||||
2. 调用 `select()` 等待任意一个就绪
|
||||
3. 遍历集合,处理就绪的描述符
|
||||
4. 重复步骤 1
|
||||
|
||||
**fd_set 操作**:
|
||||
```c
|
||||
fd_set read_set;
|
||||
FD_ZERO(&read_set); // 清空集合
|
||||
FD_SET(fd1, &read_set); // 添加 fd1
|
||||
FD_SET(fd2, &read_set); // 添加 fd2
|
||||
select(maxfd+1, &read_set, NULL, NULL, NULL); // 等待
|
||||
if (FD_ISSET(fd1, &read_set)) // 检查 fd1 是否就绪
|
||||
// 处理 fd1
|
||||
```
|
||||
|
||||
### 5. 线程池模型
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[主线程] -->|accept| B[任务队列]
|
||||
B --> C[工作线程1]
|
||||
B --> D[工作线程2]
|
||||
B --> E[工作线程3]
|
||||
C -->|处理完毕| B
|
||||
D -->|处理完毕| B
|
||||
E -->|处理完毕| B
|
||||
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**优势**:
|
||||
- 避免频繁创建销毁线程
|
||||
- 控制并发数量
|
||||
- 提高资源利用率
|
||||
|
||||
---
|
||||
|
||||
## 💻 动手实践
|
||||
|
||||
### 示例1:多线程并发服务器
|
||||
|
||||
```c
|
||||
// togglest.c - 多线程并发服务器
|
||||
#include "wrapper.h"
|
||||
|
||||
void toggle(int conn_sock);
|
||||
void *serve_client(void *vargp);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int listen_sock, conn_sock, port, *conn_sock_p;
|
||||
struct sockaddr_in clientaddr;
|
||||
struct hostent *hp;
|
||||
char *haddrp;
|
||||
socklen_t clientlen = sizeof(struct sockaddr_in);
|
||||
pthread_t tid;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s <port>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
port = atoi(argv[1]);
|
||||
|
||||
listen_sock = open_listen_sock(port);
|
||||
|
||||
while (1) {
|
||||
conn_sock_p = malloc(sizeof(int));
|
||||
*conn_sock_p = accept(listen_sock, (SA *)&clientaddr, &clientlen);
|
||||
|
||||
// 获取客户端信息
|
||||
hp = Gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr,
|
||||
sizeof(clientaddr.sin_addr.s_addr), AF_INET);
|
||||
haddrp = inet_ntoa(clientaddr.sin_addr);
|
||||
printf("server connected to %s (%s)\n", hp->h_name, haddrp);
|
||||
|
||||
// 创建新线程处理客户端
|
||||
pthread_create(&tid, NULL, serve_client, conn_sock_p);
|
||||
}
|
||||
}
|
||||
|
||||
void *serve_client(void *vargp) {
|
||||
int conn_sock = *((int *)vargp);
|
||||
pthread_detach(pthread_self()); // 分离线程
|
||||
free(vargp);
|
||||
toggle(conn_sock);
|
||||
close(conn_sock);
|
||||
return NULL;
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o togglest togglest.c -L. -lwrapper -lpthread
|
||||
|
||||
# 终端1:启动服务器
|
||||
./togglest 8080
|
||||
|
||||
# 终端2-4:启动多个客户端
|
||||
./togglec localhost 8080
|
||||
```
|
||||
|
||||
### 示例2:I/O 多路复用服务器
|
||||
|
||||
```c
|
||||
// toggless1.c - select 多路复用服务器
|
||||
#include "wrapper.h"
|
||||
|
||||
void toggle(int conn_sock);
|
||||
void read_input(void);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int listen_sock, conn_sock, port;
|
||||
socklen_t clientlen = sizeof(struct sockaddr_in);
|
||||
struct sockaddr_in clientaddr;
|
||||
fd_set read_set, ready_set;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s <port>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
port = atoi(argv[1]);
|
||||
listen_sock = open_listen_sock(port);
|
||||
|
||||
// 初始化 fd_set
|
||||
FD_ZERO(&read_set);
|
||||
FD_SET(STDIN_FILENO, &read_set); // 监听标准输入
|
||||
FD_SET(listen_sock, &read_set); // 监听套接字
|
||||
|
||||
while (1) {
|
||||
ready_set = read_set;
|
||||
select(listen_sock + 1, &ready_set, NULL, NULL, NULL);
|
||||
|
||||
// 检查标准输入
|
||||
if (FD_ISSET(STDIN_FILENO, &ready_set))
|
||||
read_input();
|
||||
|
||||
// 检查新连接
|
||||
if (FD_ISSET(listen_sock, &ready_set)) {
|
||||
conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen);
|
||||
toggle(conn_sock);
|
||||
close(conn_sock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void read_input(void) {
|
||||
char buf[MAXLINE];
|
||||
if (!fgets(buf, MAXLINE, stdin))
|
||||
exit(0);
|
||||
printf("%s", buf);
|
||||
}
|
||||
```
|
||||
|
||||
### 示例3:连接池服务器
|
||||
|
||||
```c
|
||||
// toggless2.c - 连接池服务器
|
||||
#include "wrapper.h"
|
||||
|
||||
typedef struct {
|
||||
int maxfd;
|
||||
fd_set read_set;
|
||||
fd_set ready_set;
|
||||
int nready;
|
||||
int maxi;
|
||||
int client_sock[FD_SETSIZE];
|
||||
} sock_pool;
|
||||
|
||||
void init_sock_pool(int listen_sock, sock_pool *pool);
|
||||
void add_sock(int conn_sock, sock_pool *pool);
|
||||
void serve_clients(sock_pool *pool);
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int listen_sock, conn_sock, port;
|
||||
socklen_t clientlen = sizeof(struct sockaddr_in);
|
||||
struct sockaddr_in clientaddr;
|
||||
static sock_pool pool;
|
||||
|
||||
if (argc != 2) {
|
||||
fprintf(stderr, "usage: %s <port>\n", argv[0]);
|
||||
exit(1);
|
||||
}
|
||||
port = atoi(argv[1]);
|
||||
|
||||
listen_sock = open_listen_sock(port);
|
||||
init_sock_pool(listen_sock, &pool);
|
||||
|
||||
while (1) {
|
||||
pool.ready_set = pool.read_set;
|
||||
pool.nready = select(pool.maxfd + 1, &pool.ready_set, NULL, NULL, NULL);
|
||||
|
||||
// 新连接
|
||||
if (FD_ISSET(listen_sock, &pool.ready_set)) {
|
||||
conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen);
|
||||
add_sock(conn_sock, &pool);
|
||||
}
|
||||
|
||||
// 处理客户端请求
|
||||
serve_clients(&pool);
|
||||
}
|
||||
}
|
||||
|
||||
void init_sock_pool(int listen_sock, sock_pool *p) {
|
||||
int i;
|
||||
p->maxi = -1;
|
||||
for (i = 0; i < FD_SETSIZE; i++)
|
||||
p->client_sock[i] = -1;
|
||||
p->maxfd = listen_sock;
|
||||
FD_ZERO(&p->read_set);
|
||||
FD_SET(listen_sock, &p->read_set);
|
||||
}
|
||||
|
||||
void add_sock(int conn_sock, sock_pool *p) {
|
||||
int i;
|
||||
p->nready--;
|
||||
for (i = 0; i < FD_SETSIZE; i++)
|
||||
if (p->client_sock[i] < 0) {
|
||||
p->client_sock[i] = conn_sock;
|
||||
FD_SET(conn_sock, &p->read_set);
|
||||
if (conn_sock > p->maxfd)
|
||||
p->maxfd = conn_sock;
|
||||
if (i > p->maxi)
|
||||
p->maxi = i;
|
||||
break;
|
||||
}
|
||||
if (i == FD_SETSIZE)
|
||||
perror("add_sock error: Too many clients");
|
||||
}
|
||||
|
||||
void serve_clients(sock_pool *p) {
|
||||
int i, conn_sock, n;
|
||||
char buf[MAXLINE];
|
||||
|
||||
for (i = 0; (i <= p->maxi) && (p->nready > 0); i++) {
|
||||
conn_sock = p->client_sock[i];
|
||||
if ((conn_sock > 0) && (FD_ISSET(conn_sock, &p->ready_set))) {
|
||||
p->nready--;
|
||||
if ((n = recv(conn_sock, buf, MAXLINE, 0)) != 0) {
|
||||
printf("Server received %d bytes on fd %d\n", n, conn_sock);
|
||||
send(conn_sock, buf, n, 0);
|
||||
} else {
|
||||
close(conn_sock);
|
||||
FD_CLR(conn_sock, &p->read_set);
|
||||
p->client_sock[i] = -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 知识关联
|
||||
- 多进程模型在 [[06_进程控制]] 中有详细讲解
|
||||
- 多线程模型在 [[07_多线程编程]] 中有详细讲解
|
||||
- I/O 多路复用在 [[17_IO系统]] 中有更深入的讨论
|
||||
|
||||
---
|
||||
|
||||
## 📝 思考题
|
||||
|
||||
1. **为什么 select 有 FD_SETSIZE 限制?** 如何突破这个限制?
|
||||
2. **多进程 vs 多线程**:在什么情况下多进程比多线程更合适?
|
||||
3. **epoll 的优势**:为什么 Linux 推荐使用 epoll 而不是 select?
|
||||
|
||||
---
|
||||
|
||||
## 📚 扩展阅读
|
||||
- 《UNIX网络编程》第1卷:第6章、第16章
|
||||
- [epoll 详解](https://man7.org/linux/man-pages/man7/epoll.7.html)
|
||||
- [高性能网络编程](https://www.zhihu.com/question/28594409)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,376 +1,376 @@
|
||||
# 第12讲:死锁
|
||||
|
||||
> 🎯 **本节目标**:理解死锁的概念,掌握死锁的预防、避免和检测方法
|
||||
|
||||
## 📋 前置知识
|
||||
- [[07_多线程编程]] — 互斥锁和信号量
|
||||
- [[11_处理机调度]] — 调度的基本概念
|
||||
|
||||
---
|
||||
|
||||
## 🤔 为什么需要这个?
|
||||
|
||||
想象这样一个场景:
|
||||
- 线程 A 持有锁 1,等待锁 2
|
||||
- 线程 B 持有锁 2,等待锁 1
|
||||
- 两个线程互相等待,永远无法继续
|
||||
|
||||
这就是**死锁**——多个进程互相等待对方释放资源,导致所有进程都无法继续执行。
|
||||
|
||||
**生活比喻**:
|
||||
- **死锁** = 两个人在狭窄的走廊相遇,谁都不肯让路,结果谁也过不去
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 1. 死锁的四个必要条件
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[死锁四个条件] --> B[互斥条件]
|
||||
A --> C[持有并等待]
|
||||
A --> D[不可抢占]
|
||||
A --> E[循环等待]
|
||||
|
||||
B --> B1[资源一次只能被一个进程使用]
|
||||
C --> C1[进程持有资源的同时请求新资源]
|
||||
D --> D1[资源不能被强制剥夺]
|
||||
E --> E1[存在进程的循环等待链]
|
||||
|
||||
style A fill:#ffcdd2
|
||||
```
|
||||
|
||||
**必须同时满足这四个条件才会发生死锁**。
|
||||
|
||||
### 2. 资源分配图
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph 进程
|
||||
P1[P1]
|
||||
P2[P2]
|
||||
end
|
||||
subgraph 资源
|
||||
R1[R1]
|
||||
R2[R2]
|
||||
end
|
||||
|
||||
R1 -->|分配| P1
|
||||
R2 -->|分配| P2
|
||||
P1 -->|请求| R2
|
||||
P2 -->|请求| R1
|
||||
|
||||
style P1 fill:#e1f5fe
|
||||
style P2 fill:#e1f5fe
|
||||
style R1 fill:#e8f5e9
|
||||
style R2 fill:#e8f5e9
|
||||
```
|
||||
|
||||
**图例**:
|
||||
- 方框表示资源,圆圈表示进程
|
||||
- 资源→进程:已分配
|
||||
- 进程→资源:请求中
|
||||
|
||||
### 3. 死锁的处理策略
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[死锁处理策略] --> B[死锁预防]
|
||||
A --> C[死锁避免]
|
||||
A --> D[死锁检测]
|
||||
A --> E[死锁恢复]
|
||||
|
||||
B --> B1[破坏四个条件之一]
|
||||
C --> C1[银行家算法]
|
||||
D --> D1[资源分配图]
|
||||
E --> E1[终止进程]
|
||||
|
||||
style B fill:#e8f5e9
|
||||
style C fill:#e1f5fe
|
||||
style D fill:#fff3e0
|
||||
style E fill:#ffcdd2
|
||||
```
|
||||
|
||||
### 4. 死锁预防
|
||||
|
||||
破坏死锁的四个必要条件之一:
|
||||
|
||||
| 条件 | 破坏方法 | 代价 |
|
||||
|------|----------|------|
|
||||
| 互斥 | 使用可共享资源 | 不总是可行 |
|
||||
| 持有并等待 | 一次性申请所有资源 | 资源浪费 |
|
||||
| 不可抢占 | 允许抢占资源 | 实现复杂 |
|
||||
| 循环等待 | 按顺序申请资源 | 限制灵活性 |
|
||||
|
||||
**资源有序分配法**:
|
||||
```c
|
||||
// 规定所有进程必须按编号顺序申请资源
|
||||
// 例如:先申请锁1,再申请锁2
|
||||
|
||||
pthread_mutex_lock(&mutex1); // 正确
|
||||
pthread_mutex_lock(&mutex2);
|
||||
|
||||
// 而不是
|
||||
pthread_mutex_lock(&mutex2); // 可能导致死锁
|
||||
pthread_mutex_lock(&mutex1);
|
||||
```
|
||||
|
||||
### 5. 银行家算法
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[银行家算法] --> B[检查请求是否安全]
|
||||
B --> C{安全?}
|
||||
C -->|是| D[分配资源]
|
||||
C -->|否| E[等待]
|
||||
D --> F[更新数据结构]
|
||||
E --> G[阻塞进程]
|
||||
|
||||
style C fill:#fff3e0
|
||||
style D fill:#e8f5e9
|
||||
style E fill:#ffcdd2
|
||||
```
|
||||
|
||||
**安全状态**:存在一个安全序列,使得所有进程都能顺利完成
|
||||
|
||||
**数据结构**:
|
||||
- `Available[]`:可用资源向量
|
||||
- `Max[][]`:最大需求矩阵
|
||||
- `Allocation[][]`:已分配矩阵
|
||||
- `Need[][]`:还需要的资源矩阵
|
||||
|
||||
**算法步骤**:
|
||||
1. 检查请求是否超过需要
|
||||
2. 检查请求是否超过可用资源
|
||||
3. 尝试分配,检查是否安全
|
||||
4. 如果安全,正式分配;否则等待
|
||||
|
||||
### 6. 死锁检测
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[构建资源分配图] --> B[寻找环路]
|
||||
B --> C{有环路?}
|
||||
C -->|是| D[可能存在死锁]
|
||||
C -->|否| E[无死锁]
|
||||
D --> F[进一步分析]
|
||||
|
||||
style C fill:#fff3e0
|
||||
style D fill:#ffcdd2
|
||||
style E fill:#e8f5e9
|
||||
```
|
||||
|
||||
**检测算法**:
|
||||
1. 构建资源分配图
|
||||
2. 使用深度优先搜索寻找环路
|
||||
3. 如果存在环路,可能存在死锁
|
||||
|
||||
### 7. 死锁恢复
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[检测到死锁] --> B[选择终止进程]
|
||||
B --> C[回滚操作]
|
||||
C --> D[释放资源]
|
||||
D --> E[唤醒等待进程]
|
||||
|
||||
style A fill:#ffcdd2
|
||||
style E fill:#e8f5e9
|
||||
```
|
||||
|
||||
**恢复方法**:
|
||||
- **终止所有死锁进程**:简单但代价大
|
||||
- **逐个终止进程**:直到死锁解除
|
||||
- **资源抢占**:强制剥夺资源
|
||||
|
||||
---
|
||||
|
||||
## 💻 动手实践
|
||||
|
||||
### 示例1:死锁演示
|
||||
|
||||
```c
|
||||
// deadlock_demo.c - 死锁演示
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <pthread.h>
|
||||
|
||||
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
|
||||
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
void *thread1(void *arg) {
|
||||
pthread_mutex_lock(&mutex1); // 获取锁1
|
||||
printf("Thread 1: 持有锁1,等待锁2...\n");
|
||||
sleep(1); // 等待,让线程2获取锁2
|
||||
pthread_mutex_lock(&mutex2); // 等待锁2(死锁!)
|
||||
printf("Thread 1: 获取到锁2\n");
|
||||
|
||||
pthread_mutex_unlock(&mutex2);
|
||||
pthread_mutex_unlock(&mutex1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *thread2(void *arg) {
|
||||
pthread_mutex_lock(&mutex2); // 获取锁2
|
||||
printf("Thread 2: 持有锁2,等待锁1...\n");
|
||||
sleep(1); // 等待,让线程1获取锁1
|
||||
pthread_mutex_lock(&mutex1); // 等待锁1(死锁!)
|
||||
printf("Thread 2: 获取到锁1\n");
|
||||
|
||||
pthread_mutex_unlock(&mutex1);
|
||||
pthread_mutex_unlock(&mutex2);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main() {
|
||||
pthread_t tid1, tid2;
|
||||
|
||||
pthread_create(&tid1, NULL, thread1, NULL);
|
||||
pthread_create(&tid2, NULL, thread2, NULL);
|
||||
|
||||
pthread_join(tid1, NULL);
|
||||
pthread_join(tid2, NULL);
|
||||
|
||||
printf("程序正常结束(如果这里能打印说明没有死锁)\n");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o deadlock deadlock_demo.c -lpthread
|
||||
./deadlock
|
||||
```
|
||||
|
||||
**预期结果**:程序会卡住,无法正常结束(死锁)
|
||||
|
||||
### 示例2:避免死锁
|
||||
|
||||
```c
|
||||
// no_deadlock.c - 避免死锁(按顺序获取锁)
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <pthread.h>
|
||||
|
||||
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
|
||||
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
void *thread1(void *arg) {
|
||||
pthread_mutex_lock(&mutex1); // 先获取锁1
|
||||
printf("Thread 1: 持有锁1\n");
|
||||
sleep(1);
|
||||
pthread_mutex_lock(&mutex2); // 再获取锁2
|
||||
printf("Thread 1: 持有锁1和锁2\n");
|
||||
|
||||
pthread_mutex_unlock(&mutex2);
|
||||
pthread_mutex_unlock(&mutex1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *thread2(void *arg) {
|
||||
pthread_mutex_lock(&mutex1); // 也先获取锁1(顺序一致)
|
||||
printf("Thread 2: 持有锁1\n");
|
||||
sleep(1);
|
||||
pthread_mutex_lock(&mutex2); // 再获取锁2
|
||||
printf("Thread 2: 持有锁1和锁2\n");
|
||||
|
||||
pthread_mutex_unlock(&mutex2);
|
||||
pthread_mutex_unlock(&mutex1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main() {
|
||||
pthread_t tid1, tid2;
|
||||
|
||||
pthread_create(&tid1, NULL, thread1, NULL);
|
||||
pthread_create(&tid2, NULL, thread2, NULL);
|
||||
|
||||
pthread_join(tid1, NULL);
|
||||
pthread_join(tid2, NULL);
|
||||
|
||||
printf("程序正常结束\n");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 示例3:银行家算法模拟
|
||||
|
||||
```python
|
||||
# banker.py - 银行家算法模拟
|
||||
def is_safe(available, max_need, allocation):
|
||||
"""检查系统是否处于安全状态"""
|
||||
n = len(allocation) # 进程数
|
||||
m = len(available) # 资源类型数
|
||||
|
||||
# 计算 Need 矩阵
|
||||
need = [[max_need[i][j] - allocation[i][j] for j in range(m)] for i in range(n)]
|
||||
|
||||
# 初始化工作向量和完成标志
|
||||
work = available.copy()
|
||||
finish = [False] * n
|
||||
safe_seq = []
|
||||
|
||||
while len(safe_seq) < n:
|
||||
found = False
|
||||
for i in range(n):
|
||||
if not finish[i]:
|
||||
# 检查是否可以分配
|
||||
if all(need[i][j] <= work[j] for j in range(m)):
|
||||
# 模拟分配
|
||||
for j in range(m):
|
||||
work[j] += allocation[i][j]
|
||||
finish[i] = True
|
||||
safe_seq.append(i)
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
return False, [] # 不安全
|
||||
|
||||
return True, safe_seq
|
||||
|
||||
# 测试数据
|
||||
available = [3, 3, 2]
|
||||
max_need = [
|
||||
[7, 5, 3],
|
||||
[3, 2, 2],
|
||||
[9, 0, 2],
|
||||
[2, 2, 2],
|
||||
[4, 3, 3]
|
||||
]
|
||||
allocation = [
|
||||
[0, 1, 0],
|
||||
[2, 0, 0],
|
||||
[3, 0, 2],
|
||||
[2, 1, 1],
|
||||
[0, 0, 2]
|
||||
]
|
||||
|
||||
safe, seq = is_safe(available, max_need, allocation)
|
||||
if safe:
|
||||
print(f"系统安全,安全序列: {seq}")
|
||||
else:
|
||||
print("系统不安全")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 知识关联
|
||||
- 互斥锁在 [[07_多线程编程]] 中有详细讲解
|
||||
- 资源分配在 [[05_磁盘空间管理]] 中有类似概念
|
||||
- 银行家算法在 [[14_分页存储管理]] 中的页面置换有类似思想
|
||||
|
||||
---
|
||||
|
||||
## 📝 思考题
|
||||
|
||||
1. **为什么需要同时满足四个条件?** 如果只有三个条件满足会怎样?
|
||||
2. **银行家算法的局限性**:为什么它在实际系统中很少使用?
|
||||
3. **鸵鸟策略**:为什么有些系统选择忽略死锁问题?
|
||||
|
||||
---
|
||||
|
||||
## 📚 扩展阅读
|
||||
- 《操作系统概念》第7章:死锁
|
||||
- 《现代操作系统》第6章:死锁
|
||||
- [死锁检测算法](https://www.geeksforgeeks.org/deadlock-detection-algorithm/)
|
||||
# 第12讲:死锁
|
||||
|
||||
> 🎯 **本节目标**:理解死锁的概念,掌握死锁的预防、避免和检测方法
|
||||
|
||||
## 📋 前置知识
|
||||
- [[07_多线程编程]] — 互斥锁和信号量
|
||||
- [[11_处理机调度]] — 调度的基本概念
|
||||
|
||||
---
|
||||
|
||||
## 🤔 为什么需要这个?
|
||||
|
||||
想象这样一个场景:
|
||||
- 线程 A 持有锁 1,等待锁 2
|
||||
- 线程 B 持有锁 2,等待锁 1
|
||||
- 两个线程互相等待,永远无法继续
|
||||
|
||||
这就是**死锁**——多个进程互相等待对方释放资源,导致所有进程都无法继续执行。
|
||||
|
||||
**生活比喻**:
|
||||
- **死锁** = 两个人在狭窄的走廊相遇,谁都不肯让路,结果谁也过不去
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 1. 死锁的四个必要条件
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[死锁四个条件] --> B[互斥条件]
|
||||
A --> C[持有并等待]
|
||||
A --> D[不可抢占]
|
||||
A --> E[循环等待]
|
||||
|
||||
B --> B1[资源一次只能被一个进程使用]
|
||||
C --> C1[进程持有资源的同时请求新资源]
|
||||
D --> D1[资源不能被强制剥夺]
|
||||
E --> E1[存在进程的循环等待链]
|
||||
|
||||
style A fill:#ffcdd2
|
||||
```
|
||||
|
||||
**必须同时满足这四个条件才会发生死锁**。
|
||||
|
||||
### 2. 资源分配图
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
subgraph 进程
|
||||
P1[P1]
|
||||
P2[P2]
|
||||
end
|
||||
subgraph 资源
|
||||
R1[R1]
|
||||
R2[R2]
|
||||
end
|
||||
|
||||
R1 -->|分配| P1
|
||||
R2 -->|分配| P2
|
||||
P1 -->|请求| R2
|
||||
P2 -->|请求| R1
|
||||
|
||||
style P1 fill:#e1f5fe
|
||||
style P2 fill:#e1f5fe
|
||||
style R1 fill:#e8f5e9
|
||||
style R2 fill:#e8f5e9
|
||||
```
|
||||
|
||||
**图例**:
|
||||
- 方框表示资源,圆圈表示进程
|
||||
- 资源→进程:已分配
|
||||
- 进程→资源:请求中
|
||||
|
||||
### 3. 死锁的处理策略
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[死锁处理策略] --> B[死锁预防]
|
||||
A --> C[死锁避免]
|
||||
A --> D[死锁检测]
|
||||
A --> E[死锁恢复]
|
||||
|
||||
B --> B1[破坏四个条件之一]
|
||||
C --> C1[银行家算法]
|
||||
D --> D1[资源分配图]
|
||||
E --> E1[终止进程]
|
||||
|
||||
style B fill:#e8f5e9
|
||||
style C fill:#e1f5fe
|
||||
style D fill:#fff3e0
|
||||
style E fill:#ffcdd2
|
||||
```
|
||||
|
||||
### 4. 死锁预防
|
||||
|
||||
破坏死锁的四个必要条件之一:
|
||||
|
||||
| 条件 | 破坏方法 | 代价 |
|
||||
|------|----------|------|
|
||||
| 互斥 | 使用可共享资源 | 不总是可行 |
|
||||
| 持有并等待 | 一次性申请所有资源 | 资源浪费 |
|
||||
| 不可抢占 | 允许抢占资源 | 实现复杂 |
|
||||
| 循环等待 | 按顺序申请资源 | 限制灵活性 |
|
||||
|
||||
**资源有序分配法**:
|
||||
```c
|
||||
// 规定所有进程必须按编号顺序申请资源
|
||||
// 例如:先申请锁1,再申请锁2
|
||||
|
||||
pthread_mutex_lock(&mutex1); // 正确
|
||||
pthread_mutex_lock(&mutex2);
|
||||
|
||||
// 而不是
|
||||
pthread_mutex_lock(&mutex2); // 可能导致死锁
|
||||
pthread_mutex_lock(&mutex1);
|
||||
```
|
||||
|
||||
### 5. 银行家算法
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[银行家算法] --> B[检查请求是否安全]
|
||||
B --> C{安全?}
|
||||
C -->|是| D[分配资源]
|
||||
C -->|否| E[等待]
|
||||
D --> F[更新数据结构]
|
||||
E --> G[阻塞进程]
|
||||
|
||||
style C fill:#fff3e0
|
||||
style D fill:#e8f5e9
|
||||
style E fill:#ffcdd2
|
||||
```
|
||||
|
||||
**安全状态**:存在一个安全序列,使得所有进程都能顺利完成
|
||||
|
||||
**数据结构**:
|
||||
- `Available[]`:可用资源向量
|
||||
- `Max[][]`:最大需求矩阵
|
||||
- `Allocation[][]`:已分配矩阵
|
||||
- `Need[][]`:还需要的资源矩阵
|
||||
|
||||
**算法步骤**:
|
||||
1. 检查请求是否超过需要
|
||||
2. 检查请求是否超过可用资源
|
||||
3. 尝试分配,检查是否安全
|
||||
4. 如果安全,正式分配;否则等待
|
||||
|
||||
### 6. 死锁检测
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[构建资源分配图] --> B[寻找环路]
|
||||
B --> C{有环路?}
|
||||
C -->|是| D[可能存在死锁]
|
||||
C -->|否| E[无死锁]
|
||||
D --> F[进一步分析]
|
||||
|
||||
style C fill:#fff3e0
|
||||
style D fill:#ffcdd2
|
||||
style E fill:#e8f5e9
|
||||
```
|
||||
|
||||
**检测算法**:
|
||||
1. 构建资源分配图
|
||||
2. 使用深度优先搜索寻找环路
|
||||
3. 如果存在环路,可能存在死锁
|
||||
|
||||
### 7. 死锁恢复
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[检测到死锁] --> B[选择终止进程]
|
||||
B --> C[回滚操作]
|
||||
C --> D[释放资源]
|
||||
D --> E[唤醒等待进程]
|
||||
|
||||
style A fill:#ffcdd2
|
||||
style E fill:#e8f5e9
|
||||
```
|
||||
|
||||
**恢复方法**:
|
||||
- **终止所有死锁进程**:简单但代价大
|
||||
- **逐个终止进程**:直到死锁解除
|
||||
- **资源抢占**:强制剥夺资源
|
||||
|
||||
---
|
||||
|
||||
## 💻 动手实践
|
||||
|
||||
### 示例1:死锁演示
|
||||
|
||||
```c
|
||||
// deadlock_demo.c - 死锁演示
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <pthread.h>
|
||||
|
||||
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
|
||||
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
void *thread1(void *arg) {
|
||||
pthread_mutex_lock(&mutex1); // 获取锁1
|
||||
printf("Thread 1: 持有锁1,等待锁2...\n");
|
||||
sleep(1); // 等待,让线程2获取锁2
|
||||
pthread_mutex_lock(&mutex2); // 等待锁2(死锁!)
|
||||
printf("Thread 1: 获取到锁2\n");
|
||||
|
||||
pthread_mutex_unlock(&mutex2);
|
||||
pthread_mutex_unlock(&mutex1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *thread2(void *arg) {
|
||||
pthread_mutex_lock(&mutex2); // 获取锁2
|
||||
printf("Thread 2: 持有锁2,等待锁1...\n");
|
||||
sleep(1); // 等待,让线程1获取锁1
|
||||
pthread_mutex_lock(&mutex1); // 等待锁1(死锁!)
|
||||
printf("Thread 2: 获取到锁1\n");
|
||||
|
||||
pthread_mutex_unlock(&mutex1);
|
||||
pthread_mutex_unlock(&mutex2);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main() {
|
||||
pthread_t tid1, tid2;
|
||||
|
||||
pthread_create(&tid1, NULL, thread1, NULL);
|
||||
pthread_create(&tid2, NULL, thread2, NULL);
|
||||
|
||||
pthread_join(tid1, NULL);
|
||||
pthread_join(tid2, NULL);
|
||||
|
||||
printf("程序正常结束(如果这里能打印说明没有死锁)\n");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o deadlock deadlock_demo.c -lpthread
|
||||
./deadlock
|
||||
```
|
||||
|
||||
**预期结果**:程序会卡住,无法正常结束(死锁)
|
||||
|
||||
### 示例2:避免死锁
|
||||
|
||||
```c
|
||||
// no_deadlock.c - 避免死锁(按顺序获取锁)
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <pthread.h>
|
||||
|
||||
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;
|
||||
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;
|
||||
|
||||
void *thread1(void *arg) {
|
||||
pthread_mutex_lock(&mutex1); // 先获取锁1
|
||||
printf("Thread 1: 持有锁1\n");
|
||||
sleep(1);
|
||||
pthread_mutex_lock(&mutex2); // 再获取锁2
|
||||
printf("Thread 1: 持有锁1和锁2\n");
|
||||
|
||||
pthread_mutex_unlock(&mutex2);
|
||||
pthread_mutex_unlock(&mutex1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
void *thread2(void *arg) {
|
||||
pthread_mutex_lock(&mutex1); // 也先获取锁1(顺序一致)
|
||||
printf("Thread 2: 持有锁1\n");
|
||||
sleep(1);
|
||||
pthread_mutex_lock(&mutex2); // 再获取锁2
|
||||
printf("Thread 2: 持有锁1和锁2\n");
|
||||
|
||||
pthread_mutex_unlock(&mutex2);
|
||||
pthread_mutex_unlock(&mutex1);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
int main() {
|
||||
pthread_t tid1, tid2;
|
||||
|
||||
pthread_create(&tid1, NULL, thread1, NULL);
|
||||
pthread_create(&tid2, NULL, thread2, NULL);
|
||||
|
||||
pthread_join(tid1, NULL);
|
||||
pthread_join(tid2, NULL);
|
||||
|
||||
printf("程序正常结束\n");
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 示例3:银行家算法模拟
|
||||
|
||||
```python
|
||||
# banker.py - 银行家算法模拟
|
||||
def is_safe(available, max_need, allocation):
|
||||
"""检查系统是否处于安全状态"""
|
||||
n = len(allocation) # 进程数
|
||||
m = len(available) # 资源类型数
|
||||
|
||||
# 计算 Need 矩阵
|
||||
need = [[max_need[i][j] - allocation[i][j] for j in range(m)] for i in range(n)]
|
||||
|
||||
# 初始化工作向量和完成标志
|
||||
work = available.copy()
|
||||
finish = [False] * n
|
||||
safe_seq = []
|
||||
|
||||
while len(safe_seq) < n:
|
||||
found = False
|
||||
for i in range(n):
|
||||
if not finish[i]:
|
||||
# 检查是否可以分配
|
||||
if all(need[i][j] <= work[j] for j in range(m)):
|
||||
# 模拟分配
|
||||
for j in range(m):
|
||||
work[j] += allocation[i][j]
|
||||
finish[i] = True
|
||||
safe_seq.append(i)
|
||||
found = True
|
||||
|
||||
if not found:
|
||||
return False, [] # 不安全
|
||||
|
||||
return True, safe_seq
|
||||
|
||||
# 测试数据
|
||||
available = [3, 3, 2]
|
||||
max_need = [
|
||||
[7, 5, 3],
|
||||
[3, 2, 2],
|
||||
[9, 0, 2],
|
||||
[2, 2, 2],
|
||||
[4, 3, 3]
|
||||
]
|
||||
allocation = [
|
||||
[0, 1, 0],
|
||||
[2, 0, 0],
|
||||
[3, 0, 2],
|
||||
[2, 1, 1],
|
||||
[0, 0, 2]
|
||||
]
|
||||
|
||||
safe, seq = is_safe(available, max_need, allocation)
|
||||
if safe:
|
||||
print(f"系统安全,安全序列: {seq}")
|
||||
else:
|
||||
print("系统不安全")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 知识关联
|
||||
- 互斥锁在 [[07_多线程编程]] 中有详细讲解
|
||||
- 资源分配在 [[05_磁盘空间管理]] 中有类似概念
|
||||
- 银行家算法在 [[14_分页存储管理]] 中的页面置换有类似思想
|
||||
|
||||
---
|
||||
|
||||
## 📝 思考题
|
||||
|
||||
1. **为什么需要同时满足四个条件?** 如果只有三个条件满足会怎样?
|
||||
2. **银行家算法的局限性**:为什么它在实际系统中很少使用?
|
||||
3. **鸵鸟策略**:为什么有些系统选择忽略死锁问题?
|
||||
|
||||
---
|
||||
|
||||
## 📚 扩展阅读
|
||||
- 《操作系统概念》第7章:死锁
|
||||
- 《现代操作系统》第6章:死锁
|
||||
- [死锁检测算法](https://www.geeksforgeeks.org/deadlock-detection-algorithm/)
|
||||
|
||||
@@ -1,303 +1,303 @@
|
||||
# 13. 存储管理基础
|
||||
|
||||
> **课程**: 操作系统 - 存储器管理
|
||||
> **核心内容**: 存储器层次结构、存储管理功能、程序编译链接与装入、地址空间
|
||||
|
||||
---
|
||||
|
||||
## 一、存储器层次结构
|
||||
|
||||
存储器按照速度和容量形成层次结构,越靠近CPU速度越快但容量越小、价格越高。
|
||||
|
||||
```
|
||||
速度递增 ↑ 容量递减 ↑ 价格递增 ↑
|
||||
┌─────────────────────────┐
|
||||
│ 寄存器(Register) │ ← 最快,纳秒级,几十~几百字节
|
||||
├─────────────────────────┤
|
||||
│ 高速缓存(Cache) │ ← L1/L2/L3,几MB
|
||||
├─────────────────────────┤
|
||||
│ 主存(内存/DRAM) │ ← 几GB~几百GB,百纳秒级
|
||||
├─────────────────────────┤
|
||||
│ Flash/SSD │ ← 固态存储
|
||||
├─────────────────────────┤
|
||||
│ 磁盘缓存(Disk Cache) │
|
||||
├─────────────────────────┤
|
||||
│ 固定磁盘(HDD/SSD) │ ← 几百GB~几TB,毫秒级
|
||||
├─────────────────────────┤
|
||||
│ 可移动存储(U盘/光盘/磁带) │ ← 最慢,容量可很大
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
**设计原则**: 利用**局部性原理**,将频繁访问的数据放在高速层次,较少访问的数据放在低速大容量层次。
|
||||
|
||||
---
|
||||
|
||||
## 二、存储管理的功能
|
||||
|
||||
操作系统存储管理需要实现以下五大核心功能:
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **内存分配与回收** | 为进程分配所需内存,进程结束后回收内存 |
|
||||
| **地址转换** | 将程序中的逻辑地址转换为物理地址([[14_分页存储管理\|分页]]、[[15_段式存储管理\|分段]]) |
|
||||
| **内存共享** | 多个进程共享同一段代码(如共享库) |
|
||||
| **内存保护** | 防止进程越界访问其他进程或内核的内存区域 |
|
||||
| **内存扩充** | 通过虚拟存储技术,使程序可用空间大于实际物理内存([[16_虚拟存储器\|虚拟存储器]]) |
|
||||
|
||||
---
|
||||
|
||||
## 三、程序的编译与链接过程
|
||||
|
||||
一个C语言源文件从编写到可执行,经历以下阶段:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["hello.c<br/>源文件"] -->|预处理| B["hello.i<br/>预处理后"]
|
||||
B -->|编译| C["hello.s<br/>汇编文件"]
|
||||
C -->|汇编| D["hello.o<br/>目标文件(可重定位)"]
|
||||
D -->|链接| E["hello / a.out<br/>可执行文件"]
|
||||
E -->|装入| F["内存中运行的进程"]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style C fill:#fff3e0
|
||||
style D fill:#fce4ec
|
||||
style E fill:#e8f5e9
|
||||
style F fill:#f3e5f5
|
||||
```
|
||||
|
||||
### 各阶段说明
|
||||
|
||||
| 阶段 | 输入 | 输出 | 工具 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| **预处理** | `.c` | `.i` | cpp | 展开宏、头文件、条件编译 |
|
||||
| **编译** | `.i` | `.s` | cc1 | 翻译为汇编语言 |
|
||||
| **汇编** | `.s` | `.o` | as | 翻译为机器指令(可重定位目标文件) |
|
||||
| **链接** | `.o` | 可执行文件 | ld | 合并节段、解析符号引用、重定位 |
|
||||
| **装入** | 可执行文件 | 进程 | 加载器 | 将程序载入内存并创建进程 |
|
||||
|
||||
### 查看目标文件的节段
|
||||
|
||||
使用 `objdump -h` 可以查看可执行文件或目标文件中的节段(Section)信息:
|
||||
|
||||
```bash
|
||||
gcc -c hello.c # 编译为可重定位目标文件
|
||||
objdump -h hello.o # 查看节段头信息
|
||||
|
||||
gcc hello.c -o hello # 链接为可执行文件
|
||||
objdump -h hello # 查看可执行文件的节段
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、可执行文件结构
|
||||
|
||||
可执行文件在内存中从低地址到高地址的典型布局如下:
|
||||
|
||||
```
|
||||
高地址 ┌──────────────────┐
|
||||
│ 命令行参数 │
|
||||
│ 和环境变量 │
|
||||
├──────────────────┤
|
||||
│ 栈(Stack) │ ← 局部变量、函数调用帧,向下增长 ↓
|
||||
│ ↓ │
|
||||
│ │
|
||||
│ ↑ │
|
||||
│ 堆(Heap) │ ← malloc/new动态分配,向上增长 ↑
|
||||
├──────────────────┤
|
||||
│ .bss 段 │ ← 未初始化的全局/静态变量(不占文件空间)
|
||||
├──────────────────┤
|
||||
│ .data 段 │ ← 已初始化的全局变量和静态变量
|
||||
├──────────────────┤
|
||||
│ .rodata 段 │ ← 只读数据(如字符串常量)
|
||||
├──────────────────┤
|
||||
│ .text 段 │ ← 可执行代码(机器指令)
|
||||
低地址 └──────────────────┘
|
||||
```
|
||||
|
||||
| 段名 | 内容 | 是否可写 | 说明 |
|
||||
|------|------|---------|------|
|
||||
| `.text` | 机器指令代码 | 只读/只执行 | 程序的可执行代码 |
|
||||
| `.rodata` | 只读数据 | 只读 | 字符串常量、`const`变量 |
|
||||
| `.data` | 已初始化全局变量 | 可读写 | 有初始值的全局和静态变量 |
|
||||
| `.bss` | 未初始化全局变量 | 可读写 | 不占文件空间,装入时清零 |
|
||||
| 堆(Heap) | 动态分配内存 | 可读写 | `malloc`/`new` 分配 |
|
||||
| 栈(Stack) | 函数调用帧 | 可读写 | 局部变量、返回地址等 |
|
||||
|
||||
---
|
||||
|
||||
## 五、逻辑地址与物理地址
|
||||
|
||||
### 核心概念
|
||||
|
||||
| 术语 | 别名 | 说明 |
|
||||
|------|------|------|
|
||||
| **逻辑地址** | 虚拟地址(VA)、相对地址 | 程序中使用的地址,从0开始编址 |
|
||||
| **物理地址** | PA、绝对地址 | 内存硬件中的实际地址 |
|
||||
|
||||
> **关键**: 程序中的地址(逻辑地址)**不等于**内存中的实际地址(物理地址)。地址转换由硬件([[01_系统运行机制#四、存储器管理硬件——MMU|MMU]])和操作系统配合完成。
|
||||
|
||||
### 逻辑地址空间与物理地址空间
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph VA["逻辑地址空间 (虚拟)"]
|
||||
direction TB
|
||||
V0["地址 0"]
|
||||
V1["..."]
|
||||
V2["地址 2^v - 1"]
|
||||
end
|
||||
|
||||
subgraph PA["物理地址空间 (实际)"]
|
||||
direction TB
|
||||
P0["地址 0"]
|
||||
P1["..."]
|
||||
P2["地址 2^p - 1"]
|
||||
end
|
||||
|
||||
VA -->|"地址映射<br/>(页表/段表)"| PA
|
||||
|
||||
style VA fill:#e3f2fd,stroke:#1976d2
|
||||
style PA fill:#fff8e1,stroke:#f9a825
|
||||
```
|
||||
|
||||
- **虚拟地址空间**: 大小为 $2^v$ 字节,其中 $v$ 是虚拟地址的位数
|
||||
- **物理地址空间**: 大小为 $2^p$ 字节,其中 $p$ 是物理地址的位数
|
||||
- 通常 $v \geq p$(虚拟地址空间可以大于物理内存),这就是[[16_虚拟存储器|虚拟存储器]]的基础
|
||||
|
||||
### 引入逻辑地址的好处
|
||||
|
||||
1. **进程隔离**: 每个进程拥有独立的虚拟地址空间,互不干扰
|
||||
2. **提高内存利用率**: 可以使用[[16_虚拟存储器|虚拟存储器]]技术
|
||||
3. **内存保护**: 通过地址转换实现访问权限控制
|
||||
|
||||
---
|
||||
|
||||
## 六、内核空间与用户空间
|
||||
|
||||
操作系统将每个进程的虚拟地址空间划分为**用户空间**和**内核空间**两部分:
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
columns 1
|
||||
block:linux["Linux 进程地址空间 (4GB)"]
|
||||
columns 1
|
||||
block:kernel_linux["内核空间 (高 1GB)"]
|
||||
k1["内核代码、数据、内核栈等"]
|
||||
end
|
||||
block:user_linux["用户空间 (低 3GB)"]
|
||||
u1["栈 ↓"]
|
||||
u2["..."]
|
||||
u3["堆 ↑"]
|
||||
u4[".bss / .data / .rodata / .text"]
|
||||
end
|
||||
end
|
||||
block:win["Windows 进程地址空间 (4GB)"]
|
||||
columns 1
|
||||
block:kernel_win["内核空间 (高 2GB)"]
|
||||
k2["内核代码、驱动等"]
|
||||
end
|
||||
block:user_win["用户空间 (低 2GB)"]
|
||||
u5["用户程序空间"]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
| 操作系统 | 用户空间 | 内核空间 | 说明 |
|
||||
|---------|---------|---------|------|
|
||||
| **Linux (32位)** | 0 ~ 3GB (低3GB) | 3GB ~ 4GB (高1GB) | 通过`PAGE_OFFSET`划分 |
|
||||
| **Windows (32位)** | 0 ~ 2GB (低2GB) | 2GB ~ 4GB (高2GB) | 可通过`/3GB`启动参数调整为3:1 |
|
||||
|
||||
**重要规则**: 用户态程序**不能**直接访问内核空间的地址,否则触发保护异常。内核态代码可以访问整个地址空间。
|
||||
|
||||
---
|
||||
|
||||
## 七、MMU 与内存保护
|
||||
|
||||
**MMU (Memory Management Unit)** 是CPU中负责地址转换和内存保护的硬件单元。
|
||||
|
||||
### 地址转换流程
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CPU["CPU 发出<br/>虚拟地址(VA)"] --> MMU["MMU<br/>地址转换"]
|
||||
MMU --> PA["物理地址(PA)"]
|
||||
PA --> MEM["访问内存"]
|
||||
|
||||
style MMU fill:#ffcdd2,stroke:#c62828
|
||||
```
|
||||
|
||||
### 页表保护机制
|
||||
|
||||
MMU通过页表中的**U/S位**和CPU的**模式位**配合实现内存保护:
|
||||
|
||||
| 页表U/S位 | 含义 |
|
||||
|-----------|------|
|
||||
| **U=0 (Supervisor)** | 内核态页面,只有内核可以访问 |
|
||||
| **U=1 (User)** | 用户态页面,用户态和内核态均可访问 |
|
||||
|
||||
| CPU模式 | 可访问的页面 |
|
||||
|---------|-------------|
|
||||
| **用户模式 (User mode)** | 只能访问 U=1 的页面 |
|
||||
| **内核模式 (Kernel mode)** | 可以访问 U=0 和 U=1 的页面 |
|
||||
|
||||
当用户态程序试图访问 U=0 的内核页面时,MMU会产生**保护异常(段错误/Segmentation Fault)**,终止该进程。
|
||||
|
||||
---
|
||||
|
||||
## 八、地址转换的主要方式
|
||||
|
||||
操作系统实现地址转换有多种方式,各有特点:
|
||||
|
||||
| 方式 | 原理 | 优点 | 缺点 |
|
||||
|------|------|------|------|
|
||||
| **重定位寄存器(基址寄存器)** | PA = VA + 基址值 | 简单 | 程序必须连续存放 |
|
||||
| **静态重定位** | 装入时一次性修改所有地址 | 无需硬件支持 | 装入后不能移动 |
|
||||
| **动态重定位** | 执行时通过MMU实时转换 | 灵活,支持移动 | 需要硬件支持 |
|
||||
| **[[14_分页存储管理\|分页]]** | 按页划分,通过页表映射 | 消除外部碎片 | 有内部碎片、页表开销 |
|
||||
| **[[15_段式存储管理\|分段]]** | 按逻辑段划分,通过段表映射 | 符合程序逻辑 | 外部碎片问题 |
|
||||
|
||||
---
|
||||
|
||||
## 九、小结
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((存储管理基础))
|
||||
存储层次
|
||||
寄存器
|
||||
Cache
|
||||
主存
|
||||
Flash
|
||||
磁盘
|
||||
管理功能
|
||||
分配回收
|
||||
地址转换
|
||||
内存共享
|
||||
内存保护
|
||||
内存扩充
|
||||
程序装入
|
||||
编译链接
|
||||
可执行文件结构
|
||||
逻辑地址vs物理地址
|
||||
地址空间
|
||||
虚拟地址空间
|
||||
物理地址空间
|
||||
内核空间/用户空间
|
||||
MMU保护
|
||||
页表U/S位
|
||||
CPU模式位
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[01_系统运行机制]] — CPU工作模式(用户态/内核态)与MMU硬件基础
|
||||
- [[14_分页存储管理]] — 分页式地址转换的详细实现
|
||||
- [[15_段式存储管理]] — 分段式地址转换
|
||||
- [[16_虚拟存储器]] — 虚拟存储器的实现原理
|
||||
|
||||
---
|
||||
|
||||
**上一讲**: [[01_系统运行机制]]
|
||||
**下一讲**: [[14_分页存储管理]]
|
||||
# 13. 存储管理基础
|
||||
|
||||
> **课程**: 操作系统 - 存储器管理
|
||||
> **核心内容**: 存储器层次结构、存储管理功能、程序编译链接与装入、地址空间
|
||||
|
||||
---
|
||||
|
||||
## 一、存储器层次结构
|
||||
|
||||
存储器按照速度和容量形成层次结构,越靠近CPU速度越快但容量越小、价格越高。
|
||||
|
||||
```
|
||||
速度递增 ↑ 容量递减 ↑ 价格递增 ↑
|
||||
┌─────────────────────────┐
|
||||
│ 寄存器(Register) │ ← 最快,纳秒级,几十~几百字节
|
||||
├─────────────────────────┤
|
||||
│ 高速缓存(Cache) │ ← L1/L2/L3,几MB
|
||||
├─────────────────────────┤
|
||||
│ 主存(内存/DRAM) │ ← 几GB~几百GB,百纳秒级
|
||||
├─────────────────────────┤
|
||||
│ Flash/SSD │ ← 固态存储
|
||||
├─────────────────────────┤
|
||||
│ 磁盘缓存(Disk Cache) │
|
||||
├─────────────────────────┤
|
||||
│ 固定磁盘(HDD/SSD) │ ← 几百GB~几TB,毫秒级
|
||||
├─────────────────────────┤
|
||||
│ 可移动存储(U盘/光盘/磁带) │ ← 最慢,容量可很大
|
||||
└─────────────────────────┘
|
||||
```
|
||||
|
||||
**设计原则**: 利用**局部性原理**,将频繁访问的数据放在高速层次,较少访问的数据放在低速大容量层次。
|
||||
|
||||
---
|
||||
|
||||
## 二、存储管理的功能
|
||||
|
||||
操作系统存储管理需要实现以下五大核心功能:
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| **内存分配与回收** | 为进程分配所需内存,进程结束后回收内存 |
|
||||
| **地址转换** | 将程序中的逻辑地址转换为物理地址([[14_分页存储管理\|分页]]、[[15_段式存储管理\|分段]]) |
|
||||
| **内存共享** | 多个进程共享同一段代码(如共享库) |
|
||||
| **内存保护** | 防止进程越界访问其他进程或内核的内存区域 |
|
||||
| **内存扩充** | 通过虚拟存储技术,使程序可用空间大于实际物理内存([[16_虚拟存储器\|虚拟存储器]]) |
|
||||
|
||||
---
|
||||
|
||||
## 三、程序的编译与链接过程
|
||||
|
||||
一个C语言源文件从编写到可执行,经历以下阶段:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A["hello.c<br/>源文件"] -->|预处理| B["hello.i<br/>预处理后"]
|
||||
B -->|编译| C["hello.s<br/>汇编文件"]
|
||||
C -->|汇编| D["hello.o<br/>目标文件(可重定位)"]
|
||||
D -->|链接| E["hello / a.out<br/>可执行文件"]
|
||||
E -->|装入| F["内存中运行的进程"]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style C fill:#fff3e0
|
||||
style D fill:#fce4ec
|
||||
style E fill:#e8f5e9
|
||||
style F fill:#f3e5f5
|
||||
```
|
||||
|
||||
### 各阶段说明
|
||||
|
||||
| 阶段 | 输入 | 输出 | 工具 | 说明 |
|
||||
|------|------|------|------|------|
|
||||
| **预处理** | `.c` | `.i` | cpp | 展开宏、头文件、条件编译 |
|
||||
| **编译** | `.i` | `.s` | cc1 | 翻译为汇编语言 |
|
||||
| **汇编** | `.s` | `.o` | as | 翻译为机器指令(可重定位目标文件) |
|
||||
| **链接** | `.o` | 可执行文件 | ld | 合并节段、解析符号引用、重定位 |
|
||||
| **装入** | 可执行文件 | 进程 | 加载器 | 将程序载入内存并创建进程 |
|
||||
|
||||
### 查看目标文件的节段
|
||||
|
||||
使用 `objdump -h` 可以查看可执行文件或目标文件中的节段(Section)信息:
|
||||
|
||||
```bash
|
||||
gcc -c hello.c # 编译为可重定位目标文件
|
||||
objdump -h hello.o # 查看节段头信息
|
||||
|
||||
gcc hello.c -o hello # 链接为可执行文件
|
||||
objdump -h hello # 查看可执行文件的节段
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、可执行文件结构
|
||||
|
||||
可执行文件在内存中从低地址到高地址的典型布局如下:
|
||||
|
||||
```
|
||||
高地址 ┌──────────────────┐
|
||||
│ 命令行参数 │
|
||||
│ 和环境变量 │
|
||||
├──────────────────┤
|
||||
│ 栈(Stack) │ ← 局部变量、函数调用帧,向下增长 ↓
|
||||
│ ↓ │
|
||||
│ │
|
||||
│ ↑ │
|
||||
│ 堆(Heap) │ ← malloc/new动态分配,向上增长 ↑
|
||||
├──────────────────┤
|
||||
│ .bss 段 │ ← 未初始化的全局/静态变量(不占文件空间)
|
||||
├──────────────────┤
|
||||
│ .data 段 │ ← 已初始化的全局变量和静态变量
|
||||
├──────────────────┤
|
||||
│ .rodata 段 │ ← 只读数据(如字符串常量)
|
||||
├──────────────────┤
|
||||
│ .text 段 │ ← 可执行代码(机器指令)
|
||||
低地址 └──────────────────┘
|
||||
```
|
||||
|
||||
| 段名 | 内容 | 是否可写 | 说明 |
|
||||
|------|------|---------|------|
|
||||
| `.text` | 机器指令代码 | 只读/只执行 | 程序的可执行代码 |
|
||||
| `.rodata` | 只读数据 | 只读 | 字符串常量、`const`变量 |
|
||||
| `.data` | 已初始化全局变量 | 可读写 | 有初始值的全局和静态变量 |
|
||||
| `.bss` | 未初始化全局变量 | 可读写 | 不占文件空间,装入时清零 |
|
||||
| 堆(Heap) | 动态分配内存 | 可读写 | `malloc`/`new` 分配 |
|
||||
| 栈(Stack) | 函数调用帧 | 可读写 | 局部变量、返回地址等 |
|
||||
|
||||
---
|
||||
|
||||
## 五、逻辑地址与物理地址
|
||||
|
||||
### 核心概念
|
||||
|
||||
| 术语 | 别名 | 说明 |
|
||||
|------|------|------|
|
||||
| **逻辑地址** | 虚拟地址(VA)、相对地址 | 程序中使用的地址,从0开始编址 |
|
||||
| **物理地址** | PA、绝对地址 | 内存硬件中的实际地址 |
|
||||
|
||||
> **关键**: 程序中的地址(逻辑地址)**不等于**内存中的实际地址(物理地址)。地址转换由硬件([[01_系统运行机制#四、存储器管理硬件——MMU|MMU]])和操作系统配合完成。
|
||||
|
||||
### 逻辑地址空间与物理地址空间
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph VA["逻辑地址空间 (虚拟)"]
|
||||
direction TB
|
||||
V0["地址 0"]
|
||||
V1["..."]
|
||||
V2["地址 2^v - 1"]
|
||||
end
|
||||
|
||||
subgraph PA["物理地址空间 (实际)"]
|
||||
direction TB
|
||||
P0["地址 0"]
|
||||
P1["..."]
|
||||
P2["地址 2^p - 1"]
|
||||
end
|
||||
|
||||
VA -->|"地址映射<br/>(页表/段表)"| PA
|
||||
|
||||
style VA fill:#e3f2fd,stroke:#1976d2
|
||||
style PA fill:#fff8e1,stroke:#f9a825
|
||||
```
|
||||
|
||||
- **虚拟地址空间**: 大小为 $2^v$ 字节,其中 $v$ 是虚拟地址的位数
|
||||
- **物理地址空间**: 大小为 $2^p$ 字节,其中 $p$ 是物理地址的位数
|
||||
- 通常 $v \geq p$(虚拟地址空间可以大于物理内存),这就是[[16_虚拟存储器|虚拟存储器]]的基础
|
||||
|
||||
### 引入逻辑地址的好处
|
||||
|
||||
1. **进程隔离**: 每个进程拥有独立的虚拟地址空间,互不干扰
|
||||
2. **提高内存利用率**: 可以使用[[16_虚拟存储器|虚拟存储器]]技术
|
||||
3. **内存保护**: 通过地址转换实现访问权限控制
|
||||
|
||||
---
|
||||
|
||||
## 六、内核空间与用户空间
|
||||
|
||||
操作系统将每个进程的虚拟地址空间划分为**用户空间**和**内核空间**两部分:
|
||||
|
||||
```mermaid
|
||||
block-beta
|
||||
columns 1
|
||||
block:linux["Linux 进程地址空间 (4GB)"]
|
||||
columns 1
|
||||
block:kernel_linux["内核空间 (高 1GB)"]
|
||||
k1["内核代码、数据、内核栈等"]
|
||||
end
|
||||
block:user_linux["用户空间 (低 3GB)"]
|
||||
u1["栈 ↓"]
|
||||
u2["..."]
|
||||
u3["堆 ↑"]
|
||||
u4[".bss / .data / .rodata / .text"]
|
||||
end
|
||||
end
|
||||
block:win["Windows 进程地址空间 (4GB)"]
|
||||
columns 1
|
||||
block:kernel_win["内核空间 (高 2GB)"]
|
||||
k2["内核代码、驱动等"]
|
||||
end
|
||||
block:user_win["用户空间 (低 2GB)"]
|
||||
u5["用户程序空间"]
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
| 操作系统 | 用户空间 | 内核空间 | 说明 |
|
||||
|---------|---------|---------|------|
|
||||
| **Linux (32位)** | 0 ~ 3GB (低3GB) | 3GB ~ 4GB (高1GB) | 通过`PAGE_OFFSET`划分 |
|
||||
| **Windows (32位)** | 0 ~ 2GB (低2GB) | 2GB ~ 4GB (高2GB) | 可通过`/3GB`启动参数调整为3:1 |
|
||||
|
||||
**重要规则**: 用户态程序**不能**直接访问内核空间的地址,否则触发保护异常。内核态代码可以访问整个地址空间。
|
||||
|
||||
---
|
||||
|
||||
## 七、MMU 与内存保护
|
||||
|
||||
**MMU (Memory Management Unit)** 是CPU中负责地址转换和内存保护的硬件单元。
|
||||
|
||||
### 地址转换流程
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
CPU["CPU 发出<br/>虚拟地址(VA)"] --> MMU["MMU<br/>地址转换"]
|
||||
MMU --> PA["物理地址(PA)"]
|
||||
PA --> MEM["访问内存"]
|
||||
|
||||
style MMU fill:#ffcdd2,stroke:#c62828
|
||||
```
|
||||
|
||||
### 页表保护机制
|
||||
|
||||
MMU通过页表中的**U/S位**和CPU的**模式位**配合实现内存保护:
|
||||
|
||||
| 页表U/S位 | 含义 |
|
||||
|-----------|------|
|
||||
| **U=0 (Supervisor)** | 内核态页面,只有内核可以访问 |
|
||||
| **U=1 (User)** | 用户态页面,用户态和内核态均可访问 |
|
||||
|
||||
| CPU模式 | 可访问的页面 |
|
||||
|---------|-------------|
|
||||
| **用户模式 (User mode)** | 只能访问 U=1 的页面 |
|
||||
| **内核模式 (Kernel mode)** | 可以访问 U=0 和 U=1 的页面 |
|
||||
|
||||
当用户态程序试图访问 U=0 的内核页面时,MMU会产生**保护异常(段错误/Segmentation Fault)**,终止该进程。
|
||||
|
||||
---
|
||||
|
||||
## 八、地址转换的主要方式
|
||||
|
||||
操作系统实现地址转换有多种方式,各有特点:
|
||||
|
||||
| 方式 | 原理 | 优点 | 缺点 |
|
||||
|------|------|------|------|
|
||||
| **重定位寄存器(基址寄存器)** | PA = VA + 基址值 | 简单 | 程序必须连续存放 |
|
||||
| **静态重定位** | 装入时一次性修改所有地址 | 无需硬件支持 | 装入后不能移动 |
|
||||
| **动态重定位** | 执行时通过MMU实时转换 | 灵活,支持移动 | 需要硬件支持 |
|
||||
| **[[14_分页存储管理\|分页]]** | 按页划分,通过页表映射 | 消除外部碎片 | 有内部碎片、页表开销 |
|
||||
| **[[15_段式存储管理\|分段]]** | 按逻辑段划分,通过段表映射 | 符合程序逻辑 | 外部碎片问题 |
|
||||
|
||||
---
|
||||
|
||||
## 九、小结
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((存储管理基础))
|
||||
存储层次
|
||||
寄存器
|
||||
Cache
|
||||
主存
|
||||
Flash
|
||||
磁盘
|
||||
管理功能
|
||||
分配回收
|
||||
地址转换
|
||||
内存共享
|
||||
内存保护
|
||||
内存扩充
|
||||
程序装入
|
||||
编译链接
|
||||
可执行文件结构
|
||||
逻辑地址vs物理地址
|
||||
地址空间
|
||||
虚拟地址空间
|
||||
物理地址空间
|
||||
内核空间/用户空间
|
||||
MMU保护
|
||||
页表U/S位
|
||||
CPU模式位
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[01_系统运行机制]] — CPU工作模式(用户态/内核态)与MMU硬件基础
|
||||
- [[14_分页存储管理]] — 分页式地址转换的详细实现
|
||||
- [[15_段式存储管理]] — 分段式地址转换
|
||||
- [[16_虚拟存储器]] — 虚拟存储器的实现原理
|
||||
|
||||
---
|
||||
|
||||
**上一讲**: [[01_系统运行机制]]
|
||||
**下一讲**: [[14_分页存储管理]]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,398 +1,398 @@
|
||||
# 15. 段式存储管理
|
||||
|
||||
> **课程**: 操作系统 - 存储器管理
|
||||
> **核心内容**: 分段引入原因、分段思想、地址结构、段表、地址变换、段页式存储管理
|
||||
|
||||
---
|
||||
|
||||
## 前置知识
|
||||
|
||||
- [[13_存储管理基础]] — 存储器层次结构、逻辑地址与物理地址
|
||||
- [[14_分页存储管理]] — 分页思想、页表、地址变换
|
||||
|
||||
---
|
||||
|
||||
## 一、为什么需要分段
|
||||
|
||||
[[14_分页存储管理|分页]]虽然解决了碎片问题,但在以下场景中存在不足:
|
||||
|
||||
### 1. 信息共享不方便
|
||||
|
||||
分页按固定大小划分,不考虑程序的逻辑结构。如果要共享一段代码(如共享库函数),该代码可能跨越多个页面,其中某些页面还包含不需要共享的数据,导致共享粒度过粗。
|
||||
|
||||
```
|
||||
分页视角(按固定大小切分,不考虑逻辑含义):
|
||||
┌────────┐ ┌────────┐ ┌────────┐
|
||||
│ 代码1 │ │ 代码2+ │ │ 数据 │ ← 一个逻辑模块跨了3个页
|
||||
│ │ │ 常量 │ │ │ 共享时会把不相关的内容也共享了
|
||||
└────────┘ └────────┘ └────────┘
|
||||
```
|
||||
|
||||
### 2. 动态链接问题
|
||||
|
||||
动态链接需要在运行时将目标模块装入内存并链接。分页系统中,模块的装入和链接以页为单位,不够灵活。
|
||||
|
||||
### 3. 程序员视角需求
|
||||
|
||||
程序员编写程序时,自然地将代码划分为**代码段、数据段、堆栈段**等逻辑模块。分页完全打乱了这种逻辑结构。
|
||||
|
||||
> **核心需求**: 地址空间的划分应该按照**逻辑意义**进行,而非固定大小。这就是分段的思想。
|
||||
|
||||
---
|
||||
|
||||
## 二、分段的基本思想
|
||||
|
||||
分段按照程序的**逻辑结构**将地址空间划分为若干个段:
|
||||
|
||||
- 每个段有独立的**段名**和**段长**
|
||||
- 每个段在内存中**连续存放**
|
||||
- 不同段之间**不需要连续**
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph VA["进程虚拟地址空间"]
|
||||
direction TB
|
||||
S0["主程序段 (段0)"]
|
||||
S1["子程序段 (段1)"]
|
||||
S2["数据段 (段2)"]
|
||||
S3["栈段 (段3)"]
|
||||
end
|
||||
|
||||
subgraph PM["物理内存"]
|
||||
direction TB
|
||||
M0["区域A"]
|
||||
M1["区域B"]
|
||||
M2["区域C"]
|
||||
M3["区域D"]
|
||||
M4["空闲"]
|
||||
M5["区域E"]
|
||||
end
|
||||
|
||||
S0 -->|"段表映射"| M5
|
||||
S1 -->|"段表映射"| M0
|
||||
S2 -->|"段表映射"| M2
|
||||
S3 -->|"段表映射"| M3
|
||||
|
||||
style VA fill:#e3f2fd,stroke:#1976d2
|
||||
style PM fill:#fff8e1,stroke:#f9a825
|
||||
```
|
||||
|
||||
### 分段 vs 分页
|
||||
|
||||
| 对比项 | 分页 | 分段 |
|
||||
|--------|------|------|
|
||||
| **划分依据** | 固定大小(物理需要) | 逻辑意义(程序员视角) |
|
||||
| **段/页大小** | 所有页等大 | 各段长度不同 |
|
||||
| **地址空间** | 一维(VA直接计算VPN和偏移) | **二维**(段号 + 段内偏移) |
|
||||
| **碎片类型** | 内碎片(页内浪费) | 外碎片(段间空闲区) |
|
||||
| **共享** | 以页为粒度,较粗 | 以段为粒度,符合逻辑 |
|
||||
| **动态增长** | 不方便 | 方便(如堆、栈段可动态扩展) |
|
||||
|
||||
---
|
||||
|
||||
## 三、地址结构
|
||||
|
||||
分段系统的逻辑地址是**二维**的,由两部分组成:
|
||||
|
||||
```
|
||||
逻辑地址 = (段号 S, 段内地址 d)
|
||||
```
|
||||
|
||||
```
|
||||
逻辑地址表示:
|
||||
┌──────────────┬────────────────────┐
|
||||
│ 段号 S │ 段内地址 d │
|
||||
│ (高位部分) │ (低位部分) │
|
||||
└──────────────┴────────────────────┘
|
||||
|
||||
注意: 不同于分页的VPN|VPO可以统一计算,
|
||||
分段中 d 的取值范围取决于段长,各段不同。
|
||||
```
|
||||
|
||||
**地址表示示例**:
|
||||
|
||||
- 分页地址 `(0x1A8F)` → 可直接算出 VPN=6, VPO=0x28F
|
||||
- 分段地址 `(2, 0x100)` → 段号=2,偏移=0x100(需要查段表才知道该段的基址和长度)
|
||||
|
||||
---
|
||||
|
||||
## 四、段表
|
||||
|
||||
### 段表结构
|
||||
|
||||
每个进程拥有一张**段表**,段表以**段号**为索引,每项包含:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|------|------|
|
||||
| **段号** | 段的编号(隐含在索引中) |
|
||||
| **段长 (Limit)** | 该段的长度(字节数),用于越界检查 |
|
||||
| **段基址 (Base)** | 该段在物理内存中的起始地址 |
|
||||
|
||||
```
|
||||
段表示例:
|
||||
┌──────┬────────────┬────────────────┐
|
||||
│ 段号 │ 段长(Limit) │ 基址(Base) │
|
||||
├──────┼────────────┼────────────────┤
|
||||
│ 0 │ 0x2000 │ 0x4000 │ ← 主程序: 在内存0x4000处, 长8KB
|
||||
│ 1 │ 0x1000 │ 0x8000 │ ← 子程序: 在内存0x8000处, 长4KB
|
||||
│ 2 │ 0x3000 │ 0xB000 │ ← 数据段: 在内存0xB000处, 长12KB
|
||||
│ 3 │ 0x1800 │ 0x2000 │ ← 栈段: 在内存0x2000处, 长6KB
|
||||
└──────┴────────────┴────────────────┘
|
||||
```
|
||||
|
||||
段表基址寄存器 **STBR** 指向段表在内存中的起始地址。
|
||||
|
||||
---
|
||||
|
||||
## 五、地址变换过程
|
||||
|
||||
### 变换流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["逻辑地址 (S, d)"] --> B["用STBR找到段表基址"]
|
||||
B --> C["定位段表项: 段表基址 + S × 段表项大小"]
|
||||
C --> D{"d < Limit ?"}
|
||||
D -->|"是"| E["物理地址 PA = Base + d"]
|
||||
D -->|"否"| F["**越界中断**<br/>(Segmentation Fault)"]
|
||||
E --> G["访问物理内存"]
|
||||
|
||||
style F fill:#ffcdd2,stroke:#c62828
|
||||
style D fill:#fff3e0,stroke:#e65100
|
||||
```
|
||||
|
||||
### 详细步骤
|
||||
|
||||
1. **提取段号和偏移**: 从逻辑地址中分离出段号 S 和段内地址 d
|
||||
2. **查段表**: 用 STBR + S 定位到第 S 个段表项
|
||||
3. **越界检查**: 比较 d 与 Limit
|
||||
- 若 d >= Limit,触发**越界中断**(Segmentation Fault)
|
||||
- 若 d < Limit,继续
|
||||
4. **计算物理地址**: PA = Base + d
|
||||
5. **访问内存**: 用物理地址访问实际内存
|
||||
|
||||
### 地址变换计算示例
|
||||
|
||||
> **例题**: 某分段系统,段表如下。求以下逻辑地址对应的物理地址:
|
||||
> - (0, 0x1500)
|
||||
> - (1, 0x2000)
|
||||
> - (2, 0x0800)
|
||||
|
||||
**解题过程**:
|
||||
|
||||
| 逻辑地址 | S | d | Limit | 比较 | Base | PA = Base + d | 结果 |
|
||||
|---------|---|---|-------|------|------|--------------|------|
|
||||
| (0, 0x1500) | 0 | 0x1500 | 0x2000 | 0x1500 < 0x2000 ✓ | 0x4000 | 0x4000 + 0x1500 | **0x5500** |
|
||||
| (1, 0x2000) | 1 | 0x2000 | 0x1000 | 0x2000 >= 0x1000 ✗ | — | — | **越界中断** |
|
||||
| (2, 0x0800) | 2 | 0x0800 | 0x3000 | 0x0800 < 0x3000 ✓ | 0xB000 | 0xB000 + 0x0800 | **0xB800** |
|
||||
|
||||
---
|
||||
|
||||
## 六、分段的优缺点
|
||||
|
||||
### 优点
|
||||
|
||||
| 优点 | 说明 |
|
||||
|------|------|
|
||||
| **符合程序逻辑** | 按代码、数据、栈等逻辑单元组织,便于理解和管理 |
|
||||
| **便于共享** | 以段为单位共享,粒度合理(如共享整个代码段) |
|
||||
| **便于动态链接** | 可以按段为单位进行动态链接和装入 |
|
||||
| **支持动态增长** | 堆、栈等段可以独立扩展,不影响其他段 |
|
||||
| **保护自然** | 每段可设置独立的访问权限(代码段只读、数据段可读写等) |
|
||||
|
||||
### 缺点
|
||||
|
||||
| 缺点 | 说明 |
|
||||
|------|------|
|
||||
| **外碎片** | 段长不等,内存分配/回收后产生不连续的空闲区 |
|
||||
| **段长限制** | 每段最大长度受地址结构限制 |
|
||||
| **内存紧缩开销** | 消除外碎片需要移动段(类似动态分区的紧凑操作) |
|
||||
|
||||
---
|
||||
|
||||
## 七、段页式存储管理
|
||||
|
||||
### 基本思想
|
||||
|
||||
**段页式** = 分段 + 分页,兼具两者的优点:
|
||||
|
||||
- 先按**逻辑结构分段**(保留分段的优点:共享、保护、逻辑清晰)
|
||||
- 再将每段**按固定大小分页**(保留分页的优点:消除外碎片)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph VA["虚拟地址空间"]
|
||||
direction TB
|
||||
S0["段0 (主程序)"]
|
||||
S1["段1 (子程序)"]
|
||||
S2["段2 (数据)"]
|
||||
end
|
||||
|
||||
subgraph S0P["段0 内部分页"]
|
||||
direction TB
|
||||
P0["页0"]
|
||||
P1["页1"]
|
||||
P2["页2"]
|
||||
end
|
||||
|
||||
subgraph PM["物理内存"]
|
||||
direction TB
|
||||
F0["页框0"]
|
||||
F1["页框1"]
|
||||
F2["页框2"]
|
||||
F3["页框3"]
|
||||
end
|
||||
|
||||
VA --> S0P
|
||||
P0 --> F2
|
||||
P1 --> F0
|
||||
P2 --> F3
|
||||
|
||||
style VA fill:#e3f2fd,stroke:#1976d2
|
||||
style S0P fill:#f3e5f5,stroke:#7b1fa2
|
||||
style PM fill:#fff8e1,stroke:#f9a825
|
||||
```
|
||||
|
||||
### 段页式地址结构
|
||||
|
||||
逻辑地址由**三部分**组成:
|
||||
|
||||
```
|
||||
段页式逻辑地址:
|
||||
┌──────────────┬──────────────┬──────────────┐
|
||||
│ 段号 S │ 页号 P │ 页内偏移 d │
|
||||
└──────────────┴──────────────┴──────────────┘
|
||||
```
|
||||
|
||||
### 段页式地址变换
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["逻辑地址 (S, P, d)"] --> B["1. 用STBR找到段表"]
|
||||
B --> C["2. 用段号S查段表<br/>得到该段的页表基址和段长"]
|
||||
C --> D{"P < 该段页数?"}
|
||||
D -->|"是"| E["3. 用页号P查页表<br/>得到物理页框号 PPN"]
|
||||
D -->|"否"| F["越界中断"]
|
||||
E --> G["4. 物理地址 = PPN × 页大小 + d"]
|
||||
G --> H["访问物理内存"]
|
||||
|
||||
style F fill:#ffcdd2,stroke:#c62828
|
||||
```
|
||||
|
||||
### 段页式地址变换步骤
|
||||
|
||||
1. **查段表**: 用 STBR + S 找到第 S 个段表项,得到该段的**页表基址**
|
||||
2. **查页表**: 用页表基址 + P 找到第 P 个页表项,得到 **PPN**
|
||||
3. **拼接地址**: PA = PPN × 页大小 + d
|
||||
|
||||
> **注意**: 段页式需要 **3 次内存访问**(段表 + 页表 + 数据),比纯分页多一次。TLB 的作用更加关键。
|
||||
|
||||
### 段页式地址变换计算示例
|
||||
|
||||
> **例题**: 某段页式系统,页面大小 4KB。段表如下,求逻辑地址 (1, 2, 0x100) 的物理地址。
|
||||
|
||||
**段表**:
|
||||
|
||||
| 段号 | 页表基址 | 段长(页数) |
|
||||
|------|---------|-----------|
|
||||
| 0 | 0x8000 | 4 |
|
||||
| 1 | 0xA000 | 6 |
|
||||
| 2 | 0xC000 | 3 |
|
||||
|
||||
**段1的页表** (基址 0xA000):
|
||||
|
||||
| 页号 | PPN |
|
||||
|------|-----|
|
||||
| 0 | 5 |
|
||||
| 1 | 2 |
|
||||
| 2 | 8 |
|
||||
| 3 | 1 |
|
||||
| 4 | 7 |
|
||||
| 5 | 3 |
|
||||
|
||||
**解题过程**:
|
||||
|
||||
1. S=1, P=2, d=0x100
|
||||
2. 查段表:段1的页表基址 = 0xA000,段长 = 6 页
|
||||
3. P=2 < 6,合法
|
||||
4. 查段1的页表:P=2 对应 PPN=**8**
|
||||
5. PA = 8 × 4096 + 0x100 = 0x8000 + 0x100 = **0x8100**
|
||||
|
||||
---
|
||||
|
||||
## 八、三种存储管理方式对比
|
||||
|
||||
| 对比项 | 纯分页 | 纯分段 | 段页式 |
|
||||
|--------|--------|--------|--------|
|
||||
| **划分依据** | 固定大小 | 逻辑结构 | 先逻辑后固定大小 |
|
||||
| **地址维度** | 一维 | 二维 | 三维 |
|
||||
| **碎片** | 内碎片 | 外碎片 | 内碎片 |
|
||||
| **共享** | 以页为粒度 | 以段为粒度 | 以段为粒度 |
|
||||
| **内存访问次数** | 2次(页表+数据) | 2次(段表+数据) | 3次(段表+页表+数据) |
|
||||
| **代表系统** | Linux | 早期Multics | Intel x86(32位保护模式) |
|
||||
|
||||
---
|
||||
|
||||
## 九、小结
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((段式存储管理))
|
||||
分段引入
|
||||
信息共享不便
|
||||
动态链接需求
|
||||
逻辑结构需求
|
||||
分段思想
|
||||
按逻辑分段
|
||||
代码段/数据段/栈段
|
||||
各段独立地址空间
|
||||
地址结构
|
||||
二维: 段号+偏移
|
||||
不同于分页的一维
|
||||
段表
|
||||
段号/段长/基址
|
||||
越界检查
|
||||
STBR寄存器
|
||||
地址变换
|
||||
查段表→越界检查→基址+偏移
|
||||
PA = Base + d
|
||||
段页式
|
||||
先分段再分页
|
||||
三维地址: 段号+页号+偏移
|
||||
兼具两者优点
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 思考题
|
||||
|
||||
1. **概念理解**: 分段和分页的根本区别是什么?为什么说分段的地址空间是二维的?
|
||||
|
||||
2. **地址变换**: 某分段系统有 4 个段,段表如下。求物理地址或判断是否越界:
|
||||
- (0, 0x800) → ?
|
||||
- (2, 0x5000) → ?
|
||||
- (3, 0x2000) → ?
|
||||
|
||||
| 段号 | 段长 | 基址 |
|
||||
|------|------|------|
|
||||
| 0 | 0x1000 | 0x5000 |
|
||||
| 1 | 0x2000 | 0x8000 |
|
||||
| 2 | 0x4000 | 0xA000 |
|
||||
| 3 | 0x3000 | 0x2000 |
|
||||
|
||||
3. **段页式**: 为什么段页式需要 3 次内存访问?TLB 如何缓解这个问题?
|
||||
|
||||
4. **对比分析**: 在什么场景下分段优于分页?在什么场景下分页优于分段?
|
||||
|
||||
---
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[13_存储管理基础]] — 存储器层次结构与地址空间基础
|
||||
- [[14_分页存储管理]] — 分页思想、页表与地址变换
|
||||
- [[16_虚拟存储器]] — 基于分段的虚拟存储器实现
|
||||
|
||||
---
|
||||
|
||||
**上一讲**: [[14_分页存储管理]]
|
||||
**下一讲**: [[16_虚拟存储器]]
|
||||
# 15. 段式存储管理
|
||||
|
||||
> **课程**: 操作系统 - 存储器管理
|
||||
> **核心内容**: 分段引入原因、分段思想、地址结构、段表、地址变换、段页式存储管理
|
||||
|
||||
---
|
||||
|
||||
## 前置知识
|
||||
|
||||
- [[13_存储管理基础]] — 存储器层次结构、逻辑地址与物理地址
|
||||
- [[14_分页存储管理]] — 分页思想、页表、地址变换
|
||||
|
||||
---
|
||||
|
||||
## 一、为什么需要分段
|
||||
|
||||
[[14_分页存储管理|分页]]虽然解决了碎片问题,但在以下场景中存在不足:
|
||||
|
||||
### 1. 信息共享不方便
|
||||
|
||||
分页按固定大小划分,不考虑程序的逻辑结构。如果要共享一段代码(如共享库函数),该代码可能跨越多个页面,其中某些页面还包含不需要共享的数据,导致共享粒度过粗。
|
||||
|
||||
```
|
||||
分页视角(按固定大小切分,不考虑逻辑含义):
|
||||
┌────────┐ ┌────────┐ ┌────────┐
|
||||
│ 代码1 │ │ 代码2+ │ │ 数据 │ ← 一个逻辑模块跨了3个页
|
||||
│ │ │ 常量 │ │ │ 共享时会把不相关的内容也共享了
|
||||
└────────┘ └────────┘ └────────┘
|
||||
```
|
||||
|
||||
### 2. 动态链接问题
|
||||
|
||||
动态链接需要在运行时将目标模块装入内存并链接。分页系统中,模块的装入和链接以页为单位,不够灵活。
|
||||
|
||||
### 3. 程序员视角需求
|
||||
|
||||
程序员编写程序时,自然地将代码划分为**代码段、数据段、堆栈段**等逻辑模块。分页完全打乱了这种逻辑结构。
|
||||
|
||||
> **核心需求**: 地址空间的划分应该按照**逻辑意义**进行,而非固定大小。这就是分段的思想。
|
||||
|
||||
---
|
||||
|
||||
## 二、分段的基本思想
|
||||
|
||||
分段按照程序的**逻辑结构**将地址空间划分为若干个段:
|
||||
|
||||
- 每个段有独立的**段名**和**段长**
|
||||
- 每个段在内存中**连续存放**
|
||||
- 不同段之间**不需要连续**
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph VA["进程虚拟地址空间"]
|
||||
direction TB
|
||||
S0["主程序段 (段0)"]
|
||||
S1["子程序段 (段1)"]
|
||||
S2["数据段 (段2)"]
|
||||
S3["栈段 (段3)"]
|
||||
end
|
||||
|
||||
subgraph PM["物理内存"]
|
||||
direction TB
|
||||
M0["区域A"]
|
||||
M1["区域B"]
|
||||
M2["区域C"]
|
||||
M3["区域D"]
|
||||
M4["空闲"]
|
||||
M5["区域E"]
|
||||
end
|
||||
|
||||
S0 -->|"段表映射"| M5
|
||||
S1 -->|"段表映射"| M0
|
||||
S2 -->|"段表映射"| M2
|
||||
S3 -->|"段表映射"| M3
|
||||
|
||||
style VA fill:#e3f2fd,stroke:#1976d2
|
||||
style PM fill:#fff8e1,stroke:#f9a825
|
||||
```
|
||||
|
||||
### 分段 vs 分页
|
||||
|
||||
| 对比项 | 分页 | 分段 |
|
||||
|--------|------|------|
|
||||
| **划分依据** | 固定大小(物理需要) | 逻辑意义(程序员视角) |
|
||||
| **段/页大小** | 所有页等大 | 各段长度不同 |
|
||||
| **地址空间** | 一维(VA直接计算VPN和偏移) | **二维**(段号 + 段内偏移) |
|
||||
| **碎片类型** | 内碎片(页内浪费) | 外碎片(段间空闲区) |
|
||||
| **共享** | 以页为粒度,较粗 | 以段为粒度,符合逻辑 |
|
||||
| **动态增长** | 不方便 | 方便(如堆、栈段可动态扩展) |
|
||||
|
||||
---
|
||||
|
||||
## 三、地址结构
|
||||
|
||||
分段系统的逻辑地址是**二维**的,由两部分组成:
|
||||
|
||||
```
|
||||
逻辑地址 = (段号 S, 段内地址 d)
|
||||
```
|
||||
|
||||
```
|
||||
逻辑地址表示:
|
||||
┌──────────────┬────────────────────┐
|
||||
│ 段号 S │ 段内地址 d │
|
||||
│ (高位部分) │ (低位部分) │
|
||||
└──────────────┴────────────────────┘
|
||||
|
||||
注意: 不同于分页的VPN|VPO可以统一计算,
|
||||
分段中 d 的取值范围取决于段长,各段不同。
|
||||
```
|
||||
|
||||
**地址表示示例**:
|
||||
|
||||
- 分页地址 `(0x1A8F)` → 可直接算出 VPN=6, VPO=0x28F
|
||||
- 分段地址 `(2, 0x100)` → 段号=2,偏移=0x100(需要查段表才知道该段的基址和长度)
|
||||
|
||||
---
|
||||
|
||||
## 四、段表
|
||||
|
||||
### 段表结构
|
||||
|
||||
每个进程拥有一张**段表**,段表以**段号**为索引,每项包含:
|
||||
|
||||
| 字段 | 含义 |
|
||||
|------|------|
|
||||
| **段号** | 段的编号(隐含在索引中) |
|
||||
| **段长 (Limit)** | 该段的长度(字节数),用于越界检查 |
|
||||
| **段基址 (Base)** | 该段在物理内存中的起始地址 |
|
||||
|
||||
```
|
||||
段表示例:
|
||||
┌──────┬────────────┬────────────────┐
|
||||
│ 段号 │ 段长(Limit) │ 基址(Base) │
|
||||
├──────┼────────────┼────────────────┤
|
||||
│ 0 │ 0x2000 │ 0x4000 │ ← 主程序: 在内存0x4000处, 长8KB
|
||||
│ 1 │ 0x1000 │ 0x8000 │ ← 子程序: 在内存0x8000处, 长4KB
|
||||
│ 2 │ 0x3000 │ 0xB000 │ ← 数据段: 在内存0xB000处, 长12KB
|
||||
│ 3 │ 0x1800 │ 0x2000 │ ← 栈段: 在内存0x2000处, 长6KB
|
||||
└──────┴────────────┴────────────────┘
|
||||
```
|
||||
|
||||
段表基址寄存器 **STBR** 指向段表在内存中的起始地址。
|
||||
|
||||
---
|
||||
|
||||
## 五、地址变换过程
|
||||
|
||||
### 变换流程
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["逻辑地址 (S, d)"] --> B["用STBR找到段表基址"]
|
||||
B --> C["定位段表项: 段表基址 + S × 段表项大小"]
|
||||
C --> D{"d < Limit ?"}
|
||||
D -->|"是"| E["物理地址 PA = Base + d"]
|
||||
D -->|"否"| F["**越界中断**<br/>(Segmentation Fault)"]
|
||||
E --> G["访问物理内存"]
|
||||
|
||||
style F fill:#ffcdd2,stroke:#c62828
|
||||
style D fill:#fff3e0,stroke:#e65100
|
||||
```
|
||||
|
||||
### 详细步骤
|
||||
|
||||
1. **提取段号和偏移**: 从逻辑地址中分离出段号 S 和段内地址 d
|
||||
2. **查段表**: 用 STBR + S 定位到第 S 个段表项
|
||||
3. **越界检查**: 比较 d 与 Limit
|
||||
- 若 d >= Limit,触发**越界中断**(Segmentation Fault)
|
||||
- 若 d < Limit,继续
|
||||
4. **计算物理地址**: PA = Base + d
|
||||
5. **访问内存**: 用物理地址访问实际内存
|
||||
|
||||
### 地址变换计算示例
|
||||
|
||||
> **例题**: 某分段系统,段表如下。求以下逻辑地址对应的物理地址:
|
||||
> - (0, 0x1500)
|
||||
> - (1, 0x2000)
|
||||
> - (2, 0x0800)
|
||||
|
||||
**解题过程**:
|
||||
|
||||
| 逻辑地址 | S | d | Limit | 比较 | Base | PA = Base + d | 结果 |
|
||||
|---------|---|---|-------|------|------|--------------|------|
|
||||
| (0, 0x1500) | 0 | 0x1500 | 0x2000 | 0x1500 < 0x2000 ✓ | 0x4000 | 0x4000 + 0x1500 | **0x5500** |
|
||||
| (1, 0x2000) | 1 | 0x2000 | 0x1000 | 0x2000 >= 0x1000 ✗ | — | — | **越界中断** |
|
||||
| (2, 0x0800) | 2 | 0x0800 | 0x3000 | 0x0800 < 0x3000 ✓ | 0xB000 | 0xB000 + 0x0800 | **0xB800** |
|
||||
|
||||
---
|
||||
|
||||
## 六、分段的优缺点
|
||||
|
||||
### 优点
|
||||
|
||||
| 优点 | 说明 |
|
||||
|------|------|
|
||||
| **符合程序逻辑** | 按代码、数据、栈等逻辑单元组织,便于理解和管理 |
|
||||
| **便于共享** | 以段为单位共享,粒度合理(如共享整个代码段) |
|
||||
| **便于动态链接** | 可以按段为单位进行动态链接和装入 |
|
||||
| **支持动态增长** | 堆、栈等段可以独立扩展,不影响其他段 |
|
||||
| **保护自然** | 每段可设置独立的访问权限(代码段只读、数据段可读写等) |
|
||||
|
||||
### 缺点
|
||||
|
||||
| 缺点 | 说明 |
|
||||
|------|------|
|
||||
| **外碎片** | 段长不等,内存分配/回收后产生不连续的空闲区 |
|
||||
| **段长限制** | 每段最大长度受地址结构限制 |
|
||||
| **内存紧缩开销** | 消除外碎片需要移动段(类似动态分区的紧凑操作) |
|
||||
|
||||
---
|
||||
|
||||
## 七、段页式存储管理
|
||||
|
||||
### 基本思想
|
||||
|
||||
**段页式** = 分段 + 分页,兼具两者的优点:
|
||||
|
||||
- 先按**逻辑结构分段**(保留分段的优点:共享、保护、逻辑清晰)
|
||||
- 再将每段**按固定大小分页**(保留分页的优点:消除外碎片)
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph VA["虚拟地址空间"]
|
||||
direction TB
|
||||
S0["段0 (主程序)"]
|
||||
S1["段1 (子程序)"]
|
||||
S2["段2 (数据)"]
|
||||
end
|
||||
|
||||
subgraph S0P["段0 内部分页"]
|
||||
direction TB
|
||||
P0["页0"]
|
||||
P1["页1"]
|
||||
P2["页2"]
|
||||
end
|
||||
|
||||
subgraph PM["物理内存"]
|
||||
direction TB
|
||||
F0["页框0"]
|
||||
F1["页框1"]
|
||||
F2["页框2"]
|
||||
F3["页框3"]
|
||||
end
|
||||
|
||||
VA --> S0P
|
||||
P0 --> F2
|
||||
P1 --> F0
|
||||
P2 --> F3
|
||||
|
||||
style VA fill:#e3f2fd,stroke:#1976d2
|
||||
style S0P fill:#f3e5f5,stroke:#7b1fa2
|
||||
style PM fill:#fff8e1,stroke:#f9a825
|
||||
```
|
||||
|
||||
### 段页式地址结构
|
||||
|
||||
逻辑地址由**三部分**组成:
|
||||
|
||||
```
|
||||
段页式逻辑地址:
|
||||
┌──────────────┬──────────────┬──────────────┐
|
||||
│ 段号 S │ 页号 P │ 页内偏移 d │
|
||||
└──────────────┴──────────────┴──────────────┘
|
||||
```
|
||||
|
||||
### 段页式地址变换
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["逻辑地址 (S, P, d)"] --> B["1. 用STBR找到段表"]
|
||||
B --> C["2. 用段号S查段表<br/>得到该段的页表基址和段长"]
|
||||
C --> D{"P < 该段页数?"}
|
||||
D -->|"是"| E["3. 用页号P查页表<br/>得到物理页框号 PPN"]
|
||||
D -->|"否"| F["越界中断"]
|
||||
E --> G["4. 物理地址 = PPN × 页大小 + d"]
|
||||
G --> H["访问物理内存"]
|
||||
|
||||
style F fill:#ffcdd2,stroke:#c62828
|
||||
```
|
||||
|
||||
### 段页式地址变换步骤
|
||||
|
||||
1. **查段表**: 用 STBR + S 找到第 S 个段表项,得到该段的**页表基址**
|
||||
2. **查页表**: 用页表基址 + P 找到第 P 个页表项,得到 **PPN**
|
||||
3. **拼接地址**: PA = PPN × 页大小 + d
|
||||
|
||||
> **注意**: 段页式需要 **3 次内存访问**(段表 + 页表 + 数据),比纯分页多一次。TLB 的作用更加关键。
|
||||
|
||||
### 段页式地址变换计算示例
|
||||
|
||||
> **例题**: 某段页式系统,页面大小 4KB。段表如下,求逻辑地址 (1, 2, 0x100) 的物理地址。
|
||||
|
||||
**段表**:
|
||||
|
||||
| 段号 | 页表基址 | 段长(页数) |
|
||||
|------|---------|-----------|
|
||||
| 0 | 0x8000 | 4 |
|
||||
| 1 | 0xA000 | 6 |
|
||||
| 2 | 0xC000 | 3 |
|
||||
|
||||
**段1的页表** (基址 0xA000):
|
||||
|
||||
| 页号 | PPN |
|
||||
|------|-----|
|
||||
| 0 | 5 |
|
||||
| 1 | 2 |
|
||||
| 2 | 8 |
|
||||
| 3 | 1 |
|
||||
| 4 | 7 |
|
||||
| 5 | 3 |
|
||||
|
||||
**解题过程**:
|
||||
|
||||
1. S=1, P=2, d=0x100
|
||||
2. 查段表:段1的页表基址 = 0xA000,段长 = 6 页
|
||||
3. P=2 < 6,合法
|
||||
4. 查段1的页表:P=2 对应 PPN=**8**
|
||||
5. PA = 8 × 4096 + 0x100 = 0x8000 + 0x100 = **0x8100**
|
||||
|
||||
---
|
||||
|
||||
## 八、三种存储管理方式对比
|
||||
|
||||
| 对比项 | 纯分页 | 纯分段 | 段页式 |
|
||||
|--------|--------|--------|--------|
|
||||
| **划分依据** | 固定大小 | 逻辑结构 | 先逻辑后固定大小 |
|
||||
| **地址维度** | 一维 | 二维 | 三维 |
|
||||
| **碎片** | 内碎片 | 外碎片 | 内碎片 |
|
||||
| **共享** | 以页为粒度 | 以段为粒度 | 以段为粒度 |
|
||||
| **内存访问次数** | 2次(页表+数据) | 2次(段表+数据) | 3次(段表+页表+数据) |
|
||||
| **代表系统** | Linux | 早期Multics | Intel x86(32位保护模式) |
|
||||
|
||||
---
|
||||
|
||||
## 九、小结
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((段式存储管理))
|
||||
分段引入
|
||||
信息共享不便
|
||||
动态链接需求
|
||||
逻辑结构需求
|
||||
分段思想
|
||||
按逻辑分段
|
||||
代码段/数据段/栈段
|
||||
各段独立地址空间
|
||||
地址结构
|
||||
二维: 段号+偏移
|
||||
不同于分页的一维
|
||||
段表
|
||||
段号/段长/基址
|
||||
越界检查
|
||||
STBR寄存器
|
||||
地址变换
|
||||
查段表→越界检查→基址+偏移
|
||||
PA = Base + d
|
||||
段页式
|
||||
先分段再分页
|
||||
三维地址: 段号+页号+偏移
|
||||
兼具两者优点
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 思考题
|
||||
|
||||
1. **概念理解**: 分段和分页的根本区别是什么?为什么说分段的地址空间是二维的?
|
||||
|
||||
2. **地址变换**: 某分段系统有 4 个段,段表如下。求物理地址或判断是否越界:
|
||||
- (0, 0x800) → ?
|
||||
- (2, 0x5000) → ?
|
||||
- (3, 0x2000) → ?
|
||||
|
||||
| 段号 | 段长 | 基址 |
|
||||
|------|------|------|
|
||||
| 0 | 0x1000 | 0x5000 |
|
||||
| 1 | 0x2000 | 0x8000 |
|
||||
| 2 | 0x4000 | 0xA000 |
|
||||
| 3 | 0x3000 | 0x2000 |
|
||||
|
||||
3. **段页式**: 为什么段页式需要 3 次内存访问?TLB 如何缓解这个问题?
|
||||
|
||||
4. **对比分析**: 在什么场景下分段优于分页?在什么场景下分页优于分段?
|
||||
|
||||
---
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[13_存储管理基础]] — 存储器层次结构与地址空间基础
|
||||
- [[14_分页存储管理]] — 分页思想、页表与地址变换
|
||||
- [[16_虚拟存储器]] — 基于分段的虚拟存储器实现
|
||||
|
||||
---
|
||||
|
||||
**上一讲**: [[14_分页存储管理]]
|
||||
**下一讲**: [[16_虚拟存储器]]
|
||||
|
||||
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
257
操作系统/2026年操作系统期末预测卷.md
Normal file
257
操作系统/2026年操作系统期末预测卷.md
Normal 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. 反向SCAN:53→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.8,SJF平均周转6.2,HRRF平均周转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周转=23,P2周转=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次访存
|
||||
688
操作系统/复习与教学笔记(加强版).md
Normal file
688
操作系统/复习与教学笔记(加强版).md
Normal 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 ✓缺页 换掉最久未用的1(1离上次最远)
|
||||
0 7 0 2 ✗命中 0已存在,更新它的使用时间
|
||||
3 3 0 2 ✓缺页 换掉最久未用的7(7离上次最远)
|
||||
0 3 0 2 ✗命中 0已存在
|
||||
4 3 0 4 ✓缺页 换掉最久未用的2(2离上次最远)
|
||||
...以此类推
|
||||
```
|
||||
|
||||
#### ⚠️ 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()` 就像细胞分裂 — 调用一次,变成两个进程,从同一行代码继续执行。
|
||||
|
||||
#### 黄金法则
|
||||
|
||||
```
|
||||
法则1:fork() 一次,返回两次
|
||||
- 父进程返回值 = 子进程的 PID
|
||||
- 子进程返回值 = 0
|
||||
|
||||
法则2:子进程从 fork() 的下一行开始执行(不是从 main 开头)
|
||||
|
||||
法则3:fork() 嵌套时,每个进程都要独立分析它的执行路径
|
||||
|
||||
法则4:父进程总数 = 2^n(n为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(父)
|
||||
```
|
||||
|
||||
#### 经典模式 3:fork + 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) |
|
||||
| CFS(Linux) | 红黑树 | 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,它会怎么做?"——这是最好的学习方式。
|
||||
>
|
||||
> 祝考试顺利!🎯
|
||||
626
操作系统/期末知识点总结与考点分析.md
Normal file
626
操作系统/期末知识点总结与考点分析.md
Normal 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操作(wait):S--,若S<0则阻塞
|
||||
- V操作(signal):S++,若有等待者则唤醒
|
||||
- **实现同步和互斥**
|
||||
|
||||
- **生产者消费者模型**:
|
||||
```
|
||||
信号量:
|
||||
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 / P(P为处理器数量)
|
||||
- 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^n(n为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、周转时间、响应比等
|
||||
BIN
操作系统/试卷/20-21操作系统a卷.pdf
Normal file
BIN
操作系统/试卷/20-21操作系统a卷.pdf
Normal file
Binary file not shown.
BIN
操作系统/试卷/2023A卷参考答案.pdf
Normal file
BIN
操作系统/试卷/2023A卷参考答案.pdf
Normal file
Binary file not shown.
BIN
操作系统/试卷/2023B卷参考答案.docx
Normal file
BIN
操作系统/试卷/2023B卷参考答案.docx
Normal file
Binary file not shown.
BIN
操作系统/试卷/2023年A卷(统稿).pdf
Normal file
BIN
操作系统/试卷/2023年A卷(统稿).pdf
Normal file
Binary file not shown.
BIN
操作系统/试卷/2023年B卷(统稿).doc
Normal file
BIN
操作系统/试卷/2023年B卷(统稿).doc
Normal file
Binary file not shown.
BIN
操作系统/试卷/2025年A卷试卷.pdf
Normal file
BIN
操作系统/试卷/2025年A卷试卷.pdf
Normal file
Binary file not shown.
BIN
操作系统/试卷/2025年B卷试卷.pdf
Normal file
BIN
操作系统/试卷/2025年B卷试卷.pdf
Normal file
Binary file not shown.
BIN
操作系统/试卷/2025操作系统A卷参考答案.pdf
Normal file
BIN
操作系统/试卷/2025操作系统A卷参考答案.pdf
Normal file
Binary file not shown.
BIN
操作系统/试卷/2025操作系统B卷参考答案及评分标准.docx
Normal file
BIN
操作系统/试卷/2025操作系统B卷参考答案及评分标准.docx
Normal file
Binary file not shown.
BIN
操作系统/试卷/21-22网安学院操作系统A卷.pdf
Normal file
BIN
操作系统/试卷/21-22网安学院操作系统A卷.pdf
Normal file
Binary file not shown.
BIN
操作系统/试卷/4cec9c0a5e1b0615426df487f9a6716d.docx
Normal file
BIN
操作系统/试卷/4cec9c0a5e1b0615426df487f9a6716d.docx
Normal file
Binary file not shown.
BIN
操作系统/试卷/d6fdbce11aa9feef6f246e73ed09a00e.docx
Normal file
BIN
操作系统/试卷/d6fdbce11aa9feef6f246e73ed09a00e.docx
Normal file
Binary file not shown.
BIN
操作系统/试卷/f9c2aa5324dd828dfa7840fc2458f98d.doc
Normal file
BIN
操作系统/试卷/f9c2aa5324dd828dfa7840fc2458f98d.doc
Normal file
Binary file not shown.
BIN
操作系统/试卷/操作系统期中考试参考答案2025-2026-2.docx
Normal file
BIN
操作系统/试卷/操作系统期中考试参考答案2025-2026-2.docx
Normal file
Binary file not shown.
BIN
操作系统/试卷/操作系统期中考试试卷.pdf
Normal file
BIN
操作系统/试卷/操作系统期中考试试卷.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user