前言
在读《Redis设计与实现》关于哈希表扩容的时候,发现这么一段话:
执行BGSAVE命令或者BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)来优化子进程的使用效率,所以在子进程存在期间,服务器会提高负载因子的阈值,从而避免在子进程存在期间进行哈希表扩展操作,避免不必要的内存写入操作,最大限度地节约内存。
触及到知识的盲区了,于是就去搜了一下copy-on-write写时复制这个技术究竟是怎么样的。发现涉及的东西蛮多的,也挺难读懂的。于是就写下这篇笔记来记录一下我学习copy-on-write的过程。
本文力求简单讲清copy-on-write这个知识点,希望大家看完能有所收获。
一、Linux下的copy-on-write
在说明Linux下的copy-on-write机制前,我们首先要知道两个函数:fork()
和exec()
。需要注意的是exec()
并不是一个特定的函数, 它是一组函数的统称, 它包括了execl()
、execlp()
、execv()
、execle()
、execve()
、execvp()
。
1.1简单来看看fork
首先我们来看一下fork()
函数是什么鬼:
fork is an operation whereby a process creates a copy of itself.
fork是类Unix操作系统上创建进程的主要方法。fork用于创建子进程(等同于当前进程的副本)。
- 新的进程要通过老的进程复制自身得到,这就是fork!
如果接触过Linux,我们会知道Linux下init进程是所有进程的爹(相当于Java中的Object对象)
- Linux的进程都通过init进程或init的子进程fork(vfork)出来的。
1.2再来看看exec()函数
从上面我们已经知道了fork会创建一个子进程。子进程的是父进程的副本。
exec函数的作用就是:装载一个新的程序(可执行映像)覆盖当前进程内存空间中的映像,从而执行不同的任务。
- exec系列函数在执行时会直接替换掉当前进程的地址空间。
参考资料:
- 程序员必备知识——fork和exec函数详解blog.csdn.net/bad_good_ma…
- linux中fork()函数详解(原创!!实例讲解):blog.csdn.net/jason314/ar…
- linux c语言 fork() 和 exec 函数的简介和用法:blog.csdn.net/nvd11/artic…
- Linux下Fork与Exec使用:www.cnblogs.com/hicjiajia/a…
- Linux 系统调用 —— fork()内核源码剖析:blog.csdn.net/chen8927040…
1.3回头来看Linux下的COW是怎么一回事
fork()会产生一个和父进程完全相同的子进程(除了pid)
如果按传统的做法,会直接将父进程的数据拷贝到子进程中,拷贝完之后,父进程和子进程之间的数据段和堆栈是相互独立的。
但是,以我们的使用经验来说:往往子进程都会执行exec()
来做自己想要实现的功能。
- 所以,如果按照上面的做法的话,创建子进程时复制过去的数据是没用的(因为子进程执行
exec()
,原有的数据会被清空)
既然很多时候复制给子进程的数据是无效的,于是就有了Copy On Write这项技术了,原理也很简单:
- fork创建出的子进程,与父进程共享内存空间。也就是说,如果子进程不对内存空间进行写入操作的话,内存空间中的数据并不会复制给子进程,这样创建子进程的速度就很快了!(不用复制,直接引用父进程的物理空间)。
- 并且如果在fork函数返回之后,子进程第一时间exec一个新的可执行映像,那么也不会浪费时间和内存空间了。
另外的表达方式:
在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。
当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。
如果不是因为exec,内核会给子进程的数据段、堆栈段分配相应的物理空间(至此两者有各自的进程空间,互不影响),而代码段继续共享父进程的物理空间(两者的代码完全相同)。
而如果是因为exec,由于两者执行的代码不同,子进程的代码段也会分配单独的物理空间。
Copy On Write技术实现原理:
fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。
Copy On Write技术好处是什么?
- COW技术可减少分配和复制大量资源时带来的瞬间延时。
- COW技术可减少不必要的资源分配。比如fork进程时,并不是所有的页面都需要复制,父进程的代码段和只读数据段都不被允许修改,所以无需复制。
Copy On Write技术缺点是什么?
- 如果在fork()之后,父子进程都还需要继续进行写操作,那么会产生大量的分页错误(页异常中断page-fault),这样就得不偿失。
几句话总结Linux的Copy On Write技术:
- fork出的子进程共享父进程的物理空间,当父子进程有内存写入操作时,read-only内存页发生中断,将触发的异常的内存页复制一份(其余的页还是共享父进程的)。
- fork出的子进程功能实现和父进程是一样的。如果有需要,我们会用
exec()
把当前进程映像替换成新的进程文件,完成自己想要实现的功能。
参考资料:
- Linux进程基础:www.cnblogs.com/vamei/archi…
- Linux写时拷贝技术(copy-on-write)www.cnblogs.com/biyeymyhjob…
- 当你在 Linux 上启动一个进程时会发生什么?zhuanlan.zhihu.com/p/33159508
- Linux fork()所谓的写时复制(COW)到最后还是要先复制再写吗?www.zhihu.com/question/26…
- 写时拷贝(copy-on-write) COW技术blog.csdn.net/u012333003/…
- Copy-On-Write 写时复制原理blog.csdn.net/ppppppppp20…
二、解释一下Redis的COW
基于上面的基础,我们应该已经了解COW这么一项技术了。
下面我来说一下我对《Redis设计与实现》那段话的理解:
- Redis在持久化时,如果是采用BGSAVE命令或者BGREWRITEAOF的方式,那Redis会fork出一个子进程来读取数据,从而写到磁盘中。
- 总体来看,Redis还是读操作比较多。如果子进程存在期间,发生了大量的写操作,那可能就会出现很多的分页错误(页异常中断page-fault),这样就得耗费不少性能在复制上。
- 而在rehash阶段上,写操作是无法避免的。所以Redis在fork出子进程之后,将负载因子阈值提高,尽量减少写操作,避免不必要的内存写入操作,最大限度地节约内存。
参考资料:
- fork()后copy on write的一些特性:zhoujianshi.github.io/articles/20…
- 写时复制:miao1007.github.io/gitbook/jav…