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週期。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章