Files
obsidian/操作系统/实验/实验02_进程控制.md

438 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 实验02 Linux 进程控制编程
## 实验目的
1. 掌握 `fork``exec``wait``exit` 等进程控制系统调用
2. 理解进程的创建、执行和终止过程
3. 掌握进程族亲关系(父子进程、兄弟进程)
4. 学会编写简单的交互式 shell 程序
5. 理解守护进程daemon的概念和实现方法
6. 了解信号机制的基本使用
## 涉及知识点
- `fork()` 创建子进程及返回值含义
- `exec` 家族函数:`execl``execlp``execvp``execve`
- `wait` / `waitpid` 回收子进程,避免僵尸进程
- `exit` / `_exit` 终止进程
- 进程组与会话(`setsid`
- 信号:`SIGCHLD``SIGKILL``SIGTERM``SIGALRM`
- 守护进程daemon的创建步骤
- 文件重定向:`dup2`
- 管道:`pipe`
---
## 任务一task51.c -- 进程族亲结构
### 任务要求
创建如下进程树结构,每个进程打印自己的 PID 和父进程 PPID最终输出完整的族亲关系
```
p1
+-- p11
+-- p12
+-- p121
+-- p122
```
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t pid;
printf("p1: PID=%d, PPID=%d\n", getpid(), getppid());
// 创建 p11
pid = fork();
if (pid == 0) {
printf("p11: PID=%d, PPID=%d\n", getpid(), getppid());
exit(0);
}
// 创建 p12
pid = fork();
if (pid == 0) {
printf("p12: PID=%d, PPID=%d\n", getpid(), getppid());
// p12 创建 p121
pid = fork();
if (pid == 0) {
printf("p121: PID=%d, PPID=%d\n", getpid(), getppid());
exit(0);
}
// p12 创建 p122
pid = fork();
if (pid == 0) {
printf("p122: PID=%d, PPID=%d\n", getpid(), getppid());
exit(0);
}
wait(NULL);
wait(NULL);
exit(0);
}
// p1 等待两个子进程
wait(NULL);
wait(NULL);
return 0;
}
```
### 关键要点
- `fork()` 返回值:父进程中返回子进程 PID子进程中返回 0
- `wait(NULL)` 阻塞等待任意一个子进程结束
- 子进程必须调用 `exit(0)` 终止,否则会继续执行父进程后续代码
- `fork` 后父子进程从同一位置继续执行,注意判断返回值分流
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 输出顺序不确定 | 父子进程并发执行 | 正常现象,可用 `wait` 控制部分顺序 |
| 进程数不对 | `fork` 位置错误 | 确保在正确的位置 `fork`,避免重复创建 |
| 僵尸进程 | 父进程未 `wait` | 每个 `fork` 后都要有对应的 `wait` |
| p11 的 PPID 不是 p1 | 父进程先退出 | 正常现象(孤儿进程被 init 收养),可用 `sleep` 验证 |
---
## 任务二task52.c -- 交互式 shell
### 任务要求
实现一个简化版的交互式 shell具备以下功能
1. 显示提示符 `%`,等待用户输入命令
2. 解析并执行用户输入的外部命令
3. 输入 `exit` 退出 shell
4. 支持输出重定向(`>`
5. 支持管道(`|`
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <fcntl.h>
#define MAX_LINE 1024
#define MAX_ARGS 64
// 解析命令行为参数数组
int parse(char *line, char **args) {
int argc = 0;
char *token = strtok(line, " \t\n");
while (token != NULL && argc < MAX_ARGS - 1) {
args[argc++] = token;
token = strtok(NULL, " \t\n");
}
args[argc] = NULL;
return argc;
}
// 处理输出重定向
void handle_redirect(char **args) {
for (int i = 0; args[i] != NULL; i++) {
if (strcmp(args[i], ">") == 0) {
int fd = open(args[i + 1],
O_WRONLY | O_CREAT | O_TRUNC, 0644);
dup2(fd, STDOUT_FILENO);
close(fd);
args[i] = NULL;
break;
}
}
}
// 处理管道
void handle_pipe(char *line) {
char *cmd1 = strtok(line, "|");
char *cmd2 = strtok(NULL, "|");
int pipefd[2];
pipe(pipefd);
pid_t pid1 = fork();
if (pid1 == 0) {
close(pipefd[0]);
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
char *args[MAX_ARGS];
parse(cmd1, args);
execvp(args[0], args);
perror("execvp");
exit(1);
}
pid_t pid2 = fork();
if (pid2 == 0) {
close(pipefd[1]);
dup2(pipefd[0], STDIN_FILENO);
close(pipefd[0]);
char *args[MAX_ARGS];
parse(cmd2, args);
execvp(args[0], args);
perror("execvp");
exit(1);
}
close(pipefd[0]);
close(pipefd[1]);
waitpid(pid1, NULL, 0);
waitpid(pid2, NULL, 0);
}
int main() {
char line[MAX_LINE];
while (1) {
printf("%% ");
fflush(stdout);
if (fgets(line, sizeof(line), stdin) == NULL) break;
if (strncmp(line, "exit", 4) == 0) break;
if (strchr(line, '|') != NULL) {
handle_pipe(line);
continue;
}
char *args[MAX_ARGS];
parse(line, args);
pid_t pid = fork();
if (pid == 0) {
handle_redirect(args);
execvp(args[0], args);
perror("execvp");
exit(1);
}
waitpid(pid, NULL, 0);
}
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| shell 卡住不响应 | `fgets` 缓冲问题 | 输入后确保有换行符,提示符后用 `fflush(stdout)` |
| 重定向不生效 | `dup2` 调用时机不对 | 必须在 `execvp` 之前调用 `dup2` |
| 管道只执行一半 | 文件描述符未关闭 | 父子进程中都要关闭不用的管道端 |
| `exit` 无法退出 | 字符串比较逻辑错误 | 用 `strncmp` 精确匹配前 4 个字符 |
---
## 任务三task53.c -- daemon 文件监控
### 任务要求
1. 创建守护进程daemon后台运行
2. 每隔 5 分钟读取 `task53.c` 文件内容,计算 hash 值
3. 与上一次的 hash 值对比
4. 如果文件被篡改,将事件记录到日志文件
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>
// djb2 哈希算法
unsigned long compute_hash(const char *filename) {
FILE *fp = fopen(filename, "r");
if (!fp) return 0;
unsigned long hash = 5381;
int c;
while ((c = fgetc(fp)) != EOF)
hash = ((hash << 5) + hash) + c;
fclose(fp);
return hash;
}
// daemon 创建五步法
void daemonize() {
pid_t pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0);
setsid();
pid = fork();
if (pid < 0) exit(1);
if (pid > 0) exit(0);
chdir("/");
close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);
}
int main() {
daemonize();
unsigned long last_hash = compute_hash("task53.c");
FILE *logfp;
while (1) {
sleep(300);
unsigned long current_hash = compute_hash("task53.c");
if (current_hash != last_hash) {
logfp = fopen("/var/log/task53.log", "a");
if (logfp) {
time_t now = time(NULL);
fprintf(logfp, "[%s] task53.c changed! "
"old=%lx, new=%lx\n",
ctime(&now), last_hash, current_hash);
fclose(logfp);
}
last_hash = current_hash;
}
}
return 0;
}
```
### daemon 创建步骤(五步法)
```
1. fork() + 父进程 exit -- 脱离终端控制
2. setsid() -- 创建新会话,成为会话首进程
3. fork() + 父进程 exit -- 确保不会重新获得控制终端
4. chdir("/") -- 避免占用可卸载的文件系统
5. 关闭 0/1/2 -- 释放标准输入输出
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| daemon 无法后台运行 | 没有正确 `fork` 两次 | 严格按照五步法创建 |
| 日志文件无输出 | 路径权限问题 | 检查 `/var/log/` 写权限,或改用用户目录 |
| 如何终止 daemon | 无交互终端 | `ps aux | grep task53` 找到 PID 后 `kill` |
| hash 算法冲突 | 简单 hash 可能碰撞 | 实验中 djb2 即可,正式场景可用 MD5/SHA |
---
## 任务四task54.c -- 信号管理子进程(选做)
### 任务要求
实现一个通过信号管理子进程的程序,支持以下命令:
- `create`:创建一个子进程
- `kill <pid>`:向指定子进程发送 SIGTERM
- `ps`:列出所有存活的子进程
- `exit`:终止所有子进程并退出
### 关键代码提示
```c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#define MAX_CHILDREN 64
pid_t children[MAX_CHILDREN];
int child_count = 0;
void sigchld_handler(int sig) {
pid_t pid;
while ((pid = waitpid(-1, NULL, WNOHANG)) > 0) {
for (int i = 0; i < child_count; i++) {
if (children[i] == pid) {
children[i] = children[--child_count];
break;
}
}
printf("[parent] child %d terminated\n", pid);
}
}
void child_work() {
while (1) sleep(10);
}
int main() {
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);
char cmd[64];
while (1) {
printf("cmd> ");
fflush(stdout);
if (fgets(cmd, sizeof(cmd), stdin) == NULL) break;
if (strncmp(cmd, "create", 6) == 0) {
pid_t pid = fork();
if (pid == 0) { child_work(); exit(0); }
children[child_count++] = pid;
printf("created child PID=%d\n", pid);
} else if (strncmp(cmd, "kill", 4) == 0) {
pid_t pid;
sscanf(cmd + 5, "%d", &pid);
kill(pid, SIGTERM);
} else if (strncmp(cmd, "ps", 2) == 0) {
printf("alive children: ");
for (int i = 0; i < child_count; i++)
printf("%d ", children[i]);
printf("\n");
} else if (strncmp(cmd, "exit", 4) == 0) {
for (int i = 0; i < child_count; i++)
kill(children[i], SIGTERM);
sleep(1);
break;
}
}
return 0;
}
```
### 常见问题
| 问题 | 原因 | 解决方法 |
|------|------|----------|
| 僵尸进程残留 | `SIGCHLD` 未正确处理 | handler 中用 `waitpid(-1, ..., WNOHANG)` 循环回收 |
| `ps` 列表不准 | handler 与主程序竞争共享数组 | 使用信号屏蔽(`sigprocmask`)保护临界区 |
| 子进程无法终止 | 信号被忽略或屏蔽 | 确保子进程没有屏蔽 `SIGTERM` |
---
## 实验总结
通过本实验,应掌握以下能力:
1. 使用 `fork` 创建进程树,理解父子进程关系
2. 使用 `exec` 家族函数执行外部命令
3. 使用 `wait`/`waitpid` 回收子进程,避免僵尸进程
4. 实现简单的 shell理解 shell 的工作原理
5. 创建守护进程,理解 daemon 的设计模式
6. 使用信号机制进行进程间通信