虚拟文件系统VFS

708 阅读40分钟

0. Unix五个标准文件类型

  • 普通文件
  • 目录文件
  • 符号链接文件
  • 设备文件
  • 管道文件

虚拟文件系统的设计目标、结构及实现

1. 虚拟文件系统(VFS)的作用

是一个内核软件层,用来处理与Unix标准文件系统相关的所有系统调用,可以为各种文件系统提供一个通用的接口

VFS支持的文件系统可以分为三类:

  • 磁盘文件系统:Ext2、Systen V、MS-DOS等等
  • 网络文件系统:NFS
  • 特殊文件系统

Unix的目录建立了一棵根目录为"/"的树。根目录包含在根文件系统中,在Linux中这个根文件系统通常是Ext2Ext3类型。其他所有的文件系统都可以被“安装”在根文件系统的子目录中。

1.1 通用文件模型

VFS所隐含的主要思想在于引入了一个通用的文件系统

从本质上来说,Linux内核不能对一个特定的函数进行硬编码来执行诸如read()或ioctl()这样的操作,而是对每个操作必须使用一个指针,指向要访问的具体文件系统的适当函数。

内核如何把read()转换为专对MS-DOS文件系统的一个调用。

  • 应用程序对read()的调用引起内核调用相应的sys_read()服务例程,这与其他系统调用完全类似。文件在内核内存中是由一个file数据结构来表示的。这种数据结构中包含一个称为f_op的字段,该字段中包含一个指向专对MS-DOS文件的函数指针,当然还包括读文件的函数。sys_read()查找到指向该函数的指针,并调用它。这样一来,应用程序的read()就被抓换为相对间接的调用
  • file->f_op->read(...)
  • 内核负责把一组合适的指针分配给与每个打开文件相关的file变量,然后负责调用针对每个具体文件系统的函数(由f_op字段指向)。

1.2 VFS的对象类型

  • 超级块对象super_block :存放已安装文件系统的有关信息。对基于磁盘的文件系统,这类对象通常应用于存放在磁盘上的文件系统控制块FCB
  • 索引节点对象inode :存放关于具体文件一般信息。对基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件控制块FCB。每个索引节点对象都有一个索引节点号,这个节点号唯一地表示文件系统中的文件。
  • 文件对象file :存放打开文件与进程之间进行交互的有关信息,这类信息仅当进程访问文期间存在于内核内存中。
  • 目录项对象dentry :存放目录项与对应文件进行链接的有关信息。每个磁盘文件系统都以自己特有的方式将该类信息存在磁盘上。

1.3 示例

一个进程如何跟文件进行交互的示例

三个不同进程已经打开同一个文件,其中两个进程使用同一个硬链接。在这种情况下,其中的每个进程都使用自己的文件对象,但只需要两个目录项对象,每个硬链接对应一个目录项对象。这两个目录项对象指向同一个索引节点对象,该索引节点对象标识超级块对象,以及随后的普通磁盘文件。

1.4 目录项高速缓存 dentry cache

最近最常使用的目录项对象被放在所谓目录项高速缓存dentry cache磁盘高速缓存中,以加速从文件路径名到最后一个路径分量的索引节点的转换过程。

一般来说,磁盘高速缓存disk cache属于软件机制,它允许内核将原本存在磁盘上的某些信息保存在RAM中,以便对这些数据的进一步访问能够快速进行,而不必慢速访问磁盘本身,

磁盘高速缓存不同于硬件高速缓存或内存高速缓存,后两者都与磁盘或其他设备无关。硬件高速缓存是一个快速静态RAM,它加快了直接对慢速动态RAM的请求。内存高速缓存是一种软件机制,引入它是为了绕过内核内存分配器。

1.4.1作用

提高文件系统的性能,减少对磁盘的重复访问,加快目录的查找操作

做法:文件系统查找时,先查找dentry cache,如果命中了dentry,就可以直接获取相关信息,无需再次从磁盘读取

1.4.2 哈希表和LRU
  • 哈希表用于实现快速查找,
  • LRU算法用于在缓存不足时,决定哪些dentry应该被删除以释放空间

目录项高速缓存、索引节点高速缓存、页高速缓存

1.5 VFS所处理的系统调用

涉及到文件系统、普通文件、目录文件及符号链接文件;ioperm()、ioctl()、pipe()和mknod()涉及到设备文件和管道文件;最后一组由VFS处理的系统调用,诸如socket()、connect()和bind()属于套接字系统调用,用于实现网络功能

系统调用名说明
mount()、umount()、umount2()安装/卸载文件系统
sysfs()获取文件系统信息
statfs()、fstatfs()、statfs64()、fstatfs64()、ustat()获取文件系统统计信息
chroot()、pivot_root()更改根目录
chdir()、fchdir()、getcwd()对当前目录进行操作
mkdir() 、rmdir()创建/删除目录
getdents()、getdents64()、readdir()、link()、unlink()、rename()、lookup_dcookie()对目录项进行操作
readlink()、symlink()对软链接进行操作
chown()、fchown()、lchown()、chown16()、fchown16()、lchown16()更改文件所有者属性
chmod()、fchmod()、utime()更改文件属性
stat()、fstat()、lstat()、access()、oldstat()、oldfstat()、oldlstat()、stat64()、lstat64()、fstat64()读取文件状态
open()、close()、creat()、umask()打开、关闭、创建文件
dup()、dup2()、fcntl()、fcntl64()对文件描述符进行操作
select()、poll()等待一组文件描述符上发生的事件
truncate()、ftruncate()、truncate64()、ftruncate64()更改文件长度
lseek() 、__lseek()更改文件指针
read()、write()、readv()、writev()、sendfile()、sendfile64()、readhead()进行文件I/O操作
io_setup()、io_submit()、io_getevents()、io_cancel()、io_destroy()异步IO(允许多个读请求和写请求)
pread64()、pwrite64()搜索并访问文件
mmap()、mmap2()、munmap()、madvise()、mincore()、remap_file_pages()处理文件内存映射
fdatasync()、fsync()、sync()、msync()、flock()同步文件数据
flock()处理文件锁
setxaatr()、lsetxattr()、fsetxattr()、getxattr()、lgetxattr()、fgetxattr()、listxattr()、llistxattr()、flistxattr()、removexattr()、lremovexattr()、fremovexattr()处理文件扩展属性

当进程关闭一个打开的文件时,并不需要涉及磁盘上的相应文件,因此VFS只需释放对应的文件对象。从某种意义上说,可以把VFS看成“通用”文件系统,它在必要时以来某种具体文件系统

2. VFS的数据结构

内核可以动态地修改对象的方法,因此可以为对象建立专用的行为,下面几节详细介绍VFS的对象及其内在关系。

2.1 超级块对象 super_block

一个全局的数据结构,超级块描述已安装的文件系统。文件系统的控制信息都存储在超级块中,超级块是文件系统的控制块,有整个文件系统信息,一个文件系统所有的inode都要连接到超级块上,可以说,一个超级块就代表了一个文件系统。

  • VFS超级块是各种具体文件系统在安装时建立的,并在这些文件系统卸载时自动删除这里有个问题,在安装时候是具体怎么来建立的,又是怎样实现卸载时候的自动删除的。
  • VFS超级块只存在于内存中,同时提到VFS超级块也应该说成是哪个具体文件系统的VFS超级块

在**include/linux/fs.h**中定义

struct super_block {
  struct list_head  s_list;    /* Keep this first 指向超级块链表的指针*/
  dev_t      s_dev;    /* search index; _not_ kdev_t 设备表示符*/
  unsigned char    s_blocksize_bits;   /*块大小的值占用的位数,例如,如果块大小为1024 字节,则该值为10*/
  unsigned long    s_blocksize;  /*该具体文件系统中数据块的大小,以字节为单位 */
  loff_t      s_maxbytes;  /* Max file size 文件的最大长度 */   
  struct file_system_type  *s_type;  /*指向文件系统的file_system_type 数据结构的指针 */
  const struct super_operations  *s_op;  /*指向某个特定的具体文件系统的用于超级块操作的函数集合 */
  const struct dquot_operations  *dq_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; /*对超级块读写时进行同步*/
  int      s_count;  /*对超级块的使用计数*/
  atomic_t    s_active;
#ifdef CONFIG_SECURITY
  void                    *s_security;
#endif
  const struct xattr_handler **s_xattr;

  struct list_head  s_inodes;  /* all inodes  把所有索引对象链接在一起,存放的是头结点*/
  struct hlist_bl_head  s_anon;    /* anonymous dentries for (nfs) exporting */
#ifdef CONFIG_SMP
  struct list_head __percpu *s_files;
#else
  struct list_head  s_files;  //链接所有打开的文件
#endif
  struct list_head  s_mounts;  /* list of mounts; _not_ for fs use */
  /* s_dentry_lru, s_nr_dentry_unused protected by dcache.c lru locks */
  struct list_head  s_dentry_lru;  /* unused dentry lru */
  int      s_nr_dentry_unused;  /* # of dentry on lru */

  /* s_inode_lru_lock protects s_inode_lru and s_nr_inodes_unused */
  spinlock_t    s_inode_lru_lock ____cacheline_aligned_in_smp;
  struct list_head  s_inode_lru;    /* unused inode lru */
  int      s_nr_inodes_unused;  /* # of inodes on lru */

  struct block_device  *s_bdev;
  struct backing_dev_info *s_bdi;
  struct mtd_info    *s_mtd;
  struct hlist_node  s_instances;
  struct quota_info  s_dquot;  /* Diskquota specific options */

  struct sb_writers  s_writers;

  char s_id[32];        /* Informational name 文本名字 */
  u8 s_uuid[16];        /* UUID */

  void       *s_fs_info;  /* Filesystem private info  文件系统特设信息*/
  unsigned int    s_max_links;
  fmode_t      s_mode;

  /* Granularity of c/m/atime in ns.
     Cannot be worse than a second */
  u32       s_time_gran;

  /*
   * 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 */

  /*
   * Filesystem subtype.  If non-empty the filesystem type field
   * in /proc/mounts will be "type.subtype"
   */
  char *s_subtype;

  /*
   * Saved mount options for lazy filesystems using
   * generic_show_options()
   */
  char __rcu *s_options;
  const struct dentry_operations *s_d_op; /* default d_op for dentries */

  /*
   * Saved pool identifier for cleancache (-1 means none)
   */
  int cleancache_poolid;

  struct shrinker s_shrink;  /* per-sb shrinker handle */

  /* Number of inodes with nlink == 0 but still referenced */
  atomic_long_t s_remove_count;

  /* Being remounted read-only */
  int s_readonly_remount;
};

每个已安装的文件系统都有一个超级块,所有超级块对象以双向循环链表的形式连接在一起。链表中第一个元素和最后一个元素的地址分别存放在super_blocks变量的s_list域的nextprev域中。

s_fs_info字段指向属于具体文件系统的超级块信息。为了效率起见,该信息通常被复制到内存(为什么)。任何基于磁盘的文件系统都需要访问和更改自己的磁盘分配位图这是个什么东东)(磁盘分配位图指存在于磁盘中用来标识磁盘每个块是否空间的一段存储空间),以便分配或释放块。VFS允许这些文件系统直接对内存超级块的s_fs_info字段进行操作,而无需访问磁盘。

与超级块关联的方法就是超级块操作,由super_operation来描述,该结构的起始地址存放在超级块的s_op字段中。

struct super_operations {
     struct inode *(*alloc_inode)(struct super_block *sb);  /* 为索引节点对象分配空间,包括具体文件系统的数据所需要的空间。*/
  void (*destroy_inode)(struct inode *); /* 撤销索引节点对象,包括具体文件系统的数据。 */

     void (*dirty_inode) (struct inode *, int flags); /* 当索引节点标记为修改(脏)时调用。*/
  int (*write_inode) (struct inode *, struct writeback_control *wbc);  /* 用通过传递参数指定的索引节点对象的内容更新一个文件系统的索引节点。*/
  int (*drop_inode) (struct inode *); 
  
  void (*evict_inode) (struct inode *);
  void (*put_super) (struct super_block *);  /* 释放通过传递的参数指定的超级块对象(因为相应的文件系统被卸载)。*/
  int (*sync_fs)(struct super_block *sb, int wait); /* 在清除文件系统来更新磁盘上的具体文件系统数据结构时调用(由日志文件系统使用)。*/
  int (*freeze_fs) (struct super_block *);
  int (*unfreeze_fs) (struct super_block *);
  int (*statfs) (struct dentry *, struct kstatfs *); /* 将文件系统的统计信息返回,填写在buf缓冲区中。*/
  int (*remount_fs) (struct super_block *, int *, char *);  /* 用新的选项重新安装文件系统(当某个安装选项必须被修改时被调用)。*/
  void (*umount_begin) (struct super_block *);  /* 中断一个安装操作,因为相应的卸载操作已经开始(只在网络文件系统中使用)。*/

  int (*show_options)(struct seq_file *, struct dentry *); /* 用来显示特定文件系统的选项。*/   
  int (*show_devname)(struct seq_file *, struct dentry *);
  int (*show_path)(struct seq_file *, struct dentry *);
  int (*show_stats)(struct seq_file *, struct dentry *); /* 用来显示特定文件系统的状态。*/
#ifdef CONFIG_QUOTA //一般情况下用不到最后两个方法
  ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);/* 限额系统使用该方法从文件中读取数据,该文件详细说明了所在文件系统的限制。*/ 
  ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t); /* 限额系统使用该方法将数据写入文件中,该文件详细说明了所在文件系统的限制。*/
#endif
  int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
  int (*nr_cached_objects)(struct super_block *);
  void (*free_cached_objects)(struct super_block *, int);
};

每一个文件系统都可以定义自己的超级块操作,它可以是上面结构的一个子集,因为有些操作可能不需要,未实现的方法对应的字段置为NULL,当我们需要调用其中一个操作时,比如read_inode,执行以下操作

s_b->s_op->read_node(inode);

这里的s_b存放涉及到超级块对象的地址super_operations表的read_inode字段存放着函数的地址,此时,函数被直接调用。

  • 可以在描述文件系统类型的另一个对象中找到等价的get_sb方法。该方法定义在具体文件系统的file_system_type结构中,在register_filesystem的时候会把文件系统注册进去。

2.2 索引节点inode

  • 文件系统处理文件所需要的所有信息都在inode中,用于描述一个文件的元信息,其中包含的是诸如文件的大小、拥有者、创建时间、磁盘位置等和文件相关的信息,所有文件都有一个对应的inode结构。文件名可以随意更改,但是索引节点对文件是唯一的,并且随文件的存在而存在
  • 具体文件系统的索引节点是存储在磁盘上的,是一种静态结构,要使用的话,必须调入内存,因此,VFS索引节点也叫动态节点

include/linux/fs.h

struct inode {
  umode_t      i_mode;  /*文件的类型与访问权限 */
  unsigned short    i_opflags;
  kuid_t      i_uid;   /*文件拥有者标识号*/
  kgid_t      i_gid;   /*文件拥有者所在组的标识号*/
  unsigned int    i_flags; /*文件系统的安装标志*/

#ifdef CONFIG_FS_POSIX_ACL
  struct posix_acl  *i_acl;
  struct posix_acl  *i_default_acl;
#endif

  const struct inode_operations  *i_op; /*索引节点的操作*/
  struct super_block  *i_sb;  /*指向该文件系统超级块的指针 */

/************用于分页机制的域**********************************/
  struct address_space  *i_mapping; /* 把所有可交换的页面管理起来*/



#ifdef CONFIG_SECURITY
  void      *i_security;
#endif

  /* Stat data, not accessed from path walking */
  unsigned long    i_ino;  /*索引节点号*/
  /*
   * Filesystems may only read i_nlink directly.  They shall use the
   * following functions for modification:
   *
   *    (set|clear|inc|drop)_nlink
   *    inode_(inc|dec)_link_count
   */
  union {
    const unsigned int i_nlink;
    unsigned int __i_nlink;
  };
  dev_t          i_rdev;   /*实际设备标识号*/
  loff_t          i_size;   /*文件的大小(以字节为单位)*/
  struct timespec    i_atime;
  struct timespec    i_mtime;  /*文件的最后修改时间*/
  struct timespec    i_ctime;  /*节点的修改时间*/
  spinlock_t    i_lock;  /* i_blocks, i_bytes, maybe i_size */ /*该节点是否被锁定,用于同步操作中*/
  unsigned short          i_bytes;
  unsigned int    i_blkbits;
  blkcnt_t    i_blocks;  /*块大小*/

#ifdef __NEED_I_SIZE_ORDERED
  seqcount_t    i_size_seqcount;
#endif

  /* Misc */
  unsigned long    i_state;  /*索引节点的状态标志*/
  struct mutex    i_mutex;

  unsigned long    dirtied_when;  /* jiffies of first dirtying */

  struct hlist_node  i_hash;  /*指向哈希链表的指针*/
  struct list_head  i_wb_list;  /* backing dev IO list */
  struct list_head  i_lru;    /* inode LRU list */
  struct list_head  i_sb_list;
  union {
    struct hlist_head  i_dentry;
    struct rcu_head    i_rcu;
  };
  u64      i_version;
  atomic_t    i_count; /*当前使用该节点的进程数。计数为0,表明该节点可丢弃或被重新使用 */
  atomic_t    i_dio_count;
  atomic_t    i_writecount;
  const struct file_operations  *i_fop;  /* former ->i_op->default_file_ops */ /*指向文件操作的指针 */
  struct file_lock  *i_flock; /*指向文件加锁链表的指针*/
  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;

#ifdef CONFIG_FSNOTIFY
  __u32      i_fsnotify_mask; /* all events this inode cares about */
  struct hlist_head  i_fsnotify_marks;
#endif

#ifdef CONFIG_IMA
  atomic_t    i_readcount; /* struct files open RO */
#endif
  void      *i_private; /* fs or device private pointer */
};

inode中几个比较重要的成员

  • i_uid:文件所属的用户
  • i_gid:文件所属的组
  • i_rdev:文件所在的设备号
  • i_size:文件的大小
  • i_atime:文件的最后访问时间
  • i_mtime:文件的最后修改时间
  • i_ctime:文件的创建时间
  • i_opinode相关的操作列表
  • i_fop文件相关的操作列表
  • i_sb:文件所在文件系统的超级块

重点说明i_opi_fop这两个成员

  • i_op定义了对目录相关的操作方法列表。

    • mkdir()系统调用会触发inode->i_op->mkdir()方法
    • link()系统调用会触发inode->i_op->link()方法
  • i_fop定义了打开文件后对文件的操作方法列表

    • read() 系统调用会触发inode->i_fop->read()方法
    • write() 系统调用会触发 inode->i_fop->write() 方法

进一步说明

  • 每个文件都有一个inode,每个inode 有一个索引节点号i_ino。在同一个文件系统中,每个索引节点号都是唯一的,内核有时根据索引节点号的哈希值查找其inode结构。

  • 每个文件都有个文件主,其最初的文件主是创建了这个文件的用户,但以后可以改变。每个用户都有一个用户组,且属于某个用户组,因此,inode 结构中就有相应的i_uidi_gid,以指明文件主的身份。

  • inode 中有两个设备号,i_devi_rdev。首先,除特殊文件外,每个节点都存储在某个设备上,这就是i_dev。其次,如果索引节点所代表的并不是常规文件,而是某个设备,那就还得有个设备号,这就是i_rdev。

  • 每当一个文件被访问时,系统都要在这个文件的inode 中记下时间标记,这就是inode中与时间相关的几个域。

  • 属于“ 正在使用” 或“ 脏” 链表的索引节点对象也同时存放在一个称为inode_hashtable 链表中。

    • 哈希表加快了对索引节点对象的搜索,前提是系统内核要知道索引节点号及对应文件所在文件系统的超级块对象的地址。
    • 由于散列技术可能引发冲突,所以,索引节点对象设置一个i_hash 域,其中包含向前和向后的两个指针,分别指向散列到同一地址的前一个索引节点和后一个索引节点;该域由此创建了由这些索引节点组成的一个双向链表。
    • 索引节点关联的方法也叫索引节点操作,由inode_operations结构来描述,该结构的地址存放在i_op 域中,该结构也包括一个指向文件操作方法的指针。

2.3 目录项对象

dentry定义在include/linux/dcache.h

目录项的主要作用方便查找文件。一个路径的各个组成部分,不管是目录还是普通的文件,都是一个目录项对象。

  • 例如:在路径/home/tdw/example.c中,目录/ home/ tdw/和文件example.c都对应一个目录项对象
  • 不同于前面的两个对象,目录项对象没有对应的磁盘数据结构VFS在遍历路径名的过程中现场将它们逐个地解析成目录项对象这里的不同点如何来讲解呢
  • 目录项对象在磁盘上并没有对应的镜像,因此在dentry结构中不包含指出该对象已被修改的字段,如何理解这句话
  • 目录项对象存放在名为dentry_cache的slab分配器高速缓存中,因此,目录项对象的创建和删除是通过调用kmem_cahe_alloc()kmem_cache_free()实现的

