系統中有不少與時間相關的程序(好比按期執行的任務,某一時間執行的任務,推遲一段時間執行的任務),所以,時間的管理對於linux來講很是重要。html
主要內容:linux
系統中管理的時間有2種:實際時間和定時器。shell
實際時間就是現實中鐘錶上顯示的時間,其實內核中並不經常使用這個時間,主要是用戶空間的程序有時須要獲取當前時間,數據結構
因此內核中也管理着這個時間。app
實際時間的獲取是在開機後,內核初始化時從RTC讀取的。函數
內核讀取這個時間後就將其放入內核中的 xtime 變量中,而且在系統的運行中不斷更新這個值。oop
注:RTC就是實時時鐘的縮寫,它是用來存放系統時間的設備。通常和BIOS同樣,由主板上的電池供電的,因此即便關機也可將時間保存。post
實際時間存放的變量 xtime 在文件 kernel/time/timekeeping.c中。性能
/* 按照16位對齊,其實就是2個long型的數據 */ struct timespec xtime __attribute__ ((aligned (16))); /* timespec結構體的定義以下, 參考 <linux/time.h> */ struct timespec { __kernel_time_t tv_sec; /* seconds */ long tv_nsec; /* nanoseconds */ }; /* _kernel_time_t 定義以下 */ typedef long __kernel_time_t;
系統讀寫 xtime 時用的就是順序鎖。測試
/* 寫入 xtime 參考 do_sometimeofday 方法 */ int do_settimeofday(struct timespec *tv) { /* 省略 。。。。 */ write_seqlock_irqsave(&xtime_lock, flags); /* 獲取寫鎖 */ /* 更新 xtime */ write_sequnlock_irqrestore(&xtime_lock, flags); /* 釋放寫鎖 */ /* 省略 。。。。 */ return 0; } /* 讀取 xtime 參考 do_gettimeofday 方法 */ void do_gettimeofday(struct timeval *tv) { struct timespec now; getnstimeofday(&now); /* 就是在這個方法中獲取讀鎖,並讀取 xtime */ tv->tv_sec = now.tv_sec; tv->tv_usec = now.tv_nsec/1000; } void getnstimeofday(struct timespec *ts) { /* 省略 。。。。 */ /* 順序鎖中讀鎖來循環獲取 xtime,直至讀取過程當中 xtime 沒有被改變過 */ do { seq = read_seqbegin(&xtime_lock); *ts = xtime; nsecs = timekeeping_get_ns(); /* If arch requires, add in gettimeoffset() */ nsecs += arch_gettimeoffset(); } while (read_seqretry(&xtime_lock, seq)); /* 省略 。。。。 */ }
上述場景中,寫鎖必需要優先於讀鎖(由於 xtime 必須及時更新),並且寫鎖的使用者不多(通常只有系統按期更新xtime的線程須要持有這個鎖)。
這正是 順序鎖的應用場景。
定時器是內核中主要使用的時間管理方法,經過定時器,能夠有效的調度程序的執行。
動態定時器是內核中使用比較多的定時器,下面重點討論的也是動態定時器。
內核中的定時器有2種,靜態定時器和動態定時器。
靜態定時器通常執行了一些週期性的固定工做:
動態定時器顧名思義,是在須要時(通常是推遲程序執行)動態建立的定時器,使用後銷燬(通常都是隻用一次)。
通常咱們在內核代碼中使用的定時器基本都是動態定時器,下面重點討論動態定時器相關的概念和使用方法。
定時器的使用中,下面3個概念很是重要:
節拍率(HZ)是時鐘中斷的頻率,表示的一秒內時鐘中斷的次數。
好比 HZ=100 表示一秒內觸發100次時鐘中斷程序。
HZ的值通常與體系結構有關,x86 體系結構通常定義爲 100,參考文件 include/asm-generic/param.h
HZ值的大小的設置過程其實就是平衡 精度和性能 的過程,並非HZ值越高越好。
HZ值 |
優點 |
劣勢 |
高HZ | 時鐘中斷程序運行的更加頻繁,依賴時間執行的程序更加精確, 對資源消耗和系統運行時間的統計更加精確。 |
時鐘中斷執行的頻繁,增長系統負擔 時鐘中斷佔用的CPU時間過多 |
此外,有一點須要注意,內核中使用的HZ可能和用戶空間中定義的HZ值不一致,爲了不用戶空間取得錯誤的時間,
內核中也定義了 USER_HZ,即用戶空間使用的HZ值。
通常來講,USER_HZ 和 HZ 都是相差整數倍,內核中經過函數 jiffies_to_clock_t 來將內核來將內核中的 jiffies轉爲 用戶空間 jiffies
/* 參見文件: kernel/time.c * //* * Convert jiffies/jiffies_64 to clock_t and back. */ clock_t jiffies_to_clock_t(unsigned long x) { #if (TICK_NSEC % (NSEC_PER_SEC / USER_HZ)) == 0 # if HZ < USER_HZ return x * (USER_HZ / HZ); # else return x / (HZ / USER_HZ); # endif #else return div_u64((u64)x * TICK_NSEC, NSEC_PER_SEC / USER_HZ); #endif } EXPORT_SYMBOL(jiffies_to_clock_t);
jiffies用來記錄自系統啓動以來產生的總節拍數。好比系統啓動了 N 秒,那麼 jiffies就爲 N×HZ
jiffies的相關定義參考頭文件 <linux/jiffies.h> include/linux/jiffies.h
/* 64bit和32bit的jiffies定義以下 */ extern u64 __jiffy_data jiffies_64; extern unsigned long volatile __jiffy_data jiffies;
使用定時器時通常都是以jiffies爲單位來延遲程序執行的,好比延遲5個節拍後執行的話,執行時間就是 jiffies+5
32位的jiffies的最大值爲 2^32-1,在使用時有可能會出現迴繞的問題。
好比下面的代碼:
unsigned long timeout = jiffies + HZ/2; /* 設置超時時間爲 0.5秒 */ while (timeout < jiffies) { /* 尚未超時,繼續執行任務 */ } /* 執行超時後的任務 */
正常狀況下,上面的代碼沒有問題。當jiffies接近最大值的時候,就會出現迴繞問題。
因爲是unsinged long類型,因此jiffies達到最大值後會變成0而後再逐漸變大,以下圖所示:
因此在上述的循環代碼中,會出現以下狀況:
爲了迴避回擾的問題,可使用<linux/jiffies.h>頭文件中提供的 time_after,time_before等宏
#define time_after(a,b) \ (typecheck(unsigned long, a) && \ typecheck(unsigned long, b) && \ ((long)(b) - (long)(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) - (long)(b) >= 0)) #define time_before_eq(a,b) time_after_eq(b,a)
上述代碼的原理其實就是將 unsigned long 類型轉換爲 long 類型來避免回擾帶來的錯誤,
long 類型超過最大值時變化趨勢以下:
long 型的數據的迴繞會出如今 2^31-1 變爲 -2^32 的時候,以下圖所示:
注1:result的值爲正數時,必須是在result的值 小於 2^31-1 的狀況下,大於 2^31-1 會發生迴繞。
上圖中 X + Y 表示timeout 和 J2之間通過的節拍數。
result 小於 2^31-1 ,也就是 timeout - J2 < 2^31 – 1
timeout 和 -J2 表示的節拍數如上圖所示。(由於J2是負數,全部-J2表示上圖所示範圍的值)
由於 timeout + X + Y - J2 = 2^31-1 + 2^32
因此 timeout - J2 < 2^31 - 1 時, X + Y > 2^32
也就是說,當timeout和J2之間通過至少 2^32 個節拍後,result纔可能變爲正數。
timeout和J2之間相差這麼多節拍是不可能的(不信能夠用HZ將這些節拍換算成秒就知道了。。。)
利用time_after宏就能夠巧妙的避免迴繞帶來的超時判斷問題,將以前的代碼改爲以下代碼便可:
unsigned long timeout = jiffies + HZ/2; /* 設置超時時間爲 0.5秒 */ while (time_after(jiffies, timeout)) { /* 尚未超時,繼續執行任務 */ } /* 執行超時後的任務 */
時鐘中斷處理程序做爲系統定時器而註冊到內核中,體系結構的不一樣,可能時鐘中斷處理程序中處理的內容不一樣。
可是如下這些基本的工做都會執行:
tick_periodic函數位於: kernel/time/tick-common.c 中
static void tick_periodic(int cpu) { if (tick_do_timer_cpu == cpu) { write_seqlock(&xtime_lock); /* Keep track of the next tick event */ tick_next_period = ktime_add(tick_next_period, tick_period); do_timer(1); write_sequnlock(&xtime_lock); } update_process_times(user_mode(get_irq_regs())); profile_tick(CPU_PROFILING); }
其中最重要的是 do_timer 和 update_process_times 函數。
我瞭解的步驟進行了簡單的註釋。
void do_timer(unsigned long ticks) { /* jiffies_64 增長指定ticks */ jiffies_64 += ticks; /* 更新實際時間 */ update_wall_time(); /* 更新系統的平均負載值 */ calc_global_load(); } void update_process_times(int user_tick) { struct task_struct *p = current; int cpu = smp_processor_id(); /* 更新當前進程佔用CPU的時間 */ account_process_tick(p, user_tick); /* 同時觸發軟中斷,處理全部到期的定時器 */ run_local_timers(); rcu_check_callbacks(cpu, user_tick); printk_tick(); /* 減小當前進程的時間片數 */ scheduler_tick(); run_posix_cpu_timers(p); }
這裏討論的定時器執行流程是動態定時器的執行流程。
定時器在內核中用一個鏈表來保存的,鏈表的每一個節點都是一個定時器。
參見頭文件 <linux/timer.h>
struct timer_list { struct list_head entry; unsigned long expires; void (*function)(unsigned long); unsigned long data; struct tvec_base *base; #ifdef CONFIG_TIMER_STATS void *start_site; char start_comm[16]; int start_pid; #endif #ifdef CONFIG_LOCKDEP struct lockdep_map lockdep_map; #endif };
經過加入條件編譯的參數,能夠追加一些調試信息。
一個動態定時器的生命週期中,通常會通過下面的幾個步驟:
1. 初始化定時器:
struct timer_list my_timer; /* 定義定時器 */ init_timer(&my_timer); /* 初始化定時器 */
2. 填充定時器:
my_timer.expires = jiffies + delay; /* 定義超時的節拍數 */ my_timer.data = 0; /* 給定時器函數傳入的參數 */ my_timer.function = my_function; /* 定時器超時時,執行的自定義函數 */ /* 從定時器結構體中,咱們能夠看出這個函數的原型應該以下所示: */ void my_function(unsigned long data);
3. 激活定時器和修改定時器:
激活定時器以後纔會被觸發,不然定時器不會執行。
修改定時器主要是修改定時器的延遲時間,修改定時器後,無論原先定時器有沒有被激活,都會處於激活狀態。
填充定時器結構以後,能夠只激活定時器,也能夠只修改定時器,也能夠激活定時器後再修改定時器。
因此填充定時器結構和觸發定時器之間的步驟,也就是虛線框中的步驟是不肯定的。
add_timer(&my_timer); /* 激活定時器 */ mod_timer(&my_timer, jiffies + new_delay); /* 修改定時器,設置新的延遲時間 */
4. 觸發定時器:
每次時鐘中斷處理程序會檢查已經激活的定時器是否超時,若是超時就執行定時器結構中的自定義函數。
5. 刪除定時器:
激活和未被激活的定時器均可以被刪除,已經超時的定時器會自動刪除,不用特地去刪除。
/* * 刪除激活的定時器時,此函數返回1 * 刪除未激活的定時器時,此函數返回0 */ del_timer(&my_timer);
在多核處理器上用 del_timer 函數刪除定時器時,可能在刪除時正好另外一個CPU核上的時鐘中斷處理程序正在執行這個定時器,因而就造成了競爭條件。
爲了不競爭條件,建議使用 del_timer_sync 函數來刪除定時器。
del_timer_sync 函數會等待其餘處理器上的定時器處理程序所有結束後,才刪除指定的定時器。
/* * 和del_timer 不一樣,del_timer_sync 不能在中斷上下文中執行 */ del_timer_sync(&my_timer);
內核中有個利用定時器實現延遲的函數 schedule_timeout
這個函數會將當前的任務睡眠到指定時間後喚醒,因此等待時不會佔用CPU時間。
/* 將任務設置爲可中斷睡眠狀態 */ set_current_state(TASK_INTERRUPTIBLE); /* 小睡一下子,「s「秒後喚醒 */ schedule_timeout(s*HZ);
查看 schedule_timeout 函數的實現方法,能夠看出是如何使用定時器的。
signed long __sched schedule_timeout(signed long timeout) { /* 定義一個定時器 */ struct timer_list timer; unsigned long expire; switch (timeout) { case MAX_SCHEDULE_TIMEOUT: /* * These two special cases are useful to be comfortable * in the caller. Nothing more. We could take * MAX_SCHEDULE_TIMEOUT from one of the negative value * but I' d like to return a valid offset (>=0) to allow * the caller to do everything it want with the retval. */ schedule(); goto out; default: /* * Another bit of PARANOID. Note that the retval will be * 0 since no piece of kernel is supposed to do a check * for a negative retval of schedule_timeout() (since it * should never happens anyway). You just have the printk() * that will tell you if something is gone wrong and where. */ if (timeout < 0) { printk(KERN_ERR "schedule_timeout: wrong timeout " "value %lx\n", timeout); dump_stack(); current->state = TASK_RUNNING; goto out; } } /* 設置超時時間 */ expire = timeout + jiffies; /* 初始化定時器,超時處理函數是 process_timeout,後面再補充說明一下這個函數 */ setup_timer_on_stack(&timer, process_timeout, (unsigned long)current); /* 修改定時器,同時會激活定時器 */ __mod_timer(&timer, expire, false, TIMER_NOT_PINNED); /* 將本任務睡眠,調度其餘任務 */ schedule(); /* 刪除定時器,其實就是 del_timer_sync 的宏 del_singleshot_timer_sync(&timer); /* Remove the timer from the object tracker */ destroy_timer_on_stack(&timer); timeout = expire - jiffies; out: return timeout < 0 ? 0 : timeout; } EXPORT_SYMBOL(schedule_timeout); /* * 超時處理函數 process_timeout 裏面只有一步操做,喚醒當前任務。 * process_timeout 的參數其實就是 當前任務的地址 */ static void process_timeout(unsigned long __data) { wake_up_process((struct task_struct *)__data); }
schedule_timeout 通常用於延遲時間較長的程序。
這裏的延遲時間較長是對於計算機而言的,其實也就是延遲大於 1 個節拍(jiffies)。
對於某些極其短暫的延遲,好比只有1ms,甚至1us,1ns的延遲,必須使用特殊的延遲方法。
1s = 1000ms = 1000000us = 1000000000ns (1秒=1000毫秒=1000000微秒=1000000000納秒)
假設 HZ=100,那麼 1個節拍的時間間隔是 1/100秒,大概10ms左右。
因此對於那些極其短暫的延遲,schedule_timeout 函數是沒法使用的。
好在內核對於這些短暫,精確的延遲要求也提供了相應的宏。
/* 具體實現參見 include/linux/delay.h * 以及 arch/x86/include/asm/delay.h */ #define mdelay(n) ... #define udelay(n) ... #define ndelay(n) ...
經過這些宏,能夠簡單的實現延遲,好比延遲 5ns,只需 ndelay(5); 便可。
這些短延遲的實現原理並不複雜,
首先,內核在啓動時就計算出了當前處理器1秒能執行多少次循環,即 loops_per_jiffy
(loops_per_jiffy 的計算方法參見 init/main.c 文件中的 calibrate_delay 方法)。
而後算出延遲 5ns 須要循環多少次,執行那麼屢次空循環便可達到延遲的效果。
loops_per_jiffy 的值能夠在啓動信息中看到:
[root@vbox ~]# dmesg | grep delay Calibrating delay loop (skipped), value calculated using timer frequency.. 6387.58 BogoMIPS (lpj=3193792)
個人虛擬機中看到 (lpj=3193792)
下面的例子測試了短延遲,自定義定時器以及 schedule_timeout 的使用:
#include <linux/sched.h>
#include <linux/timer.h> #include <linux/jiffies.h> #include <asm/param.h> #include <linux/delay.h> #include "kn_common.h" MODULE_LICENSE("Dual BSD/GPL"); static void test_short_delay(void); static void test_delay(void); static void test_schedule_timeout(void); static void my_delay_function(unsigned long); static int testdelay_init(void) { printk(KERN_ALERT "HZ in current system: %dHz\n", HZ); /* test short delay */ test_short_delay(); /* test delay */ test_delay(); /* test schedule timeout */ test_schedule_timeout(); return 0; } static void testdelay_exit(void) { printk(KERN_ALERT "*************************\n"); print_current_time(0); printk(KERN_ALERT "testdelay is exited!\n"); printk(KERN_ALERT "*************************\n"); } static void test_short_delay() { printk(KERN_ALERT "jiffies [b e f o r e] short delay: %lu", jiffies); ndelay(5); printk(KERN_ALERT "jiffies [a f t e r] short delay: %lu", jiffies); } static void test_delay() { /* 初始化定時器 */ struct timer_list my_timer; init_timer(&my_timer); /* 填充定時器 */ my_timer.expires = jiffies + 1*HZ; /* 2秒後超時函數執行 */ my_timer.data = jiffies; my_timer.function = my_delay_function; /* 激活定時器 */ add_timer(&my_timer); } static void my_delay_function(unsigned long data) { printk(KERN_ALERT "This is my delay function start......\n"); printk(KERN_ALERT "The jiffies when init timer: %lu\n", data); printk(KERN_ALERT "The jiffies when timer is running: %lu\n", jiffies); printk(KERN_ALERT "This is my delay function end........\n"); } static void test_schedule_timeout() { printk(KERN_ALERT "This sample start at : %lu", jiffies); /* 睡眠2秒 */ set_current_state(TASK_INTERRUPTIBLE); printk(KERN_ALERT "sleep 2s ....\n"); schedule_timeout(2*HZ); printk(KERN_ALERT "This sample end at : %lu", jiffies); } module_init(testdelay_init); module_exit(testdelay_exit);
其中用到的 kn_common.h 和 kn_common.c 參見以前的博客 《Linux內核設計與實現》讀書筆記(六)- 內核數據結構
Makefile以下:
# must complile on customize kernel
obj-m += mydelay.o mydelay-objs := testdelay.o kn_common.o #generate the path CURRENT_PATH:=$(shell pwd) #the current kernel version number LINUX_KERNEL:=$(shell uname -r) #the absolute path LINUX_KERNEL_PATH:=/usr/src/kernels/$(LINUX_KERNEL) #complie object all: make -C $(LINUX_KERNEL_PATH) M=$(CURRENT_PATH) modules rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c .tmp_versions *.unsigned #clean clean: rm -rf modules.order Module.symvers .*.cmd *.o *.mod.c *.ko .tmp_versions *.unsigned
執行測試命令及查看結果的方法以下:(個人測試系統是 CentOS 6.3 x64)
[root@vbox chap11]# make [root@vbox chap11]# insmod mydelay.ko [root@vbox chap11]# rmmod mydelay.ko [root@vbox chap11]# dmesg | tail -14 HZ in current system: 1000Hz jiffies [b e f o r e] short delay: 4296079617 jiffies [a f t e r] short delay: 4296079617 This sample start at : 4296079619 sleep 2s .... This is my delay function start...... The jiffies when init timer: 4296079619 The jiffies when timer is running: 4296080621 This is my delay function end........ This sample end at : 4296081622 ************************* 2013-5-9 23:7:20 testdelay is exited! *************************
結果說明:
1. 短延遲只延遲了 5ns,因此執行先後的jiffies是同樣的。
jiffies [b e f o r e] short delay: 4296079617 jiffies [a f t e r] short delay: 4296079617
2. 自定義定時器延遲了1秒後執行自定義函數,因爲個人系統 HZ=1000,因此jiffies應該相差1000
The jiffies when init timer: 4296079619 The jiffies when timer is running: 4296080621
實際上jiffies相差了 1002,多了2個節拍
3. schedule_timeout 延遲了2秒,jiffies應該相差 2000
This sample start at : 4296079619 This sample end at : 4296081622
實際上jiffies相差了 2003,多了3個節拍
以上結果也說明了定時器的延遲並非那麼精確,差了2,3個節拍其實就是偏差2,3毫秒(由於HZ=1000)
若是HZ=100的話,一個節拍是10毫秒,那麼定時器的偏差可能就發現不了了(偏差只有2,3毫秒,沒有超多1個節拍)。