在机器人软件里,很多时间相关故障并不是算法错误,也不是控制错误,而是时间语义错误。
典型现象包括:超时判断偶发失效、周期任务持续漂移、日志时间与内部耗时分析混在一起、传感器时间戳无法对齐。
第一阶段的目标是要先解决一个基础且重要的问题:
把 Linux 的时间轴用对,把用户态接口用对,把"时刻"和"时长"分开
本文只讨论三件事:
CLOCK_REALTIME / CLOCK_MONOTONIC / CLOCK_MONOTONIC_RAW / CLOCK_BOOTTIME / CLOCK_TAI的语义区别clock_gettime()、clock_getres()、nanosleep()、clock_nanosleep()、timerfd这些接口与不同 clock 的关系- 代码里什么时候该用绝对时间,什么时候该用单调时间
1. Linux 的时间区分
Linux 提供的不是单一"当前时间",而是多条不同语义的时间线。时间相关代码首先要回答的不是"调用哪个函数",而是下面这个问题:
当前逻辑处理的是"现实世界的时刻",还是"某段经过的时间"?
两类问题必须先分开:
| 类别 | 关注点 | 典型用途 |
|---|---|---|
| 绝对时间 | 现实世界现在几点 | 日志、文件时间、事件发生时刻、对外协议时间 |
| 单调时间 | 从某个起点到现在过去多久 | timeout、deadline、耗时测量、周期控制、watchdog |
如果这一步没有分清,后面的 API 选型几乎一定出错。
2. 五个核心 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
- 休眠唤醒后继续生效的定时逻辑
- 电源管理相关计时
- 移动设备、低功耗机器人平台
它解决的不是一个 API 细节问题,而是一个语义问题:
suspend 期间,到底算不算"时间经过"
2.6 CLOCK_TAI:连续原子时坐标
CLOCK_TAI 对应国际原子时(TAI)。
它和通常所说的 UTC/墙上时间最关键的差异在于:TAI 是连续的,不按 UTC 的闰秒表现来跳变。
第一阶段无需深入到同步系统,只需先建立这个认识:
- 它属于高精度绝对时间体系
- 它对未来的 PTP、PHC、硬件时间戳、多设备时间对齐非常重要
- 它不是普通程序默认应该随手使用的主时钟
3. 五个 clock 的速记版
如果只保留最核心的区分,可以压缩成下面五句:
CLOCK_REALTIME:现在几点CLOCK_MONOTONIC:过了多久CLOCK_MONOTONIC_RAW:更原始的底层节拍CLOCK_BOOTTIME:连 suspend 一起算CLOCK_TAI:连续原子时绝对时间轴
其中第一阶段最关键的只有一组:
REALTIME 管时刻,MONOTONIC 管时长。
4. 用户态接口与 clock 的对应关系
4.1 clock_gettime():读取指定时间轴当前值
clock_gettime() 的本质不是"获取系统时间",而是:
从指定的 clock 读取当前时刻。
示例:
c
#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);
}
运行结果:
bash
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 对外报告的最小粒度。
c
#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 纳秒
bash
resolution = 0.000000001 s
这个接口很容易被误解。需要明确三点:
- 分辨率 ≠ 线程唤醒精度
- 分辨率 ≠ 定时器触发误差
- 分辨率 ≠ 控制周期稳定度
实际唤醒精度还受调度器、系统负载、中断延迟、定时器实现、线程优先级等因素影响。
因此 clock_getres() 更适合做平台认知,而不是拿来直接评估实时性能。
4.3 nanosleep():相对睡眠,简单但容易漂移
nanosleep() 的语义是:
从当前时刻开始,再睡一段相对时间。
c
#include <time.h>
int nanosleep(const struct timespec *req, struct timespec *rem);
参数说明
1. const struct timespec *req
输入:你想休眠多久
c
struct timespec {
time_t tv_sec; // 休眠秒数
long tv_nsec; // 休眠纳秒数(0 ~ 999,999,999)
};
2. struct timespec *rem
输出:休眠被中断后,还剩多少时间没睡完
- 传
NULL表示不关心剩余时间 - 如果被信号中断,
rem会保存未休眠完的时间
例如:
c
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,支持绝对睡眠
c
clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next, NULL);
高精度、绝对时间、阻塞睡眠
参数说明:
- CLOCK_MONOTONIC:使用单调时钟(最稳定)
- TIMER_ABSTIME:表示 next 是绝对时间(不是延时长度)
- &next:睡到这个时间点再继续
- NULL:不接收被信号中断的剩余时间
相比 nanosleep(),clock_nanosleep() 更完整,因为它支持:
- 明确指定基于哪条时间轴
- 选择绝对时间睡眠(
TIMER_ABSTIME)
这是周期控制中非常重要的接口。
示例:
c
#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最优定时器方案。
头文件
c
#include <sys/timerfd.h>
3个核心函数(必记)
1. 创建定时器
c
int fd = timerfd_create(CLOCK_MONOTONIC, 0);
- 作用:创建定时器,返回文件描述符
CLOCK_MONOTONIC:稳定单调时钟(首选)
2. 设置/启动定时器
c
timerfd_settime(fd, 0, &new_val, NULL);
- 配置定时器:首次触发时间 + 循环周期
- 依赖结构体:
c
struct itimerspec {
struct timespec it_value; // 第一次触发时间
struct timespec it_interval;// 循环间隔(0=单次触发)
};
3. 读取触发
c
uint64_t count;
read(fd, &count, 8);
- 定时器到期 → fd可读
- 读取触发次数,否则会一直触发事件
核心特点
✅ 纳秒级精度 | 无时间漂移
✅ 支持 epoll 高并发
✅ 线程安全、稳定可靠
10ms 循环定时示例
c
#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);
// 定时任务
}
}
timerfd 的意义是:
把"定时触发"也变成一个文件描述符事件,从而可以和
epoll等机制统一处理。
适合场景:
- 单线程事件驱动架构
- I/O 与定时器统一管理
- 状态机程序
- 网络轮询 + 定时检查
- 驱动守护逻辑
5. 绝对时间与单调时间的使用时机
5.1 绝对时间:表示"现实世界哪个时刻"
绝对时间用于表示一个现实世界有意义的时间点,例如:
- 某张图像是在几点几分采集的
- 某条日志发生在什么时刻
- 某条消息带的系统时钟时间是多少
典型对应:
CLOCK_REALTIMECLOCK_TAI
关键词是:时刻、日期、当前几点、事件发生时间
5.2 单调时间:表示"已经过去多久"
单调时间用于表示时间差,例如:
- 两次采样间隔了多久
- 控制循环是否超时
- 算法运行了多少毫秒
- 距离 deadline 还剩多少时间
典型对应:
CLOCK_MONOTONICCLOCK_MONOTONIC_RAWCLOCK_BOOTTIME
关键词是:耗时、超时、间隔、周期、deadline
5.3 最重要的一条总规则
凡是"测持续时间、做超时、跑周期",优先单调时间;凡是"表示现实世界哪个时刻发生了什么",才使用绝对时间。
6. 机器人开发中最常见的时间语义错误
场景 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
下面这种字段设计风险很高:
cpp
uint64_t timestamp_ns;
因为它没有说明时间轴。更合理的写法是:
cpp
uint64_t realtime_ns;
uint64_t monotonic_ns;
uint64_t tai_ns;
uint64_t device_timestamp_ns;
时间戳最危险的不是没有,而是有了但语义不明。
7. Linux时间选型固定下来的工程规则
规则 1
测耗时、做 timeout、做周期,优先 CLOCK_MONOTONIC。
规则 2
日志、文件时间、对外展示时间,优先 CLOCK_REALTIME。
规则 3
长期周期任务优先使用绝对 deadline,而不是"干完再睡固定时长"。
规则 4
代码中凡是出现 timestamp,必须明确它属于哪条时间轴。
规则 5
clock_getres() 的分辨率不能等同于线程定时精度。
规则 6
第一阶段先把 REALTIME 和 MONOTONIC 用对,再进入 RAW、TAI、时间同步。
8. 选型速查表
| 需求 | 推荐选择 | 原因 |
|---|---|---|
| 打日志、记录现实时刻 | 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。