官术网_书友最值得收藏!

第3章 時間的衡量和計算

我們的作息、行為和活動都與時間息息相關,從人類的角度來看,時間是以時分秒來計算和衡量的。例如,某一時刻做某件事情或某個任務持續了多久等情況,我們基本都以時分秒來計算。但內核中卻不是這樣,從內核的角度,它的時間以滴答(jiffy)來計算。滴答,通俗地講,是時間系統周期性地提醒內核某時間間隔已經過去的行為。另一方面,內核服務于用戶,所以它還需要擁有將滴答轉為時分秒的能力。因此,內核的任務就分為維護并響應滴答和告知用戶時間兩部分。

3.1 數據結構

我們從gettimeofday系統調用說起,它的功能是獲取當前時間,結果以timeval和timezone(時區)結構體的形式返回,timeval的tv_sec字段表示從Epoch(1970-01-01 00:00:00 UTC)到現在的秒數,tv_usec表示微秒。

內核先通過ktime_get_real_ts64獲得以timespec表示的當前時間,然后轉化為timeval形式,timespec64的tv_sec以秒為單位,tv_nsec以納秒為單位,實現過程展開如下。

以上代碼段中,第一個與時間相關的結構體為timekeeper(簡稱tk,稍后詳解),它的主要功能是保持或者記錄時間,從timekeeping_get_ns函數可以看到,tk的tkr_mono->clock字段指向clocksource類型的對象,表示當前使用的時鐘源。當前時間等于上次更新時的時間(xtime_nsec)加上從上次更新到此刻的時間間隔,時間間隔是通過時鐘源度過的時鐘周期(cycle_delta)計算得來的。各時鐘源的時鐘頻率不同,所以它們時鐘周期的時間單位就不同,內核需要擁有將設備時鐘周期數與納秒相互轉換的能力,mult和shift字段就是負責實現該需求的。請注意,“上次更新”是指內核更新時間,是“寫”動作,gettimeofday是“讀”動作,并不會導致xtime_nsec變化。

時鐘源以clocksource結構體(簡稱cs)表示,它是本章第二個核心的結構體,主要字段如表3-1所示。

表3-1 clocksource字段表

flags字段有多種標志,前三種可以由時鐘設備的驅動設置,后幾種一般由內核控制,如表3-2所示。

表3-2 時鐘設備標志表

rating字段代表cs的等級,內核只會選擇一個時鐘源作為watchdog(即看門狗),也只會選擇一個時鐘源作為系統的時鐘源(與全局變量tk_core.timekeeper對應),同等條件下,等級更高的時鐘源擁有更高的優先級。

內核會選擇一個不需要被監控的連續時鐘源作為看門狗,它負責在程序運行的時候監控其他的時鐘源,如果某一個時鐘源的誤差超過了可接受的范圍,那么,則將其置為CLOCK_SOURCE_UNSTABLE,并將其rate字段置為0。

時鐘源設備在初始化后可以調用clocksource_register_hz等函數完成注冊,內核會在所有時鐘源中選擇rating值最大的作為保持時間的時鐘源,用戶調用gettimeofday獲取當前時間時,就通過該時鐘源讀取時鐘周期數來計算得到結果。

我們通過內核得到了當前的時間,但內核基本不會關心時分秒,它的重心在于某個時間間隔已經過去這類事件;這就依賴于另一種設備了,它們可以實現定時器的功能,在設定的時間間隔過去之后產生中斷提醒內核。當然,有些設備既有保持時間的功能,又有定時器的功能。為方便讀者理解,本文以下將保持時間的設備稱為時鐘源,將關注時間事件的設備稱為時鐘中斷設備(或稱為時鐘事件設備,為了突出時鐘事件的本質)。

本章的另一個核心結構體clock_event_device(簡稱evt)就與內核這第二個需求相關,它的主要字段如表3-3所示。

表3-3 clock_event_device字段表

set_state_xxx是切換當前設備狀態的回調函數,xxx可以有共有periodic、oneshot、oneshot_stopped和shutdown四種。其中,periodic表示周期性地觸發時鐘中斷,oneshot表示單觸發。所謂周期性是指觸發一次中斷之后自動進入下一次中斷計時,單觸發是指觸發一次中斷之后停止。rating字段表示設備的等級,等級越高優先級越高。最常用的設備特性(features字段)有PERIODIC、ONESHOT和C3STOP三種,其中C3STOP表示系統處于C3狀態的時候設備停止。

時鐘中斷產生后,內核會調用event_handler字段的回調函數處理中斷,該函數完成時鐘中斷相關的邏輯,還可以回調set_next_event設置下一次時鐘中斷。時鐘中斷對內核而言是必不可少的,內核對各模塊的時間控制基本都依賴它。

內核使用了幾個常見的變量和宏定義,具體如下。

HZ:一秒內的滴答數。現代內核并不一直采用周期性的滴答,硬件允許的情況下,更傾向于采用動態(非周期性)時鐘中斷,但HZ作為很多模塊衡量時間的基準被保留了下來。

jiffies:累計的滴答數。內核主要存在jiffy和ktime_t兩個時間單位,后者本質上就是納秒,內核對相關計算進行優化并提供了使用它的函數,因而產生了ktime_t。我們可以使用ktime_get獲得當前以ktime_t為單位的時間,使用ktime_to_ns和ns_to_ktime等完成轉換。

在涉及時鐘設備或者時鐘中斷設備的操作時,比如讀取時鐘周期數、設置下一個中斷等,會使用納秒為單位;如前文所述,設備數據結構中的mult和shift字段可以實現納秒與周期數的轉化。其余方面,除非需要跟蹤時間,基本都使用jiffy為單位。

tick_period:ktime_t類型的全局變量,周期性時鐘中斷的時間間隔。稍后會發現,即使采用動態時鐘中斷的系統,它在運行的某些階段依然可能周期性地觸發中斷。該變量就是為了計算周期性時鐘中斷的條件下,下一次中斷的觸發時間。

tick_cpu_device:每cpu變量,類型為tick_device,簡稱td。tick_device結構體的evtdev字段指向該cpu的evt,mode字段表示其工作模式,分為TICKDEV_MODE_PERIODIC和TICKDEV_MODE_ONESHOT兩種。

tick_cpu_sched:每cpu變量,類型為tick_sched,簡稱ts,它與系統調度和更新系統時間有關。

3.2 時鐘芯片

不同的架構或平臺使用的設備不一定相同,下面介紹幾個x86架構上常見的設備。

RTC(Real-timeClock):實時時鐘,兼具時鐘源和時鐘中斷兩種功能,它可以為人們提供精確的實時時間,或者為電子系統提供精確的時間基準,目前絕大多數計算機上都會有它。RT C有一個特性,那就是系統關機以后依然處于工作狀態。就提供實時時間而言,RT C的準確度一般要比其他幾個設備高;就時鐘中斷而言,RT C只能產生周期信號,頻率變化范圍從2Hz到8192Hz,且必須是2的倍數,所以在現代計算機上它的第二個功能基本被其他設備取代。

PIT(Programmable interval timer):可編程間隔計時器,時鐘中斷設備,頻率固定為1.193182MHz,所以在老式的系統中它也可以充當時鐘源的角色。PIT可滿足周期性和單觸發兩種時鐘中斷要求。

TSC(Time Stamp Counter):時間戳計數器,是一個64位的寄存器,從奔騰處理器開始就存在于x86架構的處理器中。TSC記錄處理器的時鐘周期數,程序可以通過RDTSC指令來讀取它的值。采用TSC作為系統的時鐘源是有挑戰的,比如處理器降頻的時候,TSC如何保持固定頻率,不過在現代的x86處理器中,這些問題已經得到了解決。TSC以其高精度、低開銷的優勢成為優選。

HPET(High Precision Event Timer):俗稱高精度定時器,兼具時鐘源和時鐘中斷兩種功能,它有一組定時器可以同時工作,數量從3到256個不等。每個定時器都可以滿足周期性和單觸發兩種時鐘中斷要求,可以代替PIT及RTC。

APIC(Advanced Programmable Interrupt Controller):高級可編程中斷控制器,用作時鐘中斷設備,單從名字看就比PIT高一個檔次。APIC的資料網絡上多如牛毛,這里不詳細闡述。每個cpu都有一個local(本地)APIC,這里的時間設備就是本地APIC的一部分。它的精度較高,可以滿足周期性和單觸發兩種時鐘中斷要求。

就cs而言,PIT、TSC和HPET的cs對象rating字段的默認值如表3-4所示(RTC并不在cs和evt的選項中)。

表3-4 cs設備rating表

因為TSC的優先級最高,所以大多數計算機會采用TSC作為時鐘源。然而TSC的頻率是開機的時候計算得出的(見下),小的誤差在運行過程中會被放大,最終導致較明顯的時間偏差。RT C倒是可以很好地完成保持時間的任務,但它的頻率太低,精度不夠。

TSC對應的cs的flags字段有IS_CONTINUOUS和MUST_VERIFY兩個標記,所以它可以滿足CLOCK_SOURCE_VALID_FOR_HRES的要求,但不能作為系統的watchdog,而屬于會被watchdog監控的時鐘源。

就evt而言,PIT、HPET和APIC的evt對象的rating字段的默認值如表3-5所示。

表3-5 evt設備rating表

3.3 從內核的角度看時間

內核維護了多種時間,其中最常用REALTIME、MONOTONIC和BOOTTIME三種。

REALTIME時間:又稱作WALLTIME(墻上時間),甚至可以稱為xtime時間或者系統時間,xtime在分析gettimeofday系統調用的時候已經出現過了,它就是根據cs的時鐘周期計算得出的時間。

需要注意的是,REALTIME時間和RTC時間并不是同一個概念。前者是內核維護的當前時間,RT C時間是RT C對應芯片維護的硬件時間。二者也是有聯系的,系統啟動的時候,內核會讀取RTC的時間作為REALTIME時間,之后才獨立。另外,settimeofday系統調用會更新REALTIME時間,并不會更新RTC時間。所以,僅僅設置系統時間,重啟機器后設置不會生效。我們可以使用hwclock-hctosys和hwclock-systohc兩個命令(Hardware Clock to System)完成二者的同步,或者設置系統時間后運行hwclock-w命令同步到RT C時間。

MONOTONIC時間:不是絕對時間,表示系統啟動到當前所經歷的非休眠時間,單調遞增,系統啟動時該值由0開始(timekeeping_init函數),之后一直增加。它不受REALTIME時間變更的影響,也不計算系統休眠的時間,也就是說,系統休眠時它不會增加。

tk的xtime_sec表示REALTIME時間,wall_to_monotonic字段表示由xtime計算得到MONOTONIC時間需要加上的時間段。

在3.10版的內核中,total_sleep_time字段表示系統休眠的時間。下面以xtime表示REALTIME時間,xtime_org表示系統啟動時的xtime初值,wtm表示tk的wall_to_monotonic字段,wtm_org表示系統啟動時wtn的初值,boot_time表示系統啟動時REALTIME時間。則3.10版內核中存在6個公式。

xtime+wtm=monotonic公式(1)

由于系統啟動時monotonic為0,所以有如下關系。

wtm_org=-xtime_org=-old_boot_time公式(2)

系統每次休眠,xtime都會變大,但由于monotonic時間不計算休眠時間,所以有如下關系。

xtime_before_suspend+wtm_before_suspend=monotonic_before_suspend

(xtime_before_suspend+sleep)+wtm_after_suspend=monotonic_after_suspend

monotonic_before_suspend=monotonic_after_suspend

得出:wtm_after_suspend=wtm_before_suspend-sleep

所以,每次休眠過后xtime增加了sleep,wtm也要相應地減去sleep,在不考慮主動修改xtime造成影響的情況下,累計所有的sleep時間得到如下關系。

wtm_current=wtm_org-total_sleep_time 公式(3)

軟件上主動修改xtime亦如此,比如sys_settimeofday系統調用。

記delta=xtime_after_set-xtime_before_set

有:wtm_after_set=wtm_before_set-delta

不考慮休眠的情況下,累計所有的delta,得到如下關系。

wtm_current=wtm_org-total_delta 公式(4)

將休眠和修改均考慮在內,有如下關系。

wtm_current=wtm_org-total_sleep_time-total_delta公式(5)

用戶修改了系統時間后,系統啟動時間也會隨之變化。原因很簡單,正常邏輯下修改之后的時間比修改之前的要正確。

new_boot_time=old_boot_time+total_delta

結合公式(2)有如下關系。

new_boot_time=-wtm_org+total_delta

結合公式(5)有如下關系。

new_boot_time=-wtm_current-total_sleep_time公式(6)

公式(1)和公式(6)就是內核計算MONOTONIC時間和系統啟動時REALTIME時間的原理,getboottime/getboottime64就是利用了公式(6)獲得系統啟動的時間。

在5.05版的內核中,tk移除了total_sleep_time字段,以offs_real字段表示MONOTONIC與REALTIME之間的差,以offs_boot表示MONOTONIC與BOOTTIME之間的差,getboottime/getboottime64直接使用offs_real-offs_boot,即可達到目的。

BOOTTIME時間:表示系統啟動到現在的時間,它也不是絕對時間,與MONOTONIC時間不同的是,BOOTTIME時間會記錄系統休眠的時間。

timekeeper為內核實現了幾個獲取時間的函數,它們為獲取不同種類的時間提供了方便,如表3-6所示。上文中BOOTTIME表示時間種類,boot_time表示系統啟動時的時間,但內核中并沒有把這兩個概念很好地區分,請注意區分下面幾個BOOTTIME。

表3-6 獲取時間的函數表

需要注意的是,MONOTONIC時間也并不是完全單調的,它會受NTP(Network Time Protocol,網絡時間協議)的影響,真正不受影響的是RAWMONOTONIC時間。

3.4 周期性和單觸發的時鐘中斷

現代計算機上一般都會包含多種時鐘中斷設備,它們可以支持周期性和單觸發的時鐘中斷,下文以PIT和APICTimer的組合為例。

TSC的頻率是運行時計算得出的,內核啟動時會利用PIT得到該值(PIT的頻率固定,利用PIT設定時間段,除該時間段內TSC的周期數)。所以PIT會優先工作,調用clockevents_register_device函數注冊它的evt。注冊過程中會執行tick_check_new_device設置該cpu對應的td,將evt賦值給td的evtdev字段。td的默認狀態(mode字段)為TICKDEV_MODE_PERIODIC,這樣PIT的evt的event_handler字段被賦值為tick_handle_periodic。這是第一個階段,系統處于周期性時鐘中斷模式,即tick階段。

APIC Timer進入工作(setup_APIC_timer)之后,由于它的rating比PIT高,td的evtdev字段被替換成APIC的evt,此時可以嘗試切換至nohz模式。

nohz模式(也可稱為tickless模式、oneshot模式、動態時鐘模式),主要要求系統當前的cs要有CLOCK_SOURCE_VALID_FOR_HRES標志(timekeeping_valid_for_hres函數)和evt可以滿足單觸發模式(tick_is_oneshot_available函數)兩個條件。條件滿足的情況下,內核會切換到nohz模式,進入第二個階段。所謂的nohz模式是指時鐘中斷不完全周期性地工作的模式,在該模式下時鐘中斷的間隔可以動態調整。

nohz模式又分為兩種模式:高精度模式和低精度模式。滿足nohz條件的情況下,切換到哪個模式由hrtimer_hres_enabled變量的值決定,該值可通過內核啟動參數設置。等于0時,調用tick_nohz_switch_to_nohz切換到普通nohz模式,否則調用hrtimer_switch_to_hres切換到高精度模式。

低精度和高精度模式不同點如下。

從字面上理解,后者比前者精度要高,事實也確實如此。低精度模式最高的頻率就是HZ,cpu處于非idle的狀態下,它一般也是以周期性的方式工作,之所以稱之為nohz是因為它的頻率可以大于HZ,比如在idle狀態下。高精度模式的最高頻率由時鐘中斷設備決定,該特性與hrtimer完美配合(見15.2.3.3),可以滿足對時間間隔要求較高的應用場景。

低精度模式下,evt的event_handler字段被賦值為tick_nohz_handler,高精度模式下被賦值為hrtimer_interrupt。

時鐘中斷發生后,低精度模式的tick_nohz_handler會在函數本體內完成進程時間計算、進程調度等操作;而hrtimer_interrupt只負責去處理到期的hrtimer,進程時間計算、進程調度等操作是通過ts的sched_timer字段表示的hrtimer完成的,tick_sched_timer函數最終負責完成這些操作(見15.2.3.3小節)。

要理解高精度模式,必須對hrtimer有清楚的認識,hrtimer并不同于timer,它們雖然有相似的函數,但hrtimer可以觸發對時鐘中斷設備編程的動作。

3.5 時間相關的系統調用

時間相關的應用程序比比皆是,它們都需要內核的系統調用支持,比如獲取當前時間或為某些操作定時。

3.5.1 獲取時間

除了前面分析過的gettimeofday和settimeofday系統調用外,內核可能還會實現time和stime系統調用,后面二者都是以秒為單位的,它們的存在也許只是為了向前兼容,程序應該采用前二者。

除此之外,內核還提供了兼容POSIX標準的系統調用clock_gettime和clock_settime等,它們的區別主要在精度上,如表3-7所示。

表3-7 獲取設置時間的系統調用表

并不是所有的時間操作都是通過系統調用來完成的,比如更新RTC時間的hwclock命令,它實際上是通過讀寫文件來實現功能的(如/dev/rtc,內核為RTC提供了一個統一的架構)。

3.5.2 給程序定個鬧鐘

就像生活中很多時候需要鬧鐘一樣,很多程序也需要定時提醒。

alarm,在早期的平臺上作為系統調用,現代平臺上基本在C庫中調用其他系統調用實現,精度為秒。

setitimer,alarm多數情況下會通過調用setitimer實現,后者是一個系統調用,通過它可以設置一個定時器。getitimer與之對應,用來獲得定時器的當前時間。setitimer的精度為微秒,函數原型(用戶空間)如下。

第一個參數表示定時器的類型,可以有ITIMER_REAL、ITIMER_VIRTUAL和ITIMER_PROF三種值。該函數不會阻塞當前進程,定時器設置完畢后返回繼續執行,到期后內核會發送信號至當前進程。定時器的類型、意義和對應的到期信號的對應關系如表3-8所示。

表3-8 定時器類型和信號表

每一種類型的定時器相互獨立,但對一個進程而言,同一時刻、同一種定時器只能存在一個;如果還有同類定時器未執行,重新調用該函數會將原定時器取消。

setitimer與alarm相似,需要配合信號機制使用,示例代碼如下。

itimerval結構體的it_value字段表示第一次運行定時器的時間(相對時間);it_interval字段表示循環運行定時器的時間間隔,為0表示不自動重新啟動定時器。

timer_create族,精度為納秒,時間到期依然是通過信號告知進程,函數原型(用戶空間)如下。

第一個參數表示時鐘類型,POSIX協議共定義了超過10個合法值,但并不是每一個都被內核支持,CLOCK_REALTIME是必定會支持的,如果內核支持MonotonicClock,那么CLOCK_MONOTONIC也會被支持。evp為sigevent結構體指針,timerid用于保存創建成功的定時器的id。與setitimer不同的是,使用timer_create同一時刻同一種定時器可以有多個,每個定時器都以它們的id區分。本質上,setitimer創建的定時器對進程而言是全局的,內核不會在每次調用setitimer的時候去創建一個新的定時器;timer_create會觸發創建新定時器的動作,內核維護定時器的鏈表,有效的id是用戶使用定時器的唯一方式。

操作成功,返回0,定時器的id存入timerid變量中,否則返回非0值,timerid的值無意義。定時器創建成功后,就可以利用它的id使用了,如表3-9所示。

表3-9 timer_create函數族表

與setitimer相同,timer_settime也可以設置定時器到期后自動重新計時。同一個定時器,到期的時候,有可能它上次到期時產生的信號還處在掛起狀態,那么信號可能會丟失,timer_getoverrun就是計算這類事件發生的次數的。

timer_create看似功能比setitimer強大,但同時也多了管理新建定時器的操作,所以本著夠用即可的原則,除非setitimer不能滿足應用場景要求,才會選擇前者。

3.6 實例分析

時間是系統的節拍,也是很多機制的基礎,我們需要掌握內核計算、維護時間以及時鐘中斷的原理。

3.6.1 實現智能手機的長按操作

我們在手機主界面上操作時,滑動可以翻頁,長按可以進入應用編輯狀態,進行文件夾創建或應用刪除等操作。那么手機操作系統是如何區分滑動和長按的呢?有時我們嘗試長按進行應用編輯卻失敗的原因是什么呢?

首先,滑動和長按的區別在于手指移動的距離。以手指觸摸屏幕的第一個點作為起點,后續的點中只要任何一個點和它的距離超過了閾值,就會被認為是滑動。所以嘗試長按的時候,手指的抖動也可能導致失敗。

其次,長按有時間的設定,比如0.5秒。如果在0.5秒內手指抬起,會被視為點擊,達到0.5秒才會被認為是長按。

從代碼角度,0.5秒是如何實現的呢?手指按下得到第一個點時,應用發送一個0.5秒后啟動長按事件的請求,事件啟動前如果手指抬起或者滑動距離超過閾值,發送取消事件的請求。0.5秒時間內如果沒有取消,長按觸發。

所以,處理事件請求的模塊需要有計時功能,有兩種實現方法,一種是不斷讀取當前時間,另外一種是使用定時器等。

3.6.2 系統的時間并不如你所想

讀者可以做一個實驗,當我們把同步網絡時間功能關閉,系統時間與真實時間的誤差會越來越大。

這是由我們選擇的時鐘導致的,選擇時鐘時優先考慮的是精度,也就是可以測量的最小時間,優點是可以快速響應時間精度要求高的請求。

系統中的RT C時鐘與真實時間接近,但它的精度不夠,往往都不會被選中,所以時間誤差會越來越大。

另外一個有誤差的時間是睡眠,睡眠的過程主要包括讓出CPU、重新可執行和被執行三步。前兩步之間的時間是實際的睡眠時間,后兩步之間的時間是不固定的。進程被喚醒后,當前CPU可能在執行其他進程,其他進程執行完畢后可能還有其他優先級更高的進程需要執行。所以從調用sleep到函數返回,除了實際的睡眠時間,還包括進程調度時間,也是有誤差的。

主站蜘蛛池模板: 九寨沟县| 贵南县| 略阳县| 县级市| 宣武区| 全椒县| 获嘉县| 湟源县| 海原县| 华坪县| 安龙县| 定安县| 汝城县| 界首市| 金昌市| 无棣县| 营口市| 安塞县| 文登市| 昌乐县| 天台县| 彩票| 荣昌县| 吕梁市| 墨脱县| 密云县| 依安县| 江津市| 和政县| 大洼县| 盐城市| 微山县| 习水县| 旬邑县| 孝义市| 大方县| 德阳市| 崇州市| 兴宁市| 太康县| 当阳市|