时间、延迟及延缓操作

现实中的设备驱动程序除了必需的操作之外还要做更多工作,如定时、内存管理、硬件访问等,现在来看看内核是如何对时间进行处理的。

关于时钟的一些说明:一台装有操作系统的计算机里一般有两个时间:硬件时钟软件时钟。

硬件时钟

硬件时钟就是CMOS时钟,由小型电池供电,并且有精确的高频晶振来来提供节拍。当计算机断电时CMOS可以继续运行,这就是计计算机总知道正确的时期和时间的原因。

硬件时钟的第一个作用当然是用来计时的,另一个作用就是用它的高频晶振来产生精确的时钟中断。

linux初始化时,iniit_IRQ函数设定晶振芯片的定时周期为10ms(根据你所定义的HZ值来确定,这里假设为100)。这样芯片每10ms就会产生一个时钟中断信号到系统。

HZ:是时钟中断的频率,在2.4及更早的内核版本中,x86平台上HZ值定义为100,现在x86平台上默认定义为1000

LATCH:每个时钟中断间隔的硬件频率计数,也就是晶振芯片产生多少个节拍发生一次时钟中断

HZ*LATCH=晶振频率

如果想改变系统时钟中断发生的频率,可以通过修改HZ的值来进行。如果修改了头文件中的HZ值,则必须重新编译内核以及所有模块。

软件时钟

软件时钟则是由操作系统维护的,所以又称为系统时钟。这个时钟在系统启动时读取硬件时钟获得的,在读完之后就完全由系统本身维护。之所以用两套时钟的原因是因为读取硬件时钟太麻烦,并且所耗时间长。本章主要就是讨论软件时钟。

度量时间差

在Linux系统中,起作用的主要是软件时钟即系统时钟。这个时钟的初始值在启动时从CMOS中读取,然后就由Linux的内核来维护。它在系统中是用从1970年1月1日00:00:00(UNIX纪元)开始算起的累积秒数来表示的。

Linux的系统时钟以读取的硬件时钟为起点,然后根据系统启动后的时钟中断数来计算时间。所有的系统计时都基于这种量度。内核有有一个内部计算器来记录操作系统引导以来的时钟中断数,这个计数器在系统引导时初始化为0,第次时钟中断发生时,这个计数器的值就增加一。这个计算器是一个64位的变量(即使在32位架构上也是64位),称为"jiffies_64"。但是驱动程序开发者通常访问的是jiffies变量,它是unsigned long型的变量,要么和jiffies_64相同,要么仅仅是jiffies_64的低32位。通常首选使用jiffies,因为对它的访问很快,从而对jiffies_64的访问并不需要在所有架构上都是原子的。

除了由内核管理的低分辨率jiffy机制,某些CPU平台还包含有一个软件可读的高分辨率计数器。尽管这个计算器在不同的平台下使用方法有所不同,但在某些情况下仍是一个非常强大的工具。

使用jiffies计数器

该计数器和读取计数器的工具包含在<linux/jiffies.h>中,但包含<linux/sched.h>文件就行了。jiffies和jiffies_64均应被看成只读变量。

代码需要计算未来的时间戳时,必须读取当前的计算器,可简单访问上面说过的unsigned long变量:

#include <linux/jiffies.h>
unsigned long j, stamp_1, stamp_half, stamp_n;
j = jiffies;
stamp_1 = j + HZ;           //未来一秒
stamp_half = j + HZ/2;       //半秒
stamp_n = j + n * HZ / 1000; //毫秒

采用正确的方法来比较不同的值,上述代码就不会因为jiffies的溢出而出现问题。比较缓存值和当前值时就使用下面的宏:

int timer_after(unsigned long a, unsigned long b); a在b后返回真
int timer_before(unsigned long a, unsigned long b);a在b前返回真
int timer_after_eq(unsigned long a, unsigned long b);
int timer_before_eq(unsigned long a, unsigned long b);

上面的宏将计数器值转换为signed long,相减然后比较结果。如果需要以安全的方式计算两个jiffies实例之间的差,可以使用相同的技巧:

diff = (long)t2 - (long)t1;

msec = diff * 1000/HZ; 将两个jiffies的差转换为毫秒值。

有时我们需要将用户空间的时间表述方法(struct timeval 和 struct timespec)和内核表述方法进行转换。在老的、流行的struct timeval中使用秒和毫秒值,而较新的struct timespce中则使用秒和纳秒。为了完成jiffies值和这些结构间的转换,内核提供了下面四个辅助函数:

#include <linux/time.h>
unsigned long timespec_to_jiffies(struct timespec *value);
void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
unsigned long timeval_to_jiffies(struct timeval *value);
void jiffies_to_timeval(unsigned long jiffies, struct timeval *value);

对64位jiffies_64的访问不像对jiffies的访问那样直接。有可以是同一个,有可能是低32位,如果必须读取64位,则应该使用内核导出的一个特殊辅助函数,该函数为我们完成了适当的锁定:

#include <linux/jiffies.h>
u64 get_jiffies_64(void); //u64代表了一个无符号的64位类型。

注意:实际的时钟频率(HZ)对用户来讲几乎是完全不可见的。当用户空间包含param.h时,HZ宏始终被扩展为100,而每个报告给用户空间的计数器值均做了相应的转换

处理器特定的寄存器

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

 CPU制造商引入了一种通过计算时钟周期来度量时间差的简便而可靠的方法,绝大多数现代处理器都包含一个随时钟周期不断递增的计数寄存器,这是时钟计数器是完成高分辨率计时任务的唯一可靠途径。

包含头文件<asm/msr.h>(x86专用的头文件)之后,就可以使用以下宏:

rdtsc(low32, high32);原子性地把64位数值读到两个32位变量中
rdtscl(low32);把寄存器的低伴部分读入一个32位变量
rdtscll(var64);将64位值读入一个long long型变量

在大多数常见的TSC应用中,读取计数器的低半部分就够了。1-GHz的处理器每4.2秒才会溢出,所以,如果我们度量的时间差确定很短的话,就不需要处理多个寄存器值。但是将来肯定需要读取64位的计数器值。

下面这段代码仅仅使用了该寄存器的低半部分,可用来测量该指令自身的运行时间:

unsigned long ini, end;
rdtscl(ini); rdscl(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.

下面是基于MIP处理器的一段内嵌的汇编代码,之所以基于它是因为大多数MIPS处理器都有一个32位的计数器,为了从内核读取该寄存器,可以定义下面的宏,它执行从“coprocessor 0(称为寄存器9)读取”的汇编指令:
#define rdtscl(dest) \
    __asm__ __volatile__("mfc0 %0, $9; nop" : "=r" (dest))
通过使用这个宏,MIPS处理器就可以执行前面用于x86的代码了。

获取当前时间

内核一般通过jiffies值来获取当前时间。该数值表示的是自最近一次系统启动到当前的时间间隔。驱动程序可以利用jiffies的来当前值来计算不同事件间的时间间隔(如在输入设备驱动程序是可以用它来分辨鼠标的单双击)。

驱动程序一般不需要知道墙时间(指日常生活使用的时间,用年月日来表达),通常只有像cron和syslogd这样的用户程序才需要知道墙钟时间。对真实世界的时间处理通常最好留给用户空间,C函数库为我们提供了更好的支持。另外这些代码通过具有更高的策略相关性,从而不能归于内核。但是内核也提供了将墙时钟转换为jiffies值的函数:

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);

该函数在许多体系结构上有“接近微秒级的分辨率”,因它通过查询定时硬件而得出了已经流逝在当前jiffies上的时间。但是实际精度是随平台的不同而变化的,因为它依赖于实际使用的硬件机制。

当然也可以通过xtime变量(类型为struct timespec)来获得,但精度要差一些,我们并不鼓励直接使用该变量,内核提供了一个辅助函数current_kernel_time:

 #include <linux/time.h>
struct timespec current_kernel_time(void);

延迟执行

设备驱动程序经常需要某些特定的代码延迟一段时间后执行,通常是为了让硬件能完成某些任务。本节将介绍许多延迟的不同技术,并介绍各自的优缺点,使用哪种技术最取取决于实际环境中的具体情况。

本章我们把涉及多个时钟滴答的延迟称为长延迟,而非常短的延迟通常必须通过软件循环的方式实现。

下面讨论了许多不同的方案,从直觉但并不合适的方案到正确的方案。

长延迟

有时驱动程序需要延迟比较长的时间,即长于一个时钟滴答。实现这种类型的延迟有好几种途径,我们先讲简单的,再讲高级的。

忙等待

如果想延若干个时钟滴答,或者对延迟的精度要求不高,最简单(不推荐)的实现方法就是一个监视jiffies计数器的循环。这种忙等待的实现方法通常具有下面的形式,其中j1是延迟终止时的jiffies值:

while(time_before(jiffies, j1)
    cpu_relax();//该函数也许根本不会做任何事情;在对称多线程系统上,它可能将处理器让给其他线程。

尽管以上代码是正确的实现,但这个忙等待会严重降低系统性能。如果内核不是抢占的那么这段时间计算机看起来就像是死掉的,如果运行的是抢占式内核,则问题不会有这么严重,但忙等待仍然有些浪费。更糟糕的是,如果在在进入循环之前关闭了中断,则jiffies值就不会得到更新,那么while循环就将永远执行下去。

让出处理器

我们已经看到了忙等待增加了系统整体的学生负担,因此有必要寻找更好的延迟技术。我们能想到的一种技术就是不需要CPU时让出CPU,这可以通过调用schedule函数实现

while(time_before(jiffies, j1)
    schedule();

但是,上面这种实现仍然不是优化的技术。当前进程虽然释放了CPU而不做任何事情,但它仍然在运行队列中。如果系统中只有一个可运动的进程,则该进程又会立即运行。换句话说机器的负荷至少为一,而空闲任务从来不会运行。尽管这个问题看起来无关痛痒,但是在计算机空闲运行空闲任务可减轻处理器负荷,降低处理器温度并增加它的寿命,如果计算机是一台笔记本电脑,这种效果对笔记本电脑的电池也是一样的。

此外当系统越来越忙时,会导致驱动程序等待更长的时间。当一个进程使用schedule释放处理器之后,没有任何保证说进程可以在随后很快就得到处理器。

因此,除了影响计算机系统的整体性能之外,上面这种调用schedule的方法对驱动程序需求来讲并不安全。

越时

到目前为止,通过监视jiffies计数器实现的延迟可以工作,但是并理想。实现延迟的最好方法应该是让内核为我们完成相应工作。

如果驱动程序使用等待队列来等待其他一些事件,而我们同时又希望在特定的时间段中运行,则可以使用wait_enent_timeout或者wait_event_interruptible_timeout函数,包含<linux/wait.h>

long wait_event_timeout(wait_queue_head_t queue, condition, long timeout);
long wait_event_interruptible_timeout((wait_queue_head_t queue,conditiion, long timeout);

上述函数会在给定的等待队列上休眠,但是会在超时到期时返回。这样这两个函数实现了一种有界的休眠,这种休眠不会永远继续。注意这里的timeout值表示的是要等待的jiffies值,而不是绝对时间值。这个值用有符号数表示。因为有些情况下它是相关的结果。当提供的超时值是负数时,这两个函数会通过一条printk语句产生抱怨信息。如果超时到期会返回零;而如果里程由其他事件唤醒,则会返回剩余的延迟数,并用jiffies。返回值从来不会是负数,即使因为系统负荷导致真正的延迟时间超过预期。

如果没有需要等待的事件,传入的条件是0:

wait_queue_head_t wait;
init_waitqueue_head(&wait);
wait_event_interruptible_timeout(wait, 0, delay);

因为没有人会在等待队列上调用wake_up(毕竟没有其他代码知道这个等待队列),因此进程始终会在超时到期时被唤醒。为了适应这种特殊情况(即不等待特定事件而延迟),内核为我们提供了schedule_timeout函数

#include <linux/sched.h>
signed long schedule_timeout(signed long timeout);

timeout是用jiffies表示的延迟时间。正常的返回值是0,除非在给定超时值到期前函数返回(比如响应某个信号)。

短延迟

当设备驱动程序需要处理硬件的延迟(latency)时,这种延迟通常最多涉及到几十个这毫秒。在这种情况下,依赖于时钟滴答显然是不正确的方法。

ndelay、udelay和mdelay这几个内核函数可很好的完成短延迟任务,它们分别延迟指定数量的纳秒、微秒、毫秒,它们原型如下:

#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);

这些函数的实现和具体的体系架构相关。真正实现的延迟至少会达到所请求的时间值,并可能会更长,延迟超过所请求的值通常不是问题,因为驱动程序的短延迟通常等待的是硬件,而需求往往是至少要等待给定的时间段。

要重点记住的是,这三个延迟函数均是忙等待函数,因而在延迟过程中无法运行其他任务。

实现毫秒级的延迟还有另一种方法,这种方法不涉及忙等待。<linux/delay.h>中声明了下面这些函数:

void msleep(unsigned int millisecs);不可中断休眠
unsigned long msleep_interruptible(unsigned int millisecs);通常返回0,如果被中断返回剩余毫秒数
void ssleep(unsigned int seconds);不可中断休眠,休眠时间以秒记

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值