[PATCH] writeback: Avoid postponing periodic data writeback indefinitely

From: Jan Kara

Date: Mon Oct 26 2026 - 11:16:15 EST


Hongyu Jin has reported that f2fs periodically calls
sync_inode_metadata() for some inodes. The logic in
writeback_single_inode() thus periodically calls to redirty_tail() and
the inode's dirtied_when timestamp never gets old enough for the inode
to be eligible for periodic data writeback. Thus dirty data from it is
only ever written on explicit request (fsync(2), sync(2), unmount) or
on dirty limit pressure.

The redirty_tail() logic in writeback_single_inode() actually should not
be necessary. Since this function is used only for once-off writeback,
there's no need for any livelock protection among inodes and
writeback_single_inode() is only responsible for demoting inode to "less
dirty" list based on dirty bits cleared as a result of writeback. So
remove the redirty_tail() logic and add a comment explaining the rules.

Signed-off-by: Jan Kara <jack@xxxxxxx>
---
fs/fs-writeback.c | 35 ++++++++++++++++++++++-------------
1 file changed, 22 insertions(+), 13 deletions(-)

diff --git a/fs/fs-writeback.c b/fs/fs-writeback.c
index 58a6403780aa..de86c9055ffb 100644
--- a/fs/fs-writeback.c
+++ b/fs/fs-writeback.c
@@ -1946,22 +1946,31 @@ static int writeback_single_inode(struct inode *inode,
* as it can be finally deleted at this moment.
*/
if (!(inode_state_read(inode) & I_FREEING)) {
+ enum inode_state_flags_enum istate = inode_state_read(inode);
+
/*
- * If the inode is now fully clean, then it can be safely
- * removed from its writeback list (if any). Otherwise the
- * flusher threads are responsible for the writeback lists.
+ * __mark_inode_dirty() is responsible for promoting inode to
+ * the proper list (b_attached -> b_dirty_time -> b_dirty).
+ * Here we are responsible for demoting inode after some dirty
+ * bits were cleared.
+ *
+ * If the inode is now fully clean, then it can be safely moved
+ * to b_attached list. If the inode is dirty, we don't want to
+ * touch it - it should be already put to the dirty list by
+ * __mark_inode_dirty() and we'd just unnecessarily clobber the
+ * dirtied_when stamp (which is particularly problematic when
+ * we were called from sync_inode_metadata()). We also cannot
+ * touch the dirty list association when I_SYNC_QUEUED is set
+ * and flush worker is responsible for it. Otherwise move the
+ * inode to b_dirty_time list as that's the only remaining
+ * possibility.
*/
- if (!(inode_state_read(inode) & I_DIRTY_ALL))
+ if (!(istate & I_DIRTY_ALL))
inode_cgwb_move_to_attached(inode, wb);
- else if (!(inode_state_read(inode) & I_SYNC_QUEUED)) {
- if ((inode_state_read(inode) & I_DIRTY))
- redirty_tail_locked(inode, wb);
- else if (inode_state_read(inode) & I_DIRTY_TIME) {
- inode->dirtied_when = jiffies;
- inode_io_list_move_locked(inode,
- wb,
- &wb->b_dirty_time);
- }
+ else if (!(istate & I_SYNC_QUEUED) && !(istate & I_DIRTY)) {
+ WARN_ON_ONCE(!(istate & I_DIRTY_TIME));
+ inode->dirtied_when = jiffies;
+ inode_io_list_move_locked(inode, wb, &wb->b_dirty_time);
}
}

--
2.51.0


--rzkagmlapmodioo3--