1. 程式人生 > >linux 中斷機制的處理過程

linux 中斷機制的處理過程

轉自https://blog.csdn.net/fan0520/article/details/52153836

 

一、中斷的概念
中斷是指在CPU正常執行期間,由於內外部事件或由程式預先安排的事件引起的CPU暫時停止正在執行的程式,轉而為該內部或外部事件或預先安排的事件服務的程式中去,服務完畢後再返回去繼續執行被暫時中斷的程式。Linux中通常分為外部中斷(又叫硬體中斷)和內部中斷(又叫異常)。

在實地址模式中,CPU把記憶體中從0開始的1KB空間作為一箇中斷向量表。表中的每一項佔4個位元組。但是在保護模式中,有這4個位元組的表項構成的中斷向量表不滿足實際需求,於是根據反映模式切換的資訊和偏移量的足夠使得中斷向量表的表項由8個位元組組成,而中斷向量表也叫做了中斷描述符表(IDT)。在CPU中增加了一個用來描述中斷描述符表暫存器(IDTR),用來儲存中斷描述符表的起始地址。


二、中斷的請求過程


外部裝置當需要作業系統做相關的事情的時候,會產生相應的中斷。裝置通過相應的中斷線向中斷控制器傳送高電平以產生中斷訊號,而作業系統則會從中斷控制器的狀態位取得那根中斷線上產生的中斷。而且只有在裝置在對某一條中斷線擁有控制權,才可以向這條中斷線上傳送訊號。也由於現在的外設越來越多,中斷線又是很寶貴的資源不可能被一一對應。因此在使用中斷線前,就得對相應的中斷線進行申請。無論採用共享中斷方式還是獨佔一箇中斷,申請過程都是先講所有的中斷線進行掃描,得出哪些沒有別佔用,從其中選擇一個作為該裝置的IRQ。其次,通過中斷申請函式申請相應的IRQ。最後,根據申請結果檢視中斷是否能夠被執行。

中斷機制的核心資料結構是 irq_desc, 它完整地描述了一條中斷線 (或稱為 “中斷通道” )。以下程式原始碼版本為linux-2.6.32.2。

其中irq_desc 結構在 include/linux/irq.h 中定義:


typedef    void (*irq_flow_handler_t)(unsigned int irq, struct irq_desc *desc);


struct irq_desc {


    unsigned int      irq;    


    struct timer_rand_state *timer_rand_state;


    unsigned int            *kstat_irqs;


#ifdef CONFIG_INTR_REMAP


    struct irq_2_iommu      *irq_2_iommu;


#endif


    irq_flow_handler_t   handle_irq; /* 高層次的中斷事件處理函式 */


    struct irq_chip      *chip; /* 低層次的硬體操作 */


    struct msi_desc      *msi_desc;


    void          *handler_data; /* chip 方法使用的資料*/


    void          *chip_data; /* chip 私有資料 */


    struct irqaction  *action;   /* 行為連結串列(action list) */


    unsigned int      status;       /* 狀態 */


    unsigned int      depth;     /* 關中斷次數 */


    unsigned int      wake_depth;   /* 喚醒次數 */


    unsigned int      irq_count; /* 發生的中斷次數 */


    unsigned long     last_unhandled;   /*滯留時間 */


    unsigned int      irqs_unhandled;


    spinlock_t    lock; /*自選鎖*/


#ifdef CONFIG_SMP


    cpumask_var_t     affinity;


    unsigned int      node;


#ifdef CONFIG_GENERIC_PENDING_IRQ


    cpumask_var_t     pending_mask;


#endif


#endif


    atomic_t      threads_active;


    wait_queue_head_t   wait_for_threads;


#ifdef CONFIG_PROC_FS


    struct proc_dir_entry    *dir; /* 在 proc 檔案系統中的目錄 */


#endif


    const char    *name;/*名稱*/


} ____cacheline_internodealigned_in_smp;


I、Linux中斷的申請與釋放:在<linux/interrupt.h>, , 實現中斷申請介面:

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

函式引數說明

unsigned int irq:所要申請的硬體中斷號

irq_handler_t handler:中斷服務程式的入口地址,中斷髮生時,系統呼叫handler這個函式。irq_handler_t為自定義型別,其原型為:

typedef irqreturn_t (*irq_handler_t)(int, void *);

而irqreturn_t的原型為:typedef enum irqreturn irqreturn_t;

enum irqreturn {

    IRQ_NONE,/*此裝置沒有產生中斷*/


    IRQ_HANDLED,/*中斷被處理*/


    IRQ_WAKE_THREAD,/*喚醒中斷*/


};

在列舉型別irqreturn定義在include/linux/irqreturn.h檔案中。

unsigned long flags:中斷處理的屬性,與中斷管理有關的位掩碼選項,有一下幾組值:

#define IRQF_DISABLED       0x00000020    /*中斷禁止*/

#define IRQF_SAMPLE_RANDOM  0x00000040    /*供系統產生隨機數使用*/

#define IRQF_SHARED      0x00000080 /*在裝置之間可共享*/

#define IRQF_PROBE_SHARED   0x00000100/*探測共享中斷*/

#define IRQF_TIMER       0x00000200/*專用於時鐘中斷*/

#define IRQF_PERCPU      0x00000400/*每CPU週期執行中斷*/

#define IRQF_NOBALANCING 0x00000800/*復位中斷*/

#define IRQF_IRQPOLL     0x00001000/*共享中斷中根據註冊時間判斷*/

#define IRQF_ONESHOT     0x00002000/*硬體中斷處理完後觸發*/

#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/*觸發式檢測中斷*/

const char *dev_name:裝置描述,表示那一個裝置在使用這個中斷。

void *dev_id:用作共享中斷線的指標.。一般設定為這個裝置的裝置結構體或者NULL。它是一個獨特的標識, 用在當釋放中斷線時以及可能還被驅動用來指向它自己的私有資料區,來標識哪個裝置在中斷 。這個引數在真正的驅動程式中一般是指向裝置資料結構的指標.在呼叫中斷處理程式的時候它就會傳遞給中斷處理程式的void *dev_id。如果中斷沒有被共享, dev_id 可以設定為 NULL。

II、釋放IRQ

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


III、中斷線共享的資料結構
   struct irqaction {
    irq_handler_t handler; /* 具體的中斷處理程式 */
    unsigned long flags;/*中斷處理屬性*/
    const char *name; /* 名稱,會顯示在/proc/interreupts 中 */
    void *dev_id; /* 裝置ID,用於區分共享一條中斷線的多個處理程式 */
    struct irqaction *next; /* 指向下一個irq_action 結構 */
    int irq;  /* 中斷通道號 */
    struct proc_dir_entry *dir; /* 指向proc/irq/NN/name 的入口*/
    irq_handler_t thread_fn;/*執行緒中斷處理函式*/
    struct task_struct *thread;/*執行緒中斷指標*/
    unsigned long thread_flags;/*與執行緒有關的中斷標記屬性*/
};


thread_flags參見列舉型
enum {
    IRQTF_RUNTHREAD,/*執行緒中斷處理*/
    IRQTF_DIED,/*執行緒中斷死亡*/
    IRQTF_WARNED,/*警告資訊*/
    IRQTF_AFFINITY,/*調整執行緒中斷的關係*/
};


多箇中斷處理程式可以共享同一條中斷線,irqaction 結構中的 next 成員用來把共享同一條中斷線的所有中斷處理程式組成一個單向連結串列,dev_id 成員用於區分各個中斷處理程式。

根據以上內容可以得出中斷機制各個資料結構之間的聯絡如下圖所示:

linux 中斷機制的處理過程 

 

 三.中斷的處理過程
Linux中斷分為兩個半部:上半部(tophalf)和下半部(bottom half)。上半部的功能是"登記中斷",當一箇中斷髮生時,它進行相應地硬體讀寫後就把中斷例程的下半部掛到該裝置的下半部執行佇列中去。因此,上半部執行的速度就會很快,可以服務更多的中斷請求。但是,僅有"登記中斷"是遠遠不夠的,因為中斷的事件可能很複雜。因此,Linux引入了一個下半部,來完成中斷事件的絕大多數使命。下半部和上半部最大的不同是下半部是可中斷的,而上半部是不可中斷的,下半部幾乎做了中斷處理程式所有的事情,而且可以被新的中斷打斷!下半部則相對來說並不是非常緊急的,通常還是比較耗時的,因此由系統自行安排執行時機,不在中斷服務上下文中執行。


中斷號的檢視可以使用下面的命令:“cat /proc/interrupts”。
Linux實現下半部的機制主要有tasklet和工作佇列。
小任務tasklet的實現
其資料結構為struct tasklet_struct,每一個結構體代表一個獨立的小任務,定義如下
struct tasklet_struct
{
    struct tasklet_struct *next;/*指向下一個連結串列結構*/

    unsigned long state;/*小任務狀態*/

    atomic_t count;/*引用計數器*/

    void (*func)(unsigned long);/*小任務的處理函式*/

    unsigned long data;/*傳遞小任務函式的引數*/
};


state的取值參照下邊的列舉型:

enum
{
    TASKLET_STATE_SCHED,    /* 小任務已被呼叫執行*/
    TASKLET_STATE_RUN   /*僅在多處理器上使用*/
};


count域是小任務的引用計數器。只有當它的值為0的時候才能被啟用,並其被設定為掛起狀態時,才能夠被執行,否則為禁止狀態。


I、宣告和使用小任務tasklet
靜態的建立一個小任務的巨集有一下兩個:

#define DECLARE_TASKLET(name, func, data)  \

struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }

#define DECLARE_TASKLET_DISABLED(name, func, data) \

struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }

這兩個巨集的區別在於計數器設定的初始值不同,前者可以看出為0,後者為1。為0的表示啟用狀態,為1的表示禁止狀態。其中ATOMIC_INIT巨集為:

#define ATOMIC_INIT(i)   { (i) }

即可看出就是設定的數字。此巨集在include/asm-generic/atomic.h中定義。這樣就建立了一個名為name的小任務,其處理函式為func。當該函式被呼叫的時候,data引數就被傳遞給它。

II、小任務處理函式程式
    處理函式的的形式為:void my_tasklet_func(unsigned long data)。這樣DECLARE_TASKLET(my_tasklet, my_tasklet_func, data)實現了小任務名和處理函式的繫結,而data就是函式引數。


III、排程編寫的tasklet
排程小任務時引用tasklet_schedule(&my_tasklet)函式就能使系統在合適的時候進行排程。函式原型為:

static inline void tasklet_schedule(struct tasklet_struct *t)
{
    if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
       __tasklet_schedule(t);
}

這個排程函式放在中斷處理的上半部處理函式中,這樣中斷申請的時候呼叫處理函式(即irq_handler_t handler)後,轉去執行下半部的小任務。

如果希望使用DECLARE_TASKLET_DISABLED(name,function,data)建立小任務,那麼在啟用的時候也得呼叫相應的函式被使能

tasklet_enable(struct tasklet_struct *); //使能tasklet

tasklet_disble(struct tasklet_struct *); //禁用tasklet

tasklet_init(struct tasklet_struct *,void (*func)(unsigned long),unsigned long);

當然也可以呼叫tasklet_kill(struct tasklet_struct *)從掛起佇列中刪除一個小任務。清除指定tasklet的可排程位,即不允許排程該tasklet 。

使用tasklet作為下半部的處理中斷的裝置驅動程式模板如下:


/*定義tasklet和下半部函式並關聯*/
void my_do_tasklet(unsigned long);

DECLARE_TASKLET(my_tasklet, my_tasklet_func, 0);

/*中斷處理下半部*/
void my_do_tasklet(unsigned long)
{

  ……/*編寫自己的處理事件內容*/

}


/*中斷處理上半部*/
irpreturn_t my_interrupt(unsigned int irq,void *dev_id)
{
 ……
 tasklet_schedule(&my_tasklet)/*排程my_tasklet函式,根據宣告將去執行my_tasklet_func函式*/


 ……
}


/*裝置驅動的載入函式*/
int __init xxx_init(void)
{
 ……
 /*申請中斷, 轉去執行my_interrupt函式並傳入引數*/
 result=request_irq(my_irq,my_interrupt,IRQF_DISABLED,"xxx",NULL);

 ……

}


/*裝置驅動模組的解除安裝函式*/
void __exit xxx_exit(void)
{

……


/*釋放中斷*/
free_irq(my_irq,my_interrupt);


……


}


工作佇列的實現


工作佇列work_struct結構體,位於/include/linux/workqueue.h

typedef void (*work_func_t)(struct work_struct *work);

struct work_struct {

      atomic_long_t data; /*傳遞給處理函式的引數*/


#define WORK_STRUCT_PENDING 0/*這個工作是否正在等待處理標誌*/             

#define WORK_STRUCT_FLAG_MASK (3UL)

#define WORK_STRUCT_WQ_DATA_MASK (~WORK_STRUCT_FLAG_MASK)

      struct list_head entry;  /* 連線所有工作的連結串列*/

      work_func_t func; /* 要執行的函式*/

#ifdef CONFIG_LOCKDEP
      struct lockdep_map lockdep_map;
#endif

};

這些結構被連線成連結串列。當一個工作者執行緒被喚醒時,它會執行它的連結串列上的所有工作。工作被執行完畢,它就將相應的work_struct物件從連結串列上移去。當連結串列上不再有物件的時候,它就會繼續休眠。可以通過DECLARE_WORK在編譯時靜態地建立該結構,以完成推後的工作。


#define DECLARE_WORK(n, f)                                 \

      struct work_struct n = __WORK_INITIALIZER(n, f)

而後邊這個巨集為一下內容:

#define __WORK_INITIALIZER(n, f) {                      \
      .data = WORK_DATA_INIT(),                            \
      .entry      = { &(n).entry, &(n).entry },                    \
      .func = (f),                                        \
      __WORK_INIT_LOCKDEP_MAP(#n, &(n))                   \
      }


其為引數data賦值的巨集定義為:

#define WORK_DATA_INIT()       ATOMIC_LONG_INIT(0)

這樣就會靜態地建立一個名為n,待執行函式為f,引數為data的work_struct結構。同樣,也可以在執行時通過指標建立一個工作:

INIT_WORK(struct work_struct *work, void(*func) (void *));

這會動態地初始化一個由work指向的工作佇列,並將其與處理函式繫結。巨集原型為:

#define INIT_WORK(_work, _func)                                        \

      do {                                                        \


             static struct lock_class_key __key;                 \

                                                              \

             (_work)->data = (atomic_long_t) WORK_DATA_INIT();  \

             lockdep_init_map(&(_work)->lockdep_map, #_work, &__key, 0);\

             INIT_LIST_HEAD(&(_work)->entry);                 \

             PREPARE_WORK((_work), (_func));                         \

      } while (0)

在需要排程的時候引用類似tasklet_schedule()函式的相應排程工作佇列執行的函式schedule_work(),如:

schedule_work(&work);/*排程工作佇列執行*/

如果有時候並不希望工作馬上就被執行,而是希望它經過一段延遲以後再執行。在這種情況下,可以排程指定的時間後執行函式:

schedule_delayed_work(&work,delay);函式原型為:

int schedule_delayed_work(struct delayed_work *work, unsigned long delay);

其中是以delayed_work為結構體的指標,而這個結構體的定義是在work_struct結構體的基礎上增加了一項timer_list結構體。

struct delayed_work {
    struct work_struct work;
    struct timer_list timer; /* 延遲的工作佇列所用到的定時器,當不需要延遲時初始化為NULL*/
};


這樣,便使預設的工作佇列直到delay指定的時鐘節拍用完以後才會執行。
使用工作佇列處理中斷下半部的裝置驅動程式模板如下:


/*定義工作佇列和下半部函式並關聯*/
struct work_struct my_wq;
void my_do_work(unsigned long);

/*中斷處理下半部*/

void my_do_work(unsigned long)
{

  ……/*編寫自己的處理事件內容*/

}


/*中斷處理上半部*/
irpreturn_t my_interrupt(unsigned int irq,void *dev_id)
{

 ……

 schedule_work(&my_wq)/*排程my_wq函式,根據工作佇列初始化函式將去執行my_do_work函式*/

 ……

}


/*裝置驅動的載入函式*/
int __init xxx_init(void)
{

 ……

 /*申請中斷,轉去執行my_interrupt函式並傳入引數*/
 result=request_irq(my_irq,my_interrupt,IRQF_DISABLED,"xxx",NULL);


 ……

 /*初始化工作佇列函式,並與自定義處理函式關聯*/


 INIT_WORK(&my_irq,(void (*)(void *))my_do_work);


 ……

}


/*裝置驅動模組的解除安裝函式*/
void __exit xxx_exit(void)
{


……


/*釋放中斷*/


free_irq(my_irq,my_interrupt);


……

}