Linux Namespacesの物理(隔離された空間の正体)

2026-11-06 / シリーズ「コンテナとLinuxカーネルの物理」第1話 / Docker, Linux, Namespaces, コンテナ, カーネル, システムコール

Dockerコンテナを立ち上げ、その中でシェルを起動して ps aux コマンドを叩いた瞬間、誰しもが奇妙な光景を目撃します。

ホストマシン(親OS)側では数百個ものプロセスが蠢いているはずなのに、コンテナの画面に表示されるのは、自分自身である bash と、今叩いた ps という**「たった2つのプロセス」だけです。 しかも、そのプロセスのID(PID)を見ると、OSの絶対的な始祖であるはずの番号――「PID 1」**が堂々と割り振られています。

さらに、ホストマシンで別のターミナルを開いて同じ ps を叩くと、コンテナの中で「PID 1」を名乗っていたはずのそのプロセスが、ホスト側では「PID 42105」といったありふれた一般プロセスとして平然と動いています。

「コンテナとは、本当に小さなOS(仮想マシン)が中で動いているのか?」

答えは**「NO」です。 コンテナの中には、独立したOSカーネルなど1ミリも存在しません。コンテナとは、ハードウェアをエミュレートする仮想マシン(VM)ではなく、「たった1つのLinuxカーネルが、特定のプロセスに対して見せている巧妙な幻覚(視野の狭窄)」**に過ぎません。

この幻覚を作り出しているカーネルの心臓部こそが、本稿のテーマである**「Linux Namespaces(名前空間)」**です。


1. 仮想マシン(VM)vs コンテナ:物理境界の決定的な差

まず、長年議論され続けてきた「仮想マシン」と「コンテナ」の違いを、ハードウェアとカーネルの物理レイヤーから解剖しましょう。

仮想マシン(VM) ハードウェアの完全エミュレーション コンテナ(Docker) 単一カーネルが見せる視野制限 アプリ A アプリ B ゲストOS A カーネル+数GB消費 ゲストOS B カーネル+数GB消費 ハイパーバイザ(KVM / ESXi) ホストOS 物理ハードウェア: CPU・メモリ・NIC コンテナ A 通常のプロセス群 『自分しかいない』と錯覚 コンテナ B 通常のプロセス群 『自分しかいない』と錯覚 Namespaces + cgroups で視野を制限 ★ たった1つの Linuxカーネル 物理ハードウェア: CPU・メモリ・NIC

仮想マシン(VM):家の中に別の家を建てる重労働

VMwareやKVMなどのハイパーバイザは、CPUの仮想化支援機構(Intel VT-x / AMD-V)を使い、「仮想的なマザーボード、仮想メモリ、仮想ハードディスク」をソフトウェアで物理的に丸ごと模倣します。 その上で、本物のゲストOS(LinuxやWindows)をゼロからブートさせます。 そのため、起動には数分かかり、ゲストOS専用のカーネルがメモリを数百MB〜数GB単位で食い潰します。完全な独立性と引き換えに、莫大なリソースを浪費するアプローチです。

コンテナ:同じ部屋に置いた「マジックミラー」

一方、コンテナにはゲストOSなど存在しません。 コンテナ内で動いているWebサーバー(Nginx等)も、データベース(PostgreSQL等)も、ホストマシンの上で直接動いている**「ただの普通のLinuxプロセス」**です。CPUの実行権も、メモリ空間も、ホストのLinuxカーネルが直接スケジューリングしています。

では、なぜコンテナ同士はお互いのファイルやプロセスを覗き見ることができないのか? カーネルがプロセスに対して、**「お前の世界には、このファイルと、このネットワークと、このプロセス番号しか存在しない」**という見取り図をすり替えて渡しているからです。

この「リソースの見え方(グローバルなシステム資源)をプロセスごとに切り離す仕切り板」こそが、Linux Namespacesの物理的な正体です。


2. 世界を6つに切り裂く「6大名前空間」の全貌

Linuxカーネルは、OS内のあらゆるリソースを管理しています。 Namespacesは、かつてOS全体で「1つ」しかなかった共有リソースを、グループごとに独立した箱庭へ隔離します。Dockerが利用している主要な名前空間は、以下の6つです。

名前空間 (Namespace)隔離されるシステムリソースこれによって起きる「幻覚」
PID (Process ID)プロセスIDの番号体系コンテナ内のプロセスが、自分が「PID 1(親分)」だと信じ込む。ホストのプロセスが一切見えなくなる。
NET (Network)ネットワークデバイス・ルーティング・ポートコンテナが独自のIPアドレス、ルーティングテーブル、独立した80番ポートを持つ。ホストとポートが衝突しない。
MNT (Mount)ファイルシステムの階層構造(マウントポイント)ホストのルート(/)とは完全に異なる、UbuntuやAlpineなどの専用ルートディレクトリ(ファイルツリー)が見える。
UTS (UNIX Timesharing)ホスト名(hostname)とドメイン名コンテナごとに固有のホスト名(例: container-web-01)を設定でき、ホストOSのホスト名を汚さない。
IPC (Inter-Process Comm)プロセス間通信(共有メモリ・メッセージキュー)共有メモリ(POSIX/SysV IPC)を隔離し、別コンテナのメモリ領域を勝手に読み書きできないようにする。
USER (User ID)ユーザーID(UID)とグループID(GID)コンテナ内では「root(UID 0)」として振る舞えるが、ホストから見ると無権限の「一般ユーザー(UID 10000等)」に格下げされる。

この6枚の「色つき眼鏡」を同時に掛けさせられたプロセスは、自分がホストマシンの上で大勢の同居人と暮らしていることに一生気づくことができません。 「自分だけの専用OSが丸ごと1台与えられた」と信じ込んだまま、軽快に動き続けるのです。

触って試す:名前空間で見える世界が変わる体験

👀 名前空間ビューア:どの「世界」から見るか

視点を切り替えてみてください。物理的には同じ1台のマシン・同じプロセス群なのに、名前空間が違うと「見えるプロセス一覧」も「見えるネットワーク」もまったく別の世界になります。

見えるプロセス(ps の結果)

見えるネットワーク


3. カーネルの体内解剖:task_struct と nsproxy のポインタ物理

「視野をすり替える」とは、C言語で書かれたLinuxカーネルの内部で、具体的にどのようなデータ構造として表現されているのでしょうか?

Linuxカーネルにおいて、すべてのプロセス(およびスレッド)は、メモリ上に存在する**task_struct**という巨大な構造体によって管理されています。

// Linuxカーネル内部のプロセス管理構造体(概念図)
struct task_struct {
    pid_t pid;                      // プロセスID
    char comm[16];                  // コマンド名
    struct mm_struct *mm;           // メモリ空間へのポインタ
    struct files_struct *files;     // 開いているファイル記述子
    
    /* 名前空間を束ねるポインタ */
    struct nsproxy *nsproxy;
    ...
};

注目すべきは、構造体の中にある**struct nsproxy *nsproxy**というポインタです。

ホストの初期プロセス(PID 1: systemd) task_struct init nsproxy pid_namespace: Root net_namespace: 物理NIC mnt_namespace: ホスト / コンテナ内のプロセス(PID 1: nginx) task_struct nginx nsproxy (独立に新規割当) pid_namespace: Child net_namespace: veth0 mnt_namespace: コンテナ /

通常、プロセスが fork() されて子供が生まれるとき、子プロセスは親プロセスの nsproxy をそのまま引き継ぎます。同じ名前空間のメモリ番地を指し示しているため、親と同じプロセス一覧が見え、同じネットワークカードが見えます。

しかし、コンテナを起動するとき、カーネルは**nsproxy の中身を新しくメモリ上に割り当て(kmalloc)、真新しい独立した名前空間構造体へのポインタへと差し替えます**。

プロセスIDが二重に存在する「PID名前空間の木構造」

特に美しいのが、PID名前空間の多重階層構造です。 PID名前空間は、ディレクトリのように「入れ子(木構造)」になっています。

ホストOSの視界 (Root PID Namespace) コンテナAの視界 (Child PID Namespace) PID 1: systemd PID 850: dockerd PID 42105: nginx(コンテナA) PID 42106: nginx-worker ホストのプロセスは一切見えない PID 1: nginx PID 2: nginx-worker 物理的には同一プロセス!

カーネル内部の struct pid には、「どの名前空間において、何番の数字に見えるか」というマップ情報が保持されています。

  • ホストのRoot名前空間から見れば:42105番
  • コンテナの子名前空間から見れば:1番

プロセスが getpid() システムコールを発行すると、カーネルはそのプロセスが属している名前空間を task_struct->nsproxy->pid_ns から手繰り寄せ、その空間におけるローカルな番号(1)を返します。 これが、コンテナ内で「PID 1」が出現する物理的な仕掛けです。


4. clone() システムコールの現場:C言語10行でコンテナを作る

Dockerなどのコンテナランタイム(runc)は、一体どうやってこの名前空間を切り離して起動しているのでしょうか? その真犯人は、Linuxのプロセス生成システムコール**clone()**です。

通常のプロセス複製である fork() は、親プロセスの完全な分身を作ります。 しかし、clone() システムコールには、第2引数に**「どの名前空間を新しく切り離すか」を指定するフラグ(CLONE_NEW*)**を渡すことができます。

#define _GNU_SOURCE
#include <sched.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>

// 子プロセス(コンテナ内)で実行される関数
int container_main(void *arg) {
    printf("【コンテナ内部】getpid() = %d\n", getpid());
    // ホスト名を勝手に変更してみる
    sethostname("isolated-box", 12);
    // コンテナ内でシェルを起動
    char *args[] = {"/bin/sh", NULL};
    execv("/bin/sh", args);
    return 0;
}

