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.

Discover more from sketch2sky

Subscribe now to keep reading and get access to the full archive.

Continue reading