1. 程式人生 > 實用技巧 >Linux 驅動框架---驅動中的中斷

Linux 驅動框架---驅動中的中斷

在微控制器開發中中斷就是執行過程中發生了一些事件需要及時處理,所以需要停止當前正在執行的處理的事情轉而去執行中斷服務函式,已完成必要的事件的處理。在Linux中斷一樣是如此使用但是基於常見的中斷控制器的特性比如不支援中斷巢狀,當CPU在處理一箇中斷時是無法響應其他中斷的,所以就會導致整個系統的實時性就比較差,所以在Linux下的思路就是儘量簡短中斷上下文執行的指令數量,把一些必須在中斷上下文執行的程式碼放在中斷上下文中執行,而一些兒可以適當推遲的處理延遲處理。這就是Linux的中斷處理程式的機制,將中斷處理分為上半段(頂半部)和下半段(低半部)。和硬體有關的實時性要求比較高的的處理過程在頂半部執行處理,而其餘的放到低半部處理。低半部的實現機制由有許多種如軟中斷,tasklet、workqueue等。這裡可以參考我的另幾篇

部落格

Linux中斷程式設計

這裡不考慮Linux中斷子系統的初始化等相關的內容,只瞭解具體的驅動程式設計需要的基本內容,驅動中使用中斷需要申請並指定處理介面,然後就是完成指定的處理介面函式,最重要的是實現低半部機制,而好訊息是現在Linux核心已經將中斷在大多數情況下執行緒化了,所以低半部很少使用但是不是不使用。

申請中斷

int request_irq(int irq,irq_handler handler,unsigned long flags,const char * name,void *dev);

其中irq為中斷號,這裡是Linux子系統的中斷號;

handler為中斷處理介面函式。

flags為中斷的標誌常有如下

//指定中斷觸發型別:邊沿和高低電平
#define IRQF_TRIGGER_NONE    0x00000000
#define IRQF_TRIGGER_RISING 0x00000001
#define IRQF_TRIGGER_FALLING 0x00000002
#define IRQF_TRIGGER_HIGH 0x00000004    高可用有效。新增加的標誌
#define IRQF_TRIGGER_LOW 0x00000008
#define IRQF_TRIGGER_MASK(IRQF_TRIGGER_HIGH | IRQF_TRIGGER_LOW | \
        IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING)
#define IRQF_TRIGGER_PROBE 0x00000010 /* *這些標誌僅由核心用作irq處理例程的一部分。 *首先考慮在共享中斷中註冊 *效能原因) */ #define IRQF_DISABLED 0x00000020 // IRQF_DISABLED-呼叫動作處理程式時保持禁用irqs #define IRQF_SAMPLE_RANDOM 0x00000040 // IRQF_SAMPLE_RANDOM-irq用於提供隨機生成器 #define IRQF_SHARED 0x00000080 // IRQF_SHARED-允許在多個裝置之間共享irq #define IRQF_PROBE_SHARED 0x00000100 // IRQF_PROBE_SHARED-由呼叫者在期望發生共享不匹配時設定 #define IRQF_TIMER 0x00000200 // IRQF_TIMER-將該中斷標記為定時器中斷的標誌 #define IRQF_PERCPU 0x00000400 // IRQF_PERCPU-中斷是每個CPU #define IRQF_NOBALANCING 0x00000800 // IRQF_NOBALANCING-將該中斷從irq平衡中排除的標誌 #define IRQF_IRQPOLL 0x00001000 // IRQF_IRQPOLL-中斷用於輪詢(僅中斷

name為建立檔案系統/proc/irq下的檔案介面需要。dev在共享中斷時必須從而區別處理不同的裝置中斷。這個函式執行成功返回0,其餘則是失敗。這種中斷的申請在中斷解除安裝或釋放時是需要釋放的釋放的介面就是free_irq():

void free_irq(unsigned int irq,void * dev_id);

irq為Linux中斷子系統的中斷號。

dev_id在共享中斷時常為裝置描述。

除此之外還有一個自動釋放中斷的介面就是linux核心常見的devm_xxx介面,第一個引數就是關聯的device。

int devm_request_irq(struct device* dev,unsigned int irq ,irq_hander_t handler,unsigned long irqflags,const char* name ,void * dev_id)

使能和遮蔽中斷

有時候需要臨時關閉一箇中斷,一個CPU上的中斷甚至整個系統的中斷Linux提供了這些API如下:

//使能或關閉某一箇中斷
void disable_irq(unsigned int irq);
void disable_irq_nosync(unsigned int irq);
void enable_irq(unsigned int irq);

其中disable_irq_nosync介面區別於disable_irq的是前者直接返回,而後者會等待當前的中斷處理完成,所以也就意味著disable_irq介面會導致休眠阻塞所以不能在中斷上下文中呼叫。除此之外核心還提供了關閉和使能指定CPU上的中斷的介面這些介面的實現依賴於具體的硬體:

//關閉呼叫介面的當前CPU上的所有中斷
local_irq_disable()
//使能呼叫介面的當前CPU上的所有中斷
local_irq_enable()    

如果需要關閉前儲存中斷控制暫存器的狀態在開啟中斷時恢復暫存器的配置則可以使用如下的介面:

//關閉並儲存暫存器狀態
local_irq_save(flags)
//開啟並恢復暫存器狀態
loacl_irq_restore(flags)

實時性提高--中斷底半部

  為了改善Linux核心的實時性核心將中斷的相應分出了頂半部和底半部,低半部的實現機制主要有tasklet,工作佇列,軟中斷和執行緒化。執行緒化是現在心核心支援的,在中斷申請註冊的過程中如果沒有強制不能執行緒化或沒有給兩個中斷服務介面(一個非執行緒化一個執行緒化)核心會預設將註冊的非執行緒化的介面進行執行緒化。具體參考Linux核心實現透視---硬中斷。其次是軟中斷這是一個依賴核心執行緒和核心資料結構的子系統,軟體介面主要是完成核心軟中斷資料結構的組織進而使軟中斷核心執行緒可以即使的處理需要處理呼叫的介面函式。tasklet是一種依賴軟中斷實現的低半部機制,他在軟中斷中劃分了一部分執行緒專門用於處理tasklet資料結構中待處理的介面(通過API介面加入的),所以他和軟中斷的特性一樣會併發的執行在不同的CPU上且執行在軟中斷上下文具體參考Linux核心實現透視---軟中斷&Tasklet,最後是工作佇列這是Linux核心為中斷低半部設計的另一機制,他的特點是對資源的消耗更加的少並且work只會線性的序列執行而不會併發具體參考Linux核心實現透視---工作佇列。接下來簡單的學習一下核心驅動開發常用的低半部機制的API介面其中軟中斷很少驅動直接使用:

tasklet

使用比較簡單,基本就是定義好處理函式後定義tasklet並初始化將處理函式繫結到定義的tasklet上後就需要通過介面進行排程,之後就等待核心排程軟中斷處理執行緒來執行了。

//資料型別
struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};

//tasklet 處理介面的型別
void tasklet_func(unsigned long);
//定義並且初始化
DECLARE_TASKLET(name,function,data)
//排程 實際上將tasklet 物件加入核心資料鏈表由核心決定執行時機
tasklet_schedule(struct tasklet_struct*  )

工作佇列

工作佇列的使用分兩種情況一種是使用核心定義工作佇列,還有一種是自己定義工作佇列,兩種使用方式要根據使用的具體情況決定使用哪一種。基本常用的是直接使用系統內定義的預設共享工作佇列,因為通常情況下這些內建的工作佇列和處理執行緒已經足夠可以及時處理這些需求,除非是有特別多的工作會加入到工作佇列時,其他一般不需要自己定義工作佇列。下面就是使用預設工作佇列的常用API。

//定義工作
struct work_struct  xxx_work;
//定義服務介面函式
void xxx_func(struct work_struct* work);
//初始化工作佇列 其實就是繫結處理函式介面
INIT_WORK(struct work_queue* wq,work_func);
//將工作加入到工作佇列等待排程
schedule_workA(struct work_struct * work);
//確保沒有工作佇列入口在系統中任何地方執行,會等待所有在處理的處理完成
void flush_scheduled_work(void);
//延時一定時間後再排程執行工作
int schedule_delayed_work(struct delayed_struct *work, unsigned long delay);
//取消加入工作佇列的工作
int cancel_delayed_work(struct delayed_struct *work);

細心點會發現取消的介面只放了延時工作佇列的,因為非延時的工作佇列通常在執行了排程請求後很快就會執行了,此時取消常常是用處不大的。當然如果發起排程和取消排程都在中斷上下文肯定就是可以的,具體的原因如果理解不了可以看前面的Linux核心實現透視---工作佇列這一篇。最後再來看一下自定義工作佇列的使用方法因為使用核心提供的共享列隊,列隊是保持順序執行的,做完一個工作才做下一個,如果一個工作內有耗時大的處理如阻塞等待訊號或鎖,那麼後面的工作都不會執行。如果確保工作佇列及時執行,那麼可以建立自己的工作者執行緒,所有工作可以加入自己建立的工作列隊,列隊中的工作執行在建立的工作者執行緒中。建立工作列隊使用3個巨集 成功後返回workqueue_struct *指標,並建立了工作者執行緒。三個巨集主要區別在後面兩個引數singlethread和freezeable,singlethread為0時會為每個cpu上建立一個工作者執行緒,為1時只在當前執行的cpu上建立一個工作者執行緒。freezeable會影響核心執行緒結構體thread_info的PF_NOFREEZE標記見程式碼片段;如果設定了PF_NOFREEZE這個flag,那麼系統掛起時候這個程序不會被掛起。

if (!cwq->freezeable)
        current->flags |= PF_NOFREEZE;
    set_user_nice(current, -5);

介面

//多處理器時會為每個cpu建立一個工作者執行緒    
#define create_workqueue(name) __create_workqueue((name), 0, 0)
//只建立一個工作者執行緒,系統掛起是執行緒也掛起                                    
#define create_freezeable_workqueue(name) __create_workqueue((name), 1, 1)     
//只建立一個工作者執行緒,系統掛起是執行緒執行緒不掛起
#define create_singlethread_workqueue(name) __create_workqueue((name), 1, 0)    
以上三個巨集呼叫__create_workqueue函式定義

extern struct workqueue_struct *__create_workqueue(const char *name,int singlethread, int freezeable);
 
//釋放建立的工作列隊資源
void destroy_workqueue(struct workqueue_struct *wq)
 
//延時呼叫指定工作列隊的工作
queue_delayed_work(struct workqueue_struct *wq,struct delay_struct *work, unsigned long delay)
 
//取消指定工作列隊的延時工作
cancel_delayed_work(struct delay_struct *work)
 
//將工作加入指定工作列隊進行排程
queue_work(struct workqueue_struct *wq, struct work_struct *work)
 
//等待列隊中的任務全部執行完畢。
void flush_workqueue(struct workqueue_struct *wq);

Linux核心的中斷開發使用到這裡基本常用的介面函式都覆蓋到了,具體的實現機制單獨有時間的時候在深入學習研究。到時候放到核心實現透視章節記錄。

參考:

宋寶華:Linux裝置驅動開發詳解

BLOG:https://blog.csdn.net/fontlose/article/details/8286445