0%

Copy-on-write(写时拷贝)

copy-on-write,写时拷贝,是计算机程序设计领域的一种优化策略,其核心思想是,当有多个调用者都需要请求相同资源时,一开始资源只会有一份,多个调用者共同读取这一份资源,当某个调用者需要修改数据的时候,才会分配一块内存,将数据拷贝过去,供这个调用者使用,而其他调用者依然还是读取最原始的那份数据。每次有调用者需要修改数据时,就会重复一次拷贝流程,供调用者修改使用。

使用copy-on-write可以避免或者减少数据的拷贝操作,极大的提高性能,其应用十分广泛,例如Linux的fork调用,Linux的文件管理系统,一些数据库服务,Java中的CopyOnWriteArrayList,C++98/C++03中的std::string等等。

Linux中的fork()

Linux在启动过程中,会初始化内核,而内核初始化的最后一步,是创建一个PID为1的超级进程,又叫做根进程。系统中所有的其他进程,都是由这个根进程直接或者间接产生的,而产生进程的方式,就是利用fork系统调用,fork是类Unix操作系统上创建进程的主要方法。

fork()的函数原型很简单:

1
pid_t fork();

我们来看一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <unistd.h>
#include <stdio.h>

int main() {
int pid = fork();

if (pid == -1) {
return -1;
}

if (pid > 0) {
printf("Hi, father: %d\n", getpid());
return 0;
} else {
printf("Hi, child: %d\n", getpid());
return 0;
}
}

通过gcc编译之后执行,输出:

Hi, father: 7562
Hi, child: 7563

从输出来看,if和else居然都执行了,因为用fork()有个神奇的地方,一次调用,两次返回。

调用fork()之后,会出现两个进程,一个是子进程,一个是父进程,在子进程中,fork()返回0,在父进程中,fork()返回新创建的子进程的进程ID,我们可以通过fork()函数的返回值来判断当前进程是子进程还是父进程。两个进程都会从调用fork()的地方继续执行。

fork()中的copy-on-write

fork进程之后,父进程中的数据怎么办?常规思路是,给子进程重新开辟一块物理内存,将父进程的数据拷贝到子进程中,拷贝完之后,父进程和子进程之间的数据段和堆栈是相互独立的。这样做会带来两个问题:

  • 拷贝本身会有CPU和内存的开销;

  • fork出来的子进程在此后多会执行exec()系统调用。

    exec系统调用会装载一个新的程序,替换掉当前进程的地址空间,从而执行不同的任务。即是会抛弃父进程的数据。

也就是说,绝大部分情况下,fork一个子进程会耗费CPU和内存资源,但是马上又被子进程抛弃不用了,那么资源的开销就显得毫无意义,于是出于效率考虑,linux引入了copy-on-write技术。

在fork()调用之后,只会给子进程分配虚拟内存地址,而父子进程的虚拟内存地址虽然不同,但是映射到物理内存上都是同一块区域,子进程的代码段、数据段、堆栈都是指向父进程的物理空间。

1

并且此时父进程中所有对应的内存页都会被标记为只读,父子进程都可以正常读取内存数据,当其中某个进程需要更新数据时,检测到内存页是read-only的,内存管理单元(MMU)便会抛出一个页面异常中断,(page-fault),在处理异常时,内核便会把触发异常的内存页拷贝一份(其他内存页还是共享的一份),让父子进程各自持有一份。

这样做的好处不言而喻,能极大的提高fork操作时的效率,但是坏处是,如果fork之后,两个进程各自频繁的更新数据,则会导致大量的分页错误,这样就得不偿失了。

Java中的CopyOnWrite容器

Java中有两个容器:CopyOnWriteArrayList和CopyOnWriteArraySet,从名字就可以看出,其实现思想也是参考了copy-on-write技术。

当我们往一个CopyOnWrite的容器中添加数据的时候,并不会直接添加到当前容器中,而是会拷贝出一个新的容器,然后往新的容器里添加数据,在添加过程中,所有的读操作都会指向旧的容器,添加操作完成之后,再将原容器的引用指向新的容器。为了避免同时有多个线程更新数据,从而拷贝出多个容器的副本,会在拷贝容器的时候进行加锁。

这样做的好处是对CopyOnWrite容器进行读操作的时候并不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

C++中的std::string

C++98/C++03中的std::string使用了copy-on-write技术,在C++11标准中为了提高并行性取消了这一策略。

C++在分配一个string对象时,会在数据区的前面多分配一点空间,用于存储string的引用计数。

2

当触发一个string的拷贝构造函数或者赋值函数时,便会对这个引用计数加一。需要修改内容时,如果引用计数不为零,表示有人在共享这块内存,那么自己需要先做一份拷贝,然后把引用计数减去一,再把数据拷贝过来。

参考



-=全文完=-