DPDK編程指南(翻譯)(三)

3. 環境抽象層

環境抽象層(EAL)為底層資源如硬件和存儲空間的訪問提供了接口。這些接口為上層應用程序和庫隱藏了不同環境的特殊性。初始化程序負責決定如何分配這些資源(即內存空間、PCI設備、計時器、控制臺等扥)。

EAL提供的服務如下:

  • DPDK的加載和啟動:DPDK和特定的應用程序鏈接成一個獨立進程,并以某種方式加載。
  • CPU親和性和分配處理:EAL提供了將執行單元分配給特定Core及創建執行實例的機制。
  • 系統內存預留:EAL實現了不同區域內存預留,例如用于設備交互的物理內存。
  • PCI地址抽象:EAL提供了對PCI地址空間的訪問接口
  • 跟蹤調試功能:日志信息,堆棧打印、異常掛起等等。
  • 公用功能:提供了標準Libc庫缺失的自旋鎖、原子計數器等。
  • CPU特征識別:運行時確定是否支持指定功能,如Intel AVX。確定當前CPU是否支持二進制編譯的功能集。
  • 中斷處理:提供接口用于向中斷注冊/解注冊中斷處理回調函數。
  • 告警功能:提供接口用于設置/取消指定時間環境下運行的毀掉函數。

3.1. Linux用戶執行環境中的EAL

在Linux用戶空間環境中,DPDK應用程序通過pthread庫作為一個用戶態程序運行。設備的PCI信息和地址空間通過/sys內核接口及內核模塊如uio_pci_generic或igb_uio來發現的。詳細信息請參閱Linux內核文檔中UIO描述,設備的UIO信息是在程序中用mmap重新映射的。

EAL使用mmap接口從hugetlb中實現物理內存的分配。這部分內存暴露給DPDK服務層,如內存池庫

據此,DPDK服務層可以完成初始化,接著通過設置線程親和性調用,每個執行單元將會分配給特定的邏輯核,以一個user-level等級的線程來運行。

定時器是通過CPU的時間戳計數器TSC或者通過mmap調用內核的HPET系統接口實現。

3.1.1.初始化及核心啟動]

部分初始化操作從Glibc的開始函數處就執行了。初始化過程中還執行一個檢查,用于保證配置文件所選擇的微架構類型是本CPU所支持的,然后才開始調用main()函數。Core的初始化和運行是在rte_eal_init()接口上執行的(參考API文檔)。它包括對pthread庫的調用(更具體的說是pthread_self(),pthread_create()和pthread_setaffinity_np())。

Figure3?1EAL Initialization in a Linux Application Environment

注意:對象的初始化,例如內存區間、ring、內存池、lpm表或hash表等,必須作為整個程序初始化的一部分,在主邏輯核上完成。創建和初始化這些對象的函數不是多線程安全的,但是,一旦初始化完成,這些對象本身可以作為安全線程運行。

3.1.2.多進程支持

Linuxapp EAL允許多進程和多線程部署模式。詳細信息請參閱“多進程支持”章節描述。

3.1.3.內存映射發現及內存預留

大型連續的物理內存分配是通過hugetlbfs內核文件系統來實現的。EAL提供了相應的接口用于預留指定名字的連續內存空間。這個API同時會將這段連續空間的地址返回給用戶程序。

注意:內存申請是使用rte_malloc接口來做的,它也是hugetlbfs文件系統大頁支持的。

3.1.4.無Huge-TLB的Xen Domain 0支持

現有的內存管理是基于Linux內核的大頁機制。然而,Xen Dom0并不支持大頁,所以要將一個新的內核模塊rte_dom0_mem加載上,以便避開這個限制。

EAL使用IOCTL接口用于通告Linux內核模塊rte_mem_dom0去申請指定大小的內存塊,并從該模塊中獲取內存段的信息。EAL使用MMAP接口來映射這段內存。對于申請到的內存段,在其內的物理地址都是連續的,但是實際上,硬件地址只在2M內連續。

3.1.5.PCI訪問

EAL使用Linux內核提供的文件系統/sys/bus/pci來掃描PCI總線上的內容。內核模塊uio_pci_generic提供了/dev/uioX設備文件及/sys下對應的資源文件用于訪問PCI設備。DPDK特有的igb_uio模塊也提供了相同的功能用于PCI設備的訪問。這兩個驅動模塊都用到了Linux內核提供的uio特性(用戶空間驅動)。

3.1.6.每邏輯核變量和共享變量

注意: 邏輯核就是處理器的邏輯單元,有時也稱為硬件線程。
默認的做法是使用共享變量。每邏輯核變量的實現則是通過線程局部存儲技術TLS來實現的,它提供了每個線程本地存儲的功能。

3.1.7.日志

EAL提供了日志信息接口。默認情況下,在Linux應用程序中,日志信息被發送到syslog和console中。當然,用戶可以通過使用不同的日志機制來重寫DPDK中的日志函數。

3.1.7.1.跟蹤與調試功能

Glibc中提供了一些調試函數用于打印堆棧信息。Rte_panic()函數可以產生一個SIG_ABORT信號,這個信號可以觸發產生coredump文件,我們可以通過gdb來加載調試。

3.1.8.CPU特性標識

EAL可以在運行時查詢CPU狀態(使用rte_cpu_get_feature()接口),用于判斷哪個CPU特性可用。

3.1.9.用戶空間中斷事件

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

EAL創建一個主機線程用于輪詢UIO設備描述文件描述符以檢測中斷。可以通過EAL提供的函數為特定的中斷事件注冊或注銷回調函數,回掉函數在主機線程中被異步調用。EAL同時也允許像NIC中斷那樣定時調用中斷處理回調。

注意: 在DPDK的PMD中,主機線程只對連接狀態改變的中斷處理,例如網卡的打開和關閉,以及設備突然移除中斷。

3.1.9.2.RX中斷事件

PMD提供的報文收發程序并不只限制于輪詢模式下執行。為了緩解小吞吐量下輪詢模式對CPU資源的浪費,暫停輪詢并等待喚醒事件發生是一種有效的手段。收包中斷是這種場景的一種很好的選擇,當然也不是唯一的。

EAL為事件驅動模式提供了相關的API。以Linuxapp為例,其實現依賴于epoll技術。每個線程可以監控一個epoll實例,而在實例中可以添加所有需要的wake-up事件文件描述符。事件文件描述符根據UIO/VFIO規范創建并映射到指定的中斷向量上。從bspapp角度看,可以使用kqueue來代替,但是目前尚未實現。

EAL初始化中斷向量和事件文件描述符之間的映射關系,同時每個設備初始化中斷向量和隊列之間的映射關系,這樣,EAL實際上并不知道在指定向量上發生的中斷,由設備驅動負責執行后面的映射。

