文件系统原理 和 VFS架构

上一篇:https://blog.csdn.net/weixin_42523774/article/details/103607874
· 本文续接上文,首先解释一下文件系统的基本原理,然后介绍VFS内部的基本框架和主要的数据结构,便于我们进一步深入分析具体文件系统的实现。本文会结合上篇《系统调用的内核实现,一文讲透open函数内核真实实现。》做讲述,便于深入理解代码。

1.文件系统原理

· 装过操作系统的人都知道,在装系统之前有一个磁盘分区的操作,每个磁盘通过最开始的MBR主引导记录)来记录分区信息,每个分区就是一个文件系统,磁盘分区的操作就是将文件系统数据分布结构写入分区。使用mount指令就是将一个带文件系统的存储设备挂载到指定目录。如下图就是磁盘内部实际分布结构:
在这里插入图片描述
·磁盘是以扇区为单位存储的(512字节),由于扇区太小(512字节),直接使用效率低下,因此就设计了更大的逻辑区块Block)作为文件系统的最小单位,通常是2、4、8个扇区的大小,也就是1KB、2KB、4KB。
· SuperBlock是文件系统最开始的Block,用来存储文件系统的大小,空的块,填满的块和各自总数等文件系统关键信息,每个文件系统都会只有一个SuperBlock。但是随着硬盘容量的不断扩大且磁盘分区有限,这就出现了区块群组 (block group),每个区块群组就相当于一个独立的文件系统,都有一个SuperBlock,这就是我们重装系统时创建的逻辑分区,逻辑分区中可以再分区就是通过这种方式实现的。
· 以ext2文件系统为例,文件包括文件内容文件属性,分别放在Blockinode中。ext2文件系统除SuperBlock外,又被分为inode table区 和 Block area区。
· 想深入了解的可以看看《Linux磁盘与文件系统管理》,介绍的非常详细。
· 以上信息都是描述一个具体的文件系统,而VFS是一个虚拟文件系统,他是所有文件系统的抽象,用来管理所有文件系统的。因此VFS也使用了几个类似的概念,super_block、inode、 dentry、file是其中最主要的概念。

2.VFS概述

· 从文章《为何linux一切皆文件》中得到文件解析图如下:
在这里插入图片描述
· 从上图中我们可知,虚拟文件系统是文件操作的入口。linux内核通过虚拟文件系统来管理文件系统。VFS为所有的文件系统提供了统一的接口,这使得用户访问文件系统可以使用同样的系统调用,为此,所有的文件系统也必须要按照VFS定义的方式来实现。
· linux内核如何通过虚拟文件系统来管理文件系统?
(1)具体文件系统需要填充超级块super_block,并注册到一个全局链表中,让内核知道这个文件系统的存在,这是通过函数register_filesystem来实现的;
(2)调用kern_mount函数为具体文件系统申请必备的数据结构
(3)从填充inode的操作函数,比如创建文件、目录等操作;
· 我们会后续会通过yaffs文件系统作为举例来说明这三步是如何具体实现,本文后续将解释VFS使用的几个核心的数据结构。

3.超级块super_block

· 该数据结构位于 include/linux/fs.h 中,由于我们是初学,所以只将一些重要的列出来,见下列代码:

struct super_block {
	unsigned char		s_blocksize_bits; //  指定文件系统的块大小的位数
	unsigned long		s_blocksize;      //  指定文件系统的块大小
	loff_t			s_maxbytes;	          // 最大的文件大小
	struct file_system_type	*s_type;      //  指向file_system_type结构的指针,这个是在 register_filesystem 的时候放到file_systems的。
	const struct super_operations	*s_op;  //超级块操作结构指针 --->
	unsigned long		s_magic;         //  魔术字,每个文件系统都有一个
	struct dentry		*s_root;         //  指向文件系统根dentry的指针
	struct list_head	s_inodes;	     /* 指向文件系统内所有的inode,可以通过其遍历inode对象 */
	struct block_device	*s_bdev;         //  指向文件系统存在的块设备指针 --->
}

超级块代表了整个文件系统本身,因此每个文件系统都有一个super_block结构。重要结构有几个:
· struct file_system_type *s_type; 指向file_system_type结构的指针,这个是在 register_filesystem 的时候放到file_systems的。
· struct dentry *s_root; 指向文件系统根dentry的指针。
· struct list_head s_inodes; 指向文件系统内所有的inode,可以通过其遍历inode对象。
· const struct super_operations *s_op; 超级块操作结构指针,这个是每个文件系统需要各自实现的,由于太过重要,也简单介绍一下:

struct super_operations {
   	struct inode *(*alloc_inode)(struct super_block *sb);  // 为一个新的inode分配内存并初始化
	void (*destroy_inode)(struct inode *);  // 收回一个inode的资源

   	void (*dirty_inode) (struct inode *, int flags);  // 标记一个inode为dirty状态
	int (*write_inode) (struct inode *, struct writeback_control *wbc);  // 写一个inode函数,也标记一个写回结构
	int (*drop_inode) (struct inode *);  //  清除上一次掉线而没释放的自旋锁
	void (*evict_inode) (struct inode *);  // 驱逐一个inode
	void (*put_super) (struct super_block *);  // 准备释放一个super_block
	void (*write_super) (struct super_block *);  // 将super_block写到磁盘中
	int (*sync_fs)(struct super_block *sb, int wait);  // 文件系统同步,就是将所有的dirty inode全写到磁盘中
	int (*freeze_fs) (struct super_block *);  //  锁住文件系统
	int (*unfreeze_fs) (struct super_block *);  // 解锁文件系统
	int (*statfs) (struct dentry *, struct kstatfs *);  // 获取文件系统数据
	int (*remount_fs) (struct super_block *, int *, char *);  // 重新挂载一个文件系统
	void (*umount_begin) (struct super_block *);  // 卸载文件系统的开始时使用
... 省略部分函数
};

· 其中比较重要的就是与inode相关的操作函数,alloc_inode是为一个新的inode分配内存并初始化,destroy_inode是收回一个inode的资源,dirty_inode是标记一个inode为dirty状态(dirty状态表示已经修改了内存中的inode,但还没有写到磁盘中)等。还有与super_block相关的函数以及fs相关的操作见注释。

4. 目录结构dentry

· 在本文第1部分介绍文件系统原理的时候,我们没有看到目录dentry。其实具体文件系统中不存在专门的目录结构。也就是说这个结构只存在于只存在于RAM中,不会存在于磁盘中。但是文件之间关系,在文件系统中是存在的,只是没有专用的结构,使用的和普通的文件一样,都是inode,只是内容不一样。dentry的存在价值是它能提高文件搜索的效率。
· 文件系统挂载的时候,VFS会遍历所有的文件关系,并为其创建dentry结构,这个结构就是上文提到的dentry cache 或叫 dcache
· 当然,由于计算机的RAM大小有限,不一定能存放这么多dentry,所以通常只创建了一部分,所以当在dcache中找不到一个文件的时候,就要通过文件系统的调用去找,这就是上一篇讲open实现中所做的事情。
· 下面介绍dentry结构:

struct dentry {
	/* RCU 搜寻需要的结构 */
	unsigned int d_flags;		/* 被顺序锁保护 */
	seqcount_t d_seq;		/* 每个目录结构都存在一个顺序锁*/
	struct hlist_bl_node d_hash;	/* 搜寻哈希表 */
	struct dentry *d_parent;	/* 父目录指针 */
	
	/* Ref 搜寻模式需要如下结构 */
	unsigned int d_count;		/* 被自旋锁保护 */
	spinlock_t d_lock;		    /* 每个目录结构都存在一个自旋锁*/
	const struct dentry_operations *d_op;  /* 目录操作函数 --->*/
	struct super_block *d_sb;	/* 目录树的根 */
	
	struct list_head d_subdirs;	/* 子目录链表 */
	struct list_head d_alias;	/* inode别名链表 */
};

· 这个结构和上文《系统调用的内核实现,一文讲透open函数内核真实实现。》联系最深刻。
· RCU 搜寻和 Ref 搜寻模式在上文的第3节讲得很细致,其中的保护机制 顺序锁d_seq自旋锁d_lock都在此定义。
· d_subdirs 用来存放子目录结构。
· d_parent 指向该目录的父目录结构。
· 操作函数d_op,我们也简单介绍一下:

struct dentry_operations {
	int (*d_revalidate)(struct dentry *, struct nameidata *);
	int (*d_hash)(const struct dentry *, const struct inode *,
			struct qstr *);
	int (*d_compare)(const struct dentry *, const struct inode *,
			const struct dentry *, const struct inode *,
			unsigned int, const char *, const struct qstr *);
	int (*d_delete)(const struct dentry *);
	void (*d_release)(struct dentry *);
	void (*d_prune)(struct dentry *);
	void (*d_iput)(struct dentry *, struct inode *);
	char *(*d_dname)(struct dentry *, char *, int);
	struct vfsmount *(*d_automount)(struct path *);
	int (*d_manage)(struct dentry *, bool);
} ____cacheline_aligned;

· 这里介绍几个上文中用到了的函数
· d_hash函数,我们在上篇文章的分析中第6节中提到了这个函数,该函数用来将一个dentry加入hash table中。
· d_compare函数,在上文中的第9节介绍了它,用来比较两个dentry的名字是否一致。
· d_revalidate函数 用来让目录重新有效,在dcache中进行文件名检索时总会调用。

5. 节点结构inode

· inode结构代表一个文件,inode保存了文件的重要参数,包括文件的大小,创建时间,操作函数等。通过inode和dentry,才能完整的表示一个文件。
· 下面介绍inode结构,也只介绍重要的部分。

struct inode {
	umode_t			i_mode;  // 文件权限属性,例如777
	unsigned short		i_opflags;
	uid_t			i_uid;  //  文件所属用户ID
	gid_t			i_gid;  //  文件所属用户组ID
	unsigned int		i_flags;

	const struct inode_operations	*i_op;  // 操作函数
	struct super_block	*i_sb;     // 所属文件系统的super_block --->
	struct address_space	*i_mapping;  //  指向文件系统的缓存内容 --->

	struct hlist_node	i_hash; //
	struct list_head	i_wb_list;	/* 用来链接到 dev IO list */
	struct list_head	i_lru;		/* inode LRU list */
	struct list_head	i_sb_list;  //  用来连接到super_block中的链表
	union {
		struct list_head	i_dentry; //用来存放包含该inode的dentry结构,可以由多个目录包含同一文件(链接)。
		struct rcu_head		i_rcu;
	};
	struct file_lock	*i_flock;    // 文件锁
	struct address_space	i_data;  //  

	struct list_head	i_devices;  //  链接到设备链表中
	union {   //这里是指向设备的具体设备文件的指针,可以是管道、块设备或字符设备。
		struct pipe_inode_info	*i_pipe;
		struct block_device	*i_bdev; 
		struct cdev		*i_cdev;
	};
};

· i_mode,i_uid, i_gid 分别表示文件权限属性,所属用户ID 和 文件所属用户组ID,这都是用户用 ls -al 命令可以查看得到的。
· i_mapping 很重要,是指向文件的缓存内容,对于文件内容的读写首先都要在此寻找是否有缓存,没有的话就要去磁盘中提取文件到缓存中再操作。写缓存之后会在合适的时候回写到磁盘中。
· i_wb_list、i_lru、i_dentry、i_devices 这都些链表结构都是链接到与该文件相关的地方,比如i_wb_list链接到设备IO链表,i_dentry 链接到包含该inode的dentry等。
· i_bdev 是指向文件所属设备的指针,大部分文件是属于一个块设备的,所以i_bdev 用的比较多。
· i_op 是inode的操作函数,由于很重要,也展开讲一讲:

struct inode_operations {
	struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
	void * (*follow_link) (struct dentry *, struct nameidata *);
	int (*permission) (struct inode *, int);
	struct posix_acl * (*get_acl)(struct inode *, int);

	int (*readlink) (struct dentry *, char __user *,int);
	void (*put_link) (struct dentry *, struct nameidata *, void *);

	int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
	int (*link) (struct dentry *,struct inode *,struct dentry *);
	int (*unlink) (struct inode *,struct dentry *);
	int (*symlink) (struct inode *,struct dentry *,const char *);
	int (*mkdir) (struct inode *,struct dentry *,int);
	int (*rmdir) (struct inode *,struct dentry *);
	int (*mknod) (struct inode *,struct dentry *,int,dev_t);
	int (*rename) (struct inode *, struct dentry *,
			struct inode *, struct dentry *);
	...skip
} ____cacheline_aligned;

· 看到以上的各个操作函数名,我们知道,很多系统调用中关于目录的部分的最终调用就是这些函数,比如create用来创建文件,link就是创建硬链接文件,mkdir就是创建文件夹,mknod就是创建一个设备节点,rename就是重命名。这是由于目录dentry都会配备一个inode来保存相关信息,这是实际磁盘中存在的。
· lookup函数是用来搜索一个inode,这个我们上文介绍open函数内核实现时分析过。

6.文件对象file

· 文件对象的作用是描述进程和文件的交互关系。这个结构只存在于内存中,进程每打开一个文件,内核都会创建一个file对象。同一个文件在不同的进程中会有不同的文件对象。

struct file {
	struct path		f_path;  //  文件路径,包括目录和挂载点
#define f_dentry	f_path.dentry
#define f_vfsmnt	f_path.mnt
	const struct file_operations	*f_op;  //  文件操作函数,用来操作一个打开了的函数
	spinlock_t		f_lock; // 用来保护f_ep_links, f_flags 和在 lseek SEEK_CUR中的f_pos 和 i_size
	atomic_long_t		f_count;  // 文件操作次数统计
	unsigned int 		f_flags;  // 文件标记,包括O_NONBLOCK,O_CREAT,O_RDONLY, O_WRONLY 等所需标记
	fmode_t			f_mode;  //  文件模式,标记了文件操作权限,比如读,写,执行等
	loff_t			f_pos;  //  指示当前进程对文件的操作位置
	struct file_ra_state	f_ra;  // 用于文件预读的设置
	void			*private_data;  //  串口等设备需要携带特定数据
	struct list_head	f_ep_links; // 在epoll中用来链接与该文件相关的钩子函数
	struct address_space	*f_mapping;  // 指向的结构封装了文件的读写缓存页面
	...skip
};

· f_lock 文件自旋锁,用于保护f_ep_links,f_flags ,lseek 函数中使用的 f_pos 和 i_size。
· f_count 是文件操作次数统计。
· f_mode 是文件模式,标记了文件操作权限,比如读,写,执行等。
· f_ra 用于文件预读的设置,这个后续文章会讲述。
· f_pos 指示当前进程对文件的操作位置。
· f_mapping 指向的结构封装了文件的读写缓存页面。
· f_op 是文件操作函数,操作一个打开了的函数需要这些操作:

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
	int (*readdir) (struct file *, void *, filldir_t);
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);
	ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);
	ssize_t (*sendfile) (struct file *, loff_t *, size_t, read_actor_t, void *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, struct pipe_inode_info *, size_t, unsigned int);
};

· 在上面的操作函数中,我们也能看到很多系统操作的影子。read 和 write就是读写文件的函数,open就是打开文件的函数,其他的一些函数虽然没有用过,但是都和系统调用有关,这也是因为我们文件的操作都是在file结构中。

7.总结

· 通过本篇文章的梳理,终于将VFS基本架构讲清楚了,这篇是后续分析具体文件系统的基础,建议要结合代码重复看,并结合前面几篇文章,对于其他的系统调用的分析,也能信手拈来。
· 下一篇,我们分析具体的yaffs文件系统如何实现对接VFS的,欢迎关注。

如果觉得我的文章还有点收获,就点个赞吧! d=====( ̄▽ ̄*)b

下一篇:https://blog.csdn.net/weixin_42523774/article/details/106914770

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值