1. 爲什麼要有中斷?
CPU的處理速度比外部硬件塊.2.採用輪詢的方式比較耗費CPU資源.3.中斷處理的效率比較高.. 內核處理優先級:硬件中斷 >軟件中斷 > 普通進程
2. 中斷的硬件觸發流程.
硬件外設產生硬件的電信號變化,這個電信號首先發送給中斷控制器(能夠打開,關閉中斷,能夠指定中斷的優先級,還能夠判斷中斷是否發生),中斷控制器判斷是否使能,判斷優先級,最終決定是否給cpu發送一個電信號,cpu一旦檢測到這個電信號以後,cpu就會處理這個中斷。
1. linux內核如何實現硬件中斷編程.
3.1gpio對於內核是一種資源,系統調用號對於內核是一種資源,設備號對於內核也是一種資源,物理內存對於內核是一種資源,同樣硬件中斷對於內核也是一種寶貴的資源!所以在內核處理中斷時,首先要向內核去申請中斷資源。
3.2中斷的處理流程包括4個部分:建立異常向量表,編寫保存現場的代碼,編寫恢復現場的代碼,編寫硬件中斷對應的服務程序。對於一般的arm裸板程序,需要完全自己實現4個部分。但是在內核中斷編程時,前3個都是內核已經幫你寫好!對於驅動開發只需實現第4部,編寫硬件中斷對應的服務程序。
2. 申請硬件中斷資源和註冊對應的硬件中斷的服務程序.
4.1int request_irq(unsigned int irq,irq_handler_t handler,unsigned long irqflags,const char *name,void *dev_id);
函數功能:
1.向內核申請硬件中斷資源2.註冊這個硬件中斷對應的服務程序,一旦這個中斷被觸發,內核就會調用這個中斷對應的服務程序;
參數:irq:待申請的中斷號,用來在內核空間中表示硬件資源,內核用irq_eint(0)來表示外部中斷0,用irq_eint(1)來表示外部中斷1,這個宏都是在內核平臺頭文件定義,由芯片公司實現。中斷號0--31內核保留!
handler:待註冊的中斷處理函數
irqflsgs:中斷標誌 給中斷控制器確定中斷的出發方式
IRQF_TRGGER_RISING|TRQF_TRIGGER_FALLING,這個宏最終是給中斷控制器配置的!如果硬件中斷時內部中斷(串口)(外設涉及中斷觸發方式無法人爲去配置,一般這個參數寫0.)
IRQF_SHARED:表示多個設備共享中斷 IRQF_SAMPLE_RANDOM:用於隨機數種子的隨機採樣.IRQF_TRIGGER_RISING:上升沿觸發中斷 IRQF_TRIGGER_FALLING:下降沿觸發中斷
IRQF_TRIGGGER_HIGH:高電平觸發 IRQF_TRIGGER_LOW:低電平觸發
IRQF_DISABLE:本中斷處理時,其它中斷進行屏蔽。
name:中斷設備的名稱,出現在cat /proc/interrupts 一般用於中斷調試
dev_id:傳遞給中斷處理函數的指針,通常用於共享中斷時傳遞設備結構體,可以通過dev_id給中斷處理函數傳遞參數 返回:成功返回0,失敗返回負值,-EINVAL:表示申請的中斷號無效或者中斷處理函數
4.2硬件中斷如果不在使用,一定要釋放硬件中斷資源和卸載對應處理函數:
free_irq(int irq,void *dev_id);函數功能:釋放中斷和卸載對應的處理函數
參數:irq:中斷號dev_id:這個參數一定要和註冊中斷時傳遞的參數保持一致,否則內核出現崩潰!在註冊的時候,中斷號就已經進行了綁定!
3. 中斷處理函數原型。
irqturn_t(*irq_handler_t)(int irq,void *dev_id);
irq:中斷號dev_id:在註冊中斷處理函數時傳遞過來的參數,如果不傳遞參數指定爲NULL,
返回:常見返回值如下:IRQ_NONE:中斷未做處理IRQ_HANDLED:正常後處理應該返回該值
6.中斷處理函數的要求。
1.明確之前所說的硬件中斷優先級僅僅適用於中斷控制器
2.linux內核對於硬件中斷無優先級這個概念,明確linux內核硬件中斷優先級
高於軟件中斷的優先級,軟中斷的優先級高於進程;
軟中斷分爲優先級(2級),進程也有優先級。
3.就是因爲linux內核對於硬件中斷無優先級,所以要求中斷處理函數的執行速度要快,讓中斷及時釋放cpu資源給別的中斷或者進程使用。如果中斷處理函數長時間的佔有cpu資源,別的硬件中斷或者進程,軟中斷無法獲取cpu資源,影響系統的併發能力和響應能力!
4.在中斷處理函數中千萬不能調用引起阻塞(忙等待或者休眠等待)的函數,例如
copy_to_user,copy_from_user,kmalloc
5.中斷不屬於任何進程,不參與進程的調度(實時linux內核將中斷線程化)
6.中斷處理函數中不能和用戶進行數據的交互,如果要進行數據交互,一定要配合系統調用
(struct file_operations)!
7.linux內核指定的中斷棧爲1頁,早期內核中斷棧共享進程內核棧(8k).
7. 共享中斷:多個硬件外設共享一個硬件中斷資源。
注意:有多個硬件外設,意味着有多個設備驅動,有多個設備驅動意味着每一個驅動都會調用request_irq註冊中斷。
共享中斷的編程要求:
1.必須指定IRQF_SHARED;2.dev_id必須不一樣3.中斷處理函數不能調用disable_irq(關閉中斷),如果關閉中斷,那麼共享設備的資源不能處理中斷。4.cpu區分外設產生中斷前提是外設必須硬件設備上具備判斷中斷是否是其產生的條件!外設硬件不具備這個條件,這個外設不能使用共享中斷!
4. 關於中斷處理的相關操作.
6.1理論知識: 在linux內核中硬件中斷無優先級,軟件中斷中有兩個優先級 可以設置進行的優先級.中斷處理爲內核空間.由於中斷處理函數不屬於任何進程,因爲沒有用戶進程佔用的地址空間,則中斷處理函數向用戶空間發送數據和接收數據。中斷中需要內核和用戶進行數據交互需要通過系統調用接口(file_operations接口),阻塞分爲忙等待和休眠等待.
6.2中斷處理函數的特點.(解決此辦法的底半部和頂半部)
1.中斷隨機出現,且中斷處理函數不屬於任何進程,處於中斷上下文. 2.執行速度要快和簡潔,且中斷棧爲一頁. 3.不能向用戶空間發送和接收數據. 4.不能調用可能引起阻塞的函數. Copy_to_user/kmalloc.
6.3如果中斷處理函數需要執行的時間比較長,怎麼解決?
明確:如果中斷處理函數長時間的佔用cpu資源,會導致別的任務無法獲取cpu資源,影響系統的併發能力和響應能力。甚至如果在中斷處理函數中進行休眠操作,最終導致linux系統處於殭屍狀態!
結論:
linux內核爲了提供系統的併發能力和響應能力,解決中斷處理函數長時間的佔有cpu的情況,linux內核將中斷處理函數進行劃分,劃分爲兩部分:頂半部,底半部。
注意這種劃分不是函數的劃分和函數之間的調用!
頂半部:本質上還是之前的中斷處理函數,其中完成的內容相對比較緊急,耗時較短,遵循linux內核要求中斷處理函數實行的速度這個原則,一旦中斷髮生之後,內核首先執行頂半部內容,但是這個頂半部佔用cpu的時間非常短,也就保證其他任務可以及時獲取到cpu的資源。其他複雜的事情可以放在底半部執行。頂半部不可以被中斷!頂半部還需要登記底半部,告訴cpu我的中斷還需要一些比較耗時的內容在將來(空閒時)要你去完成!
底半部:完成之前中斷處理函數中比較不耗時,不緊急的事情!可以被別的中斷(硬件中斷和軟件中斷,甚至是進程)打斷!
5. 中斷底半部的實現。(重點)
1.tasklet(小任務)
2.工作隊列--與等待隊列不同
3.軟中斷
7.1tasklet:又名“小任務”,軟中斷任務,優先級高於進程,但是低於硬件中斷。運行在中斷上下文!
linux內核描述tasklet使用的數據結構:
struct tasklet_struct{ <linux/interrupt.h>
void (*func)(unsigned long );//底半部處理函數
unsigned long data; //給底半部處理函數傳遞的參數,一般傳遞指針,要注意在處理函數中對數據類型的轉換};
1.分配初始化tasklet對象
方法1:
DECLARE_TASKLET(tasklet變量名,tasklet處理函數,給處理函數傳遞的參數);
方法2:
struct tasklet_struct tasklet; //分配
tasklet_init(&tasklet,處理函數,給處理函數傳遞的參數);//初始化
2.在頂半部(中斷處理函數)中調用tasklet_schedule函數進行登記底半部tasklet,是登記而不是執行!一旦登記成功,頂半部肯定會先執行完畢,趕緊釋放cpu資源,tasklet的處理函數會cpu空閒時去執行。
3.注意事項:tasklet還是工作在中斷上下文中,遵循中斷的處理過程,千萬不能做休眠阻塞的事情!
問題:底半部就需要休眠,怎麼辦?工作隊列機制
案例:將按鍵中斷採用tasklet來實現。
<linux/interrupt.h>tasklet定義:DECLARE_TASKLET(taskletname,tasklet_func,data);
--yaskletname:待定義的tasklet名字--tasklet_func:tasklet處理函數--data:待傳入tasklet處理函數的參數
代碼:
#include<linux/init.h>
#include<linux/module.h>
#include<linux/irq.h>
#include<linux/interrupt.h>
//定義硬件相關的數據結構
struct btn_resource{
int irq; //中斷號
char *name;//中斷名稱};//分配初始化按鍵信息
static structbtn_resource btn_info[] ={
[0] = {
.irq = IRQ_EINT(0),
.name ="KEY_UP"
},
[1] ={
.irq = IRQ_EINT(1),
.name ="KEY_DOWM"
}
};
static int mydata =0x5555;
static void btn_tasklet_func(unsignedlong data){
int *p=(int *)data;//數據類型轉換
printk("底半部%s:data=%#x",__func__,*p);
}
//1. 分配初始化tasklet對象//第一個參數是tasklet對象名
//第二個參數是tasklet延後處理函數//第三個參數給延後處理函數傳遞的參數
staticDECLARE_TASKLET(btn_tasklet,
btn_tasklet_func,
(unsignedlong)&mydata);
//中斷處理函數(頂半部)
static irqreturn_t button_isr(intirq, void *dev_id){
//2.登記底半部tasklet
tasklet_schedule(&btn_tasklet);
printk("頂半部%s\n",__func__);
return IRQ_HANDLED;
}
static intbtn_init(void){
int i;
//申請硬件中斷資源和註冊中斷處理函數
for(i=0;i<ARRAY_SIZE(btn_info);i++)
request_irq(btn_inifo[i].irq,button_isr,IRQF_TRIGGER_FALLING||TRQF_TRIGGER_RISING,i
btn_info[i].name,&btn_info[i]);
return 0;
}
static voidbtn_exit(void){
int i;
//釋放硬件中斷資源和卸載中斷處理函數
for(i=0;i<ARRAY_SIZE(btn_info);i++)
free_irq(btn_info[i].irq,&btn_info[i]);
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");
**********************************************
問題:底半部就需要休眠,怎麼辦?
答;tasklet的延後處理函數不允許休眠,但是在某些場合可能進行休眠操作,又要延後執行,這是可以考慮使用工作隊列。工作隊相關的延後處理函數允許休眠。明確:“休眠” 這個詞僅僅適用於進程!
7.2工作隊列(work queue)內核線程執行。
工作隊列是linux kernel中將工作推後執行的一種機制;這種機制和硬件中斷或tasklet不同之處在於工作隊列是把推後的工作交由一個內核線程去執行,因此工作隊列的優勢就在於它允許重新調用甚至睡眠.工作隊列是2.6內核開始引入的機制,在2.6.20之後,工作隊列的數據結構發生了一些變化,被拆分成兩個部分 在此,主要對2.6.20之後的版本作介紹
定義在<linux/workqueue.h>
工作隊列實現過程:
1.工作隊列延後執行設計數據結構
struct work_struct{
atomic_long data; //記錄工作狀態
work_func_t func; //工作處理函數,由用戶實現,一旦cpu空閒,cpu就會執行這個函數
} typedef void(*work_fucn_t)(struct work_struct *work);//工作原型
struct delayed_work{
struct work_struct work; //用來包含工作延後處理函數
struct timer_list timer;//用來指定執行時間間隔
}; //處理延時
2.如何使用工作隊列來進行延後處理?
分配工作或者延後工作對象
struct work_struct work; //分配一個普通的工作隊列
struct delayed_work dwork;//分配一個延時工作
初始化工作或者延時工作
INIT_WORK(&work,work_function);//初始化普通的工作,並且指定工作的延後處理函數work_function
INIT_DELAYED_WORK(&dwork,dwork_fuction);//初始化延時的工作,並且指定延時工作的處理函數dwork_fuction
在頂半部(中斷處理函數)中登記普通的工作或者延時工作:schedule_work(&work);//一旦登記,cpu在空閒時立即執行普通工作的處理函數或者schedule_delayed_work(&dwork,5*HZ);//將延時工作的登記放在5秒以後去登記,一旦登記完畢,cpu在空閒時立即執行延後工作的處理函數。
注意:工作隊列工作在進程上下文,允許休眠,但是它的優先級要低於tasklet(工作在中斷上文中)
案例:利用工作隊列實現按鍵驅動案例:利用工作隊列實現2s流水燈操作
#include <linux/init.h>
#include <linux/module.h>
#include <linux/irq.h>
#include <linux/interrupt.h>
//分配延時工作對象
static struct delayed_work dwork;//延時工作
//延時工作處理函數
//work指針指向延時工作dwork的第一個成員,也就是指向延時工作對象
static void dwork_function(struct work_struct *work){
//開關燈,如果是開,你就關,如果是關,你就開,
//不允許使用if else來判斷
//重新登記
schedule_delayed_work(&dwork,2*HZ);
}
static int btn_init(void){
//申請gpio資源,配置爲輸出口,輸出爲0
//初始化延時工作
INIT_DELAYED_WORK(&dwork,dwork_function);
schedule_delay_work(&dwork,2*HZ);
return 0;
}
static void btn_exit(void){
cancel_delayed_work(&dwork);//取消延時工作
}
module_init(btn_init);
module_exit(btn_exit);
MODULE_LICENSE("GPL");
4 0 0:00 [events/0]
4號線程(進程)來處理工作隊列 0爲單核cpu。
**************************************************************************
總結:調用schedule_work或者schedule_delayed_work這兩個函數,都會將工作和延時工作交給內核默認的工作隊列和內核線程,內核默認的線程叫【events/0】..[event/cpu編號],這種用法的優點是簡單易用,程序員不需要關心如何創建工作隊列和內核線程,但是缺點是導致內核的線程的負載過程,執行的效率太低!可以考慮創建自己的工作隊列和內核線程去處理自己的工作或者延時工作。
linux內核描述工作隊列的數據結構:
struct workqueue_strut; //裏面存放的登記的工作或者延時工作。
如何創建自己的工作隊列和內核線程?
工作者線程
1. 工作者線程本質上是一個普通的內核線程,在默認情況下,每個CPU均有一個類型爲“events”的工作者線程,當調用schedule_work時,這個工作者線程會被喚醒去執行工作鏈表上的所有工作。
2. 以上介紹操作均是採用缺省工作者線程來實現工作隊列,其優點是簡單易用,缺點是如果缺省工作隊列負載太重,執行效率會很低,返回需要我們創建自己的工作者線程和工作隊列。
struct workqueue_struct *creat_workqueue(char *name);
函數功能:會給每一個cpu(如果是多核的cpu,會耗費cpu資源)創建自己的工作隊列和內核線程,並且將自己的工作隊列和內核線程進行綁定,以後自己的內核線程只處理自己工作隊列上的工作或者延時工作。
返回值:創建的工作隊列的指針
參數name:創建的內核線程的名字,通過ps命令查看creat_singlethread_workqueue:這個函數僅僅創建一個內核或者工作隊列,他沒有和具體的cpu進行綁定,在使用的時候可以指定到具體的cpu上去!
如何將自己的工作或者延時工作交給自己的內核線程去處理?
queue_work(struct workqueue_struct *queue, struct work_struc *work);
queue_delayed_work(struct workqueue_struct *queue, struct work_struc *work, unsigned long delay);
注意跟schedule_work和schedule_delayed_work區別
銷燬自己的工作隊列和內核線程:destroy_workqueue(自己的工作隊列指針);
案例:優化按鍵驅動,創建自己的工作隊列和內核線程處理按鍵中斷。
*****************************************************************\
3.軟中斷:軟中斷對應的延後處理函數運行在中斷上下文中,tasklet本身也是基於軟中斷實現的,它和tasklet之間的區別:1.軟中斷的延後處理函數可以同時在多個cpu上同時執行!但是tasklet不行,只能在一個cpu上運行。2.軟中斷的處理函數在設計的時候必須具備可重入性。
函數一:static int g_data;//全局變量
void swap(int *x,int *y){
g_data =*x;
*x = *y;
*y = g_data;
}
函數二:void swap(int *x,int *y){
int data;
data =*x;
*x =*y;
*y =data;
}
如何將一個函數設計爲重入函數:1.儘量避免使用全局變量2.如果使用全局變量,一定要進行互斥訪問,比如加鎖,或關閉中斷.3.軟中斷的處理函數不能以模塊的形式實現,必須修改內核源碼,靜態編譯內核。
案例:按鍵驅動程序的簡單實現
下面是基於中斷和消息的按鍵驅動程序,其工作原理是:當應用程序讀取鍵值時,會調用按鍵驅動程序的read函數,而我們實現的read函數檢測完讀取長度後沒有直接讀取鍵值而是等待按鍵消息,如果沒有按鍵,程序會進入休眠狀態,這樣可以節省大量的CPU,而當我們按鍵時硬件會產生中斷,程序自動進入中斷處理函數,在中斷處理函數中,驅動程序讀取鍵值存入全局變量並激活read函數中等待的消息,應用程序被迅速喚醒並通過read函數讀取鍵值,如此,完成了獲取鍵值的工作。下面是源碼,比較簡單,也就不多說了。
上面這種方式實現的按鍵驅動程序有個弊端,如果我們不按鍵,應用程序將會永遠阻塞在這裏,幸運的是,linux內核提供了poll/select機制,可以設置超時等待時間,如果在這個時間內讀取到鍵值則正常返回,反之則超時退出。使內核支持poll非常簡單,爲file_operations的poll成員提供poll處理函數即可。
很多情況下,我們的程序在等待按鍵期間需要處理其它任務而不是在這裏空等,這時,就需要採用異步模式了。所謂異步模式,實際上是採用消息機制(以本 文的按鍵程序爲例),即當驅動程序檢測到按鍵後發送消息給應用程序,應用程序接收到消息後再去讀取鍵值。與前面的兩種模式相比,最大的不同在於異步方式是 驅動告訴應用程序來讀而不是應用程序主動去讀。
到這裏,這個驅動程序基本上就算可以了,當然,還有對阻塞和非阻塞的支持,同步與互斥的支持,而阻塞與非阻塞無非是加上個邏輯判斷,同步與互斥根應用程序的同步控制也差不多,無非就是信號量或者原子操作。
6. Linux等待隊列。使進程在內核空間進行休眠 與進程在用戶空間休眠(sleep)不同
工作隊列:是中斷底半部機制,是實現延後執行的一個手段。(內核線程處理)
等待隊列:是讓進程在內核空間進行休眠的,但是他們針對處理的對象都是進程!
案例:分析應用程序串口工具操作串口硬件設備的過程。
1.外設的處理速度遠遠慢於cpu!
2.應用程序在應用空間沒有權利訪問硬件設備,只有通過系統調用跑到內核空間纔有權限訪問硬件設備!
3.一個應用程序讀取串口硬件設備採用兩種方法:輪詢和中斷。
當串口設備沒有接收到數據,應用程序一旦發現,利用內核提供的睡眠機制,應用程序在內核空間進入到休眠狀態;一旦串口設備給cpu產生中斷信號,中斷信號的到來也就代表這數據的到來,這時只需喚醒休眠的應用程序,讓應用程序讀取串口數據。
8.1內核等待隊列。-在Linux驅動程序中,可以使用等待隊列來實現進程阻塞。等待隊列可以看作保存在進程的容器,在阻塞進程時,將進程放入等待隊列中當進程被喚醒時,從等待隊列中取出進程。 實際上,信號量等對進程的阻塞在內核中也依賴等待隊列來實現。
8.2 進程的狀態分類。
運行;TASK_RUNNING 準備就緒:TASK_READY 可中斷休眠:TASK_INTERRUOTIBLE. 不可中斷的休眠:TASK_UNINTERRUOTIBLE 。
進程要“休眠‘:要休眠的進程會將cpu資源全部從當前進程中撤出來,將cpu資源給被的任務去使用,比如另外一個進程;進程之間的切換:由內核調度器來實現,這個調度去就是用來管理進程的!
jiffies current(指向當前進程的指針) 內核全局變量
8.3linux內核等待隊列的實現。
老鷹-》調度器:內核已經實現 雞媽媽-》等待隊列頭 小雞-》休眠的進程
內核描述休眠的進程,內核等待隊列頭涉及的數據類型:
#include<linux/wait.h>
Struct_wait_queue_head{
spinlock_t lock;
struct list_head task_list;
}; Typedef struct _wait_queue_head wait_queue_head_t;
下面爲描述一個等待隊列對象等待隊列讓進程休眠,而不是喚醒。
方法一:1.分配等待隊列頭 2.初始化等待隊列頭 3.定義並初始化等待的隊列 4.將等待隊列添加到等待隊列頭中 5.設置當前進程的狀態 6.進程進行休眠 7.休眠被喚醒 8.設置進程的狀態爲運行狀態,獲取喚醒的原因 9.移除等待隊列.
具體實現:
1.分配等待隊列頭wait_queue_head_t wq;
2.初始化等待隊列頭init_waitqueue_head(&wq);
1 2 爲靜態方法 DECLARE_WAIT_QUEUE_HEAD(name); 定義並初始化一個隊列
3.如果一個進程要訪問設備,發現設備不可用,進入休眠,此時分配這個進程的休眠容器
DECLARE_WAITQUEUE(wait,current); //小雞 進程 wait:表示保存當前進程的容器
current:他是內核的全局變量,在linux內核中,內核用struct task_struct結構體來描述每一個進程,那麼當前進程獲取cpu資源是,current就指向當前進程(哪個進程獲取cpu資源,current就指向這個進程對應的task_strtct結構體對象)
例如打印出當前進程的pid和name
printk("current process name is %d pid is %d\n",
current->comm,current->pid);
或者
wait_queue_t wait;//定義一個等待的進程
init_waitqueue_entry(&wait,current); //初始化一個容器,休眠的進程.
注意:如果有多個休眠的進程,必須爲每一個進程分配一個容器,並且current也會分別執行不同的進程!
4.然後將當前進程添加到休眠的隊列中去add_wait_queue(&wq,&wait);
注意:僅僅是將當前進程添加到這個隊列中去,但進程還處於運行狀態!
5.設置當前進程的休眠狀態(可中斷或者不可中斷)
可中斷的休眠狀態:current->state =TASK_INTERRUPTIBLE;
不可中斷的休眠狀態:current->state = TASK_UNINTERRUPTIBLE;
6.讓進程進入真正的休眠狀態schedule(); //啓動調度器,並且讓cpu資源從當前進程撤下來,給別的進程去使用,至此當前進程運行到這個地方就停止不動!一旦被喚醒,這個函數返回,當前進程接着執行
指定超時時間的休眠:把schedule()換成schedule_timeout(5*HZ);
前者的休眠是永久休眠(沒有被驅動主動喚醒或者接收到信號)
後者的休眠是指定了睡眠的時間,例如5秒,如果沒有接收到信號,也沒有接收到驅動主動喚醒,一旦5秒到期,此函數也會返回,返回0,否則返回非零(驅動主動喚醒或者接收到了信號)!
7.如果進程被設置爲可中斷的休眠狀態,進程被喚醒的方法有兩種:1硬件設備可用,產生中斷,由中斷來喚醒;2進程接收到了信號引起的喚醒;
所以要判斷喚醒的原因:判斷是否接受到信號引起的喚醒:
if(signal_pending(current)){
printk("當前進程是由於接收到了信號引起的喚醒");
printf("給用戶返回-ERESTARTSYS");
}else{
printk("中斷引起的喚醒,說明數據可用");
printk("後續繼續獲取數據");
}
8.不管是硬件中斷引起的喚醒,從新設置當前進程的狀態爲運行狀態
9.將當前進程從休眠進程中移除remove_wait_queue(&wq,&wait);
進程的休眠動作完成
參考代碼:有一個進程讀取按鍵數據:
wait_queue_head_t wq; //全局變量
驅動入口函數或者open函數:
init_waitqueue_head(&wq);
驅動read函數:
static ssize_t btn_read(..){
wait_queue_t wait; //分配一個當前進程的容器
init_waitqueue_entry(&wait,current); //把當前進程添加到這個容器中
add_wait_queue(&wq,&wait); //將當前進程添加到休眠隊列中
current->state =TASK_INTERRUPTIBLE;//設置當前進程的休眠狀態
schedule(); //進入真正的休眠,一旦被喚醒,進程接着執行
if(signal_pending(current)){ //判斷喚醒的原因
printk("接收到了信號引起的喚醒");
ret =-ERESTARTSYS;
}
else {printk("按鍵有操作.chanshenyiqihonzgn");}
curent->state =TASK_RUNNING; //設置當前進程的狀態爲運行
remove_wait_queue(&wq,&wait); //從休眠隊列中移出,上報按鍵數據
return ret;
}
喚醒的方法有兩種:
1.接收到信號引起的喚醒 2.驅動主動喚醒休眠的進程,方法如下
wake_up(wait_queue_head_t *queue);
喚醒由queue指向的等待隊列數據鏈中的所有睡眠類型的等待進程
wake_up_interruptible(wait_queue_head_t *queue);
喚醒由queue指向的等待隊列數據鏈中的所有睡眠類型爲TASK_INTERRUPTIBLE的等待進程
案列1:實現讀進程喚醒寫進程,寫進程喚醒讀進程。
根據以上案例,2在底層驅動中的read函數能夠給用戶上報一個按鍵的信息(鍵值和按鍵的狀態),提示可以把底層驅動的write函數作爲終端來使用。
案例3:利用等待隊列實現按鍵驅動(將按鍵中斷的處理函數換成write的處理函數),要求按鍵上報的信息爲鍵值的狀態!例如:KEY_UP:0X50 KEY_DOWN:0X51 按鍵狀態:按下爲1,鬆開爲0
分析:read->fops->cdev->中斷->休眠->等待隊列
案例:實現按鍵驅動,指定超時,而不是永久休眠!
方法二:
1.分配等待隊列頭wait_queue_head_t wq;
2.初始化等待隊列頭init_waitqueue_head(&wq);
3.如果進程進入休眠狀態:wait_event(wq,condition);
c爲真,立即返回,不休眠 c爲假,進程進入不可中斷的休眠狀態,當前進程被添加到wq所對應的等待隊列頭中或者wait_event_interruptible(wq,condition);
c爲真,立即返回,不休眠 c爲假,進程進入可中斷的休眠狀態,進程被添加到wq所對應的等待隊列頭中
或者 wait_event_timeout(wq,condition,5*HZ);
c爲真,立即返回,不休眠 c爲假,進程進入不可中斷的休眠狀態,進程被添加到wq所對應的等待隊列頭中,並且超時時間到了,立即返回
wait_event_timeout_interruptible(wq,condition,5*HZ);
c爲真,立即返回,不休眠 c爲假,進程進入可中斷的休眠狀態,進程被添加到wq所對應的等待隊列頭中,
並且超時時間到了,進程也被喚醒。
總結:以上宏涉及的condition其實就是代表是否是驅動主動喚醒,如果驅動主動喚醒,應該讓condition設置爲真,否則還是爲假。
linux內核阻塞和非阻塞:
阻塞:進程的狀態要不爲忙,要不進行休眠忙等待:原地進程空轉 休眠等待:利用等待隊列機制
非阻塞:如果應用程序進入到內核,發現數據不可用,不會進行忙等待也不會休眠,而是返回用戶空間!
linux系統訪問文件默認採用阻塞方式:問:如果應用程序訪問設備文件等採用非阻塞如何實現?
答:只需在打開open設備或者文件時,指定一個操作選項即可:O_NONBLOCK,例如:
int fd =open("/dev/myled",O_RDWR|O_NONBLOCK);
問:雖然應用程序在打開設備的時候,指定了O_NONBLOCK,但是操作設備只能在內核空間的驅動中完成,那麼驅動如何知道應用程序時採用非阻塞呢?如果一旦驅動發現應用採用非阻塞方式來訪問設備,進程發現設備不可用,那麼進程立即返回到用戶空間,如果發現設備可用,操作設備!
答:每當應用程序調用open打開設備文件,通過軟中斷,陷入內核空間,找到對應的內核函數sys_open,sys_open創建的struct file結構體對象來描述設備文件打開以後的狀態信息,其中file結構體中有一個unsigned long f_flags來保存open打開時,指定的屬性(O_RDWR|O_NONBLOCK),底層驅動的接口函數,例如:int led_open(struct innode *inode,struct file *file)
int led_close(struct innode *inode,struct file *file)ssize_t led_close(struct file *file) .....
所以底層驅動只需要通過file指針就能夠獲取f_flags操作屬性,那麼底層驅動只需要通過以下代碼就可以判斷應用程序採用阻塞還是非阻塞:
if(file->f_flags&O_NONBLOCK){
//採用非阻塞
if(如果設備不可用){
//立即返回用戶空間
return -EAGIN;
}else{
//如果設備可用,操作設備
}else{
//採用阻塞
//採用等待隊列}
總結:以後設備驅動只要阻塞的實現,必須添加非阻塞的實現過程.:給之前的按鍵驅動添加非阻塞的實現。
7. Linux內核定時器。使用硬件定時器(具有週期性)來實現時間.
9.1時鐘中斷時鐘中斷由系統定時硬件以週期性的間隔產生。
定時器硬件的特點:1.工作頻率人爲的設定。2.定時器硬件會週期性的給CPU產生中斷信號。、
能夠通過編程指定它的工作輸出頻率,週期性得出cpu產生一個時鐘信號;
linux內核也有對應的時鐘中斷的處理函數,這個函數被內核週期性的調用;
時鐘中斷處理函數:1.更新jiffies/jiffies_64 2.更新實際時間3.檢查進程的時間片
4.檢查是否有到期的軟件定時器...
HZ:給硬件定時器使用,ARM,HZ=100表明一秒鐘產生100次時鐘中斷
jiffies:內核全局變量,記錄發生了多少次時鐘中斷,每發生一次時鐘中斷,
jiffies +1; linux內核一般使用jifies來表示時間
Jiffies:unsigned long型的變量,由於每次當發生時鐘中斷的時候,內核內部的計數器的值就會增加1,這個值子系統引導的時候被初始化爲0,因此使用jiffies用來記錄操作系統引導以來的發生時鐘中斷的次數。
9.2定時器的使用方法:
#inclued <linux/timer.h>
1.分配定時器對象
Struct timer_list{/***/
Unsigned long expires; //期望定時器執行的jffies值
Void (*function)(unsigned long);
Unsigned long data;
}; Struct timer_list timer;
2.初始化定時器對象void init_timer(struct timer_list *timer);
3.指定定時器的超時時間處理函數 Timer.function = function;
4.指定定時器的超時時間爲指定時間 Timer.expires = jiffies + msecs_to_jffies(speed) ;
speed爲毫秒值 注意:2 3 4 可以用下面代替宏來代替
Struct timer_list TIMER_INITIALIZER(_function, _expires, _data);
5.向內核註冊定時器對象並且啓動定時器void add_timer(struct timer_list *timer);
6.刪除定時器int del_timer(struct timer_list *timer);
7.修改一個已經調度的定時器結構的到期時間。
int mod_timer(struct tiimer_list *timer, unsigned long expires);
1.定時器的管理必須儘可能做到輕量級。 2.大部分的定時器在最多幾秒或者幾分鐘內到期,很少存在長期延時的定時器。3.定時器應該註冊在同一cpu上運行。
注意:1.定時器的實現基於軟中斷,所以定時器的處理函數同樣不能進行休眠操作!
2.如果想讓定時器的處理函數重複的執行,循環執行,只需在定時器處理函數中重新添加定時器即可!
案例1:利用定時器,每隔2s種打印“hello,tarena” 案例2:利用定時器,每個2s開關燈;
案例3:利用模塊參數的標識(不能採用字符設備驅動框架)來動態修改燈的閃爍頻率:2000ms,1000ms,
Static int speed =1000; Module_param(speed, int , 0664) ;
利用jiffies和定時器來實現延時;schedule_timeout(5*HZ);進程休眠,指定超時時間爲5秒。
echo 500 > /sys/module/mytimer_drv/parameters/speed
注意:加載完驅動模塊以後,無需卸載的情況下,動態修改閃爍的頻率!提示:權限爲0或者非零。
9.3linux內核的相關延時函數分爲忙等待和休眠等待:短延時(忙)、毫秒延時、長延時、睡眠延時。
忙等待 :cpu不幹別的事,原地空轉;
休眠等待:休眠只針對於進程,休眠是讓進程休眠,而非cpu休眠,cpu有可能去處理中斷,有可能在處理別的進程;
短延時:本質爲進行忙等待。
Void ndelay(unsignedlong nsecs);納秒延時 Void udelay(unsignedlong usecs);微秒延時
void mdelay(unsignedlong msece);毫秒延時
內核中,最好不要使用mdelay函數,因爲將會浪費CPU資源。
這三個函數的實現並不依賴與硬件定時器,因爲硬件定時器處理的時間最小是10ms。利用BogoMIPS,這個參數,通過cat /proc/cpuinfo 來查看,表明一秒鐘執行多少個百萬指令集;
注意:如果延時的時間大於10ms,一般不再使用mdelay。
毫秒延時:這些函數會使得調用進程休眠,時間由參數指定。
毫秒級:void msleep(unsignedint millisecs);
Unsigned long msleep_interruptible(unsignedint millisecs)
秒級:void ssleep(unsignedint seconds);
長延時:對於精度不高的延時,可以使用jiffies達到要求。
Unsigned longtime_before(unsigned long spurce, unsigned long target);
Unsigned longtime_after(unsigned long target , unsigned long source);
睡眠延時:睡眠延時是比忙等待更好的方法,睡眠延時在等待的時間到來之前處於睡眠的狀態,CPU資源可以被釋放供其它進程使用。
signed longschedule_timeout(unsigned long timeout);
-timeout:需要睡眠的jiffies計數值,用HZ來計算。
-返回非0表示超時時間到達返回,0爲進入可打斷睡眠時返回。
毫秒和jiffies轉換
unsigned long timeout =jiffies +msecs_to_jiffies(500);
unsigned long timeout =jiffies =HZ/2;