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_handler
はknl_timer_queue
に登録されている(TMEB*)event
をチェックし、もしも現在時刻
- イベント時刻 < 0x7FFFFFFF (ABSTIM_DIFF_MIN)の場合、そのイベントをキューから削除してコールバックを実行する。時刻の取り扱い方はまだよくわからない。
knl_timer_queue
にevent
を登録するのは、待ちイベントが発生したときだ。knl_timer_insert
はknt_current_time
+ tmout +
TIMER_PERIOD
を計算してknl_enqueue_tmeb(event)
を実行する。例えばknl_make_wait
がknl_timer_insert
を呼び出している。
相対時間呼び出しモードもあるけど、今はいいか。
EventFlag
ReadyQueueとWaitTimerが分かれば、あとは大体似たパターンになっている。
wai_flg
はknl_gcb_make_wait(flgcb,
tmout)
を呼ぶ。knl_gcb_make_wait
はknl_make_wait
を呼び、knl_tskque
をflgcb->wait_queue
に登録する。
細かい所は終えていないけど、大体そんな感じ。