淺析Linux的軟中斷的實現

weixin_33896726發表於2016-02-16

參考:

http://bbs.chinaunix.net/thread-2333484-1-1.html

http://liu1227787871.blog.163.com/blog/static/20536319720129210112658/


1、軟中斷

一般來說,一次中斷服務的過程通常能夠分為兩個部分。

開頭的 部分往往必須在關中斷的條件下執行,這樣才幹在不受干擾的條件下“原子”地完畢一些關鍵性操作。同一時候這部分操作的時間性又往往非常強。必須在中斷請求發生後馬上或至少在一定時間限制中執行,並且相繼的多次中斷請求也不能合併在一起來處理。

而後半部分,通常能夠並且應該在開中斷的條件下執行。這樣才不至於因中斷關閉過久而造成其它中斷的丟失,同一時候,這些操作經常同意延時到稍後才來執行。並且有可能多次中斷的相關部分合並在一起處理。

這些不同的性質經常使中斷服務的前後兩半明顯地區分開來,能夠並且應該分別加以不同的實現。這裡的後半部分就稱為"bottom half",在核心程式碼中往往寫成bf 。而bf的這部分就能夠通過軟體中斷來實現。

由於軟體中斷的啟用是通過程式碼來實現的,而不是硬體,所以就能夠自己或由系統來決定啟用的時機!

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


        open_softirq(NET_TX_SOFTIRQ, net_tx_action);
        open_softirq(NET_RX_SOFTIRQ, net_rx_action);


open_softirq向核心註冊一個軟中斷。事實上質是設定軟中斷向量表對應槽位。註冊其處理函式:

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

softirq_vec是整個軟中斷的向量表:

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

};


static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;


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

好像後為為RPS新增了一個。只是這我的核心版本號偏低。


1.2 啟用 


當須要呼叫軟中斷時,須要呼叫raise_softirq函式啟用軟中斷。這裡使用術語“啟用”而非“呼叫”。
是由於在非常多情況下不能直接呼叫軟中斷。

所以僅僅能高速地將其標誌為“可執行”。等待未來某一時刻呼叫。


為什麼“在非常多情況下不能直接呼叫軟中斷”?試想一下下半部引入的理念。就是為了讓上半部更快地執行。
假設在中斷程式程式碼中直接呼叫軟中斷函式,那麼就失去了上半部與下半部的差別,也就是失去了其存在的意義。


核心使用一個名為__softirq_pending的點陣圖來描寫敘述軟中斷,每個位相應一個軟中斷,點陣圖包括在結構irq_stat中:

typedef struct {
        unsigned int __softirq_pending;
        ……
} ____cacheline_aligned irq_cpustat_t;


DECLARE_PER_CPU_SHARED_ALIGNED(irq_cpustat_t, irq_stat);

巨集or_softirq_pending用於設定對應的位(位或操作):
#define or_softirq_pending(x)        percpu_or(irq_stat.__softirq_pending, (x))

local_softirq_pending用於取得整個點陣圖(而非某一位):
#define local_softirq_pending()        percpu_read(irq_stat.__softirq_pending)

巨集__raise_softirq_irqoff是or_softirq_pending的包裹:
#define __raise_softirq_irqoff(nr) do { or_softirq_pending(1UL << (nr)); } while (0)

raise_softirq_irqoff通過呼叫__raise_softirq_irqoff實現啟用軟中斷。它的引數nr即位軟中斷相應的點陣圖槽位:
/*
* This function must run with irqs disabled!
*/
inline void raise_softirq_irqoff(unsigned int nr)
{
        //置點陣圖,即標記為可執行狀態
        __raise_softirq_irqoff(nr);


        /*
         * If we're in an interrupt or softirq, we're done
         * (this also catches softirq-disabled code). We will
         * actually run the softirq once we return from
         * the irq or softirq.
         *
         * Otherwise we wake up ksoftirqd to make sure we
         * schedule the softirq soon.
         */
        //設定了點陣圖後。能夠推斷是否已經沒有在中斷上下文中了,假設沒有,則是一個馬上呼叫軟中斷的好時機。

//in_interrupt還有一個作用是推斷軟中斷是否被禁用。 //wakeup_softirqd喚醒軟中斷的守護程式ksoftirq。

if (!in_interrupt()) wakeup_softirqd(); } 複製程式碼 如今能夠來看"啟用"軟中斷的全部含義了,raise_softirq函式完畢這一操作: void raise_softirq(unsigned int nr) { unsigned long flags; //全部操作,應該關閉中斷,避免巢狀呼叫 local_irq_save(flags); raise_softirq_irqoff(nr); local_irq_restore(flags); }


可見,啟用的操作,主要是兩點:
<1>、最重要的,就是置對應的點陣圖。等待將來被處理;
<2>、假設此時已經沒有在中斷上下文中,則馬上呼叫(事實上是核心執行緒的喚醒操作),如今就是將來;


2、排程時機
是的。除了raise_softirq在,可能會(嗯,重要的是“可能”)通過wakeup_softirqd喚醒ksoftirqd外,還得明確軟中斷的其他呼叫時機。




A、當do_IRQ完畢了I/O中斷時呼叫irq_exit:

#ifdef __ARCH_IRQ_EXIT_IRQS_DISABLED
# define invoke_softirq()        __do_softirq()
#else
# define invoke_softirq()        do_softirq()
#endif


void irq_exit(void)
{
        account_system_vtime(current);
        trace_hardirq_exit();
        sub_preempt_count(IRQ_EXIT_OFFSET);
        if (!in_interrupt() && local_softirq_pending())
                invoke_softirq();                //呼叫軟中斷

B、假設系統使用I/O APIC,在處理完本地時鐘中斷時:
void __irq_entry smp_apic_timer_interrupt(struct pt_regs *regs)
{
        ……
        irq_exit();
        ……
}

C、local_bh_enable


local_bh_enable就是開啟下半部。當然重中之中就是軟中斷了:
void local_bh_enable(void)
{
        _local_bh_enable_ip((unsigned long)__builtin_return_address(0));
}


static inline void _local_bh_enable_ip(unsigned long ip)
{
        ……


        if (unlikely(!in_interrupt() && local_softirq_pending()))
                do_softirq();


        ……
}

D、在SMP中,當CPU處理完被CALL_FUNCTION_VECTOR處理器間中斷所觸發的函式時:
唔。對多核中CPU的之間的通訊不熟。不太清楚這個機制……

3、do_softirq


不論是哪種呼叫方式,終於都會觸發到軟中斷的核心處理函式do_softirq,它處理當前CPU上的全部軟中斷。
核心將軟中斷設計儘量與平臺無關。可是在某些情況下。它們還是會有差異,先來看一個x86 32位的do_softirq版本號:

asmlinkage void do_softirq(void)
{
        unsigned long flags;
        struct thread_info *curctx;
        union irq_ctx *irqctx;
        u32 *isp;


        //軟中斷不能在中斷上下文內巢狀呼叫。中斷處理程式或下半部採用的是"啟用"方式。
        if (in_interrupt())
                return;


        //禁止中斷。儲存中斷標誌
        local_irq_save(flags);
        //核心使用一個CPU點陣圖,確實幾個軟中斷能夠同一時候在不同的CPU上執行,包含同樣的軟中斷。比如,
        //NET_RX_SOFTIRQ能夠同一時候跑在多個處理器上。
        //local_softirq_pending用於確定當前CPU的全部點陣圖是否被設定。

即是否有軟中斷等待處理。

//回憶一下常常發生的網路卡接收資料處理:當網路卡中斷落在哪一個CPU上時,與之對應的軟中斷函式就會在其上執行。 //從這裡來看,實質就是哪個網路卡中斷落在對應的CPU上,CPU置其軟中斷點陣圖,這裡做對應的檢測(這裡local_softirq_pending僅僅 //是一個總的推斷。後面還有按位的推斷)。檢測到有對應的位,執行之 if (local_softirq_pending()) { //取得執行緒描寫敘述符 curctx = current_thread_info(); //構造中斷上下文結構,softirq_ctx是每一個CPU的軟中斷上下文 //static DEFINE_PER_CPU(union irq_ctx *, softirq_ctx); //這裡先取得當前CPU的軟中斷上下文,然後為其賦初始值——儲存當前程式和棧指標 irqctx = __get_cpu_var(softirq_ctx); irqctx->tinfo.task = curctx->task; irqctx->tinfo.previous_esp = current_stack_pointer; /* build the stack frame on the softirq stack */ //構造中斷棧幀 isp = (u32 *) ((char *)irqctx + sizeof(*irqctx)); //call_on_stack切換核心棧,並在中斷上下文上執行函式__do_softirq call_on_stack(__do_softirq, isp); /* * Shouldnt happen, we returned above if in_interrupt(): */ WARN_ON_ONCE(softirq_count()); } //恢復之 local_irq_restore(flags); }


當配置了CONFIG_4KSTACKS。每一個程式的thread_union僅僅有4K,而非8K。發生中斷時,核心棧將不使用程式的核心棧。而使用每一個 cpu的中斷請求棧。
核心棧將使用每一個 cpu的中斷請求棧。而非程式的核心棧來執行軟中斷函式:
static void call_on_stack(void *func, void *stack)
{
        asm volatile("xchgl        %%ebx,%%esp        \n"                                //交換棧指標,中斷棧幀的指標stack做為傳入引數(%ebx)。交換後esp是irq_ctx的棧頂,ebx是程式核心棧的棧
                     "call        *%%edi                \n"                                        //呼叫軟中斷函式
                     "movl        %%ebx,%%esp        \n"                                        //恢復之,直接使用movl,而非xchgl是由於函式執行完成,中斷的棧幀指標已經沒實用處了
                     : "=b" (stack)
                     : "0" (stack),
                       "D"(func)
                     : "memory", "cc", "edx", "ecx", "eax");
}

PS:全部的這些執行,應該都是在定義4K棧的基礎上的:
#ifdef CONFIG_4KSTACKS
/*
* per-CPU IRQ handling contexts (thread information and stack)
*/
union irq_ctx {
        struct thread_info      tinfo;
        u32                     stack[THREAD_SIZE/sizeof(u32)];
} __attribute__((aligned(PAGE_SIZE)));


static DEFINE_PER_CPU(union irq_ctx *, hardirq_ctx);
static DEFINE_PER_CPU(union irq_ctx *, softirq_ctx);
……


static void call_on_stack(void *func, void *stack)
……

是的,這個版本號相對複雜,可是假設看了複雜的,再來看簡單的,就easy多了,當平臺未定義do_softirq函式時(__ARCH_HAS_DO_SOFTIRQ)。
核心提供了一個通用的:
#ifndef __ARCH_HAS_DO_SOFTIRQ


asmlinkage void do_softirq(void)
{
        __u32 pending;
        unsigned long flags;


        if (in_interrupt())
                return;


        local_irq_save(flags);


        pending = local_softirq_pending();


        if (pending)
                __do_softirq();


        local_irq_restore(flags);
}


#endif

無需很多其它的解釋,它很的簡潔。


不論是哪個版本號,都將呼叫__do_softirq函式:
asmlinkage void __do_softirq(void)
{
        struct softirq_action *h;
        __u32 pending;
        int max_restart = MAX_SOFTIRQ_RESTART;
        int cpu;


        //儲存點陣圖
        pending = local_softirq_pending();
        //程式記帳
        account_system_vtime(current);


        //關閉本地CPU下半部。

為了保證同一個CPU上的軟中斷以序列方式執行。 __local_bh_disable((unsigned long)__builtin_return_address(0)); lockdep_softirq_enter(); //獲取本地CPU cpu = smp_processor_id(); restart: /* Reset the pending bitmask before enabling irqs */ //清除點陣圖 set_softirq_pending(0); //鎖中斷,僅僅是為了保持點陣圖的相互排斥,點陣圖處理完成。後面的程式碼能夠直接使用儲存的pending, //而中斷處理程式在啟用的時候,也能夠放心地使用irq_stat.__softirq_pending。

//所以。能夠開中斷了 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, entered softirq %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; } //??qsctr,這個是啥東東 rcu_bh_qsctr_inc(cpu); } //指向下一個軟中斷槽位 h++; //移位,取下一個軟中斷位 pending >>= 1; } while (pending); //當軟中斷處理完成後,由於前面已經開了中斷了。所以有可能新的軟中斷已經又被設定。 //軟中斷排程程式會嘗試又一次軟中斷。其最大重新啟動次數由max_restart決定。 //所以,這裡必須再次關閉中斷。再來一次…… local_irq_disable(); //取點陣圖 pending = local_softirq_pending(); //有軟中斷被設定,且沒有超過最大重新啟動次數,再來一次先 if (pending && --max_restart) goto restart; //超過最大重新啟動次數。還有軟中斷待處理。呼叫wakeup_softirqd。其任處是喚醒軟中斷守護程式ksoftirqd。 if (pending) wakeup_softirqd(); lockdep_softirq_exit(); account_system_vtime(current); //恢復下半部 _local_bh_enable(); }


中斷跟蹤
假設中斷跟蹤CONFIG_TRACE_IRQFLAGS被定義。lockdep_softirq_enter/lockdep_softirq_exit用於遞增/遞減當前程式的軟中斷上下文計數器softirq_context:
# define lockdep_softirq_enter()        do { current->softirq_context++; } while (0)
# define lockdep_softirq_exit()        do { current->softirq_context--; } while (0)
複製程式碼
trace_softirq_entry與trace_softirq_exit配合使用。能夠用於推斷軟中斷的延遲。


好像軟中斷不太難,沒有很多其它的內容了。歡迎大家回貼補充。

相關文章