Tiny Core LinuxでLinuxのinitプロセスが実行されるあたりを調べる

この記事はLinux Advent Calendar 2020 - Qiitaの1日目の記事です。

Tiny Core Linux(以下tcl)を使ってLinuxのブートプロセスを見てましょう。Tiny Core Linuxは軽量ディストリビューションで最小のisoイメージだと11MBほどです😃 ブートプロセスを見ると言っても電源onからの流れではなくてinitプロセスの実行に関する部分です。

この記事ではバージョン11.1のCore-current.iso を利用しています。

www.tinycorelinux.net

isoファイルの構成

まずはtclのisoがどんな感じで構成されていて、Linuxを起動させるのか確認しましょう。 isoファイルの構成はこのようになっています。

$ sudo mount -o loop Core-current.iso ./mnt
$ tree ./mnt/
./mnt/
└── boot
    ├── core.gz
    ├── isolinux
    │   ├── boot.cat
    │   ├── boot.msg
    │   ├── f2
    │   ├── f3
    │   ├── f4
    │   ├── isolinux.bin
    │   └── isolinux.cfg
    └── vmlinuz

2 directories, 9 files

vmlinuzはカーネル、core.gzはinitramfsのファイル、そして起動にはisolinuxを使っているのでそのファイル類が含まれています。

起動の設定はisolinux.cfgに書かれています。

display boot.msg
default microcore
label microcore
        kernel /boot/vmlinuz
        initrd /boot/core.gz
        append loglevel=3

label mc
        kernel /boot/vmlinuz
        append initrd=/boot/core.gz loglevel=3
implicit 0
prompt 1
timeout 300
F1 boot.msg
F2 f2
F3 f3
F4 f4

Tiny Core Linuxnのinitramfsの中身

今回はisoファイルの内容を作業用のディレクトリを作ってそこでファイルを読んだりしていきます。

$ mkdir tcl-work
$ cp -a mnt/boot/ tcl-work/.

core.gzの中も展開します。core.gzはcpioファイルをgzipで圧縮したものなので展開してからcpioコマンドで中身を取り出します。

$ cd tcl-work
$ mkdir core-work
$ cd core-work
$ zcat ../../mnt/boot/core.gz | sudo cpio -i -H newc -dv

展開するとこのようになります。

$ ls -la
total 72
drwxrwxr-x. 17 masami masami 4096 Nov 22 10:30 ./
drwxrwxr-x.  4 masami masami 4096 Nov 22 10:30 ../
drwxr-xr-x.  2 masami masami 4096 Nov 22 10:30 bin/
drwxrwxr-x.  7 masami masami 4096 Nov 22 10:30 dev/
drwxr-xr-x.  8 masami masami 4096 Nov 22 10:30 etc/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 home/
-rwxr-xr-x.  1 masami masami  496 Nov 22 10:30 init*
drwxr-xr-x.  4 masami masami 4096 Nov 22 10:30 lib/
lrwxrwxrwx.  1 masami masami   11 Nov 22 10:30 linuxrc -> bin/busybox*
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 mnt/
drwxrwsr-x.  2 masami masami 4096 Nov 22 10:30 opt/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 proc/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 root/
drwxrwxr-x.  3 masami masami 4096 Nov 22 10:30 run/
drwxr-xr-x.  2 masami masami 4096 Nov 22 10:30 sbin/
drwxrwxr-x.  2 masami masami 4096 Nov 22 10:30 sys/
drwxrwxrwt.  2 masami masami 4096 Nov 22 10:30 tmp/
drwxr-xr-x.  7 masami masami 4096 Nov 22 10:30 usr/
drwxrwxr-x.  8 masami masami 4096 Nov 22 10:30 var/

システムの起動に関連しそうなファイルとして /linuxrc/initがありますね。linuxrcはbusyboxコマンドへのリンクで/initはシェルスクリプトになっています。このファイルの内容は後で見てみます。

ちなみにfedora 33のinitramfsはこのようになっていて/linuxrcはありません。

$ /usr/lib/dracut/skipcpio ./initramfs-5.9.9-200.fc33.x86_64.img | zcat | cpio -idv
$ ls -la
total 32680
drwxrwxr-x. 12 masami masami     4096 Nov 22 02:07 ./
drwxrwxr-x.  5 masami masami     4096 Nov 22 12:45 ../
lrwxrwxrwx.  1 masami masami        7 Nov 22 02:07 bin -> usr/bin/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 dev/
drwxr-xr-x. 11 masami masami     4096 Nov 22 02:07 etc/
lrwxrwxrwx.  1 masami masami       23 Nov 22 02:07 init -> usr/lib/systemd/systemd*
lrwxrwxrwx.  1 masami masami        7 Nov 22 02:07 lib -> usr/lib/
lrwxrwxrwx.  1 masami masami        9 Nov 22 02:07 lib64 -> usr/lib64/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 proc/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 root/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 run/
lrwxrwxrwx.  1 masami masami        8 Nov 22 02:07 sbin -> usr/sbin/
-rwxr-xr-x.  1 masami masami     3418 Nov 22 02:07 shutdown*
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 sys/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 sysroot/
drwxr-xr-x.  2 masami masami     4096 Nov 22 02:07 tmp/
drwxr-xr-x.  8 masami masami     4096 Nov 22 02:07 usr/
drwxr-xr-x.  3 masami masami     4096 Nov 22 02:07 var/

では/linuxrcと/initはどのように使われるのでしょうか。/initや/linuxrcが利用される仕組みはEarly userspace support — The Linux Kernel documentationに説明があります。tclではinitramfsが利用されてます。この場合、以下の部分が該当します。/initが必須ということですね。

using initramfs. The call to prepare_namespace() must be skipped. This means that a binary must do all the work. 
Said binary can be stored into initramfs either via modifying usr/gen_init_cpio.c or via the new initrd format, an cpio archive. It must be called “/init”. 
This binary is responsible to do all the things prepare_namespace() would do.

起動時のログからもinitramfsが利用されていることがわかります。

tc@box:~$ dmesg | grep initramfs
Trying to unpack rootfs image as initramfs...

Tiny Linux Coreで起動シーケンスの確認

ではこれらの動作を確認してみましょう。 と、その前にqemuGUIを立ち上げるとコンソール出力が見にくいのでシリアルコンソールを有効にしてしまいます。 編集するのはisolinux.confです。このファイルにラベルは2つありますがデフォルトはmicrocoreのほうなのでこちらに設定しましょう。console=ttyS0,115200をappendの行に追加します。 ついでなのでログレベルも最大にしてしまいます。そうするとこのような感じになります。

append loglevel=7 console=ttyS0,115200

つぎにinitramfsのファイルを弄りましょう。シリアルコンソールの設定をetc/inittabに追加します。

ttyS0::respawn:/sbin/getty -nl /sbin/autologin 115200 ttyS0 vt102

そうしたら本題の/iniitと/linuxrcをrenameしてみます。

-rwxr-xr-x.  1 root   root    496 Nov 22 12:14 init.bk*
drwxr-xr-x.  4 root   root   4096 Nov 22 12:14 lib/
lrwxrwxrwx.  1 root   root     11 Nov 22 12:14 linuxrc.bk -> bin/busybox*

core.gzを作成しましょう。

$ sudo find | sudo cpio -o -H newc | gzip -2 > ../boot/core.gz

これで新しいintramfsのイメージもできたのでisoイメージを作成します。 実行はtcl-workディレクトリの1つ上の場所で行います。

$ sudo mkisofs -l -J -R -r -V TC-custom -no-emul-boot -boot-load-size 4 \
 -boot-info-table -b boot/isolinux/isolinux.bin \
 -c boot/isolinux/boot.cat -o test.iso tcl-work/

これでisoファイルが出来たのでqemuで起動します。

$ qemu-system-x86_64 -boot d -cdrom test.iso -m 512 -nographic

見事にカーネルパニックします😃

Floppy drive(s): fd0 is 2.88M AMI BIOS
FDC 0 is a S82078B
blk_update_request: I/O error, dev fd0, sector 0 op 0x0:(READ) flags 0x0 phys_seg 1 prio class 0
floppy: error 10 while reading block 0
VFS: Cannot open root device "(null)" or unknown-block(2,0): error -6
Please append a correct "root=" boot option; here are the available partitions:
0100            8192 ram0 
 (driver?)
0101            8192 ram1 
 (driver?)
0102            8192 ram2 
 (driver?)
0103            8192 ram3 
 (driver?)
0104            8192 ram4 
 (driver?)
0105            8192 ram5 
 (driver?)
0106            8192 ram6 
 (driver?)
0107            8192 ram7 
 (driver?)
0b00         1048575 sr0 
 driver: sr
0200               4 fd0 
 driver: floppy
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(2,0)
Kernel Offset: disabled
Rebooting in 60 seconds..

まずは/linuxrcの名前だけ戻して起動させます。

VFS: Cannot open root device "(null)" or unknown-block(2,0): error -6
Please append a correct "root=" boot option; here are the available partitions:
0100            8192 ram0 
 (driver?)
0101            8192 ram1 
 (driver?)
0102            8192 ram2 
 (driver?)
0103            8192 ram3 
 (driver?)
0104            8192 ram4 
 (driver?)
0105            8192 ram5 
 (driver?)
0106            8192 ram6 
 (driver?)
0107            8192 ram7 
 (driver?)
0b00         1048575 sr0 
 driver: sr
0200               4 fd0 
 driver: floppy
Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block(2,0)
Kernel Offset: disabled

そうするとカーネルパニックとなりました。ではlinuxrcの名前は変更した状態で、/initのほうは名前を戻して起動してみましょう。

   ( '>')
  /) TC (\   Core is distributed with ABSOLUTELY NO WARRANTY.
 (/-_--_-\)           www.tinycorelinux.net

tc@box:~$ e1000 0000:00:03.0 eth0: (PCI:33MHz:32-bit) 52:54:00:12:34:56
e1000 0000:00:03.0 eth0: Intel(R) PRO/1000 Network Connection
e1000: eth0 NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX

tc@box:~$ ls -la /
total 4
drwxrwxr-x   17 1000     1000           380 Nov 22 05:28 ./
drwxrwxr-x   17 1000     1000           380 Nov 22 05:28 ../
drwxr-xr-x    2 root     root          1380 Nov 22 03:14 bin/
drwxrwxr-x   12 root     staff         4260 Nov 22 05:32 dev/
drwxr-xr-x    8 root     root           680 Nov 22 05:32 etc/
drwxrwxr-x    3 root     staff           60 Nov 22 05:32 home/
-rwxr-xr-x    1 root     root           496 Nov 22 03:14 init
drwxr-xr-x    4 root     root           800 Nov 22 03:14 lib/
lrwxrwxrwx    1 root     root            11 Nov 22 03:14 linuxrc.bk -> bin/busybox
drwxrwxr-x    4 root     staff           80 Nov 22 05:32 mnt/
drwxrwsr-x    2 root     staff          160 Nov 22 03:14 opt/
dr-xr-xr-x   64 root     root             0 Nov 22 05:32 proc/
drwxrwxr-x    2 root     staff           80 Nov 22 03:14 root/
drwxrwxr-x    4 root     staff           80 Nov 22 05:32 run/
drwxr-xr-x    2 root     root          1200 Nov 22 03:14 sbin/
dr-xr-xr-x   12 root     root             0 Nov 22 05:32 sys/
drwxrwxrwt    4 root     staff          120 Nov 22 05:32 tmp/
drwxr-xr-x    7 root     root           140 Nov 22 03:14 usr/
drwxrwxr-x    8 root     staff          180 Nov 22 03:14 var/

今度は起動します。ということでinitramfsを利用する環境では/linuxrcなくてもOKというのが実際の動作からわかりました。

/initの処理

/initは次のような短いシェルスクリプトです。

#!/bin/sh
mount proc
grep -qw multivt /proc/cmdline && sed -i s/^#tty/tty/ /etc/inittab
if ! grep -qw noembed /proc/cmdline; then

  inodes=`grep MemFree /proc/meminfo | awk '{print $2/3}' | cut -d. -f1`

  mount / -o remount,size=90%,nr_inodes=$inodes
  umount proc
  exec /sbin/init
fi
umount proc
if mount -t tmpfs -o size=90% tmpfs /mnt; then
  if tar -C / --exclude=mnt -cf - . | tar -C /mnt/ -xf - ; then
    mkdir /mnt/mnt
    exec /sbin/switch_root mnt /sbin/init
  fi
fi
exec /sbin/init

execコマンドの実行箇所が3箇所ありますが、今回の環境では最初のifブロック部分が実行されます。tclの環境でmountコマンドの出力を見るとこんなふうになってます。

tc@box:~$ mount
rootfs on / type rootfs (rw,size=400480k,nr_inodes=141441)
proc on /proc type proc (rw,relatime)
sysfs on /sys type sysfs (rw,relatime)
devpts on /dev/pts type devpts (rw,relatime,mode=600,ptmxmode=000)
tmpfs on /dev/shm type tmpfs (rw,relatime)
fusectl on /sys/fs/fuse/connections type fusectl (rw,relatime)

この環境では搭載されているメモリサイズに応じたinode数やメモリの何%をファイルシステム容量上限とするかを設定した上でルートファイルシステムを再マウントし直しています。そして/sbin/initを実行します。

先に引用したinitramfsnの説明文で次のような一文がありました。

This binary is responsible to do all the things prepare_namespace() would do.

ここでのnamespaceはコンテナの文脈で使うnamepspaceとは別のものです。prepare_namespace()はルートファイルシステムをマウントしてそこにchrootする処理を行います。initramfsを使う場合はその処理は/initで行ってねということです。

カーネルの動作

起動処理において/initが必要というのはわかりましたが、カーネルからはどう動くのか?というのを見ていきます。tclの11.1ではカーネル5.4.3が使われているのでこのバージョンで確認しましょう。/initを実行するのはinit/main.cのrun_init_process()にて行います。このときの関数の呼ばれ方は次のようになります。

  1. start_kernel()
  2. arch_call_rest_init()
  3. rest_init()
  4. kernel_init()
  5. run_init_process()

start_kernel()の一番最後でarch_call_rest_init()を呼び出して流れていきます。arch_call_rest_init()は名前からしアーキテクチャ毎に実装しする感じですが、s390以外は独自の実装はなくてinit/main.cにあるarch_call_rest_init()を利用しています。

run_init_process()は次のように実行します。

 if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
        pr_err("Failed to execute %s (error %d)\n",
               ramdisk_execute_command, ret);
    }

ramdisk_execute_commandはkernel_init_freeable()にて次のように/initを設定します。

 if (!ramdisk_execute_command)
        ramdisk_execute_command = "/init";

ramdisk_execute_commandのデフォルト値はNULLですが、カーネルコマンドラインパラメータでrdinit=で指定することができます。

static int __init rdinit_setup(char *str)
{
    unsigned int i;

    ramdisk_execute_command = str;
    /* See "auto" comment in init_setup */
    for (i = 1; i < MAX_INIT_ARGS; i++)
        argv_init[i] = NULL;
    return 1;
}
__setup("rdinit=", rdinit_setup);

特に指定がなければ/initが使われます。ということでinitramfsに/initがないとエラーになるのはこれってことがわかります。/initの実行に成功したら0を返してkernel_init()の処理は終了です。 /initは/sbin/initを実行するので本当のinit処理は/initから実行される感じです。