コンテナを支える技術(を自分が勉強した内容)についてまとめていきます。主に次の二つの書籍を参考にし、それにプラスして自分で調べたことをまとめた内容となります。
前回は cgroup についてまとめました。
今回は、前回に引き続き Linux のカーネル機能の一つである namespace について扱います。 各種コマンドは Apple M2 Pro, macOS Sonoma 14.5 のホストマシン上に、lima を用いて Ubuntu 26.04 LTS を動かして確認しています。
$ brew install lima $ limactl start --name=linux template:ubuntu-lts $ limactl shell linux
https://github.com/lima-vm/lima
namespace
Linux namespace は、1 台のホスト上で動く複数のプロセスに対して、それぞれ別々の見える世界を与えるカーネル機能です。PID(プロセス ID)・ネットワーク・マウントポイント・ホスト名などのシステムリソースを namespace ごとに分離することで、あるプロセスからは自分専用の OS が動いているかのように見せます。Docker や Kubernetes のコンテナ分離は、この namespace の仕組みを土台にしています。
A namespace wraps a global system resource in an abstraction that makes it appear to the processes within the namespace that they have their own isolated instance of the global resource. -- namespaces(7) - Linux manual page
namespace には以下の種類があります。
- PID namespace: プロセス群の隔離
- Mount namespace: マウントポイントリストの隔離
- Network namespace: ネットワーク関連のリソース隔離
- UTS namespace: ホスト名等の隔離
- IPC namespace: プロセス間通信に関するリソースの隔離
- User namespace: ユーザー/グループや権限等の隔離
- Cgroup namespace: プロセスから見える cgroup のルートディレクトリの隔離
- Time namespace: システム起動時刻等のオフセットの隔離
いずれも unshare コマンドを利用することで手軽に試すことができます。unshare(1) - Linux manual page
その一部を見ていきましょう。
UTS namespace
unshare --uts コマンドを使うことで、ホスト名とドメイン名を隔離して設定することができます。試しに hostname を設定してみると、新しい namespace の中では hostname が変わっていますが、抜けると元に戻っていることが分かります。
lima@lima-linux:~$ sudo unshare --uts sh # hostname lima-linux # hostname experiment # hostname experiment # exit lima@lima-linux:~$ hostname lima-linux
UTS namespace では、unshare を呼び出したプロセス自身が新しい namespace に移動します。次に見る PID namespace は、この点が異なります。
PID namespace
unshare --pid コマンドを使うことで、プロセスを隔離することができます。実際にやってみましょう。
lima@lima-linux:~$ sudo unshare --pid sh # whoami root # whoami sh: 2: Cannot fork # whoami sh: 3: Cannot fork # exit
すると、1回目の whoami は成功しますが、2回目以降は Cannot fork で失敗してしまいます。なぜなのでしょうか。unshare(2) の公式ドキュメントを確認してみます。
The calling process is not moved into the new namespace. The first child created by the calling process will have the process ID 1 -- unshare(2) - Linux manual page
※ unshare(1) は実際に CLI から打つコマンドのことで、unshare(2) は Linux のシステムコールです。unshare(1) は unshare(2) の薄いラッパーとして提供されています。
呼び出したプロセスは新しい namespace に移らず、そこから作られた子プロセスが PID 1を持つようになります。先ほどの例では、sudo unshare --pid sh を打った直後はまだ新しい namespace ではなく、次に打った whoami が新しい namespace で PID 1として実行されました。試しに PID を出力してみると、確かにホスト上の PID が出力されていることが分かります。
lima@lima-linux:~$ sudo unshare --pid sh # echo $$ 526679
別ターミナルからプロセス階層を表示すると、unshare が存在しておらず、sh が sudo の子プロセスになっていることが分かります。unshare は unshare(2) を呼んだ後、execve(2) で自分自身を sh に置き換えます。プロセスは新しく作られず、中身だけが入れ替わるため、unshare はプロセス階層に残りません。
lima@lima-linux:~$ ps fa ... 526838 pts/2 S+ 0:00 \_ sudo unshare --pid sh 526840 pts/3 Ss 0:00 \_ sudo unshare --pid sh 526841 pts/3 S+ 0:00 \_ sh ...
その後の whoami コマンドはすぐに終了するため、新しい namespace 内では PID=1 のプロセスがすぐに終了してしまったことになります。この場合、後続のプロセスが全て失敗します。その挙動は pid_namespaces(7) に記載があります。
The first process created in a new namespace (i.e., the process created using clone(2) with the CLONE_NEWPID flag, or the first child created by a process after a call to unshare(2) using the CLONE_NEWPID flag) has the PID 1, and is the "init" process for the namespace (see init(1)). This process becomes the parent of any child processes that are orphaned because a process that resides in this PID namespace terminated (see below for further details). If the "init" process of a PID namespace terminates, the kernel terminates all of the processes in the namespace via a SIGKILL signal. This behavior reflects the fact that the "init" process is essential for the correct operation of a PID namespace. In this case, a subsequent fork(2) into this PID namespace fail with the error ENOMEM; it is not possible to create a new process in a PID namespace whose "init" process has terminated. -- pid_namespaces(7) - Linux manual page
PID=1 のプロセス以降が全て失敗してしまっては実用性がありません。そこで unshare(1) には --fork オプションが用意されています。指定したプログラムを直接実行するのではなく、unshare(2) を呼んだ後に fork し、その子プロセスとして実行します。これにより sh 自身が新しい namespace の PID 1 となり、以降のコマンドを正常に実行できます。
lima@lima-linux:~$ sudo unshare --pid --fork sh # echo $$ 1
また、--fork オプションなしで実行した時と異なり、whoami を何回呼び出しても成功するようになっています。
# whoami root # whoami root
別ターミナルからプロセス階層を表示すると、--fork なしの時とは異なり、unshare が存在しています。--fork を付けると unshare は自分自身を置き換えず、fork して生まれた子プロセスで sh を実行します。そのため unshare が親として階層に残ります。
lima@lima-linux:~$ ps fa ... 526806 pts/2 S+ 0:00 \_ sudo unshare --pid --fork sh 526808 pts/3 Ss 0:00 \_ sudo unshare --pid --fork sh 526809 pts/3 S 0:00 \_ unshare --pid --fork sh 526810 pts/3 S+ 0:00 \_ sh ...
さて、docker などでコンテナを立ち上げたときには、ps を実行するとコンテナの PID namespace 内で実行されたプロセスだけが出力されます。それと同じ挙動を期待して unshare で新しい PID namespace を作ってその中で ps を実行してみましょう。
lima@lima-linux:~$ sudo unshare --pid --fork sh
# ps
PID TTY TIME CMD
1 ? 00:07:15 systemd
2 ? 00:00:05 kthreadd
3 ? 00:00:00 pool_workqueue_release
4 ? 00:00:00 kworker/R-rcu_gp
5 ? 00:00:00 kworker/R-sync_wq
6 ? 00:00:00 kworker/R-kvfree_rcu_reclaim
...
すると、期待した挙動とは異なり、ホスト全体のプロセスが表示されてしまっていることが分かります。
ps は /proc 以下のディレクトリを読んでプロセス一覧を組み立てるプログラムで、自分がどの PID namespace にいるかは見ていません。そして unshare --pid は PID namespace を新しくするだけで、/proc のマウントには手を付けません。そのため ps はホストの /proc を読み、ホスト全体のプロセスを表示します。この /proc を新しい namespace 用にマウントし直す必要があり、そのためにはマウント操作をホストから分離する仕組みが要ります。それが次に扱う Mount namespace です。
Mount namespace
mount namespace は、プロセスから見えるマウントポイントの一覧を分離する仕組みです。この中で行ったマウント・アンマウントは、ホスト側には影響しません。
実際に確認してみましょう。--mount だけを指定して、tmpfs をマウントしてみます。
lima@lima-linux:~$ sudo unshare --mount sh # mkdir -p /tmp/test # mount -t tmpfs none /tmp/test # findmnt /tmp/test TARGET SOURCE FSTYPE OPTIONS /tmp/test none tmpfs rw,relatime,inode64
別のターミナルからホスト側を確認すると、このマウントは存在しません。
lima@lima-linux:~$ findmnt /tmp/test lima@lima-linux:~$
さて、前節で ps がホスト全体のプロセスを表示してしまう問題に行き当たりました。これを解決するのが --mount-proc オプションです。
lima@lima-linux:~$ sudo unshare --pid --fork --mount-proc sh
# ps
PID TTY TIME CMD
1 pts/3 00:00:00 sh
2 pts/3 00:00:00 ps
新しい PID namespace の中のプロセスだけが表示されるようになりました。unshare(1) のドキュメントによると、このオプションはプログラムを実行する直前に procfs を(既定では /proc に)マウントするもので、新しい mount namespace の作成も同時に行います。
Just before running the program, mount the proc filesystem at mountpoint (default is /proc). This is useful when creating a new PID namespace. It also implies creating a new mount namespace since the /proc mount would otherwise mess up existing programs on the system. -- unshare(1) - Linux manual page
なぜ mount namespace まで必要になるのでしょうか。ここには2つの異なる役割があります。
まず一つ目に、procfs の再マウントが、表示を変えています。
procfs はディスク上に実体を持たず、読まれるたびにカーネルが内容を組み立てる疑似ファイルシステムです。このときどの PID namespace の情報を返すかは、マウントした時点で決まります。pid_namespaces(7) に記載があります。
A /proc filesystem shows (in the /proc/[pid] directories) only processes visible in the PID namespace of the process that performed the mount -- pid_namespaces(7) - Linux manual page
unshare --pid だけでは /proc はホストでマウントされたものを引き継ぐため、ps はホストの情報を読んでいました。新しい PID namespace の中でマウントし直すことで、初めて表示が変わります。
二つ目に、mount namespace が、その副作用を封じ込めています。procfs をマウントし直すだけで良いかというと、そうはいきません。マウントポイントの一覧はシステム全体で共有されているため、/proc を差し替えるとホスト側の ps や systemd まで巻き込んでしまいます。unshare(1) のドキュメントも、/proc のマウントがシステム上の既存のプログラムを壊してしまうため mount namespace の作成を伴う、と説明しています。
Cgroup namespace
cgroup namespace はプロセスが自分の cgroup よりも上位の cgroup ディレクトリの設定を閲覧できないようにするものです。自分の cgroup がルート cgroup として扱われるようになります。
The process will have a virtualized view of /proc/self/cgroup, and new cgroup mounts will be rooted at the namespace cgroup root. -- unshare(1) - Linux manual page
cgroup namespace の外側と内側で /proc/self/cgroup の内容を比較すればその動作を確認できます。このファイルは procfs が提供する疑似ファイルの一つで、「このプロセスは cgroup ツリーのどこにいるか」を返します。
lima@lima-linux:~$ cat /proc/self/cgroup 0::/user.slice/user-501.slice/session-6.scope lima@lima-linux:~$ sudo unshare --cgroup sh # cat /proc/self/cgroup 0::/ #
unshare --cgroup を実行した時点で所属していた cgroup が、新しい namespace ではルートとして見えるようになりました。実際にはホストの階層のどこかにいるのに、それを知ることができません。ホスト側から新しい namespace を実行しているプロセスの cgroup を見に行くと実際の階層を見ることができます。
# echo $$ 527316
別のターミナルに移り、ホストから確認してみます。
lima@lima-linux:~$ cat /proc/527316/cgroup 0::/user.slice/user-501.slice/session-6.scope










