Linux 4.10 で入った Overlayfs の redirect_dir 機能の動きを軽く追ってみた
ちょっと前にリリースされたカーネルですが、4.10 で overlayfs に変更が入っていましたのでちょっと調べてみました。
Overlayfs についてはこちらをどうぞ (ちょっと古い記事なのでカーネルにマージされる前の仕様も説明しています)。
それとコンテナ勉強会のこの発表資料
- ちょっとOverlayfsの実装、読んでみました (@akachochinさん)
カーネルの変更と準備
4.10の関連するコミットは以下。
- ovl: redirect on rename-dir
- ovl: allow redirect_dir to default to “on”
- ovl: allow setting max size of redirect
- ovl: show redirect_dir mount option
lowerdir側に存在するディレクトリを移動した場合の改良ってところでしょうか。相変わらずパッチも短くて見やすくて変更が追いやすそうですね (あまりちゃんと見てませんが)。
以下は 4.13-rc5 カーネルで試しています。
カーネルの config で Overlayfs: turn on redirect dir feature by default
を “Y” にするとデフォルトで有効になるようです (CONFIG_OVERLAY_FS_REDIRECT_DIR
)。
従来の動き
まずは従来の動きにするように redirect_dir=off
というオプションを与えます。
$ mkdir lower upper work overlay # overlayfs用のディレクトリの作成 $ mkdir lower/lowerdir upper/upperdir # 下層、上層それぞれにディレクトリ作成 $ touch lower/lowerdir/lowfile upper/upperdir/upfile # ディレクトリ内にファイル作成 $ sudo mount -t overlay \ > -o lowerdir=lower,upperdir=upper,workdir=work,redirect_dir=off > overlayfs overlay/ # マウント
重ね合わされた overlay
ディレクトリの中身はこんな風になります。普通の動きですね。
$ find overlay/
overlay/
overlay/lowerdir
overlay/lowerdir/lowfile
overlay/upperdir
overlay/upperdir/upfile
lowerdir
を移動しましょう。
$ cd overlay $ mv lowerdir lowerdir2 $ ls -F lowerdir2/ upperdir/
ここで下層の lowerdir
と上層の upperdir
の中を覗いてみます。
$ find lower/
lower/
lower/lowerdir
lower/lowerdir/lowfile
Overlayfs は下層側は変化しませんので、これは当たり前。
$ ls -l upper/ 合計 8,192 c--------- 1 root root 0, 0 8月 16日 17:32 lowerdir drwxr-xr-x 2 karma users 4,096 8月 16日 17:29 lowerdir2/ drwxr-xr-x 2 karma users 4,096 8月 16日 17:29 upperdir/
Overlayfs では、削除されたファイルやディレクトリは特別なデバイスファイルになるのでした。lowerdir
は削除された状態になっており、新たに移動先の lowerdir2
が上層に作成されていますね。lowerdir2
内には、元々下層の lowerdir
内に存在していたファイルがコピーされています。
$ ls -l upper/lowerdir2/ 合計 0 -rw-r--r-- 1 karma users 0 8月 16日 17:29 lowfile
ディレクトリ内に多数のファイルやディレクトリがある場合は時間がかかりそうです。
改良後の動き
4.10 で導入された “redirect on rename-dir” という機能を使ってみましょう。
$ sudo mount -t overlay \ > -o lowerdir=lower,upperdir=upper,workdir=work,redirect_dir=on \ > overlayfs overlay
ここでは明示的に redirect_dir=on
としていますが、カーネルの config でこの機能を “Y” にしてあれば、自動的にオンになりますので、これは不要です。
$ find overlay/
overlay/
overlay/lowerdir
overlay/lowerdir/lowfile
overlay/upperdir
overlay/upperdir/upfile
先ほどと同じように下層と上層が重ね合わされた状態になっています。この状態で先ほどと同じようにディレクトリを移動してみましょう。
$ cd overlay/ $ ls lowerdir/ upperdir/ $ mv lowerdir lowerdir2 $ ls lowerdir2/ upperdir/
はい、移動されました。それでは下層と上層のディレクトリ内を覗いてみましょう。
$ ls -l upper/ 合計 12,288 c--------- 1 root root 0, 0 8月 16日 17:35 lowerdir drwxr-xr-x 2 karma users 4,096 8月 16日 17:34 lowerdir2/ drwxr-xr-x 2 karma users 4,096 8月 16日 17:34 upperdir/
これは先ほどと同じですね。移動前の lowerdir
が削除された状態になっており、新たに lowerdir2
が作成されています。この lowerdir2
内を覗いてみましょう。
$ ls -l upper/lowerdir2/ 合計 0 $ ls overlay/lowerdir2/ lowfile
おや? 先ほどは内部のファイルもコピーされていましたが、今回はファイルが存在しません。にもかかわらず、重ね合わせたディレクトリにはちゃんとファイルが存在しますね。下層側を見てみると、
$ ls -l lower/lowerdir/ 合計 0 -rw-r--r-- 1 karma users 0 8月 16日 17:34 lowfile
ファイルが存在しますが、これは従来と変わりありません。
ディレクトリを移動した場合は、中身のファイルは元の下層側のファイルを参照しているようですね。参照先はどうやって調べているのでしょう?
先に紹介したコミットにも、Documentation/filesystems/overlayfs.txt にも書かれてあります。
2. If the "redirect_dir" feature is enabled, then the directory will be copied up (but not the contents). Then the "trusted.overlay.redirect" extended attribute is set to the path of the original location from the root of the overlay. Finally the directory is moved to the new location.
調べてみましょう。
$ sudo getfattr -n trusted.overlay.redirect upper/lowerdir2 # file: upper/lowerdir2 trusted.overlay.redirect="lowerdir"
ここに参照先が保存されていますね。
階層が深くなった時は “he path of the original location from the root of the overlay” ってことだから、マウントポイントからのパスが保存されているのかと思ったら、単なるディレクトリ名でした。まあ追えるからかな。
$ sudo getfattr -n trusted.overlay.redirect upper/lowerdir2/hogehoge2 # file: upper/lowerdir2/hogehoge2 trusted.overlay.redirect="hogehoge"
ちなみに redirect_dir 機能を使って操作された overlayfs (の upperdir) を、redirect_dir 機能をサポートしていないカーネルではマウントできませんので注意しましょう (あまりないか…)。
おまけ
4.10 では、redirect 機能の他に、ディレクトリの移動時の変更がもうひとつ加えられています。xfs の時に効果を発揮するのでしょうか。
shiftfs (s_user_ns version) 試してみた
Open Source Summit Japan 2017 で聞いた話に shiftfs の話があって、興味を持ったので試してみました。この機能の前提機能なんかに関する前提知識に欠けているため、以下には間違いが含まれている可能性が大きいです。是非指摘を頂きたいと思います。試してみただけで内部の処理とかは書いてませんよw
下に関連情報を挙げておきます (こちらを直接見たほうがいいかも)。
- Container Interfaces for Storage - Are We There Yet? (Open Source Summit の James Bottomley 氏のスライド)
- shiftfs: uid/gid shifting filesystem (s_user_ns version) (lwn.net)
- Unprivileged Build Containers (James Bottomley’s random Pages)
- 【和訳】コンテナとストレージの問題:コンテナ普及の要因分析 (CREATIONLINEのブログ) (ちょっと訳がわかりづらい部分があるのでオリジナルの記事を見たほうがよいかも?)
直接関係ないけど s_user_ns
のパッチも紹介しておきます。
- s_user_ns の kernel patch (lkml.org)
User Namespace に関しては私の連載記事をどうぞ。
- 第16回 Linuxカーネルのコンテナ機能 [6] ─ユーザ名前空間 (LXCで学ぶコンテナ入門)
一般ユーザ権限のコンテナとイメージ
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 4月 22日 2016年 bin/ drwxr-xr-x 2 100000 100000 4,096 4月 13日 2016年 boot/ drwxr-xr-x 3 100000 100000 4,096 4月 22日 2016年 dev/ :(snip)
普段はこれで問題はないのですが、例えば public に流通している root 権限で起動するコンテナで利用する前提のイメージとか、単なる tar.gz のアーカイブとかを一般ユーザで使ったり、逆に一般ユーザ権限用のイメージを root で起動したりする際には、あらかじめ chown などで権限を変更しておく必要があります。一度きりの処理なら良いのですが、起動するたびに入れ代わり立ち代わりユーザを変えて起動したいとかだと不便です。
逆に一般ユーザ権限でコンテナイメージを作成すると、作成環境の ID のマッピング状況に依存してしましますね。
shiftfs
そこでどうやら 4.8 カーネルで struct super_block
に s_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 7月 5日 20:32 /var/lib/lxc/alpine01/rootfs/ # ls -l /var/lib/lxc/alpine01/rootfs 合計 0 drwxr-xr-x 1 root root 868 5月 4日 03:12 bin/ drwxr-xr-x 1 root root 130 5月 4日 03:12 dev/ drwxr-xr-x 1 root root 584 6月 22日 22:32 etc/ drwxr-xr-x 1 root root 0 5月 4日 03:12 home/ drwxr-xr-x 1 root root 354 5月 4日 03:12 lib/ drwxr-xr-x 1 root root 28 5月 4日 03:12 media/ drwxr-xr-x 1 root root 0 5月 4日 03:12 mnt/ drwxr-xr-x 1 root root 0 5月 4日 03:12 proc/ drwx------ 1 root root 24 6月 22日 20:54 root/ drwxr-xr-x 1 root root 0 5月 4日 03:12 run/ drwxr-xr-x 1 root root 1,610 5月 4日 03:12 sbin/ drwxr-xr-x 1 root root 0 5月 4日 03:12 srv/ drwxr-xr-x 1 root root 0 5月 4日 03:12 sys/ drwxrwxrwt 1 root root 36 6月 22日 22:02 tmp/ drwxr-xr-x 1 root root 40 5月 4日 03:12 usr/ drwxr-xr-x 1 root root 78 6月 22日 20: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 6月 22日 22:02 /var/lib/lxc/alpine01/rootfs/ # ls -l /var/lib/lxc/alpine01/rootfs 合計 0 drwxr-xr-x 1 nobody nogroup 868 5月 4日 03:12 bin/ drwxr-xr-x 1 nobody nogroup 130 5月 4日 03:12 dev/ drwxr-xr-x 1 nobody nogroup 584 6月 22日 22:32 etc/ drwxr-xr-x 1 nobody nogroup 0 5月 4日 03:12 home/ drwxr-xr-x 1 nobody nogroup 354 5月 4日 03:12 lib/ drwxr-xr-x 1 nobody nogroup 28 5月 4日 03:12 media/ drwxr-xr-x 1 nobody nogroup 0 5月 4日 03:12 mnt/ drwxr-xr-x 1 nobody nogroup 0 5月 4日 03:12 proc/ drwx------ 1 nobody nogroup 24 6月 22日 20:54 root/ drwxr-xr-x 1 nobody nogroup 0 5月 4日 03:12 run/ drwxr-xr-x 1 nobody nogroup 1,610 5月 4日 03:12 sbin/ drwxr-xr-x 1 nobody nogroup 0 5月 4日 03:12 srv/ drwxr-xr-x 1 nobody nogroup 0 5月 4日 03:12 sys/ drwxrwxrwt 1 nobody nogroup 36 6月 22日 22:02 tmp/ drwxr-xr-x 1 nobody nogroup 40 5月 4日 03:12 usr/ drwxr-xr-x 1 nobody nogroup 78 6月 22日 20: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 5月 4日 03:12 bin/ drwxr-xr-x 1 root root 130 5月 4日 03:12 dev/ drwxr-xr-x 1 root root 584 6月 22日 22:32 etc/ drwxr-xr-x 1 root root 0 5月 4日 03:12 home/ drwxr-xr-x 1 root root 354 5月 4日 03:12 lib/ drwxr-xr-x 1 root root 28 5月 4日 03:12 media/ drwxr-xr-x 1 root root 0 5月 4日 03:12 mnt/ drwxr-xr-x 1 root root 0 5月 4日 03:12 proc/ drwx------ 1 root root 24 6月 22日 20:54 root/ drwxr-xr-x 1 root root 0 5月 4日 03:12 run/ drwxr-xr-x 1 root root 1,610 5月 4日 03:12 sbin/ drwxr-xr-x 1 root root 0 5月 4日 03:12 srv/ drwxr-xr-x 1 root root 0 5月 4日 03:12 sys/ drwxrwxrwt 1 root root 36 6月 22日 22:02 tmp/ drwxr-xr-x 1 root root 40 5月 4日 03:12 usr/ drwxr-xr-x 1 root root 78 6月 22日 20: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 のコミットでいうと、
- ns: allow ns_entries to have custom symlink content
- pidns: expose task pid_ns_for_children to userspace
これは、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
は、子プロセスの bash
の pid
(PID Namespace) と一致していますね。でも unshare
コマンド自身の pid
(PID Namespace) はそれとは異なります。
というわけで、4.12 でちょっと nsfs に変わったのを調べてみました。(間違いの指摘歓迎!!)
nsproxy まわりについても同じく Ichikawa さんのブログが参考になります
- nsproxyとfork()周りのめも (φ(・・*)ゞ ウーン カーネルとか弄ったりのメモ)
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 には以下の制限がある:
- ディレクトリでなければならない。
new_root
とput_old
は現在の root と同じファイルシステムにあってはならない。put_old
はnew_root
以下になければならない。すなわちput_old
を差す文字列に 1 個以上の../
を付けることによってnew_root
と同じディレクトリが得られなければならない。- 他のファイルシステムが
put_old
にマウントされていてはならない。
https://linuxjm.osdn.jp/html/LDP_man-pages/man2/pivot_root.2.html
(「差す」は「指す」の 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 }
このあたりですね。新しく root としたいコンテナの root (newroot
) に移動したあと、pivot_root(".", ".")
として、以前の root である oldroot
も newroot
と同じディレクトリにマウントしてしまいます。その後 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_root
、put_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_mnt
と root_mnt
は struct 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_mnt
と root_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_parent
は fs/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_move
→attach_mnt
→mnt_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;
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_root
と put_old
は、man 2 pivot_root
にあったように:
- ディレクトリでなければならない
- 現在の root ファイルシステムと同じファイルシステム であってはならない
put_old
はnew_root
以下になければならないput_old
に他のファイルシステムがマウントされていてはいけない
加えて、
new_root
はカレントプロセスの root ファイルシステムの root 以下になければならない- カレントプロセスの root ファイルシステムの root はマウントポイントでなければならない
chroot
でマウントポイントから root が移動していてはいけない
new_root
の root ディレクトリはマウントポイントでなければならないold_put
にマウントされるファイルシステム、new_mnt
の親ファイルシステム、カレントプロセスの root ファイルシステムの親ファイルシステムが shared マウントであってはならない
こんな感じでしょうか。
ファイルシステムの実体 (配下のディレクトリやファイル) は判定には関係なく、マウントそのものとマウントポイントがキーとなります。新しい 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 みたいな。