1 VFS概述

linux 系统在用户进程与文件系统中提供了一个抽象层,称之VFS,虚拟文件系统。对上层应用程序提供统一的操作接口,屏蔽不同文件系统之间的差异。
虚拟文件系统为了能够支持不同的文件系统,需要定义一些基本的数据结构和接口,而各种文件系统需要对这些数据结构、接口进行适配。这些数据结构及接口有超级块,索引节点,目录项,目录,文件;
2 VFS数据结构
从本质上来讲,文件系统是特殊的数据分层存储结构,它包含文件、目录和相关的控制信息。VFS为了描述这些,引入了一些概念。
- 文件对象:代表一个有进程打开的文件。并非对应物理文件。 相关数据结构:struct file ,struct file _operations;
- 目录项对象:一个文件路径中的每个部分。相关数据结构: struct dentry ,struct dentry_operations;
- 索引节点对象:用来存储文件的源数据,包换文件的全部信息。相关数据结构:struct inode 、struct inode_oprations;
- 超级块对象:存放一个已安装的文件系统的信息。相关数据结构:struct super_block、struct super_operations;
2.1 超级块 super_block
每当一个文件系统被安装时,内核会从磁盘的特定区域读取相关控制信息填充超级块对象,超级块对象存在磁盘的特定扇区内。一个安装系统对应一个超级块对象。超级块中的s_type记录所属的文件系统类型。
| struct super_block { //超级块数据结构 struct list_head s_list; /指向超级块链表的指针/ …… struct file_system_type s_type; /文件系统类型/ struct super_operations s_op; /超级块方法/ …… struct list_head s_instances; /该类型文件系统/ …… };
struct super_operations { //超级块方法 …… //该函数在给定的超级块下创建并初始化一个新的索引节点对象 struct inode (alloc_inode)(struct super_block sb); …… //该函数从磁盘上读取索引节点,并动态填充内存中对应的索引节点对象的剩余部分 void (read_inode) (struct inode *); …… }; | | :—- |
2.2 索引节点对象
索引节点存储了文件的相关信息,在一个文件第一次被访问时,内核会在内存中组装相应的索引节点对象,以便向内核提供对一个文件进行操作时所需要的全部信息。
| struct inode {//索引节点结构 …… struct inode_operations i_op; /索引节点操作表/ struct file_operations i_fop; /该索引节点对应文件的文件操作集/ struct super_block i_sb; /相关的超级块*/ …… };
struct inode_operations { //索引节点方法 …… //该函数为dentry对象所对应的文件创建一个新的索引节点,主要是由open()系统调用来调用 int (create) (struct inode ,struct dentry ,int, struct nameidata );
//在特定目录中寻找dentry对象所对应的索引节点struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);……
}; | | :—- |
2.3 目录项对象
引入目录项的概念主要是为了方便查找文件的目的。目录项对象没有对应的磁盘数据结构,VFS在遍历路径名的过程中会将他们逐个解析成目录项对象。**
| struct dentry {//目录项结构 …… struct inode d_inode; /相关的索引节点/ struct dentry d_parent; /父目录的目录项对象/ struct qstr d_name; /目录项的名字/ …… struct list_head d_subdirs; /子目录/ …… struct dentry_operations d_op; /目录项操作表/ struct super_block d_sb; /文件超级块/ …… };
struct dentry_operations { //判断目录项是否有效; int (d_revalidate)(struct dentry , struct nameidata ); //为目录项生成散列值; int (d_hash) (struct dentry , struct qstr ); …… }; | | :—- |
2.4 文件对象
文件对象是已打开的文件在内存中的表示,用于建立进程与磁盘上的文件对应关系。由sys_open() 现场创建,由sys_close()销毁;
| struct file { …… struct list_head f_list; /文件对象链表/ struct dentry f_dentry; /相关目录项对象/ struct vfsmount f_vfsmnt; /相关的安装文件系统/ struct file_operations f_op; /文件操作表*/ …… };
struct file_operations { …… //文件读操作 ssize_t (read) (struct file , char user , size_t, loff_t ); …… //文件写操作 ssize_t (write) (struct file , const char user , size_t, loff_t ); …… int (readdir) (struct file , void , filldir_t); …… //文件打开操作 int (open) (struct inode , struct file ); …… }; | | :—- |
2.5 VFS其他对象
根据文件系统所在的物理介质和数据在物理介质上的组织方式来区分不同的文件系统类型的。
- file_system_type结构用于描述具体的文件系统的类型信息。被Linux支持的文件系统,都有且仅有一 个file_system_type结构而不管它有零个或多个实例被安装到系统中。
- 每当一个文件系统被实际安装,就有一个vfsmount结构体被创建,这个结构体对应一个安装点。
| struct file_system_type { const char name; /文件系统的名字/ struct subsystem subsys; /sysfs子系统对象/ int fs_flags; /文件系统类型标志*/
/*在文件系统被安装时,从磁盘中读取超级块,在内存中组装超级块对象*/struct super_block *(*get_sb) (struct file_system_type*,int, const char*, void *);void (*kill_sb) (struct super_block *); /*终止访问超级块*/struct module *owner; /*文件系统模块*/struct file_system_type * next; /*链表中的下一个文件系统类型*/struct list_head fs_supers; /*具有同一种文件系统类型的超级块对象链表*/
};
struct vfsmount { struct list_head mnt_hash; /散列表/ struct vfsmount mnt_parent; /父文件系统/ struct dentry mnt_mountpoint; /安装点的目录项对象/ struct dentry mnt_root; /该文件系统的根目录项对象/ struct super_block mnt_sb; /该文件系统的超级块/ struct list_head mnt_mounts; /子文件系统链表/ struct list_head mnt_child; /子文件系统链表/ atomic_t mnt_count; /使用计数/ int mnt_flags; /安装标志/ char mnt_devname; /设备文件名/ struct list_head mnt_list; /描述符链表/ struct list_head mnt_fslink; /具体文件系统的到期列表/ struct namespace mnt_namespace; /相关的名字空间/ }; | | :—- |
- 与进程相关,打开的文件集
每个进程用一个 files_struct 结构来记录文件描述符的使用情况, 这个 files_struct结构称为用户打开文件表, 它是进程的私有数据。
| struct files_struct {atomic_t count; / 共享该表的进程数 /
rwlock_t file_lock; / 保护以下的所有域,以免在tsk->alloc_lock中的嵌套/
int max_fds; /当前文件对象的最大数/
int max_fdset; /当前文件描述符的最大数/
int next_fd; /已分配的文件描述符加1/
struct file * fd; / 指向文件对象指针数组的指针 /
fd_set close_on_exec; /指向执行exec( )时需要关闭的文件描述符/
fd_set open_fds; /指向打开文件描述符的指针/
fd_set close_on_exec_init;/ 执行exec( )时需要关闭的文件描述符的初 值集合/
fd_set open_fds_init; /文件描述符的初值集合/
struct file fd_array[32];/ 文件对象指针的初始化数组/
};struct fs_struct {//建立进程与文件系统的关系
atomic_t count; /结构的使用计数/
rwlock_t lock; /保护该结构体的锁/
int umask; /默认的文件访问权限/
struct dentry root; /根目录的目录项对象/
struct dentry pwd; /当前工作目录的目录项对象/
struct dentry altroot; /可供选择的根目录的目录项对象/
struct vfsmount rootmnt; /根目录的安装点对象/
struct vfsmount pwdmnt; /pwd的安装点对象/
struct vfsmount altrootmnt;/可供选择的根目录的安装点对象/
}; |
| :—- |
3 VFS对象间的联系
3.1 超级块、安装点和具体的文件系统的关系
- 每个被支持的文件系统,有且仅有一个file_system_type;
- 每安装一个文件系统,就会创建一个超级块和安装点与其对应。超级块的s_type对应具体文件系统类型,文件系统类型file_system-type的fs_supers链接具有统一文件类型的超级块。
- 相同文件类型的超级块通过域s_instances链接。
3.2 进程与超级块、文件、索引节点、目录项的关系
进程task_struct 中的files_struct files来记录文件描述符的使用情况;通常所说的文件描述符(fd)其实是进程打开的文件对象数组的索引值。这个files_struct结构称为用户打开文件表,它是进程的私有数据。
文件对象通过域f_dentry找到它对应的dentry对象,再由dentry对象的域d_inode找 到它对应的索引结点,这样就建立了文件对象与实际的物理文件的关联。
- 文件对象所对应的文件操作函数 列表是通过索引结点的域i_fop得到的。
4 基于VFS的文件I/O
4.1 open()

由于sys_open()的代码量大,函数调用关系复杂,以下主要是对该函数做整体的解析;而对其中的一些关键点,则列出其关键代码。
a. 从sys_open()的函数调用关系图可以看到,sys_open()在做了一些简单的参数检验后,就把接力棒传给do_sys_open():
1)、首先,get_unused_fd()得到一个可用的文件描述符;通过该函数,可知文件描述符实质是进程打开文件列表中对应某个文件对象的索引值;
2)、接着,do_filp_open()打开文件,返回一个file对象,代表由该进程打开的一个文件;进程通过这样的一个数据结构对物理文件进行读写操作。
3)、最后,fd_install()建立文件描述符与file对象的联系,以后进程对文件的读写都是通过操纵该文件描述符而进行。
b. do_filp_open()用于打开文件,返回一个file对象;而打开之前需要先找到该文件:
1)、open_namei()用于根据文件路径名查找文件,借助一个持有路径信息的数据结构nameidata而进行;
2)、查找结束后将填充有路径信息的nameidata返回给接下来的函数nameidata_to_filp()从而得到最终的file对象;当达到目的后,nameidata这个数据结构将会马上被释放。
c.open_namei()用于查找一个文件:
1)、path_lookup_open()实现文件的查找功能;要打开的文件若不存在,还需要有一个新建的过程,则调用 path_lookup_create(),后者和前者封装的是同一个实际的路径查找函数,只是参数不一样,使它们在处理细节上有所偏差;
2)、当是以新建文件的方式打开文件时,即设置了O_CREAT标识时需要创建一个新的索引节点,代表创建一个文件。在vfs_create()里的一句 核心语句dir->i_op->create(dir, dentry, mode, nd)可知它调用了具体的文件系统所提供的创建索引节点的方法。注意:这边的索引节点的概念,还只是位于内存之中,它和磁盘上的物理的索引节点的关系就像 位于内存中和位于磁盘中的文件一样。此时新建的索引节点还不能完全标志一个物理文件的成功创建,只有当把索引节点回写到磁盘上才是一个物理文件的真正创 建。想想我们以新建的方式打开一个文件,对其读写但最终没有保存而关闭,则位于内存中的索引节点会经历从新建到消失的过程,而磁盘却始终不知道有人曾经想 过创建一个文件,这是因为索引节点没有回写的缘故。
3)、path_to_nameidata()填充nameidata数据结构;
4)、may_open()检查是否可以打开该文件;一些文件如链接文件和只有写权限的目录是不能被打开的,先检查 nd->dentry->inode所指的文件是否是这一类文件,是的话则错误返回。还有一些文件是不能以TRUNC的方式打开的,若 nd->dentry->inode所指的文件属于这一类,则显式地关闭TRUNC标志位。接着如果有以TRUNC方式打开文件的,则更新 nd->dentry->inode的信息
不管是path_lookup_open()还是path_lookup_create()最终都是调用 __path_lookup_intent_open()来实现查找文件的功能。 查找时,在遍历路径的过程中,会逐层地将各个路径组成部分解析成目录项对象,如果此目录项对象在目录项缓存中,则直接从缓存中获得;如果该目录项在缓存中 不存在,则进行一次实际的读盘操作,从磁盘中读取该目录项所对应的索引节点。得到索引节点后,则建立索引节点与该目录项的联系。如此循环,直到最终找到目 标文件对应的目录项,也就找到了索引节点,而由索引节点找到对应的超级块对象就可知道该文件所在的文件系统的类型。 从磁盘中读取该目录项所对应的索引节点;这将引发VFS和实际的文件系统的一次交互。从前面的VFS理论介绍可知,读索引节点方法是由超级块来提供的。而 当安装一个实际的文件系统时,在内存中创建的超级块的信息是由一个实际文件系统的相关信息来填充的,这里的相关信息就包括了实际文件系统所定义的超级块的 操作函数列表,当然也就包括了读索引节点的具体执行方式。 当继续追踪一个实际文件系统ext3的ext3_read_inode()时,可发现这个函数很重要的一个工作就是为不同的文件类型设置不同的索引节点操 作函数表和文件操作函数表。
这是VFS与实际的文件系统联系的一个关键点。从3.1.1小节分析中可知,调用实际文件系统读取索引节点的方法读取索引节点时,实际文件系统会根据文件 的不同类型赋予索引节点不同的文件操作函数集,如普通文件有普通文件对应的一套操作函数,设备文件有设备文件对应的一套操作函数。这样当把对应的索引节点 的文件操作函数集赋予文件对象,以后对该文件进行操作时,比如读操作,VFS虽然对各种不同文件都是执行同一个read()操作界面,但是真正读时,内核 却知道怎么区分对待不同的文件类型。
4.2 文件读写基本流程
读文件
1、进程调用库函数向内核发起读文件请求;
2、内核通过检查进程的文件描述符定位到虚拟文件系统的已打开文件列表表项;
3、调用该文件可用的系统调用函数read()
3、read()函数通过文件表项链接到目录项模块,根据传入的文件路径,在目录项模块中检索,找到该文件的inode;
4、在inode中,通过文件内容偏移量计算出要读取的页;
5、通过inode找到文件对应的address_space;
6、在address_space中访问该文件的页缓存树,查找对应的页缓存结点:
(1)如果页缓存命中,那么直接返回文件内容;
(2)如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页;重新进行第6步查找页缓存;
7、文件内容读取成功。
写文件
前5步和读文件一致,在address_space中查询对应页的页缓存是否存在:
6、如果页缓存命中,直接把文件内容修改更新在页缓存的页中。写文件就结束了。这时候文件修改位于页缓存,并没有写回到磁盘文件中去。
7、如果页缓存缺失,那么产生一个页缺失异常,创建一个页缓存页,同时通过inode找到文件该页的磁盘地址,读取相应的页填充该缓存页。此时缓存页命中,进行第6步。
8、一个页缓存中的页如果被修改,那么会被标记成脏页。脏页需要写回到磁盘中的文件块。有两种方式可以把脏页写回磁盘:
(1)手动调用sync()或者fsync()系统调用把脏页写回
(2)pdflush进程会定时把脏页写回到磁盘
同时注意,脏页不能被置换出内存,如果脏页正在被写回,那么会被设置写回标记,这时候该页就被上锁,其他写请求被阻塞直到锁释放。
5 硬链接和软连接
5.1 硬链接
一般情况下,文件名和inode号是”一一对应”关系,每个inode号码对应一个文件名。但是,Unix/Linux系统允许,多个文件名指向同一个inode号码。这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为”硬链接”(hard link)。
5.1.1 创建硬链接
ln命令可以创建硬链接,格式:ln 源文件 目标文件
运行上面这条命令以后,源文件与目标文件的inode号码相同,都指向同一个inode。inode信息中有一项叫做”链接数”,记录指向该inode的文件名总数,这时就会增加1。反过来,删除一个文件名,就会使得inode节点中的”链接数”减1。当这个值减到0,表明没有文件名指向这个inode,系统就会回收这个inode号码,以及其所对应block区域。
- 创建目录时,默认会生成两个目录项:”.”和”..”。前者的inode号码就是当前目录的inode号码,等同于当前目录的”硬链接”;后者的inode号码就是当前目录的父目录的inode号码,等同于父目录的”硬链接”。所以,任何一个目录的”硬链接”总数,总是等于2加上它的子目录总数(含隐藏目录),这里的2是父目录对其的“硬链接”和当前目录下的”.硬链接“。
相当于windows中复制一个文件,然后重命名这个复制的文件。
5.2 软链接
文件A和文件B的inode号码虽然不一样,但是文件A的内容是文件B的路径。读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的”软链接”(soft link)或者”符号链接(symbolic link)。
5.2.1 穿件软连接
ln -s命令可以创建软链接,ln -s 源文文件或目录 目标文件或目录。
文件A依赖于文件B而存在,如果删除了文件B,打开文件A就会报错:”No such file or directory”。
- 由于文件A的inode与文件B不同,所以创建文件B的软连接,不会影响文件B的inode号的链接数不会变化。
- 软链接相当于windows的快捷方式。
