Skilore

CPUスケジューリング

スケジューリングとは「どのプロセスに次にCPUを割り当てるか」を決定するアルゴリズムである。

81 分で読めます40,159 文字

CPUスケジューリング

スケジューリングとは「どのプロセスに次にCPUを割り当てるか」を決定するアルゴリズムである。

この章で学ぶこと

  • 主要なスケジューリングアルゴリズムを比較できる
  • プリエンプティブとノンプリエンプティブの違いを説明できる
  • Linuxのスケジューラの仕組みを知る
  • マルチコア環境でのスケジューリング戦略を理解する
  • リアルタイムスケジューリングの要件と実装を説明できる
  • スケジューリングのパフォーマンス評価指標を計算できる

前提知識

このガイドを読む前に、以下の知識があると理解が深まります:

  • 基本的なプログラミングの知識
  • 関連する基礎概念の理解
  • スレッドと並行性 の内容を理解していること

1. スケジューリングの基本

1.1 なぜスケジューリングが必要か

マルチプログラミングの本質:

  CPUは1コアあたり同時に1つのプロセスしか実行できない
  → 複数プロセスが同時にReady状態になった場合、
    誰を先に実行するかを「公平」かつ「効率的」に決定する必要がある

  現代のシステムでは数百〜数千のプロセス/スレッドが同時存在
  → スケジューリングの質がシステム全体の応答性・スループットを決定する

CPUバースト と I/Oバースト:

  プロセスの実行パターン:
  [CPU実行] → [I/O待ち] → [CPU実行] → [I/O待ち] → ...

  CPU-boundプロセス: CPUバーストが長い(科学計算、動画エンコード)
  I/O-boundプロセス: CPUバーストが短い(Webサーバー、エディタ)
CPU-bound:
████████████░░████████████░░██████████████░░████
I/O-bound:
██░░░░░░██░░░░░░██░░░░░░██░░░░░░██░░░░░░██░░░░
█ = CPU実行 ░ = I/O待ち
スケジューラはI/O-boundプロセスを優先すべき:
  → I/O-boundはCPUをすぐ手放してI/O待ちに入る
  → 待っている間に他のプロセスがCPUを使える
  → 全体のスループットが向上する

1.2 スケジューラの目標

スケジューラの5つの評価指標:

  1. CPU利用率(CPU Utilization):
     CPUをできるだけ忙しく保つ
     目標: 40%〜90%(システム種別による)
     100%はオーバーロード、0%はアイドル

  2. スループット(Throughput):
     単位時間あたりの完了プロセス数を最大化
     バッチ処理システムで特に重要

  3. ターンアラウンド時間(Turnaround Time):
     投入から完了までの総時間
     ターンアラウンド = 待ち時間 + 実行時間 + I/O時間
     バッチ処理で重要

  4. 待ち時間(Waiting Time):
     Ready状態で待つ合計時間
     スケジューリングアルゴリズムが直接影響する唯一の指標

  5. 応答時間(Response Time):
     リクエスト送信から最初の応答までの時間
     対話的システム(GUI、Webサーバー)で最重要

  トレードオフの関係:
CPU利用率↑ ⟷ 応答時間↑
スループット↑ ⟷ 個別の応答時間↑
公平性↑ ⟷ スループット↓
→ 万能なアルゴリズムは存在しない
→ システムの用途に応じて最適解が異なる
システム種別ごとの優先指標:
バッチシステムスループット、CPU利用率
対話的システム応答時間、公平性
リアルタイムデッドライン遵守
サーバースループット、応答時間
デスクトップ応答時間、公平性

1.3 プリエンプティブとノンプリエンプティブ

プリエンプティブ vs ノンプリエンプティブ:

  ノンプリエンプティブ(非横取り):
    プロセスが自発的にCPUを手放すまで待つ
    手放すタイミング:
    - プロセス終了
    - I/O要求(ブロック状態に遷移)
    - yield()(自発的譲渡)

    利点: コンテキストスイッチが少ない、実装が単純
    欠点: 1プロセスがCPUを独占可能、応答性が悪い
    例: Windows 3.1、初期のMac OS

  プリエンプティブ(横取り):
    OSがタイマー割り込みで強制的にCPUを切り替える
    切り替えタイミング:
    - タイムクォンタム(タイムスライス)の消費
    - より優先度の高いプロセスがReady状態に
    - 割り込み発生

    利点: 1プロセスの独占を防止、応答性が良い
    欠点: コンテキストスイッチのオーバーヘッド、同期の複雑化
    例: 現代の全てのOS(Linux, Windows, macOS)

  コンテキストスイッチのコスト:
1. CPU状態の保存(レジスタ、PC等)
2. PCB(プロセス制御ブロック)の更新
3. メモリマップの切り替え
4. TLBのフラッシュ
5. キャッシュのコールドスタート
典型的なコスト: 1〜10マイクロ秒
キャッシュミス含む実効コスト: 数十マイクロ秒

1.4 スケジューリングの発生タイミング

ディスパッチャが動作する4つのタイミング:

  1. Running → Waiting:
     プロセスがI/O要求やwait()を呼んだ時
     → ノンプリエンプティブ(自発的譲渡)

  2. Running → Ready:
     タイマー割り込みが発生した時
     → プリエンプティブ(強制切替)

  3. Waiting → Ready:
     I/O完了割り込みが発生した時
     → プリエンプティブ(優先度次第で切替)

  4. プロセス終了:
     exit()が呼ばれた時
     → ノンプリエンプティブ(必然)

  ディスパッチャの処理:
  1. 現在のプロセスのコンテキストを保存
  2. スケジューラが次のプロセスを選択
  3. 選択されたプロセスのコンテキストを復元
  4. ユーザーモードに切り替え
  5. 選択されたプロセスの適切な位置にジャンプ

  ディスパッチレイテンシ:
  ディスパッチャが1つのプロセスを停止して
  別のプロセスを開始するまでの時間
  → できるだけ短くすべき(数マイクロ秒以下が理想)

2. 主要なスケジューリングアルゴリズム

2.1 FCFS(First-Come, First-Served)

FCFS: 到着順に実行。FIFOキュー。最もシンプル。

  到着: P1(24ms) P2(3ms) P3(3ms)
P1 (24ms)P2P3
0                        24  27  30

  待ち時間: P1=0, P2=24, P3=27
  平均待ち時間: (0 + 24 + 27) / 3 = 17ms

  ターンアラウンド時間: P1=24, P2=27, P3=30
  平均ターンアラウンド: (24 + 27 + 30) / 3 = 27ms

  もし到着順がP2, P3, P1だったら:
P2P3P1 (24ms)
0   3   6                        30

  待ち時間: P2=0, P3=3, P1=6
  平均待ち時間: (0 + 3 + 6) / 3 = 3ms
  → 到着順によって性能が大きく変動する!

