時間撿拾(下篇-系統(tǒng)&硬件層)

目錄:


image.png

上篇從日常代碼出發(fā),著重討論了Java弯洗、MySQL等應(yīng)用層中日期時間的表示和存儲等操作、可能遇到的坑,及時區(qū)轉(zhuǎn)換相關(guān)方法。下篇將盡量深入底層,看看我們在用及“日期時間”時,計算機中發(fā)生了什么。

1. 奇怪的現(xiàn)象

上篇中講過 System.currentTimeMillis() 的用法,也提到了高頻調(diào)用時會產(chǎn)生一定性能問題舱痘,我們先來看現(xiàn)象:
用以下代碼大致測量 System.currentTimeMillis() 方法的執(zhí)行速度(運行一億次变骡,求平均時間):

long sum = 0L;
int N = 100000000;
long begin = System.currentTimeMillis();
for (int i = 0; i < N; i++) {
    sum += System.currentTimeMillis();
}
long end = System.currentTimeMillis();
System.out.println("sum = " + sum + "ms");
System.out.println("total time = " + (end - begin) + "ms");
System.out.println("average time = " + (end - begin) * 1.0E6 / N + " ns/iter");

查看不同系統(tǒng)的輸出如圖所示:

  1. Windows(Windows 10,Intel(R) Core(TM) i5-6500):


    image.png
  2. macOS(macOS High Sierra芭逝,2.3 GHz Intel Core i5,MacBook Pro 2017):


    image.png
  3. Linux(3.10.0-327.28.3.el7.x86_64旬盯,Intel(R) Xeon(R) CPU E5-2620 v3 @ 2.40GHz台妆,服務(wù)器實體機):
  • 默認(rèn)TSC時間源


    image.png
  • 切換為另外一常用時間源:HPET


    image.png

梳理以上實驗:

操作系統(tǒng)類型 平均執(zhí)行時間(ns/次)
Windows 4.77
macOS 33.63
Linux(TSC) 27.43
Linux(HPET) 574.93

結(jié)果是沒有預(yù)料到的,最快和最慢差異能達(dá)到兩個數(shù)量級胖翰,原因我們下面細(xì)細(xì)道來接剩。

2. 深入探索 System.currentTimeMillis()

System.currentTimeMillis() 是一個 native 方法,它的代碼可以參考OpenJDK萨咳,我們找出此方法的JVM實現(xiàn)如下 hotspot/src/share/vm/prims/jvm.cpp

JVM_LEAF(jlong, JVM_CurrentTimeMillis(JNIEnv *env, jclass ignored))
  JVMWrapper("JVM_CurrentTimeMillis");
  return os::javaTimeMillis();
JVM_END

調(diào)用了 os::javaTimeMillis() 方法懊缺,不同系統(tǒng)有不同的實現(xiàn)。Mac和Linux(TSC)的執(zhí)行時間相近培他,我們挑選Windows和Linux系統(tǒng)探究原理并比較鹃两。

2.1. Windows實現(xiàn)

hotspot/src/os/windows/vm/os_windows.cpp 中追蹤其實現(xiàn)如下:

jlong os::javaTimeMillis() {
  if (UseFakeTimers) {
    return fake_time++;
  } else {
    FILETIME wt;
    GetSystemTimeAsFileTime(&wt);
    return windows_to_java_time(wt);
  }
}

jlong windows_to_java_time(FILETIME wt) {
  jlong a = jlong_from(wt.dwHighDateTime, wt.dwLowDateTime);
  return (a - offset()) / 10000;
}

jlong offset() {
  return _offset;
}

static jlong  _offset   = 116444736000000000;

由此可見該方法關(guān)口在于Windows的 GetSystemTimeAsFileTime() 方法遗座,在Microsoft開發(fā)網(wǎng)站上(Docs/Windows/Sysinfoapi.h/GetSystemTimeAsFileTime function)說明為:

image.png

該方法返回一個 FILETIME 類型的數(shù)據(jù),其中包含兩個32位字段:dwLowDateTime 和 dwHighDateTime俊扳,分別代表當(dāng)前 FILETIME 的低位和高位途蒋。組合起來返回1601年1月1日(UTC)以來以100ns為間隔的數(shù)量,減去 116444736000000000(1601年1月1日至1970年1月1日的100納秒間隔個數(shù))再除以 10000拣度,即是當(dāng)前Unix時間戳毫秒數(shù)碎绎。

為什么是1601年?
因為現(xiàn)行 Gregorian Calendar 的時間周期是400年一輪(四年一閏抗果,百年不閏筋帖,四百年再閏),1601年是距離Windows系統(tǒng)開始開發(fā)最近的一個400年周期點冤馏,把它作為系統(tǒng)時間的起點日麸,那么把NT時間轉(zhuǎn)換為常規(guī)日期表達(dá)(或相反)就不用做任何跳躍操作。

這個方法是在 kernel32.dll 中實現(xiàn)的逮光,在VC中執(zhí)行GetSystemTimeAsFileTime(&f)方法坟奥,從調(diào)試的 disassembly 模式調(diào)出執(zhí)行的匯編語句如下圖:
執(zhí)行的兩句語句如下圖述吸,會首先去 call 調(diào)用 kernel32.dll 中的方法:

image.png

然后 jmp 到其具體實現(xiàn):
image.png

關(guān)鍵流程:
image.png

執(zhí)行后的結(jié)果,dwHighDateTime 和 dwLowDateTime 兩個字段即為取出的數(shù)據(jù):
image.png

進(jìn)行轉(zhuǎn)換操作:((dwHighDateTime << 32) + dwLowDateTime - offset) / 10000即可轉(zhuǎn)換為我們熟悉的Unix時間戳:
image.png

整個流程已經(jīng)明晰,追蹤下來可以了解到:此方法的執(zhí)行邏輯僅僅是到固定的內(nèi)存空間取這兩個32位字段的值爷抓,沒有經(jīng)過任何用戶態(tài)和內(nèi)核態(tài)的轉(zhuǎn)換過程。有一個后臺進(jìn)程會定期更新此內(nèi)存空間中的字段值腰湾,保證是能取到的最新時間暑塑。

但是我們?nèi)粘J褂玫腤indows都不是實時操作系統(tǒng)(RTOS),系統(tǒng)提供的時間精度并沒有到納秒級別驾茴,我們來測試一下實際可以達(dá)到的最小時間間隔:

#include <stdio.h>  
#include <time.h>  
#include <windows.h>  
#define N 1000000
int values[N];
int main(void)
{
    int i;
    for (i = 0; i < N; i++) {
        FILETIME f;
        GetSystemTimeAsFileTime(&f);
        values[i] = (int) f.dwLowDateTime;
    }
    for (i = 1; i < N; i++)
        if (values[i-1] != values[i])
            printf("%d %d\n", i, values[i] - values[i-1]);
    return 0;
}

以上代碼調(diào)用 1,000,000 次 GetSystemTimeAsFileTime 方法盼樟,并將得到的時間放到一個數(shù)組中,然后找出相鄰兩位不同的數(shù)字并求差锈至,就是可以測算出的最小時間間隔晨缴。實驗結(jié)果如下:


image.png

可以看到平均最小時間間隔為5000個100ns,即0.5毫秒峡捡,遠(yuǎn)沒有達(dá)到ns級击碗,但是已經(jīng)夠 System.currentTimeMillis() 的精度要求了。
綜上棋返,Windows系統(tǒng)下的 System.currentTimeMillis() 執(zhí)行速度極快延都,約為4.77ns,最小時間間隔約為0.5毫秒睛竣。

2.2. Linux實現(xiàn)

hotspot/src/os/linux/vm/os_linux.cpp 中追蹤其實現(xiàn)如下:

jlong os::javaTimeMillis() {
  timeval time;
  int status = gettimeofday(&time, NULL);
  assert(status != -1, "linux error");
  return jlong(time.tv_sec) * 1000  +  jlong(time.tv_usec / 1000);
}

乘除法的時間消耗不會到百納秒級別晰房,可見該方法的關(guān)口在于 gettimeofday() 方法。接下來讓我們一步一步深入Linux源碼探個究竟。
首先要明確實驗機器的環(huán)境殊者,如下圖:

image.png

linux/v3.10/source/arch/x86/vdso/vclock_gettime.c 中找到 gettimeofday 方法:

int gettimeofday(struct timeval *, struct timezone *)
    __attribute__((weak, alias("__vdso_gettimeofday")));

可以看到与境,這里使用了vDSO(virtual dynamic shared object,直譯為虛擬動態(tài)共享對象)猖吴。


image.png

關(guān)于 vsyscall 和 vDSO摔刁,內(nèi)容著實精彩,但不是本文重點共屈,感興趣的同學(xué)可以參考 System calls in the Linux kernel. Part 3. 等。

繼續(xù)查看 __vdso_gettimeofday() 方法(部分):

notrace int __vdso_gettimeofday(struct timeval *tv, struct timezone *tz)
{
    long ret = VCLOCK_NONE;
    if (likely(tv != NULL)) {
        // 實際獲取時鐘源時間的方法
        ret = do_realtime((struct timespec *)tv);
        tv->tv_usec /= 1000;
    }
    // 如果通過虛擬系統(tǒng)調(diào)用未獲取到党窜,則執(zhí)行真正的系統(tǒng)調(diào)用拗引,陷入內(nèi)核態(tài)獲取時間
    if (ret == VCLOCK_NONE)
        return vdso_fallback_gtod(tv, tz);
    return 0;
}

可以看到,其中主流程調(diào)用了 do_realtime() 方法獲得當(dāng)前機器時間幌衣,do_realtime 方法源碼如下:

#define gtod (&VVAR(vsyscall_gtod_data))

notrace static int __always_inline do_realtime(struct timespec *ts)
{
    unsigned long seq;
    u64 ns;
    int mode;

    ts->tv_nsec = 0;
    do {
        seq = read_seqcount_begin(&gtod->seq);
        mode = gtod->clock.vclock_mode;
        ts->tv_sec = gtod->wall_time_sec;
        ns = gtod->wall_time_snsec;
        ns += vgetsns(&mode);
        ns >>= gtod->clock.shift;
    } while (unlikely(read_seqcount_retry(&gtod->seq, seq)));

    timespec_add_ns(ts, ns);
    return mode;
}

上述源碼中可以看出矾削,虛擬內(nèi)存調(diào)用的邏輯和Windows中的邏輯很相近,都是從一個地址空間(上述源碼通過變量gtod尋址)上定義好的數(shù)據(jù)結(jié)構(gòu)中讀取出當(dāng)前機器時間豁护。同時哼凯,一些后臺線程會定期更新此結(jié)構(gòu)的字段。

值得注意的是楚里,Windows中兩次讀取高位數(shù)字并比較來確保讀取的多個值的順序一致性断部,這點在Linux中通過內(nèi)存屏障來保證,這便是 read_seqcount_begin 和 read_seqcount_retry 的作用班缎。同時家坎,Intel x86的強內(nèi)存排序也可以做到這一點。若有興趣的同學(xué)可以詳細(xì)了解源碼及系統(tǒng)實現(xiàn)方法吝梅。

當(dāng)前機器時間由兩個字段表示:wall_time_sec 和 wall_time_snsec。wall_time_sec 代表機器從機器計時起點開始經(jīng)過的秒數(shù)惹骂,wall_time_snsec 代表相對于最后一秒經(jīng)過的納秒級別時間苏携,其精度可以通過 gtod->clock.shift 控制。參考了一些文獻(xiàn)中的實驗对粪,現(xiàn)有 wall_time_snsec 的精度一般可以達(dá)到一毫秒以內(nèi)右冻。
至此,只是從固定地址空間讀取和計算著拭,并不會耗費很多時間纱扭。然而Linux并不滿足于直接讀取出的時間數(shù)值,試圖繼續(xù)通過 vgetsns() 方法獲取更高精度的時間儡遮。vgetsns() 方法源碼如下:

notrace static inline u64 vgetsns(int *mode)
{
    long v;
    cycles_t cycles;
    if (gtod->clock.vclock_mode == VCLOCK_TSC)
        cycles = vread_tsc();
    else if (gtod->clock.vclock_mode == VCLOCK_HPET)
        cycles = vread_hpet();
    else if (gtod->clock.vclock_mode == VCLOCK_PVCLOCK)
        cycles = vread_pvclock(mode);
    else
        return 0;
    v = (cycles - gtod->clock.cycle_last) & gtod->clock.mask;
    return v * gtod->clock.mult;
}

至此我們終于見到了文章開篇問題中提到的時間源問題乳蛾!系統(tǒng)中存在幾個高頻計時器,vgetsns() 方法會根據(jù)當(dāng)前系統(tǒng)指定的計時器種類去讀取該計時器中的“cycles”,即經(jīng)過了多少個高頻時鐘周期肃叶,然后通過 gtod-> mask 和 gtod-> mult 字段進(jìn)行時間的轉(zhuǎn)換蹂随,并加到 wall_time_snsec 代表的納秒時間數(shù)值中。
由文章開篇的現(xiàn)象可知因惭,不同時間源獲取時間的代價不同岳锁,追蹤至此我們可以得出,就是在 vgetsns() 方法中消耗的時間成本導(dǎo)致的蹦魔,而不同時間源的獲取成本不同激率,則是由于其硬件構(gòu)造和發(fā)展。

至此勿决,可以說是部分“破案”了乒躺,之所以不同系統(tǒng)不同時間源差異如此之大,是由于實現(xiàn)方式不同剥险。Windows平臺只是到固定的內(nèi)存空間取兩個32位字段的值并進(jìn)行計算聪蘸,所以最快;Linux中在此基礎(chǔ)上通過讀取高頻計時器來提高獲取的時間精度表制,所以普遍稍慢健爬;也就是在這一步驟中,不同計時器的讀取代價不同么介,造成總的執(zhí)行成本有巨大差異娜遵。

說了這么久時間源的問題,但還沒有了解時間源的相關(guān)概念壤短,下面我們詳細(xì)介紹设拟。

3. 常見的時鐘、定時器硬件和時間源概念

不論軟件層面如何讀取如何計算久脯,時間點的獲取和時間段的計量總是要通過硬件來完成的纳胧。本章主要介紹幾種常見的時鐘和定時器硬件,及Linux在硬件上層抽象出的“時間源”數(shù)據(jù)結(jié)構(gòu)帘撰。

3.1. 常見時鐘和定時器硬件

3.1.1. 實時時鐘(RTC)

和其他時鐘硬件不同跑慕,實時時鐘RTC(Real Time Clock)輸出的是UTC時刻,而其他硬件如TSC摧找、PIT核行、HPET等輸出的都只是周期數(shù),即“我已經(jīng)走過XXX個cycle了蹬耘,我的頻率是XXX芝雪,你自己去算吧!”
這是因為RTC是獨立于CPU和其他所有芯片的综苔,即使當(dāng)PC電源被切斷RTC還可以繼續(xù)工作惩系。RTC和 CMOS RAM 被集成在一個芯片(Motorola 146818或其他等價的芯片)上位岔,它靠主板上的一個小電池或蓄電池獨立供電。
當(dāng)然蛆挫,既然RTC已經(jīng)如此獨特赃承,它輸出的時間精度自然不會很高(秒級),不然也沒別的硬件什么事兒了悴侵。它可以在IRQ8上發(fā)出周期性的中斷瞧剖,主要用于在系統(tǒng)啟動初始化時不依靠網(wǎng)絡(luò)等外界幫助獲取當(dāng)前時間等,剩下的高精度需求就交給其他時間源來做可免。

