带你走进linux 内核 定时器(timer)实现机制

1 相关数据结构

1.1 timer_list

定时器层是基于tick层(高精度定时器)之上的,是根据系统jiffies来触发的,精度相对比较低。利用定时器,我们可以设定在未来的某一时刻,触发一个特定的事件。经常,也会把这种低精度定时器称作时间轮(Timer Wheel)。在内核中,一个定时器是使用 timer_list 结构体来表示的:

struct timer_list {
	struct hlist_node	entry;
	unsigned long		expires;
	void			(*function)(struct timer_list *);
	u32			flags;
        ......
};
  • entry:所有的定时器都会根据到期的时间被分配到一组链表中的一个中,该字段是链表的节点成员。
  • expires:字段指出了该定时器的到期时刻,也就是期望定时器到期时刻的jiffies计数值。这是一个绝对值,不是距离当前时刻再过多少jiffies。
  • function:是一个回调函数指针,定时器到期时,系统将会调用该函数,用于响应该定时器的到期事件。
  • flags:看名字应该是标志位,其定义如下:
#define TIMER_CPUMASK		0x0003FFFF
#define TIMER_MIGRATING		0x00040000
#define TIMER_BASEMASK		(TIMER_CPUMASK | TIMER_MIGRATING)
#define TIMER_DEFERRABLE	0x00080000
#define TIMER_PINNED		0x00100000
#define TIMER_IRQSAFE		0x00200000
#define TIMER_ARRAYSHIFT	22
#define TIMER_ARRAYMASK		0xFFC00000

可以看到,其实并不是标志位那么简单。其最高 10 位记录了定时器放置到桶的编号,后面会提到一共最多只有576个桶,所以10位足够了。而最低的18位指示了该定时器绑定到了哪个CPU上,注意是一个数值,而不是位图。夹在中间的一些位到真的是一些标志位。TIMER_MIGRATING表示定时器正在从一个CPU迁移到另外一个CPU。TIMER_DEFERRABLE表示该定时器是可延迟的。TIMER_PINNED表示定时器已经绑死了当前的CPU,无论如何都不会迁移到别的CPU上。TIMER_IRQSAFE表示定时器是中断安全的,使用的时候只需要加锁,不需要关中断。

1.2 timer_base

系统中可能同时存在成千上万个定时器,如果处理不好效率会非常低下。Linux目前会将定时器按照绑定的CPU和种类(普通定时器还是可延迟定时器两种)进行区分,由timer_base结构体组织起来:

struct timer_base {
	raw_spinlock_t		lock;
	struct timer_list	*running_timer;
        ......
	unsigned long		clk;
	unsigned long		next_expiry;
	unsigned int		cpu;
	bool			is_idle;
	bool			must_forward_clk;
	DECLARE_BITMAP(pending_map, WHEEL_SIZE);
	struct hlist_head	vectors[WHEEL_SIZE];
} ____cacheline_aligned;
  • lock:保护该timer_base结构体的自旋锁,这个自旋锁还同时保护包含在vectors链表数组中的所有定时器。
  • running_timer:该字段指向当前CPU正在处理的定时器所对应的timer_list结构。
  • clk:当前定时器所经过的 jiffies,用来判断包含的定时器是否已经到期或超时。
  • next_expiry:该字段指向该CPU下一个即将到期的定时器。最早 (距离超时最近的 timer) 的超时时间
  • cpu:所属的CPU号。
  • is_idle:指示是否处于空闲模式下,在NO_HZ模式下会用到。
  • must_forward_clk:指示是否需要更新当前clk的值,在NO_HZ模式下会用到。
  • pending_map:一个比特位图,时间轮中有几个桶就有几个比特位。如果某个桶内有定时器存在,那么就将相应的比特位置1。
  • vectors:时间轮所有桶的数组,每一个元素是一个链表。

每个CPU都含有一到两个timer_base结构体变量:

static DEFINE_PER_CPU(struct timer_base, timer_bases[NR_BASES]);

其中NR_BASES定义如下:

#ifdef CONFIG_NO_HZ_COMMON
# define NR_BASES	2
# define BASE_STD	0
# define BASE_DEF	1
#else
# define NR_BASES	1
# define BASE_STD	0
# define BASE_DEF	0
#endif

所以如果内核编译选项包含 CONFIG_NO_HZ_COMMON,则每个CPU有两个timer_base结构体,下标分别是BASE_STD(Standard)和BASE_DEF(Deferrable)。如果内核编译选项没有包含CONFIG_NO_HZ_COMMON,那么每个CPU只有一个timer_base结构体,BASE_STD和BASE_DEF是同一个。

