中斷下半部機制 - 軟中斷及tasklet

落塵紛擾發表於2015-04-03

1. 引入軟中斷

一箇中斷處理程式的一個或幾個中斷服務例程在執行結束之前,核心處於中斷環境中,當前CPU不再響應同型別的中斷,如果不允許中斷巢狀,則CPU需要遮蔽掉所有中斷。也就是說,一個CPU忙於服務於一箇中斷事件時,就不能處理其他中斷,同時CPU不能執行其他程式,即不能被搶佔,這種情況下,如果在中斷服務例程中消耗的時間過多,就會對效能產生潛在的影響。

一般情況下,一箇中斷事件所觸發的動作可能需要佔用很多CPU時間,但通常其中多數內容都是可以等待的。為了保證對硬體保持較短的響應時間,在一箇中斷事件到來時,可以先搶佔CPU,將必須儘快處理的事情做完,然後釋放CPU,在稍後的某一時刻,當核心不需要再做一些緊迫之事,再處理中斷事件剩下的事情。

這些可以延後的處理程式被稱為中斷下半部。例如網路卡收包的處理,當CPU收到一個收包中斷時,需要把資料包從DMA中搬運到記憶體中核心預先設定好的位置,並設定某些標記來通知核心有資料包到來,然後核心分配一個skb緩衝區,將資料內容拷貝到緩衝區裡,並初始化一個skb例項,然後將資料包交給上層協議棧處理,這個過程非常複雜,需要耗費大量CPU時間,不可能都在中斷處理程式中完成。有了中斷下半部機制,在中斷處理程式中只需將資料包放到記憶體並設好標記,這可以很快完成,而剩下實際的資料包處理過程則放到下半部中去執行。

核心使用軟中斷(softirq)和微任務(tasklet)兩種可延遲函式來實現中斷下半部機制,他們是一種非緊迫、可中斷的核心函式,因為他們在執行過程是開中斷的。

tasklet是在軟中斷之上實現的,所以在核心程式碼中“軟中斷”通常表示可延遲函式的所有種類。另外一個被廣泛使用的術語“中斷上下文”表示核心當前正在執行一箇中斷處理程式或一個可延遲函式。

2. 軟中斷的實現

核心2.6.31中使用有限個軟中斷,所有的軟中斷型別定義如下。

/*PLEASE, avoid to allocate new softirqs, if you need not _really_ high frequencythreaded job scheduling. For almost all the purposes
taskletsare more than enough. F.e. all serial device BHs et
al.should be converted to tasklets, not to softirqs.
 */
enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,  /*Preferable RCU should always be the last softirq */
    NR_SOFTIRQS
};

核心中共有NR_SOFTIRQS種軟中斷,標號從0到NR_SOFTIRQS-1,優先順序由高到低,即HI_SOFTIRQ的優先順序最高,在執行軟中斷處理函式時,將按照優先順序的順序來執行。

2.1 相關資料結構

在每個程式的thread_info結構中,都有一個搶佔計數器preempt_count。

struct thread_info {
    ……
    __u32      cpu;       /* current CPU */
    int    preempt_count;    /* 0 => preemptable */
    ……
};

在thread_info結構體中,preempt_count成員用來跟蹤核心搶佔和核心控制路徑的巢狀,這個整型數包含三個計數器和兩個標記位:

  • bits 0-7:這個計數器表示在核心程式碼中禁用本地核心搶佔的次數,等於0表示允許核心搶佔。
  • bits 8-15:可延遲函式被禁用的程度,為0表示可延遲函式處於啟用狀態。
  • bits 16-25:本地CPU上中斷處理程式的巢狀數,巢狀(nested)數還受堆疊大小的限制,所以不一定能達到1024。
  • bit 26:是否處於不可遮蔽中斷(NMI)上下文中。
  • bit 28:PREEMPT_ACTIVE標記,在程式排程的時候表示程式是通過搶佔而被排程的。

