2023-06-11
原文作者:奇小葩 原文地址:https://blog.csdn.net/u012489236/category_10976532.html

最近一直忙于基于android的动态分区的方案和virtual A/B方案通过rust重写升级的框架,里面有将IO子系统的框架学习了,主要是基于deviceMapper的用法和实现原理,只是学习了基本的原理和如何使用,对于其最底下的IO层基本不知道如何实现,准备放到文件IO章节学习。本周末接着学习文件系统,之前了解了以下文件系统的基本知识,本次开始真正进入到内核的虚拟文件系统学习之旅。

1 VFS架构

我们从一个简单的问题开始,我们如何访问文件?作为一个普通的使用者,我们不太关系我们如何访问计算机上的数据,不管这个数据是存储在机械硬盘上,还是SSD,甚至是远程的设备上;其次,我们也不想关心,我们现在用的是基于windows的ntfs还是Linux的ext4,不关心我该如何去挂载。正是通过设计,linux的设计者需要把我们的大脑从思考数据存储在哪里以及如何存储这样的细节中解放出来,Linux设计了一个文件系统的中间层,上层用户都直接和VFS打交道,文件系统开发者再把VFS转换为自己的格式。这样做的优点主要有:

  • 用户层应用不用关心具体用的是什么文件系统, 使用统一的标准接口进行文件操作,例如read,write
  • 如果一个系统包含不同分区,不同分区使用不同的文件系统,他们之间可以通过这个VFS交互,比如从U盘、网盘拷数据到硬盘就得通过VFS转换管理信息
  • • 可以动态支持很多文件系统,添加一个只需要安装驱动就可以了,不需要内核重新编译。

要先有个大视角,让我们站在万米高空,鸟瞰我们的虚拟文件系统。虚拟文件系统的主要功能,是实现 多种文件系统操作接口的统一 ,既能让上层的调用者使用 同一套接口 与底层的 各种文件系统交互 ,又能对文件系统提供一个标准接口,使Linux系统能同时支持多种文件系统。虚拟文件系统与上层应用以及底层的各种文件系统之间的关系如图1所示。

202306111257546901.png

202306111257555372.png

我们不能总是从高空去俯瞰虚拟文件系统,其中包括有很多细节,如下图所示

202306111257561323.png

202306111257569444.png

了解所有的细节会导致文档比较长,所以我们开始分解VFS并浏览所涉及的主要数据结构,VFS采用面向对象的设计,主要包括超级块、索引节点、目录项、文件四个结构体对象。由之前的理论学习,我们知道

  • 超级块: 存储文件系统的控制信息。描述文件系统的状态、文件系统类型、大小、区块数、索引节点数等,存放于磁盘的特定扇区中。
  • 索引节点: 存储文件的元数据信息。描述文件的大小、拥有者、创建时间、访问模式、磁盘mapping等。
  • 目录: 就是一个文件,存放的数据就是目录信息,所以操作文件的接口完全可以用在目录上。
  • 目录项: 在一个文件路径中,路径的每一部分都被称为目录项;如径/home/source/helloworld.c中,目录 home, source和文件 helloworld.c都是一个目录项。
  • 文件: 一组在逻辑上具有完整意义的信息项的系列。除了普通文件,其他诸如目录、设备、套接字等 也以文件被对待。

2 文件系统类型

若要内核能够识别、挂载某一类型的文件系统,内核必须知道文件系统类型,定义在include/linux/fs.h 中

    struct file_system_type {
    	const char *name;                              //文件系统类型名称,唯一标识一种文件系统
    	int fs_flags;                                  //文件系统类型标志
    #define FS_REQUIRES_DEV		1                      //文件系统保存在外部块设备中
    #define FS_BINARY_MOUNTDATA	2  
    #define FS_HAS_SUBTYPE		4        
    #define FS_USERNS_MOUNT		8	/* Can be mounted by userns root */
    #define FS_RENAME_DOES_D_MOVE	32768	/* FS will handle d_move() during rename() internally. */
    	struct dentry *(*mount) (struct file_system_type *, int,
    		       const char *, void *);                //挂载函数
    	void (*kill_sb) (struct super_block *);        //删去超级块实例函数,在卸载文件时调用
    	struct module *owner;                          //模块指针
    	struct file_system_type * next;                //单链表成员,指向下一个文件系统类型实例
    	struct hlist_head fs_supers;                   //散列链表,链接已挂载相同类型文件系统的超级块实例
    
    	struct lock_class_key s_lock_key;              //没有选择LOCKDEP配置选项为空结构体
    	struct lock_class_key s_umount_key;
    	struct lock_class_key s_vfs_rename_key;
    	struct lock_class_key s_writers_key[SB_FREEZE_LEVELS];
    
    	struct lock_class_key i_lock_key;
    	struct lock_class_key i_mutex_key;
    	struct lock_class_key i_mutex_dir_key;
    };

file_system_type结构体内主要成员简介如下:

  • mount() :文件系统类型定义的挂载函数,第一个参数为指向文件系统类型的指针,第二个参数为挂载标记,第三个参数为文件系统所在块设备文件名称字符串,第四个参数为文件系统私有数据指针。主要完成超级块super_block结构体、文件系统根目录项dentry和节点inode结构体实例的创建和初始化
  • fs_supers :散列链表头,链接内核挂载的同类型文件系统的超级块实例。内核可以挂载同类型的多个文件系统,例如:硬盘中有两个分区被格式化成ext2文件系统,将两个分区都挂载到内核根文件系统后,具有两个ext2文件系统的超级块实例,它们被链接到ext2文件系统类型实例的fs_supers链表中。
  • next: 指向下一个file_system_type实例,所有注册的file_system_type实例在内核中组成单链表。

202306111257576695.png

内核中所有注册的文件系统类型file_system_type实例由单链表管理,表头为file_systems,定义在/fs/filesystem.c文件内,注册file_system_type实例就是将其插入到单链表。

    static struct file_system_type *file_systems;
    static DEFINE_RWLOCK(file_systems_lock);

文件系统类型实例的操作函数简列如下(fs/filesystem.c):

  • register_filesystem(struct file_system_type * fs) :向内核注册文件系统类型实例,函数只是简单地将file_system_type实例添加到file_systems单链表。
  • unregister_filesystem(struct file_system_type * fs) :将file_system_type实例从全局单链表中移出。
  • ** struct file_system_type get_fs_type(const char name): 由文件系统类型名称字符串查找结构体实例。

各文件系统类型的实现代码位于/fs/目录下,每个类型对应一个子目录。例如:ext2文件系统类型的代码位于/fs/ext2/目录下。文件系统类型的初始化函数在内核初始化子系统或加载模块时调用,主要完成文件系统类型的注册,私数据结构的创建、初始化等工作

202306111257582246.png

可以通过以下的命令,查看内核支持的文件系统类型

202306111257587647.png

3 超级块

内核中每挂载文件系统需要创建超级块super_block结构体实例,super_block结构体表示挂载文件系统的整体信息,例如:文件系统类型、数据块的大小核数量等。

通常块设备(分区)中文件系统的开头分出一个指定大小的区域,用于保存此分区文件系统的信息,此区域对应虚拟文件系统中定义的超级块。在挂载文件系统时,内核从此区域中读取信息,填充至超级块super_block实例中,超级块在include/linux/fs.hs中定义

    struct super_block {
    	struct list_head	s_list;		               //将实例链接到全局双链表super_blocks
    	dev_t			s_dev;		                       //所在块设备(分区)设备号   
    	unsigned char		s_blocksize_bits;          //数据块大小以2的底取对数    
    	unsigned long		s_blocksize;               //数据块大小,字节数
    	loff_t			s_maxbytes;	                   //最大文件长度,字节数
    	struct file_system_type	*s_type;           //指向文件系统类型实例
    	const struct super_operations	*s_op;       //超级块操作结构指针
    	const struct dquot_operations	*dq_op;      //用户磁盘配额管理
    	const struct quotactl_ops	*s_qcop;         
    	const struct export_operations *s_export_op;
    	unsigned long		s_flags;                 
    	unsigned long		s_iflags;	/* internal SB_I_* flags */
    	unsigned long		s_magic;                  //魔数,内核为每种文件系统类型分配唯一的标识数字
    	struct dentry		*s_root;                  //指向文件系统根目录项dentry实例
    	struct rw_semaphore	s_umount;
    	int			s_count;
    	atomic_t		s_active;
    #ifdef CONFIG_SECURITY
    	void                    *s_security;
    #endif
    	const struct xattr_handler **s_xattr;     //扩展属性处理函数
    	const struct fscrypt_operations	*s_cop;   //
    
    	struct hlist_bl_head	s_anon;		/* anonymous dentries for (nfs) exporting */
    	struct list_head	s_mounts;	       //挂载mount结构体实例链表,一个分区可以执行多个挂载操作
    	struct block_device	*s_bdev;       //块设备(分区)对应块设备数据结构指针
    	struct backing_dev_info *s_bdi;    //后备存储设备信息
    	struct mtd_info		*s_mtd;          //MTD设备信息
    	struct hlist_node	s_instances;     //散列表节点,链入文件系统类型散列链表,表头fs_supers
    	unsigned int		s_quota_types;	/* Bitmask of supported quota types */
    	struct quota_info	s_dquot;	/* Diskquota specific options */
    
    	struct sb_writers	s_writers;
    
    	char s_id[32];				/* Informational name */
    	u8 s_uuid[16];				/* UUID */
    
    	void 			*s_fs_info;	            //具体文件系统私有数据指针
    	unsigned int		s_max_links;
    	fmode_t			s_mode;
    
    	/* Granularity of c/m/atime in ns.
    	   Cannot be worse than a second */
    	u32		   s_time_gran;
    
    	/*
    	 * The next field is for VFS *only*. No filesystems have any business
    	 * even looking at it. You had been warned.
    	 */
    	struct mutex s_vfs_rename_mutex;	/* Kludge */
    
    	/*
    	 * Filesystem subtype.  If non-empty the filesystem type field
    	 * in /proc/mounts will be "type.subtype"
    	 */
    	char *s_subtype;
    
    	/*
    	 * Saved mount options for lazy filesystems using
    	 * generic_show_options()
    	 */
    	char __rcu *s_options;
    	const struct dentry_operations *s_d_op;    //赋予所有目录项dentry实例d_op成员
    
    	/*
    	 * Saved pool identifier for cleancache (-1 means none)
    	 */
    	int cleancache_poolid;
    
    	struct shrinker s_shrink;	                //slab缓存收缩器,用于页面回收机制
    	/* Number of inodes with nlink == 0 but still referenced */
    	atomic_long_t s_remove_count;
    
    	/* Being remounted read-only */
    	int s_readonly_remount;
    
    	/* AIO completions deferred from interrupt context */
    	struct workqueue_struct *s_dio_done_wq;   //直接读写操作工作队列
    	struct hlist_head s_pins;                 //散列链表头
    
    	/*
    	 * Owning user namespace and default context in which to
    	 * interpret filesystem uids, gids, quotas, device nodes,
    	 * xattrs and security labels.
    	 */
    	struct user_namespace *s_user_ns;
    
    	/*
    	 * Keep the lru lists last in the structure so they always sit on their
    	 * own individual cachelines.
    	 */
    	struct list_lru		s_dentry_lru ____cacheline_aligned_in_smp;   //dentry实例LRU链表
    	struct list_lru		s_inode_lru ____cacheline_aligned_in_smp;    //inode实例LRU链表
    	struct rcu_head		rcu;
    	struct work_struct	destroy_work;
    
    	struct mutex		s_sync_lock;	/* sync serialisation lock */
    
    	/*
    	 * Indicates how deep in a filesystem stack this SB is
    	 */
    	int s_stack_depth;
    
    	/* s_inode_list_lock protects s_inodes */
    	spinlock_t		s_inode_list_lock ____cacheline_aligned_in_smp;
    	struct list_head	s_inodes;	/* all inodes */
    
    	spinlock_t		s_inode_wblist_lock;
    	struct list_head	s_inodes_wb;	/* writeback inodes */
    }

super_block结构体中主要成员简介如下:

  • s_list: 双链表成员,将实例链接到全局双链表super_blocks。
  • s_blocksize,s_blocksize_bits: 表示分区(块设备)数据块的大小,即读写块设备的数据单位,s_blocksize表示数据块大小,字节数,s_blocksize_bits表示数据块大小字节数以2为底取对数,即2s_blocksize_bits=s_blocksize。在对块设备进行读写操作时需要此数据,数据块大小来源于块设备的请求队列,在挂载文件系统创建超级块实例之后,从请求队列中获取此数值。
  • s_op: 超级块操作super_operations结构体指针
  • s_flags :标记成员,取值定义在/include/uapi/linux/fs.h头文件内,表示整个文件系统的属性,比如我们常见的只读,忽略suid和sgid位,禁止访问设备文件等,主要是挂载的文件属性
  • s_root: 指向挂载文件系统根目录dentry实例
  • s_inodes: 双链表成员,链接文件系统打开文件的inode实例
  • s_mounts: 双链表成员,链接挂载mount结构体实例,文件系统可以有多个挂载点,每个mount结构体实例表示一次挂载操作

super_block结构体中包含两个虚拟文件系统的接口数据结构,dentry_operations和super_operations。

  • super_operations结构体表示超级块操作结构,其中包含对整个文件系统的操作、控制函数指针,如:分配inode、销毁inode、同步文件(文件内容写入块设备)等。
    struct super_operations {
      struct inode *(*alloc_inode)(struct super_block *s  b);        //在给定的超级块下创建和初始化一个新的索引节点对象
    	void (*destroy_inode)(struct inode *);                       //销毁alloc_inode创建实例,释放给定的索引节点对象           
        /* VFS在索引节点被修改(脏)时会调用此函数。日志文件系统(如ext3/ext4)执行该函数进行日志更新 */
        void (*dirty_inode) (struct inode *, int flags);            //标记inode脏,表示inode需要回写
        /* 用于将给定的索引节点写入磁盘,wbc表示写入时的控制信息 */
    	int (*write_inode) (struct inode *, struct writeback_control *wbc); //回写inode元数据
    	/* 最后一个索引节点的引用被释放后,VFS调用该函数 */
    	int (*drop_inode) (struct inode *);               //释放inode时调用   
    	/* 用于从磁盘上删除给定的索引节点对象 */                                       
    	void (*evict_inode) (struct inode *);             //释放inode调用
    	/* 卸载文件系统时由VFS调用,用来释放超级块。s_lock进行保护 */
    	void (*put_super) (struct super_block *); //当虚拟文件系统要释放super_block时调用此函数   
    	/* 使文件系统的数据元与磁盘上的文件系统同步。wait指定操作是否同步。*/     
    	int (*sync_fs)(struct super_block *sb, int wait); //同步文件系统中所有脏inode
    	int (*freeze_super) (struct super_block *);
        /*禁止对文件系统做改变,之后使用给定的超级块更新磁盘上的超级块。LVM(逻辑卷标管理)会调用该函数*/ 
    	int (*freeze_fs) (struct super_block *);//当虚拟文件系统正在锁定一个文件系统并强制其进入一致状态时调用此函数
    	int (*thaw_super) (struct super_block *);
    	int (*unfreeze_fs) (struct super_block *);//当虚拟文件系统解锁文件系统并使其重新可写时调用此函数
    	/*获取目录项对象状态信息*/
    	int (*statfs) (struct dentry *, struct kstatfs *);  //获取文件系统统计量
    	/*指定新的安装选项重新安装文件系统时,VFS调用此函数。s_lock进行保护*/
    	int (*remount_fs) (struct super_block *, int *, char *); //重新挂载文件系统
    	void (*umount_begin) (struct super_block *);        //卸载文件系统前调用
    
    	int (*show_options)(struct seq_file *, struct dentry *);
    	int (*show_devname)(struct seq_file *, struct dentry *);
    	int (*show_path)(struct seq_file *, struct dentry *);
    	int (*show_stats)(struct seq_file *, struct dentry *);
    #ifdef CONFIG_QUOTA //分别用于VFS读写文件系统quota文件
    	ssize_t (*quota_read)(struct super_block *, int, char *, size_t, loff_t);
    	ssize_t (*quota_write)(struct super_block *, int, const char *, size_t, loff_t);
    	struct dquot **(*get_dquots)(struct inode *);
    #endif
    	int (*bdev_try_to_free_page)(struct super_block*, struct page*, gfp_t);
    	long (*nr_cached_objects)(struct super_block *,    //超级块缓存收缩函数调用此函数
    				  struct shrink_control *);
    	long (*free_cached_objects)(struct super_block *,  //用于扫描可释放对象,并释放它们
    				    struct shrink_control *);
    };

