仙石浩明の日記

2007年9月4日

initramfs (initrd) の init を busybox だけで書いてみた

linux をブートさせる際、 さまざまな PC に対応させようとすると、 多くのデバイスドライバをカーネルに組み込んでおかねばならない。 つまりルートファイルシステム (root file system, 以下 rootfs と略記) をマウントするまでは、 その rootfs 上にインストールしたモジュール群 (/lib/modules/ の下に置いた *.ko ファイル群) を読めないからだ。 rootfs さえマウントできてしまえば、 あとはいくらでもモジュールを必要に応じて読み込むことができるようになる。 だから、さまざまなハードウェアへの対応といっても、 重要なのは rootfs をマウントするまで、である。

しかしながら、 rootfs をマウントするまでの辛抱といっても、 rootfs をマウントするにはハードディスクを認識しなければならないし、 それには ATA ドライバやら SCSI ドライバやら、 果ては AHCI ドライバなどが、 ハードウェアに応じて必要になる。

個人で管理する PC の全てのハードウェアに対応させるだけなら、 全てのドライバをあらかじめカーネルに読み込んでおくのもアリだろう。 しかし汎用的なディストリビューションなど、 (カーネル再構築を行なわずに) 多くのハードウェアに対応する必要がある場合は、 必要になるかも知れない全てのドライバを、 あらかじめカーネルに読み込んでおくなどということは非現実的である。

ちなみに私は、1993年頃から linux を使っているが、 いまだに当時インストールした slackware を使用し続けている。 もちろん kernel や libc をはじめとして、 ほぼ全てのソフトウェアをアップデートしてしまっているし、 しかも起動スクリプトをはじめとして、 あらゆる設定を好き勝手に書き換えてしまっているので、 インストールしてから 10年以上たった今となっては、 元の slackware の痕跡は全くといっていいほど残っていない。 もはや、私独自のディストリビューションと呼んでしまっても差し支えないだろう。 私が個人的に管理しているマシンには、 全てこの「my distribution」をインストールしている。 そんなわけで、私は「普通の」ディストリビューションを使ったことがない。 initrd がディストリビューションの「常識」となってからも、 私は initrd は使わずに、 自分が管理する PC のハードウェアに合わせてカーネルを再構築して使ってきた。

そこで linux では、 initrd (Initial RAM Disk) という仕掛けが使われてきた。 すなわちハードディスクを rootfs としてマウントする前に、 一時的にマウントする「ミニ」ルート (mini root) である。 このミニルートには、 ハードディスク (あるいは 1CD Linux の場合であれば CD だし、 ネットワークブートする場合であれば NFS サーバ) を rootfs としてマウントするのに必要となる可能性がある モジュール群一式を置いておき、 ハードウェアに応じて必要なモジュールをミニルートから読む。 そして、ハードディスクをマウントして、 / (ルート) をミニルートからハードディスクへ切り替える。

ただ、この initrd は少々扱いが面倒くさい。 initrd は RAM ディスクという「本物の」ブロックデバイスなので、 「本物の」ファイルシステム (例えば ext2) で mkfs しなければならない。 initrd にモジュールを追加しようとすれば、 initrd イメージを (losetup コマンドを使って) ループバックデバイス経由でマウントして内容を書き換えなければならないし、 たくさんのモジュールを追加した結果、 もしファイルシステムが一杯にでもなったりしたら、 initrd イメージのサイズを大きくして mkfs からやり直しである。

メンドクサイだけでなく、 RAM ディスク自体が非効率なものであるようで、 ファイルからブロックデバイスを作る方法としては、 すでに「semi-obsolete」とまで言われてしまっているようである:

Another reason ramdisks are semi-obsolete is that the introduction of loopback devices offered a more flexible and convenient way to create synthetic block devices, now from files instead of from chunks of memory. See losetup (8) for details.
linux/Documentation/filesystems/ramfs-rootfs-initramfs.txt から引用

というわけで、initrd に代わる仕掛けとして、 linux kernel 2.6 からは initramfs と呼ばれる仕掛けが導入された。 すなわち RAM ディスクというブロックデバイスを用いるのではなく、 RAM 上に直接ファイルシステムを作る ramfs を用いた「ミニルート」である。 私自身は今まで initrd を使っていなかったのであるが、 cpio アーカイブを作るだけでいいというのは、 とても手軽であるように思えたし、 カーネルにどんどんドライバを組み込んで肥大化させるよりは、 initramfs を使う方がヨサゲである (もちろん、どんどんドライバを組み込めば、 initramfs が肥大化するのだが、 カーネルが肥大化するデメリットとは比較にならない) ように感じてきたので、 宗旨替えすることにした。

initrd initramfs
イメージ ファイルシステム (ext2など + gzip) アーカイブ (cpio + gzip)
実装 ブロックデバイス (RAM ディスク) ファイルシステム
実行 /linuxrc /init
rootfs
マウント
適当なディレクトリへマウントして
pivot_root
/ へマウント (switch_root)
init 起動 /linuxrc 終了後、カーネルが起動 /init が exec /sbin/init する

ブートパラメータとして「initrd=」を与えると、 ブートローダがイメージをメモリ上に読み込んでカーネルに渡す。 するとカーネルはそのイメージがファイルシステムなのか、 cpio アーカイブなのか調べる。 もしファイルの magic number が cpio であれば、 ramfs としてマウントする。 そして /init が実行可能ならば、 initramfs として扱い、 /init を起動する。

以上の条件が一つでも成立しない場合、 すなわち cpio アーカイブでない場合や、 /init が実行できない (/init が存在しない) 場合は、 initrd 扱いになるので注意が必要である。 すなわち RAM ディスクとしてマウントしようとするので、 カーネルに RAM ディスクドライバが組み込まれていなかったり、
「root=/dev/ram0」カーネルパラメータを指定していなかったりすると、 kernel panic を起こす。

実は、initramfs として起動できるようになるまで、 かなりハマってしまった。 まず、cpio アーカイブを作るところで、いきなりハマった。

(cd /usr/src/initramfs/; find . | cpio -o -H newc ) | gzip > initrd.gz

などとしてアーカイブを作ればいいだけの話なのであるが、 このコマンドラインを /bin/csh 上で実行したために、 アーカイブの先頭にゴミが入ってしまった。 つまり、

senri:/ % (cd /usr/src/initramfs/; find . | cpio -o -H newc ) | cpio -tv | head
cpio: Malformed number 0000000.
cpio: Malformed number 000000.
cpio: Malformed number 00000.
cpio: Malformed number 0000.
cpio: Malformed number 000.
cpio: Malformed number 00.
cpio: Malformed number 0.
cpio: Malformed number .
cpio: warning: skipped 56 bytes of junk
drwxr-xr-x  13 root     root            0 Aug 27 17:56 .
drwxr-sr-x   2 root     root            0 Aug 25 10:00 bin
-rwxr-xr-x   1 root     root      1392832 Aug 25 18:36 bin/busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/addgroup -> busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/adduser -> busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/ash -> busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/cat -> busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/catv -> busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/chattr -> busybox
lrwxrwxrwx   1 root     root            7 Aug 25 10:09 bin/chgrp -> busybox

何が起きているかお分かりだろうか? お恥ずかしながら、アーカイブにゴミが混入していると気づくまで、 何度も kernel を panic させてしまった。 シェルのカスタマイズをやりすぎるとロクなことにならない、 という典型例なのかも (^^;)。

senri:/ % (cd /usr/src/initramfs/; echo test) | od -t a
0000000 esc   E   m   A   c   S   c   d  sp   /   u   s   r   /   s   r
0000020   c   /   i   n   i   t   r   a   m   f   s  nl esc   E   m   A
0000040   c   S   c   d  sp   /   u   s   r   /   s   r   c   /   i   n
0000060   i   t   r   a   m   f   s  nl   t   e   s   t  nl
0000075
senri:/ % alias cd
set back="$cwd";chdir !*;if(!* =~ "..")set cwd="$back:h";chdir "$cwd";setProm
senri:/ % alias setProm
set prompt="${HOST}:${cwd} $prompt_tail_char "

この alias 設定は、 もうかれこれ 10年以上使い続けてきた設定。 こんな形で悪さをするとは... orz

ようやくマトモなアーカイブを作れたと思ったら、 今度は以下のような Kernel panic が起きた:

Unpacking initramfs... done
Freeing initrd memory: 1412k freed
...(中略)...
No filesystem could mount root, tried:
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(8,1)

「Unpacking initramfs... done」と出ているのだから、 cpio アーカイブはちゃんと展開できて ramfs としてマウントできているはず。 なぜに「Unable to mount root fs」なのか、 と思っていたのだが、 これは initramfs に「/init」が無いためだった (エラーメッセージが不親切杉!)。 initrd みたいなものだろうと思って、 起動スクリプトを「/linuxrc」というファイル名で作っていたのが敗因。

「/init」がないと initrd 扱いになってしまい、 RAM ディスクを mount しようとしていたが、 RAM ディスクドライバが組み込まれていなかったのでマウントできない、 というのが、このエラーメッセージの主旨だったようだ (素直に /init が見つからない、って言ってくれればいいのに...)。 「/linuxrc」を「/init」へファイル名変更してみると、 あっさり initramfs 上での起動に成功した。

% ls -lt /boot/*-2.6.20.*
-rw-r--r-- 1 root root 1643881 Sep  3 07:55 /boot/initz-2.6.20.18
-rw-r--r-- 1 root root 1193392 Sep  1 23:10 /boot/linuz-2.6.20.18
-rw-r--r-- 1 root root 2017936 May 12 19:31 /boot/linuz-2.6.20.11

2.6.20.11 を make したときは、 rootfs のマウントに必要なドライバを全てカーネルに詰め込んでいたのに対し、 2.6.20.18 では、 マウントに必要なドライバは極力 initramfs に入れた。 これにより linuz 単体のサイズは半分近くに減っている。 linuz (カーネル) + initz (initramfs) の合計サイズは 2.6.20.11 に比べ増えてしまっているが、 これは busybox だけで 1.3MB ほどあるため。 とはいえ、 initramfs 内では busybox よりも lib/modules 以下のサイズの方が倍ほど大きいので、 非効率というほどでもない。

senri:/usr/src/initramfs % ls -l bin/busybox
-rwxr-xr-x 1 root root 1392832 Sep  1 11:24 bin/busybox
senri:/usr/src/initramfs % du --max-depth=2 --byte
1397502	./bin
4740	./sbin
6204	./usr/bin
4334	./usr/sbin
8226	./usr/share
22860	./usr
4096	./dev/pts
4096	./dev/shm
12338	./dev
4108	./etc
4096	./mnt
2422679	./lib/modules
2426775	./lib
4096	./proc
4096	./tmp
4096	./var
4096	./sys
3895272	.

じゃ、いよいよハードウェアの自動認識をして、 適切なモジュールのみを組み込むようにしてみようかと思って、 いろいろ探してみたのだが、 どうしたことか適当なスクリプトが見当たらない。 1CD Linux の /linuxrc をいろいろ読んでみたのだが、 いまいちパッとするものがない。 デバイスの ID などがゴリゴリ書いてあるものが大半で、 どれもアドホックすぎるように思えたのである。

かといって、ハードウェアの認識を udev などに行なわせる、 というのは牛刀過ぎるように思えた。 なんたって init を起動する前のブートストラップなのである。 目的は rootfs をマウントするだけなのであるから、 あまりに汎用的な仕掛けは、いかがなものかと思うのである。

というわけで、 busybox だけでハードウェアの自動認識 & モジュール読み込みを実現することを 目標にしてみた。 前フリが長くなった (長すぎ!) が、ようやくここからが本題である。

ハードウェアの自動認識を、 busybox 1.7.0 に標準で含まれるコマンドだけを使って実現する、 というととても大変そうに聞こえるかも知れないが、 自動認識の要は Linux が sysfs と言う形で提供してくれるので、 以下のようなとても短い sh スクリプト (わずか 15行!) で実現できてしまった。 /bin/ash があれば実行可能なので、 klibc あるいは uClibc な環境でも、 そのまま利用可能だろう。

pcimap="/lib/modules/`uname -r`/modules.pcimap"
mount -t sysfs none /sys
for d in /sys/bus/pci/devices/*; do
    if [ -r $d/vendor -a -r $d/device ]; then
	read VENDOR < $d/vendor
	read DEVICE < $d/device
	pat=`echo "^\([^ ][^ ]*\) *$VENDOR  *$DEVICE .*" | sed 's/0x/0x0*/g'`
	mod=`sed -n "s/$pat/\1/p" $pcimap`
	if [ -n "$mod" ]; then
	    for m in $mod; do
		modprobe $m
	    done
	fi
    fi
done

sysfs は、システムに接続されたハードウェアを、 ファイルシステムの形で見せてくれる仕掛けである。 「mount -t sysfs none /sys」などと sysfs をマウントしておく。 例えば「/sys/bus/pci/devices」ディレクトリを見ると、

senri:/ % ls -F /sys/bus/pci/devices/
0000:00:00.0@  0000:00:1d.2@  0000:00:1f.0@  0000:00:1f.5@  0000:01:08.0@
0000:00:02.0@  0000:00:1d.3@  0000:00:1f.1@  0000:01:00.0@  0000:02:08.0@
0000:00:1d.0@  0000:00:1d.7@  0000:00:1f.2@  0000:01:01.0@  0000:02:09.0@
0000:00:1d.1@  0000:00:1e.0@  0000:00:1f.3@  0000:01:02.0@

以上のように、PCI バスにつながっているデバイスの一覧が得られる。 このディレクトリにはシンボリックリンクがならんでいるが、 シンボリックリンク一つが、 PCI バスに接続されたデバイス一つに対応している。 試しにシンボリックリンクの一つ、 「0000:00:1f.1」を見てみると、

senri:/ % cd /sys/bus/pci/devices/0000:00:1f.1
senri:/sys/bus/pci/devices/0000:00:1f.1 % ls -F
bus@	device	 irq	     power/	resource5	  uevent
class	driver@  local_cpus  resource	subsystem_device  vendor
config	ide1/	 modalias    resource4	subsystem_vendor
senri:/sys/bus/pci/devices/0000:00:1f.1 % head vendor device
==> vendor <==
0x8086

==> device <==
0x24db

デバイスに関する情報が、ファイルの形で得られる。 この中の「vendor」「device」というファイルを読むと、 ベンダ番号とデバイス番号が得られる。 この番号を元に、 読み込むべきモジュール名を検索すればよい。

各モジュールが、 どんなデバイスをサポートしているかは、 モジュールの中に格納されている。 その格納されているデータを取り出して、 モジュール名とデバイスの対応関係を表の形で生成してくれるコマンドが depmod である。

例えば、 「/usr/src/initramfs/lib/modules/2.6.20.18」以下に、 initramfs で必要になりそうなモジュールをコピーしてある (Linux kernel 2.6.20.18 の場合) のであれば、

depmod -b /usr/src/initramfs 2.6.20.18

などと depmod コマンドを実行すればよい。 すると、 PCI デバイスとモジュール名の対応表「modules.pcimap」を、 「/usr/src/initramfs/lib/modules/2.6.20.18」ディレクトリ下に生成する。 このファイルの中身を見てみると、

# pci module   vendor     device     subvendor  subdevice  class      class_mask driver_data
r8169          0x000010ec 0x00008129 0xffffffff 0xffffffff 0x00000000 0x00000000 0x0
r8169          0x000010ec 0x00008136 0xffffffff 0xffffffff 0x00000000 0x00000000 0x0
	...(中略)...
