Ubuntuはカーネルを起動したあと、ルートファイルシステムをマウントするために「Initramfs」というイメージファイルを使用します。今回はこのイメージファイルについて説明しましょう。
Initramfsの役割
Ubuntuはさまざまなディスクデバイスにルートファイルシステムをインストールし、起動するOSです。カーネルはブートローダーによって起動されたあと、ルートファイルシステムをマウントするために、サポートしているすべてのディスクデバイスのドライバを持っている必要があります。
しかしながらこのドライバをすべてカーネルに組み込んでしまうと、カーネルが肥大化してしまいます。しかもそのほとんどは、今使っているディスクデバイスでは不要なドライバです。必要なドライバを必要に応じてロードする仕組みとして「カーネルモジュール」がありますが、今度はその「カーネルモジュール」をどこに保存するのかという問題が発生します。当然のことながら、この時点でルートファイルシステムにはアクセスできません。
そこで出てくるのが「Initramfs」という仕組みです。
Ubuntuに限らずほとんどのLinuxディストリビューションでは、「ミニルート」とも呼ばれるメモリ上に展開可能な、小さなサイズのルートファイルシステムを持っています。このミニルートには、ルートファイルシステムをマウントするために必要なカーネルモジュールやスクリプトが保存されているのです。ブートローダーはカーネルと一緒にこのミニルートをメモリ上に展開し、カーネルにそのアドレスを伝えます。カーネルはそのアドレスを元にミニルートをマウントし、その中にあるスクリプトを実行することで本来のルートファイルシステムをマウントします。
「Initramfs」は、現在はデスクトップLinuxでもっとも使われているミニルートのファイルフォーマットの1つです[1]。Ubnutuだと「/boot/initrd.img-3.19.0-23-generic」などのカーネルバージョンごとファイルになります。
ディスクの障害などによりルートファイルシステムをマウントできなくなったとき、「BusyBoxなんたら」とかいうメッセージとともに「(initramfs)」と表示されることがあります。これはInitramfsの中のBusyBoxのシェルが起動している状態です。普通のシェルに比べると、機能的な制約はありますが、使い方はそれほど変わるわけではありませんので、障害の要因などを追求できます。これもInitramfsの使い方の1つですね。
Initramfsを再構築する
Initramfsはファイルシステムをcpio形式でアーカイブしたうえで、gzipで圧縮したファイルです[2]。このため、以下のコマンドでInitramfsの中身を展開できます。
ちなみにファイルリストを確認したいだけであれば、lsinitramfsという便利なコマンドも存在します。
展開したファイルに変更を加えたあと、新しいInitramfsを作成したい場合は次のコマンドを実行します。
あとはブートローダーにこのInitramfsを使うように設定すれば、新しいInitramfsで起動できるようになります。
カーネルアップデート時のInitramfsの再構築
この「/boot/initrd.img」ですが、dpkgで問い合わせてもどのパッケージにも属していないことがわかります。
実はこのファイルは、カーネルパッケージがアップデートされる度に作り直しているファイルです。Initramfsにはカーネルモジュールを保存します。しかしながらカーネルモジュールは全体のサイズも大きく、いくら圧縮したものとは言え、重複したデータを送るのは非効率です。
またカーネルパッケージが提供するカーネルモジュール以外にも、DKMSなどによってインストールされるモジュールもあります。よってカーネルパッケージで完成されたInitramfsを提供するよりは、システムごとに再生成したほうが効率的なのです。カーネルやカーネルモジュールパッケージのアップデート時は、以下のようなログが表示されるはずです。
これを実現するため、Ubuntuではinitramfs-toolsパッケージにより「update-initramfs」コマンドや「mkinitramfs」コマンドが提供されています。前述のlsinitramfsも同じパッケージに属しています。これらのコマンドは/etc/initramfs-tools以下の設定により、その挙動を変更できます。たとえば圧縮方式、既定のブート方式、起動時にロードすべきモジュールのリスト、その他の設定スクリプトの追加などです。
update-initramfs
update-iniramfsは、/bootにある既存のInitramfsの更新や削除、/bootへの新規Initramfsの作成に使うコマンドです。カーネルアップデート時のフックスクリプトから呼ばれる前提の作りになっているため、ユーザーが直接呼び出すことはあまりないでしょう。
具体的に呼び出しているのは、/etc/kernel/postinst.d/initramfs-toolsです。/etc/kernel以下にはカーネルのインストールや更新、削除のタイミングで呼び出すべきスクリプトを各パッケージがインストールする場所でもあります。postinst.dはカーネルパッケージのインストール後[3]に呼び出すスクリプトを保存します。
これらのスクリプトは第一引数にカーネルのバージョン、第二引数にカーネルイメージの絶対パスが渡ります。
apt-auto-removalはカーネル更新時に古いカーネルパッケージを自動削除するためのスクリプトで、dkmsはモジュールの再ビルドを行うためのスクリプトです。initramfs-toolsではInitramfsを再構築し、pm-utilsでは再起動するまでハイバネートを禁止します。
unattended-upgradesは/var/run/reboot-requiredファイルを作るだけです。これによりByobuなどの各種ツールやログイン時などに再起動が必要であることが検知できるようになります。update-notifierも役割としては同じではありますが、ファイルの中にgettextによってローカライズされた再起動要求メッセージを保存し、要因となったパッケージを/var/run/reboot-required.pkgsに記録します。zz-update-grubはGRUBの設定ファイルの更新スクリプトです。
このうち/etc/kernel/initramfs-toolsは、第二引数からカーネルイメージがある起動用のパス(/boot)を抽出したうえで、以下のコマンドを実行しています。
「-c」オプションはInitramfsの新規作成、「-t」は既存のファイルの上書きの許可、「-k」は作成するカーネルのバージョン、「-b」は作成先の起動ディレクトリを意味します。結果的にInitramfsの作成そのものはmkinitramfsコマンドが行っています。
mkinitramfs
mkinitramfsコマンドは/tmp以下にInitramfsのためにルートファイルシステムを構築したうえで、最終的にcpio形式でアーカイブするコマンドです。
/etc/initramfs-tools/modulesや/usr/share/initramfs-tools/hook-functionsの関数の中で設定されているモジュール、/usr/share/initramfs-tools以下のinitやscripts/、/usr/lib/initramfs-tools/bin/や/usr/lib/klibc/bin/以下、さらには/lib/や/bin/以下のいくつかのバイナリをコピーしたうえで、指定した形式で圧縮します。
コピーするものは各種設定ファイルの中でハードコーディングされています。何か追加や削除したい場合は/etc/initramfs-toolsの中のフックスクリプトで変更してください。
ちなみにBusyBoxは、Initramfs用に機能を落としたbusybox-initramfs版のBusyBoxが使われます。使える機能はかなり限られていますので、Initramfs内部にシェルスクリプトを追加する場合は注意してください。
/usr/lib/initramfs-tools/binには他にも、メモリ上に展開できる圧縮スワップであるramzswapデバイスを操作するためのrzscontrolや、udevを用いて指定したデバイスがマウントされるまで停止するwait-for-rootコマンドも存在します。
Initramfsの起動の流れ
最後に、Initramfsが展開されてからルートファイルシステムをマウントするまでの流れを概観しておきましょう。
カーネルはブートローダーから渡されたInitramfsのアドレスを元に、メモリ上にその内容を展開したあと、「rdinit=」や「init=」オプションで指定されたスクリプト、もしくは/sbin/init、/etc/init、/bin/init、/bin/shのうち、最初に見つかったスクリプトを起動スクリプトとして実行します。
rdinitの初期値は/initですので、何も指定しなければ実行スクリプトとして使われるのはInitramfsの「/init」です。initスクリプトはprocやsysfsのマウント、/devや/tmpなどの作成を行ったあと、/proc/cmdlineをパースしたうえで必要なプログラムを実行します。
「boot=」オプションに何も指定していない場合、「local」スクリプトが取り込まれます。これはいくつかのチェックを経て「root=」で指定されたデバイスを/rootにマウントする、mountroot関数などが定義されているスクリプトです。
マウント済みの/procや/sysをmountコマンドのmoveオプションで/root/以下に移動したら、最後にklibc-utilsパッケージのrun-initコマンドを実行します。run-initでは主に/root以外のInitramfs関連のファイルを削除し、/root(つまりは実際のルートファイルシステム)を「/」にマウントしなおしたうえでchroot(2)を実行し、指定した起動スクリプト(/sbin/init)を呼び出します。
実際のルートファイルシステムにchroot(2)で移動したあとですので、この「/sbin/init」は普通のInitプログラムとなります。つまりUbuntu 14.10以前であればUpstart、15.04以降であればsystemdです。
あとはこのInitプログラムに従って起動プロセスが進んでいき、たとえばUbuntuの起動ロゴを表示するplymouthなどもこのタイミングで起動します。よってこのロゴが表示される前に処理が止まるようであれば図1のようにInitramfs内部で何かエラーが発生している可能性が、ロゴの表示以降に処理が止まるのであればInitramfsよりも後ろのどこかで問題が発生した可能性が高いということがわかるのです。