Linux时间子系统之Tick模拟层(Tick Sched)

在分析高分辨率定时器的时候曾经提到过,一旦切换到高精度模式后,原来的Tick层就失去作用了,高分辨率定时器层将“接管”对底层定时事件设备的控制。这时,也就意味着,系统中原有的Tick将不复存在了。但是,这个Tick其实是非常重要的,系统jiffies要靠它更新,用户看到的墙上时间也需要在Tick到来的时候定期更新,进程的调度也需要用它来计算时间片。

Tick模拟层的主要目的就是当原来的Tick层不再工作了之后,用一些特殊的方式来模拟出一个系统Tick,保证系统许多原有的功能还能够正常的运行。同时,它还要处理所谓动态时钟的情况,也就是当系统中某个CPU空闲的时候,停掉该CPU上的Tick,从而达到省电的目的。

Tick模拟层主要使用tick_sched结构体来管理:

struct tick_sched {
	struct hrtimer			sched_timer;
	unsigned long			check_clocks;
	enum tick_nohz_mode		nohz_mode;

	unsigned int			inidle		: 1;
	unsigned int			tick_stopped	: 1;
	unsigned int			idle_active	: 1;
	unsigned int			do_timer_last	: 1;
	unsigned int			got_idle_tick	: 1;

	ktime_t				last_tick;
	ktime_t				next_tick;
	unsigned long			idle_jiffies;
	unsigned long			idle_calls;
	unsigned long			idle_sleeps;
	ktime_t				idle_entrytime;
	ktime_t				idle_waketime;
	ktime_t				idle_exittime;
	ktime_t				idle_sleeptime;
	ktime_t				iowait_sleeptime;
	unsigned long			last_jiffies;
	u64				timer_expires;
	u64				timer_expires_base;
	u64				next_timer;
	ktime_t				idle_expires;
	atomic_t			tick_dep_mask;
};
  • sched_timer:在高精度模式下,用来模拟系统Tick的一个高分辨率定时器。
  • check_clocks:该字段用来实现定时事件层和时钟源层向Tick模拟层的通知上报机制。当该字段的第0位被置位是,意味着有一个新的定时事件设备或者一个新的时钟源设备被添加到系统中了。
  • nohz_mode:表明当前动态时钟的工作模式,目前共有三种模式:NOHZ_MODE_INACTIVE表示还没有激活;NOHZ_MODE_LOWRES表示当前处于低精度动态时钟模式;NOHZ_MODE_HIGHRES表示当前处于高精度动态时钟模式。
enum tick_nohz_mode {
	NOHZ_MODE_INACTIVE,
	NOHZ_MODE_LOWRES,
	NOHZ_MODE_HIGHRES,
};
  • inidle:表示当前CPU处于空闲状态。
  • tick_stopped:表示当前CPU上的Tick已经被停止了。
  • idle_active:表示当前CPU确实是处于空闲状态。一般情况下inidle的值和idle_active的值应该是一样的,但有可能在CPU处于空闲状态时,收到一个中断处理请求,这时候当前CPU就会临时退出空闲状态,将idle_active置0,但inidle任然是1。
  • do_timer_last:表示在停止Tick之前,该CPU是否是负责更新系统jiffies的。
  • got_idle_tick:表示是否在空闲状态下仍收到了Tick。
  • last_tick:记录上一次Tick到来的时间。
  • next_tick:记录下一次Tick到来的时间。
  • idle_jiffies:在进入空闲状态时,系统jiffies的值。
  • idle_calls:记录一共进入了多少次空闲状态。
  • idle_sleeps:记录了进入空闲状态后,一共停了多少次Tick。
  • idle_entrytime:记录了进入空闲状态的时间。
  • idle_waketime:记录了在空闲状态下收到并处理中断的时间。
  • idle_exittime:记录上一次退出空闲状态的时间。
  • idle_sleeptime:记录了在空闲且Tick停止状态下,并且没有任何IO请求在等待的情况下,一共持续了多长时间。
  • iowait_sleeptime:记录了在空闲且Tick停止状态下,同时还有IO请求在等待的情况下,一共持续了多长时间。
  • last_jiffies:记录了在停止Tick前,系统jiffies的值。
  • timer_expires:记录了在停止Tick的情况下,下一个预期的定时器到期时间。
  • timer_expires_base:记录了在停止Tick的情况下,定时器到期的基准时间,其实就是记录了在停止Tick的时候,上一次Tick到来的时间,也就是上一次更新系统jiffies的时间。
  • next_timer:系统中所有定时器中最近要到期的到期时间。
  • idle_expires:记录了在空闲且Tick停止后,下一个到期定时器的到期时间。
  • tick_dep_mask:记录了系统中还有哪些功能需要Tick,主要用于将CONFIG_NO_HZ_FULL编译选项打开的情况下。

tick_sched结构体是一个Per CPU的变量,定义如下:

static DEFINE_PER_CPU(struct tick_sched, tick_cpu_sched);

struct tick_sched *tick_get_tick_sched(int cpu)
{
	return &per_cpu(tick_cpu_sched, cpu);
}

下面分场景介绍一下Tick模拟层的工作过程。

1)切换到高精度动态时钟模式(NOHZ_MODE_HIGHRES)

在分析高分辨率定时器层的时候,我们在分析低精度模式切换到高精度模式场景的hrtimer_switch_to_hres函数时提到过其会调用tick_setup_sched_timer函数设置Tick模拟层:

void tick_setup_sched_timer(void)
{
        /* 获得属于当前CPU的tick_sched结构体 */
	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
        /* 获得当前时间 */
	ktime_t now = ktime_get();

	/* 初始化高分辨率定时器sched_timer */
	hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS_HARD);
        /* 将定时器的到期函数设置成tick_sched_timer */
	ts->sched_timer.function = tick_sched_timer;

	/* 设置定时器的到期时间为系统中上一次jiffy更新的时间 */
	hrtimer_set_expires(&ts->sched_timer, tick_init_jiffy_update());

	/* 是否需要添加偏移避免不必要的竞争 */
	if (sched_skew_tick) {
                /* 除以2 */
		u64 offset = ktime_to_ns(tick_period) >> 1;
                /* 除以系统中所有CPU的数目 */
		do_div(offset, num_possible_cpus());
                /* 每个CPU按照ID号计算偏移量 */
		offset *= smp_processor_id();
                /* 添加偏移到定时器的到期时间上 */
		hrtimer_add_expires_ns(&ts->sched_timer, offset);
	}

        /* 更新定时器到期时间到下一个Tick到来的时间 */
	hrtimer_forward(&ts->sched_timer, now, tick_period);
        /* 激活定时器 */
	hrtimer_start_expires(&ts->sched_timer, HRTIMER_MODE_ABS_PINNED_HARD);
        /* 将模式设置成NOHZ_MODE_HIGHRES */
	tick_nohz_activate(ts, NOHZ_MODE_HIGHRES);
}

由于已经没有Tick了,而这时候高分辨率定时器层是处在高精度模式的,那么想制造一个Tick其实很简单,只需要向高分辨率定时器层添加一个定时间隔是一个Tick的高分辨率定时器模拟一下以前的系统Tick就好了。函数首先初始化了在本CPU结构体变量tick_sched中的sched_timer高分辨率定时器。可以看到,它是用的单调时间,到期时间是绝对值,并且是一个“硬”定时器,定时器的到期函数被设置成了tick_sched_timer。

tick_init_jiffy_update函数用来获得上一次jiffy更新的时间:

static ktime_t tick_init_jiffy_update(void)
{
	ktime_t period;

	write_seqlock(&jiffies_lock);
	/* 是否已经被初始化过 */
	if (last_jiffies_update == 0)
		last_jiffies_update = tick_next_period;
	period = last_jiffies_update;
	write_sequnlock(&jiffies_lock);
	return period;
}

last_jiffies_update是一个全局变量,用来记录上一次jiffy更新时的时间:

static ktime_t last_jiffies_update;

如果last_jiffies_update的值为0,表明还没有被初始化过,这时候就用全局变量tick_next_period对其赋值。tick_next_period是在Tick层定义的,表示下一次Tick的到期时间。

得到了上一次jiffy更新时间后,调用hrtimer_set_expires函数,将sched_timer定时器的“软”和“硬”到期时间都设置成这个时间:

static inline void hrtimer_set_expires(struct hrtimer *timer, ktime_t time)
{
	timer->node.expires = time;
	timer->_softexpires = time;
}

接下来会判断变量sched_skew_tick,看是否需要根据每个CPU的ID号添加一个微小偏移,尽量避免不必要的竞争:

static int sched_skew_tick;

static int __init skew_tick(char *str)
{
	get_option(&str, &sched_skew_tick);

	return 0;
}
early_param("skew_tick", skew_tick);

sched_skew_tick是一个全局变量,默认初始化成0,可以通过内核参数对其进行设置。

hrtimer_forward函数按照给定的当前时间和一个周期经过的时间来更新定时器的到期时间:

u64 hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval)
{
	u64 orun = 1;
	ktime_t delta;

        /* 计算当前时间和定时器到期时间之间的差值 */
	delta = ktime_sub(now, hrtimer_get_expires(timer));

        /* 如果差值小于0则直接退出 */
	if (delta < 0)
		return 0;

        /* 定时器必须没有被激活 */
	if (WARN_ON(timer->state & HRTIMER_STATE_ENQUEUED))
		return 0;

        /* 定时周期不能小于高分辨率定时器层当前最高的分辨率 */
	if (interval < hrtimer_resolution)
		interval = hrtimer_resolution;

        /* 如果差值超过了一个周期 */
	if (unlikely(delta >= interval)) {
		s64 incr = ktime_to_ns(interval);
                /* 计算差了几个周期 */
		orun = ktime_divns(delta, incr);
                /* 将差的几个周期添加到定时器到期时间上 */
		hrtimer_add_expires_ns(timer, incr * orun);
                /* 如果到期时间已经超过了当前时间则退出 */
		if (hrtimer_get_expires_tv64(timer) > now)
			return orun;
		/* 下面跳出循环后还要加一个周期 */
		orun++;
	}
        /* 将定时器的到期时间加上一个周期指向下一个Tick到来的时间 */
	hrtimer_add_expires(timer, interval);

	return orun;
}
EXPORT_SYMBOL_GPL(hrtimer_forward);

函数的返回值表示要添加几个周期。函数先计算出当前时间和定时器到期时间之间的差值,如果这个差值超过了一个周期,那么需要用差值除以周期时间获得差了多少个周期,然后将其加上。如果差值小于一个周期,那么就直接将到期时间加上一个周期的时间就行了。

将定时器到期时间更新到下一个Tick应该到来的时间后,tick_setup_sched_timer函数调用hrtimer_start_expires函数,正式激活该定时器:

static inline void hrtimer_start_expires(struct hrtimer *timer,
					 enum hrtimer_mode mode)
{
	u64 delta;
	ktime_t soft, hard;
        /* 获得“软”到期时间 */
	soft = hrtimer_get_softexpires(timer);
        /* 获得“硬”到期时间 */
	hard = hrtimer_get_expires(timer);
        /* 计算两者之间的时间差值 */
	delta = ktime_to_ns(ktime_sub(hard, soft));
        /* 激活定时器 */
	hrtimer_start_range_ns(timer, soft, delta, mode);
}

该函数分别获得高分辨率定时器的“软”和“硬”到期时间,计算它们的差值,然后最终调用hrtimer_start_range_ns函数激活它。hrtimer_start_range_ns函数在高分辨率定时器层的定时器激活场景下分析过。Tick模拟层的sched_timer高分辨率定时器的“软”到期时间和“硬”到期时间是一样的。

tick_setup_sched_timer函数的最后调用tick_nohz_activate函数,试着将Tick模拟层切换到NOHZ_MODE_HIGHRES模式:

static inline void tick_nohz_activate(struct tick_sched *ts, int mode)
{
        /* 如果没有启用NO_HZ模式则直接退出 */
	if (!tick_nohz_enabled)
		return;
        /* 设置tick_sched结构体的模式 */
	ts->nohz_mode = mode;
	/* 判断或者设置全局变量tick_nohz_active的最低位 */
	if (!test_and_set_bit(0, &tick_nohz_active))
                /* 通知(低分辨率)定时器层切换到NO_HZ模式 */
		timers_update_nohz();
}

tick_nohz_enabled和tick_nohz_active都是全局变量,只有打开了CONFIG_NO_HZ_COMMON内核编译选项的时候才会定义:

#ifdef CONFIG_NO_HZ_COMMON
......
bool tick_nohz_enabled __read_mostly  = true;
unsigned long tick_nohz_active  __read_mostly;
......
static int __init setup_tick_nohz(char *str)
{
	return (kstrtobool(str, &tick_nohz_enabled) == 0);
}

__setup("nohz=", setup_tick_nohz);
......
#endif /* CONFIG_NO_HZ_COMMON */

tick_nohz_enabled的默认值是true,表明在编译时打开CONFIG_NO_HZ_COMMON之后,默认情况下是会切换到NO_HZ模式的。但是,可以在启动的时候,通过设置内核参数“nohz”将其关闭。如果没有在编译时打开CONFIG_NO_HZ_COMMON选项,或者是在启动的时候人为关闭了,那么就不能切换到NO_HZ模式,也就是不能使用所谓的动态时钟的功能,系统中的Tick会一直存在。

如果可以用动态时钟,那么下面会将tick_sched结构体的模式设置成NOHZ_MODE_HIGHRES。接着会检查全局变量tick_nohz_active的最低位,如果没有被设置过,则将其置位,然后调用timers_update_nohz函数通知(低分辨率)定时器层切换到NO_HZ模式。这样做可以保证只会通知一次。

接收到通知后,(低分辨率)定时器层会将全局变量timers_nohz_active和timers_migration_enabled都设置成真。

2)高精度动态时钟模式下Tick到来的处理

前面看到,当切换成高精度模式后,会添加一个高分辨率定时器来模拟系统Tick。而这个高分辨率定时器的到期处理函数是tick_sched_timer:

static enum hrtimer_restart tick_sched_timer(struct hrtimer *timer)
{
        /* 获得包含该定时器的tick_sched结构体 */
	struct tick_sched *ts =
		container_of(timer, struct tick_sched, sched_timer);
        /* 获得指向中断上下文中所有寄存器变量的指针 */
	struct pt_regs *regs = get_irq_regs();
        /* 获得当前时间 */
	ktime_t now = ktime_get();

	tick_sched_do_timer(ts, now);

	/* 是否在中断上下文中 */
	if (regs)
		tick_sched_handle(ts, regs);
	else
		ts->next_tick = 0;

	/* 如果Tick被停掉了就没必要再激活该模拟Tick的定时器了 */
	if (unlikely(ts->tick_stopped))
		return HRTIMER_NORESTART;

        /* 更新定时器到期时间到下一个Tick到来的时间 */
	hrtimer_forward(timer, now, tick_period);

        /* 返回HRTIMER_RESTART表示还需要重新再次激活该定时器 */
	return HRTIMER_RESTART;
}

该到期处理函数的参数是指向那个模拟Tick的高分辨率定时器的指针,先通过它来获得包含它的外围tick_sched结构体指针。get_irq_regs函数获得指向中断上下文中所有寄存器栈的指针,如果该函数不是在中断上下文中调用的,则返回的是空指针。

tick_sched_do_timer主要的职责是根据当前时间来更新系统jiffies:

static void tick_sched_do_timer(struct tick_sched *ts, ktime_t now)
{
        /* 获得当前CPU的ID号 */
	int cpu = smp_processor_id();

#ifdef CONFIG_NO_HZ_COMMON
	/* 如果还没有选中由哪个CPU来更新系统jiffies */
	if (unlikely(tick_do_timer_cpu == TICK_DO_TIMER_NONE)) {
#ifdef CONFIG_NO_HZ_FULL
		......
#endif
                /* 就选择当前CPU来更新系统jiffies */
		tick_do_timer_cpu = cpu;
	}
#endif

	/* 如果是由本CPU负责更新系统jiffies */
	if (tick_do_timer_cpu == cpu)
                /* 根据当前时间来更新系统jiffies */
		tick_do_update_jiffies64(now);

	if (ts->inidle)
		ts->got_idle_tick = 1;
}

系统中第一个执行tick_sched_do_timer函数的CPU会被挑选出来,调用tick_do_update_jiffies64函数,负责更新系统jiffies:

static void tick_do_update_jiffies64(ktime_t now)
{
	unsigned long ticks = 0;
	ktime_t delta;

	/* 先在没有加锁的情况下计算当前时间和上次更新时间之间的差值 */
	delta = ktime_sub(now, READ_ONCE(last_jiffies_update));
        /* 如果差值小于一个周期则直接退出 */
	if (delta < tick_period)
		return;

	/* 获得jiffies_lock序列所 */
	write_seqlock(&jiffies_lock);

        /* 在获得锁的情况下再一次计算当前时间和上次更新时间之间的差值 */
	delta = ktime_sub(now, last_jiffies_update);
	if (delta >= tick_period) {
                /* 将差值先减去一个周期 */
		delta = ktime_sub(delta, tick_period);
		/* 对应将last_jiffies_update加上一个周期 */
		WRITE_ONCE(last_jiffies_update,
			   ktime_add(last_jiffies_update, tick_period));

		/* 如果减去了一个周期后的差值还是大于一个周期 */
		if (unlikely(delta >= tick_period)) {
			s64 incr = ktime_to_ns(tick_period);

                        /* 计算剩下的差值还包含多少个周期 */
			ticks = ktime_divns(delta, incr);

			/* 对应加上对应周期数 */
			WRITE_ONCE(last_jiffies_update,
				   ktime_add_ns(last_jiffies_update,
						incr * ticks));
		}
                /* 将周期累加到系统jiffies上 */
		do_timer(++ticks);

		/* 更新全局变量tick_next_period */
		tick_next_period = ktime_add(last_jiffies_update, tick_period);
	} else {
		write_sequnlock(&jiffies_lock);
		return;
	}
        /* 释放jiffies_lock序列锁 */
	write_sequnlock(&jiffies_lock);
        /* 更新墙上时间 */
	update_wall_time();
}

为了加快速度,tick_do_update_jiffies64采用了一些实现方面的小技巧。首先是在没加锁的时候就计算当前时间和上次更新时间之间的差值,如果差值小于一个周期就直接不用更新了,可以过滤掉一些不必要的加锁。由于绝大多数差值都是在一个周期到两个周期之间,因此先分开处理单独的一个周期。如果处理完后发现差值不止一个周期,再通过整数除法计算还剩下多少个周期,然后一起处理。

在更新完系统jiffies后,如果是在中断上下文中,tick_sched_timer函数会接着调用tick_sched_handle函数:

static void tick_sched_handle(struct tick_sched *ts, struct pt_regs *regs)
{
#ifdef CONFIG_NO_HZ_COMMON
	/* 如果当前Tick已经被停止了 */
	if (ts->tick_stopped) {
		touch_softlockup_watchdog_sched();
                /* 如果当前运行的进程是idle */
		if (is_idle_task(current))
                        /* 将idle_jiffies加1算上这次的 */
			ts->idle_jiffies++;
		/* 将下一次Tick到来时间设置成0 */
		ts->next_tick = 0;
	}
#endif
        /* 通知(低分辨率)定时器层Tick已到来 */
	update_process_times(user_mode(regs));
	profile_tick(CPU_PROFILING);
}

tick_sched_handle函数主要的功能是通知(低分辨率)定时器层Tick已经到来了,可以开始处理定时器了。一旦切换到高精度模式,(低分辨率)定时器层实际是由Tick模拟层来触发的。

最后,到期处理函数tick_sched_timer的返回值是HRTIMER_RESTART,表示还需要重新再次激活该定时器,剩下的对定时时间设备重新编程的工作将由高分辨率定时器层自动完成。

通过以上分析可以看到,tick_sched_timer函数基本上就是完成了原来Tick层周期处理函数tick_periodic要完成的工作。

3)切换到低精度动态时钟模式(NOHZ_MODE_LOWRES)

在分析高分辨率定时器层低精度模式切换到高精度模式场景时,可以看到在调用tick_check_oneshot_change函数判断是否可以切换时,哪怕底层的定时事件设备和时钟源设备全部满足要求,但是内核编译的时候没有打开CONFIG_HIGH_RES_TIMERS或者在启动的时候内核参数highres显式设置成关闭了,那会调用tick_nohz_switch_to_nohz函数,将Tick模拟层设置成低精度动态时钟模式:

static void tick_nohz_switch_to_nohz(void)
{
        /* 获得当前CPU对应的tick_sched结构体 */
	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
	ktime_t next;

        /* 如果不支持NO_HZ模式则直接退出 */
	if (!tick_nohz_enabled)
		return;

        /* 切换到单次触发模式并将到期处理函数设置成tick_nohz_handler */
	if (tick_switch_to_oneshot(tick_nohz_handler))
		return;

	/* 初始化高分辨率定时器sched_timer */
	hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS_HARD);
	/* 获得系统中上一次jiffy更新的时间 */
	next = tick_init_jiffy_update();

        /* 设置定时器到期时间为上一次jiffy更新的时间 */
	hrtimer_set_expires(&ts->sched_timer, next);
        /* 更新定时器到期时间到下一个Tick到来的时间 */
	hrtimer_forward_now(&ts->sched_timer, tick_period);
        /* 直接对定时事件设备进行编程 */
	tick_program_event(hrtimer_get_expires(&ts->sched_timer), 1);
        /* 将模式设置成NOHZ_MODE_LOWRES */
	tick_nohz_activate(ts, NOHZ_MODE_LOWRES);
}

这种模式不多见,但确实是存在的。在这种模式下,定时事件设备的到期处理函数被设置成了tick_nohz_handler,(低分辨率)定时器层和高分辨率定时器层都要靠其驱动。这时,高分辨率定时器层其实已经不会自己工作了,所有Tick都由Tick模拟层通过直接对定时事件设备进行编程来实现。代码中还要初始化一个高分辨率定时器其实只是为了后面计算到期事件比较方便,可以重用已有的代码,并不会将这个定时器激活。

4)低精度动态时钟模式下Tick到来的处理

前面提到,在切换到低精度动态时钟模式下,定时事件设备的到期处理函数被设置成了tick_nohz_handler:

static void tick_nohz_handler(struct clock_event_device *dev)
{
	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
	struct pt_regs *regs = get_irq_regs();
	ktime_t now = ktime_get();

	dev->next_event = KTIME_MAX;

	tick_sched_do_timer(ts, now);
	tick_sched_handle(ts, regs);

	/* 如果此时Tick已经停止了那就没必要再编程了直接退出  */
	if (unlikely(ts->tick_stopped))
		return;

        /* 更新定时器到期时间到下一个Tick到来的时间 */
	hrtimer_forward(&ts->sched_timer, now, tick_period);
        /* 直接对定时事件设备进行编程 */   
	tick_program_event(hrtimer_get_expires(&ts->sched_timer), 1);
}

可以看到,其实和高精度动态时钟模式下Tick到来的处理函数tick_sched_timer实现的功能非常的相似。只不过,函数的最后需要直接用下一次Tick到来时间直接对定时事件设备进行编程。

5)停掉Tick

如果当前CPU进入空闲状态,Linux系统先会调用tick_nohz_idle_enter函数,通知Tick模拟层进入空闲状态,接着会调用tick_nohz_idle_stop_tick函数,正式停掉当前CPU上的Tick。

先来看看函数tick_nohz_idle_enter的实现:

void tick_nohz_idle_enter(void)
{
	struct tick_sched *ts;

	lockdep_assert_irqs_enabled();

        /* 关本地中断 */
	local_irq_disable();

        /* 获得当前CPU的tick_sched结构体 */
	ts = this_cpu_ptr(&tick_cpu_sched);

	WARN_ON_ONCE(ts->timer_expires_base);

	ts->inidle = 1;
	tick_nohz_start_idle(ts);

        /* 开本地中断 */
	local_irq_enable();
}

这个函数主要就是找到当前CPU的tick_sched结构体,将表示当前处于空闲状态的inidle字段置1,然后调用了tick_nohz_start_idle函数:

static void tick_nohz_start_idle(struct tick_sched *ts)
{
	ts->idle_entrytime = ktime_get();
	ts->idle_active = 1;
	sched_clock_idle_sleep_event();
}

该函数也主要是完成一些字段设置的工作,先将表示进入空闲状态时间的idle_entrytime字段设置为当前时间,然后将表示当前CPU确实是处于空闲状态的字段idle_active也置1。到此,准备工作就完成了,接着会调用tick_nohz_idle_stop_tick函数开始停Tick:

void tick_nohz_idle_stop_tick(void)
{
	__tick_nohz_idle_stop_tick(this_cpu_ptr(&tick_cpu_sched));
}

该函数接着调用了__tick_nohz_idle_stop_tick函数,传入属于本CPU的tick_sched结构体:

static void __tick_nohz_idle_stop_tick(struct tick_sched *ts)
{
	ktime_t expires;
	int cpu = smp_processor_id();

	/* 如果timer_expires_base不为0表示前面已经取过了下个定时器的到期时间 */
	if (ts->timer_expires_base)
		expires = ts->timer_expires;
	else if (can_stop_idle_tick(cpu, ts))
                /* 取系统里面所有定时器的下一个最近到期时间 */
		expires = tick_nohz_next_event(ts, cpu);
	else
		return;

	ts->idle_calls++;

        /* 如果定时器的到期时间大于0 */
	if (expires > 0LL) {
		int was_stopped = ts->tick_stopped;

                /* 停掉系统的Tick */
		tick_nohz_stop_tick(ts, cpu);

		ts->idle_sleeps++;
		ts->idle_expires = expires;

		if (!was_stopped && ts->tick_stopped) {
			ts->idle_jiffies = ts->last_jiffies;
			nohz_balance_enter_idle(cpu);
		}
	} else {
		tick_nohz_retain_tick(ts);
	}
}

__tick_nohz_idle_stop_tick函数会调用can_stop_idle_tick函数判断现在是否可以真的停掉Tick:

static bool can_stop_idle_tick(int cpu, struct tick_sched *ts)
{
	/* 如果当前CPU已经处于离线状态 */
	if (unlikely(!cpu_online(cpu))) {
                /* 如果当前CPU就是负责更新系统jiffies的 */
		if (cpu == tick_do_timer_cpu)
                        /* 让出本CPU更新系统jiffies的权利 */
			tick_do_timer_cpu = TICK_DO_TIMER_NONE;
		/* 将next_tick赋值为0 */
		ts->next_tick = 0;
		return false;
	}

        /* 是否当前任然处在NOHZ_MODE_INACTIVE模式 */
	if (unlikely(ts->nohz_mode == NOHZ_MODE_INACTIVE))
		return false;

        /* 是否有其它进程等待被调度执行 */
	if (need_resched())
		return false;

        /* 当前CPU上是否有需要处理的软中断 */
	if (unlikely(local_softirq_pending())) {
		static int ratelimit;

		if (ratelimit < 10 &&
		    (local_softirq_pending() & SOFTIRQ_STOP_IDLE_MASK)) {
			pr_warn("NOHZ: local_softirq_pending %02x\n",
				(unsigned int) local_softirq_pending());
			ratelimit++;
		}
		return false;
	}

	......

	return true;
}

所以,如果当前仍然处在NOHZ_MODE_INACTIVE未激活模式下,或者目前还有其它进程等待被调度执行,或者当前CPU上有需要处理的软中断,则不需要停止当前的Tick。

虽然关掉了当前CPU的Tick,但是并不能停止当前CPU上的(低分辨率)定时器和高分辨率定时器,如果这都停了,那所有定时器都将会超时,这个是不能接受的。所以,很自然的想到,马上需要获得系统中所有定时器的最近到期的时间。不过,需要注意的是,目前系统中其实有两种类型的定时器,所以必须要分别从(低分辨率)定时器层和高分辨率定时器层获得它们各自的最近要到期的定时器的时间,然后再比较两者哪个更早。这些是在tick_nohz_next_event函数中实现的:

static ktime_t tick_nohz_next_event(struct tick_sched *ts, int cpu)
{
	u64 basemono, next_tick, next_tmr, next_rcu, delta, expires;
	unsigned long basejiff;
	unsigned int seq;

	/* 读取系统jiffies和上次更新时候的jiffies */
	do {
		seq = read_seqbegin(&jiffies_lock);
		basemono = last_jiffies_update;
		basejiff = jiffies;
	} while (read_seqretry(&jiffies_lock, seq));
        /* 记录当前系统jiffies的值 */
	ts->last_jiffies = basejiff;
        /* 更新定时器到期基准时间 */
	ts->timer_expires_base = basemono;

	/* 某些情况下还需要保留系统Tick */
	if (rcu_needs_cpu(basemono, &next_rcu) || arch_needs_cpu() ||
	    irq_work_needs_cpu() || local_timer_softirq_pending()) {
		next_tick = basemono + TICK_NSEC;
	} else {
		/* 获得系统中所有定时器中最近要到期的到期时间 */
		next_tmr = get_next_timer_interrupt(basejiff, basemono);
		ts->next_timer = next_tmr;
		/* 还需要考虑最近的RCU事件 */
		next_tick = next_rcu < next_tmr ? next_rcu : next_tmr;
	}

	/* 计算下一个到期时间和上次Tick到来时间之间的差值 */
	delta = next_tick - basemono;
        /* 如果差值小于一个Tick周期 */
	if (delta <= (u64)TICK_NSEC) {
		/* 通知(低分辨率)定时器层还没有进入空闲状态 */
		timer_clear_idle();
		/* 如果此时Tick还没停止 */
		if (!ts->tick_stopped) {
                        /* 返回0任然保留Tick */
			ts->timer_expires = 0;
			goto out;
		}
	}

	/* 如果当前CPU是负责更新系统jiffies的,则其最长睡眠时间不能超过时钟源设备记录最长跨度的时间 */
	delta = timekeeping_max_deferment();
	if (cpu != tick_do_timer_cpu &&
	    (tick_do_timer_cpu != TICK_DO_TIMER_NONE || !ts->do_timer_last))
		delta = KTIME_MAX;

	/* 计算到期时间 */
	if (delta < (KTIME_MAX - basemono))
		expires = basemono + delta;
	else
		expires = KTIME_MAX;

	ts->timer_expires = min_t(u64, expires, next_tick);

out:
	return ts->timer_expires;
}

如果tick_nohz_next_event函数返回0,则表示任然需要保留当前CPU上的Tick;而如果返回值大于0,则表示可以停止Tick了,但必须在这个返回值指定的时间后触发事件处理。不是所有情况下都需要停止Tick的,如果真的需要保留,那么就将下一次Tick的到来时间设置成本来Tick到来的时间。如果确实不需要保留Tick了,则先要获得系统中所有定时器中最近要到期的到期时间,如果这个到期时间还小于下一个Tick到来的时间,并且当前Tick还没停止的话,那还是选择保留Tick。最后,如果当前的CPU负责更新系统jiffies的话,那么对睡眠时间还有一个限制,否则想停多长时间的Tick都可以。在分析时钟源层代码的时候,曾经提到过有一个max_idle_ns值,表示最大允许的空闲间隔时间,如果停止Tick的时间超过了这个最大时间,那么在读取时钟源设备周期数并将其转换成纳秒数的时候有可能会产生溢出。

tick_nohz_next_event函数进一步通过调用get_next_timer_interrupt(代码位于kernel/time/timer.c中)获得系统中所有定时器中最近要到期的到期时间:

u64 get_next_timer_interrupt(unsigned long basej, u64 basem)
{
	struct timer_base *base = this_cpu_ptr(&timer_bases[BASE_STD]);
	u64 expires = KTIME_MAX;
	unsigned long nextevt;
	bool is_max_delta;

	/* 如果当前CPU已经离线了则返回最大值KTIME_MAX */
	if (cpu_is_offline(smp_processor_id()))
		return expires;

	raw_spin_lock(&base->lock);
        /* 搜寻timer_base下最早到期定时器的时间 */
	nextevt = __next_timer_interrupt(base);
	is_max_delta = (nextevt == base->clk + NEXT_TIMER_MAX_DELTA);
	base->next_expiry = nextevt;
	/* 更新clk的值 */
	if (time_after(basej, base->clk)) {
		if (time_after(nextevt, basej))
			base->clk = basej;
		else if (time_after(nextevt, base->clk))
			base->clk = nextevt;
	}

	if (time_before_eq(nextevt, basej)) {
		expires = basem;
		base->is_idle = false;
	} else {
		if (!is_max_delta)
			expires = basem + (u64)(nextevt - basej) * TICK_NSEC;
		/* 如果要休眠的时间大于一个Tick的周期 */
		if ((expires - basem) > TICK_NSEC) {
			base->must_forward_clk = true;
			base->is_idle = true;
		}
	}
	raw_spin_unlock(&base->lock);

	return cmp_next_hrtimer_event(basem, expires);
}

该函数的第一个参数basej是系统目前的jiffies数,单位是Tick,而第二个参数basem是对应的单调时间,单位是纳秒。

在函数的最后调用cmp_next_hrtimer_event函数,获得高分辨率定时器层即将到期定时器的到期时间并和(低分辨率)定时器层已经找到的即将到期定时器的到期时间进行比较,返回两个中较早的那个时间。

static u64 cmp_next_hrtimer_event(u64 basem, u64 expires)
{
	u64 nextevt = hrtimer_get_next_event();

	/* 如果最近高分辨率定时器的到期时间大于等于传入的到期时间则返回传入的 */
	if (expires <= nextevt)
		return expires;

	/* 如果最近到期高分辨率定时器已经过期了则返回传入的基准时间 */
	if (nextevt <= basem)
		return basem;

	/* 返回到期时间后下一次Tick周期到来的时间 */
	return DIV_ROUND_UP_ULL(nextevt, TICK_NSEC) * TICK_NSEC;
}

hrtimer_get_next_event函数负责从高分辨率定时器层获得最早将要到期的定时器的到期时间(代码位于kernel/time/hrtimer.c中):

u64 hrtimer_get_next_event(void)
{
	struct hrtimer_cpu_base *cpu_base = this_cpu_ptr(&hrtimer_bases);
	u64 expires = KTIME_MAX;
	unsigned long flags;

	raw_spin_lock_irqsave(&cpu_base->lock, flags);

	if (!__hrtimer_hres_active(cpu_base))
		expires = __hrtimer_get_next_event(cpu_base, HRTIMER_ACTIVE_ALL);

	raw_spin_unlock_irqrestore(&cpu_base->lock, flags);

	return expires;
}

__hrtimer_get_next_event函数在高分辨率定时器层已经分析过了,用来在所有激活的定时器中查找最近即将到期定时器的到期时间。可以看出来,在高分辨率定时器层还没有切换到高精度模式前,该函数会返回即将到期定时器的到期时间,而一旦已经完成了切换,该函数将返回KTIME_MAX。也就是当高分辨率定时器层切换到高精度模式后,get_next_timer_interrupt函数在查找系统中所有定时器中最近将要到期的定时器时完全不用考虑高分辨率定时器。这是因为在高精度模式下,所有系统的Tick都是靠一个高分辨率定时器模拟的,停掉系统Tick只是取消了这个定时器,对系统中其它的高分辨率定时器没有任何影响,因此也不需要特殊处理。但是对于低分辨率定时器来说,在高精度模式下,它是通过模拟出的系统Tick来触发的,因此在没有Tick的情况下,需要对其进行特殊的处理,也就是根据其最近要到期的定时器的到期时间,

最后,如果得到的定时器到期时间大于0,就要调用__tick_nohz_idle_stop_tick函数停止系统Tick:

static void tick_nohz_stop_tick(struct tick_sched *ts, int cpu)
{
	struct clock_event_device *dev = __this_cpu_read(tick_cpu_device.evtdev);
	u64 basemono = ts->timer_expires_base;
	u64 expires = ts->timer_expires;
	ktime_t tick = expires;

	/* 将定时器到期基准时间设置为0 */
	ts->timer_expires_base = 0;

	/* 如果当前CPU就是负责更新系统jiffies的 */
	if (cpu == tick_do_timer_cpu) {
                /* 让出本CPU更新系统jiffies的权利 */
		tick_do_timer_cpu = TICK_DO_TIMER_NONE;
		ts->do_timer_last = 1;
	} else if (tick_do_timer_cpu != TICK_DO_TIMER_NONE) {
		ts->do_timer_last = 0;
	}

	/* 如果到期时间没有变就不需要再编程了 */
	if (ts->tick_stopped && (expires == ts->next_tick)) {
		if (tick == KTIME_MAX || ts->next_tick == hrtimer_get_expires(&ts->sched_timer))
			return;

		WARN_ON_ONCE(1);
		printk_once("basemono: %llu ts->next_tick: %llu dev->next_event: %llu timer->active: %d timer->expires: %llu\n",
			    basemono, ts->next_tick, dev->next_event,
			    hrtimer_active(&ts->sched_timer), hrtimer_get_expires(&ts->sched_timer));
	}

	/* 如果现在Tick还没有被停止 */
	if (!ts->tick_stopped) {
		calc_load_nohz_start();
		quiet_vmstat();

                /* 记录上一次Tick到来的时间 */
		ts->last_tick = hrtimer_get_expires(&ts->sched_timer);
                /* 更新tick_stopped状态正式停止Tick */
		ts->tick_stopped = 1;
		trace_tick_stop(1, TICK_DEP_MASK_NONE);
	}

        /* 记录下一次Tick到来的时间 */
	ts->next_tick = tick;

	/* 如果到期时间等于KTIME_MAX表示系统中没有要到期的定时器要处理 */
	if (unlikely(expires == KTIME_MAX)) {
                /* 如果在高精度动态时钟模式下则停止模拟Tick的定时器 */
		if (ts->nohz_mode == NOHZ_MODE_HIGHRES)
			hrtimer_cancel(&ts->sched_timer);
		return;
	}

        /* 如果系统中有要到期的定时器要处理 */
	if (ts->nohz_mode == NOHZ_MODE_HIGHRES) {
                /* 如果在高精度动态时钟模式下激活代表该定时器到期时间的模拟Tick的定时器 */
		hrtimer_start(&ts->sched_timer, tick,
			      HRTIMER_MODE_ABS_PINNED_HARD);
	} else {
                /* 如果在低精度动态时钟模式下直接对定时事件设备编程 */
		hrtimer_set_expires(&ts->sched_timer, tick);
		tick_program_event(tick, 1);
	}
}

在调用该函数之前,如果有要到期的定时器需要特殊处理,那么其到期时间已经记录在传入的tick_sched结构体中的timer_expires字段中了,如果没有的话那timer_expires字段的值会被设置成KTIME_MAX。如果不需要处理到期的定时器,那就不需要再对其编程了,直接退出即可,当然如果是在高精度动态时钟模式下,还必须先停掉代表模拟Tick的sched_timer高精度定时器。如果在Tick时发现系统中有要到期的定时器需要特殊处理,如果在高精度动态时钟模式下,则调用hrtimer_start函数,将其到期时间设置在定时器到期时间下一个Tick周期上。注意,在调用hrtimer_start函数启动一个高分辨率定时器时,首先会删除这个定时器,也就是模拟当前系统Tick的这个定时器,这时系统Tick就被停止掉了,然后再将其添加进系统,但是到期时间会设置成新的。如果在低精度动态时钟模式,则调用tick_program_event函数直接对底层的定时事件设备进行编程。

6)恢复Tick

如果想恢复Tick,Linux系统是通过调用tick_nohz_idle_exit函数实现的:

void tick_nohz_idle_exit(void)
{
	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
	bool idle_active, tick_stopped;
	ktime_t now;

	local_irq_disable();

	WARN_ON_ONCE(!ts->inidle);
	WARN_ON_ONCE(ts->timer_expires_base);

        /* 清除inidle字段表明退出空闲状态 */
	ts->inidle = 0;
	idle_active = ts->idle_active;
	tick_stopped = ts->tick_stopped;

	if (idle_active || tick_stopped)
                /* 获得当前时间 */
		now = ktime_get();

	if (idle_active)
                /* 退出空闲状态 */
		tick_nohz_stop_idle(ts, now);

	if (tick_stopped)
                /* 恢复当前CPU上的Tick */
		__tick_nohz_idle_restart_tick(ts, now);

	local_irq_enable();
}

如果当前确实是处于空闲状态,则调用tick_nohz_stop_idle函数退出:

static void tick_nohz_stop_idle(struct tick_sched *ts, ktime_t now)
{
	update_ts_time_stats(smp_processor_id(), ts, now, NULL);
        /* 清除idle_active字段表明退出空闲状态 */
	ts->idle_active = 0;

	sched_clock_idle_wakeup_event();
}

如果当前的Tick确实是被停止调了,则调用__tick_nohz_idle_restart_tick函数恢复:

static void __tick_nohz_idle_restart_tick(struct tick_sched *ts, ktime_t now)
{
        /* 恢复当前CPU上的Tick */
	tick_nohz_restart_sched_tick(ts, now);
	tick_nohz_account_idle_ticks(ts);
}

该函数实际是调用了tick_nohz_restart_sched_tick函数恢复Tick:

static void tick_nohz_restart_sched_tick(struct tick_sched *ts, ktime_t now)
{
	/* 根据当前时间来更新系统jiffies */
	tick_do_update_jiffies64(now);

	/* 通知(低分辨率)定时器层退出空闲状态 */
	timer_clear_idle();

	calc_load_nohz_stop();
	touch_softlockup_watchdog_sched();
	/* 清除tick_stopped字段表明Tick恢复了 */
	ts->tick_stopped  = 0;
        /* 记录退出空闲状态的时间 */
	ts->idle_exittime = now;

	tick_nohz_restart(ts, now);
}

在更新了系统jiffies和一些状态字段后,直接调用了tick_nohz_restart函数:

static void tick_nohz_restart(struct tick_sched *ts, ktime_t now)
{
        /* 清除sched_timer定时器 */
	hrtimer_cancel(&ts->sched_timer);
        /* 先设置定时器的到期时间为上一次Tick的时间 */
	hrtimer_set_expires(&ts->sched_timer, ts->last_tick);

	/* 更新定时器到期时间到下一个Tick到来的时间 */
	hrtimer_forward(&ts->sched_timer, now, tick_period);

	if (ts->nohz_mode == NOHZ_MODE_HIGHRES) {
                /* 激活sched_timer高分辨率定时器 */
		hrtimer_start_expires(&ts->sched_timer,
				      HRTIMER_MODE_ABS_PINNED_HARD);
	} else {
                /* 直接用sched_timer定时器的到期时间对定时事件设备编程 */
		tick_program_event(hrtimer_get_expires(&ts->sched_timer), 1);
	}

	/* 将next_tick清0 */
	ts->next_tick = 0;
}

先要将sched_timer定时器的到期时间设置到上一次没停Tick之前Tick到来的时间,因为后面的hrtimer_forward函数需要根据这个时间基准来计算一共休眠了多少个Tick周期。

你可能感兴趣的:(Arm64,Linux,ARM,Linux,时间子系统,Tick模拟)