本系列文章的前兩節討論了用於計時的時鐘源: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. */u32mult; /* NTP adjusted clock multiplier */intshift;/* The shift value of the current clocksource. */cycle_t cycle_interval;/* Number of clock cycles in one NTP interval. */u64xtime_interval;/* Number of clock shifted nano seconds in one NTP interval. */s64xtime_remainder;/* shifted nano seconds left over when rounding cycle_interval */u32raw_interval;/* Raw nano seconds accumulated per NTP interval. */u64xtime_nsec;/* Clock shifted nano seconds remainder not stored in xtime.tv_nsec. *//* Difference between accumulated time and NTP time in ntp * shifted nano seconds. */s64ntp_error;/* Shift conversion between clock shifted nano seconds and * ntp shifted nano seconds. */intntp_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欄位就是上面所說的牆上時間,它是一個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,需要加入該變數的值:
void get_monotonic_boottime(struct timespec *ts){ ......do {seq = read_seqbegin(&timekeeper.lock);*ts = timekeeper.xtime;tomono = timekeeper.wall_to_monotonic;sleep = timekeeper.total_sleep_time;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);}
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全域靜態變數即可:
static struct 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);
然後對鎖和ntp進行必要的初始化:
seqlock_init(&timekeeper.lock);ntp_init();
接著擷取預設的clocksource,如果平台沒有重新實現clocksource_default_clock函數,預設的clocksource就是基於jiffies的clocksource_jiffies,然後通過timekeeper_setup_inernals內建函式把timekeeper和clocksource進行關聯:
write_seqlock_irqsave(&timekeeper.lock, flags);clock = clocksource_default_clock();if (clock->enable)clock->enable(clock);timekeeper_setup_internals(clock);
利用RTC的目前時間,初始化xtime,raw_time,wall_to_monotonic等欄位:
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);
最後,初始化代表即時時間和monotonic時間之間位移量的offs_real欄位,total_sleep_time欄位初始化為0:
update_rt_offset();timekeeper.total_sleep_time.tv_sec = 0;timekeeper.total_sleep_time.tv_nsec = 0;write_sequnlock_irqrestore(&timekeeper.lock, flags);
}
xtime欄位因為是儲存在記憶體中,系統掉電後無法儲存時間資訊,所以每次啟動時都要通過timekeeping_init從RTC中同步正確的時間資訊。其中,read_persistent_clock和read_boot_clock是平台級的函數,分別用於擷取RTC硬體時間和啟動時的時間,不過值得注意到是,到目前為止(My Code樹基於3.4版本),ARM體系中,只有tegra和omap平台實現了read_persistent_clock函數。如果平台沒有實現該函數,核心提供了一個預設的實現:
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;}
那麼,其他ARM平台是如何初始化xtime的?答案就是CONFIG_RTC_HCTOSYS這個核心配置項,開啟該配置後,driver/rtc/hctosys.c將會編譯到系統中,由rtc_hctosys函數通過do_settimeofday在系統初始化時完成xtime變數的初始化:
static int __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);}
在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結構