核心接收分組理解

lxgeek發表於2014-12-24
背景:
       核心接收分組的方式有兩種:第一種:傳統方式,使用中斷的方式;第二種:NAPI,使用中斷和輪詢結合的方式。
 
中斷方式:
       下圖為一個分組到達NIC之後,該分組穿過核心到達網路層函式的路徑。
 
此圖的下半部分為中斷處理,上半部分為軟中斷。在中斷處理中,函式net_interupt是
裝置驅動程式的中斷處理程式,它將確認此中斷是否由接收到分組引發的,如果確實如此,
則控制權移交到函式net_rx。函式net_rx也是特定於NIC,首先建立一個新的套接字緩衝區,
分組的內容接下來從NIC傳輸到緩衝區(進入到實體記憶體中),然後使用核心原始碼中針對
各個傳輸型別的庫函式來分析首部的資料。函式netif_rx函式不是特定於網路驅動函式,該
函式位於net/core/dev.c,呼叫該函式,標誌著控制由特定於網路卡的程式碼轉移到了網路層的
通用介面部分。此函式的作用在於,將接收分組放置到一個特定於CPU的等待佇列上,並
退出中斷上下文。核心使用softnet_data來管理進出流量,其定義入下:
 
2352 /*
2353  * Incoming packets are placed on per-cpu queues
2354  */
2355 struct softnet_data {
2356     struct list_head    poll_list;
2357     struct sk_buff_head process_queue;
2358   
2359     /* stats */
2360     unsigned int        processed;
2361     unsigned int        time_squeeze;
2362     unsigned int        cpu_collision;
2363     unsigned int        received_rps;
2364 #ifdef CONFIG_RPS
2365     struct softnet_data *rps_ipi_list;
2366 #endif
2367 #ifdef CONFIG_NET_FLOW_LIMIT
2368     struct sd_flow_limit __rcu *flow_limit;
2369 #endif
2370     struct Qdisc        *output_queue;
2371     struct Qdisc        **output_queue_tailp;
2372     struct sk_buff      *completion_queue;
2373
2374 #ifdef CONFIG_RPS
2375     /* Elements below can be accessed between CPUs for RPS */
2376     struct call_single_data csd ____cacheline_aligned_in_smp;
2377     struct softnet_data *rps_ipi_next;
2378     unsigned int        cpu;
2379     unsigned int        input_queue_head;
2380     unsigned int        input_queue_tail;
2381 #endif
2382     unsigned int        dropped;
2383     struct sk_buff_head input_pkt_queue;   //對所有進入分組建立一個連結串列。
2384     struct napi_struct  backlog;
2385
2386 };
第2383行的input_pkt_queue即是CPU的等待佇列。netif_rx的實現如下:
3329 static int netif_rx_internal(struct sk_buff *skb)
3330 {
3331     int ret;
3332
3333     net_timestamp_check(netdev_tstamp_prequeue, skb);
3334
3335     trace_netif_rx(skb);
3336 #ifdef CONFIG_RPS  //RPS 和 RFS 相關程式碼
3337     if (static_key_false(&rps_needed)) {
3338         struct rps_dev_flow voidflow, *rflow = &voidflow;
3339         int cpu;
3340
3341         preempt_disable();
3342         rcu_read_lock();
3343
3344         cpu = get_rps_cpu(skb->dev, skb, &rflow);   //選擇合適的CPU id
3345         if (cpu < 0)
3346             cpu = smp_processor_id();
3347
3348         ret = enqueue_to_backlog(skb, cpu, &rflow->last_qtail);
3349
3350         rcu_read_unlock();
3351         preempt_enable();
3352     } else
3353 #endif
3354     {
3355         unsigned int qtail;
3356         ret = enqueue_to_backlog(skb, get_cpu(), &qtail);     //將skb入隊
3357         put_cpu();
3358     }
3359     return ret;
3360 }
3361
3362 /**
3363  *  netif_rx    -   post buffer to the network code
3364  *  @skb: buffer to post
3365  *
3366  *  This function receives a packet from a device driver and queues it for
3367  *  the upper (protocol) levels to process.  It always succeeds. The buffer
3368  *  may be dropped during processing for congestion control or by the
3369  *  protocol layers.
3370  *
3371  *  return values:
3372  *  NET_RX_SUCCESS  (no congestion)
3373  *  NET_RX_DROP     (packet was dropped)
3374  *
3375  */
3376
3377 int netif_rx(struct sk_buff *skb)
3378 {
3379     trace_netif_rx_entry(skb);
3380
3381     return netif_rx_internal(skb);
3382 }
入隊函式 enqueue_to_backlog的實現如下:
3286 static int enqueue_to_backlog(struct sk_buff *skb, int cpu,
3287                   unsigned int *qtail)
3288 {
3289     struct softnet_data *sd;
3290     unsigned long flags;
3291     unsigned int qlen;
3292
3293     sd = &per_cpu(softnet_data, cpu);  //獲得此cpu上的softnet_data
3294
3295     local_irq_save(flags);  
3296
3297     rps_lock(sd);
3298     qlen = skb_queue_len(&sd->input_pkt_queue);
3299     if (qlen <= netdev_max_backlog && !skb_flow_limit(skb, qlen)) {
3300         if (qlen) {
3301 enqueue:
3302             __skb_queue_tail(&sd->input_pkt_queue, skb);
3303             input_queue_tail_incr_save(sd, qtail);
3304             rps_unlock(sd);
3305             local_irq_restore(flags);
3306             return NET_RX_SUCCESS;
3307         }
3308
3309         /* Schedule NAPI for backlog device
3310          * We can use non atomic operation since we own the queue lock
3311          */
3312         if (!__test_and_set_bit(NAPI_STATE_SCHED, &sd->backlog.state)) {   
3313             if (!rps_ipi_queued(sd))
3314                 ____napi_schedule(sd, &sd->backlog); //只有qlen是0的時候才執行到這裡
3315         }
3316         goto enqueue;
3317     }
3318
3319     sd->dropped++;
3320     rps_unlock(sd);
3321
3322     local_irq_restore(flags);
3323
3324     atomic_long_inc(&skb->dev->rx_dropped);
3325     kfree_skb(skb);
3326     return NET_RX_DROP;
3327 }
View Code

 

NAPI
NAPI是混合了中斷和輪詢機制,當一個新的分組到達,而前一個分組依然在處理,這時核心
並不需要產生中斷,核心會繼續處理並在處理完畢後將中斷開啟。這樣核心就利用了中斷和輪詢的好處,
只有有分組到達的時候,才會進行輪詢。
NAPI存在兩個優勢
1.減少了CPU使用率,因為更少的中斷。
2.處理各種裝置更加公平
只有裝置滿足如下兩個條件時,才能實現NAPI:
1.裝置必須能夠保留多個接收的分組
2.裝置必須能夠禁用用於分組接收的IRQ。而且,傳送分組或其他可能通過IRQ進行的操作,都仍然
必須是啟用的。
其執行概覽如下:
如上所示,各個裝置在進入poll list前需要禁用IRQ,而裝置中的分組都處理完畢後重新開啟IRQ。poll list使用
napi_struct來管理裝置,其定義如下:
 
296 /*
297  * Structure for NAPI scheduling similar to tasklet but with weighting
298  */
299 struct napi_struct {
300     /* The poll_list must only be managed by the entity which
301      * changes the state of the NAPI_STATE_SCHED bit.  This means
302      * whoever atomically sets that bit can add this napi_struct
303      * to the per-cpu poll_list, and whoever clears that bit
304      * can remove from the list right before clearing the bit.
305      */
306     struct list_head    poll_list;    //用作連結串列元素
307
308     unsigned long       state;
309     int         weight;        //裝置權重
310     unsigned int        gro_count;
311     int         (*poll)(struct napi_struct *, int);   //裝置提供的輪詢函式
312 #ifdef CONFIG_NETPOLL
313     spinlock_t      poll_lock;
314     int         poll_owner;
315 #endif
316     struct net_device   *dev;
317     struct sk_buff      *gro_list;
318     struct sk_buff      *skb;
319     struct hrtimer      timer;
320     struct list_head    dev_list;
321     struct hlist_node   napi_hash_node;
322     unsigned int        napi_id;
323 };    
View Code
變數state可以為NAPI_STATE_SCHED或NAPI_STATE_DISABLE, 前者表示裝置將在
核心的下一次迴圈時被輪詢,後者表示輪詢已經結束且沒有更多的分組等待處理,但
裝置並沒有從poll list移除。
 
支援NAPI的NIC需要修改中斷處理程式,將此裝置放置在poll list上。示例程式碼如下:
2215 static irqreturn_t e100_intr(int irq, void *dev_id)
2216 {  
2217     struct net_device *netdev = dev_id;
2218     struct nic *nic = netdev_priv(netdev);
2219     u8 stat_ack = ioread8(&nic->csr->scb.stat_ack);
2220       
2221     netif_printk(nic, intr, KERN_DEBUG, nic->netdev,
2222              "stat_ack = 0x%02X\n", stat_ack);
2223    
2224     if (stat_ack == stat_ack_not_ours ||    /* Not our interrupt */
2225        stat_ack == stat_ack_not_present)    /* Hardware is ejected */
2226         return IRQ_NONE;
2227    
2228     /* Ack interrupt(s) */
2229     iowrite8(stat_ack, &nic->csr->scb.stat_ack);
2230    
2231     /* We hit Receive No Resource (RNR); restart RU after cleaning */
2232     if (stat_ack & stat_ack_rnr)
2233         nic->ru_running = RU_SUSPENDED;
2234
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     }
2239
2240     return IRQ_HANDLED;
2241 }
View Code
函式__napi_schedule的定義入下:
3013 /* Called with irq disabled */
3014 static inline void ____napi_schedule(struct softnet_data *sd,
3015                      struct napi_struct *napi)
3016 {
3017     list_add_tail(&napi->poll_list, &sd->poll_list);
3018     __raise_softirq_irqoff(NET_RX_SOFTIRQ);
3019 } 

4375 /**
4376  * __napi_schedule - schedule for receive
4377  * @n: entry to schedule
4378  *
4379  * The entry's receive function will be scheduled to run.
4380  * Consider using __napi_schedule_irqoff() if hard irqs are masked.
4381  */
4382 void __napi_schedule(struct napi_struct *n)
4383 {
4384     unsigned long flags;
4385
4386     local_irq_save(flags);
4387     ____napi_schedule(this_cpu_ptr(&softnet_data), n);
4388     local_irq_restore(flags);
4389 }
4390 EXPORT_SYMBOL(__napi_schedule);
View Code
裝置除了對中斷處理進行修改,還需要提供一個poll函式,使用此函式從NIC中獲取分組。示例程式碼如下:
特定於硬體的方法 hyper_do_poll 從NIC中獲取分組,返回值work_done為處理的分組數目。當分組處理完後,
會呼叫netif_rx_complete將此裝置從poll list中移除。ixgbe網路卡的poll函式為ixgbe_poll, 而對於非NAPI的函式,
核心提供預設的處理函式process_backlog。
     
軟中斷處理相關
無論是NAPI介面還是非NAPI最後都是使用 net_rx_action 作為軟中斷處理函式。因此整個流程如下:
上圖中有些函式名已經發生變更,但是流程依然如此。在最新的3.19核心程式碼中,非NAPI的呼叫流程如下:
neif_rx會呼叫enqueue_to_backlog 將skb存入softnet_data,並呼叫____napi_schedule函式。
netif_rx===>netif_rx_internal===>enqueue_to_backlog===>____napi_schedule===>net_rx_action===>process_backlog===>__netif_receive_skb
e100網路卡的NAPI呼叫流程入下:
e100_intr===>__napi_schedule===>net_rx_action===>e100_poll===>e100_rx_clean===>e100_rx_indicate===>netif_receive_skb
 
 
 

相關文章