はじめに
CPUスケジューリング(効率よくCPUを使う方法)について、実際に手を動かして理解に努めます。
対象読者
- 若手エンジニア(2年目前後)
- 「CPUスケジューリング」を座学で学んだけどピンとこない方
- 運用・インフラの実務に活かしたい方
この記事でできること
- 30分でFCFS・ラウンドロビンを実装して動かす
- 「重い処理が全体を止める」を数字で確認
- 両者の違いを比較して理解する
準備:Python環境を用意する(3分)
Macの場合
# Pythonがインストールされているか確認
python3 --version
# なければHomebrewでインストール
brew install python3
実行方法
# 1. ファイルを作成
# テキストエディタで fcfs.py という名前で保存
# 2. 実行
python3 fcfs.py # Mac/Linux
python fcfs.py # Windows
Step 1:FCFS(先着順)を実装する
FCFSとは
First-Come, First-Served!到着順にタスクやプロセスを処理する方式です。
① ファイルを作成
テキストエディタで fcfs.py という名前で以下を保存:
# プロセス定義(名前, 実行時間)
processes = [
("A", 8),
("B", 4),
("C", 2),
]
current_time = 0
total_waiting_time = 0
print("FCFS スケジューリング結果")
for name, burst in processes:
start = current_time
end = current_time + burst
waiting_time = start
total_waiting_time += waiting_time
print(f"{name}: 実行開始={start}, 実行終了={end}, 待ち時間={waiting_time}")
current_time = end
print(f"\n合計待ち時間: {total_waiting_time}")
print(f"平均待ち時間: {total_waiting_time / len(processes):.2f}")
② 実行する
python3 fcfs.py
③ 実行結果を確認
FCFS スケジューリング結果
A: 実行開始=0, 実行終了=8, 待ち時間=0
B: 実行開始=8, 実行終了=12, 待ち時間=8
C: 実行開始=12, 実行終了=14, 待ち時間=12
合計待ち時間: 20
平均待ち時間: 6.67
何が起きているか
プロセスCは、たった2秒で終わる軽い処理ですが、AとBが終わるまで12秒も待たされています。
運用現場での例
- A = 夜間バッチ(重い)
- B = データ集計(中程度)
- C = ヘルスチェック(軽い)
→ 夜間バッチに時間が取られて後続の処理が遅延、ヘルスチェックが遅延してアラート発火する。
Step 2:ラウンドロビンを実装する
ラウンドロビンとは
各プロセスに一定時間(タイムスライス)ずつCPUを割り当て、未完了の場合はキューの末尾に回す方式です。
① ファイルを作成
テキストエディタで round_robin.py という名前で以下を保存:
from collections import deque
# プロセス定義(名前, 残り実行時間)
processes = [
["A", 8],
["B", 4],
["C", 2],
]
queue = deque(processes[:])
quantum = 2 # タイムスライス
current_time = 0
arrival_time = {p[0]: 0 for p in processes}
completion_time = {}
print("ラウンドロビン(量子=2)")
while queue:
name, remaining = queue.popleft()
run_time = min(quantum, remaining)
start = current_time
current_time += run_time
remaining -= run_time
print(f"{name}: {start} -> {current_time}(残り {remaining})")
if remaining > 0:
queue.append([name, remaining])
else:
completion_time[name] = current_time
# 待ち時間の計算
total_waiting_time = 0
print("\n各プロセスの待ち時間:")
for name, burst_time in processes:
waiting_time = completion_time[name] - arrival_time[name] - burst_time
total_waiting_time += waiting_time
print(f"{name}: {waiting_time}")
print(f"\n合計待ち時間: {total_waiting_time}")
print(f"平均待ち時間: {total_waiting_time / len(processes):.2f}")
② 実行する
python3 round_robin.py
③ 実行結果を確認
ラウンドロビン(量子=2)
A: 0 -> 2(残り 6)
B: 2 -> 4(残り 2)
C: 4 -> 6(残り 0)
A: 6 -> 8(残り 4)
B: 8 -> 10(残り 0)
A: 10 -> 12(残り 2)
A: 12 -> 14(残り 0)
各プロセスの待ち時間:
A: 6
B: 6
C: 4
合計待ち時間: 16
平均待ち時間: 5.33
何が変わったか
- Cは6秒で完了(FCFSでは14秒)
- Bも10秒で完了(FCFSでは12秒)
- Aは14秒で完了(FCFSと同じ)
- 合計待ち時間が20秒→16秒に改善
軽い処理が順番に早く終わります。それによって、重い処理による全体の影響を減らせる!
Step 3:両者を比較する
| 方式 | 合計待ち時間 | 平均待ち時間 | 特徴 |
|---|---|---|---|
| FCFS | 20秒 | 6.67秒 | シンプルだが、重い処理が先に来ると全体が遅延 |
| ラウンドロビン | 16秒 | 5.33秒 | 公平に処理、軽い処理が早く終わる |
改善率:20%の待ち時間削減
Step 4:値を変えてみる
パターン①:量子を小さくする
round_robin.py の以下の行を変更:
quantum = 1 # 2 から 1 に変更
結果
- 切り替えが頻繁になる
- 公平だが、オーバーヘッドが増える
運用での意味
- コンテキストスイッチが多すぎる
- CPU使用率は高いのに処理が進まない
パターン②:重いプロセスを追加
round_robin.py の以下の行を変更:
processes = [
["A", 30], # 8 から 30 に変更(激重バッチ)
["B", 4],
["C", 2],
]
結果
- FCFS:全プロセスが詰まる
- RR:最低限は動く
運用での意味
- 「一部の重いバッチが全体を巻き添えにしてる」
- 「夜間バッチが終わらずに朝を迎える」
運用現場とのつながり
| 実験結果 | 現場での意味 |
|---|---|
| FCFSで詰まる | 夜間バッチ・一括処理の影響 |
| RoundRobinで公平 | Web/APIサーバーの設計 |
| 量子が小さい(RoundRobinで) | オーバーヘッド増加 |
| 重いAがいる | ボトルネックプロセスの特定 |
補足:実際のOSではもっと複雑
今回のシミュレーションはシンプルでしたが、実際のOSにはさらに考慮点があります:
量子(タイムスライス)の決め方
- 短すぎる → 切り替えが多すぎて効率悪化
- 長すぎる → FCFSに近づいて公平性が失われる
- 実際のLinuxは数ミリ秒単位で調整
プロセス切り替えのコスト
- 今回は切り替えコスト = 0 として計算
- 実際はメモリ状態の保存・復元に時間がかかる
- だから切り替え回数も重要な指標
実際のLinux(CFS)の工夫
- 固定の量子ではなく、プロセス数に応じて動的に調整
- 「どれだけCPUを使ったか」を記録して公平に配分
おわりに
この記事では、CPUスケジューリングを実際に動かして体験しました。
- CPUは「速い/遅い」ではなく配り方が重要