注意:每隊列RX中斷事件只有VFIO模式支持,VFIO支持多個MSI-X向量。在UIO中,RX中斷和其他中斷共享中斷向量,因此,當RX中斷和LSC(連接狀態改變)中斷同時發生時((intr_conf.lsc == 1 && intr_conf.rxq == 1),只有前者才有能力區分。RX中斷由API(rte_eth_dev_rx_intr_*)來實現控制、使能、關閉。當PMD不支持時,這些API返回失敗。Intr_conf.rxq標識用于打開每個設備的RX中斷。

3.1.9.3.設備移除事件

當總線上的設備被移除時就出發該事件。設備底層資源可能不再可用(即PCI映射未完成)。PMD必須保證在這種情況下,應用程序仍然可以安全地使用其中斷回調。
可以使用鏈接狀態改變中斷事件相同的方式來訂閱這個中斷事件。執行上下文是相同的,即專用的中斷線程。

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

3.1.10.黑名單

EAL PCI設備的黑名單功能是用于標識指定的NIC端口,以便DPDK忽略該端口。可以使用PCIe設備地址描述符(Domain:Bus:Device:Function)將對應端口標記為黑名單。

3.1.11.雜項功能

每個架構不同的鎖和原子操作(i686和x86_64)。

3.2.內存段和內存區域

物理內存映射就是通過EAL的這個特性實現的。物理內存塊之間可能是不連續的,所有的內存通過一個內存描述符表進行管理,且表中的每個描述符指向一塊連續的物理內存。

基于此,內存區域分配器的作用就是保證分配到一塊連續的物理內存。這些區域被分配出來時會用一個唯一的名字來標識。

Rte_memzone描述符也在配置結構體中,可以通過rte_eal_get_configuration()接口來獲取。通過名字訪問一個內存區域會返回對應內存區域的描述符。

內存分配可以從指定開始地址和對齊方式來預留(默認是cache line大小對齊),對齊一般是以2的次冪來的,并且不小于高速緩存行的大小(64字節)對齊。內存區域也可以從2M或1G大小的內大頁內存中獲取,這兩者系統都支持。

3.3.多線程

DPDK通常為每個Core指定一個線程,以避免任務切換的開銷。這有利于性能的提升,但不總是有效的,并且缺乏靈活性。

電源管理通過限制CPU的運行頻率來提升CPU的工作效率。當然,我們也可以通過充分利用CPU的空閑周期來使用CPU的全部功能。

通過使用cgroup技術,CPU的使用量可以很方便的分配,這也提供了新的方法來提升CPU性能,但是這里有個前提,DPDK必須處理每個核上多個線程的上下文切換。

想要更多的靈活性,就要設置線程的CPU親和性是針對對CPU集合而不是CPU了。

3.3.1.EAL線程與邏輯核親和性

術語“lcore”指一個EAL線程,這是一個真正意義上的Linux/FreeBSD pthread。“EAL pthread”由EAL創建和管理,并執行remote_launch發出的任務。在每個EAL pthread中,有一個稱為_lcore_id的TLS(線程本地存儲)用于唯一標識線程。由于EAL pthread通常將物理CPU綁定為1:1,所以_locore_id通常等于CPU ID。

但是,當使用多線程時,EAL pthread和指定的物理CPU之間的綁定不再總是1:1了。EAL pthread可能與一組CPU相關,因此_lcore_id將不同于CPU ID。基于這個原因,EAL有一個運行參數選項“-lcores”用來定義分配的CPU親和性。對于執行的lcore ID或ID組,該選項允許設置該EAL pthread的CPU組。

設置格式如下:

注意:
-lcores=’[@cpu_set][,[@cpu_set],…]”
其中lcore_set和cpu_set可以是單個數值,區間或者組。
數值可以是“digit([0-9]+)”
區間可以是“-”
組可以是“([,,...])”

如果‘@cpu_set’值未指定,‘cpu_set’的值默認與‘lcore_set’相等。

舉例:"--lcores='1,2@(5-7),(3-5)@(0,2),(0,6),7-8'"表示啟動了9個EAL pthread:
lcore 0運行于CPU組0x41,也就是CPU(0,6)
lcore 1運行于CPU組0x2,也就是CPU(1)
lcore 2運行于CPU組0xe0,也就是CPU(5,6,7)
lcore 3-5運行于CPU組0x5也就是CPU(0,2)
lcore 6運行于CPU組0x41,也就是CPU(0,6)
lcore 7運行于CPU組0x80,也就是CPU(7)
lcore 8運行于CPU組0x100,也就是CPU(8)
使用這個選項,對于給定的lcore
ID,可以分配對應的CPU組。它也兼容corelist(' - l')選項的模式。

3.3.2.非EAL線程支持

可以在任何用戶線程(non-EAL線程)上執行DPDK任務上下文。在non-EAL pthread中,_lcore_id始終是LCORE_ID_ANY,它標識一個no-EAL線程的有效、唯一的_lcore_id。有些庫可會使用一個唯一的ID替代(如TID),有些庫將不受影響,有些庫則會受到限制(如定時器和內存池庫)。

所有這些影響將在“已知問題”章節中提到。

3.3.3.公用線程API

DPDK為線程操作引入了兩個公共API rte_thread_set_affinity() rte_pthread_get_affinity()。當他們在任何線程上下文中調用時,將獲取或設置線程本地存儲(TLS)。

這些TLS包括_cpuset和_socket_id:

  • _cpuset存儲了與線程親和的CPU位圖。
  • _socket_id存儲了CPU set所在的NUMA節點。如果CPU set中的cpu屬于不同的NUMA節點, _socket_id將設置為SOCKET_ID_ANY。

3.3.4.已知問題

  • lrte_mempool
    rte_mempool在mempool中使用per-lcore緩存。對于non-EAL pthread,rte_lcore_id()無法返回一個合法的值。因此,當rte_mempool與non-EAL線程一起使用時,put/get操作將繞過默認的mempool緩存,這個旁路操作將造成性能損失。結合rte_mempool_generic_put()和rte_mempool_generic_get()可以在non-EAL線程中使用用戶擁有的外部緩存。
  • rte_ring
    rte_ring支持多生產者入隊和多消費者出隊操作。然而,這是非搶占的,這使得rte_mempool操作都是非搶占的。
    注意:“非搶占”意味著:
  • 在給定的ring上做入隊操作的pthread不能被另一個在同一個ring上做入隊的pthread搶占
  • 在給定ring上做出對操作的pthread不能被另一個在同一ring上做出隊的pthread搶占
    繞過此約束則可能造成第二個進程自旋等待,知道第一個進程再次被調度為止。此外,如果第一個線程被優先級較高的上下文搶占,甚至可能造成死鎖。

這并不意味著不能使用它,簡單講,當同一個core上的多線程使用時,需要縮小這種情況.

  1. 它可以用于任一單一生產者或者單一消費者的情況。
  2. 它可以由多生產者/多消費者使用,要求調度策略都是SCHED_OTHER(cfs)。用戶需要預先了解性能損失。
  3. 它不能被調度策略是SCHED_FIFO或SCHED_RR的多生產者/多消費者使用。
  • lrte_timer
    不允許在non-EAL pthread上運行rte_timer_manager()。但是,允許在non-EAL pthread上重置/停止定時器。
  • lrte_log
    在non-EAL pthread上,沒有per thread
    loglevel和logtype,但是global
    loglevels可以使用。
  • lMisc
    在non-EAL pthread上不支持rte_ring,
    rte_mempool和rte_timer的調試統計信息。

3.3.5.cgroup控制

以下是cgroup控件使用的簡單示例,在同一個核心($CPU)上兩個線程(t0 and 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
echo50000 > pkt_io/cpu.cfs_quota_us

3.4.內存申請操作

EAL提供了一個malloc API用于申請任意大小內存。

這個API的目的是提供類似malloc的功能,以允許從hugepage中分配內存并方便應用程序移植。《DPDK API參考手冊》詳細介紹了接口的功能。

通常,這些類型的分配操作不應該在數據面處理中進行,因為他們比基于池的分配慢,并且在分配和釋放路徑中使用了鎖操作。但是,他們可以在配置代碼中使用。
更多信息請參閱《DPDKAPI參考手冊》中rte_malloc()函數描述。

3.4.1.Cookies

當CONFIG_RTE_MALLOC_DEBUG開啟時,分配的內存包括保護字段,這個字段用于幫助識別緩沖區溢出。

3.4.2.對齊和NUMA限制

接口rte_malloc()傳入一個對齊參數,該參數用于請求在該值的倍數上對齊的內存區域(這個值必須是2的冪次)。

在支持NUMA的系統上,對rte_malloc()接口調用將返回在調用函數的Core所在的插槽上分配的內存。DPDK還提供了另一組API,以允許在指定NUMA插槽上直接顯式分配內存,或者分配另一個NUAM插槽上的內存。

3.4.3.用例

這個API旨在由初始化時需要類似malloc功能的應用程序調用。
需要在運行時分配/釋放數據,在應用程序的快速路徑中,應該使用內存池庫。

3.4.4.內部實現

3.4.4.1.數據結構

Malloc庫中內部使用兩種數據結構類型:

  • lstruct malloc_heap:用于在每個插槽上跟蹤可用內存空間
  • lstruct malloc_elem:庫內部分配和釋放空間跟蹤的基本要素
3.4.4.1.1.malloc_heap

數據結構malloc_heap用于管理每個插槽上的可用內存空間。在內部,每個NUMA節點有一個堆結構,這允許我們根據此線程運行的NUMA節點為線程分配內存。雖然這并不能保證在NUMA節點上使用內存,但是它并不比內存總是在固定或隨機節點上的方案更糟。

堆結構及其關鍵字段和功能描述如下:

  • llock:需要鎖來同步對堆結構的訪問。假定使用鏈表來跟蹤堆中的可用空間,我們需要一個鎖來防止多個線程同時處理該鏈表。
  • lfree_head:指向這個malloc堆的空閑結點鏈表中的第一個元素。
Figure3?2Example of a malloc heap and malloc elements within the malloclibrary

注意:數據結構malloc_heap并不會跟蹤使用的內存塊,因為除了要再次釋放它們之外,它們不會i被接觸,需要釋放時,將指向塊的指針作為參數傳遞給free函數。

3.4.4.1.2.malloc_elem

數據結構malloc_elem用作各種內存塊的通用頭結構。它以三種不同的方式使用,如上圖所示:

  • 作為一個釋放/申請內存的頭部,正常使用
  • 作為內存塊內部填充頭
  • 作為內存結尾標記

結構中重要的字段和使用方法如下所述:

  • heap:這個指針指向了該內存塊從哪個堆申請。它被用于正常的內存塊,當他們被釋放時,將新釋放的塊添加到堆的空閑列表中。
  • prev:這個指針用于指向緊跟著當前memseg的頭元素。當釋放一個內存塊時,該指針用于引用上一個內存塊,檢查上一個塊是否也是空閑。如果空閑,則將兩個空閑塊合并成一個大塊。
  • next_free:這個指針用于將空閑塊列表連接在一起。它用于正常的內存塊,在malloc()接口中用于找到一個合適的空閑塊申請出來,在free()函數中用于將內存塊添加到空閑鏈表。
  • state:該字段可以有三個可能值:FREE, BUSY或PAD。前兩個是指示正常內存塊的分配狀態,后者用于指示元素結構是在塊開始填充結束時的虛擬結構,即,由于對齊限制,塊內的數據開始的地方不在塊本身的開始處。在這種情況下,pad頭用于定位塊的實際malloc元素頭。對于結尾的結構,這個字段總是BUSY,它確保沒有元素在釋放之后搜索超過memseg的結尾以供其它塊合并到更大的空閑塊。
  • pad:這個字段為塊開始處的填充長度。在正常塊頭部情況下,它被添加到頭結構的結尾,以給出數據區的開始地址,即在malloc上傳回的地址。在填充虛擬頭部時,存儲相同的值,并從虛擬頭部的地址中減去實際塊頭部的地址。
  • size:數據塊的大小,包括頭部本身。對于結尾結構,這個大小需要指定為0,雖然從未使用。對于正在釋放的正常內存塊,使用此大小值替代“next”指針,以標識下一個塊的存儲位置,在FREE情況下,可以合并兩個空閑塊。

3.4.4.2.內存申請

在EAL初始化時,所有memseg都將作為malloc堆的一部分進行設置。這個設置包括在BUSY狀態結束時放置一個虛擬結構,如果啟用了CONFIG_RTE_MALLOC_DEBUG,它可能包含一個哨兵值,并在開始時為每個memseg指定一個適當的元素頭。然后將FREE元素添加到malloc堆的空閑鏈表中。

當應用程序調用類似malloc功能的函數時,malloc函數將首先為調用線程索引lcore_config結構,并確定該線程的NUMA節點。NUMA節點將作為參數傳給heap_alloc()函數,用于索引malloc_heap結構數組。參與索引參數還有大小、類型、對齊方式和邊界參數。

函數heap_alloc()將掃描堆的空閑鏈表,嘗試找到一個適用于所請求的大小、對齊方式和邊界約束的內存塊。

當已經識別出合適的空閑元素時,將計算要返回給用戶的指針。緊跟在該指針之前的內存的高速緩存行填充了一個malloc_elem頭部。由于對齊和邊界約束,在元素的開頭和結尾可能會有空閑的空間,這將導致已下行為:

  • 檢查尾隨空間。如果尾部空間足夠大,例如> 128字節,那么空閑元素將被分割。否則,僅僅忽略它(浪費空間)。
  • 檢查元素開始處的空間。如果起始處的空間很小,<=128字節,那么使用填充頭,這部分空間被浪費。但是,如果空間很大,那么空閑元素將被分割。

從現有元素的末尾分配內存的優點是不需要調整空閑鏈表,空閑鏈表中現有元素僅調整大小指針,并且后面的元素使用“prev”指針重定向到新創建的元素位置。

3.4.4.3.內存釋放

要釋放內存,將指向數據區開始的指針傳遞給free函數。從該指針中減去malloc_elem結構的大小,以獲得內存塊元素頭部。如果這個頭部類型是PAD,那么進一步減去pad長度,以獲得整個塊的正確元素頭。

從這個元素頭中,我們獲得指向塊所分配的堆的指針及必須被釋放的位置,以及指向前一個元素的指針,并且通過size字段,可以計算下一個元素的指針。這意味著我們永遠不會有兩個相鄰的FREE內存塊,因為他們總是會被合并成一個大的塊。

原文鏈接:http://www.lxweimin.com/p/273e99f61e97

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容