我们在1,2中讲了Linux下UFS,这次我们将一下Linux下的VFS,并且与UFS做一定的对比。
VFS所隐含的主要思想在于引入了一个通用的文件模型,这个模型能够表示所支持的文件系统。有点类似于JDBC实现对数据库的统一操作。
本质上说,Linux内核不能对一个特定的函数进行硬编码执行注入read()或ioctrl()这样的操作,而是对每个操作都必须使用一个指针,指向要访问的具体文件系统的适当函数。比如应用程序对open()的调用引起内核调用的sys_open()
服务例程,这与其他系统调用完全类似。文件在内核内存中是由一个file数据结构表示的。
我们可以把通用文件系统看做是面向对象的。出于效率考虑,Linux的编码并未采用面向对象的程序设计语言。因此对象作为普通的C数据结构来实现,数据结构中指向函数的字段就对应于对象的方法。(嗯,用C语言写面向对象)
综述:
通用文件模型由下列对象类型组成:
超级块对象(superblock object):对应于存放在磁盘上的文件系统控制块(filesystem control block)。源码位于linux/fs.h
索引节点对象(inode object):存放关于具体文件的一般信息。对应于存放在磁盘上的文件控制块(file control block)。源码位于linux/fs.h
文件对象(file object):存放打开文件与进程之间进行交互的有关信息。这类信息仅当进程访问文件期间存放在内核内存中。源码位于linux/fs.h
目录项对象(dentry object):存放目录项(也就是文件的特定名称)与对应文件进行连接的有关信息。没给磁盘文件系统都以自己特有的方式将该类信息存在磁盘上。源码位于linux/dcache.h中。
进程与VFS对象之间的交互如下图(三个进程打开了同一个文件)所示:
VFS具有两个功能:1、为所有文件系统的实现提供一个通用接口;2、将最近最常使用的目录项对象放在所谓目录项高速缓存中,以加速从文件路径名到最后一个路径分量的索引节点的转换过程。
注意:
1、这里的磁盘高速缓存不同于硬件高速缓存或内存高速缓存,后两者都与磁盘或其他设备无关。硬件高速缓存是一个快速静态RAM,他加快了直接对慢速动态RAM的请求。内存高速缓存是一种软件机制,引入他是为了绕过内核内存分配器)(slab分配器)。
2、VFS虽然是应用程序和具体文件系统之间的一层,但在某些情况下,一个文件的操作可能由VFS本身去执行,无需调用底层函数。i.e. 当系统调用lseek()修改一个文件指针,而这个文件指针是打开文件与进程交互所涉及的一个属性时,VFS就只需修改对应的文件对象,而不必访问磁盘上的文件,因此,无需调用具体文件系统的函数。从某种意义上说,可以把VFS看成“通用”文件系统,他在必要时依赖某种具体文件系统。
VFS的数据结构
对上面的超级块对象、索引节点对象、文件对象和目录项对象的结构体进行介绍,这些介绍都放在内存中,所以不可能和UFS中的结构体那么详细,具体区别有哪些。是我们这里的重点。
目标文件:Linux-2.6.0includelinuxfs.h
虚拟文件系统头文件(Linux-2.6.0includelinuxfs.h)结构体概况:
超级块对象(super_block object):
所有超级块对象都是以双向循环链表的形式连接在一起。链表的第一个元素用super_blocks变量表示。
对应于ext2_super_block这个具体文件系统的超级块结构体。VFS的超级块对象则使用s_fs_info指向ext2_sb_info (最后一行)。
通常,为了效率起见,由s_fs_info 字段所指向的数据被复制到内存。任何基于磁盘的文件系统都需要访问和更改自己的磁盘分配位图,以便分配或释放磁盘块。VFS允许这些文件系统直接对内存超级块s_fs_info进行操作,而无需访问磁盘。
但是这样会带来一个新问题,就是VFS上的s_fs_info最终不再和磁盘上相应的超级块同步。因此,需要一个s_dirt标志标志该超级块是否是脏的。另外还有一个问题就是突然断电————linux通过周期性将“脏”的超级块写回磁盘来减少该问题的危害。
超级块对象(super_block object)相关操作:
/* * NOTE: write_inode, delete_inode, clear_inode, put_inode can be called * without the big kernel lock held in all filesystems. */ struct super_operations { struct inode *(*alloc_inode)(struct super_block *sb); /*为索引节点对象分配空间,包括具体文件系统的数据所需要的空间*/ void (*destroy_inode)(struct inode *); /* 撤销索引节点对象,包括具体文件系统的数据 */ void (*read_inode) (struct inode *); /* 用磁盘上的数据填充以参数传递过来的索引节点对象的字段,*/ void (*dirty_inode) (struct inode *); /* 当脏调用时,ext3用这个脏节点来更新磁盘上的文件系统日志*/ void (*write_inode) (struct inode *, int);/* 更新一文件系统的索引节点,int表示是否同步操作 */ void (*put_inode) (struct inode *); /* 释放 */ void (*drop_inode) (struct inode *); /* 用户释放最后一个的时候调用*/ void (*delete_inode) (struct inode *); /* 删除内存中VFS索引节点和磁盘上的文件数据及元数据 */ void (*put_super) (struct super_block *); /* 当相应文件系统被卸载时,释放超级块对象 */ void (*write_super) (struct super_block *); /* 更新超级块*/ int (*sync_fs)(struct super_block *sb, int wait); void (*write_super_lockfs) (struct super_block *); void (*unlockfs) (struct super_block *); int (*statfs) (struct super_block *, struct kstatfs *); int (*remount_fs) (struct super_block *, int *, char *); void (*clear_inode) (struct inode *); void (*umount_begin) (struct super_block *); int (*show_options)(struct seq_file *, struct vfsmount *); };
如果VFS需要调用其中一个操作时,比如说read_inode(),它执行了下列操作:
sb->s_op->read_inode(inode);
索引节点对象(inode object):
文件系统处理文件所需的所有信息都在一个名为索引节点的数据结构中。文件名可以随便改,但是索引节点对文件是唯一的,并随着文件存在而存在。
struct inode { struct hlist_node i_hash; // 用于hash链表的指针 struct list_head i_list; // 用于描述索引节点当前状态的链表的指针 struct list_head i_dentry; // 引用索引节点的目录对象链表的头,后面有用到! unsigned long i_ino; // 索引节点号 atomic_t i_count; // 引用计数器 // ... }
每个索引节点对象总是出现在下列双向链表的某个链表中。(所有情况下,指向相邻元素的指针存放在i_list字段中):
1. 有效未使用的索引节点链表,这个链表用作磁盘高速缓存
2. 正在使用的索引节点链表,也就是那些镜像有效的磁盘索引节点。
3. 脏索引节点的链表。
此外,每个索引节点对象也包含在每个文件系统的双向循环链表中,链表头存放在超级块s_inode字段中。
最后,索引节点对象也存放在一个称为inode_hashtable的hash表中。hash表加快了对索引节点对象的搜索,前提是系统内核要知道索引节点号及文件所在文件系统的超级块对象的地址。另外考虑到哈希冲突,所以索引节点对象包含了i_hash字段,该字段包含向前向后 两个指针,分别指向散列到同一地址的前/后的索引节点:该字段因此创建了由这些索引节点组成的一个双向链表。
文件对象(file object):
文件对象描述进程怎样与一个打开的文件进行交互。文件对象是在文件被打开时创建的,由一个file结构组成。注意:文件对象在磁盘上没有相应的映像,因此file结构中没有设置"脏"字段来表示文件对象是否已被修改。
存放在文件对象中的主要信息是文件指针,即文件中当前的位置,下个操作将在该位置发生。由于几个进程可能同时访问同一个文件,因此文件指针必须存放在文件对象而不是索引节点中。
struct file { struct list_head f_list; // 用于通用文件对象链表的指针 struct dentry *f_dentry; // 与文件相关的目录项对象 struct vfsmount *f_vfsmnt; // 含有该文件的已安装文件系统 struct file_operations *f_op; // 指向文件操作表的指针 atomic_t f_count; // 文件对象的引用计数器:他记录使用文件对象的进程数 unsigned int f_flags; // 打开文件的方式rwx mode_t f_mode; // 进程访问模式 loff_t f_pos; // 文件指针 struct fown_struct f_owner; // unsigned int f_uid, f_gid; // uid和gid int f_error; struct file_ra_state f_ra; unsigned long f_version; void *f_security; /* needed for tty driver, and maybe others */ void *private_data; /* Used by fs/eventpoll.c to link all the hooks to this file */ struct list_head f_ep_links; spinlock_t f_ep_lock; };
文件对象中通过一个名为filp的slab高速缓存分配,filp描述符地址存储在filp_cachep变量中。由于分配的文件对象数目是有限的,而files_stat变量中指定了系统可同时访问的最大文件数(RAM的大小的1/10)。
当VFS代表进程必须打开一个文件时,它调用get_empty_filp()函数来分配一个新的文件对象。该函数调用kmem_cache_alloc()从filp高速缓存中获得一个空闲的文件对象,然后初始化这个对象的字段:
memset(f, 0, sizeof(*f)); // 给 f 配置一数组 INIT_LIST_HEAD(&f->f_ep_links); // 定义一个双向链表头 spin_lock_init(&f->f_ep_links); // 内核同步机制宏 自旋锁初始化 atomic_set(&f->f_count,1); // 计数器设置为1 f->f_uid=current->fsuid; f->f_gid=current->fsgid; // 设置UID 和GID f->f_owner.lock= RW_LOCK_UNLOCKED; // 设置读写锁 INIT_LIST_HEAD(&f->f_list); // 初始化f_list,定义双向链表头 f->f_maxcount=INT_MAX ; // 最大文件数 /* #define INIT_LIST_HEAD(ptr) do { (ptr)->next = (ptr); (ptr)->prev = (ptr); } while (0) */
当内核将一个索引节点从磁盘装入内存时,就会把指向这些文件操作的指针存放在file_operations结构中,而该结构的地址存放在该索引节点对象的i_fop字段中。当进程打开这个文件时,VFS就用存放在索引节点中的这个地址初始化新文件对象的f_op字段,使得对文件操作后续调用能够使用这些函数。
注意:f_op字段中的方法和file_operations的关系,是包含关系。f_op中的方式是file_operations的子集,因为对于一个具体的文件类型,只使用其中的部分方法,那些未实现的方法被置位NULL。
目录项对象(dentry object):
1. dentry数据结构是没有存在于Linux-2.6.0includelinuxfs.h,而是存在于Linux-2.6.0includelinuxdcache.h
2. VFS实际上把dentry看作为由子目录和文件组成的“普通文件”。一旦目录项被读入内存,VFS就把它转化成基于dentry结构的一个目录项对象,数据结构如下。目录项对象将每个分量与其对应的索引节点相联系。
* 例如在查找路径`/tmp/test`时, 内核为根目录' / ' 创建一个目录项对象,为根目录项下面的 ` tmp ` 项也创建一个,为` /tmp `目录下的test创建一个第三级目录项。
* 目录项对象在磁盘没有对应的映像,因此dentry结构不包含指出该对象已被修改的字段。
* 目录项对象存放在名为dentry_cached的slab分配器高速缓存中。因此,目录项对象也是通过kmem_cache_alloc()和kmem_cache_free()实现创建和删除的。
struct dentry { atomic_t d_count; unsigned long d_vfs_flags; /* moved here to be on same cacheline */ spinlock_t d_lock; /* per dentry lock */ struct inode * d_inode; /* Where the name belongs to - NULL is negative */ struct list_head d_lru; /* LRU list */ struct list_head d_child; /* child of parent list */ struct list_head d_subdirs; /* our children */ struct list_head d_alias; /* inode alias list */ unsigned long d_time; /* used by d_revalidate */ struct dentry_operations *d_op; struct super_block * d_sb; /* The root of the dentry tree */ unsigned int d_flags; int d_mounted; void * d_fsdata; /* fs-specific data */ struct rcu_head d_rcu; struct dcookie_struct * d_cookie; /* cookie, if any */ unsigned long d_move_count; /* to indicated moved dentry while lockless lookup */ struct qstr * d_qstr; /* quick str ptr used in lockless lookup and concurrent d_move */ struct dentry * d_parent; /* parent directory */ struct qstr d_name; struct hlist_node d_hash; /* lookup hash list */ struct hlist_head * d_bucket; /* lookup hash bucket */ unsigned char d_iname[DNAME_INLINE_LEN_MIN]; /* small names */ } ____cacheline_aligned;
3. 每个目录项可以处于以下四个状态之一:
* 空闲状态(free):没有被VFS使用。对应的内存由slab分配器进行处理。
* 未使用状态(unused): 没有被内核使用。该对象的引用计数为0,目录项对象包含有效信息,但为了在必要时收回内存,它的内容可能被丢弃。d_inode有所指向
* 正在使用状态(in use):处于该状态的目录项对象当前正被内核使用。该目录项包含有效信息, d_inode有指向。
* 负状态(negative):与目录项相关的索引节点不复存在,因为相应的索引节点已经被删除了,或者因为目录项对象是通过解析一个不存在的文件的路径名创建的。这时候d_inode=NULL,但该对象仍然保存在目录项高速缓存中,以便后续对同一文件目录项的查找操作能够快速完成。
4. 与目录项相关的操作称为目录项操作,这些方法结构存放在dentry_operations结构中。而在dentry的d_op字段中存放着。文件系统定义了他们自己的目录项方法,所以dentry_operations中的这些字段通常为NULL。
struct dentry_operations { int (*d_revalidate)(struct dentry *, struct nameidata *); // 在把目录项转化为namei数据(一个文件的路径名)前,看这个目录项是否有效 int (*d_hash) (struct dentry *, struct qstr *); // 生成一个hash用于目录项的hash表 int (*d_compare) (struct dentry *, struct qstr *, struct qstr *); // 比较两个文件名。 int (*d_delete)(struct dentry *); // 当最后一个引用被删除(d_count=0)时,调用该方法。 void (*d_release)(struct dentry *); // 释放一个目录项,放入slab分配器时调用该方法。 void (*d_iput)(struct dentry *, struct inode *); // 当目录项为负状态时,调用该方法。 };
目录项高速缓存(缓冲池)
0. 目录项高速缓存的作用还相当于索引节点高速缓存(inode cache)的控制器。索引节点对象保存在RAM中,并能够借助响应的目录项快速引用他们。
1. 由于从磁盘读入一个目录项并构造相应的目录需要大量的时间,为了最大限度的提高处理目录项的效率,linux使用目录项高速缓存,它由两种类型的数据结构组成:
* 一个处于正在使用、未使用或负状态的目录项对象的集合
* 一个散列表,从中能够快速获取或给定的文件名和目录名对应的目录项对象。如果不在高速缓存,则返回NULL。
2. 一个处于正在使用、未使用或负状态的目录项对象的集合:
* 对于“未使用”目录项对象都存在一个LRU的双向链表中,该链表按照插入时间排序。d_lru字段包含指向链表中相邻目录项的指针。
* 每个“正在使用”的目录项对象都插入一个双向循环链表中,该链表由相应索引节点对象的i_dentry字段所指向(由于每个索引节点可以与若干个硬链接关联,所以需要一个链表)。当指向相应文件的最后一个硬链接被删除后,一个“正在使用”的目录项对象可能变成“负”状态。这种情况下,该目录项被移到“未使用”目录项对象组成的LRU链表中。
* 每当内核缩减目录项高速缓存时,“负”状态目录项对象就朝着LRU链表的尾部移动,这样一来,这些对象就逐步被释放。
4. 散列表是由dentry_hashtable数组实现的。数组中每个元素都是一个指针,这种链表就是把具有相同散列表指的目录项进行散列而形成的。该数组的长度取决于系统已安装RAM数量。
* 目录项的d_hash字段包含了指向具有相同散列值的链表中的相邻元素。散列函数产生的值是由目录的目录项对象和文件名计算出来的。hashno = ((diskno + blkno)/RND) % BUFHSZ
5. dcache_lock自旋锁保护目录项高速缓存数据结构免受多处理器系统上的同时访问。d_lookup()函数在散列表中查找给定的父目录项对象和文件名;为了避免发生竞争,使用顺序所锁(seqlock)。
文件系统类型
这部分的内容主要讲了VFS和UFS之间的挂在,安装操作等。不再具体描述,借用上课老师画的图来概括吧。