Timers and Time Management
时间参数的传入对于内核而言是很重要的。相对于事件驱动的函数,内核当中还有一大部分事件驱动的函数存在。他们中的有些事周期性的,比如说调度器的runqueue、或者刷新屏幕。They occur on a fixed scheduler,比如说每秒钟100次。内核调度其他的函数,比如说在将来的某一刻延迟disk I/O。又比如说,内核会在500毫秒后调度工作。Finally,内核必须管理系统uptime和当前日期、时间。
需要注意到相对时间和绝对时间。在将来的五秒后调度事件是不要求绝对时间的,仅仅是相对时间。相反地,管理系统的当前时间要求内核不仅仅理解passing time,还要求some measurement of it.
Moreover,上述两种事件的区别在于内核对于周期性时间和内核调度的将来事件的两种不同处理方式。周期性发生的时间(比如说每十秒发生的事件)是系统定时器驱动的。系统定时器是可编程的硬件以一定的频率触发的。该定时器的Interrupt handler更新系统时间并执行周期性的工作。系统定时器和他的定时器handler对于Linux必不可少,也是本章的重中之重。
本章关注的其他地方在于动态定时器,他们被用来调度那些在特定时间后执行的事件。比如说,floopy设备驱动会在设备一定时间不活动后,利用这种定时器去关闭floopy电机。内核可以动态的创建和删除定时器。本章讲解了内核对于动态定时器的实现,以及相关的调用接口。
11.1 Kernel Notion of Time
Certainly,对于一台计算机而言,时间的概念不是很clear。Indeed,内核通过系统的硬件去理解和管理时间。硬件提供了系统定时器,内核利用它来计算时间。系统定时器工作于一定的电信号,比如说处理器的数字时钟信号。系统定时器以预定的时钟频率运行,called tick rate。当系统定时器goes off,它会触发它的中断handler。
由于内核已知设定好的tick rate,it knows the time between any two successive timer interrupts.这一周期叫做一个tick,它等于1/(tickrate)。This is how内核如何知晓系统的wall time和upime(uptime:the relative time since the system booted up;wall time:the actual time of day.)。两个正常运行时间读数之间的差异只是相对性的一个简单度量。
定时器中断对于管理操作系统至关重要。大量的内核函数live and die by the passing of time.有些工作通过定时器中断周期性的执行,包括:
更新系统uptime
更新time of day
在SMP系统,确保调度器runqueue是平衡的,如果不平衡的话,balancing them
在动态定时器expired后运行对应的handler
更新资源的使用情况和处理时间静态参数
这些工作中有些是每一个定时器中断发生后都会执行----that is,the work is carried out with the frequency of the tick rate.其他的函数也会周期性的执行,but only every n timer interrupts.That is,这些函数在以一定的tick rate执行。“The Timer Interrupt Handler”章节会详细介绍到定时器中断handler。
11.2 The Tick Rate:HZ
系统定时器频率(The tick rate)是基于处理器编写的,HZ。不同的支持的处理器这项参数是不同的。在一些支持的设备中,不同的设备类型也可能是不同的。
内核定义HZ参数在文件<asm/param.h>。The tick rate has a frequency of HZ hertz and a period of 1/HZ seconds.比如说X86设备默认定义HZ为100.因此,在i386机器上,定时器中断拥有着100HZ和每秒钟100次触发。。其他common的HZ的参数是250和1000,相对的周期是4毫秒和1毫秒。表11.1列出了支持的硬件设备的tick rate值。
当编写内核代码的时候,never assume that HZ有着一个给定的参数。因为不同的设备拥有着不同的tick rate值。但是,In the past,Alpha是唯一HZ值不是100的设备。
定时器中断的频率是很重要的。正如你所看到的,定时器中断执行了很多的工作。Indeed,内核对于时间的感知能力都来自于系统周期性的定时器。
11.2.1 The Ideal HZ Value
在Linux的初始版本中,i386已经拥有了一个100HZ的定时器中断。但是,在2.5开发系列中,频率上升到了1000HZ,这是颇具争议的。现在来说,用户可以在内核的配置当中配置所需的HZ参数。因为so many of the system依赖于定时器中断,根据系统来配置更改频率是合情合理的。当然,这肯定存在着Pros和cons to larger versus smaller HZ values:
定时器中断有着更高的精度,因此所有的时间事件有着更高的精度
时间事件的准确度更好了
随着tick rate的提升,精度也会响应地提升。比如说,当HZ为100的时候,定时器的精度是10毫秒。换句话说,所有的周期性事件遵循着10毫秒的周期并且精确度也不会更好了。但是,当HZ为1000时,精度是1毫秒,是优于10毫秒的。尽管内核代码可以创建具有1毫秒分辨率的计时器,但不能保证hz=100提供的精度足以在任何大于10毫秒的间隔上执行计时器。
相似地,准确度也响应的提升了。假设内核在一个任意的时间开启了定时器,由于计时器可能随时过期,但仅在计时器中断出现时执行,因此平均计时器的关闭时间为计时器中断时间的一半。例如,当hz=100时,平均事件发生时间与期望时间相差正/–5毫秒。因此,平均错误为5毫秒。当hz=1000时,平均错误下降到0.5毫秒,提高了10倍。
11.2.2 Advantages with a Larger HZ
更高的精度和更好的准确度提供了诸多优点:
拥有着更好精度的内核定时器增加了准确性
系统调用poll和select提升了准确性(他们都会有timeout参数属性)
时间计量有着更好的精度
进程抢占发生的更加准确
更高tick rate的好处是进程抢占的更加精确,which降低了调度延迟。第四章提到的,定时器中断是负责decrement正在运行进程的时间片计数的。当count计数为0时,need_resched被设置,内核调用调度器执行进程调度as soon as possible。现在,假设一个给定的进程正在运行,还拥有着两毫秒的时间片。在两毫秒内,调度器应该抢占当前运行的进程并开始执行一个新的进程。Unfortunately,this even直到下一个定时器中断到来之前不会occur,which may not in两毫秒。最坏的情况下,下一个定时器中断可能是1/hz秒!当hz=100时,一个进程可以获得将近10毫秒的额外运行时间。Of course,this all balances out and fairness is preserved,因为所有的任务都接收到了相同的不确定性。问题来自于延迟调度产生的延时。如果被调度的任务有些时间sensitity事情的话,比如说填充音频的buffer,这样的延迟可能是不能接受的,增加tick rate到1000HZ,降低了最坏情况的下的调度overrun到了1毫秒,平均overrun仅仅为0.5毫秒。
11.2.3 Disadvantage with a Larger HZ
现在我们需要讨论下,增加tick rate的缺点,or我们从1000HZ开始讨论。Indeed,there is one larger issue:更高的tick rate会产生更多的定时器中断,which implies higher overhead,因为处理器需要更多的时间来执行定时器中断handler。The tick rate越高,处理器执行定时器中断handler的时间越多。这会让处理器留给其他工作的时间变少,以及更加频繁的处理器cache命中和增加了电源消耗。这样的overhead是具有争议的。将HZ从100增加至1000很明显的增加了10倍overhead。但是,这样的overhead到底有多严重呢?最终的结论是,至少在现代的系统中,HZ=1000不会产生不可接受的overhead并且更改到HZ=1000也不会牺牲过多的性能。无论怎样,在2.6内核中,HZ是可以配置的。
11.3 Jiffies
全局变量jiffies holds 着自系统启动后的tick的number。On boot,内核初始化该参数为0,并且随着定时器中断的触发,他的值会增加。Thus,因为每秒钟产生了HZ次定时器中断,一秒钟也产生了HZ大小的jiffies。因此系统的uptime是jiffies/HZ秒。What actually happened is more complicated:内核初始化jiffies为特定的初始值,导致变量更加频繁的overflow,catching bugs. 当寻找jiffies的实际值时,首先减去这个“offset”。
Jiffies变量定义在<linux/jiffies.h>中:
Extern unsigned long volatile jiffies;
在下一章,我们讨论他的实际的准确定义。现在,我们look at内核的一些实例。下面的例子是由秒转换为jiffies:
(seconds*HZ)
相似地,下面的表达式是由jiffies转换为秒:
(jiffies/HZ)
前者(将秒转换为ticks),应用更加普遍;比如说,code需要设置一些未来的时间,for example:
将tick转换为秒通常来说是用来跟用户空间通信的,因为内核代码很少关注绝对时间。
Note that jiffies变量为unsigned long类型。
11.3.1 Internal Representation of Jiffies
变量jiffies总是定义为unsigned long,因此32位处理器上面大小是32位的,64位处理器上面是64位的。假设说tick rate为100,一个32位处理器的jiffies变量在497天后会overflow。但是假设说HZ为1000的话,overflow仅仅在49.7天后就会发生了。如果jiffies存储在64位变量中on all architectures,那么设置任意合理的HZ值,jiffies变量都不会overflow了。
由于性能和历史原因(主要是处于兼容已存在的内核代码),内核开发者想要保持jiffies变量为unsigned long。Some smart thinking and a little linker magic saved that day.
如你之前所看到的,jiffies是定义为unsigned long:
Extern unsigned long volatile jiffies;
第二种变量定义的方式在<linux/jiffies.h>:
Extern u64 jiffies_64;
脚本ld(1)用来连接main kernel image,然后将jiffies变量覆盖在jiffies_64变量的开头:
Jiffies = jiffies_64;
因此,jiffies是64位变量jiffies_64的低32位。代码还可以和之前一样的正确的访问到jiffies变量。因为most code仅适用jiffies来简单地及时计量elapses,most code仅仅关注低32位数据。然而,时间管理代码使用了整个64位,因此防止了整个64位值的溢出。图11.1展示了jiffies和jiffies_64的layout:
访问jiffies的代码仅仅访问jiffies_64的低32位数据。函数get_jiffies_64()能够用来读取全部的64位数据。这种需求是不常见的;因此,大多数代码仅仅通过jiffies变量读取低32位数据即可。
在64位架构中,jiffies_64和jiffies是相同的。代码访问变量jiffies和调用get_jiffies_64()获得的结果是一致的。
11.3.2 Jiffies Wraparound
当变量jiffies增加到最大存储值的限制后,变量overflow。对于32位unsigned interger,这个最大值是2的32次幂减一;定时器tick 4294967295次数后,到达overlow界限。当tick值到达了最大值并继续增加,它会归零。
Look at an example of a wraparound:
Unsigned long timeout = jiffies + HZ/2; /* timeout in 0.5s*/
/* do some work …… */
/* then see whether we took too long */
If (timeout > jiffies){
/* we did not time out, good */
} else {
/* we time out, error … */
}
上述代码演示的是设置一段0.5秒的超时时间,紧接着执行了一段工作,后者说poking硬件或者等待一个回应。工作完成后,如果完成工作的时间长于已经设置的0.5秒timeout,则代码出错并处理。
存在着很多潜在的overflow的情景,我们选择其中的一个进行讲解:如果jiffies在timeout归零后,接下来会做什么工作。第一个条件可能会失败,因为jiffies可能小于timeout,虽然logicallly它是更大的。因为它超过了最大值,它现在是一个较小的值了—可能仅仅是a handful of ticks over zero.由于wraparound的存在,if条件的结果就变得whoops。
幸运的是,内核提供了四个宏用来处理wraparound的异常情况。他们定义在<linux/jiffies.h>。如下
参数unknown是通常来说是jiffies,参数known是你需要比较的变量。
宏time_after(known,unknown)返回true,如果时间unkonwn is after 时间known;否则的话,它会返回false。宏time_before(known,unknown)返回true,如果时间unknown is before时间known;否则的话,返回false。最后两个宏和前两个宏的意义基本相同,除此之外,在参数相同的时候也会返回true。
11.3.3 User-Spcae and HZ
在早于2.6版本的内核,更改HZ参数的值是会导致用户空间的disagreement。这是因为这些参数导出到用户空间是以ticks-per-second为单位的。因为这些接口是permanent,应用是依赖于特定的HZ值。因此,改变HZ参数可能会在用户空间不知情的情况下缩放输出参数。Uptime也可能会出现异常,在2hours的情况下,读取20hours。
为了解决这类问题,内核需要缩放全部导出到用户空间的的jiffies变量。它定义了变量USER_HZ,这也是用户空间期待的。由于历史原因,在X86机器上,USER_HZ参数为100。函数jiffies_to_clock()是用来缩放tick count由HZ到USER_HZ。The expression used depends on whether USER_HZ and HZ are interger multiples of themselves and whether USER_HZ is less than or equal to HZ.如果上述条件都为true的话,对于我们使用的大多数系统,the expression是很简单的:
Return x/(HZ / USER_HZ)
如果不是整数倍的话,需要使用更加复杂的算法。
最终函数jifies_64_to_clock()用来提供给64位jiffies由HZ转换为USER_HZ单位。
这些函数在需要导出到用户空间的情形中随处可见,following is an example:
用户空间期待previous value as if HZ=USER_HZ如果他们是不相等的话,the macro scales as needed and everyone is happy.
11.4 Hardware Clocks and Timers
Architecture提供了两种硬件设备来帮助keep时间:系统定时器和real-time clock。这些设备的表现和实现方式区别不同的硬件设备,但是general的设计和目的是一致的。
11.4.1 Real-Time Clock
RTC提供了nonvolatitle设备用来存储时间。系统断电后,RTC设备仍然可以通过板载电池继续对时间进行计算。在PC设备中,RTC和CMOS是集合在一起的,并且a single battery保持RTC运行和保留BIOS设置。
启动阶段,内核读取RTC时间并用来初始化wall time,which is stored in the xtime variable.通常来讲,内核不会再次读取RTC时间,但是,X86设备会周期性的保存当前的wall time到RTC设备中。毫无疑问,RTC的主要作用是在系统boot阶段,用来初始化xtime变量。
11.4.2 System Timer
系统定时器在内核的time-keeping中有着极其重要的表现。无论是什么架构,系统定时器的作用都是相同的,which is that周期性的提供中断。有些架构通过预先设置好频率的晶振提供电信号来实现这一功能。Other system提供一个decrementer:用来设置初始值和以一定tick rate减数的计数器。当counter到零后,产生一个中断。任何情况下,这一效果都是一样的。
在X86上,系统定时器主要是通过可编程中断定时器(PIT)实现。
11.5 The Timer Interrupt Handler
我们在之前已经理解了HZ,jiffies和系统定时器的角色,现在我们来探讨下定时器中断handler的完成。我们将定时器中断一分为二:一部分是和硬件架构相关的,另一部分是和硬件架构无关的。
硬件架构相关的工作是给系统定时器注册中断handler,当定时器中断hit的时候,运行中断handler。他的具体工作取决于具体的硬件架构,当然,大部分硬件架构都需要至少完成下面的工作:
获取xtime_clock锁,which保护了jiffies_64和系统wall time,即xtime
根据要求确认或者复位系统定时器
周期性的保存walltime时间到RTC
调用不依赖于硬件架构的定时器routine,tick_periodic()
硬件架构不相关的routine,tick_periodic(),执行了更多的工作:
给计数jiffies_64累加一(即使在32位机器上,这也是安全的,因为xtime_lock锁在之前已经获取到了)
更新资源的使用情况,比如说对于当前进程而言,消耗的系统时间和用户时间
运行已经超时的动态定时器
执行schedule_tick()
更新walltime,which is stored in xtime
计算infamous load average
The routine是简单的,because other functions handle most of work:
static void tick_periodic(int cpu)
{
if (tick_do_timer_cpu == cpu) {
write_seqlock(&xtime_lock);
/* Keep track of the next tick event */
tick_next_period = ktime_add(tick_next_period, tick_period);
do_timer(1);
write_sequnlock(&xtime_lock);
}
update_process_times(user_mode(get_irq_regs()));
profile_tick(CPU_PROFILING);
}
Most of the important work is enabled in do_timer() and update_process_time().前者负责给变量jiffies_64增加计数:
Void do_timer(unsigned long ticks)
{
Jiffies_64 += ticks;
Update_wall_time();
Calc_global_load();
}
函数update_wall_time(),根据实际的elapsed的ticks更新walltime,但是calc_global_load()更新系统的平均负载数据。
当do_timer()最终返回时,update_process_times()被调用来更新一个tick已经过去的各种统计信息,并通过user_tick记录它是发生在用户空间还是内核空间中:
void update_process_times(int user_tick)
{
struct task_struct p = current;
int cpu = smp_processor_id();
/ Note: this timer irq context must be accounted for as well. */
account_process_tick(p, user_tick);
run_local_timers();
rcu_check_callbacks(cpu, user_tick);
printk_tick();
scheduler_tick();
run_posix_cpu_timers§;
}
Recall from tick_periodic() that the value of user_tick is set by looking at the system’s registers:
Update_process_times(user_mode(get_irq_regs()));
函数account_process_tick用来管理进程时间的更新工作:
void account_process_tick(struct task_struct *p, int user_tick)
{
cputime_t one_jiffy_scaled = cputime_to_scaled(cputime_one_jiffy);
struct rq *rq = this_rq();
if (user_tick)
account_user_time(p, cputime_one_jiffy, one_jiffy_scaled);
else if ((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET))
account_system_time(p, HARDIRQ_OFFSET, cputime_one_jiffy,
one_jiffy_scaled);
else
account_idle_time(cputime_one_jiffy);
}
You might realize that当系统定时器中断occurred,内核会credit一个进程forrunning the entire previous tick,无论处理器处于什么模式。In reality,during last tick,进程可能进出内核多次。In fact,在last tick中,可能不仅仅存在一个运行的进程。The granular进程计数是经典的Unix的,without much more complex accounting,this is the best Linux can provide。It is also another reason for a higher frequency tick rate.
Next,the run_local_timers()函数marks a softirq去处理expired定时器的执行。下一章将会介绍定时器的相关内容。
Finally,函数schedule_tick()递减当前运行进程的时间片并在需要的时候设置need_resched。在SMP系统上,在需要的时候,还会平衡处理器runqueues。这在第四章已经讨论了。
函数tick_periodic()返回到original硬件架构相关的中断handler,which performs any needed cleanup,realeases the xtime_lock锁,and finally returns.
所有的上述工作,每1/HZ秒都会执行一次。对于X86机器而言的话,也就是说每秒钟执行100-1000次。
11.6 The Time of Day
当前系统的时间(the wall time)定义在kernel/time/timekeeping.c:
Struct timespec xtime;
数据结构timespec定义在<linux/time.h> as:
Struct timespec{
_kernel_time_t tv_sec; /seconds/
Long tv_nsec; /nanoseconds/
}
变量xtime.tv_sec存储自1970,1.1以来的时间,以秒为单位存储。这个时间叫做epoch。大多数unix系统都将当前walltime时间的概念建立在相对于这个epoch的基础上,xtime.v_nsec值存储在最后一秒中经过的纳秒数。
读写变量xtime都是要求xtime_lock锁的,which is not a normal spinlock,but a seqlock.第十章,”kernel Syncchronization Methods”我们已经讨论过了。
更新xtime,a write seqlock is required:
Write_seqlock(&xtime_lock);
/* update xtime */
Write_seunqlock(&xtime_lock);
读取xtime要求使用read_seqbegin()和read_seqretry()函数:
unsigned long seq;
do {
unsigned long lost;
seq = read_seqbegin(&xtime_lock);
usec = timer->get_offset();
lost = jiffies - wall_jiffies;
if (lost)
usec += lost * (1000000 / HZ);
sec = xtime.tv_sec;
usec += (xtime.tv_nsec / 1000);
} while (read_seqretry(&xtime_lock, seq));
此循环重复,直到读取器确信它在不进行干预写入的情况下读取数据。如果在循环过程中,定时器中断发生并更新了xtime,返回的seqence number是无效的并且循环重复运行。
用户空间获取walltime的主要接口是gettimeofday(),which is implemented as sys_gettimeofday() In kernel/time.c。
asmlinkage long sys_gettimeofday(struct timeval tv, struct timezone tz)
{
if (likely(tv)) {
struct timeval ktv;
do_gettimeofday(&ktv);
if (copy_to_user(tv, &ktv, sizeof(ktv)))
return -EFAULT;
}
if (unlikely(tz)) {
if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
return -EFAULT;
}
return 0;
}
如果用户提供了一个非NULL的tv值,硬件架构相关的do_gettimeofday() is called.该函数主要是执行xtime的循环读取工作。相似的,如果tz是一个非NULL值,系统时间zone is returned to the user.如果在拷贝wall time或者time zone到用户空间的过程中出现了错误,函数会返回-EFAULT。否则的话,成功了会返回0.
内核也完成了time()系统调用,但是gettimeofday() largely replaces it.C语言库也提供了其他的walltime相关的库调用,比如说ftime()和ctime()。
系统调用settimeofday()系统调用设置walltime到特定的值。It requires the CAP_SYS_TIME capability.
除了更新xtime,内核不像以用户空间如此频繁的使用walltime。当然,文件系统代码除外,which 存储了各种各样的时间戳节点。
11.7 Timers
定时器,有时也成为动态定时器或者内核定时器,对于内核代码管理时间相当重要。内核代码通常需要延迟某些函数的执行in a later time.在前一章,我们讨论了底半部的机制,which are great for deferring work until later。Unfortunately,later的定义是比较模糊,不清楚的。底半部的目的也不是那么明确的延迟工作,而是不是现在执行。我们需要的工具室给延迟工作执行一个特定的时间。这个解决办法就是内核定时器。
定时器是易于使用的。You执行了一部分初始化工作,指明一个expiration的时间,指明在expiration后执行的函数and激活定时器。定时器expired后,给定的函数开始执行。定时器不是循环的。定时器在它expired后会被destroyed。这也是动态命名的原因吧:定时器是不断地创建和销毁的,定时器的数量是没有限制的。在整个内核当中,定时器都是very popular。
11.7.1 Using Timers
定时器are presented by struct timer_list,which is defined in <linux/timer.h>:
Struct timer_list{
Struct list_head entry; / entry in linked list of timers /
Unsigned long expires; / expiration value, in jiffies /
Void (function)(unsigned long); / the timer handler function /
Unsigned long data; / lone argument to the handler /
Struct tvec_t_base_s base; / internal timer field,do not touch /
}
Fortunately,定时器的使用不怎么需要你深入理解this数据结构。内核提供了一系列的定时器相关的接口去做定时器管理。Everything is declared in <linux/timer.h>。
具体的定时器接口的使用,查看page222-page223即可,在此不做详细的介绍。BTW,you should watch out the difference between del_timer() and del_timer_sync(),cause del_timer_sync() cannot be used from interrupt context.
11.7.2 Timer Race Conditions
因为定时器相较于其他运行的代码是异步运行的,所以说存在着几个潜在的竞态条件需要注意。
First,不要使用下列的组合替换掉mod_timer(),因为这在SMP系统中是不安全的:
Del_timer(my_timer)
My_timer->expires = jiffies + new_delay;
Add_timer(my_timer);
Second,在绝大多数的情况下,你都应该使用del_timer_sync() over del_timer().否则的话,你是不可能假设定时器没有运行的。假设以下,如果删除了定时器后,code需要去释放或者操作定时器中断handler使用的资源。因此,sync的版本就更受欢迎了。
Finally,你必须要确定保护到定时器中断handler使用到的共享数据。相对于其他的code,内核运行这部分函数是异步地。定时器使用到的数据也需要被保护到。
11.7.3 Timer Implementation
在定时器中断完成后,内核在底半部执行定时器,as softirq。定时器中断handler运行update_process_times(),which calls run_local_timers();
Void run_local_timers(void){
Hrtimer_run_queues();
Raise_softirq(TIMER_SOFTIRQ); / raise the timer softirq /
Softlockup_tick();
}
The TIMER_SOFTIRQ softirq is handled by run_timer_softirq().该函数运行所有处理器上面的expired定时器。
定时器存储在链表中。但是对于内核来说,不断地遍历整个链表去查找expired定时器,或者通过expiration值划分链表都是不明智的,这样的话,定时器的插入和删除变得很耗时相反地,内核基于expired值将定时器分为了五组。随着定时器expiration值的下降,定时器会转移到低expiration值组该分组确认了在大多数timer softirq执行的前提下,内核做尽可能少的工作去查找expired定时器。因此,定时器管理的代码是高效的。
11.8 Delaying Execution
通常来说,内核代码需要一种方式来延迟一段时间去执行代码,但是又不使用定时器或者底半部机制。这就通常需要硬件时间来完成一个给定的任务。时间是很短暂的。比如,对于网卡来说,更改网卡模式的时间需要限定在两微妙内。在设定完成指定的速率后,驱动在运行之前需要至少等待两微妙。
根据延迟机制的不同,内核提供了多种的解决办法。这些解决办法有着各自的特性。有些人在拖延时间的同时还占用了处理器,从而有效地阻止了任何实际工作的完成。其他的解决方案不会占用处理器,但不能保证您的代码能够在所需的时间内恢复。
11.8.1 Busy Looping
完成延迟要求的最简单办法就是忙等待或者循环等待。当然,这项计数仅仅在精确度要求不高或者整数倍tick延迟的情况下才会使用到。
思路是很简单的:spin in a loop until the desired number of clock ticks pass.For example:
Unsigned long timeout = jiffies + 10; / ten ticks /
While(time_before(jiffies, timeout));
该循环会一直运行直到jiffies值大于delay,which occurs only after 10 clock ticks have passed.在X86设备中,HZ值为1000,this results in a wait of 10 miliseconds.Similarly:
Unsigned long delay = jiffies + 2HZ; / 2 seconds /
While(time_before(jiffies, delay));
This spins until 2HZ clock ticks has passed,which is always two seconds regardless of the clock rate.This approprach is not nice to the reset of the system.当你的代码在等待的时候,处理器仅仅在盲目的循环—no useful work is accomplished.
更好得办法是在你的代码在忙等待的过程中,允许处理器调度你的进程:
Unsigned long delay = jiffies + 5HZ;
While(time_before(jiffies, delay))
Cond_resched();
调用cond_resched()函数的意义就是调度一个新的进程,但是也在need_resched被置有效。换句话说,这种办法是有条件的去调动调度器,only if有更重要的工作需要执行的时候。
因为这种办法涉及到了调度器,所以说你不能在中断handler中调用这种办法----仅仅在进程context中使用。所有的上述办法推荐在进程context中使用,因为中断handler需要尽可能快的执行。Futhermore,任何形式的延迟执行都不应该在持有锁或者中断被关闭的时候执行。
11.8.2 Small Delays
有些时候,内核代码需要一些短的、精确的延时。这通常用来同步硬件,which again usually lists some minimum time for an activity to complete----often less than a millisecond.这种情况下,就不可能使用jiffies变量来做相应的延迟操作而来。如果说定时器中断频率为100HZ,那么时钟tick是大于10毫秒的。即使是1000HZ的频率,时钟tick也仍然大于1毫秒。这样,我们就需要其他的办法来解决这种更小、更精确的延时的问题。
Thankfully,内核提供了三个函数for微妙、纳秒和毫秒,defined in <linux/delay.h>,which do not use jiffies:
Void udelay(unsigned long usecs);
Void ndelay(unsigned long nsecs);
Void mdelay(unsigned long msecs);
函数udelay()应该仅在那些小延迟的时候调用;因为在fast machine上,较大的延迟可能会导致overflow。As a rule,超过一毫秒的延迟不要使用udelay();对于更大的延迟,mdelay() works fine.Remember that在持有锁或者中断被关闭的时候,不要通过忙等待的方式实现延迟,因为这样的话,系统的性能和响应都会受到影响。如果你需要精确的延时,那么these calls是最适合的。应用忙等待延迟的典型应用通常来说是为了某些小的延迟,usually in the microsecond range.
11.8.3 schedule_timeout()
A more optimal的延迟执行的办法是使用schedule_timeout()。This call puts your task to sleep until at least the specified time has elapsed.但是这不保证sleep duration后执行准确的时间。当指定的时间elapsed,内核唤醒任务并把任务放置在runqueue的后面。Usage is easy:
/ set task’s state to interruptable sleep */
Set_current_state(TASK_INTERRUPTABLE);
/* take a nap and wake up in “s” seconds */
Schedule_timeout(s * HZ);
这项单独的参数是预设的timeout,单位是jiffies。上面的额例子把任务置于可中断的睡眠态几秒钟。由于任务被标记为TASK_INTERUPTABLE,当它接收到了信号以后,它会被唤醒。如果code不想处理信号,你可以使用TASK_UNINTERRUPTABLE。在调用schedule_timeout()之前任务必须处于上述两者状态中的一种,否则的话,任务是不会睡眠的。
Note that由于schedule_timeout()函数调用到了调度器,调用到他的code必须是可以睡眠的。简而言之,你必须处于进程context并且不能持有锁。
11.8.4 schdule_timeout() Implementation
函数schedule_timeout()是直接的,它仅是内核定时器函数的简单应用。
函数创建了定时器,名叫原来的命名timer并设置了timeout时间。当timeout时间expired后,定时器设置的执行函数是process_timeout();紧接着它使能了定时器,调用schedule()。因为任务标记了TASK_INTERRUPTABLE or TASK_UNINTERRUPTABLE,所以说调度器不会运行它,而是选择一个新的进程。
当定时器expires,它会调用process_timeout():
Void process_timeout(unsigned long data)
{
Wake_up_process((task_t *) data);
}
该函数把任务置为TASK_RUNNING状态,并把它放到运行队列的后面。
当任务重新调度的时候,它会返回到schedule_timeout()离开的地方开始执行。如果任务被立刻唤醒,定时器就会被摧毁。
11.8.5 Sleeping on a Wait Queue,with a Timeout
第四章介绍了进程context代码是如何在内核中把自己放置到运行队列中,等待特定事件的occur并调用调用调度器选择一个新的任务。Elsewhere,当事件最终发生的时候,调用wake_up(),并且在等待队列中睡眠的任务被唤醒,继续运行。
Sometimes,等待特定的事件或者等待一定的时间都是desireable的。上述两种情况下,代码都会在把自己放置到等待队列后调用schedule_timeout()而不是schedule()。当特定的事件发生了或者指定的时间elapses后,任务唤醒。代码需要检查为什么会被唤醒—可能的原因有特定的事件occur了、事件elapsed、又或者接收到了信号—紧接着继续运行as appropriate。