注:本文分析基于linux-4.18.0-193.14.2.el8_2内核版本,即CentOS 8.2

1 super_block

super_block,即超级块代表的是一种文件系统类型,比如ext3、ext4都有对应的super_block结构体。一台机器可以有多块硬盘,一个硬盘可以有多个分区,每个分区都有自己的文件系统类型,可以是一样的,也可以不一样。我们来了解下超级块的主要成员变量。

2 super_block主要成员变量

struct super_block {
	struct list_head	s_list;	//通过该变量链接到超级块全局链表super_blocks上
	dev_t			s_dev;		//该文件系统对应的块设备标识符
	unsigned char		s_blocksize_bits;
	unsigned long		s_blocksize; //该文件系统的block size
	loff_t			s_maxbytes;	//文件系统支持的最大文件
	struct file_system_type	*s_type; //文件系统类型,比如ext3、ext4
	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; //文件系统的mount标记
	unsigned long		s_iflags;	/* internal SB_I_* flags */
	unsigned long		s_magic;  //该文件系统类型的魔术字
	struct dentry		*s_root; //全局根目录的dentry项
	...
	struct block_device	*s_bdev;  //对应的块设备
	struct backing_dev_info *s_bdi; //超级块对应的BDI设备
	struct mtd_info		*s_mtd;
	//通过该变量,链接到file_system_type中的fs_supers链表
	struct hlist_node	s_instances;
	...
	char			s_id[32];	/* Informational name */
	uuid_t			s_uuid;		/* UUID tune2fs -l可以查看*/

	void 			*s_fs_info;	//指向具体文件系统超级块结构,如ext4_sb_info
	...
	const struct dentry_operations *s_d_op; //该超级块默认的目录项操作函数
	...
	struct shrinker s_shrink;	//每个超级块注册的shrink函数,用于内存回收
	...
	/* AIO completions deferred from interrupt context */
	struct workqueue_struct *s_dio_done_wq;
	...
	//该超级块对应的未在使用dentry列表
	struct list_lru		s_dentry_lru ____cacheline_aligned_in_smp;
	//该超级块对应的未在使用inode列表
	struct list_lru		s_inode_lru ____cacheline_aligned_in_smp;
	...
	/* s_inode_list_lock protects s_inodes */
	spinlock_t		s_inode_list_lock ____cacheline_aligned_in_smp;
	struct list_head	s_inodes;	//该超级块包含的所有inode

	spinlock_t		s_inode_wblist_lock;
	struct list_head	s_inodes_wb;	//该超级块正在回写的inode
};

而对应的文件系统则由file_system_type结构描述,

struct file_system_type {
	const char *name; //文件系统名称,如ext4,xfs
	...
	struct dentry *(*mount) (struct file_system_type *, int,
		       const char *, void *); //对应的mount函数
	void (*kill_sb) (struct super_block *);
	struct module *owner;
	struct file_system_type * next; //通过该变量将系统上所有文件系统类型链接到一起
	struct hlist_head fs_supers; //该文件系统类型锁包含的超级块对象
	...
};

3 注册一个文件系统

内核为了管理系统上的所有文件系统,创建了一个全局链表file_systems,系统上所有文件系统类型都会挂载在上面,我们可以通过/proc/filesystems查看系统所有的文件系统类型。

下面我们以ext4文件类型,大概了解下注册过程。

static struct file_system_type ext4_fs_type = {
	.owner		= THIS_MODULE,
	.name		= "ext4",
	.mount		= ext4_mount,
	.kill_sb	= kill_block_super,
	.fs_flags	= FS_REQUIRES_DEV,
};

int register_filesystem(struct file_system_type * fs)
{
	int res = 0;
	struct file_system_type ** p;

	BUG_ON(strchr(fs->name, '.'));
	if (fs->next)
		return -EBUSY;
	write_lock(&file_systems_lock);
	//在已注册的全局文件系统链表file_systems中查找,看是否已经注册过
	p = find_filesystem(fs->name, strlen(fs->name));
	if (*p)
		//如果查找到就返回错误,同一个文件系统不能注册两次
		res = -EBUSY;
	else
		//没查找到,直接将该文件系统添加链表末尾
		*p = fs;
	write_unlock(&file_systems_lock);
	return res;
}

static struct file_system_type **find_filesystem(const char *name, unsigned len)
{
	struct file_system_type **p;
	//在已注册的全局文件系统链表file_systems中查找
	for (p = &file_systems; *p; p = &(*p)->next)
		if (strncmp((*p)->name, name, len) == 0 &&
		    !(*p)->name[len])
			break;
	return p;
}

可见,同个类型的文件系统只能注册一次,因此全局文件系统链表file_systems中不可能出现相同的文件系统类型

4 创建一个超级块

我们再以ext4文件系统为例,看下超级块创建的过程。这一些要从文件系统的挂载开始说起,

static struct dentry *ext4_mount(struct file_system_type *fs_type, int flags,
		       const char *dev_name, void *data)
{
	return mount_bdev(fs_type, flags, dev_name, data, ext4_fill_super);
}
			
struct dentry *mount_bdev(struct file_system_type *fs_type,
	int flags, const char *dev_name, void *data,
	int (*fill_super)(struct super_block *, void *, int))
{
	...
	//分配新的超级块
	s = sget(fs_type, test_bdev_super, set_bdev_super, flags | SB_NOSEC,
		 bdev);
	
	if (s->s_root) {
		...
	} else {
		s->s_mode = mode;
		snprintf(s->s_id, sizeof(s->s_id), "%pg", bdev);
		sb_set_blocksize(s, block_size(bdev));
		//对新的超级块进行信息的填充,对于ext4,就是调用ext4_fill_super
		error = fill_super(s, data, flags & SB_SILENT ? 1 : 0);
		...
	}
	...
}

主要就是两点:

  • 分配超级块
  • 填充超级块

我们先看分配超级块,有几点需要考虑:

  • 该设备是否已经mount过,不能重复mount,就像同个类型的文件系统也不能重复注册
  • 分配超级块后需要做一些初始化工作,比如各种链表初始化,以及设置缓存回收函数等
  • 分配完后就要考虑如何管理这个超级块,要加入全局超级块链表,对应的文件类型也要加到全局文件系统列表,同时还要把刚才初始化设置的缓存回收函数注册到全局回收链表
struct super_block *sget(struct file_system_type *type,
			int (*test)(struct super_block *,void *),
			int (*set)(struct super_block *,void *),
			int flags,
			void *data)
{
	...
	return sget_userns(type, test, set, flags, user_ns, data);
}			

//sget_userns -	find or create a superblock
struct super_block *sget_userns(struct file_system_type *type,
			int (*test)(struct super_block *,void *),
			int (*set)(struct super_block *,void *),
			int flags, struct user_namespace *user_ns,
			void *data)
{
	struct super_block *s = NULL;
	struct super_block *old;
	int err;
	...
	//先检查对应文件系统上的fs_supers链表,查看该设备是否已经mount过
	//有mount过,直接返回旧的超级块
	if (test) {
		hlist_for_each_entry(old, &type->fs_supers, s_instances) {
			//通过传入的test_bdev_super判断挂载的超级块对应的设备是否相同
			if (!test(old, data))
				continue;
			...
			return old;
		}
	}
	//没mount过,创建新的超级块
	if (!s) {
		spin_unlock(&sb_lock);
		//创建并初始化超级块
		s = alloc_super(type, (flags & ~SB_SUBMOUNT), user_ns);
		if (!s)
			return ERR_PTR(-ENOMEM);
		goto retry;
	}
	...
	//把对应文件系统类型赋值到超级块上
	s->s_type = type;
	strlcpy(s->s_id, type->name, sizeof(s->s_id));
	//将新的超级块通过s_list挂到全局super_blocks链表上
	list_add_tail(&s->s_list, &super_blocks);
	//把该超级块的文件系统实例挂到对应文件系统的列表头
	//比如有两个分区都是ext4文件系统,那fs_supers上就会挂上两个超级块信息
	hlist_add_head(&s->s_instances, &type->fs_supers);
	spin_unlock(&sb_lock);
	get_filesystem(type);
	//把该超级块的shrink函数挂到全局shrink链表shrinker_list
	//这样系统回收内存时,只要调用shrinker_list的shrink函数就可以了
	register_shrinker_prepared(&s->s_shrink);
	return s;
}

