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

第52回Plamo Linuxの遊び方(その3)

Plamo Linuxの特徴的な部分を紹介しているこのシリーズ、今回はカーネルと共に読み込まれ、カーネルの起動を補助するinitramfsを取り上げます。initramfsは、かつてはinitrdと呼ばれ、Linuxの初期のころから利用されてきた機能です。このinitrd/initramfsはLinuxが独自に生み出した機能で、伝統的なUnixの枠組みを越え、Linuxが独自の発展をしていく最初の一歩となりました。

initramfsの歴史

initrd/initramfsが生まれた(というか必要となった)のは「PC互換機用に開発された」というLinuxの出自が大きく関わっています。

Linuxが参考にしたUnixワークステーションは、SunやDEC、HPといったコンピュータメーカがハードウェアからOSまで一貫して作っていたので、対応すべき周辺機器も限られ、必要となる全てのドライバをあらかじめカーネルに組み込んでおくことが可能でした。

一方PC互換機では、技術的な仕様が公開、共有されたため、多数の周辺機器メーカが参入し、膨大な種類の周辺機器が開発されました。それら周辺機器メーカはDOSやWindows用のドライバは提供するものの、Linuxなど眼中にありませんでしたので、最初期のカーネルでは、ドライバを開発し対応する周辺機器を増やすことが最重要課題でした。

その後、Linuxの知名度があがるにつれ対応する周辺機器は増えてきたものの、今度はそれら周辺機器用のドライバが増えすぎて、全てをカーネルに組み込むことが困難になってきました。そこで考案されたのが、ドライバ類をモジュールとして分離し、必要に応じて組み込むモジュール化カーネルというアイデアです。

全ての機能やドライバを1つにまとめる伝統的な設計を「モノリシック(一枚岩的)カーネル⁠⁠、カーネル本体には通信制御等ごく限られた機能のみを残し、その他の機能は独立したサーバに分散する設計を「マイクロカーネル」と呼びます。Linuxの「モジュール化カーネル」は両者を折衷したような設計と言えます。

モジュール化カーネル以前では「A、B、C社のSCSI HDDを使う場合はカーネル1、それ以外のHDDの場合はカーネル2」のように複数のカーネルイメージを使い分ける必要があったのに対し、⁠1つのカーネル本体とモジュールドライバ群」で周辺機器に柔軟に対応できるようになったLinuxはますます利用範囲を拡大していきました。

もっとも、これらモジュール化されたドライバはHDD上に置くため、HDD用のドライバをモジュール化すると「缶切りは缶詰の中」な状態になってしまいます。そのような問題を回避するために考案されたのがinitrdで、⁠モジュールを読みこむために必要なモジュールは別途用意する」ための仕組みです。initrdは⁠INITialize(初期化)用 RamDisk⁠いいで、HDD上にある本来のルートパーティションをマウントする前に、仮想的なルートパーティションを使って必要なモジュールを組み込むようになっています。

initrdの元々の実装ではramdiskを仮想的なルートパーティションにしていたため、ファイルをファイルシステムとしてマウントするループバック・マウントを利用して必要なモジュールを組み込んだファイルシステム・ファイルを用意する必要がありました。

その後Linuxの進化と共に、メモリ上に仮想的なファイルシステムを作るramfsが開発され、サイズが固定されるramdiskよりも柔軟な使い方が可能となり、それに合わせてinitrdもinitramfsに進化し、ファイルシステム・ファイルを使わずに作成できるようになりました。

現在ではこのinitramfsが標準になっているものの、機能の名称としては"initrd"も使われており、grubのメニュー等では"initrd=.."で指定するままだし、Plamo Linuxでもファイル名は"initrd.img-.."となっているのでご注意ください。

initramfsの展開

さて、それではinitramfsの中身を見てみましょう。Plamo Linuxでは/bootディレクトリにカーネルイメージとそれに対応したinitramfsを収めています。

$ ls /boot
System.map@                 initrd.img-6.1.42-plamo64*
System.map-6.1.42-plamo64*  initrd.img-6.1.44-plamo64*
System.map-6.1.44-plamo64*  initrd.img-6.1.47-plamo64*
System.map-6.1.47-plamo64*  vmlinuz@
config-6.1.42-plamo64*      vmlinuz-6.1.42-plamo64*
config-6.1.44-plamo64*      vmlinuz-6.1.44-plamo64*
config-6.1.47-plamo64*      vmlinuz-6.1.47-plamo64*
efi/

initramfsに含まれるモジュールはカーネルのバージョンに依存するため、カーネルをバージョンアップすればinitramfsもそれに合わせて更新する必要があります。

さて、上記initrd.img-6.1.47-plamo64は複数のファイルをcpio形式にまとめたアーカイブファイルです。

$ file /boot/initrd.img-6.1.47-plamo64 
/boot/initrd.img-6.1.47-plamo64: ASCII cpio archive (SVR4 with no CRC)

cpio形式のアーカイブファイルの中身を調べるには"cpio -t"コマンドを用います。cpioは入力元に標準入力を想定するので、ファイルを読ませる場合は"<"(リダイレクト)を使います。

$ cpio -t < /boot/initrd.img-6.1.47-plamo64 
.
kernel
kernel/x86
kernel/x86/microcode
kernel/x86/microcode/GenuineIntel.bin
kernel/x86/microcode/AuthenticAMD.bin
25663 ブロック

おや、コマンドやモジュール類は見当りません。というのもPlamo LinuxのinitramfsにはCPU用のmicrocodeを先頭に付加しているため、そのままでは先頭のmicrocode分しか表示されないのです。

CPU用のmicrocodeはCPUのハードウェアレベルのバグ(errata)を修正するためのプログラムで、CPUに読み込まれて問題となる処理を変更します。microcodeはIntelやAMDから提供され、本来はマザーボードのBIOS経由で読み込むべきものの、BIOSの更新を怠っていたり、更新が終了した古いマザーボードでは新しいmicrocodeが利用できないことがあります。そのためLinuxではカーネルからCPUにmicrocodeを送りこむ機能(microcode loading)が用意されており、その機能を使うためにinitramfsに両社のmicrocodeを含めています。microcodeを更新するとCPUの動作が変わる可能性があるため、起動の可能な限り早い時期、すなわちカーネルが動作し始めた直後に読み込めるようinitramfsの先頭部分に配置することが推奨されています(early loading⁠⁠。

そのためinitramfsの実際の中身を見るには、先頭からmicrocode分の25663ブロックを飛ばして読む必要があります。一定数のブロックを読み飛ばすにはddコマンドの"skip"オプションが使えます。また、このアーカイブファイルはgzipで圧縮しているので、読み出すまえには展開する必要があり、アーカイブからファイルを取り出すにはcpioの"-i"オプションを指定します。

$ dd if=/boot/initrd.img-6.1.47-plamo64 skip=25663 | gunzip | cpio -iv
.
init
lib64
etc
etc/lvm
etc/lvm/lvmlocal.conf
etc/lvm/lvm.conf
etc/lvm/profile
...
lib
lib/modules
lib/modules/6.1.47-plamo64
lib/modules/6.1.47-plamo64/modules.devname
lib/modules/6.1.47-plamo64/modules.builtin.alias.bin
lib/modules/6.1.47-plamo64/modules.builtin.bin
...
28745294 bytes (29 MB, 27 MiB) copied, 0.69324 s, 41.5 MB/s
94062 ブロック
$ ls -F
bin/  dev/  etc/  init*  lib/  lib64@  proc/  run/  sbin/  sys/  usr/

ここに展開されたディレクトリやファイルがinitramfsの本体です。カーネルは、ブートローダによって共に読み込まれたinitramfsのアーカイブファイル(initrd.img-6.1.47-plamo64)からこれらを取り出し、ramfsの機能を使って仮想的なファイルシステムに展開し、そこを一時的なルートパーティションと見なします

initramfsの構造

initramfsの役割はカーネルが本来のルートファイルシステムを読み込むために必要なモジュールドライバを提供することで、そのためのコマンドやライブラリ、必要なモジュール等を用意しています。

最近のLinuxには膨大な数のモジュールドライバが提供されているものの、initramfsに取り込んでいるのはHDDやメモリカード、USBメモリといったルートパーティションになりうる各種ブロックデバイス用のドライバと各種ファイルシステム、ファイルシステムが暗号化されていた場合に備えてのcrypto回りのモジュールのみです。

$ ls -F lib/modules/6.1.47-plamo64/kernel/
crypto/  drivers/  fs/  lib/

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

$ ls -F lib/modules/6.1.47-plamo64/kernel/drivers/scsi/
3w-9xxx.ko.zst   fdomain_pci.ko.zst       pmcraid.ko.zst
3w-sas.ko.zst    fnic/                    qedf/
3w-xxxx.ko.zst   hpsa.ko.zst              qedi/
BusLogic.ko.zst  hptiop.ko.zst            qla1280.ko.zst
...
$ ls -F lib/modules/6.1.47-plamo64/kernel/fs/
btrfs/       erofs/    fuse/     lockd/          nls/        reiserfs/  unicode/
cachefiles/  exfat/    gfs2/     mbcache.ko.zst  ntfs3/      romfs/     vboxsf/
ceph/        ext2/     hfsplus/  netfs/          ocfs2/      smb/       xfs/
...

bin/以下にはモジュールの組み込みとファイルシステムをマウントするために最低限必要なツールを用意しています。

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

sbin/以下は周辺機器を認識するためのudevdと、現状のインストーラでは対応していないものの、ルートパーティションを論理ボリュームやRAID上に置いた際に必要となるコマンド類です。

$ ls sbin/
blkid*     lvdisplay@  lvscan@    pvchange@   pvscan@       vgchange@  vgscan@
dmsetup*   lvextend@   mdadm*     pvck@       switch_root*  vgck@
lvchange@  lvm*        mdmon*     pvcreate@   udevadm*      vgcreate@
lvcreate@  lvrename@   modprobe*  pvdisplay@  udevd*        vgrename@

lib/以下には上記バイナリファイルを動かすのに必要な共有ライブラリのみを置いています。

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

これらを利用して必要なモジュールを組み込むのがinitの仕事です。initramfs用のinitは100行ほどのシェルスクリプトで、udevdを起動してカーネルに周辺機器を認識させ、あらかじめ用意しているモジュールを組み込ませることが主な仕事です。

処理を簡単に紹介すると、まずカーネルの動作に必要な/sysや/procといった仮想ファイルシステムをマウントし、

 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

grubから渡された起動用パラメータを/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を起動し、"udevadm trigger"で接続されている周辺機器をカーネルに認識させ、initramfs上に用意しているブロックデバイスやファイルシステムのモジュールを読み込ませます。

 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

正しく処理が進めばこの段階からHDD等が利用できるので、起動用パラメータを元に"do_mount_root"で本来のルートパーティションを"/.root"ディレクトリにマウントし、不要になったudevdを終了させます。

106  do_mount_root
107  
108  killall -w ${UDEVD##*/}

最後に"switch_root"コマンドで新たにマウントした本来のルートパーティションに切り替えてinitramfsは終了、となります。

110  exec switch_root /.root "$init" "$@"

以後の作業は本来のルートパーティション上にある/sbin/initに引き継がれ、前回紹介したsysvinitのinitスクリプトが順次起動されることになります。

/sbin/mkinitramfs

initramfsを作成するには/sbin/mkinitramfsコマンドを用います。mkinitramfsはカーネルバージョン(たとえば"6.1.47-plamo64")を引数に取り、指定したバージョンのモジュールドライバを/lib/modules/以下からコピーしてアーカイブファイルを作ります。モジュール類は/lib/modules/以下からコピーするので、新しくビルドしたカーネル用にinitramfsを作る場合、一度"make modules_install"して、そのカーネル用のモジュールドライバを/lib/modules/以下にインストールする必要があることにご注意ください。また、mkinitramfsの実行にはroot権限が必要になります。

$ sudo /sbin/mkinitramfs 6.1.47-plamo64
[sudo] kojima のパスワード:
Creating initrd.img-6.1.47-plamo64... depmod: WARNING: could not open ..
.
./kernel
./kernel/x86
./kernel/x86/microcode
./kernel/x86/microcode/GenuineIntel.bin
./kernel/x86/microcode/AuthenticAMD.bin
25663 ブロック
done.
$ ls -lh
合計 53M
-rw-r--r-- 1 root root 41M 10月 16日  09:03 initrd.img-6.1.47-plamo64
-rw-r--r-- 1 root root 13M 10月 16日  09:03 ucode.cpio

mkinitramfsの実行時にdepmodのWARNINGが表示されているのは気にしないでください(苦笑⁠⁠。また、ucode.cpioは/lib/firmware/{intel,amd}-ucode/から取り出したmicrocodeを集めたcpioアーカイブで、作成した"initrd.img-6.1.47-plamo64"の先頭部分に追加済みです。

こうして作成した"initrd.img-6.1.47-plamo64"とカーネル本体"vmlinuz-6.1.47-plamo64"を/boot/に配置して"grub-mkconfig -o grub.cfg"すれば新しいカーネルとinitrdを読み込むためのgrub.cfgが作成されます。作成したgrub.cfgを適切な場所(EFIブートならば/boot/efi/grub/)に移せば新たなカーネルで起動する準備完了となります。

# grub-mkconfig -o grub.cfg.new
Generating grub configuration file ...
Found background: /usr/share/grub/plamo_bg01.png
Linux イメージを見つけました: /boot/vmlinuz-6.1.47-plamo64
Found initrd image: /boot/initrd.img-6.1.47-plamo64
fgrep: warning: fgrep is obsolescent; using grep -F
Linux イメージを見つけました: /boot/vmlinuz-6.1.44-plamo64
...
Check GRUB_DISABLE_OS_PROBER documentation entry.
Adding boot menu entry for UEFI Firmware Settings ...
完了

initramfをどのような作りにするかはそれぞれのディストリビューションに委ねられています。そのためPlamo Linuxでは、⁠例のごとく)LFSが公開しているスクリプト等をカスタマイズして使っているため、現状ではインストーラが対応していないルートファイルシステムの暗号化やRAID、論理ボリューム用のモジュール類も含まれているのは将来の拡張予定ということにしてください(苦笑⁠⁠。

今回紹介した起動用のinitramfsは使い捨ての簡単な作りでしたが、必要なコマンドやライブラリ類を用意すればinitramfs上で動き、HDD等を必要としないLinux環境を作ることもできます。次回はそのような使い方の例としてPlamo Linuxのインストーラを取りあげる予定です。

おすすめ記事

記事・ニュース一覧