]> git.ipfire.org Git - thirdparty/kernel/linux.git/commitdiff
timekeeping: Provide infrastructure for coupled clockevents
authorThomas Gleixner <tglx@kernel.org>
Tue, 24 Feb 2026 16:36:40 +0000 (17:36 +0100)
committerPeter Zijlstra <peterz@infradead.org>
Fri, 27 Feb 2026 15:40:08 +0000 (16:40 +0100)
Some architectures have clockevent devices which are coupled to the system
clocksource by implementing a less than or equal comparator which compares
the programmed absolute expiry time against the underlying time
counter. Well known examples are TSC/TSC deadline timer and the S390 TOD
clocksource/comparator.

While the concept is nice it has some downsides:

  1) The clockevents core code is strictly based on relative expiry times
     as that's the most common case for clockevent device hardware. That
     requires to convert the absolute expiry time provided by the caller
     (hrtimers, NOHZ code) to a relative expiry time by reading and
     substracting the current time.

     The clockevent::set_next_event() callback must then read the counter
     again to convert the relative expiry back into a absolute one.

  2) The conversion factors from nanoseconds to counter clock cycles are
     set up when the clockevent is registered. When NTP applies corrections
     then the clockevent conversion factors can deviate from the
     clocksource conversion substantially which either results in timers
     firing late or in the worst case early. The early expiry then needs to
     do a reprogam with a short delta.

     In most cases this is papered over by the fact that the read in the
     set_next_event() callback happens after the read which is used to
     calculate the delta. So the tendency is that timers expire mostly
     late.

All of this can be avoided by providing support for these devices in the
core code:

  1) The timekeeping core keeps track of the last update to the clocksource
     by storing the base nanoseconds and the corresponding clocksource
     counter value. That's used to keep the conversion math for reading the
     time within 64-bit in the common case.

     This information can be used to avoid both reads of the underlying
     clocksource in the clockevents reprogramming path:

     delta = expiry - base_ns;
     cycles = base_cycles + ((delta * clockevent::mult) >> clockevent::shift);

     The resulting cycles value can be directly used to program the
     comparator.

  2) As #1 does not longer provide the "compensation" through the second
     read the deviation of the clocksource and clockevent conversions
     caused by NTP become more prominent.

     This can be cured by letting the timekeeping core compute and store
     the reverse conversion factors when the clocksource cycles to
     nanoseconds factors are modified by NTP:

         CS::MULT      (1 << NS_TO_CYC_SHIFT)
     --------------- = ----------------------
     (1 << CS:SHIFT)       NS_TO_CYC_MULT

     Ergo: NS_TO_CYC_MULT = (1 << (CS::SHIFT + NS_TO_CYC_SHIFT)) / CS::MULT

     The NS_TO_CYC_SHIFT value is calculated when the clocksource is
     installed so that it aims for a one hour maximum sleep time.

Signed-off-by: Thomas Gleixner <tglx@kernel.org>
Signed-off-by: Peter Zijlstra (Intel) <peterz@infradead.org>
Link: https://patch.msgid.link/20260224163429.944763521@kernel.org
include/linux/clocksource.h
include/linux/timekeeper_internal.h
kernel/time/Kconfig
kernel/time/timekeeping.c
kernel/time/timekeeping.h

index 54366d5c4d190774887b5af98333f4ee2e49e164..25774fc5b53d726c9f5122e341d0ce651cb55d61 100644 (file)
@@ -150,6 +150,7 @@ struct clocksource {
 #define CLOCK_SOURCE_RESELECT                  0x100
 #define CLOCK_SOURCE_VERIFY_PERCPU             0x200
 #define CLOCK_SOURCE_CAN_INLINE_READ           0x400
+#define CLOCK_SOURCE_HAS_COUPLED_CLOCK_EVENT   0x800
 
 /* simplify initialization of mask field */
 #define CLOCKSOURCE_MASK(bits) GENMASK_ULL((bits) - 1, 0)
index b8ae89ea28abfca96f424b6a2250de7ebfe6364e..e36d11e33e0c147839f66df59b018cd88e28172e 100644 (file)
@@ -72,6 +72,10 @@ struct tk_read_base {
  * @id:                                The timekeeper ID
  * @tkr_raw:                   The readout base structure for CLOCK_MONOTONIC_RAW
  * @raw_sec:                   CLOCK_MONOTONIC_RAW  time in seconds
+ * @cs_id:                     The ID of the current clocksource
+ * @cs_ns_to_cyc_mult:         Multiplicator for nanoseconds to cycles conversion
+ * @cs_ns_to_cyc_shift:                Shift value for nanoseconds to cycles conversion
+ * @cs_ns_to_cyc_maxns:                Maximum nanoseconds to cyles conversion range
  * @clock_was_set_seq:         The sequence number of clock was set events
  * @cs_was_changed_seq:                The sequence number of clocksource change events
  * @clock_valid:               Indicator for valid clock
@@ -159,6 +163,10 @@ struct timekeeper {
        u64                     raw_sec;
 
        /* Cachline 3 and 4 (timekeeping internal variables): */
+       enum clocksource_ids    cs_id;
+       u32                     cs_ns_to_cyc_mult;
+       u32                     cs_ns_to_cyc_shift;
+       u64                     cs_ns_to_cyc_maxns;
        unsigned int            clock_was_set_seq;
        u8                      cs_was_changed_seq;
        u8                      clock_valid;
index 07b048ba0ccac2c5ad6bdd72794b225efdb9f629..b51bc5625129e8b0aaec5ab72b7fd50211d0e442 100644 (file)
@@ -47,6 +47,9 @@ config GENERIC_CLOCKEVENTS_BROADCAST_IDLE
 config GENERIC_CLOCKEVENTS_MIN_ADJUST
        bool
 
+config GENERIC_CLOCKEVENTS_COUPLED
+       bool
+
 # Generic update of CMOS clock
 config GENERIC_CMOS_UPDATE
        bool
index 63aa31f02ebc42830d66f75d33f7750ec5eeb32f..b7a0f93011e0b36ecb4f3e72bedcab987696b002 100644 (file)
@@ -391,6 +391,20 @@ static void tk_setup_internals(struct timekeeper *tk, struct clocksource *clock)
        tk->tkr_raw.mult = clock->mult;
        tk->ntp_err_mult = 0;
        tk->skip_second_overflow = 0;
+
+       tk->cs_id = clock->id;
+
+       /* Coupled clockevent data */
+       if (IS_ENABLED(CONFIG_GENERIC_CLOCKEVENTS_COUPLED) &&
+           clock->flags & CLOCK_SOURCE_HAS_COUPLED_CLOCK_EVENT) {
+               /*
+                * Aim for an one hour maximum delta and use KHz to handle
+                * clocksources with a frequency above 4GHz correctly as
+                * the frequency argument of clocks_calc_mult_shift() is u32.
+                */
+               clocks_calc_mult_shift(&tk->cs_ns_to_cyc_mult, &tk->cs_ns_to_cyc_shift,
+                                      NSEC_PER_MSEC, clock->freq_khz, 3600 * 1000);
+       }
 }
 
 /* Timekeeper helper functions. */
@@ -720,6 +734,36 @@ static inline void tk_update_ktime_data(struct timekeeper *tk)
        tk->tkr_raw.base = ns_to_ktime(tk->raw_sec * NSEC_PER_SEC);
 }
 
+static inline void tk_update_ns_to_cyc(struct timekeeper *tks, struct timekeeper *tkc)
+{
+       struct tk_read_base *tkrs = &tks->tkr_mono;
+       struct tk_read_base *tkrc = &tkc->tkr_mono;
+       unsigned int shift;
+
+       if (!IS_ENABLED(CONFIG_GENERIC_CLOCKEVENTS_COUPLED) ||
+           !(tkrs->clock->flags & CLOCK_SOURCE_HAS_COUPLED_CLOCK_EVENT))
+               return;
+
+       if (tkrs->mult == tkrc->mult && tkrs->shift == tkrc->shift)
+               return;
+       /*
+        * The conversion math is simple:
+        *
+        *      CS::MULT       (1 << NS_TO_CYC_SHIFT)
+        *   --------------- = ----------------------
+        *   (1 << CS:SHIFT)       NS_TO_CYC_MULT
+        *
+        * Ergo:
+        *
+        *   NS_TO_CYC_MULT = (1 << (CS::SHIFT + NS_TO_CYC_SHIFT)) / CS::MULT
+        *
+        * NS_TO_CYC_SHIFT has been set up in tk_setup_internals()
+        */
+       shift = tkrs->shift + tks->cs_ns_to_cyc_shift;
+       tks->cs_ns_to_cyc_mult = (u32)div_u64(1ULL << shift, tkrs->mult);
+       tks->cs_ns_to_cyc_maxns = div_u64(tkrs->clock->mask, tks->cs_ns_to_cyc_mult);
+}
+
 /*
  * Restore the shadow timekeeper from the real timekeeper.
  */
@@ -754,6 +798,7 @@ static void timekeeping_update_from_shadow(struct tk_data *tkd, unsigned int act
        tk->tkr_mono.base_real = tk->tkr_mono.base + tk->offs_real;
 
        if (tk->id == TIMEKEEPER_CORE) {
+               tk_update_ns_to_cyc(tk, &tkd->timekeeper);
                update_vsyscall(tk);
                update_pvclock_gtod(tk, action & TK_CLOCK_WAS_SET);
 
@@ -808,6 +853,71 @@ static void timekeeping_forward_now(struct timekeeper *tk)
        tk_update_coarse_nsecs(tk);
 }
 
+/*
+ * ktime_expiry_to_cycles - Convert a expiry time to clocksource cycles
+ * @id:                Clocksource ID which is required for validity
+ * @expires_ns:        Absolute CLOCK_MONOTONIC expiry time (nsecs) to be converted
+ * @cycles:    Pointer to storage for corresponding absolute cycles value
+ *
+ * Convert a CLOCK_MONOTONIC based absolute expiry time to a cycles value
+ * based on the correlated clocksource of the clockevent device by using
+ * the base nanoseconds and cycles values of the last timekeeper update and
+ * converting the delta between @expires_ns and base nanoseconds to cycles.
+ *
+ * This only works for clockevent devices which are using a less than or
+ * equal comparator against the clocksource.
+ *
+ * Utilizing this avoids two clocksource reads for such devices, the
+ * ktime_get() in clockevents_program_event() to calculate the delta expiry
+ * value and the readout in the device::set_next_event() callback to
+ * convert the delta back to a absolute comparator value.
+ *
+ * Returns: True if @id matches the current clocksource ID, false otherwise
+ */
+bool ktime_expiry_to_cycles(enum clocksource_ids id, ktime_t expires_ns, u64 *cycles)
+{
+       struct timekeeper *tk = &tk_core.timekeeper;
+       struct tk_read_base *tkrm = &tk->tkr_mono;
+       ktime_t base_ns, delta_ns, max_ns;
+       u64 base_cycles, delta_cycles;
+       unsigned int seq;
+       u32 mult, shift;
+
+       /*
+        * Racy check to avoid the seqcount overhead when ID does not match. If
+        * the relevant clocksource is installed concurrently, then this will
+        * just delay the switch over to this mechanism until the next event is
+        * programmed. If the ID is not matching the clock events code will use
+        * the regular relative set_next_event() callback as before.
+        */
+       if (data_race(tk->cs_id) != id)
+               return false;
+
+       do {
+               seq = read_seqcount_begin(&tk_core.seq);
+
+               if (tk->cs_id != id)
+                       return false;
+
+               base_cycles = tkrm->cycle_last;
+               base_ns = tkrm->base + (tkrm->xtime_nsec >> tkrm->shift);
+
+               mult = tk->cs_ns_to_cyc_mult;
+               shift = tk->cs_ns_to_cyc_shift;
+               max_ns = tk->cs_ns_to_cyc_maxns;
+
+       } while (read_seqcount_retry(&tk_core.seq, seq));
+
+       /* Prevent negative deltas and multiplication overflows */
+       delta_ns = min(expires_ns - base_ns, max_ns);
+       delta_ns = max(delta_ns, 0);
+
+       /* Convert to cycles */
+       delta_cycles = ((u64)delta_ns * mult) >> shift;
+       *cycles = base_cycles + delta_cycles;
+       return true;
+}
+
 /**
  * ktime_get_real_ts64 - Returns the time of day in a timespec64.
  * @ts:                pointer to the timespec to be set
index 543beba096c757438415641249040f8090219674..198d0608db742c5da87ffafb92fa84089d0b6e3a 100644 (file)
@@ -9,6 +9,8 @@ extern ktime_t ktime_get_update_offsets_now(unsigned int *cwsseq,
                                            ktime_t *offs_boot,
                                            ktime_t *offs_tai);
 
+bool ktime_expiry_to_cycles(enum clocksource_ids id, ktime_t expires_ns, u64 *cycles);
+
 extern int timekeeping_valid_for_hres(void);
 extern u64 timekeeping_max_deferment(void);
 extern void timekeeping_warp_clock(void);