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タスクがあればコンテキストスイッチする。
少し実装に触れると、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カーネルでは、カーネル内ではプリエンプションしなかった。