# 时间与日期

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 系统

  1. 命令跟描述对不上号的 tzselect

看起来很像一个时区选择的工具,但并非如此。事实上 tzselect 仅仅是一个查看时区表示方式的『向导』程序而已。通过依次询问大洲→国家→城市,最后告诉你如何 TZ 变量的写法,比如北京时间是: Asia/Shanghai

  1. 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 向导来确定)

  1. /etc/localtime 文件

默认情况下情况下,TZ 属性是空,这时候是靠 /etc/localtime 文件来确定的时区。而此文件通常又是一个到 /usr/share/zoneinfo/ 下各种时区文件的软连接。通过修改 /etc/localtime 指向的软连接,进而修改系统的时区。比如下面的方法,将 localtime 文件设置为了北京时间:

ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

2、对于裁剪的 Linux 系统,如:arm linux

  1. tzselect 命令

正常情况下,裁剪过的是不支持该命令的,因此相对于上面的 2、3 两点并不适用于此。

  1. 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/

  1. /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、总结

timegmtimeasctime 所表示的时间都是 UTC 时间,只是数据类型不一样,
mktimelocaltimectime 的时间都存在时区之间变换。

函数传参类型返回类型时区转换描述
time()time_ttime_tUTC+0用于获取 UTC 零时区的时间戳格式
mktime()struct tmtime_tUTC-t用于获取 UTC 零时区的时间戳格式,但会经过时区转换,把本地时间内部换成 UTC+0
gmtime()time_tstruct tmUTC+0用于获取 UTC 零时区的
localtime()time_tstruct tmUTC+t
asctime()struct tmstringUTC+0
ctime()time_tstringUTC+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

localtimelocaltime_r 的函数功能: converts the calendar time timep to broken-time representation

在调用 localtimelocaltime_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_rlocaltime 的可重入版本(线程安全版本)。

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

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

夏沫の浅雨 微信支付

微信支付

夏沫の浅雨 支付宝

支付宝