mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 1/2] f2fs: introduce max_write_bio_{folio,entry}_cnt module parameters
@ 2026-10-17  2:03 Chao Yu
  2026-10-17  2:03 ` [PATCH 2/2] f2fs: remove max_atc_write_bio_size Chao Yu
  0 siblings, 1 reply; 2+ messages in thread
From: Chao Yu @ 2026-10-17  2:03 UTC (permalink / raw)
  To: jaegeuk; +Cc: linux-f2fs-devel, linux-kernel, Chao Yu

From: Chao Yu <chao@kernel.org>

Commit 3de6b8094115 ("f2fs: Run f2fs_write_end_io() asynchronously")
introduced max_atc_write_bio_size to offload write bio completion to a
workqueue when f2fs_write_end_io() is called in atomic context.

However, relying on bio->bi_iter.bi_size as the threshold has two main
issues:

1. bio->bi_iter.bi_size is not reliable at bio completion time, as its
   value depends on the underlying storage stack:
   - On a direct blk-mq path (e.g. SCSI or UFS), blk_update_request()
     consumes the bio iterator via bio_advance() before calling
     bio_endio(), so bi_size is already 0 when f2fs_write_end_io() is
     called, and the threshold check is never triggered:
     scsi_end_request() / blk_mq_end_request()
      -> blk_update_request()
        -> bio_advance()          /* bio->bi_iter.bi_size becomes 0 */
        -> bio_endio()
          -> f2fs_write_end_io()  /* sees bi_size == 0 */
   - On a device-mapper / zoned device path (e.g. dm-linear / dm-crypt),
     DM allocates a clone bio and sets clone->bi_iter.bi_size, while the
     original bio's bi_size remains intact when completed:
     dm_submit_bio()
      -> dm_split_and_process_bio()
        -> __split_and_process_bio()
          -> alloc_tio()          /* sets clone->bi_iter.bi_size */
          -> __map_bio()
            -> dm_submit_bio_remap()
              -> submit_bio_noacct(clone)
     blk_update_request()         /* advances clone->bi_iter.bi_size to 0 */
      -> bio_endio(clone)
        -> clone_endio()
          -> dm_io_dec_pending()
            -> dm_io_complete()
              -> __dm_io_complete()
                -> bio_endio(orig_bio)
                  -> f2fs_zone_write_end_io()
                    -> f2fs_write_end_io() /* orig_bio->bi_iter.bi_size intact */

2. With large folios or multi-page bvecs, bi_size (and bi_vcnt) does not
   reflect the actual completion latency:
   - In f2fs_write_end_bio() and f2fs_cache_write_end_bio(), execution
     time is dominated by the completion loop (bio_for_each_folio_all()
     for pagecache folios, or while(entry) for cached metadata blocks).
     The CPU latency in atomic context scales with the number of entries
     in the bio, not its byte size.
   - For large folios, a large bio (e.g. 1MB comprised of a few large
     folios) only iterates a few times in the loop. Throttling by
     bi_size unnecessarily offloads such low-latency bios to the
     workqueue, adding context-switch overhead.
   - Relying on bio->bi_vcnt is also insufficient because the block
     layer (bvec_try_merge_page()) merges physically contiguous
     pages/folios into the same bio_vec segment, so a bio with
     bi_vcnt == 1 can still contain multiple entries.

Furthermore:
- The limit is used to bound the CPU latency of f2fs_write_end_io() in
  atomic context, which is a property of the platform (CPU and storage
  completion path) rather than of each f2fs instance, so a global knob
  set once at boot fits better than a per-superblock sysfs entry.
- Pagecache folios and cached metadata blocks are completed in different
  loops with different per-entry costs, i.e. f2fs_write_end_bio() and
  f2fs_cache_write_end_bio(), so it's better to tune them separately
  rather than sharing a single threshold.

To address this, track the exact number of entries (folios or cached
blocks) queued to a write bio via entry_cnt in struct f2fs_bio, and
introduce two module parameters:

- max_write_bio_folio_cnt: for write bios of pagecache folios, which
  are completed in f2fs_write_end_bio().
- max_write_bio_entry_cnt: for write bios of cached metadata blocks,
  which are completed in f2fs_cache_write_end_bio().

Both parameters are read-only at runtime, they can be set via
f2fs.max_write_bio_{folio,entry}_cnt=<n> in the kernel command line, or
via modprobe, and apply to all f2fs instances. The default value is
UINT_MAX, which means this functionality is disabled by default.

Signed-off-by: Chao Yu <chao@kernel.org>
---
 fs/f2fs/data.c | 28 +++++++++++++++++++++++++++-
 fs/f2fs/f2fs.h |  1 +
 2 files changed, 28 insertions(+), 1 deletion(-)

diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c
index 1b0395b45fb7..3caadb30496a 100644
--- a/fs/f2fs/data.c
+++ b/fs/f2fs/data.c
@@ -36,6 +36,16 @@ static struct kmem_cache *ffs_entry_slab;
 static mempool_t *bio_post_read_ctx_pool;
 static struct bio_set f2fs_bioset;
 
+static unsigned int max_write_bio_folio_cnt = UINT_MAX;
+module_param(max_write_bio_folio_cnt, uint, 0444);
+MODULE_PARM_DESC(max_write_bio_folio_cnt,
+		"Maximum number of folios in a write bio completed in atomic context");
+
+static unsigned int max_write_bio_entry_cnt = UINT_MAX;
+module_param(max_write_bio_entry_cnt, uint, 0444);
+MODULE_PARM_DESC(max_write_bio_entry_cnt,
+		"Maximum number of cache entries in a write bio completed in atomic context");
+
 struct f2fs_folio_state {
 	spinlock_t		state_lock;
 	unsigned int		read_pages_pending;
@@ -390,6 +400,12 @@ static void f2fs_write_end_io_work(struct work_struct *work)
 	f2fs_write_end_bio(bio);
 }
 
+static unsigned int f2fs_max_write_bio_cnt(struct bio *bio)
+{
+	return f2fs_is_cache_bio(bio) ? max_write_bio_entry_cnt :
+					max_write_bio_folio_cnt;
+}
+
 static void f2fs_write_end_io(struct bio *bio)
 {
 	struct f2fs_sb_info *sbi;
@@ -398,7 +414,8 @@ static void f2fs_write_end_io(struct bio *bio)
 
 	sbi = bio->bi_private;
 
-	if (in_atomic() && bio->bi_iter.bi_size > sbi->max_atc_write_bio_size) {
+	if (in_atomic() && (bio->bi_iter.bi_size > sbi->max_atc_write_bio_size ||
+		F2FS_BIO(bio)->entry_cnt > f2fs_max_write_bio_cnt(bio))) {
 		struct work_struct *w;
 
 		w = &container_of(bio, struct f2fs_bio, bio)->work;
@@ -555,6 +572,7 @@ static struct bio *__bio_alloc(struct f2fs_io_info *fio, int npages)
 						fio->type, fio->temp);
 		bio->bi_write_stream = f2fs_io_type_to_write_stream(bdev, fio->type,
 								    fio->temp);
+		F2FS_BIO(bio)->entry_cnt = 0;
 	}
 	iostat_alloc_and_bind_ctx(sbi, bio, NULL);
 
@@ -867,6 +885,9 @@ int f2fs_submit_page_bio(struct f2fs_io_info *fio)
 			fio_folio->index, fio, GFP_NOIO);
 	bio_add_folio_nofail(bio, data_folio, folio_size(data_folio), 0);
 
+	if (!is_read_io(fio->op))
+		F2FS_BIO(bio)->entry_cnt++;
+
 	if (fio->io_wbc && !is_read_io(fio->op))
 		wbc_account_cgroup_owner(fio->io_wbc, fio_folio, PAGE_SIZE);
 
@@ -937,6 +958,7 @@ static void add_bio_entry(struct f2fs_sb_info *sbi, struct bio *bio,
 	bio_get(bio);
 
 	bio_add_folio_nofail(bio, folio, folio_size(folio), 0);
+	F2FS_BIO(bio)->entry_cnt++;
 
 	f2fs_down_write(&io->bio_list_lock);
 	list_add_tail(&be->list, &io->bio_list);
@@ -977,6 +999,7 @@ static int add_ipu_page(struct f2fs_io_info *fio, struct bio **bio,
 					fio_folio->mapping->host,
 					fio_folio->index, fio) &&
 			    bio_add_folio(*bio, folio, folio_size(folio), 0)) {
+				F2FS_BIO(*bio)->entry_cnt++;
 				ret = 0;
 				break;
 			}
@@ -1234,6 +1257,8 @@ void f2fs_submit_page_write(struct f2fs_io_info *fio)
 		goto alloc_new;
 	}
 
+	F2FS_BIO(io->bio)->entry_cnt++;
+
 	if (fio->io_wbc)
 		wbc_account_cgroup_owner(fio->io_wbc, fio->folio,
 				folio_size(fio->folio));
@@ -1340,6 +1365,7 @@ void f2fs_submit_cache_write(struct f2fs_io_info *fio)
 	}
 
 	f2fs_bio_add_cache(fio, io->bio);
+	F2FS_BIO(io->bio)->entry_cnt++;
 
 	io->last_block_in_bio = fio->new_blkaddr;
 
diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h
index 362f06cb398d..4333d88cf076 100644
--- a/fs/f2fs/f2fs.h
+++ b/fs/f2fs/f2fs.h
@@ -1778,6 +1778,7 @@ struct f2fs_gc_kthread {
 struct f2fs_bio {
 	struct work_struct work;
 	struct f2fs_cached_block *entry;
+	unsigned int entry_cnt;
 	struct bio bio;
 };
 
-- 
2.49.0


^ permalink raw reply	[flat|nested] 2+ messages in thread

* [PATCH 2/2] f2fs: remove max_atc_write_bio_size
  2026-10-17  2:03 [PATCH 1/2] f2fs: introduce max_write_bio_{folio,entry}_cnt module parameters Chao Yu
@ 2026-10-17  2:03 ` Chao Yu
  0 siblings, 0 replies; 2+ messages in thread
From: Chao Yu @ 2026-10-17  2:03 UTC (permalink / raw)
  To: jaegeuk; +Cc: linux-f2fs-devel, linux-kernel, Chao Yu

From: Chao Yu <chao@kernel.org>

Commit 3de6b8094115 ("f2fs: Run f2fs_write_end_io() asynchronously")
introduced max_atc_write_bio_size to offload the completion of large
write bios to sbi->wq when f2fs_write_end_io() is called in atomic
context.

However, relying on bio->bi_iter.bi_size has two issues:
1. bi_size is not reliable at completion time: on a direct blk-mq path
   (e.g. SCSI or UFS), blk_update_request() consumes the bio iterator
   via bio_advance() before calling bio_endio(), so bi_size is already 0
   in f2fs_write_end_io(), whereas on a DM / zoned path (e.g. dm-linear)
   only the clone bio is advanced and orig_bio->bi_iter.bi_size stays
   intact.
2. With large folios enabled, folio size may be larger than 4KB, so
   bi_size cannot indicate how many folios are in a bio.

Since max_write_bio_{folio,entry}_cnt module parameters have been
introduced to limit the number of folios or cached blocks completed in
atomic context without depending on the bio iterator, let's remove
max_atc_write_bio_size logic and its sysfs entry.

Signed-off-by: Chao Yu <chao@kernel.org>
---
 Documentation/ABI/testing/sysfs-fs-f2fs | 10 ----------
 fs/f2fs/data.c                          |  4 ++--
 fs/f2fs/f2fs.h                          |  2 --
 fs/f2fs/super.c                         |  1 -
 fs/f2fs/sysfs.c                         |  2 --
 5 files changed, 2 insertions(+), 17 deletions(-)

diff --git a/Documentation/ABI/testing/sysfs-fs-f2fs b/Documentation/ABI/testing/sysfs-fs-f2fs
index 3d2e78a8bb7d..3520c29f3b9c 100644
--- a/Documentation/ABI/testing/sysfs-fs-f2fs
+++ b/Documentation/ABI/testing/sysfs-fs-f2fs
@@ -1004,16 +1004,6 @@ Description:	It can be used to tune priority of f2fs critical task, e.g. f2fs_ck
 		- it requires user has CAP_SYS_NICE capability.
 		- the range is [100, 139], by default the value is 120.
 
-What:		/sys/fs/f2fs/<disk>/max_atc_write_bio_size
-Date:		June 2026
-Contact:	Bart Van Assche <bvanassche@acm.org>
-Description:	Every time a write operation completes f2fs_write_end_io() is
-		called. This function may be called from an atomic context,
-		e.g. from inside an interrupt handler. This attribute controls
-		the maximum size of a write bio that is completed in atomic
-		(atc) context. The default value for this attribute is UINT_MAX
-		which means that this functionality is disabled by default.
-
 What:		/sys/fs/f2fs/<disk>/pinned_area_max_secno
 Date:		August 2026
 Contact:	"Daeho Jeong" <daehojeong@google.com>
diff --git a/fs/f2fs/data.c b/fs/f2fs/data.c
index 3caadb30496a..867024c4c72d 100644
--- a/fs/f2fs/data.c
+++ b/fs/f2fs/data.c
@@ -414,8 +414,8 @@ static void f2fs_write_end_io(struct bio *bio)
 
 	sbi = bio->bi_private;
 
-	if (in_atomic() && (bio->bi_iter.bi_size > sbi->max_atc_write_bio_size ||
-		F2FS_BIO(bio)->entry_cnt > f2fs_max_write_bio_cnt(bio))) {
+	if (in_atomic() &&
+		F2FS_BIO(bio)->entry_cnt > f2fs_max_write_bio_cnt(bio)) {
 		struct work_struct *w;
 
 		w = &container_of(bio, struct f2fs_bio, bio)->work;
diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h
index 4333d88cf076..e3ee28f0201f 100644
--- a/fs/f2fs/f2fs.h
+++ b/fs/f2fs/f2fs.h
@@ -1806,8 +1806,6 @@ struct f2fs_sb_info {
 	struct f2fs_sm_info *sm_info;		/* segment manager */
 
 	/* for bio operations */
-	/* Largest write bio size completed in atomic context (atc). */
-	u32 max_atc_write_bio_size;
 	struct f2fs_bio_info *write_io[NR_PAGE_TYPE];	/* for write bios */
 	/* keep migration IO order for LFS mode */
 	struct f2fs_rwsem io_order_lock;
diff --git a/fs/f2fs/super.c b/fs/f2fs/super.c
index 1664928fedca..10be5888273c 100644
--- a/fs/f2fs/super.c
+++ b/fs/f2fs/super.c
@@ -5183,7 +5183,6 @@ static int f2fs_fill_super(struct super_block *sb, struct fs_context *fc)
 		err = -EINVAL;
 		goto free_sb_buf;
 	}
-	sbi->max_atc_write_bio_size = UINT_MAX;
 
 	INIT_WORK(&sbi->s_error_work, f2fs_record_error_work);
 	memcpy(sbi->errors, raw_super->s_errors, MAX_F2FS_ERRORS);
diff --git a/fs/f2fs/sysfs.c b/fs/f2fs/sysfs.c
index 95f8dc0c4218..67202b7f8293 100644
--- a/fs/f2fs/sysfs.c
+++ b/fs/f2fs/sysfs.c
@@ -1284,7 +1284,6 @@ F2FS_SBI_RW_ATTR(gc_idle_interval, interval_time[GC_TIME]);
 F2FS_SBI_RW_ATTR(umount_discard_timeout, interval_time[UMOUNT_DISCARD_TIMEOUT]);
 F2FS_SBI_RW_ATTR(gc_pin_file_thresh, gc_pin_file_threshold);
 F2FS_SBI_RW_ATTR(gc_reclaimed_segments, gc_reclaimed_segs);
-F2FS_SBI_RW_ATTR(max_atc_write_bio_size, max_atc_write_bio_size);
 F2FS_SBI_GENERAL_RW_ATTR(max_victim_search);
 F2FS_SBI_GENERAL_RW_ATTR(migration_granularity);
 F2FS_SBI_GENERAL_RW_ATTR(migration_window_granularity);
@@ -1535,7 +1534,6 @@ static struct attribute *f2fs_attrs[] = {
 	ATTR_LIST(seq_file_ra_mul),
 	ATTR_LIST(gc_segment_mode),
 	ATTR_LIST(gc_reclaimed_segments),
-	ATTR_LIST(max_atc_write_bio_size),
 	ATTR_LIST(max_fragment_chunk),
 	ATTR_LIST(max_fragment_hole),
 	ATTR_LIST(current_atomic_write),
-- 
2.49.0


^ permalink raw reply	[flat|nested] 2+ messages in thread

end of thread, other threads:[~2026-10-17  2:04 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-17  2:03 [PATCH 1/2] f2fs: introduce max_write_bio_{folio,entry}_cnt module parameters Chao Yu
2026-10-17  2:03 ` [PATCH 2/2] f2fs: remove max_atc_write_bio_size Chao Yu

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®