DPDK Programmer’s Guide(3)環境抽象層(EAL)

官方文檔查看地址:
http://doc.dpdk.org/guides/prog_guide/env_abstraction_layer.html
PDF下載地址:
https://www.intel.com/content/www/us/en/embedded/technology/packet-processing/dpdk/dpdk-programmers-guide.html

本篇難度係數:★★★★★

3.環境抽象層

環境抽象層(EAL)負責訪問底層資源,如硬件和內存空間。它提供了一個通用接口(API),對應用程序和庫隱藏環境細節。初始化例程的職責是決定如何分配這些資源(即內存空間、設備、計時器、控制檯等等)。

EAL的典型服務包括:

  • 加載和啓動DPDK: DPDK及其應用程序作爲單個應用程序鏈接,必須通過某種方式加載。
  • 核心關聯/分配過程:EAL提供了將執行單元分配給特定核心以及創建執行實例的機制。
  • 系統內存保留:EAL可以方便地保留不同的內存區域,例如用於設備交互的物理內存區域。
  • 跟蹤和調試功能:日誌、dump_stack、panic等等。
  • 實用函數:libc中沒有提供的自旋鎖和原子計數器。
  • CPU特性識別:在運行時確定是否支持特定的特性,例如Intel®AVX。確定當前CPU是否支持編譯二進制文件所針對的特性集。
  • 中斷處理:註冊/取消註冊回調到特定中斷源的接口。
  • 報警功能:接口設置/刪除回調要運行在特定的時間。

3.1在Linux-userland用戶空間執行環境中的EAL
在Linux用戶空間環境中,DPDK應用程序使用pthread庫作爲一個用戶空間應用程序運行。

EAL在hugetlbfs中使用mmap()執行物理內存分配(使用huge page sizes巨頁大小來提高性能)。該內存公開給DPDK服務層,比如Mempool庫。

此時,DPDK服務層將被初始化,然後通過pthread setaffinity調用,將每個執行單元分配給一個特定的邏輯核心,作爲用戶級線程運行。

時間引用由CPU時間戳計數器(TSC)或HPET內核API通過mmap()調用提供。

3.1.1初始化和核心啓動
部分初始化是由glibc的start函數完成的。在初始化時還會執行檢查,以確保配置文件中選擇的微體系結構類型得到CPU的支持。然後,調用main()函數。核心初始化和啓動在rte_eal_init()中完成(請參閱API文檔)。它包括對pthread庫的調用(更具體地說,pthread_self()、pthread_create()和pthread_setaffinity_np())。
在這裏插入圖片描述
請注意

  • 對象的初始化,如內存區、環、內存池、lpm表和哈希表,應該作爲主lcore上整個應用程序初始化的一部分來完成。這些對象的創建和初始化函數不是多線程安全的。然而,一旦初始化,對象本身就可以安全地同時在多個線程中使用。

3.1.2關閉和清理
在初始化EAL資源時,核心組件可以分配hugepage支持的內存等資源。通過調用rte_eal_cleanup()函數,可以釋放在rte_eal_init()期間分配的內存。有關詳細信息,請參閱API文檔。

3.1.3多進程的支持
Linux EAL允許多進程和多線程(pthread)部署模型。有關更多細節,請參見第1章多進程支持。

3.1.4內存映射發現和內存保留
大型連續物理內存的分配是使用hugetlbfs內核文件系統完成的。EAL提供了一個API來在這個連續內存中保留命名的內存區域。內存區域保留API還將爲該內存區域保留的內存的物理地址返回給用戶。

DPDK內存子系統可以運行兩種模式:動態模式和遺留模式。下面將解釋這兩種模式。

請注意

  • 使用rte_malloc提供的api完成的內存保留也由來自hugetlbfs文件系統的頁面支持。

>動態內存模式

目前,這種模式只支持Linux。

在這種模式下,DPDK應用程序對hugepages的使用將根據應用程序的請求進行增減。通過rte_malloc()rte_memzone_reserve()或其他方法進行的任何內存分配都可能導致從系統中保留更多的大頁面。類似地,任何內存釋放位置都可能導致大量頁面被釋放回系統。

在此模式下分配的內存不能保證是 IOVA-連續的。如果需要大量的IOVA- continuous(將“large”定義爲“不止一個頁面”),建議對所有物理設備使用VFIO驅動程序(以便IOVA和VA地址可以相同,從而完全繞過物理地址),或者使用遺留內存模式。

