linux虚拟文件系统浅析

linux虚拟文件系统浅析

Posted by Albert on December 5, 2020

Linux虚拟文件系统浅析

​ 本文为原创作品,如需转发,请注明原文出处。既然是虚拟文件系统浅析,那么对于十分细节的内容,本文就不再阐述。实际上整个虚拟文件系统,就算是用一本书来讲也不够。本文的目的是和大家一起探讨Linux整个虚拟文件系统的框架,后续可以根据兴趣深入内核源码阅读。本文大量原理基于linux内核版本2.6.11.最后一部分,我会逐步研究最新内核的改动与实现,同样是以patch形式研究:PATCH研究linux内核

前言

​ 本文开始先对对虚拟文件系统的作用进行总结,然后对虚拟文件系统中的几个重要概念(文件、目录、目录项、超级块、软硬链接、文件系统)进行说明,并尽可能的用图片的方式澄清这些概念对应的内核结构体之间的关系。中间我们也会谈到文件系统的概念,以及文件系统创建、安装、以及挂载三者之间的关系。最后基于这些关系,我们讲解linux是如何从VFS一层一层抽丝剥茧找到具体的文件去操作的。

一、为什么需要虚拟文件系统

​ Linux系统的上面的虚拟文件系统是一个十分值得研究的子系统。Linux 中允许众多不同的文件系统共存,如 ext2、 ext3,、vfat、socket 等。通过使用同一套文件 I/O 系统 调用即可对 Linux 中的任意文件进行操作而无需考虑其所在的具体文件系统格式。

1.1 跨文件操作

​ 在Linux中对文件的 操作可以跨文件系统而执行。如下图所示,我们可以使用 cp 命令从 fat 文件系统格式的硬盘拷贝数据到 ext2 文件系统格式的硬盘;而这样的操作涉及到两个不同的文件系统. 上层应用几乎不用关注底层的实现细节。我们只需要使用VFS暴露出来的标准的read、write等接口就可以了,

image-20201205153405682

1.2 一切皆文件

​ “一切皆是文件”是 Unix/Linux 的基本哲学之一。不仅普通的文件,目录、字符设备、块设备、 套接字等在 Unix/Linux 中都是以文件被对待;它们虽然类型不同,但是对其提供的却是同一套操作界面。

image-20201205153830092

1.3 虚拟文件系统

​ 虚拟文件系统 是 Linux 内核中的一个软件层,它暴露统一的接口给用户空间的程序,同时向用户程序屏蔽不同文件系统的差异,所有的具体的文件系统

一来VFS共存,同时也依靠VFS协同工作。

image-20201205154536636

​ 为了能够支持各种实际文件系统,VFS 定义了所有文件系统都支持的基本的、概念上的接口和数据 结构;同时实际文件系统也提供 VFS 所期望的抽象接口和数据结构,将自身的诸如文件、目录等概念在形式 上与VFS的定义保持一致。换句话说,一个实际的文件系统想要被 Linux 支持,就必须提供一个符合VFS标准 的接口,才能与 VFS 协同工作。实际文件系统在统一的接口和数据结构下隐藏了具体的实现细节,所以在VFS 层和内核的其他部分看来,所有文件系统都是相同的。上图显示了VFS在内核中与实际的文件系统的协同关系

​ 下图会更加直观的展示这种关系(图片来自Linux阅码场):

image-20201205154807980

​ 文件系统的设计,类似抽象基类、面向对象的思想。虚函数都必须由底层派生出的实例实现,使用成员函数 file_operations。在Linux里面的文件操作,底层都要实现file_operations,抽象出owner,write,open,release。所以,无论是字符块,还是文件系统的文件,最终操作就必须是file_operations。例如,实现一个字符设备驱动,就是去实现字符驱动的file_operations。vfs_read时就会调用字符设备的file_operations

访问块设备的两种方法

一、访问裸分区

​ 当直接访问裸分区,是通过fs/block_dev.c 中的 file_operations def_blk_fops,也有read、write、open,一切继承到file_operations。

二、是访问文件系统 如果是访问文件系统,就会通过实现 {ext2}_file_operations 来对接VFS对文件的操作。块设备驱动就不需要知道file_operations,无论是裸设备,还是文件系统的file。他们实现的file_operations就是把linux中的各种设备操作方法,hook进 vfs里面的方法。

1.4 虚拟文件系统的作用

​ 说到这里,其实虚拟文件系统的作用已经很清楚了,我在这里也简单做个总结。

  1. 内核中的一个软件层,给程序提供文件系统接口
  2. 内核中的抽象功能,允许不同的文件系统共存
  3. 统一接口,隐藏实际文件系统细节,便于开发
  4. ……

二、 虚拟文件系统中的概念

2.1 文件

