以 Linux-2.6.25 的 kernel 為例,分析一下 Linux 啟動過程中 initrd 的流程。
1. 先從 Makefile說起
下面是內(nèi)核代碼中 init/Makefile 文件的一段內(nèi)容:
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
內(nèi)核中和 initrd 相關(guān)的代碼主要放在 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 是在內(nèi)核不支持 initrd 的情況下被編譯進內(nèi)核,而initramfs.c 正好相反,它處理(cpio包類型的)的 initrd 。do_mounts.c主要是負責掛載根文件系統(tǒng)的,所以總是被編譯。do_mounts_initrd.c 負責調(diào)用掛載和處理(ramdisk類型的)的initrd 。do_mounts_rd.c 是具體實現(xiàn)如何掛載(ramdisk類型的)的 initrd 。do_mount_md.c處理和 RAID 有關(guān)的一些情況。
2. cpio 包類型的 initrd 的處理
內(nèi)核在初始化啟動的時候會先注冊一個叫作 rootfs 的文件系統(tǒng),然后通過 rootfs_initcall來生成其中的內(nèi)容。根據(jù)內(nèi)核是否支持 initrd 和 ramdisk ,rootfs 的生成方法和內(nèi)容都會有所不同。當內(nèi)核不支持initrd 時,rootfs_initcall 調(diào)用 noinitramfs.c 中的 default_rootfs()函數(shù)。default_rootfs() 主要往 rootfs 中生成兩個目錄 /dev 和 /root 以及一個設(shè)備文件/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);
在調(diào)用rootfs_initcall()之前已經(jīng)通過下面的調(diào)用過程建立了rootfs文件系統(tǒng):
kernel_entry()->vfs_caches_init()->mnt_init()->init_rootfs()->init_mount_tree()->...
似乎在rootfs的init文件必須位于根目錄下,即/init,否則系統(tǒng)會嘗試去mount其它的文件系統(tǒng),例如ram0,mtdblock等。
當內(nèi)核支持 initrd 時,rootfs_initcall 調(diào)用 initramfs.c 中的populate_rootfs() 函數(shù)來填充 rootfs。下面先看一下 populate_rootfs()精簡過的主要流程:
static int __init populate_rootfs(void)
{
char *err = unpack_to_rootfs(__initramfs_start, __initramfs_end -__initramfs_start, 0);
if (initrd_start){
#ifdefCONFIG_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);
關(guān)于 initramfs (也就是內(nèi)核中自帶的 cpio 包)需要解釋的是:如果內(nèi)核支持 initrd,但并沒有配置CONFIG_INITRAMFS_SOURCE 選項的話,內(nèi)核在編譯的時候會自動生成一個最小的 cpio 包附在內(nèi)核中。這個內(nèi)核自帶的cpio 包的內(nèi)容與由 default_rootfs() 生成的一樣。具體可參見編譯后的內(nèi)核源碼樹中的usr/initramfs_data.cpio.gz 文件。
至此,內(nèi)核對(cpio包格式的)initrd 的處理流程就結(jié)束了。如果在 populate_rootfs() 中成功地unpack_to_rootfs() 的話,之后內(nèi)核就不會再對 initrd 作任何操作,也不會去掛載根文件系統(tǒng),所有的工作都留給cpio 包(也就是rootfs)中的 /init 去完成了。關(guān)于這一點,在后面分析 main.c 中的流程中會看到。
3. 如果沒有使用 initrd
下面先考慮一下內(nèi)核在沒有使用 initrd 的情況下掛載根文件系統(tǒng)的流程。這又分內(nèi)核沒有編譯支持 initrd 或 內(nèi)核支持initrd 但系統(tǒng)引導時沒有提供 initrd文件兩種情況。但這兩種情況其結(jié)果其實是一樣的,根據(jù)前面的分析,兩種情況間的差別只是在生成 rootfs 的方式不一樣,一個是通過default_rootfs() ,另一個是調(diào)用 unpack_to_rootfs() ,而且其產(chǎn)生內(nèi)容是一樣的(只要沒有配置CONFIG_INITRAMFS_SOURCE)。
如果沒有使用到 cpio 類型的 initrd,內(nèi)核會執(zhí)行 prepare_namespace()函數(shù)(關(guān)于這個函數(shù)在內(nèi)核啟動過程中的位置,后面會有講到)。prepare_namespace() 在 do_mounts.c中定義,它主要負責掛載根文件系統(tǒng)和 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 的值是內(nèi)核啟動的參數(shù) "root=" 后的值,這個是由 __setup()宏提取的。
下面看一下 mount_root() 函數(shù)精簡過的流程:
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 設(shè)備文件,這個設(shè)備文件一般就是指內(nèi)核啟動參數(shù)指定的包含根文件系統(tǒng)的設(shè)備。在rootfs 中,這個設(shè)備文件被命名為 /dev/root 。
再往下看 mount_block_root() 的精簡流程:
void __init mount_block_root(char *name, intflags)
{
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) {
... ...
}
... ...
}
... ...
}
所以在這個函數(shù)中,最主要的是調(diào)用了 do_mount_root() 來掛載根文件系統(tǒng)。
最后來看一下 do_mount_root() 函數(shù)的實現(xiàn):
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() 嘗試把參數(shù) name 指定的設(shè)備文件掛載到 /root 目錄中去,并 cd到新的根文件系統(tǒng)的根目錄中去。
至此,如果一切順利的話,我們已經(jīng)成功地把包含根文件系統(tǒng)的設(shè)備掛載到了 rootfs 中的 /root 目錄上去了。我們的調(diào)用流程是prepare_namespace() -->mount_root() --> mount_block_root()--> do_mount_root() 。
回到 prepare_namespace() 中去,在順利 mount_root() 完之后,還有兩步要做:
out:
sys_mount(".", "/", NULL, MS_MOVE,NULL);
sys_chroot(".");
}
由于之前已經(jīng)切換到了新的根文件系統(tǒng)的根目錄中去,所以這兩步的作用是用新的根文件系統(tǒng)的根目錄替換 rootfs ,使其成為 LinuxVFS 的根目錄。
至此,我們已經(jīng)走完了 prepare_namespace() 在不使用 initrd情況下的流程,根文件系統(tǒng)設(shè)備也已經(jīng)掛載上了,且替代了 rootfs 成為了 VFS 的根,剩下的任務就留給了根文件系統(tǒng)中的/sbin/init 程序了。
4. ramdisk 類型的 initrd的處理
從前面的分析可以看出,不管內(nèi)核是否使用了 initrd ,只要進入了 prepare_namespace() , 都會去調(diào)用initrd_load() 函數(shù)。下面看一下 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,當內(nèi)核啟動參數(shù)中包含 noinitrd字符串時,mount_initrd 會被設(shè)為0 。接著為了把 initrd 釋放到內(nèi)存盤中,先需要創(chuàng)建設(shè)備文件,然后通過rd_load_image 把之前保存的 /initrd.image 加載到內(nèi)存盤中。之后判斷如果內(nèi)核啟動參數(shù)中指定的最終的根文件系統(tǒng)不是內(nèi)存盤的話,那就先要執(zhí)行 initrd 中的linuxrc;如果最終的根文件系統(tǒng)就是剛加載到內(nèi)存盤的 initrd的話,那就先不處理它,留到之后當真正的根文件系統(tǒng)處理。
需要補充的是,只要沒有用到 cpio 類型的 initrd,那內(nèi)核都會執(zhí)行到rd_load_image("/initrd.image"),無論是否真的提供了 initrd 。如果沒有提供 initrd,那/initrd.image 自然不會存在,rd_load_image() 也會提早結(jié)束。另外 /dev/ram 這個設(shè)備節(jié)點文件在rd_load_image() 中用完之后總會被刪除(但相應的內(nèi)存盤中的內(nèi)容還在)。
下面看一下 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");
}
}
在這個函數(shù)中,用到了一個 real_root_dev 變量,它是一個 int 型的全局變量,它的值從變量 ROOT_DEV轉(zhuǎn)換過來。變量 real_root_dev 是和文件 /proc/sys/kernel/real-root-dev 相關(guān)聯(lián)的(參見kernel/sysctl.c 第405行左右),所以如果在執(zhí)行 initrd 中的 /linuxrc 時改了/proc/sys/kernel/real-root-dev 的話,就相當于又重新指定了真正的根文件系統(tǒng)。之所以要新弄一個real_root_dev 變量,使因為 procfs 不支持改 kdev_t 型的 ROOT_DEV 變量。
另外,在 rootfs 中會建有兩個設(shè)備文件節(jié)點:/dev/root 是真正的根文件系統(tǒng)的設(shè)備節(jié)點,其設(shè)備號是 ROOT_DEV的值;/dev/root.old 是 ramdisk 型的 initrd 的設(shè)備節(jié)點,其設(shè)備號總是 Root_RAM0 。
下面看一下 do_linuxrc() 的實現(xiàn):
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 的處理流程了,如果執(zhí)行了其中的 /linuxrc文件的話,當我們退回到 prepare_namespace() 函數(shù)時就會直接跳到 out:標簽處,剩下還有兩行代碼要執(zhí)行:
out:
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
}
這兩步前一節(jié)已經(jīng)分析過了,就是把當前目錄替換 rootfs ,使其成為 Linux VFS的根目錄。由于之前無論是用內(nèi)存盤還是指定的新設(shè)備作為我們的真正的根文件系統(tǒng),我們都會 cd 進去,所以這里就很好理解了。
另外,補充一點的是,通過比較 cpio 包和 ramdisk 類型的initrd,我們可以發(fā)現(xiàn) cpio 包里的東西是直接解壓到rootfs 中的,成為 rootfs 的一部分,而 ramdisk 的 initrd 是以 ramdisk形式存在的,需要額外掛載到 rootfs 上才能使用,兩者有很大的區(qū)別。
5. 總的流程
最后我們看一下內(nèi)核啟動過程中和 initrd 有關(guān)的一個總的流程順序。
內(nèi)核初始化從 start_kernel() 函數(shù)開始,start_kernel() 最后會調(diào) rest_init() 函數(shù)。在rest_init() 函數(shù)中,第一步就會通過調(diào)用 kernel_thread(kernel_init, ...)生成一個內(nèi)核線程來執(zhí)行 kernel_init() 函數(shù)。注意,這個 kernel_init() 線程就是后來的 init進程(1號進程)的前身,而原來的 rest_init() 函數(shù)在最后調(diào)用 cpu_idle() 后就變成 swap進程(0號進程)了。
接著,我們從 kernel_init() 函數(shù)線程繼續(xù)研究,下面列出了 kernel_init() 函數(shù)中和 initrd相關(guān)的的主要流程:
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() 中會調(diào)用 do_basic_setup(),而 do_basic_setup() 又會調(diào)用do_initcalls(),所以 cpio 包類型的 initrd (如果有的話)就會在此時被填充到 rootfs中去。接下來初始化 ramdisk_execute_command 變量,這個變量表示在 cpio包中要被執(zhí)行的第一個程序,可通過在內(nèi)核啟動參數(shù)中給 rdinit= 賦值來指定。接下來檢查在 rootfs 中是否存在變量ramdisk_execute_command 所指的文件。如果有,就說明 cpio 包類型的 initrd成功加載了,那就不需要內(nèi)核再調(diào)用 prepare_namespace() 來掛載根文件系統(tǒng)了,這些都留給 cpio 包里的ramdisk_execute_command 所指的程序去完成了;如果沒有,就說明內(nèi)核并沒有成功用上 cpio 包類型的initrd,還需要調(diào)用 prepare_namespace()來繼續(xù)準備加載根文件系統(tǒng),并清空變量ramdisk_execute_command。無論怎樣,內(nèi)核都會繼續(xù)執(zhí)行init_post()。
init_post() 是內(nèi)核初始化的終點了:把 kernel_init() 內(nèi)核線程 execve 成用戶態(tài)的 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 initialconsole.\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("Noinit found. Try passing init= option to kernel.");
}