* [PATCH v3 0/2] writeback: let foreign flushes reach dying cgwbs
@ 2026-09-29 1:40 Liz Fong-Jones
2026-09-29 1:40 ` [PATCH v3 2/2] writeback: kick writeback on a cgwb replaced in cgwb_create() Liz Fong-Jones
` (2 more replies)
0 siblings, 3 replies; 5+ messages in thread
From: Liz Fong-Jones @ 2026-09-29 1:40 UTC (permalink / raw)
To: Christian Brauner, Jan Kara, Tejun Heo
Cc: Alexander Viro, Jens Axboe, Andrew Morton, Johannes Weiner,
Roman Gushchin, Shakeel Butt, Xin Yin, linux-fsdevel, linux-mm,
cgroups, linux-kernel, ian, Liz Fong-Jones
At Honeycomb, a container that reads from Kafka and writes columnar
files to a host volume stalls for 30-60s after each deploy replaces it
(v6.18). We're increasingly confident this is the cause: the stall
looks the same as in the reproducer (little CPU use, lag growing
linearly and then recovering), and the mitigation it predicts, moving
our final syncfs after the last write, worked in production (below).
We haven't caught it with probes in production yet.
Patch 1 keeps killed wbs in bdi->cgwb_tree until they are released, so
foreign flushes can still reach them. Patch 2 kicks writeback on a
killed wb that a live memcg's new wb replaces after a blkcg association
change, which patch 1 leaves out of reach.
Based on vfs-7.4.misc, next to Xin Yin's 168a8c13159c, which fixes the
sizing but not the lookup of a removed cgroup's wb. Each helps without
the other; this series also applies to mainline. On mainline
(fd179f8a05be), without 168a8c13159c, the test from the changelog gave
52.8s, 73.3s and 5.2s worst lag before this series and 1.4s, 1.0s and
1.0s with it.
Syncing before the removal isn't enough: cleanup_offline_cgwbs_workfn()
skips a dying wb with any dirty inode and only retries on the next memcg
offline. On ext4, syncfs can leave inodes I_DIRTY_SYNC: at 5000 files
one syncfs still stalled in 7 of 9 runs (19-26s worst lag), a second
syncfs avoided it in 5 of 5, and with this series one syncfs lagged at
most 1.3s in 2 of 2. On XFS one syncfs left the wb clean in 5 of 5 runs,
but a single 4KiB append from the old cgroup after it stranded the wb
again (15-28s worst lag, 5 of 5; 1.0s in 2 of 2 with this series). In
production, our writer synced after closing its data files but before
writing its final Kafka offset to a small shutdown file, and moving the
syncfs after that write mitigated the stall for us on XFS. In the
reproducer, a 14-byte file written after the syncfs stranded the wb the
same way, whether created directly or via a temp file and rename (16-29s
worst lag, 7 of 7; with this series, 1.0-1.2s in 2 of 2 created
directly); an empty file did not.
The harness that produced the numbers (paced writer, lag per second,
MODE=alive control) is at
https://gist.github.com/lizthegrey/2209d831930588f63076bdc0ac7b78e2, and
a minimal recipe is below. Settings for all numbers: MEM_MAX=max
POD_MAX=4G RATE_MBPS=250, plus NFILES=5000 SYNC_OLD=1 (or 2) and
DIRTY_AFTER=1 or SHUTDOWN_FILE=offset|offset_rename|empty for the syncfs
runs and NFILES=500000 OLD_SECS=120 SYNC_OLD=1 POD_HOP=10 for the
500k-file runs.
Also reproduced unpatched on bare-metal arm64 with Ubuntu's 7.0 kernel
(ext4 on a loop device): 12.9s worst lag with the old cgroup removed,
2.5s with it kept.
Tested: arm64 KVM guests (virtme-ng), ext4 and XFS on virtio. No KASAN,
lockdep, PROVE_RCU, DEBUG_LIST, DEBUG_OBJECTS_WORK, DEBUG_ATOMIC_SLEEP
or DEBUG_PAGEALLOC reports, including a stress run toggling io on the
parent of two writers 40 times so their wbs are killed and replaced in
the same slot (160 takeovers, each followed by patch 2's kick, counted
with bpftrace). W=1 and checkpatch --strict clean, no new sparse
warnings. Patch 1 is under CONFIG_CGROUP_WRITEBACK; patch 2's
wb_wakeup() and wb_start_writeback() changes are not, so v3 was also
built with it disabled (arm64 defconfig without BLK_CGROUP, W=1 on the
touched files, no new warnings).
Not tested: x86 runtime, this series in production, linux-next.
Workaround until this lands: syncfs after the old container's last
write (twice on ext4). Lowering vm.dirty_expire_centisecs from 3000 to
500 cut the worst lag on unpatched mainline to 4.8s, 4.6s and 14.3s, but
did not remove the stall.
Related: without 168a8c13159c the flush is sized from the dead memcg's
own dirty pages, so this only helps while it has some. f6988c90671e
(mainline) also matters: on XFS with 500k synced inodes and the pod
cgroup removed 10s later, the handover was slow enough without it that
the replacement fell 21-22s behind (vfs-7.4.misc), against 1.0-1.2s
with it (mainline).
Patch 2 was tested with a live cgroup whose io controller is disabled on
its parent (its wb is killed and replaced) while a sibling keeps
appending to its files, on XFS with 5000 files. With the kick, the
replaced wb's inodes start moving over right at the takeover; without
it, they started 12s late in one of three instrumented runs. The sibling
still stalled in most of six runs either way (worst lag 20.3s, 1.2s,
10.8s, 28.5s, 3.8s and 17.0s without patch 2; 2.8s, 22.3s, 25.7s, 17.3s,
1.5s and 19.5s with it), because writing back the old wb's backlog took
up to 27s and the sibling's foreign flushes reached the new wb in the
meantime. Should foreign flushes also reach a replaced wb until it is
clean, or is that not worth it for this case?
Developed with Claude Opus 5.5, which wrote the code, the reproducers
and first drafts of this text, and ran the builds and VM tests. Claude
Fable 5.1 reviewed the code and the claims in this text before v2 and v3
were posted. I drove the investigation from the production symptoms,
designed the experiments and controls (repeated runs, parent-commit
baselines, testing this patch on its own), and reviewed the analysis,
code and results.
Minimal recipe:
#!/bin/bash
# As root, cgroup v2, in a directory on ext4/xfs/btrfs ($DIR).
cg=/sys/fs/cgroup/wbmini
mkdir $cg && echo +memory > $cg/cgroup.subtree_control
echo 4G > $cg/memory.max
mkdir $cg/old $cg/new
append() { # append $1 blocks of $2 to each of 1000 files
for i in $(seq 1000); do
dd if=/dev/zero of=$DIR/f$i bs=$2 count=$1 \
oflag=append conv=notrunc status=none
done
}
(echo $BASHPID > $cg/old/cgroup.procs
append 8 1M # old owner writes the files...
append 1 64k) # ...and exits with all of them dirty
rmdir $cg/old # its cgroup goes away while they are dirty
bpftrace -e 'kretprobe:cgroup_writeback_by_id { @ret[(int32)retval] = count(); }' &
sleep 3
time (echo $BASHPID > $cg/new/cgroup.procs
append 4 1M) # replacement keeps appending to the same files
kill -INT $!; wait
rmdir $cg/new $cg
# Unpatched: many @ret[-2] (-ENOENT) and the append stalls.
# Patched: @ret[0] only.
---
Changes in v3:
- Use wb_dying(), scoped_guard() with the offline_node list_del moved
into it, and {}; filter dying wbs right after the lookup in
cgwb_create(); comment wording (Tejun)
- New patch 2: kick writeback on a wb replaced in cgwb_create() (Tejun)
- Lead patch 1's changelog with the problem and its trigger (Andrew)
- Link to v2: https://patch.msgid.link/20260928-wb-dying-cgwb-flush-v2-1-56b54cda74f2@honeycomb.io
Changes in v2:
- Keep killed wbs in bdi->cgwb_tree until release instead of walking
bdi->wb_list (Tejun)
- Drop Fixes: and Cc: stable (Tejun)
- Link to v1: https://patch.msgid.link/20260926-wb-dying-cgwb-flush-v1-1-a8d898085a3a@honeycomb.io
---
Liz Fong-Jones (2):
writeback: let foreign flushes reach dying cgwbs
writeback: kick writeback on a cgwb replaced in cgwb_create()
fs/fs-writeback.c | 8 +--
include/linux/backing-dev-defs.h | 13 ++++-
include/linux/backing-dev.h | 3 +-
mm/backing-dev.c | 102 +++++++++++++++++++++++++++++++--------
4 files changed, 101 insertions(+), 25 deletions(-)
---
base-commit: 168a8c13159c6e3f0f08da6f8fa2f633a91ba9fd
change-id: 20260926-wb-dying-cgwb-flush-06e260e2abad
Best regards,
--
Liz Fong-Jones <lizf@honeycomb.io>
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v3 2/2] writeback: kick writeback on a cgwb replaced in cgwb_create()
2026-09-29 1:40 [PATCH v3 0/2] writeback: let foreign flushes reach dying cgwbs Liz Fong-Jones
@ 2026-09-29 1:40 ` Liz Fong-Jones
2026-09-29 1:40 ` [PATCH v3 1/2] writeback: let foreign flushes reach dying cgwbs Liz Fong-Jones
2026-09-29 19:32 ` [PATCH v3 0/2] " Tejun Heo
2 siblings, 0 replies; 5+ messages in thread
From: Liz Fong-Jones @ 2026-09-29 1:40 UTC (permalink / raw)
To: Christian Brauner, Jan Kara, Tejun Heo
Cc: Alexander Viro, Jens Axboe, Andrew Morton, Johannes Weiner,
Roman Gushchin, Shakeel Butt, Xin Yin, linux-fsdevel, linux-mm,
cgroups, linux-kernel, ian, Liz Fong-Jones
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@kernel.org>
Assisted-by: Claude:claude-opus-5-5 checkpatch sparse
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Liz Fong-Jones <lizf@honeycomb.io>
---
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
^ permalink raw reply [flat|nested] 5+ messages in thread
* [PATCH v3 1/2] writeback: let foreign flushes reach dying cgwbs
2026-09-29 1:40 [PATCH v3 0/2] writeback: let foreign flushes reach dying cgwbs Liz Fong-Jones
2026-09-29 1:40 ` [PATCH v3 2/2] writeback: kick writeback on a cgwb replaced in cgwb_create() Liz Fong-Jones
@ 2026-09-29 1:40 ` Liz Fong-Jones
2026-09-29 17:58 ` Tejun Heo
2026-09-29 19:32 ` [PATCH v3 0/2] " Tejun Heo
2 siblings, 1 reply; 5+ messages in thread
From: Liz Fong-Jones @ 2026-09-29 1:40 UTC (permalink / raw)
To: Christian Brauner, Jan Kara, Tejun Heo
Cc: Alexander Viro, Jens Axboe, Andrew Morton, Johannes Weiner,
Roman Gushchin, Shakeel Butt, Xin Yin, linux-fsdevel, linux-mm,
cgroups, linux-kernel, ian, Liz Fong-Jones
When a container is replaced and the new one keeps appending to files
the old one left dirty, the new one can stall in balance_dirty_pages()
for seconds to minutes, with little CPU use and its write lag growing
linearly before it recovers. This matches a 30-60s stall after each
deploy of a container at Honeycomb that reads from Kafka and writes
columnar files to a host volume.
Trigger: a cgroup dirties files and is removed while they are still
dirty, and a sibling keeps appending to the same files under a parent
memory limit. cgwb_kill() takes the removed memcg's wbs out of
bdi->cgwb_tree, but the inodes stay attached to them, so the sibling's
foreign flushes fail with -ENOENT in wb_get_lookup().
Keep killed wbs in bdi->cgwb_tree until they are released, so that
cgroup_writeback_by_id() finds them through the regular lookup. The
creation paths skip dying wbs, and a new wb takes over the slot of a
dying one when a live memcg's blkcg association changes; release only
removes a wb that still owns its slot. The blkcg association check
moves from wb_get_lookup() to the creation side: once a memcg with io
enabled is removed, cgroup_get_e_css() returns an ancestor's io css
and its dying wb would not match.
With commit 168a8c13159c ("writeback: size foreign flushes by target
wb dirty pages"), the flush is then sized from the wb's own dirty
pages and writes out what the replacement dirtied.
Test, on top of that commit: a cgroup appends to 1000 files at
250MiB/s and is removed, then a second cgroup keeps appending to the
same files under a 4G parent limit. Worst lag of the second writer
behind schedule over 120s, in three runs:
without this patch 85.3s 46.9s 52.1s
with this patch 1.1s 1.0s 1.2s
Suggested-by: Tejun Heo <tj@kernel.org>
Assisted-by: Claude:claude-opus-5-5 checkpatch sparse
Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Liz Fong-Jones <lizf@honeycomb.io>
---
include/linux/backing-dev-defs.h | 13 +++++-
include/linux/backing-dev.h | 2 +-
mm/backing-dev.c | 89 +++++++++++++++++++++++++++++++---------
3 files changed, 82 insertions(+), 22 deletions(-)
diff --git a/include/linux/backing-dev-defs.h b/include/linux/backing-dev-defs.h
index 4f1084937315..035d25f5b81b 100644
--- a/include/linux/backing-dev-defs.h
+++ b/include/linux/backing-dev-defs.h
@@ -196,7 +196,7 @@ struct backing_dev_info {
struct bdi_writeback wb; /* the root writeback info for this bdi */
struct list_head wb_list; /* list of all wbs */
#ifdef CONFIG_CGROUP_WRITEBACK
- struct radix_tree_root cgwb_tree; /* radix tree of active cgroup wbs */
+ struct radix_tree_root cgwb_tree; /* radix tree of cgroup wbs, incl. killed */
struct mutex cgwb_release_mutex; /* protect shutdown of wb structs */
struct rw_semaphore wb_switch_rwsem; /* no cgwb switch while syncing */
#endif
@@ -229,6 +229,17 @@ static inline bool wb_tryget(struct bdi_writeback *wb)
return true;
}
+/**
+ * wb_tryget_live - try to increment a wb's refcount if it hasn't been killed
+ * @wb: bdi_writeback to get
+ */
+static inline bool wb_tryget_live(struct bdi_writeback *wb)
+{
+ if (wb != &wb->bdi->wb)
+ return percpu_ref_tryget_live(&wb->refcnt);
+ return true;
+}
+
/**
* wb_get - increment a wb's refcount
* @wb: bdi_writeback to get
diff --git a/include/linux/backing-dev.h b/include/linux/backing-dev.h
index c2284466e7aa..f7ef5895625a 100644
--- a/include/linux/backing-dev.h
+++ b/include/linux/backing-dev.h
@@ -222,7 +222,7 @@ wb_get_create_current(struct backing_dev_info *bdi, gfp_t gfp)
rcu_read_lock();
wb = wb_find_current(bdi);
- if (wb && unlikely(!wb_tryget(wb)))
+ if (wb && unlikely(!wb_tryget_live(wb)))
wb = NULL;
rcu_read_unlock();
diff --git a/mm/backing-dev.c b/mm/backing-dev.c
index cecbcf9060a6..839a3c476c5b 100644
--- a/mm/backing-dev.c
+++ b/mm/backing-dev.c
@@ -614,6 +614,12 @@ static void cgwb_release_workfn(struct work_struct *work)
release_work);
struct backing_dev_info *bdi = wb->bdi;
+ scoped_guard(spinlock_irq, &cgwb_lock) {
+ /* a newer wb may have taken the slot, see cgwb_create() */
+ radix_tree_delete_item(&bdi->cgwb_tree, wb->memcg_css->id, wb);
+ list_del(&wb->offline_node);
+ }
+
mutex_lock(&wb->bdi->cgwb_release_mutex);
wb_shutdown(wb);
@@ -627,10 +633,6 @@ static void cgwb_release_workfn(struct work_struct *work)
fprop_local_destroy_percpu(&wb->memcg_completions);
- spin_lock_irq(&cgwb_lock);
- list_del(&wb->offline_node);
- spin_unlock_irq(&cgwb_lock);
-
wb_exit(wb);
bdi_put(bdi);
WARN_ON_ONCE(!list_empty(&wb->b_attached));
@@ -645,11 +647,16 @@ static void cgwb_release(struct percpu_ref *refcnt)
queue_work(cgwb_release_wq, &wb->release_work);
}
+/*
+ * A killed wb stays in bdi->cgwb_tree until it is released or replaced in
+ * cgwb_create(), so that foreign flushes can still find it through
+ * wb_get_lookup(). Inodes attached to it can keep collecting dirty pages
+ * from other memcgs until they are written back and switched away.
+ */
static void cgwb_kill(struct bdi_writeback *wb)
{
lockdep_assert_held(&cgwb_lock);
- WARN_ON(!radix_tree_delete(&wb->bdi->cgwb_tree, wb->memcg_css->id));
list_del(&wb->memcg_node);
list_del(&wb->blkcg_node);
list_add(&wb->offline_node, &offline_cgwbs);
@@ -669,7 +676,8 @@ 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;
+ struct bdi_writeback *wb, *old_wb;
+ void __rcu **slot;
unsigned long flags;
int ret = 0;
@@ -681,6 +689,8 @@ static int cgwb_create(struct backing_dev_info *bdi,
/* look up again under lock and discard on blkcg mismatch */
spin_lock_irqsave(&cgwb_lock, flags);
wb = radix_tree_lookup(&bdi->cgwb_tree, memcg_css->id);
+ if (wb && wb_dying(wb))
+ wb = NULL;
if (wb && wb->blkcg_css != blkcg_css) {
cgwb_kill(wb);
wb = NULL;
@@ -727,8 +737,22 @@ static int cgwb_create(struct backing_dev_info *bdi,
spin_lock_irqsave(&cgwb_lock, flags);
if (test_bit(WB_registered, &bdi->wb.state) &&
blkcg_cgwb_list->next && memcg_cgwb_list->next) {
- /* we might have raced another instance of this function */
- ret = radix_tree_insert(&bdi->cgwb_tree, memcg_css->id, wb);
+ /*
+ * We might have raced another instance of this function. A
+ * dying wb keeps its slot until released; take it over.
+ */
+ slot = radix_tree_lookup_slot(&bdi->cgwb_tree, memcg_css->id);
+ if (!slot) {
+ ret = radix_tree_insert(&bdi->cgwb_tree, memcg_css->id, wb);
+ } else {
+ old_wb = radix_tree_deref_slot_protected(slot, &cgwb_lock);
+ if (wb_dying(old_wb)) {
+ radix_tree_replace_slot(&bdi->cgwb_tree, slot, wb);
+ ret = 0;
+ } else {
+ ret = -EEXIST;
+ }
+ }
if (!ret) {
list_add_tail_rcu(&wb->bdi_node, &bdi->wb_list);
list_add(&wb->memcg_node, memcg_cgwb_list);
@@ -768,10 +792,29 @@ static int cgwb_create(struct backing_dev_info *bdi,
* Try to get the wb for @memcg_css on @bdi. The returned wb has its
* refcount incremented.
*
- * This function uses css_get() on @memcg_css and thus expects its refcnt
- * to be positive on invocation. IOW, rcu_read_lock() protection on
- * @memcg_css isn't enough. try_get it before calling this function.
- *
+ * The wb may have been killed and its blkcg association may be stale. This
+ * is what foreign flushes want: they target the wb that owns the dirty
+ * inodes, which can be a killed one. Use wb_get_create() to get a wb to
+ * attach inodes to.
+ */
+struct bdi_writeback *wb_get_lookup(struct backing_dev_info *bdi,
+ struct cgroup_subsys_state *memcg_css)
+{
+ struct bdi_writeback *wb;
+
+ if (!memcg_css->parent)
+ return &bdi->wb;
+
+ rcu_read_lock();
+ wb = radix_tree_lookup(&bdi->cgwb_tree, memcg_css->id);
+ if (wb && !wb_tryget(wb))
+ wb = NULL;
+ rcu_read_unlock();
+
+ return wb;
+}
+
+/*
* A wb is keyed by its associated memcg. As blkcg implicitly enables
* memcg on the default hierarchy, memcg association is guaranteed to be
* more specific (equal or descendant to the associated blkcg) and thus can
@@ -782,9 +825,13 @@ static int cgwb_create(struct backing_dev_info *bdi,
* both the memcg and blkcg associated with it and verifies the blkcg on
* each lookup. On mismatch, the existing wb is discarded and a new one is
* created.
+ *
+ * This function uses css_get() on @memcg_css and thus expects its refcnt
+ * to be positive on invocation. IOW, rcu_read_lock() protection on
+ * @memcg_css isn't enough. try_get it before calling this function.
*/
-struct bdi_writeback *wb_get_lookup(struct backing_dev_info *bdi,
- struct cgroup_subsys_state *memcg_css)
+static struct bdi_writeback *cgwb_get_live(struct backing_dev_info *bdi,
+ struct cgroup_subsys_state *memcg_css)
{
struct bdi_writeback *wb;
@@ -798,7 +845,7 @@ struct bdi_writeback *wb_get_lookup(struct backing_dev_info *bdi,
/* see whether the blkcg association has changed */
blkcg_css = cgroup_get_e_css(memcg_css->cgroup, &io_cgrp_subsys);
- if (unlikely(wb->blkcg_css != blkcg_css || !wb_tryget(wb)))
+ if (unlikely(wb->blkcg_css != blkcg_css || !wb_tryget_live(wb)))
wb = NULL;
css_put(blkcg_css);
}
@@ -813,8 +860,8 @@ struct bdi_writeback *wb_get_lookup(struct backing_dev_info *bdi,
* @memcg_css: cgroup_subsys_state of the target memcg (must have positive ref)
* @gfp: allocation mask to use
*
- * Try to get the wb for @memcg_css on @bdi. If it doesn't exist, try to
- * create one. See wb_get_lookup() for more details.
+ * Try to get the live wb for @memcg_css on @bdi. If it doesn't exist, try
+ * to create one. See cgwb_get_live() for more details.
*/
struct bdi_writeback *wb_get_create(struct backing_dev_info *bdi,
struct cgroup_subsys_state *memcg_css,
@@ -825,7 +872,7 @@ struct bdi_writeback *wb_get_create(struct backing_dev_info *bdi,
might_alloc(gfp);
do {
- wb = wb_get_lookup(bdi, memcg_css);
+ wb = cgwb_get_live(bdi, memcg_css);
} while (!wb && !cgwb_create(bdi, memcg_css, gfp));
return wb;
@@ -858,8 +905,10 @@ static void cgwb_bdi_unregister(struct backing_dev_info *bdi)
WARN_ON(test_bit(WB_registered, &bdi->wb.state));
spin_lock_irq(&cgwb_lock);
- radix_tree_for_each_slot(slot, &bdi->cgwb_tree, &iter, 0)
- cgwb_kill(*slot);
+ radix_tree_for_each_slot(slot, &bdi->cgwb_tree, &iter, 0) {
+ if (!wb_dying(*slot))
+ cgwb_kill(*slot);
+ }
spin_unlock_irq(&cgwb_lock);
mutex_lock(&bdi->cgwb_release_mutex);
--
2.53.0
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v3 1/2] writeback: let foreign flushes reach dying cgwbs
2026-09-29 1:40 ` [PATCH v3 1/2] writeback: let foreign flushes reach dying cgwbs Liz Fong-Jones
@ 2026-09-29 17:58 ` Tejun Heo
0 siblings, 0 replies; 5+ messages in thread
From: Tejun Heo @ 2026-09-29 17:58 UTC (permalink / raw)
To: Liz Fong-Jones
Cc: Christian Brauner, Jan Kara, Alexander Viro, Jens Axboe,
Andrew Morton, Johannes Weiner, Roman Gushchin, Shakeel Butt,
Xin Yin, linux-fsdevel, linux-mm, cgroups, linux-kernel, ian
Hello, Liz.
On Tue, Sep 29, 2026 at 01:40:49AM +0000, Liz Fong-Jones wrote:
> if (test_bit(WB_registered, &bdi->wb.state) &&
> blkcg_cgwb_list->next && memcg_cgwb_list->next) {
> - /* we might have raced another instance of this function */
> - ret = radix_tree_insert(&bdi->cgwb_tree, memcg_css->id, wb);
> + /*
> + * We might have raced another instance of this function. A
> + * dying wb keeps its slot until released; take it over.
> + */
> + slot = radix_tree_lookup_slot(&bdi->cgwb_tree, memcg_css->id);
> + if (!slot) {
> + ret = radix_tree_insert(&bdi->cgwb_tree, memcg_css->id, wb);
> + } else {
> + old_wb = radix_tree_deref_slot_protected(slot, &cgwb_lock);
> + if (wb_dying(old_wb)) {
> + radix_tree_replace_slot(&bdi->cgwb_tree, slot, wb);
This can also replace the wb of a removed memcg:
1. A cgroup with io enabled is removed. Killing its io css makes
cgroup_get_e_css() return the parent's right away, but
memcg_cgwb_list->next stays set until wb_memcg_offline().
2. In between, wb_get_create() for the memcg, e.g. from
__inode_attach_wb() or inode_switch_wbs(), kills the dirty wb on
blkcg mismatch and a new wb takes over its slot.
3. wb_memcg_offline() kills the new wb. Foreign flushes for the memcg
now find the new wb while the dirty inodes stay on the old one, as
css_is_dying() keeps wbc_attach_and_unlock_inode() from switching
them, so the stall comes back.
Can you also fail the link when the memcg is dying?
if (test_bit(WB_registered, &bdi->wb.state) &&
blkcg_cgwb_list->next && memcg_cgwb_list->next &&
!css_is_dying(memcg_css)) {
CSS_DYING is set before the io css is killed. Testing it here, after
the blkcg lookup and under cgwb_lock, catches every removal that could
have made the old wb replaceable.
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 5+ messages in thread
* Re: [PATCH v3 0/2] writeback: let foreign flushes reach dying cgwbs
2026-09-29 1:40 [PATCH v3 0/2] writeback: let foreign flushes reach dying cgwbs Liz Fong-Jones
2026-09-29 1:40 ` [PATCH v3 2/2] writeback: kick writeback on a cgwb replaced in cgwb_create() Liz Fong-Jones
2026-09-29 1:40 ` [PATCH v3 1/2] writeback: let foreign flushes reach dying cgwbs Liz Fong-Jones
@ 2026-09-29 19:32 ` Tejun Heo
2 siblings, 0 replies; 5+ messages in thread
From: Tejun Heo @ 2026-09-29 19:32 UTC (permalink / raw)
To: Liz Fong-Jones, Jan Kara
Cc: Christian Brauner, Alexander Viro, Jens Axboe, Andrew Morton,
Johannes Weiner, Roman Gushchin, Shakeel Butt, Xin Yin,
linux-fsdevel, linux-mm, cgroups, linux-kernel, ian
Hello, Liz.
On Tue, Sep 29, 2026 at 01:40:49AM +0000, Liz Fong-Jones wrote:
> 1.5s and 19.5s with it), because writing back the old wb's backlog took
> up to 27s and the sibling's foreign flushes reached the new wb in the
> meantime. Should foreign flushes also reach a replaced wb until it is
> clean, or is that not worth it for this case?
I don't think the old wb needs to stay reachable if the new wb takes
over its inodes. Instead of kicking writeback, can the takeover queue a
work item which switches all of the old wb's inodes to the new wb, like
cleanup_offline_cgwb() does for b_attached and b_dirty_time? The switch
carries the dirty and writeback page counts, so foreign flushes reach
the inodes through the new wb and nothing needs to be flushed right
away.
That would also mean switching inodes on b_dirty, b_io and b_more_io
while the flusher may be working on the old wb, which I'm not sure is
safe. Jan, would that be okay?
Thanks.
--
tejun
^ permalink raw reply [flat|nested] 5+ messages in thread
end of thread, other threads:[~2026-09-29 19:32 UTC | newest]
Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-29 1:40 [PATCH v3 0/2] writeback: let foreign flushes reach dying cgwbs Liz Fong-Jones
2026-09-29 1:40 ` [PATCH v3 2/2] writeback: kick writeback on a cgwb replaced in cgwb_create() Liz Fong-Jones
2026-09-29 1:40 ` [PATCH v3 1/2] writeback: let foreign flushes reach dying cgwbs Liz Fong-Jones
2026-09-29 17:58 ` Tejun Heo
2026-09-29 19:32 ` [PATCH v3 0/2] " Tejun Heo
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®