LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術

第63回Linuxカーネルのコンテナ機能 ― cgroupの改良版cgroup v2[8]

cgroup v2がstableとされた4.5カーネルがリリースされたのは2016年で、もう10年が経ちました。この連載でcgroup v2を初めて取り上げたのは2017年で、そこからでも8年以上経っています。

4.5カーネル以降もcgroup v2にはさまざまな機能が追加されています。たとえば、4.5カーネルの時点では、cgroup v2にはマウントオプションはありませんでしたが、今ではいくつもオプションがあります。そのほか、CPUコントローラの追加などの大きな機能追加以外にも、細かい機能が多数追加されています。

今回は、この連載の各回を書いた時点では存在していなかったため紹介しておらず、その後追加された細かな機能をいくつか紹介します。

favordynmodsオプション

まずは、cgroup v2を使う場合に指定できるマウントオプションfavordynmodsです。

favordynmodsオプションを指定すると、タスクのcgroup間の移動やコントローラのオン・オフといったcgroup操作のレイテンシを下げるようになります。代わりに、このようなcgroup操作に比べて頻繁に起こるforkやexitなどの処理のコストが高くなります。

Androidでは、タスクがcgroup間を移動する処理が一般的な操作であるために、その際のコストを削減するために追加されたようです。

一般的に、起動したコンテナやプロセスは、デフォルトでは親プロセスと同じcgroupに配置されますので、親プロセスと異なるcgroupに所属させたい場合は移動させなければなりません。このオプションを指定すると、このときのレイテンシを下げられるでしょう。

ただし、5.7カーネル以降では、clone3システムコールを使うと任意のcgroupに所属させた状態でプロセスを生成できます。このため、clone3システムコールを使って、直接目的のcgroupにプロセスを配置する場合はレイテンシ削減の効果はありません。

4.9から5.19までのカーネルでは、このオプションを指定したときと同じ処理になっていました[1]。しかし、常にこの処理を強制すると処理速度の低下につながるケースが多いため、6.0カーネルで4.9より前と同じ処理に戻され、このオプションが新設されました[2]

カーネルビルド時のconfigでCONFIG_CGROUP_FAVOR_DYNMODSを有効yにすると、マウントオプションを指定しなくても、指定したときと同じ動作になります。デフォルトでは無効nです。

6.5カーネル以降であれば、カーネルのブートパラメータcgroup_favordynmodstrueに設定すると、この機能を有効にできます。前述のように、カーネルビルド時に有効化されている場合は、このパラメータをfalseに設定すれば無効化できます。

cgroup.events

ここからは、cgroup内に存在するファイルについてのお話です。

第39回で、cgroupの状態を通知する機能を説明する際に、cgroup.eventsファイルについて説明しました。このときは4.11カーネルの環境で説明をしており、cgroup.eventsファイルに含まれる項目はpopulated1つだけでした。

その後、5.2カーネルで新たにfrozenという項目が追加されました。このあとの説明で使いますので、ここであらためてcgroup.eventsファイルについて紹介します。

cgroup.eventsファイルには、表1のとおり2つの項目があります。

表1 cgroup.eventsファイル内の項目
項目名 説明 実装されたバージョン
populated 自身のcgroupとその子孫のcgroup内にプロセスが存在するかどうか。cgroupにプロセスが存在するときは1、プロセスが存在しないときは0 4.5
frozen 自身のcgroupがフリーズ(一時停止)状態のときは1、そうでなければ0 5.2

このファイルには2つの機能があります。

まず、ファイルの内容を読み取ることで、自身のcgroupおよびその子孫cgroup内のタスクの存在状況や、自身のcgroupのfreezerコントローラの状態を確認できます。

次に、このファイルを監視することで、自身と子孫cgroupの状態が変化した通知を受け取れます。

cgroup.eventsファイルはroot cgroup以外に存在します。root cgroupには常にタスクが存在し、またroot cgroupではfreezerコントローラを使えないため、root cgroupに存在する意味がないからです。

