続・玩式草子 ―戯れせんとや生まれけん―

第11回Plamo-7.1とinitrd[2]

前回Plamo-7.1から採用することにしたinitrdの概要を紹介しました。その際に触れたように、initrdはドライバ・モジュールを組み込むことに特化したLinuxシステムで、機能はシンプルなものの、Linuxシステムを運用するためのファイル一式が必要なことを紹介しました。

それらのファイルを過不足なく揃え、適切に配置してinitrdイメージを作成するのがmkinitramfsスクリプトです。今回はこのスクリプトの処理を解説しながらinitrdの作り方を紹介しましょう。

/sbin/mkinitramfs

前回、initrd上のinitスクリプトをつまみ読みしながら処理の流れを概観したように、initrd上には「ルートファイルシステムをマウントするために必要なドライバ・モジュール「それらモジュールをカーネルに組み込むためのコマンド(実行ファイル)⁠、⁠それら実行ファイルが必要とする共有ライブラリなどが必要となります。mkinitramfsはこれら必要なファイルを集めてinitrdイメージを作るツールです。

mkinitramfsはシンプルなシェルスクリプトなものの、全文を掲載するには長すぎるので、つまみ読みしながら処理の流れを紹介してみます。このスクリプトは、見だしのようにPlamo-7.1環境では/sbin/mkinitramfsにありますので、全体を見たい方はそちらでご確認ください。また、このコードのオリジナルはBLFSのサイトで参照できます。

必要なコマンドの準備

mkinitramfsでは、最初の30行ほどで内部処理用のcopyコマンドを定義したり、ドライバ・モジュールのディレクトリを確認するなどの初期化処理を行った後、initrd上で使うコマンドを$binfiles$sbinfilesに指定していきます。

 36  printf "Creating $INITRAMFS_FILE... "
 37  
 38  binfiles="sh cat cp dd killall ls mkdir mknod mount "
 39  binfiles="$binfiles umount sed sleep ln rm uname"
 40  binfiles="$binfiles readlink basename"
 ...
 45  sbinfiles="modprobe blkid switch_root"
 46  
 47  #Optional files and locations
 48  for f in mdadm mdmon udevd udevadm; do
 49    if [ -x /sbin/$f ] ; then sbinfiles="$sbinfiles $f"; fi
 50  done

これらのうち、$binfilesに指定したコマンド(の実行ファイル)はinitrdのbinディレクトリに、$sbinfilesに指定した実行ファイルはsbinディレクトリに、それぞれコピーされます。

また、もう少し先でkmodlvm, dmsetupが追加されるので、initrdに含まれる実行ファイルは総計27個となります。

作業用ディレクトリの作成

次に/tmpディレクトリに作業用の一時ファイル($unsorted)とディレクトリ($WDIR)を用意します。

 52  unsorted=$(mktemp /tmp/unsorted.XXXXXXXXXX)
 ...
 57  # Create a temporary working directory
 58  WDIR=$(mktemp -d /tmp/initrd-work.XXXXXXXXXX)
 59  

$unsortedは先に指定した27個の実行ファイルが必要とする共有ライブラリを調べる際に使うファイルで、後述するように各実行ファイルにlddした結果を収めていきます。一方、$WDIRはCPIOイメージを作る際の作業ディレクトリになります。

次に、$WDIRにディレクトリ構造を作ります。initrdはシンプルなものの独立したLinux環境なので、ルートファイルシステムのディレクトリ構造一式が必要となります。

 60  # Create base directory structure
 61  mkdir -p $WDIR/{bin,dev,lib/firmware,run,sbin,sys,proc,usr}
 62  mkdir -p $WDIR/etc/{modprobe.d,udev/rules.d}
 63  touch $WDIR/etc/modprobe.d/modprobe.conf
 64  ln -s lib $WDIR/lib64
 65  ln -s ../bin $WDIR/usr/bin

initrd用の/dev/console/dev/nullを作ります。ほとんどのデバイスファイルはudevdが動的に作るものの、udevdのメッセージを表示したり、不要なメッセージを捨てたりするデバイスファイルはあらかじめ用意しておく必要があります。

 67  # Create necessary device nodes
 68  mknod -m 640 $WDIR/dev/console c 5 1
 69  mknod -m 664 $WDIR/dev/null    c 1 3

実行ファイルと共有ライブラリのコピー

まず、udev用の設定ファイルやルールファイルを、動作中の環境から作業用ディレクトリへコピーします。

 71  # Install the udev configuration files
 72  if [ -f /etc/udev/udev.conf ]; then
 73    cp /etc/udev/udev.conf $WDIR/etc/udev/udev.conf
 74  fi
 75  
 76  for file in $(find /etc/udev/rules.d/ -type f) ; do
 77    cp $file $WDIR/etc/udev/rules.d
 78  done

