コンテナ内で PID 1 として表示されていても、ホスト側で PID 1 が見えるわけではありません。新しいマウントネームスペースを作成しても、ファイルの内容がコピーされるわけではありません。ネームスペースは各種リソースに対してビューと権限の境界を確立し、cgroup はリソースの使用量を管理します。
コンテナは、ルートファイルシステム、capability、seccomp、セキュリティポリシーと組み合わせて使用され、すべての分離を一元管理するネームスペースオブジェクトは存在しません。
参照目標 読書位置
オブジェクトの範囲と参照 型、作成、進入
身元とファイルのビュー PID、User、Mount
再利用可能な実験とその他の境界 分離の例、ネットワークと時間
コンテナは複数のネームスペースを組み合わせ、プロセスはその一部を共有できる
プロセス A
独立した mount / PID
プロセス B
別の mount / PID のセット
同じ network ns
ポートとネットワークデバイスビューの共有
ネームスペースのメンバシップは種類ごとに選択されます。それは分割不可能な「コンテナの身元」ではありません。
各ネームスペースの管理対象
型 フラグ 範囲と境界
Mount CLONE_NEWNS マウントツリーのビュー;ファイル内容は共有されたままになる可能性があり、イベントは伝播する可能性があります
PID CLONE_NEWPID PID 番号と可視階層;すべてのメンバーが PID 1 になるわけではありません
User CLONE_NEWUSER UID/GID のマッピングと capability のスコープ
Network CLONE_NEWNET ネットワークデバイス、ルーティング、プロトコルスタートの状態、ポートなど
IPC CLONE_NEWIPC SysV IPC、POSIX メッセージキューなど
UTS CLONE_NEWUTS ホスト名と NIS ドメイン名;独立した DNS システムではありません
Cgroup CLONE_NEWCGROUP cgroup パスのビュー;リソースクォータは作成しません
Time CLONE_NEWTIME monotonic/boottime などのクロックオフセット;すべてのクロックを仮想化しません
具体的な機能はカーネルの設定に依存し、作成と進入は権限によって制限されます。ネームスペースの概要
作成、進入、参照の保持
clone/clone3 は、新しいタスクを新しいネームスペースに進入させることができます。unshare は、呼び出し元の一部の共有関係を解除します。setns は、fd で指定された既存のネームスペースに進入します。各型には特別なルールがあり、これらを完全に同等の「コンテナの切り替え」として扱うことはできません。
/proc/PID/ns 下のリンクはメンバシップを比較するために使用でき、対応する fd を開くか bind mount を保持することで、ネームスペースの参照を維持できます。プロセスが終了しても、保持されているネームスペースオブジェクトがすぐに破棄されるとは限りません。
nsproxy は複数のネームスペースの参照を保持しますが、user namespace はプロセスの資格情報に関連付けられており、現在の PID namespace は PID 階層に関連しています。nsproxy をすべてを含む完全なスナップショットとして単純化することはできません。pid_for_children と time_for_children は、「現在所属しているもの」と「後続のサブプロセスが所属するもの」の違いを特に示しています。
PID namespace:番号は相対的である
新しい PID namespace の最初のプロセスはそこで PID 1 となり、後続のメンバーは各自の番号を持ちます。同じタスクは祖先の PID namespace にも他の PID を持っています。祖先は子孫を見ることができますが、子 namespace はこれによってすべての祖親プロセスを閲覧する能力を得るわけではありません。
unshare(CLONE_NEWPID) は、呼び出し元自身の PID namespace を変更するのではなく、その後作成されるサブプロセスに影響を与えます。setns も PID namespace に対して同様のサブプロセスセマンティクスを持ちます。これが、unshare コマンドが実行されるプログラムを新しい PID ビューに進入させるために通常 --fork が必要である理由です。PID namespace
まずサブプロセスを作成し、その後 unshare して後続のサブプロセスを作成します。2人の観察者の間でビューを切り替え、同じタスクの身元と可視番号を比較します。ネームスペースをプロセスのコピーとして扱うのではなく、ビューの切り替えとして捉えてください。
図を準備しています 再読み込み
unshare 後、親プロセスの PID はなぜ変わらないのか? unshare(CLONE_NEWPID) は pid_for_children を変更し、呼び出し元の現在の PID namespace は移動しません。同じサブタスクは、祖先と自身の空間で異なる PID を持つことができ、2つのプロセスがコピーされるわけではありません。
{"id":"pid-namespace-identity","title":"unshare 後、親プロセスの PID はなぜ変わらないのか?","summary":"unshare(CLONE_NEWPID) は pid_for_children を変更し、呼び出し元の現在の PID namespace は移動しません。同じサブタスクは、祖先と自身の空間で異なる PID を持つことができ、2つのプロセスがコピーされるわけではありません。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"fork\" data-i18n=\"fork\"></button><button id=\"unshare\" data-i18n=\"unshare\"></button></div><div class=\"track-row\"><span data-i18n=\"current\"></span><div id=\"current\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"future\"></span><div id=\"future\" class=\"track\"></div></div><div class=\"presets\" role=\"group\" data-i18n-label=\"view\"><button data-mode=\"host\" data-i18n=\"host\"></button><button data-mode=\"child\" data-i18n=\"child\"></button></div><div class=\"track-row\"><span data-i18n=\"identity\"></span><div id=\"identity\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"visible\"></span><div id=\"visible\" class=\"track\"></div></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n#identity,#visible{display:grid;grid-template-columns:1fr}#identity .cell{min-height:45px;font:12px var(--viz-font-ui)}#visible .cell{min-height:4.8em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let created=false,children=[],serial=0,view='host';function draw(){cells('current',[{text:'NS0 · getpid() = 400',cls:'on'}]);cells('future',[{text:created?'NS1':'NS0',cls:created?'gold':'on'}]);const all=[{host:400,local:null},...children];cells('identity',Array.from({length:4},(_,i)=>{const p=all[i];return{text:p?'task '+p.host+' → NS0:'+p.host+' / NS1:'+(p.local??'—'):'—',cls:p?.local?'gold':p?'on':'empty'};}));cells('visible',Array.from({length:4},(_,i)=>{const p=all[i];return{text:!p?'—':view==='host'?'PID '+p.host:p.local?'PID '+p.local:t('hidden'),cls:p&&(view==='host'||p.local)?'on':'empty'};}));$('#fork').disabled=children.length===3;$('#unshare').disabled=created;pressed(view);set('explanation',t('note'));}$('#fork').onclick=()=>{children.push({host:401+children.length,local:created?++serial:null});draw();};$('#unshare').onclick=()=>{created=true;draw();};document.querySelectorAll('[data-mode]').forEach(e=>e.onclick=()=>{view=e.dataset.mode;draw();});$('#reset').onclick=()=>{created=false;children=[];serial=0;draw();};draw();","audio":false,"strings":{"child":"新しい子空間","current":"親タスクの現在の PID namespace","fork":"親プロセスがサブプロセスを1つ作成","future":"親タスクの pid_for_children","heading":"unshare 後、親プロセスの PID はなぜ変わらないのか?","hidden":"このビューでは不可視","host":"祖先空間","identity":"1つのオブジェクト、2層の番号","intro":"まずサブプロセスを作成し、その後続のサブプロセスのために PID namespace を構築し、続けて作成します。観察ビューを切り替え、同じタスクが祖先と子空間で持つ番号を照合します。","note":"unshare 前に存在していたサブプロセスも移動しません。その後作成されたサブタスクのみが新しい空間に入り、そこで最初の PID 1 となり、親タスクの getpid() は引き続き 400 を返します。","reset":"リセット","scope":"親タスクのホスト PID=400、最大3つのサブタスクを作成、PID namespace は1層のみ作成。番号は教育目的の順序で割り当てられ、実際の PID 割り当ては連続しているとは限りません。権限、init の終了、養子縁組、setns、proc マウントは省略;子空間のビューはその可視メンバーのみを表示します。","scopeLabel":"モデルの範囲と規約","unshare":"後続のサブプロセスのために PID を unshare","view":"観察者がいる空間","visible":"現在のビュー内の PID"}}
PID 1 には特別なシグナルルールがあります。namespace 内の他のメンバーは通常、インストールされたハンドラを持つシグナルのみを送信できます。SIGKILL/SIGSTOP は、通常のプロセスのように内部で有効にはなりません。祖先 namespace からの SIGKILL/SIGSTOP には特定の例外があります。したがって、「内部では kill できないが、SIGSTOP は可能である」という一般化は誤りです。
PID 1 が終了すると、カーネルはその namespace 内の他のプロセスを殺し、その namespace への後続のプロセス作成は制限されます。オブジェクトは fd によって保持されたままになる可能性があり、すべての参照が即座に解放されるわけではありません。
「現在の空間は変わらず、後続のサブプロセスが変わる」ことを検証する
以下では、Python 3.12+ の Linux os.unshare() インターフェースを使用して、これらの2つのメンバシップを直接観察します。まずコマンドラインから一時的な user namespace を作成し、プログラムが自身の PID namespace を作成するために必要なスコープ内の権限を取得させます。プログラムは外側の proc マウントを保持し、親プロセスが外側の PID でサブプロセスをチェックできるようにします。
これを pid_view.py として保存し、unshare --user --map-root-user python3 pid_view.py で実行します。プログラムは1つのサブプロセスのみを作成し、パイプを使用してその終了タイミングを制御します。
import errno
import os
def ns_identity ( path ) :
info = os . stat ( path )
return info . st_dev , info . st_ino
before_pid = os . getpid ( )
before_ns = ns_identity ( " /proc/self/ns/pid" )
os . unshare ( os . CLONE_NEWPID )
assert os . getpid ( ) == before_pid
assert ns_identity ( " /proc/self/ns/pid" ) == before_ns
read_fd , write_fd = os . pipe ( )
child = os . fork ( )
if child == 0 :
os . close ( write_fd )
try :
assert os . getpid ( ) == 1 and os . getppid ( ) == 0
assert os . read ( read_fd , 1 ) == b " x"
except BaseException :
os . _exit ( 1 )
os . _exit ( 0 )
os . close ( read_fd )
try :
child_ns = ns_identity ( f " /proc/ { child } /ns/pid" )
assert child_ns != before_ns
assert ns_identity ( " /proc/self/ns/pid_for_children" ) == child_ns
assert ns_identity ( " /proc/self/ns/pid" ) == before_ns
assert os . write ( write_fd , b " x" ) == 1
finally :
os . close ( write_fd )
_ , status = os . waitpid ( child , 0 )
assert os . waitstatus_to_exitcode ( status ) == 0
print ( f " parent PID unchanged: { before_pid } ; child seen outside: { child } ; inside: 1" )
try :
unexpected = os . fork ( )
except OSError as error :
assert error . errno == errno . ENOMEM
else :
if unexpected == 0 :
os . _exit ( 1 )
os . waitpid ( unexpected , 0 )
raise AssertionError ( " init が終了した PID namespace は新しいプロセスを受け付けてはならない" )
PID 番号は実行のたびに変わりますが、実際に確認すべきは4つの関係です。親プロセス自身の番号は変わっていないこと。サブプロセスは内部で 1 であること。サブプロセスは外部の親プロセスの PPID を 0 として見ていること。親プロセスの pid_for_children がサブプロセスの現在の PID namespace と同じであること。pid_for_children は最初の子プロセス作成前にも特別な可視性を持つため、コードは fork の後にそれを比較します。
unshare(PID) は後続のサブプロセス空間を変更し、呼び出し元は元の場所に残る
親プロセス:現在の PID namespace A
getpid() は元の番号を保持
子プロセス:現在の PID namespace B
内部 PID=1、外部には別の番号
fork
親の pid_for_children → B;親の ns/pid → A
子プロセスの親は B の外側に位置するため、getppid() は 0 を返します。PID 1 が終了すると、B は fork を受け付けなくなります。
この制御された軌跡における最後の ENOMEM は、namespace init が終了したことに由来しており、物理的なメモリ不足として一概に説明できるものではありません。実際のプログラムでは、errno を保持し、前置操作と組み合わせて診断する必要があります。実験では proc を再マウントしないため、コンテナ内での ps の完全なビューは示されていません。このビューが必要な場合は、独立した mount namespace と新しい proc インスタンスと併用してください。PID namespace のメンバシップと proc セマンティクス
PID 1 として実行するには、シグナルと回收の責任も負う必要がある
孤児は適切な subreaper または namespace init によって養子縁組され、責任ある側が wait して回收する必要があります。アプリケーションが PID 1 として動作する場合、終了シグナルとサブプロセスの回收を適切に処理する必要があり、通常の対話型プログラムのデフォルトの動作にのみ依存することはできません。
User namespace:root の意味はマッピングに依存する
非初期 user namespace 内で自分の /proc/self/uid_map を読み取ったとき、0 1000 1 が表示される場合、3つのフィールドとして解釈できます。
フィールド 数値 意味
内部開始 UID 0 新しい空間内の身元
親空間開始 UID 1000 外部の身元に対応
範囲の長さ 1 この UID 1つだけをマッピング
これは他のユーザーマッピングを自動的に作成するものではなく、内部の root を単純にホストの root に換算することもできません。
内部の root はホストの nobody ではありません。また、自動的にホストのファイルを読み取れないわけではありません。マウントビューがホストのファイルを露出させている場合、マッピングされた身元は通常のアクセス権限を持っている可能性があります。user namespace 内の capability は、主にその権限スコープ内のリソースに対して有効になり、初期 user namespace のグローバル権限を自動的に取得するわけではありません。User namespace
複数の UID/GID マッピングには、subuid/subgid、マッピングヘルパー、setgroups の制限などが関与します。特権を持たない user namespace の作成可否は、ディストリビューションのセキュリティポリシー、LSM、および数の制限によって影響を受ける可能性があります。kernel.unprivileged_userns_clone は、すべての Linux で統一して存在するスイッチではありません。
入れ子になったマッピングは段階的に合成し、読者の視点も記録する
第1層の namespace から初期 namespace までの範囲を 0 100000 65536、第2層から第1層までのマッピングを 0 1000 1 と設定します。この場合、第2層の UID 0 はまず第1層の UID 1000 にマッピングされ、さらに初期 namespace の UID 101000 にマッピングされます。第1層自身の UID 0 は、初期 UID 100000 に対応します。両方の層で root と呼ばれていても、それらが同じ外部身元を表すわけではありません。
def mapped ( uid , ranges ) :
for inside , outside , count in ranges :
if inside <= uid < inside + count :
return outside + uid - inside
return None
first_to_initial = [ ( 0 , 100000 , 65536 ) ]
second_to_first = [ ( 0 , 1000 , 1 ) ]
intermediate = mapped ( 0 , second_to_first )
assert intermediate == 1000
assert mapped ( intermediate , first_to_initial ) == 101000
assert mapped ( 0 , first_to_initial ) == 100000
assert mapped ( 65535 , first_to_initial ) == 165535
assert mapped ( 65536 , first_to_initial ) is None
assert mapped ( 1 , second_to_first ) is None
モデル内の None は、対応するマッピングがないことを示し、UID 0 ではなく、自動的に nobody の権限を持つことを意味するものでもありません。カーネルは異なるインターフェースで、表現できない所有者を示すために overflow UID を表示したり、有効なマッピングを必要とする操作を拒否したりする場合があります。表示結果は認可判断に代わるものではありません。上記の範囲はマッピング計算の例に過ぎず、実際にそれらを作成するには、マッピングの書き込み権限と親層のマッピング可能な範囲を満たす必要があります。
見落としがちなもう1つの詳細があります。読者と対象プロセスが同じ user namespace にいる場合、uid_map の2列目は対象の親 namespace として解釈されます。両者が異なる user namespace にいる場合、2列目はそのファイルを開いた読者 の namespace として解釈されます。したがって、マッピングを収集する際は、読者の位置を同時に記録する必要があり、任意の /proc/PID/uid_map の2列目を「ホスト UID」または「親 UID」としてマークすることはできません。User namespace:UID/GID マッピングの読み取りルール
Mount namespace:独立したマウントテーブルが独立したファイル内容を意味するわけではない
新しい空間は通常、既存のマウントビューからコピーされますが、基盤となるファイルシステムは同じままになる可能性があります。共有サブツリーの shared/slave/private などの伝播属性は、マウントイベントがビュー間でどのように伝播するかを決定します。したがって、「1つの空間での mount が常に他方に影響を与えない」という主張は成立しません。マウントネームスペース
ルートディレクトリの分離には、適切なマウントとルート切り替えの設計が必要です。chroot や新しい mount namespace 自体は、すべてのファイルをコピーするものではなく、既に開かれている fd のアクセス能力を自動的に取り消すものでもありません。
ホストの hostname を変更しないローカル例
以下のコマンドは、特権を持たない user namespace が許可された環境で user、UTS、PID、および mount namespace を作成し、サブプロセスが終了すると実験が終了します。
unshare -- user -- map-root-user -- uts -- pid -- fork -- mount -- mount-proc sh - c ' hostname kb-demo; printf "pid=%s hostname=%s\n" "$$" "$(hostname)"; cat /proc/self/uid_map'
hostname の変更は新しい UTS namespace で発生します。--mount-proc は新しい PID ビューに proc をマウントし、ps などのツールが外部の proc インスタンスを読み取らないようにします。出力される UID マッピングは実行者に依存し、必ずしもホストの 1000 になるとは限りません。権限が拒否された場合は、まず環境ポリシーを特定し、教学例を実行するために全体のセキュリティ設定を緩和しないでください。
ネットワークと時間空間の一般的な誤解
コンテナはネットワークネームスペースを共有でき、ホストネットワークを使用することもできるため、すべてのコンテナが独立した netns を持つわけではありません。新しいネットワーク空間が自動的に利用可能な外部ネットワークを取得するわけでもなく、デバイス、アドレス、ルーティング、またはユーザー空間ネットワークバックエンドを構成する必要があります。
time namespace は、対応する monotonic/boottime クロックにオフセットを提供し、移行後の時間ビューを維持するのに適しています。CLOCK_REALTIME は、これによってコンテナごとの独立した壁掛け時計を取得するわけではありません。作成後のサブプロセスとオフセット設定のタイミングにも、特別な制限があります。Time namespace
リソースの上限と委譲についてさらに理解するには、cgroup を参照してください。