マイOSに継続を使うことのできるスレッドを実装しはじめました。今日のとこ
ろはスタックの切り離し、つけ直しができるところまで。スレッドの概念はも
う随分慣れ親しんできた。しかし、ここに継続が入るとなるとプログラミング
する段階でスタックを全て開放できる条件になるように、しむけないといけな
い。このあたりがこの方式が市民権を得なかった理由だと思う。ここまでやる
なら言語に組みこんだ方がいい。でも言語の縛りだとカーネルに使うには縛り
がきつ過ぎるという微妙なとこだ。
当初、論文にあったインターフェースから初めたのだけど、マイOSとは作りが 違うので、ちょっと見た目違った実装になった。マイOSはUnix,Machとはスケ ジューリングポリシーが違う(クロック割り込みでスケジューリングをするとい う方針ではなく、割込みの終了でスケジューリングされる。なので、割り込み が起きなければ絶対に横取りされることがない)し、ARMポートでは割込みスタッ クはper-processorにとってある。これには使われなくなったブートストラップ スタックを転用している。その割にそれを使うのは入口だけで、割込みハンド ラは割り込まれたスレッドのスタックを使うようになっている。
もうちょっと仕様まとめておけばよかった。夏前に書いたのに完全に忘れた。
当初、論文にあったインターフェースから初めたのだけど、マイOSとは作りが 違うので、ちょっと見た目違った実装になった。マイOSはUnix,Machとはスケ ジューリングポリシーが違う(クロック割り込みでスケジューリングをするとい う方針ではなく、割込みの終了でスケジューリングされる。なので、割り込み が起きなければ絶対に横取りされることがない)し、ARMポートでは割込みスタッ クはper-processorにとってある。これには使われなくなったブートストラップ スタックを転用している。その割にそれを使うのは入口だけで、割込みハンド ラは割り込まれたスレッドのスタックを使うようになっている。
もうちょっと仕様まとめておけばよかった。夏前に書いたのに完全に忘れた。
今迄スレッド制御領域の後に続く領域だったスタックを別体にするために
#define THREAD_STACK_MAGIC 0xaa
struct thread_stack
{
uint8_t *bottom; //スタックの一番最初
uint8_t *top; //スタックの上限
size_t size; //スタックサイズ
size_t remain; //スタックの残り。
// これはデバッグの時に、まず最初にスタックにマジック
// (THREAD_STACK_MAGIC)を書き込んでおいて、割り込み毎にそのマジックがど
// れだけ残っているかを数えた結果。
bool active; //このスタックがどこかのスレッドが使っている。
SIMPLEQ_ENTRY (thread_stack) ts_link;
};
取り外し可能なスタックはプールにしておきます。
SIMPLEQ_HEAD (thread_stack_pool, thread_stack);
extern struct thread_stack_pool thread_stack_pool;
__BEGIN_DECLS
プールの初期化。
void thread_stack_init (void);
任意の場所のスタックを、このスタック形式にする。
void thread_stack_setup (struct thread_stack *, uint8_t *, size_t);
スタック形式になったのをプールに入れる。
void thread_stack_load (struct thread_stack *);
取り外しが不可能な形式の場合、そのスタック領域をこの方式に変換
void thread_stack_dedicate (uint8_t *, size_t);
取り外し可能なスレッド同士の間でスタックを付け替え。
void thread_stack_handoff (thread_t, thread_t);
プールからスタックを割当て。
struct thread_stack *thread_stack_allocate (void);
スタックをプールに返還。
void thread_stack_deallocate (struct thread_stack *);
スレッドにスタックを新たにつける。
void md_thread_stack_attach (thread_t, struct thread_stack *, continuation_func_t);
スレッドからスタックを取り外す。
void md_thread_stack_detach (thread_t);
__END_DECLS
// 取り外し可能なスタックのプール。
struct thread_stack_pool thread_stack_pool;
int thread_stack_loaded;
void
thread_stack_init ()
{
//プールの初期化
SIMPLEQ_INIT (&thread_stack_pool);
}
void
thread_stack_setup (struct thread_stack *ts, uint8_t *top, size_t size)
{
このスレッドシステム用の構造にして、スタックの内容を、残りスタックを
計れるようにマジックで埋めて、さらにそうでない状況でも最悪スタックが
溢れた時をみつけるためにカナリヤを入れておきます。
ts->top = top;
ts->size = size;
// Fill with magic number to estimate stack usage.
memset (ts->top, THREAD_STACK_MAGIC, size);
#if BYTE_ORDER == BIG_ENDIAN
ts->bottom = ts->top + ts->size - 4;
*(uint32_t *)(ts->top + 4) = (uint32_t)0xac1dcafe; // canary
#else
ts->bottom = ts->top + ts->size;
*(uint32_t *)ts->top = (uint32_t)0xac1dcafe; // canary
#endif
LPRINTF ("%x-%x (%x)\n", ts->bottom, ts->top, ts->size);
}
void
thread_stack_load (struct thread_stack *ts)
{
cpu_status_t s = intr_suspend ();
プールに入れるだけ。
thread_stack_loaded++;
ts->active = FALSE;
SIMPLEQ_INSERT_HEAD (&thread_stack_pool, ts, ts_link);
intr_resume (s);
}
void
thread_stack_dedicate (uint8_t *thread_area,
size_t sz /* don't include thread_control area*/)
{
これは今迄の形式のスレッドの制御領域に続いてスタックが続く形式をサポート
するため。今迄スタック上限だったところにスタック制御領域をとります。
uint8_t *p = thread_area;
struct thread_control *tc = (struct thread_control *)p;
p += sizeof (struct thread_control);
struct thread_stack *ts = (struct thread_stack *)p;
p += sizeof (struct thread_stack);
thread_stack_setup (ts, p, sz - sizeof (struct thread_stack));
tc->tc_stack = ts;
}
struct thread_stack *
thread_stack_allocate ()
{
struct thread_stack *ts = NULL;
cpu_status_t s = intr_suspend ();
空いてるスタックを探して返します。デバッグにフックしたいのでリストは
崩さずにおいてます。
SIMPLEQ_FOREACH (ts, &thread_stack_pool, ts_link)
{
if (!ts->active)
{
ts->active = TRUE;
break;
}
}
assert (ts);
LPRINTF ("%x\n", ts->bottom);
intr_resume (s);
return ts;
}
void
thread_stack_deallocate (struct thread_stack *ts0)
{
struct thread_stack *ts = NULL;
cpu_status_t s = intr_suspend ();
これもデバッグのためにリストを残してるので冗長です。
SIMPLEQ_FOREACH (ts, &thread_stack_pool, ts_link)
{
if (ts == ts0)
{
assert (ts->active);
ts->active = FALSE;
break;
}
}
assert (ts);
LPRINTF ("%x\n", ts->bottom);
intr_resume (s);
}
void
thread_stack_handoff (thread_t old_thread, thread_t new_thread)
{
スイッチする両者のスレッドがスタックを放棄できるのであれば、それらで
スタックの領域だけ渡します。
// We assume current_thread can detach stack.
new_thread->tc_stack = old_thread->tc_stack;
同じスレッドでスイッチすることもあるので、その時を気をつけます。
if (new_thread != old_thread)
old_thread->tc_stack = NULL;
}
ここからは機種依存部分。これはARM
これは、継続の関数が'return'してしまった間違いを捕獲するための埋め草。
void
md_thread_machdep_noreturn_assert ()
{
printf ("*** noreturn function return from 0x%x. ***\n",
__builtin_return_address (0));
while (/*CONSTCOND*/1)
;
// NOTREACHED
}
thread_t
md_thread_create (uint8_t *thread_area, size_t stack_size, const char *name,
void (*start)(uint32_t), uint32_t arg)
{
cpu_status_t s = intr_suspend ();
struct thread_control *tc = (struct thread_control *)thread_area;
thread_setup (tc, name);
if (stack_size)
{
スタックサイズが指定されているというのは、今迄の形式なので、このスタッ
クをこのスレッド専用のスタックとして登録します。
thread_stack_dedicate (thread_area, stack_size);
tc->regs.sp =(addr_t)tc->tc_stack->bottom;
stack_size = tc->tc_stack->size;
}
else
{
スタックサイズが指定されていなければ、スタックプールを利用する、継続ス
レッドなので、このスレッドのスタックはこのスレッドに制御が移る時にその
場で割当てられます。
tc->regs.sp = NULL; // will be allocated when thread is scheduled.
}
スレッドの一番最初というのは、それが呼び出し元に戻ることはないという意
味で継続。もし戻るようなバグを発見するために、戻った先をつけておきます。
tc->regs.lr = (addr_t)md_thread_machdep_noreturn_assert;
スレッド本体の関数。
tc->regs.pc = (addr_t)start;
マイOSでは一つだけ引数を渡せるようにしています。でも、今迄有効に使われた
ことはない。
tc->regs.a1 = arg;
tc->regs.psr = PSR_MSYS | PSR_I | PSR_F;// System mode. Interrupt disabled.
LPRINTF ("[%d]:%s pc=%A sp=%x stack=%dbyte\n",
tc->id, tc->name, start, tc->regs.sp, stack_size);
intr_resume (s);
return tc;
}
void
md_thread_stack_attach (thread_t tc, struct thread_stack *ts,
continuation_func_t cont)
{
スタックをスレッドに割当てます。この引数のcontはNULLでありえる。
tc->tc_stack = ts;
tc->continuation = cont;
md_thread_continuation_setup (tc);
LPRINTF ("[%d] %A-%A\n", tc->id, tc->tc_stack->bottom, tc->tc_stack->top);
}
void
md_thread_stack_detach (thread_t tc)
{
スレッドからスタックを取り外します。
// Return stack to pool
thread_stack_deallocate (tc->tc_stack);
tc->regs.sp = 0;
tc->tc_stack = NULL;
}
void
md_thread_continuation_setup (thread_t tc)
{
スレッドスタックと、スレッドの設定を合わせます。継続があるならば、この
スタックを使った一番最初のコンテキストスイッチは、継続になる。そうでな
ければ、あらかじめこのスレッドの作成時に設定した関数に継続する。これは
md_thread_cerateで、スタックなしでスレッドを作ることを可能にしているの
で、継続はなくて、スタックもないという状況で呼ばれるから。
tc->regs.sp = (addr_t)tc->tc_stack->bottom;
tc->regs.lr = (addr_t)md_thread_machdep_noreturn_assert;
if (tc->continuation)
tc->regs.pc = (addr_t)tc->continuation;
}
void
md_thread_continuation_call (continuation_func_t cont)
{
現在のスレッドで継続を呼びます。継続はスタックによる記憶が必要ないとい
う決まりなので、スタックを巻戻してジャンプします。
// Rewind stack and call continuation.
__asm volatile ("mov sp, %0; mov pc, %1"::
"r"(current_thread->tc_stack->bottom), "r"(cont));
// NOTREACHED
}
これに共ないスレッドシステムの初期化も変更。これを呼ぶ前にスレッドプール
を用意しておくこととした。
void
thread_system_init (thread_t tc)
{
int i;
cpu_status_t s = intr_suspend ();
既にスタックがロードできていることを前提。これはこのルートのスレッドの
スタックを放棄することができるようにするため。
assert (thread_stack_loaded);
// Ready queue.
for (i = 0; i < THREAD_PRIORITY_MAX; i++)
SIMPLEQ_INIT (thread_ready_queue + i);
// Thread list.
SLIST_INIT (&thread_list);
// Initialize myself.
thread_setup (tc, "root");
ここでスタックプールからスタックを持ってきます。継続はなし。なのでこの
スレッドの動く先は決まってない。これはこのルートが特別で、一番最初のス
レッドは直接場所を指定して制御が移行するのによっている。それはブートス
トラップの状態はスレッドではないので元の状態を保存するということができ
ないので、最初は継続からしか入ることができない。
// Allocate stack.
md_thread_stack_attach (tc, thread_stack_allocate (), NULL);
current_thread = tc;
current_thread->state = THR_RUN;
SIMPLEQ_INSERT_HEAD (__ready_queue (current_thread), current_thread, tc_link);
intr_resume (s);
}
ここで実際のコンテキストスイッチ。マイOSではdo_context_switchが入口とな
る。これはそれが呼ばれた時のレジスタ状態をスイッチフレームに保存して、
thread_context_switchで、次に走るスレッドが決まったところで、その状態復
帰して戻る。ここで、新しくこれに継続を渡せるように変更した。(r0をそのま
まthread_context_switchに渡す。)ここでdo_thread_switchは明示的なサブルー
チンコールでしか呼ばれないので、呼出し元退避(caller saved)は保存する必
要がない。
/* void do_thread_switch (continuation_t) */
/* Assume already interrupt disabled */
/* Called from thread context */
.long current_thread
FUNC (do_thread_switch)
// save context.
mrs r1, cpsr
ldr r2, [pc, #-16] // r0 = current_thread
ldr r2, [r2] // r0 = *current_thread
str r1, [r2], #20 // *r0 = r1, r0 += skip caller saved.
stmia r2!, {r4-r15} // save callee saved all.
str lr, [r2, #-4] // overwrite PC to LR. *(r0 -4) = lr
bl thread_context_switch
ldr r0, [pc, #16]
ldr r0, [r0]
ldr r1, [r0], #4 // r1 = *r0++;
msr cpsr_cf,r1 // restore all CPSR
ldmia r0, {r0-r15}
bl md_thread_machdep_noreturn_assert // for debug.
.long current_thread
継続を共なうことのできるブロックはこう実装した。
int
thread_block (continuation_func_t cont)
{
cpu_status_t s = intr_suspend ();
thread_t old_thread;
bool block;
old_thread = current_thread;
block = --old_thread->wakeup_request < 0;
assert (old_thread->state == THR_RUN);
if (block)
{
// Remove old_thread from run queue.
assert (SIMPLEQ_FIRST (__ready_queue (old_thread)) == old_thread);
SIMPLEQ_REMOVE_HEAD (__ready_queue (old_thread), tc_link);
old_thread->state = THR_WAIT;
}
else
{
ブロックしない場合、継続がなければそのままリターンすればいいのだけど、
継続を指定された場合はそこにジャンプします。この場合、スタックは完全に
巻き戻されます。
// If no need to block, directly call continuation.
intr_resume (s);
if (cont)
{
md_thread_continuation_call (cont);
// NOTREACHED
}
return E_OK;
}
ブロックする場合は継続の情報とともにブロックします。
do_thread_switch (cont);
intr_resume (s);
return E_OK;
}
do_thread_switchでは、呼び出したスレッドのコンテキストをスイッチフレー
ムに保存した後でthread_context_switchを呼び、その返り値のスレッドのコン
テキストを復帰する。
int
thread_context_switch (continuation_func_t cont)
{
thread_t old_thread, new_thread;
if (cont)
{
新しくこの関数の引数に継続が増えた。
LPRINTF ("CONTINUATION=>%A\n", cont);
}
継続があるなら、それを次にこのスレッドが呼び出される時のために登録して
おきます。
old_thread = current_thread;
if (cont)
old_thread->continuation = cont;
次に走るスレッドを探します。もし何もない場合、thread_selectの中で割り込
みを開けてbusy loopします。割り込みが入ってきて、それでもまだ走るべきス
レッドがない場合は、thread_select()は0で戻るので、もう一度busy loopに戻
ります。
if ((new_thread = thread_select ()) == 0)
return 0; // No thread is available. spin again.
ここまで来たら、確実に次に走るスレッドがあるということ。
// Thread is changed. update thread status.
if (old_thread != new_thread && old_thread->state == THR_RUN)
{
DPRINTF ("%d preempted by %d\n", old_thread->id, new_thread->id);
old_thread->state = THR_READY;
}
// It is possible that new_thread has no stack. currently use
// old_thread's stack
// If new_thread has continuation, that thread don't have dedicated stack.
前のスレッドに継続が定義されていたら、それはスタックを放棄できるということ。
if (old_thread->continuation) // old_thread has discardable stack,
{
// handoff stack to new_thread
次のスレッドに継続が定義されていたら、前のスレッドのスタックを付け替えます。
if (new_thread->continuation)
{
LPRINTF ("STACK HANDOFF (%s->%s)\n", old_thread->name,
new_thread->name);
thread_stack_handoff (old_thread, new_thread);
// Now current_thread == new_thread;
ここで、このスレッドのリターンアドレスを継続に変更します。
md_thread_continuation_setup (new_thread);
}
else
{
前のスレッドのスタックを受渡し先がないのでプールに戻します。
LPRINTF ("DISCARD STACK (%s)\n", old_thread->name);
// discard it for the other thread.
md_thread_stack_detach (old_thread);
}
}
// If new_thread has no stack, newly allocate. thraed is resumed
// from continuation in such a thread.
この状況は、スレッド一番最初の時にあり得る。スタックも継続もなくて、
スタート位置だけ指定されているという状況。
if (!new_thread->tc_stack)
{
LPRINTF ("ALLOCATE STACK (%d:%s)\n", new_thread->id, new_thread->name);
md_thread_stack_attach (new_thread, thread_stack_allocate (),
new_thread->continuation);
}
// Now change thread.
current_thread = new_thread;
current_thread->state = THR_RUN;
#ifdef DEBUG
thread_debug_state_check ();
#endif
// md_thread_debug_reg (¤t_thread->regs);
DPRINTF ("switch %d->%d: sp=%x pc=%x\n", old_thread->id, new_thread->id,
new_thread->regs.sp, new_thread->regs.pc);
return 0;
}
ユーザ側は
まずスタックプールを少なくとも一つ以上用意して、
// Prepare stack pool.
thread_stack_init ();
int i;
for (i = 0; i < 2; i++)
{
thread_stack_setup (stack_pool + i, stack_pool_area[i],
sizeof (stack_pool_area[0]));
thread_stack_load (stack_pool + i);
}
最初のスレッドを用意。
thread_system_init (&root_tc);
// Switch to buffered console.
console_init (boot_flag & BUFFERED_CONSOLE_ENABLE);
shell_init ();
// Jump to main with changing stack to thread local storage.
最初のスレッドは継続から入ります。
md_thread_continuation_call ((continuation_func_t)board_main);
ユーザ側でこのスタック放棄を使えるようにするには、
continuation_func app0_main __attribute__((noreturn));
void
app0_thread (uint32_t arg)
{
rtc_start (VIC_IRQ);
rtc_counter_incr (SEC, TRUE);
iprintf ("%s: arg=%x\n", __FUNCTION__, arg);
#if 0
これは今迄の例。thread_blockの中でスタックを保持している。
while (/*CONSTCOND*/1)
{
thread_block (NULL);
// thread_block (NULL);
iprintf ("wakeup!\n");
led_blink ();
}
#else
これはスタックを放棄する例。
app0_main ();
#endif
// NOTREACHED
}
void
app0_main ()
{
led_blink ();
このthread_blockでスタックが放棄されて、次にスケジューリングされた時には
完全にスタックになにもない状態で、app0_mainから走ることになる。
thread_block (app0_main);
// NOTREACHED
while (/*CONSTCOND*/1)
;
}
void
rtc_intr_counter_incr ()
{
thread_wakeup (app_th);
}
mon> l
Send S-record file.
~>Local file name? a.mot
5772 lines transferred in 5 seconds
!
Read 135589 byte. success
Start address: 0x40003f54
stack_start: 0x4000f800
RAM data: 0x4000adfc-0x4000b5fc 2048byte
bss: 0x4000b5fc-0x4000e6f4 12536byte
current stack=0x4000f7f8
Clock: MAIN, PLL connected, PLL MSEL:11 NSEL:0, CCLKCFG 3
Clock: MAIN, PLL connected, PLL MSEL:11 NSEL:0, CCLKCFG 3
sysclock:288000000Hz cpuclock:72000000Hz
PCLK0: 0, PCLK1: 0 PCONP: 4280ffe
calibrated. delay_parm=74
bss RAM check passed.
port 0 pin 26 func 0 => r=e002c044 shift=20
thread_stack_setup: 4000c958-4000b958 (1000)
thread_stack_setup: 4000d958-4000c958 (1000)
thread_stack_allocate: 4000d958
md_thread_stack_attach: [1] 4000d958-4000c958
thread_stack_setup: 4000e61c-4000e234 (3e8)
md_thread_create: [2]:uart recv pc=400011d4 sp=4000e61c stack=1000byte
thread_start: [2]
thread_stack_setup: 4000e158-4000dd70 (3e8)
md_thread_create: [3]:uart send pc=40001170 sp=4000e158 stack=1000byte
thread_start: [3]
md_thread_create: [4]:app0 pc=400007a8 sp=0 stack=0byte
thread_start: [4]
thread_context_switch: ALLOCATE STACK (4:app0)
thread_stack_allocate: 4000c958
md_thread_stack_attach: [4] 4000c958-4000b958
app0_thread: arg=11133aab
thread_context_switch: CONTINUATION=>40000758
thread_context_switch: DISCARD STACK (app0)
thread_stack_deallocate: 4000c958
user>
user> thread_context_switch: ALLOCATE STACK (4:app0)
thread_stack_allocate: 4000c958
md_thread_stack_attach: [4] 4000c958-4000b958
thread_context_switch: CONTINUATION=>40000758
thread_context_switch: STACK HANDOFF (app0->app0)
thread_context_switch: CONTINUATION=>40000758
thread_context_switch: STACK HANDOFF (app0->app0)
これは何もなくてスピンしている時にタイマ割り込みが入って、自分のスレッドを
自分につけかえている。
thread_context_switch: CONTINUATION=>40000758
thread_context_switch: DISCARD STACK (app0)
thread_stack_deallocate: 4000c958
シリアルの割り込みで別のスレッドに切り替えることになったのでスタックを放棄した。
user> thread_context_switch: ALLOCATE STACK (4:app0)
thread_stack_allocate: 4000c958
md_thread_stack_attach: [4] 4000c958-4000b958
また戻ってきて、スタックをプールから取り直した。
thread_context_switch: CONTINUATION=>40000758
thread_context_switch: STACK HANDOFF (app0->app0)
thread_context_switch: CONTINUATION=>40000758
