initramfs - VIVERの技術
今その価値が見直されようとしているかもしれないinitramfs。initramfs内ではほとんど何もできないかと思いきや、試してみると実は「何でもできる」ことが分かります。
そうなると、いろいろやりたくなるわけですが、initramfs内でやることと言えば大体決まっていて(ブートパラメータの解釈、ハードウェア認識、カーネルモジュールのロード、デバイスノードの作成、ネットワークの起動、HDDのマウント、tmpfsのマウントと引き継ぎファイルの避難、switch_rootなどなど)、そのあたりをVIVERの中からinitramfs内のコードを取り出してライブラリ風に使えるようにしようというアイディア(VIVER CORE)があって、ちまちまと作っています。が、WikiFormeなどなどに手を出しているうちに、素晴らしい記事が。
initramfs (initrd) の init を busybox だけで書いてみた - 仙石浩明の日記
じゃ、いよいよハードウェアの自動認識をして、 適切なモジュールのみを組み込むようにしてみようかと思って、 いろいろ探してみたのだが、 どうしたことか適当なスクリプトが見当たらない。 1CD Linux の /linuxrc をいろいろ読んでみたのだが、 いまいちパッとするものがない。 デバイスの ID などがゴリゴリ書いてあるものが大半で、 どれもアドホックすぎるように思えたのである。
かといって、ハードウェアの認識を udev などに行なわせる、 というのは牛刀過ぎるように思えた。 なんたって init を起動する前のブートストラップなのである。 目的は rootfs をマウントするだけなのであるから、 あまりに汎用的な仕掛けは、いかがなものかと思うのである。
というわけで、 busybox だけでハードウェアの自動認識 & モジュール読み込みを実現することを 目標にしてみた。 前フリが長くなった (長すぎ!) が、ようやくここからが本題である。
「1CD Linux の /linuxrc をいろいろ読んでみた」の中にVIVERは入っていたのでしょうか (^_^;) VIVERは1CD Linuxじゃないですね。
私自身VIVERを作るにあたってinitramfsをたくさんいじってきました。そのあたりのノウハウを書いておこうと思います。VIVERの概要はこのあたりのエントリを。
initramfsのcpioアーカイブ
ハードウェア自動認識から書こうと思ったのですが、長くなるので、まずは短い話題から。
作成できれば同じわけですが、私は以下の方法で作っています。VIVER 1.0のMakefile
# cd path/to/dir && find | cpio --quiet -c -o | gzip -9 -c > viverrd.gz
ブートパラメータの利用
ブートローダでカーネルに渡した引数は、/proc/cmdlineから得ることができます。VIVERでは以下のコードでパースしています。(Ruby)parseメソッド
def parse(path, rewrite = false) # …略… File.open(path) {|file| last_ns = GLOBAL_NS file.gets.split(" ").each {|cmd| if cmd =~ /\:$/ # 最後が: (名前空間指定) last_ns = cmd[0, cmd.length - 1] else key, val = cmd.split("=", 2) if val === nil; val = ""; end @entries[ [last_ns, key] ] = val $log.debug "parameter: #{last_ns}::#{key} = #{val}" end } } end
いろいろ情報が足りていませんが、良きに推測してください ^_^;
見れば分かるかと思いますが、VIVERは名前空間付きのブートパラメータを採用しています。
vmlinuz namespace1: example=hoge sample=fuga namespace2: param
VIVERはinitramfsが終わった後で「プラグインバンドル」を実行するのですが、それぞれのプラグインごとに名前空間を持ち、ブートパラメータで挙動を制御できる(=ブートローダの設定で挙動を制御できる=ネットワークブートする時にサーバーからクライアントの挙動を制御できる)というわけです。
デバイスノードの作成
/devにデバイスノードを作成する方法ですが、もちろんudevでも良いと思います。initramfsで何をやってもいいわけで、そのままinitramfs内でSSHサーバーを起動しようと、Webサーバーを起動しようと。On RAMで動作。
さて、デバイスノードですが、ブロックデバイスのデバイスノード(hda、sda1など)は、/sys/blockを元に作成できます。
シェルスクリプトで書けば↓こんな感じでしょうか。
for dev in /sys/block/*;do line=`cat $dev/dev` major=`echo $line | cut -d ":" -f 1` minor=`echo $line | cut -d ":" -f 2` mknod /dev/`basename $dev` b $major $minor done
ハードウェア自動認識
さてさて、ハードウェア自動認識です。
VIVERではinitramfsにRubyのインタプリタを入れていますので、いろいろ重量のあることをやっています。VIVERのソースコード(master.rbから開始)
まず、ハードウェアを自動認識にあたって、PCIバスに繋がったデバイスと、USBに繋がったデバイスを検出する場合では、処理を別ける必要があります。PCIに繋がったデバイスを検出するには、先の記事のように/sys/bus/pci/devicesと/lib/modules/`uname -r`/modules.pcimapを照らし合わせることで自動認識できます。sourcePCIメソッドとdetectPCIメソッド
これで検出できたカーネルモジュールをロードすると、PCIバスに繋がったデバイスが使えるようになります。PCIバスに繋がったUSBホストコントローラなど。この後にUSBデバイスの検出を行います。(ちなみに、もう一度PCIの検出手順を繰り返すと、CardBusのデバイスも使えるようになります)
そもそもinitramfs内でUSB Mass Storage以外のUSBデバイスが必要になるのか?という話がありますが、続行します。
USBも基本的には/bus/usb/devicesと/lib/modules/`uname -r`/modules.usbmapを照らし合わせるだけで良いのですが、ファイルのフォーマットがPCIとは違うため、マッチングのコードは違うものを用意する必要があります。(PCIよりも情報が多い分複雑)
VIVERでは、「ベンダーID/デバイスIDによる判別」(PCIと同じ)だけではなく、「USBデバイスクラスによる判別」も行っています。sourceUSBメソッドとdetectPCIメソッド
- modules.usbmap
# usb module match_flags idVendor idProduct bcdDevice_lo bcdDevice_hi bDeviceClass bDeviceSubClass bDeviceProtocol bInterfaceClass bInterfaceSubClass bInterfaceProtocol driver_info snd-usb-usx2y 0x0003 0x1604 0x8001 0x0000 0x0000 0x00 0x00 0x00 0x00 0x00 0x00 0x0 snd-usb-usx2y 0x0003 0x1604 0x8007 0x0000 0x0000 0x00 0x00 0x00 0x00 0x00 0x00 0x0 …中略… usb-storage 0x000f 0x03eb 0x2002 0x0100 0x0100 0x00 0x00 0x00 0x00 0x00 0x00 0x0 usb-storage 0x000f 0x03ee 0x6901 0x0000 0x0100 0x00 0x00 0x00 0x00 0x00 0x00 0x0 …以下略…
- /sys/bus/usb/devices
# USBホストコントローラ $ ls /sys/bus/usb/devices/1-0\:1.0/ bAlternateSetting bInterfaceClass bInterfaceNumber bInterfaceProtocol bInterfaceSubClass bNumEndpoints bus@ driver@ ep_81@ modalias power/ subsystem@ uevent usb_endpoint:usbdev1.1_ep81@ # USB HDD $ ls /sys/bus/usb/devices/6-3/ 6-3:1.0/ bDeviceClass bDeviceSubClass bMaxPower bNumInterfaces bmAttributes busnum dev driver@ idProduct manufacturer power/ quirks speed uevent usb_endpoint:usbdev6.3_ep00@ bConfigurationValue bDeviceProtocol bMaxPacketSize0 bNumConfigurations bcdDevice bus@ configuration devnum ep_00@ idVendor maxchild product serial subsystem@ usb_device:usbdev6.3@ version
ベンダーID/デバイスIDによる判別の場合は、
- /sys/bus/usb/devices/X/にidVendorファイルとidProductファイルが存在する
- vendor = `cat idVendor`
- device = `cat idProduct`
- ("%s%s", vendor, device)を、usbmapのidVendor, idProductとマッチ(ただしidVendorが0x000のものは除外する)
で検出できるようです。
デバイスクラスによる判別の場合は、
- /sys/bus/usb/devices/X/にbInterfaceClassファイルが存在する
- base_class = `cat bInterfaceClass`
- base_classをusbmapのbInterfaceClassとマッチ
bInterfaceClassの他にbInterfaceSubClassもあるのですが、これは使ってもあまり意味がありません。サブクラスまでマッチしても結局カーネルモジュールは同じだったりします。
…と、自動判別もできるわけですが、実際には"ehci_hcd", "ohci_hcd", "uhci_hcd", "usb_storage", "usbnet", "usbkbd", "usbmouse", "usbhid"あたりをロードしておけば、大体事足ります。
ストレージモジュール
実は上記のハードウェア自動認識をしただけでは、HDDはまだ見えません。IDEなら"ide-disk.ko"(CDは"ide-cd.ko")、SCSIなら"sd_mod.ko"(CDはsr_mod.ko)…と、ディスクの接続方法別にストレージモジュール(←勝手に命名)(とそのモジュールが依存しているモジュール)をロードする必要があります。
VIVERは、PCIバスに繋がったストレージコントローラのClass ID(/sys/bus/pci/devices/*/class)を見ることで、ディスクがどのように接続されているかを判別し、対応するモジュールをロードしています。getModulePCIメソッド
…と、VIVERはがんばっているわけですが、実際のところ、sd_modやide-disk、libataくらいは決め打ちでロードしてしまえばいいというのが現実です。
ファイルシステムタイプ判別
ここまできたら、ファイルシステムタイプ(ext3、XFS、…)まで自動判別するべきです。そこまでして始めてマウントができます。
基本的な方針は、「知っているファイルシステムタイプでひたすらマウントしてみて、マウントできたら、そのファイルシステムタイプ」というもの。しかしこれだけでは格好悪い。というわけでVIVERは、ディスクの容量、接続方法(USB、IDE…)、IDEならプライマリかセカンダリかを元に、ファイルシステムタイプをある程度推測してからマウントを試みるようにしています。guessPartitionメソッドとguessFSTypeメソッドとprobeFSTypeメソッド
容量が大きければ、おそらくHDDなので、USB接続ならFAT、IDEならNTFSかな?(←VIVERの自動認識を使うような環境では)容量が少なければおそらくフラッシュメモリなので、たぶんFAT。
ファイルシステムタイプを判別する前に、ファイルシステムモジュール(ext3.ko、xfs.koなど)をロードしておく必要があります。loadFilesystemModulesメソッド
どこまで自動認識するか、どこから決め打ちするかが問題になるわけですが、がんばっても実際は決め打ちで何とかなることが多い、というのが私の経験です。ネットワークブートなどでinitramfsを自作する場合でも、PCIデバイスの自動検出くらいにしておくのが無難かと思われます。(VIVER COREが出来るまでは!)