CPUと同様にメモリもスマホやパソコンに搭載されており、ほぼ毎日利用しているかと思います。
そこでアンドロイドなどを含むLinuxでメモリがどのように管理されているか簡単にご紹介していきます。
メモリアロケータ
下の図は、ユーザーレベルおよびカーネルレベルのソフトウェアで一般的に使用されるメモリ割り当てシステムを示しています。メモリ割り当てにlibcという標準 C ライブラリを使用するプロセスの場合、メモリはヒープと呼ばれるプロセスの仮想アドレス空間の動的セグメントに格納されます。
libcは、malloc()やfree()などのメモリ割り当て用の関数を提供しています。メモリが解放されると、libcはその場所を追跡し、その場所情報を使用して後続のmalloc()を実行できます。 libcは、使用可能なメモリがない場合にのみ、ヒープのサイズを拡張します。これはすべて仮想メモリであり、実際の物理メモリではないため、libcがヒープのサイズを縮小することはありません。
カーネルとプロセッサは、仮想メモリを物理メモリにマッピングする役割を果たします。効率を上げるために、メモリマッピングはページと呼ばれるメモリのグループで作成されます。
4キロバイトが一般的ですが、ほとんどのプロセッサはより大きなサイズもサポートしています。これはLinuxでは巨大な(Huge)ページと呼んでいます。カーネルは、効率のために各DRAMグループとCPUに対して維持する、独自の空きリストからの物理メモリページ要求を処理できます。カーネル独自のソフトウェアも、通常はスラブアロケータなどのカーネルアロケータを介して、これらの空きリストのメモリも消費します。
一般的なユーザーメモリページのライフサイクルを図2に示しています。また、次のステップをたどります。
1.アプリケーションは、メモリの割り当て要求(libc malloc()など)から始まります。
2.割り当ては、独自の空きリストからメモリ要求を処理するか、対応するために仮想メモリを拡張する必要がある場合があります。割り当てライブラリに応じて、次のいずれかになります。
a. brk()syscallを呼び出し、ヒープメモリを割り当てに使用して、ヒープのサイズを拡張します。
b. mmap()システムコールを介して新しいメモリセグメントを作成します。
3.しばらくして、アプリケーションは、ストアおよびロード命令を介して割り当てられたメモリ範囲を使用しようとします。これには、仮想アドレスから物理アドレスへの変換のためにプロセッサメモリ管理ユニット(MMU)を呼び出すことが含まれます。物理メモリがマップされていない仮想アドレス空間上のページにアクセスしたときに、ページフォールトと呼ばれるMMUエラーが発生します。
4.ページフォールトはカーネルによって処理されます。カーネルは、物理メモリの空きリストから仮想メモリへのマッピングを確立し、後で検索するためにこのマッピングをMMUに通知します。プロセスで使用されている物理メモリの量は、常駐セットサイズ(RSS)と呼ばれます。
5.システムのメモリ需要が多すぎると、カーネルページアウトデーモン(kswapd)が解放するメモリページを探す場合があります。 3種類のメモリのいずれかを解放します。
a. ディスクから読み取られ、変更されていないファイルシステムページ:これらはすぐに解放され、必要に応じて簡単に再読み取りできます。これらのページは、アプリケーションで実行可能なテキスト、データ、およびファイルシステムのメタデータです。
b. 変更されたファイルシステムページ:これらは「ダーティ(Dirty)」であり、解放する前にディスクに書き込む必要があります。
c. アプリケーションメモリのページ:ファイルの出所がないため、匿名メモリと呼ばれます。スワップデバイスが使用されている場合、これらは最初にスワップデバイスに保存することで解放できます。このスワップデバイスへのページの書き込みは、スワッピングと呼ばれます。
ページアウト デーモン
ページアウトデーモン(kswapd)は定期的にアクティブ化され、解放するメモリを検索して非アクティブページとアクティブページのLRU(Least Recently Used)リストをスキャンします。図3に示すように、空きメモリが低いしきい値(low pages)を超えるとウェイクアップされ、高いしきい値(high pages)を超えるとスリープ状態に戻ります。
LRUとは、「最も過去に使用された」の意で、過去に参照されてから最も時間が経ったデータを破棄し、新規のデータと置換するために利用されます。
kswapdはバックグラウンドでページアウトを調整します。
調整可能な最小ページしきい値(min pages)を超え、kswapdが十分な速度でメモリを解放できない場合は、直接再利用されます。これは、割り当てを満たすためにメモリを解放するフォアグラウンドモードです。このモードでは、割り当てはブロック(ストール)し、ページが解放されるのを同期的に待機します。
直接再利用は、カーネルモジュールシュリンク関数を呼び出すことができます。これらは、カーネルスラブキャッシュを含むキャッシュに保持されていた可能性のあるメモリを解放します。
基本ツール
基本的なツールを使ってメモリに関する調査をはじめてみましょう。
dmesg
Out-of-memory killerなどの情報はシステムログに記載されるので、はじめにdmesgを実行して結果をチェックするといいでしょう。
swapon
スワップデバイスの設定やどれぐらい利用されているか確認できます。
次の例では、2Gバイトのスワップパーティションがあり、そのうち169.3Mバイト利用していることが確認できます。
$ swapon
NAME TYPE SIZE USED PRIO
/dev/dm-1 partition 2G 169.3M -2
このように使用されている場合には、vmstatのsiやsoのコラムでもアクティブな情報を確認できます。
$ vmstat
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 171008 236324 1048 618388 1 7 133 21 48 66 1 0 99 0 0
slabtop
slabtopは、リアルタイムに詳細なカーネル slab キャッシュ情報を表示します。
-s cオプションによって、キャッシュのサイズでソートすることができます。
ここでは、inode_cacheが14MByteの利用が確認できます。
# slabtop -s c
Active / Total Objects (% used) : 1662948 / 1706740 (97.4%)
Active / Total Slabs (% used) : 24176 / 24176 (100.0%)
Active / Total Caches (% used) : 101 / 150 (67.3%)
Active / Total Size (% used) : 139079.61K / 148925.00K (93.4%)
Minimum / Average / Maximum Object : 0.01K / 0.09K / 14.75K
OBJS ACTIVE USE OBJ SIZE SLABS OBJ/SLAB CACHE SIZE NAME
21624 20688 95% 0.64K 1802 12 14416K inode_cache
55590 54806 98% 0.23K 3270 17 13080K vm_area_struct
11145 8855 79% 1.06K 743 15 11888K xfs_inode
50232 41131 81% 0.19K 2392 21 9568K dentry
297728 292810 98% 0.03K 2326 128 9304K kmalloc-32
392530 392530 100% 0.02K 2309 170 9236K avtab_node
2088 2075 99% 4.00K 261 8 8352K kmalloc-4k
...
free
freeコマンドでシステム全体のメモリ使用量を確認できます。とくに、利用されていないfreeコラムより、利用可能なavailableコラムを確認するといいでしょう。また、-wオプションをつけると、buffersとcacheを分て表示できます。
$free -m
total used free shared buff/cache available
Mem: 1818 983 232 8 602 667
Swap: 2047 167 1880
$ free -m -w
total used free shared buffers cache available
Mem: 1818 982 230 8 1 603 667
Swap: 2047 167 1880
buffersは、カーネルバッファに利用されているメモリです。
cacheは、ページキャッシュやスラブに利用されているメモリです。
buffersとcacheの違いは、何でしょうか?
ページキャッシュはファイルシステムに対するキャッシュであり、ファイル単位でアクセスするときに使用されるキャッシュです。例えば、ファイルへデータを書き込んだときは、ページキャッシュにデータが残されるため、次回の読み込み時には HDD にアクセスすることなくデータを利用できます。
もうひとつのバッファキャッシュはブロックデバイスを直接アクセスするときに使用されるキャッシュです。
ps
プロセス毎のメモリ使用量を確認できます。とくに、つぎの3つに注目しましょう。
%MEM | このプロセスによって利用されている物理メモリのパーセント |
VSZ | 仮想メモリのサイズ(kiloBytes) |
RSS | このプロセスによって使用されている物理メモリ(kiloBytes) |
$ ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.5 245956 10040 ? Ss 11:38 0:02 /usr/lib/systemd/systemd --switched-root --system --deserialize 17
root 2 0.0 0.0 0 0 ? S 11:38 0:00 [kthreadd]
root 3 0.0 0.0 0 0 ? I< 11:38 0:00 [rcu_gp]
root 4 0.0 0.0 0 0 ? I< 11:38 0:00 [rcu_par_gp]
....
pmap
データやプログラムがどこのメモリのアドレス空間に格納されているかを知ることができるツールです。-xオプションをつけることによって、Dirtyページという、メモリ上にあってこれからディスクに書き込まれるべきデータの容量(キロバイト)を表示できます。
# pmap -x 3027
3027: sshd: root@pts/1
Address Kbytes RSS Dirty Mode Mapping
000055d928721000 820 656 0 r-x-- sshd
000055d9289ee000 16 16 16 r---- sshd
000055d9289f2000 4 4 4 rw--- sshd
000055d9289f3000 16 16 16 rw--- [ anon ]
000055d929f65000 396 356 356 rw--- [ anon ]
00007fb23a9b2000 840 484 0 r-x-- libnss_systemd.so.2
00007fb23aa84000 2048 0 0 ----- libnss_systemd.so.2
...
vmstat
1秒ごとにシステムのメモリやスワップの使用状況を確認できます。siはスワップインで、soはスワップアウトです。スワップインとは、スワップアウトによって一時的にハードディスクへ退避していたデータをメモリ上に戻すことです。スワップアウトとは、実メモリの空き領域が不足した場合に、メモリ上のデータを一時的にハードディスクへ退避させることです。
# vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
r b swpd free buff cache si so bi bo in cs us sy id wa st
2 0 171008 227108 1048 623424 1 6 117 18 46 64 1 0 99 0 0
0 0 171008 226652 1048 623424 0 0 0 0 133 137 0 1 99 0 0
0 0 171008 226652 1048 623424 0 0 0 0 59 100 0 0 100 0 0
0 0 171008 226652 1048 623424 0 0 0 0 75 97 0 1 100 0 0
0 0 171008 226652 1048 623424 0 0 0 0 59 93 0 0 100 0 0
^C
sar
sarを使ってCPU、メモリ、I/O、ネットワークの通信状況など確認できますが、ここでは-Bオプションを使って、ページングについて調べてみます。
$ sar -B 1
Linux 5.13.6-200.fc34.x86_64 (localhost.localdomain) 10/06/21 _x86_64_ (8 CPU)
17:15:01 pgpgin/s pgpgout/s fault/s majflt/s pgfree/s pgscank/s pgscand/s pgsteal/s %vmeff
17:15:02 0.00 0.00 1337.00 0.00 2759.00 0.00 0.00 0.00 0.00
17:15:03 0.00 0.00 97.00 0.00 2808.00 0.00 0.00 0.00 0.00
17:15:04 0.00 0.00 370.00 0.00 3197.00 0.00 0.00 0.00 0.00
17:15:05 0.00 92.00 671.00 0.00 1307.00 0.00 0.00 0.00 0.00
ページフォルトが1337回/秒発生していることがわかります。ページフォールト (page fault) とは、プログラム(プロセス)がアクセスしようとした仮想メモリ領域(ページ)が物理メモリ上に無く、ストレージに退避していることが分かったときに発生する例外あるいは割り込み処理です。
pgscank/s: 1秒あたりにkswapdデーモンによってスキャンされたページ数
pgscand/s: 1秒あたりに直接スキャンされたページ数
上記2つ項目が0なので、メモリがいっぱいいっぱい使われていないことがわかります。
valgrind
valgrindを利用することによって、メモリリークを発見することができます。
プログラムのコンパイル時に-gオプションをつけることにより、何行目でメモリ解放に問題があるのかを指摘することができます。例えば、つぎのようなプログラムがあるとします。
#include <stdio.h>
#include <stdlib.h>
void get_mem()
{
char *ptr = malloc(7);
}
int main(void)
{
char *ptr1, *ptr2;
int i;
ptr1 = (char *) malloc(10);
ptr2 = (char *) malloc(10);
ptr2 = ptr1;
free(ptr2);
free(ptr1);
for(i=0; i<2; i++) {
get_mem();
}
}
このプログラムにvalgrindを使って、どこが、どれだけリークしているのか確認してみます。
# gcc -g mem_leak.c -o mem_leak
# valgrind --tool=memcheck ./mem_leak
==19909== Memcheck, a memory error detector
==19909== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==19909== Using Valgrind-3.16.0 and LibVEX; rerun with -h for copyright info
==19909== Command: ./mem_leak
==19909==
==19909== Invalid free() / delete / delete[] / realloc()
==19909== at 0x4C3210C: free (vg_replace_malloc.c:538)
==19909== by 0x400632: main (mem_leak.c:17)
==19909== Address 0x5200040 is 0 bytes inside a block of size 10 free'd
==19909== at 0x4C3210C: free (vg_replace_malloc.c:538)
==19909== by 0x400626: main (mem_leak.c:16)
==19909== Block was alloc'd at
==19909== at 0x4C30F0B: malloc (vg_replace_malloc.c:307)
==19909== by 0x400600: main (mem_leak.c:13)
==19909==
==19909==
==19909== HEAP SUMMARY:
==19909== in use at exit: 24 bytes in 3 blocks
==19909== total heap usage: 4 allocs, 2 frees, 34 bytes allocated
==19909==
==19909== LEAK SUMMARY:
==19909== definitely lost: 24 bytes in 3 blocks
==19909== indirectly lost: 0 bytes in 0 blocks
==19909== possibly lost: 0 bytes in 0 blocks
==19909== still reachable: 0 bytes in 0 blocks
==19909== suppressed: 0 bytes in 0 blocks
==19909== Rerun with --leak-check=full to see details of leaked memory
==19909==
==19909== For lists of detected and suppressed errors, rerun with: -s
==19909== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
24バイトメモリリークが発生していることがわかります。
具体的には、15行目の「ptr2 = ptr1;
」によって、ptr2が指していたアドレスが失われ、10バイトのメモリリークが発生します。また、20行目のget_mem()では、7バイトの確保が2回されますが、いづれも解放されないため14バイトのメモリリークが発生します。つまり、合計で24バイトのメモリリークが発生します。
perf
perfを利用することによってハードウェアベースの監視をすることができます。
主に2つのモードが利用されます。1つは、カウンティングと呼ばれるイベントの発生回数を数えるモードです。もう1つは、サンプリングと呼ばれ、ある一定回数のイベント発生が起こるたびにサンプルを記録するモードです。
ここでは、1秒ごとにLLC(Last Level Cache)と呼ばれるCPUから見て一番遠いキャッシュメモリのロードとミスを観測してみます。インテルのチップですと、通常L3キャッシュを指します。
# perf stat -e LLC-loads,LLC-load-misses -a -I 1000
# time counts unit events
1.001025442 47,772,400 LLC-loads
1.001025442 3,060,178 LLC-load-misses # 6.41% of all LL-cache accesses
2.001924838 46,257,102 LLC-loads
2.001924838 3,392,702 LLC-load-misses # 7.33% of all LL-cache accesses
上記の例ですと、47,772,400回LLCからロードがあり、そのうち3,060,178回LLCのロードミスが発生しています。つまり、約6.41%メインメモリにアクセスがあったこと指します。
つぎに、サンプリングモードでL1やL3のキャッシュミスを監察してみましょう。
# perf record -e L1-dcache-load-misses -a
^C[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 2.091 MB perf.data (8642 samples) ]
# perf report -n --stdio
# To display the perf.data header info, please use --header/--header-only options.
#
#
# Total Lost Samples: 0
#
# Samples: 8K of event 'L1-dcache-load-misses'
# Event count (approx.): 38987666
#
# Overhead Samples Command Shared Object Symbol >
# ........ ............ ............... .................................... ....................................................>
#
12.83% 79 sssd_kcm libc-2.33.so [.] __memset_avx2_erms
2.21% 288 swapper [kernel.kallsyms] [k] mwait_idle_with_hints.constprop.0
1.26% 85 pipewire-pulse libspa-audioconvert.so [.] 0x0000000000057c79
1.09% 74 pipewire-pulse libspa-audioconvert.so [.] 0x0000000000057c86
1.02% 125 swapper [kernel.kallsyms] [k] psi_group_change
0.79% 48 gnome-shell libglib-2.0.so.0.6800.2 [.] g_main_context_check
0.77% 96 swapper [kernel.kallsyms] [k] __schedule
0.72% 46 gnome-shell libglib-2.0.so.0.6800.2 [.] g_source_ref
...
# perf record -e LLC-load-misses -a
^C[ perf record: Woken up 1 times to write data ]
[ perf record: Captured and wrote 1.956 MB perf.data (6170 samples) ]
# perf report -n --stdio
# To display the perf.data header info, please use --header/--header-only options.
#
#
# Total Lost Samples: 0
#
# Samples: 6K of event 'LLC-load-misses'
# Event count (approx.): 7799539
#
# Overhead Samples Command Shared Object Symbol >
# ........ ............ ............... .................................... ....................................................>
#
3.31% 120 pipewire-pulse libspa-audioconvert.so [.] 0x0000000000057c86
2.57% 79 pipewire-pulse libspa-audioconvert.so [.] 0x0000000000057c79
1.67% 81 gnome-shell libglib-2.0.so.0.6800.2 [.] g_main_context_check
1.24% 43 pipewire-pulse libspa-audioconvert.so [.] 0x0000000000057c6e
1.05% 39 pipewire-pulse libspa-audioconvert.so [.] 0x0000000000057cd5
0.95% 77 swapper [kernel.kallsyms] [k] perf_event_task_tick
0.93% 67 swapper [kernel.kallsyms] [k] psi_group_change
0.89% 27 pipewire-pulse libspa-audioconvert.so [.] 0x0000000000057c8f
0.83% 62 swapper [kernel.kallsyms] [k] __schedule
0.74% 34 gnome-shell libglib-2.0.so.0.6800.2 [.] g_source_ref
0.72% 55 swapper [kernel.kallsyms] [k] __update_load_avg_cfs_rq
0.69% 45 swapper [kernel.kallsyms] [k] timerqueue_add
BPF ツール
基本ツールでもある程度の情報を取得できますが、メモリ関連のBPF ツールを使ってもう少し詳細を確認してみましょう。
oomkill
oomkillツールを実行するとout-of-memoryのイベントをトレースでき、また同時に到達したページサイズとロードアベレージも出力します。
# /usr/share/bcc/tools/oomkill
Tracing OOM kills... Ctrl-C to stop.
15:24:39 Triggered by PID 1055 ("tuned"), OOM kill of PID 5728 ("oom-test"), 989864 pages, loadavg: 0.81 0.18 0.06 3/641 5729
ここでは、oom-test.cというプログラムを実行し、意図的にOOMを再現させています。
malloc(1<<20)によって、2進数で表記された1を左へ20ビット桁移動しています。「<<」が左シフト演算子です。つまり1MiBつづOOMが発生するまでメモリ確保していきます。
メビバイト(MiB)とは2の20乗バイトです。
ん?と思った方へ、少し補足します。
ビットは、0か1が入る箱です。このビットが8つ集まったものがバイト。
キロバイトは、10の3乗バイトです。けれど、コンピュータにとっては0と1で表す2進数で2の10乗である1024のほうがキリがいいんです。
そこを正確に区別するために、1キロバイト(KB)=1,000バイト、1キビバイト(KiB)=1,024バイトと区別します。
メガは、10の6乗で、百万を表します。一方、1メビバイト(MiB)は、2の20乗バイト =1,024キロバイト = 1,048,576バイトです。
#include <stdio.h>
#include <stdlib.h>
int main (void)
{
int n = 0;
while (1) {
if (malloc(1<<20) == NULL) {
printf("malloc failure after %d MiB\n", n);
return 0;
}
printf ("got %d MiB\n", ++n);
}
}
# gcc oom-test.c -o oom-test
# ./oom-test
...
got 449367 MiB
got 449368 MiB
got 449369 MiB
got 449370 MiB
Killed
上記のように、oom-testを実行すると、dmesgでもOOMを次のように確認できます。
[14961.626074] oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/user.slice/user-0.slice/session-6.scope,task=oom-test,pid=5728,uid=0
[14961.626084] Out of memory: Killed process 5728 (oom-test) total-vm:458409016kB, anon-rss:650320kB, file-rss:4kB, shmem-rss:0kB, UID:0
[14961.891704] oom_reaper: reaped process 5728 (oom-test), now anon-rss:0kB, file-rss:0kB, shmem-rss:0kB
memleak
memleakというツールを使うと、メモリの確保と解放をデフォルトで5秒間隔で表示できます。
例えば、bashのメモリ確保や解放を表示してみます。
# /usr/share/bcc/tools/memleak -p 2288
Attaching to pid 2288, Ctrl+C to quit.
[18:37:25] Top 10 stacks with outstanding allocations:
77 bytes in 1 allocations from stack
xrealloc+0x17 [bash]
323 bytes in 18 allocations from stack
xmalloc+0x12 [bash]
memleakだけでは、実際にリークしているかは判断できませんので、valgrindを利用したほうがいいでしょう。
また-pオプションを付けないと、カーネルのメモリ確保を表示します。
# /usr/share/bcc/tools/memleak
Attaching to kernel allocators, Ctrl+C to quit.
[18:44:10] Top 10 stacks with outstanding allocations:
1024 bytes in 4 allocations from stack
kmem_cache_alloc+0x139 [kernel]
kmem_cache_alloc+0x139 [kernel]
__alloc_file+0x2a [kernel]
alloc_empty_file+0x43 [kernel]
alloc_file+0x2b [kernel]
anon_inode_getfile+0xd2 [kernel]
anon_inode_getfd+0x35 [kernel]
bpf_prog_load+0x408 [kernel]
__do_sys_bpf+0x584 [kernel]
do_syscall_64+0x5b [kernel]
entry_SYSCALL_64_after_hwframe+0x65 [kernel]
trace
traceを利用して、システムコールの呼出しをシステム全体でトレースできます。
例えばmmap() をトレースすることにより、新しいマッピングを呼び出し元プロセスの仮想アドレス空間に作成するイベントを確認できます。
# /usr/share/bcc/tools/trace -U t:syscalls:sys_enter_mmap
PID TID COMM FUNC
13129 13129 date sys_enter_mmap
b'[unknown]'
1682 1682 gnome-shell sys_enter_mmap
b'mmap64+0x47 [libc-2.28.so]'
1682 1712 llvmpipe-0 sys_enter_mmap
b'mmap64+0x47 [libc-2.28.so]'
その他、brk()をトレースするこにより、プロセスへのメモリの割り当てや解放イベントも確認できます。
brk() は プログラムブレーク (program break) の場所を変更します。 プログラムブレークはプロセスのデータセグメント (data segment) の 末尾を示します。プログラムブレークを増やすということは、そのプロセスへの メモリーを割り当てる効果があり、 プログラムブレークを減らすということは、メモリーを解放するということになります。
# /usr/share/bcc/tools/trace -U t:syscalls:sys_enter_brk
PID TID COMM FUNC
13450 13450 awk sys_enter_brk
b'brk+0xb [ld-2.28.so]'
13456 13456 sleep sys_enter_brk
b'brk+0xb [ld-2.28.so]'
page faultもtraceを利用して、簡単に確認することができます。
# /usr/share/bcc/tools/trace -U t:exceptions:page_fault_user
PID TID COMM FUNC
2288 2288 bash page_fault_user
b'[unknown] [bash]'
2288 2288 bash page_fault_user
b'__libc_fork+0x137 [libc-2.28.so]'
2288 2288 bash page_fault_user
b'make_child+0x42b [bash]'
2288 2288 bash page_fault_user
b'__libc_malloc+0x139 [libc-2.28.so]'
# /usr/share/bcc/tools/trace t:exceptions:page_fault_kernel
PID TID COMM FUNC
933 933 ksmtuned page_fault_kernel
933 933 ksmtuned page_fault_kernel
933 933 ksmtuned page_fault_kernel
16729 16729 ksmtuned page_fault_kernel
16730 16730 ksmtuned page_fault_kernel
16730 16730 awk page_fault_kernel
16730 16730 awk page_fault_kernel
コメント