文件系统 | 虚拟文件系统VFS

概述

在文件系统之下,我们看到的磁盘设备是一组线性排列的磁盘块,可以访问其中的任意磁盘块,可以独立地读写磁盘块,如果在磁盘块中写入数据,将被记录下来,并在读操作中返回。

文件系统是存储和组织文件(即一系列相关的数据),以便可以方便地进行查找和访问的一种机制,我们要能优雅地访问磁盘上的数据就得用到文件系统。不同的文件系统有不同的文件存储和组织方式。

Linux设计人员很早就注意到了如何使Linux支持不同文件系统的问题,此外,为了保证Linux的开放性和适应更复杂存储情况的需要,还必须方便用户开发新的文件系统,因此,Linux就必须从各种各样的文件系统中提取它们的共同部分,设计出一个抽象层,让上层应用程序可以通过统一的界面进行操作,当需要具体文件系统介入时,由抽象层调用具体文件系统提供的回调函数来处理。

这个抽象层叫做虚拟文件系统开关(Virtual Filesystem Switch)层,简称为虚拟文件系统(VFS)。它是具体文件系统和上层应用间的接口层,将各种不同文件系统的操作和管理纳入一个统一的框架,使得用户不需要关心各种不同文件系统的实现细节。

0f2cb5f80a57847bc4a5d3454afc84ad.jpeg

但是严格说来,VFS并不是一种实际的文件系统,它只存在于内存中,不存在于任何外存空间。VFS在系统启动时建立,系统关闭时消亡。

Linux基于公共文件模型(Common File Model)构造VFS,这里所谓的公共文件模型,有两个层次的含义:对于上层应用程序,它意味着统一的系统调用以及可预期的处理逻辑;对于具体文件系统,则是各种具体对象的公共属性以及操作接口的提取。

在公共文件模型中,文件是文件系统最基本的单位。每个文件都有文件名以方便用户引用其数据。此外,文件还具有例如文件创建日期、文件长度等信息,称作文件属性。

文件使用一种层次的方式来管理,层次中的节点被称为目录(Directory),而叶子就是文件。目录包含了一组文件和/或其他目录,包含在另一个目录下的目录被称为子目录,前者被称为父目录,这样就形成了一个层次的树状结构。其根节点被称为根(Root)目录。

每个文件系统并不是独立使用的。相反,系统有一个公共根目录和全局文件系统树,要访问一个文件系统中的文件,必须先将这个文件系统放在全局文件系统树的某个目录下。也就是我们熟悉的挂载Mount过程。

文件通过路径Path来标识,路径指的是从文件系统树的一个节点开始,到达另一个节点的通路。路径通常表示成中间所经过的节点(目录或文件)的名字,加上分隔符,连接成字符串形式。在目录下,还可以有符号链接,符号链接symlink实际上是独立于它所链接目标存在的一种特殊文件,它包含了另一个文件或目录的任意一个路径名。在Linux公共文件模型下,目录和符号链接也是文件,只不过它们有不同的操作接口,或者有不同的操作实现。上层应用程序通过系统调用对文件或文件系统进行操作,Linux提供了open、read、write、mount等标准的系统调用接口。

文件系统对象

Linux文件系统对象之间的关系可以概括为文件系统类型、超级块、inode、dentry和vfsmount之间的关系。

  • 文件系统类型规定了某种类型文件系统的行为,它存在的主要目的是为了构造这种类型文件系统的实例,或者被称为超级块实例。

  • 超级块反映了文件系统整体的控制信息,超级块以多种方式存在。对于基于磁盘的文件系统,它以特定格式存在于磁盘的固定区域,为磁盘上的超级块。在文件系统被装载时,其内容被读入内存,构建内存中的超级块。其中某些信息为各种类型的文件系统所共有,被提炼成VFS的超级块结构。如果某些文件系统不具有磁盘上超级块和内存中超级块形式,则它们必须负责从零构造出VFS的超级块。

  • inode反映了某个文件系统对象的一般元信息,dentry反映了某个文件系统在文件系统树中的位置。同超级块一样,inode和dentry也有磁盘上、内存中以及VFS三种形式,其中VFSinode和VFSdentry是被提炼出来为各种类型文件系统所共有的,而磁盘上、内存中inode和dentry则为具体文件系统所特有,根据实际情况,也可能根本不需要。

