linux内核之虚拟文件系统总结

发表于 2016-03-20 | 分类于 Linux内核 |

文章系原创,如需转载请注明:转载自”Blog of UnicornX” (http://unicornx.github.io/)

最新更新于:2016-04-21

总结得还是有点粗糙,但基本思路和涉及的内核主要数据结构都到位了。以后再慢慢改进吧。

主要参考:

  • LKD3rd
  • Linux 文件系统剖析
  • LINUX VFS精华版PPT
  • 内核代码v2.6.32.2
  • 关于VFS文件系统中的superblock、inode、d_entry和file数据结构 开头的几张图开启了我的思路
  • vfs,superblock,inode,dentry,file结构体图解 主要参考的上面的图,其基于的代码有点老,但和我主要参考的v2.6.32.2的内核代码差别不是很大S
  • VFS文件系统结构分析 几张图和例子真心不错。
  • linux内核之文件系统 貌似有点老,基于2.4内核

VFS的定义

Linux下的文件系统中宏观上主要分为三层:

  • 一是上层的文件系统的系统调用;
  • 二是虚拟文件系统VFS(Virtual File System)层,
  • 三是挂载到VFS中的各种实际文件系统。

linux内核之虚拟文件系统总结_第1张图片

这个下面的是另外一张类似的图解,表达的应该是相同的意思:

linux内核之虚拟文件系统总结_第2张图片

上面的图主要想要表达的意思是:VFS作为一个中间层,主要的作用是提供了一种软件机制,对上层应用屏蔽底层不同的调用方法,提供一套统一的调用接口;对下层的不同的文件系统,实现一种组织管理和映射,映射的实现可以理解为并抽象和定义了一套规范的行为要求不同的文件系统来适配并实现,类似于OOP中的虚函数或者Java中的接口类的意思。因此,VFS其实就是文件系统组织管理中的一个抽象层。

VFS层由四个主要部件构成

  • superblock(超级块):代表一个特定的外存设备上的文件系统
  • inode:文件系统中管理的每个对象(文件或目录)在Linux中表示为一个inode,inode包含管理文件系统中的对象所需的所有元数据(包括可以在对象上执行的操作)。
  • dentry:用来实现名称和 inode 之间的映射,有一个目录缓存用来保存最近使用的 dentry。dentry 还维护目录和文件之间的关系,从而支持在文件系统中移动。
  • file:从进程的角度维护一个打开的文件,(保存打开的文件的状态,比如写偏移量等等)。

以上组成元素只存在于内存中,每次系统初始化期间Linux都会先在内存中构造一棵VFS的目录树(也就是源码中的namespace)。注意VFS作为一个虚拟的文件系统,其存在于内存中,一旦关机后就不复存在,而实际的文件系统(不考虑虚拟文件系统,譬如sysfs或者proc等),即我们常说的ext,ntfs等常见的磁盘文件系统存在于硬盘上,或者叫外存上。为方便区别,后面凡是提到”文件系统”指的是外存上的,特指磁盘上的一个分区。而VFS我们叫它虚拟文件系统,则表示其存在于内存中。

上面说的VFS的组成,譬如superblock,inode,dentry等概念其实都来自Ext这种文件系统的组织方式,所以两者非常相似(毕竟Ext是Linux的原生文件系统)。所以如果VFS映射的是ext文件系统,很多对应关系是一对一的,操作速度会非常快,而如果映射的是其他的文件系统,转换的效率就可想而知了。后面的内容在涉及到实际的文件系统时为方便起见,都是以ext文件系统为例。

VFS组成的详细介绍

先看一张实际的磁盘文件系统ext组织磁盘上的数据的方式,

linux内核之虚拟文件系统总结_第3张图片

这张图告诉我们在磁盘(外存)上:

  • 一个磁盘由一个或者多个分区组成,每个分区对应着一个文件系统。
  • 一个文件系统,由自举块(存放引导程序),超级快(存放该文件系统的全局信息),以及多个柱面组构成。
  • 每个柱面组包括了超级快副本(用于冗余备份),配置信息,节点位图,数据块位图,节点区和数据块区。注,为了避免混用inode的问题,这里把外存上的inode叫做节点,而VFS在内存中构造的叫inode。
  • 节点区由很多节点构成,每个节点存放一个文件的元数据(metadata),元数据主要指的是文件的一些属性信息,例如:文件大小,设备标识符,用户标识符,用户组标识符,文件模式,扩展属性,文件读取或修改的时间戳,链接数量,文件分类等等。而该文件的实际内容部分则以数据块的形式存放在数据块区中。
  • 数据块区由多个数据块和目录块构成,数据块存放的是上面提到的一个文件的实际内容,比如一张照片的信息;目录块存放的是目录项的名称信息,譬如一个全路径/home/fs里面包括两个路径分量homefs对应两个目录项,分别在两个目录块中存放homefs的信息。

注意,以上描述针对的是物理磁盘上的信息。而这张图最下面画的i节点数组,以及目录块所对应的i节点号+文件名实际上应该对应的是VFS在内存中创建的inodedentry的缓存。我们也从图中可以看到每个dentry,其文件名来自数据块区中的目录块的内容,而其i节点号指向实际的inode,这应该表达的就是一种硬链接的概念。

因为我们说过,VFS的实现就是以ext文件系统为范本的,所以在内存中的VFS长的样子大致也是如此。譬如VFS在内存中,有super_block对象对应外存上的超级快,inode对应外存上的节点,dentry对应外存上的目录块。只不过对于VFS来说:实际的数据还是放在物理磁盘上,内存中的数据是为了访问方便,特别地,节点目录块不会全部加载到内存中,只会对部分当前正在频繁操作的节点目录块会以inodedentry的形式缓存在内存中以加快访问速度。具体参考上面的第二张图中的cache。

超级快

超级块,是每个FS最基本的元数据,它保存了文件系统的类型、大小、状态和其他信息等。超级快对于文件系统是非常关键的,因此一般文件系统都会冗余存储多份(参考上图)。

VFS中的一个超级块对象可以认为就对应着一个实际的FS。用结构体类型super_block表示,定义在文件中。更多的操作函数,参考fs/super.c

struct super_block {
    struct list_head    s_list;        /* Keep this first */
    ...
    const struct super_operations    *s_op;
    ...
    struct list_head    s_inodes;    /* all inodes */
    ...
    struct list_head    s_instances;
    ...
};

有关超级块,只摘录了几个我认为比较重要的成员供参考理解,总结如下:

s_list和超级快链表

系统中所有(挂载)的FS,即所有的超级快对象通过一个链表维护,参见fs/super.c

LIST_HEAD(super_blocks);

s_list这个成员就是用来串联所有的超级块对象的,见下图。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CfNW5nYg-1589775803222)(http://img.dnbcw.info/2010125/pxup2592610.gif)]

s_instances和文件系统类型

对于特定的文件系统类型, 属于同一种文件系统类型的所有的超级快对象也通过一个链表串联起来。

提到这里,需要知道VFS中还定义了一个结构体类型file_sytem_type,用来表示一种文件系统类型,参考``。

struct file_system_type {
    const char *name;
    ...
    int (*get_sb) (struct file_system_type *, int,
               const char *, void *, struct vfsmount *);
    void (*kill_sb) (struct super_block *);
    struct module *owner;
    struct file_system_type * next;
    struct list_head fs_supers;

    ...
};

其中:

  • name: 文件系统类型的名字
  • get_sb()/kill_sb(): 虚函数,由具体的文件系统实现,get_sb()在文件系统被安装时被调用,用于从磁盘上读取超级快,并在内存中组装为super_block对象。kill_sb()则是对应的析构函数。
  • owner: 文件系统模块,所以说内核支持文件系统作为一个象驱动模块一样的组件,可以动态安装。
  • next:指向下一个文件系统类型的指针。具体参考下图所示。
  • fs_supers: 属于同一个文件系统类型的FS,也就是超级块都挂在这个链表上。和super_blocks_instances是对应的关系。

内核维护了一个全局的链表,用于保存当前支持的文件系统类型的列表,定义在fs/filesystems.c

static struct file_system_type *file_systems;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S2QaUZ7i-1589775803224)(http://img.dnbcw.info/2010125/pxup2592611.gif)]

注意LKD3rd上有这么一句话:

There is only one file_system_type per filesystem, regardless of how 
many instances of the filesystem are mounted on the system, or whether
the filesystem is even mounted at all.

在Linux中添加新文件系统类型的方法是调用register_filesystem()。删除文件系统可以调用unregister_filesystem()

另外在命令行上输入cat /proc/filesystems就可以查看这个文件系统类型的列表。

s_op和超级快操作

这个也是超级快中最重要的一个域,它指向超级块的操作函数表。由结构体super_operations表示,定义在文件``中。这里依然是虚函数的概念,这些回调函数由各个具体的文件系统实现,VFS会在合适的时间点上调用它们,针对实际的FS操作其超级块和文件节点对象。

每一个具体回调函数的意义,查看LKD3第13.6章节的内容。

s_inodes

该超级块中当前操作的inodes对象,采用一个链表维护在这里,具体参考后面有关文件节点inode的总结。

索引节点inode

inode包含了一个文件的元数据,要理解所谓元数据的概念,

  • 首先要注意数据分成:元数据+数据本身两部分。元数据主要指的是文件的一些属性信息,例如:文件大小,设备标识符,用户标识符,用户组标识符,文件模式,扩展属性,文件读取或修改的时间戳,链接数量,指向存储该内容的磁盘区块的指针,文件分类等等。
  • 其次需要注意这里所说的文件的概念包括了实际的磁盘文件、目录以及设备等等。
  • 还要注意inode不包含文件名。文件名存放在目录项dentry中。为何这么做,原因是在VFS中一个inode和一个物理上的文件可以认为是一一对应的,而我们知道由于硬链接的原因,可能在VFS中一个实际的物理文件,也就是一个inode可能对应着多个路径,所以inode和文件名之间不是一对一的关系,而可能是一对多的关系。下面在dentry中一起总结。

VFS的索引节点inode仅当文件被访问时,才会在内存中创建。

索引节点用inode结构体表示,定义在文件``中。

struct inode {
    struct hlist_node    i_hash;
    struct list_head    i_list;        /* backing dev IO list */
    struct list_head    i_sb_list;
    struct list_head    i_dentry;
    unsigned long        i_ino;
    atomic_t        i_count;
    ...
    unsigned int        i_nlink;
    uid_t            i_uid;
    gid_t            i_gid;
    ...
    loff_t            i_size;
    ...
    struct timespec        i_mtime;
    ...
    umode_t            i_mode;
    ...
    const struct inode_operations    *i_op;
    ...
    union {
        struct pipe_inode_info    *i_pipe;
        struct block_device    *i_bdev;
        struct cdev        *i_cdev;
    };

    ...
    void            *i_private; /* fs or device private pointer */
};

有关索引节点,只摘录了几个我认为比较重要的成员供参考理解,总结如下:

索引节点号i_ino

索引节点号,注意节点号在FS中独立分配,所以两个FS之间的索引节点号有可能重复。

引用计数i_count

索引节点的组织结构

以下三个成员和索引节点的组织有关:

struct hlist_node    i_hash;
struct list_head    i_list;        /* backing dev IO list */
struct list_head    i_sb_list;

管理inode的四个链表,系统根据inode的使用状态,将它们组织在多个双向链表中:

  • inode_unused:将目前还没有使用的inode链接起来(通过i_list域链接)
  • inode_in_use:目前正在使用的inode链接起来(通过i_list域链接)
  • 超级块super_block结构体中的s_dirty:该链表链接了所有被修改过的inode,这些inode将被更新到磁盘上。(通过i_list域链接起来)。
  • inode_hashtable:注意为了加快inode的查找效率,将正在使用的inode和脏inode也会放在inode_hashtable这样一个hash结构中,但是,不同的inode的hash值可能相等,所以将hash值相等的这些inode通过这个i_hash字段连接起来。

参考fs/inode.c

/*
 * Each inode can be on two separate lists. One is
 * the hash list of the inode, used for lookups. The
 * other linked list is the "type" list:
 *  "in_use" - valid inode, i_count > 0, i_nlink > 0
 *  "dirty"  - as "in_use" but also dirty
 *  "unused" - valid inode, i_count = 0
 *
 * A "dirty" list is maintained for each super block,
 * allowing for low-overhead inode sync() operations.
 */

LIST_HEAD(inode_in_use);
LIST_HEAD(inode_unused);
static struct hlist_head *inode_hashtable __read_mostly;

inode的大致组织图如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OHuEQJir-1589775803226)(http://img.dnbcw.info/2010125/pxup2592612.gif)]

文件的元数据

...
unsigned int        i_nlink;
uid_t            i_uid;
gid_t            i_gid;
...
loff_t            i_size;
...
struct timespec        i_mtime;
...
umode_t            i_mode;
...

索引节点操作i_op

是结构体inode_operations的指针类型。该结构体类型定义在``文件中,VFS通过这个结构体定义了所有操作索引节点的虚函数,需要由具体的文件系统实现。具体内容参考LKD3rd的13.8章节。

和驱动开发有关的成员

union {
    struct pipe_inode_info    *i_pipe;
    struct block_device    *i_bdev;
    struct cdev        *i_cdev;
};
...
void            *i_private; /* fs or device private pointer */

如果我们做过驱动开发的话,会发现这些成员很熟悉,对,在驱动模块中它们会出现在文件操作函数的参数中传递到驱动的回调函数中。

目录项dentry

目录项是描述文件的逻辑属性,和前面描述的超级块和索引节点不同,目录项对象没有对应的磁盘数据结构,它们是VFS根据需要在内存中现场创建出来的,更确切的说是存在于内存的目录项缓存,为了提高查找性能而设计,所以也不存在”脏”标记。注意不管是文件夹还是最终的文件,都是属于目录项,所有的目录项在一起构成一颗庞大的目录树。例如:open一个文件/home/xxx/yyy.txt,需要涉及四个目录项,它们分别是/homexxxyyy.txt,VFS在查找的时候,根据一层一层的目录项找到对应的每个目录项的inode,那么沿着目录项进行操作就可以找到最终的文件。

注意:

  • 目录也是一种文件(所以也存在对应的inode)。打开目录,实际上就是打开目录文件。
  • 一个有效的dentry结构必定有一个inode结构,这是因为一个目录项要么代表着一个文件,要么代表着一个目录,而目录实际上也是文件。所以,只要dentry结构是有效的,则其指针d_inode必定指向一个inode结构。但是一个inode却可以对应多个dentry,譬如硬链接的问题。
  • 所有的dentry形成的其实就是我们用户看到的一棵大的树。

该结构体类型定义在``文件中,

struct dentry {
    atomic_t d_count;
    ...
    struct inode *d_inode;        /* Where the name belongs to - NULL is
                     * negative */
    ...
    struct dentry *d_parent;    /* parent directory */
    struct qstr d_name;

    struct list_head d_lru;        /* LRU list */
    /*
     * d_child and d_rcu can share memory
     */
    union {
        struct list_head d_child;    /* child of parent list */
         struct rcu_head d_rcu;
    } d_u;
    struct list_head d_subdirs;    /* our children */
    struct list_head d_alias;    /* inode alias list */
    ...
    const struct dentry_operations *d_op;
    struct super_block *d_sb;    /* The root of the dentry tree */
    void *d_fsdata;            /* fs-specific data */

    unsigned char d_iname[DNAME_INLINE_LEN_MIN];    /* small names */
};

dentry对象存在于三个双向链表中:

  • 表示父子目录结构的链表:每个dentry对象的d_subdirs成员维护了一个链表,用于管理其子entry。每个子entry通过d_child域链接。
  • 所有未被使用的目录项: “未被使用”指的是曾经被使用,但当前没有使用(d_count为0),该目录项仍然保留在内存中以便需要时再使用,如果内存紧张时可以释放它们。我查了一下v2.6.32.2的代码,这个链表的链表头不是dentry_unused,而是改为超级快的一个成员s_dentry_lru。具体可以参考dentry_lru_add_tail这个函数的实现。LRU的意思是Least Recently Used,很少使用项,内存紧张时可以丢弃的意思。
  • 正在被使用的目录项: “正在被使用”的目录项(d_count>0)。该项在缓存中不可以被丢弃。该链表的链表头维护在inode的i_dentry成员。譬如表达硬链接的概念。
  • 另外,还有一个重要的链表: inode_hashtable(这个暂不介绍,主要用于加快查找速度)。

更多有关所谓“正在被使用”和“未被使用”,包括源代码注释中的NULL is negative的描述,涉及目录项的三个状态,主要和缓存的使用机制有关,详细参考LKD3的13.9.1章节。

以上概念参考下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZJasKfg2-1589775803227)(http://img.dnbcw.info/2010125/pxup2592613.gif)]

有关inode和dentry之间的关系,譬如VFS是如何利用它们表达硬链接,符号链接等概念的,可以参考一篇不错的博客VFS文件系统结构分析,上面有些图画的真心不错。

文件

从进程的角度来看待虚拟文件系统。正如LKD3所述,如果我们从用户的角度来看待VFS,文件对象会首先进入我们的视野。进程直接处理的是文件,而不是超级块,索引节点以及目录项。

进程打开文件时涉及到的内核结构体:

  • 每个进程都有自己的namespace。有关命名空间的概念,参考Linux Namespaces机制,我们这主要关心的应该是mnt_namespace。每个进程都看到了一棵大树,这棵大树是怎么创建出来的,参考后面的”VFS的构建”。
  • fs_struct用于表示进程与文件系统之间的结构关系,比如当前的工作目录,进程的根目录等等。
  • files_struct用于维护当前进程打开的所有的文件。
  • 而对于一个进程所打开的每一个文件,由file对象来表示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P49pFDLM-1589775803229)(http://img.dnbcw.info/2010125/pxup2592614.gif)]

有关open等系统调用对进程打开文件影响的例子,依然参考VFS文件系统结构分析,上面有些图画的真心不错。

文件对象file

其中file结构体值得仔细看看。

其主要成员

  • f_op: file_operations的结构体的指针。这些操作是标准Unix系统调用的基础,也是驱动开发中最多涉及的东东。

和目录项dentry一样,文件对象实际上没有对应的磁盘数据,所以结构体中没有“脏”标志。

filedentry之间是多对一的关系,dentryinode之间又是多对一的关系。讲到这里,以前一直困惑我们的一张关系图应该很明了了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uLFAaoSm-1589775803230)(http://blog.fangjian.me/images/uploads/2010/10/inode.jpg)]

fs_struct

files_struct

VFS的构建

主要涉及挂载的概念。

所以说这里的列表维护的仅仅是当前支持的文件系统类型,具体一个文件系统的挂载情况通过另外一个结构体vfsmount来表达。The vfsmount structure is defined in 。

系统维护了一张”文件系统安装表”,记录了所有挂载在当前系统上的挂载点的实例。

因为一个文件系统可以被挂载多次,所以一个超级快可以对应多个vfsmount

应用程序通过mount系统调用挂载文件系统时,

  • 内核首先根据设备名称检查VFS内部维护的超级快对象链表,判断该FS的超级快对象在内存是否已经存在,否则查看FS类型注册表,如果是系统支持的FS类型,则通过调用file_system_type结构体的get_sb()读该设备的超级块,建立该设备的内存超级快对象和根目录项对象
  • 创建对应的vfsmount并填写其各项内容,包括其指向的超级快指针,设备名,安装点,根目录项指针等。

VFS总结

这么多结构体和对象,之间的对应关系,可以总结一下。LINUX VFS精华版PPT上有一张类图,但个人感觉不是很清楚。

VFS作为一个中间层,对下层的文件系统和驱动开发提出了哪些要求,即有哪些虚函数需要我们去支持。可以总结一下。

另外一张有关VFS和驱动开发之间的关系图可以参考宋宝华《Linux设备驱动开发详解(第2版)》的图5.1
其中:
字符设备:
VFS->字符设备文件(/dev/…)->字符设备驱动(直接提供file_operations)

块设备:
VFS->块设备文件系统(ext, fat, jffs等,由这些文件系统实现file_operations)->块设备驱动(看不到file_operations,是不是完全看不到,标识怀疑)。

这么多结构体和对象,之间的对应关系,可以总结一下。LINUX VFS精华版PPT上有一张类图,但个人感觉不是很清楚。

VFS作为一个中间层,对下层的文件系统和驱动开发提出了哪些要求,即有哪些虚函数需要我们去支持。可以总结一下。

另外一张有关VFS和驱动开发之间的关系图可以参考宋宝华《Linux设备驱动开发详解(第2版)》的图5.1
其中:
字符设备:
VFS->字符设备文件(/dev/…)->字符设备驱动(直接提供file_operations)

块设备:
VFS->块设备文件系统(ext, fat, jffs等,由这些文件系统实现file_operations)->块设备驱动(看不到file_operations,是不是完全看不到,标识怀疑)。

这张图还体现了一个概念,就是我们通过设备文件(/dev/…)直接访问设备,也通过驱动实现,这里指的是ioctl吗?

你可能感兴趣的:(操作系统)