Linux 下 C/C++ 的时间编程

Linux 下 C/C++ 的时间编程

一、Linux 下时间类型

  • real time:日历时间。对于 Linux 中,这个时间的起点是 1970年1月1日00点,Linux 上以此为起点的均为 UTC 时间。

    格林威治时间(Greenwich Mean Time,GMT)也被称为世界标准的时间(Coordinated Universal Time,UTC)。UTC 和 GMT 两者是同一概念的时间。区别在于 UTC 是天文学上的概念,而 GMT 是基于一个原子钟。

    GMT 是中央时区,北京在东8区,相差8小时,所以 北京时间 = GMT时间 + 8小时

    注意:会受到修改系统时间的命令/api 或者 ntp 服务的影响,导致时间出现跳跃

  • monotonic time:单调时间。意为不能被设置和影响的时间,它可以提供精确的时间信息,不会出现时间跳跃。单调时间的起点 posix 标准并没有明确指定,但在 Linux 上是以系统启动的时间为起点的。虽然说单调时钟的时间是稳定的,但它会被 adjtime 函数和 ntp 服务影响,同时当系统挂起或休眠时计时会被暂停。

二、Linux 下时间格式

1. time_t 时间类型

1
2
3
4
#ifndef __TIME_T
#define __TIME_T
typedef long time_t
#endif

time_t 是一个长整型,其值表示从 UTC 时间(1970年1月1日00时00分00秒)到当前时刻的秒数。由于 time_t 类型长度限制,它所表示的时间不能晚于 2038年1月19日03时14分07秒(UTC)。

2. struct tm 时间类型

1
2
3
4
5
6
7
8
9
10
11
12
#include <time.h>
struct tm {
int tm_sec; // 秒,取值为 [0, 59]
int tm_min; // 分,取值为 [0, 59]
int tm_hour; // 时,取值为 [0, 23]
int tm_mday; // 日期,取值为 [1, 31]
int tm_mon; // 月份,取值为 [0, 11]
int tm_year; // 年份,其值为 1900 年至今的年数
int tm_wday; // 星期,取值为 [0, 6], 0 代表星期天,1 代表星期一,以此类推
int tm_yday; // 从年的1月1日开始的天数,取值为 [0, 365], 0 代表1月1日
int tm_isdst; // 夏令时标识符,使用夏令时,tm_isdst为正;不使用夏令时,tm_isdst为0;不了解情况时,tm_isdst为负
};

3. struct timeval 时间类型

1
2
3
4
5
#include <sys/time.h>
struct timeval {
time_t tv_sec; /* seconds:秒 */
suseconds_t tv_usec; /* microseconds:微妙 */
};

tv_sec 是 time_t 时间类型,其值表示从 UTC 时间 1970年1月1日00时00分00秒到当前时刻的秒数

4. struct timespec 时间类型

1
2
3
4
5
typedef long time_t;
struct timespec {
time_t tv_sec; /* seconds:秒 */
long tv_nsec; /* microseconds:纳妙 */
};

它是POSIX.4 标准定义的时间结构,精确度到纳秒,一般由 clock_gettime(clockid_t, struct timespec *) 获取特定时钟的时间。常用如下4种时钟:

  • CLOCK_REALTIME 系统当前时间,从1970年1月1日算起
  • CLOCK_MONOTONIC 系统的启动时间,不能被设置
  • CLOCK_PROCESS_CPUTIME_ID 本进程运行时间
  • CLOCK_THREAD_CPUTIME_ID 本线程运行时间

三、Linux 时间编程接口

1. time() 函数

1
2
#include <time.h>
time_t time(time_t *tloc);
  • 说明:该函数用于获取日历时间,即从 1970年1月1日00点到现在所经历的秒数。
  • 参数:参数 tloc 通常设置为 NULL,若 tloc 不为空,time() 函数也会将返回值存到 tloc 中。
  • 返回值:函数执行成功返回秒数,失败则返回 (time_t)-1 ,错误原因存在 errno

2. 时间转换函数 gmtime()、localtime()、ctime()、asctime()、mktime()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <time.h>
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);

char *ctime(const time_t *timep);
char *ctime_r(const time_t *timep, char *buf);

char *asctime (const struct tm *__tp)
char *asctime_r(const struct tm *tm, char *buf);

time_t mktime (struct tm *__tp);
  • gmtime 函数将 time_t 类型的日历时间转换成 struct tm 结构体表示的格林威治时间
  • localtime 函数将 time_t 类型的日历时间转换成 struct tm 结构体表示的本地时区时间
  • ctime 函数将 time_t 类型的日志时间转换为本地时区时间的字符串形式
  • asctime 函数将 struct tm 结构体转换为字符串形式
  • mktime 函数将 struct tm 结构体转换为从 1970年1月1日00时00分00秒至今的 GMT 时间经过的秒数
1
2
3
4
5
6
7
8
time_t t = time(nullptr);
std::cout << t << std::endl;
// 本地时区时间
struct tm* t2 = localtime(&t);
std::cout << "Local hour: " << t2->tm_hour << std::endl;
// 格林威治时间
struct tm* t3 = gmtime(&t);
std::cout << "UTC hour: " << t3->tm_hour << std::endl;

3. 时间比较函数 difftime()

1
double difftime (time_t __time1, time_t __time0)

比较两个时间是是否相同,并返回之间相差的秒数

4. gettimeofday()、settimeofday()

1
2
int gettimeofday (struct timeval *__restrict __tv, void *__restrict __tz) 
int settimeofday (const struct timeval *__tv, const struct timezone *__tz)
  • gettimeofday 函数用于获取 UTC 时间1970年1月1日00时00分00秒到当前时刻的时间差,并将此时间存入 struct timeval 结构体中,当地时区信息则放到 tz 中。在 Linux 中 glibc 并未支持 tz,因此未使用 tz。成功返回 0,否则返回 -1,错误码存于 errno 中
  • settimeofday 函数设置当前时间。成功返回0,否则返回-1。只有 root 权限才能修改。

5. strftime()

1
2
3
size_t strftime (char *__restrict __s, size_t __maxsize, const char *__restrict __format,
const struct tm *__restrict __tp)
char *strptime (const char *__restrict __s, const char *__restrict __fmt, struct tm *__tp)
  • strftime 函数将参数 struct tm 结构的时间,按照 format 所指定的字符串格式做转换,转换后的字符串内容复制到参数 s 所指向的字符串数组中,该字符串的最大长度由 maxsize 所控制。返回值为复制到参数 s 所指的字符串数组的总字符数,不包括字符串结束符。
  • strptime 函数将一个字符串格式时间解释成为 struct tm 格式结构的时间

6. 获取精确时间 timespec_get()、clock_gettime()

1
2
3
4
5
6
int timespec_get (struct timespec *__ts, int __base)

int clock_gettime (clockid_t __clock_id, struct timespec *__tp)
# define CLOCK_REALTIME 0 // 日历时间,UTC
# define CLOCK_MONOTONIC 1 // 单调时钟时间,从系统启动开始计算
# define CLOCK_BOOTTIME 7 // 类似单调时钟时间,但是包含了系统休眠时经过的时间

获取时间,可以精确到纳秒级别。

  • timespec_get 函数,参数 base 目前只定义了 TIME_UTC,所以还无法直接获取其他时区的时间值
  • clock_gettime 函数,它不仅能获得自1970/1/1 开始的时间,还可以自定义 clock 的类型以便获取不同的时间值。在需要获取高精度的时间值时应该优先考虑使用它。

四、Linux 时间编程简单总结

struct timeval 结构和 struct timespec 结构都无法处理时区。Linux 处理时区的手段有以下两种:

  • 函数自定义参数和返回值使用 Local time 还是 UTC time
  • 系统根据环境变量 TZ 以及配置文件 /etc/localtime 等改变本地时间(Local time)

因此在处理时间的时候一定要注意,当前处理的时间是本地时间还是 UTC 时间。

五、C++ 中 chrono 库

C++11 中 chrono 库包含了三种类型的时钟

  • system_clock:系统时钟。可能会被调整
  • steady_clock:单调时钟,不会被调整
  • high_resolution_clock:拥有可用的最短滴答周期的时钟。

这三个时钟类有一些共同的成员,如下:

  • now():静态成员函数,返回当前时间,类型为clock::time_point
  • time_point:成员类型,当前时钟的时间点类型
  • duration:成员类型,时钟的时长类型
  • rep:成员类型,时钟的 tick 类型,等同于 clock::duration::rep
  • period:成员类型,时钟的单位,等同于clock::duration::period
  • is_steady:静态成员类型,是否是稳定时钟,对于 steady_clock 来说该值一定是 true
