Kubernetes コントリビューションに役立ったカーネル技術 cgroupv2 解析編

##はじめに
cgroupv2 のコンテナ基盤への適用を検証する中で kubeadm の preflight checks の修正が必要であることがわかったため、下記にて対応した。

https://github.com/kubernetes/system-validators/pull/12

具体的な修正内容は、 cgrpupv2 からサブシステム情報の取得先を変更する(/proc/cgroups は obsolete で、 /sys/fs/cgroup/cgroup.controllers から取得したい)ものである。幸い runC では同様の問題がすでに修正されており、下記のコードを流用する形で kubeadm に Pull Request した。

https://github.com/opencontainers/runc/commit/74a3fe5d1b894f01bcef94469f76ca62df80948a

この Pull Request に対して、本件に関してカーネル内部的には有効化されているが /sys/fs/cgroup/cgroup.controllers には表示されないサブシステムが存在する問題があり、それについては理由をソースコードコメント上に追加するようフィードバックを受けた。しかし、公式のドキュメント等からすぐに明確な仕様や情報を見つけることができなかったため、今回は cgroupv2 の挙動をカーネルコードを読み、 kprobe で机上調査から想定する動作を実際に確認した。
この記事では調査時のメモを元に、カーネル解析の手順を説明する。
ソースコード調査対象カーネルは5.3とする。

##/sys/fs/cgroup/cgroup.controllers を読み出した時のカーネル側の挙動
まず、 /sys/fs/cgroup/cgroup.controllers をユーザ空間から読み込んだ時に実行されるカーネル空間上のコードを調べた。
関連するインターフェースは、下記で実装されていた。

https://github.com/torvalds/linux/blob/v5.3/kernel/cgroup/cgroup.c#L4864

そこで、 .seq_show に設定されている [cgroup_controllers_show 関数](https://github.com/torvalds/linux/blob/v5.3/kernel/cgroup/cgroup.c#L2905 "cgroup_controllers_show 関数") の周辺を少し読んでみると、 [cgroup_control 関数](https://github.com/torvalds/linux/blob/v5.3/kernel/cgroup/cgroup.c#L418 "cgroup_control 関数") で表示するサブシステムを選別していることがわかった。また、表示するサブシステムの一部を cgrp_dfl_implicit_ss_mask と cgrp_dfl_inhibit_ss_mask を or して反転した値を使い選別していた。

https://github.com/torvalds/linux/blob/v5.3/kernel/cgroup/cgroup.c#L433-L434

そこで、下記の変数にどのような値がどのようにセットされるのかを調べることにした。

* [cgrp_dfl_implicit_ss_mask](https://github.com/torvalds/linux/blob/v5.3/kernel/cgroup/cgroup.c#L174 "cgrp_dfl_implicit_ss_mask") 
* [cgrp_dfl_inhibit_ss_mask](https://github.com/torvalds/linux/blob/v5.3/kernel/cgroup/cgroup.c#L171 "cgrp_dfl_inhibit_ss_mask") 

##システムブート時の cgroupv2 初期設定
まず

* [cgrp_dfl_implicit_ss_mask](https://github.com/torvalds/linux/blob/v5.3/kernel/cgroup/cgroup.c#L174 "cgrp_dfl_implicit_ss_mask") 
* [cgrp_dfl_inhibit_ss_mask](https://github.com/torvalds/linux/blob/v5.3/kernel/cgroup/cgroup.c#L171 "cgrp_dfl_inhibit_ss_mask") 

のビットが実際に設定される場所を調べた。
すると、同じソースファイル内からカーネルのブート時にサブシステム単位でビットが設定されるコードを見つけた。
https://github.com/torvalds/linux/blob/v5.3/kernel/cgroup/cgroup.c#L5783-L5786

```c
	for_each_subsys(ss, ssid) {
(中略)
		if (ss->implicit_on_dfl)
			cgrp_dfl_implicit_ss_mask |= 1 << ss->id;
		else if (!ss->dfl_cftypes)
			cgrp_dfl_inhibit_ss_mask |= 1 << ss->id;
```

詳細は割愛するが、 [cgroup_subsys 構造体](https://github.com/torvalds/linux/blob/v5.3/include/linux/cgroup-defs.h#L629 "cgroup_subsys 構造体") が持つ implicit_on_dfl と dfl_cftypes の有無でビットを立てる仕組みになっている。まず、 implicit_on_dfl が存在すれば、そのサブシステムの ID 番目のビットを cgrp_dfl_implicit_ss_mask に立て、それがない場合は、 dfl_cftypes が存在しないサブシステムの ID 番目のビットを cgrp_dfl_inhibit_ss_mask に立てる。

##kprobes で実際にビットマスクを確認
カーネルのコードを読んだことで /sys/fs/cgroup/cgroup.controllers をユーザ空間から読み込んだ時の挙動を理解できた。そこで、実際に稼働中のカーネルから想定通りビットがセットされているか確認してみる。

###検証環境
* Ubuntu 18.04(x86_64, kernel : 5.0.0-32-generic)
* cgroupv2
* Kubernetes : v1.15.2

本検証では ftrace を tracefs から直接使ってトレースする。そのため、 tracefs をマウント出来ていれば、その他に必要なツールはない。なお tracefs は、 /sys/kernel/debug/tracing にマウントされているものとする。

###cgroupv2 サブシステム設定に関する注意
現状 cgroupv1 に各サブシステムがマウントされていることで、 cgroupv2 にサブシステムがマウントできない環境も多いと思う。
筆者の環境では検証のために cgroup_no_v1=all をカーネルのコマンドラインパラメータに追加したが、本番環境での利用を推奨するものではない。

https://www.kernel.org/doc/html/v5.3/admin-guide/kernel-parameters.html

###cgroup_control 関数のプローブ
それでは、 cgroup_control 関数にフックしてみる。
今回は戻り値のビットマスクを確認したいので、 kretprobe を設定する。

```shell
# echo 'r:retcgcontrol cgroup_control ret=$retval:u16' > /sys/kernel/debug/tracing/kprobe_events
# echo 1 > /sys/kernel/debug/tracing/events/kprobes/retcgcontrol/enable 
# cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory pids rdma
# cat /sys/kernel/debug/tracing/trace | grep cat
             cat-xxxx  [xxx] d...   xxx.xxxxxx: retcgcontrol: (cgroup_controllers_show+0x44/0x60 <- cgroup_control) ret=6171
```

サブシステム「cpuset cpu io memory pids rdma」が表示された時に、 cgroup_control 関数の戻り値は 6171(1100000011011) であることがわかった。そこで、この6つのビットが「cpuset cpu io memory pids rdma」を指しているか確認する。
cgroup のサブシステムIDは、[cgroup_subsys.h](https://github.com/torvalds/linux/blob/v5.3/include/linux/cgroup_subsys.h "cgroup_subsys.h") での登場順に振られていく。詳しい実装については下記サイトが参考になる。

[cgroup の SUBSYS マクロ]
(https://tenforward.hatenablog.com/entry/2017/03/16/200009 "cgroup の SUBSYS マクロ") 

6つのビットに相当するサブシステムを確認すると、「cpuset, cpu, io, memory, pids, rdma」であることがわかった。

###cgrp_dfl_implicit_ss_mask と cgrp_dfl_inhibit_ss_mask の値を表示
表示されるサブシステムはわかったが、cgrp_dfl_implicit_ss_mask と cgrp_dfl_inhibit_ss_mask のどちらにどのサブシステムのビットが立っているのかも気になった。
そこで、これらの値も動的にダンプしてみる。
まずは、 cgrp_dfl_implicit_ss_mask と cgrp_dfl_inhibit_ss_mask のアドレスを取得する。<font color="Red">(2020/02/17 追記) ftrace は直接シンボルからアドレスを取得できたので下記は不要。むしろ KASLR が面倒なので、シンボルをそのまま記載した方が良さそう。</font>

```shell
# cat /proc/kallsyms | grep grp_dfl_implicit_ss_mask
ffffffffa3fdd8a6 b cgrp_dfl_implicit_ss_mask
# cat /proc/kallsyms | grep cgrp_dfl_inhibit_ss_mask
ffffffffa3fdd8a8 b cgrp_dfl_inhibit_ss_mask
```

次に、 cgroup_control 関数プローブの手順にアドレスのダンプを組み込んで実行する。

```shell
# echo 'r:retcgcontrol cgroup_control cgrp_dfl_implicit_ss_mask=@0xffffffffa3fdd8a6:u16 cgrp_dfl_inhibit_ss_mask=@0xffffffffa3fdd8a8:u16 ret=$retval:u16' > /sys/kernel/debug/tracing/kprobe_events
# echo 1 > /sys/kernel/debug/tracing/events/kprobes/retcgcontrol/enable 
# cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory pids rdma
# cat /sys/kernel/debug/tracing/trace | grep cat
             cat-xxxx [000] d...  xxxx.xxxxxx: retcgcontrol: (cgroup_controllers_show+0x44/0x60 <- cgroup_control) cgrp_dfl_implicit_ss_mask=256 cgrp_dfl_inhibit_ss_mask=1764 ret=6171
```

<font color="Red">(2020/02/17 追記) ftrace で直接シンボルからアドレスを取得</font>

```shell
# echo 'r:retcgcontrol cgroup_control cgrp_dfl_implicit_ss_mask=@cgrp_dfl_implicit_ss_mask:u16 cgrp_dfl_inhibit_ss_mask=@cgrp_dfl_inhibit_ss_mask:u16 ret=$retval:u16' > /sys/kernel/debug/tracing/kprobe_events
# echo 1 > /sys/kernel/debug/tracing/events/kprobes/retcgcontrol/enable 
# cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory pids rdma
# cat /sys/kernel/debug/tracing/trace | grep cat
             cat-xxxx [000] d...  xxxx.xxxxxx: retcgcontrol: (cgroup_controllers_show+0x44/0x60 <- cgroup_control) cgrp_dfl_implicit_ss_mask=256 cgrp_dfl_inhibit_ss_mask=1764 ret=6171
```

cgrp_dfl_implicit_ss_mask=256(100000000) と cgrp_dfl_inhibit_ss_mask=1764(11011100100)が表示された。[cgroup_subsys.h](https://github.com/torvalds/linux/blob/v5.3/include/linux/cgroup_subsys.h "cgroup_subsys.h") から、 cgrp_dfl_implicit_ss_mask は「perf_event」、 cgrp_dfl_inhibit_ss_mask は「cpuacct, devices, freezer, net_cls, net_prio, hugetlb」をマスクしていることがわかった。

###(おまけ)perf_event の implicit_on_dfl
現状、 perf_event のみが暗黙的に有効化されるフラグが立っていることがわかったが、その設定場所は https://github.com/torvalds/linux/blob/v5.3/kernel/events/core.c#L12216 に存在した。

```c
	/*
	 * Implicitly enable on dfl hierarchy so that perf events can
	 * always be filtered by cgroup2 path as long as perf_event
	 * controller is not mounted on a legacy hierarchy.
	 */
	.implicit_on_dfl = true,
```

##まとめ
/sys/fs/cgroup/cgroup.controllers の表示に関わる変数についてまとめると下記のようになる。

|  | cpuset | cpu | cpuacct | io | memory | devices | freezer | net_cls | perf_event | net_prio | hugetlb | pids | rdma |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| cgrp_dfl_implicit_ss_mask | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| cgrp_dfl_inhibit_ss_mask | 0 | 0 | 1 | 0 | 0 | 1 | 1 | 1 | 0 | 1 | 1 | 0 | 0 |

この２つの変数のどちらにもマスクされていないサブシステムだけが、 /sys/fs/cgroup/cgroup.controllers で表示されることになる。

##おわりに
筆者も cgroupv2 に詳しいわけではないが、カーネルの知見やデバッグ技術を生かして、 Kubernetes コントリビューションに必要な情報をカーネル無改造で迅速に調べることができた。
本件に限らず、ユーザ空間とは異なる視点から情報を取得することで思いもしなかった問題を発見したり解決できたりすることが多々ある。需要があれば本件の詳細に加えてどこかで紹介したい。