int main() {
    // 64KBのスタック領域を確保
    char *stack = malloc(1024 * 64);
    char *stack_top = stack + (1024 * 64);

    printf("【ホストOS】親プロセス起動: PID = %d\n", getpid());

    // ★Namespacesを切り離して子プロセスを産み落とす!
    int flags = CLONE_NEWPID |  // 新しいPID名前空間
                CLONE_NEWUTS |  // 新しいホスト名名前空間
                CLONE_NEWNET |  // 新しいネットワーク名前空間
                CLONE_NEWNS  |  // 新しいマウント名前空間
                SIGCHLD;

    pid_t pid = clone(container_main, stack_top, flags, NULL);

    waitpid(pid, NULL, 0);
    free(stack);
    return 0;
}

このわずか数十行のCプログラムを実行した瞬間、何が起きるでしょうか?

  1. clone() に CLONE_NEWPID や CLONE_NEWUTS フラグが渡されたことで、カーネルは新しい nsproxy を生成し、独立した箱庭を作ります。
  2. その箱庭の中で container_main が走り出します。
  3. 画面には 【コンテナ内部】getpid() = 1 と表示されます。
  4. 子プロセスが sethostname("isolated-box") でホスト名を変更しても、ホストOSのホスト名は1文字たりとも変わりません。

Dockerが裏で実行しているのは、本質的にこの clone() システムコールの発行に他なりません。 数千行・数万行のコンテナ技術も、コアの物理を突き詰めれば**「カーネルのシステムコールを正しく叩き、ポインタを切り離したプロセスを走らせているだけ」**なのです。


5. Linuxコマンドだけで名前空間を体感する:unshare の威力

C言語をコンパイルしなくても、Linuxの標準コマンド unshare を使えば、今開いているターミナルから1秒で名前空間の隔離を実験できます。

実験:ホストOSから完全に隔絶されたプロセス空間を作る

Linux環境(WSLやUbuntuサーバーなど)で、以下のコマンドを叩いてみてください。

# root権限で、PID空間とマウント空間を新しく切り離し、/proc を再マウントして bash を起動
sudo unshare --pid --fork --mount-proc /bin/bash

このコマンドを実行した瞬間に立ち上がった新しいシェルの中で、プロセス一覧を確認します。

# コンテナ(隔離空間)の中で実行
ps aux

すると、画面には以下のような驚愕の結果が出力されます。

USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.1  10240  4200 pts/0    S    10:00   0:00 /bin/bash
root         2  0.0  0.0  11420  3100 pts/0    R+   10:00   0:00 ps aux

ホストマシンで動いている数百個のプロセスが、完全に消滅しました。 そこには、自分が「PID 1」になった bash と、今実行した ps しか存在しません。 Dockerをインストールすらしていない素のLinuxの上で、コマンド1発で「コンテナの幻覚」が完成した瞬間です。


結論:Dockerとは「OSの幻覚プロバイダ」である

私たちが普段「Dockerコンテナ」と呼んでいるものは、実体のある箱ではありません。

それは、

  • PID Namespaces によって他人のプロセスを見えなくさせ、
  • NET Namespaces によって自分専用のネットワーク線があると思い込ませ、
  • MNT Namespaces によって専用のハードディスクを占有していると錯覚させた、

**「目隠しをされた、ただの普通のLinuxプロセス」**です。

しかし、視野を隔離した(Namespaces)だけでは、コンテナは完成しません。 もし、この隔離された「PID 1」のプロセスが、ホストマシンのCPUを100%使い果たしたり、物理メモリを限界まで食い尽くす無限ループを起こしたらどうなるでしょうか? 目隠しをされていても、物理的なリソースは共有されているため、ホストマシンごと道連れにしてOS全体が窒息死してしまいます。

プロセスが見える「景色」を縛った次に、プロセスが喰らう「胃袋(CPUとメモリ)」を物理的にどう縛り上げるのか? 次回第2話、カーネルが牙をむく資源制御機構**「cgroups(Control Groups)とOOM Killerの物理」**へと歩みを進めましょう。


📝 3行で頭に入る『試験・実務・投資の要点まとめ』

  1. 【コンテナの物理実体】 コンテナ内に独立したOSカーネルは存在せず、たった1つのホストLinuxカーネルが特定プロセスに視野制限(幻覚)をかけているだけである。
  2. 【6大名前空間】 プロセスIDを隠蔽するPID、仮想NICを割り当てるNET、ルートディレクトリを差し替えるMNTなど、6つのNamespacesによって完全な空間隔離が成立する。
  3. 【システムコールの急所】 コンテナ起動の本質は CLONE_NEW* フラグを伴う clone() システムコールの発行であり、task_struct->nsproxy のポインタ分離によって実現されている。

確認クイズ

📝 確認クイズ(3問)