Linux时间子系统之Tick层

所谓Tick设备,也称作滴答设备,就是系统中的一个周期中断设备,其周期间隔由内核编译选项定义。

Tick设备在Linux时间子系统中用tick_device结构体表示(代码位于kernel/time/tick-sched.h中):

struct tick_device {
	struct clock_event_device *evtdev;
	enum tick_device_mode mode;
};

可以看出来,它仅仅是对定时事件设备(Clock Event Device)的封装,其本质上还是基于下层定时事件设备的能力。当然,一般硬件提供的定时时间设备要比Tick的周期精度高得多。除此之外,还有一个字段mode专门用来指定Tick设备所工作的模式:

enum tick_device_mode {
	TICKDEV_MODE_PERIODIC,
	TICKDEV_MODE_ONESHOT,
};

也是支持周期模式(TICKDEV_MODE_PERIODIC)和单次触发模式(TICKDEV_MODE_ONESHOT)。是不是很熟悉,在定时事件设备中也有个features域,记录了当前定时事件设备的工作特性,其中也包含了周期设备(CLOCK_EVT_FEAT_PERIODIC)和单次触发设备(CLOCK_EVT_FEAT_ONESHOT)。但这里要特别注意,这两个概念是不同的,Tick设备是基于定时事件设备的,Tick设备的精度肯定是小于等于定时事件设备的精度的。如果定时事件设备是周期模式的,那么基于此设备的Tick设备一定也是周期模式的;反之,如果定时事件设备是单次触发模式的,则Tick设备可以是单次触发模式的,也可以是周期触发模式的。

Tick设备是每个处理器私有的,所以表示Tick设备的结构体变量是在内核镜像Per CPU节(Section)里面的(代码位于kernel/time/tick-common.c中):

DEFINE_PER_CPU(struct tick_device, tick_cpu_device);

下面我们还是分几个方面来解释一下Tick层的工作过程。

1)Tick设备的设置和切换

分析定时事件层的定时事件设备注册过程的时候,当注册上来一个新的定时事件设备的时候会调用Tick层的tick_check_new_device函数:

void tick_check_new_device(struct clock_event_device *newdev)
{
	struct clock_event_device *curdev;
	struct tick_device *td;
	int cpu;

        /* 获得当前正在运行CPU的id */
	cpu = smp_processor_id();
        /* 获得对应CPU的Per CPU数据中存放的tick_device结构 */
	td = &per_cpu(tick_cpu_device, cpu);
	curdev = td->evtdev;

	/* 根据新老设备是否绑定到当前CPU的情况做出判断 */
	if (!tick_check_percpu(curdev, newdev, cpu))
		goto out_bc;

	/* 根据其它条件判断,如新老设备的精度值,是否支持单次触发模式等。 */
	if (!tick_check_preferred(curdev, newdev))
		goto out_bc;

        /* 对应的驱动模块存不存在 */
	if (!try_module_get(newdev->owner))
		return;

	/* 如果当前设备是广播设备则先将其关闭 */
	if (tick_is_broadcast_device(curdev)) {
		clockevents_shutdown(curdev);
		curdev = NULL;
	}
        /* 通知定时事件层切换设备 */
	clockevents_exchange_device(curdev, newdev);
        /* 用该定时事件设备设置本CPU的Tick设备 */
	tick_setup_device(td, newdev, cpu, cpumask_of(cpu));
        /* 通知Tick模拟层定时事件设备已经改变 */
	if (newdev->features & CLOCK_EVT_FEAT_ONESHOT)
		tick_oneshot_notify();
	return;

out_bc:
	/* 尝试用新设备替换当前的广播设备 */
	tick_install_broadcast_device(newdev);
}

该函数先调用tick_check_percpu函数和tick_check_preferred函数判断是否要用新添加的设备替换当前的定时事件设备,如果不需要的话,会调用tick_install_broadcast_device函数看是不是可以用这个设备替换当前的广播设备。如果判断都通过了,确实需要用新设备替换老设备,则先调用clockevents_exchange_device函数通知ClockEvents层要更换设备了,然后调用tick_setup_device函数真的用新注册的定时事件设备替换当前的,作为当前CPU使用的Tick设备。最后,会调用tick_oneshot_notify函数通知TickSched层,对应的定时事件设备已经改变。为什么只有设备是单次触发的才通知?TickSched层是高分辨率定时器(High Resolution Timer)切换到高分辨率模式后,用来模拟系统Tick的,而切换到高分辨率模式的前提条件是设备必须是单次触发的。

注意,在替换的时候,有可能并没有当前正在运行的定时事件设备,这种情况表明本CPU可能刚刚初始化,第一次设置Tick设备。

下面我们看看判断是否更新的条件:

static bool tick_check_percpu(struct clock_event_device *curdev,
			      struct clock_event_device *newdev, int cpu)
{
        /* 如果新设备不支持本CPU则还是用老设备 */
	if (!cpumask_test_cpu(cpu, newdev->cpumask))
		return false;
        /* 如果新设备就是和本CPU绑定的则通过检查 */
	if (cpumask_equal(newdev->cpumask, cpumask_of(cpu)))
		return true;
	/* 如果新设备不是专门为本CPU服务的则看新设备的中断能不能绑定本CPU */
	if (newdev->irq >= 0 && !irq_can_set_affinity(newdev->irq))
		return false;
	/* 如果当前设备存在且与本CPU绑定则还是用老设备 */
	if (curdev && cpumask_equal(curdev->cpumask, cpumask_of(cpu)))
		return false;
	return true;
}

这个函数主要是根据新老设备是否与本CPU绑定来判断是否要替换的。

static bool tick_check_preferred(struct clock_event_device *curdev,
				 struct clock_event_device *newdev)
{
	/* 优先选择支持单次触发功能的设备 */
	if (!(newdev->features & CLOCK_EVT_FEAT_ONESHOT)) {
		if (curdev && (curdev->features & CLOCK_EVT_FEAT_ONESHOT))
			return false;
		if (tick_oneshot_mode_active())
			return false;
	}

	return !curdev ||
		newdev->rating > curdev->rating ||
	       !cpumask_equal(curdev->cpumask, newdev->cpumask);
}

函数首先是检查新设备是否支持单次触发功能,如果不支持,那么如果当前设备存在且支持单次触发,则保持老设备。或者当前的Tick层已经被切换到单次触发模式了,新加入的定时事件设备如果还不支持那当然会被淘汰。tick_oneshot_mode_active函数只是读取本Per CPU变量中tick_cpu_device结构体中的mode字段,看其是不是TICKDEV_MODE_ONESHOT(代码位于kernel/time/tick-oneshot.c中):

int tick_oneshot_mode_active(void)
{
	unsigned long flags;
	int ret;

	local_irq_save(flags);
	ret = __this_cpu_read(tick_cpu_device.mode) == TICKDEV_MODE_ONESHOT;
	local_irq_restore(flags);

	return ret;
}

为什么会优先选择支持单次触发功能的定时事件设备呢?其实这里隐含了一个前提,就是一般支持单次触发功能的设备其精度要比只支持周期触发功能的设备要高。比如高精度定时器必须要在支持单次触发功能的定时事件设备上才能切换到真正的高精度模式。

比较完了是否支持单次触发功能后,如果通过,下面接着按顺序比较其它的一些属性:

  1. 如果当前设备是空的,也就是还没有设置,那肯定要选新设备;
  2. 如果新设备的精度值大于当前设备的精度值,则还是选新设备;
  3. 如果当前设备的cpumask等于新设备的cpumask,则选择老设备,否则选择新设备。也就是如果新设备什么方面都不比老设备好,那干嘛还要换呢。

所有检测都通过后,下面就真的要用新设备替换老设备了,在Tick层,这是通过tick_setup_device函数实现的:

static void tick_setup_device(struct tick_device *td,
			      struct clock_event_device *newdev, int cpu,
			      const struct cpumask *cpumask)
{
	void (*handler)(struct clock_event_device *) = NULL;
	ktime_t next_event = 0;

	/* 本CPU的Tick设备还没有被设置过,这是第一次设置。 */
	if (!td->evtdev) {
		/* 如果没有选中任何CPU上的Tick设备作为主设备更新系统jiffies */
		if (tick_do_timer_cpu == TICK_DO_TIMER_BOOT) {
			tick_do_timer_cpu = cpu;

			tick_next_period = ktime_get();
			tick_period = NSEC_PER_SEC / HZ;
#ifdef CONFIG_NO_HZ_FULL
			......
#endif
		}

		/* 无论如何初始的Tick设备一定工作在TICKDEV_MODE_PERIODIC模式下 */
		td->mode = TICKDEV_MODE_PERIODIC;
	} else {
                /* 取出原定时事件设备的event_handler回调函数和next_event到期时间 */
		handler = td->evtdev->event_handler;
		next_event = td->evtdev->next_event;
                /* 将原定时事件设备的回调函数设置成什么都不做 */
		td->evtdev->event_handler = clockevents_handle_noop;
	}

        /* 用新设备替换本Per CPU数据中tick_device的设备 */
	td->evtdev = newdev;

	/* 如果新设备不是本CPU私有的那要将其中断绑定到当前CPU上 */
	if (!cpumask_equal(newdev->cpumask, cpumask))
		irq_set_affinity(newdev->irq, cpumask);

        /* 向Tick广播层询问 */
	if (tick_device_uses_broadcast(newdev, cpu))
		return;

        /* 根据Tick设备的不同模式分别调用不同的设置函数 */
	if (td->mode == TICKDEV_MODE_PERIODIC)
		tick_setup_periodic(newdev, 0);
	else
		tick_setup_oneshot(newdev, handler, next_event);
}

tick_do_timer_cpu表示当前系统中由哪个CPU上的Tick设备负责更新系统jiffies。tick_next_period表示下一次Tick的到期时间。tick_period表示一个Tick的周期是多少纳秒。它们都是全局变量:

ktime_t tick_next_period;
ktime_t tick_period;

int tick_do_timer_cpu __read_mostly = TICK_DO_TIMER_BOOT;

在系统初始化的时候tick_next_period和tick_period是没有设置任何初始值的;而tick_do_timer_cpu被设置成了TICK_DO_TIMER_BOOT(-2),这个值说明目前还没有任何CPU上的Tick设备被选为主设备更新系统jiffies,也就意味着目前还没有任何Tick设备被初始化过,因为第一个被初始化的设备肯定会被选中。所以这时,全局的tick_period和tick_next_period也没被初始化。tick_check_new_device函数是在获得自旋锁且关闭本地中断的情况下调用的,因此tick_setup_device函数在访问全局变量tick_do_timer_cpu、tick_next_period和tick_period时是不存在同步问题的。

tick_period的值被设置成了NSEC_PER_SEC / HZ,其中HZ的值最终是由编译内核的配置文件决定的,在笔者的环境里,其被设置成250:

# CONFIG_HZ_100 is not set
CONFIG_HZ_250=y
# CONFIG_HZ_300 is not set
# CONFIG_HZ_1000 is not set
CONFIG_HZ=250

一共有四个值可以选,分别是100、250、300和1000。在HZ是250的情况下,也就意味着每秒产生250次Tick,每次Tick之间的周期间隔是4000000纳秒。

tick_device_uses_broadcast函数的功能是向Tick广播层询问,当前CPU上Tick设备切换成新的定时事件设备后,会不会对Tick广播层产生什么影响。如果Tick广播层判断当前CPU上的Tick由于新设备的原因完全由Tick广播层来负责触发了,就直接返回了,不需要接下来的设置了。

这里说的替换不是替换Tick设备,而是替换Tick设备下面的定时事件设备,Tick设备的模式并不会因为替换而产生改变。如果当前Tick设备是处在周期触发模式的,则调用tick_setup_periodic函数:

void tick_setup_periodic(struct clock_event_device *dev, int broadcast)
{
        /* 设置当前设备的中断回调函数 */
	tick_set_periodic_handler(dev, broadcast);

	if (!tick_device_is_functional(dev))
		return;

	if ((dev->features & CLOCK_EVT_FEAT_PERIODIC) &&
	    !tick_broadcast_oneshot_active()) {
		clockevents_switch_state(dev, CLOCK_EVT_STATE_PERIODIC);
	} else {
		unsigned int seq;
		ktime_t next;

		do {
			seq = read_seqbegin(&jiffies_lock);
			next = tick_next_period;
		} while (read_seqretry(&jiffies_lock, seq));

		clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT);

                /* 对定时事件设备进行编程 */
		for (;;) {
			if (!clockevents_program_event(dev, next, false))
				return;
			next = ktime_add(next, tick_period);
		}
	}
}

如果底层的定时事件设备本身就支持周期触发功能,则直接将其状态转换成CLOCK_EVT_STATE_PERIODIC就行了。此时Tick设备的Tick周期和定时事件设备本身的触发周期是一样的。否则,将定时事件设备的状态切换到CLOCK_EVT_STATE_ONESHOT,并对其进行编程,让其在下一个Tick到来时触发中断。

全局变量tick_next_period记录了系统中所有Tick设备下一个Tick的到期时间,对其的读写和系统jiffies一样,都是用jiffies_lock顺序锁保护的,为什么要这样,在后面会解释。

tick_set_periodic_handler函数用来设置设备的中断回调函数(代码位于kernel/time/tick-broadcast.c中):

void tick_set_periodic_handler(struct clock_event_device *dev, int broadcast)
{
	if (!broadcast)
		dev->event_handler = tick_handle_periodic;
	else
		dev->event_handler = tick_handle_periodic_broadcast;
}

该函数根据是不是广播设备来设定定时事件设备的中断回调函数。在当前执行环境下,broadcast被设置成了0,因此将tick_handle_periodic函数指针赋值给event_handler。

如果当前的Tick设备是处在单次触发模式的,则调用tick_setup_oneshot函数(代码位于kernel/time/tick-oneshot.c中):

void tick_setup_oneshot(struct clock_event_device *newdev,
			void (*handler)(struct clock_event_device *),
			ktime_t next_event)
{
        /* 设置对应定时事件设备的event_handler */
	newdev->event_handler = handler;
        /* 无论如何将对应定时事件设备的状态切换成单次触发 */
	clockevents_switch_state(newdev, CLOCK_EVT_STATE_ONESHOT);
        /* 用到期时间对定时事件设备进行编程 */
	clockevents_program_event(newdev, next_event, true);
}

