“现在几点"看似是个简单问题,但在内核里它被拆成了至少三类互不相同的时间:墙上时钟(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_MONOTONICbase:不受校时影响。CLOCK_REALTIMEbase:受校时影响;当墙上时钟被调整时,内核会重新计算这些定时器的到期点(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 的精度受两个因素限制:
- clockevent 的硬件精度:若底层是 HPET,分辨率可能只有几十纳秒。
- 中断延迟与软中断处理:回调不是"精确到那一刻"执行,而是"到期后尽快执行"。
因此"定时器精度"和"定时器抖动(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 五个高频误区
用
CLOCK_REALTIME测量间隔。NTP step 会让差值为负或异常大。测耗时一律用CLOCK_MONOTONIC。sleep()与usleep()的实际精度。POSIX 只保证"至少睡这么久",实际会因调度延迟而更久。要精确等待用clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, ...)指定绝对时间点,避免累积误差。假设
gettimeofday单调。CLOCK_REALTIME会被校时修改,多次调用可能得到"倒退"的结果。在容器里读时间。容器默认共享宿主
CLOCK_REALTIME,但CLOCK_MONOTONIC也来自宿主内核。容器内改时间(除非有CAP_SYS_TIME且未隔离)会影响宿主。忽视 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% 的日常场景。
延伸阅读
- Linux Kernel Documentation:
Documentation/core-api/timekeeping.rst与Documentation/timers/ - Linux 内核源码:
kernel/time/timekeeping.c、kernel/time/hrtimer.c、kernel/time/clocksource.c - Linux 内核源码:
kernel/time/tick-sched.c(tickless 实现)与kernel/time/ntp.c - PTP 与硬件时间戳:
Documentation/networking/timestamping.rst、linuxptp 项目文档 - 《Linux Kernel Development》第 11 章定时器与时间管理(Robert Love)
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。