RTOSコード読み(T-Kernel)

githubにT-kernelのソースコードが公開されているので、少し勉強してみた。

カーネルの重要な変数とか

task.cに定義されているいくつかの変数が肝。

// disable 
INT     knl_dispatch_disabled;
// task in execution
TCB     *knl_ctxtsk; 
// next task
TCB     *knl_schedtsk;
// tasks in ready state
RDYQUE  knl_ready_queue;
// task control block
TCB     knl_tcb_table[MAX_TSKID];
// free task
QUEUE   knl_free_tcb;

全ての基本はQUEUEという、双方向リストを使ったキュー構造。実体はknl_tcb_tableにある。

TCBにタスクを動かすための重要なものが詰まっている。

struct task_control_block {
    // task properties
    QUEUE   tskque;
    ...
    // properties for kernel lock
    BOOL    klockwait:1;
    BOOL    klocked:1;
    ...
    // properties for wait
    CONST WSPEC *wspec;
    ...
    // stack pointer
    void    *isstack;
    ...
    MTXCB   *mtxlist;
};

プログラムは動くのは得意だが、止まるのは苦手だ。イベントフラグやミューテックスなど、タスクが必要な資源がないときはタスクを止めて、もしも資源が使えるようになったときはタスクを動かしてあげればよい。そのあたりをどのように実現しているかはイマイチ分かっていない。

ディスパッチャ

あるタスクを止めたときに別のタスクを開始する機能。タスクはそれぞれ独立したスタックポインタを持っているから、プログラムカウンタなどのレジスタをスタックに保存しておけば、あとで再実行できる。

T-kernelの場合、knl_dispatchという関数がある。これを呼び出すと

    *(UW*)SCB_ICSR = ICSR_PENDSVSET;

が実行され、PENDSV割込みが実行され、knl_dispatch_entryが呼び出される。ARMv7-Mで割り込みが発生するとそのときのr0-r3, r12, lr(r14), pc(r15), xpsrなどがスタックに勝手に積まれる。だから、ディスパッチャはpush {r4-r11}すればいい。

push	{r4-r11}
push	{lr}

実装を見るとpush {lr}もしているけど、なぜかはよく分からない。その方がディスパッチャから抜けるときに便利だからかなと思ってる。

ldr r0, =knl_dispatch_disabled  // r0 = &knl_dispatch_disabled
ldr r1, =1                      // r1 = 1
str r1, [r0]                    // *r0 = r1 = 1

ディスパッチを無効にする処理。こういうこともきちんと考えないといけないのが大変だと思う。

ldr r0, =knl_ctxtsk         // r0 = &knl_ctxtsk
ldr r1, [r0]                // r1 = *r0 (== knl_ctxtsk)

現在実行中のタスクへのポインタを取得する操作。

ldr	r2, [r1, #TCB_tskatr]

こんな感じで構造体の中の値にもアクセスできる。

str	sp, [r1, #TCB_tskctxb + CTXB_ssp]	// Save 'ssp' to TCB

現在のスタックポインタをTCB内のsspに保存している。

ldr	r2, =INTPRI_VAL(INTPRI_MAX_EXTINT_PRI)  // 1
msr	basepri, r2

msr命令はmove to special registerで、basepriってのは割り込み優先度を設定するレジスタ。値が0ならマスクなし、値がNなら優先度N以下の割り込みを禁止する(はず)。上の例は割り込みを禁止している。

ldr r5, =knl_schedtsk           // r5 = &knl_schedtsk
ldr r8, [r5]                    // r8 = *r5     r8 = knl_schedtsk
str r8, [r0]                    // *r0 = r8     *(&knl_ctxtsk) = knl_schedtsk
ldr sp, [r8, #TCB_tskctxb + CTXB_ssp] // restore ssp

色々なケアがあるからわかりにくいけど、本質はこの最後の1行だったりする。

pop	{lr}
pop	{r4-r11}

元に戻してあげるときはこう。r0-r3などは割り込みから抜けるときに復元される。

ReadyQueue

作成したタスクは、待ち状態にならない限りREADY状態になっている。

struct ready_queue {
    ...
    QUEUE tskque[NUM_TSKPRI];
    ...
};

優先度毎のキューが用意されている。

bool knl_ready_queue_insert(RDYQUE* rq, TCB* tcb)
{
    ...
    QueInsert(&tcb->tskque, &rq->tskque[pri]);
    ...
}

ReadyQueueとtcb->tskqueを繋げている。tcb->tskqueは初めknl_free_tcbに繋がっていて、WaitQueueに繋げられることもある。少し名前が紛らわしい気もするけど、どうやらキュー挿入用のノードみたいだ。cre_tskしたときにknl_free_tcbから削除されて、sta_tskするとknl_ready_queue_insertされる。

Mutex

ミュー手クスはどのように排他制御しているのだろうか。

loc_mtxしたとき、mtxcb->mtxtskを確認する。これがNULLなら確保できる。

if ( mtxtsk == NULL ) {
    mtxcb->mtxtsk = knl_ctxtsk;
    mtxcb->mtxlist = knl_ctxtsk->mtxlist;
    knl_ctxtsk->mtxlist = mtxcb;

mtxlistというのは単方向のリストっぽい。

確保できないときは少し待つ必要がある。

knl_make_wait(tmout, mtxcb->mtxatr);
if ( mtxatr == TA_TFIFO ) {
    QueInsert(&knl_ctxtsk->tskque, &mtxcb->wait_queue);
} else {
    knl_queue_insert_tpri(knl_ctxtsk, &mtxcb->wait_queue);
}

多くのOSコールはクリティカルセクション(CS)として保護されている。CS開始時は割込を禁止して、終了時は割り込み許可している・・・だけでなくディスパッチすることもある。なんでかな。

Wait & Timer

タスクを待ち状態にするときはReadyQueueとよく似たWaitQueueにタスクを登録する。

ER knl_timer_startup(void)
{
    ...
    QueInit(&knl_timer_queue);
	knl_start_hw_timer();
    ...
}

knl_timer_queueはタイマー待ちイベントを登録するキューで、knl_start_hw_timerはハードウェアタイマーを起動する。

    out_w(SYST_CSR, 0x00000006);
    out_w(SYST_RVR, TIMER_PERIOD * TMCLK_KHz - 1);
    out_w(SYST_CSR, 0x00000007);

Systick割込みはknl_systim_inthdrを実行する。これはknl_timer_handlerを呼ぶ。

knl_timer_handlerknl_timer_queueに登録されている(TMEB*)eventをチェックし、もしも現在時刻 - イベント時刻 < 0x7FFFFFFF (ABSTIM_DIFF_MIN)の場合、そのイベントをキューから削除してコールバックを実行する。時刻の取り扱い方はまだよくわからない。

knl_timer_queueeventを登録するのは、待ちイベントが発生したときだ。knl_timer_insertknt_current_time + tmout + TIMER_PERIODを計算してknl_enqueue_tmeb(event)を実行する。例えばknl_make_waitknl_timer_insertを呼び出している。

相対時間呼び出しモードもあるけど、今はいいか。

EventFlag

ReadyQueueとWaitTimerが分かれば、あとは大体似たパターンになっている。

wai_flgknl_gcb_make_wait(flgcb, tmout)を呼ぶ。knl_gcb_make_waitknl_make_waitを呼び、knl_tskqueflgcb->wait_queueに登録する。

細かい所は終えていないけど、大体そんな感じ。