​ 一组在逻辑上具有完整意义的信息项的系列。文件、目录、设备、套接字等 也以文件被对待。 “一切皆文件”。

2.1.1 文件对象

​ 文件对象表示进程已打开的文件。用户看到最多的就是它,包含文件对象的使用计数、用户的UID和GID等。它存放了打开文件与进程之间进行交互的信息,这类信息仅当进程访问文件期间存在与内核内存中。

​ 文件对象结构体字段(部分字段省略)

struct file {
	/**
	 * 文件对应的目录项结构。除了用filp->f_dentry->d_inode的方式来访问索引节点结构之外,设备驱动程序的开发者们一般无需关心dentry结构。
	 */
	struct dentry		*f_dentry;
	/**
	 * 含有该文件的已经安装的文件系统。
	 */
	struct vfsmount         *f_vfsmnt;
	/**
	 * 与文件相关的操作。内核在执行open操作时,对这个指针赋值,以后需要处理这些操作时就读取这个指针。
	 * 不能为了方便而保存起来。也就是说,可以在任何需要的时候修改文件的关联操作。即"方法重载"。
	 */
	struct file_operations	*f_op;

	/**
	 * 当前的读写位置。它是一个64位数。如果驱动程序需要知道文件中的当前位置,可以读取这个值但是不要去修改它。
	 * read/write会使用它们接收到的最后那个指针参数来更新这一位置。
	 */
	loff_t			f_pos;
	/**
	 * 用户的UID和GID.
	 */
	unsigned int		f_uid, f_gid;

	/* needed for tty driver, and maybe others */
	/**
	 * open系统调用在调用驱动程序的open方法前将这个指针置为NULL。驱动程序可以将这个字段用于任何目的或者忽略这个字段。
	 * 驱动程序可以用这个字段指向已分配的数据,但是一定要在内核销毁file结构前在release方法中释放内存。
	 * 它是跨系统调用时保存状态的非常有用的资源。
	 */
	void			*private_data;

	/**
	 * 指向文件地址空间的对象。
	 */
	struct address_space	*f_mapping;
};

​ 在内存中, 每个文件都有一个dentry(目录项, dcache)和inode(索引节点,icache)结构,dentry记录着文件名,上级目录等信息,正是它形成了我们所看到的树状结构;而有关该文件的组织和管理的元信息主要存放inode里面,它记录着文件在存储介质上的位置与分布。同时dentry->d_inode指向相应的inode结构。也就是在读写文件的时候我们可以通过struct file结构体找到文件的dentry,然后根据dentry项找到文件的inode. 有了文件的inode. 我们就可以操作具体的文件了(dentry是在打开文件的时候赋值给file结构体的,后面我们会详细讲解这个过程)。

2.2 目录

​ 目录好比一个文件夹,用来容纳相关文件。

2.3 索引节点

2.3.1 索引节点概念

​ 所谓索引节点,在linux中,任何文件都有一个inode对象(VFS中),是虚拟文件系统的核心数据结构(这里的索引节点指的是虚拟文件系统里面的索引节点,和磁盘中的索引节点需要做下区分)。如下图所示:

image-20201205160503069

索引节点是每个文件的元数据:大小、拥有者、创建时间、磁盘位置(索引节点号)等, (文件系统使用bitmap来标记inode是否使用)file_operations里面记录这种类型的文件,包含哪些操作。 inode_operations里面包含如何生成新的inode,根据文件名字如何找到inode,如何mkdir、unlink等操作

2.3.2 典型磁盘文件系统layout

​ 为了更好的讲解应用进程是如何访问到具体的文件的,我们以ext2为例子,说明下用ext2文件系统格式化磁盘后,磁盘的layout. 如下图所示。图中省略了组描述符。

image-20201205161745723

​ super block: 保存在全局的 super block结构中, 描述了整个文件系统在磁盘中的信息

​ inode bitmap:文件系统使用该bitmap来标识,inode table里面的inode是否使用

​ data block bitmap:来表示这些block是否占用,它在改变文件大小、创建、删除等操作时,都会改变

​ inode table : 存放inode表,每一个inode项代表一个文件,里面存放文件的元信息

​ data blocks: 存放文件数据的区域

​ 看到这里,我相信你心中已经想到操作系统是如何去访问文件的了,在2.1中讲文件对象的时候讲到,我们可以通过文件描述符可以找到vfs的inode就可以找到具体文件系统的inode。以及该文件系统的super block信息,然后根据这些信息,我们就可以知道应该具体去操作哪一个数据块了。

​ 关于ext2文件系统在磁盘中的layout请参考:深入解析ext2

​ 关于ext4文件系统在磁盘中的layout请参考:ext4文件系统之裸数据的分析实践ext4 wiki

