みつきんのメモ

組み込みエンジニアです。Interface誌で「Yocto Projectではじめる 組み込みLinux開発入門」連載中

YoctoProjectでinitramfs入門

はじめに

YoctoProjectで作成するLinuxイメージでもinitramfsを使用することができる。 これまであまりつかってこなかったので入門してみる。

使用するバージョンはkirkstone(4.0)。

環境構築

今回はqemuarm64のターゲットで実験する。

initramfsはカーネルイメージにバンドルして1つのファイルにする方法と、 別々のファイルにして、ブートローダに読み込ませる方法がある。

前者の方は、管理するファイルが少なくなり、u-bootなどのブートローダに対する設定項目も減るため、 特に理由がなければバンドルする方で問題ないと思われる。

poky

pokyを取得する。

$ ~/yocto/qemuarm64
$ git clone git://git.yoctoproject.org/poky.git -b kirkstone

環境変数の設定

$ source poky/oe-init-build-env

local.conf

Webでよく見かける使い方。local.confに下記を追加する。

MACHINE = "qemuarm64"

INITRAMFS_IMAGE = "core-image-minimal-initramfs"
INITRAMFS_IMAGE_BUNDLE = "1"
INITRAMFS_FSTYPES = "cpio.gz"

これで、initramfsがくっついたカーネルイメージが作成されるようになる。

動作確認

ビルド

これでビルドをしてみる。

$ bitbake core-image-minimal

確かにinitramfs付きのカーネルイメージがビルドされているように見える。

ls ./tmp/deploy/images/qemuarm64/Image-initramfs-qemuarm64.bin 
./tmp/deploy/images/qemuarm64/Image-initramfs-qemuarm64.bin

動作確認

普通に起動したように見える。

$ runqemu nographic
... (snip) ...
Poky (Yocto Project Reference Distro) 4.0.6 qemuarm64 ttyAMA0

qemuarm64 login:

initramfsを使用しているの確認したいが、痕跡がよくわからない。

先に結論を言うと、この状態ではinitramfsは使用されない。

使用するカーネルの変更

runqemuはcore-image-minimal-qemuarm64.qemuboot.confを参照して、QEMUを起動する。このファイルにはQEMUを起動する際に使用されるデフォルトのパラメータが記述されている。

デフォルトのカーネルイメージはImage--5.15.78+git0+f475b1a9de_a1d364fbe3-r0-qemuarm64-20230105065908.binとなっている。

$ cat ./tmp/deploy/images/qemuarm64/core-image-minimal-qemuarm64.qemuboot.conf  | grep qb_default_kernel
qb_default_kernel = Image--5.15.78+git0+f475b1a9de_a1d364fbe3-r0-qemuarm64-20230105065908.bin

これは、Imageに貼られたシンボリックリンクの実体で初期状態のカーネルイメージとなっている。 つまり、initramfsはバンドルされていない。

カーネルを指定してrunqemuを実行する。

$ runqemu nographic ./tmp/deploy/images/qemuarm64/Image-initramfs-qemuarm64.bin
... (snip) ...
Poky (Yocto Project Reference Distro) 4.0.6 qemuarm64 ttyAMA0

qemuarm64 login: 

起動した。initramfsを使用しているはずだが、痕跡を確認できない。

initramfsの構造

initramfsのディレクトリツリーを確認する。

$ pushd ./tmp/work/qemuarm64-poky-linux/core-image-minimal-initramfs/1.0-r0/rootfs
$ ls
bin  boot  dev  etc  home  init  init.d  lib  media  mnt  proc  run  sbin  sys  tmp  usr  var

initがinitramfsで最初に実行されるプログラムだと推測できる。

initramfsのinit

initの基本的な構造は下記のようになっている。

  1. 主要変数の定義
  2. /proc/cmdlineの解析
  3. modulesの実行

主要変数の定義

主要な変数の定義は下記のようになっている。注目すべきなのはMODULES_DIR=/init.dの部分。

# Variables shared amoung modules
ROOTFS_DIR="/rootfs" # where to do the switch root
MODULE_PRE_HOOKS=""  # functions to call before running each module
MODULE_POST_HOOKS="" # functions to call after running each module
MODULES_DIR=/init.d  # place to look for modules
EFI_DIR=/sys/firmware/efi  # place to store device firmware information

/proc/cmdlineの解析

カーネルに渡されたブートパラメータは/proc/cmdlineを読むことで参照できる。

# populate bootparam environment
for p in `cat /proc/cmdline`; do
    if [ -n "$quoted" ]; then
        value="$value $p"
        if [ "`echo $p | sed -e 's/\"$//'`" != "$p" ]; then
            eval "bootparam_${quoted}=${value}"
            unset quoted
        fi
        continue
    fi

    opt=`echo $p | cut -d'=' -f1`
    opt=`echo $opt | sed -e 'y/.-/__/'`
    if [ "`echo $p | cut -d'=' -f1`" = "$p" ]; then
        eval "bootparam_${opt}=true"
    else
        value="`echo $p | cut -d'=' -f2-`"
        if [ "`echo $value | sed -e 's/^\"//'`" != "$value" ]; then
            quoted=${opt}
            continue
        fi
        eval "bootparam_${opt}=\"${value}\""
    fi
done

このスクリプトではブートパラメータとして渡された内容は、bootparam_XXXという変数として参照できるようになっている。 細かいルールは下記のようにスクリプト冒頭のコメントで説明されている。

# Boot parameters are available on environment in the as:
#
# 'foo=value' as 'bootparam_foo=value'
# 'foo' as 'bootparam_foo=true'
# 'foo.bar[=value] as 'foo_bar=[value|true]'

これを踏まえて、プリント関連の関数の実装を見てみると、下記のようになっている。

# Prints information
msg() {
    echo "$@" >/dev/console
}

# Prints information if verbose bootparam is used
info() {
    [ -n "$bootparam_verbose" ] && echo "$@" >/dev/console
}

# Prints information if debug bootparam is used
debug() {
    [ -n "$bootparam_debug" ] && echo "DEBUG: $@" >/dev/console
}

ブートパラメータにverbosedebugを渡しておくと、initramfs内でのプリント出力が増えることが推測できる。

modulesの実行

modulesとはなにか?を一旦おいて、下記の部分を見る。

