引子
熟悉了基本的編程方法之後,我們的興趣就在於,計算機如何實現這一切的呢?在那些應用層 API 和底層系統硬件之間,操作系統和庫函數究竟做了些什麼?
首先看下 Linux 時間處理的一般過程:
圖 1. 時間處理過程
應用程序部分已經在第一部分詳細介紹過了,在第二部分我將介紹硬件和 GlibC 相關實現的一些概況。
硬件
PC 機裡常見的時鐘硬件有以下這些。
RTC (Real Time Clock,實時時鐘)
人們需要知道時間的時候,可以看看鐘表。計算機系統中鐘表類似的硬件就是外部時鐘。它依靠主板上的電池,在系統斷電的情況下,也能維持時鐘的准確性。計算機需要知道時間的時候,就需要讀取該時鐘。
在 x86 體系中,這個時鐘一般被稱為 Real Time Clock。RTC 是主板上的一個 CMOS 芯片,比如 Motorola 146818,該芯片獨立於 CPU 和其他芯片,可以通過 0x70 和 0x71 端口操作 RTC。RTC 可以周期性地在 IRQ 8 上觸發中斷,但精度很低,從 2HZ 到 8192HZ。
以 Motorola 146818 為例,軟件可以通過 I/O 指令讀寫以下這些值:
圖 2. Motorola 146818
可以看到,RTC 能提供精確到秒的實時時間值。
TSC (Time Stamp Counter)
CPU 執行指令需要一個外部振蕩器產生時鐘信號,從 CLK 管腳輸入。x86 提供了一個 TSC 寄存器,該寄存器的值在每次收到一個時鐘信號時加一。比如 CPU 的主頻為 1GHZ,則每一秒時間內,TSC 寄存器的值將增加 1G 次,或者說每一個納秒加一次。x86 還提供了 rtdsc 指令來讀取該值,因此 TSC 也可以作為時鐘設備。TSC 提供了比 RTC 更高精度的時間,即納秒級的時間精度。
PIT (Programmable Interval Timer)
PIT 是 Programmable Interval Timer 的縮寫,該硬件設備能定時產生中斷。早期的 PIT 設備是 8254,現在多數可以集成在 Intel 的 I/O Control Hub 電路中,可以通過端口 0x40~0x43 訪問 PIT。系統利用 PIT 來產生周期性的時鐘中斷,時鐘中斷通過 8259A 的 IRQ0 向 CPU 報告。它的精度不高,其入口 clock 的頻率為 1MHz,理論上能產生的最高時鐘頻率略小於 0.5MHz。實際系統往往使用 100 或者 1000Hz 的 PIT。
HPET (High Precision Event Timer)
PIT 的精度較低,HPET 被設計來替代 PIT 提供高精度時鐘中斷(至少 10MHz)。它是由微軟和 Intel 聯合開發的。一個 HPET 包括了一個固定頻率的數值增加的計數器以及 3 到 32 個獨立的計時器,這每一個計時器有包涵了一個比較器和一個寄存器(保存一個數值,表示觸發中斷的時機)。每一個比較器都比較計數器中的數值和寄存器中的數值,當這兩個數值相等時,將產生一個中斷。
APIC Timer (Advanced Programmable Interrupt Controller Timer)
APIC ("Advanced Programmable Interrupt Controller") 是早期 PIC 中斷控制器的升級,主要用於多處理器系統,用來支持復雜的中斷控制以及多 CPU 之間的中斷傳遞。APIC Timer 集成在 APIC 芯片中,用來提供高精度的定時中斷,中斷頻率至少可以達到總線頻率。系統中的每個 CPU 上都有一個 APIC Timer,而 PIT 則是由系統中所有的 CPU 共享的。Per CPU 的 Timer 簡化了系統設計,目前 APIC Timer 已經集成到了所有 Intel x86 處理器中。
以上這些硬件僅僅是 x86 體系結構下常見的時間相關硬件,其他的體系結構如 mips、arm 等還有它們常用的硬件。這麼多的硬件令人眼花缭亂,但其實無論這些硬件多麼復雜,Linux 內核只需要兩種功能:
一是定時觸發中斷的功能;
另一個是維護和讀取當前時間的能力。
一些硬件提供了中斷功能,一些硬件提供了讀取時間的功能,還有一些硬件則能夠提供兩種功能。下表對上面描述過的硬件進行了一個簡單的總結:
也許您已經發現,這些硬件提供的功能非常簡單,為了滿足應用程序的各種各樣的需求,Linux 內核和 C 標准庫還需要做很多工作,才能讓我們使用諸如 gettimeofday()、setitimer() 等函數進行時間相關的操作。
C 庫函數的工作
我們在第一部分已經詳細介紹了標准 C 庫中關於時間函數的用法。表 2 羅列了一些主要的 API。
本文力圖簡短,無法對上表中的每一個 API 進行詳細分析。幸運的是,我們只需要研究幾個典型 API 的實現,便可以舉一反三,了解其他 API 的大致實現思想。
time() 的實現
第一個典型 API 是 time(),我們參考 GlibC2.13 版本的實現。
清單 1.time 的 GlibC 實現
time_t time (time_t *t) { INTERNAL_SYSCALL_DECL (err); time_t res = INTERNAL_SYSCALL (time, err, 1, NULL);//系統調用 return res; }
可以看到,GlibC 的 time() 函數只是調用了 time 系統調用,來返回時間值。同樣,如果我們查看 gettimeofday() 等很多 API,將會發現它們也是僅僅調用了 Linux 的系統調用來完成指定的功能。根據我的分析,下面這些函數都是直接調用了 Linux 的系統調用來完成工作:
 
ftime() 的實現
ftime() 在 Glibc 中的代碼實現在 sysdeps/unix/bsd/ftime.c,因為在 Linux 系統中 ftime 系統調用已經過時了,目前如果還有調用 ftime() 的應用程序 GLibc 將用 gettimeofday() 來模擬,具體代碼如下:
清單 2,ftime 的 GlibC 實現
int ftime (timebuf) struct timeb *timebuf; { struct timeval tv; struct timezone tz; if (__gettimeofday (&tv, &tz) < 0) //調用 gettimeofday return -1; timebuf->time = tv.tv_sec; timebuf->millitm = (tv.tv_usec + 500) / 1000; if (timebuf->millitm == 1000) { ++timebuf->time; timebuf->millitm = 0; } timebuf->timezone = tz.tz_minuteswest; timebuf->dstflag = tz.tz_dsttime; return 0; }
timer_create() 的實現
多數 GLibC 中的時間函數只是對系統調用的簡單封裝,不過 timer_create 要算是一個特例,雖然它的大部分功能都是通過系統調用 sys_timer_create 完成的。但是如果 GlibC 發現 timer 的到期通知方式被設置為 SIGEV_THREAD 時,Glibc 需要自己完成一些輔助工作,因為內核無法在 Timer 到期時啟動一個新的線程。
考察文件 nptl\sysdeps\unix\sysv\linux\timer_create.c,可以看到 GLibc 發現用戶需要啟動新線程通知時,會自動調用 pthread_once 啟動一個輔助線程(__start_helper_thread),用 sigev_notify_attributes 中指定的屬性設置該輔助線程。
然後 Glibc 啟動一個普通的 POSIX Timer,將其通知方式設置為:SIGEV_SIGNAL | SIGEV_THREAD_ID。這樣就可以保證內核在 timer 到期時通知輔助線程。通知的 Signal 號為 SIGTIMER,並且攜帶一個包含了到期函數指針的數據。這樣,當該輔助 Timer 到期時,內核會通過 SIGTIMER 通知輔助線程,輔助線程可以在信號攜帶的數據中得到用戶設定的到期處理函數指針,利用該指針,輔助線程調用 pthread_create() 創建一個新的線程來調用該處理函數。這樣就實現了 POSIX 的定義。
綜上所述,除了少數 API(比如 timer_create),需要 GLibC 做部分輔助工作之外,大部分 GLibC API 的工作可以總結為:調用相應的系統調用。
ctime() 的實現
還有一些 API 我們還沒有分析,即那些時間格式轉換函數。這些函數的功能是將一個時間值轉換為人類容易閱讀的形式,因此這些函數的實現完全是在 GlibC 中完成,而無需內核的系統調用。下面我們看一看 ctime() 吧:
清單 3,ctime 的 GlibC 實現
char * ctime (const time_t *t)
{ return asctime (localtime (t));}
localtime() 和 asctime() 的實現都比較復雜,但歸根結底是進行復雜的格式轉換,時區轉換計算等等。這些工作都是完全在 GlibC 內部實現的,無須內核參與。感興趣的讀者可以仔細研究 Glibc time 目錄下的 localtime.c、tzset.c 等具體實現。
小結
在這一部分中,我們首先了解到了一些硬件時鐘設備的簡單知識。無論這些設備本身如何復雜和不同,它們只提供兩個主要功能:計時功能和定時中斷功能。要想利用這兩個基本功能來滿足應用的需求似乎還有很多工作,比如:如何衡量實時時間和 CPU 時間?
通過對 GlibC 的簡單分析,我們也看到庫函數實際上把復雜問題統統交給了內核。GlibC 僅僅是一個中轉站,把用戶請求發給內核。因此想了解更多,我們必須進入內核。
在接下來的第三部分和第四部分,我們將介紹 Linux 內核的時間系統。