# 实验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 #include #include #include 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 #include #include #include #include #include #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 #include #include #include #include #include // 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 `:向指定子进程发送 SIGTERM - `ps`:列出所有存活的子进程 - `exit`:终止所有子进程并退出 ### 关键代码提示 ```c #include #include #include #include #include #include #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. 使用信号机制进行进程间通信