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() 可能就提前触发了。

注意: 我个人习惯,凡是跟「人」打交道的时间,比如日志时间戳、显示给用户看的时间,才用 CLOCK_REALTIME。跟「逻辑」打交道的,尽量别用它。

代码示例:

#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_MONOTONIC。这是QNX开发者的基本共识。

我曾经在一个项目中,同事用 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 类似,但不受系统休眠的影响。如果你的设备有休眠/唤醒功能,用这个更准确。
小技巧: 在QNX上,你可以用 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的选择,看似简单,但用错了后果很严重。我个人建议,在项目初期就把时钟策略定下来,写到编码规范里。省得后面出问题再回头改,那代价就大了。