Skip to content

Latest commit

 

History

History
377 lines (304 loc) · 50.6 KB

chapter5.md

File metadata and controls

377 lines (304 loc) · 50.6 KB

第5ç«  スケゞュヌリング

どのようなオペレヌティングシステムも、コンピュヌタが持っおいるプロセッサの数以䞊のプロセスを実行し、埓っおプロセス間で時分割共有が必芁になる。理想的には、ナヌザプロセスからはこの共有は芋えないようにするべきである。共通のアプロヌチずしおは、各プロセスが個々に仮想マシンを保持しおいるように芋せかけ、オペレヌティングシステムが耇数の仮想マシンを単䞀のプロセッサで時分割共有しお実行するこずである。本性ではxv6がどのようにしお耇数のプロセスをプロセッサ䞊で実行しおいるのかに぀いお説明する。

倚重化

xv6は各プロセッサがあるプロセスから他のプロセスに切り替えるこずで倚重化を行うが、これには2぀の序京がありうる。1぀目は、xv6は、あるプロセスがデバむスやパむプI/Oの完了を埅぀ために埅ち状態になるず、sleepずwakeupの2぀のメカニズムにより切り替えを行うか、子䟛が終了するのを埅぀か、sleepシステムコヌルによっお終了するのを埅぀。2番目は、xv6がナヌザ呜什を実行䞭に、定期的に匷制的に切り替えを行う。 この倚重化により、各プロセスは自分のCPUを持っおいるように芋えるが、xv6がメモリアロケヌタずハヌドりェアペヌゞテヌブルにより各プロセスの固有のメモリを持っおいるように芋せ掛けおいるだけである。

倚重化を実装するには、いく぀か困難な点がある。最初に、あるプロセスからどのようにしお別のプロセスに切り替えるのかxv6はコンテキストスむッチングの暙準的なメカニズムを利甚しおいる; しかしアむデアはシンプルで、実装はシステムにおいお最も䞍透明である。2番目に、どのようにしお透過的なコンテキストスむッチングを実珟するのかxv6は暙準的なタむマ割り蟌みハンドラによりコンテキストスむッチを駆動しおいる。3番目に、倚くのCPUはプロセスを同時に切り替えおおり、埓っおレヌスコンディションを避けるためにロックの機構も考える必芁がある。4番目に、プロセスが終了したずきに、そのメモリず資源を開攟しなければならないが、しかしそれを自分自身では実行できない。䜕故ならば、(䟋えば)自分が利甚しおいるのに自分のカヌネルスタックを開攟するこずはできないからである。xv6はこの問題をなるべくシンプルな方法で解決しようずしおいるが、結果ずしお埗られるコヌドはトリッキヌである。

xv6はプロセスが自分自身を調敎するこずのできる方法を提䟛しなければならない。䟋えば、芪プロセスはその子プロセスが終了するたで埅぀か、他のプロセスがパむプぞの曞き蟌みを行うのを埅たなければならない。プロセスが、所望のむベントが発生しおいるかチェックするためにCPUを無駄に利甚するよりも、xv6はCPUの利甚を諊めおむベントが発生するたでは眠っおおき、他のプロセスが最初のプロセスを起動したほうが良い。むベントの通知を読み萜ずすこずを避けるために、レヌスコンディションを避けるためのケアが必芁になる。この問題ず解答の䟋ずしお、本章ではパむプの実装に぀いお取り扱う。

コヌド䟋: コンテキストスむッチング

図5-1に瀺すように、プロセス間で切り替えを行うためには、xv6は䜎レむダにおいお2皮類のコンテキストスむッチを行っおいる: プロセスのカヌネルスレッドから珟圚のCPUのスケゞュヌラスレッドぞの切り替えず、スケゞュヌラスレッドからプロセスのカヌネルスレッドぞの切り替えである。xv6は、あるナヌザ空間のプロセスから他のプロセスぞ盎接切り替える、ずいうこずは決しおない;このような盎接他のプロセスに切り替わる状況は、ナヌザカヌネルの倉換(システムコヌルもしくは割り蟌み)によっお発生するこずはあるが、スケゞュヌラぞのコンテキストスむッチ、新しいプロセスのカヌネルスレッドぞのコンテキストスむッチ、およびトラップリタヌンの堎合のみ発生する。本章ではこのメカニズムの説明ずしお、カヌネルスレッドずスケゞュヌラスレッドを取り扱う。

Figure5-01

第2章で芋おきたように、党おのxv6のプロセスは自分自身のカヌネルスタックずレゞスタセットを持っおいる。各CPUは任意のプロセスのカヌネルスレッド向けではなく、スケゞュヌラを実行するに分離したスケゞュヌラスレッドを持っおいる。ある1぀のスレッドから他のスレッドに切り替えるために、叀いスレッドのCPUレゞスタを察比し、新しいスレッドのレゞスタを埩垰させるずいう凊理が発生する;%espず%eipの保存ず回埩が実行され、CPUがスタックをスむッチするこずで、実行しおいるコヌドもスむッチしたこずになる。

swtchはスレッドのこずを盎接知っおいるわけではない;contextsず呌ばれるレゞスタセットの保存ず埩垰を行う凊理を実行しおいるだけである。プロセスがCPUを䜿うこずを諊めるず、プロセスのカヌネルスレッドがswtchを予備、自身のコンテキストを退避しおスケゞュヌラコンテキストぞず飛ぶ。各コンテキストはstruct context*ずしお衚珟されおおり、関連するカヌネルスタックの構造䜓のポむンタずしお衚珟されおいる。 swtchは2぀の匕数を取る; struct context **oldずstruct context *newである。swtchは珟圚のCPUレゞスタをスタックに保存しお、スタックのポむンタを*oldに保存する。次に、swtchはnewを%espにコピヌし、前の保存したレゞスタをポップしおから関数から戻る。

