Skip to content
困难

一句话答案

fork 创建子进程时不复制物理页只复制页表(COW),写入时才复制对应页,Redis BGSAVE 利用此特性快速快照。

核心要点

fork() 系统调用:

fork() 创建一个与调用进程(父进程)几乎完全相同的子进程。子进程是父进程的副本——拥有相同的代码、数据、打开的文件描述符等。

c
pid_t pid = fork();
if (pid == 0) {
    // 子进程执行路径(fork 返回 0)
    printf("I am child, PID=%d\n", getpid());
} else if (pid > 0) {
    // 父进程执行路径(fork 返回子进程的 PID)
    printf("I am parent, child PID=%d\n", pid);
    wait(NULL);  // 等待子进程结束
} else {
    // fork 失败(返回 -1)
    perror("fork failed");
}

fork() 的返回值规则:

  • 父进程中:返回子进程的 PID(> 0)
  • 子进程中:返回 0
  • 失败:返回 -1

写时复制(Copy-On-Write, COW):

如果 fork() 真的将父进程的整个地址空间完整复制一份给子进程,开销将非常大(一个进程可能占用数百 MB 甚至数 GB 内存)。COW 机制巧妙地解决了这个问题:

  1. fork() 后,父子进程的页表指向相同的物理页帧,这些共享页面被标记为只读
  2. 父子进程正常读取数据时,直接共享物理内存,无需任何拷贝
  3. 当某个进程(父或子)尝试写入某页时,触发页保护异常(Write Protection Fault)。
  4. OS 在异常处理中拷贝该页为一个新的物理页帧,将写入方的页表指向新帧,恢复读写权限。
  5. 两个进程从此对该页各自独立。
fork() 后(COW):
  父进程页表 ──→ 物理页 A (只读)
  子进程页表 ──→ 物理页 A (只读)   ← 共享同一物理页

子进程写入时:
  父进程页表 ──→ 物理页 A (可读写)
  子进程页表 ──→ 物理页 A'(可读写) ← 拷贝后独立

COW 的好处:

  • fork() 速度极快:只需复制页表(KB 级别),不需要复制整个地址空间(GB 级别)。
  • 内存高效:如果子进程 fork() 后立即调用 exec() 替换程序映像(如 shell 执行命令),则大部分父进程的内存页根本不需要拷贝。

exec() 系列函数:

fork() 创建的子进程与父进程执行相同的代码。如果想让子进程执行一个全新的程序,需要调用 exec()

  • exec() 将当前进程的代码段、数据段、堆栈段完全替换为新程序的内容。
  • 进程 PID 不变,但已经是一个全新的程序了。
  • exec() 如果成功则不会返回(因为原代码已经被替换了)。

fork() + exec() 的经典组合(Shell 执行命令的原理):

Shell 进程
  → fork() 创建子进程
  → 子进程调用 exec("ls") 替换为 ls 程序
  → ls 执行完毕,子进程 exit()
  → Shell 进程 wait() 回收子进程,继续等待用户输入

面试补充: Redis 的 bgsavebgrewriteaof 使用 fork() 创建子进程来进行持久化,利用 COW 机制使得子进程可以安全地遍历数据快照写入磁盘,而父进程继续处理客户端请求。只有被修改的页面才会触发实际的内存拷贝,这就是 Redis 能在持久化期间保持高性能的关键。

全文完 — 共 16 题,覆盖操作系统核心知识域。 交叉参考索引:模块 03(JVM)、模块 04(并发编程)、模块 08(计算机网络)、模块 09(消息队列)。

追问与易错

追问方向:

  • fork 后内存什么关系?
  • Redis BGSAVE 时大量写入会怎样?
  • vfork 和 fork 区别?

易错点:

  • ❌ fork 立即复制所有内存——COW 写时才复制
  • ❌ COW 没有开销——写入密集时有大量页面复制

💡 记忆锚点

fork+COW像复印一本书:不是立刻复印全部(太贵),而是先共享同一本(只读),谁要在上面写批注时,只复印那一页给他改(写时复制)。Redis BGSAVE就是fork一个子进程拿着"共享的书"去拍照(快照),父进程继续写——只有被改的页才需要复印,所以fork瞬间完成,持久化期间性能几乎不受影响。