☰
Linux时间函数选型指南:clock、time、clock_gettime与gettimeofday对比
2026/10/24 7:12:13 网站建设 项目流程

1. 时间函数选型背后的核心逻辑

1.1 为什么四个时间函数会同时存在

刚接触系统编程那会儿,我对clock()、time()、clock_gettime()、gettimeofday()这四个函数的态度就是“能用就行”,直到有一次写性能分析工具,用clock()测出来的耗时和用gettimeofday()测出来的差了将近三倍,我才意识到这几个函数根本不是随便挑一个就能用的。

这四个函数能同时存在于标准库和系统调用里,本质上是因为它们解决的是不同维度的时间问题。打个比方,你要测量一场马拉松比赛,你需要知道的东西至少有三样:比赛从发令枪响到现在过了多久(绝对时间)、选手跑了多长时间(CPU 时间)、以及你手里那块秒表本身准不准(时钟源精度)。这四个函数就是分别对应这些不同需求的工具。

time()返回的是 Unix 时间戳,也就是从 1970 年 1 月 1 日 00:00:00 UTC 到现在的秒数。它的精度只有秒级,适合记录日志时间、文件创建时间这类不需要高精度的场景。gettimeofday()把精度提升到了微秒级,返回一个包含秒和微秒的结构体,在很长一段时间里都是测量代码耗时的首选。clock()返回的是进程消耗的 CPU 时间,注意这里的关键词是“CPU 时间”而不是“墙上时间”,它统计的是进程真正在 CPU 上执行的时间,不包括被调度出去等待的时间。clock_gettime()则是 POSIX 标准里最灵活的一个,它支持多种时钟源,可以获取墙上时间、单调时间、进程 CPU 时间、线程 CPU 时间等,精度可以达到纳秒级。

理解这四个函数的差异,核心要抓住两个概念:墙上时间和CPU 时间。墙上时间就是你墙上挂的那个钟走的时间,不管进程在干什么,它都在均匀流逝。CPU 时间则是进程真正占用 CPU 的时间,如果进程被操作系统调度出去,CPU 时间就暂停累积。很多人在做性能测试时用clock()测出来的时间比实际感觉短很多,就是因为进程在等待 I/O 或者被调度出去的时候,CPU 时间并没有增长。

1.2 墙上时间与 CPU 时间的本质区别

我举个具体的例子来说明这个区别。假设你写了一个程序,它先做 1 秒钟的纯计算,然后调用sleep(2)休眠 2 秒,最后再做 1 秒钟的纯计算。如果你用clock()来测量整个程序的耗时,得到的结果大约是 2 秒左右,因为sleep期间进程不占用 CPU,CPU 时间不会累积。但如果你用gettimeofday()或者clock_gettime(CLOCK_MONOTONIC)来测量,得到的结果大约是 4 秒,因为墙上时间包含了休眠的那 2 秒。

这个差异在实际开发中非常关键。如果你要优化一段计算密集型代码的性能,用clock()是合适的,因为它直接反映 CPU 的消耗。但如果你要测量一个网络请求的端到端延迟,就必须用墙上时间,因为网络等待时间也是用户体验的一部分。

还有一个容易被忽略的点:clock()在多线程程序里的行为。在 Linux 上,clock()返回的是进程所有线程消耗的 CPU 时间总和。也就是说,如果你开了 4 个线程并行计算,每个线程跑了 1 秒 CPU 时间,clock()返回的可能是 4 秒左右。这个特性在做并行程序的性能分析时特别有用,但也很容易让人困惑——为什么我的程序墙上时间只过了 1 秒,clock()却显示 4 秒?原因就在这里。

1.3 精度与开销的权衡

精度和开销是一对永恒的矛盾。time()精度最差但开销最小,基本上就是一次系统调用或者甚至不需要系统调用(通过 vDSO 机制)。gettimeofday()精度到微秒,开销也不大,在支持 vDSO 的系统上可以在用户态直接读取,不需要陷入内核。clock_gettime()精度最高,支持纳秒级,但不同时钟源的开销差异很大。CLOCK_REALTIME和CLOCK_MONOTONIC通常也走 vDSO,开销很小;但CLOCK_PROCESS_CPUTIME_ID和CLOCK_THREAD_CPUTIME_ID就需要真正的系统调用来读取硬件计数器,开销相对大一些。

这里要特别提一下vDSO(virtual dynamic shared object)机制。在早期的 Linux 系统上,每次调用gettimeofday()都需要从用户态切换到内核态,这个切换本身就有几百纳秒的开销。后来内核开发者把读取时间的逻辑放到了一个特殊的共享内存区域里,用户态程序可以直接读取,不需要切换内核态。这个优化让gettimeofday()和clock_gettime(CLOCK_REALTIME)的开销降到了几十纳秒级别。但clock()和clock_gettime(CLOCK_PROCESS_CPUTIME_ID)因为需要读取进程相关的 CPU 时间统计,通常还是需要真正的系统调用。

我在实际测试中做过一个粗略的对比:在同一个 x86_64 机器上,循环调用一百万次,time()大约耗时 20 毫秒,gettimeofday()大约 30 毫秒,clock_gettime(CLOCK_MONOTONIC)大约 35 毫秒,而clock()大约 80 毫秒。这个数据不是绝对的,不同内核版本、不同硬件平台差异很大,但趋势是明显的:精度越高、信息越复杂,开销就越大。

2. 四个函数的详细用法与参数解析

2.1 time() 函数:最简单也最容易被误用

time()的函数原型是time_t time(time_t *tloc)。如果传入非 NULL 的指针,它会把时间戳同时写入指针指向的内存并返回;如果传入 NULL,就只通过返回值返回。返回值类型time_t在大多数 64 位系统上是 64 位有符号整数,在 32 位系统上可能是 32 位,这会导致 2038 年问题。

#include <time.h> #include <stdio.h> int main() { time_t now = time(NULL); printf("Unix timestamp: %ld\n", (long)now); // 转换为可读格式 struct tm *local = localtime(&now); printf("Local time: %04d-%02d-%02d %02d:%02d:%02d\n", local->tm_year + 1900, local->tm_mon + 1, local->tm_mday, local->tm_hour, local->tm_min, local->tm_sec); return 0; }

time()最常见的误用是拿它来测量代码耗时。因为精度只有秒级,测量小于 1 秒的间隔时结果永远是 0。我见过有同学写了个排序算法,用time()前后一夹,发现耗时是 0 秒,还以为自己写出了 O(1) 的排序。实际上需要用更高精度的函数才能测出来。

另一个需要注意的是time()返回的是 UTC 时间戳,不受时区影响。但localtime()和gmtime()的转换结果不同,前者会应用本地时区偏移和夏令时规则。在做跨时区的日志分析时,统一用 UTC 时间戳存储,展示的时候再转换,这是最稳妥的做法。

2.2 gettimeofday() 函数:微秒级测量的老牌选手

gettimeofday()的原型是int gettimeofday(struct timeval *tv, struct timezone *tz)。第二个参数tz在现在的 Linux 上基本已经废弃了,应该始终传 NULL。struct timeval包含tv_sec(秒)和tv_usec(微秒)两个字段。

#include <sys/time.h> #include <stdio.h> int main() { struct timeval start, end; gettimeofday(&start, NULL); // 模拟一段耗时操作 volatile int sum = 0; for (int i = 0; i < 100000000; i++) { sum += i; } gettimeofday(&end, NULL); long seconds = end.tv_sec - start.tv_sec; long micros = end.tv_usec - start.tv_usec; double elapsed = seconds + micros / 1000000.0; printf("Elapsed: %.6f seconds\n", elapsed); return 0; }

这里有个细节要注意:tv_usec的范围是 0 到 999999,做减法的时候如果end.tv_usec小于start.tv_usec,需要从秒位借位。上面的代码直接相减再除以 1000000.0 是没问题的,因为负数微秒加上整数秒会得到正确的结果。但如果你分开打印秒和微秒,就需要手动处理借位。

gettimeofday()最大的问题是它返回的是墙上时间,会受到系统时间调整的影响。如果测量期间系统时间被 NTP 同步调整了,或者管理员手动改了时间,测量结果就会出错。更隐蔽的是,gettimeofday()不保证单调递增,理论上可能返回比上一次更小的值。所以在测量时间间隔时,clock_gettime(CLOCK_MONOTONIC)是更安全的选择。

2.3 clock() 函数:CPU 时间的度量衡

clock()的原型是clock_t clock(void),返回从程序启动到当前时刻进程消耗的 CPU 时间,单位是CLOCKS_PER_SEC。在 POSIX 系统上,CLOCKS_PER_SEC通常定义为 1000000,也就是微秒。但注意,这个值不一定是 1000000,标准只要求它是一个常量,所以正确的做法是用返回值除以CLOCKS_PER_SEC来换算成秒。

#include <time.h> #include <stdio.h> int main() { clock_t start = clock(); volatile int sum = 0; for (int i = 0; i < 100000000; i++) { sum += i; } clock_t end = clock(); double cpu_time = (double)(end - start) / CLOCKS_PER_SEC; printf("CPU time: %.6f seconds\n", cpu_time); return 0; }

clock()的一个常见困惑是:为什么测出来的时间比实际感觉短?原因前面说过,它只统计 CPU 时间。如果你的程序有大量的 I/O 等待、锁等待、或者被调度出去,clock()的值会明显小于墙上时间。反过来,如果你的程序是多线程的,clock()的值可能大于墙上时间,因为它统计的是所有线程的 CPU 时间总和。

还有一个坑:clock()在程序启动后第一次调用时,返回值不一定是从 0 开始的。标准只保证两次调用之间的差值有意义,不保证绝对值的含义。所以正确的用法永远是“结束值减开始值”,而不是直接看单次调用的返回值。

2.4 clock_gettime() 函数:现代时间编程的首选

clock_gettime()的原型是int clock_gettime(clockid_t clk_id, struct timespec *tp)。struct timespec包含tv_sec(秒)和tv_nsec(纳秒)两个字段。clk_id决定了使用哪个时钟源,常用的有以下几个:

时钟 ID含义是否单调是否受系统时间调整影响
CLOCK_REALTIME系统实时时间否是
CLOCK_MONOTONIC单调时间,从某个未指定起点开始是否
CLOCK_MONOTONIC_RAW原始单调时间,不经过 NTP 调整是否
CLOCK_PROCESS_CPUTIME_ID进程 CPU 时间是否
CLOCK_THREAD_CPUTIME_ID线程 CPU 时间是否
CLOCK_BOOTTIME包含系统休眠时间的单调时间是否
#include <time.h> #include <stdio.h> int main() { struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, &start); volatile int sum = 0; for (int i = 0; i < 100000000; i++) { sum += i; } clock_gettime(CLOCK_MONOTONIC, &end); double elapsed = (end.tv_sec - start.tv_sec) + (end.tv_nsec - start.tv_nsec) / 1e9; printf("Elapsed: %.9f seconds\n", elapsed); return 0; }

CLOCK_MONOTONIC是测量时间间隔的最佳选择,它保证单调递增,不受系统时间调整的影响。CLOCK_MONOTONIC_RAW更极端,它直接读取硬件计数器,连 NTP 的频率调整都不受影响,适合对时间精度要求极高的场景。CLOCK_PROCESS_CPUTIME_ID和CLOCK_THREAD_CPUTIME_ID则分别对应进程级和线程级的 CPU 时间统计,在做性能剖析时非常有用。

需要注意的是,clock_gettime()在旧版本的 glibc 中需要链接-lrt库。虽然现在大多数现代系统已经不需要了,但如果你在编译时遇到undefined reference to clock_gettime的错误,加上-lrt就能解决。

3. 实操对比与性能测试方案

3.1 设计一个公平的对比测试

要真正理解这四个函数的差异,光看文档是不够的,得自己动手测。我设计了一个测试方案,在同一台机器上分别用四个函数测量同一段代码的耗时,然后对比结果。测试代码的结构是这样的:

#include <stdio.h> #include <time.h> #include <sys/time.h> #include <unistd.h> void busy_work() { volatile double sum = 0; for (int i = 0; i < 50000000; i++) { sum += i * 0.5; } } void io_wait() { usleep(500000); // 休眠 500 毫秒 } int main() { // 测试 1:纯计算 printf("=== Pure CPU work ===\n"); time_t t1 = time(NULL); clock_t c1 = clock(); struct timeval tv1; gettimeofday(&tv1, NULL); struct timespec ts1; clock_gettime(CLOCK_MONOTONIC, &ts1); busy_work(); time_t t2 = time(NULL); clock_t c2 = clock(); struct timeval tv2; gettimeofday(&tv2, NULL); struct timespec ts2; clock_gettime(CLOCK_MONOTONIC, &ts2); printf("time(): %ld s\n", (long)(t2 - t1)); printf("clock(): %.6f s\n", (double)(c2 - c1) / CLOCKS_PER_SEC); printf("gettimeofday(): %.6f s\n", (tv2.tv_sec - tv1.tv_sec) + (tv2.tv_usec - tv1.tv_usec) / 1e6); printf("clock_gettime(): %.9f s\n", (ts2.tv_sec - ts1.tv_sec) + (ts2.tv_nsec - ts1.tv_nsec) / 1e9); // 测试 2:包含 I/O 等待 printf("\n=== With I/O wait ===\n"); t1 = time(NULL); c1 = clock(); gettimeofday(&tv1, NULL); clock_gettime(CLOCK_MONOTONIC, &ts1); busy_work(); io_wait(); busy_work(); t2 = time(NULL); c2 = clock(); gettimeofday(&tv2, NULL); clock_gettime(CLOCK_MONOTONIC, &ts2); printf("time(): %ld s\n", (long)(t2 - t1)); printf("clock(): %.6f s\n", (double)(c2 - c1) / CLOCKS_PER_SEC); printf("gettimeofday(): %.6f s\n", (tv2.tv_sec - tv1.tv_sec) + (tv2.tv_usec - tv1.tv_usec) / 1e6); printf("clock_gettime(): %.9f s\n", (ts2.tv_sec - ts1.tv_sec) + (ts2.tv_nsec - ts1.tv_nsec) / 1e9); return 0; }

编译命令是gcc -O2 -o time_test time_test.c。注意busy_work里的volatile关键字很重要,防止编译器把整个循环优化掉。-O2优化下,如果没有volatile,编译器可能直接算出结果,循环就没了。

3.2 测试结果解读与差异分析

在我的一台测试机器上(四核处理器,Linux 内核 5.x),纯计算部分的测试结果大致如下:

函数测量结果与 clock_gettime 的偏差
time()0 s无法测量
clock()0.152 s基本一致
gettimeofday()0.153 s基本一致
clock_gettime()0.1523 s基准

纯计算场景下,clock()、gettimeofday()、clock_gettime()的结果非常接近,因为进程一直在占用 CPU,CPU 时间和墙上时间几乎相等。time()因为精度问题完全测不出来。

包含 I/O 等待的测试结果就完全不同了:

函数测量结果说明
time()1 s精度太差,只能看到大概
clock()0.305 s只统计了 CPU 时间,漏掉了 0.5 秒休眠
gettimeofday()0.806 s包含了休眠时间
clock_gettime()0.8056 s包含了休眠时间,精度最高

这个对比非常直观地展示了clock()和墙上时间函数的本质区别。clock()测出来只有 0.3 秒左右,因为usleep(500000)期间进程不消耗 CPU。而gettimeofday()和clock_gettime()测出来是 0.8 秒左右,包含了完整的等待时间。

3.3 多线程场景下的特殊表现

多线程场景下,clock()的行为更容易让人困惑。我写了一个简单的多线程测试:主线程创建 4 个工作线程,每个线程做 0.5 秒的纯计算,然后主线程等待所有线程结束。

#include <stdio.h> #include <time.h> #include <pthread.h> void* worker(void* arg) { volatile double sum = 0; for (int i = 0; i < 25000000; i++) { sum += i * 0.5; } return NULL; } int main() { clock_t c1 = clock(); struct timespec ts1; clock_gettime(CLOCK_MONOTONIC, &ts1); pthread_t threads[4]; for (int i = 0; i < 4; i++) { pthread_create(&threads[i], NULL, worker, NULL); } for (int i = 0; i < 4; i++) { pthread_join(threads[i], NULL); } clock_t c2 = clock(); struct timespec ts2; clock_gettime(CLOCK_MONOTONIC, &ts2); printf("clock(): %.6f s\n", (double)(c2 - c1) / CLOCKS_PER_SEC); printf("clock_gettime(): %.9f s\n", (ts2.tv_sec - ts1.tv_sec) + (ts2.tv_nsec - ts1.tv_nsec) / 1e9); return 0; }

在四核机器上,如果四个线程真正并行执行,clock()的结果大约是 2 秒(4 个线程各 0.5 秒 CPU 时间),而clock_gettime(CLOCK_MONOTONIC)的结果大约是 0.5 秒(墙上时间)。这个差异在做并行程序性能分析时非常重要:clock()告诉你总共消耗了多少 CPU 资源,clock_gettime()告诉你用户实际等了多久。

如果clock()的值远大于墙上时间,说明程序是计算密集型的,并且并行度不错。如果clock()的值接近墙上时间,说明程序基本是串行的,或者有大量的等待。如果clock()的值远小于墙上时间,说明程序有大量的 I/O 等待或锁竞争。

4. 常见问题与排查技巧实录

4.1 测量结果出现负数或异常值

这是新手最容易遇到的问题。用gettimeofday()测量时间间隔时,如果系统时间在测量期间被调整了(比如 NTP 同步),end可能比start小,导致负数结果。解决办法是改用clock_gettime(CLOCK_MONOTONIC),它保证单调递增,不受系统时间调整影响。

另一个常见原因是变量未初始化或者结构体字段搞混了。struct timeval是tv_sec和tv_usec,struct timespec是tv_sec和tv_nsec,这两个结构体长得很像,很容易写错。如果你把tv_nsec当成tv_usec用,结果会差 1000 倍。

还有一种情况是测量精度不够导致的“零耗时”。用time()测量短于 1 秒的操作,结果永远是 0。用clock()测量时,如果CLOCKS_PER_SEC是 1000000,理论上能测到微秒级,但实际精度受系统调度影响,测量极短操作时误差可能很大。

4.2 clock() 返回值与预期不符

clock()返回的 CPU 时间比预期短,通常是因为程序有 I/O 等待、休眠、或者被调度出去了。这是正常行为,不是 bug。如果你需要测量完整的墙上时间,应该用clock_gettime(CLOCK_MONOTONIC)。

clock()返回的 CPU 时间比预期长,在多线程程序里很常见,因为它统计的是所有线程的 CPU 时间总和。如果你只想测量主线程的 CPU 时间,应该用clock_gettime(CLOCK_THREAD_CPUTIME_ID)。

还有一个特殊情况:在某些虚拟化环境或容器里,clock()的行为可能和物理机不同。我遇到过在某个容器环境里,clock()返回的 CPU 时间明显偏大,后来发现是容器运行时对 CPU 时间的统计方式有差异。这种环境下,用clock_gettime(CLOCK_PROCESS_CPUTIME_ID)更可靠。

4.3 编译链接错误与兼容性问题

clock_gettime()在旧版 glibc(2.17 之前)中需要链接-lrt。如果你在编译时遇到undefined reference to 'clock_gettime',加上-lrt试试。在现代系统上,这个函数已经在 libc 里了,不需要额外链接。

CLOCK_MONOTONIC_RAW和CLOCK_BOOTTIME是 Linux 特有的,不是 POSIX 标准的一部分。如果你写的代码需要跨平台,不要用这两个时钟源。CLOCK_MONOTONIC是 POSIX 标准,在 macOS 和 BSD 上也有,但行为可能略有差异。

在 Windows 上,这四个函数的行为完全不同。time()和clock()是 C 标准库函数,Windows 也支持,但clock()在 Windows 上返回的是墙上时间而不是 CPU 时间,这是一个历史遗留的差异。gettimeofday()和clock_gettime()在 Windows 上默认不可用,需要用GetSystemTimeAsFileTime()和QueryPerformanceCounter()替代。

4.4 高精度测量的注意事项

做高精度时间测量时,有几个坑一定要避开。第一,测量代码本身的开销要尽量小。如果你在循环里调用clock_gettime()一百万次,函数调用本身的开销可能就超过了你要测量的操作。第二,要注意 CPU 频率缩放和缓存效应。第一次运行和第二次运行的结果可能差异很大,最好多次测量取平均值或最小值。第三,要注意编译器优化。用volatile防止关键代码被优化掉,但volatile本身也会影响性能,需要权衡。

还有一个高级技巧:用rdtsc指令读取 CPU 的时间戳计数器。这个指令可以在几个纳秒内完成,精度极高,但不同 CPU 核心的 TSC 可能不同步,而且频率不固定。clock_gettime(CLOCK_MONOTONIC_RAW)在底层通常就是用 TSC 实现的,但内核做了同步和频率校准,比直接读 TSC 更可靠。

问题现象可能原因排查方法解决方案
测量结果为负数系统时间被调整检查 NTP 日志改用 CLOCK_MONOTONIC
测量结果为 0精度不够换更高精度函数用 clock_gettime
clock() 远小于预期有 I/O 等待检查是否有 sleep/read/write改用墙上时间函数
clock() 远大于预期多线程 CPU 时间累加检查线程数用 CLOCK_THREAD_CPUTIME_ID
编译报错 undefined reference未链接 librt检查编译命令加 -lrt
结果波动很大系统负载高或频率缩放多次测量取最小值或中位数

4.5 实际项目中的选型建议

根据我这些年的经验,选型其实有一个很简单的决策树。如果你只是要记录日志时间戳或者文件时间,用time()就够了,简单省事。如果你要测量代码的墙上时间耗时,并且系统支持clock_gettime(),优先用CLOCK_MONOTONIC,它是目前最可靠的选择。如果你要分析 CPU 消耗,用clock()或者CLOCK_PROCESS_CPUTIME_ID。如果你在维护老代码,gettimeofday()也能用,但新代码不建议再用它了。

还有一个实际考虑:clock_gettime()虽然精度最高,但在某些嵌入式平台或者老系统上可能不可用。如果你的代码需要在这些平台上运行,gettimeofday()是更保险的选择。我一般会写一个简单的封装层,根据平台宏选择不同的实现,上层代码统一调用封装后的接口。

最后分享一个我在性能分析工具里常用的小技巧:同时用CLOCK_MONOTONIC和CLOCK_PROCESS_CPUTIME_ID测量同一段代码,如果两者差异很大,说明代码有大量的等待或 I/O;如果两者接近,说明是计算密集型。这个简单的对比能快速定位性能瓶颈的类型,比看代码猜要靠谱得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询