コピーオンライトとは
実は fork() と execve() で使用した fork() システムコールは、仮想記憶のしくみを用いて高速化されている。
fork() システムコール発行時には、親プロセスのメモリを子プロセスに全てコピーするのではなく、ページテーブルのみをコピーする。また、その際にページテーブルエントリ内の書き込み権限を一時的に無効化する。
その後、ページの読み取り操作のみであれば、どちらのプロセスも共有された物理ページにアクセス可能。しかし、親プロセスもしくは子プロセスのどちらかがページのどこかを更新しようとすると、次の流れで共有を解除する。
- ページへの書き込みが許可されていないため、書き込み時に CPU 上でページフォルトが発生
- CPU がカーネルモードに移行し、カーネルのページフォルトハンドラがトリガーされる
- ページフォルトハンドラは、アクセスされたページを別の場所にコピーし、書き込みをしようとしたプロセスに割り当てた上で、内容を書き換える
-
親プロセス、子プロセスそれぞれについて、共有が解除されたページに対応するページエントリを更新する
- 書き込みをしたプロセス側のエントリは、新たに与えられた物理ページと紐づけた上で書き込みを許可する
- もう一方のプロセス側のエントリについても、書き込みを許可する
書き込み操作時において初めてメモリのコピーが行われるため Copy on Write (CoW) と呼ばれる。
コピーオンライトを見る
実際に以下のようなコードでコピーオンライトを観測する。
- 100 MB のメモリを確保し、全てのページにアクセス
- システムのメモリ使用量を確認
- fork() システムコールを発行
- 親プロセスは子プロセスを待つ
- 子プロセスは以下の動作をする
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 しか増えていないことが確認できる。
また、実際に子プロセスがメモリアクセスを行なった後は、システムのメモリ使用量及びプロセスのページフォルトの数が増えている。