1

六、Linux 休眠编程接口

1. sleep()、usleep()

1
2
3
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
int usleep(useconds_t usec);
  • sleep 函数使程序休眠直到 seconds 秒之后才被唤醒。函数返回 0 表示 seconds 时间到了,或返回剩余的秒数
  • usleep 函数使程序休眠 usec 微妙之后才被唤醒。休眠时间会比实际时间略长。成功返回0,失败返回 -1。该函数可以被信号唤醒,同时返回 EINTER。

七、Linux 定时器编程接口

Linux 应用程序为我们的每一个进程提供了一个定时闹钟 alarm,当定时器指定的时间到时,系统会向调用进程发送SIGALARM 信号,如果忽略或者不捕获此信号,则其默认动作是终止调用该 alarm 函数的进程;当然也可以通过 signal() 函数向系统注册一个自己的定时闹钟处理函数。

1. alarm()

1
unsigned int alarm(unsigned int seconds);
  • alarm 函数向系统设定一个闹钟,并在闹钟时间到时内核向该进程发送SIGALRM信号

需要注意:

  • 一个进程只能有一个alarm闹钟;
  • 闹钟时间到了后,若不再次调用alarm(),将不会有新的闹钟产生;
  • 任何以seconds非0的调用,都将重新更新闹钟定时时间,并返回上一个闹钟剩余时间;若为第一次设置闹钟,则返回0;以seconds为0的调用,表示取消以前的闹钟,并将剩余时间返回。
  • 在Linux系统中提到,sleep()有可能是使用alarm()来实现的,因此,在一个进程中同时使用alarm()和sleep()并不明智。

2. setitimer()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int getitimer(int which, struct itimerval *curr_value);

int setitimer(int which, const struct itimerval *new_value, struct itimerval *old_value);

which 的取值如下:
ITIMER_REAL:以系统真实的时间来计算,它会送出SIGALRM信号;
ITIMER_PROF:以该进程在用户态下和内核态下所费时间来计算,它送出SIGPROF信号;
ITIMER_VIRTUAL:以该进程在用户态下花费的时间来计算,它送出SIGVTALRM信号;

itimerval 结构如下:
struct itimerval {
struct timeval it_interval; /* next value */
struct timeval it_value; /* current value */
};
struct timeval {
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};

Linux 为每个进程提供了三个独立的计时器,每个计时器在不同的时域中递减。当任何一个定时器到点了,都会向进程发送一个信号,并且重启计时器。

  • getitimer() 函数获取 ITIMER_REAL、ITIMER_PROF 和 ITIMER_VIRTUAL 三个定时器中的一个时间信息,并保存到 curr_value 指向的对象中

  • setitimer() 函数,定时器为 ITIMER_REAL、ITIMER_PROF 和 ITIMER_VIRTUAL 中的一个;定时时间为 new_value;如果 old_value 非空,则将老的时间信息保存到该对象中。成功返回 0,失败返回 -1。

    其中 itimerval 结构中,it_value 变量用于设置计时器的计时时间,为 0 表示禁止;it_interval 变量用于设置当计时器到时需要重置的时间,从而实现循环计时。也就是说,计时器从 it_value 开始递减,当递减到 0 时,向进程发送一个信号,并重置定时器为 it_interval,如此循环,从而实现循环闹钟的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void sig_func(int signo)
{
printf("Catch a signal.\n");
static int realCnt = 0;
static int virtualCnt = 0;
switch(signo)
{
case SIGALRM:
printf("The %d times:SIGALRM\n",realCnt++);
break;
case SIGVTALRM:
printf("The %d times:SIGVTALRM\n",virtualCnt++);
break;
default:
break;
}
}

void test_time_06() {
signal(SIGALRM, sig_func);

struct itimerval v1;
struct itimerval v2;
v1.it_interval.tv_sec = 1;
v1.it_interval.tv_usec = 0;
v1.it_value.tv_sec = 3;
v1.it_value.tv_usec = 0;
int res = setitimer(ITIMER_REAL, &v1, &v2);
if (res < 0) {
std::cout << "setitimer failed, errno: " << errno << ", err: " << strerror(errno) << std::endl;
}
while (1) {
sleep(30);
}
}