Linux Kernel - Virtual Filesystem

Table of Contents

虚拟文件系统(简称 VFS)作为内核子系统,为用户空间程序提供了文件和文件系统相关的接口。系统中所有文件系统不但依赖 VFS 共存,而且也依靠 VFS 系统协同工作。通过虚拟文件系统,程序可以利用标准的 Uinx 系统调用对不同的文件系统,甚至不同介质上的文件系统进行读写操作。

1. 通用文件系统接口

VFS 使得用户可以直接使用 open(), read()write() 这样的系统调用而无须考虑具体文件系统和实际物理介质。 现在听起来这并没什么新奇的(我们早就认为这是理所当然的),但是,使得这些通用的系统调用可以跨越各种文件系统和不同介质执行,绝非是微不足道的成绩。更了不起的是,系统调用可以在这些不同的文件系统和介质之间执行一一我们可以使用标准的系统调用从个文件系统拷贝或移动数据到另一个文件系统。老式的操作系统(比如 DOS)是无力完成上述工作的,任何对非本地文件系统的访问都必须依靠特殊工具才能完成。正是由于现代操作系统引入抽象层,比如 Linux,通过虚拟接口访问文件系统,オ使得这种协作性和泛型存取成为可能。

2. 文件系统抽象层

之所以可以使用这种通用接口对所有类型的文件系统进行操作,是因为内核在它的底层文件系统接口上建立了一个抽象层。该抽象层使 Linux 能够支持各种文件系统,即便是它们在功能和行为上存在很大差别。为了支持多文件系统,VFS 提供了一个通用文件系统模型,该模型囊括了任何文件系统的常用功能集和行为。当然,该模型偏重于 Unix 风格的文件系统(后文将介绍)。但即使这样,Linux 仍然可以支持很多种差异很大的文件系统,从 DOS 系统的 FAT 到 Windows 系统的 NTFS,再到各种 Unix 风格文件系统和 Linux 特有的文件系统。

VFS 抽象层之所以能衔接各种各样的文件系统,是因为它定义了所有文件系统都支持的、基本的、概念上的接口和数据结构。同时实际文件系统也将自身的诸如“如何打开文件”,“目录是什么”等概念在形式上与 VFS 的定义保持一致。因为实际文件系统的代码在统一的接口和数据结构下隐藏了具体的实现细节,所以在 VFS 层和内核的其他部分看来,所有文件系统都是相同的,它们都支持像文件和目录这样的概念,同时也支持像创建文件和删除文件这样的操作。

内核通过抽象层能够方便、简单地支持各种类型的文件系统。实际文件系统通过编程提供 VFS 所期望的抽象接口和数据结构,这样,内核就可以毫不费力地和任何文件系统协同工作,并且这样提供给用户空间的接口,也可以和任何文件系统无缝地连接在一起,完成实际工作。

其实在内核中,除了文件系统本身外,其他部分并不需要了解文件系统的内部细节。比如一个简单的用户空间程序执行如下的操作:

ret = write(fd, buf, len);

该系统调用将 buf 指针指向的长度为 len 字节的数据写入文件描述符 fd 对应的文件的当前位置。这个系统调用首先被一个通用系统调用 sys_write() 处理, sys_write() 函数要找到 fd 所在的文件系统实际给出的是哪个写操作,然后再执行该操作。实际文件系统的写方法是文件系统实现的一部分,数据最终通过该操作写入介质(或执行这个文件系统想要完成的写动作)。图 1 描述了从用户空间的 write 调用到数据被写入磁盘介质的整个流程。一方面,系统调用是通用 VFS 接口,提供给用户空间的前端;另一方面,系统调用是具体文件系统的后端,处理实现细节。接下来的小节中我们会具体看到 VFS 抽象模型以及它提供的接口。

linux_vfs_write.gif

Figure 1: 调用 write() 的过程

3. Unix 文件系统

Unix 使用了四种和文件系统相关的传统抽象概念:文件、目录项、索引节点和挂载点(mount point)。

从本质上讲文件系统是特殊的数据分层存储结构,它包含文件、目录和相关的控制信息。文件系统的通用操作包含创建、删除和挂载等。在 Unix 中,文件系统被挂载在一个特定的挂载点上。

