前言:
Linux中如何對時間進行管理?時鐘節拍的概念及延時函數的用法很多同學都用不好,下面我給大家總結一下。
一,linux時鐘運作機制
1,linux時鐘運作機制
• 大部分PC機中有兩個時鐘源,分別是實時時鐘(RTC)和 操作系統(OS)時鐘
• 實時時鐘也叫CMOS時鐘,它靠電池供電,即使系統斷電,也可以維持日期和時間。
• RTC和OS時鐘之間的關系通常也被稱作操作系統的時鐘運作機制
• 不同的操作系統,其時鐘運作機制也不同
linux中的時鐘機制大致如下圖所示
linux中時鐘機制
由上圖可知:
RTC是硬件時鐘,它為整個計算機提供一個計時標準,是原始底層的時鐘數據,由紐扣電池供電,系統斷電后仍然在工作
OS時鐘產生于PC主板上的定時/計數芯片,由操作系統控制這個芯片的工作,OS時鐘的基本單位就是該芯片的計數周期,開機時操作系統取得RTC中的時間數據來初始化OS時鐘,所以它只是在開機有效,由操作系統控制,已被稱為軟時鐘或系統時鐘。操作系統通過OS時鐘提供給應用程序和時間有關的服務。
擴展:OS時鐘其本質是一個計數器,計數器從計數初值開始,每收到一次脈沖信號,計數器減1,當減至0時,就會輸出高電平或低電平,然后獲取重載值重新從初值開始計數,不斷循環,這樣就得到一個輸出脈沖,這個脈沖作用中斷控制器上,產生中斷信號,觸發時鐘中斷。
2,OS時鐘中斷
• OS時鐘是由可編程定時/計數器產生的輸出脈沖觸發中斷而產生的,而輸出脈沖的周期叫做一個“時鐘節拍”(Tick,又稱滴答),(中斷觸發時會進入中斷處理函數,使jiffies+1)
• 操作系統的“時間基準” 由設計者決定,Linux的時間基準是1970年1月1日凌晨0點
• OS時鐘記錄的時間就是系統時間。系統時間以“時鐘節拍”為單位
•時鐘中斷觸發的頻率,由內核HZ來確定,系統啟動時會按照定義的HZ值對硬件進行設置
比如對HZ的定義如下:
#define Hz 100
內核時間頻率:表示每秒鐘觸發100次時鐘中斷,即每10ms觸發一次,
每次中斷jiffies+1,,則每秒jiffies增加了100,
• Linux中用全局變量 jiffies表示系統自啟動以來的時鐘節拍數目(時鐘中斷觸發的次數)
因此系統運行的時間以s為單位計數, 就等于 jiffies/HZ
內核啟動時將該變量初始化為0,此后,每次時鐘中斷處理程序都會增加該變量的值,每秒鐘觸發中斷的次數為Hz,
3、實際時間
實際時間就是現實中鐘表上顯示的時間,其實內核中并不常用這個時間,主要是用戶空間的程序有時需要獲取當前時間,所以內核中也管理著這個時間。
實際時間的獲取是在開機后,內核初始化時從RTC讀取的。
內核讀取這個時間后就將其放入內核中的 xtime 變量中,并且在系統的運行中不斷更新這個值。
當前實際時間(墻上時間): xtime.tv_sec以秒為單位,存放著自1970年7月1日(UTC)以來經過的時間,1970年1月1日被稱為紀元。多數Unix系統的墻上時間都是基于該紀元而言的。xtime.tv_nsec記錄自上一秒開始經過的納秒數。
在<Time.h(incluce/linux)>中
extern struct timespec xtime;
#ifndef _STRUCT_TIMESPEC
#define _STRUCT_TIMESPEC
struct timespec { /*高精度*/
time_t tv_sec; /* seconds */
long tv_nsec; /* nanoseconds 納秒*/
};
#endif
從用戶空間取得墻上時間的主要接口是gettimeofday(),在內核中對應的系統調用為sys_gettimeofday():
雖然內核也實現了time()系統調用,但是gettimeofday()幾乎完全取代了它。C庫函數也提供了墻上時間相關的庫調用,比如ftime(),ctime()。
除了更新xtime時間外,內核不會想用戶空間程序那樣頻繁的使用xtime。但是,在文件系統的實現代碼中存放訪問時間戳(創建,存取,修改等)需要使用xtime。
4,時鐘中斷處理程序----操作系統的脈搏
每一次時鐘中斷的產生都觸發下列幾個主要的操作:
– 給jiffies變量加 1
– 更新時間和日期,既更新xtime墻上時間
– 確定當前進程在CPU 上已運行了多長時間,如果已經超過了分配給它的時間,則搶占它
– 更新資源使用統計數
– 檢查定時器時間間隔是否已到,如果是,則執行它注冊的函數(運行于底半部軟中斷中)
以上工作每秒要發生 Hz次,也就是說PC上的時鐘中斷處理程序執行的頻率為Hz
5、時間系統總結
1、節拍----->jiffies
又稱時鐘滴答,是一個全局變量,它的值在系統引導的時候初始化為0,在時鐘中斷初始化完成后,每次時鐘中斷發生,在時鐘中斷處理例程中都會將jiffies的值 +1。
jiffies_64:為了解決jiffies溢出問題,更重要的是通過jiffies_64可以知道自開機以來的時間間隔。
2、節拍率---->HZ
HZ表示時鐘中斷發生的頻率。可以在.config的配置文件中改寫。1/HZ是每個jiffies+1的時間間隔。
3、通過jiffies可以進行時間的比較和時間轉換
4、時間比較
32位 64位
time_after(a,b) time_after64(a,b)
time_before(a,b) time_before64(a,b)
time_after_eq(a,b) time_after_eq64(a,b)
time_before_eq(a,b) time_before_eq64
time_in_range(a,b,c) time_in_range(a,b,c)
5、時間轉換
a、jiffies和msecs以及usecs的轉換:
unsigned int jiffies_to_msecs(const unsigned long);
unsigned int jiffies_to_usecs(const unsigned long);
unsigned long msecs_to_jiffies(const unsigned int m);
unsigned long usecs_to_jiffies(const unsigned int u);
b、jiffies和timespec以及timeval的轉換
在用戶空間,應用程序更多的使用秒以及毫秒等時間形式,而在內核中多使用jiffes。
內核定義了struct timeval 和 struct timespec 兩種數據結構
struct timespec {
__kernel_time_t tv_sec;
long tv_nsec;
}
struct timeval {
__kernel_time_t tv_sec;
__kernel_suseconds_t tv_usec;
}
相互轉換函數:
unsigned long timespec_to_jiffies(const struct timespec *value);
void jiffies_to_timespec(const unsigned long jiffies, struct timespec *value);
unsigned long timeval_to_jiffies(const struct timeval *value);
void jiffies_to_timeval(const unsigned long jiffies, struct timeval *value);
6、要注意的是jiffies的精度問題。如果HZ = 1000,則jiffies增加1代表1ms。
如果要用到更高精度的始終,要用其他的硬件機制。
二、內核短延時
Linux內核中提供了下列3個函數以分別進行納秒、微秒和毫秒延遲:
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
上述延遲的實現原理本質上是忙等待,它根據CPU頻率進行一定次數的循環。如果沒有特殊的理由(比如在中斷上下文中獲取自旋鎖的情況),不推薦使用這些函數延遲較長的時間,浪費CPU。
注:ndelay 和 mdelay都是基于udelay,將udelay的次數除1000就是ndelay,因此ndelay的次數為1000的整數倍才準確。
有時候,人們在軟件中進行下面的延遲:
void delay(unsigned int time)
{
while(time--);
}
ndelay()、udelay()和mdelay()函數的實現方式原理與此類似。
內核在啟動時,會運行一個延遲循環校準(Delay Loop Calibration),計算出lpj(Loops Per Jiffy)即處理器在一個jiffy時間內運行一個內部的延遲循環的次數,內核啟動時會打印如下類似信息:
Calibrating delay loop... 530.84 BogoMIPS (lpj=1327104)
如果我們直接在bootloader傳遞給內核的bootargs中設置lpj=1327104,則可以省掉這個校準的過程節省約百毫秒級的開機時間。
睡著延時
毫秒時延(以及更大的秒時延)已經比較大了,在內核中,好不要直接使用mdelay()函數,這將耗費CPU資源,對于毫秒級以上的時延,內核提供了下述函數:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);
void ssleep(unsigned int seconds);
上述函數將使得調用它的進程睡眠參數指定的時間為millisecs,msleep()、ssleep()不能被打斷,而msleep_interruptible()則可以被打斷。
受系統Hz以及進程調度的影響,msleep()類似函數的精度是有限的。
三、內核長延時
在內核中,一個直觀的延時的方法是將所要延遲的時間設置的當前的jiffies加上要延遲的時間,這樣就可以簡單的通過比較當前的jiffies和設置的時間來判斷延時的時間時候到來。針對此方法,內核中提供了簡單的宏用于判斷延時是否完成。
time_after(a,b); /*如果時間a在b之后 (a>b),則返回真,否則返回0*/
time_before(a,b); /*如果時間a在b之前 (a<b),則返回真,否則返回0*/
長延時實現舉例:
/* 延遲 100 個 jiffies */
unsigned long delay = jiffies + 100;
while(time_before(jiffies, delay));
/* 再延遲 2s */
unsigned long delay = jiffies + 2*Hz;
while(time_before(jiffies, delay));
與time_before()對應的還有一個time_after(),它們在內核中定義為(實際上只是將傳入的未來時間jiffies和被調用時的jiffies進行一個簡單的比較):
#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)
為了防止在time_before()和time_after()的比較過程中編譯器對jiffies的優化,內核將其定義為
volatile變量,這將保證每次都會重新讀取這個變量。因此volatile更多的作用還是避免這種讀合并。
四、讓進程睡固定的時間
下面兩個函數可以將當前進程添加到等待隊列中,從而在等待隊列上睡眠,當超時發生時,進程將被喚醒:
sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);
interrupt_sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout);