Operating Systems Development Series

はじめに

この章では、プロセス管理とマルチタスクのトピックについて詳しく説明します。これまでの章では、タスクをサポートする基本的なモノリシックなOSを構築してきました。OSの開発においてこの時点までになされた多くの決定は、複雑さを犠牲にして単純にすることでした。この章でもこの傾向を引き継ぎます。最後に紹介するデモは、決してOSを開発する唯一の方法というわけではありません。この章では、次のようなトピックを取り上げます。

  1. プロセス

  2. スレッドとタスク

  3. プロセスの内側を見る

  4. プロセス管理

  5. スケジューリング

  6. リンク

プロセス管理

プロセス管理とは、オペレーティングシステムがプロセスやスレッドを管理し、プロセスが情報を共有できるようにし、プロセス資源を保護し、要求してきたプロセスにシステム資源を安全に割り当てるためのプロセスです。これは、オペレーティングシステムの開発者にとって大変な作業であり、設計が非常に複雑になる可能性があります。では、それぞれについて詳しく見ていきましょう。

プロセスおよびスレッド

本章の残りの部分では、主にプロセスとスレッドについて説明します。プロセスの作成は、実行可能なイメージをロードし、それを実行するための少なくとも1つの実行パス(スレッド)を作成することを意味します。

プロセス間通信

プロセス間通信 (IPC) は、プロセス間の通信を可能にするために多くのオペレーティングシステムで採用されている技術です。これは通常、メッセージパッシングによって行われます。プロセスは、他のプロセスにメッセージを送信するようオペレーティングシステムに要求し、オペレーティングシステムは、それが可能であれば、他のプロセスへのメッセージを送信してキューに入れるでしょう。IPCは、ファイル、パイプ、ソケット、メッセージパッシング、シグナル、セマフォ、共有メモリ、メモリマップドファイルなど、さまざまな方法で実装できます。 オペレーティングシステムは、これらのIPCの方法のいずれかまたはすべてを実装できます。 IPCはハイブリッドおよびモノリシックカーネル設計で多用されていますが、間違いなくマイクロカーネル設計で最も顕著に見られます。

この章では、主にプロセスとスレッドの作成に焦点を当て、IPCについては触れないことにします。IPC については後で少し議論するかもしれませんが、おそらくこの章への追加として議論します。

プロセス保護

複数のプロセスを同じアドレス空間にロードすると、両方のプロセスが互いに読み書きができるという根本的な問題が発生します。この問題を解決するには、プロセスをそれぞれの仮想アドレス空間にロードし、物理アドレス空間内の別々の場所にマッピングするのが簡単です。プロセスはより多くのスレッドを作成するように要求することができます。これはプロセスごとに行われるので、すぐにわかるように、すべてのスレッドはプロセスと同じアドレス空間を共有します。

例えば、カーネルランドにいなければならないプロセスはカーネルスペースに、カーネルランドにいる必要のないプロセスはユーザースペースにというように、プロセスが必要とする最小限の制御を行うマッピングもプロセス保護が採用されている。

この章では、プロセスを作成する際に、この両方を利用します。プロセスはユーザー空間にマッピングされ、独自の仮想アドレス空間に置かれます。つまり、プロセスはカーネルページにアクセスすることはできませんし(したがってカーネルスタックや構造体をゴミ箱に入れることはできません)、別のプロセスは別のアドレス空間にあるのでゴミ箱に入れることもできません。

リソース配分

リソース割り当てとは、システムリソース(ファイルやデバイスハンドルなど)を要求するプロセスに安全に渡す方法のことです。シリーズOSの初期状態であるため、現時点では心配するような資源はありません。なぜシステム資源の割り当てを管理する必要があるかというと、マルチタスク環境で2つのプロセスが同時に同じファイルを開いて書き込みをしようとした場合を考えてみてください。

このことを念頭に置いて、以下のセクションでは、プロセスとスレッドの作成に焦点を当てます。まず、これらが何であり、何がプロセスを構成するのかについて明確な定義を提供することから始めます。

プロセス

プロセスとはメモリ上にあるプログラムのインスタンス、またはプログラムの一部のことです。プロセスは、映画やビデオの再生、ゲームのプレイ、あるいはこの文章を書くのに使用したエディタの実行など、複雑なタスクを実行するために、オペレーティングシステムまたはエグゼクティブによって実行されます。要するに、プロセスはプログラムであると言えますが、1つのプログラムは複数のプロセスを含むことができます。例えば、文字列を表示する基本的なプログラムは、それ自身のプログラムファイルの中に組み込まれているかもしれません。このプログラムをロードすると、オペレーティングシステムやエグゼクティブは他のプログラムファイルをロードすることになります。つまり、プロセスが呼び出して使用する実行可能コードを含む共有ライブラリのダイナミックロードです。これらのプログラムファイルはすべて同じプロセスの一部です。そのため、1つのプロセスは複数のプログラムファイルのインスタンスを持つことができ、さらに複数のインスタンスを持つこともできます。

プロセスは 中央処理装置(CPU) または複数のCPUやCPUコアによって 、エミュレートされた環境またはハードウェア環境で実行されることがあります ハイパースレッディングや パラレルパイピングを サポートするCPUは 異なるプロセスからの複数の命令を同時に実行することもできる。つまり、プロセスは逐次実行(1命令ずつ実行)されるのではなく、環境やハードウェア構成に応じて、さまざまな方法で実行される可能性があります。IA32 CPUファミリーのデフォルトでは、これらの機能は無効になっています。つまり、コンピュータの起動時に、CPUはすべての命令を1つずつ実行します(命令キャッシュバッファ にキャッシュすることはあります)。 しかし、オペレーティングシステムやエグゼクティブがプロセスに対してこれらの機能を有効にした場合、プロセスと システムはマルチプロセッサに安全であるように設計しなければ なりません。 この1つはしかし高度なトピックであり、非常にエラーが発生しやすいため、上級の章で説明 されます。いくつかのプロセスをスレッドとタスクに分割することができます。次にこれらについて見ていきます。

スレッドとタスク

例えば、最も基本的な例として、メッセージを表示して戻るだけのプログラムが あると します

#include <stdio.h>
int main (int argc, char** argv) {
   printf ("Hello, world!");
   return 0;
}

この例では、プロセスは1つのスレッドを持っています:それはmain()で始まり、プロセスが終了するとスレッドは終了します。(ただし、実際にはそうでないかもしれないことに注意してください。main()を呼び出すランタイムライブラリにスレッドが含まれている可能性があるからです)。マルチスレッドの例を見てみましょう。

#include <stdio.h>
static int _notExit = 0;

int thread (void* data) {
   while (_notExit) {
      /* do something useful */
   }
   return 0; /* thread terminates (returns to runtime which calls TerminateThread */
}

int main (int argc, char** argv) {
   CreateThread (thread);
   printf ("Hello, world!");
   return 0;
}

