TenForward

技術ブログ。はてなダイアリーから移転しました

shiftfs (s_user_ns version) 試してみた

Open Source Summit Japan 2017 で聞いた話に shiftfs の話があって、興味を持ったので試してみました。この機能の前提機能なんかに関する前提知識に欠けているため、以下には間違いが含まれている可能性が大きいです。是非指摘を頂きたいと思います。試してみただけで内部の処理とかは書いてませんよw

下に関連情報を挙げておきます (こちらを直接見たほうがいいかも)。

直接関係ないけど s_user_ns のパッチも紹介しておきます。

User Namespace に関しては私の連載記事をどうぞ。

一般ユーザ権限のコンテナとイメージ

User Namespace を利用する場合、LXC では shadow に実装されている subuid, subgid を使い、そのユーザに対して使用を許可された uid/gid を使ってコンテナを作成し、起動します。

例えば $HOME/.config/lxc/default.conf

lxc.id_map = u 0 100000 65536
lxc.id_map = g 0 100000 65536

のように設定していれば、lxc-create を実行した時、コンテナ内の uid:0 がホスト上では uid:100000 にマッピングされ、コンテナイメージ上の root 所有のファイルについては、ホスト上で見ると uid/gid が 100000 のユーザ・グループ所有で作成されます。

例えば次のような感じです。

$ ls -l ~/.local/share/lxc/xenial01/rootfs/
合計 77,824
drwxr-xr-x  2 100000 100000 4,096  4222016年 bin/
drwxr-xr-x  2 100000 100000 4,096  4132016年 boot/
drwxr-xr-x  3 100000 100000 4,096  4222016年 dev/
  :(snip)

普段はこれで問題はないのですが、例えば public に流通している root 権限で起動するコンテナで利用する前提のイメージとか、単なる tar.gz のアーカイブとかを一般ユーザで使ったり、逆に一般ユーザ権限用のイメージを root で起動したりする際には、あらかじめ chown などで権限を変更しておく必要があります。一度きりの処理なら良いのですが、起動するたびに入れ代わり立ち代わりユーザを変えて起動したいとかだと不便です。

逆に一般ユーザ権限でコンテナイメージを作成すると、作成環境の ID のマッピング状況に依存してしましますね。

shiftfs

そこでどうやら 4.8 カーネルstruct super_blocks_user_ns という変数が導入され、ファイルシステム上のファイルやディレクトリの処理を行う際には、Namespace 上の ID を使うことができるようになったようです (ここはちゃんと調べてないので間違ってる可能性大)。

ただし、これは単なるディレクトリを bind mount して、コンテナイメージとして使う場合には使えません。なぜなら bind mount は独自の super block は持たないからです (たぶん)。

そこで、s_user_ns を使って User Namespace 内での ID を使うように、super block を使いつつ bind mount をできる shiftfs という機能が James Bottomley 氏が提案しています。現時点では色々な理由であまり受け入れられる感じではなさそうですが…

このパッチは s_user_ns より前のバージョンもあり、その際はマウントオプションで uidmap=0:1000:1 みたいにマッピングを設定していました。(以前のバージョン)

shiftfs のパッチ

どういう経緯かわからないですが、linuxkit に 4.11 用のパッチがありました。これを 4.12 に当ててみました。私の手元は aufs のパッチも当ててるので Makefile だけ適用エラーになりましたが、多分 4.12 にはそのまま当たります。

shiftfs 使わない場合

root 権限で lxc-create で作った Alpine Linux のイメージで試してみました (あらかじめコンテナディレクトリやrootfsディレクトリは一般ユーザでアクセスできるようにしてあります)。

ホスト上で確認すると、root で作成しているので、当たり前ですが root 所有のディレクトリが並びます。

# ls -ld /var/lib/lxc/alpine01/rootfs
drwxr-xr-x 1 root root 108  7520:32 /var/lib/lxc/alpine01/rootfs/
# ls -l /var/lib/lxc/alpine01/rootfs
合計 0
drwxr-xr-x 1 root root   868  5403:12 bin/
drwxr-xr-x 1 root root   130  5403:12 dev/
drwxr-xr-x 1 root root   584  62222:32 etc/
drwxr-xr-x 1 root root     0  5403:12 home/
drwxr-xr-x 1 root root   354  5403:12 lib/
drwxr-xr-x 1 root root    28  5403:12 media/
drwxr-xr-x 1 root root     0  5403:12 mnt/
drwxr-xr-x 1 root root     0  5403:12 proc/
drwx------ 1 root root    24  62220:54 root/
drwxr-xr-x 1 root root     0  5403:12 run/
drwxr-xr-x 1 root root 1,610  5403:12 sbin/
drwxr-xr-x 1 root root     0  5403:12 srv/
drwxr-xr-x 1 root root     0  5403:12 sys/
drwxrwxrwt 1 root root    36  62222:02 tmp/
drwxr-xr-x 1 root root    40  5403:12 usr/
drwxr-xr-x 1 root root    78  62220:17 var/

このコンテナイメージを、普通に User Namespace を作成して、Namespace 内から見てみましょう。

$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# cat /proc/self/{u,g}id_map
         0       1000          1
         0        100          1
# ls -ld /var/lib/lxc/alpine01/rootfs
drwxrwxrwx 1 nobody nogroup 108  62222:02 /var/lib/lxc/alpine01/rootfs/
# ls -l /var/lib/lxc/alpine01/rootfs
合計 0
drwxr-xr-x 1 nobody nogroup   868  5403:12 bin/
drwxr-xr-x 1 nobody nogroup   130  5403:12 dev/
drwxr-xr-x 1 nobody nogroup   584  62222:32 etc/
drwxr-xr-x 1 nobody nogroup     0  5403:12 home/
drwxr-xr-x 1 nobody nogroup   354  5403:12 lib/
drwxr-xr-x 1 nobody nogroup    28  5403:12 media/
drwxr-xr-x 1 nobody nogroup     0  5403:12 mnt/
drwxr-xr-x 1 nobody nogroup     0  5403:12 proc/
drwx------ 1 nobody nogroup    24  62220:54 root/
drwxr-xr-x 1 nobody nogroup     0  5403:12 run/
drwxr-xr-x 1 nobody nogroup 1,610  5403:12 sbin/
drwxr-xr-x 1 nobody nogroup     0  5403:12 srv/
drwxr-xr-x 1 nobody nogroup     0  5403:12 sys/
drwxrwxrwt 1 nobody nogroup    36  62222:02 tmp/
drwxr-xr-x 1 nobody nogroup    40  5403:12 usr/
drwxr-xr-x 1 nobody nogroup    78  62220:17 var/
root@enterprise:~# 

マッピングされていないので nobody:nogroup に。このままではこのイメージ以下に書き込みできません。(なぜnobody:nogroupになるのかは私の連載に書いてます)

shiftfs の利用

s_user_ns バージョンの shiftfs は、まずマウントしたいディレクトリに印を付ける必要があるようです。まずは root で以下を実行。-o mark と mark オプションでマウントします。

# mount -t shiftfs -o mark /var/lib/lxc/alpine01/rootfs/ /var/lib/lxc/alpine01/rootfs/
# grep alpine01 /proc/self/mounts 
/var/lib/lxc/alpine01/rootfs /var/lib/lxc/alpine01/rootfs shiftfs rw,relatime,mark 0 0

この後、また一般ユーザで userns 作成して、別ディレクトリに shiftfs マウントしてみます。この時は特にマウントオプションは不要です。

$ id -u ; id -g
1000
100
$ unshare --pid --user --map-root-user --mount --mount-proc --fork -- /bin/bash
# cat /proc/self/{u,g}id_map
         0       1000          1
         0        100          1
# mount -t shiftfs /var/lib/lxc/alpine01/rootfs/ /home/karma/mnt
# grep shiftfs /proc/self/mounts
/var/lib/lxc/alpine01/rootfs /var/lib/lxc/alpine01/rootfs shiftfs rw,relatime,mark 0 0
/var/lib/lxc/alpine01/rootfs /home/karma/mnt shiftfs rw,relatime 0 0

ここでマウントしたディレクトリ以下を見てみると、

# ls -l /home/karma/mnt
合計 0
drwxr-xr-x 1 root root   868  5403:12 bin/
drwxr-xr-x 1 root root   130  5403:12 dev/
drwxr-xr-x 1 root root   584  62222:32 etc/
drwxr-xr-x 1 root root     0  5403:12 home/
drwxr-xr-x 1 root root   354  5403:12 lib/
drwxr-xr-x 1 root root    28  5403:12 media/
drwxr-xr-x 1 root root     0  5403:12 mnt/
drwxr-xr-x 1 root root     0  5403:12 proc/
drwx------ 1 root root    24  62220:54 root/
drwxr-xr-x 1 root root     0  5403:12 run/
drwxr-xr-x 1 root root 1,610  5403:12 sbin/
drwxr-xr-x 1 root root     0  5403:12 srv/
drwxr-xr-x 1 root root     0  5403:12 sys/
drwxrwxrwt 1 root root    36  62222:02 tmp/
drwxr-xr-x 1 root root    40  5403:12 usr/
drwxr-xr-x 1 root root    78  62220:17 var/

ちゃんと User Namespace 内から見ても root 所有に表示されています。

4.12 で nsfs の見た目がちょっと変わった

4.12 で nsfs に変更が加わってますね。

nsfs ってのは /proc/$PID/ns 以下の、そのプロセスがどの Namespace に所属しているのかを表している特殊なリンクがあるディレクトリです。

4.11 まではこんな感じ。

