[PATCH v4 5/7] timekeeping: Drive time_offset skew via per-tick ntp_error transfer

From: David Woodhouse

Date: Mon May 25 2026 - 10:04:54 EST


From: David Woodhouse <dwmw@xxxxxxxxxxxx>

Instead of inflating tick_length to effect the time_offset slew,
transfer the skew to ntp_error per-tick and drain time_offset at the
equivalent per-tick rate:

- ntp_error += skew_delta << shift (biases dithering to deliver skew)
- time_offset -= skew_delta / NTP_INTERVAL_FREQ (per-tick drain)

Compute mult from (ntp_tick + skew_delta) so the dithering has enough
bandwidth to deliver the skew rate by selecting between mult and mult+1.
This is equivalent to the old tick_length += delta approach but without
modifying tick_length, and with exact per-tick accounting of the
time_offset drain.

To eliminate remainder error in the per-tick division, skew_delta is
rounded to a multiple of NTP_INTERVAL_FREQ in second_overflow().

second_overflow() computes skew_delta (the exponential decay rate)
but no longer drains time_offset or inflates tick_length directly.

Signed-off-by: David Woodhouse <dwmw@xxxxxxxxxxxx>
Assisted-by: Kiro:claude-opus-4.6-1m
---
include/linux/timekeeper_internal.h | 1 +
kernel/time/ntp.c | 35 +++++++++++++++++++++++++++--
kernel/time/ntp_internal.h | 2 ++
kernel/time/timekeeping.c | 29 +++++++++++++++++++-----
4 files changed, 60 insertions(+), 7 deletions(-)

diff --git a/include/linux/timekeeper_internal.h b/include/linux/timekeeper_internal.h
index da6cf383bedc..9de6b5b94dc0 100644
--- a/include/linux/timekeeper_internal.h
+++ b/include/linux/timekeeper_internal.h
@@ -184,6 +184,7 @@ struct timekeeper {
u32 ntp_error_shift;
u32 ntp_err_mult;
u32 skip_second_overflow;
+ s64 skew_delta;
s32 tai_offset;
};

diff --git a/kernel/time/ntp.c b/kernel/time/ntp.c
index 97fa99b96dd0..3c1e287eb384 100644
--- a/kernel/time/ntp.c
+++ b/kernel/time/ntp.c
@@ -63,6 +63,7 @@ struct ntp_data {
int time_state;
int time_status;
s64 time_offset;
+ s64 skew_delta;
long time_constant;
long time_maxerror;
long time_esterror;
@@ -364,6 +365,31 @@ u64 ntp_tick_length(unsigned int tkid)
return tk_ntp_data[tkid].tick_length;
}

+s64 ntp_get_skew_delta(unsigned int tkid)
+{
+ return tk_ntp_data[tkid].skew_delta;
+}
+
+s64 ntp_drain_time_offset(unsigned int tkid, s64 amount)
+{
+ struct ntp_data *ntpdata = &tk_ntp_data[tkid];
+
+ /* Only drain if amount and time_offset have the same sign */
+ if (!amount || (amount > 0) != (ntpdata->time_offset > 0))
+ return amount;
+
+ /* Clamp: don't overshoot zero */
+ if (abs(amount) > abs(ntpdata->time_offset)) {
+ s64 undrained = amount - ntpdata->time_offset;
+
+ ntpdata->time_offset = 0;
+ return undrained;
+ }
+
+ ntpdata->time_offset -= amount;
+ return 0;
+}
+
/**
* ntp_get_next_leap - Returns the next leapsecond in CLOCK_REALTIME ktime_t
* @tkid: Timekeeper ID
@@ -460,9 +486,14 @@ int second_overflow(unsigned int tkid, time64_t secs)
/* Compute the phase adjustment for the next second */
ntpdata->tick_length = ntpdata->tick_length_base;

+ /*
+ * Set the per-tick skew rate for the tick code. This is in the
+ * same units as tick_length (ns << NTP_SCALE_SHIFT), and is
+ * rounded to a multiple of NTP_INTERVAL_FREQ so that the per-tick
+ * division in the tick code is exact.
+ */
delta = ntp_offset_chunk(ntpdata, ntpdata->time_offset);
- ntpdata->time_offset -= delta;
- ntpdata->tick_length += delta;
+ ntpdata->skew_delta = div_s64(delta, NTP_INTERVAL_FREQ) * NTP_INTERVAL_FREQ;

/* Check PPS signal */
pps_dec_valid(ntpdata);
diff --git a/kernel/time/ntp_internal.h b/kernel/time/ntp_internal.h
index 7084d839c207..05e5dd5e1b70 100644
--- a/kernel/time/ntp_internal.h
+++ b/kernel/time/ntp_internal.h
@@ -6,6 +6,8 @@ extern void ntp_init(void);
extern void ntp_clear(unsigned int tkid);
/* Returns how long ticks are at present, in ns / 2^NTP_SCALE_SHIFT. */
extern u64 ntp_tick_length(unsigned int tkid);
+extern s64 ntp_get_skew_delta(unsigned int tkid);
+extern s64 ntp_drain_time_offset(unsigned int tkid, s64 amount);
extern ktime_t ntp_get_next_leap(unsigned int tkid);
extern int second_overflow(unsigned int tkid, time64_t secs);
extern int ntp_adjtimex(unsigned int tkid, struct __kernel_timex *txc, const struct timespec64 *ts,
diff --git a/kernel/time/timekeeping.c b/kernel/time/timekeeping.c
index 75397f5caace..780f66b12916 100644
--- a/kernel/time/timekeeping.c
+++ b/kernel/time/timekeeping.c
@@ -2395,20 +2395,23 @@ static __always_inline void timekeeping_apply_adjustment(struct timekeeper *tk,
static void timekeeping_adjust(struct timekeeper *tk, s64 offset)
{
u64 ntp_tl = ntp_tick_length(tk->id);
+ s64 skew = ntp_get_skew_delta(tk->id);
u32 mult;

/*
- * Determine the multiplier from the current NTP tick length.
- * Avoid expensive division when the tick length doesn't change.
+ * Determine the multiplier from the current NTP tick length plus
+ * skew_delta. The skew biases mult so that ±1 dithering can deliver
+ * the time_offset slew rate. Recompute when either changes.
*/
- if (likely(tk->ntp_tick == ntp_tl)) {
+ if (likely(tk->ntp_tick == ntp_tl && tk->skew_delta == skew)) {
mult = tk->tkr_mono.mult - tk->ntp_err_mult;
} else {
if (unlikely(!tk->cycle_interval))
return;
tk->ntp_tick = ntp_tl;
- mult = div64_u64(tk->ntp_tick >> tk->ntp_error_shift,
- tk->cycle_interval);
+ tk->skew_delta = skew;
+ mult = div64_u64((tk->ntp_tick + skew) >> tk->ntp_error_shift,
+ tk->cycle_interval);
}

/*
@@ -2535,6 +2538,22 @@ static u64 logarithmic_accumulation(struct timekeeper *tk, u64 offset,
tk->ntp_error += tk->ntp_tick << shift;
tk->ntp_error -= tk->xtime_interval << (tk->ntp_error_shift + shift);

+ /*
+ * During clock skew driven by ntpdata->time_offset, transfer a
+ * *portion* of the requested total delta into ntp_error from
+ * time_offset each tick. The second_overflow() function sets
+ * the rate of skew, and the value of 'mult' has been selected
+ * in order to allow the dithering to keep ntp_error around zero
+ * even while this adjustment is being applied.
+ */
+ if (tk->skew_delta) {
+ s64 drain = div_s64(tk->skew_delta << shift,
+ NTP_INTERVAL_FREQ);
+
+ tk->ntp_error += (tk->skew_delta << shift) -
+ ntp_drain_time_offset(tk->id, drain);
+ }
+
return offset;
}

--
2.54.0