本文是对我之前写的文章: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硬件来获取或设置时间信息。
1 int rtc_test(void) 2 { 3 struct rtc_time rtc; 4 int fd = -1; 5 int ret = -1; 6 fd = open("/dev/rtc0", O_RDWR); 7 if (fd < 0){ 8 return -1; 9 } 10 ret = ioctl(fd, RTC_RD_TIME, &rtc); 11 if (ret < 0){ 12 return -1; 13 } 14 printf(" CurrentRTC data/time is %d-%d-%d, %02d:%02d:%02d. ", rtc.tm_mday, rtc.tm_mon + 1, 15 rtc.tm_year + 1900, rtc.tm_hour, rtc.tm_min, rtc.tm_sec); 16 ret = ioctl(fd, RTC_SET_TIME, &rtc); 17 if (ret < 0){ 18 return -1; 19 } 20 return 0; 21 }
或者使用上面提到的hwclock命令来搞
1 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()函数可以充分体现出来。
1 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)找到出处:
1 /* 2 * The IDs of the various system clocks (for POSIX.1b interval timers): 3 */ 4 #define CLOCK_REALTIME 0 5 #define CLOCK_MONOTONIC 1 6 #define CLOCK_PROCESS_CPUTIME_ID 2 7 #define CLOCK_THREAD_CPUTIME_ID 3 8 #define CLOCK_MONOTONIC_RAW 4 9 #define CLOCK_REALTIME_COARSE 5 10 #define CLOCK_MONOTONIC_COARSE 6 11 #define CLOCK_BOOTTIME 7 12 #define CLOCK_REALTIME_ALARM 8 13 #define CLOCK_BOOTTIME_ALARM 9
为了使开发的时间库可以通用,还是需要用syscall先去判断一下,而不要用clock_gettime函数直接去搞
1 clockid_t get_monotonic_clockid() { 2 const clockid_t MY_CLOCK_MONOTONIC_RAW = 4; 3 4 timespec ts; 5 if (0 == syscall(SYS_clock_gettime, MY_CLOCK_MONOTONIC_RAW, &ts)) { 6 return MY_CLOCK_MONOTONIC_RAW; 7 } 8 return CLOCK_MONOTONIC; 9 }
时间相关的结构体说明
在我们介绍时间相关的那一坨函数之前,先来看看相应的一些结构体再说。
1、time_t
在time.h中的定义如下:
1 typedef __time_t time_t;
然后再看bits/types.h中的定义:
1 __STD_TYPE __TIME_T_TYPE __time_t; /* Seconds since the Epoch. */
再看bits/typesizes.h中的定义:
1 #define __TIME_T_TYPE __SLONGWORD_TYPE
最后在bits/types.h中看到:
1 #define __SLONGWORD_TYPE long int
绕了一大圈其实就是long int类型……
2、timeval
在bits/time.h中的定义如下:
1 /* A time value that is accurate to the nearest 2 microsecond but also has a range of years. */ 3 struct timeval 4 { 5 __time_t tv_sec; /* Seconds. */ 6 __suseconds_t tv_usec; /* Microseconds. */ 7 };
上面的__suseconds_t用追__time_t的方法追进去其实也是long int类型……
3、timespec
在time.h中的定义如下:
1 /* POSIX.1b structure for a time value. This is like a `struct timeval' but 2 has nanoseconds instead of microseconds. */ 3 struct timespec 4 { 5 __time_t tv_sec; /* Seconds. */ 6 long int tv_nsec; /* Nanoseconds. */ 7 };
其实有了上面介绍的3个类型,我们可以对获取时间的函数做出一个取舍了,这个我们后面再说。
4、tm
在time.h中的定义如下:
1 struct tm 2 { 3 int tm_sec; /* Seconds. [0-60] (1 leap second) */ 4 int tm_min; /* Minutes. [0-59] */ 5 int tm_hour; /* Hours. [0-23] */ 6 int tm_mday; /* Day. [1-31] */ 7 int tm_mon; /* Month. [0-11] */ 8 int tm_year; /* Year - 1900. */ 9 int tm_wday; /* Day of week. [0-6] */ 10 int tm_yday; /* Days in year.[0-365] */ 11 int tm_isdst; /* DST. [-1/0/1]*/ 12 13 #ifdef __USE_BSD 14 long int tm_gmtoff; /* Seconds east of UTC. */ 15 __const char *tm_zone; /* Timezone abbreviation. */ 16 #else 17 long int __tm_gmtoff; /* Seconds east of UTC. */ 18 __const char *__tm_zone; /* Timezone abbreviation. */ 19 #endif 20 };
好了,下面可以来看具体的函数了。注意编译的时候都要加上-lrt选项。
时间获取函数
1、time
1 time_t time(time_t*tloc);
需要包含头文件:<time.h>
返回的是自Epoch时间以来的秒数。
例子:
1 #include <stdio.h> 2 #include <time.h> 3 4 int main(int argc, char* argv[]) 5 { 6 time_t t = time(NULL); 7 printf("time: %d ", static_cast<int>(t)); 8 9 return 0; 10 }
运行结果:
time: 1513419614
2、gettimeofday
1 int gettimeofday(struct timeval* restrict tp, void* restrict tzp);
需要包含头文件:<sys/time.h>
其中第2个参数tzp的唯一合法值是NULL,其他值将产生不确定的结果。某些平台支持用tzp说明时区,但这完全依实现而定,Single UNIX Specification对此并没有定义。
因为返回的值存于结构体timeval中,所以是时间表示为秒和微秒。
例子:
1 #include <stdio.h> 2 #include <sys/time.h> 3 4 int main(int argc, char* argv[]) 5 { 6 timeval tp; 7 int ret = gettimeofday(&tp, NULL); 8 printf("ret: %d sec: %d usec: %d ", ret, tp.tv_sec, tp.tv_usec); 9 10 return 0; 11 }
运行结果:
ret: 0 sec: 1513418627 usec: 881813
3、clock_gettime
1 int clock_gettime(clockid_t clk_id, struct timespec* tp);
需要包含头文件:<sys/time.h>
里面关于第1个参数已经在上面详细介绍过了,这里就不说了。
因为返回的值存于结构体timespec中,所以是时间表示为秒和纳秒。
例子:
1 #include <time.h> 2 #include <stdio.h> 3 4 int main() 5 { 6 7 struct timespec time1 = {0, 0}; 8 9 clock_gettime(CLOCK_REALTIME, &time1); 10 printf("CLOCK_REALTIME: %d, %d ", time1.tv_sec, time1.tv_nsec); 11 12 clock_gettime(CLOCK_REALTIME_COARSE, &time1); 13 printf("CLOCK_REALTIME_COARSE: %d, %d ", time1.tv_sec, time1.tv_nsec); 14 15 clock_gettime(CLOCK_MONOTONIC, &time1); 16 printf("CLOCK_MONOTONIC: %d, %d ", time1.tv_sec, time1.tv_nsec); 17 18 clock_gettime(CLOCK_MONOTONIC_COARSE, &time1); 19 printf("CLOCK_MONOTONIC_COARSE: %d, %d ", time1.tv_sec, time1.tv_nsec); 20 21 clock_gettime(CLOCK_MONOTONIC_RAW, &time1); 22 printf("CLOCK_MONOTONIC_RAW: %d, %d ", time1.tv_sec, time1.tv_nsec); 23 24 clock_gettime(CLOCK_BOOTTIME, &time1); 25 printf("CLOCK_BOOTTIME: %d, %d ", time1.tv_sec, time1.tv_nsec); 26 27 clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &time1); 28 printf("CLOCK_PROCESS_CPUTIME_ID: %d, %d ", time1.tv_sec, time1.tv_nsec); 29 30 clock_gettime(CLOCK_THREAD_CPUTIME_ID, &time1); 31 printf("CLOCK_THREAD_CPUTIME_ID: %d, %d ", time1.tv_sec, time1.tv_nsec); 32 33 return 0; 34 }
运行结果:
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取毫秒会有溢出的问题。
1 inline long getCurrentTime() 2 { 3 struct timeval tv; 4 gettimeofday(&tv, NULL); 5 return tv.tv_sec * 1000 + tv.tv_usec / 1000; 6 }
这样写的话在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。
1 inline long long getCurrentTime() 2 { 3 struct timeval tv; 4 gettimeofday(&tv, NULL); 5 long long ms = tv.tv_sec; 6 return ms * 1000 + tv.tv_usec / 1000; 7 }
这就完美了。解决完这个bug,不禁想到当unix时间戳到了2,147,483,647
会是怎么办。这个问题早就有人考虑到了,叫做2038年问题。也就是到了2038年1月19日3时14分07秒
后,如果在32位设备上用long
类型再表示unix时间戳就溢出了。
溢出
时间打印函数
有很多这种函数,直接都写在一起得了。
1 char* asctime(const struct tm* tm); 2 char* asctime_r(const struct tm* tm, char* buf); // buf: 26 bytes at least 3 4 char* ctime(const time_t* timep); 5 char* ctime_r(const time_t* timep, char* buf); // buf: 26 bytes at least 6 7 struct tm* gmtime(const time_t* timep); 8 struct tm* gmtime_r(const time_t* timep, struct tm* result); 9 10 struct tm* localtime(const time_t* timep); 11 struct tm* localtime_r(const time_t* timep, struct tm* result); 12 13 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
例子:
1 #include <stdio.h> 2 #include <time.h> 3 #include <sys/time.h> 4 5 static void ShowTM(const char* desc, const tm* t) 6 { 7 printf("%s: ", desc); 8 printf("%d-%02d-%02d %02d:%02d:%02d ", 9 t->tm_year + 1900, t->tm_mon + 1, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); 10 printf("yday: %d wday: %d isdst: %d ", 11 t->tm_yday, t->tm_wday, t->tm_isdst); 12 } 13 14 int main(int argc, char* argv[]) 15 { 16 // time 17 time_t t = time(NULL); 18 printf("time: %d ", static_cast<int>(t)); 19 20 // ctime and ctime_r 21 char* c1 = ctime(&t); 22 printf("ctime: c1: %s ", c1); 23 24 char c20[64]; 25 char* c2 = ctime_r(&t, c20); 26 printf("ctime_r: c2: %s c20: %s ", c2, c20); 27 28 // gmtime and gmtime_r 29 tm* t1 = gmtime(&t); 30 ShowTM("gmtime-t1", t1); 31 32 tm t20; 33 tm* t2 = gmtime_r(&t, &t20); 34 ShowTM("gmtime_r-t2", t2); 35 ShowTM("gmtime_r-t20", &t20); 36 37 // localtime and localtime_r 38 tm* t3 = localtime(&t); 39 ShowTM("localtime-t3", t3); 40 41 tm t40; 42 tm* t4 = localtime_r(&t, &t40); 43 ShowTM("localtime_r-t4", t4); 44 ShowTM("localtime_r-t40", &t40); 45 46 // asctime and asctime_r 47 char* asc1 = asctime(t1); 48 printf("asctime-gmtime: %s ", asc1); 49 50 char asc20[128]; 51 char* asc2 = asctime_r(t1, asc20); 52 printf("asctime-gmtime: asc2: %s asc20: %s ", asc2, asc20); 53 54 char* asc3 = asctime(t3); 55 printf("asctime-localtime: %s ", asc3); 56 57 char asc40[128]; 58 char* asc4 = asctime_r(t3, asc40); 59 printf("asctime-localtime: asc4: %s asc40: %s ", asc4, asc40); 60 61 // mktime 62 time_t mkt1 = mktime(t1); 63 printf("mktime-gmtime: %d ", static_cast<int>(mkt1)); 64 65 time_t mkt2 = mktime(t3); 66 printf("mktime-localtime: %d ", static_cast<int>(mkt2)); 67 68 return 0; 69 }
运行结果:
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.csdn.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