Linux時間子系統之Tick廣播層(Tick Broadcast)

在分析Tick模擬層的時候曾經提到過,當系統中沒有別的進程需要處理的時候,會將當前CPU切換到NO_HZ狀態,不會每一個Tick都收到定時中斷,從而達到節電的目的。但此時,當前CPU上的定時事件設備還是打開的,處於工作狀態,只不過不產生Tick了。但是,如果當前CPU上的定時事件設備還支持一種叫做C3_STOP的狀態的話,有可能當CPU進入某些空閒狀態的時候,連爲本CPU服務的定時事件設備都會被完全停止掉。這時候,本CPU將完全接收不到任何定時中斷,也不會自己把自己喚醒,必須尋求外部設備或其它沒有休眠CPU的幫助,這就是Tick廣播層存在的目的。

在Tick廣播層,定義瞭如下六個全局變量:

static cpumask_var_t tick_broadcast_mask __cpumask_var_read_mostly;
static cpumask_var_t tick_broadcast_on __cpumask_var_read_mostly;
static cpumask_var_t tmpmask __cpumask_var_read_mostly;
......
#ifdef CONFIG_TICK_ONESHOT

static cpumask_var_t tick_broadcast_oneshot_mask __cpumask_var_read_mostly;
static cpumask_var_t tick_broadcast_pending_mask __cpumask_var_read_mostly;
static cpumask_var_t tick_broadcast_force_mask __cpumask_var_read_mostly;
......
#endif /* CONFIG_TICK_ONESHOT */
  • tick_broadcast_mask:該變量中的每一位表示對應的CPU是否需要Tick廣播層提供Tick週期廣播服務,如果需要則對應的位被置位。
  • tick_broadcast_on:這個變量是用來控制打開或關閉Tick週期廣播服務的開關,如果當前CPU有可能會進入深度休眠狀態,則對應該CPU的位會被置位。
  • tmpmask:臨時變量。
  • tick_broadcast_oneshot_mask:當Tick廣播層被切換到單次觸發模式後,用來記錄哪些CPU已經進入的深度休眠模式,也就是本地定時事件設備被關閉了,需要Tick廣播層提供服務。
  • tick_broadcast_pending_mask和tick_broadcast_force_mask:用來處理特殊的競態情況,後面會解釋。

這些全局變量都是在tick_broadcast_init函數裏面初始化的:

void __init tick_broadcast_init(void)
{
	zalloc_cpumask_var(&tick_broadcast_mask, GFP_NOWAIT);
	zalloc_cpumask_var(&tick_broadcast_on, GFP_NOWAIT);
	zalloc_cpumask_var(&tmpmask, GFP_NOWAIT);
#ifdef CONFIG_TICK_ONESHOT
	zalloc_cpumask_var(&tick_broadcast_oneshot_mask, GFP_NOWAIT);
	zalloc_cpumask_var(&tick_broadcast_pending_mask, GFP_NOWAIT);
	zalloc_cpumask_var(&tick_broadcast_force_mask, GFP_NOWAIT);
#endif
}

可以看到,如果Tick廣播層被配置成不支持單次觸發模式的話,tick_broadcast_oneshot_mask、tick_broadcast_pending_mask和tick_broadcast_force_mask這三個全局變量是沒有用的。

1)Tick廣播設備的安裝

在介紹Tick層的新設備設置和切換場景時,曾提到過當註冊上來一個新的定時事件設備的時候,會調用Tick層的tick_check_new_device函數嘗試使用新的定時事件設備替換老的定時事件設備,作爲當前CPU上的Tick設備。但是,如果新的設備某些條件不滿足或者還不如老的設備的時候,會接着嘗試調用tick_install_broadcast_device函數,看看這個設備能不能作爲Tick廣播層的設備:

void tick_install_broadcast_device(struct clock_event_device *dev)
{
	struct clock_event_device *cur = tick_broadcast_device.evtdev;

        /* 是否可以用新設備替換老設備作爲Tick廣播設備 */
	if (!tick_check_broadcast_device(cur, dev))
		return;

        /* 對應的驅動模塊存不存在 */
	if (!try_module_get(dev->owner))
		return;

        /* 更換當前定時事件設備 */
	clockevents_exchange_device(cur, dev);
	if (cur)
                /* 將老定時事件設備的event_handler設置成什麼都不做的函數 */
		cur->event_handler = clockevents_handle_noop;
        /* 用新設備替換當前的Tick廣播設備 */
	tick_broadcast_device.evtdev = dev;
        /* 當前系統中是否已經有CPU需要提供Tick廣播服務了 */
	if (!cpumask_empty(tick_broadcast_mask))
		tick_broadcast_start_periodic(dev);
	/* 通知Tick模擬層有新的定時事件設備註冊上來 */
	if (dev->features & CLOCK_EVT_FEAT_ONESHOT)
		tick_clock_notify();
}

tick_check_broadcast_device函數的主要功能是檢查和比較新老定時事件設備,看看新的能不能替換老的作爲Tick廣播設備:

static bool tick_check_broadcast_device(struct clock_event_device *curdev,
					struct clock_event_device *newdev)
{
	if ((newdev->features & CLOCK_EVT_FEAT_DUMMY) ||
	    (newdev->features & CLOCK_EVT_FEAT_PERCPU) ||
	    (newdev->features & CLOCK_EVT_FEAT_C3STOP))
		return false;

	if (tick_broadcast_device.mode == TICKDEV_MODE_ONESHOT &&
	    !(newdev->features & CLOCK_EVT_FEAT_ONESHOT))
		return false;

	return !curdev || newdev->rating > curdev->rating;
}

可以看出來,不是任何定時事件設備都有資格作爲系統的Tick廣播設備的。這個設備不能是一個假的(CLOCK_EVT_FEAT_DUMMY),不能是和某個CPU綁定的私有設備(CLOCK_EVT_FEAT_PERCPU),不能是支持C3_STOP狀態的設備(CLOCK_EVT_FEAT_C3STOP)。如果這個設備本身也是支持C3_STOP狀態的,也就意味着該設備也會被徹底停止掉,肯定也就不能向其它設備廣播了。還有,如果當前作爲系統廣播的Tick設備已經切換到了單次觸發模式了,而新加入的定時事件設備並不支持單次觸發模式,則新設備也沒有資格替換老的設備。最後,即使前面的條件都滿足了,還要比較定時事件設備的精度值。

如果一切條件都滿足了,那就可以正式調用定時事件層的clockevents_exchange_device函數切換了。之後,還要把老的定時事件設備的中斷回調函數設置成一個什麼都不做的空函數(clockevents_handle_noop),並且把Tick廣播設備的定時事件設備替換成新的。

全局變量tick_broadcast_mask表示有沒有CPU會請求Tick廣播服務。如果當前系統中所有的CPU都不會睡死進入C3_STOP狀態,那麼即使進入了NO_HZ狀態,它們各自維護的定時器也會得到妥善的處理,就不需要Tick廣播了。如果tick_broadcast_mask不是0,表明系統中至少有一個設備會完全停止需要Tick廣播,那麼接下來就需要調用tick_broadcast_start_periodic函數,啓動Tick週期廣播:

static void tick_broadcast_start_periodic(struct clock_event_device *bc)
{
	if (bc)
		tick_setup_periodic(bc, 1);
}

該函數直接調用了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;

        /* 定時事件設備支持週期觸發模式且當前Tick廣播層沒有切換到單次觸發模式 */
	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;

                /* 讀取下一次Tick到來的事件 */
		do {
			seq = read_seqbegin(&jiffies_lock);
			next = tick_next_period;
		} while (read_seqretry(&jiffies_lock, seq));

                /* 將定時事件設備切換成單次觸發模式 */
		clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT);

                /* 對定時事件設備進行編程讓其在下一次Tick到來時觸發中斷 */
		for (;;) {
			if (!clockevents_program_event(dev, next, false))
				return;
			next = ktime_add(next, tick_period);
		}
	}
}

該函數首先調用了tick_set_periodic_handler函數來設置定時事件設備的中斷回調函數:

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是真,那定時事件設備的中斷回調函數會被設置成tick_handle_periodic_broadcast。

tick_device_is_functional函數用來判斷一個定時事件設備是不是可以正常工作:

static inline int tick_device_is_functional(struct clock_event_device *dev)
{
	return !(dev->features & CLOCK_EVT_FEAT_DUMMY);
}

很簡單,只是檢查了定時事件設備是不是一個“假”的。

tick_setup_periodic函數接下來會看定時事件設備是否支持週期觸發模式,並且Tick廣播層有沒有已經被切換到了單次觸發模式。如果定時事件設備支持週期觸發模式,並且Tick廣播層還沒有被切換到單次觸發模式,那麼簡單了,只需要確保定時事件設備工作在週期觸發模式就可以了。但是,如果定時事件設備不支持週期觸發模式,也就是隻支持單次觸發模式的話,就需要對其進行編程了,讓其在下一個Tick到來的時間點上觸發中斷。那有沒有可能定時事件設備只支持週期觸發,而當前Tick廣播層現在又處在單次觸發模式呢?這種情況是不可能的,這種情況下前面的tick_check_broadcast_device函數在檢查的時候就直接把這個定時事件設備淘汰掉了。另外,無論當前Tick廣播層處於何種模式下,當有一個新的定時事件設備被選中作爲Tick廣播設備後,並且當前系統中有CPU需要提供Tick廣播服務的情況下,都會將Tick廣播層設置到週期觸發狀態。如果當前Tick層已經被切換到了單次觸發狀態,又來了一個新的可以替換當前的Tick廣播設備,且這時候還有CPU需要提供Tick廣播服務,其實會有問題的,但一般用作Tick廣播設備的定時事件設備都很早就註冊並初始化了,這種情況應該不會發生。

最後還有一個問題,爲什麼在tick_install_broadcast_device函數最後,在需要安裝的新Tick廣播設備支持單次觸發模式的情況下,還需要調用tick_clock_notify函數通知Tick模擬層有新的定時事件設備註冊上來呢?這是因爲,在後面切換到單次觸發模式的場景下會介紹,每個CPU上即使當前的定時事件設備支持單次觸發模式的,但是如果它還支持C3_STOP模式的話,那麼當前CPU上的Tick設備切換到單次觸發模式的前提是現在Tick廣播層的設備必須也要支持單次觸發模式,如果不支持就不切換。那麼好了,現在來了一個支持單次觸發模式的Tick廣播設備,雖然在介紹高分辨率定時器層的時候提到,每次Tick到來都會調用tick_check_oneshot_change函數,檢查看是否能切換到高精度模式,但是這個函數第一個判斷條件就是當前系統中的時鐘源設備或定時事件設備有沒有改變,如果沒有就直接退出了:

int tick_check_oneshot_change(int allow_nohz)
{
	struct tick_sched *ts = this_cpu_ptr(&tick_cpu_sched);
 
        /* 系統中是否已經出現了新的時鐘設備 */
	if (!test_and_clear_bit(0, &ts->check_clocks))
		return 0;
 
    ......
}

所以,如果在tick_install_broadcast_device函數最後不通知的話,即使現在所有條件都滿足了,Tick層也永遠不會切換到單次觸發模式。

2)週期觸發事件處理

前面一節已經提到過了,Tick廣播層挑選出的定時事件設備是有要求的,其中一個非常重要的就是不能是隻爲某個CPU服務的私有設備,也就是說選出來的設備必須是大家公有的,系統中當前每個在線的CPU都有可能接收到週期中斷,包括已經進入空閒狀態且私有定時事件設備已經被停止掉的CPU,但是同一個中斷只會被一個CPU接收並處理。而且,前面分析也提到了,在週期觸發模式下,被Tick廣播層選中的定時事件設備的中斷回調函數會被設置成tick_handle_periodic_broadcast:

static void tick_handle_periodic_broadcast(struct clock_event_device *dev)
{
        /* 獲得屬於當前CPU的Tick設備 */
	struct tick_device *td = this_cpu_ptr(&tick_cpu_device);
	bool bc_local;

	raw_spin_lock(&tick_broadcast_lock);

	/* 如果當前Tick廣播設備已經被關閉了則直接退出 */
	if (clockevent_state_shutdown(tick_broadcast_device.evtdev)) {
		raw_spin_unlock(&tick_broadcast_lock);
		return;
	}

        /* 發送Tick廣播 */
	bc_local = tick_do_periodic_broadcast();

        /* 如果定時事件設備工作在單次觸發模式 */
	if (clockevent_state_oneshot(dev)) {
                /* 計算下一次Tick到來的事件 */
		ktime_t next = ktime_add(dev->next_event, tick_period);

                /* 對定時事件設備進行編程 */
		clockevents_program_event(dev, next, true);
	}
	raw_spin_unlock(&tick_broadcast_lock);

	/* 如果本CPU也需要Tick廣播服務 */
	if (bc_local)
                /* 直接調用當前CPU上的Tick設備的事件處理函數 */
		td->evtdev->event_handler(td->evtdev);
}

該函數會調用tick_do_periodic_broadcast函數,向可能需要服務的CPU發送廣播。不過,如果當前正在處理這個中斷的CPU也需要廣播服務的話,也就是說Tick廣播設備的定時中斷搞好激活了某個進入空閒狀態的CPU,就沒有必要再對自己進行廣播了,因此函數最後直接調用屬於本CPU的Tick設備的事件處理函數就行了。如果Tick廣播使用的定時事件設備工作在單次觸發模式,那麼還需要對其進行編程,讓它在下一個Tick週期到來的時間點上再次觸發中斷。

static bool tick_do_periodic_broadcast(void)
{
        /* 需要Tick廣播服務並且還在線的CPU */
	cpumask_and(tmpmask, cpu_online_mask, tick_broadcast_mask);
	return tick_do_broadcast(tmpmask);
}

tick_broadcast_mask全局變量記錄了有哪些CPU需要Tick廣播服務,cpu_online_mask全局變量記錄了當前系統中有哪些CPU還是在線的,沒有被拔掉,一個CPU即使已經進入C3_STOP的“睡死”狀態,它仍然還是在線的。該函數計算tick_broadcast_mask和cpu_online_mask的交集,也就是找出需要Tick廣播服務並且還在線的CPU,將其存放在tmpmask全局變量中,然後調用tick_do_broadcast函數:

static bool tick_do_broadcast(struct cpumask *mask)
{
	int cpu = smp_processor_id();
	struct tick_device *td;
	bool local = false;

	/* 是否當前CPU也需要Tick廣播服務 */
	if (cpumask_test_cpu(cpu, mask)) {
		struct clock_event_device *bc = tick_broadcast_device.evtdev;

                /* 將代表本CPU的標誌位清空 */
		cpumask_clear_cpu(cpu, mask);
		/* Tick廣播設備如果是基於高分辨率定時器模擬的則也不能算是本地的 */
		local = !(bc->features & CLOCK_EVT_FEAT_HRTIMER);
	}

        /* 仍然有其它CPU等着接收Tick廣播 */
	if (!cpumask_empty(mask)) {
		/* 假設系統中所有CPU上的Tick設備中的定時事件設備的broadcast函數都是一樣的 */
                /* 取第一個Tick設備 */
		td = &per_cpu(tick_cpu_device, cpumask_first(mask));
                /* 調用它的broadcast函數 */
		td->evtdev->broadcast(mask);
	}
	return local;
}

該函數首先檢查需要廣播服務的CPU位圖中是否包含了本CPU,如果是的話會將其清除,然後函數退出的時候會返回真。接着,如果還有CPU等着接收Tick廣播的話,就從所有這些CPU對應的Tick設備上挑選出一個,這裏選的是第一個設備,然後調用它的廣播函數。這裏其實有一個假設,就是系統中所有CPU上的Tick設備中的定時事件設備的broadcast函數都被設置成一樣的。在後面會看到它們確實都是一樣的,被設置成了tick_broadcast函數。

3)切換到單次觸發模式

在介紹高分辨率定時器層的時候提到過,在低精度模式下的週期處理函數hrtimer_run_queues中,每次都會調用tick_check_oneshot_change函數,判斷目前是否可以切換到高精度模式。而在這個函數中,還會調用tick_is_oneshot_available函數判斷Tick層是否已經準備好切換了:

int tick_is_oneshot_available(void)
{
        /* 獲取代表當前CPU上定時事件設備的clock_event_device結構體 */
	struct clock_event_device *dev = __this_cpu_read(tick_cpu_device.evtdev);

        /* 如果當前CPU上沒有定時事件設備或者不支持單次觸發模式則返回0 */
	if (!dev || !(dev->features & CLOCK_EVT_FEAT_ONESHOT))
		return 0;
        /* 如果當前CPU上的定時事件設備也不支持C3_STOP模式則返回1 */
	if (!(dev->features & CLOCK_EVT_FEAT_C3STOP))
		return 1;
        /* 如果當前CPU上的定時事件設備支持C3_STOP模式則還要查看Tick廣播層 */
	return tick_broadcast_oneshot_available();
}

該函數最後還會調用tick_broadcast_oneshot_available函數,看看Tick廣播層是否已經準備好了:

bool tick_broadcast_oneshot_available(void)
{
	struct clock_event_device *bc = tick_broadcast_device.evtdev;

	return bc ? bc->features & CLOCK_EVT_FEAT_ONESHOT : false;
}

也就是說,即使當前CPU上有專有的定時事件設備,並且它是支持單次觸發模式的,但是如果它還支持所謂的C3_STOP模式,並且當前Tick廣播層的設備不支持單次觸發模式,那麼還是不能切換成高精度模式。

還是在介紹高分辨率定時器層的時候提到過,如果通過了測試正式準備切換到單次觸發模式了,最終會調用tick_switch_to_oneshot函數。如果切換成功,函數的最後會調用tick_broadcast_switch_to_oneshot函數,將Tick廣播層也切換到單次觸發模式:

void tick_broadcast_switch_to_oneshot(void)
{
	struct clock_event_device *bc;
	unsigned long flags;

	raw_spin_lock_irqsave(&tick_broadcast_lock, flags);

	tick_broadcast_device.mode = TICKDEV_MODE_ONESHOT;
	bc = tick_broadcast_device.evtdev;
        /* 如果當前Tick廣播設備不爲空 */
	if (bc)
                /* 設置當前Tick廣播設備到單次觸發模式 */
		tick_broadcast_setup_oneshot(bc);

	raw_spin_unlock_irqrestore(&tick_broadcast_lock, flags);
}

所以,如果當前系統中用於Tick廣播的定時事件設備不爲0,則調用tick_broadcast_setup_oneshot將其設置到單次觸發模式:

static void tick_broadcast_setup_oneshot(struct clock_event_device *bc)
{
	int cpu = smp_processor_id();

	if (!bc)
		return;

	/* 是否已經切換到單次觸發模式了 */
	if (bc->event_handler != tick_handle_oneshot_broadcast) {
                /* 當前是否處於週期觸發模式 */
		int was_periodic = clockevent_state_periodic(bc);

                /* 將定時事件設備的中斷回調函數設置成tick_handle_oneshot_broadcast */
		bc->event_handler = tick_handle_oneshot_broadcast;

		/* 將tick_broadcast_mask拷貝到tmpmask */
		cpumask_copy(tmpmask, tick_broadcast_mask);
                /* 去掉當前的CPU */
		cpumask_clear_cpu(cpu, tmpmask);
                /* 合併需要提供週期Tick廣播的CPU進入tick_broadcast_oneshot_mask中 */
		cpumask_or(tick_broadcast_oneshot_mask,
			   tick_broadcast_oneshot_mask, tmpmask);

                /* 如果當前處於週期觸發模式並且還有CPU需要提供廣播服務 */
		if (was_periodic && !cpumask_empty(tmpmask)) {
                        /* 將定時事件設備切換到單次觸發模式 */
			clockevents_switch_state(bc, CLOCK_EVT_STATE_ONESHOT);
			tick_broadcast_init_next_event(tmpmask,
						       tick_next_period);
			tick_broadcast_set_event(bc, cpu, tick_next_period);
		} else
			bc->next_event = KTIME_MAX;
	} else {
		/* 去掉當前的CPU */
		tick_broadcast_clear_oneshot(cpu);
	}
}

如果系統中有多個CPU的話,要切換到高精度模式,應該每個CPU上都會有一個私有的支持單次觸發模式的定時事件設備,每個CPU都會依次調用tick_switch_to_oneshot切換到高精度模式下,因此tick_broadcast_setup_oneshot函數會被調用多次。但是,對於Tick廣播層來說,只有一個Tick廣播設備,它是系統中所有CPU共用的。所以,只要系統中第一個CPU切換到高精度模式下了,則Tick廣播設備就會被切換到單次觸發模式了。但是,系統中的其它CPU的Tick設備還是處在週期觸發模式下,還需要Tick廣播層提供週期廣播服務。因此,在計算tick_broadcast_oneshot_mask的時候還要考慮加入tick_broadcast_mask指定的CPU,並且對Tick廣播設備進行編程,讓它在下一次Tick到來的時間點觸發。