2.3.3 数据块寻址

​ 这里的inode和前面的虚拟文件系统的inode有一点区别,这个是磁盘上面存放的inode,叫磁盘的索引节点,而前面的inode是操作系统VFS的,当然操作系统中的inode的很多信息,来自与磁盘的inode。以ext2为例:磁盘索引节点的inode有一个i_block字段(如下图所示),有15个元素:每一个元素指向一个数据块

struct ext2_inode {
	/**
	 * 文件类型和访问权限。
	 *		0:未知文件
	 *		1:普通文件
	 *		2:目录
	 *		3:字符设备
	 *		4:块设备
	 *		5:命名管道
	 *		6:套接字
	 *		7:符号链接
	 */
	__le16	i_mode;		/* File mode */
	/**
	 * 拥有者标识符。
	 */
	__le16	i_uid;		/* Low 16 bits of Owner Uid */
	/**
	 * 以字节为单位的文件长度。
	 */
	__le32	i_size;		/* Size in bytes */
	/**
	 * 最后一次访问文件的时间。
	 */
	__le32	i_atime;	/* Access time */
	/**
	 * 索引节点最后改变的时间。
	 */
	__le32	i_ctime;	/* Creation time */
	/**
	 * 文件内容最后修改的时间。
	 */
	__le32	i_mtime;	/* Modification time */
	/**
	 * 文件删除的时间。
	 */
	__le32	i_dtime;	/* Deletion Time */
	/**
	 * 用户组标识符。
	 */
	__le16	i_gid;		/* Low 16 bits of Group Id */
	/**
	 * 硬链接计数。
	 */
	__le16	i_links_count;	/* Links count */
	/**
	 * 文件的数据块数。以512B为单位
	 * 包含已经用数量,以及保留数量。
	 */
	__le32	i_blocks;	/* Blocks count */
	/**
	 * 文件标志。
	 */
	__le32	i_flags;	/* File flags */
	/**
	 * 特定的操作系统信息。
	 */
	union {
		struct {
			__le32  l_i_reserved1;
		} linux1;
		/**
		 * 仅仅HURD使用了此保留值。
		 */
		struct {
			__le32  h_i_translator;
		} hurd1;
		struct {
			__le32  m_i_reserved1;
		} masix1;
	} osd1;				/* OS dependent 1 */
	/**
	 * 指向数据块的指针。
	 * 前12个块是直接数据块。
	 * 第13块是一级间接块号。
	 * 14->二级间接块。
	 * 15->三级间接块。
	 */
	__le32	i_block[EXT2_N_BLOCKS];/* Pointers to blocks */
	/**
	 * 文件版本,用于NFS
	 */
	__le32	i_generation;	/* File version (for NFS) */
	/**
	 * 文件访问控制列表。
	 */
	__le32	i_file_acl;	/* File ACL */
	/**
	 * 目录访问控制列表。
	 */
	__le32	i_dir_acl;	/* Directory ACL */
	/**
	 * 最后一个文件片的地址。
	 */
	__le32	i_faddr;	/* Fragment address */
	/**
	 * 特定的操作系统信息。
	 */
	union {
		struct {
			__u8	l_i_frag;	/* Fragment number */
			__u8	l_i_fsize;	/* Fragment size */
			__u16	i_pad1;
			__le16	l_i_uid_high;	/* these 2 fields    */
			__le16	l_i_gid_high;	/* were reserved2[0] */
			__u32	l_i_reserved2;
		} linux2;
		struct {
			__u8	h_i_frag;	/* Fragment number */
			__u8	h_i_fsize;	/* Fragment size */
			__le16	h_i_mode_high;
			__le16	h_i_uid_high;
			__le16	h_i_gid_high;
			__le32	h_i_author;
		} hurd2;
		struct {
			__u8	m_i_frag;	/* Fragment number */
			__u8	m_i_fsize;	/* Fragment size */
			__u16	m_pad1;
			__u32	m_i_reserved2[2];
		} masix2;
	} osd2;				/* OS dependent 2 */
};

​ 如果一个文件有多个数据块,这些数据块很可能不是连续存放的,应该如何寻址到每个块呢?事实上,每个文件的inode的索引项一共有 15个,从 i_block[0]到 i_block[14],每个索引项占 4字节。前 12个索引项都表示块编号,例如若Blocks[0]字段保存着 24,就表示第 24个块是该文件的数据块,如果块大小是 1 KB,这样可以表示从 0字节到 12 KB的文件。如果剩下的三个索引项 i_block[12]到 i_block[14]也是这么用的,就只能表示最大 15 KB的文件了,这是远远不够的,事实上,剩下的三个索引项都是间接索引。索引项分为如下几种:

0-11:直接块, 每一个元素指向一个文件内容的数据块 12: 间接块, 13: 双重间接块 14: 三重间接块

