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

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

第三章:进程

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


3.1 POSIX 进程模型

3.1.1 什么是进程

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

资源说明
地址空间独立的虚拟内存布局(代码段、数据段、堆、栈)
进程 ID (PID)唯一标识符,pid_t 类型
父进程 ID (PPID)创建该进程的父进程 PID
文件描述符表打开的文件/管道/套接字
信号处理表各信号的处理方式
用户/组 IDUID、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 + 重定向文件描述符