Linux時間子系統之四:定時器的引擎:clock_event_device

早期的內核版本中,進程的調度基於一個稱之爲tick的時鐘滴答,通常使用時鐘中斷來定時地產生tick信號,每次tick定時中斷都會進行進程的統計和調度,並對tick進行計數,記錄在一個jiffies變量中,定時器的設計也是基於jiffies。這時候的內核代碼中,幾乎所有關於時鐘的操作都是在machine級的代碼中實現,很多公共的代碼要在每個平臺上重複實現。隨後,隨着通用時鐘框架的引入,內核需要支持高精度的定時器,爲此,通用時間框架爲定時器硬件定義了一個標準的接口:clock_event_device,machine級的代碼只要按這個標準接口實現相應的硬件控制功能,剩下的與平臺無關的特性則統一由通用時間框架層來實現。

/*****************************************************************************************************/

聲明:本博內容均由http://blog.csdn.net/droidphone原創,轉載請註明出處,謝謝!

/*****************************************************************************************************/

1.  時鐘事件軟件架構

本系列文章的第一節中,我們曾經討論了時鐘源設備:clocksource,現在又來一個時鐘事件設備:clock_event_device,它們有何區別?看名字,好像都是給系統提供時鐘的設備,實際上,clocksource不能被編程,沒有產生事件的能力,它主要被用於timekeeper來實現對真實時間進行精確的統計,而clock_event_device則是可編程的,它可以工作在週期觸發或單次觸發模式,系統可以對它進行編程,以確定下一次事件觸發的時間,clock_event_device主要用於實現普通定時器和高精度定時器,同時也用於產生tick事件,供給進程調度子系統使用。時鐘事件設備與通用時間框架中的其他模塊的關係如下圖所示:


                                                                  圖1.1   clock_event_device軟件架構

  • 與clocksource一樣,系統中可以存在多個clock_event_device,系統會根據它們的精度和能力,選擇合適的clock_event_device對系統提供時鐘事件服務。在smp系統中,爲了減少處理器間的通信開銷,基本上每個cpu都會具備一個屬於自己的本地clock_event_device,獨立地爲該cpu提供時鐘事件服務,smp中的每個cpu基於本地的clock_event_device,建立自己的tick_device,普通定時器和高精度定時器。
  • 在軟件架構上看,clock_event_device被分爲了兩層,與硬件相關的被放在了machine層,而與硬件無關的通用代碼則被集中到了通用時間框架層,這符合內核對軟件的設計需求,平臺的開發者只需實現平臺相關的接口即可,無需關注複雜的上層時間框架。
  • tick_device是基於clock_event_device的進一步封裝,用於代替原有的時鐘滴答中斷,給內核提供tick事件,以完成進程的調度和進程信息統計,負載平衡和時間更新等操作。

2.  時鐘事件設備相關數據結構

2.1  struct clock_event_device

時鐘事件設備的核心數據結構是clock_event_device結構,它代表着一個時鐘硬件設備,該設備就好像是一個具有事件觸發能力(通常就是指中斷)的clocksource,它不停地計數,當計數值達到預先編程設定的數值那一刻,會引發一個時鐘事件中斷,繼而觸發該設備的事件處理回調函數,以完成對時鐘事件的處理。clock_event_device結構的定義如下:

  1. struct clock_event_device {  
  2.     void            (*event_handler)(struct clock_event_device *);  
  3.     int         (*set_next_event)(unsigned long evt,  
  4.                           struct clock_event_device *);  
  5.     int         (*set_next_ktime)(ktime_t expires,  
  6.                           struct clock_event_device *);  
  7.     ktime_t         next_event;  
  8.     u64         max_delta_ns;  
  9.     u64         min_delta_ns;  
  10.     u32         mult;  
  11.     u32         shift;  
  12.     enum clock_event_mode   mode;  
  13.     unsigned int        features;  
  14.     unsigned long       retries;  
  15.   
  16.     void            (*broadcast)(const struct cpumask *mask);  
  17.     void            (*set_mode)(enum clock_event_mode mode,  
  18.                         struct clock_event_device *);  
  19.     unsigned long       min_delta_ticks;  
  20.     unsigned long       max_delta_ticks;  
  21.   
  22.     const char      *name;  
  23.     int         rating;  
  24.     int         irq;  
  25.     const struct cpumask    *cpumask;  
  26.     struct list_head    list;  
  27. } ____cacheline_aligned;  

