モダンオペレーティングシステム 第5版 上

読んでる。

読み進めた記事というよりは範囲の中でわからんとこを生成AIと相談しながら文章課題とか出してもらいつつ進めたものをまとめた記事。

第1章 序論

OSの2つの役割

OSには2つの顔がある。

1. 拡張マシン(Extended Machine)

ハードウェアの複雑さを隠蔽する。アプリ開発者はディスクの物理構造を知らなくてもファイルを読み書きできる。Docker APIがLinuxの複雑さを隠すのと同じ発想。

2. リソースマネージャー(Resource Manager)

CPU・メモリ・ディスク・ネットワークを複数プロセスに割り当てる。k3s schedulerがNodeのリソースをPodに割り当てるのと同じ役割。

k3s scheduler  →  OSのリソースマネージャーと同じ役割
Docker API     →  OSの拡張マシンと同じ役割

OSがやってることをk3sやDockerが上位レイヤーで再現している。


ユーザー空間とカーネル空間

┌─────────────────────────────┐
│        ユーザー空間           │
│  Docker、アプリ、k3s、etc... │
│  → 直接ハードウェアは触れない │
└──────────┬──────────────────┘
           │ システムコール(唯一の通路)
┌──────────▼──────────────────┐
│        カーネル空間           │
│  スケジューラ                │
│  メモリ管理                  │
│  ファイルシステム             │
│  デバイスドライバ             │
│  → ハードウェアを直接触れる   │
└─────────────────────────────┘

アプリが直接ハードウェアを触れたら危険。カーネルが仲介することで安全性を保証する。


システムコール

docker runした時の実際の流れ:

docker run ubuntu
    ↓
Dockerデーモン
    ↓
clone()    ← プロセス生成
unshare()  ← namespaceを作る
mount()    ← ファイルシステムをマウント
    ↓
Linuxカーネル

Dockerはシステムコールの集合体。魔法じゃなくてLinuxのシステムコールをうまく組み合わせてるだけ。

ファイルリードの流れ:

Docker(ユーザー空間)
    ↓
fread()        ← libcの関数(ユーザー空間)
    ↓
read()         ← システムコール(ここでカーネル空間へ)
    ↓
VFS            ← どのFSか判断する仮想ファイルシステム
    ↓
デバイスドライバ ← ディスクからデータ取得
    ↓
データをユーザー空間に返す

VFS(仮想ファイルシステム):「このインターフェースを守れば何でもいい」のレイヤー。VFSに準拠していればext4でもBtrfsでもZFSでも作れる。


OSの構造:モノリシック vs マイクロカーネル

【モノリシックカーネル(Linux)】

┌─────────────────────────────┐
│        ユーザー空間           │
├─────────────────────────────┤
│        カーネル空間           │
│  ファイルシステム             │
│  メモリ管理                  │
│  プロセス管理                │
│  デバイスドライバ ← 全部一塊  │
└─────────────────────────────┘

【マイクロカーネル】

┌─────────────────────────────┐
│        ユーザー空間           │
│  ファイルサーバー             │
│  デバイスドライバ ← バラバラ  │
├─────────────────────────────┤
│  カーネル(最小限のコアのみ)  │
└─────────────────────────────┘
モノリシック マイクロカーネル
速度 ✅ 速い ❌ 遅い
安全性 ❌ バグが全体に影響 ✅ 障害が局所化
代表例 Linux、Windows QNX、Minix

LinuxはモノリシックだがProxmox CTのようにfork + namespace + cgroupの組み合わせでマイクロカーネル的な障害分離ができる。


ProxmoxのVM vs CT

┌─────────────────┬──────────────────┐
│  KVM(VM)       │  LXC(CT)        │
├─────────────────┼──────────────────┤
│ 独自カーネルを持つ│ ホストカーネル共有 │
│ 完全な仮想化     │ namespace/cgroup  │
│ オーバーヘッド大 │ オーバーヘッド小  │
│ Windowsも動く   │ Linuxのみ         │
└─────────────────┴──────────────────┘

使い分け:セキュリティ要件が高い・別OSが必要 → VM、軽量・環境分離だけでいい → CT


第2章 プロセスとスレッド

プロセスとは

docker psで見えるコンテナ = Linuxから見たらただのプロセス。

nginx(バイナリファイル) ← ただのデータ
    ↓ 実行
nginxプロセス           ← メモリ上に展開、CPUが実行中

プロセスが持つもの:

┌─────────────────────────┐
│  コード(命令列)         │ ← 読み取り専用
│  データ(グローバル変数) │
│  ヒープ(動的メモリ)    │ ← malloc()で確保
│  スタック(関数呼び出し) │ ← ローカル変数
│  PID                   │
│  状態                   │
└─────────────────────────┘

プロセスの3つの状態

┌──────────┐   スケジューラが選ぶ   ┌──────────┐
│  Ready   │ ─────────────────▶ │  Running │
│ 実行待ち  │ ◀───────────────── │  実行中   │
└──────────┘   タイムアップ        └──────────┘
                                       │
                                  I/O待ち等
                                       ▼
                                 ┌──────────┐
                                 │  Blocked │
                                 │  待機中   │
                                 └──────────┘

DockerコンテナがDBにクエリ投げて結果待ちの間 → Blocked。その間CPUは別のコンテナの処理へ。


ゾンビプロセスとtini

子プロセスが終了
    ↓
親プロセスが終了を受け取っていない
    ↓
カーネルがプロセステーブルから消せない
    ↓
ゾンビ状態(死んでるけど消えられない)

DockerはPID 1にtiniを使うことでゾンビプロセスを回収する。


スレッド

プロセス
├── コード         ← スレッド間で共有
├── ヒープ         ← スレッド間で共有(← レースコンディションの原因)
├── データ         ← スレッド間で共有
│
├── スレッドA
│   └── スタック   ← 独自(関数呼び出し履歴)
│   └── レジスタ   ← 独自
│
└── スレッドB
    └── スタック   ← 独自
    └── レジスタ   ← 独自

nginxとApacheの違い

【Apache:スレッド/プロセスモデル】
リクエスト1万 → スレッド1万 → メモリ死亡(C10K問題)

【nginx:イベント駆動モデル】
リクエスト1万 → ワーカー数個 → イベントループで捌く

nginxはスレッド間でヒープを共有するのでメモリ効率が良い。


レースコンディション

スレッドA: 残高読む → 1000円
スレッドB: 残高読む → 1000円
スレッドA: 1000 + 500 = 1500円に書き込む
スレッドB: 1000 + 500 = 1500円に書き込む
↓
本来2000円のはずが1500円になる → BON💥

ミューテックス:1個だけ通れる
セマフォ:N個まで同時に通れる(DBコネクションプール等)

DBの場合はトランザクション分離レベルで制御するのが正しい。

レベル 使いどころ
READ COMMITTED 一般的なWebアプリ
REPEATABLE READ 集計バッチ
SERIALIZABLE 金融、在庫管理

スケジューラ

誰がプロセスの順番を決めるか → スケジューラ

Linuxはかつて**CFS(Completely Fair Scheduler)**が赤黒木でプロセスを管理していた。赤黒木はO(log n)で「一番CPUを使っていないプロセス」を効率よく取り出せる。

Linux 6.6から**EEVDF(Earliest Eligible Virtual Deadline First)**に移行。CFSはレイテンシが不安定だったが、EEVDFは期限を考慮してより賢く順番を決める。


割り込み

キーボード1ストローク ≈ 50ms の間にCPUは約1億6000万命令を実行できる。CPUからすると人間は永遠に近い暇な時間を作ってくれる存在。

プロセスA: ファイル書き込み開始
    ↓
CPUは待たない → 別プロセスBを実行
    ↓
ディスクへの書き込み完了
    ↓
「終わったぞ」と割り込み信号
    ↓
プロセスAがReady状態に戻る

組み込みOSの世界(余談)

デバイス OS
Arduino なし(ベアメタル)
Raspberry Pi Linux
家電・車 FreeRTOS、VxWorks、QNX
医療機器・信号機 RTOS

RTOSは「この処理は1ms以内に必ず終わらせる」をリアルタイムで保証する。車のブレーキ制御が「ちょっと待って」では困るから。


まとめ

第1章
├── OSの2つの役割(拡張マシン・リソースマネージャー)
├── システムコール(ユーザー空間とカーネル空間の橋)
├── VFS(FSの抽象化レイヤー)
└── モノリシック vs マイクロカーネル

第2章
├── プロセス(実行中のプログラム)
├── プロセスの3状態(Running・Ready・Blocked)
├── スレッド(ヒープ共有、スタック独自)
├── レースコンディション → ミューテックス・セマフォ・トランザクション
├── スケジューラ(CFS→EEVDF、赤黒木)
└── 割り込み(I/O待ちの間に別プロセスを動かす)