另外,如果執行到了這個函數,說明本CPU已經不在空閒狀態,並且當前CPU上的定時事件設備正要切換成單次觸發狀態肯定不會被停止,因此一定是不再需要Tick廣播服務了。

static void tick_broadcast_init_next_event(struct cpumask *mask,
					   ktime_t expires)
{
	struct tick_device *td;
	int cpu;

        /* 按mask遍歷所有Per CPU變量 */
	for_each_cpu(cpu, mask) {
		td = &per_cpu(tick_cpu_device, cpu);
		if (td->evtdev)
			td->evtdev->next_event = expires;
	}
}

可以看出來tick_broadcast_init_next_event函數實際上是遍歷所有參數mask指定的CPU上的Tick設備,將其對應的代表定時事件設備結構體中的next_event變量設置成傳遞進來的expires參數的值。

static void tick_broadcast_set_event(struct clock_event_device *bc, int cpu,
				     ktime_t expires)
{
        /* 切換Tick廣播設備到單次觸發模式 */
	if (!clockevent_state_oneshot(bc))
		clockevents_switch_state(bc, CLOCK_EVT_STATE_ONESHOT);

        /* 對Tick廣播設備進行編程讓其在指定到期時間觸發 */
	clockevents_program_event(bc, expires, 1);
        /* 設置Tick廣播設備的親緣性 */
	tick_broadcast_set_affinity(bc, cpumask_of(cpu));
}

tick_broadcast_set_event函數用來對Tick廣播設備進行編程,讓其在指定的到期時間上,在指定的CPU上觸發中斷。tick_broadcast_set_affinity函數用來設置Tick廣播設備產生中斷的CPU親緣性:

static void tick_broadcast_set_affinity(struct clock_event_device *bc,
					const struct cpumask *cpumask)
{
        /* 定時事件設備是否支持設置CPU親緣性 */
	if (!(bc->features & CLOCK_EVT_FEAT_DYNIRQ))
		return;

        /* CPU親緣性是否沒有更改 */
	if (cpumask_equal(bc->cpumask, cpumask))
		return;

	bc->cpumask = cpumask;
        /* 設置CPU親緣性 */
	irq_set_affinity(bc->irq, bc->cpumask);
}

irq_set_affinity函數最終用來設置定時事件設備的CPU親緣性,到期後觸發指定CPU上的中斷。

一旦切換到單次觸發模式後,就不是像在週期觸發模式那樣,每次都是按照Tick週期觸發了,而是按照需要觸發。並且,Tick廣播設備能切換到單次觸發模式的前提是,系統中至少有一個CPU上的Tick設備被切換成了單次觸發模式。由於多處理器系統基本上都是對稱的(SMP),所以系統中剩下的CPU遲早也會註冊同樣的定時事件設備,最終各個CPU上的Tick設備都會切換到單次觸發模式下。

4)單次觸發模式事件處理

前面分析也提到了,在單次觸發模式下,被Tick廣播層選中的定時事件設備的中斷回調函數會被設置成tick_handle_oneshot_broadcast:

static void tick_handle_oneshot_broadcast(struct clock_event_device *dev)
{
	struct tick_device *td;
	ktime_t now, next_event;
	int cpu, next_cpu = 0;
	bool bc_local;

	raw_spin_lock(&tick_broadcast_lock);
	dev->next_event = KTIME_MAX;
	next_event = KTIME_MAX;
	cpumask_clear(tmpmask);
	now = ktime_get();
	/* 遍歷所有需要Tick廣播服務的CPU */
	for_each_cpu(cpu, tick_broadcast_oneshot_mask) {
		/* 如果系統中沒有CPU需要Tick廣播服務則直接退出 */
		if (!IS_ENABLED(CONFIG_SMP) &&
		    cpumask_empty(tick_broadcast_oneshot_mask))
			break;

		td = &per_cpu(tick_cpu_device, cpu);
		if (td->evtdev->next_event <= now) {
                        /* 如果對應CPU的Tick設備已經到期 */
			cpumask_set_cpu(cpu, tmpmask);
			/* 設置tick_broadcast_pending_mask對應CPU的位 */
			cpumask_set_cpu(cpu, tick_broadcast_pending_mask);
		} else if (td->evtdev->next_event < next_event) {
                        /* 找出最近Tick設備即將要到期的那個CPU和到期時間 */
			next_event = td->evtdev->next_event;
			next_cpu = cpu;
		}
	}

	/* 將本CPU從tick_broadcast_pending_mask中清除 */
	cpumask_clear_cpu(smp_processor_id(), tick_broadcast_pending_mask);

	/* 併入所有tick_broadcast_force_mask全局變量中指定的CPU */
	cpumask_or(tmpmask, tmpmask, tick_broadcast_force_mask);
        /* 清空tick_broadcast_force_mask全局變量 */
	cpumask_clear(tick_broadcast_force_mask);

	/* 排除掉已經不在線的CPU */
	if (WARN_ON_ONCE(!cpumask_subset(tmpmask, cpu_online_mask)))
		cpumask_and(tmpmask, tmpmask, cpu_online_mask);

	/* 發送Tick廣播 */
	bc_local = tick_do_broadcast(tmpmask);

	/* 對廣播設備進行編程,讓它在下一個到期時間在對應的CPU上觸發中斷。 */
	if (next_event != KTIME_MAX)
		tick_broadcast_set_event(dev, next_cpu, next_event);

	raw_spin_unlock(&tick_broadcast_lock);

        /* 如果本CPU也需要Tick廣播服務 */
	if (bc_local) {
		td = this_cpu_ptr(&tick_cpu_device);
                /* 直接調用當前CPU上的Tick設備的事件處理函數 */
		td->evtdev->event_handler(td->evtdev);
	}
}

Tick廣播層工作在週期觸發模式和單次觸發模式有很大的不同。週期觸發模式沒有所謂的CPU親緣性的概念,Tick廣播層會按照一個Tick週期觸發一箇中斷,系統中的任何CPU都有可能接收到這個中斷並對其進行處理。而單次觸發模式具有CPU親緣性的概念,如果作爲Tick廣播設備的定時事件設備支持的話,會觸發指定CPU上的中斷,將其激活,處理到期的定時器。

切換到單次觸發模式後,將檢查tick_broadcast_oneshot_mask全局變量來確定哪些CPU需要Tick廣播服務,而不再是tick_broadcast_mask全局變量。

5)打開或關閉對本CPU的Tick廣播服務

在cpuidle驅動註冊的時候(__cpuidle_register_driver函數內,代碼位於drivers/cpuidle/drivers.c。),如果某些狀態包含CPUIDLE_FLAG_TIMER_STOP選項,則會調用tick_broadcast_enable函數,打開對本CPU的Tick廣播服務;而在驅動卸載的時候(__cpuidle_unregister_driver函數內),會相應調用tick_broadcast_disable函數,關閉對本CPU的Tick廣播服務。

