1、背景
1.1、原子时(TAI)
时间测量最精确的基准来自于原子物理学。1967 年,国际计量大会定义秒的长度为:铯-133 原子基态的两个超精细能级间跃迁辐射振荡 9,192,631,770 周所持续的时间。基于这个定义,全球数十个实验室的原子钟共同加权平均,形成了 国际原子时(TAI)。
- 特点:绝对稳定,永不跳变,没有闰秒
- 局限:地球自转在缓慢变慢(由于潮汐摩擦等),TAI 与基于地球自转的天文时间(UT1)会逐渐偏离。每过约 500 天,TAI 会比 UT1 快大约 1 秒
1.2、协调世界时(UTC)
为了既利用原子时的精准,又让时钟与日出日落大致吻合,国际电信联盟引入了 协调世界时(UTC),其特点如下:
- 秒长:与 TAI 完全相同
- 校正:当 UTC 与 UT1 的偏差接近 0.9 秒时,会在 6 月 30 日或 12 月 31 日的末尾插入或删除 1 秒,即闰秒
- 现状:自 1972 年以来已添加 27 个正闰秒(最近一次是 2016 年底)。负闰秒从未发生过
重要理解:UTC 是一套标准规范,它不存储在任何一台普通电脑里。每台电脑保存的只是对 UTC 的一个本地副本,这个副本可以被校准、被修改、甚至被故意弄错。
1.3、时区与本地时间
地球自转造成时区划分。UTC 对应本初子午线(0° 经线)的时间。其他地方的时间 = UTC + 偏移量(东为正,西为负)。例如:北京时间(中国标准时间):UTC+8;纽约时间(美国东部标准时间):UTC-5(冬令时) / UTC-4(夏令时)。Linux 中的时区数据库(IANA 时区数据库)位于 /usr/share/zoneinfo/,包含全球约 400 个时区。系统当前使用的时区由 /etc/localtime 软链接指向某个文件。
2、Linux 硬件时钟与系统时钟:两个独立的时间源
Linux 维护两个完全不同的时钟:硬件时钟(RTC) 和 系统时钟。
2.1、硬件时钟(Real-Time Clock, RTC)
- 物理存在:主板上的独立芯片,由 CMOS 电池供电(通常是一颗 CR2032)
- 特性:即使电脑完全断电、拔掉电源,它依然在计时。电池耗尽才会丢失时间
- 存储内容:通常是"年月日时分秒"格式,可以存储为 UTC 或本地时间(通过 BIOS/UEFI 设置)
- 精度:较低,晶振受温度、老化影响,每天可能漂移几秒甚至几十秒
- 访问:通过 hwclock 命令或 /dev/rtc 设备
- 作用:在系统关机时"记住"时间,以便下次开机时提供一个初始参考
2.2、系统时钟(System Clock)
- 物理存在:没有独立硬件。它是 Linux 内核在内存中维护的一个软件时钟
- 特性:仅在操作系统运行时存在。断电或关机后,系统时钟的数据全部丢失
- 时间来源:开机时,内核从硬件时钟读取一次初始值,设为 CLOCK_REALTIME 的起点。之后,内核完全依赖 CPU 的时间戳计数器(TSC)、高精度事件定时器(HPET)或 ACPI 定时器等硬件计数器来驱动时钟的递增,不再读取 RTC
- 精度:极高(纳秒级),因为直接基于 CPU 高频振荡器
- 提供的时钟类型:CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_BOOTTIME、CLOCK_PROCESS_CPUTIME_ID 等,都来自系统时钟的不同视角
- 访问:date、clock_gettime()等
2.3、 两者的交互
- 开机:内核调用 hwclock --hctosys 将硬件时钟读入系统时钟
- 运行中:系统时钟独立运行,不再与硬件时钟同步
- 关机 / 重启:通常内核会自动将系统时钟写回硬件时钟(hwclock --systohc),保证下次开机时间不会跳变
bash
# 手动同步时钟指令如下
# 硬件时钟 -> 系统时钟
sudo hwclock --hctosys
# 系统时钟 -> 硬件时钟
sudo hwclock --systohc
3、clock_gettime函数
在 Linux 编程中,clock_gettime(clockid_t clk_id, struct timespec *tp) 是获取时间的核心函数。clk_id 决定你看到的是时间的哪一面
3.1、CLOCK_REALTIME ------ 挂钟时间(可调,UTC 副本)
系统当前对 UTC 的本地副本。它表示自 1970-01-01 00:00:00 +0000 (UTC) 以来经过的秒数(不包括闰秒)。挂钟时间的特性如下:
- 可以被修改:任何具有 CAP_SYS_TIME 权限的进程(如 root)都可以通过 settimeofday() 或 clock_settime() 改变它的值
- 会跳跃:当管理员手动 date -s 或 NTP 进行大幅步进校准时,REALTIME 会突然向前或向后跳
- 不单调:因为会跳,所以不适合测量时间间隔
它是不包含时区的,CLOCK_REALTIME 返回的是裸 UTC 秒数,要得到本地时间的年月日时分秒,需要调用 localtime() 或 localtime_r() 进行转换。可以理解为UTC时间是一个全球标准,由多个时钟共同定义,而挂钟时间是,机器里的utc时间,这个时间如果和标准UTC一样的话,再结合时区偏移就是本地时间。它的使用场景如下: - 日志时间戳(需要人理解的事件发生时刻)
- 文件系统的 atime/mtime/ctime
- 绝对时间定时器(例如:在 2026‑12‑31 23:59:59 执行某操作)
- 网络协议中的时间(HTTP Date 头、TLS 证书有效期)
c
#include <time.h>
#include <stdio.h>
int main() {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
printf("UTC seconds since 1970: %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
// 转换为本地时间字符串
time_t t = ts.tv_sec;
struct tm *local = localtime(&t);
printf("Local time: %04d-%02d-%02d %02d:%02d:%02d\n",
local->tm_year + 1900, local->tm_mon + 1, local->tm_mday,
local->tm_hour, local->tm_min, local->tm_sec);
return 0;
}
3.2、CLOCK_MONOTONIC ------ 单调时间(不含睡眠)
- 语义:从系统启动后开始计时的单调递增时间(具体起点不一定为 0,取决于实现)
- 特性:1>永不倒退:不受 CLOCK_REALTIME 调整的影响;1> 不包含系统休眠时间:当系统进入 S3(Suspend to RAM)睡眠状态时,MONOTONIC 停止计时;唤醒后继续;
- 典型用途:1> 测量代码执行耗时(最常用); 2> 相对时间超时(如 pthread_cond_timedwait、poll、select 的超时参数); 3>任何需要"从现在开始经过 X 秒"的场景
c
struct timespec start, end;
clock_gettime(CLOCK_MONOTONIC, &start);
// do some work...
clock_gettime(CLOCK_MONOTONIC, &end);
long elapsed_ns = (end.tv_sec - start.tv_sec) * 1000000000L
+ (end.tv_nsec - start.tv_nsec);
printf("Elapsed: %ld ns\n", elapsed_ns);
3.3、CLOCK_BOOTTIME ------ 单调时间(包含睡眠)
- 语义:与 MONOTONIC 类似,但包含系统挂起(Suspend)期间的时间
- 原理:系统在进入睡眠前会记录一个时间戳,唤醒后根据 RTC 流逝的时间进行补偿
- 典型用途:1> 定时器必须考虑设备可能休眠(例如:用户设置的"20 分钟后提醒",如果在此期间电脑合盖休眠 10 分钟,唤醒后应该还剩下 10 分钟); 2> systemd 中的某些看门狗和定时器
- 如何获取系统总睡眠时长:sleep_total = CLOCK_BOOTTIME - CLOCK_MONOTONIC
3.4、CLOCK_THREAD_CPUTIME_ID ------ 单线程 CPU 时间
- 语义:仅限当前调用线程消耗的 CPU 时间(用户态 + 内核态)
- 特性:1> 不包含同一进程中其他线程的时间 2> 线程睡眠、等待时停止增长
- 典型用途: 1> 细粒度测量某个特定线程的计算耗时
- 获取任意线程的 CPU 时间:使用 pthread_getcpuclockid(pthread_t thread, clockid_t *clk) 获得该线程专属的 clockid_t,然后 clock_gettime()
3.5、如何选择不同的时间
在编写 Linux 程序时,请根据语义选择最合适的时钟。问自己三个问题:
- 需要的是真实世界时刻还是时间间隔? → 前者用 REALTIME,后者用 MONOTONIC 或 BOOTTIME。
- 计时需要考虑 CPU 负载还是实际耗时? → 实际耗时用 MONOTONIC,CPU 负载用 CPUTIME。
- 程序可能在笔记本上休眠? → 若需经过睡眠的间隔,用 BOOTTIME。
4、常见问题
4.1、CLOCK_REALTIME 既然可以被修改,为什么 Linux 不把它设计成只读的?
因为系统必须能够校准时间(通过 NTP 或手动)。如果只读,那么一旦硬件时钟漂移,就永远无法修正。可修改是功能,不是缺陷。
4.2、闰秒会影响 CLOCK_REALTIME 吗?
不会。CLOCK_REALTIME 的秒数是 Unix 时间戳,它定义上忽略闰秒。当闰秒发生时,UTC 时间出现 23:59:60,但 Unix 时间戳只是从 n 跳到 n+1,没有额外的一秒。内核通过 CLOCK_TAI 提供真实的连续原子时。
4.3、gettimeofday() 和 clock_gettime(CLOCK_REALTIME) 有什么区别?
- gettimeofday() 提供微秒级精度,但可能受 settimeofday() 跳跃影响
- clock_gettime() 提供纳秒级精度,并且可以选择不同的时钟类型。新代码应使用 clock_gettime()
4.4、如何查看系统启动后过去了多少时间(不含休眠)?
cat /proc/uptime # 第一个数字是秒数(基于 CLOCK_MONOTONIC)
4.5、如何查看进程消耗的 CPU 时间?
bash
time ./myprogram # 用户态 + 内核态时间
getrusage(RUSAGE_SELF) # C 语言接口