10.6 内核延时
10.6.1 短延迟
Linux内核中提供下列3个函数以分别进行纳秒、微秒和毫秒延迟:
<linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
上述延迟的实现原理本质上是忙等待,它根据CPU频率进行一定次数的循环。有时,在软件中进行下面的延迟:
void delay(unsigned int time)
{
while(time--);
}
ndelay()、udelay()和mdelay()函数的实现方式原理与此类似。kernel在启动时,会运行一个延迟循环校准(Delay Loop Calibration),计算出lPJ(Loops Per Jiffy),内核启动时会打印如下类似信息:
Calibrating delay loop... 530.84 BogoMIPS (lpj=1327104)
如果直接在bootloader(U-boot)传递给内核的bootargs中设置lpj=1327104,则可以省掉这个校准的过程,节省约百毫秒级的开机时间。
毫秒时延(以及更大的秒时延)已经比较大了,在内核中,最好不要直接使用mdelay()函数,会耗费CPU资源。对于毫秒级以上的时延,内核提供如下函数:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
上述函数将使得调用这些函数的进程睡眠参数指定的时间,msleep()、ssleep()不能被打断,而msleep_interruptible()可以被打断。
备注:受系统HZ以及进程调度的影响,msleep()类似函数的精度是有限的。
10.6.2 长延迟
在内核中进行延迟的一个很直观的方法是比较当前的jiffies和目标jiffies(设置为当前jiffies加上时间间隔的jiffies),直到未来的jiffies达到目标jiffies。
代码清单10.15给出使用忙等待先延迟100个jiffies再延迟2s的实例。
代码清单10.15 忙等待时延实例
#include <linux/jiffies.h>
/* 延迟100个jiffies */
unsigned long delay = jiffies + 100;
while(time_before(jiffies, delay));
/* 再延迟2s */
unsigned long delay = jiffies + 2*HZ;
while(time_before(jiffies, delay));/*第一个参数为被调用时的jiffies,第二个参数为未来时间的jiffies*/
与time_before()对应的还有一个time_after(),这两个在内核中定义为(只是将传入的未来时间jiffies和被调用时的jiffies进行一个简单的比较):
#define time_after(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)(b) - (long)(a) < 0))
#define time_before(a,b) time_after(b,a)
为了防止在time_before()和time_after()的比较过程中编译器对jiffies的优化,内核将其jiffies定义为volatile变量,这将保证每次都会重新读取这个变量。因此volatile更多的作用还是避免这种读合并。
10.6.3 睡着延迟
睡着延迟是比忙等待更好的方式,睡着延迟是在等待的时间到来之前进程处于睡眠状态,CPU资源被其他进程使用。schedule_timeout()可以使当前任务休眠到指定的jiffies之后再重新被调度执行,msleep()和msleep_interruptible()在本质上都是依靠包含了schedule_timeout()的schedule_timeout_uninterruptible()和schedule_timeout_interruptible()来实现的。
代码清单10.16 schedule_timeout()的使用
void msleep(unsigned int msecs) /* 不可以被打断 */
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout)
timeout = schedule_timeout_uninterruptible(timeout);
}
unsigned long msleep_interruptible(unsigned int msecs)/* 可以被打断 */
{
unsigned long timeout = msecs_to_jiffies(msecs) + 1;
while (timeout && !signal_pending(current))
timeout = schedule_timeout_interruptible(timeout);
return jiffies_to_msecs(timeout);
}
实际上,schedule_timeout()的实现原理是向系统添加一个定时器,在定时器处理函数中唤醒与参数对应的进程。
代码清单10.17 schedule_timeout_uninterruptible()和schedule_timeout_interruptible()
signed long __sched schedule_timeout_uninterruptible(signed long timeout)
{
__set_current_state(TASK_UNINTERRUPTIBLE);// 设置进程状态为TASK_UNINTERRUPTIBLE
return schedule_timeout(timeout);
}
signed long __sched schedule_timeout_interruptible(signed long timeout)
{
__set_current_state(TASK_INTERRUPTIBLE);// 设置进程状态为TASK_INTERRUPTIBLE
return schedule_timeout(timeout);
}
下面两个函数可以将当前进程添加到等待队列中,从而在等待队列上睡眠。当超时发生时,进程将被唤醒。
sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t*q, unsigned long timeout);// 可以在超时前被打断
总结:
Linux的中断处理分为两个半部,上半部处理紧急的硬件操作,下半部处理不紧急的耗时操作。tasklet(小任务)和工作队列都是中断下半部实现的良好机制,tasklet基于软中断实现(软中断上下文中)。内核定时器也依靠软中断实现。
内核中的延时可以采用忙等待或睡眠等待,为了充分利用CPU资源,使系统有更好的吞吐性能,,在对延迟时间的要求并不是很精确的情况下,通常值得推荐睡眠等待,而ndelay()、udelay()忙等待机制在驱动中通常是为了配合硬件上的短时延迟要求,mdelay()通常在驱动中不推荐使用。