01
60 秒回答模板
在 Linux 中创建子进程最典型的方式是 fork()。fork 调用后会出现两个执行流:父进程中 fork 返回子进程 PID,子进程中 fork 返回 0,失败时父进程返回 -1。子进程会继承父进程的大部分进程上下文,例如虚拟地址空间、文件描述符表、当前工作目录、环境变量、信号处理设置等,但它有独立的 PID、父进程 ID、运行统计、锁状态等。Linux 为了提高效率,fork 通常使用写时复制,父子进程最初共享物理页,只有当某一方写入内存时才复制对应页面。实际启动外部程序时,一般是 fork 后在子进程里调用 exec 系列函数替换进程映像,父进程用 wait 或 waitpid 等待并回收子进程,读取退出码,避免产生僵尸进程。如果父进程先退出,子进程会成为孤儿进程并被系统的 init 或子回收进程接管;如果子进程退出而父进程不 wait,就会成为僵尸进程。工程上还要注意 fork 后关闭无用文件描述符、设置 close-on-exec、处理信号、避免多线程程序 fork 后调用不安全函数,并根据场景选择 vfork 或 posix_spawn。
考点 基本创建方式:fork
主线 fork 后为什么常接 exec
易错点 把 fork 说成创建线程,而不是创建进程。
02
深入解析
01 基本创建方式:fork
Linux 创建子进程最经典的系统调用是 fork()。它由当前进程发起,内核为新进程分配进程控制结构、PID、调度实体等资源,并让新进程从 fork 返回处继续执行。fork 的返回值用于区分父子分支:父进程得到子进程 PID,子进程得到 0,失败时返回 -1 并设置 errno。也就是说,fork 不是从头启动一个函数,而是把当前执行现场分裂成两个几乎相同的执行流。面试中要强调:fork 之后父子进程谁先运行没有确定顺序,取决于调度器,不能依赖固定执行顺序。
02 fork 后为什么常接 exec
fork 创建的是父进程的副本,但很多场景并不是想让子进程继续运行同一段业务逻辑,而是想启动一个新的可执行程序,例如 shell 执行命令、服务端拉起 worker、守护进程重启组件等。这时通常在子进程中调用 exec 系列函数。exec 不会创建新进程,而是把当前进程的代码段、数据段、堆、栈等用户态进程映像替换为新程序,PID 通常保持不变。典型模式是:父进程 fork,子进程整理环境和文件描述符,然后 exec 加载目标程序;父进程继续执行或等待子进程结束。
03 父进程如何回收:wait 和 waitpid
子进程退出后,内核会保留一小部分退出信息,例如退出码、终止信号、资源使用信息和进程号,等待父进程读取。父进程使用 wait 或 waitpid 回收这些信息。wait 等待任意一个子进程退出;waitpid 可以指定某个 PID,也可以使用选项实现非阻塞等待或等待进程组。服务端程序通常更偏向 waitpid,因为它能精确管理多个子进程,并可配合 WNOHANG 在信号处理或事件循环中回收已退出的子进程。父进程如果不回收,子进程退出后会变成僵尸进程。
04 写时复制机制
早期如果 fork 直接完整复制父进程的整个地址空间,成本会非常高,尤其父进程内存很大时。Linux 通常采用写时复制:fork 时父子进程的虚拟地址空间看起来相同,但物理页先共享,并把相关页标记为只读;当父进程或子进程尝试写某个共享页时,内核触发缺页异常,为写入方复制该页,之后双方才拥有不同的物理页。这样如果子进程马上 exec,很多用户态内存根本不会被复制,fork+exec 的成本大幅下降。不过页表复制、进程结构分配、文件表引用计数调整等仍然有成本。
05 资源继承关系
子进程会继承父进程的很多上下文:虚拟地址空间内容、打开的文件描述符、文件偏移量对应的打开文件对象、当前工作目录、根目录、umask、环境变量、信号屏蔽字、部分信号处理设置、资源限制等。需要特别注意文件描述符:fork 后父子进程拥有各自的文件描述符表,但表项可能指向同一个内核打开文件对象,所以文件偏移量和文件状态标志可能共享。比如父子进程同时写同一个日志文件,如果没有合理同步、追加模式或独立 fd 管理,就可能产生顺序和内容问题。
06 哪些东西不会完全一样
子进程不是父进程的完全复制品。它有新的 PID,父进程 ID 指向创建它的进程;运行时间统计、挂起信号、定时器、部分锁状态等会有不同语义。内存地址在虚拟地址层面可能相同,但父子写入后会因写时复制变为不同物理页。父进程中持有的线程状态也要谨慎理解:在多线程程序中调用 fork,子进程通常只保留调用 fork 的那个线程,其他线程不会存在,这会导致互斥锁、条件变量、库内部锁处于难以预期的状态。
07 孤儿进程和僵尸进程
孤儿进程是父进程先退出、子进程仍在运行的情况。此时子进程会被系统中的进程回收者接管,之后它退出时由接管者回收。僵尸进程是子进程已经退出,但父进程还没有 wait 回收其退出状态的情况。僵尸进程不再执行代码,也不占用用户态内存,但仍占用 PID 和少量内核表项。大量僵尸进程可能耗尽 PID 或进程表资源。面试中要清楚区分:孤儿进程是仍在运行但父亲没了;僵尸进程是已经退出但退出信息没人收。
08 vfork 的边界
vfork 是历史上为降低 fork 成本设计的接口,通常用于子进程立即 exec 或 _exit 的场景。它的危险点在于子进程在 exec 或 _exit 之前可能与父进程共享地址空间,并且父进程可能被挂起;如果子进程修改变量、返回到调用栈、调用复杂库函数,就可能破坏父进程状态。因此面试中不能简单说 vfork 是更快的 fork,而要说明它的约束更强,适用面更窄,现代代码一般应谨慎使用。
09 posix_spawn 的价值
posix_spawn 可以理解为一种更受控的创建并执行新程序的接口,常用于替代 fork+exec,尤其在大内存进程、多线程进程或受限环境中更有价值。它可以在创建时设置文件动作、信号属性、调度属性等,让实现层选择更高效或更安全的路径。对应用开发者来说,posix_spawn 的优势是表达意图更直接:目标就是启动新程序,而不是先复制当前进程再替换。它不适合需要在子进程 exec 前执行大量自定义逻辑的复杂场景。
10 工程实践注意事项
真实后端服务中,创建子进程不只是 fork 成功这么简单。需要在子进程关闭不需要的文件描述符,避免 socket、pipe、日志 fd 泄漏到新程序;对不希望被 exec 继承的 fd 设置 close-on-exec;正确处理 SIGCHLD 并循环 waitpid 回收多个已退出子进程;父子进程之间如果通过 pipe 通信,要关闭不用的一端,否则可能导致读端无法收到 EOF;fork 失败时要处理 EAGAIN、ENOMEM 等错误;多线程程序 fork 后,子进程在 exec 前只应调用异步信号安全的函数,避免死锁。
03
易错点
- 把 fork 说成创建线程,而不是创建进程。
- 认为 fork 后父进程一定先执行或子进程一定先执行。
- 认为 fork 会立即完整复制父进程全部物理内存,忽略写时复制。
- 认为 exec 会创建新进程,而不是替换当前进程映像。
- 只讲 fork,不讲 wait 或 waitpid,遗漏僵尸进程回收问题。
- 混淆孤儿进程和僵尸进程,把二者都说成父进程退出导致。
- 忽略文件描述符继承,没提 close-on-exec 和关闭无用 fd。
- 在多线程程序中随意 fork,并在子进程 exec 前调用复杂库函数。
- 把 vfork 简单描述成更快的 fork,不说明它的危险约束。
- 认为子进程修改普通变量会影响父进程,混淆虚拟地址空间和物理页共享。
- 没有处理 fork 失败,默认进程创建一定成功。
- 没有说明父子进程通过返回值区分执行分支。
04
面试官追问
fork 后父子进程谁先执行?
不确定。fork 之后父进程和子进程都处于可运行状态,具体谁先获得 CPU 由调度器决定。程序不能依赖固定顺序,若有顺序要求,应使用 pipe、信号、锁、wait 或其他同步机制。
fork 之后父子进程的全局变量是共享的吗?
从程序视角看不是共享的。fork 后父子进程拥有独立的虚拟地址空间,初始内容相同。底层可能通过写时复制共享物理页,但任一方写入后会复制页面,所以一方修改普通全局变量不会影响另一方。
fork 和 exec 的区别是什么?
fork 是创建一个新的子进程,子进程从 fork 返回处继续执行;exec 是在当前进程中加载并运行另一个程序,替换当前进程映像。fork 会产生两个进程,exec 本身不会增加进程数量。
为什么子进程退出后还需要父进程 wait?
因为内核需要保留子进程的退出状态供父进程获取。如果父进程不 wait,这部分状态无法释放,子进程会处于僵尸状态。wait 或 waitpid 的作用就是读取退出信息并让内核释放对应表项。
如何避免僵尸进程?
常见做法是父进程主动 wait 或 waitpid;服务端可在 SIGCHLD 处理逻辑中循环调用 waitpid(-1, ..., WNOHANG) 回收所有已退出子进程;也可以设计专门的回收进程或使用合适的进程管理器。关键是不能让退出状态长期无人回收。
fork 后文件描述符会怎样?
子进程会继承父进程已打开的文件描述符。父子进程的 fd 表是各自的,但表项可能引用同一个内核打开文件对象,因此文件偏移量、状态标志等可能共享。启动外部程序时应关闭无用 fd 或设置 close-on-exec。
为什么多线程程序中 fork 要特别小心?
因为 fork 后子进程只保留调用 fork 的线程,其他线程消失。如果其他线程在 fork 时持有互斥锁,子进程里锁可能永远无法释放。子进程在 exec 前应尽量只做非常简单、安全的操作。
vfork 为什么危险?
vfork 的语义要求子进程在 exec 或 _exit 前不能随意修改父进程相关状态,也不能返回到普通调用栈。误用可能破坏父进程栈和内存状态。它适合立即执行 exec 的场景,不适合复杂子进程初始化逻辑。
posix_spawn 适合什么场景?
posix_spawn 适合目标明确为启动新程序的场景,尤其是大内存进程或多线程进程中。它把创建进程、设置属性、文件动作和执行程序封装在更受控的接口里,减少 fork 后在子进程中执行复杂逻辑的风险。