對於必須是IOVA-連續的內存塊,建議使用rte_memzone_reserve()函數,並指定RTE_MEMZONE_IOVA_CONTIG標誌。這樣,內存分配器將確保,無論使用何種內存模式,要麼保留的內存滿足需求,要麼分配失敗。

不需要在啓動時使用-m--socket-mem命令行參數預先分配任何內存,但是仍然可以這樣做,在這種情況下,預先分配的內存將被“固定”(即應用程序永遠不會釋放回系統)。可以分配更多的大頁面,並釋放它們,但是不會釋放任何預先分配的頁面。如果既沒有指定-m也沒有指定--socket-mem,那麼就不會預先分配內存,而是根據需要在運行時分配所有內存。

在動態內存模式中使用的另一個可用選項是--single-file-segments命令行選項。這個選項將把頁面放在單個文件中(每個memseg列表),而不是每個頁面創建一個文件。這通常是不需要的,但是對於像userspace vhost這樣的用例非常有用,因爲只有有限的頁面文件描述符可以傳遞給VirtIO

如果應用程序(或dpdk內部代碼,例如設備驅動程序)希望接收關於新分配內存的通知,可以通過rte_mem_event_callback_register()函數註冊內存事件回調。這將在DPDK的內存映射發生更改時調用回調函數。

如果應用程序(或dpdk內部代碼,例如設備驅動程序)希望得到關於指定閾值以上內存分配的通知(並有機會拒絕它們),那麼還可以通過rte_mem_alloc_validator_callback_register()函數使用分配驗證器回調。

EAL提供了一個默認的驗證器回調,它可以通過一個--socket-limit命令行選項啓用,這是一種限制DPDK應用程序可以使用的最大內存量的簡單方法。

>遺留內存模式

通過指定--legacy-mem命令行切換到EAL,可以啓用此模式。這個切換對FreeBSD沒有影響,因爲FreeBSD只支持遺留模式。

這種模式模仿了EAL的歷史行爲。也就是說,EAL將在啓動時保留所有內存,將所有內存排序爲大的iova連續塊,並且不允許在運行時從系統中獲取或釋放大型頁面。

如果沒有指定-m--socket-mem,那麼將預先分配整個可用的hugepage內存。

>Hugepage分配匹配

通過將--match-allocations命令行開關指定到EAL,可以啓用此行爲。這個開關只支持linux,不支持--legacy-mem--no-huge

一些使用內存事件回調的應用程序可能要求釋放與分配是完全相同的大頁。這些應用程序還可能要求malloc堆中的任何分配不能跨與兩個不同內存事件回調關聯的分配。這些類型的應用程序可以使用Hugepage分配匹配來滿足這兩個需求。這可能導致一些內存使用量的增加,這在很大程度上取決於應用程序的內存分配模式。

>32-bit support

在32位模式下運行時還存在其他限制。在動態內存模式下,默認情況下將預先分配最大2G的VA空間,所有這些空間都將位於主lcore NUMA節點上,除非使用--socket-mem標誌。

在遺留模式下,VA空間只會爲被請求的段預先分配(加上填充,以保持iova的連續性)。

>最大存儲量

DPDK進程中所有可能用於hugepage映射的虛擬內存空間都在啓動時預先分配,從而爲DPDK應用程序的內存容量設置了上限。DPDK內存存儲在段列表中,每個段嚴格來說是一個物理頁面。可以通過編輯以下配置變量來更改啓動時預分配的虛擬內存數量:

  • CONFIG_RTE_MAX_MEMSEG_LISTS控制DPDK可以擁有多少段列表
  • CONFIG_RTE_MAX_MEM_MB_PER_LIST控制每個段列表可以處理多少兆內存
  • CONFIG_RTE_MAX_MEMSEG_PER_LIST控制每個段可以有多少段
  • CONFIG_RTE_MAX_MEMSEG_PER_TYPE控制每種內存類型可以擁有多少段(其中“類型”定義爲“頁面大小+ NUMA節點”組合)
  • CONFIG_RTE_MAX_MEM_MB_PER_TYPE控制每種內存類型可以處理多少兆字節的內存
  • CONFIG_RTE_MAX_MEM_MB爲DPDK可以保留的內存總量設置了一個全局最大值

通常,這些選項不需要更改。

請注意
預先分配的虛擬內存不要與預先分配的內存混淆!所有DPDK進程在啓動時預先分配虛擬內存。稍後可以將Hugepages映射到預先分配的VA空間(如果啓用了動態內存模式),並且可以在啓動時選擇將其映射到該空間。

>段文件描述符

在Linux上,在大多數情況下,EAL將在EAL中存儲段文件描述符。由於glibc庫的潛在限制,當使用較小的頁面大小時,這可能會成爲一個問題。例如,像select()這樣的Linux API調用可能無法正常工作,因爲glibc不支持超過一定數量的文件描述符。

這個問題有兩種可能的解決辦法。推薦的解決方案是使用--single-file-segments模式,因爲該模式不會爲每個頁面使用文件描述符,並且它將保持與Virtio和vhost-user後端之間的兼容性。當使用--legacy-mem模式時,此選項不可用。

另一個選擇是使用更大的頁面大小。由於覆蓋相同內存區域所需的頁面更少,所以EAL將在內部存儲更少的文件描述符。

3.1.5支持外部分配的內存

可以在DPDK中使用外部分配的內存。使用外部分配的內存有兩種方式:malloc堆API和手工內存管理。

>爲外部分配的內存使用堆API

使用一組malloc堆API是使用DPDK中外部分配的內存的推薦方法。通過這種方式,通過重載套接字ID來實現對外部分配內存的支持——外部分配的堆將具有套接字ID,在正常情況下,這些套接字ID將被認爲是無效的。請求從指定的外部分配內存中進行分配是向DPDK分配器提供正確的套接字ID的問題,可以直接(例如通過調用rte_malloc),也可以間接(通過數據結構特定的分配API,例如rte_ring_create)。使用這些API還可以確保在添加到DPDK malloc堆的任何內存段上也可以執行鍼對DMA的外部分配內存的映射。

由於DPDK無法驗證內存是否可用或有效,所以這個責任就落在了用戶的肩上。所有多進程同步也是用戶的責任,並確保所有添加/附加/分離/刪除內存的調用都按正確的順序執行。它不需要附加到所有進程的內存區域—只需要根據需要附加到內存區域。

預期工作流程如下:

  • 獲取指向內存區域的指針
  • 創建一個命名堆
  • 將內存區域添加到堆中
    • 如果沒有指定IOVA表,則會假定IOVA地址不可用,並且不會執行DMA映射
    • 其他進程必須先附加到內存區域,然後才能使用它
  • 獲取用於堆的套接字ID
  • 使用正常的DPDK分配過程,使用提供的套接字ID
  • 如果不再需要內存區域,則可以從堆中刪除它
    • 其他進程必須從該內存區域中分離,然後才能刪除它
  • 如果不再需要heap,則刪除它
    • 套接字ID將變爲無效且不能重用
      有關更多信息,請參考rte_malloc API文檔,特別是rte_malloc_heap_*函數調用家族。

>使用沒有DPDK API的外部分配內存

雖然使用堆API是在DPDK中使用外部分配內存的推薦方法,但是在某些用例中,DPDK堆API的開銷是不受歡迎的——例如,當在外部分配的區域上執行手動內存管理時。爲了支持不將外部分配的內存用作普通DPDK工作流的一部分的用例,在rte_extmem_*名稱空間下還有另一組API。

這些API(顧名思義)允許註冊或註銷DPDK內部頁表的外部分配內存,允許rte_virt2memseg等API處理外部分配的內存。以這種方式添加的內存將不能用於任何常規的DPDK分配器;DPDK將把這些內存留給用戶應用程序來管理。

預期工作流程如下:

  • 獲取指向內存區域的指針
  • 在DPDK中註冊內存
    • 如果沒有指定IOVA表,則假定IOVA地址不可用
    • 其他進程必須先附加到內存區域,然後才能使用它
  • 如果需要,使用rte_dev_dma_map執行DMA映射
  • 在應用程序中使用內存區域
  • 如果不再需要內存區域,則可以註銷它
    • 如果爲DMA映射了該區域,則必須在註銷內存之前執行取消映射
    • 其他進程必須從內存區域中分離,然後才能註銷

由於這些外部分配的內存區域不會由DPDK管理,因此由用戶應用程序決定如何使用它們以及註冊後如何處理它們。

3.1.6每個lcore和共享變量

請注意

lcore指處理器的邏輯執行單元,有時也稱爲硬件線程。

共享變量是默認行爲。每個lcore變量使用線程本地存儲(TLS)實現,以提供每個線程的本地存儲。

3.1.7日誌

EAL提供了一個日誌API。默認情況下,在Linux應用程序中,日誌被髮送到syslog和控制檯。但是,用戶可以重寫log函數來使用不同的日誌記錄機制。

3.1.7.1跟蹤和調試功能

在glibc中有一些調試函數可以轉儲堆棧。rte_panic()函數可以自動觸發SIG_ABORT,後者可以觸發生成核心文件,gdb可以讀取該文件。

3.1.8CPU功能鑑定

EAL可以在運行時查詢CPU(使用rte_cpu_get_features()函數)來確定哪些CPU特性可用。

3.1.9用戶空間中斷事件

  • 主機線程中的用戶空間中斷和報警處理

EAL創建一個主機線程來輪詢UIO設備文件描述符來檢測中斷。回調可以由EAL函數爲特定的中斷事件註冊或註銷,並在主機線程中異步調用。EAL還允許以與NIC中斷相同的方式使用定時回調。

請注意
在DPDK PMD中,專用主機線程處理的惟一中斷是用於更改鏈接狀態(向上鏈接和向下鏈接通知)和突然刪除設備的中斷。

  • RX中斷事件

每個PMD提供的接收和傳輸例程不限制自己在輪詢線程模式下執行。要使用較小的吞吐量來緩解空閒輪詢,最好暫停輪詢並等待喚醒事件的發生。RX中斷是這種喚醒事件的首選,但可能不是惟一的。

EAL爲這種事件驅動的線程模式提供了事件api。以Linux爲例,實現依賴於epoll。每個線程都可以監視一個epoll實例,其中添加了所有喚醒事件的文件描述符。事件文件描述符是根據UIO/VFIO規範創建並映射到中斷向量的。從FreeBSD的角度來看,kqueue是另一種方法,但尚未實現。

EAL初始化事件文件描述符和中斷向量之間的映射,而每個設備初始化中斷向量和隊列之間的映射。這樣一來,EAL實際上並不知道特定向量上的中斷原因。eth_dev驅動程序負責爲後者的映射編寫程序。

請注意
每個隊列RX中斷事件只允許在支持多個MSI-X向量的VFIO中使用。在UIO中,RX中斷與其他中斷原因共享相同的向量。在本例中,當RX中斷和LSC(鏈接狀態更改)中斷都啓用時(intr_conf.lsc == 1 && intr_conf.rxq == 1),只有前者是可行的。

RX中斷由APIs - ‘rte_eth_dev_rx_intr_*’控制/啓用/禁用。如果PMD還沒有支持它們,它們將返回失敗。intr_conf。rxq標誌用於打開每個設備的RX中斷功能。

  • 設備移除事件

此事件由在總線級別刪除的設備觸發。它的底層資源可能已經不可用(即PCI映射unmapped)。PMD必須確保在發生這種情況時,應用程序仍然可以安全地使用它的回調。

可以以訂閱鏈接狀態更改事件的相同方式訂閱此事件。因此,執行上下文是相同的,即它是專用的中斷主機線程。

考慮到這一點,應用程序很可能希望關閉發出設備刪除事件的設備。在這種情況下,調用rte_eth_dev_close()可以觸發它註銷自己的設備刪除事件回調。必須注意不要從中斷處理程序上下文中關閉設備。有必要重新安排這種關閉操作的時間。

3.1.10黑名單

EAL PCI設備黑名單功能可用於將某些NIC端口標記爲黑名單,因此它們將被DPDK忽略。要列入黑名單的端口使用PCIe* description(Domain:Bus:Device.Function)進行標識。

3.1.11Misc功能

鎖和原子操作是按體系結構進行的(i686和x86_64)。

3.1.12IOVA模式配置