piix           0x00008086 0x000024db 0xffffffff 0xffffffff 0x00000000 0x00000000 0x0
piix           0x00008086 0x0000245b 0xffffffff 0xffffffff 0x00000000 0x00000000 0x0
piix           0x00008086 0x000024ca 0xffffffff 0xffffffff 0x00000000 0x00000000 0x0
	...(後略)...

このように第一カラムにモジュール名が、 第二/第三カラムにそれぞれベンダ/デバイス番号が、 記録されている。 ベンダ番号が「0x8086」で、 デバイス番号が「0x24db」であるような行を探してみると、 モジュール名「piix」が得られる。 つまり、 前述したディレクトリ「0000:00:1f.1」が対応しているデバイスを認識するには、 「piix」モジュールを読み込めばよい、ということが分かる。 あとは「modprobe piix」を実行するだけ。

initramfs の /init スクリプトは、 以上のような方法で必要なモジュールを読み込んだ後、 起動に必要な rootfs を / (root) にマウントして rootfs 上の init を起動することになる。 より具体的いえば、次のようなステップを実行する。

  1. ミニルート (initramfs) 上の全てのファイルを消す
  2. 適当なディレクトリ (例えば /mnt) を作成して、そこに rootfs をマウント
  3. rootfs をマウントしたディレクトリ (/mnt) を root にする
    cd /mnt; mount --move . /; chroot .
  4. / にマウントした rootfs 上の init を exec

しかしながら、 各ステップを sh スクリプトで記述すると、 1. で全てのファイルを消してしまうので、 2. 以降を実行できない (^^;)。 だから 1. ~ 4. をまとめて実行するコマンド switch_root (busybox に含まれている) を使う。

ちなみに、なぜ直接 / へマウントせず、 いったん /mnt へマウントしてから --move /mnt / する (つまりマウントポイントを / へ移動する) かというと、 前者だとマウントしたディレクトリへ chdir できないから。 全てのプロセスは、 カレントディレクトリとして / 以下のディレクトリをオープン済みだから、 後から別のファイルシステムを / へマウントしても、 マウントしたファイルシステムを見ることはできない。 そこで、まず /mnt などへマウントしてそこへ chdir することにより、 新しいファイルシステムのルートディレクトリを、 カレントディレクトリとしてオープンすることができる。

以上まとめて、 次のような /init スクリプトを書いた。

#!/bin/ash
export PATH="/bin:/sbin:/usr/bin"
pcimap="/lib/modules/`uname -r`/modules.pcimap"
mount -t proc  none /proc
mount -t sysfs none /sys
for p in `cat /proc/cmdline`; do
    case $p in
	root=*)
	ROOT=`echo $p | sed 's/.*=//'`
	;;
    esac
done
for d in /sys/bus/pci/devices/*; do
    if [ -r $d/vendor -a -r $d/device ]; then
	read VENDOR < $d/vendor
	read DEVICE < $d/device
	pat=`echo "^\([^ ][^ ]*\) *$VENDOR  *$DEVICE .*" | sed 's/0x/0x0*/g'`
	mod=`sed -n "s/$pat/\1/p" $pcimap`
	if [ -n "$mod" ]; then
	    for m in $mod; do
		modprobe $m
	    done
	fi
    fi
done
modprobe af_packet
modprobe unix
modprobe ext3
modprobe xfs
if [ -n "$ROOT" ]; then
    mount -o ro $ROOT /mnt
    umount /proc
    umount /sys
    exec /sbin/switch_root /mnt /sbin/init
fi
exec /bin/ash

「root=」カーネルパラメータを指定している場合は、 その値が ROOT 変数に代入されるので、 次の部分が実行される。

    mount -o ro $ROOT /mnt
    umount /proc
    umount /sys
    exec /sbin/switch_root /mnt /sbin/init

