はじめに (対象読者・この記事でわかること)

この記事は、組み込みシステム開発に携わるエンジニア、リアルタイムOSの動作原理に興味がある方、またはLinuxシステムプログラミングにおいてスレッドの優先度制御について学びたい方を対象としています。

この記事を読むことで、以下のことがわかるようになります。 - リアルタイムシステムにおけるスケジューリングの重要性とその基本的な考え方 - POSIXスレッド(pthread)ライブラリを使った優先度スケジューリングの仕組み - SCHED_FIFOスケジューリングポリシーの特性と、C言語での具体的な実装方法 - リアルタイムスケジューリングを扱う上での注意点と陥りやすい問題

高性能なシステムや応答性が求められるアプリケーション開発において、スレッドの優先度制御は非常に重要な要素です。本記事を通じて、その基礎をしっかりと身につけ、より堅牢なシステム構築に役立ててください。

前提知識

この記事を読み進める上で、以下の知識があるとスムーズです。 - C言語の基本的なプログラミングスキル - Linuxコマンドラインの基本的な操作 - OSにおけるプロセス、スレッドの概念に関する基礎知識

リアルタイムスケジューリングとは?SCHED_FIFOの概要

リアルタイムシステムとは、単に処理が速いだけでなく、「定められた時間内に処理を完了する」ことを保証するシステムです。特に、処理遅延がシステム全体の障害や重大な結果を招く場合を「ハードリアルタイム」、多少の遅延が許容される場合を「ソフトリアルタイム」と呼びます。このようなシステムでは、タスク(スレッド)の実行順序やタイミングを厳密に制御する「リアルタイムスケジューリング」が不可欠です。

Linuxカーネルは、複数のスケジューリングポリシーを提供しており、その中にリアルタイムスケジューリングポリシーが含まれます。代表的なリアルタイムスケジューリングポリシーとして、SCHED_FIFO(First-In, First-Out)とSCHED_RR(Round Robin)があります。

SCHED_FIFOポリシーの特性

SCHED_FIFOは、その名の通り「先入れ先出し」の原則に基づくスケジューリングポリシーです。 - 優先度ベース: 各スレッドには優先度(1〜99、高いほど高優先度)が割り当てられます。 - 非プリエンプティブ(同優先度内): 一度実行を開始したスレッドは、より高い優先度のスレッドが実行可能にならない限り、自らCPUを手放す(I/O待ちなど)か、終了するまで実行を継続します。同じ優先度の他のSCHED_FIFOスレッドは、現在のスレッドが終了するまで実行されません。 - プリエンプティブ(高優先度スレッド): 現在実行中のSCHED_FIFOスレッドよりも高い優先度を持つ別のSCHED_FIFOスレッドが実行可能になると、現在実行中のスレッドは即座に中断され(プリエンプション)、高優先度のスレッドが実行を開始します。 - FIFOキュー: 同じ優先度を持つ複数のSCHED_FIFOスレッドが実行可能になった場合、それらはキューに入れられ、登録された順序(First-In)で順番に実行されます。一つのスレッドがCPUを手放すと、次にキューの先頭にあるスレッドが実行されます。

通常のLinuxプロセスが使用するSCHED_OTHER(タイムシェアリング)ポリシーとは異なり、SCHED_FIFOはスレッドが一度CPUを獲得すると、より高優先度のスレッドが登場しない限り、設定されたタイムスライスに関わらず実行し続けるため、予測可能な実行タイミングが求められるリアルタイムアプリケーションに適しています。

pthreadとSCHED_FIFOによる優先度スケジューリングの実装

ここでは、C言語とpthreadライブラリを使って、複数のスレッドをSCHED_FIFOポリシーで異なる優先度で実行し、その動作を確認する具体的な手順とコード例を解説します。

ステップ1: スレッドの属性設定 (pthread_attr_t)

pthreadでスレッドを作成する際、その動作を細かく制御するためにpthread_attr_t構造体を使用します。この構造体には、スケジューリングポリシーや優先度、スタックサイズなどの属性が含まれます。

  1. 属性オブジェクトの初期化: pthread_attr_init(&attr); スレッド属性オブジェクトをデフォルト値で初期化します。

  2. スケジューリングポリシーの設定: pthread_attr_setschedpolicy(&attr, SCHED_FIFO); スレッドのスケジューリングポリシーをSCHED_FIFOに設定します。

  3. スケジューリングパラメータの設定(優先度): struct sched_param param; param.sched_priority = priority_value; pthread_attr_setschedparam(&attr, &param); スレッドの優先度を設定します。SCHED_FIFOの場合、sched_priorityは1から99の範囲で指定します(システムによって範囲は異なりますが、sched_get_priority_min()sched_get_priority_max()で確認できます)。高い数値ほど優先度が高くなります。

  4. スケジューリングパラメータの継承設定 (オプション): pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED); この設定は、作成されるスレッドが親スレッドのスケジューリング属性を継承するか、明示的に設定された属性を使用するかを決めます。PTHREAD_EXPLICIT_SCHEDを設定することで、pthread_attr_setschedpolicypthread_attr_setschedparamで設定した値が確実に適用されます。