この例 では、CreateThreadがオペレーティングシステムを呼び出して、 新しい実行フローとしてthread()設定します。CreateThread()呼び出さ れた オペレーティングシステムまたは実行プログラムから thread()が呼び出され、 どちらかが終了するまで thread()と main() の両方で同時に実行されます。すべてのプロセスはスレッドであるがスレッドはプロセスではないことに注意してください。 プロセスには1つのスレッドを含むことも多くのスレッドを 含むことも可能です。プロセス内のスレッドは同じグローバル変数にアクセスし、共有することができます。ただし スレッドローカル変数をサポートするコンパイラも あります

スレッドをサポートするオペレーティングシステムは マルチスレッドに対応していると言われて います。このようなオペレーティングシステムの例としては、Windows、Linux、およびMac OSがあります。 タスクはスレッドと同義 です。このように、マルチスレッドをサポート するOSは、マルチタスクを効果的にサポート します。 しかし、すべてのマルチタスクOSがマルチスレッドをサポートしているわけではないことに注意することが重要です。

プロセスとは何か?ここでは、プロセスを「プログラムのインスタンス」または「プログラムの一部」と定義しましたが、この定義をより詳しく説明するために、プロセスの内部をより詳しく見てみましょう。

プロセスの内側を見る

プロセスを分解してみると、最も基本的なレベルでは、コードとデータしか見えてこない。プログラミングの経験がある人なら、これは理にかなっている。すべてのプログラムは、CPUに動作を指示したり、データを操作したりする命令に過ぎないからだ。プログラマーがプログラムの開発を容易にするために、プログラムの「データ」部分を「コード」部分と分ける傾向があるのは理解できる。 . dataと.text (プログラムコード用 )は、後にプログラムバイナリ内の さまざまなタイプの セクションの うちの2 つです。セクションは、プログラム開発に役立つだけでなく、プログラムバイナリにさまざまな種類のものを格納する方法の標準化を促進する。

まず、プログラムセクションと、それらが プロセスアドレス空間内にどのように配置 されるかについて説明します次に、シンボル情報デバッグ情報、エクスポートおよび インポートテーブルと、それらの使用方法について説明 します。

プログラムセクション

プログラムファイルには、これらのセクションとその他のセクションが含まれて います オペレーティングシステムやエグゼクティブは、プロセスが正しく実行されるように、各セクションをアドレス空間にロードすることができます。 また、ロード時に セクションを再配置することも可能です。 これにより、オペレーティングシステムまたはエグゼクティブは、必要に応じて各セクションの最適な場所を見つけ、それに応じてプロセスを更新 することができます。ただし プログラムファイル形式によってサポートするものが異なるため、すべてのプログラムファイル形式がセクションの再配置をサポートしているわけではありません。


ポータブルエグゼクティブ(PE) ファイル形式は、Windows オペレーティングシステムで使用される主要なプログラムファイル形式です。PEファイルフォーマットは、コード、データ、リソースデータ、シンボリック情報、マニフェストデータなどのための多くの異なるセクションをサポートしています。各セクションは、リンカーやコンパイラーによって書き込まれたバイナリファイル内に格納されています。どのようにファイルに格納されるかを見るために、PEファイル形式のフォーマットを見てみる必要があります。

上記は、PE実行ファイルの中身をイメージしたものです。イメージをメモリにロードし、セクションを再配置する必要がない限り、他のタイプのバイナリファイルと同じようにファイルの内容を解析することができます。このようにして、シリーズのブートローダは私たちのカーネルイメージをロードすることができるのです。シリーズのカーネルはセクションの再配置を必要としないので、ブートローダは単にファイルをロードし、ヘッダでエントリポイントを見つけて、それを直接呼び出すことができるのです。これは簡単なことです。

/* Get entry point from PE headers */
IMAGE_DOS_HEADER dosHeader = (IMAGE_DOS_HEADER*) imageBase;
IMAGE_NT_HEADERS ntHeaders = dosHeader->e_lfanew;
IMAGE_OPTIONAL_HEADER optHeader = &ntHeaders->OptionalHeader;
void (*EntryPoint) (void) = (void (*EntryPoint) (void) ) optHeader->AddressOfEntryPoint + optHeader->ImageBase;

/* Call program entry point */
EntryPoint ();

同様に、他のヘッダも解析し、セクション情報、デバッグ情報、シンボリック情報など、ファイルから必要な情報を抽出することができます。カーネルデバッガや ユーザモードデバッガは通常、デバッグを容易にするためにシンボリック情報やデバッギング情報を使用します。言い換えれば、PE イメージをデバッグ情報付き(またはなし)でビルドすることができます。デバッグ情報付きでビルドした場合、デバッガをリモートでアタッチしてソースレベルデバッグを実行することができます。

実行ファイル内に異なるセクションを持つことで、実行ファイルのパースや書き込みが容易になります。それらはデータを格納する一貫した場所と方法を提供し、ヘッダからそれらを参照します。例えば、PEファイルには、実際のリソース(文字列テーブル、ビットマップ画像、プログラム情報、カーソルなど)を格納する.rscsセクションがあります。リソースを見つけるには、OptionalHeader->DirectoryEnteries [IMAGE_DIRECTORY_ENTRY_RESOURCE]でそのディレクトリエントリーをパースすればよく、これにより .rscs.section 内のリソースデータを指すリソースツリーの構造へのRVA(相対ポインタ)が得られます。ポイントは、実行形式がPEファイル仕様で定義された特定の形式を持っていることです。特定の形式があるからこそ、ファイルから情報を取得するための標準的な方法があるのです。

GCCやCL(Microsoftのコンパイラ)のような多くのコンパイラはプログラマ定義のセクションも可能です。 つまり、プログラマは自分自身でセクション名を定義して、そのセクションに必要なものを 入れることもできます。オペレーティングシステムのカーネルやエグゼクティブは、通常、異なる目的のために特別なセクションを定義する。 例えば、LinuxとWindowsの両方は 1回限りの初期化コードとデータを含む特別な .INIT セクションを定義して います。初期化が完了すると、オペレーティング・システム・カーネルはそのセクションを解放し、他のことに再利用することができる。

共通部分

異なるオブジェクトファイルやアーキテクチャにさえ共通するセクション名とタイプのセットがあります。これらのセクションとその用途を認識できるようにすることが重要です。それらは以下の通りです。

  1. .text

  2. .data

  3. .bss

  4. .rodata

.textセクションは、プログラムコードを含むセクションに付けられる一般的なセクション名で 、コードセグメントとも呼ばれます。システムによっては、このセクションへの書き込みを防ぐために読み取り専用にすることもありますが、これはコードの自己改変を防ぐためです(通常、これは推奨されません)

.dataセクションは、その名前が示すように プログラムで使用される静的 およびグローバルなデータを含みます。これは常に書き込み可能です。

