把玩linux内核的定时器

今年对内核的slab,timer有了很深入的了解,并且有机会在windows下都重写了一次。slab的文章忘记发了,好像是网络上已经有很全的版本了,想主要说下timer,也就是俗称的定时器。

以前写过一个普通的定时器,简单来说就是串成一个长长的链表,然后分配一定的时间片来扫描。这样做,如果有排序的话还好,可以得知需要扫描的个数,并且有序的执行。快排的效率是是log2n,虽然很不错了,学习学习内核里的方法,开拓下视野也非常的美味。


换个思路来说,时间起于混沌的话(从0开始),并且只有20秒,你要分别在5秒,10秒,15秒做一件事情,你肯定会这样考虑,直接下标啊。简单代码可以写成

int array[20];
array[5]=fn1;
array[10]=fn2;
array[15]=fn3;
array[...]=NULL; //其他的设置为空
oldtime=time();
loop
{
	//do other....
	//...
	
	nowtime=time();
	if(oldtime<=nowtime)
	{
		 array[oldtime].fn();
		 oldtime++;
	}
	
}
 

你会很自然的想到,可以把时间当成下标,安排好会发生的事情,然后直接执行,避免了轮询的耗费。那么在32位的机器里,如果以毫秒为单位,可以支持到50年左右。内核里怎么做的具体不详说,列出一些代码片段揣摩:

#define TVN_BITS (CONFIG_BASE_SMALL ? 4 : 6)
#define TVR_BITS (CONFIG_BASE_SMALL ? 6 : 8)
#define TVN_SIZE (1 << TVN_BITS)
#define TVR_SIZE (1 << TVR_BITS)
#define TVN_MASK (TVN_SIZE - 1)
#define TVR_MASK (TVR_SIZE - 1)

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 {
			i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;
		vec = base->tv5.vec + i;
		}

  一堆的宏,定义了分组的大小。分为5层,每层都是一个链表头数组,数组之间的大小也有一定的关系,这里不细展开。每个层关心的定时值也不一样,比如第一层关心0-255毫秒的时间,你可以看成是优先级非常高的时间,越往后时间越松散,比如第二层关系256-10000毫秒的时间,以此类推,有点像盗梦空间的时间观念。简单的结构图如下

                        兴趣的定时值

层5: -----------   [xxx, 0xffffffff]

层4: -----------   ...

层3: -----------   [1001, 100000]

层2: ----------- [256, 1000]

层1: ------------- [0,255]


想起开头的混沌时间了吗?在时间大值的情况下,如何存入这些小数组?直接用掩码表示:

int i = expires & TVR_MASK;

expires表示一个绝对时间,TVR_MASK表示第一层的掩码。

假设,现在运行了N久时间,毫秒值为12000000,定义一个定时时间为12000100,在进行掩码操作后被存到100的下标。那么来看,是怎么运行第一层的,如果我先插入一个100毫秒值,时间到后,我又插入一个10毫秒的值,这个时候两者之间距离90,如何执行?


//插入
	unsigned long expires = timer->expires; //timer->expires,设定的执行时间点
	unsigned long idx = expires - base->timer_jiffies;//定时器上一回时间
		int i = expires & TVR_MASK;
		vec = base->tv1.vec + i;	

//执行
int index = base->timer_jiffies & TVR_MASK;

也就是说不管现在时间如何,你什么时候插入,都以当前时间作为一个起始点,后来的时间顺序插入,如果超出则取模。接着刚才,现在时间是12000100,插入一个10毫秒,timer->expires为12000110,对12000110直接取模,得到110,插入在110。 而旧的索引在100,中间刚好10个间隔。


解决完运行的插入后,来看看如果从上层空间取时间。在每次的运行函数里,都有如下代码

#define INDEX(N) ((base->timer_jiffies >> (TVR_BITS + (N) * TVN_BITS)) & TVN_MASK)
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) {
		BUG_ON(tbase_get_base(timer->base) != base);
		internal_add_timer(base, timer);
	}

	return index;
}

		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));
 

看似每次都执行cascade函数,其实不是。index为刚才说锁的下标掩码,TVR_MASK表示第一个层的长度,也就是说,执行完一遍第一层,才向上层索取。而INDEX这个宏里,根据定时器旧的时间取出一个索引,然后把该索引下链表重新分配。然后返回index,如果是大于0的,则不往下执行了。也就是说,其实只是遍历了一个节点,进行重新分配,而且产生的条件是把第一层都给遍历完了。那么

疑问1:如果其实点不从0开始,而从250开始,隔5个点就要向上层索取一次吗?

疑问2:为什么只取上层的一个节点?

疑问3:上层和下层怎么保持同步的?为什么不从0开始,10开始?


解答疑问1:如果你理解了刚才的10,100毫秒的执行解释,就会发现,这个距离是相对的。如果你也理解了解释2,其实无论如何,第一层只要递增了一步,就可以从第二层取当前下标的数据。情形

如下:

0 (1).....255

0  ......(250) 255

(0) .....255

下回执行的,肯定是在() 的地方,刚好一个周期,所以见0就Cascade吧!


解答疑问2:在时间分层里,其实是有技巧,第一层大小是255, 第二层大小是64,时间范围是N。经过我的验算发现,N/64==253,也就说只要解开一个节点就够了,这里面全都是满足下一回对第一层遍历的定时点。


解答疑问3:代码显示

int i = (expires >> TVR_BITS) & TVN_MASK;


TVR_BITS是第一层(相对来说),取了一个倍数的关系。也就是说,如果第一层走完一圈,倍数+1,走完N圈,+N,而第二层(相对来说)通过插入时的这种取模已经确认的大小和时间关系,因此,第一层执行,相当时一个小齿轮,相对的推动了第二层的下标索引。因此也解释了,为什么第一层大小可以和第二层不一样。更重要的是符合一个进位的关系(N/64==253)



最后串起来:

看似上面讲了很多细节,包括我自己在写的时候,很多东西还要推敲一番。但说到本质,就三条:

1.取模

2.时间组织的相对值,相对位置

3.高层的每个时间刻度对下一层的进位关系


深深觉得,学东西,学得更多,更深,更精粹,越要理解本质的公式推理。细节,是用来验证,但不是用来记忆。


猜你喜欢

转载自lin-style.iteye.com/blog/775481
今日推荐