2012年7月5日 星期四

Linux Interrupt 整理 (一)

  • 前言
CPU的速度很快,假設每每都必須等待週邊的硬體動作完成CPU才能繼續工作的話,效率未免太低了。因此,我們的系統設計了中斷(Interrupt),當有 event 發生(例如 工作完成)就觸發一個Interrupt告訴CPU,這時候CPU就會"馬上"去處理。這麼一來就可以避免等待時間的浪費。
我們可以針對裝置的Interrupt建立Handler,讓不同的Device Interrupt發生後去執行相對應的"程序" ,這個程序就稱為 Interrupt Service Routine(ISR)。如果沒有相對應的ISR則僅會回覆ACK而不作任何事。
傳統電腦是利用兩個Programmable Interrupt Controller (PIC)提供15組Interrupt Line連接Device和CPU。Device 觸發中斷請求(Interrupt Request)給PIC然後在傳至CPU,也因為數量有限所以很寶貴。現代的設計是使用Advanced Programmable Interrupt Controller(APIC) 
在Linux Kernel 中存在一個資料結構叫irq_desc 實作在kernel/irq用來鍊結IRQ及ISR,另外也建立Radix treer叫irq_desc_tree來存放irq_desc結構的變數。
  • 建立IRQ
要使用IRQ必須要向核心註冊。註冊的方式則是利用request_irq長相如下:
//include/linux/interrupt.h
/**
 *第一個參數是IRQ的號碼
 *第二個參數是ISR的pointer
 *第三個參數
 *    IRQF_SHARED         Interrupt is shared
 *    IRQF_SAMPLE_RANDOM  代表這個interrupt可以作為系統random的依據(有固定週期的interrupt
 *                        不適合當成random依據)
 *    IRQF_TRIGGER_*      Specify active edge(s) or level
 *第四個參數是顯示在例如/proc/interrupts的名稱
 *第五個參數用途可以參考http://www.cnblogs.com/ITmelody/archive/2012/05/12/2497018.html
 *一般直接填NULL
*/
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
     const char *name, void *dev)
{
    //request_threaded_irq第三個參數是irq_handler_t thread_fn
    return request_threaded_irq(irq, handler, NULL, flags, name, dev);
}
從這裡我們也可以發現其實request_irq()會直接call request_threaded_irq()。因此我們也會看到有人就直接使用request_threaded_irq()。直接舉nVidia的mmc的例子。
//driver/mmc/host/sdhci-tegra.c
static irqreturn_t carddetect_irq(int irq, void *data)
{
 ...
}

static int __devinit sdhci_tegra_probe(struct platform_device *pdev)
{
    struct sdhci_host *host;
    ...
    rc = request_threaded_irq(gpio_to_irq(plat->cd_gpio), NULL,
     carddetect_irq,
     IRQF_TRIGGER_FALLING | IRQF_TRIGGER_RISING,
     mmc_hostname(host->mmc), host);
    ...
}
上述的例子是利用GPIO的變化來當成Interrupt的條件。IRQ number是利用gpio_to_irq取得對應的IRQ;ISR是carddetect_irq(),觸發的條件是當此GPIO rising/falling都會觸發Interrupt。

當我們remove module的時候,我們可能需要free掉這個IRQ,這個時候就可以呼叫free_irq來達成。
static int __devexit sdhci_tegra_remove(struct platform_device *pdev)
{
    ...
    if (gpio_is_valid(plat->cd_gpio)) {
        free_irq(gpio_to_irq(plat->cd_gpio), host);
        ...
    }
    ...
}
  • ISR Top and Bottom Halves
Interrupt設計的目的就是不希望卡在攏長的處理。因此,收到中斷後,我們也會希望可以快速處理完要作的事情。然而,常常我們需要作的事情就是這麼多無法快速的做完造成系統的遲鈍。因此,Linux Kernel提出了將ISR拆成top half 跟bottom half。當Interrupt發生後一定要馬上作的事情就屬於Top half直接寫在原本的ISR,而可以晚點做的就屬於Bottom half。我們可以利用SoftIRQTaskletWorkqueu來排程bottom half的部份。下表比較了三種的差異。
下面我們分別介紹Tasklet及Workqueue。
 Tasklet
Tasklets 與Linux Kernel的timer類似,他們都在Interrupt time(Atomic time)執行而且會執行在schedule他的CPU上。不同於timer,我們無法要求tasklet的task在我們期望的時間執行 。同樣的,我們使用nVidia的mmc driver為例來說明Tasklet。
首先,我們必須先註冊一個tasklet,指定這個tasklet的名字、func及data。
//mmc/host/sdhci.c
static void sdhci_tasklet_card(unsigned long param)
{
   ...
}

int sdhci_add_host(struct sdhci_host *host){
   ...
/*
 * Init tasklet.
 *void tasklet_init(struct tasklet_struct *t,
 *                  void (*func)(unsigned long), unsigned long data)
 *簡單來說就是tasklet_init(name, func,data)
*/
    tasklet_init(&host->card_tasklet,
                 sdhci_tasklet_card, (unsigned long)host);
 
   ...
}

接著,這個task必須要在呼叫schedule_tasklet之後才會被排程執行。理所當然,我們會在top half執行完之後去啟動bottom half的部份。因此我們回到top half(carddetect_irq())看看我們如何去啟動bottom half。
//driver/mmc/host/sdhci-tegra.c
static irqreturn_t carddetect_irq(int irq, void *data)
{
    ...
    tasklet_schedule(&sdhost->card_tasklet);
    
    return IRQ_HANDLED;
};


Workqueue
Workqueue的部份則利用omap的mmc driver來舉例。如同tasklet,我們必須利用INIT_WORK(name,func)先註冊一個workqueue。以下面的範例,利用schedule_work()將這個workqueue交給一個叫"event"的公用work thread來執行。
首先我們來看註冊 workqueue的部份。
//driver/mmc/omap_hsmmc.c
static void omap_hsmmc_detect(struct work_struct *work)
{
   ...
}

static int __init omap_hsmmc_probe(struct platform_device *pdev)
{
   ...
   INIT_WORK(&host->mmc_carddetect_work, omap_hsmmc_detect);
   ...
}
而去啟動他的部份則是在top half的omap_hsmmc_cd_handler()則是...
//driver/mmc/omap_hsmmc.c

static irqreturn_t omap_hsmmc_cd_handler(int irq, void *dev_id)
{
 struct omap_hsmmc_host *host = (struct omap_hsmmc_host *)dev_id;

 if (host->suspended)
  return IRQ_HANDLED;
 //啟動Workqueue 
 schedule_work(&host->mmc_carddetect_work);

 return IRQ_HANDLED;
}

  • Reference
Linux Kernel Development Third Edition 
Linux Device Drivers, Third Edition
Intel APIC Architecture
Interrupt_request
Essential Linux Device Drivers

沒有留言: