From nobody Thu Jun 11 05:15:07 2026 Received: from casper.infradead.org (casper.infradead.org [90.155.50.34]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id CBCC429C328 for ; Wed, 10 Jun 2026 23:40:36 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=90.155.50.34 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781134839; cv=none; b=u+9z9pQ/XCN6UaTbrY8SMIvuvIt1KbOieVpl87Ik8vtmA2Cb11EZyQOVeHYFdZbZjvKZUv6Thy6qlnQYwhgfHCJl0VIUCgfrMAa87jyGS+jMlznO7371lxhkKgj6xMpQRvEo1h3LfJxAQxWrmf6+b0LEmjCWTdORu3kIHL5tAfw= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781134839; c=relaxed/simple; bh=Ymshpxts35jtznKhArfX/R613HuA86pKX1gp5ZhctLU=; h=From:To:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=tM4NpKZhvIH7jdyLOJSHi4jBWhC3TGdi/s5Q379WLTcJGBAze3kxvhtLzjXnXiyKdQJWb1f8XhZxez+IBvtZfzfEWqbDUA37I123eNxrrs17dIHnleom2dP+rpA1ojW1ljk96j+9OMnMHCFGdSbJnVHqsNWgWyG/AwE+Cp3mEQc= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org; spf=none smtp.mailfrom=casper.srs.infradead.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b=gCgLpD4p; arc=none smtp.client-ip=90.155.50.34 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org Authentication-Results: smtp.subspace.kernel.org; spf=none smtp.mailfrom=casper.srs.infradead.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b="gCgLpD4p" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=infradead.org; s=casper.20170209; h=Sender:Content-Transfer-Encoding: MIME-Version:References:In-Reply-To:Message-ID:Date:Subject:To:From:Reply-To: Cc:Content-Type:Content-ID:Content-Description; bh=ls9Wyv1Fa/gNVOsC4hng3TBpLmPYrLEVnG7kPOtVWWo=; b=gCgLpD4paLP8VzW0QDPIz1oPrM +Ow+O/y3FvFNDhhT0xtIlBzVOBvUTd+PfCon/ZrIRRwbE0mZ6EzeSQjaswyK74OtqvYH4OgG79O8T RMt9PrNSLMdTbhrcq3GP3d54fs8AWDvuxhEFyinn4mDpjq5ssirLOyaDVKuJJ/Cwar8c8azU/yHS1 Qm+nGWqi03x9EpGyCYZNf8jdFA1NPHOaFP0tHW9tbqLDFizvNeEZFalpr9nFqnjWvqAedr0xKee5z NSOx9bTszjB/d1IyrUPzOus6Gz25O6QJUH7Dkfy8Q5BFq2rgj/cYP5MHBIGLxCubFJu2DFii+txru yDj6AQIA==; Received: from [2001:8b0:10b:1::425] (helo=i7.infradead.org) by casper.infradead.org with esmtpsa (Exim 4.99.1 #2 (Red Hat Linux)) id 1wXSWy-00000000pnr-3RhR; Wed, 10 Jun 2026 23:40:21 +0000 Received: from dwoodhou by i7.infradead.org with local (Exim 4.99.2 #2 (Red Hat Linux)) id 1wXSWy-00000000aOy-46jp; Thu, 11 Jun 2026 00:40:21 +0100 From: David Woodhouse To: Richard Cochran , Wen Gu , David Woodhouse , Andrew Lunn , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , John Stultz , Thomas Gleixner , Stephen Boyd , Anna-Maria Behnsen , Frederic Weisbecker , Shuah Khan , Peter Zijlstra , =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= , Arnd Bergmann , Miroslav Lichvar , Julien Ridoux , Ryan Luu , linux-kernel@vger.kernel.org Subject: [PATCH v5 1/6] MAINTAINERS: Add Miroslav as timekeeping reviewer Date: Wed, 10 Jun 2026 23:30:07 +0100 Message-ID: <20260610234020.137999-2-dwmw2@infradead.org> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260610234020.137999-1-dwmw2@infradead.org> References: <20260610234020.137999-1-dwmw2@infradead.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Sender: David Woodhouse X-SRS-Rewrite: SMTP reverse-path rewritten from by casper.infradead.org. See http://www.infradead.org/rpr.html Content-Type: text/plain; charset="utf-8" From: David Woodhouse If Thomas is going to nudge me on IRC to add Miroslav to Cc on timekeeping patches, then he might as well actually be listed in the MAINTAINERS file. Signed-off-by: David Woodhouse Acked-by: John Stultz --- MAINTAINERS | 1 + 1 file changed, 1 insertion(+) diff --git a/MAINTAINERS b/MAINTAINERS index 882214b0e7db..de39cc3e7e68 100644 --- a/MAINTAINERS +++ b/MAINTAINERS @@ -26877,6 +26877,7 @@ TIMEKEEPING, CLOCKSOURCE CORE, NTP, ALARMTIMER M: John Stultz M: Thomas Gleixner R: Stephen Boyd +R: Miroslav Lichvar L: linux-kernel@vger.kernel.org S: Supported T: git git://git.kernel.org/pub/scm/linux/kernel/git/tip/tip.git timers/co= re --=20 2.54.0 From nobody Thu Jun 11 05:15:07 2026 Received: from desiato.infradead.org (desiato.infradead.org [90.155.92.199]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3F1CE3750D5 for ; Wed, 10 Jun 2026 23:40:39 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=90.155.92.199 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781134842; cv=none; b=PNkPz4UhN0TVXMDosetpxdj7KizgmMsfxXABO2LSkgqyoKlcxSHeTZ0bLS5wam38u/fqQAqLwvEozstj3hjJ2HlXQSUfFsW5o92xjD3QcigqoPUmcXZSGwpqg+RV+hJZi2S99wIcEIvB3VYU1iCtkWZ18sODAxAP4pAoMnugMAg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781134842; c=relaxed/simple; bh=1CfCTXexf2k7xMD3muUF/71pwldoWJanL/uuy0tYMDY=; h=From:To:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=B2fFuLUcXo+cyyfsdumzi3az9fo27sQsUyoU9wEmqP9/PLSHOny1gEqmsZFojG2htU/Rd6gwQ75tZFrcZHEG+B2R29d6YzAkvgivv3GN7FkeGRxPsEpxWXO0Eu/7qrPS/noWL7kFzfxX5ZjvVMEhelyVKSmfAlMqTOkPWeCGMd0= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org; spf=none smtp.mailfrom=desiato.srs.infradead.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b=VGNqT5MW; arc=none smtp.client-ip=90.155.92.199 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org Authentication-Results: smtp.subspace.kernel.org; spf=none smtp.mailfrom=desiato.srs.infradead.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b="VGNqT5MW" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=infradead.org; s=desiato.20200630; h=Sender:Content-Transfer-Encoding: Content-Type:MIME-Version:References:In-Reply-To:Message-ID:Date:Subject:To: From:Reply-To:Cc:Content-ID:Content-Description; bh=vszRhYGWDTuXfCwd/RZJArbSolbcFq+kA1LYuxWTosQ=; b=VGNqT5MWqwqrQC/e4DD+GGuRle S9kHpGlFTF0ENhG1+1BAqVulOml/1ptbf94CUpts2IM0RaEMFgR3KzBLTfDRgn4uDtucdJ6HrhF40 lM2j0z+sfQXcEtrZWXrnMUz9X0DScYZPtgtGxywGtbym53vi0DWaUaI15/FbvxbtaqOaTAipFMpej GlAM2ZbijVedm6hDEuj/cBRvFic8nD0Uuxwwqj5MNtP4IBMhRmKHFlNXdEtqFLsXyRtcNvNfLRoiz iv0suZo4acH9lbzu3PuHPG7HlGGG7Kfv1oSDDMSQyDxYwLwnN4Z5NmAGZ3PSflBSqWNwE78VqwHNM 91b4sBog==; Received: from [2001:8b0:10b:1::425] (helo=i7.infradead.org) by desiato.infradead.org with esmtpsa (Exim 4.99.2 #2 (Red Hat Linux)) id 1wXSX1-00000004Sa8-2aYZ; Wed, 10 Jun 2026 23:40:24 +0000 Received: from dwoodhou by i7.infradead.org with local (Exim 4.99.2 #2 (Red Hat Linux)) id 1wXSWz-00000000aP1-09aw; Thu, 11 Jun 2026 00:40:21 +0100 From: David Woodhouse To: Richard Cochran , Wen Gu , David Woodhouse , Andrew Lunn , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , John Stultz , Thomas Gleixner , Stephen Boyd , Anna-Maria Behnsen , Frederic Weisbecker , Shuah Khan , Peter Zijlstra , =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= , Arnd Bergmann , Miroslav Lichvar , Julien Ridoux , Ryan Luu , linux-kernel@vger.kernel.org Subject: [PATCH v5 2/6] timekeeping: Account for monotonicity adjustment in ntp_error Date: Wed, 10 Jun 2026 23:30:08 +0100 Message-ID: <20260610234020.137999-3-dwmw2@infradead.org> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260610234020.137999-1-dwmw2@infradead.org> References: <20260610234020.137999-1-dwmw2@infradead.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Sender: David Woodhouse X-SRS-Rewrite: SMTP reverse-path rewritten from by desiato.infradead.org. See http://www.infradead.org/rpr.html From: David Woodhouse timekeeping_apply_adjustment() modifies xtime_nsec to maintain vDSO monotonicity when mult changes: xtime_nsec -=3D offset This ensures that the time reported to userspace does not jump when the multiplier is adjusted from one tick to the next. However, the ntp_error accumulator which tracks the difference between intended and actual clock position was not being updated updated to reflect this additional discrepancy. An earlier attempt at this compensation existed as: ntp_error -=3D (interval - offset) << ntp_error_shift but was removed in commit c2cda2a5bda9 ("timekeeping/ntp: Don't align NTP frequency adjustments to ticks") because it was a major source of NTP error. That's because (interval - offset) was wrong: the subtraction of "interval" prematurely accounted for the changed xtime_interval of the next tick, which would be correctly accounted in the next accumulation anyway =E2=80=94 a double subtraction. What is actually needed is just the "offset" part: ntp_error must be told that xtime_nsec moved by "offset" without a corresponding change in the intended position. For the normal =C2=B11 mult dithering this is negligible (the adjustments cancel over time), but for larger mult changes =E2=80=94 such as when an external reference clock sets a new frequency =E2=80=94 the one-time uncompensated offset is significant. Fix by adjusting ntp_error by the correct amount: ntp_error +=3D offset << ntp_error_shift This keeps ntp_error consistent with the actual xtime_nsec position after the adjustment. Fixes: c2cda2a5bda9 ("timekeeping/ntp: Don't align NTP frequency adjustment= s to ticks") Signed-off-by: David Woodhouse Assisted-by: Kiro:claude-opus-4.6-1m Acked-by: John Stultz --- kernel/time/timekeeping.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kernel/time/timekeeping.c b/kernel/time/timekeeping.c index 0d5b67f609bb..d847bba0481b 100644 --- a/kernel/time/timekeeping.c +++ b/kernel/time/timekeeping.c @@ -2389,6 +2389,11 @@ static __always_inline void timekeeping_apply_adjust= ment(struct timekeeper *tk, * xtime_nsec_2 =3D xtime_nsec_1 - offset * Which simplifies to: * xtime_nsec -=3D offset + * + * When subtracting offset from xtime_nsec, the same amount + * (in appropriate units) has to be added to ntp_error, in + * order to correctly track the delta between the time + * reported in xtime_nsec, and the intended time. */ if ((mult_adj > 0) && (tk->tkr_mono.mult + mult_adj < mult_adj)) { /* NTP adjustment caused clocksource mult overflow */ @@ -2399,6 +2404,7 @@ static __always_inline void timekeeping_apply_adjustm= ent(struct timekeeper *tk, tk->tkr_mono.mult +=3D mult_adj; tk->xtime_interval +=3D interval; tk->tkr_mono.xtime_nsec -=3D offset; + tk->ntp_error +=3D offset << tk->ntp_error_shift; } =20 /* --=20 2.54.0 From nobody Thu Jun 11 05:15:07 2026 Received: from casper.infradead.org (casper.infradead.org [90.155.50.34]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 0337335DA65 for ; Wed, 10 Jun 2026 23:40:37 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=90.155.50.34 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781134840; cv=none; b=Nlp36mGKXScrFZBWhMK6ObeHQd5wgMattViElXK2FPZUG90R3dqGQ11rtzqK1v7NxHfRyX434X7iqiCfQhSyNZMjChhVUhF0Gvvm76F66RycFT6TFxetyWzhBtWyKLa+XvO/8znNoCdFOfm5ivKchzuUMEAfssJn2LAOJ9OEznI= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781134840; c=relaxed/simple; bh=EH6x/Nm61mghTS1Np/hMU04/wO976rtOYknS2VE/ObI=; h=From:To:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=aakwPs+5Wg/ZBWCNfJz8+XRptNIllSGODH9aEu0z02NxexKsOoJbrBPM1HyXktNyplO4DTOD6CdujKJvMi9xFg5gPAoHMf/do2vdzUSwnqrqHVWSH69ocN+X0oBMBlYRxY5sBewXCAnirDu/sTVjUYUL8bM6A6gHiqNf4FOB9s4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org; spf=none smtp.mailfrom=casper.srs.infradead.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b=I2NBysF1; arc=none smtp.client-ip=90.155.50.34 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org Authentication-Results: smtp.subspace.kernel.org; spf=none smtp.mailfrom=casper.srs.infradead.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b="I2NBysF1" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=infradead.org; s=casper.20170209; h=Sender:Content-Transfer-Encoding: Content-Type:MIME-Version:References:In-Reply-To:Message-ID:Date:Subject:To: From:Reply-To:Cc:Content-ID:Content-Description; bh=9zlzMxDVS0f8LQHzIorbQ1BMCRBbxSHLTbAnE4v4wZA=; b=I2NBysF1l1IqOGR2LhOgCCoXky E7eA9tVGbMZzNPSsRAVMqou4sftCUz5HWmlqEGMx4CyoOtTcq/ZDfIvajA68vwoeRhspxF/oZmQcJ E0U3Rio/C1JUWDxOz5XjlU2VHVtOQZCRN53rm2m05Wmq81VNLYf4c61pUsCkXrQfIznqa5UZI76vH LSspmMqAaIulCvqFNryLWIch5Mk7TwMSAXUxr4+080oUrdFWqw2Jan6ZBGjcwpjAOedWK+DuU5sTQ Sh/polNmXwgmT6IBbykoPDdFUzNrxFKJYqt+6AY/whfNT04bzRdxiZLhcOdtorWOc61DfVNEqEJdO chIN/XCg==; Received: from [2001:8b0:10b:1::425] (helo=i7.infradead.org) by casper.infradead.org with esmtpsa (Exim 4.99.1 #2 (Red Hat Linux)) id 1wXSWy-00000000pnt-3iyC; Wed, 10 Jun 2026 23:40:21 +0000 Received: from dwoodhou by i7.infradead.org with local (Exim 4.99.2 #2 (Red Hat Linux)) id 1wXSWz-00000000aP6-0OsT; Thu, 11 Jun 2026 00:40:21 +0100 From: David Woodhouse To: Richard Cochran , Wen Gu , David Woodhouse , Andrew Lunn , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , John Stultz , Thomas Gleixner , Stephen Boyd , Anna-Maria Behnsen , Frederic Weisbecker , Shuah Khan , Peter Zijlstra , =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= , Arnd Bergmann , Miroslav Lichvar , Julien Ridoux , Ryan Luu , linux-kernel@vger.kernel.org Subject: [PATCH v5 3/6] timekeeping: Account for clocksource tick quantisation via NTP Date: Wed, 10 Jun 2026 23:30:09 +0100 Message-ID: <20260610234020.137999-4-dwmw2@infradead.org> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260610234020.137999-1-dwmw2@infradead.org> References: <20260610234020.137999-1-dwmw2@infradead.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Sender: David Woodhouse X-SRS-Rewrite: SMTP reverse-path rewritten from by casper.infradead.org. See http://www.infradead.org/rpr.html From: David Woodhouse cycle_interval is an integer number of counter cycles per NTP interval, so the real time it represents differs from the nominal NTP_INTERVAL_LENGTH by up to half a counter period. For coarse clocksources this is significant: the 3.579545 MHz ACPI PM timer at HZ=3D1000 rounds 3579.545 cycles up to 3580, making each tick 1.000127 ms (+127 PPM). Commit a386b5af8edd ("time: Compensate for rounding on odd-frequency clocksources") introduced xtime_remainder to compensate for exactly this, citing the same 127 PPM ACPI PM example. The compensation is correct and necessary, but it was applied inside the timekeeping accumulation in timekeeping.c: subtracted in the mult computation in timekeeping_adjust() and folded into the ntp_error update in logarithmic_accumulation(). That keeps the base rate correct and leaves NTP its full symmetric +/-MAXFREQ range rather than +373/-627 PPM, but the NTP code in ntp.c never sees it: tick_length is computed without the correction, so ntp.c's notion of how long a tick is disagrees with the rate timekeeping actually produces. Make the offset an explicit part of the NTP tick_length instead. Add ntp_data::cs_tick_adj, a fixed per-second addend that ntp_update_frequency() includes alongside ntp_tick_adj and time_freq. tk_setup_internals() computes it from the difference between the real cycle_interval duration and the nominal interval, stores it in the timekeeper, and hands it to NTP through a new argument to ntp_clear() -- which already recomputes the frequency and is invoked after every clocksource (re)configuration. timekeeping_init() now uses TK_UPDATE_ALL for this; clearing NTP there is otherwise redundant since ntp_init() has just initialised it. ntp.c now computes the true tick rate, giving a single source of truth. Like ntp_tick_adj, cs_tick_adj stays internal to the kernel: userspace still sees the nominal 1.000000 ms tick via adjtimex and is unaware of the addends. timekeeping_adjust() and logarithmic_accumulation() use ntp_tick / xtime_interval directly, and xtime_remainder is removed. The base-rate arithmetic is unchanged: ntp_tick becomes xtime_interval << ntp_error_shift, so the mult division yields the same base mult and the ntp_error accumulation still nets to zero per tick. Beyond the cleanup of treating all the tick_length contributions (nominal interval, ntp_tick_adj, cs_tick_adj, time_freq) consistently as addends in one place, this prepares for a future timekeeping_set_reference() which will set tick_length to track an absolute external reference such as a vmclock: that path needs ntp.c to own a tick_length that already reflects the clocksource quantisation, with no hidden correction applied elsewhere. Signed-off-by: David Woodhouse Assisted-by: Kiro:claude-opus-4.8 --- include/linux/timekeeper_internal.h | 8 +++++--- kernel/time/ntp.c | 27 ++++++++++++++++++++++--- kernel/time/ntp_internal.h | 2 +- kernel/time/timekeeping.c | 31 +++++++++++++++++++++-------- 4 files changed, 53 insertions(+), 15 deletions(-) diff --git a/include/linux/timekeeper_internal.h b/include/linux/timekeeper= _internal.h index e36d11e33e0c..ec81587a1400 100644 --- a/include/linux/timekeeper_internal.h +++ b/include/linux/timekeeper_internal.h @@ -84,8 +84,6 @@ struct tk_read_base { * @cycle_interval: Number of clock cycles in one NTP interval * @xtime_interval: Number of clock shifted nano seconds in one NTP * interval. - * @xtime_remainder: Shifted nano seconds left over when rounding - * @cycle_interval * @raw_interval: Shifted raw nano seconds accumulated per NTP interval. * @next_leap_ktime: CLOCK_MONOTONIC time value of a pending leap-second * @ntp_tick: The ntp_tick_length() value currently being @@ -99,6 +97,10 @@ struct tk_read_base { * @ntp_error_shift: Shift conversion between clock shifted nano seconds = and * ntp shifted nano seconds. * @ntp_err_mult: Multiplication factor for scaled math conversion + * @cs_tick_adj: Per-second adjustment handed to NTP via ntp_clear() + * accounting for the difference between the nominal + * NTP interval and the real time taken by the + * clocksource's integer @cycle_interval (upscaled). * @skip_second_overflow: Flag used to avoid updating NTP twice with same = second * @tai_offset: The current UTC to TAI offset in seconds * @@ -178,7 +180,6 @@ struct timekeeper { =20 u64 cycle_interval; u64 xtime_interval; - s64 xtime_remainder; u64 raw_interval; =20 ktime_t next_leap_ktime; @@ -186,6 +187,7 @@ struct timekeeper { s64 ntp_error; u32 ntp_error_shift; u32 ntp_err_mult; + s64 cs_tick_adj; u32 skip_second_overflow; s32 tai_offset; }; diff --git a/kernel/time/ntp.c b/kernel/time/ntp.c index 97fa99b96dd0..3fad82c47c4c 100644 --- a/kernel/time/ntp.c +++ b/kernel/time/ntp.c @@ -39,6 +39,10 @@ * @time_reftime: Time at last adjustment in seconds * @time_adjust: Adjustment value * @ntp_tick_adj: Constant boot-param configurable NTP tick adjustment (up= scaled) + * @cs_tick_adj: Fixed per-second adjustment compensating for the differen= ce + * between the nominal NTP interval and the real time taken + * by the clocksource's integer @cycle_interval (upscaled). + * Set by the timekeeping core via ntp_clear(). * @ntp_next_leap_sec: Second value of the next pending leapsecond, or TIM= E64_MAX if no leap * * @pps_valid: PPS signal watchdog counter @@ -70,6 +74,7 @@ struct ntp_data { time64_t time_reftime; long time_adjust; s64 ntp_tick_adj; + s64 cs_tick_adj; time64_t ntp_next_leap_sec; #ifdef CONFIG_NTP_PPS int pps_valid; @@ -255,6 +260,7 @@ static void ntp_update_frequency(struct ntp_data *ntpda= ta) second_length =3D (u64)(tick_usec * NSEC_PER_USEC * USER_HZ) << NTP_SCA= LE_SHIFT; =20 second_length +=3D ntpdata->ntp_tick_adj; + second_length +=3D ntpdata->cs_tick_adj; second_length +=3D ntpdata->time_freq; =20 new_base =3D div_u64(second_length, NTP_INTERVAL_FREQ); @@ -350,11 +356,26 @@ static void __ntp_clear(struct ntp_data *ntpdata) } =20 /** - * ntp_clear - Clears the NTP state variables - * @tkid: Timekeeper ID to be able to select proper ntp data array member + * ntp_clear - Clear NTP state and set the clocksource quantisation adjust= ment + * @tkid: Timekeeper ID + * @cs_tick_adj: Per-second adjustment in ns << NTP_SCALE_SHIFT + * + * The timekeeping core uses an integer number of cycles (@cycle_interval) + * per NTP interval, so the real time that interval represents differs from + * the nominal NTP_INTERVAL_LENGTH by up to half a counter period. Folding + * this fixed offset into @cs_tick_adj makes it an explicit part of the NTP + * tick_length computation in ntp.c, instead of being applied during + * timekeeping accumulation where the NTP code never saw it. Like + * @ntp_tick_adj it stays internal to the kernel; userspace still sees the + * nominal tick via adjtimex. NTP retains its full symmetric =C2=B1MAXFREQ= range + * around the corrected base rate. + * + * Called whenever the clocksource is (re)configured, which is also when t= he + * rest of the NTP state must be cleared, so the two are done together. */ -void ntp_clear(unsigned int tkid) +void ntp_clear(unsigned int tkid, s64 cs_tick_adj) { + tk_ntp_data[tkid].cs_tick_adj =3D cs_tick_adj; __ntp_clear(&tk_ntp_data[tkid]); } =20 diff --git a/kernel/time/ntp_internal.h b/kernel/time/ntp_internal.h index 7084d839c207..598e5dd2fc5b 100644 --- a/kernel/time/ntp_internal.h +++ b/kernel/time/ntp_internal.h @@ -3,7 +3,7 @@ #define _LINUX_NTP_INTERNAL_H =20 extern void ntp_init(void); -extern void ntp_clear(unsigned int tkid); +extern void ntp_clear(unsigned int tkid, s64 cs_tick_adj); /* Returns how long ticks are at present, in ns / 2^NTP_SCALE_SHIFT. */ extern u64 ntp_tick_length(unsigned int tkid); extern ktime_t ntp_get_next_leap(unsigned int tkid); diff --git a/kernel/time/timekeeping.c b/kernel/time/timekeeping.c index d847bba0481b..bdafd599413d 100644 --- a/kernel/time/timekeeping.c +++ b/kernel/time/timekeeping.c @@ -366,7 +366,6 @@ static void tk_setup_internals(struct timekeeper *tk, s= truct clocksource *clock) =20 /* Go back from cycles -> shifted ns */ tk->xtime_interval =3D interval * clock->mult; - tk->xtime_remainder =3D ntpinterval - tk->xtime_interval; tk->raw_interval =3D interval * clock->mult; =20 /* if changing clocks, convert xtime_nsec shift units */ @@ -386,7 +385,19 @@ static void tk_setup_internals(struct timekeeper *tk, = struct clocksource *clock) =20 tk->ntp_error =3D 0; tk->ntp_error_shift =3D NTP_SCALE_SHIFT - clock->shift; - tk->ntp_tick =3D ntpinterval << tk->ntp_error_shift; + + /* + * cycle_interval is a whole number of counter cycles, so the real + * time it represents differs from the nominal NTP_INTERVAL_LENGTH by + * up to half a counter period (e.g. +127 PPM on the 3.579545 MHz ACPI + * PM timer at HZ=3D1000). Record this fixed per-tick offset, scaled up + * to a per-second value to match the ntp_update_frequency() addends, + * so it can be handed to NTP via ntp_clear() and reflected explicitly + * in tick_length rather than applied behind NTP's back. + */ + tk->cs_tick_adj =3D (((s64)tk->xtime_interval - (s64)ntpinterval) << + tk->ntp_error_shift) * NTP_INTERVAL_FREQ; + tk->ntp_tick =3D (u64)tk->xtime_interval << tk->ntp_error_shift; =20 /* * The timekeeper keeps its own mult values for the currently @@ -803,7 +814,7 @@ static void timekeeping_update_from_shadow(struct tk_da= ta *tkd, unsigned int act =20 if (action & TK_CLEAR_NTP) { tk->ntp_error =3D 0; - ntp_clear(tk->id); + ntp_clear(tk->id, tk->cs_tick_adj); } =20 tk_update_leap_state(tk); @@ -2075,7 +2086,12 @@ void __init timekeeping_init(void) =20 tk_set_wall_to_mono(tks, wall_to_mono); =20 - timekeeping_update_from_shadow(&tk_core, TK_CLOCK_WAS_SET); + /* + * Use TK_UPDATE_ALL so the NTP layer picks up the clocksource's + * cs_tick_adj via ntp_clear(). Clearing NTP here is otherwise + * redundant as ntp_init() already initialised it above. + */ + timekeeping_update_from_shadow(&tk_core, TK_UPDATE_ALL); } =20 /* time in seconds when suspend began for persistent clock */ @@ -2424,8 +2440,8 @@ static void timekeeping_adjust(struct timekeeper *tk,= s64 offset) mult =3D tk->tkr_mono.mult - tk->ntp_err_mult; } else { tk->ntp_tick =3D ntp_tl; - mult =3D div64_u64((tk->ntp_tick >> tk->ntp_error_shift) - - tk->xtime_remainder, tk->cycle_interval); + mult =3D div64_u64(tk->ntp_tick >> tk->ntp_error_shift, + tk->cycle_interval); } =20 /* @@ -2550,8 +2566,7 @@ static u64 logarithmic_accumulation(struct timekeeper= *tk, u64 offset, =20 /* Accumulate error between NTP and clock interval */ tk->ntp_error +=3D tk->ntp_tick << shift; - tk->ntp_error -=3D (tk->xtime_interval + tk->xtime_remainder) << - (tk->ntp_error_shift + shift); + tk->ntp_error -=3D tk->xtime_interval << (tk->ntp_error_shift + shift); =20 return offset; } --=20 2.54.0 From nobody Thu Jun 11 05:15:07 2026 Received: from desiato.infradead.org (desiato.infradead.org [90.155.92.199]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 3D9E936F8FB for ; Wed, 10 Jun 2026 23:40:39 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=90.155.92.199 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781134841; cv=none; b=WQ54/A4rylBvruOOTqBQPWLwDJ23DSvaJ3efvnYhp3T10d5Uss1UqLXeU9rRBP80V6OlOcu7q3Umih0ij1JTroEk9i9Gi+N7D/FUJxrfcFzmx8BuIxvupKYgepda6UQUdQAp6fw0vHpMxV7ES4IRDVmEEyCQq7h/kZXBcichgd0= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781134841; c=relaxed/simple; bh=GPxul/vfyzIx4rWt9Q0dlQXeP/O3LapiHBOPzJ9Rl5E=; h=From:To:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=ek/BjWz2gd7RA3TrD0vrKpF7kQSCYvS7YWsUFf2EX2V7vg8Os1NhKyAUcZ1vf3vep0rw9s6h7tkFfIZItiYXY4Eu44GyJ4Ouqe0EfhW65ETPp6nObUkfa7h83C3yx9G4R7qxktjAkPTs8X/o2Jw0H8q8HR1g3cuLFcXJ9/Dr/yE= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org; spf=none smtp.mailfrom=desiato.srs.infradead.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b=nMDCvkAZ; arc=none smtp.client-ip=90.155.92.199 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org Authentication-Results: smtp.subspace.kernel.org; spf=none smtp.mailfrom=desiato.srs.infradead.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b="nMDCvkAZ" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=infradead.org; s=desiato.20200630; h=Sender:Content-Transfer-Encoding: Content-Type:MIME-Version:References:In-Reply-To:Message-ID:Date:Subject:To: From:Reply-To:Cc:Content-ID:Content-Description; bh=ud4JpRi/fjBLdfX3A+zU68UaZsBVFq6W0B2eof8zGH4=; b=nMDCvkAZoprb/YWYjrOapWpwkb rCglqnE9BVfe2dlLGQaMuvZ0z6bOS8VkjHFyEHYaZcXSesRg+xyLOH+lBrtGBxXDi68X3Llj/IPOw RwNiblyp0YYsvaiP4mrvAE94flH4VrTPNWgbYhHV04qpW+Vdddb781QBL+hPDhNYU6T70dQJXDTEM amdbHX8hYmLDGD3RlikEhBEDVsLPU4XYWCdUm0CoNFCdZaHEtmx2Jb/O7W+7bDPReWX0Vpv7MVkU3 6vX0NYLnFYAUBplxu25tkFoWjNz4ZZYEHprnSRMRnW3I5aSb+qWs6OnXmd/mH+WN5TbrotCcRHG8Y rB/82R1g==; Received: from [2001:8b0:10b:1::425] (helo=i7.infradead.org) by desiato.infradead.org with esmtpsa (Exim 4.99.2 #2 (Red Hat Linux)) id 1wXSX1-00000004Sa9-2aZS; Wed, 10 Jun 2026 23:40:24 +0000 Received: from dwoodhou by i7.infradead.org with local (Exim 4.99.2 #2 (Red Hat Linux)) id 1wXSWz-00000000aPA-0Ydx; Thu, 11 Jun 2026 00:40:21 +0100 From: David Woodhouse To: Richard Cochran , Wen Gu , David Woodhouse , Andrew Lunn , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , John Stultz , Thomas Gleixner , Stephen Boyd , Anna-Maria Behnsen , Frederic Weisbecker , Shuah Khan , Peter Zijlstra , =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= , Arnd Bergmann , Miroslav Lichvar , Julien Ridoux , Ryan Luu , linux-kernel@vger.kernel.org Subject: [PATCH v5 4/6] timekeeping: Drive time_offset skew via per-tick ntp_error transfer Date: Wed, 10 Jun 2026 23:30:10 +0100 Message-ID: <20260610234020.137999-5-dwmw2@infradead.org> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260610234020.137999-1-dwmw2@infradead.org> References: <20260610234020.137999-1-dwmw2@infradead.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Sender: David Woodhouse X-SRS-Rewrite: SMTP reverse-path rewritten from by desiato.infradead.org. See http://www.infradead.org/rpr.html From: David Woodhouse 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 +=3D skew_delta << shift (biases dithering to deliver skew) - time_offset -=3D skew_delta << shift (per-tick drain) skew_delta is in per-tick units (same as time_offset), computed in second_overflow() as ntp_offset_chunk() / NTP_INTERVAL_FREQ. 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 +=3D delta approach but without modifying tick_length, and with exact per-tick accounting of the time_offset drain. Once time_offset is so low that skew_delta would round to zero, impart the minimum =C2=B11 per tick. It won't overshoot because anything that can't be drained from time_offset gets accounted to ntp_error and will drive the selection of mult vs. mult+1 from the very next tick to compensate. 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 Assisted-by: Kiro:claude-opus-4.8 --- include/linux/timekeeper_internal.h | 1 + kernel/time/ntp.c | 48 ++++++++++++++++++++++++++--- kernel/time/ntp_internal.h | 2 ++ kernel/time/timekeeping.c | 45 ++++++++++++++++++++++++--- 4 files changed, 87 insertions(+), 9 deletions(-) diff --git a/include/linux/timekeeper_internal.h b/include/linux/timekeeper= _internal.h index ec81587a1400..fb37a736ec1c 100644 --- a/include/linux/timekeeper_internal.h +++ b/include/linux/timekeeper_internal.h @@ -189,6 +189,7 @@ struct timekeeper { u32 ntp_err_mult; s64 cs_tick_adj; u32 skip_second_overflow; + s64 skew_delta; s32 tai_offset; }; =20 diff --git a/kernel/time/ntp.c b/kernel/time/ntp.c index 3fad82c47c4c..f2670e7985b8 100644 --- a/kernel/time/ntp.c +++ b/kernel/time/ntp.c @@ -67,6 +67,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; @@ -385,6 +386,31 @@ u64 ntp_tick_length(unsigned int tkid) return tk_ntp_data[tkid].tick_length; } =20 +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 =3D &tk_ntp_data[tkid]; + + /* Only drain if amount and time_offset have the same sign */ + if (!amount || (amount > 0) !=3D (ntpdata->time_offset > 0)) + return amount; + + /* Clamp: don't overshoot zero */ + if (abs(amount) > abs(ntpdata->time_offset)) { + s64 undrained =3D amount - ntpdata->time_offset; + + ntpdata->time_offset =3D 0; + return undrained; + } + + ntpdata->time_offset -=3D amount; + return 0; +} + /** * ntp_get_next_leap - Returns the next leapsecond in CLOCK_REALTIME ktime= _t * @tkid: Timekeeper ID @@ -419,7 +445,6 @@ ktime_t ntp_get_next_leap(unsigned int tkid) int second_overflow(unsigned int tkid, time64_t secs) { struct ntp_data *ntpdata =3D &tk_ntp_data[tkid]; - s64 delta; int leap =3D 0; s32 rem; =20 @@ -481,9 +506,24 @@ int second_overflow(unsigned int tkid, time64_t secs) /* Compute the phase adjustment for the next second */ ntpdata->tick_length =3D ntpdata->tick_length_base; =20 - delta =3D ntp_offset_chunk(ntpdata, ntpdata->time_offset); - ntpdata->time_offset -=3D delta; - ntpdata->tick_length +=3D delta; + /* + * Set the per-tick skew rate for the tick code. This is in + * the same units as tick_length (ns << NTP_SCALE_SHIFT). + * tick_offset is so low that the skew imparted would round to + * zero, pass the bare minimum =C2=B11. It won't overshoot because + * logarithmic_accumulation() only drains what it can from + * time_offset and the rest ends up in ntp_error which drives + * the selection of 'mult' immediately each tick. + */ + if (ntpdata->time_offset) { + s64 delta =3D ntp_offset_chunk(ntpdata, ntpdata->time_offset); + ntpdata->skew_delta =3D div_s64(delta, NTP_INTERVAL_FREQ); + + if (!ntpdata->skew_delta) + ntpdata->skew_delta =3D (ntpdata->time_offset > 0) ? 1 : -1; + } else { + ntpdata->skew_delta =3D 0; + } =20 /* Check PPS signal */ pps_dec_valid(ntpdata); diff --git a/kernel/time/ntp_internal.h b/kernel/time/ntp_internal.h index 598e5dd2fc5b..1e708a2562ea 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, s64 cs_tick_adj); /* 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, con= st struct timespec64 *ts, diff --git a/kernel/time/timekeeping.c b/kernel/time/timekeeping.c index bdafd599413d..b8b0e9d7fc10 100644 --- a/kernel/time/timekeeping.c +++ b/kernel/time/timekeeping.c @@ -408,6 +408,7 @@ static void tk_setup_internals(struct timekeeper *tk, s= truct clocksource *clock) tk->tkr_raw.mult =3D clock->mult; tk->ntp_err_mult =3D 0; tk->skip_second_overflow =3D 0; + tk->skew_delta =3D 0; =20 tk->cs_id =3D clock->id; =20 @@ -2430,18 +2431,27 @@ static __always_inline void timekeeping_apply_adjus= tment(struct timekeeper *tk, static void timekeeping_adjust(struct timekeeper *tk, s64 offset) { u64 ntp_tl =3D ntp_tick_length(tk->id); + s64 skew =3D ntp_get_skew_delta(tk->id); u32 mult; =20 /* - * 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 =C2=B11 dithering can deliver + * the time_offset slew rate. Recompute when either changes. */ - if (likely(tk->ntp_tick =3D=3D ntp_tl)) { + if (likely(tk->ntp_tick =3D=3D ntp_tl && tk->skew_delta =3D=3D skew)) { + /* Revert to the base mult rate. */ mult =3D tk->tkr_mono.mult - tk->ntp_err_mult; } else { tk->ntp_tick =3D ntp_tl; - mult =3D div64_u64(tk->ntp_tick >> tk->ntp_error_shift, - tk->cycle_interval); + tk->skew_delta =3D skew; + /* + * skew_delta is stored pre-divided by HZ (matching time_offset); + * scale it back up to the full per-tick rate for the mult bias. + */ + skew *=3D NTP_INTERVAL_FREQ; + mult =3D div64_u64((tk->ntp_tick + skew) >> tk->ntp_error_shift, + tk->cycle_interval); } =20 /* @@ -2568,6 +2578,31 @@ static u64 logarithmic_accumulation(struct timekeepe= r *tk, u64 offset, tk->ntp_error +=3D tk->ntp_tick << shift; tk->ntp_error -=3D tk->xtime_interval << (tk->ntp_error_shift + shift); =20 + /* + * The above accounting of ntp_error includes the part of clock + * skew which was *intentional*, imparted through deliberately + * adjusting 'mult' in timekeeping_adjust() taking skew_delta + * into account. + * + * Drain the intentional skew from time_offset, and readjust + * ntp_error by the amount that *could* actually be drained. + * This ensures that any *overshoot* is correctly left in + * ntp_error and will be correctly compensated for over time. + */ + if (tk->skew_delta) { + /* + * skew_delta is stored pre-divided by HZ, matching time_offset, + * so drain it directly. Fold the amount actually drained back + * into ntp_error in full clock units (=C3=97 NTP_INTERVAL_FREQ); any + * undrainable overshoot is left in ntp_error to be compensated + * by the dithering over subsequent ticks. + */ + s64 drain =3D tk->skew_delta << shift; + s64 unclaimed =3D ntp_drain_time_offset(tk->id, drain); + + tk->ntp_error +=3D (drain - unclaimed) * NTP_INTERVAL_FREQ; + } + return offset; } =20 --=20 2.54.0 From nobody Thu Jun 11 05:15:07 2026 Received: from desiato.infradead.org (desiato.infradead.org [90.155.92.199]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1D02B3603DA for ; Wed, 10 Jun 2026 23:40:38 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=90.155.92.199 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781134841; cv=none; b=pxck3aZfgp49nl9KZHqOXvsRz9EKy5PWAKkoYTlbU+16K96/GqoFd3eeWbEDkgihSgztvd3SUFk33ISVwOlsIFsOkV+91Azx+MT4kaolgKuBAMDKxUYsWWs5ic4tniA7gs4t87AAvr+q8o6BODkWTUaJATjWs6h60zDqFZYwxS8= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781134841; c=relaxed/simple; bh=wQyretqU/3S0K03ueF9umWn8y9s5uOm0IMKZ8+xpkqk=; h=From:To:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version:Content-Type; b=KbdTLwzZDniNwcmWw33L7EN0I/yXnpoqFduiojZZlq7df9z61e4WS01N+pq1yaFBAtbs40pyIb/S7EERtOSVFgsE8Vtqz7c9TTfkBwyZL7U0n4F/p0dnMRAM46jNAOYBDhi8t2Z+apq861frqoh8ZzUatFqV4VkYG8ZPJOgNB+E= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org; spf=none smtp.mailfrom=desiato.srs.infradead.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b=j3paTxvN; arc=none smtp.client-ip=90.155.92.199 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org Authentication-Results: smtp.subspace.kernel.org; spf=none smtp.mailfrom=desiato.srs.infradead.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b="j3paTxvN" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=infradead.org; s=desiato.20200630; h=Sender:Content-Transfer-Encoding: Content-Type:MIME-Version:References:In-Reply-To:Message-ID:Date:Subject:To: From:Reply-To:Cc:Content-ID:Content-Description; bh=538Sjbh9ebknT2yJLWFGpp1AcywRaBImzHBc2wLoG/s=; b=j3paTxvNwsMezfvznTuUslSX2t rZWa6YA//MVccWCGHoYjbhu9XQy+6uKeEq5iOpKxMRgK4koUo66fAStadAVxrHF2N/hvrnpkPI04e hhj30PE1OYshKO9ewf3E3xnCwVV5CrfDWcFlHcSi4d6I7lur/HQvVkeyvLXFAZkX9s4aK+D2nZg6s zLDm5CkwVHseeklEcEkztYncGEjtszaA6z3GdjWYbrsccaVVNnC7QgkKEscSTA31oGEuaUR0BwcYC RaV6w6gjJ+fgSiAf8CZ18aceDR3EYiNucbzTMaD/82+Q60Vp+33/C5tMbB3SUjToRUZGJ1aS7vud6 a66S7jiw==; Received: from [2001:8b0:10b:1::425] (helo=i7.infradead.org) by desiato.infradead.org with esmtpsa (Exim 4.99.2 #2 (Red Hat Linux)) id 1wXSX1-00000004SaA-2aaA; Wed, 10 Jun 2026 23:40:24 +0000 Received: from dwoodhou by i7.infradead.org with local (Exim 4.99.2 #2 (Red Hat Linux)) id 1wXSWz-00000000aPE-0iVH; Thu, 11 Jun 2026 00:40:21 +0100 From: David Woodhouse To: Richard Cochran , Wen Gu , David Woodhouse , Andrew Lunn , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , John Stultz , Thomas Gleixner , Stephen Boyd , Anna-Maria Behnsen , Frederic Weisbecker , Shuah Khan , Peter Zijlstra , =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= , Arnd Bergmann , Miroslav Lichvar , Julien Ridoux , Ryan Luu , linux-kernel@vger.kernel.org Subject: [PATCH v5 5/6] timekeeping: deliver adjtime() time_adjust via skew_delta Date: Wed, 10 Jun 2026 23:30:11 +0100 Message-ID: <20260610234020.137999-6-dwmw2@infradead.org> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260610234020.137999-1-dwmw2@infradead.org> References: <20260610234020.137999-1-dwmw2@infradead.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Sender: David Woodhouse X-SRS-Rewrite: SMTP reverse-path rewritten from by desiato.infradead.org. See http://www.infradead.org/rpr.html From: David Woodhouse The legacy adjtime() slew (ADJ_OFFSET_SINGLESHOT) was the last user of tick_length !=3D tick_length_base: it slewed the clock by inflating tick_length directly, which delivered the correction imprecisely (the per-tick truncation lost ~us over a multi-second slew) and hid the end-of-slew over-delivery in the inflated baseline. Route it through the same per-tick skew_delta mechanism as the time_offset PLL, so tick_length always equals tick_length_base and the slew is both delivered and accounted precisely: - second_overflow() adds the MAX_TICKADJ-per-second linear rate to skew_delta (biasing mult), without inflating tick_length or decrementing time_adjust itself; - the per-tick drain in logarithmic_accumulation() apportions the delivered skew: ntp_drain_time_adjust() claims the linear share (rate-limited to MAX_TICKADJ/s), ntp_drain_time_offset() takes the exponential rest, and any overshoot stays in ntp_error. time_adjust keeps its whole-microsecond API value and full range; the sub-microsecond remainder being delivered lives in a new time_adjust_frac (plain shifted-ns), topped up by borrowing whole us from time_adjust as the drain consumes it. The linear drive is sourced from time_adjust *and* time_adjust_frac so the final sub-us residual is flushed and the slew converges exactly rather than stopping ~1us short. Signed-off-by: David Woodhouse Assisted-by: Kiro:claude-opus-4.8 --- kernel/time/ntp.c | 108 +++++++++++++++++++++++++++++++------ kernel/time/ntp_internal.h | 1 + kernel/time/timekeeping.c | 14 ++--- 3 files changed, 100 insertions(+), 23 deletions(-) diff --git a/kernel/time/ntp.c b/kernel/time/ntp.c index f2670e7985b8..7a5ff2a6c93b 100644 --- a/kernel/time/ntp.c +++ b/kernel/time/ntp.c @@ -74,6 +74,7 @@ struct ntp_data { s64 time_freq; time64_t time_reftime; long time_adjust; + s64 time_adjust_frac; s64 ntp_tick_adj; s64 cs_tick_adj; time64_t ntp_next_leap_sec; @@ -107,6 +108,9 @@ static struct ntp_data tk_ntp_data[TIMEKEEPERS_MAX] =3D= { =20 #define SECS_PER_DAY 86400 #define MAX_TICKADJ 500LL /* usecs */ +/* One microsecond of phase, in plain shifted-ns (ns << NTP_SCALE_SHIFT) */ +#define ONE_US_NS ((s64)NSEC_PER_USEC << NTP_SCALE_SHIFT) +/* Per-tick MAX_TICKADJ slew, in plain shifted-ns */ #define MAX_TICKADJ_SCALED \ (((MAX_TICKADJ * NSEC_PER_USEC) << NTP_SCALE_SHIFT) / NTP_INTERVAL_FREQ) #define MAX_TAI_OFFSET 100000 @@ -342,6 +346,7 @@ static void __ntp_clear(struct ntp_data *ntpdata) { /* Stop active adjtime() */ ntpdata->time_adjust =3D 0; + ntpdata->time_adjust_frac =3D 0; ntpdata->time_status |=3D STA_UNSYNC; ntpdata->time_maxerror =3D NTP_PHASE_LIMIT; ntpdata->time_esterror =3D NTP_PHASE_LIMIT; @@ -411,6 +416,66 @@ s64 ntp_drain_time_offset(unsigned int tkid, s64 amoun= t) return 0; } =20 +/* + * Drain the legacy adjtime() correction (time_adjust) as it is delivered. + * + * @amount is the total intentional per-tick skew for this accumulation + * (skew_delta << shift), in time_offset units (shifted_ns / HZ); it covers + * both the exponential time_offset slew and the linear adjtime slew. This + * function claims only the adjtime share =E2=80=94 capped at the MAX_TICK= ADJ rate =E2=80=94 + * and returns the remainder for ntp_drain_time_offset(). + * + * time_adjust is in whole us. The sub-us remainder being delivered lives = in + * time_adjust_frac (same shifted_ns/HZ units as time_offset); we top it u= p by + * borrowing whole us from time_adjust as the drain consumes it. + */ +s64 ntp_drain_time_adjust(unsigned int tkid, s64 amount, unsigned int shif= t) +{ + struct ntp_data *ntpdata =3D &tk_ntp_data[tkid]; + /* Sign reference: time_adjust if any whole us remain, else the drawer */ + s64 ref =3D ntpdata->time_adjust ? (s64)ntpdata->time_adjust + : ntpdata->time_adjust_frac; + s64 deliver, deficit, claimed; + + if (!amount || !ref || (amount > 0) !=3D (ref > 0)) + return amount; + + /* + * Phase to deliver this accumulation, in plain shifted-ns. The drain + * @amount is in =C3=B7HZ units, so multiply by HZ first, then clamp to t= he + * MAX_TICKADJ rate (MAX_TICKADJ_SCALED is the per-tick slew in + * shifted-ns). Multiply-then-clamp avoids an s64 divide for the cap. + */ + deliver =3D min(abs(amount) * NTP_INTERVAL_FREQ, + (s64)MAX_TICKADJ_SCALED << shift); + + /* Top up the sub-us drawer from whole-us time_adjust as needed */ + deficit =3D deliver - abs(ntpdata->time_adjust_frac); + if (deficit > 0 && ntpdata->time_adjust) { + long borrow =3D div64_u64(deficit + ONE_US_NS - 1, ONE_US_NS); + + borrow =3D min(borrow, (long)abs(ntpdata->time_adjust)); + if (ntpdata->time_adjust > 0) { + ntpdata->time_adjust -=3D borrow; + ntpdata->time_adjust_frac +=3D (s64)borrow * ONE_US_NS; + } else { + ntpdata->time_adjust +=3D borrow; + ntpdata->time_adjust_frac -=3D (s64)borrow * ONE_US_NS; + } + } + + /* Never deliver more than the drawer holds */ + deliver =3D min(deliver, abs(ntpdata->time_adjust_frac)); + if (ntpdata->time_adjust_frac > 0) + ntpdata->time_adjust_frac -=3D deliver; + else + ntpdata->time_adjust_frac +=3D deliver; + + /* Return the unclaimed remainder in =C3=B7HZ drain units for time_offset= */ + claimed =3D div_s64(deliver, NTP_INTERVAL_FREQ); + return (amount > 0) ? amount - claimed : amount + claimed; +} + /** * ntp_get_next_leap - Returns the next leapsecond in CLOCK_REALTIME ktime= _t * @tkid: Timekeeper ID @@ -528,26 +593,33 @@ int second_overflow(unsigned int tkid, time64_t secs) /* Check PPS signal */ pps_dec_valid(ntpdata); =20 - if (!ntpdata->time_adjust) - goto out; - - if (ntpdata->time_adjust > MAX_TICKADJ) { - ntpdata->time_adjust -=3D MAX_TICKADJ; - ntpdata->tick_length +=3D MAX_TICKADJ_SCALED; - goto out; - } + /* + * Bias the per-tick skew for any pending adjtime() correction, at up + * to MAX_TICKADJ (500us) per second. This only sizes the mult bias + * (and hence the per-tick drain budget); time_adjust itself is drained + * in logarithmic_accumulation() via ntp_drain_time_adjust(), per tick, + * so it is never decremented behind the per-tick accounting's back and + * never staged into time_offset (which would smear the exponential). + */ + if (ntpdata->time_adjust || ntpdata->time_adjust_frac) { + long adj =3D clamp(ntpdata->time_adjust, + (long)-MAX_TICKADJ, (long)MAX_TICKADJ); + /* + * Per-second linear phase to drive, in shifted-ns: the + * whole-us part plus the sub-us drawer, clamped to the + * MAX_TICKADJ rate. Including the drawer ensures the drive + * (and hence the drain) continues until the last sub-us + * remainder is flushed, not just until time_adjust hits zero. + */ + s64 chunk =3D (s64)adj * ONE_US_NS + ntpdata->time_adjust_frac; + s64 max =3D (s64)MAX_TICKADJ * ONE_US_NS; =20 - if (ntpdata->time_adjust < -MAX_TICKADJ) { - ntpdata->time_adjust +=3D MAX_TICKADJ; - ntpdata->tick_length -=3D MAX_TICKADJ_SCALED; - goto out; + chunk =3D clamp(chunk, -max, max); + /* shifted-ns/second -> per-tick skew_delta: divide by HZ*HZ */ + ntpdata->skew_delta +=3D div_s64(chunk, + (s64)NTP_INTERVAL_FREQ * NTP_INTERVAL_FREQ); } =20 - ntpdata->tick_length +=3D (s64)(ntpdata->time_adjust * NSEC_PER_USEC / NT= P_INTERVAL_FREQ) - << NTP_SCALE_SHIFT; - ntpdata->time_adjust =3D 0; - -out: return leap; } =20 @@ -840,6 +912,7 @@ int ntp_adjtimex(unsigned int tkid, struct __kernel_tim= ex *txc, const struct tim if (!(txc->modes & ADJ_OFFSET_READONLY)) { /* adjtime() is independent from ntp_adjtime() */ ntpdata->time_adjust =3D txc->offset; + ntpdata->time_adjust_frac =3D 0; ntp_update_frequency(ntpdata); =20 audit_ntp_set_old(ad, AUDIT_NTP_ADJUST, save_adjust); @@ -1081,6 +1154,7 @@ static void hardpps_update_phase(struct ntp_data *ntp= data, long error) NTP_INTERVAL_FREQ); /* Cancel running adjtime() */ ntpdata->time_adjust =3D 0; + ntpdata->time_adjust_frac =3D 0; } /* Update jitter */ ntpdata->pps_jitter +=3D (jitter - ntpdata->pps_jitter) >> PPS_INTMIN; diff --git a/kernel/time/ntp_internal.h b/kernel/time/ntp_internal.h index 1e708a2562ea..1647fd1a0d90 100644 --- a/kernel/time/ntp_internal.h +++ b/kernel/time/ntp_internal.h @@ -8,6 +8,7 @@ extern void ntp_clear(unsigned int tkid, s64 cs_tick_adj); 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 s64 ntp_drain_time_adjust(unsigned int tkid, s64 amount, unsigned i= nt shift); 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, con= st struct timespec64 *ts, diff --git a/kernel/time/timekeeping.c b/kernel/time/timekeeping.c index b8b0e9d7fc10..a9b4f4b8626e 100644 --- a/kernel/time/timekeeping.c +++ b/kernel/time/timekeeping.c @@ -2591,15 +2591,17 @@ static u64 logarithmic_accumulation(struct timekeep= er *tk, u64 offset, */ if (tk->skew_delta) { /* - * skew_delta is stored pre-divided by HZ, matching time_offset, - * so drain it directly. Fold the amount actually drained back - * into ntp_error in full clock units (=C3=97 NTP_INTERVAL_FREQ); any - * undrainable overshoot is left in ntp_error to be compensated - * by the dithering over subsequent ticks. + * skew_delta (stored =C3=B7HZ, matching time_offset) is the total + * intentional skew delivered this accumulation. Apportion it: + * the adjtime() linear share goes to time_adjust (capped at + * MAX_TICKADJ/s), the exponential rest to time_offset, and any + * undrainable overshoot stays in ntp_error (in full clock units, + * =C3=97 NTP_INTERVAL_FREQ) for the dithering to compensate. */ s64 drain =3D tk->skew_delta << shift; - s64 unclaimed =3D ntp_drain_time_offset(tk->id, drain); + s64 unclaimed =3D ntp_drain_time_adjust(tk->id, drain, shift); =20 + unclaimed =3D ntp_drain_time_offset(tk->id, unclaimed); tk->ntp_error +=3D (drain - unclaimed) * NTP_INTERVAL_FREQ; } =20 --=20 2.54.0 From nobody Thu Jun 11 05:15:07 2026 Received: from desiato.infradead.org (desiato.infradead.org [90.155.92.199]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 1D0B636BCFB for ; Wed, 10 Jun 2026 23:40:38 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=90.155.92.199 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781134841; cv=none; b=CElIpiggRpD6UpV9Digi9ev4pUMWOZgogNRohojx7cu59C7LynBxfCOlPuf0gVgFIE7hzFsL9H6urvmhsM+0jkFsD23aYafMaeLfsgAetcHCTBFl7353nhpBqW1/+5F6fBt09MLxGVZzsGIb3OB2JDcyCsRzd8R6b4dpRjFqifg= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1781134841; c=relaxed/simple; bh=SsnlcvbpcAA3vOoc9+M1BGaaS8FG2W6fqCcMYbvHCNs=; h=From:To:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=rxGdVOoz8jjmYX/luMP5E6KRrM5MK8fyf0Hy927AP41EKkAAMzzYHCyDd3vZ05iZjMAep6CrJXs4k0S6sljXaT/bORneVz7SgZ7tABZzXk04J4E3iZhJ1CVfIobUSqH+sojBtO4AGK6HfGwCU272UQl1FiDNPnIQo49f/yCuDYg= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org; spf=none smtp.mailfrom=desiato.srs.infradead.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b=TuTYmyu/; arc=none smtp.client-ip=90.155.92.199 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=infradead.org Authentication-Results: smtp.subspace.kernel.org; spf=none smtp.mailfrom=desiato.srs.infradead.org Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=infradead.org header.i=@infradead.org header.b="TuTYmyu/" DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=infradead.org; s=desiato.20200630; h=Sender:Content-Transfer-Encoding: MIME-Version:References:In-Reply-To:Message-ID:Date:Subject:To:From:Reply-To: Cc:Content-Type:Content-ID:Content-Description; bh=f9fQFV4xYjInWzkVtg01qT+6LZwbYM7ieA+MzJyeQyA=; b=TuTYmyu/l9gClUMI/vPulv2Wan QnbCT4rW8V1n68DK2v8qBT5UXGuji1hDgsMmwCNB24aW76kwL9djGYZTwhbjO7UMWkUMiKplHZ+2G 7ilN6KpCmz9ZmI3fELJn/Ijz30F9I8diXmcIi+eK87aERZb3tTecD0SfbxBVdbjRWf4blrUV+yEB9 KKlbiuRNGVeHShNrxf/7l/QWQzl32VqtRBfvNev4YUZ+em30uFkG7462+MS8cyHzdXs4E8ORFaN6V 7EWGg8PFLE1esGcMbAATXcjj+LIziItXT/ScsW8vh5CS5mAXwWQeTC6Zj5v7r44OSh3OeQCTU5+Uq foKV875w==; Received: from [2001:8b0:10b:1::425] (helo=i7.infradead.org) by desiato.infradead.org with esmtpsa (Exim 4.99.2 #2 (Red Hat Linux)) id 1wXSX1-00000004SaB-2anM; Wed, 10 Jun 2026 23:40:24 +0000 Received: from dwoodhou by i7.infradead.org with local (Exim 4.99.2 #2 (Red Hat Linux)) id 1wXSWz-00000000aPI-0wKc; Thu, 11 Jun 2026 00:40:21 +0100 From: David Woodhouse To: Richard Cochran , Wen Gu , David Woodhouse , Andrew Lunn , "David S. Miller" , Eric Dumazet , Jakub Kicinski , Paolo Abeni , John Stultz , Thomas Gleixner , Stephen Boyd , Anna-Maria Behnsen , Frederic Weisbecker , Shuah Khan , Peter Zijlstra , =?UTF-8?q?Thomas=20Wei=C3=9Fschuh?= , Arnd Bergmann , Miroslav Lichvar , Julien Ridoux , Ryan Luu , linux-kernel@vger.kernel.org Subject: [PATCH v5 6/6] ntp: Remove tick_length_base, use tick_length directly Date: Wed, 10 Jun 2026 23:30:12 +0100 Message-ID: <20260610234020.137999-7-dwmw2@infradead.org> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260610234020.137999-1-dwmw2@infradead.org> References: <20260610234020.137999-1-dwmw2@infradead.org> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Sender: David Woodhouse X-SRS-Rewrite: SMTP reverse-path rewritten from by desiato.infradead.org. See http://www.infradead.org/rpr.html Content-Type: text/plain; charset="utf-8" From: David Woodhouse Now that nothing inflates tick_length beyond tick_length_base (the adjtime path was converted to use time_offset in the previous commit), the two fields are always equal. Remove tick_length_base and keep tick_length as the single field. Remove the per-second reset and the delta update in ntp_update_frequency() since there is no separate base to track. No functional change intended. Signed-off-by: David Woodhouse Assisted-by: Kiro:claude-opus-4.6-1m --- kernel/time/ntp.c | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/kernel/time/ntp.c b/kernel/time/ntp.c index 7a5ff2a6c93b..28009f23f694 100644 --- a/kernel/time/ntp.c +++ b/kernel/time/ntp.c @@ -26,8 +26,7 @@ /** * struct ntp_data - Structure holding all NTP related state * @tick_usec: USER_HZ period in microseconds - * @tick_length: Adjusted tick length - * @tick_length_base: Base value for @tick_length + * @tick_length: Tick length in ns << NTP_SCALE_SHIFT * @time_state: State of the clock synchronization * @time_status: Clock status bits * @time_offset: Time adjustment in nanoseconds @@ -63,7 +62,6 @@ struct ntp_data { unsigned long tick_usec; u64 tick_length; - u64 tick_length_base; int time_state; int time_status; s64 time_offset; @@ -255,8 +253,7 @@ static inline void pps_fill_timex(struct ntp_data *ntpd= ata, struct __kernel_time #endif /* CONFIG_NTP_PPS */ =20 /* - * Update tick_length and tick_length_base, based on tick_usec, ntp_tick_a= dj and - * time_freq: + * Update tick_length based on tick_usec, ntp_tick_adj and time_freq: */ static void ntp_update_frequency(struct ntp_data *ntpdata) { @@ -274,8 +271,7 @@ static void ntp_update_frequency(struct ntp_data *ntpda= ta) * Don't wait for the next second_overflow, apply the change to the * tick length immediately: */ - ntpdata->tick_length +=3D new_base - ntpdata->tick_length_base; - ntpdata->tick_length_base =3D new_base; + ntpdata->tick_length =3D new_base; } =20 static inline s64 ntp_update_offset_fll(struct ntp_data *ntpdata, s64 offs= et64, long secs) @@ -353,7 +349,6 @@ static void __ntp_clear(struct ntp_data *ntpdata) =20 ntp_update_frequency(ntpdata); =20 - ntpdata->tick_length =3D ntpdata->tick_length_base; ntpdata->time_offset =3D 0; =20 ntpdata->ntp_next_leap_sec =3D TIME64_MAX; @@ -569,7 +564,6 @@ int second_overflow(unsigned int tkid, time64_t secs) } =20 /* Compute the phase adjustment for the next second */ - ntpdata->tick_length =3D ntpdata->tick_length_base; =20 /* * Set the per-tick skew rate for the tick code. This is in --=20 2.54.0