外观
一句话答案
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 机制巧妙地解决了这个问题:
fork()后,父子进程的页表指向相同的物理页帧,这些共享页面被标记为只读。- 父子进程正常读取数据时,直接共享物理内存,无需任何拷贝。
- 当某个进程(父或子)尝试写入某页时,触发页保护异常(Write Protection Fault)。
- OS 在异常处理中拷贝该页为一个新的物理页帧,将写入方的页表指向新帧,恢复读写权限。
- 两个进程从此对该页各自独立。
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 的 bgsave 和 bgrewriteaof 使用 fork() 创建子进程来进行持久化,利用 COW 机制使得子进程可以安全地遍历数据快照写入磁盘,而父进程继续处理客户端请求。只有被修改的页面才会触发实际的内存拷贝,这就是 Redis 能在持久化期间保持高性能的关键。
全文完 — 共 16 题,覆盖操作系统核心知识域。 交叉参考索引:模块 03(JVM)、模块 04(并发编程)、模块 08(计算机网络)、模块 09(消息队列)。
追问与易错
追问方向:
- fork 后内存什么关系?
- Redis BGSAVE 时大量写入会怎样?
- vfork 和 fork 区别?
易错点:
- ❌ fork 立即复制所有内存——COW 写时才复制
- ❌ COW 没有开销——写入密集时有大量页面复制
💡 记忆锚点
fork+COW像复印一本书:不是立刻复印全部(太贵),而是先共享同一本(只读),谁要在上面写批注时,只复印那一页给他改(写时复制)。Redis BGSAVE就是fork一个子进程拿着"共享的书"去拍照(快照),父进程继续写——只有被改的页才需要复印,所以fork瞬间完成,持久化期间性能几乎不受影响。