深入理解Linux網路技術內幕——中斷與網路驅動程式
阿新 • • 發佈:2019-01-06
接收到幀時通知驅動程式
在網路環境中,裝置(網絡卡)接收到一個數據幀時,需要通知驅動程式進行處理。有一下幾種通知機制: 輪詢: 核心不斷檢查裝置是否有話要說。(比較耗資源,但在一些情況下卻是最佳方法) 中斷: 特定事件發生時,裝置驅動程式代表核心指示裝置產生硬體中斷,核心中斷其它活動滿足裝置的需要。多數網路驅動程式使用中斷。 中斷期間處理多幀: 中斷被通知,且驅動程式執行。然後保持幀的接收(載入),直到輸入佇列達到指定的數目、或者一直做下去知道佇列清空、或者經過指定時間。 定時器驅動的中斷事件 驅動程式指定裝置定期產生中斷事件(驅動程式主動,而不是裝置主動,與前面的中斷不同)。然後處理函式處理上次驅動以來到達的幀。這種機制會導致幀處理的延時,比如指定時間為100ms,而幀可能在第0ms、第50ms、也可能在第100ms剛好到達,平均延時為50ms。中斷處理函式
為什麼有下半部函式
簡單的說,下半部函式之所以存在是因為中斷是不可搶佔的。而我們如果花太多時間去處理一箇中斷,則可能導致其他中斷遲遲不能執行。為此,我們將中斷處理程式分為上半部函式和下半部函式。上半部函式主要執行中斷處理程式中不可搶佔的內容(如把幀從裝置拷貝到輸入佇列),下半部函式執行可被搶佔的內容(如幀的具體給各自協議的處理)。 上半部函式獨佔CPU資源執行,下半部函式執行時可以被其他中斷搶佔CPU資源。有了下半部函式後,中斷處理程式的模型如下: 1)裝置傳送訊號給CPU,通知有中斷事件 2)CPU關中斷,執行上半部函式 3)上半部函式執行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) */
};