15、时间与日期:time_t与struct tm、clock_gettime高精度计时、定时器实现

时间处理,是C语言命令行工具里绕不开的一环。你写个日志工具要打时间戳,做个性能分析器要精确计时,写个定时任务还得自己搓个定时器。我这些年踩过的坑,有一小半都跟时间有关——不是精度不够,就是跨平台炸了。

今天咱们就把C语言里跟时间相关的几个核心工具捋一遍。从最基础的time_t,到高精度的clock_gettime,再到一个实用的定时器实现。嗯,保证都是实战干货。

15.1 time_t 与 struct tm:日常时间处理

先说最常用的两个类型:time_tstruct 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 知识体系总览

下面这张图把本章的核心知识点串起来了。你一看就明白什么时候该用哪个工具:

C语言时间处理知识体系 日常时间处理 time_t 时间戳 struct tm 结构化时间 strftime 格式化 ⚠ 用 localtime_r 非 localtime 高精度计时 clock_gettime() struct timespec CLOCK_MONOTONIC ⚠ 测耗时用 MONOTONIC 定时器实现 timer_create() timer_settime() SIGEV_THREAD 回调 ⚠ 回调勿做重操作 选择原则:精度要求低用 time_t,高精度测量用 clock_gettime,周期性任务用 timer_create

我的个人建议:写命令行工具时,把时间处理封装成几个小函数放在util.h里。比如 get_timestamp_ms() 返回毫秒时间戳,format_time() 把time_t转成字符串。这样项目里所有地方都调用同一套代码,出问题也好修。

好了,时间处理这块就聊到这儿。从基础的time_t到高精度的clock_gettime,再到实用的定时器,每个工具都有它最合适的场景。你写代码的时候多想想:我到底是要看时间、测时间、还是等时间?选对了工具,事半功倍。


公众号:蓝海资料掘金营,微信deep3321