Linux有一棵全局文件系统树,反映了Linux VFS对象之间的关系。文件系统要被用户空间使用必须先挂载到这棵树上,每一次挂载,称为一个挂载实例,某些文件系统即使只在内核中使用,也需要这样一个挂载实例,每个挂载实例有四个必备元素:vfsmount、超级块、根inode和根dentry。它们之间的关系如图所示。

f198a1962de8ce93c1edba15d69cb2df.jpeg

一个相同的文件系统可以被挂载到不同的路径下(生成不同的超级块实例,分别访问),一个相同超级块实例也可以被挂载到不同的路径下(相同超级块实例,共同访问),即一个文件系统类型可能有多个超级块实例,而每个超级块实例又可以有多个挂载实例。

eg. /dev/sda1和/dev/sda2都被格式化为Minix文件系统,当/dev/sda1和/dev/sda2上的文件系统实例先后被挂载到系统时,假设在/mnt/d10和/mnt/d2下生成了两个超级块实例,分别对应一个挂载实例。透过/mnt/d10和/mnt/d2所做的改动分别反映到/dev/sda1和/dev/sda2上,然后,倘若/dev/sda1又被挂载到了/mnt/d11下,则/mnt/d10和/mnt/d11所作的改动都会被反映到/dev/sda1上的文件系统实例中。

文件系统类型

VFS需要知道具体文件系统的关键信息,通过file_system_type结构反映,无论是编译到内核还是作为模块动态装载的文件系统,都需要通过调用register_filesystem向VFS核心进行注册,如果不再使用,应该调用unregister_filesystem进行注销。

struct file_system_type {
  const char *name; // 文件系统名
  int fs_flags; // 文件系统类型标志
#define FS_REQUIRES_DEV    1 
#define FS_BINARY_MOUNTDATA  2
#define FS_HAS_SUBTYPE    4
#define FS_USERNS_MOUNT    8  /* Can be mounted by userns root */
#define FS_DISALLOW_NOTIFY_PERM  16  /* Disable fanotify permission events */
#define FS_ALLOW_IDMAP         32      /* FS has been updated to handle vfs idmappings. */
#define FS_THP_SUPPORT    8192  /* Remove once all fs converted */
#define FS_RENAME_DOES_D_MOVE  32768  /* FS will handle d_move() during rename() internally. */
  int (*init_fs_context)(struct fs_context *); // 初始化文件系统上下文函数指针
  const struct fs_parameter_spec *parameters;
  struct dentry *(*mount) (struct file_system_type *, int,
           const char *, void *); // 挂载回调函数指针
  void (*kill_sb) (struct super_block *); // 释放超级块函数指针
  struct module *owner; // 指向实现了这个文件系统的模块指针
  struct file_system_type * next; // 构成链表
  struct hlist_head fs_supers; // 链表头


  struct lock_class_key s_lock_key;
  struct lock_class_key s_umount_key;
  struct lock_class_key s_vfs_rename_key;
  struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];


  struct lock_class_key i_lock_key;
  struct lock_class_key i_mutex_key;
  struct lock_class_key invalidate_lock_key;
  struct lock_class_key i_mutex_dir_key;
};

VFS超级块super_block

超级块是整个文件系统的元数据的容器。对于基于磁盘的文件系统,磁盘上的超级块是保存在磁盘设备上固定位置的一个或多个块,在挂载该磁盘上的文件系统时,磁盘上超级块被读入内存,并根据它构造内存中超级块。其中一部分是文件系统共有的,被提取出来,即VFS超级块。

5e29262667b2bc1c2ea987ce09838d76.jpeg

