前置知识: C

进程与管道

3 minAdvanced2026/6/14

进程创建与进程间通信

概述

进程是操作系统资源分配的基本单位,每个进程拥有独立的地址空间。在Unix/Linux系统中,通过 fork 创建子进程,通过 exec 族函数执行新程序,通过 wait 等待子进程结束。管道(pipe)是最简单的进程间通信机制,允许具有亲缘关系的进程之间传递数据。

基础概念

进程的生命周期

  1. 创建:通过 fork 从父进程创建子进程
  2. 执行:通过 exec 族函数加载新程序
  3. 等待:父进程通过 wait 等待子进程结束
  4. 终止:进程通过 exit 或从 main 返回终止

管道的分

说明适用场景
匿名管道pipe() 创建,单向父子进程间通信
命名管道mkfifo() 创建,文件系统可见无亲缘关系进程间通信

进程相关头文件

头文件说明
<unistd.h>fork, exec, pipe, getpid 等
<sys/wait.h>wait, waitpid
<sys/types.h>pid_t 等型定义
<fcntl.h>文件控制选项

快速上手

创建子进程

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    pid_t pid = fork();

    if (pid < 0) {
        // 创建失败
        perror("fork 失败");
        return 1;
    } else if (pid == 0) {
        // 子进程
        printf("子进程: PID=%d, 父进程PID=%d\n", getpid(), getppid());
    } else {
        // 父进程
        printf("父进程: PID=%d, 子进程PID=%d\n", getpid(), pid);
        wait(NULL); // 等待子进程结束
        printf("子进程已结束\n");
    }

    return 0;
}

使用管道通信

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(void) {
    int pipefd[2]; // pipefd[0] 读端, pipefd[1] 写端

    // 创建管道
    if (pipe(pipefd) == -1) {
        perror("pipe 失败");
        return 1;
    }

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程:写入数据
        close(pipefd[0]); // 关闭读端

        const char *msg = "来自子进程的消息";
        write(pipefd[1], msg, strlen(msg) + 1);

        close(pipefd[1]); // 关闭写端
    } else {
        // 父进程:读取数据
        close(pipefd[1]); // 关闭写端

        char buf[256];
        ssize_t n = read(pipefd[0], buf, sizeof(buf));
        printf("父进程收到: %s (%zd 字节)\n", buf, n);

        close(pipefd[0]); // 关闭读端
        wait(NULL);
    }

    return 0;
}

详细用法

exec 族函数

#include <stdio.h>
#include <unistd.h>

// exec 族函数对比
// l: 参数以列表形式传递(以 NULL 结尾)
// v: 参数以数组形式传递
// p: 在 PATH 中搜索程序
// e: 自定义环境变量

int main(void) {
    pid_t pid = fork();

    if (pid == 0) {
        // 方式一:execl - 列表参数
        execl("/bin/ls", "ls", "-l", "/tmp", NULL);

        // 方式二:execlp - 搜索 PATH
        // execlp("ls", "ls", "-l", NULL);

        // 方式三:execv - 数组参数
        // char *args[] = {"ls", "-l", "/tmp", NULL};
        // execv("/bin/ls", args);

        // 如果 exec 成功,以下代码不会执行
        perror("exec 失败");
        return 1;
    }

    wait(NULL);
    printf("子进程执行完毕\n");
    return 0;
}

wait 和 waitpid

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
    pid_t pid = fork();

    if (pid == 0) {
        printf("子进程运行中...\n");
        sleep(2);
        return 42; // 子进程退出码
    }

    // 等待子进程结束
    int status;
    pid_t ret = waitpid(pid, &status, 0);

    if (WIFEXITED(status)) {
        printf("子进程正常退出,退出码: %d\n", WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        printf("子进程被信号终止,信号: %d\n", WTERMSIG(status));
    }

    return 0;
}

命名管道(FIFO)

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define FIFO_PATH "/tmp/my_fifo"

// 写入端
int writer(void) {
    mkfifo(FIFO_PATH, 0666); // 创建命名管道

    int fd = open(FIFO_PATH, O_WRONLY);
    if (fd == -1) { perror("open 失败"); return 1; }

    const char *msg = "通过命名管道发送的消息";
    write(fd, msg, strlen(msg) + 1);
    close(fd);
    return 0;
}

// 读取端
int reader(void) {
    int fd = open(FIFO_PATH, O_RDONLY);
    if (fd == -1) { perror("open 失败"); return 1; }

    char buf[256];
    read(fd, buf, sizeof(buf));
    printf("收到: %s\n", buf);
    close(fd);
    unlink(FIFO_PATH); // 删除命名管道
    return 0;
}

常见场景

场景一:实现简单的 shell

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define MAX_ARGS 64

int main(void) {
    char line[256];

    while (1) {
        printf("mysh> ");
        if (!fgets(line, sizeof(line), stdin)) break;

        // 去除换行符
        line[strcspn(line, "\n")] = '\0';

        if (strcmp(line, "exit") == 0) break;
        if (line[0] == '\0') continue;

        // 解析命令和参数
        char *args[MAX_ARGS];
        int argc = 0;
        char *token = strtok(line, " ");
        while (token && argc < MAX_ARGS - 1) {
            args[argc++] = token;
            token = strtok(NULL, " ");
        }
        args[argc] = NULL;

        // 创建子进程执行命令
        pid_t pid = fork();
        if (pid == 0) {
            execvp(args[0], args);
            perror("命令执行失败");
            exit(1);
        } else {
            wait(NULL);
        }
    }

    return 0;
}

场景二:管道连接两个命令

#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

// 实现 ls | wc -l 的效果
int main(void) {
    int pipefd[2];
    pipe(pipefd);

    pid_t pid1 = fork();
    if (pid1 == 0) {
        // 子进程1:执行 ls,输出到管道
        close(pipefd[0]); // 关闭读端
        dup2(pipefd[1], STDOUT_FILENO); // 标准输出重定向到管道写端
        close(pipefd[1]);
        execlp("ls", "ls", NULL);
        perror("exec ls 失败");
        return 1;
    }

    pid_t pid2 = fork();
    if (pid2 == 0) {
        // 子进程2:执行 wc -l,从管道读取
        close(pipefd[1]); // 关闭写端
        dup2(pipefd[0], STDIN_FILENO); // 标准输入重定向到管道读端
        close(pipefd[0]);
        execlp("wc", "wc", "-l", NULL);
        perror("exec wc 失败");
        return 1;
    }

    // 父进程:关闭管道两端,等待子进程
    close(pipefd[0]);
    close(pipefd[1]);
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);

    return 0;
}

场景三:进程池

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

#define WORKER_COUNT 4

// 工作进程函数
void worker(int id) {
    printf("工作进程 %d (PID=%d) 启动\n", id, getpid());
    sleep(1 + id % 3); // 模拟不同耗时的工作
    printf("工作进程 %d 完成\n", id);
}

int main(void) {
    pid_t workers[WORKER_COUNT];

    // 创建工作进程
    for (int i = 0; i < WORKER_COUNT; i++) {
        workers[i] = fork();
        if (workers[i] == 0) {
            worker(i);
            exit(0);
        }
    }

    // 等待所有工作进程完成
    for (int i = 0; i < WORKER_COUNT; i++) {
        int status;
        pid_t pid = wait(&status);
        printf("进程 %d 已结束\n", pid);
    }

    printf("所有工作进程已完成\n");
    return 0;
}

注意事项

僵尸进程

子进程结束后,如果父进程没有调用 wait,子进程会变成僵尸进程(Z状态),占用系统资源:

// 避免僵尸进程的方式一:父进程调用 wait
wait(NULL);

// 方式二:忽略 SIGCHLD 信号
signal(SIGCHLD, SIG_IGN);

// 方式三:双重 fork
pid_t pid = fork();
if (pid == 0) {
    if (fork() == 0) {
        // 孙子进程执行工作
        worker();
        exit(0);
    }
    exit(0); // 子进程立即退出
}
wait(NULL); // 等待子进程(不等待孙子进程)

管道是单向的

管道只能单向传输数据。如果需要双向通信,需要创建两个管道:

int pipe1[2]; // 父 -> 子
int pipe2[2]; // 子 -> 父
pipe(pipe1);
pipe(pipe2);

管道的阻塞特性

当管道读端关闭后,写端继续写入会收到 SIGPIPE 信号。当管道写端关闭后,read 返回0表示文件结束。

进阶用法

使用 popen 执行命令

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // 执行命令并读取输出
    FILE *fp = popen("ls -l /tmp", "r");
    if (!fp) {
        perror("popen 失败");
        return 1;
    }

    char line[256];
    while (fgets(line, sizeof(line), fp)) {
        printf(">> %s", line);
    }

    int status = pclose(fp);
    printf("命令退出状态: %d\n", WEXITSTATUS(status));
    return 0;
}

守护进程

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

// 创建守护进程
void daemonize(void) {
    // 步骤一:创建子进程,父进程退出
    pid_t pid = fork();
    if (pid < 0) exit(1);
    if (pid > 0) exit(0);

    // 步骤二:创建新会话
    if (setsid() < 0) exit(1);

    // 步骤三:再次 fork,防止获取终端
    pid = fork();
    if (pid < 0) exit(1);
    if (pid > 0) exit(0);

    // 步骤四:设置文件权限掩码
    umask(0);

    // 步骤五:更改工作目录
    chdir("/");

    // 步骤六:关闭标准文件描述符
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 将标准输入输出重定向到 /dev/null
    open("/dev/null", O_RDWR);
    dup(0);
    dup(0);
}

int main(void) {
    daemonize();

    // 守护进程主循环
    while (1) {
        // 执行守护任务
        sleep(60);
    }

    return 0;
}

多进程并发服务器

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <signal.h>

#define PORT 8080

void handle_client(int client_fd) {
    char buf[1024];
    ssize_t n = read(client_fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        printf("收到请求: %s\n", buf);
        const char *response = "HTTP/1.1 200 OK\r\n\r\nHello, World!";
        write(client_fd, response, strlen(response));
    }
    close(client_fd);
}

int main(void) {
    signal(SIGCHLD, SIG_IGN); // 避免僵尸进程

    int server_fd = socket(AF_INET, SOCK_STREAM, 0);
    int opt = 1;
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));

    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(PORT),
        .sin_addr.s_addr = INADDR_ANY
    };

    bind(server_fd, (struct sockaddr *)&addr, sizeof(addr));
    listen(server_fd, 10);
    printf("服务器监听端口 %d\n", PORT);

    while (1) {
        int client_fd = accept(server_fd, NULL, NULL);
        if (client_fd < 0) continue;

        pid_t pid = fork();
        if (pid == 0) {
            close(server_fd); // 子进程不需要监听套接字
            handle_client(client_fd);
            exit(0);
        }
        close(client_fd); // 父进程不需要客户端套接字
    }

    return 0;
}