まず rootfs を /mnt へマウントし、 不要になった /proc と /sys をアンマウントした後、 switch_root を exec している。 /init は PID=1 (プロセスID) で実行されるので、 その ID を switch_root が引き継ぎ、 さらに rootfs 上の init に引き継がれる。 switch_root は自分の PID が 1 でないと、 「switch_root: not rootfs」という 謎のエラーメッセージを表示して終了してしまうので注意。

/init スクリプト冒頭の「export PATH=...」がなにげに重要である。 最初、「export」をつけていなかったのであるが、 modprobe コマンドが内部で insmod を呼び出しているために、 もし「export」をつけないと insmod が実行できず、 モジュール読み込みに失敗する。

実は、modprobe がモジュール読み込みに失敗するなら、 自分で modprobe 相当のスクリプトを書いちゃえ、てことで 書いてしまった後で、 PATH を export すれば解決することに気づいた。 (ボ) にしたコードをせっかくだから晒しておく:

mod_dep="/lib/modules/`uname -r`/modules.dep"
load() {
    mod=$1
    echo "loading $mod ..."
    m=`sed -n "s/\([^:]*\/$mod.ko\): *\(.*\)/\1 \2/p" $mod_dep`
    if [ -n "$m" ]; then
	post=" "`echo "$m" | sed 's/ .*/ /'`;
	rest=`echo "$m" | sed 's/[^ ]*  *//'`;
	while [ -n "$rest" ]; do
	    m=`echo "$rest" | sed 's/ .*//'`;
	    rest=`echo "$rest" | sed 's/^ *[^ ]* *//'`
	    case $post in *" $m "*);; *)post=" $m$post"; esac
	    m=`fgrep "$m:" $mod_dep | sed "s/[^:]*: *//"`
	    if [ -n "$m" ]; then
		rest="$m $rest"
	    fi
	done
    fi
    for m in $post; do
	if [ -r $m ]; then
	    insmod $m
	fi
    done
}

この他にもいろいろハマった。 きちんと動くようになるまで Kernel panic の山を築き (まあほとんどは QEMU 上の実行なので実害はないのだが)、 挙げ句の果てに XFS なパーティションを大破させてしまい、 さっそく起動ディスク (アドエス)のお世話になってしまった。

上記 /init スクリプトは、 「root=」カーネルパラメータを指定しないと、 exec /bin/ash して入力待ちになる。 必要なモジュールが正しく読み込まれているか確認したり、 あるいは手動で rootfs をマウントしてみたりすることができるので便利。

4件のコメント »

  1. initramfs (initrd) ? init ? busybox ????????

    コメント by universityupdate.com — 2007年9月4日 @ 08:45

  2. initramfs – VIVERの技術

    今その価値が見直されようとしているかもしれないinitramfs。initramfs内ではほとんど何もできないかと思いきや、試してみると実は「何でもできる」ことが分かります。 そうなると、いろいろやりたくなるわけですが、initramfs内でやることと言えば大体決まっていて(ブート

    コメント by 古橋貞之の日記 — 2007年9月5日 @ 02:33

  3. [gentoo]Gentooなんていかがでしょうか

    initramfs (initrd) の init を busybox だけで書いてみた – 仙石浩明の日記 嗚呼、仙石さんにオススメのディストリビューションがありますよ。その名もGentoo Linux。 ちなみに私は、1993年頃から linux を使っているが、いまだに当時インストールした slackware を使用し

    コメント by もしもし、matsuuですが... — 2007年9月6日 @ 23:39

  4. 10行でできる高精度ハードウェア自動認識 (initramfs の init を busybox だけで書く)

    これまでLinuxのハードウェア自動認識と言えば、
    /sys/bus/pci/devices 以下と、
    /lib/modules/`uname -r`/modules.pcimap を照らし合わせて
    解析していくのが定石でした。
    USBにも対応しようとすると、もう一つ大変です。
    しかしこれからの常識は、
    /sys/bus/*/devi…

    コメント by 仙石浩明の日記 — 2007年9月29日 @ 22:47

この投稿へのコメントの RSS フィード。

コメントする