15、时间与日期:time_t与struct tm、clock_gettime高精度计时、定时器实现
时间处理,是C语言命令行工具里绕不开的一环。你写个日志工具要打时间戳,做个性能分析器要精确计时,写个定时任务还得自己搓个定时器。我这些年踩过的坑,有一小半都跟时间有关——不是精度不够,就是跨平台炸了。
今天咱们就把C语言里跟时间相关的几个核心工具捋一遍。从最基础的time_t,到高精度的clock_gettime,再到一个实用的定时器实现。嗯,保证都是实战干货。
15.1 time_t 与 struct tm:日常时间处理
先说最常用的两个类型:time_t 和 struct tm。
time_t 本质上是个整数,代表从1970年1月1日0点(UTC)到现在的秒数。说白了就是个时间戳。而 struct tm 是个结构体,把时间拆成了年、月、日、时、分、秒这些人类看得懂的字段。
我刚开始写C的时候,老搞混这两个东西。后来总结了一个口诀:存用time_t,看用struct tm。
核心函数速查表
| 函数 | 作用 | 注意点 |
|---|---|---|
| time() | 获取当前时间戳(time_t) | 参数传NULL即可 |
| localtime_r() | time_t → struct tm(本地时间) | 线程安全,推荐用带_r的版本 |
| gmtime_r() | time_t → struct tm(UTC时间) | 同上 |
| mktime() | struct tm → time_t | 会修正tm字段的越界值 |
| strftime() | struct tm → 格式化字符串 | 类似printf,但格式符是%Y%m%d |
来看个实际例子。假设你要写个工具,打印当前时间并计算三天后的日期:
#include <stdio.h>
#include <time.h>
int main() {
time_t now;
struct tm local_tm, future_tm;
char buf[64];
// 获取当前时间戳
time(&now);
// 转为本地时间(线程安全)
localtime_r(&now, &local_tm);
// 格式化输出
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &local_tm);
printf("当前时间: %s\n", buf);
// 计算三天后:直接操作tm结构体
future_tm = local_tm;
future_tm.tm_mday += 3;
// mktime会处理跨月、跨年
time_t future = mktime(&future_tm);
localtime_r(&future, &future_tm);
strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &future_tm);
printf("三天后: %s\n", buf);
return 0;
}
避坑指南:我曾经在日志模块里直接用localtime(),结果多线程下时间戳全乱了。localtime()返回的是静态缓冲区,多线程调用会互相覆盖。记住:生产环境一律用localtime_r()和gmtime_r()。
15.2 clock_gettime:高精度计时
time_t只能精确到秒。你要测一段代码跑了多少毫秒甚至纳秒?那就得上 clock_gettime。
这个函数是POSIX标准里的,Linux、macOS都支持。它用 struct timespec 来存储时间,精度能达到纳秒级:
struct timespec {
time_t tv_sec; // 秒
long tv_nsec; // 纳秒
};
关键是要选对时钟源。我常用的有这几个:
| 时钟ID | 说明 | 适用场景 |
|---|---|---|
| CLOCK_REALTIME | 系统实时时间,可被用户调整 | 获取绝对时间戳 |
| CLOCK_MONOTONIC | 单调递增,不受系统时间调整影响 | 测量时间间隔(首选) |
| CLOCK_PROCESS_CPUTIME_ID | 进程消耗的CPU时间 | 性能分析 |
| CLOCK_THREAD_CPUTIME_ID | 线程消耗的CPU时间 | 线程级性能分析 |
注意:CLOCK_REALTIME在系统时间被NTP调整时可能往回跳。你测时间差如果用了它,可能得到负数。我有个同事就因为这个bug排查了一整天。测量耗时,永远用CLOCK_MONOTONIC。
写个简单的性能测量工具:
#include <stdio.h>
#include <time.h>
// 计算两个timespec的差值(纳秒)
long long timespec_diff_ns(struct timespec *start, struct timespec *end) {
return (end->tv_sec - start->tv_sec) * 1000000000LL
+ (end->tv_nsec - start->tv_nsec);
}
int main() {
struct timespec t1, t2;
volatile double sum = 0.0; // 防止被优化掉
// 开始计时
clock_gettime(CLOCK_MONOTONIC, &t1);
// 模拟一些计算
for (int i = 0; i < 1000000; i++) {
sum += i * 0.001;
}
// 结束计时
clock_gettime(CLOCK_MONOTONIC, &t2);
long long elapsed_ns = timespec_diff_ns(&t1, &t2);
printf("循环耗时: %lld 纳秒 (%.3f 毫秒)\n",
elapsed_ns, elapsed_ns / 1000000.0);
return 0;
}
我个人习惯把 timespec_diff_ns 这种函数放到一个公共工具头文件里,项目里到处都能用。你想想看,每次都要手写纳秒差值计算,多烦人。
15.3 定时器实现:从轮询到信号
命令行工具里经常需要定时执行某个操作。比如每5秒检查一次文件状态,或者每100毫秒采集一次数据。
最简单的做法是 sleep + 轮询。但精度差,而且sleep期间啥也干不了。更好的方案是用 timer_create + 信号。
来看一个实用的定时器框架:
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <time.h>
#include <unistd.h>
// 定时器回调函数
void timer_handler(union sigval sv) {
int id = *(int*)sv.sival_ptr;
struct timespec now;
clock_gettime(CLOCK_REALTIME, &now);
printf("[定时器 %d] 触发时间: %lld.%09ld\n",
id, (long long)now.tv_sec, now.tv_nsec);
}
int main() {
timer_t timer_id;
struct sigevent sev;
struct itimerspec its;
int id = 1;
// 配置定时器事件
sev.sigev_notify = SIGEV_THREAD; // 新线程执行回调
sev.sigev_value.sival_ptr = &id; // 传给回调的参数
sev.sigev_notify_function = timer_handler; // 回调函数
// 创建定时器
if (timer_create(CLOCK_REALTIME, &sev, &timer_id) == -1) {
perror("timer_create");
exit(1);
}
// 设置定时器:首次1秒后触发,之后每2秒触发一次
its.it_value.tv_sec = 1;
its.it_value.tv_nsec = 0;
its.it_interval.tv_sec = 2;
its.it_interval.tv_nsec = 0;
if (timer_settime(timer_id, 0, &its, NULL) == -1) {
perror("timer_settime");
exit(1);
}
printf("定时器已启动,等待触发...\n");
// 主线程做其他事情
sleep(10);
// 清理
timer_delete(timer_id);
printf("定时器已销毁\n");
return 0;
}
关键点解析:
SIGEV_THREAD:让定时器触发时在新线程里执行回调,不阻塞主流程it_value:首次触发时间it_interval:后续周期时间。设为0表示只触发一次- 记得在程序退出前调用
timer_delete清理资源
我曾经在做一个网络监控工具时,用这个定时器每100毫秒采集一次网卡流量。配合CLOCK_MONOTONIC做时间戳,精度完全够用。唯一要注意的是回调函数里别做太重的操作,否则定时器会堆积。
15.4 知识体系总览
下面这张图把本章的核心知识点串起来了。你一看就明白什么时候该用哪个工具:
我的个人建议:写命令行工具时,把时间处理封装成几个小函数放在util.h里。比如 get_timestamp_ms() 返回毫秒时间戳,format_time() 把time_t转成字符串。这样项目里所有地方都调用同一套代码,出问题也好修。
好了,时间处理这块就聊到这儿。从基础的time_t到高精度的clock_gettime,再到实用的定时器,每个工具都有它最合适的场景。你写代码的时候多想想:我到底是要看时间、测时间、还是等时间?选对了工具,事半功倍。