每个目录项对象可以处于一下四种状态之一

  • 空闲状态free :处于该状态的目录项目对象不包括有效的信息,而且还没有被VFS使用。对应的内存区由slab分配器进行处理
  • 未使用状态 unused:处于该状态的目录项对象当前还没有被内核使用对象的引用计数d_count的值为0,但其d_inode字段仍然指向关联的索引节点对象。该目录项对象包含有效的信息,然是为了在必要时回收内存,它的内容可能被丢弃。
  • 正在使用状态 in use:处于该状态的目录项对象当前正在被内核使用。该对象的引用计数器d_count的值为正数,其d_inode字段指向关联的索引节点对象。该目录项对象包含有效的信息,并且不能被丢弃。
  • 负状态 negative与目录项关联的索引节点不复存在,那是因为相应的磁盘索引节点已被删除,或者因为目录项对象是通过解析一个不存在文件的路径名创建的。目录项对象的d_inode字段被置为NULL,但该对象仍然被保存在目录项高速缓存中,以便后续对同一文件目录名的查找操作能够快速完成。

目录项源代码include/linux/dcache.h

struct dentry {
  /* RCU lookup touched fields */
  unsigned int d_flags;    /* protected by d_lock */
  seqcount_t d_seq;    /* per dentry seqlock */
  struct hlist_bl_node d_hash;  /* lookup hash list */
  struct dentry *d_parent;  /* parent directory */
  struct qstr d_name;
  struct inode *d_inode;    /* Where the name belongs to - NULL is
           * negative */
  unsigned char d_iname[DNAME_INLINE_LEN];  /* small names */

  /* Ref lookup also touches following */
  unsigned int d_count;    /* protected by d_lock */
  spinlock_t d_lock;    /* per dentry lock */
  const struct dentry_operations *d_op;  //注意这个指针,指向响应的目录项的操作函数
  struct super_block *d_sb;  /* The root of the dentry tree */
  unsigned long d_time;    /* used by d_revalidate */
  void *d_fsdata;      /* fs-specific data */

  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 hlist_node d_alias;  /* inode alias list */
};

与目录项对象关联的方法称为目录项操作。这些方法由dentry_operations结构加以描述,该结构的地址存放在目录项对象的d_op字段中。尽管一些文件系统定义了它们自己的目录项方法,但是这些字段通常为NULL,而VFS使用缺省函数代替这些方法:

struct dentry_operations {
  int (*d_revalidate)(struct dentry *, unsigned int);
  int (*d_weak_revalidate)(struct dentry *, unsigned int);
  int (*d_hash)(const struct dentry *, const struct inode *,
      struct qstr *);
  int (*d_compare)(const struct dentry *, const struct inode *,
      const struct dentry *, const struct inode *,
      unsigned int, const char *, const struct qstr *);
  int (*d_delete)(const struct dentry *);
  void (*d_release)(struct dentry *);
  void (*d_prune)(struct dentry *);
  void (*d_iput)(struct dentry *, struct inode *);
  char *(*d_dname)(struct dentry *, char *, int);
  struct vfsmount *(*d_automount)(struct path *);
  int (*d_manage)(struct dentry *, bool);
} ____cacheline_aligned;

下面对于dentry结构给出进一步的解释

  • dentry与inode的关系一个有效的dentry必定有一个inode结构,这是因为一个目录项要么代表着一个文件,要么代表一个目录,而目录实际上也是文件。只要dentry结构是有效的,则其d_inode指针必定指向一个inode结构。可是,反过来则不然,一个inode却可能对应着不止一个dentry结构;也就是说,一个文件可以有不止一个文件名或路径名。这是因为一个已经建立的文件可以被链接link到其他文件名。所以在inode结构中有一个队列i_dentry,凡是代表着同一个文件的所有目录项都通过其dentry结构中的d_alias域挂入相应的inode结构中的identry队列。

  • dentry_hashtable:在内核中有一个哈希表dentry_hashtable,是一个list_head的指针数组。一旦在内存中建立起一个目录节点的dentry结构,该dentry结构就通过其d_hash域链入哈希表中。

  • dentry_unused队列:内核中还有一个队列dentry_unused,凡是已经**没有用户(count域为0)使用的dentry结构就通过其d_lru域挂入这个队列。当内存资源紧张需要回收资源时,会从DCACHE_LRU链表中删除一些dentry并释放其占用的资源

  • dentry 结构中除了d_alias、d_hash、d_lru 三个队列外,还有d_vfsmnt、d_child 及d_subdir 三个队列。

    • 其中d_vfsmnt 仅在该dentry 为一个安装点时才使用。
    • 当该目录节点有父目录时,则其dentry 结构就通过d_child挂入其父节点的d_subdirs 队列中,同时又通过指针d_parent指向其父目录的dentry 结构,而它自己各个子目录的dentry 结构则挂在其d_subdirs 域指向的队列中。

从上面的叙述可以看出,一个文件系统中所有目录项结构或组织为一个哈希表d_hash),或组织为一棵树d_child、d_parent、d_subdir),或按照某种需求组织为一个链表d_ lru),这将为文件访问和文件路径搜索奠定下良好的基础。

2.4 目录项高速缓存

由于从磁盘读入一个目录项构造相应的目录项对象需要花费大量时间,所以,在完成对目录项对象的操作后,可能后面还要使用它,因此仍在内存中保留它有重要的意义。

为了最大限度地提高处理这些目录项对象的效率,Linux使用目录项高速缓存,它由两种类型的数据结构组成:

  1. 一个处于正在使用,未使用或负状态的目录项对象的集合
  2. 一个散列表,从中能快速获取与给定的文件名和目录名对应的目录项对象。同样,如果访问的对象不再目录项高速缓存中,则散列函数返回一个空值。

目录项高速缓存的作用还相当于索引节点高速缓存(inode cache)的控制器。在内核内存中,并不丢弃与未用目录项相关的索引节点,这是由于目录项高速缓存仍在使用它们。因此,这些索引节点对象保存在RAM中,并且能够借助相应的目录项快速引用他们。这里的这个说法是因为unused状态的dentry对象的存在嘛

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

  • “未被使用的”目录项双向链表

    • 所有unused目录对象都存放在一个LRU的双向链表中,该链表按照插入的时间排序
    • 换句话说,最后释放的目录对象放在链表的首部,所以最近最少使用的目录项对象总是靠近链表的尾部。一旦目录项高速缓存的空间开始变小,内核就从链表的尾部删除元素,使得最近最常用的对象得以保存
    • LRU链表的首元素和尾元素的地址存放在list_head类型的dentry_unused变量的next字段和prev字段中。目录项对象的d_lru字段包括指向链表中的相邻目录项的指针。
  • “被使用的”双向链表

    • 每个in use的目录项对象都被插入一个正在使用的双向链表中
    • 该链表由相应索引节点对象的i_dentry字段所指向,这句话读起来好抽象(由于每个索引节点可能与若干个硬链接相关联,所以需要一个链表)
    • 目录项对象的d_alias字段存放链表中相邻元素的地址。这两个字段的类型都是 struct list_head
    • 当指向相应文件的最后一个硬链接被删除后,一个in use的目录项对象可能变成negative 。在这种情况下,该目录项对象被移到unused目录项对象组成的LRU链表中。每当内核缩减目录项高速缓存时, negative目录项对象就朝着LRU链表的尾部移动,这样一来,这些对象就逐渐被释放
  • 散列表和相应的散列函数—看起来像是拉链法

    • 散列表是由dentry_hashtable数组实现的,方便VFS快速索引dentry
    • 数组中每个元素是一个指向链表的指针,这种链表就是把具有相同散列值的目录项进行散列而形成的。
    • 该数组的长度取决于系统已经安装的RAM的数量,缺省值就是每MB包含256个元素,意思是一个占用的长度是4KB嘛
    • 散列函数产生的值是由目录项对象及其文件名计算出来的

还有一个数据结构就是表示父子结构的树/链表这里是不是称为树形结构会更好一点呢


3. 与进程相关的文件结构

3.1 文件对象

3.1.1文件描述符 fd
  • 在Linux中,进程是通过文件描述符 file descirptows,fd而不是文件名来访问文件的,文件描述符实际上是一个整数。
  • Linux中规定每个进程最多能同时使用NR_OPEN个文件描述符(进程最大打开文件数限制sysctl_nr_open
  • sysctl_nr_open值在include/linux/fs.h中定义,为1024×1024(2.0 版中仅定义为256)
3.1.2文件位置
  • 每个文件都有一个32位的数字来表示下一个读写的字节位置,叫做文件位置
  • 每次打开一个文件,除非明确要求,否则文件位置都被置为0 ,即文件的开始处,此后的读写操作都从文件的开始处执行,但可以通过执行系统调用lseek对这个文件位置进行修改
3.1.3打开的文件描述符 open file descriptor
  • Linux中专门用了一个数据结构file来保存打开文件的文件位置,这个结构称为打开的文件描述符。这个数据结构的设置是煞费苦心的,因为它与进程的联系非常紧密,可以说这个是VFS中一个比较难以理解的数据结构。总结下来就是,不同的进程之间不能共享文件位置,父子进程之间需要共享文件位置(这个就是设置file结构的原因)。

  • 为什么不把文件位置干脆放在索引节点中,而要多次一举,设置一个新的数据结构呢?

    • 为了避免多个进程对同一个文件的lseek操作互相影响:我们知道,Linux的文件是能够共享的,假如把文件位置存放在索引节点中,则如果有两个或更多个进程同时打开同一个文件时,它们将去访问同一个索引节点,于是一个进程的lseek操作将影响到另一个进程的读操作,这显然是不允许也不可想象的。
  • 既然进程是通过文件描述符访问文件的,为什么不用一个与文件描述符相平行的数组来保存每个打开文件的文件位置?

    • 尽管设置平行数组保存每个打开文件的文件位置可以解决多个进程对一个文件操作的问题,但是无法解决子进程继续操作文件的问题。设置平行数组这个想法也是不能实现的,原因就在于子进程会共享父进程内核态的所有信息,包括文件描述符数组
    • 一个文件不仅可以被不同的进程分别打开,而且也可以被同一个进程先后多次打开。一个进程如果先后多次打开同一个文件,则每一次打开都要分配一个新的文件描述符,并且指向一个新的file 结构,尽管它们都指向同一个索引节点,但是,如果一个子进程不和父进程共享同一个file 结构,而是也如上面一样,分配一个新的file 结构,会出现什么情况了?让我们来看一个例子。
    • 假设有一个输出重定位到某文件A 的shell script(shell 脚本),我们知道,shell是作为一个进程运行的,当它生成第1 个子进程时,将以0 作为A 的文件位置开始输出,假设输出了2KB 的数据,则现在文件位置为2KB。然后,shell 继续读取脚本,生成另一个子进程,它要共享shell 的file 结构,也就是共享文件位置,所以第2 个进程的文件位置是2KB,将接着第1 个进程输出内容的后面输出。如果shell 不和子进程共享文件位置,则第2 个进程就有可能重写第1 个进程的输出了,这显然不是希望得到的结果
3.1.4 file 结构

file结构中主要保存了文件位置,和指向该文件索引节点的指针

系统打开文件表:file结构形成一个双链表,称为系统打开文件表,最大长度是NR_FILE,在fs.h中定义为8192。file结构在include/linux/fs.h中定义如下

struct file {
  /*
   * fu_list becomes invalid after file_free is called and queued via
   * fu_rcuhead for RCU freeing
   */
  union {
    struct list_head  fu_list; //文件对象链表
    struct rcu_head   fu_rcuhead; //释放之后的RCU链表
  } f_u;
  struct path    f_path;          //包含目录项
#define f_dentry  f_path.dentry
  struct inode    *f_inode;  /* cached value */
  const struct file_operations  *f_op; // 文件的操作列表

  /*
   * Protects f_ep_links, f_flags, f_pos vs i_size in lseek SEEK_CUR.
   * Must not be taken from IRQ context.
   */
  spinlock_t    f_lock;      //单个文件结构锁
#ifdef CONFIG_SMP
  int      f_sb_list_cpu;
#endif
  atomic_long_t    f_count; // 计数器(表示有多少个用户打开此文件)
  unsigned int     f_flags; // 标识位  
  fmode_t      f_mode;      // 打开模式
  loff_t      f_pos;       // 读写偏移量
  struct fown_struct  f_owner; // 所属者信息,通过信号进行异步I/O 数据的传送
  const struct cred  *f_cred; //文件的信任状
  struct file_ra_state  f_ra;//预读状态

  u64      f_version;       //版本号
#ifdef CONFIG_SECURITY
  void      *f_security; //安全模块
#endif
  /* needed for tty driver, and maybe others */
  void      *private_data;      /* tty 驱动程序的钩子 */

#ifdef CONFIG_EPOLL
  /* Used by fs/eventpoll.c to link all the hooks to this file */
  struct list_head  f_ep_links;     //事件池链表
  struct list_head  f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
  struct address_space  *f_mapping; //页缓存映射
#ifdef CONFIG_DEBUG_WRITECOUNT
  unsigned long f_mnt_write_state;    //调试状态
#endif
};

类似于目录项对象,文件对象实际上没有对应的磁盘数据,所以在结构体中没有代表对象是否为脏、是否需要写回磁盘的标志。文件对象通过f_dentry指针指向相关的目录项对象,目录项会指向相关的索引节点,索引节点会记录文件是否是脏的那这里有个问题,如果有多个文件对象的情况下,应该是写回哪个文件对象中的信息呢?

每个文件对象总是会包含在下列的一个双向循环链表

  • unused 文件对象的链表

    • 该链表既可以用做文件对象的内存高速缓存,又可以当作超级用户的备用存储器,也就是说,即使系统的动态内存用完,也允许超级用户打开文件。
    • 由于这些对象是未使用的,它们的f_conut是NULL,该链表首元素的地址存放在free_list中,内核必须确认该链表总是至少包含NR_RESERVED_FILES个对象,通常该值设为10
  • in use 文件对象的链表

    • 该链表中的每个元素至少由一个进程使用,因此,各个元素的f_count不会为NULL,该链表中第一个元素的地址存放在变量anoa_list
    • 如果VFS需要分配一个新的文件对象,就调用函数get_empty_filp() ,该函数检测 unused文件对象链表的元素个数是否多于NR_RESERVED_FILES,如果是,可以为新打开的文件使用其中一个元素;如果有,可以为新打开的文件使用其中的一个元素;如果没有,则退回到正常的内存分配。

每个文件系统都有其自己的文件操作集合,执行诸如读写文件这样的操作

  • 内核将一个索引节点从磁盘装入内存的时候,就会把指向这些文件操作的指针存放在file_operations结构中,而该结构的地址存放在索引节点的i_fop字段中
  • 进程打开这个文件的时候,vfs就用存放在索引节点中的这个地址初始化新文件对象的f_op字段,使得对文件操作的的后续调用能够使用这些函数。
  • 如果需要,vfs随后也可以通过在f_op字段存放一个新值而修改文件操作的集合

file的操作集合如下,代码在include/linux/fs.h中:

struct file_operations {
  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);
  int (*readdir) (struct file *, void *, filldir_t);
  unsigned int (*poll) (struct file *, struct poll_table_struct *);
  long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
  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 *, loff_t, loff_t, int datasync);
  int (*aio_fsync) (struct kiocb *, int datasync);
  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 **);
  long (*fallocate)(struct file *file, int mode, loff_t offset,
        loff_t len);
  int (*show_fdinfo)(struct seq_file *m, struct file *f);
};
3.1.5 用户打开文件表

每个进程使用一个files_struct结构来记录文件描述符的使用情况,这个files_struct结构也称为用户打开文件表

files_struct 结构include/linux/fdtable.h 中定义如下:

struct fdtable {
  unsigned int max_fds;
  struct file __rcu **fd;      /* current fd array */
  unsigned long *close_on_exec;
  unsigned long *open_fds;
  struct rcu_head rcu;
};

