如果想知道一段代码运行了多久,要在 log 文件中记录事件发生时的时间戳;再比如说需要一个定时器以便能够定时做某些计算机操作呢?
在计算机世界中,时间在不同场合也往往有不同的含义,让试图思考它的人,感到捉不透它。但值得庆幸的是,Linux 中的时间终究是可以理解的。下面尝试着深入理解 Linux 系统中 C 语言编程中的时间问题。
获取当前时间
在程序当中, 我们经常要输出系统当前的时间,比如日志文件中的每一个事件都要记录其产生时间。在 C 语言中获取当前时间的方法有以下几种,它们所获得的时间精度从秒级到纳秒,各有所不同。
time() 函数获取当前时间
time函数会返回从公元1970年1月1日的UTC时间从0时0分0秒算起到现在所经过的秒数。如果t 并非空指针的话,此函数也会将返回值存到t指针所指的内存。
SYNOPSIS
#include <time.h>
time_t time(time_t *t);
DESCRIPTION
time() returns the time as the number of seconds since the Epoch, 1970-01-01 00:00:00 +0000 (UTC).
RETURN VALUE
On success, the value of time in seconds since the Epoch is returned. On error, ((time_t) -1) is returned, and errno is
set appropriately.
ERRORS
EFAULT t points outside your accessible address space.
//成功返回秒数,错误则返回(time_t) -1),错误原因存于errno中
来看看一个获取当前时间小例子:
#include <stdio.h>
#include <string.h>
#include <time.h>
int main(int argc , char *argv[])
{
time_t seconds;
seconds = time((time_t *) NULL);
printf("time = %d\n",seconds);
return 0;
}
编译输出:
gettimeofday() 获取当前时间
gettimeofday函数获取当前时间存于tv结构体中,相应的时区信息则存于tz结构体中。这里需要注意的是tz是依赖于系统,不同的系统可能存在获取不到的可能,因此通常设置为NULL
函数原型
#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
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 */
};
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
int main(int argc , char *argv[])
{
struct timeval tv;
gettimeofday(&tv,NULL);
printf("tv_sec: %d\n",tv.tv_sec);
printf("tv_usec: %d\n",tv.tv_usec);
return 0;
}
编译输出:
clock_gettime() 获取精确时间函数
clock_gettime() 函数是基于linux 操作系统的。可以根据需要,获取不同要求的精确时
函数原型:
#include <time.h>
int clock_gettime(clockid_t clk_id, struct timespec* tp);
clk_id : 检索和设置的clk_id指定的时钟时间。
CLOCK_REALTIME:系统实时时间,随系统实时时间改变而改变,即从UTC1970-1-1 0:0:0开始计时,
中间时刻如果系统时间被用户改成其他,则对应的时间相应改变
CLOCK_MONOTONIC:从系统启动这一刻起开始计时,不受系统时间被用户改变的影响
CLOCK_PROCESS_CPUTIME_ID:本进程到当前代码系统CPU花费的时间
CLOCK_THREAD_CPUTIME_ID:本线程到当前代码系统CPU花费的时间
struct timespec
{
time_t tv_sec; /* 秒*/
long tv_nsec; /* 纳秒*/
};
举个例子:
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <sys/time.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
time_t dwCurTime1;
dwCurTime1 = time(NULL);
struct timeval stCurTime2;
gettimeofday(&stCurTime2, NULL);
struct timespec stCurTime3;
clock_gettime(CLOCK_REALTIME, &stCurTime3);
printf("Time1 = %d s\n",dwCurTime1);
printf("Time2 = %d s\n",stCurTime2.tv_sec);
printf("Time3 = %d s\n",stCurTime3.tv_sec);
printf("Time2 = %d us\n",stCurTime2.tv_usec);
printf("Time3 = %d ns\n",stCurTime3.tv_nsec);
return 0;
}
编译输出:
上面介绍的三个API是GUN/Linux 提供了三个标准的 API 用来获取当前时间,time()/gettimeofday()/clock_gettime(),它们的区别仅在于获取的时间精度不同,可以根据需要选取合适的调用。
时间显示和转换
目前我们得到的时间是一个数字,无论精度如何,它代表的仅是一个差值。比如精度为秒的 time() 函数,返回一个 time_t 类型的整数。
假设当前时间为 2020 年 6 月 21 日下午 12 点 16 分 51 秒,那么 time_t 的值为:1592713009
。即距离 1970 年 1 月 1 日零点,我们已经过去了 1592713009秒。这里的 1970 年 1 月 1 日零点是格林威治时间,而不是北京时间。下面讨论的时间如果不特别说明都是格林威治时间,也叫 GMT 时间,或者 UTC 时间。
字符串“1592713009秒”对于多数人都没有太大的意义,我们更愿意看到“2020 年 6 月 21 日”这样的显示。因此当我们得到秒,毫秒,甚至纳秒表示的当前时间之后,往往需要将这些数字转换为人们所熟悉的时间表示方法。
各种时间显示格式转换函数关系图
由于国家,习惯和时区的不同,时间的表示方法并没有一个统一的格式。为了满足各种时间显示的需求,标准 C 库提供了许多时间格式转换的函数。这些函数的数量众多,容易让人迷惑,记住它们的用法十分不易。来看看一张图:
图片来源:《Linux Programming Interface》
从上图可以看到,time()/gettimeofday() 从内核得到当前时间之后,该当前时间值可以被两大类函数转换为更加容易阅读的显示格式,分别为固定格式转换和用户指定格式转换函数。
固定格式转换
用 ctime() 函数转换出来的时间格式是系统固定的,调用者无法改动,因此被称为固定格式转换。如果您对日期格式没有特殊的要求,那么用它基本上就可以了
举个例子,固定格式打印时间:
#include <stdio.h>
#include <time.h>
int main (int argc ,char *argv[])
{
time_t curtime;
time(&curtime); //获取当前时间
printf("curtime = %s", ctime(&curtime));
return 0;
}
编译输出:
自定义格式转换
为了更灵活的显示,需要把类型 time_t 转换为 tm 数据结构。tm 数据结构将时间分别保存到代表年,月,日,时,分,秒等不同的变量中。不再是一个令人费解的 64 位整数了。这种数据结构是各种自定义格式转换函数所需要的输入形式。
struct tm结构:
struct tm {
int tm_sec; /* seconds */
int tm_min; /* minutes */
int tm_hour; /* hours */
int tm_mday; /* day of the month */
int tm_mon; /* month */
int tm_year; /* year */
int tm_wday; /* day of the week */
int tm_yday; /* day in the year */
int tm_isdst; /* daylight saving time */
};
//int tm_sec 代表目前秒数,正常范围为0-59,但允许至61秒
//int tm_min 代表目前分数,范围0-59
//int tm_hour 从午夜算起的时数,范围为0-23
//int tm_mday 目前月份的日数,范围01-31
//int tm_mon 代表目前月份,从一月算起,范围从0-11
//int tm_year 从1900 年算起至今的年数
//int tm_wday 一星期的日数,从星期一算起,范围为0-6
//int tm_yday 从今年1月1日算起至今的天数,范围为0-365
//int tm_isdst 日光节约时间的旗标
可以使用 gmtime() 和 localtime() 把 time_t 转换为 tm 数据格式,其中 gmtime() 把时间转换为格林威治时间;localtime 则转换为当地时间。
下面的实例演示了 gmtime() 函数的用法。
#include <stdio.h>
#include <time.h>
#define BST (+1)
#define CCT (+8)
int main ()
{
time_t rawtime;
struct tm *info;
time(&rawtime); //
/* 获取 GMT 时间 */
info = gmtime(&rawtime );
printf("当前的世界时钟:\n");
printf("London: %2d:%02d\n", (info->tm_hour+BST)%24, info->tm_min);
printf("China: %2d:%02d\n", (info->tm_hour+CCT)%24, info->tm_min);
return(0);
}
编译输出:
从以上的例子可以看到,利用从 time() 得到的时间值,可以调用各种转换函数将其转换成更方便我们阅读的形式。
从前面的总结中我们也了解到,还有两个 C 函数可以获得当前时间,gettimeofday() 以及 clock_gettime(),它们分别返回 struct timeval 或者 timespec 代表的高精度的时间值。
在目前的 GLibC 中,还没有直接把 struct timeval/timespec 转换为 struct tm 的函数。一般的做法是将 timeval 中的 tv_sec 转换为 tm,使用上面所述的方法转换为字符串,最后在显示的时候追加上 tv_usec,比如下面的例子代码:
#include <stdio.h>
#include <time.h>
#include <string.h>
#include <sys/time.h>
#include <stdio.h>
int main (int argc ,char *argv[])
{
struct timeval tv;
time_t nowtime;
struct tm *nowtm;
char tmbuf[64], buf[64];
gettimeofday(&tv, NULL); //获取当前时间到 tv
nowtime = tv.tv_sec; //nowtime 存储了秒级的时间值
nowtm = localtime(&nowtime); //转换为 tm 数据结构
//用 strftime 函数将 tv 转换为字符串,但 strftime 函数只能达到秒级精度
strftime(tmbuf, sizeof tmbuf, "%Y-%m-%d %H:%M:%S", nowtm);
printf("tmbuf = %s\n",tmbuf);
//将毫秒值追加到 strftime 转换的字符串末尾
snprintf(buf, sizeof buf, "%s.%06d", tmbuf, tv.tv_usec);
printf("buf = %s\n",buf);
return(0);
}
编译输出:
高精度的时间获取方式
有时候我们要计算某段程序执行的时间,比如需要对算法进行时间分析。基本的实现思路为在被测试代码的开始和结束的地方获取当时时间,相减后得到相对值,即所需要的统计时间。为了实现高精度的时间测量,必须使用高精度的时间获取方式。例如使用gettimeofday。
gettimeofday() 获取当前时间
gettimeofday函数获取当前时间存于tv结构体中,相应的时区信息则存于tz结构体中,需要注意的是tz是依赖于系统,不同的系统可能存在获取不到的可能,因此通常设置为NULL 。
函数原型:
#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
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 */
};
这个函数获取从1970年1月1日到现在经过的时间和时区(UTC时间),(按照linux的官方文档,时区已经不再使用,正常应该传NULL)。
#include <sys/time.h>
#include <stdio.h>
#include <errno.h>
#include <math.h>
void function()
{
unsigned int i,j;
double y;
for(i=0;i<1000;i++)
for(j=0;j<1000;j++)
y=sin((double)i); //耗时操作
}
int main(int argc, char *argv[])
{
struct timeval tpstart,tpend;
float timeuse;
gettimeofday(&tpstart,NULL); //记录开始时间戳
function();
//2次调用 gettimeofday,然后计算的差值就是时间间隔
gettimeofday(&tpend,NULL); //记录结束时间戳
timeuse = 1000000*(tpend.tv_sec-tpstart.tv_sec)+
tpend.tv_usec-tpstart.tv_usec; //计算差值
timeuse /= 1000000;
printf("Used Time:%f\n",timeuse);
return 0;
}
编译输出:
这个程序输出函数的执行时间,我们可以使用这个来进行系统性能的测试,或者是函数算法的效率分析。
也可以使用time()计算精度,但是精度很低(秒级),不建议使用,这里就稍微带下用法。
time_t start,stop;
start = time(NULL);
function();
stop = time(NULL);
总结
本篇讲解Linux 系统中 C 语言编程中的时间问题,主要讲解获取当前时间、时间显示和转换、 高精度的时间获取方式,希望本篇对你有帮助。
欢迎关注微信公众号【程序猿编码】,添加本人微信号(17865354792),回复:领取学习资料。或者回复:进入技术交流群。网盘资料有如下: