VFS基础
1. VFS在内核中与其他的内核模块的协同关系
为了能够支持各种实际文件系统,VFS 定义了所有文件系统都支持的基本的、概念上的接口和数据 结构;同时实际文件系统也提供 VFS 所期望的抽象接口和数据结构,将自身的诸如文件、目录等概念在形式 上与VFS的定义保持一致。换句话说,一个实际的文件系统想要被 Linux 支持,就必须提供一个符合VFS标准 的接口,才能与 VFS 协同工作。实际文件系统在统一的接口和数据结构下隐藏了具体的实现细节,所以在VFS 层和内核的其他部分看来,所有文件系统都是相同的。
2. 一些基本概念
**文件:**一组逻辑上具有完整意义的信息项的系列,在linux中,除了普通文件,其他诸如目录、设备、套接字等也以文件被对待。
**目录:**目录好比一个文件夹,用来容纳相关文件,目录是层层嵌套,形成文件路径,目录以一种特殊文件被对待,所以用于文件的操作统一也可以用在目录上。
**目录项:**一个文件路径中,路径的每一个部分都被称为目录项。
索引节点:用于存储文件的元数据的一个数据结构,文件的元数据,也就是文件的相关信息,和文件本身是两个不同的概念,包含文件的大小、拥有者、创建实际、磁盘位置等。
超级块:存储文件系统的控制信息的数据结构,描述文件系统的状态、文件系统类型、大小、区块数、索引节点数、存放于磁盘的特定扇区中。
这几个概念在的关系图如下:
3. 四个关键的对象
VFS依靠四个主要的数据结构和一些辅助的数据结构来描述其结构信息,这些数据结构表现得就像是对象;每个对象中都包含由操作函数表构成的操作对象。
**超级快对象:**存储一个已安装的文件系统的控制信息,代表一个已安装的文件系统;每次一个实际的文件系统被安装时,内核会从磁盘的特定位置读取一些控制信息来填充内存中的超级块对象。一个安装实例和一个超级块对象一一对应。超级块通过其结构体中的一个域s_type记录它所属的文件系统类型。
超级块定义在:<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 {
struct inode *(*alloc_inode)(struct super_block *sb); /* 创建和初始化一个索引节点对象 */
void (*destroy_inode)(struct inode *); /* 释放给定的索引节点 */
void (*dirty_inode) (struct inode *); /* VFS在索引节点被修改时会调用这个函数 */
int (*write_inode) (struct inode *, int); /* 将索引节点写入磁盘,wait表示写操作是否需要同步 */
void (*drop_inode) (struct inode *); /* 最后一个指向索引节点的引用被删除后,VFS会调用这个函数 */
void (*delete_inode) (struct inode *); /* 从磁盘上删除指定的索引节点 */
void (*put_super) (struct super_block *); /* 卸载文件系统时由VFS调用,用来释放超级块 */
void (*write_super) (struct super_block *); /* 用给定的超级块更新磁盘上的超级块 */
int (*sync_fs)(struct super_block *sb, int wait); /* 使文件系统中的数据与磁盘上的数据同步 */
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 *); /* VFS调用该函数中断安装操作 */
};
超级块用来描述整个文件系统的信息
-
每个具体的文件系统都有自己的超级块
-
VFS超级块是各种文件系统在安装时建立起来的,并在卸载时被自动删除。
-
所有超级块对象都以双向循环链表的形式链接在一起。
**索引节点:**索引节点是VFS中的核心概念,它包含内核在操作文件或目录时需要的全部信息。一个索引节点代表文件系统中的一个文件(这里的文件不仅是指我们平时所认为的普通的文件,还包括目录,特殊设备文件等等)。索引节点和超级块一样是实际存储在磁盘上的,当被应用程序访问到时才会在内存中创建。
索引节点定义在:<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 私有指针 */
};
/*
* 其中的 i_op 中定义了索引节点的操作方法
* 这里只介绍一些相对重要的函数
*/
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 *);
};
-
文件系统处理文件所需要的所有信息都保存在被称为索引节点的inode结构体中
-
同一个文件系统中,每个文件的索引节点号都是唯一的
-
与索引节点关联的方法由struct inode_operations来描述
-
inode有两个设备号:i_dev(常规文件的设备号),i_rdev(某一设备的设备号)
-
Linux文件系统的另一大特殊:设备即文件,驱动中设备号的来源
目录项:
/* 目录项对象结构 */
struct dentry {
atomic_t d_count; /* 使用计数 */
unsigned int d_flags; /* 目录项标识 */
spinlock_t d_lock; /* 单目录项锁 */
int d_mounted; /* 是否登录点的目录项 */
struct inode *d_inode; /* 相关联的索引节点,通过这个索引节点就可以读取到文件数据 */
struct hlist_node d_hash; /* 散列表 */
struct dentry *d_parent; /* 父目录的目录项对象 */
struct qstr d_name; /* 目录项名称 */
struct list_head d_lru; /* 未使用的链表 */
/*
* 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; /* 子目录链表 */
struct list_head d_alias; /* 索引节点别名链表 */
unsigned long d_time; /* 重置时间 */
const struct dentry_operations *d_op; /* 目录项操作相关函数 */
struct super_block *d_sb; /* 文件的超级块 */
void *d_fsdata; /* 文件系统特有数据 */
unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* 短文件名 */
};
/* 目录项相关操作函数 */
struct dentry_operations {
/* 该函数判断目录项对象是否有效。VFS准备从dcache中使用一个目录项时会调用这个函数 */
int (*d_revalidate)(struct dentry *, struct nameidata *);
/* 为目录项对象生成hash值 */
int (*d_hash) (struct dentry *, struct qstr *);
/* 比较 qstr 类型的2个文件名 */
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);
};
-
每个文件除了一个struct inode结构体外,还要一个目录项struct denty结构
-
dentry代表的逻辑意义上的文件,描述的是文件逻辑的属性,目录项对象在磁盘上并没有对应的映像。
-
inode代表的是物理意义上的文件,记录的是物理上的属性,对于一个具体的文件系统,其inode在磁盘上有对应的映像。
-
一个索引节点可能对应多个目录项对象
文件对象:
/路径及结构体,包含目录项
struct path {
struct vfsmount *mnt; //文件系统挂载使用
struct dentry *dentry;
};
/*
* 文件对象结构中定义的字段非常多,
* 这里只介绍一些重要的属性
*/
struct file {
union {
struct list_head fu_list; /* 文件对象链表 */
struct rcu_head fu_rcuhead; /* 释放之后的RCU链表 */
} f_u;
struct path f_path; /* 包含的目录项 */
const struct file_operations *f_op; /* 文件操作函数 */
atomic_long_t f_count; /* 文件对象引用计数 */
};
/*
* 其中的 f_op 中定义了文件对象的操作方法
* 这里只介绍一些相对重要的函数
*/
struct file_operations {
/* 用于更新偏移量指针,由系统调用lleek()调用它 */
loff_t (*llseek) (struct file *, loff_t, int);
/* 由系统调用read()调用它 */
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
/* 由系统调用write()调用它 */
ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
/* 由系统调用 aio_read() 调用它 */
ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
/* 由系统调用 aio_write() 调用它 */
ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
/* 将给定文件映射到指定的地址空间上,由系统调用 mmap 调用它 */
int (*mmap) (struct file *, struct vm_area_struct *);
/* 创建一个新的文件对象,并将它和相应的索引节点对象关联起来 */
int (*open) (struct inode *, struct file *);
/* 当已打开文件的引用计数减少时,VFS调用该函数 */
int (*flush) (struct file *, fl_owner_t id);
};
-
文件描述符用来描述打卡的文件
-
每一个进程用一个file_struct结构来记录文件描述符的使用情况
-
这个file_struct结构称为用户打开文件表,它是进程的私有数据
4. dentry与inode的关系
在内存中, 每个文件都有一个dentry(目录项)和inode(索引节点)结构,dentry记录着文件名,上级目录等信息,正是它形成了我们所看到的树状结构;而有关该文件的组织和管理的信息主要存放inode里面,它记录着文件在存储介质上的位置与分布。同时dentry->d_inode指向相应的inode结构。dentry与inode是多对一的关系,因为有可能一个文件有好几个文件名(通过硬链接)。
所有的dentry用d_parent和d_child连接起来,就形成了我们熟悉的树状结构。
那么假设没有dentry会出现怎样的情况?
比如我要打开/usr/bin/vim 文件
1 首先需要去/所在的inode找到/的数据块,从/的数据块中读取到usr这个条目的inode
2 跳转到user 对应的inode,根据/usr inode 指向的数据块,读取到/usr 目录的内容,从中读取到bin这个条目的inode 3 跳转到/usr/bin/对应的inode,根据/usr/bin/指向的数据块,从中读取到/usr/bin/目录的内容,从里面找到vim的inode
我们都知道,Linux提供了page cache页高速缓存,很多文件的内容已经缓存在内存里,如果没有dentry,文件名无法快速地关联到inode,即使文件的内容已经缓存在页高速缓存,但是每一次不得不重复地从磁盘上找出来文件名到VFS inode的关联。
因此理想情况下,我们需要将文件系统所有文件名到VFS inode的关联都纪录下来,但是这么做并不现实,首先并不是所有磁盘文件的inode都会纪录在内存中,其次磁盘文件数字可能非常庞大,我们无法简单地建立这种关联,耗尽所有的内存也做不到将文件树结构照搬进内存。
dentry就是为了解决这个难题的
5. 几个关键结构体的关系图
的内容已经缓存在内存里,如果没有dentry,文件名无法快速地关联到inode,即使文件的内容已经缓存在页高速缓存,但是每一次不得不重复地从磁盘上找出来文件名到VFS inode的关联。
因此理想情况下,我们需要将文件系统所有文件名到VFS inode的关联都纪录下来,但是这么做并不现实,首先并不是所有磁盘文件的inode都会纪录在内存中,其次磁盘文件数字可能非常庞大,我们无法简单地建立这种关联,耗尽所有的内存也做不到将文件树结构照搬进内存。
dentry就是为了解决这个难题的