sched: Fix round-robin and watchdog re-arm under CONFIG_SCHED_TICKLESS. - #20047
Open
FelipeMdeO wants to merge 2 commits into
Open
sched: Fix round-robin and watchdog re-arm under CONFIG_SCHED_TICKLESS.#20047FelipeMdeO wants to merge 2 commits into
FelipeMdeO wants to merge 2 commits into
Conversation
Under CONFIG_SCHED_TICKLESS, nxsched_reassess_timer() (which programs the round-robin timeslice timer) was only called from sched_unlock.c, guarded by "the task that was just running is itself SCHED_RR and had already exhausted its timeslice". There was no hook to arm the timeslice timer when a *new* SCHED_RR task starts running via an ordinary context switch (e.g. a non-RR task blocking and handing off to a same-priority RR task). Since tickless mode has no periodic tick interrupt to fall back on, the round-robin accounting for that task was never re-armed, and it would run to completion instead of yielding to other same-priority SCHED_RR tasks. Reproducer: apps/testing/ostest's rr_test (two SCHED_RR pthreads at the same priority) never interleaves under tickless -- one thread runs all its iterations before the other's first iteration is even recorded, and zero calls to the arch's up_timer_start() occur during the whole test. Fix: when a task transitions to TSTATE_TASK_RUNNING in nxsched_add_readytorun() (non-SMP path) and is SCHED_RR, explicitly call nxsched_reassess_timer() so its timeslice timer is armed. Validated on real hardware with apps/testing/ostest's rr_test: * esp32c3-legacy-devkit:tickless (own esp32c3_tickless.c backend) -- PASS * esp32c3-devkit:tickless (shared esp_tickless.c backend) -- PASS * esp32s3-xiao (own esp32s3_tickless.c backend) -- PASS The equivalent CONFIG_SMP path (nxsched_switch_running()) is not addressed here -- no SMP hardware was available to validate against. Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Felipe Moura <moura.fmo@gmail.com>
Under CONFIG_SCHED_TICKLESS (non-alarm), wd_timer_start() computed the delay to the next watchdog expiration as a plain subtraction of two absolute clock_t values (next_tick - clock_systime_ticks()) with no clamping, then handed it to up_timer_tick_start(). Two related bugs followed from that: 1. If the watchdog at the head of the active list had already expired by the time wd_timer_start() ran (routine when wd_start()/wd_cancel() are called back-to-back with very short or zero delays, and clock_t is a signed int64_t), the subtraction went negative. That negative value flows through clock_ticks2time() into a timespec with a negative tv_nsec, which arch code's NSEC_2_CTICK()-style macros then cast to unsigned before multiplying -- turning "already due" into a multi-year hardware alarm that never fires. 2. Even clamping just to zero isn't enough: many oneshot hardware timers (e.g. the ESP32-S3 systimer) only raise their interrupt when the running counter *crosses* the target from below. Arming for "right now" can miss that edge if the target is already at or behind the counter by the time the register write lands, silently dropping the alarm forever. Fix: clamp the computed delta to a minimum of one tick, so arch code always gets a target strictly in the future. This matches the coarser but dependable behavior tick-based (non-tickless) mode already has for a zero-delay wd_start() -- it, too, only ever fires on the next periodic tick. Reproducer: apps/testing/ostest's wdog_test() spawns 4 threads that hammer wd_start()/wd_cancel() with delays from 0ns up to ~12us across 1024 iterations each (wdtest_rand()). Under CONFIG_SCHED_TICKLESS on esp32s3-xiao, this reliably hung forever inside the first iteration's busy-wait for its own callback, with zero further watchdog callbacks ever firing system-wide. Validated on real hardware (esp32s3-xiao): apps/testing/ostest run to completion end-to-end (all subtests, including wdog_test, hrtimer, round-robin, sporadic, barrier, priority inheritance, etc.) with CONFIG_SCHED_TICKLESS + CONFIG_TESTING_OSTEST enabled, no hangs, no failed asserts -- "ostest_main: Exiting with status 0". Assisted-by: Claude:claude-sonnet-5 Signed-off-by: Felipe Moura <moura.fmo@gmail.com>
FelipeMdeO
requested review from
GUIDINGLI,
jerpelea,
masayuki2009 and
xiaoxiang781216
as code owners
September 2, 2026 15:34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
apps/testing/ostest's full suite hangs or crashes underCONFIG_SCHED_TICKLESSdue to two separate, generic (arch-independent)scheduler bugs. Both are required together to get the suite passing
end-to-end under tickless.
sched/sched_addreadytorun.c:nxsched_reassess_timer()(whicharms the round-robin timeslice timer) was only ever called from
sched_unlock.c, gated on the task that was running havingalready exhausted its timeslice. Nothing armed the timer when a
new
SCHED_RRtask started running via an ordinary contextswitch, so under tickless (no periodic tick to fall back on) two
same-priority
SCHED_RRthreads never interleaved -- one just ranto completion before the other's first iteration was recorded.
sched/wdog/wdog.h:wd_timer_start()computed the delay to thenext watchdog expiration with no clamp. An already-due watchdog
(common with back-to-back short-delay
wd_start()/wd_cancel()calls) made that delta go negative -- which arch code's
NSEC_2_CTICK()-style macros then cast to unsigned, turning"already due" into a multi-year alarm that never fires. Clamping to
zero isn't sufficient either: oneshot hardware comparators (e.g.
the ESP32-S3 systimer) only interrupt on the counter crossing the
target from below, so arming for "right now" can miss the edge and
never fire at all.
Neither bug is specific to any one chip -- both are in generic
sched/code reachable by any architecture combiningCONFIG_SCHED_TICKLESSwithCONFIG_RR_INTERVAL > 0(round-robinbug) or any watchdog user issuing short/zero delays (wdog bug). The
round-robin bug is additionally confirmed on 3 independent tickless
backends across 2 architectures (Xtensa ESP32-S3, RISC-V ESP32-C3
legacy and shared backends).
The in-tree
boards/xtensa/esp32s3/esp32s3-devkit/configs/ticklessexample(
CONFIG_ESP32S3_TICKLESS+CONFIG_TESTING_OSTEST, both alreadyenabled by default in that defconfig) reproduces the round-robin crash
immediately and deterministically on real hardware -- see the
Reproduction and Testing sections below.
Impact
Impact on hardware (will arch(s) / board(s) / driver(s) change)?
YES -- fixes generic
sched/code exercised by any board usingCONFIG_SCHED_TICKLESS; no board/driver files touched.Anything else to consider or add? The
CONFIG_SMPpath of the round-robin fix (nxsched_switch_running()) is intentionally not addressed here -- no SMP hardware was available to validate against. Flagging for maintainer input on whether that gap should block this PR or be tracked separately.Testing
I confirm that changes are verified on local setup and works as intended:
xtensa-esp-elf-gcc, crosstool-NG esp-14.2.0_20241119, 14.2.0).xtensa (esp32s3-xiao):boards/xtensa/esp32s3/esp32s3-xiao/configs/usbnsh+CONFIG_ESP32S3_TICKLESS=y+CONFIG_TESTING_OSTEST=y(kconfig-tweak).xtensa (esp32s3-devkit): in-treeesp32s3-devkit:ticklessexample config, unmodified.riscv (esp32c3-legacy-devkit): in-tree:ticklessconfig (ownesp32c3_tickless.cbackend) -- round-robin fix only.riscv (esp32c3-devkit): in-tree:ticklessconfig (sharedesp_tickless.cbackend) -- round-robin fix only.How to reproduce (before this fix)
Simplest repro uses only in-tree, unmodified files -- no board-specific
changes needed:
(Any board with
CONFIG_ESP32S3_TICKLESS/equivalent tickless backendCONFIG_TESTING_OSTEST+CONFIG_RR_INTERVAL > 0reproduces theround-robin crash identically -- this is just the simplest way to get
there with zero custom config, since it's an existing in-tree example.)
Console output before the fix (
esp32s3-devkit:tickless, stockmaster):Note
id=1fully finishing (found 3246 primes) beforeid=2evenstarts -- the two same-priority
SCHED_RRthreads neverinterleaved, so
rr_test's own pass check(
apps/testing/ostest/roundrobin.c:263-280, which requires a threadswitch within the first
CONFIG_TESTING_OSTEST_RR_RUNSentries ofg_rr_values[]) fails and hitsASSERT(false).The watchdog bug (bug 2/2 in this PR) is timing-dependent and didn't
trigger on that particular
esp32s3-devkitrun, but reliably hungapps/testing/ostest/wdog.c'swdog_test()forever onesp32s3-xiao(confirmed via a temporary progress printf that everyone of its 4 threads got stuck on the very first iteration of
wdtest_rand()'s busy-wait for its own watchdog callback -- thecallback never fired).
After this fix
Full, unmodified
apps/testing/ostestsuite,esp32s3-xiao,CONFIG_SCHED_TICKLESS+CONFIG_TESTING_OSTEST, both commitsapplied -- runs to completion with no hangs, no failed asserts:
Note both
id=1andid=2now start together (get_primes_thread id=1 startedimmediately followed byid=2 started, instead of onefinishing before the other starts), and
rr_test: Done(notRoundrobin Failed) confirmsroundrobin.c's own interleaving checkpassed.
The round-robin fix alone was additionally validated with just
rr_test()(skipping the rest of the suite) on bothesp32c3-legacy-devkit:ticklessandesp32c3-devkit:tickless--PASS on both, confirming the fix is backend- and architecture-independent.