デマンドページング

May 04, 2018

デマンドページングとは

メモリアロケーション の項では簡単のために、プロセスの生成時、またはその後の mmap() システムコールによってプロセスにメモリを割り当てる際の手順は以下のものであると記載した。

  1. カーネルが必要な領域を物理メモリ上に確保
  2. カーネルがページテーブルを設定し、仮想アドレスと物理アドレスの紐付けを行う

しかし、この方法の場合は、メモリ領域確保後、実際に使用されるまで時間がかかる、もしくは全く使用されない領域が存在するため、メモリの無駄遣いにつながる。
具体的には以下のような場合である。

  • プログラム中において、実行中に使われなかった機能のためのコード領域及びデータ領域
  • glibc が確保したメモリプールのうち、ユーザーが malloc() で確保しなかった部分

これらの状況におけるメモリの無駄遣いを防ぐために、Linux にはデマンドページングという機能が備わっている。

デマンドページングでは、“実際に当該ページにアクセスが行われた時にのみ” 物理メモリの割り当てを行う。
つまり、プロセスにはメモリを割り当てているように見えるが、実際に物理メモリの割り当ては行われておらず、アクセスがあった際に初めて物理メモリを割り当てる、という仕組みである。

デマンドページングを考慮すると、プロセス開始時の流れは以下のようになる。

  1. プログラムがエントリポイントにアクセス
  2. CPU がページテーブルを参照し、エントリポイントが属するページに対応する仮想アドレスに紐づく物理アドレスが存在しないことを検知
  3. CPU においてページフォルトが発生
  4. カーネルのページフォルトハンドラが、物理メモリを割り当て、ページテーブルを更新
  5. ユーザーモードに戻り、プロセスが実行を継続する

なお、プロセスは自分自信の実行中にページフォルトが発生したことを認識しない。

デマンドページングを感じる

ここでは実際にデマンドページングが発生する様子を観測する。
確認項目は以下。

  • メモリを確保すると、仮想メモリの使用量は増加するが、物理メモリの使用量は増加しない
  • 確保したメモリのアクセス時に初めて物理メモリの使用量が増加し、その際にページフォルトが発生する

このために、以下のようなコードを作成する。

  1. メモリ獲得前である旨を伝えるメッセージを出力し、Enter キー入力を待つ
  2. 100 MB のメモリを確保する
  3. メモリ獲得後であることを伝えるメッセージを出力し、Enter キー入力を待つ
  4. 確保したメモリの最初から最後まで 1 ページずつアクセス

実際のコードは以下。

demand-paging.c
#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(&amp;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(&amp;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 &amp;&amp; i % (BUFFER_SIZE/NCYCLE) == 0) {  
      t = time(NULL);  
      s = ctime(&amp;t);  
      printf("%.*s: touched %d MB.\n", (int)(strlen(s) - 1), s, i / (1024*1024));  
      sleep(1);  
    }  
  }  
  t = time(NULL);  
  s = ctime(&amp;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 でメモリ確保を行う。

vsz-rss.sh
#!/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  

 © 2023, Dealing with Ambiguity