image-20201205113242555

​ 如上图所示:索引项i_block[12]所指向的块并非数据块,而是称为间接寻址块( Indirect Block),其中存放的都是类似 i_block[0]这种索引项,再由索引项指向数据块。设块大小是 b,那么一个间接寻址块中可以存放 b/4个索引项,指向 b/4个数据块。所以如果把 i_block[0]到 i_block[12]都用上,最多可以表示 b/4+12个数据块,对于块大小是 1 K的情况,最大可表示 268 K的文件。

​ 如上图所示:索引项 i_block[13]指向两级的间接寻址块,总共最多可表示 (b/4)^2 +b/4+12个数据块,对于 1 K的块大小最大可表示 64.26 MB的文件。

​ 索引项 i_block[14]指向三级的间接寻址块,总共最多可表示 (b/4)^3 +(b/4)^2 +b/4+12个数据块,对于 1 K的块大小最大可表示 16.06 GB的文件。

​ 可见,这种寻址方式对于访问不超过 12个数据块的小文件是非常快的,访问文件中的任意数据只需要两次读盘操作,一次读 inode(也就是读索引项)一次读数据块。而访问大文件中的数据则需要最多五次读盘操作: inode、一级间接寻址块、二级间接寻址块、三级间接寻址块、数据块。实际上,磁盘中的 inode(索引节点高速缓存)和数据块(块高速缓存)往往已经被内核缓存了,读大文件的效率也不会太低。

​ 如下图所示:inode描述文件存放在磁盘中的块:

image-20201205163625830

2.3.4 索引节点高速缓存

​ 硬盘里的inode diagram里的数据结构,在内存中会通过slab分配器,组织成 xxx_inode_cache,统计在meminfo的可回收的内存中。 inode表也会记录每一个inode 在硬盘中摆放的位置。这里所说的索引节点高速缓存也就是我们常说的icache.

image-20201205171419138

image-20201205164040357

​ 关于索引节点高速缓存的更加详细的信息,请参考其他资料:inode缓冲区. 在内核中,并不丢弃与未用目录项相关的索引节点,这是由于目录项高速缓存仍在使用它们。因此,这些索引节点对象保存在RAM中,并能够借助相应的目录项快速引用它们。

2.4 目录项对象

​ 文件路径中,每一部分都被称为目录项。/home/jie/vfs.c中,目录 /、home、 jie和文件 vfs.c对应了一个目录项。

​ 注意区分目录项和目录,目录在文件系统里面也是一个文件,但是它是一个特殊的文件,文件的内容是一个inode号和名字的映射表格

image-20201205172146722

目录在硬盘里是一个特殊的文件。目录在硬盘中也对应一个inode,记录文件的名字和inode号。查找一个文件时(/home/vfs.c),根据文件系统的根据根目录和根inode,找到根目录所在硬盘的位置,将根目录的文件读出来,再去做字符串匹配,能够找到 home这个字符串, 于是就再去读home这个目录对应的inode的文件内容,再做字符串匹配,然后就找到了vfs.c 。最后发现vfs.c是一个常规文件,返回他的inode即可。(上一小节在讲解inode的时候说过,通过inode就可以表示整个文件的元信息,数据内容等)

​ 目录文件在磁盘中存放的内容:

image-20201205172655276

2.4.1 目录项高速缓存

​ 目录项不同于inode、目录项对象在磁盘上没有对应的映射。目录项对象存放在名为dentry_cache的slab分配器高速缓存中。由于磁盘中并没有目录项,所以需要动态的构造,当然就要花费时间。完成构造操作后,我们会在内存中保留它。所以你连续多次访问文件会比第一次快。这个保存它的内存才是目录项高速缓存。

管理目录项高速缓存的数据结构有两个:

一个是处于正在使用、未使用或负状态的目录项对象的集合。这用的是双向链表。

一个叫dentry_hashtable的散列表,从中能够快速获取与给定的文件名和目录名对应的目录项对象。

​ 如下代码是初始化目录项对象的代码:

static void __init dcache_init(unsigned long mempages)
{
	int loop;

	/* 
	 * A constructor could be added for stable state like the lists,
	 * but it is probably not worth it because of the cache nature
	 * of the dcache. 
	 */
	dentry_cache = kmem_cache_create("dentry_cache",
					sizeof(struct dentry),
					0,
					SLAB_RECLAIM_ACCOUNT|SLAB_PANIC,
					NULL, NULL);
	
	set_shrinker(DEFAULT_SEEKS, shrink_dcache_memory);

	/* Hash may have been set up in dcache_init_early */
	if (!hashdist)
		return;

	dentry_hashtable =alloc_large_system_hash("Dentry cache",
					sizeof(struct hlist_head),
					dhash_entries,
					13,
					0,
					&d_hash_shift,
					&d_hash_mask,
					0);

	for (loop = 0; loop < (1 << d_hash_shift); loop++)
		INIT_HLIST_HEAD(&dentry_hashtable[loop]);
}
目录项对象我们常常称为dcache. 而前面章节中介绍的inode的缓存叫做icache. 在内核中,并不丢弃与未用目录项相关的索引节点,这是由于目录项高速缓存仍在使用它们。因此,这些索引节点对象保存在RAM中,并能够借助相应的目录项快速引用它们。