ステップ2: スレッドの作成と実行

属性が設定できたら、それを使ってスレッドを作成します。

C
#include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <unistd.h> #include <sched.h> // sched_get_priority_min, sched_get_priority_max 用 #define NUM_THREADS 3 // 各スレッドが実行する関数 void *thread_func(void *arg) { long thread_id = (long)arg; struct sched_param param; int policy; // 現在のスレッドのスケジューリングポリシーと優先度を取得 pthread_getschedparam(pthread_self(), &policy, &param); printf("スレッド %ld: 開始 (ポリシー: %s, 優先度: %d)\n", thread_id, (policy == SCHED_FIFO ? "SCHED_FIFO" : (policy == SCHED_RR ? "SCHED_RR" : "SCHED_OTHER")), param.sched_priority); // 優先度が高いスレッドの実行が優先されることを示すループ for (int i = 0; i < 5; ++i) { printf("スレッド %ld: 実行中 (%d)\n", thread_id, i); // 短時間スリープで他のスレッドにCPUを譲る(デモンストレーションのため) // SCHED_FIFOの場合、これは他のスレッドにCPUを譲る唯一の方法に近い usleep(100000); // 100ms } printf("スレッド %ld: 終了\n", thread_id); return NULL; } int main() { pthread_t threads[NUM_THREADS]; pthread_attr_t attr; struct sched_param param; int ret; // 優先度の範囲を取得 int min_priority = sched_get_priority_min(SCHED_FIFO); int max_priority = sched_get_priority_max(SCHED_FIFO); printf("SCHED_FIFO 優先度範囲: %d (最小) - %d (最大)\n", min_priority, max_priority); if (min_priority == -1 || max_priority == -1) { perror("sched_get_priority_min/max"); return 1; } // スレッド属性オブジェクトを初期化 pthread_attr_init(&attr); // スレッドが明示的に設定されたスケジューリング属性を使用するように設定 pthread_attr_setinheritsched(&attr, PTHREAD_EXPLICIT_SCHED); // スケジューリングポリシーをSCHED_FIFOに設定 pthread_attr_setschedpolicy(&attr, SCHED_FIFO); // 異なる優先度でスレッドを作成 // 優先度は高いものから順に 90, 80, 70 とする int priorities[NUM_THREADS] = {90, 80, 70}; for (long i = 0; i < NUM_THREADS; ++i) { // 優先度が有効な範囲内にあるか確認 if (priorities[i] < min_priority || priorities[i] > max_priority) { fprintf(stderr, "エラー: 優先度 %d がSCHED_FIFOの範囲外です。\n", priorities[i]); pthread_attr_destroy(&attr); return 1; } param.sched_priority = priorities[i]; ret = pthread_attr_setschedparam(&attr, &param); if (ret != 0) { fprintf(stderr, "pthread_attr_setschedparam エラー: %d\n", ret); pthread_attr_destroy(&attr); return 1; } ret = pthread_create(&threads[i], &attr, thread_func, (void *)(i + 1)); if (ret != 0) { fprintf(stderr, "pthread_create エラー: %d\n", ret); pthread_attr_destroy(&attr); return 1; } } // スレッド属性オブジェクトを破棄 pthread_attr_destroy(&attr); // すべてのスレッドの終了を待機 for (int i = 0; i < NUM_THREADS; ++i) { pthread_join(threads[i], NULL); } printf("メインスレッド: 全スレッドが終了しました。\n"); return 0; }

コードのコンパイルと実行

上記のコードをrealtime_scheduling.cとして保存し、以下のコマンドでコンパイルします。pthreadライブラリをリンクするために-pthreadオプションを忘れずに指定してください。

Bash
gcc realtime_scheduling.c -o realtime_scheduling -pthread

実行する際は、リアルタイムスケジューリングポリシーを設定するには特権が必要なため、sudoコマンドを使用する必要があります。

Bash
sudo ./realtime_scheduling

