FreeRTOS: スケジューリング

前回に続きFreeRTOSネタで、タスクスケジューリングについて眺めてみよう。FreeRTOSはシングルプロセッサコアが対象なので、一度に一つのタスクしか動かない。タスクはUNIXで言うところのスレッドに近く独自のコンテキスト(要はレジスタセットに格納された値)とスタックを持つ。UNIXプロセスのように独自のアドレス空間は持たない。というか、そもそもカーネルもタスクもすべてが特権モード&単一アドレス空間で動作する。

次に実行すべきタスクを選択するのがスケジューラ。実際にタスクを切り替えることをコンテキストスイッチと呼ぶ。また、コンテキストを含むタスクの管理データ構造体をTCB(Task Control Block)と呼ぶ。あとプリエンプションの概念は最初に知っておきたい。というか今時のOSでプリエンプションでないものはほぼ絶滅していると思うが、Mac OS X以前(Classic Mac OS)は協調マルチタスキングといって、タスクが自発的にCPUを手放してやらないとコンテキストスイッチしなかった。つまりOSが強制的にタスクから強制的にCPUを取り上げる(タスクをサスペンドする)ことをプリエンプションと呼ぶ。

まず、前回実行したタスクのコードを眺めてみる(コメントは省略)。main関数の最後で、自発的にスケジューラをキックして、カーネルのコードに突入する。タスクは基本的に無限ループで、終了するときは明示的にvTaskDelete()を呼ぶ。

void main_blinky( void )
{
const TickType_t xTimerPeriod = mainTIMER_SEND_FREQUENCY_MS;

       xQueue = xQueueCreate( mainQUEUE_LENGTH, sizeof( uint32_t ) );

       if( xQueue != NULL )
       {
               xTaskCreate( prvQueueReceiveTask, "Rx", configMINIMAL_STACK_SIZE, NULL, mainQUEUE_RECEIVE_TASK_PRIORITY, NULL );
               xTaskCreate( prvQueueSendTask, "TX", configMINIMAL_STACK_SIZE, NULL, mainQUEUE_SEND_TASK_PRIORITY, NULL 
);
               xTimer = xTimerCreate( "Timer", xTimerPeriod, pdTRUE, NULL, prvQueueSendTimerCallback );
               if( xTimer != NULL )
               {
                       xTimerStart( xTimer, 0 );
               }

               /* 自発的にスケジューラをキックする */
               vTaskStartScheduler();
       }

       /* 基本的にここには来ない */
       for( ;; );
}

スケジューリングポリシは優先度付きラウンドロビン。

各タスクは状態(下図の4つのいずれか)と優先度を持つ。Running状態になるのはシステムで一つだけであり、現在実行中のタスクよりも優先度の高いReadyタスクがあればコンテキストスイッチする。

画像1

少し実装に触れると、Running状態のタスクは一つだけなので、pxCurrentTCB変数に保持する。Readyは優先度毎にリストがあるpxReadyTasksLists[ configMAX_PRIORITIES ]。Suspended/Blockedもリスト。

コンテキストスイッチするタイミングは、次のいずれか。
・タイマ割込み処理
・API呼出しにより、RunningタスクがBlocked/Suspended状態に遷移
・API呼出しにより、Blocked/SuspendedタスクがReady状態に遷移

コンテキストスイッチはプロセッサ依存であり、最初のはportYIELD_FROM_ISR()関数、後者二つはportYIELD_WITHIN_API()関数としてそれぞれ定義される。

タイムスライスはクオンタムと呼ばれるが、LinuxでいうところのJiffiesで、タスク毎に割り当てられている実行時間。タイムスライシングが有効な場合は、タイムスライスが経過したら、コンテキストスイッチする。なお、タイムスライスを0にするとFCFS(First Come First Serve)になる。また、Ticklessにもできるようなので、後で調べてみよう。

プリエンプションを無効にして、協調(co-operative)スケジューリングも可能。この場合は、taskYIELD()を明示的に呼んで自らCPUを手離す。協調スケジューリングでは、実行中のタスクより優先度の高いタスクがReady状態になっても、プリエンプション(コンテキストスイッチ)は起きない。言い方を変えると、タイマ割り込み処理の延長で、プリエンプションを行わないのが協調スケジューリング。余談だが、SMP対応以前のLinuxカーネルでは、カーネル内ではプリエンプションしなかった。

この記事が気に入ったらサポートをしてみませんか?