Linux时间系统1 --- 正确使用时间

在机器人软件里,很多时间相关故障并不是算法错误,也不是控制错误,而是时间语义错误

典型现象包括:超时判断偶发失效、周期任务持续漂移、日志时间与内部耗时分析混在一起、传感器时间戳无法对齐。

第一阶段的目标是要先解决一个基础且重要的问题:

把 Linux 的时间轴用对,把用户态接口用对,把"时刻"和"时长"分开

本文只讨论三件事:

  1. CLOCK_REALTIME / CLOCK_MONOTONIC / CLOCK_MONOTONIC_RAW / CLOCK_BOOTTIME / CLOCK_TAI 的语义区别
  2. clock_gettime()clock_getres()nanosleep()clock_nanosleep()timerfd 这些接口与不同 clock 的关系
  3. 代码里什么时候该用绝对时间,什么时候该用单调时间

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:00
  • date 命令看到的系统时间
  • 日志中给人阅读的时刻

它有两个关键特征:

  • 可被人工修改
  • 可被 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_REALTIME
  • CLOCK_TAI

关键词是:时刻、日期、当前几点、事件发生时间


5.2 单调时间:表示"已经过去多久"

单调时间用于表示时间差,例如:

  • 两次采样间隔了多久
  • 控制循环是否超时
  • 算法运行了多少毫秒
  • 距离 deadline 还剩多少时间

典型对应:

  • CLOCK_MONOTONIC
  • CLOCK_MONOTONIC_RAW
  • CLOCK_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

第一阶段先把 REALTIMEMONOTONIC 用对,再进入 RAWTAI、时间同步。


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

相关推荐
星纬智联技术1 小时前
给 Amp 配置自定义 API:CLIProxyAPI 接入教程
运维·服务器·数据库
KK溜了溜了1 小时前
Prometheus配置监控项和告警规则
linux·grafana·prometheus
吴声子夜歌1 小时前
Java——泛型
java·开发语言·泛型
XiYang-DING1 小时前
【Java EE】 HTTP协议
java·http·java-ee
SoveTingღ1 小时前
【问题解析】Socket已经关闭了,但是端口还处于listening状态?
linux·服务器·c++·qt·socket
无限进步_1 小时前
【Linux】进程基础:task_struct、fork 与查看进程
linux·运维·服务器
小夏子_riotous1 小时前
Kubernetes学习路径——3. Kubernetes 1.25 高可用集群部署实战:从 Docker 到 Calico 全链路详解
linux·运维·学习·docker·容器·kubernetes·centos
bukeyiwanshui1 小时前
20260512 docker笔记
linux·运维·笔记·docker·容器
敖正炀1 小时前
JDBC 批处理内核:addBatch、executeBatch 与驱动 SQL 重写
java