第56回から続けてcgroup v2から使うメモリコントローラの機能を紹介してきました。今回も引き続き、まだ紹介していない機能を紹介していきます。
memory.reclaim
まずはmemory.ファイルです。このファイルは5.
| ファイル名 | 機能 | 可能な操作 | 実装されたバージョン |
|---|---|---|---|
memory. |
このファイルに書き込んだバイト数分メモリを回収する | 書き込み専用 | 5. |
メモリの回収については、第56回で説明していますので、ご参照ください。
Linuxカーネルには、メモリが不足した際にメモリを回収するための仕組みとして、kswapdとdirect reclaim
memory.ファイルは、これらのカーネルに備わった仕組みを置き換える機能ではありません。すでにメモリが足りない状況ではなく、メモリが足りている状況でも、積極的にメモリを監視して、メモリを安定して供給する仕組みです。ユーザ空間でメモリを常時監視し、負荷が高いタスクや新たに起動するタスクがメモリを利用できるように、しばらくアクセスされていない
カーネルは、アクセスが発生している
LRUリストは、メモリへのアクセスや割り当て、回収処理が発生した際に更新されます。
そこで、ユーザ空間からメモリを監視し、memory.ファイルを使って、少量のメモリを定期的に回収させることで、LRUリストを最新の状態に更新できます。
これにより、カーネルがより正確にメモリの使用状況を把握できるようになります。その結果、より多くのメモリを必要とするタスクや、新規タスクをスケジューリングするために、メモリを確保しやすくなります。
memory.reclaim ファイルの使い方
memory.ファイルは、書き込み専用のファイルで、ファイルに書き込んだ値だけメモリを回収する処理を起動します。次のように使います。
echo "1G" > memory.reclaim
この例では、メモリを1GB回収しようとする処理が起動します。もちろん、回収するメモリがない場合は指定した値より少ない量だけが回収される場合もあります。その場合はエラーEAGAIN)
メモリ使用量の確認方法
memory.ファイルの動きを確認するには、cgroupに所属するプロセスのメモリ使用量を確認する必要があります。今回も、ここまでの連載で使ったようにmemory.を使います。そのほかに、メモリの統計情報を表示するmemory.ファイルを使います。
memory.ファイルは、プロセスが使用しているメモリの状況をメモリタイプごとに表示します。1行に1つ、エントリ名と値が表示されており、執筆時点で最新のLTSカーネルである6.
このうち、今回の説明で使用するエントリを表2で紹介します。
memory.statファイルの今回使用するエントリ| エントリ名 | 説明 |
|---|---|
anon |
匿名メモリが使っているメモリ量 |
file |
ページキャッシュが使っているメモリ量[2] |
pgsteal |
cgroupが作成されてから回収されたページ数 |
匿名メモリやページキャッシュについてはあとで説明します。
それでは、実際にmemory.ファイルを使って、動きを見ていきましょう。今回の実行例は、Ubuntu 24.
ページキャッシュを使った実験
まずは、ファイルシステム上に保存されているファイルへのアクセスを高速化するために、ファイルのデータをメモリ上にコピーしているページキャッシュを使い、memory.ファイルの動きを見ていきます。
ページキャッシュについては、第52回でもう少し詳しく説明していますので、そちらもご覧ください。
ストレージ上のファイルデータと、メモリ上のページキャッシュが完全に一致している場合、ページキャッシュを消しても、データはストレージにあるわけですから、ページキャッシュとして使われているメモリを回収しても問題はありません。
また、ページキャッシュのデータが更新されている場合は、ストレージへ書き戻せば、同様にページキャッシュを回収しても問題ありません。
このように、ページキャッシュとして使われているメモリは、回収の対象としやすいです。memory.ファイルの動きを見る場合にも、簡単に動作を確認できます。
そこで実際に、ファイルのデータをページキャッシュに載せ、memory.ファイルを使って回収させてみましょう。
まずは、確認に使うcgroupを作成し、プロセスを登録します。
$ sudo mkdir /sys/fs/cgroup/test01 $ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control (子孫cgroupでメモリコントローラが使えるように設定) $ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs (シェルをtest01に所属させる) 1164
Ubuntuでシステムを起動すると、root cgroupでメモリコントローラが利用できるように設定されている状態が多いと思いますが、設定されていない環境もあるかもしれません。そのため、念のため子孫でメモリコントローラが利用できるように操作しています。
この時点のメモリ消費量をmemory.とmemory.ファイルで確認します。
$ cat /sys/fs/cgroup/test01/memory.current 532480 (cgroupのメモリ消費量の確認) $ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat anon 446464 file 0 (cgroupのメモリ消費量の詳細の確認)
memory.ファイルを見ると、500KBほどのメモリを消費していることがわかります。memory.ファイルを見ると、もう少し詳細にメモリがどのように使われているかを確認できます。fileは0ですので、ページキャッシュとしては使われていないことがわかります。
ここでファイルをddコマンドで作成してみましょう。ddコマンドでのファイル作成はページキャッシュ経由ですので、ページキャッシュとしての消費量が増えるはずです。
$ dd if=/dev/urandom of=/tmp/testfile bs=1M count=512 512+0 records in 512+0 records out 536870912 bytes (537 MB, 512 MiB) copied, 1.69457 s, 317 MB/s (ファイルの作成) $ cat /sys/fs/cgroup/test01/memory.current 553488384 (cgroupのメモリ消費量の確認) $ grep -E '(^file |^anon |^pgsteal )' /sys/fs/cgroup/test01/memory.stat anon 454656 file 536944640 pgsteal 0 (cgroupのメモリ消費量の詳細とメモリ回収量の確認)
上の例では、500MBほどのファイルを作成しています。ファイルを作成したのち、memory.ファイルを確認すると500MBほど増えていることがわかります。
そして、memory.ファイルを見ると、fileエントリの値が増えており、こちらも500MBほど増えています。anonはそれほど変化がないので、主にファイルがメモリ上にキャッシュされていることにより、cgroupに所属するプロセスのメモリ消費が増加したことがわかります。あとで比較するために、pgstealの値も確認しました。0ですので、この時点ではこのcgroupで回収されたメモリはありません。
ここで、memory.ファイルを使って200MBほどメモリを回収させてみます。
$ echo "200M" | sudo tee /sys/fs/cgroup/test01/memory.reclaim 200M
memory.ファイルに200Mという文字列を書き込みました。
さきほどと同様にmemory.とmemory.ファイルを確認してみましょう。
$ cat /sys/fs/cgroup/test01/memory.current 338075648 (cgroupのメモリ消費量の確認) $ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat anon 479232 file 327229440 (cgroupのメモリ消費量の詳細の確認)
memory.ファイルの値は確かに200MBほど減少しています。そして、memory.ファイルを確認すると、さきほどと同様にanonは値が大きく変化しておらず、fileの値は200MBほど減少していますので、ページキャッシュからメモリが回収されたことがわかります。
ここで、memory.内でメモリ回収量を示すpgstealの値を見ると51200となっています。pgstealの値はページ数ですので、51200*4096でちょうど200MB回収されたことがわかります。
$ grep 'pgsteal ' /sys/fs/cgroup/test01/memory.stat pgsteal 51200 (pgstealはページ数なので51200*4096=200MB)
memory.を使って、ページキャッシュからメモリが回収される様子が確認できました。
スワップがあるときの匿名メモリの実験
次に、ページキャッシュのようにファイルをキャッシュしているメモリではなく、mallocなどでプログラムから動的に確保されたメモリをmemory.ファイルで回収させてみましょう。このようなメモリは匿名
ファイルのキャッシュの場合は、ファイルという実体がありますのでメモリを解放しても問題ありません。
しかし、匿名メモリの場合は、他に同じデータはシステム上に存在しませんので、いきなりメモリを解放するわけにはいきません。回収する場合はスワップ領域へ書き出す必要があります。
スワップが存在するシステム上で、memory.ファイルを使って、匿名メモリからメモリを回収させてみましょう。
まずは実験の準備をしたあと、メモリ消費量を確認します。
$ sudo mkdir /sys/fs/cgroup/test01 $ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control $ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 1011 $ grep SwapTotal /proc/meminfo SwapTotal: 3304444 kB (スワップがある) $ cat /sys/fs/cgroup/test01/memory.current 401408 (cgroupのメモリ消費量の確認) $ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat anon 323584 file 0 (cgroupのメモリ消費量の詳細の確認)
上のように、/proc/で確認したとおりスワップ領域が存在しています。そして、シェルのプロセスを登録した時点でのメモリ消費をmemory.とmemory.ファイルで確認しました。
それでは、前回までの実行例で使用していたstress-ngコマンドを使って、匿名メモリを確保します。
$ stress-ng --vm 1 --vm-bytes 512M --vm-hang 0 > /dev/null 2>&1 & [1] 1047 $ cat /sys/fs/cgroup/test01/memory.current 564518912 (cgroupのメモリ消費量の確認) $ cat /sys/fs/cgroup/test01/memory.swap.current 0 (スワップの使用量は0) $ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat anon 538857472 file 23613440 (cgroupのメモリ消費量の詳細の確認)
512MBのメモリを消費するようにstress-ngコマンドを実行しました。memory.ファイルを確認すると、およそ512MBほどメモリ消費が増加していることがわかります。そのときの匿名メモリとページキャッシュの消費量もmemory.ファイルで確認しました。
ここでmemory.ファイルを使って256MBのメモリを回収するように指示します。
$ echo "256M" | sudo tee /sys/fs/cgroup/test01/memory.reclaim 256M $ cat /sys/fs/cgroup/test01/memory.current 295796736 (cgroupのメモリ消費量の確認) $ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat anon 293531648 file 20480 (ページキャッシュと匿名メモリの両方から回収されている) $ cat /sys/fs/cgroup/test01/memory.swap.current 245645312 (スワップの使用量が増えている)
memory.ファイルに書き込んだ直後に、memory.ファイルを確認すると295,796,736stress-ngコマンド実行直後との差分を取ると564,518,912-295,796,736=268,722,176
memory.ファイルを確認すると、ページキャッシュとして消費していた分はほぼすべて回収され、残りは匿名メモリから回収されていることがわかります。
このとき、スワップ消費量を示すmemory.ファイルを確認すると、memory.ファイル内のanonの値が減少した分と同じくらいスワップ消費量が増えており、匿名メモリで消費されていた分のメモリ領域がスワップに移動したことがわかります。
memory.ファイルで回収を指示すると、匿名メモリに確保されていたメモリがスワップに移動することが確認できました。
スワップがないときの匿名メモリの実験
ここまでで、匿名メモリ上のデータがスワップに移動し、メモリが回収されることを確認しました。
それではスワップがない場合、memory.ファイルを使うとどうなるのでしょうか? 確認してみましょう。
$ sudo mkdir /sys/fs/cgroup/test01 $ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control $ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 1036 $ sudo swapoff -a (スワップの無効化)
これまでと同様にcgroupを準備し、スワップを無効にします。
$ cat /sys/fs/cgroup/test01/memory.current 454656 $ stress-ng --vm 1 --vm-bytes 512M --vm-hang 0 > /dev/null 2>&1 & [1] 1040 $ cat /sys/fs/cgroup/test01/memory.current 564621312 $ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat anon 538935296 file 23793664
先の実験と同様に、stress-ngコマンドで512MBのメモリを消費させます。memory.、memory.ファイルでメモリ消費量の増加と、匿名メモリ消費量の増加も確認できました。
ここで、memory.ファイルを使って256MBのメモリを回収するように指示します。
$ echo "256M" | sudo tee /sys/fs/cgroup/test01/memory.reclaim 256M (返ってこない)
すると、実行したechoコマンドが返ってきません。
別シェルを起動してmemory.ファイルを確認すると、メモリ消費量は減少していません。同様にmemory.ファイルを確認しても、匿名メモリの消費量は減少しておらず、メモリが回収されていないことがわかります。
$ cat /sys/fs/cgroup/test01/memory.current 559521792 (回収されていない) $ grep -E '(^file |^anon )' /sys/fs/cgroup/test01/memory.stat anon 540250112 file 17027072
このように匿名メモリが消費されており、データを移動させるスワップがない場合は、メモリが回収できないことがわかります。
実験では、応答が返ってこないechoコマンドをしばらく放置しました。しかし、返ってこないのでCtrl+Cで終了させました。このように、指定した量を回収するまで応答が返らない場合があります。先に説明したように、指定した量より少なく回収してEAGAINが返ることもあるようです。
ここまでで、memory.ファイルの動きが確認できました。
memory.peak
memory.ファイルは、5.
このファイルには、cgroupが作成されてから、そのcgroupと子孫のcgroupで消費されたメモリの最大値が記録されています。
6.
また、同様にスワップ消費量の最大値もmemory.というファイルから確認できます。
| ファイル名 | 機能 | 可能な操作 | デフォ ルト値 |
実装されたバージョン |
|---|---|---|---|---|
memory. |
cgroupとその子孫cgroupに対して、cgroup作成時またはリセット以降に使用されたメモリ消費の最大値 | 読み書き | 0 |
5. |
memory. |
cgroupとその子孫cgroupに対して、cgroup作成時またはリセット以降に使用されたスワップ消費の最大値 | 読み書き | 0 |
6. |
さて、表3に示すとおり、memory.ファイルの動きをすべて確認するには、6.
今回は、先に書いたようにUbuntu 24.memory.ファイルのリセット機能は試せません。
実際、6.memory.やmemory.ファイルは次のように読み取り専用となっています。
$ uname -r 6.8.0-110-generic (24.04標準のカーネルは6.8.0) $ ls -l /sys/fs/cgroup/user.slice/memory*peak -r--r--r-- 1 root root 0 Apr 25 12:09 /sys/fs/cgroup/user.slice/memory.peak -r--r--r-- 1 root root 0 Apr 25 12:09 /sys/fs/cgroup/user.slice/memory.swap.peak (読み取り専用)
Ubuntuの場合は24.
次のように24.memory.もmemory.ファイルも読み書き可能になりました。
$ uname -r 6.17.0-22-generic (HWEカーネルをインストールしバージョンが上がった) $ ls -l /sys/fs/cgroup/user.slice/memory*peak -rw-r--r-- 1 root root 0 Apr 25 12:19 /sys/fs/cgroup/user.slice/memory.peak -rw-r--r-- 1 root root 0 Apr 25 12:19 /sys/fs/cgroup/user.slice/memory.swap.peak (読み書き可能になった)
それではこの環境でmemory.ファイルの動きを確認していきましょう。
これまでと同様に準備したあと、memory.とmemory.ファイルの中身を確認します。
$ sudo mkdir /sys/fs/cgroup/test01 $ echo "+memory" | sudo tee /sys/fs/cgroup/cgroup.subtree_control +memory $ cat /sys/fs/cgroup/test01/memory.peak 0 $ cat /sys/fs/cgroup/test01/memory.swap.peak 0 (いずれのファイルも初期値は0)
上の実行例のように、test01 cgroupを作成した直後は、いずれのファイルにも0が記録されています。
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 1058 $ cat /sys/fs/cgroup/test01/memory.peak 524288
シェルをtest01 cgroupに登録すると、memory.ファイルの値が増えます。
ここでstress-ngコマンドでメモリを消費させてみましょう。
$ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 > /dev/null 2>&1 & [1] 1096 $ cat /sys/fs/cgroup/test01/memory.peak 311988224 $ cat /sys/fs/cgroup/test01/memory.current 311984128
256MBメモリを使用するようにstress-ngコマンドを実行すると、memory.ファイルの値が増加しました。このとき当然、memory.ファイルの値も増加しています。
stress-ng実行前後の値の差から、stress-ngコマンドの実行によって増加したメモリ消費量がわかります。
$ kill 1096 (stress-ngプロセスをkill) $ cat /sys/fs/cgroup/test01/memory.peak 312279040 $ cat /sys/fs/cgroup/test01/memory.current 24150016
その後、stress-ngプロセスをkillしました。memory.ファイルを確認すると、値は減少した一方で、memory.ファイルの値は減少していません。
それでは、引き続き値のリセットを試していきましょう。
ここで注意が必要なのは、カーネル付属文書に記載されているこの一文です。
A write of any non-empty string to this file resets it to the current memory usage for subsequent reads through the same file descriptor.
訳すと
つまり、memory.自身の値がグローバルにリセットされるわけではなく、文字列を書き込んだファイルディスクリプタを通して読み込むときのみ、現在の値にリセットされます。
例としてPythonのスクリプトからリセットを試してみましょう。
$ cat reset.py
#!/usr/bin/env python3
# test01 cgroupのmemory.peakに0という文字列を書き込み、値を読む
with open('/sys/fs/cgroup/test01/memory.peak', 'r+') as f:
f.write('0')
f.seek(0)
print(f.read().strip())
(memory.peakの値をリセットし、その後値を表示するためのスクリプト)
$ cat /sys/fs/cgroup/test01/memory.peak
312279040
$ sudo python3 reset.py
29687808
(スクリプトを実行し、リセットして値を表示)
$ cat /sys/fs/cgroup/test01/memory.current
23986176
$ cat /sys/fs/cgroup/test01/memory.peak
312279040
(別のファイルディスクリプタで読むとリセットされる前の値が読み取れる)
上の実行例のように、Pythonのスクリプトからファイルをオープンして文字列を書き込んだあとに、そのまま同一のファイルディスクリプタから値を読み取ると、その時点のmemory.の値に近い値にリセットされていることがわかります[3]。
一方で、catコマンドを実行し、別のファイルディスクリプタを使ってmemory.ファイルを表示させると、リセットされていない最大値が表示されます。
このように、オープンしたファイルディスクリプタごとにピーク値を追跡できますので、複数のファイルディスクリプタをオープンし、リセットすることで任意のタイミングで独立したメモリ消費の最大値を追跡できます。
たとえば、
- fd-A:ワークロード開始前にオープンしてリセット → ワークロード全体の最大値を観測
- fd-B:ワークロードの途中でオープンしてリセット → その時点以降の最大値を観測
というようなことができます。
memory.ファイルの動きは確認できましたので、次はmemory.の動きを簡単に見ておきましょう。これまでの実行例と同じように準備し、stress-ngコマンドでメモリを消費させます。
$ sudo mkdir /sys/fs/cgroup/test01 $ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 1130 $ stress-ng --vm 1 --vm-bytes 256M --vm-hang 0 > /dev/null 2>&1 & [1] 1162 $ cat /sys/fs/cgroup/test01/memory.swap.current 0 $ cat /sys/fs/cgroup/test01/memory.swap.peak 0
この時点で、memory.とmemory.ファイルを確認しました。この時点では、スワップにはデータがなく、両方とも0です。
では、今回最初に紹介したmemory.を使ってメモリを回収させてみましょう。stress-ngコマンドでメモリを消費させており、匿名メモリを使っていますので、メモリ上のデータはスワップに書き出されるはずです。
$ echo "256M" | sudo tee /sys/fs/cgroup/test01/memory.reclaim 256M $ cat /sys/fs/cgroup/test01/memory.swap.current 246075392 $ cat /sys/fs/cgroup/test01/memory.swap.peak 246136832
memory.を使って256MBのメモリを回収させると、memory.、memory.ファイルともに同じくらいの値になっています。
ここで、stress-ngのプロセスをkillしてみます。stress-ngプロセスが消費していたメモリは不要になりますので解放されるはずです。
$ kill 1162 $ cat /sys/fs/cgroup/test01/memory.swap.current 0 $ cat /sys/fs/cgroup/test01/memory.swap.peak 246136832
killコマンド実行後は、memory.ファイルは0となりました。一方で、memory.ファイルは、killする前と値が変わっておらず、これまでに消費した最大の値が記録されたままであることがわかります。
ここまでで、memory.とmemory.ファイルの動きが確認できました。
まとめ
今回は、指定したメモリ量を回収させるmemory.ファイルと、cgroupで消費したメモリの最大値を記録するmemory.、memory.ファイルについて説明しました。
memory.ファイルの説明では、実際にメモリ回収が起こる様子とともに、ページキャッシュと匿名メモリから回収する際の動きの違いも確認しました。memory.によるメモリ回収の動きを見ることで、ページキャッシュと匿名メモリから回収される際の動きの違いもわかりますね。
memory.ファイルについては、値をリセットすることで、任意の間隔でのメモリ消費の差を見ることができることも確認できました。
さて、次回はcgroup v2でまだ紹介していない機能について紹介する予定です。
