DPDK — 數據平面開發技術

目錄

DPDK 完全內核旁路技術實現

DPDK 技術分爲基本技術和優化技術兩類。其中,前者指標準的 DPDK 數據平面開發包和 I/O 轉發實現技術。

DPDK 實現原理

在這裏插入圖片描述

  • 內核協議棧(左邊):網卡 -> 驅動 -> 協議棧 -> Socket 接口 -> 業務。
  • DPDK 基於 UIO(User Space I/O)的內核旁路(右邊):網卡 -> DPDK 輪詢模式-> DPDK 基礎庫 -> 業務。

NOTE:說 DPDK 依賴網卡設備不如說 DPDK 依賴的是網卡設備對應的驅動程序。支持 DPDK 的 NIC Kernel Driver 可以轉換爲 UIO Driver 模式。由此,如有需要,DPDK 實際上是可以在虛擬機上使用的,前提是網卡設備通過 Passthrough 的方式給到虛擬機。所以,該場景中 SR-IOV 網卡會是一個不錯的選擇。

DPDK 架構

在這裏插入圖片描述

在最底部的內核態(Linux Kernel),DPDK 擁有兩個模塊:KNI 與 IGB_UIO。而 DPDK 的上層用戶態由很多庫組成,主要包括核心部件庫(Core Libraries)、平臺相關模塊(Platform)、網卡輪詢模式驅動模塊(PMD-Natives&Virtual)、QoS 庫、報文轉發分類算法(Classify)等幾大類,用戶應用程序可以使用這些庫進行二次開發。下面我們逐一介紹這些組件的功能和作用。

UIO,DPDK 的基石

傳統的收發數據包方式,首先網卡通過中斷方式通知內核協議棧對數據包進行處理,內核協議棧先會對數據包進行合法性進行必要的校驗,然後判斷數據包目標是否爲本機的 Socket,滿足條件則會將數據包拷貝一份向上遞交到用戶態 Socket 來處理。不僅處理路徑冗長,還需要從內核到應用層的一次拷貝過程。

爲了使得網卡驅動(e.g. PMD Driver)運行在用戶態,實現內核旁路。Linux 提供了 UIO(User Space I/O)機制。使用 UIO 可以通過 read() 感知中斷,通過 mmap() 實現和網卡設備的通訊。

簡單來說,UIO 是用戶態的一種 I/O 技術,DPDK 能夠繞過內核協議棧,提供了用戶態 PMD Driver 的支持,根本上是得益於 UIO 技術。DPDK 架構在 Linux 內核中安裝了 IGB_UIO(igb_uio.ko 和 kni.ko.IGB_UIO)模塊,以此藉助 UIO 技術來截獲中斷,並重設中斷回調行爲,從而繞過內核協議棧後續的處理流程。並且 IGB_UIO 會在內核初始化的過程中將網卡硬件寄存器映射到用戶態。

UIO 的實現機制是:對用戶態暴露文件接口。當註冊一個 UIO 設備 uioX 時,就會出現系統文件 /dev/uioX,對該文件的讀寫就是對網卡設備內存的讀寫。除此之外,對網卡設備的控制還可以通過 /sys/class/uio 下的各個文件的讀寫來完成。如下圖:

在這裏插入圖片描述

此外,DPDK 還在用戶態實現了一套精巧的內存池技術,內核態和用戶態之間的的內存交互不進行拷貝,只做控制權轉移。這樣,當收發數據包時,就減少了內存拷貝的開銷。

PMD,DPDK 的核心優化

我們知道,Linux 內核在收包時有兩種方式可供選擇,一種是中斷方式,另外一種是輪詢方式。

從哲學的角度來說,中斷是外界強加給你的信號,你必須被動應對,而輪詢則是你主動地處理事情。前者最大的影響就是打斷你當前工作的連續性,而後者則不會,事務的安排自在掌握。

中斷對性能的影響有多大?在 x86 體系結構中,一次中斷處理需要將 CPU 的狀態寄存器保存到堆棧,並運行中斷服務程序,最後再將保存的狀態寄存器信息從堆棧中恢復。整個過程需要至少 300 個處理器時鐘週期。

輪詢對性能的提升有多大?網卡收到報文後,可以藉助 DDIO(Direct Data I/O)技術直接將報文保存到 CPU 的 Cache 中,或者保存到內存中(沒有 DDIO 技術的情況下),並設置報文到達的標誌位。應用程序則可以週期性地輪詢報文到達的標誌位,檢測是否有新報文需要處理。整個過程中完全沒有中斷處理過程,因此應用程序的網絡報文處理能力得以極大提升。

故此,想要 CPU 執行始終高效,就必然需要一個內核線程去主動 Poll(輪詢)網卡,而這種行爲與當前的內核協議棧是不相容的,即便當前內核協議棧可以使用 NAPI 中斷+輪詢的方式,但依舊沒有根本上解決問題。除非再重新實現一套全新的內核協議棧,顯然這並不現實,但幸運的是,我們可以在用戶態實現這一點。

針對 Intel 網卡,DPDK 實現了基於輪詢方式的 PMD(Poll Mode Drivers)網卡驅動。該驅動由用戶態的 API 以及 PMD Driver 構成,內核態的 UIO Driver 屏蔽了網卡發出的中斷信號,然後由用戶態的 PMD Driver 採用主動輪詢的方式。除了鏈路狀態通知仍必須採用中斷方式以外,均使用無中斷方式直接操作網卡設備的接收和發送隊列。

PMD Driver 從網卡上接收到數據包後,會直接通過 DMA 方式傳輸到預分配的內存中,同時更新無鎖環形隊列中的數據包指針,不斷輪詢的應用程序很快就能感知收到數據包,並在預分配的內存地址上直接處理數據包,這個過程非常簡潔。

PMD 極大提升了網卡 I/O 性能。此外,PMD 還同時支持物理和虛擬兩種網絡接口,支持 Intel、Cisco、Broadcom、Mellanox、Chelsio 等整個行業生態系統的網卡設備,以及支持基於 KVM、VMware、 Xen 等虛擬化網絡接口。PMD 實現了 Intel 1GbE、10GbE 和 40GbE 網卡下基於輪詢收發包。

UIO+PMD,前者旁路了內核,後者主動輪詢避免了硬中斷,DPDK 從而可以在用戶態進行收發包的處理。帶來了零拷貝(Zero Copy)、無系統調用(System call)的優化。同時,還避免了軟中斷的異步處理,也減少了上下文切換帶來的 Cache Miss。

值得注意的是,運行在 PMD 的 Core 會處於用戶態 CPU 100% 的狀態,如下圖:
在這裏插入圖片描述
由於,網絡空閒時 CPU 會長期處於空轉狀態,帶來了電力能耗的問題。所以,DPDK 引入了 Interrupt DPDK(中斷 DPDK)模式。Interrupt DPDK 的原理和 NAPI 很像,就是 PMD 在沒數據包需要處理時自動進入睡眠,改爲中斷通知,接收到收包中斷信號後,激活主動輪詢。這就是所謂的鏈路狀態中斷通知。並且 Interrupt DPDK 還可以和其他進程共享一個 CPU Core,但 DPDK 進程仍具有更高的調度優先級。

在這裏插入圖片描述

IGB_UIO

雖然 PMD 是在用戶態實現的驅動程序,但實際上還是會依賴於內核提供的策略。其中 UIO 內核模塊,是內核提供的用戶態驅動框架,而 igb_uio 是 DPDK 用於與 UIO 交互的內核模塊,通過 igb_uio 來 bind 指定的 PCI 網卡設備到 DPDK 使用。

igb_uio 內核模塊主要功能之一就是用於註冊一個 PCI 設備。實際上這是由 DPDK 提供個一個 Python 腳本 dpdk-devbind 來完成的,當執行 dpdk-devbind 來 bind 網卡時,會通過 sysfs 與內核交互,讓內核使用指定的驅動程序來匹配網卡。具體的行爲是向文件 /sys/bus/pci/devices/(pci id)/driver_override 寫入指定驅動的名稱,或者向 /sys/bus/pci/drivers/igb_uio(驅動名稱)/new_id 寫入要 bind 的網卡設備的 PCI ID。前者是配置設備,讓其選擇驅動;後者是是配置驅動,讓其支持新的 PCI 設備。按照內核的文檔 https://www.kernel.org/doc/Documentation/ABI/testing/sysfs-bus-pci 中提到,這兩個動作都會促使驅動程序 bind 新的網卡設備。

dpdk-devbind 具體的步驟如下

  1. 獲取腳本執行參數指定的網卡(e.g. eth1)設備的 PCI 信息。實際是執行指令 lspci–Dvmmn 查看,主要關注 Slot、Vendor 以及 Device 信息。
Slot: 0000:06:00.1
Class: 0200
Vendor: 8086
Device: 1521
SVendor: 15d9
SDevice: 1521
Rev: 01
  1. unbind 網卡設備之前的 igb 模塊,將 Step 1 中獲取到的 eth1 對應的 Slot 信息 0000:06:00.1 值寫入 igb 驅動的 unbind 文件。e.g. echo 0000:06:00.1 > /sys/bus/pci/drivers/igb/unbind
  2. bind 網卡設備到新的 igb_uio 模塊,將 eth1 的 Vendor 和 Device ID 信息寫入 igb_uio 驅動的 new_id 文件。e.g. echo 0x8086 0x1521 > /sys/bus/pci/drivers/igb_uio/new_id

igb_uio 內核模塊的另一個主要功能就是讓用於態的 PMD 驅動程序得以與 UIO 進行交互

  1. 調用 igbuio_setup_bars,設置 uio_info 的 uio_mem 和 uio_port。
  2. 設置 uio_info 的其他成員。
  3. 調用 uio_register_device,註冊 UIO 設備。
  4. 打開 UIO 設備並註冊中斷。
  5. 調用 uio_event_notify,將註冊的 UIO 設備的 “內存空間” 映射到用戶態的應用空間。其 mmap 的函數爲 uio_mmap。至此,UIO 就可以讓 PMD 驅動程序在用戶態應用層訪問設備的大部分資源了。
  6. 應用層 UIO 初始化。同時,DPDK 還需要把 PCI 設備的 BAR 映射到應用層。在 pci_uio_map_resource 函數中會調用 pci_uio_map_resource_by_index 做資源映射。
  7. 在 PMD 驅動程序中,DPDK 應用程序,會調用 rte_eth_rx_burst 讀取數據報文。如果網卡接收 Buffer 的描述符表示已經完成一個報文的接收(e.g. 有 E1000_RXD_STAT_DD 標誌),則 rte_mbuf_raw_alloc 一個 mbuf 進行處理。
  8. 對應 RTC 模型的 DPDK 應用程序來說,就是不斷的調用 rte_eth_rx_burst 去詢問網卡是否有新的報文。如果有,就取走所有的報文或達到參數 nb_pkts 的上限。然後進行報文處理,處理完畢,再次循環。

KNI

KNI(Kernel NIC Interface,內核網卡接口),是 DPDK 允許用戶態和內核態交換報文的解決方案,模擬了一個虛擬的網口,提供 DPDK 應用程序和 Linux 內核之間通訊沒接。即 KNI 接口允許報文從用戶態接收後轉發到 Linux 內核協議棧中去。

雖然 DPDK 的高速轉發性能很出色,但是也有自己的一些缺點,比如沒有標準協議棧就是其中之一,當然也可能當時設計時就將沒有將協議棧考慮進去,畢竟協議棧需要將報文轉發處理,可能會使處理報文的能力大大降低。

在這裏插入圖片描述

上圖是 KNI 的 mbuf 的使用流程,也可以看出報文的流向,因爲報文在代碼中其實就是一個個內存指針。其中 rx_q 右邊是用戶態,左邊是內核態。最後通過調用 netif_rx 將報文送入 Linux 內核協議棧,這其中需要將 DPDK 的 mbuf 轉換成標準的 skb_buf 結構體。當 Linux 內核向 KNI 端口發送報文時,調用回調函數 kni_net_tx,然後報文經過轉換之後發送到端口上。

核心部件庫

核心部件庫(Core Libraries)是 DPDK 面向用戶態協議棧應用程序員開發的模塊。

  • EAL(Environment Abstraction Layer,環境抽象層):對 DPDK 的運行環境(e.g. Linux 操作系統)進行初始化,包括:HugePage 內存分配、內存/緩衝區/隊列分配、原子性無鎖操作、NUMA 親和性、CPU 綁定等,並通過 UIO 或 VFIO 技術將 PCI/PCIe 設備地址映射到用戶態,方便了用戶態的 DPDK 應用程序調用。同時爲應用程序提供了一個通用接口,隱藏了其與底層庫以及設備打交道的相關細節。

  • MALLOC(堆內存管理組件):爲 DPDK 應用程序提供從 HugePage 內分配堆內存的接口。當需要爲 SKB(Socket Buffer,本質是若干個數據包的緩存區)分配大量的小塊內存時(如:分配用於存儲 Buffer descriptor table 中每個表項指針的內存)可以調用該接口。由於堆內存是從 HugePage 內存分配的,所以可以減少 TLB 缺頁。

NOTE:堆,是由開發人員主動分配和釋放的存儲空間, 若開發人員不釋放,則程序結束時由 OS 回收,分配方式類似於鏈表;與堆不同,棧,是由操作系統自動分配和釋放的存儲空間 ,用於存放函數的參數值、局部變量等,其操作方式類似於數據結構中的棧。

  • MBUF(網絡報文緩存塊管理組件):爲 DPDK 應用程序提供創建和釋放用於存儲數據報文信息的緩存塊的接口。提供了兩種類型的 MBUF,一種用於存儲一般信息,一種用於存儲實際的報文數據。這些 MBUF 存儲在一個內存池中。

  • MEMPOOL(內存池管理組件):爲 DPDK 應用程序和其它組件提供分配內存池的接口,內存池是一個由固定大小的多個內存塊組成的內存容器,可用於存儲不同的對像實體,如:數據報文緩存塊等。內存池由內存池的名稱(一個字符串)進行唯一標識,它由一個 Ring 緩衝區和一組本地緩存隊列組成,每個 CPU Core 優先從自身的緩存隊列中分配內存塊,當本地緩存隊列減少到一定程度時,開始從內存環緩衝區中申請內存塊來進行補充。

  • RING(環緩衝區管理組件):爲 DPDK 應用程序和其它組件提供一個無鎖的多生產者多消費者 FIFO 隊列。

NOTE:DPDK 基於 Linux 內核的無鎖環形緩衝 kfifo 實現了一套自己的無鎖機制。支持單生產者入列/單消費者出列和多生產者入列/多消費者出列操作,在數據傳輸的時候,降低性能的同時還能保證數據的同步。

  • TIMER(定時器組件):提供一些異步週期執行的接口(也可以只執行一次),可以指定某個函數在規定時間內的異步執行,就像 LIBC 中的 timer 定時器。但是這裏的定時器需要 DPDK 應用程序在主循環中週期內調用 rte_timer_manage 來使能定時器,使用起來不那麼方便。TIMER 的時間參考來自 EAL 層提供的時間接口。

核心部件庫對應的 DPDK 核心組件實現

在這裏插入圖片描述

  • RTE:Run-Time Environment
  • EAL:Environment Abstraction Layer
  • PMD:Poll-Mode Driver

Memory Manager(librte_malloc,堆內存管理器):提供一組 API,用於從 HugePages 內存創建的 memzones 中分配內存。

Ring Manager(librte_ring,環形隊列管理器):在一個大小有限的頁表中,Ring 數據結構提供了一個無鎖的多生產者-多消費者 FIFO API。相較於無鎖隊列,它有一些的優勢,如:更容易實現,適應於大容量操作,而且速度更快。 Ring 在 Memory Pool Manager 中被使用,而且 Ring 還用於不同 CPU Core 之間或是 Processor 上處理單元之間的通信。

Memory Pool Manager(librte_mempool,內存池管理器):內存池管理器負責分配的內存中的 Pool 對象。Pool 由名稱唯一標識,並使用一個 Ring 來存儲空閒對象。它提供了其他一些可選的服務,例如:每個 CPU Core 的對象緩存和對齊方式幫助,以確保將填充的對象在所有內存通道上得到均勻分佈。

Network Packet Buffer Management(librte_mbuf,網絡報文緩衝管理):提供了創建和銷燬數據報文緩衝區的能力。DPDK 應用程序中可以使用這些緩存區來存儲消息以及報文數據。

Timer Manager(librte_timer,定時器管理):爲 DPDK 應用程序的執行單元提供了定時服務,爲函數異步執行提供支持。定時器可以設置週期調用或只調用一次。DPDK 應用程序可以使用 EAL 提供的接口獲取高精度時鐘,並且能在每個核上根據需要進行初始化。

平臺相關模塊

平臺相關模塊(Platform)包括 KNI、POWER(能耗管理)以及 IVSHMEM 接口。

  • KNI:主要通過 Linux 內核中的 kni.ko 模塊將數據報文從用戶態傳遞給內核態協議棧處理,以便常規的用戶進程(e.g. Container)可以使用 Linux 內核協議棧傳統的 Socket 接口對相關報文進行處理。

  • POWER:提供了一些 API,讓 DPDK 應用程序可以根據收包速率動態調整 CPU 頻率或讓 CPU 進入不同的休眠狀態。

  • IVSHMEM:模塊提供了虛擬機與虛擬機之間,或者虛擬機與主機之間的零拷貝共享內存機制。當 DPDK 應用程序運行時,IVSHMEM 模塊會調用 Core Libraries 的 API,把幾個 HugePage 內存映射爲一個 IVSHMEM 設備池,並通過參數傳遞給 QEMU,這樣,就實現了虛擬機之間的零拷貝內存共享。

幾個關鍵 API 的使用舉例

  • Buffer Manager API:通過預先從 EAL 上分配固定大小的多個內存對象,避免了在運行過程中動態進行內存分配和回收,以此來提高效率,用於數據包 Buffer 的管理。
  • Queue/Ring Manager API:以高效的方式實現了無鎖的 FIFO 環形隊列,適用於一個生產者多個消費者、一個消費者多個生產者模型。支持批量無鎖操作,可避免鎖衝突導致的等待。
  • Packet Flow Classification API:通過 Intel SSE 基於多元組的方式實現了高效的 HASH 算法,以便快速對數據包進行分類處理。該 API 一般用於路由查找過程中的最長前綴匹配。此外,安全產品場景中,可以根據 DataFlow 五元組來標記不同的用戶。

在這裏插入圖片描述

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