DPDK開發(fā)者指南 - 環(huán)境抽象層

3. 環(huán)境抽象層

環(huán)境抽象層(Environment Abstraction Layer,下文簡稱EAL)是對操作系統(tǒng)底層資源(如內(nèi)存空間)的抽象, 用于DPDK應(yīng)用程序訪問底層資源。 EAL隱藏了不同操作系統(tǒng)訪問底層資源的接口, 給DPDK應(yīng)用程序提供了統(tǒng)一的訪問接口。 EAL的初始化例程負(fù)責(zé)底層資源的申請,如內(nèi)存,PCI設(shè)備等。

EAL中提供的典型服務(wù)有:

DPDK庫的加載和啟動: DPDK庫和DPDK應(yīng)用在編譯階段被鏈接成一個應(yīng)用程序。而且?guī)斓募虞d需要一些額外的操作,這些操作由EAL完成,應(yīng)用開發(fā)者無需特別關(guān)心。

CPU親和性/分配: EAL能把一個執(zhí)行單元分配到指定CPU上運(yùn)行。

系統(tǒng)內(nèi)存預(yù)留: EAL預(yù)留了各種內(nèi)存,比如,用于設(shè)備交互的物理內(nèi)存區(qū)域。

PCI地址抽象: EAL提供了訪問PCI設(shè)備地址空間的接口。

跟蹤和調(diào)試功能: 日志、dump堆棧、panic等等。

實(shí)用功能: 自旋鎖和原子計數(shù)器(這類函數(shù)libc中沒有提供)。

CPU特性標(biāo)識: 在程序運(yùn)行期間決定CPU是否支持指定的特性,如Intel? AVX。 判斷當(dāng)前CPU是否和DPDK庫所支持的CPU匹配。

中斷處理: 注冊/注銷中斷處理函數(shù)的接口。

Alarm Functions: Interfaces to set/remove callbacks to be run at a specific time.

3.1. Linux下的EAL

在Linux中,DPDK程序以一個用戶態(tài)程序運(yùn)行,使用的線程庫是pthread。 設(shè)備的PCI信息和地址使用linux的sysfs內(nèi)核接口和內(nèi)核模塊(uio_pci_generic或者igb_uio)獲取。 內(nèi)存則是通過mmap映射到程序內(nèi)存空間的。

EAL在hugetlbfs(巨頁內(nèi)存,提高性能) 中通過mmap()申請物理內(nèi)存,這些內(nèi)存是提供給DPDK服務(wù)層使用的, 如內(nèi)存池庫.

在DPDK服務(wù)層初始化的時候,會調(diào)用線程親和性設(shè)定函數(shù)(pthread提供), 讓每一個執(zhí)行單元綁定到一個邏輯CPU上,并讓每個執(zhí)行單元以一個用戶線程運(yùn)行。

時鐘則是由CPU的TSC或者內(nèi)核的HPET提供(通過mmap調(diào)用)。

3.1.1. EAL初始化和核心啟動

glibc中的啟動函數(shù)(入口函數(shù))完成了程序初始化的一部分,也會檢查當(dāng)前CPU和DPDK程序的架構(gòu)是否匹配。 然后主函數(shù)main()被調(diào)用。核心的初始化和啟動是由rte_eal_init() 完成的。 它由一組pthread調(diào)用組成(具體有pthread_self(), pthread_create(), and pthread_setaffinity_np())。

圖 3.1Linux環(huán)境下EAL的初始化

注解

對象(如內(nèi)存區(qū)域、ring、內(nèi)存池、lpm表、哈希表)的初始化應(yīng)該在程序初始化階段在主核上完成, 因?yàn)檫@些對象的創(chuàng)建和初始化函數(shù)不是線程安全的。 但是,這些對象一旦初始化完成,對它們的使用是線程安全的。

3.1.2. 多進(jìn)程支持

Linux EAL允許以多進(jìn)程模式開發(fā)應(yīng)用,詳細(xì)參考多進(jìn)程支持

3.1.3. 內(nèi)存映射和內(nèi)存預(yù)留

初始化階段EAL會從hugetlbfs申請大量且地址連續(xù)的內(nèi)存, 這些內(nèi)存可以通過EAL提供的"內(nèi)存區(qū)預(yù)留API"提供給上層應(yīng)用使用。 API會把這個內(nèi)存區(qū)對應(yīng)的物理地址返回給用戶。

注解

內(nèi)存預(yù)留使用rte_malloc提供的API。rte_malloc的內(nèi)存也是從hugetlbfs文件系統(tǒng)中獲取的。

3.1.4. 對無hugetbls的Xen Dom0支持

目前的內(nèi)存管理實(shí)現(xiàn)是基于Linux內(nèi)核的巨頁機(jī)制。但是,Xen Dom0不支持巨頁,因此需要額外的rte_dom0_mm內(nèi)核模塊完成這項工作。

EAL使用IOCTL接口通知rte_dom0_mm內(nèi)核模塊申請指定大小內(nèi)存,并獲取所有內(nèi)存段信息,然后EAL使用MMAP接口映射申請的內(nèi)存。 對于每個內(nèi)存段它的物理地址都是連續(xù)的,但是硬件地址是2MB連續(xù)的。

3.1.5. PCI訪問

EAL通過掃描/sys/bus/pci獲取PCI總線上設(shè)備信息。 為了訪問PCI設(shè)備內(nèi)存,uio_pci_generic內(nèi)核模塊會提供/dev/uioX設(shè)備文件和sysfs資源文件, 通過這些就可以使用mmap把PCI設(shè)備內(nèi)存映射到應(yīng)用程序內(nèi)存空間。 DPDK定制的模塊igb_uio也可以用于此。兩種模塊都使用了uio這個內(nèi)核特性(用戶空間I/O)

3.1.6. Per-lcore和共享變量

注解

lcore(邏輯核)指的是處理器的邏輯執(zhí)行單元,有時也稱為硬線程

共享變量是在所有線程之間共享的,所有線程都可以訪問。 Per-lcore變量則是線程本地存儲,線程只能訪問自己的Per-lcore變量,Per-lcore變量使用Thread Local Storage(TLS) 實(shí)現(xiàn)。

3.1.7. 日志

EAL提供了日志API。 在Linux中,默認(rèn)情況下,日志會被發(fā)送到syslog和控制臺。 用戶也可以覆蓋這些日志函數(shù),使用自定義的日志機(jī)制。

3.1.7.1. 跟蹤和調(diào)試功能

glibc中的調(diào)試函數(shù)可以把程序堆棧dump出來。 EAL提供的rte_panic()函數(shù)能夠自動發(fā)出SIG_ABORT信號,這個信號能觸發(fā)core文件的生成, 然后開發(fā)者可以通過gdb讀取core文件排除錯誤。

3.1.8. CPU特性標(biāo)識

EAL能夠在運(yùn)行時查詢CPU信息(rte_cpu_get_feature()函數(shù))并判斷哪些CPU特性可用。

3.1.9. 用戶空間中斷事件

主線程(Host Thread)的中斷和告警的處理

EAL初始化時創(chuàng)建了一個專用的主線程用于檢測中斷(通過輪詢UIO設(shè)備描述符/dev/uioX)。 開發(fā)者通過EAL提供的函數(shù)為指定中斷事件注冊/注銷回調(diào)函數(shù),事件發(fā)生時回調(diào)函數(shù)會被這個主線程異步調(diào)用。 對于NIC中斷,EAL也提供了同樣的定時回調(diào)。

注解

在DPDK PMD(輪詢模式驅(qū)動)中,主線程處理的中斷事件只有鏈路狀態(tài)變更(鏈路連接和斷開通知)和設(shè)備意外移除事件。

Rx(接收)中斷事件

PMD提供的數(shù)據(jù)包接收和發(fā)送例程允許在線程中輪詢執(zhí)行。 在網(wǎng)絡(luò)吞吐量小的時候,為了降低空閑輪詢可以先暫停輪詢,然后等待一個“喚醒”事件的發(fā)生。 Rx中斷事件可以作為首選“喚醒”事件,但也可能有其他事件作為“喚醒”事件。

EAL為事件驅(qū)動線程模式提供了事件API。 以Linux環(huán)境中的應(yīng)用為例,它的事件驅(qū)動依賴于epoll。每個事件驅(qū)動的線程會監(jiān)視一個epoll實(shí)例, 所關(guān)心的“喚醒”事件描述符會被加到這個epoll實(shí)例中。 事件描述符通過UIO/VFIO創(chuàng)建和映射到中斷向量表中。 對于BSD應(yīng)用,kqueue也是一種方式,只是目前還沒有實(shí)現(xiàn)。

EAL會負(fù)責(zé)事件描述符和中斷向量之間的映射,然而設(shè)備的隊列和中斷向量之間的映射是由設(shè)備自己完成的, EAL無法感知到這些中斷向量上面的中斷事件,因此以太網(wǎng)設(shè)備驅(qū)動會負(fù)責(zé)把這些中斷向量和事件描述符映射起來。(原文:EAL initializes the mapping between event file descriptors and interrupt vectors, while each device initializes the mapping between interrupt vectors and queues. In this way, EAL actually is unaware of the interrupt cause on the specific vector. The eth_dev driver takes responsibility to program the latter mapping.)

注解

每個隊列的Rx中斷事件僅在VFIO中可用(VFIO支持multiple MSI-X vector)。在UIO中,Rx中斷和其他中斷共享同一個中斷向量, 這種情況下,如果Rx中斷和LSC(link status change)中斷同時啟用的話(intr_conf.lsc == 1 && intr_conf.rxq == 1),只有前者有效。

Rx中斷能夠使用ethdev API進(jìn)行控制/啟用/關(guān)閉 - 'rte_eth_dev_rx_intr_*'。PMD不支持的操作返回失敗。 intr_conf.rxq標(biāo)志是用來開啟設(shè)備Rx中斷的。

設(shè)備移除事件

當(dāng)設(shè)備從總線上移除時會觸發(fā)該事件。事件發(fā)生時其底層資源可能已經(jīng)不可用了(也就是PCI映射解除)。 PMD要確保這種情況下,應(yīng)用仍能夠安全地使用它的回調(diào)。

設(shè)備移除事件的訂閱和鏈接狀態(tài)變更訂閱一樣。因此執(zhí)行的環(huán)境也一樣,也就是專門用于處理中斷的主線程。

考慮這樣一種情況,應(yīng)用程序去關(guān)閉一個已經(jīng)發(fā)出設(shè)備移除事件的設(shè)備。這種情況下,rte_eth_dev_close()調(diào)用會注銷設(shè)備移除事件的回調(diào)。 要小心的是不要在中斷處理上下文中關(guān)閉設(shè)備,應(yīng)該通過其他方式去關(guān)閉設(shè)備。

3.1.10. 黑名單

PCI設(shè)備黑名單功能能夠把特定NIC端口加入到黑名單中,DPDK會忽略黑名單中的設(shè)備。 黑名單中的端口通過PCIe*描述(Domain:Bus:Device.Function)標(biāo)識。

3.1.11. 其他

Locks and atomic operations are per-architecture (i686 and x86_64).

3.2. 內(nèi)存段和內(nèi)存區(qū)域(memzone)

物理內(nèi)存映射是EAL的特性。物理內(nèi)存其實(shí)會有間隙、不是連續(xù)的,因此需要使用內(nèi)存描述符表, 其中存放的就是各個內(nèi)存段的描述符(rte_memseg),每個描述符代表一段連續(xù)的物理內(nèi)存。

除此以外,memzone分配器的任務(wù)是預(yù)留一段地址連續(xù)的物理內(nèi)存。這些memzone在預(yù)留內(nèi)存時以唯一的名稱標(biāo)識。

我們可以在應(yīng)用的配置結(jié)構(gòu)體(配置通過rte_eal_get_configuration()獲取)中找到rte_memzone描述符表。 查找(通過名稱)內(nèi)存區(qū)域時返回的是包含該內(nèi)存區(qū)域物理地址的描述符。

內(nèi)存區(qū)域能夠按照指定的對齊參數(shù)對齊預(yù)留(起始地址對齊, 默認(rèn)cache line大小對齊)。 對齊大小應(yīng)該是2的n次冪并且不小于cache line大小(64 bytes)。 內(nèi)存區(qū)域也能夠預(yù)留系統(tǒng)提供的兩種可用的巨頁(2MB和1GB)。

3.3. 多線程

DPDK通常會在一個核上啟動一個線程,避免線程切換的額外開銷。 這會很顯著地增加性能,但是缺乏靈活性并且不一定總是高效的。

我們可以通過電源管理限制CPU運(yùn)行頻率進(jìn)而提升CPU效能。也可以把CPU的空閑周期(idle cycles)利用起來從而充分發(fā)揮CPU性能。

通過cgroup可以很容易地指定CPU利用率。這為CPU效率提升提供了另外一種方法,但是需要一個先決條件, DPDK必須能處理每個核上面多個線程之間的上下文切換。

為了更加靈活,我們應(yīng)該把線程的親和性設(shè)置到一組而不是一個CPU上。

3.3.1. EAL pthread和lcore親和性

術(shù)語"lcore"指的是EAL線程,事實(shí)上它是Linux/FreeBSD上的pthread。 "EAL pthreads"由EAL創(chuàng)建和管理,由remote_launch執(zhí)行。 每一個EAL pthread都有一個叫_lcore_id的線程本地存儲用于唯一標(biāo)識一個線程。 因?yàn)橥ǔthreads和CPU是一對一地綁定,所以_lcore_id通常和CPU ID相等。

在使用多線程時,EAL線程和CPU不總是一對一綁定,EAL線程可能會對應(yīng)一組CPU,這種情況下_lcore_id和CPU ID就不相等了。 為此,EAL提供了一個'--lcores'選項用于分配lcore的CPU親和性。 你可以使用這個選項為一組lcore分配一組CPU。

參數(shù)格式:

--lcores='[@cpu_set][,[@cpu_set],...]'

'lcore_set'和'cpu_set'可以是一個數(shù),范圍或者組。

數(shù)字: "digit([0-9]+)"; 范圍: "-"; 組: "([,,...])".

如果'@cpu_set'沒有提供, 默認(rèn)和'lcore_set'相同。

比如, "--lcores='1,2@(5-7),(3-5)@(0,2),(0,6),7-8'" 啟動9個線程;? ? 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會分配給相關(guān)的CPU。 該選項和啟用核列表選項'-l'兼容。

3.3.2. 非EAL線程支持

在DPDK應(yīng)用中用戶可以創(chuàng)建線程(也就是非EAL線程)。 在非EAL線程中,_lcore_id總是LCORE_ID_ANY。 由于很多基礎(chǔ)庫需要使用_lcore_id,因此在非EAL線程中,有的庫會使用其他的唯一ID(比如,線程ID), 有的庫則不受影響,還有些庫會受限使用(比如,定時器和內(nèi)存池庫)。

所有的影響看這里已知問題

3.3.3. 公共線程API

rte_thread_set_affinity()和rte_thread_get_affinity()用于設(shè)置和獲取與親和性相關(guān)的TLS。

這些TLS包括_cpuset_socket_id:

_cpuset存儲的是CPU和線程綁定關(guān)系的位圖。

_socket_id存儲的是CPU集合的NUMA節(jié)點(diǎn)。如果CPU集合中的CPU屬于其他NUMA節(jié)點(diǎn),那么_socket_id就設(shè)置為SOCKET_ID_ANY。

3.3.4. 已知問題

rte_mempool

rte_mempool在內(nèi)存池中使用了per-lcore緩存。 在非EAL線程中調(diào)用rte_lcore_id()會返回非法值。 目前,當(dāng)在非EAL線程中使用rte_mempool的put/get操作時,不會使用默認(rèn)的內(nèi)存池緩存,但這會導(dǎo)致性能下降。 用戶自己創(chuàng)建的緩存可以通過函數(shù)rte_mempool_generic_put()和rte_mempool_generic_get()在非EAL環(huán)境中使用。 這兩個函數(shù)會接收一個參數(shù)用于指定使用的緩存。

rte_ring

rte_ring支持多生產(chǎn)者入隊和多消費(fèi)者出隊并且是非搶占的, 由于rte_mempool使用了rte_ring,所以這使得rte_mempool也是非搶占的。

注解

"非搶占"約束意味著:

同一個ring,一個線程的多生產(chǎn)者入隊操作不可被另一個線程多生產(chǎn)者入隊操作搶占。

同一個ring,一個線程的多消費(fèi)者出隊操作不可被另一個線程多消費(fèi)者出隊操作搶占。

開啟搶占會導(dǎo)致第二個線程一直自旋,直到第一個線程再次被調(diào)度執(zhí)行。 而且,如果第一個線程被高優(yōu)先級的任務(wù)搶占可能會導(dǎo)致死鎖。

這并不意味著rte_ring無法使用,簡單的說,應(yīng)該盡量不要在同一個核心的多個線程上使用。

可以用于單生產(chǎn)者或單消費(fèi)者的情況。

可以用于使用SCHED_OTHER(cfs)調(diào)度策略的多生產(chǎn)者/消費(fèi)者線程中。注意:使用者應(yīng)該意識到這會導(dǎo)致性能損耗

禁止用于使用SCHED_FIFO或者SCHED_RR調(diào)度策略的多生產(chǎn)者/消費(fèi)者線程中。

rte_timer

不允許在非EAL線程中調(diào)用rte_timer_manager()。但是可以在非EAL線程中重置/停止定時器。

rte_log

在非EAL線程中,只用全局日志等級可用,沒有線程日志等級和日志類型可用。

其他

非EAL線程中不支持rte_ring、rte_mempool和rte_timer調(diào)試統(tǒng)計功能。

3.3.5. cgroup控制

下面是一個使用cgroup控制使用率的簡單例子,其中有兩個線程(t0和t1)在同一個CPU($cpu)上做包I/O操作。 我們希望CPU的50%的時間用于包IO。

mkdir /sys/fs/cgroup/cpu/pkt_iomkdir /sys/fs/cgroup/cpuset/pkt_ioecho $cpu > /sys/fs/cgroup/cpuset/cpuset.cpusecho $t0 > /sys/fs/cgroup/cpu/pkt_io/tasksecho $t0 > /sys/fs/cgroup/cpuset/pkt_io/tasksecho $t1 > /sys/fs/cgroup/cpu/pkt_io/tasksecho $t1 > /sys/fs/cgroup/cpuset/pkt_io/taskscd /sys/fs/cgroup/cpu/pkt_ioecho 100000 > pkt_io/cpu.cfs_period_usecho? 50000 > pkt_io/cpu.cfs_quota_us

3.4. Malloc

EAL提供用于申請任意大小內(nèi)存的API。

該API的目的是提供一個類似于malloc的函數(shù),可以從操作系統(tǒng)的巨頁內(nèi)存中申請內(nèi)存, 還有簡化程序的移植。DPDK API Reference手冊中敘述了可用的函數(shù)。

顯然,這些內(nèi)存申請操作不應(yīng)該在數(shù)據(jù)處理過程中進(jìn)行,因?yàn)樗鼈儽然趦?nèi)存池的內(nèi)存申請操作慢很多, 而且在內(nèi)存申請和釋放的過程中還使用了鎖。

更多有關(guān)rte_malloc()函數(shù)的描述請查看DPDK API Reference

3.4.1. Cookies

當(dāng)啟用調(diào)試模式(CONFIG_RTE_MALLOC_DEBUG is enabled)時, 申請的內(nèi)存會包含覆寫保護(hù)域用于標(biāo)識緩沖區(qū)溢出。

3.4.2. 對齊和NUMA約束

rte_malloc()接收一個對齊參數(shù)n(n必須是2的冪),申請的內(nèi)存將對齊于n的倍數(shù)。

在支持NUMA的系統(tǒng)當(dāng)中,rte_malloc()將從本地NUMA socket申請內(nèi)存。 DPDK中也提供了直接從指定NUMA socket或者其他核心所在NUMA socket(比如,為其他核申請內(nèi)存)中申請內(nèi)存的API。

3.4.3. 使用案例

該API用于在初始化階段需要使用像malloc函數(shù)的應(yīng)用中。

在應(yīng)用運(yùn)行時為了快速申請和釋放內(nèi)存應(yīng)該使用內(nèi)存池庫代替真正的內(nèi)存申請和釋放操作。

3.4.4. 內(nèi)部實(shí)現(xiàn)

3.4.4.1. 數(shù)據(jù)結(jié)構(gòu)

malloc庫中有兩種內(nèi)部使用的數(shù)據(jù)結(jié)構(gòu)類型:

struct malloc_heap - 用于記錄每個socket(per-socket basis)上空閑內(nèi)存

struct malloc_elem - 內(nèi)存申請的基本元素,還用于malloc庫內(nèi)空閑內(nèi)存記錄

3.4.4.1.1. 結(jié)構(gòu)體: malloc_heap

malloc_heap結(jié)構(gòu)體用于管理每個socket上空閑內(nèi)存。每個NUMA節(jié)點(diǎn)有一個malloc_heap結(jié)構(gòu)體, 通過這個結(jié)構(gòu)體我們可以給該NUMA節(jié)點(diǎn)上的線程申請內(nèi)存。 但這并不保證該內(nèi)存僅會被該NUMA節(jié)點(diǎn)上的線程使用。 一個很爛的設(shè)計: 總是在固定節(jié)點(diǎn)或總是在隨機(jī)節(jié)點(diǎn)上申請內(nèi)存。

malloc_heap結(jié)構(gòu)體的關(guān)鍵字段:

lock - 鎖保證堆訪問的同步性。由于堆中的空閑內(nèi)存是用鏈表記錄的, 所以需要使用鎖防止兩個線程同時操作該鏈表。

free_head - 指向該堆空閑內(nèi)存鏈表的第一個元素。

注解

malloc_heap結(jié)構(gòu)體不會記錄使用中的內(nèi)存塊,因?yàn)槌酸尫艃?nèi)存,malloc庫不會對它們做任何操作。

圖 3.2malloc庫中堆(malloc_heap)和元素(malloc_elem)的示例

3.4.4.1.2. 結(jié)構(gòu)體: malloc_elem

malloc_elem結(jié)構(gòu)體用于各種內(nèi)存塊的通用頭結(jié)構(gòu)。上圖中有三種內(nèi)存塊用到該結(jié)構(gòu)體:

空閑或已申請內(nèi)存塊頭 - 正常用法

內(nèi)存塊里的填充頭

內(nèi)存段(memseg)的結(jié)束標(biāo)記

注解

上面三種使用方法中有個別字段沒有描述,沒有描述的字段其值是未定義的,比如, 在填充頭中僅"state"和"pad"字段有合法值。

該結(jié)構(gòu)體中最重要的字段如下:

heap - 該指針指向該內(nèi)存塊所屬的堆。在內(nèi)存塊釋放的使用, 通過該指針把該空閑內(nèi)存塊加到堆空閑列表。

prev - 該指針指向內(nèi)存段(memseg)中的前一個元素(內(nèi)存塊)。 在釋放內(nèi)存塊時,通過這個指針找到前一個塊,判斷前一個內(nèi)存塊是否是空閑的, 如果是空閑的則將這兩塊內(nèi)存合并成一個大塊內(nèi)存。

next_free - 該指針用于把空閑內(nèi)存塊鏈接成一個空閑鏈表。僅用于正常(空閑或使用中的)內(nèi)存塊; 在malloc()中用于尋找合適的空閑塊,在free()中用于把新釋放的內(nèi)存塊加入到空閑列表中。

state - 該字段有三個值:FREE,BUSY和PAD。 前兩個值代表的是正常內(nèi)存塊的狀態(tài); 最后一個值PAD表示該malloc_elem是啞頭(不代表任何正常內(nèi)存塊), 因?yàn)閷R約束,這種塊只是用來填充的,該結(jié)構(gòu)體位于填充區(qū)域的結(jié)尾, 因?yàn)樘畛涞拇嬖冢搩?nèi)存塊內(nèi)數(shù)據(jù)的起始地址并不是塊的地址。在這種情況下, 填充頭被用于定位該內(nèi)存塊實(shí)際的頭。對于內(nèi)存段(memseg)結(jié)束結(jié)構(gòu),state總是BUSY, 這樣可以防止內(nèi)存段結(jié)束元素被free()合并。

pad - 存放的是內(nèi)存塊中從起始位置開始填充的長度。在正常塊的頭中, 該值加上頭結(jié)束地址得到數(shù)據(jù)區(qū)域的起始地址,也就是malloc()的返回值。 填充塊的啞頭中這個字段存儲的也是填充長度,啞頭的地址減去該值得到實(shí)際內(nèi)存塊頭的地址。

size - 數(shù)據(jù)塊的長度,包括頭的長度。對于內(nèi)存段結(jié)束結(jié)構(gòu),該值為零。 對于將要釋放的內(nèi)存塊,這個值被用來作為"next"指針識別下一個內(nèi)存塊的位置。 如果下一個內(nèi)存塊是FREE的,那么這兩個內(nèi)存塊會被合并成一個。

3.4.4.2. 內(nèi)存申請

在EAL初始化時,所有的內(nèi)存段(memseg)被加入到malloc堆中, 并且會在每個段的尾部放置一個帶有BUSY狀態(tài)的啞頭(如果開啟了CONFIG_RTE_MALLOC_DEBUG啞頭中也會包含一個哨兵元素), 每個段的開頭放置狀態(tài)為FREE的element header。 然后把FREE的元素加入到malloc堆的free_list中。

當(dāng)程序調(diào)用像malloc這樣的函數(shù)時,malloc函數(shù)會首先從調(diào)用線程中索引lcore_config結(jié)構(gòu), 然后判斷該線程的NUMA節(jié)點(diǎn)。NUMA節(jié)點(diǎn)用于從malloc堆數(shù)組中索引具體的堆, 然后把具體的堆作為參數(shù)傳遞給malloc_heap_alloc()函數(shù)。

malloc_heap_alloc()會掃描堆的空閑列表,嘗試找到一個合適的(大小、對齊、boundary等約束)空閑塊。

當(dāng)找到合適的空閑內(nèi)存塊時,會計算出返回給用戶的指針。 在計算返回給用戶的指針前,內(nèi)存的cache-line會立即被malloc_elem頭填充。 (原文:The cache-line of memory immediately preceding this pointer is filled with a struct malloc_elem header.) 由于對齊和boundary約束,元素的開頭和/或結(jié)尾會有空閑空間,這會導(dǎo)致下面的行為:

尾部空間檢查 如果尾部空間足夠大,即大于128字節(jié),會分割一個空閑元素出去。否則忽略這段空間(空間浪費(fèi))。

起始空間檢查 如果起始空間很小,即小于等于128字節(jié),會在其中放置一個填充頭,其余空間會被浪費(fèi)掉。 否則會分割出一個空閑元素。

從已存在元素尾部申請內(nèi)存的好處是不用調(diào)整空閑列表 - the existing element on the free list just has its size pointer adjusted, and the following element has its "prev" pointer redirected to the newly created element.

3.4.4.3. 內(nèi)存釋放

釋放內(nèi)存時需要把指向數(shù)據(jù)區(qū)的指針傳遞給釋放函數(shù)。 用這個指針減去malloc_elem的大小得到內(nèi)存塊的頭結(jié)構(gòu)體。 如果內(nèi)存塊頭的類型是PAD,再從指針中減去填充長度得到整個內(nèi)存塊的真確的頭結(jié)構(gòu)體。

從這個頭結(jié)構(gòu)體中可以獲取該內(nèi)存塊所屬的堆指針和前一個元素指針, 并且通過大小字段我們可以計算出下一個元素的指針。 這些前后的元素會被檢查是否FREE,如果是空閑的則會被合并當(dāng)前內(nèi)存塊中。 這意味著不會有兩個FREE內(nèi)存塊相鄰,因?yàn)樗鼈兛倳缓喜⒌揭粋€塊中。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容