3.1.2. 時間戳計數(shù)器(TSC)

所有的 80x86 微處理器都包含一條CLK輸入引線抓于,它接收外部振蕩器的時鐘信號,從CLK管腳輸入浇借,以提供執(zhí)行指令所需時鐘沿捉撮。80x86 提供了一個 TSC 寄存器,該寄存器的值在每次收到一個時鐘信號時加一妇垢。比如 CPU 的主頻為 1GHZ巾遭,則每一秒時間內(nèi),TSC 寄存器的值將增加 1G 次闯估,或者說每一個納秒加一次灼舍。x86 還提供了 rtdsc 指令來讀取該值,因此 TSC 也可以作為時鐘設(shè)備涨薪。要注意骑素,當(dāng)使用這個寄存器時,必須考慮到時鐘信號的頻率刚夺。

3.1.3. 可編程間隔定時器(PIT)

可編程間隔定時器PIT(Programmable Interval Timer)以內(nèi)核確定的固定頻率不停地發(fā)出時鐘中斷献丑,類似于“打拍子”。由于PIT出現(xiàn)較早侠姑,時鐘頻率也不高创橄,漸漸被更加精確的HPET所取代,在此不過多介紹莽红。

3.1.4. 高精度事件定時器(HPET)

HPET是由微軟和英特爾聯(lián)合開發(fā)的定時器芯片筐摘,用以取代PIT提供高精度的時鐘中斷(10MHz以上)。一個HPET芯片包含了8個32位或64位的獨立計數(shù)器船老,每個計數(shù)器由自己的時鐘信號驅(qū)動,每個計時器又包含了一個比較器和一個寄存器(保存一個數(shù)值圃酵,表示觸發(fā)中斷的時機)柳畔。每一個比較器都比較計數(shù)器中的數(shù)值和寄存器的數(shù)值,相等就會產(chǎn)生中斷郭赐。

3.1.5. 其他定時器硬件

其他定時器硬件還包括:CPU本地定時器薪韩、ACPI電源管理定時器等确沸。他們處于不同的位置,也有不同的時鐘頻率俘陷,但是由于綜合功能和性能不及TSC和HPET罗捎,多數(shù)系統(tǒng)并未以其作為 current_clocksource。

3.2. clocksource數(shù)據(jù)結(jié)構(gòu)

上文講了眾多的時間相關(guān)硬件拉盾,Linux為了管理這些硬件桨菜,抽象出來clocksource數(shù)據(jù)結(jié)構(gòu)。
查看實驗機器可用的clocksource捉偏,包括:tsc倒得、hpet、acpi_pm:


image.png

linux/v3.10/source/include/linux/clocksource.h#L166 中可以找到clocksource數(shù)據(jù)結(jié)構(gòu)的定義:

struct clocksource {
    /*
     * Hotpath data, fits in a single cache line when the
     * clocksource itself is cacheline aligned.
     */
    cycle_t (*read)(struct clocksource *cs);
    cycle_t cycle_last;
    cycle_t mask;
    u32 mult;
    u32 shift;
    u64 max_idle_ns;
    u32 maxadj;
#ifdef CONFIG_ARCH_CLOCKSOURCE_DATA
    struct arch_clocksource_data archdata;
#endif

    const char *name;
    struct list_head list;
    int rating;
    int (*enable)(struct clocksource *cs);
    void (*disable)(struct clocksource *cs);
    unsigned long flags;
    void (*suspend)(struct clocksource *cs);
    void (*resume)(struct clocksource *cs);

    /* private: */
#ifdef CONFIG_CLOCKSOURCE_WATCHDOG
    /* Watchdog related data, used by the framework */
    struct list_head wd_list;
    cycle_t cs_last;
    cycle_t wd_last;
#endif
} ____cacheline_aligned;

此數(shù)據(jù)結(jié)構(gòu)中定義了很多時間源相關(guān)參數(shù)夭禽,比較重要的是 rating霞掺、shift讹躯、mult骗灶。

3.2.1. rating

精度越高的時間源,頻率越高,rating值越大糊肤。從該源碼的注釋中可以得知:

  • 1--99: 不適合于用作實際的時鐘源,只用于啟動過程或用于測試舷暮;
  • 100--199:基本可用,可用作真實的時鐘源耗啦,但不推薦;
  • 200--299:精度較好似将,可用作真實的時鐘源;
  • 300--399:很好,精確的時鐘源侦厚;
  • 400--499:理想的時鐘源,如有可能就必須選擇它作為時鐘源;
    linux/v3.10/source/drivers/clocksource/acpi_pm.c#L66中顯示来破,ACPI時間源的rating是200髓堪;
    linux/v3.10/source/arch/x86/kernel/hpet.c#L748 中顯示驶沼,HPET時間源的rating是250祭阀;linux/v3.10/source/arch/x86/kernel/tsc.c#L775 中顯示抹凳,TSC時間源的rating是300。
    對應(yīng)的幸冻,時間源硬件的頻率查看如下:
    image

    可見碑定,TSC時間源簡直“超凡脫俗”。

3.2.2. shift 和 mult

由于除RTC外的硬件輸出都是“節(jié)拍數(shù)”,所以要根據(jù)硬件頻率換算成具體的時間段,公式為:時間段 = 節(jié)拍數(shù) / 頻率劫哼。其中伤溉,節(jié)拍數(shù)可以通過 clocksource 數(shù)據(jù)結(jié)構(gòu)中的成員變量 read 所指向的函數(shù)獲取。例如 clocksource_tsc 的定義中深挖到最后是通過 rdtsc 指令來獲取當(dāng)前計數(shù)值cycles的券时。
可是這些和shift、mult兩個變量有什么關(guān)系呢炸枣?計算機中對于除法的計算轉(zhuǎn)化為乘法和移位更加方便,公式就變成了:時間段 = 節(jié)拍數(shù) * mult >> shift。在 linux/v3.10/source/include/linux/clocksource.h#L275 中 定義如下:

static inline s64 clocksource_cyc2ns(cycle_t cycles, u32 mult, u32 shift)
{
    return ((u64) cycles * mult) >> shift;
}

3.3. 不同時間源的比較

3.3.1. 如何比較

由于有上述好多種時間源息尺,不可避免要比較各個功能性能優(yōu)劣,并將它們放在最合適的位置。比較的維度有多種:

  • 從功能上說嘲碧,RTC時間源有自己獨特的直接返回UTC時刻且“不斷電”的能力加矛,雖然只能精確到秒級,但是足以應(yīng)對系統(tǒng)時間初始化等場景。
  • 從性能上講已烤,比如上文已經(jīng)提到的MHz為單位的時鐘頻率鸠窗,部分反映在clocksource數(shù)據(jù)結(jié)構(gòu)中的rating字段,是重要的衡量標(biāo)準(zhǔn)胯究,決定了時間到底能精確到什么程度塌鸯,CPU中的時間精度也就決定了計算速度的上限(時鐘沿變化)。
  • 還有一些其他要注意的點星持。比如TSC代表時間戳記計數(shù)器,它僅僅是自啟動以來計算的CPU周期數(shù)。曾經(jīng)在很長一段時間里,這個值有兩個問題:來自不同內(nèi)核或物理處理器的值可能相互不統(tǒng)一猛们,因為處理器可能在不同的時間開始啟動;處理器的時鐘頻率可能會在執(zhí)行期間發(fā)生變化,給計算實際流逝時間段造成困難。但是這些問題隨著處理器不斷更新?lián)Q代進(jìn)行了修復(fù)與升級,具體有興趣的同學(xué)可以參考Intel等的官方文檔。

