Linux 挂载文件系统

3,147 阅读21分钟

挂载文件系统

虚拟文件系统在内存中把目录组织为一棵树一个文件系统,只有挂载到内存中目录 树的一个目录下,进程才能访问这个文件系统。 管理员可以执行命令 mount-t- fstype[--o options] device dir,把存储设备 device 上类 型为 fstype 的文件系统挂载到目录 dir 下。例如:命令 mount-t ext4 /dev/sdal /a 把 SATA 硬盘 a 的第一个分区上的 EXT4 文件系统挂载到目录 “/a” 下。 管理员可以执行命令 “umount dir” 来卸载在目录 dir 下挂载的文件系统 glibc 库封装了挂载文件系统的函数 mount:

int mount(const char *dev_name, const char dir_name,
  const char *type, unsigned long flags,
  const void *data);

参数 dev_name 是设备名称,参数 dir_name 是目录名称,参数 type 是文件系统类型的 名称,参数 flags 是挂载标志位,参数 data 是挂载选项。这个函数调用内核的系统调用 mount。

glibc 库封装了两个卸载文件系统的函数。 (1)函数umount,对应内核的系统调用 oldumount。 int umount (const char *target) (2)函数umount22,对应内核的系统调用 umount。 int umount2 (const char *target, int flags) 每次挂载文件系统,虚拟文件系统就会创建一个挂载描述符: mount 结构体。挂载描述符用来描述文件系统的一个挂载实例,同一个存储设备上的文件系统可以多次挂载,每次挂载到不同的目录下。假设我们把文件系统 2 挂载到目录 “/a” 下,目录 a 属于文件系统 1,挂载描述符的数据结构如图所示。

挂载描述符的数据结构
为了能够快速找到目录 a 下挂载的文件系统,把文件系统 2 的挂载描述符加入全局散列表 mount_hashtable,关键字是{父挂载描述符,挂载点},根据文件系统 1 的挂载描述符和目录 a 可以在散列表中找到文件系统 2 的挂载描述符。

如图所示,在文件系统 1 中,目录 a 下可能有子目录和文件。在目录 a 下挂载文件系统 2 以后,当进程访问目录 “a” 的时候,虚拟文件系统发现目录 a 是挂载点,就会跳转到文件系统 2 的根目录。所以进程访问目录 “/a” 实际上是访问目录 a 下挂载的文件系统 2 的根目录,进程看不到文件系统 1 中的目录 a 下的子目录和文件。只有从目录 a 卸载文件系统 2 以后,进程才能重新看到文件系统 1 中目录 a 下的子目录和文件。

访问目录a
假设在文件系统 1 中,在目录 a 下挂载文件系统 2,在目录 b 下挂载文件系统 3,在目录 c 下挂载文件系统 4。假设文件系统 1 的挂载描述符是 m1,文件系统 2 的挂载描述符是 m2,文件系统 3 的挂载描述符是 m3,文件系统 4 的挂载描述符是 m4,如上图所示,这些挂载描述符组成一颗挂载树。

系统调用 mount

系统调用 mount 用来挂载文件系统,其定义如下: [fs/namespace.c]

SYSCALL_DEFINE5(mount, char __user *, dev_name, char __user *, dir_name,
		char __user *, type, unsigned long, flags, void __user *, data)

使用命令 “mount -r fstype [-o options] device dir” 执行一个标准的挂载操作时,系统调用 mount 的执行流程如下:

  1. 调用函数 user_path,根据目录名称找到挂载描述符和 dentry 实例。
  2. 调用函数 get_fs_type,根据文件系统类型的名称查找 file_system_type 实例。
  3. 调用函数 alloc_vfsmnt,分配挂载描述符。
  4. 调用文件系统类型的挂载方法,读取并且解析超级块。
  5. 把挂载描述符添加到超级块的挂载实例链表中。
  6. 把挂载描述符加入散列表。
  7. 把挂载描述符加入父亲的孩子链表。

绑定挂载

绑定挂载(bind mount)用来把目录树的一棵子树挂载到其他地方。执行绑定挂载的命 令如下所示: mount --bind olddir newdir 把以目录 olddir 为根的子树挂载到目录 newdir,以后从目录 newdir 和目录 olddir 可以看到相同的内容。可以把一个文件绑定到另一个文件,访问这两个文件时看到的数据完全相同。例如:执行命令 “mount --bind /a/c.txt /b/d.txt”,把文件 “/a/c.txt” 绑定挂载到文件 “/b/d.txt”命令。 “mount --bind olddir newdir” 只会挂载一个文件系统(即目录 olddir 所属的文件系统)或其中的一部分。如果需要绑定挂载目录 olddir 所属的文件系统及其所有子挂载,应该执行下面的命令: “mount --rbind olddir newdir” rbind 中的 r 是递归 (recursively) 的意思。

如果需要在程序中执行绑定挂载, 方法是:调用系统调用 mount,把参数 flags 设置为 MSBIND

举例说明:假设执行命令 “mount --bind /a/b /c”,把目录 “/a/b” 绑定挂载到目录 “/c”,目录 a 和 b 属于文件系文件系统 1 的目录,目录 c 属于文件系统 2,实际上是把文件系统 1 中以目录 b 为根的子树挂载到文件系统 2 的目录 “c”,数据结构如下图所示。注意:只挂载了文件系统 1 的一部分,文件系统 1 的 mount 实例的成员 mnt.mnt_root 指向文件系统 1 的目录 b,而不是指向文件系统 1 的根目录。

绑定挂载

挂载命名空间

和虚拟机相比,容器是一种轻量级的虚拟化技术,直接使用宿主机的内核,使用命名空间隔离资源,其中挂载命名空间用来隔离挂载点。 每个进程属于一个挂载命名空间,数据结构如下图:

进程和挂载命名空间的关系
可以使用一项两种方法创建新的挂载命名空间。

  1. 调用 clone 创建子进程时,如果指定标志位 CLONE_NEWNS,那么子进程将会从父进程的挂载命名空间复制生成一个新的挂载命名空间;如果没有指定标志位 CLONE_NEWNS,那么子进程将会和父进程属于同一个挂载命名空间。
  2. 调用 unshare(CLONE_NEWNS)以设置不再和父进程共享挂载命名空间,从父进程的挂载命名空间复制生成一个新的挂载命名空间。

复制生成的挂载命名空间的级别和旧的挂载命名空间是平等的,不存在父子关系。

调用系统调用 clone 创建子进程,如果指定标志位 CLONE_NEWNS,执行流程如下:

  1. 调用函数 alloc_mnt 以分配挂载命名空间。
  2. 调用函数 copy_tree 以复制挂载树。
  3. 把子进程的根目录的挂载描述符(task_struct->root.mnt)设置为复制生成的挂载描述符。如果父进程的根目录的挂载描述符是 ml,复制挂载树时从挂载描述符 ml 复制生成挂载描述符 m1-1,那么子进程的根目录的挂载描述符是 ml-1。
  4. 把子进程的当前工作目录的挂载描述符(task_ struct.fs->pwd.mnt)设置为复制生成的挂载描述符。如果父进程的当前工作目录的挂载描述符是 m2,复制挂载树时从挂载描述符 m2 复制生成挂载描述符 m2-1,那么子进程的当前工作目录的挂载描述符是 m2-1。

假设在文件系统 1 中,在目录 a 下挂载文件系统 2,在目录 b 下挂载文件系统 3,在目录 c 下挂载文件系统 4。假设文件系统 1 的挂载描述符是 ml,文件系统 2 的挂载描述符是 m2,文件系统 3 的挂载描述符是 m3,文件系统 4 的挂载描述符是 m4,那么这些挂载描述符组成一棵挂载树,假设这棵挂载树属于挂载命名空间 1,挂载命名空间 1 的成员 root 指向挂载树的根。

如下图所示,从挂载命名空间 1 复制生成挂载命名空间 2 的时候,把挂载命名空间 1 的挂载树复制一份,也就是把挂载树中的每个挂载描述符复制一份:“从 ml 复制生成 m1-1,从 m2 复制生成 m2-1,从 m3 复制生成 m3-1,从 m4 复制生成 m4-1”,实际上是在挂载命名空间 2 中把挂载命名空间 1 的所有文件系统重新挂载一遍。m 和 ml-1 是文件系统 1 的两个挂载描述符,m2 和 m2-1 是文件系统 2 的两个挂载描述符,m2 和 m2-1 的挂载点都是文件系统 1 的目录 a,同一个挂载点下有两个挂载描述符。

复制生新的挂载命名空间

1 标准的挂载命名空间

标准的挂载命名空间是完全隔离的,在一个挂载命名空间中挂载或卸载一个文件系统,不会影响其他挂载命名空间。 如图所示,如果在挂载命名空间 1 的 m2 的一个目录下挂载文件系统 5,挂载描述符是 m5;在挂载命名空间 2 的 m2-1 的一个目录下挂载文件系统 6,挂载描述符是 m6,那么出现的结果是:挂载命名空间 2 看不到 m5 对应的文件系统 5,挂载命名空间 1 看不到 m6 对应的文件系统 6。

在挂载命名空间中挂载文件系统

如下图所示,如果在挂载命名空间 1 中卸载 m2 对应的文件系统,不会影响挂载命名空间 2。在挂载命名空间 2 中,文件系统 1 的目录 a 仍然挂载文件系统 2。

在挂载命名空间卸载文件系统

2 共享子树

在一个标志的挂载命名空间中挂载或卸载一个文件系统,不会影响其他挂载命名空间。在某些情况下,隔离程度太重了。例如:用户插入一个移动硬盘,为了使移动硬盘在所有的挂载命名空间中可用,必须在每个挂载命名空间中执行挂载操作,非常麻烦。用户的需求是:只执行一次挂载操作,所有挂载命名空间都可用访问移动硬盘。为了满足这种用户需求,Linux 2.6.15 版本引入了共享子树。 共享子树提供了 4 种挂载类型。

  • 共享挂载(shared mount)。
  • 从属挂载(slave mount)。
  • 私有挂载(private mount)。
  • 不可绑定挂载(unbindable mount)。 默认的挂载类型为私有挂载。
共享挂载。

共享挂载的特点是:同一个挂载点下面的所有共享挂载共享挂载/卸载事件。如果我们在一个共享挂载下面挂载或卸载文件系统,那么会自动传播到所有其他共享挂载,即自动在所有其他共享挂载下面执行挂载或卸载操作。 如果需要把一个挂载设置为共享挂载,可以执行下面命令: “mount --make-shared mountpoint” 同一个挂载点下面的所有共享挂载组成一个对等体组(peer group),内核自动给每个对等体组分配一个唯一的标识符。执行命令“cat /proc/[pid]mountinfo” 以查看挂载信息的时候,共享挂载会显示标志 “shared:X”,X 是对等体组的标识符。

# mount --make-shared /mount_fs
# cat /proc/self/mountinfo
77 61 8:18 / /mntS rw,relatime shared:1

如果需要在程序中吧一个挂载设置为共享挂载,方法是:调用系统调用 mount,把参数 flags 设置为 MS_SHARED。 假设我们把 m2 和 m2-1 设置为共享挂载,当我们在挂载命名空间 1 的 m2 下面挂载文件系统 5 的时候,会自动把挂载事件传播到挂载命名空间 2 的 m2-1,即自动在挂载命名空间 2 的 m2-1 下面挂载文件系统 5,最终的结果是:在 m2 下面生成子挂载 m5,在 m2-1 下面生成子挂载 m5-1。 当我们在挂载命名空间 1 的 m2 下面卸载文件系统 5 的时候,会自动把卸载事件传播到挂载命名空间 2 的 m2-1,即自动在挂载命名空间 2 的 m2-1 下面卸载文件系统 5。

在共享挂载m2下面挂载文件系统5

从属挂载

