VFS

来源:互联网 发布:linux用户修改默认目录 编辑:程序博客网 时间:2024/06/05 18:52

VFS 

http://blog.csdn.net/henzox/article/details/42777471

        一切皆为文件,在类 unix 系统中,这一思想影响了一代操作系统,作为著名的 Linux,如何实现这一思想,如何让市面上所有的文件系统能够完美兼容地运行在 Linux 系统中,这背后显然隐藏着一系统复杂的机制,本文将带你窥探其中的奥秘。
        假设你是一个 Linux 使用者,你应该知道要想使用一个 U 盘里面的文件,需要系统支持你 U 盘里的操作系统,并且你还需要把 U 盘这个设备 mount 到现有的一个目录下面,其实这里面就牵涉到几个概念,首先,要有文件系统,那么就有一块数据需要来表明这些信息,这就是 super block, 其实有目录结构,在内核就它就是 dentry,还要有文件 inode,以及挂载到 dentry 上的一个挂载信息来表明,该目录下被挂载了一个文件系统。这几个重要的数据结构便是组成了 VFS 整体动作的基础。

        下图1,展示了 file, dentry, inode, super block 之间的关系。


        图解:
        Process1 打开文件硬链接 /home/Henzox/file1, 该硬链接表示的文件为 /home/Henzox/file;
        Process2 打开文件硬链接 /home/Henzox/file1,同上;
        Process3 打开文件 /home/Henzox/file。

        每次调用 open 打开文件,就会得到一个文件对象,它们记录了本次打开各自的信息,最重要的信息莫过于文件指针位置,即使是相同的文件,每次打开操作,都会有自己的操作位置,所以会有三个 File object, 而由于 /home/Henzox/file1 是 /home/Henzox/file 的硬链接,所以它们在磁盘上相当于同一个文件,所以三个进程其实是在操作同一个文件,帮只有一个 inode object,而 Process1 和 Process2 是使用相同的路径打开的,所以它们共享的是一个 dentry object,因为 dentry 代表一个目录项结构,目录项,故名思义,即可以是目录,也可以是文件,因为目录也被当作文件来处理。而一个文件系统就会有一个超级块来表示该文件系统下各结构体的信息,所以 inode 会指向它所在的文件系统的超级块对象。
        那么现在这几个结构体之间的关系就大致明白了。


        上一篇中已经介绍了,VFS 中几个重要的数据结构,下面我们来看一下,当有其它文件系统挂载到以上目录时,会是一个什么情况呢?

        我借用一张图来表示首个文件系统挂载上去后它们之间的关系。


        内核首先挂载的文件系统为 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 来实现。代码如下:

[cpp] view plaincopy
  1.    static int ramfs_fill_super(struct super_block * sb, void * data, int silent)  
  2. {  
  3.     struct inode * inode;  
  4.     struct dentry * root;  
  5.   
  6.     sb->s_maxbytes = MAX_LFS_FILESIZE;  
  7.     sb->s_blocksize = PAGE_CACHE_SIZE;  
  8.     sb->s_blocksize_bits = PAGE_CACHE_SHIFT;  
  9.     sb->s_magic = RAMFS_MAGIC;  
  10.     sb->s_op = &ramfs_ops;  
  11.     sb->s_time_gran = 1;  
  12.     inode = ramfs_get_inode(sb, S_IFDIR | 0755, 0);  
  13.     if (!inode)  
  14.         return -ENOMEM;  
  15.   
  16.     root = d_alloc_root(inode);  
  17.     if (!root) {  
  18.         iput(inode);  
  19.         return -ENOMEM;  
  20.     }  
  21.     sb->s_root = root;  
  22.     return 0;  
  23. }  
        它会初始化刚才的 super block,并且创建一个 inode,和一个 dentry (root),并且该 sb->s_root = root; 即 rootfs 的根目录。那么这里还是没有告诉该文件系统挂载到哪里呀,但 rootfs 的重要的数据结构都已经明了了。接着看,回到 get_sb_nodev, 它会调用 simple_set_mnt。


[cpp] view plaincopy
  1. int simple_set_mnt(struct vfsmount *mnt, struct super_block *sb)  
  2. {  
  3.     mnt->mnt_sb = sb;  
  4.     mnt->mnt_root = dget(sb->s_root);  
  5.     return 0;  
  6. }  
        它会设置挂载点的根目录,那么路径遍历时,当遇到目录被挂载时,找到挂载点,就知道挂载的文件系统的根目录了。
        再回去到 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 还是相对复杂的,两篇文章只是提到了很小的一部分,如果有兴趣,源码中都有你想知道的细节。

0 0
原创粉丝点击