新闻详情

新闻详情

首页 / 资讯中心 / 详情

Linux C开发必知:time与gettimeofday函数深度解析与选型指南

发布时间:2026/10/1 16:33:20来源:尧图网络
Linux C开发必知:time与gettimeofday函数深度解析与选型指南
做Linux C开发的人几乎都会碰到获取系统时间的需求。写日志要打时间戳、统计接口耗时、生成文件名、做定时任务样样都离不开系统时间。我刚接触Linux编程那会儿最常用的就是time函数后来发现它只能拿到秒级精度做性能统计根本不够用才慢慢接触到gettimeofday。这两个函数看似简单实际用起来有不少细节坑比如返回值怎么判断、时区怎么处理、线程安全与否、精度到底差多少。这篇就系统聊一聊time和gettimeofday的用法、原理、选型思路以及我在实战中踩过的一些坑。1. 先搞懂Linux时间戳1970年秒数到底怎么来的1.1 从Epoch说起为什么时间是个不断累加的秒数Linux系统内部并不直接存储“2024年5月20日 14:30:00”这种人类可读的时间它存储的是一个整数从1970年1月1日00:00:00 UTC协调世界时到当前时刻经过的秒数。这个起点被称为Unix Epoch也就是Unix纪元的零点。这个设计的好处非常明显时间成为一个单调递增的整数比较大小、做差值、排序都极其高效。但代价就是我们拿到的原始时间戳是不可读的必须通过转换函数把它变成年月日时分秒的格式。time函数返回的就是这个整数gettimeofday也是以这个整数为基础只不过额外多了微秒部分。理解这一点你就能明白为什么time函数只能精确到秒——它本质上就是把“流逝的秒数”直接抛给你。1.2 UTC、本地时间与时区同一个时间戳不同的人看到不同时刻UTC是全世界统一的时间基准可以粗浅理解为“伦敦时间”。但伦敦其实有夏令时严格说UTC是一种原子时标准不随季节变化。中国地区的本地时间等于UTC加8小时也就是UTC0800。这里有个关键认知时间戳本身没有时区概念无论你在北京还是纽约同一时刻time(NULL)返回的数值完全一样。变的是显示环节——localtime函数会把时间戳按照系统当前时区换算成“本地时间”gmtime则永远输出UTC时间。我在实际项目中见过不少同事把时间戳和时区混为一谈日志里打印的时间比实际时间差了8小时最后排查半天发现是目标机器的/etc/localtime或者TZ环境变量没有配置正确。1.3 时间戳精度带来的连锁影响time函数返回的time_t精度只有1秒。如果你在一个循环里连续调用两次time(NULL)只要循环体执行时间小于1秒两次返回值大概率完全相同。这在很多场景下够用但一旦涉及高频事件排序、性能统计、并发去重秒级时间戳就明显不够。这也是gettimeofday存在的意义——它把精度提升到微秒级足够覆盖绝大多数日常开发需求。2. time函数最基础的秒级时间接口2.1 函数原型与基础用法time函数的原型非常简单定义在头文件time.h中#include time.h time_t time(time_t *tloc);用法有两种等价姿势。第一种传NULL直接用返回值。第二种传入一个time_t变量的地址函数会把当前时间戳写入这个变量同时返回值也是当前时间戳。time_t t1 time(NULL); time_t t2; time(t2);两种写法结果一致实际代码中以time(NULL)的形式最为常见因为它少声明一个变量。需要特别注意的是返回值失败时time函数返回(time_t)-1也就是-1。虽然是极其古老且极少失败的函数但严谨一点的做法仍然要判断这个错误值避免把-1当成合法时间戳拿去格式化打印出一些莫名其妙的时间。2.2 把秒数变成可读时间localtime与strftime的标准组合拿到time_t之后通常要格式化成“2024-05-20 14:30:00”这样的可读字符串。标准做法是先用localtime把时间戳拆解成struct tm结构体再用strftime格式化输出#include stdio.h #include time.h int main(void) { time_t t time(NULL); if (t (time_t)-1) { perror(time); return 1; } struct tm *tm_local localtime(t); if (tm_local NULL) { perror(localtime); return 1; } char buf[128]; strftime(buf, sizeof(buf), %Y-%m-%d %H:%M:%S, tm_local); printf(本地时间: %s\n, buf); return 0; }strftime的格式占位符非常灵活%Y是四位数年份%m是两位月份%d是两位日期%H是24小时制小时%M是分钟%S是秒。想带毫秒都带不了因为struct tm压根没有毫秒字段。这个局限在写日志的时候特别明显——同一秒内打出的多条日志时间部分完全一模一样排障时根本分不清先后顺序。2.3 time函数适合的场景与不可避免的局限time函数适合对时间精度要求不高的场景生成按天切割的日志文件名、判断某个时间点是否已经过去、计算两个日期之间相差多少天用秒数差值除以86400、给数据库记录打一个粗糙的时间戳。局限也很明显秒级精度无法区分同一秒内的事件先后无法测量短于1秒的代码耗时无法生成高并发的唯一时间标识。我早期做日志模块时就是用time函数打时间戳结果同时刻并发请求一多日志时间全是一模一样的秒数客户端调用顺序都理不清后来才全面切到gettimeofday。3. gettimeofday微秒级精度的时间利器3.1 函数原型与struct timeval结构体gettimeofday定义在sys/time.h头文件中原型如下#include sys/time.h int gettimeofday(struct timeval *tv, struct timezone *tz);struct timeval包含两个字段struct timeval { time_t tv_sec; // 秒从Epoch开始的秒数 suseconds_t tv_usec; // 微秒取值范围 0 ~ 999999 };第二个参数struct timezone是当年用来获取时区信息的早已废弃在Linux Manual Page中明确建议传NULL。代码里如果真有人传了tzglibc会打印“gettimeofday warning: use of obsolete timezone”之类的提示新版glibc甚至可能直接忽略这个参数。所以我的建议很简单永远传NULL。3.2 基础示例一次调用取出秒和微秒#include stdio.h #include sys/time.h int main(void) { struct timeval tv; if (gettimeofday(tv, NULL) ! 0) { perror(gettimeofday); return 1; } printf(秒: %ld, 微秒: %06ld\n, (long)tv.tv_sec, (long)tv.tv_usec); return 0; }注意tv_usec是“当前秒内已经过去的微秒数”范围永远是0到999999。它不是一个独立的时间值而是对tv_sec的补充。比如时间戳是1716186600tv_usec是123456完整的时刻是1716186600.123456秒。有同事直接打印tv_usec当独立数值用排障的时候看半天对不上号这就是没理解结构体两个字段的关系。3.3 用gettimeofday测量代码执行耗时这是gettimeofday非常经典的用途在待测代码前后各取一次时间然后做差。函数本身精度微秒级对绝大多数业务代码的耗时统计都够用。核心是计算差值时要先转成double再减避免整数溢出问题#include stdio.h #include sys/time.h static double time_diff(const struct timeval *begin, const struct timeval *end) { return (end-tv_sec - begin-tv_sec) (end-tv_usec - begin-tv_usec) / 1000000.0; } int main(void) { struct timeval t1, t2; if (gettimeofday(t1, NULL) ! 0) { perror(gettimeofday); return 1; } // 模拟一段需要测量的操作 volatile int sum 0; for (int i 0; i 10000000; i) { sum i; } if (gettimeofday(t2, NULL) ! 0) { perror(gettimeofday); return 1; } printf(耗时: %.6f 秒\n, time_diff(t1, t2)); return 0; }这个方法简单直接不需要额外链接第三方库。但要注意微秒级测量本身就受系统调度、中断、CPU频率变化影响单次测量结果可能有波动。更可靠的做法是同一段代码跑多次取最小值或平均值。有人习惯取最小值认为接近真实执行时间我则倾向于把多次结果记录下来看分布因为线上延迟往往正是一些偶发的慢请求拖垮了整体体验。3.4 gettimeofday的精度与性能开销很多人误以为gettimeofday能看到“微秒级”就真的是微秒级精度。实际上它的精度依赖内核时钟和硬件时钟源真实精度通常在几微秒到几百微秒之间远达不到纳秒级。对业务开发来说够用但对需要精确测量极端短耗时比如几微秒级别函数的性能工程师来说就要考虑clock_gettime或者更底层的rdtsc指令了。gettimeofday本身是一个系统调用严格说每次调用都有用户态到内核态的切换开销。早期实现确实如此后来内核和glibc做了优化引入了vDSO机制在用户态就能读到时钟值gettimeofday的调用开销已经降到极低的水平大概几十纳秒到一百纳秒的量级。所以在绝大多数业务代码里频繁调用gettimeofday不会成为性能瓶颈不需要过度担心。4. time与gettimeofday怎么选精度、场景与开销对照4.1 两函数的对比一览为了直观对比我整理了一张表格对比维度timegettimeofday头文件time.hsys/time.h精度秒微秒实际受时钟源影响返回值time_t失败返回-10成功-1失败时间输出秒级整数tv_sec tv_usec组合主要用途日志时间戳、日期计算、定时判断耗时统计、精确排序、性能分析调用开销极低略高但有vDSO优化业务场景可忽略线程安全time本身安全但localtime不安全gettimeofday安全排除tz参数现代替代clock_gettime(CLOCK_REALTIME)clock_gettime(CLOCK_REALTIME)4.2 按实际场景选型什么时候用哪个我的选择逻辑大致是这样的记录日志且只需要精确到秒的场景直接用time。日志量大时时间字段足够简单存储也省空间。虽然精度不够会导致同一秒内日志无法排序但很多系统分析根本不需要这样细的粒度。接口耗时监控、慢查询分析、请求链路追踪必须用gettimeofday最好把微秒部分完整记录进日志。比如一个接口平均耗时30毫秒用秒级时间戳只能看到“这一秒处理了N个请求”完全看不出单个请求的延迟分布。生成文件名或幂等标识符如果并发量不高秒级加上进程号、自增序号也够用高并发场景则需要gettimeofday的微秒甚至可以升级到clock_gettime的纳秒。做定时器和超时判断优先用monotonic时钟也就是CLOCK_MONOTONIC而不是实时时钟。因为系统时间可能被人为修改或者被NTP校准实时时钟会跳变而单调时钟保证只增不减。这是time和gettimeofday都替代不了的场景。4.3 为什么不直接劝大家升级到clock_gettime既然 clock_gettime(CLOCK_REALTIME, ts) 能拿到纳秒而且在现代Linux内核上同样有vDSO加速为什么还要学老旧的time和gettimeofday原因有三第一存量代码大量使用time和gettimeofday你在维护项目时大概率要读这些代码。如果连最基本的时间戳获取方式都看不懂谈何改造。第二很多开源项目、数据库连接池、中间件SDK内部的超时判断和日志时间戳仍然基于这两个函数掌握了底层语义才能理解它们的运行逻辑。第三面试和笔试中time和gettimeofday是高频考点尤其是gettimeofday的timeval结构体成员、tz参数废弃、返回值判断这类细节。基础扎实了再往上理解clock_gettime只是顺水推舟。5. 实战中踩过的坑时区、溢出与线程安全5.1 时区设置导致的时间显示偏差最经典的坑就是“时间差了8小时”。time函数本身不受时区影响但localtime转换出来的显示值会根据系统的时区设置变化。系统时区由/etc/localtime文件决定TZ环境变量可以临时覆盖。我调试过一起线上事故同一个二进制程序部署在几十台机器上大部分机器日志正常个别机器日志时间比真实时间慢8小时。最终定位到是那几台机器的/etc/localtime被误配成了UTC。排查思路很简单在故障机器上执行date命令看系统时间再运行zdump /etc/localtime查看时区文件内容果然都是“UTC”。修复方法是用ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime重建时区软链接或者修改/etc/timezone并使用dpkg-reconfigure tzdata重新配置。如果你的程序需要固定使用某个时区不要依赖运行环境可以在启动初期显式设置TZ环境变量setenv(TZ, Asia/Shanghai, 1); tzset();这样即使部署到UTC机器上程序内部的时间显示也能保持一致。5.2 32位time_t溢出与2038年问题time_t本质上是一个有符号整数。在32位系统上time_t是32位最大值为2的31次方减1即2147483647。这个数字对应的UTC时间是2038年1月19日03:14:07。一旦时间戳超过这个值32位time_t会溢出变成负数导致所有时间相关的函数全部异常。当前绝大多数服务器是64位系统time_t是64位这个溢出在可预见的未来不会发生。但嵌入式设备、旧版ARM板子、某些精简内核的工控机上32位time_t依然存在。遇到这类环境要么确认内核和glibc是否支持64位time_t部分新版工具链可以编译-D_TIME_BITS64强制使用64位要么在业务层避免依赖2038年之后的时间计算要么直接放弃在这类设备上处理远期日期逻辑。5.3 localtime不是线程安全的多线程环境要用localtime_rtime本身是线程安全的返回值或者传入指针都是每线程独有的不存在数据竞争。但localtime和gmtime返回的是指向静态结构体的指针这个静态变量是线程共享的。多线程同时调用localtime后调用的线程可能覆盖前一个线程正在使用的数据导致时间错乱。我写服务端程序时就遇到过一次诡异现象日志打印的时间偶尔会变成“1970-01-01 08:00:00”排查发现是A线程调用了localtime还没等strftime格式化完成B线程也调用了localtime把A线程的struct tm内容覆盖成了异常值。正确做法是用可重入版本localtime_r#include stdio.h #include time.h int main(void) { time_t t time(NULL); struct tm tm_local; if (localtime_r(t, tm_local) NULL) { perror(localtime_r); return 1; } char buf[128]; strftime(buf, sizeof(buf), %Y-%m-%d %H:%M:%S, tm_local); printf(本地时间: %s\n, buf); return 0; }这个版本的struct tm由调用方自己分配线程间互不干扰。类似的还有gmtime_r。在写多线程服务时凡是涉及到localtime和gmtime的地方都需要检查一下是不是用了_r版本。5.4 gettimeofday的tv_usec格式化遗漏前导零用printf输出微秒时如果不加前导零时间戳就会出现“1716186600.1”和“1716186600.000001”这样的困惑。实际项目中日志格式统一很关键解析的时候才能对齐字段。规范的写法是printf(%ld.%06ld\n, (long)tv.tv_sec, (long)tv.tv_usec);%06ld的意思是最少占6位不足6位时左侧补零。这样输出的时间戳永远是“秒.六位微秒”的等长格式日志解析器和监控系统解析起来省心很多。如果后端脚本用split按点号切分能保证第二部分永远是6位微秒不会出现歧义。5.5 测量耗时结果异常时的排查思路如果发现用gettimeofday测量出的代码耗时结果忽大忽小甚至出现负值可以从几个方向排查第一确认前后两次调用没有用错结构体变量。有人复制代码时把两个gettimeofday都写成了同一个timeval变量导致begin和end永远相同耗时恒为0。第二确认系统没有发生大规模时间跳变。NTP校时或手动date -s修改系统时间会导致CLOCK_REALTIME突然跳跃。如果耗时进程跨过了这个跳变点end减begin就会出现异常大或者负值。解决方法是改用CLOCK_MONOTONIC但用gettimeofday本身是拿不到单调时钟的此时需要切换到clock_gettime。第三如果在虚拟机里测试时间测量更容易受宿主机调度影响。单次测量的结果要谨慎看待应该跑多轮取统计值而不是依赖单次数据。5.6 最后再分享一个实际开发中的小习惯我写日志时间戳模块时习惯把所有时间相关的封装收敛到一个统一函数中比如用一个get_timestamp_str()函数同时兼容秒级和微秒级配置内部按编译宏或者运行时配置切换time和gettimeofday。好处是以后想切换到clock_gettime时只改这一个文件即可不用全工程各处散落着time(NULL)和gettimeofday的原始调用。我见过太多代码仓库里时间获取方式五花八门有的用time有的用gettimeofday有的甚至自己读/proc/uptime或者syscall维护成本极高。基础函数本身不难难的是约定一致、处理完备、边界严谨。把这几个函数的语义吃透再制定一套统一的时间封装比临时抱佛脚查资料要稳妥得多。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

