C
OS/基礎/Lesson 02

OS基礎 + プロセス・スレッド

45分·theory

OS基礎 + プロセス・スレッド

🎯 このlessonを読んだ後に

このlessonを読み終えると、以下の3つを自信を持って説明できるようになります。

  • ✅ OSの4大役割 (プロセス・メモリ・ファイルシステム・I/O)
  • ✅ User Mode vs Kernel Mode + syscall
  • ✅ Node.jsがなぜシングルスレッドなのに速いのか

学習目標をチェックリストとして持ち、すべてに答えられるようになったらlessonを閉じてください。

OSがする4つのこと

一言で: オペレーティングシステム (OS) = ハードウェアの上でプロセス・メモリ・ファイル・I/Oを抽象化するソフトウェア層。

OSの4大責務:

領域意味
プロセス管理実行中プログラムの生成・切り替え・終了。fork()·exec()·exit()
メモリ管理仮想メモリ・ページング・割り当て。各プロセスが独立したメモリの幻想を持つ
ファイルシステムディスクの抽象化・ディレクトリ・権限。inodeベース
I/O管理キーボード・ネットワーク・ディスクとの通信。デバイスドライバ

ブート → カカオトーク起動の流れ (6段階):
1. 電源ON — BIOS/UEFI → ブートローダー → カーネルロード
2. カーネル初期化 — メモリマッピング・ドライバロード・スケジューラ起動
3. initプロセス — PID 1、すべてのユーザープロセスの親 (systemd / launchd)
4. ログインシェル — bash/zsh 起動、環境変数ロード
5. アプリ起動fork() + exec("/Applications/KakaoTalk.app/...") → 新しいプロセス
6. イベントループ — キーボード・ネットワーク入力の待機・処理

> 💡 カカオトークを1回起動 = OSが上記6段階を見えないところで実行。毎回0.5秒以内に完了。

プロセス vs スレッド — 並行性の2つのモデル

項目プロセススレッド
メモリ完全に独立同じプロセス内で共有
生成コスト重い (数ms)軽い (数μs)
通信IPC (パイプ・ソケット・共有メモリ)メモリ共有 + 同期
分離性強い (1つのプロセスが死んでも他に影響なし)弱い (1スレッドがクラッシュ → 全体が死ぬ)
コンテキストスイッチコスト高い (メモリマップ・キャッシュ無効化)低い

いつ何を使うか:

  • プロセス分離: 安定性優先 (Chromeのタブごとのプロセス)、セキュリティ分離 (Dockerコンテナ)、他言語との統合
  • スレッド使用: 軽量な並行性 (Webサーバーのリクエストごとのスレッド)、共有データ処理 (GUIイベントループ)
  • 現代のトレンド: 仮想スレッド (Java 21)・ゴルーチン (Go)・async/await (Python、Node) — スレッドよりさらに軽い軽量並行性

よくある間違い:

  • ❌ スレッドを1000個生成 → コンテキストスイッチコストが爆発
  • ❌ ロックなしで共有データを変更 → race condition
  • ✅ スレッドプール (ThreadPoolExecutor) + ロック または async/await を使用

コンテキストスイッチング — 並行性の*本当のコスト*

コンテキストスイッチングの6段階 (CPUがプロセスA → Bに切り替える):
1. 割り込み — タイマー・I/O完了・システムコール
2. Aの状態保存 — レジスタ・PC・スタックポインタをPCB (Process Control Block) に
3. スケジューラ呼び出し — 次に実行するプロセスを選択 (CFS・O(1)・リアルタイム)
4. BのPCBロード — レジスタ・MMUマッピングを復元
5. キャッシュ無効化 — L1/L2キャッシュ・TLB (Translation Lookaside Buffer) の一部をフラッシュ
6. B実行 — 停止した箇所から再開

コスト:

リソースコスト
時間~1〜10 μs (単純なスイッチ)
キャッシュミスTLBフラッシュ時に+数μs
メモリ帯域幅PCB I/O

オーバーヘッドの罠:

  • ❌ スレッドが多すぎる (1000+) → CPUが実際の作業よりスイッチングに多くの時間を費やす
  • ❌ 非同期コードなのにCPUバウンドな処理 → メインスレッドをブロック
  • ✅ CPUコア数に近いスレッド数 (おおよそN + 1) を推奨

> 💡 なぜ非同期が速いのか? = コンテキストスイッチングなしで1スレッドが複数タスクを処理 (イベントループ)。

