【问题标题】:How does copy-on-write work in fork()?写时复制如何在 fork() 中工作?
【发布时间】:2015-01-25 12:06:49
【问题描述】:

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

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

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

【问题讨论】:

  • 孩子没有“分配”任何东西。子进程从父进程的精确、完整副本开始,然后从那里继续执行。
  • 那么,A和B共享数组?
  • @KerrekSB:但它会获得自己独立的进程内存,应该是写时复制。
  • COW 是一个实现细节,它不会影响您的程序。对于 unix.stackexchange.com 或 superuser.com,这个问题可能会更好。

标签: c linux unix fork copy-on-write


【解决方案1】:

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

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

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

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

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

【讨论】:

  • 但是子进程什么时候为数组分配内存呢? (只分配内存不复制)
  • @MinFu 取决于您对“内存”和“分配”的含义:)(我在答案中添加了一些额外的解释)。
  • 如果子级将其与父级分离会发生什么?我猜在孩子分离之前,父母的所有页面都会被复制到孩子空间。如果没有,当父节点在分叉后死亡/存在时会发生什么?
  • 在多个进程之间共享的页面没有所有者,这些页面不必在分离时复制。只要页面映射到任何进程,页面就处于活动状态。
  • 对于像我这样不知道brk 是什么意思的人:这是程序数据部分的结束。增加它可以有效地为进程分配更多内存。
【解决方案2】:

有些系统有一个系统调用vfork(),原来是 设计为fork() 的低开销版本。自从 fork() 涉及复制进程的整个地址空间, 因此相当昂贵,vfork() 函数是 引入(在 3.0BSD 中)。

但是,由于引入了vfork()fork() 的实施有了显着改善,最显着的是 随着“写时复制”的引入,复制 通过允许两个进程透明地伪造进程地址空间 引用相同的物理内存,直到它们中的任何一个修改 这在很大程度上消除了vfork(); 的理由 大部分系统现在缺乏原有的功能 vfork() 完全。不过为了兼容性,可能还是有 一个 vfork() 呼叫存在,它只是调用 fork() 而没有 试图模拟所有 vfork() 语义。

因此,实际使用任何 fork()vfork() 之间的区别。的确是 可能根本不明智地使用vfork(),除非您确切知道 为什么要这样做。

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

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

【讨论】:

    猜你喜欢
    • 2020-02-29
    • 2012-06-15
    • 2017-01-28
    • 2013-02-12
    • 2011-03-31
    • 1970-01-01
    • 1970-01-01
    • 2014-05-30
    • 1970-01-01
    相关资源
    最近更新 更多