2014/12/07

自作OSでのプロセス実装について (2) ~初めてのユーザプロセス~

この投稿は Aizu Advent Calendar 2014 の 7 日目の記事です。

前の人 @masaponto
次の人 @MiZuKi_Sonoko

また、自作OS Advent Calendar 2014 の 7日目の記事でもあります。


自作OSでのプロセス実装について (1) ~初めてのユーザプロセス~の続きです。

前回の内容
  • プロセスとは?
  • x86リングプロテクション(ユーザモードとカーネルモード)
  • マルチタスク
  • 割り込み
  • コンテキストスイッチ
を一気に説明しました。
正直、多少知識が無いとわからないよなあって感じになっているのでその点は反省しております。

今回は

  • コンテキストスイッチの実装
  • ユーザプロセス起動

を見て行きたいと思います。

前回はタイマ割り込みによってコンテキストスイッチが起きる、というところで終わったと思います。
そこの流れが大事ですので再掲します。

タイマ割り込み発生
実行中プロセスは割り込まれる
(中断される)
カーネルモード突入
今のカーネルモードスタックへ
今の実行コンテキストを保存
(CPUのレジスタとか)
次のプロセスを選ぶ
次のカーネルモードスタックから
次の実行コンテキスト復元
ユーザモードに復帰
はい、こんな感じです。
(※カーネルスタックではわかりづらいのでカーネルモードスタックに変更します。)

では、早速ですがもぷりんOSのコンテキストスイッチ実装を見て行きましょう。
この部分は主にLinuxの実装を真似ています。
と言っても、x86上で同じことをやろうとしたら大体こうなると思います。。。

リポジトリはこちら
mopp/Axel

上記の流れに沿ってコードを掲載していきます。
まずは、一番はじめに呼ばれる割り込みコードを見てみましょう。
以下のこれは、割り込みエントリ関数で、タイマー割り込み以外のキーボード割り込みや一般保護例外などでも呼ばれる部分です。

上記コードはsrc/interrupt_asm.asmにあります。
ただし、Gistに貼った方は見やすさのためにマクロなど全て展開済みです。

レジスタ操作があるのでC言語では書けません。ですのでアセンブラを使用しています。
asm_interrupt_timerという関数がカーネル初期化時にCPUのタイマ割り込みハンドラに登録されます。
ですので、タイマ割り込みが発生したらCPUが勝手にasm_interrupt_timerを呼び出してくれます。

ここで大切なのは、割り込みが発生した際、CPUが自動的にカーネルモードに切り替えるということです。
これは前回も書いたかもしれないですが、きちんと書いていないので、ちゃんと説明します。
詳細は、仕様書: IA-32 インテル ® アーキテクチャ ソフトウェア・デベロッパーズ・マニュアル 下巻:システム・プログラミング・ガイドの5.12.1を参照してください。
同じことを書いています。

割り込まれる状況というのは、カーネルモード時割り込みとユーザモード時割り込みが有ります。
前回、スタックも切り替わると書きましたが、これはユーザモード時割り込みの時だけです。
そもそもとして、カーネルモードだったら、カーネルモードスタックになっているに決まってるんだから切り替える必要はないですよね。

以下の図を見てください。
ユーザモード時割り込みのスタック切り替え
以前動作していたコンテキストの一部がカーネルモードスタックに保存されます。
ここで特に大事なのは、eip, cs, esp, ssですね。割り込みから戻るときにこれらを使って戻ります。csとssを説明する気力がないのでWikipediaに投げます…
ここは、混乱しがちですので注意してください。
割り込まれた時のスタックでは無く、カーネルモードになってからのスタック(カーネルモードスタック)です。
カーネルモード時割り込みの場合は、以前のespとssは変わらないから不要ですので、error codeからeflagsまでがカーネルモードスタックに保存されます。

