1. 程式人生 > >Linux時間子系統(十二) periodic tick

Linux時間子系統(十二) periodic tick

比較 efi test 例如 當前 替代 void 其中 有意

一、tick device概念介紹

1、數據結構

在內核中,使用struct tick_device來抽象系統中的tick設備,如下:

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

從上面的定義就可以看出:所謂tick device其實就是工作在某種模式下的clock event設備。工作模式體現在tick device的mode成員,evtdev指向了和該tick device關聯的clock event設備。

tick device的工作模式定義如下:

enum tick_device_mode {
TICKDEV_MODE_PERIODIC,
TICKDEV_MODE_ONESHOT,
};

tick device可以工作在兩種模式下,一種是周期性tick模式,另外一種是one shot模式。one shot模式主要和tickless系統以及高精度timer有關,會在另外的文檔中描述,本文主要介紹periodic mode。

2、tick device的分類以及和CPU的關系

(1) local tick device。在單核系統中,傳統的unix都是在tick驅動下進行任務調度、低精度timer觸發等,在多核架構下,系統為每一個cpu建立了一個tick device,如下:

DEFINE_PER_CPU(struct tick_device, tick_cpu_device);

local tick device的clock event device應該具備下面的特點:

(a)該clock event device對應的HW timer必須是和該CPU core是有關聯的的(也就是說,該hw timer的中斷是可以送達到該CPU core的)。struct clock_event_device 有一個cpumask成員,它可以指示該clock event device為哪一個或者哪幾個CPU core工作。如果采用ARM generic timer的硬件,其HW timer總是為一個CPU core服務的,我們稱之為per cpu timer。

(b)該clock event device支持one shot模式,並且精度最高(rating最大)

(2)global tick device。具體定義如下:

int tick_do_timer_cpu __read_mostly = TICK_DO_TIMER_BOOT;

有些任務不適合在local tick device中處理,例如更新jiffies,更新系統的wall time,更新系統的平均負載(不是單一CPU core的負載),這些都是系統級別的任務,只需要在local tick device中選擇一個作為global tick device就OK了。tick_do_timer_cpu指明哪一個cpu上的local tick作為global tick。

(3)broadcast tick device,定義如下:

static struct tick_device tick_broadcast_device;

我們會單獨一份文檔描述它,這裏就不再描述了。

二、初始化tick device

1、註冊一個新的clock event device的時候,tick device layer要做什麽?

在clock event device的文章中,我們知道:底層的timer硬件驅動在初始化的時候會註冊clock event device,在註冊過程中就會調用tick_check_new_device函數來看看是否需要進行tick device的初始化,如果已經已經初始化OK的tick device是否有更換更高精度clock event device的需求。代碼如下:

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

cpu = smp_processor_id();---------------------------(1)
if (!cpumask_test_cpu(cpu, newdev->cpumask)) goto out_bc;

td = &per_cpu(tick_cpu_device, cpu);---獲取當前cpu的tick device
curdev = td->evtdev; ---目前tick device正在使用的clock event device

if (!tick_check_percpu(curdev, newdev, cpu))-------------------(2)
goto out_bc;

if (!tick_check_preferred(curdev, newdev))--------------------(3)
goto out_bc;

if (!try_module_get(newdev->owner)) -----增加新設備的reference count
return;


if (tick_is_broadcast_device(curdev)) { ----------------------(4)
clockevents_shutdown(curdev);
curdev = NULL;
}
clockevents_exchange_device(curdev, newdev); ---通知clockevent layer
tick_setup_device(td, newdev, cpu, cpumask_of(cpu)); ---------------(5)
if (newdev->features & CLOCK_EVT_FEAT_ONESHOT) ---其他文檔中描述
tick_oneshot_notify();
return;

out_bc:
tick_install_broadcast_device(newdev); ----其他文檔中描述
}

(1)是否是為本CPU服務的clock event device?如果不是,那麽不需要考慮per cpu tick device的初始化或者更換該cpu tick device的clock event device。當然,這是還是可以考慮用在broadcast tick device的。

(2)第二個關卡是per cpu的檢查。如果檢查不通過,那麽說明這個新註冊的clock event device和該CPU不來電,不能用於該cpu的local tick。如果註冊的hw timer都是cpu local的(僅僅屬於一個cpu,這時候該clock event device的cpumask只有一個bit被set),那麽事情會比較簡單。然而,事情往往沒有那麽簡單,一個hw timer可以服務多個cpu。我們這裏說HW timer服務於某個cpu其實最重要的是irq是否可以分發到指定的cpu上。我們可以看看tick_check_percpu的實現:

static bool tick_check_percpu(struct clock_event_device *curdev,
struct clock_event_device *newdev, int cpu)
{
if (!cpumask_test_cpu(cpu, newdev->cpumask))-------------------(a)
return false;
if (cpumask_equal(newdev->cpumask, cpumask_of(cpu)))---------------(b)
return true;
if (newdev->irq >= 0 && !irq_can_set_affinity(newdev->irq))--------------(c)
return false;
if (curdev && cpumask_equal(curdev->cpumask, cpumask_of(cpu)))----------(d)
return false;
return true;
}

(a)判斷這個新註冊的clock event device是否可以服務該CPU,如果它根本不鳥這個cpu那麽不用浪費時間了。

(b)判斷這個新註冊的clock event device是否只服務該CPU。如果這個clock event device就是服務該cpu的,那麽別想三想四了,這個clock event device就是你這個CPU的人了。

(c)如果能走到這裏,說明該clock event device可以服務多個CPU,指定的cpu(作為參數傳遞進來)只是其中之一而已,這時候,可以通過設定irq affinity將該clock event device的irq定向到該cpu。當前,前提是可以進行irq affinity的設定,這裏就是進行這樣的檢查。

(d)走到這裏,說明該新註冊的clock event device是可以進行irq affinity設定的。我們可以通過修改irq affinity讓該hw timer服務於這個指定的CPU。恩,聽起來有些麻煩,的確如此,如果當前CPU的tick device正在使用的clock event device就是special for當前CPU的(根本不鳥其他CPU),有如此專情的clock event device,夫復何求,果斷拒絕新註冊的設備。

(3)程序來到這裏,說明tick_check_percpu返回true,CPU和該clock event device之間的已經是眉目傳情了,不過是否可以入主,就看該cpu的原配是否有足夠強大的能力(精度和特性)。tick_check_preferred代碼如下:

static bool tick_check_preferred(struct clock_event_device *curdev,
struct clock_event_device *newdev)
{
if (!(newdev->features & CLOCK_EVT_FEAT_ONESHOT)) {--------------(a)
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);-------------(b)
}

(a)首先進行one shot能力比拼。如果新的clock event device沒有one shot能力而原配有,新歡失敗。如果都沒有one shot的能力,那麽要看看當前系統是否啟用了高精度timer或者tickless。本質上,如果clock event device沒有oneshot功能,那麽高精度timer或者tickless都是處於委曲求全的狀態,如果這樣,還是維持原配的委曲求全的狀態,新歡失敗

(b)如果current是NULL的話,事情變得非常簡單,當然是新來的這個clock event device勝出了(這時候,後面的比較都沒有意義了)。如果原配存在的話,那麽可以看rating,如果新來的精度高,那也選擇新來的clock event device。是否精度低就一定不選新的呢?也不是,新設備還是有機會力挽狂瀾的:如果新來的是local timer,而原配是非local timer,這時候,也可以考慮選擇新的,畢竟新來的clock event device是local timer,精度低一些也沒有關系。

當tick_check_percpu返回true的時候有兩種情況:一種是不管current是什麽狀態,新設備是CPU的local timer(只為這個cpu服務)。另外一種情況是新設備不是CPU的local timer,當然原配也沒有那麽專一。

我們先看看第一種情況:如果cpumask_equal返回true,那麽說明原配也是local timer,那麽沒有辦法了,誰的rating高就選誰。如果cpumask_equal返回false,那麽說明原配不是local timer,那麽即便新來的rating低一些也還是優先選擇local timer。

我們再看看第二種情況:這裏我絕對邏輯有問題,不知道是代碼的問題還是我還沒有考慮清楚,先TODO吧。

(4)OK,經過復雜的檢查,我們終於決定要用這個新註冊的clock event device來替代current了(當然,也有可能current根本不存在)。在進行替換之前,我們還有檢查一下current是否是broadcast tick device,如果是的話,還不能將其退回clockevents layer,僅僅是設定其狀態為shutdown。curdev = NULL這一句很重要,在clockevents_exchange_device函數中,如果curdev == NULL的話,old device將不會從全局鏈表中摘下,掛入clockevents_released鏈表。

(5)setup tick device,參考下一節描述。

2、如何Setup 一個 tick device?

所謂setup一個tick device就是對tick device心儀的clock event設備進行設置,並將該tick device的evtdev指向新註冊的這個clock event device,具體代碼如下:

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

if (!td->evtdev) {
if (tick_do_timer_cpu == TICK_DO_TIMER_BOOT) {--------------(1)
……
}

td->mode = TICKDEV_MODE_PERIODIC;------------------(2)
} else {
handler = td->evtdev->event_handler;
next_event = td->evtdev->next_event;
td->evtdev->event_handler = clockevents_handle_noop; ------------(3)
}

td->evtdev = newdev; -----終於修成正果了,呵呵

if (!cpumask_equal(newdev->cpumask, cpumask)) ---------------(4)
irq_set_affinity(newdev->irq, cpumask);

if (tick_device_uses_broadcast(newdev, cpu)) -------留給broadcast tick文檔吧
return;

if (td->mode == TICKDEV_MODE_PERIODIC)
tick_setup_periodic(newdev, 0); ----------------------(5)
else
tick_setup_oneshot(newdev, handler, next_event); -----其他文檔描述
}

(1)在multi core的環境下,每一個CPU core都自己的tick device(可以稱之local tick device),這些tick device中有一個被選擇做global tick device,負責維護整個系統的jiffies。如果該tick device的是第一次設定,並且目前系統中沒有global tick設備,那麽可以考慮選擇該tick設備作為global設備,進行系統時間和jiffies的更新。更細節的內容請參考timekeeping文檔。

(2)在最初設定tick device的時候,缺省被設定為周期性的tick。當然,這僅僅是初始設定,實際上在滿足一定的條件下,在適當的時間,tick device是可以切換到其他模式的,下面會具體描述。

(3)舊的clockevent設備就要退居二線了,將其handler修改為clockevents_handle_noop。

(4)如果不是local timer,那麽還需要調用irq_set_affinity函數,將該clockevent的中斷,定向到本CPU。

(5)tick_setup_periodic的代碼如下(註:下面的代碼分析中暫不考慮broadcast tick的情況):

void tick_setup_periodic(struct clock_event_device *dev, int broadcast)
{
tick_set_periodic_handler(dev, broadcast); ----設定event handler為tick_handle_periodic

if ((dev->features & CLOCK_EVT_FEAT_PERIODIC) && !tick_broadcast_oneshot_active()) {
clockevents_set_mode(dev, CLOCK_EVT_MODE_PERIODIC);---------(a)
} else {
unsigned long seq;
ktime_t next;

do {
seq = read_seqbegin(&jiffies_lock);
next = tick_next_period; -----獲取下一個周期性tick觸發的時間
} while (read_seqretry(&jiffies_lock, seq));

clockevents_set_mode(dev, CLOCK_EVT_MODE_ONESHOT); ---模式設定

for (;;) {
if (!clockevents_program_event(dev, next, false)) ----program next clock event
return;
next = ktime_add(next, tick_period); ------計算下一個周期性tick觸發的時間
}
}
}

(a)如果底層的clock event device支持periodic模式,那麽直接調用clockevents_set_mode設定模式就OK了

(b)如果底層的clock event device不支持periodic模式,而tick device目前是周期性tick mode,那麽要稍微復雜一些,需要用clock event device的one shot模式來實現周期性tick。

三、周期性tick的運作

1、從中斷到clock event handler

一般而言,底層的clock event chip driver會註冊中斷,我們用ARM generic timer驅動為例,註冊的代碼如下:

err = request_percpu_irq(ppi, arch_timer_handler_phys, "arch_timer", arch_timer_evt);

……

具體的timer的中斷handler如下:

static irqreturn_t arch_timer_handler_phys_mem(int irq, void *dev_id)
{

……
evt->event_handler(evt);
……


}

也就是說,在timer interrupt handler中會調用clock event device的event handler,而在周期性tick的場景下,這個event handler被設定為tick_handle_periodic。

2、周期性tick的clock event handler的執行分析

由於每個cpu都有自己的tick device,因此,在每個cpu上,每個tick到了的時候,都會調用tick_handle_periodic函數進行周期性tick中要處理的task,具體如下:

void tick_handle_periodic(struct clock_event_device *dev)
{
int cpu = smp_processor_id();
ktime_t next;

tick_periodic(cpu); ----周期性tick中要處理的內容,參考下節描述

if (dev->mode != CLOCK_EVT_MODE_ONESHOT)
return;

next = ktime_add(dev->next_event, tick_period);----計算下一個周期性tick觸發的時間
for (;;) {
if (!clockevents_program_event(dev, next, false))---設定下一個clock event觸發的時間
return;

if (timekeeping_valid_for_hres())------在其他文檔中描述
tick_periodic(cpu);
next = ktime_add(next, tick_period);
}
}

如果該tick device所屬的clock event device工作在one shot mode,那麽還需要為產生周期性tick而進行一些額外處理。

2、周期性tick中要處理的內容

代碼如下:

static void tick_periodic(int cpu)
{
if (tick_do_timer_cpu == cpu) {----global tick需要進行一些額外處理
write_seqlock(&jiffies_lock);
tick_next_period = ktime_add(tick_next_period, tick_period);

do_timer(1);-------------更新jiffies,計算平均負載
write_sequnlock(&jiffies_lock);
update_wall_time();----------更新wall time
}

update_process_times(user_mode(get_irq_regs()));----更新和當前進程相關的內容
profile_tick(CPU_PROFILING);------和性能剖析相關,不詳述了
}

Linux時間子系統(十二) periodic tick