这是一个很复杂的结构体,节选其中一部分:
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; // 文件系统的块长度的位数
  unsigned long    s_blocksize; // 文件系统字节单位块长度
  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_iflags;  /* internal SB_I_* flags */
  unsigned long    s_magic; // 魔数
  struct dentry    *s_root; // 指向文件系统根目录dentry
  struct rw_semaphore  s_umount; // 用于卸载的信号量
  int      s_count; // 引用计数器
  atomic_t    s_active; // 活动引用计数器表示被mount了多少次


  ......




  struct list_head  s_mounts;  /* list of mounts; _not_ for fs use */
  struct block_device  *s_bdev; // 对于磁盘文件系统指向块设备描述符
  struct backing_dev_info *s_bdi; // 指向后备设备信息描述符
  struct mtd_info    *s_mtd; // 对于基于MTD的超级块,该域为指向MTD信息结构的指针
  struct hlist_node  s_instances; // 超级块实例链表的链接件
  unsigned int    s_quota_types;  /* Bitmask of supported quota types */
  struct quota_info  s_dquot;  /* Diskquota specific options 磁盘配额信息描述符*/


  struct sb_writers  s_writers;


  /*
   * Keep s_fs_info, s_time_gran, s_fsnotify_mask, and
   * s_fsnotify_marks together for cache efficiency. They are frequently
   * accessed and rarely modified.
   */
  void      *s_fs_info;  /* (上图所示)指向具体文件系统的超级块信息指针 Filesystem private info */


  /* Granularity of c/m/atime in ns (cannot be worse than a second) */
  u32      s_time_gran;
  /* Time limits for c/m/atime in seconds */
  time64_t       s_time_min;
  time64_t       s_time_max;
#ifdef CONFIG_FSNOTIFY
  __u32      s_fsnotify_mask;
  struct fsnotify_mark_connector __rcu  *s_fsnotify_marks;
#endif


  char      s_id[32];  /* Informational name 块设备名(对于磁盘文件系统)、文件系统类型名*/
  uuid_t      s_uuid;    /* UUID */


  unsigned int    s_max_links;
  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 跨目录重命名文件时使用的信号量*/


  /*
   * Filesystem subtype.  If non-empty the filesystem type field
   * in /proc/mounts will be "type.subtype"
   */
  const char *s_subtype; // 文件系统子类型
  const struct dentry_operations *s_d_op; /* default d_op for dentries 目录操作函数表指针*/
  ......
  
  /* s_inode_list_lock protects s_inodes */
  spinlock_t    s_inode_list_lock ____cacheline_aligned_in_smp;
  struct list_head  s_inodes;  /* all inodes 文件系统的所有inode链表表头*/


  spinlock_t    s_inode_wblist_lock;
  struct list_head  s_inodes_wb;  /* writeback inodes */


} __randomize_layout;

在s_op所指向的操作表中,定义了一系列方法的回调函数。

struct super_operations {
  // 传入超级块,返回指向对应VFS inode的指针
  // 分配具体文件系统的inode   被VFS核心调用
  struct inode *(*alloc_inode)(struct super_block *sb);
  // 销毁具体文件系统的inode  被VFS核心调用
  void (*destroy_inode)(struct inode *);
  // 释放具体文件系统的inode 被VFS核心调用
  void (*free_inode)(struct inode *);
  // 标记一个inode为脏  被VFS核心调用
  void (*dirty_inode) (struct inode *, int flags);
  // 需要将inode指针写入磁盘时调用 被VFS核心调用,典型的文件系统不会在此函数中IO,往往只是做标记
  int (*write_inode) (struct inode *, struct writeback_control *wbc);
  // 引用计数为0释放inode
  int (*drop_inode) (struct inode *);
  void (*evict_inode) (struct inode *);
  // 释放超级块 被VFS核心调用
  void (*put_super) (struct super_block *);
  // VFS核心正在写出和一个超级块关联的所有脏数据时调用
  int (*sync_fs)(struct super_block *sb, int wait);
  // 锁住一个超级块
  int (*freeze_super) (struct super_block *);
  // 锁住一个文件系统
  int (*freeze_fs) (struct super_block *);
  int (*thaw_super) (struct super_block *);
  // 解锁文件系统
  int (*unfreeze_fs) (struct super_block *);
  // VFS需要获得文件系统统计信息时调用
  int (*statfs) (struct dentry *, struct kstatfs *);
  // 在文件系统被重新挂载时调用
  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);
  struct dquot **(*get_dquots)(struct inode *);
#endif
  long (*nr_cached_objects)(struct super_block *,
          struct shrink_control *);
  long (*free_cached_objects)(struct super_block *,
            struct shrink_control *);
};

VFS索引节点 inode

inode包含了文件系统各种对象(文件、目录、块设备文件、字符设备文件等)的元数据,对于基于磁盘的文件系统,inode存在于磁盘上,其形式取决于文件系统的类型。在打开该对象进行访问时,其inode被读入内存,内存中inode有一部分是各种文件系统共有的,抽出来,作为VFS inode。不同于超级块的是,VFSinode结构是内嵌于具体文件系统内存中的inode中的,关系如下图。