每一个类型的文件系统,需要提供这个结构体,例如ext4文件系统

202306111257593328.png

其数据结构的关系图如下图所示

202306111257599939.png

全局super_block链表 :这个链表包含了系统中的所有super_block(不止一种文件系统super_block)。通过一个list_head类型的 全局变量super_blocks 来访问,内核中使用宏实现这个全局变量的初始化:

    static LIST_HEAD(super_blocks);
    static DEFINE_SPINLOCK(sb_lock);
    
    list_add_tail(&s->s_list, &super_blocks);
    	hlist_add_head(&s->s_instances, &type->fs_supers);

当有新的文件系统挂载时,它会通过自身super_block结构中list_head类型的 成员s_list 链接到super_blocks上,形成一个全局的链表

2023061112580105010.png

一个文件系统类型(如ext4),可以有多个super_block实例。举例来说,我的Linux系统中,划分了3个分区分别挂载在/,/home,/boot目录上,挂载的文件系统都是ext4,因此,这3个ext4文件系统实例都有自己的super_block,它们通过super_block中hlist_node类型的 成员s_instances 链接成“ext4的super_block链表”。可以通过file_system_type的 fs_supers成员 来访问:

2023061112580344311.png

4 索引节点

目录项中保存了普通目录、文件的名称等少量信息,主要用于文件的管理,构成文件树状层次结构。文件的具体信息,如读写权限、修改时间、文件内容等,保存在被称为节点的结构中。回顾下上一节介绍的目录结构,在实际的文件系统中,保存文件内容的数据块号等信息不是保存在目录项中,而是保存在被称为节点的结构中,如下图所示

2023061112580454012.png

内存中,每个文件都有一个inode,一切皆文件,其实应该是说一切皆inode。inode保存了文件系统中一个文件的属性描述,比如文件类型,文件权限,属主,文件创建、读取或修改的时间等等。除了内存中的inode,磁盘中也有对应的inode结构,比如ext4文件系统中,内存中的inode是struct ext4_inode_info,而磁盘中的inode是struct ext4_inode_info,今天我们主要分析下内存中的inode结构。

4.1 节点数据结构

对于所有文件系统对象类型,所有文件系统中的每个对象都存在一个 inode,即索引节点的缩写。它在include/linux/fs.h中定义

    struct inode {
    	umode_t			i_mode;                        //文件类型及访问权限           
    	unsigned short		i_opflags;               //进程打开文件的标记
    	kuid_t			i_uid;                         //文件用户主ID  
    	kgid_t			i_gid;                         //文件用户组ID
    	unsigned int		i_flags;                   //表示文件属性
    
    #ifdef CONFIG_FS_POSIX_ACL
    	struct posix_acl	*i_acl;
    	struct posix_acl	*i_default_acl;
    #endif
    
    	const struct inode_operations	*i_op;       //节点操作结构指针
    	struct super_block	*i_sb;                 //文件系统超级块结构指针
    	struct address_space	*i_mapping;          //文件地址空间结构体指针,通常指向i_data成员,指向inode在内存中的pagecache
    
    #ifdef CONFIG_SECURITY
    	void			*i_security;
    #endif
    
    	/* Stat data, not accessed from path walking */
    	unsigned long		i_ino;                    //inode编号,inode节点号 ls -i 可以查看文件inode号
    	/*
    	 * Filesystems may only read i_nlink directly.  They shall use the
    	 * following functions for modification:
    	 *
    	 *    (set|clear|inc|drop)_nlink
    	 *    inode_(inc|dec)_link_count
    	 */
    	union {
    		const unsigned int i_nlink;
    		unsigned int __i_nlink;
    	};
    	dev_t			i_rdev;                      //表示设备文件时,保存设备号
    	loff_t			i_size;                    //文件大小,字节数
    	struct timespec		i_atime;             //最后访问时间
    	struct timespec		i_mtime;             //最后修改时间
    	struct timespec		i_ctime;             //最后修改inode时间
    	spinlock_t		i_lock;	                 //锁定i_blocks, i_bytes, maybe i_size
    	unsigned short          i_bytes;       //块大小,字节数
    	unsigned int		i_blkbits;             //块大小字节数取对数
    	blkcnt_t		i_blocks;                  //文件大小,块大小
    
    #ifdef __NEED_I_SIZE_ORDERED
    	seqcount_t		i_size_seqcount;
    #endif
    
    	/* Misc */
    	unsigned long		i_state;               //状态信息
    	struct rw_semaphore	i_rwsem;           //互斥锁用于保护地址空间
    
    	unsigned long		dirtied_when;	 //inode变脏的时间
    	unsigned long		dirtied_time_when;
      //通过该变量链接到全局inode哈希表inode_hashtable,用于inode的快速查找
    	struct hlist_node	i_hash;              //将实例链入全局散列表
    	struct list_head	i_io_list;	         //将实例链入后备存储设备回写inode链表
    #ifdef CONFIG_CGROUP_WRITEBACK           //组回写
    	struct bdi_writeback	*i_wb;		/* the associated cgroup wb */
    
    	/* foreign inode detection, see wbc_detach_inode() */
    	int			i_wb_frn_winner;
    	u16			i_wb_frn_avg_time;
    	u16			i_wb_frn_history;
    #endif
    	struct list_head	i_lru;		     //将实例链入超级块LRU链表,表头sb.s_inode_lru
    	struct list_head	i_sb_list;     //将实例链入超级块inode链表,表头sb.s_inodes
    	struct list_head	i_wb_list;	/* backing dev writeback list */
    	union {
    		struct hlist_head	i_dentry;     //散列链表头,链接关联的dentry实例
    		struct rcu_head		i_rcu;
    	};
    	u64			i_version;
    	atomic_t		i_count;              //引用计数
    	atomic_t		i_dio_count;
    	atomic_t		i_writecount;
    #ifdef CONFIG_IMA
    	atomic_t		i_readcount; /* struct files open RO */
    #endif
    	const struct file_operations	*i_fop;	  //文件操作结构指针,文件操作的接口
    	struct file_lock_context	*i_flctx;      //文件地址空间实例,缓存文件内容(页缓存)
    	struct address_space	i_data;
    	struct list_head	i_devices;   //inode表示设备文件时,将实例链接到cdev.list或block_device.bd_inodes链表
    	union {
    		struct pipe_inode_info	*i_pipe;   //管道文件
    		struct block_device	*i_bdev;       //指向块设备结构体 block_device
    		struct cdev		*i_cdev;             //指向字符设备结构体cdev
    		char			*i_link;                 //链接文件内容字符串
    		unsigned		i_dir_seq;       
    	};
    
    	__u32			i_generation;
    
    #ifdef CONFIG_FSNOTIFY
    	__u32			i_fsnotify_mask; /* all events this inode cares about */
    	struct hlist_head	i_fsnotify_marks;
    #endif
    
    #if IS_ENABLED(CONFIG_FS_ENCRYPTION)
    	struct fscrypt_info	*i_crypt_info;
    #endif
    
    	void			*i_private;    //指向具体文件系统私有数据
    }

inode结构体主要成员成员简介如下:

  • i_mode :标记文件类型及访问权限。数据类型为umode_t,它是一个16位的无符号整型数,定义在/include/linux/types.h头文件内。各标记位定义如下:

    2023061112580518513.png

  • i_mapping: 文件地址空间address_space结构体指针成员,通常指向i_data成员

  • i_data: 文件地址空间address_space结构体实例,主要用于在基于块设备的文件系统中管理文件系统内容的页缓存和实现文件缓存页数据与块设备的同步。

  • i_op: inode_operations结构体指针,此结构主要用于对目录文件内容的操作,目录项的操作,如查找目录项,创建目录项,删去目录项,文件重命名等

  • i_fop: file_operations结构体,此结构主要用于对文件内容的操作,如读写文件内容等

inode_operations结构体主要包含对目录文件内容操作的函数指针。目录文件的内容是目录项,因此对目录文件内容的操作就是对目录项的操作,例如:在目录下查找指定名称的子目录项、创建新目录项(新文件)、删除目录项、文件重命名等。inode_operations结构体还包含对文件属性的操作函数等。

inode_operations结构体实例由具体文件系统类型实现,在创建inode实例时由具体文件系统类型代码将inode_operations实例指针赋予inode实例i_op成员。inode_operations结构体体定义在/include/linux/fs.h头文件:

    struct inode_operations {
        /*在特定目录中寻找索引节点,该索引节点要对应于dentry中给出的文件名*/
    	struct dentry * (*lookup) (struct inode *,struct dentry *, unsigned int);  //查找子目录 
    	const char * (*get_link) (struct dentry *, struct inode *, struct delayed_call *); //inode表示符号链接,用于查找链接文件
    	int (*permission) (struct inode *, int);  //文件访问权限检查
    	struct posix_acl * (*get_acl)(struct inode *, int);  
        
    	int (*readlink) (struct dentry *, char __user *,int);
        /*VFS通过系统调用create()和open()来调用该函数,从而为dentry对象创建一个新的索引节点*/
    	int (*create) (struct inode *,struct dentry *, umode_t, bool); //创建普通文件
    	/*被系统调用link()调用,用来创建硬连接。名称由最后一个参数指定,连接对象是inode目录中当一个参数(目录项)所代表的文件*/
    	int (*link) (struct dentry *,struct inode *,struct dentry *); //创建链接
    	/*系统调用unlink调用*/
    	int (*unlink) (struct inode *,struct dentry *);
    	/*系统调用symlink,创建符号连接(软连接)*/
    	int (*symlink) (struct inode *,struct dentry *,const char *);
    	/*系统调用mkdir()调用,创建一个新目录*/
    	int (*mkdir) (struct inode *,struct dentry *,umode_t);  //创建目录
    	/*系统调用rmdir()调用,删除一个目录*/
    	int (*rmdir) (struct inode *,struct dentry *);          //删去目录
    	/*系统调用mknod()调用,创建特殊文件(设备文件、命名管道或套接字)*/
    	int (*mknod) (struct inode *,struct dentry *,umode_t,dev_t);//创建设备节点,命名管道等
    	/*VFS调用该函数来移动文件。前两个参数是旧路径,后两个是新路径*/
    	int (*rename) (struct inode *, struct dentry *,  //重命名
    			struct inode *, struct dentry *, unsigned int);
    	int (*setattr) (struct dentry *, struct iattr *);   //用于设置文件属性,在chmod()和相关的系统调用中调用
    	int (*getattr) (struct vfsmount *mnt, struct dentry *, struct kstat *);//用于获取文件属性,在stat()和相关的系统调用中调用
    	ssize_t (*listxattr) (struct dentry *, char *, size_t);//用于列出给定文件所有的扩展属性
    	int (*fiemap)(struct inode *, struct fiemap_extent_info *, u64 start,
    		      u64 len);
    	int (*update_time)(struct inode *, struct timespec *, int); //用于更新特定时间和inode的i_version
    	int (*atomic_open)(struct inode *, struct dentry *,
    			   struct file *, unsigned open_flag,
    			   umode_t create_mode, int *opened);
    	int (*tmpfile) (struct inode *, struct dentry *, umode_t);
    	int (*set_acl)(struct inode *, struct posix_acl *, int);
    } ____cacheline_aligned;

lookup :在inode表示目录文件内容中查找子目录项,并填充dentry实例(已创建dentry实例,实例中包含目录项(文件)名称,用于查找),这是一个非常重要的函数,在此函数内需要创建inode实例并初始化。

file_operations结构体主要包含对文件内容的操作函数指针,是内核操作文件的主要接口,是内核中非常重要的一个数据结构。因为外部设备也被视为文件(设备文件),file_operations结构体也是内核操作外部设备的接口,设备驱动程序的主要工作就是实现file_operations结构体实例。
file_operations结构体与inode_operations结构体一样由具体文件系统类型实现,在打开文件创建inode实例时将结构体实例指针赋予inode实例i_fop成员。file_operations结构体定义在/include/linux/fs.h头文件内,这个我们太熟悉了,就不多做介绍了。

4.2 节点操作

内核为inode结构体建立了slab缓存和全局散列表inode_hashtable,实例还关联到超级块中双链表。inode实例与dentry实例关联,添加到内核根文件系统结构中,文件系统超级块super_block结构体中包含两个inode实例双链表,表头分别是super_block.s_inode_lru(LRU链表)和super_block.s_inodes。

2023061112580646314.png

内核在搜索文件时为每个目录项dentry实例创建关联的inode实例(或关联到现有inode实例),并将inode实例添加到全局散列表和超级块s_inodes双链表中,释放inode实例时,暂时不用的实例将被添加到超级块s_inode_lru双链表中。

    void __init vfs_caches_init(void)
    {
    	//生成names_cache slab缓存,是VFS为保存路径path信息,而临时使用的内存空间缓存
    	//可以通过__getname()/__putname()等函数使用names_cache slab缓存
    	names_cachep = kmem_cache_create("names_cache", PATH_MAX, 0,
    			SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);
    	//为支持对目录项的缓存做准备,生成目录项的slab缓存后,构建散列表进行管理
    	//此外设置系统可用内存不足时回收目录项缓存的方式
    	dcache_init();
    	
    	//对索引节点执行缓存做准备,和初始化目录项一样,生成索引节点的slab缓存,构建
    	//散列表后设置回收方式
    	inode_init();
    	//VFS使用file结构体显示系统打开的文件,为了管理file结构体
    	files_init();
    	files_maxfiles_init();
    	//Linux系统中所有的文件系统通过VFS层向用户显示,被挂载的文件系统通过vfsmount结构体显示,该结构体还显示VFS层
    	//生成vfsmount结构体的mnt_cacha_slab缓存,虚拟系统则初始化sysfs,rootfs
    	mnt_init();
    	//Linux将与系统连接的所有设备显示为文件,这些文件就是设备文件,注册块设备驱动
    	bdev_cache_init();
    	//字符设备初始化
    	chrdev_init();
    }

我们这里主要关注节点初始化,该接口主要是生成索引节点的slab缓存并构建散列表

    void __init inode_init(void)
    {
    	unsigned int loop;
    
    	//生成索引节点结构体inode_cacha_slab缓存
    	inode_cachep = kmem_cache_create("inode_cache",
    					 sizeof(struct inode),
    					 0,
    					 (SLAB_RECLAIM_ACCOUNT|SLAB_PANIC|
    					 SLAB_MEM_SPREAD|SLAB_ACCOUNT),
    					 init_once);
    
    	/* Hash may have been set up in inode_init_early */
    	if (!hashdist)
    		return;
    	//生成散列表管理各索引节点对象并进行初始化
    	inode_hashtable =
    		alloc_large_system_hash("Inode-cache",
    					sizeof(struct hlist_head),
    					ihash_entries,
    					14,
    					0,
    					&i_hash_shift,
    					&i_hash_mask,
    					0,
    					0);
    
    	for (loop = 0; loop < (1U << i_hash_shift); loop++)
    		INIT_HLIST_HEAD(&inode_hashtable[loop]);
    }

5 目录项

从根文件系统某个目录项至另一个目录项的路径,称为目录。例如:/mnt/abc.txt,表示一个目录。而目录中每个分量,如mnt、abc.txt等称目录项。

VFS中目录项是对具体文件系统中的普通目录项和文件目录项的抽象。目录项在虚拟文件系统中由dentry结构体表示,dentry实例构成了根文件系统的树状层次结构。在打开文件时会创建路径中各目录项对应的dentry实例和inode实例。

