Linux VFS I

为了实现VFS的设计目的 – 抽象出文件系统的共性,作为抽象,VFS不但打通了上下的通道,还打通了文件系统之间的通道,就像当下流行的”云”,将各种设备(文件系统)和系统联系到一起。在VFS中,有6个关键类:inode,dentry,file,super_block,vfs_mount和file_system_type,整个VFS”大厦”的建造都是围绕这6个”支柱”的。

这几个数据结构的实现都**“include/linux/fs.h”**中,下面这张图简要的描述了本文介绍这几个对象及其关系

super_block

–1326–>s_blocksize_bits;当块大于256byte时,块大小是2的多少次幂,比如块大小是512时,就是9
–1327–>s_blocksize;块的大小,这两个域参见sb_set_blocksize()
–1328–>file_system_type对象指针,该super_block对象表示的具体的文件系统类型,内核使用指针链表管理所有的file_system_type对象,其中有将一个mount一个文件系统回调接口
–1330–>和inode,file,dentry等一样, super_block”类”也封装了自己的操作方法集
–1334–>这个文件系统挂载到根目录时的flag,比如MS_RDONLY表示只读挂载etc。同样定义在fs.h
–1336–>不同文件系统的魔幻数,比如ramfs的RAMFS_MAGIC,ext4的EXT4_SUPER_MAGIC,定义在“include/linux/magic.h”
–1337–>文件系统的根目录,即mount一个目录到目录树时,该文件系统的挂载点在目录树的dentry对象
–1348–>使用NFS时的导出dentry
–1350–>文件系统的block_device对象指针
–1351–>文件系统的backing_dev_info对象指针
–1352–>MTD设备的。。。
–1362–>文件系统私有数据
–1380–>
–1427–>文件系统stack的深度
–1431–>该super_block管理的所有的inode对象链表
–1434–>该super_block管理的所有的回写inode对象链表

实际磁盘的文件都是由文件系统管理的,文件系统挂载到VFS上之后,内核想要从磁盘上获取这个文件,就要按照事前的约定,从磁盘头读取相应文件系统的信息,比如类型,分区,校验码等,叫做文件系统的super block信息,可以看作是整个文件系统的元数据的容器,只有读取到super block,内核才能正确的进行接下来的操作。对于ext4文件系统,super block按照内核struct ext4_super_block的格式存储在相应分区,内核使用struct ext4_super_block描述该super block信息并进一步将其封装到VFS super_block中。

inode

–602–>i_mode;描述文件的类型, 就是linux的7种文件类型(bcd-lsp),用宏表示可能是S_IFBLK, S_IFCHR, S_IFDIR, S_IFREG, S_IFLINK,S_IFSOCK,S_IFFIFO, 参见“include/linux/stat.h”
–603–>i_opflags;可能是IOP_FASTPERM,IOP_LOOKUP,IOP_NOFOLLOW, 参见“include/linux/fs.h”
–606–>i_flags;使用bitmap标记inode标志,可能是S_SYNC, S_NOATIME, S_APPEND, S_IMMUTABLE, S_DEAD, S_NOQUOTA, S_DIRSYNC,…参见“include/linux/fs.h”
–613–>i_op;inode对象本身的操作方法集,包括创建,销毁一个inode等操作
–614–>i_sb;关联的superblock对象,通过这个对象可以找到具体的文件系统???
–615–>i_mapping;表示该inode对应的文件使用的内存页的相关信息,包括radix_tree,使用的锁等等
–634–>i_rdev;对于设备文件的inode,设备号等都在这里保存
–635–>i_size;以byte为单位,该inode表示的内存页大小。
–636–>i_atime;表示最后access的时间
–637–>i_mtime;表示最后modify的时间,modify指修改文件的内容
–638–>i_ctime;表示最后change的时间,change指修改文件属性
–640–>i_bytes;该inode描述的块们中,最后那部分不足一个block的数据的量,以字节为单位,可以参考“stat.c”
–641–>i_blkbits;控制一个block大小,如果该域为9,则block_size = 1 << 9就是512 byte –642–>i_blocks; 该inode描述的block的数量,每一个block当然都是512byte
–652–>dirtied_when; 与该inode对应的内存页们第一次置为dirty的jiffies值
–653–>dirtied_time_when;jiffies没搞清楚和dirtied_when啥区别???
–655–>i_hash;内核使用hash表管理众多inode实例
–656–>list_head i_io_list; 该磁盘的bdi(backing dev IO)线程的inode链表
–665–>list_head i_lru;inode的LRU链表
–666–>list_head i_sb_list;SuperBlock对象的inode链表
–667–>list_head i_wb_list;SuperBlock对象的bdi回写的inode链表
–669–>list_head i_dentry; 指向该inode的dentry链表
–673–>i_count引用计数,当引用计数变为0时,会释放inode实例
–675–>i_writecount写者计数
–679–>i_fop;缺省的文件操作方法,创建设备文件的时候i_fops填充的是def_chr_fops,blk_blk_fops,def_fifo_fops,bad_sock_fops之一,参见创建过程中调用的init_special_inode()
–682–>list_head i_devices;该inode所属的设备,eg,chrdev_open():list_add(&inode->i_devices, &cdev->list);一个设备可以注册多个设备文件,而每一个文件都对应一个inode实例, so,可以多个设备inode对应一个device对象
–683–>union都是用来表示动态信息的,这里表示inode可以描述的特殊文件类型,有pipe,cdev,blk,link .etc, 没有socket,dir和reg都是磁盘文件系统的一部分, inode天然的可以表示,不需要放这里
–679–>inode中指向struct file_operations对象的指针, 即该inode的操作方法集,i_fops最终要赋值给struct file.f_ops(参见do_dentry_open())。当我们open一个文件的时候, 首先执行的就是上述赋值,接下来回调的file.f_ops.open()其实就是inode.i_fops.open()。比如Linux设备管理(二)_从cdev_add说起一文中的字符设备inode通用的chrdev_open(),之后再回调具体的cdev.fops.open(),…
–702–>具体文件系统或设备的私有数据(非标数据)

向下对应具体文件系统的inode,向上提供文件信息,比如ext2 inode 对象描述一个文件在磁盘的位置,相应的VFS inode也根据这些信息被填充。对于一个文件磁盘上的文件hello.c,首先被分割存储在多个扇区,比如ext4文件系统中使用inode存储该hello.c存在的扇区等信息,该inode的结构是标准的,在kernel中使用struct ext4_inode来描述,读上来的ext4 inode信息使用ext4_inode来描述并从中取出相关信息封装到vfs的inode中(没有文件名)和dentry中,注意,**VFS的inode不是对ext4_inode的直接封装或关联指向,而是取出其中的信息重新构造的,其他的文件系统同理。**这个VFS inode是该文件在系统中的唯一标识。

VFS的inode不但抽象了各种磁盘文件系统的inode,还抽象了内存文件系统,eg,procfs,sysfs etc的inode,内核通过在VFS inode.i_fops中注册不同文件系统的操作方法实现VFS inode到具体文件系统操作的转换。Linux中一切皆文件,Linux中一切皆inode,皆 VFS inode。

VFS的inode依照不同的文件类型通常被注册为不同的回调接口,比如,任何一个文件系统的root都是一个dir,其inode注册的回调函数也以lookup等dir相关操作为主,而普通文件的inode则以read/write等IO操作为主。

struct inode *ext4_iget()
{
  	if (S_ISREG(inode->i_mode)) {
		inode->i_op = &ext4_file_inode_operations;
		if (test_opt(inode->i_sb, DAX))
			inode->i_fop = &ext4_dax_file_operations;
		else
			inode->i_fop = &ext4_file_operations;
		ext4_set_aops(inode);
	} else if (S_ISDIR(inode->i_mode)) {
		inode->i_op = &ext4_dir_inode_operations;
		inode->i_fop = &ext4_dir_operations;
	} else if (S_ISLNK(inode->i_mode)) {
		if (ext4_inode_is_fast_symlink(inode)) {
			inode->i_op = &ext4_fast_symlink_inode_operations;
			nd_terminate_link(ei->i_data, inode->i_size,
				sizeof(ei->i_data) - 1);
		} else {
			inode->i_op = &ext4_symlink_inode_operations;
			ext4_set_aops(inode);
		}
	} else if (S_ISCHR(inode->i_mode) || S_ISBLK(inode->i_mode) ||
	      S_ISFIFO(inode->i_mode) || S_ISSOCK(inode->i_mode)) {
		inode->i_op = &ext4_special_inode_operations;
		if (raw_inode->i_block[0])
			init_special_inode(inode, inode->i_mode,
			   old_decode_dev(le32_to_cpu(raw_inode->i_block[0])));
		else
			init_special_inode(inode, inode->i_mode,
			   new_decode_dev(le32_to_cpu(raw_inode->i_block[1])));
	} else if (ino == EXT4_BOOT_LOADER_INO) {
		make_bad_inode(inode);
	} else {
		ret = -EIO;
		EXT4_ERROR_INODE(inode, "bogus i_mode (%o)", inode->i_mode);
		goto bad_inode;
	}
}

file

进程上下文对所操作文件的描述,文件描述符本质就是每个进程的struct file[]的索引。inode是在系统级标识一个文件,与进程无关,但一个文件要被操作,总要在进程进行,特别是,一个文件常常会被多个进程打开,所以显然一个inode实例是不够的,此外,进程管理也需要将一个进程对文件的”独有的”操作与这个”系统级”的inode实例对应,这点inode也是难以办到的,如此便有了进程级的对文件的描述struct file。文件作为进程的一个资源,该struct file也可以在taskt_struct中找到。

dentry

–88–>dentry *parent,父dentry对象指针
–89–>d_name, quick string,协助路径解析,存储的是char *name(不是数组)以及字符串的元数据:长度和hash值
–90–>inode对象指针, 两个dentry对象的这个域指向同一个inode对象,就形成了硬链接
–92–>d_iname, dentry的名字,对于一个dentry, d_name.name == d_iname,则是internal,否则是external,参见swap_names()以及dname_external(),笔者在ubuntu-1604-amd64版本的系统测试,目录名的最大长度是255(要给NUL留一个byte),但是DNAME_INLINE_LEN不是256,这点有待研究。
–96–>操作方法集,包括d_real(),d_init(),d_release()等函数指针
–97–>superblock对象指针,dentry 树的根节点,linux的mount命令可以将一个文件系统挂接到另一个的文件系统的一个目录,这个根节点就是那个被mount的文件系统的superblock对象
–99–>d_fsdata,void* 文件系统相关的数据
–102–>d_lru,dentry们的LRU链表
–105–>dentry对象的同级dentry链表
–106–>dentry对象的子dentry对象,即该”目录”的”子目录”
–111–>inode的别名链表

dentry即directory entry(目录项)。对于各个实际文件系统来说,OS中的”路径”本质还是”文件”,是存储在一些扇区的数据,只要是文件系统的inode信息,kernel都是是通过ext_inode读取上来,并在VFS中创建与之对应的dentry。与inode不同,dentry主要是用于维持inode之间的关系,比如这个目录/文件包含了哪些目录/文件,内核通过dentry之间的关系构造了我们在系统中看到目录树。

“/etc/network/interfaces”中的”/“,”etc”,”network”,”interfaces”都有自己的dentry实例。因为唯一标识系统中的一个文件的inode并不保存文件名,也得益于此
我们才能够创建两个dentry指向同一个inode 的dentry链表来实现硬链接,n个dentry:1个inode就是硬链接的原理。如果路径中出现了符号链接,也会将其转化成真实的路径,而路径中的目录部分,又是一串dentry对象。可以说, 我们看到的文件系统的树状结构,本质就是一个内存中dentry对象的树。

此外, 不论是linux还是windows,出于节约内存的考虑,都不会一次性将整个dentry树都加载到内存,通常只会加载当前目录的前后几层到内存,这就涉及到了另外一个的概念:dentry cache,当我们打开一个文件的时候,首先要做的就是解析路径字符串,找到文件所在目录的dentry对象,这个过程中,首先就是搜索已经在内存中detry cache,如果不在,再从磁盘中继续加载目录文件到内存来搜索,所以,内存中dentry对象及其构成的树,本质上是来自于磁盘中的目录文件及其连接关系。

file_system_type

文件系统类型用于抽象每一种具体的文件系统,向内核注册一个文件系统(注意,不是挂载),即为内核提供对该文件系统的支持,本质上就是将封装好的file_system_type对象通过register_filesystem()注册到VFS,这个注册过程可以进一步解释为: 将file_system_type实例链入全局的file_systems链表中。

register_filesystem(fs)
	p = find_filesystem(fs)
		for(p=&file_systems;*p;p=&(*p)->next)
		strcmp()
		return p
	p = (0 == p? fs:...)

vfs_mount

反映了一个已经挂载到内核目录树的文件系统实例,当我们mount一个分区到挂载点的时候,内核就会构造一个vfs_mount实例并与内核的目录树相连,如此便可无差别的在内核目录树中看到这个文件系统

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.