VFS之基本数据结构

Posted ty_laurel

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了VFS之基本数据结构相关的知识,希望对你有一定的参考价值。

文件系统是一种存储和组织计算机中文件数据的一系列抽象数据类型,它们用来实现数据的存储、管理、查看、等功能。在Linux系统中,所有的设备、进程都是以文件的形式存在,字符设备、块设备以及这些设备的驱动均要依靠文件系统来实现,设备管理的基础框架也要以来文件系统(sysfs),所以文件系统在Linux操作系统中担任着重大的作用。

一、VFS介绍

  Linux内核通过虚拟文件系统(Virtual File System,VFS)管理文件系统。VFS对所有的文件系统(如Ext3、Ext2等)提供了一个统一的接口,它在Linux文件系统中担任着及其重要的角色,是一个不可或缺的基础设施;在文件系统的访问操作中,必须通过虚拟文件系统提供的接口才能实现正常的文件操作。同时,在Linux操作系统中,对于所有的文件系统都要遵循虚拟文件系统VFS定义的方式实现。

这里写图片描述
VFS与具体文件系统的关系

VFS支持三种类型的文件系统:基于磁盘的文件系统、特殊的文件系统和网络文件系统。

  • 基于磁盘的文件系统:管理在非易事戒指上存储的文件。这种类型的文件系统最多,比如常见的ext2/3/4、fat、ntfs等。
  • 特殊的文件系统:在内核中生成,是一种使用户应用程序与用户通信的方法,比如常见的proc文件系统,它存储于内存,不占用硬件存储空间。
  • 网络文件系统:这种文件系统可以通过网络访问另一台计算机上的数据,相当于连接到本地计算机一样。比如nfs文件系统。

二、VFS基本数据结构

VFS通用文件模型包含有4种基本的数据结构,通过这几个数据结构实现将硬盘上的文件抽象到内存中:

  • 超级块
  • 目录项
  • 索引节点
  • 文件

这四个数据结构的关系如图所示:

这里写图片描述

2.1. 超级块(super-block)

超级块代表了整个文件系统本身。Super_block保存了Linux文件系统设定的操作函数还有文件块的大小等信息;super_block是文件系统自己的控制块结构,它的链表头链接来文件系统中的所有inode。由于控制块对于一个具体的文件系统来说,有可能还包含其它的信息;想要找到这些信息,我们就可以通过超级块super_block对象进行查找。

由于超级块super_block是具体物理文件系统super_block超级块在内存的抽象,因而需要物理文件系统的超级块来获取超级快的内容。

super_operations 提供了最重要的超级块操作。例如super_operation 的成员函数read_inode提供了读取inode 信息的功能。VFS虚拟文件系统提供了文件系统的架构,具体的物理文件系统就要遵循这个定义好的架构来实现。Super_operations函数是每个文件系统都要提供的一个函数,因为要通过它来获取inode的信息。

struct super_block {
struct list_head    s_list;  /* 指向超级块链表的指针,系统将所有文件系统的超级块组成链表 */
dev_t    s_dev;  /* 设备标识符 */
unsigned long    s_blocksize;   /* 以字节为单位的块大小 */
unsigned char    s_blocksize_bits;   /* 以位为单位的块大小 */
unsigned char    s_dirt;     /* 修改脏标志 */
loff_t   s_maxbytes;    /* Max file size */
struct file_system_type *s_type;
const struct super_operations   *s_op;   /* 超级块操作方法 */
const struct dquot_operations   *dq_op;  /* 磁盘限额处理方法 */
const struct quotactl_ops   *s_qcop;     /* 磁盘限额管理方法 */
const struct export_operations *s_export_op;    /* 网络文件系统使用的输出操作 */
unsigned long    s_flags;   /* 安装标志 */
unsigned long    s_magic;   /* 文件系统的魔数 */
struct dentry    *s_root;   /* 文件系统根目录的目录项对象 */
struct rw_semaphore s_umount;   /* 卸载所用的信号量 */
struct mutex     s_lock;
int  s_count;    /* 引用计数器 */
int  s_need_sync;   /* 对超级块进行同步的标志 */
atomic_t     s_active;
#ifdef CONFIG_SECURITY
void                    *s_security;
#endif
struct xattr_handler    **s_xattr;
struct list_head    s_inodes;   /* all inodes的链表 */
struct hlist_head   s_anon;  /* anonymous dentries for (nfs) exporting */
struct list_head    s_files;    /* 文件对象链表 */
/* s_dentry_lru and s_nr_dentry_unused are protected by dcache_lock */
struct list_head    s_dentry_lru;   /* unused dentry lru */
int  s_nr_dentry_unused;    /* # of dentry on lru */
struct block_device *s_bdev;    /* 指向块设备驱动程序描述符的指针 */
struct backing_dev_info *s_bdi;
struct mtd_info  *s_mtd;
struct list_head    s_instances;    /* 用于给定文件系统类型的超级块对象链表的指针 */
struct quota_info   s_dquot;    /* Diskquota specific options */
int  s_frozen;
wait_queue_head_t   s_wait_unfrozen;    /* 进程挂起的等待队列,直到文件系统被解冻 */
char s_id[32];   /* Informational name */
void *s_fs_info;    /* Filesystem private info指向具体文件系统的超级块 */
fmode_t  s_mode;
/**
 * The next field is for VFS *only*. No filesystems have any business
 * even looking at it. You had been warned.
 */
struct mutex s_vfs_rename_mutex;    /* Kludge 当VFS通过目录重命名文件时使用的互斥锁 */
/* Granularity of c/m/atime in ns.
Cannot be worse than a second */
u32    s_time_gran;  /* 时间戳的粒度(纳秒级) */
......
};

2.2. inode节点

inode代表一个文件。Inode记录了文件的基本信息,包括文件创建时间、大小、使用者、块大小等参数,以及对文件的读写缓存和读写函数等信息。由于考虑到文件系统中存在文件的链接,所以说一个物理文件系统可能会有多个目录项dentry,指向多个文件的路径;而inode就只有一个。

索引节点inode的数据结构非常庞大,下边介绍介个比较重要的结构成员。inode的结构定义如下:

inode的代码数据结构

struct inode {
struct hlist_node   i_hash;  /* hash链表的指针 */
struct list_head    i_list;  /* backing dev IO list */
struct list_head    i_sb_list;  /* 超级块的inode链表 */
struct list_head    i_dentry;   /* 引用inode的目录项对象链表头 */
unsigned long    i_ino;  /* 索引节点号 */
atomic_t     i_count;    /* 引用计数器 */
unsigned int     i_nlink;   /* 硬链接数目 */
uid_t    i_uid;
gid_t    i_gid;
dev_t    i_rdev;     /* 实设备标识符 */
u64  i_version;  /* 版本号(每次使用后自动递增) */
loff_t   i_size;     /* 文件字节数 */
#ifdef __NEED_I_SIZE_ORDERED
seqcount_t   i_size_seqcount;
#endif
struct timespec  i_atime;
struct timespec  i_mtime;
struct timespec  i_ctime;
blkcnt_t     i_blocks;   /* 文件的块数 */
unsigned int     i_blkbits; /* 块的位数 */
unsigned short          i_bytes;    /* 块的字节数 */
umode_t  i_mode;
spinlock_t   i_lock;    /* i_blocks, i_bytes, maybe i_size 保护inode一些字段的自旋锁 */
struct mutex     i_mutex;
struct rw_semaphore i_alloc_sem;
const struct inode_operations   *i_op;  /* 索引节点的操作 */
const struct file_operations    *i_fop; /* former ->i_op->default_file_ops */
struct super_block  *i_sb;   /* 指向超级块的指针 */
struct file_lock    *i_flock;   /* 指向文件锁链表的指针 */
struct address_space    *i_mapping;
struct address_space    i_data;
#ifdef CONFIG_QUOTA
struct dquot     *i_dquot[MAXQUOTAS];   /* 索引节点的磁盘限额 */
#endif
struct list_head    i_devices;   /* 用于具体的字符或块设备索引节点链表的指针 */
union {
struct pipe_inode_info  *i_pipe;
struct block_device *i_bdev;
struct cdev  *i_cdev;
};
__u32    i_generation;  /* inode版本号(由某些文件系统使用) */
#ifdef CONFIG_FSNOTIFY
__u32    i_fsnotify_mask; /* all events this inode cares about */
struct hlist_head   i_fsnotify_mark_entries; /* fsnotify mark entries */
#endif
#ifdef CONFIG_INOTIFY
struct list_head    inotify_watches; /* watches on this inode */
struct mutex     inotify_mutex; /* protects the watches list */
#endif
unsigned long    i_state;    /* inode的状态标志 */
unsigned long    dirtied_when;  /* jiffies of first dirtying */
unsigned int     i_flags;    /* 文件系统的安装标志 */
atomic_t     i_writecount;   /* 用于写进程的引用计数器 */
#ifdef CONFIG_SECURITY
void     *i_security;    /* 指向inode安全结构的指针 */
#endif
#ifdef CONFIG_FS_POSIX_ACL
struct posix_acl    *i_acl;
struct posix_acl    *i_default_acl;
#endif
void     *i_private; /* fs or device private pointer */
};
  • 成员i_list、i_sb_list、i_dentry分别是三个链表头。i_sb_list成员用于链接到super_block中的inode链表;i_list成员用于连接描述索引节点当前状态的链表。在对一个inode进行操作时,为了表示inode处于使用状态,i_list成员需要连接到inode_in_use链表;同时文件系统的super_block的s_inodes链表头也要被i_sb_list成员链接。
  • 成员i_ino是inode的号,而i_count是inode的引用计数。成员i_size是以字节为单位的文件长度。
  • 成员i_blkbits是文件块的位数。
  • 成员i_fop是一个struct file_operations类型的指针。这个结构提供了文件的异步输入输出函数和读写函数;每个文件系统,对于文件的操作都有属于自己的函数。
  • i_mapping是一个重要的成员。这个结构主要提供了缓存文件内容的功能在对文件进行读写操作时,首先在i_mapping成员包含的缓存中寻找文件内容,缓存中如果有,就直接获取,可以大大的提高文件的读操作效率,否则的话就需要去物理磁盘读取文件内容。同样,写操作也是如此,先写入至缓存文件,然后再写入磁盘。
  • 成员i_bdev是指向块设备的指针。文件所在文件系统绑定的块设备就是成员i_bdev指向的块设备。

2.3. dentry目录项

  dentry的中文名是目录项,它是Linux中索引节点的链接。直观一点来看,大多数的文件系统都是以树状结构存储;目录递归存储,一层一层的,从树根目录开始,直到目录的最底层文件;而在Linux系统中dentry就反应了文件系统中目录和文件的树状关系。
  dentry代表逻辑意义上的文件,描述的是文件逻辑上的属性,磁盘上没有对应的映像;而inode结构代表的是物理意义上的文件,记录的是物理上的属性,对于一个具体文件系统,在磁盘上有对应的映像。一个inode对象可能对应多个目录项对象。一个有效的dentry结构必定有一个inode结构,这是因为一个目录项要么代表一个文件,要么代表一个目录。是文件:d_inode指向文件的inode结构,存储文件基本信息,该inode指向文件的数据区(data block);是目录:d_inode指向的inode结构中存储目录的基本信息,该inode指向的数据区存储目录中文件名与其对应inode信息的一个表。inode结构体中的i_mode字段记录文件的类型和权限信息,是一个文件还是目录。
  在Linux系统中所有的东西都是以文件的形式存在,同样在虚拟文件系统中,目录也相当于一个文件。根目录有一个目录项结构,并且每一个文件也都至少有一个目录项,一层一层递归,下级目录或文件链接到上级目录的dentry,二级目录、三级目录同样如此,从而形成一个树状结构,并且可以通过根目录项遍历所有的文件和目录。
  在Linux中有一个称为dentry cache的东西,是内核使用哈希表对dentry进行缓存,以便加快系统对dentry的查找。由于目录项的查找经常都是先在dentry cache中进行的,所以dentry cache在文件的操作中是经常会被用到的。
  dentry目录项和super_block的结构定义类似,很复杂,下边会列出一些比较重要的部分。简化后的dentry结构定义如下:

dentry架构的简化定义

struct dentry {
    atomic_t d_count;       /* 目录项对象引用计数器 */
    unsigned int d_flags;       /* protected by d_lock */
    spinlock_t d_lock;      /* per dentry lock */
    int d_mounted;
    struct inode *d_inode;      /* Where the name belongs to - NULL is
                     * negative */
    /*
     * The next three fields are touched by __d_lookup.  Place them here
     * so they all fit in a cache line.
     */
    struct hlist_node d_hash;   /* 哈希表项链表指针 */
    struct dentry *d_parent;    /* 父目录的目录项对象 */
    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 */
    unsigned long d_time;       /* used by d_revalidate */
    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];    /* 存放短文件名的空间 */
};

对dentry结构的成员解释如下。

  • d_inode指向一个inode结构。这个inode和dentry共同描述了一个普通文件或者目录文件。
  • d_subdirs成员和d_child成员。d_subdirs是子目录项(其可以是文件也可以是目录)的链表头,dentry结构中的所有子项都要链接到d_subdirs链表。d_child是dentry自身的链表头,需要链接到父dentry的d_subdirs成员。在进行文件的移动操作时,要将目录项结构从当前父目录项的链表上脱离,然后再将其链接至新的父dentry目录上的d_subdirs成员。
  • d_parent是指针,指向父dentry结构。
  • d_hash是链接到dentry cache的hash链表。
  • d_name成员保存的是文件或者目录的名字。在打开文件的时候,根据这个成员和目录的的名字来寻找目标文件。
  • d_mounted用来指目录项是否是一个挂载点。如果是挂载点,该成员不为零。

2.4. file

  描述文件和进程交互的关系就是文件对象的作用。实际上,在硬盘上就不存在一个文件结构。对于同一个文件,在不一样的进程中就有不一样的文件对象;在用户进程打开一个文件时,系统内核就会动态的创建一个对应的文件对象。
  file结构中主要保存了文件位置,指向该文件索引节点的指针等。为什么不把文件位置存放在索引节点中,而要设一个file数据结构呢?
  我们知道,Linux中你给的文件是能够共享的,假如把文件位置存放在索引节点中,则如果有两个或更多个进程同时打开同一个文件时,它们将去访问同一个索引节点,于是一个进程的lseek操作将影响到另一个进程的读操作,这显然是不允许也是不可想象的。
