3、时间度量工具入门:Linux time命令、gettimeofday()函数、clock_gettime()系统调用

聊到实时性分析,第一个绕不开的问题就是:时间怎么量?

你想想看,如果连时间都量不准,后面算响应时间、分析延迟,那都是空中楼阁。我刚开始做性能优化那会儿,就吃过这个亏——拿着一个工具测出来的数据折腾了两天,结果发现是工具本身精度不够,白费功夫。

所以这一章,咱们把 Linux 下最常用的三个时间度量工具摸透。说白了,就是三把尺子:一把粗的,一把细的,一把更细的。

3.1 time 命令:最粗的那把尺子

time 命令,应该是大家接触最早的一个。用法简单到不行:

$ time ls
real    0m0.003s
user    0m0.001s
sys     0m0.002s

它告诉你三件事:

  • real:墙上时钟时间,也就是从开始到结束,你掐表算出来的时间。
  • user:用户态 CPU 时间,程序自己在用户空间跑的时间。
  • sys:内核态 CPU 时间,程序调用系统调用、让内核帮忙干活的时间。

重点来了:real 不等于 user + sys。因为 real 包含了等待 I/O、被其他进程抢占、甚至 CPU 调度延迟的时间。我见过有人拿 real 当 CPU 时间用,结果分析出来的结论完全跑偏。

我个人习惯,time 只用来做粗筛。比如两个程序跑完,一个 real 0.1s,一个 real 10s,那肯定有问题。但要是想精确到毫秒甚至微秒级别,time 就不够用了。

小技巧:用 /usr/bin/time -v 可以输出更详细的信息,包括上下文切换次数、缺页次数等。我在排查一个高延迟服务时,就是靠这个发现进程被频繁换出,导致 real 时间远大于 CPU 时间。

3.2 gettimeofday():微秒级的好帮手

如果 time 是秒表,那 gettimeofday() 就是电子计时器。它能精确到微秒(µs)。

看个例子:

#include <sys/time.h>
#include <stdio.h>

int main() {
    struct timeval start, end;
    gettimeofday(&start, NULL);
    
    // 这里放你要测量的代码
    for (int i = 0; i < 1000000; i++);
    
    gettimeofday(&end, NULL);
    
    long seconds = end.tv_sec - start.tv_sec;
    long microseconds = end.tv_usec - start.tv_usec;
    double elapsed = seconds + microseconds / 1000000.0;
    
    printf("耗时: %f 秒\n", elapsed);
    return 0;
}

这里有个坑,我曾经踩过:tv_usec 是微秒部分,范围是 0 到 999999。如果你直接 end.tv_usec - start.tv_usec,万一跨秒了怎么办?比如 start 是 1.999999s,end 是 2.000001s,直接减会得到负数。

避坑指南:我曾经在线上环境用 gettimeofday 做性能统计,结果日志里偶尔出现负的时间差。排查了半天才发现是跨秒时没处理好。正确的做法是先算秒差,再算微秒差,或者直接用 timersub() 宏。

另外要注意,gettimeofday() 返回的是墙上时间,不是单调时间。如果系统时间被 NTP 调整了,或者管理员手动改了时间,那测量结果就会出问题。嗯,这一点在后面的章节还会细讲。

3.3 clock_gettime():精度之王

如果说 gettimeofday() 是微秒级,那 clock_gettime() 就是纳秒级——理论上能到 1ns 的精度。当然,实际精度取决于硬件,但至少它给了你一个纳秒级的数据结构。

用法如下:

#include <time.h>
#include <stdio.h>

int main() {
    struct timespec start, end;
    clock_gettime(CLOCK_MONOTONIC, &start);
    
    // 要测量的代码
    for (int i = 0; i < 1000000; i++);
    
    clock_gettime(CLOCK_MONOTONIC, &end);
    
    long seconds = end.tv_sec - start.tv_sec;
    long nanoseconds = end.tv_nsec - start.tv_nsec;
    double elapsed = seconds + nanoseconds / 1000000000.0;
    
    printf("耗时: %f 秒\n", elapsed);
    return 0;
}

这里我特意用了 CLOCK_MONOTONIC,而不是 CLOCK_REALTIME。为什么?

  • CLOCK_REALTIME:墙上时间,可能被 NTP 调整,会跳变。
  • CLOCK_MONOTONIC:单调递增时间,从系统启动开始算,不受时间调整影响。

我个人习惯,做性能测量一律用 CLOCK_MONOTONIC。你想想看,如果测量过程中 NTP 把时间往回拨了 1 秒,那你的耗时结果就变成负数了——这谁受得了?

核心建议:测量耗时用 CLOCK_MONOTONIC,获取当前时间戳用 CLOCK_REALTIME。两者分工不同,别混用。

3.4 三把尺子的对比

咱们用一张表总结一下:

工具/函数 精度 时间类型 适用场景
time 命令 毫秒级 墙上时间 + CPU 时间 粗筛、对比程序整体耗时
gettimeofday() 微秒级 墙上时间 函数级性能分析、日志打点
clock_gettime() 纳秒级 可指定(推荐 MONOTONIC) 高精度测量、内核/驱动开发

选哪个?我的经验是:

  • 如果只是看个大概,time 就够了。
  • 如果需要精确到微秒,用 gettimeofday(),但注意处理跨秒和 NTP 问题。
  • 如果追求极致精度,或者需要单调时间,直接上 clock_gettime()

额外提醒gettimeofday() 在 POSIX 标准中已经被标记为过时,虽然 Linux 上还能用,但新代码建议直接用 clock_gettime()。我最近重构一个老项目时,就把所有 gettimeofday 换成了 clock_gettime,顺便统一了时间源。

好了,这一章就到这里。下一章咱们聊聊更高级的时间度量——怎么用这些工具搭一个完整的测量框架,以及如何避免那些常见的测量陷阱。