💻 📌 シナリオで学ぶプロセス・スレッドコマンド
# ============================================================
# シナリオ 1: "あれ?サーバーが遅くなった。何がCPUを食っているんだ?"
# ============================================================
# 1) リアルタイムモニターで *CPUを多く使う* プロセスを探す
htop                          # カラーインタラクティブ (topの発展形、推奨)
top -o %CPU                   # htopがない場合

# 2) 疑わしいプロセスのPIDを探す
pgrep -fl node                # 'node' を含むコマンド + PID を出力
# 結果: 28391 /usr/bin/node /app/server.js

# 3) 詳細を調査 — メモリ・状態・開始時刻
ps -p 28391 -o pid,ppid,user,stat,%cpu,%mem,start,cmd
# STAT カラム:
#   R = 実行中   S = 待機中   D = I/O待機 (強制終了不可)
#   Z = ゾンビ      T = 停止中    < = 高い優先度

# 4) 1つのプロセスが *いくつのスレッド* を使っているか
ps -T -p 28391                # スレッドリスト
# または: cat /proc/28391/status | grep Threads

# ============================================================
# シナリオ 2: "プロセスが停止した。どうやって終了させる?"
# ============================================================
# *常に* 正常終了を試みる — データ保存・クリーンアップの機会を与える
kill 28391                    # = kill -TERM (SIGTERM, 15)
                              # アプリがグレースフルシャットダウン可能 (DB接続のクリーンアップ・進行中のリクエスト完了)

# 5秒待っても終了しない場合は強制終了
kill -9 28391                 # SIGKILL — OSが *即座に* メモリから削除
                              # ⚠️ データ破損のリスクあり。最終手段

# 設定を再読み込み (再起動ではない)
kill -HUP 28391               # SIGHUP — nginx・systemd サービスの再読み込み標準

# 名前で一括
killall -TERM nginx           # すべてのnginxプロセスを正常終了
pkill -f 'node.*server.js'    # パターンマッチング

# ============================================================
# シナリオ 3: "バックグラウンドの重いタスクが *他のタスク* を妨害している"
# ============================================================
# 優先度調整 — nice 値 -20(高) ~ +19(低)。デフォルト 0。
nice -n 10 python heavy_batch.py    # 低い優先度で新規実行
                                     # 他のタスクに譲り、自分はゆっくり実行

renice -n 5 -p 28391                # *実行中の* プロセスの優先度を変更
                                     # 正の数 = 譲る。↑ 値 = ↓ 優先度 (直感と逆!)

# ============================================================
# シナリオ 4: "プロセスが終了したが、ps に Z(ゾンビ)として残っている"
# ============================================================
# ゾンビ = 子プロセス終了 → 親プロセスが wait() を呼ばない → PCB のみ残る
ps aux | awk '$8=="Z"'        # ゾンビのみ抽出
# または: ps -eo stat,pid,ppid,cmd | grep -w Z

# ゾンビは *親プロセスを終了させる* と init(PID 1) が引き取り → クリーンアップ
# 親プロセスのPIDを確認後
kill -CHLD <親PID>          # 親プロセスに子プロセスをクリーンアップするようシグナルを送る
# 解決しない場合は親プロセスの再起動を推奨

# ============================================================
# シナリオ 5: "プロセスがどのファイルを開いているか見たい"
# ============================================================
lsof -p 28391                 # そのPIDが開いているすべてのファイル・ソケット・ライブラリ
lsof -i :3000                 # *3000ポート* を占有しているプロセス (逆方向)
lsof -i TCP:443 -sTCP:LISTEN  # 443でLISTEN中のもののみ

# fd制限の確認 (よく遭遇する 'too many open files')
cat /proc/28391/limits | grep 'open files'
ulimit -n                     # 現在のシェル制限
ulimit -n 65536               # 増やす (永続化は /etc/security/limits.conf)

# ============================================================
# シナリオ 6: "バックグラウンドで実行し、シグナルを無視する"
# ============================================================
nohup python server.py &      # ログアウトしても終了しない、nohup.out に出力
disown -h %1                  # 既に実行中のジョブに nohup 効果
# または systemd サービスとして登録 (長期運用標準)

システムコール・割り込み・コンテキストスイッチング — 1ページで

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

OSのメモリは2つの領域に分かれています:

  • ユーザー空間 (User Space) — 私たちのアプリが実行される場所。権限が制限されている。メモリ・ディスク・ネットワークへの直接アクセス不可
  • カーネル空間 (Kernel Space) — OS自体が存在する場所。すべてのハードウェア権限を持つ。
