1. 定时器
定时器有时也称为动态定时器或内核定时器,是管理内核时间的基础。定时器的使用很简单。你只需要执行一些初始化工作,设置一个超时时间,指定超时发生后执行的函数,然后激活定时器就可以了。
注意,定时器并不是周期运行,它在超时后就自行销毁。
1.1. 使用定时器
定时器由结构timer_list表示,在<linux/timer.h>中:
struct timer_list{
struct list_head entry; /*定时器链表入口*/
unsigned long expires; /*以jiffies单位的定时值*/
spin_lock lock; /*保护定时器的锁/
void (*func)(unsigned long); /* 定时器处理函数*/
unsigned long data; /*参数*/
struct tvec_t_base_s *base; /*定时内部值,用户不要使用*/
};
内核提供了一组接口操作定时器,在<linux/timer.h>中
创建定时器时需要定义它:
struct timer_list my_timer;
接着需要初始化,初始化必须在使用其他定时器管理函数对定时器操作前完成:
init_timer(&my_timer);
my_timer.expires表示超时时间,它是以节拍为单位的绝对计数值。处理函数必须符合下面的函数原型:
void my_timer_fuc(unsigned long data);
data参数使你可以利用同一个处理函数注册多个定时器,只需要通过参数就能区别对待它们。如果不需要参数,可以简单的传递0(或任何其他值)给处理函数。
最后,必须激活定时器:
add_timer(&my_tiner);
现在,只要节拍计数大于或等于指定的超时时,内核就开始执行定时器处理函数。
内核通过函数mod_timer来实现已经激活的定时器超时时间:
mod_timer(&my_timer, jiffies+new_delay);
mod_timer函数也可以操作那些已经初始化,但还没有被激活的定时器,如果定时器没有激活,mod_timer会激活它。如果调用时定时器未被激活,该函数返回0,否则返回1。一旦从mod_timer函数返回,定时器都将被激活而且设置了新的定时值。
如果需要在定时器超时前停止定时器,可以使用del_timer函数:
del_timer(&my_timer);
被激活或未被激活的定时器都可以使用该函数,如果定时器还未被激活,该函数返回0;否则返回1。当删除定时器,必须小心一个潜在的竞争条件。当del_timer返回后,可以保证的只是:定时器不会被再激活,但是多处理器上定时器中断可能已经在其他处理上运行了,所以需要等待可能在其他处理器上运行的定时器处理程序都退出,这时需要使用del_timer_sync函数执行删除工作:
del_timer_sync(&my_timer);
和del_timer函数不同,del_timer_sync数不能在中断上下文中使用。
1.2. 定时器竞争条件
因为定时器代码与当前执行代码是异步的,因此就可能存在潜在的竞争条件。首先修改定时器使用mod_tiimer函数,不要自行解决。其次,一般使用del_timer_sync函数来删除定时器。
1.3. 实现定时器
内核在时钟中断发生后执行定时器,定时器作为软中断在下半部上下文中执行。具体来说:时钟中断处理程序会执行update_proccess_timers函数,该函数随即调用run_local_timers函数:
void run_local_tiemrs(void)
{
raise_sofirq(TIMER_SOFTIRQ);
}
run_timer_softirq函数处理软中断TIMER_SOFTIRQ,从而在当前处理器上运行所有的超时定时器。
为了提高搜索效率,内核将定时器按它们的超时时间划为五组。当定时器超时时间接近时,定时器将随组一起下移。采用分组定时器的方法可以在执行软中断的多数情况下,确保内核尽可能减少搜索超时定时器所带来的负担。
2. 延迟执行
内核代码(尤其是驱动程序)除了使用定时器或下半部机制以外还需要其他方法来推迟执行任务。比如,重新设置网卡的以太模式要花费2毫秒,驱动程序必须至少等待2毫秒才能继续运行。
2.1. 忙等待
最简单的延迟方法是忙等待(或者说忙循环)。但是该方法仅仅在想要延迟的时间是节拍的整数倍,或者精确要求不高时才可以使用。
忙循环实现简单,如下:
unsigned long delay=jiffies+10; /*10个节拍*/
while(time_before(jiffies, delay))
;
循环不断执行,直到jiffies大于delay为止。这样,处理器只能在原地旋转等待,不会处理其他任何任务。更好的方法是在代码等待时,运行内核重新调度执行其他任务:
unsigned long delay=jiffies+5*HZ; /*5秒*/
while(time_before(jiffies, delay))
cond_resched();
cond_resched函数将调度一个新程序投入运行,但它只有在设置完need_resched标志后,才能生效。换句话说,该方法有效的条件是系统存在更重要的任务需要运行。因为该方法需要调度调度程序,所以不能在中断上下文中使用,只能在进程上下文中使用。事实上,所有延迟方法在进程上下文中使用,因为中断处理程序都应该尽可能快的执行。延迟执行不管在哪种情况下都不应该在持有锁时或禁止中断时发生。
C编译器通常只将变量装载一次。为了解决后台jiffies值会随时钟中断的发生而不断增加的问题,<linux/jiffies.h>中jiffies变量被标记为关键字volatile,指示编译器在每次访问变量时都需要重新从主内存中获得,而不是通寄存器中的变量别名来访问。
2.2. 短延迟
有时驱动程序不但需要很短暂的延迟(比时钟节拍还短)而且还要求延迟的时间很精确。内核为此提供了两个可以处理微妙和毫秒级别的延迟的函数,定义在<linux/delay.h>中:
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
前一个函数利用忙循环将任务雁翅刀指定的微妙数后运行,后者延迟指定的毫秒数。用起来很简单:
udelay(150);/*延迟150微妙*/
udelay()函数依靠执行数次循环达到延迟效果,而mdelay函数又是通过udelay()函数实现的。因为内核知道处理器在一秒内能执行多少次循环(查看BogoMips内容),所以udelay函数仅仅需要根据指定的延迟时间在1秒中占的比例。
经验表明,不要使用udelay函数处理超过1毫秒的延迟,超过1毫秒的情况下,使用mdelay函数,以避免溢出。千万注意,不要在持有锁时或禁止中断时使用忙等待,因为这时忙等待会使系统响应速度和性能大打折扣。
BogoMips值记录处理器在给定时间内忙循环执行的次数。该值存放在变量loops_per_jiffy中,可以从文件/proc/cpuinfo中读到它。延迟循环函数使用loops_per_jiffy值来计算为提供精确延迟而需要进行多少次循环。
内核会在启动时利用calibrate_delay计数loops_per_jiffy值,该函数在init/main.c中。
2.3. 函数schedule_timeout
更理想的延迟执行方法是使用schedule_timeout函数,该方法会让需要延迟执行的任务睡眠到指定的延迟时间耗尽后再重新运行。当该方法不能保证睡眠时间正好等于指定的延迟时间。当指定的时间到期后,内核唤醒被延迟的任务将其重新放回运行队列,如下:
/*将任务设置为可中断睡眠状态*/
set_current_state(TASK_INTERRUPTIBLE);
/*小睡一会儿,s秒后自动唤醒*/
schedule_timeout(s*HZ);
唯一的参数是延迟的相对时间,单位为jiffies。因为任务处于可中断状态,所以如果任务收到信号将被唤醒。如果睡眠任务不想接收信号,可以将任务状态设置为TASK_UNINTERRUPTIBLE,然后睡眠。注意,调用schedule_timeout函数前必须首先将任务设置成上面两种状态之一,否则任务不会睡眠。由于schedule_timeout函数需要调用调度程序,所以调用它的代码必须保证能够睡眠,调用代码必须处于进程上下文中,并且不能持有锁。
schedule_timeout函数是内核定时器的一简单应用,代码如下:
signed long schedule_timeout(signed long timeout)
{
timer_t timer;
unsigned long expire;
switch(timeout)
{
case MAX_SCHEDULE_TIMEOUT:
schedule();
goto out:
default:
if(timeout < 0)
{
printk(KERR_ERR”schedule_timeout:wrong timeout”
” value %lx form %p/n”,timeout,builtin_return_address(0));
current->state = TASK_RUNNING;
go out;
}
}
expire = timeout + jiffies;
init_timer(&timer);
timer.expires = expire;
timer.data = (unsigned long )current;
timer.function = process_timeout;
add_timer(&timer);
schedule();
del_time_sync(&timer);
timeout = expire – jiffies;
out:
return timeout < 0 ? 0 : timeout;
}
该函数用原始的名字timer创建一个定时器timer,然后设置它的超时时间timeout;设置超时执行函数process_timeout,然后激活定时器而且调用schedule函数。
当定时器超时,process_timeout函数被调用:
void process_time(unsigned long data)
{
wake_up_process((task_t *)data);
}
该函数将任务设置为TASK_RUNNING状态,然后将其放入运行队列。
当任务重新被调度时,将返回代码进入睡眠的位置继续执行(正好在调用schedule后),如果任务提前被唤醒(比如收到信号),那么定时器被销毁,process_timeout函数返回剩余的时间。
2.4. 设置超时时间,在等待队列上睡眠
休眠通过等待队列处理,等待队列是为了等待特定事件发生,可以将自己放入等待队列,然后调用调度程序区执行新任务。一旦事件发生后,内核调用wake_up函数唤醒在睡眠队列上的任务,使其重新投入运行。当然,代码需要检查被唤醒的原因——有可能被实践唤醒,有可能因为延迟的时间到期,还可能是因为收到了信号,然后执行相应的操作。