以 Linux-2.6.25 的 kernel 爲例,分析一下 Linux 啓動過程中 initrd 的流程。
1. 先從 Makefile說起
下面是內核代碼中 init/Makefile 文件的一段內容:
obj-y := main.o version.o mounts.o
ifneq ($(CONFIG_BLK_DEV_INITRD),y)
obj-y += noinitramfs.o
else
obj-$(CONFIG_BLK_DEV_INITRD) += initramfs.o
endif
obj-$(CONFIG_GENERIC_CALIBRATE_DELAY) += calibrate.o
mounts-y := do_mounts.o
mounts-$(CONFIG_BLK_DEV_RAM) += do_mounts_rd.o
mounts-$(CONFIG_BLK_DEV_INITRD) += do_mounts_initrd.o
mounts-$(CONFIG_BLK_DEV_MD) += do_mounts_md.o
內核中和 initrd 相關的代碼主要放在 init 目錄下,包括 main.c,noinitramfs.c ,initramfs.c ,do_mounts.c ,do_mounts_initrd.c ,do_mounts_rd.c 和 do_mounts_md.c 。
從 Makefile 中可以看出,noinitramfs.c 是在內核不支持 initrd 的情況下被編譯進內核,而 initramfs.c 正好相反,它處理(cpio包類型的)的 initrd 。do_mounts.c 主要是負責掛載根文件系統的,所以總是被編譯。do_mounts_initrd.c 負責調用掛載和處理(ramdisk類型的)的 initrd 。do_mounts_rd.c 是具體實現如何掛載(ramdisk類型的)的 initrd 。do_mount_md.c 處理和 RAID 有關的一些情況。
2. cpio 包類型的 initrd 的處理
內核在初始化啓動的時候會先註冊一個叫作 rootfs 的文件系統,然後通過 rootfs_initcall 來生成其中的內容。根據內核是否支持 initrd 和 ramdisk ,rootfs 的生成方法和內容都會有所不同。當內核不支持 initrd 時,rootfs_initcall 調用 noinitramfs.c 中的 default_rootfs() 函數。default_rootfs() 主要往 rootfs 中生成兩個目錄 /dev 和 /root 以及一個設備文件 /dev/console 。下面是default_rootfs()
精簡過的流程:
static int __init default_rootfs(void)
{
sys_mkdir("/dev", 0755);
sys_mknod((const char __user *) "/dev/console",
S_IFCHR | S_IRUSR | S_IWUSR,
new_encode_dev(MKDEV(5, 1)));
sys_mkdir("/root", 0700);
return 0;
}
rootfs_initcall(default_rootfs);
在調用rootfs_initcall()之前已經通過下面的調用過程建立了rootfs文件系統:
kernel_entry()->vfs_caches_init()->mnt_init()->init_rootfs()->init_mount_tree()->...
似乎在rootfs的init文件必須位於根目錄下,即/init,否則系統會嘗試去mount其它的文件系統,例如ram0,mtdblock等。
當內核支持 initrd 時,rootfs_initcall 調用 initramfs.c 中的 populate_rootfs() 函數來填充 rootfs。下面先看一下 populate_rootfs() 精簡過的主要流程:
static int __init populate_rootfs(void)
{
char *err = unpack_to_rootfs(__initramfs_start, __initramfs_end - __initramfs_start, 0);
if (initrd_start) {
#ifdef CONFIG_BLK_DEV_RAM
err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start, 1);
if (!err) {
unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start, 0);
free_initrd();
return 0;
}
fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 0700);
if (fd >= 0) {
sys_write(fd, (char *)initrd_start, initrd_end - initrd_start);
sys_close(fd);
free_initrd();
}
#else
err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start, 0);
if (err)
panic(err);
free_initrd();
#endif
}
return 0;
}
rootfs_initcall(populate_rootfs);
關於 initramfs (也就是內核中自帶的 cpio 包)需要解釋的是:如果內核支持 initrd,但並沒有配置 CONFIG_INITRAMFS_SOURCE 選項的話,內核在編譯的時候會自動生成一個最小的 cpio 包附在內核中。這個內核自帶的 cpio 包的內容與由 default_rootfs() 生成的一樣。具體可參見編譯後的內核源碼樹中的 usr/initramfs_data.cpio.gz 文件。
至此,內核對(cpio包格式的)initrd 的處理流程就結束了。如果在 populate_rootfs() 中成功地 unpack_to_rootfs() 的話,之後內核就不會再對 initrd 作任何操作,也不會去掛載根文件系統,所有的工作都留給 cpio 包(也就是rootfs)中的 /init 去完成了。關於這一點,在後面分析 main.c 中的流程中會看到。
3. 如果沒有使用 initrd
下面先考慮一下內核在沒有使用 initrd 的情況下掛載根文件系統的流程。這又分內核沒有編譯支持 initrd 或 內核支持 initrd 但系統引導時沒有提供 initrd 文件兩種情況。但這兩種情況其結果其實是一樣的,根據前面的分析,兩種情況間的差別只是在生成 rootfs 的方式不一樣,一個是通過 default_rootfs() ,另一個是調用 unpack_to_rootfs() ,而且其產生內容是一樣的(只要沒有配置 CONFIG_INITRAMFS_SOURCE)。
如果沒有使用到 cpio 類型的 initrd,內核會執行 prepare_namespace() 函數(關於這個函數在內核啓動過程中的位置,後面會有講到)。prepare_namespace() 在 do_mounts.c 中定義,它主要負責掛載根文件系統和 ramdisk 類型的 initrd (如果需要的話)。下面看一下它精簡過的大致流程:
void __init prepare_namespace(void)
{
if (saved_root_name[0]) {
root_device_name = saved_root_name;
ROOT_DEV = name_to_dev_t(root_device_name);
if (strncmp(root_device_name, "/dev/", 5) == 0)
root_device_name += 5;
}
if (initrd_load())
goto out;
mount_root();
out:
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
}
變量 saved_root_name 的值是內核啓動的參數 "root=" 後的值,這個是由 __setup() 宏提取的。
下面看一下 mount_root() 函數精簡過的流程:
void __init mount_root(void)
{
#ifdef CONFIG_BLOCK
create_dev("/dev/root", ROOT_DEV);
mount_block_root("/dev/root", root_mountflags);
#endif
}
這裏在 rootfs 中新建了一個 /dev/root 設備文件,這個設備文件一般就是指內核啓動參數指定的包含根文件系統的設備。在 rootfs 中,這個設備文件被命名爲 /dev/root 。
再往下看 mount_block_root() 的精簡流程:
void __init mount_block_root(char *name, int flags)
{
get_fs_names(fs_names);
retry:
for (p = fs_names; *p; p += strlen(p)+1) {
int err = do_mount_root(name, p, flags, root_mount_data);
switch (err) {
... ...
}
... ...
}
... ...
}
所以在這個函數中,最主要的是調用了 do_mount_root() 來掛載根文件系統。
最後來看一下 do_mount_root() 函數的實現:
static int __init do_mount_root(char *name, char *fs, int flags, void *data)
{
int err = sys_mount(name, "/root", fs, flags, data);
if (err)
return err;
sys_chdir("/root");
ROOT_DEV = current->fs->pwd.mnt->mnt_sb->s_dev;
... ...
return 0;
}
do_mount_root() 嘗試把參數 name 指定的設備文件掛載到 /root 目錄中去,並 cd 到新的根文件系統的根目錄中去。
至此,如果一切順利的話,我們已經成功地把包含根文件系統的設備掛載到了 rootfs 中的 /root 目錄上去了。我們的調用流程是
prepare_namespace() --> mount_root() --> mount_block_root() --> do_mount_root() 。
回到 prepare_namespace() 中去,在順利 mount_root() 完之後,還有兩步要做:
out:
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
}
由於之前已經切換到了新的根文件系統的根目錄中去,所以這兩步的作用是用新的根文件系統的根目錄替換 rootfs ,使其成爲 Linux VFS 的根目錄。
至此,我們已經走完了 prepare_namespace() 在不使用 initrd 情況下的流程,根文件系統設備也已經掛載上了,且替代了 rootfs 成爲了 VFS 的根,剩下的任務就留給了根文件系統中的 /sbin/init 程序了。
4. ramdisk 類型的 initrd 的處理
從前面的分析可以看出,不管內核是否使用了 initrd ,只要進入了 prepare_namespace() , 都會去調用 initrd_load() 函數。下面看一下 initrd_load() 流程:
int __init initrd_load(void)
{
if (mount_initrd) {
create_dev("/dev/ram", Root_RAM0);
if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {
sys_unlink("/initrd.image");
handle_initrd();
return 1;
}
}
sys_unlink("/initrd.image");
return 0;
}
變量 mount_initrd 是是否要加載 initrd 的標誌,默認爲1,當內核啓動參數中包含 noinitrd 字符串時,mount_initrd 會被設爲0 。接着爲了把 initrd 釋放到內存盤中,先需要創建設備文件,然後通過 rd_load_image 把之前保存的 /initrd.image 加載到內存盤中。之後判斷如果內核啓動參數中指定的最終的根文件系統不是內存盤的話,那就先要執行 initrd 中的 linuxrc;如果最終的根文件系統就是剛加載到內存盤的 initrd 的話,那就先不處理它,留到之後當真正的根文件系統處理。
需要補充的是,只要沒有用到 cpio 類型的 initrd,那內核都會執行到 rd_load_image("/initrd.image"),無論是否真的提供了 initrd 。如果沒有提供 initrd,那 /initrd.image 自然不會存在,rd_load_image() 也會提早結束。另外 /dev/ram 這個設備節點文件在 rd_load_image() 中用完之後總會被刪除(但相應的內存盤中的內容還在)。
下面看一下 handle_initrd() 的主要流程:
static void __init handle_initrd(void)
{
int error;
int pid;
real_root_dev = new_encode_dev(ROOT_DEV);
create_dev("/dev/root.old", Root_RAM0);
mount_block_root("/dev/root.old", root_mountflags & ~MS_RDONLY);
sys_mkdir("/old", 0700);
root_fd = sys_open("/", 0, 0);
old_fd = sys_open("/old", 0, 0);
sys_chdir("/root");
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
pid = kernel_thread(do_linuxrc, "/linuxrc", SIGCHLD);
if (pid > 0)
while (pid != sys_wait4(-1, NULL, 0, NULL))
yield();
sys_fchdir(old_fd);
sys_mount("/", ".", NULL, MS_MOVE, NULL);
sys_fchdir(root_fd);
sys_chroot(".");
sys_close(old_fd);
sys_close(root_fd);
if (new_decode_dev(real_root_dev) == Root_RAM0) {
sys_chdir("/old");
return;
}
ROOT_DEV = new_decode_dev(real_root_dev);
mount_root();
printk(KERN_NOTICE "Trying to move old root to /initrd ... ");
error = sys_mount("/old", "/root/initrd", NULL, MS_MOVE, NULL);
if (!error)
printk("okay\n");
else {
int fd = sys_open("/dev/root.old", O_RDWR, 0);
if (error == -ENOENT)
printk("/initrd does not exist. Ignored.\n");
else
printk("failed\n");
printk(KERN_NOTICE "Unmounting old root\n");
sys_umount("/old", MNT_DETACH);
printk(KERN_NOTICE "Trying to free ramdisk memory ... ");
if (fd < 0) {
error = fd;
} else {
error = sys_ioctl(fd, BLKFLSBUF, 0);
sys_close(fd);
}
printk(!error ? "okay\n" : "failed\n");
}
}
在這個函數中,用到了一個 real_root_dev 變量,它是一個 int 型的全局變量,它的值從變量 ROOT_DEV 轉換過來。變量 real_root_dev 是和文件 /proc/sys/kernel/real-root-dev 相關聯的(參見 kernel/sysctl.c 第405行左右),所以如果在執行 initrd 中的 /linuxrc 時改了 /proc/sys/kernel/real-root-dev 的話,就相當於又重新指定了真正的根文件系統。之所以要新弄一個 real_root_dev
變量,使因爲 procfs 不支持改 kdev_t 型的 ROOT_DEV 變量。
另外,在 rootfs 中會建有兩個設備文件節點:/dev/root 是真正的根文件系統的設備節點,其設備號是 ROOT_DEV 的值;/dev/root.old 是 ramdisk 型的 initrd 的設備節點,其設備號總是 Root_RAM0 。
下面看一下 do_linuxrc() 的實現:
static int __init do_linuxrc(void * shell)
{
static char *argv[] = { "linuxrc", NULL, };
extern char * envp_init[];
sys_close(old_fd);sys_close(root_fd);
sys_close(0);sys_close(1);sys_close(2);
sys_setsid();
(void) sys_open("/dev/console",O_RDWR,0);
(void) sys_dup(0);
(void) sys_dup(0);
return kernel_execve(shell, argv, envp_init);
}
這樣我們就走完了整個 ramdisk 類型的 initrd 的處理流程了,如果執行了其中的 /linuxrc 文件的話,當我們退回到 prepare_namespace() 函數時就會直接跳到 out: 標籤處,剩下還有兩行代碼要執行:
out:
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
}
這兩步前一節已經分析過了,就是把當前目錄替換 rootfs ,使其成爲 Linux VFS 的根目錄。由於之前無論是用內存盤還是指定的新設備作爲我們的真正的根文件系統,我們都會 cd 進去,所以這裏就很好理解了。
另外,補充一點的是,通過比較 cpio 包和 ramdisk 類型的initrd,我們可以發現 cpio 包裏的東西是直接解壓到 rootfs 中的,成爲 rootfs 的一部分,而 ramdisk 的 initrd 是以 ramdisk 形式存在的,需要額外掛載到 rootfs 上才能使用,兩者有很大的區別。
5. 總的流程
最後我們看一下內核啓動過程中和 initrd 有關的一個總的流程順序。
內核初始化從 start_kernel() 函數開始,start_kernel() 最後會調 rest_init() 函數。在 rest_init() 函數中,第一步就會通過調用 kernel_thread(kernel_init, ...) 生成一個內核線程來執行 kernel_init() 函數。注意,這個 kernel_init() 線程就是後來的 init 進程(1號進程)的前身,而原來的 rest_init() 函數在最後調用 cpu_idle() 後就變成 swap 進程(0號進程)了。
接着,我們從 kernel_init() 函數線程繼續研究,下面列出了 kernel_init() 函數中和 initrd 相關的的主要流程:
static int __init kernel_init(void * unused)
{
... ...
do_basic_setup();
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
init_post();
return 0;
}
在 kernel_init() 中會調用 do_basic_setup(),而 do_basic_setup() 又會調用 do_initcalls(),所以 cpio 包類型的 initrd (如果有的話)就會在此時被填充到 rootfs 中去。接下來初始化 ramdisk_execute_command 變量,這個變量表示在 cpio 包中要被執行的第一個程序,可通過在內核啓動參數中給 rdinit= 賦值來指定。接下來檢查在 rootfs 中是否存在變量 ramdisk_execute_command
所指的文件。如果有,就說明 cpio 包類型的 initrd 成功加載了,那就不需要內核再調用 prepare_namespace() 來掛載根文件系統了,這些都留給 cpio 包裏的 ramdisk_execute_command 所指的程序去完成了;如果沒有,就說明內核並沒有成功用上 cpio 包類型的 initrd,還需要調用 prepare_namespace() 來繼續準備加載根文件系統,並清空變量ramdisk_execute_command。無論怎樣,內核都會繼續執行 init_post()。
init_post() 是內核初始化的終點了:把 kernel_init() 內核線程 execve 成用戶態的 init 進程。下面是它的主要流程:
static int noinline init_post(void)
{
... ...
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
(void) sys_dup(0);
(void) sys_dup(0);
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s\n", ramdisk_execute_command);
}
if (execute_command) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults...\n", execute_command);
}
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
}