连接超时、合盖后凭据过期、跨机器日志排序,需要的不是同一条时间线。
| 查阅目标 | 阅读位置 |
| 选时钟与计数换算 | 第 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 还受日历突跳影响。到期条件成立和回调已经执行是不同状态。
{"id":"clock-deadline-semantics","title":"同一个五秒期限,休眠和校时改变谁?","summary":"MONOTONIC 不累计 suspend,BOOTTIME 累计;REALTIME 还受日历突跳影响。到期条件成立和回调已经执行是不同状态。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"awake\" data-i18n=\"awake\"></button><button id=\"suspend\" data-i18n=\"suspend\"></button></div><div class=\"control\"><label for=\"step\" data-i18n=\"step\"></label><output for=\"step\" id=\"step-value\"></output><input id=\"step\" type=\"range\" min=\"-10\" max=\"10\" value=\"-4\" step=\"1\"></div><div class=\"actions\"><button id=\"apply\" data-i18n=\"apply\"></button></div><div class=\"clock\"><header>MONOTONIC<strong id=\"now-M\"></strong></header><div class=\"deadline\"><i id=\"bar-M\"></i></div><div class=\"clock-result\"><span id=\"remaining-M\"></span><span id=\"status-M\"></span></div></div><div class=\"clock\"><header>BOOTTIME<strong id=\"now-B\"></strong></header><div class=\"deadline\"><i id=\"bar-B\"></i></div><div class=\"clock-result\"><span id=\"remaining-B\"></span><span id=\"status-B\"></span></div></div><div class=\"clock\"><header>REALTIME<strong id=\"now-R\"></strong></header><div class=\"deadline\"><i id=\"bar-R\"></i></div><div class=\"clock-result\"><span id=\"remaining-R\"></span><span id=\"status-R\"></span></div></div><div class=\"actions\"><button id=\"dispatch\" data-i18n=\"dispatch\"></button></div><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"note\"></p></div><div class=\"actions\"><button id=\"reset\" data-i18n=\"reset\"></button></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n.clock{margin:18px 0;padding:14px;border:1px solid var(--rule);border-radius:8px}.clock header{display:flex;justify-content:space-between;gap:8px;font:12px var(--viz-font-ui)}.clock header strong{font-size:16px}.deadline{height:20px;width:100%;position:relative;background:var(--surface);border-right:2px solid var(--second);margin:12px 0}.deadline i{height:100%;display:block;background:var(--accent);transition:width .25s}.clock-result{display:grid;grid-template-columns:1fr 1fr;gap:8px;min-height:5em;font-size:12px}.clock-result span:last-child{text-align:right;color:var(--muted)}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}let now=[0,0,1000],fired=[false,false,false];const due=[5,5,1005],keys=['M','B','R'];function draw(){set('step-value',$('#step').value+' s');keys.forEach((k,i)=>{set('now-'+k,now[i]+' s');set('remaining-'+k,t('remaining')+'\\n'+(due[i]-now[i]));set('status-'+k,t(fired[i]?'fired':now[i]>=due[i]?'due':'waiting'));$('#bar-'+k).style.width=Math.max(0,Math.min(100,(now[i]-(i===2?1000:0))/5*100))+'%';});$('#dispatch').disabled=!now.some((v,i)=>v>=due[i]&&!fired[i]);set('explanation',t('note'));}$('#awake').onclick=()=>{now=now.map(v=>v+2);draw();};$('#suspend').onclick=()=>{now[1]+=4;now[2]+=4;draw();};$('#apply').onclick=()=>{now[2]+=+$('#step').value;draw();};$('#dispatch').onclick=()=>{fired=fired.map((v,i)=>v||now[i]>=due[i]);draw();};$('#step').oninput=draw;$('#reset').onclick=()=>{now=[0,0,1000];fired=[false,false,false];draw();};draw();","audio":false,"strings":{"apply":"应用这次校时","awake":"运行 2 秒","dispatch":"处理已到期回调","due":"已到期,尚未处理","fired":"回调已执行","heading":"同一个五秒期限,休眠和校时改变谁?","intro":"三种时钟各设置一次“当前值 + 5 秒”的绝对期限。推进运行时间、经过休眠或调整日历时间,最后显式处理已经到期的回调。","note":"竖线固定代表原来的期限。进度只画到期限,数字保留带符号的剩余秒数;越过期限后仍需处理回调,不把系统休眠伪装成 CPU 在执行。","remaining":"距离原期限 / 秒","reset":"重置","scope":"示意时钟从 M=0、B=0、R=1000 秒开始,期限固定为 5、5、1005。休眠操作直接返回唤醒后的状态,不执行回调。省略 NTP 渐调、alarm 唤醒能力、TAI 和 timerfd CANCEL_ON_SET;已执行回调不会因回拨再次执行。","scopeLabel":"模型范围与约定","step":"日历时钟调整 / 秒","suspend":"休眠 4 秒后唤醒","waiting":"等待"}}
2. Timekeeping 怎样把计数变成时间
核心维护最近更新的计数与基准时间,读取新计数后计算经过量。简化的整数换算为:
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 和硬件读取开销。
关联阅读:时钟与定时器、同步机制。