函数首先获得到本地 CPU 的 base 。然后检测如果 jiffies
注: hrtimer_run_pending() 函数是高精度时钟的处理。本文暂没有涉及高精度时钟相关的内容。
大于等于 timer_jiffies ,说明可能已经有软件时钟到期了,此
时就要进行软件时钟的处理,调用函数 __run_timers 进行处
理。如果 jiffies 小于 timer_jiffies ,表明没有软件时钟到期,
则不用对软件时钟进行处理。函数返回。
接下来看一下函数 __run_timers 都作了些什么,如清单3-9
清单3-9 __run_timers函数
static inline void __run_timers(struct tvec_base *base) { …… spin_lock_irq(&base->lock); while (time_after_eq(jiffies, base->timer_jiffies)) { …… int index = base->timer_jiffies & TVR_MASK; if (!index && (!cascade(base, &base->tv2, INDEX(0))) && (!cascade(base, &base->tv3, INDEX(1))) && !cascade(base, &base->tv4, INDEX(2))) cascade(base, &base->tv5, INDEX(3)); ++base->timer_jiffies; list_replace_init(base->tv1.vec + index, &work_list); while (!list_empty(head)) { …… timer = list_first_entry(head, struct timer_list,entry); fn = timer->function; data = timer->data; …… set_running_timer(base, timer); detach_timer(timer, 1); spin_unlock_irq(&base->lock); { int preempt_count = preempt_count(); fn(data); …… } spin_lock_irq(&base->lock); } } set_running_timer(base, NULL); spin_unlock_irq(&base->lock); } |
代码解释:
获得 base 的同步锁
如果 jiffies 大于等于 timer_jiffies (当前正要处理的软件时钟的到期时间,说明可能有软件时钟到期了),就一直运行3~7,否则跳转至8
计算得到 tv1 的索引,该索引指明当前到期的软件时钟所在 tv1 中的链表(结构参见3.2节),代码:
int index = base->timer_jiffies & TVR_MASK; |
调用 cascade 函数对软件时钟进行必要的调整(稍后会介绍调整的过程)
使得 timer_jiffies 的数值增加1
取出相应的软件时钟链表
遍历该链表,对每个元素进行如下操作
设置当前软件时钟为 base 中正在运行的软件时钟(即保存当前软件时钟到 base-> running_timer 成员中)
将当前软件时钟从链表中删除,即卸载该软件时钟
释放锁,执行软件时钟处理程序
再次获得锁
设置当前 base 中不存在正在运行的软件时钟
释放锁
3.3.3 软件时钟调整过程
函数 cascade 用于调整软件时钟(这个调整过程是指:将马上就要到期的软件时钟从其所在的链表中删除,重新计算到期时间的相对值(到期时间 - timer_jiffies ),然后根据该值重新插入到 base 中)。注意到在软件时钟处理过程中,每次都是从 tv1 中取出一个链表进行处理,而不是从 tv2~tv5 中取,所以对软件时钟就要进行必要的调整。
在讲解 cascade 函数之前,再从直观上理解下为什么需要进行调整。所有软件时钟都是按照其到期时间的相对值(相对于 timer_jiffies )被调加到 base 中的。但是 timer_jiffies 的数值都会在处理中增加1(如3.3.2节所示),也就是说这个相对值会随着处理发生变化,当这个相对值小于等于256时,就要将软件时钟从 tv2~tv5 中转移到 tv1 中( tv1 中保存着下256个 tick 内到期的所有软件时钟)。
函数 cascade 的实现如清单3-10
清单3-10 cascade 函数
static int cascade(struct tvec_base *base, struct tvec *tv, int index) { struct timer_list *timer, *tmp; struct list_head tv_list; list_replace_init(tv->vec + index, &tv_list); list_for_each_entry_safe(timer, tmp, &tv_list, entry) { …… internal_add_timer(base, timer); } return index; } |
该函数根据索引,取出相应的 tv ( tv2~tv5 )中的链表,然后遍历链表每一个元素。按照其到期时间重新将软件时钟加入到软件时钟的 base 中。该函数返回 tv 中被调整的链表索引值(参见图3-1)。
清单3-9中调整软件时钟的代码如下:
int index = base->timer_jiffies & TVR_MASK; if (!index && (!cascade(base, &base->tv2, INDEX(0))) && (!cascade(base, &base->tv3, INDEX(1))) && !cascade(base, &base->tv4, INDEX(2))) cascade(base, &base->tv5, INDEX(3)); |
这部分代码表明:如果 index 有0再到0时( index 是对 timer_jiffies 取模),说明时间已经过了256个 tick ,这时要把 tv2 中软件时钟转移到 tv1 中。如果 index 和第一个 cascade 函数的返回值都从0再到到0时,说明时间已经过了256*64个 tick ,这时要把 tv3 中软件时钟转移到 tv1 或者 tv2 中。之后的调整过程依次类推。
3.4 自我激活
软件时钟可分为两种类型:
仅仅激活一次
激活多次或者周期性激活
多次激活的实现机制就是要在软件时钟处理函数中重新设置软件时钟的到期时间为将来的一个时间,这个过程通过调用 mod_timer 函数来实现。该函数的实现如清单3-11
清单3-11 mod_timer 函数
int mod_timer(struct timer_list *timer, unsigned long expires) { …… if (timer->expires == expires && timer_pending(timer)) return 1; return __mod_timer(timer, expires); } |
从代码中可以看出,该函数实际上调用 __mod_timer 函数(参见3.3.1节)来调整软件时钟的到期时间。
3.5 软件时钟的应用
软件时钟的处理是在处理软中断时触发的,而软中断的处理又会紧接着硬件中断处理结束而进行,并且系统会周期地产生时钟中断(硬件中断),这样,软件时钟的处理至少会在系统每一次时钟中断处理完成后触发(如果软件时钟的到期时间大于系统当前的 jiffies ,表明时间未到期,则不会调用保存在软件时钟中的函数,但此时的确提供了处理软件时钟的时机)。从这点上看,软件时钟会有较快的相应——一旦时间到期,保存在软件时钟中的函数会将快地被调用(在时钟软中断中被调用,参见3.3.2节)。所以内核中凡是需要隔一段时间间隔后作指定操作的过程都通过软件时钟完成。例如大部分设备驱动程序使用软件时钟探测异常条件、软盘驱动程序利用软件时钟关闭有一段时间没有被访问软盘的设备马达、进程的定时睡眠( schedule_timeout 函数)和网络超时重传等等。
本节主要通过介绍进程的定时睡眠( schedule_timeout 函数)和网络超时重传来说明软件时钟的应用。
3.5.1 进程的定时睡眠
函数 schedule_timeout 的代码如清单3-12
清单3-12 函数 schedule_timeout
signed long __sched schedule_timeout(signed long timeout) { struct timer_list timer; unsigned long expire; …… 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; } |
函数 schedule_timeout 定义了一个软件时钟变量 timer ,在计算到期时间后初始化这个软件时钟:设置软件时钟当时间到期时的处理函数为 process_timeout ,参数为当前进程描述符,设置软件时钟的到期时间为 expire 。之后调用 schedule() 函数。此时当前进程睡眠,交出执行权,内核调用其它进程运行。但内核在每一个时钟中断处理结束后都要检测这个软件时钟是否到期。如果到期,将调用 process_timeout 函数,参数为睡眠的那个进程描述符。 process_timeout 函数的代码如清单3-13。
清单3-13 函数 process_timeout
static void process_timeout(unsigned long __data) { wake_up_process((struct task_struct *)__data); } |
函数 process_timeout 直接调用 wake_up_process 将进程唤醒。当内核重新调用该进程执行时,该进程继续执行 schedule_timeout 函数,执行流则从 schedule 函数中返回,之后调用 del_singleshot_timer_sync 函数将软件时钟卸载,然后函数 schedule_timeout 结束。函数 del_singleshot_timer_sync 是实际上就是函数 del_timer_sync (参见3.3.2节),如清单3-14
清单3-14 函数del_singleshot_timer_sync
#define del_singleshot_timer_sync(t) del_timer_sync(t) |
以上就是进程定时睡眠的实现过程。接下来介绍的是软件时钟在网络超时重传上的应用。