三、硬件时钟设备
参见第二件时钟事件设备和时钟源的描述
四、一帘幽梦:低精度定时器的实现
前面的知识看起来似乎都很简单,现在问题来了,内核是如何管理定时器的?一个系统只要处理的数据上升一个数据量,其复杂程度与性能的挑战是非常大的。如果系统只有几十个定时器,那似乎没有什么,只需要一个for循环遍历是否超时即可,但如果定时器数量上升到了几千几万几十万呢?我们知道,硬件触发定时事件,定时事件的中断处理部分必须非常迅速,如果全部循环判断性能上是无法接受的。那linux内核是如何做到的呢?下面先看看低精度定时器的管理。
对于各个定时器来说,唯一的差异就是定时时间,而且我们知道,如果定时时间短的还没有超时,那么定时时间长的肯定也没有超市。那能不能根据定时时间把定时器分类散列开呢,每次定时中断到达后只判断定时时间最短的那些定时器?可以的,问题是怎么做?考虑一个32位的系统,那么定时时间就有1到约42亿个不同值,完全散列是不可能的(请你算下需要多少内存?)。
内核把定时时间散列成5组, 每组对应的时间间隔,分别如下:
tv1: 0 -> 255
tv2: 256 -> 2^14-1
tv3: 2^14 -> 2^20-1
tv4: 2^20 -> 2^26-1
tv5: 2^26 -> 2^32-1
参考struct tvec_base,
struct tvec_base {
spinlock_t lock;
struct timer_list *running_timer;
unsigned long timer_jiffies; -----当前时间轮上的jiffies,一般情况下比内核的jiffies相等或差1。可以看出,低精度定时器与系统jiffies是强相关的。
unsigned long next_timer;
struct tvec_root tv1;
struct tvec tv2;
struct tvec tv3;
struct tvec tv4;
struct tvec tv5;
} ____cacheline_aligned;
第一组有256个表项,对应着超时时间在下一个tick值到255个tick值。如果有多个定时器的超时值是相同的呢?相同的定时器通过链表串在一起。第二组有64个表项,这里的每个表项就不是对应着一个超时值了,而是一定超时间隔的都在同一个表项,相当于把把散列表压缩了。例如,第一项的链表里面的定时器的超时值就在范围256->511上。
第三组到第五组与第二组类似。
每个超时时间如何散列到各个组的,可以参考internal_add_timer的实现。
想象一下,有5个组,每个组下面挂着很多表项(256,64,64,64,64),每个表项下面又串着定时器,与一个帘子是很像的。
定时器是放到了合适的位置上了,那么超时时怎么处理。
由于定时器已经按照超时时间排好队了,那么就只需要判断第一组的第一个表项是否有定时器,如果有,则触发定时器;下一个超时时间到来,则判断第一组的第二个表项是否有定时器,依此。当到了256个定时器时间到来时,由于第一组的定时器都已经处理完毕,这时候该处理第二组的第一个表项了,前面说过,从第二组开始,是把散列表给压缩了,第二组的第一个表项下的定时器超时时间在256到511,因此,不能直接把这个表项下的所有定时器都触发,因此,必须把这里的定时器加压散开,系统就把这个表项下的定时器重新散列到第一组去,然后执行第一组的第一个表项的定时器链表。到了512个定时时间时,就把第二组的第二个表项下的定时器解压散列到第一组去。
依此往下,到了一定的时间间隔后,就把下一组的定时器往前移。
这就是定时器级联,或者说时间轮,与我们日常的时钟是很类似的,秒钟走了一圈后,分钟才动一下,分钟走完一圈后,时钟才动一下。
具体实现参考__run_timers函数:
int index = base->timer_jiffies & 255;
/*
* Cascade timers:、
*/
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));