1. 程式人生 > >深入理解 Linux 核心---定時測量

深入理解 Linux 核心---定時測量

很多計算機化的活動都是由定時測量驅動的,這常常對使用者不可見。

Linux 核心必須完成兩種主要的定時測量:

  • 儲存當前的時間和日期,可由 time()、ftime()、gettimeofday() 返回給使用者程式,也可由核心本身把當前時間作為檔案和網路包的時間戳。
  • 維持定時器,告訴核心或使用者程式某一時間間隔已經過去了。

定時測量是由基於固定頻率振盪器計數器的幾個硬體電路完成的。

時鐘和定時器電路

實時時鐘(RTC)

獨立於 CPU 和其他晶片。即使 PC 被切斷電源,RTC 仍能工作。

RTC 能在 IRQ8 上發出週期性中斷,也可對 RTC 程式設計使之在特定時間啟用 IRQ8 線。

Linux 只用 RTC 獲取時間和日期,但通過對 /dev/rtc 裝置檔案操作,也允許對 RTC 程式設計。核心通過 0x70 和 0x71 I/O 埠訪問 RTC。

時間戳計數器(TSC)

64 位的時間戳計數器,在每個時鐘訊號到來時加 1。rdtsc 彙編指令讀該暫存器。

為了比可程式設計間隔定時器精確,Linux 在初始化系統的時候必須確定時鐘訊號的頻率。calibrate_tsc() 通過計算一個大約在 5ms 的時間間隔內產生的時鐘訊號的個數算出 CPU 的實際頻率。

可程式設計間隔定時器(PIT)

通過時鐘中斷通知核心有一個時間間隔過去了。PIT 永遠以核心確定的固定頻率發出中斷。

Linux 給 PC 的第一個 PIT 程式設計,使它以大約 1000 Hz 的頻率向 IRQ0 發出時鐘中斷,即每 1ms

產生一次時鐘中斷。該時間間隔為一個節拍,以納秒為單位存放在 tick_nsec 變數中,被初始化為 9999848ns,如果被計算機外部時鐘同步,可被核心自動調整。

短的節拍可產生較高分辨度的定時器。Linux 程式碼中,有幾個巨集產生決定時鐘中斷頻率的常量:

  • HZ 產生每秒鐘時鐘中斷的近似個數,即時鐘中斷頻率。IBM PC 上,為 1000。
  • CLOCK_TICK_RATE 產生的值為 1193182,為 8254 晶片的內部振盪器頻率。
  • LATCH 產生 CLOCK_TICK_RATE 和 HZ 的比值四捨五入後的整數值。

PIT 由 setup_pit_timer() 進行初始化

// 使 PIT 以大約 1000 Hz 的頻率產生時鐘中斷,即每 1ms 產生一次時鐘中斷

setup_pit_timer()
spin_lock_irqsave(&i8253_lock, flags);

// 類似於 outb(),但會通過一個同操作而產生一個暫停,以避免硬體難以分辨
// 讓 PIT 以新的頻率產生中斷
outb_p(0x34, 0x43);  

// 接下來的兩條 outb_p()、out_b() 為裝置提供新的中斷頻率
// 將 16 位 LATCH 常量作為兩個連續的自己傳送到裝置的 8 位 I/O 埠 0x40
udelay(10);   // 引入一個更短的延遲
outb_p(LATCH & 0xff, 0x40);
udelay(10);
outb(LATCH >> 8, 0x40);   // 將第一個運算元拷貝到第二個運算元指定的 I/O 埠

spin_unlock_irqrestore(&i8253_lock, flags);

CPU 本地定時器

APIC 計數器是 32 位,而 PIC 計數器是 16 位;所以可對本地定時器程式設計產生很低頻率的中斷。

本地 APIC 定時器只把中斷髮送給自己的處理器,而 PIT 產生一個全域性性中斷。

APIC 定時器是基於匯流排時鐘訊號的,PIT 有自己的內部時鐘振盪器,可更靈活地程式設計。

高精度事件定時器(HPET)

在終端使用者機器上並不普遍。

主要包含 8 個 32 位或 64 位的獨立計數器。每個計數器由自己的時鐘訊號驅動,時鐘訊號頻率必須至少為 10MHz。每個計數器最多可與 32 個定時器關聯,每個定時器由一個比較器和一個匹配暫存器組成。

可通過對映到記憶體空間的暫存器低 HPET 晶片程式設計。

ACPI 電源管理定時器(ACPI PMT)

