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