概念:
librte_malloc庫提供了一套用於管理內存空間的API接口,它管理的內存是hugepages上創建出來的memzone,而不是系統的堆空間。通過這套接口,可以提高系統訪問內存的命中率,防止了在使用Linux用戶空間環境的4K頁內存管理時容易出現TLB miss。
以下內容基於DPDK 16.11版本。
接口函數:
void∗ rte_malloc( void ∗ptr, size_t size, unsigned align )
:用來替代malloc,從內存的huge_page中分配所需內存空間,分配的空間未被初始化。當size爲0或者align不是2的整數倍的時候,返回NULL。如果align爲0,則對齊方式爲任意長。若空間不足或者參數錯誤(size=0或者align不是2的整數倍)則函數返回NULL,若執行成功則返回分配的內存空間的起始地址。void ∗rte_zmalloc (const char∗type, size_t size, unsigned align)
:和rte_malloc基本相同,只是額外將申請的內存空間初始化爲0。若空間不足或者參數錯誤(size=0或者align不是2的整數倍)則函數返回NULL,若執行成功則返回分配的內存空間的起始地址。void ∗rte_calloc (const char∗type, size_t num, size_t size, unsigned align)
:用來替代calloc,和rte_malloc基本相同,申請的總空間大小爲num * size,申請了num個連續空間,每個空間的大小爲size。內存空間初始化爲0。若空間不足或者參數錯誤(size=0或者align不是2的整數倍)則函數返回NULL,若執行成功則返回分配的內存空間的起始地址。void ∗rte_realloc (void∗ptr, size_t size, unsigned align)
:用來替代realloc,重新分配ptr指向的內存空間的大小。如果size爲0,則釋放此ptr指向空間。若空間不足或者參數錯誤(align不是2的整數倍)則函數返回NULL,若執行成功則返回分配的內存空間的起始地址。int rte_malloc_validate (void∗ptr, size_t∗size)
:如果debug宏被打開,那麼此函數會檢查ptr指向的內存空間的header和trailer標記是否正常,如果正常則將這個空間的長度寫入到size中。當ptr無效或者pte指向的內存空間有錯誤時,函數返回-1,否則返回0。void rte_free( void ∗ptr )
:釋放ptr指向的內存空間,和free函數基本一致。如果ptr爲NULL,則不做任何改變。void rte_malloc_dump_stats (const char∗type)
:將指定的type的信息轉存到控制檯。如果type爲NULL,則轉存所有的內存type。int rte_malloc_set_limit (const char∗type, size_t max)
:設置type所能分配的最大內存。若執行成功則返回0,否則返回-1。
源碼分析:
rte_malloc()
代碼如下所示:
void *
rte_malloc(const char *type, size_t size, unsigned align)
{
//直接調用rte_malloc_socket處理,指定的堆爲程序運行的lcore所在的socket的堆
return rte_malloc_socket(type, size, align, SOCKET_ID_ANY);
}
- rte_malloc_socket():在指定的堆上分配內存。
void *
rte_malloc_socket(const char *type, size_t size, unsigned align, int socket_arg)
{
//聲明mcfg,指向 rte_config->mem_config
struct rte_mem_config *mcfg = rte_eal_get_configuration()->mem_config;
int socket, i;
void *ret;
/* return NULL if size is 0 or alignment is not power-of-2 */
//如果申請的內存size爲0或者align對齊參數爲0或者align不是2的整數倍,直接返回NULL
if (size == 0 || (align && !rte_is_power_of_2(align)))
return NULL;
//如果 internal_config.no_hugetlbfs =1即程序在初始化的時候已經設置了不存在hugepage
//則將socket_arg設爲SOCKET_ID_ANY
if (!rte_eal_has_hugepages())
socket_arg = SOCKET_ID_ANY;
//如果socket_arg參數爲SOCKET_ID_ANY,則獲取當前運行程序的lcore對應的socket的id
if (socket_arg == SOCKET_ID_ANY)
socket = malloc_get_numa_socket();
//否則socket還爲函數入參socket_arg
else
socket = socket_arg;
/* Check socket parameter */
//參數檢查。檢查socket的Id是否可能大於最大值RTE_MAX_NUMA_NODES
if (socket >= RTE_MAX_NUMA_NODES)
return NULL;
//調用malloc_heap_alloc開始處理內存申請
ret = malloc_heap_alloc(&mcfg->malloc_heaps[socket], type,
size, 0, align == 0 ? 1 : align, 0);
if (ret != NULL || socket_arg != SOCKET_ID_ANY)
return ret;
/* try other heaps */
//如果在指定的socket上或者在當前運行程序的lcore對應的socket上未能成功申請到內存
//則嘗試其他的socket
for (i = 0; i < RTE_MAX_NUMA_NODES; i++) {
/* we already tried this one */
if (i == socket)
continue;
ret = malloc_heap_alloc(&mcfg->malloc_heaps[i], type,
size, 0, align == 0 ? 1 : align, 0);
if (ret != NULL)
return ret;
}
return NULL;
}
- malloc_heap_alloc()函數如下所示:
void *
malloc_heap_alloc(struct malloc_heap *heap,
const char *type __attribute__((unused)), size_t size, unsigned flags,
size_t align, size_t bound)
{
struct malloc_elem *elem;
//size參數和align參數做對齊操作,以RTE_CACHE_LINE_SIZE的大小做對齊
size = RTE_CACHE_LINE_ROUNDUP(size);
align = RTE_CACHE_LINE_ROUNDUP(align);
//自旋鎖,加鎖操作
rte_spinlock_lock(&heap->lock);
//從堆中找到一個合適的元素用來作爲malloc所申請的內存塊
elem = find_suitable_element(heap, size, flags, align, bound);
if (elem != NULL) {
//將內存劃分出去
elem = malloc_elem_alloc(elem, size, align, bound);
/* increase heap's count of allocated elements */
heap->alloc_count++;
}
rte_spinlock_unlock(&heap->lock);
//這裏需要十分注意,return的指針指向的是data部分的頭指針,即這塊內存塊向後偏移了1個elem結構體長度
//&elem[1]即elem的指針向後偏移 sizeof(elem)長度。
return elem == NULL ? NULL : (void *)(&elem[1]);
}
- find_suitable_element()函數如下所示,
static struct malloc_elem *
find_suitable_element(struct malloc_heap *heap, size_t size,
unsigned flags, size_t align, size_t bound)
{
size_t idx;
struct malloc_elem *elem, *alt_elem = NULL;
//獲取一個index,這個index和堆中的free list有關,free list如下所示:
//在釋放的時候,已經把相似大小的元素分類放在了不同的free list中:
/* heap->free_head[0] - (0 , 2^8]
* heap->free_head[1] - (2^8 , 2^10]
* heap->free_head[2] - (2^10 ,2^12]
* heap->free_head[3] - (2^12, 2^14]
* heap->free_head[4] - (2^14, MAX_SIZE]
*/
//所以在申請的時候,我們就可以直接快速根據要申請的空間大小來匹配合適的free list。
//如果在該free list中沒有找到合適的用來申請的空間元素,則會從更大的free list中查找。
for (idx = malloc_elem_free_list_index(size);
idx < RTE_HEAP_NUM_FREELISTS; idx++) {
//獲取該free list對應的第一個元素(頭節點之後的第一個節點)
for (elem = LIST_FIRST(&heap->free_head[idx]);
!!elem; elem = LIST_NEXT(elem, free_list)) {
//判斷是否可以獲取到合適的elem用來作爲申請的內存空間
if (malloc_elem_can_hold(elem, size, align, bound)) {
//在這裏flags的值爲1,所以此函數直接返回了1
if (check_hugepage_sz(flags, elem->ms->hugepage_sz))
return elem;
if (alt_elem == NULL)
alt_elem = elem;
}
}
}
if ((alt_elem != NULL) && (flags & RTE_MEMZONE_SIZE_HINT_ONLY))
return alt_elem;
return NULL;
}
- malloc_elem_free_list_index():這個函數其實就是根據size的大小來返回對應的index值,運算規則其實是根據:
/* heap->free_head[0] - (0 , 2^8]
* heap->free_head[1] - (2^8 , 2^10]
* heap->free_head[2] - (2^10 ,2^12]
* heap->free_head[3] - (2^12, 2^14]
* heap->free_head[4] - (2^14, MAX_SIZE]
*/
來進行運算的。比如,如果size爲2^7,則返回的index爲0,如果size爲2^10+1,則返回的index爲3。
代碼如下所示:
size_t
malloc_elem_free_list_index(size_t size)
{
#define MALLOC_MINSIZE_LOG2 8
#define MALLOC_LOG2_INCREMENT 2
size_t log2;
size_t index;
if (size <= (1UL << MALLOC_MINSIZE_LOG2))
return 0;
/* Find next power of 2 >= size. */
log2 = sizeof(size) * 8 - __builtin_clzl(size-1);
/* Compute freelist index, based on log2(size). */
index = (log2 - MALLOC_MINSIZE_LOG2 + MALLOC_LOG2_INCREMENT - 1) /
MALLOC_LOG2_INCREMENT;
return index <= RTE_HEAP_NUM_FREELISTS-1?
index: RTE_HEAP_NUM_FREELISTS-1;
}
- malloc_elem_can_hold():這個函數其實是直接調用的函數 elem_start_pt()。
- elem_start_pt():在elem中嘗試分配所需要的size的內存空間,並且是按照對齊規則進行的。返回值爲在elem節點中分配了所需的size之後的包含size所在空間的新elem。需要注意的是,每個elem對應的空間都有一段空間(MALLOC_ELEM_HEADER_LEN)是用來存放header信息的,這個header信息就是elem結構體。如果debug開關打開,每個elem對應的空間還會保存MALLOC_ELEM_TRAILER_LEN長度的一段信息。在malloc中,由於bound=0,所以check boundary這段程序實際上不會執行。
static void *
elem_start_pt(struct malloc_elem *elem, size_t size, unsigned align,
size_t bound)
{
//在malloc中,由於bound爲0,所以bmask爲0
const size_t bmask = ~(bound - 1);
//計算elem的底部指針。elem->size是包含頭部信息和MALLOC_ELEM_TRAILER_LEN、以及數據部分三個部分。
//這裏的end_pt指向MALLOC_ELEM_TRAILER_LEN之前的空間。
uintptr_t end_pt = (uintptr_t)elem +
elem->size - MALLOC_ELEM_TRAILER_LEN;
//從elem的底部指針(不包含MALLOC_ELEM_TRAILER_LEN)向前倒推size長度,按照對齊方式進行,
//new_data_start是倒推後得到的所要分配的空間的指針,此空間不包含頭部信息
uintptr_t new_data_start = RTE_ALIGN_FLOOR((end_pt - size), align);
uintptr_t new_elem_start;
/* check boundary */
if ((new_data_start & bmask) != ((end_pt - 1) & bmask)) {
end_pt = RTE_ALIGN_FLOOR(end_pt, bound);
new_data_start = RTE_ALIGN_FLOOR((end_pt - size), align);
if (((end_pt - 1) & bmask) != (new_data_start & bmask))
return NULL;
}
//new_elem_start是包含頭部信息的,所要分配的空間的指針
new_elem_start = new_data_start - MALLOC_ELEM_HEADER_LEN;
/* if the new start point is before the exist start, it won't fit */
//判斷elem空間是否支持此new_elem存在
return (new_elem_start < (uintptr_t)elem) ? NULL : (void *)new_elem_start;
}
malloc_elem_alloc():在前面的過程中,如果在嘗試時發現可以在某個elem對應的空間中成功獲得所要分配的空間,則調用此函數進行真正的分配操作。代碼如下:
“`C
struct malloc_elem *
malloc_elem_alloc(struct malloc_elem *elem, size_t size, unsigned align,
size_t bound)
{
//在elem中根據所需的大小獲得新的分配的空間,新的空間的頭部信息爲new_elem
//這一步一定是在之前已經成功執行過的,纔會在此函數中再次進行
struct malloc_elem *new_elem = elem_start_pt(elem, size, align, bound);//elem在分出去new_elem後所剩餘的部分,其實就是兩個頭指針相減
const size_t old_elem_size = (uintptr_t)new_elem - (uintptr_t)elem;//由於在分配內存的時候按照對齊進行的,所以有可能分配的新的new_elem的實際空間是大於size+sizeof(new_elem),
//那麼old_elem_size實際上等於elem->size - size - align所需 小了一個對齊所用的空間
//所以trailer_size實際上就是align所需的多分配出來的空間,是小於align的
const size_t trailer_size = elem->size - old_elem_size - size -
MALLOC_ELEM_OVERHEAD;//將elem從free list中去除
elem_free_list_remove(elem);//如果trailer_size大於 MALLOC_ELEM_HEADER_LEN + MALLOC_ELEM_TRAILER_LEN + RTE_CACHE_LINE_SIZE
//也就是大於一個最小數據單元外加頭部節點和MALLOC_ELEM_TRAILER_LEN的大小,就需要進行拆分
if (trailer_size > MALLOC_ELEM_OVERHEAD + MIN_DATA_SIZE) {
/* split it, too much free space after elem *///獲取需要再次截取的new_free_elem的地址,即new_elem向後偏移一個 //MALLOC_ELEM_HEADER_LEN + MALLOC_ELEM_TRAILER_LEN,然後偏移new_elem的size struct malloc_elem *new_free_elem = RTE_PTR_ADD(new_elem, size + MALLOC_ELEM_OVERHEAD); //將new_free_elem從elem中分拆出來 split_elem(elem, new_free_elem); //將new_free_elem加入free list,加入的時候還要遵循上面列出的free list表大小規則,加入到對應的表中 malloc_elem_free_list_insert(new_free_elem);
}
//如果舊的old_elem的剩餘空間不足MALLOC_ELEM_HEADER_LEN + MALLOC_ELEM_TRAILER_LEN + MIN_DATA_SIZE
//則不進行拆分
if (old_elem_size < MALLOC_ELEM_OVERHEAD + MIN_DATA_SIZE) {
/* don’t split it, pad the element instead */
elem->state = ELEM_BUSY;
elem->pad = old_elem_size;/* put a dummy header in padding, to point to real element header */ //填充頭部結構體信息 if (elem->pad > 0){ /* pad will be at least 64-bytes, as everything * is cache-line aligned */ new_elem->pad = elem->pad; new_elem->state = ELEM_PAD; new_elem->size = elem->size - elem->pad; set_header(new_elem); } return new_elem;
}
/* we are going to split the element in two. The original element
- remains free, and the new element is the one allocated.
- Re-insert original element, in case its new size makes it
- belong on a different list.
*/
//如果舊的old_elem的剩餘空間大於等於MALLOC_ELEM_HEADER_LEN + MALLOC_ELEM_TRAILER_LEN + MIN_DATA_SIZE
//則進行拆分
split_elem(elem, new_elem);
new_elem->state = ELEM_BUSY;
malloc_elem_free_list_insert(elem);
return new_elem;
}
## rte_zmalloc()
代碼如下所示:
```C
void *
rte_zmalloc_socket(const char *type, size_t size, unsigned align, int socket)
{
return rte_malloc_socket(type, size, align, socket);
}
void *
rte_zmalloc(const char *type, size_t size, unsigned align)
{
return rte_zmalloc_socket(type, size, align, SOCKET_ID_ANY);
}
<div class="se-preview-section-delimiter"></div>
和rte_malloc()完全一致,這裏沒有做memset處理將內存空間初始化爲0,不曉得爲什麼,難道是BUG?
rte_calloc()
代碼如下所示:
void *
rte_calloc_socket(const char *type, size_t num, size_t size, unsigned align, int socket)
{
return rte_zmalloc_socket(type, num * size, align, socket);
}
void *
rte_calloc(const char *type, size_t num, size_t size, unsigned align)
{
return rte_zmalloc(type, num * size, align);
}
<div class="se-preview-section-delimiter"></div>
和rte_malloc()完全一致,神奇的是,這個函數的註釋竟然和rte_zmalloc一樣:“Allocate zero’d memory on specified heap.”,並且直接調用的是rte_zmalloc(),那麼函數rte_calloc_socket()是準備幹嘛? … are you kidding me?
rte_realloc()
代碼如下所示:
void *
rte_realloc(void *ptr, size_t size, unsigned align)
{
if (ptr == NULL)
return rte_malloc(NULL, size, align);
//獲取到elem的地址。傳入的ptr指向的是那塊內存空間的data部分的起始地址,在那個之前還有elem結構體
//故需要偏移elem的長度來獲取elem結構體的首地址,即整個內存區塊的首地址
struct malloc_elem *elem = malloc_elem_from_data(ptr);
if (elem == NULL)
rte_panic("Fatal error: memory corruption detected\n");
//將size和align按照RTE_CACHE_LINE_SIZE做對齊操作
size = RTE_CACHE_LINE_ROUNDUP(size), align = RTE_CACHE_LINE_ROUNDUP(align);
/* check alignment matches first, and if ok, see if we can resize block */
//檢查ptr是否是以align爲對齊的,並嘗試直接在原內存空間中直接進行resize操作
if (RTE_PTR_ALIGN(ptr,align) == ptr &&
malloc_elem_resize(elem, size) == 0)
return ptr;
/* either alignment is off, or we have no room to expand,
* so move data. */
//若ptr不以align爲對齊或者原內存空間中直接resize失敗,則重新malloc,調用rte_malloc函數
void *new_ptr = rte_malloc(NULL, size, align);
if (new_ptr == NULL)
return NULL;
//將ptr中的數據搬移到新申請的內存空間中
const unsigned old_size = elem->size - MALLOC_ELEM_OVERHEAD;
rte_memcpy(new_ptr, ptr, old_size < size ? old_size : size);
//釋放之前ptr指向的內存空間
rte_free(ptr);
return new_ptr;
}
<div class="se-preview-section-delimiter"></div>
int
malloc_elem_resize(struct malloc_elem *elem, size_t size)
{
//所需的大小,即size加上 MALLOC_ELEM_HEADER_LEN 和 MALLOC_ELEM_TRAILER_LEN
const size_t new_size = size + MALLOC_ELEM_OVERHEAD;
/* if we request a smaller size, then always return ok */
//當前的內存區塊的大小,減去填充的大小
const size_t current_size = elem->size - elem->pad;
//如果這個resize實際上是將原來的內存塊大小變小,則直接返回成功
//但是我覺得這裏是不是應該有點問題?難道不需要重新做拆分或者做pad嗎?
//什麼都不做直接返回合適嗎?
if (current_size >= new_size)
return 0;
//獲取緊跟在此elem的後一個elem的頭指針
struct malloc_elem *next = RTE_PTR_ADD(elem, elem->size);
rte_spinlock_lock(&elem->heap->lock);
//判斷是否是free狀態
if (next ->state != ELEM_FREE)
goto err_return;
//判斷這兩個elem加起來的大小是否可以滿足resize的需求,如果不足返回失敗
//那麼問題來了,僅僅判斷相鄰的兩個elem,不判斷三個或更多的情況嗎?
if (current_size + next->size < new_size)
goto err_return;
/* we now know the element fits, so remove from free list,
* join the two
*/
//將next elem從free list中去除,併合並這兩個elem
elem_free_list_remove(next);
join_elem(elem, next);
//如果剩餘空間過大,則進行拆分
if (elem->size - new_size >= MIN_DATA_SIZE + MALLOC_ELEM_OVERHEAD){
/* now we have a big block together. Lets cut it down a bit, by splitting */
struct malloc_elem *split_pt = RTE_PTR_ADD(elem, new_size);
split_pt = RTE_PTR_ALIGN_CEIL(split_pt, RTE_CACHE_LINE_SIZE);
split_elem(elem, split_pt);
malloc_elem_free_list_insert(split_pt);
}
rte_spinlock_unlock(&elem->heap->lock);
return 0;
err_return:
rte_spinlock_unlock(&elem->heap->lock);
return -1;
}
rte_malloc_validate()
代碼如下所示:
int
rte_malloc_validate(const void *ptr, size_t *size)
{
const struct malloc_elem *elem = malloc_elem_from_data(ptr);
if (!malloc_elem_cookies_ok(elem))
return -1;
if (size != NULL)
*size = elem->size - elem->pad - MALLOC_ELEM_OVERHEAD;
return 0;
}
此函數會檢查ptr指向的內存空間的header和trailer標記是否正常,如果正常則將這個空間的長度寫入到size中。
rte_free()
代碼如下所示:
void rte_free(void *addr)
{
if (addr == NULL) return;
if (malloc_elem_free(malloc_elem_from_data(addr)) < 0)
rte_panic("Fatal error: Invalid memory\n");
}
如果傳入的指針爲空,則不作任何處理。
如果傳入的指針不爲空,則找到這塊內存塊的真正起始地址(通過malloc_elem_from_data(),將addr向前偏移一個elem結構體長度),然後調用malloc_elem_free()處理。
釋放的過程其實主要就是做了內存塊free鏈表的操作,將新釋放的內存塊加入到free鏈表中。加入的策略如下:
- 如果此被釋放的內存塊的next指針指向的內存塊爲free,則將它和後一個內存塊合併,並將後一個內存塊從free鏈表中去除;
- 如果此被釋放的內存塊的pre指針指向的內存塊爲free,則將它和前一個內存塊亦進行合併;
- 將合併後(或不滿足合併條件則不合並)的內存塊,插入到對應free鏈表的頭部。這裏的對應值得是根據此內存塊的大小進行匹配的,上文中已經描述過。