カーネルの構造(★★★)

[組込み製品、リアルタイムOS、組込みドライバーの開発依頼は、フィールドデザインまでお気軽にお問い合わせください。]

リアルタイムOSのカーネルとは、一言でいうと、タスクをスケジューリングする部分のことです。CPUの処理を、優先順位に従って、実行可能状態にあるタスクの1つに切り替えることです(優先度ベース・プリエンプティブのスケジューリング)。



具体的にどのように実装しているか説明していきます。実装はどのようにでもできますが、一般的と思われるものを記載します。

OSが管理しているデータ
OSが管理しているものとして、優先順位ごとにREADY状態にあるタスクのキュー(READYキューという)があります(下図)。
新たにWAIT状態からREADY状態になったタスクや、RUN状態からREADY状態になったタスクは、キューの最後に追加されます。
優先順位の高いREADYキューにあるタスクから順番にRUN状態にして、実行していきます。

また、OSが各タスク用に管理しているものとして、タスクコントロールブロック(TCB)と言われているパラメータがあります(下記)。各タスクがこれらの値と専用のスタック領域を持っていることにより、1つのCPU上で並列動作ができるようになります。

  • タスクのID
  • タスク優先度
  • タスク状態(RUN, READY, WAIT)
  • 待ち要因(セマフォ、イベント等)
  • スタックポインタ値

上記では、タスク切り替え時に、CPUレジスタ値(CPUコンテキスト)は、スタックに積まれる実装としています。ARM系のOSなどは、割り込み時に自動でCPUレジスタ値をスタックに退避してくれるので、その方が簡単に実装できます。なお、CPUレジスタ値をタスクコントロールブロックに持つ実装も考えられます。


OSのスケジューリング動作
タスクを切り替えるタイミングは、割り込みが発生した時と、実行中のタスクのシステムコールが呼ばれた時です(それ以外の場合は、全タスクの状態が変化しないので、どうやっても切り替えられません)。
割り込みルーチンは、外部要因で呼ばれるため、OSとは独立に動作します。すべての割り込みルーチンの最後で、タスク切り替えを発生させています(現在、実行中のタスクより、優先度の高いREADY状態のタスクがない場合は、そのまま現在実行中のタスクに戻ります)。
システムコールでタスクを切り替える場合も、割り込みでタスクを切り替える場合も、行っていることは完全に同じで、CPUレジスタ値(CPUコンテキスト)を新しく動かすタスクのものに切り替えているだけです。

スケジューリングの動作は下記のようになります。
下記では、現在、タスクAが動作しているとして、なんらかの要因でタスクBに切り替わる動作を示しています。

  • ①CPUのレジスタ値をタスクAのスタックに退避
  • ②CPUのSP値をタスクAのTCBに保存
  • <<次に実行するタスクをREADYキューから選択する→タスクBに決定>>
  • ③タスクBのTCBのSP値を読み出してCPUのSPにセット
  • ④そのSP値を使って、タスクBのスタックからCPUのレジスタ値を復帰

複数のタスクが1つの要因(セマフォ等)で待たされている場合は、どのように動くかと言いますと、


OSのWAIT解除動作
通常は1つのタスクが1つの要因(セマフォ等)で待たされる場合が多いですが、排他制御でセマフォを利用している場合には、複数のタスクが同じ要因で待たされるケースがあります。
このようなケースで要因の待ちが解除された場合(セマフォがpostされた場合等)は、下記のように動作します(OS実装によっても異なりますが、FreeRTOSなどは下記のように動きます)。

  • 要因を待っている中で、最も高いプライオリティのタスクがWAIT状態からREADY状態になる(RUN状態になるかは後述)。
  • それ以外のタスクはWAIT状態のままになる。
  • 現在実行中のタスクの優先度が、要因を待っているどのタスクよりも高い場合は、そのまま現在実行中のタスクがRUN状態になる。そうでない場合は、READYキューの原則に従って、上記の要因を待っている中で最も高いプライオリティのタスクがRUN状態になる。

OSの実際のコード(FreeRTOS)
文章と図だけでは、イメージがわかないため、ここでは、ARM系のFreeRTOSのスケジューリングの実際のコードを説明します。
FreeRTOSでは、PendSVというハードウェアとは紐づいてない割り込みハンドラ(ソフト割り込み)を呼び出すことによって、スケジューリングを行っています。別に、割り込みハンドラを呼び出さずに、自分でコードを書いても良いのですが、すべてのCPUレジスタをスタックに入れるということは、その入れる作業をする時に使っているレジスタもスタックにいれなければなりません。これはかなり手間がかかります。しかし、ARM系のプロセッサは割り込み時にPC, LR, PSR, 汎用レジスタなどをスタックに積んでくれるように作られているため、それを使わない手はありません。
説明のために、コードは重要な部分だけを抜き出しています。なお、FreeRTOSでは、TCBの最初のメンバーは、スタックポインタ値となっています。
簡単のために、このスケジューリングでは、タスクAからタスクBに切り替わったとして説明します。


PendSV: 
    /*ここを呼ばれた時には、R0-R3,R12,LR,PC,PSRはすでにタスクAのスタックに退避済み*/

    /* アプリケーションでのSP(タスクAでのSP)をR0にコピー*/
    mrs r0, psp  

    /* 現在のTCB(タスクAのTCBになっている)の場所をR3に保存 */
    ldr    r3, pxCurrentTCBConst   
    /* タスクAのTCBの最初のメンバー(スタックポインタ値)の場所をR2にコピー */   
    ldr    r2, [r3]   

    /* 自動でスタックに積まれなかったレジスタをタスクAのスタックに積む */
    stmdb r0!, {r4-r11, r14}  

    /* タスクAでのSPをTCBのスタックポインタ値に保存 */
    str r0, [r2]  

    /* R3(現在のTCBの場所)を割り込みハンドラのスタックに積む
      (次のタスクスイッチ処理の後で使いたいから)*/              
    stmdb sp!, {r3}  

   /* 次に実行するタスクを選択する。
      ここを抜けた時には、pxCurrentTCBConstの場所に
      次に実行するタスクのTCB(タスクBのTCB)が入っている。*/
    bl vTaskSwitchContext   
    /* R3(現在のTCBの場所)を復帰する */                       
    ldmia sp!, {r3} 

    /* タスクBのTCBの最初のメンバー(スタックポインタ値)
       の場所をR1にコピー */
    ldr r1, [r3]  
    /* タスクBでのスタックポインタ値をR0にコピー */                     
    ldr r0, [r1]  

    /* タスクBのスタックからCPUレジスタ値を復帰
       (これら以外のレジスタは割り込みを抜ける時に自動で復帰する) */
    ldmia r0!, {r4-r11, r14}  

    /* タスクBでのスタックポインタ値をアプリケーションでのSPにコピー */
    msr psp, r0 

    /* 割り込みを抜ける */
    bx r14 


/* 現在のTCBを示す変数へのポインタ */
pxCurrentTCBConst: .word pxCurrentTCB