Linux虚拟文件系统解析
概述
虚拟文件系统(Virtual Filesystem Switch,简称VFS)所有的数据结构都是在运行以后建立的,并在卸载时删除,在磁盘中并没有存储这些数据结构。虚拟文件系统只有和实际文件系统(例如:Ext2、VFAT)等相结合,才能开始工作,所以虚拟文件系统不是一个真正的文件系统。与VFS相对应,Ext2、VFAT等为具体文件系统。
1、虚拟文件系统的作用
VFS对于具体文件系统来说,相当于一个管理者,提供统一的文件系统接口。对于内核其他子系统以及在操作系统之上的用户程序而言,所有的文件系统都是一样的。实际上,要支持新的文件系统,主要需要完成的工作就是编写这些接口函数。虚拟文件系统主要有以下几个作用:
(1)对具体文件系统的数据结构进行抽象,以统一的数据结构进行管理;
(2)接收用户层的调用,例如write、read、open等;
(3)支持多种具体文件系统之间的相互访问;
(4)接收内核其他子系统的操作请求,特别是内存管理子系统。
图1 Linux中文件系统的逻辑关系示意图
2、虚拟文件系统的系统调用
表1列出VFS中部分系统调用,这些系统调用涉及文件系统、常规文件、目录以及符号链接。
表1 VFS的部分系统调用
VFS是应用程序与具体文件系统之间的一个层。不过,在某些情况下,一个文件操作可能由VFS本身去执行,无需调用下一层程序。例如,当某个进程关闭一个打开的文件时,并不需要涉及磁盘上的相应文件,因此,VFS只需释放对应的文件对象。从某种意义来说,可以把VFS当作“通用”文件系统,在必要时依赖某种具体的文件系统。
VFS的数据结构
虚拟文件系统所隐含的主要思想在于引入一种通用的文件模型,这种模型能够支持所有的文件系统。该模型严格遵守传统UNIX文件系统的文件模型。
通用数据模型只能存在于内存中,由下列对象类型组成:
(1)超级块(super block)对象:存放系统中已安装的文件系统的所有信息。对于基于磁盘的文件系统,这类对象通常对应于存放在磁盘上的文件系统控制块。也就是说,每个文件系统都有一个超级块对象;
(2)索引节点(inode)对象:存放对于具体文件的一般信息。对于基于磁盘的文件系统,这类对象通常对应于存档在磁盘上的文件控制块(PCB),也就是说,每个文件都有一个索引节点对象,每个索引节点对象有一个索引节点号,这个号唯一地标识在某个文件系统中的指定文件;
(3)目录项(dentry)对象:存放目录项与对应文件进行链接的信息。VFS把每个目录看作一个有若干个子目录和文件组成的常规文件;
(4)文件(file)对象:存放打开文件与进程之间进行交互的有关信息。这类信息仅当进程访问文件存放于内存中。
1、超级块
很多具体文件系统中都有超级块结构,超级块是这些文件系统中最重要的数据结构,它是来描述整个文件系统信息的,可以说是一个全局的数据结构。VFS超级块是各种具体文件系统在安装时建立的,并在这些文件系统卸载时自动删除,可见,VFS超级块只存在于内存中,同时提到VFS超级块应该说成是哪个具体文件系统的VFS超级块。VFS超级块在include/fs/fs.h中定义,即数据结构super_block,该结构及其主要域含义如下:
1 struct super_block 2 { 3 /************描述具体文件系统的整体信息的域*****************/ 4 kdev_t s_dev; /* 包含该具体文件系统的块设备标识符。例如,对于 /dev/hda1,其设备标识符为 0x301*/ 5 unsigned long s_blocksize; /*该具体文件系统中数据块的大小,以字节为单位 */ 6 unsigned char s_blocksize_bits; /*块大小的值占用的位数,例如,如果块大小为 1024 字节,则该值为 10*/ 7 unsigned long long s_maxbytes; /* 文件的最大长度 */ 8 unsigned long s_flags; /* 安装标志*/ 9 unsigned long s_magic; /*魔数,即该具体文件系统区别于其他文件系统的一个标志*/ 10 11 /**************用于管理超级块的域******************/ 12 struct list_head s_list; /*指向超级块链表的指针*/ 13 struct semaphore s_lock /*锁标志位,若置该位,则其他进程不能对该超级块操作*/ 14 struct rw_semaphore s_umount /*对超级块读写时进行同步*/ 15 unsigned char s_dirt; /*脏位,若置该位,表明该超级块已被修改*/ 16 struct dentry *s_root; /*指向该具体文件系统安装目录的目录项*/ 17 int s_count; /*对超级块的使用计数*/ 18 atomic_t s_active; 19 struct list_head s_dirty; /*已修改的索引节点形成的链表 */ 20 struct list_head s_locked_inodes;/* 要进行同步的索引节点形成的链表*/ 21 struct list_head s_files 22 23 /***********和具体文件系统相联系的域*************************/ 24 struct file_system_type *s_type; /*指向文件系统的 25 file_system_type 数据结构的指针 */ 26 struct super_operations *s_op; /*指向某个特定的具体文件系统的用于超级块操作的函数集合 */ 27 struct dquot_operations *dq_op; /* 指向某个特定的具体文件系统用于限额操作的函数集合 */ 28 u; /*一个共用体,其成员是各种文件系统的 fsname_sb_info 数据结构 */ 29 30 };
所有超级块对象(每个已安装的文件系统都有一个超级块)以双向链表的形式链接在一起。链表中的第一个元素和最后一个元素的地址分别存放在super_block对象的s_list域中的next和prev域中。超级块最后一个u共用体包括属于具体文件系统的超级块信息:
union { struct Minix_sb_info Minix_sb; struct Ext2_sb_info Ext2_sb; struct ext3_sb_info ext3_sb; struct hpfs_sb_info hpfs_sb; struct ntfs_sb_info ntfs_sb; struct msdos_sb_info msdos_sb; struct isofs_sb_info isofs_sb; struct nfs_sb_info nfs_sb; struct sysv_sb_info sysv_sb; struct affs_sb_info affs_sb; struct ufs_sb_info ufs_sb; struct efs_sb_info efs_sb; struct shmem_sb_info shmem_sb; struct romfs_sb_info romfs_sb; struct smb_sb_info smbfs_sb; struct hfs_sb_info hfs_sb; struct adfs_sb_info adfs_sb; struct qnx4_sb_info qnx4_sb; struct reiserfs_sb_info reiserfs_sb; struct bfs_sb_info bfs_sb; struct udf_sb_info udf_sb; struct ncp_sb_info ncpfs_sb; struct usbdev_sb_info usbdevfs_sb; struct jffs2_sb_info jffs2_sb; struct cramfs_sb_info cramfs_sb; void *generic_sbp; } u;
通常,为了效率起见 u 域的数据被复制到内存。任何基于磁盘的文件系统都需要访问和更改自己的磁盘分配位示图,以便分配和释放磁盘块。VFS 允许这些文件系统直接对内存超级块的 u 联合体域进行操作,无需访问磁盘。为了保证VFS超级块域磁盘上相应的超级块同步,使用s_dirt标志来表示数据是否需要被更新。Linux是通过周期性地将所有"脏"的超级块协会磁盘来减少该问题带来的危害。
2、VFS的索引节点
文件系统处理文件所需要的所有信息都放在称为索引节点的数据结构中,索引节点对于文件是唯一的。具体文件系统的索引节点是存储在磁盘上的,是一种静态结构,要使用它,必须调入内存,填写VFS的索引节点,因此,也称VFS索引节点为动态节点。VFS索引节点的数据结构inode在/include/fs/fs.h/中定义如下:
1 struct inode 2 { 3 /**********描述索引节点高速缓存管理的域****************/ 4 struct list_head i_hash; /*指向哈希链表的指针*/ 5 struct list_head i_list; /*指向索引节点链表的指针*/ 6 struct list_head i_dentry;/*指向目录项链表的指针*/ 7 struct list_head i_dirty_buffers; 8 struct list_head i_dirty_data_buffers; 9 10 /**********描述文件信息的域****************/ 11 unsigned long i_ino; /*索引节点号*/ 12 kdev_t i_dev; /*设备标识号 */ 13 umode_t i_mode; /*文件的类型与访问权限 */ 14 nlink_t i_nlink; /*与该节点建立链接的文件数 */ 15 uid_t i_uid; /*文件拥有者标识号*/ 16 gid_t i_gid; /*文件拥有者所在组的标识号*/ 17 kdev_t i_rdev; /*实际设备标识号*/ 18 off_t i_size; /*文件的大小(以字节为单位)*/ 19 unsigned long i_blksize; /*块大小*/ 20 unsigned long i_blocks; /*该文件所占块数*/ 21 time_t i_atime; /*文件的最后访问时间*/ 22 time_t i_mtime; /*文件的最后修改时间*/ 23 time_t i_ctime; /*节点的修改时间*/ 24 unsigned long i_version; /*版本号*/ 25 struct semaphore i_zombie; /*僵死索引节点的信号量*/ 26 27 /***********用于索引节点操作的域*****************/ 28 struct inode_operations *i_op; /*索引节点的操作*/ 29 struct super_block *i_sb; /*指向该文件系统超级块的指针 */ 30 atomic_t i_count; /*当前使用该节点的进程数。计数为 0,表明该节点可丢弃或被重新使用 */ 31 struct file_operations *i_fop; /*指向文件操作的指针 */ 32 unsigned char i_lock; /*该节点是否被锁定,用于同步操作中*/ 33 struct semaphore i_sem; /*指向用于同步操作的信号量结构*/ 34 wait_queue_head_t *i_wait; /*指向索引节点等待队列的指针*/ 35 unsigned char i_dirt; /*表明该节点是否被修改过,若已被修改,则应当将该节点写回磁盘*/ 36 struct file_lock *i_flock; /*指向文件加锁链表的指针*/ 37 struct dquot *i_dquot[MAXQUOTAS]; /*索引节点的磁盘限额*/ 38 /************用于分页机制的域**********************************/ 39 struct address_space *i_mapping; /* 把所有可交换的页面管理起来*/ 40 struct address_space i_data; 41 42 /**********以下几个域应当是联合体****************************************/ 43 struct list_head i_devices; /*设备文件形成的链表*/ 44 struct pipe_inode_info i_pipe; /*指向管道文件*/ 45 struct block_device *i_bdev; /*指向块设备文件的指针*/ 46 struct char_device *i_cdev; /*指向字符设备文件的指针*/ 47 48 /*************************其他域***************************************/ 49 unsigned long i_dnotify_mask; /* Directory notify events */ 50 struct dnotify_struct *i_dnotify; /* for directory notifications */ 51 unsigned long i_state; /*索引节点的状态标志*/ 52 unsigned int i_flags; /*文件系统的安装标志*/ 53 unsigned char i_sock; /*如果是套接字文件则为真*/ 54 atomic_t i_writecount; /*写进程的引用计数*/ 55 unsigned int i_attr_flags; /*文件创建标志*/ 56 __u32 i_generation /*为以后的开发保留*/ 57 58 /*************************各个具体文件系统的索引节点********************/ 59 union; /*类似于超级块的一个共用体,其成员是各种具体文件系统的 fsname_inode_info 数据结构 */ 60 }
每个文件都有一个inode,每个inode有一个索引节点号i_ino;inode中有两个设备号,i_dev和i_rdev。除特殊文件外,每个节点都存储在某个设备上,这就是 i_dev。如果索引节点所代表的并不是常规文件,而是某个设备,那就还得有个设备号,这就是 i_rdev;每个索引节点都会复制磁盘索引节点包含的一些数据,比如文件占用的磁盘块数。属于"正在使 用"或"脏"链表的索引节点对象也同时存放在一个称为inode_hashtable链表中。
3、目录项结构
每个文件除了有一个索引节点 inode 数据结构外,还有一个目录项 dentry(directory enrty)数据结构。dentry 结构中有个 d_inode 指针指向相应的 inode 结构。dentry 结构代表的是逻辑意义上的文件,所描述的是文件逻辑上的属性,因此,目录项对象在磁盘上并没有对应的映像;而 inode 结构代表的是物理意义上的文件,记录的是物理上的属性,对于一个具体的文件系统(如 Ext2),Ext2_ inode 结构在磁盘上就有对应的映像。所以说,一个索引节点对象可能对应多个目录项对象。dentry的定义在include/linux/deach.h中:
1 struct dentry { 2 atomic_t d_count; /* 目录项引用计数器 */ 3 unsigned int d_flags; /* 目录项标志 */ 4 struct inode * d_inode; /* 与文件名关联的索引节点 */ 5 struct dentry * d_parent; /* 父目录的目录项 */ 6 struct list_head d_hash; /* 目录项形成的哈希表 */ 7 struct list_head d_lru; /*未使用的 LRU 链表 */ 8 struct list_head d_child; /*父目录的子目录项所形成的链表 */ 9 struct list_head d_subdirs; /* 该目录项的子目录所形成的链表*/ 10 struct list_head d_alias; /* 索引节点别名的链表*/ 11 int d_mounted; /* 目录项的安装点 */ 12 struct qstr d_name; /* 目录项名(可快速查找) */ 13 unsigned long d_time; /* 由 d_revalidate 函数使用 */ 14 struct dentry_operations *d_op; /* 目录项的函数集*/ 15 struct super_block * d_sb; /* 目录项树的根 (即文件的超级块)*/ 16 unsigned long d_vfs_flags; 17 void * d_fsdata; /* 具体文件系统的数据 */ 18 unsigned char d_iname[DNAME_INLINE_LEN]; /* 短文件名 */ 19 };
一个有效的dentry结构必定有一个inode结构,一个文件可以有不止一个文件名或路径名,因为一个已经建立的文件可能被链接(link)到其他文件名。在inode结构中有一个队列i_dentry,凡是代表着同一个文件的所有目录项都通过其 dentry 结构中的d_alias域挂入相应inode结构中的i_dentry队列。
一个文件系统中所有目录项结构或组织成一个哈希表,或组织成一棵树,或按照某种需要组织成一个链表,为文件访问和文件路径搜索奠定下良好的基础。
4、与进程相关的文件结构
<1> 文件对象
每个文件都有一个32位的数字来表示下一个读写的字节位置,Linux中专门使用数据结构file来保存打开文件的文件位置,此外,file结构还把指向该文件索引节点的指针也放在其中。file结构形成一个双链表,称为系统打开文件表,其最大长度为NR_FILE,在fs.h中定义为8192。file结构在include/inux/fs.h中定义如下:
1 struct file 2 { 3 struct list_head f_list; /*所有打开的文件形成一个链表*/ 4 struct dentry *f_dentry; /*指向相关目录项的指针*/ 5 struct vfsmount *f_vfsmnt; /*指向 VFS 安装点的指针*/ 6 struct file_operations *f_op; /*指向文件操作表的指针*/ 7 mode_t f_mode; /*文件的打开模式*/ 8 loff_t f_pos; /*文件的当前位置*/ 9 unsigned short f_flags; /*打开文件时所指定的标志*/ 10 unsigned short f_count; /*使用该结构的进程数*/ 11 unsigned long f_reada, f_ramax, f_raend, f_ralen, f_rawin;/*预读标志、要预读的最多页面数、上次预读后的文件指针、预读的字节数以及预读的页面数*/ 12 int f_owner; /* 通过信号进行异步 I/O 数据的传送*/ 13 unsigned int f_uid, f_gid; /*用户的 UID 和 GID*/ 14 int f_error; /*网络写操作的错误码*/ 15 unsigned long f_version; /*版本号*/ 16 void *private_data; /* tty 驱动程序所需 */ 17 };
<2> 用户打开文件表
每个进程用一个file_struct结构来记录文件描述符的使用情况,这个file_struct结构称为用户打开文件表,是进程的私有数据。file_struct结构在include/linux/sched.h中定义如下:
1 struct files_struct { 2 atomic_t count; /* 共享该表的进程数 */ 3 rwlock_t file_lock; /* 保护以下的所有域,以免在tsk->alloc_lock 中的嵌套*/ 4 int max_fds; /*当前文件对象的最大数*/ 5 int max_fdset; /*当前文件描述符的最大数*/ 6 int next_fd; /*已分配的文件描述符加 1*/ 7 struct file ** fd; /* 指向文件对象指针数组的指针 */ 8 fd_set *close_on_exec; /*指向执行 exec( )时需要关闭的文件描述符*/ 9 fd_set *open_fds; /*指向打开文件描述符的指针*/ 10 fd_set close_on_exec_init;/* 执行 exec( )时需要关闭的文件描述符的初 值集合*/ 11 fd_set open_fds_init; /*文件描述符的初值集合*/ 12 struct file * fd_array[32];/* 文件对象指针的初始化数组*/ 13 };
当开始使用一个文件当对象时调用内核提供的 fget()函数。这个函数接收文件描述符fd作为参数,返回在current->files->fd[fd]中的地址,即对应文件对象的地址,如果没有任何文件与fd对应,则返回 NULL。在第1种情况下,fget()使文件对象引用计数器 f_count的值增1。
当内核完成对文件对象的使用时,调用内核提供的fput()函数。该函数将文件对象的地址作为参数,并递减文件对象引用计数器f_count的值,另外,如果这个域变为NULL,该函数就调用文件操作的“释放”方法(如果已定义),释放相应的目录项对象,并递减对应索引节点对象的i_writeaccess域的值(如果该文件是写打开),最后,将该文件对象从“正在使用”链表移到“未使用”链表。
<3> 关于文件系统信息的fs_struct结构
fs_struct结构在include/linux/fs_struct.h中定义如下:
1 struct fs_struct { 2 atomic_t count; 3 rwlock_t lock; 4 int umask; 5 struct dentry * root, * pwd, * altroot; 6 struct vfsmount * rootmnt, * pwdmnt, * altrootmnt; 7 };
5、主要结构之间的关系
超级块是对一个文件系统的描述;索引节点是对一个文件物理属性的描述;目录项是对一个文件逻辑属性的描述。一个进程所处的位置是由fs_struct来描述的,一个进程(或用户)打开的文件是由file_struct来描述的,而整个系统所打开的文件是由file结构描述的。图2表示不同数据结构之间的关系:
图2 与进程联系的文件结构的关系示意图
6、有关操作的数据结构
<1> 超级块操作
超级块操作使用super_operations数据结构来描述的,该结构的其实地址存放在超级块的s_op域中,该结构定义与fs.h中:
1 struct super_operations { 2 void (*read_inode) (struct inode *); 3 void (*read_inode2) (struct inode *, void *) ; 4 void (*dirty_inode) (struct inode *); 5 void (*write_inode) (struct inode *, int); 6 void (*put_inode) (struct inode *); 7 void (*delete_inode) (struct inode *); 8 void (*put_super) (struct super_block *); 9 void (*write_super) (struct super_block *); 10 void (*write_super_lockfs) (struct super_block *); 11 void (*unlockfs) (struct super_block *); 12 int (*statfs) (struct super_block *, struct statfs *); 13 int (*remount_fs) (struct super_block *, int *, char *); 14 void (*clear_inode) (struct inode *); 15 void (*umount_begin) (struct super_block *); 16 };
<2> 索引节点操作
索引节点操作是由inode_operations结构来描述的,主要是用来将VFS对索引节点的操作转化为具体文件系统处理相应操作的函数,在fs.h中描述如下:
1 struct inode_operations { 2 int (*create) (struct inode *,struct dentry *,int); 3 struct dentry * (*lookup) (struct inode *,struct dentry *); 4 int (*link) (struct dentry *,struct inode *,struct dentry *); 5 int (*unlink) (struct inode *,struct dentry *); 6 int (*symlink) (struct inode *,struct dentry *,const char *); 7 int (*mkdir) (struct inode *,struct dentry *,int); 8 int (*rmdir) (struct inode *,struct dentry *); 9 int (*mknod) (struct inode *,struct dentry *,int,int); 10 int (*rename) (struct inode *, struct dentry *, 11 struct inode *, struct dentry *); 12 int (*readlink) (struct dentry *, char *,int); 13 int (*follow_link) (struct dentry *, struct nameidata *); 14 void (*truncate) (struct inode *); 15 int (*permission) (struct inode *, int); 16 int (*revalidate) (struct dentry *); 17 int (*setattr) (struct dentry *, struct iattr *); 18 int (*getattr) (struct dentry *, struct iattr *); 19 };
<3> 目录项操作
目录项操作是由dentry_operations数据结构来描述的,定义在include/linux/dcache.h中:
1 struct dentry_operations { 2 int (*d_revalidate)(struct dentry *, int); 3 int (*d_hash) (struct dentry *, struct qstr *); 4 int (*d_compare) (struct dentry *, struct qstr *, struct qstr *); 5 int (*d_delete)(struct dentry *); 6 void (*d_release)(struct dentry *); 7 void (*d_iput)(struct dentry *, struct inode *); 8 };
<4> 文件操作
文件操作是由file_operations结构来描述的,定义在fs.h中:
1 struct file_operations { 2 struct module *owner; 3 loff_t (*llseek) (struct file *, loff_t, int); 4 ssize_t (*read) (struct file *, char *, size_t, loff_t *); 5 ssize_t (*write) (struct file *, const char *, size_t, loff_t *); 6 int (*readdir) (struct file *, void *, filldir_t); 7 unsigned int (*poll) (struct file *, struct poll_table_struct *); 8 int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); 9 int (*mmap) (struct file *, struct vm_area_struct *); 10 int (*open) (struct inode *, struct file *); 11 int (*flush) (struct file *); 12 int (*release) (struct inode *, struct file *); 13 int (*fsync) (struct file *, struct dentry *, int datasync); 14 int (*fasync) (int, struct file *, int); 15 int (*lock) (struct file *, int, struct file_lock *); 16 ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); 17 ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); 18 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int); 19 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, 20 unsigned long, unsigned long); 21 };
这个数据结构就是连接VFS文件操作与具体文件系统的文件操作之间的枢纽,也是编写设备驱动程序的重要接口。
高速缓存
1、块高速缓存
Linux支持的文件系统大多以块的形式组织文件,为了减少对物理块设备的访问,在文件以块的形式调入内存后,使用块高速缓存(buffer_cache)对它们进行管理。每个缓冲区由两部分组成,第一部分称为缓冲区首部,用数据结构buffer_head表示,第二部分是真正的缓冲区内容。由于缓冲区首部不与数据区域相连,数据区域独立存储,因而在缓冲区首部中,有一个指向数据的指针和一个缓冲区长度的字段。图3为缓冲区格式:
缓冲区首部包含以下内容:
(1)用于描述缓冲区内容的信息,包括:所在设备号、起始物理块号、包含在缓冲区中的字节数;
(2)缓冲区状态的域:是否有游泳数据、是否正在使用、重新利用之前是否要写回磁盘等;
(3)用于管理的域。
图3 缓冲区格式
buffer_head数据结构在include/linux/fs.h中定义如下:
1 struct buffer_head { 2 /* First cache line: */ 3 struct buffer_head *b_next; /* 哈希队列链表*/ 4 unsigned long b_blocknr; /* 逻辑块号 */ 5 unsigned short b_size; /* 块大小 */ 6 unsigned short b_list; /* 本缓冲区所出现的链表 */ 7 kdev_t b_dev; /* 虚拟设备标示符(B_FREE = free) */ 8 atomic_t b_count; /* 块引用计数器 */ 9 kdev_t b_rdev; /* 实际设备标识符*/ 10 unsigned long b_state; /* 缓冲区状态位图 */ 11 unsigned long b_flushtime; /* 对脏缓冲区进行刷新的时间*/ 12 13 struct buffer_head *b_next_free;/* 指向 lru/free 链表中的下一个元素 */ 14 struct buffer_head *b_prev_free;/* 指向链表中的上一个元素*/ 15 struct buffer_head *b_this_page;/* 每个页面中的缓冲区链表*/ 16 struct buffer_head *b_reqnext; /*请求队列 */ 17 struct buffer_head **b_pprev; /* 哈希队列的双向链表 */ 18 char * b_data; /* 指向数据块 */ 19 struct page *b_page; /* 这个 bh 所映射的页面*/ 20 void (*b_end_io)(struct buffer_head *bh, int uptodate); /* I/O 结束方法*/ 21 void *b_private; /* 给 b_end_io 保留 */ 22 unsigned long b_rsector; /* 缓冲区在磁盘上的实际位置*/ 23 wait_queue_head_t b_wait; /* 缓冲区等待队列 */ 24 struct inode * b_inode; 25 struct list_head b_inode_buffers; /* inode 脏缓冲区的循环链表*/ 26 };
其中缓冲区状态在fs.h中定义为枚举类型:
enum bh_state_bits { BH_Uptodate, /* 如果缓冲区包含有效数据则置 1 */ BH_Dirty, /* 如果缓冲区数据被改变则置 1 */ BH_Lock, /* 如果缓冲区被锁定则置 1*/ BH_Req, /* 如果缓冲区数据无效则置 0 */ BH_Mapped, /* 如果缓冲区有一个磁盘映射则置 1 */ BH_New, /* 如果缓冲区为新且还没有被写出则置 1 */ BH_Async, /* 如果缓冲区是进行 end_buffer_io_async I/O 同步则置 1 */ BH_Wait_IO, /* 如果我们应该把这个缓冲区写出则置 1 */ BH_launder, /* 如果我们应该“清洗”这个缓冲区则置 1 */ BH_JBD, /* 如果与 journal_head 相连接则置 1 */ BH_PrivateStart,/* 这不是一个状态位,但是,第 1 位由其他实体用于私有分配*/ }
buffer_head结构中的b_count就可以反映的b_count就可以反映出缓冲区是否处于使用状态,如果它为0,则缓冲区是空闲的,大于0 ,则缓冲区正被进程访问。
缓冲区的大小不是固定的,当前Linux支持5中大小的缓冲区,分别是512、1024、2048、4096和8192字节。Linux所支持的文件系统都是用共同的块高速缓冲,在同一时刻,块高速缓冲中存在着来自不同物理设备的数据块,为了支持这些不同大小的数据块,Linux使用几种不同大小的缓冲区。
当前Linux缓冲区有3种类型,在include/linux/fs.h中定义如下:
#define BUF_CLEAN 0 /*未使用的、干净的缓冲区*/ #define BUF_LOCKED 1 /*被锁定的缓冲区,正等待写入*/ #define BUF_DIRTY 2 /*脏的缓冲区,其中有有效数据,需要写回磁盘*/
VFS使用了多个链表来管理块高速缓存中的缓冲区。
首先,对于包含了有效数据的缓冲区,用一个哈希表来管理,用hash_table来指向这个哈希表。哈希索引值由数据块号以及其所在的设备标示号计算(散列)得到。
对于每一种类型的未使用的有效缓冲区,系统还使用一个LRU(最近最少使用)双链表管理,即lru-list链。由于共有3种类型的缓冲区,所以有3个这样的LRU链表。当需要访问某个数据块时,系统采取如下算法。
(1)首先,根据数据块号和所在设备号在块高速缓存中查找,如果找到,则将它的b_count域加1,因为这个域正是反映了当前使用这个缓冲区的进程数。如果这个缓冲区同时又处于某个LRU链中,则将它从LRU链中解开。
(2)如果数据块还没有调入缓冲区,则系统必须进行磁盘I/O操作,将数据块调入块高速缓存,同时将空缓冲区分配一个给它。如果块高速缓存已满(即没有空缓冲区可供分配),则从某个LRU链首取下一个,先看是否置了“脏”位,如已置,则将它的内容写回磁盘。然后清空内容,将它分配给新的数据块。
(3)在缓冲区使用完了后,将它的b_count域减1,如果b_count变为0,则将它放在某个LRU 链尾,表示该缓冲区已可以重新利用。
• 对于每一种大小的空闲缓冲区,系统使用一个链表管理,即free_list链。
• 对于空缓冲区,系统使用一个unused_list链管理。
Linux中,用bdflush守护进程完成对块高速缓存的一般管理。bdflush守护进程是一个简单的内核线程,在系统启动时运行,它在系统中注册的进程名称为kflush,作用是见识块高速缓存中的“脏”缓冲区,在分配或丢弃缓冲区时,将对“脏”缓冲区数目作一个统计。通常情况下,该进程将处于休眠状态,当高速缓存中“脏”缓冲区的数据达到一定的比例时,该进程将被唤醒。当系统急需时,可在任何时刻唤醒该进程。当有数据写入缓冲区使之变成“脏”时,所有的“脏”缓冲区被连接到一个BUF_DIRTY_LRU链表中,bdflush 会将适当数目的缓冲区中的数据块写到磁盘上。
2、索引节点高速缓存
• 全局哈希表 inode_hashtable,其中哈希值是根据每个超级块指针的值和 32 位索引节点号而得;
• 正在使用的索引节点链表。全局变量 inode_in_use 指向该链表中的首元素和尾元素。函数 get_empty_inode()获得一个空节点,get_new_inode()获得一个新节点,通过这两个函数新分配的索引节点就加入到这个链表中;
• 未用索引节点链表。全局变量inode_unused的next域 和prev域分别指向该链表中的首元素和尾元素;
• 脏索引节点链表。由相应超级块的s_dirty域指向该链表中的首元素和尾元素。
• 对inode对象的缓存,定义如下:static kmem_cache_t * inode_cachep这是一个Slab缓存,用于分配和释放索引节点对象。
索引节点的i_hash域指向哈希表,i_list指向in_use、unused或dirty某个链表。所有这些链表都受单个自旋锁inode_lock的保护,其定义如下:static spinlock_t inode_lock = SPIN_LOCK_UNLOCKED;
索引节点高速缓存的初始化是由 inode_init()实现的,而这个函数是在系统启动时由init/main.c 中的 start_kernel()函数调用的。
3、目录高速缓存
每个目录项对象属于以下4种状态之一:
• 空闲状态:处于该状态的目录项对象不包含有效的信息,还没有被VFS使用。它对应的内存区由slab分配器进行管理。
• 未使用状态:处于该状态的目录项对象当前还没有被内核使用。该对象的引用计数器d_count 的值为NULL。
• 正在使用状态:处于该状态的目录项对象当前正在被内核使用。该对象的引用计数器d_count的值为正数,而其d_inode 域指向相关的索引节点对象。该目录项对象包含有效的信息,并且不能被丢弃。
• 负状态:与目录项相关的索引节点不复存在,那是因为相应的磁盘索引节点已被删除。该目录项对象的d_inode域置为NULL,但该对象仍然被保存在目录项高速缓存中,以便后续对同一文件目录名的查找操作能够快速完成。
Linux 使用目录项高速缓存,它由以下两种类型的数据结构组成。
• 处于正在使用、未使用或负状态的目录项对象的集合。
• 一个哈希表,从中能够快速获取与给定的文件名和目录名对应的目录项对象。如果访问的对象不在目录项高速缓存中,哈希函数返回一个空值。
目录项高速缓存的作用也相当于索引节点高速缓存的控制器。
所有“未使用” 目录项对象都存放在一个“最近最少使用”的双向链表中,该链表按照插入的时间排序。
每个“正在使用”的目录项对象都被插入一个双向链表中,该链表由相应索引节点对象的i_dentry域所指向。目录项对象的d_alias域存放链表中相邻元素的地址。
高速缓存
1、文件系统的注册
当内核被编译时,就已经确定了可以支持哪些文件系统,这些文件系统在系统引导时,在VFS中进行注册。每个文件系统都有一个初始化例程,它的作用就是在VFS中进行注册,即填写一个叫做file_system_type的数据结构。该结构包含了文件系统的名称一集一个指向对应的VFS超级块读取例程的地址,所有已注册的文件系统的文件系统的file_system_type结构形成一个链表。file_system_type的数据结构在fs.h中定义如下:
1 struct file_system_type { 2 const char *name; 3 int fs_flags; 4 struct super_block *(*read_super) (struct super_block *, void *, int); 5 struct module *owner; 6 struct file_system_type * next; 7 struct list_head fs_supers; 8 };
2、文件系统的安装
在系统启动后看到的文件系统,都是在启动时安装的,如果需要自己(超级用户)安装文件系统,需要指定3种信息:文件系统的名称、包含文件系统的物理块设备以及文件系统在已有文件系统中的安装点。例如:$ mount -t iso9660 /dev/hdc /mnt/cdrom。
把一个文件系统(或设备)安装到一个目录点时要要用到的主要数据结构为vfsmount,在include/linux/mount.h中定义如下:
1 struct vfsmount 2 { 3 struct list_head mnt_hash; 4 struct vfsmount *mnt_parent; /* fs we are mounted on */ 5 struct dentry *mnt_mountpoint; /* dentry of mountpoint */ 6 struct dentry *mnt_root; /* root of the mounted tree */ 7 struct super_block *mnt_sb; /* pointer to superblock */ 8 struct list_head mnt_mounts; /* list of children, anchored here */ 9 struct list_head mnt_child; /* and going through their mnt_child */ 10 atomic_t mnt_count; 11 int mnt_flags; 12 char *mnt_devname; /* Name of device e.g. /dev/dsk/hda1 */ 13 struct list_head mnt_list; 14 };
<1> 安装根文件系统
当系统启动时,,就要在变量 ROOT_DEV 中寻找包含根文件系统的磁盘主码。当编译内核或向最初的启动装入程序传递一个合适的选项时,根文件系统可以被指定为/dev 目录下的一或向最初的启动装入程序传递一个合适的选项时,根文件系统可以被指定为/dev 目录下的一或向最初的启动装入程序传递一个合适的选项时,根文件系统可以被指定为/dev 目录下的一个设备文件。类似地,根文件系统的安装标志存放在 root_mountflags 变量中。用户可以指定这些标志,这是通过对已编译的内核映像执行/sbin/rdev 外部程序,或者向最初的启动装入程序传递一个合适的选项来达到的。根文件系统的安装函数为 mount_root()。
<2> 安装常规文件系统
安装完根文件系统之后就可以安装常规文件系统,每个系统都可以安装在系统目录树中的一个目录中,在用户程序中要安装一个文件系统则可以调用mount()系统调用,在内核中的实现函数为sys_mount(),在fs/namespace.c中实现。
3、文件系统的卸载
如果文件系统中的文件当前正在使用,则该文件系统是不能被卸载的。如果文件系统中的文件或目录正在使用,则VFS索引节点高速缓存中是否有来自该文件系统的VFS索引节点,如果有且使用计数大于0,则说明该文件系统正在被使用,因此该文件系统不能被卸载。否则,查看对应的VFS超级块,如果该文件系统的VFS超级块标志为“脏”,则必须将超级块信息写回磁盘。上述过程结束之后,对应的VFS超级块被释放,vfsmount数据结构将从vfsmntlist链表中断开并被释放,其实现函数为sys_umount()函数,在fs/super.c中实现。