tasklet && workqueue && kernel timer


一、kernel timer

1、適用環境
內核中許多部分的工作都高度依賴於時間信息。Linux內核利用硬件提供的不同的定時器以支持忙等待或睡眠等待等時間相關的服務。忙等待時,CPU會不斷運轉。但是睡眠等待時,進程將放棄CPU。因此,只有在後者不可行的情況下,才考慮使用前者。當然內核也提供了某些便利,如果我們需要在將來的某個時間點調度執行某個動作,同時在該時間點到達之前不會阻塞當前進程,則可以使用內核定時器。

2. HZ和jiffies
內核通過定時器中斷來跟蹤時間流。系統定時器能以可編程的頻率中斷處理器。此頻率即爲每秒的定時器節拍數,也可以說是系統時鐘中斷髮生的頻率,對應着內核變量HZ。選擇合適的HZ值需要權衡。HZ值大,定時器間隔時間就小,因此進程調度的準確性會更高。但是,HZ值越大也會導致開銷和電源消耗更多,因爲更多的處理器週期將被耗費在定時器中斷上下文中。
HZ的值取決於體系架構,而且目前內核支持無節拍的內核(CONFIG_NO_HZ),它會根據系統的負載動態觸發定時器中斷。
每次當時鍾中斷髮生時,內核內部計數器的值就增加一。這個計數器的值在系統引導時被初始化爲0,因此,它的值就是自上次操作系統引導以來的時鐘滴答數。這個計數器是一個64位的變量(即使是在32位架構上也是64位),稱爲"jiffies_64",但是驅動程序開發者通常訪問的是jiffies變量,它是unsigned long型的變量,要麼和jiffies_64相同,要麼僅僅是jiffies_64的低32位。通常首選使用jiffies,因爲對它的訪問很快,從而對64位jiffies_64值的訪問並不需要在所有架構上都是原子的。
 因此jiffies變量記錄了系統啓動以來,系統定時器已經觸發的次數。內核每秒鐘將jiffies變量增加HZ次。因此,對於HZ值爲100的系統,1個jiffy等於10ms,而對於HZ爲1000的系統,1個jiffy僅爲1ms。

3.比較緩存值與當前值所用到的宏

#include <linux/jiffies.h>

int time_after(unsigned long a,unsigned long b) 如果a(jiffies的某個快照)所代表的時間比b靠後,則返回真
int time_before(unsigned long a,unsigned long b) 如果a 比 b靠前,則返回真
int time_after_eq(unsigned long a,unsigned long b) 如果 a 比 b靠後或者相等,返回真
int time_before_eq(unsigned long a,unsigned long b) 如果 a 比 b靠前或者相等,返回真

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

(1) 有關時間的結構體
struct timeval
{
int tv_sec;
int tv_usec;
};
其中tv_sec是由凌晨開始算起的秒數,tv_usec則是微秒(10E-6 second)。

struct timezone
{
int tv_minuteswest;
int tv_dsttime;
};
tv_minuteswest是格林威治時間往西方的時差,tv_dsttime則是時間的修正方式。

struct timespec
{
long int tv_sec;
long int tv_nsec;
};
其中tv_sec是由凌晨開始算起的秒數,tv_nsec是nano second(10E-9 second)。

struct tm
{
int tm_sec;
int tm_min;
int tm_hour;
int tm_mday;
int tm_mon;
int tm_year;
int tm_wday;
int tm_yday;
int tm_isdst;
};
tm_sec表「秒」數,在[0,61]之間,多出來的兩秒是用來處理跳秒問題用的。
tm_min表「分」數,在[0,59]之間。
tm_hour表「時」數,在[0,23]之間。
tm_mday表「本月第幾日」,在[1,31]之間。
tm_mon表「本年第幾月」,在[0,11]之間。
tm_year要加1900表示那一年。
tm_wday表「本第幾日」,在[0,6]之間。
tm_yday表「本年第幾日」,在[0,365]之間,閏年有366日。
tm_isdst表是否爲「日光節約時間」。

struct itimerval
{
struct timeval it_interval;
struct timeval it_value;
};
it_interval成員表示間隔計數器的初始值,而it_value成員表示間隔計數器的當前值。

(2) jiffies與上述結構之間的轉換
#include <linux/time.h>
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)

(3)對 jiffies_64 進行讀取的特殊輔助函數
#include <linux\jiffies>
u64 get_jiffies_64(void)
5.短延時
當設備驅動程序需要處理硬件的延遲時,這種延遲通常最多涉及到幾十個毫秒,這種情況下,依賴於時鐘滴答是不正確的。以下幾個函數可很好阿文拿出短延遲任務:
#include <linux/delay.h>
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
這三個延遲函數均是忙等待函數,因爲在延遲過程中無法運行其他函數。

實現毫秒級(或者更長)延遲還有另外一種方法,這種方法不涉及忙等待。
#include <linux/delay.h>
void msleep(ungigned int millisecs)
unsgined long msleep_interruptible(unsigned int millisecs)
void ssleep(unsigned int seconds)
前兩個函數將調用進程休眠以給定的millisecs.
對msleep的調用是不可中斷的,我們可以確信進程至少休眠給定的毫秒數。
對msleep_interruptible的調用是可以中斷的,如果驅動程序正在某個等待隊列上
等待,而又希望有喚醒能夠打斷這個等待的話,使用此函數。
對ssleep的調用將使進程進入不可中斷的休眠,但休眠時間以秒計。

6.長延時
在內核中,以jiffies爲單位進行的延遲通常被認爲是長延時。
(1)忙等待
實現忙等待的函數本身不利用CPU進行有用的工作,同時還不讓其他程序使用CPU.這並不
實現長延時的好方法。
(2)睡眠等待
睡眠等待比忙等待的方式更好一些,本進程會在等待時將處理器讓給其他進程。
用於睡眠等待的兩個函數是:
#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);
函數使用場合是:在一個特定的條件滿足或者超時發生後,希望代碼繼續運行。
它們的實現都是基於schedule_timeout();
#include <linux/sched.h>
signed long schedule_timeout(signed long timeout)

(3)內核定時器
內核定時器可以用在未來的某個特定時間點(基於時鐘滴答)調度執行某個函數,從而
可用於完成許多任務。
@ 定時器API
#include <linux/timer.h>
truct timer_list {
struct list_head entry; timer_list結構體鏈表的頭部
unsigned long expires;
用於存放延時結束時間
void (*function)(unsigned long); 延時結束時執行的回調函數,注意這裏傳遞一個無符號長整型數字
unsigned long data;
常用於存儲數據的指針
......
}
DEFINE_TIMER() 靜態創建定時器
init_timer() 動態定義一個定時器
struct timer_list TIMER_INITIALIZER(_function,_expires,_data) 初始化timer_list數據結構
void add_timer(struct timer_list *timer); 註冊定時器結構,以在當前CPU上運行
int mod_timer(struct timer_list *timer,unsigned long expires) 修改一個已經調度的定時器結構的到期時間,它也可以代替add_timer 函數使用
int timer_pending (struct timer_list *timer) 返回布爾值,用來判斷給定的定時器結構是否已經被註冊運行
void del_timer(struct timer_list *timer)
void del_timer_sync(struct timer_list *timer)從活動定時器清單中刪除一個定時器。後一個函數確保定時器不會在其他CPU上運行。
@定時器的應用實例
可以通過init_timer()動態定義一個定時器,也可以通過DEFINE_TIMER()靜態創建定時器。然後,將處理函數的地址和參數綁定給一個timer_list,並使用add_timer()註冊它即可:
#include <linux/timer.h>
struct timer_list my_timer;

init_timer(&my_timer); /* Also see setup_timer() */
my_timer.expire = jiffies + n*HZ; /* n is the timeout in number of seconds */
my_timer.function = timer_func; /* Function to execute after n seconds */
my_timer.data = func_parameter; /* Parameter to be passed to timer_func */
add_timer(&my_timer); /* Start the timer */

上述代碼只會讓定時器運行一次。如果想讓timer_func()函數週期性地執行,需要在timer_func()加上相關代碼,指定其在下次超時後調度自身:
static void timer_func(unsigned long func_parameter)
{
/* Do work to be done periodically */
/* ... */

init_timer(&my_timer);
my_timer.expire = jiffies + n*HZ;
my_timer.data = func_parameter;
my_timer.function = timer_func;
add_timer(&my_timer);
}

   可以使用mod_timer()修改my_timer的到期時間,使用del_timer()取消定時器,或使用timer_pending()以查看my_timer當前是否處於等待狀態。
