如果需要特别精确的时间,就需要使用平台相关的资源,现代cpu基本上都包含一个随时钟周期而不断增长的计数寄存器,这个时钟计数寄存器是完成高分辨率计时任务的唯一可靠途径。
基于不同的平台,这个寄存器可能是可读的,有可能是不可读的;可能32位,也可能64位;可能是可写的也可能不可写;如果是32位,那还得考虑溢出问题。在某一些平台上,该寄存器甚至根本不可能存在,或者如果如果cpu缺少这个特性,而我们又需要处理这种特殊的需求的时候,则可能会由硬件设计者通过外部设备实现。
无论该寄存器是否可以置0,我们都强烈建议不要重置它,即使硬件允许这么做。毕竟我们不是该计数器唯一用户,例如在支持SMP的平台上,内核会依赖这种计数器来保持处理器之间的同步。因为总可以通过多次读取寄存器并比较读出数值的差异来完成要做的事,故无需要求独占该寄存器并修改它的当前值。
最有名的计数寄存器就是TSC时间戳计数器,x86奔腾处理器开始提供该寄存器,并包括在以后的所有CPU中,包括x86_64在内,它是一个64位的寄存器,记录CPU时钟周期数,从内核空间和用户空间都可以读取它。
包含头文件<asm/msr.h>(x86专用头文件意思是机器特有寄存器)只有就可以使用如下的宏:
rdtsc(low32,high32);
rdtscl(low32);
rdtscll(var64);
第一个宏原子性地把64位的数值读到两个32位变量中;后一个只把寄存器的地板部分读入一个32位变量中;最后一个将64位的的计数寄存器读入一个long long型的变量。
下面这段代码仅仅使用了该寄存器的低半部分,可用来测量该指令自身的运行时间:
unsigned long ini,end;
rdtscl(ini);rdtscl(end);
printk("time lapse:%li\n",end-ini);
其他一些平台也提供了类似的功能,在内核头文件中还有一个与体系结构无关的函数可以代替rdtsc,即get_cycles,他定义在<asm/timex.h>(由<linux/timex.h>包含),其原型如下:
#include <linux/timex.h>
cycles_t get_cycles(void);
各种平台上都可以使用这个函数,在没有时钟周期计数寄存器的平台上它总是返回0。cycles_t 类型是能装入读取值的合适的无符号类型。
除了这个与体系结构无关的函数外,我们还将举例说明一段内嵌的汇编代码。为此,我们将针对MIPS实现一个rdtscl函数,其功能和x86的一样。
这个例子之所以基于IPS是因为大多数MIPS处理器都有一个32位的计数器,在它们内部的"coprocessor 0"中称它为寄存器9。为了从内核空间读取该寄存器,可以定义下面的宏,他执行"从coprocessor 0读取"的汇编指令。
#define rdtscl(dest) __asm__ __volatile__ ("mfc0 %0,$9;nop":"=r" (dest))
通过这个宏,MIPS处理器就可以执行前面用于x86的代码了。gcc内嵌汇编的有趣之处在于通用寄存器的分配使用是由编译器完成的。这个宏汇总使用的%0只是"参数0"的占位符,参数0由随后的"作为输出(=)使用的任意寄存器(r)"指定。该宏还声明了输出寄存器要对应于C的表达式dest。内联汇编的语法强大,但也十分复杂,特别是在对于各级存器使用的有限制的平台上更是如此,如x86系列。完整的语法描述在gcc文档中提供,一般在info文档树种就可以找到。
本小节展示的短小的c代码段已经在一个K7系列的x86处理器和一个MIPS VR4181处理器(使用了刚才的宏)上运行过了。前者给出的事件消耗为11时钟周期,后者仅为2个时钟周期。这是可以理解的,因为RISC处理器通常在每时钟周期运行一条指令。
关于时间戳计数器,还有值得一提的一点是:在SMp系统中,他们不会在多个处理器间保持同步。为了确保获得一致的值,我们需要为查询该计数器的代码禁止抢占。
获取当前时间
内核一般通过jiffies来获得当前时间。该数值表示的是子最近一次系统启动到当前的时间间隔,它和设备驱动程序无关,因为它的生命期只限于系统的运行期(uptime)。但驱动程序可以利用jiffies的当前值来计算不同事件间的时间间隔(比如在数设备驱动程序中就用它来分辨鼠标的单双击)。简而言之,利用jiffies值来测量时间间隔在大多数情况下已经够了,如果还需要测量更短的时间差,就只能利用处理器特定的寄存器了(但这会带来严重的兼容性问题)。
驱动程序一般不需要知道墙钟时间(之日常生活使用的时间,用年月日来表达),通常只有像cron和syslogd这样的用户程序才需要墙钟时间。对真实世界的时间处理通常最好留给用户空间,C函数库为我们提供了更好的支持。另外,这些代码通常具有更高的策略相关性,从而不能归于内核。但是内核也提供了将墙钟时间转换为jiffies值的函数:
#include <linux/time.h>
unsigned long mktime(unsigned int year,unsigned int mon,unsigned int day,unsigned int hour,unsigned int min,unsigned int sec);
直接处理墙钟时间意味着实现某种策略,因此我们应该仔细审视一下。
虽然内核空间中我们不必处理时间的人类可读取表达,但有时也需要处理绝对时间戳。为此,<linux/time.h>导出了do_gettimeofday函数。该函数用秒或者微妙值来填充一个指向struct timeval的指针变量--gettimeofday系统调用中用的也是同一变量。do_gettimeofday的原型如下:
#include<linux/time.h>
void do_gettimeofday(struct timeval *tv);
此内核源码表明do_gettimeofday在许多体系结构上有"接近微妙级的分辨率",因为它通过查询定时硬件而得出了已经流逝在当前jiffies上的时间。但是实际精度是随平台的不同而变化的,因为它依赖于实际使用的硬件机智。例如,某些处理器无法提供高于jiffies的分辨率。另一方面,奔腾系列可以通过读取本章前面描述的时间戳计数器来获得非常快而精确的子滴答度量值。
当前时间也可以通过xtime变量(struct timespec类型)获得,单精度要差一些。但是我们并不鼓励直接使用该函数,因为很难原子的访问timeval变量的两个成员。因此内核提供一个副主函数current_kernek_time:
#include <linux/time.h>
struct timespec current_kernel_time(void);
获取当前时间的代码可见于jit模块中。jit模块将创建/proc/currentime文件,读取该文件,将以ASSCII码的形式返回下面几项数据:
以十六进制表达的jiffies以及jiffies_64的当前值
由do_gettimeofday返回的当前时间
由current_kernel_time返回的timespec结构值
这是动态的/proc文件方式----为了输出这几个不多的文本信息,不值得创建一个完整的设备。