コピーオンライト

May 05, 2018

コピーオンライトとは

実は fork() と execve() で使用した fork() システムコールは、仮想記憶のしくみを用いて高速化されている。
fork() システムコール発行時には、親プロセスのメモリを子プロセスに全てコピーするのではなく、ページテーブルのみをコピーする。また、その際にページテーブルエントリ内の書き込み権限を一時的に無効化する。

その後、ページの読み取り操作のみであれば、どちらのプロセスも共有された物理ページにアクセス可能。しかし、親プロセスもしくは子プロセスのどちらかがページのどこかを更新しようとすると、次の流れで共有を解除する。

  1. ページへの書き込みが許可されていないため、書き込み時に CPU 上でページフォルトが発生
  2. CPU がカーネルモードに移行し、カーネルのページフォルトハンドラがトリガーされる
  3. ページフォルトハンドラは、アクセスされたページを別の場所にコピーし、書き込みをしようとしたプロセスに割り当てた上で、内容を書き換える
  4. 親プロセス、子プロセスそれぞれについて、共有が解除されたページに対応するページエントリを更新する

    • 書き込みをしたプロセス側のエントリは、新たに与えられた物理ページと紐づけた上で書き込みを許可する
    • もう一方のプロセス側のエントリについても、書き込みを許可する

書き込み操作時において初めてメモリのコピーが行われるため Copy on Write (CoW) と呼ばれる。

コピーオンライトを見る

実際に以下のようなコードでコピーオンライトを観測する。

  1. 100 MB のメモリを確保し、全てのページにアクセス
  2. システムのメモリ使用量を確認
  3. fork() システムコールを発行
  4. 親プロセスは子プロセスを待つ
  5. 子プロセスは以下の動作をする
    5.1. システムのメモリ使用量及びプロセスの仮想メモリ使用量、物理メモリ使用量、メジャー/マイナーフォルトの回数を表示
    5.2. 1. で確保した全てのページにアクセス
    5.3. システムのメモリ使用量及びプロセスの仮想メモリ使用量、物理メモリ使用量、メジャー/マイナーフォルトの回数を表示

実際のコードは以下。

cow.c
#include <sys/types.h>  
#include <sys/wait.h>  
#include <unistd.h>  
#include <sys/mman.h>  
#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <err.h>  
  
#define BUFFER_SIZE (100*1024*1024)  
#define PAGE_SIZE 4096  
#define COMMAND_SIZE 4096  
  
static char *p;  
static char command[COMMAND_SIZE];  
  
static void child_fn(char *p) {  
  printf("====== child ps info before memory access======\n");  
  fflush(stdout);  
  snprintf(command, COMMAND_SIZE, "ps -o pid,comm,vsz,rss,min_flt,maj_flt | grep ^%d", getpid());  
  system(command);  
  printf("====== free memory info before memory access======\n");  
  fflush(stdout);  
  system("free");  
  
  int i;  
  for (i = 0; i < BUFFER_SIZE; i += PAGE_SIZE)  
    p[i] = 0;  
  
  printf("====== child ps info after memory access======\n");  
  fflush(stdout);  
  system(command);  
  printf("====== free memory info after memory access======\n");  
  fflush(stdout);  
  system("free");  
  
  exit(EXIT_SUCCESS);  
    
}  
  
static void parent_fn(void) {  
  wait(NULL);  
  
  exit(EXIT_SUCCESS);  
}  
  
int main(void) {  
  char *buf;  
  p = malloc(BUFFER_SIZE);  
  if (p == NULL)  
    err(EXIT_FAILURE, "malloc() failed.");  
  
  int i;  
  for (i = 0; i < BUFFER_SIZE; i += PAGE_SIZE)  
    p[i] = 0;  
  
  printf("====== free memory info before fork======\n");  
  fflush(stdout);  
  system("free");  
  
  pid_t ret;  
  ret = fork();  
  if (ret == -1)  
    err(EXIT_FAILURE, "fork() failed.");  
  
  if (ret == 0)  
    child_fn(p);  
  else  
    parent_fn();  
  
  err(EXIT_FAILURE, "should not reach here.");  
}  

実際に実行すると以下の出力が得られる。

$ ./cow   
====== free memory info before fork======  
             total       used       free     shared    buffers     cached  
Mem:      32943184     444304   32498880         68      24128     174592  
-/+ buffers/cache:     245584   32697600  
Swap:            0          0          0  
====== child ps info before memory access======  
25769 cow             106588 102484    28      0  
====== free memory info before memory access======  
             total       used       free     shared    buffers     cached  
Mem:      32943184     444648   32498536         68      24128     174668  
-/+ buffers/cache:     245852   32697332  
Swap:            0          0          0  
====== child ps info after memory access======  
25769 cow             106588 103448 25636      0  
====== free memory info after memory access======  
             total       used       free     shared    buffers     cached  
Mem:      32943184     546560   32396624         68      24128     174668  
-/+ buffers/cache:     347764   32595420  
Swap:            0          0          0  

上記より、親プロセスのメモリ使用量は 100 MB を超えるにも関わらず、fork() システムコール実行直後は、メモリ使用量は数百 KB しか増えていないことが確認できる。
また、実際に子プロセスがメモリアクセスを行なった後は、システムのメモリ使用量及びプロセスのページフォルトの数が増えている。


 © 2023, Dealing with Ambiguity