# Load and run modules
for m in $MODULES_DIR/*; do
    # Skip backup files
    if [ "`echo $m | sed -e 's/\~$//'`" != "$m" ]; then
        continue
    fi

    module=`basename $m | cut -d'-' -f 2`
    debug "Loading module $module"

... (snip) ...

    # process module
 . $m

    if ! eval "${module}_enabled"; then
        debug "Skipping module $module"
        continue
    fi

    debug "Running ${module}_run"
    eval "${module}_run"

... (snip) ...

done

MODULES_DIRにあるファイルを読み込んで何やら実行していることは読み取ることができる。 MODULES_DIRは/init.dが設定されているので、下記を確認する。

$ ls ./init.d
01-udev  80-setup-live  90-rootfs  99-finish  install-efi.sh  install.sh

何やらスクリプトが格納されている。 moduleについては、スクリプト冒頭のコメントの下記の部分で説明されている。

# Modules need to provide the following functions:
#
# <module>_enabled : check if the module ought to run (return 1 to skip)
# <module>_run     : do what is need

modulesはinitramfsの/init.dに格納されていて、<module>_enabled<module>_runの2つの関数が実装されているスクリプトということになる。 modulesは辞書順に読み込まれるため、現状のcore-image-minimal-initramfsでは01-udevから99-finishまで順に実行される。 ちなみに、99-finishの中でswitch_rootするため、それ以降のファイルは実行されない。

出力を増やして動作確認

$ popd
$ runqemu nographic ./tmp/deploy/images/qemuarm64/Image-initramfs-qemuarm64.bin bootparams="verbose debug"
... (snip) ...
DEBUG: Loading module udev
DEBUG: Running udev_run
Starting version 250.5+
[    4.720551] EXT4-fs (vda): recovery complete
[    4.721767] EXT4-fs (vda): mounted filesystem with ordered data mode. Opts: (null). Quota mode: disabled.
DEBUG: Loading module setup
DEBUG: Calling module hook (pre): udev_shutdown_hook_handler
DEBUG: Finished module hook (pre): udev_shutdown_hook_handler
DEBUG: Running setup_run
DEBUG: Loading module rootfs
DEBUG: Calling module hook (pre): udev_shutdown_hook_handler
DEBUG: Finished module hook (pre): udev_shutdown_hook_handler
DEBUG: Running rootfs_run
DEBUG: No e2fs compatible filesystem has been mounted, mounting /dev/vda...
DEBUG: Loading module finish
DEBUG: Calling module hook (pre): udev_shutdown_hook_handler
DEBUG: Finished module hook (pre): udev_shutdown_hook_handler
DEBUG: Running finish_run
... (snip) ...

initramfs中のmodulesが実行されていることが確認できた。

YoctoProjectのinitrmafsのmodulesについて

YoctoProjectのinitramfsはmodulesによって任意の処理を実行することができる。 これらのモジュールはどの様に選択、変更するのかを調査する。

core-image-minimal-initramfs.bbを参照する。

INITRAMFS_SCRIPTS ?= "\
                      initramfs-framework-base \
                      initramfs-module-setup-live \
                      initramfs-module-udev \
                      initramfs-module-install \
                      initramfs-module-install-efi \
                     "

PACKAGE_INSTALL = "${INITRAMFS_SCRIPTS} ${VIRTUAL-RUNTIME_base-utils} udev base-passwd ${ROOTFS_BOOTSTRAP_INSTALL}"

PACKAGE_INSTALLがinitramfsにパッケージを追加するための変数で、INITRAMFS_SCRIPTSを参照している。 INITRAMFS_SCRIPTSで指定されているパッケージは、initramfs-framework_1.0.bbで提供されている。

... (snip) ...

PACKAGES = "${PN}-base \
            initramfs-module-exec \
            initramfs-module-mdev \
            initramfs-module-udev \
            initramfs-module-e2fs \
            initramfs-module-nfsrootfs \
            initramfs-module-rootfs \
            initramfs-module-debug \
            initramfs-module-lvm \
            initramfs-module-overlayroot \
           "

... (snip) ...

SUMMARY:initramfs-module-exec = "initramfs support for easy execution of applications"
RDEPENDS:initramfs-module-exec = "${PN}-base"
FILES:initramfs-module-exec = "/init.d/89-exec"

SUMMARY:initramfs-module-mdev = "initramfs support for mdev"
RDEPENDS:initramfs-module-mdev = "${PN}-base busybox-mdev"
FILES:initramfs-module-mdev = "/init.d/01-mdev"

SUMMARY:initramfs-module-udev = "initramfs support for udev"
RDEPENDS:initramfs-module-udev = "${PN}-base udev"
FILES:initramfs-module-udev = "/init.d/01-udev"

SUMMARY:initramfs-module-e2fs = "initramfs support for ext4/ext3/ext2 filesystems"
RDEPENDS:initramfs-module-e2fs = "${PN}-base"
FILES:initramfs-module-e2fs = "/init.d/10-e2fs"

SUMMARY:initramfs-module-nfsrootfs = "initramfs support for locating and mounting the root partition via nfs"
RDEPENDS:initramfs-module-nfsrootfs = "${PN}-base"
FILES:initramfs-module-nfsrootfs = "/init.d/85-nfsrootfs"

SUMMARY:initramfs-module-rootfs = "initramfs support for locating and mounting the root partition"
RDEPENDS:initramfs-module-rootfs = "${PN}-base"
FILES:initramfs-module-rootfs = "/init.d/90-rootfs"

SUMMARY:initramfs-module-debug = "initramfs dynamic debug support"
RDEPENDS:initramfs-module-debug = "${PN}-base"
FILES:initramfs-module-debug = "/init.d/00-debug"

SUMMARY:initramfs-module-lvm = "initramfs lvm rootfs support"
RDEPENDS:initramfs-module-lvm = "${PN}-base"
FILES:initramfs-module-lvm = "/init.d/09-lvm"

SUMMARY:initramfs-module-overlayroot = "initramfs support for mounting a RW overlay on top of a RO root filesystem"
RDEPENDS:initramfs-module-overlayroot = "${PN}-base initramfs-module-rootfs"
FILES:initramfs-module-overlayroot = "/init.d/91-overlayroot"

modulesを変更してみる

INITRAMFS_SCRIPTSの内容を修正することで実行するmodulesを変更できそうなことがわかった。 この変数はcore-image-minimal-initramfsにbbappendを作成して変更できるが、手っ取り早く試すにはlocal.confでも上書きすることができる。

local.confに下記の内容を追加する。

INITRAMFS_SCRIPTS = "\
                      initramfs-framework-base \
                      initramfs-module-debug \
 "

QB_DEFAULT_KERNEL = "Image-initramfs-qemuarm64.bin"
QB_KERNEL_CMDLINE_APPEND:append = " debug shell"

QB_DEFAULT_KERNELで、runqemu実行時にinitramfs付きのカーネルを使用するように変更。 QB_KERNEL_CMDLINE_APPENDで、ブートパラメータを追加している。

initramfs-module-debugは文字通り、initramfsをデバッグするためのmodulesで、下記のようにフックを追加している。

debug_run() {
    add_module_pre_hook "debug_hook_handler"
    add_module_post_hook "debug_hook_handler"
}

フックは下記のような条件で実行される。

# Adds support to dynamic debugging of initramfs using bootparam in
# following format:
#   shell                 : starts a shell before and after each module
#   shell=before:<module> : starts a shell before <module> is loaded and run
#   shell=after:<module>  : starts a shell after <module> is loaded and run
#
#   shell-debug                 : run set -x as soon as possible
#   shell-debug=before:<module> : run set -x before <module> is loaded and run
#   shell-debug=after:<module>  : run set -x after <module> is loaded and run

今回はブートパラメータにshellを追加しているので、他のすべてのモジュールが読み込まれる前後にshにフォールバックされる。

動作確認

下記のようにしてイメージを作り直し、動作確認する。

$ bitbake core-image-minimal-initramfs -c cleansstate
$ bitbake core-image-minimal
$ runqemu nographic

これで、ログインの前にシェルが実行されれば成功。

DEBUG: Loading module debug
DEBUG: Running debug_run
DEBUG: Calling module hook (post): debug_hook_handler
Starting shell after debug...
sh: can't access tty; job control turned off
/ # ls
bin     dev     home    init.d  media   proc    run     sys     usr
boot    etc     init    lib     mnt     rootfs  sbin    tmp     var
/ # 

exitでシェルを抜けるときちんと次のモジュールの前と後ろで毎回シェルが実行される。

まとめ

YoctoProjectでinitramfsを使用する方法を調査した。 initramfsに関連するイメージレシピはいくつかあるが、core-image-minimal-initramfsを使用するので問題はない感じ。

その上で初期化処理を変更したりデバッグしたい場合はINITRAMFS_SCRIPTSを上書きしてmodulesを変更する。

initramfsを使用するためにはINITRAMFS_IMAGEでinitramfsを有効化するだけでは不十分で、カーネルにバンドルした場合は、 デフォルトのカーネルイメージではなくinitramfsがバンドルされたカーネルをイメージを使用する様に、ブートローダなどの 設定を変更しておく必要がある。

DebianUbuntuでinitramfs-toolsを使ってinitramfsをカスタマイズしたことがある人は、その作業手順や内容の違いにちょっと驚くかもしれない。