カーネルの構造と機能
この章では狭義のオペレーティングシステムであるカーネルの概観とその構造と機能を見ていきます。
カーネル空間とユーザ空間
何をさしてオペレーティングシステムと呼ぶのかの議論でもでも少し取り上げましたが、 いろいろな切口でカーネルを眺めることはできます。ここでは、最初はわかりやすいであろう「プログラムが動作する」という切口から考えてみます。
プログラムが動作する時、システム上では2つの処理空間で処理が行われます。1つはユーザ空間(User space)、もう1つがカーネル空間(Kernel space)です。この2つの空間を行き来して処理を進めます。といっても、プログラムが動作する際、どちらの空間で処理されているかといったことをユーザが意識する必要はありません。
ユーザ空間は、ユーザに割り当てられる計算リソースで、実行しているプログラムが直接アクセス可能なリソースです。
一方、カーネル空間はユーザが直接アクセスできない空間です。システムコール(UNIXのカーネルAPI) を呼ぶことや、
あるいは/sys/以下のファイルのようにファイルの形でカーネルへのインタフェースを介してカーネルの機能を利用できる形にしています。ユーザからは直接カーネル空間を操作することはできません。Kernel(核)という言葉は元々は堅い殻に守られた種の意味ですが、この意味のようにユーザ側から見ると、カーネル空間は堅い殻に守られたオペレーティングシステム内部というように見えます。
ここでプログラムがデータを処理し、ファイルにデータを書き込む時を考えてみましょう。ユーザ空間でデータが処理され、write(2)でファイルに書込にいきます。すると、制御は一旦カーネル空間に移ります。カーネルでの処理が済んで、またユーザ空間に戻ってきます。このようにカーネルはユーザ空間で動くプログラムの制御と、カーネル内での必要な資源の提供と管理を行っています。以下に単純化したモデルを示してみます。
- 補足
- write(2)と書いている2の意味はオンラインマニュアルの区分を示しています。2はシステムコール、3はライブラリの意味です。 manコマンドにオプション 2 write と与えるとwrite(2)が表示されます。
システムコール
システムコールはユーザ空間からカーネル空間で処理を行うための切替えポイントです。今風にいうならばカーネルとのAPIとして用意された関数群です。UNIXではシステムコールと呼びます。
カーネル空間に処理が入ってしまうと基本的にユーザはタッチすることができません。もし制限なしにユーザ側からカーネル空間を操作できるということであれば、システムのどの部分にもアクセスできるということと同じ意味なので、極めて重大なシステム障害やセキュリティ侵害をいとも簡単におこすことが出来るようになります。
システムコール
[1]はたくさんあります。どのようなシステムコールがあるのかを調べるにはオンラインマニュアルをチェックするのがいいでしょう。システムコールはセクション2
[2]
に含まれています。オンラインマニュアルのファイル数を数えると約300個ありました。
- 調べてみよう
- 今使っているLinuxにはおおよそいくつのシステムコールが用意されているのが調べてみよう。次に、既に使われなくなりなくなったシステムコールと、互換性のために残してあるが使用するのに推奨されていないシステムコールを1つ以上みつけてみよう。欠番となっているシステムコールを探すには
/usr/include/asm/unistd.h がヒントになります。
ユーザ側から見た目はシステムコールもライブラリ関数も一見違いがありません。ただし実際の動作ではシステムコールの呼出は、ユーザ空間からカーネル空間へモードが移行するコストがかかるので、仮にまったく同じ内容の処理をするにしても、同じユーザ空間で動作しているライブラリを呼び出すよりもコストがかかります。
この逆のこともいえます。システムから上がってくる処理をユーザ空間まで処理を移行せず、直接カーネルの中で処理をすれば、それだけ移行するコストを削減でき処理が速くなります。あまり良い例えとはいえないのですがLinux 2.4系に入っていたkhttpd カーネル版サーバ[3]
(ただし2.6系以降では削除されています) は、ある種のhttpリクエストのベンチマークに対して驚異的なhttpリクエストを処理します。
- 補足
- khttpd は本気でWebの能力をカーネルに入れるためではなく、質の悪いベンチマークテストへの皮肉として入れているので、Web処理を本気でカーネルで処理しようとしたなどと誤解しないようにしてください。
システムコールとライブラリ関数
もう少し、システムコールとライブラリ関数の関係を見てみます。システムコールである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ドライバ
プロセス管理
プロセス管理は、プロセスの生成と消滅についての処理と、そのプロセスのスケジュールを管理するスケジュラーの部分です。利用しているCPU数と比べプロセス数の方がはるかに多いわけですから、プロセスに上手に計算資源を割り振ってあげなければなりません。
ここではプロセスという言葉をつかっていますが、タスクやスレッドという言葉もあります。ある処理の計算資源と処理の流れをすべて含めて呼ぶ時はタスクです。計算資源は共有し、さらにCPU資源を個別に割り当てられている複数あるいは単数の処理の流れのことをスレッドと呼びます。ですから1つのタスクで複数のスレッドを持つことができます。1つのタスクに対し、1つのスレッドだけの場合、UNIXの伝統的なプロセスと同等であり、呼び方もプロセスと呼んでかまいません。
- 補足
- CPU(演算装置)資源は、プログラムが実行するのに必要な計算資源の中の1つにしか過ぎません。また1つの計算資源の中に複数のCPU資源を持つことが可能です。
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以降)
- SCHED_IDLE: 非常に低いプライオリティ
FIFOは、そのプロセスがCPU資源を占有する方式です。CPU資源を占有といっても、一般ユーザで動作するよりもさらに、優先度よりも高い優先度で動くプロセスあると、そちらの方を優先します。ラウンドロビンは順繰りにプロセスが処理されます。デフォルトでは、プロセスの動作時間を見て、優先順位を計算し、その優先順位が高い方から処理するようになっています。このスケジュリングの処理アルゴリズムはLinux 2.6 では入れ換えれており処理効率がよくなっています。
2.6.12ではあたらしくSCHED_BATCHのスケジュールが加わり、
2.6.23ではCFS( Completely Fair Scheduler )スケジューラ
[4]
が使えるようになりました。
オペレーティングシステムの心臓部ともいえるスケジュラー部分でも進化は続いています。
ファイルシステム
ファイルシステムを説明する前に、まずはUNIXのファイルという考え方について説明します。 UNIXのファイルの特徴はファイルがバイト単位のデータストリームです。 ファイルの中身というのは最初から最後に向かって1バイトづつのデータの列であるだけでそれ以上でもそれ以下でもありません。 カーネルでは、そこまでしか関知しません。データの扱いに関してはプログラム側が決めます。
- 補足
- デバイスの観点からファイルをみるとブロックデバイスなのか、あるいはキャラクタデバイスなのかの違いはあります。
次にファイルシステムで大切な点は、"/"を先頭に木構造になっており /foo/bar.txt といった具合にファイルパスとしてアクセスできることです。
ファイルの情報はディレクトリと呼ばれる場所に記録されていることです。この名前でアクセスできる名前の空間のことをネームスペース(name space)といいます。
今日ではこの木構造の名前空間を持ったファイルシステムは一般的になっていますが、これはUNIX以降に一般化していったことは覚えておいても損はないと思います。
バイト単位のデータストリームや、あるいはファイルをファイルパスで探すというのは、今やあたりまえのような気がしますが、UNIX以前では物理的な場所をまずプログラムを稼働させるための前準備の段階で形式や場所を定義しておいて、そこを参照する形を取っていました。大型汎用機のファイルシステムではいまでもそのような方式があります。
- 調べてみよう
- 大型汎用機[5]ではどのようにファイルを扱うのか調べてみよう。
Windows NT以降のNTFSの持つファイルシステムの機能はUNIX系と遜色ありません。しかし、C:とかD:といった場所はドライブと呼び、
ハードウェアとファイルシステムが直結していた頃の名残を残しています。
ちなみにWindows系の世界でもMS-DOS時代の名残でフォルダのことをディレクトリと呼ぶ場合もあります。
プログラム側から見た場合、ファイルには同じようにアクセスできるのですが、最終的な記憶装置、あるいはオブジェクトへの読み書きをする部分には ext2、 ext3、 XFS、 JFS、 OCFS2 などいろいろな種類のファイルシステム[6]が用意されています。ファイルシステムの部分でも、このようにレイヤ化されています。また名前付きパイプ(FIFO)はプログラム間でデータを"流す"ために使われる見かけ上のファイルです。あるいは/procなどのファイルは、実際にはファイルではなくファイルのように見せかけたカーネルとのAPIになっています。
唯一の共通点は、ファイルシステム上の名前空間に存在し、統一的なファイルへのアクセスが提供されているということです。そのためプログラム側はファイルを介すという形でのデータの入出力のためのアクセス方法がシンプルになります。
- 補足
- /p/tcp/www.sample.com/wwwというファイルをアクセスするとwww.sample.com の www ポートがアクセスできるというファイルシステム Portalsを4.4BSDに対して実装した人もいます。
プロセス間通信
ここでのプロセス間通信の意味は同一カーネル上に存在しているプログラム間での通信です。 ここではTCP/IPなどの通信はネットワーク通信と分類しています。
さてプロセス間通信で特徴的なものとして最初にあげるのはセマフォ (semaphore)です。
セマフォは別々に動作するプロセス間での同期を取るためのメカニズムで、
セマフォを介してプロセス間で正しい処理順番を確保するような仕組が出来るようになっています。
どのプロセスからもアクセスでき、そして情報の書き込み、取り出しに関しては順序を守ってくれます。
共有しているデータのロックやロック解除の時を行うような時、セマフォを使いプロセス間のやり取りをし、複数のプロセス間での排他制御などを行います。
- 補足
- ちなみに セマフォ とは列車運行の時につかう路線上に 列車が一本しか運行していないことを保証するために使った昔の腕木信号機の メタファ から来ているとのことです。
メッセージキュー(message queue)はプロセス間でメッセージ(データ)を送出、受信の順序を約束し受け取る機能です。
ちょうどFIFO名前付きパイプと同じような役割です。[7]
記憶管理
記憶管理はプロセスが使っているメモリ空間の割り当ての管理です。 メモリ空間といってもプロセス自体はハードウェアのメモリを直接意識していません。 カーネル側がプロセスに割り当てた仮想的に扱う記憶空間を管理しているため、 プロセスは実際に搭載しているハードウェアのメモリの容量よりも大きな記憶空間を利用することができます。
記憶参照時に記憶領域としては存在しているがハードウェアのメモリ上に存在していない場合は、実行できるようにハードウェア上に読み込みます。 この時、記憶領域として扱う単位のことをページといいます。 その入れ換えとして実行に不必要のない記憶領域はハードウェア上のメモリから他のデバイス(主にハードディスク)へ退逃します。 この入れ換えのことをLinuxではスワップといっています。
- 補足
- 本来のスワップの意味はプロセスが利用してい記憶空間をまるごと入れ替えることを指します。このあたりの用語は今日的意味で使っている用語と歴史的経緯から使い続けられている用語と混乱していますので注意が必要です。
mmap はファイルやデバイスを記憶領域にマップする機能を提供します。たとえばファイルをmmapすると、記憶領域をアクセスするのと同じ方法でファイルへのアクセスします。別の言い方をすると、記憶領域もファイルも同じ扱いになります。
シェアードメモリはプロセス間で同じ記憶空間を共有するメカニズムです。
ネットワーク
現代において、ネットワークの機能はオペレーティングシステムの中でも重要な役割を果たすようになっています。 TCP/IPは1981年に4.1BSDへの組込みがはじまり、1982年には4.1aBSDとして内部のみで使われ [8] 、1983年に4.2BSDの標準機能として広まって以来、 UNIXでは重要な位置を占めてきました。安定して使えるようになって来たのは1986年に出て来た4.3BSDになってからです。
- 補足
- 米国バークレー大学で行われていたBerkeley DARPA UNIX projectはTCP/IPをBSDに実装するプロジェクトで資金は名前の通りDARPAから出ていました。
GNU/Linuxとして使う場合、TCP/IP以外のネットワークプロトコルを扱う以外を使うことは滅多にはありませんが、 LinuxはTCP/IP以外にも色々なネットワークプロトコルをサポートしています。
あと分類という観点からいけば、同じマシン上に存在しているプロセス間の通信に使われる
UNIXソケットもカーネルのソースコードツリーを見てみるとネットワークに分類されているのがわかります。
- 調べてみよう
- 使っているカーネルのバージョンでは、どんなプロトコルをサポートしているだろうか。
デバイスドライバ
様々なデバイスを利用するため、これまた様々なドライバが用意されています。様々なハードウェアをサポートする関係で、カーネルのデバイスドライバのソースコードの量としては格段に多くなっています。IDE、SATA、SCSIといったハードディスク、USBやFirewireのようなデバイス、シリアルデバイス、ビデオビデオカード、特殊な暗号ハードウェア、などなどハードウェアのサポートが増えれば増えるほど、単純に増加しています。たまには複数のデバイスをまとめ整理し共通化したデバイスドライバが作られますが、どんどん増加する傾向にあります。
実際に物理的なデバイスとして存在しているものを使うためではなく、仮想的なデバイスを提供するための仮想デバイスのドライバもあります。たとえばloopbackやdm(Device-mapper)のデバイスドライバなどを使い、ファイル(やデバイス)をブロックデバイスに見立てることができ、そのままファイルをファイルシステムとしてマウントすることができます。
- 調べてみよう
- 1GBのファイルをブロックデバイスに見立てその上にファイルシステムを構築しマウントするためには、どうすればよいのだろうか。(参考: Loopbackを使ったファイルシステム)
脚注
- ↑ システムコールの実装をチェックするのに便利なサイト http://syscalls.kernelgrok.com/
- ↑ /usr/share/man/man2
- ↑ http://www.fenrus.demon.nl/
- ↑ Completely Fair Scheduler によるマルチプロセッシング http://www.ibm.com/developerworks/jp/linux/library/l-cfs/index.html
- ↑ http://www.hitachi.co.jp/Prod/comp/soft1/VOS3/index.html
- ↑ ReiserFSは、まだカーネルに入っているが、あえて抜いた。
- ↑ 加えて一言いえば、筆者は FIFO 名前付きパイプの方が簡単で便利に思えます。 過去のUNIX System Vの互換のために用意しているのではないかと思えてなりません。
- ↑ Twenty Years of Berkeley Unix From AT&T-Owned to Freely Redistributable Marshall Kirk McKusick Early History. Text
目次へ