文件其实可以做一个有序字节串,字节串中第一个字节是文件的头,最后一个字节是文件的尾。每一个文件为了便于系统和用户识别,都被分配了一个便于理解的名字。典型的文件操作有读、写、创建和删除等。Unix 文件的概念与面向记录的文件系统(如 Open VMS 的 File-11)形成鲜明的对照。面向记录的文件系统提供更丰富、更结构化的表示,而简单的面向字节流抽象的 Unix 文件则以简单性和相当的灵活性为代价。

文件通过目录组织起来。文件目录好比一个文件夹,用来容纳相关文件。因为目录也可以包含其他目录,即子目录,所以目录可以层层嵌套,形成文件路径。 路径中的每一部分都被称作目录条目。“/home/wolfman/butter”是文件路径的一个例子——根目录/,目录 home, wolfman 和文件 butter 都是目录条目,它们统称为“目录项(dentries)”。

在 Unix 中,目录(directory)属于普通文件,它列出包含在其中的所有文件。由于 VFS 把目录当作文件对待,所以可以对目录执行和文件相同的操作。 注:“目录项”和“目录”是两个不同的概念,目录项是路径的一部分,有单独的结构体表示;而目录属于普通文件。

Unix 系统将文件的相关信息和文件本身这两个概念加以区分,例如访问控制权限、大小、拥有者、创建时间等信息是文件相关信息,有时被称作文件的元数据,被存储在一个单独的数据结构中,该结构被称为索引节点(inode),它其实是 index node 的缩写。

所有这些信息都和文件系统的控制信息密切相关,文件系统的控制信息存储在超级块(superblock)中, 超级块是一种包含文件系统信息的数据结构。 有时,把这些收集起来的信息称为文件系统数据元,它集单独文件信息和文件系统的信息于一身。

一直以来,Unix 文件系统在它们物理磁盘布局中也是按照上述概念实现的。比如说在磁盘上,文件(目录也属于文件)信息按照索引节点形式存储在单独的块中;控制信息被集中存储在磁盘的超级块中,等等。Unix 中文件的概念从物理上被映射到存储介质。 Linux 的 VFS 的设计目标就是要保证能与支持和实现了这些概念的文件系统协同工作。像如 FAT 或 NTFS 这样的非 Unix 风格的文件系统,虽然也可以在 Linux 上工作,但是它们必须经过封装,提供一个符合这些概念的界面。比如,即使一个文件系统不支持索引节点,它也必须在内存中装配索引节点结构体,就像它本身包含索引节点一样。再比如,如果一个文件系统将目录看做一种特殊对象,那么要想使用 VFS,就必须将目录重新表示为文件形式。通常,这种转换需要在使用现场(on the fly)引入一些特殊处理,使得非 Unix 文件系统能够兼容 Unix 文件系统的使用规则并满足 VFS 的需求。这种文件系统当然仍能工作,但是其带来的开销则不可思议(开销太大了)。

4. VFS 对象及其数据结构

VFS 其实采用的是面向对象的设计思路,使用一组数据结构来代表通用文件对象。这些数据结构类似于对象。因为内核纯粹使用 C 代码实现,没有直接利用面向对象的语言,所以内核中的数据结构都使用 C 语言的结枃体实现,而这些结构体包含数据的同时也包含操作这些数据的函数指针,其中的操作函数由具体文件系统实现。

VFS 中有四个主要的对象类型,它们分别是:

  1. 超级块(superblock)对象,它代表一个具体的已挂载文件系统。
  2. 索引节点(inode)对象,它代表一个具体文件。
  3. 目录项(dentry)对象,它代表一个目录项,是路径的一个组成部分。
  4. 文件(file)对象,它代表由进程打开的文件。

注意,因为 VFS 将目录作为一个文件来处理,所以不存在目录对象。回忆本章前面所提到的目录项代表的是路径中的一个组成部分,它可能包括一个普通文件。换句话说,目录项不同于目录,但目录却是另一种形式的文件,明白了吗?

每个主要对象中都包含一个操作对象,这些操作对象描述了内核针对主要对象可以使用的方法:

  1. super_operations 对象,其中包括内核针对特定文件系统所能调用的方法,比如 write_inode()sync_fs() 等方法。
  2. inode_operations 对象,其中包括内核针对特定文件所能调用的方法,比如 create()link() 等方法。
  3. dentry_operations 对象,其中包括内核针对特定目录所能调用的方法,比如 d_compare()d_delete() 等方法。
  4. file_operations 对象,其中包括进程针对已打开文件所能调用的方法,比如 read()write() 等方法。

操作对象作为一个结构体指针来实现,此结构体中包含指向操作其父对象的函数指针。对于其中许多方法来说,可以继承使用 VFS 提供的通用函数,如果通用函数提供的基本功能无法满足需要,那么就必须使用实际文件系统的独有方法填充这些函数指针,使其指向文件系统实例。

再次提醒,我们这里所说的对象就是指结构体,而不是像 C++ 或 Java 那样的真正的对象数据类类型。但是这些结构体的确代表的是一个对象,它含有相关的数据和对这些数据的操作,所以可以说它们就是对象。

VFS 使用了大量结构体对象,它所包括的对象远远多于上面提到的这几种主要对象。比如每个注册的文件系统都由 file_system_type 结构体来表示,它描述了文件系统及其性能;另外,每一个挂载点也都用 vfsmount 结构体表示,它包含的是挂载点的相关信息,如位置和挂载标志等。

5. 超级块对象

各种文件系统都必须实现超级块对象,该对象用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区中的文件系统超级块或文件系统控制块(所以称为超级块对象)。对于并非基于磁盘的文件系统(如基于内存的文件系统,比如 sysfs),它们会在使用现场(on the fly)创建超级块并将其保存到内存中。

超级块对象由 super_block 结构体表示,定义在文件<linux/fs.h>中,下面给出它的结构和各个域的描述:

struct super_block {
    struct list_head         s_list;             /* list of all superblocks */
    dev_t                    s_dev;              /* identifier */
    unsigned long            s_blocksize;        /* block size in bytes */
    unsigned char            s_blocksize_bits;   /* block size in bits */
    unsigned char            s_dirt;             /* dirty flag */
    unsigned long long       s_maxbytes;         /* max file size */
    struct file_system_type  s_type;             /* filesystem type */
    struct super_operations  s_op;               /* superblock methods */
    struct dquot_operations  *dq_op;             /* quota methods */
    struct quotactl_ops      *s_qcop;            /* quota control methods */
    struct export_operations *s_export_op;       /* export methods */
    unsigned long            s_flags;            /* mount flags */
    unsigned long            s_magic;            /* filesystem’s magic number */
    struct dentry            *s_root;            /* directory mount point */
    struct rw_semaphore      s_umount;           /* unmount semaphore */
    struct semaphore         s_lock;             /* superblock semaphore */
    int                      s_count;            /* superblock ref count */
    int                      s_need_sync;        /* not-yet-synced flag */
    atomic_t                 s_active;           /* active reference count */
    void                     *s_security;        /* security module */
    struct xattr_handler     **s_xattr;          /* extended attribute handlers */
    struct list_head         s_inodes;           /* list of inodes */
    struct list_head         s_dirty;            /* list of dirty inodes */
    struct list_head         s_io;               /* list of writebacks */
    struct list_head         s_more_io;          /* list of more writeback */
    struct hlist_head        s_anon;             /* anonymous dentries */
    struct list_head         s_files;            /* list of assigned files */
    struct list_head         s_dentry_lru;       /* list of unused dentries */
    int                      s_nr_dentry_unused; /* number of dentries on list */
    struct block_device      *s_bdev;            /* associated block device */
    struct mtd_info          *s_mtd;             /* memory disk information */
    struct list_head         s_instances;        /* instances of this fs */
    struct quota_info        s_dquot;            /* quota-specific options */
    int                      s_frozen;           /* frozen status */
    wait_queue_head_t        s_wait_unfrozen;    /* wait queue on freeze */
    char                     s_id[32];           /* text name */
    void                     *s_fs_info;         /* filesystem-specific info */
    fmode_t                  s_mode;             /* mount permissions */
    struct semaphore         s_vfs_rename_sem;   /* rename semaphore */
    u32                      s_time_gran;        /* granularity of timestamps */
    char                     *s_subtype;         /* subtype name */
    char                     *s_options;         /* saved mount options */
};

创建、管理和撤销超级块对象的代码位于文件 fs/super.c 中。超级块对象通过 alloc_super() 函数创建并初始化。在文件系统挂载时,文件系统会调用该函数以便从磁盘读取文件系统超级块,并且将其信息填充到内存中的超级块对象中。

6. 索引节点对象

前面介绍过,Unix 系统将文件的相关信息和文件本身这两个概念加以区分,例如访问控制权限、大小、拥有者、创建时间等信息是文件相关信息,它们保存在索引节点对象中。

索引节点对象包含了内核在操作文件或目录时需要的全部辅助信息。 对于 Unix 风格的文件系统来说,这些信息可以从磁盘索引节点直接读入。 如果一个文件系统没有索引节点,那么,不管这些相关信息在磁盘上是怎么存放的,文件系统都必须从中提取这些信息。没有索引节点的文件系统通常将文件的描述信息作为文件的一部分来存放。这些文件系统与 Unix 风格的文件系统不同,没有将数据与控制信息分开存放。有些现代文件系统使用数据库来存储文件的数据。不管哪种情况、采用哪种方式,索引节点对象必须在内存中创建,以便于文件系统使用。

索引节点对象由 inode 结构体表示,它定义在文件<linux/fs.h>中,下面给出它的结构体和各项的描述:

struct inode {
    struct hlist_node       i_hash;              /* hash list */
    struct list_head        i_list;              /* list of inodes */
    struct list_head        i_sb_list;           /* list of superblocks */
    struct list_head        i_dentry;            /* list of dentries */
    unsigned long           i_ino;               /* inode number */
    atomic_t                i_count;             /* reference counter */
    unsigned int            i_nlink;             /* number of hard links */
    uid_t                   i_uid;               /* user id of owner */
    gid_t                   i_gid;               /* group id of owner */
    kdev_t                  i_rdev;              /* real device node */
    u64                     i_version;           /* versioning number */
    loff_t                  i_size;              /* file size in bytes */
    seqcount_t              i_size_seqcount;     /* serializer for i_size */
    struct timespec         i_atime;             /* last access time */
    struct timespec         i_mtime;             /* last modify time */
    struct timespec         i_ctime;             /* last change time */
    unsigned int            i_blkbits;           /* block size in bits */
    blkcnt_t                i_blocks;            /* file size in blocks */
    unsigned short          i_bytes;             /* bytes consumed */
    umode_t                 i_mode;              /* access permissions */
    spinlock_t              i_lock;              /* spinlock */
    struct rw_semaphore     i_alloc_sem;         /* nests inside of i_sem */
    struct semaphore        i_sem;               /* inode semaphore */
    struct inode_operations *i_op;               /* inode ops table */
    struct file_operations  *i_fop;              /* default inode ops */
    struct super_block      *i_sb;               /* associated superblock */
    struct file_lock        *i_flock;            /* file lock list */
    struct address_space    *i_mapping;          /* associated mapping */
    struct address_space    i_data;              /* mapping for device */
    struct dquot            *i_dquot[MAXQUOTAS]; /* disk quotas for inode */
    struct list_head        i_devices;           /* list of block devices */
    union {
        struct pipe_inode_info *i_pipe;          /* pipe information */
        struct block_device    *i_bdev;          /* block device driver */
        struct cdev            *i_cdev;          /* character device driver */
    };
    unsigned long           i_dnotify_mask;      /* directory notify mask */
    struct dnotify_struct   *i_dnotify;          /* dnotify */
    struct list_head        inotify_watches;     /* inotify watches */
    struct mutex            inotify_mutex;       /* protects inotify_watches */
    unsigned long           i_state;             /* state flags */
    unsigned long           dirtied_when;        /* first dirtying time */
    unsigned int            i_flags;             /* filesystem flags */
    atomic_t                i_writecount;        /* count of writers */
    void                    *i_security;         /* security module */
    void                    *i_private;          /* fs private pointer */
};

