前回はLinuxの起動の仕組みをブートローダ段階、カーネル段階、/sbin/init段階の3つに分けて解説しました。
その際にも簡単に触れましたが、最近のほとんどのディストリビューションでは、初期化用ramdiskを用いて起動に必要なモジュールドライバを組み込むようになっています。
初期化用ramdiskは、1つの汎用的なカーネル設定であらゆる環境に対応できるようになる、ディストリビューターにとっては便利な仕組みですが、余計な仕組みが入っている分、カーネル再構築の敷居が高くなっていることも事実です。そこで、今回はこの初期化用ramdiskやその元になっているモジュールカーネルの考え方について解説すると共に、実際の初期化用ramdiskを調べてその中身を紹介しようと思います。
モノリシックカーネルとマイクロカーネル
最近では大型コンピュータから携帯機器までカバーしていますが、元々のLinuxはUNIXを参考にしながらPC互換機向けに0から開発されたカーネルです。
最初のバージョンのLinuxは、ヘルシンキ大学の学生だったリーナス・トーバルズ(Linus Torvalds)さんが一人で書きあげました。その際に彼は、プログラミングやデバッグの容易さから、モノリシック(monolithic)カーネルという伝統的なデザインを採用しました。
カーネルには各種ハードウェアを操作するデバイスドライバからファイルシステムやネットワークの操作、メモリやプロセスの管理、スケジューリングといったさまざまな機能が必要です。モノリシック(一枚岩)カーネルとは、これらすべての機能が1つの実行ファイルの中にまとめて組み込まれているタイプのカーネルのことです。
元々UNIXのカーネルはモノリシックカーネルのデザインになっていましたが、OSの研究が進むにつれ、モノリシックカーネルには欠点が目立つようになってきました。
たとえば、モノリシックカーネルは多数の機能が1つの実行ファイルに詰め込まれているため、複雑で肥大化しがちな上、一部の機能に問題が生じても全体に影響が出ますし、新しいハードウェア用のデバイスドライバを追加するだけでも全体を再構築する必要がありました。
それらの欠点を克服するために考案されたのがマイクロ(micro)カーネルというデザインです。マイクロカーネルでは、カーネルに必要なさまざまな機能をカーネル本体とは独立したアプリケーションとして実現し、カーネル本体にはそれら外部アプリケーション間の通信を管理する最小限の機能だけを残すことで、見通しがよく移植性や拡張性を高めることを目指しています。
'80年代の半ばにマイクロカーネルの概念とそれを実装したMachと呼ばれるマイクロカーネルが公開されると、OS研究の中心はモノリシックカーネルからマイクロカーネルへと移り、OSF/1やNEXTSTEPなど新しく開発されたOSではマイクロカーネルのデザインが積極的に採用されるようになり、Linuxが公開されたころにはモノリシックカーネルのデザインは時代遅れと考えられていました。
もっとも、モノリシックカーネルは時代遅れな分、必要な機能や処理の詳細が十分検討され、実現が容易になっていたことも忘れてはならないでしょう。GNUプロジェクトではHurdというマイクロカーネルのデザインを採用したシステムを開発していますが、HurdはLinuxと同じころに開発が始まったものの、完成は大幅に遅れ現在も正式版のリリースには至っていません。
モジュールカーネル
モノリシックカーネルとして生まれ、開発の容易さから多数の開発者を集めることで急速に成長したLinuxですが、リーナスさんを始めとする開発者たちもモノリシックカーネルの問題点は熟知していました。一方、マイクロカーネルではカーネル外部のアプリケーション間の通信が多数発生するためにパフォーマンスが出にくいという問題点も明らかになってきました。そこでLinuxの開発者たちが採用したのは、両者の長所を組み合わせたモジュールカーネルというアプローチです。
LinuxがターゲットとしているPC互換機の場合、利用可能な周辺機器は膨大な種類に及びます。これら周辺機器を操作するためのドライバをすべてカーネルの実行ファイルに組み込むことは現実的ではありません。かと言って、新しい周辺機器を追加する度に実行ファイルを作り直すのも面倒な作業です。
そこでLinuxではこれら周辺機器用のドライバをモジュールとしてカーネル本体の実行ファイルとは独立に用意しておき、必要に応じてそれらをカーネル本体に動的に組み込むという方法を採用しました。
モジュールカーネルの場合、マイクロカーネルのようにOSの本質的な機能を分割してカーネル外部へ出すのではなく、必要な機能を担うモジュールをプラグイン的にカーネル本体に組み込むため、モノリシックカーネルの特徴である効率性は維持しつつ、マイクロカーネルの持つ拡張性も手に入れた形になっています。
当初、モジュール化に対応していたのは一部のデバイスドライバだけでしたが、次第にその範囲は広がり、最近のカーネルではネットワーク機能やファイルシステム、I/Oスケジューラといったカーネルの本質的な部分までモジュール化され、多くの機能を動的に追加したり削除したりすることが可能になっています。
初期化用ramdisk
カーネル本体の実行ファイルと周辺機器用のドライバを分離して、最小機能のみを持つ汎用的なカーネル本体にユーザ環境に合わせた周辺機器用のドライバを組み合わせることで、それぞれのユーザ環境にカスタマイズしたカーネルを利用することができるようになりました。
しかしながら、モジュールドライバはハードディスク(HDD)上に保存しておく必要があるため、HDD用のドライバやファイルシステム用のドライバは単純にカーネル本体から独立させることはできません。
この問題に対処するために考案されたのが初期化用ramdiskです。
初期化用ramdiskとは、その名の通りシステムの初期化用に使われるramdiskで、モジュールドライバやそれらを組み込むためのコマンドなどを収めた小さなファイルシステムをメモリ上に展開し、そこからHDD上のrootファイルシステムを読み込む際に必要となるモジュールドライバなどを読み込ませるようになっています。
初期化用ramdiskは複数のファイルやコマンドを含むファイルシステムですが、HDD上では1つのファイルとしてcpio形式に固めて圧縮されています。
圧縮された初期化用ramdiskファイルは、ブートローダからカーネルのイメージファイルと共にBIOS経由で(カーネルの機能を使わずに)読み込まれ、メモリ上に配置されます。メモリに読み込まれブートローダから処理を委ねられたカーネルは、共に読み込まれた initrd のイメージをramdisk上に展開して仮のrootファイルシステムとしてマウントし、そのファイルシステム上の/initコマンドを実行します。
この/initコマンドが初期化用ramdisk内に用意されているユーザの環境に応じたHDD用のドライバモジュールやrootファイルシステム用のモジュールなどをカーネル本体に組み込むことで、カーネルがHDD上のrootファイルシステムを利用できるようになります。
初期化用ramdisk上の/initは、必要なモジュールの読み込み等を終えれば、HDD上の本来のrootファイルシステムをマウントし、rootファイルシステムを切り替えます。以後の処理は本来のrootファイルシステム上にある/sbin/initの仕事となり、前回説明したように /sbin/init は/etc/inittabを見ながら必要なサービスを起動していきます。
初期化用ramdiskの実際
さて、前置きがずいぶん長くなってしまいましたが、いよいよ初期化用ramdiskの中身を具体的に調べてみることにします。
今回紹介する初期化用ramdiskはFedora Core 9のβ版を手元のVMware Server環境にインストールした際に生成されたものです。初期化用ramdiskのファイル名はカーネルのバージョンやビルド番号によって変わりますし、初期化用ramdiskに組み込まれるモジュールドライバもインストールしたハードウェア環境やファイルシステムの設定によってさまざまに変化するのでご注意ください。
まず、圧縮されている初期化用ramdiskを展開します。初期化用ramdiskは/boot ディレクトリにinitrd-<カーネルバージョン>.imgの名称で保存されています。このファイルは先述のようにcpio形式のアーカイブをgzipで圧縮した形式になっていますので、適当なディレクトリを用意してそこに展開することにします。なお、/boot以下のファイルを読み出すにはroot権限が必要です。
これで初期化用ramdiskが作業用に作成した Initrd ディレクトリ内に展開されました。展開されたディレクトリのいくつかを眺めてみます。
このディレクトリにあるのが初期化用ramdisk内で動作するコマンドです。
lvmは最近のfedora coreが採用しているLVM(Logical Volume Manager)を制御するコマンドで、HDD上のrootファイルシステム自身がLVM上に構築されているため、初期化用ramdiskでLVMを読む準備をしておく必要があります。
modprobeとrmmodはモジュールドライバを組み込んだり取り外したりするコマンド、nashはRed Hatが開発している初期化用ramdisk用のコマンドインタプリタです。nashはシェルの一種のような名称を持っていますが、通常のシェルのような対話機能は持たず、初期化用ramdisk用の初期化スクリプトを実行するための機能に特化しています。
/libディレクトリには /bin以下のコマンドが利用するライブラリが収められています。Fedora Coreの場合、初期化用ramdiskにも通常のCライブラリ(libc-2.7.90)を使っているようです。
/lib/modulesディレクトリには初期化用ramdiskで組み込まれるモジュールドライバが収められています。今回、VMware Server環境用に組み込まれたモジュールドライバは以下の通りでした。
これらのうち、BusLogic.ko、scsi_mod.ko、scsi_wait_scan.ko、sd_mod.koがVMware ServerがエミュレートしているBusLogic製のSCSIアダプタとそれを経由して利用するSCSI HDD用のドライバモジュールです。
dm-mirror.ko、dm-mod.ko、dm-snapshot.ko、dm-zero.koはLVM用のデバイスマッパー用のドライバモジュール、ext3.koとjbd.ko、mbcache.koはext3形式でフォーマットしたrootファイルシステム用のドライバモジュール、echi-hcd.ko、ohci-hcd.ko、uhci-hcd.koはUSBホストコントローラ用のドライバモジュールです。
modules.alias等、modules.XXX という名称のファイルはモジュールドライバ用の設定ファイルで、モジュールドライバの別名定義やそれぞれのモジュールドライバが対応するハードウェアのリストになっています。
この初期化用ramdiskが仮のrootファイルシステムとしてマウントされると、カーネルは/initを実行します。このファイルは先に紹介したnash用のスクリプトになっていますので、まず先頭部分を眺めてみます。
1行目はスクリプトのお約束であるシェバンの指定です。実際の処理の始まりである3行目で各種情報を利用するためにprocファイルシステム、7行目でsysファイルシステムをマウントします。次に、udevを用いて動的にデバイスファイルを生成するため、メモリ上に生成されるtmpファイルシステムを/devにマウントして(9行目)、/dev 上に必要なデバイスファイルを生成していきます(15行目以下)。
その後、42行目あたりから /lib/modules ディレクトリに用意されたモジュール類を組み込んでいきます。
HDDとファイルシステムを読み込むために必要なモジュールを組み込んだ後、LVM用のモジュールを組み込んだ上で、lvmコマンドを実行して論理ボリュームを利用可能にします(73行目)。
その後、nashは76行目で論理ボリューム上のファイルシステムをrootファイルシステムとして設定し、78行目でそれをマウント、新しくマウントしたファイルシステムをrootファイルシステムとして設定し(80行目)SELinuxのポリシーファイルを読み込んだ上で(81行目)、新しいrootファイルシステムに切り替える(83行目)、という処理を行ないます。この処理の結果、rootファイルシステムが初期化用ramdiskからHDD上の本来のrootファイルシステムに切り替わり、以後の処理はHDD上(実際はHDDを抽象化したLVM上)のrootファイルシステムの/sbin/initの仕事になります。
初期化用ramdiskの使い方はディストリビューションごとに大きく異なり、今回紹介したFedora Core以外にも、SUSEやDebianなどさまざまなディストリビューションが独自の考え方から初期化用ramdiskを活用しています。考え方はディストリビューションごとに異なりますが、cpio+gzipという形式は共通していますので、今回紹介した手順で一度自分の使っている初期化用ramdiskを調べてみるのも面白いでしょう。