問題点 — Convoy Effect(護送船団効果):
  長いプロセスの後ろに短いプロセスが並ぶと、
  短いプロセスが不必要に長く待たされる

  CPU-boundプロセス(長い)がCPUを占有
  → I/O-boundプロセス(短い)が全てReady queue で待機
  → I/Oデバイスがアイドル状態
  → CPU-boundプロセスがI/Oを開始
  → I/O-boundプロセスがCPUバーストを高速に処理してI/O待ちに
  → CPUがアイドル
  → CPU-boundプロセスがI/O完了して戻ってくる
  → 繰り返し…

  結果: CPU利用率、I/Oデバイス利用率ともに低下

特性:
  - ノンプリエンプティブ
  - 飢餓なし(全プロセスがいつか実行される)
  - 実装が最も単純
  - 平均待ち時間が一般に長い
  - バッチシステム以外には不向き

2.2 SJF(Shortest Job First)

SJF: 実行時間が最も短いプロセスを先に実行

  到着: P1(6ms) P2(8ms) P3(7ms) P4(3ms)(全て到着時刻0)
P4P1P3P2
0   3      9      16       24

  待ち時間: P4=0, P1=3, P3=9, P2=16
  平均待ち時間: (0 + 3 + 9 + 16) / 4 = 7ms

  ※FCFSなら: (0 + 6 + 14 + 21) / 4 = 10.25ms

SJFの最適性の証明(直感的理解):
  n個のジョブ t1 ≤ t2 ≤ ... ≤ tn がある場合
  SJF順で実行した時の平均待ち時間:
  = (0 + t1 + (t1+t2) + ... + (t1+t2+...+t(n-1))) / n
  = Σ(n-i)ti / n  (i=1..n)

  短いジョブの係数(n-i)が大きくなる → 合計が最小化される
  → SJFは平均待ち時間を最小化する最適アルゴリズム

問題点:
  1. 次の実行時間(CPUバースト長)の予測が困難
     → 過去のバースト長から指数平均で推定
     τ(n+1) = α * t(n) + (1-α) * τ(n)
     α = 0なら過去の推定値のみ使用
     α = 1なら直前の実測値のみ使用
     典型的にはα = 0.5

  2. 飢餓(Starvation):
     短いプロセスが次々到着すると
     長いプロセスが永遠に実行されない

SRTF(Shortest Remaining Time First):
  SJFのプリエンプティブ版
  → 新しいプロセスが到着した時、
    残り時間が現在実行中より短ければ切替

  到着: P1(0, 8ms) P2(1, 4ms) P3(2, 9ms) P4(3, 5ms)
P1P2P4P1P3
0 1    5    10       17      26

  t=0: P1開始(残り8)
  t=1: P2到着(残り4 < P1の残り7)→ P2に切替
  t=2: P3到着(残り9 > P2の残り3)→ P2継続
  t=3: P4到着(残り5 > P2の残り2)→ P2継続
  t=5: P2完了、P4(残り5) vs P1(残り7) vs P3(残り9) → P4
  t=10: P4完了 → P1(残り7)
  t=17: P1完了 → P3(残り9)
  t=26: P3完了

  平均待ち時間: (9 + 0 + 15 + 2) / 4 = 6.5ms

2.3 ラウンドロビン(Round Robin)

ラウンドロビン: タイムクォンタム(タイムスライス)で交互に実行

  プロセス: P1(24ms) P2(3ms) P3(3ms)、クォンタム=4ms
P1P2P3P1P1P1P1P1
0    4   7  10   14   18   22   26   30

  P1: 0-4(残20), 10-14(残16), 14-18(残12), 18-22(残8), 22-26(残4), 26-30(完了)
  P2: 4-7(完了、3ms < 4ms クォンタム)
  P3: 7-10(完了)

  待ち時間: P1 = (10-4) = 6ms(最初の待機のみでも6ms分)
           ※ 正確には P1の総待ち時間 = 30-24 = 6ms
           P2 = 4ms, P3 = 7ms
  平均待ち時間: (6 + 4 + 7) / 3 = 5.67ms

  応答時間: P1=0, P2=4, P3=7
  平均応答時間: (0 + 4 + 7) / 3 = 3.67ms
  → FCFS(応答: 0, 24, 27 = 17ms) より大幅に改善

タイムクォンタムの影響:

  クォンタムが短すぎる場合(例: 1ms):
  → コンテキストスイッチが頻発
  → オーバーヘッドでスループットが大幅に低下
  → 実質的な実行時間が減少

  クォンタムが長すぎる場合(例: 100ms):
  → FCFSに退化
  → 応答時間が悪化

  最適なクォンタム:
経験則:
- CPUバーストの80%がクォンタム以内に
完了するように設定
- 典型的には10〜100ms
- コンテキストスイッチ時間の100倍以上
コンテキストスイッチ = 10μsの場合
→ クォンタム最低1ms以上(100倍 = 1ms)
→ 現実的には10ms程度
クォンタムと性能の関係:
クォンタム応答時間スルーコンテキ
プットストスイッチ
1ms最良最悪非常に多い
10ms適度
100ms少ない
FCFSFCFS最少
特性:
  - プリエンプティブ
  - 公平(全プロセスが均等にCPU時間を得る)
  - 飢餓なし
  - SJFより平均待ち時間は悪いが応答時間は良い
  - 対話的システムに最適

2.4 優先度スケジューリング

優先度スケジューリング: 優先度の高いプロセスを先に実行

  プロセス  バースト  優先度(小=高)
  P1        10ms     3
  P2         1ms     1
  P3         2ms     4
  P4         1ms     5
  P5         5ms     2

  実行順:
P2P5P1P3P4
0  1     6         16 18 19

  待ち時間: P1=6, P2=0, P3=16, P4=18, P5=1
  平均待ち時間: (6 + 0 + 16 + 18 + 1) / 5 = 8.2ms

優先度の決定方法:

  静的優先度:
  - プロセス作成時に決定、変更しない
  - 例: nice値、ユーザーの権限レベル

  動的優先度:
  - 実行状況に応じて変動
  - 例: I/O待ちからの復帰で優先度上昇
  - 例: CPU時間消費で優先度降下

  内部要因による優先度:
  - メモリ要求量
  - ファイルオープン数
  - CPU/I/Oバースト比

  外部要因による優先度:
  - プロセスの重要度
  - 支払い額(クラウド)
  - 政策的要因

飢餓(Starvation)問題:
  低優先度のプロセスが永遠に実行されない

  解決策 — エージング(Aging):
  待ち時間に応じて優先度を徐々に上昇させる

  例: 15秒ごとに優先度を1段上昇
時刻初期優先度実効優先度
0秒2020
15秒2019
30秒2018
.........
→ いつかは必ず実行される(飢餓の防止)

優先度の逆転(Priority Inversion):
  高優先度プロセスが低優先度プロセスの
  ロック解放を待つ現象

  H(高) → ロック待ち
  M(中) → 実行中(Lをプリエンプト)
  L(低) → ロック保持中だがプリエンプトされている

  → Hは事実上Mより低い優先度になる!

  解決策:
  1. 優先度継承(Priority Inheritance):
     ロック保持者が待機者の優先度を継承
     L → 一時的にHの優先度 → ロック解放 → 元の優先度

  2. 優先度上限(Priority Ceiling):
     ロックに最高優先度を設定
     ロック取得時に即座にその優先度に昇格

  実例: Mars Pathfinder(1997年)
  → 優先度の逆転でシステムリセットが頻発
  → VxWorksの優先度継承を有効化して解決

2.5 マルチレベルキュー

マルチレベルキュー:
  複数の優先度キューを持ち、キュー間にも優先度
[Q1] システムプロセス
[Q2] 対話的プロセス
各キュー内は独自のスケジューリングアルゴリズム:
  Q0: FCFS(リアルタイム)
  Q1: 優先度スケジューリング
  Q2: ラウンドロビン(クォンタム短い)
  Q3: FCFS

  キュー間のスケジューリング:
  - 固定優先度: 上位キューが空になるまで下位を実行しない
    → 下位キューの飢餓の危険
  - タイムスライス配分: 各キューにCPU時間を割合で配分
    Q0: 10%, Q1: 20%, Q2: 50%, Q3: 20%

  問題: プロセスが固定のキューに留まる
  → 性質が変わっても適切なキューに移動しない

2.6 マルチレベルフィードバックキュー(MLFQ)

マルチレベルフィードバックキュー:
  複数の優先度キューを持ち、動的にプロセスを移動

  ルール:
  1. 高優先度キューのプロセスを先に実行
  2. 同一キュー内はラウンドロビン
  3. タイムクォンタムを使い切ったら降格
  4. I/O待ちから復帰したら昇格(または維持)
  5. 一定時間ごとに全プロセスを最高優先度に引き上げ(ブースト)

  高優先度 [Q0] ─── クォンタム 8ms
               ↓ タイムアウト(CPU-bound)
  中優先度 [Q1] ─── クォンタム 16ms
               ↓ タイムアウト
  低優先度 [Q2] ─── FCFS
               ↑ I/O完了で昇格

  動作例:
  1. 新プロセスはQ0に投入
  2. 8ms以内に完了 or I/O待ち → Q0に留まる
  3. 8msを使い切り → Q1に降格
  4. Q1で16msを使い切り → Q2に降格
  5. 周期的ブーストで全員Q0に(飢餓防止)

  パラメータの設定:
設定すべきパラメータ:
1. キューの数
2. 各キューのスケジューリングアルゴリズム
3. 各キューのタイムクォンタム
4. 昇格の条件
5. 降格の条件
6. 新プロセスの初期キュー
7. ブーストの周期
→ パラメータが多い = 最も複雑だが最も柔軟
→ 多くの汎用OSで採用
ゲーミング(Gaming)への対策:
  悪意あるプロセスがI/Oを意図的に発行して
  高優先度を維持しようとする行為

  対策: アカウンティング(accounting)
  → キュー内での累積CPU使用時間を追跡
  → 累積時間がしきい値を超えたら降格
  → I/O直前の自発的放棄も計上

2.7 各アルゴリズムの比較

アルゴリズムプリエンプ飢餓平均待ち応答時間実装複雑度
ティブ時間
FCFSNoなし長い悪い○ 簡単
SJFNoあり最短△ 予測
SRTFYesあり最短良い△ 予測
RRYesなし良い○ 簡単
優先度両方あり良い
MLFQYesなし(※)短い良い× 複雑
※ ブーストにより飢餓を防止

3. Linuxのスケジューラ

3.1 Linuxスケジューラの歴史

Linux スケジューラの進化:

  Linux 2.4以前: O(n) スケジューラ
  → Runキュー内の全プロセスを走査して次を選択
  → プロセス数に比例して遅くなる
  → SMP対応が不十分

  Linux 2.6.0〜2.6.22: O(1) スケジューラ
  → 優先度ごとにキューを持つ
  → Active配列とExpired配列を交互に使用
  → O(1)で次のプロセスを選択
  → 対話的プロセスの判定にヒューリスティック使用
  → 判定が不正確で公平性に問題

  Linux 2.6.23〜6.5: CFS(Completely Fair Scheduler)
  → Con Kolivas氏のSD/RSDLスケジューラに触発
  → Ingo Molnár氏が開発
  → 赤黒木による公平なスケジューリング
  → 10年以上使用された安定したスケジューラ

  Linux 6.6〜: EEVDF(Earliest Eligible Virtual Deadline First)
  → Peter Zijlstra氏が開発
  → CFSの改良版、仮想デッドラインベース
  → レイテンシの改善とワークロード分離
バージョン年代スケジューラ
〜2.4〜2003O(n) スケジューラ
2.6.02003O(1) スケジューラ
2.6.232007CFS
6.62023EEVDF

3.2 CFS(Completely Fair Scheduler)

CFS(Completely Fair Scheduler, 2007〜2023):

  原理: 全プロセスに「公平に」CPU時間を分配
  → 理想: n個のプロセスがそれぞれ1/nのCPU時間を得る
  → 現実: 1プロセスずつ実行するため完全な公平は不可能
  → 「最も不公平な状態のプロセス」を次に実行する

  仮想ランタイム(vruntime):
  → プロセスが実行されると vruntime が増加
  → vruntime が最小のプロセスが「最も不公平な状態」
  → そのプロセスを次に選択

  vruntimeの計算:
  vruntime += 実行時間 × (NICE_0_LOAD / weight)

  nice値 → weight → vruntimeの増加速度
nice値weightvruntime速度
-2088761最もゆっくり(多くのCPU時間)
-53121やや遅い
01024基準(1倍速)
5335やや速い
1915最も速い(少ないCPU時間)
nice値が1違うと約1.25倍のCPU時間差
  → nice 0 vs nice 1: 55% vs 45%(約10%差)
  → nice 0 vs nice 5: 75% vs 25%(約3倍差)

  データ構造: 赤黒木(Red-Black Tree)
  → 最小vruntimeのプロセスをO(log n)で取得
  → 実際にはキャッシュされておりO(1)
赤黒木
┌───┐
5
/ \
┌───┐ ┌───┐
38
/ \ \
┌───┐┌───┐┌───┐
149
└───┘└───┘└───┘
↑ 次に実行
(最小vruntime)
最左ノード(min vruntime)がO(1)でキャッシュされている
  → 実質的にO(1)の選択時間

  スケジューリング粒度:
  → sched_latency: 全プロセスが一巡する目標時間(デフォルト6ms × プロセス数、上限24ms)
  → sched_min_granularity: 最小実行時間(デフォルト0.75ms)
  → nr_running > sched_latency / sched_min_granularity の場合
    各プロセスにsched_min_granularity を保証

  グループスケジューリング:
  → cgroup v2によるCPU帯域制御
  → ユーザーAとユーザーBがそれぞれ公平にCPU使用
  → ユーザーA(100プロセス) vs ユーザーB(1プロセス)でも50:50

  例(cgroupsでのCPU制御):
/sys/fs/cgroup/
├── group_a/
├── cpu.max = "50000 100000"
→ 100ms中50ms使用可能(50%)
└── cgroup.procs = 1234, 1235...
├── group_b/
├── cpu.max = "25000 100000"
→ 100ms中25ms使用可能(25%)
└── cgroup.procs = 2345, 2346...
└── cpu.max = "max 100000" (ルート)

3.3 EEVDF(Earliest Eligible Virtual Deadline First)

EEVDF(Earliest Eligible Virtual Deadline First, Linux 6.6〜):

  CFSの後継。CFSの問題点を解決:

  CFSの問題点:
  1. レイテンシに敏感なプロセスへの対応が不十分
  2. スリープしたプロセスへのボーナス処理が複雑
  3. nice値とレイテンシの関係が直感的でない
  4. 多くのヒューリスティック(経験則)に依存

  EEVDFの仕組み:
  → 各プロセスに仮想デッドライン(virtual deadline)を設定
  → eligible(実行可能)かつ最も早いデッドラインのプロセスを選択

  仮想デッドライン = 仮想開始時刻 + タイムスライス / weight

  2つの条件:
  1. Eligible(資格あり): vruntimeが十分に小さい
     → CPU時間を「借り過ぎていない」こと
  2. Earliest Deadline: 仮想デッドラインが最も早い
     → 早く完了すべきプロセスを優先
CFS:
「最も少ないCPU時間のプロセス」を選択
→ レイテンシの保証なし
EEVDF:
「デッドラインが最も早い資格あるプロセス」を選択
→ レイテンシが自然に制御される
→ ヒューリスティック不要
利点:
  - レイテンシに敏感なワークロード(音声、動画)の改善
  - sleeper bonus等のヒューリスティック排除
  - よりシンプルで予測可能な動作
  - タイムスライスの概念がnice値と直結

  sched_ext(Linux 6.11〜):
  → BPFプログラムによるカスタムスケジューラ
  → ユーザー空間からスケジューリングポリシーを定義
  → 再起動不要でスケジューラを切り替え可能
  → Meta(Facebook)が開発・活用
  → ゲーミング、サーバー等用途別に最適化

3.4 リアルタイムスケジューリング

Linuxのリアルタイムスケジューリングクラス:

  優先度順:
  1. SCHED_DEADLINE: デッドラインベース(最高優先度)
  2. SCHED_FIFO: リアルタイムFIFO
  3. SCHED_RR: リアルタイムラウンドロビン
  4. SCHED_OTHER(CFS/EEVDF): 通常プロセス(最低優先度)
  5. SCHED_BATCH: バッチ処理向け(CPU-bound)
  6. SCHED_IDLE: 最低優先度(アイドル時のみ実行)

  リアルタイムプロセスは常に通常プロセスより優先:
優先度 0〜99: リアルタイム(FIFO/RR/DEADLINE)
優先度 100〜139: 通常プロセス(CFS/EEVDF)
nice -20 → 優先度100
nice 0 → 優先度120
nice +19 → 優先度139
SCHED_FIFO:
  → 同優先度内ではFIFO(先着順)
  → より高い優先度が来るまでCPUを手放さない
  → yield()または終了まで実行し続ける
  → 危険: 暴走するとシステムがハングする

  SCHED_RR:
  → SCHED_FIFOにタイムクォンタムを追加
  → 同優先度内でラウンドロビン
  → 高優先度には即座にプリエンプトされる

  SCHED_DEADLINE:
  → EDF(Earliest Deadline First)ベース
  → 3つのパラメータ: Runtime, Deadline, Period
  → Runtime: Period内に保証されるCPU時間
  → Deadline: Runtime分の実行を完了すべき期限
  → Period: タスクの繰り返し周期
  → CBS(Constant Bandwidth Server)で帯域を予約

  例: 音声処理(5ms/20ms周期)
Period = 20ms
Runtime = 5ms
Deadline = 20ms
→ 20msごとに5msのCPU時間が保証される
→ CPU帯域 = 5/20 = 25%
┌──────────┬────────────────────┐
Run(5ms)Wait(15ms)→繰返
└──────────┴────────────────────┘
0 5 20
# リアルタイムスケジューリングの設定例
 
# 現在のスケジューリングポリシーの確認
chrt -p $$
 
# SCHED_FIFO(優先度50)で実行
sudo chrt -f 50 ./realtime_app
 
# SCHED_RR(優先度30)で実行
sudo chrt -r 30 ./realtime_app
 
# SCHED_DEADLINE で実行
# Runtime=5ms, Deadline=10ms, Period=20ms
sudo chrt -d --sched-runtime 5000000 \
             --sched-deadline 10000000 \
             --sched-period 20000000 \
             0 ./realtime_app
 
# リアルタイム帯域の制限(暴走防止)
# デフォルト: 1秒中950msまでリアルタイムに使用可能
cat /proc/sys/kernel/sched_rt_runtime_us   # 950000
cat /proc/sys/kernel/sched_rt_period_us    # 1000000
 
# リアルタイム帯域制限を無効化(危険)
echo -1 > /proc/sys/kernel/sched_rt_runtime_us

3.5 マルチコアスケジューリング

マルチコアスケジューリングの課題:

  1. ロードバランシング:
     各コアに均等にプロセスを分配

     Push migration: 過負荷のコアからプロセスを移動
     Pull migration: アイドルのコアがプロセスを引き取る
CPU 0CPU 1CPU 2
[P1,P2,[P5][]
P3,P4]idle
│ push                      │ pull
          └───────────→ P3 ←──────────┘

  2. キャッシュアフィニティ:
     プロセスをできるだけ同じコアで実行し続ける
     → L1/L2キャッシュにデータが残っている
     → 別コアに移動するとキャッシュミスが発生

     キャッシュのウォームアップコスト:
     L1キャッシュ: 〜1ns(数サイクル)
     L2キャッシュ: 〜5ns
     L3キャッシュ: 〜20ns
     メインメモリ: 〜100ns

     → 移動のコスト vs バランスの利益を天秤にかける

  3. NUMA(Non-Uniform Memory Access)対応:
     メモリへのアクセス速度がCPU位置に依存
NUMA Node 0NUMA Node 1
┌──────┐ ┌─────┐┌─────┐ ┌──────┐
CPU 0-3Mem A←→Mem BCPU 4-7
└──────┘ └─────┘└─────┘ └──────┘
ローカルアクセス      リモートアクセス
       〜100ns              〜300ns(3倍遅い)

     → プロセスはメモリが近いNUMAノードで実行すべき
     → Linuxはnumactl, libnumaで制御可能

  4. SMT(Simultaneous Multi-Threading / Hyper-Threading):
     1つの物理コアに2つ以上の論理コア
     → 実行ユニットを共有するため、100%の性能向上にはならない
     → 典型的には20〜30%の性能向上
     → スケジューラはSMT兄弟の使い方を考慮

  Linuxのスケジューリングドメイン:
SD_LEVEL_0: SMT (ハイパースレッド)
SD_LEVEL_1: MC (マルチコア)
SD_LEVEL_2: PKG (パッケージ/ソケット)
SD_LEVEL_3: NUMA (NUMAノード)
各レベルでロードバランシングの頻度と
閾値が異なる:
- SMT: 高頻度でバランス
- NUMA: 低頻度でバランス(コスト大)
# マルチコアスケジューリングの実践
 
# CPUアフィニティの設定
taskset -p 0x3 $$              # CPU 0,1に限定(ビットマスク)
taskset -c 0-3 ./my_program   # CPU 0〜3で実行
 
# NUMAの確認
numactl --hardware              # NUMAトポロジの表示
numactl --membind=0 ./program  # Node 0のメモリに限定
numactl --cpunodebind=0 --membind=0 ./program  # CPU+メモリをNode 0に
 
# スケジューリング統計の確認
cat /proc/schedstat             # グローバル統計
cat /proc/$$/sched              # プロセス別統計
cat /proc/$$/status | grep -i cpu  # 実行コア
 
# per-CPUのRunキュー長
cat /proc/schedstat | awk '{if(NR%3==1) print "CPU"(NR-1)/3": rq_len="$2}'
 
# CPU使用率のリアルタイム監視
mpstat -P ALL 1                 # コアごと
pidstat -u 1                    # プロセスごと
htop                            # 対話的(CPU棒グラフ表示)

4. 他のOSのスケジューラ

4.1 Windows のスケジューラ

Windows スケジューラ:

  優先度ベースのプリエンプティブスケジューリング
  32段階の優先度:
  - 0: ゼロページスレッド(メモリゼロ化専用)
  - 1-15: 通常プロセス(動的優先度)
  - 16-31: リアルタイムプロセス(固定優先度)

  優先度クラス × 相対優先度 = 実効優先度:
優先度クラスLowNormalHighRT
Idle11116
Below Normal57921
Normal681024
Above Normal791125
High11131526
Realtime16243131
動的優先度ブースト:
  - フォアグラウンドウィンドウのスレッド: +2
  - I/O完了: +1〜+8(デバイス種別による)
  - GUI入力待ちからの復帰: +2
  → ブースト後は1ずつ元の基本優先度に戻る

  Multimedia Class Scheduler Service (MMCSS):
  → 音声・動画再生のスレッドに自動的に高優先度を付与
  → 定期的にCPU時間を保証

4.2 macOS/iOS のスケジューラ

macOS/iOS(XNU Kernel)スケジューラ:

  マルチレベルフィードバックキュー + デケイスケジューリング

  スケジューリングバンド:
  1. RT (リアルタイム): 固定優先度
  2. System High: カーネルスレッド
  3. System: カーネルスレッド
  4. User Initiated: ユーザー操作
  5. Default: 一般
  6. Utility: 長時間タスク
  7. Background: バックグラウンド
  8. Maintenance: メンテナンス

  Quality of Service (QoS):
  → アプリはQoSクラスを指定して意図を伝える
  → システムがCPU、I/O、タイマーの優先度を自動調整

  GCD(Grand Central Dispatch)連携:
  → dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ ... });
  → スケジューラとGCDが連携してリソースを最適配分

  Apple Silicon(M1〜)の高効率/高性能コア:
  → P-core(Performance): 高優先度タスク
  → E-core(Efficiency): 低優先度・バックグラウンドタスク
  → スケジューラがQoSに基づいて自動振り分け
QoS: User Interactive → P-core優先
QoS: Background → E-core限定
QoS: Default → 負荷に応じて動的

5. スケジューリングの応用

5.1 コンテナ環境でのスケジューリング

コンテナとCPUスケジューリング:

  Dockerコンテナ:
  → cgroups v2でCPU帯域を制御
  → スケジューラはcgroup単位でも公平性を保証

  CPU制限の設定:
  # CPUの50%に制限
  docker run --cpus="0.5" nginx

  # CPU 0と1のみ使用
  docker run --cpuset-cpus="0,1" nginx

  # 相対的なCPU weightの設定
  docker run --cpu-shares=512 app_low    # 通常の半分
  docker run --cpu-shares=2048 app_high  # 通常の2倍

  Kubernetes のCPUリソース:
resources:
requests:
cpu: "250m" # 0.25 CPU保証
limits:
cpu: "500m" # 0.5 CPU上限
requests → cpu.shares (CFS weight)
limits → cpu.max (帯域制限)
1000m = 1 CPU = 100ms/100ms
500m = 0.5 CPU = 50ms/100ms
250m = 0.25 CPU = 25ms/100ms
CPU throttling問題:
  → limits設定時、バースト的なCPU使用でスロットリングが発生
  → 短時間で大量のCPUを消費するとperiod終了まで待機させられる
  → レイテンシが急増する原因になる
  → 対策: limitsを設定しない or 十分に余裕を持たせる

5.2 データベースのCPUスケジューリング

データベースでのスケジューリング考慮事項:

  PostgreSQL:
  → 各接続が独立したプロセス(fork)
  → OSスケジューラに依存
  → hugepages推奨(TLBミス削減)
  → NUMAローカルメモリ配置が重要

  MySQL (InnoDB):
  → スレッドプール + ワーカースレッド
  → innodb_thread_concurrency でスレッド数制御
  → OSスケジューラに依存するが、内部にも軽量スケジューラ

  推奨設定例(Linux):
# CPUアフィニティ
taskset -c 0-15 postgres
# NUMA ローカルメモリ
numactl --interleave=all postgres
# リアルタイム優先度(注意して使用)
chrt -r 5 postgres
# CPUガバナー(省電力無効化)
cpupower frequency-set -g performance
# CPU C-States無効化(レイテンシ削減)
echo 0 > /dev/cpu_dma_latency

5.3 仮想化環境でのスケジューリング

