Linux中的时间编程和实现原理.docx
《Linux中的时间编程和实现原理.docx》由会员分享,可在线阅读,更多相关《Linux中的时间编程和实现原理.docx(21页珍藏版)》请在冰豆网上搜索。
![Linux中的时间编程和实现原理.docx](https://file1.bdocx.com/fileroot1/2023-2/8/9258c306-9c75-43ca-83c7-93b2dd045101/9258c306-9c75-43ca-83c7-93b2dd0451011.gif)
Linux中的时间编程和实现原理
Linux中的时间编程和实现原理
本文试图完整地描述Linux系统中C语言编程中的时间问题。
主要内容包括应用程序中的时间编程方法;时钟硬件简介;Glibc时间函数的实现以及Linux内核对时间的支持和实现原理。
这一部分,探讨应用开发中的时间编程问题。
引子
我们都生活在时间中,但却无法去思考它。
什么是时间呢?
似乎这是一个永远也不能被回答的问题。
然而作为一个程序员,在工作中,总有那么几次我必须思考什么是时间。
比如,需要知道一段代码运行了多久;要在log文件中记录事件发生时的时间戳;再比如需要一个定时器以便能够定期做某些计算机操作。
我发现,在计算机世界中,时间在不同场合也往往有不同的含义,让试图思考它的人感到迷茫。
但值得庆幸的是,Linux中的时间终究是可以理解的。
因此我打算讨论一下有关时间的话题,尝试着深入理解Linux系统中C语言编程中的时间问题。
主要内容如下:
∙第1部分是应用程序中的时间问题。
有三个方面:
程序计时需要;获取当前时间;定时器。
∙第2部分包括时间硬件简介和GlibC实现时间函数的原理。
∙第3和第4部分是Linux内核对时间的支持和实现原理。
现在开始第1部分,探讨应用开发中的时间编程问题。
在这一部分中,所有的例子代码都在GlibC2.14,内核2.6.33的Linux系统下编译并验证执行过。
读者如果使用低版本的GlibC和Linux内核有可能无法正确执行。
获取当前时间
时间的获取
在程序当中,我们经常要输出系统当前的时间,比如日志文件中的每一个事件都要记录其产生时间。
在C语言中获取当前时间的方法有以下几种,它们所获得的时间精度从秒级到纳秒,各有所不同。
表1.C时间函数
function定义
含义
返回值
精度
time()
time函数获得从1970年1月1日0点到当前的秒数,存储在time_t结构之中。
time_t
秒
gettimeofday()
gettimeofday函数返回从1970年1月1日0点以来,到现在的时间。
用timeval数据结构表示。
structtimeval
{
time_ttv_sec;
longinttv_usec;
};
微秒
clock_gettime()
clock_gettime函数返回从1970年1月1日0点以来,到现在的时间。
用timespec数据结构表示。
支持不广泛。
属于实时扩展。
structtimespec
{
time_ttv_sec;
longinttv_nsec;
};
纳秒
ftime()
函数返回从1970年1月1日0点以来,到现在的时间。
用timeb数据结构表示。
已经过时,被time()替代。
尽量不使用。
structtimeb{
time_ttime;
unsignedshort
millitm;
shorttimezone;
shortdstflag;
};
毫秒
GUN/Linux提供了三个标准的API用来获取当前时间,time()/gettimeofday()/clock_gettime(),它们的区别仅在于获取的时间精度不同,您可以根据需要选取合适的调用。
ftime()是老的一些系统中的时间调用,很多Linux版本虽然支持它,但仅仅是为了向前兼容性,新开发的软件不建议使用ftime()来获得当前时间。
时间显示和转换
目前我们得到的时间是一个数字,无论精度如何,它代表的仅是一个差值。
比如精度为秒的time()函数,返回一个time_t类型的整数。
假设当前时间为2011年12月7日下午20点29分51秒,那么time_t的值为:
1323318591。
即距离1970年1月1日零点,我们已经过去了1323318591秒。
(这里的1970年1月1日零点是格林威治时间,而不是北京时间。
)我们下面讨论的时间如果不特别说明都是格林威治时间,也叫GMT时间,或者UTC时间。
字符串“1323318591秒”对于多数人都没有太大的意义,我们更愿意看到“2011年12月7日”这样的显示。
因此当我们得到秒,毫秒,甚至纳秒表示的当前时间之后,往往需要将这些数字转换为人们所熟悉的时间表示方法。
由于国家,习惯和时区的不同,时间的表示方法并没有一个统一的格式。
为了满足各种时间显示的需求,标准C库提供了许多时间格式转换的函数。
这些函数的数量众多,容易让人迷惑,记住它们的用法十分不易。
在这里我借用MichaelKerrisk在《LinuxProgrammingInterface》一书中的插图,来对这些标准C函数进行一个总体的概览。
图1.各种时间显示格式转换函数关系图
从上图可以看到,time()/gettimeofday()从内核得到当前时间之后,该当前时间值可以被两大类函数转换为更加容易阅读的显示格式:
∙固定格式转换
∙用户指定格式转换函数。
固定格式转换
用ctime()函数转换出来的时间格式是系统固定的,调用者无法改动,因此被称为固定格式转换。
如果您对日期格式没有特殊的要求,那么用它基本上就可以了,简单,不用记忆很多的参数。
用户指定格式转换
典型的ctime()格式如下:
WedDec720:
45:
43PST2011
有些人觉得这个格式太长,类似Wed,星期三这样的信息很多情况下都没有啥用途。
人们可能更喜欢其他格式:
比如2011-12-0720:
45。
在这种情况下,就需要进行时间显示格式转换。
做法为:
先把从内核得到的时间值转换为structtm类型的值,然后调用strftime()等函数来输出自定义的时间格式字符串。
下面我列举一些实例,以便读者更清晰地理解众多的时间转换函数的用法。
各标准C时间转换函数的解释和举例
char*ctime(consttime_t*clock);
使用函数ctime将秒数转化为字符串.这个函数的返回类型是固定的:
一个可能值为”ThuDec714:
58:
592000”。
这个字符串的长度和显示格式是固定的。
清单1,time的使用
#include
intmain()
{
time_ttime_raw_format;
time(&time_raw_format);//获取当前时间
printf("timeis[%d]\n",time_raw_format);
//用ctime将时间转换为字符串输出
printf("Thecurrentlocaltime:
%s",ctime(&time_raw_format));
return0;
}
自定义格式转换
为了更灵活的显示,需要把类型time_t转换为tm数据结构。
tm数据结构将时间分别保存到代表年,月,日,时,分,秒等不同的变量中。
不再是一个令人费解的64位整数了。
这种数据结构是各种自定义格式转换函数所需要的输入形式。
清单2,数据结构tm
structtm{
inttm_sec;/*Seconds(0-60)*/
inttm_min;/*Minutes(0-59)*/
inttm_hour;/*Hours(0-23)*/
inttm_mday;/*Dayofthemonth(1-31)*/
inttm_mon;/*Month(0-11)*/
inttm_year;/*Yearsince1900*/
inttm_wday;/*Dayoftheweek(Sunday=0)*/
inttm_yday;/*Dayintheyear(0-365;1Jan=0)*/
inttm_isdst;/*Daylightsavingtimeflag
>0:
DSTisineffect;
=0:
DSTisnoteffect;
<0:
DSTinformationnotavailable*/
};
可以使用gmtime()和localtime()把time_t转换为tm数据格式,其中gmtime()把时间转换为格林威治时间;localtime则转换为当地时间。
清单3,时间转换函数定义
#include
structtm*gmtime(consttime_t*timep);
structtm*localtime(consttime_t*timep);
使用tm来表示时间,您就可以调用asctime()和strftime()将时间转换为字符串了。
asctime()的输出格式固定,和ctime()相同。
strftime()则类似我们最熟悉的printf()函数,您可以通过输入参数自定义时间的输出格式。
size_tstrftime(char*outstr,size_tmaxsize,constchar*format,
conststructtm*timeptr);
清单4,时间显示转换
intmain()
{
time_ttime_raw_format;
structtm*time_struct;
charbuf[100];
time(&time_raw_format);
time_struct=localtime(&time_raw_format);
strftime(buf,100,"Itisnow:
%I:
%M%p.",time_struct);
puts(buf);
return0;
}
该例子程序的输出结果如下:
Itisnow:
02:
45PM.
从以上的例子可以看到,利用从time()得到的时间值,可以调用各种转换函数将其转换成更方便人们阅读的形式。
此外从前面的总结中我们也了解到,还有两个C函数可以获得当前时间,gettimeofday()以及clock_gettime(),它们分别返回structtimeval或者timespec代表的高精度的时间值。
在目前的GLibC中,还没有直接把structtimeval/timespec转换为structtm的函数。
一般的做法是将timeval中的tv_sec转换为tm,使用上面所述的方法转换为字符串,最后在显示的时候追加上tv_usec,比如下面的例子代码:
清单5,更多时间显示转换
structtimevaltv;
time_tnowtime;
structtm*nowtm;
chartmbuf[64],buf[64];
gettimeofday(&tv,NULL);//获取当前时间到tv
nowtime=tv.tv_sec;//nowtime存储了秒级的时间值
nowtm=localtime(&nowtime);//转换为tm数据结构
//用strftime函数将tv转换为字符串,但strftime函数只能达到秒级精度
strftime(tmbuf,sizeoftmbuf,"%Y-%m-%d%H:
%M:
%S",nowtm);
//将毫秒值追加到strftime转换的字符串末尾
snprintf(buf,sizeofbuf,"%s.%06d",tmbuf,tv.tv_usec);
时间的测量
有时候我们要计算某段程序执行的时间,比如需要对算法进行时间分析。
基本的实现思路为在被测试代码的开始和结束的地方获取当时时间,相减后得到相对值,即所需要的统计时间。
为了实现高精度的时间测量,必须使用高精度的时间获取方式,一般有两种方法:
∙系统调用gettimeofday
∙汇编指令RDTSC。
gettimeofday
可以使用gettimeofday()函数进行时间测量,其精度在us级别,可以用来做一般的时间分析。
gettimeofday()将时间保存在结构tv之中。
gettimeofday()的第二个参数代表时区,在Linux中已经废弃不用,只能用NULL传入。
一个典型的例子程序如下:
清单6,gettimeofday例子程序
voidfunction()
{
unsignedinti,j;
doubley;
for(i=0;i<1000;i++)
for(j=0;j<1000;j++)
y=sin((double)i);//耗时操作
}
main()
{
structtimevaltpstart,tpend;
floattimeuse;
gettimeofday(&tpstart,NULL);//记录开始时间戳
function();
gettimeofday(&tpend,NULL);//记录结束时间戳
timeuse=1000000*(tpend.tv_sec-tpstart.tv_sec)+
tpend.tv_usec-tpstart.tv_usec;//计算差值
timeuse/=1000000;
printf("UsedTime:
%f\n",timeuse);
exit(0);
}
这个程序输出函数的执行时间,我们可以使用这个来进行系统性能的测试,或者是函数算法的效率分析。
在我个人机器上的输出结果是:
UsedTime:
0.556070
RDTSC
gettimeofday()是一个系统调用,在某些场合下频繁调用它是不合适的。
比如性能要求很高的代码段内。
因为gettimeofday()需要用户态/内核态切换,开销较大。
IntelX86处理器提供了TSC硬件,并且可以用非特权指令rdtsc来读取该硬件的时间值,这就避免了过度的内核用户态切换。
如何使用RDTSC
参考下面的例子代码,采用GCC的汇编扩展,定义rdtsc的函数,它返回当前时间戳。
#definerdtsc(low,high)__asm__\
__volatile__("rdtsc":
"=a"(low),"=d"(high))
在C代码中使用rdtsc十分简单。
比如:
清单7,RDTSC例子程序
unsignedlonglongget_cycles()
{
unsignedlow,high;
unsignedlonglongval;
rdtsc(low,high);
val=high;
val=(val<<32)|low;//将low和high合成一个64位值
returnval;
}
doubleget_cpu_mhz(void)
{
FILE*f;
charbuf[256];
doublemhz=0.0;
f=fopen("/proc/cpuinfo","r");//打开proc/cpuinfo文件
if(!
f)
return0.0;
while(fgets(buf,sizeof(buf),f)){
doublem;
intrc;
rc=sscanf(buf,"cpuMHz:
%lf",&m);//读取cpuMHz
if(mhz==0.0){
mhz=m;
break;
}
}
fclose(f);
returnmhz;//返回HZ值
}
intmain()
{
doublemhz;
mhz=get_cpu_mhz();
cycles_tc1,c2;
for(;;)
{
c1=get_cycles();
sleep
(1);
c2=get_cycles();
//c2和c1的差值应该为1000000us,即1秒
printf("1sec=%gusec\n",(c2-c1)/mhz);
}
}
函数get_cycles将返回64位整数,代表当前时间,单位是CPU的cycle数。
函数get_cpu_mhz获得当前CPU的工作频率。
用两个CPUcycle的差值除以CPU频率,就是微妙。
但RDTSC只能在IA系列处理器上使用。
而且由于处理器的乱序执行,RDTSC有些情况下并不准确,在SMP下使用RDTSC也有一定的问题。
但这些问题只有在需要极高时间精度的情况下才会出现,对于一般的时间测量要求,采用RDTSC是一个可以考虑的选择。
计时器的使用
有时我们需要定时完成一些任务。
简单的方法是使用while循环加sleep。
比如每隔1分钟检查链接情况的heartbeat任务等。
清单8,sleep加循环
while(condtion)
{
//dosomething
sleep(interval);
}
这可以满足很多程序的定时需要,但假如您不希望程序“偷懒”,即上例中sleep的时候您还是希望程序做些有用的工作,那么使用定时器是通常的选择。
Linux系统上最常用的定时器是setitmer计时器。
setitimer
Linux为每一个进程提供了3个setitimer间隔计时器:
∙ITIMER_REAL:
减少实际时间,到期的时候发出SIGALRM信号。
∙ITIMER_VIRTUAL:
减少有效时间(进程执行的时间),产生SIGVTALRM信号。
∙ITIMER_PROF:
减少进程的有效时间和系统时间(为进程调度用的时间)。
这个经常和上面一个使用用来计算系统内核时间和用户时间。
产生SIGPROF信号。
所谓REAL时间,即我们人类自然感受的时间,英文计算机文档中也经常使用wall-clock这个术语。
说白了就是我们通常所说的时间,比如现在是下午5点10分,那么一分钟的REAL时间之后就是下午5点11分。
VIRTUAL时间是进程执行的时间,Linux是一个多用户多任务系统,在过去的1分钟内,指定进程实际在CPU上的执行时间往往并没有1分钟,因为其他进程会被Linux调度执行,在那些时间内,虽然自然时间在流逝,但指定进程并没有真正的运行。
VIRTUAL时间就是指定进程真正的有效执行时间。
比如5点10分开始的1分钟内,进程P1被Linux调度并占用CPU的执行时间为30秒,那么VIRTUAL时间对于进程P1来讲就是30秒。
此时自然时间已经到了5点11分,但从进程P1的眼中看来,时间只过了30秒。
PROF时间比较独特,对进程P1来说从5点10分开始的1分钟内,虽然自己的执行时间为30秒,但实际上还有10秒钟内核是在执行P1发起的系统调用,那么这10秒钟也被加入到PROF时间。
这种时间定义主要用于全面衡量进程的性能,因为在统计程序性能的时候,10秒的系统调用时间也应该算到P1的头上。
这也许就是PROF这个名字的来历吧。
使用setitimerTimer需要了解下面这些接口API:
intgetitimer(intwhich,structitimerval*value);
intsetitimer(intwhich,structitimerval*newval,
structitimerval*oldval);
itimerval的定义如下:
structitimerval{
structtimevalit_interval;
structtimevalit_value;
}
getitimer函数得到间隔计时器的时间值,保存在value中。
setitimer函数设置间隔计时器的时间值为newval.并将旧值保存在oldval中;which表示使用三个计时器中的哪一个。
itimerval结构中的it_value是第一次调用后触发定时器的时间,当这个值递减为0时,系统会向进程发出相应的信号。
此后将以it_internval为周期定时触发定时器。
给出一个具体的例子:
清单9,setitmer例子
voidprint_info(intsigno)
{
printf(“timerfired\n”);//简单的打印,表示timer到期
}
voidinit_sigaction(void)
{
structsigactionact;
act.sa_handler=print_info;
act.sa_flags=0;
sigemptyset(&act.sa_mask);
sigaction(SIGPROF,&act,NULL);//设置信号SIGPROF的处理函数为print_info
}
voidinit_time()
{
structitimervalvalue;
value.it_value.tv_sec=2;
value.it_value.tv_usec=0;
value.it_interval=value.it_value;
setitimer(ITIMER_PROF,&value,NULL);//初始化timer,到期发送SIGPROF信号
}
intmain()
{
len=strlen(prompt);
init_sigaction();
init_time();
while
(1);
exit(0);
}
这个程序使用PROF时间,每经过两秒PROF时间之后就会打印一下timerfired字符串。
需要指出:
setitimer计时器的精度为ms,即1000分之1秒,足以满足绝大多数应用程序的需要。
但多媒体等应用可能需要更高精度的定时,那么就需要考虑使用下一类定时器:
POSIXTimer。
POSIXTimer
间隔定时器setitimer有一些重要的缺点,POSIXTimer对setitimer进行了增强,克服了setitimer的诸多问题:
首先,一个进程同一时刻只能有一个timer。
假如应用需要同时维护多个Interval不同的计时器,必须自己写代码来维护。
这非常不方便。
使用POSIXTimer,一个进程可以创建任意多个Timer。
setitmer计时器时