# ls -l /proc/self/ns
total 0
lrwxrwxrwx 1 root root 0 Jul  4 17:14 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Jul  4 17:14 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jul  4 17:14 mnt -> mnt:[4026531840]
lrwxrwxrwx 1 root root 0 Jul  4 17:14 net -> net:[4026531957]
lrwxrwxrwx 1 root root 0 Jul  4 17:14 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0 Jul  4 17:14 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jul  4 17:14 uts -> uts:[4026531838]

サポートしている Namespace それぞれの Namespace を表しています。

4.12 は

# ls -l /proc/self/ns
total 0
lrwxrwxrwx 1 root root 0 Jul  4 17:00 cgroup -> cgroup:[4026531835]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 ipc -> ipc:[4026531839]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 mnt -> mnt:[4026532397]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 net -> net:[4026531961]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 pid -> pid:[4026532399]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 pid_for_children -> pid:[4026532399]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 user -> user:[4026531837]
lrwxrwxrwx 1 root root 0 Jul  4 17:00 uts -> uts:[4026531838]

こんな感じ。pid_for_children というファイルが加わっています。CRIU 方面で必要だからということのようです (よく知りません)。kernel のコミットでいうと、

これは、PID Namespace だけちょっと特殊な部分があるからでしょうね。この辺りは Masami Ichikawa さんの、

の 28 ページ辺りをご覧になるのがよろしいかと。(nsfs についても 35 ページ辺りに言及があります)

つまり新しいプロセスを生成しない限りは、新しい PID Namespace に所属できません。unshare(2) とか setns(2) でも CLONE_NEWPID は指定できますが、指定した新しい PID Namespace に所属するのはその子プロセスからです。

お気軽コンテナコマンド unshare コマンドも --pid だけ指定してもエラーになります。--fork を同時に指定する必要があります。

ということは、プロセスの状態として、子供用に PID Namespace を持っていながら、自身は元の Namespace に所属しているという状態があるということですね。そういうプロセスの「子供用 PID Namespace を知る」ための機能ということでしょうか。

カーネルの Namespace を実現するための構造体、nsproxy

    30 struct nsproxy {
    31         atomic_t count;
    32         struct uts_namespace *uts_ns;
    33         struct ipc_namespace *ipc_ns;
    34         struct mnt_namespace *mnt_ns;
    35         struct pid_namespace *pid_ns_for_children;
    36         struct net »         *net_ns;
    37         struct cgroup_namespace *cgroup_ns;
    38 };

という風に pid_namespace *pid_ns_for_children となってますね。これを表示しているのかな (たぶん)。

試してみました。

# unshare --mount --pid --mount-proc --fork
# ls -l /proc/self/ns | grep pid
lrwxrwxrwx 1 root root 0 Jul  4 17:27 pid -> pid:[4026532399]
lrwxrwxrwx 1 root root 0 Jul  4 17:27 pid_for_children -> pid:[4026532399]

↑は新しい PID Namespace 内で調べてるのでまあこんな感じ。元の PID Namespace から見てみましょう。

# pgrep unshare
5354
# pstree -p 5354
unshare(5354)───bash(5355)

unshare コマンド (pid: 5354) の子プロセスとして bash が pid: 5355 で起動していますね。

# ls -l /proc/5354/ns | grep pid
lrwxrwxrwx 1 root root 0  7月  4日  17:00 pid -> pid:[4026531836]
lrwxrwxrwx 1 root root 0  7月  4日  17:00 pid_for_children -> pid:[4026532399]
# ls -l /proc/5355/ns | grep pid
lrwxrwxrwx 1 root root 0  7月  4日  17:00 pid -> pid:[4026532399]
lrwxrwxrwx 1 root root 0  7月  4日  17:04 pid_for_children -> pid:[4026532399]

というわけで、unshare コマンド自身の pid_for_children は、子プロセスの bashpid (PID Namespace) と一致していますね。でも unshare コマンド自身の pid (PID Namespace) はそれとは異なります。

というわけで、4.12 でちょっと nsfs に変わったのを調べてみました。(間違いの指摘歓迎!!)

nsproxy まわりについても同じく Ichikawa さんのブログが参考になります

pivot_root できる条件

ふとしたきっかけで man 2 pivot_root の制限に疑問を持ったので、雑にカーネルのコードを読んでみたエントリです。かなり雑にみただけなので間違いの指摘を歓迎します。というか指摘を受けるために書いたようなもの😅

pivot_root の使われ方

コンテナを起動して、コンテナイメージの root をコンテナの root に設定する際、chroot を抜けられないように権限を制御したりしながら chroot を使ったりすることがあります。

一方で、LXC や Docker では pivot_root が使われます。chroot は比較的簡単に使えるのに対して、pivot_root はいくつか制限があります。

man 2 pivot_root すると、その制限について説明があります。

new_root および put_old には以下の制限がある:

(「差す」は「指す」の Typo ?)

しかし、この説明は少し微妙です。例えば、LXC ではコンテナイメージの root を、例えば /usr/lib/lxc/rootfs にバインドマウントして、そこに pivot_root します。

バインドマウントですので、言ってみれば new_root は新たにマウントされたファイルシステムとも言えますが、同じファイルシステム上のディレクトリとも言えます。

他に、

 998        /* change into new root fs */
 999        if (fchdir(newroot)) {
1000                SYSERROR("can't chdir to new rootfs '%s'", rootfs);
1001                goto fail;
1002        }
1003
1004        /* pivot_root into our new root fs */
1005        if (pivot_root(".", ".")) {
1006                SYSERROR("pivot_root syscall failed");
1007                goto fail;
1008        }
1009
1010        /*
1011         * at this point the old-root is mounted on top of our new-root
1012         * To unmounted it we must not be chdir'd into it, so escape back
1013         * to old-root
1014         */
1015        if (fchdir(oldroot) < 0) {
1016                SYSERROR("Error entering oldroot");
1017                goto fail;
1018        }
1019        if (umount2(".", MNT_DETACH) < 0) {
1020                SYSERROR("Error detaching old root");
1021                goto fail;
1022        }
1023
1024        if (fchdir(newroot) < 0) {
1025                SYSERROR("Error re-entering newroot");
1026                goto fail;
1027        }

(lxc-2.0.8時点のsrc/lxc/conf.c)

このあたりですね。新しく root としたいコンテナの root (newroot) に移動したあと、pivot_root(".", ".") として、以前の root である oldrootnewroot と同じディレクトリにマウントしてしまいます。その後 oldroot をアンマウントしています。これは「すなわち put_old を指す文字列に 1 個以上の ../ を付けることによって new_root と同じディレクトリが得られなければならない」ではないようにも思えてしまいます。

なので、実際どうなのかカーネルのコードをみてみました。

カーネルのコメント

pivot_root システムコールfs/namespace.c 内にあります。手元にはなぜか 4.1.15 のソースコードがあるので、それでみると 2941 行目付近からが実装です。

実はここのコメントにも詳細な説明があります。これを読めば一件落着! かも。

/*
 * pivot_root Semantics:
 * Moves the root file system of the current process to the directory put_old,
 * makes new_root as the new root file system of the current process, and sets
 * root/cwd of all processes which had them on the current root to new_root.
 *
 * Restrictions:
 * The new_root and put_old must be directories, and  must not be on the
 * same file  system as the current process root. The put_old  must  be
 * underneath new_root,  i.e. adding a non-zero number of /.. to the string
 * pointed to by put_old must yield the same directory as new_root. No other
 * file system may be mounted on put_old. After all, new_root is a mountpoint.
 *
 * Also, the current root cannot be on the 'rootfs' (initial ramfs) filesystem.
 * See Documentation/filesystems/ramfs-rootfs-initramfs.txt for alternatives
 * in this situation.
 *
 * Notes:
 *  - we don't move root/cwd if they are not at the root (reason: if something
 *    cared enough to change them, it's probably wrong to force them elsewhere)
 *  - it's okay to pick a root that isn't the root of a file system, e.g.
 *    /nfs/my_root where /nfs is the mount point. It must be a mountpoint,
 *    though, so you may need to say mount --bind /nfs/my_root /nfs/my_root
 *    first.
 */

私の超(=ヒドい)訳を。

/*
 * pivot_root のセマンティクス:
 * カレントプロセスのルートファイルシステムを put_old ディレクトリへ移
 *  動させ、new_root をカレントプロセスの新しいルートファイルシステムに
 *  します。そして、現在のルートを使用しているすべてのプロセスのルート
 *  とカレントワーキングディレクトリを新しいルートに設定します
 *
 * 制限:
 * new_root と put_old はディレクトリでなくてはなりません。そして、
 * 現在のプロセスのルートと同じファイルシステム上にあってはなりません。
 * put_old は new_root の下になくてはなりません。すなわち、put_old
 * が指す文字列に 0 個以外の /.. を追加すると new_root と同じディレク
 * トリにならなくてはいけません。他のファイルシステムが put_old にマ
 * ウントされていてはいけません。結局、new_root はマウントポイントで
 * なくてはなりません。
 *
 * さらに、カレントの root が 'rootfs' (initramfs) となることはできま
 * せん。この場合の代替策は
 * Documentation/filesystems/ramfs-rootfs-initramfs.txt をご覧ください。
 *
 * 注意:
 *  - root にいない場合は、root/cwd に移動しません (理由: 十分に注意し
 *    て、それらを変更するのであれば、別の場所を強制するのはたぶん間違
 *    いでしょう)
 *  - ファイルシステムの root でない root を選択することができます。例
 *    えば、/nfs がマウントポイントである /nfs/my_root を選択できます。
 *    マウントポイントでなくてはなりませんので、mount --bind
 *    /nfs/my_root /nfs/my_root を最初に実行しておく必要があるかもしれ
 *    ません
 */

これでほぼ解決でしょうか :-)

カーネルコード

ですが、カーネルのコードを追って、どのような条件が設定されていうのかを確認しておきましょう。

2966SYSCALL_DEFINE2(pivot_root, const char __user *, new_root,
2967                const char __user *, put_old)
2968{
2969        struct path new, old, parent_path, root_parent, root;
2970        struct mount *new_mnt, *root_mnt, *old_mnt;
2971        struct mountpoint *old_mp, *root_mp;
2972        int error;

文字列で与えられたパスから struct path を取得します。ここで new_rootput_oldディレクトリであるかどうかのチェックをしているようです。また、カレントプロセスの root のパス (= root) を取得します。

2977        error = user_path_dir(new_root, &new);
  :(snip)
2981        error = user_path_dir(put_old, &old);
  :(snip)
2989        get_fs_root(current->fs, &root);

それぞれのパスから、struct mount を取得します。

2996        new_mnt = real_mount(new.mnt);
2997        root_mnt = real_mount(root.mnt);
2998        old_mnt = real_mount(old.mnt);

shared mount 以外

まず最初の条件、

2999        if (IS_MNT_SHARED(old_mnt) ||
3000                IS_MNT_SHARED(new_mnt->mnt_parent) ||
3001                IS_MNT_SHARED(root_mnt->mnt_parent))

new_mntroot_mntstruct mount で、そのメンバ mnt_parent はマウントの親子関係がある場合、要は他のマウント配下にマウントポイントがあり、そこにマウントされているような場合に、その親マウントを示すメンバです (たぶん)。(struct mount)

以前の root の移動先のマウント (put_old)、新しい root (new_root)およびカレントプロセスの root がマウントされている親のマウント (ファイルシステム) が shared マウントであってはいけません。

新しいrootのマウントが現プロセスと同じマウント名前空間

ふたつめの条件、

 if (!check_mnt(root_mnt) || !check_mnt(new_mnt))
        goto out4;

check_mnt は、次のように引数 mnt で指定したマウントの名前空間とカレントプロセスのマウント名前空間が等しいかどうかをチェックしています。

 771static inline int check_mnt(struct mount *mnt)
 772{
 773        return mnt->mnt_ns == current->nsproxy->mnt_ns;
 774}

つまり、新しい root のマウントは、カレントプロセスのマウント名前空間に属していなければなりません。

カレントプロセスのrootと、new, oldが異なるマウント

同じファイルシステムでグルグル循環してはいけませんので、

3011        if (new_mnt == root_mnt || old_mnt == root_mnt)
3012                goto out4; /* loop, on the same file system  */
  • 新しい root の mount 構造体と、カレントプロセスの root の mount 構造体が同じ、つまり同じマウント (ファイルシステム) である
  • 古い root の mount 構造体と、カレントプロセスの root の mount 構造体が同じ、つまり同じマウント (ファイルシステム) である

このような場合はエラーとなります。old_mntroot_mnt は同じじゃないの? と一瞬思ってしまうかもしれませんが、元の root を、新しい root 以下の put_old にマウントする際のマウントを表しています。例えば、マウントポイントが違いますので構造体のインスタンスは別ですね。

そもそもここが同じだと、pivot_root でルートを移動することになりません。

カレントプロセスのrootはマウントポイント

root.mnt->mnt_rootで、カレントプロセスの root マウント (ファイルシステム) の root の dentry を求めています。これがカレントプロセスの root の dentry と異なっている場合はエラーになります。

3014        if (root.mnt->mnt_root != root.dentry)
3015                goto out4; /* not a mountpoint */

ややこしいですが、カレントプロセスの root ディレクトリがマウントポイントと異なっていてはいけません。これは、カレントプロセスが chroot でマウントポイント以外を root としている場合でしょう (たぶん)。

カレントプロセスの root は attach されている

ここはちょっとよくわからないのですが、カレントプロセスの root がマウントの親子関係のツリー内にいるかどうかをチェックしています。

3016        if (!mnt_has_parent(root_mnt))
3017                goto out4; /* not attached */

mnt_has_parentfs/mount.h 内にあります。

static inline int mnt_has_parent(struct mount *mnt)
{
    return mnt != mnt->mnt_parent;
}

指定した mnt と、自身のメンバである親マウントを表す mnt->mnt_parent が異なっている場合、つまり親マウントがあるかどうかをチェックしています。(構造体の初期化時点で mnt->mnt_parent = mnt という処理があるので、ちゃんと親子関係がなければ mnt == mnt->mnt_parent となるはずです、たぶん)

システム起動時の root を含め、きちんとマウントされていれば、親マウントは存在するんだと思います。というのは、カーネルパラメータで指定した root を起動時にマウントする際、do_move_mountを通りますが、この中でも同じようなチェックをしていて、エラーになったら root がマウントできないはずです (たぶん)。

このあたりで、元の root のマウントを mnt_parent に入れているような処理があります (たぶん、do_mount_moveattach_mntmnt_set_mountpointあたりの流れ)。

新しい root はマウントポイントで attach されている

3019        if (new.mnt->mnt_root != new.dentry)
3020                goto out4; /* not a mountpoint */
3021        if (!mnt_has_parent(new_mnt))
3022                goto out4; /* not attached */

先の説明のカレントプロセスのチェックと同じですね。新しい root マウントの root ディレクトリはマウントポイントで、きちんとマウントの親子関係の中に入っている必要があります。

old は new (新たな root) 配下

3023        /* make sure we can reach put_old from new_root */
3024        if (!is_path_reachable(old_mnt, old.dentry, &new))
3025                goto out4;

is_path_reachable は関数名から想像はつきますが、

2916/*
2917 * Return true if path is reachable from root
2918 *
2919 * namespace_sem or mount_lock is held
2920 */
2921bool is_path_reachable(struct mount *mnt, struct dentry *dentry,
2922                         const struct path *root)
2923{
2924        while (&mnt->mnt != root->mnt && mnt_has_parent(mnt)) {
2925                dentry = mnt->mnt_mountpoint;
2926                mnt = mnt->mnt_parent;
2927        }
2928        return &mnt->mnt == root->mnt && is_subdir(dentry, root->dentry);
2929}

という関数です。つまり、

  • old と new が異なるマウントで old に親マウントがある場合には、old の親マウントを参照

という処理を終えた後に条件判定をしていますので、

  • old の親 (マウント) が new
  • old (のマウントポイント) が new (のマウントポイント) のサブディレクト

という条件になります。ちなみに is_subdir はこんな。

3302/**
3303 * is_subdir - is new dentry a subdirectory of old_dentry
3304 * @new_dentry: new dentry
3305 * @old_dentry: old dentry
3306 *
3307 * Returns 1 if new_dentry is a subdirectory of the parent (at any depth).
3308 * Returns 0 otherwise.
3309 * Caller must ensure that "new_dentry" is pinned before calling is_subdir()3310 */
3311
3312int is_subdir(struct dentry *new_dentry, struct dentry *old_dentry)
3313{
3314        int result;
3315        unsigned seq;
3316
3317        if (new_dentry == old_dentry)
3318                return 1;

(fs/dcache.cのis_subdir付近付近)

3317行目で、指定されているふたつのディレクトリが同じでも 1 を返してるので、「"/..“ をつけて」とかは説明につけなくても良いのでは?

new はカレントプロセス root 配下

3026        /* make certain new is below the root */
3027        if (!is_path_reachable(new_mnt, new.dentry, &root))
3028                goto out4;

これは上 (old は new 配下) と同じですね。

まとめ

ここまできて、man やコード中のコメントをみても、わかったようなわかってないような気分になるのは、用語の曖昧さと気づきました。

ファイルシステム
マウントポイントにマウントされるマウントの情報と含まれるツリー (=`mount`構造体)

とするとすっきりします。つまり

new_rootput_old は、man 2 pivot_root にあったように:

加えて、

こんな感じでしょうか。

ファイルシステムの実体 (配下のディレクトリやファイル) は判定には関係なく、マウントそのものとマウントポイントがキーとなります。新しい root はマウントポイントであれば良いので、bind マウントでも良いということになりますね。

pivot_root の条件のところだけ追って力尽きたので、実際のマウントやら移動の部分の解説はありません😅

Linux 4.11 での cgroup 関連の話題

Linux 4.11 で cgroup に動きがありました。と言ってもしばらく新機能追えてないので、これまでも色々変更されているかも?

追加された事自体を忘れてしまいそうなのでメモしておくだけで、詳しく調べるわけではありません。

rdma controller

rdma コントローラというコントローラが新たに追加されています。RDMA は “Remote Direct Memory Access” ですか。知りませんでした。

Added rdma cgroup controller that does accounting, limit enforcement on rdma/IB resources.

とのことですから、Infiniband 関係でリソース制限を行うために追加されたのでしょうか。

Drop the matching uid requirement on migration for cgroup v2

cgroup v2 での、root 以外のユーザによる cgroup 操作の制限を一部外しましょう、というもののようですね。cgroup v2 では別に制限かかっているから、これがなくても OK みたいな。

slub: make sysfs directories for memcg sub-caches optional

cgroup 初期化時の v1 の処理だけ抜き出して追ってみる

cgroup 初期化のメモ。間違っている可能性大なので信用しないでください。何度も同じところを繰り返し見てるので忘れないようにメモです。

サブシステム (cgroup_subsys) と各グループのサブシステムの状態 (cgroup_subsys_state) とそれらのセット (css_set) の関係とかわかってないと以下を見てもわけわからんかも。自分用のメモで、間違いあるかもしれません。

start_kernel からまず cgroup_init_early が呼び出された後、cgroup_init が呼び出される。

   492 asmlinkage __visible void __init start_kernel(void)
    :(snip)
   511         cgroup_init_early();
    :(snip)
   661         cgroup_init();

cgroup_init_early

  4960         RCU_INIT_POINTER(init_task.cgroups, &init_css_set);

init_taskcss_set 型のメンバ cgroupsinit_css_set で初期化します。init_css_set は cgroup.c で定義されています。

  4962         for_each_subsys(ss, i) {

これは静的に定義されているサブシステム分要素を持つ cgroup_subsys 型の cgroup_subsys[] 変数 (配列) 分ループをまわすものでした (cgroup の SUBSYS マクロ 参照)。ss には i 番目のサブシステムが入ります。

  4963                 WARN(!ss->css_alloc || !ss->css_free || ss->name || ss->id,
  4964                      "invalid cgroup_subsys %d:%s css_alloc=%p css_free=%p name:id=%d:%s\n",
  4965                      i, cgroup_subsys_name[i], ss->css_alloc, ss->css_free,
  4966                      ss->id, ss->name);

まずは

  • サブシステムに必須のメソッドである css_alloc/free が存在しているかのチェック
  • サブシステム名とIDがまだ構造体のメンバに設定されていないチェック

を行います。

  4967                 WARN(strlen(cgroup_subsys_name[i]) > MAX_CGROUP_TYPE_NAMELEN,
  4968                      "cgroup_subsys_name %s too long\n", cgroup_subsys_name[i]);

次にサブシステム名 (cgroup_subsys_name[i] に入ってます) の名前が長すぎないかチェックしています。

  4969 
  4970                 ss->id = i;
  4971                 ss->name = cgroup_subsys_name[i];

サブシステム (変数の各メンバ) に、ID と名前を設定します。(各サブシステムで cgroup_subsys を定義する際には定義されていないメンバ)

  4973                 if (ss->early_init)
  4974                         cgroup_init_subsys(ss, true);

そしてサブシステムの early_init を行うように 1 が設定されている時は cgroup_init_subsys を呼び出します。

cgroup_init_subsys を見てみると、

  4895 static void __init cgroup_init_subsys(struct cgroup_subsys *ss, bool early)
  4896 {
   :(snip)
  4906         /* Create the root cgroup state for this subsystem */
  4907         ss->root = &cgrp_dfl_root;
  4908         css = ss->css_alloc(cgroup_css(&cgrp_dfl_root.cgrp, ss));

ss->root には v2 の root が放り込まれます。つまり cgroup_init_subsys は v2 の処理を行うだけです。

cgroup_init

cgroup_init はちょっと長いのですが、これも v2 の処理を無視するとやっていることはシンプルです。

  4992         BUG_ON(cgroup_init_cftypes(NULL, cgroup_legacy_base_files));

まずはこれ。cgroup を操作するために各グループに現れるファイルがあります。このファイルを cftype という構造体で定義します。

このファイルのうち、サブシステムの処理とは直接関係ない、cgroup 自体の操作に関連する cgroup コアで扱うファイル群があり、その定義が cftype の配列として静的に定義されています。それが cgroup_legacy_base_files 変数です。これを cgroup_init_cftypes 関数に渡して、ファイル操作の定義を行います (多分)。

  4996         /* Add init_css_set to the hash table */
  4997         key = css_set_hash(init_css_set.subsys);
  4998         hash_add(css_set_table, &init_css_set.hlist, key);

システム上の css_set はすべて css_set_table というハッシュテーブルで管理されますので、それに init_css_set を登録します。css_set 内のサブシステムから計算される値を使ってハッシュのキー (key) を生成し、使用します。とは言っても init_css_set って v2 用じゃ…

そしてまたサブシステム分ループ。ループの最初に以下のような処理がありますが、

  5005                 if (ss->early_init) {
  5006                         struct cgroup_subsys_state *css =
  5007                                 init_css_set.subsys[ss->id];
  5008 
  5009                         css->id = cgroup_idr_alloc(&ss->css_idr, css, 1, 2,
  5010                                                    GFP_KERNEL);
  5011                         BUG_ON(css->id < 0);
  5012                 } else {
  5013                         cgroup_init_subsys(ss, false);
  5014                 }

ここですが、cgroup_init_early で初期化したサブシステムは ID を確保、されなかったサブシステムは cgroup_init_subsys で初期化します。とは言っても、いずれも v2 処理。

その後の条件分岐、

  5035                 if (ss->dfl_cftypes == ss->legacy_cftypes) {
  5036                         WARN_ON(cgroup_add_cftypes(ss, ss->dfl_cftypes));
  5037                 } else {
  5038                         WARN_ON(cgroup_add_dfl_cftypes(ss, ss->dfl_cftypes));
  5039                         WARN_ON(cgroup_add_legacy_cftypes(ss, ss->legacy_cftypes));
  5040                 }

if 文 true の条件は特別な起動オプションを付けないと満たさない開発目的の条件なので無視。通常は false になるので、v2 の設定を行った (cgroup_add_dfl_cftypes呼び出し) 後の 5039 行目だけが v1 の処理でしょう、と思えますが、実は通常はここは何もしません。この理由は後で。

  5042                 if (ss->bind)
  5043                         ss->bind(init_css_set.subsys[ssid]);
  5044         }

その後、サブシステムに bind 関数が設定されている場合はそれを実行。

  5046         err = sysfs_create_mount_point(fs_kobj, "cgroup");

sysfs にマウントポイントとなるディレクトリを作成 (/sys/fs/cgroup)。

  5050         err = register_filesystem(&cgroup_fs_type);

cgroup というファイルシステムを登録。

  5056         proc_create("cgroups", 0, NULL, &proc_cgroupstats_operations);

proc に “cgroups” というファイルを作成。で終わりです。

cgroup_add_legacy_cftypes で何もしない理由

cgroup_add_legacy_cftypes で v1 っぽい処理を呼び出しているにも関わらず「通常は何もしません」と書いた理由を追ってみます。

cgroup_add_legacy_cftypes

cgroup_add_legacy_cftypes にサブシステムと cgroup_subsys 構造体型の各サブシステムの変数で定義されている legacy_cftypes を渡します。legacy_cftypes は各サブシステムで v1 のグループに出現させるファイルが各サブシステムごとに定義されています。

cgroup_add_legacy_cftypes 関数を見てみます。

  3313 int cgroup_add_legacy_cftypes(struct cgroup_subsys *ss, struct cftype *cfts)
  3314 {
    :(snip)
  3322         if (!cgroup_legacy_files_on_dfl ||
  3323             ss->dfl_cftypes != ss->legacy_cftypes) {
  3324                 for (cft = cfts; cft && cft->name[0] != '\0'; cft++)
  3325                         cft->flags |= __CFTYPE_NOT_ON_DFL;
  3326         }
  3327 
  3328         return cgroup_add_cftypes(ss, cfts);
  3329 }

通常は (開発目的のフラグがオン、またはデフォルトの cftypes が v1 の場合 (これも通常はない) でなければ)、各 cftype 型の変数のメンバ flags に “v2 階層には表示させない” というフラグを設定したのち、cgroup_add_cftypes を呼び出します。

cgroup_add_cftypes

cgroup_add_cftypes 関数

  3263 static int cgroup_add_cftypes(struct cgroup_subsys *ss, struct cftype *cfts)
  3264 {
   :(snip)
  3273         ret = cgroup_init_cftypes(ss, cfts);
   :(snip)
  3280         ret = cgroup_apply_cftypes(cfts, true);

cgroup_init_cftypescft->ss = ss という cftype 構造体の ss にサブシステムを入れている部分があります。そして cgroup_apply_cftypes 関数を呼びます。

cgroup_apply_cftypes

  3138 static int cgroup_apply_cftypes(struct cftype *cfts, bool is_add)
  3139 {
  3141         struct cgroup_subsys *ss = cfts[0].ss;
  3142         struct cgroup *root = &ss->root->cgrp;

ss には cfts[0].ss を入れてますが、これは cgroup_init_cftypes でサブシステムそのものを入れました。root 変数にはサブシステムの root が入りますが、これは cgroup_init_subsys で v2 の root が入っていたはず。

  3155                 ret = cgroup_addrm_files(cgrp, cfts, is_add);

cgroup_addrm_files

そして cgroup_addrm_files 呼び出し。

  3105 static int cgroup_addrm_files(struct cgroup *cgrp, struct cftype cfts[],
  3106                               bool is_add)
  3107 {
   :(snip)
  3113         for (cft = cfts; cft->name[0] != '\0'; cft++) {
  3114                 /* does cft->flags tell us to skip this file on @cgrp? */
  3115                 if ((cft->flags & __CFTYPE_ONLY_ON_DFL) && !cgroup_on_dfl(cgrp))
  3116                         continue;
  3117                 if ((cft->flags & __CFTYPE_NOT_ON_DFL) && cgroup_on_dfl(cgrp))
  3118                         continue;
   :(snip)

というループがあり、

  • 現在の cgrp は v2 の root の cgroup が入っているはず (cgroup_apply_cftypes の処理参照)
  • cft->flags には __CFTYPE_NOT_ON_DFL が放り込まれていたはず (cgroup_add_legacy_cftypes の処理参照)

というわけで、ここはずっと continue で何もしないままループを抜けます。ループを抜けなければ cgroup_add_file で cgroup 用のファイルを追加するのですが、何もしないということです。

関連