参考:
Linux中gmtime和localtime的区别(time_t格式转换为tm格式)
mktime和localtime_r能在多线程环境下使用么?
一、C标准库time.h
C的标准库定义了函数的获取方法,关于函数的数据类型有:time_t、clock_t和struct tm。
- time_t:long long型
- clock_t:long long型
- struct tm
1.1 时间获取函数tmie()
cpp
time_t time( time_t *arg );
// 用法
time_t t;
time(&t);
t = time(NULL);
time()返回自 1970年来的时间,以秒为单位。
1.2 时间转换函数
图中展示了时间转换函数:(1)gmtime、loacltime;(2)mktime;(3)asctime;(4)strftime。
gmtime和loacltime函数
cpp
struct tm *gmtime(const time_t *timer)
struct tm *localtime(const time_t *timer)
从函数声明上两个函数一样。但是区别在于:gmtime是将输入转换成0时区,如果想转换成北京时间,需要在年数上加1900,月份上加1(因为是从0开始的),小时数加上8;而localtime是转换成本地时区,还句话来说,是根据系统设置的时区tzname/timezone/daylight转换成设定的时区,如果想转换成北京时间,需要在年数上加1900,月份上加1;
这里有没有好奇,输入是time_t的指针,但是输出一个指向struct tm的指针,这个指针哪来的?指向哪?看一个示例
cpp
#include <stdio.h>
#include <time.h>
int main()
{
time_t t = time(NULL);
struct tm* ttm = localtime(&t);
printf("%d-%d-%d %d:%d:%d\n", ttm->tm_year + 1900, ttm->tm_mon + 1,
ttm->tm_mday, ttm->tm_hour, ttm->tm_min, ttm->tm_sec);
}
这也是这两个函数的问题所在:其返回的结果是time.h文件中定义的全局静态变量。这就造成了多线程安全的问题。因此多线程下,可能获得时间被其他线程修改了。因此linux中有了线程安全的函数,也在头文件time.h中。
cpp
struct tm *gmtime_r( const time_t *timer, struct tm *buf );
struct tm *localtime_r(const time_t *timep, struct tm *buf);
也就是输入变成两个,返回的指针就是输入的buf指针。例如
cpp
#include <stdio.h>
#include <time.h>
int main()
{
time_t t = time(NULL);
struct tm ttm;
localtime_r(&t, &ttm);
printf("%d-%d-%d %d:%d:%d\n", ttm->tm_year + 1900, ttm->tm_mon + 1,
ttm->tm_mday, ttm->tm_hour, ttm->tm_min, ttm->tm_sec);
}
这样就是线程安全的?然而并不是的,在mktime和localtime_r能在多线程环境下使用么?文章中说这localtime_r函数都考虑了时区转换,而时区的计算要使用全局变量tzname/timezone/daylight。这本质上就是线程不安全的。
mktime函数
cpp
time_t mktime(struct tm *timeptr)
mktime是一个逆过程,将tm的时间转换成秒数。同样,考虑了时区转换,本质上就是线程不安全的。
asctime和strftime函数
这两个函数都是将tm结构体转换成字符串。
cpp
char* asctime(const struct tm *timeptr);
char* asctime_r( const struct tm* time_ptr, char* buf );
从函数原型上看,上面第一个函数也是线程不安全的,莫名其妙输出一个指向指针。下面则是线程安全版本。首先,第一个函数输出固定形式是
cpp
Www Mmm dd hh:mm:ss yyyy\n
Www ------来自 time_ptr->tm_wday 的星期之日的三字母英文缩写, Mon 、 Tue 、 Wed 、 Thu 、 Fri 、 Sat 、 Sun 之一。
Mmm ------来自 time_ptr->tm_mon 的月名的三字母英文缩写, Jan 、 Feb 、 Mar 、 Apr 、 May 、 Jun 、 Jul 、 Aug 、 Sep 、 Oct 、 Nov 、 Dec 之一。
dd ------来自 timeptr->tm_mday 的 2 位月之日,如同由 sprintf 以 %2d 打印
hh ------来自 timeptr->tm_hour 的 2 位时,如同由 sprintf 以 %.2d 打印
mm ------来自 timeptr->tm_min 的 2 位分,如同由 sprintf 以 %.2d 打印
ss ------来自 timeptr->tm_sec 的 2 位秒,如同由 sprintf 以 %.2d 打印
yyyy ------来自 timeptr->tm_year + 1900 的 4 位年,如同由 sprintf 以 %4d 打印
而下面那个函数输出形式和第一个一样,但是就存在一个问题,就是buf大小小于26可能就会越界。因此,推荐使用strftime函数。
strftime函数原型如下:
cpp
size_t strftime(char *str, size_t maxsize, const char *format, const struct tm *timeptr)
str -- 这是指向目标数组的指针,用来复制产生的 C 字符串。
maxsize -- 这是被复制到 str 的最大字符数。
format -- 这是 C 字符串,包含了普通字符和特殊格式说明符的任何组合。这些格式说明符由函数替换为表示 tm 中所指定时间的相对应值。格式说明符是:
说明符 | 替换为 | 实例 |
---|---|---|
%a | 缩写的星期几名称 | Sun |
%A | 完整的星期几名称 | Sunday |
%b | 缩写的月份名称 | Mar |
%B | 完整的月份名称 | March |
%c | 日期和时间表示法 | Sun Aug 19 02:56:02 2012 |
%d | 一月中的第几天(01-31) | 19 |
%H | 24 小时格式的小时(00-23) | 14 |
%I | 12 小时格式的小时(01-12) | 05 |
%j | 一年中的第几天(001-366) | 231 |
%m | 十进制数表示的月份(01-12) | 08 |
%M | 分(00-59) | 55 |
%p | AM 或 PM 名称 | PM |
%S | 秒(00-61) | 02 |
%U | 一年中的第几周,以第一个星期日作为第一周的第一天(00-53) | 33 |
%w | 十进制数表示的星期几,星期日表示为 0(0-6) | 4 |
%W | 一年中的第几周,以第一个星期一作为第一周的第一天(00-53) | 34 |
%x | 日期表示法 | 08/19/12 |
%X | 时间表示法 | 02:50:06 |
%y | 年份,最后两个数字(00-99) | 01 |
%Y | 年份 | 2012 |
%Z | 时区的名称或缩写 | CDT |
%% | 一个 % 符号 | % |
例如
cpp
#include <stdio.h>
#include <time.h>
int main ()
{
time_t rawtime;
struct tm *info;
char buffer[80];
time( &rawtime );
info = localtime( &rawtime );
strftime(buffer, 80, "%Y-%m-%d %H:%M:%S", info);
printf("格式化的日期 & 时间 : |%s|\n", buffer );
return(0);
}
// 输出
// 格式化的日期 & 时间 : |2018-09-19 08:59:07|
ctime函数
cpp
char *ctime(const time_t *timer)
同理,输出是Www Mmm dd hh:mm:ss yyyy\n形式
1.3 时间差值函数difftime()
time.h还提供了计算两个时间差的函数difftime()
cpp
double difftime(time_t time1, time_t time2)
有点奇怪,为什么返回值是double,解释说:在POSIX系统中,time_t是以秒为单位计量的,并且difftime相当于算术减法,但C和C++允许time_t具有分数单位。
1.4 clock函数
在time.h中还定义了一种用于程序计时的函数clock
cpp
clock_t clock(void)
需要配合CLOCKS_PER_SEC一起使用,这个返回的tick数目是从程序开始运行时的tick数,在 32 位系统中,CLOCKS_PER_SEC 等于 1000000,该函数大约每 72 分钟会返回相同的值。在64位系统中clock_t时64位。
cpp
#include <time.h>
#include <stdio.h>
int main()
{
clock_t start_t, end_t;
double total_t;
int i;
start_t = clock();
printf("程序启动,start_t = %ld\n", start_t);
printf("开始一个大循环,start_t = %ld\n", start_t);
for(i=0; i< 10000000; i++)
{
}
end_t = clock();
printf("大循环结束,end_t = %ld\n", end_t);
total_t = (double)(end_t - start_t) / CLOCKS_PER_SEC;
printf("CPU 占用的总时间:%f\n", total_t );
printf("程序退出...\n");
return(0);
}
/*输出
程序启动,start_t = 2614
开始一个大循环,start_t = 2614
大循环结束,end_t = 28021
CPU 占用的总时间:0.025407
程序退出...
*/
1.5 timespec_get函数
前面的时间精度很低,只到秒,有时候,需要更精确的时间,所以可以有了这个函数
cpp
int timespec_get(struct timespec *ts, int base);
这里涉及到另一个数据结构struct timespec
cpp
struct timespec {
time_t tv_sec; // 秒
long tv_nsec; // 纳秒
};
base:时间基准常量,C11 标准定义了 TIME_UTC
,表示协调世界时 (UTC)。
若成功则为 base
的值,否则为零。
1.6 clock_gettime函数
time.h文件另一个用于获取高分辨率时间的函数,但需要 POSIX 支持(至少对于unix系统是满足的)。
cpp
int clock_gettime(clockid_t clk_id, struct timespec *tp);
cld_id有四种类型:
- CLOCK_REALTIME:系统实时时间,随系统实时时间改变而改变
- CLOCK_MONOTONIC,从系统启动这一刻起开始计时,不受系统时间被用户改变的影响
- CLOCK_PROCESS_CPUTIME_ID,本进程到当前代码系统CPU花费的时间
- CLOCK_THREAD_CPUTIME_ID,本线程到当前代码系统CPU花费的时间
成功时,这些函数返回 0;否则,这些函数返回 -1 ,并设置 errno,其时间精度依然可以达到纳秒。实际上,还有类似用于设置时间和进程休眠的函数clock_settime、clock_nanosleep。而clock_getres则是用于获取时钟分辨率的
cpp
int clock_getres(clockid_t clk_id, struct timespec *res);
函数返回由 clk_id 指定的时钟的分辨率,并将其放置在res指向的位置。但是,如果res为 NULL,则不返回任何分辨率。
二、linux提供的时间函数
linux系统本身提供了一个时间接口函数在头文件sys/time.h中,函数名称:gettimeofday
cpp
int gettimeofday(struct timeval *tv, struct timezone *tz);
gettimeofday()会把目前的时间用tv 结构体返回,当地时区的信息则放到tz所指的结构中,日常使用中并不会获得时区信息,所以第二个参数一般为null。
cpp
struct timeval{
long tv_sec; /*秒*/
long tv_usec; /*微妙*/
};
struct timezone{
int tz_minuteswest;/*和greenwich 时间差了多少分钟*/
int tz_dsttime; /*type of DST correction*/
}
两个时间结构体如上。相比时间库time.h,比time精度高,但比timespec_get精度低。同样,可以配合tm结构一起使用,例如
cpp
#include <stdio.h>
#include <sys/time.h>
#include <time.h>
// gcc -o time_2 time_2.c
int main()
{
struct timeval tm_now;
//1.获取当前时间戳(tv_sec, tv_usec)
gettimeofday(&tm_now,NULL); // 第二个参数是时区
//2.转换成本地时间,精确到秒
struct tm *p_local_tm;
p_local_tm = localtime(&tm_now.tv_sec) ;
printf("now datetime: %04d-%02d-%02d %02d:%02d:%02d.%06ld\n",
p_local_tm->tm_year+1900,
p_local_tm->tm_mon+1,
p_local_tm->tm_mday,
p_local_tm->tm_hour,
p_local_tm->tm_min,
p_local_tm->tm_sec,
tm_now.tv_usec); // 有微秒时间戳了
return 0;
}
三、更高级版本
更高级的时间获取版本不在经过系统或者库函数,而是通过内联汇编直接读取cpu内部保存时间的寄存器。
cpp
uint64_t get_tsc() // TSC == Time Stamp Counter寄存器
{
#ifdef __i386__
uint64_t x;
__asm__ volatile("rdtsc" : "=A"(x));
return x;
#elif defined(__amd64__) || defined(__x86_64__)
uint64_t a, d;
__asm__ volatile("rdtsc" : "=a"(a), "=d"(d));
return (d << 32) | a;
#else // ARM架构CPU
uint32_t cc = 0;
__asm__ volatile ("mrc p15, 0, %0, c9, c13, 0":"=r" (cc));
return (uint64_t)cc;
#endif
}
以x86_64为例(因为看不懂其他的),通过rdtsc指令,可以将TSC的数值存放在EDX:EAX中,然后读取寄存器的数值。
-----------------------------------以下内容全部来自 细说时间测量RDTSC和RDTSCP----------------------
3.1 TSC的坑
TSC曾经是一个极高精度,极低开销的取时间的方法,但是随着CPU往多核、多处理器、低功耗的方向上走,在使用TSC时就会遇到很多坑。
**【坑1】**比如有的CPU会根据机器负载情况动态调节工作频率, 那么单位时间CPU的指令周期数就会发生变化,也就很难将其转换成时间。另外,CPU进入休眠再次重启后,TSC会清零。
**【坑2】**再比如,在同一处理器的多个核心之间,以及不同处理器的不同核心之间,rdtsc的结果是否是同步的呢?如果不同步,那么取时的结果就不能用来相互比较。
**【坑3】**再比如,Intel的处理器自Pentium Pro开始,引入了乱序执行的功能,导致程序读取的TSC结果可能不准。如果编写测试程序的时候没有主动回避,也可能会掉到坑里。
3.2 官方填坑
在较新版本的CPU中,引入了常量速率TSC的特性(constant rate TSC)。可以通过如下命令查看你的CPU是否支持(我的机器有四个核,因此输出了四条):cat /proc/cpuinfo | grep constant_tsc
支持该特性的CPU,其TSC是按照其标称频率流逝的,与CPU的实际工作频率与状态无关。如果你的CPU也是支持constant_tsc特性的,那么【坑1】算是填上了。
关于【坑2 】,即不同核心读取的tsc是否同步,目前没有找到统一的说法,Intel的官方手册也没有明说,比如:vol 3b,17.15.1 Invariant TSC章节:
The time stamp counter in newer processors may support an enhancement, referred to as invariant TSC. Processor's support for invariant TSC is indicated by CPUID.80000007H:EDX[8].
The invariant TSC will run at a constant rate in all ACPI P-, C-. and T-states. This is the architectural behavior moving forward. On processors with invariant TSC support, the OS may use the TSC for wall clock timer services (instead of ACPI or HPET timers). TSC reads are much more efficient and do not incur the overhead associated with a ring transition or access to a platform resource.
但这里面只是说TSC能够在CPU处于任何(电源)状态下都能保证以标称速率递增,并没有明确说明TSC能够在多核甚至多处理器的情况下保持同步。另一个蛛丝马迹是在Linux内核代码中
这里有一个unsynchronized_tsc()函数,用于判断系统的TSC是不是同步的,代码实现如下:
cpp
/*
* Make an educated guess if the TSC is trustworthy and synchronized
* over all CPUs.
*/
int unsynchronized_tsc(void)
{
if (!boot_cpu_has(X86_FEATURE_TSC) || tsc_unstable)
return 1;
#ifdef CONFIG_SMP
if (apic_is_clustered_box())
return 1;
#endif
if (boot_cpu_has(X86_FEATURE_CONSTANT_TSC))
return 0;
if (tsc_clocksource_reliable)
return 0;
/*
* Intel systems are normally all synchronized.
* Exceptions must mark TSC as unstable:
*/
if (boot_cpu_data.x86_vendor != X86_VENDOR_INTEL) {
/* assume multi socket systems are not synchronized: */
if (num_possible_cpus() > 1)
return 1;
}
return 0;
}
这里有几个有意思的点:
- 开头的注释说,"make an educated guess",即有根据的猜测,即这里是不是TSC同步的判断依然是一个猜测
- 中间的代码判断了是否开启了CONSTANT TSC特性,如果开启就直接返回0,即TSC是同步的,也就是说,只要我们在cpuinfo里看到constant_tsc的flag,就证明我们的机器的TSC是同步的
- 后面还有一句注释"Intel systems are normally all synchronized.Exceptions must mark TSC as unstable:",即Intel的系统,只要用户没有手动禁用TSC同步,一般都是同步的。
- 在Intel CPU下还有一个注释"assume multi socket systems are not synchronized",即在多处理器系统上,不同CPU(处理器、socket、NUMA节点)之间的TSC是不同步的。
看到这里,我们基本上可以确定了,即:
- 如果你的cpuinfo里有constant_tsc的flag,那么无论在同一CPU不同核心之间,还是在不同CPU的不同核心之间,TSC都是同步的,可以随便用
- 如果你用的是Intel的CPU,但是cpuinfo里没有constant_tsc的flag,那么在同一处理器的不同核心之间,TSC仍然是同步的,但是不同CPU的不同核心之间不同步,尽量不要用
至此,**【坑2】**也基本上解决了。
关于**【坑3】**,即乱序执行问题,可以使用RDTSCP命令来代替RDTSC,前者开销虽然略高,但胜在稳定好用。另外,如果不想用这个指令,还可以用memory barrier技术(后面的文章中我们将详细解释该技术)或者CPUID指令来实现,不过这两者我都没试,据说开销也不小,详细资料可以就参见参考资料中的wiki页面和intel的官方手册。
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <sys/time.h>
// gcc -o time_6 time_6.c
uint64_t get_tsc()
{
uint64_t a, d;
__asm__ volatile("rdtsc" : "=a"(a), "=d"(d));
return (d << 32) | a;
}
uint64_t get_tscp()
{
uint64_t a, d;
__asm__ volatile("rdtscp" : "=a"(a), "=d"(d));
return (d << 32) | a;
}
#define LOOP_TIMES 1000000000
int main(int argc, char **argv)
{
uint64_t beg_tsc, end_tsc;
long loop;
long sum;
printf("-------------rdtsc-------------\n");
loop = LOOP_TIMES;
sum = 0;
while(loop--)
{
beg_tsc = get_tsc();
end_tsc = get_tsc();
sum += (end_tsc - beg_tsc);
}
printf("AVG_CYCLE : %ld\n", sum / LOOP_TIMES);
sleep(1);
printf("-------------rdtscp-------------\n");
loop = LOOP_TIMES;
sum = 0;
while(loop--)
{
beg_tsc = get_tscp();
end_tsc = get_tscp();
sum += (end_tsc - beg_tsc);
}
printf("AVG_CYCLE : %ld\n", sum / LOOP_TIMES);
return 0;
}
测试结果如下:
我一共跑了三次,每次差别都不大,RDTSCP指令比RDTSC多耗费10个指令周期左右,慢不到1倍。如果你能接受这点差别,建议还是用RDTSCP命令吧。另外,RDTSCP指令也是需要平台支持的,是否支持可以使用cat /proc/cpuinfo | grep rdtscp命令查看。
3.3 使用建议
- 如果你的cpuinfo里面没有constant_tsc的flag,建议老老实实用clock_gettime吧,或者换台支持constant_tsc的机器
- 如果你的cpuinfo里面有constant_tsc的flag,那么在同一处理器的不同核心之间可以放心使用TSC,跨处理器的不同核之间,尽量避免使用,可能会有未知的问题
- 如果不是对性能极其敏感,尽量使用RDTSCP代替RDTSC,前者略慢,但能避免CPU乱序执行问题
参考资料
再论 Time stamp counter - 一念天堂 - 博客园
Pitfalls of TSC usage | Oliver Yang
linux - rdtsc accuracy across CPU cores - Stack Overflow
另外C++封装代码:GitHub - MengRao/tscns: A low overhead nanosecond clock based on x86 TSC
-----------------------------------------至此,上面全部来自那篇博客的内容已结束-----------------------------
四、总结
最后放上还是上面那个博客测试的上面的时间函数的性能(没办法,这个人太厉害了.....)
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdint.h>
#include <time.h>
#include <sys/time.h>
// gcc -o time_5 time_5.c
uint64_t get_by_time()
{
time_t tm_now;
time(&tm_now);
return tm_now;
}
uint64_t get_by_gettimeofday()
{
struct timeval tm_now;
gettimeofday(&tm_now,NULL);
return tm_now.tv_sec;
}
uint64_t get_by_clock_gettime()
{
struct timespec tm_now;
clock_gettime(CLOCK_REALTIME, &tm_now);
return tm_now.tv_sec;
}
uint64_t get_cpu_freq()
{
FILE *fp=popen("lscpu | grep CPU | grep MHz | awk {'print $3'}","r");
if(fp == NULL)
return 0;
char cpu_mhz_str[200] = { 0 };
fgets(cpu_mhz_str,80,fp);
fclose(fp);
return atof(cpu_mhz_str) * 1000 * 1000;
}
uint64_t get_by_tsc()
{
uint64_t a, d;
__asm__ volatile("rdtsc" : "=a"(a), "=d"(d));
return (d << 32) | a;
}
void print_diff(uint64_t loop_times, uint64_t beg_tsc, uint64_t end_tsc)
{
double tt_ns = (end_tsc - beg_tsc) * 1.0 * 1000 * 1000 * 1000 / get_cpu_freq();
printf("Number Loop : %lu\n", loop_times);
printf("Total Time : %.02lf ns\n", tt_ns);
printf("Avg Time : %.02lf ns\n", tt_ns / loop_times);
}
#define LOOP_TIMES 1000000000
int main(int argc, char **argv)
{
uint64_t beg_tsc, end_tsc;
long loop;
printf("-------------time()-------------\n");
loop = LOOP_TIMES;
beg_tsc = get_by_tsc();
while(loop--)
get_by_time();
end_tsc = get_by_tsc();
print_diff(LOOP_TIMES, beg_tsc, end_tsc);
printf("-------------gettimeofday()-------------\n");
loop = LOOP_TIMES;
beg_tsc = get_by_tsc();
while(loop--)
get_by_gettimeofday();
end_tsc = get_by_tsc();
print_diff(LOOP_TIMES, beg_tsc, end_tsc);
printf("-------------clock_gettime()-------------\n");
loop = LOOP_TIMES;
beg_tsc = get_by_tsc();
while(loop--)
get_by_clock_gettime();
end_tsc = get_by_tsc();
print_diff(LOOP_TIMES, beg_tsc, end_tsc);
printf("-------------rdtsc-------------\n");
loop = LOOP_TIMES;
beg_tsc = get_by_tsc();
while(loop--)
get_by_tsc();
end_tsc = get_by_tsc();
print_diff(LOOP_TIMES, beg_tsc, end_tsc);
return 0;
}
可以看到:
- time函数最快,但是精度太低
- gettimeofday和clock_gettime虽然精度高,但是都比较慢
- rdtsc精度和速度都十分优秀
另外需要注意一点的是,上述测试结果跟机器配置有很大关系。