05. OS の基礎概念 - プロセス・メモリ・ファイルシステム

読了時間: 50-70分(移動中に分割して読むと丁度よい) 想定読者: 「ps -aux って何? ターミナルの黒い画面の向こうで何が動いてるの?」と思っている人 ゴール: OS が「あなたのアプリと CPU の間で何をしているか」を、自分の言葉で人に説明できる 手は動かさない: スマホ片手に読むだけでOK

この記事で持って帰れること

  • OS の役割 (翻訳者 / 秘書 / 警察官) を語れる
  • プロセスとスレッドの違い、メモリ管理 (仮想メモリ / ヒープ / スタック) を人に説明できる
  • SIGTERM と SIGKILL の差を理解し、graceful shutdown が組める
  • ps -aux の出力が呪文ではなく意味のある情報に見える
  • Docker や WSL の正体 (Linux カーネルの機能の上にある) を理解できる

前回 (04_暗号とTLS) との繋がり

ここまで「ネットワークの上で何が起きているか」を上から下に降りてきた (HTTP → DNS → TLS)。今回は そもそも「アプリが動いている土台」そのもの を覗く話。あなたが Go や Node.js で書いたコードは、最後は OS のプロセスとして CPU に乗る。ネットワーク章で扱ったソケット (socket)、ポート、TLS 証明書のファイル、全部 OS がアプリに提供してくれているもの。


大前提: なぜ「OS」を知らないとバックエンドで詰むのか

Chrome のアイコンをダブルクリックする。これだけの操作で、OS は裏で大量の仕事をしている。実行ファイルをディスクから探し、メモリに空きを確保し、CPU に「これを実行しろ」と命令し、通信や他アプリとの調停を全部やる。あなたはマウスを動かしただけ。残りは全部 OS の仕事。

バックエンドエンジニアになると、この OS の仕事が「自分の責任範囲」に侵入してくる。本番サーバーで自分のアプリが落ちる。ログには Out of memory。「メモリって何? なぜ足りない? どう監視する?」これが分からないと、ただ祈ることしかできない。

ps -aux でプロセス一覧を見る。top で CPU 使用率を見る。df -h でディスク残量を見る。これらは全部、OS の状態を覗く窓。窓の向こうで何が起きているか分からないと、覗いても何も読み取れない。

この記事はその「窓の向こう」の話。スマホで読んで、top の画面を見た時に「あ、あの記事で読んだやつだ」と思えれば勝ち。

この記事の楽しみ方

用語が多いが暗記不要。「世界観」を掴むのが目的。一気に読まなくていい。電車で1章、待ち時間で1章、と分けて読めるサイズに切ってある。


第1章: OS とは何か - ハードウェアと人間の翻訳者

1-1. ハードウェアは何も知らない

CPU は「1+1 を計算しろ」と命令されれば計算する。でも「Chrome を起動しろ」と言われても、Chrome が何か知らない。SSD は「2048 番のセクタを読め」と言われたら読むだけで、それが Chrome かなど興味ない。

つまり、ハードウェアは 超低レベルな命令しか理解できない。一方、人間は「Chrome を開きたい」みたいな 高レベルな要求 を持っている。この巨大なギャップを埋めるのが OS。

1-2. OS は「翻訳者」かつ「秘書」かつ「警察官」

OS には3つの顔がある。

翻訳者:「Chrome を起動して」という要求を機械語に翻訳する。

秘書: 複数アプリが同時に動く時、CPU の時間を切り分けて配る。CPU 1個でも、1秒間に何百回も切り替えれば「同時に動いて見える」。これを マルチタスク と言う。

警察官: アプリが他アプリのメモリを覗こうとしたら止める。クラッシュしたアプリが OS 全体を巻き込まないよう隔離する。

OS の本質

OS は「ハードウェアの上に、人間とアプリが使いやすい抽象層を作るソフトウェア」。

抽象化が OS の本質。あなたが書く Go や Python のコードは、本当はハードウェアの細かい違いを意識しないと動かない。でも OS がその違いを隠してくれているから、同じ Go コードが Mac でも Linux でも Windows でも動く。

「Write once, run anywhere」を支えているのは OS の抽象化 という視点を持つと、OS の存在意義が腹落ちする。

1-3. 主要な OS たち

世界には数百種類の OS があるが、エンジニアが意識すべきは大きく3系統。

OS系統バックエンドでの位置付け
LinuxUnix 系本番サーバーの 9 割以上。AWS、GCP、Azure 全部 Linux ベース
macOSUnix 系(Darwin)開発機で人気。サーバーには使われない
WindowsNT 系(独自)企業デスクトップが多い。サーバー版も存在するが少数派

Linux と macOS が「Unix 系」と呼ばれるのは、1970年代に AT&T ベル研究所で作られた Unix の流れを汲んでいるから。ターミナルで ls cd grep が共通で使えるのは、両方とも Unix の子孫だから。

Windows は別系統(Windows NT 設計)だが、最近は WSL (Windows Subsystem for Linux) で内部に Linux 環境を持てる。

Unix が世界を支配した理由

1970年代、Unix のソースコードは大学に安価で配布され、学生たちが改造して学んだ。これが Linux(1991年、フィンランドの学生 Linus Torvalds 作)や BSD(バークレー校)に繋がる。macOS は BSD ベース。Microsoft は独自路線で Windows NT を作った。これが \ vs / のような文化的断絶を生んだ。


第2章: Unix の哲学 - 小さなツール、テキストで通信

2-1. 「Unix 哲学」とは何か

Unix を作った人たち(Ken Thompson、Dennis Ritchie)は設計の原則を残した。これが Unix 哲学。要点は3つ。

  1. 一つのことを上手くやれ (Do one thing well)
  2. プログラムは互いに連携できるように書け
  3. テキストストリームを共通のインターフェースにせよ

「小さくシンプルなツールをたくさん作って、それらをパイプで繋いで大きな仕事をする」という考え方。

