Linux-Copy On Write写时复制机制初探

2021-08-17 11:31:54 浏览数 (1)


COW概述

来看下 https://en.wikipedia.org/wiki/Copy-on-write的说明

Copy-on-write (COW), sometimes referred to as implicit sharing[1] or shadowing,[2] is a resource-management technique used in computer programming to efficiently implement a “duplicate” or “copy” operation on modifiable resources.[3] If a resource is duplicated but not modified, it is not necessary to create a new resource; the resource can be shared between the copy and the original. Modifications must still create a copy, hence the technique: the copy operation is deferred to the first write. By sharing resources in this way, it is possible to significantly reduce the resource consumption of unmodified copies, while adding a small overhead to resource-modifying operations.

写入时复制(COW),有时也称为隐式共享,是一种计算机管理中用来有效地对可修改资源执行“复制”操作的资源管理技术。

如果资源重复但未修改,则无需创建新资源,资源可以在副本和原始副本之间共享。

修改仍然必须创建一个副本,因此使用COW,可以将复制操作推迟到第一次写入。

通过以这种方式共享资源,可以显着减少未修改副本的资源消耗,当然了资源修改操作的时候也会增加少量开销。

简单来说 COW 写时复制是提高资源使用效率的一种手段, 在内存管理(进程的 fork),数据存储( 比如 Docker 的 AUFS 文件系统),软件开发(Java的Copy On Write容器)、高可用HA软件中广泛使用。


*Unix

在传统的Unix环境下,有两个基本的操作用于创建和修改进程:

  • 函数fork( )用来创建一个新的进程,该进程几乎是当前进程的一个完全拷贝
  • 函数族exec( )用来启动另外的进程以取代当前运行的进程(函数族exec( )是一组函数的统称, 它包括了execl()、execlp()、execv()、execle()、execve()、execvp()等)

fork

fork是类Unix操作系统上创建进程的主要方法,fork用于创建子进程。

新的进程要通过老的进程复制自身得到,Linux下init进程是所有进程的父 。 Linux的进程都通过init进程或init的子进程fork(vfork)出来的

代码语言:javascript复制
#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;  
}  

输出结果

代码语言:javascript复制
我是子进程,由父进程fork出来

统计结果是: 1

我是父进程

统计结果是: 1

fork 函数会有两次返回 1) 将子进程的PID返回给父进程,2) 0返回给子进程。(如果小于0,则说明创建子进程失败)。

当前进程调用fork(),会创建一个跟当前进程完全相同的子进程(除了pid),所以子进程同样是会执行fork()之后的代码。

故: 父进程在执行if代码块的时候,fpid变量的值是子进程的pid,子进程在执行if代码块的时候,fpid变量的值是0


函数族exec( )

在Linux中要使用exec函数族。系统调用execve()对当前进程进行替换,替换者为一个指定的程序,其参数包括文件名(filename)、参数列表(argv)以及环境变量(envp)。

exec函数族不止一个,但它们大致相同,在 Linux中,它们分别是:execl,execlp,execle,execv,execve和execvp。

一个进程一旦调用exec类函数,它本身就"死亡"了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。


为什么有了COW?

早期的 Unix 在实现 fork 系统调用时,并没有使用该技术,创建新进程的开销很大。

出于效率考虑,Copy On Write 技术引入到进程中,fork 之后的父进程和子进程完全共享数据段、代码段、堆和栈等的完全副本。

Linux在使用fork()函数进程创建时,传统fork()的做法是系统把所有的资源复制给新创建的进程,这种方式不仅单一,而且效率低下。因为所拷贝的数据或别的资源可能是可以共享的。现在Linux的fork()使用写时拷贝页来实现新进程的创建,它是一种可推迟甚至避免数据拷贝的技术,刚开始时内核并不会复制整个地址空间,而是让父子进程共享地址空间,只有在写时才复制地址空间,使得父子进程都拥有独立的地址空间,即资源的复制是在只有需要写入时才会发生,因此而称之为Copy on Write(COW)。

在此之前都是以读的方式去和父进程共享资源,这样,在页根本不会被写入的场景下,fork()立即执行exec(),无需对地址空间进行复制,fork()的实际开销就是复制父进程的一个页表和为子进程创建一个进程描述符,也就是说只有当进程空间中各段的内存内容发生变化时,父进程才将其内容复制一份传给子进程,大大提高了效率。

通俗来说 fork创建出的子进程,与父进程共享内存空间。 如果子进程不对内存空间进行写入操作的话,内存空间中的数据并不会复制给子进程,这样创建子进程的速度就很快 ,因为不用复制,直接引用父进程的物理空间 ,并且如果在fork函数返回之后,子进程第一时间exec一个新的可执行映像,那么也不会浪费时间和内存空间了 。


COW 原理

fork()之后,kernel把父进程中所有的内存页的权限都设为read-only,然后子进程的地址空间指向父进程。当父子进程都只读内存时,相安无事。当其中某个进程写内存时,CPU硬件检测到内存页是read-only的,于是触发页异常中断(page-fault),陷入kernel的一个中断例程。中断例程中,kernel就会把触发的异常的页复制一份,于是父子进程各自持有独立的一份。


COW的优缺点

优点

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

缺点

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

小结

fork出的子进程共享父进程的物理空间,当父子进程有内存写入操作时,read-only内存页发生中断,将触发的异常的内存页复制一份(其余的页还是共享父进程的)。

fork出的子进程功能实现和父进程是一样的。如果有需要, 会调用exec()把当前进程映像替换成新的进程文件,完成自定义的功能。


参考: 维基百科-Copy-on-write COW奶牛!Copy On Write机制了解一下


0 人点赞