mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
* [PATCH 0/2] softirq: fix idle accounting for an interrupted idle task
@ 2026-11-06  9:43 Radu Toader
  2026-11-06  9:43 ` [PATCH 1/2] sched_ext: Add scx_tickbug, a scheduler that holds a task on an idle CPU Radu Toader
  2026-11-06  9:43 ` [PATCH 2/2] softirq: Treat an interrupted idle task as idle in tick_irq_exit() Radu Toader
  0 siblings, 2 replies; 5+ messages in thread
From: Radu Toader @ 2026-11-06  9:43 UTC (permalink / raw)
  To: peterz, mingo, tj, tglx
  Cc: juri.lelli, vincent.guittot, dietmar.eggemann, rostedt, bsegall,
	mgorman, vschneid, kprateek.nayak, void, arighi, changwoo,
	frederic, anna-maria, bigeasy, joshdon, haoluo, brho,
	linux-kernel, sched-ext, bpf

tick_irq_exit() decides whether an interrupt that just finished ended
inside the idle loop, and it asked sched_core_idle_cpu(), which is
idle_cpu(): false whenever the run queue is not empty. But a CPU can be
in the idle loop with tasks queued on it - forced idle under core
scheduling, and any task a sched_ext BPF scheduler holds back while
leaving it runnable - and every interrupt taken in that state skipped
tick_nohz_irq_exit(). The fix asks is_idle_task(current) instead, which
is the question the function means to ask.

Patch 1 adds the scheduler that holds the state open long enough to
measure, patch 2 is the one-line change.

Measurements:

One thread spinning 10 ms in every 100 ms in a cgroup, pinned to the
CPU the scheduler holds it on, 20 s windows, three repetitions per
cell, arms interleaved, on one 16-CPU KVM guest booted with
"no-kvmclock tsc=unstable" - which is what it takes to get an unstable
sched_clock in a guest, because mark_tsc_unstable() clears sched_clock
stability only when using_native_sched_clock(). Two kernels from this
tree differing only in patch 2, booted in turn.

The number is the cgroup's cpu.stat usage_usec over the thread's own
CLOCK_MONOTONIC measurement of its busy time, which is an upper bound
on the CPU it can possibly have used. 1.00 is correct accounting;
above 1.00 the kernel charged the task time it did not spend.

  hold (ms)   without patch 2   with patch 2
      7            1.32            0.964
     13            1.21            0.963
     23            2.00            0.963
     37            1.69            0.964
     53            1.33            0.964

  control, fair class, no BPF scheduler loaded:
                   1.005           1.006

Nothing in the fixed kernel's figures varies with the hold, and none of
the unfixed kernel's is within 20% of correct. The three repetitions of
each cell span at most 0.04.

The unfixed kernel also understates how long it held the task, which is
the same bug seen from the other side: the scheduler measures the hold
with scx_bpf_now(), the sched_clock that is not being resynchronized.
For the 23 ms cell it reported a 5.1 ms mean hold where the fixed
kernel reported 15.2 ms for the identical configuration. So the fixed
kernel accounts correctly across holds three times longer than the ones
the unfixed kernel gets wrong.

What is not shown, and what was not done:

No loss in /proc/stat. The sum of that CPU's fields 2 to 9 over the
window stayed within 0.6% of the window on both kernels, so in this
configuration the kcpustat_irq_exit() half of what tick_nohz_irq_exit()
does has no visible effect, unlike the forced idle case that commit
548796e2e70b ("sched/core: introduce sched_core_idle_cpu()") widened
the gate for.

The mechanism is bounded rather than measured. A kprobe on
tick_nohz_irq_exit() filtered to the idle task counted 998 calls per
20 s window on the held CPU without patch 2 against 1321 with it, n=3
each, but those ranges overlap and the two kernels run the load on
different timings.

The forced-idle half of the problem is not measured here at all. Patch
2 changes the same gate for core scheduling, where that commit first
widened it, but every number above comes from the sched_ext path.

The figures were taken with an earlier revision of patch 1, before an
automated review of the tool found six defects in it. Four of them
cannot move these numbers: the per-task hold timestamp (only one task
was ever held, so the global it replaces was never overwritten), the
ops.dequeue() bookkeeping (the load never changes affinity or priority
while parked), the release timer's pinning check (a mispinned timer
fails the run loudly, and no run failed) and the userspace CPU
validation. Two could: @target_cpu now stays in the idle loop while it
holds something instead of taking shared work, and a %SCX_ENQ_LAST
enqueue of the tagged task is now held rather than put back. Both make
the held state wider, so both move the unfixed kernel further from
1.00, not closer. The load exhausted a 20 ms slice only if a 10 ms
burst ran long, so the second one was rarely reached. The test machines
are gone and this was not re-measured.

Open question:

The Fixes: tag. The sched_ext path this was found on dates from commit
f0e1a0643a59 ("sched_ext: Implement BPF extensible scheduler class"),
which is what patch 2 carries. But the gate was idle_cpu() before that,
and what put sched_core_idle_cpu() there was commit 548796e2e70b
("sched/core: introduce sched_core_idle_cpu()"). Whichever of the two
you prefer, or neither, is yours to pick.

Tooling:

Both patches were written by Claude Opus 5 (Claude Code), including the
test scheduler, the measurement harness and these changelogs. The
measurements were run and the results checked by hand. The one-line
change in patch 2 and the reasoning for it are in Documentation terms
tool-generated content; review it as such.

Radu Toader (2):
  sched_ext: Add scx_tickbug, a scheduler that holds a task on an idle
    CPU
  softirq: Treat an interrupted idle task as idle in tick_irq_exit()

 include/linux/sched.h             |   2 -
 kernel/sched/syscalls.c           |  12 -
 kernel/softirq.c                  |  12 +-
 tools/sched_ext/Makefile          |   2 +-
 tools/sched_ext/README.md         |  13 +
 tools/sched_ext/scx_tickbug.bpf.c | 534 ++++++++++++++++++++++++++++++
 tools/sched_ext/scx_tickbug.c     | 177 ++++++++++
 7 files changed, 735 insertions(+), 17 deletions(-)
 create mode 100644 tools/sched_ext/scx_tickbug.bpf.c
 create mode 100644 tools/sched_ext/scx_tickbug.c


base-commit: f66469d9905b39d2c260a15d7d6db450f4db67c1
-- 
2.43.0


^ permalink raw reply	[flat|nested] 5+ messages in thread
* [PATCH 0/2] softirq: fix idle accounting for an interrupted idle task
@ 2026-11-05 20:02 Radu Toader
  2026-11-05 20:02 ` [PATCH 2/2] softirq: Treat an interrupted idle task as idle in tick_irq_exit() Radu Toader
  0 siblings, 1 reply; 5+ messages in thread
From: Radu Toader @ 2026-11-05 20:02 UTC (permalink / raw)
  To: peterz, mingo
  Cc: juri.lelli, vincent.guittot, dietmar.eggemann, rostedt, bsegall,
	mgorman, vschneid, kprateek.nayak, tj, void, arighi, changwoo,
	frederic, anna-maria, tglx, sched-ext, linux-kernel, Radu Toader

tick_irq_exit() skips tick_nohz_irq_exit() whenever the CPU's run queue
has tasks on it, even though the CPU is sitting in the idle loop. Under
sched_ext that is an ordinary steady state - a BPF scheduler holding a
runnable task back, e.g. a cgroup at its CPU limit - and with an
unstable sched_clock the CPU's clock is then never resynchronized on
the way out of idle, so the idle stretch is charged to the next task as
runtime.

Patch 1 adds scx_tickbug, an example scheduler that holds one tagged
task runnable on an idling CPU for a configurable stretch. It exists
because the in-tree example schedulers do reach that state but leave it
within microseconds, so nothing accumulates and the bug is invisible.
Patch 2 is the fix, one line of behaviour: ask is_idle_task(current)
instead of sched_core_idle_cpu().

The patches are independent in the files they touch, but patch 1 comes
first because without it nothing in this tree can produce the state
patch 2 is about.

Measurements
------------

Two kernels built from this tree, differing only in patch 2, booted in
turn on one 16-CPU x86_64 KVM guest with "no-kvmclock tsc=unstable".
Both are needed: tsc=unstable alone only marks the clocksource,
because mark_tsc_unstable() clears sched_clock stability only when
using_native_sched_clock(), which is false under kvm-clock.

Load: one thread spinning 10 ms in every 100 ms in a cgroup, pinned to
the CPU the scheduler holds it on. 20 s windows, three repetitions per
cell, arms interleaved. The figure is the cgroup's cpu.stat usage_usec
over the thread's own CLOCK_MONOTONIC measurement of its busy time,
which is an upper bound on the CPU it can have used, so correct is
<= 1.00:

  hold (ms)   without patch 2   with patch 2
      7            1.32            0.964
     13            1.21            0.963
     23            2.00            0.963
     37            1.69            0.964
     53            1.33            0.964

  control, fair class, no BPF scheduler loaded:
                   1.005           1.006

Nothing in the fixed kernel's figures varies with the hold, and none of
the unfixed kernel's is within 20% of correct. At the 23 ms hold the
matched pair is 0.087 CPU of real work charged as 0.173 unfixed against
0.084 fixed, with 173 and 172 releases respectively - the same work,
accounted twice over.

The unfixed kernel also understates how long it held the task, which is
the same bug from the other side: the scheduler measures the hold with
scx_bpf_now(), the sched_clock that is not being resynchronized. For a
23 ms configured hold it reported a 5.2 ms mean hold where the fixed
kernel reported 15.2 ms for the identical configuration. So the fixed
kernel accounts correctly across holds three times longer than the ones
the unfixed kernel gets wrong.

An earlier measurement, with an out-of-tree sched_ext scheduler that
caps how many threads of a cgroup run at once: one thread spinning in a
cgroup limited to 0.1 CPU in an 8-vCPU guest was charged 0.27 CPUs, and
four such threads 1.12 CPUs, while a sched_switch trace and the work
completed both showed 0.100. With nohz=off, idle=poll or a stable
sched_clock the three agree. That scheduler is not part of this series,
so that one cannot be reproduced from this tree; patch 1 is what makes
everything above reproducible.

What is not shown, and what was not done
----------------------------------------

No loss in /proc/stat. The sum of that CPU's fields 2 to 9 over the
window stayed within 0.6% of the window on both kernels, so in this
configuration the kcpustat_irq_exit() half of what tick_nohz_irq_exit()
does has no visible effect, unlike the forced idle case in 548796e2e70b.
A kprobe on tick_nohz_irq_exit() filtered to the idle task counted 998
calls per 20 s window on the held CPU without patch 2 against 1321 with
it, n=3 each, but those ranges overlap and the two kernels run the load
on different timings, so that bounds the mechanism rather than
measuring it.

The ttwu_pending widening described in patch 2 was reached by reading
the code, not measured on its own. The control arm above bounds any
effect on an ordinary workload (1.005 against 1.006) without isolating
that path.

There is no automated test, and I do not see how to write a useful one.
The wrong number only appears with an unstable sched_clock, which takes
a command line, so a selftest on a machine with a stable TSC would pass
on an unfixed kernel and prove nothing. scx_tickbug reproduces the
state and reports counters, but it asserts nothing and is not a
selftest.

All of the figures come from one 16-CPU x86_64 KVM guest on one host.
No other architecture, topology or guest clocksource was tried.

Open question
-------------

The Fixes: tag on patch 2 names f0e1a0643a59, the commit that made "the
idle task runs while nr_running > 0" reachable without
CONFIG_SCHED_CORE. 548796e2e70b is arguable instead, as the commit that
fixed this same gate for core scheduling with a predicate narrower than
the problem; and the ttwu_pending case predates both. Happy to retag.

Tooling
-------

The bug was found, and both patches, both changelogs and this cover
letter were written, by Claude Opus 5 (Claude Code), in a session
measuring cgroup CPU limit enforcement under sched_ext. The prompts
were about why a cgroup's measured CPU usage disagreed with a
sched_switch trace by a factor of 2.7; the model localized that to this
gate, wrote the fix and the reproducer, ran the measurements and wrote
the text. The content is tool-generated throughout, per
Documentation/process/generated-content.rst. I have reviewed it, I am
signing off on it under the DCO, and I can answer for it in review.

Radu Toader (2):
  sched_ext: Add scx_tickbug, a scheduler that holds a task on an idle
    CPU
  softirq: Treat an interrupted idle task as idle in tick_irq_exit()

 include/linux/sched.h             |   2 -
 kernel/sched/syscalls.c           |  12 -
 kernel/softirq.c                  |  12 +-
 tools/sched_ext/Makefile          |   2 +-
 tools/sched_ext/README.md         |  13 ++
 tools/sched_ext/scx_tickbug.bpf.c | 364 ++++++++++++++++++++++++++++++
 tools/sched_ext/scx_tickbug.c     | 166 ++++++++++++++
 7 files changed, 554 insertions(+), 17 deletions(-)
 create mode 100644 tools/sched_ext/scx_tickbug.bpf.c
 create mode 100644 tools/sched_ext/scx_tickbug.c


base-commit: 05441d697f7f9a927264f4058cbbc6a03dfcac9d
-- 
2.43.0


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

end of thread, other threads:[~2026-11-06 10:23 UTC | newest]

Thread overview: 5+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-11-06  9:43 [PATCH 0/2] softirq: fix idle accounting for an interrupted idle task Radu Toader
2026-11-06  9:43 ` [PATCH 1/2] sched_ext: Add scx_tickbug, a scheduler that holds a task on an idle CPU Radu Toader
2026-11-06  9:43 ` [PATCH 2/2] softirq: Treat an interrupted idle task as idle in tick_irq_exit() Radu Toader
2026-11-06 10:23   ` Peter Zijlstra
  -- strict thread matches above, loose matches on Subject: below --
2026-11-05 20:02 [PATCH 0/2] softirq: fix idle accounting for an interrupted idle task Radu Toader
2026-11-05 20:02 ` [PATCH 2/2] softirq: Treat an interrupted idle task as idle in tick_irq_exit() Radu Toader

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®