Linux时间系统1 --- 正确使用时间 REALTIME、MONOTONIC、BOOTTIME
在机器人软件里,很多时间相关故障并不是算法错误,也不是控制错误,而是时间语义错误。
典型现象包括:超时判断偶发失效、周期任务持续漂移、日志时间与内部耗时分析混在一起、传感器时间戳无法对齐。
第一阶段的目标是要先解决一个基础且重要的问题:
把 Linux 的时间轴用对,把用户态接口用对,把“时刻”和“时长”分开
本文只讨论三件事:
CLOCK_REALTIME / CLOCK_MONOTONIC / CLOCK_MONOTONIC_RAW / CLOCK_BOOTTIME / CLOCK_TAI的语义区别clock_gettime()、clock_getres()、nanosleep()、clock_nanosleep()、timerfd这些接口与不同 clock 的关系- 代码里什么时候该用绝对时间,什么时候该用单调时间
一. Linux 的时间区分
Linux 提供的是多条不同语义的时间线。时间相关代码首先要回答的是当前逻辑处理的是绝对时间,还是单调时间
两类问题必须先分开:
| 类别 | 关注点 | 用途 |
|---|---|---|
| 绝对时间 | 现实世界现在几点 | 日志、文件时间、事件发生时刻、对外协议时间 |
| 单调时间 | 从某个起点到现在过去多久 | timeout、deadline、耗时测量、周期控制、watchdog |
二. 五个clock
2.1 总览表
| Clock | 本质语义 | 是否可能跳变 | 适合 | 不适合 |
|---|---|---|---|---|
CLOCK_REALTIME | 墙上时间、日历时间 | 会 | 日志、记录现实时刻、对外展示 | timeout、耗时、周期控制 |
CLOCK_MONOTONIC | 单调递增时间轴 | 基本不倒退 | 超时、耗时、周期、deadline | 表示现实世界当前时刻 |
CLOCK_MONOTONIC_RAW | 更接近底层硬件原始节拍的单调时间 | 不倒退 | 漂移分析、jitter 分析、时钟行为研究 | 普通业务代码主时钟 |
CLOCK_BOOTTIME | 包含 suspend 的开机后经过时间 | 不倒退 | 跨 suspend 的定时/超时 | 日历时间表达 |
CLOCK_TAI | 连续原子时绝对时间轴 | 通常不按 UTC 方式跳秒 | 高精度绝对时间、未来对时体系 | 普通应用随手乱用 |
2.2 CLOCK_REALTIME:墙上时间
CLOCK_REALTIME 表示现实世界中的日期与时间,例如:
2026-04-19 10:30:00date命令看到的系统时间- 日志中给人阅读的时刻
它有两个关键特征:
- 可被人工修改
- 可被 NTP/PTP 等同步机制调整
这意味着它可能前跳,也可能后跳。因此:
- 适合:日志、文件时间、事件发生时刻、对外展示
- 不适合:超时判断、周期控制、耗时测量
CLOCK_REALTIME解决“现在几点”,不解决“过了多久”。
2.3 CLOCK_MONOTONIC:工程计时主力
CLOCK_MONOTONIC 是一条只向前走的单调时间线。
它通常是机器人软件内部最常用、最重要的 clock。
适用场景非常集中:
- 函数耗时统计
- 服务调用 timeout
- 控制周期 deadline
- 心跳监测
- watchdog
- 延迟测量
这些场景关心的是时间差,不是“当前几点”。
即便系统墙上时间被修改,MONOTONIC 的时间差语义仍然可靠。
只要问题是“持续了多久”“是否超时”“周期是否到点”,首选
CLOCK_MONOTONIC。
2.4 CLOCK_MONOTONIC_RAW:更原始的单调节拍
CLOCK_MONOTONIC_RAW 也是单调递增的,但它更接近底层硬件 clock source 的原始行为,尽量少受系统时间校正影响。
它的价值主要在分析层,而不是业务层:
- 观察底层漂移
- 对比时钟校正前后行为
- 研究 jitter
- 做更底层的性能测试
RAW 不是“比 MONOTONIC 更高级”,而是“更适合研究时钟本身”。
普通工程代码通常优先使用 MONOTONIC,只有在确实需要分析原始节拍行为时,才引入 RAW。
2.5 CLOCK_BOOTTIME:把 suspend 算进去
CLOCK_BOOTTIME 可以理解为:
从开机到现在真实经过了多久,包括系统 suspend(挂起) 的那段时间。
这和 MONOTONIC 的区别在于:某些语义下,MONOTONIC 不希望把 suspend 期间算作正常运行时间;而 BOOTTIME 明确希望计算进去。
适合场景:
- 跨 suspend 的 timeout
- 休眠唤醒后继续生效的定时逻辑
- 电源管理相关计时
- 移动设备、低功耗机器人平台
它解决的是一个语义问题: suspend 期间,到底算不算“时间经过”
2.6 CLOCK_TAI:连续原子时坐标
CLOCK_TAI 对应国际原子时(TAI)。
它和通常所说的 UTC/墙上时间最关键的差异在于:TAI 是连续的,不按 UTC 的闰秒表现来跳变。
第一阶段无需深入到同步系统,只需先建立这个认识:
- 它属于高精度绝对时间体系
- 它对未来的 PTP、PHC、硬件时间戳、多设备时间对齐非常重要
- 它不是普通程序默认应该随手使用的主时钟
三. 五个 clock 的速记版
CLOCK_REALTIME:现在几点CLOCK_MONOTONIC:过了多久CLOCK_MONOTONIC_RAW:更原始的底层节拍CLOCK_BOOTTIME:连 suspend 一起算CLOCK_TAI:连续原子时绝对时间轴
其中第一阶段最关键的只有一组:
REALTIME 管时刻,MONOTONIC 管时长。
四. 用户态接口与 clock 的对应关系
4.1 clock_gettime():读取指定时间轴当前值
clock_gettime() 从指定的 clock 读取当前时刻。
示例:
#include <time.h>
#include <stdio.h>
int main() {
struct timespec { //定义在<time.h>中
time_t tv_sec; // 秒:从纪元时间(1970-01-01 00:00:00 UTC)开始的秒数
long tv_nsec; // 纳秒:0 ~ 999,999,999 之间(不足1秒的部分)
};
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
printf("REALTIME : %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
clock_gettime(CLOCK_MONOTONIC, &ts);
printf("MONOTONIC : %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
clock_gettime(CLOCK_MONOTONIC_RAW, &ts);
printf("RAW : %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
clock_gettime(CLOCK_BOOTTIME, &ts);
printf("BOOTTIME : %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
clock_gettime(CLOCK_TAI, &ts);
printf("TAI : %ld.%09ld\n", ts.tv_sec, ts.tv_nsec);
}
运行结果:
REALTIME : 1741857221.312567124
MONOTONIC : 12536.789012789
RAW : 12536.789015678
BOOTTIME : 12536.789018901
TAI : 1741857258.312570123
- 不是“系统里有一个统一时间,
clock_gettime()把它读出来” - 而是“系统里有多条时间线,
clock_gettime()按 clock id 读取其中一条”
4.2 clock_getres():查看分辨率,不等于调度精度
clock_getres() 用来查看某个 clock 对外报告的最小粒度。
#include <time.h>
#include <stdio.h>
int main() {
struct timespec res;
clock_getres(CLOCK_MONOTONIC, &res);
printf("resolution = %ld.%09ld s\n", res.tv_sec, res.tv_nsec);
}
运行结果:最小精度是 1 纳秒
resolution = 0.000000001 s
需要明确三点:
- 分辨率 ≠ 线程唤醒精度
- 分辨率 ≠ 定时器触发误差
- 分辨率 ≠ 控制周期稳定度
实际唤醒精度还受调度器、系统负载、中断延迟、定时器实现、线程优先级等因素影响。
因此 clock_getres() 更适合做平台认知,而不是拿来直接评估实时性能。
4.3 nanosleep():相对睡眠,简单但容易漂移
nanosleep() :从当前时刻开始,再睡一段相对时间。
#include <time.h>
int nanosleep(const struct timespec *req, struct timespec *rem);
参数说明
1. const struct timespec *req
输入:你想休眠多久
struct timespec {
time_t tv_sec; // 休眠秒数
long tv_nsec; // 休眠纳秒数(0 ~ 999,999,999)
};
2. struct timespec *rem
输出:休眠被中断后,还剩多少时间没睡完
- 传
NULL表示不关心剩余时间 - 如果被信号中断,
rem会保存未休眠完的时间
例如:
while (running) {
do_work();
nanosleep(&(struct timespec){0, 10 * 1000 * 1000}, NULL); // 10ms
}
这看起来像 100 Hz 循环,但本质上并不是严格 100 Hz。
因为每轮总周期是:
do_work() 执行时间 + nanosleep() 设定时间 + 调度延迟
所以它的问题很明确:
- 工作耗时会叠加进总周期
- 调度误差会持续积累
- 长时间运行后节拍越来越漂
因此 nanosleep() 的定位应当是:
- 简单等待可用
- 低要求退避可用
- 严格周期控制不推荐默认使用
4.4 clock_nanosleep():指定 clock,支持绝对睡眠
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
高精度、绝对时间、阻塞睡眠
参数说明:
- CLOCK_MONOTONIC:使用单调时钟(最稳定)
- TIMER_ABSTIME:表示 next 是绝对时间(不是延时长度)
- &next:睡到这个时间点再继续
- NULL:不接收被信号中断的剩余时间
相比 nanosleep(),clock_nanosleep() 更完整,因为它支持:
- 明确指定基于哪条时间轴
- 选择绝对时间睡眠(
TIMER_ABSTIME)
这是周期控制中非常重要的接口。
示例:
#include <time.h>
static inline void add_ms(struct timespec* t, long ms) {
t->tv_nsec += ms * 1000000L; //把毫秒转成纳秒
while (t->tv_nsec >= 1000000000L) { //如果纳秒 ≥ 1 秒(10⁹),就需要进位到秒
t->tv_sec += 1;
t->tv_nsec -= 1000000000L;
}
}
int main() {
struct timespec next;
clock_gettime(CLOCK_MONOTONIC, &next);
while (1) {
add_ms(&next, 10); // 下一个 10ms 周期点
// do_work();
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
}
}
这段写法的关键点在于:
- 不是“从现在起再睡 10ms”
- 而是睡到下一个绝对周期点
这样做的好处是:
- 某一轮慢了一点,不会让后续周期无限累积漂移
- 周期逻辑围绕 deadline,而不是围绕“当前时刻再加一个睡眠量”
因此在机器人控制、定时采样、周期发布等场景中,推荐形成固定习惯:
周期任务优先考虑 CLOCK_MONOTONIC + clock_nanosleep(TIMER_ABSTIME)。
4.5 timerfd:把定时器纳入事件循环
Linux 专用:把定时器变成一个文件描述符,高精度、无漂移,可配合 epoll 监听,是Linux最优定时器方案。
timerfd 是内核硬件定时器,内核定时器是独立在后台跑的,不受用户态代码快慢影响
头文件
#include <sys/timerfd.h>
3个核心函数
1. 创建定时器
int fd = timerfd_create(CLOCK_MONOTONIC, 0);
- 作用:创建定时器,返回文件描述符
CLOCK_MONOTONIC:稳定单调时钟(首选)
2. 设置/启动定时器
计时起点 = timerfd_settime 成功返回的时刻,不是 main 入口,不是 timerfd_create。it_value 是从该起点到第一次定时器事件的等待时长
timerfd_settime(fd, 0, &new_val, NULL);
- 配置定时器:首次触发时间 + 循环周期
- 依赖结构体:
struct itimerspec {
struct timespec it_value; // 第一次触发时间
struct timespec it_interval;// 循环间隔(0=单次触发)
};
3. 读取触发
uint64_t count;
read(fd, &count, 8);
- 定时器到期 → fd可读
- 读取触发次数,否则会一直触发事件
核心特点
✅ 纳秒级精度 | 无时间漂移
✅ 支持 epoll 高并发
✅ 线程安全、稳定可靠
适合场景:
- 单线程事件驱动架构
- I/O 与定时器统一管理
- 状态机程序
- 网络轮询 + 定时检查
- 驱动守护逻辑
10ms 循环定时示例
#include <sys/timerfd.h>
#include <stdint.h>
int main() {
int fd = timerfd_create(CLOCK_MONOTONIC, 0);
// 10ms 循环
struct itimerspec it = {
.it_interval = {0, 10000000}, // 周期10ms
.it_value = {0, 10000000} // 首次10ms
};
timerfd_settime(fd, 0, &it, NULL); //启动定时器,计时起点
while(1) {
uint64_t c;
read(fd, &c, 8); //// 阻塞等待定时器到期,读取累计溢出次数
work(); // 定时任务
}
}
阶段 1:初始化
timerfd_create(CLOCK_MONOTONIC,0)- 内核创建 timerfd 上下文
timerfd_ctx,kernel_cnt = 0,初始化等待队列 wq,创建匿名 inode,返回 fd。 - ⚠️ 此时定时器还没启动,不计时
- 内核创建 timerfd 上下文
- 初始化
struct itimerspec it结构体,填入首次延时 10ms、循环周期 10ms。 timerfd_settime(fd,0,&it,NULL)- 内核拿到
it参数,配置 hrtimer 高精度定时器。 - 调用成功返回这一刻 = 计时起点,hrtimer 开始倒计时
it_value=10ms。 kernel_cnt保持 0。
- 内核拿到
- 进入
while(1)死循环。
阶段 2:第一轮循环,执行read(fd, &c,8)
- 用户态调用
read,陷入内核,进入timerfd_read回调 - 内核检查
timerfd_ctx->kernel_cnt→ kernel_cnt = 0(还没到期)- 把当前用户线程加入 timerfd 的等待队列 wq
- 线程状态置为可中断休眠,
schedule()让出 CPU - 用户态代码停在
read这里阻塞休眠,不再往下跑,不占用 CPU
线程休眠期间,内核 hrtimer 继续独立倒计时,和用户线程无关。
阶段 3:t=10ms,第一次定时器到期(硬件中断上下文)
- 硬件定时器触发中断,进入 hrtimer 中断处理函数
kernel_cnt++→kernel_cnt = 1- 调用
wake_up(&ctx->wq):扫描等待队列,发现我们的线程在休眠,唤醒该线程 - 判断
it_interval不为 0,内核 hrtimer 自动安排下一次 10ms 到期时间,继续倒计时 - 中断处理结束,返回。
阶段 4:线程被唤醒,继续完成 read 系统调用
- 线程被调度器重新上 CPU,从 wait_event 之后继续执行
timerfd_read - 将
kernel_cnt=1拷贝到用户态变量c - 内核清零 kernel_cnt → kernel_cnt = 0
- read 系统调用返回,返回值 = 8,回到用户代码。
- 用户态:
c=1
- 用户态:
阶段 5:执行定时任务 work ()
- 运行
work()业务代码。
分两种分支:
分支 A:work () 执行很快(<10ms,理想情况)
- work () 快速执行完毕,回到 while 循环开头,再次执行
read(fd, &c,8) - 再次进入内核,检查
kernel_cnt=0(下一个 10ms 还没到) - 线程再次进入等待队列休眠,卡在 read,等待下一次到期。
循环重复阶段 3~ 阶段 5,每次c=1。
分支 B:work () 执行很慢,耗时 25ms(任务超过周期)
承接上面:read 拿到 c=1,开始执行 work (),需要 25ms 才能跑完
- t=20ms:第二次定时器到期,触发中断
kernel_cnt++ → kernel_cnt=1- 尝试唤醒线程,但线程此刻正在 CPU 上跑 work (),不在等待队列,唤醒不生效;hrtimer 继续安排下一轮 10ms 倒计时
- t=30ms:第三次定时器到期,触发中断
- 线程此刻仍在 CPU 上跑 work (),唤醒不生效,
kernel_cnt++ → kernel_cnt=2 - 继续安排下一轮定时器
- 线程此刻仍在 CPU 上跑 work (),唤醒不生效,
- t=35ms:work () 终于跑完,回到循环,再次调用
read- 进入内核,
kernel_cnt=2 >0 - 拷贝数值到用户 c,c=2
- 内核清零 kernel_cnt=0
- read直接返回,不会阻塞
- 进入内核,
- 用户拿到 c=2,执行 work ()。
此时如果代码没有 for 循环补跑,只跑一次 work;如果写了 for (i< c),会连续跑两次 work,容易出现任务雪崩。
任务周期远大于设定的10ms,如果业务持续耗时 > 定时器周期,timerfd 这套模型会发生任务堆积,越欠越多,最后线程一直疯狂跑任务,永远不再阻塞,定时器失去 “周期性调度” 的效果
循环往复的规则总结
- hrtimer 在内核独立跑,不受用户线程 work 快慢影响,到点就中断、kernel_cnt+1
- read 两个作用:
- kernel_cnt=0:线程阻塞休眠,挂等待队列
- kernel_cnt>0:把计数拷贝到用户 c,内核 kernel_cnt 清零,read 返回
- 唤醒只发生在定时器中断里,wake_up 等待队列上休眠的线程。
- 只有 read 成功返回时,kernel_cnt 才清零;中断只会增加 kernel_cnt,不会清零。
五. 绝对时间与单调时间的使用时机
5.1 绝对时间:表示“现实世界哪个时刻”
绝对时间用于表示一个现实世界有意义的时间点,例如:
- 某张图像是在几点几分采集的
- 某条日志发生在什么时刻
- 某条消息带的系统时钟时间是多少
典型对应:
CLOCK_REALTIMECLOCK_TAI
关键词是:时刻、日期、当前几点、事件发生时间
5.2 单调时间:表示“已经过去多久”
单调时间用于表示时间差,例如:
- 两次采样间隔了多久
- 控制循环是否超时
- 算法运行了多少毫秒
- 距离 deadline 还剩多少时间
典型对应:
CLOCK_MONOTONICCLOCK_MONOTONIC_RAWCLOCK_BOOTTIME
关键词是:耗时、超时、间隔、周期、deadline
5.3 最重要的规则
凡是“测持续时间、做超时、跑周期”,优先单调时间;凡是“表示现实世界哪个时刻发生了什么”,才使用绝对时间。
六. 机器人开发中常见的时间语义错误
场景 1:服务调用超时
- 错误:用
CLOCK_REALTIME记录开始时刻和当前时刻做差 - 风险:系统校时后超时逻辑失真
- 正解:
CLOCK_MONOTONIC
场景 2:周期循环
- 错误:每轮执行结束后
nanosleep(固定时长) - 风险:执行时间叠加进周期,长期漂移
- 正解:
CLOCK_MONOTONIC + TIMER_ABSTIME
场景 3:图像采集时间戳
- “这张图是什么时候采的”是绝对时间问题
- “这张图距离上一张图隔了多久”是单调时间问题
- 两种语义不能混用
场景 4:算法耗时统计
- 正解:
CLOCK_MONOTONIC - 进一步分析底层节拍行为时,再考虑
CLOCK_MONOTONIC_RAW
场景 5:watchdog / 心跳监测
- 关心的是“距离上次事件过去多久”
- 本质是单调时间问题,不是现实时间问题
场景 6:suspend 后 timeout 是否继续生效
- 如果 suspend 期间也算时间经过,考虑
CLOCK_BOOTTIME - 如果不算,通常使用
CLOCK_MONOTONIC
场景 7:日志时间和内部耗时共用同一套时间戳
- 日志、展示:
CLOCK_REALTIME - timeout、耗时、周期:
CLOCK_MONOTONIC - 两套时间并存是正常设计,不是冗余
场景 8:代码里只有 timestamp_ns
下面这种字段设计风险很高:
uint64_t timestamp_ns;
因为它没有说明时间轴。更合理的写法是:
uint64_t realtime_ns;
uint64_t monotonic_ns;
uint64_t tai_ns;
uint64_t device_timestamp_ns;
时间戳有了但语义不明非常危险。
七. Linux时间选型的工程规则
规则 1
测耗时、做 timeout、做周期,优先 CLOCK_MONOTONIC。
规则 2
日志、文件时间、对外展示时间,优先 CLOCK_REALTIME。
规则 3
长期周期任务优先使用绝对 deadline,而不是“干完再睡固定时长”。
规则 4
代码中凡是出现 timestamp,必须明确它属于哪条时间轴。
规则 5
clock_getres() 的分辨率不能等同于线程定时精度。
规则 6
第一阶段先把 REALTIME 和 MONOTONIC 用对,再进入 RAW、TAI、时间同步。
选型速查表
| 需求 | 推荐选择 | 原因 |
|---|---|---|
| 打日志、记录现实时刻 | CLOCK_REALTIME + clock_gettime() | 需要现实世界时间 |
| 统计函数耗时 | CLOCK_MONOTONIC + clock_gettime() | 关心时间差 |
| 判断服务是否超时 | CLOCK_MONOTONIC | 避免墙钟跳变影响 |
| 周期控制 | CLOCK_MONOTONIC + clock_nanosleep(TIMER_ABSTIME) | 避免累计漂移 |
| 简单等待 | nanosleep() | 简单但不适合严谨周期 |
| 事件驱动定时器 | timerfd_create() + timerfd_settime() | 方便接入 epoll |
| 分析漂移/jitter | CLOCK_MONOTONIC_RAW | 更接近原始硬件节拍 |
| 跨 suspend 的超时 | CLOCK_BOOTTIME | suspend 期间也计时 |
| 高精度绝对时间体系 | CLOCK_TAI | 适合后续对时体系 |
总结
Linux 时间系统的第一阶段,重点不是同步,而是时间语义。
只要先把下面两件事彻底分开,后续很多问题都会简单很多:
- “这是现实世界哪个时刻”
- “这段逻辑已经过去多久”
时刻问题用绝对时间,时长问题用单调时间;日志看
REALTIME,控制看MONOTONIC。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)