mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [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

* [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

* 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®