RE: [PATCH v3] printk: fix zero-valued printk timestamps in early boot

Michael Kelley <[email protected]> Wed, 11 Mar 2026 15:47:01 +0000
Newsgroups org.kernel.vger.linux-embedded,org.kernel.vger.linux-kernel
Message-ID <SN6PR02MB41571D45C1C8ADF89BD1FE8DD447A@SN6PR02MB4157.namprd02.prod.outlook.com>
From: Tim Bird <[email protected]> Sent: Tuesday, February 10, 2026 3:48 PM
>=20
> During early boot, printk timestamps are reported as zero before
> kernel timekeeping starts (e.g. before time_init()).  This
> hinders boot-time optimization efforts.  This period is about 400
> milliseconds for many current desktop and embedded machines
> running Linux.
>=20
> Add support to save cycles during early boot, and output correct
> timestamp values after timekeeping is initialized.  get_cycles()
> is operational on arm64 and x86_64 from kernel start.  Add code
> and variables to save calibration values used to later convert
> cycle counts to time values in the early printks.  Add a config
> to control the feature.
>=20
> This yields non-zero timestamps for printks from the very start
> of kernel execution.  The timestamps are relative to the start of
> the architecture-specified counter used in get_cycles
> (e.g. the TSC on x86_64 and cntvct_el0 on arm64).
>=20
> All timestamps reflect time from processor power-on instead of
> time from the kernel's timekeeping initialization.

I tried this patch in linux-next20260302 kernel running as a guest VM
on a Hyper-V host. Two things:

1) In the dmesg output, I'm seeing a place where the timestamps briefly go
backwards -- i.e., they are not monotonically increasing. Here's a snippet,
where there's a smaller timestamp immediately after the tsc is detected:

[   27.994891] SMBIOS 3.1.0 present.
[   27.994893] DMI: Microsoft Corporation Virtual Machine/Virtual Machine, =
BIOS Hyper-V UEFI Release v4.1 09/25/2025
[   27.994898] DMI: Memory slots populated: 2/2
[   27.995202] Hypervisor detected: Microsoft Hyper-V
[   27.995205] Hyper-V: privilege flags low 0xae7f, high 0x3b8030, ext 0x62=
, hints 0xa0e24, misc 0xe0bed7b2
[   27.995208] Hyper-V: Nested features: 0x0
[   27.995209] Hyper-V: LAPIC Timer Frequency: 0xc3500
[   27.995210] Hyper-V: Using hypercall for remote TLB flush
[   27.995216] clocksource: hyperv_clocksource_tsc_page: mask: 0xffffffffff=
ffffff max_cycles: 0x24e6a1710, max_idle_ns: 440795202120 ns
[   27.995218] clocksource: hyperv_clocksource_msr: mask: 0xfffffffffffffff=
f max_cycles: 0x24e6a1710, max_idle_ns: 440795202120 ns
[   27.995220] tsc: Detected 2918.401 MHz processor
[   27.991060] e820: update [mem 0x00000000-0x00000fff] System RAM =3D=3D> =
device reserved
[   27.991062] e820: remove [mem 0x000a0000-0x000fffff] System RAM
[   27.991064] last_pfn =3D 0x210000 max_arch_pfn =3D 0x400000000
[   27.991065] x86/PAT: PAT support disabled because CONFIG_X86_PAT is disa=
bled in the kernel.
[   27.991066] x86/PAT: Configuration [0-7]: WB  WT  UC- UC  WB  WT  UC- UC

Hyper-V provides a synthetic clocksource (two actually), and perhaps they
are the cause of the problem, though I haven't spent any time debugging.

2) A Linux VM running in the Azure cloud is also running on Hyper-V. Such a
VM typically uses cloud-init to set everything up at boot time, and cloud-i=
nit
is outputting lines to the serial console with a timestamp that looks like =
the
printk() timestamp, but apparently is not adjusted for the early timestampi=
ng
that this patch adds. Again, I haven't debugged what's going on -- I'm not
immediately sure of the mechanism that cloud-init uses to do output to the
serial console. The use of the Hyper-V synthetic clock source might the cau=
se
of the problem here as well. Here's an output snippet from the serial conso=
le:

[   20.330414] systemd[1]: Condition check resulted in OpenVSwitch configur=
ation for cleanup being skipped.
[   20.332911] systemd[1]: Condition check resulted in Set Up Additional Bi=
nary Formats being skipped.
[   20.333257] pstore: Registered efi_pstore as persistent store backend
[   20.334360] systemd[1]: Condition check resulted in File System Check on=
 Root Device being skipped.
[   20.338319] systemd[1]: Starting Load Kernel Modules...
[   20.341094] systemd[1]: Starting Remount Root and Kernel File Systems...
[   20.350993] systemd[1]: Starting udev Coldplug all Devices...
[   20.356255] systemd[1]: Starting Uncomplicated firewall...
[   20.361536] systemd[1]: Started Journal Service.
[   20.386902] EXT4-fs (sda1): re-mounted c02dce0c-0c40-4e6e-88af-c5a0987b0=
adb r/w.
[   22.532033] /dev/sr0: Can't lookup blockdev
[    7.955973] cloud-init[783]: Cloud-init v. 24.3.1-0ubuntu0~20.04.1 runni=
ng 'init-local' at Wed, 11 Mar 2026 15:27:06 +0000. Up 7.48 seconds.
[    9.933120] cloud-init[822]: Cloud-init v. 24.3.1-0ubuntu0~20.04.1 runni=
ng 'init' at Wed, 11 Mar 2026 15:27:08 +0000. Up 9.82 seconds.
[    9.935483] cloud-init[822]: ci-info: ++++++++++++++++++++++++++++++++++=
++++Net device info+++++++++++++++++++++++++++++++++++++++
[    9.937726] cloud-init[822]: ci-info: +--------+------+-----------------=
------------+---------------+--------+-------------------+
[    9.939905] cloud-init[822]: ci-info: | Device |  Up  |           Addres=
s           |      Mask     | Scope  |     Hw-Address    |
[    9.942059] cloud-init[822]: ci-info: +--------+------+-----------------=
------------+---------------+--------+-------------------+

The cloud-init lines don't show up in dmesg, so there's no problem there.

I will look into both issues further, but probably not today.

Michael

