欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

linux文件体系结构和虚拟文件系统

程序员文章站 2022-03-09 10:38:24
...

        linux中的虚拟文件系统(Virtual File System, VFS)是一种采用面向对象编程策略(尽管书写操作系统的C语言本身不支持面向对象编程,但是思想还是可以借鉴的),是对该操作系统所支持的所有实际物理文件系统的一种抽象,用于提供给用户进程或者系统调用操作当前系统中所有文件统一的接口,而不用关心当前操作系统中存在哪几种实际的物理文件系统。也可以这么说VFS实际提供了面向对象编程中的一种接口(或者抽象类),而所有的实际物理文件系统需要实现接口所定义的接口(或者使用抽象类中的缺省实现)。

        在用户应用程序中或者通过C标准库提供的操作文件的接口或者直接使用系统调用操作文件系统时,使用的是VFS提供的一种抽象接口。而系统调用就像是一种切换器,是进程从用户空间进入内核空间的入口,之后由内核代表用户进程运行在该进程的系统空间的堆栈中(在创建进程的时候,内核会在内核空间为每个进程创建两个内存页(每页4K或者8K)或者一个系统页、一个中断页)。

        很明显,VFS屏蔽了底层物理文件系统的实现细节,只对外提供一组操作系统中文件的统一入口。因此,即使各个底层物理文件系统之间的差别再大,对于用户而言也是透明的。每种底层的物理文件系统只需对VFS系统提供各自的接口,供VFS系统调用即可。

        与VFS相关的所有数据结构只存在于物理内存中。所以系统每次初始化的时候都需要首先在内存中建立起一颗VFS的目录树(在linux中成为namespace),实际上便是在物理内存中建立相应的数据结构。 VFS目录树在linux的文件系统模块中是个很重要的概念,不能与实际文件系统相混淆。

        linux中文件系统的组织和系统调用之间的结构图如下:


                               linux文件体系结构和虚拟文件系统

图 1 VFS系统结构图

        buffer cache在这里的作用就是缓冲区高速缓存,用于减小对实际物理存储设备之间的请求次数,加快应用程序的响应速度。内核使用LRU(最近最少使用算法)管理缓冲区中缓存。当缓冲区中的脏数据达到一定的比例、距离上次一周期刷新的时间间隔超过一定的数值时,内核自动唤醒flusher后台进程刷新内存中的脏数据回实际的后备存储空间。具体的介绍可以参见《linux内核设计与实现》页高速缓存和页回写一章。

        如前所述,linux站在高度抽象的角度看待本系统中的文件系统和针对文件系统的相关操作。linux使用超级块、文件、目录项和安装点四个概念管理系统的所有文件及目录。

        超级块在每个每个文件系统的根目录上,是针对特定的文件系统的数据结构,用于描述和维护每个文件系统的状态。不同的文件系统可以通过超级块形成对外的统一抽象。

        文件(inode)用于表示文件系统中的每个文件或者目录,管理文件系统中的每个对象。inode其实包含了管理文件系统中每个对象所需要的所有元数据(包含可以在该对象上执行的操作),但是inode不包含该文件的文件名。inode是通过dentry结构体的d_inode指针指向的。

        目录项(dentry)用于实现文件名称和inode之间的映射,dentry同时维护目录和文件之间的关系,从而支持在文件系统中移动。

        inux对于最近经常使用的inode文件和dentry目录项,操作系统内核各为其维护一个缓存,用于快速实现最近经常使用的文件和目录项的查找。

虚拟文件系统层

        VFS作为文件系统的接口的根层,记录当前支持的文件系统和挂载的文件系统。

虚拟文件系统的注册

        VFS的文件系统可以静态或者动态的添加到系统中。如果是静态的方式添加,那么需要在编译内核的时候确定内核所支持的文件系统类型,并在系统初始化的时候通过内嵌的函数调用在VFS中进行注册。如果是动态的添加,则把相应的文件系统当做一个模块,利用模块的加载和卸载特征向注册表登记类型或者从注册表中注销掉该系统类型,此时需要调用int register_filesystem(struct file_system_type *fs)。这里需要引用一个file_system_type结构体,该结构体包含了一个文件系统的名称以及一个指向相应的VFS超级块读取例程的地址,具体结构体如下所示。所有的已经注册的file_system_type将会形成一个注册链表,如图2所示。无论是可加载的模块还是静态注册模块,都是用register_filesystem进行注册。
struct file_system_type {
    //文件系统的名字;
    const char *name; 
    //文件系统的标志,bitmap;         
    int fs_flags;
    //在安装文件时,调用get_sb()从磁盘中读取超级块;
    int (*get_sb)(struct file_system_rype *,  int,  const char* ,  void *,  struct vfsmount *);
    //卸载文件系统时,调用该函数完成一些清理工作;
    void (*kill_sb)  (struct super_block *);
    //模块的拥有者,如果是静态编译进内核的,那么该字段为NULL;
    struct module *owner;
    //形成文件系统类型链表;
    struct file_system_type_type *next;
    //同一种文件类型的超级块形成一个链表,fs_supers是这个链表的头;也就是说该域指向所有使用该文件类型的超级块;
    struct list_head  fs_supers;
    //相关的锁;
    struct lock_class_key s_lock_key;
    struct lock_class_key s_umount_key;
};
linux文件体系结构和虚拟文件系统
图 2 file_system_type形成的链表

        在图2中的file_systems指针是定义在fs/filesystems.c文件中的全局变量,指向第一个file_system_type。其定义如下:static struct file_system_type *file_systems。如果要对文件系统形成的链表进行写操作(加载新的文件类型或者卸载文件类型,那么应该加锁)。如下所示。

//寻找特定的文件系统;
static struct file_system_type **find_filesystem(const char *name, unsigned len)
{
    struct file_system_type **p;
    for (p=&file_systems; *p; p=&(*p)->next)
    if (strlen((*p)->name) == len &&  strncmp((*p)->name, name, len) == 0)
        break;
    return p;
}                 
//加载新的文件系统到系统内核;
int register_filesystem(struct file_system_type * fs)
{
    int res = 0;
    struct file_system_type ** p;
    BUG_ON(strchr(fs->name, '.'));
    if (fs->next)
        return -EBUSY;

    //加锁;
    write_lock(&file_systems_lock);
    p = find_filesystem(fs->name, strlen(fs->name));
    if (*p)
        res = -EBUSY;
    else
        *p = fs;
    //释放锁
    write_unlock(&file_systems_lock);

    return res;
}
//从内核中卸载文件系统
int unregister_filesystem(struct file_system_type * fs)
{
    struct file_system_type ** tmp;
    //加锁;
    write_lock(&file_systems_lock);
    tmp = &file_systems;
    while (*tmp) {
        if (fs == *tmp) {
            *tmp = fs->next;
            fs->next = NULL;
            write_unlock(&file_systems_lock);
            synchronize_rcu();

            return 0;

        }

        tmp = &(*tmp)->next;
    }
    //释放锁;
    write_unlock(&file_systems_lock);
    return -EINVAL;
}

虚拟文件系统的安装

        以上只是完成了文件系统的注册,为了真正的能够使用文件进行读、写,需要将注册的文件进行安装。除了根文件系统是在内核导引阶段被安装之外,其他文件或者由初始化脚本安装或者由用户使用mount命令安装在已经安装的文件系统的某一个目录上。也就是说根系统的安装是在系统的初始化阶段进行安装的,此时对应的目录的为'/',该目录为其他文件系统的安装提供了落脚点。以后所有的文件系统都需要安装在'/'或者其子目录下,从而逐渐在内存中建立起VFS目录树。建立VFS目录树的本质就是初始化、并建立一系列的vfsmount、dentry、inode和super_block,并将他们通过指针链接起来。

        每个文件系统都有自己的根目录,安装文件系统的那个目录称为安装点。

        在众多的文件系统中,我们重点说明一下VFS中重要的一个文件系统---rootfs,也称为根系统。该文件系统是VFS存在的基础,它为其他文件系统提供了一个作为初始安装点的空目录。以后安装的文件系统都是挂载在该文件系统提供的空目录下的。rootfs的注册过程是内核初始化的一个不可缺少的阶段。其对应的file_system_type为

static struct file_system_type rootfs_fs_type =  {
    .name = "rootfs",
    .mount = rootfs_mount,
    .kill_sb = kill_litter_super,
};

        然后,系统通过调用init_mount_tree安装该目录。

        关于根文件系统的安装和普通文件系统的安装,还是要参考文章末尾的比较好的一篇博客。这里只是说一下几个结构体建立的顺序。

        1.首先建立安装点的结构体vfsmount,并初始化其部分成员变量;

        2.通过file_system_type中的get_sb指向的例程建立相应的超级块,并将超级块挂在到file_system_type中的fs_super域,形成该种文件类型的一个链表;

        3.建立inode结构体,并初始化i_op和i_fop两个域,用于指向针对该文件的操作函数,i_sb域指向超级块(如果是同一个文件系统,则不需要重新建立超级块,此时指向根目录下的超级块即可);

        4.建立目录项的结构体dentry,并将d_inode域指向刚建立的inode;并将d_sb指向刚建立的超级块(如果是同一个文件系统,则不需要重新建立超级块,此时指向根目录下的超级块即可);

        前面说过每种文件类型有对应的struct  file_system_type,无论存在该种文件类型的多少个实例化实例,都会有且仅有一个file_system_type结构体。但是安装文件系统则不同,管它有零个或者是多个实例安装到系统中,每当一个文件系统被安装就会实例化一个vfsmount结构体实例。其代表该文件系统的一个安装实例,也代表了该文件系统的一个安装点。vfsmout对应的结构结构体如下。

struct vfsmount  {
    struct list_head mnt_hash;
    struct vfsmount *mnt_header;
    //挂载点目录;
    struct dentry *mnt_mountpoint;
    struct dentry *mnt_root;
    struct super_block *mnt_sb;
    struct list_head mnt_mounts;
    struct list_head mnt_child;
    atomic_t mnt_count;
    int mnt_flags;
    char*  mnt_devname;
    //所有挂载系统通过mnt_list形成一个链表;
    struct list_head mnt_list;
};
        所有的vfs挂载点通过mnt_list双链表挂在于namespace->list链表中,如图3所示。该mnt命名空间可以通过任意进程获取,之所以能通过任何进程获取该namespace,是因为在系统建立VFS目录树的时候,在init_mount_tree函数中的最后两步会对系统的第一个进程init_task建立其进程数据块中的namespace域。所以所有以后由该进程派生出来的进程都先天的继承该域,因此该域可以通过任意的进程获取。current始终指向当前的进程,所以可以获取。在init_mount_tree函数中建立namespace域的过程如下:
namespace  =  kmalloc(sizeof(*namespace),  GFP_KERNEL);
list_add(&mnt->list,  &namespace->list);  //mnt是由do_kern_mount返回的值;
namespace->root  =  mnt;
init_task.namespace =  namespace;
for_each_task(p)  {
    get_namespace(namespace);
    p->namespace =  namespace;
}
set_fs_pwd(current->fs,  namespace->root,  namespace->root->mnt_root);
set_fs_root(current->fs,  namespace->root,  namespace->root->mnt_root);

        最后两行就是将do_kern_mount()函数建立的mnt和dentry信息记录在当前进程的fs的结构中。

        子vfsmount挂载点通过结构挂载于父vfsmount的mnt_child链表中,并且mnt_parent直接执行父亲fs的vfsmount结构,从而形成层次结构;

        vfs的super_block指向实际挂载的文件系统对应的超级块;

linux文件体系结构和虚拟文件系统
图 3 挂在文件系统文件列表
        关于linux下虚拟文件系统的注册和安装更详细的讲解,可以参考博文

        关于虚拟文件系统中各个主要的数据结构之间如何通过指针相互关联起来,建立起VFS系统的大致的结构框架,参考博文。          在这篇文章中,坐着将VFS系统中各个主要的数据结构之间的关系用图的描述出来,对于了解各个主要的数据结构的各个域具有非常好的帮助作用。欢迎吐槽。