1. 四种时间函数的整体设计思路与选型逻辑在Linux/Unix环境下做C/C开发时间处理几乎是绕不开的一环。不管你是做性能分析、日志打点、定时任务还是超时控制总会和clock()、time()、clock_gettime()、gettimeofday()这四个函数打交道。很多刚接触系统编程的朋友会困惑明明都是获取时间为什么要搞出四个函数它们之间到底有什么区别什么场景该用哪个我刚开始写服务端程序的时候就踩过一个典型的坑用clock()去测量一段网络请求的耗时结果发现算出来的时间和实际感受完全对不上。后来才搞明白clock()返回的是CPU时间而不是墙上时间网络等待期间CPU根本没干活自然就不计入。这个教训让我意识到把这四个函数搞清楚不是学术问题而是实打实会影响代码正确性的工程问题。这篇文章我会从实际使用角度出发把四个函数的返回值语义、精度、适用场景、底层实现以及常见坑全部拆开讲清楚。不管你是刚学系统编程的新手还是写了几年代码但一直没系统梳理过时间函数的老手看完应该都能有收获。核心关键词就四个CPU时间、墙上时间、单调时钟、精度与开销理解了这四个概念选型就不会再纠结。先说结论性的选型思路后面再展开细节要测程序真正消耗了多少CPU算力用clock()只要秒级精度、记录日志时间戳用time()要纳秒级高精度计时、做性能分析或超时控制用clock_gettime()维护老代码或者需要微秒级墙上时间且不想改太多gettimeofday()还能用但新代码不建议。这个选型逻辑背后其实对应着两类完全不同的时间概念下面先把这个根上的问题讲透。1.1 先分清CPU时间和墙上时间这两个概念这是理解四个函数的第一道分水岭。**墙上时间wall-clock time就是你墙上挂的钟显示的时间也叫真实时间、日历时间。它回答的问题是现在是几点几分几秒。而CPU时间CPU time**回答的是这个进程真正占用了CPU多少时间。举个生活化的例子。你去银行办业务从进门到出门总共花了1小时这是墙上时间。但其中真正在柜台办理的时间可能只有10分钟剩下50分钟都在排队等待。这10分钟就相当于CPU时间1小时相当于墙上时间。这个区别在程序里非常关键。一个进程可能运行了10秒墙上时间但如果它大部分时间在等IO、等网络、等锁那它实际消耗的CPU时间可能只有0.5秒。反过来一个多线程程序在4核机器上跑满墙上时间1秒CPU时间可能达到4秒。clock()返回的就是CPU时间time()、gettimeofday()、clock_gettime()默认的CLOCK_REALTIME返回的都是墙上时间。这是它们最本质的区别也是选型时第一个要问自己的问题我到底要测的是过了多久还是算了多久。1.2 单调时钟为什么在计时场景里更可靠除了CPU时间和墙上时间的区别还有一个容易被忽视但极其重要的概念单调性。墙上时间是可以被调整的——系统管理员手动改时间、NTP服务自动校时、闰秒处理都会让墙上时间发生跳变。可能突然往前跳一秒也可能往后倒退。如果你用墙上时间去测量一段代码的耗时恰好中间发生了时间校准那算出来的耗时可能是负数或者大得离谱。这种bug非常隐蔽因为平时测试根本复现不了只有线上环境偶尔校时时才会冒出来。**单调时钟monotonic clock**就是为了解决这个问题设计的。它保证时间只会单调递增不受系统时间调整影响专门用来测量时间间隔。clock_gettime()配合CLOCK_MONOTONIC就是干这个的。而clock()虽然也是单调递增的CPU时间不会倒退但它测的是CPU时间不是墙上时间用途不同。所以完整的选型逻辑其实是两个维度的组合要CPU时间还是墙上时间要不要单调性。把这两个维度想清楚四个函数该用哪个就一目了然了。2. 四个函数的核心细节与实操要点逐个拆解这一部分我把四个函数一个一个拆开讲包括函数原型、返回值含义、精度、底层实现和实际使用中的注意点。每个函数我都会给出可直接编译运行的示例代码你可以直接抄过去改。2.1 clock()测CPU时间的老牌选手clock()的函数原型在time.h里#include time.h clock_t clock(void);它返回的是进程自启动以来消耗的CPU时间单位是CLOCKS_PER_SEC分之一秒。在POSIX系统上CLOCKS_PER_SEC固定是1000000也就是返回值单位是微秒。注意这个值在不同平台上可能不一样所以永远要用CLOCKS_PER_SEC去换算不要硬编码。#include stdio.h #include time.h int main(void) { clock_t start clock(); // 模拟一段计算密集型工作 volatile double sum 0; for (long i 0; i 100000000; i) { sum i * 0.5; } clock_t end clock(); double cpu_seconds (double)(end - start) / CLOCKS_PER_SEC; printf(CPU time used: %.6f seconds\n, cpu_seconds); return 0; }这里有几个实操要点必须强调。第一clock()测的是整个进程的CPU时间包括所有线程。在Linux上它底层调用的是clock_gettime(CLOCK_PROCESS_CPUTIME_ID)所以多线程程序里它统计的是所有线程CPU时间的总和。第二它不包含进程阻塞等待的时间比如sleep()、等IO、等网络这些时间都不计入。第三它的精度受限于系统时钟中断频率通常是1ms到10ms级别做粗粒度统计够用做精细性能分析就不够了。注意clock()的返回值类型是clock_t在32位系统上可能溢出。如果你的程序要跑很长时间end - start可能因为溢出变成负数。做长时间统计时建议用clock_gettime()替代。我个人的经验是clock()现在主要用在两个场景一是快速粗略地看一段纯计算代码消耗了多少CPU二是教学演示。真正做性能分析我基本都用clock_gettime(CLOCK_PROCESS_CPUTIME_ID)精度更高也更可控。2.2 time()最简单粗暴的秒级时间戳time()的原型同样在time.h#include time.h time_t time(time_t *tloc);它返回自Unix纪元1970-01-01 00:00:00 UTC以来的秒数也就是常说的Unix时间戳。如果参数tloc不为NULL返回值也会同时写入tloc指向的内存。#include stdio.h #include time.h int main(void) { time_t now time(NULL); printf(Unix timestamp: %ld\n, (long)now); // 转成可读格式 char buf[64]; struct tm *tm_info localtime(now); strftime(buf, sizeof(buf), %Y-%m-%d %H:%M:%S, tm_info); printf(Local time: %s\n, buf); return 0; }time()的特点就是简单、精度低、开销小。精度只有1秒所以它绝对不能用来测耗时测出来的结果要么是0要么是1毫无意义。它的正确用途是记录事件发生的绝对时间点比如日志时间戳、文件创建时间、缓存过期时间计算。这里有个细节值得说time()返回的是UTC秒数本身不带时区信息。你要显示成本地时间得用localtime()或localtime_r()转换。多线程环境下一定要用localtime_r()这个可重入版本localtime()返回的是静态缓冲区指针多线程同时调用会互相覆盖这是个经典坑。提示time_t在32位系统上是有符号32位整数2038年会溢出这就是著名的2038问题。新代码建议直接用64位时间类型或者用clock_gettime()。2.3 gettimeofday()微秒级墙上时间的老将gettimeofday()的原型在sys/time.h#include sys/time.h int gettimeofday(struct timeval *tv, struct timezone *tz);它把当前墙上时间填入struct timeval结构struct timeval { time_t tv_sec; // 秒 suseconds_t tv_usec; // 微秒 };第二个参数tz是历史遗留的时区参数现在必须传NULL传了也没用而且行为未定义。#include stdio.h #include sys/time.h int main(void) { struct timeval tv; gettimeofday(tv, NULL); printf(Seconds: %ld, Microseconds: %ld\n, (long)tv.tv_sec, (long)tv.tv_usec); double total tv.tv_sec tv.tv_usec / 1000000.0; printf(Total: %.6f\n, total); return 0; }gettimeofday()提供微秒级的墙上时间比time()精度高得多所以在clock_gettime()普及之前它是测量耗时的主力。但它有两个致命问题一是不单调系统时间被调整时它会跳变二是精度受限于系统时钟源实际分辨率往往只有1ms到4ms标称的微秒级是能表示到微秒而不是精确到微秒。现在gettimeofday()在Linux上已经被标记为过时obsolescentPOSIX标准建议新代码用clock_gettime()替代。但大量存量代码还在用它所以你必须看得懂、改得动。2.4 clock_gettime()现代高精度计时的首选clock_gettime()是POSIX标准推荐的现代时间接口原型在time.h#include time.h int clock_gettime(clockid_t clk_id, struct timespec *tp);struct timespec的精度是纳秒struct timespec { time_t tv_sec; // 秒 long tv_nsec; // 纳秒 };关键在于第一个参数clk_id它决定了你要获取哪种时钟。常用的有四种时钟ID含义是否单调典型用途CLOCK_REALTIME墙上时间否日志时间戳、绝对时间CLOCK_MONOTONIC单调时间是测量耗时、超时控制CLOCK_PROCESS_CPUTIME_ID进程CPU时间是进程性能分析CLOCK_THREAD_CPUTIME_ID线程CPU时间是单线程性能分析#include stdio.h #include time.h int main(void) { struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, start); // 模拟工作 volatile long sum 0; for (long i 0; i 50000000; 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_gettime()的优势非常明显纳秒级精度、支持多种时钟源、单调时钟可选、线程安全。测量耗时用CLOCK_MONOTONIC测进程CPU消耗用CLOCK_PROCESS_CPUTIME_ID测单线程CPU消耗用CLOCK_THREAD_CPUTIME_ID一个函数全搞定。注意链接时可能需要加-lrt老版本glibc新版本glibc2.17以后已经把它并入libc不用额外链接。另外CLOCK_MONOTONIC的起点是系统启动时刻不是Unix纪元所以它的值不能直接当时间戳用只能做差值。3. 实操过程与核心环节实现光看函数原型不够这一部分我给出几个真实场景下的完整实现包括参数计算、代码结构和实测结果。你可以直接拿去改。3.1 场景一测量一段代码的真实耗时这是最常见的需求。正确做法是用CLOCK_MONOTONIC因为要测的是墙上时间流逝而且不能被系统校时干扰。#include stdio.h #include time.h static double now_seconds(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, ts); return ts.tv_sec ts.tv_nsec / 1e9; } int main(void) { double t0 now_seconds(); // 你的业务代码 volatile double x 0; for (long i 0; i 200000000; i) x i * 0.1; double t1 now_seconds(); printf(Wall time: %.6f s\n, t1 - t0); return 0; }这里我把时间获取封装成了now_seconds()函数返回double类型的秒数。这样做的好处是差值计算简单不用每次手动处理tv_sec和tv_nsec的借位。注意tv_nsec是纳秒除以1e9转成秒。实测下来这段代码在普通x86机器上跑2亿次浮点加法大约0.3到0.5秒clock_gettime本身的开销在20到30纳秒左右相对于业务代码可以忽略。但如果你要测的代码本身只有几百纳秒那这个开销就不能忽视了得考虑批量测量取平均。3.2 场景二同时测量墙上时间和CPU时间有时候你需要知道一段代码既花了多少真实时间又消耗了多少CPU。比如判断一个任务是计算密集型还是IO密集型就要对比这两个值。#include stdio.h #include time.h int main(void) { struct timespec wall_start, wall_end; struct timespec cpu_start, cpu_end; clock_gettime(CLOCK_MONOTONIC, wall_start); clock_gettime(CLOCK_PROCESS_CPUTIME_ID, cpu_start); // 模拟混合负载计算 等待 volatile long sum 0; for (long i 0; i 30000000; i) sum i; struct timespec req {0, 100000000}; // 100ms nanosleep(req, NULL); clock_gettime(CLOCK_MONOTONIC, wall_end); clock_gettime(CLOCK_PROCESS_CPUTIME_ID, cpu_end); double wall (wall_end.tv_sec - wall_start.tv_sec) (wall_end.tv_nsec - wall_start.tv_nsec) / 1e9; double cpu (cpu_end.tv_sec - cpu_start.tv_sec) (cpu_end.tv_nsec - cpu_start.tv_nsec) / 1e9; printf(Wall: %.6f s, CPU: %.6f s, CPU%%: %.1f%%\n, wall, cpu, cpu / wall * 100); return 0; }运行结果大概是墙上时间0.15秒左右CPU时间0.05秒左右CPU占用率约33%。这个比例直观反映了程序有大量时间在等待。这种对比分析在性能调优时非常有用能快速判断瓶颈在计算还是在IO。3.3 场景三用clock()和clock_gettime()对比验证为了让你直观感受clock()和clock_gettime()的区别我写了个对比程序#include stdio.h #include time.h #include unistd.h int main(void) { clock_t c0 clock(); struct timespec m0, m1; clock_gettime(CLOCK_MONOTONIC, m0); sleep(1); // 纯等待不消耗CPU clock_t c1 clock(); clock_gettime(CLOCK_MONOTONIC, m1); double cpu (double)(c1 - c0) / CLOCKS_PER_SEC; double wall (m1.tv_sec - m0.tv_sec) (m1.tv_nsec - m0.tv_nsec) / 1e9; printf(clock() CPU time: %.6f s\n, cpu); printf(monotonic wall time: %.6f s\n, wall); return 0; }实测结果clock()算出来接近0clock_gettime()算出来约1.0秒。这就是最直观的证明——sleep期间进程没消耗CPU所以CPU时间几乎为零但墙上时间实实在在过了1秒。如果你用clock()去测带IO等待的代码就会严重低估真实耗时。3.4 场景四gettimeofday()迁移到clock_gettime()如果你手上有老代码用gettimeofday()测耗时迁移到clock_gettime()其实很简单。核心就是把struct timeval换成struct timespec把gettimeofday(tv, NULL)换成clock_gettime(CLOCK_MONOTONIC, ts)。// 老写法 struct timeval tv1, tv2; gettimeofday(tv1, NULL); // ... work ... gettimeofday(tv2, NULL); double elapsed (tv2.tv_sec - tv1.tv_sec) (tv2.tv_usec - tv1.tv_usec) / 1e6; // 新写法 struct timespec ts1, ts2; clock_gettime(CLOCK_MONOTONIC, ts1); // ... work ... clock_gettime(CLOCK_MONOTONIC, ts2); double elapsed (ts2.tv_sec - ts1.tv_sec) (ts2.tv_nsec - ts1.tv_nsec) / 1e9;迁移后最大的收益是单调性保证和更高精度。我做过实测在同样的机器上gettimeofday()的实际分辨率约1微秒到4微秒而clock_gettime(CLOCK_MONOTONIC)能到几十纳秒。对于测量短耗时操作这个差距是数量级的。4. 常见问题与排查技巧实录这一部分是我这些年踩过的坑和帮别人排查过的问题整理成速查表加详细说明希望能帮你少走弯路。4.1 常见问题速查表问题现象可能原因排查方法解决方案测出的耗时是负数用了非单调时钟中间发生校时检查是否用了CLOCK_REALTIME或gettimeofday改用CLOCK_MONOTONIC测出的耗时远小于预期用了clock()测带IO的代码对比CPU时间和墙上时间改用CLOCK_MONOTONIC耗时精度只有毫秒级系统时钟源分辨率低查看/sys/devices/system/clocksource换高精度时钟源或接受现实多线程下时间戳错乱用了localtime()等非可重入函数检查是否用了_r版本改用localtime_r()链接报错undefined reference老glibc需要-lrt检查glibc版本编译加-lrt2038年后时间错误time_t是32位检查sizeof(time_t)用64位类型或clock_gettime4.2 坑一clock()在多线程下的统计陷阱clock()返回的是整个进程的CPU时间包括所有线程。这在单线程程序里没问题但在多线程程序里会误导你。假设你开了4个线程每个线程跑满一个核墙上时间过了1秒clock()会返回大约4秒。如果你不知道这个特性会以为程序跑了4秒。更麻烦的是clock()的统计粒度比较粗线程调度切换的开销可能被算进去也可能不算结果不太稳定。要做精确的多线程性能分析应该用CLOCK_THREAD_CPUTIME_ID分别测每个线程或者用CLOCK_PROCESS_CPUTIME_ID测整体。提示CLOCK_PROCESS_CPUTIME_ID和clock()在Linux上底层是同一个东西但前者精度更高建议优先用前者。4.3 坑二CLOCK_REALTIME做超时控制的隐患很多人做超时控制时习惯用CLOCK_REALTIME因为可以直接和time(NULL)比较。但这是有风险的。如果系统时间在超时等待期间被往前调了超时会提前触发被往后调了超时会延后甚至永远不触发。正确做法是用CLOCK_MONOTONIC做超时控制。它的起点是系统启动时刻不受校时影响只会单调递增。计算超时时间时用当前单调时间 超时秒数作为截止点然后循环检查当前单调时间是否超过截止点。#include time.h int wait_with_timeout(int fd, double timeout_sec) { struct timespec deadline; clock_gettime(CLOCK_MONOTONIC, deadline); deadline.tv_sec (time_t)timeout_sec; deadline.tv_nsec (long)((timeout_sec - (time_t)timeout_sec) * 1e9); if (deadline.tv_nsec 1000000000L) { deadline.tv_sec 1; deadline.tv_nsec - 1000000000L; } while (1) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, now); if (now.tv_sec deadline.tv_sec || (now.tv_sec deadline.tv_sec now.tv_nsec deadline.tv_nsec)) { return -1; // 超时 } // 这里做实际的等待逻辑比如poll带剩余时间 // ... } }这段代码里我特意处理了纳秒进位因为tv_nsec必须保持在0到999999999之间。这个细节很容易被忽略一旦越界行为就未定义了。4.4 坑三时间差计算的借位问题计算两个timespec的差值时如果直接end.tv_nsec - start.tv_nsec可能得到负数。比如start是{10, 900000000}end是{11, 100000000}直接相减纳秒部分是-800000000。正确做法是先算秒差再算纳秒差或者统一转成纳秒再算。// 方法一分别处理借位 double diff(struct timespec *a, struct timespec *b) { double sec (b-tv_sec - a-tv_sec) (b-tv_nsec - a-tv_nsec) / 1e9; return sec; } // 方法二统一转纳秒注意溢出风险 long long diff_ns(struct timespec *a, struct timespec *b) { return (long long)(b-tv_sec - a-tv_sec) * 1000000000LL (b-tv_nsec - a-tv_nsec); }方法一用double简单但有精度损失适合大多数场景。方法二用整数精确但要注意long long在极端情况下也可能溢出大约292年实际够用。我一般用方法一因为double的53位尾数在秒级范围内精度足够。4.5 坑四系统时钟源对精度的影响clock_gettime()标称纳秒精度但实际精度取决于系统的时钟源。Linux常见的时钟源有tsc、hpet、acpi_pm等。tsc时间戳计数器精度最高开销最小acpi_pm精度只有约300纳秒开销还大。你可以这样查看当前时钟源cat /sys/devices/system/clocksource/clocksource0/current_clocksource如果显示的是acpi_pm那你的clock_gettime()实际精度会大打折扣。现代x86机器一般默认用tsc问题不大。但在虚拟机里tsc可能不稳定系统会自动降级到hpet或acpi_pm这时候精度就会下降。做高精度性能分析时这个因素要考虑进去。注意不要手动去改时钟源除非你非常清楚后果。改错了可能导致系统时间异常。这个信息只用来解释为什么我的纳秒计时看起来没那么准。4.6 坑五vDSO对clock_gettime性能的影响现代Linux上clock_gettime()和gettimeofday()通常通过**vDSO虚拟动态共享对象**实现不需要真正陷入内核态所以开销非常小几十纳秒级别。但clock()和time()在某些实现下可能还是要走系统调用开销大一些。这个差异在微基准测试里能看出来。如果你要在一个循环里调用几百万次时间函数选vDSO加速的clock_gettime(CLOCK_MONOTONIC)会比time()快不少。不过实际业务代码里时间函数调用频率不会那么高这个差异通常可以忽略。知道有这回事就行不用过度优化。4.7 实操心得封装一个统一的时间工具踩了这么多坑之后我的做法是在项目里封装一个统一的时间工具模块把时钟选择、差值计算、超时判断全部收口。这样业务代码里只调用now_monotonic()、elapsed()、deadline_after()这几个语义清晰的函数不用每次都纠结用哪个时钟、怎么算差值。// time_util.h #ifndef TIME_UTIL_H #define TIME_UTIL_H #include time.h static inline void now_monotonic(struct timespec *ts) { clock_gettime(CLOCK_MONOTONIC, ts); } static inline double ts_diff(const struct timespec *a, const struct timespec *b) { return (b-tv_sec - a-tv_sec) (b-tv_nsec - a-tv_nsec) / 1e9; } static inline void deadline_after(struct timespec *dl, double sec) { clock_gettime(CLOCK_MONOTONIC, dl); dl-tv_sec (time_t)sec; dl-tv_nsec (long)((sec - (time_t)sec) * 1e9); if (dl-tv_nsec 1000000000L) { dl-tv_sec 1; dl-tv_nsec - 1000000000L; } } static inline int deadline_expired(const struct timespec *dl) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, now); return (now.tv_sec dl-tv_sec) || (now.tv_sec dl-tv_sec now.tv_nsec dl-tv_nsec); } #endif这个头文件用static inline定义不会有链接开销每个编译单元包含一份。用起来很顺手业务代码里再也不用直接碰struct timespec的字段了。这套封装我在好几个项目里用过实测稳定推荐你也试试。最后再分享一个小技巧如果你需要同时记录日志时间戳和测量耗时日志时间戳用CLOCK_REALTIME因为要人能看懂耗时测量用CLOCK_MONOTONIC因为要准确两个时钟各司其职不要混用。我见过有人为了省事全用CLOCK_REALTIME结果线上偶尔出现耗时统计异常排查半天才发现是校时导致的。这种问题一旦发生就很隐蔽提前用对时钟能省掉很多麻烦。
阅读完成 · 觉得有帮助?