EXT4 inode 分配策略

| 分类 linux  | 标签 linux 

前言

EXT4文件系统的inode是非常多的,按照Inodes Per Block Group = 4096而言,1T空间能提供出32M个inode左右,当我们创建一个目录,或者创建一个文件时,EXT4会怎么选择inode呢?

在2002年发表的The Orlov block allocator一文中提到,影响文件系统性能的因素有很多,但是文件在磁盘上的分布非常关键。有一个总体的原则:相关的数据寻处位置相近。因为位置相近甚至连续,那么花在磁盘寻道上的时间机会少,从而提高性能。

但是实际情况下,还有一种特殊情况,对于文件系统的顶层目录,尽量要伸展开,分离开,否则顶层目录挤在一起,底层目录更具局部性原理也挤在一起,最终就会导致文件系统,从远到近地线性展开,很多文件与其父目录的位置隔得很远,从而损坏性能。

因此,一个很重要的原则是,顶层目录尽量在整个存储空间铺开,而目录树深处的目录相关的目录尽量靠近。这个原则是不难理解的。Linux采用了一种叫做Orlov allocator的算法来负责选择inode的位置。该算法时由 Grigory Orlov[提出],在EXT2和EXT3 就开始使用该算法,但是EXT4由于支持flex_bg,所以算法上也有了一点点变化或者变异

内核实现

对于EXT4文件系统,无论是创建文件(create),还是创建目录,还是创建软链接,都会牵扯到inode分配(如图所示)。茫茫无数inode,到底选用那一个?

这个__ext4_new_inode是EXT4非常重要的一个函数,它的内容有400行之多,占据了fs/ext4/ialloc.c的四分之一以上的篇幅。本文重点介绍选择inode分配给新的文件或目录的这个流程。

其实我们不难想到,由于EXT4支持flex block group这种战斗小组,选择inode可以分解成3步

  1. which flex block group
  2. which block group
  3. which inode

这些步骤中,第一步决定了目录是分散还是聚集在一起。文件和目录的规则是不同的:

	if (S_ISDIR(mode))
		ret2 = find_group_orlov(sb, dir, &group, mode, qstr);
	else
		ret2 = find_group_other(sb, dir, &group, mode);

从上面代码也不难看出,对于目录和文件,选择group的规则是不同的。对于目录而言,使用find_group_orlov函数来寻找合适的group,该函数使用了前面提到的orlov allocator算法,而对于文件,软链接等则 使用了find_group_other来寻找合适的group。

目录的inode分配

首先介绍目录文件的inode分配,目录inode分配,调用的是find_group_orlov函数。

static int find_group_orlov(struct super_block *sb, struct inode *parent,
			    ext4_group_t *group, umode_t mode,
			    const struct qstr *qstr)
{
	ext4_group_t parent_group = EXT4_I(parent)->i_block_group;
	struct ext4_sb_info *sbi = EXT4_SB(sb);
	/*获取块组的个数,存放到real_ngroups变量中*/
	ext4_group_t real_ngroups = ext4_get_groups_count(sb); 
	int inodes_per_group = EXT4_INODES_PER_GROUP(sb);
	unsigned int freei, avefreei, grp_free;
	ext4_fsblk_t freeb, avefreec;
	unsigned int ndirs;
	int max_dirs, min_inodes;
	ext4_grpblk_t min_clusters;
	ext4_group_t i, grp, g, ngroups;
	struct ext4_group_desc *desc;
	struct orlov_stats stats;
	int flex_size = ext4_flex_bg_size(sbi);
	struct dx_hash_info hinfo;

	ngroups = real_ngroups;
	/*如果文件系统打开了flex_bg选项,就以逻辑块组为单位分配*/
	if (flex_size > 1) {
		ngroups = (real_ngroups + flex_size - 1) >>
			sbi->s_log_groups_per_flex;
		parent_group >>= sbi->s_log_groups_per_flex;
	}

	freei = percpu_counter_read_positive(&sbi->s_freeinodes_counter);
	avefreei = freei / ngroups;
	freeb = EXT4_C2B(sbi,
		percpu_counter_read_positive(&sbi->s_freeclusters_counter));
	avefreec = freeb;
	do_div(avefreec, ngroups);
	ndirs = percpu_counter_read_positive(&sbi->s_dirs_counter);

	if (S_ISDIR(mode) &&
	    ((parent == d_inode(sb->s_root)) ||
	     (ext4_test_inode_flag(parent, EXT4_INODE_TOPDIR)))) {
		int best_ndir = inodes_per_group;
		int ret = -1;

		if (qstr) {
			hinfo.hash_version = DX_HASH_HALF_MD4;
			hinfo.seed = sbi->s_hash_seed;
			ext4fs_dirhash(qstr->name, qstr->len, &hinfo);
			grp = hinfo.hash;
		} else
			grp = prandom_u32();
		parent_group = (unsigned)grp % ngroups;
		for (i = 0; i < ngroups; i++) {
			g = (parent_group + i) % ngroups;
			get_orlov_stats(sb, g, flex_size, &stats);
			if (!stats.free_inodes)
				continue;
			if (stats.used_dirs >= best_ndir)
				continue;
			if (stats.free_inodes < avefreei)
				continue;
			if (stats.free_clusters < avefreec)
				continue;
			grp = g;
			ret = 0;
			best_ndir = stats.used_dirs;
		}
		if (ret)
			goto fallback;
	found_flex_bg:
		if (flex_size == 1) {
			*group = grp;
			return 0;
		}

		/*
		 * We pack inodes at the beginning of the flexgroup's
		 * inode tables.  Block allocation decisions will do
		 * something similar, although regular files will
		 * start at 2nd block group of the flexgroup.  See
		 * ext4_ext_find_goal() and ext4_find_near().
		 */
		grp *= flex_size;
		for (i = 0; i < flex_size; i++) {
			if (grp+i >= real_ngroups)
				break;
			desc = ext4_get_group_desc(sb, grp+i, NULL);
			if (desc && ext4_free_inodes_count(sb, desc)) {
				*group = grp+i;
				return 0;
			}
		}
		goto fallback;
	}

	max_dirs = ndirs / ngroups + inodes_per_group / 16;
	min_inodes = avefreei - inodes_per_group*flex_size / 4;
	if (min_inodes < 1)
		min_inodes = 1;
	min_clusters = avefreec - EXT4_CLUSTERS_PER_GROUP(sb)*flex_size / 4;

	/*
	 * Start looking in the flex group where we last allocated an
	 * inode for this parent directory
	 */
	if (EXT4_I(parent)->i_last_alloc_group != ~0) {
		parent_group = EXT4_I(parent)->i_last_alloc_group;
		if (flex_size > 1)
			parent_group >>= sbi->s_log_groups_per_flex;
	}

	for (i = 0; i < ngroups; i++) {
		grp = (parent_group + i) % ngroups;
		get_orlov_stats(sb, grp, flex_size, &stats);
		if (stats.used_dirs >= max_dirs)
			continue;
		if (stats.free_inodes < min_inodes)
			continue;
		if (stats.free_clusters < min_clusters)
			continue;
		goto found_flex_bg;
	}

fallback:
	ngroups = real_ngroups;
	avefreei = freei / ngroups;
fallback_retry:
	parent_group = EXT4_I(parent)->i_block_group;
	for (i = 0; i < ngroups; i++) {
		grp = (parent_group + i) % ngroups;
		desc = ext4_get_group_desc(sb, grp, NULL);
		if (desc) {
			grp_free = ext4_free_inodes_count(sb, desc);
			if (grp_free && grp_free >= avefreei) {
				*group = grp;
				return 0;
			}
		}
	}

	if (avefreei) {
		/*
		 * The free-inodes counter is approximate, and for really small
		 * filesystems the above test can fail to find any blockgroups
		 */
		avefreei = 0;
		goto fallback_retry;
	}

	return -1;
}

这么一大段代码,概括来讲可以描述为以下情况

  1. 采用spread out 策略让目录散开
  2. 根据局部性原理,让兄弟目录,父子目录尽可能的靠近

当然这两种搜索都可能失败,那么就忘记战斗小组,各个块组地毯式搜索。

OK,我们开始进入代码细节。首先如果文件系统使用了flex_bg就以战斗小组为单位分配,率先需要确定inode所在的战斗小组,即ngroup的值为文件系统中包含的所有逻辑块组的个数,当然了,如果并没有采用flex_bg,就退化成普通的块组。

接下来,对文件系统的总体使用情况做一个评估,我们如果是伸展开的策略,那么我们希望我们的inode分布在一个试用情况低于平均水平的逻辑块组。这样各个逻辑块组使用才能均衡。


	freei = percpu_counter_read_positive(&sbi->s_freeinodes_counter);
	avefreei = freei / ngroups;
	freeb = EXT4_C2B(sbi,
		percpu_counter_read_positive(&sbi->s_freeclusters_counter));
	avefreec = freeb;
	do_div(avefreec, ngroups);
	ndirs = percpu_counter_read_positive(&sbi->s_dirs_counter);
	

接下来的情况是何时采用伸展开的策略,何时采用靠近的策略。

if (S_ISDIR(mode) &&
	    ((parent == d_inode(sb->s_root)) ||
	     (ext4_test_inode_flag(parent, EXT4_INODE_TOPDIR)))) {
		int best_ndir = inodes_per_group;
		int ret = -1;

		if (qstr) {
			hinfo.hash_version = DX_HASH_HALF_MD4;
			hinfo.seed = sbi->s_hash_seed;
			ext4fs_dirhash(qstr->name, qstr->len, &hinfo);
			grp = hinfo.hash;
		} else
			grp = prandom_u32();
		parent_group = (unsigned)grp % ngroups;
		for (i = 0; i < ngroups; i++) {
			g = (parent_group + i) % ngroups;
			get_orlov_stats(sb, g, flex_size, &stats);
			if (!stats.free_inodes)
				continue;
			if (stats.used_dirs >= best_ndir)
				continue;
			if (stats.free_inodes < avefreei)
				continue;
			if (stats.free_clusters < avefreec)
				continue;
			grp = g;
			ret = 0;
			best_ndir = stats.used_dirs;
		}
		if (ret)
			goto fallback;
	found_flex_bg:
		if (flex_size == 1) {
			*group = grp;
			return 0;
		}

首先inode必须为目录文件的inode,除此外,两个条件满足一个,都可以

  • 文件系统的顶层目录
  • 正在创建的目录的父目录设置了 EXT4_INODE_TOPDIR标志位

第一个条件非常好理解,第二个条件是个什么鬼? 我们不妨以/home目录为例,来理解这个EXT4_INODE_TOPDIR. 我们知道,默认情况下,各个用户的家目录位于/home/下,我们可以想象/home/下可能存在以下的目录:

  • bean
  • chandler
  • bruce
  • tony
  • henry

事实上,每个子目录都对应一个用户,这些用户的文件可能并不相关,bean用户登陆之后,一般访问的是/home/bean下的文件,极少会访问/home/tony下的文件,因此,从局部性原理上讲,这下目录对应的inode 更应该散开而不是聚集在一起。而且/home/下的任何一个目录,可能都是一个消耗磁盘空间很多,文件数非常多的节点,从这个角度看,这些目录对应的inode应该尽可能分散在整个存储空间上。但是很不幸,如果/home目录不是某个文件系统的根,即/home不是挂载点,默认策略并不是尽可能的散开。

有没有办法补救,告诉文件系统,这个/home下的子目录采取的策略应该是spread out,而不是聚集在一起。答案就是这个EXT_INODE_TOPDIR标志位。我们可以给/home目录添加上该标志位,后续在/home/下新建目录,EXT4会采取spread out的策略。

那么如何给/home目录添加EXT4_INODE_TOPDIR的标志位呢?

chattr  +T /home

接下来的for循环,遍历所有的逻辑块组,来寻找最合适的,所谓最合适的,不过是目录数要少,已经分配出去的inode也低于平均值。注意此处采用的并不是first-fit的策略,而是采用的是寻找最优的逻辑块组。即目录数最小的那个逻辑块组。

从for (i = 0; i < ngroups; i++) {这个for循环以可以看出,flex_bg是一种优化,如果没有flex_bg,那么EXT4的layout就过于扁平化,如果没有flex_bg,128MB一个块组,1T的文件系统就有8192个块组,现在4T盘到处都是,8T盘也出现的情况下,块组个数就会更多。如果没有flex_bg,那么搜索的代价就有点大。

如果没有设置flex_bg,此时逻辑块组和块组是一个意思,因此就会可以返回了。 如果设置了flex_bg,上述步骤仅仅是找到了逻辑块组,并未找到块组。寻找合适块组就比较简单了,就依次询问,只要存在free inode,就选择该块组,即first-fit的策略。

上面这一大段,讨论的都是spread out策略的情况,下面代码讨论的是尽可能靠近父目录的策略。

   max_dirs = ndirs / ngroups + inodes_per_group / 16;
	min_inodes = avefreei - inodes_per_group*flex_size / 4;
	if (min_inodes < 1)
		min_inodes = 1;
	min_clusters = avefreec - EXT4_CLUSTERS_PER_GROUP(sb)*flex_size / 4;

	/*
	 * Start looking in the flex group where we last allocated an
	 * inode for this parent directory
	 */
	if (EXT4_I(parent)->i_last_alloc_group != ~0) {
		parent_group = EXT4_I(parent)->i_last_alloc_group;
		if (flex_size > 1)
			parent_group >>= sbi->s_log_groups_per_flex;
	}

	for (i = 0; i < ngroups; i++) {
		grp = (parent_group + i) % ngroups;
		get_orlov_stats(sb, grp, flex_size, &stats);
		if (stats.used_dirs >= max_dirs)
			continue;
		if (stats.free_inodes < min_inodes)
			continue;
		if (stats.free_clusters < min_clusters)
			continue;
		goto found_flex_bg;
	}

各个逻辑块组尽可能平衡的目标明显放松了,

    max_dirs = ndirs / ngroups + inodes_per_group / 16;
    min_inodes = avefreei - inodes_per_group*flex_size / 4;

即只要不是太不平衡,就一律选择父目录所在的逻辑块,更确切的说法是,父目录上次分配inode所在的逻辑块组,即记录在 EXT4_I(parent)->i_last_alloc_group;中的值。

本次选择的group会记录在父目录inode的i_last_alloc_group中,当在同一个父目录下创建目录是,i_last_alloc_group就是搜索的起点,因为尽可能均衡的条件放送了,只要不太过分,再次创建的目录很可能和上一次创建的目录在同一个逻辑块组中。

	if (S_ISDIR(mode))
		ret2 = find_group_orlov(sb, dir, &group, mode, qstr);
	else
		ret2 = find_group_other(sb, dir, &group, mode);

got_group:
	EXT4_I(dir)->i_last_alloc_group = group;

找到逻辑块组之后,同样是采用first-fit的策略进行选择块组。

无论是采用spread-out策略还是聚集的策略,都可能失败,如果失败,就一律化整为零,不再以逻辑块组为单位搜索了,而是以细粒度的块组为单位搜索。第一次目标是低于平均值即可,如果这个目标也达不到,只要有free inode即可。

fallback:
	ngroups = real_ngroups;
	avefreei = freei / ngroups;
fallback_retry:
	parent_group = EXT4_I(parent)->i_block_group;
	for (i = 0; i < ngroups; i++) {
		grp = (parent_group + i) % ngroups;
		desc = ext4_get_group_desc(sb, grp, NULL);
		if (desc) {
			grp_free = ext4_free_inodes_count(sb, desc);
			if (grp_free && grp_free >= avefreei) {
				*group = grp;
				return 0;
			}
		}
	}

	if (avefreei) {
		/*
		 * The free-inodes counter is approximate, and for really small
		 * filesystems the above test can fail to find any blockgroups
		 */
		avefreei = 0;
		goto fallback_retry;
	}

其它文件的inode分配

在文件系统中,目录文件有点特殊,它是文件系统的骨架,或者是一颗树的枝干,而普通文件是一颗树的树叶。因此目录文件分配inode,单拎出来,由专门的函数处理。其它文件inode分配由find_group_other函数实现.

在这个函数中,我们主要讲enable flex_bg feature时采用的策略。原因是随着硬盘空间越来越大,flex_bg几乎总是enable的。

static int find_group_other(struct super_block *sb, struct inode *parent,
			    ext4_group_t *group, umode_t mode)
{
	ext4_group_t parent_group = EXT4_I(parent)->i_block_group;
	ext4_group_t i, last, ngroups = ext4_get_groups_count(sb);
	struct ext4_group_desc *desc;
	int flex_size = ext4_flex_bg_size(EXT4_SB(sb));

	/*
	 * Try to place the inode is the same flex group as its
	 * parent.  If we can't find space, use the Orlov algorithm to
	 * find another flex group, and store that information in the
	 * parent directory's inode information so that use that flex
	 * group for future allocations.
	 */
	if (flex_size > 1) {
		int retry = 0;

	try_again:
		parent_group &= ~(flex_size-1);
		last = parent_group + flex_size;
		if (last > ngroups)
			last = ngroups;
		for  (i = parent_group; i < last; i++) {
			desc = ext4_get_group_desc(sb, i, NULL);
			if (desc && ext4_free_inodes_count(sb, desc)) {
				*group = i;
				return 0;
			}
		}
		if (!retry && EXT4_I(parent)->i_last_alloc_group != ~0) {
			retry = 1;
			parent_group = EXT4_I(parent)->i_last_alloc_group;
			goto try_again;
		}
		/*
		 * If this didn't work, use the Orlov search algorithm
		 * to find a new flex group; we pass in the mode to
		 * avoid the topdir algorithms.
		 */
		*group = parent_group + flex_size;
		if (*group > ngroups)
			*group = 0;
		return find_group_orlov(sb, parent, group, mode, NULL);
	}

	/*
	 * Try to place the inode in its parent directory
	 */
	*group = parent_group;
	desc = ext4_get_group_desc(sb, *group, NULL);
	if (desc && ext4_free_inodes_count(sb, desc) &&
	    ext4_free_group_clusters(sb, desc))
		return 0;

	/*
	 * We're going to place this inode in a different blockgroup from its
	 * parent.  We want to cause files in a common directory to all land in
	 * the same blockgroup.  But we want files which are in a different
	 * directory which shares a blockgroup with our parent to land in a
	 * different blockgroup.
	 *
	 * So add our directory's i_ino into the starting point for the hash.
	 */
	*group = (*group + parent->i_ino) % ngroups;

	/*
	 * Use a quadratic hash to find a group with a free inode and some free
	 * blocks.
	 */
	for (i = 1; i < ngroups; i <<= 1) {
		*group += i;
		if (*group >= ngroups)
			*group -= ngroups;
		desc = ext4_get_group_desc(sb, *group, NULL);
		if (desc && ext4_free_inodes_count(sb, desc) &&
		    ext4_free_group_clusters(sb, desc))
			return 0;
	}

	/*
	 * That failed: try linear search for a free inode, even if that group
	 * has no free blocks.
	 */
	*group = parent_group;
	for (i = 0; i < ngroups; i++) {
		if (++*group >= ngroups)
			*group = 0;
		desc = ext4_get_group_desc(sb, *group, NULL);
		if (desc && ext4_free_inodes_count(sb, desc))
			return 0;
	}

	return -1;
}

主旋律是很明确的,就是靠近。注释信息说的很清楚了,即首先在父目录的逻辑块组(flex block groups)中寻找有没有可用的inode:

	   parent_group &= ~(flex_size-1);
		last = parent_group + flex_size;
		if (last > ngroups)
			last = ngroups;
		for  (i = parent_group; i < last; i++) {
			desc = ext4_get_group_desc(sb, i, NULL);
			if (desc && ext4_free_inodes_count(sb, desc)) {
				*group = i;
				return 0;
			}
		}

如果没有找到,去父目录上次分配inode的逻辑块组(flex block groups),在此寻找有没有可以用的inode。

		if (!retry && EXT4_I(parent)->i_last_alloc_group != ~0) {
			retry = 1;
			parent_group = EXT4_I(parent)->i_last_alloc_group;
			goto try_again;
		}

如果还没有,那就只能用 find_group_orlov函数来搜索。注意搜索的时候,会将mode 传入,因为S_ISDIR(mode)总是false,因此,为文件搜索inode的时候,并不会采用展开的策略,还是采用尽可能靠近的策略。

		*group = parent_group + flex_size;
		if (*group > ngroups)
			*group = 0;
		return find_group_orlov(sb, parent, group, mode, NULL);

上一篇     下一篇