2-2. 具体例: ログから「アクセス回数トップ10の IP」を取る

cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10

これだけで終わる。catawksortuniqhead も、それぞれ独立した小さなコマンド。|(パイプ)で繋ぐと、ログ解析という複雑な仕事ができる。

パイプ | の本質

パイプは「あるコマンドの出力を、次のコマンドの入力に流す」仕組み。Unix が世界に残した最大の発明の一つ。Python のジェネレータ、SQL のサブクエリ、Docker のマルチステージビルドまで、現代のあらゆる「処理を繋ぐ」概念のルーツはここ。

支えているのは「全部テキストで流す」という規約。バイナリじゃなくテキストだから、人間が途中で見られるしデバッグできる。

2-3. Everything is a file(すべてはファイル)

Unix のもう一つの強烈な哲学が「すべてはファイル」。

ディスク上のファイルだけがファイルじゃない。Unix では以下も全部「ファイル」として扱える。

ファイルとしての姿
キーボード入力/dev/tty から読める
ハードディスク/dev/sda
ネットワーク接続ソケットファイル
プロセスの情報/proc/1234/status(Linux)
乱数/dev/random から読み出すと乱数が出てくる
ゴミ箱/dev/null(書き込むと消える)

例えば、Linux で cat /proc/cpuinfo と打つと CPU の情報が出てくる。これは実体としてのファイルじゃない。OS が「ファイルを読まれた振り」をして CPU 情報を返している。

なぜ「ファイル」に統一したのか

「読む」「書く」「開く」「閉じる」という共通操作が、ありとあらゆるリソースに使える。プログラマは open() read() write() close() の4つだけで、ハードディスクもネットワークも CPU 情報も操作できる。

これが「統一されたインターフェース」の威力。OOP の「ポリモーフィズム」のはるか前に、Unix はこれを OS レベルで実現していた。

/dev/null は宇宙のゴミ箱

/dev/null は「書き込んだものが消えるファイル」。./noisy.sh > /dev/null 2>&1 で「標準出力もエラー出力も全部捨てる」。本番運用で「ログは要らないけど実行はしたい」時に使う。


第3章: カーネルとユーザー空間 - OS の中の二つの世界

3-1. カーネルって何

OS の本体を カーネル (kernel = 核) と呼ぶ。ハードウェアを直接操作できる唯一のソフトウェア。「OS = カーネル + 周辺ツール」と思うとよい。Linux カーネル + GNU ツール群 = Linux、Darwin カーネル + Apple ツール = macOS、NT カーネル + Microsoft ツール = Windows。

ニュースで「Linux」と言われる時、大抵は Ubuntu / Red Hat / CentOS のような ディストリビューション(カーネル + ツール集の詰め合わせ)のこと。

3-2. ユーザー空間とカーネル空間

CPU には「特権モード」と「ユーザーモード」がある(Intel 系では Ring 0 と Ring 3)。

  • カーネル空間 (Ring 0): OS のコアが動く。ハードウェアに直接アクセスできる
  • ユーザー空間 (Ring 3): アプリが動く。ハードウェアには直接触れない

なぜこんな仕分けをするか。セキュリティと安定性のため。全アプリがハードウェアに直接触れたら、悪意やバグでディスクを全消去できてしまう。それを防ぐため「アプリは直接ハードウェアを触れない」を CPU レベルで強制している。

┌────────────────────────────────┐
│   ユーザー空間 (Ring 3)        │
│   Chrome / Slack / VS Code 等  │
│                                │
│   ハードウェアに触れない       │
└──────────┬─────────────────────┘
           │ system call (橋渡し)
           ▼
┌────────────────────────────────┐
│   カーネル空間 (Ring 0)        │
│   Linux カーネル等             │
│                                │
│   ハードウェアに触れる         │
└──────────┬─────────────────────┘
           │
           ▼
   [CPU][メモリ][SSD][NIC]...

↓ ユーザー空間とカーネル空間の関係を図で表したもの ↓

flowchart TB
    subgraph User["ユーザー空間 (Ring 3)"]
        A1[Chrome]
        A2[Slack]
        A3[VS Code]
    end
    subgraph Kernel["カーネル空間 (Ring 0)"]
        K1[プロセス管理]
        K2[メモリ管理]
        K3[ファイルシステム]
        K4[ネットワークスタック]
        K5[デバイスドライバ]
    end
    subgraph HW["ハードウェア"]
        H1[CPU]
        H2[メモリ]
        H3[SSD]
        H4[NIC]
    end
    A1 -- system call --> Kernel
    A2 -- system call --> Kernel
    A3 -- system call --> Kernel
    Kernel --> HW

3-3. システムコール - ユーザーからカーネルへの橋

じゃあアプリはどうやってファイルを読むのか。答えが システムコール (syscall)。アプリが「ファイル読みたい」と思ったらカーネルに頼む。カーネルが代わりにディスクにアクセスして結果を返す。

C で int fd = open("/etc/passwd", O_RDONLY); を実行すると内部で open システムコールが呼ばれる。CPU が一瞬「ユーザーモード」から「カーネルモード」に切り替わり、ディスクアクセスし、結果を返して「ユーザーモード」に戻る。

↓ システムコールの流れ(ユーザー → カーネル → ハードウェア)↓

sequenceDiagram
    participant App as アプリ (ユーザーモード)
    participant Kernel as カーネル (カーネルモード)
    participant HW as ハードウェア (SSD)
    App->>App: open("/etc/passwd")
    App->>Kernel: syscall (モード切替)
    Note over Kernel: ユーザーモード→カーネルモード
    Kernel->>HW: ディスク読み取り命令
    HW-->>Kernel: データ返却
    Kernel-->>App: ファイルディスクリプタ
    Note over App: カーネルモード→ユーザーモード
    App->>App: 後続処理を継続

システムコールの実務的な意味

システムコールは「アプリと OS の境界線」そのもの。アプリができることは、究極的にはシステムコールの組み合わせでしかない。Linux は約 400 種類。open read write close fork exec socket mmap などが代表。

ユースケース

  • パフォーマンス調査: strace ./myapp でアプリが呼ぶシステムコールが丸見え。「なんでこの処理遅いの?」を追う必殺技
  • セキュリティ: Docker の隔離はシステムコール制限(seccomp)で実現

システムコールはタダじゃない

ユーザーモードとカーネルモードの切り替えはコストが高い。「1バイトずつ100万回 read」より「1MB 一気に read」が圧倒的に速い。バッファリングが重要な理由はここ。Nginx や Redis はシステムコール回数を極限まで減らす設計。


第4章: プロセスとスレッド - 動くプログラムの正体

4-1. プロセスとは何か

実行中のプログラムを プロセス と呼ぶ。ディスクの実行ファイルはただのファイル。起動すると OS がメモリ上に展開し、CPU が実行を始める。この「動いている状態」がプロセス。

同じ Chrome を3回起動すれば、実行ファイルは1個だがプロセスは3個できる。各プロセスは固有の PID (Process ID) を持つ。ps コマンドで一覧が見られる。

4-2. スレッドとは何か

プロセスの中で動く「実行の流れ」が スレッド。1プロセスは1個以上のスレッドを持つ。

イメージ:プロセス = 会社、スレッド = 社員。会社は社員を雇って仕事をさせる。社員が複数いれば並行作業できる。社員同士は同じオフィス(メモリ空間)を共有する。

Chrome がタブをたくさん開いても落ちにくいのは、内部で大量のスレッド(と複数プロセス)で並行処理しているから。

4-3. プロセスとスレッドの違い

これがよく聞かれる質問の答え。

観点プロセススレッド
メモリ独立した空間を持つ同じプロセス内で共有
通信プロセス間通信 (IPC) が必要、重い直接メモリ共有で軽い
起動コスト重い(数ミリ秒〜)軽い(マイクロ秒〜)
クラッシュ時1個落ちても他は無事1個落ちるとプロセス全体が死ぬ
隔離OS が強く隔離同じ船の上、お互い干渉できる

いつプロセス、いつスレッドを使うか

プロセス: 独立性が大事な時、セキュリティ的に隔離したい時。Chrome がタブごとにプロセス分けるのは、悪意あるサイトが他タブを侵せないようにするため。

スレッド: 高速な並行処理が欲しい時、同じデータを大量にやり取りする時。

実務例: Nginx は「マスタープロセス + ワーカープロセス複数」構成。各ワーカーの中で多数の接続をイベントループで処理。両者の良いとこ取り。

スレッドは怖い

スレッドは同じメモリを共有するので、複数スレッドが同じ変数を同時に書き換えると 競合状態 (race condition) が起きる。count = count + 1 を2スレッドで同時実行すると、期待は2増えるはずが1しか増えないことがある(読み込み→計算→書き込みの途中で割り込まれる)。

これがマルチスレッドプログラミングの地獄。ロック、ミューテックス、アトミック操作で対処するが難易度が高い。Go の goroutine や Rust の所有権システムは、この地獄を緩和する設計思想で生まれている。


第5章: プロセスのライフサイクル - 生まれて、働いて、死ぬ

5-1. プロセスの誕生 - fork と exec

Unix の新プロセス生成は独特。「無から作る」のではなく「既存プロセスをコピーして作る」。これを fork と呼ぶ。fork() を呼ぶと親の完全なコピー(子)が作られる。親と子はメモリの中身も同じ。PID だけが違う。

fork したばかりの子は「親のコピー」でしかない。これを別プログラムにするのが exec システムコール。「自分のプロセスの中身を別の実行ファイルに置き換える」操作。PID は変わらない。

シェルで ls を打った時の流れ:

  1. bash が fork() で子プロセスを作る
  2. 子が exec("/usr/bin/ls") で中身を ls に置き換える
  3. ls が実行される
  4. 親(bash)は wait() で子の終了を待つ
  5. 子が終わったら bash が制御を取り戻す

↓ fork / exec / wait の親子関係 ↓

sequenceDiagram
    participant Bash as bash (親プロセス)
    participant Child as 子プロセス
    participant LS as ls (置き換え後)
    Bash->>Bash: fork() 呼び出し
    Bash->>Child: 子プロセス生成 (親のコピー)
    Bash->>Bash: wait() で子の終了待ち
    Child->>LS: exec("/usr/bin/ls")
    Note over Child,LS: PID は変わらず<br/>中身だけ ls に置き換え
    LS->>LS: ls 実行
    LS-->>Bash: exit ステータス
    Bash->>Bash: 制御を取り戻し<br/>プロンプト表示

↓ プロセスのライフサイクル(状態遷移)↓

stateDiagram-v2
    [*] --> Created: fork()
    Created --> Ready: 準備完了
    Ready --> Running: スケジューラが選択
    Running --> Ready: タイムスライス終了
    Running --> Waiting: I/O 待ち / sleep
    Waiting --> Ready: I/O 完了 / 起床
    Running --> Terminated: exit()
    Terminated --> [*]: 親が wait()
    Terminated --> Zombie: 親が wait() しない
    Zombie --> [*]: init が引き取り

なぜ fork + exec の2段階か

「コピー」と「中身入れ替え」を分けているのには深い意味がある。fork 直後、exec する前に、子プロセスは色々な準備ができる。標準入出力を別ファイルに繋ぎ替えたり、環境変数を変えたり。

これが Unix のパイプやリダイレクトを支えている。ls > out.txt が動くのは、fork した子が exec 前に「標準出力を out.txt に繋ぎ替え」てから ls を実行するから。Unix の柔軟性のすべてがこの分離設計から来ている。

5-2. プロセスの死とゾンビ

プロセスは exit() で終了し、終了ステータス(成功なら 0)を OS に渡す。重要なのは、プロセスは死んでも完全には消えないこと。親が wait() で受け取るまで、OS は「死んだプロセスの記録」を残す。これが ゾンビプロセス