内核在打开文件时,将在具体文件系统中查找各目录项分量,提取其中信息,创建denty实例,并将实例添加到根文件系统树状层次结构中。dentry结构体定义在/include/linux/dcache.h头文件:

    struct dentry {
    	/* RCU lookup touched fields */
    	unsigned int d_flags;		 //目录项标志
    	seqcount_t d_seq;		     //per dentry seqlock
    	struct hlist_bl_node d_hash;	//散列表节点成员,将实例链表入管理散列表
    	struct dentry *d_parent;	 //指向父目录项
    	struct qstr d_name;        //目录项名称,文件名称
    	struct inode *d_inode;		 //指向表示文件的inode实例
    
    	unsigned char d_iname[DNAME_INLINE_LEN];	//用于存放较短的目录项名称
    
    	/* Ref lookup also touches following */
    	struct lockref d_lockref;	     //锁和引用计数
    	const struct dentry_operations *d_op;  //目录项操作结构指针
    	struct super_block *d_sb;	 //指向所在具体文件系统的超级块实例
    	unsigned long d_time;		/* used by d_revalidate */
    	void *d_fsdata;			 //指向具体文件系统定义的私有数据结构
    
    	union {
    		struct list_head d_lru;		/* LRU list */
    		wait_queue_head_t *d_wait;	/* in-lookup ones only */
    	};
    	struct list_head d_child;     //链接同一父目录项下的兄弟目录项
    	struct list_head d_subdirs;	  //链接子目录项
    	/*
    	 * d_alias and d_rcu can share memory
    	 */
    	union {
    		struct hlist_node d_alias;	 //用于在inode中链接dentry,链接文件具有多个目录项
    		struct hlist_bl_node d_in_lookup_hash;	/* only for in-lookup ones */
    	 	struct rcu_head d_rcu;
    	} d_u;
    };
  • **d_hash:**散列表节点成员,hlist_bl_node结构体实例,用于将dentry实例链入全局散列表
  • **d_parent:**指向父目录项
  • d_child,d_subdirs :双链表成员,d_child用于链接处于同一父目录下的兄弟目录项,d_subdirs表示双链表头,管理其下子目录项
  • d_inode :指向目录项对应的inode实例
  • d_sb :指向目录项所在文件系统的超级块实例,每个挂载的文件系统在虚拟文件系统中具一个super_block结构体实例

dentry_operations结构体包含特定于具体文件系统的目录项操作,须由具体文件系统类型代码实现。dentry结构体中d_op成员指向dentry_operations结构体实例,实例来源于超级块super_block结构体s_d_op成员。在挂载文件系统时,dentry_operations实例由具体文件系统的挂载函数赋予超级块实例,再由超级块实例传递给其下目录项dentry实例。dentry_operations结构体定义在/include/linux/dcache.h头文件内:

    struct dentry_operations {
    	int (*d_revalidate)(struct dentry *, unsigned int);    //用于网络文件系统,检查释放仍然有效
    	int (*d_weak_revalidate)(struct dentry *, unsigned int);
    	int (*d_hash)(const struct dentry *, struct qstr *);    //计算目录项散列值
    	int (*d_compare)(const struct dentry *,                 //比较目录项名称
    			unsigned int, const char *, const struct qstr *);
    	int (*d_delete)(const struct dentry *);                //引用计数小于等于1时调用d_delete()
    	int (*d_init)(struct dentry *);
    	void (*d_release)(struct dentry *);                   //最后销毁目录项前调用d_release()
    	void (*d_prune)(struct dentry *);
    	void (*d_iput)(struct dentry *, struct inode *);      //释放目录项对应的inode实例
    	char *(*d_dname)(struct dentry *, char *, int);       //设置目录项名称
    	struct vfsmount *(*d_automount)(struct path *);
    	int (*d_manage)(struct dentry *, bool);
    	struct dentry *(*d_real)(struct dentry *, const struct inode *,
    				 unsigned int);
    } ____cacheline_aligned;

dentry_operations结构体中函数功能简要说明如下(/Documentation/filesystems/vfs.txt):

  • d_revalidate :目录项是否有效。在根文件系统中查找到dentry实例时,需调用此函数确认目录项是否有效,一般文件系统此函数为空,因为目录项在缓存中就表示目录项有效,此函数主要用于网络文件系统。目录项仍然有效返回正值,否则返回0或负值。

同上面inode的初始化一样,内核在打开文件时,会为各路径分量创建对应的dentry实例,新创建的实例首先添加到根文件系统层次结构中,在为dentry实例创建并关联对应的inode实例时会将其添加到全局散列表dentry_hashtable。当dentry实例被释放时被添加到超级块 LRU链表。在关闭文件时,内核会释放dentry实例。dentry实例管理结构如下图所示:

2023061112580831015.png

6 文件

进程task_struct结构体中fs成员表示了进程可见根文件系统的根节点及当前工作目录:

    task_struct{
            ...
      	struct fs_struct *fs;          /*进程目录信息*/
        struct files_struct *files;   /*进程打开文件信息*/
       ...
    }

fs_struct结构体定义在/include/linux/fs_struct.h头文件:

    struct fs_struct {
    	int users;
    	spinlock_t lock;
    	seqcount_t seq;
    	int umask;
    	int in_exec;
    	struct path root, pwd;
    };

fs_struct结构体主要成员:

  • umask :进程新建文件访问权限掩码,置1的位表示新建文件屏蔽该属性,例如:umask=001,表示屏蔽其它用户对文件的执行权限。
  • root,pwd :表示进程根目录和当前目录信息,path结构体实例,结构体定义如下:
    struct path {
    
    struct vfsmount *mnt;     /*目录项所在文件系统挂载信息,vfsmount.mnt*/
    
    **struct dentry *dentry**;    /*目录项指针*/
    
    };
  • root成员表示进程访问内核根文件系统的起点(顶点),通常为根文件系统的根节点,如下图所示,但也可以通过chroot()系统调用修改进程根目录。进程以绝对路径搜索文件时,从进程根目录开始。
  • pwd成员表示进程当前工作目录。进程以相对路径访问文件时,将会从当前工作目录开始查找。chdir()系统调用用于改变进程当前工作目录。在前面介绍的VFS初始化中,将创建内核根文件系统,并设置内核线程的根目录、当前工作目录为根文件系统根目录。

2023061112580901216.png

内核文件由根文件系统统一管理,进程访问文件前,需要先通过 打开文件操作建立与内核文件的关联 。进程通过**file结构体建立进程与内核文件(inode)**之间的关联,如下图所示。进程task_struct结构体管理着file实例的指针数组(由files_struct结构体表示),通过file建立与内核文件inode之间的关联,数组项索引值即进程文件描述符。进程与内核文件建立关联后,inode指向的file_operations实例将赋予file实例,进程通过此file_operations实例中的函数操作文件。

2023061112580963317.png

files成员指向files_struct结构体实例,结构体定义在/include/linux/fdtable.h头文件

    struct files_struct {
    	atomic_t  count;             /*实例引用计数*/
    	bool  resize_in_progress;      /*进程正在扩展fdtab实例*/
    	wait_queue_head_t  resize_wait;    /*进程等待队列,等待fdtab扩展的进程*/
    
    	struct fdtable  __rcu  *fdt;     /*fdtable结构体指针,初始值指向fdtab成员*/
    	struct fdtable  fdtab;           /*fdtable结构体成员*/
    	spinlock_t  file_lock ____cacheline_aligned_in_smp;
    	int  next_fd;         /*下一个打开文件的文件描述符,初始值为0,每次分配描述符后设置*/
    	unsigned long  close_on_exec_init[1];  /*执行execve()系统调用时关闭文件的位图*/
    	unsigned long  open_fds_init[1];      /*打开文件位图*/
    	struct file __rcu  * fd_array[NR_OPEN_DEFAULT];  /*打开文件file指针数组*/
    };

进程打开的文件由file结构体表示,结构体定义在/include/linux/fs.h头文件:

    struct file {
    	   union {
    		   struct  llist_node	fu_llist;      /*单链表成员*/
    		   struct  rcu_head 	fu_rcuhead;
    	   } f_u;
    	   struct path	 f_path;            /*文件路径信息*/
    	   struct inode	*f_inode;	       /*指向内核文件inode实例*/
    	   const struct file_operations	*f_op;  
    /*文件操作结构指针,通常在打开文件时设为inode->i_fop*/
    	   spinlock_t		f_lock;
    	   atomic_long_t		f_count;
    	   unsigned int 		f_flags;    /*open()系统调用传递的flags标记参数*/
    	   fmode_t			f_mode;    /*标记进程以何种模式打开文件*/
    	   struct mutex		f_pos_lock;
    	   loff_t			    f_pos;      /*文件当前读写位置,相对于文件开头处的字节偏移量*/
    	   struct fown_struct	f_owner;
    	   const struct cred	*f_cred;
    	   struct file_ra_state	f_ra;   /*文件预读结构体,见第11章*/
    	   u64		f_version;
        #ifdef  CONFIG_SECURITY
    	   void		*f_security;
        #endif
    	   void		*private_data;  /*文件私有数据指针,例如设备文件指向驱动程序定义的数据结构*/
    
        #ifdef CONFIG_EPOLL
    	   struct list_head	  f_ep_links;
    	   struct list_head   f_tfile_llink;
    #endif        
       struct address_space	*f_mapping;     /*文件地址空间指针,用于具有外部存储介质的文件*/
    } __attribute__((aligned(4)));

通过以上的数据结构,我们可以得出一个进程访问一个文件的基本流程如下

2023061112581020418.png

7 总结

我们通过虚拟文件系统的重要的数据结构,可以得出虚拟文件系统的结构如下图所示,

  • 每个编译入内核(加载模块)的文件系统类型在内核中由file_system_type结构体实例表示,所有的实例在内核中组成单链表,表头为file_systems。

  • 对于目录项dentry,根文件系统是由目录项dentry结构体实例组成的层次树状结构,用于管理所有内核打开的文件系统及文件。

    • dentry结构体中包含目录项的名称(普通目录项名称或文件名称),并构成父子、兄弟的层次关系。
    • 树状结构中具有唯一的根节点,其名称为“/”。
    • 每个dentry实例关联一个inode结构体实例,
    • inode结构体实例表示目录项对应的文件。不只是表示文件的目录项具有inode实例,表示普通目录项的dentry实例也具有inode实例,因为目录也是文件,只不过目录文件的内容是目录项,即此目录下的子目录项和文件目录项。
  • inode结构体(节点)用于在VFS中唯一地表示一个文件,结构体中包含了文件元信息、缓存文件内容的文件地址空间指针、节点操作结构指针、文件操作结构指针等成员。其中节点操作结构inode_operations和文件操作结构file_operations是两个非常重要的数据结构,主要包含对文件内容执行操作的函数指针,具体函数由具体文件系统类型实现。

  • 具体文件系统(硬盘、分区中文件系统等)在使用前需要挂载到根文件系统某一目录项下,内核通过此目录项进入此文件系统。挂载操作会创建文件系统的超级块super_block结构体实例,添加到文件系统类型链表中,创建表示此文件系统根目录项的dentry和inode实例。超级块super_block结构体表示整个文件系统的组织结构信息,而不是单个文件的信息

2023061112581141419.png

2023061112581337220.png

8 参考资料

https://www.starlab.io/blog/introduction-to-the-linux-virtual-filesystem-vfs-part-i-a-high-level-tour

linux内核解析

https://www.kernel.org/doc/html/latest/filesystems/vfs.html

阅读全文