linux中斷源碼分析 - 軟中斷(四)


本文為原創,轉載請注明:http://www.cnblogs.com/tolimit/

 

  在上一篇文章中,我們看到中斷實際分為了兩個部分,俗稱就是一部分是硬中斷,一部分是軟中斷。軟中斷是專門用於處理中斷過程中費時費力的操作,而為什么系統要分硬中斷和軟中斷呢?問得明白點就是為什么需要軟中斷。我們可以試着想想,如果只有硬中斷的情況下,我們需要在中斷處理過程中執行一些耗時的操作,比如浮點數運算,復雜算法的運算時,其他的外部中斷就不能得到及時的響應,因為在硬中斷過程中中斷是關閉着的,甚至一些很緊急的中斷會得不到響應,系統穩定性和及時性都受到很大影響。所以linux為了解決上述這種情況,將中斷處理分為了兩個部分,硬中斷和軟中斷。首先一個外部中斷得到響應時,會先關中斷,並進入到硬中斷完成較為緊急的操作,然后開中斷,並在軟中斷執行那些非緊急、可延時執行的操作;在這種情況下,緊急操作可以立即執行,而其他的外部中斷也可以獲得一個較為快速的響應。這也是軟中斷存在的必要性。在軟中斷過程中是不可以被搶占也不能被阻塞的,也不能在一個給定的CPU上交錯執行。

 

軟中斷

  軟中斷是在中斷框架中專門用於處理非緊急操作的,在SMP系統中,軟中斷可以並發地運行在多個CPU上,但在一些路徑在需要使用自旋鎖進行保護。在系統中,很多東西都分優先級,軟中斷也不例外,有些軟中斷要求更快速的響應運行,在內核中軟中斷一共分為10個,同時也代表着10種不同的優先級,系統用一個枚舉變量表示:

enum
{
    HI_SOFTIRQ=0,                     /* 高優先級tasklet */                              /* 優先級最高 */
    TIMER_SOFTIRQ,                    /* 時鍾相關的軟中斷 */
    NET_TX_SOFTIRQ,                   /* 將數據包傳送到網卡 */
    NET_RX_SOFTIRQ,                   /* 從網卡接收數據包 */
    BLOCK_SOFTIRQ,                    /* 塊設備的軟中斷 */
    BLOCK_IOPOLL_SOFTIRQ,             /* 支持IO輪詢的塊設備軟中斷 */
    TASKLET_SOFTIRQ,                  /* 常規tasklet */
    SCHED_SOFTIRQ,                    /* 調度程序軟中斷 */
    HRTIMER_SOFTIRQ,                  /* 高精度計時器軟中斷 */
    RCU_SOFTIRQ,                      /* RCU鎖軟中斷,該軟中斷總是最后一個軟中斷 */       /* 優先級最低 */

    NR_SOFTIRQS                       /* 軟中斷數,為10 */
};

  注釋中的tasklet我們之后會說明,這里先無視它。每一個優先級的軟中斷都使用一個struct softirq_action結構來表示,在這個結構中,只有一個成員變量,就是action函數指針,因為不同的軟中斷它的處理方式可能不同,從優先級表中就可以看出來,有塊設備的,也有網卡處理的。系統將這10個軟中斷用softirq_vec[10]的數組進行保存。

/* 用於描述一個軟中斷 */
struct softirq_action
{
    /* 此軟中斷的處理函數 */        
    void    (*action)(struct softirq_action *);
};

/* 10個軟中斷描述符都保存在此數組 */
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

  系統一般使用open_softirq()函數進行軟中斷描述符的初始化,主要就是將action函數指針指向該軟中斷應該執行的函數。在start_kernel()進行系統初始化中,就調用了softirq_init()函數對HI_SOFTIRQ和TASKLET_SOFTIRQ兩個軟中斷進行了初始化

void __init softirq_init(void)
{
    int cpu;

    for_each_possible_cpu(cpu) {
        per_cpu(tasklet_vec, cpu).tail =
            &per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail =
            &per_cpu(tasklet_hi_vec, cpu).head;
    }

    /* 開啟常規tasklet */
    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    /* 開啟高優先級tasklet */
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}


/* 開啟軟中斷 */
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

   可以看到,TASKLET_SOFTIRQ的action操作使用了tasklet_action()函數,HI_SOFTIRQ的action操作使用了tasklet_hi_action()函數,這兩個函數我們需要結合tasklet進行說明。我們也可以看看其他的軟中斷使用了什么函數:

    open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
    open_softirq(NET_TX_SOFTIRQ, net_tx_action);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action);
    open_softirq(BLOCK_SOFTIRQ, blk_done_softirq);
    open_softirq(BLOCK_IOPOLL_SOFTIRQ, blk_iopoll_softirq);
    open_softirq(SCHED_SOFTIRQ, run_rebalance_domains);
    open_softirq(HRTIMER_SOFTIRQ, run_hrtimer_softirq);
    open_softirq(RCU_SOFTIRQ, rcu_process_callbacks);

   其實很明顯可以看出,除了TASKLET_SOFTIRQ和HI_SOFTIRQ,其他的軟中斷更多地是用於特定的設備和環境,對於我們普通的IO驅動和設備而已,使用的軟中斷幾乎都是TASKLET_SOFTIRQ和HI_SOFTIRQ,而系統為了對這些不同IO設備進行統一的處理,就在TASKLET_SOFTIRQ和HI_SOFTIRQ的action函數中使用到了tasklet。

  對於每個CPU,都有一個irq_cpustat_t的數據結構,里面有一個__softirq_pending變量,這個變量很重要,用於表示該CPU的哪個軟中斷處於掛起狀態,在軟中斷處理時可以根據此值跳過不需要處理的軟中斷,直接處理需要處理的軟中斷。內核使用local_softirq_pending()獲取此CPU的__softirq_pending的值。

  當使用open_softirq設置好某個軟中斷的action指針后,該軟中斷就會開始可以使用了,其實更明了地說,從中斷初始化完成開始,即使所有的軟中斷都沒有使用open_softirq()進行初始化,軟中斷都已經開始使用了,只是所有軟中斷的action都為空,系統每次執行到軟中斷都沒有軟中斷需要執行罷了。

  在每個CPU上一次軟中斷處理的一個典型流程是:

  1. 硬中斷執行完畢,開中斷。
  2. 檢查該CPU是否處於嵌套中斷的情況,如果處於嵌套中,則不執行軟中斷,也就是在最外層中斷才執行軟中斷。
  3. 執行軟中斷,設置一個軟中斷執行最多使用時間和循環次數(10次)。
  4. 進入循環,獲取CPU的__softirq_pending的副本。
  5. 執行此__softirq_pending副本中所有需要執行的軟中斷。
  6. 如果軟中斷執行完畢,退出中斷上下文。
  7. 如果還有軟中斷需要執行(在軟中斷期間又發發生了中斷,產生了新的軟中斷,新的軟中斷記錄在CPU的__softirq_pending上,而我們的__softirq_pending只是個副本)。
  8. 檢查此次軟中斷總共使用的時間和循環次數,條件允許繼續執行軟中斷,循環次數減一,並跳轉到第4步。

  我們具體看一下代碼,首先在irq_exit()中會檢查是否需要進行軟中斷處理:

void irq_exit(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
    local_irq_disable();
#else
    WARN_ON_ONCE(!irqs_disabled());
#endif

    account_irq_exit_time(current);
    /* 減少preempt_count的硬中斷計數器 */
    preempt_count_sub(HARDIRQ_OFFSET);
    
    /* in_interrupt()會檢查preempt_count上的軟中斷計數器和硬中斷計數器來判斷是否處於中斷嵌套中 */
    /* local_softirq_pending()則會檢查該CPU的__softirq_pending變量,是否有軟中斷掛起 */
    if (!in_interrupt() && local_softirq_pending())
        invoke_softirq();

    tick_irq_exit();
    rcu_irq_exit();
    trace_hardirq_exit(); /* must be last! */
}

 

 

  我們再進入到invoke_softirq():

