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結構來組織與時間相關的資料,它的定義如下:
- struct timekeeper {
- struct clocksource *clock; /* Current clocksource used for timekeeping. */
- u32 mult; /* NTP adjusted clock multiplier */
- int shift; /* The shift value of the current clocksource. */
- cycle_t cycle_interval; /* Number of clock cycles in one NTP interval. */
- u64 xtime_interval; /* Number of clock shifted nano seconds in one NTP interval. */
- s64 xtime_remainder; /* shifted nano seconds left over when rounding cycle_interval */
- u32 raw_interval; /* Raw nano seconds accumulated per NTP interval. */
- u64 xtime_nsec; /* Clock shifted nano seconds remainder not stored in xtime.tv_nsec. */
- /* Difference between accumulated time and NTP time in ntp
- * shifted nano seconds. */
- s64 ntp_error;
- /* Shift conversion between clock shifted nano seconds and
- * ntp shifted nano seconds. */
- int ntp_error_shift;
- struct timespec xtime; /* The current time */
- struct timespec wall_to_monotonic;
- struct timespec total_sleep_time; /* time spent in suspend */
- struct timespec raw_time; /* The raw monotonic time for the CLOCK_MONOTONIC_RAW posix clock. */
- ktime_t offs_real; /* Offset clock monotonic -> clock realtime */
- ktime_t offs_boot; /* Offset clock monotonic -> clock boottime */
- seqlock_t lock; /* Seqlock for all timekeeper values */
- };
核心除了用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,需要加入該變數的值:
- void get_monotonic_boottime(struct timespec *ts)
- {
- ......
- do {
- seq = read_seqbegin(&timekeeper.lock);
- *ts = timekeeper.xtime;
- tomono = timekeeper.wall_to_monotonic;
- <span style="color:#ff0000;">sleep = timekeeper.total_sleep_time;</span>
- nsecs = timekeeping_get_ns();
- } while (read_seqretry(&timekeeper.lock, seq));
- set_normalized_timespec(ts, ts->tv_sec + tomono.tv_sec + sleep.tv_sec,
- ts->tv_nsec + tomono.tv_nsec + sleep.tv_nsec + nsecs);
- }
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全域性靜態變數即可:
- staticstruct timekeeper timekeeper;
3. timekeeper的初始化
timekeeper的初始化由timekeeping_init完成,該函式在start_kernel的初始化序列中被呼叫,timekeeping_init首先從RTC中獲取當前時間:
- void __init timekeeping_init(void)
- {
- struct clocksource *clock;
- unsigned long flags;
- struct timespec now, boot;
- read_persistent_clock(&now);
- read_boot_clock(&boot);
- seqlock_init(&timekeeper.lock);
- ntp_init();
- write_seqlock_irqsave(&timekeeper.lock, flags);
- clock = clocksource_default_clock();
- if (clock->enable)
- clock->enable(clock);
- timekeeper_setup_internals(clock);
- timekeeper.xtime.tv_sec = now.tv_sec;
- timekeeper.xtime.tv_nsec = now.tv_nsec;
- timekeeper.raw_time.tv_sec = 0;
- timekeeper.raw_time.tv_nsec = 0;
- if (boot.tv_sec == 0 && boot.tv_nsec == 0) {
- boot.tv_sec = timekeeper.xtime.tv_sec;
- boot.tv_nsec = timekeeper.xtime.tv_nsec;
- }
- set_normalized_timespec(&timekeeper.wall_to_monotonic,
- -boot.tv_sec, -boot.tv_nsec);
- update_rt_offset();
- timekeeper.total_sleep_time.tv_sec = 0;
- timekeeper.total_sleep_time.tv_nsec = 0;
- write_sequnlock_irqrestore(&timekeeper.lock, flags);
- void __attribute__((weak)) read_persistent_clock(struct timespec *ts)
- {
- ts->tv_sec = 0;
- ts->tv_nsec = 0;
- }
- void __attribute__((weak)) read_boot_clock(struct timespec *ts)
- {
- ts->tv_sec = 0;
- ts->tv_nsec = 0;
- }
- staticint __init rtc_hctosys(void)
- {
- ......
- err = rtc_read_time(rtc, &tm);
- ......
- rtc_tm_to_time(&tm, &tv.tv_sec);
- do_settimeofday(&tv);
- ......
- return err;
- }
- 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的溢位:
- void do_timer(unsigned long ticks)
- {
- jiffies_64 += ticks;
- update_wall_time();
- calc_global_load(ticks);
- }
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結構