Skip to content
Published on

[オペレーティングシステム] 05. CPUスケジューリングアルゴリズム

Authors

CPUスケジューリングの基本概念

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

プロセスの実行はCPUバーストとI/Oバーストの交代で構成される。

[プロセス実行サイクル]

CPUバースト -> I/Oバースト -> CPUバースト -> I/Oバースト -> ...

  |-------|          |--------|          |--|
  CPU実行           I/O待ち            CPU実行
  (計算)          (ディスク読み取り)  (計算)

CPUバウンドプロセス:長いCPUバースト、少ないI/Oバースト
I/Oバウンドプロセス:短いCPUバースト、多いI/Oバースト

プリエンプティブ vs ノンプリエンプティブスケジューリング

  • ノンプリエンプティブ(Non-preemptive): プロセスが自発的にCPUを返すまで実行を継続
  • プリエンプティブ(Preemptive): OSが実行中のプロセスからCPUを強制的に回収可能

CPUスケジューリングが必要な4つの状況がある。

  1. 実行状態から待機状態への遷移(I/O要求)- ノンプリエンプティブ
  2. 実行状態から準備状態への遷移(割り込み)- プリエンプティブ
  3. 待機状態から準備状態への遷移(I/O完了)- プリエンプティブ
  4. プロセスの終了 - ノンプリエンプティブ

ディスパッチャ(Dispatcher)

ディスパッチャはCPUスケジューラが選択したプロセスに実際にCPUを渡すモジュールである。

  • コンテキストスイッチの実行
  • ユーザーモードへの切り替え
  • プログラムの適切な位置へのジャンプ

ディスパッチレイテンシ(dispatch latency)はできるだけ短くなければならない。


スケジューリング基準

基準説明最適化方向
CPU利用率CPUが動作している時間の比率最大化
スループット(Throughput)単位時間あたりの完了プロセス数最大化
ターンアラウンドタイム提出から完了までの時間最小化
待ち時間(Waiting)レディキューで待機した総時間最小化
応答時間(Response)提出から最初の応答までの時間最小化

スケジューリングアルゴリズム

1. FCFS(First-Come, First-Served)

最もシンプルなスケジューリング。先に到着したプロセスが先に実行される。

[FCFS]

プロセス  到着時間  CPUバースト
  P1        0         24
  P2        0          3
  P3        0          3

実行順序:P1 -> P2 -> P3

ガントチャート:
|----P1----|--P2--|--P3--|
0         24    27     30

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

P2, P3, P1の順序だったら?
|P2|P3|----P1----|
0  3  6         30

待ち時間:P1=6, P2=0, P3=3
平均待ち時間:(6+0+3)/3 = 3(大きな差!)

コンボイ効果(Convoy Effect): 長いプロセスの後ろに短いプロセスが並ぶ現象。FCFSの代表的な欠点である。

2. SJF(Shortest-Job-First)

CPUバーストが最も短いプロセスを先に実行する。理論的に最適な平均待ち時間を保証する。

[SJF- ノンプリエンプティブ]

プロセス  到着時間  CPUバースト
  P1        0         6
  P2        2         8
  P3        4         7
  P4        5         3

ガントチャート:
|--P1--|P4|---P3---|----P2----|
0      6  9      16         24

待ち時間:P1=0, P2=16, P3=5, P4=1
平均待ち時間:(0+16+5+1)/4 = 5.5
[SRTF(Shortest Remaining Time First)- プリエンプティブSJF]

プロセス  到着時間  CPUバースト
  P1        0         6
  P2        2         8
  P3        4         7
  P4        5         3

時間 0: P1開始(残り:6時間 2: P2到着(残り:8-> P1継続(残り:4時間 4: P3到着(残り:7-> P1継続(残り:2時間 5: P4到着(残り:3-> P1継続(残り:1時間 6: P1完了 -> P4開始(残り:3時間 9: P4完了 -> P3開始(残り:7時間 16: P3完了 -> P2開始(残り:8時間 24: P2完了

ガントチャート:
|--P1--|P4|---P3---|----P2----|
0      6  9      16         24

SJFの核心的な問題は、次のCPUバースト長を事前に知ることができないことである。実際には過去のバーストに基づく**指数平均(exponential averaging)**で予測する。

次の予測値 = alpha * 最近の実測値 + (1 - alpha) * 前回の予測値

alpha = 0 なら最近の値を無視(前回の予測値のみ使用)
alpha = 1 なら前回の予測を無視(最近の実測値のみ使用)
通常 alpha = 0.5 を使用

3. 優先度スケジューリング(Priority Scheduling)

各プロセスに優先度を付与し、最も高い優先度のプロセスを先に実行する。

[優先度スケジューリング例](小さい数値 = 高い優先度)

プロセス  CPUバースト  優先度
  P1        10         3
  P2         1         1
  P3         2         4
  P4         1         5
  P5         5         2

実行順序:P2 -> P5 -> P1 -> P3 -> P4

ガントチャート:
|P2|--P5--|----P1----|P3|P4|
0  1      6        16 18 19

平均待ち時間:(6+0+16+18+1)/5 = 8.2

飢餓(Starvation): 低い優先度のプロセスが永遠に実行されない問題。**エイジング(Aging)**技法で解決する。時間の経過とともに待機中のプロセスの優先度を徐々に上げる。

4. ラウンドロビン(Round Robin)

タイムシェアリングシステム用のスケジューリング。各プロセスに固定タイムクォンタム(time quantum)を付与する。

[ラウンドロビン例](タイムクォンタム = 4
プロセス  CPUバースト
  P1        24
  P2         3
  P3         3

ガントチャート:
|P1|P2|P3|P1|P1|P1|P1|P1|
0  4  7 10 14 18 22 26 30

P14単位実行 -> レディキューの末尾へ
P23単位実行(完了)
P33単位実行(完了)
P1:残り20単位を4ずつ分けて実行

待ち時間:P1=6, P2=4, P3=7
平均待ち時間:(6+4+7)/3 = 5.67

タイムクォンタム(q)の選択が重要である。

  • qが大きすぎると:FCFSと同じ
  • qが小さすぎると:コンテキストスイッチオーバーヘッド増加
  • 経験的にCPUバーストの80%がq以内に完了するように設定

5. 多段キュースケジューリング(Multilevel Queue)

レディキューを複数のキューに分離し、各キューに異なるスケジューリングアルゴリズムを適用する。

[多段キュースケジューリング]

高優先度
+------------------------------------------+
| システムプロセス       | FCFS              |
+------------------------------------------+
| 対話型プロセス         | RR (q=8)          |
+------------------------------------------+
| 対話型編集プロセス     | RR (q=16)         |
+------------------------------------------+
| バッチプロセス         | FCFS              |
+------------------------------------------+
| 学生プロセス           | FCFS              |
+------------------------------------------+
低優先度

上位キューが空になって初めて下位キューを実行(固定優先度)
またはキュー間の時間配分(例:80%対話型、20%バッチ)

6. 多段フィードバックキュー(Multilevel Feedback Queue)

プロセスがキュー間を移動できる。最も柔軟なスケジューリングアルゴリズムである。

[多段フィードバックキュー]

キュー 0: RR (q=8)   <--- 新しいプロセスがここに入る
  |
  | q=8以内に未完了ならキュー1へ移動
  v
キュー 1: RR (q=16)
  |
  | q=16以内に未完了ならキュー2へ移動
  v
キュー 2: FCFS

動作例:
- CPUバーストが短いプロセス:キュー0ですぐに完了(速い応答)
- CPUバウンドプロセス:キュー0 -> キュー1 -> キュー2へ下がる
- I/O完了後に復帰するプロセス:再びキュー0に上がる

結果:I/Oバウンド+対話型プロセスに高い優先度
      CPUバウンドプロセスは低いキューで長い時間割り当て

スレッドスケジューリング

  • PCS(Process-Contention Scope): ユーザーレベルで同じプロセスのスレッド同士が競合
  • SCS(System-Contention Scope): システムレベルですべてのスレッドが競合

LinuxとWindowsはSCSを使用する。カーネルスレッドのみがCPUにスケジューリングされる。


マルチプロセッサスケジューリング

アプローチ

  • 非対称マルチプロセッシング: 1つのプロセッサがすべてのスケジューリング決定(単純だがボトルネック)
  • 対称マルチプロセッシング(SMP): 各プロセッサが独立してスケジューリング(現代の標準)

プロセッサアフィニティ(Processor Affinity)

プロセスは現在実行中のプロセッサに留まる傾向がある。キャッシュにデータが載っているためである。

  • ソフトアフィニティ: できるだけ同じプロセッサで実行するが保証しない
  • ハードアフィニティ: 特定のプロセッサセットでの実行を保証
// Linuxでのプロセッサアフィニティ設定
#define _GNU_SOURCE
#include <sched.h>

void set_cpu_affinity(int cpu_id) {
    cpu_set_t cpuset;
    CPU_ZERO(&cpuset);
    CPU_SET(cpu_id, &cpuset);

    // 現在のスレッドを特定のCPUにバインド
    sched_setaffinity(0, sizeof(cpuset), &cpuset);
}

負荷分散(Load Balancing)

SMPシステムですべてのプロセッサに作業を均等に分配する。

  • Push移行: 定期的に過負荷プロセッサの作業を移動
  • Pull移行: アイドルプロセッサが忙しいプロセッサから作業を取得

Linux CFS(Completely Fair Scheduler)

Linux 2.6.23からデフォルトスケジューラとして使用されるCFSは、各プロセスに「公平な」CPU時間を分配する。

核心概念:仮想実行時間(vruntime)

[CFS動作原理]

各プロセスのvruntimeを追跡
vruntimeが最も小さいプロセスを次に実行

vruntime増加率 = 実際の実行時間 / 重み(nice値ベース)

nice値が低い(優先度が高い)プロセス:
  -> 重みが大きい -> vruntimeがゆっくり増加 -> より頻繁にスケジューリング

nice値が高い(優先度が低い)プロセス:
  -> 重みが小さい -> vruntimeが速く増加 -> あまりスケジューリングされない

Red-Blackツリー

CFSはRed-Blackツリーを使用してプロセスをvruntime基準で管理する。

[CFSのRed-Blackツリー]

         vruntimeでソート

              (P3: 50)
             /        \
        (P1: 30)    (P5: 70)
        /    \          \
   (P2: 20) (P4: 40)  (P6: 80)

最も左のノード(P2, vruntime=20)が次の実行対象

挿入/削除/検索:O(log N)保証
// CFSで次に実行するプロセスを選択(概念的コード)
struct task_struct *pick_next_task_fair(struct rq *rq) {
    struct cfs_rq *cfs_rq = &rq->cfs;
    struct rb_node *left;

    // Red-Blackツリーで最も左のノードを選択
    // = vruntimeが最も小さいプロセス
    left = rb_first_cached(&cfs_rq->tasks_timeline);
    if (!left)
        return NULL;

    struct sched_entity *se = rb_entry(left,
                                        struct sched_entity,
                                        run_node);
    return container_of(se, struct task_struct, se);
}

ターゲットレイテンシ(Target Latency)

CFSはターゲットレイテンシ内にすべての実行可能なプロセスが最低1回は実行されることを保証する。

例:ターゲットレイテンシ = 20ms、実行可能プロセス = 4
同一優先度なら各プロセスに5ms割り当て
プロセスが多すぎる場合は最小単位(min_granularity)を保証

アルゴリズム比較まとめ

[スケジューリングアルゴリズム比較]

アルゴリズム     プリエンプ  飢餓  利点              欠点
---------       --------  ----  ----              ----
FCFS            X         X     シンプル           コンボイ効果
SJF             X         O     最適待ち時間       バースト予測困難
SRTF            O         O     SJFより良い性能    バースト予測困難
優先度          両方       O     柔軟             飢餓(エイジング必要)
ラウンドロビン    O         X     公平、良い応答     q選択が重要
多段キュー       O         O     差別化サービス     キュー間移動不可
多段FBキュー     O         可能   最も柔軟         パラメータ設定複雑
CFS             O         X     公平なCPU分配     実装が複雑

まとめ

CPUスケジューリングはマルチプログラミングOSの核心である。FCFS、SJF、優先度、ラウンドロビンなど様々なアルゴリズムがあり、それぞれスループット、応答時間、公平性の間で異なるトレードオフを提供する。LinuxのCFSはRed-Blackツリーと仮想実行時間の概念を活用して、効率的かつ公平なスケジューリングを実現する。