是一個簡單的計數器,在每個時鐘節拍到來時增加一次。

如果作業系統或 BIOS 通過動態降低 CPU 的工作頻率或工作電壓來解釋電池的電能,TSC 的頻率會改變,而 ACPI PMT 的頻率不會變。但 TSC 計數器的高頻率便於測量特別小的時間間隔。

Linux 計時體系結構

是一組與時間流相關的核心資料結構和函式。

  • 單處理系統上,計時活動由全域性定時器產生的中斷觸發。
  • 多處理器系統上,普通活動由全域性定時器產生的中斷觸發,具體 CPU 活動由本地 APIC 定時器產生的中斷觸發。

Linux 計時器體系結構還依賴於 TSC、ACPI PMT、HPET 的可用性。核心使用兩個基本的計時函式:

  • 保持當前最新的時間。
  • 計算在當前秒內走過的納秒數。可通過 TSC 或 HPET 等方式獲得。

計時體系的資料結構

定時器物件

timer_opts

  • name
  • mark_offset,記錄上一個節拍的準確時間,由時鐘中斷處理程式呼叫
  • get_offset,返回自上一個節拍開始所經過的時間
  • monotonic_clock
  • delay

mark_offset、get_offset 使得 Linux 計時體系結構能夠以比節拍週期更高的精度測定當前的時間,這種操作稱為“定時插補”

變數 cur_timer 存放了某個定時器物件的地址。

  • 最初,cur_timer 指向 timer_none,timer_none 是一個虛擬的定時器資源物件,核心在初始化的時候用它。
  • 在核心初始化期間,select_timer() 函式將 cur_timer 設定為指向適當定時器物件的地址。select_timer() 按優先順序選則的順序:HPET、ACPI PMT、TSC、PIT。

本地 APIC 定時器沒有對應的定時器物件。因為本地 APIC 定時器僅用來產生週期性中斷而不用來獲得子節拍的分辨度。

jiffies 變數

jiffies 變數是一個計數器,記錄自系統啟動以來產生的節拍總數。jiffies 是一個 32 位的變數,每隔約 50 天會迴繞到 0。

jiffies 在系統啟動時初始化為 0xfffb6c20,等於 -300000,系統啟動 5 分鐘內處於溢位狀態,使得不對 jiffies 作溢位檢測的有缺陷的核心程式碼在開發階段被及時發現。

核心有時需獲得自系統啟動以來產生的系統節拍的真實數目,因此,80x86 系統中,jiffies 變數通過聯結器被換算為一個 64 位計數器 jiffies_64 的低 32 位,1 ms 一個節拍的情況下,數十億年後才會迴繞。

get_jiffies_64() 函式用來讀取並返回 jiffies_64 的值:

unsigned long long get_jiffies_64(void)
{
	unsigned long seq;
	unsigned long long ret;
	do {
		seq = read_seqbegin(&xtime_lock);    // xtime_lock 順序鎖用來保護 64 位的讀操作
		ret = jiffies_64;   
	}while(read_seqretry(&xime_lock, seq));  // 一直讀 jiffies_64 變數直到確認該變數沒有被其他核心控制路徑更新
}

在臨界區增加 jiffies_64 變數的值時必須使用 write_seqlock(&xtime_lock) 和 write_sequnlock(&xtime_lock) 進行保護。

xtime 變數

xtime 變數存放當前時間和日期:是一個 timespec 型別的資料結構,有兩個欄位:

  • tv_sec,存放自 1970年1月1日(UTC)午夜以來經過的秒數。
  • tv_nsec,存放在上一秒開始經過的納秒數(值域範圍:0~999999999)。

xtime 每個節拍更新一次。

單處理器系統上的計時體系結構

單處理系統上,所有與定時有關的活動都是由 IRQ0 上的可程式設計間隔定時器 PIT 產生的中斷觸發的。

初始化階段

