目錄
注冊掛載rootfs文件系統
解壓initramfs到rootfs中
prepare_namespace掛載磁盤上的文件系統
題外話:啟動引導程序怎么識別"/boot"分區(或者目錄)
參考資料
注冊掛載rootfs文件系統
首先是rootfs的注冊和掛載,rootfs作為一切后續文件操作的基石。
start_kernel vfs_caches_init mnt_init init_rootfs注冊rootfs文件系統 init_mount_tree 掛載rootfs文件系統 vfs_kern_mount mount_fs type->mount其實是rootfs_mount mount_nodev fill_super 其實是ramfs_fill_super inode = ramfs_get_inode(sb, NULL, S_IFDIR | fsi->mount_opts.mode, 0); sb->s_root = d_make_root(inode); static const struct qstr name = QSTR_INIT("/", 1);[1*] __d_alloc(root_inode->i_sb, &name); ... mnt->mnt.mnt_root = root;[2*] mnt->mnt.mnt_sb = root->d_sb;[3*] mnt->mnt_mountpoint = mnt->mnt.mnt_root;[4*] mnt->mnt_parent = mnt;[5*] root.mnt = mnt; root.dentry = mnt->mnt_root; mnt->mnt_flags |= MNT_LOCKED; set_fs_pwd(current->fs, &root); set_fs_root(current->fs, &root); ... rest_init kernel_thread(kernel_init, NULL, CLONE_FS);
在執行kernel_init之前,會建立roofs文件系統。
- [1*]處設置了根目錄的名字為“/”。
- [2*]處設置了vfsmount中的root目錄
- [3*]處設置了vfsmount中的超級塊
- [4*]處設置了vfsmount中的文件掛載點,指向了自己
- [5*]處設置了vfsmount中的父文件系統的vfsmount為自己
解壓initramfs到rootfs中
根目錄有了,接下來就可以按照傳遞給內核的參數與內核編譯選項來決定如何建立根文件系統。
內核編譯選項可以選擇是否支持initramfs,是不是指定了initramfs目錄。不管配置內核的時候是不是支持initramfs,內核要保證在__initramfs_start處放着一個initramfs文件系統。
分三種情況討論內核對initramfs的支持
- 配置內核支持initramfs,並指定了initramfs所在目錄,那么內核會把這個目錄壓縮到__initramfs_start指向的段”.init.ramfs”。此種情況在grub引導的時候不必要指定外部文件系統,此時initramfs就作為根文件系統來使用了。當然也可以指定。
- 配置內核支持initramfs,但是沒有指定initramfs所在目錄。內核會執行default_initramfs()來創建一個最小的initramfs到__initramfs_start指向的段”.init.ramfs”,包含/dev目錄、/dev/console設備節點和/root目錄。此種情況需要告訴grub外部文件系統。
- 配置內核不支持initramfs。內核會執行default_rootfs()來創建一個最小的initramfs到__initramfs_start指向的段”.init.ramfs”,包含/dev目錄、/dev/console設備節點和/root目錄。此種情況需要告訴grub外部文件系統。
如何告訴grub外部文件系統所在,initrd_start變量保存了外部文件系統的起始地址,應該是由鏈接腳本指定。
外部文件系統可以是initrd格式,也可以是cpio格式。如何處理外部文件系統,是在populate_rootfs函數中。
內核編譯的時候rootfs_initcall(populate_rootfs);會將populate_rootfs函數加入到初始化區段。會在kernel_init中被調用。
kernel_init
kernel_init_freeable
do_basic_setup
do_initcalls
populate_rootfs
populate_rootfs函數的作用就是將編譯的initramfs文件系統解壓到rootfs的根目錄中。
static int __init populate_rootfs(void) { char *err = unpack_to_rootfs(__initramfs_start, __initramfs_size);//將__initramfs_start處的文件系統解壓出來,上面說過了,內核編譯時候保證至少會有一個initramfs在此處 if (err) panic("%s", err); /* Failed to decompress INTERNAL initramfs */ if (initrd_start) {//如果配置grub時候指定了外部文件系統,grub會將外部文件數據加載到initrd_start #ifdef CONFIG_BLK_DEV_RAM//如果配置內核支持initrd格式的文件系統 int fd; printk(KERN_INFO "Trying to unpack rootfs image as initramfs...\n"); err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start);//首先還是按照initramfs格式解壓grub加載的文件系統 if (!err) { free_initrd(); goto done; } else { clean_rootfs(); unpack_to_rootfs(__initramfs_start, __initramfs_size);//如果grub加載的文件系統不是initramfs格式,那么清除rootfs中的數據,重新解壓__initramfs_start,因為目錄可能被破壞 } printk(KERN_INFO "rootfs image is not initramfs (%s)" "; looks like an initrd\n", err); fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 0700); if (fd >= 0) { ssize_t written = xwrite(fd, (char *)initrd_start, initrd_end - initrd_start);//將grub加載的文件系統寫入到/initrd.image文件中 if (written != initrd_end - initrd_start) pr_err("/initrd.image: incomplete write (%zd != %ld)\n", written, initrd_end - initrd_start); sys_close(fd); free_initrd(); } done: #else printk(KERN_INFO "Unpacking initramfs...\n"); err = unpack_to_rootfs((char *)initrd_start, initrd_end - initrd_start);//如果配置內核不支持initrd格式文件系統,那么統一按照initramfs格式解壓 if (err) printk(KERN_EMERG "Initramfs unpacking failed: %s\n", err); free_initrd(); #endif /* * Try loading default modules from initramfs. This gives * us a chance to load before device_initcalls. */ load_default_modules(); } return 0; }
此時rootfs文件系統中的基本的目錄結構已經被populate_rootfs處理好。
populate_rootfs()執行完后,
如果是initramfs和cpio-initrd的話,都已經將他們釋放到了前面初始化完成的rootfs中去了,那么根目錄下肯定會出現init文件。
而如果是image-initrd,那么只會在rootfs根目錄下出現一個initrd.image文件。
返回到kernel_init_freeable
static noinline void __init kernel_init_freeable(void) { ... do_basic_setup(); ... if (!ramdisk_execute_command)//如果沒有指定內核啟動參數rdinit ramdisk_execute_command = "/init";//內核默認最開始執行的腳本是init腳本 if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) { ramdisk_execute_command = NULL;//如果內核沒有在此時的rootfs的根目錄下發現init文件,就會執行prepare_namespace函數,說明grub加載的initrd格式的文件系統 prepare_namespace();//函數的主要功能就是進一步檢查是不是舊的塊設備的initrd。 }
假設此時正確加載了根文件系統,返回到kernel_init
static int __ref kernel_init(void *unused) { ... kernel_init_freeable(); ... if (ramdisk_execute_command) {//如果在kernel_init_freeable函數中找到init腳本,那么就執行這個腳本,由參數rdinit指定 ret = run_init_process(ramdisk_execute_command); if (!ret) return 0; pr_err("Failed to execute %s (error %d)\n", ramdisk_execute_command, ret); } /* * We try each of these until one succeeds. * * The Bourne shell can be used instead of init if we are * trying to recover a really broken machine. */ if (execute_command) {//由參數init指定 ret = run_init_process(execute_command); if (!ret) return 0; panic("Requested init %s failed (error %d).", execute_command, ret); } if (!try_to_run_init_process("/sbin/init") ||//上面都沒有找到的話,依次嘗試幾個目錄下的啟動腳本 !try_to_run_init_process("/etc/init") || !try_to_run_init_process("/bin/init") || !try_to_run_init_process("/bin/sh")) return 0; panic("No working init found. Try passing init= option to kernel. " "See Linux Documentation/init.txt for guidance."); }
prepare_namespace掛載磁盤上的文件系統
來看看prepare_namespace函數如何進一步檢查是不是舊的塊設備的initrd。
void __init prepare_namespace(void) { ... if (saved_root_name[0]) { root_device_name = saved_root_name;//這個是root參數指定。 if (!strncmp(root_device_name, "mtd", 3) || !strncmp(root_device_name, "ubi", 3)) { mount_block_root(root_device_name, root_mountflags); goto out; } ROOT_DEV = name_to_dev_t(root_device_name);//通過指定的根文件系統所在設備匹配出ROOT_DEV號。此時sysfs文件系統已經建立了,各個硬件設備已經被掃描過並在sysfs下建立對應的層次結構了。 if (strncmp(root_device_name, "/dev/", 5) == 0) root_device_name += 5; } if (initrd_load())//加載老式塊設備的initrd,這里也分兩種情況,一種是將initrd作為真實文件系統返回0,也就是ram0作為根設備;另外一種就是作為一種過渡的文件系統,加載了硬盤上的文件系統,返回1。 goto out; ... out: devtmpfs_mount("dev"); sys_mount(".", "/", NULL, MS_MOVE, NULL);//將當前目錄“/root"上的vfsmount(該結構體此時代表硬盤上實際文件系統)掛載到“/”目錄,又是很神奇的一步 sys_chroot(".");
找到ROOT_DEV號之后,initrd_load接着處理掛載事務。
int __init initrd_load(void) { if (mount_initrd) {//如果指定了noinitrd參數,mount_initrd才會等於0 create_dev("/dev/ram", Root_RAM0);//創建一個/dev/ram0設備節點 /* * Load the initrd data into /dev/ram0. Execute it as initrd * unless /dev/ram0 is supposed to be our actual root device, * in that case the ram disk is just set up here, and gets * mounted in the normal path. */ if (rd_load_image("/initrd.image") && ROOT_DEV != Root_RAM0) {//rd_load_image函數將/initrd.image文件寫入/dev/ram0中 sys_unlink("/initrd.image"); handle_initrd();//如果grub配置文件中指定的根設備不是Root_RAM0就調用handle_initrd處理 return 1;//表示image-initrd 作為中間過渡的文件系統 } }
//如果你在bootloader里配置了root=/dev/ramx,則實際上真正的根設備就是這個initrd了,所以就不把它作為initrd處理 ,而是作為真實文件系統來處理。 sys_unlink("/initrd.image"); return 0;//image-initrd作為真實的文件系統 }
rd_load_image函數主要流程,就是將/initrd.image寫入/dev/ram0
int __init rd_load_image(char *from) { ... out_fd = sys_open("/dev/ram", O_RDWR, 0); if (out_fd < 0) goto out; in_fd = sys_open(from, O_RDONLY, 0); if (in_fd < 0) goto noclose_input; ... sys_read(in_fd, buf, BLOCK_SIZE); sys_write(out_fd, buf, BLOCK_SIZE);
實際處理落入handle_initrd
static void __init handle_initrd(void){ struct subprocess_info *info; static char *argv[] = { "linuxrc", NULL, }; extern char *envp_init[]; int error; real_root_dev = new_encode_dev(ROOT_DEV); create_dev("/dev/root.old", Root_RAM0);//以相同的設備號建立一個設備節點,其實還是/dev/ram0,還是/initrd.image,還是grub加載的外部文件系統 /* mount initrd on rootfs' /root */ mount_block_root("/dev/root.old", root_mountflags & ~MS_RDONLY);//將/dev/root.old掛載到/root上,並且切換當前目錄到/root上 sys_mkdir("/old", 0700); sys_chdir("/old");//切換當前目錄到/old /* try loading default modules from initrd */ load_default_modules(); /* * In case that a resume from disk is carried out by linuxrc or one of * its children, we need to tell the freezer not to wait for us. */ current->flags |= PF_FREEZER_SKIP; info = call_usermodehelper_setup("/linuxrc", argv, envp_init,//linuxrc腳本用來真正做一些初始化工作 GFP_KERNEL, init_linuxrc, NULL, NULL);//創建一個工作隊列,用來調用執行/linuxrc腳本,init_linuxrc函數會將根目錄切換到/root上,這個時候,會將“/root”掛載到“/”上,也就是“/dev/ram0”的vfsmount結構體掛到“rottfs”的vfsmount if (!info) return; call_usermodehelper_exec(info, UMH_WAIT_PROC);//等待linuxrc完成 current->flags &= ~PF_FREEZER_SKIP; /* move initrd to rootfs' /old *///不得不說vfsmount的設計讓下面兩步顯得有點魔幻 sys_mount("..", ".", NULL, MS_MOVE, NULL);//“..”是“/”目錄,這個時候的根目錄掛載的是initrd的文件系統,也就是“/dev/ram0”文件系統的vfsmount,將該vfsmount的掛載點設置到/old目錄 /* switch root and cwd back to / of rootfs */ sys_chroot("..");//由於上一步將initrd的vfsmount掛載到/old上了,此時,“/”上掛載的是rootfs的vfsmount,將current的root指向rootfs的根目錄 if (new_decode_dev(real_root_dev) == Root_RAM0) { sys_chdir("/old");//如果initrd作為真實文件系統,切換到old目錄,直接返回 return; } sys_chdir("/"); ROOT_DEV = new_decode_dev(real_root_dev); mount_root();//創建/dev/root,將/dev/root掛載到/root上,並且將當前目前切換到/root上 printk(KERN_NOTICE "Trying to move old root to /initrd ... "); error = sys_mount("/old", "/root/initrd", NULL, MS_MOVE, NULL);//將/old掛載到現在root下的initrd,如果root目錄下沒有initrd目錄則釋放/old 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"); } }
mount_root創建/dev/root節點,掛載到/root目錄上。
void __init mount_root(void) { ... #ifdef CONFIG_BLOCK { int err = create_dev("/dev/root", ROOT_DEV); if (err < 0) pr_emerg("Failed to create /dev/root: %d\n", err); mount_block_root("/dev/root", root_mountflags); } #endif }
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); ... }
static int __init do_mount_root(char *name, char *fs, int flags, void *data) { struct super_block *s; int err = sys_mount(name, "/root", fs, flags, data); if (err) return err; sys_chdir("/root"); s = current->fs->pwd.dentry->d_sb; ROOT_DEV = s->s_dev; printk(KERN_INFO "VFS: Mounted root (%s filesystem)%s on device %u:%u.\n", s->s_type->name, s->s_flags & MS_RDONLY ? " readonly" : "", MAJOR(ROOT_DEV), MINOR(ROOT_DEV)); return 0; }
題外話:啟動引導程序怎么識別"/boot"分區(或者目錄)
分區是怎么識別的呢?為什么計算機就知道這個分區叫做boot呢?
我先開始疑惑的是,到底什么是根文件系統?這個根文件系統的根掛載在哪里?
什么又是真實文件系統,中間為什么又加入一層initrd?
這個initrd又是如何被識別的?
我試着結合實踐過程中的收獲來梳理一下:
分區的信息都存放在MBR里面,記錄着起始扇區號和扇區數。MBR里面的啟動引導程序會根據當初安裝系統是指定的分區信息尋找所謂的boot分區。當初安裝系統的時候,我們手工指定的一塊硬盤空間的起始扇區號和扇區大小的信息肯定被初始化為引導程序中的某些變量。引導程序執行的時候就根據這些信息通過BIOS中斷將boot所在的扇區加載進來。為什么叫做boot呢,我感覺叫做toob也沒關系,主要是方便用戶辨別。
內核的加載不僅依靠代碼來完成,還要硬盤上的數據按照指定的格式來存放,這樣引導程序的代碼才能按照一定順序來讀取硬盤上的數據來計算或者比較。這也體現了文件系統的動靜兩面的特性。不能認為只有內存中運行的代碼才叫文件系統,硬盤中按照一定格式躺着的數據也叫文件系統。
根文件系統就是一個叫做rootfs的文件系統,之后的文件操作都是依附在rootfs下。rootfs的根並沒有掛載在其它文件系統上,掛載點指向自己目錄結構體。
這個真實文件系統就是配置引導程序時候指定的root參數。這個參數指定了最后作為init進程(0號進程)的根文件系統所在的分區。
中間為什么又加入一層initrd呢?就是為了減少內核體積,使得設計內核時候不用考慮多種硬件如何識別。
這個initrd如何被識別呢?應該是通過鏈接腳本來指定加載地址initrd_start,然后內核將initrd_start處的壓縮包解開。
參考資料
[1]從linux啟動到rootfs的掛載分析
http://blog.csdn.net/kevin_hcy/article/details/17663341
[2]解析 Linux 中的 VFS 文件系統機制
http://www.ibm.com/developerworks/cn/linux/l-vfs/index.html
[3]linux內核4.4版本
[4]linux2.6內核initrd機制解析
https://blog.csdn.net/lizhiguo0532/article/details/5946690