前面寫了這麼多,終於可以開始分析數據報的傳輸過程了,那我們就愉快的開始吧!
我們知道,一箇中斷處理函數主要分兩個部分,上半部和下半部,這篇文章主要介紹上半部分。
當一個數據包到達的時候,網卡驅動會完成接收並且觸發中斷,我們就從這個中斷處理函數開始:
當一箇中斷產生併發送給CPU的時候,對於NAPI和不支持NAPI的設備來說處理結果是不一樣的,NAPI調用的函數是napi_schedule,非NAPI調用的函數是netif_rx,這兩個函數都是在網卡驅動的中斷處理函數上半部分被調用的。
產生中斷的每個設備都有一個相應的中斷處理程序,是設備驅動程序的一部分。
每個網卡都有一箇中斷處理程序,用於通知網卡該中斷已經被接收了,以及把網卡緩衝區的數據包拷貝到內存中。當網卡接收來自網絡的數據包時,需要通知內核數據包到了。網卡立即發出中斷:嗨,內核,我這裏有最新的數據包了。內核通過執行網卡已註冊的中斷處理函數來做出應答。
中斷處理程序開始執行,通知硬件,拷貝最新的網絡數據包到內存,然後讀取網卡更多的數據包。
這些都是重要、緊迫而又與硬件相關的工作。內核通常需要快速的拷貝網絡數據包到系統內存,因爲網卡上接收網絡數據包的緩存大小固定,而且相比系統內存也要小得多。所以上述拷貝動作一旦被延遲,必然造成網卡緩存溢出 - 進入的數據包占滿了網卡的緩存,後續的包只能被丟棄。當網絡數據包被拷貝到系統內存後,中斷的任務算是完成了,這時它把控制權交還給被系統中斷前運行的程序,處理和操作數據包的其他工作在隨後的下半部中進行。
我們現在知道了不管是否支持NAPI,對於驅動來說無非是調用napi_schedule或者netif_rx來通知內核,將數據包交給內核。所以如果不知道驅動使用的中斷處理程序是哪個,那麼只要搜索一下這兩個函數就能定位出來了。下面我們來分析一下這兩個函數,因爲NAPI是基於前者發展出來的,所以我們先了解netif_rx。一、非NAPI (netif_rx)
/**
* netif_rx - post buffer to the network code
* @skb: buffer to post
*
* This function receives a packet from a device driver and queues it for
* the upper (protocol) levels to process. It always succeeds. The buffer
* may be dropped during processing for congestion control or by the
* protocol layers.
*
* return values:
* NET_RX_SUCCESS (no congestion)
* NET_RX_DROP (packet was dropped)
*
*/
int netif_rx(struct sk_buff *skb)
{
trace_netif_rx_entry(skb);
return netif_rx_internal(skb);
}
static int netif_rx_internal(struct sk_buff *skb)
{
int ret;
net_timestamp_check(netdev_tstamp_prequeue, skb); //記錄接收時間到skb->tstamp
trace_netif_rx(skb);
#ifdef CONFIG_RPS
if (static_key_false(&rps_needed)) {
struct rps_dev_flow voidflow, *rflow = &voidflow;
int cpu;
preempt_disable();
rcu_read_lock();
cpu = get_rps_cpu(skb->dev, skb, &rflow); //如果有支持rps,則獲取這個包交給了哪個cpu處理
if (cpu < 0)
cpu = smp_processor_id(); //如果上面獲取失敗,則用另外一種方式獲取當前cpu的id
ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail); //調用該函數將包添加到queue->input_pkt_queue裏面
rcu_read_unlock();
preempt_enable();
} else
#endif
{
unsigned int qtail;
ret = enqueue_to_backlog(skb, get_cpu(), &qtail);
put_cpu();
}
return ret;
}
這個函數最後調用enqueue_to_backlog將包添加到queue->input_pkt_queue的尾部,這個input_pkt_queue是每個cpu都有的一個隊列,如果不夠清楚它的作用,可以看看前面一篇文章的截圖,這個隊列的初始化在net_dev_init()中完成:
8568 for_each_possible_cpu(i) {
8569 struct work_struct *flush = per_cpu_ptr(&flush_works, i);
8570 struct softnet_data *sd = &per_cpu(softnet_data, i);
8571
8572 INIT_WORK(flush, flush_backlog);
8573
8574 skb_queue_head_init(&sd->input_pkt_queue);
8575 skb_queue_head_init(&sd->process_queue);
8576 INIT_LIST_HEAD(&sd->poll_list);
8577 sd->output_queue_tailp = &sd->output_queue;
8578 #ifdef CONFIG_RPS
8579 sd->csd.func = rps_trigger_softirq;
8580 sd->csd.info = sd;
8581 sd->cpu = i;
8582 #endif
8583
8584 sd->backlog.poll = process_backlog;
8585 sd->backlog.weight = weight_p;
8586 }
...
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
現在我們來看看enqueue_to_backlog函數怎麼將包添加到queue->input_pkt_queue尾部的:
/*
* enqueue_to_backlog is called to queue an skb to a per CPU backlog
* queue (may be a remote CPU queue).
*/
static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
unsigned int *qtail)
{
struct softnet_data *sd;
unsigned long flags;
unsigned int qlen;
sd = &per_cpu(softnet_data, cpu); //獲取當前cpu的softnet_data對象
local_irq_save(flags); //保存中斷狀態
rps_lock(sd);
if (!netif_running(skb->dev)) //確認net_device的dev->state是不是__LINK_STATE_START狀態,如果該網絡設備沒有運行,直接退出,不進行包的處理
goto drop;
qlen = skb_queue_len(&sd->input_pkt_queue); //獲取input_pkt_queue的當前長度
if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) { //如果當前長度小於最大長度,而且滿足流量限制的要求
if (qlen) {
enqueue:
__skb_queue_tail(&sd->input_pkt_queue, skb); //關鍵在這裏,將SKB添加到input_pkt_queue隊列的後面
input_queue_tail_incr_save(sd, qtail); //隊列尾部指針加1
rps_unlock(sd);
local_irq_restore(flags); //恢復中斷狀態
return NET_RX_SUCCESS; //返回成功標識
}
/* Schedule NAPI for backlog device
* We can use non atomic operation since we own the queue lock
*/
if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {
if (!rps_ipi_queued(sd))
____napi_schedule(sd, &sd->backlog); //把虛擬設備backlog添加到sd->poll_list中以便進行輪詢,最後設置NET_RX_SOFTIRQ標誌觸發軟中斷
}
goto enqueue;
}
drop:
sd->dropped++; /* 如果接收隊列滿了就直接丟棄 */
rps_unlock(sd);
local_irq_restore(flags); /* 恢復本地中斷 */
atomic_long_inc(&skb->dev->rx_dropped);
kfree_skb(skb);
return NET_RX_DROP;
}
在非NAPI中,我們只要將skb添加到input_pkt_queue就可以了嗎?我們要看到最後,它將backlog添加到了sd->poll_list裏面,並且調用__napi_schedule()觸發軟中斷。我們還記得,在協議棧初始化的時候,net_dev_init()有初始化軟中斷:
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
所以接下來,軟中斷會執行net_rx_action 函數。這個部分我們放到下篇文章數據包接收的下半部裏面分析。
網絡數據包在上半部的處理通常有兩種模式:傳統的netif_rx模式和NAPI(napi_schedule)模式,在這裏我們主要討論網絡上半部的內容,無論上半部採用何種收包模式,都會調用__netif_rx_schedule()函數,netif_receive_skb函數會根據不同的協議調用不同的協議處理函數。
二、 NAPI(napi_schedule)
在分析NAPI前, 我們先來看看網卡驅動是怎麼調用NAPI的函數的:
2235 if (likely(napi_schedule_prep(&nic->napi))) { //設置state爲NAPI_STATE_SCHED 2236 e100_disable_irq(nic); 2237 __napi_schedule(&nic->napi); //將設備添加到 poll list,並開啓軟中斷。 2238 }
/**
* napi_schedule - schedule NAPI poll
* @n: NAPI context
*
* Schedule NAPI poll routine to be called if it is not already
* running.
*/
static inline void napi_schedule(struct napi_struct *n)
{
if (napi_schedule_prep(n)) //確定設備處於運行,而且設備還沒有被添加到網絡層的POLL 處理隊列中
__napi_schedule(n);
}
/**
* __napi_schedule - schedule for receive
* @n: entry to schedule
*
* The entry's receive function will be scheduled to run.
* Consider using __napi_schedule_irqoff() if hard irqs are masked.
*/
void __napi_schedule(struct napi_struct *n)
{
unsigned long flags;
local_irq_save(flags);
____napi_schedule(this_cpu_ptr(&softnet_data), n);
local_irq_restore(flags);
}
EXPORT_SYMBOL(__napi_schedule);
/* Called with irq disabled */
static inline void ____napi_schedule(struct softnet_data *sd,
struct napi_struct *napi)
{
list_add_tail(&napi->poll_list, &sd->poll_list); //將設備添加到poll隊列
__raise_softirq_irqoff(NET_RX_SOFTIRQ); //觸發軟中斷
}
到這裏可以看出,它間設備添加到poll隊列以後觸發了軟中斷,我們還記得在net_dev_init()裏面我們註冊了軟中斷的處理函數 net_rx_action,所以後面文章我們分析軟中斷處理函數net_rx_action.
到這裏可以得出的結論是:無論是NAPI接口還是非NAPI最後都是使用 net_rx_action 作爲軟中斷處理函數。也就是中斷的上半部分雖然有所不一樣,但是他們下半部分的入口的是由net_rx_action,我們下篇文章將從這個函數開始分析。