[PATCH v3 2/2] writeback: kick writeback on a cgwb replaced in cgwb_create()
From: Liz Fong-Jones
Date: Mon Sep 28 2026 - 21:41:09 EST
When a live memcg's blkcg association changes, cgwb_create() replaces
its killed wb in bdi->cgwb_tree. The old wb is then out of foreign
flushes' reach, but its inodes can still be dirty, and they only move to
the new wb as they are written back (see wbc_attach_and_unlock_inode()),
which may not start for a while.
Start writeback on the replaced wb after the takeover. cgwb_create()
can run with interrupts disabled (folio_account_dirtied() ->
inode_attach_wb()), so do it after dropping cgwb_lock, and make
wb_wakeup() irq-safe.
Suggested-by: Tejun Heo <tj@xxxxxxxxxx>
Assisted-by: Claude:claude-opus-5-5 checkpatch sparse
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Liz Fong-Jones <lizf@xxxxxxxxxxxx>
---
fs/fs-writeback.c | 8 +++++---
include/linux/backing-dev.h | 1 +
mm/backing-dev.c | 15 ++++++++++++++-
3 files changed, 20 insertions(+), 4 deletions(-)
diff --git a/fs/fs-writeback.c b/fs/fs-writeback.c
index d26b2cf05283..b9d69ec0649c 100644
--- a/fs/fs-writeback.c
+++ b/fs/fs-writeback.c
@@ -131,10 +131,12 @@ static bool inode_io_list_move_locked(struct inode *inode,
static void wb_wakeup(struct bdi_writeback *wb)
{
- spin_lock_irq(&wb->work_lock);
+ unsigned long flags;
+
+ spin_lock_irqsave(&wb->work_lock, flags);
if (test_bit(WB_registered, &wb->state))
mod_delayed_work(bdi_wq, &wb->dwork, 0);
- spin_unlock_irq(&wb->work_lock);
+ spin_unlock_irqrestore(&wb->work_lock, flags);
}
/*
@@ -1334,7 +1336,7 @@ static unsigned long get_nr_dirty_pages(void)
get_nr_dirty_inodes();
}
-static void wb_start_writeback(struct bdi_writeback *wb, enum wb_reason reason)
+void wb_start_writeback(struct bdi_writeback *wb, enum wb_reason reason)
{
if (!wb_has_dirty_io(wb))
return;
diff --git a/include/linux/backing-dev.h b/include/linux/backing-dev.h
index f7ef5895625a..7d5e2896711e 100644
--- a/include/linux/backing-dev.h
+++ b/include/linux/backing-dev.h
@@ -37,6 +37,7 @@ void bdi_unregister(struct backing_dev_info *bdi);
struct backing_dev_info *bdi_alloc(int node_id);
void wb_start_background_writeback(struct bdi_writeback *wb);
+void wb_start_writeback(struct bdi_writeback *wb, enum wb_reason reason);
void wb_workfn(struct work_struct *work);
void wb_wait_for_completion(struct wb_completion *done);
diff --git a/mm/backing-dev.c b/mm/backing-dev.c
index 839a3c476c5b..0b09c66eafc6 100644
--- a/mm/backing-dev.c
+++ b/mm/backing-dev.c
@@ -676,7 +676,7 @@ static int cgwb_create(struct backing_dev_info *bdi,
struct mem_cgroup *memcg;
struct cgroup_subsys_state *blkcg_css;
struct list_head *memcg_cgwb_list, *blkcg_cgwb_list;
- struct bdi_writeback *wb, *old_wb;
+ struct bdi_writeback *wb, *old_wb, *kick_wb = NULL;
void __rcu **slot;
unsigned long flags;
int ret = 0;
@@ -748,6 +748,8 @@ static int cgwb_create(struct backing_dev_info *bdi,
old_wb = radix_tree_deref_slot_protected(slot, &cgwb_lock);
if (wb_dying(old_wb)) {
radix_tree_replace_slot(&bdi->cgwb_tree, slot, wb);
+ if (wb_tryget(old_wb))
+ kick_wb = old_wb;
ret = 0;
} else {
ret = -EEXIST;
@@ -763,6 +765,17 @@ static int cgwb_create(struct backing_dev_info *bdi,
}
}
spin_unlock_irqrestore(&cgwb_lock, flags);
+
+ /*
+ * The replaced wb is out of foreign flushes' reach but may still have
+ * dirty inodes. Write it back so that they move over to @wb, see
+ * wbc_attach_and_unlock_inode().
+ */
+ if (kick_wb) {
+ wb_start_writeback(kick_wb, WB_REASON_FOREIGN_FLUSH);
+ wb_put(kick_wb);
+ }
+
if (ret) {
if (ret == -EEXIST)
ret = 0;
--
2.53.0