核心初始期間,time_init() 函式被呼叫來建立計時體系結構。

  1. 初始化 xtime 變數。get_cmos_time() 從實時時鐘 RTC 上讀取自 1970年1月1日午夜以來經過的秒數。設定 xime 的 tv_nsec 欄位,使得 jiffies 變數的溢位與 tv_sec 欄位的增加都落在秒的範圍內。
  2. 初始化 wall_to_monotonic 變數。同 xtime 一樣是 timespec 型別,但存放將被加到 xtime 上的秒數和納秒數。
  3. 如果核心支援 HPET,呼叫 hpet_enable() 函式確認 ACPI 韌體是否探測到該晶片並將其暫存器對映到記憶體地址空間。如果是,hpet_enable() 將對 HPET 晶片的第一個定時器程式設計,使其以每秒 1000 次的頻率引發 IRQ0 處的中斷引發 IRQ0 處的中斷;否則,不能獲得 HPET 晶片,核心將使用 PIT:該晶片已被 init_IRQ() 程式設計,以每秒 1000 次的頻率引發 IRQ 0 處的中斷。
  4. 呼叫 select_timer() 挑選系統中可利用的最好的定時器,將 cur_timer 指向其地址。
  5. 呼叫 set_irq(0, &irq0) 建立與 IRQ0 相應的中斷門,IRQ0 引腳線連線著系統時鐘中斷源(PIT 或 HPET)。irq0 變數的定義:
struct irqaction irq0 = {timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL}; 
// timer_interrupt() 會在每個節拍到來時被呼叫,
// 而中斷被禁止,因為 IRQ0 主描述符的狀態欄位中的 SA_INTERRUPT 標誌被置位。

時鐘中斷處理程式

timer_interrupt() 是 PIT 或 HPET 中的中斷服務例程(ISR),執行以下步驟:

  1. 在 xtime_lock 順序鎖上產生一個 write_seqlock() 來保護與定時相關的核心變數。
  2. 執行 cur_timer 定時器物件的 mark_offset 方法,有 4 種情況:
    a. cur_timer 指向 timer_hpet 物件:HPET 晶片作為時鐘中斷源。mark_offset 方法檢查自上一個節拍以來是否丟失時鐘中斷,如果丟失,更新 jiffies_64。接著,記錄 HPET 週期計數器的當前值。
    b. cur_timer 指向 timer_pmtmr 物件:PIT 晶片作為時鐘中斷源,但核心採用 APCI PMT 以更高分辨度測量時間。mark_offset 方法檢查自上一個節拍以來是否丟失時鐘中斷,如果丟失,更新 jiffies_64。接著,記錄 APIC PMT 計數器的當前值。
    c. cur_timer 指向 timer_tsc 物件:PIT 晶片作為時鐘中斷源,但核心採用 TSC 以更高分辨度測量時間。mark_offset 方法檢查自上一個節拍以來是否丟失時鐘中斷,如果丟失,更新 jiffies_64。接著,記錄 TSC 計數器的當前值。
    d. cur_timer 指向 timer_pit 物件:PIT 晶片作為時鐘中斷源,mark_offset 什麼方法也不做。
  3. 呼叫 do_timer_interrupt() 函式,執行以下操作:
    a. 使 jiffies_64 值增 1.
    b. 使用 update_times() 更新系統日期和時間,並計算當前系統負載。
    c. 呼叫 update_process_times() 為本地 CPU 執行幾個與定時相關的計數操作。
    d. 呼叫 profile_tick()。
    e. 如果使用外部時鐘同步系統時鐘,則每隔 660 秒呼叫一次 set_rtc_mmss() 調整實時時鐘。
  4. 呼叫 write_sequnlock() 釋放順序鎖。
  5. 返回 1,報告中斷已被有效處理。

多處理其系統上的計時體系結構

多處理器系統可依賴兩種不同的時鐘中斷源:PIT 或 HPET,及 APIC 計時器 產生的中斷。

Linux 2.6 中,PIT 或 HPET 產生的全域性時鐘中斷不涉及具體 CPU 的活動,而 APIC 時鐘中斷涉及本地 CPU 的計時活動。

初始化階段

全域性時鐘中斷處理程式由 time_init() 函式初始化。

Linux 核心為本地時鐘中斷保留第 239 號(0xef)中斷向量。

核心初始化階段,

  1. apic_intr_init() 根據第 239 號向量和低階中斷處理程式 apic_timer_interrupt() 的地址設定 IDT 的中斷門。
  2. calibrate_APIC_clock() 通過正在啟動的 CPU 的本地 APIC 來計算一個節拍內收到了多少個匯流排時鐘訊號。
  3. setup_APIC_timer() 用第 2 步中的值對本地所有 APIC 程式設計,使得每個 APIC 在每個節拍產生一次本地時鐘中斷。

所有本地 APIC 定時器都是同步的,因為它們都基於公共匯流排時鐘訊號,因此第 2 步算出的值對系統中的其他 CPU 同樣有效。

全域性時鐘中斷處理程式

SMP 版本的 timer_interrupt() 與 UP 版本有幾處差異:

  • timer_interrupt() 呼叫 do_timer_interrupt() 向 I/O APIC 晶片的一個埠寫入,以應答定時器的中斷請求。
  • update_process_times()、profile_tick() 不被呼叫,因為它們執行與特定 CPU 相關的操作。

本地時鐘中斷處理程式

執行與特定 CPU 相關的計時活動。

apic_timer_interrupt:
	pushl $(239-256)
	SAVE_ALL
	movl %esp, %eax
	call smp_apic_timer_interrupt
	jmp ret_from_intr

smp_apic_timer_interrupt() 的高階中斷處理函式執行如下步驟:

  1. 獲得 CPU 邏輯號(比如 n)。
  2. 使 irq_stat 陣列中第 n 項的 apic_timer_irqs 欄位加 1。
  3. 應答本地 APIC 上的中斷。
  4. 呼叫 irq_enter() 函式。
  5. 呼叫 smp_local_timer_interrupt() 函式。
  6. 呼叫 irq_exit() 函式。

smp_local_timer_interrupt() 執行每個 CPU 的計時活動:

  1. 呼叫 profile_tick() 函式。
  2. 呼叫 update_process_times() 檢查當前程序執行的時間並更新一些本地 CPU 統計數。

更新時間和日期

使用者程式從 xtime 變數中獲得當前時間和日期,核心必須週期性地更新該變數。

void update_times(void)
{
	unsigned long ticks;
	ticks = jiffies - vall_jiffies;   

	if(ticks)
	{
		wall_jiffies += ticks;           // 存放 xtime 變數最後更新的時間
		update_wall_time(ticks);
	}
	calc_load(ticks);  // 記錄系統負載
}

update_wall_time() 呼叫 update_wall_time_one_tick() ticks 次

  • 每次呼叫都給 xtime.tv_nsec 欄位加 1000000。
  • 當 tv_nsec 的值大於 999999999,update_wall_time() 會更新 xtime 的 tv_sec 欄位。

如果系統發出 adjtimex() 呼叫,函式可能會調整 1000000 這個值。

更新系統統計計數

核心在與定時相關的其他任務中必須週期性地收集若干資料用於:

  • 檢查執行程序的 CPU 資源限制
  • 更新與本地 CPU 工作負載有關的統計數
  • 計算平均系統負載
  • 監管核心程式碼

更新本地 CPU 統計數

update_process_times() 更新一些核心統計計數,執行以下步驟:

  1. 檢查當前程序運行了多長時間。時鐘中斷髮生時,根據當前程序執行在使用者態還是核心態,選擇呼叫 account_user_time() 還是 account_system_time(),執行以下步驟:
    a. 更新當前程序描述符的 utime 欄位或 stime 欄位。程序描述符中的 cutime 和 cstime 附加欄位分別用來統計子程序在使用者態和核心態下所經過的 CPU 節拍數。為保證效率update_process_times() 只有當父程序詢問它的一個子程序的狀態時,這些欄位才會更新。
    b. 檢查是否已達到總的 CPU 時限,如果是,向 current 程序傳送 SIGXCPU 和 SIGKILL 訊號。
    c. 呼叫 account_it_virt() 和 account_it_prof() 檢查程序定時器。
    d. 更新一些核心統計數,這些統計數存放在每 CPU 變數 kstat 中。
  2. raise_softirq() 啟用本地 CPU 上的 TIMER_SOFTIRQ 任務佇列。
  3. 如果必須回收一些老版本的、受 RCU 保護的資料結構,那麼檢查本地 CPU 是否經歷了靜止狀態並呼叫 tasklet_schedule() 啟用本地 CPU 的 rcu_tasklet 任務佇列。
  4. scheduler_tick() 將當前程序的時間片計數器減 1,並檢查計數器是否已減到 0。

記錄系統負載

update_times() 在每個節拍都要呼叫 calc_load() 計算處於 TASK_RUNNING 或 TASK_UNINTERRUPTIBLE 狀態的程序數,並用這個資料更新平均系統負載。

監管核心程式碼

Linux 包含一個被稱為 readprofiler 的最低要求的程式碼監管器,用來確定核心的“熱點”— 執行最頻繁的核心程式碼片段。

監管器基於非常簡單的蒙特卡洛斷方:每次時鐘中斷髮生時,核心確定該中斷是否發生在核心態;如果是,核心總堆疊取回中斷髮生前 eip 暫存器的值,以判斷中斷髮生前核心正在做什麼。

profile_tick() 採集資料。在單處理器系統上被 do_timer_interrupt() 呼叫;多處理器系統上由 smp_local_timer_interrupt() 呼叫。

為啟用程式碼監管器,Linux 在核心啟動時傳遞字串引數“profile=N",2N2^N 表示要監管的程式碼段的大小。

採集的資料可通過 readprofile 系統命令從 /proc/profile 檔案中讀取。可通過修改該檔案重置計數器;多處理器系統上,還可改變抽樣頻率。

oprofile 監管器比 readprofile 更靈活、更可定製,還能發現核心程式碼、使用者態程式及系統中的熱點。

檢查非遮蔽中斷(NMI)監視器

在多處理器系統中,看門狗系統可探測引起系統凍結的核心 bug,為啟用它,必須在核心啟動時傳遞 nmi_watchdog 引數。

看門狗系統基於本地 I/O APIC,在每個 CPU 上產生週期性的 NMI 中斷,且不能被遮蔽。

一旦每個時鐘節拍到來,所有 CPU 都必須執行 NMI 中斷處理程式;該中斷處理程式呼叫 do_nmi()。

do_nmi():

  • 獲得 CPU 的邏輯號 n。
  • 檢查 irq_stat 陣列第 n 項的 apic_timer_irqs 欄位。如果該 CPU 欄位正常,第 n 項的 apic_timer_irq 欄位會被本地時鐘中斷處理程式增加,如果計數器沒有增加,說明本地時鐘中斷處理程式在整個時鐘節拍期間沒有被執行。

當 NMI 中斷處理程式檢測到一個 CPU 凍結時,就好敲響所有的鐘:把引起恐慌的訊號記錄在系統日誌,轉儲該 CPU 暫存器的內容和核心佔,最後殺死當前程序。為核心開發者提供了發現錯誤的機會。

軟定時器和延遲函式

每個定時器都包含一個欄位,表示定時器將需要多長時間才到期。該欄位的初值是 jiffies 的當前值加上合適的節拍數。每當核心檢查定時器時,就把該欄位和當前時刻的 jiffies 值比較,如果小於 jiffies,則定時器到期。

動態定時器由核心使用。間隔定時器可由程序在使用者態建立。

因為對定時器函式的檢查總是由可延遲函式執行,所以不適用於實時應用。

除了軟定時器,核心還使用延遲函式,執行一個緊湊的指令迴圈直到指定的時間間隔用完。

動態定時器

struct timer_list
{
	struct list_head entry;           // 將定時器插入雙向迴圈連結串列佇列種,根據定時器 expires 欄位的值將它們分組存放
	unsigned long expires;            // 定時器到期時間,用節拍數表示,<= jiffies 值時,到期
	spinlock_t lock;
	unsigned long magic;
	void (*function)(unsigned long);  // 定時器到期時執行的函式
	unsigned long data;               // 傳遞給定時器函式的引數,如裝置 ID 等
	tvsec_base_t *base;
};

為建立並激活一個動態定時器,核心必須:

  1. 如果需要,建立一個新的 timer_list 物件 t,可通過以下幾種方式建立:
  • 在程式碼種定義一個靜態全域性變數。
  • 在函式內定義一個區域性變數,存放在核心堆疊種。
  • 在動態分配的描述符種包含該物件。
  1. init_timer(&t) 初始化該物件。 t.base = NULL,將 t.lock 自旋鎖設為”開啟“。
  2. 將定時器到期時啟用函式的地址存放 function 欄位,如果需要,將傳遞函式的引數存入 data 欄位。
  3. 如果動態定時器還沒有被插入連結串列中,給 expires 賦一個合適的值,add_timer(&t) 將 t 插入到合適的連結串列中。
  4. 否則,mod_timer() 更新 expires 欄位。

定時器到期後,核心會自動把 t 從連結串列中刪除。程序也可通過呼叫 del_timer()、del_timer_sys() 或 del_singleshot_timer_sync() 顯式刪除。

Linux 2.6 中,動態定時器需要 CPU 來啟用。

動態定時器與競爭條件

釋放資源前停止定時器:

...
del_timer(&t);
X_Release_Resource();
...

但在多處理器系統上,呼叫 del_timer() 時,定時器函式可能已經在其他 CPU 上運行了。結果,當定時器函式還作用在資源上時,資源可能已經被釋放。為避免這種競爭條件,核心提供了 del_timer_sync() 函式,從連結串列中刪除定時器,如果定時器函式還在其他 CPU 上執行,等待直到定時器函式結束。

del_timere_sync() 必須小心考慮:定時器函式重新啟用自己。如果核心開發者直到定時器函式從不重新啟用定時器,可使用更簡單快速的 del_singleshot_timer_sync()。

還有其他競爭條件,如修改已啟用定時器 expires 欄位的方法 mod_timer(),為避免多個核心路徑交錯,每個 timer_list 物件包含 lock 自旋鎖,每當核心訪問動態定時器的連結串列時,就禁止中斷並獲取該自旋鎖。

動態定時器的資料結構

為提高效率,將 expires 值劃分成不同的大小,並允許動態定時器按 expires 值從大到小進行有效過濾。此外,在多處理器系統中活動的動態定時器集合被分配到各個不同的 CPU 中。

動態定時器的主要資料結構時一個叫 tvec_bases 的每 CPU 變數:包含 NR_CPUS 個元素,每個 CPU 一個。每個元素是一個 tvec_base_t 型別的資料結構

typedef struct tvsec_t_base_s
{
	spinlock_t lock;
	unsigned long timer_jiffies;       // 需要檢查的動態定時器的最早到期時間,如果小於 jiffies,說明前幾個節拍相關的可延遲函式必須處理
	struct timer_list *running_timer;  // 指向由本地 CPU 當前正處理的動態定時器的 timer_list 資料結構
	tvec_root_t tvl;   // 包含一個 vec 陣列,由 256 個 list_head 元素組成。包含了緊接到來的 255 個節拍內將要到期的所有動態定時器
	tvec_t tv2;  // 有一個 vec 陣列,包含 64 個 list_head 元素,包含了緊接到來的 $2^{14}-1$ 個節拍內將要到期的所有動態定時器
	tvec_t tv3;  // 包含了緊接到來的 $2^{20}-1$ 個節拍內將要到期的所有動態定時器
	tvec_t tv4;  // 包含了緊接到來的 $2^{26}-1$ 個節拍內將要到期的所有動態定時器
	tvec_t tv5;  // 包含了一個大 expires 欄位值的動態定時器連結串列
}tvsec_base_t;

動態定時器處理

對軟定時器的處理是一種耗時活動,不應由時鐘中斷處理程式執行,而是由可延遲函式— TIMER_SOFTIRQ 軟中斷執行。

run_timer_softirq() 是與 TIMER_SOFTIRQ 軟中斷請求相關的可延遲函式,實際執行如下操作:

  1. 將於本地 CPU 相關的 tvec_base_t 資料結構的地址存放到 base 本地變數中。
  2. 獲得 base->lock 自旋鎖並禁止本地中斷。
  3. while(base->timer_jiffies <= jiffies):
    a. index = base->timer_jiffies & 255; // 該索引儲存著下一次將要處理的定時器
    b. if(index == 0),說明 base_tvl 中的所有連結串列都被檢查過了,所以為空,於是呼叫 cascade() 過濾動態定時器。
    c. base->timer_jiffies += 1;
    d. 對於 base->tv1.vec[index] 連結串列上的每一個定時器,執行它所對應的定時器函式。連結串列上的每個 timer_list 元素 t 執行一下步驟:
    (1)將 t 從 base->tv1 的連結串列上刪除。
    (2)多處理器系統中,base->running_timer = &t;
    (3)t.base = NULL;
    (4)釋放 base->lock 自旋鎖,允許本地中斷。
    (5)傳遞 t.data 作為引數,執行定時器函式 t.function。
    (6)獲得 base->lock 自旋鎖,並禁止本地中斷。
    (7)如果連結串列中還有其他定時器,則繼續處理。
    e. 連結串列上的所有定時器已經被處理。繼續執行 while 迴圈。
  4. 所有到期的定時器已經被處理,多處理器系統中,base->running_timer = NULL;
  5. 釋放 base->lock 自旋鎖並允許本地中斷。

3.b. 中的 casecade():

if(!index && 
   !casecade(base, &base->tv2, (base->timer_jiffies>>8) & 63)) &&   //  將 base->tv2 中連結串列上所有動態定時器移到 base->tv1 的適當連結串列上
   !casecade(base, &base->tv3, (base->timer_jiffies>>14) & 63)) && // 如果 base->tv2 中的連結串列不為空,返回一個正值;否則,casecade() 再次被呼叫
   !casecade(base, &base->tv4, (base->timer_jiffies>>20) & 63)) )
	 casecade(base, &base->tv5, (base->timer_jiffies>>26) & 63);

進入最外層迴圈前,禁止中斷並獲取 base->lock 自旋鎖,呼叫每個動態定時器函式前,啟用中斷並釋放自旋鎖,直到函式執行結束,保證了動態定時器的資料不被交錯執行的核心路徑破壞。

以上覆雜的演算法保證了極好的效能。

動態定時器應用之一:nanosleep() 系統呼叫

nanosleep() 呼叫服務例程 sys_nanosleep(),它將 timespec 指標作為引數,將呼叫程序掛起直到特定的時間間隔用完。

sys_nanosleep()
1.先呼叫 copy_from_user() 將包含在 timespec 結構中的值賦值到區域性變數 t 中。
2.current->state = TASK_INTERRUPTIBLE;
remaining = schedule_timeout(timespec_to_jiffies(&t)+1);
3. 如果 schedule_timeout() 返回的值表示程序延時到期(0),系統呼叫結束;否則,系統呼叫將自動重新啟動。

核心使用動態定時器實現程序的延時, schedule_timeout():

struct timer_list timer;
unsigned long expire = timeout + jiffies;
init_timer(&timer);
timer.expires = expire;
timer.data = (unsigned long) current;
timer.function = process_timeout;
add_timer(&timer);
schedule();  // 程序掛起直到定時器到時
del_singleshot_timer_sync(&timer);  // 程序恢復執行,刪除該動態定時器
timeout = expire - jiffies;
return (timeout < 0 ? 0 : timeout);  // 0 表示延時到期,timeout 表示程序被其他原因喚醒,到期時還剩餘的節拍數

延時到期時,核心執行下列函式:

void process_timeout(unsigned long __data)  // 將程序描述符指標作為引數
{
	wake_up_process((task_t *)__data);
}

延遲函式

當核心需要等待一個較短時間間隔,無需使用軟定時器。這時,可用 udelay() 和 ndelay() 函式:前者引數為微秒級的時間間隔,後者的引數為納秒級。

void udelay(unsigned long usecs)
{
	unsigned long loops;
	loops = (usecs * HZ * current_cpu_data.loops_per_jiffy) / 1000000;
	cur_timer->delay(loops);
}

void ndelay(unsigned long usecs)
{
	unsigned long loops;
	loops = (nsecs * HZ * current_cpu_data.loops_per_jiffy) / 1000000000;
	cur_timer->delay(loops);
}

每一次“loop”精確的持續時間取決於 cur_timer 涉及的定時器物件

  • 如果 cur_timer 指向 timer_hpet、timer_pmtmr 和 timer_tsc 物件,一次“loop”對應一個 CPU 迴圈。
  • 如果 cur_timer 指向 timer_none 或 timer_pit 物件,一次“loop”對應於一條緊湊指令迴圈在一次單獨的迴圈中花費的時間。

在初始化階段:

  • select_timer() 設定號 cur_timer
  • 核心通過執行 calibrate_delay() 決定一個節拍裡有多少次“loop”,存於 current_cpu_data.loops_per_jiffy 變數中
  • udelay() 和 ndelay() 根據它將微妙和納秒轉換成“loops”。

與定時測量相關的系統呼叫

time() 和 gettimeofday() 系統呼叫

time() 返回從 1970 年 1月 1日午夜(UTC)開始走過的秒數。
gettimeofday() 返回從 UTC 開始所走過的秒數及在前 1 秒內走過的微妙數,存放於 timeval 中。

