當內核收到了一個網絡包,它該怎麼辦?

在計算機網絡的世界裏,數據的傳輸與處理猶如一場精密的交響樂演奏。當內核收到了一個網絡包,就如同指揮家收到了新的樂譜信號,接下來一系列的操作將決定這個‘音符’如何在系統中奏響。從硬件底層到軟件上層,從網卡驅動到協議棧處理,每一個環節都緊密相扣,共同完成數據的接收、解析與傳遞。現在,讓我們深入探究一下,當內核收到一個網絡包後,究竟會發生什麼。

一、網卡 “迎接” 網絡包

1.1 從網線到網卡

當網絡包從網線抵達網卡時,網卡作爲網絡世界與計算機硬件的 “前沿哨所”,率先行動起來。它內置的直接內存訪問(DMA)引擎就像一位高效的 “搬運工”,會迅速把網絡包搬運到預先分配好的收包隊列中,這個過程無需 CPU 過多操心,極大地減輕了 CPU 的負擔。就好比快遞到達快遞站後,工作人員會先把包裹按照區域分類放置,等待後續處理。

這個數據包來了之後,現在只是一堆電信號,離內核程序的處理還十萬八千里呢,需要先經歷網卡這個硬件的折磨。我們把上圖中的網卡放大看看。

而且,這個過程完全不需要 CPU 參與,只需要 DMA 這個硬件設備,配合網卡這個硬件設備即可完成。當然,這個過程的前提是,網卡驅動需要在內存中申請一個緩衝區叫 sk_buffer,然後把這個 sk_buffer 的地址告訴網卡,這樣 DMA 才知道等網卡的緩衝區有數據到來時,把它拷貝到內存的什麼位置上。具體過程展開如下:

1.2 註冊硬中斷處理程序

緊接着,網卡會向 CPU 發送一個硬中斷信號,如同大聲呼喊:“有新包裹到啦,快來處理!” 這個硬中斷信號具有極高的優先級,CPU 收到後,會立刻停下手頭正在執行的其他任務,轉而響應網卡的請求。不過,硬中斷處理的事情相對簡單且迅速,只爲後續的處理做好鋪墊,把真正耗時的 “細活” 交給軟中斷和其他模塊去完成。

總之現在,這份數據包,已經從網卡內的緩衝區,然後通過 DMA 的方式,拷貝到了內存中的 sk_buffer 這個結構中。由於這個過程完全是由硬件完成的,所以下一步網卡該做的最後一件事,就是通知內核,讓內核去處理這個數據。怎麼通知呢?就是中斷。網卡向 CPU 發起中斷信號,CPU 打斷當前的程序,根據中斷號找到中斷處理程序,開始執行。那我們主要去看,這個網卡收包這個中斷處理程序是什麼,以及它是如何註冊到中斷向量表中的。由於各個類型的網卡驅動程序是不同的,這裏我們拿 e1000 這個網卡驅動來舉例。我們在 e1000_main.c 中找到了這樣一行代碼。

request_irq(netdev->irq, &e1000_intr, ...);

這段代碼的作用就是,當數據包從網卡緩衝區到內存中的 sk_buffer 後發出中斷,將會執行到 e1000_intr 這個中斷處理函數。

二、中斷處理程序 “接手”

CPU 響應硬中斷後,就輪到中斷處理程序登場了。它可是個 “急性子”,首先要做的就是爲剛抵達的這個網絡幀分配一個叫做 sk_buff(套接字緩衝區,socket buffer 的簡稱)的結構體,這個結構體就像是一個 “多功能收納盒”,它不僅能存放網絡包的數據,還附帶了許多與網絡包相關的重要信息,諸如源地址、目的地址、協議類型等等。接着,中斷處理程序會迅速把網卡收包隊列中的數據拷貝到 sk_buff 緩衝區中,有了這個 “收納盒”,後續處理數據就方便多了。

不過,中斷處理程序也明白 “好鋼用在刀刃上” 的道理,它不會在這時候耗費過多時間去深度處理數據,畢竟 CPU 還有其他緊急任務等着處理呢。所以,在完成 sk_buff 的分配與數據拷貝後,中斷處理程序就會發起一個軟中斷,然後瀟灑地把接力棒交給軟中斷去處理,自己則快速退出,好讓 CPU 能儘快恢復其他工作。這就好比快遞站收到包裹後,簡單登記入庫,就通知快遞員去派送一樣,專業分工,高效協作。

硬中斷 e1000_intr 幹了什麼?

drivers\net\e1000\e1000_main.c

