Unix 时间戳常被当作一个简单的整数:自 1970 年 1 月 1 日 00:00:00 UTC 起经过的秒数。真把它放进程序里,麻烦才出现:两个事件可能落在同一个时间戳上,系统时钟会倒退,两台服务器对“现在几点”各执一词,而一个能报告纳秒的时钟仍可能差上数毫秒。
这就是 Linux 的时间管理:一个看似枯燥的整数,背后是一串并不轻松的工程问题。
墙钟与单调时钟:先分清用途
设想程序开始下载一个文件,记录开始时间,下载完成后记录结束时间。
但下载进行到一半时,系统时钟被校正了。于是程序认为下载在开始之前就已经结束。
文件当然没有穿越时间。问题在于:你用了一个可以被调整的时钟去测量耗时。
Linux 为不同用途提供了不同的时钟,其中两个最有用的时钟是:
CLOCK_REALTIME表示当前的墙钟时间。CLOCK_MONOTONIC提供一个持续前进的时钟,用于测量经过的时间。
系统调整时间时,墙钟可能向前或向后跳变。单调时钟不会倒退,不过在 Linux 上它的速率仍然可以被调整。
在 Python 中也能看到同样的区分:
import time
print("Wall clock:", time.time())
print("Monotonic clock:", time.monotonic())
这些值的含义不同。time.time() 返回基于 Unix 纪元的时间;time.monotonic() 使用某个与时钟相关的参考点,因此它的原始值并不是日历时间戳。
要点: 回答“这件事发生在什么时候”,用墙钟;回答“这件事花了多久”,用单调时钟。
时钟漂移与 NTP 同步
计算机有时钟,并不意味着它总知道正确的时间。
硬件时钟会漂移:有的走得稍快,有的走得稍慢。两台机器运行足够久,它们的时钟就可能不一致。
这正是 NTP(Network Time Protocol,网络时间协议)的用武之地:它帮助计算机与外部时间源同步时钟。
但同步不是魔法。网络延迟、出错的时间源以及时钟校正都会影响结果。取决于配置,时间同步软件可能逐步调整时钟速率,也可能把时钟向前或向后跳变。
在典型的基于 systemd 的 Linux 发行版上,可以用下面的命令查看时间配置:
timedatectl status
输出会因发行版和配置而异,重点看系统时钟与时间同步状态。
要点: 时间戳只告诉你某个时钟当时报告了什么,并不自动告诉你这个读数离正确时间有多近。
本地日历时间不是固定秒数
假设你要让一个脚本每天 9 点运行。
最直观的做法似乎是每次运行后等待 86,400 秒(24 hours)。
但本地日历日并不总是包含 86,400 个流逝秒。夏令时切换会让某个本地日变成 23 或 25 hours 小时长。历史上,闰秒也让 UTC 与简单的秒数计数之间的关系变得复杂。
而且时区并不只是相对 UTC 的固定偏移。政府可以修改规则,软件需要最新的信息才能应对这些变化。
在 Linux 系统上试试这两条命令:
TZ=UTC date
TZ=Asia/Kolkata date
它们按指定时区显示当前的日期与时间。
对于一次性事件,Unix 时间戳可能正好够用。但考虑这样的需求:
每周一纽约时间 9 点运行这个脚本。
单靠一个时间戳无法表达这个意图。你需要一条重复规则、一个本地时间,以及一个时区标识符。
关于这些规则,可以查阅 IANA 时区数据库。
要点: “某个时间点”和“关于本地日历时间的规则”是两回事。
跨机器排序不能只看时间戳
事情到这里才真正变得有意思。
服务器 A 在 12:00:00.100 记录了一个事件。
服务器 B 在 12:00:00.080 记录了另一个事件。
哪个事件先发生?
你可能会因为服务器 B 的时间戳更小而认为它的事件更早。
但如果服务器 B 的时钟比服务器 A 的走得快呢?
这就触及了分布式系统的一个基本问题:仅凭时间戳无法可靠地确定跨机器事件的先后顺序。
时钟同步有帮助,但无法保证每台服务器都报告完全相同的时间。
分布式系统会用其他机制来推断事件顺序,视问题而定,可能包括序列号、逻辑时钟、数据库日志位置或协调协议。
还要记住:即使两个时钟都工作正常,两个事件也可能拥有同一个时间戳——它们可能只是落在同一个时钟分辨率之内。
要点: 不要假定时间戳最小的事件就一定最早发生。
用时间戳当主键的冲突
设想你在开发一个支付服务,决定用当前 Unix 时间戳作为每笔支付的主键。
起初一切正常。
然后两位客户在同一秒内完成支付。两个请求生成了相同的时间戳,都试图插入主键相同的行。
其中一次插入失败。
你把精度从秒换成毫秒,冲突概率降低,但仍可能发生。换成纳秒也不能保证唯一性。
问题未必在于精度不够,而在于你要求时钟提供一项它本就不是为此设计的保证。
更好的设计把“身份”与“时间”分开:
CREATE TABLE payments (
id UUID PRIMARY KEY,
created_at TIMESTAMPTZ NOT NULL,
amount NUMERIC NOT NULL
);
这个 PostgreSQL 风格的例子用 UUID 标识每笔支付,用时间戳记录创建时间。UUID 仍需由应用或数据库生成。
每个字段职责清晰。
要点: 时间戳描述某事何时发生,标识符则把它与其他事物区分开。
终端实验:date、timedatectl 与 Python
理论说够了,打开终端运行这些命令:
# What time does your system report?
date
# What time is it in UTC?
date -u
# What is the current Unix timestamp?
date +%s
# What does the system report about time synchronization?
timedatectl status
最后一条命令面向基于 systemd 的 Linux 系统。其余命令在类 Unix 系统上普遍可用,但选项可能因实现而异。
如果你安装了 Python 3,再运行:
import time
print("Wall clock:", time.time())
print("Monotonic clock:", time.monotonic())
print("Performance counter:", time.perf_counter())
这三个调用用途不同:
time.time()给出基于纪元(epoch)的墙钟时间。time.monotonic()适合测量经过的时间。time.perf_counter()用于测量短时性能。
示例输出:
Wall clock: 1792679694.0232277
Monotonic clock: 445.774622884
Performance counter: 445.774647309
单调时钟和性能计数器不提供普通的日历时间戳,不要拿它们的原始值与 Unix 时间戳比较。
底层 Linux 细节可参阅 clock_gettime() 手册。
真正复杂的地方
你从一个自 1970 起计数的整数出发,如今已经遇到不准确的时钟、时间同步、夏令时切换、分布式事件排序和数据库身份问题。
出人意料的不是时间管理本身有多复杂,而是如此多的软件依赖它,却把它当成已经解决的问题。
还有更多内容值得探索:闰秒、2038 年问题、时间戳精度,以及编程语言处理 Unix 纪元之前日期的微妙方式。