$ ps -aux
USER  PID  STAT  COMMAND
root  500  Z     [defunct]   ← ゾンビ

普通は親がすぐ wait() するので一瞬で消える。親が忘れるとゾンビが残り、大量に溜まると PID 枯渇でシステムが新規プロセスを作れなくなる。

アンチパターン: ゾンビを撒き散らすコード

プロセスを起動するアプリで、終了を待たずに次々と新規プロセスを作るのは危険。while True: subprocess.Popen(["./worker.sh"]) のように待たないコードは長時間動かすとゾンビが溜まる。

正解: 子プロセスの終了を待つ、ダブルフォークして孤児にする(init が拾う)、または SIGCHLD ハンドラを設定する。

Docker コンテナで「init を入れろ」と言われるのも、ゾンビ刈り取り機構が必要だから。tini という小さなプログラムがこの目的で使われる。

5-3. 孤児プロセス

親が先に死んで子が残ると 孤児プロセス。孤児は自動的に init プロセス(PID 1)が引き取る。init は責任を持って wait() する。だから孤児は溜まらない。

Linux のすべてのプロセスを遡ると、最終的に PID 1 の systemd(または昔ながらの init)に行き着く。「プロセスの王様」。


第6章: メモリ管理 - 限られた RAM をどう配るか

6-1. 仮想メモリという嘘

16GB の RAM に Chrome、Slack、VS Code、Docker を起動する。各アプリは「メモリのアドレス 0 から自分のもの」と思って動いている。全部が「0番地から自分のもの」って、ぶつからないの? ぶつからない。仮想メモリ という仕組みで、各プロセスに「自分専用の仮想アドレス空間」を提供しているから。

プロセスA の世界:            プロセスB の世界:
0x0000 ┌─────────┐           0x0000 ┌─────────┐
       │ コード  │                  │ コード  │
       │ ヒープ  │                  │ ヒープ  │
       │ スタック│                  │ スタック│
       └─────────┘                  └─────────┘

A も B も「自分が 0 番地から始まる」と思っているが、実際の物理メモリでは全く別の場所にマッピングされている。この「仮想 → 物理」の対応表を OS が管理。

↓ 仮想メモリとページングの仕組み ↓

flowchart LR
    subgraph ProcA["プロセスA の仮想アドレス空間"]
        VA1[ページ 0x0000]
        VA2[ページ 0x1000]
        VA3[ページ 0x2000]
    end
    subgraph ProcB["プロセスB の仮想アドレス空間"]
        VB1[ページ 0x0000]
        VB2[ページ 0x1000]
    end
    subgraph PageTable["ページテーブル (OS が管理)"]
        PT[仮想→物理 マッピング]
    end
    subgraph Physical["物理メモリ"]
        P1[フレーム 0x8000]
        P2[フレーム 0x9000]
        P3[フレーム 0xA000]
        P4[フレーム 0xB000]
    end
    subgraph Disk["スワップ領域 (ディスク)"]
        D1[退避されたページ]
    end
    VA1 --> PT
    VA2 --> PT
    VA3 --> PT
    VB1 --> PT
    VB2 --> PT
    PT --> P1
    PT --> P2
    PT --> P3
    PT --> P4
    P4 -. 使われてないページ .-> D1
    D1 -. 必要時に読み戻し .-> P4

6-2. なぜ仮想メモリが必要か

  1. 隔離: A が 0x1234 にアクセスしても B のデータには触れない。物理アドレスが違うから
  2. 使いやすさ: アプリは「自分は 0 番地から始まる」と思って書ける
  3. 柔軟性: 物理メモリが足りなくなったらディスクに退避できる(スワップ)

6-3. ページング

仮想メモリの管理単位を ページ と呼ぶ(普通 4KB)。メモリを 4KB ずつに分割し、ページ単位で物理メモリにマッピング。変換は CPU の MMU が高速にやってくれる。

物理メモリが足りなくなると、OS は「あまり使われていないページ」をディスクに退避する(スワップアウト)。必要になったら読み戻す(スワップイン)。

スワップが地獄を呼ぶ

RAM はナノ秒、ディスク(SSD でも)はマイクロ〜ミリ秒。1000倍以上遅い。スワップ頻発でメモリアクセス毎にディスクが動くとアプリが致命的に遅くなる。これを スラッシング と呼ぶ。本番でスワップが激しく使われているのは「メモリ不足」の兆候。free -hvmstat で監視するのが定石。

6-4. メモリマップ (mmap)

ファイルをメモリ上にマッピングし、まるでメモリのように扱う仕組みが mmapread() でちまちま読む代わりに、配列のように直接アクセスできる。

mmap の実務的な価値

ユースケース: 巨大ファイルの一部だけ読みたい時(必要な部分だけ物理メモリにロード)、DB の内部実装(SQLite、LMDB が使う)、プロセス間メモリ共有。

落とし穴: 便利だがページフォールトが裏で起きるので、ランダムアクセスが激しいと遅い。シーケンシャル読み向き。


第7章: スタックとヒープ - 二種類のメモリ

7-1. プロセスのメモリ配置

1つのプロセスが使うメモリは、用途別に区画分けされている。

高アドレス  ┌─────────────────┐
            │ スタック        │ ← 関数呼び出し、ローカル変数
            │   ↓ 下向きに伸びる │
            │                 │
            │   ↑ 上向きに伸びる │
            │ ヒープ          │ ← 動的に確保した変数
            ├─────────────────┤
            │ BSS / データ    │ ← グローバル変数、static 変数
            ├─────────────────┤
            │ テキスト        │ ← コード(機械語)
低アドレス  └─────────────────┘

このうち、エンジニアが日常的に意識するのが スタックヒープ

↓ プロセスのメモリレイアウト(5つの区画)↓

flowchart TB
    subgraph Memory["プロセスのメモリ空間"]
        S["スタック (Stack)<br/>ローカル変数・関数呼び出し<br/>↓ 下向きに伸びる"]
        Gap["未使用領域"]
        H["ヒープ (Heap)<br/>malloc / new で確保<br/>↑ 上向きに伸びる"]
        BSS["BSS<br/>未初期化のグローバル変数"]
        Data[".data<br/>初期化済みグローバル変数"]
        Text[".text<br/>機械語コード (読み取り専用)"]
    end
    S -.->|高アドレス| Gap
    Gap -.-> H
    H --> BSS
    BSS --> Data
    Data -.->|低アドレス| Text

7-2. スタック - 関数呼び出しの記憶

スタックは関数を呼ぶたびに「積まれていく」メモリ領域。LIFO(後入れ先出し)。関数を呼んだら積む、終わったら外す。

特徴: 確保・解放が高速(ポインタを動かすだけ)、サイズは小さい(普通 1〜8MB)、ローカル変数や引数や戻り先アドレスが入る、関数終了で自動解放。

7-3. ヒープ - 動的に確保するメモリ

より大きく長生きなデータのための領域が ヒープ。Go なら make()、C なら malloc()、Java なら new

特徴: 確保・解放にコストあり、サイズは大きい(GB 単位)、関数が終わっても残る、自分で解放するか GC に任せる。

7-4. スタックオーバーフロー

スタックは 1〜8MB と小さい。これを超えるとクラッシュ。

アンチパターン: 無限再帰

末尾再帰最適化のない言語(Go、Java、Python)で深い再帰を書くと簡単にスタック爆発する。func recurse() { recurse() } で即死。

正解: ループに書き換える、もしくは明示的にスタック構造を使う。Stack Overflow(質問サイト)の名前はこのエラーから来ている。

7-5. メモリリーク

ヒープから確保したメモリを解放し忘れる現象が メモリリーク。C/C++ は手動管理なのでリークしやすい。Go や Java は GC があるので参照が切れれば自動回収。

ただし GC ありの言語でも「参照を持ち続けている」とリークになる。

var cache = make(map[string][]byte)
 
func handleRequest(key string) {
    cache[key] = bigData    // どんどん追加するだけ
}

cache から削除する処理がない。リクエスト毎に膨らみ続けメモリを食い尽くす。GC は「参照されているもの」は回収しない。これが GC ありの言語のリーク典型パターン。


第8章: ファイルシステム - inode と階層構造

8-1. ファイルシステムって何

ディスク(HDD/SSD)はただの「巨大な連続したバイト列」。これに「フォルダ構造」「ファイル名」「権限」みたいな概念を被せるのが ファイルシステム

ファイルシステムは「ディスク上のデータの整理ルール」。同じ SSD でも、フォーマットの仕方(どのファイルシステムを使うか)で挙動が変わる。

8-2. inode - ファイルの正体

Unix 系ファイルシステムでは、ファイルは inode という構造で管理される。

inode は「ファイルのメタ情報を持つ小さなデータ構造」。具体的には:

  • ファイルサイズ
  • 所有者 / グループ
  • 権限 (rwx)
  • 更新日時 / 作成日時
  • データが実際に格納されているディスクブロックのアドレス

ファイル名は inode に含まれない。ファイル名は「ディレクトリエントリ」という別の場所に書かれていて、「この名前は inode 番号 12345」みたいに対応付けている。

ディレクトリ (/home/takato/)
├── "memo.txt"     → inode 12345
├── "data.csv"     → inode 12346
└── "config.yml"   → inode 12347

inode 12345
├── size: 1234 bytes
├── owner: takato
├── mode: rw-r--r--
└── data blocks: [0x1000, 0x2000, ...]

↓ inode を中心としたファイルシステム構造 ↓

flowchart LR
    subgraph Dir["ディレクトリエントリ (/home/takato/)"]
        E1["'memo.txt'"]
        E2["'data.csv'"]
        E3["'config.yml'"]
    end
    subgraph Inodes["inode テーブル"]
        I1["inode 12345<br/>size, owner, mode<br/>data blocks ポインタ"]
        I2["inode 12346<br/>size, owner, mode<br/>data blocks ポインタ"]
        I3["inode 12347<br/>size, owner, mode<br/>data blocks ポインタ"]
    end
    subgraph Blocks["データブロック (実体)"]
        B1[0x1000]
        B2[0x2000]
        B3[0x3000]
        B4[0x4000]
    end
    E1 --> I1
    E2 --> I2
    E3 --> I3
    I1 --> B1
    I1 --> B2
    I2 --> B3
    I3 --> B4

8-3. ハードリンクとシンボリックリンク

inode の概念があるおかげで、Unix では「同じファイルに複数の名前」を付けられる。

ハードリンク: 同じ inode を指すディレクトリエントリを複数作る。

ln original.txt link.txt

original.txtlink.txt は inode 番号が同じ。中身も同じ。どちらを変更しても両方に反映される。original.txtrm しても、link.txt から同じ inode が参照されているので、データは消えない。

シンボリックリンク: 「別のパスへの矢印」を作る。

ln -s original.txt link.txt

link.txt は独立した小さなファイルで、中身は「original.txt」というパス文字列。original.txt を消すと link.txt は「リンク切れ」になる。

リンクの実務的な使い所

シンボリックリンクのユースケース

  • /usr/local/bin/pythonpython3.11 に張る(バージョン切り替え)
  • 設定ファイルを ~/dotfiles/ に置いて、~/.zshrc~/dotfiles/zshrc のリンクにする
  • デプロイで「current」ディレクトリを最新リリースに付け替える(Capistrano パターン)

ハードリンクのユースケース

  • バックアップツール(Time Machine など)が「変更がないファイルはハードリンクで参照」して容量節約
  • 普段の開発ではほぼ使わない

8-4. ファイルシステムの種類

Unix 系の代表的なファイルシステム:

名前主な使用先特徴
ext4Linux のデフォルト安定、無難、迷ったらこれ
xfs大規模 Linux サーバー巨大ファイル・高並列に強い
zfsサーバー、NASスナップショット、データ整合性、圧縮、最強だが重い
btrfsLinux 一部zfs の Linux 版を目指したやつ
APFSmacOSスナップショット、暗号化、SSD 向け最適化
NTFSWindowsNT 系の標準

なぜファイルシステムが何種類もあるか

用途が違うから。

  • 「とにかく安定」が欲しい → ext4
  • 「テラバイト級のファイル」を扱う → xfs
  • 「データが絶対に壊れてほしくない、スナップショット欲しい」 → zfs
  • 「SSD のスマホで省電力に」 → APFS

どれが「最強」というのはない。それぞれトレードオフがある。

ちなみに、Google や Facebook のような巨大企業は 独自のファイルシステム を作っている(GFS、HDFS、Tectonic 等)。「ペタバイト級の分散ファイルシステム」になると既存の ext4 や xfs では足りないから。

8-5. ジャーナリング

現代のファイルシステムは大抵 ジャーナル という仕組みを持つ。

「これからファイルを書き換える」と先に記録(journal)してから実際の書き換えをする。途中で電源が落ちても、起動時にジャーナルを見れば「途中だった操作」を完了またはロールバックできる。

データベースのトランザクションログと同じ思想。データが半端な状態にならないための保険。


第9章: パーミッションと所有権

9-1. Unix の権限モデル

Unix 系では、ファイルやディレクトリに 所有者 (owner) グループ (group) その他 (others) の3階層で権限を設定する。

-rw-r--r--  1  takato  staff  1234  May 17  memo.txt
 ^^^^^^^^^     ^^^^^^  ^^^^^
 権限          所有者  グループ

権限は r (read) w (write) x (execute) の3つを、所有者・グループ・その他のそれぞれに割り当てる。

表記意味
-rw-r--r--所有者: 読み書き / グループ: 読みのみ / その他: 読みのみ
-rwx------所有者だけが全部できる
-rwxr-xr-x所有者は全部 / 他は読み実行のみ(実行ファイルでよく見る)

9-2. 数値表記 - 644 とか 755 とか

権限は数値でも表せる。r=4, w=2, x=1 の和。

  • rwx = 4+2+1 = 7
  • rw- = 4+2 = 6
  • r-x = 4+1 = 5
  • r-- = 4

これを所有者・グループ・その他の順で並べる。

  • 644 = rw-r--r--(普通のファイル)
  • 755 = rwxr-xr-x(実行ファイル、ディレクトリ)
  • 600 = rw-------(秘密のファイル、SSH キーなど)
  • 777 = rwxrwxrwx(誰でも何でもできる、危険)

9-3. root と sudo

Unix には特権ユーザー root がいる。UID 0。何でもできる。

普段は一般ユーザーで作業し、必要な時だけ sudo で root 権限を借りる。これが Unix の鉄則。

root 運用の危険

root で常時作業するのは超危険。rm -rf / を間違えて打ったら、システム全消去。

