天天看點

linux軟中斷與硬中斷實作原理概述。

1、軟中斷通過open_softirq注冊一個軟中斷處理函數,即在軟中斷向量表softirq_vec數組中添加新的軟中斷處理action函數。

2、調用raised_softirq軟中斷觸發函數,即把軟中斷标記為挂起狀态。

核心會在一些位置檢查是否有挂起狀态的軟中斷,如果有的話會調用do_softirq函數來執行軟中斷處理函數。

3、do_softirq函數做的一個重要的工作是切換到軟中斷請求棧。切換到軟中斷請求棧就代表了此時已經處于了軟中斷上下文中了。

                 (1)切換到軟中斷請求棧(2)調用__do_softirq函數

4、__do_softirq函數

(1)開始執行軟中斷處理函數

(2)如果還有更多挂起的軟中斷沒有處理,則調用wakeup_softirq函數喚醒核心線程來處理本地CPU的軟中斷。該線程會循環調用do_softirq函數而不是在核心那些檢查軟中斷是否挂起的位置進行檢查,節省了時間。

是以軟中斷的本質就是在核心某些位置檢查是否有挂起的軟中斷(local_software_pending()不為0則表示有挂起的軟中斷),若有則調用do_softirq函數,在do_softirq函數中切換到軟中斷請求棧後,調用__do_softirq軟中斷回調函數。

硬中斷的本質是接收到中斷信号後,跳轉到公共段代碼執行do_IRQ,并切換到硬中斷請求棧,執行中斷回調函數。

        如果在每次中斷的時候才檢查是否有軟中斷需要執行的話,那麼由于兩次軟中斷的間隔的等待時間過長,這樣對于用軟中斷處理收發網絡包是不可取的,是以do_softirq可以連續檢查10次是否有需要執行的軟中斷,這樣處理軟中斷效率會比較高,之是以沒有讓do_softirq執行更多次的檢查是因為由于軟中斷優先級比較高是以會導緻使用者程式長時間得不到執行。解決這個平衡的方法是如果有超過10次的軟中觀需要處理則喚醒ksoftirqd線程,該線程會循環檢查是否有挂起的軟中斷,由于ksoftirqd線程的優先級低于使用者程序,是以不會影響使用者程序的執行。

《深入了解linux核心》 -P179,P180

以下文章轉自http://bbs.chinaunix.net/thread-2333484-1-1.html

作者:獨孤九賤

平台:2.6.31.13 + x86 32位

供僅讨論學習之處,不對錯誤之處負責,轉載請注明出處。

1、軟中斷

軟中斷的原理就略過了,講核心的書上都有,此處省略1500字。。。。。。

1.1 注冊

還是以我最熟悉的兩個老朋友做為開篇:

        open_softirq(NET_TX_SOFTIRQ, net_tx_action);

        open_softirq(NET_RX_SOFTIRQ, net_rx_action);

open_softirq向核心注冊一個軟中斷,其實質是設定軟中斷向量表相應槽位,注冊其處理函數:
  1. void open_softirq(int nr, void (*action)(struct softirq_action *))
  2. {
  3.         softirq_vec[nr].action = action;
  4. }
複制代碼 softirq_vec是整個軟中斷的向量表:
  1. struct softirq_action
  2. {
  3.         void        (*action)(struct softirq_action *);
  4. };
  5. static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
複制代碼 NR_SOFTIRQS是最大軟中斷向量數,核心支援的所有軟中斷如下:
  1. enum
  2. {
  3.         HI_SOFTIRQ=0,
  4.         TIMER_SOFTIRQ,
  5.         NET_TX_SOFTIRQ,
  6.         NET_RX_SOFTIRQ,
  7.         BLOCK_SOFTIRQ,
  8.         TASKLET_SOFTIRQ,
  9.         SCHED_SOFTIRQ,
  10.         HRTIMER_SOFTIRQ,
  11.         RCU_SOFTIRQ,       
  12.         NR_SOFTIRQS
  13. };

複制代碼 好像後為為RPS新增了一個,不過這我的核心版本偏低。

1.2 激活 

當需要調用軟中斷時,需要調用raise_softirq函數激活軟中斷,這裡使用術語“激活”而非“調用”,

是因為在很多情況下不能直接調用軟中斷。是以隻能快速地将其标志為“可執行”,等待未來某一時刻調用。

為什麼“在很多情況下不能直接調用軟中斷”?試想一下下半部引入的理念,就是為了讓上半部更快地執行。

如果在中斷程式代碼中直接調用軟中斷函數,那麼就失去了上半部與下半部的差別,也就是失去了其存在的意義。

核心使用一個名為__softirq_pending的位圖來描述軟中斷,每一個位對應一個軟中斷,位圖包含在結構irq_stat中:

  1. typedef struct {
  2.         unsigned int __softirq_pending;
  3.         ……
  4. } ____cacheline_aligned irq_cpustat_t;
  5. DECLARE_PER_CPU_SHARED_ALIGNED(irq_cpustat_t, irq_stat);
複制代碼 宏or_softirq_pending用于設定相應的位(位或操作):
  1. #define or_softirq_pending(x)        percpu_or(irq_stat.__softirq_pending, (x))
複制代碼 local_softirq_pending用于取得整個位圖(而非某一位):
  1. #define local_softirq_pending()        percpu_read(irq_stat.__softirq_pending)
複制代碼 宏__raise_softirq_irqoff是or_softirq_pending的包裹:
  1. #define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)
複制代碼 raise_softirq_irqoff通過調用__raise_softirq_irqoff實作激活軟中斷,它的參數nr即位軟中斷對應的位圖槽位:
  1. inline void raise_softirq_irqoff(unsigned int nr)
  2. {
  3.         //置位圖,即标記為可執行狀态
  4.         __raise_softirq_irqoff(nr);
  5.         //設定了位圖後,可以判斷是否已經沒有在中斷上下文中了,如果沒有,則是一個立即調用軟中斷的好時機。
  6.         //in_interrupt另一個作用是判斷軟中斷是否被禁用。
  7.         //wakeup_softirqd喚醒軟中斷的守護程序ksoftirq。
  8.         if (!in_interrupt())
  9.                 wakeup_softirqd();
  10. }
複制代碼 現在可以來看"激活"軟中斷的所有含義了,raise_softirq函數完成這一操作:
  1. void raise_softirq(unsigned int nr)
  2. {
  3.         unsigned long flags;
  4.         //所有操作,應該關閉中斷,避免嵌套調用
  5.         local_irq_save(flags);
  6.         raise_softirq_irqoff(nr);
  7.         local_irq_restore(flags);
  8. }

複制代碼 可見,激活的操作,主要是兩點:

<1>、最重要的,就是置相應的位圖,等待将來被處理;

<2>、如果此時已經沒有在中斷上下文中,則立即調用(其實是核心線程的喚醒操作),現在就是将來;

2、排程時機

是的,除了raise_softirq在,可能會(嗯,重要的是“可能”)通過wakeup_softirqd喚醒ksoftirqd外,還得明白軟中斷的其它調用時機。

A、當do_IRQ完成了I/O中斷時調用irq_exit:

  1. #ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
  2. # define invoke_softirq()        __do_softirq()
  3. #else
  4. # define invoke_softirq()        do_softirq()
  5. #endif
  6. void irq_exit(void)
  7. {
  8.         account_system_vtime(current);
  9.         trace_hardirq_exit();
  10.         sub_preempt_count(IRQ_EXIT_OFFSET);
  11.         if (!in_interrupt() && local_softirq_pending())
  12.                 invoke_softirq();                //調用軟中斷
複制代碼 B、如果系統使用I/O APIC,在處理完本地時鐘中斷時:
  1. void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs)
  2. {
  3.         ……
  4.         irq_exit();
  5.         ……
  6. }

複制代碼 C、local_bh_enable

local_bh_enable就是打開下半部,當然重中之中就是軟中斷了:

  1. void local_bh_enable(void)
  2. {
  3.         _local_bh_enable_ip((unsigned long)__builtin_return_address(0));
  4. }
  5. static inline void _local_bh_enable_ip(unsigned long ip)
  6. {
  7.         ……
  8.         if (unlikely(!in_interrupt() && local_softirq_pending()))
  9.                 do_softirq();
  10.         ……
  11. }

複制代碼 D、在SMP中,當CPU處理完被CALL_FUNCTION_VECTOR處理器間中斷所觸發的函數時:

唔,對多核中CPU的之間的通信不熟,不太清楚這個機制……

linux軟中斷與硬中斷實作原理概述。

3、do_softirq

不論是哪種調用方式,最終都會觸發到軟中斷的核心處理函數do_softirq,它處理目前CPU上的所有軟中斷。

核心将軟中斷設計盡量與平台無關,但是在某些情況下,它們還是會有差異,先來看一個x86 32位的do_softirq版本:

  1. asmlinkage void do_softirq(void)
  2. {
  3.         unsigned long flags;
  4.         struct thread_info *curctx;
  5.         union irq_ctx *irqctx;
  6.         u32 *isp;
  7.         //軟中斷不能在中斷上下文内嵌套調用。中斷處理程式或下半部采用的是"激活"方式。
  8.         if (in_interrupt())
  9.                 return;
  10.         //禁止中斷,儲存中斷标志
  11.         local_irq_save(flags);
  12.         //核心使用一個CPU位圖,确實幾個軟中斷可以同時在不同的CPU上運作,包括相同的軟中斷。例如,
  13.         //NET_RX_SOFTIRQ可以同時跑在多個處理器上。
  14.         //local_softirq_pending用于确定目前CPU的所有位圖是否被設定。即是否有軟中斷等待處理。
  15.         //回想一下經常發生的網卡接收資料處理:當網卡中斷落在哪一個CPU上時,與之相應的軟中斷函數就會在其上執行。
  16.         //從這裡來看,實質就是哪個網卡中斷落在相應的CPU上,CPU置其軟中斷位圖,這裡做相應的檢測(這裡local_softirq_pending隻
  17.         //是一個總的判斷,後面還有按位的判斷),檢測到有相應的位,執行之
  18.         if (local_softirq_pending()) {
  19.                 //取得線程描述符
  20.                 curctx = current_thread_info();
  21.                 //構造中斷上下文結構,softirq_ctx是每個CPU的軟中斷上下文
  22.                 //static DEFINE_PER_CPU(union irq_ctx *, softirq_ctx);
  23.                 //這裡先取得目前CPU的軟中斷上下文,然後為其賦初始值——儲存目前程序和棧指針
  24.                 irqctx = __get_cpu_var(softirq_ctx);
  25.                 irqctx->tinfo.task = curctx->task;
  26.                 irqctx->tinfo.previous_esp = current_stack_pointer;
  27.                 //構造中斷棧幀
  28.                 isp = (u32 *) ((char *)irqctx + sizeof(*irqctx));
  29.                 //call_on_stack切換核心棧,并在中斷上下文上執行函數__do_softirq
  30.                 call_on_stack(__do_softirq, isp);
  31.                 WARN_ON_ONCE(softirq_count());
  32.         }
  33.         //恢複之
  34.         local_irq_restore(flags);
  35. }

複制代碼 當配置了CONFIG_4KSTACKS,每個程序的thread_union隻有4K,而非8K。發生中斷時,核心棧将不使用程序的核心棧,而使用每個 cpu的中斷請求棧。

核心棧将使用每個 cpu的中斷請求棧,而非程序的核心棧來執行軟中斷函數:

  1. static void call_on_stack(void *func, void *stack)
  2. {
  3.         asm volatile("xchgl        %%ebx,%%esp        \n"                                //交換棧指針,中斷棧幀的指針stack做為傳入參數(%ebx),交換後esp是irq_ctx的棧頂,ebx是程序核心棧的棧
  4.                      "call        *%%edi                \n"                                        //調用軟中斷函數
  5.                      "movl        %%ebx,%%esp        \n"                                        //恢複之,直接使用movl,而非xchgl是因為函數執行完畢,中斷的棧幀指針已經沒有用處了
  6.                      : "=b" (stack)
  7.                      : "0" (stack),
  8.                        "D"(func)
  9.                      : "memory", "cc", "edx", "ecx", "eax");
  10. }
複制代碼 PS:所有的這些執行,應該都是在定義4K棧的基礎上的:
  1. #ifdef CONFIG_4KSTACKS
  2. union irq_ctx {
  3.         struct thread_info      tinfo;
  4.         u32                     stack[THREAD_SIZE/sizeof(u32)];
  5. } __attribute__((aligned(PAGE_SIZE)));
  6. static DEFINE_PER_CPU(union irq_ctx *, hardirq_ctx);
  7. static DEFINE_PER_CPU(union irq_ctx *, softirq_ctx);
  8. ……
  9. static void call_on_stack(void *func, void *stack)
  10. ……

複制代碼 是的,這個版本相對複雜,但是如果看了複雜的,再來看簡單的,就容易多了,當平台沒有定義do_softirq函數時(__ARCH_HAS_DO_SOFTIRQ),

核心提供了一個通用的:

  1. #ifndef __ARCH_HAS_DO_SOFTIRQ
  2. asmlinkage void do_softirq(void)
  3. {
  4.         __u32 pending;
  5.         unsigned long flags;
  6.         if (in_interrupt())
  7.                 return;
  8.         local_irq_save(flags);
  9.         pending = local_softirq_pending();
  10.         if (pending)
  11.                 __do_softirq();
  12.         local_irq_restore(flags);
  13. }
  14. #endif

複制代碼 無需更多的解釋,它非常的簡潔。

不論是哪個版本,都将調用__do_softirq函數:

  1. asmlinkage void __do_softirq(void)
  2. {
  3.         struct softirq_action *h;
  4.         __u32 pending;
  5.         int max_restart = MAX_SOFTIRQ_RESTART;
  6.         int cpu;
  7.         //儲存位圖
  8.         pending = local_softirq_pending();
  9.         //程序記帳
  10.         account_system_vtime(current);
  11.         //關閉本地CPU下半部。為了保證同一個CPU上的軟中斷以串行方式執行。
  12.         __local_bh_disable((unsigned long)__builtin_return_address(0));
  13.         lockdep_softirq_enter();
  14.         //擷取本地CPU
  15.         cpu = smp_processor_id();
  16. restart:
  17.         //清除位圖
  18.         set_softirq_pending(0);
  19.         //鎖中斷,隻是為了保持位圖的互斥,位圖處理完畢。後面的代碼可以直接使用儲存的pending,
  20.         //而中斷處理程式在激活的時候,也可以放心地使用irq_stat.__softirq_pending。
  21.         //是以,可以開中斷了
  22.         local_irq_enable();
  23.         //取得軟中斷向量
  24.         h = softirq_vec;
  25.         //循環處理所有的軟中斷
  26.         do {
  27.                 //逐漸取位圖的每一位,判斷該位上是否有軟中斷被設定。若有,處理之
  28.                 if (pending & 1) {
  29.                         //儲存搶占計數器
  30.                         int prev_count = preempt_count();
  31.                         kstat_incr_softirqs_this_cpu(h - softirq_vec);
  32.                         trace_softirq_entry(h, softirq_vec);
  33.                         //調用軟中斷
  34.                         h->action(h);
  35.                         trace_softirq_exit(h, softirq_vec);
  36.                         //判斷軟中斷是否被搶占,如果是,則輸出一段錯誤資訊
  37.                         if (unlikely(prev_count != preempt_count())) {
  38.                                 printk(KERN_ERR "huh, entered softirq %td %s %p"
  39.                                        "with preempt_count %08x,"
  40.                                        " exited with %08x?\n", h - softirq_vec,
  41.                                        softirq_to_name[h - softirq_vec],
  42.                                        h->action, prev_count, preempt_count());
  43.                                 preempt_count() = prev_count;
  44.                         }
  45.                         //??qsctr,這個是啥東東
  46.                         rcu_bh_qsctr_inc(cpu);
  47.                 }
  48.                 //指向下一個軟中斷槽位
  49.                 h++;
  50.                 //移位,取下一個軟中斷位
  51.                 pending >>= 1;
  52.         } while (pending);
  53.         //當軟中斷處理完畢後,因為前面已經開了中斷了,是以有可能新的軟中斷已經又被設定,
  54.         //軟中斷排程程式會嘗試重新軟中斷,其最大重新開機次數由max_restart決定。
  55.         //是以,這裡必須再次關閉中斷,再來一次……
  56.         local_irq_disable();
  57.         //取位圖
  58.         pending = local_softirq_pending();
  59.         //有軟中斷被設定,且沒有超過最大重新開機次數,再來一次先
  60.         if (pending && --max_restart)
  61.                 goto restart;
  62.         //超過最大重新開機次數,還有軟中斷待處理,調用wakeup_softirqd。其任處是喚醒軟中斷守護程序ksoftirqd。
  63.         if (pending)
  64.                 wakeup_softirqd();
  65.         lockdep_softirq_exit();
  66.         account_system_vtime(current);
  67.         //恢複下半部
  68.         _local_bh_enable();
  69. }

複制代碼 中斷跟蹤

如果中斷跟蹤CONFIG_TRACE_IRQFLAGS被定義,lockdep_softirq_enter/lockdep_softirq_exit用于遞增/遞減目前程序的軟中斷上下文計數器softirq_context:

  1. # define lockdep_softirq_enter()        do { current->softirq_context++; } while (0)
  2. # define lockdep_softirq_exit()        do { current->softirq_context--; } while (0)
複制代碼 trace_softirq_entry與trace_softirq_exit配合使用,可以用于判斷軟中斷的延遲。

繼續閱讀