/*
 * Open file table structure
 */
 struct files_struct {
  /*
   * read mostly part
   */
  atomic_t count;   //结构的使用计数
  struct fdtable __rcu *fdt; //指向其他fd表的指针
  struct fdtable fdtab; //基fd表
  /*
   * written part on a separate cache line in SMP
   */
  spinlock_t file_lock ____cacheline_aligned_in_smp;
  int next_fd;   //缓存下一个可以使用的fd
  unsigned long close_on_exec_init[1];  //exec()时关闭的文件描述符链表
  unsigned long open_fds_init[1];      //打开的文件描述符链表
  struct file __rcu * fd_array[NR_OPEN_DEFAULT]; //缺省的文件对象数组,文件对象指针的初始化数组
};
  • fd_array数组指针指向已经打开的文件对象。因为NR_OPEN_DEFAULT等于BITS_PER_LONG,在64位机器体系结构中这个宏的值为64,所以该数组可以容纳64个文件对象。如果一个进程打开的文件对象超过64个,内核将分配一个新数组,并将fdt指针指向它。所以对适当数量的文件对象的访问会执行的很快,因为它是对静态数组的操作;如果一个进程打开的文件数量过多,那么内核就需要建立新数组。所以如果系统中有大量的进程都要打开超过64 个文件,为了优化性能,管理员可以适当增NR_OPEN_DEFAULT 的预定义值.
  • open_fds定义在include/linux/fdtable.h中

4. VFS数据结构之间的关系

  • 超级块:对一个文件系统的描述

  • 索引节点:对一个文件物理属性的描述

  • 目录项:对一个文件逻辑属性的描述

  • 文件与进程之间的关系由一下数据结构来描述

    • 一个进程所处的位置是由fs_struct来描述的
    • 一个进程或用户打开的文件是由files_struct来描述的
    • 整个系统所打开的文件是由file结构来描述的

这些数据结构之间的关系

4. 问题总结

1. VFS中最常见的四种数据结构是什么?

  • 超级块对象super_block :存放已安装文件系统的有关信息。对基于磁盘的文件系统,这类对象通常应用于存放在磁盘上的文件系统控制块FCB
  • 索引节点对象inode :存放关于具体文件一般信息。对基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件控制块FCB。每个索引节点对象都有一个索引节点号,这个节点号唯一地表示文件系统中的文件。
  • 文件对象file :存放打开文件与进程之间进行交互的有关信息,这类信息仅当进程访问文期间存在于内核内存中。
  • 目录项对象dentry :存放目录项与对应文件进行链接的有关信息。每个磁盘文件系统都以自己特有的方式将该类信息存在磁盘上。

2. dentry的四种状态

  1. free(空闲):此状态表示目录项对象未被分配和使用,处于可分配的空闲状态
  2. unused(未使用):目录项对象已经被分配,但当前未被任何进程使用。它可能是之前被使用过,但现在处于闲置状态。
  3. in use(正在使用):表示目录项对象正在被某个进程或内核操作所引用和使用
  4. negative(负向):这种状态通常与不存在的文件或目录(没有关联的inode )相关联。当查找一个不存在的路径时,可能会创建一个处于负向状态的目录项来表示查找失败。

3. VFS中dentry状态、分配和使用过程的说明

在Linux内核的VFS中,dentry通常由struct dentry结构体表示。

分配过程

free状态的对象在概念上类似内存池中的可用元素

当需要新的dentry时,可以从这些处于free状态的对象中快速获取,而不必每次都进行新的内存分配操作,从而提高了系统的性能和效率

使用过程

文件系统操作(如打开文件、读取目录内容等)会触发对dentry的使用

  1. 首先,会通过名称在dentry哈希表中进行查找,使用计算得到的哈希值快速定位可能的匹配项
  2. 如果在哈希表中找到了匹配的dentry,并且其状态为in use(意味着正在被其他操作使用)或者unused(表示之前被使用过,但当前暂时未被使用),则会根据需要增加其引用计数d_count,一表示正在使用
  3. 如果未在哈希表中找到匹配的dentry,可能会触发一系列的操作来获取相关信息,例如从磁盘读取目录数据,创建新的inode和dentry,并将其状态设置为in use
  4. 在使用过程中,根据内存管理策略和系统的负载情况,dentry的状态可能会发生变化。例如,如果一段时间未被使用,其状态可能从in use转变为unused,并被转移到LRU中
  5. 当不再需要使用某个dentry时,会减少其引用计数。如果引用计数为0,并且满足其他条件(如不在LRU链表的头部等),则可能会释放该dentry占用的内存

4.dentry项中的d_alias、d_hash、d_lru字段的作用

  • d_alias:用于处理硬链接或别名。当存在多个路径指向相同的文件或目录时,通过 d_alias 可以关联到其他具有相同底层 inodedentry 结构,从而实现对硬链接或别名的有效管理。

  • d_hash:通过对 dentry 的关键信息(如名称)进行哈希计算得到的值存储在这个字段。在查找操作中,根据计算的哈希值可以快速在哈希表中定位可能的位置,从而加速对 dentry 的查找过程,提高文件系统操作的性能。

  • d_lru:用于(LRU)算法。它有助于确定哪些 dentry 在一段时间内未被频繁使用,当内存紧张需要释放一些 dentry 占用的资源时,LRU 链表中的较不活跃的 dentry 可能会被优先选择删除,以腾出内存空间供其他更需要的操作使用

  • dentry 结构中除了d_alias、d_hash、d_lru 三个队列外,还有d_vfsmnt、d_child 及d_subdir 三个队列。

    • 其中d_vfsmnt 仅在该dentry 为一个安装点时才使用。
    • 当该目录节点有父目录时,则其dentry 结构就通过d_child 挂入其父节点的d_subdirs 队列中,同时又通过指针d_parent 指向其父目录的dentry 结构,而它自己各个子目录的dentry 结构则挂在其d_subdirs 域指向的队列中。