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

第10回Plamo-7.1とinitrd[1]

前回簡単に紹介したように、新元号が施行された勢いに便乗して、約1年ぶりのバージョンアップとなるPlamo Linux 7.1をリリースしました。

図1 www.plamolinux.orgのリリースアナウンス
図1 www.plamolinux.orgのリリースアナウンス

もっとも最近では、新しいパッケージは随時FTPで公開していて、get_pkginfoコマンドでFTP上のパッケージと同期を取るのも簡単なので、⁠バージョンアップ」といっても、含まれているソフトウェアにそれほど目新しいものはありません。

しかしながら、Plamo-7.0でデフォルトの文字コードをUTF-8に変更したことに続き、今回のPlamo-7.1でもシステムレベルではかなり大きな変更を加えています。それが今回取りあげるinitrdの採用です。

もっともinitrd自体は古くからある技術ですし、Plamo以外のディストリビューションではたいていinitrdを採用しているので、いまさら、という気はするものの、この機会にinitrdの仕組みや動作の流れを紹介してみます。

initrdとinitramfs

Linuxでは、周辺機器用のドライバはカーネル本体とは独立したモジュールとしてビルドでき、/lib/modulesディレクトリ以下から必要に応じて組み込めるようになっています。しかしながら/lib/modulesディレクトリを使うためには、ハードディスク用のドライバとルートファイルシステムに応じたファイルシステム用のドライバが必要で、これらをモジュール化すると「缶詰の中に缶切りが入った」状態になってしまいます。

このような状況を回避するために考案されたのがinitrdです。initrdは、ハードディスクとファイルシステム用のドライバを/lib/modulesとは別に用意しておいて、カーネルがルートファイルシステムをマウントする前に必要なモジュールを組み込んでしまおう、という仕組みです。

