Linux 系统编程 · 第 9 章:进程创建
本章深入讲解 Linux 进程创建的完整体系:
fork的写时复制机制、vfork的特殊语义、exec族函数的程序替换原理,以及fork+exec组合模式、进程间的资源继承与关闭规则。
目录
- [fork 深度解析](#fork 深度解析)
- 写时复制(Copy-On-Write)
- [vfork --- 轻量级进程创建](#vfork — 轻量级进程创建)
- [exec 族函数](#exec 族函数)
- [fork + exec 组合模式](#fork + exec 组合模式)
- 进程资源的继承与关闭
- [clone --- 底层进程/线程创建](#clone — 底层进程/线程创建)
- 综合实践
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 cloneman 3 exec/man 3 execl/man 3 execvp/man 2 execveman 3 posix_spawn/man 3 posix_spawn_file_actions_addopenman 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 实现)