Linux根目录的文件系统是如何被挂载的 . 续

继上篇文章 Linux根目录的文件系统是如何被挂载的,我们继续分析。

首先看下下面的方法:

// init/do_mounts.c
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);
                ...
        }
        ...
        mount_root();
out:
        ...
        ksys_mount(".", "/", NULL, MS_MOVE, NULL);
        ksys_chroot(".");
}

该方法中的saved_root_name变量的值是在kernel启动时,由传给kernel的root参数决定的,对应的设置方法如下:

// init/do_mounts.c
static int __init root_dev_setup(char *line)
{
  strlcpy(saved_root_name, line, sizeof(saved_root_name));
        return 1;
}

__setup("root=", root_dev_setup);

kernel启动时指定的参数可由如下命令查看:

➜  linux git:(master) cat /proc/cmdline
BOOT_IMAGE=/vmlinuz-linux root=UUID=86f6f775-c2d2-4577-9f6d-b1f2d1a13471 rw quiet
➜  linux git:(master) cat /etc/fstab
# Static information about the filesystems.
# See fstab(5) for details.

# <file system> <dir> <type> <options> <dump> <pass>
# /dev/nvme0n1p2
UUID=86f6f775-c2d2-4577-9f6d-b1f2d1a13471  /           ext4        rw,relatime  0 1

# /dev/nvme0n1p1
UUID=AF48-CA1E        /boot       vfat        rw,relatime,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,utf8,errors=remount-ro  0 2

由上可知,最终挂载到根目录的硬盘分区为 /dev/nvme0n1p2,其文件系统类型为ext4。

在prepare_namespace方法中,saved_root_name变量的值被赋值给了root_device_name变量,然后由该变量通过name_to_dev_t方法算出该硬盘分区的设备编号,并赋值给ROOT_DEV。

之后,prepare_namespace方法里又调用了mount_root方法,来挂载真正的根目录文件系统,即上面的/dev/nvme0n1p2硬盘分区中存放的ext4文件系统。

// init/do_mounts.c
void __init mount_root(void)
{
        ...
#ifdef CONFIG_BLOCK
        {
                int err = create_dev("/dev/root", ROOT_DEV);
                ...
                mount_block_root("/dev/root", root_mountflags);
        }
#endif
}

该方法中先调用create_dev方法,使/dev/root目录指向ROOT_DEV代表的设备,访问/dev/root目录等价于访问ROOT_DEV代表的设备的内容。

此时,/dev/root目录就等价于硬盘分区/dev/nvme0n1p2里的根目录。

// init/do_mounts.h
static inline int create_dev(char *name, dev_t dev)
{
        ksys_unlink(name);
        return ksys_mknod(name, S_IFBLK|0600, new_encode_dev(dev));
}

继续看下ksys_mknod方法:

// include/linux/syscalls.h
static inline long ksys_mknod(const char __user *filename, umode_t mode,
                              unsigned int dev)
{
        return do_mknodat(AT_FDCWD, filename, mode, dev);
}

继续看下do_mknodat方法:

// fs/namei.c
long do_mknodat(int dfd, const char __user *filename, umode_t mode,
                unsigned int dev)
{
        struct dentry *dentry;
        struct path path;
        ...
        dentry = user_path_create(dfd, filename, &path, lookup_flags);
        ...
        switch (mode & S_IFMT) {
                case 0: case S_IFREG:
                        ...
                case S_IFCHR: case S_IFBLK:
                        error = vfs_mknod(path.dentry->d_inode,dentry,mode,
                                        new_decode_dev(dev));
                        break;
                case S_IFIFO: case S_IFSOCK:
                        ...
        }
        ...
        return error;
}

该方法中,user_path_create方法最终的结果是初始化path变量,使其对应于/dev目录,返回值dentry对应于/dev/root中的root目录。

此时dentry的d_inode字段是null。

之后又调用了vfs_mknod方法,其中参数path.dentry->d_inode代表/dev目录。

// fs/namei.c
int vfs_mknod(struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev)
{
        error = dir->i_op->mknod(dir, dentry, mode, dev);
        ...
        return error;
}
EXPORT_SYMBOL(vfs_mknod);

该方法中的dir->i_op->mknod字段对应的方法为ramfs_mknod:

// fs/ramfs/inode.c
static int
ramfs_mknod(struct inode *dir, struct dentry *dentry, umode_t mode, dev_t dev)
{
        struct inode * inode = ramfs_get_inode(dir->i_sb, dir, mode, dev);
        ...
        if (inode) {
                d_instantiate(dentry, inode);
                ...
        }
        return error;
}

该方法中先调用ramfs_get_inode方法,生成一个inode实例:

// fs/ramfs/inode.c
struct inode *ramfs_get_inode(struct super_block *sb,
                                const struct inode *dir, umode_t mode, dev_t dev)
{
        struct inode * inode = new_inode(sb);

        if (inode) {
                ...
                switch (mode & S_IFMT) {
                default:
                        init_special_inode(inode, mode, dev);
                        break;
                case S_IFREG:
                        inode->i_op = &ramfs_file_inode_operations;
                        inode->i_fop = &ramfs_file_operations;
                        break;
                case S_IFDIR:
                        inode->i_op = &ramfs_dir_inode_operations;
                        inode->i_fop = &simple_dir_operations;

                        /* directory inodes start off with i_nlink == 2 (for "." entry) */
                        inc_nlink(inode);
                        break;
                case S_IFLNK:
                        inode->i_op = &page_symlink_inode_operations;
                        inode_nohighmem(inode);
                        break;
                }
        }
        return inode;
}

由上可知,mode代表的文件类型为S_IFBLK,所以上面的代码会进入到init_special_inode方法。

// fs/namei.c
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
        inode->i_mode = mode;
        if (S_ISCHR(mode)) {
                inode->i_fop = &def_chr_fops;
                inode->i_rdev = rdev;
        } else if (S_ISBLK(mode)) {
                inode->i_fop = &def_blk_fops;
                inode->i_rdev = rdev;
        } else if (S_ISFIFO(mode))
                inode->i_fop = &pipefifo_fops;
        else if (S_ISSOCK(mode))
                ;       /* leave it no_open_fops */
        else
                ...
}
EXPORT_SYMBOL(init_special_inode);

由该方法可以看到,inode->i_fop字段被设置为def_blk_fops,inode->i_rdev字段被设置为rdev,即上文中的ROOT_DEV,也就是说,inode->i_rdev指向的是硬盘的/dev/nvme0n1p2分区,其实就是我们真正的根目录所在的硬盘分区。

该方法完毕后,最终会返回到ramfs_mknod方法,在ramfs_mknod方法中,又调用d_instantiate方法将新生成的inode赋值给dentry的d_inode字段。

ramfs_mknod方法执行完毕后,最终会返回到create_dev方法,create_dev方法执行完毕后,最终会返回到mount_root方法。

在mount_root方法里,会继续执行mount_block_root方法。

// init/do_mounts.c
void __init mount_block_root(char *name, int flags)
{
        struct page *page = alloc_page(GFP_KERNEL);
        char *fs_names = page_address(page);
        char *p;
        ...
        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) {
                        case 0:
                                goto out;
                        case -EACCES:
                        case -EINVAL:
                                continue;
                }
                ...
        }
        ...
}

该方法作用是,遍历注册的文件系统类型,依次尝试将/dev/root指向的硬盘分区挂载到/root目录下。

// init/do_mounts.c
static int __init do_mount_root(char *name, char *fs, int flags, void *data)
{
        struct super_block *s;
        int err = ksys_mount(name, "/root", fs, flags, data);
        ...
        ksys_chdir("/root");
        ...
        return 0;
}

由上面的代码可以看到,该方法先调用ksys_mount方法将/dev/root挂载到/root目录,如果成功,再调用ksys_chdir方法,将当前目录切换到/root目录。

mount_root方法执行完毕后,退回到最开始的prepare_namespace方法。

在prepare_namespace方法中,调用ksys_mount(".", "/", NULL, MS_MOVE, NULL)方法将当前目录挂载的文件系统移动到根目录。

最后,调用ksys_chroot(".")方法,将当前进程的根目录切换成当前目录,即真正的硬盘分区所代表的文件系统的根目录。

至此,Linux下根目录挂载的整个流程就结束了。

细心的朋友可能还会有个小疑问,硬盘分区所属的文件系统的原始目录为/dev/root,之后/dev/root又被挂载到/root目录,这里所说的目录都是rootfs文件系统的目录,但是,由上一篇文章可以看到,rootfs文件系统初始化时,只创建了根目录,并没有创建/dev/root和/root目录啊,没有这些目录,这些挂载操作怎么可能执行成功呢?

带着这个疑问,我们看下面的代码:

// init/noinitramfs.c
static int __init default_rootfs(void)
{
        int err;

        err = ksys_mkdir((const char __user __force *) "/dev", 0755);
        if (err < 0)
                goto out;

        err = ksys_mknod((const char __user __force *) "/dev/console",
                        S_IFCHR | S_IRUSR | S_IWUSR,
                        new_encode_dev(MKDEV(5, 1)));
        if (err < 0)
                goto out;

        err = ksys_mkdir((const char __user __force *) "/root", 0700);
        if (err < 0)
                goto out;

        return 0;

out:
        printk(KERN_WARNING "Failed to create a rootfs\n");
        return err;
}
rootfs_initcall(default_rootfs);

是不是有种 “原来如此” 的感觉?这些目录都是在上面的方法里创建的。

好了,到此整个分析过程就已经结束了。

完。

原文发布于微信公众号 - Linux内核及JVM底层相关技术研究(ytcode)

原文发表时间:2019-06-06

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券