setlabel/gotolabelによるコルーチン
Plan9はTSS(タイムシェアリングシステム)であり,プロセススケジューリングに対する基本的な考えはUNIXと同じである.あるプロセスが動作しているときに,タイマ割込みなどによってカーネルに動作が遷移し,スケジューラが起動され,プロセスを入れ替え,ユーザモードに復帰したときは別プロセスが動き始める.これをプロセスプリエンプト(横取り)と呼ぶ.ます,スケジューラのコアの部分であるプロセススイッチから見ていこう.
前回書いたように,カーネル内ではsetlabel/gotolabelを使ってコルーチンを提供している.最近はマルチプロセスやマルチスレッドが当たり前になっているので,コルーチンなんて聴きなれないかもしれないが,フロー制御構造の一種で,複数の制御フローを明示的に中断,再開することで,並列的に処理を行なうことができる.Plan9のプロセススイッチはこのコルーチンを使って実装されている.関係があるコードは,port/proc.cのschedinit関数とsched関数である.以下に関係のある箇所を抜き出してみた.また,プロセッサごとの構造体であるstruct Machへのポインタmと,カレントプロセスの構造体struct Procへのポインタupの2つのグローバル変数が参照される.それぞれの構造体はschedというラベルを持っていて,m->schedはschedinitの先頭を,up->schedはプロセスをスイッチする156行目地点のプログラムカウンタとスタックポインタを保持している.
schedの呼び出しから見ていこう.sleep(1)やタイマ割込みなどがきっかけでschedが呼ばれる.カレントプロセスが存在するので,splhiで割り込みを禁止して,procsaveでstruct Procにコンテキストを保存,setlabelを呼ぶ(156行).setlabelの戻り値は必ず0なので,gotolabelが呼ばれ(161行),ラベルm->schedであるschedinitの先頭にジャンプする(schedinitについてはさらに後述する).プロセスはRunning状態なので,readyを呼び,プロセスをレディキューにつなげる.そして,upをnilに設定し,再度schedを呼ぶ.
今度はupがnilなので,runporcによって,次に実行するプロセスpが選択される.カレントプロセスをpに設定して,Running状態にし,mmuswitchでアドレス空間を切り替える.176行目のgotolabelは156行目のsetlabelの箇所にジャンプする(正確に言えばPCとSPを書き戻す).gotolabelの戻り値は必ず1になる(この挙動はsetjmp/longjmpを知っていれば,すぐわかるだろう).そして,struct Procに格納されたコンテキストを復帰し,割込みを許可して,リターンする.
67: void 68: schedinit(void) /* never returns */ 69: { 72: setlabel(&m->sched); 73: if(up) { 76: m->proc = 0; 77: switch(up->state) { 78: case Running: 79: ready(up); 80: break; 101: } 102: up->mach = nil; 103: updatecpu(up); 104: up = nil; 105: } 106: sched(); 107: } 113: void 114: sched(void) 115: { 116: Proc *p; 124: if(up){ 150: splhi(); 155: procsave(up); 156: if(setlabel(&up->sched)){ 157: procrestore(up); 158: spllo(); 159: return; 160: } 161: gotolabel(&m->sched); 162: } 163: p = runproc(); 171: up = p; 172: up->state = Running; 173: up->mach = MACHP(m->machno); 174: m->proc = up; 175: mmuswitch(up); 176: gotolabel(&up->sched); 177: }