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

11 KiB
Raw Blame History

实验02 Linux 进程控制编程

实验目的

  1. 掌握 forkexecwaitexit 等进程控制系统调用
  2. 理解进程的创建、执行和终止过程
  3. 掌握进程族亲关系(父子进程、兄弟进程)
  4. 学会编写简单的交互式 shell 程序
  5. 理解守护进程daemon的概念和实现方法
  6. 了解信号机制的基本使用

涉及知识点

  • fork() 创建子进程及返回值含义
  • exec 家族函数:execlexeclpexecvpexecve
  • wait / waitpid 回收子进程,避免僵尸进程
  • exit / _exit 终止进程
  • 进程组与会话(setsid
  • 信号:SIGCHLDSIGKILLSIGTERMSIGALRM
  • 守护进程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子进程中返回 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. 支持管道(|

关键代码提示

#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. 如果文件被篡改,将事件记录到日志文件

关键代码提示

#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>:向指定子进程发送 SIGTERM
  • ps:列出所有存活的子进程
  • 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

实验总结

通过本实验,应掌握以下能力:

  1. 使用 fork 创建进程树,理解父子进程关系
  2. 使用 exec 家族函数执行外部命令
  3. 使用 wait/waitpid 回收子进程,避免僵尸进程
  4. 实现简单的 shell理解 shell 的工作原理
  5. 创建守护进程,理解 daemon 的设计模式
  6. 使用信号机制进行进程间通信