从属挂载的特点是:就是在同一个挂载点下面同时又共享挂载和从属挂载,所有共享挂载组成一个共享对等体组,如果我们在共享对等体组中的任何一个共享挂载下面挂载或卸载文件系统,会自动传播到所有从属挂载;如果在任何一个从属挂载下面挂载或卸载文件系统,则不会传播到所有共享挂载。可以看出,传播是单向的,只能从共享挂载传播到从属挂载,不能从从属挂载传播到共享挂载。 如果需要把一个挂载设置为从属挂载,可以执行下面的命令: mount --make-slave mountpoint 执行命令 “cat /proc/pid/mountinfo” 以查看挂载信息的时候,从属挂载会显示标志 “master:X”,表示这个挂载是共享对等体组 X 的从属;如果从属挂载是从共享挂载传播过来的,会显示标志 “propagate_from:X”,表示这个挂载是从属挂载,是从共享对等体组 X 传播过来的。 如果需要在程序中把一个挂载设置为从属挂载,方法是:调用系统调用 mount,把参数 flags 设置为 MS_SLAVE。 我们继续使用前面的例子,就是我们把 m2 设置为共享挂载,把 m2-1 设置为从属挂载。当我们在挂载命名空间 1 的 m2 下面挂载文件系统 5 的时候,会自动把挂载事件传播到挂载命名空间 2 的 m2-1,即自动在挂载命名空间 2 的 m2-1 下面挂载文件系统 5,最终的结果是:在 m2 下面生成子挂载 m5,在 m2-1 下面生成子挂载 m5-1。

从属挂载

当我们在挂载命名空间 2 的 m2-1 下面挂载文件系统 6 的时候,不会吧挂载事件传播到挂载命名空间 1 的 m2,即不会再挂载命名空间 1 的 m2 下面挂载文件系统 6,最终的结果是:在 m2-1 下面生成子挂载 m6。当我们在挂载命名空间 1 的 m2 下面卸载文件系统 5 的时候,会自动把卸载事件传播到挂载命名空间 2 的 m2-1,即自动在挂载命名空间 2 的 m2-1 下面卸载文件系统 5。

私有挂载。

载下面挂载或卸载文件系统,不会传播到同一个挂载点下面的所有其他挂载;在同一个挂载点的其他挂载下面挂载或卸载文件系统,也不会传播到私有挂载。 如果需要把一个挂载设置为私有挂载,可以执行下面的命令: mount --make-private mountpoint 默认的挂载类型是私有挂载,当执行命令 “mount-t- fstype--o options] device dir”,把存储设备 device 上类型为 fstype 的文件系统挂载到目录 dir 的时候,挂载类型是私有挂载。如果需要在程序中把一个挂载设置为私有挂载,方法是:调用系统调用 mount,把参数 flags 设置为 MS_PRIVATE。

不可绑定挂载。

不可绑定挂载是私有挂载,并且不允许被绑定挂载。如果需要把一个挂载设置为不可绑定挂载,可以执行下面的命令: mount --make-unbindable mountpoint 执行命令“ “cat proc/[pid]/mountinfo” 以查看挂载信息的时候,不可绑定挂载会显示标记 “unbindable”。如果需要在程序中把一个挂载设置为不可绑定挂载,方法是:调用系统调用 mount 把参数 flags 设置为 M_UNBINDABLE。

挂载根文件系统

一个文件系统,只有挂载到内存中目录树的一个目录下,进程才能访问这个文件系统。问题是:怎么挂载第一个文件系统呢?第一个文件系统称为根文件系统,没法执行 mount 命令来挂载根文件系统,也不能通过系统调用 mount 挂载根文件系统。 内核有两个根文件系统。

  1. 一个是隐藏的根文件系统,文件系统类型的名称是 “rootfs”。
  2. 另一个是用户指定的根文件系统,引导内核时通过内核参数指定,内核把这文件系统挂载到 rootfs 文件系统的根目录下。
    注册和挂载rootfs文件系统

根文件系统 rootfs

内核初始化的时候最先挂载的根文件系统是 rootfs 文件系统,它是一个内存文件系统,对用户隐藏。虽然 我们看不见这个根文件系统,但是我们每天都在使用,每个进程使用的标准输入、标准输出和标准错误,对应文件描述符0、1 和 2,这 3 个文件描述符都对应控制台的字符设备文件 “/dev/console”,这个文件属于 rootfs 文件系统。 如图所示,内核初始化的时候,调用函数 init_rootfs 以注册 rootfs 文件文件系统,然后调用函数 init_mount_tree 以挂载 rootfs 文件系统。

函数init_rootfs

函数 init_rootfs 负责注册 rootfs 文件系统,代码如下: [init/do_mounts.c]

static struct file_system_type rootfs_fs_type = {
	.name		= "rootfs",
	.mount		= rootfs_mount,
	.kill_sb	= kill_litter_super,
};
int __init init_rootfs(void)
{
	int err = register_filesystem(&rootfs_fs_type);
    ...
}
函数 init_mount_tree。

函数 init_mount_tree 负责挂载 rootfs 文件系统,代码如下: [fs/namespace.c]

static void __init init_mount_tree(void)
{
	struct vfsmount *mnt;
	struct mnt_namespace *ns;
	struct path root;
	struct file_system_type *type;

	type = get_fs_type("rootfs");
	if (!type)
		panic("Can't find rootfs type");
    // 挂载 “rootfs” 文件系统。
	mnt = vfs_kern_mount(type, 0, "rootfs", NULL);
	put_filesystem(type);
	if (IS_ERR(mnt))
		panic("Can't create rootfs");
    // 创建第一个挂载命名空间
	ns = create_mnt_ns(mnt);
	if (IS_ERR(ns))
		panic("Can't allocate initial namespace");
    // 设置 0 号线程的挂载命名空间
	init_task.nsproxy->mnt_ns = ns;
	get_mnt_ns(ns);

	root.mnt = mnt;
	root.dentry = mnt->mnt_root;
	mnt->mnt_flags |= MNT_LOCKED;
  // 把 0 号线程的当前工作目录设置为 “rootfs” 文件系统的根目录
	set_fs_pwd(current->fs, &root);
  // 把 0 号线程的根目录设置为 “rootfs” 文件系统的根目录
	set_fs_root(current->fs, &root);
}
函数 default_rootfs

接下来,函数 default_rootfs 在 rootfs 文件系统中创建必需的目录和文件。

  1. 创建目录 “/dev”。
  2. 创建控制台的字符设备文件 “/dev/console”,主设备号是 5,从设备号是 1。
  3. 创建目录 “/root”

[init/noinitramfs.c]

int __init default_rootfs(void)
{
	int err;

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

	err = sys_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 = sys_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;
}
打开文件描述符 0、1 和 2。

然后 1 号线程打开控制台的字符设备文件 “/dev/console”,得到文件描述符 0,接着两次复制文件描述符 0,得到文件描述符 1 和 2。 rest_init -> kenel_thread -> kernel_init -> kernel_init_freeable [init/main.c]

static noinline void __init kernel_init_freeable(void)
{
    ...
	do_basic_setup();

	/* 打开 rootfs 文件系统的字符设备文件 “/dev/console” */
	if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
		pr_err("Warning: unable to open an initial console.\n");

	(void) sys_dup(0);
	(void) sys_dup(0);
	/*
	 * check if there is an early userspace init.  If yes, let it do all
	 * the work
	 */
	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();
	}

	/*
	 * Ok, we have completed the initial bootup, and
	 * we're essentially up and running. Get rid of(摆脱,去除) the
	 * initmem segments and start the user-mode stuff..
	 *
	 * rootfs is available now, try loading the public keys
	 * and default modules
	 */
	integrity_load_keys();
	load_default_modules();
}

最后 1 号线程在函数 kernel_init 中装载用户程序,转换成用户空间的 1 号进程,分叉生成子进程,子进程从 1 号线程继承打开文件表,继承文件描述符0、1 和 2。

用户指定的根文件系统

引导内核的时候,可以使用内核参数 “root” 指定存储设备的名称,使用内核参数 “rootfstype” 指定根文件系统的类型。

