Skip to content

Timekeeping and Clocksources

How the kernel tracks time: TSC, HPET, clocksources, and NTP

Two abstractions: clocksource and clockevent

The kernel splits time hardware into two roles:

Abstraction Role Examples
clocksource Free-running counter to read elapsed time TSC, HPET, ACPI PM timer
clockevent Programmable device to generate interrupts at a future time LAPIC timer, HPET, ARM generic timer

A clocksource is polled; a clockevent fires interrupts. Both are needed: the clocksource provides nanosecond-resolution reads; the clockevent drives the scheduler tick and hrtimers.

struct clocksource

/* include/linux/clocksource.h */
struct clocksource {
    u64             (*read)(struct clocksource *cs);  /* read hardware counter */
    u64             mask;          /* bitmask for counter width */
    u32             mult;          /* counter→nanoseconds multiplier */
    u32             shift;         /* fractional shift for mult */
    u64             max_idle_ns;   /* max time without a read (for wrap detection) */
    u32             maxadj;        /* max adjustment to mult */
    u64             max_cycles;    /* max counter delta before overflow */
    const char     *name;
    int             rating;        /* quality: 400-499 perfect, 300-399 desired, 200-299 good, 100-199 base, 1-99 unfit */
    /* ... */
};

/* TSC clocksource (arch/x86/kernel/tsc.c) */
static struct clocksource clocksource_tsc = {
    .name           = "tsc",
    .rating         = 300,
    .read           = read_tsc,   /* rdtsc */
    .mask           = CLOCKSOURCE_MASK(64),
    .flags          = CLOCK_SOURCE_IS_CONTINUOUS | CLOCK_SOURCE_MUST_VERIFY,
    /* mult/shift calibrated at boot */
};

Counter → nanoseconds conversion

The mult and shift fields avoid division in the hot path:

/*
 * ns = (cycles * mult) >> shift
 *
 * mult is computed so that:
 *   (10^9 * 2^shift) / frequency = mult
 */
static inline s64 clocksource_cyc2ns(u64 cycles, u32 mult, u32 shift)
{
    return ((u64) cycles * mult) >> shift;
}

For a 3.6 GHz TSC: mult ≈ 1165084, shift = 22. Each call to clock_gettime does one rdtsc + one multiply + one shift — no division, no lock.

struct timekeeper

The timekeeper holds the current time state and is updated on every tick and NTP adjustment:

/* include/linux/timekeeper_internal.h */
struct timekeeper {
    /* Cacheline 0 (together with prepended seqcount of timekeeper core): */
    struct tk_read_base    tkr_mono;

    /* Cacheline 1: */
    u64                    xtime_sec;
    unsigned long          ktime_sec;
    struct timespec64      wall_to_monotonic;
    ktime_t                offs_real;
    ktime_t                offs_boot;
    union {
        ktime_t            offs_tai;
        ktime_t            offs_aux;
    };
    u32                    coarse_nsec;
    enum timekeeper_ids    id;

    /* Cacheline 2: */
    struct tk_read_base    tkr_raw;
    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;

    union {
        struct timespec64  monotonic_to_boot;
        struct timespec64  monotonic_to_aux;
    };

    u64                    cycle_interval;
    u64                    xtime_interval;
    s64                    xtime_remainder;
    u64                    raw_interval;

    ktime_t                next_leap_ktime;
    u64                    ntp_tick;
    s64                    ntp_error;
    u32                    ntp_error_shift;
    u32                    ntp_err_mult;
    u32                    skip_second_overflow;
    s32                    tai_offset;
};

struct tk_read_base {
    struct clocksource  *clock;
    u64                  mask;
    u64                  cycle_last;   /* last read counter value */
    u32                  mult;         /* adjusted mult (NTP modifies this) */
    u32                  shift;
    u64                  xtime_nsec;   /* accumulated nanoseconds (fractional) */
    ktime_t              base;         /* nanoseconds base */
    u64                  base_real;
};

Clock IDs

Clock ID Description Affected by
CLOCK_REALTIME Wall clock (UTC) settimeofday, NTP jumps
CLOCK_MONOTONIC Monotonically increasing from boot NTP rate adjustment only; never jumps
CLOCK_MONOTONIC_RAW Like MONOTONIC but no NTP adjustment Nothing — pure hardware
CLOCK_BOOTTIME Like MONOTONIC but includes suspend time Nothing
CLOCK_TAI International Atomic Time (no leap seconds) Leap second offset only
CLOCK_PROCESS_CPUTIME_ID Process CPU time
CLOCK_THREAD_CPUTIME_ID Thread CPU time
struct timespec64 ts;
clock_gettime(CLOCK_MONOTONIC, &ts);  /* nanosecond precision */
clock_gettime(CLOCK_REALTIME, &ts);   /* wall clock */

TSC: Time Stamp Counter

The TSC is the primary clocksource on x86. It's a 64-bit counter incremented every CPU cycle.

TSC calibration

At boot, the kernel calibrates the TSC frequency against a known-good reference (HPET, PIT, or ACPI PM timer):

