首页 > 代码库 > VFS (2)
VFS (2)
我借用一张图来表示首个文件系统挂载上去后它们之间的关系。
内核首先挂载的文件系统为 rootfs,它的重要数据结构之间的关系在上图中已经非常清晰了,它有自己的根目录,假设我样要在这个文件系统的根目录下创建一个 /dev 目录,那么之后,各数据结构之间的关系如下图:
因为新生成了一个目录,所以会有一个 new_inode,并且会有一个 new_dentry,新的目录项结构名字叫 "dev", 它的 d_parent 即父目录为 dentry, 即刚才的根目录 "/", 现在有了 /dev,我们要在这个目录上挂载一个实际的文件系统,ext4,那么挂载之后,它们之间的结构关系如下图:
因为是一个新的文件系统,所以它会有自己的 super block (e2_sb), 因为它是挂载到 rootfs 的 /dev 目录下,所以会产生一个挂载结构体 vfsmount (e2_mnt), 并且 e2_mnt->mnt_mountpoint 指向它所挂载的目录,new_dentry,然后设置了 new_dentry->d_mounted 为 1,它还产生了自己的根目录 e2_entry 以及该目录项的结点对象 e2_inode。
有了这样的结构后,假设要访问新文件系统中的一个文件 /dev/tmp/file 时,首先,经过路径遍历,会得到 rootfs 的 /dev 目录的目录项结构 new_dentry,结果发现它的 d_mounted 为 1,表明有其它文件系统挂载到该目录下面,即 ext4, 此时通过某种算法,得到挂载到该目录项下的 e2_mnt,然后得到 e2_mnt->mnt_root,即新文件系统的根目录,然后就可以接着继续查找,整个过程衔接的天衣无缝。
那么你可能要问了,那 rootfs 挂载到哪里呢?也就是根目录项是怎么创建的呢 ?
这确实是一个 egg first or chicken first 的问题,因为 rootfs 是一个内存文件系统,在内核加载时,通过 fs/namespace.c 中的 mnt_init 最后的 init_rootfs() 及 init_mount_tree() 来初始化。在 init_rootfs 中会注册 rootfs 文件系统,在 init_mount_tree 来挂载它。让源码来告诉我们真相。
在 init_mount_tree() 中会调用 do_kern_mount("rootfs", 0, "rootfs", NULL) 来挂载 rootfs,核心方法是 vfs_kern_mount, 它会创建 vfsmount 结构体来表示一个挂载点,然后会调用,注册 rootfs 时,file_system_type 中的 get_sb 来让 rootfs 文件系统驱动自己来产生一个 super block 来填充到 vfsmount 中,(这里可以看出 VFS 的灵活性,因为某些文件系统可能没有 super block ,所以当文件系统自己实现该功能,就可以虚拟一个超级块),奥秘就在这里。对于 rootfs 中的 get_sb 其实是由函数 rootfs_get_sb 来实现的,核心函数为 get_sb_nodev, 它会创建一个 super block,并且调用 fill_super 函数指针,在这里它是 ramfs_fill_super 来实现。代码如下:
static int ramfs_fill_super(struct super_block * sb, void * data, int silent) { struct inode * inode; struct dentry * root; sb->s_maxbytes = MAX_LFS_FILESIZE; sb->s_blocksize = PAGE_CACHE_SIZE; sb->s_blocksize_bits = PAGE_CACHE_SHIFT; sb->s_magic = RAMFS_MAGIC; sb->s_op = &ramfs_ops; sb->s_time_gran = 1; inode = ramfs_get_inode(sb, S_IFDIR | 0755, 0); if (!inode) return -ENOMEM; root = d_alloc_root(inode); if (!root) { iput(inode); return -ENOMEM; } sb->s_root = root; return 0; }它会初始化刚才的 super block,并且创建一个 inode,和一个 dentry (root),并且该 sb->s_root = root; 即 rootfs 的根目录。那么这里还是没有告诉该文件系统挂载到哪里呀,但 rootfs 的重要的数据结构都已经明了了。接着看,回到 get_sb_nodev, 它会调用 simple_set_mnt。
int simple_set_mnt(struct vfsmount *mnt, struct super_block *sb) { mnt->mnt_sb = sb; mnt->mnt_root = dget(sb->s_root); return 0; }它会设置挂载点的根目录,那么路径遍历时,当遇到目录被挂载时,找到挂载点,就知道挂载的文件系统的根目录了。
再回去到 vfs_kern_mount 中,当超级块得到后,开始设置挂载点结构体的时候,
mnt->mnt_mountpoint = mnt->mnt_root;
mnt->mnt_parent = mnt;
这两句重要的代码解开了真相,它又设置了挂载结构体的所挂载的目录时,设置为了自己的根目录,也就是说,在 rootfs 中创建的根目录,既作为它的根目录,也作为它的挂载目录,即 VFS 的根目录,这样它就成功地成为了第一个 VFS 的根了。
此时,VFS 中的根及第一个文件系统已经初始化好了,但一般我们还会挂载一个根文件系统,好让 Linux 能够真正跑起来,假设挂载的为一个包含 ext4 的磁盘的首分区,在笔者的源码中,它会先挂载到 rootfs 的 /root 下面,有了 rootfs,这一切都非常简单,加载完 ext4 之后,设置了初始化任务的当前目录为 ext4 的根目录,相当于前面说的 e2_entry, 这些工作主要是在 prepare_namespace 里面完成的。设置了初始任务的当前目录后,但是没有设置根目录,根目录的设置是由 prepare_namespace 中下面代码实现的,
mount_root();
sys_mount(".", "/", NULL, MS_MOVE, NULL);
sys_chroot(".");
mount_root 加载了根文件系统到 /root 下,即 ext4,但随后又移到了 VFS 的根目录,及 rootfs 的根目录下,覆盖了 rootfs,然后又设置了初始任务的根目录为当前目录,即 e2_entry,即 ext4 的根目录,相当于 VFS 的根目录。因为如果不移动根文件系统,设置初始任务的当前目录和根目录为 /root,那么派生进程如果继承了这两个,它们通过普通的 /bin/xxx 也是可以找到常规的 linux 目录的,但万一未继承到这两个,或者任务 0的这两项未正确设置,那么再用 /bin/xxx 遍历时,其实是从 rootfs 的根遍历的,这样就会出现问题。
还有 VFS 之所有灵活,是因为很多操作都是以指针的形式,提供给文件系统开发者来实现,这样,就最大化的提供了灵活性,有兴趣的朋友可以查看 open mkdir read 等接口,来感受它的强大。
VFS 还是相对复杂的,两篇文章只是提到了很小的一部分,如果有兴趣,源码中都有你想知道的细节。
VFS (2)