可以看到,如果Tick设备是单次触发模式的,则对应的底层定时事件设备一定也要是单次触发模式的。会不会觉得有点奇怪,Tick设备应该要周期触发,一定是工作在TICKDEV_MODE_PERIODIC模式下的,为什么还会设置成TICKDEV_MODE_ONESHOT。后面会说到,当切换到高分辨率时钟模式后,Tick设备就会被“架空”了。但是,不管处在什么模式下,不管Tick层是否还会负责处理中断回调函数,新设备的替换都是在Tick层做的。在替换之前,新设备的状态肯定是处于CLOCK_EVT_STATE_SHUTDOWN状态。因此,在这里首先要将新设备的状态切换到CLOCK_EVT_STATE_ONESHOT状态,然后还需要对其进行编程,让其在到期时间时触发中断,当然这个时候的中断回调函数就不是tick_handle_periodic了,而是在高精度定时器层(hrtimer)中定义的hrtimer_interrupt函数。

2)Tick设备的到期处理

如果Tick设备工作在TICKDEV_MODE_ONESHOT模式,那么到期处理函数实际上已经不在Tick层了,因此这里我们只分析TICKDEV_MODE_PERIODIC模式下的处理函数。

通过前面分析可知,当工作在周期触发模式下时,对应定时事件设备的event_handler会被设置成函数tick_handle_periodic,当定时事件设备触发中断后,其中断处理程序会调用event_handler,也就是会调用tick_handle_periodic函数:

void tick_handle_periodic(struct clock_event_device *dev)
{
        /* 获得当前CPU的id */
	int cpu = smp_processor_id();
        /* 获得下一次到期的时间 */
	ktime_t next = dev->next_event;

	tick_periodic(cpu);

#if defined(CONFIG_HIGH_RES_TIMERS) || defined(CONFIG_NO_HZ_COMMON)
	/* 有可能已经被切换到高分辨率定时器,如果是这样,则直接返回。 */
	if (dev->event_handler != tick_handle_periodic)
		return;
#endif

        /* 如果对应的定时事件设备不是单次触发的则不需要再编程 */
	if (!clockevent_state_oneshot(dev))
		return;
        /* 对定时事件设备重新编程 */
	for (;;) {
		next = ktime_add(next, tick_period);

		if (!clockevents_program_event(dev, next, false))
			return;

		if (timekeeping_valid_for_hres())
			tick_periodic(cpu);
	}
}

该函数首先调用tick_periodic函数处理到期的事件,然后如果定时事件设备是单次触发的话,还需要对其进行重编程。循环中每次递加tick_period周期时间,直到成功为止。

static void tick_periodic(int cpu)
{
        /* 当前Tick设备是更新系统jiffies的主设备 */
	if (tick_do_timer_cpu == cpu) {
		write_seqlock(&jiffies_lock);

		/* 更新全局的下一次Tick到期时间 */
		tick_next_period = ktime_add(tick_next_period, tick_period);

                /* 将系统jiffies加1 */
		do_timer(1);
		write_sequnlock(&jiffies_lock);
                /* 更新墙上时间 */
		update_wall_time();
	}

        /* 通知定时器层 */
	update_process_times(user_mode(get_irq_regs()));
        /* 更新CPU负载 */
	profile_tick(CPU_PROFILING);
}

如果当前的Tick设备被选中做了更新系统jiffies的主设备,则更新全局变量tick_next_period,将系统jiffies加1,更新墙上时间。无论是不是主设备,最后都要调用函数update_process_times,通知上面的定时器(Timer)层。

全局变量tick_next_period也是要通过顺序锁jiffies_lock进行读写保护的。这是因为对于对称多处理(SMP)系统来说,系统里面有多个CPU(或多线程),每一个CPU都要注册Tick设备。在系统初始化时,主CPU起来之后,其上的Tick设备会被选做主设备来更新,每次Tick来时,都会更新全局变量tick_next_period。这时,如果有另一个CPU被激活了,其最终都会调用前面说的tick_setup_periodic函数,读取tick_next_period全局变量来设置自己CPU上的Tick设备。虽然在调用tick_setup_periodic函数时,已经获得了自旋锁并关闭了本地中断,但自旋锁clockevent_devices只保护了ClockEvents层的全局变量,并没有保护Tick层的tick_next_period全局变量(在tick_handle_periodic函数中并没有尝试获得clockevent_devices自旋锁),关闭本地中断只对本CPU有用,不能阻止前面已经选做主设备的Tick设备产生中断。因此,此时在tick_next_period这个全局变量上,两个CPU上运行的程序会处于竞态,所以要用顺序锁对其进行保护。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页