每個程式都有一個thread_info結構,所以每個程式都有自己的preempt_count,通過current_thread_info()->preempt_count可以獲取該計數器。

每個CPU都維護一個全域性的irq_cpustat_t結構體,在mips中該結構體只有一個成員:掛起的軟中斷的掩碼,它是32位整型,也就是說系統最多支援32個軟中斷,現有的軟中斷在上面已列出。

typedef struct {
    unsigned int __softirq_pending;
}____cacheline_aligned irq_cpustat_t;
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;

通過檢視__softirq_pending成員的值(每個bit位對應一個軟中斷號)就可以知道哪些軟中斷正等待被處理,函式介面為local_softirq_pending()。

#define__IRQ_STAT(cpu, member) (irq_stat[cpu].member)
#define local_softirq_pending() \
       __IRQ_STAT(smp_processor_id(),__softirq_pending)

2.2 處理softirq

註冊軟中斷:

所有軟中斷的處理函式都放到一個全域性陣列softirq_vec中,

static struct softirq_action softirq_vec[NR_SOFTIRQS]__cacheline_aligned_in_smp;

註冊軟中斷處理函式的介面為open_softirq():

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

該函式接受兩個引數:軟中斷型別的標號nr和將要註冊的處理函式action。從第二個引數action可以看出,註冊的處理函式需要一個引數:指向特定型別軟中斷的struct softirq_action例項,不過在處理函式中一般不會使用這個引數。

啟用軟中斷:

想要執行某個軟中斷時,需要啟用該軟中斷,讓相應處理函式函式後續得到執行。raise_softirq()用來啟用軟中斷,它接受軟中斷型別標號nr作為引數。

void raise_softirq(unsigned int nr)
{
    unsigned long flags;
 
    local_irq_save(flags); //儲存當前中斷狀態到flags並關中斷。
    raise_softirq_irqoff(nr);
    local_irq_restore(flags); //從flags中恢復之前的中斷狀態
}

可見該函式主體部分需要在關中斷環境下進行。

raise_softirq_irqoff()做的事情:

1. 將當前CPU的irq_stat.__softirq_pending的第nr位置為1,即掛起該軟中斷(掛起即是等待稍後被執行)。

2. 如果當前處在中斷環境(preempt_count的softirq/hardirq/nmi三個計數器任何一個不為0)中,就直接返回,因為這時可能已經在中斷上下文中呼叫了raise_softirq,或者當前禁用了軟中斷。不過這時已經設定了pending位,下次喚醒軟中斷時就會執行到。

3. 第2步如果為假,則喚醒本地CPU的ksoftirqd核心執行緒來處理掛起的軟中斷(將ksoftirqd對應的task_struct加入可執行佇列等待被排程)。

ksoftirqd核心執行緒:

執行軟中斷處理函式的實際工作交給了ksoftirqd核心執行緒,每個CPU都有一個ksoftirqd執行緒。其定義如下:

static DEFINE_PER_CPU(struct task_struct *, ksoftirqd);

即定義了一個per-cpu的task_struct指標。建立ksoftirqd執行緒的工作在cpu_callback()中完成:

static int __cpuinit cpu_callback(struct notifier_block *nfb,
               unsigned long action,
               void *hcpu)
{
    int hotcpu = (unsigned long)hcpu;
    struct task_struct *p;
 
    switch (action) {
    case CPU_UP_PREPARE:
    case CPU_UP_PREPARE_FROZEN:
       /* 建立一個核心執行緒p,名稱為"ksoftirqd/%d"*/
       p = kthread_create(ksoftirqd, hcpu,"ksoftirqd/%d", hotcpu);
       if (IS_ERR(p)) {
           printk("ksoftirqd for %ifailed\n", hotcpu);
           return NOTIFY_BAD;
       }
       /* 將p的task_struct結構繫結到當前CPU */
       kthread_bind(p, hotcpu);
       /* 將執行緒p賦值給per-cpu變數ksoftirqd */
       per_cpu(ksoftirqd,hotcpu) = p;
    ……
}

上述程式碼建立了per-cpu執行緒"ksoftirqd/%d",其處理函式為ksoftirqd(),執行緒名中%d為CPU的邏輯編號。所以,前面講到的raise_softirq()函式的工作就是嘗試喚醒ksoftirqd核心執行緒並執行ksoftirqd()函式,它的引數為當前CPU的邏輯編號(單CPU系統編號為0)。

static int ksoftirqd(void * __bind_cpu)
{
    set_current_state(TASK_INTERRUPTIBLE);
 
    while (!kthread_should_stop()) {
       preempt_disable(); //關閉搶佔
       /* 如果沒有pending的軟中斷,則放棄CPU */
       if (!local_softirq_pending()) {
           preempt_enable_no_resched();
           schedule();
           preempt_disable();
       }
 
       __set_current_state(TASK_RUNNING);
 
       /* 如果有未決軟中斷,則進行do_softirq() */
       while (local_softirq_pending()) {
           /* Preempt disable stops cpu goingoffline.
             If already offline, we'll be on wrong CPU:
             don't process */
           if (cpu_is_offline((long)__bind_cpu))
              goto wait_to_die;
           /* 處理軟中斷 */
           do_softirq();
           /* 檢查是否需要re-schedule */
           preempt_enable_no_resched();
           cond_resched();
           preempt_disable();
           rcu_qsctr_inc((long)__bind_cpu);
       }
       preempt_enable();//開啟搶佔
       set_current_state(TASK_INTERRUPTIBLE);
    }
    __set_current_state(TASK_RUNNING);
    return 0;
 
wait_to_die:
    ……
    return 0;
}

處理軟中斷的任務在do_softirq()函式中進行,它呼叫了__do_softirq()。

asmlinkage void __do_softirq(void)
{
    struct softirq_action *h;
    __u32 pending;
    /* 限制每次處理軟中斷的數量 */
    int max_restart = MAX_SOFTIRQ_RESTART;
    int cpu;
 
    /* 獲取未決的軟中斷,賦值給臨時變數pending */
    pending = local_softirq_pending();
    account_system_vtime(current);
 
    /* 防止軟中斷重入 */
    __local_bh_disable((unsignedlong)__builtin_return_address(0));
    lockdep_softirq_enter();
 
    cpu = smp_processor_id();
restart:
    /* 重置未決軟中斷點陣圖,以便可以啟用新的軟中斷 */
    set_softirq_pending(0);
 
    /* 在開中斷環境下處理軟中斷 */
    local_irq_enable();
 
    /* 存放軟中斷的陣列指標 */
    h = softirq_vec;
 
    do {
       /* 依次處理每一個型別的未決軟中斷 */
       if (pending & 1) {
           int prev_count = preempt_count();
           kstat_incr_softirqs_this_cpu(h -softirq_vec);
 
           trace_softirq_entry(h,softirq_vec);
           /* 執行軟中斷處理函式 */
           h->action(h);
           trace_softirq_exit(h, softirq_vec);
           if (unlikely(prev_count !=preempt_count())) {
              printk(KERN_ERR "huh, enteredsoftirq %td %s %p"
                     "with preempt_count %08x,"
                      " exited with %08x?\n", h -softirq_vec,
                     softirq_to_name[h - softirq_vec],
                     h->action, prev_count,preempt_count());
              preempt_count() = prev_count;
           }
 
           rcu_bh_qsctr_inc(cpu);
       }
       h++;
       pending >>= 1;
    } while (pending);
 
    local_irq_disable();
 
    /* 重新獲取未決軟中斷 */
    pending = local_softirq_pending();
    /* 如果還有未決軟中斷並且restart的次數沒超過限制,則繼續處理
    軟中斷*/
    if (pending && --max_restart)
       goto restart;
 
    /* 如果restart次數超過限制,但仍有未決軟中斷,則重新喚醒ksoftirqd */
    if (pending)
       wakeup_softirqd();
 
    lockdep_softirq_exit();
 
    account_system_vtime(current);
    _local_bh_enable();
}

__do_softirq()函式的主要工作為:

1. 獲得當前未決的軟中斷點陣圖和softirq_vec陣列,二者一一對應,例如點陣圖的bit0位為1,則說明軟中斷HI_SOFTIRQ 待處理,即softirq_vec[HI_SOFTIRQ]->action()函式需要執行。

2. 開啟硬體中斷,從軟中斷點陣圖的bit0開始(即從優先順序由高到低)檢查被置為1的位, 並依次執行相應的處理函式。

3. 如果已經處理的軟中斷又被啟用,則__do_softirq()重新呼叫wakeup_softirqd()喚醒ksoftirqd執行緒,而不是直接判斷__softirq_pending的值來處理軟中斷,這樣做是為了防止高頻率的軟中斷導致其他任務無法得到執行。因為核心執行緒優先順序較低,所以如果有使用者程式等待執行,則wakeup_softirqd()不會使ksoftirqd立即被喚醒,但如果機器空閒,掛起的軟中斷很快就可以被執行。

由第2步可知,在軟中斷處理過程中是開中斷的,所以如果這時產生了一個硬體中斷,則系統會先處理硬體中斷,這樣,就不用擔心軟中斷進行復雜的處理導致長時間佔用CPU了。

在軟中斷的處理函式中,HI_SOFTIRQ 和TASKLET_SOFTIRQ兩種tasklet對應的處理函式分別為tasklet_hi_action ()和tasklet_action(),他們是在softirq_init()中註冊到softirq_vec陣列中的。

       open_softirq(TASKLET_SOFTIRQ,tasklet_action);
       open_softirq(HI_SOFTIRQ,tasklet_hi_action);

2.3 處理tasklet

所有已註冊的tasklet也都存放在per-cpu的連結串列中,HI_SOFTIRQ 和TASKLET_SOFTIRQ 兩種tasklet連結串列的連結串列頭分別記錄在tasklet_hi_vec和tasklet_vec 中,用來找到對應連結串列。連結串列元素為struct tasklet_struct型別。

static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
 
struct tasklet_struct
{
    struct tasklet_struct *next;
    unsigned long state;
    atomic_t count;
    void (*func)(unsigned long);
    unsigned long data;
};

tasklet是在軟中斷之上實現的,二者區別為:

軟中斷:編譯時靜態分配,同一型別的軟中斷可以併發執行在多個CPU,所以處理函式必須是可重入的且必須使用自旋鎖保護其資料結構。

tasklet:分配和初始化可以在執行時進行(如安裝核心模組),相同型別的tasklet總是被序列的執行,即不能在兩個CPU上同時執行相同型別的tasklet,所以tasklet函式不必是可重入的。

幾個tasklet可以同時與一個軟中斷號相關聯,各自執行自己的函式,開發者可以在核心執行過程中註冊自己的tasklet,而不需要預先定義好所有的tasklet型別。

註冊一個tasklet:

tasklet_init(structtasklet_struct *t,
        void (*func)(unsigned long), unsigned long data);

第一個引數t是一個指向tasklet結構體的指標,第二個引數給t->func賦值,第三個引數是要傳給func的引數。

也可以方便的使用巨集DECLARE_TASKLET(name, func, data)來註冊tasklet。

啟用tasklet:即把tasklet加入到處理列表中,兩種tasklet分別用下面的兩個函式啟用:

tasklet_schedule(struct tasklet_struct *t);

tasklet_hi_schedule(struct tasklet_struct *t);

兩種tasklet都接收一個tasklet_struct引數,不需要軟中斷號,因為是固定的(HI_SOFTIRQ或TASKLET_SOFTIRQ)。對比一下啟用softirq的函式:

raise_softirq(unsigned int nr);

啟用softirq要傳入nr,不需要傳入其他內容,因為nr和處理函式是一一對應並且是預先定義好的而不是tasklet的一對多的關係。

tasklet_schedule和tasklet_hi_schedule函式做的事情基本相同:

1.   如果待啟用的tasklet物件的狀態已經是TASKLET_STATE_SCHED了,則已經被啟用了,所以直接返回。

2.   將tasklet物件的狀態的TASKLET_STATE_SCHED標記標記位置位,將tasklet物件新增到tasklet_hi_vec或tasklet_vec連結串列的末尾,並啟用HI_SOFTIRQ或TASKLET_SOFTIRQ軟中斷。

tasklet_schedule()和tasklet_hi_schedule()啟用tasklet的區別就是前者啟用的tasklet處於軟中斷優先順序最低的位置,其處理函式要等一段時間才能執行,後者啟用的tasklet處於軟中斷優先順序最高的位置,在啟用後很快就可以看到其處理函式被執行。

還有一個函式tasklet_hi_schedule_first()是將tasklet物件新增到連結串列的開頭。

先要說一下struct tasklet_struct結構體的state和count欄位:

state欄位有兩個標記位:

       TASKLET_STATE_SCHED:該標記被設定時,表示tasklet是掛起的(被tasklet_schedule或tasklet_hi_schedule過),即被插入到了tasklet_hi_vec或tasklet_vec連結串列中。

       TASKLET_STATE_RUN:該標記被設定時,表示tasklet正在被執行。在單處理器系統上不使用這個標誌。

count:標識相應tasklet物件是否被禁止,count的值為0時表示tasklet可以正常啟用並執行。有時可能想禁止一個tasklet,就可以用tasklet_disable_nosync()或tasklet_disable ()函式增加count的值,兩個函式的區別為:後者必須要等到tasklet已經執行的例項結束後(state的TASKLET_STATE_RUN標記位清零)才返回。重新使能一個tasklet使用tasklet_enable()函式。

處理tasklet:

我們知道tasklet是在軟中斷基礎上實現的,在啟用tasklet之後,ksoftirqd核心執行緒處理軟中斷的時候就會去呼叫tasklet_action()和tasklet_hi_action()函式去處理掛起的tasklet例項。兩個函式的實現基本大同小異,我們只看一下tasklet_action()的實現:

static void tasklet_action(struct softirq_action *a)
{
    struct tasklet_struct *list;
 
    local_irq_disable();
    /* 從tasklet_vec列表中取出列表頭 */
    list = __get_cpu_var(tasklet_vec).head;
    /* 把tasklet_vec列表清空,即*tail = head = NULL */
    __get_cpu_var(tasklet_vec).head = NULL;
    __get_cpu_var(tasklet_vec).tail =&__get_cpu_var(tasklet_vec).head;
    local_irq_enable();
 
    /* 遍歷tasklet_vec列表*/
    while (list) {
       /* 獲取一個tasklet_struct例項*/
       struct tasklet_struct *t = list;
       list = list->next;
 
       /* 如果tasklet的state已經是RUN了,比如有其他CPU在用,則跳過該tasklet例項 */
       if (tasklet_trylock(t)) {
           /* 如果tasklet的count不為0,即被disable了,則跳過該tasklet例項*/
           if (!atomic_read(&t->count)) {
              if (!test_and_clear_bit(TASKLET_STATE_SCHED,&t->state))
                  BUG();
              /* 執行tasklet的處理函式 */
              t->func(t->data);
              tasklet_unlock(t); /* 清掉TASKLET_STATE_RUN標記 */
              continue; //接著取list中的下一個tasklet。
           }
           tasklet_unlock(t); /* 清掉TASKLET_STATE_RUN標記 */
       }
 
       /* 如果上面因為state或count而沒有處理tasklet,則把這個未處理的tasklet節點重新放回tasklet_vec列表中。 */
       local_irq_disable();
       /* 放到末尾,tail指向最後一個節點的next */
       t->next = NULL;
       *__get_cpu_var(tasklet_vec).tail = t;
       __get_cpu_var(tasklet_vec).tail =&(t->next);
       /* 重新放回列表後,將相應軟中斷的softirq_pending位置1,下次處理的時候就可以看到了 */
       __raise_softirq_irqoff(TASKLET_SOFTIRQ);
       local_irq_enable();
    }
}

tasklet_action()函式的流程如下:

1. 先把tasklet_vec連結串列賦值給一個臨時變數list,然後清空連結串列,使tasklet可以重新被啟用。

2. 從list中獲取一個tasket,如果state的TASKLET_STATE_RUN被置位或count!=0,即tasklet例項的func函式可能在其他CPU上正在被執行,或者被禁用了,則跳到第4步。

3.如果state的TASKLET_STATE_SCHED被置位,說明可以被執行了,將state的TASKLET_STATE_RUN置位以防止其他CPU嘗試執行,如果檢查count值發現沒有被禁用,就清掉TASKLET_STATE_SCHED標記以允許tasklet被重新排程,接著開始執行func函式,執行完後將state的TASKLET_STATE_RUN位清除來允許其他CPU執行此tasklet。

4. 如果第2步中tasklet的func因為state或count沒有被執行,將tasklet重新加入到連結串列末尾,啟用TASKLET_SOFTIRQ軟中斷,如果上面由於tasklet在其他CPU上執行,通過重新啟用,可以達到延遲執行的效果,或者響應重新使能的tasket。

5. 獲取list中的下一個tasklet,繼續2-4步,直到結束遍歷。

tasklet_hi_action()和tasklet_action()的過程相同,只是遍歷的連結串列不同。

2.4 對中斷上下文的理解

本文一開始提到過“中斷上下文”表示核心當前正在執行一箇中斷處理程式或一個可延遲函式。

為了保證中斷上下文中的內容可以儘快執行,在中斷上下文中不允許睡眠,因為睡眠就意味著可能發生程式切換。在schedule()函式中也會做這樣的判斷,如果在schedule()的時候正處於中斷上下文,會引發一個錯誤。

在需要處理軟中斷時,有兩種方式可以執行軟中斷處理函式,一個是硬體中斷返回時檢查是否有掛起的軟中斷,一個是ksoftirqd自身排程執行軟中斷。前者是在中斷上下文中執行的,後者是在程式上下文執行的。所以在do_softirq中的任何程式碼,都必須考慮中斷上下文中執行時可能會出現的問題。所以在軟中斷(包括tasklet)處理函式中也是不允許睡眠的。

在程式的preempt_count中,硬體中斷計數器、軟中斷計數器和nmi標記共同標識了當前是否處在中斷上下文中。

通過local_irq_save()或local_irq_disable()禁止中斷的結果為關閉了核心的全域性中斷,通過irqs_disabled()可以判斷當前是否關閉了全域性中斷。但是這樣禁止中斷並沒有操作preemp_count,所以對in_interrupt(),in_irq(), in_nmi()沒有影響。因此,在local_irq_save()之後進行睡眠,核心仍然會正常執行,當然不建議這樣做。

由上可知,執行可睡眠(阻塞)函式的唯一方式是在程式上下文中執行。

3. 工作佇列

使用工作佇列也可以使核心函式被啟用並在稍後由核心中一個特定的執行緒來執行,因此工作佇列可以實現任務的延遲執行,但是它和可延遲函式又有區別:可延遲函式執行在中斷上下文,而工作佇列中的函式是由核心執行緒來執行的,因此工作在程式上下文。這樣一來,工作列隊中的任務可以被排程和睡眠。

每個工作佇列都通過一個執行緒來執行,核心中有一個預定義的工作佇列keventd_wq,對應的執行緒名為“[events/n]”,每建立一個工作佇列都會在系統中每個CPU上都建立一個相應的執行緒,系統中所有的CPU都可以執行掛載到佇列上的函式。

使用工作佇列非常簡單,只需將需要稍後執行的函式插入工作佇列即可。如果使用預定義的工作佇列keventd_wq,則只需兩步:

1.   初始化一個work_struct結構ws,並指定將要執行的函式func:

static struct work_struct ws;

INIT_WORK(&ws, fun);

注意,其中func函式的原型為:

typedef void (*work_func_t)(structwork_struct *work);

2.   將ws新增到工作佇列中等待執行:

       schedule_work(&ws);

之後核心執行緒[events/n]便會執行到該函式。如果需要不斷的執行func,可以在func函式的末尾重新呼叫schedule_work(&ws)。

核心還提供了schedule_delayed_work()函式可以讓函式在指定時間之後執行,使用方法為:

1.   初始化一個delayed_work結構ws,並指定將要執行的函式func:

static struct delayed_work dw;

INIT_DELAYED_WORK (&dw, func);

2.   將dw新增到工作佇列中等待執行:

schedule_delayed_work(&dw, HZ /10);

第二個引數為需要等待的jiffies數。

在一個函式被新增到工作佇列中,可以通過cancel_work_sync(work)或cancel_delayed_work_sync(dwork)來刪除它,但如果這時函式已經在執行,這兩個cancel函式會阻塞直到函式執行完畢。

如果需要等待某個函式執行完成,例如解除安裝一個模組時必須確保與其相關的工作佇列中的函式都被執行完,可以使用flush_work(work),該函式會阻塞直到掛起的函式執行完畢,如果需要等待工作佇列keventd_wq上所有函式執行完,可以使用flush_scheduled_work()函式。但是這兩個flush函式不會等待呼叫flush_work(work)或flush_scheduled_work()之後被重新加入工作佇列的掛起函式。

在定義work_struct或delayed_work結構時,除了可以用INIT_WORK()和INIT_DELAYED_WORK(),還可以使用DECLARE_WORK()和DECLARE_DELAYED_WORK()兩個巨集來靜態的定義。如:

static DECLARE_WORK(button_work, do_button_work);

上面的函式都是針對核心預定義的工作佇列keventd_wq。當函式很少被呼叫時,預定義的工作佇列節省了系統資源,另一方面,由於工作佇列連結串列中的掛起函式是在每個CPU上以序列的方式執行的,所以不應該使預定義工作佇列中執行的函式長時間處於阻塞狀態,否則會影響工作佇列中其他函式。

使用者可以定義自己的工作佇列,方法也很簡單:

1.   建立自定義工作佇列:

static struct workqueue_struct * wq;

wq = create_workqueue("new_wq");

其中引數“new_wq”是工作佇列的名字。建立成功後,會建立出per-cpu的核心執行緒[new_wq/n]。

2.   初始化一個work_struct結構ws,並指定將要執行的函式func:

INIT_WORK(&ws,func);

3.   將ws新增到工作佇列中等待執行:

queue_work(wq,&ws);

 同樣可以指定延遲執行的時間,初始化介面為INIT_DELAYED_WORK(dw, func),將待執行函式新增到工作佇列的介面為queue_delayed_work(wq, dw, delay)。

flush工作佇列的介面為flush_workqueue(wq)。

我們還可以銷燬自定義的工作佇列:destroy_workqueue(wq)。

4. 下半部機制的選擇

當我們需要延遲執行一個任務時,可以使用下半部機制,如果需要比較精確的延遲,可以使用可指定延遲的工作佇列,當然也可以使用核心定時器。

如果函式中需要申請某些資源並且可能導致阻塞,比如等待訊號量或執行阻塞I/O操作,那就要用工作佇列。

核心中不建議新增軟中斷了,所以如果可延遲函式不需要睡眠,就儘量使用tasklet來實現,並且tasklet在多處理器中不必實現為可重入函式,降低了實現難度。而softirq有較強的加鎖需求,僅僅在一些對效能敏感的子系統(如網路層、SCSI層和核心定時器)中才會使用softirq,因此,除非有嚴格的可擴充套件性和速度要求,都建議使用tasklet。

相關文章