1. 程式人生 > >Linux時間子系統之三:時間的維護者:timekeeper

Linux時間子系統之三:時間的維護者:timekeeper

本系列文章的前兩節討論了用於計時的時鐘源:clocksource,以及核心內部時間的一些表示方法,但是對於真實的使用者來說,我們感知的是真實世界的真實時間,也就是所謂的牆上時間,clocksource只能提供一個按給定頻率不停遞增的週期計數,如何把它和真實的牆上時間相關聯?本節的內容正是要討論這一點。

1.  時間的種類

核心管理著多種時間,它們分別是:

  • RTC時間
  • wall time:牆上時間
  • monotonic time
  • raw monotonic time
  • boot time:總啟動時間

RTC時間  在PC中,RTC時間又叫CMOS時間,它通常由一個專門的計時硬體來實現,軟體可以讀取該硬體來獲得年月日、時分秒等時間資訊,而在嵌入式系統中,有使用專門的RTC晶片,也有直接把RTC整合到Soc晶片中,讀取Soc中的某個暫存器即可獲取當前時間資訊。一般來說,RTC是一種可持續計時的,也就是說,不管系統是否上電,RTC中的時間資訊都不會丟失,計時會一直持續進行,硬體上通常使用一個後備電池對RTC硬體進行單獨的供電。因為RTC硬體的多樣性,開發者需要為每種RTC時鐘硬體提供相應的驅動程式,核心和使用者空間通過驅動程式訪問RTC硬體來獲取或設定時間資訊。

xtime  xtime和RTC時間一樣,都是人們日常所使用的牆上時間,只是RTC時間的精度通常比較低,大多數情況下只能達到毫秒級別的精度,如果是使用外部的RTC晶片,訪問速度也比較慢,為此,核心維護了另外一個wall time時間:xtime,取決於用於對xtime計時的clocksource,它的精度甚至可以達到納秒級別,因為xtime實際上是一個記憶體中的變數,它的訪問速度非常快,核心大部分時間都是使用xtime來獲得當前時間資訊。xtime記錄的是自1970年1月1日24時到當前時刻所經歷的納秒數。

monotonic time  該時間自系統開機後就一直單調地增加,它不像xtime可以因使用者的調整時間而產生跳變,不過該時間不計算系統休眠的時間,也就是說,系統休眠時,monotoic時間不會遞增。

raw monotonic time  該時間與monotonic時間類似,也是單調遞增的時間,唯一的不同是:raw monotonic time“更純淨”,他不會受到NTP時間調整的影響,它代表著系統獨立時鐘硬體對時間的統計。

boot time  與monotonic時間相同,不過會累加上系統休眠的時間,它代表著系統上電後的總時間。

時間種類 精度(統計單位) 訪問速度 累計休眠時間 受NTP調整的影響
RTC Yes Yes
xtime Yes Yes
monotonic No Yes
raw monotonic No No
boot time Yes Yes

2.  struct timekeeper

核心用timekeeper結構來組織與時間相關的資料,它的定義如下:

  1. struct timekeeper {  
  2.     struct clocksource *clock;    /* Current clocksource used for timekeeping. */
  3.     u32 mult;    /* NTP adjusted clock multiplier */
  4.     int shift;  /* The shift value of the current clocksource. */
  5.     cycle_t cycle_interval; /* Number of clock cycles in one NTP interval. */
  6.     u64 xtime_interval; /* Number of clock shifted nano seconds in one NTP interval. */
  7.     s64 xtime_remainder;    /* shifted nano seconds left over when rounding cycle_interval */
  8.     u32 raw_interval;   /* Raw nano seconds accumulated per NTP interval. */
  9.     u64 xtime_nsec; /* Clock shifted nano seconds remainder not stored in xtime.tv_nsec. */
  10.     /* Difference between accumulated time and NTP time in ntp 
  11.      * shifted nano seconds. */
  12.     s64 ntp_error;  
  13.     /* Shift conversion between clock shifted nano seconds and 
  14.      * ntp shifted nano seconds. */
  15.     int ntp_error_shift;  
  16.     struct timespec xtime;  /* The current time */
  17.     struct timespec wall_to_monotonic;  
  18.     struct timespec total_sleep_time;   /* time spent in suspend */
  19.     struct timespec raw_time;   /* The raw monotonic time for the CLOCK_MONOTONIC_RAW posix clock. */
  20.     ktime_t offs_real;  /* Offset clock monotonic -> clock realtime */
  21.     ktime_t offs_boot;  /* Offset clock monotonic -> clock boottime */
  22.     seqlock_t lock; /* Seqlock for all timekeeper values */
  23. };  
