时间子系统与时钟源:jiffies、hrtimer、clocksource 与 NTP/PTP 校时

从 jiffies 与周期性 tick 到 tickless 内核,从 clocksource 抽象到 hrtimer 高精度定时器,再延伸到 NTP/PTP 校时与 CLOCK_ 系列接口的语义差异,系统梳理 Linux 时间子系统的分层结构与常见陷阱。

“现在几点"看似是个简单问题,但在内核里它被拆成了至少三类互不相同的时间:墙上时钟(wall clock,可被校时跳变)、单调时钟(monotonic,只增不减)、以及启动时间(boottime,包含休眠时长)。内核要同时维护它们,还要在纳秒精度上调度定时器、在多核之间保持时间一致、并在硬件时钟漂移时平滑校正。这套机制就是时间子系统(timekeeping)。

本文从最底层的 jiffies 与时钟中断出发,逐层向上拆解:tick 如何驱动调度、clocksource 如何抽象硬件计数器、hrtimer 如何实现纳秒级定时、NTP/PTP 如何把系统时间对齐到外部基准,最后落到用户态接口与调优。它与 https://plumephp.com/os-interrupts/(时钟中断)、https://plumephp.com/os-cpu-scheduling/(调度节拍)、https://plumephp.com/os-realtime-scheduling/(确定性延迟)以及 https://plumephp.com/os-kernel-module/(内核模块)互为补充。


一、时间的多重身份

1.1 三类时间语义

内核(以及 POSIX 接口)把时间分成几个语义完全不同的时钟,混淆它们是应用层最常见的 bug 来源:

时钟 ID语义是否受校时影响是否含休眠
CLOCK_REALTIME墙上时间(Unix epoch)会跳变是
CLOCK_MONOTONIC自启动单调递增平滑校正否
CLOCK_BOOTTIME同 monotonic 但含 suspend平滑校正是
CLOCK_MONOTONIC_RAW原始硬件计数,无校正否否

核心区别在于"是否会被 NTP 跳变影响”:CLOCK_REALTIME 可能因为 settimeofday 或 NTP step 而向前或向后跳,用它测量时间间隔(如 t1 - t0)会得到荒谬结果;测量耗时必须用 CLOCK_MONOTONIC。

1.2 内核中的 timekeeper

内核用一个全局的 struct timekeeper 保存所有时间状态:

/* kernel/time/timekeeping.c */
struct timekeeper {
    struct tk_read_base tkr_mono;   /* 单调时钟读基准 */
    struct tk_read_base tkr_raw;    /* 原始时钟读基准 */
    u64 xtime_sec;                  /* 墙上时钟的秒部分 */
    unsigned long xtime_nsec;
    u32 tai_offset;                 /* TAI - UTC 闰秒差 */
    int clock_was_set_seq;
    ...
};

读时间是"读取硬件计数器 + 按 mult/shift 换算成纳秒 + 加上累积的基准偏移"三步。为了避免每次读都抢锁,内核用 seqcount 做无锁读:读者在前后各读一次序号,若一致则数据有效,否则重试。

1.3 一次 gettimeofday 的代价

由于 vDSO(virtual DSO),用户态读 clock_gettime(CLOCK_REALTIME) 通常不进内核:内核把换算参数与时钟源内存映射给用户态,由 vDSO 在用户空间直接算。这就是为什么高频时间读取在现代 Linux 上只需几十纳秒。若发现时间读取慢,先确认是否真的走了 vDSO:

# 查看进程是否使用了 vDSO
ldd /bin/ls | grep vdso
# linux-vdso.so.1 => (0x00007ffd...)

# 用 perf 看是否发生 clock_gettime 系统调用
perf trace -e clock_gettime ./app

二、jiffies 与 tick

2.1 jiffies 是什么

jiffies 是内核启动以来的"节拍计数",每个时钟中断(tick)加一。它是一个 unsigned long 的全局变量:

/* include/linux/jiffies.h */
extern unsigned long volatile jiffies;
extern u64 jiffies_64;

时间单位换算依赖 HZ(每秒节拍数):

#define HZ 1000                      /* 常见配置 */
#define msecs_to_jiffies(m) ((m) * HZ / 1000)

HZ=1000 时一个 jiffy 等于 1 毫秒。这就是为什么基于 jiffies 的传统定时器精度上限约为 1 毫秒——它们只能以 tick 为粒度触发。

2.2 溢出与回绕

jiffies 是 unsigned long,32 位系统上约 49.7 天回绕一次(HZ=1000)。因此绝不能用 if (a > b) 直接比较 jiffies,必须用宏:

/* 正确:time_after 处理了回绕 */
if (time_after(jiffies, timeout)) { ... }

/* 错误:回绕后会得到相反结果 */
if (jiffies > timeout) { ... }

time_after 利用无符号减法:只要两个时间点相差不超过半个回绕周期,(long)(a - b) > 0 就是正确的。

2.3 从周期性 tick 到 tickless

传统内核每个 CPU 都有一个周期性时钟中断(CONFIG_HZ_PERIODIC),无论有无工作都每秒打断 HZ 次。这带来两个问题:空闲时白白耗电,且 tick 会干扰实时任务的确定性。

于是有了 tickless(NO_HZ):

  • CONFIG_NO_HZ_IDLE:CPU 空闲时停止周期性 tick,只在有定时器到期时唤醒。
  • CONFIG_NO_HZ_FULL:连运行用户态任务时也停 tick(“自适应 tickless”),只给需要节拍的任务保留。这是实时/低延迟场景的重要配置。
# 查看当前配置
grep CONFIG_NO_HZ= /boot/config-$(uname -r)
# 查看 tick 是否被停用
cat /proc/timer_list | head -40

tickless 的代价是调度器不再有固定节拍,“负载统计"改为在需要时按时间差推算。

2.4 tick 与调度

即使有 tickless,scheduler_tick() 仍承担着关键职责:更新运行时间统计(update_curr)、检查是否需要抢占、处理 CFS 的时间片。sched_latency_ns 与 sched_min_granularity_ns 决定了单次调度周期,它们以纳秒为单位、由 hrtimer 驱动而非 jiffies——这正是 CFS 能做出亚毫秒级公平性的原因。


三、clocksource 与 clockevent

3.1 两个方向的抽象

时间子系统把硬件时钟抽象成两类对象:

  • clocksource(时钟源):一个"只会单调累加"的计数器,用于读时间。典型如 TSC(x86 时间戳计数器)、HPET、ACPI PM Timer、ARM 的 arch_timer。
  • clockevent(时钟事件设备):一个"能在未来某个时刻产生中断"的设备,用于设定时器。典型如 LAPIC timer、HPET、ARM generic timer。
/* include/linux/clocksource.h */
struct clocksource {
    u64 (*read)(struct clocksource *cs);   /* 读计数器 */
    u64 mask;                              /* 计数器位宽掩码 */
    u32 mult;                              /* 换算:ns = cycles * mult >> shift */
    u32 shift;
    u64 max_idle_ns;
    int rating;                            /* 优先级评分,越高越优先 */
    const char *name;
    ...
};

3.2 rating 与选择逻辑

内核启动时遍历所有注册的 clocksource,选 rating 最高的那个作为当前时钟源:

# 查看可用时钟源与当前选择
cat /sys/devices/system/clocksource/clocksource0/available_clocksource
# tsc hpet acpi_pm
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
# tsc

# 手动切换(需重启生效)
echo hpet > /sys/devices/system/clocksource/clocksource0/current_clocksource

x86 上 TSC 的 rating 最高(通常 300+),因为它是每核寄存器、读取极快且无总线开销。但 TSC 有两个历史陷阱:核间不同步与变频漂移。现代 CPU 的 “invariant TSC” 保证主频变化时 TSC 频率恒定,才让它重新成为首选。

3.3 TSC 不稳时的表现

如果 TSC 不可靠而内核没检测到,会表现为:dmesg 出现 TSC unstable 或 clocksource: Switched to clocksource hpet、系统时间跑快/跑慢、CLOCK_MONOTONIC 与实际耗时不符。排查:

dmesg | grep -iE 'tsc|clocksource'
# 观察 TSC 是否被标记 unstable,以及切换到了哪个源

内核通过 watchdog(clocksource_watchdog)周期性比对不同时钟源,发现偏差过大就降级 TSC。


四、hrtimer:高精度定时器

4.1 为什么需要它

基于 jiffies 的 timer_list 精度受 HZ 限制(毫秒级)。hrtimer 则直接由 clockevent 驱动,可以做到纳秒级精度,并且支持红黑树组织的到期队列:

/* include/linux/hrtimer.h */
struct hrtimer {
    struct timerqueue_node node;   /* 红黑树节点 */
    ktime_t _softexpires;
    enum hrtimer_restart (*function)(struct hrtimer *);
    struct hrtimer_clock_base *base;
    u8 state;
    u8 is_rel;
    ...
};

红黑树保证插入/删除 O(log n),最早到期的定时器在树的最左端,O(1) 可取出。

4.2 两种时钟基准

hrtimer 可以挂在两种 base 上:

  • CLOCK_MONOTONIC base:不受校时影响。
  • CLOCK_REALTIME base:受校时影响;当墙上时钟被调整时,内核会重新计算这些定时器的到期点(clock_was_set 路径)。

写驱动时,若定时器用于周期性内部逻辑,应挂 monotonic base,避免 NTP 校时导致定时器"提前或推迟”。

4.3 一个内核模块示例

#include <linux/hrtimer.h>
#include <linux/ktime.h>
#include <linux/module.h>

static struct hrtimer my_timer;

static enum hrtimer_restart my_cb(struct hrtimer *t)
{
    pr_info("hrtimer fired at %lld ns\n", ktime_get_ns());
    hrtimer_forward_now(t, ms_to_ktime(100));   /* 每 100ms 重排 */
    return HRTIMER_RESTART;
}

static int __init my_init(void)
{
    hrtimer_init(&my_timer, CLOCK_MONOTONIC, HRTIMER_MODE_REL);
    my_timer.function = my_cb;
    hrtimer_start(&my_timer, ms_to_ktime(100), HRTIMER_MODE_REL);
    return 0;
}

static void __exit my_exit(void)
{
    hrtimer_cancel(&my_timer);
}

module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");

回调运行在软中断上下文(HRTIMER_MODE_REL 时通常在 hrtimer_softirq),因此不能睡眠、不能调用可能阻塞的函数。

4.4 定时器精度与开销

hrtimer 的精度受两个因素限制:

  1. clockevent 的硬件精度:若底层是 HPET,分辨率可能只有几十纳秒。
  2. 中断延迟与软中断处理:回调不是"精确到那一刻"执行,而是"到期后尽快执行"。

因此"定时器精度"和"定时器抖动(jitter)“是两回事。在实时场景,抖动往往比精度更致命。


五、时间同步:NTP、PTP 与校时路径

5.1 两种校时方式:step 与 slew

NTP 校时有两条路径:

  • step(阶跃):直接跳到正确时间。用于偏差很大时(如启动后首次同步)。会导致 CLOCK_REALTIME 跳变。
  • slew(平滑):通过调整 timekeeper 的换算系数,让时间"走快/走慢"一点点,逐渐追上。不产生跳变。

内核通过 adjtimex(2) 暴露这个控制面:

struct timex tx = {0};
tx.modes = ADJ_FREQUENCY;
tx.freq  = 100;              /* 微调频率,单位 ppm·2^16 */
adjtimex(&tx);

NTP 守护进程(chronyd、ntpd)就是周期性调用 adjtimex 来微调频率、偶尔用 clock_settime 做 step。

5.2 PTP:更高精度的对齐

NTP 通过普通网络包往返估算延迟,典型精度毫秒级。PTP(Precision Time Protocol, IEEE 1588)依赖支持硬件时间戳的网卡,把打戳点放到 PHY/MAC 层,消除协议栈抖动,可做到亚微秒级。

# Linux 上的 PTP 实现
ptp4l -i eth0 -m            # PTP 主/从同步
phc2sys -s eth0 -w -m       # 把网卡硬件时钟同步给系统时钟

关键概念是 PHC(PTP Hardware Clock):网卡自带一个高精度时钟,/dev/ptp0 暴露给用户态。phc2sys 负责把 PHC 的时间"喂"给系统时钟,反过来也可以。金融、工业控制、5G 前传都依赖这条路径。

5.3 闰秒:时间子系统的"地震”

闰秒会导致 CLOCK_REALTIME 与 CLOCK_MONOTONIC 的差值突变。2012 年的闰秒事件曾导致大量 Linux 机器 hrtimer 自旋、CPU 100%。内核后来引入 leap second smearing(由 chronyd 在用户态实现)与 tai_offset 的平滑处理来缓解。运维上,现代实践是用 smear 把闰秒摊平到 24 小时,避免跳变。


六、用户态接口与常见误区

6.1 关键接口

#include <time.h>

/* 读时间:优先用 vDSO 加速 */
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);

/* 高精度睡眠 */
struct timespec req = { .tv_sec = 0, .tv_nsec = 500000 };  /* 0.5ms */
nanosleep(&req, NULL);

/* POSIX 定时器:到期投递信号 */
timer_create(CLOCK_MONOTONIC, NULL, &tid);
timer_settime(tid, 0, &its, NULL);
接口粒度是否受校时适用场景
time()秒是日志时间戳
clock_gettime(MONOTONIC)纳秒否测量耗时
nanosleep纳秒相对精确定时
timerfd纳秒可配epoll 集成
alarm秒是老旧超时

6.2 五个高频误区

  1. 用 CLOCK_REALTIME 测量间隔。NTP step 会让差值为负或异常大。测耗时一律用 CLOCK_MONOTONIC。

  2. sleep() 与 usleep() 的实际精度。POSIX 只保证"至少睡这么久",实际会因调度延迟而更久。要精确等待用 clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, ...) 指定绝对时间点,避免累积误差。

  3. 假设 gettimeofday 单调。CLOCK_REALTIME 会被校时修改,多次调用可能得到"倒退"的结果。

  4. 在容器里读时间。容器默认共享宿主 CLOCK_REALTIME,但 CLOCK_MONOTONIC 也来自宿主内核。容器内改时间(除非有 CAP_SYS_TIME 且未隔离)会影响宿主。

  5. 忽视 jiffies 回绕。写内核模块时用 time_after 系列宏,不要直接比较。

6.3 观测工具

# 时钟源与当前选择
cat /sys/devices/system/clocksource/clocksource0/current_clocksource

# 内核时间偏移统计
cat /proc/timer_list | grep -A5 'tick'
grep -E 'clocksource|tsc' /proc/clocksource 2>/dev/null || dmesg | grep -i clocksource

# NTP 同步状态
chronyc tracking            # 偏差、频率、是否同步
timedatectl status          # 系统时间、NTP 开关

# 测量调度延迟
cyclictest -m -p 80 -i 1000 -l 100000   # 实时场景的标准工具

cyclictest 的 max 值直接反映系统在最坏情况下的唤醒抖动,是实时内核调优的核心指标。


结语

时间子系统的精髓是"分层":硬件提供单调计数器(clocksource)与可编程中断(clockevent),内核用 jiffies 提供粗粒度节拍、用 hrtimer 提供纳秒精度、用 timekeeper 维护三类时间语义,用户态再通过 vDSO 与 clock_gettime 无锁读取。理解这套分层,才能在"时间不准"“定时器不精确"“延迟抖动大"这三类问题前快速定位到正确的层。

一个实用的记忆法:测耗时用 MONOTONIC,报时间用 REALTIME,要确定性看 NO_HZ_FULL 与 cyclictest。这三句话覆盖了 90% 的日常场景。


延伸阅读

  1. Linux Kernel Documentation: Documentation/core-api/timekeeping.rst 与 Documentation/timers/
  2. Linux 内核源码:kernel/time/timekeeping.c、kernel/time/hrtimer.c、kernel/time/clocksource.c
  3. Linux 内核源码:kernel/time/tick-sched.c(tickless 实现)与 kernel/time/ntp.c
  4. PTP 与硬件时间戳:Documentation/networking/timestamping.rst、linuxptp 项目文档
  5. 《Linux Kernel Development》第 11 章定时器与时间管理(Robert Love)

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「os」更多文章

  1. 内核调试:kgdb、kdump、crash 与动态追踪
  2. cgroups v2 与命名空间底层实现与资源隔离
  3. Linux 设备驱动模型:字符设备、块设备与 sysfs