前言
在我的Rust入門及實戰系列文章中已經說明, Rust是一門內存安全的高性能編程語言,從它的這些優秀特性來看,就是一門專為系統開發而誕生的語言。至于很多使用Rust來進行web開發的行為,不能說它們不好,只能說是殺雞焉用牛刀耳。
本系列的文章旨在為大家介紹一個新的專用于開發eBPF程序的Rust框架:Aya。Aya 是 Rust 中的一個 eBPF(Extended Berkeley Packet Filter)庫,提供了用 Rust 語言編寫、加載和運行 eBPF 程序的能力。eBPF 是一種強大的技術,用于在 Linux 內核中安全地運行沙盒程序,常用于網絡編程、性能監控和安全增強。我們在后面的文章中將對eBPF進行更加詳細和深入的介紹。
Linux內核網絡基礎
如果你還不了解什么是eBPF, 那么其實從它的名稱中便可見一斑,從Packet Filter
可以看出,這顯然是用于包的處理的一門技術。它通過在Linux的內核中的不同掛載點,加入一個隔離可控的二進制程序,來達到我們想要為內核增加功能處理網絡包的目的。既然是在內核的網絡處理流程中動手腳,那么在開始之前,我們有必要對Linux內核原本的網絡處理流程有基本的認知, 否則,如果直接開始eBPF的編寫,那么我們很可能會變成狗拿刺猬,無從下手。
網絡模型概覽
眾所周知,網絡模型的的劃分有不同的方法, 最流行的莫過于經典的OSI七層網絡模型, TCP/IP四層網絡模型,還有綜合兩者而成的五層網絡模型。從理解內核網絡工作原理的角度出發,我們選擇五層網絡模型進行接下來的探究:
注: 在Linux系統中,內核源碼的位置位于/usr/src/
目錄下的對應內核命名的目錄下,例如我使用的azure虛擬機上,內核代碼的目錄為/usr/src/linux-headers-5.15.0-1052-azure
。 后文提到的內核代碼相關的目錄都是以此為根的相對路徑。
Linux內核在收到一個網絡數據包后,首先會由網卡驅動程序(相關內核代碼位于drivers/net/ethernet
中)先進行處理, 然后回交由內核中處理協議棧相關的代碼進行處理(相關內核代碼位于kernel/
和net/
中),處理完成后的結果,再由socket
提供接口,供用戶空間的應用層程序訪問。
網絡中斷處理原理
那么從網卡收到數據包,是怎么傳遞給內核進行處理的呢,這里就要談到中斷處理了。 從硬件的角度來看,當一個網卡收到數據包時, 它會進行以下兩件事情:
- 將數據包以DMA(Direct Memory Access)的方式, 將收到的數據幀存放到內存的環形緩沖區中(Buffer Ring);
- 向CPU的引腳施加一個電壓變化,向CPU表明現在有一個數據來了,需要處理。
上述這種通過向CPU引腳施加電壓的硬件操作,被稱為硬中斷。那么CPU此時就會對收到的數據包進行處理,那么如何處理呢?網絡包如果一直不停的到來,而對網絡包的處理往往又是復雜和耗時的,如果CPU每收到一個數據包都對它進行處理完成后再干別的工作,就會導致CPU的占用率過高,而無法對其他的硬中斷進行響應了,比如鼠標鍵盤等設備發起的硬中斷請求。
軟中斷注冊
因此,當CPU收到一個網卡發來的硬中斷時,它會告訴網卡驅動程序: “你先去內存登記一下待辦事項吧”,于是網卡驅動程序會在內存中標記一個變量,表示這里有一個網絡包需要人手來處理了。這個在內存中設置標志的操作,就被稱為軟中斷。
上述過程的圖示如下:
軟中斷處理
內核驅動程序處理
在我們的Linux啟動后,內核中會運行一個進程ksoftirqd
, 它的職責就是檢測內存中是否有軟中斷需要處理, 一旦檢測到這是一個網絡驅動注冊的軟中斷,就會調用網卡驅動中的poll
函數,從內存的環形緩沖區中將網絡數據包收下來并進行處理, 這個過程的圖示如下:
其中, 網卡驅動程序中的igb_poll()
函數會從內存的環形緩沖區中將完整的網絡數據包取出來,然后調用igb_clean_rx_irq()
函數進行處理,這些處理包括:
- 校驗收到的數據格式是否是一個合法的網絡包;
- 將收到的數據包格式化成
skb
,解析timestamp, VLAN id, protocol等字段信息
內核協議棧處理
在驅動程序對數據包進行處理后,處理完的數據將被發送到內核的協議棧進行處理,在進入協議棧之前,內核中存在一個GRO
引擎,它的作用是把一些小的網絡包合成一個大的網絡包,一次性發給協議棧進行處理,目的是減少傳送給協議棧的包數量,這有助于減少 CPU 的使用量。
如上圖所示,在數據包進入協議棧后:
- 首先會調用
netif_receive_skb()
函數,其中會辨別數據包的網絡層協議,根據網絡層協議調用不同的函數; - 例如判斷得到這個數據包是個IP包,則會接下來調用
ip_rcv()
函數,在其中又會判斷它的傳輸層協議,根據其是TCP
還是UDP
而調用不同的函數; - 例如判斷得到這是一個TCP包,那么將繼續調用
tcp_rcv()
函數對數據進行處理; - 處理完成后的數據可以供用戶通過
socket
訪問;
小結
網絡模塊在Linux內核中及其復雜,上面的介紹以盡可能簡單明了的方式描述了一個數據包從網卡收到它開始,如何被內核進行處理的整個過程。其中包含了CPU硬中斷,ksoftiqrd
線程,軟中斷處理,網卡驅動對數據包的處理,skb
的創建, 網絡協議棧對數據包的處理等過程。
本文沒有涉及用戶空間應用程序從socket
取包的過程,這涉及到recvfrom
系統調用,也是一個比較復雜的話題。本系列文章旨在介紹Rust開發eBPF程序,只關注內核對網絡包的處理流程,因此用戶空間取包不在我們的關注范圍內。在了解了網絡包在內核中的處理流程之后,對于后需eBPF程序的掛載點,我們應當會有更清晰的認識。