继上篇文章 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底层相关技术研究 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!