Re: [PATCH 1/3] tick/nohz: Re-evaluate the tick on polling idle and on user entry
From: Marco Crivellari
Date: Mon Nov 09 2026 - 12:30:07 EST
Hi,
On Fri, Nov 6, 2026 at 11:15 PM Frederic Weisbecker <frederic@xxxxxxxxxx> wrote:
>
> Le Fri, Nov 06, 2026 at 05:06:13PM +0100, Marco Crivellari a écrit :
> > On nohz_full CPUs the tick is re-evaluated on IRQ exit and on idle exit.
> > There are three places where this is not enough:
> >
> > - A kick handled while the CPU is in polling idle: in idle,
> > tick_nohz_irq_exit() only does accounting and relies on the idle loop
> > iterating again. HLT and MWAIT return on any IRQ, but cpu_idle_poll()
> > only returns on TIF_NEED_RESCHED. The tick always runs in polling idle
> > for now, but once the next patch stops it there, a timer enqueued from
> > an interrupt on the polling CPU would not be programmed until something
> > else wakes the CPU up.
> >
> > Set TIF_NEED_RESCHED on the idle task when the kick is handled in idle,
> > so that cpu_idle_poll() returns and tick_nohz_idle_exit() re-evaluates
> > the tick.
> >
> > - The return to userspace: if the tick is left running in the kernel,
> > for example because a tick dependency was cleared (tick_nohz_dep_clear*()
> > don't kick the CPU) or a timer that kept it running was cancelled, it
> > fires at least once in userspace before tick_nohz_irq_exit() stops it.
> >
> > Stop it from context tracking right before resuming userspace. When the
> > tick is already stopped, the common case, this costs a few flag tests
> > on each return to userspace.
>
> This patch should only be about the return to userspace. Sorry if I
> confused you about that.
Oh, okay. Maybe I misunderstood; no problem anyhow, I will move that
hunk to the other patch.
> > --- a/kernel/time/tick-sched.c
> > +++ b/kernel/time/tick-sched.c
> > @@ -323,8 +323,17 @@ static enum hrtimer_restart tick_nohz_handler(struct hrtimer *timer)
> > * - to the idle task if in dynticks-idle
> > * - to IRQ exit if in full-dynticks.
> > */
> > - if (unlikely(tick_sched_flag_test(ts, TS_FLAG_STOPPED)))
> > + if (unlikely(tick_sched_flag_test(ts, TS_FLAG_STOPPED))) {
> > + /*
> > + * In polling idle on nohz_full the idle loop doesn't iterate on
> > + * interrupts, so it would never reprogram the tick for the next
> > + * timer. Make cpu_idle_poll() return.
> > + */
> > + if (tick_sched_flag_test(ts, TS_FLAG_INIDLE) &&
> > + tick_nohz_full_cpu(smp_processor_id()))
> > + set_tsk_need_resched(current);
> > return HRTIMER_NORESTART;
> > + }
>
> I don't think we need this. The change in nohz_full_kick_func() should be
> enough.
Mmh I'm not entirely sure. Look at this:
Without that hunk:
<idle>-0 [002] dN.1. 21.838685: csd_function_entry:
func=idle_cycle [nohz_poll_timer_test], csd=0000000006a53f0a
<idle>-0 [002] dN.2. 21.889256: hrtimer_start:
hrtimer=000000007acc0071 function=tick_nohz_handler
expires=21879000000 softexpires=21879000000 mode=ABS|PINNED|HARD
was_armed=1
<idle>-0 [002] d.h1. 21.900238: local_timer_entry: vector=236
<idle>-0 [002] d.h1. 21.900238: hrtimer_expire_entry:
hrtimer=000000007acc0071 function=tick_nohz_handler now=21879002056
<idle>-0 [002] ..s1. 21.900246: timer_expire_entry:
timer=0000000076975009 function=test_timer_fn [nohz_poll_timer_test]
now=4294689147 baseclk=4294689147
<idle>-0 [002] dN.2. 23.737243: hrtimer_start:
hrtimer=000000007acc0071 function=tick_nohz_handler
expires=23716000000 softexpires=23716000000 mode=ABS|PINNED|HARD
was_armed=0
<idle>-0 [002] dNh1. 23.737250: local_timer_entry: vector=236
<idle>-0 [002] dNh1. 23.737251: hrtimer_expire_entry:
hrtimer=000000007acc0071 function=tick_nohz_handler now=23716014595
<idle>-0 [002] .Ns1. 23.737255: timer_expire_entry:
timer=00000000134bb09d function=test_timer_fn [nohz_poll_timer_test]
now=4294690984 baseclk=4294689177
<idle>-0 [002] dN.1. 23.737261: csd_function_entry:
func=watchdog_check_skew_remote, csd=00000000ecfea5e9
With the hunk applied:
<idle>-0 [002] dN.1. 10.363861: csd_function_entry:
func=idle_cycle [nohz_poll_timer_test], csd=0000000046d09235
<idle>-0 [002] dN.2. 10.414660: hrtimer_start:
hrtimer=00000000ccc99756 function=tick_nohz_handler
expires=10405000000 softexpires=10405000000 mode=ABS|PINNED|HARD
was_armed=1
<idle>-0 [002] d.h1. 10.425656: local_timer_entry: vector=236
<idle>-0 [002] d.h1. 10.425656: hrtimer_expire_entry:
hrtimer=00000000ccc99756 function=tick_nohz_handler now=10405001482
<idle>-0 [002] .ns1. 10.425660: timer_expire_entry:
timer=00000000654cb5a4 function=test_timer_fn [nohz_poll_timer_test]
now=4294677672 baseclk=4294677672
<idle>-0 [002] dN.2. 10.425661: hrtimer_start:
hrtimer=00000000ccc99756 function=tick_nohz_handler
expires=10435000000 softexpires=10435000000 mode=ABS|PINNED|HARD
was_armed=0
<idle>-0 [002] d.h1. 10.455656: local_timer_entry: vector=236
<idle>-0 [002] d.h1. 10.455657: hrtimer_expire_entry:
hrtimer=00000000ccc99756 function=tick_nohz_handler now=10435001994
<idle>-0 [002] .ns1. 10.455659: timer_expire_entry:
timer=00000000a060ca9a function=test_timer_fn [nohz_poll_timer_test]
now=4294677702 baseclk=4294677702
With the hunk, the stopped tick firing for the first timer sets
TIF_NEED_RESCHED, the polling loop exits and the tick is reprogrammed
for the second timer within a few microseconds; the 10 iterations
captured take 0.92s instead of ~18s. Without that hunk, the CPU stops
spinning on an unrelated event.
So in my understanding tick_nohz_idle_exit() program the tick only for
the earliest timer; when it expire, the tick is stopped and not
rearmed in tick_nohz_handler() (HRTIMER_NORESTART). Without setting
NEED_RESCHED if another timer has been queued it has to wait for
another event that set NEED_RESCHED.
> >
> > hrtimer_forward(timer, now, TICK_NSEC);
> >
> > @@ -414,6 +423,12 @@ static void nohz_full_kick_func(struct irq_work *work)
> > if (WARN_ON_ONCE(tick_sched_flag_test(ts, TS_FLAG_INIDLE)))
> > return;
> > tick_nohz_full_update_tick(ts);
> > + } else if (tick_sched_flag_test(ts, TS_FLAG_INIDLE)) {
> > + /*
> > + * In idle, tick_nohz_irq_exit() doesn't re-evaluate the tick, and
> > + * cpu_idle_poll() only returns on need_resched().
> > + */
> > + set_tsk_need_resched(current);
>
> And this part should move to the next patch.
Sure, I will!
Thanks,
--
Marco Crivellari
SUSE Labs