1. 程式人生 > >Linux時間子系統之六:高精度定時器(HRTIMER)的原理和實現

Linux時間子系統之六:高精度定時器(HRTIMER)的原理和實現

3.4 size 屬於 running return repr 而是 復雜度 ctu

上一篇文章,我介紹了傳統的低分辨率定時器的實現原理。而隨著內核的不斷演進,大牛們已經對這種低分辨率定時器的精度不再滿足,而且,硬件也在不斷地發展,系統中的定時器硬件的精度也越來越高,這也給高分辨率定時器的出現創造了條件。內核從2.6.16開始加入了高精度定時器架構。在實現方式上,內核的高分辨率定時器的實現代碼幾乎沒有借用低分辨率定時器的數據結構和代碼,內核文檔給出的解釋主要有以下幾點:

  • 低分辨率定時器的代碼和jiffies的關系太過緊密,並且默認按32位進行設計,並且它的代碼已經經過長時間的優化,目前的使用也是沒有任何錯誤,如果硬要基於它來實現高分辨率定時器,勢必會打破原有的時間輪概念,並且會引入一大堆#if--#else判斷;
  • 雖然大部分時間裏,時間輪可以實現O(1)時間復雜度,但是當有進位發生時,不可預測的O(N)定時器級聯遷移時間,這對於低分辨率定時器來說問題不大,可是它大大地影響了定時器的精度;
  • 低分辨率定時器幾乎是為“超時”而設計的,並為此對它進行了大量的優化,對於這些以“超時”未目的而使用定時器,它們大多數期望在超時到來之前獲得正確的結果,然後刪除定時器,精確時間並不是它們主要的目的,例如網絡通信、設備IO等等。

為此,內核為高精度定時器重新設計了一套軟件架構,它可以為我們提供納秒級的定時精度,以滿足對精確時間有迫切需求的應用程序或內核驅動,例如多媒體應用,音頻設備的驅動程序等等。以下的討論用hrtimer(high resolution timer)表示高精度定時器。
/*****************************************************************************************************/
聲明:本博內容均由http://blog.csdn.NET/droidphone原創,轉載請註明出處,謝謝!
/*****************************************************************************************************/

1. 如何組織hrtimer?

我們知道,低分辨率定時器使用5個鏈表數組來組織timer_list結構,形成了著名的時間輪概念,對於高分辨率定時器,我們期望組織它們的數據結構至少具備以下條件:

  • 穩定而且快速的查找能力;
  • 快速地插入和刪除定時器的能力;
  • 排序功能;

內核的開發者考察了多種數據結構,例如基數樹、哈希表等等,最終他們選擇了紅黑樹(rbtree)來組織hrtimer,紅黑樹已經以庫的形式存在於內核中,並被成功地使用在內存管理子系統和文件系統中,隨著系統的運行,hrtimer不停地被創建和銷毀,新的hrtimer按順序被插入到紅黑樹中,樹的最左邊的節點就是最快到期的定時器,內核用一個hrtimer結構來表示一個高精度定時器:

[cpp] view plain copy

  1. struct hrtimer {
  2. struct timerqueue_node node;
  3. ktime_t _softexpires;
  4. enum hrtimer_restart (*function)(struct hrtimer *);
  5. struct hrtimer_clock_base *base;
  6. unsigned long state;
  7. ......
  8. };

定時器的到期時間用ktime_t來表示,_softexpires字段記錄了時間,定時器一旦到期,function字段指定的回調函數會被調用,該函數的返回值為一個枚舉值,它決定了該hrtimer是否需要被重新激活:

[cpp] view plain copy

  1. enum hrtimer_restart {
  2. HRTIMER_NORESTART, /* Timer is not restarted */
  3. HRTIMER_RESTART, /* Timer must be restarted */
  4. };

state字段用於表示hrtimer當前的狀態,有幾下幾種位組合:

[cpp] view plain copy

  1. #define HRTIMER_STATE_INACTIVE 0x00 // 定時器未激活
  2. #define HRTIMER_STATE_ENQUEUED 0x01 // 定時器已經被排入紅黑樹中
  3. #define HRTIMER_STATE_CALLBACK 0x02 // 定時器的回調函數正在被調用
  4. #define HRTIMER_STATE_MIGRATE 0x04 // 定時器正在CPU之間做遷移