本番サーバーでは:

  • SSH ログインは一般ユーザーのみ
  • root への直接ログインは禁止
  • sudo はログを残す(/var/log/auth.log
  • sudo に NOPASSWD(パスワードなし)を許可するのは限定的に

「権限は最小限に」(Principle of Least Privilege)。これは Linux 章でも触れたが、本番運用の鉄則。


第10章: シグナル - プロセス間の連絡手段

10-1. シグナルとは

プロセスに「メッセージを送る」仕組みが シグナル。OS が用意した、ごく単純な「通知システム」。

kill <PID> というコマンドを聞いたことがあるかもしれない。これは実は「プロセスにシグナルを送る」コマンド。デフォルトでは SIGTERM というシグナルを送る。「殺す」とは限らない(紛らわしい名前)。

10-2. 代表的なシグナル

シグナル番号意味
SIGTERM15終了してほしい(お願いベース、無視可能)
SIGKILL9強制終了(無視できない、即死)
SIGINT2Ctrl+C で送られる「中断」
SIGHUP1端末が切れた、設定再読み込み
SIGSEGV11セグメンテーション違反(不正なメモリアクセス)
SIGSTOP19一時停止
SIGCONT18再開
SIGCHLD17子プロセスが終了した通知

10-3. SIGTERM と SIGKILL の違い

これは実務で必ず問われる。

  • SIGTERM: 「お願い、終わってくれる?」 アプリ側で受け取って「片付けてから終わる」ができる。ログを書き出し、DB 接続を閉じ、キャッシュをフラッシュしてから終了。graceful shutdown と呼ぶ
  • SIGKILL: 「問答無用で殺す」 アプリ側では何もできない。OS がプロセスをその場で消す
kill 1234           # SIGTERM (デフォルト)
kill -9 1234        # SIGKILL (強制)
kill -TERM 1234     # 同じく SIGTERM

↓ シグナル送信フロー(SIGTERM → graceful shutdown と SIGKILL → 即死の比較)↓

sequenceDiagram
    participant User as 運用者
    participant OS as カーネル
    participant App as 対象プロセス
    Note over User,App: ケース1: SIGTERM (graceful)
    User->>OS: kill 1234
    OS->>App: SIGTERM 配送
    App->>App: シグナルハンドラ起動
    App->>App: DB 接続を閉じる
    App->>App: ログをフラッシュ
    App->>OS: exit(0)
    OS-->>User: 終了完了
    Note over User,App: ケース2: SIGKILL (強制)
    User->>OS: kill -9 1234
    OS->>App: プロセスを即時消去
    Note over App: ハンドラ実行されず<br/>後片付けできない
    OS-->>User: 終了完了 (データ破損リスク)

kill -9 を安易に使うな

SIGKILL を送られたプロセスは、書きかけのファイルが半端だったり、DB トランザクションが宙ぶらりんになったりする。

正しい順序

  1. まず SIGTERM を送る
  2. 数秒待つ
  3. それでも生きていたら SIGKILL

Docker の docker stop も内部でこの順序を踏んでいる(デフォルトで SIGTERM → 10秒待つ → SIGKILL)。

自分のアプリでは SIGTERM を捕まえて適切に片付ける処理を入れるのが本番運用のマナー。Go なら signal.Notify、Python なら signal.signal() で実装できる。

10-4. SIGSEGV - 不正アクセス

C や C++ で「ヌルポインタを参照した」「配列の境界を超えた」みたいな不正なメモリアクセスをすると OS が検知して SIGSEGV を送ってくる。

int* p = NULL;
*p = 42;   // SIGSEGV

これは「セグメンテーション違反 (Segmentation Fault)」というエラーで現れる。C 系言語のクラッシュの代表。

Go や Rust は型システムで防ぐ(Go は nil でも nil pointer dereference というエラーで panic する)。


第11章: Linux カーネル開発の現代

11-1. Linus Torvalds と Linux

Linux カーネルの作者は Linus Torvalds(リーナス・トーバルズ)。1991年、フィンランドの大学生だった彼が「趣味で OS を作ってみた」という Usenet 投稿が始まり。

それから35年、Linux は世界のサーバーの 90% 以上、Android スマホ、スパコン、組み込みデバイスを支配する OS になった。

Linus は今もカーネルの「最終承認者」。世界中のエンジニアが書いたコードを彼(と少数の信頼するメンテナ)がレビューしてマージする。

11-2. Linux カーネルの規模

Linux カーネルのソースコードは現在 約3000万行。コミット数は 百万コミット超。これは世界最大級の OSS プロジェクト。

開発体制:

  • Linus が最終決定権
  • 各サブシステムに「メンテナ」がいる(ファイルシステム、ネットワーク、ドライバ等)
  • パッチは「メーリングリスト (LKML)」でやり取りされる
  • Git はもともと Linux カーネル管理のために Linus が作った

Git は Linux のために生まれた

2005年、Linux カーネルが使っていた「BitKeeper」というバージョン管理ソフトのライセンスが問題になり、Linus は2週間で Git を自作した。

今あなたが git push できるのは、Linus が「使いやすいバージョン管理がなかったから自分で作った」結果。

Linux カーネル → Git → GitHub → 現代のソフトウェア開発文化、という流れ。Linus 一人が作った OS が、世界の開発スタイルを変えた。

11-3. ディストリビューションの世界

Linux カーネルだけでは何もできない。シェル、コマンド、ライブラリ、パッケージマネージャ、デスクトップ環境などを揃えた「セット」が ディストリビューション

代表的なもの:

  • Ubuntu: デスクトップ・サーバー両方で人気
  • Debian: Ubuntu の親、安定志向
  • Red Hat Enterprise Linux (RHEL): 企業向け有償サポート
  • CentOS / Rocky Linux / AlmaLinux: RHEL 互換の無料版
  • Arch Linux: 「自分で全部設定したい」上級者向け
  • Alpine Linux: 軽量、Docker イメージのベースで定番

クラウドサーバーは Ubuntu か Amazon Linux か RHEL 系を選ぶことが多い。Docker のベースイメージは Alpine か Debian。


第12章: 3大 OS の系統比較

12-1. macOS - 美しき Unix の末裔

macOS のカーネルは Darwin。Darwin は Mach(カーネルアーキテクチャ)と BSD(Unix の一系統)のハイブリッド。

特徴:

  • 商用 Unix の血を引く(だから ls grep などが Linux と似ている)
  • ファイルシステムは APFS
  • パッケージマネージャは Homebrew(非公式だが事実上の標準)
  • 開発機としては最高、サーバーには使われない

12-2. Linux - サーバーの覇者

カーネルは Linux。ユーザーランドは GNU(だから「GNU/Linux」と呼ぶのが厳密)。

特徴:

  • 完全オープンソース、無料、改造自由
  • 世界中のサーバーで動く(AWS EC2、GCP Compute Engine、皆 Linux)
  • Docker、Kubernetes など現代インフラの基盤
  • バックエンドエンジニアの「家」

12-3. Windows - 独自路線の巨人

カーネルは Windows NT。Unix とは別系統。

特徴:

  • 企業デスクトップで最大シェア
  • ファイルシステムは NTFS
  • ファイルパス区切りが \ で Unix と違う(互換性問題の元)
  • 最近は WSL2 で内部に Linux カーネルを動かせる

WSL2 の革命

WSL2 (Windows Subsystem for Linux 2) は、Windows の中で本物の Linux カーネルを動かす仕組み。VM ベースだが軽量で、Windows ⇔ Linux のファイル共有もできる。

これにより、Windows 機でも「実質 Linux」で開発できるようになった。Microsoft が GitHub を買収して以降、Windows は急速に「開発者ファースト」にシフトしている。


第13章: Docker と WSL の裏で OS は何をしているか

13-1. Docker の正体

Docker は「軽量な仮想マシン」と言われることがあるが、厳密には違う。Docker は Linux カーネルの機能を使って「プロセスを隔離」しているだけ

使っている Linux カーネル機能:

  • namespaces: プロセスから見える世界を分離する(プロセス、ネットワーク、ファイルシステム等)
  • cgroups: CPU、メモリの使用量に制限をかける
  • chroot / pivot_root: ルートディレクトリを差し替える
  • OverlayFS: イメージのレイヤーを重ねる

Docker コンテナは、ホスト OS(Linux カーネル)の上で動く「特別に隔離されたプロセス」。仮想マシンのように OS を丸ごとエミュレートしない。だから軽い、速い。

コンテナと VM の違い

仮想マシン (VM): ハードウェアごとエミュレート。ゲスト OS を完全に動かす。重い、遅い、でも完全隔離

コンテナ (Docker): ホスト OS のカーネルを共有。プロセスを隔離するだけ。軽い、速い、でもカーネルレベルでは同じ世界

同じハードに VM なら10個、コンテナなら1000個動かせるイメージ。

Mac で Docker が動くのは、実は裏で Linux VM を起動してその中でコンテナを動かしているから。Docker Desktop は LinuxKit という小さな Linux VM を持っている。

13-2. WSL2 の正体

WSL2 も似ている。Windows の中で「Hyper-V」という Microsoft の仮想化技術を使って軽量な Linux VM を起動し、その中で Linux カーネルを動かす。

Windows ⇔ Linux の橋渡しは、ファイル共有プロトコル(9P)や、コマンドの相互呼び出し(wsl.exe cmd.exe)で実現している。

13-3. なぜ Linux カーネルが大事か

Docker も Kubernetes も WSL2 も、根本は Linux カーネルの機能に依存している。つまり、現代のバックエンド開発の 下層は全部 Linux カーネル

クラウドサーバーは Linux、コンテナは Linux、CI/CD のランナーも Linux、エッジで動く AWS Lambda も内部は Linux。

「Linux を知る = 現代インフラの土台を知る」ということ。


第14章: もう一度 ps -aux の話

ここまで読んで、最初の「ps -aux って何?」に戻ろう。

$ ps -aux
USER       PID  %CPU %MEM    VSZ   RSS  STAT  COMMAND
root         1   0.0  0.1   1234  5678   Ss   /sbin/init
takato    1234   2.3  1.5  12345  6789   S    /usr/bin/google-chrome
takato    1235   0.1  0.3   3456  1234   R    /usr/bin/code
www-data  2000   5.0  0.5  20000  8000   S    nginx: worker process

今なら全部読める。

  • USER: そのプロセスを動かしている 所有者(第9章)
  • PID: プロセス固有の通し番号(第4章)。PID 1 は init(第5章)
  • %CPU / %MEM: CPU とメモリの使用率
  • VSZ: 仮想メモリ のサイズ(第6章)。アプリが「これだけ使える」と思っている量
  • RSS: Resident Set Size = 実際に物理メモリに乗っている量
  • STAT: プロセスの状態。R=実行中、S=スリープ、Z=ゾンビ(第5章)、Sss は session leader
  • COMMAND: 何のプログラムか

最初は呪文だったこの一行が、各列の意味が分かるようになる。これが「OS の窓の向こうが見える」状態。

本番サーバーで ps -aux を打って、STATZ が大量に並んでいたら「あ、ゾンビが溜まってる、親プロセスのコードがおかしい」と即座に判断できる。%MEM が一個のプロセスで 80% を超えていたら「メモリリーク疑惑、pmapvalgrind で調べよう」と動ける。

これが、OS を知っているエンジニアと知らないエンジニアの差。同じログを見ても、見える情報量が全く違う。


章の問い (考えるための小さなトリガー)

読みながら / 読み終わって考えてほしいこと

  1. あなたのアプリは SIGTERM を受け取って何秒以内に綺麗に落ちられる? その間に DB 接続クローズ、ログフラッシュ、キャッシュ書き出し、全部できる?
  2. Docker コンテナと VM の違いを「カーネル」という言葉で1分で説明できるか?
  3. ローカル開発機で動くのに本番で動かない、という現象は、OS のどのレイヤーの差が原因として多そう?

まとめ - OS は「世界観」を作っている

この記事で扱ったこと

  • OS の役割: ハードウェアと人間の翻訳者、秘書、警察官
  • Unix 哲学: 小さなツール、テキスト通信、everything is a file
  • カーネルとユーザー空間、システムコール
  • プロセスとスレッド、fork/exec、ゾンビ、孤児
  • 仮想メモリ、ページング、mmap
  • スタックとヒープ、リーク、スタックオーバーフロー
  • ファイルシステム、inode、ハードリンク / シンボリックリンク
  • パーミッション、root、sudo
  • シグナル、SIGTERM vs SIGKILL の差
  • Linux カーネルの開発体制、ディストリビューション
  • macOS / Linux / Windows の系統
  • Docker と WSL の正体は「Linux カーネル機能の活用」

OS は「下層のインフラ」ではなく、「ソフトウェア全体の世界観」を作っている。あなたが書く Go や Python のコードは、結局のところ OS の上で動く。OS が決めたルール(プロセス、メモリ、ファイル、シグナル)の上で踊る。

ルールを知らずに踊ると、本番でこける。

読んだ後に考えてみる

  • 自分が触っているアプリで、プロセス数とメモリ消費を pstop で見るとどんな世界が広がっている?
  • 自分が書いたコードで、グローバルなキャッシュやスライスが「無限に伸びる」ような書き方をしていないか?
  • 本番サーバーが落ちた時、最初に何を cat して、何を tail -f する? どのコマンドで何が見えるか答えられる?

これに答えられたら、もうあなたは「OS の世界が見えている」エンジニア。


さらに深掘りするなら

  • 書籍: 『Linuxシステムプログラミング』(Robert Love) - C 視点の Linux 入門。本気で OS を理解するならこれ
  • 書籍: 『プログラマのためのSQL』ではなく『Operating System Concepts』(Silberschatz) - 大学の OS の教科書、英語版が無料で読める
  • ドキュメント: The Linux Kernel documentation - 公式
  • 動画: 「Linux カーネルとは何か」系の YouTube 動画は質が高いものが多い。“Linux kernel deep dive” で検索
  • OSS: htop のソースを読むと「ps がどうやって情報を取っているか」が分かる。Linux なら /proc を読みまくっている

次の読み物

essay_06_database_internals.md をまだ生成していなければ Claude Code に依頼:

バックエンドマスター/読み物/essay_06_database_internals.md を、
05_OSの基礎.md と同じフォーマットで作って。
内容はデータベースの中身(B-tree、トランザクション、WAL、MVCC、レプリケーション、
PostgreSQL vs MySQL、NoSQL の系譜)を、スマホで読める教養記事として。