2c04d564439fc1cd5eb437c3124a806d.jpeg

struct inode {
  umode_t      i_mode; // 文件系统类型和模式
  unsigned short    i_opflags; // 操作标志位
  kuid_t      i_uid; // 创建该文件的用户id
  kgid_t      i_gid; // 创建该文件的用户组id
  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; // 上图所示,指向inode操作表
  struct super_block  *i_sb; // 指向superblock
  struct address_space  *i_mapping; // 指向address_space对象指针


#ifdef CONFIG_SECURITY
  void      *i_security; // 指向inode安全结构指针
#endif


  /* Stat data, not accessed from path walking */
  unsigned long    i_ino; // inode 编号,识别文件
  /*
   * 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; // 设备号,如果本inode代表一个块设备或字符设备
  loff_t      i_size; // 以字节为单位的文件长度
  struct timespec64  i_atime; // 文件最后访问时间 access
  struct timespec64  i_mtime; // 文件最后修改时间 modify
  struct timespec64  i_ctime; // inode 最后修改时间
  spinlock_t    i_lock;  /* 用于保护i_blocks, i_bytes, maybe i_size 的自旋锁*/
  unsigned short          i_bytes; // 以512字节的块为单位,文件最后一个块的字节数
  u8      i_blkbits; // 文件块长度的位数
  u8      i_write_hint; 
  blkcnt_t    i_blocks; // 文件的块数


#ifdef __NEED_I_SIZE_ORDERED
  seqcount_t    i_size_seqcount; // 被SMP系统用来正确获取和设置文件长度
#endif


  /* Misc */
  unsigned long    i_state; // inode 状态标志
  struct rw_semaphore  i_rwsem; // 读写信号量


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


  struct hlist_node  i_hash; // 链入全局inode哈希表的链接件
  struct list_head  i_io_list;  /* backing dev IO list */
#ifdef CONFIG_CGROUP_WRITEBACK
  struct bdi_writeback  *i_wb;    /* the associated cgroup wb */


  /* foreign inode detection, see wbc_detach_inode() */
  int      i_wb_frn_winner;
  u16      i_wb_frn_avg_time;
  u16      i_wb_frn_history;
#endif
  struct list_head  i_lru;    /* inode LRU list */
  struct list_head  i_sb_list; // 链入到所属文件系统超级块的inode链表的链接件
  struct list_head  i_wb_list;  /* backing dev writeback list */
  union {
    struct hlist_head  i_dentry; // 引用这个inode的dentry链表的表头
    struct rcu_head    i_rcu;
  };
  atomic64_t    i_version; // 版本号,每次使用后递增
  atomic64_t    i_sequence; /* see futex */
  atomic_t    i_count; // 使用计数器
  atomic_t    i_dio_count; 
  atomic_t    i_writecount; // 记录有多少个进程以可写的方式打开此文件
#if defined(CONFIG_IMA) || defined(CONFIG_FILE_LOCKING)
  atomic_t    i_readcount; /* struct files open RO */
#endif
  union {
    const struct file_operations  *i_fop;  /* former ->i_op->default_file_ops */
    void (*free_inode)(struct inode *);
  };
  struct file_lock_context  *i_flctx;
  struct address_space  i_data; // 文件的address_space对象
   // 如果这个inode代表一个块设备,该域为链入到块设备的slave inode链表(
   // 表头为block_device结构的bd_inodes域)的链接件。如果代表字符设备
   // 该域为链入字符设备的inode链表(表头为cdev结构的list域)的链接件,
   // 即公用同一个驱动的设备链表
  struct list_head  i_devices; 
  // 这个联合体依据inode代表的不同类型指向具体信息的指针
  union {
    struct pipe_inode_info  *i_pipe; // 管道类型
    struct cdev    *i_cdev; // 字符设备类型
    char      *i_link; 
    unsigned    i_dir_seq;
  };


  __u32      i_generation;


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


#ifdef CONFIG_FS_ENCRYPTION
  struct fscrypt_info  *i_crypt_info;
#endif


#ifdef CONFIG_FS_VERITY
  struct fsverity_info  *i_verity_info;
#endif


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

inode 将在文件系统中实际组织一个文件,我们在使用文件名打开一个文件时,系统首先找到对应的inode,再从inode中索引数据块data blocks。

小知识:硬链接和软链接

inode可以被不止一个文件名映射,也就是说可以存在两个相同的文件路径访问到同一个inode的情况,那就可以采用硬链接

ln 源文件 目标

硬链接之后,会使得源文件和目标文件指向同一个inode,inode信息中的链接数就会加1。当一个文件拥有多个硬链接时,对文件内容修改,所有文件名都会改;但是删除一个文件名,不影响另一个文件名的访问(关掉一扇门不影响从另一扇门进入)。而只会使inode中的链接数减1。需要注意的是,不能对目录创建硬链接。

而软链接则是类似于快捷方式,可以快速连接到目标文件或目录。

ln -s 源文件或目录 目标文件或目录

软链接就是再创建一个独立的文件,而这个文件会让数据的读取指向它链接的那个文件的文件名,即源文件的内容是目标文件的路径,它们的inode号码不同,但读写源文件时系统会自动将访问者导向目标文件。这与硬链接不同,源文件无法独立存在,目标文件的链接数也不会发生变化。

VFS目录项 dentry

dentry虽然翻译为目录项,但它和文件系统中的目录并不是同一个概念,dentry属于所有文件系统对象,包括目录、常规文件、符号链接、块设备文件、字符设备文件等。反映的是文件系统对象在内核中所在文件系统树中的位置。它们的关系如下图。

0f776c31c41c90e3d9102b96c39c877b.jpeg

所有文件系统共有的内容被提炼处理,形成了VFS dentry,而对于具体文件系统,还可以又内存中目录项和磁盘上目录项两种形式。在打开对象时,磁盘上目录项被读取,用来构造VFS dentry和内存目录项,同时根据文件系统类型设置其dentry操作表,这个构造与superblock类似。

struct dentry {
  /* RCU lookup touched fields */
  unsigned int d_flags;    /* protected by d_lock || dentry cache 标志*/
  seqcount_spinlock_t d_seq;  /* per dentry seqlock */
  struct hlist_bl_node d_hash;  /* lookup hash list */
  struct dentry *d_parent;  /* parent directory 根dentry指向自己*/
  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 */
  struct lockref d_lockref;  /* per-dentry lock and refcount */
  const struct dentry_operations *d_op;
  struct super_block *d_sb;  /* The root of the dentry tree 指向本dentry所属文件系统超级块的指针 */
  unsigned long d_time;    /* used by d_revalidate */
  void *d_fsdata;      /* fs-specific data */


  union {
    struct list_head d_lru;    /* LRU list */
    wait_queue_head_t *d_wait;  /* in-lookup ones only */
  };
  struct list_head d_child;  /* child of parent list */
  struct list_head d_subdirs;  /* our children */
  /*
   * d_alias and d_rcu can share memory
   */
  union {
    struct hlist_node d_alias;  /* inode alias list */
    struct hlist_bl_node d_in_lookup_hash;  /* only for in-lookup ones */
     struct rcu_head d_rcu;
  } d_u;
} __randomize_layout;

因为inode反映的是文件系统对象的元数据,而dentry表示文件系统对象在文件系统树中的位置。dentry和inode是多对一的关系,每个dentry只有明确一个inode,而一个inode可以被多个dentry指向(上述硬链接),他将这些dentry组成以i_dentry的链表,每个dentry通过d_alias加入到所属inode的i_dentry链表中。

根dentry的父dentry为自身,每个dentry的所有子dentry组织在以d_subdirs为首的链表中,子dentry通过d_child链入。除根dentry以外,所有dentry加入到一个dentry_hashtable全局哈希表中,其哈希项的索引计算基于父dentry描述符的地址以及它的文件名哈希值,因此,这个哈希表的作用就是方便查找给定目录下的文件。

vfsmount 文件系统挂载

struct vfsmount {
  struct dentry *mnt_root;  /* root of the mounted tree 指向文件系统根目录 dentry*/
  struct super_block *mnt_sb;  /* pointer to superblock 指向文件系统超级块 */
  int mnt_flags; // 挂载标志
  struct user_namespace *mnt_userns; // 挂载用户命名空间
} __randomize_layout;

vfsmount结构较之前版本以及大幅简化,仅仅需要提供superblock挂载在哪一个dentry这样的必要信息,vfsmount结构在文件系统挂载时起到了重要的作用。

参考资料:《存储技术原理分析》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值