第39回では、cgroup.eventsファイルのうち、populatedについて値の変化や値が変化したときの通知について説明していますので、populatedについてはそちらをご覧ください。

frozenについては、このあとfreezerコントローラの説明で実行例を紹介します。

freezerコントローラとcgroup.freeze

cgroup v2では、cgroupと子孫のcgroupに所属するタスクをすべてフリーズ(一時停止)させたり、再開させたりできます。

また、先に紹介したcgroup.eventsファイルを用いてフリーズ状態を監視できます。

cgroup v1にもfreezerというコントローラが存在しました。cgroup v2リリース時点ではfreezerコントローラは使えず、使えるようになったのは5.2カーネルからです。cgroup v2では、cgroup.freezeファイルからfreezerコントローラの機能を利用します。

cgroup v2では、cgroupのコア機能に関係するファイルの名前はcgroup.で始まります。ファイル名からわかるように、この機能はコントローラではなくcgroupコアの機能として位置づけられるようになりました。

表2 cgroup.freezeファイル
ファイル名 機能 可能な操作 実装されたバージョン
cgroup.freeze cgroup 内と子孫 cgroup 内のすべてのタスクを一時停止・再開させる 読み書き 5.2

ここからはcgroup.freezeファイルの動きを見てみましょう。

シェルを起動してcgroupを作成し、そのシェルのプロセスをcgroupに登録します。その後、cgroup.freezeファイルに1を書き込みます。

$ sudo mkdir /sys/fs/cgroup/test01 (test01 cgroupの作成)
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs (シェルをcgroupに登録)
1060
$ echo 1 | sudo tee /sys/fs/cgroup/test01/cgroup.freeze (cgroup.freezeに1を書き込みフリーズ状態にした)


(ここで改行を入力しようとしているがシェルの反応がなくなる)

cgroup.freezeファイルに1を書き込むとシェルの応答がなくなります。たとえば、キーボードから文字を入力しようとしても応答がありません。

このとき、別のシェルからtest01 cgroupのcgroup.eventsファイルを確認してみます。

$ cat /sys/fs/cgroup/test01/cgroup.events 
populated 1
frozen 1

cgroup.eventsファイルのfrozenの項目が1になっているはずです。

この状態で、別のシェルからtest01 cgroupのcgroup.freezeファイルに0を書き込みます。

$ echo 0 | sudo tee /sys/fs/cgroup/test01/cgroup.freeze
0

すると、さきほど応答のなかったシェルがふたたび応答するようになります。

$ echo 1 | sudo tee /sys/fs/cgroup/test01/cgroup.freeze(freeze前に実行したコマンド)

(さきほどcgroup.freezeに1を書き込んだあと改行を入力したが、応答がなくなっていた)
1 (シェルの実行が再開され、teeコマンドの出力が表示される)
$ (freezeのあとに入力した改行に対する応答がある)
$

親子関係にあるcgroupに所属する複数のタスクが同時にフリーズ状態になることも確認しておきましょう。

$ openssl speed -multi $(grep processor /proc/cpuinfo|wc -l)