为什么支持NO_HZ模式要包含两个timer_base呢?这其实和NO_HZ的工作模式有关。如果NO_HZ模式,那么当CPU处于空闲状态时,定时器层是收不到也不需要收到任何Tick的,这样可以节省电力。这时候底层的Tick层(准确说是Tick Sched)不会按照预定好的HZ频率,每次到期后都去不停的设置底层的定时事件设备(启动NO_HZ模式的前提是已经切换到了高精度模式下而高精度模式又要求定时事件设备是单次触发模式的)。但是,如果定时器到期了不就错过去了嘛。所以,在停止Tick之前,Tick层会从定时器层获得最近的下一次定时器到期的时间(通过调用get_next_timer_interrupt函数),然后对下面的定时事件设备进行编程,让其在这个最近的到期时刻到期,触发中断。但是,系统中有很多定时器,它们对到期的要求没有那么严格,迟一点到期也不是很要紧。对于这类定时器,在停止Tick之前,就没必要管他们到低什么时候到期。具体点说,就是Tick层在向定时器层询问下一次最近到期时间时,定时器层更本就不会查找这些可延迟的定时器。对于前面说的第一种定时器存放在BASE_STD指明的那个timer_base结构体里面,而第二种定时器存放在BASE_DEF指明的那个timer_base结构体里面。如果在编译内核的时候没有包含CONFIG_NO_HZ_COMMON,也就是内核不支持NO_HZ模式,Tick从来就没有停止过,当然就不存在前面说的问题,也就没必要分两个了。

如果在内核配置文件里定义Tick周期是100的话,一共有8个级别(编号从0到7);而如果大于100的话,则一共会包含9个级别(编号从0到8)。

#if HZ > 100
# define LVL_DEPTH	9
# else
# define LVL_DEPTH	8
#endif

一个级(Level)里面共有64(LVL_SIZE)个桶(Bucket),用6个比特表示:

#define LVL_BITS	6
#define LVL_SIZE	(1UL << LVL_BITS)
#define LVL_MASK	(LVL_SIZE - 1)
#define LVL_OFFS(n)	((n) * LVL_SIZE)

宏LVL_OFFS定义了每一级桶下表的起始编号。所以,对于每个timer_base一共需要的桶的数目定义为:

#define WHEEL_SIZE	(LVL_SIZE * LVL_DEPTH)

还有一个概念叫做粒度(Granularity),表示系统至少要过多少个Tick才会检查某一个级里面的所有定时器。每一级的64个桶的检查粒度是一样的,而不同级内的桶之间检查的粒度不同,级数越小,检查粒度越细。每一级粒度的Tick数由宏定义LVL_CLK_DIV的值决定:

#define LVL_CLK_SHIFT	3
#define LVL_CLK_DIV	(1UL << LVL_CLK_SHIFT)
#define LVL_CLK_MASK	(LVL_CLK_DIV - 1)
#define LVL_SHIFT(n)	((n) * LVL_CLK_SHIFT)
#define LVL_GRAN(n)	(1UL << LVL_SHIFT(n))

具体的计算公式为:

也就是第0级内64个桶中存放的所有定时器每个Tick都会检查,第1级内64个桶中存放的所有定时器每8个Tick才会检查,第2级内64个桶中存放的所有定时器每64个Tick才会检查,以此类推。

对应每一个级,都有一个范围,其起始的Tick值由LVL_START定义:

#define LVL_START(n)	((LVL_SIZE - 1) << (((n) - 1) * LVL_CLK_SHIFT))

这里 n 从 1 开始,取值范围 1 到 7 或 1到8 。不过这个定义貌似有问题,应该是:

#define LVL_START(n)	((LVL_SIZE) << (((n) - 1) * LVL_CLK_SHIFT))

下面具体举个例子,内核配置选项将HZ配置位250,那么就一共需要9个级别,每个级别里面有64个桶,所以一共需要576个桶。每个级别的情况如下表:

因为配置的是250Hz,所以每次Tick之间经过4毫秒。可以看出来,定时到期时间距离现在越久,那粒度就越差,误差也越大。

具体将定时器放到哪一个级下面是由到期时间距离现在时间的差值,也就是距离现在还要过多长时间决定的;而要放到哪个桶里面,则单纯是由到期时间决定的。

所以,综上所述,定时器层的数据结构如下图所示:

【文章福利】小编推荐自己的Linux内核技术交流群: 【977878001】整理一些个人觉得比较好得学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100进群领取,额外赠送一份 价值699的内核资料包(含视频教程、电子书、实战项目及代码)

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

2 定时器工作过程

2.1 桶编号计算

calc_wheel_index 函数根据到期 jiffies 和已经过 jiffies 两个参数,计算要将定时器放置到哪个桶下:

static int calc_wheel_index(unsigned long expires, unsigned long clk)
{
        /* 到期jiffies和已经过jiffies的差 */
	unsigned long delta = expires - clk;
	unsigned int idx;
 
        /* 按照差所处的范围来决定把定时器放到哪一级 */
	if (delta < LVL_START(1)) {
		idx = calc_index(expires, 0);
	} else if (delta < LVL_START(2)) {
		idx = calc_index(expires, 1);
	} else if (delta < LVL_START(3)) {
		idx = calc_index(expires, 2);
	} else if (delta < LVL_START(4)) {
		idx = calc_index(expires, 3);
	} else if (delta < LVL_START(5)) {
		idx = calc_index(expires, 4);
	} else if (delta < LVL_START(6)) {
		idx = calc_index(expires, 5);
	} else if (delta < LVL_START(7)) {
		idx = calc_index(expires, 6);
	} else if (LVL_DEPTH > 8 && delta < LVL_START(8))
  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值