因为每种文件系统的超级块的格式不同,所以每种文件系统需要向虚拟文件系统注册文件系统类型 file_system_type,实现 mount 方法用来读取和解析超级块。
函数 register_filesystem 用来注册文件系统类型:
int register_filesystem(struct file_system_type *fs);
函数 unregister_filesystem 用来注销文件系统类型:
int unregister_filesystem(struct file_system_type *fs);
管理员可以执行命令“cat /proc/filesystems”来查看已经注册的文件系统类型。
虚拟文件系统在内存中把目录组织为一棵树。一个文件系统,只有挂载到内存中目录树的一个目录下,进程才能访问这个文件系统。
管理员可以执行命令“mount -t fstype [-o options] device dir”,把存储设备 device 上类型为 fstype 的文件系统挂载到目录 dir 下。例如:命令“mount -t ext4 /dev/sda1 /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)函数 umount2,对应内核的系统调用 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下的子目录和文件。
假设在文件系统1中,在目录a下挂载文件系统2,在目录b下挂载文件系统3,在目录c下挂载文件系统4。假设文件系统1的挂载描述符是m1,文件系统2的挂载描述符是m2,文件系统3的挂载描述符是m3,文件系统4的挂载描述符是m4,如图所示,这些挂载描述符组成一棵挂载树。
系统调用 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 -t 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 设置为 MS_BIND。
举例说明:假设执行命令“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_ns 以分配挂载命名空间。
(2)调用函数 copy_tree 以复制挂载树。
(3)把子进程的根目录的挂载描述符(task_struct.fs->root.mnt)设置为复制生成的挂载描述符。如果父进程的根目录的挂载描述符是 m1,复制挂载树时从挂载描述符 m1 复制生成挂载描述符 m1-1,那么子进程的根目录的挂载描述符是 m1-1。
(4)把子进程的当前工作目录的挂载描述符(task_struct.fs->pwd.mnt)设置为复制生成的挂载描述符。如果父进程的当前工作目录的挂载描述符是 m2,复制挂载树时从挂载描述符 m2 复制生成挂载描述符 m2-1,那么子进程的当前工作目录的挂载描述符是 m2-1。
假设在文件系统 1 中,在目录 a 下挂载文件系统 2,在目录 b 下挂载文件系统 3,在目录 c 下挂载文件系统 4。假设文件系统 1 的挂载描述符是 m1,文件系统 2 的挂载描述符是m2,文件系统 3 的挂载描述符是 m3,文件系统 4 的挂载描述符是 m4,那么这些挂载描述符组成一棵挂载树,假设这棵挂载树属于挂载命名空间 1,挂载命名空间 1 的成员 root 指向挂载树的根。
如图所示,从挂载命名空间 1 复制生成挂载命名空间 2 的时候,把挂载命名空间 1 的挂载树复制一份,也就是把挂载树中的每个挂载描述符复制一份:“从 m1 复制生成 m1-1,从 m2 复制生成 m2-1,从 m3 复制生成 m3-1,从 m4 复制生成 m4-1”,实际上是在挂载命名空间 2 中把挂载命名空间 1 的所有文件系统重新挂载一遍。m1 和 m1-1 是文件系统 1 的两个挂载描述符,m2 和 m2-1 是文件系统 2 的两个挂载描述符,m2 和 m2-1 的挂载
点都是文件系统 1 的目录 a,同一个挂载点下有两个挂载描述符。
标准的挂载命名空间是完全隔离的,在一个挂载命名空间中挂载或卸载一个文件系统,不会影响其他挂载命名空间。
如下图所示,如果在挂载命名空间 1 的 m2 的一个目录下挂载文件系统 5,挂载描述符是 m5;在挂载命名空间 2 的 m2-1 的一个目录下挂载文件系统 6,挂载描述符是 m6,那么出现的结果是:挂载命名空间 2 看不到 m5 对应的文件系统 5,挂载命名空间 1 看不到 m6 对应的文件系统 6。
如下图所示,如果在挂载命名空间 1 中卸载 m2 对应的文件系统,不会影响挂载命名空间 2。在挂载命名空间 2 中,文件系统 1 的目录 a 仍然挂载文件系统 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 /mntS
# cat /proc/self/mountinfo
77 61 8:17 / /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。
从属挂载的特点是:假设在同一个挂载点下面同时有共享挂载和从属挂载,所有共享挂载组成一个共享对等体组,如果我们在共享对等体组中的任何一个共享挂载下面挂载或卸载文件系统,会自动传播到所有从属挂载;如果我们在任何一个从属挂载下面挂载或卸载文件系统,则不会传播到所有共享挂载。可以看出,传播是单向的,只能从共享挂载传播到从属挂载,不能从从属挂载传播到共享挂载。
如果需要把一个挂载设置为从属挂载,可以执行下面的命令:
mount --make-slave mountpoint
执行命令“cat /proc/[pid]/mountinfo”以查看挂载信息的时候,从属挂载会显示标记“master:X”,表示这个挂载是共享对等体组 X 的从属;如果从属挂载是从共享挂载传播过来的,会显示标记“propagate_from:X”,表示这个挂载是从属挂载,是从共享对等体组 X 传播过来的。
# mount --make-slave /mntY
# cat /proc/self/mountinfo
169 167 8:22 / /mntY rw,relatime master:2
如果需要在程序中把一个挂载设置为从属挂载,方法是:调用系统调用 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 设置为 MS_UNBINDABLE。
一个文件系统,只有挂载到内存中目录树的一个目录下,进程才能访问这个文件系统。问题是:怎么挂载第一个文件系统呢?第一个文件系统称为根文件系统,没法执行 mount 命令来挂载根文件系统,也不能通过系统调用 mount 挂载根文件系统。
内核有两个根文件系统。
(1)一个是隐藏的根文件系统,文件系统类型的名称是“rootfs”。
(2)另一个是用户指定的根文件系统,引导内核时通过内核参数指定,内核把这个根
文件系统挂载到 rootfs 文件系统的根目录下。
内核初始化的时候最先挂载的根文件系统是 rootfs 文件系统,它是一个内存文件系统,对用户隐藏。虽然我们看不见这个根文件系统,但是我们每天都在使用,每个进程使用的标准输入、标准输出和标准错误,对应文件描述符 0、1 和 2,这 3 个文件描述符都对应控制台的字符设备文件“/dev/console”,这个文件属于 rootfs 文件系统。
如图所示,内核初始化的时候,调用函数 init_ rootfs 以注册 rootfs 文件系统,然后调用函数 init_mount_tree 以挂载 rootfs 文件系统。
(1)函数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);
…
}
(2)函数 init_mount_tree。
函数 init_mount_tree 负责挂载 rootfs 文件系统,其代码如下:
fs/namespace.c
1 static void __init init_mount_tree(void)
2 {
3 struct vfsmount *mnt;
4 struct mnt_namespace *ns;
5 struct path root;
6 struct file_system_type *type;
7
8 type = get_fs_type("rootfs");
9 if (!type)
10 panic("Can't find rootfs type");
11 mnt = vfs_kern_mount(type, 0, "rootfs", NULL);
12 put_filesystem(type);
13 if (IS_ERR(mnt))
14 panic("Can't create rootfs");
15
16 ns = create_mnt_ns(mnt);
17 if (IS_ERR(ns))
18 panic("Can't allocate initial namespace");
19
20 init_task.nsproxy->mnt_ns = ns;
21 get_mnt_ns(ns);
22
23 root.mnt = mnt;
24 root.dentry = mnt->mnt_root;
25 mnt->mnt_flags |= MNT_LOCKED;
26
27 set_fs_pwd(current->fs, &root);
28 set_fs_root(current->fs, &root);
29 }
第 11 行代码,挂载 rootfs 文件系统。
第 16 行代码,创建第一个挂载命名空间。
第 20 行代码,设置 0 号线程的挂载命名空间。
第 27 行代码,把 0 号线程的当前工作目录设置为 rootfs 文件系统的根目录。
第 28 行代码,把 0 号线程的根目录设置为 rootfs 文件系统的根目录。
(3)函数 default_rootfs。
接下来,函数 default_rootfs 在 rootfs 文件系统中创建必需的目录和文件。
1)创建目录“/dev”。
2)创建控制台的字符设备文件“/dev/console”,主设备号是 5,从设备号是 1。
3)创建目录“/root”。
init/noinitramfs.c
static 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;
}
rootfs_initcall(default_rootfs);
(4)打开文件描述符 0、1 和 2。
然后 1 号线程打开控制台的字符设备文件“/dev/console”,得到文件描述符 0,接着两
次复制文件描述符 0,得到文件描述符 1 和 2。
kernel_init -> kernel_init_freeable
init/main.c
static noinline void __init kernel_init_freeable(void)
{
…
/* 打开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);
…
}
最后 1 号线程在函数 kernel_init 中装载用户程序,转换成用户空间的 1 号进程,分叉生成子进程,子进程从 1 号进程继承打开文件表,继承文件描述符 0、1 和 2。
引导内核的时候,可以使用内核参数“root”指定存储设备的名称,使用内核参数“rootfstype”指定根文件系统的类型。
假设使用SATA硬盘作为存储设备,根文件系统是SATA硬盘a的第一个分区上的EXT4文件系统,那么指定根文件系统的方法如下:
root=/dev/sda1 rootfstype=ext4
假设使用 NAND 闪存作为存储设备,根文件系统是 UBI 设备 1 的卷 rootfs 上的 UBIFS 文件系统,那么指定根文件系统的方法如下:
root=ubi1:rootfs rootfstype=ubifs
UBIFS 文件系统基于 UBI 设备,UBI 设备是虚拟设备,用户可以在 NAND 闪存的一个分区上创建一个 UBI 设备,然后对 UBI 设备分区,UBI 把分区称为卷。UBI 设备负责如下。
(1)管理 NAND 闪存的坏块。
(2)实现损耗均衡,保证所有擦除块的擦除次数均衡,UBI 使用逻辑擦除块,把逻辑擦除块映射到 NAND 闪存的物理擦除块。内核实现了 UBI 层,位于 UBIFS 文件系统和 MTD 层之间。
(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)
{
strlcpy(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 以挂载根文件系统,主要代码如下:
kernel_init -> kernel_init_freeable -> prepare_namespace
init/do_mounts.c
1 void __init prepare_namespace(void)
2 {
3 …
4 if (saved_root_name[0]) {
5 root_device_name = saved_root_name;
6 if (!strncmp(root_device_name, "mtd", 3) ||
7 !strncmp(root_device_name, "ubi", 3)) {
8 mount_block_root(root_device_name, root_mountflags);
9 goto out;
10 }
11 ROOT_DEV = name_to_dev_t(root_device_name);
12 if (strncmp(root_device_name, "/dev/", 5) == 0)
13 root_device_name += 5;
14 }
15
16 …
17 mount_root();
18 out:
19 devtmpfs_mount("dev");
20 sys_mount(".", "/", NULL, MS_MOVE, NULL);
21 sys_chroot(".");
22 }
第 6~10 行代码,如果存储设备是闪存分区(设备名称以“mtd”开头)或是在闪存分区的基础上封装的 UBI 设备(设备名称以“ubi”开头),那么调用函数 mount_block_root,把根文件系统挂载到 rootfs 文件系统的目录“/root”下。第 17 行代码,如果存储设备是其他设备,例如机械硬盘或固态硬盘,那么调用函数 mount_root,把根文件系统挂载到 rootfs 文件系统的目录“/root”下。第 20 行代码,把根文件系统从目录“/root”移动到根目录下,换言之,以前挂载到目录“/root”下,现在挂载到根目录下。在执行这一步之前,1 号线程的当前工作目录已经是目录“/root”,也就是刚刚挂载的根文件系统的根目录。第 21 行代码,把 1 号线程的根目录设置为根文件系统的根目录。函数 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)创建块设备文件“/dev/root”,使用根设备的设备号。
2)调用函数 mount_block_root,把根文件系统挂载到目录“/root”下。假设根文件系统的类型是 EXT4,存储设备是“/dev/sda1”,挂载根文件系统相当于执行命令“mount –t ext4 /dev/sda1 /root”。现在内核创建块设备文件“/dev/root”,设备号和块设备文件“/dev/sda1”的设备号相同,挂载根文件系统相当于执行命令“mount –t ext4 /dev/root /root”。
(3)函数 mount_block_root。
参数“rootfstype”可能指定多个文件系统类型,使用逗号分隔,函数 mount_block_root依次使用每种文件系统类型尝试挂载根文件系统,直到挂载成功为止,主要代码如下:
init/do_mounts.c
1 void __init mount_block_root(char *name, int flags)
2 {
3 struct page *page = alloc_page(GFP_KERNEL |
4 __GFP_NOTRACK_FALSE_POSITIVE);
5 char *fs_names = page_address(page);
6 char *p;
7 #ifdef CONFIG_BLOCK
8 char b[BDEVNAME_SIZE];
9 #else
10 const char *b = name;
11 #endif
12
13 get_fs_names(fs_names);
14 retry:
15 for (p = fs_names; *p; p += strlen(p)+1) {
16 int err = do_mount_root(name, p, flags, root_mount_data);
17 switch (err) {
18 case 0:
19 goto out;
20 case -EACCES:
21 case -EINVAL:
22 continue;
23 }
24 printk("VFS: Cannot open root device \"%s\" or %s: error %d\n",
25 root_device_name, b, err);
26 printk("Please append a correct \"root=\" boot option; here are the available partitions:\n");
27 …
28 panic("VFS: Unable to mount root fs on %s", b);
29 }
30 …
31 panic("VFS: Unable to mount root fs on %s", b);
32 out:
33 put_page(page);
34 }
第 13 行代码,调用函数 get_fs_names:如果使用参数“rootfstype”指定多个文件系统类型,使用逗号分隔,那么函数 get_fs_names 把逗号替换为字符串的结束符号;如果没有使用参数“rootfstype”指定文件系统类型,那么函数 get_fs_names 把所有注册的文件系统类型添加进来。
第 15~29 行代码,针对指定的每种文件系统类型,调用函数 do_mount_root 尝试挂载。如果指定的文件系统类型和存储设备上的文件系统类型一致,那么挂载成功。
(4)函数 do_mount_root。
函数 do_mount_root 把根文件系统挂载到 rootfs 文件系统的“/root”目录下,代码如下:
init/do_mounts.c
1 static int __init do_mount_root(char *name, char *fs, int flags, void *data)
2 {
3 struct super_block *s;
4 int err = sys_mount(name, "/root", fs, flags, data);
5 if (err)
6 return err;
7
8 sys_chdir("/root");
9 s = current->fs->pwd.dentry->d_sb;
10 ROOT_DEV = s->s_dev;
11 …
12 return 0;
13 }
第 4 行代码,在目录“/root”下挂载根文件系统。
第 8 行代码,把 1 号线程的当前工作目录设置为目录“/root”,也就是刚刚挂载的根文件系统的根目录。