linux3.10 时间子系统(二)定时器原理

本文引用自如下博文:https://www.cnblogs.com/hoys/archive/2012/02/21/2361675.html

软件定时器(Software Timers),是一个软件上的概念,是建立在硬件时钟基础之上的。它记录了未来某一时刻要执行的操作(函数),并使得当这一时刻真正到来时,这些操作(函数)能够被按时执行。举个例子说明:它就像生活中的闹铃,给闹铃设定振铃时间(未来的某一时间)后,当时间(相当于硬件时钟)更新到这个振铃时间后,闹铃就会振铃。这个振铃时间好比软件时钟的到期时间,振铃这个动作好比软件时钟到期后要执行的函数,而闹铃时间更新好比硬件时钟的更新。

实现软件时钟原理也比较简单:每一次硬件时钟中断到达时,内核更新的 jiffies ,然后将其和软件时钟的到期时间进行比较。如果 jiffies 等于或者大于软件时钟的到期时间,内核就执行软件时钟指定的函数。

1 相关数据结构

struct timer_list :软件时钟,记录了软件时钟的到期时间以及到期后要执行的操作。

struct timer_list {
	/*
	 * All fields that change during normal runtime grouped to the
	 * same cacheline
	 */
	struct list_head entry; //所在的链表
	unsigned long expires; //到期时间,以 tick 为单位
	struct tvec_base *base;//记录该软件时钟所在的 struct tvec_base 变量

	void (*function)(unsigned long);//回调函数,到期后执行的操作
	unsigned long data;

	int slack;

#ifdef CONFIG_TIMER_STATS
	int start_pid;
	void *start_site;
	char start_comm[16];
#endif
#ifdef CONFIG_LOCKDEP
	struct lockdep_map lockdep_map;
#endif
};

struct tvec_base :用于组织、管理软件时钟的结构。在 SMP 系统中,每个 CPU 有一个。

struct tvec_base {
	spinlock_t lock;//用于同步操作
	struct timer_list *running_timer;//正在处理的软件时钟
	unsigned long timer_jiffies;//当前正在处理的软件时钟到期时间
	unsigned long next_timer;
	unsigned long active_timers;
	struct tvec_root tv1;//保存了到期时间从 timer_jiffies 到 timer_jiffies + 2^8-1之间(包括边缘值)的所有软件时钟
	struct tvec tv2;//保存了到期时间从 timer_jiffies +  2^8 到 timer_jiffies + 2^14-1之间(包括边缘值)的 所有软件时钟
	struct tvec tv3;//保存了到期时间从 timer_jiffies + 2^14到 timer_jiffies + 2^20-1 之间(包括边缘值)的所有软件时钟
	struct tvec tv4;//保存了到期时间从 timer_jiffies + 2^20 到 timer_jiffies + 2^26-1 之间(包括边缘值)的所有软件时钟
	struct tvec tv5;//保存了到期时间从 timer_jiffies + 2^26 到 timer_jiffies + 2^32-1 之间(包括边缘值)的所有软件时钟
} ____cacheline_aligned;

一个 tick 表示的时间长度为两次硬件时钟中断发生时的时间间隔.

其中 tv1 的类型为 struct tvec_root ,tv 2~ tv 5的类型为 struct tvec:

struct tvec {
	struct list_head vec[TVN_SIZE];
};

struct tvec_root {
	struct list_head vec[TVR_SIZE];
};

可见它们实际上就是类型为 struct list_head 的数组,其中 TVN_SIZE 和 TVR_SIZE 在系统没有配置宏 CONFIG_BASE_SMALL 时分别被定义为64和256。

2 数据结构之间的关系

从图中可以清楚地看出:软件时钟( struct timer_list ,在图中由 timer 表示)以双向链表( struct list_head )的形式,按照它们的到期时间保存相应的桶( tv1~tv5 )中。tv1 中保存了相对于 timer_jiffies 下256个 tick 时间内到期的所有软件时钟; tv2 中保存了相对于 timer_jiffies 下256*64个 tick 时间内到期的所有软件时钟; tv3 中保存了相对于 timer_jiffies 下256*64*64个 tick 时间内到期的所有软件时钟; tv4 中保存了相对于 timer_jiffies 下256*64*64*64个 tick 时间内到期的所有软件时钟; tv5 中保存了相对于 timer_jiffies 下256*64*64*64*64个 tick 时间内到期的所有软件时钟。具体的说,从静态的角度看,假设 timer_jiffies 为0,那么 tv1[0] 保存着当前到期(到期时间等于 timer_jiffies )的软件时钟(需要马上被处理), tv1[1] 保存着下一个 tick 到达时,到期的所有软件时钟, tv1[n] (0<= n <=255)保存着下 n 个 tick 到达时,到期的所有软件时钟。而 tv2[0] 则保存着下256到511个 tick 之间到期所有软件时钟, tv2[1] 保存着下512到767个 tick 之间到期的所有软件时钟, tv2[n] (0<= n <=63)保存着下256*(n+1)到256*(n+2)-1个 tick 之间到达的所有软件时钟。 tv3~tv5 依次类推。

从上面的说明中可以看出:软件时钟是按照其到期时间相对于当前正在处理的软件时钟的到期时间( timer_jiffies 的数值)保存在 struct tvec_base 变量中的。而且这个到期时间的最大相对值(到期时间 - timer_jiffies )为 0xffffffffUL ( tv5 最后一个元素能够表示的相对到期时间的最大值)。

还需要注意的是软件时钟的处理是局部于 CPU 的,所以在 SMP 系统中每一个 CPU 都保存一个类型为 struct tvec_base 的变量,用来组织、管理本 CPU 的软件时钟。从图中也可以看出 struct tvec_base 变量是 per-CPU 的(关于 per-CPU 的变量原理和使用参见参考资料)。

由于以后的讲解经常要提到每个 CPU 相关的 struct tvec_base 变量,所以为了方便,称保存软件时钟的 struct tvec_base 变量为该软件时钟的 base ,或称 CPU 的 base 。

2.1 添加软件时钟

在 Linux 内核中要添加一个软件时钟,首先必须分配 struct timer_list 类型的变量,然后调用函数 add_timer() 将该软件时钟添加到相应调用 add_timer 函数的 CPU 的 base 中。 Add_timer 是对函数 __mod_timer() 的一层包装。函数 __mod_timer():

static inline int
__mod_timer(struct timer_list *timer, unsigned long expires,
						bool pending_only, int pinned)
{
	struct tvec_base *base, *new_base;
	unsigned long flags;
	int ret = 0 , cpu;

	timer_stats_timer_set_start_info(timer);
	BUG_ON(!timer->function);

	base = lock_timer_base(timer, &flags);

	ret = detach_if_pending(timer, base, false);
	if (!ret && pending_only)
		goto out_unlock;

	debug_activate(timer, expires);

	cpu = smp_processor_id();

#if defined(CONFIG_NO_HZ_COMMON) && defined(CONFIG_SMP)
	if (!pinned && get_sysctl_timer_migration() && idle_cpu(cpu))
		cpu = get_nohz_timer_target();
#endif
	new_base = per_cpu(tvec_bases, cpu);

	if (base != new_base) {
		/*
		 * We are trying to schedule the timer on the local CPU.
		 * However we can't change timer's base while it is running,
		 * otherwise del_timer_sync() can't detect that the timer's
		 * handler yet has not finished. This also guarantees that
		 * the timer is serialized wrt itself.
		 */
		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);

out_unlock:
	spin_unlock_irqrestore(&base->lock, flags);

	return ret;
}

 

代码解释:

  1. 取得软件时钟所在 base 上的同步锁( struct tvec_base 变量中的自旋锁),并返回该软件时钟的 base ,保存在 base 变量中
  2. 取得本 CPU 上的 base 指针(类型为 struct tvec_base* ),保存在 new_base 中
  3. 如果 base 和 new_base 不一样,也就是说软件时钟发生了迁移(从一个 CPU 中移到了另一个 CPU 上),那么如果该软件时钟的处理函数当前没有在迁移之前的那个 CPU 上运行,则先将软件时钟的 base 设置为 NULL ,然后再将该软件时钟的 base 设置为 new_base 。否则,跳到5。
  4. 设置软件时钟的到期时间
  5. 调用 internal_add_timer 函数将软件时钟添加到软件时钟的 base 中(本 CPU 的 base )
  6. 释放锁

 

这里有必要详细说明一下软件时钟如何被添加到软件时钟的 base 中的(添加到本 CPU base 的 tv1~tv5 里面),因为这是软件时钟处理的基础。来看函数 internal_add_timer 函数的实现:

internal_add_timer

       ------------>__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) {
		/*
		 * Can happen if you add a timer with expires == jiffies,
		 * or you set a timer to go off in the past
		 */
		vec = base->tv1.vec + (base->timer_jiffies & TVR_MASK);
	} else {
		int i;
		/* If the timeout is larger than MAX_TVAL (on 64-bit
		 * architectures or with CONFIG_BASE_SMALL=1) then we
		 * use the maximum timeout.
		 */
		if (idx > MAX_TVAL) {
			idx = MAX_TVAL;
			expires = idx + base->timer_jiffies;
		}
		i = (expires >> (TVR_BITS + 3 * TVN_BITS)) & TVN_MASK;
		vec = base->tv5.vec + i;
	}
	/*
	 * Timers are FIFO:
	 */
	list_add_tail(&timer->entry, vec);
}
  • 计算该软件时钟的到期时间和timer_jiffies (当前正在处理的软件时钟的到期时间)的差值,作为索引保存到 idx 变量中。
  • 判断 idx 所在的区间,在
    • [0,  2^8-1 ]或者( -无穷, 0)(该软件时钟已经到期),则将要添加到 tv1 中
    • [ 2^8 ,  2^14-1 ],则将要添加到 tv2 中
    • [ 2^14,  2^20-1 ],则将要添加到 tv3 中
    • [ 2^20 ,  2^26-1 ],则将要添加到 tv4 中
    • [ 2^26 ,  2^32-1 ),则将要添加到 tv5 中,但实际上最大值为 0xffffffffUL
  • 计算所要加入的具体位置(哪个链表中,即 tv1~tv5 的哪个子链表,参考图3-1)
  • 最后将其添加到相应的链表中
  • 从这个函数可以得知,内核中是按照软件时钟到期时间的相对值(相对于 timer_jiffies 的值)将软件时钟添加到软件时钟所在的 base 中的,但是确定好base中的tv级别后,在tv中的具体位置,却是直接用expires 和掩码计算得出的。 

2.2  删除软件时钟

内核可调用 del_timer 函数删除软件时钟

int del_timer(struct timer_list *timer)
{
	struct tvec_base *base;
	unsigned long flags;
	int ret = 0;

	debug_assert_init(timer);

	timer_stats_timer_clear_start_info(timer);
	if (timer_pending(timer)) {
		base = lock_timer_base(timer, &flags);
		ret = detach_if_pending(timer, base, true);
		spin_unlock_irqrestore(&base->lock, flags);
	}

	return ret;
}

del_timer

       --------->detach_if_pending

              ---------->detach_timer

static inline void detach_timer(struct timer_list *timer, bool clear_pending)
{
	struct list_head *entry = &timer->entry;

	debug_deactivate(timer);

	__list_del(entry->prev, entry->next);
	if (clear_pending)
		entry->next = NULL;
	entry->prev = LIST_POISON2;
}

把 timer从链表中删除

3 时钟软中断处理

软中断的一个重要的处理时机是在每个硬件中断处理完成后(参见 irq_exit 函数),且由2.4节的内容可知:在硬件时钟中断处理中,会唤醒时钟的软中断,所以每次硬件时钟中断处理函数执行完成后都要进行时钟的软中断处理。和时钟相关的软中断是 TIMER_SOFTIRQ ,其处理函数为 run_timer_softirq ,该函数用来处理所有的软件时钟。这部分初始化代码在函数 init_timers:

start_kernel

        ---------->init_timers

void __init init_timers(void)
{
	int err;

	/* ensure there are enough low bits for flags in timer->base pointer */
	BUILD_BUG_ON(__alignof__(struct tvec_base) & TIMER_FLAG_MASK);

	err = timer_cpu_notify(&timers_nb, (unsigned long)CPU_UP_PREPARE,
			       (void *)(long)smp_processor_id());
	init_timer_stats();

	BUG_ON(err != NOTIFY_OK);
	register_cpu_notifier(&timers_nb);
	open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}

函数 run_timer_softirq 所作的工作就是找出所有到期的软件时钟,然后依次执行其处理函数:

static void run_timer_softirq(struct softirq_action *h)
{
	struct tvec_base *base = __this_cpu_read(tvec_bases);

	hrtimer_run_pending();

	if (time_after_eq(jiffies, base->timer_jiffies))
		__run_timers(base);
}

 函数首先获得到本地 CPU 的 base 。然后检测如果 jiffies大于等于 timer_jiffies ,说明可能已经有软件时钟到期了,此时就要进行软件时钟的处理,调用函数 __run_timers 进行处理。如果 jiffies 小于 timer_jiffies ,表明没有软件时钟到期,则不用对软件时钟进行处理。函数返回。接下来看一下函数 __run_timers 都作了些什么:

static inline void __run_timers(struct tvec_base *base)
{
	struct timer_list *timer;

	spin_lock_irq(&base->lock);
	while (time_after_eq(jiffies, base->timer_jiffies)) {
		struct list_head work_list;
		struct list_head *head = &work_list;
		int index = base->timer_jiffies & TVR_MASK;

		/*
		 * 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));
		++base->timer_jiffies;
		list_replace_init(base->tv1.vec + index, &work_list);
		while (!list_empty(head)) {
			void (*fn)(unsigned long);
			unsigned long data;
			bool irqsafe;

			timer = list_first_entry(head, struct timer_list,entry);
			fn = timer->function;
			data = timer->data;
			irqsafe = tbase_get_irqsafe(timer->base);

			timer_stats_account_timer(timer);

			base->running_timer = timer;
			detach_expired_timer(timer, base);

			if (irqsafe) {
				spin_unlock(&base->lock);
				call_timer_fn(timer, fn, data);
				spin_lock(&base->lock);
			} else {
				spin_unlock_irq(&base->lock);
				call_timer_fn(timer, fn, data);
				spin_lock_irq(&base->lock);
			}
		}
	}
	base->running_timer = NULL;
	spin_unlock_irq(&base->lock);
}

上面函数完成:

  1. 获得 base 的同步锁
  2. 如果 jiffies 大于等于 timer_jiffies (当前正要处理的软件时钟的到期时间,说明可能有软件时钟到期了),就一直运行最外面的循环:while (time_after_eq(jiffies, base->timer_jiffies))
  3. 计算得到 tv1 的索引,该索引指明当前到期的软件时钟所在 tv1 中的链表,代码:
int index = base->timer_jiffies & TVR_MASK;
  1. 调用 cascade 函数对软件时钟进行必要的调整(稍后会介绍调整的过程)
  2. 使得 timer_jiffies 的数值增加1
  3. 取出相应的软件时钟链表
  4. 遍历该链表,对每个元素进行如下操作
  • 设置当前软件时钟为 base 中正在运行的软件时钟(即保存当前软件时钟到 base-> running_timer 成员中)
  • 将当前软件时钟从链表中删除,即卸载该软件时钟
  • 释放锁,执行软件时钟处理程序
  • 再次获得锁
  1. 设置当前 base 中不存在正在运行的软件时钟
  2. 释放锁

函数 cascade 用于调整软件时钟(这个调整过程是指:将马上就要到期的软件时钟从其所在的链表中删除,重新计算到期时间的相对值(到期时间 - timer_jiffies ),然后根据该值重新插入到 base 中)。注意到在软件时钟处理过程中,每次都是从 tv1 中取出一个链表进行处理,而不是从 tv2~tv5 中取,所以对软件时钟就要进行必要的调整。

在讲解 cascade 函数之前,再从直观上理解下为什么需要进行调整。所有软件时钟都是按照其到期时间的相对值(相对于 timer_jiffies )被调加到 base 中的。但是 timer_jiffies 的数值都会在处理中增加1(如3.3.2节所示),也就是说这个相对值会随着处理发生变化,当这个相对值小于等于256时,就要将软件时钟从 tv2移到 tv1 中( tv1 中保存着下256个 tick 内到期的所有软件时钟)。相似的,或者要把t3移动到t2,依次类推

static int cascade(struct tvec_base *base, struct tvec *tv, int index)
{
	/* cascade all the timers from tv up one level */
	struct timer_list *timer, *tmp;
	struct list_head tv_list;

	list_replace_init(tv->vec + index, &tv_list);

	/*
	 * We are removing _all_ timers from the list, so we
	 * don't have to detach them individually.
	 */
	list_for_each_entry_safe(timer, tmp, &tv_list, entry) {
		BUG_ON(tbase_get_base(timer->base) != base);
		/* No accounting, while moving them */
		__internal_add_timer(base, timer);
	}

	return index;
}

 这部分代码表明:如果 index 有0再到0时( index 是对 timer_jiffies 取模),说明时间已经过了256个 tick ,这时要把 tv2 中软件时钟转移到 tv1 中。如果 index 和第一个 cascade 函数的返回值都从0再到到0时,说明时间已经过了256*64个 tick ,这时要把 tv3 中软件时钟转移到 tv1 或者 tv2 中。之后的调整过程依次类推。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值