FreeRTOS タスクスケジューリングまとめ


カーネル

カーネルはOSの核となる部品です。OSはカーネルを用いて見かけ上複数の処理を同時に実行します。
OSでは同時に実行するそれぞれの処理をタスクといいます。複数のタスクを見かけ上同時に実行することをマルチタスキングといいます。

マルチタスキングを使うことによってソフトウェアの設計をシンプルにすることができます。

  • それぞれのタスクの処理内容を小さく、扱いやすくします。
  • 小さく分割されたタスクはテストしやすい上に、コードの再利用性が高まります。
  • 複雑なタイミングや処理順序の調整をOSに任せることができます。

FeeRTOSではタスクの生成にxTaskCreate()関数を用います。
xTaskCreate()関数を用いたタスク生成をするコードを以下に示します。

void vTask1( void *pvParameters )
{
    const char* const message = "buzz\r\n";
    for ( ;; ) {
        /* buzzを出力 */
        vPrintString( message );
    }
    /* タスクは基本的に無限ループで実装されなければいけません。 */
    /* タスクを終了させたいときはタスクの終了前にvTaskDeleteを読んでタスクを削除する必要があります。 */
    vTaskDelete(NULL); // NULLを渡すと現在実行中のタスクをタスクを削除します
}

void vTask2( void *pvParameters )
{
    const char* const message = "fizz\r\n";
    for ( ;; ) {
        /* fizzを出力 */
        vPrintString( message );
    }
}

int main( void )
{
    /**/
    xTaskCreate(
        vTask1, /* タスクとして実行する関数への関数ポインタ */
        "Task 1",/* タスク名です。デバッグのために設定されます。 */
        1000, /* タスクに割り当てられるスタックのサイズ */
        NULL, /* タスクに渡される引数へのポインタ */
        1, /* タスクの優先度。大きいほど優先的に実行されます。 */
        NULL ); /* 生成されたタスクへのハンドラです。タスクの優先度を変更する際に使われます。ハンドラが不要な場合はNULLを指定します。 */

    /* タスク2を生成します。 */
    xTaskCreate( vTask2, "Task 2", 1000, NULL, 1, NULL );

    /* タスクの実行を始めます。 */
    vTaskStartScheduler();

    /*main関数は最後まで到達してはいけません。そこで、無限ループを挿入します。 */
    for ( ;; );
}

スケジューラー

スケジューラはプログラムの実行中、タスクの処理の中断・再開を何度も繰り返します。
基本的にはスケジューラーがタスクを中断したり再開したりするのですが、タスク自身が自身を中断(ブロック)させることができます。タスク自身が自身の実行をブロックさせるには以下の方法があります。

  • ディレイ
    タスクを決められた時間の間ブロックします。
  • イベント
    特定の事象(割り込みや他のタスクからの通信)が発生するまで自身の処理をブロックします。
  • リソース取得 リソース(キューやミューテックス、セマフォなど。)が使えるようになるまでブロックします。

スケジューラはブロック中のタスクを再開される条件が満たされるまで(一定時間経過する、イベントが発生する、リソースが使えるようになる)タスクを実行しません。

ディレイにはvTaskDelay()または、vTaskDealyUntil()を使います。

void vTask1( void *pvParameters )
{
    const char* const message = "buzz\r\n";
    for ( ;; )
    {
        /* buzzを出力 */
        vPrintString( message );
        vTaskDealy(pdMS_TO_TICKS(250)); // 250msecスリープする
        // vTaskDealyの引数はスリープする割り込み回数
        // msecに換算するためにpdMS_TO_TICKSマクロを使う
    }

    vTaskDelete(NULL);
}

void vTask2( void *pvParameters )
{

    TickType_t xLastWakeTime;
    const char* const message = "fizz\r\n";
    for ( ;; )
    {
        xLastWakeTime = xTaskGetTickCount(); // 現在の時刻を取得します
        /* fizzを出力 */
        vPrintString( message );
        vTaskDelayUntil(&xLastWakeTime, pdMS_TO_TICKS(500)); // 500 msecスリープする
        // vTaskDelayは最後にvTaskDealyが呼ばれた時点からのスリープ時間を指定します。
        // vTaskDealyUntil()は第一引数+第二引数の時刻までスリープします。
        // 正確にタスクを一定周期で呼びたいときはvTaskDelayUntil()を使います。
    }
}

ディレイ以外のブロックについては別の記事で取り上げます。

スケジューリングポリシー

スケジューリングポリシーとは、スケジューラがいつどのタスクを実行するかを決定するためのアルゴリズムのことです。
FreeRTOSではFreeRTOSConfig.hで宣言される以下2つの変数がスケジューリングポリシーの選択に用いられます。

  1. configUSE_PREEMPTION
    preempt(英語で先取りする)。あるタスクが実行中にそのタスクよりも優先度の高いタスクが実行できる状態になった場合、優先度の低いタスクを中断して優先度の高いタスクを実行します。
    configUSE_PREEMPTIONが1のときはpreemptが有効になります。優先度が高いタスクが低いタスクよりも優先的に実行されます。

  2. configUSE_TIME_SLICING
    同じ優先度のタスクが2つあった場合、一定時間ごとに実行するタスクを切り替えます。これをタイムスライシングといいます。
    configUSE_TIME_SLICINGが1のときはタイムスライシングが有効になります。

configUSE_PREEMPTIONが1かつconfigUSE_TIME_SLICINGが1のときのタスクスケジューリングの例を示します。
この例では4つのタスクが現れます。

  1. task1
    実行されるすべてのタスクのうち最も優先度(20)の高いタスクです。
    処理の完了にタイムスライス2つ分を必要とします。時刻、t5で実行可能となります。

  2. task2
    task1よりも低い優先度(10)を持つタスクです。
    処理の完了にタイムスライス2つ分を必要とします。時刻、t2で実行可能となります。task2はtask3よりも先に生成されるタスクです。

  3. task3
    task2と同じ優先度(10)を持つタスクです。
    処理の完了にタイムスライス2つ分を必要とします。時刻、t2で実行可能となります。

  4. idle task
    実行できるタスクが無いときにFreeRTOSが挿入するタスクです。何もしません。

これらのタスクの実行状態を以下の図に示します。

  1. 初期状態t1ではどのタスクも実行できません。すべてのタスクが実行可能でないためです。なので、FreeRTOSはidleタスクを実行します。
  2. t2でtask2とtask3が実行可能状態になりました。task2とtask3は同じ優先度(10)です。タスクが生成された順にtask2が先に実行されます。
  3. t3でタイムスライシングによるタスクの切り替えが行われます。task2代わって同じ優先度(10)のtask3が実行されます。
  4. t4で再度タイムスライシングによるタスクの切り替えが行われます。task3に代わってtask2が実行されます。
  5. t5でtask1が実行可能になりました。task1は最も優先度(20)が高いのですぐに実行されます。task3は処理を中断されます。
  6. t7でtask1の実行が完了します。実行途中だったtask3に処理が戻ります。

このスケジューリングはもっともオーソドックスなスケジューリングです。多くの実装で用いられています。難点はタスクの切り替えが頻繁に行われるので、オーバヘッドが多くなる点です。

次にconfigUSE_PREEMPTIONが1かつconfigUSE_TIME_SLICINGが0のときのタスクスケジューリングの例を示します。各タスクの優先度は先の例と同じです。

  1. 初期状態t1ではどのタスクも実行できません。なので、FreeRTOSはidleタスクを実行します。
  2. t2でtask2とtask3が実行可能状態になりました。task2とtask3は同じ優先度(10)です。先程と同様に生成順に基づいて、task2が実行されます。
  3. タイムスライシングは無効となっているのでtask2が完了するt4からtask3の実行が始まります。
  4. t5でtask1が実行可能になりました。task1は最も優先度(20)が高いのですぐに実行されます。task3は処理を中断されます。
  5. t7でtask1の実行が完了します。実行途中だったtask3に処理が戻ります。

configUSE_TIME_SLICINGが0の時タイムスライシングは無効になります。タイムスライシング無効になるとタスクの切り替え回数が減ります。タスクの切り替えにはCPUの処理を消費するので、タイムスライシングを無効にすることで効率よくタスクを実行できるようになります。反面、タスクへの処理時間の分配が不均一になるというデメリットもあります。タイムスライシングを無効にするときはこのメリットとデメリットを考慮して決めましょう。

次にconfigUSE_PREEMPTIONが0のときのタスクスケジューリングの例を示します。configUSE_PREEMPTIONが0のときはタスクがブロック状態になるまでタスクの切り替えは行われません。このようなタスクスケジューリングは協調的マルチタスクと呼ばれます。なお、configUSE_TIME_SLICINGの値は無視されます。

先程と同じタスクを協調的マルチタスクでスケジューリングした時の実行を以下の図に示します。

タスクの切り替えは全く行われません。各、タスクが準備できてから順に完了するまで実行されます。
RTOSを使うメリットである応答性・同時実行が薄まってしまうので、協調的マルチタスクはあまり使われません。
一方で、各タスクが同時にアクセスできないリソース(メモリやシリアル通信など)を共有する場合は、基本的に複数のタスクが同時に実行されないので、排他制御を行う必要がありません。各タスクの処理時間が十分に短いときに採用されることがあります。

参考文献