Linux写时复制

最后更新:2019-09-06

copy-on-write,写时复制,是计算机程序设计领域的一种优化策略,其核心思想是,当有多个调用者都需要请求相同资源时,一开始资源只会有一份,多个调用者共同读取这一份资源,当某个调用者需要修改数据的时候,才会分配一块内存,将数据拷贝过去,供这个调用者使用,而其他调用者依然还是读取最原始的那份数据。每次有调用者需要修改数据时,就会重复一次拷贝流程,供调用者修改使用。

使用copy-on-write可以避免或者减少数据的拷贝操作,极大的提高性能,其应用十分广泛,例如Linux的fork调用,Linux的文件管理系统,一些数据库服务,Java中的CopyOnWriteArrayList。

Linux在启动过程中,会初始化内核,而内核初始化的最后一步,是创建一个PID为1的超级进程,又叫做根进程。系统中所有的其他进程,都是由这个根进程直接或者间接产生的,而产生进程的方式,就是利用fork系统调用,fork是类Unix操作系统上创建进程的主要方法。

fork()中的copy-on-write

fork进程之后,父进程中的数据怎么办?常规思路是,给子进程重新开辟一块物理内存,将父进程的数据拷贝到子进程中,拷贝完之后,父进程和子进程之间的数据段和堆栈是相互独立的。这样做会带来两个问题:

  • 拷贝本身会有CPU和内存的开销;

  • fork出来的子进程在此后多会执行exec()系统调用。exec系统调用会装载一个新的程序,替换掉当前进程的地址空间,从而执行不同的任务。即是会抛弃父进程的数据。

也就是说,绝大部分情况下,fork一个子进程会耗费CPU和内存资源,但是马上又被子进程抛弃不用了,那么资源的开销就显得毫无意义,于是出于效率考虑,linux引入了copy-on-write技术。

在fork()调用之后,只会给子进程分配虚拟内存地址,而父子进程的虚拟内存地址虽然不同,但是映射到物理内存上都是同一块区域,子进程的代码段、数据段、堆栈都是指向父进程的物理空间。

并且此时父进程中所有对应的内存页都会被标记为只读,父子进程都可以正常读取内存数据,当其中某个进程需要更新数据时,检测到内存页是read-only的,内存管理单元(MMU)便会抛出一个页面异常中断,(page-fault),在处理异常时,内核便会把触发异常的内存页拷贝一份(其他内存页还是共享的一份),让父子进程各自持有一份。

下图分别反映了修改页面 C 的前与后。

例如,假设子进程试图修改包含部分堆栈的页面,并且设置为写时复制。操作系统会创建这个页面的副本,将其映射到子进程的地址空间。然后,子进程会修改复制的页面,而不是属于父进程的页面。显然,当使用写时复制技术时,仅复制任何一进程修改的页面,所有未修改的页面可以由父进程和子进程共享。 还要注意,只有可以修改的页面才需要标记为写时复制。不能修改的页面(包含可执行代码的页面)可以由父进程和子进程共享。

Copy On Write技术好处是什么?

  • COW技术可减少分配和复制大量资源时带来的瞬间延时
  • COW技术可减少不必要的资源分配。比如fork进程时,并不是所有的页面都需要复制,父进程的代码段和只读数据段都不被允许修改,所以无需复制

Copy On Write技术缺点是什么?

  • 如果在fork()之后,父子进程都还需要继续进行写操作,那么会产生大量的分页错误(页异常中断page-fault),这样就得不偿失。

几句话总结Linux的Copy On Write技术:

  • fork出的子进程共享父进程的物理空间,当父子进程有内存写入操作时,read-only内存页发生中断,将触发的异常的内存页复制一份(其余的页还是共享父进程的)。
  • fork出的子进程功能实现和父进程是一样的。如果有需要,我们会用exec()把当前进程映像替换成新的进程文件,完成自己想要实现的功能。
Edgar

Edgar
一个略懂Java的小菜比