写时复制(Copy - on - Write,COW)
基本概念
写时复制是计算机编程里的一种优化策略。它的核心思想在于,进行数据修改操作时,并非马上复制整个数据对象,而是要等到真正需要修改数据的那个时刻才执行复制操作。这样做能够避免不必要的数据复制,进而提升系统性能,提高资源利用率。
工作原理
初始化
当多个进程或者线程共享同一个数据副本时,它们实际上都是指向同一块物理内存区域。在这个阶段,并不会进行数据的复制操作。
- 多个进程/线程 --> 同一块物理内存区域(共享数据)
写操作触发
一旦其中某个进程或者线程需要对共享数据执行写操作,操作系统或者相关系统就会先复制一份该数据的副本。之后,让进行写操作的进程或线程在这个副本上进行修改,而其他进程或线程依旧使用原来的数据副本。
- 要进行写操作的进程/线程 --触发复制–> 生成数据副本 --> 在副本上修改
- 其他进程/线程 --> 继续使用原数据副本
应用场景
操作系统
在一些操作系统(例如 Linux)中,当使用 fork()
系统调用创建子进程的时候,就会运用写时复制技术。子进程和父进程会共享相同的物理内存页面,只有在其中一个进程对页面进行写操作时,才会复制该页面。这样能够减少内存的使用以及复制开销。
- 父进程和子进程 --> 初始共享物理内存页面
- 有写操作的进程 --> 触发页面复制
Linux下的COW
传统子进程创建方式的问题
在 Linux 系统中,fork()
系统调用会创建一个与父进程几乎完全相同的子进程(唯一不同的是进程 ID,即 pid)。按照传统做法,会直接把父进程的数据复制到子进程中。复制完成后,父进程和子进程的数据段与堆栈是相互独立的。
然而,从实际使用情况来看,子进程通常会执行 exec()
函数来实现自身的特定功能。这就意味着,若采用传统复制数据的方式,创建子进程时复制过去的数据很多时候是无用的,因为子进程执行 exec()
时,原有的数据会被清空。
Copy On Write 技术的引入
鉴于很多情况下复制给子进程的数据是无效的,于是就诞生了 Copy On Write(COW)技术。其原理较为简单:
fork
创建的子进程会与父进程共享内存空间。也就是说,只要子进程不对内存空间进行写入操作,内存空间中的数据就不会复制给子进程。这样一来,创建子进程的速度会非常快,因为无需进行数据复制,子进程直接引用父进程的物理空间即可。- 若在
fork
函数返回后,子进程立即执行exec
加载一个新的可执行映像,那么就不会造成时间和内存空间的浪费。
换一种表达方式来理解:
- 在
fork
之后、exec
之前,父子进程使用相同的物理空间(内存区)。子进程的代码段、数据段和堆栈都指向父进程的物理空间,即两者的虚拟空间不同,但对应的物理空间是同一个。 - 当父子进程中有对相应段进行更改的操作发生时,才会为子进程的相应段分配物理空间。
- 若不执行
exec
,内核会为子进程的数据段和堆栈段分配对应的物理空间(至此,父子进程拥有各自独立的进程空间,互不影响),而代码段则继续共享父进程的物理空间(因为两者的代码完全相同)。 - 若执行
exec
,由于父子进程执行的代码不同,子进程的代码段也会分配单独的物理空间。
- 若不执行
copy - on - write 的实现原理
在 fork()
之后,内核会将父进程中所有内存页的权限设置为只读(read - only),然后让子进程的地址空间指向父进程。当父子进程都只是对内存进行只读操作时,一切相安无事。但当其中某个进程要对内存进行写操作时,CPU 硬件会检测到该内存页是只读的,从而触发页异常中断(page - fault),进而陷入内核的一个中断例程。在内核的中断例程中,会将触发异常的页复制一份,这样父子进程就各自持有一份独立的该页数据。
文件系统
部分文件系统(像 Btrfs、ZFS)也采用了写时复制技术。当对文件进行修改时,不会直接覆盖原数据,而是把修改后的数据写入新的磁盘块。这样做可以提高数据的可靠性和恢复能力,同时也便于进行文件系统的快照和版本管理。
- 修改文件 --> 不覆盖原数据,写入新磁盘块
- 利于数据恢复、快照及版本管理
优缺点
优点
- 减少了不必要的数据复制,提高了系统的性能和资源利用率。因为在多数情况下不需要马上复制大量数据,避免了额外的开销。
- 在创建子进程或者进行数据共享时,可以快速完成操作。由于无需立即复制大量数据,能显著缩短操作时间。
缺点
- 增加了系统的复杂度,需要额外的管理机制来处理数据的复制和共享。这可能会带来更多的代码编写和维护工作。
- 在某些情况下,可能会导致内存使用量的增加。因为写操作会触发数据的复制,可能会使内存中存在多份数据副本。