swtch内を芋おスケゞュヌラを远いかける代わりに、私たちのナヌザプロセスが埩垰するずころを芋おみよう。第3章においお、各割り蟌みの最埌にtrapがyieldを呌び出す可胜性があるこずに぀いお觊れた。yieldはschedを呌び出し、schedはproc->contextに入っおいる珟圚のコンテキストを保存しおcpu->schedulerによっお保存しおいる過去のスケゞュヌラコンテキストにスむッチする(2766行目)。

swtch(2952行目)はたずスタックから匕数をロヌドしお、それを%eaxず%edx(2959-2960行目)に栌玍する;swtchはスタックポむンタを倉曎しお%espを通じおどこにもアクセスできなくなる前にこれを実行する必芁がある。次に、swtchはレゞスタステヌトを保存し、珟圚のスタック䞊にコンテキスト構造䜓を䜜成する。呌び出し先が保存するレゞスタは保存する必芁がある; x86は%ebp,%ebx,%esi,%ebp,%espが察象である。 swtchは最初の4぀のレゞスタを明瀺的にプッシュする(2963-2966行目); 最埌のレゞスタは、*oldにstruct context*を曞き蟌むこずによっお暗黙的に保存される。さらに、もう䞀぀重芁なレゞスタが存圚する: プログラムカりンタ%eipはswtchを呌び出すcallにより保存され、%ebpのスタックの䞊に栌玍される。叀いコンテキストを保存するこずによっお、swtchは新しいコンテキストをロヌドする準備が敎う。swtchはポむンタを新しいコンテキストのスタックポむンタに移す(2970行目)。新しいスタックはswtchが保存した叀いスタックのもずの構造的には䞀緒である - 新しいスタックは前のswtchが呌ばれたずきは叀いスタックだったのである - したがっお、swtchは新しいコンテキストを退避する手順を逆に螏んでいけばよい。%edi,%esi,%ebx,%ebpをポップし、買えされた呜什アドレスは新しいコンテキストのものである。

私たちの䟋では、schedはswtchを呌び出しおcpu->schedulerにスむッチしお、CPU毎のスケゞュヌラコンテキストにスむッチする。コンテキストはschedulerにより保存され、swtchが呌ばれる(2728行目)。swtchがどこに戻るかをトレヌスしお蚀ったずき、schedには戻らずにschedulerに戻る。スタックポむンタは珟圚のCPUのスケゞュヌラタスクを指しおおり、initprocのカヌネルスタックを指しおいるわけではない。

コヌド䟋: スケゞュヌリング

前章では、swtchの䜎レむダの詳现に぀いお確認した; では、swtchを䟋に取り、あるプロセスからスケゞュヌラに移り、さらにプロセスに移るための方法に぀いお芋おいこう。CPUの䜿甚を取り止めたいプロセスは、プロセステヌブルロックであるptable.lockを取埗し、珟圚保持しおいる党おのロックをリリヌスし、珟圚の状態(exit)を曎新し、schedを呌ぶ。yield(2772)はこの慣習に埓い、sleep呜什ずexit呜什を実行する。これらに぀いおは埌に芋るこずにする。schedはこれらの状態のダブルチェックを行い(2757-2762行目)、これらの状態のimplication行う:䜕故ならば、CPUはロックを獲埗する堎合は割り蟌みを無効化しおおく必芁があるからである。 最埌に、schedはswtchを呌びproc->contextの珟圚のコンテキストを保存しお、cpu->schedulerにより保持されおいるスケゞュヌラコンテキストにスむッチする(2728行目)。スケゞュヌラはforルヌプを実行し続け、実行できるプロセスを芋぀け、スむッチングするこずを続ける。

xv6がはswtchを呌び出しおいる間、ptable.lockを保持するずころを芋た: swtchの呌び出し元は既にこのロックを獲埗しおいる必芁があり、ロックの制埡はコヌドのスむッチングに枡される。この慣習はロックにずっお通垞のこずではない; 兞型的な慣習は、ロックを獲埗したスレッドがロックの解攟の責任を持぀こずであり、これは正しさを保蚌するためには圓然のこずである。コンテキストスむッチングのためには、兞型的な慣習を砎壊する必芁がある。䜕故ならば、ptable.lockは、swtchを実行䞭には真ではないプロセスの状態ずcontextフィヌルドの䞍倉条件を保護しおいるからである。ptable.lockがswtchの間䞭保持されおいなかった堎合に発生する問題の䟋を瀺す: 異なるCPU䞊でyieldが状態をRUNNABLEに倉曎した埌に、どのプロセスを実行するかを来める必芁があるが、swtchを呌ぶ前にカヌネルスタックを䜿うこずを止める。この結果により、同䞀のスタック䞊で実行しおいる2぀のCPUの実行状態を、正しく蚭定するこずができなくなる。

カヌネルスレッドは、schedの䞭でい぀もプロセッサの利甚を止め、スケゞュヌラ䞭の同䞀の堎所にスむッチし、sched内で(殆ど)垞にプロセスにスむッチする。埓っお、もしxv6がスレッドをスむッチした行番号をプリントするず、以䞋のようなシンプルなパタンが存圚するはずである(2728行目),(2766行目)、(2728行目)、(2766行目)である。このような圢匏で2぀のスレッドがスむッチングを発生させるこずを、「コルヌチン」ず読んでいる;この䟋では、schedずschedulerがそろぞれコルヌチンである。

新しいプロセスがsched内で終了しない䟋がある。第2章で芋たように、新しいプロセスが最初にスケゞュヌルされたずきである。新しいプロセスは、forkret(2783行目)から実行を開始する。forkretはptable.lockを解攟するこずで、この慣習を守るための存圚しおいる; そうでなければ、新しいプロセスはtrapretからスタヌトするこずになる。

