記錄一下linux數據包從網卡進入協議棧的過程,不涉及驅動,不涉及其他層的協議處理。
內核是如何知道網卡收到數據的,這就涉及到網卡和內核的交互方式:
輪詢(poll):內核周期性的檢查網卡,查看是否收到數據。優點:數據包非常多的時候,這種處理方法會非常快速有效。缺點:數據包少的時候會CPU總是輪詢卻沒有收到數據包,造成CPU資源的浪費。這種方法很少使用。
中斷(interrupt):網卡收到數據就給內核發送硬件中斷打斷內核的正常運行,讓內核來處理數據包。優點:在數據包少的時候CPU能及時中斷其他任務來處理數據包,比較高效。缺點:數據包多的時候每個數據包都引發一次中斷,造成CPU頻繁地在收包過程和其他過程之間切換,浪費時間。在極端情況下收包中斷可能會一直搶占CPU造成軟中斷無法運行,收包隊列得不到處理,進而造成大量丟包。這就是所謂的receive-livelock。
Llinux早期是采用中斷的方式處理數據包的,之后引入了另一種方式NAPI,NAPI結合了輪詢和中斷的優點,在數據包少的時候采用中斷方式,數據包多的時候采用輪詢的方式,從而在兩種極端情況下也會有比較好的表現。
在NAPI下收包的過程
先看一個比較關鍵的結構softnet_data,每個邏輯CPU都有一個softnet_data結構,這個結構的poll_list是非常重要的。
struct softnet_data { struct net_device *output_queue; //收報隊列,這個隊列是給傳統收報方法兼容新收報架構用的(backlog_dev),用來模擬NAPI的 struct sk_buff_head input_pkt_queue; //用於收包的net_device struct list_head poll_list; struct sk_buff *completion_queue; //backlog_dev是一個偽造的net_device用來來處理input_pkt_queue里的數據 struct net_device backlog_dev; /* Sorry. 8) */ };
收包過程可以分成兩步:
- 當網卡收到數據包中斷發生,中斷處理程序就會把當前網卡的net_device插入當前CPU的softnet_data的poll_list鏈表,調度軟中斷。
- 軟中斷處理鏈表poll_list,讀出數據包,放入協議棧。
第一步的中斷的代碼可以參考drivers/net/tg3.c文件的tg3_interrupt函數,中斷發生的時候它會調用netif_rx_schedule把當前網卡的net_device插入當前CPU的softnet_data的poll_list鏈表。netif_rx_schedule函數又調度了軟中斷NET_RX_SOFTIRQ。大致結構就是下圖這樣子
第二步軟中斷在合適的時機得以執行,看一下他的執行過程:
static void net_rx_action(struct softirq_action *h) { struct softnet_data *queue = &__get_cpu_var(softnet_data); unsigned long start_time = jiffies; //預算,所有網卡的總配額 int budget = netdev_budget; void *have; local_irq_disable(); while (!list_empty(&queue->poll_list)) { struct net_device *dev; //預算用完了,或者時間太長了,跳出等下一輪處理 if (budget <= 0 || jiffies - start_time > 1) goto softnet_break; local_irq_enable(); dev = list_entry(queue->poll_list.next, struct net_device, poll_list); have = netpoll_poll_lock(dev); if (dev->quota <= 0 || dev->poll(dev, &budget)) { netpoll_poll_unlock(have); local_irq_disable(); //沒處理完,放到隊尾准備下次處理,注意是list_move_tai,不是 //list_insert_tail list_move_tail(&dev->poll_list, &queue->poll_list); if (dev->quota < 0) dev->quota += dev->weight; else dev->quota = dev->weight; } else { netpoll_poll_unlock(have); dev_put(dev); local_irq_disable(); } } //省略部分代碼 }
軟中斷不能長時間占用CPU,否則會造成用戶態進程長時間得不到調度,net_rx_action也一樣。所以net_rx_action函數每次執行最多會處理budget個數據包(所有網卡都算),同時這budget個數據包也需要平均分配,不能只處理一個網卡造成其他網卡得不到處理,net_device的weight和quota是用來處理這個問題的。這個代碼的大概意思是每次從poll_list里取出一個網卡,調用該網卡的poll函數盡可能多的收包(但是不會超過weight),poll函數收包后調用netif_receive_skb把數據包放入協議棧。如果網卡里的數據包沒處理完就會把net_device繼續放到poll_list鏈表等待下一次軟中斷繼續處理,如果網卡里的數據包處理完了就把該net_device從poll_list摘除。
傳統中斷收包方式
linux網卡驅動還有部分是用的傳統中斷收包方式,為了兼容也都挪到了NAPI架構上。用softnet_data結構的backlog_dev偽造了一個net_device。中斷發生的時候把數據包放到了softnet_data結構的input_pkt_queue鏈表里。
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); set_bit(__LINK_STATE_START, &queue->backlog_dev.state); queue->backlog_dev.weight = weight_p; queue->backlog_dev.poll = process_backlog; atomic_set(&queue->backlog_dev.refcnt, 1); } }
軟中斷的處理過程和NAPI類似。