本文转载自:http://www.cnblogs.com/qingchen1984/p/7007631.html
简介: 本文试图完整地描述 Linux 系统中 C 语言编程中的时间问题。主要内容包括应用程序中的时间编程方法;时钟硬件简介;Glibc 时间函数的实现以及 Linux 内核对时间的支持和实现原理。这是第 1 部分,探讨应用开发中的时间编程问题。
引子
我们都生活在时间中,但却无法去思考它。什么是时间呢?似乎这是一个永远也不能被回答的问题。然而作为一个程序员,在工作中,总有那么几次我必须思考什么是时间。比如,需要知道一段代码运行了多久;要在 log 文件中记录事件发生时的时间戳;再比如需要一个定时器以便能够定期做某些计算机操作。我发现,在计算机世界中,时间在不同场合也往往有不同的含义,让试图思考它的人感到迷茫。但值得庆幸的是,Linux 中的时间终究是可以理解的。因此我打算讨论一下有关时间的话题,尝试着深入理解 Linux 系统中 C 语言编程中的时间问题。主要内容如下:
- 第 1 部分是应用程序中的时间问题。有三个方面:程序计时需要;获取当前时间;定时器。
- 第 2 部分包括时间硬件简介和 GlibC 实现时间函数的原理。
- 第 3 和第 4 部分是 Linux 内核对时间的支持和实现原理。
现在开始第 1 部分,探讨应用开发中的时间编程问题。在这一部分中,所有的例子代码都在 GlibC 2.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 数据结构表示。 |
struct timeval { time_t tv_sec; long int tv_usec; };
|
微秒 |
clock_gettime() | clock_gettime 函数返回从 1970 年 1 月 1 日 0 点以来,到现在的时间。用 timespec 数据结构表示。 支持不广泛。属于实时扩展。 |
struct timespec { time_t tv_sec; long int tv_nsec; };
|
纳秒 |
ftime() | 函数返回从 1970 年 1 月 1 日 0 点以来,到现在的时间。用timeb 数据结构表示。已经过时, 被 time() 替代。尽量不使用。 |
struct timeb { time_t time; unsigned short
millitm; short timezone; short dstflag; };
|
毫秒 |
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 库提供了许多时间格式转换的函数。这些函数的数量众多,容易让人迷惑,记住它们的用法十分不易。在这里我借用 Michael Kerrisk 在《Linux Programming Interface》一书中的插图,来对这些标准 C 函数进行一个总体的概览。
图 1. 各种时间显示格式转换函数关系图
从上图可以看到,time()/gettimeofday() 从内核得到当前时间之后,该当前时间值可以被两大类函数转换为更加容易阅读的显示格式:
- 固定格式转换
- 用户指定格式转换函数。
固定格式转换
用 ctime() 函数转换出来的时间格式是系统固定的,调用者无法改动,因此被称为固定格式转换。如果您对日期格式没有特殊的要求,那么用它基本上就可以了,简单,不用记忆很多的参数。
用户指定格式转换
典型的 ctime() 格式如下:
Wed Dec 7 20:45:43 PST 2011
有些人觉得这个格式太长,类似 Wed,星期三这样的信息很多情况下都没有啥用途。人们可能更喜欢其他格式:比如2011-12-07 20:45。在这种情况下,就需要进行时间显示格式转换。做法为:先把从内核得到的时间值转换为 struct tm 类型的值,然后调用 strftime() 等函数来输出自定义的时间格式字符串。
下面我列举一些实例,以便读者更清晰地理解众多的时间转换函数的用法。
各标准 C 时间转换函数的解释和举例
char *ctime(const time_t *clock); |
使用函数 ctime 将秒数转化为字符串. 这个函数的返回类型是固定的:一个可能值为”Thu Dec 7 14:58:59 2000”。这个字符串的长度和显示格式是固定的。
清单 1,time 的使用
#include <time.h> |
自定义格式转换
为了更灵活的显示,需要把类型 time_t 转换为 tm 数据结构。tm 数据结构将时间分别保存到代表年,月,日,时,分,秒等不同的变量中。不再是一个令人费解的 64 位整数了。这种数据结构是各种自定义格式转换函数所需要的输入形式。
清单 2,数据结构 tm
struct tm { |
可以使用 gmtime() 和 localtime() 把 time_t 转换为 tm 数据格式,其中 gmtime() 把时间转换为格林威治时间;localtime 则转换为当地时间。
清单 3,时间转换函数定义
#include <time.h> |
使用 tm 来表示时间,您就可以调用 asctime() 和 strftime() 将时间转换为字符串了。asctime() 的输出格式固定,和 ctime() 相同。strftime() 则类似我们最熟悉的 printf() 函数,您可以通过输入参数自定义时间的输出格式。
size_t strftime(char *outstr, size_t maxsize, const char *format, |
清单 4,时间显示转换
int main () |
该例子程序的输出结果如下:
It is now: 02:45PM. |
从以上的例子可以看到,利用从 time() 得到的时间值,可以调用各种转换函数将其转换成更方便人们阅读的形式。
此外从前面的总结中我们也了解到,还有两个 C 函数可以获得当前时间,gettimeofday() 以及 clock_gettime(),它们分别返回 struct timeval 或者 timespec 代表的高精度的时间值。在目前的 GLibC 中,还没有直接把 struct timeval/timespec 转换为 struct tm 的函数。一般的做法是将 timeval 中的 tv_sec 转换为 tm,使用上面所述的方法转换为字符串,最后在显示的时候追加上 tv_usec,比如下面的例子代码:
清单 5,更多时间显示转换
struct timeval tv; |
时间的测量
有时候我们要计算某段程序执行的时间,比如需要对算法进行时间分析。基本的实现思路为在被测试代码的开始和结束的地方获取当时时间,相减后得到相对值,即所需要的统计时间。为了实现高精度的时间测量,必须使用高精度的时间获取方式,一般有两种方法:
- 系统调用 gettimeofday
- 汇编指令 RDTSC。
gettimeofday
可以使用 gettimeofday() 函数进行时间测量,其精度在 us 级别,可以用来做一般的时间分析。
gettimeofday() 将时间保存在结构 tv 之中。gettimeofday() 的第二个参数代表时区,在 Linux 中已经废弃不用,只能用 NULL 传入。一个典型的例子程序如下:
清单 6,gettimeofday 例子程序
void function() |
这个程序输出函数的执行时间,我们可以使用这个来进行系统性能的测试,或者是函数算法的效率分析。在我个人机器上的输出结果是:Used Time:0.556070
RDTSC
gettimeofday() 是一个系统调用,在某些场合下频繁调用它是不合适的。比如性能要求很高的代码段内。因为 gettimeofday() 需要用户态/内核态切换,开销较大。Intel X86 处理器提供了 TSC 硬件,并且可以用非特权指令 rdtsc 来读取该硬件的时间值,这就避免了过度的内核用户态切换。
如何使用 RDTSC
参考下面的例子代码,采用 GCC 的汇编扩展,定义 rdtsc 的函数,它返回当前时间戳。
#define rdtsc(low,high) __asm__ \ |
在 C 代码中使用 rdtsc 十分简单。比如:
清单 7,RDTSC 例子程序
unsigned long long get_cycles() |
函数 get_cycles 将返回 64 位整数,代表当前时间,单位是 CPU 的 cycle 数。函数 get_cpu_mhz 获得当前 CPU 的工作频率。用两个 CPU cycle 的差值除以 CPU 频率,就是微妙。
但 RDTSC 只能在 IA 系列处理器上使用。而且由于处理器的乱序执行,RDTSC 有些情况下并不准确,在 SMP 下使用 RDTSC 也有一定的问题。但这些问题只有在需要极高时间精度的情况下才会出现,对于一般的时间测量要求,采用 RDTSC 是一个可以考虑的选择。
计时器的使用
有时我们需要定时完成一些任务。简单的方法是使用 while 循环加 sleep。比如每隔 1 分钟检查链接情况的 heartbeat 任务等。
清单 8,sleep 加循环
while(condtion) |
这可以满足很多程序的定时需要,但假如您不希望程序“偷懒”,即上例中 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 这个名字的来历吧。
使用 setitimer Timer 需要了解下面这些接口 API:
int getitimer(int which,struct itimerval *value); |
itimerval 的定义如下:
struct itimerval { |
getitimer 函数得到间隔计时器的时间值,保存在 value 中。
setitimer 函数设置间隔计时器的时间值为 newval. 并将旧值保存在 oldval 中;which 表示使用三个计时器中的哪一个。
itimerval 结构中的 it_value 是第一次调用后触发定时器的时间,当这个值递减为 0 时,系统会向进程发出相应的信号。此后将以 it_internval 为周期定时触发定时器。
给出一个具体的例子:
清单 9,setitmer 例子
void print_info(int signo) |
这个程序使用 PROF 时间,每经过两秒 PROF 时间之后就会打印一下 timer fired
字符串。
需要指出:setitimer 计时器的精度为 ms,即 1000 分之 1 秒,足以满足绝大多数应用程序的需要。但多媒体等应用可能需要更高精度的定时,那么就需要考虑使用下一类定时器:POSIX Timer。
POSIX Timer
间隔定时器 setitimer 有一些重要的缺点,POSIX Timer 对 setitimer 进行了增强,克服了 setitimer 的诸多问题:
首先,一个进程同一时刻只能有一个 timer。假如应用需要同时维护多个 Interval 不同的计时器,必须自己写代码来维护。这非常不方便。使用 POSIX Timer,一个进程可以创建任意多个 Timer。
setitmer 计时器时间到达时,只能使用信号方式通知使用 timer 的进程,而 POSIX timer 可以有多种通知方式,比如信号,或者启动线程。
使用 setitimer 时,通知信号的类别不能改变:SIGALARM,SIGPROF 等,而这些都是传统信号,而不是实时信号,因此有 timer overrun 的问题;而 POSIX Timer 则可以使用实时信号。
setimer 的精度是 ms,POSIX Timer 是针对有实时要求的应用所设计的,接口支持 ns 级别的时钟精度。
表 2. POSIX Timer 函数
函数名 | 功能描述 |
---|---|
timer_create |
创建一个新的 Timer;并且指定定时器到时通知机制 |
timer_delete |
删除一个 Timer |
timer_gettime |
Get the time remaining on a POSIX.1b interval timer |
timer_settime |
开始或者停止某个定时器。 |
timer_getoverrun |
获取丢失的定时通知个数。 |
使用 Posix Timer 的基本流程很简单,首先创建一个 Timer。创建的时候可以指定该 Timer 的一些特性,比如 clock ID。
clock ID 即 Timer 的种类,可以为下表中的任意一种:
表 3. POSIX Timer clock ID
Clock ID | 描述 |
---|---|
CLOCK_REALTIME | Settable system-wide real-time clock; |
CLOCK_MONOTONIC | Nonsettable monotonic clock |
CLOCK_PROCESS_CPUTIME_ID | Per-process CPU-time clock |
CLOCK_THREAD_CPUTIME_ID | Per-thread CPU-time clock |
CLOCK_REALTIME 时间是系统保存的时间,即可以由 date 命令显示的时间,该时间可以重新设置。比如当前时间为上午 10 点 10 分,Timer 打算在 10 分钟后到时。假如 5 分钟后,我用 date 命令修改当前时间为 10 点 10 分,那么 Timer 还会再等十分钟到期,因此实际上 Timer 等待了 15 分钟。假如您希望无论任何人如何修改系统时间,Timer 都严格按照 10 分钟的周期进行触发,那么就可以使用 CLOCK_MONOTONIC。
CLOCK_PROCESS_CPUTIME_ID 的含义与 setitimer 的 ITIMER_VIRTUAL 类似。计时器只记录当前进程所实际花费的时间;比如还是上面的例子,假设系统非常繁忙,当前进程只能获得 50%的 CPU 时间,为了让进程真正地运行 10 分钟,应该到 10 点 30 分才允许 Timer 到期。
CLOCK_THREAD_CPUTIME_ID 以线程为计时实体,当前进程中的某个线程真正地运行了一定时间才触发 Timer。
设置到期通知方式
timer_create 的第二个参数 struct sigevent 用来设置定时器到时时的通知方式。该数据结构如下:
清单 10,结构 sigevent
struct sigevent { |
其中 sigev_notify 表示通知方式,有如下几种:
表 3. POSIX Timer 到期通知方式
通知方式 | 描述 |
---|---|
SIGEV_NONE | 定时器到期时不产生通知。。。 |
SIGEV_SIGNAL | 定时器到期时将给进程投递一个信号,sigev_signo 可以用来指定使用什么信号。 |
SIGEV_THREAD | 定时器到期时将启动新的线程进行需要的处理 |
SIGEV_THREAD_ID(仅针对 Linux) | 定时器到期时将向指定线程发送信号。 |
如果采用 SIGEV_NONE 方式,使用者必须调用timer_gettime 函数主动读取定时器已经走过的时间。类似轮询。
如果采用 SIGEV_SIGNAL 方式,使用者可以选择使用什么信号,用 sigev_signo 表示信号值,比如 SIG_ALARM。
如果使用 SIGEV_THREAD 方式,则需要设置 sigev_notify_function,当 Timer 到期时,将使用该函数作为入口启动一个线程来处理信号;sigev_value 保存了传入 sigev_notify_function 的参数。sigev_notify_attributes 如果非空,则应该是一个指向 pthread_attr_t 的指针,用来设置线程的属性(比如 stack 大小,detach 状态等)。
SIGEV_THREAD_ID 通常和 SIGEV_SIGNAL 联合使用,这样当 Timer 到期时,系统会向由 sigev_notify_thread_id 指定的线程发送信号,否则可能进程中的任意线程都可能收到该信号。这个选项是 Linux 对 POSIX 标准的扩展,目前主要是 GLibc 在实现 SIGEV_THREAD 的时候使用到,应用程序很少会需要用到这种模式。
启动定时器
创建 Timer 之后,便可以调用 timer_settime() 函数指定定时器的时间间隔,并启动该定时器了。
int timer_settime(timer_t timerid, int flags, |
第一次看到 timer_settime 的参数列表或许会令人觉得费解。先来看看 new_value 和 old_value,它们都是 struct itimerspec 数据结构。
struct itimerspec |
启动和停止 Timer 都可以通过设置 new_value 来实现:
new_value->it_interval 为定时器的周期值,比如 1 秒,表示定时器每隔 1 秒到期;
new_value->it_value 如果大于 0,表示启动定时器,Timer 将在 it_value 这么长的时间过去后到期,此后每隔 it_interval 便到期一次。如果 it_value 为 0,表示停止该 Timer。
有些时候,应用程序会先启动用一个时间间隔启动定时器,随后又修改该定时器的时间间隔,这都可以通过修改 new_value 来实现;假如应用程序在修改了时间间隔之后希望了解之前的时间间隔设置,则传入一个非 NULL 的 old_value 指针,这样在 timer_settime() 调用返回时,old_value 就保存了上一次 Timer 的时间间隔设置。多数情况下我们并不需要这样,便可以简单地将 old_value 设置为 NULL,忽略它。
下面给出一个使用 posix timer 的例子程序。最传统的例子就是创建通知方式为 SIGEV_SIGNAL 的 Timer。这样当定时器到期时,将产生信号通知,主程序需要定义自己的信号处理函数,来处理信号到期事件。这种例子比比皆是,我打算在这里写一个采用通知方式为 SIGEV_THREAD 的例子。该例子程序从 main 函数开始主线程,在开始的时候打印出主线程的进程 ID 和线程 ID。
清单 11,打印 TID
pid_t tid = (pid_t) syscall (SYS_gettid); |
获得 ThreadID 的系统调用尚未被 GLibC 标准化,因此这里直接调用 syscall。
然后,主线程初始化创建 Timer 所需要的数据结构:
清单 12,设置通知方式
se.sigev_notify = SIGEV_THREAD; |
这里将通知方式设为 SIGEV_THREAD,timer_thread 为线程入口函数。
然后主线程设置定时器间隔,并启动 Timer:
清单 13,启动 Timer
ts.it_value.tv_sec = 5; |
此后主线程进入一个循环,在循环中等待线程条件变量:
清单 14,主程序中的循环
while (counter < 5) { |
条件变量 cond 将在 timer_thread() 处理函数中触发,这样每 5 秒钟,定时器将调用 timer_thread() 处理函数,并唤醒主线程等待的条件变量一次。5 次之后测试程序退出。
现在我们看看 timer_thread() 函数:
清单 15,timer_thread 函数
void timer_thread (void *arg) |
在整个程序中我们都没有使用信号,定时器到期时,将启动新的线程运行 timer_thread。因此在该函数中,我们还打印了当前的线程号以便可以看出它们确实在不同线程中运行。
这里是运行该程序的一个输出:
-bash-3.2$ gcc threadtimer.c -lrt -lpthread -o test |
可以看到每次 Timer 都运行在不同的线程中。
小结
至此,希望我已经讲述了 Linux 系统提供的大多数关于时间的编程方法。使用这些方法我们可以:
- 获得当前时间,并转换为合适的显示方式;
- 衡量程序运行经过的时间;
- 使用定时器完成周期性的任务;
另外不知道您是否和我一样,对于 Linux 系统如何实现这些机制十分好奇。计算机毕竟是一个机器,底层硬件提供了怎样的功能,操作系统和 C 库如何协同工作才可以提供这些一丝不苟的,优美的方法呢?我将在后续的部分试图探讨这个话题。
参考资料
学习
- *上关于 RDTSC 的介绍:介绍了 RDTSC 指令的细节,以及 X86 相关硬件 Time Stamp Counter 的相关信息。
- Linux 下 C 语言编程时间概念:作者 hoyt,系统介绍了很多 Linux 时间编程的细节,值得阅读。
- 在 developerWorks Linux 专区寻找为 Linux 开发人员(包括 Linux 新手入门)准备的更多参考资料。