假设使用 SATA 硬盘作为存储设备,根文件系统是 SATA 硬盘 a 的第一个分区上的 EXT4文件系统,那么指定根文件系统的方法如下: root=/dev/sdal rootfstype=ext4 假设使用 NAND 闪存作为存储设备,根文件系统是 UBI 设备 1 的卷 rootfs 上的 UBIFS 文件系统,那么指定根文件系统的方法如下: root=ubil:rootfs rootfstype=ubifs UBIFS 文件系统基于 UBI 设备,UBI 设备是虚拟设备,用户可以在 NAND 闪存的一个分区上创建一个 UBI 设备,然后对 UBI 设备分区,UBI把分区称为卷。UBI 设备负责如下。

  1. 管理 NAND 闪存的坏块。
  2. 实现损耗均衡,保证所有擦除块的擦除次数均衡,UBI 使用逻辑擦除块,把逻辑擦除块映射到 NAND 闪存的物理擦除块。

内核实现了 UBI 层, 位于 UBIFS 文件系统和 MTD 层(memorgy Technolagy Device 存储技术设备)之间。

(1) 解析参数 “root” 和 “rootfstype” 内核初始化的时候,调用函数 parse_args 解析参数,调用参数的解析函数。 [init/main. c]

asmlinkage __visible void __init start_kernel(void)
{
    ...
	after_dashes = parse_args("Booting kernel",
				  static_command_line,
				  __start___param,
				  __stop___param - __start___param,
				  -1,
				  -1,
				  NULL,
				  &unknown_bootoption);
   ...
}

参数 “root” 用来指定根文件系统所在的存储设备,解析函数是 root_dev_setup,把设备名称保存在静态变量 saved_root_name 中。 [init/do_mounts. c]

 static char _initdata saved_ root_name [64]:

 static int init root_dev_setup(char *line){
   strlepy(saved_root_name, line, sizeof(saved_root_name));
   return 1;
 }
 setup("root=", root_dev_setup);

参数 “rootfstype” 用来指定根文件系统的类型,解析函数是 fs_names_setup,把根文件系统的类型保存在静态变量 root_fs_names 中。 [init/do_mounts.c]

 static char * _initdata root_fs_names:
 static int init fs_names_setup(char *str){
   root_fs_names = str;
   return 1;
 }
 __setup("rootfstype=", fs_names_setup);

(2)函数 prepare_namespace 接下来 1 号线程调用函数 prepare_namespace 以挂载根文件系统,主要代码如下: [init/do_mounts.c]

/*
 * Prepare the namespace - decide what/where to mount, load ramdisks, etc.
 */
void __init prepare_namespace(void)
{
	int is_floppy;

	if (root_delay) {
		printk(KERN_INFO "Waiting %d sec before mounting root device...\n",
		       root_delay);
		ssleep(root_delay);
	}

	/*
	 * wait for the known devices to complete their probing
	 *
	 * Note: this is a potential source of long boot delays.
	 * For example, it is not atypical to wait 5 seconds here
	 * for the touchpad of a laptop to initialize.
	 */
	wait_for_device_probe();

	md_run_setup();
	dm_run_setup();

	if (saved_root_name[0]) {
		root_device_name = saved_root_name;
    /*
    如果存储设备是闪存分区(设备名称以 “mtd” 开头)或是在闪存分区的基础上封装的
    UBI 设备(设备名称以 “ubi” 开头),那么调用函数 mount_block_root,把根文件系统
    挂载到 rootfs 文件系统的目录 “/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);
		if (strncmp(root_device_name, "/dev/", 5) == 0)
			root_device_name += 5;
	}

	if (initrd_load())
		goto out;

	/* wait for any asynchronous scanning to complete */
	if ((ROOT_DEV == 0) && root_wait) {
		printk(KERN_INFO "Waiting for root device %s...\n",
			saved_root_name);
		while (driver_probe_done() != 0 ||
			(ROOT_DEV = name_to_dev_t(saved_root_name)) == 0)
			msleep(100);
		async_synchronize_full();
	}

	is_floppy = MAJOR(ROOT_DEV) == FLOPPY_MAJOR;

	if (is_floppy && rd_doload && rd_load_disk(0))
		ROOT_DEV = Root_RAM0;
    // 如果存储设备是其他设备,例如:机械硬盘或固态硬盘,那么调用函数 mount_root,
    // 把根文件系统挂载到 rootfs文件系统的目录 “/root” 下。
	mount_root();
out:
	devtmpfs_mount("dev");
    // 把根文件系统从目录 “/root” 移动到根(“/”)目录下,换言之,以前挂载到目录
    // “/root” 下,现在挂载到根目录下。在执行这一步之前,1 号线程的当前工作目录
    // 已经是目录 “/root”,也就是刚刚挂载的根文件系统的根目录。
	sys_mount(".", "/", NULL, MS_MOVE, NULL);
    // 把 1 号线程的根目录设置为根文件系统的根目录。
	sys_chroot(".");
}

函数 mount_root 的主要代码如下: [init/do_mounts.c]

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
}

如果根设备是块设备,执行过程如下。

  1. 创建块设备文件 “/devroot”,使用根设备的设备号。
  2. 调用函数 mount_block_root,把根文件系统挂载到目录 “root” 下。 假设根文件系统的类型是 EXT4,存储设备是 “/dev/sda1”,挂载根文件系统相当于执行命令 “mount -t ext4 /dev/sdal /root”。现在内核创建块设备文件 “dev/root”, 它的设备号和块设备件 “dev/sdal” 的设备号相同,挂载根文件系统相当于执行命令 “mount -t ext4 /dev/root /root”。

(3)函数 mount_block_root。 参数 “rootfstype” 可能指定多个文件系统类型,使用逗号分隔,函数 mount_block_root 依次使用每种文件系统类型尝试挂载根文件系统直到挂载成功为止,主要代码如下: [init/do_mounts.c]

void __init mount_block_root(char *name, int flags)
{
	struct page *page = alloc_page(GFP_KERNEL |
					__GFP_NOTRACK_FALSE_POSITIVE);
	char *fs_names = page_address(page);
	char *p;
#ifdef CONFIG_BLOCK
	char b[BDEVNAME_SIZE];
#else
	const char *b = name;
#endif
  /*
  调用函数 get_fs_names:如果使用参数 “rootfstype” 指定多个文件系统类型并使用逗号分隔,
  那么函数 get_fs_names 把逗号替换为字符串的结束符号;如果没有使用参数 “rootfstype”
  指定文件系统类型,那么函数 get_fs_names 把所有注册的文件系统类型添加进来。
  */
	get_fs_names(fs_names);
retry:
  /*
  针对指定的每种文件系统类型,调用函数 do_mount_root试挂载。如果指定的文件系统类型和
  存储设备上的文件系统类型一致,那么挂载成功。
  */
	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;
		}
	        /*
		 * Allow the user to distinguish between failed sys_open
		 * and bad superblock on root device.
		 * and give them a list of the available devices
		 */
#ifdef CONFIG_BLOCK
		__bdevname(ROOT_DEV, b);
#endif
		printk("VFS: Cannot open root device \"%s\" or %s: error %d\n",
				root_device_name, b, err);
		printk("Please append a correct \"root=\" boot option; here are the available partitions:\n");
        ...
		panic("VFS: Unable to mount root fs on %s", b);
	}
    ...
	panic("VFS: Unable to mount root fs on %s", b);
out:
	put_page(page);
}

(4)函数 do_mount_root 函数 do_mount_root 把根文件系统挂载到 rootfs 文件系统的 “/root” 目录下,代码如下: [init/do_mounts.c]

static int __init do_mount_root(char *name, char *fs, int flags, void *data)
{
	struct super_block *s;
    // 在目录 “root” 下挂载根文件系统。
	int err = sys_mount(name, "/root", fs, flags, data);
	if (err)
		return err;
    // 把 1 号线程的当前工作目录设置为目录 “root”,也就是刚刚挂载的根文件系统的根目录。
	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;
}

参考: 《Linux 内核深度解析》 余华兵