hrtimer的到期時間可以基於以下幾種時間基準系統:

[cpp] view plain copy

  1. enum hrtimer_base_type {
  2. HRTIMER_BASE_MONOTONIC, // 單調遞增的monotonic時間,不包含休眠時間
  3. HRTIMER_BASE_REALTIME, // 平常使用的墻上真實時間
  4. HRTIMER_BASE_BOOTTIME, // 單調遞增的boottime,包含休眠時間
  5. HRTIMER_MAX_CLOCK_BASES, // 用於後續數組的定義
  6. };

和低分辨率定時器一樣,處於效率和上鎖的考慮,每個cpu單獨管理屬於自己的hrtimer,為此,專門定義了一個結構hrtimer_cpu_base:

[cpp] view plain copy

  1. struct hrtimer_cpu_base {
  2. ......
  3. struct hrtimer_clock_base clock_base[HRTIMER_MAX_CLOCK_BASES];
  4. };

其中,clock_base數組為每種時間基準系統都定義了一個hrtimer_clock_base結構,它的定義如下:

[cpp] view plain copy

  1. struct hrtimer_clock_base {
  2. struct hrtimer_cpu_base *cpu_base; // 指向所屬cpu的hrtimer_cpu_base結構
  3. ......
  4. struct timerqueue_head active; // 紅黑樹,包含了所有使用該時間基準系統的hrtimer
  5. ktime_t resolution; // 時間基準系統的分辨率
  6. ktime_t (*get_time)(void); // 獲取該基準系統的時間函數
  7. ktime_t softirq_time;// 當用jiffies
  8. ktime_t offset; //
  9. };

active字段是一個timerqueue_head結構,它實際上是對rbtree的進一步封裝:

[cpp] view plain copy

  1. struct timerqueue_node {
  2. struct rb_node node; // 紅黑樹的節點
  3. ktime_t expires; // 該節點代表隊hrtimer的到期時間,與hrtimer結構中的_softexpires稍有不同
  4. };
  5. struct timerqueue_head {
  6. struct rb_root head; // 紅黑樹的根節點
  7. struct timerqueue_node *next; // 該紅黑樹中最早到期的節點,也就是最左下的節點
  8. };

timerqueue_head結構在紅黑樹的基礎上,增加了一個next字段,用於保存樹中最先到期的定時器節點,實際上就是樹的最左下方的節點,有了next字段,當到期事件到來時,系統不必遍歷整個紅黑樹,只要取出next字段對應的節點進行處理即可。timerqueue_node用於表示一個hrtimer節點,它在標準紅黑樹節點rb_node的基礎上增加了expires字段,該字段和hrtimer中的_softexpires字段一起,設定了hrtimer的到期時間的一個範圍,hrtimer可以在hrtimer._softexpires至timerqueue_node.expires之間的任何時刻到期,我們也稱timerqueue_node.expires為硬過期時間(hard),意思很明顯:到了此時刻,定時器一定會到期,有了這個範圍可以選擇,定時器系統可以讓範圍接近的多個定時器在同一時刻同時到期,這種設計可以降低進程頻繁地被hrtimer進行喚醒。經過以上的討論,我們可以得出以下的圖示,它表明了每個cpu上的hrtimer是如何被組織在一起的:

技術分享

圖 1.1 每個cpu的hrtimer組織結構

總結一下:

  • 每個cpu有一個hrtimer_cpu_base結構;
  • hrtimer_cpu_base結構管理著3種不同的時間基準系統的hrtimer,分別是:實時時間,啟動時間和單調時間;
  • 每種時間基準系統通過它的active字段(timerqueue_head結構指針),指向它們各自的紅黑樹;
  • 紅黑樹上,按到期時間進行排序,最先到期的hrtimer位於最左下的節點,並被記錄在active.next字段中;
  • 3中時間基準的最先到期時間可能不同,所以,它們之中最先到期的時間被記錄在hrtimer_cpu_base的expires_next字段中。

2. hrtimer如何運轉

