Java架构直通车——避不开的COW奶牛

文章目录

  • 引言
  • Linux下的COW
  • Redis下的COW
  • 文件系统下的COW

引言

在Java架构直通车——Redis持久化和宕机恢复机制一文中曾经提到过COW(写时复制机制),在执行BGSAVE命令或者BGREWRITEAOF命令(AOF重写)的过程中,Redis需要创建fork()当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)来优化子进程的使用效率。

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


  • fork()

fork是类Unix操作系统上创建进程的主要方法。fork用于创建子进程(等同于当前进程的副本)。Linux的进程都通过init进程或init的子进程fork(vfork)出来的,这里init的地位相当于Java里Object类的地位。

以一段代码说明

#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(),会创建一个跟当前进程完全相同的子进程(除了pid),所以子进程同样是会执行fork()之后的代码


  • exec()

exec函数的作用就是:装载一个新的程序(可执行映像)覆盖当前进程内存空间中的映像,从而执行不同的任务。exec系列函数在执行时会直接替换掉当前进程的地址空间。

因为子进程会执行fork()后的代码,但是子进程实际上是用来执行其他任务的,而不是和父进程执行一样的任务,所以需要用exec()来保障子进程实现自己的功能。

Linux下的COW

上一节我们知道,fork()会产生一个和父进程完全相同的子进程,并且会直接将父进程的数据拷贝到子进程中,拷贝完成后,父进程和子进程之间的数据段和堆栈是相互独立的。
Java架构直通车——避不开的COW奶牛_第1张图片

但是,以我们的使用经验来说:往往子进程都会执行exec()来做自己想要实现的功能。所以,如果按照上面的做法的话,创建子进程时复制过去的数据是没用的(因为子进程执行exec(),原有的数据会被清空)

那么就产生了一个问题,既然要清空,那为什么还要多此一举做拷贝呢?既然很多时候复制给子进程的数据是无效的,于是就有了Copy On Write这项技术了,原理也很简单:fork创建出的子进程,与父进程共享内存空间,即子进程会引用父进程的物理空间。也就是说,如果子进程不对内存空间进行写入操作的话,内存空间中的数据并不会复制给子进程,这样创建子进程的速度就很快了!

更准确的说:

  • 在fork之后exec之前两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,也就是说, 两者的虚拟空间不同,但其对应的物理空间是同一个
  • 当父子进程中有更改相应段的行为发生时,再为子进程 相应的段 分配物理空间

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


  • Copy On Write技术好处是什么?

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

  • Copy On Write技术缺点是什么?

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

Redis下的COW

在《Redis设计与实现》中关于哈希表扩容有这么一段话:

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

  • Redis在持久化时,如果是采用BGSAVE命令或者BGREWRITEAOF的方式,那Redis会fork出一个子进程来读取数据,从而写到磁盘中。
  • 总体来看,Redis还是读操作比较多。如果子进程存在期间,父进程发生了大量的写操作,那可能就会出现很多的分页错误(页异常中断page-fault),这样就得耗费不少性能在复制上
  • 而在rehash阶段上,写操作是无法避免的。所以Redis在fork出子进程之后,将负载因子阈值提高,尽量减少写操作,避免不必要的内存写入操作,最大限度地节约内存。

文件系统下的COW

Copy-on-write在对数据进行修改的时候,不会直接在原来的数据位置上进行操作,而是重新找个位置修改,这样的好处是一旦系统突然断电,重启之后不需要做Fsck。好处就是能保证数据的完整性,掉电的话容易恢复。

比如说:要修改数据块A的内容,先把A读出来,写到B块里面去。如果这时候断电了,原来A的内容还在!

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

参考:COW

你可能感兴趣的:(Java架构直通车)