仮想マシンのスケジューリング:

  2レベルのスケジューリング:
  1. ハイパーバイザが仮想CPUをスケジュール
  2. ゲストOSが仮想CPU上でプロセスをスケジュール
ゲストOS A ゲストOS B
┌─────────────┐ ┌─────────────┐
P1 P2 P3P4 P5
スケジューラスケジューラ
vCPU0 vCPU1vCPU0 vCPU1
└──────┬──────┘ └──────┬──────┘
┌──────┴──────────────────┴──────┐
ハイパーバイザスケジューラ
pCPU 0 pCPU 1 pCPU 2
└────────────────────────────────┘
問題: Lock Holder Preemption
  → ゲストOS内でロックを保持しているvCPUが
    ハイパーバイザにプリエンプトされる
  → 他のvCPUがロック待ちでスピン
  → CPU時間の無駄遣い

  対策:
  - pause loop exiting(Intel VT-x)
  - halting(スピンを検出してハイパーバイザに通知)
  - CPU overcommit を避ける(vCPU ≤ pCPU)

  VMware vSphere:
  → Proportional Share Based Scheduling
  → CPU shares, reservations, limits でリソース配分
  → DRS(Distributed Resource Scheduler)でクラスタ間バランス

6. 実践演習

演習1: [基礎] — スケジューリングの手計算

以下のプロセスをFCFS、SJF、RR(q=2)でスケジュールし、
平均待ち時間と平均ターンアラウンド時間を比較せよ。

| プロセス | 到着時刻 | 実行時間 |
|---------|---------|---------|
| P1      | 0       | 6       |
| P2      | 1       | 3       |
| P3      | 2       | 1       |
| P4      | 3       | 4       |

解答例(FCFS):
P1(6)P2P3P4
0          6   9 10   14

待ち時間: P1=0, P2=6-1=5, P3=9-2=7, P4=10-3=7
平均待ち時間: (0+5+7+7)/4 = 4.75ms

ターンアラウンド: P1=6, P2=9-1=8, P3=10-2=8, P4=14-3=11
平均ターンアラウンド: (6+8+8+11)/4 = 8.25ms

解答例(SJF ノンプリエンプティブ):
t=0: P1のみ → P1開始
t=6: P1完了。Ready: P2(3), P3(1), P4(4) → P3(最短)
t=7: P3完了。Ready: P2(3), P4(4) → P2
t=10: P2完了。→ P4
t=14: P4完了
P1(6)P3P2P4
0          6 7   10   14

待ち時間: P1=0, P3=7-2-1=4, P2=7-1=6, P4=10-3=7
→ 修正: P1=0, P3=6-2=4, P2=7-1=6, P4=10-3=7
平均待ち時間: (0+6+4+7)/4 = 4.25ms

解答例(RR, q=2):
t=0-2:   P1(残4)  → Ready: P2, P1
t=2-4:   P2(残1)  → Ready: P3, P4, P1, P2
t=4-5:   P3(完了) → Ready: P4, P1, P2
t=5-7:   P4(残2)  → Ready: P1, P2, P4
t=7-9:   P1(残2)  → Ready: P2, P4, P1
t=9-10:  P2(完了) → Ready: P4, P1
t=10-12: P4(完了) → Ready: P1
t=12-14: P1(完了)

待ち時間: P1=14-6=8, P2=10-1-3=6, P3=5-2-1=2, P4=12-3-4=5
平均待ち時間: (8+6+2+5)/4 = 5.25ms

演習2: [応用] — Linuxスケジューラの観察

# プロセスのスケジューリング情報を確認
chrt -p $$                    # 現在のスケジューリングポリシー
cat /proc/$$/sched            # スケジューリング統計(Linux)
 
# nice値の変更
nice -n 10 sleep 100 &        # nice値10で起動
renice -n 5 -p <PID>          # 実行中のプロセスのnice値変更
 
# CPU使用率の監視
top                           # リアルタイム監視
mpstat -P ALL 1               # コアごとの使用率
pidstat -u 1                  # プロセスごとのCPU使用率
 
# スケジューリング遅延の測定
perf sched record sleep 10     # 10秒間のスケジューリングイベント記録
perf sched latency             # スケジューリング遅延の統計
 
# スケジューラのデバッグ情報
cat /proc/sched_debug          # スケジューラの内部状態
cat /sys/kernel/debug/sched/debug  # デバッグ情報(debugfs)
 
# CFS パラメータの確認・変更
cat /proc/sys/kernel/sched_latency_ns          # 目標レイテンシ
cat /proc/sys/kernel/sched_min_granularity_ns  # 最小粒度
cat /proc/sys/kernel/sched_wakeup_granularity_ns  # ウェイクアップ粒度

演習3: [応用] — CPUバウンドとI/Oバウンドの違いの体験

#!/bin/bash
# CPUバウンドプロセスの生成
cpu_bound() {
    echo "CPU-bound (PID: $$) starting with nice $1"
    nice -n $1 dd if=/dev/urandom of=/dev/null bs=1M count=1000 2>&1
}
 
# I/Oバウンドプロセスの生成
io_bound() {
    echo "I/O-bound (PID: $$) starting"
    for i in $(seq 1 1000); do
        dd if=/dev/zero of=/tmp/test_$$ bs=4K count=1 2>/dev/null
        rm -f /tmp/test_$$
    done
}
 
# 異なるnice値で同時実行して比較
time nice -n  0 python3 -c "sum(range(10**8))" &
time nice -n 19 python3 -c "sum(range(10**8))" &
wait
 
# 結果: nice 0のプロセスが先に終了する

演習4: [発展] — コンテナのCPU制限

# Docker でCPU制限のテスト
# CPUを0.5コアに制限
docker run --rm --cpus="0.5" ubuntu:22.04 \
    bash -c "time dd if=/dev/urandom of=/dev/null bs=1M count=500"
 
# CPU制限なし
docker run --rm ubuntu:22.04 \
    bash -c "time dd if=/dev/urandom of=/dev/null bs=1M count=500"
 
# CPU throttling の確認
docker run -d --name test --cpus="0.2" ubuntu:22.04 sleep 3600
# コンテナのcgroup統計
cat /sys/fs/cgroup/system.slice/docker-$(docker inspect test --format '{{.Id}}').scope/cpu.stat
# nr_throttled: スロットリングされた回数
# throttled_time: スロットリングされた合計時間
 
# クリーンアップ
docker rm -f test

トラブルシューティング

よくあるエラーと解決策

エラー 原因 解決策
初期化エラー 設定ファイルの不備 設定ファイルのパスと形式を確認
タイムアウト ネットワーク遅延/リソース不足 タイムアウト値の調整、リトライ処理の追加
メモリ不足 データ量の増大 バッチ処理の導入、ページネーションの実装
権限エラー アクセス権限の不足 実行ユーザーの権限確認、設定の見直し
データ不整合 並行処理の競合 ロック機構の導入、トランザクション管理

