Linux內核之時間系統
1、Linux時間系統
Linux系統中有兩個時鐘源,一個叫做RTC,另一個叫做系統時鐘。時鐘運作機制如下圖:
(1)CMOS時鐘
RTC(Real Time Clock,實時時鐘)也叫做CMOS時鐘,它獨立於操作系統,由PC主板上的一塊鈕釦電池供電,當操作系統關機的時候,用它來記錄時間,當系統啓動時,內核通過讀取RTC來初始化牆上時間,它爲計算機提供一個計時標準,是最原始最底層的時鐘數據。(後面介紹牆上時間)
(2)系統時鐘
系統時鐘是由操作系統控制PC主板上的定時/計數芯片來工作的,它依賴CMOS時鐘而啓動,初始化後由操作系統完全管理,操作系統通過OS時鐘提供給應用程序所有和時間有關的服務。系統時鐘產生於PC主板上的定時/計數芯片,常見的有8253/8254可編程定時/計數芯片,其工作原理是由晶振、電容等組成的振盪電路,產生脈衝(高低電平),這些脈衝輸入到中斷控制器上,產生中斷信號,觸發時鐘中斷,由時鐘中斷服務程序維持OS時鐘正常工作。
系統時鐘記錄的時間也就是我們常見的系統時間,它是以“時鐘節拍”爲單位的,時鐘中斷的頻率(節拍率)決定了一個時鐘節拍的長短。節拍率是通過靜態預處理定義的,也就是Hz(赫茲),Linux內核版本4.19中是這樣定義的:
#ifndef __ASM_GENERIC_PARAM_H
#define __ASM_GENERIC_PARAM_H
#include <uapi/asm-generic/param.h>
# undef HZ
# define HZ CONFIG_HZ /* Internal kernel timer frequency */
# define USER_HZ 100 /* some user interfaces are */
# define CLOCKS_PER_SEC (USER_HZ) /* in "ticks" like times() */
#endif /* __ASM_GENERIC_PARAM_H */
一個tick代表多長時間,在內核的CONFIG_HZ中定義。比如CONFIG_HZ=200,則一個jiffies對應5ms時間,所以內核基於jiffies的定時器精度也是5ms。(下面介紹jiffies)
(3)節拍數(jiffies)
jiffies是Linux內核中的一個全局變量,用來記錄自系統啓動以來產生的節拍總數。啓動時內核將該變量初始化爲0,此後每次時鐘中斷jiffies的值+1,每一秒鐘中斷次數HZ,jiffies一秒內增加HZ。
- 將秒轉換爲jiffies可採用公式:seconds*HZ
- jiffies轉換爲秒可採用公式:jiffies/HZ
- 系統運行時間(s) = jiffies/HZ
jiffies用途:計算流逝時間和時間管理。
jiffies 變量總是無符號長整數(Unsigned Long)。因此,在32位體系結構上是32位,在64位體系結構是64位,當 jiffies 的值超過它的最大存放範圍後就會發生溢出,它的值會迴繞到0。內核提供了這樣的宏來幫助解決由於jiffies溢出而造成程序邏輯出錯的情況:
#define time_after(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)((b) - (a)) < 0))
#define time_before(a,b) time_after(b,a)
#define time_after_eq(a,b) \
(typecheck(unsigned long, a) && \
typecheck(unsigned long, b) && \
((long)((a) - (b)) >= 0))
#define time_before_eq(a,b) time_after_eq(b,a)
在宏time_after中,首先確保兩個輸入參數a和b的數據類型爲unsigned long,然後才執行實際的比較。
(4)牆上時間(xtime)
牆上時間就是實際時間,實際時間的獲取是在開機後,內核初始化時從RTC讀取的。內核讀取這個時間後就將其放入內核中的 xtime 變量中,並且在系統的運行中不斷更新這個值。內核中並不常用牆上時間,主要是方便用戶空間的程序獲取當前時間。它的精度可以達到納秒級別,因爲xtime實際上是一個內存中的變量,它的訪問速度非常快,內核大部分時間都是使用xtime來獲得當前時間信息。Linux中的xtime記錄的是自1970年1月1日0時到當前時刻所經歷的納秒數。
2、重要數據結構
Linux內核提供各種time line,real time clock,monotonic clock、monotonic raw clock等,timekeeping模塊就是負責跟蹤、維護這些timeline的,並且向其他模塊(timer相關模塊、用戶空間的時間服務等)提供服務,而timekeeping模塊維護timeline的基礎是基於clocksource模塊和tick模塊。通過tick模塊的tick事件,可以週期性的更新time line,通過clocksource模塊、可以獲取tick之間更精準的時間信息。
Linux內核版本4.19中在linux-4.19\include\linux\timekeeper_internal.h
下有這樣兩個數據結構:
(1)struct tk_read_base
讀取時間的基本結構tk_read_base,內核版本4.19源碼如下:
struct tk_read_base {
struct clocksource *clock;
u64 mask;
u64 cycle_last;
u32 mult;
u32 shift;
u64 xtime_nsec;
ktime_t base;
u64 base_real;
};
其中:
- clock:timekeeping當前使用的時鐘源
這個clock應該是系統中最優的那個,如果有好過當前clocksource註冊入系統,那麼clocksource模塊會通知timekeeping模塊來切換clocksource。
- mask:非64位時鐘的二進制補碼減法的位掩碼
- cycle_last:最後一次更新的時鐘週期值
- mult :(經過NTP調整)數學換算的乘數
- shift:數學換算的移位值
- xtime_nsec:用於讀出納秒的偏移量
- base:用於讀取ktime_t(納秒)的基本時間
- base_real:用於讀出實際時間的基本納秒值
- base_real:用於快速NMI安全訪問器,以允許讀取時鐘
此結構在64位上的大小爲56字節,連同一個seqcount佔用64字節緩存。
這個結構體與timekeeper結構是分開的,因爲它也被使用用於快速NMI安全訪問器。
base_real用於快速NMI安全訪問器以允許從任何上下文讀取實際時間。
(2)struct timekeeper
timekeeper結構用於保存計時值,內核版本4.19源碼如下:
struct timekeeper {
struct tk_read_base tkr_mono;
struct tk_read_base tkr_raw;
u64 xtime_sec;
unsigned long ktime_sec;
struct timespec64 wall_to_monotonic;
ktime_t offs_real;
ktime_t offs_boot;
ktime_t offs_tai;
s32 tai_offset;
unsigned int clock_was_set_seq;
u8 cs_was_changed_seq;
ktime_t next_leap_ktime;
u64 raw_sec;
/* The following members are for timekeeping internal use */
u64 cycle_interval;
u64 xtime_interval;
s64 xtime_remainder;
u64 raw_interval;
/* The ntp_tick_length() value currently being used.
* This cached copy ensures we consistently apply the tick
* length for an entire tick, as ntp_tick_length may change
* mid-tick, and we don't want to apply that new value to
* the tick in progress.
*/
u64 ntp_tick;
/* Difference between accumulated time and NTP time in ntp
* shifted nano seconds. */
s64 ntp_error;
u32 ntp_error_shift;
u32 ntp_err_mult;
/* Flag used to avoid updating NTP twice with same second */
u32 skip_second_overflow;
#ifdef CONFIG_DEBUG_TIMEKEEPING
long last_warning;
/*
* These simple flag variables are managed
* without locks, which is racy, but they are
* ok since we don't really care about being
* super precise about how many events were
* seen, just that a problem was observed.
*/
int underflow_seen;
int overflow_seen;
#endif
};
其中:
- tkr_mono:CLOCK_MONOTONIC的讀出基本結構
- tkr_raw:CLOCK_MONOTONIC_RAW的讀出基本結構
- xtime_sec:當前的CLOCK_REALTIME時間,以秒爲單位
Linux中CLOCK_REALTIME time,直接使用秒以及納秒在當前秒內的偏移來表示。這裏xtime_sec用秒這個的刻度單位來度量CLOCK_REALTIME time line上,時間原點到當前點的距離值。當然xtime_sec是一個對current time point的取整值,爲了更好的精度,還需要一個納秒錶示的offset,也就是在剛纔那個數據結構tk_read_base結構中的xtime_nsec。不過爲了內核內部計算精度(內核對時間的計算是基於cycle的),並不是保存了時間的納秒偏移值,而是保存了一個shift之後的值,因此,用戶看來,當前時間點的值應該是距離時間原點xtime_sec + (xtime_nsec << shift)距離的那個時間點值。
- ktime_sec:當前的CLOCK_MONOTONIC時間,以秒爲單位
- wall_to_monotonic:從CLOCK_REALTIME到CLOCK_MONOTONIC偏移
CLOCK_MONOTONIC類型的系統時鐘。這種系統時鐘並沒有像牆上時鐘一樣定義一個相對於linux epoch的值,這個成員定義了monotonic clock到real time clock的偏移,也就是說,這裏的wall_to_monotonic和offs_real需要加上real time clock的時間值才能得到monotonic clock的時間值。wall_to_monotonic和offs_real的意思是一樣的,不過時間的格式不一樣,用在不同的場合,以便獲取性能的提升。
- offs_real:偏移時鐘單調->時鐘實時
- offs_boot:偏移時鐘單調->時鐘啓動時間
- offs_tai:偏移時鐘單調->時鐘tai
- tai_offset:當前UTC到TAI的偏移量,以秒爲單位
CLOCK_TAI類型的系統時鐘。TAI(international atomic time)是原子鐘,在時間的基本概念文檔中,UTC就是base TAI的,也就是說用銫133的振盪頻率來定義秒的那個時鐘,CLOCK_TAI類型的系統時鐘就是完完全全使用銫133的振盪頻率來定義秒的那個時鐘。
- clock_was_set_seq:時鐘被設置事件的序號
- cs_was_changed_seq:時鐘源更改事件的序列號
- next_leap_ktime:待處理的leap秒的CLOCK_MONOTONIC時間值
- raw_sec:CLOCK_MONOTONIC_RAW時間以秒爲單位
- cycle_interval:一個NTP間隔中的時鐘週期數
- xtime_interval:一個NTP中的時鐘移位納秒數間隔
- xtime_remainder:四捨五入時還剩納秒
- cycle_interval:一個NTP間隔中的時鐘週期數
- raw_interval:每個NTP間隔累積的偏移原始毫微秒
- ntp_error:ntp中的累積時間和NTP時間之間的差偏移了納秒
- ntp_error_shift:時鐘移位納秒和ntp移位納秒之間的轉換
- last_warning:警告速率限制器(DEBUG_TIMEKEEPING)
- underflow_seen:下溢警告標誌(DEBUG_TIMEKEEPING)
- overflow_seen:溢出警告標誌(DEBUG_TIMEKEEPING)
(3)內核中表示時間的數據結構
在內核版本4.19linux-4.19\include\uapi\linux\time.h
中有這樣幾個表示時間的數據結構,源碼如下:
#ifndef _STRUCT_TIMESPEC
#define _STRUCT_TIMESPEC
struct timespec {
__kernel_time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds */
};
#endif
該數據結構來自於POSIX.1b規範,用於在用戶態和內核態之間傳遞時間信息。POSIX還定義了許多API供用戶態調用,例如clock_gettime(),clock_settime()等,其中的表示時間的結構都是timespec。
struct timeval {
__kernel_time_t tv_sec; /* seconds */
__kernel_suseconds_t tv_usec; /* microseconds */
};
該結構也用於用戶態和內核態間的時間信息傳遞。不同於timespec的是它的兩個成員分別是秒和微妙,精度要比timespec差。另外一個區別是相關API也不同,timeval相關的API是gettimeofday()等。
3、Linux常用的關於時間的命令
命令如下,結果如圖:
(1)查看時間和日期
date
(2)設置時間和日期
將日期設置爲2019年11月18日
date -s 11/18/2019
將系統時間設定成下午6點16分26秒
date -s 6:16:26
(3)同步網絡時間
ntpdate -u 0.asia.pool.ntp.org
(4)將當前時間和日期寫入BIOS,避免重啓後失效
hwclock -w
(5)查看月曆
call
更多關於時間的操作,可以使用命令man date
來查看。。。