内核代码(尤其是驱动程序)除了使用定时器或下半部机制以外还需要其他方法来推迟执行任务。这种推迟通常发生在等待硬件完成某些工作时,而且等待时间非常短。 内核提供了许多延迟方法处理各种延迟要求。不同的方法有不同的处理特点,有些是在延迟任务时挂起处理器,防止处理器执行任何实际工作;另一些不会挂起处理器,所以也不能确保被延迟的代码能够在指定的延迟时间运行。
忙等待
最简单的延迟方法(虽然也是最不理想的方法)是忙等带。该方法仅仅在想要延迟的时间是节拍的整数倍,或者精确率要求不太高时才可以使用。
忙循环实现起来很简单,在循环中不断旋转直到希望的时钟节拍数耗尽,比如:
unsigned long delay = jiffies + 10;
while(time_before(jiffies,delaly))
;
更好的方法应该是在代码等待时,运行内核重新调度执行其他任务:
unsigned long delay = jiffies + 2 * HZ;
while(time_before(jiffies,delay))
cond_resched();
在<Sched.c(kernel)>中
int __sched cond_resched(void)
{
if (need_resched() && !(preempt_count() & PREEMPT_ACTIVE) &
system_state == SYSTEM_RUNNING) {
__cond_resched();
return 1;
}
return 0;
}
static void __cond_resched(void)
{
#ifdef CONFIG_DEBUG_SPINLOCK_SLEEP
__might_sleep(__FILE__, __LINE__);
#endif
/*
* The BKS might be reacquired before we have dropped
* PREEMPT_ACTIVE, which could trigger a second
* cond_resched() call.
*/
do {
add_preempt_count(PREEMPT_ACTIVE);
schedule();
sub_preempt_count(PREEMPT_ACTIVE);
} while (need_resched());
}
cond_resched()函数将调度一个新程序投入运行,但是它只有在设置完need_resched标志后,才能生效。也就是说,该方法有效的条件是系统中存在更重要的任务需要执行。注意,因为该方法需要调用调度程序,所以它不能在中断上下文中使用,只能在进程上下文中使用。实际上,所有延迟方法在进程上下文中使用,因为中断处理程序都应该尽可能快地执行。另外,延迟执行不管在哪种情况下都不应该在持有锁时或禁止中断时发生。
C编译器通常只将变量装载一次,一般情况下不能保证循环中的jiffies变量在每次循环中被读取时都重新被载入。但是要求jiffies在每次循环时都必须重新装载,因为在后台jiffies值会随时钟中断的发生而不断增加。为了解决这个问题,jiffies变量被标记为volatile。
关键字volatile指示编译器在每次访问变量时都重新从主内存中获得,而不是通过寄存器中变量的别名来访问,从而确保前面的循环能够按预期的方式执行。
短延迟
有时候,内核代码(通常也是驱动程序)不但需要很短暂的延迟(比时钟节拍还短)而且还要求延迟的时间很精确。这种情况多发生在和硬件同步时,也就是说需要短暂等待某个动作的完成,等待时间往往小于1毫秒,所以不能使用像前面例子中那种基于jiffies的延迟方法。
内核提供了两个可以处理为妙和毫秒级别的延迟的函数udelay()和mdelay()。前一个函数利用忙循环来将任务延迟到指定的微妙数后运行,后者延迟指定的毫秒数:
在<Delay.h(include/i386)>中
/* 0x10c7 is 2**32 / 1000000 (rounded up) */
#define udelay(n) (__builtin_constant_p(n) ? /
((n) > 20000 ? __bad_udelay() : __const_udelay((n) * 0x10c7ul)) : /
__udelay(n))
/* 0x5 is 2**32 / 1000000000 (rounded up) */
#define ndelay(n) (__builtin_constant_p(n) ? /
((n) > 20000 ? __bad_ndelay() : __const_udelay((n) * 5ul)) : /
__ndelay(n))
在<Delay.h(include/linux)>中
/*
* Copyright (C) 1993 Linus Torvalds
*
* Delay routines, using a pre-computed "loops_per_jiffy" value.
*/
extern unsigned long loops_per_jiffy;
#include <asm/delay.h>
/*
* Using udelay() for intervals greater than a few milliseconds can
* risk overflow for high loops_per_jiffy (high bogomips) machines. The
* mdelay() provides a wrapper to prevent this. For delays greater
* than MAX_UDELAY_MS milliseconds, the wrapper is used. Architecture
* specific values can be defined in asm-???/delay.h as an override.
* The 2nd mdelay() definition ensures GCC will optimize away the
* while loop for the common cases where n <= MAX_UDELAY_MS -- Paul G.
*/
#ifndef MAX_UDELAY_MS
#define MAX_UDELAY_MS 5
#endif
#ifndef mdelay
#define mdelay(n) (/
(__builtin_constant_p(n) && (n)<=MAX_UDELAY_MS) ? udelay((n)*1000) : /
({unsigned long __ms=(n); while (__ms--) udelay(1000);}))
#endif
#ifndef ndelay
#define ndelay(x) udelay(((x)+999)/1000)
#endif
udelay()函数依靠执行数次达到延迟效果,而mdelay()函数通过udelay()函数实现。因为内核知道处理器在一秒内能执行多少次循环,所以udelay()函数紧急需要根据指定的延迟时间在1秒中占的比例就能决定需要进行多少次循环就能达到要求的推迟时间。
不要使用udelay()函数处理超过1毫秒的延迟,延迟超过1毫秒使用mdelay()更安全。
千万要注意不要在持有锁时或禁止中断时使用忙等待,因为这时忙等待会时系统响应速度和性能大打折扣。
内核如何知道处理器在一秒内能执行多少循环呢?
BogoMIPS值记录的是处理器在给定时间内忙循环执行的次数。其实就是记录处理器在空闲时速度有多快。它主要被udelay()和mdelay()函数使用。该值存放在变量loops_per_jiffies中,可以从文件/proc/cpuinfo中读到它。延迟循环函数使用loops_per_jiffies值来计算为提供精确延迟而需要进行多少次循环。内核在启动时利用函数calibrate_delay()函数计算loops_per_jiffies值,该函数在文件init/main.c中使用到。
在<init/main.c>中
/*
* This should be approx 2 Bo*oMips to start (note initial shift), and will
* still work even if initially too large, it will just take slightly longer
*/
unsigned long loops_per_jiffy = (1<<12);
在<init/Calibrate.h>中
void __devinit calibrate_delay(void)
{
unsigned long ticks, loopbit;
int lps_precision = LPS_PREC;
if (preset_lpj) {
loops_per_jiffy = preset_lpj;
printk("Calibrating delay loop (skipped)... "
"%lu.%02lu BogoMIPS preset/n",
loops_per_jiffy/(500000/HZ),
(loops_per_jiffy/(5000/HZ)) % 100);
} else if ((loops_per_jiffy = calibrate_delay_direct()) != 0) {
printk("Calibrating delay using timer specific routine.. ");
printk("%lu.%02lu BogoMIPS (lpj=%lu)/n",
loops_per_jiffy/(500000/HZ),
(loops_per_jiffy/(5000/HZ)) % 100,
loops_per_jiffy);
} else {
loops_per_jiffy = (1<<12);
printk(KERN_DEBUG "Calibrating delay loop... ");
while ((loops_per_jiffy <<= 1) != 0) {
/* wait for "start of" clock tick */
ticks = jiffies;
while (ticks == jiffies)
/* nothing */;
/* Go .. */
ticks = jiffies;
__delay(loops_per_jiffy);
ticks = jiffies - ticks;
if (ticks)
break;
}
/*
* Do a binary approximation to get loops_per_jiffy set to
* equal one clock (up to lps_precision bits)
*/
loops_per_jiffy >>= 1;
loopbit = loops_per_jiffy;
while (lps_precision-- && (loopbit >>= 1)) {
loops_per_jiffy |= loopbit;
ticks = jiffies;
while (ticks == jiffies)
/* nothing */;
ticks = jiffies;
__delay(loops_per_jiffy);
if (jiffies != ticks) /* longer than 1 tick */
loops_per_jiffy &= ~loopbit;
}
/* Round the value and print it */
printk("%lu.%02lu BogoMIPS (lpj=%lu)/n",
loops_per_jiffy/(500000/HZ),
(loops_per_jiffy/(5000/HZ)) % 100,
loops_per_jiffy);
}
}
schedule_timeout()
更理想的延迟方法时使用schedule_timeout()函数。该方法会让需要延迟执行的任务睡眠到指定的延迟时间耗尽后再重新运行。但该方法也不能保证睡眠时间正好等于指定的延迟时间,只能尽量使睡眠时间接近指定的延迟时间。当指定时间到期后,内核唤醒被延迟的任务并将其重新放回运行队列:
set_current_state(TASK_INTERRUPTABLE);
schedule_timeout(s*HZ);//参数是延迟的相对时间,单位为jiffies
注意,在调用schedule_timeout()函数前,必须将任务状态设置成TASK_INTERRUPTABLE和TASK_UNINTERRUPTABLE状态之一,否则任务不会睡眠。
注意,由于schedule_timeout()函数需要调用调度程序,所以调用它的代码必须保证能够睡眠。调用代码必须处于进程上下文中,并且不能持有锁。
在(Timer.c(kernel))中
/**
* schedule_timeout - sleep until timeout
* @timeout: timeout value in jiffies
*
* Make the current task sleep until @timeout jiffies have
* elapsed. The routine will return immediately unless
* the current task state has been set (see set_current_state()).
*
* You can set the task state as follows -
*
* %TASK_UNINTERRUPTIBLE - at least @timeout jiffies are guaranteed to
* pass before the routine returns. The routine will return 0
*
* %TASK_INTERRUPTIBLE - the routine may return early if a signal is
* delivered to the current task. In this case the remaining time
* in jiffies will be returned, or 0 if the timer expired in time
*
* The current task state is guaranteed to be TASK_RUNNING when this
* routine returns.
*
* Specifying a @timeout value of %MAX_SCHEDULE_TIMEOUT will schedule
* the CPU away without a bound on the timeout. In this case the return
* value will be %MAX_SCHEDULE_TIMEOUT.
*
* In all cases the return value is guaranteed to be non-negative.
*/
fastcall signed long __sched schedule_timeout(signed long timeout)
{
struct timer_list timer;
unsigned long expire;
switch (timeout)
{
case MAX_SCHEDULE_TIMEOUT: //用于检查任务是否无限期的睡眠,如果这样的话,函数不会为它设置定时器
/*
* These two special cases are useful to be comfortable
* in the caller. Nothing more. We could take
* MAX_SCHEDULE_TIMEOUT from one of the negative value
* but I' d like to return a valid offset (>=0) to allow
* the caller to do everything it want with the retval.
*/
schedule();
goto out;
default:
/*
* Another bit of PARANOID. Note that the retval will be
* 0 since no piece of kernel is supposed to do a check
* for a negative retval of schedule_timeout() (since it
* should never happens anyway). You just have the printk()
* that will tell you if something is gone wrong and where.
*/
if (timeout < 0) {
printk(KERN_ERR "schedule_timeout: wrong timeout "
"value %lx/n", timeout);
dump_stack();
current->state = TASK_RUNNING;
goto out;
}
}
expire = timeout + jiffies;
setup_timer(&timer, process_timeout, (unsigned long)current); /*创建定时器,设置超时时间,设置超时函数 */
__mod_timer(&timer, expire); /*激活定时器 */
schedule(); /*选择其他任务运行 */
del_singleshot_timer_sync(&timer); /*销毁定时器*/
timeout = expire - jiffies;
out:
return timeout < 0 ? 0 : timeout;
}
在<Time.h(include/linux)>中
static inline void setup_timer(struct timer_list * timer,
void (*function)(unsigned long),
unsigned long data)
{
timer->function = function;
timer->data = data;
init_timer(timer);
}
在<Time.c(kernel)>中
//当定时器超时,该函数被调用
static void process_timeout(unsigned long __data)
{
wake_up_process((struct task_struct *)__data);
}
schedule_timeout()函数是内核定时器的一个简单应用。
设置超时时间,在等待队列上睡眠
进程上下文中的代码为了等待特定事件发生,可以将自己放入等待队列,然后调用调度程序去执行新任务。一旦事件发生后,内核调用wake_up()函数唤醒在睡眠队列上的任务使其重新投入运行。
有时,等待队列上的某个任务可能既在等待一个特定的事件到来,又在等待一个特定的时间到期,在这种情况下,代码可以简单地使用schedule_timeout()函数代替schedule()函数,当希望指定时间到期,任务就会被唤醒。代码需要检查被唤醒的原因,可能是被事件唤醒,可能是因为延迟时间到期,也可能是因为接收到了信号;然后执行相应的操作。 |