hrtimer的實現需要一定的硬件基礎,它的實現依賴於我們前幾章介紹的timekeeper和clock_event_device,如果你對timekeeper和clock_event_device不了解請參考以下文章:Linux時間子系統之三:時間的維護者:timekeeper,Linux時間子系統之四:定時器的引擎:clock_event_device。hrtimer系統需要通過timekeeper獲取當前的時間,計算與到期時間的差值,並根據該差值,設定該cpu的tick_device(clock_event_device)的下一次的到期時間,時間一到,在clock_event_device的事件回調函數中處理到期的hrtimer。現在你或許有疑問:前面在介紹clock_event_device時,我們知道,每個cpu有自己的tick_device,通常用於周期性地產生進程調度和時間統計的tick事件,這裏又說要用tick_device調度hrtimer系統,通常cpu只有一個tick_device,那他們如何協調工作?這個問題也一度困擾著我,如果再加上NO_HZ配置帶來tickless特性,你可能會更暈。這裏我們先把這個疑問放下,我將在後面的章節中來討論這個問題,現在我們只要先知道,一旦開啟了hrtimer,tick_device所關聯的clock_event_device的事件回調函數會被修改為:hrtimer_interrupt,並且會被設置成工作於CLOCK_EVT_MODE_ONESHOT單觸發模式。

2.1 添加一個hrtimer

要添加一個hrtimer,系統提供了一些api供我們使用,首先我們需要定義一個hrtimer結構的實例,然後用hrtimer_init函數對它進行初始化,它的原型如下:

[cpp] view plain copy

  1. void hrtimer_init(struct hrtimer *timer, clockid_t which_clock,
  2. enum hrtimer_mode mode);

which_clock可以是CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_BOOTTIME中的一種,mode則可以是相對時間HRTIMER_MODE_REL,也可以是絕對時間HRTIMER_MODE_ABS。設定回調函數:

[cpp] view plain copy

  1. timer.function = hr_callback;

如果定時器無需指定一個到期範圍,可以在設定回調函數後直接使用hrtimer_start激活該定時器:

[cpp] view plain copy

  1. int hrtimer_start(struct hrtimer *timer, ktime_t tim,
  2. const enum hrtimer_mode mode);

如果需要指定到期範圍,則可以使用hrtimer_start_range_ns激活定時器:

[cpp] view plain copy

  1. hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,
  2. unsigned long range_ns, const enum hrtimer_mode mode);

要取消一個hrtimer,使用hrtimer_cancel:

[cpp] view plain copy

  1. int hrtimer_cancel(struct hrtimer *timer);

以下兩個函數用於推後定時器的到期時間:

[cpp] view plain copy

  1. extern u64
  2. hrtimer_forward(struct hrtimer *timer, ktime_t now, ktime_t interval);
  3. /* Forward a hrtimer so it expires after the hrtimer‘s current now */
  4. static inline u64 hrtimer_forward_now(struct hrtimer *timer,
  5. ktime_t interval)
  6. {
  7. return hrtimer_forward(timer, timer->base->get_time(), interval);
  8. }

以下幾個函數用於獲取定時器的當前狀態:

[cpp] view plain copy

  1. static inline int hrtimer_active(const struct hrtimer *timer)
  2. {
  3. return timer->state != HRTIMER_STATE_INACTIVE;
  4. }
  5. static inline int hrtimer_is_queued(struct hrtimer *timer)
  6. {
  7. return timer->state & HRTIMER_STATE_ENQUEUED;
  8. }
  9. static inline int hrtimer_callback_running(struct hrtimer *timer)
  10. {
  11. return timer->state & HRTIMER_STATE_CALLBACK;
  12. }

hrtimer_init最終會進入__hrtimer_init函數,該函數的主要目的是初始化hrtimer的base字段,同時初始化作為紅黑樹的節點的node字段:

[cpp] view plain copy

  1. static void __hrtimer_init(struct hrtimer *timer, clockid_t clock_id,
  2. enum hrtimer_mode mode)
  3. {
  4. struct hrtimer_cpu_base *cpu_base;
  5. int base;
  6. memset(timer, 0, sizeof(struct hrtimer));
  7. cpu_base = &__raw_get_cpu_var(hrtimer_bases);
  8. if (clock_id == CLOCK_REALTIME && mode != HRTIMER_MODE_ABS)
  9. clock_id = CLOCK_MONOTONIC;
  10. base = hrtimer_clockid_to_base(clock_id);
  11. timer->base = &cpu_base->clock_base[base];
  12. timerqueue_init(&timer->node);
  13. ......
  14. }

hrtimer_start和hrtimer_start_range_ns最終會把實際的工作交由__hrtimer_start_range_ns來完成:

[cpp] view plain copy

  1. int __hrtimer_start_range_ns(struct hrtimer *timer, ktime_t tim,
  2. unsigned long delta_ns, const enum hrtimer_mode mode,
  3. int wakeup)
  4. {
  5. ......
  6. /* 取得hrtimer_clock_base指針 */
  7. base = lock_hrtimer_base(timer, &flags);
  8. /* 如果已經在紅黑樹中,先移除它: */
  9. ret = remove_hrtimer(timer, base); ......
  10. /* 如果是相對時間,則需要加上當前時間,因為內部是使用絕對時間 */
  11. if (mode & HRTIMER_MODE_REL) {
  12. tim = ktime_add_safe(tim, new_base->get_time());
  13. ......
  14. }
  15. /* 設置到期的時間範圍 */
  16. hrtimer_set_expires_range_ns(timer, tim, delta_ns);
  17. ......
  18. /* 把hrtime按到期時間排序,加入到對應時間基準系統的紅黑樹中 */
  19. /* 如果該定時器的是最早到期的,將會返回true */
  20. leftmost = enqueue_hrtimer(timer, new_base);
  21. /*
  22. * Only allow reprogramming if the new base is on this CPU.
  23. * (it might still be on another CPU if the timer was pending)
  24. *
  25. * XXX send_remote_softirq() ?
  26. * 定時器比之前的到期時間要早,所以需要重新對tick_device進行編程,重新設定的的到期時間
  27. */
  28. if (leftmost && new_base->cpu_base == &__get_cpu_var(hrtimer_bases))
  29. hrtimer_enqueue_reprogram(timer, new_base, wakeup);
  30. unlock_hrtimer_base(timer, &flags);
  31. return ret;
  32. }
  33. <p>
  34. </p>
2.2 hrtimer的到期處理

高精度定時器系統有3個入口可以對到期定時器進行處理,它們分別是:

  • 沒有切換到高精度模式時,在每個jiffie的tick事件中斷中進行查詢和處理;
  • 在HRTIMER_SOFTIRQ軟中斷中進行查詢和處理;
  • 切換到高精度模式後,在每個clock_event_device的到期事件中斷中進行查詢和處理;

低精度模式 因為系統並不是一開始就會支持高精度模式,而是在系統啟動後的某個階段,等待所有的條件都滿足後,才會切換到高精度模式,當系統還沒有切換到高精度模式時,所有的高精度定時器運行在低精度模式下,在每個jiffie的tick事件中斷中進行到期定時器的查詢和處理,顯然這時候的精度和低分辨率定時器是一樣的(HZ級別)。低精度模式下,每個tick事件中斷中,hrtimer_run_queues函數會被調用,由它完成定時器的到期處理。hrtimer_run_queues首先判斷目前高精度模式是否已經啟用,如果已經切換到了高精度模式,什麽也不做,直接返回:

[cpp] view plain copy

  1. void hrtimer_run_queues(void)
  2. {
  3. if (hrtimer_hres_active())
  4. return;

如果hrtimer_hres_active返回false,說明目前處於低精度模式下,則繼續處理,它用一個for循環遍歷各個時間基準系統,查詢每個hrtimer_clock_base對應紅黑樹的左下節點,判斷它的時間是否到期,如果到期,通過__run_hrtimer函數,對到期定時器進行處理,包括:調用定時器的回調函數、從紅黑樹中移除該定時器、根據回調函數的返回值決定是否重新啟動該定時器等等:

[cpp] view plain copy

  1. for (index = 0; index < HRTIMER_MAX_CLOCK_BASES; index++) {
  2. base = &cpu_base->clock_base[index];
  3. if (!timerqueue_getnext(&base->active))
  4. continue;
  5. if (gettime) {
  6. hrtimer_get_softirq_time(cpu_base);
  7. gettime = 0;
  8. }
  9. raw_spin_lock(&cpu_base->lock);
  10. while ((node = timerqueue_getnext(&base->active))) {
  11. struct hrtimer *timer;
  12. timer = container_of(node, struct hrtimer, node);
  13. if (base->softirq_time.tv64 <=
  14. hrtimer_get_expires_tv64(timer))
  15. break;
  16. __run_hrtimer(timer, &base->softirq_time);
  17. }
  18. raw_spin_unlock(&cpu_base->lock);
  19. }

上面的timerqueue_getnext函數返回紅黑樹中的左下節點,之所以可以在while循環中使用該函數,是因為__run_hrtimer會在移除舊的左下節點時,新的左下節點會被更新到base->active->next字段中,使得循環可以繼續執行,直到沒有新的到期定時器為止。
高精度模式 切換到高精度模式後,原來給cpu提供tick事件的tick_device(clock_event_device)會被高精度定時器系統接管,它的中斷事件回調函數被設置為hrtimer_interrupt,紅黑樹中最左下的節點的定時器的到期時間被編程到該clock_event_device中,這樣每次clock_event_device的中斷意味著至少有一個高精度定時器到期。另外,當timekeeper系統中的時間需要修正,或者clock_event_device的到期事件時間被重新編程時,系統會發出HRTIMER_SOFTIRQ軟中斷,軟中斷的處理函數run_hrtimer_softirq最終也會調用hrtimer_interrupt函數對到期定時器進行處理,所以在這裏我們只要討論hrtimer_interrupt函數的實現即可。

hrtimer_interrupt函數的前半部分和低精度模式下的hrtimer_run_queues函數完成相同的事情:它用一個for循環遍歷各個時間基準系統,查詢每個hrtimer_clock_base對應紅黑樹的左下節點,判斷它的時間是否到期,如果到期,通過__run_hrtimer函數,對到期定時器進行處理,所以我們只討論後半部分,在處理完所有到期定時器後,下一個到期定時器的到期時間保存在變量expires_next中,接下來的工作就是把這個到期時間編程到tick_device中:

[cpp] view plain copy

  1. void hrtimer_interrupt(struct clock_event_device *dev)
  2. {
  3. ......
  4. for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++) {
  5. ......
  6. while ((node = timerqueue_getnext(&base->active))) {
  7. ......
  8. if (basenow.tv64 < hrtimer_get_softexpires_tv64(timer)) {
  9. ktime_t expires;
  10. expires = ktime_sub(hrtimer_get_expires(timer),
  11. base->offset);
  12. if (expires.tv64 < expires_next.tv64)
  13. expires_next = expires;
  14. break;
  15. }
  16. __run_hrtimer(timer, &basenow);
  17. }
  18. }
  19. /*
  20. * Store the new expiry value so the migration code can verify
  21. * against it.
  22. */
  23. cpu_base->expires_next = expires_next;
  24. raw_spin_unlock(&cpu_base->lock);
  25. /* Reprogramming necessary ? */
  26. if (expires_next.tv64 == KTIME_MAX ||
  27. !tick_program_event(expires_next, 0)) {
  28. cpu_base->hang_detected = 0;
  29. return;
  30. }

如果這時的tick_program_event返回了非0值,表示過期時間已經在當前時間的前面,這通常由以下原因造成:

  • 系統正在被調試跟蹤,導致時間在走,程序不走;
  • 定時器的回調函數花了太長的時間;
  • 系統運行在虛擬機中,而虛擬機被調度導致停止運行;
為了避免這些情況的發生,接下來系統提供3次機會,重新執行前面的循環,處理到期的定時器:

[cpp] view plain copy

  1. raw_spin_lock(&cpu_base->lock);
  2. now = hrtimer_update_base(cpu_base);
  3. cpu_base->nr_retries++;
  4. if (++retries < 3)
  5. goto retry;

如果3次循環後還無法完成到期處理,系統不再循環,轉為計算本次總循環的時間,然後把tick_device的到期時間強制設置為當前時間加上本次的總循環時間,不過推後的時間被限制在100ms以內:

[cpp] view plain copy

  1. delta = ktime_sub(now, entry_time);
  2. if (delta.tv64 > cpu_base->max_hang_time.tv64)
  3. cpu_base->max_hang_time = delta;
  4. /*
  5. * Limit it to a sensible value as we enforce a longer
  6. * delay. Give the CPU at least 100ms to catch up.
  7. */
  8. if (delta.tv64 > 100 * NSEC_PER_MSEC)
  9. expires_next = ktime_add_ns(now, 100 * NSEC_PER_MSEC);
  10. else
  11. expires_next = ktime_add(now, delta);
  12. tick_program_event(expires_next, 1);
  13. printk_once(KERN_WARNING "hrtimer: interrupt took %llu ns\n",
  14. ktime_to_ns(delta));
  15. }