文件的数据结构

struct file {
    struct path     f_path;
    #define f_dentry    f_path.dentry   /*文件对象的目录项*/
    #define f_vfsmnt    f_path.mnt      /*含有该文件的已挂载文件系统*/
    const struct file_operations    *f_op;
    spinlock_t      f_lock;      /* f_ep_links, f_flags, no IRQ */
    atomic_long_t       f_count;    /*文件对象引用计数器*/
    unsigned int        f_flags;    /*打开文件时所制定的标志*/
    fmode_t         f_mode;         /*进程的访问模式*/
    loff_t          f_pos;          /*当前文件的位移量*/
    struct fown_struct   f_owner;
    unsigned int     f_uid,  f_gid;
    struct file_ra_state     f_ra;
    struct address_space    *f_mapping;     /*指向文件地址空间对象的指针*/
} ;
  • 成员f_dentry和f_vfsmnt成员分别指向文件对应的目录项结构和文件所属于的已挂载文件系统。
  • f_pos成员存储着进程操作文件对象时的位置。比如读取文件的前20个字节内容,f_pos成员就会指向第21个字节位置。
  • f_uid和f_gid分别表示文件的用户ID和用户组ID。
  • f_mapping指向一个address_space结构。

2.5 用户打开文件表files_struct

文件描述符是用来描述打开的文件的。每个进程用一个files_struct结构来记录文件描述符的使用情况,这个files_struct结构称为用户打开文件表,它是进程的私有数据。该结构定义如下:

/*
 * Open file table structure
 */
struct files_struct {
  /*
   * read mostly part
   */
    atomic_t count;
    struct fdtable __rcu *fdt;
    struct fdtable fdtab;
  /*
   * written part on a separate cache line in SMP
   */
    spinlock_t file_lock ____cacheline_aligned_in_smp;
    int next_fd;
    unsigned long close_on_exec_init[1];
    unsigned long open_fds_init[1];
    struct file __rcu * fd_array[NR_OPEN_DEFAULT];
};

一个进程有一个files_struct结构体对象,该结构体中有一个文件描述符表对象,该对象是一个数组结构,数组中的每一个元素指向一个进程打开的文件对象file,通过file对象就可以对文件进行操作。每个进程都有一个task_struct结构体对象,该对象中可以找到files_struct对象,从而由files_struct对象的文件描述符数组中找到指向的file对象。

三、read系统调用在VFS中的实现

在用户态使用read函数时,内核会根据具体系统调用来选择对应的处理函数(即就是read函数)。

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
struct file *file;
ssize_t ret = -EBADF;
int fput_needed;
file = fget_light(fd, &fput_needed);     /*根据fd指定的索引,从当前进程描述符中取出相应的file对象*/
if (file) {
    loff_t pos = file_pos_read(file);     /*返回当前struct file结构体的当前操作指针位置*/
    ret = vfs_read(file, buf, count, &pos);
    file_pos_write(file, pos);      /*更新文件的当前读写位置*/
    fput_light(file, fput_needed);     /*更新文件的引用计数*/
}
return ret;
}

vfs_read函数根据file对象当前操作指针的位置得到f_op(file_operations结构体)的指针,从而获得file_operations结构体中的read操作函数,对文件进行read操作,将内核读取的文件内容存入char __user* buf指向的内存地址

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    ssize_t ret;
    if (!(file->f_mode & FMODE_READ))
        return -EBADF;

    if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))
        return -EINVAL;
    if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
        return -EFAULT;
    ret = rw_verify_area(READ, file, pos, count);
    if (ret >= 0) {
        count = ret;
        if (file->f_op->read)     /*文件系统定义了file_operations的read(同步读)函数就使用它读;否则使用读函数do_sync_read*/
            ret = file->f_op->read(file, buf, count, pos);
        else
            ret = do_sync_read(file, buf, count, pos);        /*调用f_op->aio_read(异步读)*/
        if (ret > 0) {
            fsnotify_access(file->f_path.dentry);    /*读取成功,唤醒相关的等待进程*/
            add_rchar(current, ret);
        }
        inc_syscr(current);
    }
    return ret;
}

以上是关于VFS之基本数据结构的主要内容,如果未能解决你的问题,请参考以下文章

VFS - 虚拟文件系统基本操作方法的封装

VFS - 虚拟文件系统基本操作方法的封装

linux内核源码分析之虚拟文件系统VFS

VFS - 代码生成器预览功能实现

VFS - 虚拟文件系统的加载和导出

VFS - 虚拟文件系统的加载和导出