概览
开门见山先上图
界定一些术语,方便后面说明:
- GMT:格林威治平均时,太阳每天经过位于英国伦敦郊区的皇家格林威治天文台的时间为中午 12 点,1972 年之前使用的国际标准时间,因地球在它的椭圆轨道里的运动速度不均匀,这个时刻可能和实际的太阳时相差16分钟。
- UTC:国际标准时间,相当于本初子午线 (即经度0度) 上的平均太阳时。UTC 时间是经过平均太阳时 (以格林威治时间 GMT 为准)、地轴运动修正后的新时标以及以秒为单位的国际原子时所综合精算而成。
- Epoch:日历时间,自国际标准时间公元 1970 年 1 月 1 日 00:00:00 以来经过的秒数。
Unix 日期时间
获取
unix 通过接口 time 将 Epoch 作为整数返回,自然的包含了日期和时间两部分:
time_t time(time_t *tloc);
其中 time_t 在 64 位系统上是 8 字节整数 (long long):
sizeof (time_t) = 8
在 32 位系统上可能是 4 字节整数,没有试。
time 例程的 tloc 参数如果不为空,则时间值也存放在由 tloc 指向的单元内。
如果想获取更精准的时间,需要借助另外的接口:
int gettimeofday(struct timeval *tv, struct timezone *tz);
时间通过参数 tv 返回:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /* microseconds */
};
除了代表 UTC 的 tv_sec 外还有代表微秒的 tv_usec,注意如果只需要精确到毫秒,需要将这个值除以 1000。
在 64 位 CentOS 上它是 8 字节整数:
sizeof (suseconds_t) = 8, sizeof (struct timeval) = 16
不过不是所有 64 位系统这个字段都是 long long,在 64 位 Darwin 上它是 4 字节整数:
sizeof (suseconds_t) = 4, sizeof (struct timeval) = 16
但最终 timeval 结构体的长度还是 16,可能是内存对齐的缘故。
tz 参数用来指定时区信息:
struct timezone {
int tz_minuteswest; /* minutes west of Greenwich */
int tz_dsttime; /* type of DST correction */
};
因为一些原因,tz 在 SUS 标准中唯一合法值是 NULL,某些平台支持使用 tz 说明时区,但完全没有可移植性,例如在 Linux 上,建议这个参数设置为 NULL:
The use of the timezone structure is obsolete; the tz argument should normally be specified as NULL. (See NOTES below.)
不为 NULL 也不会报错,但是不会修改指定参数的内容。Darwin 支持这个参数,下面是它的日常返回:
minuteswest = -480, dsttime = 0
具体可参考时区和夏时制一节。
转换
time_t 类型利于接口返回,但可读性比较差,需要将它转换为人能理解的日期和时间。
struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* day of the month */
int tm_mon; /* month */
int tm_year; /* year */
int tm_wday; /* day of the week */
int tm_yday; /* day in the year */
int tm_isdst; /* daylight saving time */
};
这就是 struct tm,除了年月日时分秒,还有两个字段 wday / yday 用于方便的展示当前周/年中的天数,另外 isdst 标识了是否为夏时制 (参考夏时制一节)。
int tm_sec; /* seconds (0 - 60) */
int tm_min; /* minutes (0 - 59) */
int tm_hour; /* hours (0 - 23) */
int tm_mday; /* day of month (1 - 31) */
int tm_mon; /* month of year (0 - 11) */
int tm_year; /* year - 1900 */
int tm_wday; /* day of week (Sunday = 0) */
int tm_yday; /* day of year (0 - 365) */
int tm_isdst; /* is summer time in effect? */
char *tm_zone; /* abbreviation of timezone name */
long tm_gmtoff; /* offset from UTC in seconds */
上面给出了各个字段的取值范围,有几个点值得注意:
- 秒:可取值 60,这是因闰秒的原因 (参考闰秒一节)
- 年:从 1900 开始计数
- mday:从 1 开始计数,设置为 0 表示上月最后一天
- wday:从 0 开始计数,以周日作为第一天,因此 1 就是表示周一,以此类推
- isdst:
- > 0:夏时制生效
- = 0:夏时制不生效
- < 0:此信息不可用
- tm_zone 和 tm_gmtoff 两个字段是 Drawin 独有的,作用有点类似上面介绍过的 timezone,不属于 SUS 标准
如果直接用这个结构体显示给用户,经常会看到以下校正代码:
printf ("%04d/%02d/%02d %02d:%02d:%02d",
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec);
对 yday 的处理类似 mon。
再复习一下开始的关系图:
将 time_t 转换为 struct tm 的是 localtime 和 gmtime,反过来是 mktime:
struct tm *gmtime(const time_t *timep);
struct tm *localtime(const time_t *timep);
time_t mktime(struct tm *tm);
localtime 和 gmttime 的区别是,前者将 Epoch 转换为本地时间 (受时区、夏时制影响)、后者将 Epoch 转换为 UTC (不受时区、夏时制影响)。
mktime 只接受本地时间作为参数、将其转换为 Epoch,注意没有 mkgmtime 这类东东。
mktime 并不使用 tm 参数的所有字段,例如 wday 和 yday 就会被忽略,isdst 参数将按如下取值进行解释:
- > 0:启用夏时制
- = 0:禁用夏时制
- < 0:依据系统或环境设置自行决定是否使用夏时制
mktime 还会自动规范化 (normalize) 各个字段,例如 70 秒会被更新为 1 分 10 秒。除此之外,还有以下字段会被更新:
- wday:赋值
- yday:赋值
- isdst:
- 0:不生效
- > 0:生效
- 不再存在 < 0 的情况
极端的情况下,struct tm 中的每个字段都可能被修改,这也是参数 tm 没有加 const 修饰的原因。
利用 mktime 的 normalize 特性,很容易就可以求出 "N 年/月/日/时/分/秒" 前/后的时间,像下面这段代码:
#include "../apue.h"
#include <sys/time.h>
#include <time.h>
void print_tm (struct tm* t)
{
printf ("%04d-%02d-%02d %02d:%02d:%02d (week day %d) (year day %d) (daylight saving time %d)\n",
t->tm_year + 1900,
t->tm_mon + 1,
t->tm_mday,
t->tm_hour,
t->tm_min,
t->tm_sec,
t->tm_wday == 0 ? 7 : t->tm_wday,
t->tm_yday + 1,
t->tm_isdst);
}
int main (int argc, char *argv[])
{
if (argc < 2)
{
printf ("Usage: %s [+/-] [N[Y/M/D/H/m/S/w/y]]\n", argv[0]);
return 0;
}
int ret = 0;
time_t now = time (NULL);
printf ("sizeof (time_t) = %d, now = %ld\n", sizeof(time_t), now);
struct tm *tm_now = localtime (&now);
print_tm (tm_now);
int shift = 0;
char *endptr = 0;
shift = strtol (argv[1], &endptr, 10);
switch (*endptr)
{
case 'Y':
tm_now->tm_year += shift;
break;
case 'M':
tm_now->tm_mon += shift;
break;
case 'D':
case 'd':
tm_now->tm_mday += shift;
break;
case 'H':
case 'h':
tm_now->tm_hour += shift;
break;
case 'm':
tm_now->tm_min += shift;
break;
case 's':
case 'S':
tm_now->tm_sec += shift;
break;
/*
* tm_wday & tm_yday is ignored normally,
* here just do a test !!
*/
case 'w':
case 'W':
tm_now->tm_wday += shift;
break;
case 'y':
tm_now->tm_yday += shift;
break;
default:
printf ("unkonwn postfix %c", *endptr);
break;
}
print_tm (tm_now);
time_t tick = mktime (tm_now);
printf ("tick = %ld\n", tick);
print_tm (tm_now);
return 0;
}
运行时随意指定:
> ./timeshift +70s
sizeof (time_t) = 8, now = 1678544442
2023-03-11 22:20:42 (week day 6) (year day 70) (daylight saving time 0)
2023-03-11 22:20:112 (week day 6) (year day 70) (daylight saving time 0)
tick = 1678544512
2023-03-11 22:21:52 (week day 6) (year day 70) (daylight saving time 0)
观察到增加 sec 字段 70 秒后达到 112 秒,经过 mktime 规范化后变为 52 秒并向上进位 1 分钟。
这个 demo 还可以用来验证设置 wday 或 yday 没有效果,例如:
> ./timeshift +100y
sizeof (time_t) = 8, now = 1678544584
2023-03-11 22:23:04 (week day 6) (year day 70) (daylight saving time 0)
2023-03-11 22:23:04 (week day 6) (year day 170) (daylight saving time 0)
tick = 1678544584
2023-03-11 22:23:04 (week day 6) (year day 70) (daylight saving time 0)
直接被忽略了,yday 根据其它字段推导,恢复了 70 的初始值。
同时也可以验证 mday = 0 时其实是指上个月最后一天,例如:
> ./timeshift -11d
sizeof (time_t) = 8, now = 1678544711
2023-03-11 22:25:11 (week day 6) (year day 70) (daylight saving time 0)
2023-03-00 22:25:11 (week day 6) (year day 70) (daylight saving time 0)
tick = 1677594311
2023-02-28 22:25:11 (week day 2) (year day 59) (daylight saving time 0)
观察到 2023-03-00 其实是 2023-02-28。
闰秒
为了减少学习曲线,一些相对零碎的概念将在遇到的时候再行说明,闰秒就属于这种情况。在解释闰秒之前,先介绍两个新的术语:
- TAI:原子时间,基于铯原子的能级跃迁原子秒作为时标,结合了全球 400 个所有的原子钟而得到的时间,它决定了我们每个人的钟表中时间流动的速度
- UT:世界时间,也称天文时间,或太阳时,他的依据是地球的自转,我们用它来确定多少原子时对应于一个地球日的时间长度
在确定 TAI 起点之后&