基於探測總線和IOMMU配置的IOVA模式的自動檢測,在沒有直接連接到總線的虛擬設備存在時,可能不會報告所需的尋址模式。爲了方便將IOVA模式強制爲特定值,EAL命令行選項--iova-mode可用於選擇物理尋址(’ pa ‘)或虛擬尋址(’ va ')。

3.2內存段和內存區域(memzone)

物理內存的映射是由EAL中的這個特性提供的。由於物理內存可能有間隙,內存在一個描述符表中進行描述,每個描述符(稱爲rte_memseg)描述一個物理頁面。

除此之外,memzone分配器的作用是保留物理內存的連續部分。當內存被保留時,這些區域由一個惟一的名稱標識。

rte_memzone描述符也位於配置結構中。使用rte_eal_get_configuration()訪問該結構。內存區域的查找(按名稱)返回包含內存區域物理地址的描述符。

通過提供align參數,可以使用特定的起始地址對齊來保留內存區域(默認情況下,它們與高速緩存線大小對齊)。對齊值應爲2的冪,且不小於高速緩存線大小(64字節)。還可以從2mb或1gb的大頁中保留內存區域,前提是這兩個內存區域在系統上都可用。

memsegs和memzone都使用rte_fbarray結構存儲。有關更多信息,請參考DPDK API Reference。

3.3多個pthread

DPDK通常爲每個CPU核固定一個pthread,以避免任務切換的開銷。這可以顯著提高性能,但是缺乏靈活性,而且並不總是有效的。

電源管理通過限制CPU運行時頻率來幫助提高CPU效率。但是,也可以利用可用的空閒週期來充分利用CPU的全部功能。

通過利用cgroup,可以簡單地分配CPU利用率配額。這給了另一種提高CPU效率的方法,但是,有一個先決條件;DPDK必須處理每個CPU核的多個pthread之間的上下文切換。

爲了獲得更大的靈活性,不僅可以將pthread關聯設置爲CPU,還可以設置爲CPU集。

3.3.1EAL pthread與lcore的親和力

術語“lcore”指的是一個EAL線程,它實際上是一個Linux/FreeBSD pthread。“EAL pthreads”由EAL創建和管理,並執行由remote_launch發出的任務。在每個EAL pthread中,都有一個名爲_lcore_id的TLS(線程本地存儲)用於惟一標識。由於EAL pthreads通常以1:1的比例綁定到物理CPU,所以_lcore_id通常等於CPU ID。

然而,當使用多個pthread時,EAL pthread與指定的物理CPU之間的綁定不再總是1:1。EAL pthread可能與CPU集有親緣關係,因此_lcore_id與CPU ID不相同。因此,定義了一個EAL long選項“- lcore”來分配lcore的CPU親緣關係。對於指定的lcore ID或ID組,該選項允許爲該EAL pthread設置CPU集。

格式模式:

–lcores=’<lcore_set>[@cpu_set][,<lcore_set>[@cpu_set],...]’

“lcore_set”和“cpu_set”可以是單個數字、範圍或組。

A number is a “digit([0-9]+)”; a range is “-”; a group is “(<number|range>[,<number|range>,…])”.

如果沒有提供’ @cpu_set ‘值,’ cpu_set ‘的值將默認爲’ lcore_set '的值。

For example, "--lcores='1,[email protected](5-7),(3-5)@(0,2),(0,6),7-8'" which means start 9 EAL thread;
    lcore 0 runs on cpuset 0x41 (cpu 0,6);
    lcore 1 runs on cpuset 0x2 (cpu 1);
    lcore 2 runs on cpuset 0xe0 (cpu 5,6,7);
    lcore 3,4,5 runs on cpuset 0x5 (cpu 0,2);
    lcore 6 runs on cpuset 0x41 (cpu 0,6);
    lcore 7 runs on cpuset 0x80 (cpu 7);
    lcore 8 runs on cpuset 0x100 (cpu 8).

使用此選項,可以爲每個給定的lcore ID分配關聯的cpu。它還兼容corelist(’ -l ')選項的模式。

3.3.2non-EAL pthread支持

可以將DPDK執行上下文與任何用戶pthread(aka. Non-EAL pthreads)一起使用。在non-EAL pthread中,_lcore_id始終是LCORE_ID_ANY,它標識它不是一個具有有效的、惟一的_lcore_id的EAL線程。一些庫將使用一個可選的惟一ID(例如TID),一些庫將完全不受影響,還有一些庫可以工作,但有限制(例如timer和mempool庫)。

所有這些影響都在“已知問題( Known Issues)”一節中提到。

3.3.3公共線程API

爲線程引入了兩個公共API rte_thread_set_affinity()rte_thread_get_affinity()。當它們在任何pthread上下文中使用時,將設置/獲取線程本地存儲(TLS)。
這些TLS包括_cpuset和_socket_id:

  • _cpuset存儲pthread被仿射到的cpu位圖。
  • _socket_id存儲CPU集中的NUMA節點。如果CPU集中的CPU屬於不同的NUMA節點,則將_socket_id設置爲SOCKET_ID_ANY。

3.3.4控制線程API

可以使用公共API rte_ctrl_thread_create()創建控制線程。這些線程可用於管理/基礎設施任務,並由DPDK在內部用於多進程支持和中斷處理。

這些線程將被調度在CPU上,這是原始進程CPU關聯性的一部分,其中數據平面和服務lcore被排除在外。

例如,在8個cpu系統上,用- l2,3 (dataplane core)啓動一個dpdk應用程序,然後根據可以使用taskset (Linux)或cpuset (FreeBSD)等工具控制的關聯配置

  • 如果沒有關聯配置,控制線程最終將位於0-1,4-7cpu上。
  • 當關聯限制爲2-4時,控制線程將終止於CPU 4。
  • 當關聯限制爲2-3時,控制線程將終止於CPU 2(主lcore,這是在沒有CPU可用時的默認值)。

3.3.5已知的問題

  • rte_mempoolrte_
    rte_mempool在mempool中使用每個lcore的緩存。對於non-EAL pthreads, rte_lcore_id()將不會返回一個有效的數字。因此,現在,當rte_mempool與non-EAL pthreads一起使用時,put/get操作將繞過默認的mempool緩存,由於這種繞過,性能會受到影響。只有用戶擁有的外部緩存才能在non-EAL上下文中與接受顯式緩存參數的rte_mempool_generic_put()rte_mempool_generic_get()一起使用。

  • rte_ring
    rte_ring支持多生產者進入隊列和多消費者退出隊列。然而,它是不可搶佔的,這對使rte_mempool不可搶佔有一定的影響。

請注意
“非搶佔式”約束是指:在給定環上執行多生產者隊列的pthread不能被在同一環上執行多生產者隊列的pthread搶佔。在一個給定的環上執行多消費者下隊列操作的pthread不能被另一個在同一環上執行多消費者下隊列操作的pthread搶佔。繞過這個約束可能會導致第二個pthread旋轉,直到第一個pthread再次被調度。此外,如果第一個pthread被具有更高優先級的上下文搶佔,它甚至可能導致死鎖。

這意味着,涉及可搶佔pthreads的用例應該仔細考慮使用rte_ring。

  1. 它可以用於可搶佔的單生產者和單消費者用例。
  2. 它可以用於不可搶佔的多生產者和可搶佔的單消費者用例。
  3. 它可以用於可搶佔的單生產者和不可搶佔的多消費者用例。
  4. 它可以由可搶佔的多生產者和/或可搶佔的多消費者pthread使用,這些pthread的調度策略都是SCHED_OTHER(cfs)、SCHED_IDLE或SCHED_BATCH。在使用它之前,用戶應該意識到性能損失。
  5. 多生產者/消費者pthread不能使用它,它們的調度策略是SCHED_FIFO或SCHED_RR。

或者,應用程序可以使用無鎖堆棧mempool處理程序。在考慮這個處理程序時,請注意:
它目前僅限於x86_64平臺,因爲它使用的指令(16字節比較和交換)在其他平臺上還不可用。
它的平均情況性能比非搶佔式rte_ring差,但是軟件緩存(例如mempool緩存)可以通過減少堆棧訪問次數來緩解這種情況。

  • rte_timer

不允許在非eal pthread上運行rte_timer_manage()。但是,允許從non-EAL pthread重置/停止計時器。

  • rte_log

在非eal pthread中,沒有每個線程的日誌級別和日誌類型,而是使用全局日誌級別。

  • mis

cnon-EAL pthread中不支持rte_ring、rte_mempool和rte_timer的調試統計信息。

3.3.6cgroup控制

下面是cgroup控件使用的一個簡單示例,在同一個核心($CPU)上有兩個pthread (t0和t1)執行包I/O。我們預計只有50%的CPU花費在包IO上。

mkdir /sys/fs/cgroup/cpu/pkt_io
mkdir /sys/fs/cgroup/cpuset/pkt_io

echo $cpu > /sys/fs/cgroup/cpuset/cpuset.cpus

echo $t0 > /sys/fs/cgroup/cpu/pkt_io/tasks
echo $t0 > /sys/fs/cgroup/cpuset/pkt_io/tasks

echo $t1 > /sys/fs/cgroup/cpu/pkt_io/tasks
echo $t1 > /sys/fs/cgroup/cpuset/pkt_io/tasks

cd /sys/fs/cgroup/cpu/pkt_io
echo 100000 > pkt_io/cpu.cfs_period_us
echo  50000 > pkt_io/cpu.cfs_quota_us

3.4Malloc

EAL提供了一個malloc API來分配任意大小的內存。

這個API的目標是提供類似於malloc-like的函數,允許從hugepage內存分配,並促進應用程序移植。DPDK API參考手冊描述了可用的函數。

通常,這些類型的分配不應該在數據平面處理中執行,因爲它們比基於池的分配慢,並且使用了分配和自由路徑中的鎖。但是,它們可以在配置代碼中使用。

有關更多信息,請參考DPDK API參考手冊中的rte_malloc()函數描述。

3.4.1信息記錄程序

當啓用CONFIG_RTE_MALLOC_DEBUG時,分配的內存包含覆蓋保護字段,以幫助識別緩衝區溢出。

3.4.2對齊和NUMA約束

rte_malloc()接受一個align參數,該參數可用於請求在該值的倍數上對齊的內存區域(該值必須是2的冪)(which must be a power of two)。

在支持NUMA的系統上,對rte_malloc()函數的調用將返回在執行調用的CPU核的NUMA套接字上分配的內存。還提供了一組api,允許顯式內存分配在NUMA直接套接字,或分配NUMA插座的另一個核心所在,如果所使用的內存是一個邏輯核心以外的內存分配。

3.4.3用例

這個API用於在初始化時需要類mallocs函數的應用程序。對於在運行時分配/釋放數據,在應用程序的快速路徑中,應該使用內存池庫。

3.4.4內部實現

3.4.4.1數據結構

malloc庫內部使用了兩種數據結構類型:

  • struct malloc_heap—用於在每個套接字的基礎上跟蹤空閒空間
  • struct malloc_elem—庫中分配和自由空間跟蹤的基本元素。

3.4.4.1.1結構:malloc_heap

malloc_heap結構用於基於每個套接字管理空閒空間。在內部,每個NUMA節點有一個堆結構,這允許我們根據這個線程運行的NUMA節點將內存分配給一個線程。雖然這並不保證內存將在NUMA節點上使用,但它並不比總是在固定或隨機節點上分配內存的方案差。

堆結構的關鍵字段及其功能描述如下(見上圖):

  • lock——lock字段用於同步對堆的訪問。假設使用鏈表跟蹤堆中的空閒空間,我們需要一個鎖來防止兩個線程同時操作該列表。
  • free_head——指向這個malloc堆的空閒節點列表中的第一個元素。
  • first——指向堆中的第一個元素。
  • last——指向堆中的最後一個元素。
    在這裏插入圖片描述Fig. 3.2 Example of a malloc heap and malloc elements within the malloc library

3.4.4.1.2結構:malloc_elem

malloc_elem結構用作各種內存塊的通用頭結構。它有兩種不同的用法——都在上面的圖表中顯示:

  1. 作爲一個頭塊上的空閒或分配的內存-正常情況下
  2. 作爲內存塊內的填充頭

結構中最重要的字段及其使用方法如下所述。

Malloc堆是一個雙鏈表,其中每個元素都跟蹤它的前一個和下一個元素。由於大分頁內存可以來來去去,所以相鄰的malloc元素在內存中不一定是相鄰的。此外,由於malloc元素可以跨多個頁面,所以它的內容也不一定是iova連續的——每個malloc元素只保證是虛擬連續的。

請注意
如果沒有描述上述三種用法之一中特定字段的用法,則可以假定該字段在這種情況下具有未定義的值,例如,對於填充頭,只有“state”和“pad”字段具有有效值。

  • heap——這個指針是對分配這個塊的堆結構的引用。它用於釋放普通內存塊時,將新釋放的內存塊添加到堆的空閒列表中。
  • priv——這個指針指向內存中以前的頭元素/塊。當釋放一個塊時,這個指針用於引用前一個塊,以檢查該塊是否也是空閒的。如果是這樣,並且這兩個塊立即相鄰,那麼這兩個空閒塊合併成一個更大的塊。
  • next——這個指針指向內存中的下一個頭元素/塊。當釋放一個塊時,這個指針用於引用下一個塊,以檢查該塊是否也是空閒的。如果是這樣,並且這兩個塊立即相鄰,那麼這兩個空閒塊合併成一個更大的塊。
  • free_list——這是一個指向堆的空閒列表中的前一個和下一個元素的結構。它只在普通內存塊中使用;在malloc()上查找要分配的適當空閒塊,在free()上將新釋放的元素添加到空閒列表。
  • state——該字段可以有三個值之一:FREEBUSYPAD。前兩個是顯示正常的內存塊的分配狀態,後者是表明元素結構是一個虛擬的結構最終start-of-block填充,即在數據塊的開始不是塊本身的開始時,由於一致性約束。在這種情況下,pad頭用於定位塊的實際malloc元素頭。
  • pad——它包含塊開始處的填充的長度。對於普通塊標頭,它被添加到標頭末尾的地址中,以給出數據區域開始的地址,即在malloc上傳遞迴應用程序的值。在填充內的虛擬標頭中,存儲相同的值,並從虛擬標頭的地址中減去該值,以生成實際塊標頭的地址。
  • size——數據塊的大小,包括頭本身。

3.4.4.2內存分配

在EAL初始化時,所有預分配的內存段都設置爲malloc堆的一部分。這種設置包括在每個幾乎相鄰的內存段的開始處放置一個帶有FREE的元素標頭。然後,FREE元素被添加到malloc堆的free_list中。

只要在運行時分配內存(如果支持),這種設置也會發生,在這種情況下,新分配的頁面也會添加到堆中,如果有相鄰的空閒段,則與它們合併。

當應用程序調用類似於malloc函數時,malloc函數將首先索引調用線程的lcore_config結構,並確定該線程的NUMA節點。NUMA節點用於索引malloc_heap結構數組,該數組作爲參數傳遞給heap_alloc()函數,以及請求的大小、類型、對齊方式和邊界參數。

heap_alloc()函數將掃描堆的free_list,並嘗試找到一個適合存儲具有請求對齊和邊界約束的請求大小的數據的空閒塊。

當確定了合適的空閒元素後,將計算返回給用戶的指針。緊位於這個指針之前的內存緩存行被一個struct malloc_elem頭文件填充。由於對齊和邊界約束,元素的開始和/或結束處可能存在自由空間,導致如下行爲:

  • 檢查尾隨空格。如果尾隨空間足夠大, i.e.即> 128字節,則分割空閒元素。如果不是,那麼我們就忽略它(浪費空間)。
  • 檢查元素開頭是否有空格。如果開始的空間很小,例如<=128字節,那麼使用pad頭,剩餘的空間將被浪費。但是,如果剩餘的空間更大,則會分割空閒元素。

分配內存的優勢從現有的空閒列表的元素是沒有調整需要,現有的元素在空閒列表的大小值調整,和下一個/之前的元素“上一頁”/“下一個”重定向到新創建的元素的指針。

如果堆中沒有足夠的內存來滿足分配請求,EAL將嘗試從系統中分配更多的內存(如果支持),並且在成功分配之後,將再次嘗試保留內存。在多處理場景中,所有主進程和輔助進程將同步它們的內存映射,以確保任何指向DPDK內存的有效指針在當前運行的所有進程中始終有效。

在其中一個進程中同步內存映射失敗將導致分配失敗,即使其中一些進程可能已經成功分配了內存。除非主進程確保所有其他進程都成功映射了該內存,否則不會將內存添加到malloc堆中。

任何成功的分配事件都將觸發回調,用戶應用程序和其他DPDK子系統可以註冊回調。此外,如果新分配的內存超過用戶設置的閾值,則會在分配之前觸發驗證回調,從而允許或拒絕分配。

請注意
任何新頁面的分配都必須經過主進程。如果主進程不是活動的,那麼即使理論上可以分配內存,也不會分配內存。這是因爲主進程的進程映射充當應該映射或不應該映射什麼的權威,而每個輔助進程都有自己的本地內存映射。輔助進程不更新共享內存映射,它們只將其內容複製到本地內存映射。

3.4.4.3釋放內存
爲了釋放內存區域,指向數據區域開始的指針被傳遞給空閒函數。從這個指針中減去malloc_elem結構的大小,得到塊的元素頭。如果這個頭是PAD類型的,則從指針中進一步減去PAD長度,以獲得整個塊的適當元素頭。

從這個元素頭中,我們得到指向分配塊的堆和必須釋放塊的位置的指針,以及指向前一個和下一個元素的指針。然後檢查這些next和previous元素,看它們是否也是FREE的,是否與當前元素相鄰,如果是,則將它們與當前元素合併。這意味着我們永遠不會有兩個相鄰的FREE內存塊,因爲它們總是合併成一個塊。

如果支持在運行時釋放頁面,並且free元素包含一個或多個頁面,則可以釋放並從堆中刪除這些頁面。如果DPDK是使用預分配內存的命令行參數啓動的(-m或-socket-mem),那麼在啓動時分配的那些頁面將不會被釋放。

任何成功的釋放位置事件都將觸發回調,用戶應用程序和其他DPDK子系統可以註冊回調。

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