gettimeofday() 呼叫 sys_gettimeofday(),該函式呼叫 do_gettimeofday(),它執行下列動作:

  1. 為讀操作獲取 xtime_lock 順序鎖。
  2. usec = cur_timer->getoffset(); 確定自上一次時鐘中斷以來走過的微妙數。
    cur_timer 可能指向物件 timer_hpet、timer_pmtmr、timer_tsc、timer_pit,分別獲取相應計數器的當前值與上一次時鐘中斷處理程式時的值比較。
  3. 如果某定時器中斷丟失,usec += (jiffies - wall_jiffies) * 1000; usec 加上相應的延遲。
  4. usec += (xtime.tv_nsec / 1000); 為 usec 加上前 1 秒內走過的微妙數。
  5. tv->tv_sec = xtime->tv_sec;
    tv->tv_usec = usec;
  6. 在 xtime_lock 順序鎖上呼叫 read_seqretry(),如果另一條核心控制路徑同時為寫操作獲得了 xtime_lock,跳回步驟 1。
  7. while(tv->tv_usec >= 1000000){
    {
    tv->tv_usec -= 1000000;
    tv->tv_sec++;
    }

adjtimex() 系統呼叫

通常把系統配置成能在常規基準上執行時間同步協議,如網路定時協議(NTP),在每個節拍逐漸調整時間。這依賴於 adjtimex()。

adjtimex() 接收指向 timex 結構的指標作為引數,用 timex 自動中的值更新核心引數,並返回具有當前核心值的同一結構。
update_wall_time_one_tick() 使用這以核心值對每個節拍中加到 xtime.tv_usec 的微秒進行微調。

setitimer() 和 alarm() 系統呼叫

間隔定時器可能引起 Unix 訊號被週期性地傳送到程序,也可能在指定的延時後僅傳送一個訊號。

setitimer() 可啟用間隔定時器,第一個引數指定應當採取下面哪一個策略:

  • ITIMER_REAL,真正過去的時間,程序接收 SIGALRM 訊號。
  • ITIMER_VIRTUAL,程序在使用者態下花費的時間,程序接收 SIGVTALRM 訊號。
  • ITIMER_PROF,程序既在使用者態下又在核心態下所花費的時間,程序接收 SIGPROF 訊號。

間隔定時器既能一次執行,也能週期迴圈。

setitimer() 的第二個引數指向一個 itimerval 型別的結構,它指定了定時器初始的持續時間以及定時器被重新啟用後使用的持續時間。

setitimer() 的第三個引數是一個指標,可選,指向一個 itimerval 型別的結構,系統呼叫將先前定時器的引數填充到該結構中。

為分別實現前述每種策略的定時器,程序描述符包含 3 對欄位:

  • it_real_incr、it_real_value
  • it_virt_incr、it_virt_value
  • it_prof_incr、it_prof_value

第一個欄位存放兩個訊號之間以節拍為單位的間隔,第二個欄位存放定時器當前值。

ITIMER_REAL 間隔定時器利用動態定時器實現,因為及即使程序不執行,核心也能向其傳送訊號。每個程序描述符包含一個叫 real_timer 的動態定時器物件。setitimer():

  • 初始化 real_timer 欄位
  • 呼叫 add_timer() 將動態定時器插入到合適的連結串列中
  • 定時器到期時,it_real_fn() 函式向程序傳送一個 SIGALRM 訊號
  • 如果 it_real_incr 不為空,再次設定 expires 欄位,並重新啟用定時器

ITIMER_VIRTUAL、ITIMER_PROF 間隔定時器不需要動態定時器,因只有程序執行時才會被更新。

  • account_it_virt()、account_it_prof() 被 update_process_times() 呼叫
  • update_process_times() 在單處理器系統上被 PIT 的時鐘中斷處理程式呼叫,在多處理器上被本地時鐘中斷處理程式呼叫
    因此,每個節拍中,這兩個間隔定時器都會被更新一次,如果到期,就給當前程序傳送一個合適的訊號。

alarm() 會在一個指定的時間間隔用完時向呼叫的程序傳送一個 SIGALRM 訊號,引數為 ITIMER_REAL 時類似於 setitimer()。

與 POSIX 定時器相關的系統呼叫

Linux 2.6 核心提高兩種型別的 POSIX 時鐘:

  • CLOCK_REALTIME,該虛擬時鐘表示系統的實時時鐘,本質上是 xtime 變數的值。clock_getres() 系統呼叫返回的分辨度為 999 848ns,1s 內更新 xtime 約 1000 次。
  • CLOCK_MONOTONIC,該虛擬時鐘表示由於與外部時間源的同步,每次回到初值的系統實時時鐘。實際上,該虛擬時鐘由 xtime 和 wal_to_monotonic 兩個變數的和表示。分辨度由 clock_getres() 返回,為 999 848ns。

Linux 核心使用動態定時器實現 POSIX 定時器,與 ITIMER_REAL 間隔定時器相似,但更靈活、可靠,區別如下:

  • 一個 POSIX 定時器到期時,核心可以傳送各種訊號給整個多執行緒應用程式,也可傳送給單個指定執行緒。
  • 對於 POSIX 定時器,程序可呼叫 timer_getoverrun() 呼叫自第一個訊號產生以來定時器到期的次數。