# 时间与日期
GMT 和 UTC
GMT,即格林尼治标准时间,也就是世界时。GMT 的正午是指当太阳横穿格林尼治子午线(本初子午线)时的时间。但由于地球自转不均匀不规则,导致 GMT 不精确,现在已经不再作为世界标准时间使用。
UTC,即协调世界时。UTC 是以原子时秒长为基础,在时刻上尽量接近于 GMT 的一种时间计量系统。为确保 UTC 与 GMT 相差不会超过 0.9 秒,在有需要的情况下会在 UTC 内加上正或负闰秒。UTC 现在作为世界标准时间使用。
所以,UTC 与 GMT 基本上等同,误差不超过 0.9 秒。
时区
地球自西向东旋转,东边比西边先看到太阳,东边的时间也比西边的早。为了统一世界的时间,1884 年的国际经度会议规规定将全球划分为 24 个时区(东、西各 12 个时区)。规定英国(格林尼治天文台旧址)为零时区(GMT+00),东 1-12 区,西 1-12 区,中国北京处于东 8 区(GMT+08)。
若中国当前时间为 8 点整,则英国时间为 0 点整。
UTC 时间与本地时间
UTC + 时区差 = 本地时间
时区差东为正,西为负。在此,把东八区时区差记为 +08
UTC + (+08) = 本地(北京)时间
UNIX 时间戳
由 Unix 内核提供的基本时间服务是自国际标准时间公元 1970 年 1 月 1 日 00:00:00 以来的秒数。
# 时间及时区查看命令
- 获取 UTC 世界时间
date -u
- 获取当地时间
date
- 获取当地时间及时差
date -R
# 时区设置命令
1、对于完整的 Linux 系统
- 命令跟描述对不上号的
tzselect
看起来很像一个时区选择的工具,但并非如此。事实上 tzselect 仅仅是一个查看时区表示方式的『向导』程序而已。通过依次询问大洲→国家→城市,最后告诉你如何 TZ 变量的写法,比如北京时间是: Asia/Shanghai
- TZ 变量
根据上面的指导,可以获知通过修改 TZ 变量,直接修改时区信息,例如:
date -R | ||
Tue, 17 Jan 2017 13:57:06 +0000 | ||
export TZ='Asia/Shanghai' | ||
date -R | ||
Tue, 17 Jan 2017 19:57:18 +0600 |
但如果不写在环境变量文件配置里的话,一般是会话级的操作,取消重新打开便会失效;因此对于在 shell 中实现更改 TZ 变量,只能做到临时变更时区信息。
正确做法是到 /etc/profile
里(或用户的 ~/.profile
或 ~/.bashrc
文件等),直接 export TZ='xxx'
更改时区(时区的名字可以用 tzselect
向导来确定)
/etc/localtime
文件
默认情况下情况下,TZ 属性是空,这时候是靠 /etc/localtime
文件来确定的时区。而此文件通常又是一个到 /usr/share/zoneinfo/
下各种时区文件的软连接。通过修改 /etc/localtime
指向的软连接,进而修改系统的时区。比如下面的方法,将 localtime 文件设置为了北京时间:
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime |
2、对于裁剪的 Linux 系统,如:arm linux
tzselect
命令
正常情况下,裁剪过的是不支持该命令的,因此相对于上面的 2、3 两点并不适用于此。
- TZ 变量
与上面一样可通过修改 TZ 变量,直接修改时区信息,但与此不同的是,并不能通过 TZ='Asia/Shanghai'
去修改,只能通过时区偏移量来修改,例如我们中国的北京时间相对于 UTC-0
的偏移量是 UTC+8
;那么则修改为:
date -R | ||
Tue, 17 Jan 2017 13:57:06 +0000 | ||
export TZ='CST-8' | ||
date -R | ||
Tue, 17 Jan 2017 19:57:18 +0600 |
注意,在这里,UTC+、- 是相反的,UTC-8 代表的是相对于 UTC 加八个小时,反之减八个小时......;而 CST 则是对应为北京时区缩写。
时区表信息可看:https://www.timeanddate.com/time/zones/
/etc/localtime
文件
由于裁剪问题,系统中是没有 /usr/share/zoneinfo/
文件夹的,如有需要,将 PC 端的 /usr/share/zoneinfo
整个 zoneinfo
文件夹复制到 rootfs 的 /usr/share
下,这样嵌入式系统中就有了 timezone
。
最后同样执行:
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime |
note:时区信息保存在 /etc/localtime
文件里,如果没有该文件则系统是零时区,有该文件时系统会去读取该文件。具体该文件的内容可以不同关心,在 /usr/share/zoneinfo/
目录下有各个时区对应的文件,只需要拷贝过去就可以。比如我们常用的东八区时间就是对应 /usr/share/zoneinfo/Asia/Shanghai
文件,只需要将该文件指向或拷贝到 /etc/localtime
就将系统时间改为东八区。
# 常用 API
# 时间结构体
struct tm { | |
int tm_sec; /* Seconds (0-60) */ | |
int tm_min; /* Minutes (0-59) */ | |
int tm_hour; /* Hours (0-23) */ | |
int tm_mday; /* Day of the month (1-31) */ | |
int tm_mon; /* Month (0-11) */ | |
int tm_year; /* Year - 1900 */ | |
int tm_wday; /* Day of the week (0-6, Sunday = 0) */ | |
int tm_yday; /* Day in the year (0-365, 1 Jan = 0) */ | |
int tm_isdst; /* Daylight saving time */ | |
}; |
struct timeval { | |
time_t tv_sec; /* seconds (秒) */ | |
suseconds_t tv_usec; /* microseconds (微秒) */ | |
}; | |
struct timezone { | |
int tz_minuteswest; /* minutes west of Greenwich */ | |
int tz_dsttime; /* type of DST correction */ | |
}; |
# 时间函数归类
1、C99 标准库函数
需 #include <time.h>
- 获取时间戳:
/* time () 函数,返回一个从 1970 年 1 月 1 日 00:00:00 到现在的 time_t 类型 UTC 时间,当参数为 NULL 时直接返回秒数,当然也会将该值写入 t 指针指向的地址。 */ | |
time_t time(time_t *t); | |
/* mktime () 会把本地时间转换为 UTC 时间 */ | |
time_t mktime(struct tm *tm); | |
//note:两者区别在于传入的参数结构体不同,mktime 存在时区转换;time (t) 等价于 mktime (localtime (time (t)))。 |
- 获取
struct tm
类型的时间:
/* gmtime () 是零时区,把 UTC 时间转换成北京时间的话,需要在年数上加 1900,月份上加 1,小时数加上 8。 */ | |
struct tm *gmtime(const time_t *timep); | |
struct tm *gmtime_r(const time_t *timep, struct tm *result); | |
/* localtime () 将得到本地时间,该函数与 gmtime () 唯一区别是,在转换成北京时间的小时数不需要加上 8。 */ | |
struct tm *localtime(const time_t *timep); | |
struct tm *localtime_r(const time_t *timep, struct tm *result); | |
//note:localtime 是将时区考虑在内了,转出的是当前时区的时间。但是注意,有些嵌入式设备上被裁减过的系统,时区没有被设置好,导致二者转出来的时间都是零时区的。在多线程应用里面,应该用后缀不带 `_r` 的函数,如: localtime_r 函数替代 localtime 函数,因为 localtime_r 是线程安全的,例子看第五大点。 |
- 时间日期格式化:
/* 将 tm 结构中的时间信息转换为相应时间的字符串 */ | |
char *asctime(const struct tm *tm); | |
char *asctime_r(const struct tm *tm, char *buf); | |
/* 将日历时间参数 timep 转换为一个表示本地当前时间的字符串,函数已经由时区转换成当地时间 */ | |
char *ctime(const time_t *timep); | |
char *ctime_r(const time_t *timep, char *buf); | |
//note:两者区别在于传入的参数结构体不同,但转换出来的信息格式显示是一样的;asctime 是直接把时间格式化,而 ctime 是经过时区转换后再格式化输出;ctime (t) 等价于 asctime (localtime (t))。 | |
/* 常用时间格式化参数看下方描述 */ | |
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm); |
在使用 strftime
时间格式化函数所涉及的相关参数的含义如下:
参数 | 含义 |
---|---|
%F | 将时间格式化为年 - 月 - 日 |
%T | 将时间格式化为显示时分秒: hh:mm:ss |
%Y | 将时间格式化为带世纪部分的十制年份 |
%m | 将时间格式化为十进制表示的月份 |
%d | 将时间格式化为十进制的每月中的第几天 |
%H | 将时间格式化为 24 小时制的小时 |
%M | 将时间格式化为十进制表示的分钟数 |
%S | 将时间格式化为十进制表示的秒数 |
更多参数请阅:https://www.runoob.com/cprogramming/c-function-strftime.html
- 获取时间差:
double difftime(time_t time1, time_t time0); |
2、Uinx 系统函数
需 #include <sys/time.h>
#include <sys/time.h> | |
获取 struct timeval 类型的时间: | |
/* 相对于 time () 和 mktime () ,gettimeofday () 能获取更精准的微秒级别,及相应的时区信息,需要注意的是 tz 是依赖于系统,不同的系统可能存在获取不到的可能,因此通常设置为 NULL */ | |
int gettimeofday(struct timeval *tv, struct timezone *tz); | |
// 成功则返回 0,失败返回 -1,错误代码存于 errno | |
// EFAULT:指针 tv 或 tz 所指的内存空间无效。 | |
设置 struct timeval 类型的时间: | |
int settimeofday(const struct timeval *tv,const struct timezone *tz); | |
// 成功则返回 0,失败返回 -1,错误代码存于 errno | |
// EPERM:调用进程没有足够权限调用 settimeofday (),即权限不够。 | |
// EINVAL:时区或其它内容无效,无法正确设置时间。 | |
//note:settimeofday 的修改时间需要在 root 权限下才能配置成功。 |
3、总结
time
、 gmtime
、 asctime
所表示的时间都是 UTC 时间,只是数据类型不一样,
而 mktime
、 localtime
、 ctime
的时间都存在时区之间变换。
函数 | 传参类型 | 返回类型 | 时区转换 | 描述 |
---|---|---|---|---|
time() | time_t | time_t | UTC+0 | 用于获取 UTC 零时区的时间戳格式 |
mktime() | struct tm | time_t | UTC-t | 用于获取 UTC 零时区的时间戳格式,但会经过时区转换,把本地时间内部换成 UTC+0 |
gmtime() | time_t | struct tm | UTC+0 | 用于获取 UTC 零时区的 |
localtime() | time_t | struct tm | UTC+t | |
asctime() | struct tm | string | UTC+0 | |
ctime() | time_t | string | UTC+t |
# 函数应用
1、获取当前时区的时间戳偏移量
time_t get_localtime_interval(void) | |
{ | |
time_t timep_utc, timep_local; | |
struct tm tm_utc; | |
time(&timep_utc); | |
gmtime_r(&timep_utc, &tm_utc); | |
timep_local = mktime(&tm_utc); | |
return (timep_utc - timep_local); | |
} |
2、获取当前时区的时间戳(带时区的本地时间)
time_t get_time_stamp(void) | |
{ | |
time_t timep; | |
time(&timep); | |
timep += get_localtime_interval(timep); | |
return timep; | |
} |
3、获取时区偏移量
time_t get_time_stamp(void) | |
{ | |
time_t timep_zone; | |
timep_zone = get_localtime_interval(timep) / 3600; | |
return timep_zone; | |
} |
4、时区设置函数
时间函数除了 gmttime ()、asctime () 不受环境变量 TZ 的影响外,大部分函数都受到环境变量 TZ 的影响,这几个函数是: localtime、mktime、ctime 和 strftime。如果定义了 TZ,则这些函数将使用其值以代替系统默认时区。
在 Unix 环境下可以通过改变系统文件修改环境变量,也可以通过函数 setenv () 修改。
TZ 指定了当前的系统时区。这个时区会影响我们所做的时间转换。例如假设当前的系统时间是 8:00AM,如果我们把当前的时区设置成东八区,则标准时间就是(即 UTC+0)的时间就是 8-8=0:00AM,如果是看成是东六区的话,则标准时间就变成了 8-6=2:00AM。
#include <stdio.h> | |
#include <time.h> | |
#include <stdlib.h> | |
int main(int argc, const char * argv[]) | |
{ | |
setenv("TZ", "CST-8", 1); // 北京东八区 | |
//setenv ("TZ", "UTC+0", 1); // 将当前时区设置成标准区 | |
char buf[64]; | |
struct timeval tv; | |
struct tm* tm_time; | |
gettimeofday(&tv, NULL); | |
tm_time = gmtime(&tv.tv_sec); | |
strftime(buf, sizeof(buf), "%a %b %m %H:%M:%S %Z %Y", tm_time); | |
printf("GMT time: %s\n", buf); | |
tm_time = localtime(&tv.tv_sec); | |
strftime(buf, sizeof(buf), "%a %b %m %H:%M:%S %Z %Y", tm_time); | |
printf("local time: %s\n", buf); | |
return 0; | |
} |
# localtime 和 localtime_r 区别
示例来源(下方为原文备份记录):https://blog.csdn.net/test1280/article/details/80917962
localtime
和 localtime_r
的函数功能: converts the calendar time timep to broken-time representation
在调用 localtime
和 localtime_t
函数时,需特别注意:
localtime
是不可重入函数,非线程安全localtime_r
是可重入函数,线程安全
1、使用 localtime
时不可重入示范:
#include <stdio.h> | |
#include <stdlib.h> | |
#include <unistd.h> | |
#include <time.h> | |
int main() | |
{ | |
time_t curTime = time(NULL); | |
time_t aftTime = curTime + 3600*3; | |
struct tm *pTm1 = localtime(&curTime); | |
struct tm *pTm2 = localtime(&aftTime); | |
fprintf(stdout, "%04d%02d%02d%02d%02d%02d\n", | |
pTm1->tm_year + 1900, | |
pTm1->tm_mon + 1, | |
pTm1->tm_mday, | |
pTm1->tm_hour, | |
pTm1->tm_min, | |
pTm1->tm_sec); | |
fprintf(stdout, "%04d%02d%02d%02d%02d%02d\n", | |
pTm2->tm_year + 1900, | |
pTm2->tm_mon + 1, | |
pTm2->tm_mday, | |
pTm2->tm_hour, | |
pTm2->tm_min, | |
pTm2->tm_sec); | |
return 0; | |
} |
编译 & 运行:
$ gcc -o main main.c | |
$ ./main | |
20180704225205 | |
20180704225205 |
调用 localtime
函数并获取其返回值(一个指向 struct tm
结构类型数据的指针)后,我们并未对返回值进行显式地释放。
这并没有什么问题(不会导致内存泄漏)。
因为 localtime
函数返回值是一个指针,指向一个静态变量,这个静态变量是库中的一个 static struct tm
类型数据。
man localtime:
The return value points to a statically allocated struct which might be overwritten by subsequent calls to any of the date and time functions.
这将引出新的问题,同一进程多个线程中同时调用(极短时间内连续调用) localtime
函数,返回值 tm
可能被覆盖。
举个栗子:
两个线程 A 和 B 同时调用 localtime
函数:
时刻 1:线程 A 调用 localtime
函数,得到一个指针,指向 static struct tm
类型变量;(tm 中存储的值更新为 value-a)
时刻 2:线程 B 调用 localtime
函数,得到一个指针,指向 static struct tm
类型变量;(tm 中存储的值更新为 value-b)
时刻 3:线程 A 对 localtime
返回的指针进行相关引用操作(例如 printf 输出某字段),此时 static struct tm
中的值实际是 value-b,并非预期的 value-a。
时刻 4:线程 B 对 localtime
返回的指针进行相关引用操作,此时 static struct tm
中的值实际是 value-b。
上面的示范代码虽然是在同一线程中,但是已经可以简单模拟这样的多线程执行调用流程。
如何解决?
localtime_r
是 localtime
的可重入版本(线程安全版本)。
localtime
不可重入是由于 static struct tm
是库中的一个静态变量,如果我们在调用 localtime
时传入一个 struct tm
类型变量(指针)用于存放结果,岂不是实现可重入?
Bingo!
struct tm *localtime(const time_t *timep);
struct tm *localtime_r(const time_t *timep, struct tm *result);
调用 localtime
只需要传入指向 time_t
的一个常量指针;
调用 localtime_t
不仅需要传入指向 time_t
的一个常量指针,还需要传入指向 struct tm
的一个指针,结果将存储在 result
指向的 struct tm
对象中;
The return value points to a statically allocated struct which might be overwritten by subsequent calls to any of the date and time functions.
The localtime_r() function does the same, but stores the data in a user-supplied struct.
2、使用 localtime_r
时可重入示范:
#include <stdio.h> | |
#include <stdlib.h> | |
#include <unistd.h> | |
#include <time.h> | |
int main() | |
{ | |
time_t curTime = time(NULL); | |
time_t aftTime = curTime + 3600*3; | |
struct tm tm1; | |
struct tm tm2; | |
localtime_r(&curTime, &tm1); | |
localtime_r(&aftTime, &tm2); | |
fprintf(stdout, "%04d%02d%02d%02d%02d%02d\n", | |
tm1.tm_year + 1900, | |
tm1.tm_mon + 1, | |
tm1.tm_mday, | |
tm1.tm_hour, | |
tm1.tm_min, | |
tm1.tm_sec); | |
fprintf(stdout, "%04d%02d%02d%02d%02d%02d\n", | |
tm2.tm_year + 1900, | |
tm2.tm_mon + 1, | |
tm2.tm_mday, | |
tm2.tm_hour, | |
tm2.tm_min, | |
tm2.tm_sec); | |
return 0; | |
} |
编译 & 运行:
$ gcc -o main main.c | |
$ ./main | |
20180704200531 | |
20180704230531 |
# 参考
https://www.cnblogs.com/sun-frederick/p/4772535.html
https://blog.csdn.net/test1280/article/details/80917962