写日志、打时间戳、统计耗时,是Linux下每个C/C++开发都会遇到的事。刚开始接触Linux编程那会儿,我获取系统时间只会用time函数,后来看到同事代码里的gettimeofday,才意识到不同场景对时间精度的要求完全不一样。这篇文章就围绕这两个最常用的函数,把原理、用法、选型思路和我在实际项目中踩过的坑一次性讲透,适合刚入门的Linux开发者、嵌入式工程师,以及任何想在代码里正确处理时间的小伙伴。
1. 两块“表”能干什么:time 与 gettimeofday 的定位
1.1 从日常需求看时间获取
在Linux下开发,获取系统时间几乎是绕不开的操作。比如服务端程序要往日志里写一条记录,必须带上“2025年X月X日 XX:XX:XX”这样的时间前缀;压测工具要统计某个接口处理花了多少毫秒;数据库要生成一条带时间戳的数据;甚至rand()取随机数之前,大家也都习惯用时间来做种子。
这些需求看起来都是“拿时间”,但细细分下来,精度要求差异很大。写日志用秒级就够了,记录到哪一秒完全没问题;可要是测量一段代码的执行性能,秒级就太粗糙了,你需要的是毫秒甚至微秒级别的精度。正是因为这个差异,Linux下才会有time和gettimeofday这两个经常被放在一起讨论的函数。
简单理解就是:time负责给你“秒”,gettimeofday给你“秒加微秒”。它们底层都依赖内核维护的系统时间,但从使用方式、返回的数据结构、适用的场景来看,区别还是很明显的。
1.2 time 的定位与适用场景
time函数的定位就是获取当前时间对应的Unix时间戳,单位是秒。它返回的是一个time_t类型的长整型数值,这个数值表示从1970年1月1日0时0分0秒(UTC)到当前时刻经过的秒数,这个起点在计算机领域被称为“Unix纪元”(Unix Epoch)。
它的适用场景,我总结了一下,主要有这么几类:
- 日志记录。给日志行加一个秒级的时间戳前缀,完全够用。
- 日期计算。比如判断一个时间戳是否已过期、计算两个时间点相差多少天。
- 生成随机数种子。给
srand传一个变化足够快的值,秒级也够了。 - 做数据快照时标记数据的最后修改时间。
说句实在话,很多初级开发者一开始容易陷入一个误区,觉得“时间这个值越精确越好”,所以在所有场景里都上gettimeofday。但实际上,精度越高,意味着底层调用的复杂度和潜在坑也越多。如果只是给日志加个时间戳,用秒级的time不仅代码简洁,也完全满足需求。
1.3 gettimeofday 的定位与适用场景
gettimeofday解决的问题,是time给不了的高精度场景。它能够返回当前时间,精度理论上可以达到微秒(microsecond,百万分之一秒)级别。
需要用到微秒级时间的场景,典型的有这些:
- 性能测试。你写了一个排序算法,想比较不同数据量下的耗时,微秒级的计时才能看出差异。
- 帧率控制。在图形程序或游戏逻辑里计算每帧间隔,毫秒和微秒级的计时能提供更平滑的控制。
- 数据采集打时标。传感器采集程序需要给每个数据点打上精确的时间标记。
- 生成短时间内不会重复的序列号或ID的一部分。
如果你要做的是测量“这段代码跑了多久”,那gettimeofday可以在绝大多数场景下帮你完成任务。
2. time 函数深度拆解:低精度但够用
2.1 函数声明与原理解读
先看time函数的原型:
#include <time.h> time_t time(time_t *tloc);time_t在不同的平台上实现不同,在Linux下通常是一个long类型的整数。它本质上就是一个整数计数器,内核在启动后,按照系统维护的墙上时钟(wall clock)不断更新这个秒数。每一次调用time,返回值就是当前这个计数器的快照。
有个有意思的点是,这个值在32位系统上是一个32位的整数。32位有符号整数的最大值是2147483647,换算成时间便是2038年1月19日03:14:07(UTC)。再过一秒钟,这个数就会溢出变成负数。这个问题在嵌入式领域尤其需要警惕,后面第5节我会专门展开讲。
从内核角度看,time函数最终会触发一个系统调用,但在glibc的实现里,由于内核会维护一个由内核定时器周期性更新的“快速时间变量”,这个系统调用可以被优化成直接从内存读取数据,所以它的开销非常小,甚至在很多场景下可以认为是接近零成本的。正因为这样,你在循环里高频调用time都不用太担心性能问题。
2.2 参数 tloc 的两种用法
time函数带了一个指针参数tloc,这个参数的设计初看有点多余,但理解它的两种用法,能帮你读懂很多老的C代码。
第一种用法是传入NULL,这样函数只通过返回值把时间戳交给你:
time_t now = time(NULL); printf("current seconds: %ld\n", (long)now);第二种用法是传入一个time_t类型变量的地址,函数会把结果同时写到这个变量里,并且依然返回同样的值:
time_t now; time(&now); printf("current seconds: %ld\n", (long)now);从功能上讲,这两种写法结果完全一样。我个人的习惯是第一种,因为代码看起来更直观,少写一行。不过有一点需要提醒,在阅读老代码时,经常会看到time(&now);这种写法,它并不代表什么高级用法,只是作者的个人风格或者历史习惯,知道它的等价关系就可以了。
2.3 结构化输出:localtime / gmtime / strftime 配套使用
time拿到的是time_t,一个很大的数字,例如1785456000。这样的数字给人看完全没概念。你需要把它转成人类可读的结构化时间。
这里就要引出三个常用的配套函数:
struct tm *localtime(const time_t *timep); struct tm *gmtime(const time_t *timep); size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);localtime会把时间戳转换成本地时区对应的年月日时分秒,返回一个struct tm结构体指针;gmtime则转换成UTC时区对应的结构体。struct tm结构体定义在<time.h>里,成员包括年、月、日、时、分、秒、星期等:
struct tm { int tm_sec; // 秒,范围 0-60(60 表示闰秒) int tm_min; // 分,范围 0-59 int tm_hour; // 时,范围 0-23 int tm_mday; // 日,范围 1-31 int tm_mon; // 月,范围 0-11,注意 0 代表一月 int tm_year; // 年,从 1900 年起的年数 int tm_wday; // 星期,范围 0-6,0 代表周日 int tm_yday; // 一年中的第几天,范围 0-365 int tm_isdst; // 夏令时标志 };这里有两个特别容易踩坑的地方:一个是tm_mon从0开始,1月是0,2月是1,直接用tm_mon打印月份会出现差一个月的怪问题;另一个是tm_year是从1900年开始计算的,要拿到实际年份需要加1900。很多打印出“1970年”的bug,十有八九就是漏掉了这个偏移量。
拿到struct tm之后,最灵活的输出方式是strftime。它类似于printf,通过格式化占位符把时间拼成任何你想要的字符串格式。常用格式有:
%Y:四位年份,如2025%m:两位月份,01-12%d:两位日期,01-31%H:24小时制小时,00-23%M:分钟,00-59%S:秒,00-59%z:时区偏移,如+0800
2.4 带格式的取时间示例
下面这段代码展示了从time到格式化输出的完整流程,包括本地时间和UTC时间两种打印方式:
#include <stdio.h> #include <time.h> int main(void) { time_t now = time(NULL); struct tm tm_local; struct tm tm_utc; localtime_r(&now, &tm_local); gmtime_r(&now, &tm_utc); char buf_local[128]; char buf_utc[128]; strftime(buf_local, sizeof(buf_local), "%Y-%m-%d %H:%M:%S", &tm_local); strftime(buf_utc, sizeof(buf_utc), "%Y-%m-%d %H:%M:%S", &tm_utc); printf("local time: %s\n", buf_local); printf("utc time : %s\n", buf_utc); return 0; }注意我在这里用了localtime_r和gmtime_r,而不是localtime和gmtime。原因很简单,localtime和gmtime返回的是静态存储区的指针,在多线程环境下会被覆盖,这是非常经典的一个坑。带_r后缀的版本把结果写入调用者提供的缓冲区,线程安全,是实际开发中的推荐写法。关于线程安全,第5节还会专门说明。
3. gettimeofday 函数深度拆解:微秒级计时的利器
3.1 函数声明与 struct timeval 结构体
接下来是重头戏gettimeofday。它的函数原型如下:
#include <sys/time.h> int gettimeofday(struct timeval *tv, struct timezone *tz);这里涉及一个关键结构体struct timeval,定义如下:
struct timeval { time_t tv_sec; // 秒 suseconds_t tv_usec; // 微秒 };tv_sec和time函数的返回值一样,是从Unix纪元开始经过的秒数;tv_usec是当前这一秒内已经过去的微秒数,范围是0到999999。两者相加,就得到了一个“秒.微秒”形式的时间值。
第二个参数tz是时区相关的旧接口,用来获取时区偏移和夏令时标志。在POSIX标准里已经明确不推荐使用,glibc手册里也写得很清楚,这个参数是为了兼容历史代码才保留的,实际使用中直接传NULL即可。
调用示例:
struct timeval tv; gettimeofday(&tv, NULL); printf("seconds: %ld, microseconds: %ld\n", (long)tv.tv_sec, (long)tv.tv_usec);一个需要特别留意的点是,tv_usec是“当前秒内的微秒数”,不是一个完整的时间戳。如果要用微秒为单位表示当前时间,正确的计算方式是:
long long timestamp_us = (long long)tv.tv_sec * 1000000 + tv.tv_usec;先乘后加,不要直接用tv_usec当微秒时间戳。
3.2 为什么它号称能到微秒级
很多人会有疑问:time返回的是秒,gettimeofday怎么能做到微秒?这背后是内核时间子系统的设计。
Linux内核维护的不是一个简单的“秒计数器”,而是一个高精度的时钟源。在x86平台上,内核会结合硬件定时器和时钟芯片来维护一个纳秒级别的系统时间。当用户态调用gettimeofday时,内核会基于当前系统时间,计算出现在是多少秒、多少微秒,然后填到struct timeval里返回给用户态。
这里必须澄清一个概念:gettimeofday的“精确到微秒”是指时间戳数值可以精确到微秒,但实际的精度还取决于系统时钟源、硬件定时器的中断频率和虚拟化平台等因素。也就是说,在大部分普通Linux桌面和服务器上,你调用它拿到的微秒数字是可信的;但在某些虚拟化环境或者老式嵌入式硬件上,实际精度可能只有毫秒级,只是数值上最后几位凑出来的。
另外,gettimeofday本身是一次系统调用,存在用户态和内核态的切换开销。单次调用的耗时一般在几百纳秒到几微秒的量级,如果你在性能测试里把gettimeofday放在被测代码内部循环里,会有一定的测量扰动,需要在设计时考虑到这一点。
3.3 经典用法:精确计算程序耗时
gettimeofday最经典的用法就是秒表式计时:开始时取一个时间点,结束时再取一个时间点,两者相减得到耗时。
网上很多示例代码是这样写的:
struct timeval start, end; gettimeofday(&start, NULL); // 这里是待测代码 gettimeofday(&end, NULL); long cost_us = (end.tv_sec - start.tv_sec) * 1000000 + (end.tv_usec - start.tv_usec);这个写法本身没问题,但有几个细节建议优化。第一个,不要直接比较tv_sec和tv_usec的绝对值,因为tv_usec在跨秒时会从999999跳回0,直接减容易出错。正确的做法是统一换算成微秒再相减,也就是上面的公式。第二个,如果耗时可能超过2^31微秒(约35.8分钟),建议用long long类型保存结果,避免溢出。
下面是一个更完整的计时工具示例,计算一个循环的总耗时和每次迭代的平均耗时:
#include <stdio.h> #include <sys/time.h> static long long now_us(void) { struct timeval tv; gettimeofday(&tv, NULL); return (long long)tv.tv_sec * 1000000 + tv.tv_usec; } int main(void) { long long start = now_us(); volatile int sum = 0; for (int i = 0; i < 1000000; i++) { sum += i; } long long end = now_us(); long long total_us = end - start; printf("total time: %lld us (%.3f ms)\n", total_us, total_us / 1000.0); printf("average per iteration: %.3f ns\n", total_us * 1000.0 / 1000000); return 0; }封装一个now_us函数,把struct timeval的转换逻辑隐藏掉,主流程看起来就清爽很多。这里加volatile是为了防止编译器把空循环优化掉,否则可能测出一个几乎为0的耗时,这也是性能测试中常见的一个坑。
3.4 什么时候不选它:和 clock_gettime 的取舍
讲了这么多gettimeofday的好,必须说一个现实情况:在较新的POSIX标准里,gettimeofday已经被标记为过时(obsolete)接口,推荐替代方案是clock_gettime。
clock_gettime支持多种时钟源,其中两个最常用:
CLOCK_REALTIME:和gettimeofday一样,表示墙上时钟,受系统时间调整影响。CLOCK_MONOTONIC:单调时钟,不受NTP跳变或手动改时间影响,只增不减,专门用来测量时间间隔。
clock_gettime用的结构体是struct timespec,精度到了纳秒级别:
struct timespec { time_t tv_sec; // 秒 long tv_nsec; // 纳秒 };所以在测量耗时、计算时间间隔这类场景下,我更推荐直接使用clock_gettime(CLOCK_MONOTONIC, &ts)。它的好处是无论系统时间怎么被调整,计时结果都不受影响,这对性能测试的稳定性很关键。
不过,gettimeofday在现实代码中的存量非常大,网上大量开源项目、老教程都还在用它,而且它足够简单,处理绝大多数场景已经够了。所以我的建议是:存量代码里看到gettimeofday,不用急着改,能用就行;新写的耗时测量代码,优先考虑clock_gettime(CLOCK_MONOTONIC);只是记录一个时间戳给日志用,那就直接用time。
4. 对比与选型:两个函数什么时候用哪个
4.1 一张表格看明白核心差异
为了让你一眼看清楚,我把两个函数的核心差异整理成了表格:
| 对比项 | time | gettimeofday |
|---|---|---|
| 头文件 | time.h | sys/time.h |
| 返回时间精度 | 秒 | 微秒 |
| 关键类型/结构体 | time_t | struct timeval |
| 返回值 | time_t 时间戳 | 0表示成功,-1表示失败 |
| 典型用途 | 日志时间戳、日期计算、随机数种子 | 耗时统计、高精度时标 |
| 是否受时区影响 | 本身是UTC秒数,显示时受时区影响 | 本身是UTC秒数,显示时受时区影响 |
| 是否受系统时间调整影响 | 受影响,时间可能跳动 | 受影响,时间可能跳动 |
| 线程安全性 | time本身安全;localtime/ctime不安全 | 本身安全,需注意结构体归属 |
| 底层调用开销 | 很小(可能直接读内存) | 相对大,有系统调用开销 |
| 标准状态 | 标准且常用 | POSIX标记为过时,仍广泛使用 |
这张表基本覆盖了选型时需要考虑的关键因素。最简单的记忆方式是:只要精度要求没超过秒,就用time;要求微秒级,才用gettimeofday。
4.2 开销、精度与时区这些隐藏因素
很多初学者容易忽略性能开销这个维度。time在主流glibc实现中甚至可能不触发真正的系统调用,因为内核为它维护了一个vdso(Virtual Dynamic Shared Object)映射,用户态可以直接读取时间数据,几乎零成本。而gettimeofday在多数平台上也走了vdso优化,性能很好,但它返回的数据结构更复杂,毕竟要填充秒和微秒两个字段。
这里简单解释一下vdso。为了减少频繁获取时间时的系统调用开销,内核把一段精心设计的代码映射到用户进程的地址空间,用户态调用gettimeofday时,实际上可能直接执行这段映射代码,从内核共享的内存页里读取时间信息,而不需要陷入内核。所以实际开销比很多人想象中要小得多。但在高频率轮询(比如每微秒调一次)的场景下,即使有vdso,仍然会有缓存一致性和计算开销,设计时还是要尽量批量获取时间,而不是在循环里频繁调用。
时区问题也值得强调。time和gettimeofday返回的都是UTC标准下的秒数,本身不带任何时区信息。时区只是在你把它转换成struct tm并显示时才参与到计算中。localtime系列会通过读取系统的TZ环境变量或/etc/localtime配置来转换时区。如果你的服务器时区配置错了,即使底层时间戳完全正确,打印出来的日志时间也会跟着错。
所以,在存储和传输时间时,最佳实践是存time_t这种绝对秒数;只在给人看的时候转成本地时间字符串。这样无论部署到哪个时区的机器上,时间逻辑都不会乱。
4.3 可移植性和嵌入式环境的注意点
跨平台方面,time是C标准库函数,所有主流平台都支持,可移植性最好。gettimeofday是POSIX标准里的接口,在Linux、macOS、BSD上都能用,但在Windows原生环境下没有这个函数,如果代码要跨Windows编译,需要做条件编译或者改用Windows API。
在嵌入式Linux领域,情况要复杂一些。如果你的程序跑在完整的Linux用户空间,time和gettimeofday都可以正常用。但如果你在写内核驱动或者嵌入式裸机程序,这两个函数都不能直接用:内核驱动里应该用ktime_get_real_ts64这类内核API,裸机环境则要依赖芯片自带的定时器外设。很多从裸机转Linux开发的工程师,经常会下意识地在驱动里调用用户态的gettimeofday,然后发现编译都过不了,这个需要特别注意。
另一个嵌入式环境常见的问题是,有些裁剪过的内核或者精简libc库可能没有实现gettimeofday的vdso优化,这时候每次调用都是完整系统调用,性能影响会比普通桌面系统大很多,高精度计时时要考虑到这一层开销。
5. 实操过程中踩过的坑(经验之谈)
5.1 time_t 回绕问题:2038年不是玩笑
前面提到过,time_t在32位系统上是32位有符号整数,最大能表示到2038年1月19日03:14:07 UTC。在这之后再调用time函数,返回值会变成一个负数,程序里所有基于时间的判断、日志打印、过期校验都会崩坏。
这个坑在传统的32位ARM嵌入式设备上尤其危险,因为很多IoT硬件还在用32位MCU和32位Linux系统。如果你的产品计划在2038年之后继续运行,现在写的代码就应该考虑这个问题。办法主要有几个:
- 编译时加上
-D_TIME_BITS=64(配合-D_FILE_OFFSET_BITS=64),强制把time_t变成64位。 - 检查你使用的libc版本和内核版本是否支持64位
time_t,老版本glibc可能要升级。 - 设计存储协议时,不用裸的
time_t,而是自定义64位整数来存时间戳,比如统一的long long类型。
实际工作中我建议,凡是新写的代码,涉及到存时间戳的字段,一律用long long或int64_t,不要直接依赖time_t的宽度。这样即使换了平台或者编译选项,数据结构也不会因为time_t宽度不同而出问题。
5.2 时区导致的日志时间偏移
这个坑我记忆犹新。有一次排查线上问题,发现服务日志的时间和真实时间差了整整8个小时,客户反馈说日志里的时间线完全对不上。那时候第一反应是代码bug,查了半天才发现是服务器系统时区没设置好,/etc/localtime指向了UTC。
在Linux下,time函数本身拿到的UTC秒数是没错的,但只要转换成显示字符串,就和系统时区强相关。排查这个问题有几个关键命令:
date # 查看当前系统时间和时区 timedatectl # 现代systemd系统上查看和设置时区 cat /etc/timezone # Debian/Ubuntu上查看时区配置 cat /etc/localtime # 查看符号链接指向要稳妥地处理时区问题,我总结了三条经验:
- 服务器统一设置成UTC,日志里明确标注时区,这样多台机器比对日志时最省心。
- 如果业务要求显示本地时间,优先用
tm_isdst字段配合tzset()函数正确初始化时区解析。 - 代码里不要自己写“UTC+8”这种时区偏移计算逻辑,直接交给
localtime全家桶处理,手动加减小时会在夏令时地区翻车。
5.3 gettimeofday 时间跳跃:NTP 和 date -s 的杀伤力
用gettimeofday测量耗时,最隐蔽的坑是系统时间被调整。最常见的触发源有两个,一个是NTP自动同步,另一个是运维手动执行date -s修改时间。
NTP发现本地时间和标准时间偏差过大时,会直接“跳变”校准,而不是慢慢微调。如果程序正好在这一瞬间用gettimeofday做耗时统计,测出来的时间差可能是一个负数,或者一个离谱的巨大值。我自己就遇到过一次性能测试报告里出现耗时-3秒的情况,查到最后就是NTP同步引起的。
关键点在于,gettimeofday读取的是墙上时钟(wall clock),它的本质是“现在是什么时间”,而不是“过去了多少时间”。系统时间被人为改动时,它根本无法区分到底是正常的时间流逝还是被跳变了。
解决办法也很明确:测量时间间隔,换用clock_gettime(CLOCK_MONOTONIC)。单调时钟从系统启动开始计数,不受NTP和人工改时的影响,只反映真实流逝的时间。例如:
struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, &start); // 待测代码 clock_gettime(CLOCK_MONOTONIC, &end); long long diff_ns = (end.tv_sec - start.tv_sec) * 1000000000LL + (end.tv_nsec - start.tv_nsec);这个写法在长时间运行的服务里非常重要。你的服务可能连续跑几个月,期间一定会发生NTP同步,用墙上时钟计时等于给自己埋雷。
5.4 线程安全:localtime 与 ctime 的隐藏坑
现代服务端程序基本都是多线程的,时间相关的线程安全问题是我排查过最多的一类诡异bug。
localtime、gmtime、ctime、asctime这几个函数返回的都是指向静态存储区的指针。所谓静态存储区,就是整个程序只有一份、所有线程共享的内存。线程A调用localtime拿到了指针,还没来得及打印,线程B也调用了localtime,就把这份内存里的内容覆盖了。结果线程A打印出来的时间,可能跟它自己的时间点完全对不上。
多线程下正确做法是用带_r后缀的可重入版本:
struct tm *localtime_r(const time_t *timep, struct tm *result); char *ctime_r(const time_t *timep, char *buf);它们把结果写入调用者提供的缓冲区,互不干扰。下面是一个实际项目中我常用的线程安全日志时间封装:
void get_log_time(char *buf, size_t len) { time_t now = time(NULL); struct tm tm_now; localtime_r(&now, &tm_now); strftime(buf, len, "%Y-%m-%d %H:%M:%S", &tm_now); }在所有需要打日志的模块里调用这个函数,不需要考虑锁的问题,也基本不存在性能瓶颈。如果你看到老代码里在多线程环境下直接用localtime,那这个程序迟早会在某个高并发时刻输出混乱的时间。
5.5 中断和驱动里的时间获取限制
这个问题主要涉及嵌入式Linux和内核开发。在用户空间里,获取时间是个再普通不过的操作。但进入内核态或者驱动上下文之后,情况就不一样了。
内核驱动中不能直接用用户态的time或gettimeofday,因为依赖关系不对。内核提供了自己的时间接口,比如:
ktime_get_real_ts64(struct timespec64 *ts):获取墙上时钟时间。ktime_get_ts64(struct timespec64 *ts):获取单调时钟时间。jiffies变量:内核心跳计数,适合粗略的时间判断。
在中断上下文里,还要注意不能调用任何可能引起睡眠的函数,比如获取时间时如果涉及锁等待,可能导致系统挂死。多数内核时间接口是考虑过这个问题的,但如果你调了一个会分配内存、拿信号量的辅助函数,就有可能导致死锁或者严重调度延迟。
做驱动的朋友一定要建立这个意识:你不是在写普通的用户态程序,内核API的使用限制比用户态严格得多,时间获取也不例外。最简单的方式是,在驱动里需要时间戳时,先查一下对应内核版本头文件里推荐的API,不要凭用户态编程的经验来猜。
6. 拿来即用的完整代码:日志时间与耗时测量
6.1 秒级日志时间戳:time + localtime_r + strftime
下面这个封装函数可以直接放进你的工具库,给日志模块打时间戳用。它做成了线程安全版本,也考虑到了strftime缓冲区大小的问题:
#include <stdio.h> #include <string.h> #include <time.h> void format_current_time(char *buf, size_t size) { if (buf == NULL || size == 0) { return; } time_t now = time(NULL); struct tm tm_now; localtime_r(&now, &tm_now); strftime(buf, size, "%Y-%m-%d %H:%M:%S", &tm_now); } int main(void) { char time_buf[64]; format_current_time(time_buf, sizeof(time_buf)); printf("log time: %s\n", time_buf); return 0; }这个函数有几个设计细节值得说一下。第一,统一使用localtime_r而不是localtime,多线程安全;第二,允许调用者传入缓冲区大小,避免越界;第三,返回值通过指针传递,不依赖静态缓冲区,灵活性高。
如果你的日志需要做到毫秒级区分,可以在这个基础上把gettimeofday的微秒字段拼上去:
#include <stdio.h> #include <sys/time.h> #include <time.h> void format_current_time_ms(char *buf, size_t size) { if (buf == NULL || size == 0) { return; } struct timeval tv; gettimeofday(&tv, NULL); struct tm tm_now; localtime_r(&tv.tv_sec, &tm_now); strftime(buf, size, "%Y-%m-%d %H:%M:%S", &tm_now); size_t len = strlen(buf); snprintf(buf + len, size - len, ".%03ld", (long)tv.tv_usec / 1000); }这样做的好处是既保持了秒级部分的易读性,又能通过毫秒字段区分同一秒内的多条日志顺序,对排查性能问题特别有用。
6.2 微秒级耗时测量:gettimeofday 封装宏
做性能测试时,经常要对某段代码反复计时。写成一个宏或者封装函数,代码会整洁很多。下面这种封装方式是很多项目里常见的做法:
#include <stdio.h> #include <sys/time.h> #define TIME_COST_US(expr) do { \ struct timeval _start, _end; \ gettimeofday(&_start, NULL); \ do { expr; } while (0); \ gettimeofday(&_end, NULL); \ long long _cost = (_end.tv_sec - _start.tv_sec) * 1000000LL \ + (_end.tv_usec - _start.tv_usec); \ printf("cost: %lld us\n", _cost); \ } while (0) void test_function(void) { volatile int sum = 0; for (int i = 0; i < 100000; i++) { sum += i * 2; } } int main(void) { TIME_COST_US(test_function()); return 0; }使用宏而不是函数来包住被测试代码,是因为宏能保留调用现场,用法更灵活,可以测试任意表达式、函数调用或代码块。do { ... } while (0)这个写法是为了让宏在if、for等控制结构里安全使用,避免分号或作用域问题。
如果你要测量的代码耗时非常短(比如纳秒级别的单指令操作),那gettimeofday本身的调用开销会污染测量结果,这时候应该把被测代码放进循环里跑很多次,用总耗时除以次数得到单次平均耗时。这也是为什么很多benchmark工具都有“预热”和“多次迭代”的阶段。
从工程实践角度看,处理Linux系统时间并不是背两个函数原型那么简单,核心是理解精度需求、时区语义、线程安全这三件事。日志打点优先用time加localtime_r加strftime;高精度测量优先用clock_gettime(CLOCK_MONOTONIC);存量代码里的gettimeofday可以继续用,但要明白它的局限。我自己这几年的体会是,真正困难的问题往往不是“怎么获取时间”,而是“获取到时间之后怎么保证它不给你挖坑”:
- 存储和传输时间,始终坚持用UTC秒数或64位时间戳,不要存“格式化后的字符串”。
- 多线程代码里永远不要用
localtime、ctime,强烈建议统一使用_r版本。 - 凡是测量时间间隔,一律用单调时钟,不要用墙上时钟。
- 涉及时间戳溢出的场景,尽早使用64位类型,不要赌自己的程序活不过2038年。
把这几点记牢,你在Linux下和时间打交道的基本功就算扎实了。如果后续有什么更深入的问题,欢迎留言交流。