code
[User Space]    [内 App]
     ↓ syscall
[Kernel Space]  [OS カーネル]
     ↓
[Hardware]      [ディスク, NIC, ...]

ユーザーアプリがファイルを開くには — ディスクに直接アクセスできません。必ずOSにリクエストしなければなりません。このリクエストがシステムコールです。

システムコール (System Call)

c
// C でファイルを開く
int fd = open("data.txt", O_RDONLY);    // syscall
read(fd, buf, 100);                       // syscall
close(fd);                                 // syscall

open / read / write / close / fork / exec — Linuxでは約300個。すべてのI/O・プロセス生成がsyscall。

高水準言語 (Python, Java) のすべてのファイル/ネットワーク呼び出しは内部的にsyscallを呼び出します。

syscallのコスト

ユーザー空間 → カーネル空間の切り替え自体が高コストです (マイクロ秒単位)。そのため:

  • バッファリング — 毎回syscallせず、まとめて一度に処理。BufferedReaderconsole.logの内部バッファ。
  • 非同期I/O — Node.js、async/await — syscall待機中に他の処理を実行。

割り込み vs ポーリング

ポーリング (Polling) — ひたすら確認する

python
while not done():
    pass    # 終わったか? 終わったか? 終わったか? — CPU 100%

CPU時間の無駄遣い。ビジーウェイト

割り込み (Interrupt) — 通知を受け取る

ハードウェアが「準備完了!」というシグナルを送ると、CPUが即座に処理します:

  • キーボード入力 — キーを押した瞬間にIRQが発生 → カーネルがイベントに変換 → アプリに配送
  • ネットワークパケット到着 — NICが割り込みを発生 → カーネルがバッファに保存 → アプリを起こす
  • タイマー満了setTimeoutの基盤

「ポーリングは非効率、割り込みが標準」 — 現代OSのほぼすべてのI/Oは割り込みベース。

コンテキストスイッチング (Context Switching)

CPUがプロセス/スレッドを切り替えるとき — 現在の状態 (レジスタ・PC・メモリマッピング) を保存し、次のタスクの状態を復元します。

コスト

  • プロセススイッチング — メモリマッピングも切り替える必要がある。高コスト (~数μs)。
  • スレッドスイッチング — 同じメモリを共有。プロセスより5〜10倍軽い
  • 関数呼び出し — 単純なスタックプッシュ。ナノ秒単位

そのため — マルチスレッドはマルチプロセスより高速です。

なぜNode.jsはシングルスレッドなのに速いのか

Node.jsの「シングルスレッド + イベントループ」:

code
[メインスレッド — JS 実行]
     ↑
   イベントキュー
     ↑
[libuv スレッドプール] ←— 非同期I/O時に使用
[カーネル — async syscall (epoll, kqueue)]

核心アイデア:

1. I/Oが99%の時間を占める (DB・API・ディスク)
2. I/O待機中にメインスレッドが他のリクエストを処理
3. コンテキストスイッチングコストがほぼゼロ — スレッドが1つだけ

Apacheのような「リクエストごとのスレッド」モデル: 1万リクエスト = 1万スレッド = コンテキストスイッチング爆発 + メモリ爆発。

Nodeモデル: 1万リクエスト = 1メインスレッド + 4〜8ワーカースレッド + イベントキュー。比較にならない効率

まとめ — バイブコーディングとの接続

  • fetch / ファイル読み込み = syscall → 高コスト → 最小化 (バッチ処理・キャッシング)
  • Node.js・async/await = イベント駆動I/Oの多いサーバーに強い
  • CPU負荷の高い処理はWorker Thread → メインイベントループをブロックしないこと

面接で「Node.jsはなぜ速いのか」と聞かれたら、このページの内容をそのまま答えれば合格です。

🤖 AIにこう依頼してみよう

このlessonの概念を知っていると、AIに具体的に指示できます。漠然とした「直して」ではなく、語彙を持ったリクエスト — それがトークン節約の出発点です。

  • 「この処理のsyscallトレースをstrace (Linux) で実行するコマンドを教えて」
  • 「このコードがユーザー空間 / カーネル空間のどちらでコストが高いか診断して」

なぜこれがトークンを減らすのか

概念を知らないと、AIの回答を受け取っても「それは何ですか?」と再度質問しなければなりません。その「再質問」がトークンを消費します。概念を一度習得すれば、会話が一度で完結します。

OS基礎 + プロセス・スレッド - OS