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

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

前回は、4.14カーネルでcgroup v2に導入されたスレッドモードの概要を説明しました。今回は、実際にスレッド化サブツリーを作成して操作を行ってみましょう。

今回の実行例は、デフォルトではcgroupをマウントしないPlamo Linux 7.2環境で試しています。systemdを採用したディストリビューションの場合、カーネルの起動パラメータにcgroup_no_v1=allと指定して、すべてのコントローラがcgroup v2から使えるようにするとcgroup v2の機能が試しやすいでしょう。

そして、今回の実行例はいずれもrootユーザで実行しています。

それでは早速、cgroup v2のツリー中でどのようにスレッド化サブツリーを作り、操作していくのかを見ていきましょう。

スレッド化サブツリーを作るには2つの方法があります。

cgroup.typeファイルへの書き込みによるスレッド化サブツリーの作成

まずは1つめの方法です。

シンプルにcgroup.typeファイルにthreadedという文字列を書き込むだけです。これまで説明してきたcgroupの操作方法をご存知であれば想像がつく方法かと思います。

この結果次のようになります。

  • threadedという文字列を書き込んだcgroupのタイプは"threaded"になる
  • threadedという文字列を書き込んだcgroupの親cgroup"domain threaded"になる
  • threadedという文字列を書き込んだcgroupの親cgroup配下の"threaded"以外のcgroupは"domain invalid"になる

つまり、threadedと書き込んだcgroupの親cgroup配下が、前回説明したスレッド化サブツリーとなるわけです。前回説明したように、スレッド化サブツリー内はすべて"threaded"なcgroupでなければ使えませんので、"domain"だったcgroupは"domain invalid"に変化するという動きになっています。

できあがったスレッド化サブツリー内の"domain invalid"な状態のcgroupすべてにthreadedという文字列を書き込んでいかないと、スレッド化サブツリーが完成しないのは面倒ですね。これは将来的にスレッドモードの機能を拡張して、動作を変える必要が出てきた場合に備えてこのようになっているようです。

この動きを確認してみましょう。

cgroup v2のroot配下にtdというディレクトリを、td配下にth01th02というcgroupを2つ作成します。

# mount -t cgroup2 cgroup2 /sys/fs/cgroup/
(cgroup v2のマウント)
# mkdir -p td/th{01,02}
(tdとその配下にth01,th02というcgroupを作成)
# tree -d /sys/fs/cgroup/
/sys/fs/cgroup/
└── td
    ├── th01
    └── th02

3 directories

作成した3つのcgroup内のプロセスとコントローラファイルの中身を確認します。

# cat td/cgroup.procs 
# cat td/cgroup.controllers 
# cat td/th0{1,2}/cgroup.procs
# cat td/th0{1,2}/cgroup.controllers
(作成直後なのでプロセスもコントローラも表示されない)

まだ作っただけでプロセスの登録もコントローラの登録も行っていませんので、すべて空の状態です。

それぞれのcgroup.typeファイルも確認しておきましょう。

# find /sys/fs/cgroup -name "cgroup.type" -exec cat {} \;
domain
domain
domain
(作成したいずれのcgroupのタイプもdomain)

いずれのcgroupも初期状態ですのですべて"domain"となっています図1⁠。

図1 作成直後ですべてdomainの状態
図1 作成直後ですべてdomainの状態

この状態でth01を"threaded"としてみましょう。

# echo "threaded" > /sys/fs/cgroup/td/th01/cgroup.type 
(th01をthreadedに変更)
# cat /sys/fs/cgroup/td/th01/cgroup.type 
threaded
(threadedに変更されている)

threadedになりましたね。他のcgroupがどうなっているのかも見てみましょう。

# cat /sys/fs/cgroup/td/cgroup.type 
domain threaded
(th01の親cgroupはdomain threadedに変化している)
# cat /sys/fs/cgroup/td/th02/cgroup.type 
domain invalid
(td配下のcgroupはdomain invalidに変化している)

"threaded"であるth01の親cgroupであるtdはスレッド化サブツリーのrootである"domain threaded"に、兄弟cgroupであるth02は"domain invalid"となりました。

つまり"threaded cgroup"の親は"domain threaded"に、スレッド化サブツリー内のdomain cgroupは"invalid"になります図2⁠。

図2 スレッド化サブツリーができあがった直後
図2 スレッド化サブツリーができあがった直後

