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必須要向核心註冊。註冊的方式則是利用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。我們可以利用SoftIRQ、Tasklet或Workqueu來排程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;
}
Linux Kernel Development Third Edition
Linux Device Drivers, Third Edition
Intel APIC Architecture
Interrupt_request
Essential Linux Device Drivers
沒有留言:
張貼留言