《linux设备驱动程式》中对时间流的一段分析

我们来看看内核代码是如何对时间问题进行处理的。按复杂程度递增排列,该问题包括: 理解内核时间机制 如何获得当前时间 如何将操作延迟指定的一段时间 如何调度异步函数到指定的时间后执行 内核中的时间间隔 我们首先要涉及的是时钟中断,操作系统通过时钟中断来确定时间间隔。中断是异步事件,通常由外部硬件触发。中断发生时,CPU停止正在进行的任务,转而执行另一段特别的代码(即中断服务例程,又称ISR)来响应这个中断。中断和 ISR 的实现将在第9章讨论。 时钟中断由系统计时硬件以周期性的间隔产生,这个间隔由内核根据 HZ 的值设定,HZ 是个和体系结构有关的常数,在文档 中定义。当前的Linux版本为大多数平台定义的 HZ 的值是100,某些平台上是 1024,IA-64 仿真器上是 20。驱动程式研发者不应使用任何特定的 HZ 值来计数,不管您的平台使用的是哪一个值。 当时钟中断发生时,变量jiffies的值就增加。jiffies在系统启动时初始化为0,因此,jiffies 值就是自操作系统启动以来的时钟滴答的数目,jiffies在头文档 中被定义为数据类型为 unsigned long volatile型变量,这个变量在经过长时间的连续运行后有可能溢出(但是现在还没有哪种平台会在运行不到 16 个月就使jiffies溢出)。为了确保 jiffies 溢出时内核仍能正常工作,人们已做了很多努力。驱动程式研发人员通常不用考虑 jiffies 的溢出问题,知道有这种可能性就行了。 假如想改变系统时钟中断发生的频率,能够修改HZ值。有人使用Linux处理硬实时任务,他们增加了HZ值以获得更快的响应时间,为此情愿忍受额外的时钟中断产生的系统开销。总而言之,时钟中断的最好方法是保留 HZ 的缺省值,因为我们能够完全相信内核的研发者们,他们一定已为我们挑选了最好值。 处理器特有的寄存器 假如需要度量很短的时间,或是需要极高的时间精度,能够使用和特定平台相关的资源,这是将时间精度的重要性凌驾于代码的可移植性之上的做法。 大多数较新的CPU都包含一个高精度的计数器,他每个时钟周期递增一次。这个计数器可用于精确地度量时间。由于大多数系统中的指令执行时间具备不可预测性(由于指令调度、分支预测、缓存等等),在运行具备很小时间粒度的任务时,使用这个时钟计数器是唯一可靠的计时方法。为适应现代处理器的高速度,满足衡量性能指标的紧迫需求,同时由于CPU设计中的多层缓存引起的指令时间的不可预测性,CPU 的制造商们引入了记录时钟周期这一测量时间的简单可靠的方法。所以绝大多数现代处理器都包含一个随时钟周期不断递增的计数寄存器。 基于不同的平台,在用户空间,这个寄存器可能是可读的,也可能不可读;可能是可写的,也可能不可写;可能是64位的也可能是32位的。假如是 32 位的,还得注意处理溢出的问题。无论该寄存器是否能够置0,我们都强烈建议不要重置他,即使硬件允许这么做。因为总能够通过多次读取该寄存器并比较读出数值的差异来完成要做的事,我们无须需要独占该寄存器并修改他的当前值。 最有名的计数器寄存器就是TSC(timestamp counter,时间戳计数器),从x86的Pentium处理器开始提供该寄存器,并包括在以后的任何 CPU 中。他是个64位寄存器,记录 CPU时钟周期数,内核空间和用户空间都能够读取他。 包含了头文档 (意指“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\en", end - ini); 其他一些平台也提供了类似的功能,在内核头文档中更有一个和体系结构无关的函数能够代替rdtsc,他就是get_cycles,是在2.1版的研发过程中引入的。其原型是: #include 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读取”的汇编指令: =======footnote begins============= nop 指令是必需的,防止了编译器在指令mfc0之后立即访问目标寄存器。这种互锁(interlock)在 RISC处理器中是很典型的,在延迟期间编译器仍然能够调度其他指令执行。我们在这里使用nop,是因为内嵌汇编指令对编译器来说是个黑盒,不能进行优化。 =======footnote ends============= #define rdtscl(dest) \e _ _asm_ _ _ _volatile_ _("mfc0 %0,; nop" : "=r" (dest)) 通过使用这个宏,MIPS处理器就能够执行和前面所示用于x86的相同的代码了。 gcc内嵌汇编的有趣之处在于通用寄存器的分配使用是由编译器完成的。这个宏中使用的 %0只是“参数0”的占位符,参数0由随后的“作为输出(=)使用的任意寄存器(r)”定义。该宏还说明了输出寄存器要对应于 C 表达式dest 。内嵌汇编的语法功能强大但也比较复杂,特别是在对各寄存器使用有限制的平台上更是如此,如x86系列。完整的语法描述在gcc文档中提供,一般在info中就可找到。 这节展示的短小的C代码段已在一个K7类的x86处理器和一个MIPS VR4181处理器(使用了刚才的宏)上运行过了。前者给出的时间消耗为11时钟周期,后者仅为2时钟周期。这是能够理解的,因为RISC处理器通常每时钟周期运行一条指令。 获取当前时间 内核一般通过jiffies值来获取当前时间。该数值表示的是自最近一次系统启动到当前的时间间隔,他和设备驱动程式不怎么相关,因为他的生命期只限于系统的运行期(uptime)。但驱动程式能够利用jiffies的当前值来计算不同事件间的时间间隔(比如在输入设备驱动程式中就用他来分辨鼠标的单双击)。简而言之,利用jiffies值来测量时间间隔在大多数情况下已足够了,假如还需要测量更短的时间,就只能使用处理器特有的寄存器了。 驱动程式一般无需知道墙钟时间(指日常生活使用的时间),通常只有象 cron 和 at 这样用户程式才需要墙钟时间。需要墙钟时间的情形是使用设备驱动程式的特别情况,此时能够通过用户程式来将墙钟时间转换成系统时钟。直接处理墙钟时间常常意味着正在实现某种策略,应该仔细审视一下是否该这样做。 假如驱动程式真的需要获取当前时间,能够使用do_gettimeofday函数。该函数并不返回今天是本周的星期几或类似的信息;他是用秒或微秒值来填充一个指向struct timeval的指针变量,gettimeofday系统调用中用的也是同一变量。do_gettimeofday的原型如下: #include

文章整理:西部数码--专业提供域名注册虚拟主机服务
http://www.west263.com
以上信息与文章正文是不可分割的一部分,如果您要转载本文章,请保留以上信息,谢谢!