さて、asm_interrupt_timerに戻りましょう。
まず、タイマ割り込みにはエラーコードが無いので擬似的に設定します。
これは後々出てくる構造体のためです。
次に、割り込み前の状態を維持し、正しく復帰するために汎用レジスタなどを保存します。
そして、タイマ割り込みハンドラ本体を呼び出します。
この時に、pushしているespを、"後々出てくる構造体のポインタ"として、関数に渡しています。

本体の処理が終わったら、レジスタ状態の復元と擬似エラーコードの削除をします。
一番最後のiret命令これが、割り込み復帰処理です。
正しく元々実行していたところへ帰ってくれます。
その際、以前のespやeipが必須になるわけです。

次にinterrupt_timer関数ですが、これはsrc/time.cにあります。
でも、実際、下記の関数を呼び出しているだけなので省略。

上記コードはsrc/proc.cにあります。

このコードでは、プロセスの切り替えを行っています。
スレッド実装したかったので、プロセス構造体無いのスレッド構造体に色々入っています。

currentが現在実行中のプロセスで、nextに実行可能なプロセスを持ってきます。

ここでキモなのは、インラインアセンブラの部分です。
30, 31行目で現在プロセスが復帰した時のeipとespを保存しています。
なので、次に実行されるときは、next_turnから実行されることになります。

次に、切り替え先プロセスの情報を復元します。ここが一番大事です。
34行目でespを切り替えています。
これはまあ普通ですね。

次はeipを切り替えます。
ですが、eipに対してmov命令を実行することは出来ません。
なので、スタックに入れて、やります。
そして、call命令ではなくjmp命令で関数に移動します。
すると、関数の終了時にはret命令が呼ばれるはずです。
この、ret命令はスタックから戻り先アドレスをeipに復元します。
これを利用しています。
なので、change_contextから戻った時点で、既にプロセスは切り替わったことになります。

なお、ここでchange_contextに渡す引数はレジスタ渡し、fastcall規約で渡しています。
(Linuxのを真似してみたんですが、現状fastcallである必要はあまりない…)

今の流れを延々と繰り返して、マルチタスクが実現されているわけですね。


やっとコンテキストスイッチの説明が終わりました。(長すぎた)
初めてのユーザプロセス起動を見て行きましょう。
上記コードはsrc/proc.cにあります。

この関数はプロセス初期化中に呼ばれます。
まず、プロセスを一つ確保します。
そして、textセグメントとstackセグメントを確保します。(dataセグメント忘れてた…)

この次の15行目、これがポイントです!
thread.ipにinterrupt_returnを設定しています。
先のswitch_contextではthread.ipを次の復帰先として、使っていました。
つまり、このプロセスは次にswitch_contextによって切り替えられた時に、interrupt_returnから実行が始まるのです。
そして、interrupt_returnでは汎用レジスタの復元、そして、iret命令を実行しています。
iret命令では上図(ユーザモード時割り込みのスタック切り替え)のようなスタックを読み取ってユーザプロセスを再開しています。

つまり、あたかも割り込みから戻ったようにカーネルモードスタックを調節してやればいいのです!

18〜32行目でそれを行っています。
ここで使っているInterrupt_frameが後に出てくる構造体です。
これを使って、割り込み時のスタックをいい感じに参照、変更することが出来ます。
eip, prev_espが上図の前の〜〜に対応するものです。
これをプログラムの実行開始アドレスにしてやればいいわけです。
もぷりんOSの現在のinitプログラムでは実行開始アドレスはDEFAULT_TEXT_ADDRです。
この辺は実行プログラムによります。

そして、48行目で、switch_context内のnext_procで拾えるように、実行可能状態にします。

あとは、普通にスケジューラが動き出すのを待つだけです。
初期ユーザプロセスの起動はこれで完了になります。


時間があれば、forkとかexecの話ももっとちゃんと書きたいなと思っています。
それでは。

2014/12/05

自作OSでのプロセス実装について (1) ~初めてのユーザプロセス~

この投稿は Aizu Advent Calendar 2014 の 5 日目の記事です。

前の人 @i__yahoo
次の人 @masaponto

また、自作OS Advent Calendar 2014 の 5日目の記事でもあります。

Aizuの方に登録してたのですが、ネタ的に自作OSだし、自作OSカレンダーの方は空きが激しいので、まとめて登録してしまいました。
今回は自作OS Advent Calendar的に先日の記事の続きみたいなものです。
また、ALTで話した内容をもっとわかりやすく書いたものです。(次回のALTもあるらしいので興味のある人はぜひに)


この記事では、もぷりんOSの現在の実装における観点から話を進めていきますので、LinuxなどのOSとは異なることもあると思いますので注意してください。
特にもぷりんOSは権限周りなどがまだまだですし、私もよくわかって無いことが多いです。それだと危なくない?みたいなことがめっちゃ有ります(つまり、吹けば飛ぶ)
それに、趣味で楽しいからやっていることです。(だからといってセキュリティを軽視しているわけではないです。安心安全もぷりんOSを目指しますよ)
なので、あんまり細かいことは言わず、こんな風に書いてれば動くんだふーん程度の気持ちでお願いしたいです。
そして、何より自作OSを作ってる人、少し興味を持ってる人が見て、こんなの俺でもできるじゃねーかと感じて、自作OS作成してくれればなあと思いつつ書いていきます。


さてさて、本題に入るその前に、通常話しているプロセスについていくつか事前知識として、さらっと話そうと思います。
詳しいことはプロセス-Wikipediaにどうぞ
大雑把に言うと、プロセスは実行されているあるプログラムのことです。
もぷりんOSは一応?UNIX系のOSなので、ここでいうプロセスはUNIX系のプロセスを指しています。UNIX系プロセスは親子関係を持ちます。この親子関係というのはデータ構造で言うところの木構造で、大体こんな感じです。
数字はプロセスID(pid)を表します。
プロセス0がルートプロセス、その子プロセスがプロセス1
この時、プロセス1から見れば親はプロセス0になります。

当たり前ですが、プロセスの生成と削除はOSでは頻繁に行われます。
例えば、目の前のターミナルでlsコマンドを実行したとしましょう。
その瞬間、シェルが子プロセスlsを生成し、lsが実行されます。
色々表示した後に、lsは終了し、プロセスは削除され、元のシェルに戻るというわけです。
ここで、シェルのプロセスの親は何でしょうか、更にその親は?それのまた更に親は?
という疑問が湧いてきます。

と、いうことで本題です。
上図のルートプロセスであるプロセス0とその子であるプロセス1の話です。
プロセス識別子-Wikipediaからの引用ですが、
Unix系OSでは、プロセス識別子 0 と 1 は特別なタスクを指している。プロセス識別子 0 は swapper または sched と呼ばれ、ページングを担当している。これは実はカーネルの一部であり、ユーザーモードのプロセスではない。プロセス識別子 1 は init プロセスで、主にシステムの立ち上げとシャットダウンを担当している。
と、言うわけです。

もぷりんOSでもプロセス0はOSカーネル自体の事を指します。
ハードウェア(CPU,ディスク,マウス,キーボードなど)の初期化を主に行っています。
それらの初期化を終えた後に初めてのユーザプロセスであるプロセス1を立ち上げるわけです。

ここで、ユーザモードという言葉が出てきました。
これは大事です。テストに出ます。
このモードというのはIntel CPUの持つリングプロテクションのものです。
もぷりんOSはx86のみのサポートなのでこれ以降、x86前提で話していきます。
カーネル管理下の領域にはハードウェア情報やらなにやらがあり、誰でもアクセス可能だったら、めちゃくちゃ困りますし、セキュリティもあったもんじゃありません。なので、カーネルとユーザを分離する必要があります。そのための機構がリングプロテクションで、以下のような図で表されます。

x86リングプロテクション
数字が小さいほうが権限が高く、大きい方が権限が低いです。
x86では4つのリングがありますが、もぷりんOSでは0と3しか使っていません。
LinuxやFreeBSDも基本的に同様です(XenでRing1を使うとかなんとかあるらしいですが)
そして、このRing0をカーネルモード、Ring3をユーザモードと言っています。
このリングは外側のリング領域全てにアクセス可能です。
つまり、Ring0なら全てにアクセス可能で、Ring3なら外にはもう無いので、自分自身だけです。
カーネルはユーザに干渉できて、ユーザはカーネルに干渉出来ないことなります。
この機構はOSの基本的なセキュリティにぴったりですね。

と言ってみましたが、全くアクセス出来なくてはそれはそれで困ります。
上記で既に言ったように、カーネル領域にはハードウェア情報などがあります。
全くアクセス出来ないのではユーザプログラムはハードウェア資源を使えないことになってしまいます。ですので、一定の手順を踏むとユーザモードからカーネルモードに切り替わることが出来ます。イメージとしてはリングの内側にジャンプする感じですね。
そして、そういったカーネルプログラムをユーザプロセスが実行出来るようになるわけです。そのためのインターフェースがシステムコールというやつです。
ユーザプログラムはシステムコールを発行し、リング内側のカーネルに使いたい機能を要求して、色々と処理をしていくわけです。このとき、実行中プロセスはユーザプロセスですが、実際に動いているのはカーネルプログラムなので、OSが処理をしているのと変わりません。


かなりてきとうですが、大まかにユーザモードとカーネルモードについて説明しました。
話を戻しますと、起動時にはカーネルしか無いわけです、ユーザプログラムは一切存在しません。つまりカーネルモードプロセス一個のみです。
そして、今やりたいことは、それとは別のユーザプロセスを起動することです。
ここで暗に言っていますが、別のプロセスを起動することは、2つのプログラムを並列実行させることです。
これは何を隠そうマルチタスク-wikipediaです。
一般的に、CPUの計算処理時間に比べ、ディスクやネットワークの処理時間は数十から数百倍かかる。シングルタスク環境では、逐次処理が行われるため、入力待ちや通信待ちなど、CPUが計算を実行できずに、待つ時間が発生する。マルチタスクの導入によって、これらの待ち時間の間にCPUを動作させ別の計算を行い、全体の処理時間の短縮を実現することが可能になる。
またもやWikipediaからの引用です。
別の計算を行うということが、つまり別のプロセスに切り替えるということです。
この切り替えることをコンテキストスイッチと言います。
CPUの持つ汎用レジスタ(eax, ecx, ebx, edx)や現在のスタック(esp)やインストラクションポインタ(eip)などのコンテキストを全て切り替えるためにコンテキストスイッチと言われます。もちろん、それを実行出来るのはカーネルモードのみです。
図にしてみるとわかりやすいかと思います。
プロセスがいっぱいあるときも同様です。

もぷりんOSにおいて、コンテキストスイッチはタイマー割り込みによって生じます。
上記リンクのプリエンプティブ・マルチタスクというやつです。
これはタイマー割り込みによって、一定時間が経過すると、次のプロセスに切り替わる処理が実行されます。また、割り込みの処理をするもの(割り込みハンドラ)は全てカーネルが提供するものです。ゆえに、この時、実行していたプロセスはカーネルモードに切り替わって、コンテキストスイッチを実行します。
具体的には10ms秒ごとに割り込まれます。結構早いように思えますが、CPUの周波数からすればある程度プログラムを実行するには十分です。
コンテキストスイッチの流れを以下に図示します。
これはかなり重要です。マルチタスクの肝と言ってもいいでしょう。
以下の流れで動作します。

タイマ割り込み発生
実行中プロセスは割り込まれる
(中断される)
カーネルモード突入
今のカーネルスタックへ
今の実行コンテキストを保存
(CPUのレジスタとか)
次のプロセスを選ぶ
次のカーネルスタックから
次の実行コンテキスト復元
ユーザモードに復帰

ここで出てきた、プロセスのカーネルスタックとは、それぞれのプロセスがカーネルモード時に使用するスタックのことです。
先程は説明しませんでしたが、カーネルモードに切り替わるときに、スタックもカーネルスタックに切り替わります。またユーザプログラムのコンテキストも退避されます。これはx86の仕様です。上で同じことを言いましたが、雰囲気としてはカーネルに化けるような感じかなと思っています。なお、カーネルスタックはプロセスを生成する過程で設定されます。
ここで、この一連の割り込みの流れを、ユーザプログラムの立場から見ると、割り込まれた時、カーネルプログラムに移り、処理再開される時にはきっちり割り込まれた部分から再開するので、割り込まれたことすらわかりません。これによってユーザはマルチタスクのことなど気にせずにプログラミングができるし、我々も普段からしているというわけです。
やってみるとわかりますが、当たり前の事のように行われているのに裏では大変なんだなとしみじみ思います。



さて、だいぶ長くなってしまいました、疲れてしまったので今回の記事はここまでとします。思ったより大変な記事になりそう…
(2)も数日中に書いて公開します。

2014/12/03

[自作OS Advent Calendar 2014] 自作OSの紹介的なの

この記事は「自作OS Advent Calendar 2014」3日目の記事として書かれました。


まずはじめに、上記リンクを見ていただければ直ぐにわかりますが、自分が主催にもかかわらず1,2日が空きで書いてないです。これについて申し訳ありません。(言い訳ですが多忙につき書いていられませんでした)。

さて、とりあえず、何でもいいから書かないと始まらないということで、3日目として、現在開発中の自作OSの紹介的な話をしたいと思います。

開発リポジトリはこれ
https://github.com/mopp/Axel



名前は"Axel"と書いて、"あくせる"と呼んでいます。
通称"もぷりんOS"です。(余談だけど、Mopris(もぷりす)って案もあった)
イメージカラーはメタリックレッドです。この時点で、名前の由来がわかる人にはわかるかもしれませんね。

きっかけとしては、元々Linuxとかの中身が気になっていたということと、自作OS入門を読んで作って見たいなーと思っていたからです。
あと、セキュキャンに応募して2回落とされているので、それも開発モチベーションとして一つあります。来年も応募します(来年で最後なので行きたいんですよかなり)

具体的な話に移ります。
この記事を書いている時点のOSスペックは以下です。


  • モノリシックカーネル
  • ソースコード長さ 8000 行
  • ソースコード容量 250 KB
  • 実行ファイル容量 105 KB
  • 対応アーキテクチャ x86_32
  • 最低動作メモリ 12MB
  • 使用言語 C, nasm
  • 参考OS Linux, FreeBSD, Unix v6, xv6, HariboteOS

実装されている機能は以下です。


  • 物理メモリ管理 (BuddySystem)
  • x86ページング
  • 仮想メモリ管理 (Two Level Segregated Fit Allocator)
  • 割り込み
  • PS/2マウス、キーボード
  • VBEグラフィック
  • ATAデバイスアクセス
  • FAT32/16 ファイルシステム
  • ACPI(シャットダウンのみ)
  • システムコール
  • ELF形式ロード (静的のみ)
  • プロセス管理 (fork, execのみ)
  • プロセススケジューリング(ラウンドロビン)

環境にはQEMUとBochsを使ってます。
Bochsはデバッガが良い感じなのでおすすめです。
スタック、レジスタ、ページテーブル、実行コードの逆アセンブル表示、MagicBreakなどかなり助かります。
実機でも動かして見ましたが、やはりエミュレータと違いなかなかうまく行かないし、デバッグもめっちゃつらいです。

開発は動かすことを考えて実装するときもあり、バグがあったり、未実装な部分(FIXMEやTODO)も多いです。
いまはプロセス周りを書いているので、やっとOS開発らしくなってきたなあと感じてます。

現時点の今後の予定としては、プロセス周りの作り込み、OS内部の抽象化(デバイスやファイルシステム)、システムコールの追加を考えています。

今日は以上です。
書く余裕が出来る限りカレンダーを埋めていこうと思っています。
話題は上のリストから拾って詳細を書こうかなと。
また、この程度の記事でも超OKなので自作OS気になるな〜って人は書いていただければ嬉しいです。
「自作OS Advent Calendar 2014」