第 10 章 时间 (Time)
核心结论
-
两种时间:Real time(日历时间或流逝时间)vs Process time(CPU 时间);分别由
time/gettimeofday和times/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 与 DST:
TZ环境变量控制时区;DST 通过tm_isdst字段控制;localtime_r与mktime都会考虑。 -
进程时间:
times()返回进程及子进程 CPU 时间(clock_t,需除以_SC_CLK_TCK);clock_gettime(CLOCK_PROCESS_CPUTIME_ID)高精度。 -
settimeofday/stime:设置系统时间(需要 root);NTP 守护进程用。
|
本章主旨
本章介绍 Linux 系统编程的两类时间:real time(日历时间)和 process time(CPU 时间)。重点是日历时间的各种表示( |
一、核心概念
本章围绕 6 个核心概念展开:从「时间表示」到「转换函数」再到「时区与进程时间」。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
time_t 与日历时间 |
整数类型;自 1970-01-01 UTC 起的秒数;32 位系统 2038 年溢出;用 |
§10.1;32 位最大日期 2038-01-19 03:14:07 UTC。 |
timeval 与高精度时间 |
|
§10.1;x86 现代系统精度 ±1 微秒;旧硬件可能毫秒级。 |
时间转换函数 |
|
§10.2; |
时区与 DST |
|
§10.3; |
Locale |
|
§10.4;不同 locale 下月份名称、星期名称不同。 |
进程时间 |
|
§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 时间转换函数
What:gmtime/localtime 把 time_t 转为 struct tm;mktime 反向;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
What:TZ 环境变量控制时区;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 设置系统时间
What:settimeofday() 和 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 速查)
|
四、思维导图
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 守护
五、重点与易错点
-
32 位 time_t 2038 年溢出:64 位系统无此问题;嵌入式 32 位仍需关注。
-
time() vs gettimeofday():前者秒级、后者微秒级;旧系统微秒不准确。
-
gmtime/localtime 返回静态结构:多线程必须用
_r版本。 -
ctime 共享缓冲区:与 gmtime、localtime、asctime 共享静态缓冲区;一个调用会覆盖另一个。
-
tm_isdst = -1 让 mktime 自动判断:业务代码推荐;显式 0/1 用于「我确定这是 DST/不是 DST」。
-
TZ 影响 localtime 和 mktime:服务器保持 UTC(
TZ=UTC);客户端用本地时区。 -
setlocale 影响 strftime 输出:国际化程序用
%c让 locale 决定格式。 -
CLOCK_REALTIME 会被 NTP 调整:用作时间间隔测量会被突然改变;用 CLOCK_MONOTONIC。
-
times() 返回时钟滴答:要除以
sysconf(_SC_CLK_TCK)才能转为秒。 -
settimeofday 需要 CAP_SYS_TIME:非 root 调用失败;普通应用不要用。
-
跨章衔接:第 23 章展开定时器(alarm、setitimer、timer_create);第 35 章展开调度(CPU 时间片分配)。