// 註冊的硬中斷處理函數
static irqreturn_t e1000_intr(int irq, void *data, struct pt_regs *regs) {
   __netif_rx_schedule(netdev);
}
include\linux\netdevice.h
static inline void __netif_rx_schedule(struct net_device *dev) {
    list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
    // 發出軟中斷    
    __raise_softirq_irqoff(NET_RX_SOFTIRQ);
}

沒錯,幾乎啥也沒幹,將網卡設備 dev 放入 poll_list 裏,然後立刻發起了一次軟中斷,然後就結束了。軟中斷原理這裏不敘述,其實就是修改 pending 的某個標誌位,然後內核中有一個線程不斷輪詢這組標誌位,看哪個是 1 了,就去軟中斷向量表裏,尋找這個標誌位對應的處理程序,然後執行它。

三、軟中斷 “喚醒” 內核協議棧

這是爲了儘快響應硬中斷,以便計算機可以儘快處理下一個硬中斷,畢竟鼠標點擊、鍵盤敲擊等需要響應特別及時。而像網絡包到來後的拷貝和解析過程,在硬中斷面前優先級沒那麼高,所以就觸發一個軟中斷等着內核線程去執行就好了。收到軟中斷信號後,就輪到內核協議棧 “大展身手” 了。

內核協議棧如同一個經驗豐富的 “安檢員”,它會從 sk_buff 緩衝區中取出網絡幀,然後按照鏈路層、網絡層、傳輸層的順序,逐層對網絡包進行仔細的檢查與處理。

在鏈路層,它會嚴謹地檢查報文的合法性,像是查看幀的校驗和是否正確,確保數據沒有在傳輸過程中出現損壞。同時,它還會精準地找出上層協議的類型,判斷這個包是基於 IPv4 還是 IPv6 協議的,接着去掉幀頭、幀尾這些傳輸過程中附加的 “包裝”,把 “純淨” 的數據交給網絡層。

網絡層拿到數據後,會取出 IP 頭,依據其中的信息判斷網絡包下一步該何去何從,是交給上層的應用程序處理,還是需要轉發到其他設備。當確認這個包是要發送到本機後,網絡層就會取出上層協議的類型,比如是 TCP 還是 UDP,然後去掉 IP 頭,把接力棒遞給傳輸層。

傳輸層接過網絡包後,會取出 TCP 頭或者 UDP 頭,根據 <源 IP、源端口、目的 IP、目的端口> 四元組作爲獨一無二的標識,迅速找出對應的 Socket。就好比快遞員根據收件地址找到對應的收件人家一樣,精準無誤。找到後,把數據拷貝到 Socket 的接收緩存中,至此,網絡包已經順利抵達了 “家門口”,就等着應用程序來取用了。

3.1 註冊軟中斷處理程序

剛剛代碼中我們就觸發了一個值爲 NET_RX_SOFTIRQ 的軟中斷,那這個軟中斷會執行到哪個軟中斷處理函數呢?內核早在網絡子系統初始化的過程中,把這個軟中斷對應的處理函數註冊好了。net\core\dev.c

static int __init net_dev_init(void) {
    open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
    open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
}

// transmit 發送
static void net_tx_action(struct softirq_action *h) {...}
// receive 接收
static void net_rx_action(struct softirq_action *h) {...}

這個 open_softirq 就是註冊一個軟中斷函數,很簡單,就是把這個函數賦值給軟中斷向量表中對應位置的 action 上。

這裏註冊了兩個軟中斷,一個發送,一個接收。我們這次是接收,所以軟中斷觸發後,就執行到了 net_rx_action 這個函數。

3.2 軟中斷 net_rx_action 幹了什麼

直接看!net\core\dev.c

static void net_rx_action(struct softirq_action *h) {
    struct softnet_data *queue = &__get_cpu_var(softnet_data);   
    while (!list_empty(&queue->poll_list)) {
        struct net_device dev = list_entry(
            queue->poll_list.next, struct net_device, poll_list);
        dev->poll(dev, &budget)}
}

遍歷 poll_list 取出一個個的設備 dev,然後調用其 poll 函數。

還記得我們發起軟中斷前的一行代碼吧?正是把當前有數據包到來的這個網卡設備 dev 放入了這個 poll_list,現在又取出來了。由於要調用該網卡相應驅動的 poll 函數,那網卡初始化時,e1000 這款網卡的 poll 函數被附上了這個函數地址。

netdev->poll = &e1000_clean;

所以,接下來就看這個函數就好了,聽名字就知道是清理這個網卡的數據包的工作。drivers\net\e1000\e1000_main.c

static int e1000_clean(struct net_device *netdev, int *budget) {
    struct e1000_adapter *adapter = netdev->priv;    
    e1000_clean_tx_irq(adapter);
    e1000_clean_rx_irq(adapter, &work_done, work_to_do);
}

由於本講我們只看讀數據的過程,所以就看 rx 部分就好了。這個函數過長,我們只順着一條線往下跟。

// drivers\net\e1000\e1000_main.c
e1000_clean_rx_irq(struct e1000_adapter *adapter) {
    ...
    netif_receive_skb(skb);
    ...
}

// net\core\dev.c
int netif_receive_skb(struct sk_buff *skb) {
    ...
    list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type)&15], list) {
        ...
        deliver_skb(skb, ptype, 0);
        ...
    }
    ...
}

static __inline__ int deliver_skb(
        struct sk_buff *skb, struct packet_type *pt_prev, int last) {
    ...
    return pt_prev->func(skb, skb->dev, pt_prev);
}

我們看到,一路跟來,執行了 pt_prev 的 func 函數。這個函數是幹嘛的呢?或者先問,這個函數具體的實現指向的是哪個函數呢?這就涉及到協議棧的註冊。

3.3 協議棧的註冊

// net\ipv4\ip_output.c
static struct packet_type ip_packet_type = {
    .type = __constant_htons(ETH_P_IP),
    .func = ip_rcv,
};

void __init ip_init(void) {
    dev_add_pack(&ip_packet_type);
}

// net\core\dev.c
void dev_add_pack(struct packet_type *pt) {
    if (pt->type == htons(ETH_P_ALL)) {
        list_add_rcu(&pt->list, &ptype_all);
    } else {
        hash = ntohs(pt->type) & 15;
        list_add_rcu(&pt->list, &ptype_base[hash]);
    }
}

我們看到,func 被賦值爲了 ip_rcv,那上一步自然就執行到了這個函數,其實就是網絡層交給誰來負責解析的意思。那我們順便把傳輸層的協議註冊也看了吧,不難想到,ip_rcv 這個函數處理完必然交給傳輸層繼續處理。

module_init(inet_init);

static struct inet_protocol tcp_protocol = {
    .handler =  tcp_v4_rcv,
    .err_handler =  tcp_v4_err,
    .no_policy =    1,
};

static struct inet_protocol udp_protocol = {
    .handler =  udp_rcv,
    .err_handler =  udp_err,
    .no_policy =    1,
};

static int __init inet_init(void) {
    inet_add_protocol(&udp_protocol, IPPROTO_UDP);
    inet_add_protocol(&tcp_protocol, IPPROTO_TCP);
    ip_init();
    tcp_init();
}

非常直觀明瞭,記住上面兩個 handler 分別是 tcp_v4_rcv 和 udp_rcv。

四、數據 “奔赴” 應用程序

經過內核協議棧的層層 “安檢”,網絡包終於來到了應用程序的 “家門口”。此時,應用程序正通過 Socket 接口焦急地等待着數據的到來,就像等待快遞上門的收件人。內核會依據 < 源 IP、源端口、目的 IP、目的端口 > 四元組,精準地找到對應的 Socket,然後把數據拷貝到 Socket 的接收緩存中。

至此,網絡包已經成功 “入住” 接收緩存,就等應用程序前來讀取,開啓後續的數據處理流程,爲用戶提供豐富多彩的網絡服務,比如展示網頁內容、播放視頻音頻等等。整個過程,內核就像是一位嚴謹高效的 “管家”,有條不紊地打理着網絡包的接收事務,確保每一個數據包都能準確無誤地找到歸宿,讓網絡世界順暢運行。

隨着網絡技術的飛速發展,人們對網絡性能的要求越來越高,這就促使內核網絡包處理流程需要不斷優化。一方面,優化方向聚焦在減少中斷開銷、優化協議棧處理、提升內存管理效率等方面。例如,採用 NAPI(New API)機制,它結合中斷驅動和輪詢機制,在高負載時,網卡只需產生一次中斷,然後驅動程序進入輪詢模式,批量處理數據包,減少了中斷次數,降低了 CPU 的中斷處理開銷,大大提高了系統的網絡吞吐量。

另一方面,內核網絡包處理也面臨諸多挑戰。在高併發場景下,大量的網絡包瞬間湧入,可能導致收包隊列溢出、內核協議棧處理不及,進而引發丟包現象。而且,隨着網絡應用的多元化,不同協議、不同類型的數據包混雜,如何確保內核能快速、準確地分類處理,也是亟待解決的難題。

此外,硬件性能的瓶頸,如網卡速率、CPU 處理能力,也會對網絡包處理流程造成限制。不過,正是這些挑戰促使着技術人員不斷探索創新,推動內核網絡處理技術持續向前發展,讓我們的網絡世界更加順暢、高效。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/hRgpl9LwIayyhU-wIob6hQ