Linux中斷底半部機制總結


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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM