検索
特集

プロセスディスパッチャの実装――プロセススケジューリング(その2)UNIX USER 2004年6月号「Linuxカーネル2.6解読室」より転載

プロセスが切り替わるイメージを持つことはなかなか難しいため、実際のプロセスディスパッチャのコードを少しのぞいてみることにしましょう。ここでは、Intel x86用Linuxのコードを参照します。

PC用表示 関連情報
Share
Tweet
LINE
Hatena

 プロセスが切り替わるイメージを持つことはなかなか難しいため、実際のプロセスディスパッチャのコードを少しのぞいてみることにしましょう。ここでは、Intel x86用Linuxのコードを参照します。

context_switch関数

 Linuxカーネルのプロセスディスパッチャのコードは、context_switch関数にあります(リスト1)。context_switch関数は、プロセス空間の切り替え処理(switch_mm関数1)と、各種レジスタの切り替え処理(switch_to関数2)から成ります。プロセス空間の切り替え処理を効率化するために、その前後では様々な処理を行っています。これらの処理については、「空間管理」の回で詳しく説明します。

*** 一部省略されたコンテンツがあります。PC版でご覧ください。 ***

switch_to関数

 リスト2のswitch_toマクロ関数が、プロセス切り替え処理の中で一番の核心となる部分です。switch_to関数は、各種レジスタの切り替えを行います。このレジスタ切り替え処理をもう少し詳しく見てみます。とくに、EIPレジスタ(命令ポインタ)とESPレジスタ(スタックポインタ)の切り替えに注目してください。

*** 一部省略されたコンテンツがあります。PC版でご覧ください。 ***

 引数prevは、現在動作中のプロセスのtask_struct構造体を指しています。引数nextは、次に動作させるプロセスのtask_struct構造体を指しています。task_struct構造体中のthread.eipメンバーは、EIPレジスタを保存する領域です。また、thread.espメンバーは、ESPレジスタを保存する領域です。

 switch_to関数は、prevで示されるプロセスのESPレジスタを退避し(5)、nextで示されるプロセスのESPレジスタを復帰しています(6)。これら処理によって、プロセスのスタック領域の切り替えが実現します。

 プロセスprevが次回動作を再開するとき、(10)の個所から処理を行うようにEIPレジスタの退避域(thread.eip)に(10)のアドレスを退避します(7)。次に動作するプロセスnextの実行再開個所は、nextのレジスタの退避域(thread.eip)から取り出したEIPレジスタの値をスタック上に置いておきます。__switch_to関数を呼び出した後、__switch_to関数(9)から戻るとき、スタック上に積んだEIPレジスタの値が自動的にCPUに読み込まれます。その瞬間、プロセスnextの中断地点から、CPUは実行を再開することになります。

 またしばらく後に、プロセスprevは再度選択され、実行権が与えられます。そのときは、先ほどプロセスprevの復帰アドレスとしてthread.eipに退避した個所(10)から実行を再開します。

 ここのコードだけ見ていると、すべてのプロセスが同じ地点(10)から処理を再開することになるように見えますが、実はいくつか例外があります。forkシステムコールによって生まれたばかりの子プロセスは、forkシステムコールの後半処理から動作を始めます。この子プロセスの__switch_to関数からの戻り番地として、forkシステムコールの後半処理を開始アドレスとしてEIPレジスタの退避域(thread.eip)に登録しておきます。子プロセスはswitch_to関数の前半の処理(3〜9)を実行していません。突然__switch_to関数の中からわき出てきたように動き始めます。

 また、生成されたばかりのカーネルスレッドも同様に、独自の番地から実行を開始するようにEIPレジスタの退避域(thread.eip)を設定しています。カーネルスレッドも__switch_to関数の中からわき出てきたように動き始めます。

__switch_to関数

 __switch_to関数(リスト3)は、EIPとESP以外のレジスタの切り替えを行います(9)。プロセス切り替え処理を少しでも効率化するために、必要のないときはなるべくレジスタへのアクセスを減らす努力がなされています。最も特徴的な個所は、FPUレジスタ(浮動小数点演算レジスタ)の遅延切り替え機能です。

*** 一部省略されたコンテンツがあります。PC版でご覧ください。 ***

 プロセス切り替えでは、FPUレジスタの値も切り替える必要があります。__unlazy_fpu関数は、そのための関数です(13)。__unlazy_fpu関数は、呼び出された瞬間にはFPUレジスタを切り替えません。FPUレジスタには大きなレジスタが複数存在し、レジスタ値の退避/復帰にかかるコストが大きいため、少々トリッキーな手段を使って性能劣化を緩和しています。

 具体的には、__unlazy_fpu関数は、プロセスprevがFPUレジスタを利用している(プロセスprevが実行権を得てから、実行権を手放すまでにFPUレジスタを利用した)場合、プロセスprevのFPUレジスタ値の退避を行います。しかし、プロセスnextのFPUレジスタ値の復帰は行いません。その代わりに、FPUレジスタへのアクセスを禁止状態に変更します。この状態でプロセスnextがFPUレジスタにアクセスしようとすると、例外が発生し、その例外処理中にFPUレジスタの復帰処理を行います。プロセスnextが、FPUレジスタにアクセスしなかった場合は、例外が発生せず、FPUレジスタの復帰処理を省くことができます。FPUレジスタの復帰処理は、この先で浮動小数点演算を行うプロセスが動作するときまで行われません。実際にFPUレジスタを利用するプロセスは限られているため、FPUレジスタの切り替え回数を大幅に減らすことができます。

 ところで、一見FPUレジスタの退避処理も遅延させられそうに思えますが、なぜ遅延させていないのでしょうか? 理由の1つは、マルチプロセッサシステムの場合、このプロセスprevがほかのCPU上で再スケジューリングされてしまうことがあるためです。仮にそうなると、異なるCPU上のFPUレジスタ群をプロセスprevのもので初期化する必要があり、その処理は非常に苦労することになります。

汎用レジスタの退避

 ここまで見てきて気が付いた人もいると思いますが、汎用レジスタはいつ切り替えたのでしょうか? 実は、Intel x86ではこのタイミングで切り替えることは不要です。必要な情報はすでにスタック上に退避されています。もちろんこれはCPUアーキテクチャ依存(もちろん、コンパイラにも依存)です。関数呼び出しにおいて、呼び出された側でレジスタ値が壊れないことを保証しなければならないアーキテクチャである場合、それらのレジスタ値を退避する必要があります。

 この節では、Intel x86アーキテクチャ用のプロセス切り替えのコードを見てきました。このプロセス切り替えのコードは、CPUアーキテクチャごとに用意されています。しかし、その本質はIntel x86のものと同じです。Intel x86のコードと同じ視点から、ほかのCPUアーキテクチャ用のコードも理解することが可能だと思います。


 次回はプロセススケジューラについて解説していく。

*** 一部省略されたコンテンツがあります。PC版でご覧ください。 ***

Copyright(C) 2010 SOFTBANK Creative Inc. All Right Reserved.

ページトップに戻る