static inline void tick_broadcast_enable(void)
{
	tick_broadcast_control(TICK_BROADCAST_ON);
}
static inline void tick_broadcast_disable(void)
{
	tick_broadcast_control(TICK_BROADCAST_OFF);
}

最終都是調用的tick_broadcast_control函數,只不過傳入的參數不一樣:

void tick_broadcast_control(enum tick_broadcast_mode mode)
{
	struct clock_event_device *bc, *dev;
	struct tick_device *td;
	int cpu, bc_stopped;
	unsigned long flags;

	/* 獲得tick_broadcast_lock自旋鎖並關中斷 */
	raw_spin_lock_irqsave(&tick_broadcast_lock, flags);
	td = this_cpu_ptr(&tick_cpu_device);
	dev = td->evtdev;

	/* 如果當前CPU上的Tick設備不支持C3_STOP模式則直接退出 */
	if (!dev || !(dev->features & CLOCK_EVT_FEAT_C3STOP))
		goto out;

        /* 如果當前CPU上的Tick設備不可以正常工作則直接退出 */
	if (!tick_device_is_functional(dev))
		goto out;

	cpu = smp_processor_id();
	bc = tick_broadcast_device.evtdev;
        /* 當前Tick廣播設備是否是停止的 */
	bc_stopped = cpumask_empty(tick_broadcast_mask);

	switch (mode) {
	case TICK_BROADCAST_FORCE:
                /* 強制打開 */
		tick_broadcast_forced = 1;
	case TICK_BROADCAST_ON:
                /* 設置tick_broadcast_on中當前CPU對應的位 */
		cpumask_set_cpu(cpu, tick_broadcast_on);
                /* 設置tick_broadcast_mask中當前CPU對應的位 */
		if (!cpumask_test_and_set_cpu(cpu, tick_broadcast_mask)) {
                        /* 如果之前沒有由Tick廣播層對當前CPU發送Tick廣播 */
			/* 如果Tick廣播設備存在且不是由高分辨率定時器模擬的且處在週期觸發模式 */
			if (bc && !(bc->features & CLOCK_EVT_FEAT_HRTIMER) &&
			    tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC)
                                /* 關閉本CPU的Tick設備 */
				clockevents_shutdown(dev);
		}
		break;

	case TICK_BROADCAST_OFF:
                /* 如果強制打開不允許關閉 */
		if (tick_broadcast_forced)
			break;
                /* 清除tick_broadcast_on中當前CPU對應的位 */
		cpumask_clear_cpu(cpu, tick_broadcast_on);
                /* 清除tick_broadcast_mask中當前CPU對應的位 */
		if (cpumask_test_and_clear_cpu(cpu, tick_broadcast_mask)) {
                        /* 如果之前已經由Tick廣播層對當前CPU發送Tick廣播 */
                        /* 如果當前Tick廣播層還處在週期觸發模式 */
			if (tick_broadcast_device.mode ==
			    TICKDEV_MODE_PERIODIC)
                                /* 打開本CPU的Tick設備生成Tick */
				tick_setup_periodic(dev, 0);
		}
		break;
	}

	if (bc) {
		if (cpumask_empty(tick_broadcast_mask)) {
                        /* 如果沒有CPU需要提供Tick廣播服務 */
                        /* 如果當前Tick廣播設備是打開的 */
			if (!bc_stopped)
                                /* 關閉Tick廣播設備 */
				clockevents_shutdown(bc);
		} else if (bc_stopped) {
                        /* 如果有CPU需要提供Tick廣播服務且當前Tick廣播設備是關閉的 */
                        /* 按照當前Tick廣播層所處的模式分別打開Tick廣播設備 */
			if (tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC)
				tick_broadcast_start_periodic(bc);
			else
				tick_broadcast_setup_oneshot(bc);
		}
	}
out:
        /* 釋放tick_broadcast_lock自旋鎖並開中斷 */
	raw_spin_unlock_irqrestore(&tick_broadcast_lock, flags);
}
EXPORT_SYMBOL_GPL(tick_broadcast_control);

cpuidle驅動的註冊時機應該是在系統所有的定時事件設備都已經註冊完成之後,也就是在調用tick_broadcast_control函數之後不會有新的定時事件設備再註冊進系統中。因此,如果當前CPU上的Tick定時事件設備不可用或者不工作,那麼沒得選,肯定要靠Tick廣播層提供服務,因此也沒必要設置tick_broadcast_on打開或關閉了。後面會介紹(在tick_device_uses_broadcast函數中),在這種情況下,其實在這個不工作的定時事件設備被當前CPU選中作爲產生Tick的設備時,就已經通知Tick廣播層對該CPU打開了Tick廣播服務。而如果當前CPU上的Tick設備不支持C3_STOP狀態,那它自己就可以搞定當前CPU上的Tick,肯定不需要Tick廣播服務了。

當Tick廣播層工作在週期觸發模式的時候,只要CPU有可能被切換到會關閉本地定時事件設備的狀態,且本地定時事件設備支持C3_STOP狀態時,就會讓Tick廣播層提供服務了,即使當前CPU還沒進入那種狀態。

6)Tick廣播服務註冊

不是任何時候對任何CPU都需要啓動Tick廣播服務的,前面介紹了在cpuidle驅動中主動打開或關閉某個CPU的Tick廣播服務的情況。還有一種情況,如果當前CPU上的Tick設備將要發生改變的時候也可能對Tick廣播服務產生影響。

在介紹Tick層的時候,曾經提到過,當有一個新的定時事件設備註冊上來,需要替換當前CPU上Tick層的設備時,會調用tick_setup_device函數,在這個函數中,會調用tick_device_uses_broadcast函數:

int tick_device_uses_broadcast(struct clock_event_device *dev, int cpu)
{
	struct clock_event_device *bc = tick_broadcast_device.evtdev;
	unsigned long flags;
	int ret = 0;

	raw_spin_lock_irqsave(&tick_broadcast_lock, flags);

	/* 定時事件設備是否是“假”的 */
	if (!tick_device_is_functional(dev)) {
                /* 將定時事件設備的中斷回調函數設置成tick_handle_periodic */
		dev->event_handler = tick_handle_periodic;
                /* 設置定時事件設備的broadcast函數 */
		tick_device_setup_broadcast_func(dev);
                /* 該CPU需要提供Tick週期廣播服務 */
		cpumask_set_cpu(cpu, tick_broadcast_mask);
                /* 根據當前Tick廣播層所處的模式啓動Tick廣播服務 */
		if (tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC)
			tick_broadcast_start_periodic(bc);
		else
			tick_broadcast_setup_oneshot(bc);
		ret = 1;
	} else {
		/* 如果定時事件設備不支持C3_STOP模式 */
		if (!(dev->features & CLOCK_EVT_FEAT_C3STOP))
                        /* 不需要對這個CPU提供Tick廣播服務 */
			cpumask_clear_cpu(cpu, tick_broadcast_mask);
		else
                        /* 否則設置定時事件設備的broadcast函數 */
			tick_device_setup_broadcast_func(dev);

		/* 如果對該CPU的Tick廣播服務被關閉了 */
		if (!cpumask_test_cpu(cpu, tick_broadcast_on))
                        /* 清除對該CPU的Tick廣播服務 */
			cpumask_clear_cpu(cpu, tick_broadcast_mask);

		switch (tick_broadcast_device.mode) {
		case TICKDEV_MODE_ONESHOT:
                        /* 在單次觸發模式下 */
			/* 直接清除對該CPU的Tick單次廣播服務 */
			tick_broadcast_clear_oneshot(cpu);
			ret = 0;
			break;

		case TICKDEV_MODE_PERIODIC:
                        /* 在週期觸發模式下 */
			/* 如果系統中沒有一個CPU需要提供Tick週期廣播服務 */
			if (cpumask_empty(tick_broadcast_mask) && bc)
                                /* 關閉Tick廣播設備 */
				clockevents_shutdown(bc);
			/* 如果Tick廣播設備存在且不是由高分辨率定時器模擬的 */
			if (bc && !(bc->features & CLOCK_EVT_FEAT_HRTIMER))
                                /* 如果該CPU需要Tick週期廣播服務 */
				ret = cpumask_test_cpu(cpu, tick_broadcast_mask);
			break;
		default:
			break;
		}
	}
	raw_spin_unlock_irqrestore(&tick_broadcast_lock, flags);
	return ret;
}

tick_device_uses_broadcast函數主要是檢查當前CPU上的Tick定時事件設備的特性,並確定是否要啓用對這個CPU的Tick廣播服務。如果這個函數返回非0,則表示當前CPU上的Tick將完全有Tick廣播層來負責。

如果當前CPU打算選擇一個“假”的定時事件設備作爲Tick設備,那麼沒有辦法,只能由Tick廣播層對其提供Tick廣播服務了。並且這個結果是不受tick_broadcast_on變量控制的,也就是即使已經顯式關閉了對本CPU的Tick廣播服務,照樣還需要Tick廣播層提供服務,這也很好理解,畢竟自己CPU的Tick設備已經沒法工作了,必須要尋求外部的幫助。

如果當前CPU選擇的定時事件設備是真的,且不支持C3_STOP模式,那麼在這個CPU上的Tick完全可以由這個本地設備搞定,就沒必要Tick廣播層對其提供服務了,因此可以直接清除。還有,如果對該CPU的Tick週期廣播服務被關閉了,也需要清除對該CPU的Tick廣播服務。如果當前Tick廣播層已經工作在單次觸發模式了,那非常簡單,直接清除對該CPU的單次Tick廣播服務就行了,因爲執行到這個函數就證明當前CPU不是在空閒狀態,對應CPU上的定時事件設備不會被停止掉。但是,如果當前Tick廣播層還是工作在週期模式,情況就有點複雜了。如果沒有任何設備需要提供Tick週期廣播服務,則當前的Tick廣播設備就可以關閉了。並且,如果當前的Tick廣播設備存在,且不是有高分辨率定時器模擬的,且當前CPU還需要提供Tick廣播服務,則返回非0,也就是當前CPU上的週期Tick將完全由Tick廣播層來提供。

函數tick_device_setup_broadcast_func負責設置指定定時事件設備的broadcast函數:

static void tick_device_setup_broadcast_func(struct clock_event_device *dev)
{
	if (!dev->broadcast)
		dev->broadcast = tick_broadcast;
	if (!dev->broadcast) {
		pr_warn_once("%s depends on broadcast, but no broadcast function available\n",
			     dev->name);
		dev->broadcast = err_broadcast;
	}
}

可以看到,如果不出問題的話,每個CPU上私有的定時事件設備的廣播函數都被設置成了tick_broadcast。

7)打開或關閉本地定時器

當系統中的某個CPU要進入或退出一個標記有CPUIDLE_FLAG_TIMER_STOP選項的空閒狀態的時候,會調用tick_broadcast_enter函數,從而告訴Tick廣播層屬於本CPU的本地定時事件設備就要停止掉了,需要Tick廣播層提供服務。相反,如果退出了空閒狀態之後,會調用tick_broadcast_exit函數,恢復本CPU的定時事件設備,停止掉針對本CPU的Tick廣播服務(代碼位於include/linux/tick.h中):

static inline int tick_broadcast_enter(void)
{
	return tick_broadcast_oneshot_control(TICK_BROADCAST_ENTER);
}
static inline void tick_broadcast_exit(void)
{
	tick_broadcast_oneshot_control(TICK_BROADCAST_EXIT);
}

就是直接調用了tick_broadcast_oneshot_control函數,傳遞了不同的參數(代碼位於kernel/time/tick-common.c中):

int tick_broadcast_oneshot_control(enum tick_broadcast_state state)
{
	struct tick_device *td = this_cpu_ptr(&tick_cpu_device);

        /* 如果本CPU的Tick設備不支持C3_STOP狀態則直接退出 */
	if (!(td->evtdev->features & CLOCK_EVT_FEAT_C3STOP))
		return 0;

	return __tick_broadcast_oneshot_control(state);
}
EXPORT_SYMBOL_GPL(tick_broadcast_oneshot_control);

如果本CPU的Tick設備不支持C3_STOP狀態,就意味着它不會被停掉,那當然就不用做什麼特殊操作了,直接返回就可以了。否則,會接着調用__tick_broadcast_oneshot_control函數:

int __tick_broadcast_oneshot_control(enum tick_broadcast_state state)
{
	struct clock_event_device *bc, *dev;
	int cpu, ret = 0;
	ktime_t now;

	/* 如果Tick廣播設備不存在則直接返回-EBUSY */
	if (!tick_broadcast_device.evtdev)
		return -EBUSY;

	dev = this_cpu_ptr(&tick_cpu_device)->evtdev;

	raw_spin_lock(&tick_broadcast_lock);
	bc = tick_broadcast_device.evtdev;
	cpu = smp_processor_id();

	if (state == TICK_BROADCAST_ENTER) {
                /* 要進入空閒狀態 */
		/* 判斷當前CPU是否能進入休眠狀態 */
		ret = broadcast_needs_cpu(bc, cpu);
		if (ret)
			goto out;

		/* 如果Tick廣播層還處在週期觸發模式 */
		if (tick_broadcast_device.mode == TICKDEV_MODE_PERIODIC) {
			/* 如果Tick廣播設備是由高分辨率定時器模擬的則返回-EBUSY */
			if (bc->features & CLOCK_EVT_FEAT_HRTIMER)
				ret = -EBUSY;
                        /* 否則返回0 */
			goto out;
		}

                /* 設置tick_broadcast_oneshot_mask中當前CPU對應的位 */
		if (!cpumask_test_and_set_cpu(cpu, tick_broadcast_oneshot_mask)) {
                        /* 如果之前沒有由Tick廣播層爲當前CPU服務 */
			WARN_ON_ONCE(cpumask_test_cpu(cpu, tick_broadcast_pending_mask));

			/* 嘗試關閉本CPU上的定時事件設備 */
			broadcast_shutdown_local(bc, dev);

			/* 如果tick_broadcast_force_mask中對應當前CPU的位被設置了 */
			if (cpumask_test_cpu(cpu, tick_broadcast_force_mask)) {
                                /* 返回-EBUSY先不讓休眠 */
				ret = -EBUSY;
			} else if (dev->next_event < bc->next_event) {
                                /* 如果當前要休眠CPU上的Tick設備到期時間早於Tick廣播設備到期時間 */
                                /* 用當前CPU上Tick設備的到期時間對Tick廣播設備重編程 */
				tick_broadcast_set_event(bc, cpu, dev->next_event);
				/* 再次判斷當前CPU是否能進入休眠狀態 */
				ret = broadcast_needs_cpu(bc, cpu);
				if (ret) {
                                        /* 如果不能清除tick_broadcast_oneshot_mask中當前CPU對應的位 */
					cpumask_clear_cpu(cpu,
						tick_broadcast_oneshot_mask);
				}
			}
		}
	} else {
                /* 要退出空閒狀態 */
		if (cpumask_test_and_clear_cpu(cpu, tick_broadcast_oneshot_mask)) {
                        /* 打開當前CPU的定時事件設備並切換到單次觸發模式 */
			clockevents_switch_state(dev, CLOCK_EVT_STATE_ONESHOT);
			/* 清除tick_broadcast_pending_mask中當前CPU對應的位 */
			if (cpumask_test_and_clear_cpu(cpu,
				       tick_broadcast_pending_mask))
                                /* 如果清除之前tick_broadcast_pending_mask中當前CPU對應的位已經被設置則直接退出 */
				goto out;

			/* 如果當前CPU上Tick設備的到期時間是KTIME_MAX則直接退出 */
			if (dev->next_event == KTIME_MAX)
				goto out;
			/* 獲得當前時間 */
			now = ktime_get();
                        /* 如果當前CPU上的Tick設備已經到期 */
			if (dev->next_event <= now) {
				cpumask_set_cpu(cpu, tick_broadcast_force_mask);
				goto out;
			}
			/* 對當前CPU上的Tick設備進行編程 */
			tick_program_event(dev->next_event, 1);
		}
	}
out:
	raw_spin_unlock(&tick_broadcast_lock);
	return ret;
}

tick_broadcast_pending_mask是一個全局變量,是在單次觸發模式事件處理函數tick_handle_oneshot_broadcast裏面設置的。在遍歷所有需要提供Tick廣播服務的CPU時,如果發現其定時事件設備已經到期了,就會設置tick_broadcast_pending_mask變量相應的位。這個變量主要用來處理這樣一種特殊的競態情況,所有到期的Tick設備,最終在tick_handle_oneshot_broadcast函數裏都會調用廣播函數對其發送誇處理器中斷(IPI)將其喚醒,但是這個時候如果要被喚醒的CPU突然被其它條件喚醒了後,會退出空閒狀態,如果它不知道可能後面會被Tick廣播層喚醒的話,就會立即執行到期處理函數,而後面到來的誇處理器中斷又會再讓它執行一次到期處理函數,就會把時間搞亂。所以,與其這樣還不如不做任何處理,等着下面的誇處理器中斷到來反正還要處理一次。因此,在當前CPU要退出空閒狀態時,如果發現了對應tick_broadcast_pending_mask上的位被設置了,就可以直接退出了,不需要再對當前CPU上的Tick設備進行編程。

tick_broadcast_force_mask也是一個全局變量,不過處理的情況跟前面剛好相反。這次是某個空閒的CPU先被其它條件喚醒了,它發現當前CPU上的Tick設備已經到期了,如果用當前時間對本CPU的Tick設備編程顯然不划算,還不如等Tick廣播設備的誇處理器中斷來處理。因爲,本CPU的Tick設備到期了,也就意味着Tick廣播設備馬上也要到期了。因此,在當前CPU進入空閒狀態時,如果檢查發現對應tick_broadcast_force_mask上的位被設置了,就知道馬上自己又要被激活了,如果現在進入休眠不划算,因此需要返回-EBUSY。

如果Tick廣播層還處在週期觸發模式,且當前的Tick廣播設備是由一個高分辨率定時器模擬的,那麼當嘗試進入空閒狀態時,都是返回-EBUSY,強制不讓進入,保證當前CPU上的Tick還是由自己負責。

broadcast_needs_cpu函數用來判斷當前的Tick廣播設備是否是一個用高分辨率定時器模擬的設備,並且這個設備是否是綁定在參數cpu指定的CPU上的:

static int broadcast_needs_cpu(struct clock_event_device *bc, int cpu)
{
        /* Tick廣播設備是否是用高分辨率定時器模擬的 */
	if (!(bc->features & CLOCK_EVT_FEAT_HRTIMER))
		return 0;
        /* 到期時間是KTIME_MAX意味着該設備可以被停止 */
	if (bc->next_event == KTIME_MAX)
		return 0;
        /* 模擬Tick廣播設備的高分辨率定時器是否綁定到指定的CPU上 */
	return bc->bound_on == cpu ? -EBUSY : 0;
}

如果條件都成立的話,仍然需要當前CPU爲系統中的其它處理器提供廣播服務,所以該函數將返回-EBUSY,表示本CPU忙,不能切換到空閒狀態。

broadcast_shutdown_local函數會根據Tick廣播設備和本CPU的定時事件設備的狀態,看看是否需要關閉本CPU的定時事件設備:

static void broadcast_shutdown_local(struct clock_event_device *bc,
				     struct clock_event_device *dev)
{
	/* 如果Tick廣播設備是由高分辨率定時器模擬的 */
	if (bc->features & CLOCK_EVT_FEAT_HRTIMER) {
                /* 是否還需要當前CPU繼續工作 */
		if (broadcast_needs_cpu(bc, smp_processor_id()))
			return;
                /* 當前CPU上定時事件設備的到期時間是否小於Tick廣播設備的到期時間 */
		if (dev->next_event < bc->next_event)
			return;
	}
	clockevents_switch_state(dev, CLOCK_EVT_STATE_SHUTDOWN);
}

如果Tick廣播設備不是高分辨率定時器模擬的話,很簡單直接關閉就好了。如果是的話,在兩種情況下不能關閉。一是,如果這個模擬Tick廣播的高分辨率定時器剛好運行在當前CPU上時,理由和前面一樣;二是,如果當前CPU上定時事件設備的到期時間早於Tick廣播設備的到期時間。第一種情況很好理解,那第二種情況又是爲什麼呢?這是因爲,如果當前CPU上定時事件設備的到期時間是否小於Tick廣播設備的到期時間,那麼在__tick_broadcast_oneshot_control函數後面會調用tick_broadcast_set_event函數,用這個時間對Tick廣播設備進行重編程,但這個重編程的設備又是通過高分辨率定時器模擬的,這樣就會將該高分辨率定時器遷移到當前CPU上,如果之前把它又關了,那就沒法再觸發Tick廣播了。

和工作在週期觸發模式不同,當Tick廣播層工作在單次觸發模式的時候,要等到真的當前CPU進入到會關閉本地定時事件設備的狀態時,纔會由Tick廣播層提供服務。

8)廣播的發送和接收

在前面提到過,系統中所有CPU上的Tick設備的broadcast函數都會被設置成tick_broadcast,這個函數是分平臺實現的,對於Arm64處理器來說,其實現如下(代碼位於arch/arm64/kernel/smp.c中):

#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
void tick_broadcast(const struct cpumask *mask)
{
	smp_cross_call(mask, IPI_TIMER);
}
#endif

可以看到,就是將IPI_TIMER的誇處理器中斷髮送給參數mask指定的CPU。

而當“遠端”的CPU收到了本CPU發送的誇處理器中斷後,會調用handle_IPI函數進行處理:

void handle_IPI(int ipinr, struct pt_regs *regs)
{
	unsigned int cpu = smp_processor_id();
	struct pt_regs *old_regs = set_irq_regs(regs);

	if ((unsigned)ipinr < NR_IPI) {
		trace_ipi_entry_rcuidle(ipi_types[ipinr]);
		__inc_irq_stat(cpu, ipi_irqs[ipinr]);
	}

	switch (ipinr) {
	......
#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
	case IPI_TIMER:
		irq_enter();
		tick_receive_broadcast();
		irq_exit();
		break;
#endif
    ......
	default:
		pr_crit("CPU%u: Unknown IPI message 0x%x\n", cpu, ipinr);
		break;
	}

	if ((unsigned)ipinr < NR_IPI)
		trace_ipi_exit_rcuidle(ipi_types[ipinr]);
	set_irq_regs(old_regs);
}

可以看到,對於IPI_TIMER誇處理器中斷,其處理函數是tick_receive_broadcast:

#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
int tick_receive_broadcast(void)
{
	struct tick_device *td = this_cpu_ptr(&tick_cpu_device);
	struct clock_event_device *evt = td->evtdev;

	if (!evt)
		return -ENODEV;

	if (!evt->event_handler)
		return -EINVAL;

	evt->event_handler(evt);
	return 0;
}
#endif

這下就簡單了,直接找到當前CPU上的定時事件設備,然後直接調用它的中斷處理函數就可以了,彷彿就是由這個定時事件設備自己觸發的定時中斷一樣。需要注意的是,收到了誇處理器中斷的請求後,並不會打開當前CPU上的定時事件設備。

9)基於高分辨率定時器的Tick廣播設備

前面提到過,如果Tick層要切換到單次觸發模式,或者高分辨率定時器層要切換到高精度模式,對於定時事件設備來說,必須要滿足下面兩個條件:

  1. 本地CPU的定時事件設備必須支持單次觸發模式;
  2. 如果本地CPU的定時事件設備支持C3_STOP模式,那麼還必須要求Tick廣播設備支持單次觸發模式。

但是,如果本地CPU是支持單次觸發模式的,且爲了節電的目的也支持C3_STOP模式,不過全系統中沒有任何共享的定時事件設備支持單次觸發模式怎麼辦?永遠也切換不到更高級別的模式上去了。Linux內核想到了一個辦法,讓系統中的某一個CPU不進入休眠模式,它的定時事件設備保持打開的狀態,讓它爲系統中的其它CPU服務,去喚醒其它休眠的CPU。具體的操作方式是系統設計了一個用來模擬Tick廣播設備的高分辨率定時器,其初始化代碼如下(代碼位於kernel/time/tick-broadcast-hrtimer.c中):

void tick_setup_hrtimer_broadcast(void)
{
        /* 初始化該高分辨率定時器 */
	hrtimer_init(&bctimer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS_HARD);
        /* 將該高分辨率定時器的處理函數設置成bc_handler */
	bctimer.function = bc_handler;
        /* 註冊模擬的定時事件設備 */
	clockevents_register_device(&ce_broadcast_hrtimer);
}

bctimer是一個全局變量,表示模擬Tick廣播的那個高分辨率定時器:

static struct hrtimer bctimer;

這個高分辨率定時器的處理函數被設置成了bc_handler:

static enum hrtimer_restart bc_handler(struct hrtimer *t)
{
	ce_broadcast_hrtimer.event_handler(&ce_broadcast_hrtimer);

	return HRTIMER_NORESTART;
}

所以,定時器到期了之後會驅動模擬出來的一個定時事件設備ce_broadcast_hrtimer,定義如下:

static struct clock_event_device ce_broadcast_hrtimer = {
	.name			= "bc_hrtimer",
	.set_state_shutdown	= bc_shutdown,
	.set_next_ktime		= bc_set_next,
	.features		= CLOCK_EVT_FEAT_ONESHOT |
				  CLOCK_EVT_FEAT_KTIME |
				  CLOCK_EVT_FEAT_HRTIMER,
	.rating			= 0,
	.bound_on		= -1,
	.min_delta_ns		= 1,
	.max_delta_ns		= KTIME_MAX,
	.min_delta_ticks	= 1,
	.max_delta_ticks	= ULONG_MAX,
	.mult			= 1,
	.shift			= 0,
	.cpumask		= cpu_possible_mask,
};

這個通過高分辨率定時器模擬出來的定時事件設備只支持單次觸發模式,不支持週期觸發模式。CLOCK_EVT_FEAT_HRTIMER唯一表示這個定時事件設備是用高分辨率定時器模擬的,不是真的物理設備,可以通過判斷這個特徵對其進行特殊處理。它的cpumask是系統中所有存在的CPU,不是和某個CPU綁定的,因此在註冊的時候不會被當前CPU給截獲掉,而是隻能用來作爲Tick廣播設備使用。該設備的觸發時間設置函數被設置成了bc_set_next:

static int bc_set_next(ktime_t expires, struct clock_event_device *bc)
{
	RCU_NONIDLE( {
                /* 激活用來模擬Tick廣播設備的高分辨率定時器 */
		hrtimer_start(&bctimer, expires, HRTIMER_MODE_ABS_PINNED_HARD);
		/* 更新綁定的CPU */
		bc->bound_on = bctimer.base->cpu_base->cpu;
	} );
	return 0;
}

注意,這個挑選出來不休眠的CPU不是一直不變的,由於激活高分辨率定時器的時候有可能會發生定時器的遷移,系統中任何一個CPU都可能被挑選出來,但每次系統中只會有一個CPU不能休眠。由於會發生遷移,因此在激活之後要更新設備的bound_on參數。

如果當前Tick廣播設備是高分辨率定時器模擬的,且Tick廣播層處於週期觸發模式,那麼各個CPU上的Tick都還是由自己負責,哪怕其支持C3_STOP模式,自己的定時事件設備永遠不會被停掉,CPU也不會徹底休眠。

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