8

我想知道 fork() 中的写时复制是如何发生的。

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

int *array = malloc(1000000*sizeof(int));

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

for(a in array){
    a = a+1;
}
  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;
}

在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
4

2 回答 2

8

取决于操作系统、硬件架构和 libc。但是是的,在最近带有 MMU 的 Linux 的情况下,fork(2)将与写时复制一起使用。它只会(分配和)复制一些系统结构和页表,但堆页面实际上指向父页面,直到被写入。

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

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

如果孩子分配一个新数组,它取决于它是否适合已经存在的堆页面,或者是否需要增加进程的 brk。在这两种情况下,只有修改过的页面会被复制,新页面只会分配给子页面。

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

于 2014-11-27T00:57:22.957 回答
4

一些系统有一个系统调用vfork(),它最初被设计为一个低开销版本的fork(). 由于 fork()涉及复制进程的整个地址空间,因此非常昂贵,因此vfork()引入了该功能(在 3.0BSD 中)。

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

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

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

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

于 2015-03-27T20:54:53.793 回答