基于残差连接结合1D_CNN的滚动轴承故障诊断 2026/10/1 17:19:21

基于残差连接结合1D_CNN的滚动轴承故障诊断

一、试验数据 本次依旧采用凯斯西储大学滚动轴承数据集,数据集的划分形式和之前的帖子一样。 二、模型结构 本次的模型为1D_CNN结合残差网络的直连结构,残差结构直连能够避免深度梯度消失,一维卷积神经网络是运用一维卷积对一维时序序列进行特…

阅读更多 →
Python实现SIFT图像拼接:特征匹配与单应性矩阵实战 2026/10/1 17:19:21

Python实现SIFT图像拼接:特征匹配与单应性矩阵实战

简介:这份资源是面向计算机视觉初学者与课程设计学习者的Python图像拼接实战项目,围绕SIFT尺度不变特征变换算法展开,帮助读者理解从关键点检测到图像融合的完整流程。压缩包共8个文件,包含4个Python脚本、3张测试图片和1份说明文…

阅读更多 →
参考文献格式乱如麻?博导推荐这几个一键生成论文工具 2026/10/1 17:19:08

参考文献格式乱如麻?博导推荐这几个一键生成论文工具

论文写作总是被参考文献格式搞得焦头烂额?选题、大纲、初稿、文献整理、润色降重,每一个环节都可能成为拖延的借口。其实只要用对 AI 工具、走对流程,就能大幅提升效率——资深教授普遍推荐:千笔AI(中文全流程首选&…

阅读更多 →
AI接入SAP实战:Codex、WorkBuddy、豆包三条路径配置详解 2026/10/1 17:19:01

AI接入SAP实战:Codex、WorkBuddy、豆包三条路径配置详解

手上正好有一批SAP系统,业务那边天天抱怨查个物料库存要开五六个事务代码,报表导出来还得自己拼。我上个月接了个挺头疼的需求:把AI接进去,让业务直接用大白话问SAP“XX物料还有多少”、“那张采购单到哪一步了”。实测下来&#…

阅读更多 →
项目信息不全时如何生成博文?给出最小信息集即可 2026/10/1 17:19:01

项目信息不全时如何生成博文?给出最小信息集即可

我发现这次的输入内容里,项目标题和正文都是空的(标题显示为“【无标题】”,项目正文、关键词、摘要描述也都没有提供)。这种情况我没法凭空生成一篇围绕某个主题展开的博文——硬写的话,内容就会偏离你真正的意图&…

阅读更多 →
python代码如何单步运行 2026/10/1 17:19:01

python代码如何单步运行

这代码能够借助于诸多不同的途径来实施逐行运行的操作, 这些途径囊括了使用调试器、集成开发环境(即通常所讲的简称 IDE), 以及运用命令行工具。人们常常采纳的常规手段有: 调用其内部附带的 pdb 模块、利用集成开发环境(比如诸如 Code 这类软…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