コンテキストスイッチ
thread_invoke()でコンテキストスイッチを行う。
通常は以下の手順でコンテキストスイッチを実行する(各関数の概要は[関連関数]参照)。
ただし、旧スレッドがcontinuationを指定していて、新スレッドにカーネルスタックが存在しない場合は、上記の machine_switch_context()の変わりにmachine_stack_handoff()を使用してコンテキストスイッチをする。machine_stack_handoff()はold_threadのカーネルスタックをnew_threadに渡して流用する(*1)。old_threadがカーネルスタックを予約(スレッド参照)しており、new_threadがカーネルスタックの予約を持っていない場合は、流用できないのでスタックの割り当てを行い、通常のコンテキストスイッチを行う。
(*1)
[カーネルスタックの捕捉]
(*2) Thread Stack Daemon
[関連関数]
thread_done(old_thread, new_thread, processor)
machine_stack_handoff(old, new) - (i386の場合)
thread_begin(thread, processor)
thread_dispatch(thread)
thread_setrun(new_thread, options)
通常は以下の手順でコンテキストスイッチを実行する(各関数の概要は[関連関数]参照)。
- thread_done()
- machine_switch_context()
- thread_begin()
- thread_dispatch()
ただし、旧スレッドがcontinuationを指定していて、新スレッドにカーネルスタックが存在しない場合は、上記の machine_switch_context()の変わりにmachine_stack_handoff()を使用してコンテキストスイッチをする。machine_stack_handoff()はold_threadのカーネルスタックをnew_threadに渡して流用する(*1)。old_threadがカーネルスタックを予約(スレッド参照)しており、new_threadがカーネルスタックの予約を持っていない場合は、流用できないのでスタックの割り当てを行い、通常のコンテキストスイッチを行う。
(*1)
continuationを指定してブロックする場合は、指定関数から実行を再開するため、ブロック時にスタックを残しておく必要はない。このため、新スレッドにスタックを使いまわすようにしてメモリ使用量を抑えるようにしていると思われる。
[カーネルスタックの捕捉]
上記のようにcontinuation付きでブロックしたスレッドはカーネルスタックを使いまわす場合があるため、thread->kernel_stackがNULLになる場合がある。このため、通常のコンテキストスイッチの時、新スレッド(new_thread)にカーネルスタックがなければstack_alloc_try()で割り当てるようになっている。スタックが割り当てられなかった場合はQueue(thread_stack_enqueue)にスレッドを登録して、Thread
Stack Daemon(カーネルスレッド)(*2)に非同期にスタックを割り当てさせている。
なお、ppcではthread_dispatch()において、continuation指定があったらstack_free()でカーネルスタックを解放するようになっているので、continuation付きでブロックしたらスタックを流用しない場合でもthread->kernel_stackがNULLになる。i386ではなぜこの処理をしていないのかは不明。
なお、ppcではthread_dispatch()において、continuation指定があったらstack_free()でカーネルスタックを解放するようになっているので、continuation付きでブロックしたらスタックを流用しない場合でもthread->kernel_stackがNULLになる。i386ではなぜこの処理をしていないのかは不明。
(*2) Thread Stack Daemon
thread_invoke()でカーネルスタックの割り当てに失敗した時に、スタックの割り当て処理を行うカーネルスレッド。実体はthread_stack_daemon()。スタックが割り当てられない場合でもthread_invoke()をブロックさせないために、このDaemonで処理を肩代りする。thread_stack_enqueue(thread)でQueueにスレッドを入れると動き出す。
thread_invoke()の概要
if ((old_thread->sched_mode & TH_MODE_REALTIME) &&
!old_thread->reserved_stack) {
old_thread->reserved_stack = old_thread->kernel_stack;
(リアルタイムスレッドだった場合、need_stack:でのスタックの
再割り当てに失敗して実行が遅延しないように予約しておく。
(スタックの予約についてはスレッドも参照))
}
if (continuation != NULL) {
if (!new_thread->kernel_stack) {
if (old_thread->kernel_stack ==
old_thread->reserved_stack &&
!new_thread->reserved_stack)
goto need_stack;
<== 流用できないので通常のコンテキストスイッチへ
old_threadのスタックをnew_threadに渡して
コンテキストスイッチ
new_threadのcontinuationへ飛ぶ
}
}
else {
if (!new_thread->kernel_stack) {
need_stack:
カーネルスタックの割り当て
(new_threadがcontinuation付きでブロックした
場合はスタックが使いまわされてNULLになって
いる可能性があるので、その時の再割り当て処理)
}
}
通常のコンテキストスイッチ
if (continuation) {
new_threadのcontinuationへ飛ぶ。
(この時点でコンテキストスイッチは完了しているので
continuationは新スレッドのcontinuationになっているので注意)
}
[関連関数]
thread_done(old_thread, new_thread, processor)
コンテキストスイッチでスレッド実行が終了する時の処理。
- 残りのQuatum値を計算してcurrent_quantumに格納する。
- Quantumを使いきっていたら要因(thread->reason)AにST_QUANTUMをセットする。
- 切替えの要因がAST_HANDOFFならold_threadのcurrent_quantumをnew_threadに引き継ぐ
コンテキストスイッチのCPU依存部分を処理する。
- fpu_save_context() - FPUのレジスタを保存
- PMAP_SWITCH_CONTEXT - PageTableの切替え(CR3に新スレッドのPageDirectoryを設定)
- act_machine_switch_pcb - TR,LDTの設定。新スレッドのFPUコンテキストをロード
- Switch_context() - CPUのレジスタを保存して、新スレッドのものをロードする。
machine_stack_handoff(old, new) - (i386の場合)
カーネルスタックを流用してコンテキストスイッチする際に使用するmachine_switch_context()の代替ルーチン。
- oldスレッドのカーネルスタックをnewスレッドに渡す。
- PMAP_SWITCH_CONTEXT()でページテーブルを新スレッドのものに設定する
thread_begin(thread, processor)
新スレッドのQuantum満了時刻に合わせてタイマを設定する。Idleスレッドだった場合はタイマ解除。
processor->quantum_end = (processor->last_dispatch +
thread->current_quantum);
timer_call_enter1(&processor->quantum_timer,
thread, processor->quantum_end);
thread_dispatch(thread)
コンテキストスイッチの後、前のスレッドを再度RunQueueに入れたりする。スレッドが終了(TH_TERMINATE)していたらTerminateQueueに入れてThreadTerminateDaemonにプロセスを解放させる。
thread_setrun(new_thread, options)
スレッドにCPUを割り当てる。
特定のCPUに括り付け(bind)られていた場合は、そのCPUを使用する。Bindされてなければ、最後に使用したCPU(thread->last_processor)がIdleであればそれを使用し、そうでなければ、同じプロセッサセット内のIdleプロセッサを採用する。
特定のCPUに括り付け(bind)られていた場合は、そのCPUを使用する。Bindされてなければ、最後に使用したCPU(thread->last_processor)がIdleであればそれを使用し、そうでなければ、同じプロセッサセット内のIdleプロセッサを採用する。
プロセッサがIdleだった場合はRunQueueに入れるのではなく、processor->next_threadにつないで、Idleスレッドから指定スレッドへ切替えを実行させるようにしている。