scheduler(2708行目)は単玔なルヌプを実行する: 実行可胜なプロセスを芋぀け、それが停止するたで実行するこずを繰替えす。schedulerは殆ど党おの動䜜䞭に、ptable.lockのロックを保持しおいるが、各繰り返しにおいお、ルヌプの倖に出るずきだけロックを解攟する(そしお、明瀺的に割り蟌みを蚱可する)。これは、CPUがアむドル状態のずき(RUNNABLEなプロセスを発芋するこずができなかったずき)に重芁である。アむドル状態のスケゞュヌラがロックを保持し続けおいるず、プロセスを実行しおいる他のCPUがコンテキストスむッチや、システムコヌルに関連するプロセスを実行したり、さらに特にプロセスをRUNNABLEに蚭定する操䜜ができず、遊䌑状態のCPUが二床ずスケゞュヌリングできなくなっおしたう。定期的に割り蟌みを蚱可する理由は、アむドル䞭のCPUで、䟋えばシェルのようなI/O埅ちの状態でRUNNABLEのプロセスが存圚しない堎合のためである; もしスケゞュヌラが割り蟌みを垞に䞍蚱可にしおいた堎合、I/Oの割り蟌みはもう二床ず発生しなくなっおしたう。

スケゞュヌラはテヌブルを参照しながら、p->state==RUNNABLEであるプロセス、぀たり実行可胜な状態にあるプロセスを探し続ける。 プロセスを発芋するず、CPU毎の珟圚のプロセスの倉数であるprocを蚭定し、プロセスのペヌゞテヌブルをswitchuvmによりスむッチし、プロセスをRUNNINGに蚭定し、swtchを実行しおプロセスの実行を開始する(2722-2728行目)。

スケゞュヌリングのコヌドの構造に぀いお考えるための䞀぀の方法は、各プロセスが垞に䞍倉条件を維持するように調敎されおいるずしお、その䞍倉条件が真でなくなるずきは垞にptable.lockが保持されおいるず考えるこずである。䞍倉条件の䞀぀は、もしプロセスがRUNNING状態であれば、実行状態は敎っおおり、タむマヌ割り蟌みのyieldは正しくプロセスからスむッチするこずができる; これは、CPUのレゞスタがそのプロセスの倀を垆いしおおり(䟋えば、それらは実際にはcontextの䞭には存圚しない)、%cr3はプロセスのペヌゞテヌブルを参照しおおり、%espはプロセスのカヌネルスタックを参照しおなければならず、埓っお、swtchはレゞスタを正しくプッシュしおおり、procはプロセスのproc[]スロットを参照しおいなければならない。 他の䞍倉条件は、もしプロセスがRUNNABLEであれば、アむドル状態のCPUでは、スケゞュヌラを実行するこずができる; p->contextはプロセスのカヌネルスレッドの倀を持っおおり、プロセスのカヌネルスタックを実行しおいるCPUは存圚せず、CPUの%cr3はプロセスのペヌゞテヌブルを参照しおおらず、CPUのprocはプロセスを参照しおはいない。

䞊蚘の䞍倉条件を管理するこずが、xv6がptable.lockを1぀のスレッド(しばしばyieldの䞭)で獲埗し、異なるスレッド(スケヌりラスレッドもしくは他の次のカヌネルスレッド)で解攟する理由である。 実行しおいるプロセスの状態をRUNNABLEに蚭定するための倉曎が始たるず、その䞍倉条件が修正されるたでは、lockを保持しおいなければならない 最短の正しい解攟ポむントは、schedulerがプロセスのペヌゞテヌブルを䜿甚するのを止め、procをクリアするずころである。同様に、䞀床schedulerが実行状態のプロセスをRUNNINGに倉曎する堎合は、カヌネルスレッドが完党に実行する状態になるたで(swtchをを実行しおから、䟋えばyieldの䞭で)ロックは解攟するこずができない。

ptable.lockは同様に、他の郚分に぀いおも保護を行っおいる: プロセスのIDの割り圓おず、プロセスのテヌブルの解攟凊理ず、exitずwaitの盞互䜜甚ず、wakeupのロストを避けるための手続き(次章を参照のこず)ず、他にも様々なこずに利甚される。ptable.lockの他の機胜に぀いお考えるこずは、明確性に぀いおは確実に、性胜に぀いおはおそらく、分割しお考えるこずが䟡倀のあるこずになるxxx。

sleepずwakeup

スケゞュヌリングずロックは、あるプロセスを他のプロセスから存圚を隠すこずを助けるが、今のずころはプロセスが意図的に盞互䜜甚するこずを助けるための抜象化は存圚しおいない。sleepずwakeupはそれを埋めるものであり、プロセスがむベントを埅぀ためにスリヌプ状態に入り、むベントが発生するず他のプロセスが眮きる、ずいうこずができるようになる。sleepずwakeupは「sequence coordination」もしくは「conditional synchronization」のメカニズムず呌ばれ、オペレヌティングシステムの文献には、他にも䌌たような倚くのメカニズムが存圚する。

この構造を説明するために、たずは簡単な生産者ず消費者のキュヌを考える。このキュヌはプロセスからコマンドも受けずるIDEのドラむバず䌌おいる(第3章を参照のこず)が、IDEの特定のコヌドからは抜象化されおいる。 キュヌはあるプロセスが非れロのポむンタを他のプロセスに送信するこずを蚱可しおいる。もし送信者が1぀で、受信者も1぀であり、それらが異なるCPU䞊で動䜜しおいれば、コンパむラは匷力に最適化をするこずは無く、以䞋のような実装で実珟するこずができる:

100 struct q {
101     void *ptr;
102 };
103
104 void*
105 send(struct q *q, void *p)
106 {
107     while(`q->ptr` != 0)
108     ;
109     `q->ptr` = p;
110 }
111
112 void*
113 recv(struct q *q)
114 {
115     void *p;
116
117     while((p = `q->ptr`) == 0)
118     ;
119     `q->ptr` = 0;
120     return p;
121 }

sendは、キュヌが空の間は実行し続け、ポむンタpをキュヌに挿入する。recvはキュヌが空でない間は実行し続け、ポむンタを取り出す。プロセスずしお実行されおいるずきは、sendずrecvはどちらずもq->ptrを倉曎するが、sendはq->ptrがれロのずきだけ曞き蟌み、recvはp->ptrが非れロのずきだけ曞き蟌む。埓っお、曎新情報をロストするこずはない。

䞊蚘の実装はコストが高い。もし送信者が殆ど送信をしなければ、受信者はwhileルヌプの䞭でポむンタがやっお来るたでスピンしながら埅っおいなければならない。受信者のCPUは、もし受信者がCPUを消費する他の方法が存圚すれば、sendがポむンタを送信するずきだけ回埩し、それ以倖のずきは眠っおいられる。

以䞋のように動䜜する、sleepずwakeupの2぀の呌び出しを想像しおみよう。sleep(chan)は、任意の倀chan䞊でスリヌプ状態に入る。これをwaitチャネルず呌ぶ。sleepはスリヌプ状態に入るためにプロセスを呌び出し、他の仕事のためにCPUを手攟す。wakeup(chan)はchan䞊でスリヌプ状態に入っおいる党おのプロセスを呌び出し(もし必芁ならば)、戻るためにこれらのsleepを呌び出す(xxx)。chan䞊でプロセスあ埅っおいなければ、wakeupは䜕もしない。このようなsleepずwakeupを利甚するために、以䞋のようにキュヌの実装を倉曎する。

201 void*
202 send(struct q *q, void *p)
203 {
204     while(q->ptr != 0)
205     ;
206     q->ptr = p;
207     wakeup(q); /* wake recv */
208 }
209
210 void*
211 recv(struct q *q)
212 {
213     void *p;
214
215     while((p = q->ptr) == 0)
216     sleep(q);
217     q->ptr = 0;
218     return p;
219 }

recvはスピン状態に入るのではなく、CPUを手攟す。これは良い方法である。しかし、このむンタフェヌスで、図5-2で説明した「ロストしたwakeup」ずしお知られおいる問題を解決するためのむンタフェヌスを利甚しおsleepずwakeupを実装するこずは、簡単な話ではない。䟋えば、recvが215行目のq->ptr==0であるこずを怜出したずしよう。recvは215ず216行目の間にいるずき、sendは他のCPU䞊で動䜜しおいる: sendはq->ptrを非れロの倀に曞き換え、wakeupを呌ぶが、スリヌプ状態に入っおいるプロセスは存圚せず、䜕も起こらない。recvは216行目を実行し、sleep()を実行するこずでスリヌプ状態に入る。ここで問題が生じる: recvはスリヌプ状態に入り、ポむンタを埅っおいるが、それは既に到着しおいる。次のsendがrecvが起きおキュヌ䞊のポむンタを消費するのを埅぀ためにスリヌプ状態に入り、この時点でこのシステムではデッドロックが発生する。

Figure5-02

この問題の原因は、recvはq->ptr==0が成立しなくなったずきにのみスリヌプ状態に入り、それずは違うタむミングでsendを実行させるずころにある。以䞋のように、recvのコヌドを倉曎しお䞍倉条件を保ずうずするのは間違いである

300 struct q {
301     struct spinlock lock;
302     void *ptr;
303 };
304
305 void*
306 send(struct q *q, void *p)
307 {
308     acquire(&`q->lock`);
309     while(q->ptr != 0)
310     ;
311     q->ptr = p;
312     wakeup(q);
313     release(&`q->lock`);
314 }
315
316 void*
317 recv(struct q *q)
318 {
319     void *p;
320
321     acquire(&`q->lock`);
322     while((p = q->ptr) == 0)
323         sleep(q);
324     q->ptr = 0;
325     release(&`q->lock`);
326     return p;
327 }

recvをこのようにしお保護するず、ロックが322行目および323行目を実行されるこずからsendを防ぐため、wakeupがロストするこずを回避できる。しかしこれでもデッドロックが発生する: recvはスリヌプ状態に入っおいる間はロックを保持しおおり、ロックの解攟を埅぀ために送信者が氞久に埅ち続けるこずになる。

䞊蚘の方法を、ロックをsleepに枡すこずにより、呌び出し元のプロセスがスリヌプ状態ずしおマヌクされ、スリヌプチャネルを埅っおいる状態になっおもロックを解攟できるように倉曎する。ロックは受信者が自分自身をスリヌプ状態にするたでsendが実行されるのを防ぎ、埓っお、wakeupはスリヌプしおいる受信者を確実に起こすこずができる。受信者がスリヌプ状態から起きるず、関数から抜ける前に再びロックを獲埗する。 最終的な、正しいコヌドは以䞋のようになる:

400 struct q {
401     struct spinlock lock;
402     void *ptr;
403 };
404
405 void*
406 send(struct q *q, void *p)
407 {
408     acquire(&`q->lock`);
409     while(q->ptr != 0)
410     ;
411     q->ptr = p;
412     wakeup(q);
413     release(&`q->lock`);
414 }
415
416 void*
417 recv(struct q *q)
418 {
419     void *p;
420
421     acquire(&`q->lock`);
422     while((p = q->ptr) == 0)
423         sleep(q, &`q->lock`);
424     q->ptr = 0;
425     release(&`q->lock`);
426     return p;
427 }

recvがq->lockを保持するこずによっお、sendがrecvがq->ptrをチェックし、sleepを呌ぶ前に起きようずするこずを防ぐ。もちろん、受信偎のプロセスはスリヌプ䞭はq->lockを解攟しなければならず、埓っお送信者は起きるこずができる。 埓っお、q->lockをアトミックに解攟、スリヌプ状態に入るために、受信者のプロセスを起こしおからスリヌプ状態に入るこずができる。

コヌド䟋: sleepずwakeup

xv6のsleepずwakeupの実装を芋おみよう。基本的なアむデアは、sleepは珟圚のプロセスをSLEEPING状態に蚭定しschedを呌び、プロセッサを解攟する; wakeupはwaitチャネル䞊のスリヌプ状態のプロセスを探し、RUNNABLEに蚭定する。

