[PATCH 0/3] Stop the idle tick on polling idle on nohz_full CPUs

From: Marco Crivellari

Date: Fri Nov 06 2026 - 11:07:58 EST


Hi,

On nohz_full CPUs booted with idle=poll, the tick interrupts the task once
after every wakeup from idle. do_idle() restarts the tick before polling,
and on wakeup tick_nohz_idle_exit() only re-evaluates the tick if it is
stopped, so it keeps running and fires in userspace before
tick_nohz_irq_exit() stops it. Without idle=poll this doesn't happen:
since commit a5183862e76f ("tick/nohz: Conditionally restart tick on idle
exit") a nohz_full CPU leaves idle with the tick still stopped.

Marcelo Tosatti proposed stopping the tick on polling idle entry:
https://lore.kernel.org/all/aQiWfnnSzxsnwa2o@tpad/

Following Frederic's review, this series does it only on nohz_full CPUs,
and handles on the target CPU the cases where stopping the tick in polling
idle would leave a timer unprogrammed.

[ P1 ] tick/nohz: Re-evaluate the tick in polling idle and on user entry

On nohz_full CPUs the tick is re-evaluated on IRQ exit and on idle exit.
In idle, IRQ exit only does accounting and relies on the idle loop
running again, which cpu_idle_poll() doesn't do: it only returns on
TIF_NEED_RESCHED. P1 closes three gaps:

- A nohz_full kick handled in idle, e.g. after a timer is enqueued from
an interrupt on the polling CPU: nohz_full_kick_func() sets
TIF_NEED_RESCHED so that the polling loop exits and
tick_nohz_idle_exit() re-evaluates the tick.

- The expiry of a stopped tick in idle: a stopped tick is programmed for
the next timer only, and reprogramming it for the following one is
left to the idle loop. tick_nohz_handler() sets TIF_NEED_RESCHED so
that timers already queued are programmed in turn.

- The return to userspace: if the tick was left running in the kernel,
e.g. because a tick dependency was cleared (which doesn't kick the
CPU), stop it from context tracking instead of letting it fire once
in userspace.

The first two only matter once P2 stops the tick in polling idle, so P1
must come first.

[ P2 ] sched/idle: Stop the tick on polling idle on nohz_full CPUs

With idle=poll, nohz_full CPUs stop the tick on idle entry instead of
restarting it. Housekeeping CPUs keep restarting it, and the
tick_check_broadcast_expired() polling case is unchanged.

[ P3 ] Documentation: no_hz: Add tick stop in polling idle

Document the above in the idle=poll item of "Known Issues".

With big effort on the Claude side, I made a test suite to help testing
the work:

https://github.com/DispatchCode/nohz-full-polling-idle-tick-test-suite

The following is a short summary of the results.

On x86_64 guest, HZ=1000, nohz_full=2-3 idle=poll), a task alternating
100us sleeps with 10ms of userspace work, 200 times:
- ticks in userspace: 200/200 -> 0/200
- ticks while polling: 35 -> 0
- housekeeping CPUs: unchanged (tick runs while polling)
- P2 without the kick change: 15 of 20 timers enqueued from an
interrupt on the polling CPU lost
- P2 without the stopped tick fix: the second of two queued timers up
to 1.9s late
Tick dependencies (POSIX CPU timers, two tasks on the CPU) and CPU hotplug
were also checked.

Thanks,

Marco Crivellari (3):
tick/nohz: Re-evaluate the tick on polling idle and on user entry
sched/idle: Stop the tick on polling idle on nohz_full CPUs
Documentation: no_hz: Add tick stop in polling idle

Documentation/timers/no_hz.rst | 3 ++
include/linux/tick.h | 10 ++++++
kernel/context_tracking.c | 9 ++++++
kernel/sched/idle.c | 5 ++-
kernel/time/tick-sched.c | 59 +++++++++++++++++++++++++++++++++-
5 files changed, 84 insertions(+), 2 deletions(-)

--
2.56.0