几种时间的说明
UTC:全称是Coordinated Universal Time,协调世界时间,它是一个与时区相关的时间,目前将世界时区分为24个,如下所示:
时区 | 时区范围 | 时区中心线 |
UTC | 7.5°W~7.5°E | 0° |
UTC+1 | 7.5°E~22.5°E | 15°E |
UTC+2 | 22.5°E~37.5°E | 30°E |
UTC+3 | 37.5°E~52.5°E | 45°E |
UTC+4 | 52.5°E~67.5°E | 60°E |
UTC+5 | 67.5°E~82.5°E | 75°E |
UTC+6 | 82.5°E~97.5°E | 90°E |
UTC+7 | 97.5°E~112.5°E | 105°E |
UTC+8 | 112.5°E~127.5°E | 120°E |
UTC+9 | 127.5°E~142.5°E | 135°E |
UTC+10 | 142.5°E~157.5°E | 150°E |
UTC+11 | 157.5°E~172.5°E | 165°E |
东西十二区 | 172.5°E~172.5°W | 180° |
UTC-11 | 172.5°W~157.5°W | 165°W |
UTC-10 | 157.5°W~142.5°W | 150°W |
UTC-9 | 142.5°W~127.5°W | 135°W |
UTC-8 | 127.5°W~112.5°W | 120°W |
UTC-7 | 112.5°W~97.5°W | 105°W |
UTC-6 | 97.5°W~82.5°W | 90°W |
UTC-5 | 82.5°W~67.5°W | 75°W |
UTC-4 | 67.5°W~52.5°W | 60°W |
UTC-3 | 52.5°W~37.5°W | 45°W |
UTC-2 | 37.5°W~22.5°W | 30°W |
UTC-1 | 22.5°W~7.5°W | 15°W |
UTC跟GMT(Greenwich Mean Time,格林威治时间)一致,因为格林威治位于0时区,UTC时间即是0时区的时间。中国位于东八区,所以对应的时间是UTC时间加8小时,即UTC+8,也称为CST(China Standard Time)。UTC倾向于跟标准相关的时间。UTC的查看可以通过date命令(注意需要加上-u,否则看到的是本地时间,即UTC+8的时间):
RTC:Real Time Clock,实时时钟。RTC和UTC不是一个概念上的时间,RTC通常指的是系统上一个计时的芯片,它将时间保存在CMOS中,而CMOS通过电池可以保持数据不丢失,因此在系统没有上电的情况下也还会正常运行。操作系统启动时会查看这个时间,也可以同步这个时间。RTC倾向于跟硬件相关的时间。查看RTC的命令如下(需要在root权限下查看,因为它涉及硬件,需要的权限较高):
Calendar Time:日历时间,指的是从当前时间到某个标准时间经历的秒数。这里的某个时间通常是指1970年1月1日0时0分0秒,这个时间点也被称为Epoch。日历时间是一个相对的时间值,倾向于跟软件相关。
时间相关的c函数和结构体
首先需要关注的是一个结构体(来自struct_tm.h),该结构体在之后介绍的时间函数中会使用到:
/* ISO C `broken-down time' structure. */
struct tm
{
int tm_sec; /* Seconds. [0-60] (1 leap second) */
int tm_min; /* Minutes. [0-59] */
int tm_hour; /* Hours. [0-23] */
int tm_mday; /* Day. [1-31] */
int tm_mon; /* Month. [0-11] */
int tm_year; /* Year - 1900. */
int tm_wday; /* Day of week. [0-6] */
int tm_yday; /* Days in year.[0-365] */
int tm_isdst; /* DST. [-1/0/1]*/
# ifdef __USE_MISC
long int tm_gmtoff; /* Seconds east of UTC. */
const char *tm_zone; /* Timezone abbreviation. */
# else
long int __tm_gmtoff; /* Seconds east of UTC. */
const char *__tm_zone; /* Timezone abbreviation. */
# endif
};
之后是几个重要的函数,下面将一一介绍。
首先是time()函数,其原型如下:
/* Return the current time and put it in *TIMER if TIMER is not NULL. */
extern time_t time (time_t *__timer) __THROW;
可以看到它的入参和返回值都是time_t,它实际上就是一个长整型,表示的就是从Epoch到函数调用之时经历的秒数,也就是前面提到的日历时间。为了得到这个时间,通常传入一个NULL作为参数,返回值就是我们需要的。下面是一个例子:
#include <stdio.h>
#include <time.h>
int main(int argc, char *argv[])
{
time_t t = time(NULL);
printf("time_t: %ld\n", t);
return 0;
}
运行程序得到的是:
time_t: 1606627854
用计算器简单地算下就可以得到它就是从1970年1月1日0时0分0秒开始到现在的一个值。当然用计算器算没必要,下面会介绍函数来处理这个值。
gmtime()函数会利用time_t的值,并将其转换为一个真正的时间,从函数名称就可以看出来是格林威治时间,也就是UTC的时间。与gmtime对应的就是转化为本地时间的函数localtime()。它们的函数原型如下:
/* Return the `struct tm' representation of *TIMER
in Universal Coordinated Time (aka Greenwich Mean Time). */
extern struct tm *gmtime (const time_t *__timer) __THROW;
/* Return the `struct tm' representation
of *TIMER in the local timezone. */
extern struct tm *localtime (const time_t *__timer) __THROW;
这两个函数的返回值就是前面提到的结构体strum tm,接收的参数就是time()函数的返回值time_t。下面是使用这两个函数的例子:
int main(int argc, char *argv[])
{
struct tm *tm;
time_t t = time(NULL);
printf("time_t: %ld\n", t);
tm = gmtime(&t);
printf("%d-%d-%d %d:%d:%d %s\n", tm->tm_year + 1900, tm->tm_mon + 1,
tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, tm->tm_zone);
tm = localtime(&t);
printf("%d-%d-%d %d:%d:%d %s\n", tm->tm_year + 1900, tm->tm_mon + 1,
tm->tm_mday, tm->tm_hour, tm->tm_min, tm->tm_sec, tm->tm_zone);
return 0;
}
得到的结果如下:
time_t: 1606629854
2020-11-29 6:4:14 GMT
2020-11-29 14:4:14 CST
这里需要注意的是struct tm里面年和月两个值的参考值,所以需要增加特定的偏移之后才是真实值。从打印的结果也可以看到本地时间和UTC时间之间的差异刚好是八小时。
上面的代码中我们自己构造了显示时间的字符串,实际上这可以通过ctime()和asctime()来完成,两者功能相同,但接收的参数不用:
/* Return a string of the form "Day Mon dd hh:mm:ss yyyy\n"
that is the representation of TP in this format. */
extern char *asctime (const struct tm *__tp) __THROW;
/* Equivalent to `asctime (localtime (timer))'. */
extern char *ctime (const time_t *__timer) __THROW;
有了这两个函数就不需要在自己构造字符串,下面是代码示例:
#include <stdio.h>
#include <time.h>
int main(int argc, char *argv[])
{
struct tm *tm;
time_t t = time(NULL);
printf("time_t: %ld\ntime: %s", t, ctime(&t));
tm = gmtime(&t);
printf("%s", asctime(tm));
tm = localtime(&t);
printf("%s", asctime(tm));
return 0;
}
得到的结果如下:
time_t: 1606630276
time: Sun Nov 29 14:11:16 2020
Sun Nov 29 06:11:16 2020
Sun Nov 29 14:11:16 2020
这里可以看出ctime()显示的是本地时间。两个函数返回的时间字符串还自带换行,所以不需要我们添加。
最后还要介绍的函数是mktime()。它实际上跟gmtime()和localtime()刚好相反,是将struct tm转换为time_t,其原型如下:
/* Return the `time_t' representation of TP and normalize TP. */
extern time_t mktime (struct tm *__tp) __THROW;
RTC与时间函数
前面介绍的这些函数,相关的时间只是UTC和日历时间,似乎与RTC时间没有直接的关系。要了解RTC与时间函数的关系,首先需要了解RTC时间本身。RTC时间其实是存放在CMOS中的时间,CMOS可以看成是一段非易失的介质,里面的内容是固定的,如下表所示:
而这里面的值会通过一个芯片按固定的时间修改,以保持时间的同步。time()函数的一个实现方式就是通过读取里面的值,然后与Epoch时间做减法,得到的时间间隔再转化为秒数,就可以作为返回值了。
时区
前面的内容中已经有介绍到时区相关的概念,比如UTC表示的是0时区的时间,也称为格林威治时间,UTC+8是东八区的时间,也就是我们这边的本地时间。
关于时区,在Linux下可以有很多种方式来查看,前面的说明中已经在显示时间的命令中看到,另外还可以通过几个文件来查看时区。如下所示:
这个文件比较好理解,就是一个放置了当前时区描述字符串的文件。另外还有一个:
这是一个软连接,最终指向的是一个二进制文件,而这样的二进制文件还有很多,想要改变时区可以通过修改链接来完成,当然直接覆盖并不是一个好主意,而应该通过相关的命令来实现,比如dpkg-reconfigure tzdata(Ubuntu可用),它会打开一个图形界面供设置:
修改之后相应的/etc/timezone和/etc/localtime都会改变。Linux下还提供了命令tzselect来修改时区:
但是这里只是告诉你怎么改,并没有什么实际效果,为了改变时区,还是需要依照它的说法来改,如下所示:
设置之后时间才会改变,由此也可以看出TZ的时区优先级是高于/etc/timezone和/etc/localtime的,如果要改回时区,就需要去掉TZ这个环境变量(光设置为空是没有用的):
最后需要关注的一点是这个/etc/localtime对应的二进制从哪里来,包含那些内容。这实际上是一个时区的数据库,它们由http://www.iana.org/time-zones维护,该组织定义了时区相关的信息,并提供相关的代码来操作时区相关的内容。
比如zdump.c就可以解析这里的二进制,如下所示: