天天看點

中斷機制與核心定時器

一、中斷的概念

中斷是指在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;

    void          *chip_data;

    struct irqaction  *action;  

    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;

#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

#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。由于現在的外設越來越多,中斷線又是很寶貴的資源不可能被一一對應,是以中斷線會被共享。dev_id是唯一的,用來辨別唯一的外設裝置,以及可能還被驅動用來指向它自己的私有資料區,來辨別哪個裝置在中斷 。這個參數在真正的驅動程式中一般是指向裝置資料結構的指針.在調用中斷處理程式的時候它就會傳遞給中斷處理程式的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;

    void *dev_id;

    struct irqaction *next;

    int irq; 

    struct proc_dir_entry *dir;

    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中斷分為兩個半部:頂半部(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作為下半部的進行中斷的裝置驅動程式模闆如下:

void my_do_tasklet(unsigned long);

DECLARE_TASKLET(my_tasklet, my_do_tasklet, 0);

void my_do_tasklet(unsigned long)

{

  ……

}

irpreturn_t my_interrupt(unsigned int irq,void *dev_id)

{

 ……

 tasklet_schedule(&my_tasklet)

 ……

}

int __init xxx_init(void)

{

 ……

 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;

};

這樣,便使預設的工作隊列直到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)

 ……

}

int __init xxx_init(void)

{

 ……

 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);

……

}

記住在中斷上下文中不能使用信号量 down()等函數,down()擷取不到信号量會進入休眠狀态,影響程式結果!!!

Linux核心定時器與使用方法

一.度量時間差

時鐘中斷是由系統的定時硬體以周期性的時間間隔産生,這個間隔(即頻率)由核心根據HZ來确定,HZ是一個與體系結構無關的常量(定義在),可配置(50-1200),在X86平台,預設值為1000.HZ的含義是系統每秒鐘産生的時鐘中斷的次數.可以說  HZ=1s。

每當時鐘中斷發生時,全局變量jiffies(一個32位的unsigned long變量,定義在)就加1,是以jiffies記錄了字linux系統啟動後時鐘中斷發生的次數.驅動程式常利用jiffies來計算不同僚件間的時間間隔.

2.6核心的時鐘中斷頻率是1000,也就是說,在1秒裡jiffies會被增加1000。是以jiffies + 2 * HZ表示推後2秒鐘。

在定時器應用中經常需要比較兩個時間值,以确定timer是否逾時,是以Linux核心在timer.h頭檔案中定義了4個時間關系比較操作宏。這裡我們說時刻a在時刻b之後,就意味着時間值a≥b。Linux強烈推薦使用者使用它所定義的下列4個時間比較操作宏(include/linux/timer.h):

#include

int time_after(unsigned long a, unsigned long b);

int time_before(unsigned long a, unsigned long b);

int time_after_eq(unsigned long a, unsigned long b);

int time_after_eq(unsigned long a, unsigned long b);

擷取目前時間:

#include

struct timeval {

time_t tv_sec;

suseconds_t tv_usec;

};

void do_gettimeofday(struct timeval *tv)

二.核心定時器

核心定時器用于控制某個函數(定時器處理函數)在未來的某個特定時間執行.核心定時器注冊的處理函數隻執行一次.處理過後即失效.

當核心定時器被排程運作時,幾乎可以肯定其不會在注冊這些函數的程序正在執行時.相反,它會異步的執行.這種異步類似于硬體中斷發生時的情景.實際上,核心定時器是被"軟體中斷"排程運作的.是以,其運作于原子上下文中.這點和tasklet很類似.處于原子上下文的程序有一些運作時的限制:

1. 不能通路使用者空間.因為沒有程序上下文.無法與特定的程序與使用者關聯

2. 不能執行排程或休眠.

3. Current指針在原子模式下無意義.

核心定時器被組織成雙向連結清單,使用struct timer_list結構描述.

struct time_list{

unsigned long expires; //逾時的jiffies值

void(*function)(unsigned long) ; //注冊的定時器處理函數

unsigned long data; //定時器處理函數的參數

}

這3個字段表示,當jiffies等于expires時,核心會排程function函數運作.data是傳遞給function的參數的指針,如果function函數需要不止一個參數,那麼可以将這幾個參數組成一個結構體,并将結構體的指針指派給data.

三.管理定時器的接口

void init_timer(struct time_list *timer);

初始化定時器隊列結構.timer_list結構在使用前必須初始化,這是要保證結構體中其他的成員能正确指派.

void add_timer(struct time_list *timer);

啟動定時器.

int del_timer(struct time_list *timer);

在定時器逾時前将定時器删除.當定時器逾時後,系統會自動将其删除.

四.核心定時器的使用方法

#include // for timer_list API

#include // for HZ

#include // for jiffies

struct timer_list my_timer;

init_timer(&my_timer);

my_timer.expires = jiffies + delay_sec*HZ; //延時delay_sec秒

my_timer.data = **; //給定時器處理函數傳入的參數

my_timer.function = my_function; //定時器逾時時調用的函數

add_timer(&my_timer);

mod_timer(&my_timer);

result = del_timer(&my_timer);

當删除定時器時,必須小心一個潛在的競争條件。當del_timer()傳回後,可以保證的隻是:定時器不會再被激活(也就是,将來不會執行),但是在多處理機器上定時器中斷可能已經在其他處理器上運作了,是以删除定時器時需要等待可能在其他處理器上運作的定時器處理程式都退出,這時就要使用del_timer_sync()函數執行删除工作:

del_timer_sync(&my_timer);

和del_timer()函數不同,del_timer_sync()函數不能在中斷上下文中使用.

五.短延時

當一個裝置驅動需要處理它的硬體的反應時間, 涉及到的延時常常是最多幾個毫秒.

核心函數 ndelay, udelay, 以及 mdelay 對于短延時好用, 分别延後執行指定的納秒數, 微秒數或者毫秒數. 它們的原型是:

#include void ndelay(unsigned long nsecs); void udelay(unsigned long usecs); void mdelay(unsigned long msecs);

有另一個方法獲得毫秒(和更長)延時而不用涉及到忙等待. 檔案 聲明這些函數:

void msleep(unsigned int millisecs); unsigned long msleep_interruptible(unsigned int millisecs); void ssleep(unsigned int seconds)

前 2 個函數使調用程序進入睡眠給定的毫秒數. 一個對 msleep 的調用是不可中斷的; 你能確定程序睡眠至少給定的毫秒數. 如果你的驅動位于一個等待隊列并且你想喚醒來打斷睡眠, 使用 msleep_interruptible. 從 msleep_interruptible 的傳回值正常地是 0; 如果, 但是, 這個程序被提早喚醒, 傳回值是在初始請求睡眠周期中剩餘的毫秒數. 對 ssleep 的調用使程序進入一個不可中斷的睡眠給定的秒數.

繼續閱讀