static inline void invoke_softirq(void)
{

    if (!force_irqthreads) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
        /*
         * We can safely execute softirq on the current stack if
         * it is the irq stack, because it should be near empty
         * at this stage.
         */
        /* 軟中斷處理函數 */
        __do_softirq();
#else
        /*
         * Otherwise, irq_exit() is called on the task stack that can
         * be potentially deep already. So call softirq in its own stack
         * to prevent from any overrun.
         */
        do_softirq_own_stack();
#endif
    } else {
        /* 如果強制使用軟中斷線程進行軟中斷處理,會通知調度器喚醒軟中斷線程ksoftirqd */
        wakeup_softirqd();
    }
}

 

  重頭戲就在__do_softirq()中,我已經注釋好了,方便大家看:

 1 asmlinkage __visible void __do_softirq(void)
 2 {
 3     /* 為了防止軟中斷執行時間太長,設置了一個軟中斷結束時間 */
 4     unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
 5     /* 保存當前進程的標志 */
 6     unsigned long old_flags = current->flags;
 7     /* 軟中斷循環執行次數: 10次 */
 8     int max_restart = MAX_SOFTIRQ_RESTART;
 9     /* 軟中斷的action指針 */
10     struct softirq_action *h;
11     bool in_hardirq;
12     __u32 pending;
13     int softirq_bit;
14 
15     /*
16      * Mask out PF_MEMALLOC s current task context is borrowed for the
17      * softirq. A softirq handled such as network RX might set PF_MEMALLOC
18      * again if the socket is related to swap
19      */
20     current->flags &= ~PF_MEMALLOC;
21 
22     /* 獲取此CPU的__softirq_pengding變量值 */
23     pending = local_softirq_pending();
24     /* 用於統計進程被軟中斷使用時間 */
25     account_irq_enter_time(current);
26 
27     /* 增加preempt_count軟中斷計數器,也表明禁止了調度 */
28     __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
29     in_hardirq = lockdep_softirq_start();
30 
31 /* 循環10次的入口,每次循環都會把所有掛起需要執行的軟中斷執行一遍 */
32 restart:
33     /* 該CPU的__softirq_pending清零,當前的__softirq_pending保存在pending變量中 */
34     /* 這樣做就保證了新的軟中斷會在下次循環中執行 */
35     set_softirq_pending(0);
36 
37     /* 開中斷 */
38     local_irq_enable();
39 
40     /* h指向軟中斷數組頭 */
41     h = softirq_vec;
42 
43     /* 每次獲取最高優先級的已掛起軟中斷 */
44     while ((softirq_bit = ffs(pending))) {
45         unsigned int vec_nr;
46         int prev_count;
47         /* 獲取此軟中斷描述符地址 */
48         h += softirq_bit - 1;
49         
50         /* 減去軟中斷描述符數組首地址,獲得軟中斷號 */
51         vec_nr = h - softirq_vec;
52         /* 獲取preempt_count的值 */
53         prev_count = preempt_count();
54 
55         /* 增加統計中該軟中斷發生次數 */
56         kstat_incr_softirqs_this_cpu(vec_nr);
57 
58         trace_softirq_entry(vec_nr);
59         /* 執行該軟中斷的action操作 */
60         h->action(h);
61         trace_softirq_exit(vec_nr);
62 
63         /* 之前保存的preempt_count並不等於當前的preempt_count的情況處理,也是簡單的把之前的復制到當前的preempt_count上,這樣做是防止最后軟中斷計數不為0導致系統不能夠執行調度 */
64         if (unlikely(prev_count != preempt_count())) {
65             pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
66                    vec_nr, softirq_to_name[vec_nr], h->action,
67                    prev_count, preempt_count());
68             preempt_count_set(prev_count);
69         }
70         /* h指向下一個軟中斷,但下個軟中斷並不一定需要執行,這里只是配合softirq_bit做到一個處理 */
71         h++;
72         pending >>= softirq_bit;
73     }
74 
75     rcu_bh_qs();
76     /* 關中斷 */
77     local_irq_disable();
78 
79     /* 循環結束后再次獲取CPU的__softirq_pending變量,為了檢查是否還有軟中斷未執行 */
80     pending = local_softirq_pending();
81     /* 還有軟中斷需要執行 */
82     if (pending) {
83         /* 在還有軟中斷需要執行的情況下,如果時間片沒有執行完,並且循環次數也沒到10次,繼續執行軟中斷 */
84         if (time_before(jiffies, end) && !need_resched() &&
85             --max_restart)
86             goto restart;
87         /* 這里是有軟中斷掛起,但是軟中斷時間和循環次數已經用完,通知調度器喚醒軟中斷線程去執行掛起的軟中斷,軟中斷線程是ksoftirqd,這里只起到一個通知作用,因為在中斷上下文中是禁止調度的 */
88         wakeup_softirqd();
89     }
90 
91     lockdep_softirq_end(in_hardirq);
92     /* 用於統計進程被軟中斷使用時間 */
93     account_irq_exit_time(current);
94     /* 減少preempt_count中的軟中斷計數器 */
95     __local_bh_enable(SOFTIRQ_OFFSET);
96     WARN_ON_ONCE(in_interrupt());
97     /* 還原進程標志 */
98     tsk_restore_flags(current, old_flags, PF_MEMALLOC);
99 }

  流程就和上面所說的一致,如果還有不懂,可以去內核代碼目錄/kernel/softirq.c查看源碼。

 

 

tasklet

  軟中斷有多種,部分種類有自己特殊的處理,如從NET_TX_SOFTIRQ和NET_RT_SOFTIRQ、BLOCK_SOFTIRQ等,而如HI_SOFTIRQ和TASKLET_SOFTIRQ則是專門使用tasklet。它是在I/O驅動程序中實現可延遲函數的首選方法,如上一句所說,它建立在HI_SOFTIRQ和TASKLET_SOFTIRQ這兩種軟中斷之上,多個tasklet可以與同一個軟中斷相關聯,系統會使用一個鏈表組織他們,而每個tasklet執行自己的函數處理。而HI_SOFTIRQ和TASKLET_SOFTIRQ這兩個軟中斷並沒有什么區別,他們只是優先級上的不同而已,系統會先執行HI_SOFTIRQ的tasklet,再執行TASKLET_SOFTIRQ的tasklet。同一個tasklet不能同時在幾個CPU上執行,一個tasklet在一個時間上只能在一個CPU的軟中斷鏈上,不能同時在多個CPU的軟中斷鏈上,並且當這個tasklet正在執行時,其他CPU不能夠執行這個tasklet。也就是說,tasklet不必要編寫成可重入的函數。

  系統會為每個CPU維護兩個鏈表,用於保存HI_SOFTIRQ的tasklet和TASKLET_SOFTIRQ的tasklet,這兩個鏈表是tasklet_vec和tasklet_hi_vec,它們都是雙向鏈表,如下:

struct tasklet_head {
    struct tasklet_struct *head;
    struct tasklet_struct **tail;
};

static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

   在softirq_init()函數中,會將每個CPU的tasklet_vec鏈表和tasklet_hi_vec鏈表進行初始化,將他們的頭尾相連,實現為一個空鏈表。由於tasklet_vec和tasklet_hi_vec處理方式幾乎一樣,只是軟中斷的優先級別不同,我們只需要理解系統如何對tasklet_vec進行處理即可。需要注意的是,tasklet_vec鏈表都是以順序方式執行,並不會出現后一個先執行,再到前一個先執行(在軟中斷期間被中斷的情況),之后的代碼我們詳細說明。

  介紹完tasklet_vec和tasklet_hi_vec鏈表,我們來看看tasklet,tasklet簡單來說,就是一個處理函數的封裝,類似於硬中斷中的irqaction結構。一般來說,在一個驅動中如果需要使用tasklet進行軟中斷的處理,只需要一個中斷對應初始化一個tasklet,它可以在每次中斷產生時重復使用。系統使用tasklet_struct結構進行描述一個tasklet,而且對於同一個tasklet_struct你可以選擇放在tasklet_hi_vec鏈表或者tasklet_vec鏈表上。我們來看看:

struct tasklet_struct
{
    struct tasklet_struct *next;      /* 指向鏈表下一個tasklet */
    unsigned long state;              /* tasklet狀態 */
    atomic_t count;                   /* 禁止計數器,調用tasklet_disable()會增加此數,tasklet_enable()減少此數 */
    void (*func)(unsigned long);      /* 處理函數 */
    unsigned long data;               /* 處理函數使用的數據 */
};

  tasklet狀態主要分為以下兩種:

  • TASKLET_STATE_SCHED:這種狀態表示此tasklet處於某個tasklet鏈表之上(可能是tasklet_vec也可能是tasklet_hi_vec)。
  • TASKLET_STATE_RUN:表示此tasklet正在運行中。

  這兩個狀態主要就是用於防止tasklet同時在幾個CPU上運行和在同一個CPU上交錯執行。

 

  而func指針就是指向相應的處理函數。在編寫驅動時,我們可以使用tasklet_init()函數或者DECLARE_TASKLET宏進行一個task_struct結構的初始化,之后可以使用tasklet_schedule()或者tasklet_hi_schedule()將其放到相應鏈表上等待CPU運行。我們使用一張圖描述一下軟中斷和tasklet結合運行的情況:

 

  我們知道,每個軟中斷都有自己的action函數,在HI_SOFTIRQ和TASKLET_SOFTIRQ的action函數中,就用到了它們對應的TASKLET_HI_VEC鏈表和TASKLET_VEC鏈表,並依次順序執行鏈表中的每個tasklet結點。

  在SMP系統中,我們會遇到一個問題:兩個CPU都需要執行同一個tasklet的情況,雖然一個tasklet只能放在一個CPU的tasklet_vec鏈表或者tasklet_hi_vec鏈表上,但是這種情況是有可能發生的,我們設想一下,中斷在CPU1上得到了響應,並且它的tasklet放到了CPU1的tasklet_vec上進行執行,而當中斷的tasklet上正在執行時,此中斷再次發生,並在CPU2上進行了響應,此時CPU2將此中斷的tasklet放到CPU2的tasklet_vec上,並執行到此中斷的tasklet。

  實際上,為了處理這種情況,在HI_SOFTIRQ和TASKLET_SOFTIRQ的action函數中,會先將對應的tasklet鏈表取出來,並把對應的tasklet鏈表的head和tail清空,如果在執行過程中,某個tasklet的state為TASKLET_STATE_RUN狀態,說明其他CPU正在處理這個tasklet,這時候當前CPU則會把此tasklet加入到當前CPU已清空的tasklet鏈表的末尾,然后設置__softirq_pending變量,這樣,在下次循環軟中斷的過程中,會再次檢查這個tasklet。也就是如果其他CPU的這個tasklet一直不退出,當前CPU就會不停的置位tasklet的pending,然后不停地循環檢查。

  我們可以看看TASKLET_SOFTIRQ的action處理:

 1 static void tasklet_action(struct softirq_action *a)
 2 {
 3     struct tasklet_struct *list;
 4 
 5     local_irq_disable();
 6     /* 將tasklet鏈表從該CPU中拿出來 */
 7     list = __this_cpu_read(tasklet_vec.head);
 8     /* 將該CPU的此軟中斷的tasklet鏈表清空 */
 9     __this_cpu_write(tasklet_vec.head, NULL);
10     __this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
11     local_irq_enable();
12 
13     /* 鏈表已經處於list中,並且該CPU的tasklet_vec鏈表為空 */
14     while (list) {
15         struct tasklet_struct *t = list;
16 
17         list = list->next;
18 
19         /* 檢查並設置該tasklet為TASKLET_STATE_RUN狀態 */
20         if (tasklet_trylock(t)) {
21             /* 檢查是否被禁止 */
22             if (!atomic_read(&t->count)) {
23                 /* 清除其TASKLET_STATE_SCHED狀態 */
24                 if (!test_and_clear_bit(TASKLET_STATE_SCHED,
25                             &t->state))
26                     BUG();
27                 /* 執行該tasklet的func處理函數 */
28                 t->func(t->data);
29                 /* 清除該tasklet的TASKLET_STATE_RUN狀態 */
30                 tasklet_unlock(t);
31                 continue;
32             }
33             tasklet_unlock(t);
34         }
35 
36         /* 以下為tasklet為TASKLET_STATE_RUN狀態下的處理 */
37         /* 禁止中斷 */
38         local_irq_disable();
39         /* 將此tasklet添加的該CPU的tasklet_vec鏈表尾部 */
40         t->next = NULL;
41         *__this_cpu_read(tasklet_vec.tail) = t;
42         __this_cpu_write(tasklet_vec.tail, &(t->next));
43         /* 設置該CPU的此軟中斷處於掛起狀態,設置irq_cpustat_t的__sofirq_pending變量,這樣在軟中斷的下次執行中會再次執行此tasklet */
44         __raise_softirq_irqoff(TASKLET_SOFTIRQ);
45         /* 開啟中斷 */
46         local_irq_enable();
47     }
48 }

 

  

 

軟中斷處理線程

  當有過多軟中斷需要處理時,為了保證進程能夠得到一個滿意的響應時間,設計時給定軟中斷一個時間片和循環次數,當時間片和循環次數到達但軟中斷又沒有處理完時,就會把剩下的軟中斷交給軟中斷處理線程進行處理,這個線程是一個內核線程,其作為一個普通進程,優先級是120。其核心處理函數是run_ksoftirqd(),其實此線程的處理也很簡單,就是調用了上面的__do_softirq()函數,我們可以具體看看:

 1 /* 在smpboot_thread_fun的一個死循環中被調用 */
 2 static void run_ksoftirqd(unsigned int cpu)
 3 {
 4     /* 禁止中斷,在__do_softirq()中會開啟 */
 5     local_irq_disable();
 6     /* 檢查該CPU的__softirq_pending是否有軟中斷被掛起 */
 7     if (local_softirq_pending()) {
 8         /*
 9          * We can safely run softirq on inline stack, as we are not deep
10          * in the task stack here.
11          */
12         /* 執行軟中斷 */
13         __do_softirq();
14         rcu_note_context_switch(cpu);
15         /* 開中斷 */
16         local_irq_enable();
17         /* 檢查是否需要調度 */
18         cond_resched();
19         return;
20     }
21     /* 開中斷 */
22     local_irq_enable();
23 }

 


免責聲明!

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



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