デバッグの手順

  1. エラーメッセージの確認: スタックトレースを読み、発生箇所を特定する
  2. 再現手順の確立: 最小限のコードでエラーを再現する
  3. 仮説の立案: 考えられる原因をリストアップする
  4. 段階的な検証: ログ出力やデバッガを使って仮説を検証する
  5. 修正と回帰テスト: 修正後、関連する箇所のテストも実行する
# デバッグ用ユーティリティ
import logging
import traceback
from functools import wraps
 
# ロガーの設定
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s [%(levelname)s] %(name)s: %(message)s'
)
logger = logging.getLogger(__name__)
 
def debug_decorator(func):
    """関数の入出力をログ出力するデコレータ"""
    @wraps(func)
    def wrapper(*args, **kwargs):
        logger.debug(f"呼び出し: {func.__name__}(args={args}, kwargs={kwargs})")
        try:
            result = func(*args, **kwargs)
            logger.debug(f"戻り値: {func.__name__} -> {result}")
            return result
        except Exception as e:
            logger.error(f"例外発生: {func.__name__}: {e}")
            logger.error(traceback.format_exc())
            raise
    return wrapper
 
@debug_decorator
def process_data(items):
    """データ処理(デバッグ対象)"""
    if not items:
        raise ValueError("空のデータ")
    return [item * 2 for item in items]

パフォーマンス問題の診断

パフォーマンス問題が発生した場合の診断手順:

  1. ボトルネックの特定: プロファイリングツールで計測
  2. メモリ使用量の確認: メモリリークの有無をチェック
  3. I/O待ちの確認: ディスクやネットワークI/Oの状況を確認
  4. 同時接続数の確認: コネクションプールの状態を確認
問題の種類 診断ツール 対策
CPU負荷 cProfile, py-spy アルゴリズム改善、並列化
メモリリーク tracemalloc, objgraph 参照の適切な解放
I/Oボトルネック strace, iostat 非同期I/O、キャッシュ
DB遅延 EXPLAIN, slow query log インデックス、クエリ最適化

設計判断ガイド

選択基準マトリクス

技術選択を行う際の判断基準を以下にまとめます。

判断基準 重視する場合 妥協できる場合
パフォーマンス リアルタイム処理、大規模データ 管理画面、バッチ処理
保守性 長期運用、チーム開発 プロトタイプ、短期プロジェクト
スケーラビリティ 成長が見込まれるサービス 社内ツール、固定ユーザー
セキュリティ 個人情報、金融データ 公開データ、社内利用
開発速度 MVP、市場投入スピード 品質重視、ミッションクリティカル

アーキテクチャパターンの選択

アーキテクチャ選択フロー
① チーム規模は?
├─ 小規模(1-5人)→ モノリス
└─ 大規模(10人+)→ ②へ
② デプロイ頻度は?
├─ 週1回以下 → モノリス + モジュール分割
└─ 毎日/複数回 → ③へ
③ チーム間の独立性は?
├─ 高い → マイクロサービス
└─ 中程度 → モジュラーモノリス

トレードオフの分析

技術的な判断には必ずトレードオフが伴います。以下の観点で分析を行いましょう:

1. 短期 vs 長期のコスト

  • 短期的に速い方法が長期的には技術的負債になることがある
  • 逆に、過剰な設計は短期的なコストが高く、プロジェクトの遅延を招く

2. 一貫性 vs 柔軟性

  • 統一された技術スタックは学習コストが低い
  • 多様な技術の採用は適材適所が可能だが、運用コストが増加

3. 抽象化のレベル

  • 高い抽象化は再利用性が高いが、デバッグが困難になる場合がある
  • 低い抽象化は直感的だが、コードの重複が発生しやすい
# 設計判断の記録テンプレート
class ArchitectureDecisionRecord:
    """ADR (Architecture Decision Record) の作成"""
 
    def __init__(self, title: str):
        self.title = title
        self.context = ""
        self.decision = ""
        self.consequences = []
        self.alternatives = []
 
    def set_context(self, context: str):
        """背景と課題の記述"""
        self.context = context
        return self
 
    def set_decision(self, decision: str):
        """決定内容の記述"""
        self.decision = decision
        return self
 
    def add_consequence(self, consequence: str, positive: bool = True):
        """結果の追加"""
        self.consequences.append({
            'description': consequence,
            'type': 'positive' if positive else 'negative'
        })
        return self
 
    def add_alternative(self, name: str, reason_rejected: str):
        """却下した代替案の追加"""
        self.alternatives.append({
            'name': name,
            'reason_rejected': reason_rejected
        })
        return self
 
    def to_markdown(self) -> str:
        """Markdown形式で出力"""
        md = f"# ADR: {self.title}\n\n"
        md += f"## 背景\n{self.context}\n\n"
        md += f"## 決定\n{self.decision}\n\n"
        md += "## 結果\n"
        for c in self.consequences:
            icon = "✅" if c['type'] == 'positive' else "⚠️"
            md += f"- {icon} {c['description']}\n"
        md += "\n## 却下した代替案\n"
        for a in self.alternatives:
            md += f"- **{a['name']}**: {a['reason_rejected']}\n"
        return md

実務での適用シナリオ

シナリオ1: スタートアップでのMVP開発

状況: 限られたリソースで素早くプロダクトをリリースする必要がある

アプローチ:

  • シンプルなアーキテクチャを選択
  • 必要最小限の機能に集中
  • 自動テストはクリティカルパスのみ
  • モニタリングは早期から導入

学んだ教訓:

  • 完璧を求めすぎない(YAGNI原則)
  • ユーザーフィードバックを早期に取得
  • 技術的負債は意識的に管理する

シナリオ2: レガシーシステムのモダナイゼーション

状況: 10年以上運用されているシステムを段階的に刷新する

アプローチ:

  • Strangler Fig パターンで段階的に移行
  • 既存のテストがない場合はCharacterization Testを先に作成
  • APIゲートウェイで新旧システムを共存
  • データ移行は段階的に実施
フェーズ 作業内容 期間目安 リスク
1. 調査 現状分析、依存関係の把握 2-4週間
2. 基盤 CI/CD構築、テスト環境 4-6週間
3. 移行開始 周辺機能から順次移行 3-6ヶ月
4. コア移行 中核機能の移行 6-12ヶ月
5. 完了 旧システム廃止 2-4週間

シナリオ3: 大規模チームでの開発

状況: 50人以上のエンジニアが同一プロダクトを開発する

アプローチ:

  • ドメイン駆動設計で境界を明確化
  • チームごとにオーナーシップを設定
  • 共通ライブラリはInner Source方式で管理
  • APIファーストで設計し、チーム間の依存を最小化
# チーム間のAPI契約定義
from dataclasses import dataclass
from typing import List, Optional
from enum import Enum
 
class Priority(Enum):
    LOW = "low"
    MEDIUM = "medium"
    HIGH = "high"
    CRITICAL = "critical"
 
@dataclass
class APIContract:
    """チーム間のAPI契約"""
    endpoint: str
    method: str
    owner_team: str
    consumers: List[str]
    sla_ms: int  # レスポンスタイムSLA
    priority: Priority
 
    def validate_sla(self, actual_ms: int) -> bool:
        """SLA準拠の確認"""
        return actual_ms <= self.sla_ms
 
    def to_openapi(self) -> dict:
        """OpenAPI形式で出力"""
        return {
            'path': self.endpoint,
            'method': self.method,
            'x-owner': self.owner_team,
            'x-consumers': self.consumers,
            'x-sla-ms': self.sla_ms
        }
 
# 使用例
contracts = [
    APIContract(
        endpoint="/api/v1/users",
        method="GET",
        owner_team="user-team",
        consumers=["order-team", "notification-team"],
        sla_ms=200,
        priority=Priority.HIGH
    ),
    APIContract(
        endpoint="/api/v1/orders",
        method="POST",
        owner_team="order-team",
        consumers=["payment-team", "inventory-team"],
        sla_ms=500,
        priority=Priority.CRITICAL
    )
]

シナリオ4: パフォーマンスクリティカルなシステム

状況: ミリ秒単位のレスポンスが求められるシステム

最適化ポイント:

  1. キャッシュ戦略(L1: インメモリ、L2: Redis、L3: CDN)
  2. 非同期処理の活用
  3. コネクションプーリング
  4. クエリ最適化とインデックス設計
最適化手法 効果 実装コスト 適用場面
インメモリキャッシュ 頻繁にアクセスされるデータ
CDN 静的コンテンツ
非同期処理 I/O待ちが多い処理
DB最適化 クエリが遅い場合
コード最適化 低-中 CPU律速の場合

7. FAQ

Q1: タイムクォンタムの最適値は?

一般的には10〜100ms。短すぎるとコンテキストスイッチのオーバーヘッドが増大、長すぎると応答性が悪化。Linuxのデフォルトは約6ms(CFSの目標レイテンシから算出、sched_latency_ns / プロセス数)。ただしEEVDFでは明示的なタイムスライスの概念がnice値のweightから自然に導出される。

Q2: リアルタイムOSと汎用OSの違いは?

リアルタイムOS(RTOS)は「デッドラインまでに必ず処理を完了する」保証を提供。ハードリアルタイム(航空機制御、ABS)は違反が致命的。ソフトリアルタイム(動画再生、VoIP)は違反が品質低下に留まる。LinuxはCONFIG_PREEMPT_RTパッチで「ほぼリアルタイム」を実現可能(Linux 6.12でメインラインに完全統合)。代表的なRTOSにはVxWorks、FreeRTOS、QNX、Zephyrなどがある。

Q3: nice値と優先度の違いは?

nice値はユーザーが設定する相対的な「親切さ」の度合い(-20〜+19)。高いnice値のプロセスは「他のプロセスに親切」=自分のCPU時間を譲る。OSの内部優先度はnice値から計算されるが、他の要因(I/O待ちからの復帰、リアルタイム優先度など)も加味される。Linuxでは内部優先度100〜139がnice -20〜+19に対応する。一般ユーザーはniceを上げることのみ可能(nice値を下げるにはroot権限が必要)。

Q4: なぜLinuxはMLFQではなくCFS/EEVDFを採用しているのか?

MLFQは多くのパラメータ(キュー数、各キューのクォンタム、昇降格条件など)を適切に設定する必要があり、ワークロードに依存する。CFSは「公平性」という単一の原理に基づき、パラメータが少なく、幅広いワークロードで良好な性能を発揮する。EEVDFはさらにレイテンシの保証を改善し、ヒューリスティックを排除した。ただし、sched_ext(Linux 6.11+)によりBPFベースのカスタムMLFQスケジューラを実装することも可能になった。

Q5: スケジューリングはパフォーマンスにどの程度影響するのか?

通常のワークロードではスケジューラの選択による影響は数%〜10%程度。しかし、以下の場合は大きな影響がある:

  • レイテンシに敏感なワークロード(ゲーム、音声、HFT): 数ms〜数十msの差が品質に直結
  • 大量のスレッド(数百〜数千): スケジューラのオーバーヘッドが顕著に
  • NUMA環境: 不適切なスケジューリングでメモリアクセスが3倍遅くなる
  • コンテナ環境: CPU throttlingにより99パーセンタイルレイテンシが10倍以上に

Q6: コンテキストスイッチを減らすには?

  1. スレッド数をCPUコア数に合わせる(過剰なスレッド作成を避ける)
  2. I/O多重化(epoll/kqueue/io_uring)でスレッド数を削減
  3. ユーザー空間スケジューリング(goroutine, Tokio, Green Thread)を使用
  4. CPUアフィニティを設定してコア間移動を防止
  5. リアルタイム優先度でプリエンプトを防ぐ(慎重に)
  6. ロックの粒度を適切にしてロック待ちによるスイッチを削減

FAQ

Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?

実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。

Q2: 初心者がよく陥る間違いは何ですか?

基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。

Q3: 実務ではどのように活用されていますか?

このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。


8. まとめ

アルゴリズム 特徴 用途
FCFS シンプル、Convoy Effect バッチ処理
SJF/SRTF 最適だが予測困難 理論的に重要
ラウンドロビン 公平、応答性良好 対話的システム
優先度 重要なプロセスを優先 リアルタイム要素あり
MLFQ 動的優先度、最も柔軟 汎用OS
CFS 仮想ランタイムで公平 Linux 2.6.23〜6.5
EEVDF 仮想デッドラインで改善 Linux 6.6+

次に読むべきガイド


参考文献

  1. Silberschatz, A. et al. "Operating System Concepts." 10th Ed, Ch.5, 2018.
  2. Love, R. "Linux Kernel Development." 3rd Ed, Ch.4, 2010.
  3. Arpaci-Dusseau, R. H. & Arpaci-Dusseau, A. C. "Operating Systems: Three Easy Pieces." Ch.7-10, 2018.
  4. Molnár, I. "CFS Scheduler." Linux Kernel Documentation, 2007.
  5. Zijlstra, P. "EEVDF Scheduler." Linux Kernel Mailing List, 2023.
  6. Corbet, J. "An EEVDF CPU scheduler for Linux." LWN.net, 2023.
  7. Corbet, J. "Extensible scheduling with sched_ext." LWN.net, 2024.
  8. Tanenbaum, A. S. & Bos, H. "Modern Operating Systems." 4th Ed, Ch.2, 2014.