vault backup: 2026-06-13 23:46:22
This commit is contained in:
868
操作系统/06_进程控制/06_进程控制.md
Normal file
868
操作系统/06_进程控制/06_进程控制.md
Normal file
@@ -0,0 +1,868 @@
|
||||
# 第06讲:进程控制
|
||||
|
||||
> **本节目标**:理解进程的概念与生命周期,掌握 `fork()`、`exec()`、`wait()`、`exit()` 等进程控制系统调用,理解信号机制与僵尸进程,最终能实现一个简易 shell 和 daemon 进程。
|
||||
|
||||
## 前置知识
|
||||
|
||||
- [[03_C语言编程基础]] -- C 语言指针、数组、字符串操作
|
||||
- [[07_多线程编程]] -- 线程与进程的对比
|
||||
|
||||
---
|
||||
|
||||
## 一、进程概念
|
||||
|
||||
### 1.1 什么是进程
|
||||
|
||||
**进程(Process)** 是程序的一次执行实例。程序是静态的代码文件,进程是动态的执行过程。当操作系统将程序加载到内存并开始执行时,就创建了一个进程。
|
||||
|
||||
每个进程拥有独立的地址空间,包含以下组成部分:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
P[进程] --> T[代码段 Text]
|
||||
P --> D[数据段 Data]
|
||||
P --> H[堆 Heap]
|
||||
P --> S[栈 Stack]
|
||||
P --> PCB[PCB 进程控制块]
|
||||
|
||||
T -->|存放| T1[程序指令]
|
||||
D -->|存放| D1[全局变量/静态变量]
|
||||
H -->|动态分配| H1[malloc/free]
|
||||
S -->|自动管理| S1[局部变量/函数调用]
|
||||
PCB -->|记录| PCB1[PID/状态/寄存器/文件描述符]
|
||||
|
||||
style P fill:#fff3e0
|
||||
style PCB fill:#e1f5fe
|
||||
```
|
||||
|
||||
### 1.2 进程 vs 程序
|
||||
|
||||
| 对比维度 | 程序(Program) | 进程(Process) |
|
||||
|----------|-----------------|-----------------|
|
||||
| 本质 | 静态的代码文件 | 动态的执行过程 |
|
||||
| 存储位置 | 硬盘 | 内存 |
|
||||
| 生命周期 | 长期保存 | 有创建、运行、终止 |
|
||||
| 对应关系 | 一个程序可对应多个进程 | 一个进程只对应一个程序 |
|
||||
| 包含内容 | 代码 + 数据 | 代码 + 数据 + 堆栈 + PCB |
|
||||
|
||||
### 1.3 进程状态
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> 创建: fork()
|
||||
创建 --> 就绪: 分配资源
|
||||
就绪 --> 运行: 调度器选中
|
||||
运行 --> 就绪: 时间片用完
|
||||
运行 --> 阻塞: 等待I/O
|
||||
阻塞 --> 就绪: I/O完成
|
||||
运行 --> 终止: exit()
|
||||
终止 --> [*]
|
||||
|
||||
就绪: Ready
|
||||
运行: Running
|
||||
阻塞: Blocked/Waiting
|
||||
终止: Terminated/Zombie
|
||||
```
|
||||
|
||||
### 1.4 进程标识
|
||||
|
||||
每个进程都有唯一的 **PID**(Process ID)。常用函数:
|
||||
|
||||
```c
|
||||
pid_t getpid(); // 获取当前进程的 PID
|
||||
pid_t getppid(); // 获取父进程的 PID
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、fork() -- 创建子进程
|
||||
|
||||
### 2.1 基本原理
|
||||
|
||||
`fork()` 是 Linux 中创建新进程的唯一方式。调用 `fork()` 后,操作系统会创建一个与父进程几乎完全相同的子进程。子进程获得父进程的代码段、数据段、堆栈的**副本**。
|
||||
|
||||
**关键特性**:`fork()` 被调用一次,但**返回两次**:
|
||||
- 父进程中返回子进程的 PID(正整数)
|
||||
- 子进程中返回 0
|
||||
- 如果出错返回 -1
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["父进程调用 fork()"] --> B{操作系统创建子进程}
|
||||
B --> C["父进程:返回子进程 PID(>0)"]
|
||||
B --> D["子进程:返回 0"]
|
||||
C --> E["父子进程各自独立执行"]
|
||||
D --> E
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style C fill:#fff3e0
|
||||
style D fill:#e8f5e9
|
||||
```
|
||||
|
||||
### 2.2 fork 基础示例
|
||||
|
||||
参考 `实例源代码/chap5/fork1.c`:
|
||||
|
||||
```c
|
||||
#include "wrapper.h"
|
||||
int main()
|
||||
{
|
||||
pid_t pid;
|
||||
int x = 1;
|
||||
|
||||
pid = fork();
|
||||
if (pid == 0) { // 子进程执行这段代码
|
||||
x = x + 1;
|
||||
printf("child: x=%d\n", x); // 输出 child: x=2
|
||||
}
|
||||
|
||||
if (pid > 0) { // 父进程执行这段代码
|
||||
x = x - 1;
|
||||
printf("parent: x=%d\n", x); // 输出 parent: x=0
|
||||
}
|
||||
sleep(10); // 让父子进程都执行完代码
|
||||
}
|
||||
```
|
||||
|
||||
**关键理解**:
|
||||
- `fork()` 后,父子进程拥有**独立的变量副本**
|
||||
- 子进程修改 `x` 不影响父进程的 `x`,反之亦然
|
||||
- 父子进程的执行顺序是不确定的,取决于调度器
|
||||
|
||||
### 2.3 多次 fork 的进程数计算
|
||||
|
||||
参考 `实例源代码/chap5/fork3.c`:
|
||||
|
||||
```c
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
int main()
|
||||
{
|
||||
fork(); // 第1次:2个进程
|
||||
fork(); // 第2次:4个进程
|
||||
fork(); // 第3次:8个进程
|
||||
printf("hello \n");
|
||||
sleep(10);
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**进程树分析**:n 次 `fork()` 会产生 2^n 个进程。上面的代码执行 3 次 `fork()`,最终产生 2^3 = 8 个进程,每个进程打印一次 "hello"。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
P0["P0 (原始进程)"] -->|第1次fork| P1["P1"]
|
||||
P0 -->|继续| P0a["P0"]
|
||||
P0a -->|第2次fork| P2["P2"]
|
||||
P0a -->|继续| P0b["P0"]
|
||||
P1 -->|第2次fork| P3["P3"]
|
||||
P1 -->|继续| P1a["P1"]
|
||||
P0b -->|第3次fork| P4["P4"]
|
||||
P1a -->|第3次fork| P5["P5"]
|
||||
P2 -->|第3次fork| P6["P6"]
|
||||
P3 -->|第3次fork| P7["P7"]
|
||||
|
||||
style P0 fill:#ffcdd2
|
||||
style P1 fill:#e1f5fe
|
||||
style P2 fill:#e1f5fe
|
||||
style P3 fill:#e1f5fe
|
||||
style P4 fill:#e8f5e9
|
||||
style P5 fill:#e8f5e9
|
||||
style P6 fill:#e8f5e9
|
||||
style P7 fill:#e8f5e9
|
||||
```
|
||||
|
||||
### 2.4 fork 的复杂示例
|
||||
|
||||
参考 `实例源代码/chap5/fork2.c`:
|
||||
|
||||
```c
|
||||
#include <unistd.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
int main()
|
||||
{
|
||||
int pid;
|
||||
pid = fork(); // 创建 P1
|
||||
pid = fork(); // P0 和 P1 各创建一个子进程
|
||||
if (pid > 0) fork(); // pid>0 的进程再创建一个子进程
|
||||
printf("hello \n");
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
**分析**:需要追踪每个进程的 `pid` 值来确定哪些进程会执行第三次 `fork()`。最终打印 6 个 "hello"。
|
||||
|
||||
### 2.5 fork 的注意事项
|
||||
|
||||
| 要点 | 说明 |
|
||||
|------|------|
|
||||
| 返回值判断 | 必须用 `if-else` 分别处理父子进程 |
|
||||
| 资源复制 | 子进程获得父进程的副本(写时复制 COW 优化) |
|
||||
| 执行顺序 | 父子进程执行顺序不确定 |
|
||||
| 文件描述符 | 子进程继承父进程打开的文件描述符 |
|
||||
| 必须 exit | 子进程处理完后必须调用 `exit()` 终止 |
|
||||
|
||||
---
|
||||
|
||||
## 三、exec 族函数 -- 替换进程映像
|
||||
|
||||
### 3.1 基本原理
|
||||
|
||||
`exec` 族函数用一个新的程序**替换**当前进程的代码段、数据段和堆栈。进程的 PID 不变,但执行的代码完全改变。`exec` 调用成功后**不会返回**,只有出错时才返回 -1。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["当前进程<br/>PID=1234<br/>执行 myprogram"] -->|execvp("ps", args)| B["同一进程<br/>PID=1234<br/>执行 ps 命令"]
|
||||
B --> C["原来的代码段<br/>被完全替换"]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style B fill:#e8f5e9
|
||||
```
|
||||
|
||||
### 3.2 exec 族函数对比
|
||||
|
||||
| 函数 | 参数形式 | 路径搜索 | 环境变量 |
|
||||
|------|----------|----------|----------|
|
||||
| `execl(path, arg0, ..., NULL)` | 列表 | 完整路径 | 继承父进程 |
|
||||
| `execlp(file, arg0, ..., NULL)` | 列表 | 搜索 PATH | 继承父进程 |
|
||||
| `execle(path, arg0, ..., NULL, envp)` | 列表 | 完整路径 | 自定义 |
|
||||
| `execv(path, argv)` | 数组 | 完整路径 | 继承父进程 |
|
||||
| `execvp(file, argv)` | 数组 | 搜索 PATH | 继承父进程 |
|
||||
|
||||
**记忆技巧**:
|
||||
- `l` = list(参数列表),`v` = vector(参数数组)
|
||||
- `p` = path(搜索 PATH 环境变量),`e` = environment(自定义环境变量)
|
||||
|
||||
### 3.3 exec 示例
|
||||
|
||||
参考 `实例源代码/chap5/exec1.c`:
|
||||
|
||||
```c
|
||||
#include "wrapper.h"
|
||||
int main(void)
|
||||
{
|
||||
char *arg[] = {"ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL};
|
||||
execvp("ps", arg); // 用 ps 命令替换当前进程
|
||||
perror("exec ps"); // 如果 execvp 返回,说明出错
|
||||
exit(1);
|
||||
}
|
||||
```
|
||||
|
||||
**运行结果**:程序执行后变成了 `ps` 命令,显示当前系统的进程信息。
|
||||
|
||||
### 3.4 fork + exec 组合
|
||||
|
||||
在实际应用中,`fork()` 和 `exec()` 通常配合使用:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 父进程
|
||||
participant 子进程
|
||||
participant 新程序
|
||||
|
||||
父进程->>子进程: fork()
|
||||
Note over 子进程: 子进程是父进程的副本
|
||||
子进程->>新程序: execvp("ls", args)
|
||||
Note over 新程序: 子进程的代码被 ls 替换
|
||||
新程序-->>父进程: 执行完毕
|
||||
父进程->>父进程: wait() 回收子进程
|
||||
```
|
||||
|
||||
这种模式是 shell 执行命令的核心机制:先 `fork` 创建子进程,再在子进程中 `exec` 执行新程序。
|
||||
|
||||
---
|
||||
|
||||
## 四、wait/waitpid -- 等待子进程
|
||||
|
||||
### 4.1 为什么需要 wait
|
||||
|
||||
父进程创建子进程后,需要等待子进程结束并回收其资源。如果不调用 `wait`,子进程终止后会变成**僵尸进程**(详见第六节)。
|
||||
|
||||
### 4.2 wait 与 waitpid
|
||||
|
||||
| 函数 | 说明 |
|
||||
|------|------|
|
||||
| `wait(&status)` | 等待任意一个子进程结束 |
|
||||
| `waitpid(pid, &status, options)` | 可指定等待的子进程 |
|
||||
| `WIFEXITED(status)` | 判断子进程是否正常退出 |
|
||||
| `WEXITSTATUS(status)` | 获取子进程的退出状态 |
|
||||
|
||||
**waitpid 的 pid 参数**:
|
||||
- `pid > 0`:等待指定 PID 的子进程
|
||||
- `pid = -1`:等待任意子进程(等同于 `wait`)
|
||||
- `pid = 0`:等待同一进程组的任意子进程
|
||||
- `pid < -1`:等待进程组 ID 为 |pid| 的任意子进程
|
||||
|
||||
### 4.3 waitpid 示例
|
||||
|
||||
参考 `实例源代码/chap5/waitpid1.c`:
|
||||
|
||||
```c
|
||||
#include "wrapper.h"
|
||||
#define N 2
|
||||
|
||||
int main()
|
||||
{
|
||||
int status, i;
|
||||
pid_t pid;
|
||||
|
||||
// 父进程创建 N 个子进程
|
||||
for (i = 0; i < 2; i++)
|
||||
if ((pid = fork()) == 0) // 子进程
|
||||
exit(100 + i); // 以不同状态退出
|
||||
|
||||
// 父进程按任意顺序等待所有子进程
|
||||
while ((pid = waitpid(-1, &status, 0)) > 0) {
|
||||
if (WIFEXITED(status))
|
||||
printf("child %d terminated normally with exit status=%d\n",
|
||||
pid, WEXITSTATUS(status));
|
||||
else
|
||||
printf("child %d terminated abnormally\n", pid);
|
||||
}
|
||||
|
||||
// 所有子进程已结束,waitpid 返回 -1,errno 为 ECHILD
|
||||
if (errno != ECHILD)
|
||||
perror("waitpid error");
|
||||
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
**输出示例**:
|
||||
```
|
||||
child 1235 terminated normally with exit status=100
|
||||
child 1236 terminated normally with exit status=101
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、exit/_exit -- 进程终止
|
||||
|
||||
### 5.1 两种终止方式
|
||||
|
||||
| 函数 | 头文件 | 行为 |
|
||||
|------|--------|------|
|
||||
| `exit(status)` | `<stdlib.h>` | 执行清理工作(刷新缓冲区、调用 atexit 注册的函数),然后终止 |
|
||||
| `_exit(status)` | `<unistd.h>` | 立即终止,不执行任何清理 |
|
||||
|
||||
### 5.2 exit 的清理过程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["调用 exit(status)"] --> B["执行 atexit() 注册的清理函数"]
|
||||
B --> C["刷新 stdio 缓冲区<br/>(fclose 所有打开的流)"]
|
||||
C --> D["调用 _exit(status)"]
|
||||
D --> E["内核回收进程资源"]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style E fill:#ffcdd2
|
||||
```
|
||||
|
||||
### 5.3 退出状态
|
||||
|
||||
参考 `实例源代码/chap5/exitstatus.c`:
|
||||
|
||||
```c
|
||||
#include <stdlib.h>
|
||||
int main() { exit(100); }
|
||||
```
|
||||
|
||||
父进程通过 `waitpid` 的 `WEXITSTATUS(status)` 宏获取子进程的退出状态值(0-255)。
|
||||
|
||||
---
|
||||
|
||||
## 六、僵尸进程
|
||||
|
||||
### 6.1 什么是僵尸进程
|
||||
|
||||
当子进程终止后,如果父进程没有调用 `wait()` 或 `waitpid()` 回收子进程的退出状态,子进程的进程控制块(PCB)仍然保留在系统中,成为**僵尸进程(Zombie Process)**。
|
||||
|
||||
### 6.2 僵尸进程的产生
|
||||
|
||||
参考 `实例源代码/chap5/zombie.c`:
|
||||
|
||||
```c
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
int main()
|
||||
{
|
||||
pid_t pid;
|
||||
if ((pid = fork()) < 0) {
|
||||
perror("fork failed");
|
||||
exit(1);
|
||||
} else if (pid == 0) {
|
||||
printf("child...\n"); // 子进程打印后退出
|
||||
} else {
|
||||
printf("parent...\n");
|
||||
while (1); // 父进程不调用 wait(),子进程成为僵尸
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 僵尸进程的生命周期
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 父进程
|
||||
participant 子进程
|
||||
participant 操作系统内核
|
||||
|
||||
父进程->>子进程: fork() 创建子进程
|
||||
子进程->>子进程: 执行任务
|
||||
子进程->>操作系统内核: exit() 终止
|
||||
操作系统内核->>父进程: 发送 SIGCHLD 信号
|
||||
Note over 父进程: 父进程没有调用 wait()
|
||||
Note over 操作系统内核: 子进程的 PCB 无法释放
|
||||
Note over 操作系统内核: 子进程成为僵尸进程
|
||||
Note over 操作系统内核: 僵尸进程占用 PID 资源
|
||||
```
|
||||
|
||||
### 6.4 僵尸进程的危害与解决
|
||||
|
||||
| 危害 | 说明 |
|
||||
|------|------|
|
||||
| 占用 PID | 僵尸进程的 PID 无法被复用 |
|
||||
| 资源泄漏 | 虽然不占内存,但 PCB 信息占用内核资源 |
|
||||
| 累积效应 | 大量僵尸进程可耗尽 PID 资源 |
|
||||
|
||||
**解决方法**:
|
||||
1. 父进程调用 `wait()` 或 `waitpid()` 回收子进程
|
||||
2. 注册 `SIGCHLD` 信号处理函数,在其中调用 `waitpid()`
|
||||
3. 父进程先退出,让 init 进程(PID=1)自动回收孤儿进程
|
||||
|
||||
---
|
||||
|
||||
## 七、信号机制
|
||||
|
||||
### 7.1 信号的概念
|
||||
|
||||
信号是操作系统通知进程发生了某种事件的一种**异步通信机制**。进程可以注册信号处理函数来响应特定信号。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
SRC["信号来源"] --> SIG["信号"]
|
||||
SIG --> PROC["目标进程"]
|
||||
|
||||
SRC --> S1["用户输入 Ctrl+C"]
|
||||
SRC --> S2["kill 命令"]
|
||||
SRC --> S3["子进程终止"]
|
||||
SRC --> S4["定时器超时"]
|
||||
SRC --> S5["非法内存访问"]
|
||||
|
||||
PROC --> H1["执行信号处理函数"]
|
||||
PROC --> H2["忽略信号"]
|
||||
PROC --> H3["执行默认动作"]
|
||||
|
||||
style SIG fill:#fff3e0
|
||||
style SRC fill:#e1f5fe
|
||||
style PROC fill:#e8f5e9
|
||||
```
|
||||
|
||||
### 7.2 常见信号
|
||||
|
||||
| 信号 | 编号 | 默认动作 | 说明 |
|
||||
|------|:----:|----------|------|
|
||||
| `SIGINT` | 2 | 终止 | 用户按 Ctrl+C |
|
||||
| `SIGKILL` | 9 | 终止 | 强制终止(**不可捕获**) |
|
||||
| `SIGTERM` | 15 | 终止 | 请求终止(可捕获) |
|
||||
| `SIGCHLD` | 17 | 忽略 | 子进程状态改变 |
|
||||
| `SIGALRM` | 14 | 终止 | 定时器超时 |
|
||||
| `SIGSEGV` | 11 | 终止+core | 段错误 |
|
||||
| `SIGSTOP` | 19 | 停止 | 暂停进程(**不可捕获**) |
|
||||
| `SIGCONT` | 18 | 继续 | 恢复被暂停的进程 |
|
||||
|
||||
### 7.3 signal() 注册信号处理函数
|
||||
|
||||
参考 `实例源代码/chap5/signal1.c`:
|
||||
|
||||
```c
|
||||
#include "wrapper.h"
|
||||
|
||||
void handler1(int sig)
|
||||
{
|
||||
pid_t pid;
|
||||
if ((pid = waitpid(-1, NULL, 0)) < 0)
|
||||
perror("waitpid error");
|
||||
printf("Handler reaped child %d\n", (int)pid);
|
||||
sleep(2);
|
||||
return;
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
int i, n;
|
||||
char buf[MAXBUF];
|
||||
|
||||
// 注册 SIGCHLD 信号处理函数
|
||||
if (signal(SIGCHLD, handler1) == SIG_ERR)
|
||||
perror("signal error");
|
||||
|
||||
// 父进程创建 3 个子进程
|
||||
for (i = 0; i < 3; i++) {
|
||||
if (fork() == 0) {
|
||||
printf("Hello from child %d\n", (int)getpid());
|
||||
sleep(1);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// 父进程等待终端输入
|
||||
if ((n = read(STDIN_FILENO, buf, sizeof(buf))) < 0)
|
||||
perror("read");
|
||||
|
||||
printf("Parent processing input\n");
|
||||
while (1);
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
**信号处理流程**:
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 父进程
|
||||
participant 子进程1
|
||||
participant 子进程2
|
||||
participant 子进程3
|
||||
participant 信号处理函数
|
||||
|
||||
父进程->>信号处理函数: signal(SIGCHLD, handler1)
|
||||
父进程->>子进程1: fork()
|
||||
父进程->>子进程2: fork()
|
||||
父进程->>子进程3: fork()
|
||||
|
||||
子进程1->>父进程: exit() 触发 SIGCHLD
|
||||
父进程->>信号处理函数: 调用 handler1()
|
||||
信号处理函数->>信号处理函数: waitpid() 回收子进程
|
||||
|
||||
子进程2->>父进程: exit() 触发 SIGCHLD
|
||||
父进程->>信号处理函数: 调用 handler1()
|
||||
信号处理函数->>信号处理函数: waitpid() 回收子进程
|
||||
|
||||
子进程3->>父进程: exit() 触发 SIGCHLD
|
||||
父进程->>信号处理函数: 调用 handler1()
|
||||
信号处理函数->>信号处理函数: waitpid() 回收子进程
|
||||
```
|
||||
|
||||
### 7.4 SIGALRM 定时器
|
||||
|
||||
参考 `实例源代码/chap5/alarm.c`:
|
||||
|
||||
```c
|
||||
#include "wrapper.h"
|
||||
|
||||
void handler(int sig)
|
||||
{
|
||||
static int beeps = 0;
|
||||
printf("BEEP\n");
|
||||
if (++beeps < 5)
|
||||
alarm(1); // 1秒后再次触发 SIGALRM
|
||||
else {
|
||||
printf("BOOM!\n");
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
signal(SIGALRM, handler); // 注册 SIGALRM 处理函数
|
||||
alarm(1); // 1秒后触发第一次 SIGALRM
|
||||
while (1); // 等待信号
|
||||
exit(0);
|
||||
}
|
||||
```
|
||||
|
||||
**输出**:每隔 1 秒打印一次 "BEEP",第 5 次后打印 "BOOM!" 并退出。
|
||||
|
||||
---
|
||||
|
||||
## 八、Shell 的实现
|
||||
|
||||
### 8.1 Shell 的工作原理
|
||||
|
||||
Shell 是一个命令解释器,其核心逻辑可以用一个循环概括:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["打印提示符 %"] --> B["读取用户输入命令"]
|
||||
B --> C{"命令是否为空?"}
|
||||
C -->|是| A
|
||||
C -->|否| D{"是否为内置命令?"}
|
||||
D -->|是 exit| E["退出 shell"]
|
||||
D -->|是其他| F["执行内置命令"]
|
||||
F --> A
|
||||
D -->|否| G["fork() 创建子进程"]
|
||||
G --> H["子进程: execvp() 执行命令"]
|
||||
G --> I["父进程: waitpid() 等待子进程"]
|
||||
H --> A
|
||||
I --> A
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style E fill:#ffcdd2
|
||||
style G fill:#e8f5e9
|
||||
```
|
||||
|
||||
### 8.2 shellex.c 核心实现
|
||||
|
||||
参考 `实例源代码/chap5/shellex.c`,这是一个完整的简易 shell 实现:
|
||||
|
||||
```c
|
||||
#include "wrapper.h"
|
||||
#define MAXARGS 128
|
||||
|
||||
// 解析命令行,返回是否为后台作业
|
||||
int parseline(char *buf, char **argv)
|
||||
{
|
||||
char *delim;
|
||||
int argc;
|
||||
int bg;
|
||||
|
||||
buf[strlen(buf)-1] = ' '; // 用空格替换末尾换行
|
||||
while (*buf && (*buf == ' ')) // 跳过前导空格
|
||||
buf++;
|
||||
|
||||
argc = 0;
|
||||
while ((delim = strchr(buf, ' '))) {
|
||||
argv[argc++] = buf;
|
||||
*delim = '\0';
|
||||
buf = delim + 1;
|
||||
while (*buf && (*buf == ' '))
|
||||
buf++;
|
||||
}
|
||||
argv[argc] = NULL;
|
||||
|
||||
if (argc == 0) return 1;
|
||||
|
||||
// 检查是否应在后台执行(最后一个参数为 &)
|
||||
if ((bg = (*argv[argc-1] == '&')) != 0)
|
||||
argv[--argc] = NULL;
|
||||
return bg;
|
||||
}
|
||||
|
||||
// 执行命令
|
||||
void execute(char *cmdline)
|
||||
{
|
||||
char *argv[MAXARGS];
|
||||
char buf[MAXLINE];
|
||||
int bg;
|
||||
pid_t pid;
|
||||
|
||||
strcpy(buf, cmdline);
|
||||
bg = parseline(buf, argv);
|
||||
if (argv[0] == NULL) return;
|
||||
|
||||
if (!builtin_command(argv)) {
|
||||
if ((pid = fork()) == 0) { // 子进程
|
||||
if (execvp(argv[0], argv) < 0) {
|
||||
printf("%s: Command not found.\n", argv[0]);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
if (!bg) { // 前台执行
|
||||
int status;
|
||||
if (waitpid(pid, &status, 0) < 0)
|
||||
perror("waitpid error");
|
||||
}
|
||||
else // 后台执行
|
||||
printf("%d %s", pid, cmdline);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 判断内置命令(如 exit)
|
||||
int builtin_command(char **argv)
|
||||
{
|
||||
if (!strcmp(argv[0], "exit"))
|
||||
exit(0);
|
||||
if (!strcmp(argv[0], "&"))
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main()
|
||||
{
|
||||
char cmdline[MAXLINE];
|
||||
while (1) {
|
||||
printf("%% "); // 打印提示符
|
||||
fgets(cmdline, MAXLINE, stdin); // 读取命令
|
||||
if (feof(stdin)) exit(0);
|
||||
execute(cmdline); // 执行命令
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 8.3 Shell 执行流程图
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 用户
|
||||
participant Shell主进程
|
||||
participant 子进程
|
||||
participant 操作系统
|
||||
|
||||
loop Shell 主循环
|
||||
用户->>Shell主进程: 输入 "ls -l"
|
||||
Shell主进程->>Shell主进程: parseline() 解析命令
|
||||
Shell主进程->>子进程: fork() 创建子进程
|
||||
子进程->>操作系统: execvp("ls", ["ls", "-l", NULL])
|
||||
Note over 操作系统: ls 程序替换子进程
|
||||
操作系统-->>子进程: ls 执行完毕
|
||||
子进程->>Shell主进程: exit()
|
||||
Shell主进程->>Shell主进程: waitpid() 回收子进程
|
||||
Shell主进程-->>用户: 显示结果,打印提示符
|
||||
end
|
||||
```
|
||||
|
||||
### 8.4 前台进程与后台进程
|
||||
|
||||
- **前台进程**:Shell 调用 `waitpid()` 等待子进程结束后才继续接受输入
|
||||
- **后台进程**:Shell 不等待,直接打印子进程 PID 并继续接受输入(命令末尾加 `&`)
|
||||
|
||||
### 8.5 管道与重定向
|
||||
|
||||
实验中的 `task52.c` 实现了管道和重定向功能:
|
||||
|
||||
**输出重定向**:使用 `dup2()` 将标准输出重定向到文件
|
||||
```c
|
||||
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
|
||||
dup2(fd, STDOUT_FILENO); // 将 STDOUT 重定向到文件
|
||||
close(fd);
|
||||
execvp(args[0], args); // 执行命令,输出写入文件
|
||||
```
|
||||
|
||||
**管道**:使用 `pipe()` 创建管道,连接两个进程的输入输出
|
||||
```c
|
||||
int pipefd[2];
|
||||
pipe(pipefd); // pipefd[0]=读端, pipefd[1]=写端
|
||||
|
||||
// 子进程1: 写入管道
|
||||
dup2(pipefd[1], STDOUT_FILENO);
|
||||
close(pipefd[0]);
|
||||
execvp(cmd1_args[0], cmd1_args);
|
||||
|
||||
// 子进程2: 从管道读取
|
||||
dup2(pipefd[0], STDIN_FILENO);
|
||||
close(pipefd[1]);
|
||||
execvp(cmd2_args[0], cmd2_args);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、Daemon 进程
|
||||
|
||||
### 9.1 什么是 Daemon
|
||||
|
||||
Daemon(守护进程)是在后台运行的特殊进程,通常在系统启动时创建,一直运行到系统关闭。常见的 daemon 有:`sshd`(SSH 服务)、`httpd`(Web 服务)、`crond`(定时任务)等。
|
||||
|
||||
### 9.2 Daemon 的特点
|
||||
|
||||
- 没有控制终端(不与任何终端关联)
|
||||
- 在后台运行
|
||||
- 父进程是 init(PID=1)
|
||||
- 通常以 `d` 结尾命名
|
||||
|
||||
### 9.3 创建 Daemon 的五步法
|
||||
|
||||
参考 `实例源代码/chap5/daemon.c`:
|
||||
|
||||
```c
|
||||
int init_daemon(void)
|
||||
{
|
||||
pid_t pid;
|
||||
int i;
|
||||
|
||||
// 第1步:fork() + 父进程退出,脱离终端控制
|
||||
pid = fork();
|
||||
if (pid == -1) return -1;
|
||||
else if (pid != 0) exit(EXIT_SUCCESS);
|
||||
|
||||
// 第2步:setsid() 创建新会话,成为会话首进程
|
||||
if (setsid() == -1) return -1;
|
||||
|
||||
// 第3步:(可选) 再次 fork(),确保不会重新获得控制终端
|
||||
// daemon.c 中省略了这一步,但 task53.c 中包含
|
||||
|
||||
// 第4步:chdir("/") 避免占用可卸载的文件系统
|
||||
if (chdir("/") == -1) return -1;
|
||||
|
||||
// 第5步:关闭所有打开的文件描述符,重定向 0/1/2 到 /dev/null
|
||||
for (i = 0; i < NR_OPEN; i++) close(i);
|
||||
open("/dev/null", O_RDWR); // stdin -> /dev/null
|
||||
dup(0); // stdout -> /dev/null
|
||||
dup(0); // stderr -> /dev/null
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
**五步法流程图**:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["第1步: fork() + 父进程 exit()"] --> B["子进程成为孤儿进程<br/>被 init 收养"]
|
||||
B --> C["第2步: setsid()"]
|
||||
C --> D["创建新会话<br/>成为会话首进程<br/>脱离控制终端"]
|
||||
D --> E["第3步: fork() + 父进程 exit()"]
|
||||
E --> F["确保不会重新获得<br/>控制终端"]
|
||||
F --> G["第4步: chdir('/')"]
|
||||
G --> H["避免占用可卸载的<br/>文件系统"]
|
||||
H --> I["第5步: 关闭 0/1/2<br/>重定向到 /dev/null"]
|
||||
I --> J["Daemon 创建完成<br/>在后台运行"]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style J fill:#e8f5e9
|
||||
```
|
||||
|
||||
### 9.4 Daemon 文件监控实例
|
||||
|
||||
实验中的 `task53.c` 实现了一个 daemon 文件监控程序:
|
||||
1. 创建守护进程
|
||||
2. 每隔 5 分钟读取目标文件内容,计算 hash 值
|
||||
3. 与上一次的 hash 值对比
|
||||
4. 如果文件被篡改,将事件记录到日志文件
|
||||
|
||||
---
|
||||
|
||||
## 十、实验任务概览
|
||||
|
||||
本讲对应 [[实验02_进程控制]],包含以下任务:
|
||||
|
||||
| 任务 | 文件 | 内容 | 核心知识点 |
|
||||
|------|------|------|------------|
|
||||
| 任务一 | task51.c | 创建进程族亲结构(p1 -> p11, p12 -> p121, p122) | fork() + wait() + 进程树 |
|
||||
| 任务二 | task52.c | 简单 shell 实现(命令解析、重定向、管道) | fork() + exec() + dup2() + pipe() |
|
||||
| 任务三 | task53.c | daemon 文件监控(hash 值检测篡改) | daemon 五步法 + 文件 I/O |
|
||||
| 任务四 | task54.c | 信号机制管理子进程(create/kill/ps/exit) | signal() + kill() + SIGCHLD |
|
||||
|
||||
---
|
||||
|
||||
## 知识关联
|
||||
|
||||
- 进程状态转换在处理机调度中有更详细的讨论,参见相关课件
|
||||
- 信号机制与进程间通信密切相关,参见 [[08_进程间通信]]
|
||||
- Shell 的并发版本(多进程并发服务器)在网络编程中会深入讨论
|
||||
- 线程可以看作轻量级进程,参见 [[07_多线程编程]]
|
||||
|
||||
---
|
||||
|
||||
## 思考题
|
||||
|
||||
1. **fork 返回值的设计**:为什么 `fork()` 要返回两次而不是只返回一次?如果子进程不知道自己的 PID,可以用什么方式获取?
|
||||
2. **exec 的不可逆性**:为什么 `exec()` 成功后不返回?如果 `exec()` 执行失败会怎样?
|
||||
3. **僵尸进程的危害**:如果一个服务器程序不断创建子进程但从不 `wait`,最终会怎样?如何用 `SIGCHLD` 信号处理函数避免僵尸进程?
|
||||
4. **shell 的本质**:shell 执行 `ls -l` 命令时,为什么需要 `fork()` + `exec()` 两个步骤?只用 `exec()` 行不行?
|
||||
5. **daemon 的双重 fork**:为什么 daemon 创建通常要 `fork()` 两次?只 `fork()` 一次有什么问题?
|
||||
|
||||
---
|
||||
|
||||
## 扩展阅读
|
||||
|
||||
- 《UNIX环境高级编程》第8章:进程控制
|
||||
- 《深入理解计算机系统》第8章:异常控制流
|
||||
- 《Linux/UNIX系统编程手册》第24-27章:进程的创建、终止、监控
|
||||
315
操作系统/06_进程控制/06_进程控制_深入.md
Normal file
315
操作系统/06_进程控制/06_进程控制_深入.md
Normal file
@@ -0,0 +1,315 @@
|
||||
# 第06讲:进程控制(进阶)
|
||||
|
||||
> 🎯 **本节目标**:深入理解 fork/exec 的工作原理,掌握 Shell 的实现机制
|
||||
|
||||
## 📋 前置知识
|
||||
- [[06_进程控制]] — fork、exec、wait 的基本概念
|
||||
|
||||
---
|
||||
|
||||
## 🤔 为什么需要这个?
|
||||
|
||||
你每天都在用 Shell(命令行),但你有没有想过:
|
||||
- Shell 是怎么执行你的命令的?
|
||||
- 为什么输入 `ls` 就能列出文件?
|
||||
- 后台运行 `&` 是怎么实现的?
|
||||
|
||||
理解这些,需要深入掌握 fork 和 exec 的配合机制。
|
||||
|
||||
---
|
||||
|
||||
## 📖 核心概念
|
||||
|
||||
### 1. Shell 的工作原理
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[用户输入命令] --> B[Shell 解析命令]
|
||||
B --> C[Shell 调用 fork]
|
||||
C --> D[子进程调用 execvp]
|
||||
D --> E[执行命令程序]
|
||||
E --> F[子进程结束]
|
||||
F --> G[Shell 调用 waitpid]
|
||||
G --> A
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style D fill:#e8f5e9
|
||||
```
|
||||
|
||||
**Shell 的核心逻辑**:
|
||||
```c
|
||||
while (1) {
|
||||
printf("%% "); // 打印提示符
|
||||
fgets(cmdline); // 读取命令
|
||||
if (feof(stdin)) exit(0);
|
||||
|
||||
pid = fork(); // 创建子进程
|
||||
if (pid == 0) { // 子进程
|
||||
execvp(argv[0], argv); // 执行命令
|
||||
exit(1); // exec 失败
|
||||
}
|
||||
|
||||
if (!background) // 前台运行
|
||||
waitpid(pid); // 等待子进程结束
|
||||
}
|
||||
```
|
||||
|
||||
### 2. fork() 的实现细节
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[父进程调用 fork] --> B[内核复制父进程的 PCB]
|
||||
B --> C[复制页表(写时复制)]
|
||||
C --> D[设置子进程的 PID]
|
||||
D --> E[父子进程各返回一次]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style B fill:#fff3e0
|
||||
```
|
||||
|
||||
**写时复制(Copy-on-Write)**:
|
||||
- fork() 时不立即复制物理内存
|
||||
- 父子进程共享同一份物理页面
|
||||
- 只有当某一方尝试写入时,才复制该页面
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant 父进程
|
||||
participant 子进程
|
||||
participant 内存
|
||||
|
||||
父进程->>内存: fork()
|
||||
Note over 内存: 父子共享物理页面
|
||||
子进程->>内存: 尝试写入
|
||||
Note over 内存: 触发写时复制
|
||||
Note over 内存: 复制该页面给子进程
|
||||
子进程->>内存: 写入新页面
|
||||
```
|
||||
|
||||
### 3. exec() 的工作流程
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[调用 execvp] --> B[查找可执行文件]
|
||||
B --> C[释放旧的地址空间]
|
||||
C --> D[加载新的代码段]
|
||||
D --> E[加载新的数据段]
|
||||
E --> F[设置新的栈]
|
||||
F --> G[跳转到新程序入口]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style G fill:#e8f5e9
|
||||
```
|
||||
|
||||
**exec 的关键特性**:
|
||||
- **不创建新进程**:只是替换当前进程的内容
|
||||
- **PID 不变**:进程还是原来的进程
|
||||
- **文件描述符保留**:打开的文件不会自动关闭(除非设置了 close-on-exec)
|
||||
|
||||
### 4. 进程组与会话
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[会话 Session] --> B[前台进程组]
|
||||
A --> C[后台进程组1]
|
||||
A --> D[后台进程组2]
|
||||
|
||||
B --> B1[Shell]
|
||||
B --> B2[当前命令]
|
||||
|
||||
style A fill:#ffcdd2
|
||||
style B fill:#e8f5e9
|
||||
style C fill:#e1f5fe
|
||||
style D fill:#e1f5fe
|
||||
```
|
||||
|
||||
**进程组**:一组相关进程的集合
|
||||
- 用于信号的批量发送
|
||||
- 用于作业控制(前台/后台切换)
|
||||
|
||||
**会话**:一个用户登录期间的所有进程
|
||||
- 一个终端对应一个会话
|
||||
- 会话有一个控制终端
|
||||
|
||||
### 5. 守护进程
|
||||
|
||||
守护进程是在后台运行的特殊进程,没有控制终端:
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[创建子进程] --> B[父进程退出]
|
||||
B --> C[创建新会话]
|
||||
C --> D[改变工作目录]
|
||||
D --> E[关闭文件描述符]
|
||||
E --> F[重定向 stdin/stdout/stderr]
|
||||
F --> G[进入主循环]
|
||||
|
||||
style A fill:#e1f5fe
|
||||
style G fill:#e8f5e9
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💻 动手实践
|
||||
|
||||
### 示例1:实现简单的 Shell
|
||||
|
||||
```c
|
||||
// myshell.c - 简单的 Shell 实现
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
int main(void) {
|
||||
int ret, i, k, len, pid;
|
||||
char cmd[100]; // 命令串,最多100个字符
|
||||
char *arg[20]; // 参数数组,最多20个参数
|
||||
|
||||
printf("%% "); // 打印提示符
|
||||
|
||||
fgets(cmd, 100, stdin); // 从标准输入读取一行命令
|
||||
|
||||
// 将命令串中的空格替换成'\0',并将各参数提取出来
|
||||
// 例如 cmd[100]="ls -l -a\0",替换后变为 cmd="ls\0-l\0-a\0"
|
||||
len = strlen(cmd);
|
||||
cmd[len - 1] = '\0'; // 去掉 fgets 加在串尾的换行符
|
||||
|
||||
for (i = 0; i < len - 1; i++)
|
||||
if (cmd[i] == ' ') cmd[i] = '\0';
|
||||
|
||||
// 准备参数数组 arg
|
||||
// arg[0]=cmd, arg[1]=cmd+3, arg[2]=cmd+6, arg[3]=NULL
|
||||
arg[0] = cmd;
|
||||
k = 1;
|
||||
for (i = 1; i < len - 1; i++) {
|
||||
if (cmd[i] == '\0') {
|
||||
arg[k] = cmd + i + 1;
|
||||
k++;
|
||||
}
|
||||
}
|
||||
arg[k] = NULL;
|
||||
|
||||
pid = fork();
|
||||
if (pid == 0) {
|
||||
ret = execvp(arg[0], arg); // 子进程执行命令
|
||||
if (ret == -1) {
|
||||
perror("exec error");
|
||||
exit(1);
|
||||
}
|
||||
} else {
|
||||
wait(-1); // 父进程等待子进程结束
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o myshell myshell.c
|
||||
./myshell
|
||||
% ls -l
|
||||
```
|
||||
|
||||
### 示例2:后台运行
|
||||
|
||||
```c
|
||||
// shellex.c - 支持后台运行的 Shell
|
||||
#include "wrapper.h"
|
||||
|
||||
#define MAXARGS 128
|
||||
|
||||
int parseline(char *buf, char **argv) {
|
||||
char *delim;
|
||||
int argc;
|
||||
int bg; // 后台作业标志
|
||||
|
||||
buf[strlen(buf) - 1] = ' '; // 用空格替换末尾换行
|
||||
while (*buf && (*buf == ' '))
|
||||
buf++;
|
||||
|
||||
argc = 0;
|
||||
while ((delim = strchr(buf, ' '))) {
|
||||
argv[argc++] = buf;
|
||||
*delim = '\0';
|
||||
buf = delim + 1;
|
||||
while (*buf && (*buf == ' '))
|
||||
buf++;
|
||||
}
|
||||
argv[argc] = NULL;
|
||||
|
||||
if (argc == 0) return 1;
|
||||
|
||||
// 检查是否应该在后台执行
|
||||
if ((bg = (*argv[argc - 1] == '&')) != 0)
|
||||
argv[--argc] = NULL;
|
||||
|
||||
return bg;
|
||||
}
|
||||
|
||||
void execute(char *cmdline) {
|
||||
char *argv[MAXARGS];
|
||||
char buf[MAXLINE];
|
||||
int bg;
|
||||
pid_t pid;
|
||||
|
||||
strcpy(buf, cmdline);
|
||||
bg = parseline(buf, argv);
|
||||
|
||||
if (argv[0] == NULL) return; // 忽略空行
|
||||
|
||||
if ((pid = fork()) == 0) { // 子进程
|
||||
if (execvp(argv[0], argv) < 0) {
|
||||
printf("%s: Command not found.\n", argv[0]);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!bg) { // 前台运行
|
||||
int status;
|
||||
if (waitpid(pid, &status, 0) < 0)
|
||||
perror("waitpid error");
|
||||
} else { // 后台运行
|
||||
printf("%d %s", pid, cmdline);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
char cmdline[MAXLINE];
|
||||
while (1) {
|
||||
printf("%% ");
|
||||
fgets(cmdline, MAXLINE, stdin);
|
||||
if (feof(stdin)) exit(0);
|
||||
execute(cmdline);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**编译运行**:
|
||||
```bash
|
||||
gcc -o shellex shellex.c -L. -lwrapper
|
||||
./shellex
|
||||
% sleep 10 & # 后台运行
|
||||
% ps # 查看进程
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 知识关联
|
||||
- Shell 的 I/O 重定向在 [[04_文件IO编程]] 中的 dup2 有详细讲解
|
||||
- 守护进程在 [[09_网络编程]] 中会实际使用
|
||||
- 进程组在 [[11_处理机调度]] 中用于作业控制
|
||||
|
||||
---
|
||||
|
||||
## 📝 思考题
|
||||
|
||||
1. **为什么 exec() 后文件描述符还保留?** 什么时候需要关闭它们?
|
||||
2. **写时复制的优势是什么?** 如果 fork() 时立即复制所有内存会怎样?
|
||||
3. **Shell 是怎么实现管道的?** 例如 `ls | grep .c` 的执行过程是什么?
|
||||
|
||||
---
|
||||
|
||||
## 📚 扩展阅读
|
||||
- 《UNIX环境高级编程》第8章:进程控制
|
||||
- 《深入理解计算机系统》第8章:异常控制流
|
||||
- [Bash 源码](https://git.savannah.gnu.org/cgit/bash.git/)
|
||||
Reference in New Issue
Block a user