このままではth02は使えない状態のままです。th02を機能させるためには"threaded"にする必要があります。

# echo "threaded" > /sys/fs/cgroup/td/th02/cgroup.type 
# cat /sys/fs/cgroup/td/th02/cgroup.type 
threaded

これでth02がthreadedになり、使える状態になりました。プロセスやスレッドが追加できます図3⁠。

図3 domain invalidのcgroupをthreadedに変更(スレッド化サブツリーの完成)
図3 domain invalidのcgroupをthreadedに変更(スレッド化サブツリーの完成)

スレッド化サブツリー内でのタスク操作

2つめの方法を見る前に、スレッド化サブツリー内へのプロセスやスレッドの追加について見ておきましょう。

マルチスレッドプロセスについて

プロセスやスレッドの追加について見る前に、マルチスレッドに関わるIDの用語がややこしいので、説明で使う用語の整理をしておきます。

まずはマルチスレッドプロセスを起動してみます。次のように2つのスレッドを持つプロセスですpstreeコマンドではスレッドは中かっこ{}で囲われます⁠⁠。

# pstree -p 907
threadtest(907)─┬─{threadtest}(908)
                └─{threadtest}(909)

この後は、上の例を使って説明を進めます。

  • スレッドグループ:マルチスレッドプロセス内のプロセス、スレッドの集合(ここではID 907908909のプロセスからなるグループ)
  • スレッドグループリーダ:スレッドグループ内でスレッドを生成する元となるプロセス(ここではID 907のプロセス)
  • TGID:スレッドグループのID。スレッドグループリーダのPIDに等しい(ここでは907
  • PID:各スレッドのID(ここではそれぞれ907908909

プロセスやスレッドはそれぞれ、IDとしてカーネル内ではpidというデータを持ちます。ここでは各スレッドのIDである907908909です。スレッドIDとしてTIDと説明されることもありますが、カーネル内ではpidという名前の変数ですので、ここでは各スレッドのIDとしてPIDという用語を使います。gettid(2)というシステムコールを使うとこのpidが返ります。

そして、907908909からなるスレッドグループのID(TGID)907です。getpid(2)というシステムコールでマルチスレッドプロセスのIDを取得すると、このTGIDが返ります。

PID、TID、TGIDとややこしいのですが、ここでは上記のようにカーネル内の名称をベースに説明を進めます。

スレッド化サブツリー内へのタスクの追加

用語の説明が済んだところで、スレッド化サブツリー内へのプロセスやスレッドの追加を見ておきましょう。

スレッド化サブツリーにマルチスレッドプロセスを登録させるには、まずはプロセスをcgroup.procsに登録する必要があります。"threaded"や"domain threaded"のcgroupであっても、いきなりcgroup.threadsにプロセスやスレッドのIDは登録できません。

このスレッドグループリーダのPID(=TGID)である907図3で作ったcgroup tdに登録してみます。

# echo 907 > /sys/fs/cgroup/td/cgroup.procs
(スレッドグループリーダのPIDをdomain threadedのcgroup.procsに登録)
# cat /sys/fs/cgroup/td/cgroup.procs 
907
(cgroup.procsにはスレッドグループリーダのPID(TGID)のみ登録されている)
# cat /sys/fs/cgroup/td/cgroup.threads 
907
908
909
(cgroup.threadsには同じプロセスに属するスレッドも含めて登録されている)

スレッドグループリーダの907cgroup.procsに登録すると、cgroup.procsにはそのPIDが登録され、cgroup.threadsにはその子となるスレッドのID(PID)である908909が登録されています。

cgroup.procsへの登録は"domain threaded'であるcgroupへの登録である必要はなく、いきなり"threaded"であるth01th02への登録でも構いません。次の操作は"threaded"であるth01へマルチスレッドプロセスを登録している例です。

# echo 907 > /sys/fs/cgroup/cgroup.procs
(前の例の後に実行するために一度プロセスをrootに戻す)
# echo 907 > /sys/fs/cgroup/td/th01/cgroup.procs 
(threadedであるth01へマルチスレッドプロセスを登録)
# cat /sys/fs/cgroup/td/th01/cgroup.threads 
907
908
909
(cgroup.threadsには同じプロセスに属するスレッドも含めて登録されている)

上の例で、cgroupに登録されたプロセスとスレッドのIDを確認するためにcgroup.threadsの内容を見ています。これは"threaded"なcgroupのcgroup.procsファイルは読み取りできないからです。cgroup.procsを読もうとすると次のようにエラーになります。

"domain threaded"であるcgroupのcgroup.procsは先の例で実行したように読み取れます。

# cat /sys/fs/cgroup/td/th01/cgroup.procs 
cat: /sys/fs/cgroup/td/th01/cgroup.procs: Operation not supported

この例ではスレッドグループリーダのPIDを登録しました。代わりにスレッドのIDをcgroup.procsに登録しても同じ結果となります。上の例のマルチスレッドプロセス内のスレッドのPIDである909cgroup.procsに登録してみましょう。

# echo 909 > /sys/fs/cgroup/td/cgroup.procs 
(マルチスレッド内のスレッドのPIDをcgroup.procsに登録)
# cat /sys/fs/cgroup/td/cgroup.procs 
907
(cgroup.procsにはスレッドグループリーダのPIDが登録されている)
# cat /sys/fs/cgroup/td/cgroup.threads 
907
908
909
(cgroup.threadsにはスレッドグループリーダのPIDと各スレッドのPIDが登録されている)

このように、マルチスレッドプロセス内のスレッドのPIDを登録しても、cgroup.procsではTGIDが表示され、cgroup.threadsにはスレッドグループ内のスレッドすべてが表示されていることがわかります。

先に「まずはプロセスをcgroup.procsに登録する必要があります」と書きました。これを確認しておきましょう。

プロセスをcgroup.procsではなく、いきなりcgroup.threadsに登録を試みます。先の例に続けて実行する場合は、先と同様に一度プロセスをrootに戻して試してください。

# echo 907 > /sys/fs/cgroup/cgroup.procs
(前の例の後に実行するために一度プロセスをrootに戻す)
# echo 907 > /sys/fs/cgroup/td/cgroup.threads 
bash: echo: write error: Operation not supported
# echo 907 > /sys/fs/cgroup/td/th01/cgroup.threads 
bash: echo: write error: Operation not supported
(エラーになる)

このようにいきなりcgroup.threadsに登録しようとするとエラーとなります。

一度TGIDをスレッド化サブツリーに登録したあとは、スレッド化サブツリー内で自由に移動できることを確認してみましょう。

# echo 907 > /sys/fs/cgroup/td/cgroup.procs 
(マルチスレッドプロセスをdomain threadedに登録)
# cat /sys/fs/cgroup/td/cgroup.procs 
907
(スレッドグループリーダのPIDが登録された)
# cat /sys/fs/cgroup/td/cgroup.threads 
907
908
909
(スレッドグループリーダのPIDとスレッドのPIDが登録されている)
# echo 908 > /sys/fs/cgroup/td/th01/cgroup.threads 
(スレッドをth01に移動)
# cat /sys/fs/cgroup/td/th01/cgroup.threads 
908 (移動された)
# echo 909 > /sys/fs/cgroup/td/th02/cgroup.threads 
(別のスレッドをth02に移動)
# cat /sys/fs/cgroup/td/th02/cgroup.threads 
909 (移動された)

ここでスレッドのPIDをcgroup.threadsでなくcgroup.procsに登録すると、マルチスレッドプロセス全体が移動してしまうことに注意してください。

# echo 909 > /sys/fs/cgroup/td/th01/cgroup.procs   
(スレッドグループ内のスレッドのPIDをth01のcgroup.procsに登録)
# cat /sys/fs/cgroup/td/th01/cgroup.threads 
908
907
909
(登録したスレッドだけでなく属するプロセス全体が移動した)

色々試して頭が混乱したかもしれませんね。ここまでに試したことをまとめておきましょう。

  • マルチスレッドプロセスをスレッド化サブツリーに移動する場合は、まずはスレッド化サブツリー内のcgroupのcgroup.procsに登録する必要がある
    • 登録するIDはスレッドグループ内のスレッドのPIDでも良い
  • "threaded"なcgroupのcgroup.procsは読み取れない。登録されているIDを確認するにはcgroup.threadsを読む必要がある
    • domain threadedなcgroupのcgroup.procsは読み取れる
  • スレッドグループ内のスレッドグループリーダ以外のスレッドのPIDであってもcgroup.procsへの書き込みではマルチスレッドプロセス全体が移動する

プロセスが存在するcgroupへのスレッドコントローラの登録によるスレッド化サブツリーの作成

スレッド化サブツリーを作る方法はもう1つあります。

この方法では次の2つの手順が必要です。いずれも同じcgroupに対して操作を行います。

  • "domain"タイプであるcgroupでスレッドコントローラ有効にし、その子cgroupでもスレッドコントローラを有効にする(該当cgroupとその親cgroup両方のcgroup.subtree_controllにスレッドコントローラが登録された状態)
  • プロセスをcgroupに登録する

この手順の順番はどちらが先でも問題ありません。この結果は次のようになります。

  • 該当のcgroupは"domain threaded"なcgroupとなる
  • 上記の"domain threaded"なcgroup配下の"domain" cgroupは"domain invalid"となる

cgroup v2の当初の仕様では、すでにプロセスが登録されているcgroupのサブツリーでコントローラを有効にできませんでした。しかし、スレッド化サブツリーでは自身と子孫のcgroupでコントローラを有効にでき、スレッド化サブツリーを作成できます。

試してみましょう。

まずは先ほどの例と同様のツリーを作成します。そしてtdの親cgroup(ここの例ではroot cgroup)内のcgroup.subtree_controlファイルにcpuコントローラを登録します。cgroupでコントローラを有効化するあたりのお話は連載の第38回をご覧ください。

# mkdir -p td/th{01,02}
# tree -d .
.
└── td
    ├── th01
    └── th02

3 directories
# echo "+cpu" > cgroup.subtree_control 
# cat cgroup.subtree_control 
cpu
# cat td/cgroup.controllers 
cpu (tdでcpuコントローラが有効になった)

そしてcgroup tdにプロセスを登録します。

# echo $$ > td/cgroup.procs 
# cat td/cgroup.procs 
912
938
# cat td/cgroup.type
domain

この時点ではまだcgroup tdのタイプは"domain"です図4⁠。

図4 スレッドコントローラを登録して末端ではないcgroupにプロセスを登録

図4 スレッドコントローラを登録して末端ではないcgroupにプロセスを登録

ここでtdcgroup.subtree_controlファイルにCPUコントローラを登録し、tdの子孫cgroupでCPUコントローラが使えるようにしてみます。

# echo "+cpu" > td/cgroup.subtree_control 
# cat td/cgroup.subtree_control 
cpu
# cat td/th{01,02}/cgroup.controllers
cpu
cpu
図5 cgroup.subtree_controlファイルにスレッドコントローラを登録
図5 cgroup.subtree_controlファイルにスレッドコントローラを登録

tdcgroup.subtree_controlファイルにCPUコントローラが登録され、その子cgroupであるth01th02cgroup.controllersファイルにはcpuが登録されており、CPUコントローラが使える状態になっています。

ここでおもむろにtdcgroup.typeファイルを確認して、tdのcgroupタイプを確認してみましょう。

# cat td/cgroup.type 
domain threaded

プロセスが所属し、なおかつCPUコントローラが使える状態となったtdはdomain threadedになっています。その子cgroupであるth01th02のタイプを確認してみましょう。

# cat td/th{01,02}/cgroup.type
domain invalid
domain invalid

親となるtdが"domain threaded"となったので、元々domainであった子cgroup th01th02はいずれも"domain invalid"となっています。スレッド化サブツリーを完成させるには、この2つのcgroupをthreadedに変えましょう。

図6 tdがdomain threadedに変化
図6 tdがdomain threadedに変化
# echo "threaded" > td/th01/cgroup.type
# echo "threaded" > td/th02/cgroup.type
# cat td/th{01,02}/cgroup.type
threaded
threaded

th01th02がthreadedに設定されました。これでスレッド化サブツリーの完成です。

図7 スレッド化サプツリーの完成
図7 スレッド化サプツリーの完成

cgroup.typeファイルへの書き込みでスレッド化サブツリーを作成した際の例と同様にスレッドを登録してみましょう。

# pstree -p 928
threadtest(928)─┬─{threadtest}(929)
                └─{threadtest}(930)
# echo 928 > /sys/fs/cgroup/td/cgroup.procs 
# echo 929 > /sys/fs/cgroup/td/th01/cgroup.threads 
# echo 930 > /sys/fs/cgroup/td/th02/cgroup.threads 
(プロセスとスレッドを同一スレッド化サブツリーのそれぞれ別々のcgroupに登録)
# cat /sys/fs/cgroup/td/cgroup.procs 
893
928 (←登録できている)
939
# cat /sys/fs/cgroup/td/th01/cgroup.threads 
929 (←登録できている)
# cat /sys/fs/cgroup/td/th02/cgroup.threads 
930 (←登録できている)

親プロセスのPIDとそのプロセスに所属するスレッドが、同一のスレッド化サブツリーの別々のcgroupに登録できていることがわかります。

cgroup.threadsファイルへのタスクの登録権限

第40回で、cgroup v2を操作する際の権限についての説明をしました。

今回の実行例では、いずれもroot権限で操作を行っていますので、特に気にする必要はありません。しかし、一般ユーザ権限でプロセスやスレッドの操作を行う場合は、第40回での説明と同様に必要な権限を持っているかどうかを考える必要があります。

つまり、cgroup.threadsファイルへの書き込みには第40回で説明した、cgroupへのプロセス登録の際と同様の権限が必要です。

  • 書き込む先のcgroup.threadsファイルへの書き込み権
  • 移動元と移動先のcgroupの共通の祖先にあるcgroup.procsファイルへの書き込み権

そして、前回説明したとおり、同じプロセス内のスレッドは同じスレッド化サブツリーに所属する必要がありますので、

  • 移動元と移動先のcgroupは同じスレッド化サブツリー内に所属している必要がある

という条件が加わります。

root cgroupの扱い

これまでの例では、root cgroupの下に"domain threaded"となるcgroupを作成し、その配下に"threaded"なcgroupを作りました。

では、root直下に"threaded"なcgroupを作成できるのでしょうか? 試してみましょう。

root直下にth01というcgroupを作成し、"threaded"に変更します。

# mkdir /sys/fs/cgroup/th01
# cd /sys/fs/cgroup/
# echo threaded > th01/cgroup.type 
(root直下のth01をthreadedに変更する)
# cat th01/cgroup.type 
threaded
(threadedに変更された)

このth01に、これまでの実行例と同様のマルチスレッドプロセスを登録してみます。

# pstree -p 902
threadtest(902)─┬─{threadtest}(903)
                └─{threadtest}(904)
# echo 902 > th01/cgroup.procs (マルチスレッドプロセスをth01に登録)
# cat th01/cgroup.threads 
902
903
904
(プロセス内のタスクがすべて登録された)

このように何の問題もなく登録できました。つまりroot直下に"threaded" cgroupを作成できるということです。

ここでスレッドを1つrootに移動してみましょう。

# echo 904 > /sys/fs/cgroup/cgroup.threads 
(スレッドの1つをrootに移動)
# grep 904 cgroup.threads 
904 (rootに移動した)
# cat th01/cgroup.threads 
902
903
(th01には残りのタスクが残っている)

移動したスレッドだけがrootに移動し、残りのタスクはth01 cgroupに残っています。

つまり、root直下に"threaded" cgroupを作ると、root cgroupが"domain threaded"と同じ働きをします。そして"domain threaded"なcgroupが存在する場合と同様に操作できます。

root cgroupは"domain"と"threaded"なcgroupの親になります。このようにroot cgroupを例外的に扱っているのは、"threaded"なcgroupをroot直下に作ることでなるべく階層を浅くして、cgroup階層を走査するコストを下げるためです。

図8 root直下のcgroupを"threaded"に変更する
図8 root直下のcgroupを

もし"threaded"に変化したroot直下のcgroupが子cgroupを持っていた場合は、子cgroupは"domain invalid"に変化します。その子cgroupを使用するためには、そのcgroupを"threaded"に変更する必要があります。

# mkdir -p th02/th03
(2階層のcgroupを作成する)
# echo threaded > th02/cgroup.type 
(親cgroupをthreadedに変更する)
# cat th02/cgroup.type 
threaded
# cat th02/th03/cgroup.type 
domain invalid
(子cgroupはdomain invalidに変化する)

まとめ

今回はスレッド化サブツリーの操作について説明をしました。

スレッド化サブツリーを作成するには2つの方法がありました。

  • cgroup.typeファイルにthreadedという文字列を書き込む
  • プロセスが存在するcgroupへスレッドコントローラーを登録する

そして、スレッド化サブツリー内のプロセス、スレッドの操作方法や権限、root cgroupの例外的な扱いについても説明しました。

今回でcgroup v2のスレッドモードの説明は終わりです。前回と今回の説明で、スレッドモードがcgroup v2の仕様とうまく共存できるように作られていることが理解できたのではないでしょうか。

おすすめ記事

記事・ニュース一覧