在Unix的世界中,file system佔據一個極重要的抽象化地位。其中,/ 所代表的rootfs更是所有後續新增file system所必須依賴前提條件。以Linux為例,黑客Jserv就曾經詳細說明過initramfs的背後設計考量。本篇文章不再重複背景知識,主要將追蹤rootfs初始化的流程作點整理,免得自己日後忘記。 :-)
file system與特定CPU架構無關,所以我觀察的起點從init/main.c的start_kernel()開始,這是Linux作完基本CPU初始化後首先跳進的C function(我閱讀的版本為3.12)。跟root file system有關的流程羅列如下:
start_kernel()
-> vfs_caches_init_early()
-> vfs_caches_init()
-> mnt_init()
-> init_rootfs()
-> init_mount_tree()
-> rest_init()
-> kernel_thread(kernel_init,...)
其中比較重要的是mnt_int()中的init_rootfs()與init_mout_tree()。init_rootfs()實作如下:
int __init init_rootfs(void)
{
int err = register_filesystem(&rootfs_fs_type);
if (err)
return err;
if (IS_ENABLED(CONFIG_TMPFS) && !saved_root_name[0] &&
(!root_fs_names || strstr(root_fs_names, "tmpfs"))) {
err = shmem_init();
is_tmpfs = true;
} else {
err = init_ramfs_fs();
}
if (err)
unregister_filesystem(&rootfs_fs_type);
return err;
}
我們可以看到,Linux kernel總是會在開機時先行註冊一個rootfs_fs_tyype作為第一階段的rootfs,這個rootfs要麼是tmpfs,不然就是ramfs,端看tmpfs有無開啟以及使用者是否明白指定boot option去使用tmpfs,否則預設此第一階段就是用ramfs。rootfs_fs_type 變數定義如下:
static struct file_system_type rootfs_fs_type = {
.name = "rootfs",
.mount = rootfs_mount,
.kill_sb = kill_litter_super,
};
接著我們就會看到在init_mount_tree()掛載rootfs時,便是使用"rootfs"去尋找對應的file system:
static void __init init_mount_tree(void)
{
struct vfsmount *mnt;
struct mnt_namespace *ns;
struct path root;
struct file_system_type *type;
type = get_fs_type("rootfs");
if (!type)
panic("Can't find rootfs type");
mnt = vfs_kern_mount(type, 0, "rootfs", NULL);
put_filesystem(type);
if (IS_ERR(mnt))
panic("Can't create rootfs");
ns = create_mnt_ns(mnt);
if (IS_ERR(ns))
panic("Can't allocate initial namespace");
init_task.nsproxy->mnt_ns = ns;
get_mnt_ns(ns);
root.mnt = mnt;
root.dentry = mnt->mnt_root;
set_fs_pwd(current->fs, &root);
set_fs_root(current->fs, &root);
}
init_mount_tree()執行完後,rootfs的相關資料結構就已經掛在current上了,但此時該file system尚無任何內容,會在接下去的動作中將rootfs的內容寫入。
kernel_init()
-> kernel_init_freeable()
-> do_basic_setup()
-> rootfs_initcall
-> populate_rootfs()
-> unpack_to_rootfs()
-> ramdisk_execute_command = "/init"
-> sys_access( ramdisk_execute_command )
-> if sys_access fails, do prepare_namespace()
-> run_init_process()
rootfs的內容如何倒進RAM中呢?關鍵處在do_basic_setup()裡,實作如下:
static void __init do_basic_setup(void)
{
cpuset_init_smp();
usermodehelper_init();
shmem_init();
driver_init();
init_irq_proc();
do_ctors();
usermodehelper_enable();
do_initcalls();
random_int_secret_init();
}
其中的do_initcalls()會將註冊進對應init level的所有init functions依序喚起,其中就包含了init/initramfs.c中的populate_rootfs():
static int __init populate_rootfs(void)
{
char *err = unpack_to_rootfs(__initramfs_start, __initramfs_size);
if (err)
panic(err); /* Failed to decompress INTERNAL initramfs */
...
return 0;
}
rootfs_initcall(populate_rootfs);
如果對這種透過ELF的section儲存function pointer的技法不熟悉的話,可以參考這篇文章。
populate_rootfs()中的unpack_to_rootfs()會將initramfs_start開頭,長度為__initramfs_size bytes的rootfs image根據壓縮格式解壓後,將initramfs image(cpio format)的檔案、目錄以及link動態地在rootfs上建立起來。至此,使用者自建的rootfs就在kernel中可見了。接下來的動作就較為單純,主要就是判斷rootfs中是否存在/init,若存在,就會喚起/init,接著控制權就完全交由user space了,若不存在/init,那就會再判斷是否有從boot option傳入要請求kernel直接掛載rootfs,可能是透過NFS或一般的block device的方式去掛載。
從這段流程我們可以發現,Linux的rootfs初始化流程頗為曲折,然而這些流程的分枝往往都是在面對現實需求的不斷加入後,逐漸修繕而來。無論是利用init section作模組化或用intramfs簡化kernel掛載rootfs的實作複雜度,都是本著同樣的精神在不斷地refine。透過閱讀經典程式碼,讓自己在心裡層面感染一點偉大的黑客心靈,也算為煩躁的職場生涯添點樂趣。 :-)
留言
張貼留言