[PATCH 1/2] timer: add add_timer_active_cpu()

From: Partha Satapathy

Date: Thu Apr 23 2026 - 05:23:44 EST


From: Partha Sarathi Satapathy <partha.satapathy@xxxxxxxxxx>

add_timer_on() can queue a timer on a CPU that goes offline before the
timer is enqueued. When that happens, the timer remains unserviced until
the CPU comes back online.

Callers can try to avoid that by checking CPU state themselves, but that
does not close the race with CPU hotplug. Taking the hotplug lock around
every enqueue is also too expensive for users that only want CPU-local
placement as a performance hint.

Add add_timer_active_cpu() for callers that want to queue a timer on a
specific CPU when that CPU is active, but otherwise fall back to an
active CPU. Implement this by teaching the enqueue path to verify that
the target timer base is active and, if not, requeue the timer on the
current CPU.

Leave add_timer_on() semantics unchanged for callers that require strict
CPU placement.

Signed-off-by: Partha Sarathi Satapathy <partha.satapathy@xxxxxxxxxx>
---
include/linux/timer.h | 1 +
kernel/time/timer.c | 45 ++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 45 insertions(+), 1 deletion(-)

diff --git a/include/linux/timer.h b/include/linux/timer.h
index 62e1cea71125..8c771b367662 100644
--- a/include/linux/timer.h
+++ b/include/linux/timer.h
@@ -148,6 +148,7 @@ static inline int timer_pending(const struct timer_list * timer)
}

extern void add_timer_on(struct timer_list *timer, int cpu);
+extern void add_timer_active_cpu(struct timer_list *timer, int cpu);
extern int mod_timer(struct timer_list *timer, unsigned long expires);
extern int mod_timer_pending(struct timer_list *timer, unsigned long expires);
extern int timer_reduce(struct timer_list *timer, unsigned long expires);
diff --git a/kernel/time/timer.c b/kernel/time/timer.c
index 1f2364126894..c73a28701a31 100644
--- a/kernel/time/timer.c
+++ b/kernel/time/timer.c
@@ -260,6 +260,7 @@ struct timer_base {
bool next_expiry_recalc;
bool is_idle;
bool timers_pending;
+ bool is_active;
DECLARE_BITMAP(pending_map, WHEEL_SIZE);
struct hlist_head vectors[WHEEL_SIZE];
} ____cacheline_aligned;
@@ -1296,7 +1297,7 @@ EXPORT_SYMBOL(add_timer_global);
*
* See add_timer() for further details.
*/
-void add_timer_on(struct timer_list *timer, int cpu)
+static void __add_timer_on(struct timer_list *timer, int cpu, bool need_ol_cpu)
{
struct timer_base *new_base, *base;
unsigned long flags;
@@ -1333,6 +1334,18 @@ void add_timer_on(struct timer_list *timer, int cpu)
WRITE_ONCE(timer->flags,
(timer->flags & ~TIMER_BASEMASK) | cpu);
}
+#ifdef CONFIG_HOTPLUG_CPU
+ if (need_ol_cpu) {
+ if (!base->is_active) {
+ raw_spin_unlock(&base->lock);
+ base = this_cpu_ptr(&timer_bases[BASE_LOCAL]);
+ raw_spin_lock(&base->lock);
+ cpu = smp_processor_id();
+ WRITE_ONCE(timer->flags,
+ (timer->flags & ~TIMER_BASEMASK) | cpu);
+ }
+ }
+#endif /* CONFIG_HOTPLUG_CPU */
forward_timer_base(base);

debug_timer_activate(timer);
@@ -1340,8 +1353,31 @@ void add_timer_on(struct timer_list *timer, int cpu)
out_unlock:
raw_spin_unlock_irqrestore(&base->lock, flags);
}
+
+void add_timer_on(struct timer_list *timer, int cpu)
+{
+ bool need_ol_cpu = false;
+
+ __add_timer_on(timer, cpu, need_ol_cpu);
+}
EXPORT_SYMBOL_GPL(add_timer_on);

+/**
+ * add_timer_active_cpu - Start a timer on a particular CPU if online or current
+ * @timer: The timer to be started
+ * @cpu: The CPU to start it on
+ *
+ * This is like add_timer_on(), except that it queues the timer on the
+ * given CPU only when that CPU is online or on the current CPU.
+ */
+void add_timer_active_cpu(struct timer_list *timer, int cpu)
+{
+ bool need_ol_cpu = true;
+
+ __add_timer_on(timer, cpu, need_ol_cpu);
+}
+EXPORT_SYMBOL_GPL(add_timer_active_cpu);
+
/**
* __timer_delete - Internal function: Deactivate a timer
* @timer: The timer to be deactivated
@@ -2507,6 +2543,7 @@ int timers_prepare_cpu(unsigned int cpu)
base->next_expiry_recalc = false;
base->timers_pending = false;
base->is_idle = false;
+ base->is_active = true;
}
return 0;
}
@@ -2535,6 +2572,11 @@ int timers_dead_cpu(unsigned int cpu)

WARN_ON_ONCE(old_base->running_timer);
old_base->running_timer = NULL;
+ /*
+ * Make the dead CPU base unavailable to add_timer_on()
+ * when the caller wants an active target CPU.
+ */
+ old_base->is_active = false;

for (i = 0; i < WHEEL_SIZE; i++)
migrate_timer_list(new_base, old_base->vectors + i);
@@ -2559,6 +2601,7 @@ static void __init init_timer_cpu(int cpu)
raw_spin_lock_init(&base->lock);
base->clk = jiffies;
base->next_expiry = base->clk + TIMER_NEXT_MAX_DELTA;
+ base->is_active = true;
timer_base_init_expiry_lock(base);
}
}
--
2.43.7