1. fork
的基本原理
当调用 fork()
时,操作系统会执行以下主要步骤:
-
分配新的进程结构:
- 操作系统会为新进程分配一个新的
task_struct
结构(进程控制块,PCB),它用于存储进程的状态、内存信息、文件描述符等。
- 操作系统会为新进程分配一个新的
-
复制进程上下文:
- 进程地址空间: 操作系统会复制父进程的内存页表,从而子进程获得父进程相同的虚拟地址空间内容。现代操作系统中,通常使用写时复制(Copy-On-Write, COW)技术,避免立即复制物理内存数据,而是将父进程的内存页标记为只读,等到子进程或父进程尝试写入时,才真正复制该页数据。
- 文件描述符表: 子进程会继承父进程的文件描述符表,但文件描述符的引用计数会增加,以表示它们被多个进程共享。
- 信号处理: 子进程继承父进程的信号处理设置(除了一些特殊情况,如
SIGCHLD
信号会被重置为默认处理方式)。 - 进程 ID: 子进程会获得一个新的进程 ID,与父进程不同,但子进程会继承父进程的进程组 ID 和会话 ID。
-
更新进程树关系:
- 内核会将新创建的子进程加入进程树中,成为父进程的子进程。子进程的
parent
指针指向父进程,父进程的子进程链表会新增该子进程。
- 内核会将新创建的子进程加入进程树中,成为父进程的子进程。子进程的
-
返回值:
- 对于父进程,
fork()
返回子进程的 PID。 - 对于子进程,
fork()
返回 0。
- 对于父进程,
2. fork
的详细步骤
从 Linux 内核源码角度来看,fork()
的实现实际上是通过 do_fork()
完成的,该函数负责创建子进程并复制父进程的各种信息。
具体流程如下:
-
调用
do_fork()
:- 用户进程调用
fork()
时,会陷入内核,由内核函数do_fork()
来处理实际的创建工作。
- 用户进程调用
-
复制进程信息:
do_fork()
函数会调用copy_process()
函数,后者负责复制父进程的各种信息。包括创建一个新的task_struct
,复制父进程的内存地址空间(使用 COW),文件描述符表,信号处理表等。
-
设置子进程状态:
- 内核会将新创建的子进程的状态设置为就绪态,等待被调度。
-
返回:
do_fork()
最后会将子进程的 PID 返回给父进程,并将 0 返回给子进程。
3. 写时复制 (Copy-On-Write, COW)
在内存管理中,COW 技术是 fork()
中一个关键的优化点。父子进程共享同一块物理内存,只是在内存页被写入时才进行实际的物理内存复制。这大大提高了 fork()
的效率,特别是对于不需要立即修改内存的场景,例如 fork
后立即执行 exec
,加载新的程序映像。
- 页表复制:
fork()
时,子进程会继承父进程的页表结构,这样子进程的虚拟内存布局与父进程保持一致。 - 页表标记: 所有被继承的页表项会被标记为只读。当父或子进程尝试写入内存时,CPU 会触发页错误(page fault),内核在处理页错误时检测到这是 COW 的情况,就会复制该内存页,解除只读限制并重新映射。
4. 进程上下文切换与 fork()
当一个进程调用 fork()
后,子进程进入就绪队列,等待调度程序的调度。当调度程序决定切换到子进程时,子进程会从复制的父进程上下文开始执行。这意味着子进程会从 fork()
的返回点开始执行,只不过它返回的是 0
。
推荐学习 https://xxetb.xetslk.com/s/p5Ibb