initrdの歴史は古く、ソースコードのDocumentation/admin-guide/initrd.rstを見ると、文書の初版は1996年になっています(多分、linux-1.3シリーズの後期⁠⁠。もっとも、当初のinitrdは、INITial RamDiskの名前通り、あらかじめファイルとして作成しておいたファイルシステムをメモリ上に割り当てたram diskにマウントする、というスタイルで、固定サイズのファイルシステムイメージを用意しなければならない等、使い勝手はよくありませんでした。

そのため後になると、ブロックデバイスのram diskではなく、ディスクキャッシュを汎用化する形で実装されたramfsの機能を使うinitramfsが開発されました(2005年前後、linux-2.5の後期⁠⁠。

initramfsはinitrdと同じ機能を果すものの、あらかじめファイルシステムを用意する必要が無く、使いたいコマンドやライブラリを集めたcpioアーカイブを作るだけで利用できるため、急速にinitrdに取って代わりました。しかしながら、grub等のブートローダでは"initrd=...."という指定が定着していることもあり、最近ではinitrd/initramfsの双方を"initrd"という名前で呼ぶようになっています。そのため、Plamo-7.1で採用したのは正確にはinitramfsなものの、通称であるinitrdで通しておきます。

前述のように、initrdはHDDやファイルシステムのようなルートファイルシステムを読み込むのに必要なドライバを、起動時に直接カーネルに組み込む仕組みで、カーネル本体の機能はできるだけ小さくして、必要な機能は環境に応じて動的に組み込むマイクロカーネル的なデザインにするなら必須の機能になります。

しかしながら、モノリシックカーネルであるLinuxでは、ルートファイルシステムをマウントするのに必要なドライバはあらかじめカーネル本体に組み込んでおくことも可能で、カーネル本体が多少大きくなっても構わなければinitrdを使う必要はありません。

一般ユーザ向けのPCを前提にすれば、HDD用のドライバはSATA(libata)だけで間に合いますし、ファイルシステムもext2/3/4系とxfs、btrfsくらいで充分なので、この程度ならカーネル組み込みでも問題ないだろう、と考えて、従来のPlamo Linuxではinitrdを使っていませんでした。

しかしながら最近では、ノートPCを中心にeMMCNVMeといった新しいブロックデバイスが普及してきたり、USBも3.0になるとUAS(USB Attached SCSI)のようにSCSI機能と絡んできたりして、組み込まなければならないドライバがずいぶん増加し、ドライバ間の依存関係もずいぶん複雑になってきた上、新しいドライバも日々増加し続けています。

その結果、make menuconfigでカーネル構築時の設定を決める際も、どのドライバを組み込み、どれをモジュールにすべきかにずいぶん悩むようになりました。それならいっそモジュールにできるドライバは全部モジュールにして、組み込むかどうかはカーネル自身に任せた方がいいんじゃないかと考えて、Plamo-7.1ではあらかじめ用意したinitrdイメージをカーネルパッケージに含めることにしたわけです。

次節以降で紹介するように、initrdは仕組み的にはそれほど難しいわけではないものの、どのコマンドやライブラリ、ドライバ・モジュールを含めるかを0から考えていくのはかなり面倒です。そこで今回も、BLFSプロジェクトで紹介されているスクリプトを元に、Plamo用に多少アレンジして採用することにしました。

Plamo-7.1のinitrd

initrdの実体は、ドライバ・モジュールとそれをカーネルに組み込むためのツール一式をcpio形式で固めて圧縮したファイルです。このファイルのことをinitrdイメージと呼ぶことにしましょう。

initrdイメージがどのような作りになっているのか、実際のファイルを展開しながら紹介します。なお、カーネルパッケージは随時バージョンアップするので、バージョン番号は適宜読み替えてください。

Plamo-7.1のカーネルパッケージはplamolinux.orgからダウンロードできます。このパッケージに含まれているboot/initrd.img-4.19.35_plamo64というファイルがinitrdイメージなので、まずパッケージからこのファイルを取り出します。Plamo Linuxのパッケージは単純なtar+xz形式なので、tarコマンドでファイルを取り出すことができます。

$ tar xvf kernel-4.19.35-x86_64-B1.txz boot/initrd.img-4.19.35_plamo64
boot/initrd.img-4.19.35_plamo64
$ ls -lh boot
合計 17M
-rwxr--r--+ 1 kojima users 17M  4月 25日  11:37 initrd.img-4.19.35_plamo64*

約17MBのinitrdイメージが取り出せました。このファイルは、cpio形式のアーカイブをgzipで圧縮した形になっているため、展開する際はzcatコマンドを通してからcpioを適用する必要があります。取り出したファイルはカレント・ディレクトリ以下にばらまかれるので、まずはInitrdという作業用ディレクトリを作り、そこでinitrdイメージを展開します。

$ mkdir Initrd ; cd Initrd
$ zcat ../boot/initrd.img-4.19.35_plamo64 | cpio -ivd
.
init
lib64
etc
etc/lvm
etc/lvm/cache
etc/lvm/lvm.conf
...
bin/cat
bin/sh
51847 ブロック

展開したinitrdイメージの中身を見ると、/devや/proc、/sysを含む、シンプルなルートファイルシステムになっています。

$ ls
bin/  dev/  etc/  init*  lib/  lib64@  proc/  run/  sbin/  sys/  usr/

Linuxのお手本であるUNIXでは、起動したカーネルは必要な初期化処理を終えれば、ルートファイルシステム上にある/sbin/initコマンドを起動することになっています。この流儀を崩さずに必要な処理を実行するため、initrdはいったん仮のルートファイルシステムを提供し、そこにあるinitコマンドを実行させてカーネルに必要なモジュールを組み込むわけです。

このinitはシェルスクリプトなので、つまみ読みしながら動作を紹介してみましょう。

まず、initrd内のコマンドのあるディレクトリへのパスを設定してから、

  1  #!/bin/sh
  2  
  3  PATH=/bin:/usr/bin:/sbin:/usr/sbin
  4  export PATH
  5  

本来のルートファイルシステムをマウントする際に用いるパラメータを準備して、

 57  init=/sbin/init
 58  root=
 59  rootdelay=
 60  rootfstype=auto
 61  ro="ro"
 62  rootflags=
 63  device=

devtmpfs等の仮想ファイルシステムをマウント。これでカーネルが認識している周辺機器の情報が/sys以下に見えるようになります。

 65  mount -n -t devtmpfs devtmpfs /dev
 66  mount -n -t proc     proc     /proc
 67  mount -n -t sysfs    sysfs    /sys
 68  mount -n -t tmpfs    tmpfs    /run

ブートローダから渡されたカーネルパラメータを/proc/cmdlineを使ってチェックし、先に準備したパラメータに値を保存。

 70  read -r cmdline < /proc/cmdline
 71  
 72  for param in $cmdline ; do
 73    case $param in
 74      init=*      ) init=${param#init=}             ;;
 75      root=*      ) root=${param#root=}             ;;
 76      rootdelay=* ) rootdelay=${param#rootdelay=}   ;;
 77      rootfstype=*) rootfstype=${param#rootfstype=} ;;
 78      rootflags=* ) rootflags=${param#rootflags=}   ;;
 79      ro          ) ro="ro"                         ;;
 80      rw          ) ro="rw"                         ;;
 81    esac
 82  done

デバイスファイルを動的に生成するudevdを起動。udevdは先にマウントした/sys以下の情報を用いて、カーネルが認識している周辺機器用のドライバ・モジュールを組み込むと共に、/dev以下にそれらデバイスを操作するためのデバイスファイルを作成します。

 96  ${UDEVD} --daemon --resolve-names=never
 97  udevadm trigger --action=add --type=subsystems
 98  udevadm trigger --action=add --type=devices
 99  udevadm trigger --action=change --type=devices
100  udevadm settle

組み込むべきドライバ・モジュールはinitrd内の/lib/modules/4.19.35-plamo64/以下に収められているものの、用意されているのはブロックデバイス用のモジュールとファイルシステム用のモジュール程度です。

$ ls lib/modules/4.19.35-plamo64/
kernel/        modules.alias.bin  modules.builtin.bin  modules.dep.bin  modules.order
modules.symbols  modules.alias  modules.builtin    modules.dep          modules.devname
modules.softdep  modules.symbols.bin

$ ls lib/modules/4.19.35-plamo64/kernel/drivers/
ata/  block/  firewire/  md/  message/  mmc/  mtd/  pcmcia/  scsi/  usb/  virtio/

$ ls lib/modules/4.19.35-plamo64/kernel/fs/
9p/     cachefiles/  dlm/       fat/      hfsplus/  minix/       nls/       overlayfs/  ubifs/
affs/   ceph/        ecryptfs/  fscache/  isofs/    nfs/         ntfs/      quota/      udf/
afs/    cifs/        exofs/     fuse/     jffs2/    nfs_common/  ocfs2/     reiserfs/   ufs/
befs/   coda/        ext2/      gfs2/     jfs/      nfsd/        omfs/      romfs/      xfs/
btrfs/  cramfs/      f2fs/      hfs/      lockd/    nilfs2/      orangefs/  squashfs/

udevdは、initrd上に用意されたkmodと協働して、上記ブロックデバイスやファイルシステム用のモジュールを組み込み、カーネルが本当のルートファイルシステムを読めるように準備を整えます。

準備が終われば、カーネルパラメータ経由で渡された設定を用いて、本当のルートパーティションを/.rootにマウントします。ルートパーティションの指定は、"/dev/sda1"といったデバイス名でも、パーティションのUUIDやPARTUUID、あるいはラベルを使うことも可能です。

 29  do_mount_root()
 30  {
 31     mkdir /.root
 32     [ -n "$rootflags" ] && rootflags="$rootflags,"
 33     rootflags="$rootflags$ro"
 34  
 35     case "$root" in
 36        /dev/* ) device=$root ;;
 37        UUID=* ) eval $root; device="/dev/disk/by-uuid/$UUID"  ;;
 38        PARTUUID=* ) eval $root; device=/dev/disk/by-partuuid/$PARTUUID ;;
 39        LABEL=* ) eval $root; device="/dev/disk/by-label/$LABEL" ;;
 40        ""     ) echo "No root device specified." ; problem    ;;
 41     esac

do_mount_rootの処理が正常に終了して、本当のルートパーティションがマウントできれば、仕事を終えたudevdを終了させて、本来のルートファイルシステムにswitch_rootします。

106  do_mount_root
107  
108  killall -w ${UDEVD##*/}
109  
110  exec switch_root /.root "$init" "$@"

以後の処理は、本当のルートファイルシステム上にある/sbin/initが引き継ぎ、/etc/inittabの設定に従って、必要な初期化処理を実行します。

その際にはudevdが改めて起動され、ネットワークやサウンド、グラフィック回りといったinitrdには含まれていなかったドライバ・モジュールも組み込まれ、フル機能を装備したカーネルが出来上がることになります。

このようにinitrdでは

  • /sysや/dev等の仮想ファイルシステムのマウント
  • udevdを起動して必要なモジュールの組み込み
  • 実際のルートパーティションをマウントしてswitch_root

といった程度の処理をするだけなので、見てきたinitのようにシェルスクリプトでも間に合います。

しかしながら、initrdは独立したLinux環境なので、使うコマンドは全てinitrd内に入れておく必要があります。すなわち、initスクリプトを動かすにはbashが、bashを動かすにはlibcが、mountコマンドを動かすにはlibblkid.so.1やlibuuid.so.1といったライブラリがそれぞれ必要となり、実際、先に展開したinitrdの/binや/libディレクトリには、必要なコマンドやライブラリ一式が収められています。

$ ls bin
basename*  cp*  insmod@   kmod*  ls*     mkdir*  mount*     rm*   sh*     umount*
cat*       dd*  killall*  ln*    lsmod@  mknod*  readlink*  sed*  sleep*  uname*

$ ls lib
firmware/              libc.so.6*                   libkmod.so.2*      libpthread.so.0*   libz.so.1*
ld-linux-x86-64.so.2*  libcap.so.2*                 liblzma.so.5*      libreadline.so.7*  modules/
libacl.so.1*           libdevmapper-event.so.1.02*  libm.so.6*         librt.so.1*        udev/
libattr.so.1           libdevmapper.so.1.02*        libmount.so.1*     libudev.so.1*
libblkid.so.1*         libdl.so.2*                  libncursesw.so.6*  libuuid.so.1*

これら必要なコマンドやライブラリをいかに集めてくるかが、initrdを採用する際の最大の問題なのですが、そのあたりの詳細は次回をお楽しみに。

おすすめ記事

記事・ニュース一覧