event_handler  該字段是一個回調函數指針,通常由通用框架層設置,在時間中斷到來時,machine底層的的中斷服務程序會調用該回調,框架層利用該回調實現對時鐘事件的處理。

set_next_event  設置下一次時間觸發的時間,使用類似於clocksource的cycle計數值(離現在的cycle差值)作爲參數。

set_next_ktime  設置下一次時間觸發的時間,直接使用ktime時間作爲參數。

max_delta_ns  可設置的最大時間差,單位是納秒。

min_delta_ns  可設置的最小時間差,單位是納秒。

mult shift  與clocksource中的類似,只不過是用於把納秒轉換爲cycle。

mode  該時鐘事件設備的工作模式,兩種主要的工作模式分別是:

  • CLOCK_EVT_MODE_PERIODIC  週期觸發模式,設置後按給定的週期不停地觸發事件;
  • CLOCK_EVT_MODE_ONESHOT  單次觸發模式,只在設置好的觸發時刻觸發一次;

set_mode  函數指針,用於設置時鐘事件設備的工作模式。

rating  表示該設備的精度等級。

list  系統中註冊的時鐘事件設備用該字段掛在全局鏈表變量clockevent_devices上。

2.2  全局變量clockevent_devices

系統中所有註冊的clock_event_device都會掛在該鏈表下面,它在kernel/time/clockevents.c中定義:
  1. static LIST_HEAD(clockevent_devices);  

2.3  全局變量clockevents_chain

通用時間框架初始化時會註冊一個通知鏈(NOTIFIER),當系統中的時鐘時間設備的狀態發生變化時,利用該通知鏈通知系統的其它模塊。
  1. /* Notification for clock events */  
  2. static RAW_NOTIFIER_HEAD(clockevents_chain);  

3.  clock_event_device的初始化和註冊

每一個machine,都要定義一個自己的machine_desc結構,該結構定義了該machine的一些最基本的特性,其中需要設定一個sys_timer結構指針,machine級的代碼負責定義sys_timer結構,sys_timer的聲明很簡單:
  1. struct sys_timer {  
  2.     void            (*init)(void);  
  3.     void            (*suspend)(void);  
  4.     void            (*resume)(void);  
  5. #ifdef CONFIG_ARCH_USES_GETTIMEOFFSET  
  6.     unsigned long       (*offset)(void);  
  7. #endif  
  8. };  
通常,我們至少要定義它的init字段,系統初始化階段,該init回調會被調用,該init回調函數的主要作用就是完成系統中的clocksource和clock_event_device的硬件初始化工作,以samsung的exynos4爲例,在V3.4內核的代碼樹中,machine_desc的定義如下:
  1. MACHINE_START(SMDK4412, "SMDK4412")  
  2.     /* Maintainer: Kukjin Kim <[email protected]> */  
  3.     /* Maintainer: Changhwan Youn <[email protected]> */  
  4.     .atag_offset    = 0x100,  
  5.     .init_irq   = exynos4_init_irq,  
  6.     .map_io     = smdk4x12_map_io,  
  7.     .handle_irq = gic_handle_irq,  
  8.     .init_machine   = smdk4x12_machine_init,  
  9.     .timer      = &exynos4_timer,  
  10.     .restart    = exynos4_restart,  
  11. MACHINE_END  
定義的sys_timer是exynos4_timer,它的定義和init回調定義如下:
  1. static void __init exynos4_timer_init(void)  
  2. {  
  3.     if (soc_is_exynos4210())  
  4.         mct_int_type = MCT_INT_SPI;  
  5.     else  
  6.         mct_int_type = MCT_INT_PPI;  
  7.   
  8.     exynos4_timer_resources();  
  9.     exynos4_clocksource_init();  
  10.     exynos4_clockevent_init();  
  11. }  
  12.   
  13. struct sys_timer exynos4_timer = {  
  14.     .init       = exynos4_timer_init,  
  15. };  
exynos4_clockevent_init函數顯然是初始化和註冊clock_event_device的合適時機,在這裏,它註冊了一個rating爲250的clock_event_device,並把它指定給cpu0:
  1. static struct clock_event_device mct_comp_device = {  
  2.     .name       = "mct-comp",  
  3.     .features       = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT,  
  4.     .rating     = 250,  
  5.     .set_next_event = exynos4_comp_set_next_event,  
  6.     .set_mode   = exynos4_comp_set_mode,  
  7. };  
  8. ......  
  9. static void exynos4_clockevent_init(void)  
  10. {  
  11.     clockevents_calc_mult_shift(&mct_comp_device, clk_rate, 5);  
  12.         ......  
  13.     mct_comp_device.cpumask = cpumask_of(0);  
  14.     clockevents_register_device(&mct_comp_device);  
  15.   
  16.     setup_irq(EXYNOS4_IRQ_MCT_G0, &mct_comp_event_irq);  
  17. }  
因爲這個階段其它cpu核尚未開始工作,所以該clock_event_device也只是在啓動階段給系統提供服務,實際上,因爲exynos4是一個smp系統,具備2-4個cpu核心,前面說過,smp系統中,通常會使用各個cpu的本地定時器來爲每個cpu單獨提供時鐘事件服務,繼續翻閱代碼,在系統初始化的後段,kernel_init會被調用,它會調用smp_prepare_cpus,其中會調用percpu_timer_setup函數,在arch/arm/kernel/smp.c中,爲每個cpu定義了一個clock_event_device:
  1. /* 
  2.  * Timer (local or broadcast) support 
  3.  */  
  4. static DEFINE_PER_CPU(struct clock_event_device, percpu_clockevent);  
percpu_timer_setup最終會調用exynos4_local_timer_setup函數完成對本地clock_event_device的初始化工作:
  1. static int __cpuinit exynos4_local_timer_setup(struct clock_event_device *evt)  
  2. {  
  3.     ......  
  4.     evt->name = mevt->name;  
  5.     evt->cpumask = cpumask_of(cpu);  
  6.     evt->set_next_event = exynos4_tick_set_next_event;  
  7.     evt->set_mode = exynos4_tick_set_mode;  
  8.     evt->features = CLOCK_EVT_FEAT_PERIODIC | CLOCK_EVT_FEAT_ONESHOT;  
  9.     evt->rating = 450;  
  10.   
  11.     clockevents_calc_mult_shift(evt, clk_rate / (TICK_BASE_CNT + 1), 5);  
  12.     ......  
  13.     clockevents_register_device(evt);  
  14.     ......  
  15.     enable_percpu_irq(EXYNOS_IRQ_MCT_LOCALTIMER, 0);  
  16.     ......  
  17.     return 0;  
  18. }  
由此可見,每個cpu的本地clock_event_device的rating是450,比啓動階段的250要高,顯然,之前註冊給cpu0的精度要高,系統會用本地clock_event_device替換掉原來分配給cpu0的clock_event_device,至於怎麼替換?我們先停一停,到這裏我們一直在討論machine級別的初始化和註冊,讓我們回過頭來,看看框架層的初始化。在繼續之前,讓我們看看整個clock_event_device的初始化的調用序列圖:


                                                                                           圖3.1  clock_event_device的系統初始化

由上面的圖示可以看出,框架層的初始化步驟很簡單,又start_kernel開始,調用tick_init,它位於kernel/time/tick-common.c中,也只是簡單地調用clockevents_register_notifier,同時把類型爲notifier_block的tick_notifier作爲參數傳入,回看2.3節,clockevents_register_notifier註冊了一個通知鏈,這樣,當系統中的clock_event_device狀態發生變化時(新增,刪除,掛起,喚醒等等),tick_notifier中的notifier_call字段中設定的回調函數tick_notify就會被調用。接下來start_kernel調用了time_init函數,該函數通常定義在體系相關的代碼中,正如前面所討論的一樣,它主要完成machine級別對時鐘系統的初始化工作,最終通過clockevents_register_device註冊系統中的時鐘事件設備,把每個時鐘時間設備掛在clockevent_device全局鏈表上,最後通過clockevent_do_notify觸發框架層事先註冊好的通知鏈,其實就是調用了tick_notify函數,我們主要關注CLOCK_EVT_NOTIFY_ADD通知,其它通知請自行參考代碼,下面是tick_notify的簡化版本:

  1. static int tick_notify(struct notifier_block *nb, unsigned long reason,  
  2.                    void *dev)  
  3. {  
  4.     switch (reason) {  
  5.   
  6.     case CLOCK_EVT_NOTIFY_ADD:  
  7.         return tick_check_new_device(dev);  
  8.   
  9.     case CLOCK_EVT_NOTIFY_BROADCAST_ON:  
  10.     case CLOCK_EVT_NOTIFY_BROADCAST_OFF:  
  11.     case CLOCK_EVT_NOTIFY_BROADCAST_FORCE:  
  12.             ......  
  13.     case CLOCK_EVT_NOTIFY_BROADCAST_ENTER:  
  14.     case CLOCK_EVT_NOTIFY_BROADCAST_EXIT:  
  15.             ......  
  16.     case CLOCK_EVT_NOTIFY_CPU_DYING:  
  17.             ......  
  18.     case CLOCK_EVT_NOTIFY_CPU_DEAD:  
  19.             ......  
  20.     case CLOCK_EVT_NOTIFY_SUSPEND:  
  21.             ......  
  22.     case CLOCK_EVT_NOTIFY_RESUME:  
  23.             ......  
  24.     }  
  25.   
  26.     return NOTIFY_OK;  
  27. }  
可見,對於新註冊的clock_event_device,會發出CLOCK_EVT_NOTIFY_ADD通知,最終會進入函數:tick_check_new_device,這個函數比對當前cpu所使用的與新註冊的clock_event_device之間的特性,如果認爲新的clock_event_device更好,則會進行切換工作。下一節將會詳細的討論該函數。到這裏,每個cpu已經有了自己的clock_event_device,在這以後,框架層的代碼會根據內核的配置項(CONFIG_NO_HZ、CONFIG_HIGH_RES_TIMERS),對註冊的clock_event_device進行不同的設置,從而爲系統的tick和高精度定時器提供服務,這些內容我們留在本系列的後續文章進行討論。

4.  tick_device

當內核沒有配置成支持高精度定時器時,系統的tick由tick_device產生,tick_device其實是clock_event_device的簡單封裝,它內嵌了一個clock_event_device指針和它的工作模式:
  1. struct tick_device {  
  2.     struct clock_event_device *evtdev;  
  3.     enum tick_device_mode mode;  
  4. };  
在kernel/time/tick-common.c中,定義了一個per-cpu的tick_device全局變量,tick_cpu_device:
  1. /* 
  2.  * Tick devices 
  3.  */  
  4. DEFINE_PER_CPU(struct tick_device, tick_cpu_device);  
