LInux fork的写时复制(copy on write)

在读《Redis设计与实现》关于哈希表扩容的时候,发现这么一段话:

执行BGSAVE命令或者BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程,而大多数操作系统都采用 写时复制(copy-on-write)来优化子进程的使用效率,所以在子进程存在期间,服务器会提高负载因子的阈值,从而避免在子进程存在期间进行哈希表扩展操作,避免不必要的内存写入操作,最大限度地节约内存。

 

Linux下的copy-on-write


在说明Linux下的copy-on-write机制前,我们首先要知道两个函数:fork()exec()。需要注意的是exec()并不是一个特定的函数, 它是一组函数的统称, 它包括了execl()execlp()execv()execle()execve()execvp()

 

简单来用用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   
#include   
 
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

 

再来看看exec()函数


从上面我们已经知道了fork会创建一个子进程。子进程的是父进程的副本

exec函数的作用就是:装载一个新的程序(可执行映像)覆盖当前进程内存空间中的映像,从而执行不同的任务

  • exec系列函数在执行时会直接替换掉当前进程的地址空间

我去画张图来理解一下:

1.创建出一个子进程,往往是用来做其他事的,而不是跟父进程一样。

2.所以exec函数可以将当前的数据替换掉(将子进程变成自己想要实现的功能)

                   LInux fork的写时复制(copy on write)_第1张图片

例子∶

1.原本进程A是用来打印Hello World的,fork出的子进程默认也是打印Hello World的

2.子进程调用了exec(函数以后,就会替换掉打印Hello World的功能了(变成是自己想要实现的功能)

 

回头来看Linux下的COW是怎么一回事


fork()会产生一个和父进程完全相同的子进程(除了pid)

如果按传统的做法,会直接将父进程的数据拷贝到子进程中,拷贝完之后,父进程和子进程之间的数据段和堆栈是相互独立的

                               LInux fork的写时复制(copy on write)_第2张图片

但是,以我们的使用经验来说:往往子进程都会执行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()把当前进程映像替换成新的进程文件,完成自己想要实现的功能。

 

 如果还是不懂再举个例子


 看下面的代码

#include   
#include   
#include   
#include   
#include   
#include   
#include 
#include 
  
int main(){  
        int fd;  
        char c[10];  
        char *child = "#>Child.....output\n";  
  
        fd = open("foobar.txt",O_RDWR|O_CREAT,0666);  
        printf("fd:%d\n",fd); 
        write(fd,"foobar.txt",7);
        close(fd);

        //父进程  
        fd = open("foobar.txt",O_RDONLY,0);
        printf("fd:%d\n",fd);
        if(fork()==0)//子进程
        {  
                fd = 1;//stdout
                write(fd,child,strlen(child)+1);
                exit(0);  
        }  
        printf("fd:%d\n",fd);
        read(fd,c,sizeof(c));
        close(fd); 
        c[10]='\0';  
        printf("c = %s\n",c);  
        exit(0);  
}  

先不要往下看,猜测下这个代码的输出是啥

特别是 fd 在fork出来的进程里面进行了修改,那是不是读出来的内容会是不对的呢?

实际输出如下:

weiqifa@bsp-ubuntu1804:~/linux$ gcc forkc4.c && ./a.out
fd:3
fd:3
fd:3
c = foobar
#>Child.....output
weiqifa@bsp-ubuntu1804:~/linux$

这涉及一个知识点,叫做写时复制,就是在使用的使用,我再分配实际的物理内存给子进程,如果没有需要使用的资源,那我就还是用父进程的东西。

fork函数用于创建子进程,典型的调用一次,返回两次的函数,其中返回子进程的PID是0,其中调用进程返回了子进程的PID,而子进程则返回了0,这是一个比较有意思的函数。

但是两个进程的执行顺序是不定的。fork()函数调用完成以后父进程的虚拟存储空间被拷贝给了子进程的虚拟存储空间,因此也就实现了共享文件等操作。

但是虚拟的存储空间映射到物理存储空间的过程中采用了写时拷贝技术(具体的操作大小是按着页控制的),该技术主要是将多进程中同样的对象(数据)在物理存储其中只有一个物理存储空间,而当其中的某一个进程试图对该区域进行写操作时,内核就会在物理存储器中开辟一个新的物理页面,将需要写的区域内容复制到新的物理页面中,然后对新的物理页面进行写操作。这时就是实现了对不同进程的操作而不会产生影响其他的进程,同时也节省了很多的物理存储器。

写时复制的技术让操作系统大大降低了实际的物理内存空间。

 

最后


最后我们再来看一下写时复制的思想(摘录自维基百科):

写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被建立,因此多个调用者只是读取操作时可以共享同一份资源。

至少从本文我们可以总结出:

  • Linux通过Copy On Write技术极大地减少了Fork的开销
  • 文件系统通过Copy On Write技术一定程度上保证数据的完整性

你可能感兴趣的:(Linux,性能优化,Redis,linux,redis)