Linux 系统编程 · 第 9 章:进程创建

Linux 系统编程 · 第 9 章:进程创建

本章深入讲解 Linux 进程创建的完整体系:fork 的写时复制机制、vfork 的特殊语义、exec 族函数的程序替换原理,以及 fork+exec 组合模式、进程间的资源继承与关闭规则。


目录

  1. [fork 深度解析](#fork 深度解析)
  2. 写时复制(Copy-On-Write)
  3. [vfork --- 轻量级进程创建](#vfork — 轻量级进程创建)
  4. [exec 族函数](#exec 族函数)
  5. [fork + exec 组合模式](#fork + exec 组合模式)
  6. 进程资源的继承与关闭
  7. [clone --- 底层进程/线程创建](#clone — 底层进程/线程创建)
  8. 综合实践

1. fork 深度解析

1.1 fork 的完整语义

c 复制代码
#include <unistd.h>
#include <sys/types.h>

/*
 * fork:创建当前进程的副本(子进程)
 *
 * 返回值:
 *   父进程中:返回子进程的 PID(> 0)
 *   子进程中:返回 0
 *   失败:返回 -1,errno 设置为:
 *     EAGAIN  进程数超过限制(RLIMIT_NPROC 或系统限制)
 *     ENOMEM  内存不足,无法复制页表
 *
 * fork 后子进程继承的内容:
 *   ✓ 代码段(共享,只读)
 *   ✓ 数据段、BSS、堆、栈(写时复制)
 *   ✓ 文件描述符表(共享打开文件表项)
 *   ✓ 信号处理函数(disposition)
 *   ✓ 进程组 ID、会话 ID
 *   ✓ 当前工作目录、根目录
 *   ✓ 文件创建掩码(umask)
 *   ✓ 环境变量
 *   ✓ 资源限制(rlimit)
 *   ✓ nice 值
 *
 * fork 后子进程不继承的内容:
 *   ✗ 父进程的 PID(子进程有自己的 PID)
 *   ✗ 父进程的内存锁(mlock)
 *   ✗ 待处理信号(pending signals 清空)
 *   ✗ 文件记录锁(fcntl 锁)
 *   ✗ 定时器(alarm、setitimer)
 *   ✗ 异步 I/O 操作
 *   ✗ 线程(fork 只复制调用线程)
 */
pid_t fork(void);

1.2 fork 的执行流程

c 复制代码
/* 文件名:fork_deep.c
 * 深入演示 fork 的执行流程和各种细节
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <signal.h>
#include <time.h>

/* ── 演示1:fork 后的执行顺序不确定 ── */
void demo_exec_order(void) {
    printf("=== 演示1:fork 后执行顺序不确定 ===\n\n");
    /*
     * fork 后父子进程的执行顺序由调度器决定
     * 不能假设父进程或子进程先执行
     * 需要用 wait/信号/管道等同步机制
     */
    for (int i = 0; i < 3; i++) {
        pid_t pid = fork();
        if (pid == 0) {
            printf("  子进程 %d(PID=%d)执行\n", i, getpid());
            _exit(0);
        }
    }
    /* 等待所有子进程 */
    int status;
    while (wait(&status) > 0);
    printf("  (父子进程执行顺序由调度器决定)\n\n");
}

/* ── 演示2:fork 与信号处理 ── */
static volatile int g_signal_count = 0;
void sig_usr1(int sig) { (void)sig; g_signal_count++; }

void demo_fork_signals(void) {
    printf("=== 演示2:fork 与信号处理 ===\n\n");

    /* 父进程注册信号处理函数 */
    signal(SIGUSR1, sig_usr1);

    /* 向自己发送信号(pending)*/
    raise(SIGUSR1);
    raise(SIGUSR1);
    printf("  fork 前 pending SIGUSR1 数: %d\n", g_signal_count);

    pid_t pid = fork();
    if (pid == 0) {
        /*
         * 子进程继承信号处理函数(disposition)
         * 但不继承待处理信号(pending signals 清空)
         */
        printf("  子进程继承信号处理函数: %s\n",
               signal(SIGUSR1, sig_usr1) != SIG_DFL ? "✓" : "✗");
        printf("  子进程 pending 信号已清空: g_signal_count=%d\n",
               g_signal_count);
        _exit(0);
    }
    waitpid(pid, NULL, 0);
    printf("  父进程 g_signal_count=%d(未受影响)\n\n", g_signal_count);
}

/* ── 演示3:fork 与定时器 ── */
void demo_fork_timer(void) {
    printf("=== 演示3:fork 不继承定时器 ===\n\n");

    /* 设置 2 秒后的 alarm */
    alarm(2);
    printf("  父进程设置 alarm(2)\n");
    printf("  剩余时间: %u 秒\n", alarm(0));   /* 查询并取消 */
    alarm(2);   /* 重新设置 */

    pid_t pid = fork();
    if (pid == 0) {
        /* 子进程不继承定时器 */
        unsigned remaining = alarm(0);   /* 查询子进程的 alarm */
        printf("  子进程的 alarm 剩余: %u 秒(不继承父进程定时器)\n",
               remaining);
        _exit(0);
    }
    waitpid(pid, NULL, 0);
    alarm(0);   /* 取消父进程的 alarm */
    printf("  父进程取消 alarm\n\n");
}

/* ── 演示4:fork 与文件描述符 ── */
void demo_fork_fd(void) {
    printf("=== 演示4:fork 与文件描述符共享 ===\n\n");

    const char *path = "/tmp/fork_fd_demo.txt";
    int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644);

    /* 写入初始内容 */
    write(fd, "ABCDEFGHIJ", 10);
    lseek(fd, 0, SEEK_SET);   /* 回到开头 */

    printf("  fork 前文件偏移: %ld\n", (long)lseek(fd, 0, SEEK_CUR));

    pid_t pid = fork();
    if (pid == 0) {
        /* 子进程:读取3字节(移动共享偏移量)*/
        char buf[4] = {0};
        read(fd, buf, 3);
        printf("  子进程读取3字节: [%s],偏移变为: %ld\n",
               buf, (long)lseek(fd, 0, SEEK_CUR));
        close(fd);
        _exit(0);
    }
    waitpid(pid, NULL, 0);

    /* 父进程:偏移量已被子进程移动 */
    printf("  子进程退出后,父进程文件偏移: %ld(被子进程修改!)\n",
           (long)lseek(fd, 0, SEEK_CUR));

    char buf[8] = {0};
    read(fd, buf, 7);
    printf("  父进程继续读取: [%s]\n\n", buf);

    close(fd);
    unlink(path);
}

/* ── 演示5:fork 失败处理 ── */
void demo_fork_failure(void) {
    printf("=== 演示5:fork 失败处理 ===\n\n");

    /* 临时降低进程数限制来模拟 fork 失败 */
    struct rlimit rl, old_rl;
    getrlimit(RLIMIT_NPROC, &old_rl);

    /* 设置为当前进程数(不允许再创建新进程)*/
    rl.rlim_cur = 1;
    rl.rlim_max = old_rl.rlim_max;
    setrlimit(RLIMIT_NPROC, &rl);

    pid_t pid = fork();
    if (pid == -1) {
        printf("  fork 失败(预期): errno=%d (%s)\n",
               errno, strerror(errno));
    } else if (pid == 0) {
        _exit(0);
    } else {
        waitpid(pid, NULL, 0);
    }

    /* 恢复限制 */
    setrlimit(RLIMIT_NPROC, &old_rl);
    printf("  进程数限制已恢复\n\n");
}

int main(void) {
    demo_exec_order();
    demo_fork_signals();
    demo_fork_timer();
    demo_fork_fd();
    demo_fork_failure();
    return 0;
}
bash 复制代码
gcc -o fork_deep fork_deep.c
./fork_deep
# 输出示例:
# === 演示1:fork 后执行顺序不确定 ===
#   子进程 0(PID=12346)执行
#   子进程 2(PID=12348)执行
#   子进程 1(PID=12347)执行
#   (父子进程执行顺序由调度器决定)
#
# === 演示2:fork 与信号处理 ===
#   fork 前 pending SIGUSR1 数: 2
#   子进程继承信号处理函数: ✓
#   子进程 pending 信号已清空: g_signal_count=0
#   父进程 g_signal_count=2(未受影响)

1.3 多进程 fork 模式

c 复制代码
/* 文件名:fork_patterns.c
 * 演示常见的多进程 fork 模式
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>

/* ── 模式1:顺序 fork(串行创建子进程)── */
void pattern_sequential(int n) {
    printf("【模式1:顺序 fork(父进程创建 %d 个子进程)】\n", n);
    /*
     * 父进程依次创建 n 个子进程
     * 每个子进程都是父进程的直接子进程
     * 子进程之间是兄弟关系
     */
    pid_t pids[16];
    for (int i = 0; i < n; i++) {
        pids[i] = fork();
        if (pids[i] == -1) {
            perror("fork");
            break;
        }
        if (pids[i] == 0) {
            /* 子进程 */
            printf("  子进程 %d: PID=%d, PPID=%d\n",
                   i, getpid(), getppid());
            usleep(100000 * (i + 1));
            _exit(i);
        }
        /* 父进程继续循环 */
    }

    /* 父进程等待所有子进程 */
    for (int i = 0; i < n; i++) {
        if (pids[i] > 0) {
            int status;
            waitpid(pids[i], &status, 0);
            printf("  子进程 %d(PID=%d)退出码: %d\n",
                   i, pids[i], WEXITSTATUS(status));
        }
    }
    printf("\n");
}

/* ── 模式2:链式 fork(进程链)── */
void pattern_chain(int depth) {
    printf("【模式2:链式 fork(进程链,深度=%d)】\n", depth);
    /*
     * 每个进程 fork 一个子进程,形成链式结构
     * 进程0 → 进程1 → 进程2 → ... → 进程n
     * 每个进程的 PPID 是上一个进程的 PID
     */
    int level = 0;
    while (level < depth) {
        pid_t pid = fork();
        if (pid == -1) { perror("fork"); break; }
        if (pid == 0) {
            /* 子进程继续链 */
            level++;
            if (level == depth) {
                /* 链的末端 */
                printf("  链末端: 层级=%d, PID=%d, PPID=%d\n",
                       level, getpid(), getppid());
                _exit(0);
            }
            /* 继续循环创建下一级 */
        } else {
            /* 父进程等待子进程 */
            waitpid(pid, NULL, 0);
            printf("  层级=%d, PID=%d 完成\n", level, getpid());
            break;
        }
    }
    if (level == 0) printf("\n");
}

/* ── 模式3:双重 fork(防止僵尸进程)── */
void pattern_double_fork(void) {
    printf("【模式3:双重 fork(防止僵尸进程)】\n");
    /*
     * 双重 fork 技巧:
     * 父进程 fork 出子进程1
     * 子进程1 再 fork 出子进程2(孙进程)
     * 子进程1 立即退出
     * 孙进程被 init 收养,父进程无需等待孙进程
     * 孙进程退出时由 init 回收,不会变成僵尸
     */
    pid_t child1 = fork();
    if (child1 == -1) { perror("fork"); return; }

    if (child1 == 0) {
        /* 子进程1:立即再 fork */
        pid_t child2 = fork();
        if (child2 == -1) { perror("fork"); _exit(1); }

        if (child2 == 0) {
            /* 孙进程:被 init 收养,执行实际工作 */
            printf("  孙进程 PID=%d, PPID=%d(将被 init 收养)\n",
                   getpid(), getppid());
            usleep(100000);
            printf("  孙进程 PID=%d, PPID=%d(已被 init 收养)\n",
                   getpid(), getppid());
            _exit(0);
        }

        /* 子进程1:立即退出(孙进程变为孤儿,被 init 收养)*/
        printf("  子进程1 PID=%d 立即退出\n", getpid());
        _exit(0);
    }

    /* 父进程:只需等待子进程1(很快退出)*/
    int status;
    waitpid(child1, &status, 0);
    printf("  父进程:子进程1已退出,无需等待孙进程\n");
    usleep(200000);   /* 等孙进程输出 */
    printf("\n");
}

/* ── 模式4:fork 后立即 exec(最常见模式)── */
void pattern_fork_exec(void) {
    printf("【模式4:fork + exec(执行外部命令)】\n");

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

    if (pid == 0) {
        /* 子进程:执行外部命令 */
        execlp("echo", "echo", "Hello from exec!", NULL);
        /* exec 失败才会到这里 */
        perror("execlp");
        _exit(127);
    }

    int status;
    waitpid(pid, &status, 0);
    printf("  外部命令退出码: %d\n\n", WEXITSTATUS(status));
}

int main(void) {
    pattern_sequential(3);
    pattern_chain(3);
    pattern_double_fork();
    pattern_fork_exec();
    return 0;
}
bash 复制代码
gcc -o fork_patterns fork_patterns.c
./fork_patterns
# 输出示例:
# 【模式1:顺序 fork(父进程创建 3 个子进程)】
#   子进程 0: PID=12346, PPID=12345
#   子进程 1: PID=12347, PPID=12345
#   子进程 2: PID=12348, PPID=12345
#   子进程 0(PID=12346)退出码: 0
#   子进程 1(PID=12347)退出码: 1
#   子进程 2(PID=12348)退出码: 2

2. 写时复制(Copy-On-Write)

2.1 COW 原理

复制代码
写时复制(Copy-On-Write,COW)机制:
─────────────────────────────────────────────────────────────
  fork 后的内存状态:

  父进程页表                    子进程页表
  ┌──────────────────┐          ┌──────────────────┐
  │ 虚拟页A → 物理页1│          │ 虚拟页A → 物理页1│ ← 共享!
  │ 虚拟页B → 物理页2│          │ 虚拟页B → 物理页2│ ← 共享!
  │ 虚拟页C → 物理页3│          │ 虚拟页C → 物理页3│ ← 共享!
  └──────────────────┘          └──────────────────┘
  所有页标记为只读(即使原来可写)

  当子进程写入虚拟页A时:
  1. 触发写保护异常(Page Fault)
  2. 内核分配新的物理页4
  3. 将物理页1的内容复制到物理页4
  4. 更新子进程页表:虚拟页A → 物理页4
  5. 恢复物理页1的写权限(父进程可写)
  6. 子进程继续执行写操作

  结果:
  父进程页表                    子进程页表
  ┌──────────────────┐          ┌──────────────────┐
  │ 虚拟页A → 物理页1│(可写)  │ 虚拟页A → 物理页4│(新页)
  │ 虚拟页B → 物理页2│(共享)  │ 虚拟页B → 物理页2│(共享)
  │ 虚拟页C → 物理页3│(共享)  │ 虚拟页C → 物理页3│(共享)
  └──────────────────┘          └──────────────────┘

  优势:
  ✓ fork 速度极快(只复制页表,不复制物理内存)
  ✓ 如果子进程立即 exec,几乎不需要复制任何内存
  ✓ 只读数据(代码段、常量)永远共享,节省内存
─────────────────────────────────────────────────────────────
c 复制代码
/* 文件名:cow_demo.c
 * 演示写时复制的行为和性能影响
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <time.h>

static long long now_ns(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1000000000LL + ts.tv_nsec;
}

/* 读取进程的物理内存使用量(RSS)*/
long get_rss_kb(void) {
    FILE *fp = fopen("/proc/self/status", "r");
    if (!fp) return -1;
    char line[128];
    long rss = 0;
    while (fgets(line, sizeof(line), fp)) {
        if (strncmp(line, "VmRSS:", 6) == 0) {
            sscanf(line + 6, " %ld", &rss);
            break;
        }
    }
    fclose(fp);
    return rss;
}

int main(void) {
    const size_t MEM_SIZE = 64 * 1024 * 1024;   /* 64MB */

    printf("=== 写时复制(COW)演示 ===\n\n");

    /* 分配 64MB 内存并写入数据 */
    char *data = malloc(MEM_SIZE);
    memset(data, 0xAB, MEM_SIZE);
    printf("父进程分配并写入 64MB 内存\n");
    printf("父进程 RSS: %ld KB\n\n", get_rss_kb());

    /* ── 场景1:子进程只读(COW 不触发)── */
    printf("【场景1:子进程只读(COW 不触发)】\n");
    long long t1 = now_ns();
    pid_t pid = fork();
    long long t2 = now_ns();

    if (pid == 0) {
        /* 子进程:只读数据,不触发 COW */
        long sum = 0;
        for (size_t i = 0; i < MEM_SIZE; i += 4096)
            sum += data[i];   /* 只读 */
        printf("  子进程只读,RSS: %ld KB(与父进程共享物理页)\n",
               get_rss_kb());
        (void)sum;
        _exit(0);
    }
    printf("  fork 耗时: %lld μs(只复制页表)\n",
           (t2 - t1) / 1000);
    waitpid(pid, NULL, 0);

    /* ── 场景2:子进程写入(触发 COW)── */
    printf("\n【场景2:子进程写入(触发 COW)】\n");
    t1 = now_ns();
    pid = fork();
    t2 = now_ns();

    if (pid == 0) {
        printf("  子进程 fork 后 RSS: %ld KB\n", get_rss_kb());

        /* 写入所有页,触发 COW */
        long long write_start = now_ns();
        memset(data, 0xCD, MEM_SIZE);   /* 触发 COW,复制所有页 */
        long long write_end = now_ns();

        printf("  子进程写入后 RSS: %ld KB(COW 触发,复制了物理页)\n",
               get_rss_kb());
        printf("  COW 写入耗时: %lld ms\n",
               (write_end - write_start) / 1000000);
        _exit(0);
    }
    printf("  fork 耗时: %lld μs\n", (t2 - t1) / 1000);
    waitpid(pid, NULL, 0);

    /* ── 场景3:验证 COW 的独立性 ── */
    printf("\n【场景3:验证 COW 独立性】\n");
    int shared_val = 42;
    printf("  fork 前 shared_val = %d,地址 = %p\n",
           shared_val, (void *)&shared_val);

    pid = fork();
    if (pid == 0) {
        printf("  子进程看到 shared_val = %d,地址 = %p(相同虚拟地址)\n",
               shared_val, (void *)&shared_val);
        shared_val = 999;   /* 触发 COW */
        printf("  子进程修改后 shared_val = %d,地址 = %p(新物理页)\n",
               shared_val, (void *)&shared_val);
        _exit(0);
    }
    waitpid(pid, NULL, 0);
    printf("  父进程的 shared_val = %d(未受子进程修改影响)\n\n",
           shared_val);

    free(data);
    return 0;
}
bash 复制代码
gcc -O2 -o cow_demo cow_demo.c
./cow_demo
# 输出示例:
# === 写时复制(COW)演示 ===
#
# 父进程分配并写入 64MB 内存
# 父进程 RSS: 65536 KB
#
# 【场景1:子进程只读(COW 不触发)】
#   fork 耗时: 234 μs(只复制页表)
#   子进程只读,RSS: 65540 KB(与父进程共享物理页)
#
# 【场景2:子进程写入(触发 COW)】
#   fork 耗时: 198 μs
#   子进程 fork 后 RSS: 65540 KB
#   子进程写入后 RSS: 131072 KB(COW 触发,复制了物理页)
#   COW 写入耗时: 45 ms
#
# 【场景3:验证 COW 独立性】
#   fork 前 shared_val = 42,地址 = 0x7ffd...
#   子进程看到 shared_val = 42,地址 = 0x7ffd...(相同虚拟地址)
#   子进程修改后 shared_val = 999,地址 = 0x7ffd...(新物理页)
#   父进程的 shared_val = 42(未受子进程修改影响)

3. vfork --- 轻量级进程创建

3.1 vfork 的特殊语义

c 复制代码
/* 文件名:vfork_demo.c
 * 演示 vfork 的特殊语义和使用限制
 *
 * ⚠️ 注意:vfork 在现代 Linux 中已基本废弃
 * 现代 fork + COW 已足够高效
 * 本节仅用于理解历史背景和底层机制
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int g_value = 10;

int main(void) {
    printf("=== vfork vs fork 对比 ===\n\n");

    printf("【vfork 的特殊语义】\n");
    printf("  1. 子进程与父进程共享地址空间(不复制页表!)\n");
    printf("  2. 父进程被挂起,直到子进程调用 exec 或 _exit\n");
    printf("  3. 子进程必须立即 exec 或 _exit,不能做其他操作\n");
    printf("  4. 子进程修改变量会影响父进程(共享地址空间)\n\n");

    printf("【vfork 演示】\n");
    printf("  vfork 前: g_value = %d\n", g_value);

    pid_t pid = vfork();

    if (pid == 0) {
        /*
         * 子进程:与父进程共享地址空间
         * 修改变量会影响父进程!
         * 必须立即 exec 或 _exit
         */
        g_value = 999;   /* 修改共享地址空间中的变量 */
        printf("  子进程修改 g_value = %d\n", g_value);

        /*
         * ⚠️ 必须用 _exit,不能用 exit!
         * exit 会调用 atexit 函数和刷新 stdio 缓冲区
         * 这会破坏父进程的 stdio 状态
         */
        _exit(0);
    }

    /*
     * 父进程:子进程调用 _exit 后才继续执行
     * 由于共享地址空间,g_value 已被子进程修改
     */
    printf("  父进程(子进程退出后): g_value = %d(被子进程修改!)\n\n",
           g_value);

    /* ── vfork 的正确用法:立即 exec ── */
    printf("【vfork 的正确用法:立即 exec】\n");
    pid = vfork();
    if (pid == 0) {
        /* 正确:立即执行 exec */
        execlp("echo", "echo", "vfork + exec 正确用法", NULL);
        _exit(127);   /* exec 失败 */
    }
    int status;
    waitpid(pid, &status, 0);

    /* ── vfork vs fork 性能对比 ── */
    printf("\n【vfork vs fork 性能对比(各 10000 次)】\n");

    struct timespec t1, t2;
    int ROUNDS = 10000;

    /* fork 性能 */
    clock_gettime(CLOCK_MONOTONIC, &t1);
    for (int i = 0; i < ROUNDS; i++) {
        pid_t p = fork();
        if (p == 0) _exit(0);
        waitpid(p, NULL, 0);
    }
    clock_gettime(CLOCK_MONOTONIC, &t2);
    long long fork_ns = (t2.tv_sec - t1.tv_sec) * 1000000000LL
                      + (t2.tv_nsec - t1.tv_nsec);
    printf("  fork:  %lld μs/次\n", fork_ns / ROUNDS / 1000);

    /* vfork 性能 */
    clock_gettime(CLOCK_MONOTONIC, &t1);
    for (int i = 0; i < ROUNDS; i++) {
        pid_t p = vfork();
        if (p == 0) _exit(0);
        waitpid(p, NULL, 0);
    }
    clock_gettime(CLOCK_MONOTONIC, &t2);
    long long vfork_ns = (t2.tv_sec - t1.tv_sec) * 1000000000LL
                       + (t2.tv_nsec - t1.tv_nsec);
    printf("  vfork: %lld μs/次\n", vfork_ns / ROUNDS / 1000);
    printf("  vfork 比 fork 快约 %.1f 倍\n",
           (double)fork_ns / vfork_ns);

    return 0;
}
bash 复制代码
gcc -o vfork_demo vfork_demo.c
./vfork_demo
# 输出示例:
# === vfork vs fork 对比 ===
#
# 【vfork 演示】
#   vfork 前: g_value = 10
#   子进程修改 g_value = 999
#   父进程(子进程退出后): g_value = 999(被子进程修改!)
#
# 【vfork vs fork 性能对比(各 10000 次)】
#   fork:  120 μs/次
#   vfork:  45 μs/次
#   vfork 比 fork 快约 2.7 倍

4. exec 族函数

4.1 exec 族函数总览

c 复制代码
#include <unistd.h>

/*
 * exec 族函数:用新程序替换当前进程的映像
 *
 * exec 不创建新进程,而是:
 *   1. 加载新程序的代码段、数据段
 *   2. 重置堆、栈
 *   3. 保留 PID、PPID、打开的 fd(除非设置了 FD_CLOEXEC)
 *   4. 从新程序的 main 函数开始执行
 *
 * 函数命名规律:
 *   l = list(参数列表,以 NULL 结尾)
 *   v = vector(参数数组 char *argv[])
 *   p = path(在 PATH 中搜索程序)
 *   e = environment(自定义环境变量)
 */

/* 参数列表 + 不搜索 PATH */
int execl(const char *path, const char *arg, ... /*, NULL */);

/* 参数列表 + 搜索 PATH */
int execlp(const char *file, const char *arg, ... /*, NULL */);

/* 参数列表 + 自定义环境变量 */
int execle(const char *path, const char *arg, ... /*, NULL, char *const envp[] */);

/* 参数数组 + 不搜索 PATH */
int execv(const char *path, char *const argv[]);

/* 参数数组 + 搜索 PATH */
int execvp(const char *file, char *const argv[]);

/* 参数数组 + 自定义环境变量 */
int execve(const char *path, char *const argv[], char *const envp[]);

/* 参数数组 + fd 指定程序(Linux 特有,安全)*/
int execveat(int dirfd, const char *pathname,
             char *const argv[], char *const envp[], int flags);

/*
 * 所有 exec 函数:
 *   成功:不返回(进程映像已被替换)
 *   失败:返回 -1,设置 errno
 */

4.2 exec 族函数详细演示

c 复制代码
/* 文件名:exec_family.c
 * 演示 exec 族函数的各种用法
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>

/* 辅助:fork 后在子进程中执行,父进程等待 */
#define RUN_IN_CHILD(code) do { \
    pid_t _pid = fork(); \
    if (_pid == 0) { code; _exit(127); } \
    int _st; waitpid(_pid, &_st, 0); \
    printf("  退出码: %d\n", WEXITSTATUS(_st)); \
} while(0)

int main(void) {
    printf("=== exec 族函数演示 ===\n\n");

    /* ── execl:参数列表,绝对路径 ── */
    printf("【execl:参数列表 + 绝对路径】\n");
    RUN_IN_CHILD({
        printf("  执行: execl(\"/bin/echo\", ...)\n");
        fflush(stdout);
        execl("/bin/echo", "echo", "Hello from execl!", NULL);
        perror("execl");
    });

    /* ── execlp:参数列表,搜索 PATH ── */
    printf("\n【execlp:参数列表 + 搜索 PATH】\n");
    RUN_IN_CHILD({
        printf("  执行: execlp(\"echo\", ...)\n");
        fflush(stdout);
        execlp("echo", "echo", "Hello from execlp!", NULL);
        perror("execlp");
    });

    /* ── execv:参数数组,绝对路径 ── */
    printf("\n【execv:参数数组 + 绝对路径】\n");
    RUN_IN_CHILD({
        char *args[] = { "ls", "-la", "/tmp", NULL };
        printf("  执行: execv(\"/bin/ls\", args)\n");
        fflush(stdout);
        execv("/bin/ls", args);
        perror("execv");
    });

    /* ── execvp:参数数组,搜索 PATH ── */
    printf("\n【execvp:参数数组 + 搜索 PATH】\n");
    RUN_IN_CHILD({
        char *args[] = { "uname", "-r", NULL };
        printf("  执行: execvp(\"uname\", args)\n");
        fflush(stdout);
        execvp("uname", args);
        perror("execvp");
    });

    /* ── execle:参数列表 + 自定义环境变量 ── */
    printf("\n【execle:参数列表 + 自定义环境变量】\n");
    RUN_IN_CHILD({
        char *envp[] = {
            "MY_VAR=hello_from_execle",
            "PATH=/usr/bin:/bin",
            NULL
        };
        printf("  执行: execle(\"/usr/bin/env\", ...)\n");
        fflush(stdout);
        execle("/usr/bin/env", "env", NULL, envp);
        perror("execle");
    });

    /* ── execve:最底层,参数数组 + 自定义环境变量 ── */
    printf("\n【execve:底层接口(其他 exec 函数的基础)】\n");
    RUN_IN_CHILD({
        char *args[] = { "printenv", "EXEC_TEST_VAR", NULL };
        char *envp[] = {
            "EXEC_TEST_VAR=execve_value",
            "PATH=/usr/bin:/bin",
            NULL
        };
        printf("  执行: execve(\"/usr/bin/printenv\", args, envp)\n");
        fflush(stdout);
        execve("/usr/bin/printenv", args, envp);
        perror("execve");
    });

    /* ── exec 失败的情况 ── */
    printf("\n【exec 失败情况】\n");
    RUN_IN_CHILD({
        /* 文件不存在 */
        execl("/nonexistent/program", "program", NULL);
        printf("  execl 失败: errno=%d (%s)\n",
               errno, strerror(errno));
        _exit(1);
    });

    RUN_IN_CHILD({
        /* 没有执行权限 */
        execl("/etc/passwd", "passwd", NULL);
        printf("  execl 失败: errno=%d (%s)\n",
               errno, strerror(errno));
        _exit(1);
    });

    /* ── exec 后保留的属性 ── */
    printf("\n【exec 后保留的进程属性】\n");
    printf("  ✓ PID、PPID、PGID、SID\n");
    printf("  ✓ 打开的文件描述符(除非设置 FD_CLOEXEC)\n");
    printf("  ✓ 当前工作目录\n");
    printf("  ✓ 文件创建掩码(umask)\n");
    printf("  ✓ 资源限制(rlimit)\n");
    printf("  ✓ 进程组、会话\n");
    printf("  ✗ 信号处理函数(重置为默认)\n");
    printf("  ✗ 内存映射(mmap)\n");
    printf("  ✗ 线程(只保留调用 exec 的线程)\n");
    printf("  ✗ 内存锁(mlock)\n");

    return 0;
}
bash 复制代码
gcc -o exec_family exec_family.c
./exec_family
# 输出示例:
# === exec 族函数演示 ===
#
# 【execl:参数列表 + 绝对路径】
#   执行: execl("/bin/echo", ...)
#   Hello from execl!
#   退出码: 0
#
# 【execlp:参数列表 + 搜索 PATH】
#   执行: execlp("echo", ...)
#   Hello from execlp!
#   退出码: 0
#
# 【execle:参数列表 + 自定义环境变量】
#   执行: execle("/usr/bin/env", ...)
#   MY_VAR=hello_from_execle
#   PATH=/usr/bin:/bin
#   退出码: 0

4.3 exec 的内部机制

c 复制代码
/* 文件名:exec_internals.c
 * 演示 exec 的内部机制:PID 保留、fd 继承、信号重置
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>

/*
 * 这个程序被设计为既可以作为"父程序"运行
 * 也可以作为 exec 后的"子程序"运行
 * 通过环境变量 EXEC_ROLE 区分
 */
int main(int argc, char *argv[]) {
    char *role = getenv("EXEC_ROLE");

    if (role && strcmp(role, "child") == 0) {
        /* ── 作为 exec 后的子程序运行 ── */
        printf("\n  [exec 后的子程序]\n");
        printf("  PID = %d(与 exec 前相同)\n", getpid());
        printf("  PPID = %d\n", getppid());

        /* 检查继承的 fd */
        printf("  继承的 fd:\n");
        for (int fd = 0; fd <= 10; fd++) {
            if (fcntl(fd, F_GETFD) != -1) {
                char link[256] = {0};
                char path[64];
                snprintf(path, sizeof(path), "/proc/self/fd/%d", fd);
                readlink(path, link, sizeof(link) - 1);
                printf("    fd=%d → %s\n", fd, link);
            }
        }

        /* 检查信号处理(exec 后应重置为默认)*/
        struct sigaction sa;
        sigaction(SIGUSR1, NULL, &sa);
        printf("  SIGUSR1 处理函数: %s(exec 后重置为默认)\n",
               sa.sa_handler == SIG_DFL ? "SIG_DFL ✓" :
               sa.sa_handler == SIG_IGN ? "SIG_IGN" : "自定义");

        /* 检查环境变量 */
        printf("  EXEC_ROLE = %s\n", getenv("EXEC_ROLE"));
        printf("  INHERITED_VAR = %s\n",
               getenv("INHERITED_VAR") ? getenv("INHERITED_VAR") : "(未设置)");
        return 0;
    }

    /* ── 作为父程序运行 ── */
    printf("=== exec 内部机制演示 ===\n\n");

    /* 设置信号处理函数(exec 后会重置)*/
    signal(SIGUSR1, SIG_IGN);   /* 忽略 SIGUSR1 */
    printf("设置 SIGUSR1 为 SIG_IGN\n");

    /* 打开一个文件(测试 fd 继承)*/
    int fd3 = open("/etc/hostname", O_RDONLY);
    int fd4 = open("/etc/hostname", O_RDONLY | O_CLOEXEC);
    printf("打开 fd=%d(无 FD_CLOEXEC,exec 后继承)\n", fd3);
    printf("打开 fd=%d(有 FD_CLOEXEC,exec 后关闭)\n", fd4);

    /* 设置环境变量 */
    setenv("EXEC_ROLE", "child", 1);
    setenv("INHERITED_VAR", "from_parent", 1);

    printf("当前 PID = %d\n", getpid());
    printf("\nexecv 替换进程映像...\n");
    fflush(stdout);

    /* exec 替换当前进程 */
    char *args[] = { argv[0], NULL };
    execv(argv[0], args);

    /* 只有 exec 失败才会到这里 */
    perror("execv");
    return 1;
}
bash 复制代码
gcc -o exec_internals exec_internals.c
./exec_internals
# 输出示例:
# === exec 内部机制演示 ===
#
# 设置 SIGUSR1 为 SIG_IGN
# 打开 fd=3(无 FD_CLOEXEC,exec 后继承)
# 打开 fd=4(有 FD_CLOEXEC,exec 后关闭)
# 当前 PID = 12345
#
# execv 替换进程映像...
#
#   [exec 后的子程序]
#   PID = 12345(与 exec 前相同)← PID 保留!
#   PPID = 12344
#   继承的 fd:
#     fd=0 → /dev/pts/0
#     fd=1 → /dev/pts/0
#     fd=2 → /dev/pts/0
#     fd=3 → /etc/hostname  ← 继承(无 FD_CLOEXEC)
#     (fd=4 已关闭,有 FD_CLOEXEC)
#   SIGUSR1 处理函数: SIG_DFL ✓(exec 后重置为默认)
#   INHERITED_VAR = from_parent  ← 环境变量继承

5. fork + exec 组合模式

5.1 标准 shell 命令执行模型

c 复制代码
/* 文件名:fork_exec_shell.c
 * 实现一个简单的 shell 命令执行器
 * 演示 fork + exec 的标准组合模式
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>

/* 执行命令并等待完成 */
int run_command(char *const argv[], char *const envp[]) {
    pid_t pid = fork();
    if (pid == -1) {
        perror("fork");
        return -1;
    }

    if (pid == 0) {
        /* 子进程:执行命令 */

        /* 关闭不需要的 fd(安全实践)*/
        /* 实际 shell 会在这里设置重定向 */

        if (envp) {
            execvpe(argv[0], argv, envp);
        } else {
            execvp(argv[0], argv);
        }

        /* exec 失败 */
        fprintf(stderr, "exec '%s' 失败: %s\n",
                argv[0], strerror(errno));
        _exit(127);   /* 127 = 命令未找到(shell 约定)*/
    }

    /* 父进程:等待子进程完成 */
    int status;
    if (waitpid(pid, &status, 0) == -1) {
        perror("waitpid");
        return -1;
    }

    if (WIFEXITED(status))   return WEXITSTATUS(status);
    if (WIFSIGNALED(status)) return 128 + WTERMSIG(status);
    return -1;
}

/* 带输出重定向的命令执行 */
int run_with_redirect(char *const argv[],
                      const char *stdout_file,
                      const char *stdin_file) {
    pid_t pid = fork();
    if (pid == -1) { perror("fork"); return -1; }

    if (pid == 0) {
        /* 重定向 stdout */
        if (stdout_file) {
            int fd = open(stdout_file,
                          O_WRONLY | O_CREAT | O_TRUNC, 0644);
            if (fd == -1) { perror("open stdout"); _exit(1); }
            dup2(fd, STDOUT_FILENO);
            close(fd);
        }

        /* 重定向 stdin */
        if (stdin_file) {
            int fd = open(stdin_file, O_RDONLY);
            if (fd == -1) { perror("open stdin"); _exit(1); }
            dup2(fd, STDIN_FILENO);
            close(fd);
        }

        execvp(argv[0], argv);
        perror("execvp");
        _exit(127);
    }

    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

/* 管道连接两个命令(cmd1 | cmd2)*/
int run_pipeline(char *const cmd1[], char *const cmd2[]) {
    int pipefd[2];
    if (pipe(pipefd) == -1) { perror("pipe"); return -1; }

    /* 创建 cmd1 进程 */
    pid_t pid1 = fork();
    if (pid1 == -1) { perror("fork cmd1"); return -1; }

    if (pid1 == 0) {
        /* cmd1:stdout → 管道写端 */
        close(pipefd[0]);
        dup2(pipefd[1], STDOUT_FILENO);
        close(pipefd[1]);
        execvp(cmd1[0], cmd1);
        perror("execvp cmd1");
        _exit(127);
    }

    /* 创建 cmd2 进程 */
    pid_t pid2 = fork();
    if (pid2 == -1) { perror("fork cmd2"); return -1; }

    if (pid2 == 0) {
        /* cmd2:stdin ← 管道读端 */
        close(pipefd[1]);
        dup2(pipefd[0], STDIN_FILENO);
        close(pipefd[0]);
        execvp(cmd2[0], cmd2);
        perror("execvp cmd2");
        _exit(127);
    }

    /* 父进程:关闭管道,等待两个子进程 */
    close(pipefd[0]);
    close(pipefd[1]);

    int status1, status2;
    waitpid(pid1, &status1, 0);
    waitpid(pid2, &status2, 0);

    return WIFEXITED(status2) ? WEXITSTATUS(status2) : -1;
}

int main(void) {
    printf("=== fork + exec 组合模式演示 ===\n\n");

    /* ── 基本命令执行 ── */
    printf("【基本命令执行】\n");
    char *cmd1[] = { "echo", "Hello from fork+exec!", NULL };
    int ret = run_command(cmd1, NULL);
    printf("退出码: %d\n\n", ret);

    /* ── 带自定义环境变量 ── */
    printf("【自定义环境变量】\n");
    char *cmd2[] = { "printenv", "CUSTOM_VAR", NULL };
    char *envp[] = { "CUSTOM_VAR=custom_value",
                     "PATH=/usr/bin:/bin", NULL };
    ret = run_command(cmd2, envp);
    printf("退出码: %d\n\n", ret);

    /* ── 输出重定向 ── */
    printf("【输出重定向到文件】\n");
    char *cmd3[] = { "ls", "/etc", NULL };
    ret = run_with_redirect(cmd3, "/tmp/ls_output.txt", NULL);
    printf("ls /etc 输出已重定向到 /tmp/ls_output.txt,退出码: %d\n", ret);
    system("wc -l /tmp/ls_output.txt");
    unlink("/tmp/ls_output.txt");
    printf("\n");

    /* ── 管道 ── */
    printf("【管道:ls /etc | grep conf】\n");
    char *pipe_cmd1[] = { "ls", "/etc", NULL };
    char *pipe_cmd2[] = { "grep", "conf", NULL };
    ret = run_pipeline(pipe_cmd1, pipe_cmd2);
    printf("管道退出码: %d\n\n", ret);

    /* ── 命令不存在的处理 ── */
    printf("【命令不存在的处理】\n");
    char *cmd4[] = { "nonexistent_command_xyz", NULL };
    ret = run_command(cmd4, NULL);
    printf("退出码: %d(127 = 命令未找到)\n", ret);

    return 0;
}
bash 复制代码
gcc -o fork_exec_shell fork_exec_shell.c
./fork_exec_shell
# 输出示例:
# === fork + exec 组合模式演示 ===
#
# 【基本命令执行】
# Hello from fork+exec!
# 退出码: 0
#
# 【自定义环境变量】
# custom_value
# 退出码: 0
#
# 【管道:ls /etc | grep conf】
# adduser.conf
# ca-certificates.conf
# ...
# 管道退出码: 0

5.2 posix_spawn --- fork+exec 的高效替代

c 复制代码
/* 文件名:posix_spawn_demo.c
 * 演示 posix_spawn:fork+exec 的高效替代
 * 在某些平台上比 fork+exec 更高效(避免 COW 开销)
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <spawn.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <time.h>

extern char **environ;

static long long now_ns(void) {
    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);
    return ts.tv_sec * 1000000000LL + ts.tv_nsec;
}

int main(void) {
    printf("=== posix_spawn 演示 ===\n\n");

    /* ── 基本用法 ── */
    printf("【基本用法】\n");
    pid_t pid;
    char *args[] = { "echo", "Hello from posix_spawn!", NULL };

    int ret = posix_spawn(&pid, "/bin/echo", NULL, NULL, args, environ);
    if (ret != 0) {
        fprintf(stderr, "posix_spawn 失败: %s\n", strerror(ret));
        return 1;
    }
    printf("posix_spawn 创建进程 PID=%d\n", pid);

    int status;
    waitpid(pid, &status, 0);
    printf("退出码: %d\n\n", WEXITSTATUS(status));

    /* ── 带文件操作(重定向)── */
    printf("【带文件操作(输出重定向)】\n");

    posix_spawn_file_actions_t file_actions;
    posix_spawn_file_actions_init(&file_actions);

    /* 将 stdout 重定向到文件 */
    posix_spawn_file_actions_addopen(&file_actions,
                                      STDOUT_FILENO,
                                      "/tmp/spawn_output.txt",
                                      O_WRONLY | O_CREAT | O_TRUNC,
                                      0644);

    char *args2[] = { "ls", "/etc", NULL };
    ret = posix_spawn(&pid, "/bin/ls", &file_actions, NULL, args2, environ);
    if (ret == 0) {
        waitpid(pid, &status, 0);
        printf("ls /etc 输出已重定向,退出码: %d\n", WEXITSTATUS(status));
        system("wc -l /tmp/spawn_output.txt");
        unlink("/tmp/spawn_output.txt");
    }
    posix_spawn_file_actions_destroy(&file_actions);

    /* ── 带属性设置 ── */
    printf("\n【带属性设置(进程组)】\n");

    posix_spawnattr_t attr;
    posix_spawnattr_init(&attr);

    /* 设置子进程为新进程组的领导 */
    posix_spawnattr_setflags(&attr, POSIX_SPAWN_SETPGROUP);
    posix_spawnattr_setpgroup(&attr, 0);   /* 0 = 使用子进程 PID */

    char *args3[] = { "echo", "new process group", NULL };
    ret = posix_spawn(&pid, "/bin/echo", NULL, &attr, args3, environ);
    if (ret == 0) {
        waitpid(pid, &status, 0);
        printf("子进程 PID=%d 在新进程组中\n", pid);
    }
    posix_spawnattr_destroy(&attr);

    /* ── 性能对比:posix_spawn vs fork+exec ── */
    printf("\n【性能对比(各 1000 次)】\n");
    int ROUNDS = 1000;
    long long t1, t2;

    /* fork+exec */
    t1 = now_ns();
    for (int i = 0; i < ROUNDS; i++) {
        pid_t p = fork();
        if (p == 0) {
            execl("/bin/true", "true", NULL);
            _exit(1);
        }
        waitpid(p, NULL, 0);
    }
    t2 = now_ns();
    printf("  fork+exec:     %lld μs/次\n",
           (t2 - t1) / ROUNDS / 1000);

    /* posix_spawn */
    char *true_args[] = { "true", NULL };
    t1 = now_ns();
    for (int i = 0; i < ROUNDS; i++) {
        posix_spawn(&pid, "/bin/true", NULL, NULL, true_args, environ);
        waitpid(pid, NULL, 0);
    }
    t2 = now_ns();
    printf("  posix_spawn:   %lld μs/次\n",
           (t2 - t1) / ROUNDS / 1000);

    return 0;
}
bash 复制代码
gcc -o posix_spawn_demo posix_spawn_demo.c
./posix_spawn_demo
# 输出示例:
# === posix_spawn 演示 ===
#
# 【基本用法】
# Hello from posix_spawn!
# posix_spawn 创建进程 PID=12346
# 退出码: 0
#
# 【性能对比(各 1000 次)】
#   fork+exec:     245 μs/次
#   posix_spawn:   198 μs/次

6. 进程资源的继承与关闭

6.1 fork 后的资源管理

c 复制代码
/* 文件名:resource_inherit.c
 * 演示 fork/exec 后各种资源的继承规则
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <sys/mman.h>
#include <pthread.h>

/* ── 测试1:文件描述符继承 ── */
void test_fd_inherit(void) {
    printf("=== 文件描述符继承规则 ===\n\n");

    /* 打开各种类型的 fd */
    int fd_normal   = open("/etc/hostname", O_RDONLY);
    int fd_cloexec  = open("/etc/hostname", O_RDONLY | O_CLOEXEC);
    int pipefd[2];
    pipe(pipefd);

    printf("fd_normal  = %d(无 FD_CLOEXEC)\n", fd_normal);
    printf("fd_cloexec = %d(有 FD_CLOEXEC)\n", fd_cloexec);
    printf("pipe 读端  = %d,写端 = %d\n\n", pipefd[0], pipefd[1]);

    pid_t pid = fork();
    if (pid == 0) {
        printf("【fork 后子进程的 fd】\n");
        printf("  fd_normal  (%d): %s\n", fd_normal,
               fcntl(fd_normal, F_GETFD) != -1 ? "继承 ✓" : "未继承");
        printf("  fd_cloexec (%d): %s(fork 不受 FD_CLOEXEC 影响)\n",
               fd_cloexec,
               fcntl(fd_cloexec, F_GETFD) != -1 ? "继承 ✓" : "未继承");
        printf("  pipe 读端  (%d): %s\n", pipefd[0],
               fcntl(pipefd[0], F_GETFD) != -1 ? "继承 ✓" : "未继承");

        /* exec 后 FD_CLOEXEC 的 fd 会被关闭 */
        printf("\n【exec 后的 fd(通过 /proc 验证)】\n");
        /* 设置 FD_CLOEXEC 标志 */
        fcntl(fd_normal, F_SETFD, FD_CLOEXEC);   /* 现在设置 */

        char fd_str[8], cloexec_str[8];
        snprintf(fd_str, sizeof(fd_str), "%d", fd_normal);
        snprintf(cloexec_str, sizeof(cloexec_str), "%d", fd_cloexec);

        /* 执行一个程序来检查 fd */
        char *args[] = { "sh", "-c",
            "ls /proc/self/fd 2>/dev/null", NULL };
        execvp("sh", args);
        _exit(0);
    }
    waitpid(pid, NULL, 0);

    close(fd_normal);
    close(fd_cloexec);
    close(pipefd[0]);
    close(pipefd[1]);
    printf("\n");
}

/* ── 测试2:信号处理继承 ── */
void sig_handler(int sig) {
    printf("  收到信号 %d\n", sig);
}

void test_signal_inherit(void) {
    printf("=== 信号处理继承规则 ===\n\n");

    /* 设置各种信号处理 */
    signal(SIGUSR1, sig_handler);   /* 自定义处理函数 */
    signal(SIGUSR2, SIG_IGN);       /* 忽略 */
    /* SIGTERM 保持默认(SIG_DFL)*/

    printf("父进程信号设置:\n");
    printf("  SIGUSR1: 自定义处理函数\n");
    printf("  SIGUSR2: SIG_IGN(忽略)\n");
    printf("  SIGTERM: SIG_DFL(默认)\n\n");

    pid_t pid = fork();
    if (pid == 0) {
        struct sigaction sa;

        sigaction(SIGUSR1, NULL, &sa);
        printf("【fork 后子进程】\n");
        printf("  SIGUSR1: %s(继承自定义处理函数)\n",
               sa.sa_handler == sig_handler ? "自定义 ✓" : "其他");

        sigaction(SIGUSR2, NULL, &sa);
        printf("  SIGUSR2: %s(继承 SIG_IGN)\n",
               sa.sa_handler == SIG_IGN ? "SIG_IGN ✓" : "其他");

        /* exec 后信号处理重置 */
        printf("\n  exec 后信号处理规则:\n");
        printf("  ✓ SIG_DFL → 保持 SIG_DFL\n");
        printf("  ✓ SIG_IGN → 保持 SIG_IGN(重要!)\n");
        printf("  ✗ 自定义函数 → 重置为 SIG_DFL\n");
        _exit(0);
    }
    waitpid(pid, NULL, 0);
    printf("\n");
}

/* ── 测试3:资源限制继承 ── */
void test_rlimit_inherit(void) {
    printf("=== 资源限制继承 ===\n\n");

    /* 修改文件描述符限制 */
    struct rlimit rl;
    getrlimit(RLIMIT_NOFILE, &rl);
    printf("父进程 RLIMIT_NOFILE: 软=%lu 硬=%lu\n",
           (unsigned long)rl.rlim_cur,
           (unsigned long)rl.rlim_max);

    /* 降低软限制 */
    rl.rlim_cur = 64;
    setrlimit(RLIMIT_NOFILE, &rl);

    pid_t pid = fork();
    if (pid == 0) {
        struct rlimit child_rl;
        getrlimit(RLIMIT_NOFILE, &child_rl);
        printf("子进程 RLIMIT_NOFILE: 软=%lu(继承父进程修改)\n",
               (unsigned long)child_rl.rlim_cur);
        _exit(0);
    }
    waitpid(pid, NULL, 0);

    /* 恢复 */
    rl.rlim_cur = rl.rlim_max;
    setrlimit(RLIMIT_NOFILE, &rl);
    printf("\n");
}

/* ── 测试4:fork 与多线程的危险 ── */
static pthread_mutex_t g_mutex = PTHREAD_MUTEX_INITIALIZER;

void *thread_func(void *arg) {
    (void)arg;
    pthread_mutex_lock(&g_mutex);
    sleep(10);   /* 持有锁的线程 */
    pthread_mutex_unlock(&g_mutex);
    return NULL;
}

void test_fork_multithread(void) {
    printf("=== fork 与多线程(重要警告)===\n\n");
    printf("⚠️  在多线程程序中使用 fork 非常危险!\n\n");
    printf("问题:\n");
    printf("  1. fork 只复制调用线程,其他线程消失\n");
    printf("  2. 其他线程持有的锁在子进程中永远不会释放\n");
    printf("  3. 子进程可能死锁!\n\n");

    /* 创建一个持有锁的线程 */
    pthread_t tid;
    pthread_create(&tid, NULL, thread_func, NULL);
    usleep(100000);   /* 等线程获取锁 */

    printf("主线程 fork(另一个线程持有 mutex)...\n");
    pid_t pid = fork();
    if (pid == 0) {
        printf("子进程尝试获取 mutex(可能死锁!)\n");
        /* 在实际程序中,这里会死锁 */
        /* 我们用 trylock 来演示 */
        int ret = pthread_mutex_trylock(&g_mutex);
        if (ret == 0) {
            printf("子进程获取 mutex 成功(运气好)\n");
            pthread_mutex_unlock(&g_mutex);
        } else {
            printf("子进程无法获取 mutex(死锁风险!errno=%d)\n", ret);
        }
        _exit(0);
    }
    waitpid(pid, NULL, 0);

    pthread_cancel(tid);
    pthread_join(tid, NULL);

    printf("\n解决方案:\n");
    printf("  1. fork 后立即 exec(最安全)\n");
    printf("  2. 使用 pthread_atfork 注册 fork 处理函数\n");
    printf("  3. 避免在多线程程序中使用 fork\n");
}

int main(void) {
    test_fd_inherit();
    test_signal_inherit();
    test_rlimit_inherit();
    test_fork_multithread();
    return 0;
}
bash 复制代码
gcc -o resource_inherit resource_inherit.c -lpthread
./resource_inherit
# 输出示例:
# === 文件描述符继承规则 ===
#
# fd_normal  = 3(无 FD_CLOEXEC)
# fd_cloexec = 4(有 FD_CLOEXEC)
#
# 【fork 后子进程的 fd】
#   fd_normal  (3): 继承 ✓
#   fd_cloexec (4): 继承 ✓(fork 不受 FD_CLOEXEC 影响)
#   pipe 读端  (5): 继承 ✓

7. clone --- 底层进程/线程创建

7.1 clone 系统调用

c 复制代码
/* 文件名:clone_demo.c
 * 演示 clone 系统调用:进程和线程创建的底层接口
 */
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sched.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mman.h>
#include <signal.h>

/*
 * clone 是 fork/vfork/pthread_create 的底层实现
 *
 * flags 控制子进程与父进程共享哪些资源:
 *   CLONE_VM       共享虚拟内存(线程)
 *   CLONE_FS       共享文件系统信息(工作目录、根目录)
 *   CLONE_FILES    共享文件描述符表
 *   CLONE_SIGHAND  共享信号处理函数
 *   CLONE_THREAD   加入父进程的线程组(线程)
 *   CLONE_NEWPID   新 PID 命名空间(容器)
 *   CLONE_NEWNET   新网络命名空间(容器)
 *   CLONE_NEWNS    新挂载命名空间(容器)
 *   CLONE_NEWUTS   新 UTS 命名空间(主机名)
 *   CLONE_NEWIPC   新 IPC 命名空间
 *   CLONE_NEWUSER  新用户命名空间
 *
 * fork  ≈ clone(SIGCHLD)
 * vfork ≈ clone(CLONE_VM | CLONE_VFORK | SIGCHLD)
 * pthread_create ≈ clone(CLONE_VM | CLONE_FS | CLONE_FILES |
 *                        CLONE_SIGHAND | CLONE_THREAD | ...)
 */

#define STACK_SIZE (1024 * 1024)   /* 1MB 栈 */

/* 子进程/线程的入口函数 */
int child_func(void *arg) {
    const char *label = (const char *)arg;
    printf("  [%s] PID=%d, PPID=%d\n", label, getpid(), getppid());
    return 0;
}

int main(void) {
    printf("=== clone 系统调用演示 ===\n\n");

    /* 分配子进程/线程的栈 */
    char *stack1 = mmap(NULL, STACK_SIZE,
                        PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK,
                        -1, 0);
    char *stack2 = mmap(NULL, STACK_SIZE,
                        PROT_READ | PROT_WRITE,
                        MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK,
                        -1, 0);

    /* ── clone 创建进程(类似 fork)── */
    printf("【clone 创建进程(类似 fork)】\n");
    printf("  flags = SIGCHLD(只共享信号)\n");
    pid_t pid1 = clone(child_func, stack1 + STACK_SIZE,
                       SIGCHLD,   /* 子进程退出时发送 SIGCHLD */
                       (void *)"clone-process");
    if (pid1 == -1) { perror("clone process"); }
    else {
        waitpid(pid1, NULL, 0);
        printf("  子进程 PID=%d 已退出\n\n", pid1);
    }

    /* ── clone 创建线程(类似 pthread_create)── */
    printf("【clone 创建线程(类似 pthread_create)】\n");
    printf("  flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND\n");
    pid_t pid2 = clone(child_func, stack2 + STACK_SIZE,
                       CLONE_VM | CLONE_FS | CLONE_FILES |
                       CLONE_SIGHAND | SIGCHLD,
                       (void *)"clone-thread");
    if (pid2 == -1) { perror("clone thread"); }
    else {
        waitpid(pid2, NULL, 0);
        printf("  线程 TID=%d 已退出\n\n", pid2);
    }

    /* ── fork/vfork/clone 对比 ── */
    printf("=== fork / vfork / clone 对比 ===\n\n");
    printf("%-15s %-12s %-12s %-12s\n",
           "特性", "fork", "vfork", "clone");
    printf("%s\n", "─────────────────────────────────────────────────");
    printf("%-15s %-12s %-12s %-12s\n",
           "地址空间", "COW复制", "共享", "可配置");
    printf("%-15s %-12s %-12s %-12s\n",
           "文件描述符", "复制", "共享", "可配置");
    printf("%-15s %-12s %-12s %-12s\n",
           "父进程挂起", "否", "是", "可配置");
    printf("%-15s %-12s %-12s %-12s\n",
           "命名空间", "继承", "继承", "可新建");
    printf("%-15s %-12s %-12s %-12s\n",
           "典型用途", "进程创建", "已废弃", "线程/容器");

    munmap(stack1, STACK_SIZE);
    munmap(stack2, STACK_SIZE);
    return 0;
}
bash 复制代码
gcc -o clone_demo clone_demo.c
./clone_demo
# 输出示例:
# === clone 系统调用演示 ===
#
# 【clone 创建进程(类似 fork)】
#   flags = SIGCHLD(只共享信号)
#   [clone-process] PID=12346, PPID=12345
#   子进程 PID=12346 已退出
#
# 【clone 创建线程(类似 pthread_create)】
#   flags = CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND
#   [clone-thread] PID=12347, PPID=12345
#   线程 TID=12347 已退出

8. 综合实践

8.1 实现一个简单的进程池

c 复制代码
/* 文件名:process_pool.c
 * 实现一个简单的进程池(Worker Pool)
 * 演示 fork + exec + 进程管理的综合应用
 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <time.h>

#define POOL_SIZE    4
#define MAX_TASKS   20

/* 任务结构 */
typedef struct {
    int   task_id;
    int   duration_ms;   /* 模拟任务耗时 */
    char  command[64];   /* 要执行的命令 */
} Task;

/* 工作进程信息 */
typedef struct {
    pid_t  pid;
    int    busy;         /* 是否正在执行任务 */
    int    task_id;      /* 当前任务 ID */
    time_t start_time;   /* 任务开始时间 */
} Worker;

static Worker workers[POOL_SIZE];
static volatile int g_running = 1;

void sig_chld(int sig) {
    (void)sig;
    int status;
    pid_t pid;
    /* 非阻塞等待所有已退出的子进程 */
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        for (int i = 0; i < POOL_SIZE; i++) {
            if (workers[i].pid == pid) {
                printf("  [工作进程 %d] PID=%d 完成任务 #%d,退出码=%d\n",
                       i, pid, workers[i].task_id,
                       WIFEXITED(status) ? WEXITSTATUS(status) : -1);
                workers[i].busy = 0;
                workers[i].pid  = 0;
                break;
            }
        }
    }
}

