1. 程式人生 > >深入理解Linux網路技術內幕——中斷與網路驅動程式

深入理解Linux網路技術內幕——中斷與網路驅動程式

接收到幀時通知驅動程式

    在網路環境中,裝置(網絡卡)接收到一個數據幀時,需要通知驅動程式進行處理。有一下幾種通知機制: 輪詢:     核心不斷檢查裝置是否有話要說。(比較耗資源,但在一些情況下卻是最佳方法) 中斷:     特定事件發生時,裝置驅動程式代表核心指示裝置產生硬體中斷,核心中斷其它活動滿足裝置的需要。多數網路驅動程式使用中斷。 中斷期間處理多幀:     中斷被通知,且驅動程式執行。然後保持幀的接收(載入),直到輸入佇列達到指定的數目、或者一直做下去知道佇列清空、或者經過指定時間。 定時器驅動的中斷事件     驅動程式指定裝置定期產生中斷事件(驅動程式主動,而不是裝置主動,與前面的中斷不同)。然後處理函式處理上次驅動以來到達的幀。這種機制會導致幀處理的延時,比如指定時間為100ms,而幀可能在第0ms、第50ms、也可能在第100ms剛好到達,平均延時為50ms。
組合機制     低流量負載下使用中斷     高流量負載下使用定時器驅動的中斷 中斷的優缺點:     中斷在低流量負載下是很好的選擇,但在高流量負載情況下,由於沒接收到一個幀就進行一次中斷,很容易讓CPU在處理中斷上浪費時間,甚至崩潰。     負責接收幀的程式碼,分為兩部分(實際上為中斷的上半部函式、下半部函式)。上半部函式將幀拷貝到輸出佇列,並執行其他一些不可搶佔的工作。下半部函式的內容則是核心處理輸入佇列中的幀(將幀傳給具體的協議處理)。由於上半部函式可以搶佔下半部函式的執行,在高流量負載下,就有可能上半部函式一直執行,而下半部函式被擱置,而導致輸入佇列溢位,系統崩潰。

中斷處理函式

為什麼有下半部函式

    簡單的說,下半部函式之所以存在是因為中斷是不可搶佔的。而我們如果花太多時間去處理一箇中斷,則可能導致其他中斷遲遲不能執行。為此,我們將中斷處理程式分為上半部函式和下半部函式。上半部函式主要執行中斷處理程式中不可搶佔的內容(如把幀從裝置拷貝到輸入佇列),下半部函式執行可被搶佔的內容(如幀的具體給各自協議的處理)。     上半部函式獨佔CPU資源執行,下半部函式執行時可以被其他中斷搶佔CPU資源。有了下半部函式後,中斷處理程式的模型如下:     1)裝置傳送訊號給CPU,通知有中斷事件     2)CPU關中斷,執行上半部函式     3)上半部函式執行
    4)上半部函式執行完畢,CPU開中斷,並執行下半部函式     上半部函式處理的主要內容包括:
    a)把核心稍後要處理的中斷事件的所有資訊儲存到RAM     b)設定標識,一邊核心之後知道需要處理該中斷,及如何處理     c)開中斷,

