天天看點

Linux裝置驅動程式之中斷與時鐘

Linux裝置驅動程式之中斷與時鐘(一)   http://www.cnblogs.com/hanyan225/archive/2010/10/24/1859796.html

“小王,醒醒,開始上課了,今天咱們開始講中斷,這可是進階東西,錯過不補哈”我使勁推着睡夢中的小王。

“嗯?感情好啊,快點,快點”小王一聽有新東西講,像打了雞血似的興奮,連我都懷疑起她是不是性格中喜新厭舊。

不管那麼多了,我講我的,她厭她的…

  啥叫中斷?就是指cpu在執行過程中,出現了某些突發事件時CPU必須暫停執行目前的程式,轉去處理突發事件,處理完畢後CPU有傳回原程式被中斷的位置并繼續執行。

  中斷的分法不懂,分類就不同,向什麼内外部中斷,可/不可屏蔽中斷…等等亂七八糟一大堆,我這裡要說明的一點是按照中斷入口跳轉方法的不同,可分為向量中斷和非向量中斷。采用向量中斷的CPU通常為不同的中斷配置設定不同的中斷号,當檢測到某中斷号的中斷到來後,就自動跳轉到與該中斷号對應的位址執行。不同的中斷号有不同的中斷位址(即入口)。而非向量中斷的多個中斷共享一個入口位址。進入後根據軟體判斷中斷标志來識别具體是哪個中斷。也就是說,向量中斷是由硬體提供中斷服務程式入口位址,非向量中斷由軟體提供中斷服務程式入口位址。

  我們在後邊會說到一個時鐘定時器,它也是通過中斷來實作的。它的原理很簡單,嵌入式微處理器它接入一個時鐘輸入,當時鐘脈沖到來時,就将目前的計數器值加1并和預先設定的計數值比較,若相等,證明計數周期滿,産生定時器中斷并複位目前計數器值。

Linux裝置驅動程式之中斷與時鐘

Linux中斷處理架構

裝置的中斷會打斷核心中程序的正常排程和運作,會影響系統的性能。為了在中斷執行時間盡可能短和中斷處理需完成大量工作之間找到一個平衡點,Linux将中斷處理程式分解成兩個半部:頂半部和底半部。其中頂半部盡可能完成盡可能少的比較緊急的功能。而底半部幾乎做了中斷處理程式所有的事情,而且可以被新的中斷打斷。

在linux裝置驅動中,提供了一系列函數來幫助裝置實作中斷的相關操作:

1)裝置申請中斷

int request_irq(unsigned int irq,  //irq是要申請的中斷号

                    void (*handler)(int irq, void *dev_id, struct pt_regs * *regs),//回調函數,中斷發生時,系統會調用該函數,

                    unsigned long irqflags,

                    const char *devname,

                    void *dev_id);

其中irqflags是中斷處理的屬性,若設定為SA_INTERRUPT,則表示中斷處理程式是快速處理程式,它被調用時屏蔽所有中斷。若設定為SA_SHIRQ,則表示多個裝置共享中斷,dev_id在中斷共享時會用到,一般設定為這個裝置的裝置結構體或者NULL.

該函數傳回0表示成功,傳回-INVAL表示中斷号無效或處理函數指針為NULL,傳回EBUSY表示中斷已經被占用且不能共享。

2)釋放中斷

free_irq(unsigned int irq, void *dev_id);

3)使能和屏蔽中斷

void disable_irq(int irq);   //這個會立即傳回

void disable_irq_nosync(int irq);//等待目前的中斷處理完成再傳回。

void enable_irq(int irq);

上述三個函數作用于可程式設計中斷處理器,是以對系統内所有的CPU都生效。

void local_irq_save(unsigned long flags);//會将目前的中斷狀态保留在flags中

void local_irq_disable(void);//直接中斷

這兩個将屏蔽本CPU内的所有中斷。對應的上邊兩個中斷的方法如下

void local_irq_restore(unsigned long flags);

void local_irq_enable(void);

我們兩邊說了Linux系統中中斷是分為頂半部和底半部的,那麼在系統實作方面是具體怎樣實作的呢,這主要有tasklet,工作隊列,軟中斷:

1)tasklet:使用比較簡單,如下:

void my_tasklet_function(unsigned long); //定義一個處理函數

DECLARE_TASKLET(my_tasklet, my_tasklet_function, data); //定義了一個名叫my_tasklet的tasklet并将其與處理函數綁定,而傳入參數為data

在需要排程tasklet的時候引用一個tasklet_schedule()函數就能使系統在适當的時候進行排程運作:tasklet_schedule(&my_tasklet);

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_irq);來在系統在适當的時候需要排程時使用運作。

3)軟中斷:使用軟體方式模拟硬體中斷的概念,實作宏觀上的異步執行效果,tasklet也是基于軟中斷實作的。

在Linux核心中,用softirq_action結構體表征一個軟中斷,這個結構體中包含軟中斷處理函數指針和傳遞給函數的參數,使用open_softirq()可以注冊軟中斷對應的處理函數,而raise_softirq()函數可以觸發一個中斷。

軟中斷和tasklet仍然運作與中斷上下文,而工作隊列則運作于程序上下文。是以,軟中斷和tasklet的處理函數不能休眠,但工作隊列是可以的。

local_bh_disable()和local_bh_enable()是核心用于禁止和使能軟中斷和tasklet底半部機制的函數。

下邊咱們再來說說有關中斷共享的相關點:中斷共享即是多個裝置共享一根硬體中斷線的情況。Linux2.6核心支援中斷共享,使用方法如下:

*共享中斷的多個裝置在申請中斷時都應該使用SA_SHIRQ标志,而且一個裝置以SA_SHIRQ申請某中斷成功的前提是之前該中斷的所有裝置也都以SA_SHIRQ标志申請該終端

*盡管核心子產品可通路的全局位址都可以作為request_irq(….,void *dev_id)的最後一個參數dev_id,但是裝置結構體指針是可傳入的最佳參數。

*在中斷帶來時,所有共享此中斷的中斷處理程式都會被執行,在中斷處理程式頂半部中,應迅速根據硬體寄存器中的資訊比照傳入的dev_id參數判斷是否是被裝置的中斷,如果不是,應迅速傳回。

結語:在這次講解中說了三種Linux系統中中斷的頂/底半部機制和中斷共享的先關内容,但礙于頁面空間的原因,沒有給出例子,我在下次部落格中會專門來對每個點給出典型的模版.

Linux裝置驅動程式之中斷與時鐘(二)  

http://www.cnblogs.com/hanyan225/archive/2010/10/25/1860087.html

“小濤哥,快醒醒,快醒醒..”小王使勁推着睡夢中的我,“你不是說今天要講昨天有關的典型模闆執行個體嗎…”

“啊?小姐啊,現在才早上8點,還讓人睡覺不,别吵”我一頭鑽進被子裡說。

“不管,誰讓你昨天不說完,還賣個小關子,害我昨天晚上都沒睡好,想了一晚上…”

我揉揉蒙蒙的眼說:“行,權當看在你渴求的心情上,但隻此一次,下不為例,我還想好好睡懶覺呢..”

昨天我們講了有關中斷方面的東西,鑒于小王你不太懂,我今天就專門拿出一章來說說前邊中斷的使用典型模版,你照抄也友善不是:

1)在中斷分類中,我們說到了有關向量中斷和非向量中斷,向量中斷就是入口位址不同,進不同的位址做不同的事。那非向量中斷則是進同一位址,至于區分就放在了進去後用條件判斷,請看下邊的模闆:

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)在底半部機制中,我們講了tasklet,工作隊列和軟中斷先來看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, struct pt_regs *regs)  //中斷處理頂半部
{
  ...
  tasklet_schedule(&xxx_tasklet);
}
int __init xxx_init(void)   //裝置驅動子產品加載函數
{
  ..
  result= request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "XXX",NULL);  //申請中斷
  ...
}
void __exit xxx_exit(void)   //裝置驅動解除安裝子產品
{
  ..
  free_irq(xxx_irq, xxx_interrupt);   //釋放中斷
  ..
}      

工作隊列模版:

struct work_struct xxx_wq;
void xxx_do_work(unsigned long);
void xxx_do_work(unsigned long)   //中斷處理底半部
{
    .....
}
irqreturn_t xxx_interrupt(int irq, void *dev_id, struct pt_regs *regs)  //中斷處理頂半部
{
  ...
  schedule_work(&xxx_wq);
}
int xxx_init(void)   //裝置驅動子產品加載函數
{
  ..
  result= request_irq(xxx_irq, xxx_interrupt, SA_INTERRUPT, "XXX",NULL);  //申請中斷
  ...
  INIT_WORK(&xxx_wq, (void (*)(void *))xxx_do_work, NULL);
    ...
}
void __exit xxx_exit(void)   //裝置驅動解除安裝子產品
{
  ..
  free_irq(xxx_irq, xxx_interrupt);   //釋放中斷
  ..
}      

3)在上節最後我還給你講了有關中斷共享的東西吧,小王,也把模版給你:

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;
}
int __init xxx_init(void)   //裝置驅動子產品加載函數
{
  ..
  result= request_irq(xxx_irq, xxx_interrupt, SA_SHIRQ, "XXX",xxx_dev);  //申請共享中斷
  ...
}
void __exit xxx_exit(void)   //裝置驅動解除安裝子產品
{
  ..
  free_irq(xxx_irq, xxx_interrupt);   //釋放中斷
  ..
}      

共享中斷中,我們仔細看一下其實也沒什麼,不是。就是在和前邊中斷中要修改一下中斷标志,在中斷進行中判斷一下是否是自己本地的中斷,這個我都用紅色的辨別出來了。

“小王,看,小濤哥說話算數吧,上邊給出了所有模版,結合前一篇,相信你可以看的很順利的。好了,我要補補剛的覺了,中間不許叫我哈,想我也不行”我打打哈欠說。

Linux裝置驅動程式之中斷與時鐘(三)

  http://www.cnblogs.com/hanyan225/archive/2010/10/25/1860201.html

  晚上7點10分..

“小濤哥,這章不是叫Linux裝置驅動程式之中斷與時鐘,前邊你講了中斷,還給了我很多模版,我都看懂了,這次是不是要開始講時鐘了..”

“真聰明,越來越喜歡你這聰明的樣子了,說的不錯,今天就要開始一個新的子產品--核心時鐘”我很少誇人,為啥今天誇她呢了,呵呵.

  定時器,意思大家都明白,我就不說了,要是不明白,把它想成個鬧鐘總可以吧..

  定時器分為硬體和軟體定時器,軟體定時器最終還是要依靠硬體定時器來完成。核心在時鐘中斷發生後檢測各定時器是否到期,到期後的定時器處理函數将作為軟中斷在底半部執行。實質上,時鐘中斷處理程式執行update_process_timers函數,該函數調用run_local_timers函數,這個函數處理TIMER_SOFTIRQ軟中斷,運作目前處理上到期的所有定時器。

Linux核心中定義提供了一些用于操作定時器的資料結構和函數如下:

1)timer_list:說定時器,當然要來個定時器的結構體

struct timer_list{
     struct list_head entry;  //定時器清單
      unsigned long expires;  //定時器到期時間
      void (*function)(unsigned long) ;//定時器處理函數
      unsigned long data;   //作為參數被傳入定時器處理函數
      struct timer_base_s *base;
}      
2)初始化定時器:void init_timer(struct timer_list *timer);經過這個初始化後,entry的next為NULL,并給base指派
3)增加定時器:void add_timer(struct timer_list *timer); 該函數用于注冊核心定時器,并将定時器加入到核心動态定時器連結清單中。      
4)删除定時器:int del_timer(struct timer_list *timer);      
說明:del_timer_sync是del_timer的同步版,主要在多處理器系統中使用,如果編譯核心時不支援SMP,del_timer_sync和del_timer等價.      
5)修改定時器:int mod_timer(struct timer_list *timer, unsigned long expires);      
下邊是一個使用定時器的模版:      
struct xxx_dev  /*second裝置結構體*/ 
{
  struct cdev cdev; /*cdev結構體*/
  ...
  struct timer_list xxx_timer; /*裝置要使用的定時器*/
};
int xxx_func1(...)  //xxx驅動中某函數 
{
  struct xxx_dev *dev = filp->private_data;
    ...
  /*初始化定時器*/
  init_timer(&dev->xxx_timer);
  dev->xxx_timer.function = &xxx_do_handle;
  dev->xxx_timer.data = (unsigned long)dev;
  dev->xxx_timer.expires = jiffies + delay;
  
  add_timer(&dev->xxx_timer); /*添加(注冊)定時器*/
  ...
  return 0;
}
int xxx_func2(...)   //驅動中某函數 
{
  ...
  del_timer(&second_devp->s_timer);
  ...
}
static void xxx_do_timer(unsigned long arg)  //定時器處理函數 
{
  struct xxx_device *dev = (struct xxx_device *)(arg);
    ...
    //排程定時器再執行
  dev->xxx_timer.expires = jiffies + delay;
  add_timer(&dev->xxx_timer);
}      

在定時器函數中往往會在做完具體工作後,延遲expires并将定時器再次添加到核心定時器連結清單中,以便定時器能被再次觸發(這句話我也是從别處抄來的,别告訴小王哈)。

在核心定時器中,常常少不了要說下核心延遲的事,請接着往下看:

1)短延遲:在linux核心中提供了三個函數來分别實作納秒,微秒,毫秒延遲,原理上是忙等待,它根據CPU頻率進行一定次數的循環

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

毫秒延遲已經相當大了,當然更秒延遲當然要小一些,在核心中,為了性能,最好不要用mdelay,這會耗費大量cpu資源,那麼咋辦呢,涼拌..

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

這三個是核心專門提供該我們用來處理毫秒以上的延遲。上述函數将使得調用它的程序睡眠參數指定的秒數,其中第二個是可以被打斷的,其餘的兩個是不可以的。

2)長延遲:核心中進行延遲最常用的方法就是比較目前的jiffies和目标jiffies(目前的加上時間間隔的jiffies),直到未來的jiffies達到目标jiffies。比如:

unsigned long delay = jiffies + 100;  //延遲100個jiffies
while(time_before(jiffies, delay));      

與time_before對應的還有一個time_after().其實就是#define time_before(a,b)  time_after(b,a);

另外兩個是time_after_eq(a,b)和time_before_eq(a,b)

3)睡着延遲:這顯然是比忙等待好的方法,因為在未到來之前,程序會處于睡眠狀态,把CPU空出來,讓CPU可以做别的事情,等時間到了,調用schedule_timeout()就可以喚醒它并重新排程執行。msleep和msleep_interruptible本質上都是依靠包含了schedule_timeout的schedule_timeout_uninterruptible()和schedule_

timeout_interruptible()實作。就像下邊這樣:

void msleep(unsigned int msecs)         
{
    unsigned long timeout = msecs_to_jiffies(msecs) + 1;
    while(timeout)
         timeout = schedule_timeout_uninterruptible(timeout);
}
unsigned long msleep_interruptible(unsigned int msecs)
{
    unsigned long timeout = msecs_to_jiffies(msecs) + 1;
    while(timeout && !signal_pending(current))
         timeout = schedule_timeout_interruptible(timeout);
    return jiffies_to_msecs(timeout);
}      
signed long __sched schedule_timeout_interruptible()signed long timeout)
{
    __set_current_state(TASK_INTERRUPTIBLE);
    return schedule_timeout(timeout);
}
signed long __sched schedule_timeout_uninterruptible()signed long timeout)
{
    __set_current_state(TASK_UNINTERRUPTIBLE);
    return schedule_timeout(timeout);
}      

另外還有如下: time_on_timeout(wait_queue_head_t *q, unsigned long  timeout); interruptible_sleep_on_timeout(wait_queue_head_t *q, unsigned long timeout); 這兩個将目前程序添加到等待隊列,進而在等待隊列上睡眠,當逾時發生時,程序将被喚醒。   “小王,有關中斷和系統時鐘的咱們也講完了,下次就給你來一個有關系統時鐘的裝置驅動例子,鞏固一下吧,你可要抓緊哦..“

繼續閱讀