Linux 时钟处理机制(二)
3.3 添加或删除软件时钟
在了解了软件时钟的数据组织关系之后,现在来看一下如何添加以及删除一个软件时钟。
3.3.1 添加软件时钟
在 Linux 内核中要添加一个软件时钟,首先必须分配 struct timer_list 类型的变量,然后调用函数 add_timer() 将该软件时钟添加到相应调用 add_timer 函数的 CPU 的 base 中。 Add_timer 是对函数 __mod_timer() 的一层包装。函数 __mod_timer() 的代码如清单3-2:
清单3-2 __mod_timer 函数
int __mod_timer(struct timer_list *timer, unsigned long expires){ struct tvec_base *base, *new_base; unsigned long flags; int ret = 0; …… base = lock_timer_base(timer, &flags); if (timer_pending(timer)) { detach_timer(timer, 0); ret = 1; } new_base = __get_cpu_var(tvec_bases); if (base != new_base) { if (likely(base->running_timer != timer)) { /* See the comment in lock_timer_base() */ timer_set_base(timer, NULL); spin_unlock(&base->lock); base = new_base; spin_lock(&base->lock); timer_set_base(timer, base); } } timer->expires = expires; internal_add_timer(base, timer); spin_unlock_irqrestore(&base->lock, flags); return ret;}
代码解释:
注:卸载软件时钟的意思是指将软件时钟从软件时钟所在 base 中删除,以后所说的卸载软件时钟也都是这个意思
取得软件时钟所在 base 上的同步锁( struct tvec_base 变量中的自旋锁),并返回该软件时钟的 base ,保存在 base 变量中
如果该软件时钟处在 pending 状态(在 base 中,准备执行),则卸载该软件时钟
取得本 CPU 上的 base 指针(类型为 struct tvec_base* ),保存在 new_base 中
如果 base 和 new_base 不一样,也就是说软件时钟发生了迁移(从一个 CPU 中移到了另一个 CPU 上),那么如果该软件时钟的处理函数当前没有在迁移之前的那个 CPU 上运行,则先将软件时钟的 base 设置为 NULL ,然后再将该软件时钟的 base 设置为 new_base 。否则,跳到5。
设置软件时钟的到期时间
调用 internal_add_timer 函数将软件时钟添加到软件时钟的 base 中(本 CPU 的 base )
释放锁
这里有必要详细说明一下软件时钟如何被添加到软件时钟的 base 中的(添加到本 CPU base 的 tv1~tv5 里面),因为这是软件时钟处理的基础。来看函数 internal_add_timer 函数的实现,如清单3-3
清单3-3 internal_add_timer 函数
static void internal_add_timer(struct tvec_base *base, struct timer_list *timer){ unsigned long expires = timer->expires; unsigned long idx = expires - base->timer_jiffies; struct list_head *vec; if (idx < TVR_SIZE) { int i = expires & TVR_MASK; vec = base->tv1.vec + i; } else if (idx < 1 << (TVR_BITS + TVN_BITS)) { int i = (expires >> TVR_BITS) & TVN_MASK; vec = base->tv2.vec + i; } else if (idx < 1 << (TVR_BITS + 2 * TVN_BITS)) { int i = (expires >> (TVR_BITS + TVN_BITS)) & TVN_MASK; vec = base->tv3.vec + i; } else if (idx < 1 << (TVR_BITS + 3 * TVN_BITS)) { int i = (expires >> (TVR_BITS + 2 * TVN_BITS)) & TVN_MASK; vec = base->tv4.vec + i; } else if ((signed long) idx < 0) { vec = base->tv1.vec + (base->timer_jiffies & TVR_MASK); } else { int i; if (idx > 0xffffffffUL) { idx = 0xffffffffUL; expires = idx + base->timer_jiffies; } i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK; vec = base->tv5.vec + i; } list_add_tail(&timer->entry, vec);}
代码解释:
计算该软件时钟的到期时间和 timer_jiffies (当前正在处理的软件时钟的到期时间)的差值,作为索引保存到 idx 变量中。
判断 idx 所在的区间,在
[0, ]或者( , 0)(该软件时钟已经到期),则将要添加到 tv1 中
[, ],则将要添加到 tv2 中
[, ],则将要添加到 tv3 中
[, ],则将要添加到 tv4 中
[, ),则将要添加到 tv5 中,但实际上最大值为 0xffffffffUL
计算所要加入的具体位置(哪个链表中,即 tv1~tv5 的哪个子链表,参考图3-1)
最后将其添加到相应的链表中
从这个函数可以得知,内核中是按照软件时钟到期时间的相对值(相对于 timer_jiffies 的值)将软件时钟添加到软件时钟所在的 base 中的。
3.3.2 删除软件时钟
内核可调用 del_timer 函数删除软件时钟, del_timer 的代码如清单3-4
清单3-4 del_timer 函数
int del_timer(struct timer_list *timer){ struct tvec_base *base; unsigned long flags; int ret = 0; …… if (timer_pending(timer)) { base = lock_timer_base(timer, &flags); if (timer_pending(timer)) { detach_timer(timer, 1); ret = 1; } spin_unlock_irqrestore(&base->lock, flags); } return ret;}
代码解释:
检测该软件时钟是否处在 pending 状态(在 base 中,准备运行),如果不是则直接函数返回
如果处于 pending 状态,则获得锁
再次检测软件时钟是否处于 pending 状态(该软件时钟可能被卸载了),不是则释放锁然后函数返回
如果还是 pending 状态,则将其卸载,之后释放锁,函数返回
如果在 SMP 系统中,则需使用 del_timer_sync 函数来删除软件时钟。在讲解 del_timer_sync 函数之前,先来看下 try_to_del_timer_sync 函数的实现(该函数被 del_timer_sync 函数使用),其代码如清单3-5
清单3-5 try_to_del_timer_sync 函数
int try_to_del_timer_sync(struct timer_list *timer){ struct tvec_base *base; unsigned long flags; int ret = -1; base = lock_timer_base(timer, &flags); if (base->running_timer == timer) goto out; ret = 0; if (timer_pending(timer)) { detach_timer(timer, 1); ret = 1; }out: spin_unlock_irqrestore(&base->lock, flags); return ret;}
该函数检测当前运行的软件时钟是不是该软件时钟,如果是,则函数返回-1,表明目前不能删除该软件时钟;如果不是检测该软件时钟是否处于 pending 状态,如果不是,则函数返回0,表明软件时钟已经被卸载,如果处于 pending 状态再把软件时钟卸载,函数返回1,表明成功卸载该软件时钟。
接下来,再来看看函数 del_timer_sync 定义,如清单3-6
清单3-6 del_timer_sync 函数
int del_timer_sync(struct timer_list *timer){ for (;;) { int ret = try_to_del_timer_sync(timer); if (ret >= 0) return ret; cpu_relax(); }}
del_timer_sync 函数无限循环试图卸载该软件时钟,直到该软件时钟能够被成功卸载。从其实现中可以看出:如果一个软件时钟的处理函数正在执行时,对其的卸载操作将会失败。 一直等到软件时钟的处理函数运行结束后,卸载操作才会成功。这样避免了在 SMP 系统中一个 CPU 正在执行软件时钟的处理函数,而另一个 CPU 则要将该软件时钟卸载所引发的问题。
3.3 时钟的软中断处理
软件时钟的处理是在时钟的软中断中进行的。
3.3.1 软中断初始化
软中断的一个重要的处理时机是在每个硬件中断处理完成后(参见 irq_exit 函数),且由2.4节的内容可知:在硬件时钟中断处理中,会唤醒时钟的软中断,所以每次硬件时钟中断处理函数执行完成后都要进行时钟的软中断处理。和时钟 相关的软中断是 TIMER_SOFTIRQ ,其处理函数为 run_timer_softirq ,该函数用来处理所有的软件时钟。这部分初始化代码在函数 init_timers 中进行,如清单3-7
清单3-7 init_timers 函数
void __init init_timers(void){ …… open_softirq(TIMER_SOFTIRQ, run_timer_softirq, NULL);}
3.3.2 处理过程
函数 run_timer_softirq 所作的工作就是找出所有到期的软件时钟,然后依次执行其处理函数。其代码如清单3-8
清单3-8 run_timer_softirq函数
static void run_timer_softirq(struct softirq_action *h){ struct tvec_base *base = __get_cpu_var(tvec_bases); hrtimer_run_pending(); if (time_after_eq(jiffies, base->timer_jiffies)) __run_timers(base);}
函数首先获得到本地 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;}
- 最新评论
