由于Log-Structured结构,F2FS文件系统的可用空间会快速被消耗,因此需要引入垃圾回收(GC)来回收空间。

本源码阅读主要为个人阅读F2FS GC源码过程中的记录,主要介绍以下内容。

  1. 【当前】介绍f2fs gc的主体流程
  2. 介绍victim segment选择策略(涉及一定的SSR机制相关的内容)
  3. 介绍GC的执行过程

在每部分内容中,会对涉及的关键数据结构进行阅读与记录,并分析函数的执行流程与若干细节。

F2FS GC 主体流程

1. GC线程创建以及GC的触发时机

此部分为后期补充部分,由于这部分源码变动不大,且网上已经有较为详尽的解析,便不再赘述。

相关链接:F2FS-NOTES/F2FS-GC/GC流程介绍.md at master · RiweiPan/F2FS-NOTES · GitHub

2. f2fs_gc 函数(F2FS GC的关键执行函数)

由上面的gc线程函数可知,f2fs_gc函数是F2FS文件系统进行GC的关键执行函数。

2.1 关键数据结构

  1. gc_inode_list

    f2fs_gc函数中,初始化变量gc_list需要额外关注。该变量的主要作用是将受到GC影响的inode都加入到list中。在数据迁移步骤中,可能需要读取inode信息、以及对data block所属的inode进行一定的修改,因此将这些inode都放入list中,统一处理。

    struct gc_inode_list {
    	struct list_head ilist;
    	struct radix_tree_root iroot;
    };
    
  2. f2fs_gc_control(fs/f2fs/f2fs.h)

    f2fs_gc_control存储了F2FS GC所需要的一些控制配置参数。

    struct f2fs_gc_control {
    	unsigned int victim_segno;	/* target victim segment number */
    	int init_gc_type;		/* 初始化的GC类型, 其值为FG_GC or BG_GC */
    	bool no_bg_gc;			/* check the space and stop bg_gc */
    	bool should_migrate_blocks;	/* should migrate blocks */
    	bool err_gc_skipped;		/* return EAGAIN if GC skipped */
    	unsigned int nr_free_secs;	/* # of free sections to do GC,本次GC需要释放的section数 */
    };
    

    使用:在gc触发线程中进行初始化以及赋值,并作为函数f2fs_gc的参数传入,指导相应的GC逻辑。

    • 对于前台GC,设定了gc_control.nr_free_secs=1,说明本次GC必须要至少回收一个segment,
    • 对于后台GC,设定了gc_control.nr_free_secs=0,说明后台GC可以没有收获。
    //函数 gc_thread_func
    if (foreground)
    	sync_mode = false;
    gc_control.init_gc_type = sync_mode ? FG_GC : BG_GC;
    gc_control.no_bg_gc = foreground;	//foreground状态下,不进行bggc
    gc_control.nr_free_secs = foreground ? 1 : 0;
       
    if (f2fs_gc(sbi, &gc_control)) {
    	...
    }else ...
    

2.2 函数主要流程

由F2FS GC原理可知,F2FS GC的核心步骤主要有两个:

  1. 选择合适的victim segment

    在作者定义中,F2FS GC选取的单位为section,但在默认F2FS下,一般一个section大小等同于一个segment大小,大小均为2MB,即能够存储512个block(4KB),且在实现中一般直接使用segno(# of segment)来作为选取的结果,因此一般也可视为segment为GC的基本单位。

  2. 对选取的victim segment中的有效块迁移到其他位置,待全部迁移完成后,释放该segment的空间

其中,完成步骤1的关键函数在于__get_victim(间接调用的核心函数为f2fs_get_victim函数),完成步骤2的关键函数在于do_garbage_collect。在当前函数主流程中,先不进行分析,无需关注这些函数的具体实现,只需知道它们实现了上述的功能即可。(若想进一步了解,请查看f2fs_get_victimdo_garbage_collect部分的源码阅读)

f2fs_gc主要流程如下

  1. 若可用空间不足,则将gc_type设定为FG_GC

    • 空间不足的判断条件:has_not_enough_free_secs(sbi, freed=0, needed=0),下面为该函数的核心判断部分。

      /**计算dirty node/dentry所需的最少section数量和最多section数量
      *lower_secs = node_secs + dent_secs;
      *upper_secs = node_secs + dent_secs + (node_blocks ? 1 : 0) + (dent_blocks ? 1 : 0);
      */
      __get_secs_required(sbi, &lower_secs, &upper_secs, &curseg_space);
           
      free_secs = free_sections(sbi) + freed;
           
      lower_secs += needed + reserved_sections(sbi);
           
      upper_secs += needed + reserved_sections(sbi);
      // reserved_sections = SM_I(sbi)->reserved_segments + SM_I(sbi)->additional_reserved_segments
      // additional_reserved_segment: reserved segs for IO align feature
      if (free_secs > upper_secs)
      	return false;
      else if (free_secs <= lower_secs)
      	return true;
      return !curseg_space;
      
    • 空间不足的情况下,若当前还有PRE FREE状态的segment,则会触发一次checkpoint把这些segment变为free,从而尽量不触发FG_GC。

  2. 选择victim segment:ret = __get_victim(sbi, &segno, gc_type)

    • 若ret返回值为0,则代表成功找到了victim segment,其段号存储在segno中。

    • 若ret返回值为-ENODATA,且当前为FG_GC模式,且存在pinned_section,则unpin这些sections。

      FG_GC状态下,急需使用空间,因此在一般方法找不到victim segment的情况下,对于pinned segment的空间也要利用上。

  3. 对选取的victim segment进行垃圾回收(调用函数do_garbage_collect),并且统计相应的数据

    seg_freed = do_garbage_collect(sbi, segno, &gc_list, gc_type,
    			gc_control->should_migrate_blocks);
    total_freed += seg_freed;
    
  4. 运行完上述步骤后,进行一些后处理

    • BG_GC下,且可用空间不紧缺(has_enough_free_secs),更新相应信息,退出。

      注意,在这种情况下,没有触发checkpoint

    • FG_GC下,若可用空间不再紧缺(has_enough_free_secs(sbi, sec_freed, 0))

      若回收的segment数量还没达到要求,则重新进行上述gc步骤。否则更新相应信息,退出。

    • 无论是BG_GC还是FG_GC,若可用空间依然紧缺,则

      1. 重新计算当前dirty node/dentry所需的最大segment数量

        通过函数__get_secs_required(sbi, NULL, &upper_secs, NULL);得到upper_secs

      2. 若free segment数量不足,且PRE链表中仍然有PRE FREE segment,则会触发一次checkpoint把这些segment变为free,若已经不再有PRE FREE segment或CP失败了,则重新进行上述gc步骤。

        // NR_GC_CHECKPOINT_SECS:3,需要3个extra sections for writer's data/node/dentry
        if (free_sections(sbi) <= upper_secs + NR_GC_CHECKPOINT_SECS &&
        			prefree_segments(sbi)) {
        	ret = f2fs_write_checkpoint(sbi, &cpc);
        	if (ret)
        		goto stop;	//退出
        	/* Reset due to checkpoint */
        	sec_freed = 0;
        }
        go_gc_more:
        	segno = NULL_SEGNO;
        	goto gc_more;	//	重新进行GC步骤
        
      3. 对于FG_GC,还要判断roundskipped_round(这部分仍有疑问)

        if (sbi->skipped_gc_rwsem)
        	skipped_round++;
        round++;
        if (skipped_round > MAX_SKIP_GC_COUNT &&
        		skipped_round * 2 >= round) {
        	ret = f2fs_write_checkpoint(sbi, &cpc);
        	goto stop;
        }