手把手教你写一个多进程控制器:fork + kill + wait 完整实战

💡 本文带你逐行剖析一个经典的 Unix 多进程管理程序 —— 创建多个子进程、键盘控制杀死指定进程、优雅回收资源。涵盖 fork()kill()wait() 核心机制,适合系统编程初学者和进阶者!


1️⃣ 程序功能预览

运行效果如下:

Bash

$ ./a.out 3
This is process No.0 and its pid is 12345
This is process No.1 and its pid is 12346
This is process No.2 and its pid is 12347
...

然后你可以:

  • 按 2 + 回车 → 杀死 2 号子进程
  • 按 q + 回车 → 杀死所有剩余进程并退出

输出:

已杀死子进程 2 (PID: 12347)

所有子进程已终止,程序结束

👉 这是一个“进程上帝模拟器”—— 你创建生命,赋予编号,监听心跳,赐予死亡,最后体面安葬。


2️⃣ 完整源码展示

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <signal.h>
#include <ctype.h>
#include <stdlib.h>
#include <sys/wait.h>

#define MAX_CHILD_NUMBER 10
#define SLEEP_INTERVAL 2

int proc_number = 0;

void do_something();

int main(int argc, char* argv[]) {
    int child_proc_number = MAX_CHILD_NUMBER;
    int i, ch;
    pid_t child_pid;
    pid_t pid[10] = {0};
    
    if (argc > 1) {
        child_proc_number = atoi(argv[1]);
        child_proc_number = (child_proc_number > 10) ? 10 : child_proc_number;
    }
    
    for (i = 0; i < child_proc_number; i++) {
        child_pid = fork();
        if (child_pid < 0) {
            perror("fork failed");
            exit(1);
        } else if (child_pid == 0) {
            proc_number = i;
            do_something();
            exit(0);
        } else {
            pid[i] = child_pid;
        }
    }
    
    while ((ch = getchar()) != 'q') {
        if (isdigit(ch)) {
            int index = ch - '0';
            if (index < child_proc_number) {
                if (kill(pid[index], SIGTERM) == 0) {
                    printf("\n已杀死子进程 %d (PID: %d)\n", index, pid[index]);
                } else {
                    perror("kill failed");
                }
            }
        }
    }
    
    for (i = 0; i < child_proc_number; i++) {
        if (pid[i] > 0) {
            kill(pid[i], SIGTERM);
        }
    }
    
    while (wait(NULL) > 0);
    
    printf("\n所有子进程已终止,程序结束\n");
    return 0;
}

void do_something() {
    for (;;) {
        printf("This is process No.%d and its pid is %d\n", proc_number, getpid());
        sleep(SLEEP_INTERVAL);
    }
}

3️⃣ 零基础讲解:程序想干嘛?

想象你是“机器人指挥官” 👨‍💼:

  • 你批量制造 N 个机器人(子进程)🤖
  • 每个机器人每隔 2 秒喊一次:“我是X号,PID是YYYY!”
  • 你可以按数字键,精准“销毁”某个机器人
  • 最后按 q,一键清场 + 办理“离职手续”

→ 这就是一个简化版的“进程池控制器”,是 Web 服务器、数据库连接池、监控系统的原型!


4️⃣ 深度拆解:每一行代码都讲透

▶️ 第一部分:头文件 & 宏定义

#include <stdio.h>      // printf, getchar
#include <unistd.h>     // fork, getpid, sleep
#include <signal.h>     // kill, SIGTERM
#include <sys/wait.h>   // wait
...
#define MAX_CHILD_NUMBER 10  // 最多10个子进程
int proc_number = 0;         // 子进程编号(fork后独立副本)

💡 proc_number 是全局变量,但 fork 后每个进程有独立副本,所以安全!


▶️ 第二部分:命令行参数解析

if (argc > 1) {
    child_proc_number = atoi(argv[1]);
    child_proc_number = (child_proc_number > 10) ? 10 : child_proc_number;
}
  • ./a.out 5 → 创建 5 个子进程
  • ./a.out 99 → 自动限制为 10(防数组越界)

⚠️ 改进建议:用 strtol 替代 atoi 做错误检查。


▶️ 第三部分:创建子进程(核心!)

