本文是对我之前写的文章:C++时间操作 的更深入补充。之前那个文章就是一个快速入门的东西,后面力图把一些更深入的细节补充完整。
时间分类的基本介绍
在介绍一些时间相关的操作函数之前,先来介绍一下linux/UNIX下面的几种常用的时间。
在内核中维护了以下的时间:
从大类别上分类的话,主要分为硬件时钟与系统时钟。硬件时钟是指主机板上的时钟设备,也就是通常可在BIOS画面设定的时钟。系统时钟则是指kernel中 的时钟。所有Linux相关指令与函数都是读取系统时钟的设定。因为存在两种不同的时钟,那么它们之间就会存在差异。根据不同参数设置,hwclock命令既可以将硬件时钟同步到系统时钟,也可以将系统时钟同步到硬件时钟。可以通过hwclock命令来读取或设置硬件时钟。
-r, --show 读取并打印硬件时钟(read hardware clock and print result )
-s, --hctosys 将硬件时钟同步到系统时钟(set the system time from the hardware clock
)
-w, --systohc 将系统时钟同步到硬件时钟(set the hardware clock to the current system time
)
1、硬件时钟:
(1)RTC时间
在PC中,RTC时间又叫CMOS时间,它通常由一个专门的计时硬件来实现,软件可以读取该硬件来获得年月日、时分秒等时间信息,而在嵌入式系统中,有使用专门的RTC芯片,也有直接把RTC集成到Soc芯片中,读取Soc中的某个寄存器即可获取当前时间信息。一般来说,RTC是一种可持续计时的,也就是说,不管系统是否上电,RTC中的时间信息都不会丢失,计时会一直持续进行,硬件上通常使用一个后备电池对RTC硬件进行单独的供电。因为RTC硬件的多样性,开发者需要为每种RTC时钟硬件提供相应的驱动程序,内核和用户空间通过驱动程序访问RTC硬件来获取或设置时间信息。
int rtc_test(void)
{
struct rtc_time rtc;
int fd = -;
int ret = -;
fd = open("/dev/rtc0", O_RDWR);
if (fd < ){
return -;
}
ret = ioctl(fd, RTC_RD_TIME, &rtc);
if (ret < ){
return -;
}
printf("\nCurrentRTC data/time is %d-%d-%d, %02d:%02d:%02d.\n", rtc.tm_mday, rtc.tm_mon + ,
rtc.tm_year + , rtc.tm_hour, rtc.tm_min, rtc.tm_sec);
ret = ioctl(fd, RTC_SET_TIME, &rtc);
if (ret < ){
return -;
}
return ;
}
或者使用上面提到的hwclock命令来搞
system("hwclock -w");
2、系统时钟:
(1)wall时间
wall时间也称为xtime,取决于用于对xtime计时的clocksource,它的精度甚至可以达到纳秒级别。因为xtime实际上是一个内存中的变量,它的访问速度非常快,内核大部分时间都是使用xtime来获得当前时间信息。xtime记录的是自Epoch(1970-01-01 00:00:00 UTC)到当前时刻所经历的纳秒数。
(2)monotonic时间
该时间自系统开机后就一直单调地增加,它不像xtime可以因用户的调整时间而产生跳变,不过该时间不计算系统休眠的时间,也就是说,系统休眠时,monotoic时间不会递增。
(3)raw monotonic时间
该时间与monotonic时间类似,也是单调递增的时间,唯一的不同是:raw monotonic time“更纯净”,不会受到NTP时间调整的影响,它代表着系统独立时钟硬件对时间的统计。
(4)boot时间
与monotonic时间相同,不过会累加上系统休眠的时间,它代表着系统上电后的总时间。
时间种类 | 精度(统计单位) | 访问速度 | 累计休眠时间 | 受NTP调整的影响 |
RTC | 低 | 慢 | Yes | Yes |
xtime | 高 | 快 | Yes | Yes |
monotonic | 高 | 快 | No | Yes |
raw monotonic | 高 | 快 | No | No |
boot time | 高 | 快 | Yes | Yes |
上面介绍的系统时间其实在linux系统调用的clock_gettime()函数可以充分体现出来。
int clock_gettime(clockid_t clk_id, struct timespec *tp);
在这里我们就简单的看一下,后面再详细介绍时间相关的函数。所以这里就只关注第一个参数clk_id,它支持的值就对应了上面我们提到的系统时间,而且比提到的稍微还多了一些时间支持。下面引用一下man手册里面对于clk_id的选项:
CLOCK_REALTIME:系统实时时间,从Epoch计时,可以被用户更改以及adjtime和NTP影响。
CLOCK_REALTIME_COARSE:系统实时时间,比起CLOCK_REALTIME有更快的获取速度,更低一些的精确度。(since Linux 2.6.32; Linux-specific)
CLOCK_MONOTONIC:从系统启动这一刻开始计时,即使系统时间被用户改变,也不受影响。系统休眠时不会计时。受adjtime和NTP影响。
CLOCK_MONOTONIC_COARSE:如同CLOCK_MONOTONIC,但有更快的获取速度和更低一些的精确度。受NTP影响。(since Linux 2.6.32; Linux-specific)
CLOCK_MONOTONIC_RAW:与CLOCK_MONOTONIC一样,系统开启时计时,不受NTP和adjtime影响。(since Linux 2.6.28; Linux-specific)
CLOCK_BOOTTIME: 从系统启动这一刻开始计时,包括休眠时间,受到settimeofday的影响。(since Linux 2.6.39; Linux-specific)
CLOCK_PROCESS_CPUTIME_ID: 本进程开始到此刻调用的时间。
CLOCK_THREAD_CPUTIME_ID: 本线程开始到此刻调用的时间。
内核新增的这些选项在以前不支持的时候只能通过某些系统调用syscall去搞,比如syscall(SYS_clock_gettime, CLOCK_MONOTONIC_RAW, &monotonic_time)。
update 2017-12-19:
上面的这些参数实际上可以从linux的uapi中(http://elixir.free-electrons.com/linux/latest/source/include/uapi/linux/time.h)找到出处:
/*
* The IDs of the various system clocks (for POSIX.1b interval timers):
*/
#define CLOCK_REALTIME 0
#define CLOCK_MONOTONIC 1
#define CLOCK_PROCESS_CPUTIME_ID 2
#define CLOCK_THREAD_CPUTIME_ID 3
#define CLOCK_MONOTONIC_RAW 4
#define CLOCK_REALTIME_COARSE 5
#define CLOCK_MONOTONIC_COARSE 6
#define CLOCK_BOOTTIME 7
#define CLOCK_REALTIME_ALARM 8
#define CLOCK_BOOTTIME_ALARM 9
为了使开发的时间库可以通用,还是需要用syscall先去判断一下,而不要用clock_gettime函数直接去搞
clockid_t get_monotonic_clockid() {
const clockid_t MY_CLOCK_MONOTONIC_RAW = ; timespec ts;
if ( == syscall(SYS_clock_gettime, MY_CLOCK_MONOTONIC_RAW, &ts)) {
return MY_CLOCK_MONOTONIC_RAW;
}
return CLOCK_MONOTONIC;
}
时间相关的结构体说明
在我们介绍时间相关的那一坨函数之前,先来看看相应的一些结构体再说。
1、time_t
在time.h中的定义如下:
typedef __time_t time_t;
然后再看bits/types.h中的定义:
__STD_TYPE __TIME_T_TYPE __time_t; /* Seconds since the Epoch. */
再看bits/typesizes.h中的定义:
#define __TIME_T_TYPE __SLONGWORD_TYPE
最后在bits/types.h中看到:
#define __SLONGWORD_TYPE long int
绕了一大圈其实就是long int类型……
2、timeval
在bits/time.h中的定义如下:
/* A time value that is accurate to the nearest
microsecond but also has a range of years. */
struct timeval
{
__time_t tv_sec; /* Seconds. */
__suseconds_t tv_usec; /* Microseconds. */
};
上面的__suseconds_t用追__time_t的方法追进去其实也是long int类型……
3、timespec
在time.h中的定义如下:
/* POSIX.1b structure for a time value. This is like a `struct timeval' but
has nanoseconds instead of microseconds. */
struct timespec
{
__time_t tv_sec; /* Seconds. */
long int tv_nsec; /* Nanoseconds. */
};
其实有了上面介绍的3个类型,我们可以对获取时间的函数做出一个取舍了,这个我们后面再说。
4、tm
在time.h中的定义如下:
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_BSD
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
};
好了,下面可以来看具体的函数了。注意编译的时候都要加上-lrt选项。
时间获取函数
1、time
time_t time(time_t*tloc);
需要包含头文件:<time.h>
返回的是自Epoch时间以来的秒数。
例子:
#include <stdio.h>
#include <time.h> int main(int argc, char* argv[])
{
time_t t = time(NULL);
printf("time: %d\n", static_cast<int>(t)); return ;
}
运行结果:
time: 1513419614
2、gettimeofday
int gettimeofday(struct timeval* restrict tp, void* restrict tzp);
需要包含头文件:<sys/time.h>
其中第2个参数tzp的唯一合法值是NULL,其他值将产生不确定的结果。某些平台支持用tzp说明时区,但这完全依实现而定,Single UNIX Specification对此并没有定义。
因为返回的值存于结构体timeval中,所以是时间表示为秒和微秒。
例子:
#include <stdio.h>
#include <sys/time.h> int main(int argc, char* argv[])
{
timeval tp;
int ret = gettimeofday(&tp, NULL);
printf("ret: %d\t sec: %d\t usec: %d\n", ret, tp.tv_sec, tp.tv_usec); return ;
}
运行结果:
ret: 0 sec: 1513418627 usec: 881813
3、clock_gettime
int clock_gettime(clockid_t clk_id, struct timespec* tp);
需要包含头文件:<sys/time.h>
里面关于第1个参数已经在上面详细介绍过了,这里就不说了。
因为返回的值存于结构体timespec中,所以是时间表示为秒和纳秒。
例子:
#include <time.h>
#include <stdio.h> int main()
{ struct timespec time1 = {, }; clock_gettime(CLOCK_REALTIME, &time1);
printf("CLOCK_REALTIME: %d, %d\n", time1.tv_sec, time1.tv_nsec); clock_gettime(CLOCK_REALTIME_COARSE, &time1);
printf("CLOCK_REALTIME_COARSE: %d, %d\n", time1.tv_sec, time1.tv_nsec); clock_gettime(CLOCK_MONOTONIC, &time1);
printf("CLOCK_MONOTONIC: %d, %d\n", time1.tv_sec, time1.tv_nsec); clock_gettime(CLOCK_MONOTONIC_COARSE, &time1);
printf("CLOCK_MONOTONIC_COARSE: %d, %d\n", time1.tv_sec, time1.tv_nsec); clock_gettime(CLOCK_MONOTONIC_RAW, &time1);
printf("CLOCK_MONOTONIC_RAW: %d, %d\n", time1.tv_sec, time1.tv_nsec); clock_gettime(CLOCK_BOOTTIME, &time1);
printf("CLOCK_BOOTTIME: %d, %d\n", time1.tv_sec, time1.tv_nsec); clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &time1);
printf("CLOCK_PROCESS_CPUTIME_ID: %d, %d\n", time1.tv_sec, time1.tv_nsec); clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time1);
printf("CLOCK_THREAD_CPUTIME_ID: %d, %d\n", time1.tv_sec, time1.tv_nsec); return ;
}
运行结果:
CLOCK_REALTIME: 1513420159, 235041261
CLOCK_REALTIME_COARSE: 1513420159, 234034596
CLOCK_MONOTONIC: 39739363, 412544528
CLOCK_MONOTONIC_COARSE: 39739363, 411488462
CLOCK_MONOTONIC_RAW: 39739427, 821184328
CLOCK_BOOTTIME: 39739363, 412569480
CLOCK_PROCESS_CPUTIME_ID: 0, 1418885
CLOCK_THREAD_CPUTIME_ID: 0, 1423199
综合上面的分析,在计时的时候,只使用 gettimeofday 来获取当前时间,主要原因如下:
(1)time 的精度太低,ftime 已被废弃,clock_gettime 精度最高,但是它系统调用的开销比 gettimeofday 大。
(2)在 x86-64 平台上,gettimeofday 不是系统调用,而是在用户态实现的(搜 vsyscall),没有上下文切换和陷入内核的开销。
(3)gettimeofday 的分辨率 (resolution) 是 1 微秒,足以满足日常计时的需要。
不过在使用gettimeofday的时候有个坑是需要注意的,那就是用gettimeofday取毫秒会有溢出的问题。
inline long getCurrentTime()
{
struct timeval tv;
gettimeofday(&tv, NULL);
return tv.tv_sec * + tv.tv_usec / ;
}
这样写的话在32位机器上是有坑的,因为tv.tv_sec * 1000后会溢出的。比如上面gettimeofday的例子中我们获取的值为1,
513,
418,
627,乘以1000为1,
513,
418,
627,
000,早就远大于long
在32位的机器上的范围为−2,147,483,648 ~ 2,147,483,647了
。那么解决这个问题需要把long类型转换为long long,或者不支持long long类型的时候转为double。
inline long long getCurrentTime()
{
struct timeval tv;
gettimeofday(&tv, NULL);
long long ms = tv.tv_sec;
return ms * + tv.tv_usec / ;
}
这就完美了。解决完这个bug,不禁想到当unix时间戳到了2,147,483,647
会是怎么办。这个问题早就有人考虑到了,叫做2038年问题。也就是到了2038年1月19日3时14分07秒
后,如果在32位设备上用long
类型再表示unix时间戳就溢出了。
溢出
时间打印函数
有很多这种函数,直接都写在一起得了。
char* asctime(const struct tm* tm);
char* asctime_r(const struct tm* tm, char* buf); // buf: 26 bytes at least char* ctime(const time_t* timep);
char* ctime_r(const time_t* timep, char* buf); // buf: 26 bytes at least struct tm* gmtime(const time_t* timep);
struct tm* gmtime_r(const time_t* timep, struct tm* result); struct tm* localtime(const time_t* timep);
struct tm* localtime_r(const time_t* timep, struct tm* result); time_t mktime(struct tm* tm);
需要包含头文件:<time.h>
asctime/asctime_r : 把tm所表示的日期和时间转为字符串, 且会自动转为本地时区
ctime/ctime_r : 把日期和时间转为字符串
gmtime/gmtime_r : 把time_t转换为tm,未经时区转换
localtime/localtime_r : 把time_t转换为tm,转为本地时区
mktime : 将tm转换为time_t,UTC
例子:
#include <stdio.h>
#include <time.h>
#include <sys/time.h> static void ShowTM(const char* desc, const tm* t)
{
printf("%s:\n", desc);
printf("%d-%02d-%02d %02d:%02d:%02d\n",
t->tm_year + , t->tm_mon + , t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec);
printf("yday: %d \t wday: %d \t isdst: %d\n\n",
t->tm_yday, t->tm_wday, t->tm_isdst);
} int main(int argc, char* argv[])
{
// time
time_t t = time(NULL);
printf("time: %d\n\n", static_cast<int>(t)); // ctime and ctime_r
char* c1 = ctime(&t);
printf("ctime: c1: %s\n", c1); char c20[];
char* c2 = ctime_r(&t, c20);
printf("ctime_r: c2: %s \t c20: %s \n\n", c2, c20); // gmtime and gmtime_r
tm* t1 = gmtime(&t);
ShowTM("gmtime-t1", t1); tm t20;
tm* t2 = gmtime_r(&t, &t20);
ShowTM("gmtime_r-t2", t2);
ShowTM("gmtime_r-t20", &t20); // localtime and localtime_r
tm* t3 = localtime(&t);
ShowTM("localtime-t3", t3); tm t40;
tm* t4 = localtime_r(&t, &t40);
ShowTM("localtime_r-t4", t4);
ShowTM("localtime_r-t40", &t40); // asctime and asctime_r
char* asc1 = asctime(t1);
printf("asctime-gmtime: %s\n", asc1); char asc20[];
char* asc2 = asctime_r(t1, asc20);
printf("asctime-gmtime: asc2: %s \t asc20: %s\n", asc2, asc20); char* asc3 = asctime(t3);
printf("asctime-localtime: %s\n", asc3); char asc40[];
char* asc4 = asctime_r(t3, asc40);
printf("asctime-localtime: asc4: %s \t asc40: %s\n", asc4, asc40); // mktime
time_t mkt1 = mktime(t1);
printf("mktime-gmtime: %d\n\n", static_cast<int>(mkt1)); time_t mkt2 = mktime(t3);
printf("mktime-localtime: %d\n", static_cast<int>(mkt2)); return ;
}
运行结果:
time: 1513421843 ctime: c1: Sat Dec 16 18:57:23 2017 ctime_r: c2: Sat Dec 16 18:57:23 2017
c20: Sat Dec 16 18:57:23 2017 gmtime-t1:
2017-12-16 10:57:23
yday: 349 wday: 6 isdst: 0 gmtime_r-t2:
2017-12-16 10:57:23
yday: 349 wday: 6 isdst: 0 gmtime_r-t20:
2017-12-16 10:57:23
yday: 349 wday: 6 isdst: 0 localtime-t3:
2017-12-16 18:57:23
yday: 349 wday: 6 isdst: 0 localtime_r-t4:
2017-12-16 18:57:23
yday: 349 wday: 6 isdst: 0 localtime_r-t40:
2017-12-16 18:57:23
yday: 349 wday: 6 isdst: 0 asctime-gmtime: Sat Dec 16 18:57:23 2017 asctime-gmtime: asc2: Sat Dec 16 18:57:23 2017
asc20: Sat Dec 16 18:57:23 2017 asctime-localtime: Sat Dec 16 18:57:23 2017 asctime-localtime: asc4: Sat Dec 16 18:57:23 2017
asc40: Sat Dec 16 18:57:23 2017 mktime-gmtime: 1513421843 mktime-localtime: 1513421843
本文参考自:
《APUE》第3版
http://elixir.free-electrons.com/linux/latest/source/include/uapi/linux/time.h
https://linux.die.net/man/2/clock_gettime
http://blog.****.net/droidphone/article/details/7989566
http://blog.chinaunix.net/uid-20662820-id-3880162.html
http://wuzhiwei.net/one_overflow_issue/
http://www.cnblogs.com/Solstice/archive/2011/02/06/1949555.html