メモリ管理
色々忘れているので基礎からやり直す
例えば Tomcat でヒープ領域のメモリ割当量を最小最大 2 GBに設定しておく。すると最初に 2GB 確保するのだが、実メモリが割り当てられるのは実際に使用した時
そのため、NewRelic 等で監視していると、使用されたタイミングでメモリ使用量が増え、その後はりついているように見える。
あとはメモリリークについてとか
---
プロセスは直接 物理アドレス にアクセスせず、仮想アドレス を介して間接的にアクセする
プロセスから物理メモリに直接アクセスする方法は存在しない
ページテーブル
仮想アドレス -> 物理アドレス の変換表
全てのメモリを ページ という単位で管理している
1つのページに対応するデータを ページテーブルエントリ という
ページサイズは、 CPU アーキテクチャ毎に異なる
x86_64 : 4K byte
仮想アドレスと物理アドレスのマッピングは 1:1 である必要はなく、仮想アドレスに必ずしも対応する物理アドレスがあるとは限らないし、別々の仮想アドレスが同一の物理アドレスを指している場合もある
そのため、複数のプロセスから同一の物理 RAM のページが参照される場合がある
ページフォールト
仮想アドレス空間の大きさは固定
範囲外の仮想アドレスにアクセスしようとすると、ページフォールト が発生する
CPU 上でページフォールト割り込みが発生する
実行中の命令は中断し、カーネル内のページフォールトハンドラが動く
プロセスは SIGSEGV シグナルを受け取り、強制終了する
物理アドレスにマッピングされていない仮想アドレスにアクセスしても発生する?
Linux では、メモリ確保のための関数として mmap を利用する。
code:c
pid_t pid;
pid = getpid();
// 最大 BUFFER_SIZE サイズの文字列を command に書き込む
snprintf(command, BUFFER_SIZE, "cat /proc/%d/maps", pid);
// cat /proc/<pid>/maps によりメモリマップを確認
fflush(stdout);
system(command);
void *new_memory;
// ページを addr から最大 len サイズを確保する
// MAP_FIXED フラグが指定されている場合は、指定されたアドレスからマッピングを開始し、
// マッピング先に既存のマッピングがあれば削除する
// MAP_FIXED フラグがない場合、あるいは addr が 0 の場合、マッピング済みの箇所を避けながらマッピングする
// (実装アルゴリズムに依存する)
// prot は protections の略で、確保したページへのアクセシビリティを指定する。
// ハードウェア制約上、PROT_WRITE が PROT_READ も含んでいる場合がある
// flags はマップされるオブジェクトの種別を指定する
//
// 新たに ALLOC_SIZE 分メモリを確保する
new_memory = mmap(NULL, ALLOC_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
// 再度メモリマップを確認
fflush(stdout);
system(command);
C 言語の標準ライブラリ内の malloc も、内部的には mmap を呼び出している。前者はバイト単位、後者はページ単位で利用できる。具体的には、glibc は事前にmmap システムコールにより大きなメモリ領域を獲得してプールしておき、malloc 発行時に必要な量をバイト単位で切り出してプログラムに返す。
mmap システムコール時には、以下の手順でメモリ割り当てが行われる。
1. カーネルが必要な領域を物理メモリ上に獲得する
2. カーネルがページテーブルを設定し仮想アドレス空間を物理アドレス空間に紐づける
仮想記憶により、以下が解決する。
メモリの断片化
物理メモリ上では断片化している領域を仮想アドレス空間上では大きな一つの領域として見せることができる
別用途のメモリへのアクセス
ページテーブルがプロセス毎に作られるので、干渉不可
マルチプロセスの扱いが困難
ページテーブルがプロセス毎に作られるので、干渉不可
File backed mapping, File backed pages
ファイルマップ
Linux には、ファイルの領域を仮想アドレス空間上にメモリマップする機能がある。
ストレージデバイス上のファイルを、物理メモリ上に書き出し、さらに仮想アドレス空間にマップする
仮想アドレス空間に対し、メモリアクセスと同様の方法でファイルアクセス、読み書き
最終的に、メモリ上で編集した内容がストレージデバイスに書き戻される
mmap でファイル内容をメモリに読み出し、それを書き換えるとストレージデバイス上のファイル内容も書き換わる。
code:c
int fd;
// O_RDWR: 読み書き可
fd = open("testfile", O_RDWR);
if (fd == -1)
err(EXIT_FAILURE, "open() failed.");
// ファイルマップ
char *file_contents;
file_contents = mmap(NULL, ALLOC_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (file_contents == (void *) -1) {
warn("mmap() failed.");
goto close_file;
}
// ファイルマップを上書きする
memcpy(file_contents, overwrite_data, strlen(overwrite_data));
メリット
複数回ファイルアクセスが必要な場合、ディスクI/Oを頻発させることを防げる
ファイル全体を読み取る必要はなく、その一部のみを読み取れば良くなる
デマンドページング
各プロセスが mmap システムコールでメモリを獲得はするものの、その中には無駄なメモリが存在する。
実行中使われなかった機能のためのコード領域やデータ領域
glibc が確保したメモリプールのうち、ユーザが malloc 関数で確保しなかった部分
上記のようなものが存在するので、デマンドページングという仕組みでは仮想アドレス空間内の各ページに対応する物理メモリは、当該ページに最初にアクセスした時に初めて割り当てる。
処理の流れは以下のようになる。この時ページフォールトが発生するが、プロセスはそれには気づかない。
1. プロセスが (未アクセスのページの) 仮想メモリにアクセス
2. CPU がページテーブルを参照し、仮想アドレスが物理アドレスと紐づいてないことを検出
3. CPU によりページフォールトが発生
4. カーネルのページフォールトハンドラが、以下を行う
仮想メモリに物理メモリを割り当て
ページテーブルを書き換える
5. ユーザモードに戻り、プロセス実行を継続する
mmap 関数等によるメモリの確保は 仮想メモリの確保、その後仮想メモリにアクセスし物理メモリに紐づけられるのを 物理メモリの確保 という。
以下のような動作をするコードを書けば、仮想/物理メモリの確保を実感できる。
1. 何もせずにキー入力を待つ (getchar())
2. キー入力後、100 MB を確保 (malloc())
100 MB 長の配列を確保
3. 何もせずキー入力を待つ
4. 1 秒ごとに、各ページに 10 MB ずつ書き込みを行う
ページサイズ (4096 byte) 毎に間を開け配列に書き込む
sar -r 1 の結果を見ると、以下のようになっている。
100 Byte 獲得
kbmemused に変化はなし
kbcommit が変化 (約 +100 MB)
10 MB ずつ利用
kbmemused が増加
kbmemfree が減少
プロセスが終了
各値がほぼ元に戻る = メモリ使用量がプロセス開始前の状態に戻る
code:tsv
05:20:55 PM kbmemfree kbmemused %memused kbbuffers kbcached kbcommit %commit kbactive
05:21:06 PM 188084 826428 81.46 98980 419404 505644 49.84 431132
05:21:07 PM 188084 826428 81.46 98980 419404 505644 49.84 431132
05:21:08 PM 188084 826428 81.46 98980 419404 505644 49.84 431132
05:21:09 PM 188084 826428 81.46 98980 419404 608384 59.97 431132 <- 100MB
05:21:10 PM 188084 826428 81.46 98980 419404 608384 59.97 431132
05:21:11 PM 188084 826428 81.46 98980 419404 608384 59.97 431132
05:21:12 PM 188084 826428 81.46 98980 419404 608384 59.97 431152
05:21:13 PM 188084 826428 81.46 98980 419404 608384 59.97 431152
05:21:14 PM 188084 826428 81.46 98980 419404 608384 59.97 431152
05:21:15 PM 188084 826428 81.46 98980 419404 608384 59.97 431152
05:21:16 PM 188100 826412 81.46 98980 419404 608384 59.97 431152
05:21:17 PM 188100 826412 81.46 98980 419404 608384 59.97 431152
05:21:18 PM 188100 826412 81.46 98980 419404 608384 59.97 431152
05:21:19 PM 177488 837024 82.51 98980 419404 608384 59.97 435576 <- 10 MBずつ
05:21:20 PM 167244 847268 83.51 98980 419404 608384 59.97 435576
05:21:21 PM 157000 857512 84.52 98980 419404 608384 59.97 435576
05:21:22 PM 146744 867768 85.54 98980 419404 608384 59.97 464248
05:21:23 PM 136892 877620 86.51 98980 419404 608384 59.97 482296
05:21:24 PM 126632 887880 87.52 98980 419404 608384 59.97 492536
05:21:25 PM 116372 898140 88.53 98980 419404 608384 59.97 502776
05:21:26 PM 106112 908400 89.54 98980 419404 608384 59.97 513004
05:21:27 PM 95852 918660 90.55 98980 419404 608384 59.97 523256
05:21:28 PM 85536 928976 91.57 98980 419404 608384 59.97 533532
05:21:29 PM 85536 928976 91.57 98980 419404 608384 59.97 533536
05:21:30 PM 85536 928976 91.57 98980 419404 608384 59.97 533536
05:21:31 PM 85412 929100 91.58 98980 419404 608384 59.97 533536
05:21:32 PM 85412 929100 91.58 98980 419404 608384 59.97 533536
05:21:33 PM 187844 826668 81.48 98980 419404 505644 49.84 431068 <- 終了
05:21:34 PM 187844 826668 81.48 98980 419404 505644 49.84 431068
05:21:35 PM 187844 826668 81.48 98980 419404 505644 49.84 431068
05:21:36 PM 187828 826684 81.49 98980 419404 505644 49.84 431068
05:21:37 PM 187844 826668 81.48 98980 419404 505644 49.84 431068
05:21:38 PM 187844 826668 81.48 98980 419404 505644 49.84 431068
table:sar の出力カラム (一部)
カラム名 概要
kbmemfree Amount of free memory available in kilobytes.
kbmemused Amount of used memory in kilobytes. This does not take into account memory used by the kernel itself.
memused Percentage of used memory.
kbcommit Amount of memory in kilobytes needed for current workload. This is an estimate of how much RAM/swap is needed to guarantee that there never is out of memory.
commit Percentage of memory needed for current workload in relation to the total amount of memory (RAM+swap). This number may be greater than 100% because the kernel usually overcommits memory.
kbactive Amount of active memory in kilobytes (memory that has been used more recently and usually not reclaimed unless absolutely necessary).
ps の結果を見ると、以下のようになっている。
100 Byte 獲得
vsz (仮想メモリ) が 100MB 増加
rss (実メモリ) は変化なし
ページフォールトほぼ発生せず
10 MB ずつ利用
vsz (仮想メモリ) は変化なし
rss (実メモリ) が増加
ページフォールトが毎回発生
code:tsv
vsz rss maj_flt min_flt
4352 680 0 77
4352 680 0 77
4352 680 0 77
4352 680 0 77
4352 680 0 77
106756 680 0 78 <- 100 MB 割り当て
106756 680 0 78
106756 680 0 78
106756 680 0 78
106756 680 0 78
106756 13424 0 533 <- 10 MB ずつ利用
106756 23664 0 538
106756 33904 0 543
106756 44144 0 548
106756 52436 0 1637
106756 62732 0 4197
106756 73028 0 6757
106756 83324 0 9317
106756 93620 0 11877
106756 103652 0 14436
106756 103652 0 14436
table:psの出力結果のカラム名 (一部)
カラム名 概要
vsz virtual memory size of the process in KiB (1024-byte units). Device mappings are currently excluded; this is subject to change. (alias vsize).
rss resident set size, the non-swapped physical memory that a task has used (in kiloBytes). (alias rssize, rsz)
maj_flt The number of major page faults that have occurred with this process.
min_flt The number of minor page faults that have occurred with this process.
コピーオンライト (CoW)
参考
武内覚 (2018-03-08). 試して理解Linuxのしくみ. 274p.