2.5 超级块

​ 存储一个已安装的文件系统的控制信息(文件系统的状态、类型、大小、区块数、索引节点数等),代表一个已安装的文件系统;每次一个实际的文件系统被安装时, 内核会从磁盘的特定位置读取一些控制信息来填充内存中的超级块对象。一个安装实例和一个超级块对象一一对应。 超级块通过其结构中的一个域s_type记录它所属的文件系统类型。

image-20201205174353637

2.6 符号链接和硬链接

2.6.1 符号链接

​ 符号链接是linux中是真实存在的实体文件,文件内容指向其他文件。符号链接和文件是不同的inode。

​ 如下图所示:cbw_file和my_file指向不同的inode. 但是cbw_file的文件内容指向了my_file的inode

img

​ 符号链接特性:

  1. 针对目录的软链接,用rm -fr 删除不了目录里的内容
  2. 针对目录的软链接,”cd ..”进的是软链接所在目录的上级目录
  3. 可以对文件执行unlink或rm,但是不能对目录执行unlink

2.6.2 硬链接

​ 硬链接在硬盘中是同一个inode存在,在目录文件中多了一个目录和该inode对应。

image-20201205172655276

硬链接特性
  1. 硬链接不能跨本地文件系统
  2. 硬链接不能针对目录

2.7 文件缓存

​ 关于文件系统的相关的缓存,前面我们讲到了icache以及dcache. 其实还有一个文件内容的缓存叫page cache. 那么他是存放在哪里的呢? 我们下面简单来看一下。当打开一个文件后,内核中会为struct fille建立如下的映射关系:图中描述了struct file、inode、dentry、address_space之间的关系:

​ 我们通过file结构体按照图中箭头指向,可以一步一步找到page cache. 或者是发起IO操作。

file_page_cache

​ 上图中的radix tree里面的内容就是文件的page cache. 其中索引值是需要访问的数据在文件里面的偏移量, 关于radix tree更详细的信息,请参考:: 页高速缓存

​ 其中i_fop(struct file_operations)和a_ops(struct address_space_operations)的关系是, i_fop是hook到虚拟文件系统中的,a_ops完成page cache的访问,包括page cache不存在的时候,发起IO请求等操作。

​ 代码只是列举一下,不感兴趣的直接忽略掉。

struct address_space_operations {
	/**
	 * 写操作(从页写到所有者的磁盘映象)
	 */
	int (*writepage)(struct page *page, struct writeback_control *wbc);
	/**
	 * 读操作(从所有者的磁盘映象读到页)
	 */
	int (*readpage)(struct file *, struct page *);
	/**
	 * 如果对所有者页进行的操作已准备好,则立刻开始I/O数据的传输
	 */
	int (*sync_page)(struct page *);

	/* Write back some dirty pages from this mapping. */
	/**
	 * 把指定数量的所有者脏页写回磁盘
	 */
	int (*writepages)(struct address_space *, struct writeback_control *);

	/* Set a page dirty */
	/**
	 * 把所有者的页设置为脏页
	 */
	int (*set_page_dirty)(struct page *page);

	/**
	 * 从磁盘中读所有者页的链表
	 */
	int (*readpages)(struct file *filp, struct address_space *mapping,
			struct list_head *pages, unsigned nr_pages);

	/*
	 * ext3 requires that a successful prepare_write() call be followed
	 * by a commit_write() call - they must be balanced
	 */
	/**
	 * 为写操作做准备(由磁盘文件系统使用)
	 */
	int (*prepare_write)(struct file *, struct page *, unsigned, unsigned);
	/**
	 * 完成写操作(由磁盘文件系统使用)
	 */
	int (*commit_write)(struct file *, struct page *, unsigned, unsigned);
	/* Unfortunately this kludge is needed for FIBMAP. Don't use it */
	/**
	 * 从文件块索引中获取逻辑块号
	 */
	sector_t (*bmap)(struct address_space *, sector_t);
	/**
	 * 使所有者的页无效(截断文件时用)
	 */
	int (*invalidatepage) (struct page *, unsigned long);
	/**
	 * 由日志文件系统使用,以准备释放页
	 */
	int (*releasepage) (struct page *, int);
	/**
	 * 所有者页的直接I/O传输(绕过页高速缓存)
	 */
	ssize_t (*direct_IO)(int, struct kiocb *, const struct iovec *iov,
			loff_t offset, unsigned long nr_segs);
};

​ 处于ext2文件系统下的文件对a_ops的赋值为:

struct address_space_operations ext2_nobh_aops = {
	.readpage		= ext2_readpage,
	.readpages		= ext2_readpages,
	.writepage		= ext2_writepage,
	.sync_page		= block_sync_page,
	.prepare_write		= ext2_nobh_prepare_write,
	.commit_write		= nobh_commit_write,
	.bmap			= ext2_bmap,
	.direct_IO		= ext2_direct_IO,
	.writepages		= ext2_writepages,
};

​ 读取文件的时候有如下流程:伪代码如下:

f_ops.read
   if  O_DIRECT
       a_ops.direct_IO();
   else
       do_generic_file_read;//如果page cache读到数据,直接返回。否则使用a_ops->readpage发起io请求

​ 这里我们第一次见到了IO , 实际上IO就是从a_ops->readpage或者a_ops->wirtepage函数触发的。 关于IO流程,请参考其他文章。后面我会梳理一下IO流程, 这里不再过多的阐述。

三、 文件系统

​ 关于文件系统的三个易混淆的概念:后续我们逐一对三个概念进行澄清。

创建 以某种方式格式化磁盘的过程就是在其之上建立一个文件系统的过程。创建文件系统时,会在磁盘的特定位置写入 关于该文件系统的控制信息。

注册 向内核报到,声明自己能被内核支持。一般在编译内核的时侯注册;也可以加载模块的方式手动注册。注册过程实 际上是将表示各实际文件系统的数据结构struct file_system_type 实例化。

挂载 也就是我们熟悉的mount操作,将文件系统加入到Linux的根文件系统的目录树结构上;这样文件系统才能被访问。

3.1 文件系统创建

​ 以某种方式格式化磁盘的过程就是在其之上建立一个文件系统的过程。创建文件系统时,会在磁盘的特定位置写入 关于该文件系统的控制信息。用指定文件系统格式话磁盘分区后。磁盘上被格式化的分区如下图所示:

image-20201205200644293

​ 下图是组描述符的缓存示意图:

image-20201205200845047

3.2 文件系统的注册

​ 文件系统注册就是文件系统向内核报到,声明该文件系统能被内核支持,一般是在内核的初始化阶段完成。或者在文件系统内核模块的(KO)初始化函数中完成注册。

如下是ext2文件系统初始化时候的注册:


static int __init init_ext2_fs(void)
{
	int err = init_ext2_xattr();
	if (err)
		return err;
	err = init_inodecache();
	if (err)
		goto out1;
        err = register_filesystem(&ext2_fs_type);//挂到static struct file_system_type *file_systems;为链表头的链表上
	if (err)
		goto out;
	return 0;
out:
	destroy_inodecache();
out1:
	exit_ext2_xattr();
	return err;
}

//ext2文件系统
static struct file_system_type ext2_fs_type = {
	.owner		= THIS_MODULE,
	.name		= "ext2",  //文件系统名字
	.get_sb		= ext2_get_sb, //用于从磁盘中获取超级块并初始化超级快对象
	.kill_sb	= kill_block_super,
	.fs_flags	= FS_REQUIRES_DEV,
};
注册文件系统就是将该文件系统挂载到file_systems链表中,以供后续使用。如下图所示:

register_ext2

关于根文件系统的注册和挂载请参考深入理解linux内核第12章–虚拟文件系统。本篇文章不再过多阐述。

3.3 挂载(安装)文件系统

​ 文件系统加入到Linux的根文件系统的目录树结构上,这样文件系统上面的文件才能被访问。在内核中描述文件系统的文件系统描述符为如下代码块的结构体所示:

struct vfsmount
{
	/**
	 * 用于散列表链表的指针。
	 */
	struct list_head mnt_hash;
	/**
	 * 指向父文件系统,这个文件系统安装在其上。
	 */
	struct vfsmount *mnt_parent;	/* fs we are mounted on */
	/**
	 * 安装点目录节点。
	 */
	struct dentry *mnt_mountpoint;	/* dentry of mountpoint */
	/**
	 * 指向这个文件系统根目录的dentry。
	 */
	struct dentry *mnt_root;	/* root of the mounted tree */
	/**
	 * 该文件系统的超级块对象。
	 */
	struct super_block *mnt_sb;	/* pointer to superblock */
	/**
	 * 包含所有文件系统描述符链表的头
	 */
	struct list_head mnt_mounts;	/* list of children, anchored here */
	/**
	 * 已安装文件系统链表头。通过此字段将其加入父文件系统的mnt_mounts链表中。
	 */
	struct list_head mnt_child;	/* and going through their mnt_child */
	/**
	 * 引用计数器,禁止文件系统被卸载。
	 */
	atomic_t mnt_count;
	/**
	 * mount标志
	 */
	int mnt_flags;
	/**
	 * 如果文件系统标记为过期,就设置这个标志。
	 */
	int mnt_expiry_mark;		/* true if marked for expiry */
	/**
	 * 设备文件名。
	 */
	char *mnt_devname;		/* Name of device e.g. /dev/dsk/hda1 */
	/**
	 * 已安装文件系统描述符的namespace链表指针?
	 * 通过此字段将其加入到namespace的list链表中。
	 */
	struct list_head mnt_list;
	/**
	 * 文件系统到期链表指针。
	 */
	struct list_head mnt_fslink;	/* link in fs-specific expiry list */
	/**
	 * 进程命名空间指针
	 */
	struct namespace *mnt_namespace; /* containing namespace */
};

//命名空间
struct namespace {
	atomic_t		count; //共享该命令空间进程个数
	struct vfsmount *	root;  //命名空间根目录已安装文件系统描述符
	struct list_head	list;  //所有已经安装文件系统的描述符
	struct rw_semaphore	sem;//保护这个结构体的读写信号量
};

​ 一个vfs_mount可以理解为一个文件系统的实例。它有挂载点、有挂载点的dentry项等、同一个文件系统可以有多个vfs_mount实例。也就是同一个文件系统可以被安装多次。例如:/home/jie/test1 、/home/jie/test2、这两个目录可以mount同一种文件系统,假设均为ext2,实际上就是有了ext2文件系统的两个实例。对于每个安装操作(mount), 内存里面都需要保存安装点、安装标记、以及已安装文件系统与其他文件系统之间的关系(是否挂在其他文件系统下等)。

​ 对于每一个 mount 的文件系统,都由一个 vfsmount 结构来表示。对于每一个目录项,都用一个dentry来表示,例如对于/usr/local/lib : / usr local lib 表示4个目录项。例如我们要mount一个设备 /dev/sdb1 到 /home/my目录下,我们假设 /home/my 就是当前进程的根文件系统中的目录(即 home 和 my 都没有mount任何文件系统),命令行是: mount -t ext2 /dev/sdb1 /home/my。我们mount的时候,传入的参数有三个: 要mount的设备( /dev/sdb1 ) , 设备的文件系统 ( ext2 之类的), mount到什么目录 ( /home/my )

​ mount的过程就是把设备的文件系统入到 vfs 框架

  1. 首先,要mount一个新的设备,需要创建一个新的 super block。 这通过要mount的文件系统的 file_system_type, 调用其 get_sb方法来创建一个新的 super block。
  2. 需要创建一个新的vfsmount ,对于任何一个 mount 的文件系统,都要有一个 vfsmount, 创建这个vfsmount, 并设置好vfsmount 中的各个成员
  3. 将创建好的 vfsmount 加入到系统中

对于新的vfsmount:

  1. 其mount_point为目录 “my” 的dentry,
  2. 其mnt_root 是设备sdb1上的根目录的 dentry
  3. 其父 vfsmount 就是原文件系统中的那个 vfsmount

vfs_mount_struct

​ 如上图所示,已安装的文件系统vfsmount存放在如下3个双向循环链表中:

mount_hashtable: 以父文件系统vfsmount地址和安装点目录项地址为索引的散列表

namespace->list: 命名空间的双向链表

vfsmount->mnt_mcounts :所有该vfsmount下挂的子文件系统、mnt_child为vfsmount链接到mnt_mcounts上

3.4 文件系统几个关键数据结构的关系

​ file_system_type描述了一个文件系统、vfs_mount则表示一个文件系统的实例、超级块对象则是描述对应文件系统的磁盘空间信息。他们之间并不是孤立存在的。正是通过它们的有机联系,VFS才能正常工作。如下图是对它们之间的联系的描述。

​ 如下图所示,被Linux支持的文件系统,都有且仅有一个file_system_type结构而不管它有零个或多个实例被安装到系统 中。每安装一个文件系统,就对应有一个超级块安装点超级块通过它的一个域s_type指向其对应的具体的文件系统类型。具体的 文件系统通过file_system_type中的一个域fs_supers链接具有同一种文件类型的超级块。同一种文件系统类型的超级块通过域s_instances链 接。

image-20201205201332898

四、 文件操作

4.1 路径名查找

​ 如下图所示,当你在硬盘查找 /usr/bin/emacs文件时,从根的inode和dentry,根据/的inode表,找到/ 目录文件所在的硬盘中的位置,读硬盘/目录文件的内容,发现 usr 对应inode 2, bin 对应inode 3, share 对应inode4。再去查inode表,inode 2所在硬盘的位置,即/usr 目录文件所在硬盘的位置。读出内容包括 var 对应 inode 10, bin 对应inode 11, opt对应inode 12。于是又去找inode 2所在的磁盘位置,emacs 对应的inode是119. 我们现在就找到了119这个inode,它就对应了 /usr/bin/emacs这个文件。这个过程会查找很多inode和 dentry,这些都会通过 icache 和dcache缓存。

img

​ 前面我们讲了很多文件系统的创建、注册、挂载,那么和我们的路径查找有什么关系呢,关系大着呢。每当我们搜索到目录项对象的时候,如果dentry->d_mounted大于1的话,说明该目录项被文件系统挂载,那么需要调用lookup_mnt()查找vfs_mount. 并切换文件系统,这样就会找到新的文件系统的根dentry项以及对应的inode项。如果没有切换文件系统的,那么是无法继续解析下去的。试想一下,挂载点下面文件都是存放在ext4文件系统下的,文件也是按ext4文件系统去组织的。这个时候你还是按上一个目录的文件系统,比如exfat去解析后面的文件,肯定是解析不了的。

​ 路径名查找完成后,将找到的文件的dentry以及inode对应的f_ops赋值给struct file结构体, 系统调用返回文件描述符给应用成,这个时候我们就可以操作通过fd(文件描述符)号先找到struct file, 顺藤摸瓜,可以找到对应文件的inode。

4.2 读写文件过程

​ 进程通过task_struct中的一个域files_struct files来了解它当前所打开的文件对象;而我们通常所说的文件 描述符其实是进程打开的文件对象数组的索引值。文件对象通过域f_dentry找到它对应的dentry对象,再由dentry对象的域d_inode找 到它对应的索引结点,这样就建立了文件对象与实际的物理文件的关联。最后,还有一点很重要的是, 文件对象所对应的文件操作函数列表是通过索引结点的域i_fop得到的。如下图所示:

图片示例_进程与超级块,文件,索引结点,目录项的关系

​ 如下图所示:通过文件描述符fd我们可以找到具体文件的inode的文件描述符,通过注册的回调函数a_ops以及page cache. 我们就可以实际的读写文件的内容了。file_page_cache

​ 如下图所示:文件系统在磁盘中的layout. 通过磁盘中的inode, 我们可以读写具体的文件,当然读写文件最后会发起IO。 对应文件系统的IO。 后面专门写一篇文章来介绍。

image-20201205201805668

五、 特殊文件系统操作

​ 套接字以及pipe、netlink等文件在磁盘中没有内容,他们的inode只有VFS中的inode. 下面我们以套接字为例,说明linux是如何处理这种特殊的文件系统的。

5.1 套接字的打开socket

入口函数:

SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)

1、sock_create(family, type, protocol, &sock);

1、 协议栈相关的创建 ,其实就是一个creat函数,inet协议栈就是inet_create,如果是netlink就是netlink的creat函数 2、根据protocol注册不同的回调函数,那么就可以使用时udp、tcp、还是stcp了,并分配函数接收的收包和发包函数

image-20201205202217840

2、sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));

image-20201205202329425

5.2 读写套接字

​ 内核建立起了虚拟文件系统和套接字的映射关系后,那么直接调用open、read等函数就可以操作和读写套接字。

image-20201205202427626

​ 对于套接字f_op被初始化为socket_file_ops

file->f_op = SOCK_INODE(sock)->i_fop = &socket_file_ops;

socket_file_ops为如下定义:

static struct file_operations socket_file_ops = {
	.owner =	THIS_MODULE,
	.llseek =	no_llseek,
	.aio_read =	sock_aio_read,
	.aio_write =	sock_aio_write,
	.poll =		sock_poll,
	.unlocked_ioctl = sock_ioctl,
	.mmap =		sock_mmap,
	.open =		sock_no_open,	/* special open code to disallow open via /proc */
	.release =	sock_close,
	.fasync =	sock_fasync,
	.readv =	sock_readv,
	.writev =	sock_writev,
	.sendpage =	sock_sendpage
};

​ 其中sock_aio_readsock_aio_writesock_readvsock_writev等函数会调用到具体协议的读写函数、比如udp就是调用到udp的函数、tcp就调用到tcp的函数,这一切的映射关系是根据套接字建立的时候根据传入的family、type、protocol这几次参数决定的。

六、参考文档

1、《深入理解Linux内核》

2、《Linux 2.6.11》 源码

3、 深入浅析ext2文件系统

七、内核最新实现与变动

写上面的篇幅已经陆陆续续花了我好几个周末了,后续持续更新最新内核的实现。