Linux中斷處理驅動程序編寫
中斷處理是操作系統必須具備的上要功能之一,下面我們一起來探討一下Linux中的中斷處理。
1. 什么是中斷
中斷就是CPU正常運行期間,由于內、外部事件引起的CPU暫時停止正在運行的程序,去執行該內部事件或外部事件的引起的服務中去,服務執行完畢后再返回斷點處繼續執行的情形。這樣的中斷機制極大的提高了CPU運行效率。
1.1. 中斷的分類:
1) 根據中斷的來源可分為內部中斷和外部中斷,內部中斷的中斷源來自于CPU內部(軟件中斷指令、溢出、除法錯誤等),例如操作系統從用戶態切換到內核態需要借助CPU內部的軟件中斷,外部中斷的中斷源來自于CPU外部,由外設觸發。
2) 根據中斷是否可以被屏蔽,中斷可分為可屏蔽中斷和不可屏蔽中斷,可屏蔽中斷可以通過設置中斷控制器寄存器等方法被屏蔽,屏蔽后,該中斷不再得到響應,而不可屏蔽中斷不能被屏蔽。
3) 根據中斷入口跳轉方式的不同,中斷可分為向量中斷和非向量中斷。采用向量中斷的CPU通常為不同的中斷分配不同的中斷號,當檢測到中斷的中斷號到來時,就自動跳轉到該中斷對應的地址處去執行程序。不同的中斷號對應不同的中斷入口地址。非向量中斷的多個中斷共享一個入口程序處理入口地址,中斷程序跳轉到該入口地址執行時,再通過中斷程序來判斷中斷標志來識別具體是哪一個中斷,也就是說向量中斷由硬件提供中斷服務程序入口地址,非向量中斷由軟件提供中斷服務程序入口地址。
4) 非向量中斷處理流程:
/*典型的非向量中斷首先會判斷中斷源,然后調用不同中斷源的中斷處理程序*/
irq_handler()
{
...
int int_src = read_int_status();/*讀硬件的中斷相關寄存器*/
switch(int_src)
{
//判斷中斷標志
case DEV_A:
dev_a_handler();
break;
case DEV_B:
dev_b_handler();
break;
...
default:
break;
}
...
}
2. linux中斷頂部、底部概念
為保證系統實時性,中斷服務程序必須足夠簡短,但實際應用中某些時候發生中斷時必須處理大量的工作,這時候如果都在中斷服務程序中完成,則會嚴重降低中斷的實時性,基于這個原因,linux系統提出了一個概念:把中斷服務程序分為兩部分:頂半部、底半部。
2.1. 頂半部
完成盡可能少的比較急的功能,它往往只是簡單的讀取寄存器的中斷狀態,并清除中斷標志后就進行“中斷標記”(也就是把底半部處理程序掛到設備的底半部執行隊列中)的工作。特點是響應速度快。
2.2. 底半部
中斷處理的大部分工作都在底半部,它幾乎做了中斷處理程序的所有事情。 特點:處理相對來說不是非常緊急的事件 ,底半部機制主要有:tasklet、工作隊列和軟中斷。
Linux中查看/proc/interrupts文件可以獲得系統中斷的統計信息:
3. Linux中斷編程
3.1. 申請和釋放中斷
3.1.1. 申請中斷:
int request_irq(unsigned int irq,irq_handler_t handler,unsigned long irqflags,const char *devname,void *dev_id);
參數介紹:irq是要申請的硬件中斷號。
Handler:是向系統登記的中斷處理程序(頂半部),是一個回調函數,中斷發生時,系統調用它,將dev_id參數傳遞給它。
irqflags:是中斷處理的屬性,可以指定中斷的觸發方式和處理方式:
觸發方式:IRQF_TRIGGER_RISING、IRQF_TRIGGER_FALLING、IRQF_TRIGGER_HIGH、IRQF_TRIGGER_LOW,處理方式:IRQF_DISABLE表明中斷處理程序是快速處理程序,快速處理程序被調用時屏蔽所有中斷,IRQF_SHARED表示多個設備共享中斷,dev_id在中斷共享時會用到,一般設置為NULL。
返回值:為0表示成功,返回-EINVAL表示中斷號無效,返回-EBUSY表示中斷已經被占用,且不能共享。
頂半部的handler的類型irq_handler_t定義為:
typedef irqreturn_t (*irq_handler_t)(int,void*);
typedef int irqreturn_t;
3.1.2. 釋放IRQ
有請求當然就有釋放。中斷的釋放函數為:
void free_irq(unsigned int irq,void *dev_id);
參數定義與request_irq類似。
3.1.3. 中斷的使能和屏蔽
void disable_irq(int irq);//等待目前中斷處理完成(最好別在頂板部使用,你懂得)
void disable_irq_nosync(int irq);//立即返回
void enable_irq(int irq);//
3.1.4. 屏蔽本CPU內所有中斷:
#define local_irq_save(flags)...//禁止中斷并保存狀態。
void local_irq_disable(void); //禁止中斷,不保存狀態。
下面來分別介紹一下頂半部和底半部的實現機制
3.1.5. 底半部機制:
簡介:底半部機制主要有tasklet、工作隊列和軟中斷
3.1.5.1. 底半部實現方法之一tasklet
(1) 我們需要定義tasklet機器處理器并將兩者關聯,例如:
void my_tasklet_func(unsigned long);/*定義一個處理函數*/
DECLARE_TASKLET(my_tasklet,my_tasklet_func,data);
/*上述代碼定義了名為my_tasklet的tasklet并將其與my_tasklet_func()函數綁定,傳入的參數為data*/
(2)調度
tasklet_schedule(&my_tasklet);
//使用此函數就能在是當的時候進行調度運行
(3)tasklet使用模板:
/*定義tasklet和底半部函數并關聯*/
void xxx_do_tasklet(unsigned long);
DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet,0);
/*中斷處理底半部*/
void xxx_do_tasklet(unsigned long)
{
...
}
/*中斷處理頂半部*/
irqreturn_t xxx_interrupt(int irq,void *dev_id)
{
...
tasklet_schedule(&xxx_tasklet);//調度地半部
...
}
/*設備驅動模塊加載函數*/
int __init xxx_init(void)
{
...
/*申請中斷*/
result = request_irq(xxx_irq,xxx_interrupt, IRQF_DISABLED,"xxx",NULL);
...
return IRQ_HANDLED;
}
/*設備驅動模塊卸載函數*/
void __exit xxx_exit(void)
{
...
/*釋放中斷*/
free_irq(xxx_irq,xxx_interrupt);
...
}
3.1.5.2. 底半部實現方法之二---工作隊列
使用方法和tasklet類似,相關操作:
struct work_struct my_wq;/*定義一個工作隊列*/
void my_wq_func(unsigned long);/*定義一個處理函數*/
通過INIT_WORK()可以初始化這個工作隊列并將工作隊列與處理函數綁定INIT_WORK(&my_wq,(void (*)(void *))my_wq_func,NULL);/*初始化工作隊列并將其與處理函數綁定*/
schedule_work(&my_wq);/*調度工作隊列執行*/
/*工作隊列使用模板*/
/*定義工作隊列和關聯函數*/
struct work_struct(unsigned long);
void xxx_do_work(unsigned long);
/*中斷處理底半部*/
void xxx_do_work(unsigned long)
{
...
}
/*中斷處理頂半部*/
irqreturn_t xxx_interrupt(int irq,void *dev_id)
{
...
schedule_work(&my_wq);//調度底半部
...
return IRQ_HANDLED;
}
/*設備驅動模塊加載函數*/
int xxx_init(void)
{
...
/*申請中斷*/
result = request_irq(xxx_irq,xxx_interrupt,IRQF_DISABLED,"xxx",NULL);
...
/*初始化工作隊列*/
INIT_WORK(&my_wq,(void (*)(void *))xxx_do_work,NULL);
}
/*設備驅動模塊卸載函數*/
void xxx_exit(void)
{
...
/*釋放中斷*/
free_irq(xxx_irq,xxx_interrupt);
...
}
4. 中斷共享
中斷共享是指多個設備共享一根中斷線的情況,中斷共享的使用方法:
(1).在申請中斷時,使用IRQF_SHARED標識;
(2).在中斷到來時,會遍歷共享此中斷的所有中斷處理程序,直到某一個函數返回IRQ_HANDLED,在中斷處理程序頂半部中,應迅速根據硬件寄存器中的信息參照dev_id參數判斷是否為本設備的中斷,若不是立即返回IR1_NONE
/*共享中斷編程模板*/
irqreturn_t xxx_interrupt(int irq,void *dev_id,struct pt_regs *regs)
{
...
int status = read_int_status();/*獲知中斷源*/
if(!is_myint(dev_id,status))/*判斷是否為本設備中斷*/
return IRQ_NONE;/*不是本設備中斷,立即返回*/
/*是本設備中斷,進行處理*/
...
return IRQ_HANDLED;/*返回IRQ_HANDLER表明中斷已經被處理*/
}
/*設備模塊加載函數*/
int xxx_init(void)
{
...
/*申請共享中斷*/
result = request_irq(sh_irq,xxx_interrupt,IRQF_SHARE,"xxx",xxx_dev);
...
}
/*設備驅動模塊卸載函數*/
void xxx_exit()
{
...
/*釋放中斷*/
free_irq(xxx_irq,xxx_interrupt);
...
}
5. 內核定時器
內核定時器編程:
簡介:軟件意義上的定時器最終是依賴于硬件定時器實現的,內核在時鐘中斷發生后檢測各定時器是否到期,到期后定時器處理函數作為軟中斷在底半部執行。
Linux內核定時器操作:
5.1. timer_list結構體
每一個timer_list對應一個定時器
struct timer_list{
struct list_head entry;/*定時器列表*/
unsigned long expires;/*定時器到期時間*/
void (*function)(unsigned long);/*定時器處理函數*/
unsigned long data;/*作為參數被傳遞給定時器處理函數*/
struct timer_base_s *base;
...
};
當定時器滿的時候,定時器處理函數將被執行
5.2. 初始化定時器
void init_timer(struct timer_list * timer);
//初始化timer_list的entry的next為NULL,并給base指針賦值。
TIMER_INITIALIZER(_function,_expires,_data);//此宏用來
//賦值定時器結構體的function、expires、data和base成員
#define TIMER_INITIALIZER(function,_expires,_data)
{
.entry = {.prev = TIMER_ENTRY_STATIC},\
.function= (_function), \
.expires = (_expire), \
.data = (_data), \
.base = &boot_tvec_bases,\
}
DEFINE_TIMER(_name,_function,_expires,_data)//定義一個定時器結構體變量//并為此變量取名_name
//還有一個setup_timer()函數也可以用于定時器結構體的初始化。
5.3. 增加定時器
void add_timer(struct timer_list * timer);//注冊內核定時器,也就是將定時器加入到內核動態定時器鏈表當中。
5.4. 刪除定時器
del_timer(struct timer_list *timer);
del_timer_sync()//在刪除一個定時器時等待刪除操作被處理完(不能用于中斷上下文中)
5.5. 修改定時器expires
int mod_timer(struct timer_list * timer,unsigned long expires);//修改定時器的到期時間
/*內核定時器使用模板*/
/*xxx設備結構體*/
struct xxx_dev
{
struct cdev cdev;
...
timer_list xxx_timer;/*設備要使用的定時器*/
};
/*xxx驅動中的某函數*/
xxx_funcl(...)
{
struct xxx_dev *dev = filp->private_data;
...
/*初始化定時器*/
init_timer(&dev->xxx_timer);
dev->xxx_timer.function = &xxx_do_timer;
dev->xxx_timer.data = (unsigned long)dev;
/*設備結構體指針作為定時器處理函數參數*/
dev->xxx_timer.expires = jiffes + delays;
/*添加(注冊)定時器*/
add_timer(&dev->xxx_timer);
...
}
/*xxx驅動中的某函數*/
xxx_func2(...)
{
...
/*刪除定時器*/
del_timer(&dev->xxx_timer);
...
}
/*定時器處理函數*/
static void xxx_do_timer(unsigned long arg)
{
struct xxx_device *dev = (struct xxx_device *)(arg);
...
/*調度定時器再執行*/
dev->xxx_timer.expires = jiffes + delay;
add_timer(&dev -> xxx_timer);
...
}
//定時器到期時間往往是在jiffies的基礎上添加一個時延,若為HZ則表示延遲一秒
5.6. 內核中的延遲工作:
簡介:對于這種周期性的工作,Linux提供了一套封裝好的快捷機制,本質上利用工作隊列和定時器實現,這其中用到兩個結構體:
(1)struct delayed_work
{
struct work_struct work;
struct timer_list timer;
};
(2) struct work_struct
{
atomic_long_t data;
...
}
相關操作:
int schedule_delay_work(struct delayed_work *work,unsigned long delay);//當指定的delay到來時delay_work中的work成員的work_func_t類型成員func()會被執行work_func_t類型定義如下:
typedef void (*work_func_t)(struct work_struct *work);//delay參數的單位是jiffes
mescs_to_jiffies(unsigned long mesc);//將毫秒轉化成jiffes單位
int cancel_delayed_work(struct delayed_work *work);
int cancel_delayed_work_sync(struct delayed_work *work);//等待直到刪除(不能用于中斷上下文)
內核延遲的相關函數:
短延遲:
Linux內核提供了如下三個函數分別進行納秒、微妙和毫秒延遲:
void ndelay(unsigned long nsecs);
void udelay(unsigned long usecs);
void mdelay(unsigned long msecs);
機制:根據CPU頻率進行一定次數的循環(忙等待)
注意:在Linux內核中最好不要使用毫秒級的延時,因為這樣會無謂消耗CPU的資源。
對于毫秒以上的延時,Linux提供如下函數:
void msleep(unsigned int millisecs);
unsigned long msleep_interruptible(unsigned int millisecs);//可以被打斷
void ssleep(unsigned int seconds);
//上述函數使得調用它的進程睡眠指定的時間
長延遲:
機制:設置當前jiffies加上時間間隔的jiffies,直到未來的jiffies達到目標jiffires
/*實例:先延遲100個jiffies再延遲2s*/
unsigned long delay = jiffies + 100;
while(time_before(jiffies,delay));
/*再延遲2s*/
unsigned long delay = jiffies + 2*Hz;
while(time_before(jiffies,delay));//循環直到到達指定的時間與timer_before()相對應的還有一個time_after
睡著延遲:
睡著延遲是比忙等待更好的一種方法
機制:在等待的時間到來之前進程處于睡眠狀態,CPU資源被其他進程使用,實現函數有:
schedule_timeout()
schedule_timeout_uninterruptible()
其實在短延遲中的msleep() msleep_interruptible()
本質上都是依賴于此函數實現的,下面兩個函數可以讓當前進程加入到等待隊列中,從而在等待隊列上睡眠,當超時發生時,進程被喚醒
sleep_on_timeout(wait_queue_head_t *q,unsigned long timeout);
interruptible_sleep_on_timeout(wait_queue_head_t *q,unsigned long timeout);