for (i = 0; i < child_proc_number; i++) {
    child_pid = fork();           // ← 关键!返回值决定父子身份

    if (child_pid == 0) {         // 子进程
        proc_number = i;
        do_something();           // 无限循环打印
        exit(0);
    } else {                      // 父进程
        pid[i] = child_pid;       // 记录子进程 PID,后续控制用
    }
}
🖼️ 图解 fork 过程(创建3个子进程):
父进程 PID=1000
├── i=0 → fork → child_pid=1001 → pid[0]=1001 → 子进程0干活
├── i=1 → fork → child_pid=1002 → pid[1]=1002 → 子进程1干活
└── i=2 → fork → child_pid=1003 → pid[2]=1003 → 子进程2干活

✅ child_pid 是临时扫描仪,pid[] 是永久档案柜!


▶️ 第四部分:用户交互控制

while ((ch = getchar()) != 'q') {
    if (isdigit(ch)) {
        int index = ch - '0';                 // '3' → 3
        kill(pid[index], SIGTERM);            // 发送终止信号
        printf("已杀死子进程 %d (PID: %d)\n", index, pid[index]);
    }
}

💡 终端默认规范模式 → 必须按回车才触发。想“按下即响应”需设 raw mode。


▶️ 第五部分:清理阶段

// 杀死残余子进程
for (i = 0; i < child_proc_number; i++) {
    if (pid[i] > 0) kill(pid[i], SIGTERM);
}

// 回收僵尸进程
while (wait(NULL) > 0);

printf("所有子进程已终止,程序结束\n");

👻 不 wait → 子进程变僵尸 → 占用 PID 和进程表项!


5️⃣ 三大核心系统调用详解:fork / kill / wait

🧬 fork() —— “生孩子”

pid_t fork(void);
  • 返回 0 → 子进程
  • 返回 >0 → 父进程(值为子 PID)
  • 返回 <0 → 失败

🧠 底层:复制地址空间(写时复制)、文件描述符、信号处理表等。


💣 kill() —— “发信号”

int kill(pid_t pid, int sig);
  • SIGTERM (15):请求终止(可捕获)
  • SIGKILL (9):强制终止(不可抗拒)
  • kill(pid, 0):测试进程是否存在

✅ 默认行为:终止进程。可通过 signal() 自定义处理。


🧹 wait() —— “收尸”

pid_t wait(int *status);
  • 阻塞等待子进程结束
  • 回收资源,避免僵尸
  • 解析退出状态:
if (WIFEXITED(status)) {
    printf("正常退出,状态码:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
    printf("被信号 %d 终止\n", WTERMSIG(status));
}

🔄 非阻塞版:waitpid(pid, &status, WNOHANG)


6️⃣ 缺陷分析与生产级改进建议

❗ 1. atoi 不安全 → 改用 strtol

char *end;
long n = strtol(argv[1], &end, 10);
if (*end != '\0' || n <= 0 || n > 10) {
    fprintf(stderr, "请输入 1~10 的数字\n");
    exit(1);
}

❗ 2. 防止重复 kill

if (pid[index] > 0 && kill(pid[index], 0) == 0) {
    kill(pid[index], SIGTERM);
    pid[index] = 0; // 标记已杀
} else {
    printf("进程已不存在\n");
}

❗ 3. 子进程优雅退出

volatile sig_atomic_t running = 1;
void handler(int sig) { running = 0; }

void do_something() {
    signal(SIGTERM, handler);
    while (running) {
        printf("...");
        fflush(stdout);
        sleep(2);
    }
    printf("正在优雅退出...\n");
    exit(0);
}

❗ 4. 输出防交错

printf("...\n");
fflush(stdout); // 强制刷新缓冲区

7️⃣ 扩展方向与学习路线

学完这个程序,你可以:

🔹 加入管道通信 → 父子进程传数据
🔹 实现进程间同步 → 信号量、共享内存
🔹 图形化/Web 控制 → 用 ncurses 或 Flask
🔹 日志系统 → 记录每个子进程生命周期
🔹 守护进程化 → 脱离终端后台运行

📚 推荐学习路径:
fork/kill/wait → pipe/fifo → shm/semaphore → select/poll → socket → epoll → 多线程 → 并发服务器


8️⃣ 总结

✅ 这个不到 100 行的程序,涵盖了 Unix 进程管理的核心:
创建(fork)→ 控制(kill)→ 回收(wait)→ 交互(getchar)→ 资源安全(防僵尸)

它不是玩具,而是通向系统编程、服务器开发、操作系统理解的第一块真正基石


📌 收藏 + 点赞 + 转发,让更多人看到这份深度解析!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区