概述
在文件系统之下,我们看到的磁盘设备是一组线性排列的磁盘块,可以访问其中的任意磁盘块,可以独立地读写磁盘块,如果在磁盘块中写入数据,将被记录下来,并在读操作中返回。
文件系统是存储和组织文件(即一系列相关的数据),以便可以方便地进行查找和访问的一种机制,我们要能优雅地访问磁盘上的数据就得用到文件系统。不同的文件系统有不同的文件存储和组织方式。
Linux设计人员很早就注意到了如何使Linux支持不同文件系统的问题,此外,为了保证Linux的开放性和适应更复杂存储情况的需要,还必须方便用户开发新的文件系统,因此,Linux就必须从各种各样的文件系统中提取它们的共同部分,设计出一个抽象层,让上层应用程序可以通过统一的界面进行操作,当需要具体文件系统介入时,由抽象层调用具体文件系统提供的回调函数来处理。
这个抽象层叫做虚拟文件系统开关(Virtual Filesystem Switch)层,简称为虚拟文件系统(VFS)。它是具体文件系统和上层应用间的接口层,将各种不同文件系统的操作和管理纳入一个统一的框架,使得用户不需要关心各种不同文件系统的实现细节。
但是严格说来,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。它们之间的关系如图所示。
一个相同的文件系统可以被挂载到不同的路径下(生成不同的超级块实例,分别访问),一个相同超级块实例也可以被挂载到不同的路径下(相同超级块实例,共同访问),即一个文件系统类型可能有多个超级块实例,而每个超级块实例又可以有多个挂载实例。
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超级块。
这是一个很复杂的结构体,节选其中一部分:
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中的,关系如下图。
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属于所有文件系统对象,包括目录、常规文件、符号链接、块设备文件、字符设备文件等。反映的是文件系统对象在内核中所在文件系统树中的位置。它们的关系如下图。
所有文件系统共有的内容被提炼处理,形成了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结构在文件系统挂载时起到了重要的作用。
参考资料:《存储技术原理分析》