11 KiB
11 KiB
实验02 Linux 进程控制编程
实验目的
- 掌握
fork、exec、wait、exit等进程控制系统调用 - 理解进程的创建、执行和终止过程
- 掌握进程族亲关系(父子进程、兄弟进程)
- 学会编写简单的交互式 shell 程序
- 理解守护进程(daemon)的概念和实现方法
- 了解信号机制的基本使用
涉及知识点
fork()创建子进程及返回值含义exec家族函数:execl、execlp、execvp、execvewait/waitpid回收子进程,避免僵尸进程exit/_exit终止进程- 进程组与会话(
setsid) - 信号:
SIGCHLD、SIGKILL、SIGTERM、SIGALRM - 守护进程(daemon)的创建步骤
- 文件重定向:
dup2 - 管道:
pipe
任务一:task51.c -- 进程族亲结构
任务要求
创建如下进程树结构,每个进程打印自己的 PID 和父进程 PPID,最终输出完整的族亲关系:
p1
+-- p11
+-- p12
+-- p121
+-- p122
关键代码提示
#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,子进程中返回 0wait(NULL)阻塞等待任意一个子进程结束- 子进程必须调用
exit(0)终止,否则会继续执行父进程后续代码 fork后父子进程从同一位置继续执行,注意判断返回值分流
常见问题
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 输出顺序不确定 | 父子进程并发执行 | 正常现象,可用 wait 控制部分顺序 |
| 进程数不对 | fork 位置错误 |
确保在正确的位置 fork,避免重复创建 |
| 僵尸进程 | 父进程未 wait |
每个 fork 后都要有对应的 wait |
| p11 的 PPID 不是 p1 | 父进程先退出 | 正常现象(孤儿进程被 init 收养),可用 sleep 验证 |
任务二:task52.c -- 交互式 shell
任务要求
实现一个简化版的交互式 shell,具备以下功能:
- 显示提示符
%,等待用户输入命令 - 解析并执行用户输入的外部命令
- 输入
exit退出 shell - 支持输出重定向(
>) - 支持管道(
|)
关键代码提示
#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 文件监控
任务要求
- 创建守护进程(daemon),后台运行
- 每隔 5 分钟读取
task53.c文件内容,计算 hash 值 - 与上一次的 hash 值对比
- 如果文件被篡改,将事件记录到日志文件
关键代码提示
#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 |
| hash 算法冲突 | 简单 hash 可能碰撞 | 实验中 djb2 即可,正式场景可用 MD5/SHA |
任务四:task54.c -- 信号管理子进程(选做)
任务要求
实现一个通过信号管理子进程的程序,支持以下命令:
create:创建一个子进程kill <pid>:向指定子进程发送 SIGTERMps:列出所有存活的子进程exit:终止所有子进程并退出
关键代码提示
#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 |
实验总结
通过本实验,应掌握以下能力:
- 使用
fork创建进程树,理解父子进程关系 - 使用
exec家族函数执行外部命令 - 使用
wait/waitpid回收子进程,避免僵尸进程 - 实现简单的 shell,理解 shell 的工作原理
- 创建守护进程,理解 daemon 的设计模式
- 使用信号机制进行进程间通信