我想知道如何在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
答案 0 :(得分:8)
取决于操作系统,硬件架构和libc。但是,对于最近使用MMU的Linux,fork(2)将使用copy-on-write。它只会(分配和)复制一些系统结构和页面表,但堆页面实际上指向父页面,直到写入。
可以通过clone(2)调用对此进行更多控制。并且vfork(2)是一种特殊的变体,它不期望使用这些页面。这通常在exec()之前使用。
至于分配:malloc()在请求的内存块(地址和大小)上有元信息,C变量是指针(在进程内存堆和堆栈中都有)。对于孩子来说,这两个看起来是一样的(相同的值,因为在两个进程的地址空间中看到相同的底层内存页)。因此,从C程序的角度来看,数组已经被分配,并且当进程存在时变量被初始化。然而,底层内存页面指向父进程的原始物理页面,因此在修改它们之前不需要额外的内存页面。
如果子节点分配一个新数组,它取决于它是否适合已经存在的堆页面,或者是否需要增加该进程的brk。在这两种情况下,只复制修改后的页面,并且仅为子项分配新页面。
这也意味着物理内存可能会在malloc()之后耗尽。 (由于程序无法检查“随机代码行中的操作”的错误返回代码,这是不好的)。某些操作系统不允许这种形式的过度使用:因此,如果你分叉一个进程,它将不会分配页面,但它要求它们在那个时候可用(保留它们)以防万一。在Linux中,这是configurable and called overcommit-accounting。
答案 1 :(得分:4)
有些系统最初有系统调用vfork()
设计为fork()
的低开销版本。以来
fork()
涉及复制流程的整个地址空间,
因此,vfork()
功能非常昂贵
介绍(在3.0BSD中)。
然而,自从vfork()
被引入以来,
fork()
的实施已经大大改善,最值得注意的是
随着'copy-on-write'的引入,在哪里复制了
通过允许两个进程透明地伪造进程地址空间
在它们中的任何一个修改之前引用相同的物理内存
它。这在很大程度上消除了vfork();
的理由,a
大部分系统现在缺乏原有的功能
完全vfork()
。但是,为了兼容性,可能仍然存在
存在vfork()
来电,只需拨打fork()
即可
试图模仿所有vfork()
语义。
因此,实际使用任何一个是非常不明智的
fork()
和vfork()
之间的差异。的确是
除非你确切知道,否则完全使用vfork()
可能是不明智的
为什么你想要。
两者之间的基本区别在于,当使用vfork()
创建新进程时,父进程将暂时挂起,子进程可能会借用父进程的地址空间。这种奇怪的状态一直持续到子进程退出或调用execve()
,此时父进程继续。
这意味着vfork()
的子进程必须小心
避免意外修改父进程的变量。在
特别是,子进程不能从函数返回
包含vfork()
电话,但不得拨打电话
exit()
(如果需要退出,则应使用_exit();
实际上,对于正常fork()
的孩子来说也是如此。