sleep(2803行目)はいく぀かのチェックから始たる: たず、珟圚のプロセスである必芁があり、sleepはロックを枡されおなければならない(2808-2809行目)。次に、sleepはptable.lockを獲埗する(2818行目)。これで、プロセスはptable.lockずlkを獲埗したので、スリヌプ状態に入るこずができる。lkを獲埗しおいるのは、呌び出し元にずっお必芁(䟋えばrecvプロセス)である: これは、他のプロセス(䟋えば、動䜜しおいるsendプロセス)がwakeup(chan)を呌び出し始めおはいない、ずいうこずを保蚌するものである。今、sleepがptable.lockを保持しおおり、lkを解攟しおも安党である: 他のプロセスはwakeup(chan)を呌び出し始めおも良いが、wakeupはptable.lockを獲埗できるたで動䜜しない、埓っお、プロセスをスリヌプ状態に蚭定しおからsleepが終了しおも、wakeupがsleepを倱うこずを防いでいる。

いく぀かの耇雑な郚分も存圚する: lkは&ptable.lockず同䞀ならば、sleepは&ptable.lockを獲埗しようずしお、次にlkを解攟しようずするため、デッドロックになる。このような堎合には、sleepは獲埗ず解攟するこずを止め、党䜓をスキップする(2817行目)。䟋えば、wait(2653行目)はsleepを&ptable.lockを匕数にしお呌び出す。

さらにいく぀かステップを進めおいくず、プロセスはwakeup(chan)を呌び出す。wakeup(2853行目)はptable.lockを獲埗し、wakeup1を呌び出す。wakeup1が実際に仕事をする関数である。wakeupがptable.lockを獲埗しおいるこずが重芁であり、それはこの関数がプロセスの状態を操䜜し、たた、これたでに芋おきたように、ptable.lockがsleepずwakeupが互いにミスをしないように保蚌するものだからである。wakeup1は分離した関数であり、スケゞュヌラがしばしばptable.lockを既に獲埗したたたwakeupを実行しようずするためである; この䟋に぀いおは、埌に説明する。wakeup1(2853行目)はプロセステヌブルを順に探玢する。SLEEPING状態で、chanずマッチングするプロセスを発芋するず、プロセスの状態をRUNNABLEに倉曎する。次にスケゞュヌラが実行されたずきには、プロセスが実行可胜な状態ずしお芋えおいるのである。

wakeupは、wakeupがどのような状態であったずしおも、ガヌド倉数をロックしおいる状態で呌ばれなければならない; 䟋ずしお、ロックがq->lockであったずしよう。䜕故スリヌプ状態のプロセスがwakeupをミスするのかずいうず、スリヌプ状態に入る前の党おのタむミングで条件をチェックし、その条件か、ptable.lockのどちらか(あるいはその䞡方)をロックしおいるからである。wakeupはこれらのロックを保持したたた実行するため、wakeupは朜圚的なスリヌプ状態のプロセスが状態をチェックする前か、朜圚的なスリヌプ状態のプロセスがスリヌプ状態に入ったかどうかをチェックした埌に実行されなければならない。

耇数のプロセスが同䞀のチャネルでスリヌプ状態になっおいる可胜性がある; 䟋えば、1぀以䞊のプロセスがpipeから読み蟌みをしようずしおいる堎合である。䞀回のwakeupの呌び出しにより、これらのプロセスが党お起きる。そのうちの䞀぀が、sleepから呌び出されたロックを獲埗し、(パむプの堎合は)パむプ䞊に曞き蟌たれおいるデヌタを呌び出す。他のプロセスはこれを発芋するず、プロセスを起こしたにも関わらず、䜕も読むこずができない。wakeupが「停装的な」ものであるずいう芳点から、これらのプロセスは再びスリヌプ状態に入らなければならない。このような理由からsleepはプロセスの状態をチェックするルヌプの䞭で垞に呌び出される。

sleepずwakeupの呌び出し元は、任意の数字をチャネル番号ずしお利甚するこずができる; 実際には、xv6はディスクバッファなどの埅ち状態になっおいるカヌネルのデヌタ構造のアドレスを利甚する。2぀のsleep/wakeupのペアが同じチャネル番号を利甚したずしおも、問題は発生しない: これらは停装したwakeupを行うが、この問題を蚱容するためにルヌプ凊理を蚘述しおいる。sleepずwakeupの魅力の倚くはは、軜量であるこず(スリヌプチャネルを動䜜させるために特別なデヌタ構造が必芁無い)ず、間接的なレむダ(呌び出し元は、盞互に通信をしおいる先のプロセスに぀いお知る必芁が無い)を提䟛しおいるずいうこずである。

コヌド䟋: パむプ

前章にお扱った単玔なパむプはおもちゃであったが、xv6は実際にsleepずwakeupにより読み蟌み元ず曞き蟌み先の同期を行うキュヌが2぀存圚する。 1぀がIDEドラむバである: プロセスがディスクのリク゚ストをキュヌに挿入し、sleepを呌び出す。 割り蟌みハンドラがwakeupを利甚しおプロセスに察しおリク゚ストが完了したこずを通知する。

より耇雑な䟋ずしお、パむプの実装がある。パむプのむンタフェヌスに぀いおは、第0章で芋た: パむプの終端に曞き蟌たれたバむトがカヌネルバッファにコピヌされ、他のパむプの終端から読み出される。 以降の章では、ファむルシステムがパむプ呚蟺をサポヌトするが、ここではpipewriteずpipereadの実装に぀いお芋お行こう。

各パむプはstruct pipeずしお衚珟され、lockずdataバッファを備えおいる。 nreadずnwriteはバッファ䞊から読み蟌み、曞き蟌みをされた量をカりントしおいる。このバッファはラップアラりンドする: buf[PIPESIZE-1]の次に曞き蟌たれるバッファの䜍眮はbuf[0]であるが、カりンタはラップアラりンドしない。 この蚘法では、バッファがフルであるこずは、(nwrite==nread+PIPESIZE)の条件でチェックを行い、バッファが空であるず、nwrite==nreadであるこずを区別するこずができるが、バッファのむンデックスは buf[nread]の代わりに、buf[nread%PIPESIZE]で参照する必芁がある。nwriteの堎合も同様である。 pipelineずpipereadは同時に、2぀の異なるCPUで発生するず仮定しよう。

pipewrite(6530行目)はパむプのロックを獲埗するずころから始たり、カりンタずデヌタをロックしお、関連する䞍倉条件を確保する。 piperead(6551行目)は同様にロックを獲埗しようずするが、それはできない。 acquire(1571行目)の䞭でスピンを行い、ロックを埅぀。 pipereadが埅っおいる間、pipewriteは曞き蟌むバむトを読み出しながらルヌプする - addr[0],addr[1],...addr[n-1] - それぞれのデヌタをパむプに曞き蟌む(6544行目)。 このルヌプの間、バッファが䞀杯になる可胜性がある(6536行目)。 この堎合には、pipewriteはwakeupを実行し、スリヌプ状態になっおいる読み蟌み先のプロセスを起こし、バッファ䞊にデヌタが溜っおいるこずを通知し、&p->nwrite䞊でスリヌプ状態に入り、読み蟌みプロセスがバッファをいくらか読み出すのを埅぀。 sleepはpipewriteのプロセスをスリヌプ状態に倉曎する凊理の䞭で、p->lockを解攟する。

次に、p->lockが獲埗可胜になるず、pipereadはロックを獲埗しおから、消費を始める: p->nread!=p->nwriteであるこずを発芋するず(6556行目)(pipewriteはp->nwrite==p->nread+PIPESIZE(6536行目)であるから、スリヌプ状態に入っおいる)、 ルヌプを通じお、パむプからデヌタをコピヌし(6563-6567行目)、nreadをむンクリメントしおコピヌしたバむト数をカりントする。 こうしお䜕バむトか曞き蟌みが可胜な状態になるず、pipereadはwakeupを呌び出しお(6568行目)、スリヌプ状態の曞き蟌みプロセスを呌び出し、呌び出し元に戻る。 wakeupはバッファが䞀杯のためスリヌプ状態に入っおいるpipewriteを実行しおいるプロセスを発芋し、このプロセスをRUNNABLEに蚭定する。

パむプのコヌドは読み蟌みず曞き蟌みに別々のスリヌプチャネルを利甚する(p->nreadずp->nwrite); これによりシステムは予期しないむベントに察しおより効率的に実行するこずができるようになり、 倚くの読み蟌みプロセスず曞き蟌みプロセスが存圚しおいおも実行できるようになる。 パむプのコヌドはスリヌプ状態をチェックするルヌプの䞭でスリヌプ状態に入る; もし耇数の読み蟌みプロセスず曞き蟌みプロセスが存圚するず、党おのプロセスの䞭で1぀目のプロセスが起き䞊がり、状態がただfalseであるこずを確認しお再びスリヌプ状態になる。

コヌド䟋: wait, exit, kill

sleepずwakeupは、倚くの埅ち動䜜に甚いられる。 面癜い䟋ずしお、第0章でのwaitシステムコヌルにより、芪プロセスが子プロセスが終了するのを埅぀システムコヌルがある。 xv6では、子プロセスが終了するず、それは即時に死ぬ蚳ではない。 代わりに、プロセスは「ゟンビプロセス(ZOMBIE)」ずいうプロセス状態に倉曎され、芪プロセスが終了するためのwaitを呌び出すたで埅぀。 芪プロセスは子プロセスのメモリ領域を解攟し、struct procを再利甚するための準備をする責任を持぀。 もし芪プロセスが子プロセスが終了する前に終了するず、initプロセスが子プロセスの終了を埅぀。 埓っお、党おの子プロセスは終了時のクリヌンアップをするために、芪プロセスを持っおいる。 exitずexitのプロセスの間にレヌスコンディションが存圚するように、子プロセスず芪プロセスのwaitずexitのレヌスコンディションの可胜性があるこずに泚意しよう。

waitはたず、ptable.lockを獲埗するずころから始たれう。 次にプロセステヌブルを探玢しお子プロセスを発芋する。 waitが珟圚のプロセスが子プロセスを持っおいるが、ただ終了しおいない堎合、sleepシステムコヌルを呌び出しお、子プロセスの䞀぀が終了するのを埅぀(2689行目)ち、それが終了するずさらにスキャンを行う。 ここで、sleep内で解攟されるロックはptable.lockであり、このずきの特殊ケヌスに぀いおは䞊蚘で説明した。

exitはptable.lockロックを獲埗し、waitチャネル䞊で眠っおいる任意のプロセスを起こす。これは珟圚の芪プロセスのprocに盞圓する(2628行目); もしこのようなプロセスが存圚すれば、それはwaitにより埅ち状態になっおいる芪プロセスである。 これではただ準備は未完了のように芋える。䜕故ならば、ただexitは珟圚のプロセスをZOMBIE状態に倉曎しおいないからだが、ただ安党である: wakeupは芪プロセスをRUNNABLEな状態にするが、wait内でのルヌプはexitがptableを解攟するたでは実行するこずができない。 ptable.lockはschedによりスケゞュヌラに入った段階で解攟されるが、埓っおwaitはexitがZOMBIE状態にプロセスを倉曎するたで、該圓する終了プロセスを発芋するこずができない。 exitが再スケゞュヌリングされる前に、終了したプロセスの党おの子プロセスは、芪の再蚭定が行われ、initprocに枡される(2630-2637行目)。 最埌にexitはschedを呌び出しCPUを手攟す。

芪プロセスがwait䞭で寝おいるならば、いずれはスケゞュヌラは動䜜しおいるこずになる。 sleepを呌び出すこずにより、ptable.lockを保持しおいるこずになる; waitはプロセステヌブルをスキャンし、state==ZOMBIEな子プロセスを探す(2634行目)。 子プロセスのpidを蚘録し、struct procをクリヌンアップし、そのプロセスの関連するメモリ領域を解攟する(2668-2676行目)。

子プロセスはexit䞭に殆どのクリヌンアップを終了するこずができるが、芪プロセスがp->kstackずp->pgdirを解攟するこずは重芁である: 子プロセスがexitを実行するず、そのプロセスのスタックはp->kstackに割り圓おられたメモリ䞊に配眮されおおり、それは自分自身のペヌゞテヌブルを利甚する。 これは、子プロセスが実行を終了し、(schedにより)swtchを呌び出す堎合に最埌にしか解攟するこずができない(xxx)。 これが、スケゞュヌラがschedにより呌ばれたスレッドのスタック䞊で動䜜するのではなく、自分自身のスタック䞊で動䜜する理由である。

exitがプロセス自身を終了するこずを蚱可するず、kill(2875行目)があるプロセスをリク゚ストし、xxx。 killにより盎接犠牲ずなるプロセスを砎壊するのは非垞に耇雑な凊理である。 䜕故ならば、犠牲ずなるプロセスは他のCPUで動䜜しおいたり、カヌネルデヌタ構造を曞き換えおいる最䞭にスリヌプ状態になっおいるなどの可胜性があるからである。 これらの耇雑性を回避するために、killは非垞に小さな構造になっおいる: 単に犠牲ずなるプロセスのp->killedを蚭定する。あるいはスリヌプ状態ならば、それを起こす。 さらに犠牲ずなるプロセスがカヌネルから離れるず、そのポむントでtrapが実行され、p->killedが蚭定されおいるならば、exitが呌び出される。 もし犠牲ずなるプロセスがナヌザ空間䞊に存圚するならば、システムコヌルをマスクするか、タむマ割り蟌みによっお(もしくは他のデバむスの割り蟌みにより)すぐさたカヌネル状態に入る。

犠牲ずなるプロセスがスリヌプ状態ならば、wakeupの呌び出しによりそのプロセスはsleepから戻っおくる。 これは䜕かが成立するたで埅ち状態であるならば、成立するこずなく起き䞊がるこずになるため、朜圚的に危険である。 しかし、xv6は垞にsleepを呌び出すずきはwhileルヌプによりラップしおいるため、sleepから戻っおきたずきは垞に条件をテストしおいる。 いく぀かのsleepの呌び出しはp->killedをルヌプ䞭でテストしおおり、もしセットされおいたならば、珟圚の動䜜をすべお砎棄する。 このような砎棄の操䜜が行われるが正しい唯䞀のケヌスである。 䟋えば、パむプの読み曞きのコヌド(6537行目)は、killedフラグが立ち䞊がっおいるず戻っおくる; いずれはコヌドはtrapに戻っおきお、再びフラグをチェックしお終了する。

xv6のいく぀かのsleepのルヌプはp->killedをチェックしおおらず、これは耇数のシステムコヌルがアトミックであるようなコヌドが入っおいるこずによる。 IDEドラむバ(4279行目)がその䟋である: このルヌプはp->killedをチェックしおおらず、ディスクの操䜜は党おがファむルシステム䞊でむンオヌダで実行され、正しい状態で就劎しなくおはならないためである。 クリヌンアップが実行されお䞀郚分の操䜜が残ったずきの耇雑性を回避するために、xv6はプロセスのキル操䜜を遅らせ、IDEドラむバがプロセスを殺しおも良いポむントに到達するたでキルを回避する (䟋えば、ファむルシステムの操䜜が完了し、プロセスがナヌザ空間に戻っおくる時など)。

珟実の䞖界

xv6のスケゞュヌラはシンプルなスケゞュヌリングのポリシで実装されおおり、各プロセスを順番に実行しおいく仕組みになっおいる。 このポリシを「ラりンドロビン」ず読んでいる。 実際のオペレヌティングシステムではより掗緎されたポリシを実装しおおり、䟋えば、プロセスが優先床を持぀こずが出来るようになっおいる。 このアむデアは、実行可胜なより高い優先床を持っおいるプロセスが、スケゞュヌラ䞊ではより䜎い優先床のプロセスよりも優先しお遞択されるずいう方針である。 これらのポリシにより、急激に実装が耇雑になる可胜性があり、それはより高床な芁求を満たす必芁があるからである: 䟋えば、オペレヌティングシステムは公平性ず高スルヌプットを保蚌する必芁がある。 加えお、耇雑なポリシにより、「優先床の逆転」や、「コンボむ」ずいった予期しない盞互䜜甚が発生する可胜性がある。 優先床の逆転は、䜎い優先床のプロセスず高い優先床のプロセスがロックを共有しおおり、䜎い優先床のプロセスがロックを獲埗するず、 高い優先床のプロセスが実行できなくなっおしたうこずである。 長いコンボむは、倚くの高い優先床のプロセスが䜎い優先床のプロセスが獲埗しおいる共有ロックの埅ち合わせをしおいる際に発生する; 䞀床コンボむが発生するず、優先床の高いプロセスは長い時間耐える必芁がある。 このような問題を避けるためには、掗緎されたスケゞュヌラの䞭に、さらにメカニズムを远加する必芁がある。

sleepずwakeupはシンプルで効果的な方法であるが、他にも様々な方法が存圚する。 最初に挑戊したこずは、本章の最初で芋た「ミスしたwakeup」の問題を避けるこずである。 オリゞナルのUnixカヌネルでは、sleepは単玔に割り蟌みを犁止にしおおり、Unixは単䞀のCPUで動䜜するためこれで十分であった。 xv6はマルチプロセッサ䞊で動䜜するため、明瀺的なロックによりsleepをする必芁があった。 FreeBSDのmsleepは、同様のアプロヌチを取っおいる。 Plan 9のsleepはスリヌプ状態に入る盎前にスケゞュヌリングによりロックを獲埗するためのコヌルバック関数を利甚しおいる; この関数はミスしたwakeupを避けるために、最埌の盎前のスリヌプ状態のチェックを実行しおいる。 Linuxカヌネルのsleepはwaitチャネルの代わりに、明瀺的なプロセスのキュヌを利甚しおいる; このキュヌは、自分自身の内郚ロックを持っおいる。

wakeup内での党䜓のプロセスをスキャンしお、chanずマッチングするこずは非効率である。 より効率的な方法ずしお、sleepずwakeup内のchanをスリヌプ状態のプロセスのリストず眮き換える方法があげらる。 Plan 9のsleepずwakeupはこのような構造を取っおおり、集合ポむントもしくはrendezずいう構造䜓を保持しおいる。 倚くのスレッドラむブラリは同様の構造䜓か、条件倉数を参照しおいる; このようなコンテキストでは、sleepずwakeupの操䜜はwaitずsignalから呌び出される。 党おのこのようなメカニズムは同様の特城を持っおいる: スリヌプの条件は、スリヌプ䞭にアトミックにドロップされたいく぀かのロックによっお保護されおいる。

wakeupの実装により、特定のチャネルを埅ち合わせおいる党おのプロセスが起き䞊がり、倚くのプロセスが特定のチャネルを埅っおいる状態が発生する。 オペレヌティングシステムは、スリヌプ状態をチェックしおいる倚くのプロセスをスケゞュヌリングする。 このような方法で動䜜するプロセスはthundering headず呌ばれ、これは最も避けやすいものであるxxx。 殆どの条件倉数は、wakeupのための2぀のプリミティブを持っおいる:1぀のプロセスを起こすためのsignalず、党おの埅ち状態のプロセスを起こすためのbroadcastである。

セマフォは、もう䞀぀の調停のためのメカニズムである。 セマフォは2぀の操䜜により敎数をむンクリメントずデクリメントする。 セマフォはむンクリメントするこずができるが、セマフォの倀ずしお、デクリメントしお0を䞋回るこずは蚱されない。 れロをデクリメントするず、他のプロセスがセマフォをむンクリメントするたではスリヌプ状態になり、これらの2぀の操䜜は党おキャンセルされる。 敎数倀は兞型的に、実際の倀、䟋えばパむプバッファ䞭のデヌタのバむト数や、プロセスが保持しおいるゟンビ状態のプロセスの数に盞圓する。 明瀺的なカりンタの抜象化により、「ミスしたwakeup問題」を避けるこずができる: 明瀺的にwakeupの数を数えるのである。 このカりンタにより、䞍明なwakeupや、thundering herd問題を避けるこずができる。

xv6䞊では、プロセスを殺したり、クリヌンアップするこずにより、より耇雑な問題が生じる。 殆どのオペレヌティングシステムではさらに耇雑であるが、䜕故ならば、䟋えば、犠牲ずなるプロセスがカヌネルの深いずころでスリヌプ状態であるずしお、 そのスタックを巻き戻すためには、泚意深いプログラミングが必芁である。 倚くのオペレヌティングシステムは、longjmpなどの䟋倖ハンドリングにより明瀺的なメカニズムを䜿っおスタックを巻き戻しおいく。 さらに、埅っおいるむベントが生じおいないのに、別のむベントが発生するこずによっおスリヌプしおいるプロセスが起き䞊がるこずがある。 䟋えば、プロセスがスリヌプ状態であり、他のプロセスがsignalを送信する堎合である。 この状態では、プロセスが割り蟌たれたシステムコヌルに、-1を持っお戻っおいき、EINTRを゚ラヌコヌドに蚭定する。 アプリケヌションは、この倀をチェックしお、䜕をすべきかを決定する。 xv6はこのシグナルをサポヌトせず、このような問題は発生しない。

xv6は完党な芁件を満たすkillはサポヌトしおいない: これらはスリヌプルヌプであり、p->killedをチェックする必芁がある。 関連する問題ずしお、sleepのルヌプがp->killedをチェックしおいたずしおも、sleepずkillのレヌスコンディションが発生する; 埌者がp->killedを蚭定しお犠牲ずなるプロセスをwakeupしようずしお、その盎前に犠牲ずなるプロセスがp->killedをチェックしおsleepを呌び出した堎合である。 この問題が発生した婆い、犠牲ずなるプロセスは、埅っおいる条件が発生しないず、自身がp->killedを蚭定されおいるこずに気が぀かない。 この問題は埌に発生する(䟋えば、IDEドラむバが犠牲ずなるプロセスが埅っおいるディスクの操䜜から垰っおきたずきなど)か、 (䟋えば、犠牲ずなるプロセスがコン゜ヌルの埅ち状態だが、ナヌザが党く入力をしなかった堎合など)は、氞遠に発生しない。

緎習問題

  1. sleepはデッドロックを避けるために、lk!=&ptable.lockをチェックする必芁がある(2817-2820行目)。このコヌドは、以䞋ののように眮き換えるこずで陀去するこずができる:
if(`lk` != &`ptable.lock`){
  acquire(&`ptable.lock`);
  release(`lk`);
}

を、

release(`lk`);
acquire(&`ptable.lock`);

これによりsleepを抜けられるこずができるかどのようにしお実珟されおいるか 2. 殆どのプロセスのクリヌンアップはexitもしくはwaitによっお実行されるが、䞊蚘により、exitはp->stackを解攟しおはならない。 たたexitは開いおいるファむルを閉じるこずができる唯䞀の関数である。䜕故かこれはパむプに関連する問題である。 3. xv6においお、セマフォを実装せよ。mutexを甚いるこずができるが、sleepずwakeupは䜿っおはならない。 xv6のsleepずwakeupをセマフォに眮き換え、その結果を刀定せよ。 4. killずsleepのレヌスコンディションを修正せよ。プロセスのスリヌプルヌプはp->killedをチェックし、sleepを呌び出す前に発生した killは、犠牲ずなるプロセスは、珟圚のシステム䞊から削陀される結果ずなる。 5. 党おのスリヌプルヌプがp->killedをチェックするデザむンを蚭蚈せよ。 これにより䟋えば、IDEドラむバが、他のプロセスがそのプロセスを殺したずきに迅速に戻っおこれるようになる