整體來講耳高,系統(tǒng)的默認(rèn)時間源是在根據(jù)硬件發(fā)展不停變化的诈皿,現(xiàn)在大多數(shù)默認(rèn)時間源是TSC宵睦,在2007版的《深入理解Linux內(nèi)核》一書中攻礼,當(dāng)時最優(yōu)的時間源還是HPET:


image.png

所以瞬沦,大家要盡量在穩(wěn)定基礎(chǔ)理論的前提下罢洲,具體實操時了解確認(rèn)最新的技術(shù)。
針對現(xiàn)有的硬件放祟,還要給大家提醒一個可能遇到的“坑”:多線程使用問題鳍怨。

3.3.2. 不同時間源的多線程使用的“坑”

文章開頭已經(jīng)給出,在Linux環(huán)境下跪妥,TSC和HPET的執(zhí)行速度大概是 27.43 ns/次 和 574.93 ns/次 鞋喇。這是單線程執(zhí)行的結(jié)果,實際開發(fā)中會有很多多線程場景眉撵,或一個物理機上有多個程序在跑侦香,那么他們會互相有影響嗎?

3.3.2.1 現(xiàn)象

我們通過實驗來驗證纽疟。測試代碼如下:

public class Time {
    static double sumAvg = 0;
    public static void main(String[] args) throws InterruptedException {
        // 測試機器24核鄙皇,所以以24為上限進(jìn)行測試,
        for (int threadNums = 1; threadNums <= 24; threadNums++) {
            // 各線程平均時間的累加仰挣,用于最后計算平均時間的平均數(shù)(可能中文表述難以理解,看代碼吧)
            sumAvg = 0;
            // 控制主線程在子線程執(zhí)行之后再進(jìn)行平均值計算
            final CountDownLatch countDownLatch = new CountDownLatch(threadNums);
            for (int i = 0; i < threadNums; i++) {
                // 每個線程測試平均每次 System.currentTimeMillis() 的請求時間
                new Thread(() -> {
                    int N = 10000000;
                    long begin = System.currentTimeMillis();
                    for (int j = 0; j < N; j++) {
                        System.currentTimeMillis();
                    }
                    long end = System.currentTimeMillis();
                    double thisAvg = (end - begin) * 1.0E6 / N;
                    sumAvg += thisAvg;
                    countDownLatch.countDown();
                }).start();
            }
            countDownLatch.await();
            System.out.println("thread numbers = " + threadNums + ", average time = " + sumAvg / threadNums + " ns/iter");
        }
    }
}
  • 使用TSC時間源


    image.png

    Excel圖表表示更為清晰:


    image.png

    忽略double的丟失精度缠沈,可以看出:隨著線程數(shù)增加膘壶,單個請求的耗時變化并不算大,只有在線程數(shù)接近核數(shù)時才些許上升颓芭。
  • 使用HPET時間源


    image.png

    image.png

    在10個線程的時候已經(jīng)達(dá)到了 44564ns/iter ,為了不影響服務(wù)沒有繼續(xù)增加線程數(shù)。但是趨勢已經(jīng)非常明顯:隨著線程數(shù)增加床玻,單個請求的耗時呈線性增長贫堰。合理猜想,HPET時間源可能會串行化請求偎行,或是有某個或多個步驟是處于臨界區(qū)的壮不,導(dǎo)致多個線程之間的相互影響隐孽,性能線性下降。至于理論依據(jù)還有待從硬件的角度考察。

  • 可能造成的問題
    由于處理器在使用HPET時會相互影響虑稼,因此存在潛在的安全問題。一個進(jìn)程可能會執(zhí)行緊密循環(huán)調(diào)用 gettimeofday() 方法溯壶,從而導(dǎo)致所有其他進(jìn)程調(diào)用此方法時性能降低。況且本來HPET的時間成本就較高,所以更要謹(jǐn)慎使用效扫。

3.3.2.2 高并發(fā)下可用的解決方案

(1)如果并發(fā)量高但是對時間精確度要求不高的話可以使用獨立線程緩存時間戳浩习。只要用一個變量存當(dāng)前時間戳,每過固定的一段間隔更新一次,其他調(diào)用需求直接取這個變量即可近哟,實現(xiàn)舉例如下:

class MillisecondClock {  
    // 自定義的間隔時間段
    private long gap = 0;
    // 要緩存的當(dāng)前時間點
    private volatile long now = 0; 
  
    private MillisecondClock(long gap) {  
        this.gap = gap;  
        this.now = System.currentTimeMillis();  
        start();  
    }  
  
    private void start() {  
        new Thread(new Runnable() {  
            @Override  
            public void run() {  
                try {  
                    Thread.sleep(gap);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
                now = System.currentTimeMillis();  
            }  
        }).start();  
    }  
  
    public long now() {  
        return now;  
    }
}

(2)如果有必要,使用JNI將 System.currentTimeMillis() 方法調(diào)用的 gettimeofday() 方法換成 clock_gettime() 方法,會更加高效咕宿,詳細(xì)不再展開;
(3)使用 System.nanoTime() 方法;
至此诉瓦,系統(tǒng)常用的時鐘、定時器硬件及系統(tǒng)為其抽象出的數(shù)據(jù)結(jié)構(gòu)及其區(qū)別簡要介紹完畢煞聪。

4. Linux計時體系結(jié)構(gòu)

以上文中我們從探究的思路步步深入了 System.currentTimeMillis() 方法是如何獲取當(dāng)前時間點的啄糙,以及在時間源的層次考慮不同硬件及其抽象出的數(shù)據(jù)結(jié)構(gòu)之間的差異。本章將從體系結(jié)構(gòu)的角度燕雁,選取博大精深的Linux系統(tǒng)刑赶,介紹系統(tǒng)對于時間的管理呛伴。
所謂“體系結(jié)構(gòu)”的概念是指一組部件和部件之間的聯(lián)系,具體到操作系統(tǒng)我們可以理解為定義的數(shù)據(jù)結(jié)構(gòu)和操作硬件賦能這些數(shù)據(jù)結(jié)構(gòu)的方法姐军。
Linux對于時間的管理主要還是那兩方面:時間點和時間段村生,即維護(hù)系統(tǒng)時間和管理定時器辽话。下面依次介紹,有些上文中提過的將會簡略些益咬。

4.1. 系統(tǒng)時間

這里的系統(tǒng)時間就是上文中 gettimeofday() 方法所獲取的時間梅鹦,由于有了 vDSO 不用再進(jìn)行用戶態(tài)和內(nèi)核態(tài)的切換,直接讀取系統(tǒng)時間蝶念。但是這個系統(tǒng)時間是怎么維護(hù)的呢?
Linux中定義了一個xtime對象,用來存放當(dāng)前的時間和日期桃犬,它是一個timestamp類型的數(shù)據(jù)結(jié)構(gòu)子房,該數(shù)據(jù)結(jié)構(gòu)在 linux/v3.10/source/include/uapi/linux/time.h#L9 中定義如下:

struct timespec {
    __kernel_time_t    tv_sec;
    long     tv_nsec;
};

其中的兩個變量:

  • tv_sec 存放自1970年1月1日(UTC)00:00以來經(jīng)過的秒數(shù)
  • tv_nsec 存放自上一秒開始經(jīng)過的納秒數(shù)
    是不是十分熟悉?下面我們拋開vDSO來介紹系統(tǒng)對于時間的操作镇饺。

4.1.1. 系統(tǒng)時間的初始化

內(nèi)核初始化期間,在 linux/v3.10/source/init/main.c 中調(diào)用了timekeeping_init() 方法來初始化“墻上時間”肥橙。向下追溯宠互,在 linux/v3.10/source/kernel/time/timekeeping.c#L770 中的 timekeeping_init() 方法 => linux/v3.10/source/arch/x86/kernel/rtc.c#L142 中的 read_persistent_clock() 方法:

void read_persistent_clock(struct timespec *ts)
{
    unsigned long retval;

    retval = x86_platform.get_wallclock();

    ts->tv_sec = retval;
    ts->tv_nsec = 0;
}

linux/v3.10/source/arch/x86/kernel/rtc.c#L61 中的 get_wallclock() => mach_get_cmos_time() 方法(截取部分):

unsigned long mach_get_cmos_time(void)
{
    unsigned int status, year, mon, day, hour, min, sec, century = 0;
    unsigned long flags;

    spin_lock_irqsave(&rtc_lock, flags);

    while ((CMOS_READ(RTC_FREQ_SELECT) & RTC_UIP))
        cpu_relax();

    sec = CMOS_READ(RTC_SECONDS);
    min = CMOS_READ(RTC_MINUTES);
    hour = CMOS_READ(RTC_HOURS);
    day = CMOS_READ(RTC_DAY_OF_MONTH);
    mon = CMOS_READ(RTC_MONTH);
    year = CMOS_READ(RTC_YEAR);

    return mktime(year, mon, day, hour, min, sec);
}

而后 linux/v3.10/source/kernel/time.c#L322 中 mktime() 方法對年月日等進(jìn)行了轉(zhuǎn)換:

unsigned long mktime(const unsigned int year0, const unsigned int mon0,
                     const unsigned int day, const unsigned int hour,
                     const unsigned int min, const unsigned int sec)
{
    unsigned int mon = mon0, year = year0;

    /* 1..12 -> 11,12,1..10 */
    if (0 >= (int) (mon -= 2)) {
        mon += 12;  /* Puts Feb last since it has leap day */
        year -= 1;
    }

    return ((((unsigned long)
          (year/4 - year/100 + year/400 + 367*mon/12 + day) +
          year*365 - 719499
        )*24 + hour /* now have hours */
      )*60 + min /* now have minutes */
    )*60 + sec; /* finally seconds */
}

可見频轿,系統(tǒng)初始化時會進(jìn)行時鐘初始化航邢,讀取RTC時間源上的UTC毫秒時間九火,賦值給xtime變量的 tv_sec 字段,并將xtime變量的 tv_nsec 字段賦值為0。初始化過程完成掀鹅。
這樣初始化出的系統(tǒng)時間值其實是不精確的扔嵌,因為并沒有地方獲取準(zhǔn)確的納秒數(shù)。其實計算機會用到NTP等服務(wù)進(jìn)行時間的精確同步,有興趣的同學(xué)可以進(jìn)一步了解案疲。但是話說回來鳖昌,不聯(lián)網(wǎng)的單機用戶對于當(dāng)前絕對時間點的精確度要求其實并沒有那么高备畦,給手槍加上狙擊鏡的意義不大。反而是對于時間段的精確度很高许昨,那留到下一節(jié)定時器去介紹吧萍恕。

linux/v3.10/source/init/main.c 的 start_kernel() 方法中不僅有上述初始化“墻上時間”的過程,還會調(diào)用 tick_init()车要、init_timers()翼岁、hrtimers_init()、time_init() 等方法來建立計時體系結(jié)構(gòu)市袖,分別注冊通知鏈蜓肆、初始化軟件時鐘相關(guān)數(shù)據(jù)結(jié)構(gòu)逼友、初始化高精度定時器照棋、初始化各種時間源霹疫。各個方法都有獨特的用途,以及嚴(yán)絲合縫的配合厂抽,有興趣的同學(xué)可以閱讀源碼,在此不進(jìn)行展開(實在是太多了==)。

4.1.2. 系統(tǒng)時間的讀取

系統(tǒng)時間是通過 getnstimeofday() 方法讀取的靠欢,在上文中已經(jīng)詳細(xì)介紹了vDSO的讀取方法,其中還加入了高精度定時器的讀取掩缓。在 linux/v3.10/source/kernel/time/timekeeping.c 中可以看到單純系統(tǒng)中 getnstimeofday() 的實現(xiàn):

int __getnstimeofday(struct timespec *ts)
{
    struct timekeeper *tk = &timekeeper;
    unsigned long seq;
    s64 nsecs = 0;

    do {
        seq = read_seqcount_begin(&timekeeper_seq);

        ts->tv_sec = tk->xtime_sec;
        nsecs = timekeeping_get_ns(tk);

    } while (read_seqcount_retry(&timekeeper_seq, seq));

    ts->tv_nsec = 0;
    timespec_add_ns(ts, nsecs);

    return 0;
}

源碼已經(jīng)很清楚了窘行,不過多解釋。值得一提的是 __getnstimeofday() 是順序鎖的典型應(yīng)用杭抠,“寫請求并發(fā)相對較少害碾,寫鎖必須優(yōu)先于讀鎖”,這樣的特點使用順序鎖大大提高了效率。

4.1.3. 系統(tǒng)時間的更新

所謂“前人栽樹后人乘涼”虱肄,讀取效率高是因為有后臺線程一直在更新xtime變量端蛆。更新過程是怎樣的谢揪?linux/v3.10/source/kernel/time/timekeeping.c#L489 中的 do_settimeofday() 方法(部分):

int do_settimeofday(const struct timespec *tv)
{
    struct timekeeper *tk = &timekeeper;
    struct timespec ts_delta, xt;
    unsigned long flags;

    // 獲取寫鎖
    raw_spin_lock_irqsave(&timekeeper_lock, flags);
    write_seqcount_begin(&timekeeper_seq);
        
    // 更新時間
    timekeeping_forward_now(tk);
    xt = tk_xtime(tk);
    ts_delta.tv_sec = tv->tv_sec - xt.tv_sec;
    ts_delta.tv_nsec = tv->tv_nsec - xt.tv_nsec;
    tk_set_wall_to_mono(tk, timespec_sub(tk->wall_to_monotonic, ts_delta));
    tk_set_xtime(tk, tv);
    timekeeping_update(tk, true, true);

    // 釋放寫鎖
    write_seqcount_end(&timekeeper_seq);
    raw_spin_unlock_irqrestore(&timekeeper_lock, flags);

    return 0;
}

每一個時鐘中斷(節(jié)拍)便會更新一次捐凭,很清晰的 “獲取寫鎖 -> 更新時間 -> 釋放寫鎖” 的過程拨扶。

4.2. 定時器

定時器顧名思義,是一種軟件功能茁肠,即允許在將來的某個時刻患民,函數(shù)在給定的時間間隔用完時被調(diào)用。這是一個真正的重頭戲垦梆,各種系統(tǒng)中的事件都依賴于定時器去完成匹颤。
定時器分為靜態(tài)定時器和動態(tài)定時器仅孩,靜態(tài)定時器一般執(zhí)行一些周期性的固定工作,如更新系統(tǒng)運行時間惋嚎、更新實際時間杠氢、平衡各個處理器上的運行隊列、檢查進(jìn)程時間片另伍、更新各種統(tǒng)計值等鼻百。
動態(tài)定時器被動態(tài)地創(chuàng)建和撤銷。由于硬件定時器的有限性摆尝,動態(tài)定時器應(yīng)用更為廣泛温艇,故我們著重對動態(tài)定時器進(jìn)行展開。

4.2.1. 相關(guān)數(shù)據(jù)結(jié)構(gòu)

4.2.2.1. HZ

節(jié)拍率(HZ)是時鐘中斷的頻率堕汞,表示一秒內(nèi)時鐘中斷的次數(shù)勺爱。HZ值一般與體系結(jié)構(gòu)有關(guān),常設(shè)置為100讯检。HZ值高則時鐘中斷程序運行的更加頻繁琐鲁,依賴時間執(zhí)行的程序更加精確,對資源消耗和系統(tǒng)運行時間的統(tǒng)計更加精確人灼;同時围段,時鐘中斷執(zhí)行的頻繁會占用的CPU時間過多,增加系統(tǒng)負(fù)擔(dān)投放。

4.2.2.2. jiffies

jiffies變量是一個計數(shù)器奈泪,用來記錄自系統(tǒng)啟動以來產(chǎn)生的節(jié)拍總數(shù)。
linux/v3.10/source/include/linux/jiffies.h 中查看定義:

/*
 * The 64-bit value is not atomic - you MUST NOT read it
 * without sampling the sequence number in jiffies_lock.
 * get_jiffies_64() will do this for you as appropriate.
 */
extern u64 __jiffy_data jiffies_64;
extern unsigned long volatile __jiffy_data jiffies;

每次時鐘中斷(節(jié)拍)發(fā)生它便加一灸芳,32位的 jiffies 最大值為 (2^32 - 1)涝桅,因此每隔大約50天便會回繞一次。為了避免此問題烙样,jiffies在啟動時并不是被指定為0冯遂,而是指定為 (-300*HZ) 烫饼,因此语稠,計數(shù)器將會在系統(tǒng)啟動后的5分鐘內(nèi)處于溢出狀態(tài),使得沒有做溢出檢測的內(nèi)核代碼在開發(fā)階段被及時地發(fā)現(xiàn)蛛碌。在 linux/v3.10/source/include/linux/jiffies.h#L162 中可以找到該定義:

/*
 * Have the 32 bit jiffies value wrap 5 minutes after boot
 * so jiffies wrap bugs show up earlier.
 */
#define INITIAL_JIFFIES ((unsigned long)(unsigned int) (-300*HZ))

不僅如此究反,Linux還定義了time_after寻定、time_after_eq、time_before精耐、time_before_eq等宏處理回繞問題狼速。其實可以類比為將 unsigned long 類型轉(zhuǎn)換為 long 類型,在1ms為一個節(jié)拍的情況下卦停,jiffies_64需要數(shù)十億年才會發(fā)生回繞向胡,巧妙地解決了此問題恼蓬。

4.2.2.3. timer_list

動態(tài)定時器用 timer_list 數(shù)據(jù)結(jié)構(gòu)表示,該數(shù)據(jù)結(jié)構(gòu)在 /linux/v3.10/source/include/linux/timer.h#L12 中定義如下(部分):

struct timer_list {
    struct list_head entry;
    unsigned long expires;
    struct tvec_base *base;
    void (*function)(unsigned long);
    unsigned long data;
};

其中僵芹,entry字段用于將軟定時器插入鏈表中处硬,后文將詳細(xì)介紹相關(guān)算法;expire字段指定定時器的到期時間拇派,用節(jié)拍數(shù)表示荷辕,其值為系統(tǒng)啟動以來所經(jīng)過的節(jié)拍數(shù),當(dāng)expire的值小于或等于jiffies的值時件豌,就說明定時器到期或終止疮方;function字段包含定時器到期執(zhí)行函數(shù)的地址;data字段指定傳遞給定時器函數(shù)的參數(shù)茧彤。

4.2.2. 動態(tài)定時器算法

講過動態(tài)定時器的表示方法骡显,接下來講動態(tài)定時器的工作算法。定時器的三個操作:添加 (add_timer)曾掂、刪除 (del_timer) 以及到期處理(tick 中斷)都對精度和延遲有巨大影響惫谤,而其精度和延遲又對應(yīng)用有巨大影響。Linux在定時器的處理上經(jīng)歷了幾個階段:

4.2.2.1. “原始”算法

簡單考慮珠洗,動態(tài)定時器被定義為一個帶指針的數(shù)據(jù)結(jié)構(gòu)溜歪,那么只需要一個全局的鏈表即可存儲所有動態(tài)定時器。每次需要新的timer時险污,只需要向這個鏈表中添加一個 timer_list 元素痹愚,每個節(jié)拍到來時遍歷該鏈表富岳。但是顯然蛔糯,一個無序鏈表元素數(shù)量增加時遍歷的成本會線性增加,增窖式、刪蚁飒、到期的時間復(fù)雜度分別為O(1)、O(n)萝喘、O(n)淮逻,計算機中會存在極大數(shù)量的定時器,不理想阁簸。


image.png

4.2.2.2. 排序后的算法

自然而然想到在插入鏈表時排序爬早,增、刪启妹、到期的時間復(fù)雜度分別為O(n)筛严、O(1)、O(1)饶米,雖然到期處理請求快了一些桨啃,但是整體還不理想车胡。


image.png

4.2.2.3. 最小堆算法

最小堆是我們熟悉的數(shù)據(jù)結(jié)構(gòu),利用最小堆可以在鏈表排序的基礎(chǔ)上進(jìn)一步減小新增定時器的操作復(fù)雜度照瘾。增匈棘、刪、到期的時間復(fù)雜度分別為O(logn)析命、O(1)主卫、O(1)。


image.png

4.2.2.4. 時間輪算法

時間輪算法并不會遍歷每一個定時器節(jié)點鹃愤,而是將定時器按照到期時間分配到各個節(jié)拍上队秩,隨著時鐘的增加,如果發(fā)現(xiàn)該節(jié)拍上存在定時器則該定時器到期昼浦。這樣形成一個類似輪狀結(jié)構(gòu)馍资,輪上每一個節(jié)點對應(yīng)一個鏈表或數(shù)組,節(jié)點的間隔就是定時器最小時間區(qū)分关噪。示意圖如下:


image.png

如圖所示鸟蟹,輪中共有N個bucket,每個bucket代表一秒(只作為示意使兔,實際精度很高)建钥,每個bucket對應(yīng)一個鏈表,鏈表中為當(dāng)前bucket將要到期的定時器對象虐沥。中間的指針被稱為cursor熊经,cursor指向bucket時則bucket對應(yīng)的鏈表全部做到期處理。
這樣一來欲险,增镐依、刪、到期的時間復(fù)雜度全部為O(1)天试。該算法的圖中圓輪可以通過數(shù)組實現(xiàn)槐壳。


image.png

時間復(fù)雜度上達(dá)到預(yù)期,可惜這個算法有一個致命的缺陷喜每,當(dāng)時間線被拉長务唐,精度提高,圖中的N將會非常大带兜,數(shù)組需要巨大的內(nèi)存消耗枫笛,這顯然是不現(xiàn)實的。

4.2.2.5. 分層時間輪算法

分層時間輪(Hierarchical Timing Wheels)算法是在時間輪的基礎(chǔ)上做了改進(jìn)刚照,將單一的 bucket 數(shù)組分成了幾個不同的數(shù)組刑巧,每個數(shù)組表示不同的時間精度。示意圖如下所示:


image.png

上圖的一個分層時間輪有三級,分別表示小時海诲、分鐘和秒繁莹。在小時數(shù)組中,每個 bucket 代表一個小時特幔。采用原始的時間輪如果我們要表示一天咨演,且 bucket 精度為一秒時,我們需要 24 * 60 * 60 = 86400 個 bucket蚯斯;而采用分層時間輪薄风,我們只需要 24 + 60 + 60 = 144 個 bucket。極大地減小了空間復(fù)雜度拍嵌,同時增遭赂、刪、到期的時間復(fù)雜度全部為O(1)横辆。
看上去像一個完美的算法撇他,實際并不然。當(dāng)每次秒鐘數(shù)組復(fù)位時狈蚤,下一分鐘的定時器需要重排列到各個秒鐘bucket中困肩,每次分鐘數(shù)組復(fù)位時同樣,這個操作被稱為“cascades”脆侮。舉例說明锌畸,比如當(dāng)前時間是早上08:00:00,要添加一個早上09:05:58觸發(fā)的定時器靖避,則該定時器剛添加時指針是掛在小時數(shù)組中09的位置的潭枣;當(dāng)08:59:59過去后,將掛在小時數(shù)組中09位置的所有定時器按照到期的分鐘數(shù)排列到分鐘數(shù)組中幻捏;當(dāng)09:04:59秒過去后盆犁,將掛在分鐘數(shù)組中05位置的所有定時器按照到期的秒鐘數(shù)排列到秒鐘數(shù)組中。則該09:05:58觸發(fā)的定時器被分配到了秒鐘數(shù)組的58位置粘咖。當(dāng)cursor經(jīng)過該位置時蚣抗,定時器被觸發(fā)侈百。每次cascades操作的時間復(fù)雜度是O(m)瓮下,該m為每個bucket中定時器的個數(shù),多數(shù)情況下 m 遠(yuǎn)小于系統(tǒng)中所有定時器個數(shù)钝域。

4.2.2.6. 紅黑樹算法

實時應(yīng)用讽坏、多媒體軟件對時鐘和定時器的精度要求不斷提高,在早期 Linux 內(nèi)核中例证,定時器所能支持的最高精度是一個 tick路呜。為了提高時鐘精度,人們只能提高內(nèi)核的 HZ 值,更高的 HZ 值意味著時鐘中斷更加頻繁胀葱,內(nèi)核要花更多的時間進(jìn)行時鐘處理漠秋。同時,高精度時鐘硬件的出現(xiàn)對于定時器算法也提出了更高的要求抵屿。
雖然時間輪是一種有效的管理數(shù)據(jù)結(jié)構(gòu)庆锦,但其 cascades 操作有不可預(yù)料的延遲。它更適于被稱為“timeout”類型的低精度定時器轧葛,即不等觸發(fā)便被取消的 Timer搂抒。這種情況下,cascades 可能造成的時鐘到期延誤不會有任何不利影響尿扯,因為根本等不到 cascades求晶,換句話說,多數(shù) Timer 都不會觸發(fā) cascades 操作衷笋。而高精度定時器的用戶往往是需要等待其精確地被觸發(fā)芳杏,執(zhí)行對時間敏感的任務(wù),因此 cascades 操作帶來的延遲是無法接受的辟宗。所以內(nèi)核開發(fā)人員不得不放棄時間輪算法蚜锨,轉(zhuǎn)而尋求其他的高精度時鐘算法,最終開發(fā)人員選擇了內(nèi)核中最常用的高性能查找算法:紅黑樹來實現(xiàn) hrtimer慢蜓。
所有的 hrtimer 實例都被保存在紅黑樹中亚再,添加 Timer 就是在紅黑樹中添加新的節(jié)點;刪除 Timer 就是刪除樹節(jié)點晨抡。紅黑樹的值為到期時間氛悬。Timer 的觸發(fā)和設(shè)置管理不在定期的 tick 中斷中進(jìn)行,而是動態(tài)調(diào)整:當(dāng)前 Timer 觸發(fā)后耘柱,在中斷處理的時候如捅,將高精度時鐘硬件的下次中斷觸發(fā)時間設(shè)置為紅黑樹中最早到期的 Timer 的時間。時鐘到期后從紅黑樹中得到下一個 Timer 的到期時間调煎,并設(shè)置硬件镜遣,如此循環(huán)反復(fù)。

4.2.2.7. 延遲函數(shù)

嚴(yán)格來講延遲函數(shù)并不應(yīng)該歸在這一小節(jié)士袄,因為并不屬于動態(tài)定時器的演化范圍悲关。但是它也是一種定時器的實現(xiàn)方式,故在此簡單講解娄柳。
當(dāng)內(nèi)核需要等待一個較短的時間間隔寓辱,如不超過毫秒時,就無需使用動態(tài)定時器赤拒,且動態(tài)定時器由于相對較大的設(shè)置開銷也是不精確的秫筏。在這些情況下诱鞠,內(nèi)核使用 udelay() 或 ndelay() 函數(shù),接收一個微秒或納秒級別的參數(shù)这敬,并在延遲結(jié)束后返回航夺。如果可以利用TSC或HPET硬件,則該方法使用它們來獲得精確的時間測量崔涂,否則該方法會執(zhí)行一個緊湊指令循環(huán)的n次循環(huán)敷存,至到達(dá)指定時間。

5. 時鐘振蕩器硬件

上文中所有都還停留在軟件和集成硬件層面堪伍,我們常說“節(jié)拍到來時觸發(fā)何種操作”锚烦,可是節(jié)拍是如何產(chǎn)生的?芯片本身通常并不具備時鐘信號源帝雇,因此須由專門的振蕩電路提供時鐘信號涮俄。說起振蕩,我們腦海里第一反應(yīng)可能是高中學(xué)習(xí)的LC振蕩器尸闸,通過電場和磁場的周期性變化產(chǎn)生固定的頻率:


image.png

根據(jù)這個簡單的原理產(chǎn)生了EE專業(yè)傳世經(jīng)典:NE555定時器芯片


image.png

當(dāng)然這是經(jīng)典的實現(xiàn)方式彻亲,但是如今計算機中常用的時鐘振蕩源是晶體振蕩器,也就是我們常說的“晶振”吮廉。
石英晶體振蕩器(Quartz Crystal OSC)是一種最常用的時鐘信號振蕩源苞尝。石英晶體就是純凈的二氧化硅,是二氧化硅的單晶體宦芦。從一塊晶體上按一定的方位角切下薄片(稱為"晶片")宙址,在晶片的兩個表面上涂覆一層薄薄的銀層后接上一對金屬板,焊接引腳调卑,并用金屬外殼封裝抡砂,就構(gòu)成了石英晶體振蕩器。

石英晶片之所以能當(dāng)為振蕩器使用恬涧,是基于它的壓電效應(yīng):在晶片的兩個極上加電場注益,會使晶體產(chǎn)生機械變形;在石英晶片上加上交變電壓溯捆,晶體就會產(chǎn)生機械振動丑搔,同時機械變形振動又會產(chǎn)生交變電場,雖然這種交變電場的電壓極其微弱提揍,但其振動頻率是十分穩(wěn)定的啤月。

為什么石英晶片會有壓電效應(yīng)?下面是唐僧念經(jīng)可以不看==
石英是日常最常見的非線性晶體碳锈,來源于硅氧四面體不能完美密堆積而產(chǎn)生的各種晶胞構(gòu)象顽冶。由有潛在極性的硅氧四面體規(guī)則堆積成晶體,所以在某些晶面上售碳,石英整體會呈現(xiàn)極性,并因為電荷的規(guī)則分層排布成為電介質(zhì)。當(dāng)外加電壓的時候贸人,石英晶體會發(fā)揮電介質(zhì)的特性间景,讓硅氧四面體變形來極化自己,從而抵消外加的電場艺智,在此過程中產(chǎn)生力學(xué)特性倘要。相反,當(dāng)石英晶體受力的時候十拣,晶胞參數(shù)就會改變封拧,使其極性變化,從而硅氧四面體和石英本身的介電常數(shù)改變夭问,這樣一來再受電場的時候泽西,因為其介電常數(shù)變化,就會使得電容變化缰趋,可以測得電壓變化捧杉,形變的彈性也會改變。這就是壓電晶體的正壓電和反壓電效應(yīng)秘血。當(dāng)固定的晶體受力的時候味抖,當(dāng)然會回彈產(chǎn)生振動。如果外加電場也周期性變化灰粮,使得晶體周期性受力仔涩,和石英晶體的固有頻率相同,那么晶體就會受到電場而反復(fù)振動粘舟,反過來增強電場红柱,形成諧振。

由于我們的主攻方向是軟件層面蓖乘,辛勤的EE工程師為我們提供了有力的硬件保障锤悄,所以關(guān)于實際底層硬件就不再多講,感興趣的同學(xué)可以自行繼續(xù)研究模電數(shù)電等知識嘉抒。

6. 總結(jié)與思考

6.1. 文章總結(jié)

本篇文章接續(xù)上篇-應(yīng)用篇零聚,從系統(tǒng)和硬件的角度著重講解了在高級語言之下的層次,時間和日期的相關(guān)問題些侍。
首先從問題探究的角度隶症,拋出了不同系統(tǒng)中 System.currentTimeMillis() 方法調(diào)用的時間成本相差巨大的問題,并從此問題入手深入探究了 Windows 和 Linux 系統(tǒng)中 System.currentTimeMillis() 方法的執(zhí)行流程岗宣,也即上層對系統(tǒng)時間點的獲取過程蚂会,并解釋了文首的問題。
之后耗式,根據(jù)延續(xù)該問題引出的硬件時鐘概念胁住,介紹了系統(tǒng)中主要的幾種時間相關(guān)硬件趁猴,包括實時時鐘、計數(shù)器彪见、高精度定時器等儡司。同時介紹了Linux系統(tǒng)對于不同硬件的數(shù)據(jù)結(jié)構(gòu)表示管理方法,及不同硬件時間源的比較余指。
接下來以Linux為例捕犬,介紹了系統(tǒng)的計時體系結(jié)構(gòu),主要包括系統(tǒng)時間和定時器酵镜。系統(tǒng)時間包括時間點的初始化碉碉、獲取、更新方法淮韭。定時器介紹了相關(guān)的概念和數(shù)據(jù)結(jié)構(gòu)垢粮,著重講解了動態(tài)定時器算法的演進(jìn)過程,從演進(jìn)過程中可以看出Linux對于系統(tǒng)的一步步優(yōu)化缸濒。
最后足丢,簡要介紹了時鐘的最底層來源——時鐘振蕩器硬件,以及它的原理壓電效應(yīng)庇配,從而基本打通了從應(yīng)用層到硬件層的時間相關(guān)原理斩跌。

6.2. 反思回顧

全文(包含應(yīng)用篇和系統(tǒng)&硬件篇)是從實際開發(fā)的角度出發(fā)的,按照從頂層向底層的角度進(jìn)行介紹捞慌。但是實際的歷史是自下而上發(fā)展的耀鸦,先有了基礎(chǔ)的物理學(xué)支撐和電子硬件支撐,才能發(fā)展出上層至高級語言和數(shù)據(jù)庫技術(shù)的應(yīng)用啸澡,應(yīng)用的需求進(jìn)而倒逼硬件的發(fā)展袖订。技術(shù)發(fā)展進(jìn)步是迅速的,一個個框架更新?lián)Q代嗅虏,一個個新技術(shù)不斷涌現(xiàn)洛姑,但是很多底層原理鮮有改變,這也是我們努力的方向皮服,厚積才能薄發(fā)楞艾,掌握基本原理才能更好更快地學(xué)習(xí)從而跟上時代技術(shù)進(jìn)步。
本文Linux版本是v3.10龄广,每個版本的實現(xiàn)方式都有一定差異硫眯,此次深入探索只是講出了大致的思想方法,具體版本還需要讀者自己查看對應(yīng)源碼择同。
時間和節(jié)拍是計算機系統(tǒng)運行的根基两入,相關(guān)概念、原理敲才、源碼等車載斗量裹纳。本篇文章雖篇幅較長择葡,但是并不能詳盡的解釋系統(tǒng)所有關(guān)于日期時間的問題,只是撿拾了筆者認(rèn)為需要關(guān)注的一些點痊夭,更多的問題歡迎大家一同探討刁岸。
在寫文章過程中脏里,越向底層探究越發(fā)現(xiàn)參考資料變少她我,很多問題想搞清楚要通過閱讀源碼來解決。而自己閱讀源碼的功力不足迫横,加之本身知識經(jīng)驗有限番舆,所以可能某些問題上理解不恰當(dāng)或表述不清楚,歡迎大家指正矾踱。

