Linux block layer
 help / color / mirror / Atom feed
* [PATCH] blk-iocost: publish hweights as one atomic value
@ 2026-10-15 13:59 Kunhai Dai
  2026-10-16 11:13 ` Tao Cui
  2026-10-16 19:45 ` Tejun Heo
  0 siblings, 2 replies; 3+ messages in thread
From: Kunhai Dai @ 2026-10-15 13:59 UTC (permalink / raw)
  To: tj
  Cc: axboe, josef, yukuai, cgroups, linux-block, linux-kernel, rostedt,
	mhiramat, mathieu.desnoyers, linux-trace-kernel

current_hweight() uses separate plain stores for the cache generation and
both weights. The compiler can publish the generation before the weights,
letting an interrupt accept stale weights as current. Multiple refreshers
can also interleave their stores. Invalid donation budgets may then lead
to a divide error in transfer_surpluses().

Pack the generation and both weights into a single atomic64_t. Hierarchical
weights are in [1, 65536], so storing each weight minus one preserves its
full range in 16 bits and leaves 32 bits for the generation. Readers copy
one complete entry, and refreshers publish one complete entry. Return the
local snapshot instead of rereading fields another refresher can modify.

Keep the existing lockless refresh and cache reuse behavior. A late
refresh may replace a newer entry, but carries its own generation so the
next reader can detect the mismatch. Preserve the existing barrier paired
with commit_weights() and the allowance for approximate hierarchy reads.

Keep the old weight fields solely as donation workspace. Update cache
initialization, activation invalidation, tracepoints and the drgn monitor
accordingly. Tracepoints decode one cached snapshot without recalculating
or refreshing weights. Stop invalid donation calculations before applying
leaf adjustments, and check donating before subtracting one.

Validated against commit 24a19b953e2a ("Linux 7.3-rc7"):
- clean application, whitespace and strict checkpatch checks;
- W=1 compilation of blk-iocost.o with the existing x86-64 configuration
  and PREEMPT_RT plus PROVE_LOCKING;
- extracted production-function checks covering all weight values,
  generation high bits, publication interleavings, donation cleanup and
  400000 concurrent refresh calls using C11 atomic test stubs;
- Python syntax and actual monitor-decoder checks;
- x86-64 disassembly confirms a single 64-bit cache load and store.

Runtime kprobe injection on an x86-64 VM with 32 vCPUs completed three
rounds without WARN or panic. With a 30 ms publication delay, the probe
reported 23102 publication-point holds, 3234 cache-change observations
within those windows and 72 matching-generation reader observations. The
unpatched baseline panicked under the corresponding reproduction load.

The runtime build predates this revision's tracepoint-only simplification;
the packed-cache and donation logic are unchanged. The current tracepoint
change has passed normal and PREEMPT_RT compilation, but has not been
rerun in the runtime injection test. Performance regression testing has
not been performed.

Developed with assistance from OpenAI Codex.

Assisted-by: LLM
Signed-off-by: Kunhai Dai <daikunhai@didiglobal.com>

---
Reproduction outline (x86-64, cgroup v2):
1. Enable BLK_CGROUP_IOCOST and KPROBES. Enable the io controller at each
   parent, and configure io.cost.qos and io.cost.model for the test device.
2. Create four leaves: rt/s1, rt/K/s2, rt/K/P/s3 and rt/K/P/C. Run continuous
   direct 1 MiB writes in the first three; issue four direct 4 KiB writes
   every 200 ms in C, keeping it active with low utilization.
3. Every 300 ms, alternate the K/P/C chain weights between 1 and 10000,
   with the sibling leaves s1/s2/s3 taking the opposite weights. This
   repeatedly makes C's hierarchical share fall to the minimum of 1.
4. On the unpatched build, confirm that the compiler placed the cached
   generation store before the weight stores. Insert a kprobe delay of
   30 ms after that generation store and before the weight stores.
   Derive the instruction offset from the binary being tested.
5. On the packed-cache build, delay just before the atomic cache store
   instead: there is no separate generation-publication window. This
   stresses concurrent refresh completion and late cache overwrites.

Unpatched baseline, abbreviated crash and stack excerpt:

  iocg: invalid donation weights in /rt/K/P:
        active=1 donating=1 after=0
  Oops: divide error: 0000 [#1] SMP PTI
  RIP: 0010:transfer_surpluses+0x43e/0x6b0
  RAX: ffffffffffffffff  RCX: 0000000000000000
   <IRQ>
   ioc_timer_fn+0x1158/0x13b0
   call_timer_fn+0x1e/0x100
   __run_timer_base.part.36+0x1f0/0x220
   [...]
   handle_softirqs+0xa9/0x2a0
   irq_exit_rcu+0x48/0xe0
   sysvec_apic_timer_interrupt+0x6b/0x80
   </IRQ>
   [...]
   win_pre+0x44/0x70 [repro_iocost]
   [...]
   ? current_hweight+0xc1/0x110
   ? adjust_inuse_and_calc_cost+0x44/0x290
   ? ioc_rqos_throttle+0x13e/0x590
  Kernel panic - not syncing: Fatal exception in interrupt

 block/blk-iocost.c             | 65 ++++++++++++++++++++++++++++--------------
 include/trace/events/iocost.h  |  6 ++--
 tools/cgroup/iocost_monitor.py |  7 +++--
 3 files changed, 53 insertions(+), 25 deletions(-)

diff --git a/block/blk-iocost.c b/block/blk-iocost.c
index 2745bffcd..fcc7ae866 100644
--- a/block/blk-iocost.c
+++ b/block/blk-iocost.c
@@ -472,8 +472,8 @@ struct ioc_gq {
 	 *
 	 * When an idle cgroup becomes active its `active` goes from 0 to
 	 * `weight`.  `inuse` is the surplus adjusted active weight.
-	 * `active` and `inuse` are used to calculate `hweight_active` and
-	 * `hweight_inuse`.
+	 * `active` and `inuse` determine the hierarchical weights cached in
+	 * `hweight_cache`.
 	 *
 	 * `last_inuse` remembers `inuse` while an iocg is idle to persist
 	 * surplus adjustments.
@@ -520,7 +520,9 @@ struct ioc_gq {
 	u64				child_active_sum;
 	u64				child_inuse_sum;
 	u64				child_adjusted_sum;
-	int				hweight_gen;
+	/* Generation in bits 63:32; active - 1 and inuse - 1 in 31:16/15:0. */
+	atomic64_t			hweight_cache;
+	/* Donation workspace, protected by ioc->lock. */
 	u32				hweight_active;
 	u32				hweight_inuse;
 	u32				hweight_donating;
@@ -1154,13 +1156,17 @@ static void current_hweight(struct ioc_gq *iocg, u32 *hw_activep, u32 *hw_inusep
 {
 	struct ioc *ioc = iocg->ioc;
 	int lvl;
-	u32 hwa, hwi;
-	int ioc_gen;
-
-	/* hot path - if uptodate, use cached */
-	ioc_gen = atomic_read(&ioc->hweight_gen);
-	if (ioc_gen == iocg->hweight_gen)
+	u32 hwa, hwi, ioc_gen;
+	u64 cache;
+
+	/* Read the generation and both weights as one atomic snapshot. */
+	ioc_gen = (u32)atomic_read(&ioc->hweight_gen);
+	cache = (u64)atomic64_read(&iocg->hweight_cache);
+	if ((u32)(cache >> 32) == ioc_gen) {
+		hwa = ((cache >> 16) & (WEIGHT_ONE - 1)) + 1;
+		hwi = (cache & (WEIGHT_ONE - 1)) + 1;
 		goto out;
+	}
 
 	/*
 	 * Paired with wmb in commit_weights(). If we saw the updated
@@ -1194,14 +1200,22 @@ static void current_hweight(struct ioc_gq *iocg, u32 *hw_activep, u32 *hw_inusep
 		hwi = div64_u64((u64)hwi * inuse, inuse_sum);
 	}
 
-	iocg->hweight_active = max_t(u32, hwa, 1);
-	iocg->hweight_inuse = max_t(u32, hwi, 1);
-	iocg->hweight_gen = ioc_gen;
+	hwa = max_t(u32, hwa, 1);
+	hwi = max_t(u32, hwi, 1);
+	/*
+	 * Each weight is in [1, WEIGHT_ONE]. Subtracting one preserves its
+	 * full range in 16 bits, leaving 32 bits for the generation. An
+	 * interrupt or another CPU can only observe a complete snapshot.
+	 * A late refresher may overwrite a newer cache, but its generation
+	 * stays paired with its own weights and forces a future refresh.
+	 */
+	cache = ((u64)ioc_gen << 32) | ((u64)(hwa - 1) << 16) | (hwi - 1);
+	atomic64_set(&iocg->hweight_cache, (s64)cache);
 out:
 	if (hw_activep)
-		*hw_activep = iocg->hweight_active;
+		*hw_activep = hwa;
 	if (hw_inusep)
-		*hw_inusep = iocg->hweight_inuse;
+		*hw_inusep = hwi;
 }
 
 /*
@@ -1300,10 +1314,12 @@ static bool iocg_activate(struct ioc_gq *iocg, struct ioc_now *now)
 
 	/*
 	 * Activate, propagate weight and start period timer if not
-	 * running.  Reset hweight_gen to avoid accidental match from
-	 * wrapping.
+	 * running. Invalidate the cached generation to avoid accidental match
+	 * from wrapping.
 	 */
-	iocg->hweight_gen = atomic_read(&ioc->hweight_gen) - 1;
+	atomic64_set(&iocg->hweight_cache,
+		     ((u64)(u32)(atomic_read(&ioc->hweight_gen) - 1) << 32) |
+		     (u32)atomic64_read(&iocg->hweight_cache));
 	list_add(&iocg->active_list, &ioc->active_iocgs);
 
 	propagate_weights(iocg, iocg->weight,
@@ -1863,7 +1879,7 @@ static void transfer_surpluses(struct list_head *surpluses, struct ioc_now *now)
 	list_for_each_entry(iocg, surpluses, surplus_list) {
 		u32 hwa;
 
-		current_hweight(iocg, &hwa, NULL);
+		current_hweight(iocg, &hwa, &iocg->hweight_inuse);
 		after_sum += iocg->hweight_after_donation;
 
 		if (iocg->hweight_after_donation > hwa) {
@@ -1946,8 +1962,10 @@ static void transfer_surpluses(struct list_head *surpluses, struct ioc_now *now)
 
 		iocg->hweight_donating = min(iocg->hweight_donating,
 					     iocg->hweight_active);
-		iocg->hweight_after_donation = min(iocg->hweight_after_donation,
-						   iocg->hweight_donating - 1);
+		if (iocg->hweight_donating > 1)
+			iocg->hweight_after_donation =
+				min(iocg->hweight_after_donation,
+				    iocg->hweight_donating - 1);
 		if (WARN_ON_ONCE(iocg->hweight_active <= 1 ||
 				 iocg->hweight_donating <= 1 ||
 				 iocg->hweight_after_donation == 0)) {
@@ -1956,6 +1974,7 @@ static void transfer_surpluses(struct list_head *surpluses, struct ioc_now *now)
 			pr_cont(": active=%u donating=%u after=%u\n",
 				iocg->hweight_active, iocg->hweight_donating,
 				iocg->hweight_after_donation);
+			goto out;
 		}
 	}
 
@@ -2054,6 +2073,7 @@ static void transfer_surpluses(struct list_head *surpluses, struct ioc_now *now)
 		__propagate_weights(iocg, iocg->active, inuse, true, now);
 	}
 
+out:
 	/* walk list should be dissolved after use */
 	list_for_each_entry_safe(iocg, tiocg, &inner_walk, walk_list)
 		list_del_init(&iocg->walk_list);
@@ -2360,7 +2380,7 @@ static void ioc_timer_fn(struct timer_list *timer)
 				 */
 				TRACE_IOCG_PATH(inuse_shortage, iocg, &now,
 						iocg->inuse, iocg->active,
-						iocg->hweight_inuse, new_hwi);
+						old_hwi, new_hwi);
 
 				__propagate_weights(iocg, iocg->active,
 						    iocg->active, true, &now);
@@ -3032,6 +3052,9 @@ static void ioc_pd_init(struct blkg_policy_data *pd)
 	INIT_LIST_HEAD(&iocg->active_list);
 	INIT_LIST_HEAD(&iocg->walk_list);
 	INIT_LIST_HEAD(&iocg->surplus_list);
+	atomic64_set(&iocg->hweight_cache,
+		     ((u64)(u32)(atomic_read(&ioc->hweight_gen) - 1) << 32) |
+		     U32_MAX);
 	iocg->hweight_active = WEIGHT_ONE;
 	iocg->hweight_inuse = WEIGHT_ONE;
 
diff --git a/include/trace/events/iocost.h b/include/trace/events/iocost.h
index e772b1bc6..aac25edaa 100644
--- a/include/trace/events/iocost.h
+++ b/include/trace/events/iocost.h
@@ -34,6 +34,8 @@ DECLARE_EVENT_CLASS(iocost_iocg_state,
 	),
 
 	TP_fast_assign(
+		u64 cache = (u64)atomic64_read(&iocg->hweight_cache);
+
 		__assign_str(devname);
 		__assign_str(cgroup);
 		__entry->now = now->now;
@@ -44,8 +46,8 @@ DECLARE_EVENT_CLASS(iocost_iocg_state,
 		__entry->vtime = vtime;
 		__entry->weight = iocg->weight;
 		__entry->inuse = iocg->inuse;
-		__entry->hweight_active = iocg->hweight_active;
-		__entry->hweight_inuse = iocg->hweight_inuse;
+		__entry->hweight_active = ((cache >> 16) & (WEIGHT_ONE - 1)) + 1;
+		__entry->hweight_inuse = (cache & (WEIGHT_ONE - 1)) + 1;
 	),
 
 	TP_printk("[%s:%s] now=%llu:%llu vrate=%llu "
diff --git a/tools/cgroup/iocost_monitor.py b/tools/cgroup/iocost_monitor.py
index bdd78ba27..8a4f844c9 100644
--- a/tools/cgroup/iocost_monitor.py
+++ b/tools/cgroup/iocost_monitor.py
@@ -149,8 +149,11 @@ class IocgStat:
         self.weight = iocg.weight.value_() / WEIGHT_ONE
         self.active = iocg.active.value_() / WEIGHT_ONE
         self.inuse = iocg.inuse.value_() / WEIGHT_ONE
-        self.hwa_pct = iocg.hweight_active.value_() * 100 / WEIGHT_ONE
-        self.hwi_pct = iocg.hweight_inuse.value_() * 100 / WEIGHT_ONE
+        hweight_cache = iocg.hweight_cache.counter.value_()
+        hwa = ((hweight_cache >> 16) & (WEIGHT_ONE - 1)) + 1
+        hwi = (hweight_cache & (WEIGHT_ONE - 1)) + 1
+        self.hwa_pct = hwa * 100 / WEIGHT_ONE
+        self.hwi_pct = hwi * 100 / WEIGHT_ONE
         self.address = iocg.value_()
 
         vdone = iocg.done_vtime.counter.value_()

^ permalink raw reply related	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-10-16 19:45 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-15 13:59 [PATCH] blk-iocost: publish hweights as one atomic value Kunhai Dai
2026-10-16 11:13 ` Tao Cui
2026-10-16 19:45 ` Tejun Heo

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox