根文件系统挂载过程
目录
注册挂载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机制解析