実行結果の考察

実行結果を見ると、最も優先度の高いスレッド(この例ではスレッド1、優先度90)が最初に実行され、そのループが完了するまで他のスレッドは実行されないことがわかります。その後、次に優先度の高いスレッド(スレッド2、優先度80)が実行され、最後に優先度が最も低いスレッド(スレッド3、優先度70)が実行されます。usleepによって意図的にCPUを手放すため、その間に次の高優先度スレッドが実行されます。もしusleepがない場合、最も優先度の高いスレッドが完全に完了するまで、他のスレッドは全く実行されないでしょう。

この挙動は、SCHED_FIFOが「高優先度スレッドが実行可能になるとすぐにCPUを奪う」「一度実行を開始したスレッドは、より高優先度のスレッドが現れない限り、自らCPUを手放すまで実行を続ける」という特性を明確に示しています。

ハマった点やエラー解決

1. 権限不足によるエラー

pthread_attr_setschedparampthread_createが失敗し、errnoEPERM(Operation not permitted)になる場合があります。これは、非特権ユーザー(rootではないユーザー)がリアルタイムスケジューリングポリシーを設定しようとした場合に発生します。

解決策: - sudoで実行する: 最も簡単な方法は、実行ファイルをsudo ./your_programのようにroot権限で実行することです。 - ケーパビリティを付与する: 実行ファイルにCAP_SYS_NICEケーパビリティを付与することで、非rootユーザーでもリアルタイムスケジューリングポリシーを設定できるようになります。 bash sudo setcap cap_sys_nice+ep ./realtime_scheduling ./realtime_scheduling # sudoなしで実行可能になる これはセキュリティ上のリスクも伴うため、慎重に検討する必要があります。

2. 優先度の範囲外エラー

sched_param.sched_priorityに設定する値が、システムがサポートするSCHED_FIFOの優先度範囲外である場合、エラーになります。

解決策: - sched_get_priority_min(SCHED_FIFO)sched_get_priority_max(SCHED_FIFO)を呼び出して、現在のシステムで有効な優先度範囲を確認し、その範囲内の値を設定するようにしましょう。一般的には1〜99ですが、システムやカーネルの設定によって異なる場合があります。

3. 優先度逆転問題

これは実装コードに直接関係するエラーではありませんが、リアルタイムシステムで発生しうる重要な問題です。高優先度スレッドが、低優先度スレッドが保持しているリソース(ミューテックスなど)を待つためにブロックされ、結果として低優先度スレッドよりも後に実行されてしまう現象です。

解決策: - 優先度継承プロトコル (PTHREAD_PRIO_INHERIT) や優先度上限プロトコル (PTHREAD_PRIO_PROTECT) を使用してミューテックスを保護することで、この問題を緩和できます。これはpthread_mutexattr_tを使って設定します。

C
// 優先度継承ミューテックスの例(参考) pthread_mutexattr_t mutex_attr; pthread_mutex_t mutex; pthread_mutexattr_init(&mutex_attr); pthread_mutexattr_setprotocol(&mutex_attr, PTHREAD_PRIO_INHERIT); pthread_mutex_init(&mutex, &mutex_attr);

これは本記事の範囲を超えるため詳細な説明は省きますが、リアルタイムシステムを設計する際には常に考慮すべき重要な点です。

まとめ

本記事では、pthreadとSCHED_FIFOによるリアルタイム優先度スケジューリングについて解説しました。

  • SCHED_FIFOは、決められた時間内に処理を完了するリアルタイムシステムにおいて、予測可能なスレッド実行順序を保証する重要なスケジューリングポリシーです。
  • C言語とpthreadライブラリを使用することで、pthread_attr_tを介してスケジューリングポリシー(SCHED_FIFO)と優先度を設定し、スレッドを作成できます。
  • リアルタイムスケジューリングポリシーを使用する際には、権限不足優先度範囲の問題に注意が必要であり、sudoでの実行やケーパビリティの付与、優先度範囲の確認などの対策を講じる必要があります。

この記事を通して、リアルタイムスケジューリングの基本的な概念と、C言語での具体的な実装方法を理解し、より応答性の高いアプリケーション開発に役立つ知識を得られたことでしょう。

今後は、SCHED_RRポリシーとの比較、複数のスレッド間の同期(ミューテックス、条件変数)、さらには優先度逆転問題とその解決策(優先度継承/上限プロトコル)など、より発展的な内容についても学習を進めることで、リアルタイムシステムプログラミングのスキルをさらに深めることができます。

参考資料