* [PATCH v3 3/5] f2fs: cache: wake up f2fs_writeback when exceeding threshold
@ 2026-10-12 9:17 Chao Yu
2026-10-12 9:52 ` Wenjie Qi
0 siblings, 1 reply; 2+ messages in thread
From: Chao Yu @ 2026-10-12 9:17 UTC (permalink / raw)
To: jaegeuk; +Cc: linux-f2fs-devel, linux-kernel, Chao Yu, Daeho Jeong, Wenjie Qi
From: Chao Yu <chao@kernel.org>
This patch introduces cache_wb_dirty_threshold and cache_wb_total_threshold
sysfs nodes under /sys/fs/f2fs/<disk>/, and wakes up the f2fs_writeback
kthread when dirty meta/node caches exceed cache_wb_dirty_threshold and
total cached entries exceed cache_wb_total_threshold; a zero threshold
disables the corresponding check. By default, cache_wb_dirty_threshold is
8192 and cache_wb_total_threshold is 0.
If both thresholds are zero, f2fs_writeback only runs periodically, unless
checkpoint is disabled, as nothing else will flush dirty caches then.
f2fs_writeback won't be woken up unless there are enough dirty caches for
f2fs_write_{meta,node}_caches() to gather. And if thresholds are exceeded
but one round fails to write any caches, e.g. due to cp_error, freezing,
frozen fs, or lock contention, threshold based wakeup is throttled until
a later round makes progress, in order to avoid busy-looping. To detect
progress, introduce a new parameter for f2fs_write_{meta,node}_caches()
and f2fs_writeback_node_caches() to return the number of caches submitted
for writeback.
Thanks Daeho and Wenjie for help and suggestion on this patch.
Suggested-by: Daeho Jeong <daehojeong@google.com>
Suggested-by: Wenjie Qi <qiwenjie@xiaomi.com>
Signed-off-by: Chao Yu <chao@kernel.org>
---
v3:
- fix to throttle cache writeback in f2fs_writeback
- fix to handle chckpoint disable case correctly
Documentation/ABI/testing/sysfs-fs-f2fs | 21 ++++
fs/f2fs/cache.c | 124 ++++++++++++++++++++++--
fs/f2fs/cache.h | 6 ++
fs/f2fs/checkpoint.c | 10 +-
fs/f2fs/f2fs.h | 7 +-
fs/f2fs/node.c | 11 ++-
fs/f2fs/sysfs.c | 8 +-
7 files changed, 171 insertions(+), 16 deletions(-)
diff --git a/Documentation/ABI/testing/sysfs-fs-f2fs b/Documentation/ABI/testing/sysfs-fs-f2fs
index 5b196806b3bc..3d2e78a8bb7d 100644
--- a/Documentation/ABI/testing/sysfs-fs-f2fs
+++ b/Documentation/ABI/testing/sysfs-fs-f2fs
@@ -1027,3 +1027,24 @@ Contact: "Chao Yu" <chao@kernel.org>
Description: This is a writable entry to control writeback interval of
f2fs_writeback-x:y, the range is [100, 30000], by default the value
is 5000, unit is ms.
+
+What: /sys/fs/f2fs/<disk>/cache_wb_dirty_threshold
+Date: October 2026
+Contact: "Chao Yu" <chao@kernel.org>
+Description: This is a writable entry for metadata cache. f2fs_writeback-x:y
+ will be woken up for writeback once the number of dirty meta and
+ node caches reaches this value. 0 disables this check. By default
+ the value is 8192, unit is cache entries.
+
+What: /sys/fs/f2fs/<disk>/cache_wb_total_threshold
+Date: October 2026
+Contact: "Chao Yu" <chao@kernel.org>
+Description: This is a writable entry for metadata cache. f2fs_writeback-x:y
+ will be woken up for writeback only if the total number of cached
+ entries also reaches this value. 0 disables this check. By default
+ the value is 0, unit is cache entries.
+
+ If both cache_wb_dirty_threshold and cache_wb_total_threshold are
+ 0, f2fs_writeback-x:y only runs every cache_wb_interval, unless
+ checkpoint is disabled, in which case it will still be woken up
+ once there are enough dirty caches to be gathered for writeback.
diff --git a/fs/f2fs/cache.c b/fs/f2fs/cache.c
index f385fe928b5b..c53dc4629ac2 100644
--- a/fs/f2fs/cache.c
+++ b/fs/f2fs/cache.c
@@ -81,6 +81,7 @@ bool f2fs_mark_cache_dirty(struct f2fs_cached_block *entry)
f2fs_cache_update_tag(entry, F2FS_CACHE_TAG_NONE,
F2FS_CACHE_TAG_DIRTY);
inc_cache_count(cache->sbi, type);
+ f2fs_wake_up_cache_wb(cache->sbi);
return true;
}
@@ -681,6 +682,74 @@ unsigned long f2fs_shrink_node_cache(struct f2fs_sb_info *sbi,
return f2fs_do_shrink_cache(NODE_CACHE(sbi), nr_to_scan);
}
+static inline unsigned long f2fs_total_cached_entries(struct f2fs_sb_info *sbi)
+{
+ unsigned long total = READ_ONCE(META_CACHE(sbi)->num_entries) +
+ READ_ONCE(NODE_CACHE(sbi)->num_entries);
+#ifdef CONFIG_F2FS_FS_COMPRESSION
+ if (test_opt(sbi, COMPRESS_CACHE))
+ total += READ_ONCE(COMPRESS_CACHE(sbi)->num_entries);
+#endif
+ return total;
+}
+
+static inline s64 f2fs_nr_dirty_caches(struct f2fs_sb_info *sbi)
+{
+ return get_nr_caches(sbi, F2FS_DIRTY_META) +
+ get_nr_caches(sbi, F2FS_DIRTY_NODES);
+}
+
+/*
+ * f2fs_write_{meta,node}_caches() skip writeback until there are enough
+ * dirty caches to gather, so don't wake up f2fs_writeback for nothing.
+ */
+static inline bool f2fs_cache_wb_collectable(struct f2fs_sb_info *sbi,
+ s64 nr_dirty, int type)
+{
+ return nr_dirty && nr_dirty >= nr_caches_to_skip(sbi, type);
+}
+
+static bool f2fs_cache_wb_exceeded(struct f2fs_sb_info *sbi)
+{
+ struct f2fs_cache_kthread *cache_thread = &sbi->cache_thread;
+ unsigned int total_threshold, dirty_threshold;
+ s64 nr_meta, nr_node;
+
+ total_threshold = READ_ONCE(cache_thread->cache_wb_total_threshold);
+ dirty_threshold = READ_ONCE(cache_thread->cache_wb_dirty_threshold);
+
+ /*
+ * Both thresholds are disabled, rely on periodic writeback only,
+ * unless checkpoint is disabled, as nothing else will flush dirty
+ * caches then.
+ */
+ if (!total_threshold && !dirty_threshold &&
+ !is_sbi_flag_set(sbi, SBI_CP_DISABLED))
+ return false;
+
+ if (total_threshold &&
+ f2fs_total_cached_entries(sbi) < total_threshold)
+ return false;
+
+ nr_meta = get_nr_caches(sbi, F2FS_DIRTY_META);
+ nr_node = get_nr_caches(sbi, F2FS_DIRTY_NODES);
+
+ if (dirty_threshold && nr_meta + nr_node < dirty_threshold)
+ return false;
+
+ return f2fs_cache_wb_collectable(sbi, nr_meta, META) ||
+ f2fs_cache_wb_collectable(sbi, nr_node, NODE);
+}
+
+static bool f2fs_should_wake_up_cache_wb(struct f2fs_sb_info *sbi)
+{
+ /* last round made no progress, wait for periodic writeback */
+ if (READ_ONCE(sbi->cache_thread.cache_wb_throttled))
+ return false;
+
+ return f2fs_cache_wb_exceeded(sbi);
+}
+
static int f2fs_cache_writeback_kthread(void *data)
{
struct f2fs_sb_info *sbi = data;
@@ -690,15 +759,27 @@ static int f2fs_cache_writeback_kthread(void *data)
set_freezable();
while (!kthread_should_stop()) {
- unsigned int interval = cache_thread->cache_wb_interval;
+ unsigned int interval = READ_ONCE(cache_thread->cache_wb_interval);
+ long nr_meta = 0, nr_node = 0;
+ s64 nr_dirty;
wait_event_freezable_timeout(*wq,
- kthread_should_stop(),
+ kthread_should_stop() ||
+ f2fs_should_wake_up_cache_wb(sbi),
msecs_to_jiffies(interval));
if (kthread_should_stop())
break;
+ /*
+ * If thresholds are exceeded, throttle threshold based wakeup
+ * until a round makes progress, otherwise we may busy-loop if
+ * this round can not write anything, e.g. on cp_error, freezing,
+ * frozen fs, or lock contention.
+ */
+ WRITE_ONCE(cache_thread->cache_wb_throttled,
+ f2fs_cache_wb_exceeded(sbi));
+
if (f2fs_readonly(sbi->sb))
continue;
@@ -711,14 +792,41 @@ static int f2fs_cache_writeback_kthread(void *data)
if (!sb_start_write_trylock(sbi->sb))
continue;
- f2fs_write_meta_caches(sbi);
- f2fs_write_node_caches(sbi);
+ nr_dirty = f2fs_nr_dirty_caches(sbi);
+
+ f2fs_write_meta_caches(sbi, &nr_meta);
+ f2fs_write_node_caches(sbi, &nr_node);
sb_end_write(sbi->sb);
+
+ /*
+ * Don't rely on the delta of dirty caches only, as caches may
+ * be dirtied concurrently; also count in caches cleaned by
+ * others, e.g. checkpoint triggered by f2fs_balance_fs_bg().
+ */
+ if (nr_meta || nr_node || f2fs_nr_dirty_caches(sbi) < nr_dirty)
+ WRITE_ONCE(cache_thread->cache_wb_throttled, false);
+
+ cond_resched();
}
return 0;
}
+void f2fs_wake_up_cache_wb(struct f2fs_sb_info *sbi)
+{
+ struct f2fs_cache_kthread *cache_thread = &sbi->cache_thread;
+
+ /* pairs with smp_store_release() in f2fs_start_cache_wb_thread() */
+ if (!smp_load_acquire(&cache_thread->cache_wb_task))
+ return;
+
+ if (!f2fs_should_wake_up_cache_wb(sbi))
+ return;
+
+ if (wq_has_sleeper(&cache_thread->cache_wb_wq))
+ wake_up(&cache_thread->cache_wb_wq);
+}
+
int f2fs_start_cache_wb_thread(struct f2fs_sb_info *sbi)
{
struct f2fs_cache_kthread *cache_thread = &sbi->cache_thread;
@@ -731,6 +839,9 @@ int f2fs_start_cache_wb_thread(struct f2fs_sb_info *sbi)
init_waitqueue_head(&cache_thread->cache_wb_wq);
cache_thread->cache_wb_interval = DEF_DIRTY_CACHE_TIMEOUT;
+ cache_thread->cache_wb_dirty_threshold = DEF_CACHE_WB_DIRTY_THRESH;
+ cache_thread->cache_wb_total_threshold = 0;
+ cache_thread->cache_wb_throttled = false;
snprintf(name, sizeof(name), "f2fs_writeback-%u:%u",
MAJOR(dev), MINOR(dev));
@@ -738,7 +849,8 @@ int f2fs_start_cache_wb_thread(struct f2fs_sb_info *sbi)
if (IS_ERR(task))
return PTR_ERR(task);
- cache_thread->cache_wb_task = task;
+ /* pairs with smp_load_acquire() in f2fs_wake_up_cache_wb() */
+ smp_store_release(&cache_thread->cache_wb_task, task);
return 0;
}
@@ -750,5 +862,5 @@ void f2fs_stop_cache_wb_thread(struct f2fs_sb_info *sbi)
return;
kthread_stop(cache_thread->cache_wb_task);
- cache_thread->cache_wb_task = NULL;
+ WRITE_ONCE(cache_thread->cache_wb_task, NULL);
}
diff --git a/fs/f2fs/cache.h b/fs/f2fs/cache.h
index 3df3a562c46f..5408727dd321 100644
--- a/fs/f2fs/cache.h
+++ b/fs/f2fs/cache.h
@@ -234,13 +234,19 @@ unsigned long f2fs_shrink_node_cache(struct f2fs_sb_info *sbi,
#define MIN_DIRTY_CACHE_TIMEOUT 100
#define MAX_DIRTY_CACHE_TIMEOUT 30000
+#define DEF_CACHE_WB_DIRTY_THRESH (8192)
+
struct f2fs_cache_kthread {
struct task_struct *cache_wb_task;
wait_queue_head_t cache_wb_wq;
unsigned int cache_wb_interval;
+ unsigned int cache_wb_dirty_threshold;
+ unsigned int cache_wb_total_threshold;
+ bool cache_wb_throttled; /* last round made no progress */
};
int f2fs_start_cache_wb_thread(struct f2fs_sb_info *sbi);
void f2fs_stop_cache_wb_thread(struct f2fs_sb_info *sbi);
+void f2fs_wake_up_cache_wb(struct f2fs_sb_info *sbi);
#endif /* _LINUX_F2FS_CACHE_H */
diff --git a/fs/f2fs/checkpoint.c b/fs/f2fs/checkpoint.c
index 2e5e0ee5feb2..fd0a6c03987e 100644
--- a/fs/f2fs/checkpoint.c
+++ b/fs/f2fs/checkpoint.c
@@ -581,10 +581,11 @@ static bool __f2fs_write_meta_cache(struct f2fs_cached_block *entry,
return false;
}
-void f2fs_write_meta_caches(struct f2fs_sb_info *sbi)
+void f2fs_write_meta_caches(struct f2fs_sb_info *sbi, long *nr_written)
{
struct f2fs_lock_context lc;
long nr_to_write = LONG_MAX;
+ long nwritten;
if (unlikely(is_sbi_flag_set(sbi, SBI_POR_DOING)))
return;
@@ -599,8 +600,11 @@ void f2fs_write_meta_caches(struct f2fs_sb_info *sbi)
return;
nr_to_write = adjust_flush_cache_number(sbi, META);
- f2fs_sync_meta_caches(sbi, nr_to_write, false, FS_META_IO);
+ nwritten = f2fs_sync_meta_caches(sbi, nr_to_write, false, FS_META_IO);
f2fs_up_write_trace(&sbi->cp_global_sem, &lc);
+
+ if (nr_written)
+ *nr_written = nwritten;
}
long f2fs_sync_meta_caches(struct f2fs_sb_info *sbi, long nr_to_write,
@@ -1553,7 +1557,7 @@ static int block_operations(struct f2fs_sb_info *sbi)
f2fs_up_write(&sbi->node_write);
atomic_inc(&sbi->wb_sync_req[NODE]);
err = f2fs_writeback_node_caches(sbi, LONG_MAX,
- true, false, FS_CP_NODE_IO);
+ true, false, FS_CP_NODE_IO, NULL);
atomic_dec(&sbi->wb_sync_req[NODE]);
if (err) {
f2fs_up_write(&sbi->node_change);
diff --git a/fs/f2fs/f2fs.h b/fs/f2fs/f2fs.h
index 089a62c054ea..362f06cb398d 100644
--- a/fs/f2fs/f2fs.h
+++ b/fs/f2fs/f2fs.h
@@ -4132,7 +4132,7 @@ int f2fs_truncate_inode_blocks(struct inode *inode, pgoff_t from);
int f2fs_truncate_xattr_node(struct inode *inode);
int f2fs_wait_on_node_caches_writeback(struct f2fs_sb_info *sbi,
unsigned int seq_id);
-int f2fs_write_node_caches(struct f2fs_sb_info *sbi);
+int f2fs_write_node_caches(struct f2fs_sb_info *sbi, long *nr_written);
int f2fs_remove_inode_cache(struct inode *inode);
struct f2fs_cached_block *f2fs_new_inode_cache(struct inode *inode);
struct f2fs_cached_block *f2fs_new_node_cache(struct dnode_of_data *dn, unsigned int ofs);
@@ -4151,7 +4151,8 @@ void f2fs_flush_inline_data(struct f2fs_sb_info *sbi);
int f2fs_fsync_node_caches(struct f2fs_sb_info *sbi, struct inode *inode,
bool atomic, unsigned int *seq_id);
int f2fs_writeback_node_caches(struct f2fs_sb_info *sbi, long nr_to_write,
- bool sync, bool do_balance, enum iostat_type io_type);
+ bool sync, bool do_balance, enum iostat_type io_type,
+ long *nr_written);
int f2fs_build_free_nids(struct f2fs_sb_info *sbi, bool sync, bool mount);
bool f2fs_alloc_nid(struct f2fs_sb_info *sbi, nid_t *nid);
void f2fs_alloc_nid_done(struct f2fs_sb_info *sbi, nid_t nid);
@@ -4317,7 +4318,7 @@ int f2fs_ra_meta_caches(struct f2fs_sb_info *sbi, block_t start, int nrpages,
int type, bool sync);
void f2fs_ra_meta_caches_cond(struct f2fs_sb_info *sbi, pgoff_t index,
unsigned int ra_blocks);
-void f2fs_write_meta_caches(struct f2fs_sb_info *sbi);
+void f2fs_write_meta_caches(struct f2fs_sb_info *sbi, long *nr_written);
long f2fs_sync_meta_caches(struct f2fs_sb_info *sbi, long nr_to_write,
bool sync, enum iostat_type io_type);
void f2fs_add_ino_entry(struct f2fs_sb_info *sbi, nid_t ino, int type);
diff --git a/fs/f2fs/node.c b/fs/f2fs/node.c
index 7b2ff2c3dd03..1863e8f1c859 100644
--- a/fs/f2fs/node.c
+++ b/fs/f2fs/node.c
@@ -2111,7 +2111,8 @@ void f2fs_flush_inline_data(struct f2fs_sb_info *sbi)
}
int f2fs_writeback_node_caches(struct f2fs_sb_info *sbi, long nr_to_write,
- bool sync, bool do_balance, enum iostat_type io_type)
+ bool sync, bool do_balance, enum iostat_type io_type,
+ long *nr_written)
{
pgoff_t index;
struct f2fs_cached_block *entries[F2FS_ONSTACK_CACHES];
@@ -2231,6 +2232,9 @@ int f2fs_writeback_node_caches(struct f2fs_sb_info *sbi, long nr_to_write,
trace_f2fs_write_caches(sbi, nr_to_write, nwritten, NODE);
+ if (nr_written)
+ *nr_written = nwritten;
+
if (unlikely(f2fs_cp_error(sbi)))
return -EIO;
return ret;
@@ -2271,7 +2275,7 @@ int f2fs_wait_on_node_caches_writeback(struct f2fs_sb_info *sbi,
return f2fs_cp_error(sbi) ? -EIO : 0;
}
-int f2fs_write_node_caches(struct f2fs_sb_info *sbi)
+int f2fs_write_node_caches(struct f2fs_sb_info *sbi, long *nr_written)
{
struct blk_plug plug;
long nr_to_write = LONG_MAX;
@@ -2296,7 +2300,8 @@ int f2fs_write_node_caches(struct f2fs_sb_info *sbi)
nr_to_write = adjust_flush_cache_number(sbi, NODE);
blk_start_plug(&plug);
- f2fs_writeback_node_caches(sbi, nr_to_write, false, true, FS_NODE_IO);
+ f2fs_writeback_node_caches(sbi, nr_to_write, false, true, FS_NODE_IO,
+ nr_written);
blk_finish_plug(&plug);
return 0;
}
diff --git a/fs/f2fs/sysfs.c b/fs/f2fs/sysfs.c
index afce74744d4d..95f8dc0c4218 100644
--- a/fs/f2fs/sysfs.c
+++ b/fs/f2fs/sysfs.c
@@ -1019,7 +1019,9 @@ static ssize_t f2fs_sbi_store(struct f2fs_attr *a,
a->struct_type == GC_THREAD);
bool thread_entry = !strcmp(a->attr.name, "ckpt_thread_ioprio") ||
!strcmp(a->attr.name, "critical_task_priority") ||
- !strcmp(a->attr.name, "cache_wb_interval");
+ !strcmp(a->attr.name, "cache_wb_interval") ||
+ !strcmp(a->attr.name, "cache_wb_dirty_threshold") ||
+ !strcmp(a->attr.name, "cache_wb_total_threshold");
if (gc_entry || thread_entry) {
if (!down_read_trylock(&sbi->sb->s_umount))
@@ -1363,6 +1365,8 @@ ATGC_INFO_RW_ATTR(atgc_age_threshold, age_threshold);
/* WB_THREAD ATTR */
WB_THREAD_RW_ATTR(cache_wb_interval, cache_wb_interval);
+WB_THREAD_RW_ATTR(cache_wb_dirty_threshold, cache_wb_dirty_threshold);
+WB_THREAD_RW_ATTR(cache_wb_total_threshold, cache_wb_total_threshold);
F2FS_GENERAL_RO_ATTR(dirty_segments);
F2FS_GENERAL_RO_ATTR(free_segments);
@@ -1552,6 +1556,8 @@ static struct attribute *f2fs_attrs[] = {
ATTR_LIST(adjust_lock_priority),
ATTR_LIST(critical_task_priority),
ATTR_LIST(cache_wb_interval),
+ ATTR_LIST(cache_wb_dirty_threshold),
+ ATTR_LIST(cache_wb_total_threshold),
NULL,
};
ATTRIBUTE_GROUPS(f2fs);
--
2.49.0
^ permalink raw reply [flat|nested] 2+ messages in thread
* Re: [PATCH v3 3/5] f2fs: cache: wake up f2fs_writeback when exceeding threshold
2026-10-12 9:17 [PATCH v3 3/5] f2fs: cache: wake up f2fs_writeback when exceeding threshold Chao Yu
@ 2026-10-12 9:52 ` Wenjie Qi
0 siblings, 0 replies; 2+ messages in thread
From: Wenjie Qi @ 2026-10-12 9:52 UTC (permalink / raw)
To: Chao Yu; +Cc: Jaegeuk Kim, linux-f2fs-devel, linux-kernel, qiwenjie, qwjhust
Hi Chao,
> f2fs_writeback won't be woken up unless there are enough dirty caches for
> f2fs_write_{meta,node}_caches() to gather.
The collectable-cache check and no-progress throttling address my concerns.
Reviewed-by: Wenjie Qi <qiwenjie@xiaomi.com>
Best regards,
Wenjie
^ permalink raw reply [flat|nested] 2+ messages in thread
end of thread, other threads:[~2026-10-12 9:52 UTC | newest]
Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-10-12 9:17 [PATCH v3 3/5] f2fs: cache: wake up f2fs_writeback when exceeding threshold Chao Yu
2026-10-12 9:52 ` Wenjie Qi
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®