>=20
> Signed-off-by: Tim Bird <[email protected]>
> ---
> V2->V3
>  Default CONFIG option to 'n'
>  Move more code from into early_times.h
>   (reducing ifdefs in init/main.c)
>  Use math64 helper routines
>  Use cycles_t instead of u64 type
>  Add #defines for EARLY_CYCLES_BIT and MASK
>  Invert if logic in adjust_early_ts()
>  (note: no change to 'depends on' in Kconfig entry)
>=20
> V1->V2
>  Remove calibration CONFIG vars
>  Add 'depends on' to restrict arches (to handle ppc bug)
>  Add early_ts_offset to avoid discontinuity
>  Save cycles in ts_nsec, and convert on output
>  Move conditional code to include file (early_times.h)
>=20
>  include/linux/early_times.h | 85 +++++++++++++++++++++++++++++++++++++
>  init/Kconfig                | 14 ++++++
>  init/main.c                 |  6 +++
>  kernel/printk/printk.c      | 18 +++++++-
>  4 files changed, 121 insertions(+), 2 deletions(-)
>  create mode 100644 include/linux/early_times.h
>=20
> diff --git a/include/linux/early_times.h b/include/linux/early_times.h
> new file mode 100644
> index 000000000000..05388dcb8573
> --- /dev/null
> +++ b/include/linux/early_times.h
> @@ -0,0 +1,85 @@
> +/* SPDX-License-Identifier: GPL-2.0 */
> +
> +#ifndef _KERNEL_PRINTK_EARLY_TIMES_H
> +#define _KERNEL_PRINTK_EARLY_TIMES_H
> +
> +#include <linux/timex.h>
> +#include <linux/clocksource.h>
> +
> +/* use high bit of a u64 to indicate cycles instead of a timestamp */
> +#define EARLY_CYCLES_BIT	BIT_ULL(63)
> +#define EARLY_CYCLES_MASK	~(BIT_ULL(63))
> +
> +#if defined(CONFIG_EARLY_PRINTK_TIMES)
> +extern cycles_t start_cycles;
> +extern u64 start_ns;
> +extern u32 early_mult, early_shift;
> +extern u64 early_ts_offset;
> +
> +static inline void early_times_start_calibration(void)
> +{
> +	start_cycles =3D get_cycles();
> +	start_ns =3D local_clock();
> +}
> +
> +static inline void early_times_finish_calibration(void)
> +{
> +	cycles_t end_cycles;
> +	u64 end_ns;
> +
> +	/* set calibration data for early_printk_times */
> +	end_cycles =3D get_cycles();
> +	end_ns =3D local_clock();
> +	clocks_calc_mult_shift(&early_mult, &early_shift,
> +		mul_u64_u64_div_u64(end_cycles - start_cycles,
> +			NSEC_PER_SEC, end_ns - start_ns),
> +		NSEC_PER_SEC, 100);
> +	early_ts_offset =3D mul_u64_u32_shr(start_cycles, early_mult, early_shi=
ft) -
> start_ns;
> +
> +	pr_debug("Early printk times: mult=3D%u, shift=3D%u, offset=3D%llu ns\n=
",
> +		early_mult, early_shift, early_ts_offset);
> +}
> +
> +static inline u64 early_cycles(void)
> +{
> +	return (get_cycles() | EARLY_CYCLES_BIT);
> +}
> +
> +/*
> + * adjust_early_ts detects whether ts in is cycles or nanoseconds
> + * and converts it or adjusts it, taking into account the offset
> + * from cycle-counter start.
> + *
> + * Note that early_mult may be 0, but that's OK because
> + * we'll just multiply by 0 and return 0. This will
> + * only occur if we're outputting a printk message
> + * before the calibration of the early timestamp.
> + * Any output after user space start (eg. from dmesg or
> + * journalctl) will show correct values.
> + */
> +static inline u64 adjust_early_ts(u64 ts)
> +{
> +	if (likely(!(ts & EARLY_CYCLES_BIT)))
> +		/* if timestamp is not in cycles, just add offset */
> +		return ts + early_ts_offset;
> +
> +	/* mask high bit and convert to nanoseconds */
> +	return mul_u64_u32_shr(ts & EARLY_CYCLES_MASK, early_mult, early_shift)=
;
> +}
> +
> +#else
> +# define early_times_start_calibration() do { } while (0)
> +# define early_times_finish_calibration() do { } while (0)
> +
> +static inline u64 early_cycles(void)
> +{
> +	return 0;
> +}
> +
> +static inline u64 adjust_early_ts(u64 ts)
> +{
> +	return ts;
> +}
> +#endif /* CONFIG_EARLY_PRINTK_TIMES */
> +
> +#endif /* _KERNEL_PRINTK_EARLY_TIMES_H */
> diff --git a/init/Kconfig b/init/Kconfig
> index fa79feb8fe57..a928c1efb09d 100644
> --- a/init/Kconfig
> +++ b/init/Kconfig
> @@ -777,6 +777,20 @@ config IKHEADERS
>  	  or similar programs.  If you build the headers as a module, a module =
called
>  	  kheaders.ko is built which can be loaded on-demand to get access to h=
eaders.
>=20
> +config EARLY_PRINTK_TIMES
> +	bool "Show non-zero printk timestamps early in boot"
> +	default n
> +	depends on PRINTK
> +	depends on ARM64 || X86_64
> +	help
> +	  Use a cycle-counter to provide printk timestamps during
> +	  early boot.  This allows seeing timestamps for printks that
> +	  would otherwise show as 0.  Note that this will shift the
> +	  printk timestamps to be relative to processor power on, instead
> +	  of relative to the start of kernel timekeeping.  This should be
> +	  closer to machine power on, giving a better indication of
> +	  overall boot time.
> +
>  config LOG_BUF_SHIFT
>  	int "Kernel log buffer size (16 =3D> 64KB, 17 =3D> 128KB)"
>  	range 12 25
> diff --git a/init/main.c b/init/main.c
> index b84818ad9685..d5774aec1aff 100644
> --- a/init/main.c
> +++ b/init/main.c
> @@ -104,6 +104,7 @@
>  #include <linux/pidfs.h>
>  #include <linux/ptdump.h>
>  #include <linux/time_namespace.h>
> +#include <linux/early_times.h>
>  #include <net/net_namespace.h>
>=20
>  #include <asm/io.h>
> @@ -1118,6 +1119,9 @@ void start_kernel(void)
>  	timekeeping_init();
>  	time_init();
>=20
> +	/* This must be after timekeeping is initialized */
> +	early_times_start_calibration();
> +
>  	/* This must be after timekeeping is initialized */
>  	random_init();
>=20
> @@ -1600,6 +1604,8 @@ static int __ref kernel_init(void *unused)
>=20
>  	do_sysctl_args();
>=20
> +	early_times_finish_calibration();
> +
>  	if (ramdisk_execute_command) {
>  		ret =3D run_init_process(ramdisk_execute_command);
>  		if (!ret)
> diff --git a/kernel/printk/printk.c b/kernel/printk/printk.c
> index 1d765ad242b8..5afd31c3345c 100644
> --- a/kernel/printk/printk.c
> +++ b/kernel/printk/printk.c
> @@ -46,6 +46,7 @@
>  #include <linux/ctype.h>
>  #include <linux/uio.h>
>  #include <linux/sched/clock.h>
> +#include <linux/early_times.h>
>  #include <linux/sched/debug.h>
>  #include <linux/sched/task_stack.h>
>  #include <linux/panic.h>
> @@ -75,6 +76,13 @@ EXPORT_SYMBOL(ignore_console_lock_warning);
>=20
>  EXPORT_TRACEPOINT_SYMBOL_GPL(console);
>=20
> +#ifdef CONFIG_EARLY_PRINTK_TIMES
> +cycles_t start_cycles;
> +u64 start_ns;
> +u32 early_mult, early_shift;
> +u64 early_ts_offset;
> +#endif
> +
>  /*
>   * Low level drivers may need that to know if they can schedule in
>   * their unblank() callback or not. So let's export it.
> @@ -639,7 +647,7 @@ static void append_char(char **pp, char *e, char c)
>  static ssize_t info_print_ext_header(char *buf, size_t size,
>  				     struct printk_info *info)
>  {
> -	u64 ts_usec =3D info->ts_nsec;
> +	u64 ts_usec =3D adjust_early_ts(info->ts_nsec);
>  	char caller[20];
>  #ifdef CONFIG_PRINTK_CALLER
>  	u32 id =3D info->caller_id;
> @@ -1352,7 +1360,11 @@ static size_t print_syslog(unsigned int level, cha=
r *buf)
>=20
>  static size_t print_time(u64 ts, char *buf)
>  {
> -	unsigned long rem_nsec =3D do_div(ts, 1000000000);
> +	unsigned long rem_nsec;
> +
> +	ts =3D adjust_early_ts(ts);
> +
> +	rem_nsec =3D do_div(ts, 1000000000);
>=20
>  	return sprintf(buf, "[%5lu.%06lu]",
>  		       (unsigned long)ts, rem_nsec / 1000);
> @@ -2242,6 +2254,8 @@ int vprintk_store(int facility, int level,
>  	 * timestamp with respect to the caller.
>  	 */
>  	ts_nsec =3D local_clock();
> +	if (!ts_nsec)
> +		ts_nsec =3D early_cycles();
>=20
>  	caller_id =3D printk_caller_id();
>=20
> --
> 2.43.0
>=20