名前空間の内部実装(PID・mount・network・user)
コンテナの隔離は魔法ではなく、カーネル構造体の付け替えで動く。PID階層の番号変換やUIDマッピングの原理まで踏み込み、非特権コンテナが成り立つ理由を解き明かします。
- 1.名前空間は task_struct から参照される nsproxy(PID名前空間は task_struct 直下)に各種別の構造体をぶら下げ、プロセスから見える資源の「索引」を差し替えることで隔離する。
- 2.PID名前空間は階層を成し、1つのプロセスが各階層で別々のPIDを持つ。親からは見えるが子からは親が見えない非対称構造で、コンテナ内のPID 1 が成立する。
- 3.user名前空間は uid_map / gid_map で内外のIDを範囲対応づけする。内側の root(UID 0)を外側の非特権UIDへ写すことで、root権限なしにコンテナを作れる。
名前空間は「索引の差し替え」で隔離する
名前空間と cgroups で見たとおり、名前空間はプロセスから見える資源を区切ります。では実装上、何が分離されているのか。核心は グローバルだった資源の「索引(テーブル)」を、プロセスごとに別物へ差し替える ことです。資源そのものを複製するのではなく、名前を解決する経路 を分けるのが名前空間の本質です。
Linux では各プロセスを表す task_struct から、名前空間の束を指す nsproxy 構造体が参照されます。nsproxy は mount・network・UTS・IPC・time などへのポインタを束ね、同一名前空間に属するプロセス間で共有されます。ただし PID 名前空間とユーザー名前空間は nsproxy 経由ではなく task_struct から直接 参照される点に注意が必要です(PID はプロセスの素性に直結し、user は他の名前空間の所有者になるため別扱い)。
新しい名前空間は clone(2) のフラグ(CLONE_NEWPID など)、既存プロセスでの unshare(2)、あるいは /proc/[pid]/ns/ のファイルを開いて setns(2) で参加、の3経路で生成・参加します。名前空間は参照カウントで生き続け、最後の参照が消えた時点で破棄されます。
mount名前空間:マウントツリーを丸ごと複製する
mount 名前空間は、プロセスから見えるマウントポイントの集合(マウントツリー) を分離します。実装上は、グローバルに1本だったマウントツリーが名前空間ごとに別インスタンスとして保持されます。CLONE_NEWNS で新しい名前空間を作ると、親のマウントツリーが コピー され、以後それぞれ独立して mount/umount できます。
ここで重要なのが マウント伝播(propagation) です。各マウントには伝播タイプがあり、これがツリー間でのイベント波及を決めます。
| 伝播タイプ | 意味 | 典型用途 |
|---|---|---|
| shared | 自分への mount/umount を peer グループへ双方向に伝播 | ホスト全体で共有したいマウント |
| slave | 親からの変更は受けるが、自分の変更は親へ返さない(片方向) | ホストのデバイスは見せたいコンテナ |
| private | 一切伝播しない。完全に独立 | コンテナの隔離マウント |
| unbindable | private かつ bind マウント不可 | 複製してほしくない地点 |
コンテナランタイムが最初に mount --make-rprivate / 相当の操作を行うのは、ホストのマウント変更がコンテナへ漏れないよう全体を private 化するためです。root ファイルシステムの切り替えには、古い chroot ではなく pivot_root(2) が使われます。pivot_root は古い root を別ディレクトリへ退避してから新 root へ差し替えるため、退避先を umount すれば 古い root への参照を完全に断てる からです。
network名前空間:ネットワークスタックの実体を分ける
network 名前空間(netns)は、ネットワークスタックの状態を丸ごと 分離します。分離対象はネットワークインターフェース、ルーティングテーブル、ファイアウォール(netfilter/iptables)ルール、ソケットのポート空間、/proc/net の各種統計まで含みます。mount 名前空間が「ツリーのコピー」だったのに対し、netns は 各インターフェースがちょうど1つの netns に属する 排他的な所有モデルです。
新しい netns はループバック(lo)すらダウン状態の、ほぼ空のスタックとして始まります。外と通信させるには、veth ペア(仮想イーサネットの2端を結んだトンネル)の片端をコンテナの netns へ移し、もう片端をホスト側のブリッジへ繋ぐ、という配線をランタイムが行います。インターフェースの netns 間移動は、デバイスを一方から外して他方へ登録し直す操作で、ソケットは元の netns に紐づいたまま残ります。
名前空間は参照さえ残れば生き続けます。ip netns add は /var/run/netns/ 配下にファイルを作り、そのファイルが netns を bind マウントで参照し続けるため、中で動くプロセスがいなくても netns が破棄されません。後から setns(2) で何度でも入り直せる、というのがこの仕組みの肝です。
PID名前空間:階層構造とPID変換
PID 名前空間が最も独特なのは、階層(入れ子)を成す 点です。CLONE_NEWPID で作られた子名前空間は親の下にぶら下がり、最大で数十段ネストできます。そして1つのプロセスは、自分が属する名前空間と、その全祖先名前空間それぞれで、別々の PID を持ちます。
カーネル内部では、各プロセスの番号は struct pid で表されます。これは単一の整数ではなく、[レベル0のPID, レベル1のPID, ...] という 階層ごとの番号の配列 を内包します(実体は upid の並び)。あるプロセスの PID を問い合わせると、カーネルは 問い合わせ元がどの名前空間にいるか を見て、その階層に対応する番号を返します。だから同じプロセスが、ホストからは PID 4217、コンテナ内からは PID 1 に見える、という変換が成立します。
この階層は 非対称 です。親(祖先)の名前空間からは子のプロセスがすべて見え、シグナルも送れます。逆に子からは親や兄弟名前空間のプロセスは 一切見えず、PID も付与されません。コンテナ内の ps がホストのプロセスを表示しないのはこのためです。
PID 名前空間の最初のプロセスは、その名前空間の init として PID 1 になります。init には2つの特権的責務があります。第一に、孤児プロセス の里親(reaper)として、親を失った子を引き取り wait でゾンビを回収すること。第二に、この PID 1 が終了すると、同名前空間の全プロセスが SIGKILL で道連れに終了 し、名前空間自体も畳まれること。コンテナの PID 1 にシグナルを正しく扱う init を置かないと、ゾンビが溜まったり停止シグナルが無視されたりするのはこのためです。
user名前空間:UID/GIDマッピングと非特権コンテナ
user 名前空間(userns)は、UID と GID の対応づけ を名前空間ごとに持ち替えます。これにより、内側の UID 0(root)を、外側の非特権 UID へ写す ことができます。コンテナ内では root として振る舞えるのに、ホストから見れば一般ユーザーの権限しか持たない——これが userns の最重要効果です。
対応づけは /proc/[pid]/uid_map と gid_map に書き込んだ範囲テーブルで定義します。各行は 内側の開始ID 外側の開始ID 長さ の3整数で、ID 範囲を区間ごとに写像します。たとえば次の1行は、内側 UID 0 を外側 UID 100000 に起点を合わせ、0–65535 の範囲をまとめて写します。
# /proc/[pid]/uid_map : 内側ID 外側ID 長さ
0 100000 65536
権限判定の原理はこうです。あるプロセスが資源にアクセスするとき、カーネルは そのプロセスの資格情報(credential)を、資源が属する user 名前空間の視点へ写し戻して から、所有者・パーミッションを比較します。capability(権限ビット)も userns 単位で持たれ、内側で CAP_SYS_ADMIN を全部持っていても、それは その userns の支配下にある資源にしか効きません。ホストが所有する資源には外側の(非特権の)資格情報で当たるため、突破できないわけです。
uid_map/gid_map は誰でも好きに書けるわけではありません。外側 ID として 自分が持つ UID/GID しか写せない(CAP_SETUID/CAP_SETGID を親 userns で持つ場合を除く)、gid_map を書く前に /proc/[pid]/setgroups を deny にして setgroups(2) を封じる必要がある、といった制約があります。これらは、非特権ユーザーが userns 経由でファイルパーミッションを迂回する攻撃(特に補助グループの悪用)を塞ぐための安全装置です。
非特権コンテナ(rootless container)が成り立つのは、まさにこの userns のおかげです。unshare --user --map-root-user のように 最初に user 名前空間を作って内側 root を獲得 すれば、その新しい権限を足場にして PID・mount・network など他の名前空間を 追加の root 権限なしに 作れます。「root でないユーザーが、コンテナの中だけで root になる」という一見矛盾した要求を、ID の写像で解決しているのです。
頻出の対比は「どの名前空間が階層を持つか」。階層を成すのは PID 名前空間と user 名前空間 で、mount/network/UTS/IPC はフラットです。PID 変換は「1プロセスが階層ごとに別 PID を持ち、問い合わせ元の名前空間に応じて番号が返る」、user は「uid_map による範囲写像で内側 root を外側の非特権 UID へ写す」と言えれば十分。「PID 1 が死ぬと名前空間ごと畳まれる」「userns が他の名前空間作成の前提になる(rootless の足場)」も押さえどころです。
まとめ
名前空間の正体は 資源を解決する索引の差し替え です。task_struct から nsproxy(PID と user は直接)を辿り、種別ごとの構造体を付け替えることで、見える資源を分離します。mount はマウントツリーをコピーし伝播タイプで波及を制御、network はスタックを排他所有させ veth で外と繋ぎ、PID は階層構造と番号の配列で1プロセスに複数 PID を与えて変換し、user は uid_map による範囲写像で内側 root を外側の非特権 UID へ写します。これらが組み合わさって、プロセス は「自分専用の OS」という景色を得る。とりわけ user 名前空間は、カーネルモードとユーザーモード の権限境界を ID 写像で再構成し、root なしのコンテナを可能にする、現代コンテナの土台です。
OS Article
名前空間の内部実装(PID・mount・network・user)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
名前空間
比較で見る軸
難易度: advanced / カテゴリ: OS / タグ数: 6
導入後に効く点
PID名前空間は階層を成し、1つのプロセスが各階層で別々のPIDを持つ。親からは見えるが子からは親が見えない非対称構造で、コンテナ内のPID 1 が成立する。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- OS
- タグ数
- 6
判断チェックリスト
- 自社の用途が「名前空間 / コンテナ」に近いか確認する。
- 強みである「名前空間は task_struct から参照される nsproxy(PID名前空間は task_struct 直下)に各種別の構造体をぶら下げ、プロセスから見える資源の「索引」を差し替えることで隔離する。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。