第 10 章 时间 (Time)

      +

      核心结论

      • 两种时间:Real time(日历时间或流逝时间)vs Process time(CPU 时间);分别由 time/gettimeofdaytimes/clock_gettime 提供。

      • time_t 与 Epoch:日历时间用 time_t 表示秒数(自 1970-01-01 00:00:00 UTC);32 位系统 2038 年溢出;64 位无此问题。

      • gettimeofday:返回 timeval 结构(秒 + 微秒);tz 参数已废弃;x86 现代系统微秒精度。

      • time 转换函数gmtime(UTC)、localtime(本地时区)、mktime(本地→time_t)、ctime(time_t→字符串)、strftime(自定义格式化)、strptime(解析);_r 后缀版本可重入。

      • Timezone 与 DSTTZ 环境变量控制时区;DST 通过 tm_isdst 字段控制;localtime_rmktime 都会考虑。

      • 进程时间times() 返回进程及子进程 CPU 时间(clock_t,需除以 _SC_CLK_TCK);clock_gettime(CLOCK_PROCESS_CPUTIME_ID) 高精度。

      • settimeofday/stime:设置系统时间(需要 root);NTP 守护进程用。

      本章主旨

      本章介绍 Linux 系统编程的两类时间:real time(日历时间)和 process time(CPU 时间)。重点是日历时间的各种表示(time_tstruct tm、字符串)及相互转换函数;理解时区与 DST;了解进程时间的查询。本章是性能分析、定时操作、日志时间戳的基础。

      一、核心概念

      本章围绕 6 个核心概念展开:从「时间表示」到「转换函数」再到「时区与进程时间」。

      概念 定义 + 重要性 实现提示

      time_t 与日历时间

      整数类型;自 1970-01-01 UTC 起的秒数;32 位系统 2038 年溢出;用 time()gettimeofday() 获取。

      §10.1;32 位最大日期 2038-01-19 03:14:07 UTC。

      timeval 与高精度时间

      struct timeval { tv_sec, tv_usec };微秒精度;gettimeofday() 返回。

      §10.1;x86 现代系统精度 ±1 微秒;旧硬件可能毫秒级。

      时间转换函数

      gmtime/localtime(time_t→struct tm)、mktime(struct tm→time_t)、ctime/asctime(→字符串)、strftime/strptime(格式化/解析)。

      §10.2;_r 后缀版本可重入;多数函数返回静态分配结构。

      时区与 DST

      TZ 环境变量控制时区;tzset() 读取;DST 通过 tm_isdst 字段控制;mktime 会自动调整。

      §10.3;/etc/localtime 是时区数据;二进制格式(tzdata)。

      Locale

      setlocale(LC_ALL, "") 影响 strftime 输出;LC_TIME 控制日期/时间格式。

      §10.4;不同 locale 下月份名称、星期名称不同。

      进程时间

      times() 返回 tms 结构(utime/stime/cutime/cstime);clock_gettime(CLOCK_PROCESS_CPUTIME_ID) 纳秒精度。

      §10.6;性能分析基础。

      二、详细笔记

      10.1 日历时间:time_t 与 gettimeofday

      What:日历时间用 time_t 表示秒数(自 1970-01-01 00:00:00 UTC);gettimeofday() 返回微秒精度。

      Why:理解 time_t 是理解所有时间 API 的基础;2038 年问题在 32 位系统上需要关注。

      How

      // 摘自《The Linux Programming Interface》第 10 章
      #include <time.h>
      #include <sys/time.h>
      
      time_t time(time_t *timep);
      // 返回自 Epoch 起的秒数;同时存入 *timep(如果非 NULL)
      // 失败返回 (time_t) -1
      
      int gettimeofday(struct timeval *tv, struct timezone *tz);
      // 返回微秒精度时间
      // tz 已废弃,应传 NULL
      // 成功返回 0
      
      struct timeval {
          time_t      tv_sec;     // 秒
          suseconds_t tv_usec;    // 微秒
      };

      Epoch 起点:1970-01-01 00:00:00 UTC(UNIX 诞生之时);UTC 之前是 GMT(Greenwich Mean Time)。

      time_t 范围

      • 32 位有符号:1901-12-13 至 2038-01-19。

      • 64 位有符号:约 ±292 亿年。

      When

      • 需要时间戳——time(NULL)

      • 需要高精度时间——gettimeofday(&tv, NULL)

      • 32 位系统处理 2038 年之后的时间——升级到 64 位或用 int64_t 自定义时间表示。

      Example

      // 摘自《The Linux Programming Interface》第 10 章
      time_t t = time(NULL);
      printf("Seconds since Epoch: %ld\n", (long) t);
      
      struct timeval tv;
      gettimeofday(&tv, NULL);
      printf("%ld.%06ld seconds\n", (long) tv.tv_sec, (long) tv.tv_usec);

      10.2 时间转换函数

      Whatgmtime/localtimetime_t 转为 struct tmmktime 反向;ctime/asctime 转为字符串;strftime 自定义格式;strptime 解析。

      Why:程序通常要把 time_t 转为人可读格式(日志、时间戳);时区转换也要靠这些函数。

      How

      // 摘自《The Linux Programming Interface》第 10 章
      #include <time.h>
      
      struct tm *gmtime(const time_t *timep);
      // 转为 UTC;返回静态分配结构(不可重入)
      
      struct tm *localtime(const time_t *timep);
      // 转为本地时间(考虑 TZ 和 DST);返回静态分配结构
      
      time_t mktime(struct tm *timeptr);
      // 把本地时间的 struct tm 转为 time_t
      // 会规范化字段(如 sec=70 → min+1, sec=10)
      // 设置 tm_wday、tm_yday
      
      char *ctime(const time_t *timep);
      // 直接 time_t → 字符串("Wed Jun  8 14:22:34 2011\n")
      
      char *asctime(const struct tm *timeptr);
      // struct tm → 字符串(同 ctime 格式)
      
      size_t strftime(char *out, size_t maxsize, const char *format, const struct tm *t);
      // 自定义格式化;类似 printf 但用于时间
      // %Y 年 %m 月 %d 日 %H 时 %M 分 %S 秒 %Z 时区名
      
      char *strptime(const char *str, const char *format, struct tm *t);
      // 解析字符串到 struct tm(strftime 的反向)

      可重入版本:

      // 摘自《The Linux Programming Interface》第 10 章
      struct tm *gmtime_r(const time_t *timep, struct tm *result);
      struct tm *localtime_r(const time_t *timep, struct tm *result);
      char *ctime_r(const time_t *timep, char *buf);  // buf 至少 26 字节
      char *asctime_r(const struct tm *timeptr, char *buf);

      struct tm 字段:

      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)
          int tm_year;   // 年(自 1900 起)
          int tm_wday;   // 周几 (0-6, Sunday=0)
          int tm_yday;   // 年内第几天 (0-365)
          int tm_isdst;  // DST 标志:>0 DST 生效,=0 不生效,<0 未知
      };

      When

      • 显示当前时间——time + localtime + strftime

      • 计算时间差——mktime 把两个时间点转为 time_t 后相减。

      • 日志时间戳——ctime 或自定义 strftime

      • 多线程——用 _r 版本。

      Example

      // 摘自《The Linux Programming Interface》第 10 章 time/calendar_time.c
      time_t t = time(NULL);
      struct tm tm;
      localtime_r(&t, &tm);
      
      char buf[100];
      strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S %Z", &tm);
      printf("Current: %s\n", buf);

      10.3 时区与 DST

      WhatTZ 环境变量控制时区;DST(夏令时)通过 tm_isdst 字段控制;tzset()TZ 读取配置。

      Why:跨时区程序(服务器、客户端)需要正确处理时区;DST 处理错误会导致「一年一度的 bug」。

      How

      • TZ 环境变量

      • TZ=America/New_York → 用 tzdata 数据库。

      • TZ=EST5EDT,M3.2.0,M11.1.0 → POSIX 格式:标准名、UTC offset、DST 名、DST 开始/结束规则。

      • 未设置 → 用 /etc/localtime

      • tzset():读取 TZ 并初始化全局变量 tzname[0]tzname[1]timezone(UTC 偏移秒数)、daylight

      • tm_isdst

      • mktime 输入 > 0 → 强制当作 DST。

      • mktime 输入 = 0 → 强制当作标准时间。

      • mktime 输入 < 0 → 自动判断(推荐)。

      • mktime 输出 > 0 → DST 在该日期生效;= 0 → 不生效。

      When

      • 服务器程序需要 UTC——用 gmtime 而不是 localtime

      • 显示本地时间给用户——用 localtime + 用户 TZ。

      • DST 边界附近的逻辑——小心测试。

      Example

      # 设置时区
      $ TZ=America/New_York date
      Wed Jun  8 09:40:07 EDT 2011
      
      $ TZ=UTC date
      Wed Jun  8 13:40:07 UTC 2011
      
      $ TZ=Asia/Shanghai date
      Wed Jun  8 21:40:07 CST 2011

      10.4 Locale

      What:Locale 影响 strftime 输出(月份名、星期名、AM/PM);LC_TIME 控制时间格式;LC_ALL 控制所有类别。

      Why:国际化程序需要根据用户 locale 显示时间;否则中文用户看到英文月份名会困惑。

      How

      // 摘自《The Linux Programming Interface》第 10 章
      #include <locale.h>
      
      char *setlocale(int category, const char *locale);
      // category: LC_ALL / LC_TIME / LC_NUMERIC / ...
      // locale: ""(用环境变量)、"C"、"en_US.UTF-8"、"zh_CN.UTF-8"
      // 返回新 locale 名称
      
      struct lconv *localeconv(void);
      // 返回数字/货币格式信息

      strftime 在不同 locale 下输出不同:

      // 摘自《The Linux Programming Interface》第 10 章
      setlocale(LC_TIME, "");  // 用环境变量 LANG
      struct tm tm = { ... };
      strftime(buf, sizeof(buf), "%c", &tm);  // %c = 本地化的日期时间
      // C locale: "Wed Jun  8 14:22:34 2011"
      // zh_CN.UTF-8: "2011年06月08日 14时22分34秒"

      When

      • 国际化程序——setlocale(LC_ALL, "") 然后用 %c%x %X

      • 服务器程序——保持 "C" locale(避免不同 locale 下日志格式不同)。

      • 解析用户输入——strptime 也受 locale 影响。

      Example

      // 摘自《The Linux Programming Interface》第 10 章
      setlocale(LC_TIME, "zh_CN.UTF-8");  // 设置中文 locale
      time_t t = time(NULL);
      struct tm tm;
      localtime_r(&t, &tm);
      char buf[100];
      strftime(buf, sizeof(buf), "%c", &tm);
      // 输出中文格式日期

      10.5 进程时间

      What:进程时间 = 进程使用的 CPU 时间;分 user time(用户态)和 system time(内核态);times() 返回,clock_gettime 高精度。

      Why:性能分析、限流、计费系统需要知道「进程实际消耗多少 CPU 时间」(与 wall-clock 不同)。

      How

      // 摘自《The Linux Programming Interface》第 10 章
      #include <sys/times.h>
      #include <time.h>
      
      clock_t times(struct tms *buf);
      // 返回自系统启动起的时钟滴答数(需除以 _SC_CLK_TCK)
      // 同时填充 struct tms
      
      struct tms {
          clock_t tms_utime;   // 用户态 CPU 时间
          clock_t tms_stime;   // 内核态 CPU 时间
          clock_t tms_cutime;  // 已终止子进程的用户态总和
          clock_t tms_cstime;  // 已终止子进程的内核态总和
      };
      
      int clock_gettime(clockid_t clk_id, struct timespec *tp);
      // 高精度时间(纳秒);clk_id:
      //   CLOCK_REALTIME: 真实时间
      //   CLOCK_MONOTONIC: 单调递增时间(不受 NTP 调整影响)
      //   CLOCK_PROCESS_CPUTIME_ID: 调用进程 CPU 时间
      //   CLOCK_THREAD_CPUTIME_ID: 调用线程 CPU 时间
      //   CLOCK_MONOTONIC_RAW: 类似 MONOTONIC 但不受 NTP 调整
      
      struct timespec {
          time_t tv_sec;
          long   tv_nsec;  // 纳秒(0-999999999)
      };

      When

      • 性能分析——clock_gettime(CLOCK_MONOTONIC) 测量 wall-clock;CLOCK_PROCESS_CPUTIME_ID 测量 CPU 时间。

      • 限流——比较进程 CPU 时间是否超过阈值。

      • 计时器——CLOCK_MONOTONIC 不会因 NTP 而倒退,适合用作定时器基准。

      Example

      // 摘自《The Linux Programming Interface》第 10 章
      struct timespec start, end;
      clock_gettime(CLOCK_MONOTONIC, &start);
      do_work();
      clock_gettime(CLOCK_MONOTONIC, &end);
      
      double elapsed = (end.tv_sec - start.tv_sec) +
                       (end.tv_nsec - start.tv_nsec) / 1e9;
      printf("Elapsed: %.3f seconds\n", elapsed);

      10.6 设置系统时间

      Whatsettimeofday()stime() 设置系统时间;需要 root 权限;NTP 守护进程用。

      Why:容器化环境、虚拟化平台、嵌入式设备常需要程序化设置时间。

      How

      // 摘自《The Linux Programming Interface》第 10 章
      #include <sys/time.h>
      
      int settimeofday(const struct timeval *tv, const struct timezone *tz);
      // 设置系统时间;需要 CAP_SYS_TIME
      // tz 已废弃,传 NULL
      // 成功返回 0

      警告

      • 突然改变系统时间可能破坏 make(1)、日志轮转、定时任务。

      • 现代系统用 timedatectl(systemd)或 ntpdate/chrony 同步时间。

      When

      • 嵌入式设备无 RTC——启动后从外部源设置时间。

      • 测试环境——模拟特定时间。

      • NTP 同步——chrony/ntpd 守护进程调用。

      三、关键图表

      非可视化条目(关键 API 速查)
      API 用途

      time(t)

      获取当前时间(秒);time_t

      gettimeofday(&tv, NULL)

      获取当前时间(微秒)

      gmtime/localtime

      time_t → struct tm;UTC 或本地

      mktime(&tm)

      struct tm → time_t;规范化字段

      ctime/asctime

      转固定格式字符串

      strftime

      自定义格式化

      strptime

      字符串 → struct tm

      TZ 环境变量

      控制时区

      setlocale(LC_TIME, "")

      设置 locale

      times(&tms)

      进程 CPU 时间(utime/stime)

      clock_gettime

      高精度时间(纳秒);多种 clock

      CLOCK_MONOTONIC

      单调递增时间;不受 NTP 影响

      四、思维导图

      mindmap
        root((第 10 章 时间))
          日历时间
            time_t
            Epoch 1970
            32 位 2038 问题
            gettimeofday 微秒
          时间转换
            gmtime UTC
            localtime 本地
            mktime 反向
            ctime asctime
            strftime strptime
            _r 可重入
          时区 DST
            TZ 环境变量
            tzset
            tm_isdst
            localtime mktime
          Locale
            setlocale
            LC_TIME
            strftime 本地化
            C locale zh_CN
          进程时间
            times tms
            utime stime
            clock_gettime
            CLOCK_MONOTONIC
            CLOCK_PROCESS_CPUTIME
          设置时间
            settimeofday
            需要 root
            NTP 守护

      五、重点与易错点

      1. 32 位 time_t 2038 年溢出:64 位系统无此问题;嵌入式 32 位仍需关注。

      2. time() vs gettimeofday():前者秒级、后者微秒级;旧系统微秒不准确。

      3. gmtime/localtime 返回静态结构:多线程必须用 _r 版本。

      4. ctime 共享缓冲区:与 gmtime、localtime、asctime 共享静态缓冲区;一个调用会覆盖另一个。

      5. tm_isdst = -1 让 mktime 自动判断:业务代码推荐;显式 0/1 用于「我确定这是 DST/不是 DST」。

      6. TZ 影响 localtime 和 mktime:服务器保持 UTC(TZ=UTC);客户端用本地时区。

      7. setlocale 影响 strftime 输出:国际化程序用 %c 让 locale 决定格式。

      8. CLOCK_REALTIME 会被 NTP 调整:用作时间间隔测量会被突然改变;用 CLOCK_MONOTONIC。

      9. times() 返回时钟滴答:要除以 sysconf(_SC_CLK_TCK) 才能转为秒。

      10. settimeofday 需要 CAP_SYS_TIME:非 root 调用失败;普通应用不要用。

      11. 跨章衔接:第 23 章展开定时器(alarm、setitimer、timer_create);第 35 章展开调度(CPU 时间片分配)。