查看kernel/timer.c源代碼,會發現schedule_timeout()內部就使用了這些 API。
二、tasklet
1.tasklet介紹
tasklet是一個延遲方法,可以實現將已登記的函數進行推後運行。一個給定的tasklet只運行在一個 CPU 中(就是用於調用該tasklet的那個 CPU), 同一tasklet永遠不會同時運行在多個 CPU 中。 但是不同的tasklet可以
同時運行在不同的 CPU 中。和內核定時器的相同點是:(1)它們始終在中斷期間運行;(2)始終會在調度它們的同一CPU上運行;(3)都接受一個unsigned long參數;(4)也會在“軟件中斷”上下文以原子模式來執行。
和內核定時器不同的是:不能要求在某個給定時間執行給定的函數。調度一個tasklet,表明我們只是希望內核選擇某個其後的時間來執行給定的函數。這個對中斷例程來說尤其有用。
tasklet以數據結構的形式存在,並在使用前必須初始化。 tasklet_struct 結構體包含了用於管理和維護tasklet的必要數據 (狀態,通過 atomic_t 來實現允許/禁止控制,函數指針,數據,以及鏈表引用)

2.tasklet的API

#include <linux/interrupt.h>

該宏調用只是利用所提供的信息對結構體 tasklet_struct 進行初始化(tasklet名,函數,以及tasklet專有數據)。 默認情況下,微線程處於允許狀態,這意味着它可以被調度。
DECLARE_TASKLET( name, func, data );

將tasklet默認聲明爲禁止狀態。這時需要調用函數tasklet_enable來實現tasklet可被調度。
DECLARE_TASKLET_DISABLED( name, func, data);

此函數初始化一個通過分配或者其他途徑獲得的tasklet結構。
void tasklet_init( struct tasklet_struct *, void (*func)(unsigned long),unsigned long data );

禁用指定的tasklet,但不會等待任何正在運行的tasklet退出。返回後,tasklet是禁用的直到重新啓用之前,不會再次調度;返回時,有可能tasklet仍在運行。
void tasklet_disable_nosync( struct tasklet_struct * );

函數禁用指定的tasklet,但仍可以被調度,但會推遲執行,直到該tasklet被重新啓用。如果tasklet正在運行,該函數會進入忙等待直到tasklet退出爲止。
void tasklet_disable( struct tasklet_struct * );

存在兩個 enable 函數: 一個用於正常優先級調度(tasklet_enable),另一個用於允許高優先級調度(tasklet_hi_enable)。
正常優先級調度通過 TASKLET_SOFTIRQ-level 軟中斷來執行, 高優先級調度則通過 HI_SOFTIRQ-level 軟中斷執行。
void tasklet_enable( struct tasklet_struct * );
void tasklet_hi_enable( struct tasklet_struct * );

由於存在正常優先級和高優先級的 enable 函數, 因此要有正常優先級和高優先級的調度函數,每個函數利用特殊的軟中斷矢量來爲tasklet排隊(tasklet_vec 用於正常優先級, 而 tasklet_hi_vec 用於高優先級)。
來自高優先級矢量的tasklet先得到服務,隨後是來自正常優先級矢量的tasklet。 注意,每個 CPU 維持其自己的正常優先級和高優先級軟中斷矢量。
void tasklet_schedule( struct tasklet_struct * );
void tasklet_hi_schedule( struct tasklet_struct * );

tasklet 生成之後,可以通過函數tasklet_kill來停止。函數tasklet_kill保證tasklet不會再運行。而且,如果按照進度該tasklet應該運行,將會等到它運行完後,再kill該線程。
tasklet_immediate 只在指定的CPU處於dead狀態時被採用。
void tasklet_kill( struct tasklet_struct * );
void tasklet_kill_immediate( struct tasklet_struct *, unsigned int cpu );

上述API的實現可以通過./kernel/softirq.c 與 ./include/linux/interrupt.h來了解。

3.應用實例

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/interrupt.h>

char my_tasklet_data[]="my_tasklet_function was called";

void my_tasklet_function( unsigned long data )
{
printk( "%s\n", (char *)data );
return;
}

DECLARE_TASKLET( my_tasklet, my_tasklet_function, (unsigned long) &my_tasklet_data );

int init_module( void )
{
tasklet_schedule( &my_tasklet );

return 0;
}

void cleanup_module( void )
{

tasklet_kill( &my_tasklet );

return;
}

三、workqueue
1.workqueue介紹

工作隊列是實現延遲的新機制,從 2.5 版本 Linux 內核開始提供該功能。 不同於tasklet一步到位的延遲方法,工作隊列採用通用的延遲機制, 工作隊列的處理程序函數能夠休眠(這在tasklet模式下無法實現)。
工作隊列可以有比微線程更高的時延,併爲任務延遲提供功能更豐富的 API。 處於核心的是工作隊列(結構體 workqueue_struct), 任務被安排到該結構體當中。 任務由結構體 work_struct 來說明, 用來鑑別哪些任務
被延遲以及使用哪個延遲函數。events/X 內核線程(每 CPU 一個)從工作隊列中抽取任務並激活一個處理程序(由處理程序函數在結構體 work_struct 中指定)。
表明類似於tasklet,它們都允許內核代碼請求某個函數在將來的時間被調用。但兩者之間存在一些非常重要的區別,包括:
(1)tasklet在軟件中斷上下文中運行,因此所有的tasklet代碼都必須是原子的。相反,工作隊列在一個特殊內核進程的上下文中運行,因此它們具有更好的靈活性。尤其是,工作隊列函數可以休眠。
(2)tasklet始終運行在被初始提交的同一個處理器上,但這只是工作隊列的默認方式。
(3)內核代碼可以請求工作隊列函數的執行延遲給定的時間間隔。
兩者的關鍵區別在於:tasklet會在很短的時間段內很快執行,並且以原子模式執行,而工作隊列函數可具有更長的延遲且不必原子化。兩種機制有各自適應的情形。

2.相關的API
#include <linux/workqueue.h>

工作隊列通過宏調用生成 create_workqueue,返回一個 workqueue_struct 值。 可以通過調用函數 destroy_workqueue 銷燬一個工作隊列。
其中,每個工作隊列有一個或者多個專用的進程(“內核線程”),這些進程運行提交到該隊列的函數。如果我們使用create_workqueue,則內核會在系統中的每個處理器上爲該工作隊列創建專用的線程。
在許多情況下,衆多的線程可能對性能具有某種程度的殺傷力;因此,如果單個線程夠用,那麼應該使用create_singlethread_workqueue創建工作隊列。
struct workqueue_struct *create_workqueue( const char *name );
struct workqueue_struct *create_singlethread_workqueue(const char * name );
void destroy_workqueue( struct workqueue_struct * );

通過工作隊列與之通信的任務可以由結構體 work_struct 來定義。 通常,該結構體是用來進行任務定義的結構體的第一個元素。要向工作隊列提交一個任務,需要填充一個work_struct結構。
如果在編譯時創建和初始化一個work_struct結構,就使用DECLARE_WORK來完成;如果在運行時構造work_struct結構,就使用INIT_WORK和PREPARE_WORK這個兩個宏。
INIT_WORK完成更加徹底的結構初始化工作;首次構造該結構時,應該使用這個宏;PREPARE_WORK完成幾乎相同的工作,但它不會初始化用來將work_struct結構鏈接到工作隊列的指針。如果結構已經被提交到工作隊列,而只是需要
修改該結構,則應該使用PREPARE_WORK,而不是INIT_WORK.如果開發人員需要在任務被排入工作隊列之前發生延遲,可以使用宏 INIT_DELAYED_WORK 和 INIT_DELAYED_WORK_DEFERRABLE。

DECLARE_WORK(name,void (*function)(void *),void *data)
INIT_WORK( struct work_struct *work, void (*function)(void*),void *data );
PREPARE_WORK(struct work_struct *work,void (*function)(void *),void *data);
INIT_DELAYED_WORK( work, func );
INIT_DELAYED_WORK_DEFERRABLE( work, func );

任務結構體的初始化完成後,接下來要將任務安排進工作隊列。 可採用多種方法來完成這一操作。 首先,利用 queue_work 簡單地將任務安排進工作隊列(這將任務綁定到當前的 CPU)。 或者,
可以通過 queue_work_on 來指定處理程序在哪個 CPU 上運行。 兩個附加的函數爲延遲任務提供相同的功能(其結構體裝入結構體 work_struct 之中,並有一個計時器用於任務延遲 )
int queue_work( struct workqueue_struct *wq, struct work_struct *work );
int queue_work_on( int cpu, struct workqueue_struct *wq, struct work_struct *work );
int queue_delayed_work( struct workqueue_struct *wq,struct delayed_work *dwork, unsigned long delay );
int queue_delayed_work_on( int cpu, struct workqueue_struct *wq,struct delayed_work *dwork, unsigned long delay );

在使用全局的內核共享工作隊列的時候,不需要定義工作隊列結構體,利用以下函數來爲工作隊列定位。由於是共享隊列,我們不應該長期佔用該隊列,即不能長時間休眠,而且我們的任務可能需要更長時間才能獲得處理器時間。
int schedule_work( struct work_struct *work );
int schedule_work_on( int cpu, struct work_struct *work );
int scheduled_delayed_work( struct delayed_work *dwork, unsigned long delay );
int scheduled_delayed_work_on(int cpu, struct delayed_work *dwork, unsigned long delay );

還有一些幫助函數用於清理或取消工作隊列中的任務。想清理特定的任務項目並阻塞任務,直到操作完成爲止, 可以調用 flush_work 來實現。 指定工作隊列中的所有任務的清理能夠通過調用 flush_workqueue 來完成。
這兩種情形下,調用者阻塞直到操作完成爲止。 爲了清理內核全局工作隊列,可調用 flush_scheduled_work。
int flush_work( struct work_struct *work );
int flush_workqueue( struct workqueue_struct *wq );
void flush_scheduled_work( void );

還沒有在處理程序當中執行的任務可以被取消。 調用 cancel_work_sync 將會終止隊列中的任務或者阻塞任務直到回調結束(如果處理程序已經在處理該任務)。 如果任務被延遲,可以調用 cancel_delayed_work_sync。
int cancel_work_sync( struct work_struct *work );
int cancel_delayed_work_sync( struct delayed_work *dwork );

可以通過調用 work_pending 或者 delayed_work_pending 來確定任務項目是否在進行中。
work_pending( work );
delayed_work_pending( work );

結束對工作隊列的使用後,可調用下面的函數釋放相關資源:
void destroy_workqueue(struct workqueue_struct *queue)

這就是工作隊列 API 的核心。API 在 ./include/linux/workqueue.h 中定義,在 ./kernel/workqueue.c 中能夠找到工作隊列 API 的實現方法。

3.應用實例

工作隊列的使用又分兩種情況,一種是利用系統共享的全局工作隊列來添加自己的工作,這種情況處理函數不能消耗太多時間,這樣會影響共享隊列中其他任務的處理;另外一種是創建自己的工作隊列並添加工作。

一、利用系統共享的工作隊列添加工作:
第一步:聲明或編寫一個工作處理函數
void my_func();

第二步:創建一個工作結構體變量,並將處理函數和參數的入口地址賦給這個工作結構體變量
DECLARE_WORK(my_work,my_func,&data); 編譯時創建名爲my_work的結構體變量並把函數入口地址和參數地址賦給它;
如果不想要在編譯時就用DECLARE_WORK()創建並初始化工作結構體變量,也可以在程序運行時再用INIT_WORK()創建
struct work_struct my_work;
創建一個名爲my_work的結構體變量,創建後才能使用INIT_WORK()
INIT_WORK(&my_work,my_func,&data); 初始化已經創建的my_work,其實就是往這個結構體變量中添加處理函數的入口地址和data的地址,通常在驅動的open函數中完成

第三步:將工作結構體變量添加入系統的共享工作隊列
schedule_work(&my_work);
添加入隊列的工作完成後會自動從隊列中刪除

schedule_delayed_work(&my_work,tick); 延時tick個滴答後再提交工作

二、創建自己的工作隊列來添加工作
第一步:聲明工作處理函數和一個指向工作隊列的指針
void my_func();
struct workqueue_struct *p_queue;

第二步:創建自己的工作隊列和工作結構體變量(通常在open函數中完成)
p_queue=create_workqueue("my_queue");
創建一個名爲my_queue的工作隊列並把工作隊列的入口地址賦給聲明的指針

struct work_struct my_work;
INIT_WORK(&my_work,my_func,&data); 創建一個工作結構體變量並初始化,和第一種情況的方法一樣

第三步:將工作添加入自己創建的工作隊列等待執行
queue_work(p_queue,&my_work);
作用與schedule_work()類似,不同的是將工作添加入p_queue指針指向的工作隊列而不是系統共享的工作隊列

第四步:刪除自己的工作隊列
destroy_workqueue(p_queue); 一般是在close函數中刪除
發佈了49 篇原創文章 · 獲贊 21 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章