學習Linux-4.12內核網路協議棧(2.2)——接口層數據包的接收(上半部)

前面寫了這麼多,終於可以開始分析數據報的傳輸過程了,那我們就愉快的開始吧!
我們知道,一箇中斷處理函數主要分兩個部分,上半部和下半部,這篇文章主要介紹上半部分。

當一個數據包到達的時候,網卡驅動會完成接收並且觸發中斷,我們就從這個中斷處理函數開始:

當一箇中斷產生併發送給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,我們下篇文章將從這個函數開始分析。

內核接收分組理解


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章