代入している部分は省略したものの、89行目で使っている$DATADIR/$INITINは/usr/share/mkinitramfs/init.inで、あらかじめ用意してあるこのファイルを、initrd上のinitとしてインストールします。このファイルは前回紹介しました。

 88  # Install the init file
 89  install -m0755 $DATADIR/$INITIN $WDIR/init

ドライバ・モジュールを組み込むためのkmodを$binfilesのリストに追加します。念のため、古いmodule-init-toolsを使っている場合も考慮しています。

 91  if [  -n "$KERNEL_VERSION" ] ; then
 92    if [ -x /bin/kmod ] ; then
 93      binfiles="$binfiles kmod"
 94    else
 95      binfiles="$binfiles lsmod"
 96      sbinfiles="$sbinfiles insmod"
 97    fi
 98  fi

次に$binfilesに指定したコマンド(実行ファイル)を作業用ディレクトリ内へコピーします。$binfilesにはコマンド名しか指定していないため、実行ファイルが/bin/にあるのか/usr/bin/にあるのかをチェックして(102行目⁠⁠、利用する共有ライブラリの情報を$unsortedに記録(103行目)してから、作業用ディレクトリのbin/以下にコピーしていきます。なお、ここで使っているcopyは、省略した部分(5~22行目)で定義している内部コマンドで、宛先を$WDIR内に限定したコピーです。

100  # Install basic binaries
101  for f in $binfiles ; do
102    if [ -e /bin/$f ]; then d="/bin"; else d="/usr/bin"; fi
103    ldd $d/$f | sed "s/\t//" | cut -d " " -f1 >> $unsorted
104    copy $d/$f bin
105  done

同様に$sbinfilesに指定したコマンド(実行ファイル)を作業用ディレクトリ内へコピーします。こちらの実行ファイルは/sbin/以下にあるはずなので一手間省けますが、共有ライブラリは忘れずにチェックします。ここまでで、initrdに組み込む実行ファイルが必要とする共有ライブラリの一覧(重複あり)が$unsortedに記録されました。

110  for f in $sbinfiles ; do
111    ldd /sbin/$f | sed "s/\t//" | cut -d " " -f1 >> $unsorted
112    copy $f sbin
113  done

次に、$unsortedに集めた共有ライブラリのリストからsortとuniqを使って重複を除き、残ったライブラリを作業ディレクトリのlib/以下にコピーします。155~157行目は、lddコマンドでは共有ライブラリとして表示されるものの、カーネルが提供する仮想的なライブラリで実体は存在しない"linux-vdso.1"や"linux-gate.so.1"を省く処理です。

153  # Install libraries
154  sort $unsorted | uniq | while read library ; do
155    if [ "$library" == "linux-vdso.so.1" ] ||
156       [ "$library" == "linux-gate.so.1" ]; then
157      continue
158    fi
159  
160    copy $library lib
161  done

このあたり、コードとしてはそれほど凝ったことをしているわけではないものの、initrdを自作しようと苦労したことがある人間にとって、これだけの処理でinitrdに必要な共有ライブラリが揃うというのは、結構「目から鱗」な体験でした。

udevは/lib/udevに支援用の実行ファイルがあるので、それらをまとめて作業用ディレクトリにコピーします。

163  if [ -d /lib/udev ]; then
164    cp -a /lib/udev $WDIR/lib
165  fi

ドライバ・モジュールのコピー

次に、findcpioを使って、カーネルのドライバ・モジュールを作業用ディレクトリにコピーします。

170  # Install the kernel modules if requested
171  if [ -n "$KERNEL_VERSION" ]; then
172    find                                                                        \
173       /lib/modules/$KERNEL_VERSION/kernel/{crypto,fs,lib}                      \
174       /lib/modules/$KERNEL_VERSION/kernel/drivers/{block,ata,md,firewire,mmc,mtd}      \
175       /lib/modules/$KERNEL_VERSION/kernel/drivers/{scsi,message,pcmcia,virtio} \
176       /lib/modules/$KERNEL_VERSION/kernel/drivers/usb/{host,storage}           \
177       -type f 2> /dev/null | cpio --make-directories -p --quiet $WDIR
178  

このあたり、ちょっと凝ったコードなので多少わかりにくいですが、172~177行は一連のコマンドになっていて、まず"find"コマンドの引数にinitrdに収めたいドライバ・モジュールのあるディレクトリをbashの拡張機能である「ブレース展開("{...}"⁠⁠」で列挙し、"-type f"でそれらディレクトリにある通常ファイル(=ドライバ・モジュール)の一覧を表示します。

一方、そのファイル一覧をパイプ("|")経由で受けとるcpioコマンドは、"-p"オプションの指定でコピーパスモードで動作し、受けとったファイル名を作業用ディレクトリである$WDIR以下にコピーします。その際、"--make-directories"オプションを指定しているので必要なディレクトリも作成され、/lib/modules/$KERNEL_VERSION/kernel 以下のディレクトリ構成が、そのまま作業用ディレクトリの lib/modules/以下に再現されるわけです。

このあたり、cpioコマンドを使わずに書こうとすると、それぞれのドライバ・モジュールごとにdirnameして収められているディレクトリを調べ、mkdir -pでそのディレクトリを作った上で、basenameで調べたドライバ・モジュール名にコピーする、みたいな作業が必要になるでしょう。

cpioはUNIXの初期に開発されたアーカイブツールで、さまざまな機能が追加された長い歴史を持つ分、オプション指定が複雑で、初心者には敬遠されがちなコマンドです。その分、用途にぴったりとハマったこのような使い方を見せられると、⁠⁠なるほど」と感心してしまいます。

ドライバ・モジュールの依存関係の更新とCPIOアーカイブ化

次に、カーネルに組みこんだドライバのリスト(modules.builtin)やドライバ・モジュールの一覧表(modules.order)をコピーし、depmodコマンドで作業用ディレクトリ内のモジュール・ドライバに限定した依存関係データベースを作成します。

179    cp /lib/modules/$KERNEL_VERSION/modules.{builtin,order}                     \
180              $WDIR/lib/modules/$KERNEL_VERSION
181  
182    depmod -b $WDIR $KERNEL_VERSION
183  fi

以上でカーネルのドライバ・モジュールとそれらの依存関係情報、モジュールを組み込むためのコマンドやライブラリ一式が作業用ディレクトリに揃ったので、それらをCPIO形式にまとめてgzipで圧縮すればinitrdイメージが完成します。最後に、不要になった作業用ファイルとディレクトリを削除します。

185  ( cd $WDIR ; find . | cpio -o -H newc --quiet | gzip -9 ) > $INITRAMFS_FILE
...
188  rm -rf $WDIR $unsorted
189  printf "done.\n"

なお、今回紹介したのはmkinitramfs-0.2のコードで、最近更新したmkinitramfs-0.4では、最後の部分にCPU用のfirmwareを追加する処理を加えており、それらについては次回紹介する予定です。

mkinitramfsの特徴

以上、Plamo-7.1で採用した、LFS/BLFS由来のmkinitramfsについて紹介しました。このmkinitramfsは、インストール済のドライバ・モジュールのうち、ルートファイルシステムのマウントに必要なモジュールは、使う使わないに関わらず全て収めてしまおう、という大胆な設計になっているのが特徴です。

この設計方針は従来のinitrdに慣れている人には奇異に見えるかも知れません。というのも、CentOSやArch Linuxなど主要なディストリビューションが採用しているinitrdでは、インストール済のドライバ・モジュールのうち、その環境で実際にカーネルに組み込まれたモジュールのみをinitrdに組み込む、という設計になっているからです。

前者の設計では、その環境には不要なドライバ・モジュールも大量にinitrdに組み込まれるため、起動時の読み込みに余計な時間がかかります。一方、後者の設計ではinitrdのサイズはコンパクトになるものの、動作中のカーネルに組み込まれたドライバ・モジュールのうち、どのモジュールをinitrdに持ち込むかの判断が必要で、その際はモジュールの依存関係にも考慮を払う必要があります。

また、前者の設計ではカーネル毎(ごと)にinitrdを作り置きできるのに対し、後者ではインストール毎にinitrdを作ることになるため、作成されるinitrdは環境によって異なり、トラブル時の対応が難しくなりそうです。

LFS/BLFSの場合、手作りできるシンプルさが何よりも重視されるので、多少initrdのサイズが大きくなっても構わない今回のような設計になったように思うものの、ディストリビューションとして使うには改善の余地がありそうなので、今後の検討課題と考えています。


今回紹介したLFS/BLFS由来のmkinitramfsは、主流のディストリビューションが採用しているmkinitramfsよりも粗い作りになっているものの、その分汎用性は高く、$binfilesや$sbinfilesに使いたいコマンドを追加し、initスクリプトを調整することで、ルートファイルシステムをマウントする以外の機能、たとえばルータやfirewall専用のinitrdイメージも簡単に作れそうです。このように既存ツールの新しい応用方法を考えていくのも、ソースコードが公開されているOSSならではの魅力でしょう。

おすすめ記事

記事・ニュース一覧