* [PATCH v2 1/2] f2fs: use BIT_ULL() for mount option bits [not found] <CGME20260707225035epcms2p812b92e6001afb57eb06d98225f1acd87@epcms2p8> @ 2026-07-07 22:50 ` Yonggil Song [not found] ` <CGME20260707225035epcms2p812b92e6001afb57eb06d98225f1acd87@epcms2p4> 0 siblings, 1 reply; 4+ messages in thread From: Yonggil Song @ 2026-07-07 22:50 UTC (permalink / raw) To: jaegeuk, chao, corbet Cc: linux-f2fs-devel, linux-doc, linux-kernel, Dongjin Kim, Seokhwan Kim, Daejun Park The mount option bitmasks mount_opt.opt and f2fs_fs_context.opt_mask are unsigned long long, but the bits are manipulated with BIT(), which is unsigned long. On 32-bit architectures a 33rd mount option would shift past the type width, which is undefined behaviour: fs/f2fs/f2fs.h:2945:4: warning: shift count >= width of type [-Wshift-count-overflow] Switch test_opt()/set_opt()/clear_opt(), the fs_context helpers and the direct opt_mask users to BIT_ULL(). No functional change with the current 32 options; this prepares for adding more. Reported-by: kernel test robot <lkp@intel.com> Closes: https://lore.kernel.org/oe-kbuild-all/202607071623.ZxOpKv3S-lkp@intel.com/ Signed-off-by: Yonggil Song <yonggil.song@samsung.com> --- fs/f2fs/f2fs.h | 6 +++--- fs/f2fs/super.c | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index e40b6b2784ee..20a1e2353f60 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -142,11 +142,11 @@ enum f2fs_mount_opt { #define F2FS_OPTION(sbi) ((sbi)->mount_opt) #define clear_opt(sbi, option) \ - (F2FS_OPTION(sbi).opt &= ~BIT(F2FS_MOUNT_##option)) + (F2FS_OPTION(sbi).opt &= ~BIT_ULL(F2FS_MOUNT_##option)) #define set_opt(sbi, option) \ - (F2FS_OPTION(sbi).opt |= BIT(F2FS_MOUNT_##option)) + (F2FS_OPTION(sbi).opt |= BIT_ULL(F2FS_MOUNT_##option)) #define test_opt(sbi, option) \ - (F2FS_OPTION(sbi).opt & BIT(F2FS_MOUNT_##option)) + (F2FS_OPTION(sbi).opt & BIT_ULL(F2FS_MOUNT_##option)) #define ver_after(a, b) (typecheck(unsigned long long, a) && \ typecheck(unsigned long long, b) && \ diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index ccf806b676f5..62d3a58cb1b2 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -416,21 +416,21 @@ struct f2fs_fs_context { static inline void ctx_set_opt(struct f2fs_fs_context *ctx, enum f2fs_mount_opt flag) { - ctx->info.opt |= BIT(flag); - ctx->opt_mask |= BIT(flag); + ctx->info.opt |= BIT_ULL(flag); + ctx->opt_mask |= BIT_ULL(flag); } static inline void ctx_clear_opt(struct f2fs_fs_context *ctx, enum f2fs_mount_opt flag) { - ctx->info.opt &= ~BIT(flag); - ctx->opt_mask |= BIT(flag); + ctx->info.opt &= ~BIT_ULL(flag); + ctx->opt_mask |= BIT_ULL(flag); } static inline bool ctx_test_opt(struct f2fs_fs_context *ctx, enum f2fs_mount_opt flag) { - return ctx->info.opt & BIT(flag); + return ctx->info.opt & BIT_ULL(flag); } void f2fs_printk(struct f2fs_sb_info *sbi, bool limit_rate, @@ -1422,7 +1422,7 @@ static int f2fs_check_compression(struct fs_context *fc, ctx_test_opt(ctx, F2FS_MOUNT_COMPRESS_CACHE)) f2fs_info(sbi, "Image doesn't support compression"); clear_compression_spec(ctx); - ctx->opt_mask &= ~BIT(F2FS_MOUNT_COMPRESS_CACHE); + ctx->opt_mask &= ~BIT_ULL(F2FS_MOUNT_COMPRESS_CACHE); return 0; } if (ctx->spec_mask & F2FS_SPEC_compress_extension) { @@ -1490,43 +1490,43 @@ static int f2fs_check_opt_consistency(struct fs_context *fc, return -EINVAL; if (f2fs_hw_should_discard(sbi) && - (ctx->opt_mask & BIT(F2FS_MOUNT_DISCARD)) && + (ctx->opt_mask & BIT_ULL(F2FS_MOUNT_DISCARD)) && !ctx_test_opt(ctx, F2FS_MOUNT_DISCARD)) { f2fs_warn(sbi, "discard is required for zoned block devices"); return -EINVAL; } if (!f2fs_hw_support_discard(sbi) && - (ctx->opt_mask & BIT(F2FS_MOUNT_DISCARD)) && + (ctx->opt_mask & BIT_ULL(F2FS_MOUNT_DISCARD)) && ctx_test_opt(ctx, F2FS_MOUNT_DISCARD)) { f2fs_warn(sbi, "device does not support discard"); ctx_clear_opt(ctx, F2FS_MOUNT_DISCARD); - ctx->opt_mask &= ~BIT(F2FS_MOUNT_DISCARD); + ctx->opt_mask &= ~BIT_ULL(F2FS_MOUNT_DISCARD); } if (f2fs_sb_has_device_alias(sbi) && - (ctx->opt_mask & BIT(F2FS_MOUNT_READ_EXTENT_CACHE)) && + (ctx->opt_mask & BIT_ULL(F2FS_MOUNT_READ_EXTENT_CACHE)) && !ctx_test_opt(ctx, F2FS_MOUNT_READ_EXTENT_CACHE)) { f2fs_err(sbi, "device aliasing requires extent cache"); return -EINVAL; } if (test_opt(sbi, RESERVE_ROOT) && - (ctx->opt_mask & BIT(F2FS_MOUNT_RESERVE_ROOT)) && + (ctx->opt_mask & BIT_ULL(F2FS_MOUNT_RESERVE_ROOT)) && ctx_test_opt(ctx, F2FS_MOUNT_RESERVE_ROOT)) { f2fs_info(sbi, "Preserve previous reserve_root=%u", F2FS_OPTION(sbi).root_reserved_blocks); ctx_clear_opt(ctx, F2FS_MOUNT_RESERVE_ROOT); - ctx->opt_mask &= ~BIT(F2FS_MOUNT_RESERVE_ROOT); + ctx->opt_mask &= ~BIT_ULL(F2FS_MOUNT_RESERVE_ROOT); ctx->spec_mask &= ~F2FS_SPEC_reserve_root; } if (test_opt(sbi, RESERVE_NODE) && - (ctx->opt_mask & BIT(F2FS_MOUNT_RESERVE_NODE)) && + (ctx->opt_mask & BIT_ULL(F2FS_MOUNT_RESERVE_NODE)) && ctx_test_opt(ctx, F2FS_MOUNT_RESERVE_NODE)) { f2fs_info(sbi, "Preserve previous reserve_node=%u", F2FS_OPTION(sbi).root_reserved_nodes); ctx_clear_opt(ctx, F2FS_MOUNT_RESERVE_NODE); - ctx->opt_mask &= ~BIT(F2FS_MOUNT_RESERVE_NODE); + ctx->opt_mask &= ~BIT_ULL(F2FS_MOUNT_RESERVE_NODE); ctx->spec_mask &= ~F2FS_SPEC_reserve_node; } base-commit: cb8ff3ead9a3fc43727980be58c7099506f65261 -- 2.43.0 ^ permalink raw reply [flat|nested] 4+ messages in thread
[parent not found: <CGME20260707225035epcms2p812b92e6001afb57eb06d98225f1acd87@epcms2p4>]
* [PATCH v2 2/2] f2fs: introduce gcless mount option to avoid foreground GC [not found] ` <CGME20260707225035epcms2p812b92e6001afb57eb06d98225f1acd87@epcms2p4> @ 2026-07-07 22:54 ` Yonggil Song 2026-08-10 2:10 ` Chao Yu [not found] ` <CGME20260707225035epcms2p812b92e6001afb57eb06d98225f1acd87@epcms2p2> 0 siblings, 2 replies; 4+ messages in thread From: Yonggil Song @ 2026-07-07 22:54 UTC (permalink / raw) To: jaegeuk, chao, corbet Cc: linux-doc, linux-kernel, linux-f2fs-devel, Seokhwan Kim, Dongjin Kim Under heavy out-of-place overwrite at near-full utilization, foreground GC picks nearly-fully-valid victims and relocates almost every block, while the scattered invalid space is not SSR-reusable until a checkpoint stabilizes it. WAF explodes even though reclaimable space exists. Add a "gcless" mount option that counts checkpoint-stable invalid blocks (invalid as of the last checkpoint, hence SSR-reusable) and credits them as free sections in has_not_enough_free_secs(). The watermark then sees the slack as free, so f2fs_balance_fs() skips foreground GC and allocation reclaims the space through SSR instead. The count is recomputed at mount and after each successful checkpoint under block_operations(), where it is exact and needs no locking. The option is limited to adaptive (non-LFS) mode. 8 GiB UFS, 2 GiB random overwrite at 99% utilization: baseline: WAF 76.7, foreground GC calls 578k gcless: WAF 1.2, foreground GC calls 504 Signed-off-by: Yonggil Song <yonggil.song@samsung.com> --- v2: - split out the BIT_ULL() conversion for mount option bits into a preparation patch, fixing the 32-bit shift-count-overflow warning reported by kernel test robot <lkp@intel.com> Documentation/filesystems/f2fs.rst | 8 +++++ fs/f2fs/checkpoint.c | 1 + fs/f2fs/debug.c | 4 +++ fs/f2fs/f2fs.h | 12 ++++++++ fs/f2fs/gc.c | 5 ++-- fs/f2fs/segment.c | 47 ++++++++++++++++++++++++++++++ fs/f2fs/segment.h | 15 ++++++++++ fs/f2fs/super.c | 17 +++++++++++ 8 files changed, 107 insertions(+), 2 deletions(-) diff --git a/Documentation/filesystems/f2fs.rst b/Documentation/filesystems/f2fs.rst index 7e4031631286..05ac3f76bfba 100644 --- a/Documentation/filesystems/f2fs.rst +++ b/Documentation/filesystems/f2fs.rst @@ -409,6 +409,14 @@ lookup_mode=%s Control the directory lookup behavior for casefolded on-disk `SB_ENC_NO_COMPAT_FALLBACK_FL` flag. ================== ======================================== +gcless Avoid foreground GC by crediting checkpoint-stable invalid + blocks (invalid at the last checkpoint and thus SSR-reusable) + as free space in the free section watermark, so allocation + recycles that slack via SSR instead of relocating valid + blocks. Intended for heavy out-of-place overwrite at + near-full utilization, where it reduces write amplification. + Not allowed in LFS mode (including zoned block devices), + by default it's disabled. ======================== ============================================================ Debugfs Entries diff --git a/fs/f2fs/checkpoint.c b/fs/f2fs/checkpoint.c index 01e1ba77263e..0d83e90b583c 100644 --- a/fs/f2fs/checkpoint.c +++ b/fs/f2fs/checkpoint.c @@ -1932,6 +1932,7 @@ int f2fs_write_checkpoint(struct f2fs_sb_info *sbi, struct cp_control *cpc) f2fs_release_discard_addrs(sbi); } else { f2fs_clear_prefree_segments(sbi, cpc); + f2fs_update_cib(sbi); } f2fs_restore_inmem_curseg(sbi); diff --git a/fs/f2fs/debug.c b/fs/f2fs/debug.c index af88db8fdb71..b1435e01447d 100644 --- a/fs/f2fs/debug.c +++ b/fs/f2fs/debug.c @@ -285,6 +285,8 @@ static void update_general_status(struct f2fs_sb_info *sbi) for (i = 0; i < MAX_CALL_TYPE; i++) si->cp_call_count[i] = atomic_read(&sbi->cp_call_count[i]); + si->cib_total_blocks = READ_ONCE(sbi->cib_total_blocks); + for (i = 0; i < 2; i++) { si->segment_count[i] = sbi->segment_count[i]; si->block_count[i] = sbi->block_count[i]; @@ -623,6 +625,8 @@ static int stat_show(struct seq_file *s, void *v) seq_printf(s, " - Total : %4d\n", si->nr_total_ckpt); seq_printf(s, " - Cur time : %4d(ms)\n", si->cur_ckpt_time); seq_printf(s, " - Peak time : %4d(ms)\n", si->peak_ckpt_time); + seq_printf(s, "GCless CIB budget : %u blocks\n", + si->cib_total_blocks); seq_printf(s, "GC calls: %d (gc_thread: %d)\n", si->gc_call_count[BACKGROUND] + si->gc_call_count[FOREGROUND], diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h index 20a1e2353f60..a2a85eaa636d 100644 --- a/fs/f2fs/f2fs.h +++ b/fs/f2fs/f2fs.h @@ -137,6 +137,7 @@ enum f2fs_mount_opt { * string rather than using the MS_LAZYTIME flag, so this must remain. */ F2FS_MOUNT_LAZYTIME, + F2FS_MOUNT_GCLESS, F2FS_MOUNT_RESERVE_NODE, }; @@ -1870,6 +1871,15 @@ struct f2fs_sb_info { struct f2fs_mount_info mount_opt; /* mount options */ + /* + * Checkpoint-stable invalid blocks: sum of blocks that were invalid at + * the last checkpoint and are thus SSR-eligible while their section stays + * dirty. Written only by f2fs_update_cib() (mount and after each + * successful checkpoint, under block_operations()), read locklessly, so a + * plain block_t with READ_ONCE()/WRITE_ONCE() suffices -- no atomic. + */ + block_t cib_total_blocks; + /* for cleaning operations */ struct f2fs_rwsem gc_lock; /* * semaphore for GC, avoid @@ -4000,6 +4010,7 @@ bool f2fs_issue_discard_timeout(struct f2fs_sb_info *sbi, bool need_check); void f2fs_clear_prefree_segments(struct f2fs_sb_info *sbi, struct cp_control *cpc); void f2fs_dirty_to_prefree(struct f2fs_sb_info *sbi); +void f2fs_update_cib(struct f2fs_sb_info *sbi); block_t f2fs_get_unusable_blocks(struct f2fs_sb_info *sbi); int f2fs_disable_cp_again(struct f2fs_sb_info *sbi, block_t unusable); void f2fs_release_discard_addrs(struct f2fs_sb_info *sbi); @@ -4289,6 +4300,7 @@ struct f2fs_stat_info { int dirty_count, node_pages, meta_pages, compress_pages; int compress_page_hit; int prefree_count, free_segs, free_secs; + block_t cib_total_blocks; int cp_call_count[MAX_CALL_TYPE], cp_count; int gc_call_count[MAX_CALL_TYPE]; int gc_segs[2][2]; diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c index e60c1106f70b..ebf47ef5fff0 100644 --- a/fs/f2fs/gc.c +++ b/fs/f2fs/gc.c @@ -1964,10 +1964,11 @@ int f2fs_gc(struct f2fs_sb_info *sbi, struct f2fs_gc_control *gc_control) * threshold, we can make them free by checkpoint. Then, we * secure free segments which doesn't need fggc any more. */ - if (prefree_segments(sbi)) { + if (prefree_segments(sbi) || test_opt(sbi, GCLESS)) { stat_inc_cp_call_count(sbi, TOTAL_CALL); ret = f2fs_write_checkpoint(sbi, &cpc); - if (ret) + if (ret || + (test_opt(sbi, GCLESS) && has_enough_free_secs(sbi, 0, 0))) goto stop; /* Reset due to checkpoint */ sec_freed = 0; diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c index 788f8b050249..e020714261ff 100644 --- a/fs/f2fs/segment.c +++ b/fs/f2fs/segment.c @@ -305,6 +305,53 @@ static void __complete_revoke_list(struct inode *inode, struct list_head *head, f2fs_do_truncate_blocks(inode, start_index * PAGE_SIZE, false); } +static inline u32 cib_contrib_of_se(struct f2fs_sb_info *sbi, unsigned long seg) +{ + struct dirty_seglist_info *dirty_i = DIRTY_I(sbi); + struct free_segmap_info *free_i = SM_I(sbi)->free_info; + struct seg_entry *se = get_seg_entry(sbi, seg); + u32 usable = f2fs_usable_blks_in_seg(sbi, seg); + u32 ckpt_v = se->ckpt_valid_blocks; + + /* free, prefree and current segments hold no reusable SSR slack */ + if (test_bit(seg, free_i->free_segmap)) + return 0; + if (test_bit(seg, dirty_i->dirty_segmap[PRE])) + return 0; + if (is_curseg(sbi, seg)) + return 0; + if (ckpt_v >= usable) + return 0; + + return usable - ckpt_v; +} + +/* + * Recompute the checkpoint-stable invalid-block budget by scanning all main + * segments via ckpt_valid_blocks (free/prefree/current segments contribute + * nothing). Called at mount and after every successful checkpoint, both under + * block_operations(), so it is the only writer and needs no atomic; readers use + * READ_ONCE(). ckpt_valid_blocks is fixed between checkpoints, so the value is + * exact at each checkpoint -- gcless checkpoints often enough to keep it fresh, + * which is why no per-allocation delta hooks are needed. + */ +void f2fs_update_cib(struct f2fs_sb_info *sbi) +{ + unsigned long nsegs = MAIN_SEGS(sbi); + unsigned long seg; + block_t total = 0; + + if (!test_opt(sbi, GCLESS)) { + WRITE_ONCE(sbi->cib_total_blocks, 0); + return; + } + + for (seg = 0; seg < nsegs; seg++) + total += cib_contrib_of_se(sbi, seg); + + WRITE_ONCE(sbi->cib_total_blocks, total); +} + static int __f2fs_commit_atomic_write(struct inode *inode) { struct f2fs_sb_info *sbi = F2FS_I_SB(inode); diff --git a/fs/f2fs/segment.h b/fs/f2fs/segment.h index 068845660b0f..3226930d759a 100644 --- a/fs/f2fs/segment.h +++ b/fs/f2fs/segment.h @@ -702,6 +702,21 @@ static inline bool has_not_enough_free_secs(struct f2fs_sb_info *sbi, free_secs = free_sections(sbi) + freed; required_secs = needed + reserved_sections(sbi) + __get_secs_required(sbi); + /* + * Credit the checkpoint-stable invalid-block budget (SSR-reusable slack) + * to free_secs, so the watermark lets allocation recycle that slack via + * SSR instead of running foreground GC. cib_total_blocks is a section/ + * block count (always non-negative), so the math stays in unsigned int and + * is capped at the sections still unaccounted for. + */ + if (test_opt(sbi, GCLESS)) { + unsigned int sec_blks = CAP_BLKS_PER_SEC(sbi); + unsigned int add_secs = READ_ONCE(sbi->cib_total_blocks) / sec_blks; + unsigned int room = free_secs < MAIN_SECS(sbi) ? + MAIN_SECS(sbi) - free_secs : 0; + + free_secs += min(add_secs, room); + } return free_secs < required_secs; } diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c index 62d3a58cb1b2..07c7d88719a6 100644 --- a/fs/f2fs/super.c +++ b/fs/f2fs/super.c @@ -234,6 +234,7 @@ enum { Opt_jqfmt, Opt_checkpoint, Opt_lookup_mode, + Opt_gcless, Opt_err, }; @@ -336,6 +337,7 @@ static const struct fs_parameter_spec f2fs_param_specs[] = { fsparam_flag("usrquota", Opt_usrquota), fsparam_flag("grpquota", Opt_grpquota), fsparam_flag("prjquota", Opt_prjquota), + fsparam_flag("gcless", Opt_gcless), fsparam_string("usrjquota", Opt_usrjquota), fsparam_flag("usrjquota", Opt_usrjquota), fsparam_string("grpjquota", Opt_grpjquota), @@ -1230,6 +1232,9 @@ static int f2fs_parse_param(struct fs_context *fc, struct fs_parameter *param) case Opt_nat_bits: ctx_set_opt(ctx, F2FS_MOUNT_NAT_BITS); break; + case Opt_gcless: + ctx_set_opt(ctx, F2FS_MOUNT_GCLESS); + break; case Opt_lookup_mode: F2FS_CTX_INFO(ctx).lookup_mode = result.uint_32; ctx->spec_mask |= F2FS_SPEC_lookup_mode; @@ -1603,6 +1608,13 @@ static int f2fs_check_opt_consistency(struct fs_context *fc, f2fs_err(sbi, "Allow to mount readonly mode only"); return -EROFS; } + + /* Only for adaptive mode */ + if (test_opt(sbi, GCLESS) && f2fs_lfs_mode(sbi)) { + f2fs_err(sbi, "gcless is not allowed in LFS mode"); + return -EINVAL; + } + return 0; } @@ -2542,6 +2554,9 @@ static int f2fs_show_options(struct seq_file *seq, struct dentry *root) else if (F2FS_OPTION(sbi).lookup_mode == LOOKUP_AUTO) seq_show_option(seq, "lookup_mode", "auto"); + if (test_opt(sbi, GCLESS)) + seq_puts(seq, ",gcless"); + return 0; } @@ -5344,6 +5359,8 @@ static int f2fs_fill_super(struct super_block *sb, struct fs_context *fc) if (err) goto sync_free_meta; + f2fs_update_cib(sbi); + /* * If filesystem is not mounted as read-only then * do start the gc_thread. -- 2.43.0 ^ permalink raw reply [flat|nested] 4+ messages in thread
* Re: [PATCH v2 2/2] f2fs: introduce gcless mount option to avoid foreground GC 2026-07-07 22:54 ` [PATCH v2 2/2] f2fs: introduce gcless mount option to avoid foreground GC Yonggil Song @ 2026-08-10 2:10 ` Chao Yu [not found] ` <CGME20260707225035epcms2p812b92e6001afb57eb06d98225f1acd87@epcms2p2> 1 sibling, 0 replies; 4+ messages in thread From: Chao Yu @ 2026-08-10 2:10 UTC (permalink / raw) To: yonggil.song, jaegeuk, corbet Cc: chao, linux-doc, linux-kernel, linux-f2fs-devel, Seokhwan Kim, Dongjin Kim Hi Yonggil, Sorry for the delay. On 7/7/26 22:54, Yonggil Song wrote: > Under heavy out-of-place overwrite at near-full utilization, foreground > GC picks nearly-fully-valid victims and relocates almost every block, > while the scattered invalid space is not SSR-reusable until a checkpoint > stabilizes it. WAF explodes even though reclaimable space exists. > > Add a "gcless" mount option that counts checkpoint-stable invalid blocks > (invalid as of the last checkpoint, hence SSR-reusable) and credits them > as free sections in has_not_enough_free_secs(). The watermark then sees > the slack as free, so f2fs_balance_fs() skips foreground GC and > allocation reclaims the space through SSR instead. The count is > recomputed at mount and after each successful checkpoint under > block_operations(), where it is exact and needs no locking. > > The option is limited to adaptive (non-LFS) mode. > > 8 GiB UFS, 2 GiB random overwrite at 99% utilization: > baseline: WAF 76.7, foreground GC calls 578k > gcless: WAF 1.2, foreground GC calls 504 Can we mitigate this by increasing min_ssr_sections? > > Signed-off-by: Yonggil Song <yonggil.song@samsung.com> > --- > v2: > - split out the BIT_ULL() conversion for mount option bits into a > preparation patch, fixing the 32-bit shift-count-overflow warning > reported by kernel test robot <lkp@intel.com> > Documentation/filesystems/f2fs.rst | 8 +++++ > fs/f2fs/checkpoint.c | 1 + > fs/f2fs/debug.c | 4 +++ > fs/f2fs/f2fs.h | 12 ++++++++ > fs/f2fs/gc.c | 5 ++-- > fs/f2fs/segment.c | 47 ++++++++++++++++++++++++++++++ > fs/f2fs/segment.h | 15 ++++++++++ > fs/f2fs/super.c | 17 +++++++++++ > 8 files changed, 107 insertions(+), 2 deletions(-) > > diff --git a/Documentation/filesystems/f2fs.rst b/Documentation/filesystems/f2fs.rst > index 7e4031631286..05ac3f76bfba 100644 > --- a/Documentation/filesystems/f2fs.rst > +++ b/Documentation/filesystems/f2fs.rst > @@ -409,6 +409,14 @@ lookup_mode=%s Control the directory lookup behavior for casefolded > on-disk `SB_ENC_NO_COMPAT_FALLBACK_FL` > flag. > ================== ======================================== > +gcless Avoid foreground GC by crediting checkpoint-stable invalid > + blocks (invalid at the last checkpoint and thus SSR-reusable) > + as free space in the free section watermark, so allocation > + recycles that slack via SSR instead of relocating valid > + blocks. Intended for heavy out-of-place overwrite at > + near-full utilization, where it reduces write amplification. > + Not allowed in LFS mode (including zoned block devices), > + by default it's disabled. > ======================== ============================================================ > > Debugfs Entries > diff --git a/fs/f2fs/checkpoint.c b/fs/f2fs/checkpoint.c > index 01e1ba77263e..0d83e90b583c 100644 > --- a/fs/f2fs/checkpoint.c > +++ b/fs/f2fs/checkpoint.c > @@ -1932,6 +1932,7 @@ int f2fs_write_checkpoint(struct f2fs_sb_info *sbi, struct cp_control *cpc) > f2fs_release_discard_addrs(sbi); > } else { > f2fs_clear_prefree_segments(sbi, cpc); > + f2fs_update_cib(sbi); > } > > f2fs_restore_inmem_curseg(sbi); > diff --git a/fs/f2fs/debug.c b/fs/f2fs/debug.c > index af88db8fdb71..b1435e01447d 100644 > --- a/fs/f2fs/debug.c > +++ b/fs/f2fs/debug.c > @@ -285,6 +285,8 @@ static void update_general_status(struct f2fs_sb_info *sbi) > for (i = 0; i < MAX_CALL_TYPE; i++) > si->cp_call_count[i] = atomic_read(&sbi->cp_call_count[i]); > > + si->cib_total_blocks = READ_ONCE(sbi->cib_total_blocks); > + > for (i = 0; i < 2; i++) { > si->segment_count[i] = sbi->segment_count[i]; > si->block_count[i] = sbi->block_count[i]; > @@ -623,6 +625,8 @@ static int stat_show(struct seq_file *s, void *v) > seq_printf(s, " - Total : %4d\n", si->nr_total_ckpt); > seq_printf(s, " - Cur time : %4d(ms)\n", si->cur_ckpt_time); > seq_printf(s, " - Peak time : %4d(ms)\n", si->peak_ckpt_time); > + seq_printf(s, "GCless CIB budget : %u blocks\n", > + si->cib_total_blocks); > seq_printf(s, "GC calls: %d (gc_thread: %d)\n", > si->gc_call_count[BACKGROUND] + > si->gc_call_count[FOREGROUND], > diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h > index 20a1e2353f60..a2a85eaa636d 100644 > --- a/fs/f2fs/f2fs.h > +++ b/fs/f2fs/f2fs.h > @@ -137,6 +137,7 @@ enum f2fs_mount_opt { > * string rather than using the MS_LAZYTIME flag, so this must remain. > */ > F2FS_MOUNT_LAZYTIME, > + F2FS_MOUNT_GCLESS, > F2FS_MOUNT_RESERVE_NODE, > }; > > @@ -1870,6 +1871,15 @@ struct f2fs_sb_info { > > struct f2fs_mount_info mount_opt; /* mount options */ > > + /* > + * Checkpoint-stable invalid blocks: sum of blocks that were invalid at > + * the last checkpoint and are thus SSR-eligible while their section stays > + * dirty. Written only by f2fs_update_cib() (mount and after each > + * successful checkpoint, under block_operations()), read locklessly, so a > + * plain block_t with READ_ONCE()/WRITE_ONCE() suffices -- no atomic. > + */ > + block_t cib_total_blocks; > + > /* for cleaning operations */ > struct f2fs_rwsem gc_lock; /* > * semaphore for GC, avoid > @@ -4000,6 +4010,7 @@ bool f2fs_issue_discard_timeout(struct f2fs_sb_info *sbi, bool need_check); > void f2fs_clear_prefree_segments(struct f2fs_sb_info *sbi, > struct cp_control *cpc); > void f2fs_dirty_to_prefree(struct f2fs_sb_info *sbi); > +void f2fs_update_cib(struct f2fs_sb_info *sbi); > block_t f2fs_get_unusable_blocks(struct f2fs_sb_info *sbi); > int f2fs_disable_cp_again(struct f2fs_sb_info *sbi, block_t unusable); > void f2fs_release_discard_addrs(struct f2fs_sb_info *sbi); > @@ -4289,6 +4300,7 @@ struct f2fs_stat_info { > int dirty_count, node_pages, meta_pages, compress_pages; > int compress_page_hit; > int prefree_count, free_segs, free_secs; > + block_t cib_total_blocks; > int cp_call_count[MAX_CALL_TYPE], cp_count; > int gc_call_count[MAX_CALL_TYPE]; > int gc_segs[2][2]; > diff --git a/fs/f2fs/gc.c b/fs/f2fs/gc.c > index e60c1106f70b..ebf47ef5fff0 100644 > --- a/fs/f2fs/gc.c > +++ b/fs/f2fs/gc.c > @@ -1964,10 +1964,11 @@ int f2fs_gc(struct f2fs_sb_info *sbi, struct f2fs_gc_control *gc_control) > * threshold, we can make them free by checkpoint. Then, we > * secure free segments which doesn't need fggc any more. > */ > - if (prefree_segments(sbi)) { > + if (prefree_segments(sbi) || test_opt(sbi, GCLESS)) { > stat_inc_cp_call_count(sbi, TOTAL_CALL); > ret = f2fs_write_checkpoint(sbi, &cpc); > - if (ret) > + if (ret || > + (test_opt(sbi, GCLESS) && has_enough_free_secs(sbi, 0, 0))) > goto stop; > /* Reset due to checkpoint */ > sec_freed = 0; > diff --git a/fs/f2fs/segment.c b/fs/f2fs/segment.c > index 788f8b050249..e020714261ff 100644 > --- a/fs/f2fs/segment.c > +++ b/fs/f2fs/segment.c > @@ -305,6 +305,53 @@ static void __complete_revoke_list(struct inode *inode, struct list_head *head, > f2fs_do_truncate_blocks(inode, start_index * PAGE_SIZE, false); > } > > +static inline u32 cib_contrib_of_se(struct f2fs_sb_info *sbi, unsigned long seg) > +{ > + struct dirty_seglist_info *dirty_i = DIRTY_I(sbi); > + struct free_segmap_info *free_i = SM_I(sbi)->free_info; > + struct seg_entry *se = get_seg_entry(sbi, seg); > + u32 usable = f2fs_usable_blks_in_seg(sbi, seg); > + u32 ckpt_v = se->ckpt_valid_blocks; > + > + /* free, prefree and current segments hold no reusable SSR slack */ > + if (test_bit(seg, free_i->free_segmap)) > + return 0; > + if (test_bit(seg, dirty_i->dirty_segmap[PRE])) > + return 0; > + if (is_curseg(sbi, seg)) > + return 0; > + if (ckpt_v >= usable) > + return 0; > + > + return usable - ckpt_v; If SSR is enabled, there may be valid but not checkpointed blocks in section? such space can not be treated as free? And, only updating sbi->cib_total_blocks w/ f2fs_update_cib() in checkpoint() is not enough? since free space in section may change due to SSR allocation and deletion. right? > +} > + > +/* > + * Recompute the checkpoint-stable invalid-block budget by scanning all main > + * segments via ckpt_valid_blocks (free/prefree/current segments contribute > + * nothing). Called at mount and after every successful checkpoint, both under > + * block_operations(), so it is the only writer and needs no atomic; readers use > + * READ_ONCE(). ckpt_valid_blocks is fixed between checkpoints, so the value is > + * exact at each checkpoint -- gcless checkpoints often enough to keep it fresh, > + * which is why no per-allocation delta hooks are needed. > + */ > +void f2fs_update_cib(struct f2fs_sb_info *sbi) > +{ > + unsigned long nsegs = MAIN_SEGS(sbi); > + unsigned long seg; > + block_t total = 0; > + > + if (!test_opt(sbi, GCLESS)) { > + WRITE_ONCE(sbi->cib_total_blocks, 0); > + return; > + } > + > + for (seg = 0; seg < nsegs; seg++) > + total += cib_contrib_of_se(sbi, seg); > + > + WRITE_ONCE(sbi->cib_total_blocks, total); > +} > + > static int __f2fs_commit_atomic_write(struct inode *inode) > { > struct f2fs_sb_info *sbi = F2FS_I_SB(inode); > diff --git a/fs/f2fs/segment.h b/fs/f2fs/segment.h > index 068845660b0f..3226930d759a 100644 > --- a/fs/f2fs/segment.h > +++ b/fs/f2fs/segment.h > @@ -702,6 +702,21 @@ static inline bool has_not_enough_free_secs(struct f2fs_sb_info *sbi, > free_secs = free_sections(sbi) + freed; > required_secs = needed + reserved_sections(sbi) + > __get_secs_required(sbi); > + /* > + * Credit the checkpoint-stable invalid-block budget (SSR-reusable slack) > + * to free_secs, so the watermark lets allocation recycle that slack via > + * SSR instead of running foreground GC. cib_total_blocks is a section/ > + * block count (always non-negative), so the math stays in unsigned int and > + * is capped at the sections still unaccounted for. > + */ > + if (test_opt(sbi, GCLESS)) { > + unsigned int sec_blks = CAP_BLKS_PER_SEC(sbi); > + unsigned int add_secs = READ_ONCE(sbi->cib_total_blocks) / sec_blks; > + unsigned int room = free_secs < MAIN_SECS(sbi) ? > + MAIN_SECS(sbi) - free_secs : 0; > + > + free_secs += min(add_secs, room); A free section can be reused by any DATA or NODE type write, but above GCLESS "free_secs" may not, e.g. all free_secs are from DATA type, then latter checkpoint won't write any node into DATA type section w/ SSR. Thanks, > + } > > return free_secs < required_secs; > } > diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c > index 62d3a58cb1b2..07c7d88719a6 100644 > --- a/fs/f2fs/super.c > +++ b/fs/f2fs/super.c > @@ -234,6 +234,7 @@ enum { > Opt_jqfmt, > Opt_checkpoint, > Opt_lookup_mode, > + Opt_gcless, > Opt_err, > }; > > @@ -336,6 +337,7 @@ static const struct fs_parameter_spec f2fs_param_specs[] = { > fsparam_flag("usrquota", Opt_usrquota), > fsparam_flag("grpquota", Opt_grpquota), > fsparam_flag("prjquota", Opt_prjquota), > + fsparam_flag("gcless", Opt_gcless), > fsparam_string("usrjquota", Opt_usrjquota), > fsparam_flag("usrjquota", Opt_usrjquota), > fsparam_string("grpjquota", Opt_grpjquota), > @@ -1230,6 +1232,9 @@ static int f2fs_parse_param(struct fs_context *fc, struct fs_parameter *param) > case Opt_nat_bits: > ctx_set_opt(ctx, F2FS_MOUNT_NAT_BITS); > break; > + case Opt_gcless: > + ctx_set_opt(ctx, F2FS_MOUNT_GCLESS); > + break; > case Opt_lookup_mode: > F2FS_CTX_INFO(ctx).lookup_mode = result.uint_32; > ctx->spec_mask |= F2FS_SPEC_lookup_mode; > @@ -1603,6 +1608,13 @@ static int f2fs_check_opt_consistency(struct fs_context *fc, > f2fs_err(sbi, "Allow to mount readonly mode only"); > return -EROFS; > } > + > + /* Only for adaptive mode */ > + if (test_opt(sbi, GCLESS) && f2fs_lfs_mode(sbi)) { > + f2fs_err(sbi, "gcless is not allowed in LFS mode"); > + return -EINVAL; > + } > + > return 0; > } > > @@ -2542,6 +2554,9 @@ static int f2fs_show_options(struct seq_file *seq, struct dentry *root) > else if (F2FS_OPTION(sbi).lookup_mode == LOOKUP_AUTO) > seq_show_option(seq, "lookup_mode", "auto"); > > + if (test_opt(sbi, GCLESS)) > + seq_puts(seq, ",gcless"); > + > return 0; > } > > @@ -5344,6 +5359,8 @@ static int f2fs_fill_super(struct super_block *sb, struct fs_context *fc) > if (err) > goto sync_free_meta; > > + f2fs_update_cib(sbi); > + > /* > * If filesystem is not mounted as read-only then > * do start the gc_thread. ^ permalink raw reply [flat|nested] 4+ messages in thread
[parent not found: <CGME20260707225035epcms2p812b92e6001afb57eb06d98225f1acd87@epcms2p2>]
* RE:(2) [PATCH v2 2/2] f2fs: introduce gcless mount option to avoid foreground GC [not found] ` <CGME20260707225035epcms2p812b92e6001afb57eb06d98225f1acd87@epcms2p2> @ 2026-08-11 5:47 ` Yonggil Song 0 siblings, 0 replies; 4+ messages in thread From: Yonggil Song @ 2026-08-11 5:47 UTC (permalink / raw) To: Chao Yu, jaegeuk, corbet Cc: linux-doc, linux-kernel, linux-f2fs-devel, Seokhwan Kim, Dongjin Kim, Yonggil Song To: Chao Yu <chao@kernel.org> Cc: jaegeuk@kernel.org, corbet@lwn.net, linux-f2fs-devel@lists.sourceforge.net, linux-doc@vger.kernel.org, linux-kernel@vger.kernel.org, Dongjin Kim <dongjin_.kim@samsung.com>, Daejun Park <daejun7.park@samsung.com> Subject: Re: [PATCH v2 2/2] f2fs: introduce gcless mount option to avoid foreground GC In-Reply-To: <f7dabaa3-6d3c-4c70-b953-08dd52c35f49@kernel.org> References: <20260707225453epcms2p4f8323f4f8b88f3ee892cddc126da51a6@epcms2p4> <f7dabaa3-6d3c-4c70-b953-08dd52c35f49@kernel.org> Hi Chao, No problem at all, and thanks a lot for the careful review. Let me answer the four points in order. Two of them (the update timing and the node/data typing) are real defects in v2; for the other two I think the current code is already correct, and I explain why below so you can tell me if I am missing something. On 8/10/26 02:10, Chao Yu wrote: >> 8 GiB UFS, 2 GiB random overwrite at 99% utilization: >> baseline: WAF 76.7, foreground GC calls 578k >> gcless: WAF 1.2, foreground GC calls 504 > > Can we mitigate this by increasing min_ssr_sections? I tried it, and it has no effect. Near-full random overwrite at 99% utilization, everything fixed except min_ssr_sections: raising it from the default 66 to 2000 left every counter identical (94 foreground GC calls, 602823 SSR blocks, FS-WAF 1.53), while lowering it to 0 did change things (476 GC calls, WAF 1.67) -- so the knob is live, it is just already saturated at the default. >> + if (test_bit(seg, free_i->free_segmap)) >> + return 0; >> + if (test_bit(seg, dirty_i->dirty_segmap[PRE])) >> + return 0; >> + if (is_curseg(sbi, seg)) >> + return 0; >> + if (ckpt_v >= usable) >> + return 0; >> + >> + return usable - ckpt_v; > > If SSR is enabled, there may be valid but not checkpointed blocks in section? > such space can not be treated as free? Such blocks exist, but they are already excluded, because ckpt_valid_blocks is not a pure "as of the last checkpoint" counter -- allocation bumps it immediately. In update_sit_entry_for_alloc(): /* * SSR should never reuse block which is checkpointed * or newly invalidated. */ if (!is_sbi_flag_set(sbi, SBI_CP_DISABLED)) { if (!f2fs_test_and_set_bit(offset, se->ckpt_valid_map)) { se->ckpt_valid_blocks++; ... } if (!f2fs_test_bit(offset, se->ckpt_valid_map)) { se->ckpt_valid_blocks += del; ... So every block written after the last checkpoint (LFS or SSR, and with checkpointing disabled too, via the second hunk) is counted in ckpt_valid_blocks right away, and update_sit_entry_for_del() only decrements it for blocks that are *not* set in ckpt_valid_map. As a result ckpt_valid_blocks is exactly the population count of ckpt_valid_map | cur_valid_map, which is precisely the target_map that __next_free_blkoff() refuses to allocate from: for (i = 0; i < entries; i++) target_map[i] = ckpt_map[i] | cur_map[i]; return __find_rev_next_zero_bit(target_map, BLKS_PER_SEG(sbi), start); So "usable - ckpt_valid_blocks" is the number of blocks SSR can actually hand out in that segment, not an upper bound that includes freshly written blocks. If you still see a case where the two diverge I would very much like to know -- that would be a bug in the counter itself. > And, only updating sbi->cib_total_blocks w/ f2fs_update_cib() in checkpoint() > is not enough? since free space in section may change due to SSR allocation > and deletion. right? You are right, and this is the weaker half of the same argument. The per-segment counter is accurate at all times as described above, but sbi->cib_total_blocks is only a snapshot of it taken at checkpoint time, so between two checkpoints the aggregate goes stale: SSR allocation consumes slack that the watermark still credits, and truncation creates slack that it does not yet see. The stale direction that matters is the first one -- it can keep foreground GC skipped after the budget is already spent. My reasoning in v2 was that gcless checkpoints often enough to bound the drift, but "often enough" is not a correctness argument, and I should not have relied on it. >> + if (test_opt(sbi, GCLESS)) { >> + unsigned int sec_blks = CAP_BLKS_PER_SEC(sbi); >> + unsigned int add_secs = READ_ONCE(sbi->cib_total_blocks) / sec_blks; >> + unsigned int room = free_secs < MAIN_SECS(sbi) ? >> + MAIN_SECS(sbi) - free_secs : 0; >> + >> + free_secs += min(add_secs, room); > > A free section can be reused by any DATA or NODE type write, but above GCLESS > "free_secs" may not, e.g. all free_secs are from DATA type, then latter checkpoint > won't write any node into DATA type section w/ SSR. Agreed, this is a real defect. SSR never crosses the node/data boundary: select_policy() restricts the victim search to dirty_segmap[type], and get_ssr_segment() only falls back among the three node types or among the three data types, never between them. So crediting one pooled block count to free_secs is wrong exactly as you describe -- a budget made entirely of data-type slack would still let has_not_enough_free_secs() claim there is room for node writes, and the node allocation would then have to fall back to a real free section that the watermark just pretended existed. I will take both points into account and validate the result carefully before sending the next version. Thanks a lot for taking the time to review this. Thanks, Yonggil ^ permalink raw reply [flat|nested] 4+ messages in thread
end of thread, other threads:[~2026-08-11 5:47 UTC | newest]
Thread overview: 4+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
[not found] <CGME20260707225035epcms2p812b92e6001afb57eb06d98225f1acd87@epcms2p8>
2026-07-07 22:50 ` [PATCH v2 1/2] f2fs: use BIT_ULL() for mount option bits Yonggil Song
[not found] ` <CGME20260707225035epcms2p812b92e6001afb57eb06d98225f1acd87@epcms2p4>
2026-07-07 22:54 ` [PATCH v2 2/2] f2fs: introduce gcless mount option to avoid foreground GC Yonggil Song
2026-08-10 2:10 ` Chao Yu
[not found] ` <CGME20260707225035epcms2p812b92e6001afb57eb06d98225f1acd87@epcms2p2>
2026-08-11 5:47 ` Yonggil Song
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®