このようにopensslコマンドでCPUを激しく使うようなタスクを複数起動します。ここで検証に使っている環境はCPUが2つのホストです。別シェルでpsコマンドで確認すると、次のようにopensslのタスクが2つ、CPUを消費していることがわかります(PIDが1188と1189のプロセス⁠⁠。

$ ps auxf | grep openssl
karma       1187  0.0  0.3   9660  6144 pts/1    S+   14:51   0:00      |   \_ openssl speed -multi 2
karma       1188  100  0.1   9788  3460 pts/1    R+   14:51   0:09      |       \_ openssl speed -multi 2
karma       1189 99.4  0.1   9788  3716 pts/1    R+   14:51   0:09      |       \_ openssl speed -multi 2
(PID 1188と1189が実行中でCPUを消費していることがわかる)
$ grep State /proc/{1188,1189}/status
/proc/1188/status:State:        R (running)
/proc/1189/status:State:        R (running)

プロセスの状態の列を見るとR+となっており、プロセスは実行中であることが確認できます。/proc/[PID]/statusファイルからも実行中であることがわかります[3]

それでは、この2つのプロセスの一方を親であるtest01 cgroupに、もう一方を子であるtest02 cgroupに所属させます。

$ echo 1188 | sudo tee /sys/fs/cgroup/test01/cgroup.procs 
(PID:1188は親cgroupであるtest01に所属させる)
1188
$ echo 1189 | sudo tee /sys/fs/cgroup/test01/test02/cgroup.procs 
(PID:1189は子cgroupであるtest02に所属させる)
1189

ここで、test01 cgroupのcgroup.freeze1を書き込みます。

$ echo 1 | sudo tee /sys/fs/cgroup/test01/cgroup.freeze
1
$ cat /sys/fs/cgroup/test01/cgroup.events
populated 1
frozen 1 (frozenが1に変化した)
$ grep State /proc/{1188,1189}/status
/proc/1188/status:State:        S (sleeping)
/proc/1189/status:State:        S (sleeping)
(runningだったタスクがsleepingに変化している)

cgroup.freezeファイルに1を書き込むと、test01 cgroupおよびその子であるtest02 cgroupがフリーズ状態になり、先ほどまで実行中だったプロセスの状態が両方ともsleepingに変化したことが確認できます。

このとき、/proc/[PID]/schedファイル内のタスクが実際に実行された時間を表示する項目se.sum\_exec\_runtimeを定期的に表示させてみると、タスクがフリーズされた時点以降は値が増加していないことがわかります。

$ while :; do sleep 1; grep sum_exec_runtime /proc/1188/sched; done
  :(略)
se.sum_exec_runtime                          :        478584.436171
se.sum_exec_runtime                          :        479586.493546
se.sum_exec_runtime                          :        480585.942886 (ここまでは値が増加している)
se.sum_exec_runtime                          :        481361.444251 (このあたりでcgroup.freezeに1を書き込んだ)
se.sum_exec_runtime                          :        481361.444251 (このあと値が増加しない)
se.sum_exec_runtime                          :        481361.444251
  :(略)

cgroup.freezeファイルに0を書き込むことで、タスクの実行が再開されます。

$ echo 0 | sudo tee /sys/fs/cgroup/test01/cgroup.freeze
$ cat /sys/fs/cgroup/test01/cgroup.events 
populated 1
frozen 0 (frozenが0になる)
$ grep State /proc/{1188,1189}/status
/proc/1188/status:State:        R (running)
/proc/1189/status:State:        R (running)
(タスクの実行が再開されている)

このとき、inotifywaitコマンドでcgroup.eventsファイルを監視すると、frozen1になった時点で通知が受信できます。

$ inotifywait -m /sys/fs/cgroup/test01/cgroup.events
Setting up watches.
Watches established.
/sys/fs/cgroup/test01/cgroup.events MODIFY (cgroup.freezeに1を書き込んだ)
/sys/fs/cgroup/test01/cgroup.events MODIFY (cgroup.freezeに0を書き込んだ)

ここまで見たように、cgroupをフリーズ状態にすると、その子孫のcgroupもフリーズ状態になります。しかし、子孫のcgroupを直接フリーズ状態にしても、自身のcgroupがフリーズ状態ではない場合は、cgroup.eventsファイルのfrozen0のままです。

$ cat /sys/fs/cgroup/test01/cgroup.events | grep frozen
frozen 0
$ cat /sys/fs/cgroup/test01/test02/cgroup.events | grep frozen
frozen 0
(test01, test02 cgroupともにフリーズ状態ではない)
$ echo 1 | sudo tee /sys/fs/cgroup/test01/test02/cgroup.freeze 
(test02 cgroupをフリーズ状態にする)
1
$ cat /sys/fs/cgroup/test01/cgroup.events | grep frozen
frozen 0
(test01 cgroupはフリーズ状態ではない)
$ cat /sys/fs/cgroup/test01/test02/cgroup.events | grep frozen
frozen 1

cgroup.eventsファイルのpopulatedが子孫を考慮した値になっているのに対し、同じファイル内のfrozenは自身の状態のみを表示していることに注意してください。

フリーズ状態になっているcgroupには、タスクを追加することができます。

$ echo 1 | sudo tee /sys/fs/cgroup/test01/cgroup.freeze 
1
$ echo $$ | sudo tee /sys/fs/cgroup/test01/cgroup.procs 
1172

(シェルの反応がなくなった)

上の実行例は、まずtest01 cgroupをフリーズ状態にしたあとに、そのフリーズ状態にあるtest01 cgroupにシェルのプロセスを登録しました。すると、登録した直後にシェルからの応答がなくなりました。

ここまで、cgroup.freezeファイルとcgroup.eventsファイルを使ったfreezerコントローラ機能の紹介でした。

cgroup.kill

cgroup.killファイルを使うと、cgroup内とその子孫cgroup内のプロセスすべてにSIGKILLシグナルを送って、プロセスをkillできます。

表3 cgroup.killファイル
ファイル名 機能 可能な操作 実装されたバージョン
cgroup.kill cgroup内と子孫cgroup内のすべてのタスクを強制終了させる 書き込み専用 5.14

たとえば、複数のプロセスが動いているコンテナが不安定になったために、コンテナ内のプロセスをすべて同時に停止させるような場合に使えます。

それでは、cgroup.killファイルの動きを見てみましょう。

cgroup.killファイルには1以外の数値は書き込めません。また、書き込み以外の処理はできません。1以外の値を書き込もうとしたり、ファイルを読もうとしたりするとエラーが発生します。

$ echo 5 | sudo tee /sys/fs/cgroup/test01/cgroup.kill
5
tee: /sys/fs/cgroup/test01/cgroup.kill: Numerical result out of range
(cgroup.killに5を書き込もうとしたらエラーになった)
$ cat /sys/fs/cgroup/test01/cgroup.kill
cat: /sys/fs/cgroup/test01/cgroup.kill: Permission denied
(cgroup.killファイルを読もうとしたらエラーになった)

test01 cgroupとその子cgroupとしてtest02を作成します。そしてプロセスを複数起動させ、test01test02 cgroupに所属させます。

$ sudo mkdir -p /sys/fs/cgroup/test01/test02
$ sleep 1800 &
[1] 1114
$ sleep 1800 &
[2] 1115
$ sleep 1800 &
[3] 1116
$ echo 1114 | sudo tee /sys/fs/cgroup/test01/cgroup.procs 
1114
$ echo 1115 | sudo tee /sys/fs/cgroup/test01/cgroup.procs 
1115
$ echo 1116 | sudo tee /sys/fs/cgroup/test01/test02/cgroup.procs 
1116

上の実行例では、sleepコマンドを3つ実行し、そのうちの2つを親cgroupであるtest01 cgroupに、1つを子cgroupであるtest02 cgroupに所属させました。

そして、親cgroupであるtest01 cgroup内のcgroup.killファイルに1を書き込みます。

$ echo 1 | sudo tee /sys/fs/cgroup/test01/cgroup.kill 
1
[1]   Killed                  sleep 1800
[2]-  Killed                  sleep 1800
[3]+  Killed                  sleep 1800
$ 

するとtest01test02 cgroupにいるsleepプロセスがすべてkillされました。

cgroup.killファイルはプロセス単位での操作にのみ対応しており、第45回第46回で説明した、スレッドモードのスレッド化サブツリー内ではEOPNOTSUPPでエラーになります。

まとめ

今回は、これまで連載では紹介していなかったcgroup v2の機能を紹介しました。

cgroup v2がstableになって以降も、さまざまな変更が加わっています。この連載で紹介したあとにも機能が追加されており、まだ紹介できていない機能があります。

今後も、時々はそのような機能を取り上げて紹介したいと思います。

おすすめ記事

記事・ニュース一覧