void sig_int(int sig) {
    (void)sig;
    g_running = 0;
}

/* 找一个空闲的工作进程槽 */
int find_free_worker(void) {
    for (int i = 0; i < POOL_SIZE; i++) {
        if (!workers[i].busy) return i;
    }
    return -1;
}

/* 提交任务到进程池 */
int submit_task(const Task *task) {
    int slot = find_free_worker();
    if (slot == -1) return -1;   /* 没有空闲工作进程 */

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

    if (pid == 0) {
        /* 工作进程:执行任务 */
        printf("  [工作进程 slot=%d] PID=%d 开始任务 #%d(%dms)\n",
               slot, getpid(), task->task_id, task->duration_ms);

        /* 模拟任务执行 */
        usleep(task->duration_ms * 1000);

        /* 执行实际命令(如果有)*/
        if (task->command[0]) {
            char *args[] = { "sh", "-c", task->command, NULL };
            execvp("sh", args);
        }

        _exit(0);
    }

    /* 父进程:记录工作进程信息 */
    workers[slot].pid        = pid;
    workers[slot].busy       = 1;
    workers[slot].task_id    = task->task_id;
    workers[slot].start_time = time(NULL);

    return slot;
}

int main(void) {
    printf("=== 进程池演示(池大小=%d)===\n\n", POOL_SIZE);

    /* 初始化 */
    memset(workers, 0, sizeof(workers));

    /* 注册信号处理 */
    struct sigaction sa_chld = {
        .sa_handler = sig_chld,
        .sa_flags   = SA_RESTART | SA_NOCLDSTOP,
    };
    sigemptyset(&sa_chld.sa_mask);
    sigaction(SIGCHLD, &sa_chld, NULL);
    signal(SIGINT, sig_int);

    /* 创建任务队列 */
    Task tasks[MAX_TASKS];
    for (int i = 0; i < MAX_TASKS; i++) {
        tasks[i].task_id     = i + 1;
        tasks[i].duration_ms = 200 + (i % 5) * 100;   /* 200~600ms */
        snprintf(tasks[i].command, sizeof(tasks[i].command),
                 "echo 'Task #%d done'", i + 1);
    }

    printf("提交 %d 个任务到进程池...\n\n", MAX_TASKS);

    int submitted = 0;
    int completed = 0;

    while ((submitted < MAX_TASKS || completed < MAX_TASKS) && g_running) {
        /* 尝试提交新任务 */
        while (submitted < MAX_TASKS) {
            int slot = submit_task(&tasks[submitted]);
            if (slot == -1) break;   /* 没有空闲工作进程 */
            printf("  任务 #%d 提交到 slot=%d(PID=%d)\n",
                   tasks[submitted].task_id, slot, workers[slot].pid);
            submitted++;
        }

        /* 等待任意子进程完成 */
        int status;
        pid_t done_pid = waitpid(-1, &status, WNOHANG);
        if (done_pid > 0) {
            for (int i = 0; i < POOL_SIZE; i++) {
                if (workers[i].pid == done_pid) {
                    completed++;
                    printf("  任务 #%d 完成(%d/%d)\n",
                           workers[i].task_id, completed, MAX_TASKS);
                    workers[i].busy = 0;
                    workers[i].pid  = 0;
                    break;
                }
            }
        } else {
            usleep(10000);   /* 10ms 轮询间隔 */
        }
    }

    /* 等待所有工作进程完成 */
    while (wait(NULL) > 0);

    printf("\n所有 %d 个任务已完成!\n", MAX_TASKS);
    return 0;
}
bash 复制代码
gcc -O2 -o process_pool process_pool.c
./process_pool
# 输出示例:
# === 进程池演示(池大小=4)===
#
# 提交 20 个任务到进程池...
#
#   任务 #1 提交到 slot=0(PID=12346)
#   任务 #2 提交到 slot=1(PID=12347)
#   任务 #3 提交到 slot=2(PID=12348)
#   任务 #4 提交到 slot=3(PID=12349)
#   [工作进程 slot=0] PID=12346 开始任务 #1(200ms)
#   ...
#   所有 20 个任务已完成!

8.2 fork/exec 速查手册

bash 复制代码
#!/bin/bash
# 文件名:fork_exec_guide.sh
# 功能:fork/exec 使用指南和常见陷阱

cat << 'EOF'
═══════════════════════════════════════════════════════════════
  fork / exec 使用指南
═══════════════════════════════════════════════════════════════

【fork 使用模板】
─────────────────────────────────────────────────────────────
  fflush(stdout);          // ① fork 前刷新缓冲区
  pid_t pid = fork();
  if (pid == -1) {
      perror("fork");      // ② 检查错误
      // 错误处理
  } else if (pid == 0) {
      // 子进程
      // ... 做一些工作 ...
      _exit(0);            // ③ 子进程用 _exit,不用 exit
  } else {
      // 父进程
      int status;
      waitpid(pid, &status, 0);  // ④ 必须 wait,防止僵尸
  }

【fork + exec 模板】
─────────────────────────────────────────────────────────────
  pid_t pid = fork();
  if (pid == 0) {
      // 设置重定向(dup2)
      // 关闭不需要的 fd
      execvp(argv[0], argv);
      perror("exec");
      _exit(127);          // exec 失败,用 127 表示命令未找到
  }
  waitpid(pid, &status, 0);

【exec 函数选择】
─────────────────────────────────────────────────────────────
  已知完整路径 + 参数列表  → execl("/bin/ls", "ls", "-l", NULL)
  搜索 PATH + 参数列表     → execlp("ls", "ls", "-l", NULL)
  已知完整路径 + 参数数组  → execv("/bin/ls", argv)
  搜索 PATH + 参数数组     → execvp("ls", argv)
  自定义环境变量           → execle/execve(带 envp 参数)

【常见陷阱】
─────────────────────────────────────────────────────────────
  ❌ fork 前未 fflush → 缓冲区内容被复制,输出重复
  ❌ 子进程用 exit   → 调用 atexit,破坏父进程状态
  ❌ 父进程不 wait   → 子进程变成僵尸进程
  ❌ 多线程中 fork   → 其他线程消失,可能死锁
  ❌ exec 后访问旧变量 → 地址空间已替换,未定义行为
  ❌ 忘记关闭 fd     → fd 泄漏到子进程

【资源继承速查】
─────────────────────────────────────────────────────────────
  资源              fork 继承    exec 保留
  ─────────────────────────────────────────
  PID               新 PID       保留
  文件描述符        ✓ 继承       ✓(除 FD_CLOEXEC)
  信号处理函数      ✓ 继承       ✗(重置为默认)
  SIG_IGN           ✓ 继承       ✓ 保留
  环境变量          ✓ 继承       ✓ 保留(或自定义)
  工作目录          ✓ 继承       ✓ 保留
  umask             ✓ 继承       ✓ 保留
  资源限制          ✓ 继承       ✓ 保留
  内存映射          COW 复制     ✗ 清除
  定时器            ✗ 不继承     ✗ 清除
  待处理信号        ✗ 清空       ✗ 清空
  文件记录锁        ✗ 不继承     ✗ 清除
  内存锁(mlock)     ✗ 不继承     ✗ 清除

═══════════════════════════════════════════════════════════════
EOF

知识点总结

复制代码
第 9 章 核心知识图谱
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

┌──────────────────────────────────────────────────────────┐
│                    进程创建                               │
└────────┬──────────┬──────────┬──────────┬────────────────┘
         │          │          │          │
  ┌──────▼───┐ ┌────▼─────┐ ┌──▼──────┐ ┌▼──────────────┐
  │   fork   │ │  vfork   │ │  exec   │ │    clone      │
  └──────┬───┘ └────┬─────┘ └──┬──────┘ └┬──────────────┘
         │          │          │          │
  COW机制      共享地址空间   execl/v/p/e  CLONE_VM
  执行顺序     父进程挂起     exec内部机制  CLONE_FILES
  信号继承     立即exec/_exit PID保留      命名空间
  fd共享       已废弃         fd继承规则   容器基础
  定时器不继承              信号重置

exec 函数命名规律:
  l = list(参数列表)  v = vector(参数数组)
  p = path(搜索PATH)  e = environment(自定义环境)

fork + exec 标准模式:
  fflush → fork → 子进程设置重定向 → exec → 父进程 waitpid

黄金法则:
  ① fork 前必须 fflush(stdout/stderr)
  ② 子进程用 _exit,不用 exit(避免重复调用 atexit)
  ③ 父进程必须 waitpid,防止僵尸进程
  ④ exec 后 FD_CLOEXEC 的 fd 自动关闭(安全实践)
  ⑤ exec 后自定义信号处理函数重置为 SIG_DFL
  ⑥ 多线程程序中 fork 后应立即 exec(避免死锁)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

📚 参考资料

  • man 2 fork / man 2 vfork / man 2 clone
  • man 3 exec / man 3 execl / man 3 execvp / man 2 execve
  • man 3 posix_spawn / man 3 posix_spawn_file_actions_addopen
  • man 2 waitpid / man 7 signal
  • 《Linux/UNIX 系统编程手册》第 24、25、27、28 章 --- Michael Kerrisk
  • 《UNIX 环境高级编程(APUE)》第 8 章 --- W. Richard Stevens
  • Linux 内核源码:kernel/fork.c(fork/clone 实现)
相关推荐
skywalk81631 小时前
段言项目推进6.15 @ Dumate+Trae
开发语言·学习·编程
我命由我123451 小时前
Android 开发问题:全局的主题颜色设置,导致 CheckBox 控件在勾选状态下不显示样式
android·java·开发语言·java-ee·intellij-idea·intellij idea·android jetpack
babytiger1 小时前
银河麒麟v11,apt 安装不好用了,要打开维护模式
linux·运维·服务器
Android小码家1 小时前
andoird13 + bazel 编译 Linux kernel
linux·运维·服务器
码农爱学习1 小时前
Linux进程内存监测与内存泄漏示例
linux
Cloud_Shy6181 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第七章 Item 51)
开发语言·人工智能·笔记·python·学习方法
AI+程序员在路上1 小时前
CSP、PP、PV、HM 在 CiA402 标准下的差异解析
linux·c语言·开发语言·嵌入式硬件
nix.gnehc1 小时前
Python 并发深度解析
服务器·开发语言·python
我是一颗柠檬1 小时前
【Java项目技术亮点】Leaf号段模式双Buffer优化
java·开发语言·分布式·后端·架构