copy-on-write如何在fork()中工作?

Min*_* Fu 7 c unix linux fork copy-on-write

我想知道如何在fork()中发生copy-on-write.

假设我们有一个具有动态int数组的进程A:

int *array = malloc(1000000*sizeof(int));
Run Code Online (Sandbox Code Playgroud)

数组中的元素初始化为一些有意义的值.然后,我们使用fork()创建一个子进程,即B.B将迭代数组并进行一些计算:

for(a in array){
    a = a+1;
}
Run Code Online (Sandbox Code Playgroud)
  1. 我知道B不会立即复制整个数组,但是什么时候子B为数组分配内存?在fork()期间?
  2. 它是一次性分配整个数组,还是只分配一个整数a = a+1
  3. 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;
}
Run Code Online (Sandbox Code Playgroud)

在fork()之后,子进程修改数组中的一半数字,然后修改整个数组.产出是:

===
Total: 16694571008
Free: 2129162240
===
Total: 16694571008
Free: 2126106624
===
Total: 16694571008
Free: 1325101056
===
Total: 16694571008
Free: 533794816
Run Code Online (Sandbox Code Playgroud)

似乎数组没有作为整体分配.如果我稍微改变第一个修改阶段:

i = 0;
for(; i<len/2; i++){
    array[i*2] = i+1;
}
Run Code Online (Sandbox Code Playgroud)

产出将是:

===
Total: 16694571008
Free: 2129924096
===
Total: 16694571008
Free: 2126868480
===
Total: 16694571008
Free: 526987264
===
Total: 16694571008
Free: 526987264
Run Code Online (Sandbox Code Playgroud)

eck*_*kes 8

取决于操作系统,硬件架构和libc.但是,如果最近使用MMU的Linux,fork(2)将使用copy-on-write.它只会(分配和)复制一些系统结构和页表,但堆页实际上指向父页面,直到写入.

可以使用clone(2)调用对此进行更多控制.并且vfork(2)是一种特殊的变体,它不期望使用页面.这通常在exec()之前使用.

至于分配:malloc()在请求的内存块(地址和大小)上有元信息,C变量是指针(在进程内存堆和堆栈中).对于孩子来说,这两个看起来是一样的(相同的值,因为在两个进程的地址空间中看到相同的底层内存页).因此,从C程序的角度来看,数组已经被分配,并且当进程存在时变量被初始化.然而,底层内存页面指向父进程的原始物理页面,因此在修改它们之前不需要额外的内存页面.

如果子节点分配了一个新数组,它取决于它是否适合已经存在的堆页面,或者是否需要增加该进程的brk.在这两种情况下,只复制已修改的页面,并且仅为子项分配新页面.

这也意味着物理内存可能会在malloc()之后耗尽.(由于程序无法检查"随机代码行中的操作"的错误返回代码,这是不好的).某些操作系统不允许这种形式的过度使用:因此,如果你分叉一个进程,它将不会分配页面,但它要求它们在那个时候可用(保留它们)以防万一.在Linux中,这是可配置的,称为过度使用会计.


小智 5

有些系统有一个系统调用vfork(),它最初被设计为fork(). 由于 fork()涉及复制进程的整个地址空间,因此相当昂贵,因此vfork()引入了该函数(在 3.0BSD 中)。

然而,自从vfork()引入以来,实现fork()已经有了很大的改进,最显着的是“写时复制”的引入,其中通过允许两个进程引用相同的物理内存来透明地伪造进程地址空间的复制,直到他们中的任何一个都会修改它。vfork();这在很大程度上消除了现在很大一部分系统完全缺乏原始功能的 理由vfork()。不过,为了兼容性,可能仍然存在一个vfork()调用,它只是简单地调用,fork()而不尝试模拟所有vfork()语义。

因此,实际利用 和 之间的任何差异是非常不明智fork()vfork()。事实上,使用它可能是不明智的vfork(),除非您确切地知道为什么要这样做。

两者之间的基本区别在于,当使用 创建新进程时vfork(),父进程会暂时挂起,并且子进程可能会借用父进程的地址空间。这种奇怪的事态一直持续到子进程退出或调用execve(),此时父进程继续。

这意味着a的子进程vfork()必须小心,避免意外修改父进程的变量。特别是,子进程不能从包含调用的函数中返回vfork(),并且不能调用 exit()(如果需要退出,则应该_exit(); 实际使用,对于普通 的子进程也是如此fork())。