Skip to content

sched/timer: Queue one signal of a POSIX timer at a time. - #21152

Merged
acassis merged 3 commits into
apache:masterfrom
casaroli:fb-timer-overrun-up
Nov 1, 2026
Merged

acassis merged 3 commits into
apache:masterfrom
casaroli:fb-timer-overrun-up

Conversation

@casaroli

@casaroli casaroli commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A periodic POSIX timer whose signal is not taken at once queued one more signal at each expiration. A blocked real-time signal and a signal for the handler of a thread that does not run are not merged, so the pending signals grew until the preallocated ones ran out. The timer runs in the watchdog interrupt, which cannot allocate more: nxsig_dispatch() then failed with EAGAIN, and the DEBUGVERIFY() of timer_signotify() stopped the system (with CONFIG_DEBUG_ASSERTIONS). An unprivileged program can do this in a few milliseconds.

  1. sched/timer: Queue one signal of a POSIX timer at a time. POSIX: "Only a single signal shall be queued to the process for a given timer at any point in time. When a timer for which a signal is still pending expires, no signal shall be queued, and a timer overrun shall occur." The pending and the queued signal entries now keep their source (the timer, or NULL). At an expiration the timer checks with nxsig_source_queued() whether its last signal is still pending or queued (the same source and signal number). If so, or if no signal can be queued, the expiration is an overrun. The overrun count starts again with each signal that is queued, and the periods that the watchdog missed add to it. timer_getoverrun() gives it, at most DELAYTIMER_MAX. The internal nxsig_tcbdispatch(), nxsig_dispatch() and group_signal() get a source argument; their other callers pass NULL. nxsig_notification(), which drivers use, keeps its arguments; only the timer calls the internal nxsig_notification_source().
  2. sched/signal: Let an interrupt handler use the reserved pending signals. nxsig_alloc_dyn_pending() returned EAGAIN in an interrupt as soon as a free list was empty, so the structures reserved for interrupt handlers (CONFIG_SIG_PREALLOC_IRQ_ACTIONS) were never used. Now it fails only when the free list and its reserve are both empty.
  3. Documentation: timer_getoverrun() is not "not implemented" any more; the signature of group_signal().

Commit 2 does not need commit 1. It is in this PR because the same test finds it; it can go in a PR of its own if reviewers prefer.

Impact

  • A periodic POSIX timer has at most one queued signal, and timer_getoverrun() counts the expirations in between, as POSIX says. A timer whose signals are not taken does not stop the system.
  • Signals from interrupt handlers (timers, driver notifications) can use the reserved structures.
  • Kernel-internal change only: no public header, no user-visible structure. The same code in flat, protected and kernel builds, and in SMP builds (the search holds the critical section and the group's tg_lock).

Testing

Test program timerovr.c (7 cases; not in nuttx-apps). The POSIX timer test of ostest passes before and after.

Case master this PR
waitmask: after a sigtimedwait() with a zero timeout, a blocked signal that comes during usleep() stays pending, and the sleep lasts its time (master has this fix since #21065) FAIL (the signal was lost; the sleep ended after 10 ms of 60) PASS
drain: three sigqueue() of a real-time signal are taken three times (a check of the test) PASS PASS
blocked: a 2 ms timer, its real-time signal blocked for 300 ms: one signal pending, timer_getoverrun() = DELAYTIMER_MAX FAIL (0 pending) PASS
again: after the signal was taken, the timer queues a new one FAIL PASS
two: two timers with the same signal and value: one signal of each FAIL (1 pending) PASS
handler: a handler for a thread that cannot run for 200 ms (SIGEV_THREAD_ID, a busy thread of higher priority) runs once system stop: Assertion failed at file: timer/timer_settime.c:79 PASS
Build Config (QEMU, HVF) master (192ac6d) this PR
kernel, arm64 qemu-armv8a:knsh (DEBUG_ASSERTIONS is on in this config) 4 FAIL, then the system stops in the handler case all PASS; ostest 0 (knsh and nsh)
kernel, arm64 qemu-armv8a:knsh + DEBUG_FEATURES all PASS
flat, arm64 qemu-armv8a:nsh (the test as a builtin app) 4 FAIL, then the system stops in the handler case all PASS; ostest 0
flat, arm64, SMP qemu-armv8a:nsh_smp, 2 CPUs (the test as a builtin app; the handler case keeps both threads on CPU 0) ostest does not end (its spinlock test) all PASS; ostest 0

The "this PR" column is from this head, rebased on master 290d9ec (nuttx-apps d40132fba).

Builds (make), each without new warnings: nucleo-f072rb:nsh (flat, armv6-m, partial signals, no pthreads, no POSIX timers), nucleo-f302r8:ihm07m1_f32 (flat, no signals), esp32s3-devkit:knsh (kernel, xtensa; it builds the two nxsig_tcbdispatch() callers in arch/xtensa/src/esp32s3), rv-virt:pnsh64 (protected, risc-v), rv-virt:knsh64 (kernel, risc-v), qemu-armv8a:nsh_smp (SMP), qemu-armv8a:nsh with DISABLE_PTHREAD (no group members; ostest and getprime off, they need pthreads): OK. lm3s6965-ek:qemu-protected (protected, armv7-m) overflows its kernel flash by 720 bytes with the local GCC 13.2 (master: 288 bytes); with CONFIG_DISABLE_MQUEUE_SYSV (#21321) it links.

tools/checkpatch.sh -c -u -m -g passes.

@github-actions github-actions Bot added Area: Documentation Improvements or additions to documentation Size: M The size of the change in this PR is medium labels Oct 30, 2026
@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

MemBrowse Memory Report · Explore what changed →

arduino-mega2560

  • sram: .bss +16 B (+0.6%, 2,738 B / 8,192 B, total: 33% used)

esp32-devkitc

  • ROM: .flash.text +64 B (+0.0%, 128,432 B / 4,194,272 B, total: 3% used)
  • dram0_0_seg: .dram0.bss +96 B (+1.0%, 9,992 B / 180,736 B, total: 6% used)
  • irom0_0_seg: .flash.text +64 B (+0.1%, 94,304 B / 3,342,304 B, total: 3% used)

hifive1-revb

  • flash: .text +64 B (+0.1%, 85,088 B / 4,194,304 B, total: 2% used)
  • sram: .bss +32 B (+0.8%, 3,924 B / 16,384 B, total: 24% used)

mirtoo

  • kseg1_datamem: .bss +32 B (+1.3%, 2,552 B / 32,256 B, total: 8% used)

qemu-armv8a

  • Code: .rodata -24 B, .text.group_signal +16 B, .text.group_signal_handler +4 B, .text.nxsig_dispatch +12 B, .text.nxsig_dispatch_unmasked +4 B, .text.nxsig_kill +4 B, .text.nxsig_notification -84 B, .text.nxsig_queue +4 B, .text.nxsig_tcbdispatch +52 B, .text.nxsig_tgkill +4 B, .text.nxtask_exithook +4 B, .text.timer_timeout +72 B (+0.2%, 355,100 B)
  • Data: .bss.g_sigpool +192 B (+0.2%, 77,783 B)

qemu-intel64

  • Code: .text +433 B (+0.0%, 8,688,606 B)
  • Data: .bss +192 B, .rodata -96 B (+0.1%, 125,177 B)

rx65n-rsk2mb

  • RAM: .bss +96 B (+1.5%, 6,650 B / 262,144 B, total: 3% used)
  • ROM: .text +192 B (+0.2%, 90,632 B / 2,097,152 B, total: 4% used)

s698pm-dkit

  • Code: .text +864 B (+0.2%, 386,432 B)
  • Data: .bss +64 B (+0.5%, 11,883 B)

stm32-nucleo-f103rb

  • sram: .bss +32 B (+1.3%, 2,420 B / 20,480 B, total: 12% used)

Catch memory regressions in CI with MemBrowse. Free for open source.

Comment thread sched/group/group_signal.c Resolved
Comment thread sched/signal/sig_dispatch.c Resolved
Comment thread sched/signal/sig_dispatch.c Resolved
Comment thread sched/signal/sig_notification.c Resolved
POSIX: "Only a single signal shall be queued to the process for a given
timer at any point in time.  When a timer for which a signal is still
pending expires, no signal shall be queued, and a timer overrun shall
occur."  NuttX queued one more signal at each expiration.  A blocked
real-time signal and a signal for a handler of a thread that does not
run are not merged, so the pending signals grew with each expiration.
The timer runs in the watchdog interrupt, which cannot allocate more:
when the preallocated ones ran out, nxsig_dispatch() failed with EAGAIN
and the DEBUGVERIFY() of timer_signotify() stopped the system.

The pending and the queued signal entries now keep their source (the
timer, or NULL), which nxsig_tcbdispatch(), nxsig_dispatch(),
nxsig_notification_source() and group_signal() pass on, also when a
pending signal is unmasked.  At an expiration the timer checks with
nxsig_source_queued() whether its last signal is still pending or
queued (the same source and signal number): if so, or if no signal can
be queued, the expiration is an overrun.  The overrun count starts again
with each signal that is queued, and the periods that the watchdog
missed add to it; timer_getoverrun() gives it, at most DELAYTIMER_MAX.
The other callers of nxsig_tcbdispatch() and nxsig_dispatch() pass
NULL; nxsig_notification(), which drivers use, keeps its arguments and
passes NULL.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
NuttX keeps pending signal structures for interrupt handlers
(CONFIG_SIG_PREALLOC_IRQ_ACTIONS), and nxsig_alloc_pendingsignal() and
nxsig_alloc_pendingsigaction() take them in an interrupt when the free
lists are empty.  But nxsig_alloc_dyn_pending() returned EAGAIN in an
interrupt as soon as a free list was empty, so the reserved structures
were never used: when other tasks held the few preallocated pending
signals, every signal sent from an interrupt handler (a POSIX timer, a
driver notification) failed.

In an interrupt, fail only when a needed free list and its reserve are
both empty.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
…nal().

timer_getoverrun() counts the overruns of POSIX now: remove the notes
that say it is not implemented.  group_signal() takes the source of the
signal.

Assisted-by: Claude Code:claude-opus-5-5
Signed-off-by: Marco Casaroli <marco.casaroli@gmail.com>
@casaroli
casaroli force-pushed the fb-timer-overrun-up branch from cfb672a to 769b6a1 Compare November 1, 2026 10:50
@github-actions github-actions Bot added the Arch: xtensa Issues related to the Xtensa architecture label Nov 1, 2026
@casaroli
casaroli marked this pull request as draft November 1, 2026 11:28
@casaroli
casaroli marked this pull request as ready for review November 1, 2026 11:28
@acassis
acassis merged commit dde0b72 into apache:master Nov 1, 2026
37 of 41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Arch: xtensa Issues related to the Xtensa architecture Area: Documentation Improvements or additions to documentation Size: M The size of the change in this PR is medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants