[英]How does copy-on-write work in fork()?
我想知道如何在fork()中发生copy-on-write。
假设我们有一个具有动态int数组的进程A:
int *array = malloc(1000000*sizeof(int));
数组中的元素初始化为一些有意义的值。 然后,我们使用fork()创建一个子进程,即B.B将迭代数组并进行一些计算:
for(a in array){
a = a+1;
}
a = a+1
分配一个整数? a = a+1;
这是怎么发生的? B是否从A读取数据并将新数据写入其自己的数组? 我写了一些代码来探索COW是如何工作的。 我的环境:ubuntu 14.04,gcc4.8.2
#include <stdlib.h>
#include <stdio.h>
#include <sys/sysinfo.h>
void printMemStat(){
struct sysinfo si;
sysinfo(&si);
printf("===\n");
printf("Total: %llu\n", si.totalram);
printf("Free: %llu\n", si.freeram);
}
int main(){
long len = 200000000;
long *array = malloc(len*sizeof(long));
long i = 0;
for(; i<len; i++){
array[i] = i;
}
printMemStat();
if(fork()==0){
/*child*/
printMemStat();
i = 0;
for(; i<len/2; i++){
array[i] = i+1;
}
printMemStat();
i = 0;
for(; i<len; i++){
array[i] = i+1;
}
printMemStat();
}else{
/*parent*/
int times=10;
while(times-- > 0){
sleep(1);
}
}
return 0;
}
在fork()之后,子进程修改数组中的一半数字,然后修改整个数组。 产出是:
===
Total: 16694571008
Free: 2129162240
===
Total: 16694571008
Free: 2126106624
===
Total: 16694571008
Free: 1325101056
===
Total: 16694571008
Free: 533794816
似乎数组没有作为整体分配。 如果我稍微改变第一个修改阶段:
i = 0;
for(; i<len/2; i++){
array[i*2] = i+1;
}
产出将是:
===
Total: 16694571008
Free: 2129924096
===
Total: 16694571008
Free: 2126868480
===
Total: 16694571008
Free: 526987264
===
Total: 16694571008
Free: 526987264
取决于操作系统,硬件架构和libc。 但是,如果最近使用MMU的Linux, fork(2)将使用copy-on-write。 它只会(分配和)复制一些系统结构和页表,但堆页实际上指向父页面,直到写入。
可以使用clone(2)调用对此进行更多控制。 并且vfork(2)是一种特殊的变体,它不期望使用页面。 这通常在exec()之前使用。
至于分配:malloc()在请求的内存块(地址和大小)上有元信息,C变量是指针(在进程内存堆和堆栈中)。 对于孩子来说,这两个看起来是一样的(相同的值,因为在两个进程的地址空间中看到相同的底层内存页)。 因此,从C程序的角度来看,数组已经被分配,并且当进程存在时变量被初始化。 然而,底层内存页面指向父进程的原始物理页面,因此在修改它们之前不需要额外的内存页面。
如果子节点分配了一个新数组,它取决于它是否适合已经存在的堆页面,或者是否需要增加该进程的brk。 在这两种情况下,只复制已修改的页面,并且仅为子项分配新页面。
这也意味着物理内存可能会在malloc()之后耗尽。 (由于程序无法检查“随机代码行中的操作”的错误返回代码,这是不好的)。 某些操作系统不允许这种形式的过度使用:因此,如果你分叉一个进程,它将不会分配页面,但它要求它们在那个时候可用(保留它们)以防万一。 在Linux中,这是可配置的,称为过度使用会计 。
有些系统有一个系统调用vfork()
,最初设计为fork()
的低开销版本。 由于fork()
涉及复制进程的整个地址空间,因此非常昂贵,因此引入了vfork()
函数(在3.0BSD中)。
但是,由于引入了vfork()
, fork()
的实现得到了极大的改进,最引人注目的是引入了“copy-on-write”, 其中通过允许两个进程引用来透明地伪造进程地址空间的复制到相同的物理内存,直到它们中的任何一个修改它。 这在很大程度上消除了vfork();
的理由vfork();
实际上,很大一部分系统现在完全缺乏vfork()
的原始功能。 但是为了兼容性,可能仍然存在vfork()
调用, vfork()
调用fork()
而不尝试模拟所有的vfork()
语义。
因此,实际使用fork()
和vfork()
之间的任何差异是非常不明智的。 实际上,使用vfork()
可能是不明智的,除非你确切知道你想要的原因。
两者之间的基本区别在于,当使用vfork()
创建新进程时,父进程将暂时挂起,子进程可能会借用父进程的地址空间。 这种奇怪的状态一直持续到子进程退出或调用execve()
,此时父进程继续。
这意味着vfork()
的子进程必须小心,以避免意外修改父进程的变量。 特别是,子进程不能从包含vfork()
调用的函数返回,并且它不能调用exit()
(如果需要退出,它应该使用_exit();
实际上,对于子进程也是如此正常的fork()
)。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.