linux設備驅動時間、延遲及延緩操做

hz:上述間隔由hz的值設定,hz是一個與體系結構相關的常數linux

計數器:發生中斷一次,計數器加一,這個計數器的值(只有)在系統引導時被初始化爲0緩存

jiffies變量:unsigned long 型變量,要麼與jiffies_64相同,要麼取其低32位安全

 

使用jiffies計數器數據結構

包含在<linux/jiffies.h>中,可是一般只需使用<linux/sched.h>,前者會自動包含併發

jiffies與jiffies_64均應被看作只讀變量app

jiffies變量應被聲明爲volatile異步

使用舉例:函數

#include<linux/jiffies.h>性能

unsigned long j,stamp_1,stamp_half,stamp_n;atom

j=jiffies;  //read the current value

stamp_1=j+HZ;  //1second in the future

stamp_half=j+HZ/2;  //0.5second in the future

stamp_n=j+n*HZ/1000;  // n milliseconds

 

比較緩存值(例如上述的stamp_1)與當前值:

#include<linux/jiffies.h>

int time_after(unsigned long a,unsigned long b);

int time_before(unsigned long a,unsigned long b);

int time_after_eq(unsigned long a,unsigned long b);

int time _before_eq(unsigned long a,unsigned long b);

上述幾個宏會將計數器值轉換爲signed long,相減,而後比較結果。若是須要以安全的方式計算兩個jiffies實例之間的差,以下:

diff = (long) t2 - (long) t1;

而經過下面的方法,可將兩個jiffies的差轉換爲毫秒值:

msec = diff *1000/HZ;

 

用戶空間和內核空間的時間表述方法的轉換:

用戶空間方法:timeval,timespec

內核空間方法:jiffies

#include<linux/time.h>

struct timespec {
    time_t    tv_sec;        /* seconds */
    long    tv_nsec;    /* nanoseconds */
};

struct timeval {
    time_t        tv_sec;        /* seconds */
    SUSEconds_t    tv_usec;    /* microseconds */
};

unsigned long timespec_to_jiffies(struct timespec *value);

void jiffies_to_timespec(unsigned long jiffies,struct timespec *value);

unsigned long timeval_to_jiffies(struct timeval *value);

void jiffies_to _timeval(unsigned long jiffies,struct timeval *value);

 

讀取64爲jiffies:jiffies_64

#include<linux/jiffies.h>

u64 get_jiffies_64(void);

 

處理器特定的寄存器

若是須要精度很高的計時,jiffies已不可知足須要,這時就引入了一種技術就是CPU包含一個隨時鐘週期不斷遞增的計數寄存器。這是完成高分辨率計時任務的惟一可靠途徑。

1.無論該寄存器是否置0,咱們都強烈建議不要重置它。

2.TSC:這是一個64位寄存器,記錄CPU的時鐘週期數,從內核空間和用戶空間均可以讀取它。

 

<asm/msr.h>

如下宏是與體系結構相關的,上述頭文件是x86專用頭文件

rdtsc(low32,high32);

rdtscl(low32);

rdtscll(var64);

第一個宏原子性的把64位變量讀到兩個32位的變量中。

第二個讀取低32位,廢棄高32位。

第三個把64值讀到一個long long型變量中。

舉例:

下面代碼完成測量指令自身運行時間

unsigned long ini,end;

rdtscl(ini);

rdtscl(end);

printk("time lapse:%li\n",end-ini);

 

現提供一個與體系結構無關的函數,能夠替代rdtsc

<linux/timex.h>

cycles_t get_cycles(void);

在各類平臺上均可以使用這個函數,在沒有時鐘週期計數寄存器的平臺上它老是返回0。cycles_t類型是能裝入讀取值的合適的無符號類型。

 

 

獲取當前時間

jiffies用來測量時間間隔

牆鍾時間-->jiffies時間:

#include<linux/time.h>

unsigned long mktime(unsigned int year,unsigned int month,

                                        unsigned int day, unsigned int  hour,

                                        unsigned int  minute,unsigned int second);

 

爲了處理絕對時間, <linux/time.h> 導出了 do_gettimeofday 函數,它填充一個指向 struct timeval 的指針變量。絕對時間也可來自 xtime 變量,一個 struct timespec 值,爲了原子地訪問它,內核提供了函數 current_kernel_time。它們的精確度由硬件決定,原型是:

#include<linux/time.h>
void do_gettimeofday(structtimeval*tv);
struct timespec current_kernel_time(void);

/*獲得的數據都表示當前時間距UNIX時間基準1970-01-01 00:00:00的相對時間*/

以上兩個函數在ARM平臺都是經過 xtime 變量獲得數據的。

全局變量xtime:它是一個timeval結構類型的變量,用來表示當前時間距UNIX時間基準1970-01-01 00:00:00的相對秒數值。

結構timeval是Linux內核表示時間的一種格式(Linux內核對時間的表示有多種格式,每種格式都有不一樣的時間精度),其時間精度是微秒。該結構是內核表示時間時最經常使用的一種格式,它定義在頭文件include/linux/time.h中,以下所示:

struct timeval {

time_t tv_sec; /* seconds */

SUSEconds_t tv_usec; /* microseconds */

};

其中,成員tv_sec表示當前時間距UNIX時間基準的秒數值,而成員tv_usec則表示一秒以內的微秒值,且1000000>tv_usec>=0。

 

Linux內核經過timeval結構類型的全局變量xtime來維持當前時間,該變量定義在kernel/timer.c文件中,以下所示:

/* The current time */

volatile struct timeval xtime __attribute__ ((aligned (16)));

可是,全局變量xtime所維持的當前時間一般是供用戶來檢索和設 置的,而其餘內核模塊一般不多使用它(其餘內核模塊用得最多的是jiffies),所以對xtime的更新並非一項緊迫的任務,因此這一工做一般被延遲 到時鐘中斷的底半部(bottom half)中來進行。因爲bottom half的執行時間帶有不肯定性,所以爲了記住內核上一次更新xtime是何時,Linux內核定義了一個相似於jiffies的全局變量 wall_jiffies,來保存內核上一次更新xtime時的jiffies值。時鐘中斷的底半部分每一次更新xtime的時侯都會將 wall_jiffies更新爲當時的jiffies值。全局變量wall_jiffies定義在kernel/timer.c文件中:

/* jiffies at the most recent update of wall time */

unsigned long wall_jiffies;

 

延遲

長延遲

忙等待

若想延遲執行若干個時鐘嘀噠,精度要求不高。最容易的( 儘管不推薦 ) 實現是一個監視 jiffy 計數器的循環。這種忙等待實現的代碼以下:

while(time_before(jiffies, j1))
    cpu_relax();

對 cpu_relex 的調用將以體系相關的方式執行,在許多系統中它根本不作任何事,這個方法應當明確地避免。對於ARM體系來講:

#define cpu_relax()            barrier()

也就是說在ARM上運行忙等待至關於:

while(time_before(jiffies, j1)) ;

這種忙等待嚴重地下降了系統性能。若是未配置內核爲搶佔式, 這個循環在延時期間徹底鎖住了處理器,計算機直到時間 j1 到時會徹底死掉。若是運行一個可搶佔的內核時會改善一點,可是忙等待在可搶佔系統中仍然是浪費資源的。更糟的是, 當進入循環時若是中斷碰巧被禁止, jiffies 將不會被更新, 而且 while 條件永遠保持真,運行一個搶佔的內核也不會有幫助, 惟一的解決方法是重啓。

 

讓出處理器

忙等待加劇了系統負載,必須找出一個更好的技術:不須要CPU時釋放CPU 。 這可經過調用schedule函數實現(在 <linux/sched.h> 中聲明):

while(time_before(jiffies, j1)){
    schedule();
}

在計算機空閒時運行空閒任務(進程號 0, 因爲歷史緣由也稱爲swapper)可減輕處理器工做負載、下降溫度、增長壽命。

 

超時

實現延遲的最好方法應該是讓內核爲咱們完成相應的工做。

(1)若驅動使用一個等待隊列來等待某些其餘事件,並想確保它在一個特定時間段內運行,可以使用:

#include<linux/wait.h>
long wait_event_timeout(wait_queue_head_t q, condition,long timeout);
long wait_event_interruptible_timeout(wait_queue_head_t q, condition,long timeout);
/*這些函數在給定隊列上睡眠, 可是它們在超時(以 jiffies 表示)到後返回。若是超時,函數返回 0; 若是這個進程被其餘事件喚醒,則返回以 jiffies 表示的剩餘的延遲實現;返回值從不會是負值*/

(2)爲了實現進程在超時到期時被喚醒而又不等待特定事件(避免聲明和使用一個多餘的等待隊列頭),內核提供了 schedule_timeout 函數:

 

#include<linux/sched.h>
signed long schedule_timeout(signedlong timeout);

/*timeout 是要延時的 jiffies 數。除非這個函數在給定的 timeout 流失前返回,不然返回值是 0 。schedule_timeout 要求調用者首先設置當前的進程狀態。爲得到一個不可中斷的延遲, 可以使用 TASK_UNINTERRUPTIBLE 代替。若是你忘記改變當前進程的狀態, 調用 schedule_time 如同調用 shcedule,創建一個不用的定時器。一個典型調用以下:*/
set_current_state(TASK_INTERRUPTIBLE);
schedule_timeout (delay);

 

短延遲

當一個設備驅動須要處理硬件的延遲(latency潛伏期), 涉及到的延時一般最多幾個毫秒,在這個狀況下, 不該依靠時鐘嘀噠,而是內核函數 ndelay, udelay和 mdelay ,他們分別延後執行指定的納秒數, 微秒數或者毫秒數,定義在 <asm/delay.h>,原型以下:

#include<linux/delay.h>
void ndelay(unsignedlong nsecs);
void udelay(unsignedlong usecs);
void mdelay(unsignedlong msecs);

重要的是記住這 3 個延時函數是忙等待; 其餘任務在時間流失時不能運行。每一個體系都實現 udelay, 可是其餘的函數可能未定義; 若是它們沒有定義, <linux/delay.h> 提供一個缺省的基於 udelay 的版本。在全部的狀況中, 得到的延時至少是要求的值, 但可能更多。udelay 的實現使用一個軟件循環, 它基於在啓動時計算的處理器速度和使用整數變量 loos_per_jiffy肯定循環次數。

爲避免在循環計算中整數溢出, 傳遞給udelay 和 ndelay的值有一個上限,若是你的模塊沒法加載和顯示一個未解決的符號:__bad_udelay, 這意味着你調用 udleay時使用太大的參數。
做爲一個通用的規則:若試圖延時幾千納秒, 應使用 udelay 而不是 ndelay; 相似地, 毫秒規模的延時應當使用 mdelay 完成而不是一個更細粒度的函數。

有另外一個方法得到毫秒(和更長)延時而不用涉及到忙等待的方法是使用如下函數(在<linux/delay.h> 中聲明):

void msleep(unsignedint millisecs);
unsigned long msleep_interruptible(unsignedint millisecs);
void ssleep(unsignedint seconds)

若可以容忍比請求的更長的延時,應使用 schedule_timeout, msleep 或 ssleep。

 

內核定時器

當須要調度一個之後發生的動做, 而在到達該時間點時不阻塞當前進程, 則可以使用內核定時器。內核定時器用來調度一個函數在未來一個特定的時間(基於時鐘嘀噠)執行,從而可完成各種任務。
內核定時器是一個數據結構, 它告訴內核在一個用戶定義的時間點使用用戶定義的參數執行一個用戶定義的函數,函數位於 <linux/timer.h> 和 kernel/timer.c 。被調度運行的函數幾乎肯定不會在註冊它們的進程在運行時運行,而是異步運行。實際上, 內核定時器一般被做爲一個"軟件中斷"的結果而實現。當在進程上下文以外(即在中斷上下文)中運行程序時, 必須遵照下列規則:

(1)不容許訪問用戶空間;
(2)current 指針在原子態沒有意義;
(3)不能進行睡眠或者調度. 例如:調用 kmalloc(..., GFP_KERNEL) 是非法的,信號量也不能使用由於它們可能睡眠。


經過調用函數 in_interrupt()可以告知是否它在中斷上下文中運行,無需參數並若是處理器當前在中斷上下文運行就返回非零。
經過調用函數 in_atomic()可以告知調度是否被禁止,若調度被禁止返回非零; 調度被禁止包含硬件和軟件中斷上下文以及任何持有自旋鎖的時候。

在後一種狀況, current 多是有效的,可是訪問用戶空間是被禁止的,由於它能致使調度發生. 當使用 in_interrupt()時,都應考慮是否真正該使用的是 in_atomic 。他們都在 <asm/hardirq.h> 中聲明。

內核定時器的另外一個重要特性是任務能夠註冊它自己在後面時間從新運行,由於每一個 timer_list 結構都會在運行前從激活的定時器鏈表中去鏈接,所以可以當即鏈入其餘的鏈表。一個從新註冊它本身的定時器一直運行在同一個 CPU.

即使在一個單處理器系統,定時器是一個潛在的態源,這是異步運行直接結果。所以任何被定時器函數訪問的數據結構應當經過原子類型或自旋鎖被保護,避免併發訪問

定時器 API

內核提供給驅動許多函數來聲明、註冊以及刪除內核定時器:


#include <linux/timer.h>
struct timer_list {
    struct list_head entry;
    unsigned long expires;/*指望定時器運行的絕對 jiffies 值,不是一個 jiffies_64 值,由於定時器不被指望在未來好久到時*/
    void (*function)(unsigned long); /*指望調用的函數*/
    unsigned long data;/*傳遞給函數的參數,若須要在參數中傳遞多個數據項,能夠將它們捆綁成單個數據結構而且將它的指針強制轉換爲 unsiged long 的指針傳入。這種作法在全部支持的體系上都是安全的而且在內存管理中至關廣泛*/
    struct tvec_t_base_s *base;
#ifdef CONFIG_TIMER_STATS
    void *start_site;
    char start_comm[16];
    int start_pid;
#endif
};
/*這個結構必須在使用前初始化,以保證全部的成員被正確創建(包括那些對調用者不透明的初始化):*/
void init_timer(struct timer_list *timer);
struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
/*在初始化後和調用 add_timer 前,能夠改變 3 個公共成員:expires、function和data*/
void add_timer(struct timer_list * timer);
int del_timer(struct timer_list * timer);/*在到時前禁止一個已註冊的定時器*/
int del_timer_sync(struct timer_list *timer); /*如同 del_timer ,但還保證當它返回時, 定時器函數不在任何 CPU 上運行,以免在 SMP 系統上競態, 而且在 單處理器內核中和 del_timer 相同。這個函數應當在大部分狀況下優先考慮。 若是它被從非原子上下文調用, 這個函數可能睡眠,可是在其餘狀況下會忙等待。當持有鎖時要當心調用 del_timer_sync ,若是這個定時器函數試圖得到同一個鎖, 系統會死鎖。若是定時器函數從新註冊本身, 調用者必須首先確保這個從新註冊不會發生; 這一般經過設置一個" 關閉 "標誌來實現, 這個標誌被定時器函數檢查*/
int mod_timer(struct timer_list *timer, unsigned long expires); /*更新一個定時器的超時時間, 經常使用於超時定時器。也可在正常使用 add_timer時在不活動的定時器上調用mod_timer*/
int timer_pending(const struct timer_list * timer); /*經過調用timer_list結構中一個不可見的成員,返回定時器是否在被調度運行*/

內核定時器的實現《LDD3》介紹的比較籠統,之後看《ULK3》的時候再細細研究。

 一個內核定時器還遠未完善,由於它受到 jitter 、硬件中斷,還有其餘定時器和其餘異步任務的影響。雖然一個簡單數字 I/O關聯的定時器對簡單任務是足夠的,但不合適在工業環境中的生產系統,對於這樣的任務,你將最可能須要實時內核擴展(RT-Linux).

相關文章
相關標籤/搜索