7. 參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末恨狈,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子呛讲,更是在濱河造成了極大的恐慌禾怠,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件贝搁,死亡現(xiàn)場離奇詭異吗氏,居然都是意外死亡,警方通過查閱死者的電腦和手機雷逆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進(jìn)店門弦讽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人膀哲,你說我怎么就攤上這事往产。” “怎么了某宪?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵仿村,是天一觀的道長。 經(jīng)常有香客問我兴喂,道長蔼囊,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任瞻想,我火速辦了婚禮压真,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蘑险。我一直安慰自己滴肿,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布佃迄。 她就那樣靜靜地躺著泼差,像睡著了一般贵少。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上堆缘,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天滔灶,我揣著相機與錄音,去河邊找鬼录平。 笑死表箭,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼阳似!你這毒婦竟也來了泽疆?” 一聲冷哼從身側(cè)響起驱证,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎币狠,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肛炮,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡止吐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了侨糟。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片碍扔。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖秕重,靈堂內(nèi)的尸體忽然破棺而出不同,到底是詐尸還是另有隱情,我是刑警寧澤悲幅,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布套鹅,位于F島的核電站站蝠,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏卓鹿。R本人自食惡果不足惜菱魔,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望吟孙。 院中可真熱鬧澜倦,春花似錦、人聲如沸杰妓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽巷挥。三九已至桩卵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間倍宾,已是汗流浹背雏节。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留高职,地道東北人钩乍。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像怔锌,于是被迫代替她去往敵國和親寥粹。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 1.描述計算機的組成及其功能 (一)計算機的組成 1.CPU 2.CPU風(fēng)扇 3.BIOS 4.內(nèi)存 5.硬盤 6...
    whamai閱讀 1,433評論 0 1
  • 1 綜述 1.1 時鐘源 在STM32中埃元,一共有5個時鐘源涝涤,分別是HSI、HSE亚情、LSI妄痪、LSE、PLL楞件。 HSI...
    hackvilin閱讀 3,407評論 0 6
  • 陷阱分發(fā) 陷阱(trap)指的是這樣一種機制衫生,當(dāng)異常或中斷發(fā)生時土浸,處理器捕捉到一個執(zhí)行線程罪针,并且將控制權(quán)轉(zhuǎn)移到...
    kotw_zjc閱讀 1,215評論 0 0
  • 其實這一年來 過得一點也不好 不好就是不好 一遮掩就會累一解釋就疲憊 倒是學(xué)會很多 做事保留話說三分不付出真心 人...
    暴風(fēng)語閱讀 241評論 0 1
  • 進(jìn)程和線程 進(jìn)程是資源分配的基本單位,例如內(nèi)存資源等黄伊,進(jìn)程之間資源是相互隔離的泪酱;線程是操作系統(tǒng)調(diào)度的最小單位。一個...
    Pig_wu飼養(yǎng)員閱讀 2,746評論 0 0