Linux f2fs_gc垃圾回收与前GC后GC模式切换
f2fs是一种基于Log-structured设计的文件系统,所有写操作都追加写入新的block,导致旧block中的有效数据占比逐渐下降。垃圾回收(GC)的任务是回收这些低效segment中的剩余有效数据,将segment整块释放以供后续写入。f2fs的GC分为前GC(Foreground GC)和后GC(Background GC)两种模式。
GC的入口函数是f2fs_gc(),定义在gc.c中:
int f2fs_gc(struct f2fs_sb_info *sbi, bool sync,
bool background, unsigned int segno)
{
unsigned int init_victim_cnt = 0;
unsigned int gc_type = sync ? FG_GC : BG_GC;
int ret;
if (gc_type == BG_GC)
down_read(&sbi->gc_lock);
else
down_write(&sbi->gc_lock);
for (; ;) {
struct victim_sel_policy p;
unsigned int cost;
unsigned int nfree;
nfree = prefree_segments(sbi);
if (nfree > (unsigned int)sbi->gc_thread->gc_min_free_blocks) {
ret = 0;
break;
}
p.min_cost = UINT_MAX;
p.ofs_unit = 1;
p.gc_mode = (gc_type == BG_GC) ? GC_CB : GC_GREEDY;
p.victim_segno = NULL_SEGNO;
ret = f2fs_get_victim(sbi, &segno, gc_type, &p);
if (ret) {
if (gc_type == FG_GC && ret == -ENODATA)
goto stop;
break;
}
ret = f2fs_do_garbage_collect(sbi, segno, gc_type);
if (ret)
break;
}
if (gc_type == BG_GC)
up_read(&sbi->gc_lock);
else
up_write(&sbi->gc_lock);
return ret;
}
f2fs_gc()接收三个参数:sync决定使用前GC还是后GC,background标识是否为后台触发,segno指定特定segment(SSR模式用)。GC类型通过gc_type区分:FG_GC(前GC)和BG_GC(后GC)。前GC使用写锁(down_write),会阻塞所有写入;后GC使用读锁(down_read),允许并发写入。
segment选择策略通过struct victim_sel_policy配置。前GC默认使用GC_GREEDY贪心策略,选择有效block最多的segment进行回收(最大化回收收益)。后GC默认使用GC_CB(Cost-Benefit)策略,综合考虑segment的年龄和有效数据占比:
static unsigned int get_cost(struct f2fs_sb_info *sbi,
unsigned int segno,
struct victim_sel_policy *p)
{
struct seg_entry *se = get_seg_entry(sbi, segno);
unsigned int cost = 0;
if (p->gc_mode == GC_GREEDY) {
cost = se->valid_blocks;
} else if (p->gc_mode == GC_CB) {
unsigned int age = 0;
unsigned long long mtime = se->mtime;
unsigned long long cur = get_mtime(sbi);
if (mtime < cur)
age = cur - mtime;
cost = (se->valid_blocks * 100) / max(1U, (unsigned int)age);
}
return cost;
}
GC_GREEDY下cost等于valid_blocks数量,选择valid_blocks最小的segment。GC_CB下cost = valid_blocks * 100 / age,选择"有效数据少且存在时间长"的segment,这种segment在未来被再次修改的概率更低。
实际的block迁移在f2fs_do_garbage_collect()中执行:
int f2fs_do_garbage_collect(struct f2fs_sb_info *sbi,
unsigned int segno, int gc_type)
{
struct blk_plug plug;
unsigned int sec_start;
unsigned int nr_blocks;
int i, ret = 0;
sec_start = GET_SEC_FROM_SEG(sbi, segno);
nr_blocks = sbi->blocks_per_seg;
blk_start_plug(&plug);
for (i = 0; i < nr_blocks; i++) {
struct page *page;
block_t addr;
struct f2fs_summary sum;
addr = f2fs_get_block_addr(sbi, segno, i);
if (f2fs_check_valid_map(sbi, segno, i))
continue;
page = f2fs_get_lock_data_page(sbi->meta_inode,
addr, false);
f2fs_read_data_page(page);
get_summary(sbi, segno, i, &sum);
f2fs_allocate_data_block(sbi, NULL, addr, NULL,
&new_addr, &sum, gc_type);
f2fs_update_sit_entry(sbi, new_addr, 1);
f2fs_update_sit_entry(sbi, addr, -1);
}
blk_finish_plug(&plug);
f2fs_submit_merged_write(sbi, DATA);
return ret;
}
该函数逐block遍历segment。通过f2fs_check_valid_map()跳过无效block(位图对应bit为0)。对有效block执行:读取数据页、从SSA获取该block所在的文件信息(struct f2fs_summary)、分配新数据block、将数据写入新位置、更新SIT(旧block减1、新block加1)。前GC模式下,写操作同步等待IO完成;后GC模式下写操作为异步。
关键函数f2fs_allocate_data_block()在两种GC模式下行为不同:
void f2fs_allocate_data_block(struct f2fs_sb_info *sbi,
struct page *page, block_t old_addr,
struct block_device **last_dev,
block_t *new_addr,
struct f2fs_summary *sum,
int gc_type)
{
if (gc_type == FG_GC) {
f2fs_wait_on_page_writeback(page, DATA, true, true);
f2fs_submit_merged_write_cond(sbi, NULL, page, DATA);
}
*new_addr = f2fs_new_block(sbi, old_addr, sum, gc_type);
if (gc_type == FG_GC)
f2fs_submit_merged_write(sbi, DATA);
}
前GC在分配新block前等待页面写回完成并刷入合并写请求,确保GC迁移的原子性。后GC不做同步等待,由后台writeback线程异步处理。
后GC由f2fs的GC daemon线程gc_thread_func()驱动,周期性触发:
static int gc_thread_func(void *data)
{
struct f2fs_sb_info *sbi = data;
struct f2fs_gc_thread *gc_th = sbi->gc_thread;
wait_queue_head_t *wq = &gc_th->gc_wait_queue_head;
while (1) {
if (kthread_should_stop())
break;
if (free_segments(sbi) < gc_th->min_free_segments) {
f2fs_gc(sbi, false, true, NULL_SEGNO);
continue;
}
if (wait_event_interruptible_timeout(wq,
kthread_should_stop(),
msecs_to_jiffies(gc_th->gc_sleep_time)))
continue;
}
return 0;
}
当空闲segment数量低于min_free_segments阈值时,GC daemon持续执行后GC。空闲segment充足时进入休眠,休眠时长由sysfs参数gc_sleep_time控制。
前GC的触发路径在f2fs_balance_fs()和f2fs_balance_fs_bg()中。当用户写入操作检测到空闲segment严重不足时,直接在当前进程上下文中调用f2fs_gc(sbi, true, false, NULL_SEGNO),同步等待GC完成后才返回。这种方式显著增加写入时延但确保写入操作可以继续。
GC的victim segment选择还有一个特殊模式gc_mode == GC_URGENT。当设置URGENT后,f2fs_urgent_gc()被触发,以最高优先级执行GC,适用于用户显式调用或设备空闲容量极低时的紧急回收。
Linux f2fs_gc垃圾回收与前GC后GC模式切换
张小明
前端开发工程师
FigmaCN深度探索:设计师的语言革命与效率突破
FigmaCN深度探索:设计师的语言革命与效率突破 【免费下载链接】figmaCN 中文 Figma 插件,设计师人工翻译校验 项目地址: https://gitcode.com/gh_mirrors/fi/figmaCN 当中国设计师面对全球顶尖设计工具Figma时,语言障碍往往成为创意表…
Silvaco TCAD电极定义报错?手把手教你排查‘Cannot find the electrode’问题(附完整PIN二极管仿真流程)
Silvaco TCAD电极定义报错深度排查指南:从原理到PIN二极管实战在半导体器件仿真领域,电极定义是构建有效仿真模型的关键第一步。许多Silvaco TCAD初学者,尤其是正在进行毕业设计项目的研究生们,常常在电极定义环节遭遇"Canno…
SH9对话量子场论(DQFT)雏形中以话轮转换为场激发的符号体系构建报告(世毫九实验室原创研究)
SH9对话量子场论(DQFT)雏形中以话轮转换为场激发的符号体系构建报告(世毫九实验室原创研究) 作者:方见华 单位:世毫九实验室 摘要 本报告基于世毫九实验室(Shardy Lab)原创的对话量子…
AMD平台装机避坑指南:微星迫击炮B550M主板的内存插法,你真的搞懂了吗?
AMD平台装机避坑指南:微星迫击炮B550M主板的内存插法解析最近帮朋友装机时遇到一个有趣的现象:他的微星B550M迫击炮主板插上三根内存后频繁蓝屏,而换成两根或四根却一切正常。这让我意识到,很多DIY玩家对AMD平台的内存插法存在认知…
CAN总线调试避坑:为什么你的设备总报错?从位填充到错误帧的实战排查指南
CAN总线调试实战:从波形异常到错误帧的深度排查手册当逻辑分析仪上突然出现连续6个相同电平的波形时,作为嵌入式工程师的你,是否曾陷入"硬件问题还是协议理解偏差"的困境?CAN总线通信的稳定性问题往往隐藏在看似简单的位…
蓝桥杯嵌入式备赛:从第十四届真题看CubeMX配置与状态机设计的实战技巧
蓝桥杯嵌入式备赛:从第十四届真题看CubeMX配置与状态机设计的实战技巧在嵌入式开发竞赛中,蓝桥杯一直以其贴近实际工程应用的题目设计著称。第十四届真题尤其体现了这一特点,不仅考察基础外设操作能力,更注重工程化思维和系统架构…