一个索引节点代表文件系统中(但是索引节点仅当文件被访问时,才在内存中创建)的一个文件,它也可以是设备或管道这样的特殊文件。因此索引节点结构体中有一些和特殊文件相关的项,比如 i_pipe 项就指向一个代表有名管道的数据结构, i_bdev 指向块设备结构体, i_cdev 指向字符设备结构体。这三个指针被存放在一个公用体中,因为一个给定的索引节点每次只能表示三者之一(或三者均不)。

有时,某些文件系统可能并不能完整地包含索引节点结构体要求的所有信息。举个例子,有的文件系统可能并不记录文件的访问时间,这时,该文件系统就可以在实现中选择任意合适的办法来解决这个问题。它可以在 i_atime 中存储 0,或者让 i_atime 等于 i_mtime ,或者只在内存中更新 i_atme 而不将其写回磁盘,或者由文件系统的实现者来决定。

7. 目录项对象

VFS 把目录当作文件对待,所以在路径/bin/vi 中,bin 和 vi 都属于文件——bin 是特殊的目录文件而 vi 是一个普通文件,路径中的每个组成部分都由一个索引节点对象表示。虽然它们可以统一由索引节点表示,但是 VFS 经常需要执行目录相关的操作,比如路径名査找等。路径名査找需要解析路径中的每一个组成部分,不但要确保它有效,而且还需要再进一步寻找路径中的下一个部分。

为了方便査找操作,VFS 引入了目录项的概念。每个 dentry 代表路径中的一个特定部分。 对前一个例子来说,/、bin 和 vi 都属于目录项对象。前两个是目录,最后一个是普通文件。必须明确一点:在路径中(包括普通文件在内),每一个部分都是目录项对象。解析一个路径并遍历其分量绝非简单的演练,它是耗时的、常规的字符申比较过程,执行耗时、代码繁琐。目录项对象的引入使得这个过程更加简单。

目录项也可包括挂载点。在路径/mnt/cdrom/foo 中,构成元素/、mnt、cdrom 和 foo 都属于目录项对象。VFS 在执行目录操作时(如果需要的话)会现场创建目录项对象。

目录项对象由 dentry 结构体表示,定义在文件<linux/dcache.h>中。下面给出该结构体和其中各项的描述:

struct dentry {
    atomic_t                 d_count;    /* usage count */
    unsigned int             d_flags;    /* dentry flags */
    spinlock_t               d_lock;     /* per-dentry lock */
    int                      d_mounted;  /* is this a mount point? */
    struct inode             *d_inode;   /* associated inode */
    struct hlist_node        d_hash;     /* list of hash table entries */
    struct dentry            *d_parent;  /* dentry object of parent */
    struct qstr              d_name;     /* dentry name */
    struct list_head         d_lru;      /* unused list */
    union {
        struct list_head     d_child;    /* list of dentries within */
        struct rcu_head      d_rcu;      /* RCU locking */
    } d_u;
    struct list_head         d_subdirs;  /* subdirectories */
    struct list_head         d_alias;    /* list of alias inodes */
    unsigned long            d_time;     /* revalidate time */
    struct dentry_operations *d_op;      /* dentry operations table */
    struct super_block       *d_sb;      /* superblock of file */
    void                     *d_fsdata;  /* filesystem-specific data */
    unsigned char            d_iname[DNAME_INLINE_LEN_MIN]; /* short name */
};

与前面的两个对象不同,目录项对象没有对应的磁盘数据结构, VFS 根据字符串形式的路径名现场创建它。 而且由于目录项对象并非真正保存在磁盘上,所以目录项结构体没有是否被修改的标志(也就是是否为脏、是否需要写回磁盘的标志)。

7.1. 目录项状态

目录项对象有三种有效状态:被使用、未被使用和负状态。

一个被使用的目录项对应一个有效的索引节点(即 d_inode 指向相应的索引节点)并且表明该对象存在一个或多个使用者(即 d_count 为正值)。一个目录项处于被使用状态,意味着它正被 VFS 使用并且指向有效的数据,因此不能被丢弃。

一个未被使用的目录项对应一个有效的索引节点( d_inode 指向一个索引节点),但是应指明 VFS 当前并未使用它( d_count 为 0)。该目录项对象仍然指向一个有效对象,而且被保留在缓存中以便需要时再使用它。由于该目录项不会过早地被撤销,所以以后再需要它时,不必重新创建,与未缓存的目录项相比,这样使路径査找更迅速。但如果要回收内存的话,可以撤销未使用的目录项。

一个负状态(注:称为“无效状态”更合适)的目录项没有对应的有效索引节点( d_inode 为 NULL),因为索引节点已被除了,或路径不再正确了,但是目录项仍然保留,以便快速解析以后的路径査询。比如,一个守护进程不断地去试图打开并读取一个不存在的配置文件。 open() 系统调用不断地返回 ENOENT,直到内核构建了这个路径、遍历磁盘上的目录结构体并检査这个文件的确不存在为止。即便这个失败的查找很浪费资源,但是将负状态缓存起来还是非常值得的。虽然负状态的目录项有些用处,但是如果有需要,可以撤销它,因为毕竟实际上很少用到它。

目录项对象释放后也可以保存到 slab 对象缓存中去。此时,任何 VFS 或文件系统代码都没有指向该目录项对象的有效引用。

8. 文件对象

VFS 的最后一个主要对象是文件对象。 文件对象表示进程已打开的文件。 如果我们站在用户角度来看待 VFS,文件对象会首先进入我们的视野。进程直接处理的是文件,而不是超级块、索引节点或目录项。所以不必奇怪:文件对象包含我们非常熟悉的信息(如访问模式,当前偏移等),同样道理,文件操作和我们非常熟悉的系统调用 read()write() 等也很类似。

文件对象是已打开的文件在内存中的表示。该对象(不是物理文件)由相应的 open() 系统调用创建,由 close() 系统调用撤销, 所有这些文件相关的调用实际上都是文件操作表中定义的方法。因为多个进程可以同时打开和操作同一个文件,所以同一个文件也可能存在多个对应的文件对象。文件对象仅仅在进程观点上代表已打开文件,它反过来指向目录项对象(反过来指向索引节点),其实只有目录项对象才表示已打开的实际文件。虽然一个文件对应的文件对象不是唯一的,但对应的索引节点和目录项对象无疑是唯一的。

文件对象由 file 结构体表示,定义在文件<linux/fs.h>中,下面给出该结构体和各项的描述:

struct file {
    union {
        struct list_head   fu_list;       /* list of file objects */
        struct rcu_head    fu_rcuhead;    /* RCU list after freeing */
    } f_u;
    struct path            f_path;        /* contains the dentry */
    struct file_operations *f_op;         /* file operations table */
    spinlock_t             f_lock;        /* per-file struct lock */
    atomic_t               f_count;       /* file object’s usage count */
    unsigned int           f_flags;       /* flags specified on open */
    mode_t                 f_mode;        /* file access mode */
    loff_t                 f_pos;         /* file offset (file pointer) */
    struct fown_struct     f_owner;       /* owner data for signals */
    const struct cred      *f_cred;       /* file credentials */
    struct file_ra_state   f_ra;          /* read-ahead state */
    u64                    f_version;     /* version number */
    void                   *f_security;   /* security module */
    void                   *private_data; /* tty driver hook */
    struct list_head       f_ep_links;    /* list of epoll links */
    spinlock_t             f_ep_lock;     /* epoll lock */
    struct address_space   *f_mapping;    /* page cache mapping */
    unsigned long          f_mnt_write_state; /* debugging state */
};

类似于目录项对象,文件对象实际上没有对应的磁盘数据。所以在结构体中没有代表其对象是否为脏、是否需要写回磁盘的标志。文件对象通过 f_dentry 指针指向相关的目录项对象。目录项会指向相关的索引节点,索引节点会记录文件是否是脏的。

9. 参考

本文摘自《Linux 内核设计与实现,第 3 版》

Author: cig01

Created: <2018-10-26 Fri>

Last updated: <2020-06-13 Sat>

Creator: Emacs 27.1 (Org mode 9.4)