linux内核设计与实现——虚拟文件系统

时间:2021-07-17 15:43:42

虚拟文件系统

虚拟文件系统(有时也称作虚拟文件交换,更常见的是简称VFS)作为内核子系统,为用户空间程序提供了文件和文件系统相关的接口。

之所以可以使用这种通用接口对所有类型的文件系统进行操作,是因为内核在它的底层文件系统接口上建立了一个VFS抽象层,该抽象层使Linux能够支持各种文件系统,即便是它们在功能和行为上存在很大的差别。

VFS抽象层能够支持各种各样的文件系统,因为它定义了所有文件系统都支持的、基本的、概念上的接口和数据结构。而实际的文件系统的代码在统一的接口和数据结构下隐藏了具体的实现细节。

Unix文件系统

Unix使用了四种和文件系统相关的传统抽象概念:文件、目录项、索引节点和安装点。
1. 文件:文件就是一个有序字节串,字节串中第一个字节是文件的头,最后一个字节是文件的尾。典型的文件操作有读、写、创建和删除等。
2. 目录项:文件路径中的每一部分都被称作目录条目。“/home/wolfman/butter”是文件路径的一个例子——根目录/,目录home,wolfman和文件butter都是目录条目,它们统称为目录项。(在Unix中,目录属于普通文件,它列出包含在其中的所有文件。)
3. 索引节点:Unix系统将文件的相关信息和文件本身这两个概念加以区分,例如访问控制权限、大小、拥有者、创建时间等信息。文件相关信息,有时被称作文件的元数据,被存储在一个单独的数据结构中,该结构被称为索引节点(inode)。
4. 安装点:在Unix中,文件系统被安装在一个特定的安装点上,该安装点在全局层次结构中被称作命名空间,所有的已安装文件系统都作为根文件系统树的枝叶出现在系统中。

VFS对象及其数据结构

VFS采用的是面向对象的设计思路,但在C语言中,只能使用结构体方式实现,即包含数据的同时又包含操作这些数据的函数指针。

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

  • 超级块对象,代表一个具体的已安装文件系统。
  • 索引节点对象,代表一个具体文件。
  • 目录项对象,代表一个目录项,是路径的一个组成部分。
  • 文件对象,代表有进程打开的文件。

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

  • super_operations对象:包含内核针对特定文件系统所能调用的方法。
  • inode_operations对象:包含内核对特定文件所能调用的方法。
  • dentry_operations对象:包含内核对特定目录所能调用的方法。
  • file_operations对象:包含进程针对已打开文件所能调用的方法。

超级块对象

各种文件系统都必须实现超级块对象,该对象用于存储特定文件系统的信息,通常对应于存放在磁盘特定扇区的文件系统超级块或文件系统控制块。对于并非基于磁盘的文件系统(基于内存的文件系统sysfs、proc等),它们会在使用时创建超级块并将其保存到内存中。
超级块对象由结构体super_block表示,定义在 linux/fs.h 中,下面列出了一些重要的属性:

/** * 超级块结构中定义的字段非常多, * 这里只介绍一些重要的属性 */
struct super_block {
    struct list_head                s_list;     /* 指向所有超级块的链表 */
    const struct super_operations   *s_op;      /* 超级块方法 */
    struct dentry                   *s_root;    /* 目录挂载点 */
    struct mutex                    s_lock;     /* 超级块信号量 */
    int                             s_count;    /* 超级块引用计数 */
    struct list_head                s_inodes;   /* inode链表 */
    struct mtd_info                 *s_mtd;     /* 存储磁盘信息 */
    fmode_t                         s_mode;     /* 安装权限 */
};

其中,s_op指向超级块的操作函数表,由struct super_operations结构体表示,定义在 linux/fs.h 中,其形式如下:

/* * 其中的 s_op 中定义了超级块的操作方法 * 这里只介绍一些相对重要的函数 */
struct super_operations {

    /* 创建和初始化一个索引节点对象 */
    struct inode *(*alloc_inode)(struct super_block *sb);

    /* 释放给定的索引节点 */
    void (*destroy_inode)(struct inode *);

    /* VFS在索引节点被修改时会调用这个函数 */
    void (*dirty_inode) (struct inode *); 

    /* 将索引节点写入磁盘,wait表示写操作是否需要同步 */
    int (*write_inode) (struct inode *, int);

    /* 最后一个指向索引节点的引用被删除后,VFS会调用这个函数 */
    void (*drop_inode) (struct inode *);

    /* 从磁盘上删除指定的索引节点 */
    void (*delete_inode) (struct inode *);

    /* 卸载文件系统时由VFS调用,用来释放超级块 */
    void (*put_super) (struct super_block *);

    /* 用给定的超级块更新磁盘上的超级块 */
    void (*write_super) (struct super_block *);

    /* 使文件系统中的数据与磁盘上的数据同步 */
    int (*sync_fs)(struct super_block *sb, int wait);

    /* VFS调用该函数获取文件系统状态 */
    int (*statfs) (struct dentry *, struct kstatfs *);

    /* 指定新的安装选项重新安装文件系统时,VFS会调用该函数 */
    int (*remount_fs) (struct super_block *, int *, char *);

    /* VFS调用该函数释放索引节点,并清空包含相关数据的所有页面 */
    void (*clear_inode) (struct inode *);

    /* VFS调用该函数中断安装操作 */
    void (*umount_begin) (struct super_block *);
};

索引节点对象

索引节点对象包含了内核在操作文件或目录时需要的全部信息。索引节点对象在内存中创建,以便于文件系统使用。

索引节点对象由inode结构体表示,它定义在 linux/fs.h 中,下面列出了一些重要的属性:

/* * 索引节点结构中定义的字段非常多, * 这里只介绍一些重要的属性 */
struct inode {
    struct hlist_node       i_hash;     /* 散列表,用于快速查找inode */
    struct list_head        i_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;      /* 使用者id */
    gid_t                   i_gid;      /* 使用组id */
    struct timespec         i_atime;    /* 最后访问时间 */
    struct timespec         i_mtime;    /* 最后修改时间 */
    struct timespec         i_ctime;    /* 最后改变时间 */
    const struct inode_operations       *i_op;      /* 索引节点操作函数 */
    const struct file_operations        *i_fop;     /* 缺省的索引节点操作 */
    struct super_block      *i_sb;          /* 相关的超级块 */
    struct address_space    *i_mapping;     /* 相关的地址映射 */
    struct address_space    i_data;         /* 设备地址映射 */
    unsigned int            i_flags;        /* 文件系统标志 */
    void                    *i_private;     /* fs 私有指针 */
};

索引节点对象的操作方法由结构体inode_operations定义,这里只介绍一些重要的函数:

struct inode_operations {

    /* 为dentry对象创造一个新的索引节点 */
    int (*create) (struct inode *,struct dentry *,int, struct nameidata *);

    /* 在特定文件夹中寻找索引节点,该索引节点要对应于dentry中给出的文件名 */
    struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);

    /* 创建硬链接 */
    int (*link) (struct dentry *,struct inode *,struct dentry *);

    /* 从一个符号链接查找它指向的索引节点 */
    void * (*follow_link) (struct dentry *, struct nameidata *);

    /* 在 follow_link调用之后,该函数由VFS调用进行清除工作 */
    void (*put_link) (struct dentry *, struct nameidata *, void *);

    /* 该函数由VFS调用,用于修改文件的大小 */
    void (*truncate) (struct inode *);
};

目录项对象

VFS把目录当作文件对待,文件都可以由索引节点对象表示。
VFS经常需要执行目录相关的操作,比如文件查找等。为了方便查找操作,VFS引入了目录项概念。在路径中(包括普通文件在内),每一个部分都是目录项对象。
VFS在执行目录操作时(如果需要的话)会现场创建目录项对象。
目录项对象由dentry结构体表示,定义在文件 linux/dcache.h 中,下面列出几个重要的选项:

struct dentry {
    atomic_t                d_count;        /* 使用计数 */
    unsigned int            d_flags;        /* 目录项标识 */
    struct inode            *d_inode;       /* 相关联的索引节点 */
    struct hlist_node       d_hash;         /* 散列表 */
    struct dentry           *d_parent;      /* 父目录的目录项对象 */
    struct qstr             d_name;         /* 目录项名称 */
    struct list_head        d_subdirs;      /* 子目录链表 */
    struct list_head        d_alias;        /* 索引节点别名链表 */
    struct dentry_operations    *d_op;      /* 目录项操作指针 */
    ...
}

目录项对象有三种有效状态:被使用、未被使用和负状态。被使用和未被使用的目录项都对应一个有效的索引节点,而负状态的目录项没有对应的有效索引节点。

为了减少VFS层遍历文件路径的时间,内核将目录项对象缓存在目录项缓存(简称dcache)中。目录项缓存包括三个主要部分:

  1. “被使用的”目录项链表;
  2. “未被使用的”双向链表;
  3. 散列表和相应的散列函数。

dentry_operations结构体指明了VFS操作目录项的所有方法,定义在文件 linux/dcache.h 中:

struct dentry_operations {

    /* 判断目录对象是否有效 */
    int (*d_revalidate)(struct dentry *, struct nameidata *);

    /* 为目录项对象生成散列值 */
    int (*d_hash)(struct dentry *, struct qstr *);

    /* 比较name1和name2这两个文件名 */
    int (*d_compare)(struct dentry *, struct qstr *, struct qstr *);

    /* 当目录项对象的d_count计数值等于0时,VFS调用该函数. */
    int (*d_delete)(struct dentry *);

    /* 当目录项对象将要被释放时,VFS调用该函数。 */
    void (*d_release)(struct dentry *);

    /* 当一个目录项对象丢失了其相关的索引节点时,VFS调用该函数 */
    void (*d_iput)(struct dentry *, struct inode *);

    char (*d_dname)(struct dentry *, char *, int);
}

文件对象

文件对象是已打开的文件在内存中的表示。该对象由相应的open()系统调用创建,由close()系统调用撤销,所有这些文件相关的调用实际上都是文件操作表中定义的方法。
文件对象由file结构体表示,定义在文件 linux/fs.h 中,下面给出几个重要的选项:

struct file {
    struct path f_path; /* 包含目录项 */
    struct file_operations *f_op;  /* 文件操作表 */
    atomic_t f_count; /* 文件对象的使用计数 */
    unsigned int f_flags; /* 当打开文件时所指定的标志 */
    mode_t f_mode; /* 文件的访问模式 */
    loff_t f_pos; /* 文件当前的位移量(文件指针)*/
    ......
}

文件对象的操作由file_operations结构体表示,定义在文件 linux/fs.h 中:

struct file_operations { 

    /* 拥有该结构的模块的指针,一般为THIS_MODULES */
    struct module *owner;

    /* 用来修改文件当前的读写位置 */
    loff_t (*llseek) (struct file *, loff_t, int);

    /* 从设备中同步读取数据 */
    ssize_t (*read) (struct file *, char __user *, size_t, loff_t *); 

    /* 向设备发送数据 */
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

    /* 初始化一个异步的读取操作 */
    ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

    /* 初始化一个异步的写入操作 */
    ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);

    /* 仅用于读取目录,对于设备文件,该字段为NULL */
    int (*readdir) (struct file *, void *, filldir_t);

    /* 轮询函数,判断目前是否可以进行非阻塞的读写或写入 */
    unsigned int (*poll) (struct file *, struct poll_table_struct *); 

    /* 执行设备I/O控制命令 */
    int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); 

    /* 不使用BLK文件系统,将使用此种函数指针代替ioctl */
    long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);

    /* 在64位系统上,32位的ioctl调用将使用此函数指针代替 */
    long (*compat_ioctl) (struct file *, unsigned int, unsigned long); 

    /* 用于请求将设备内存映射到进程地址空间 */
    int (*mmap) (struct file *, struct vm_area_struct *); //

    /* 打开文件 */
    int (*open) (struct inode *, struct file *); 

    int (*flush) (struct file *, fl_owner_t id); 

    int (*release) (struct inode *, struct file *); 

    /* 刷新待处理的数据 */
    int (*fsync) (struct file *, struct dentry *, int datasync);  

    /* 异步刷新待处理的数据 */
    int (*aio_fsync) (struct kiocb *, int datasync); 

    /* 通知设备FASYNC标志发生变化 */
    int (*fasync) (int, struct file *, int); 

    int (*lock) (struct file *, int, struct file_lock *); 

    ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); 

    unsigned long (*get_unmapped_area) (struct file *, unsigned long, unsigned long, unsigned long, unsigned long); 

    int (*check_flags) (int); 

    int (*flock) (struct file *, int, struct file_lock *);

    ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);

    ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int); 

    int (*setlease)(struct file *, long, struct file_lock **); 

};