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 | 系統 | バックエンドでの位置付け |
|---|---|---|
| Linux | Unix 系 | 本番サーバーの 9 割以上。AWS、GCP、Azure 全部 Linux ベース |
| macOS | Unix 系(Darwin) | 開発機で人気。サーバーには使われない |
| Windows | NT 系(独自) | 企業デスクトップが多い。サーバー版も存在するが少数派 |
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つ。
- 一つのことを上手くやれ (Do one thing well)
- プログラムは互いに連携できるように書け
- テキストストリームを共通のインターフェースにせよ
「小さくシンプルなツールをたくさん作って、それらをパイプで繋いで大きな仕事をする」という考え方。
2-2. 具体例: ログから「アクセス回数トップ10の IP」を取る
cat access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -10これだけで終わる。cat も awk も sort も uniq も head も、それぞれ独立した小さなコマンド。|(パイプ)で繋ぐと、ログ解析という複雑な仕事ができる。
パイプ
|の本質パイプは「あるコマンドの出力を、次のコマンドの入力に流す」仕組み。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 種類。
openreadwritecloseforkexecsocketmmapなどが代表。ユースケース
- パフォーマンス調査:
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 を打った時の流れ:
- bash が
fork()で子プロセスを作る - 子が
exec("/usr/bin/ls")で中身を ls に置き換える - ls が実行される
- 親(bash)は
wait()で子の終了を待つ - 子が終わったら 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. なぜ仮想メモリが必要か
- 隔離: A が 0x1234 にアクセスしても B のデータには触れない。物理アドレスが違うから
- 使いやすさ: アプリは「自分は 0 番地から始まる」と思って書ける
- 柔軟性: 物理メモリが足りなくなったらディスクに退避できる(スワップ)
6-3. ページング
仮想メモリの管理単位を ページ と呼ぶ(普通 4KB)。メモリを 4KB ずつに分割し、ページ単位で物理メモリにマッピング。変換は CPU の MMU が高速にやってくれる。
物理メモリが足りなくなると、OS は「あまり使われていないページ」をディスクに退避する(スワップアウト)。必要になったら読み戻す(スワップイン)。
スワップが地獄を呼ぶ
RAM はナノ秒、ディスク(SSD でも)はマイクロ〜ミリ秒。1000倍以上遅い。スワップ頻発でメモリアクセス毎にディスクが動くとアプリが致命的に遅くなる。これを スラッシング と呼ぶ。本番でスワップが激しく使われているのは「メモリ不足」の兆候。
free -hやvmstatで監視するのが定石。
6-4. メモリマップ (mmap)
ファイルをメモリ上にマッピングし、まるでメモリのように扱う仕組みが mmap。read() でちまちま読む代わりに、配列のように直接アクセスできる。
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.txtoriginal.txt と link.txt は inode 番号が同じ。中身も同じ。どちらを変更しても両方に反映される。original.txt を rm しても、link.txt から同じ inode が参照されているので、データは消えない。
シンボリックリンク: 「別のパスへの矢印」を作る。
ln -s original.txt link.txtlink.txt は独立した小さなファイルで、中身は「original.txt」というパス文字列。original.txt を消すと link.txt は「リンク切れ」になる。
リンクの実務的な使い所
シンボリックリンクのユースケース
/usr/local/bin/pythonをpython3.11に張る(バージョン切り替え)- 設定ファイルを
~/dotfiles/に置いて、~/.zshrcを~/dotfiles/zshrcのリンクにする- デプロイで「current」ディレクトリを最新リリースに付け替える(Capistrano パターン)
ハードリンクのユースケース
- バックアップツール(Time Machine など)が「変更がないファイルはハードリンクで参照」して容量節約
- 普段の開発ではほぼ使わない
8-4. ファイルシステムの種類
Unix 系の代表的なファイルシステム:
| 名前 | 主な使用先 | 特徴 |
|---|---|---|
| ext4 | Linux のデフォルト | 安定、無難、迷ったらこれ |
| xfs | 大規模 Linux サーバー | 巨大ファイル・高並列に強い |
| zfs | サーバー、NAS | スナップショット、データ整合性、圧縮、最強だが重い |
| btrfs | Linux 一部 | zfs の Linux 版を目指したやつ |
| APFS | macOS | スナップショット、暗号化、SSD 向け最適化 |
| NTFS | Windows | NT 系の標準 |
なぜファイルシステムが何種類もあるか
用途が違うから。
- 「とにかく安定」が欲しい → 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 = 7rw-= 4+2 = 6r-x= 4+1 = 5r--= 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. 代表的なシグナル
| シグナル | 番号 | 意味 |
|---|---|---|
| SIGTERM | 15 | 終了してほしい(お願いベース、無視可能) |
| SIGKILL | 9 | 強制終了(無視できない、即死) |
| SIGINT | 2 | Ctrl+C で送られる「中断」 |
| SIGHUP | 1 | 端末が切れた、設定再読み込み |
| SIGSEGV | 11 | セグメンテーション違反(不正なメモリアクセス) |
| SIGSTOP | 19 | 一時停止 |
| SIGCONT | 18 | 再開 |
| SIGCHLD | 17 | 子プロセスが終了した通知 |
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 トランザクションが宙ぶらりんになったりする。
正しい順序
- まず SIGTERM を送る
- 数秒待つ
- それでも生きていたら 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 の血を引く(だから
lsgrepなどが 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章)、Ssのsは session leader - COMMAND: 何のプログラムか
最初は呪文だったこの一行が、各列の意味が分かるようになる。これが「OS の窓の向こうが見える」状態。
本番サーバーで ps -aux を打って、STAT に Z が大量に並んでいたら「あ、ゾンビが溜まってる、親プロセスのコードがおかしい」と即座に判断できる。%MEM が一個のプロセスで 80% を超えていたら「メモリリーク疑惑、pmap か valgrind で調べよう」と動ける。
これが、OS を知っているエンジニアと知らないエンジニアの差。同じログを見ても、見える情報量が全く違う。
章の問い (考えるための小さなトリガー)
読みながら / 読み終わって考えてほしいこと
- あなたのアプリは SIGTERM を受け取って何秒以内に綺麗に落ちられる? その間に DB 接続クローズ、ログフラッシュ、キャッシュ書き出し、全部できる?
- Docker コンテナと VM の違いを「カーネル」という言葉で1分で説明できるか?
- ローカル開発機で動くのに本番で動かない、という現象は、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 が決めたルール(プロセス、メモリ、ファイル、シグナル)の上で踊る。
ルールを知らずに踊ると、本番でこける。
読んだ後に考えてみる
- 自分が触っているアプリで、プロセス数とメモリ消費を
psやtopで見るとどんな世界が広がっている?- 自分が書いたコードで、グローバルなキャッシュやスライスが「無限に伸びる」ような書き方をしていないか?
- 本番サーバーが落ちた時、最初に何を
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 の系譜)を、スマホで読める教養記事として。