强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

POSIX 标准详解教程 / 第三章:进程

第三章:进程

理解 POSIX 进程模型:进程生命周期、fork/exec/wait、进程组、会话管理。


3.1 POSIX 进程模型

3.1.1 什么是进程

在 POSIX 中,进程(Process) 是程序执行的实例。每个进程拥有:

资源 说明
地址空间 独立的虚拟内存布局(代码段、数据段、堆、栈)
进程 ID (PID) 唯一标识符,pid_t 类型
父进程 ID (PPID) 创建该进程的父进程 PID
文件描述符表 打开的文件/管道/套接字
信号处理表 各信号的处理方式
用户/组 ID UID、GID(实际/有效/保存的)
工作目录 当前工作目录
环境变量 KEY=VALUE 键值对列表

3.1.2 进程生命周期

fork()
  │
  ▼
子进程诞生 ──→ exec() 加载新程序 ──→ 运行中
                                         │
                                    exit() / 信号终止
                                         │
                                         ▼
                                    僵尸状态 (Zombie)
                                         │
                                    父进程 wait()
                                         │
                                         ▼
                                      进程消亡

3.1.3 进程终止状态

退出方式 说明 获取方法
exit(n) / return n 正常退出,状态码 n(0-255) WEXITSTATUS(status)
信号终止 被信号杀死(如 SIGKILL) WTERMSIG(status)
core dump 信号终止 + 核心转储 WCOREDUMP(status)(非 POSIX 标准但广泛可用)

3.2 fork():创建子进程

fork() 是 POSIX 中创建新进程的唯一标准方式。它创建当前进程的副本——子进程。

3.2.1 fork() 特性

特性 说明
返回值(父进程) 子进程的 PID(> 0)
返回值(子进程) 0
返回值(失败) -1
内存 子进程获得父进程地址空间的副本(写时复制 COW)
文件描述符 子进程共享父进程的文件描述符表
信号处理 子进程继承父进程的信号处理设置

3.2.2 基本 fork 示例

/*
 * fork_basic.c - fork() 基础示例
 * 编译: gcc -Wall -o fork_basic fork_basic.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void)
{
    pid_t pid;
    int parent_var = 100;

    printf("[父进程] PID=%d, PPID=%d\n", getpid(), getppid());

    pid = fork();
    if (pid == -1) {
        perror("fork");
        return EXIT_FAILURE;
    }

    if (pid == 0) {
        /* 子进程代码 */
        parent_var += 50;  /* 修改副本,不影响父进程 */
        printf("[子进程] PID=%d, PPID=%d, parent_var=%d\n",
               getpid(), getppid(), parent_var);
        return 42;  /* 子进程退出码 */
    } else {
        /* 父进程代码 */
        printf("[父进程] 创建了子进程 PID=%d, parent_var=%d\n",
               pid, parent_var);

        /* 等待子进程结束 */
        int status;
        pid_t child_pid = waitpid(pid, &status, 0);
        if (child_pid == -1) {
            perror("waitpid");
            return EXIT_FAILURE;
        }

        if (WIFEXITED(status)) {
            printf("[父进程] 子进程 %d 退出,状态码=%d\n",
                   child_pid, WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("[父进程] 子进程被信号 %d 终止\n",
                   WTERMSIG(status));
        }
    }

    return EXIT_SUCCESS;
}
$ ./fork_basic
[父进程] PID=1234, PPID=1000
[父进程] 创建了子进程 PID=1235, parent_var=100
[子进程] PID=1235, PPID=1234, parent_var=150
[父进程] 子进程 1235 退出,状态码=42

注意:父进程和子进程的输出顺序是不确定的,取决于调度器。生产代码中需要使用同步机制(信号量、管道等)来协调顺序。


3.3 exec() 家族:替换进程映像

exec() 系列函数用新程序替换当前进程的代码段、数据段和栈,但保留 PID 和文件描述符(除非设置了 FD_CLOEXEC)。

3.3.1 exec 族函数对照

函数 路径 参数形式 环境
execl() 完整路径 逐个参数,NULL 结尾 继承当前
execv() 完整路径 数组 继承当前
execlp() 使用 PATH 搜索 逐个参数,NULL 结尾 继承当前
execvp() 使用 PATH 搜索 数组 继承当前
execle() 完整路径 逐个参数,NULL 结尾 自定义
execvpe() 使用 PATH 搜索 数组 自定义

命名记忆法:l=list(参数列表),v=vector(参数数组),p=PATH(搜索 PATH),e=environment(自定义环境)。

3.3.2 fork + exec 经典模式

/*
 * fork_exec.c - fork() + execvp() 执行外部命令
 * 编译: gcc -Wall -o fork_exec fork_exec.c
 * 用法: ./fork_exec ls -la /tmp
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "用法: %s <命令> [参数...]\n", argv[0]);
        return EXIT_FAILURE;
    }

    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return EXIT_FAILURE;
    }

    if (pid == 0) {
        /* 子进程:执行命令 */
        /* argv[1..] 作为命令和参数 */
        execvp(argv[1], &argv[1]);
        /* 如果 execvp 返回,说明出错 */
        perror(argv[1]);
        _exit(127);  /* 命令未找到 */
    } else {
        /* 父进程:等待子进程 */
        int status;
        if (waitpid(pid, &status, 0) == -1) {
            perror("waitpid");
            return EXIT_FAILURE;
        }
        if (WIFEXITED(status))
            return WEXITSTATUS(status);
        return 1;
    }
}
$ ./fork_exec ls -la /tmp
total 48
drwxrwxrwt 15 root root 4096 May 10 10:00 .
drwxr-xr-x 20 root root 4096 Jan  1 00:00 ..
...

3.4 wait() 与 waitpid():回收子进程

3.4.1 状态检查宏

说明
WIFEXITED(status) 子进程正常退出?
WEXITSTATUS(status) 获取正常退出的状态码(0-255)
WIFSIGNALED(status) 子进程被信号终止?
WTERMSIG(status) 获取终止信号的编号
WIFSTOPPED(status) 子进程被停止(如 SIGSTOP)?
WSTOPSIG(status) 获取停止信号的编号
WIFCONTINUED(status) 子进程被 SIGCONT 继续?

3.4.2 多子进程管理

/*
 * multi_children.c - 创建多个子进程并等待
 * 编译: gcc -Wall -o multi_children multi_children.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

#define NUM_CHILDREN 5

int main(void)
{
    pid_t children[NUM_CHILDREN];

    /* 创建 5 个子进程 */
    for (int i = 0; i < NUM_CHILDREN; i++) {
        pid_t pid = fork();
        if (pid == -1) {
            perror("fork");
            return EXIT_FAILURE;
        }
        if (pid == 0) {
            /* 子进程:模拟工作 */
            printf("  子进程 %d (PID=%d) 开始工作\n", i, getpid());
            sleep(1 + i);  /* 模拟不同耗时 */
            printf("  子进程 %d (PID=%d) 完成\n", i, getpid());
            _exit(i);  /* 以索引作为退出码 */
        }
        children[i] = pid;
        printf("创建子进程 %d, PID=%d\n", i, pid);
    }

    /* 等待所有子进程 */
    printf("\n等待所有子进程完成...\n");
    for (int i = 0; i < NUM_CHILDREN; i++) {
        int status;
        pid_t pid = waitpid(children[i], &status, 0);
        if (pid == -1) {
            perror("waitpid");
            continue;
        }
        if (WIFEXITED(status)) {
            printf("子进程 PID=%d 退出码=%d\n",
                   pid, WEXITSTATUS(status));
        }
    }

    printf("所有子进程已回收\n");
    return EXIT_SUCCESS;
}

3.5 进程组与会话

3.5.1 概念层次

会话 (Session)
├── 前台进程组 (Foreground Process Group)
│   ├── 进程 A
│   └── 进程 B
└── 后台进程组 (Background Process Group)
    ├── 进程 C
    └── 进程 D
概念 说明 获取函数
进程组 (Process Group) 一组相关进程的集合 getpgid(), getpgrp()
会话 (Session) 一个登录会话中的所有进程 getsid()
控制终端 (Controlling Terminal) 会话关联的终端 /dev/tty

3.5.2 守护进程 (Daemon)

守护进程是在后台运行、不与终端关联的进程。POSIX 标准创建守护进程的步骤:

/*
 * daemon.c - 创建 POSIX 守护进程
 * 编译: gcc -Wall -o daemon daemon.c
 * 运行: ./daemon; sleep 2; cat /tmp/posix_daemon.log; kill $(cat /tmp/posix_daemon.pid)
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <time.h>

static volatile sig_atomic_t running = 1;

static void handle_signal(int sig)
{
    (void)sig;
    running = 0;
}

static int create_daemon(void)
{
    /* 第一次 fork:脱离终端 */
    pid_t pid = fork();
    if (pid == -1) return -1;
    if (pid > 0) _exit(EXIT_SUCCESS);  /* 父进程退出 */

    /* 创建新会话 */
    if (setsid() == -1) return -1;

    /* 第二次 fork:防止重新获取控制终端 */
    pid = fork();
    if (pid == -1) return -1;
    if (pid > 0) _exit(EXIT_SUCCESS);

    /* 设置文件权限掩码 */
    umask(0);

    /* 切换到根目录 */
    if (chdir("/") == -1) return -1;

    /* 关闭标准文件描述符,重定向到 /dev/null */
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);
    open("/dev/null", O_RDONLY);  /* stdin */
    open("/dev/null", O_WRONLY);  /* stdout */
    open("/dev/null", O_WRONLY);  /* stderr */

    return 0;
}

int main(void)
{
    if (create_daemon() == -1) {
        perror("create_daemon");
        return EXIT_FAILURE;
    }

    /* 设置信号处理 */
    signal(SIGTERM, handle_signal);
    signal(SIGINT, handle_signal);

    /* 写入 PID 文件 */
    FILE *f = fopen("/tmp/posix_daemon.pid", "w");
    if (f) { fprintf(f, "%d\n", getpid()); fclose(f); }

    /* 守护进程主循环 */
    f = fopen("/tmp/posix_daemon.log", "a");
    while (running) {
        if (f) {
            time_t now = time(NULL);
            fprintf(f, "[%ld] daemon running, PID=%d\n",
                    (long)now, getpid());
            fflush(f);
        }
        sleep(5);
    }

    if (f) {
        fprintf(f, "daemon shutting down\n");
        fclose(f);
    }
    unlink("/tmp/posix_daemon.pid");

    return EXIT_SUCCESS;
}

3.6 _exit() 与 exit() 的区别

函数 头文件 刷新 stdio 缓冲区 调用 atexit 处理器
exit(status) <stdlib.h> ✅ 是 ✅ 是
_exit(status) <unistd.h> ❌ 否 ❌ 否

关键规则:在 fork() 之后的子进程中,应使用 _exit() 而非 exit(),以避免刷新父进程的 stdio 缓冲区(导致重复输出)。


3.7 环境变量操作

/*
 * env_ops.c - POSIX 环境变量操作
 * 编译: gcc -Wall -o env_ops env_ops.c
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

extern char **environ;  /* POSIX 定义的全局环境变量数组 */

int main(void)
{
    /* 1. 获取环境变量 */
    const char *path = getenv("PATH");
    printf("PATH = %s\n\n", path ? path : "(未设置)");

    /* 2. 设置环境变量 */
    setenv("MY_VAR", "hello_posix", 1);  /* 1 = 覆盖已有值 */
    printf("MY_VAR = %s\n", getenv("MY_VAR"));

    /* 3. 修改环境变量 */
    setenv("MY_VAR", "modified", 1);
    printf("MY_VAR (修改后) = %s\n", getenv("MY_VAR"));

    /* 4. 删除环境变量 */
    unsetenv("MY_VAR");
    printf("MY_VAR (删除后) = %s\n", getenv("MY_VAR"));

    /* 5. 遍历所有环境变量(前 5 个) */
    printf("\n前 5 个环境变量:\n");
    for (int i = 0; environ[i] && i < 5; i++)
        printf("  [%d] %s\n", i, environ[i]);

    return 0;
}

3.8 业务场景:并行任务执行器

/*
 * parallel_exec.c - 并行执行多个命令
 * 编译: gcc -Wall -o parallel_exec parallel_exec.c
 * 用法: ./parallel_exec "sleep 2" "sleep 1" "echo done"
 */
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

static pid_t run_command(const char *cmd)
{
    pid_t pid = fork();
    if (pid == -1) { perror("fork"); return -1; }
    if (pid == 0) {
        execl("/bin/sh", "sh", "-c", cmd, (char *)NULL);
        perror("execl");
        _exit(127);
    }
    return pid;
}

int main(int argc, char *argv[])
{
    if (argc < 2) {
        fprintf(stderr, "用法: %s \"cmd1\" \"cmd2\" ...\n", argv[0]);
        return 1;
    }

    pid_t *pids = malloc(sizeof(pid_t) * (argc - 1));
    if (!pids) { perror("malloc"); return 1; }

    /* 并行启动所有命令 */
    for (int i = 1; i < argc; i++) {
        pids[i - 1] = run_command(argv[i]);
        if (pids[i - 1] > 0)
            printf("启动: \"%s\" (PID=%d)\n", argv[i], pids[i - 1]);
    }

    /* 等待所有命令完成 */
    int failed = 0;
    for (int i = 0; i < argc - 1; i++) {
        if (pids[i] <= 0) continue;
        int status;
        waitpid(pids[i], &status, 0);
        if (WIFEXITED(status) && WEXITSTATUS(status) != 0) {
            fprintf(stderr, "PID=%d 退出码=%d\n",
                    pids[i], WEXITSTATUS(status));
            failed++;
        }
    }

    printf("\n完成: %d/%d 成功\n", argc - 1 - failed, argc - 1);
    free(pids);
    return failed ? 1 : 0;
}

3.9 注意事项

⚠️ fork() 后的线程状态fork() 只复制调用线程,其他线程不会在子进程中存在。在多线程程序中 fork() 需要特别小心,推荐在 fork() 后立即 exec()

⚠️ 僵尸进程:父进程不调用 wait() 的子进程会变成僵尸进程(Zombie),占用进程表条目。使用 SIGCHLD 信号处理或 waitpid(-1, &status, WNOHANG) 来避免。

⚠️ fork() 与文件描述符fork() 后父子进程共享文件描述符偏移量。一个进程的写入会影响另一个进程看到的偏移位置。

⚠️ exec() 后的 FD_CLOEXEC:默认情况下 exec() 后文件描述符保持打开。使用 fcntl(fd, F_SETFD, FD_CLOEXEC) 标记不继承的文件描述符。


3.10 扩展阅读

  1. man 2 forkman 2 execveman 2 waitpid
  2. APUE 第 8 章:Process Control
  3. TLPI 第 24-26 章:Process Creation, Process Termination, Monitoring Child Processes
  4. clone() 系统调用:Linux 特有的进程/线程创建接口,是 fork() 的超集
  5. posix_spawn():POSIX.1-2001 引入的 fork()+exec() 替代方案,某些嵌入式环境更高效

3.11 本章小结

要点 说明
fork() 创建子进程,父子进程地址空间独立(COW)
exec() 替换进程映像,PID 不变
wait()/waitpid() 回收子进程,获取退出状态
exit() vs _exit() 子进程中应使用 _exit() 避免刷新父进程缓冲
进程组 相关进程的集合,用于信号分发和作业控制
会话 登录会话,由 setsid() 创建
守护进程 双 fork + setsid + 重定向文件描述符