ext4_file_open()を調べるついでにinitramfsのinit(Arch Linux)を読んでみた

ext4_file_open()の226行目のif文内を実行するのはどんな時なんだろう?と思って調べてみたらinit処理に関係している部分だったのでinitramfsのinitを読んでみた。

218 static int ext4_file_open(struct inode * inode, struct file * filp)
219 {
220         struct super_block *sb = inode->i_sb;
221         struct ext4_sb_info *sbi = EXT4_SB(inode->i_sb);
222         struct vfsmount *mnt = filp->f_path.mnt;
223         struct path path;
224         char buf[64], *cp;
225 
226         if (unlikely(!(sbi->s_mount_flags & EXT4_MF_MNTDIR_SAMPLED) &&
227                      !(sb->s_flags & MS_RDONLY))) {
228                 sbi->s_mount_flags |= EXT4_MF_MNTDIR_SAMPLED;

if文内にprintk()を追加してカーネルの再コンパイルと再起動。

        if (unlikely(!(sbi->s_mount_flags & EXT4_MF_MNTDIR_SAMPLED) &&
                     !(sb->s_flags & MS_RDONLY))) {
+               pr_info("[-]AAA: pid:%d %s\n", current->pid, current->comm);
+               pr_info("[-]AAA: %s\n", filp->f_path.dentry->d_name.name);
                sbi->s_mount_flags |= EXT4_MF_MNTDIR_SAMPLED;

起動したらdmesgで確認。

masami@kerntest:~/linux-kernel/fs/ext4 (ktest)$ dmesg | grep AAA
[    0.702590] [-]AAA: pid:1 switch_root
[    0.702592] [-]AAA: systemd

テストした環境はパーティションは/だけでファイルシステムext4。ここが実行されたのは1度だけでプロセスはswitch_root、オープンしようとしたファイルはsystemd。ここのifの条件はスーパーブロックのマウントフラグを見ているのでext4のパーティションが複数ある場合、そのパーティションにあるファイルを最初に(書き込み可で)開くタイミングで実行されるんでしょうね。2回目はEXT4_MF_MNTDIR_SAMPLEDビットが立っているから条件は成立しないし。


では、initramfsにあるinitの主な処理を見てみよう。Arch LinuxのinitramfsはArch Linux独自のmkinitcpioというコマンドで作られてます。mkinitcpioで作られたinitramfsはcpioのアーカイブをgzipしたものなので以下のように展開。

$ gzip -dc /boot/initramfs-linux.img | cpio -iv

展開後のディレクトリ構成は以下のような形。

masami@saga:~/tmp/initramfs$ ls -la
total 76
drwxr-xr-x 11 masami masami  4096 Jan  6 11:40 .
drwxr-xr-x  3 masami masami  4096 Jan  6 00:26 ..
lrwxrwxrwx  1 masami masami     7 Jan  6 00:24 bin -> usr/bin
-rw-r--r--  1 masami masami  2490 Jan  6 00:24 buildconfig
-rw-r--r--  1 masami masami    64 Jan  6 00:24 config
drwxr-xr-x  2 masami masami  4096 Jan  6 00:24 dev
drwxr-xr-x  4 masami masami  4096 Jan  6 00:24 etc
drwxr-xr-x  2 masami masami  4096 Jan  6 00:24 hooks
-rwxr-xr-x  1 masami masami  2385 Jan  6 00:24 init
-rw-r--r--  1 masami masami 12469 Jan  6 00:24 init_functions
lrwxrwxrwx  1 masami masami     7 Jan  6 00:24 lib -> usr/lib
lrwxrwxrwx  1 masami masami     7 Jan  6 00:24 lib64 -> usr/lib
drwxr-xr-x  2 masami masami  4096 Jan  6 00:24 new_root
drwxr-xr-x  2 masami masami  4096 Jan  6 00:24 proc
drwxr-xr-x  2 masami masami  4096 Jan  6 00:24 run
lrwxrwxrwx  1 masami masami     7 Jan  6 00:24 sbin -> usr/bin
drwxr-xr-x  2 masami masami  4096 Jan  6 00:24 sys
drwxr-xr-x  2 masami masami  4096 Jan  6 00:24 tmp
drwxr-xr-x  5 masami masami  4096 Jan  6 00:24 usr
-rw-r--r--  1 masami masami     2 Jan  6 00:24 VERSION

initが本体でinit_functionsがサポートライブラリでどちらもシェルスクリプト。シェルは/usr/bin/ashで実体はbusyboxです。

最初に実行するのはこなんだけど、./usr/bin/にはsystemd-timestampがないので今は実行されていない。

if [ -x /usr/bin/systemd-timestamp ]; then
    RD_TIMESTAMP=$(systemd-timestamp)
fi

擬似ファイルシステム、tmpfs等のマウント。

mount -t proc proc /proc -o nosuid,noexec,nodev
mount -t sysfs sys /sys -o nosuid,noexec,nodev
mount -t devtmpfs dev /dev -o mode=0755,nosuid
mount -t tmpfs run /run -o nosuid,nodev,mode=0755

カーネルコマンドラインの読み込み。

parse_cmdline </proc/cmdline

/hookにあるファイルのうち実行したくないファイルから実行権限を外す。

for d in ${disablehooks//,/ }; do
    [ -e "/hooks/$d" ] && chmod 644 "/hooks/$d"
done

このdisablehooksはカンマ区切りの文字列でカーネルのコマンドラインで渡すことができる。

今あるhookはudebだけだった。

masami@saga:~/tmp/initramfs$ ls hooks/
.  ..  udev

hookを実行。

run_hookfunctions 'run_earlyhook' 'early hook' $EARLYHOOKS

$EARLYHOOKSは/configで定義されている変数。

EARLYHOOKS="udev"
HOOKS="udev"
LATEHOOKS=""
CLEANUPHOOKS="udev"

カーネルモジュールの読み込み。ここではカーネルコマンドラインでearlymodules=foo,barのように渡されたものが対象。

if [ -n "$earlymodules$MODULES" ]; then
    modprobe -qab ${earlymodules//,/ } $MODULES
fi

再度hookの実行。

run_hookfunctions 'run_hook' 'hook' $HOOKS

rootデバイスの設定。

rootdev=$(resolve_device "$root") && root=$rootdev
unset rootdev

必要ならfsck

fsck_root

本当の/になる/new_rootをマウント。

# Mount root at /new_root
"$mount_handler" /new_root

$mount_handlerはデフォルトではdefault_mount_handler()が設定されている。
default_mount_handlerの処理は対象がブロックデバイスか調べるのと、ブロックデバイスだったらマウントする。

マウントに失敗した場合とかマウント後に何か処理をしたいような場合にシェルを立ち上げる処理が入るけどそこは飛ばして、最後にルートファイルシステムの切り替え。

exec env -i \
    "TERM=$TERM" \
    /usr/bin/switch_root /new_root $init "$@"

switch_rootはutil-linuxにあるコマンド。$initはinitスクリプトの最初のほうで以下のように定義済み。

init=/sbin/init

/sbin/initは新しい/では以下のようにsystemdを指す。

masami@saga:~/tmp/initramfs$ ls -la /sbin/init
lrwxrwxrwx 1 root root 22 Dec 15 22:56 /sbin/init -> ../lib/systemd/systemd

これでext4_file_open()のif文内実行時のプロセスがswitch_rootだった理由が見えたので次はオープンしようとしたファイルがsystemdだったのはなんでかというのを見てみる。
これにはutli-linuxのswitch_root.cを見る必要がある。

int main(int argc, char *argv[])
{
        char *newroot, *init, **initargs;
        atexit(close_stdout);

        if (argv[1] && (!strcmp(argv[1], "--help") || !strcmp(argv[1], "-h")))
                usage(stdout);
        if (argv[1] && (!strcmp(argv[1], "--version") || !strcmp(argv[1], "-V"))) {
                printf(UTIL_LINUX_VERSION);
                return EXIT_SUCCESS;
        }
        if (argc < 3)
                usage(stderr);

        newroot = argv[1];
        init = argv[2];
        initargs = &argv[2];

        if (!*newroot || !*init)
                usage(stderr);

        if (switchroot(newroot))
                errx(EXIT_FAILURE, _("failed. Sorry."));

        if (access(init, X_OK))
                warn(_("cannot access %s"), init);

        execv(init, initargs);
        err(EXIT_FAILURE, _("failed to execute %s"), init);
}

なるほど、execv()で/sbin/initをオープンしているからですね。

ついでの実験。ext4_file_open()に以下を追加。

+       if (!strcmp("a.out", current->comm))
+               pr_info("[-]: %s\n", filp->f_path.dentry->d_name.name);

これをコンパイルして実行。

#include <unistd.h>

int
main(int argc, char **argv)
{
        char *path = "/bin/ls";
        char *av[] = { "ls", "/", NULL };

        execve(path, av, NULL);

        return 0;
}

dmesgで確認。

[   13.647315] [-]: ld.so.cache
[   13.647336] [-]: libc-2.18.so
[   13.647512] [-]: ls
[   13.647708] [-]: ld-2.18.so

このa.outの実行時に開かれるファイルは4ファイル。

Linuxカーネル Hacks ―パフォーマンス改善、開発効率向上、省電力化のためのテクニック

Linuxカーネル Hacks ―パフォーマンス改善、開発効率向上、省電力化のためのテクニック