4、ClockId与时钟操作:CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_PROCESS_CPUTIME_ID等时钟ID详解
时钟ID,说白了就是告诉系统「我要用哪个时钟」。QNX里提供了好几种时钟,各有各的脾气。我刚开始接触时也犯过迷糊——明明调了clock_gettime(),时间却不对。后来才明白,是选错了ClockId。
这一节,咱们就把几个最常用的时钟ID掰开揉碎讲清楚。
4.1 CLOCK_REALTIME:墙上时间
CLOCK_REALTIME 代表的是系统当前的「墙上时间」。也就是你手表上看到的时间,从1970年1月1日0点算起的秒数。
这个时钟有个特点:它可以被修改。系统管理员用 date 命令改时间,或者NTP同步时,这个时钟的值会跳变。你想想看,如果你的程序依赖这个时钟做超时判断,突然时间被往后调了10分钟,那你的 timer_settime() 可能就提前触发了。
代码示例:
#include <time.h>
#include <stdio.h>
int main() {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
printf("当前墙上时间: %ld.%09ld 秒\n", ts.tv_sec, ts.tv_nsec);
return 0;
}
4.2 CLOCK_MONOTONIC:单调递增时钟
这个时钟,是我在项目中用得最多的。它从系统启动开始计时,只增不减。不管你怎么改系统时间,它都自顾自地往前走。
为什么会这样?因为它的设计初衷就是「测量时间间隔」。你想想看,如果你用 CLOCK_REALTIME 测量一段代码的执行时间,中间NTP把时间往回调了1秒,那结果就变成负数了——这显然不合理。
我曾经在一个项目中,同事用 CLOCK_REALTIME 做帧率控制。结果半夜NTP同步后,画面卡了整整两秒。排查了半天,最后发现是时钟ID选错了。从那以后,我就在团队里立了个规矩:所有跟「间隔」相关的逻辑,一律用 CLOCK_MONOTONIC。
代码示例:
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("耗时: %ld 纳秒\n", elapsed_ns);
4.3 CLOCK_PROCESS_CPUTIME_ID:进程CPU时间
这个时钟有点特别。它不测量「真实时间」,而是测量「CPU花在这个进程上的时间」。说白了,就是你的代码在CPU上真正跑的时间,不包括被其他进程抢走的时间。
嗯,这里要注意:如果你的进程被调度出去了,这个时钟是停的。只有你的代码在CPU上执行时,它才往前走。
我建议在做性能分析时用这个时钟。比如你想知道某个函数到底消耗了多少CPU周期,用 CLOCK_PROCESS_CPUTIME_ID 就对了。它不受系统负载的影响,能真实反映代码本身的效率。
| 时钟ID | 特点 | 典型用途 | 是否可修改 |
|---|---|---|---|
| CLOCK_REALTIME | 墙上时间,可被NTP或用户修改 | 日志时间戳、用户界面显示 | 是 |
| CLOCK_MONOTONIC | 单调递增,从系统启动开始 | 超时判断、性能测量、周期性任务 | 否 |
| CLOCK_PROCESS_CPUTIME_ID | 进程消耗的CPU时间 | 代码性能分析、CPU占用统计 | 否 |
4.4 其他常用时钟ID
除了上面三个,QNX还提供了几个特殊的时钟ID,我简单提一下:
- CLOCK_THREAD_CPUTIME_ID:跟
CLOCK_PROCESS_CPUTIME_ID类似,但只统计当前线程的CPU时间。多线程调试时很有用。 - CLOCK_SOFTTIME:QNX特有的时钟。它只在CPU空闲时才会前进。嗯,这个用得不多,但在某些低功耗场景下会用到。
- CLOCK_UPTIME:跟
CLOCK_MONOTONIC类似,但不受系统休眠的影响。如果你的设备有休眠/唤醒功能,用这个更准确。
clock_getres() 查看每个时钟的精度。不同硬件平台,精度可能不一样。我习惯在初始化时打印一下,心里有个底。
4.5 时钟操作函数
知道了时钟ID,怎么用呢?QNX提供了几个核心函数:
clock_gettime():获取当前时间。最常用。clock_settime():设置时间。只有CLOCK_REALTIME能用,其他时钟会返回错误。clock_getres():获取时钟精度。比如返回1ms,说明这个时钟每1ms才更新一次。nanosleep():高精度睡眠。可以指定用哪个时钟来计时。
代码示例——用 nanosleep 实现精确延时:
struct timespec req, rem;
req.tv_sec = 0;
req.tv_nsec = 50000000; // 50ms
// 使用 CLOCK_MONOTONIC 来睡眠
// 这样即使系统时间被修改,也不会影响睡眠时长
if (nanosleep(&req, &rem) == -1) {
// 被信号中断,rem 中还有剩余时间
printf("剩余 %ld ns 未睡眠\n", rem.tv_nsec);
}
4.6 避坑指南
最后,分享几个我踩过的坑:
- 别用 CLOCK_REALTIME 做超时判断。 我曾经有个项目,NTP同步后所有定时器都乱了。后来全部改成
CLOCK_MONOTONIC,问题解决。 - 注意时钟精度。 有些硬件上
CLOCK_MONOTONIC的精度只有1ms。如果你需要微秒级精度,得先查一下。 - 多线程环境下用
CLOCK_THREAD_CPUTIME_ID。 如果你在A线程里获取CLOCK_PROCESS_CPUTIME_ID,得到的是整个进程的CPU时间,不是你当前线程的。
嗯,时钟ID的选择,看似简单,但用错了后果很严重。我个人建议,在项目初期就把时钟策略定下来,写到编码规范里。省得后面出问题再回头改,那代价就大了。