时间管理:时间尺度、校正与读取

本页目录

连接超时、合盖后凭据过期、跨机器日志排序,需要的不是同一条时间线。

查阅目标阅读位置
选时钟与计数换算第 1~2 节
校正与时间尺度第 3~4 节
硬件同步、读取与观测后续章节

时间管理要回答不同问题:日志发生在什么日期,操作实际经过多久,系统睡眠是否计入超时,以及两台机器的时间戳能否比较。选择时钟应先回答这些语义问题,再考虑读取成本。

1. 按语义选择时钟

时钟时间线与校正suspend典型用途
CLOCK_REALTIME日历时间,可被 step 或渐进校正计入经过的休眠时间日志、人类日期、日历截止时间
CLOCK_MONOTONIC不受日历时钟突跳影响,但会受频率渐进调整不计入单机运行期间的超时与间隔
CLOCK_MONOTONIC_RAW不施加 NTP 等相应频率调整不计入分析计时器频率/校正影响
CLOCK_BOOTTIME类似 MONOTONIC,另外累计 suspend计入需要跨 suspend 到期的期限
CLOCK_TAI使用内核配置的 TAI 偏移等形成相应时间尺度计入正确同步和配置下的特定时间标记
PROCESS/THREAD_CPUTIME_ID累计相应 CPU 执行时间不把睡眠算作执行CPU 消耗分析

MONOTONIC 的单调性通常允许相邻读取相等,不保证每读一次一定严格增加。它并非“不受 NTP 调整”,更不代表计数器永远无频率误差。RAW 也不是跨机器可比较的 UTC,更不能无条件称为最精确业务时钟。API 语义见 clock_gettime。

两个期限例子

连接建立最多等 5 秒,通常需要 MONOTONIC 截止时间,避免管理员把日历时钟调回一分钟导致多等一分钟。若认证凭据在电脑合盖后仍应按经过时间过期,则应结合 BOOTTIME 或经过正确同步的绝对期限设计。

保存 MONOTONIC 数值到磁盘,重启后继续比较通常没有意义。将 A 机器的 MONOTONIC 与 B 机器的值相减同样不能得到网络时延。分布式日志需要日历时间、误差评估和因果标识等共同解释。

先推进清醒时间,再跨过一次休眠或跳变墙钟。下面把“时钟已经越过期限”与“回调得到执行”分开,避免把到期误读为当时已经运行。

正在呈现知识画面
同一个五秒期限,休眠和校时改变谁?

MONOTONIC 不累计 suspend,BOOTTIME 累计;REALTIME 还受日历突跳影响。到期条件成立和回调已经执行是不同状态。

2. Timekeeping 怎样把计数变成时间

同一经过时间,可以映射到不同时间尺度 Clocksource 计数差 频率、mask 与换算 Timekeeping 维护基准、校正与 suspend 等偏移 MONOTONIC 不倒退;不含 suspend BOOTTIME 在前者基础上计入 suspend REALTIME / TAI 日历/国际时间尺度 RAW 保留未施加相应频率校正的时间线;时间尺度与读取实现是两回事。

核心维护最近更新的计数与基准时间,读取新计数后计算经过量。简化的整数换算为:

delta_ns ≈ ((cycles_now - cycles_last) & mask) × mult >> shift

mask 处理有限宽度计数器回绕,mult/shift 减少昂贵计算,但真实实现还要避免中间溢出、处理不同基准和校正,不能把公式当成完整驱动代码。维护频率必须足以区分回绕,不能无限期不更新。

读取时间不必等下一个 tick:可由当前计数差推算基准之后经过多少。timekeeper 的实际结构随版本变化,维护它的更新路径也不等于“所有 hrtimer 中断都完整重算全部时钟”。底层职责见 timekeeping。

为什么需要一致性快照

基准、mult、shift 等字段要共同解释。若读到更新前的基准和更新后的换算参数,结果可能错误。因此读取路径使用序列验证等机制,发现并发更新就重试;它不是只把计数器读一次即可完成。

3. NTP:时间服务器、用户态控制与内核校正

chronyd/ntpd 等用户态服务处理网络时间源和估计,内核通过 adjtimex/clock_adjtime 等接口执行相应频率与相位调整。内核的 ntp.c 不是独立联网的“NTP daemon”。

校正方式时间线怎样变化对测量的影响
step直接改变日历时间REALTIME 可突跳,MONOTONIC 通常不随之倒退
slew调整速率,逐步消除误差MONOTONIC 会反映相应频率渐变

RAW 与 MONOTONIC 的长期差值变化可提供校正线索,仍需考虑测量噪声和时钟选择。

struct timex 的字段单位依 flags/mode 不同;例如频率使用规定的缩放单位,offset 的微秒/纳秒语义与状态有关。不能把所有 adjtimex 字段都注释成普通纳秒和 ppm。见 adjtimex。

同步状态与误差范围要一起读

同步服务报告同步,也不等于两机时间完全相同。网络路径不对称、时钟漂移、采样和硬件都会产生误差。对毫秒以下间隔做跨机器判断时,必须把误差范围纳入结论。

4. TAI、UTC 与闰秒

TAI 与 UTC 的偏移由历史闰秒决定,不应在长期知识文章里把某个数值写成永久常数。Linux CLOCK_TAI 还依赖系统设置了正确偏移;偏移未配置或错误时,调用成功也不代表返回可信 TAI。

TAI 时间尺度没有 UTC 闰秒插入的同类不连续处理,但内核时间校正和配置变更仍可能影响实际观测。使用 leap smear 的时间服务还会在窗口内有意偏离严格 UTC;混合不同 smear 策略的机器不能假设时间戳直接一致。

协议应明确时间尺度、单位和闰秒/校正策略,不能只传一个整数并命名为 timestamp。

5. PTP、PHC 与硬件时间戳

PHC 是设备上的 PTP Hardware Clock,Linux 可通过 /dev/ptpN 提供访问。网卡在更靠近收发硬件的位置打时间戳,可减少软件排队带来的不确定性;PTP 协议及用户态服务再使用这些测量同步相应时钟。

网卡 PHC 与系统 REALTIME 是不同的时钟对象,不会仅因安装了 ptp4l 就保证二者自动完全一致。phc2sys 等工具负责相应时钟之间的同步,配置需说明谁是参考、谁被调整。

SO_TIMESTAMPING 的软件、转换及原始硬件时间戳字段和 flags 必须按接口解释。纳秒存储单位不承诺纳秒同步准确度,实际还受网卡、交换机、链路、配置和温度等影响。见 PTP clock与网络时间戳。

6. vDSO 是快速读取路径,不是新的时间尺度

内核把 vDSO ELF 映射给用户进程,libc 可调用其中的时钟函数。配合内核发布的数据页和允许在用户态读取的计数源,常用时钟查询可避免每次陷入内核。

这仍然依赖内核维护的时间参数和一致性协议。目标架构、clocksource 或 clock ID 不受快速路径支持时,可能退回 syscall。strace 没看到 clock_gettime syscall,不表示程序没有读时间。

syscall 进入内核涉及特权级转换,不必然发生任务上下文切换。读取成本需测量,不能固定承诺 vDSO 20 ns、syscall 100 ns。x86 路径也不应伪写成调用 ARM 的 __arch_counter_get_cntvct()。见 vDSO。

7. 硬件能力不等于最终时间质量

TSC、HPET、ACPI PM timer、ARM Generic Timer 和虚拟化 paravirtual clock 具有不同访问、频率及稳定性属性。TSC 的 invariant 等能力有助于稳定计时,但跨 CPU 同步、固件、虚拟化和迁移仍需要内核判断,不能直接用 CPU 频率倒数作为测量准确度。

clocksource watchdog 和内核校验可发现某些不可靠来源并切换。强制选择一个“理论更快”的源可能降低可靠性;只读观察是理解当前配置的第一步。

from pathlib import Path
import time

root = Path('/sys/devices/system/clocksource/clocksource0')
for field in ('current_clocksource', 'available_clocksource'):
    try:
        print(field, (root / field).read_text().strip())
    except OSError as exc:
        print(field, type(exc).__name__)

names = ('CLOCK_REALTIME', 'CLOCK_MONOTONIC', 'CLOCK_MONOTONIC_RAW',
         'CLOCK_BOOTTIME', 'CLOCK_TAI')
for name in names:
    if not hasattr(time, name):
        continue
    clock = getattr(time, name)
    try:
        print(name, 'ns=', time.clock_gettime_ns(clock),
              'resolution_s=', time.clock_getres(clock))
    except OSError as exc:
        print(name, type(exc).__name__)

start = time.clock_gettime_ns(time.CLOCK_MONOTONIC)
time.sleep(0.02)
elapsed = time.clock_gettime_ns(time.CLOCK_MONOTONIC) - start
assert elapsed >= 20_000_000
print('observed sleep elapsed_ns=', elapsed)

这些时钟是先后读取的,不能直接把相邻打印值之差当成精确 offset;示例也不修改时钟、不触发 suspend。clock_getres() 给出分辨率信息,不证明当前 UTC 误差或实际唤醒抖动。

8. 时间测量的检查顺序

先确认比较的是同一时间尺度和同一机器/启动周期;再确认 suspend、校时和虚拟化暂停是否应计入。需要周期执行时检查绝对/相对截止与补跑规则,需要跨机器比较时确认同步误差,最后再分析 vDSO 和硬件读取开销。

关联阅读:时钟与定时器、同步机制。