* [PATCH v3 0/3] writeback: fix race between cgroup_writeback_umount() and inode_switch_wbs()
@ 2026-05-18 13:53 Baokun Li
2026-05-18 13:53 ` [PATCH v3 1/3] " Baokun Li
` (2 more replies)
0 siblings, 3 replies; 8+ messages in thread
From: Baokun Li @ 2026-05-18 13:53 UTC (permalink / raw)
To: linux-fsdevel; +Cc: viro, brauner, jack, tj, linux-kernel
Hi all,
Changes since v2:
* Collect RVB from Jan Kara. (Thanks for your review!)
* Patch 3: switch to wake_up_var() / wait_var_event() to drain
s_isw_nr_in_flight. (Suggested by Christian Brauner and Sashiko)
* Polish comments and changelogs.
Changes since v1:
* Use a simple RCU-based fix (patch 1) that is easy to backport to
older kernels; the per-sb refcount optimization is split out as a
separate performance patch (patch 3). (Suggested by Jan Kara)
v1: https://patch.msgid.link/20260513094829.867648-1-libaokun@linux.alibaba.com
v2: https://patch.msgid.link/20260517142147.3354909-1-libaokun@linux.alibaba.com
======
When a container exits, a race between cgroup_writeback_umount() and
inode_switch_wbs() / cleanup_offline_cgwb() can trigger
"VFS: Busy inodes after unmount" followed by a use-after-free on
percpu counters.
There is a window between inode_prepare_wbs_switch() returning true
(having passed the SB_ACTIVE check and grabbed the inode) and the
subsequent wb_queue_isw() call. If cgroup_writeback_umount() observes
the global isw_nr_in_flight counter as non-zero but flush_workqueue()
finds nothing queued, it returns early -- leaving a held inode
reference that blocks evict_inodes() and a later iput() that hits
freed percpu counters.
Patch 1 closes the race by extending the RCU read-side critical
section to cover the window from inode_prepare_wbs_switch() through
wb_queue_isw(), and adding synchronize_rcu() in the umount path so
that all in-flight switchers complete queueing before
flush_workqueue() runs. rcu_barrier() is intentionally retained so
the same hunk applies cleanly to stable trees that still queue
switches via queue_rcu_work().
Patch 2 removes the now-dead rcu_barrier() that was left over from
the queue_rcu_work() era (replaced by plain queue_work() in commit
e1b849cfa6b6 "writeback: Avoid contention on wb->list_lock when
switching inodes"). This is mainline-only.
Patch 3 replaces the global synchronize_rcu()/flush_workqueue() pair
with a per-sb counter (s_isw_nr_in_flight) plus three small helpers
(cgroup_writeback_pin / cgroup_writeback_unpin /
cgroup_writeback_drain), eliminating the global serialization
penalty. This also reverts the RCU extension from patch 1 since the
per-sb counter makes it unnecessary.
Performance
-----------
Measured on a 16 vCPU QEMU guest, all kernels share the same .config.
Background load: 4 ext4 superblocks each running
while :; do
mkdir /sys/fs/cgroup/<tag>-tmp$N
( echo $BASHPID > <tag>-tmp$N/cgroup.procs
dd if=/dev/zero of=$mp/burner bs=4k count=256 conv=notrunc \
oflag=sync)
rmdir /sys/fs/cgroup/<tag>-tmp$N
done
This drives both inode_switch_wbs() (different cgroups writing the
same inode) and cleanup_offline_cgwb() (dying memcgs), keeping the
global isw_nr_in_flight non-zero throughout the run. Latencies are
wall-clock around umount(8) on a separate target sb; only the target
sb's umount is measured.
Four kernels are compared at each step of the series:
base pre-fix mainline
+race base + patch 1 (race fix, keeps rcu_barrier)
+rmbarrier +race + patch 2 (drop rcu_barrier)
+persb +rmbarrier + patch 3 (per-sb counter)
Target sb runs its own cgwb churn:
p50 p95 p99 max
base 99.7 ms 112.9 ms 112.9 ms 127.2 ms
+race 110.2 ms 153.8 ms 153.8 ms 160.4 ms
+rmbarrier 67.6 ms 88.3 ms 88.3 ms 96.8 ms
+persb 7.9 ms 10.0 ms 10.0 ms 10.1 ms
Idle target umount under cross-sb cgwb-switch pressure:
p50 p95 p99 max
base 92.0 ms 123.5 ms 136.5 ms 141.3 ms
+race 118.8 ms 154.6 ms 164.7 ms 165.3 ms
+rmbarrier 62.7 ms 95.4 ms 108.1 ms 108.6 ms
+persb 5.3 ms 6.9 ms 7.4 ms 7.4 ms
8 concurrent umounts of idle sbs under the same pressure:
p50 p95 p99 max
base 137.5 ms 166.9 ms 166.9 ms 171.3 ms
+race 162.2 ms 183.9 ms 183.9 ms 217.0 ms
+rmbarrier 61.3 ms 99.5 ms 99.5 ms 113.7 ms
+persb 8.1 ms 9.1 ms 9.1 ms 9.5 ms
A no-pressure baseline run (no background load) measures ~5 ms p50
across all four kernels, validating that the methodology has no
systematic bias.
In-kernel cgroup_writeback_umount() cumulative cost across the same
run (bpftrace, ~340 calls covering all four scenarios):
cgroup_writeback_umount() time
base 21240 ms total (~62 ms / call)
+race (rcu_barrier+sync) 24966 ms total (~73 ms / call)
+rmbarrier (synchronize_rcu) 12371 ms total (~36 ms / call)
+persb (per-sb counter) 1.37 ms total ( ~4 us / call)
Under +persb the wait_var_event() condition is true on entry
whenever the target sb has nothing in flight, so synchronize_rcu()
and flush_workqueue() are never called on this path.
Notes:
- Patch 1 adds ~10-27 ms p50 over base by introducing
synchronize_rcu(). This is the cost of closing the race
correctly and is paid by stable backports as well.
- Patch 2 ("drop rcu_barrier()") was expected to be a pure cleanup
on mainline, but actually removes a real wait: rcu_barrier()
drains call_rcu() callbacks from *all* subsystems, and the
cgroup teardown path keeps that pipeline busy under this
workload. Removing it cuts ~43-101 ms p50 on top of patch 1.
- Patch 3 (per-sb counter) replaces the global wait entirely; the
target sb no longer waits for activity on unrelated sbs,
recovering near-baseline latency in all three scenarios.
Comments and questions are, as always, welcome.
Thanks,
Baokun
Baokun Li (3):
writeback: fix race between cgroup_writeback_umount() and
inode_switch_wbs()
writeback: drop now-unnecessary rcu_barrier() in
cgroup_writeback_umount()
writeback: use a per-sb counter to drain inode wb switches at umount
fs/fs-writeback.c | 81 ++++++++++++++++++++++------------
include/linux/fs/super_types.h | 8 ++++
2 files changed, 62 insertions(+), 27 deletions(-)
--
2.43.7
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 1/3] writeback: fix race between cgroup_writeback_umount() and inode_switch_wbs()
2026-05-18 13:53 [PATCH v3 0/3] writeback: fix race between cgroup_writeback_umount() and inode_switch_wbs() Baokun Li
@ 2026-05-18 13:53 ` Baokun Li
2026-05-18 13:53 ` [PATCH v3 2/3] writeback: drop now-unnecessary rcu_barrier() in cgroup_writeback_umount() Baokun Li
2026-05-18 13:53 ` [PATCH v3 3/3] writeback: use a per-sb counter to drain inode wb switches at umount Baokun Li
2 siblings, 0 replies; 8+ messages in thread
From: Baokun Li @ 2026-05-18 13:53 UTC (permalink / raw)
To: linux-fsdevel; +Cc: viro, brauner, jack, tj, linux-kernel, stable
When a container exits, the following BUG_ON() is occasionally triggered:
==================================================================
VFS: Busy inodes after unmount of sdb (ext4)
------------[ cut here ]------------
kernel BUG at fs/super.c:695!
CPU: 3 PID: 6 Comm: containerd-shim Tainted: G OE K 6.6 #1
pstate: 63400009 (nZCv daif +PAN -UAO +TCO +DIT -SSBS BTYPE=--)
pc : generic_shutdown_super+0xf0/0x100
lr : generic_shutdown_super+0xf0/0x100
Call trace:
generic_shutdown_super+0xf0/0x100
kill_block_super+0x20/0x48
ext4_kill_sb+0x28/0x60
deactivate_locked_super+0x54/0x130
deactivate_super+0x84/0xa0
cleanup_mnt+0xa4/0x140
__cleanup_mnt+0x18/0x28
task_work_run+0x78/0xe0
do_notify_resume+0x204/0x240
==================================================================
The root cause is a race between cgroup_writeback_umount() and
inode_switch_wbs()/cleanup_offline_cgwb(). There is a window between
inode_prepare_wbs_switch() returning true and the subsequent
wb_queue_isw() call. Following is the process that triggers the issue:
CPU A (umount) | CPU B (writeback)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
inode_switch_wbs/cleanup_offline_cgwb
atomic_inc(&isw_nr_in_flight)
inode_prepare_wbs_switch
-> passes SB_ACTIVE check
__iget(inode)
generic_shutdown_super
sb->s_flags &= ~SB_ACTIVE
cgroup_writeback_umount(sb)
smp_mb()
atomic_read(&isw_nr_in_flight)
rcu_barrier()
-> no pending RCU callbacks
flush_workqueue(isw_wq)
-> nothing queued, returns
evict_inodes(sb)
-> Inode skipped as isw still holds a ref.
sop->put_super(sb)
/* destroys percpu counters */
-> VFS: Busy inodes after unmount!
wb_queue_isw()
queue_work(isw_wq, ...)
/* later in work function */
inode_switch_wbs_work_fn
process_inode_switch_wbs
iput() -> evict
percpu_counter_dec() // UAF!
Fix this by extending the RCU read-side critical section in
inode_switch_wbs() and cleanup_offline_cgwb() to cover from
inode_prepare_wbs_switch() through wb_queue_isw(). Since there is
no sleep in this window, rcu_read_lock() can be used. Then add a
synchronize_rcu() in cgroup_writeback_umount() before the existing
rcu_barrier(), so that all in-flight switchers that have passed the
SB_ACTIVE check have completed queue_work() before flush_workqueue()
is called.
The existing rcu_barrier() is intentionally retained so this fix can
be backported unchanged to stable kernels (5.10.y, 6.6.y, ...) that
still queue switches via queue_rcu_work(). It is a no-op on current
mainline (since commit e1b849cfa6b6 ("writeback: Avoid contention on
wb->list_lock when switching inodes")) and is removed in a follow-up
patch.
Fixes: a1a0e23e4903 ("writeback: flush inode cgroup wb switches instead of pinning super_block")
Cc: stable@vger.kernel.org
Suggested-by: Jan Kara <jack@suse.cz>
Link: https://lore.kernel.org/all/mxnjq2l6guusfchvauxr3v7c4bwjasybxlleqbbh4efloeqspz@iqylk76ohufz
Reviewed-by: Jan Kara <jack@suse.cz>
Signed-off-by: Baokun Li <libaokun@linux.alibaba.com>
---
fs/fs-writeback.c | 31 +++++++++++++++++++++++++++++--
1 file changed, 29 insertions(+), 2 deletions(-)
diff --git a/fs/fs-writeback.c b/fs/fs-writeback.c
index a65694cbfe68..6766de9f9d75 100644
--- a/fs/fs-writeback.c
+++ b/fs/fs-writeback.c
@@ -660,12 +660,19 @@ static void inode_switch_wbs(struct inode *inode, int new_wb_id)
atomic_inc(&isw_nr_in_flight);
- /* find and pin the new wb */
+ /*
+ * Paired with synchronize_rcu() in cgroup_writeback_umount():
+ * holding rcu_read_lock across inode_prepare_wbs_switch()
+ * (covering the SB_ACTIVE check and the inode grab) and
+ * wb_queue_isw() ensures synchronize_rcu() cannot return until
+ * the work is queued, so the subsequent flush_workqueue() will
+ * wait for the switch.
+ */
rcu_read_lock();
+ /* find and pin the new wb */
memcg_css = css_from_id(new_wb_id, &memory_cgrp_subsys);
if (memcg_css && !css_tryget(memcg_css))
memcg_css = NULL;
- rcu_read_unlock();
if (!memcg_css)
goto out_free;
@@ -681,9 +688,11 @@ static void inode_switch_wbs(struct inode *inode, int new_wb_id)
trace_inode_switch_wbs_queue(inode->i_wb, new_wb, 1);
wb_queue_isw(new_wb, isw);
+ rcu_read_unlock();
return;
out_free:
+ rcu_read_unlock();
atomic_dec(&isw_nr_in_flight);
if (new_wb)
wb_put(new_wb);
@@ -741,6 +750,14 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
new_wb = &wb->bdi->wb; /* wb_get() is noop for bdi's wb */
nr = 0;
+ /*
+ * Paired with synchronize_rcu() in cgroup_writeback_umount().
+ * Holding rcu_read_lock across the SB_ACTIVE check, the inode grab
+ * and wb_queue_isw() ensures synchronize_rcu() cannot return until
+ * the work is queued, so the subsequent flush_workqueue() will wait
+ * for the switch.
+ */
+ rcu_read_lock();
spin_lock(&wb->list_lock);
/*
* In addition to the inodes that have completed writeback, also switch
@@ -758,6 +775,7 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
/* no attached inodes? bail out */
if (nr == 0) {
+ rcu_read_unlock();
atomic_dec(&isw_nr_in_flight);
wb_put(new_wb);
kfree(isw);
@@ -766,6 +784,7 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
trace_inode_switch_wbs_queue(wb, new_wb, nr);
wb_queue_isw(new_wb, isw);
+ rcu_read_unlock();
return restart;
}
@@ -1221,6 +1240,14 @@ void cgroup_writeback_umount(struct super_block *sb)
smp_mb();
if (atomic_read(&isw_nr_in_flight)) {
+ /*
+ * Paired with rcu_read_lock() in inode_switch_wbs() and
+ * cleanup_offline_cgwb(). synchronize_rcu() waits for any
+ * in-flight switcher that already passed the SB_ACTIVE check
+ * to finish queueing its work, so flush_workqueue() below
+ * will then drain it.
+ */
+ synchronize_rcu();
/*
* Use rcu_barrier() to wait for all pending callbacks to
* ensure that all in-flight wb switches are in the workqueue.
--
2.43.7
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 2/3] writeback: drop now-unnecessary rcu_barrier() in cgroup_writeback_umount()
2026-05-18 13:53 [PATCH v3 0/3] writeback: fix race between cgroup_writeback_umount() and inode_switch_wbs() Baokun Li
2026-05-18 13:53 ` [PATCH v3 1/3] " Baokun Li
@ 2026-05-18 13:53 ` Baokun Li
2026-05-20 8:46 ` Jan Kara
2026-05-18 13:53 ` [PATCH v3 3/3] writeback: use a per-sb counter to drain inode wb switches at umount Baokun Li
2 siblings, 1 reply; 8+ messages in thread
From: Baokun Li @ 2026-05-18 13:53 UTC (permalink / raw)
To: linux-fsdevel; +Cc: viro, brauner, jack, tj, linux-kernel
Commit e1b849cfa6b6 ("writeback: Avoid contention on wb->list_lock when
switching inodes") replaced the queue_rcu_work() based scheduling of
inode wb switches with a plain queue_work(). Since then no switcher
goes through call_rcu(), so rcu_barrier() in cgroup_writeback_umount()
has no callbacks of its own to wait for. It still drains unrelated
call_rcu() callbacks from other subsystems on busy systems, which
incidentally slows umount down; drop it.
Fixes: e1b849cfa6b6 ("writeback: Avoid contention on wb->list_lock when switching inodes")
Signed-off-by: Baokun Li <libaokun@linux.alibaba.com>
---
fs/fs-writeback.c | 5 -----
1 file changed, 5 deletions(-)
diff --git a/fs/fs-writeback.c b/fs/fs-writeback.c
index 6766de9f9d75..325a30cc35bf 100644
--- a/fs/fs-writeback.c
+++ b/fs/fs-writeback.c
@@ -1248,11 +1248,6 @@ void cgroup_writeback_umount(struct super_block *sb)
* will then drain it.
*/
synchronize_rcu();
- /*
- * Use rcu_barrier() to wait for all pending callbacks to
- * ensure that all in-flight wb switches are in the workqueue.
- */
- rcu_barrier();
flush_workqueue(isw_wq);
}
}
--
2.43.7
^ permalink raw reply [flat|nested] 8+ messages in thread
* [PATCH v3 3/3] writeback: use a per-sb counter to drain inode wb switches at umount
2026-05-18 13:53 [PATCH v3 0/3] writeback: fix race between cgroup_writeback_umount() and inode_switch_wbs() Baokun Li
2026-05-18 13:53 ` [PATCH v3 1/3] " Baokun Li
2026-05-18 13:53 ` [PATCH v3 2/3] writeback: drop now-unnecessary rcu_barrier() in cgroup_writeback_umount() Baokun Li
@ 2026-05-18 13:53 ` Baokun Li
2026-05-19 6:33 ` Baokun Li
2026-05-20 8:57 ` Jan Kara
2 siblings, 2 replies; 8+ messages in thread
From: Baokun Li @ 2026-05-18 13:53 UTC (permalink / raw)
To: linux-fsdevel; +Cc: viro, brauner, jack, tj, linux-kernel
Tracking in-flight inode wb switches with a single global counter
(isw_nr_in_flight) plus a synchronize_rcu() based wait in
cgroup_writeback_umount() forces every umount to take a global hit
whenever any other superblock on the system has wb switches in flight,
even if the superblock being unmounted has none of its own.
Replace the global synchronize_rcu()/flush_workqueue() pair with a
per-sb counter, s_isw_nr_in_flight, plus three small helpers:
- cgroup_writeback_pin(sb) - increment counter
- cgroup_writeback_unpin(sb) - decrement and wake drainer if last
- cgroup_writeback_drain(sb) - wait for counter to reach zero
The wiring is:
- inode_prepare_wbs_switch() pins before checking SB_ACTIVE and
grabbing the inode; failure paths unpin before returning. A
lockless SB_ACTIVE check at the top of the function lets us skip
the atomic_inc/smp_mb dance once SB_ACTIVE has been cleared (it
is monotonic and never set back).
- process_inode_switch_wbs() unpins after the matching iput().
- cgroup_writeback_umount() drains the per-sb counter via
wait_var_event().
The smp_mb() pair between inode_prepare_wbs_switch() and
cgroup_writeback_umount() keeps the SB_ACTIVE / counter ordering:
either the umounter sees a non-zero counter and waits, or the
switcher sees SB_ACTIVE cleared and aborts before grabbing the
inode.
The global isw_nr_in_flight is left in place, since it is still used
to throttle in-flight switches via WB_FRN_MAX_IN_FLIGHT.
The rcu_read_lock() extension in inode_switch_wbs() and
cleanup_offline_cgwb() that the race fix added is no longer needed
and is reverted; the synchronize_rcu() that the race fix added to
cgroup_writeback_umount() is dropped as well.
The following numbers were measured on a 16 vCPU QEMU guest with 4
background superblocks each churning "create memcg -> write 1 MiB ->
rmdir memcg" to keep the global isw_nr_in_flight non-zero. Latencies
are wall-clock around umount(8); only the target sb's umount is
measured.
Target sb runs its own cgwb churn:
p50 p95 p99 max
global synchronize_rcu() 67.6 ms 88.3 ms 88.3 ms 96.8 ms
per-sb counter (this) 7.9 ms 10.0 ms 10.0 ms 10.1 ms
Idle target umount latency under cross-sb cgwb-switch pressure:
p50 p95 p99 max
global synchronize_rcu() 62.7 ms 95.4 ms 108.1 ms 108.6 ms
per-sb counter (this) 5.3 ms 6.9 ms 7.4 ms 7.4 ms
no-pressure baseline 4.9 ms 5.9 ms 6.3 ms 6.7 ms
8 concurrent umounts of idle sbs under the same pressure:
p50 p95 max
global synchronize_rcu() 61.3 ms 99.5 ms 113.7 ms
per-sb counter (this) 8.1 ms 9.1 ms 9.5 ms
In-kernel cgroup_writeback_umount() time across the same run
(bpftrace, ~340 calls covering all scenarios):
global synchronize_rcu() 12371 ms total (~36 ms / call)
per-sb counter (this) 1.37 ms total ( ~4 us / call)
Suggested-by: Christian Brauner <brauner@kernel.org>
Link: https://lore.kernel.org/r/177910456953.488929.2169908940676707307.b4-review@b4
Signed-off-by: Baokun Li <libaokun@linux.alibaba.com>
---
fs/fs-writeback.c | 87 ++++++++++++++++++----------------
include/linux/fs/super_types.h | 8 ++++
2 files changed, 54 insertions(+), 41 deletions(-)
diff --git a/fs/fs-writeback.c b/fs/fs-writeback.c
index 325a30cc35bf..32fec4b9094e 100644
--- a/fs/fs-writeback.c
+++ b/fs/fs-writeback.c
@@ -497,6 +497,23 @@ static bool inode_do_switch_wbs(struct inode *inode,
return switched;
}
+static inline void cgroup_writeback_pin(struct super_block *sb)
+{
+ atomic_inc(&sb->s_isw_nr_in_flight);
+}
+
+static inline void cgroup_writeback_unpin(struct super_block *sb)
+{
+ if (atomic_dec_and_test(&sb->s_isw_nr_in_flight))
+ wake_up_var(&sb->s_isw_nr_in_flight);
+}
+
+static inline void cgroup_writeback_drain(struct super_block *sb)
+{
+ wait_var_event(&sb->s_isw_nr_in_flight,
+ !atomic_read(&sb->s_isw_nr_in_flight));
+}
+
static void process_inode_switch_wbs(struct bdi_writeback *new_wb,
struct inode_switch_wbs_context *isw)
{
@@ -554,8 +571,12 @@ static void process_inode_switch_wbs(struct bdi_writeback *new_wb,
wb_put_many(old_wb, nr_switched);
}
- for (inodep = isw->inodes; *inodep; inodep++)
+ for (inodep = isw->inodes; *inodep; inodep++) {
+ struct super_block *sb = (*inodep)->i_sb;
+
iput(*inodep);
+ cgroup_writeback_unpin(sb);
+ }
wb_put(new_wb);
kfree(isw);
atomic_dec(&isw_nr_in_flight);
@@ -598,16 +619,19 @@ void inode_switch_wbs_work_fn(struct work_struct *work)
static bool inode_prepare_wbs_switch(struct inode *inode,
struct bdi_writeback *new_wb)
{
+ /* Avoid the atomic_inc/smp_mb dance once SB_ACTIVE is gone. */
+ if (!(inode->i_sb->s_flags & SB_ACTIVE))
+ return false;
+
/*
- * Paired with smp_mb() in cgroup_writeback_umount().
- * isw_nr_in_flight must be increased before checking SB_ACTIVE and
- * grabbing an inode, otherwise isw_nr_in_flight can be observed as 0
- * in cgroup_writeback_umount() and the isw_wq will be not flushed.
+ * Pairs with smp_mb() in cgroup_writeback_umount(): the umounter either
+ * sees a non-zero counter and waits, or we see SB_ACTIVE clear below.
*/
+ cgroup_writeback_pin(inode->i_sb);
smp_mb();
if (IS_DAX(inode))
- return false;
+ goto out_dec;
/* while holding I_WB_SWITCH, no one else can update the association */
spin_lock(&inode->i_lock);
@@ -615,13 +639,17 @@ static bool inode_prepare_wbs_switch(struct inode *inode,
inode_state_read(inode) & (I_WB_SWITCH | I_FREEING | I_WILL_FREE) ||
inode_to_wb(inode) == new_wb) {
spin_unlock(&inode->i_lock);
- return false;
+ goto out_dec;
}
inode_state_set(inode, I_WB_SWITCH);
__iget(inode);
spin_unlock(&inode->i_lock);
return true;
+
+out_dec:
+ cgroup_writeback_unpin(inode->i_sb);
+ return false;
}
static void wb_queue_isw(struct bdi_writeback *wb,
@@ -673,6 +701,7 @@ static void inode_switch_wbs(struct inode *inode, int new_wb_id)
memcg_css = css_from_id(new_wb_id, &memory_cgrp_subsys);
if (memcg_css && !css_tryget(memcg_css))
memcg_css = NULL;
+ rcu_read_unlock();
if (!memcg_css)
goto out_free;
@@ -688,11 +717,9 @@ static void inode_switch_wbs(struct inode *inode, int new_wb_id)
trace_inode_switch_wbs_queue(inode->i_wb, new_wb, 1);
wb_queue_isw(new_wb, isw);
- rcu_read_unlock();
return;
out_free:
- rcu_read_unlock();
atomic_dec(&isw_nr_in_flight);
if (new_wb)
wb_put(new_wb);
@@ -750,14 +777,6 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
new_wb = &wb->bdi->wb; /* wb_get() is noop for bdi's wb */
nr = 0;
- /*
- * Paired with synchronize_rcu() in cgroup_writeback_umount().
- * Holding rcu_read_lock across the SB_ACTIVE check, the inode grab
- * and wb_queue_isw() ensures synchronize_rcu() cannot return until
- * the work is queued, so the subsequent flush_workqueue() will wait
- * for the switch.
- */
- rcu_read_lock();
spin_lock(&wb->list_lock);
/*
* In addition to the inodes that have completed writeback, also switch
@@ -775,7 +794,6 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
/* no attached inodes? bail out */
if (nr == 0) {
- rcu_read_unlock();
atomic_dec(&isw_nr_in_flight);
wb_put(new_wb);
kfree(isw);
@@ -784,7 +802,6 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
trace_inode_switch_wbs_queue(wb, new_wb, nr);
wb_queue_isw(new_wb, isw);
- rcu_read_unlock();
return restart;
}
@@ -1217,39 +1234,27 @@ int cgroup_writeback_by_id(u64 bdi_id, int memcg_id,
}
/**
- * cgroup_writeback_umount - flush inode wb switches for umount
+ * cgroup_writeback_umount - wait for in-flight inode wb switches on @sb
* @sb: target super_block
*
- * This function is called when a super_block is about to be destroyed and
- * flushes in-flight inode wb switches. An inode wb switch goes through
- * RCU and then workqueue, so the two need to be flushed in order to ensure
- * that all previously scheduled switches are finished. As wb switches are
- * rare occurrences and synchronize_rcu() can take a while, perform
- * flushing iff wb switches are in flight.
+ * Wait until every inode wb switch that already passed the SB_ACTIVE
+ * check on this superblock has been completed by the worker. Since
+ * SB_ACTIVE is cleared before this is called, no new switches can start
+ * for @sb, so s_isw_nr_in_flight will monotonically drop to zero.
*/
void cgroup_writeback_umount(struct super_block *sb)
{
-
if (!(sb->s_bdi->capabilities & BDI_CAP_WRITEBACK))
return;
/*
- * SB_ACTIVE should be reliably cleared before checking
- * isw_nr_in_flight, see generic_shutdown_super().
+ * Pairs with smp_mb() in inode_prepare_wbs_switch(): we either observe
+ * a non-zero counter and wait, or the switcher sees SB_ACTIVE clear
+ * (cleared by generic_shutdown_super()) and bails before grabbing the
+ * inode.
*/
smp_mb();
-
- if (atomic_read(&isw_nr_in_flight)) {
- /*
- * Paired with rcu_read_lock() in inode_switch_wbs() and
- * cleanup_offline_cgwb(). synchronize_rcu() waits for any
- * in-flight switcher that already passed the SB_ACTIVE check
- * to finish queueing its work, so flush_workqueue() below
- * will then drain it.
- */
- synchronize_rcu();
- flush_workqueue(isw_wq);
- }
+ cgroup_writeback_drain(sb);
}
static int __init cgroup_writeback_init(void)
diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h
index 383050e7fdf5..1ab4e2265129 100644
--- a/include/linux/fs/super_types.h
+++ b/include/linux/fs/super_types.h
@@ -274,6 +274,14 @@ struct super_block {
/* number of fserrors that are being sent to fsnotify/filesystems */
refcount_t s_pending_errors;
+
+#ifdef CONFIG_CGROUP_WRITEBACK
+ /*
+ * Number of in-flight inode wb switches for this sb. Drained by
+ * cgroup_writeback_umount() before tear-down.
+ */
+ atomic_t s_isw_nr_in_flight;
+#endif
} __randomize_layout;
/*
--
2.43.7
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v3 3/3] writeback: use a per-sb counter to drain inode wb switches at umount
2026-05-18 13:53 ` [PATCH v3 3/3] writeback: use a per-sb counter to drain inode wb switches at umount Baokun Li
@ 2026-05-19 6:33 ` Baokun Li
2026-05-20 8:57 ` Jan Kara
1 sibling, 0 replies; 8+ messages in thread
From: Baokun Li @ 2026-05-19 6:33 UTC (permalink / raw)
To: linux-fsdevel; +Cc: viro, brauner, jack, tj, linux-kernel
Sashiko[1] flagged a stale comment in patch 3 — the rcu_read_lock()
comment still describes the old synchronize_rcu()/flush_workqueue()
scheme which no longer exists after this patch.
I'll fold the following fixup into the next version:
--- a/fs/fs-writeback.c
+++ b/fs/fs-writeback.c
@@ -688,16 +688,8 @@ static void inode_switch_wbs(struct inode *inode,
int new_wb_id)
atomic_inc(&isw_nr_in_flight);
- /*
- * Paired with synchronize_rcu() in cgroup_writeback_umount():
- * holding rcu_read_lock across inode_prepare_wbs_switch()
- * (covering the SB_ACTIVE check and the inode grab) and
- * wb_queue_isw() ensures synchronize_rcu() cannot return until
- * the work is queued, so the subsequent flush_workqueue() will
- * wait for the switch.
- */
- rcu_read_lock();
/* find and pin the new wb */
+ rcu_read_lock();
memcg_css = css_from_id(new_wb_id, &memory_cgrp_subsys);
if (memcg_css && !css_tryget(memcg_css))
memcg_css = NULL;
In no hurry to resend — further review is welcome.
[1]
https://sashiko.dev/#/patchset/20260518135349.1187628-1-libaokun%40linux.alibaba.com
Thanks,
Baokun
On 2026/5/18 21:53, Baokun Li wrote:
> Tracking in-flight inode wb switches with a single global counter
> (isw_nr_in_flight) plus a synchronize_rcu() based wait in
> cgroup_writeback_umount() forces every umount to take a global hit
> whenever any other superblock on the system has wb switches in flight,
> even if the superblock being unmounted has none of its own.
>
> Replace the global synchronize_rcu()/flush_workqueue() pair with a
> per-sb counter, s_isw_nr_in_flight, plus three small helpers:
>
> - cgroup_writeback_pin(sb) - increment counter
> - cgroup_writeback_unpin(sb) - decrement and wake drainer if last
> - cgroup_writeback_drain(sb) - wait for counter to reach zero
>
> The wiring is:
>
> - inode_prepare_wbs_switch() pins before checking SB_ACTIVE and
> grabbing the inode; failure paths unpin before returning. A
> lockless SB_ACTIVE check at the top of the function lets us skip
> the atomic_inc/smp_mb dance once SB_ACTIVE has been cleared (it
> is monotonic and never set back).
> - process_inode_switch_wbs() unpins after the matching iput().
> - cgroup_writeback_umount() drains the per-sb counter via
> wait_var_event().
>
> The smp_mb() pair between inode_prepare_wbs_switch() and
> cgroup_writeback_umount() keeps the SB_ACTIVE / counter ordering:
> either the umounter sees a non-zero counter and waits, or the
> switcher sees SB_ACTIVE cleared and aborts before grabbing the
> inode.
>
> The global isw_nr_in_flight is left in place, since it is still used
> to throttle in-flight switches via WB_FRN_MAX_IN_FLIGHT.
>
> The rcu_read_lock() extension in inode_switch_wbs() and
> cleanup_offline_cgwb() that the race fix added is no longer needed
> and is reverted; the synchronize_rcu() that the race fix added to
> cgroup_writeback_umount() is dropped as well.
>
> The following numbers were measured on a 16 vCPU QEMU guest with 4
> background superblocks each churning "create memcg -> write 1 MiB ->
> rmdir memcg" to keep the global isw_nr_in_flight non-zero. Latencies
> are wall-clock around umount(8); only the target sb's umount is
> measured.
>
> Target sb runs its own cgwb churn:
>
> p50 p95 p99 max
> global synchronize_rcu() 67.6 ms 88.3 ms 88.3 ms 96.8 ms
> per-sb counter (this) 7.9 ms 10.0 ms 10.0 ms 10.1 ms
>
> Idle target umount latency under cross-sb cgwb-switch pressure:
>
> p50 p95 p99 max
> global synchronize_rcu() 62.7 ms 95.4 ms 108.1 ms 108.6 ms
> per-sb counter (this) 5.3 ms 6.9 ms 7.4 ms 7.4 ms
> no-pressure baseline 4.9 ms 5.9 ms 6.3 ms 6.7 ms
>
> 8 concurrent umounts of idle sbs under the same pressure:
>
> p50 p95 max
> global synchronize_rcu() 61.3 ms 99.5 ms 113.7 ms
> per-sb counter (this) 8.1 ms 9.1 ms 9.5 ms
>
> In-kernel cgroup_writeback_umount() time across the same run
> (bpftrace, ~340 calls covering all scenarios):
>
> global synchronize_rcu() 12371 ms total (~36 ms / call)
> per-sb counter (this) 1.37 ms total ( ~4 us / call)
>
> Suggested-by: Christian Brauner <brauner@kernel.org>
> Link: https://lore.kernel.org/r/177910456953.488929.2169908940676707307.b4-review@b4
> Signed-off-by: Baokun Li <libaokun@linux.alibaba.com>
> ---
> fs/fs-writeback.c | 87 ++++++++++++++++++----------------
> include/linux/fs/super_types.h | 8 ++++
> 2 files changed, 54 insertions(+), 41 deletions(-)
>
> diff --git a/fs/fs-writeback.c b/fs/fs-writeback.c
> index 325a30cc35bf..32fec4b9094e 100644
> --- a/fs/fs-writeback.c
> +++ b/fs/fs-writeback.c
> @@ -497,6 +497,23 @@ static bool inode_do_switch_wbs(struct inode *inode,
> return switched;
> }
>
> +static inline void cgroup_writeback_pin(struct super_block *sb)
> +{
> + atomic_inc(&sb->s_isw_nr_in_flight);
> +}
> +
> +static inline void cgroup_writeback_unpin(struct super_block *sb)
> +{
> + if (atomic_dec_and_test(&sb->s_isw_nr_in_flight))
> + wake_up_var(&sb->s_isw_nr_in_flight);
> +}
> +
> +static inline void cgroup_writeback_drain(struct super_block *sb)
> +{
> + wait_var_event(&sb->s_isw_nr_in_flight,
> + !atomic_read(&sb->s_isw_nr_in_flight));
> +}
> +
> static void process_inode_switch_wbs(struct bdi_writeback *new_wb,
> struct inode_switch_wbs_context *isw)
> {
> @@ -554,8 +571,12 @@ static void process_inode_switch_wbs(struct bdi_writeback *new_wb,
> wb_put_many(old_wb, nr_switched);
> }
>
> - for (inodep = isw->inodes; *inodep; inodep++)
> + for (inodep = isw->inodes; *inodep; inodep++) {
> + struct super_block *sb = (*inodep)->i_sb;
> +
> iput(*inodep);
> + cgroup_writeback_unpin(sb);
> + }
> wb_put(new_wb);
> kfree(isw);
> atomic_dec(&isw_nr_in_flight);
> @@ -598,16 +619,19 @@ void inode_switch_wbs_work_fn(struct work_struct *work)
> static bool inode_prepare_wbs_switch(struct inode *inode,
> struct bdi_writeback *new_wb)
> {
> + /* Avoid the atomic_inc/smp_mb dance once SB_ACTIVE is gone. */
> + if (!(inode->i_sb->s_flags & SB_ACTIVE))
> + return false;
> +
> /*
> - * Paired with smp_mb() in cgroup_writeback_umount().
> - * isw_nr_in_flight must be increased before checking SB_ACTIVE and
> - * grabbing an inode, otherwise isw_nr_in_flight can be observed as 0
> - * in cgroup_writeback_umount() and the isw_wq will be not flushed.
> + * Pairs with smp_mb() in cgroup_writeback_umount(): the umounter either
> + * sees a non-zero counter and waits, or we see SB_ACTIVE clear below.
> */
> + cgroup_writeback_pin(inode->i_sb);
> smp_mb();
>
> if (IS_DAX(inode))
> - return false;
> + goto out_dec;
>
> /* while holding I_WB_SWITCH, no one else can update the association */
> spin_lock(&inode->i_lock);
> @@ -615,13 +639,17 @@ static bool inode_prepare_wbs_switch(struct inode *inode,
> inode_state_read(inode) & (I_WB_SWITCH | I_FREEING | I_WILL_FREE) ||
> inode_to_wb(inode) == new_wb) {
> spin_unlock(&inode->i_lock);
> - return false;
> + goto out_dec;
> }
> inode_state_set(inode, I_WB_SWITCH);
> __iget(inode);
> spin_unlock(&inode->i_lock);
>
> return true;
> +
> +out_dec:
> + cgroup_writeback_unpin(inode->i_sb);
> + return false;
> }
>
> static void wb_queue_isw(struct bdi_writeback *wb,
> @@ -673,6 +701,7 @@ static void inode_switch_wbs(struct inode *inode, int new_wb_id)
> memcg_css = css_from_id(new_wb_id, &memory_cgrp_subsys);
> if (memcg_css && !css_tryget(memcg_css))
> memcg_css = NULL;
> + rcu_read_unlock();
> if (!memcg_css)
> goto out_free;
>
> @@ -688,11 +717,9 @@ static void inode_switch_wbs(struct inode *inode, int new_wb_id)
>
> trace_inode_switch_wbs_queue(inode->i_wb, new_wb, 1);
> wb_queue_isw(new_wb, isw);
> - rcu_read_unlock();
> return;
>
> out_free:
> - rcu_read_unlock();
> atomic_dec(&isw_nr_in_flight);
> if (new_wb)
> wb_put(new_wb);
> @@ -750,14 +777,6 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
> new_wb = &wb->bdi->wb; /* wb_get() is noop for bdi's wb */
>
> nr = 0;
> - /*
> - * Paired with synchronize_rcu() in cgroup_writeback_umount().
> - * Holding rcu_read_lock across the SB_ACTIVE check, the inode grab
> - * and wb_queue_isw() ensures synchronize_rcu() cannot return until
> - * the work is queued, so the subsequent flush_workqueue() will wait
> - * for the switch.
> - */
> - rcu_read_lock();
> spin_lock(&wb->list_lock);
> /*
> * In addition to the inodes that have completed writeback, also switch
> @@ -775,7 +794,6 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
>
> /* no attached inodes? bail out */
> if (nr == 0) {
> - rcu_read_unlock();
> atomic_dec(&isw_nr_in_flight);
> wb_put(new_wb);
> kfree(isw);
> @@ -784,7 +802,6 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
>
> trace_inode_switch_wbs_queue(wb, new_wb, nr);
> wb_queue_isw(new_wb, isw);
> - rcu_read_unlock();
>
> return restart;
> }
> @@ -1217,39 +1234,27 @@ int cgroup_writeback_by_id(u64 bdi_id, int memcg_id,
> }
>
> /**
> - * cgroup_writeback_umount - flush inode wb switches for umount
> + * cgroup_writeback_umount - wait for in-flight inode wb switches on @sb
> * @sb: target super_block
> *
> - * This function is called when a super_block is about to be destroyed and
> - * flushes in-flight inode wb switches. An inode wb switch goes through
> - * RCU and then workqueue, so the two need to be flushed in order to ensure
> - * that all previously scheduled switches are finished. As wb switches are
> - * rare occurrences and synchronize_rcu() can take a while, perform
> - * flushing iff wb switches are in flight.
> + * Wait until every inode wb switch that already passed the SB_ACTIVE
> + * check on this superblock has been completed by the worker. Since
> + * SB_ACTIVE is cleared before this is called, no new switches can start
> + * for @sb, so s_isw_nr_in_flight will monotonically drop to zero.
> */
> void cgroup_writeback_umount(struct super_block *sb)
> {
> -
> if (!(sb->s_bdi->capabilities & BDI_CAP_WRITEBACK))
> return;
>
> /*
> - * SB_ACTIVE should be reliably cleared before checking
> - * isw_nr_in_flight, see generic_shutdown_super().
> + * Pairs with smp_mb() in inode_prepare_wbs_switch(): we either observe
> + * a non-zero counter and wait, or the switcher sees SB_ACTIVE clear
> + * (cleared by generic_shutdown_super()) and bails before grabbing the
> + * inode.
> */
> smp_mb();
> -
> - if (atomic_read(&isw_nr_in_flight)) {
> - /*
> - * Paired with rcu_read_lock() in inode_switch_wbs() and
> - * cleanup_offline_cgwb(). synchronize_rcu() waits for any
> - * in-flight switcher that already passed the SB_ACTIVE check
> - * to finish queueing its work, so flush_workqueue() below
> - * will then drain it.
> - */
> - synchronize_rcu();
> - flush_workqueue(isw_wq);
> - }
> + cgroup_writeback_drain(sb);
> }
>
> static int __init cgroup_writeback_init(void)
> diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h
> index 383050e7fdf5..1ab4e2265129 100644
> --- a/include/linux/fs/super_types.h
> +++ b/include/linux/fs/super_types.h
> @@ -274,6 +274,14 @@ struct super_block {
>
> /* number of fserrors that are being sent to fsnotify/filesystems */
> refcount_t s_pending_errors;
> +
> +#ifdef CONFIG_CGROUP_WRITEBACK
> + /*
> + * Number of in-flight inode wb switches for this sb. Drained by
> + * cgroup_writeback_umount() before tear-down.
> + */
> + atomic_t s_isw_nr_in_flight;
> +#endif
> } __randomize_layout;
>
> /*
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v3 2/3] writeback: drop now-unnecessary rcu_barrier() in cgroup_writeback_umount()
2026-05-18 13:53 ` [PATCH v3 2/3] writeback: drop now-unnecessary rcu_barrier() in cgroup_writeback_umount() Baokun Li
@ 2026-05-20 8:46 ` Jan Kara
2026-05-20 11:38 ` Baokun Li
0 siblings, 1 reply; 8+ messages in thread
From: Jan Kara @ 2026-05-20 8:46 UTC (permalink / raw)
To: Baokun Li; +Cc: linux-fsdevel, viro, brauner, jack, tj, linux-kernel
On Mon 18-05-26 21:53:48, Baokun Li wrote:
> Commit e1b849cfa6b6 ("writeback: Avoid contention on wb->list_lock when
> switching inodes") replaced the queue_rcu_work() based scheduling of
> inode wb switches with a plain queue_work(). Since then no switcher
> goes through call_rcu(), so rcu_barrier() in cgroup_writeback_umount()
> has no callbacks of its own to wait for. It still drains unrelated
> call_rcu() callbacks from other subsystems on busy systems, which
> incidentally slows umount down; drop it.
>
> Fixes: e1b849cfa6b6 ("writeback: Avoid contention on wb->list_lock when switching inodes")
> Signed-off-by: Baokun Li <libaokun@linux.alibaba.com>
I've already replied to previous version but anyway: feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> fs/fs-writeback.c | 5 -----
> 1 file changed, 5 deletions(-)
>
> diff --git a/fs/fs-writeback.c b/fs/fs-writeback.c
> index 6766de9f9d75..325a30cc35bf 100644
> --- a/fs/fs-writeback.c
> +++ b/fs/fs-writeback.c
> @@ -1248,11 +1248,6 @@ void cgroup_writeback_umount(struct super_block *sb)
> * will then drain it.
> */
> synchronize_rcu();
> - /*
> - * Use rcu_barrier() to wait for all pending callbacks to
> - * ensure that all in-flight wb switches are in the workqueue.
> - */
> - rcu_barrier();
> flush_workqueue(isw_wq);
> }
> }
> --
> 2.43.7
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v3 3/3] writeback: use a per-sb counter to drain inode wb switches at umount
2026-05-18 13:53 ` [PATCH v3 3/3] writeback: use a per-sb counter to drain inode wb switches at umount Baokun Li
2026-05-19 6:33 ` Baokun Li
@ 2026-05-20 8:57 ` Jan Kara
1 sibling, 0 replies; 8+ messages in thread
From: Jan Kara @ 2026-05-20 8:57 UTC (permalink / raw)
To: Baokun Li; +Cc: linux-fsdevel, viro, brauner, jack, tj, linux-kernel
On Mon 18-05-26 21:53:49, Baokun Li wrote:
> Tracking in-flight inode wb switches with a single global counter
> (isw_nr_in_flight) plus a synchronize_rcu() based wait in
> cgroup_writeback_umount() forces every umount to take a global hit
> whenever any other superblock on the system has wb switches in flight,
> even if the superblock being unmounted has none of its own.
>
> Replace the global synchronize_rcu()/flush_workqueue() pair with a
> per-sb counter, s_isw_nr_in_flight, plus three small helpers:
>
> - cgroup_writeback_pin(sb) - increment counter
> - cgroup_writeback_unpin(sb) - decrement and wake drainer if last
> - cgroup_writeback_drain(sb) - wait for counter to reach zero
>
> The wiring is:
>
> - inode_prepare_wbs_switch() pins before checking SB_ACTIVE and
> grabbing the inode; failure paths unpin before returning. A
> lockless SB_ACTIVE check at the top of the function lets us skip
> the atomic_inc/smp_mb dance once SB_ACTIVE has been cleared (it
> is monotonic and never set back).
> - process_inode_switch_wbs() unpins after the matching iput().
> - cgroup_writeback_umount() drains the per-sb counter via
> wait_var_event().
>
> The smp_mb() pair between inode_prepare_wbs_switch() and
> cgroup_writeback_umount() keeps the SB_ACTIVE / counter ordering:
> either the umounter sees a non-zero counter and waits, or the
> switcher sees SB_ACTIVE cleared and aborts before grabbing the
> inode.
>
> The global isw_nr_in_flight is left in place, since it is still used
> to throttle in-flight switches via WB_FRN_MAX_IN_FLIGHT.
>
> The rcu_read_lock() extension in inode_switch_wbs() and
> cleanup_offline_cgwb() that the race fix added is no longer needed
> and is reverted; the synchronize_rcu() that the race fix added to
> cgroup_writeback_umount() is dropped as well.
>
> The following numbers were measured on a 16 vCPU QEMU guest with 4
> background superblocks each churning "create memcg -> write 1 MiB ->
> rmdir memcg" to keep the global isw_nr_in_flight non-zero. Latencies
> are wall-clock around umount(8); only the target sb's umount is
> measured.
>
> Target sb runs its own cgwb churn:
>
> p50 p95 p99 max
> global synchronize_rcu() 67.6 ms 88.3 ms 88.3 ms 96.8 ms
> per-sb counter (this) 7.9 ms 10.0 ms 10.0 ms 10.1 ms
>
> Idle target umount latency under cross-sb cgwb-switch pressure:
>
> p50 p95 p99 max
> global synchronize_rcu() 62.7 ms 95.4 ms 108.1 ms 108.6 ms
> per-sb counter (this) 5.3 ms 6.9 ms 7.4 ms 7.4 ms
> no-pressure baseline 4.9 ms 5.9 ms 6.3 ms 6.7 ms
>
> 8 concurrent umounts of idle sbs under the same pressure:
>
> p50 p95 max
> global synchronize_rcu() 61.3 ms 99.5 ms 113.7 ms
> per-sb counter (this) 8.1 ms 9.1 ms 9.5 ms
>
> In-kernel cgroup_writeback_umount() time across the same run
> (bpftrace, ~340 calls covering all scenarios):
>
> global synchronize_rcu() 12371 ms total (~36 ms / call)
> per-sb counter (this) 1.37 ms total ( ~4 us / call)
>
> Suggested-by: Christian Brauner <brauner@kernel.org>
> Link: https://lore.kernel.org/r/177910456953.488929.2169908940676707307.b4-review@b4
> Signed-off-by: Baokun Li <libaokun@linux.alibaba.com>
Looks good. Feel free to add:
Reviewed-by: Jan Kara <jack@suse.cz>
Honza
> ---
> fs/fs-writeback.c | 87 ++++++++++++++++++----------------
> include/linux/fs/super_types.h | 8 ++++
> 2 files changed, 54 insertions(+), 41 deletions(-)
>
> diff --git a/fs/fs-writeback.c b/fs/fs-writeback.c
> index 325a30cc35bf..32fec4b9094e 100644
> --- a/fs/fs-writeback.c
> +++ b/fs/fs-writeback.c
> @@ -497,6 +497,23 @@ static bool inode_do_switch_wbs(struct inode *inode,
> return switched;
> }
>
> +static inline void cgroup_writeback_pin(struct super_block *sb)
> +{
> + atomic_inc(&sb->s_isw_nr_in_flight);
> +}
> +
> +static inline void cgroup_writeback_unpin(struct super_block *sb)
> +{
> + if (atomic_dec_and_test(&sb->s_isw_nr_in_flight))
> + wake_up_var(&sb->s_isw_nr_in_flight);
> +}
> +
> +static inline void cgroup_writeback_drain(struct super_block *sb)
> +{
> + wait_var_event(&sb->s_isw_nr_in_flight,
> + !atomic_read(&sb->s_isw_nr_in_flight));
> +}
> +
> static void process_inode_switch_wbs(struct bdi_writeback *new_wb,
> struct inode_switch_wbs_context *isw)
> {
> @@ -554,8 +571,12 @@ static void process_inode_switch_wbs(struct bdi_writeback *new_wb,
> wb_put_many(old_wb, nr_switched);
> }
>
> - for (inodep = isw->inodes; *inodep; inodep++)
> + for (inodep = isw->inodes; *inodep; inodep++) {
> + struct super_block *sb = (*inodep)->i_sb;
> +
> iput(*inodep);
> + cgroup_writeback_unpin(sb);
> + }
> wb_put(new_wb);
> kfree(isw);
> atomic_dec(&isw_nr_in_flight);
> @@ -598,16 +619,19 @@ void inode_switch_wbs_work_fn(struct work_struct *work)
> static bool inode_prepare_wbs_switch(struct inode *inode,
> struct bdi_writeback *new_wb)
> {
> + /* Avoid the atomic_inc/smp_mb dance once SB_ACTIVE is gone. */
> + if (!(inode->i_sb->s_flags & SB_ACTIVE))
> + return false;
> +
> /*
> - * Paired with smp_mb() in cgroup_writeback_umount().
> - * isw_nr_in_flight must be increased before checking SB_ACTIVE and
> - * grabbing an inode, otherwise isw_nr_in_flight can be observed as 0
> - * in cgroup_writeback_umount() and the isw_wq will be not flushed.
> + * Pairs with smp_mb() in cgroup_writeback_umount(): the umounter either
> + * sees a non-zero counter and waits, or we see SB_ACTIVE clear below.
> */
> + cgroup_writeback_pin(inode->i_sb);
> smp_mb();
>
> if (IS_DAX(inode))
> - return false;
> + goto out_dec;
>
> /* while holding I_WB_SWITCH, no one else can update the association */
> spin_lock(&inode->i_lock);
> @@ -615,13 +639,17 @@ static bool inode_prepare_wbs_switch(struct inode *inode,
> inode_state_read(inode) & (I_WB_SWITCH | I_FREEING | I_WILL_FREE) ||
> inode_to_wb(inode) == new_wb) {
> spin_unlock(&inode->i_lock);
> - return false;
> + goto out_dec;
> }
> inode_state_set(inode, I_WB_SWITCH);
> __iget(inode);
> spin_unlock(&inode->i_lock);
>
> return true;
> +
> +out_dec:
> + cgroup_writeback_unpin(inode->i_sb);
> + return false;
> }
>
> static void wb_queue_isw(struct bdi_writeback *wb,
> @@ -673,6 +701,7 @@ static void inode_switch_wbs(struct inode *inode, int new_wb_id)
> memcg_css = css_from_id(new_wb_id, &memory_cgrp_subsys);
> if (memcg_css && !css_tryget(memcg_css))
> memcg_css = NULL;
> + rcu_read_unlock();
> if (!memcg_css)
> goto out_free;
>
> @@ -688,11 +717,9 @@ static void inode_switch_wbs(struct inode *inode, int new_wb_id)
>
> trace_inode_switch_wbs_queue(inode->i_wb, new_wb, 1);
> wb_queue_isw(new_wb, isw);
> - rcu_read_unlock();
> return;
>
> out_free:
> - rcu_read_unlock();
> atomic_dec(&isw_nr_in_flight);
> if (new_wb)
> wb_put(new_wb);
> @@ -750,14 +777,6 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
> new_wb = &wb->bdi->wb; /* wb_get() is noop for bdi's wb */
>
> nr = 0;
> - /*
> - * Paired with synchronize_rcu() in cgroup_writeback_umount().
> - * Holding rcu_read_lock across the SB_ACTIVE check, the inode grab
> - * and wb_queue_isw() ensures synchronize_rcu() cannot return until
> - * the work is queued, so the subsequent flush_workqueue() will wait
> - * for the switch.
> - */
> - rcu_read_lock();
> spin_lock(&wb->list_lock);
> /*
> * In addition to the inodes that have completed writeback, also switch
> @@ -775,7 +794,6 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
>
> /* no attached inodes? bail out */
> if (nr == 0) {
> - rcu_read_unlock();
> atomic_dec(&isw_nr_in_flight);
> wb_put(new_wb);
> kfree(isw);
> @@ -784,7 +802,6 @@ bool cleanup_offline_cgwb(struct bdi_writeback *wb)
>
> trace_inode_switch_wbs_queue(wb, new_wb, nr);
> wb_queue_isw(new_wb, isw);
> - rcu_read_unlock();
>
> return restart;
> }
> @@ -1217,39 +1234,27 @@ int cgroup_writeback_by_id(u64 bdi_id, int memcg_id,
> }
>
> /**
> - * cgroup_writeback_umount - flush inode wb switches for umount
> + * cgroup_writeback_umount - wait for in-flight inode wb switches on @sb
> * @sb: target super_block
> *
> - * This function is called when a super_block is about to be destroyed and
> - * flushes in-flight inode wb switches. An inode wb switch goes through
> - * RCU and then workqueue, so the two need to be flushed in order to ensure
> - * that all previously scheduled switches are finished. As wb switches are
> - * rare occurrences and synchronize_rcu() can take a while, perform
> - * flushing iff wb switches are in flight.
> + * Wait until every inode wb switch that already passed the SB_ACTIVE
> + * check on this superblock has been completed by the worker. Since
> + * SB_ACTIVE is cleared before this is called, no new switches can start
> + * for @sb, so s_isw_nr_in_flight will monotonically drop to zero.
> */
> void cgroup_writeback_umount(struct super_block *sb)
> {
> -
> if (!(sb->s_bdi->capabilities & BDI_CAP_WRITEBACK))
> return;
>
> /*
> - * SB_ACTIVE should be reliably cleared before checking
> - * isw_nr_in_flight, see generic_shutdown_super().
> + * Pairs with smp_mb() in inode_prepare_wbs_switch(): we either observe
> + * a non-zero counter and wait, or the switcher sees SB_ACTIVE clear
> + * (cleared by generic_shutdown_super()) and bails before grabbing the
> + * inode.
> */
> smp_mb();
> -
> - if (atomic_read(&isw_nr_in_flight)) {
> - /*
> - * Paired with rcu_read_lock() in inode_switch_wbs() and
> - * cleanup_offline_cgwb(). synchronize_rcu() waits for any
> - * in-flight switcher that already passed the SB_ACTIVE check
> - * to finish queueing its work, so flush_workqueue() below
> - * will then drain it.
> - */
> - synchronize_rcu();
> - flush_workqueue(isw_wq);
> - }
> + cgroup_writeback_drain(sb);
> }
>
> static int __init cgroup_writeback_init(void)
> diff --git a/include/linux/fs/super_types.h b/include/linux/fs/super_types.h
> index 383050e7fdf5..1ab4e2265129 100644
> --- a/include/linux/fs/super_types.h
> +++ b/include/linux/fs/super_types.h
> @@ -274,6 +274,14 @@ struct super_block {
>
> /* number of fserrors that are being sent to fsnotify/filesystems */
> refcount_t s_pending_errors;
> +
> +#ifdef CONFIG_CGROUP_WRITEBACK
> + /*
> + * Number of in-flight inode wb switches for this sb. Drained by
> + * cgroup_writeback_umount() before tear-down.
> + */
> + atomic_t s_isw_nr_in_flight;
> +#endif
> } __randomize_layout;
>
> /*
> --
> 2.43.7
>
--
Jan Kara <jack@suse.com>
SUSE Labs, CR
^ permalink raw reply [flat|nested] 8+ messages in thread
* Re: [PATCH v3 2/3] writeback: drop now-unnecessary rcu_barrier() in cgroup_writeback_umount()
2026-05-20 8:46 ` Jan Kara
@ 2026-05-20 11:38 ` Baokun Li
0 siblings, 0 replies; 8+ messages in thread
From: Baokun Li @ 2026-05-20 11:38 UTC (permalink / raw)
To: Jan Kara; +Cc: linux-fsdevel, viro, brauner, tj, linux-kernel
On 2026/5/20 16:46, Jan Kara wrote:
> On Mon 18-05-26 21:53:48, Baokun Li wrote:
>> Commit e1b849cfa6b6 ("writeback: Avoid contention on wb->list_lock when
>> switching inodes") replaced the queue_rcu_work() based scheduling of
>> inode wb switches with a plain queue_work(). Since then no switcher
>> goes through call_rcu(), so rcu_barrier() in cgroup_writeback_umount()
>> has no callbacks of its own to wait for. It still drains unrelated
>> call_rcu() callbacks from other subsystems on busy systems, which
>> incidentally slows umount down; drop it.
>>
>> Fixes: e1b849cfa6b6 ("writeback: Avoid contention on wb->list_lock when switching inodes")
>> Signed-off-by: Baokun Li <libaokun@linux.alibaba.com>
> I've already replied to previous version but anyway: feel free to add:
>
> Reviewed-by: Jan Kara <jack@suse.cz>
>
> Honza
Hi Honza,
Thank you for your review!
Sorry for the rushed v3 — your Reviewed-by on v2 came in right after
I hit send, so I missed picking it up. I'll carry it forward in v4.
Thanks,
Baokun
>> ---
>> fs/fs-writeback.c | 5 -----
>> 1 file changed, 5 deletions(-)
>>
>> diff --git a/fs/fs-writeback.c b/fs/fs-writeback.c
>> index 6766de9f9d75..325a30cc35bf 100644
>> --- a/fs/fs-writeback.c
>> +++ b/fs/fs-writeback.c
>> @@ -1248,11 +1248,6 @@ void cgroup_writeback_umount(struct super_block *sb)
>> * will then drain it.
>> */
>> synchronize_rcu();
>> - /*
>> - * Use rcu_barrier() to wait for all pending callbacks to
>> - * ensure that all in-flight wb switches are in the workqueue.
>> - */
>> - rcu_barrier();
>> flush_workqueue(isw_wq);
>> }
>> }
>> --
>> 2.43.7
>>
^ permalink raw reply [flat|nested] 8+ messages in thread
end of thread, other threads:[~2026-05-20 11:38 UTC | newest]
Thread overview: 8+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-05-18 13:53 [PATCH v3 0/3] writeback: fix race between cgroup_writeback_umount() and inode_switch_wbs() Baokun Li
2026-05-18 13:53 ` [PATCH v3 1/3] " Baokun Li
2026-05-18 13:53 ` [PATCH v3 2/3] writeback: drop now-unnecessary rcu_barrier() in cgroup_writeback_umount() Baokun Li
2026-05-20 8:46 ` Jan Kara
2026-05-20 11:38 ` Baokun Li
2026-05-18 13:53 ` [PATCH v3 3/3] writeback: use a per-sb counter to drain inode wb switches at umount Baokun Li
2026-05-19 6:33 ` Baokun Li
2026-05-20 8:57 ` Jan Kara
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®