/* arch/x86/kernel/tsc.c */
static unsigned long pit_calibrate_tsc(u32 latch, unsigned long ms, int loopmin)
{
    u64 tsc, t1, t2, delta;
    unsigned long tscmin, tscmax;

    /* Program PIT channel 2 for a known interval */
    outb((inb(0x61) & ~0x02) | 0x01, 0x61);

    /* Measure TSC ticks in that interval */
    t1 = get_cycles();
    /* ... wait for PIT ... */
    t2 = get_cycles();

    delta = t2 - t1;
    /* delta / ms = TSC frequency in kHz */
    return delta / ms;
}

TSC invariance

Modern CPUs have an invariant TSC (CPUID[0x80000007] bit 8) that runs at a fixed rate regardless of P-states or C-states. This makes TSC a reliable clocksource even on laptops with frequency scaling.

# Check invariant TSC support
grep "constant_tsc nonstop_tsc" /proc/cpuinfo

Without invariant TSC, TSC drifts when the CPU frequency changes — unreliable for timekeeping.

NTP: adjusting the clock

NTP runs in userspace (ntpd/chronyd) but applies corrections to the kernel clock via adjtimex(2):

/* Userspace NTP daemon calls: */
struct timex tx = {
    .modes  = ADJ_FREQUENCY | ADJ_OFFSET,
    .freq   = ppm_scaled,   /* frequency error in scaled PPM */
    .offset = offset_ns,    /* time offset in nanoseconds */
};
adjtimex(&tx);

The kernel applies the frequency correction by adjusting the clocksource mult value:

new_mult = orig_mult + (freq_error * orig_mult) / NSEC_PER_SEC

This makes the counter-to-nanosecond conversion run slightly faster or slower without any visible jumps.

NTP phase-locked loop (PLL)

/* kernel/time/ntp.c */
static void ntp_update_frequency(struct ntp_data *ntpdata)
{
    u64 second_length, new_base, tick_usec = (u64)ntpdata->tick_usec;

    second_length        = (u64)(tick_usec * NSEC_PER_USEC * USER_HZ) << NTP_SCALE_SHIFT;

    second_length       += ntpdata->ntp_tick_adj;
    second_length       += ntpdata->time_freq;

    new_base             = div_u64(second_length, NTP_INTERVAL_FREQ);

    /*
     * Don't wait for the next second_overflow, apply the change to the
     * tick length immediately:
     */
    ntpdata->tick_length        += new_base - ntpdata->tick_length_base;
    ntpdata->tick_length_base    = new_base;
}

vDSO: time without a syscall

clock_gettime(CLOCK_MONOTONIC) is the most-called libc function. The kernel optimizes it via the vDSO (virtual dynamic shared object) — a read-only page mapped into every process that contains the timekeeping fast path.

/* lib/vdso/gettimeofday.c — runs in userspace without syscall */
static __always_inline int
do_hres(const struct vdso_time_data *vd, const struct vdso_clock *vc, clockid_t clk, struct __kernel_timespec *ts)
{
    u64 sec, ns;
    u32 seq;

    /* Allows to compile the high resolution parts out */
    if (!__arch_vdso_hres_capable())
        return false;

    do {
        if (vdso_read_begin_timens(vc, &seq))
            return do_hres_timens(vd, vc, clk, ts);

        if (!vdso_get_timestamp(vd, vc, clk, &sec, &ns))
            return false;
    } while (vdso_read_retry(vc, seq));

    vdso_set_timespec(ts, sec, ns);

    return true;
}

The vDSO page is updated by the kernel on every tick. User reads are entirely in userspace — no ring transitions, no TLB misses to kernel pages.

Observing timekeeping

# Current clocksource and available alternatives
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
cat /sys/devices/system/clocksource/clocksource0/available_clocksource

# Force a different clocksource (for testing/debugging)
echo hpet | sudo tee /sys/devices/system/clocksource/clocksource0/current_clocksource

# TSC frequency and stability
dmesg | grep -E "TSC|tsc"
# tsc: Detected 3600.000 MHz processor
# tsc: Detected 3600.000 MHz TSC
# clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x...

# NTP synchronization status
timedatectl show | grep NTP
chronyc tracking

# Time namespace (containers may have their own offset)
# /proc/<pid>/timens_offsets shows MONOTONIC/BOOTTIME offsets

# Kernel time debugging
cat /proc/timer_list | head -50

Further reading

Kernel source

Man pages

  • clock_gettime(2) — retrieving clock timestamps across supported clock domains
  • clock_settime(2) — setting CLOCK_REALTIME and triggering clock_was_set() notifications
  • adjtimex(2) — adjusting kernel clock parameters and retrieving synchronization status
  • time(7) — overview of time and timer APIs in Linux

LWN articles

  • Introduce CLOCK_REALTIME_COARSE — John Stultz, August 2009: the introduction of CLOCK_REALTIME_COARSE and CLOCK_MONOTONIC_COARSE vDSO clocks
  • The trouble with the TSC — Jake Edge, May 2010: invariant TSC guarantees, frequency scaling drift, and hardware clock calibration

External