static struct super_block *alloc_super(struct file_system_type *type, int flags,
				       struct user_namespace *user_ns)
{
	...
	//初始化各个链表和锁
	INIT_HLIST_NODE(&s->s_instances);
	INIT_HLIST_BL_HEAD(&s->s_roots);
	mutex_init(&s->s_sync_lock);
	INIT_LIST_HEAD(&s->s_inodes);
	spin_lock_init(&s->s_inode_list_lock);
	INIT_LIST_HEAD(&s->s_inodes_wb);
	spin_lock_init(&s->s_inode_wblist_lock);
	...
	s->s_shrink.seeks = DEFAULT_SEEKS;
	//超级块缓存回收函数
	s->s_shrink.scan_objects = super_cache_scan;
	s->s_shrink.count_objects = super_cache_count;
	s->s_shrink.batch = 1024;
	s->s_shrink.flags = SHRINKER_NUMA_AWARE | SHRINKER_MEMCG_AWARE;
	if (prealloc_shrinker(&s->s_shrink))
		goto fail;
	//初始化存放未使用的dentry链表s_dentry_lru
	if (list_lru_init_memcg(&s->s_dentry_lru, &s->s_shrink))
		goto fail;
	//初始化存放未使用的indode链表s_inode_lru
	if (list_lru_init_memcg(&s->s_inode_lru, &s->s_shrink))
		goto fail;
	return s;

fail:
	destroy_unused_super(s);
	return NULL;
}

回过头,我们再看下超级块的填充,虽然初始化时设置了一些,但是还是有很多和具体文件系统相关的信息没有体现,

  • 针对具体的文件系统,设置对应的超级块信息,super_block是VFS层的体现,具体到实际文件系统,还有对应的超级块
  • 设置该文件系统对应的超级块操作函数,里面包含了对inode资源本身的一些操作集合,比如分配,销毁,写,脏化等操作
  • 调用刚才设置的ext4_alloc_inode函数,为超级块的root分配对应的inode,同样inode也是VFS层的体现,实际ext4分配的是ext4_inode_info,而inode内嵌在该结构的vfs_inode变量中,这样就把VFS和实际文件系统关联在一起
  • 同样需要为超级块分配一个目录项,并关联到对应的inode
static const struct super_operations ext4_sops = {
	.alloc_inode	= ext4_alloc_inode,
	.destroy_inode	= ext4_destroy_inode,
	.write_inode	= ext4_write_inode,
	.dirty_inode	= ext4_dirty_inode,
	...
};

static int ext4_fill_super(struct super_block *sb, void *data, int silent)
{
	struct ext4_sb_info *sbi = kzalloc(sizeof(*sbi), GFP_KERNEL);
	...
	sb->s_fs_info = sbi; //设置实际文件系统的超级块信息
	sbi->s_sb = sb;
	...
	sb->s_op = &ext4_sops; //设置该超级块对应的操作函数,主要是对inode自身的操作
	...
	//调用ext4_alloc_inode分配一个用于root的inode
	root = ext4_iget(sb, EXT4_ROOT_INO, EXT4_IGET_SPECIAL);
	...
	sb->s_root = d_make_root(root); //为root分配目录项
	...
}

5 结构关系

我们用图对以上几个结构进行展示,
在这里插入图片描述

Logo

更多推荐