linux實現底半部的機制主要有tasklet、workqueue、softirq和線程化irq。
1.tasklet
tasklet的使用較為簡單,它的執行上下文是軟中斷,所以在tasklet中不能睡眠,它的執行時機通常是中斷頂半部返回的時候。我們只需要定義tasklet及其處理函數,並將兩者關聯起來即可,例如:
1 void my_tasklet_func(unsigned long); /* 定義一個處理函數 */ 2 DECLARE_TASKLET(my_tasklet, my_tasklet_func, data); /* 定義一個tasklet結構my_tasklet,與my_tasklet_func(data)函數相關聯 */
代碼 DECLEARE_TASKLET(my_tasklet, my_tasklet_func, data)實現了定義名稱為my_tasklet的tasklet,並將其與my_tasklet_func()這個函數綁定,而傳入這個函數的參數為data。在需要調度tasklet的時候引用一個tasklet_schedule()函數就能使系統在適當的時候進行調度運行:
tasklet_schedule(&my_tasklet);
使用tasklet作為底半部處理中斷的設備驅動程序模板如下:
struct xxx_struct { int xxx_irq; ... ... }; static unsigned long data; /* 定義 tasklet 和底半部函數並將它們關聯 */ DECLARE_TASKLET(xxx_tasklet, xxx_do_tasklet, (unsigned long)&data); /* 中斷處理底半部 */ void xxx_do_tasklet(unsigned long data) { struct xxx_struct *pdata = (void *)*(unsigned long *)data; ... } /* 中斷處理頂半部 */ irqreturn_t xxx_interrupt(int irq, void *dev_id) { ... data = (unsigned long)dev_id; tasklet_schedule(&xxx_tasklet); ... } /* 設備驅動模塊探測函數 */ static int xxx_probe(struct platform_device *pdev) { ... struct xxx_struct *pdata = devm_kzalloc(&pdev->dev, sizeof(struct xxx_struct), GFP_KERNEL); if (!pdata) { dev_err(&pdev->dev, "Out of memory\n"); return -ENOMEM; } platform_set_drvdata(pdev, pdata); /* 申請中斷 */ result = request_irq(pdata->xxx_irq, xxx_interrupt, 0, "xxx", pdata); ...
return IRQ_HANDLED; } /* 設備驅動模塊remove函數 */ static int xxx_remove(struct platforn_device *pdev) { struct xxx_struct *pdata = platform_get_drvdata(pdev); ... /* 釋放中斷 */ free_irq(pdata->xxx_irq, NULL); ... }
上述程序在模塊加載函數中申請中斷,並在模塊卸載函數中釋放它。對應於xxx_irq的中斷處理程序被設置為xxx_interrupt()函數,在這個函數中,tasklet_schedule(&xxx_tasklet)調度被定義的tasklet函數xxx_do_tasklet()在適當的時候執行。
2.工作隊列
工作隊列的使用方法和tasklet非常相似,但是工作隊列的執行上下文是內核線程,因此可以調度和睡眠。下面的代碼用於定義一個工作隊列和一個底半部執行函數:
struct work_struct my_wq; /* 定義一個工作隊列 */ void my_wq_func(struct work_struct *work); /* 定義一個處理函數 */
通過INIT_WORK()可以初始化這個工作隊列並將工作隊列與處理函數綁定:
INIT_WORK(&my_wq, my_wq_func); /* 初始化工作隊列並將其與處理函數綁定 */
與tasklet_schedule()對應的用於調度工作隊列執行的函數為 schedule_work(),如:
schedule_work(&my_wq); /* 調度工作隊列執行 */
使用工作隊列處理中斷底半部的設備驅動程序模板代碼如下:
struct xxx_struct { int xxx_irq; struct work_struct xxx_wq; /* 定義工作隊列 */ ... ... }; /* 中斷處理底半部 */ void xxx_do_work(struct work_struct *work) { struct xxx_struct *pdata = container_of(work, struct xxx_struct, xxx_wq); ... } /* 中斷處理頂半部 */ irqreturn_t xxx_interrupt(int irq, void *dev_id) { struct xxx_struct *pdata = (struct xxx_struct *)dev_id; ... schedule_work(&pdata->xxx_wq); ... return IRQ_HANDLED; } /* 設備驅動模塊探測函數 */ int xxx_probe(struct platform_device *pdev) { ... struct xxx_struct *pdata = devm_kzalloc(&pdev->dev, sizeof(struct xxx_struct ), GFP_KERNEL); /*申請中斷*/ result = request_irq(pdata->xxx_irq, xxx_interrupt, 0, "xxx", pdata); ... /* 初始化工作隊列 */ INIT_WORK(&pdata->xxx_wq, xxx_do_work); platform_set_drvdata(pdev, pdata); ... } /* 設備驅動模塊remove函數 */ int xxx_remove(struct platform_device *pdev) { struct xxx_struct *pdata = platform_get_drvdata(pdev); ...
cancel_work_sync(&pdata->xxx_wq); /* 釋放中斷 */ free_irq(pdata->xxx_irq, NULL); ... }
工作隊列早期的實現是在每個CPU核上創建一個worker內核線程,所有在這個核上調度的工作都在該worker線程中執行,其並發性顯然差強人意。在linux 2.6.36以后,轉而實現了 “Concurrency-managed workqueues”,簡稱"cmwq",cmwq會自動維護工作隊列的線程池以提高並發性,同時保持了API的向后兼容。
延時工作隊列
1. 數據結構delayed_work用於處理延遲執行
struct delayed_work { struct work_struct work; struct timer_list timer; /* target workqueue and CPU ->timer uses to queue ->work */ struct workqueue_struct *wq; int cpu; };
2.在工作隊列中被調用的函數原形如下
typedef void (*work_func_t)(struct work_struct *work);
3. 初始化數據結構
INIT_DELAYED_WORK(struct delayed_work *work, work_func_t func)
4. 提交延時任務到工作隊列
int schedule_delayed_work(struct delayed_work *work, unsigned long delay);
5.刪除提交到工作隊列的任務
int cancel_delayed_work(strcut delayed_work *work);
6. 刷新默認工作隊列(常跟cancle_delayed_work一起使用)
void flush_schedlue_work(void); 或者 int cancel_delayed_work_sync(strcut delayed_work *work);
使用延時工作隊列處理中斷底半部的設備驅動程序模板代碼如下:
struct xxx_struct {
int xxx_irq; struct delayed_work xxx_dwork; /* 定義延時工作隊列 */ ... ... }; /* 中斷處理底半部 */ void xxx_do_work(struct work_struct *work) { struct xxx_struct *pdata = container_of(work, struct xxx_struct, xxx_dwork.work); ... } /* 中斷處理頂半部 */ irqreturn_t xxx_interrupt(int irq, void *dev_id) { struct xxx_struct *pdata = (struct xxx_struct *)dev_id; ...
/* * delay 2000ms */ schedule_delayed_work(&pdata->xxx_dwork, msesc_to_jiffies(2000)); ...
return IRQ_HANDLED; } /* 設備驅動模塊探測函數 */ int xxx_probe(struct platform_device *pdev) { ... struct xxx_struct *pdata = devm_kzalloc(&pdev->dev, sizeof(struct xxx_struct ), GFP_KERNEL); /*申請中斷*/ result = request_irq(pdata->xxx_irq, xxx_interrupt, 0, "xxx", pdata); ... /* 初始化工作隊列 */ INIT_DELAYED_WORK(&pdata->xxx_dwork, xxx_do_work); platform_set_drvdata(pdev, pdata); ... } /* 設備驅動模塊remove函數 */ int xxx_remove(struct platform_device *pdev) { struct xxx_struct *pdata = platform_get_drvdata(pdev); ...
cancel_delayed_work(&pdata->xxx_dwork);
cancel_delayed_work_sync(&pdata->xxx_dwork);
/* 釋放中斷 */ free_irq(pdata->xxx_irq, NULL); ... }
3.軟中斷
軟中斷(Softirq)也是一種傳統的底半部處理機制,它的執行時機通常是頂半部返回的時候,tasklet是基於軟中斷實現的,因此也運行於軟中斷上下文。
在Linux內核中,用softirq_action結構體表征一個軟中斷,這個結構體包含軟中斷處理函數指針和傳遞給該函數的參數。使用open_softirq()函數可以注冊軟中斷對應的處理函數,而raise_softirq()函數可以觸發一個軟中斷。
軟中斷和tasklet運行於軟中斷上下文,仍然屬於原子上下文的一種,而工作隊列則運行於進程上下文。因此,在軟中斷和tasklet處理函數中不允許睡眠,而在工作隊列處理函數中允許睡眠。
local_bh_disable() 和 local_bh_enable() 是內核中用於禁止和使能軟中斷及tasklet底半部機制的函數。
內核中采用softirq的地方包括 HI_SOFTIRQ、TIMER_SOFTIRQ、NET_TX_SOFTIRQ、NET_RX_SOFTIRQ、SCSI_SOFTIRQ、TASKLET_SOFTIRQ等,一般來說,驅動的編寫者不會也不宜直接使用softirq。
硬中斷、軟中斷和信號的區別:
硬中斷是外部設備對CPU的中斷,軟中斷是中斷底半部的一種處理機制,而信號則是由內核(或其他進程)對某個進程的中斷。在設計系統調用的場合,人們也常說通過軟中斷(例ARM為swi)陷入內核,此時軟中斷的概念是指由軟件指令引發的中斷,和我們這個地方所說的softirq是兩個完全不同的概念,一個是software,一個是soft。
需要特別說明的是,軟中斷以及基於軟中斷的tasklet如果在某段時間內大量出現的話,內核會把后續軟中斷放入 ksoftirqd 內核線程中執行。總的來說,中斷優先級高於軟中斷,軟中斷優先級又高於任何一個線程。軟中斷適度線程化,可以緩解高負載情況下系統的響應。
4.threaded_irq
在內核中除了可以通過request_irq()、devm_request_irq()申請中斷以外,還可以通過request_threaded_irq() 和 devm_request_threaded_irq() 申請。這兩個函數的原型為:
/** * request_threaded_irq - allocate an interrupt line * @irq: Interrupt line to allocate * @handler: Function to be called when the IRQ occurs. * Primary handler for threaded interrupts. * If handler is NULL and thread_fn != NULL * the default primary handler is installed. * @thread_fn: Function called from the irq handler thread * If NULL, no irq thread is created * @irqflags: Interrupt type flags * @devname: An ascii name for the claiming device * @dev_id: A cookie passed back to the handler function * * This call allocates interrupt resources and enables the * interrupt line and IRQ handling. From the point this * call is made your handler function may be invoked. Since * your handler function must clear any interrupt the board * raises, you must take care both to initialise your hardware * and to set up the interrupt handler in the right order. * * If you want to set up a threaded irq handler for your device * then you need to supply @handler and @thread_fn. @handler is * still called in hard interrupt context and has to check * whether the interrupt originates from the device. If yes it * needs to disable the interrupt on the device and return * IRQ_WAKE_THREAD which will wake up the handler thread and run * @thread_fn. This split handler design is necessary to support * shared interrupts. * * Dev_id must be globally unique. Normally the address of the * device data structure is used as the cookie. Since the handler * receives this value it makes sense to use it. * * If your interrupt is shared you must pass a non NULL dev_id * as this is required when freeing the interrupt. * * Flags: * * IRQF_SHARED Interrupt is shared * IRQF_TRIGGER_* Specify active edge(s) or level * IRQF_ONESHOT Run thread_fn with interrupt line masked */ int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id); /** * devm_request_threaded_irq - allocate an interrupt line for a managed device * @dev: device to request interrupt for * @irq: Interrupt line to allocate * @handler: Function to be called when the IRQ occurs * @thread_fn: function to be called in a threaded interrupt context. NULL * for devices which handle everything in @handler * @irqflags: Interrupt type flags * @devname: An ascii name for the claiming device, dev_name(dev) if NULL * @dev_id: A cookie passed back to the handler function * * Except for the extra @dev argument, this function takes the * same arguments and performs the same function as * request_threaded_irq(). IRQs requested with this function will be * automatically freed on driver detach. * * If an IRQ allocated with this function needs to be freed * separately, devm_free_irq() must be used. */ int devm_request_threaded_irq(struct device *dev, unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id);
由此可見,它們比request_irq()、devm_request_irq()多了一個參數 thread_fn。用這兩個API申請中斷的時候,內核會為相應的中斷號分配一個對應的內核線程。注意這個線程只針對這個中斷號,如果其他中斷也通過request_threaded_irq()申請,自然會得到新的內核線程。
參數handler對應的函數執行於中斷上下文,thread_fn參數對應的函數則執行於內核線程。如果handler結束的時候,返回值是 IRQ_WAKE_THREAD,內核會調度對應線程執行 thread_fn 對應的函數。
request_threaded_irq() 和 devm_request_threaded_irq() 支持在 irqflags 中設置 IRQF_ONESHOT標記,這樣內核會自動幫助我們在中斷上下文中屏蔽對應的中斷號,而在內核調度 thread_fn 執行后,重新使能該中斷號。對於我們無法在上半部清除中斷的情況, IRQ_ONESHOT 特別有用,避免了中斷服務程序一退出,中斷就洪泛的情況。
handler 參數可以設置為NULL,這種情況下,內核會用默認的 irq_default_primary_handler() 代替 handler,並會使用 IRQ_ONESHOT標記。 irq_default_primary_handler() 定義為:
/* * Default primary interrupt handler for threaded interrupts. Is * assigned as primary handler when request_threaded_irq is called * with handler == NULL. Useful for oneshot interrupts. */ static irqreturn_t irq_default_primary_handler(int irq, void *dev_id) { return IRQ_WAKE_THREAD; }