其中的xtime欄位就是上面所說的牆上時間,它是一個timespec結構的變數,它記錄了自1970年1月1日以來所經過的時間,因為是timespec結構,所以它的精度可以達到納秒級,當然那要取決於系統的硬體是否支援這一精度。

核心除了用xtime表示牆上的真實時間外,還維護了另外一個時間:monotonic time,可以把它理解為自系統啟動以來所經過的時間,該時間只能單調遞增,可以理解為xtime雖然正常情況下也是遞增的,但是畢竟使用者可以主動向前或向後調整牆上時間,從而修改xtime值。但是monotonic時間不可以往後退,系統啟動後只能不斷遞增。奇怪的是,核心並沒有直接定義一個這樣的變數來記錄monotonic時間,而是定義了一個變數wall_to_monotonic,記錄了牆上時間和monotonic時間之間的偏移量,當需要獲得monotonic時間時,把xtime和wall_to_monotonic相加即可,因為預設啟動時monotonic時間為0,所以實際上wall_to_monotonic的值是一個負數,它和xtime同一時間被初始化,請參考timekeeping_init函式。

計算monotonic時間要去除系統休眠期間花費的時間,核心用total_sleep_time記錄休眠的時間,每次休眠醒來後重新累加該時間,並調整wall_to_monotonic的值,使其在系統休眠醒來後,monotonic時間不會發生跳變。因為wall_to_monotonic值被調整。所以如果想獲取boot time,需要加入該變數的值:

  1. void get_monotonic_boottime(struct timespec *ts)  
  2. {  
  3.         ......  
  4.     do {  
  5.         seq = read_seqbegin(&timekeeper.lock);  
  6.         *ts = timekeeper.xtime;  
  7.         tomono = timekeeper.wall_to_monotonic;  
  8.         <span style="color:#ff0000;">sleep = timekeeper.total_sleep_time;</span>  
  9.         nsecs = timekeeping_get_ns();  
  10.     } while (read_seqretry(&timekeeper.lock, seq));  
  11.     set_normalized_timespec(ts, ts->tv_sec + tomono.tv_sec + sleep.tv_sec,  
  12.             ts->tv_nsec + tomono.tv_nsec + sleep.tv_nsec + nsecs);  
  13. }  
raw_time欄位用來表示真正的硬體時間,也就是上面所說的raw monotonic time,它不受時間調整的影響,monotonic時間雖然也不受settimeofday的影響,但會受到ntp調整的影響,但是raw_time不受ntp的影響,他真的就是開完機後就單調地遞增。xtime、monotonic-time和raw_time可以通過使用者空間的clock_gettime函式獲得,對應的ID引數分別是 CLOCK_REALTIME、CLOCK_MONOTONIC、CLOCK_MONOTONIC_RAW。

clock欄位則指向了目前timekeeper所使用的時鐘源,xtime,monotonic time和raw time都是基於該時鐘源進行計時操作,當有新的精度更高的時鐘源被註冊時,通過timekeeping_notify函式,change_clocksource函式將會被呼叫,timekeeper.clock欄位將會被更新,指向新的clocksource。

早期的核心版本中,xtime、wall_to_monotonic、raw_time其實是定義為全域性靜態變數,到我目前的版本(V3.4.10),這幾個變數被移入到了timekeeper結構中,現在只需維護一個timekeeper全域性靜態變數即可:

  1. staticstruct timekeeper timekeeper;  

3.  timekeeper的初始化

timekeeper的初始化由timekeeping_init完成,該函式在start_kernel的初始化序列中被呼叫,timekeeping_init首先從RTC中獲取當前時間:

  1. void __init timekeeping_init(void)  
  2. {  
  3.     struct clocksource *clock;  
  4.     unsigned long flags;  
  5.     struct timespec now, boot;  
  6.     read_persistent_clock(&now);  
  7.     read_boot_clock(&boot);  
然後對鎖和ntp進行必要的初始化:
  1. seqlock_init(&timekeeper.lock);  
  2. ntp_init();  
接著獲取預設的clocksource,如果平臺沒有重新實現clocksource_default_clock函式,預設的clocksource就是基於jiffies的clocksource_jiffies,然後通過timekeeper_setup_inernals內部函式把timekeeper和clocksource進行關聯:
  1. write_seqlock_irqsave(&timekeeper.lock, flags);  
  2. clock = clocksource_default_clock();  
  3. if (clock->enable)  
  4.     clock->enable(clock);  
  5. timekeeper_setup_internals(clock);  
利用RTC的當前時間,初始化xtime,raw_time,wall_to_monotonic等欄位:
  1. timekeeper.xtime.tv_sec = now.tv_sec;  
  2. timekeeper.xtime.tv_nsec = now.tv_nsec;  
  3. timekeeper.raw_time.tv_sec = 0;  
  4. timekeeper.raw_time.tv_nsec = 0;  
  5. if (boot.tv_sec == 0 && boot.tv_nsec == 0) {  
  6.     boot.tv_sec = timekeeper.xtime.tv_sec;  
  7.     boot.tv_nsec = timekeeper.xtime.tv_nsec;  
  8. }  
  9. set_normalized_timespec(&timekeeper.wall_to_monotonic,  
  10.             -boot.tv_sec, -boot.tv_nsec);  
最後,初始化代表實時時間和monotonic時間之間偏移量的offs_real欄位,total_sleep_time欄位初始化為0:
  1. update_rt_offset();  
  2. timekeeper.total_sleep_time.tv_sec = 0;  
  3. timekeeper.total_sleep_time.tv_nsec = 0;  
  4. write_sequnlock_irqrestore(&timekeeper.lock, flags);  
xtime欄位因為是儲存在記憶體中,系統掉電後無法儲存時間資訊,所以每次啟動時都要通過timekeeping_init從RTC中同步正確的時間資訊。其中,read_persistent_clock和read_boot_clock是平臺級的函式,分別用於獲取RTC硬體時間和啟動時的時間,不過值得注意到是,到目前為止(我的程式碼樹基於3.4版本),ARM體系中,只有tegra和omap平臺實現了read_persistent_clock函式。如果平臺沒有實現該函式,核心提供了一個預設的實現:
  1. void __attribute__((weak)) read_persistent_clock(struct timespec *ts)  
  2. {  
  3.     ts->tv_sec = 0;  
  4.     ts->tv_nsec = 0;  
  5. }  
  1. void __attribute__((weak)) read_boot_clock(struct timespec *ts)  
  2. {  
  3.     ts->tv_sec = 0;  
  4.     ts->tv_nsec = 0;  
  5. }  
那麼,其他ARM平臺是如何初始化xtime的?答案就是CONFIG_RTC_HCTOSYS這個核心配置項,開啟該配置後,driver/rtc/hctosys.c將會編譯到系統中,由rtc_hctosys函式通過do_settimeofday在系統初始化時完成xtime變數的初始化:
  1. staticint __init rtc_hctosys(void)   
  2. {   
  3.         ......   
  4.         err = rtc_read_time(rtc, &tm);   
  5.         ......  
  6.         rtc_tm_to_time(&tm, &tv.tv_sec);   
  7.         do_settimeofday(&tv);   
  8.         ......   
  9.         return err;   
  10. }   
  11. late_initcall(rtc_hctosys);  

4.  時間的更新

xtime一旦初始化完成後,timekeeper就開始獨立於RTC,利用自身關聯的clocksource進行時間的更新操作,根據核心的配置項的不同,更新時間的操作發生的頻度也不盡相同,如果沒有配置NO_HZ選項,通常每個tick的定時中斷週期,do_timer會被呼叫一次,相反,如果配置了NO_HZ選項,可能會在好幾個tick後,do_timer才會被呼叫一次,當然傳入的引數是本次更新離上一次更新時相隔了多少個tick週期,系統會保證在clocksource的max_idle_ns時間內呼叫do_timer,以防止clocksource的溢位:

  1. void do_timer(unsigned long ticks)  
  2. {  
  3.     jiffies_64 += ticks;  
  4.     update_wall_time();  
  5.     calc_global_load(ticks);  
  6. }  
在do_timer中,jiffies_64變數被相應地累加,然後在update_wall_time中完成xtime等時間的更新操作,更新時間的核心操作就是讀取關聯clocksource的計數值,累加到xtime等欄位中,其中還設計ntp時間的調整等程式碼,詳細的程式碼就不貼了。

5.  獲取時間

timekeeper提供了一系列的介面用於獲取各種時間資訊。

  • void getboottime(struct timespec *ts);    獲取系統啟動時刻的實時時間
  • void get_monotonic_boottime(struct timespec *ts);     獲取系統啟動以來所經過的時間,包含休眠時間
  • ktime_t ktime_get_boottime(void);   獲取系統啟動以來所經過的c時間,包含休眠時間,返回ktime型別
  • ktime_t ktime_get(void);    獲取系統啟動以來所經過的c時間,不包含休眠時間,返回ktime型別
  • void ktime_get_ts(struct timespec *ts) ;   獲取系統啟動以來所經過的c時間,不包含休眠時間,返回timespec結構
  • unsigned long get_seconds(void);    返回xtime中的秒計數值
  • struct timespec current_kernel_time(void);    返回核心最後一次更新的xtime時間,不累計最後一次更新至今clocksource的計數值
  • void getnstimeofday(struct timespec *ts);    獲取當前時間,返回timespec結構
  • void do_gettimeofday(struct timeval *tv);    獲取當前時間,返回timeval結構