◆
在本节中,将描述在 VFS 的目录树中向其中某个目录(安装点 mount point)上挂载(mount)一个文件系统的过程。
这一过程可简单描述为:将某一设备(dev_name)上某一文件系统(file_system_type)安装到VFS目录树上的某一安装点(dir_name)。它要解决的问题是:将对 VFS 目录树中某一目录的操作转化为具体安装到其上的实际文件系统的对应操作。比如说,如果将 hda2 上的根文件系统(假设文件系统类型为 ext2)安装到了前一节中新建立的 "/dev" 目录上(此时,"/dev" 目录就成为了安装点),那么安装成功之后应达到这样的目的,即:对 VFS 文件系统的 "/dev" 目录执行 "ls" 指令,该条指令应能列出 hda2 上 ext2 文件系统的根目录下所有的目录和文件。很显然,这里的关键是如何将对 VFS 树中 "/dev" 的目录操作指令转化为安装在其上的 ext2 这一实际文件系统中的相应指令。所以,接下来的叙述将抓住如何转化这一核心问题。在叙述之前,读者不妨自己设想一下 Linux 系统会如何解决这一问题。记住:对目录或文件的操作将最终由目录或文件所对应的 inode 结构中的 i_op 和 i_fop 所指向的函数表中对应的函数来执行。所以,不管最终解决方案如何,都可以设想必然要通过将对 "/dev" 目录所对应的 inode 中 i_op 和 i_fop 的调用转换到 hda2 上根文件系统 ext2 中根目录所对应的 inode 中 i_op 和 i_fop 的操作。
初始过程由 sys_mount() 系统调用函数发起,该函数原型声明如下:
asmlinkage long sys_mount(char * dev_name, char * dir_name, char * type, unsigned long flags, void * data); |
其中,参数 char *type 为标识将要安装的文件系统类型字符串,对于 ext2 文件系统而言,就是"ext2"。参数 flags 为安装时的模式标识数,和接下来的 data 参数一样,本文不将其做为重点。
为了帮助读者更好地理解这一过程,笔者用一个具体的例子来说明:我们准备将来自主硬盘第 2 分区(hda2)上的 ext2 文件系统安装到前面创建的 "/dev" 目录中。那么对于 sys_mount() 函数的调用便具体为:
sys_mount("hda2","/dev ","ext2",…); |
该函数在将这些来自用户内存空间(user space)的参数拷贝到内核空间后,便调用 do_mount() 函数开始真正的安装文件系统的工作。同样,为了便于叙述和讲清楚主流程,接下来的说明将不严格按照具体的函数调用细节来进行。
do_mount() 函数会首先调用 path_lookup() 函数来得到安装点的相关信息,如同创建目录过程中叙述的那样,该安装点的信息最终记录在 struct nameidata 类型的一个变量当中,为叙述方便,记该变量为nd。在本例中当 path_lookup() 函数返回时,nd 中记录的信息如下:nd.entry = new_entry; nd.mnt = mnt; 这里的变量如图 3 和 4 中所示。
然后,do_mount() 函数会根据调用参数 flags 来决定调用以下四个函数之一:do_remount()、 do_loopback()、do_move_mount()、do_add_mount()。
在我们当前的例子中,系统会调用 do_add_mount() 函数来向 VFS 树中安装点 "/dev " 安装一个实际的文件系统。在 do_add_mount() 中,主要完成了两件重要事情:一是获得一个新的安装区域块,二是将该新的安装区域块加入了安装系统链表。它们分别是调用 do_kern_mount() 函数和 graft_tree() 函数来完成的。这里的描述可能有点抽象,诸如安装区域块、安装系统链表等,不过不用着急,因为它们都是笔者自己定义出来的概念,等一下到后面会有专门的图表解释,到时便会清楚。
do_kern_mount() 函数要做的事情,便是建立一新的安装区域块,具体的内容在前面的章节 VFS 目录树的建立中已经叙述过,这里不再赘述。
graft_tree() 函数要做的事情便是将 do_kern_mount() 函数返回的一 struct vfsmount 类型的变量加入到安装系统链表中,同时 graft_tree() 还要将新分配的 struct vfsmount 类型的变量加入到一个hash表中,其目的我们将会在以后看到。
这样,当 do_kern_mount() 函数返回时,在图 4 的基础上,新的数据结构间的关系将如图 5 所示。其中,红圈区域里面的数据结构便是被称做安装区域块的东西,其中不妨称 e2_mnt 为安装区域块的指针,蓝色箭头曲线即构成了所谓的安装系统链表。
在把这些函数调用后形成的数据结构关系理清楚之后,让我们回到本章节开始提到的问题,即将 ext2 文件系统安装到了 "/dev " 上之后,对该目录上的操作如何转化为对 ext2 文件系统相应的操作。从图 5上看到,对 sys_mount() 函数的调用并没有直接改变 "/dev " 目录所对应的 inode (即图中的 new_inode变量)结构中的 i_op 和 i_fop 指针,而且 "/dev " 所对应的 dentry(即图中的 new_dentry 变量)结构仍然在 VFS 的目录树中,并没有被从其中隐藏起来,相应地,来自 hda2 上的 ext2 文件系统的根目录所对应的 e2_entry 也不是如当初笔者所想象地那样将 VFS 目录树中的 new_dentry 取而代之,那么这之间的转化到底是如何实现的呢?
请读者注意下面的这段代码:
while (d_mountpoint(dentry) && __follow_down(&nd->mnt, &dentry)); |
这段代码在 link_path_walk() 函数中被调用,而 link_path_walk() 最终又会被 path_lookup() 函数调用,如果读者阅读过 Linux 关于文件系统部分的代码,应该知道 path_lookup() 函数在整个 Linux 繁琐的文件系统代码中属于一个重要的基础性的函数。简单说来,这个函数用于解析文件路径名,这里的文件路径名和我们平时在应用程序中所涉及到的概念相同,比如在 Linux 的应用程序中 open 或 read 一个文件 /home/windfly.cs 时,这里的 /home/windfly.cs 就是文件路径名,path_lookup() 函数的责任就是对文件路径名中进行搜索,直到找到目标文件所属目录所对应的 dentry 或者目标直接就是一个目录,笔者不想在有限的篇幅里详细解释这个函数,读者只要记住 path_lookup() 会返回一个目标目录即可。
上面的代码非常地不起眼,以至于初次阅读文件系统的代码时经常会忽略掉它,但是前文所提到从 VFS 的操作到实际文件系统操作的转化却是由它完成的,对 VFS 中实现的文件系统的安装可谓功不可没。现在让我们仔细剖析一下该段代码: d_mountpoint(dentry) 的作用很简单,它只是返回 dentry 中 d_mounted 成员变量的值。这里的dentry 仍然还是 VFS 目录树上的东西。如果 VFS 目录树上某个目录被安装过一次,那么该值为 1。对VFS 中的一个目录可进行多次安装,后面会有例子说明这种情况。在我们的例子中,"/dev" 所对应的new_dentry 中 d_mounted=1,所以 while 循环中第一个条件满足。下面再来看__follow_down(&nd->mnt, &dentry)代
码做了什么?到此我们应该记住,这里 nd 中的 dentry 成员就是图 5 中的 new_dentry,nd 中的 mnt成员就是图 5 中的 mnt,所以我们现在可以把 __follow_down(&nd->mnt, &dentry) 改写成__follow_down(&mnt, &new_dentry),接下来我们将 __follow_down() 函数的代码改写(只是去处掉一些不太相关的代码,并且为了便于说明,在部分代码行前加上了序号)如下:
static inline int __follow_down(struct vfsmount **mnt, struct dentry **dentry) { struct vfsmount *mounted; [1]mounted = lookup_mnt(*mnt, *dentry); if (mounted) { [2]*mnt = mounted; [3]*dentry = mounted->mnt_root; return 1; } return 0; } |
代码行[1]中的 lookup_mnt() 函数用于查找一个 VFS 目录树下某一目录最近一次被 mount 时的安装区域块的指针,在本例中最终会返回图 5 中的 e2_mnt。至于查找的原理,这里粗略地描述一下。记得当我们在安装 ext2 文件系统到 "/dev" 时,在后期会调用 graft_tree() 函数,在这个函数里会把图 5 中的安装区域块指针 e2_mnt 挂到一 hash 表(Linux 2.4.20源代码中称之为 mount_hashtable)中的某一项,而该项的键值就是由被安装点所对应的 dentry(本例中为 new_dentry)和 mount(本例中为 mnt)所共同产生,所以自然地,当我们知道 VFS 树中某一 dentry 被安装过(该 dentry 变成为一安装点),而要去查找其最近一次被安装的安装区域块指针时,同样由该安装点所对应的 dentry 和 mount 来产生一键值,以此值去索引 mount_hashtable,自然可找到该安装点对应的安装区域块指针形成的链表的头指针,然后遍历该链表,当发现某一安装区域块指针,记为 p,满足以下条件时:
(p->mnt_parent == mnt && p->mnt_mountpoint == dentry) |
P 便为该安装点所对应的安装区域块指针。当找到该指针后,便将 nd 中的 mnt 成员换成该安装区域块指针,同时将 nd 中的 dentry 成员换成安装区域块中的 dentry 指针。在我们的例子中,e2_mnt->mnt_root成员指向 e2_dentry,也就是 ext2 文件系统的 "/" 目录。这样,当 path_lookup() 函数搜索到 "/dev"时,nd 中的 dentry 成员为 e2_dentry,而不再是原来的 new_dentry,同时 mnt 成员被换成 e2_mnt,转化便在不知不觉中完成了。
现在考虑一下对某一安装点多次安装的情况,同样作为例子,我们假设在 "/dev" 上安装完一个 ext2文件系统后,再在其上安装一个 ntfs 文件系统。在安装之前,同样会对安装点所在的路径调用path_lookup() 函数进行搜索,但是这次由于在 "/dev" 目录上已经安装过了 ext2 文件系统,所以搜索到最后,由 nd 返回的信息是:nd.dentry = e2_dentry, nd.mnt = e2_mnt。由此可见,在第二次安装时,安装点已经由 dentry 变成了 e2_dentry。接下来,同样地,系统会再分配一个安装区域块,假设该安装区域块的指针为 ntfs_mnt,区域块中的 dentry 为 ntfs_dentry。ntfs_mnt 的父指针指向了e2_mnt,mnfs_mnt 中的 mnt_root 指向了代表 ntfs 文件系统根目录的 ntfs_dentry。然后,系统通过 e2_dentry和 e2_mnt 来生成一个新的 hash 键值,利用该值作为索引,将 ntfs_mnt 加入到 mount_hashtable 中,同时将 e2_dentry 中的成员 d_mounted 值设定为 1。这样,安装过程便告结束。
读者可能已经知道,对同一安装点上的最近一次安装会隐藏起前面的若干次安装,下面我们通过上述的例子解释一下该过程:
在先后将 ext2 和 ntfs 文件系统安装到 "/dev" 目录之后,我们再调用 path_lookup() 函数来对"/dev" 进行搜索,函数首先找到 VFS 目录树下的安装点 "/dev" 所对应的 dentry 和 mnt,此时它发现dentry 成员中的 d_mounted 为 1,于是它知道已经有文件系统安装到了该 dentry 上,于是它通过 dentry 和 mnt 来生成一个 hash 值,通过该值来对 mount_hashtable 进行搜索,根据安装过程,它应该能找到 e2_mnt 指针并返回之,同时原先的 dentry 也已经被替换成 e2_dentry。回头再看一下前面已经提到的下列代码: while (d_mountpoint(dentry) && __follow_down(&nd->mnt, &dentry)); 当第一次循环结束后, nd->mnt 已经是 e2_mnt,而 dentry 则变成 e2_dentry。此时由于 e2_dentry 中的成员 d_mounted 值为 1,所以 while 循环的第一个条件满足,要继续调用 __follow_down() 函数,这个函数前面已经剖析过,当它返回后 nd->mnt 变成了 ntfs_mnt,dentry 则变成了 ntfs_dentry。由于此时 ntfs_dentry 没有被安装过其他文件,所以它的成员 d_mounted 应该为 0,循环结束。对 "/dev" 发起的 path_lookup() 函数最终返回了 ntfs 文件系统根目录所对应的 dentry。这就是为什么 "/dev" 本身和安装在其上的 ext2 都被隐藏的原因。如果此时对 "/dev" 目录进行一个 ls 命令,将返回安装上去的 ntfs 文件系统根目录下所有的文件和目录。