デマンドページングとは
メモリアロケーション の項では簡単のために、プロセスの生成時、またはその後の mmap() システムコールによってプロセスにメモリを割り当てる際の手順は以下のものであると記載した。
- カーネルが必要な領域を物理メモリ上に確保
- カーネルがページテーブルを設定し、仮想アドレスと物理アドレスの紐付けを行う
しかし、この方法の場合は、メモリ領域確保後、実際に使用されるまで時間がかかる、もしくは全く使用されない領域が存在するため、メモリの無駄遣いにつながる。
具体的には以下のような場合である。
- プログラム中において、実行中に使われなかった機能のためのコード領域及びデータ領域
- glibc が確保したメモリプールのうち、ユーザーが malloc() で確保しなかった部分
これらの状況におけるメモリの無駄遣いを防ぐために、Linux にはデマンドページングという機能が備わっている。
デマンドページングでは、“実際に当該ページにアクセスが行われた時にのみ” 物理メモリの割り当てを行う。
つまり、プロセスにはメモリを割り当てているように見えるが、実際に物理メモリの割り当ては行われておらず、アクセスがあった際に初めて物理メモリを割り当てる、という仕組みである。
デマンドページングを考慮すると、プロセス開始時の流れは以下のようになる。
- プログラムがエントリポイントにアクセス
- CPU がページテーブルを参照し、エントリポイントが属するページに対応する仮想アドレスに紐づく物理アドレスが存在しないことを検知
- CPU においてページフォルトが発生
- カーネルのページフォルトハンドラが、物理メモリを割り当て、ページテーブルを更新
- ユーザーモードに戻り、プロセスが実行を継続する
なお、プロセスは自分自信の実行中にページフォルトが発生したことを認識しない。
デマンドページングを感じる
ここでは実際にデマンドページングが発生する様子を観測する。
確認項目は以下。
- メモリを確保すると、仮想メモリの使用量は増加するが、物理メモリの使用量は増加しない
- 確保したメモリのアクセス時に初めて物理メモリの使用量が増加し、その際にページフォルトが発生する
このために、以下のようなコードを作成する。
- メモリ獲得前である旨を伝えるメッセージを出力し、Enter キー入力を待つ
- 100 MB のメモリを確保する
- メモリ獲得後であることを伝えるメッセージを出力し、Enter キー入力を待つ
- 確保したメモリの最初から最後まで 1 ページずつアクセス
実際のコードは以下。
#include <unistd.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <err.h>
#define BUFFER_SIZE (100*1024*1024)
#define NCYCLE 10
#define PAGE_SIZE 4096
int main(void) {
char *p;
time_t t;
char *s;
t = time(NULL);
s = ctime(&t);
printf("%.*s: before memory allocation. please press Enter\n", (int)(strlen(s) - 1), s);
getchar();
p = malloc(BUFFER_SIZE);
if (p == NULL)
err(EXIT_FAILURE, "malloc() failed.");
t = time(NULL);
s = ctime(&t);
printf("%.*s: allocated %d MB. please press Enter\n", (int)(strlen(s) - 1), s, BUFFER_SIZE/(1024*1024));
getchar();
int i;
for (i = 0; i < BUFFER_SIZE; i += PAGE_SIZE) {
p[i] = 0;
int cycle = i / (BUFFER_SIZE/NCYCLE);
if (cycle != 0 && i % (BUFFER_SIZE/NCYCLE) == 0) {
t = time(NULL);
s = ctime(&t);
printf("%.*s: touched %d MB.\n", (int)(strlen(s) - 1), s, i / (1024*1024));
sleep(1);
}
}
t = time(NULL);
s = ctime(&t);
printf("%.*s: touched %d MB. please press Enter\n", (int)(strlen(s) - 1), s, BUFFER_SIZE / (1024*1024));
getchar();
exit(EXIT_SUCCESS);
}
システムにおける統計遷移
別端末で sar コマンドで統計を取りながら、実行する。
$ ./demand-paging
Fri May 4 14:21:20 2018: before memory allocation. please press Enter
Fri May 4 14:21:32 2018: allocated 100 MB. please press Enter
Fri May 4 14:21:38 2018: touched 10 MB.
Fri May 4 14:21:39 2018: touched 20 MB.
Fri May 4 14:21:40 2018: touched 30 MB.
Fri May 4 14:21:41 2018: touched 40 MB.
Fri May 4 14:21:42 2018: touched 50 MB.
Fri May 4 14:21:43 2018: touched 60 MB.
Fri May 4 14:21:44 2018: touched 70 MB.
Fri May 4 14:21:45 2018: touched 80 MB.
Fri May 4 14:21:46 2018: touched 90 MB.
Fri May 4 14:21:47 2018: touched 100 MB. please press Enter
物理メモリの遷移を見ると、kbmemused は確かにメモリアクセスが行われるまでは減っていないことがわかる。
$ sar -r 1
Linux 4.14.26-46.32.amzn1.x86_64 (ip-10-1-11-92) 05/04/2018 _x86_64_ (8 CPU)
02:21:29 PM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit
02:21:30 PM 32601480 341704 1.04 21992 174128 253068 0.77
02:21:31 PM 32601480 341704 1.04 22000 174120 253068 0.77
02:21:32 PM 32601480 341704 1.04 22000 174128 365028 1.11 <- メモリ確保
02:21:33 PM 32601480 341704 1.04 22000 174128 365028 1.11
02:21:34 PM 32601480 341704 1.04 22000 174128 365028 1.11
02:21:35 PM 32601480 341704 1.04 22000 174128 365028 1.11
02:21:36 PM 32601356 341828 1.04 22000 174128 365028 1.11
02:21:37 PM 32601356 341828 1.04 22000 174128 365028 1.11
02:21:38 PM 32591684 351500 1.07 22000 174128 365028 1.11 <- メモリアクセス開始
02:21:39 PM 32581144 362040 1.10 22000 174128 365028 1.11
02:21:40 PM 32570976 372208 1.13 22000 174128 365028 1.11
02:21:41 PM 32560684 382500 1.16 22000 174128 365028 1.11
02:21:42 PM 32550392 392792 1.19 22000 174128 365028 1.11
02:21:43 PM 32540224 402960 1.22 22000 174128 365028 1.11
02:21:44 PM 32529808 413376 1.25 22000 174128 365028 1.11
02:21:45 PM 32519640 423544 1.29 22000 174128 365028 1.11
02:21:46 PM 32509348 433836 1.32 22000 174128 365028 1.11
02:21:47 PM 32499056 444128 1.35 22000 174128 365028 1.11 <- 全てのページにアクセス完了
02:21:48 PM 32499056 444128 1.35 22000 174128 365028 1.11
02:21:49 PM 32499056 444128 1.35 22000 174128 365028 1.11
02:21:50 PM 32499056 444128 1.35 22000 174128 365028 1.11
02:21:51 PM 32498932 444252 1.35 22000 174128 365028 1.11
02:21:52 PM 32601364 341820 1.04 22000 174128 262276 0.80 <- プロセス終了
次にページイン/アウトを観測すると、実際のメモリアクセス時にページフォルトが発生していることが確認できる。
$ sar -B 1
Linux 4.14.26-46.32.amzn1.x86_64 (ip-10-1-11-92) 05/04/2018 _x86_64_ (8 CPU)
02:27:55 PM pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
02:27:56 PM 0.00 12.00 33.00 0.00 35.00 0.00 0.00 0.00 0.00
02:27:57 PM 0.00 0.00 35.00 0.00 36.00 0.00 0.00 0.00 0.00
02:27:58 PM 0.00 0.00 33.00 0.00 35.00 0.00 0.00 0.00 0.00
02:27:59 PM 0.00 0.00 39.00 0.00 44.00 0.00 0.00 0.00 0.00
02:28:00 PM 0.00 0.00 31.00 0.00 35.00 0.00 0.00 0.00 0.00
02:28:01 PM 0.00 0.00 2597.00 0.00 47.00 0.00 0.00 0.00 0.00 <- メモリアクセス開始
02:28:02 PM 0.00 0.00 2593.00 0.00 36.00 0.00 0.00 0.00 0.00
02:28:03 PM 0.00 0.00 2591.00 0.00 36.00 0.00 0.00 0.00 0.00
02:28:04 PM 0.00 0.00 2593.00 0.00 35.00 0.00 0.00 0.00 0.00
02:28:05 PM 0.00 0.00 2591.00 0.00 35.00 0.00 0.00 0.00 0.00
02:28:06 PM 0.00 0.00 2591.00 0.00 35.00 0.00 0.00 0.00 0.00
02:28:07 PM 0.00 0.00 2591.00 0.00 35.00 0.00 0.00 0.00 0.00
02:28:08 PM 0.00 0.00 2591.00 0.00 35.00 0.00 0.00 0.00 0.00
02:28:09 PM 0.00 0.00 2599.00 0.00 43.00 0.00 0.00 0.00 0.00
02:28:10 PM 0.00 0.00 2590.00 0.00 36.00 0.00 0.00 0.00 0.00
02:28:11 PM 0.00 0.00 33.00 0.00 36.00 0.00 0.00 0.00 0.00 <- プロセス終了
プロセスにおける統計遷移
プロセス毎のメモリ統計を取るために以下のスクリプトの実行中に demand-paging.c でメモリ確保を行う。
#!/bin/bash
while true; do
DATA=$(data | tr -d '\n')
INFO=$(ps -eo pid,comm,vsz,rss,maj_flt,min_flt | grep demand-paging | grep -v grep)
if [ -z "$INFO" ] ; then
echo "$DATE: target process seems to be finished"
break
fi
echo "${DATE}: ${INFO}"
sleep 1
done
上記のスクリプトでは、プロセス毎の仮想メモリの量、確保済みの物理メモリ量、プロセス生成時からのページフォルトの総量が確認できる。
実際に実行すると以下のように、メモリ割り当て直後は VSZ のみが増加していることが確認できる。
$ sh vsz-rss.sh
Fri May 4 14:36:58 UTC 2018: 25224 demand-paging 4316 1076 0 80
Fri May 4 14:36:59 UTC 2018: 25224 demand-paging 4316 1076 0 80
Fri May 4 14:37:00 UTC 2018: 25224 demand-paging 4316 1076 0 80
Fri May 4 14:37:01 UTC 2018: 25224 demand-paging 106720 1076 0 82 <- メモリ割り当て
Fri May 4 14:37:02 UTC 2018: 25224 demand-paging 106720 1076 0 82
Fri May 4 14:37:03 UTC 2018: 25224 demand-paging 106720 11220 0 2643 <- メモリアクセス開始
Fri May 4 14:37:04 UTC 2018: 25224 demand-paging 106720 21576 0 5203
Fri May 4 14:37:05 UTC 2018: 25224 demand-paging 106720 31872 0 7763
Fri May 4 14:37:06 UTC 2018: 25224 demand-paging 106720 42168 0 10323
Fri May 4 14:37:07 UTC 2018: 25224 demand-paging 106720 52464 0 12883
Fri May 4 14:37:08 UTC 2018: 25224 demand-paging 106720 62496 0 15443
Fri May 4 14:37:09 UTC 2018: 25224 demand-paging 106720 72792 0 18003
Fri May 4 14:37:10 UTC 2018: 25224 demand-paging 106720 83088 0 20563
Fri May 4 14:37:11 UTC 2018: 25224 demand-paging 106720 93384 0 23123
Fri May 4 14:37:12 UTC 2018: 25224 demand-paging 106720 103416 0 25682
Fri May 4 14:37:13 UTC 2018: 25224 demand-paging 106720 103416 0 25682
Fri May 4 14:37:14 UTC 2018: 25224 demand-paging 106720 103416 0 25682
Fri May 4 14:37:15 UTC 2018: 25224 demand-paging 106720 103416 0 25682
Fri May 4 14:37:16 UTC 2018: 25224 demand-paging 106720 103416 0 25682
Fri May 4 14:37:17 UTC 2018: target process seems to be finished