869 lines
24 KiB
Markdown
869 lines
24 KiB
Markdown
# 第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章:进程的创建、终止、监控
|