下半部函式解決方案

    核心提供多種下半部函式的解決方案,主要有舊式下半部、微任務、軟IRQ三種。不同的解決方案的差別主要在於執行環境及併發與上鎖。     1)舊式下半部: 任何時刻,只有一箇舊式下半部函式可以執行(不管多少個CPU)     2)微任務:    任何時刻,每個CPU,只有一個微任務例項可以執行.(多數情況下的選擇)     3)軟IRQ:    任何時刻,一個CPU的每個軟IRQ只有一個例項可以執行。(收發幀等需要及時響應的的網路任務的選擇
/***********************Linux-2.6.32************************************/
//include/linux/hardirq.h
in_irq()       //CPU正服務於硬體中斷時,返回True
in_softirq()   //CPU正服務於軟體中斷時,返回True
in_interrupt() //CPU正在服務於一個硬體中斷或軟體中斷,或搶佔功能關閉時,返回True  
 
//arch/x86/include/asm/hardirq.h
local_softirq_pending()  //本地CPU至少有一個IRQ出於未決狀態時,返回True
 
//include/linux/interrupt.h
__raise_softirq_irqoff()  //設定與軟IRQ相關聯的標識,將IRQ標記為未決
raise_softirq_irqoff()    //__raise_softirq_irqoff包裹函式,當in_interrupt為False時,喚醒ksoftirqd
raise_softirq()           //包裹raise_softirq_irqoff,呼叫raise_softirq_irqoff前先關中斷
 
//kernel/softirq.c
__local_bh_enable()   //開啟本地CPU的下半部 
local_bh_enable()     //如果有任何軟IRQ未決,且in_interrupt返回False,則invoke_softirq
local_bh_disable()    //關閉CPU下半部
 
//include/linux/irqflags.h
local_irq_enable()    //開啟本地CPU中斷功能
local_irq_disable()   //關閉本地CPU中斷功能
local_irq_save()      //先把本地CPU中斷狀態儲存,再予以關閉
local_irq_restore()   //恢復本地CPU之前的中斷狀態,恢復local_irq_save儲存的中斷資訊
 
//include/linux/spinlock.h
spin_lock_bh()      //取得迴旋鎖,關閉下半部及搶佔功能
spin_unlock_bh()    //釋放回旋鎖,重啟下半部搶佔功能


搶佔功能

    Linux2.5之後的核心實現了完全搶佔(preemptitle)的功能,(即核心本身也可以被搶佔)。但是有些時候,核心執行的任務不希望被搶佔,(比如正在服務於硬體)這時就需要關閉搶佔功能。下面是幾個與搶佔功能的管理相關的函式。
//inculde/linux/preempt.h
preempt_disable()          //為當前任務關閉搶佔功能。可重複呼叫,遞增引用計數器
preempt_enable()           //搶佔功能再度開啟,(需要先檢查引用計數器是否為0)
preempt_enable_no_resch()  //遞減引用計數器,只有引用計數器為0時,搶佔功能才能再度開啟
preempt_check_resched()    //由preempt_enable呼叫,檢查引用計數器是否為0.
 
// arch/x86/include/asm/thread_info.h
struct thread_info {
    ……
    int         preempt_count;  /* 0 => preemptable,
                           <0 => BUG */ //搶佔計數器,指定程序是否能被搶佔
    ……
};

下半部函式

   下半部函式的基礎構架有以下幾個部分: 1)分類:把下半部函式分成適當型別 2)關聯:註冊(登記)下半部函式型別及其處理函式間的關聯關係 3)排程:為下半部函式進行排程,以準備執行 4)通知:通知核心BH的存在

舊式下半部函式(linux-2.2以前)

    舊式下半部函式模型(如linux-2.2版本)把下半部分為很多種型別,如下:
enum {
    TIMER_BH = 0,
    CONSOLE_BH,
    TQUEUE_BH,
    DIGI_BH,
    SERIAL_BH,
    RISCOM8_BH,
    SPECIALIX_BH,
    AURORA_BH,
    ESP_BH,
    NET_BH,      //網路下半部
    SCSI_BH,
    IMMEDIATE_BH,
    KEYBOARD_BH,
    CYCLADES_BH,
    CM206_BH,
    JS_BH,
    MACSERIAL_BH,
    ISICOM_BH
};


各個型別及其處理函式用init_bh()關聯,如網路下半部在net_dev_init中關聯
_ _initfunc(int net_dev_init(void))
{
    ... ... ...
    init_bh(NET_BH, net_bh);
    ... ... ...
}


中斷處理函式要觸發下半部函式時,就使用mark_bh在全域性點陣圖bh_active設定標誌位
extern inline void mark_bh(int nr)
{
    set_bit(nr, &bh_active);
};


如網路裝置接收到一個幀時,就呼叫netif_rx通知核心,將幀拷貝到輸入佇列backlog,然後標記NET_BH下半部標識:
skb_queue_tail(&backlog, skb);
mark_bh(NET_BH);
return

引入軟IRQ

    linux-2.4版本以後的linux核心引入了軟IRQ。(軟IRQ可以視為IRQ的多執行緒版本)
    新式軟IRQ有以下幾種型別(linux-2.4只有六種,後面又發展了):
//include/linux/interrupt.h
enum
{
    HI_SOFTIRQ=0,     //高優先順序微任務
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,  //網路軟IRQ
    NET_RX_SOFTIRQ,  //網路軟IRQ
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,  //低優先順序微任務軟IRQ
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */
 
    NR_SOFTIRQS
};      
    一種軟IRQ在一個CPU上只能由一個例項在執行。
    為此,每種軟IRQ型別維護一個softnet_data型別的陣列,陣列的大小為CPU的數目,而每個CPU對應該型別的軟IRQ維護一個softnet_data的資料結構。
/*
 * Incoming packets are placed on per-cpu queues so that
 * no locking is needed.
 */
struct softnet_data
{
    struct Qdisc        *output_queue;      //qdisc是queueing discipline的簡寫,也就是排隊規則,即qos.這裡也就是輸出幀的控制。
    struct sk_buff_head input_pkt_queue;    //當輸入幀被驅動取得之前,就儲存在這個佇列裡,(不適用與napi驅動,napi有自己的私有佇列)
    struct list_head    poll_list;          //表示有輸入幀待處理的裝置連結串列。 
    struct sk_buff      *completion_queue;  //表示已經成功被傳遞出的幀的連結串列。
 
    struct napi_struct  backlog;            //用來相容非napi的驅動。                                                                    
};

初始化在net_dev_init中
static int __init net_dev_init(void)
{
    ......
    for_each_possible_cpu(i) {
        struct softnet_data *queue;
 
        queue = &per_cpu(softnet_data, i);
        skb_queue_head_init(&queue->input_pkt_queue);
        queue->completion_queue = NULL;
        INIT_LIST_HEAD(&queue->poll_list);
 
        queue->backlog.poll = process_backlog;
        queue->backlog.weight = weight_p;
        queue->backlog.gro_list = NULL;
        queue->backlog.gro_count = 0;
    }
    ......
}


軟IRQ的註冊於排程機制

    軟IRQ的註冊與排程機制與舊式模型類似,只是函式不一樣。     對應init_bh(),軟IRQ使用spen_softirq()對軟IRQ型別與其關聯函式的關係進行註冊。
// kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action *))                            
{
    softirq_vec[nr].action = action;
}

軟IRQ通過下列函式在本地CPU上進行排程,準備執行:
__raise_softirq_irqoff()  //設定與軟IRQ相關聯的標識,將IRQ標記為未決
raise_softirq_irqoff()    //__raise_softirq_irqoff包裹函式,當in_interrupt為False時,喚醒ksoftirqd
raise_softirq()           //包裹raise_softirq_irqoff,呼叫raise_softirq_irqoff前先關中斷
軟IRQ具體的執行參考其他博文 do_IRQ schecule do_softirq 參考其他博文

微任務

    微任務是建立在軟IRQ的基礎之上的。對應軟IRQ的HI_SOFTIRQ(高優先順序微任務)和TASKLET_SOFTIRQ(普通優先順序微任務)。
    每個CPU有兩份tasklet_struct表,一份對應HI_SOFTIRQ,一份對應TASKLET_SOFTIRQ。
/*
 * Tasklets
 */
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);


微任務有一些特徵(與舊式下半部函式的區別) 1)微任務不限數目,但是base_bh的每一位標識只限於一種型別的下半部函式 2)微任務提供兩種等級的優先順序 3)不同微任務可以再不同CPU上同事執行 4)微任務相對於舊式下半部來說是動態的,不需要靜態地在XXX_BH或XXX_SOFTIRQ列舉列表中靜態宣告
struct tasklet_struct
{
    struct tasklet_struct *next;  //把關聯到同一個CPU的結構連結起來                                                                                                   
    unsigned long state;          //點陣圖標識,其可能的取值由TASKLET_STATE_XXX列舉
    atomic_t count;               //計數器,0表示微任務被關閉,不可執行。非0表示微任務已經開啟
    void (*func)(unsigned long);  //要執行的函式
    unsigned long data;           //上面函式的引數
}; 
 
enum
{
    TASKLET_STATE_SCHED,    /* Tasklet is scheduled for execution */
    TASKLET_STATE_RUN   /* Tasklet is running (SMP only) */
};