草莓仙生头像
关注

页表 & 写时复制 COW(Copy‑On‑Write)

页表 & 写时复制 COW(Copy‑On‑Write)

继续沿用比喻:

进程 = 车间
物理内存 = 真实的物理仓库,硬件实际内存条
虚拟地址 = 车间内部图纸上的编号(虚拟地址)
页表 = 图纸和真实仓库的映射对照表

CPU不直接用物理内存地址。每个车间(进程)拿到一套独立虚拟地址空间,车间只认识虚拟地址。CPU通过页表,把虚拟地址翻译成内存条真正的物理地址。

重点:每个进程有自己一套页表。不同进程,同一个虚拟地址,映射到完全不同物理内存。

页表干什么

  1. CPU访问内存时,MMU(内存管理单元硬件)查页表,虚拟地址 → 物理地址
  2. 标记页权限:可读、可写、执行、是否在内存中(缺页中断)
  3. fork子进程,核心操作就是操作页表,而不是立刻拷贝全部内存。

没有COW的fork(老时代)

父进程fork,内核把父进程全部物理内存完整复制一份给子进程。

  • 父进程1GB内存,fork瞬间就要拷贝1GB,很慢,占用双倍内存。
  • 绝大多数场景fork之后马上调用exec()加载新程序,复制完立刻全部丢弃,完全白干活。

于是诞生写时复制 COW Copy‑On‑Write

fork + COW 完整过程

父进程调用fork()系统调用进入内核:

  1. 创建子进程task_struct,分配PID
  2. 复制父进程的页表,不复制物理内存!
  3. 父子进程的页表,全部指向同一套物理内存页
  4. 内核修改页表项标记:把这些共享页设置成只读

📝比喻:
父车间有一套完整图纸+仓库货物。
fork:给子车间复印一份【对照表(页表)】,货物不复制,父子共用同一堆真实货物,并且约定:谁都不许修改货物。

此时父子读内存完全没问题,读的时候共用物理内存,内存不会翻倍。


什么时候真正复制内存?

只要任意一方(父 or 子)尝试写这块内存

  1. CPU执行写指令,MMU查页表,发现页面标记为只读,触发缺页异常(页面故障),陷入内核。
  2. 内核捕获异常:知道这是COW共享页,不是真的权限错误。
  3. 内核分配一块新的物理内存,把原来那一页内容拷贝过去。
  4. 修改触发写操作那一方的页表:指向新复制出来的物理页,标记为可写。
  5. 另一方依旧指向老物理页。
  6. 返回用户态,程序继续执行写操作。

比喻:父子共用货物,父车间想改其中一件货物。
内核管家:复制一份这件货物放到新仓库位置,父车间以后修改自己这份;子车间继续用老的那一份。从此以后两者不再共享这一页。

👉 只有发生写的时候,才真正拷贝那一页内存;只读全程共享。

两个典型场景

场景1:fork之后立刻execve()(shell跑命令)

bash(父进程)fork出子进程,此时全部内存共享。
紧接着子进程调用exec("ls"),直接加载ls程序,直接丢弃整个子进程的地址空间和页表
因为没有发生写,几乎没有内存拷贝开销,COW在这里收益最大。

场景2:fork之后父子都修改全局变量

int a = 100;
pid_t pid = fork();
if(pid == 0) {
    a = 200; //子进程写a,触发COW,复制这一页
    printf("child a=%d\n",a);
}else{
    sleep(1);
    printf("parent a=%d\n",a);
}

输出:

child a=200
parent a=100
  • fork瞬间,父子共享存a的物理页。
  • 子进程执行a=200写操作,触发COW,复制页面。子拿到自己副本。
  • 父进程的a依旧是100,两者互不影响。

很多人疑惑:为什么子进程改变量父进程看不见?根源就在这里。

关键易错点

  1. COW是按页粒度复制,不是整个进程内存一次性复制。一页通常4KB。哪一页被写,就复制哪一页。
  2. 只读访问不会复制,只有写才复制。
  3. 页表本身在内核内存,不属于用户进程地址空间,用户代码不能直接修改页表,全部靠内核+MMU硬件。
  4. 线程没有COW:多线程共享同一份页表,同一个物理内存,线程修改全局变量,其他线程立刻可见。

对比记忆:

  • pthread多线程:同一个页表,全部内存共享,写直接修改原物理页。
  • fork子进程:复制页表,初始共享物理页,写触发COW复制。

缺页中断两种情况

  1. 真正缺页:页面不在物理内存,被交换到磁盘swap,内核从磁盘读回内存。
  2. COW缺页:页面就在物理内存,只是页表标记只读,写触发复制。

整体串联回顾整套链路

  1. 每个进程拥有独立虚拟地址空间,靠页表映射到物理内存条。
  2. fork:内核新建task_struct、PID,复制页表,父子页表指向相同物理页,全部标记只读。
  3. 读内存:MMU通过页表访问共享物理内存,无拷贝。
  4. 任意进程写内存 → MMU报页错误,进入内核。
  5. 内核分配新物理页,拷贝页面内容,修改触发方页表映射。
  6. 用户态继续运行,父子从此各自持有独立副本。

一句话总结

fork不会复制真正内存,只会复制页表,父子共享物理内存;只有写操作发生的时候,内核才复制对应的内存页,这就是写时复制COW

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/Logintern09/article/details/163896819

文章来源转载

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--