Docker 等でコンテナを作成する際、Linux カーネル機能の一つである cgroups が使われます。youki という OCI Runtime を中心に色々見ていく中で実際にコンテナ作成時にどのような cgroup が用意されるのかといったことがわかってきたので知識の整理として吐き出してみました。

主な目的:

  • コンテナでどのように cgroup, cgroup fs のマウント、および cgroup namespace が設定されるかの把握

なお cgroups 自体の説明はあまりここにはありません。 ページ末尾にいくつか参考になるリンクを記載しています。

コンテナにおける cgroups の利用目的

cgroups はコンテナで利用できるリソース (e.g. CPU やメモリ) に制限をかけるために使用されます。

例えば

docker container run --memory=200m -it --rm ubuntu:20.04 /bin/bash

は Docker にメモリ使用量の上限を 200 MB に設定したコンテナを作成するよう指示します。

実際にコンテナ用に作成された cgroup の内容を確認すると memory.max に指定した値が設定されていることが確認できます (ここでは cgroup v2 環境を利用)。

$ cat /sys/fs/cgroup/system.slice/docker-0adcd26d10322b943d3a5d0e507831695b385d0116da3c52a62caecca32d5f07.scope/memory.max
209715200

cgroup fs について

上記手順でさらっと /sys/fs/cgroup 以下のファイルにアクセスしていましたが、このように cgroup への操作は cgroup ファイルシステム (cgroupfs) を介して行います。

cgroup には v1, v2 がありそれによりファイルシステムの階層構造が異なります。

cgroup v1

cgroup v1 では cgroup で管理する各リソース (cgroup ではこれをコントローラあるいはサブシステムと呼ぶようです。ここでは以降コントローラと呼びます) 毎に cgroupfs をマウントします。

以下は CentOS 7 上で cgroup に関連するマウントポイントを表示した例です。 通常 cgroup 関連のマウント先には /sys/fs/cgroup 以下が使われます。

$ sudo mount -l | grep cgroup
tmpfs on /sys/fs/cgroup type tmpfs (ro,nosuid,nodev,noexec,seclabel,mode=755)
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,pids)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,net_prio,net_cls)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,devices)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,perf_event)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,memory)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,blkio)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,freezer)

cgroup は各コントローラで独立に管理されるので例えば cpuset コントローラでは A という cgroup に所属するが memory コントローラでは B に所属するといったことができます。

なお自身の所属する cgroup 一覧は /proc/<pid>/cgroup で確認できます。

$ cat /proc/self/cgroup
11:memory:/user.slice/user-1000.slice/session-6.scope
10:devices:/user.slice
9:net_cls,net_prio:/
8:cpu,cpuacct:/
7:hugetlb:/
6:perf_event:/
5:freezer:/
4:cpuset:/
3:pids:/user.slice/user-1000.slice/session-6.scope
2:blkio:/
1:name=systemd:/user.slice/user-1000.slice/session-6.scope
0::/user.slice/user-1000.slice/session-6.scope

cgroup v2

cgroup v2 では v1 と違い各コントローラ毎に cgroup に所属するのではなく単一の cgroup に所属しそこで各コントローラによるリソース制御を行います。

システムで cgroup v2 ファイルシステムを利用しているかは cgroup2 タイプのマウントが存在するかで判断できます。

$ sudo mount -l | grep cgroup
cgroup2 on /sys/fs/cgroup type cgroup2 (rw,nosuid,nodev,noexec,relatime)

現在のプロセスが所属する cgroup を見ると一つしか表示されません。

$ cat /proc/self/cgroup
0::/user.slice/user-1000.slice/session-2.scope

ある cgroup でどのコントローラがサポートされているかは cgroup.controllers から判断できます。

$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids rdma misc

また cgroup は階層構造を持つ (cgroup に親子関係が存在する) のですが、子の cgroup でどのコントローラを有効にするかを cgroup.subtree_control で制御できます。

$ cat /sys/fs/cgroup/cgroup.subtree_control
cpuset cpu io memory hugetlb pids rdma misc

cgroup namespace について

namespace はコンテナという隔離された環境を作るために用いられる Linux カーネル機能の一つです。 扱いたいリソース毎に namespace が存在し、その中に cgroup namespace というのが存在します。

これは文字通り cgroup 用の namespace であり、実際のルートとは異なる cgroup をルートとみなした環境でプログラムを実行できます。

cgroup namespace の動作例としてここでは unshare を使ってみます。unshare コマンドでは --cgroup オプションにより、unshare を呼び出したプロセスが所属する cgroup をルートにした新しい cgroup namespace でコマンド実行を行います。

# with cgroup v2

# cgroup の作成
[sample]$ sudo mkdir /sys/fs/cgroup/mytest

# 作成した cgroup に所属
[sample]$ sudo bash -c "echo $$ > /sys/fs/cgroup/mytest/cgroup.procs"
[sample]$ cat /proc/self/cgroup
0::/mytest
[sample]$ ls /sys/fs/cgroup
cgroup.controllers      cgroup.threads         dev-mqueue.mount  io.stat           sys-fs-fuse-connections.mount
cgroup.max.depth        cpu.pressure           init.scope        memory.numa_stat  sys-kernel-config.mount
cgroup.max.descendants  cpuset.cpus.effective  io.cost.model     memory.pressure   sys-kernel-debug.mount
cgroup.procs            cpuset.mems.effective  io.cost.qos       memory.stat       sys-kernel-tracing.mount
cgroup.stat             cpu.stat               io.pressure       misc.capacity     system.slice
cgroup.subtree_control  dev-hugepages.mount    io.prio.class     mytest            user.slice

# 新しい mount, cgroup namespace でコマンド実行
[sample]$ sudo unshare --mount --cgroup /bin/bash

# cgroupfs のマウントし直し
[sample]# mount --make-rprivate /
[sample]# umount /sys/fs/cgroup
[sample]# mount -t cgroup2 none /sys/fs/cgroup

# cgroup の確認
# 自身の所属していた cgroup がルートになっている
[sample]# cat /proc/self/cgroup
0::/
[sample]# ls /sys/fs/cgroup
cgroup.controllers      cpuset.cpus               hugetlb.1GB.rsvd.current  io.stat              memory.swap.events
cgroup.events           cpuset.cpus.effective     hugetlb.1GB.rsvd.max      io.weight            memory.swap.high
cgroup.freeze           cpuset.cpus.partition     hugetlb.2MB.current       memory.current       memory.swap.max
cgroup.kill             cpuset.mems               hugetlb.2MB.events        memory.events        misc.current
cgroup.max.depth        cpuset.mems.effective     hugetlb.2MB.events.local  memory.events.local  misc.max
cgroup.max.descendants  cpu.stat                  hugetlb.2MB.max           memory.high          pids.current
cgroup.procs            cpu.uclamp.max            hugetlb.2MB.rsvd.current  memory.low           pids.events
cgroup.stat             cpu.uclamp.min            hugetlb.2MB.rsvd.max      memory.max           pids.max
cgroup.subtree_control  cpu.weight                io.bfq.weight             memory.min           rdma.current
cgroup.threads          cpu.weight.nice           io.latency                memory.numa_stat     rdma.max
cgroup.type             hugetlb.1GB.current       io.low                    memory.oom.group
cpu.max                 hugetlb.1GB.events        io.max                    memory.pressure
cpu.max.burst           hugetlb.1GB.events.local  io.pressure               memory.stat
cpu.pressure            hugetlb.1GB.max           io.prio.class             memory.swap.current

Docker 経由のコンテナ作成における cgroup

Docker によって用意されるコンテナでここまで見てきた cgroup, cgroupfs のマウント、cgroup namespace の設定がどのように扱われるかを調べてみました。

Docker の実装ではコンテナの作成は最終的に OCI Runtime (e.g. runc) に委ねられます (参考)。 このとき OCI Runtime にはどのようなコンテナを作成してほしいかを記述した OCI Runtime Spec を渡します。

Docker のソースを見ると moby/oci/defaults.go でデフォルトの spec が定義されています。コンテナ作成のリクエストハンドラである api/server/router/container_router.gopostContainersStart から始まる処理も併せて確認すると、デフォルトでは cgroup 関連の設定は以下のようになります。

  • コンテナ毎に cgroup は新しく切られる
  • cgroup v1, v2 に依らず cgroupfs はマウントされる
  • cgroup namespace については cgroup v1, v2 で異なる
    • cgroup v1 では cgroup namespace は新しく切られない
      • その場合 OCI Runtime 側で bind マウントにより cgroup の隔離が行われる
    • cgroup v2 では切られる

以下では cgroup v1, v2 それぞれについてより詳細を見ていきます。

cgroup v1 環境 (legacy)

cgroup

cgroup v1 環境においては上述のように各コントローラ毎に cgroup が存在します。どこに cgroup を作成するかは OCI Runtime Spec の linux.cgroupsPath で指定することができ、Docker 越しだと /docker/<container_id> となっています。

下記は実際に Docker (with youki) で作成されたコンテナの cgroup 確認例です。

# OCI Runtime に youki を指定してコンテナ実行
# Runtime の追加は https://github.com/containers/youki に従って行った
$ sudo docker container run --rm -it --runtime=youki busybox /bin/sh

# 別ターミナルで (ホストからはコンテナが pid=12129 として見えている)
$ cat /proc/12129/cgroup
11:memory:/docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b
10:devices:/docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b
9:net_cls,net_prio:/docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b
8:cpu,cpuacct:/docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b
7:hugetlb:/docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b
6:perf_event:/docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b
5:freezer:/docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b
4:cpuset:/docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b
3:pids:/docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b
2:blkio:/docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b
1:name=systemd:/user.slice/user-1000.slice/session-5.scope
0::/user.slice/user-1000.slice/session-5.scope

cgroupfs

Docker 経由で作成されるコンテナ内でも cgroupfs はマウントされます (あるいは少なくともそのようにコンテナには見せる、という方が正確かもしれません。詳細は次の cgroup namespace 項)。OCI Runtime Spec の mounts 内で cgroup type のマウントを含めることで作成を指示します。

"mounts": [
  ...
  {
    "destination": "/sys/fs/cgroup",
    "type": "cgroup",
    "source": "cgroup",
    "options": [
      "ro",
      "nosuid",
      "noexec",
      "nodev"
    ]
  },
  ...
],

作成されたコンテナに相当するプロセスの mountinfo を見ると実際にマウントされた内容が確認できます。

$ cat /proc/12129/mountinfo | grep cgroup
567 566 0:60 / /sys/fs/cgroup rw,nosuid,nodev,noexec,relatime - tmpfs tmpfs rw,seclabel,mode=755
568 567 0:28 /user.slice/user-1000.slice/session-5.scope /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:6 - cgroup cgroup rw,seclabel,xattr,name=systemd
569 567 0:31 /docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:7 - cgroup cgroup rw,seclabel,blkio
570 567 0:32 /docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:8 - cgroup cgroup rw,seclabel,pids
571 567 0:33 /docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:9 - cgroup cgroup rw,seclabel,cpuset
726 567 0:34 /docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:10 - cgroup cgroup rw,seclabel,freezer
727 567 0:35 /docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:11 - cgroup cgroup rw,seclabel,perf_event
728 567 0:36 /docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:12 - cgroup cgroup rw,seclabel,hugetlb
729 567 0:37 /docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:13 - cgroup cgroup rw,seclabel,cpu,cpuacct
730 567 0:38 /docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:14 - cgroup cgroup rw,seclabel,net_cls,net_prio
731 567 0:39 /docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:15 - cgroup cgroup rw,seclabel,devices
732 567 0:40 /docker/2f7f1f4fe4865f25c728dd6dbdcbf243b7ea968aef3902b90a2ca2580ea5324b /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:16 - cgroup cgroup rw,seclabel,memory

cgroup namespace

cgroup v1 ではどうやらコンテナ用に新しく cgroup namespace を切ることはしないようです。

実際にコンテナに相当するプロセスの cgroup namespace を確認するとホストのそれと一致します。

# namespaces of host
$ sudo ls -l /proc/self/ns
total 0
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 ipc -> 'ipc:[4026531839]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 mnt -> 'mnt:[4026531840]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 net -> 'net:[4026531992]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 pid -> 'pid:[4026531836]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 pid_for_children -> 'pid:[4026531836]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 time -> 'time:[4026531834]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 user -> 'user:[4026531837]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 uts -> 'uts:[4026531838]'

# namespaes of container
$ sudo ls -l /proc/12129/ns
total 0
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 cgroup -> 'cgroup:[4026531835]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 ipc -> 'ipc:[4026532178]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 mnt -> 'mnt:[4026532252]'
lrwxrwxrwx. 1 root root 0 Nov  5 20:59 net -> 'net:[4026532181]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 pid -> 'pid:[4026532177]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 pid_for_children -> 'pid:[4026532177]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 time -> 'time:[4026531834]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 time_for_children -> 'time:[4026531834]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 user -> 'user:[4026531837]'
lrwxrwxrwx. 1 root root 0 Nov  5 21:32 uts -> 'uts:[4026532179]'

しかし一方でコンテナ向けに cgroupfs はマウントするよう指示しているので何も考慮しないとコンテナからホスト上の他の cgroup についての閲覧、操作ができてしまいそうです。

実際 unshare(1) を使ってそのような状況を再現してみました。 ここでは memory を /mytest という cgroup に所属させているのでそれをルートにしてコンテナ内では見せたいのですが、そうはなっておらずホストのルートがそのまま見えてしまっていることがわかります。

# memory コントローラ下に /mytest cgroup を作成し所属する
[mycontainer]$ sudo mkdir /sys/fs/cgroup/memory/mytest
[mycontainer]$ sudo bash -c "echo $$ > /sys/fs/cgroup/memory/mytest/tasks"
[mycontainer]$ cat /proc/$$/cgroup
11:memory:/mytest
10:devices:/user.slice
9:net_cls,net_prio:/
8:cpu,cpuacct:/
7:hugetlb:/
6:perf_event:/
5:freezer:/
4:cpuset:/
3:pids:/user.slice/user-1000.slice/session-6.scope
2:blkio:/
1:name=systemd:/user.slice/user-1000.slice/session-6.scope
0::/user.slice/user-1000.slice/session-6.scope

# 疑似コンテナ環境を用意する
# mount namespace のみを新しく切っている
[mycontainer]$ sudo unshare --mount /bin/bash
[mycontainer]# mount --make-rprivate /
[mycontainer]# mount --rbind ./rootfs ./rootfs
[mycontainer]# mount -t proc proc ./rootfs/proc

# cgroupfs のマウント
# cpuset, memory コントローラのみ用意
[mycontainer]# mkdir -p rootfs/sys/fs/cgroup
[mycontainer]# mount -t tmpfs cgroup_root rootfs/sys/fs/cgroup
[mycontainer]# mkdir rootfs/sys/fs/cgroup/cpuset
[mycontainer]# mount -t cgroup -ocpuset cpuset rootfs/sys/fs/cgroup/cpuset
[mycontainer]# mkdir rootfs/sys/fs/cgroup/memory
[mycontainer]# mount -t cgroup -omemory memory rootfs/sys/fs/cgroup/memory

# ルートファイルシステムを切り替え
[mycontainer]# mkdir rootfs/oldrootfs
[mycontainer]# pivot_root ./rootfs ./rootfs/oldrootfs
[mycontainer]# cd /
[/]# umount -l oldrootfs/

# コンテナ内からマウントポイント一覧の確認
[/]# cat /proc/self/mountinfo
540 504 8:1 /home/vagrant/sample/rootfs / rw,relatime - ext4 /dev/sda1 rw,seclabel
541 540 0:53 / /proc rw,relatime - proc proc rw
542 540 0:54 / /sys/fs/cgroup rw,relatime - tmpfs cgroup_root rw,seclabel
543 542 0:33 / /sys/fs/cgroup/cpuset rw,relatime - cgroup cpuset rw,seclabel,cpuset
544 542 0:40 / /sys/fs/cgroup/memory rw,relatime - cgroup memory rw,seclabel,memory

# コンテナ内から cgroup 一覧の確認
# (これはホストから見ても同じ)
[/]# cat /proc/self/cgroup
11:memory:/mytest
10:devices:/user.slice
9:net_cls,net_prio:/
8:cpu,cpuacct:/
7:hugetlb:/
6:perf_event:/
5:freezer:/
4:cpuset:/
3:pids:/user.slice/user-1000.slice/session-6.scope
2:blkio:/
1:name=systemd:/user.slice/user-1000.slice/session-6.scope
0::/user.slice/user-1000.slice/session-6.scope

[/]# ls -l /sys/fs/cgroup
total 0
dr-xr-xr-x    3 root     root             0 Nov  4 23:37 cpuset
dr-xr-xr-x    7 root     root             0 Nov  4 23:37 memory

# コンテナ内から cpuset コントローラのルート直下の一覧確認
[/]# ls -l /sys/fs/cgroup/cpuset
total 0
-rw-r--r--    1 root     root             0 Nov  5 21:41 cgroup.clone_children
-rw-r--r--    1 root     root             0 Nov  5 21:41 cgroup.procs
-r--r--r--    1 root     root             0 Nov  5 21:41 cgroup.sane_behavior
-rw-r--r--    1 root     root             0 Nov  5 21:41 cpuset.cpu_exclusive
-rw-r--r--    1 root     root             0 Nov  5 20:57 cpuset.cpus
-r--r--r--    1 root     root             0 Nov  5 21:41 cpuset.effective_cpus
-r--r--r--    1 root     root             0 Nov  5 21:41 cpuset.effective_mems
-rw-r--r--    1 root     root             0 Nov  5 21:41 cpuset.mem_exclusive
-rw-r--r--    1 root     root             0 Nov  5 21:41 cpuset.mem_hardwall
-rw-r--r--    1 root     root             0 Nov  5 21:41 cpuset.memory_migrate
-r--r--r--    1 root     root             0 Nov  5 21:41 cpuset.memory_pressure
-rw-r--r--    1 root     root             0 Nov  5 21:41 cpuset.memory_pressure_enabled
-rw-r--r--    1 root     root             0 Nov  5 21:41 cpuset.memory_spread_page
-rw-r--r--    1 root     root             0 Nov  5 21:41 cpuset.memory_spread_slab
-rw-r--r--    1 root     root             0 Nov  5 20:57 cpuset.mems
-rw-r--r--    1 root     root             0 Nov  5 21:41 cpuset.sched_load_balance
-rw-r--r--    1 root     root             0 Nov  5 21:41 cpuset.sched_relax_domain_level
drwxr-xr-x    2 root     root             0 Nov  5 20:59 docker
-rw-r--r--    1 root     root             0 Nov  5 21:41 notify_on_release
-rw-r--r--    1 root     root             0 Nov  5 21:41 release_agent
-rw-r--r--    1 root     root             0 Nov  5 21:41 tasks

# コンテナ内から memory コントローラのルート直下の一覧確認
# ここで本来は /mytest 下の情報だけ表示されてほしいが、docker 等がいるように
# ルートの情報が表示されている
[/]# ls -l /sys/fs/cgroup/memory
total 0
-rw-r--r--    1 root     root             0 Nov  5 23:52 cgroup.clone_children
--w--w--w-    1 root     root             0 Nov  5 23:52 cgroup.event_control
-rw-r--r--    1 root     root             0 Nov  5 23:52 cgroup.procs
-r--r--r--    1 root     root             0 Nov  5 23:52 cgroup.sane_behavior
drwxr-xr-x    2 root     root             0 Nov  5 20:59 docker
drwxr-xr-x    2 root     root             0 Nov  5 23:52 init.scope
-rw-r--r--    1 root     root             0 Nov  5 23:52 memory.failcnt
--w-------    1 root     root             0 Nov  5 23:52 memory.force_empty
-rw-r--r--    1 root     root             0 Nov  5 23:52 memory.kmem.failcnt
-rw-r--r--    1 root     root             0 Nov  5 20:59 memory.kmem.limit_in_bytes
-rw-r--r--    1 root     root             0 Nov  5 23:52 memory.kmem.max_usage_in_bytes
-r--r--r--    1 root     root             0 Nov  5 23:52 memory.kmem.slabinfo
-rw-r--r--    1 root     root             0 Nov  5 23:52 memory.kmem.tcp.failcnt
-rw-r--r--    1 root     root             0 Nov  5 20:59 memory.kmem.tcp.limit_in_bytes
-rw-r--r--    1 root     root             0 Nov  5 23:52 memory.kmem.tcp.max_usage_in_bytes
-r--r--r--    1 root     root             0 Nov  5 23:52 memory.kmem.tcp.usage_in_bytes
-r--r--r--    1 root     root             0 Nov  5 23:52 memory.kmem.usage_in_bytes
-rw-r--r--    1 root     root             0 Nov  5 23:52 memory.limit_in_bytes
-rw-r--r--    1 root     root             0 Nov  5 23:52 memory.max_usage_in_bytes
-rw-r--r--    1 root     root             0 Nov  5 23:52 memory.memsw.failcnt
-rw-r--r--    1 root     root             0 Nov  5 20:59 memory.memsw.limit_in_bytes
-rw-r--r--    1 root     root             0 Nov  5 23:52 memory.memsw.max_usage_in_bytes
-r--r--r--    1 root     root             0 Nov  5 23:52 memory.memsw.usage_in_bytes
-rw-r--r--    1 root     root             0 Nov  5 23:52 memory.move_charge_at_immigrate
-r--r--r--    1 root     root             0 Nov  5 23:52 memory.numa_stat
-rw-r--r--    1 root     root             0 Nov  5 20:59 memory.oom_control
----------    1 root     root             0 Nov  5 23:52 memory.pressure_level
-rw-r--r--    1 root     root             0 Nov  5 20:59 memory.soft_limit_in_bytes
-r--r--r--    1 root     root             0 Nov  5 23:52 memory.stat
-rw-r--r--    1 root     root             0 Nov  5 20:59 memory.swappiness
-r--r--r--    1 root     root             0 Nov  5 23:52 memory.usage_in_bytes
-rw-r--r--    1 root     root             0 Nov  5 23:52 memory.use_hierarchy
drwxr-xr-x    2 root     root             0 Nov  5 21:43 mytest
-rw-r--r--    1 root     root             0 Nov  5 23:52 notify_on_release
-rw-r--r--    1 root     root             0 Nov  5 23:52 release_agent
drwxr-xr-x   30 root     root             0 Nov  5 20:59 system.slice
-rw-r--r--    1 root     root             0 Nov  5 23:52 tasks
drwxr-xr-x    3 root     root             0 Nov  5 20:57 user.slice

しかし実際に Docker 経由で作成されるコンテナではちゃんと自分の cgroup をルートにした環境が整えられています。 youki の実装を見るとどうやら spec で cgroup namespace を切らないよう指示された場合には cgroup type のマウントを生やすのではなく bind マウントを使うようです。

作成されたコンテナの mountinfo をホストから見ると、

726 568 0:33 /docker/74029a18e504f42b1f743c88939c1dda81e94429c307bc2d551ab5cf1017858c /sys/fs/cgroup/cpuset ro,nosuid,nodev,noexec,relatime master:9 - cgroup cgroup rw,seclabel,cpuset

のように master:x という表記があり、これは mount propagation type のうち slave であることを示しています。 対応するマウントポイントはホストの mountinfo 内にある

36 29 0:33 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:9 - cgroup cgroup rw,seclabel,cpuset

であり、ここから bind マウントによってコンテナの cgroup 用のディレクトリ構造が用意されていることがわかります。

実装は見ていませんが作成されるコンテナの mountinfo を見る限りは runc も同様の挙動のようです。

cgroup v2 環境 (unified)

cgroup

cgroup v2 でもコンテナ毎に新しく cgroup が用意されます。

# OCI Runtime に youki を指定してコンテナ実行
$ sudo docker container run -it --rm --runtime=youki ubuntu:20.04 /bin/bash

# 別ターミナルで (ホストからはコンテナが pid=147356 として見えている)
$ cat /proc/147356/cgroup
0::/system.slice/docker-a2c88db75e2beb0a88972247854d51684dad061fdca888007ed04915976037c1.scope

cgroupfs

cgroup v2 でもコンテナ向けに cgroupfs が用意されます。

$ cat /proc/147356/mountinfo | grep cgroup
285 284 0:30 /system.slice/docker-a2c88db75e2beb0a88972247854d51684dad061fdca888007ed04915976037c1.scope /sys/fs/cgroup ro,nosuid,nodev,noexec,relatime - cgroup2 cgroup rw

cgroup namespace

cgroup v2 では v1 と違いデフォルトでは cgroup namespace が新しくコンテナ用に作られます。

OCI Runtime Spec 上では linux.namespacescgroup を含めて指示します。

  "linux": {
    ...
    "namespaces": [
      {
        "type": "mount"
      },
      {
        "type": "network"
      },
      {
        "type": "uts"
      },
      {
        "type": "pid"
      },
      {
        "type": "ipc"
      },
      {
        "type": "cgroup"
      }
    ],
    ...
  }

その設定が加えられるのは恐らく moby/daemon_unix.go です。 cgroups.Mode() == cgroups.Unified、つまり cgroup v2 であれば hostConfig.CgroupnsMode = containertypes.CgroupnsPrivate が設定されるようになっています。これがのちに OCI Runtime Spec を生成する際に cgroup namespace を作成するような指示に変換されます。

youki のソースを確認したところ、コンテナで新しく cgroup namespace を作成する手順はおおよそ以下のようになるという理解です。ここでは unshare(1) でできるだけ手順を再現してみています。新しく cgroup namespace を作成した際、unshare を呼んだプロセスが所属していた cgroup がルートに見えるので、先に希望の cgroup に所属させた上で unshare を呼んでいます。

# 事前にコンテナ用の cgroup を用意する
[mycontainer]$ sudo mkdir /sys/fs/cgroup/mytest

# 新しい mount, pid namespace を用意した /bin/bash を実行する
# --fork は pid namespace を作成するときには必要
[mycontainer]$ sudo unshare --mount --pid --fork /bin/bash

# 自身を用意した cgroup に加入させる
[mycontainer]# bash -c "echo $$ > /sys/fs/cgroup/mytest/cgroup.procs"

# 新しい cgroup namespace を用意した /bin/bash を実行する
# この中では unshare を呼んだプロセスが所属していた cgroup が root cgroup になる
[mycontainer]# unshare --cgroup /bin/bash

# ここではカレントディレクトリ下の rootfs をコンテナの rootfs として使用する想定
[mycontainer]# mount --make-rprivate /
[mycontainer]# mount --rbind ./rootfs ./rootfs
[mycontainer]# mount -t proc proc ./rootfs/proc

# cgroup2 fs をマウントする
[mycontainer]# mount -t cgroup2 none ./rootfs/sys/fs/cgroup

[mycontainer]# mkdir ./rootfs/oldrootfs
[mycontainer]# pivot_root ./rootfs ./rootfs/oldrootfs
[mycontainer]# cd /
[/]# umount -l oldrootfs
[/]# rmdir oldrootfs/

これでコンテナ自身からは自分がルートの cgroup に所属しているように見えます。 (ただしここでは手順上余計な bash プロセスが存在しています。これを避けるなら pid namespace の作成を 2 回目の unshare で行い、1 回目の unshare で作成されたプロセスを後で終了させればよいはず)

[/]# cat /proc/$$/cgroup
0::/

[/]# cat /sys/fs/cgroup/cgroup.procs
1
3
41

[/]# ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/bash
    3 root      0:00 /bin/bash
   42 root      0:00 ps aux

一方でホストからコンテナプロセスの cgroup を見るとルートではなく /mytest に所属しています。 (rootfs のパスを一部改変して記載)

[sample]$ cat /sys/fs/cgroup/mytest/cgroup.procs
105353
105370

[sample]$ cat /proc/105370/cgroup
0::/mytest

[sample]$ cat /proc/105370/mountinfo
265 233 0:25 <host_path_to_mycontainer>/rootfs / rw,relatime - btrfs /dev/nvme0n1p7 rw,compress=zstd:3,ssd,space_cache,subvolid=258,subvol=/@home
266 265 0:161 / /proc rw,relatime - proc proc rw
267 265 0:30 /mytest /sys/fs/cgroup rw,relatime - cgroup2 none rw

参考