.bssセクションは.dataセクションの一部で、通常、ゼロに初期化される静的割り当てデータに使用されます。.bssセクションは、オペレーティングシステムのローダーによって常にクリアされるので、その中のデータはすべてゼロに設定されます。Wikipediaによると、「.bss」という名前は当初、United Aircraft Symbolic Assembly ProgramのBlock Started by Symbolを意味していた。.bssセクションはすべてのNULL変数を含むため、オブジェクトファイル内でスペースを取りません。

.rodataセクションは、読み取り専用に静的に割り当てられたデータを含んでいます。これは、通常、LinuxやUnix環境でよく見られます。

一時データ用のセクションがないことに注意してください。一時変数はスタックに格納されるので、プログラムファイルに格納する必要はないことを思い出してください。

Microsoft Visual Cのカスタムセクション

マイクロソフトのコンパイラは、プログラマが特定のセクションのどこにデータやコードを配置するかを制御したり、カスタムセクションを作成するために使用できるいくつかのpragmaディレクティブを提供しています。これらは

  1. alloc_text

  2. code_seg

  3. const_seg

  4. data_seg

  5. bss_seg

  6. init_seg

  7. section

プログラムローダーは、プログラムが持つ特別なセクションを気にする必要はなく、メモリにロードすることだけを考えればよい。 その責任はプログラム(ひいてはプログラマー)が負う。

以下は、alloc_text を使って、特別なセクションに機能を追加する例である。

error_t DECL mmInitialize (SystemBoot* mb) {
   return SUCCESS;
}
#pragma alloc_text (".init", mmInitialize);

上記の例では mmInitializeがセクション .initに追加 されます。これは、いくつかのオペレーティングシステムのカーネルとエグゼクティブによって使用される便利な戦術です。例えば、オペレーティングシステムのカーネルやエグゼクティブは、特別な.initセクションに初期化コードやデータを追加することができます。初期化が完了すると、オペレーティングシステムはそのセクションを解放して、メモリの一部を取り戻すことができます。

シンボリック情報

記号 情報とは プログラマがアドレスの名前として 与える記号のことである。例えば、printf()ような関数を呼び 出すとき、コンパイラやリンカはどのようにして何をすべきかを知ることができるのだろうか。もう少し詳しく見てみよう。

"printf "は、ライブラリで定義されている関数のシンボル です。printf()」を呼び出すと、コンパイラが ビルド時に管理する シンボルテーブルに printf」というシンボルが追加 されます。関数の名前がシンボルであることに注意して ください。同様に静的変数やグローバル変数もシンボルです。 アドレスにつける名前( アセンブリ言語のラベルのようなもの )もシンボルであると言えます。 このように、シンボルには2つのものがあるのです。名前とアドレスです。

printfのコードを含むライブラリをリンクせずに構築すると、コンパイラはコード全体を機械語に翻訳することができないため、最終的な実行ファイルを出力することができません。つまり、アセンブラのように、関数が何であるかがわからないと、次のようなコードでは何もできないのである。

call _printf

アセンブラは、シンボル_printfのアドレスを知らない限り、この命令を完全にアセンブルすることはできません。シンボルについて何も知らなければ、アセンブラはそのアドレスを知ることができません。 これを解決するために、シンボルは外部であると宣言 し、アセンブラまたはコンパイラは 実行ファイルの代わりにオブジェクトファイルを出力 します。部分的に機械語に翻訳しているが、次のような形になっている。

0xe8 _printf

このため リンカーという別のプログラムを使って これらのシンボルを解決することができます。リンカはオブジェクトファイルやライブラリのエクスポートシンボルテーブルを調べて、"_printf "というシンボルを探します。 もし見つかったら、リンカは関数コードのアドレスを取得して、そのアドレスでマシンコードを更新し、最終的に実行プログラムを正しくリンクして出力することができるのです。もしシンボルが見つからなければ未解決となり、リンカーは有名な "unresolved external symbol "というエラーを出します。

実行イメージのシンボリック情報は、デバッガが人間が読める情報(関数や変数)名を表示するために使用できますが、その代償としてプログラムのファイルサイズが大きくなります。

シンボルに関する追加情報をシンボル名自体に「格納」する方法は、ビルド環境や呼び出し方法によって異なります。C言語の標準的な呼び出し規約はCDECLで、すべての名前の前にアンダースコアを付ける だけです。例えば、"printf() "を呼び出す場合、CDECLのシンボル名は"_printf "となります。C++のシンボル名はコンパイラによって異なり、名前以外にも多くの情報(戻り値のデータ型やオペランド型、名前空間、クラス、テンプレート名など)を格納しています。このため、C++のシンボル 名はネームマングリングを受けると言われています。例えば、CL(Microsoftのコンパイラ)の関数「void h(void」はシンボル名 ?h@YAXXZ変換しますここでネームマングリングの形式の詳細について触れるのはやめておきましょう。

ここで面白いことに気がついた。Cのシンボル名は戻り値のデータ型やオペランドについて何も保存しませんが、C++のシンボル名には名前のマングリングによるものがあります。これは理解できることですが、言語間の多くの違いの1つを示しています。Cコンパイラでは、オペランド型やオペランド数が異なる関数を、エラーなしに(検出された場合は警告が出るかもしれませんが)呼び出すことができますが、C++コンパイラでは、それが可能です。

テーブルのエクスポートとインポート

プログラムライブラリやオブジェクトファイル内のシンボルは 他のライブラリや プログラムで使用するためにエクスポートすることができます。エクスポートされたシンボルはコンパイラとリンカにそれぞれのシンボルを エクスポートテーブルに追加 するように指示するだけです。 プログラムファイルと共有ライブラリ(Windows DLL)は、他のプログラムやデバッガが使用するためにシンボルをエクスポート することができます。同様に、プログラムファイルはシンボルをインポートして使用するように要求 できます。ここで、上記のprintf()の例を完成させることができます。

Microsoft C Runtime Libraryは、プログラムファイルと一緒にロードされる共有ライブラリです。 オペレーティングシステムまたは エグゼクティブは、プログラムのインポートテーブルを見ることによって、プログラムファイルが動作するためにどのDLLが必要かを知ることが できます。 デフォルトでは、CL(Microsoftのコンパイラ)は、インポートテーブルを含むMicrosoft C Runtime Libraryインポートスタティックライブラリとリンクするので、シンボルが追加されて各DLLはテーブルに含まれて います。オペレーティングシステムまたはエグゼクティブは、プログラムが必要とする共有ライブラリファイルをすべてメモリにロードし、プログラムファイルの IAT(Import Address Table)をこれらの他のDLL内の関数のアドレスで更新する必要が あります。ロードされるMicrosoft C Runtime Library DLLは_printfのコードを含むだけでなく、シンボル_printfをエクスポートするので、オペレーティングシステムはランタイム中にこれらをリンクします(これについては後で詳しく説明します)。

したがって、プログラムファイルから「printf()」を呼び出すと、これはCランタイムライブラリDLL内の関数「_printf」を呼び出す更新されたIATアドレスを呼び出すジャンプテーブルを呼び出すことになるのです。

これまで、プロセス、スレッド、タスクについて説明し、プログラムファイルとは何か、どのように動作するかについて見てきました。この章の目標は、複数のプロセスやタスクをロードし、実行し、管理できるようになることです。次にそれを見てみましょう。

プロセスマネジメント

プロセス管理とはソフトウェアシステムにおけるプロセスを 管理する ことである。先ほど、プロセスとはメモリ上のプログラムまたはその一部と定義した。つまり、プロセスを管理するということは、メモリ上のプログラムの複数のインスタンスを協調して管理することを意味する。これは、最近のオペレーティングシステムでは典型的な要件であり、カーネルまたはエグゼクティブに実装されています。プロセス管理をサポートするオペレーティングシステムはマルチタスクオペレーティングシステムとみなされる。

表現方法

OSの設計者は、プロセスを管理するために、OSの設計条件と必要なシステム資源から、どのようにプロセスを表現するのが最適かを決定する必要がある。プロセスは次のような構成になっています。

オペレーティングシステムは、プロセスを管理し、要求したプロセスに対して公平にシステムリソースを割り当てることが要求されます。それぞれについて詳しく見ていきましょう。

メモリ上の実行ファイルのイメージ

実行可能なプログラムは、プログラムのロードと管理を容易にするために、ディスク上にファイルとして保存されます。プログラムをロードするには、オペレーティングシステムのローダーがファイルをメモリにロードします。ローダーは、ファイルの種類(オペレーティングシステムが扱うことができる実行可能ファイルであること)を理解し、場合によってはこれらのファイルタイプの機能(リソースやデバッグ情報など)をサポートすることもできる必要があります。

メモリ上の実行ファイルのイメージは、そのイメージのマシンコードとデータの現在の表現であり、任意の時点でメモリ上にどのように表示されるかを示しています。ここでいう「イメージ」とは、メモリ上のものの「スナップショット」を表します。例えば、カメラで大きなバイトの配列を見て、写真を撮るようなものです。バイトの配列は、マシンコードでもデータでも、どちらでもないものでも、私たちにはわかりませんし、気になりません。プログラム命令だけが知っている。

プログラムイメージの中には、他のプログラムやオペレーティングシステムにとって有用なデータもあります。例えば、プログラムファイルにはデバッギング情報が含まれていることがあります。例えば、プログラムファイルにはデバッグ情報が含まれています。

つまり、オペレーティングシステムは、ファイルを実行するために、ディスクからどこかのメモリにロードできる必要があります。 これは、単にファイルをメモリに「そのまま」ロードするようなものです。 オペレーティングシステムまたは他のプログラムは、プログラムファイルから必要な有用なデータを取得することができます。

プロセスが使用しているメモリとその仮想アドレス空間

プロセスは通常、オペレーティングシステムと同様に、動的にメモリを割り当てたり、スタック領域を使用するための呼び出しを行います。オペレーティングシステムは、プロセスが使用するプロセススタックとヒープメモリのためのスペースを割り当てる必要があります。例えば、オペレーティングシステムは通常、すべてのプロセスに対してデフォルトのスタックサイズを割り当てます。しかし、プロセスの実行ファイルは、プロセスが必要とする場合、より大きなスタックスペースを割り当てるようにオペレーティングシステムに指示することができます。

プロセスヒープは違います。スタックはプロセスを実行する前にオペレーティングシステムによって割り当てられますが、ヒープはそうではありません。その代わり、各プロセスはユーザモードで独自のヒープアロケータを持ちます。CRTとリンクしているプログラムは、これらの関数を呼び出してメモリを確保することができます。しかし、CRTにリンクされていないプログラムは、独自のヒープアロケーターを実装するか、実装している他のライブラリとリンクする必要があります。

CRTランタイムはユーザモードのヒープアロケータ(通常はフリーリスト)を実装しています。C の関数 malloc は、System API を使用して OS を呼び出す brk を呼び出すことがあります。C関数brkは、必要なときにヒープを拡張するためにさらに仮想メモリを割り当てるために、OSを呼び出します。

要するに、ユーザーモードのヒープは次のように動作する。プログラムは malloc を呼び出し、brk を呼び出すかもしれません。brk はシステム API を使って OS を呼び出し、ヒープに仮想メモリを割り当てます。malloc と free ファミリーの関数は、独自のユーザモードヒープアロケータを実装しています。これらの関数は、OSを呼び出して、仮想アドレス空間からメモリを確保したり解放したりするだけです。

プリエンプティブマルチタスクでは、すべてのプロセスが独自の仮想アドレス空間を持ちます。これは、すべてのプロセスが独自のページディレクトリと関連するページテーブルを持つ必要があることを意味します。プロセス固有の情報を管理するために、プロセスコントロールブロック (PCB) を使用します。次にそれを見てみましょう。

プロセスを表現するために使用される記述子

プロセスコントロールブロック(PCB )は、プロセスまたはタスクに関する情報を格納するために使用されるデータ構造です。PCBは、割り込みディスクリプタのポインタ、ページディレクトリベースレジスタ(PDBR)などの情報を含んでいます。保護レベル、実行時間、プロセス状態、プロセスフラグ、VM86 フラグ、プライオリティ、プロセス ID (PID) などの情報が含まれます。PCB には、さらに多くの情報(OS 固有の情報)が含まれることがあります。

オペレーティングシステムはプロセスを管理するために PCB のリンクリストを使用することがあります。新しいプロセスを作成するとき、オペレーティングシステムは新しい仮想アドレス空間を割り当て、イメージをロードしてマップし、リストに新しい PCB 構造をアタッチする必要があります。スケジューラは、実行するプロセスを決定し、現在の状態を保存するために PCB リストを使用します。

プロセス状態情報

プロセス状態情報には、ある時点のプロセスの全レジスタ状態、インメモリ状態、入出力要求状態などが含まれます。プロセスの状態は、タスクを切り替えるときにPCBに保存されます。これは、マルチタスクOSの心臓部であるスケジューラによって行われます。さらに、プロセスの現在の実行状態は、オペレーティングシステムによるプロセスの実行を制御するために使用されます。

最も単純なケースでは、状態はRUNNINGかNOT RUNNINGのどちらかである。このモデルでは、作成されたばかりのプロセスはNOT RUNNINGキューに格納され、実行中のときだけRUNNINGとラベル付けされます。NOT RUNNINGのプロセスは、RUNNINGのプロセスが終了するか、スケジューラ内のプロセスディスパッチャによって中断されるまで、メモリ内にまだ存在しますが、待機状態にあります。

3状態のプロセス管理モデルでは、プロセスはRUNNING、READY、またはBLOCKEDのいずれかになります。RUNNINGプロセスが待機を必要とするアクセス(I/O要求など)を要求すると、オペレーティングシステムはそのプロセスをRUNNINGからBLOCKEDに変更することができる。要求が実行できるようになると、プロセスはRUNNING状態またはREADY状態のいずれかに移行することができる。READY状態のプロセスは、プロセスディスパッチャによる実行の準備が整ったことを意味するだけです。RUNNING状態のプロセスは、すでに実行されています。

最後のモデルは、「5状態プロセス管理モデル」である。このモデルは5つの状態を利用する。SUSPEND BLOCKED、BLOCKED、SUSPEND READY、READY、SUSPENDEDの5つの状態を利用します。

スケジューリング

スケジューラとは 、オペレーティングシステムのカーネルやエグゼクティブのコンポーネントで、タスクの切り替えやCPUの使用量の割り当てを行うものである。オペレーティングシステムは、次にどのタスクを実行するかを決定するために、スケジューリングアルゴリズムを採用している。一般的なスケジューリングアルゴリズムには、先入れ先出し、最短残時間、固定優先順位プリエンプティブ、ラウンドロビン、マルチレベルキューなどがあるが、これらに限定されるものではない。WindowsとLinuxの両方で使用されている最も一般的なアルゴリズムは、おそらくマルチレベル・フィードバック・キューでしょう。

基本的なプロセス管理支援

これで、基本的なプロセス管理のサポートを実装することができます。目標はシンプルであることなので、vm86タスクのサポートやI/Oリソースの割り当てなど、高度なマルチレベルフィードバックシステムを実装するのではなく、よりシンプルでありながら効率的なスケジューラーに焦点を当てることにします。

そのためには、何をすればいいのか、その目標を考えてみましょう。

  1. 実行イメージをメモリにロードし、パースする。

  2. プロセス用PCBのリストを管理します。

  3. ユーザーモードタスクをサポートします。

  4. 複数の仮想アドレス空間をサポート

  5. 各プロセスにスタック領域を割り当てる。デフォルトのサイズは4k。

  6. スケジューリングアルゴリズムの選択とタスクスイッチの実装

これらは、マルチタスクに対応するための目標です。プロセスは、ユーザーモードプロセスとなる。しかし、マルチタスクは、プロセス管理とスケジューリングの両方に依存しています。このため、本章では、マルチタスクをサポートするフレームワークの構築に焦点を当てますが、1プロセス1スレッドのみを許可することにします。このことは、次章でスケジューラを実装することにより、拡張していきます。

プロセス制御ブロック

私たちのシステムの基板構成は、シンプルなものになります。

#define PROCESS_STATE_SLEEP  0
#define PROCESS_STATE_ACTIVE 1

#define MAX_THREAD 5

typedef struct _process {
   int            id;
   int            priority;
   pdirectory*    pageDirectory;
   int            state;
/* typedef struct _process* next; */
/* thread* threadList; */
   thread  threads[MAX_THREAD];
   /*
     note: we can add more information, such as the following:
       -LDT descriptor [if used]
       -Processor count being used
       -User and kernel times
       -Execution options, etc
   */
}process;

この構造体にさらに追加することもできますが、本当に必要なのは上記のものだけです。プロセスID(PID)、優先度、仮想アドレス空間が格納されていることに注意してください。コメントされた2つのエントリは、完全性のためだけに提供されています。典型的なOSでは、それらはプロセスとスレッドのリンクリストであるべきです。しかし、これにはカーネルのヒープアロケータが必要であり、我々はそれを書いていない。簡単のために、プロセス内の5つのスレッドオブジェクトを配列として格納します。

最後に必要なのは、スレッドを処理する方法である。すべてのプロセスは、エントリポイントで実行を開始する最大1つのスレッドを持っています。

typedef struct _thread {
   process*  parent;
   void*     initialStack;
   void*     stackLimit;
   void*     kernelStack;
   uint32_t  priority;
   int       state;
   trapFrame frame;
}thread;

スレッド構造体は、プロセス内のスレッドに関する一般的な情報を格納します。この構造体は、親プロセスへのポインタと、スレッドスタック、優先度、状態(実行中かどうか)、およびトラップフレームに関する情報を格納することに注意してください。トラップフレームは、実行中のスレッドの現在のレジスタの状態を格納します。

typedef struct _trapFrame {
   uint32_t esp;
   uint32_t ebp;
   uint32_t eip;
   uint32_t edi;
   uint32_t esi;
   uint32_t eax;
   uint32_t ebx;
   uint32_t ecx;
   uint32_t edx;
   uint32_t flags;
   /*
      note: we can add more registers to this.
      For a complete trap frame, you should add:
        -Debug registers
        -Segment registers
        -Error condition [if any]
        -v86 mode segment registers [if used]
   */
}trapFrame;

この章では、まだマルチタスクを実装していないので、トラップフレーム構造体をあまり使用しません。しかし、次の章では、各スレッドの現在の状態を保存するスケジューラを開発するため、トラップフレーム構造体をより多く使用する予定です。

仮想アドレス空間

複数の仮想アドレス空間をサポートする場合、複雑な問題が発生します。 各プロセスのアドレス空間は、カーネルコードとデータが2GBに配置された4GBのアドレス空間全体から構成されています。 プロセスを切り替える際には、アドレス空間を切り替えられるようにする必要があるが、アドレス空間の下位2GB(「ユーザーランド」)だけは切り替えられるようにしなければならない。つまり、ユーザーモードプロセスが動作しているとします。そこで、カーネル内のスケジューラを呼び出して、タスクを切り替えられるようにする必要があります。しかし、そのためには、カーネルのコードも同じアドレス空間になければなりません。そうでないと、即座にクラッシュしてしまう。

この問題を解決するためには、カーネルコードを各プロセスのアドレス空間にマッピングする必要があります。どうやったらそんなことができるのかと思われるかもしれません。しかし複数の仮想アドレスがメモリ内の同じ物理フレームを参照できることを考えると、より明確になるかもしれません。言い換えれば、カーネルスタックとコードを両方のアドレス空間にマップすることができます。次の画像をご覧ください。


上の画像では、2つの仮想アドレス空間と物理アドレス空間が表示されています。プロセススタックとコードの位置が、物理メモリ内の異なる場所を共有していることに注目してください。つまり、仮想メモリマネージャを使って、同じ基本的な仮想アドレスの場所を異なる物理アドレスフレームにマッピングしているのです。カーネルを少し考えてみましょう。カーネルは、単一のアドレス空間を持つ環境で起動します。カーネルは初期化プロセスで自分自身のアドレス空間にマッピングします。問題を防ぐために、カーネル空間を他のプロセスのアドレス空間にもマッピングできるようにする必要があります。カーネルはすでに自分のアドレス空間にマッピングされ、物理メモリのある場所に位置しています。つまり、カーネルは他のプロセスのアドレス空間にも再マップすることができるのです。

シリーズカーネルでは、カーネル自身が1MBの物理領域から3GBの仮想領域にマッピングする。カーネルは、自分自身を各プロセスのアドレス空間にマップするために、次に、すべてのプロセスの3GBの領域を1MBの物理にマップする必要があります。 カーネルとカーネルスタックは、すべてのプロセスのアドレス空間の同じ場所にマッピングされなければなりません

オペレーティングシステムは、カーネル全体ではなく、カーネルの一部をプロセスアドレス空間にマッピングすることもできます。これは大規模なシステムでよく見られます。

アドレス空間管理

異なるアドレス空間から仮想ページをマッピングできるようにする必要があります。具体的には、次のようなことができるようにする必要があります。

  1. 任意のページディレクトリからページテーブルを作成

  2. 任意のページディレクトリから任意の物理アドレスを仮想アドレスにマップする

  3. 任意のページディレクトリから任意の仮想マッピングの物理アドレスを取得する

  4. 新しいアドレス空間の作成

本シリーズのVirtual Memory Managerは、現在この機能をサポートしていません。 しかし、すぐに実装することができますので、今すぐ実装してしまいましょう。

ページテーブルの作成

ページテーブルを作成するには、空きフレーム(ページテーブルは1024個のPTEで構成され、1ページのサイズは4096バイトであることを思い出してください)を確保し、ページディレクトリのPDEのフレームに追加するだけです。Virt >> 22は仮想アドレスからディレクトリのインデックスを取得することができます。pagedir [directory_index]のPDEが0であれば、このページテーブルは存在しないことがわかるので、物理メモリマネージャを使用して割り当てます。存在する場合は、割り当てる必要はありません。最後に、ページテーブルをクリアして、その存在ビットを 効果的に0(存在しない)にセットします。

int vmmngr_createPageTable (pdirectory* dir, uint32_t virt, uint32_t flags) {

        pd_entry* pagedir = dir->m_entries;
        if (pagedir [virt >> 22] == 0) {
	void* block = pmmngr_alloc_block();
	if (!block)
	   return 0; /* Should call debugger */
          pagedir [virt >> 22] = ((uint32_t) block) | flags;
          memset ((uint32_t*) pagedir[virt >> 22], 0, 4096);

	/* map page table into directory */
	vmmngr_mapPhysicalAddress (dir, (uint32_t) block, (uint32_t) block, flags);
        }
	return 1; /* success */
}

This function allows us to create page tables for any page directory.

この機能により、任意のページディレクトリのページテーブルを作成することができる。

物理アドレスのマッピング

次に足りない機能は、異なるページディレクトリに対して、物理アドレスと仮想アドレスを対応付けることです。これは簡単です。

void mapPhysicalAddress (pdirectory* dir, uint32_t virt, uint32_t phys, uint32_t flags) {

        pd_entry* pagedir = dir->m_entries;
        if (pagedir [virt >> 22] == 0)
                createPageTable (dir, virt, flags);
        ((uint32_t*) (pagedir[virt >> 22] & ~0xfff))[virt << 10 >> 10 >> 12] = phys | flags;
}

この関数は、以前、仮想メモリマネージャで実装した基本的な機能を踏襲しています。有効なページテーブルがあるかどうかをテストし、存在しないことが示された場合は作成します。最後の行はマッピングを実行します。

この機能により、任意の仮想アドレス空間の物理アドレスと仮想アドレスを対応付けることができる。

物理アドレスの取得

次に足りない機能は、上記と逆で、特定のアドレス空間から任意の仮想アドレスの物理アドレスを取得することです。

void* getPhysicalAddress (pdirectory* dir, uint32_t virt) {

        pd_entry* pagedir = dir->m_entries;
        if (pagedir [virt >> 22] == 0)
                return 0;
        return (void*) ((uint32_t*) (pagedir[virt >> 22] & ~0xfff))[virt << 10 >> 10 >> 12];
}

この関数は、その仮想アドレスに有効なページテーブルがあるかどうかをテストし(存在するかどうかをチェックすることによって)、PDEとPTEをデリファレンスして物理フレームを返します。

新しいアドレス空間の作成

各プロセスは、それぞれ独自の仮想アドレス空間で実行される。これを実現するためには、複数のアドレス空間を作れるようにしなければならない。

pdirectory* createAddressSpace () {
        pdirectory* dir = 0;

        /* allocate page directory */
        dir = (pdirectory*) pmmngr_alloc_block ();
        if (!dir)
                return 0;

        /* clear memory (marks all page tables as not present) */
        memset (dir, 0, sizeof (pdirectory));
        return dir;
}

この関数は、ブロックを確保してクリアするだけというシンプルなものです。ページディレクトリはアドレス空間を表し、1ページディレクトリは4096バイトなので、これは理にかなっています。クリアすることで、すべてのPDEの現在ビットを事実上0に設定しているのです。

プロセスを実行するときには、先ほど作成した新しいアドレス空間に切り替えられるようにしなければなりません。言い換えれば、この新しいページディレクトリをPDBRにロードできるようにする必要があるのです。この機能はすでにPMMに実装されています。しかし、空のページディレクトリをPDBRにロードするだけでは、必ずその直後にトリプルフォールトが発生します。この原因は単純で、カーネルコードやスタックのどれもがこの新しいアドレス空間にマッピングされていないためです。

興味深いことに、次のようにカレントページディレクトリ(PDBRに格納)を新しいアドレス空間にコピーするだけで、これを解決することができます。

        memcpy (dst->m_entries, cur->m_entries, sizeof(uint32_t)*1024);

必要なのはこれだけです。ページテーブルはすでにオリジナルのページディレクトリにマップされているので、ページテーブルのコピーについて心配する必要はありません。上記は効果的にアドレス空間のコピーを作成します - カーネルページテーブルは、我々が望む両方のアドレス空間にマッピングされます。

スレッドの作成

スレッドを作成するためには、まず createThread 関数が何を必要とし、スレッドの作成が実際に何を意味するのかを決める必要があります。スレッドを単一の実行経路として定義したことを思い出してください。これを知っていれば、必要なのはエントリポイント関数だけです。この関数が完成したら、オペレーティングシステムを呼び出してスレッドを終了させます。これは通常、スレッドの作成と終了を単純化するためにシステムAPI(Win32 APIなど)によって行われます。

スレッドを作成するために必要なことは、スレッド構造を割り当てて、それをプロセスに追加することです。当初はこのデモのためにこの機能を実装する予定でしたが、マルチスレッドについて見る第25章に残すことにしました。

プロセス作成

プロセスを作成するためには、OS専用のローダー・コンポーネントをすでに用意しておく必要があります。ローダーコンポーネントは、実行ファイルのロードと解析、BSSセクションのクリア、セクションのアライメント、その他、ダイナミックリンクライブラリのダイナミックロードなど、必要なことを担当します。 ローダーの作成は、特にPEと同じくらい複雑なファイル形式の場合、複雑な作業になることがあります。このため、本章の目標であるプロセス管理に集中できるよう、このシリーズではよりシンプルなソリューションを選択しました。

プロセスを作成するためには、プロセスとは何か、スレッドとどう違うのかを明確に理解する必要があります。具体的に言うと

  1. スレッドはそれぞれ専用のスタックを持ち、プロセス自体はスタックを持ちません。

  2. 各プロセスは少なくとも1つのスレッドを持たなければならない。 これはプロセスのエントリポイントから始まる。

  3. 各プロセスは、独自の仮想アドレス空間を持つ必要があります。プロセス内のスレッドは、プロセスと同じアドレス空間を共有します。

  4. 各プロセスは、実行可能なイメージとしてディスクからロードされる必要があります。これは通常、別のローダーコンポーネントを使用して行われます。

このシリーズには専用の画像ローダーコンポーネントがないため、簡単のために、これらの手順をすべてcreateProcessという一つの関数で実行します。 この関数は次のような手順で実行します。

  1. 実行ファイルを読み込む。

  2. プロセスのアドレス空間を作成する。

  3. プロセス制御ブロック(PCB)を作成します。

  4. メインスレッドを作成します。

  5. イメージをプロセスの仮想アドレス空間にマップする。

これは一つの機能としては非常に大きなものです。しかし、ローダーをプロセス作成から分離した方が良いことを忘れないでください。後で、実行ファイルのロードを専用のローダーに移行することができます。簡単のために、このルーチンはブートローダと同じ基準を想定しています。つまり、ロードするイメージはセクタアラインされたセクションを持ち(/align:512フラグを使用)、Microsoft Windowsランタイムライブラリとリンクされていないことが必要です。将来、この機能をデモに追加することになるかもしれませんが、これはロードコードを複雑にしますし、前述のように、通常はローダーコンポーネントによって処理されます。

プロセスアドレス空間構造

現時点では、シリーズOSのカーネルは、IDマップドメモリの1MB以下に多くのカーネル構造をロードしています。これには、カーネルスタック、初期ページディレクトリテーブル、ページテーブルが含まれます。また、DMAC(Direct Memory Access Controller)のメモリ領域もこの領域にある可能性がある。また、カーネルは、このアイデンティティマップド領域にある他のメモリ領域(ディスプレイメモリなど)も利用することを考慮しなければならない。

これはすべて単純化するためだけに行われました。典型的なカーネルは、カーネルスタックとカーネルメモリの初期ページテーブルを、PIC(Position Independent Code)を使って最初に初期化します。PICはまた、他の物理的なベースアドレスでロードされたときに、より高いハーフカーネルを開始することを可能にするものです。しかし、その結果、1MB以下のカーネル構造がいくつか存在するようになり、混乱が生じました。

移動させるよりも、0〜4MBをカーネルモードだけに確保するのがベストな選択と判断したのです。これにより、カーネルは全く手を加えることなく機能を継続し、ディスプレイ出力などの基本的なもののためにメモリをリマップしても問題はない。つまり、アドレス空間は次のようになる。

0x00000000-0x00400000  Kernel reserved
0x00400000-0x80000000  User land
0x80000000-0xffffffff  Kernel reserved

つまり、すべてのプロセスは4MBから2GBの領域内にイメージベースを持つ必要があります。私はすべてのユーザーモードプロセスのベースアドレスとして4MBを使用することにします。最初の4MBはカーネルモードページとしてマッピングされたまま(これはすでに行われています)、カーネル自体は3GBにマッピングされたままです。つまり、プロセスのすべてのページは、4MBと2GBの間のユーザーモードページとしてマップされます。

プロセスの作成

このことを念頭に置いて、この関数を見てみましょう。これはかなり長いルーチンで、通常ローダーで行われるいくつかのソフトウェアが含まれています。このデモでは、画像をロードして現在のアドレス空間にマッピングし、新しいものを作成するようにしています。これは、このソフトウェアが一度に1つのプロセスしか実行できないように設計されているためです。第25章でマルチタスクを取り上げる際には、この点を変更する予定です。

vmmngr_createAddressSpaceと mapKernelSpaceという2つの新しい関数に注目してください; これらは現在デモでは使用されていません。最初の関数は新しいアドレス空間を割り当て、2番目の関数はカーネル空間を仮想アドレス空間にマップします。 つまり、カーネルメモリ、スタック、ページディレクトリ、およびディスプレイメモリを新しいアドレス空間にマップします。これらの関数は使用されませんが、次章で使用されるでしょう。

validateImage関数は、画像ヘッダを解析し、サポートされているかどうかを確認するだけです。最後に、初期スレッド構造を作成しますが、マルチスレッドをサポートしません。1プロセスあたり1スレッドを想定しており、1プロセスあたり1スレッドしか実行できません。

int createProcess (char* appname) {

        IMAGE_DOS_HEADER* dosHeader;
        IMAGE_NT_HEADERS* ntHeaders;
        FILE              file;
        pdirectory*       addressSpace;
        process*          proc;
        thread*           mainThread;
        unsigned char*    memory;
        uint32_t          i;
        unsigned char     buf[512];
 
       /* open file */
        file = volOpenFile (appname);
        if (file.flags == FS_INVALID)
                return 0;
        if (( file.flags & FS_DIRECTORY ) == FS_DIRECTORY)
                return 0;

        /* read 512 bytes into buffer */
        volReadFile ( &file, buf, 512);
        if (! validateImage (buf)) {
            volCloseFile ( &file );
            return 0;
        }
        dosHeader = (IMAGE_DOS_HEADER*)buf;
        ntHeaders = (IMAGE_NT_HEADERS*)(dosHeader->e_lfanew + (uint32_t)buf);

        /* get process virtual address space */
        //addressSpace = vmmngr_createAddressSpace ();
        addressSpace = vmmngr_get_directory ();
        if (!addressSpace) {
            volCloseFile (&file);
            return 0;
        }
        /*
           map kernel space into process address space.
           Only needed if creating new address space
        */
        //mapKernelSpace (addressSpace);

        /* create PCB */
        proc = getCurrentProcess();
        proc->id            = 1;
        proc->pageDirectory = addressSpace;
        proc->priority      = 1;
        proc->state         = PROCESS_STATE_ACTIVE;
        proc->threadCount   = 1;

        /* create thread descriptor */
        mainThread               = &proc->threads[0];
        mainThread->kernelStack  = 0;
        mainThread->parent       = proc;
        mainThread->priority     = 1;
        mainThread->state        = PROCESS_STATE_ACTIVE;
        mainThread->initialStack = 0;
        mainThread->stackLimit   = (void*) ((uint32_t) mainThread->initialStack + 4096);
        mainThread->imageBase    = ntHeaders->OptionalHeader.ImageBase;
        mainThread->imageSize    = ntHeaders->OptionalHeader.SizeOfImage;
        memset (&mainThread->frame, 0, sizeof (trapFrame));
        mainThread->frame.eip    = ntHeaders->OptionalHeader.AddressOfEntryPoint
                + ntHeaders->OptionalHeader.ImageBase;
        mainThread->frame.flags  = 0x200;

        /* copy our 512 block read above and rest of 4k block */
        memory = (unsigned char*)pmmngr_alloc_block();
        memset (memory, 0, 4096);
        memcpy (memory, buf, 512);

        /* load image into memory */
        for (i=1; i <= mainThread->imageSize/512; i++) {
           if (file.eof == 1)
              break;
           volReadFile ( &file, memory+512*i, 512);
        }

        /* map page into address space */
        vmmngr_mapPhysicalAddress (proc->pageDirectory,
                ntHeaders->OptionalHeader.ImageBase,
                (uint32_t) memory,
                I86_PTE_PRESENT|I86_PTE_WRITABLE|I86_PTE_USER);

        /* load and map rest of image */
        i = 1;
        while (file.eof != 1) {
                /* allocate new frame */
                unsigned char* cur = (unsigned char*)pmmngr_alloc_block();
                /* read block */
                int curBlock = 0;
                for (curBlock = 0; curBlock < 8; curBlock++) {
                        if (file.eof == 1)
                                break;
                        volReadFile ( &file, cur+512*curBlock, 512);
                }
                /* map page into process address space */
                vmmngr_mapPhysicalAddress (proc->pageDirectory,
                        ntHeaders->OptionalHeader.ImageBase + i*4096,
                        (uint32_t) cur,
                        I86_PTE_PRESENT|I86_PTE_WRITABLE|I86_PTE_USER);
                i++;
        }

         /* Create userspace stack (process esp=0x100000) */
         void* stack =
            (void*) (ntHeaders->OptionalHeader.ImageBase
             + ntHeaders->OptionalHeader.SizeOfImage + PAGE_SIZE);
         void* stackPhys = (void*) pmmngr_alloc_block ();

         /* map user process stack space */
         vmmngr_mapPhysicalAddress (addressSpace, (uint32_t) stack, (uint32_t) stackPhys,
		I86_PTE_PRESENT|I86_PTE_WRITABLE|I86_PTE_USER);

         /* final initialization */
         mainThread->initialStack = stack;
         mainThread->frame.esp    = (uint32_t)mainThread->initialStack;
         mainThread->frame.ebp    = mainThread->frame.esp;

         /* close file and return process ID */
         volCloseFile(&file);
         return proc->id;
}

プロセス実行

プロセスを実行するには、そのプロセスのメインスレッドからEIPとESPを取得し、ユーザーモードに落として実行すればよいのです。しかし、どのプロセスを実行すればいいのかがわからないという問題があります。スケジューラがまだないため、一度に実行できるのは1つのプロセスだけです。GetCurrentProcess()は、このオブジェクトへのポインタを返しますそのメインスレッドからESPとEIPを取得し、プロセスアドレス空間に切り替えて、ユーザモードに落として実行します。

enter_usermodeを 呼んでいないことに注意して ください。これは、ユーザーモードソフトウェアがカーネル専用ページにアクセスできないためです。もしこれを呼び出すと、ページフォルトが発生します。代わりに、ユーザモードにドロップして、IRETDを使用して直接プログラムを実行します。

void executeProcess () {
        process* proc = 0;
        int entryPoint = 0;
        unsigned int procStack = 0;

        /* get running process */
        proc = getCurrentProcess();
		if (proc->id==PROC_INVALID_ID)
			return;
        if (!proc->pageDirectory)
			return;

        /* get esp and eip of main thread */
        entryPoint = proc->threads[0].frame.eip;
        procStack  = proc->threads[0].frame.esp;

        /* switch to process address space */
        __asm cli
        pmmngr_load_PDBR ((physical_addr)proc->pageDirectory);

        /* execute process in user mode */
        __asm {
                mov     ax, 0x23		; user mode data selector is 0x20 (GDT entry 3). Also sets RPL to 3
                mov     ds, ax
                mov     es, ax
                mov     fs, ax
                mov     gs, ax
                ;
                ; create stack frame
                ;
                push   0x23			; SS, notice it uses same selector as above
                push   [procStack]		; stack
                push    0x200			; EFLAGS
                push    0x1b			; CS, user mode code selector is 0x18. With RPL 3 this is 0x1b
                push    [entryPoint]	; EIP
                iretd
        }
}

プロジェクト "proc"

使用したusermodeプロセスはprocと呼ばれ、32ビットPE実行イメージ、イメージベースは4MB、512バイトセクションアライメントでビルドされています。以下、参考までにプロジェクトのソースを示します。

void processEntry () {

	char* str="\n\rHello world!";

	__asm {

		/* display message through kernel terminal */
		mov ebx, str
		mov eax, 0
		int 0x80

		/* terminate */
		mov eax, 1
		int 0x80
	}
	for (;;);
}

このプロセスでは、メッセージの表示と終了にシステムコールを使用していることに注意してください。これらのシステムコールは、以前の章で実装したシステムAPIに追加されたものです。 Int 0x80 関数 0 は DebugPrintfを呼び出しInt 0x80 関数 1は新しい関数 TerminateProcessです。 このデモの機能を向上させるために、さらにシステムサービスを追加することができます。

TerminateProcessはプロセスリソースをクリーンアップし、カーネルコマンドシェルに実行を戻す役割を 担っています。int 0x80が 実行されると、CPUはカーネルモードに移行し、CS、SS、ESPをTSSからそれぞれの値に復元することを思い出して ください。 これによりTerminateProcessから直接カーネル関数を呼び出したり、カーネルコマンドシェルを呼び出したりすることができるようになります。

extern "C" {
void TerminateProcess () {
	process* cur = &_proc;
	if (cur->id==PROC_INVALID_ID)
		return;

	/* release threads */
	int i=0;
	thread* pThread = &cur->threads[i];

	/* get physical address of stack */
	void* stackFrame = vmmngr_getPhysicalAddress (cur->pageDirectory,
		(uint32_t) pThread->initialStack); 

	/* unmap and release stack memory */
	vmmngr_unmapPhysicalAddress (cur->pageDirectory, (uint32_t) pThread->initialStack);
	pmmngr_free_block (stackFrame);

	/* unmap and release image memory */
	for (uint32_t page = 0; page < pThread->imageSize/PAGE_SIZE; page++) {
		uint32_t phys = 0;
		uint32_t virt = 0;

		/* get virtual address of page */
		virt = pThread->imageBase + (page * PAGE_SIZE);

		/* get physical address of page */
		phys = (uint32_t) vmmngr_getPhysicalAddress (cur->pageDirectory, virt);

		/* unmap and release page */
		vmmngr_unmapPhysicalAddress (cur->pageDirectory, virt);
		pmmngr_free_block ((void*)phys);
	}

	/* restore kernel selectors */
	__asm {
		cli
		mov eax, 0x10
		mov ds, ax
		mov es, ax
		mov fs, ax
		mov gs, ax
		sti
	}

	/* return to kernel command shell */
	run ();

	DebugPrintf ("\nExit command recieved; demo halted");
	for (;;);
}
} // extern "C"

結論

この章では、プロセス、スレッド、プロセス管理について調べ、基本的なプロセス 管理のサポートを構築しました。オペレーティングシステムにとって大きな節目となる、ディスクからのユーザモードプログラムの実行に必要なすべてをカバーしました。

次章では、本章で実装したプロセス管理機能をベースに、スケジューラを構築し、プリエンプティブ・マルチタスクを完全にサポートします。