CSDN博客

img collide

linux 设备驱动程序 时间流 总结

发表于2004/9/27 20:16:00  2577人阅读

分类: dsr阅读源码相关 Linux 内核研究


第 6 章  时间流


至此,我们基本知道怎样编写一个功能完整的字符模块了。现实中的设备驱动程序,除了实现必需的操作外还要做更多工作,如计时、内存管理,硬件访问等等。幸好,内核中提供的许多机制可以简化驱动程序开发者的工作,我们将在后面几章陆续讨论驱动程序可以访问的一些内核资源。本章,我们先来看看内核代码是如何对时间问题进行处理的。按复杂程度递增排列,该问题包括:


  • 理解内核时间机制
  • 如何获得当前时间
  • 如何将操作延迟指定的一段时间
  • 如何调度异步函数到指定的时间后执行


6.1  内核中的时间间隔

我们首先要涉及的是时钟中断,操作系统通过时钟中断来确定时间间隔。中断是异步事件,通常由外部硬件触发。中断发生时,CPU 停止正在进行的任务,转而执行另一段特殊的代码(即中断服务例程,又称 ISR)来响应这个中断。中断和 ISR 的实现将在第 9 章讨论。

时钟中断由系统计时硬件以周期性的间隔产生,这个间隔由内核根据 HZ 的值设定,HZ 是一个与体系结构有关的常数,在文件 <linux/param.h> 中定义。当前的Linux版本为大多数平台定义的 HZ 的值是 100,某些平台上是 1024,IA-64 仿真器上是 20。驱动程序开发者不应使用任何特定的 HZ 值来计数,不管你的平台使用的是哪一个值。

当时钟中断发生时,变量 jiffies 的值就增加。jiffies 在系统启动时初始化为 0,因此,jiffies 值就是自操作系统启动以来的时钟滴答的数目,jiffies 在头文件 <linux/sched.h> 中被定义为数据类型为 unsigned long volatile 型变量,这个变量在经过长时间的连续运行后有可能溢出(不过现在还没有哪种平台会在运行不到 16 个月就使 jiffies 溢出)。为了保证 jiffies 溢出时内核仍能正常工作,人们已做了很多努力。驱动程序开发人员通常不用考虑 jiffies 的溢出问题,知道有这种可能性就行了。

如果想改变系统时钟中断发生的频率,可以修改 HZ 值。有人使用 Linux 处理硬实时任务,他们增加了 HZ 值以获得更快的响应时间,为此情愿忍受额外的时钟中断产生的系统开销。总而言之,时钟中断的最好方法是保留 HZ 的缺省值,因为我们可以完全相信内核的开发者们,他们一定已经为我们挑选了最佳值。

6.1.1  处理器特有的寄存器

如果需要度量非常短的时间,或是需要极高的时间精度,可以使用与特定平台相关的资源,这是将时间精度的重要性凌驾于代码的可移植性之上的做法。

大多数较新的 CPU 都包含一个高精度的计数器,它每个时钟周期递增一次。这个计数器可用于精确地度量时间。由于大多数系统中的指令执行时间具有不可预测性(由于指令调度、分支预测、缓存等等),在运行具有很小时间粒度的任务时,使用这个时钟计数器是唯一可靠的计时方法。为适应现代处理器的高速度,满足衡量性能指标的紧迫需求,同时由于 CPU 设计中的多层缓存引起的指令时间的不可预测性,CPU 的制造商们引入了记录时钟周期这一测量时间的简单可靠的方法。所以绝大多数现代处理器都包含一个随时钟周期不断递增的计数寄存器。

基于不同的平台,在用户空间,这个寄存器可能是可读的,也可能不可读;可能是可写的,也可能不可写;可能是 64 位的也可能是 32 位的。如果是 32 位的,还得注意处理溢出的问题。无论该寄存器是否可以置 0,我们都强烈建议不要重置它,即使硬件允许这么做。因为总可以通过多次读取该寄存器并比较读出数值的差异来完成要做的事,我们无须要求独占该寄存器并修改它的当前值。

最有名的计数器寄存器就是 TSC(timestamp counter,时间戳计数器),从 x86 的 Pentium 处理器开始提供该寄存器,并包括在以后的所有 CPU 中。它是一个 64 位寄存器,记录 CPU 时钟周期数,内核空间和用户空间都可以读取它。

包含了头文件 <asm/msr.h>(意指“machine-specific registers,机器特有的寄存器”)之后,就可以使用如下的宏:



rdtsc(low,high);
rdtscl(low);



前一个宏原子性地把 64 位的数值读到两个 32 位变量中;后一个只把寄存器的低半部分读入一个 32 位变量,在大多数情况,这已经够用了。举例来说,一个 500MHz 的系统使一个 32 位计数器溢出需 8.5 秒,如果要处理的时间肯定比这短的话,那就没有必要读出整个寄存器。

下面这段代码可以测量该指令自身的运行时间:



unsigned long ini, end;
rdtscl(ini); rdtscl(end);
printk("time lapse: %li/n", end - ini);



其他一些平台也提供了类似的功能,在内核头文件中还有一个与体系结构无关的函数可以代替 rdtsc,它就是 get_cycles,是在2.1版的开发过程中引入的。其原型是:



#include <linux/timex.h>
cycles_t get_cycles(void);



在各种平台上都可以使用这个函数,在没有时钟周期记数寄存器的平台上它总是返回 0。cycles_t 类型是能装入对应 CPU 单个寄存器的合适的无符号类型。选择能装入单个寄存器的类型意味着,举例来说,get_cycles 用于 Pentium 的时钟周期计数器时只返回低 32 位。这种选择是明智的,它避免了多寄存器操作的问题,与此同时并未阻碍对该计数器的正常用法,即用来度量很短的时间间隔。

除了这个与体系结构无关的函数外,我们还将示例使用一段内嵌的汇编代码。为此,我们来给 MIPS 处理器实现一个 rdtscl 函数,功能就象 x86 的一样。

这个例子之所以基于 MIPS,是因为大多数 MIPS 处理器都有一个 32 位的计数器,在它们的内部“coprocessor 0”中命名为 register 9 寄存器。为了从内核空间读取该寄存器,可以定义下面的宏,它执行“从 coprocessor 0 读取”的汇编指令:*



#define rdtscl(dest) /
   _ _asm_ _ _ _volatile_ _("mfc0 %0,$9; nop" : "=r" (dest))





注:nop 指令是必需的,防止了编译器在指令mfc0之后立刻访问目标寄存器。这种互锁(interlock)在 RISC处理器中是很典型的,在延迟期间编译器仍然可以调度其它指令执行。我们在这里使用nop,是因为内嵌汇编指令对编译器来说是个黑盒,不能进行优化。



通过使用这个宏,MIPS 处理器就可以执行和前面所示用于 x86 的相同的代码了。

gcc 内嵌汇编的有趣之处在于通用寄存器的分配使用是由编译器完成的。这个宏中使用的 %0 只是“参数 0”的占位符,参数 0 由随后的“作为输出(=)使用的任意寄存器(r)”定义。该宏还说明了输出寄存器要对应于 C 表达式 dest。内嵌汇编的语法功能强大但也比较复杂,特别是在对各寄存器使用有限制的平台上更是如此,如 x86 系列。完整的语法描述在 gcc 文档中提供,一般在 info 中就可找到。

这节展示的短小的C代码段已经在一个 K7 类的 x86 处理器和一个 MIPS VR4181 处理器(使用了刚才的宏)上运行过了。前者给出的时间消耗为 11 时钟周期,后者仅为 2 时钟周期。这是可以理解的,因为 RISC 处理器通常每时钟周期运行一条指令。

6.2  获取当前时间

内核一般通过 jiffies 值来获取当前时间。该数值表示的是自最近一次系统启动到当前的时间间隔,它和设备驱动程序不怎么相关,因为它的生命期只限于系统的运行期(uptime)。但驱动程序可以利用 jiffies 的当前值来计算不同事件间的时间间隔(比如在输入设备驱动程序中就用它来分辨鼠标的单双击)。简而言之,利用 jiffies 值来测量时间间隔在大多数情况下已经足够了,如果还需要测量更短的时间,就只能使用处理器特有的寄存器了。

驱动程序一般不需要知道墙钟时间(指日常生活使用的时间),通常只有象 cron 和 at 这样用户程序才需要墙钟时间。需要墙钟时间的情形是使用设备驱动程序的特殊情况,此时可以通过用户程序来将墙钟时间转换成系统时钟。直接处理墙钟时间常常意味着正在实现某种策略,应该仔细审视一下是否该这样做。

如果驱动程序真的需要获取当前时间,可以使用 do_gettimeofday 函数。该函数并不返回今天是本周的星期几或类似的信息;它是用秒或微秒值来填充一个指向 struct timeval 的指针变量,gettimeofday 系统调用中用的也是同一变量。do_gettimeofday 的原型如下:



#include <linux/time.h>
void do_gettimeofday(struct timeval *tv);



源码中描述 do_gettimeofday 在许多体系结构上有“接近微秒级的分辨率”,然而实际精度是随不同的平台而变化的,在旧版本的内核中还会低些。当前时间也可以通过 xtime 变量(类型为struct timeval)获得(但精度差些),但是,并不鼓励直接使用该变量,因为除非关闭中断,否则无法原子性地访问 timeval 变量的两个成员 tv_sec 和 tv_usec。在 2.2 版的内核中,一个快捷安全的获得时间的办法(可能精度会差些)是使用 get_fast_time:



void get_fast_time(struct timeval *tv);



获取当前时间的代码可见于 jit(“Just In Time”)模块,源文件可以从 O'Reilly 公司的 FTP 站点获得。jit 模块将创建 /proc/currentime 文件,读取该文件将以 ASCII 码的形式返回三项:


  • 由 do_gettimeofday 返回的当前时间
  • 从 xtime 钟获得的当前时间
  • jiffies 的当前值


我们选择用动态的 /proc 文件,是因为这样模块代码量会小些――不值得为返回三行文本而写一个完整的设备驱动程序。

如果用 cat 命令在一个时钟滴答内多次读该文件,就会发现 xtime 和 do_gettimeofday 两者的差异了,xtime 更新的次数不那么频繁:



morgana% cd /proc; cat currentime currentime currentime
gettime: 846157215.937221
xtime:   846157215.931188
jiffies: 1308094
gettime: 846157215.939950
xtime:   846157215.931188
jiffies: 1308094
gettime: 846157215.942465
xtime:   846157215.941188
jiffies: 1308095
阅读全文
0 0

相关文章推荐

img
取 消
img