fork()复制父进程生成子进程,子进程返回0、父进程返回PID;实际采用写时复制降低开销;需wait()回收僵尸进程;子进程应优先用_exit()避免缓冲区问题。

进程是怎么被 fork() 出来的
Linux 中新进程几乎都源于 fork() 系统调用,它会复制当前进程的地址空间、文件描述符、信号处理等状态,生成一个几乎完全相同的子进程。注意:子进程从 fork() 返回值为 0,父进程返回子进程 PID(正整数),出错则返回 -1。
常见误区是认为 fork() 后父子进程执行顺序确定——实际由调度器决定,无先后保障。若需同步,必须显式使用 wait() 或信号机制。
-
fork()不加载新程序,只是复制;要运行不同代码得紧接着调用execve()类函数 - 写时复制(Copy-on-Write)让
fork()实际开销远小于内存大小所暗示的那样 - 频繁
fork()但不exec(如某些守护进程模型)可能因页表膨胀引发性能抖动
execve() 替换进程映像的关键细节
execve() 不创建新进程,而是用指定程序完全替换当前进程的用户空间代码、数据、堆栈和文件描述符表(除非标记了 CLOEXEC)。调用成功后,原进程“变成”新程序,execve() 永远不会返回;失败才返回 -1。
容易踩的坑:
- 传入的
argv数组必须以NULL结尾,否则execve()可能读越界并失败(错误码EFAULT) - 路径必须绝对或相对于当前工作目录;用
execvpe()可自动查$PATH,但需确保环境变量有效 - 若目标程序是脚本且无
#!解释器声明,内核会直接报ENOEXEC
子进程退出后,父进程不 wait() 会发生什么
子进程终止后进入“僵尸状态(Zombie)”,内核保留其退出状态、PID 和少量元数据,直到父进程调用 wait()、waitpid() 或类似接口回收。不回收会导致:ps 显示状态为 Z,PID 无法复用,长期积累可能耗尽进程表。
典型应对方式:
- 父进程主动调用
waitpid(-1, &status, WNOHANG)非阻塞轮询 - 注册
SIGCHLD信号处理器,在其中调用waitpid()回收(注意信号安全函数限制) - 对完全不关心子进程结果的场景,可提前设
signal(SIGCHLD, SIG_IGN),内核将自动清理僵尸进程(仅限 Linux 2.6+)
为什么 exit() 和 _exit() 不能混用
exit() 是 C 库函数,会刷新 stdio 缓冲区、调用 atexit() 注册的函数、关闭打开的流;_exit() 是系统调用封装,立即终止进程,不执行任何清理。
在 fork() 后的子进程中,若已调用 execve(),用哪个都行;但若没 exec,直接 exit() 可能导致父进程的 stdio 缓冲区被重复刷新(父子共享 FILE 结构体副本,底层 fd 相同)。
稳妥做法:
- 子进程
fork()后立即_exit(),避免 stdio 干扰 - 主流程或 exec 后的进程可用
exit() - 在信号处理中只能用
_exit(),因exit()不是异步信号安全函数
pid_t pid = fork();
if (pid == 0) {
// 子进程
execve("/bin/ls", (char*[]){ "ls", "-l", NULL }, environ);
_exit(127); // execve 失败,必须用 _exit
} else if (pid > 0) {
int status;
waitpid(pid, &status, 0); // 父进程等待回收
} else {
perror("fork");
}
进程生命周期里最易被忽略的是资源归属边界:文件描述符是否继承、信号处置是否重置、stdio 缓冲是否隐式共享——这些不体现在代码行数里,却直接决定程序在长时间运行或高并发下的稳定性。










