只有光头才能变强
在读《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)出来的。
下面以例子说明一下fork吧:
#include <unistd.h> #include <stdio.h> int main () { pid_t fpid; //fpid表示fork函数返回的值 int count=0; // 调用fork,创建出子进程 fpid=fork(); // 所以下面的代码有两个进程执行! if (fpid < 0) printf("创建进程失败!/n"); else if (fpid == 0) { printf("我是子进程,由父进程fork出来/n"); count++; } else { printf("我是父进程/n"); count++; } printf("统计结果是: %d/n",count); return 0; }得到的结果输出为:
我是子进程,由父进程fork出来 统计结果是: 1 我是父进程 统计结果是: 1解释一下:
fork作为一个函数被调用。这个函数会有两次返回,将子进程的PID返回给父进程,0返回给子进程。(如果小于0,则说明创建子进程失败)。
再次说明:当前进程调用fork(),会创建一个跟当前进程完全相同的子进程(除了pid),所以子进程同样是会执行fork()之后的代码。
所以说:
父进程在执行if代码块的时候,fpid变量的值是子进程的pid
子进程在执行if代码块的时候,fpid变量的值是0
1.2再来看看exec()函数从上面我们已经知道了fork会创建一个子进程。子进程的是父进程的副本。
exec函数的作用就是:装载一个新的程序(可执行映像)覆盖当前进程内存空间中的映像,从而执行不同的任务。
exec系列函数在执行时会直接替换掉当前进程的地址空间。
我去画张图来理解一下:
参考资料:
程序员必备知识——fork和exec函数详解https://blog.csdn.net/bad_good_man/article/details/49364947
linux中fork()函数详解(原创!!实例讲解):https://blog.csdn.net/jason314/article/details/5640969
linux c语言 fork() 和 exec 函数的简介和用法:https://blog.csdn.net/nvd11/article/details/8856278
Linux下Fork与Exec使用:https://www.cnblogs.com/hicjiajia/archive/2011/01/20/1940154.html
Linux 系统调用 —— fork()内核源码剖析:https://blog.csdn.net/chen892704067/article/details/76596225
1.3回头来看Linux下的COW是怎么一回事fork()会产生一个和父进程完全相同的子进程(除了pid)
如果按传统的做法,会直接将父进程的数据拷贝到子进程中,拷贝完之后,父进程和子进程之间的数据段和堆栈是相互独立的。
但是,以我们的使用经验来说:往往子进程都会执行exec()来做自己想要实现的功能。
所以,如果按照上面的做法的话,创建子进程时复制过去的数据是没用的(因为子进程执行exec(),原有的数据会被清空)
既然很多时候复制给子进程的数据是无效的,于是就有了Copy On Write这项技术了,原理也很简单: