资讯动态

Linux时间函数选型指南:clock、time、clock_gettime与gettimeofday对比

发布时间:2026/10/24 7:12:14 来源:尧图企业网站定制
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期间进程不占用 CPUCPU 时间不会累积。但如果你用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就需要真正的系统调用来读取硬件计数器开销相对大一些。这里要特别提一下vDSOvirtual 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()的结果非常接近因为进程一直在占用 CPUCPU 时间和墙上时间几乎相等。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_usecstruct 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()在旧版 glibc2.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_gettimeclock() 远小于预期有 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如果两者接近说明是计算密集型。这个简单的对比能快速定位性能瓶颈的类型比看代码猜要靠谱得多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