カーネル(その3)
今回話す内容は次の2つ。
○ 物理アドレス全体のマッピング
○ プロセススイッチの仕組み
尚、最新のソースは、
http://sourceforge.jp/projects/onix/
の subversion リポジトリに置いてあり、不定期に更新する。
○ 物理アドレス全体のマッピング
前回、startup ルーチンにおける仮のページングの話をした。今回は、物理アドレス全体のページを対象としたページテーブルの初期化及び有効化の話をする。
前回話した仮のページングでは、物理メモリの下位 8 MB をリニアアドレス 0x00000000 - 0x00800000 と 0xC0000000 - 0xC0800000 にマッピングさせ、この領域に対するアクセスのアドレス変換を行わせるものであった。
今回話すページングは、マシンに搭載されている物理アドレス全体を 0xC0000000 から始まるリニアアドレスにマッピングさせる。何故、0xC0000000 (以下、ベースアドレス) なのかというと、ベースアドレスより上位のメモリ空間をユーザープロセスが使用する領域とす為である。別に 0xC0000000 という場所に特別な意味は無い。単純に、ユーザー空間とカーネル空間という概念的な区切り場所として分かりやすい場所である為である。実際に、0x80000000 で区切る OS の一つが Windows ある。
さて、物理アドレス全体をマッピングさせるわけなので、その実メモリの容量を知らなければならない。これを調べる為の方法が、int 0x15 (0xE801) の bios コールである。当然これはリアルモード中に調べなければならない。ブートローダで調べた実メモリの容量を、カーネルが知る為に、ブートローダがカーネルパラメータを作成し、カーネルに渡さなければならない。
onix では、物理アドレス 0x00080000 番地をカーネルパラメータの予約領域として、カーネル側でこれを参照する事でこれを実現する。
startup ルーチンにおける仮のページングでは、ポピュラーな 2 段のページング機構を利用していたが、onix のカーネルでは拡張ページを扱う。この為、cr3 が指すページディレクトリが直接ページを参照する。
拡張ページを扱う場合、CPU に対して次の 2 つの手続きが必要になる。
・CR4 レジスタの 5 bit目のページサイズ拡張ビットを立てる
・各ページディレクトリの下位 8 bit目の PS フラグを立て、直接ページを参照させる。
実際に試してはいないが、手続きが二つあるという事は部分的に通常ページを利用できるのだろう。
しかし、上述した処理を実行し、ページディレクトリのアドレスを cr3 レジスタに登録しても、それを実行した直後に処理が停止する。これは、cr4 レジスタ以外のレジスタがアドレスをリニアアドレスとして解釈する為である。
つまり、ブートシーケンスで初期化・登録した gdtr はリニアアドレスを参照する為、リニアアドレスの先頭付近を参照し、アドレス解決が出来きなくなる為に引き起こる。通常、この設定はカーネルが呼び出されてから、再度設定処理を行うのだが、リニアアドレスの最初の 1 ページ分を物理アドレスの先頭 1 ページにマッピングさせれば話が済むので、リニアアドレスの先頭ページをカーネルの予約領域として扱う事にする。
これで、物理メモリを支配的に扱えるようになった。
○ プロセススイッチの仕組み
プロセスの概念自体、まだ onix には存在しないのだが、マルチプログラミングを実現するカーネルの仕組みについて考えてみる。
これを実現する最も低レベル(*)な処理は、やはりプロセススイッチであろう。
(*) レイヤが低いという意味
プロセススイッチによって、実行中のプロセスが中断され、別のプロセスのプロセスが CPU を使う。そして一定時間経過してから、またプロセスに処理が切り替わる。
これをまともに動かす為には、以下の 2 つの仕組みが必要になる。
・各プロセスの状態を保存するデータ構造
・プロセス・スイッチ
ここでは、2 つ目に挙げたプロセス・スイッチの実現方法について考えてみる。
実行中のユーザープロセスの処理を切替えるという事は、次の 2 つを切替える事を意味する。
・ページテーブルの切替え
・プログラムカウンタの切替え
ページテーブルは、プロセスの付属情報としてページテーブルを持たせ、切替え時に cr3 レジスタをそれに書き換えればよい。
intel ではプログラムカウンタを切替える cpu 命令として、jmp 命令と ret 命令が存在する。
ここで、linux におけるプロセススイッチの実装を覗いてみる。linux におけるプロセススイッチの処理は、include/asm-i386/system.h でマクロ定義される switch_to で行われる。
14 #define switch_to(prev,next,last) do { \
15 unsigned long esi,edi; \
16 asm volatile("pushfl\n\t" /* Save flags */ \
17 "pushl %%ebp\n\t" \
18 "movl %%esp,%0\n\t" /* save ESP */ \
19 "movl %5,%%esp\n\t" /* restore ESP */ \
20 "movl $1f,%1\n\t" /* save EIP */ \
21 "pushl %6\n\t" /* restore EIP */ \
22 "jmp __switch_to\n" \
23 "1:\t" \
24 "popl %%ebp\n\t" \
25 "popfl" \
26 :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \
27 "=a" (last),"=S" (esi),"=D" (edi) \
28 :"m" (next->thread.esp),"m" (next->thread.eip), \
29 "2" (prev), "d" (next)); \
30 } while (0)
ミソは、21 行目と 22 行目である。
通常、"call foo" のように関数を呼び出した場合、呼び出し側の次の命令アドレスをスタックに積み、リンカが解決した関数シンボルのアドレスにジャンプする。
関数側では ret 命令によって、スタックにつまれた戻りアドレスに対してジャンプする。
switch_to ではこれを利用し、スタックにスイッチするプロセスの命令アドレスを積み、__switch_to 関数 [arch/i386/process.c] にジャンプする。
__switch_to 関数内で cpu レジスタの退避等を行い、ret 命令を実行する。ret 命令によって切替えたプロセスの処理が行われる。
実に秀逸なこの動作を実現させる為には、プロセスのデータ構造がきちんと設計されていなければ当然できない上、fork や exec といった処理を実装しなければまともなプロセス管理はできない。
なので、これを実装するのはまだ先の話になりそうだ。
- Category(s)
- 学習経過
- The URL to Trackback this entry is:
- http://dev.ariel-networks.com/Members/ohyama/30ab30fc30cd30eb-305d306e3/tbping