3. 切換到高精度模式

上面提到,盡管內核配置成支持高精度定時器,但並不是一開始就工作於高精度模式,系統在啟動的開始階段,還是按照傳統的模式在運行:tick_device按HZ頻率定期地產生tick事件,這時的hrtimer工作在低分辨率模式,到期事件在每個tick事件中斷中由hrtimer_run_queues函數處理,同時,在低分辨率定時器(時間輪)的軟件中斷TIMER_SOFTIRQ中,hrtimer_run_pending會被調用,系統在這個函數中判斷系統的條件是否滿足切換到高精度模式,如果條件滿足,則會切換至高分辨率模式,另外提一下,NO_HZ模式也是在該函數中判斷並切換。

[cpp] view plain copy

  1. void hrtimer_run_pending(void)
  2. {
  3. if (hrtimer_hres_active())
  4. return;
  5. ......
  6. if (tick_check_oneshot_change(!hrtimer_is_hres_enabled()))
  7. hrtimer_switch_to_hres();
  8. }

因為不管系統是否工作於高精度模式,每個TIMER_SOFTIRQ期間,該函數都會被調用,所以函數一開始先用hrtimer_hres_active判斷目前高精度模式是否已經激活,如果已經激活,則說明之前的調用中已經切換了工作模式,不必再次切換,直接返回。hrtimer_hres_active很簡單:

[cpp] view plain copy

  1. DEFINE_PER_CPU(struct hrtimer_cpu_base, hrtimer_bases) = {
  2. ......
  3. }
  4. static inline int hrtimer_hres_active(void)
  5. {
  6. return __this_cpu_read(hrtimer_bases.hres_active);
  7. }

hrtimer_run_pending函數接著通過tick_check_oneshot_change判斷系統是否可以切換到高精度模式,

[cpp] view plain copy

  1. int tick_check_oneshot_change(int allow_nohz)
  2. {
  3. struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);
  4. if (!test_and_clear_bit(0, &ts->check_clocks))
  5. return 0;
  6. if (ts->nohz_mode != NOHZ_MODE_INACTIVE)
  7. return 0;
  8. if (!timekeeping_valid_for_hres() || !tick_is_oneshot_available())
  9. return 0;
  10. if (!allow_nohz)
  11. return 1;
  12. tick_nohz_switch_to_nohz();
  13. return 0;
  14. }

函數的一開始先判斷check_clock標誌的第0位是否被置位,如果沒有置位,說明系統中沒有註冊符合要求的時鐘事件設備,函數直接返回,check_clock標誌由clocksource和clock_event_device系統的notify系統置位,當系統中有更高精度的clocksource被註冊和選擇後,或者有更精確的支持CLOCK_EVT_MODE_ONESHOT模式的clock_event_device被註冊時,通過它們的notify函數,check_clock標誌的第0為會置位。

如果tick_sched結構中的nohz_mode字段不是NOHZ_MODE_INACTIVE,表明系統已經切換到其它模式,直接返回。nohz_mode的取值有3種:

  • NOHZ_MODE_INACTIVE // 未啟用NO_HZ模式
  • NOHZ_MODE_LOWRES // 啟用NO_HZ模式,hrtimer工作於低精度模式下
  • NOHZ_MODE_HIGHRES // 啟用NO_HZ模式,hrtimer工作於高精度模式下
接下來的timerkeeping_valid_for_hres判斷timekeeper系統是否支持高精度模式,tick_is_oneshot_available判斷tick_device是否支持CLOCK_EVT_MODE_ONESHOT模式。如果都滿足要求,則繼續往下判斷。allow_nohz是函數的參數,為true表明可以切換到NOHZ_MODE_LOWRES 模式,函數將進入tick_nohz_switch_to_nohz,切換至NOHZ_MODE_LOWRES 模式,這裏我們傳入的allow_nohz是表達式:

(!hrtimer_is_hres_enabled())

所以當系統不允許高精度模式時,將會在tick_check_oneshot_change函數內,通過tick_nohz_switch_to_nohz切換至NOHZ_MODE_LOWRES 模式,如果系統允許高精度模式,傳入的allow_nohz參數為false,tick_check_oneshot_change函數返回1,回到上面的hrtimer_run_pending函數,hrtimer_switch_to_hres函數將會被調用,已完成切換到NOHZ_MODE_HIGHRES高精度模式。好啦,真正的切換函數找到了,我們看一看它如何切換:

首先,它通過hrtimer_cpu_base中的hres_active字段判斷該cpu是否已經切換至高精度模式,如果是則直接返回:

[cpp] view plain copy

  1. static int hrtimer_switch_to_hres(void)
  2. {
  3. int i, cpu = smp_processor_id();
  4. struct hrtimer_cpu_base *base = &per_cpu(hrtimer_bases, cpu);
  5. unsigned long flags;
  6. if (base->hres_active)
  7. return 1;

接著,通過tick_init_highres函數接管tick_device關聯的clock_event_device:

[cpp] view plain copy

  1. local_irq_save(flags);
  2. if (tick_init_highres()) {
  3. local_irq_restore(flags);
  4. printk(KERN_WARNING "Could not switch to high resolution "
  5. "mode on CPU %d\n", cpu);
  6. return 0;
  7. }

tick_init_highres函數把tick_device切換到CLOCK_EVT_FEAT_ONESHOT模式,同時把clock_event_device的回調handler設置為hrtimer_interrupt,這樣設置以後,tick_device的中斷回調將由hrtimer_interrupt接管,hrtimer_interrupt在上面已經討論過,它將完成高精度定時器的調度和到期處理。

接著,設置hres_active標誌,以表明高精度模式已經切換,然後把3個時間基準系統的resolution字段設為KTIME_HIGH_RES:

[cpp] view plain copy

  1. base->hres_active = 1;
  2. for (i = 0; i < HRTIMER_MAX_CLOCK_BASES; i++)
  3. base->clock_base[i].resolution = KTIME_HIGH_RES;

最後,因為tick_device被高精度定時器接管,它將不會再提供原有的tick事件機制,所以需要由高精度定時器系統模擬一個tick事件設備,繼續為系統提供tick事件能力,這個工作由tick_setup_sched_timer函數完成。因為剛剛完成切換,tick_device的到期時間並沒有被正確地設置為下一個到期定時器的時間,這裏使用retrigger_next_event函數,傳入參數NULL,使得tick_device立刻產生到期中斷,hrtimer_interrupt被調用一次,然後下一個到期的定時器的時間會編程到tick_device中,從而完成了到高精度模式的切換:

[cpp] view plain copy

  1. tick_setup_sched_timer();
  2. /* "Retrigger" the interrupt to get things going */
  3. retrigger_next_event(NULL);
  4. local_irq_restore(flags);
  5. return 1;

整個切換過程可以用下圖表示:

技術分享

圖3.1 低精度模式切換至高精度模式

4. 模擬tick事件

根據上一節的討論,當系統切換到高精度模式後,tick_device被高精度定時器系統接管,不再定期地產生tick事件,我們知道,到目前的版本為止(V3.4),內核還沒有徹底廢除jiffies機制,系統還是依賴定期到來的tick事件,供進程調度系統和時間更新等操作,大量存在的低精度定時器也仍然依賴於jiffies的計數,所以,盡管tick_device被接管,高精度定時器系統還是要想辦法繼續提供定期的tick事件。為了達到這一目的,內核使用了一個取巧的辦法:既然高精度模式已經啟用,可以定義一個hrtimer,把它的到期時間設定為一個jiffy的時間,當這個hrtimer到期時,在這個hrtimer的到期回調函數中,進行和原來的tick_device同樣的操作,然後把該hrtimer的到期時間順延一個jiffy周期,如此反復循環,完美地模擬了原有tick_device的功能。下面我們看看具體點代碼是如何實現的。

在kernel/time/tick-sched.c中,內核定義了一個per_cpu全局變量:tick_cpu_sched,從而為每個cpu提供了一個tick_sched結構, 該結構主要用於管理NO_HZ配置下的tickless處理,因為模擬tick事件與tickless有很強的相關性,所以高精度定時器系統也利用了該結構的以下字段,用於完成模擬tick事件的操作:

[cpp] view plain copy

  1. struct tick_sched {
  2. struct hrtimer sched_timer;
  3. unsigned long check_clocks;
  4. enum tick_nohz_mode nohz_mode;
  5. ......
  6. };

sched_timer就是要用於模擬tick事件的hrtimer,check_clock上面幾節已經討論過,用於notify系統通知hrtimer系統需要檢查是否切換到高精度模式,nohz_mode則用於表示當前的工作模式。

上一節提到,用於切換至高精度模式的函數是hrtimer_switch_to_hres,在它的最後,調用了函數tick_setup_sched_timer,該函數的作用就是設置一個用於模擬tick事件的hrtimer:

[cpp] view plain copy

  1. void tick_setup_sched_timer(void)
  2. {
  3. struct tick_sched *ts = &__get_cpu_var(tick_cpu_sched);
  4. ktime_t now = ktime_get();
  5. /*
  6. * Emulate tick processing via per-CPU hrtimers:
  7. */
  8. hrtimer_init(&ts->sched_timer, CLOCK_MONOTONIC, HRTIMER_MODE_ABS);
  9. ts->sched_timer.function = tick_sched_timer;
  10. /* Get the next period (per cpu) */
  11. hrtimer_set_expires(&ts->sched_timer, tick_init_jiffy_update());
  12. for (;;) {
  13. hrtimer_forward(&ts->sched_timer, now, tick_period);
  14. hrtimer_start_expires(&ts->sched_timer,
  15. HRTIMER_MODE_ABS_PINNED);
  16. /* Check, if the timer was already in the past */
  17. if (hrtimer_active(&ts->sched_timer))
  18. break;
  19. now = ktime_get();
  20. }
  21. #ifdef CONFIG_NO_HZ
  22. if (tick_nohz_enabled)
  23. ts->nohz_mode = NOHZ_MODE_HIGHRES;
  24. #endif
  25. }

該函數首先初始化該cpu所屬的tick_sched結構中sched_timer字段,把該hrtimer的回調函數設置為tick_sched_timer,然後把它的到期時間設定為下一個jiffy時刻,返回前把工作模式設置為NOHZ_MODE_HIGHRES,表明是利用高精度模式實現NO_HZ。

接著我們關註一下hrtimer的回調函數tick_sched_timer,我們知道,系統中的jiffies計數,時間更新等是全局操作,在smp系統中,只有一個cpu負責該工作,所以在tick_sched_timer的一開始,先判斷當前cpu是否負責更新jiffies和時間,如果是,則執行更新操作:

[cpp] view plain copy

  1. static enum hrtimer_restart tick_sched_timer(struct hrtimer *timer)
  2. {
  3. ......
  4. #ifdef CONFIG_NO_HZ
  5. if (unlikely(tick_do_timer_cpu == TICK_DO_TIMER_NONE))
  6. tick_do_timer_cpu = cpu;
  7. #endif
  8. /* Check, if the jiffies need an update */
  9. if (tick_do_timer_cpu == cpu)
  10. tick_do_update_jiffies64(now);

然後,利用regs指針確保當前是在中斷上下文中,然後調用update_process_timer:

[cpp] view plain copy

  1. if (regs) {
  2. ......
  3. update_process_times(user_mode(regs));
  4. ......
  5. }

最後,把hrtimer的到期時間推進一個tick周期,返回HRTIMER_RESTART表明該hrtimer需要再次啟動,以便產生下一個tick事件。

[cpp] view plain copy

  1. hrtimer_forward(timer, now, tick_period);
  2. return HRTIMER_RESTART;
  3. }

關於update_process_times,如果你你感興趣,回看一下本系列關於clock_event_device的那一章:Linux時間子系統之四:定時器的引擎:clock_event_device中的第5小節,對比一下模擬tick事件的hrtimer的回調函數tick_sched_timer和切換前tick_device的回調函數tick_handle_periodic,它們是如此地相像,實際上,它們幾乎完成了一樣的工作。

Linux時間子系統之六:高精度定時器(HRTIMER)的原理和實現