カーネルの構造と機能
この章では狭義のオペレーティングシステムであるカーネルの概観とその構造と機能を見ていきます。
カーネル空間とユーザ空間
いろいろな切口でカーネルを眺めることはできますが、最初はわかりやすいであろう「プログラムが動作する」という切口から考えてみます。
プログラムが動作する時、システム上では2つの処理空間で処理が行われます。1つはユーザ空間、もう1つがカーネル空間です。この2つの空間を行き来して処理を進めます。といっても、プログラムが動作する際、どちらの空間で処理されているかといったことをユーザが意識することは、まずありませ ん。
---------------------- ユーザ空間 ---------------------- カーネル空間 ----------------------
ユーザ空間は、ユーザに割り当てられるプログラムがアクセス可能な計算資源です。一方、カーネル空間はユーザが直接アクセスできない空間で、システムコール(UNIXのカーネルAPI) を呼ぶことによって始めて、カーネルの機能を利用できます。ユーザからは直接カーネル空間を操作することはできません。Kernel(核)という言葉は元々は堅い殻に守られた種の意味です。この意味のようにユーザ側から見ると、カーネル空間は堅い殻に守られたオペレーティングシステム内部というように見えます。
ここでプログラムがデータを処理し、ファイルにデータを書き込む時を考えてみましょう。ユーザ空間でデータが処理され、write(2)でファイルに書込にいきます。すると、制御は一旦カーネル空間に移ります。カーネルでの処理が済んで、またユーザ空間に戻ってきます。このようにカーネルはユーザ空間で動くプログラムの制御と、カーネル内での必要な資源の提供と管理を行っています。以下に単純化したモデルを示してみます。
- 補足
- write(2)と書いている2の意味はオンラインマニュアルの区分を示しています。2はシステムコール、3はライブラリの意味です。 manコマンドにオプション 2 write と与えるとwrite(2)が表示されます。
write(fd,buf,length) . ------------------->時間軸 . ------------------------------ ユーザ空間 ----+ +------>処理の流れ | | -----|--------|---------------- | | +-+ +-+ カーネル空間 -------|---|------------------ +--+ ハードウェア資源
システムコール
システムコールはユーザ空間からカーネル空間で処理を行うための切替えポイントです。今風にいうならばカーネルとのAPIとして用意された関数群です。UNIXではシステムコールと呼びます。
カーネル空間に処理が入ってしまうと基本的にユーザはタッチすることができません。もし制限なしにユーザ側からカーネル空間を操作できるということであれば、システムのどの部分にもアクセスできるということと同じ意味なので、極めて重大なシステム障害やセキュリティ侵害をいとも簡単におこすことが出来るようになります。
システムコールはたくさんあります。どのようなシステムコールがあるのかを調べるにはオンラインマニュアルをチェックするのがいいでしょう。システムコールはセクション2に含まれています。オンラインマニュアルのファイル数を数えると約300個ありました。
- 調べてみよう
- 今使っているLinuxにはおおよそいくつのシステムコールが用意されているのが調べてみよう。次に、既に使われなくなりなくなったシステムコールと、互換性のために残してあるが使用するのに推奨されていないシステムコールを1つ以上みつけてみよう。欠番となっているシステムコールを探すには/usr/include/asm/unistd.hがヒントになります。
ユーザ側から見た目はシステムコールもライブラリ関数も一見違いがありません。ただし実際の動作ではシステムコールの呼出は、ユーザ空間からカーネル空間へモードが移行するコストがかかるので、たとえまったく同じ内容の処理をするにしても、同じユーザ空間で動作しているライブラリを呼び出すよりもコストがかかります。
- 補足
- この逆のこともいえます。システムから上がってくる処理をユーザ空間まで処理を移行せず、直接カーネルの中で処理をすれば、それだけ移行するコストを削減でき処理が速くなります。例としてはLinux 2.4系に入っていたkhttpd(カーネル版サーバ:ただし2.6系では削除されています) があります。
もう少し、システムコールとライブラリ関数の関係を見てみます。システムコールであるwrite (2)と ライブラリ関数であるfwrite(3) は、一見似たようなものに見えます。
write (2) #include <unistd.h> ssize_t write(int fd, const void *buf, size_t count);
fwrite(3) #include <stdio.h> size_t fwrite( const void *ptr, size_t size, size_t nmemb, FILE *stream);
fwrite(3)の方は、ユーザ空間で動作していて、さらに入出力を効率的にするためのバッファを用いています。バッファはファイルポインタFILE *stream が保持しています。/usr/include/libio.hの構造体である struct _IO_FILE を見てみるとわかります。ですからfwrite(3)を呼び出したからといって、その先のファイル(あるいは書き出す実体)へ書き込んでいるとは限りません。
write(2)はオープンしているディスクリプタfdに書き込みます。ですからファイル(書き出す実体)へ、データを書き込みます。write(2)を使うと入出力効率が落ちるならば、fwrite(3)のような機能をシステムコールレベルでサポートすればいいではないかという考え方をしたくなるはずです。
しかし、そのようなことをすればするほど、当然ながらカーネルのコードサイズが増大していきます。カーネルが肥大すればするほど、その分、移植性が損なわれていきますし、また、一部を書き換えようとすると、カーネルやカーネルモジュールを入れ換えることになってしまい非効率的です。カーネルは最低限のことをすばやく動かすことをするすべきであって、それ以外、別にカーネルでなくてもできる範囲は、ユーザ空間ですべきであるという切り分けの方が安定性やメンテナンス性が向上するというアドバンテージがあります。逆に、これが将来的に発展性も乏しく、新しい機能も入らず固定化しているような、停滞しているようなオペレーティングシステムであれば、どんどんカーネルに組み込んでいく可能性もあるでしょう。
- 補足
- 少なくとも、カーネルに関しては、何でもかんでも組み入れて肥大化していく例は見当たりませんが、一般的なアプリケーションに関してはよく見られる傾向です。もちろん"Small is Beautiful"という言葉が好きなUNIX文化圏では、基本的なレベルに分割されるべきソフトウェアが肥大していくのはよくない傾向であると考えられています。
カーネルの構造
カーネルの構造を考えるために、カーネルが提供している主要な機能を書き出してみます。この分類はカーネルのソースコードレベルで分類しているのを参考にしています。個々のソースコードファイルは機能別に分割されており、さらにディレクトリは大きなレベルでの機能の違いで分類されています。これを分類の指針とするとかなり判りやすくなります。
個々の説明は後の章に譲るとして、ここでは概観と個々の機能の簡単な説明をします。
- 補足
- ここに書き出しているものがカーネルのすべての機能というわけではなく、この授業用テキストで取り上げる注目点をリストアップしています。
- プロセス管理
- プロセス生成・消滅
- スケジュラ
- ファイルシステム
- 名前空間管理
- VFS
- ext2, etc3, JFS, ReiserFS, XFS, ISO9660...
- ネットワークファイルシステム
- デバイスファイル
- プロセス間通信
- セマフォ
- メッセージキュー
- パイプ, fifo (named pipe)
- 記憶管理
- ページ管理
- バーチャルメモリ (swap)
- mmap
- シェアードメモリ
- ネットワーク
- IPスタック (TCP/UDP/ICMP/..)
- その他プロトコル
- デバイスドライバ
- 仮想デバイス
- 各種I/Oドライバ
+---------------------------------------------------------------+ | アプリケーション | +---------------------------------------------------------------+ | システムコール ・ ファイル(デバイスファイルなど) | +------------+----- --+----------------+--------------+---------+ |プロセス管理 |記憶管理|ファイルシステム |プロセス間通信 |ネットワーク | | | +----+----------------+--------------+----------+ | | | デバイスドライバ | | | | ( 仮想デバイス ・ 各種 I/Oドライバ ) | +---------------------------------------------------------------+ | 各種ハードウェア | +---------------------------------------------------------------+
プロセス管理
プロセス管理は、プロセスの生成と消滅についての処理と、そのプロセスのスケジュールを管理するスケジュラーの部分です。利用しているCPU数と比べプロセス数の方がはるかに多いわけですから、プロセスに上手に計算資源を割り振ってあげなければなりません。
ここではプロセスという言葉をつかっていますが、タスクやスレッドという言葉もあります。ある処理の計算資源と処理の流れをすべて含めて呼ぶ時はタスクです。計算資源は共有していてる複数あるいは単数の処理の流れのことをスレッドと呼びます。ですから1つのタスクで複数のスレッドを持つことができます。1つのタスクに対し、1つのスレッドだけの場合、これをUNIXの伝統的な呼び方であるプロセスと呼びます。1タスク・1スレッドが伝統的な意味でのUNIXのプロセスと同等の意味を持ちます。
- 補足
- UNIXは最初からスレッドを持っていたわけではありません。そのため80年代に入ってから色々な組織がスレッドの実装を試みました。Brown Univ.のブラウンスレッド、サンマイクロシステムズのLWP (Lightweiht Process)、DECのCMA スレッド、CMUのCスレッドパッケージがあります。各々互換性はありません。紆余曲折あり、現在ではPOSIX仕様のスレッドであるpthreads(7)が主流になっています。
プロセスのスケジュリングでは、Linuxの場合、 システムコールsched_setscheduler(2)を使い、プロセスのスケジュリングの方法を指定することができます。
- SCHED_FIFO: First-In-First-Out
- SCHED_RR: ラウンドロビン
- SCHED_OTHER: デフォルトのLinuxのタイムシェア
- SCHED_BATCH: バッチでの処理を行う(2.6.12以降)
FIFOは、そのプロセスがCPU資源を占有する方式です。CPU資源を占有といっても、一般ユーザで動作するよりもさらに、優先度よりも高い優先度で動くプロセスあると、そちらの方を優先します。ラウンドロビンは順繰りにプロセスが処理されます。デフォルトでは、プロセスの動作時間を見て、優先順位を計算し、その優先順位が高い方から処理するようになっています。このスケジュリングの処理アルゴリズムは Linux 2.4 と Linux 2.6 では入れ換えれておりLinux 2.6では処理効率がよくなっています。2.6.12以降ではあたらしくSCHED_BATCHのスケジュールが加わるなど、オペレーティングシステムの心臓部ともいえるスケジュラー部分でも進化しています。
- 補足
- 別な見地からいけば、Linux 2.4あたりまでのスケジュラーはやはり他のオペレーティングシステムからみると見劣りするものだというのは否定できない事実です。