前面曾經說過,當machine的代碼爲每個cpu註冊clock_event_device時,通知回調函數tick_notify會被調用,進而進入tick_check_new_device函數,下面讓我們看看該函數如何工作,首先,該函數先判斷註冊的clock_event_device是否可用於本cpu,然後從per-cpu變量中取出本cpu的tick_device:
  1. static int tick_check_new_device(struct clock_event_device *newdev)  
  2. {  
  3.         ......  
  4.     cpu = smp_processor_id();  
  5.     if (!cpumask_test_cpu(cpu, newdev->cpumask))  
  6.         goto out_bc;  
  7.   
  8.     td = &per_cpu(tick_cpu_device, cpu);  
  9.     curdev = td->evtdev;  
如果不是本地clock_event_device,會做進一步的判斷:如果不能把irq綁定到本cpu,則放棄處理,如果本cpu已經有了一個本地clock_event_device,也放棄處理:
  1. if (!cpumask_equal(newdev->cpumask, cpumask_of(cpu))) {  
  2.                ......  
  3.     if (!irq_can_set_affinity(newdev->irq))  
  4.         goto out_bc;  
  5.                ......  
  6.     if (curdev && cpumask_equal(curdev->cpumask, cpumask_of(cpu)))  
  7.         goto out_bc;  
  8. }  
反之,如果本cpu已經有了一個clock_event_device,則根據是否支持單觸發模式和它的rating值,決定是否替換原來舊的clock_event_device:
  1. if (curdev) {  
  2.     if ((curdev->features & CLOCK_EVT_FEAT_ONESHOT) &&  
  3.         !(newdev->features & CLOCK_EVT_FEAT_ONESHOT))  
  4.         goto out_bc;  // 新的不支持單觸發,但舊的支持,所以不能替換  
  5.     if (curdev->rating >= newdev->rating)  
  6.         goto out_bc;  // 舊的比新的精度高,不能替換  
  7. }  
在這些判斷都通過之後,說明或者來cpu還沒有綁定tick_device,或者是新的更好,需要替換:
  1. if (tick_is_broadcast_device(curdev)) {  
  2.     clockevents_shutdown(curdev);  
  3.     curdev = NULL;  
  4. }  
  5. clockevents_exchange_device(curdev, newdev);  
  6. tick_setup_device(td, newdev, cpu, cpumask_of(cpu));  
上面的tick_setup_device函數負責重新綁定當前cpu的tick_device和新註冊的clock_event_device,如果發現是當前cpu第一次註冊tick_device,就把它設置爲TICKDEV_MODE_PERIODIC模式,如果是替換舊的tick_device,則根據新的tick_device的特性,設置爲TICKDEV_MODE_PERIODIC或TICKDEV_MODE_ONESHOT模式。可見,在系統的啓動階段,tick_device是工作在週期觸發模式的,直到框架層在合適的時機,纔會開啓單觸發模式,以便支持NO_HZ和HRTIMER。

5.  tick事件的處理--最簡單的情況

clock_event_device最基本的應用就是實現tick_device,然後給系統定期地產生tick事件,通用時間框架對clock_event_device和tick_device的處理相當複雜,因爲涉及配置項:CONFIG_NO_HZ和CONFIG_HIGH_RES_TIMERS的組合,兩個配置項就有4種組合,這四種組合的處理都有所不同,所以這裏我先只討論最簡單的情況:
  • CONFIG_NO_HZ == 0;
  • CONFIG_HIGH_RES_TIMERS == 0;
在這種配置模式下,我們回到上一節的tick_setup_device函數的最後:
  1. if (td->mode == TICKDEV_MODE_PERIODIC)  
  2.     tick_setup_periodic(newdev, 0);  
  3. else  
  4.     tick_setup_oneshot(newdev, handler, next_event);  
因爲啓動期間,第一個註冊的tick_device必然工作在TICKDEV_MODE_PERIODIC模式,所以tick_setup_periodic會設置clock_event_device的事件回調字段event_handler爲tick_handle_periodic,工作一段時間後,就算有新的支持TICKDEV_MODE_ONESHOT模式的clock_event_device需要替換,再次進入tick_setup_device函數,tick_setup_oneshot的handler參數也是之前設置的tick_handle_periodic函數,所以我們考察tick_handle_periodic即可:
  1. void tick_handle_periodic(struct clock_event_device *dev)  
  2. {  
  3.     int cpu = smp_processor_id();  
  4.     ktime_t next;  
  5.   
  6.     tick_periodic(cpu);  
  7.   
  8.     if (dev->mode != CLOCK_EVT_MODE_ONESHOT)  
  9.         return;  
  10.   
  11.     next = ktime_add(dev->next_event, tick_period);  
  12.     for (;;) {  
  13.         if (!clockevents_program_event(dev, next, false))  
  14.             return;  
  15.         if (timekeeping_valid_for_hres())  
  16.             tick_periodic(cpu);  
  17.         next = ktime_add(next, tick_period);  
  18.     }  
  19. }  
該函數首先調用tick_periodic函數,完成tick事件的所有處理,如果是週期觸發模式,處理結束,如果工作在單觸發模式,則計算並設置下一次的觸發時刻,這裏用了一個循環,是爲了防止當該函數被調用時,clock_event_device中的計時實際上已經經過了不止一個tick週期,這時候,tick_periodic可能被多次調用,使得jiffies和時間可以被正確地更新。tick_periodic的代碼如下:
  1. static void tick_periodic(int cpu)  
  2. {  
  3.     if (tick_do_timer_cpu == cpu) {  
  4.         write_seqlock(&xtime_lock);  
  5.   
  6.         /* Keep track of the next tick event */  
  7.         tick_next_period = ktime_add(tick_next_period, tick_period);  
  8.   
  9.         do_timer(1);  
  10.         write_sequnlock(&xtime_lock);  
  11.     }  
  12.   
  13.     update_process_times(user_mode(get_irq_regs()));  
  14.     profile_tick(CPU_PROFILING);  
  15. }  
如果當前cpu負責更新時間,則通過do_timer進行以下操作:
  • 更新jiffies_64變量;
  • 更新牆上時鐘;
  • 每10個tick,更新一次cpu的負載信息;
調用update_peocess_times,完成以下事情:
  • 更新進程的時間統計信息;
  • 觸發TIMER_SOFTIRQ軟件中斷,以便系統處理傳統的低分辨率定時器;
  • 檢查rcu的callback;
  • 通過scheduler_tick觸發調度系統進行進程統計和調度工作;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章