一文看懂Linux內存分配,告別“內存小白”!
在 Linux 系統的復雜生態中,內存管理堪稱是維持系統高效穩定運行的核心樞紐。想象一下,Linux 系統就像是一座繁忙的超級大都市,內存則是城市里的土地資源,每一個運行的程序、進程都如同在這片土地上建造的建筑,它們都需要占用一定的內存空間來存儲數據和執行指令 。如果沒有一個科學合理的內存管理機制,這座 “城市” 將會陷入混亂,出現內存資源分配不均、內存泄漏等問題,進而導致程序運行緩慢、系統響應遲鈍,甚至引發系統崩潰。
比如,當你在 Linux 系統上同時運行多個大型程序時,若內存管理不佳,可能會使某些程序因得不到足夠的內存而無法正常運行,或者因為內存的不合理分配,導致部分內存空間被浪費,造成資源的低效利用。所以,深入了解 Linux 內存的常見分配方式,對于系統管理員、開發者以及追求系統高性能的用戶來說,就如同掌握了城市規劃的秘籍,能夠更好地調配內存資源,讓 Linux 系統這座 “城市” 有序且高效地運轉,極大地激發讀者探索 Linux 內存分配方式的興趣,為后續深入講解內容做好鋪墊。
一、內存分配三劍客
在 Linux 系統中,malloc、kmalloc、vmalloc 屬于不同層次的內存分配函數,它們看似相似,卻各懷絕技:一個服務用戶態,一個專注內核連續物理頁,另一個則駕馭虛擬映射的離散空間。理解它們的差異與妙用,正是優化性能、規避陷阱的關鍵一步。
1.1 malloc用戶空間內存分配
malloc 函數是 C 標準庫中用于動態內存分配的函數,其底層由 C 庫實現。C 庫維護著一個緩存,當該緩存中的內存足夠滿足分配需求時,malloc 會直接從 C 庫緩存中分配內存。而當 C 庫緩存中的內存不足時,malloc 則需借助系統調用與操作系統交互來獲取更多內存。
在 Linux 系統中,這主要涉及 brk 和 mmap 這兩個系統調用。當申請的內存小于 128KB 時,malloc 通常會通過系統調用 brk 向內核申請內存,具體來說是從堆空間申請一個虛擬內存區域(VMA)。brk 系統調用通過移動程序數據段的結束地址(即 “堆頂” 指針)來增加堆的大小,從而分配新的內存。當申請的內存大于等于 128KB 時,malloc 一般會使用 mmap 系統調用。mmap 系統調用是在進程的虛擬地址空間中(堆和棧中間,稱為文件映射區域的地方)找一塊空閑的虛擬內存來滿足內存分配請求。
malloc實現步驟:
- 請求大小調整:首先,malloc 需要調整用戶請求的大小,以適應內部數據結構(例如,可能需要存儲額外的元數據)。通常,這包括對齊調整,確保分配的內存地址滿足特定硬件要求(如對齊到8字節或16字節邊界)。
- 空閑鏈表搜索:接下來,malloc 會在一個空閑內存塊鏈表中搜索一個足夠大的空閑塊。這個鏈表通常由多個空閑塊組成,每個塊都記錄了大小和指向下一個塊的指針。
- 分裂或合并空閑塊:如果找到的空閑塊大小大于請求的大小,malloc 可能會將這個塊分裂成兩部分:一部分用于滿足當前請求,另一部分保留在鏈表中以供未來使用。如果空閑塊正好等于請求的大小,則直接使用該塊。
- 更新元數據:在使用選定的空閑塊之前,malloc 需要更新其元數據(如大小和下一個塊的指針),以反映內存已經被分配的事實。這可能涉及到修改當前塊的大小字段或設置一個特殊的標記來表示該塊已被占用。
- 返回指針:malloc 返回指向已分配內存的指針給用戶。
typedef struct node {
size_t size; // 塊大小
struct node* next; // 下一個節點指針
struct node* prev; // 上一個節點指針(可選)
} Node;
Node* free_list = NULL; // 空閑鏈表頭指針
void* malloc(size_t size) {
// 步驟1: 調整大?。ɡ缣砑釉獢祿笮。? size += sizeof(Node); // 為元數據留出空間
// 步驟2: 搜索空閑鏈表
Node* block = find_suitable_block(free_list, size);
if (!block) {
// 步驟3: 分裂或合并(如果需要)
// 步驟4: 更新元數據和鏈表結構
block = allocate_new_block(size); // 可能需要擴展堆或分裂現有塊
} else {
remove_from_list(block); // 從空閑鏈表中移除
}
// 步驟5: 設置元數據并返回指針(跳過Node頭)
block->size = size; // 設置大小
return (void*)((char*)block + sizeof(Node)); // 返回用戶數據的指針部分
}
⑴_do_sys_brk函數
經過平臺相關實現,malloc最終會調用SYSCALL_DEFINE1宏,擴展為__do_sys_brk函數:
SYSCALL_DEFINE1(brk, unsigned long, brk)
{
unsigned long retval;
unsigned long newbrk, oldbrk, origbrk;
struct mm_struct *mm = current->mm;
struct vm_area_struct *next;
unsigned long min_brk;
bool populate;
bool downgraded = false;
LIST_HEAD(uf);
if (down_write_killable(&mm->mmap_sem)) ///申請寫類型讀寫信號量
return -EINTR;
origbrk = mm->brk; ///brk記錄動態分配區的當前底部
#ifdef CONFIG_COMPAT_BRK
/*
* CONFIG_COMPAT_BRK can still be overridden by setting
* randomize_va_space to 2, which will still cause mm->start_brk
* to be arbitrarily shifted
*/
if (current->brk_randomized)
min_brk = mm->start_brk;
else
min_brk = mm->end_data;
#else
min_brk = mm->start_brk;
#endif
if (brk < min_brk)
goto out;
/*
* Check against rlimit here. If this check is done later after the test
* of oldbrk with newbrk then it can escape the test and let the data
* segment grow beyond its set limit the in case where the limit is
* not page aligned -Ram Gupta
*/
if (check_data_rlimit(rlimit(RLIMIT_DATA), brk, mm->start_brk,
mm->end_data, mm->start_data))
goto out;
newbrk = PAGE_ALIGN(brk);
oldbrk = PAGE_ALIGN(mm->brk);
if (oldbrk == newbrk) {
mm->brk = brk;
goto success;
}
/*
* Always allow shrinking brk.
* __do_munmap() may downgrade mmap_sem to read.
*/
if (brk <= mm->brk) { ///請求釋放空間
int ret;
/*
* mm->brk must to be protected by write mmap_sem so update it
* before downgrading mmap_sem. When __do_munmap() fails,
* mm->brk will be restored from origbrk.
*/
mm->brk = brk;
ret = __do_munmap(mm, newbrk, oldbrk-newbrk, &uf, true);
if (ret < 0) {
mm->brk = origbrk;
goto out;
} else if (ret == 1) {
downgraded = true;
}
goto success;
}
/* Check against existing mmap mappings. */
next = find_vma(mm, oldbrk);
if (next && newbrk + PAGE_SIZE > vm_start_gap(next)) ///發現有重疊,不需要尋找
goto out;
/* Ok, looks good - let it rip. */
if (do_brk_flags(oldbrk, newbrk-oldbrk, 0, &uf) < 0) ///無重疊,新分配一個vma
goto out;
mm->brk = brk; ///更新brk地址
success:
populate = newbrk > oldbrk && (mm->def_flags & VM_LOCKED) != 0;
if (downgraded)
up_read(&mm->mmap_sem);
else
up_write(&mm->mmap_sem);
userfaultfd_unmap_complete(mm, &uf);
if (populate) ///調用mlockall()系統調用,mm_populate會立刻分配物理內存
mm_populate(oldbrk, newbrk - oldbrk);
return brk;
out:
retval = origbrk;
up_write(&mm->mmap_sem);
return retval;
}
總結下_do_sys_brk()功能:
- (1)從舊的brk邊界去查詢,是否有可用vma,若發現有重疊,直接使用;
- (2)若無發現重疊,新分配一個vma;
- (3)應用程序若調用mlockall(),會鎖住進程所有虛擬地址空間,防止內存被交換出去,且立刻分配物理內存;否則,物理頁面會等到使用時,觸發缺頁異常分配;
⑵do_brk_flags函數
- (1)尋找一個可使用的線性地址;
- (2)查找最適合插入紅黑樹的節點;
- (3)尋到的線性地址是否可以合并現有vma,所不能,新建一個vma;
- (4)將新建vma插入mmap鏈表和紅黑樹中
/*
* this is really a simplified "do_mmap". it only handles
* anonymous maps. eventually we may be able to do some
* brk-specific accounting here.
*/
static int do_brk_flags(unsigned long addr, unsigned long len, unsigned long flags, struct list_head *uf)
{
struct mm_struct *mm = current->mm;
struct vm_area_struct *vma, *prev;
struct rb_node **rb_link, *rb_parent;
pgoff_t pgoff = addr >> PAGE_SHIFT;
int error;
unsigned long mapped_addr;
/* Until we need other flags, refuse anything except VM_EXEC. */
if ((flags & (~VM_EXEC)) != 0)
return -EINVAL;
flags |= VM_DATA_DEFAULT_FLAGS | VM_ACCOUNT | mm->def_flags; ///默認屬性,可讀寫
mapped_addr = get_unmapped_area(NULL, addr, len, 0, MAP_FIXED); ///返回未使用過的,未映射的線性地址區間的,起始地址
if (IS_ERR_VALUE(mapped_addr))
return mapped_addr;
error = mlock_future_check(mm, mm->def_flags, len);
if (error)
return error;
/* Clear old maps, set up prev, rb_link, rb_parent, and uf */
if (munmap_vma_range(mm, addr, len, &prev, &rb_link, &rb_parent, uf)) ///尋找適合插入的紅黑樹節點
return -ENOMEM;
/* Check against address space limits *after* clearing old maps... */
if (!may_expand_vm(mm, flags, len >> PAGE_SHIFT))
return -ENOMEM;
if (mm->map_count > sysctl_max_map_count)
return -ENOMEM;
if (security_vm_enough_memory_mm(mm, len >> PAGE_SHIFT))
return -ENOMEM;
/* Can we just expand an old private anonymous mapping? */ ///檢查是否能合并addr到附近的vma,若不能,只能新建一個vma
vma = vma_merge(mm, prev, addr, addr + len, flags,
NULL, NULL, pgoff, NULL, NULL_VM_UFFD_CTX);
if (vma)
goto out;
/*
* create a vma struct for an anonymous mapping
*/
vma = vm_area_alloc(mm);
if (!vma) {
vm_unacct_memory(len >> PAGE_SHIFT);
return -ENOMEM;
}
vma_set_anonymous(vma);
vma->vm_start = addr;
vma->vm_end = addr + len;
vma->vm_pgoff = pgoff;
vma->vm_flags = flags;
vma->vm_page_prot = vm_get_page_prot(flags);
vma_link(mm, vma, prev, rb_link, rb_parent); ///新vma添加到mmap鏈表和紅黑樹
out:
perf_event_mmap(vma);
mm->total_vm += len >> PAGE_SHIFT;
mm->data_vm += len >> PAGE_SHIFT;
if (flags & VM_LOCKED)
mm->locked_vm += (len >> PAGE_SHIFT);
vma->vm_flags |= VM_SOFTDIRTY;
return 0;
}
mm_populate()函數
依次調用:
mm_populate()
->__mm_populate()
->populate_vma_page_range()
->__get_user_pages()
當設置VM_LOCKED標志時,表示要馬上申請物理頁面,并與vma建立映射;否則,這里不操作,直到訪問該vma時,觸發缺頁異常,再分配物理頁面,并建立映射;
⑶get_user_pages()函數
static long __get_user_pages(struct mm_struct *mm,
unsigned long start, unsigned long nr_pages,
unsigned int gup_flags, struct page **pages,
struct vm_area_struct **vmas, int *locked)
{
long ret = 0, i = 0;
struct vm_area_struct *vma = NULL;
struct follow_page_context ctx = { NULL };
if (!nr_pages)
return 0;
start = untagged_addr(start);
VM_BUG_ON(!!pages != !!(gup_flags & (FOLL_GET | FOLL_PIN)));
/*
* If FOLL_FORCE is set then do not force a full fault as the hinting
* fault information is unrelated to the reference behaviour of a task
* using the address space
*/
if (!(gup_flags & FOLL_FORCE))
gup_flags |= FOLL_NUMA;
do { ///依次處理每個頁面
struct page *page;
unsigned int foll_flags = gup_flags;
unsigned int page_increm;
/* first iteration or cross vma bound */
if (!vma || start >= vma->vm_end) {
vma = find_extend_vma(mm, start); ///檢查是否可以擴增vma
if (!vma && in_gate_area(mm, start)) {
ret = get_gate_page(mm, start & PAGE_MASK,
gup_flags, &vma,
pages ? &pages[i] : NULL);
if (ret)
goto out;
ctx.page_mask = 0;
goto next_page;
}
if (!vma) {
ret = -EFAULT;
goto out;
}
ret = check_vma_flags(vma, gup_flags);
if (ret)
goto out;
if (is_vm_hugetlb_page(vma)) { ///支持巨頁
i = follow_hugetlb_page(mm, vma, pages, vmas,
&start, &nr_pages, i,
gup_flags, locked);
if (locked && *locked == 0) {
/*
* We've got a VM_FAULT_RETRY
* and we've lost mmap_lock.
* We must stop here.
*/
BUG_ON(gup_flags & FOLL_NOWAIT);
BUG_ON(ret != 0);
goto out;
}
continue;
}
}
retry:
/*
* If we have a pending SIGKILL, don't keep faulting pages and
* potentially allocating memory.
*/
if (fatal_signal_pending(current)) { ///如果當前進程收到SIGKILL信號,直接退出
ret = -EINTR;
goto out;
}
cond_resched(); //判斷是否需要調度,內核中常用該函數,優化系統延遲
page = follow_page_mask(vma, start, foll_flags, &ctx); ///查看VMA的虛擬頁面是否已經分配物理內存,返回已經映射的頁面的page
if (!page) {
ret = faultin_page(vma, start, &foll_flags, locked); ///若無映射,主動觸發虛擬頁面到物理頁面的映射
switch (ret) {
case 0:
goto retry;
case -EBUSY:
ret = 0;
fallthrough;
case -EFAULT:
case -ENOMEM:
case -EHWPOISON:
goto out;
case -ENOENT:
goto next_page;
}
BUG();
} else if (PTR_ERR(page) == -EEXIST) {
/*
* Proper page table entry exists, but no corresponding
* struct page.
*/
goto next_page;
} else if (IS_ERR(page)) {
ret = PTR_ERR(page);
goto out;
}
if (pages) {
pages[i] = page;
flush_anon_page(vma, page, start); ///分配完物理頁面,刷新緩存
flush_dcache_page(page);
ctx.page_mask = 0;
}
next_page:
if (vmas) {
vmas[i] = vma;
ctx.page_mask = 0;
}
page_increm = 1 + (~(start >> PAGE_SHIFT) & ctx.page_mask);
if (page_increm > nr_pages)
page_increm = nr_pages;
i += page_increm;
start += page_increm * PAGE_SIZE;
nr_pages -= page_increm;
} while (nr_pages);
out:
if (ctx.pgmap)
put_dev_pagemap(ctx.pgmap);
return i ? i : ret;
}
follow_page_mask函數返回已經映射的頁面的page,最終會調用follow_page_pte函數,其實現如下:
⑷follow_page_pte函數
static struct page *follow_page_pte(struct vm_area_struct *vma,
unsigned long address, pmd_t *pmd, unsigned int flags,
struct dev_pagemap **pgmap)
{
struct mm_struct *mm = vma->vm_mm;
struct page *page;
spinlock_t *ptl;
pte_t *ptep, pte;
int ret;
/* FOLL_GET and FOLL_PIN are mutually exclusive. */
if (WARN_ON_ONCE((flags & (FOLL_PIN | FOLL_GET)) ==
(FOLL_PIN | FOLL_GET)))
return ERR_PTR(-EINVAL);
retry:
if (unlikely(pmd_bad(*pmd)))
return no_page_table(vma, flags);
ptep = pte_offset_map_lock(mm, pmd, address, &ptl); ///獲得pte和一個鎖
pte = *ptep;
if (!pte_present(pte)) { ///處理頁面不在內存中,作以下處理
swp_entry_t entry;
/*
* KSM's break_ksm() relies upon recognizing a ksm page
* even while it is being migrated, so for that case we
* need migration_entry_wait().
*/
if (likely(!(flags & FOLL_MIGRATION)))
goto no_page;
if (pte_none(pte))
goto no_page;
entry = pte_to_swp_entry(pte);
if (!is_migration_entry(entry))
goto no_page;
pte_unmap_unlock(ptep, ptl);
migration_entry_wait(mm, pmd, address); ///等待頁面合并完成再嘗試
goto retry;
}
if ((flags & FOLL_NUMA) && pte_protnone(pte))
goto no_page;
if ((flags & FOLL_WRITE) && !can_follow_write_pte(pte, flags)) {
pte_unmap_unlock(ptep, ptl);
return NULL;
}
page = vm_normal_page(vma, address, pte); ///根據pte,返回物理頁面page(只返回普通頁面,特殊頁面不參與內存管理)
if (!page && pte_devmap(pte) && (flags & (FOLL_GET | FOLL_PIN))) { ///處理設備映射文件
/*
* Only return device mapping pages in the FOLL_GET or FOLL_PIN
* case since they are only valid while holding the pgmap
* reference.
*/
*pgmap = get_dev_pagemap(pte_pfn(pte), *pgmap);
if (*pgmap)
page = pte_page(pte);
else
goto no_page;
} else if (unlikely(!page)) { ///處理vm_normal_page()沒返回有效頁面情況
if (flags & FOLL_DUMP) {
/* Avoid special (like zero) pages in core dumps */
page = ERR_PTR(-EFAULT);
goto out;
}
if (is_zero_pfn(pte_pfn(pte))) { ///系統零頁,不會返回錯誤
page = pte_page(pte);
} else {
ret = follow_pfn_pte(vma, address, ptep, flags);
page = ERR_PTR(ret);
goto out;
}
}
/* try_grab_page() does nothing unless FOLL_GET or FOLL_PIN is set. */
if (unlikely(!try_grab_page(page, flags))) {
page = ERR_PTR(-ENOMEM);
goto out;
}
/*
* We need to make the page accessible if and only if we are going
* to access its content (the FOLL_PIN case). Please see
* Documentation/core-api/pin_user_pages.rst for details.
*/
if (flags & FOLL_PIN) {
ret = arch_make_page_accessible(page);
if (ret) {
unpin_user_page(page);
page = ERR_PTR(ret);
goto out;
}
}
if (flags & FOLL_TOUCH) { ///FOLL_TOUCH, 標記頁面可訪問
if ((flags & FOLL_WRITE) &&
!pte_dirty(pte) && !PageDirty(page))
set_page_dirty(page);
/*
* pte_mkyoung() would be more correct here, but atomic care
* is needed to avoid losing the dirty bit: it is easier to use
* mark_page_accessed().
*/
mark_page_accessed(page);
}
if ((flags & FOLL_MLOCK) && (vma->vm_flags & VM_LOCKED)) {
/* Do not mlock pte-mapped THP */
if (PageTransCompound(page))
goto out;
/*
* The preliminary mapping check is mainly to avoid the
* pointless overhead of lock_page on the ZERO_PAGE
* which might bounce very badly if there is contention.
*
* If the page is already locked, we don't need to
* handle it now - vmscan will handle it later if and
* when it attempts to reclaim the page.
*/
if (page->mapping && trylock_page(page)) {
lru_add_drain(); /* push cached pages to LRU */
/*
* Because we lock page here, and migration is
* blocked by the pte's page reference, and we
* know the page is still mapped, we don't even
* need to check for file-cache page truncation.
*/
mlock_vma_page(page);
unlock_page(page);
}
}
out:
pte_unmap_unlock(ptep, ptl);
return page;
no_page:
pte_unmap_unlock(ptep, ptl);
if (!pte_none(pte))
return NULL;
return no_page_table(vma, flags);
}
(1)malloc函數,從C庫緩存分配內存,其分配或釋放內存,未必馬上會執行;
(2)malloc實際分配內存動作,要么主動設置mlockall(),人為觸發缺頁異常,分配物理頁面;或者在訪問內存時觸發缺頁異常,分配物理頁面;
(3)malloc分配虛擬內存,有三種情況:
- malloc()分配內存后,直接讀,linux內核進入缺頁異常,調用do_anonymous_page函數使用零頁映射,此時PTE屬性只讀;
- malloc()分配內存后,先讀后寫,linux內核第一次觸發缺頁異常,映射零頁;第二次觸發異常,觸發寫時復制;
- c.malloc()分配內存后, 直接寫,linux內核進入匿名頁面的缺頁異常,調alloc_zeroed_user_highpage_movable分配一個新頁面,這個PTE是可寫的;
1.2 kmalloc內核空間常規內存分配
一般來說內核程序中對小于一頁的小塊內存的請求會通過slab分配器提供的接口kmalloc來完成(雖然它可分配32到131072字節的內存)。從內核內存分配角度講kmalloc可被看成是get_free_page(s)的一個有效補充,內存分配粒度更靈活了。
kmalloc()函數類似與我們常見的malloc()函數,前者用于內核態的內存分配,后者用于用戶態;kmalloc()函數在物理內存中分配一塊連續的存儲空間,且和malloc()函數一樣,不會清除里面的原始數據,如果內存充足,它的分配速度很快,其原型如下:
static inline void *kmalloc(size_t size, gfp_t flags); /*返回的是虛擬地址*/
- size:待分配的內存大小。由于Linux內存管理機制的原因,內存只能按照頁面大小(一般32位機為4KB,64位機為8KB)進行分配,這樣就導致了當我們僅需要幾個字節內存時,系統仍會返回一個頁面的內存,顯然這是極度浪費的。所以,不同于malloc的是,kmalloc的處理方式是:內核先為其分配一系列不同大?。?2B、64B、128B、… 、128KB)的內存池,當需要分配內存時,系統會分配大于等于所需內存的最小一個內存池給它。即kmalloc分配的內存,最小為32字節,最大為128KB。如果超過128KB,需要采樣其它內存分配函數,例如vmalloc()。
- flag:該參數用于控制函數的行為,最常用的是GFP_KERNEL,表示當當前沒有足夠內存分配時,進程進入睡眠,待系統將緩沖區中的內容SWAP到硬盤中后,獲得足夠內存后再喚醒進程,為其分配。
使用 GFP_ KERNEL 標志申請內存時,若暫時不能滿足,則進程會睡眠等待頁,即會引起阻塞,因此不能在中斷上下文或持有自旋鎖的時候使用GFP_KERNE 申請內存。所以,在中斷處理函數、tasklet 和內核定時器等非進程上下文中不能阻塞,此時驅動應當使用 GFP_ATOMIC 標志來申請內存。當使用 GFP_ATOMIC 標志申請內存時,若不存在空閑頁,則不等待,直接返回。
kmalloc函數
static __always_inline void *kmalloc(size_t size, gfp_t flags)
{
if (__builtin_constant_p(size)) {
#ifndef CONFIG_SLOB
unsigned int index;
#endif
if (size > KMALLOC_MAX_CACHE_SIZE)
return kmalloc_large(size, flags);
#ifndef CONFIG_SLOB
index = kmalloc_index(size); ///查找使用的哪個slab緩沖區
if (!index)
return ZERO_SIZE_PTR;
return kmem_cache_alloc_trace( ///從slab分配內存
kmalloc_caches[kmalloc_type(flags)][index],
flags, size);
#endif
}
return __kmalloc(size, flags);
}
kmem_cache_alloc_trace分配函數
void *
kmem_cache_alloc_trace(struct kmem_cache *cachep, gfp_t flags, size_t size)
{
void *ret;
ret = slab_alloc(cachep, flags, size, _RET_IP_); ///分配slab緩存
ret = kasan_kmalloc(cachep, ret, size, flags);
trace_kmalloc(_RET_IP_, ret,
size, cachep->size, flags);
return ret;
}
可見,kmalloc()基于slab分配器實現,因此分配的內存,物理上都是連續的。
1.3 vmalloc內核空間虛擬內存分配
vmalloc()一般用在為只存在于軟件中(沒有對應的硬件意義)的較大的順序緩沖區分配內存,當內存沒有足夠大的連續物理空間可以分配時,可以用該函數來分配虛擬地址連續但物理地址不連續的內存。由于需要建立新的頁表,所以它的開銷要遠遠大于kmalloc及后面將要講到的__get_free_pages()函數。且vmalloc()不能用在原子上下文中,因為它的內部實現使用了標志為 GFP_KERNEL 的kmalloc(),其函數原型如下:
void *vmalloc(unsigned long size);
void vfree(const void *addr);
使用 vmalloc 函數的一個例子函數是create_module()系統調用,它利用 vmalloc()函數來獲取被創建模塊需要的內存空間。
內存分配是一項要求嚴格的任務,無論什么時候,都應該對返回值進行檢測,在驅動編程中可以使用copy_from_user()對內存進行使用。下面舉一個使用vmalloc函數的示例:
static int xxx(...)
{
...
cpuid_entries = vmalloc(sizeof(struct kvm_cpuid_entry) * cpuid->nent);
if(!cpuid_entries)
goto out;
if(copy_from_user(cpuid_entries, entries, cpuid->nent * sizeof(struct kvm_cpuid_entry)))
goto out_free;
for(i=0; i<cpuid->nent; i++){
vcpuid->arch.cpuid_entries[i].eax = cpuid_entries[i].eax;
...
vcpuid->arch.cpuid_entries[i].index = 0;
}
...
out_free:
vfree(cpuid_entries);
out:
return r;
}
核心函數__vmalloc_node_range
static void *__vmalloc_area_node(struct vm_struct *area, gfp_t gfp_mask,
pgprot_t prot, unsigned int page_shift,
int node)
{
const gfp_t nested_gfp = (gfp_mask & GFP_RECLAIM_MASK) | __GFP_ZERO;
unsigned long addr = (unsigned long)area->addr;
unsigned long size = get_vm_area_size(area); ///計算vm_struct包含多少個頁面
unsigned long array_size;
unsigned int nr_small_pages = size >> PAGE_SHIFT;
unsigned int page_order;
struct page **pages;
unsigned int i;
array_size = (unsigned long)nr_small_pages * sizeof(struct page *);
gfp_mask |= __GFP_NOWARN;
if (!(gfp_mask & (GFP_DMA | GFP_DMA32)))
gfp_mask |= __GFP_HIGHMEM;
/* Please note that the recursion is strictly bounded. */
if (array_size > PAGE_SIZE) {
pages = __vmalloc_node(array_size, 1, nested_gfp, node,
area->caller);
} else {
pages = kmalloc_node(array_size, nested_gfp, node);
}
if (!pages) {
free_vm_area(area);
warn_alloc(gfp_mask, NULL,
"vmalloc size %lu allocation failure: "
"page array size %lu allocation failed",
nr_small_pages * PAGE_SIZE, array_size);
return NULL;
}
area->pages = pages; ///保存已分配頁面的page數據結構的指針
area->nr_pages = nr_small_pages;
set_vm_area_page_order(area, page_shift - PAGE_SHIFT);
page_order = vm_area_page_order(area);
/*
* Careful, we allocate and map page_order pages, but tracking is done
* per PAGE_SIZE page so as to keep the vm_struct APIs independent of
* the physical/mapped size.
*/
for (i = 0; i < area->nr_pages; i += 1U << page_order) {
struct page *page;
int p;
/* Compound pages required for remap_vmalloc_page */
page = alloc_pages_node(node, gfp_mask | __GFP_COMP, page_order); ///分配物理頁面
if (unlikely(!page)) {
/* Successfully allocated i pages, free them in __vfree() */
area->nr_pages = i;
atomic_long_add(area->nr_pages, &nr_vmalloc_pages);
warn_alloc(gfp_mask, NULL,
"vmalloc size %lu allocation failure: "
"page order %u allocation failed",
area->nr_pages * PAGE_SIZE, page_order);
goto fail;
}
for (p = 0; p < (1U << page_order); p++)
area->pages[i + p] = page + p;
if (gfpflags_allow_blocking(gfp_mask))
cond_resched();
}
atomic_long_add(area->nr_pages, &nr_vmalloc_pages);
if (vmap_pages_range(addr, addr + size, prot, pages, page_shift) < 0) { ///建立物理頁面到vma的映射
warn_alloc(gfp_mask, NULL,
"vmalloc size %lu allocation failure: "
"failed to map pages",
area->nr_pages * PAGE_SIZE);
goto fail;
}
return area->addr;
fail:
__vfree(area->addr);
return NULL;
}
可見,vmalloc是臨時在vmalloc內存區申請vma,并且分配物理頁面,建立映射;直接分配物理頁面,至少一個頁4K,因此vmalloc適合用于分配較大內存,并且物理內存不一定連續;
二、mmap函數詳解最后
2.1mmap函數
mmap 即 memory map,也就是內存映射。mmap 是一種內存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關系。實現這樣的映射關系后,進程就可以采用指針的方式讀寫操作這一段內存,而系統會自動回寫臟頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調用 read、write 等系統調用函數。相反,內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間的文件共享。如下圖所示:
圖片
mmap的作用,在應用這一層,是讓你把文件的某一段,當作內存一樣來訪問。將文件映射到物理內存,將進程虛擬空間映射到那塊內存。這樣,進程不僅能像訪問內存一樣讀寫文件,多個進程映射同一文件,還能保證虛擬空間映射到同一塊物理內存,達到內存共享的作用。
mmap 是 Linux 中用處非常廣泛的一個系統調用,它將一個文件或者其它對象映射進內存。文件被映射到多個頁上,如果文件的大小不是所有頁的大小之和,最后一個頁不被使用的空間將會清零。mmap 必須以 PAGE_SIZE 為單位進行映射,而內存也只能以頁為單位進行映射,若要映射非 PAGE_SIZE 整數倍的地址范圍,要先進行內存對齊,強行以 PAGE_SIZE 的倍數大小進行映射。
其函數原型為:void *mmap (void start, size_t length, int prot, int flags, int fd, off_t offset);int munmap(void start, size_t length);。下面介紹一下內存映射的步驟:
- 用 open 系統調用打開文件,并返回描述符 fd。
- 用 mmap 建立內存映射,并返回映射首地址指針 start。
- 對映射(文件)進行各種操作,如顯示(printf)、修改(sprintf)等。
- 用 munmap (void *start, size_t length) 關閉內存映射。
- 用 close 系統調用關閉文件 fd。
2.2mmap工作原理
mmap函數創建一個新的vm_area_struct結構,并將其與文件/設備的物理地址相連。
vm_area_struct:linux使用vm_area_struct來表示一個獨立的虛擬內存區域,一個進程可以使用多個vm_area_struct來表示不用類型的虛擬內存區域(如堆,棧,代碼段,MMAP區域等)。
vm_area_struct結構中包含了區域起始地址。同時也包含了一個vm_opt指針,其內部可引出所有針對這個區域可以使用的系統調用函數。從而,進程可以通過vm_area_struct獲取操作這段內存區域所需的任何信息。
進程通過vma操作內存,而vma與文件/設備的物理地址相連,系統自動回寫臟頁面到對應的文件磁盤上(或寫入到設備地址空間),實現內存映射文件。
內存映射文件的原理:
首先創建虛擬區間并完成地址映射,此時還沒有將任何文件數據拷貝至主存。當進程發起讀寫操作時,會訪問虛擬地址空間,通過查詢頁表,發現這段地址不在物理頁上,因為只建立了地址映射,真正的數據還沒有拷貝到內存,因此引發缺頁異常。缺頁異常經過一系列判斷,確定無非法操作后,內核發起請求調頁過程。
最終會調用nopage函數把所缺的頁從文件在磁盤里的地址拷貝到物理內存。之后進程便可以對這片主存進行讀寫,如果寫操作修改了內容,一定時間后系統會自動回寫臟頁面到對應的磁盤地址,完成了寫入到文件的過程。另外,也可以調用msync()來強制同步,這樣所寫的內存就能立刻保存到文件中。
mmap內存映射的實現過程,總的來說可以分為三個階段:
⑴進程啟動映射過程,并在虛擬地址空間中為映射創建虛擬映射區域
- 進程在用戶空間調用庫函數mmap,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
- 在當前進程的虛擬地址空間中,尋找一段空閑的滿足要求的連續的虛擬地址
- 為此虛擬區分配一個vm_area_struct結構,接著對這個結構的各個域進行了初始化
- 將新建的虛擬區結構(vm_area_struct)插入進程的虛擬地址區域鏈表或樹中
⑵調用內核空間的系統調用函數mmap(不同于用戶空間函數),實現文件物理地址和進程虛擬地址的一一映射關系
- 為映射分配了新的虛擬地址區域后,通過待映射的文件指針,在文件描述符表中找到對應的文件描述符,通過文件描述符,鏈接到內核“已打開文件集”中該文件的文件結構體(struct file),每個文件結構體維護著和這個已打開文件相關各項信息。
- 通過該文件的文件結構體,鏈接到file_operations模塊,調用內核函數mmap,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma),不同于用戶空間庫函數。
- 內核mmap函數通過虛擬文件系統inode模塊定位到文件磁盤物理地址。
- 通過remap_pfn_range函數建立頁表,即實現了文件地址和虛擬地址區域的映射關系。此時,這片虛擬地址并沒有任何數據關聯到主存中。
⑶進程發起對這片映射空間的訪問,引發缺頁異常,實現文件內容到物理內存(主存)的拷貝
注:前兩個階段僅在于創建虛擬區間并完成地址映射,但是并沒有將任何文件數據的拷貝至主存。真正的文件讀取是當進程發起讀或寫操作時。
- 進程的讀或寫操作訪問虛擬地址空間這一段映射地址,通過查詢頁表,發現這一段地址并不在物理頁面上。因為目前只建立了地址映射,真正的硬盤數據還沒有拷貝到內存中,因此引發缺頁異常。
- 缺頁異常進行一系列判斷,確定無非法操作后,內核發起請求調頁過程。
- 調頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內存頁,如果沒有則調用nopage函數把所缺的頁從磁盤裝入到主存中。
- 之后進程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間后系統會自動回寫臟頁面到對應磁盤地址,也即完成了寫入到文件的過程。
注:修改過的臟頁面并不會立即更新回文件中,而是有一段時間的延遲,可以調用msync()來強制同步, 這樣所寫的內容就能立即保存到文件里了。
2.3如何使用mmap技術
(1)mmap使用細節
使用mmap需要注意的一個關鍵點是,mmap映射區域大小必須是物理頁大小(page_size)的整倍數(32位系統中通常是4k字節)。原因是,內存的最小粒度是頁,而進程虛擬地址空間和內存的映射也是以頁為單位。為了匹配內存的操作,mmap從磁盤到虛擬地址空間的映射也必須是頁。
內核可以跟蹤被內存映射的底層對象(文件)的大小,進程可以合法的訪問在當前文件大小以內又在內存映射區以內的那些字節。也就是說,如果文件的大小一直在擴張,只要在映射區域范圍內的數據,進程都可以合法得到,這和映射建立時文件的大小無關。
映射建立之后,即使文件關閉,映射依然存在。因為映射的是磁盤的地址,不是文件本身,和文件句柄無關。同時可用于進程間通信的有效地址空間不完全受限于被映射文件的大小,因為是按頁映射。
在上面的知識前提下,我們下面看看如果大小不是頁的整倍數的具體情況:
情形一:一個文件的大小是5000字節,mmap函數從一個文件的起始位置開始,映射5000字節到虛擬內存中。
分析:因為單位物理頁面的大小是4096字節,雖然被映射的文件只有5000字節,但是對應到進程虛擬地址區域的大小需要滿足整頁大小,因此mmap函數執行后,實際映射到虛擬內存區域8192個 字節,5000~8191的字節部分用零填充。映射后的對應關系如下圖所示:
圖片
此時:(1)讀/寫前5000個字節(0~4999),會返回操作文件內容。(2)讀字節50008191時,結果全為0。寫50008191時,進程不會報錯,但是所寫的內容不會寫入原文件中 。(3)讀/寫8192以外的磁盤部分,會返回一個SIGSECV錯誤。
情形二:一個文件的大小是5000字節,mmap函數從一個文件的起始位置開始,映射15000字節到虛擬內存中,即映射大小超過了原始文件的大小。
分析:由于文件的大小是5000字節,和情形一一樣,其對應的兩個物理頁。那么這兩個物理頁都是合法可以讀寫的,只是超出5000的部分不會體現在原文件中。由于程序要求映射15000字節,而文件只占兩個物理頁,因此8192字節~15000字節都不能讀寫,操作時會返回異常。如下圖所示:
圖片
此時:(1)進程可以正常讀/寫被映射的前5000字節(0~4999),寫操作的改動會在一定時間后反映在原文件中。(2)對于5000~8191字節,進程可以進行讀寫過程,不會報錯。但是內容在寫入前均為0,另外,寫入后不會反映在文件中。(3)對于8192~14999字節,進程不能對其進行讀寫,會報SIGBUS錯誤。(4)對于15000以外的字節,進程不能對其讀寫,會引發SIGSEGV錯誤。
情形三:一個文件初始大小為0,使用mmap操作映射了10004K的大小,即1000個物理頁大約4M字節空間,mmap返回指針ptr。
分析:如果在映射建立之初,就對文件進行讀寫操作,由于文件大小為0,并沒有合法的物理頁對應,如同情形二一樣,會返回SIGBUS錯誤。但是如果,每次操作ptr讀寫前,先增加文件的大小,那么ptr在文件大小內部的操作就是合法的。例如,文件擴充4096字節,ptr就能操作ptr ~ [ (char)ptr + 4095]的空間。只要文件擴充的范圍在1000個物理頁(映射范圍)內,ptr都可以對應操作相同的大小。這樣,方便隨時擴充文件空間,隨時寫入文件,不造成空間浪費。
(2)函數定義及參數解釋
在 Linux 中,mmap 函數定義如下:void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);。參數解釋如下:
- addr:希望映射的起始地址,通常為 NULL,表示由內核決定映射的地址。
- length:映射區域的大小(以字節為單位)。
- prot:映射區域的保護權限,決定映射的頁面是否可讀、可寫等。常見的權限選項包括:PROT_READ(可讀)、PROT_WRITE(可寫)、PROT_EXEC(可執行)、PROT_NONE(無權限)。
- flags:映射的類型和行為控制。常見的標志包括:MAP_SHARED(共享映射,對該內存的修改會同步到文件)、MAP_PRIVATE(私有映射,對該內存的修改不會影響原文件,寫時拷貝)、MAP_ANONYMOUS(匿名映射,不涉及文件,通常用于分配未初始化的內存)。
- fd:文件描述符,指向要映射的文件。如果使用匿名映射,應將 fd 設置為 -1,并且需要設置 MAP_ANONYMOUS 標志。
- offset:文件映射的偏移量,必須是頁面大小的整數倍(通常為 4096 字節)。
返回值:返回映射區域的起始地址,如果映射失敗,則返回 MAP_FAILED。
(3)mmap映射
在內存映射的過程中,并沒有實際的數據拷貝,文件沒有被載入內存,只是邏輯上被放入了內存,具體到代碼,就是建立并初始化了相關的數據結構(struct address_space),這個過程有系統調用mmap()實現,所以建立內存映射的效率很高。既然建立內存映射沒有進行實際的數據拷貝,那么進程又怎么能最終直接通過內存操作訪問到硬盤上的文件呢?
那就要看內存映射之后的幾個相關的過程了。mmap()會返回一個指針ptr,它指向進程邏輯地址空間中的一個地址,這樣以后,進程無需再調用read或write對文件進行讀寫,而只需要通過ptr就能夠操作文件。但是ptr所指向的是一個邏輯地址,要操作其中的數據,必須通過MMU將邏輯地址轉換成物理地址,這個過程與內存映射無關。
前面講過,建立內存映射并沒有實際拷貝數據,這時,MMU在地址映射表中是無法找到與ptr相對應的物理地址的,也就是MMU失敗,將產生一個缺頁中斷,缺頁中斷的中斷響應函數會在swap中尋找相對應的頁面,如果找不到(也就是該文件從來沒有被讀入內存的情況),則會通過mmap()建立的映射關系,從硬盤上將文件讀取到物理內存中,如圖1中過程3所示。這個過程與內存映射無關。如果在拷貝數據時,發現物理內存不夠用,則會通過虛擬內存機制(swap)將暫時不用的物理頁面交換到硬盤上,這個過程也與內存映射無關。
mmap內存映射的實現過程:
- 進程啟動映射過程,并在虛擬地址空間中為映射創建虛擬映射區域
- 調用內核空間的系統調用函數mmap(不同于用戶空間函數),實現文件物理地址和進程虛擬地址的一一映射關系
- 進程發起對這片映射空間的訪問,引發缺頁異常,實現文件內容到物理內存(主存)的拷貝
適合的場景
- 您有一個很大的文件,其內容您想要隨機訪問一個或多個時間
- 您有一個小文件,它的內容您想要立即讀入內存并經常訪問。這種技術最適合那些大小不超過幾個虛擬內存頁的文件。(頁是地址空間的最小單位,虛擬頁和物理頁的大小是一樣的,通常為4KB。)
- 您需要在內存中緩存文件的特定部分。文件映射消除了緩存數據的需要,這使得系統磁盤緩存中的其他數據空間更大 當隨機訪問一個非常大的文件時,通常最好只映射文件的一小部分。映射大文件的問題是文件會消耗活動內存。如果文件足夠大,系統可能會被迫將其他部分的內存分頁以加載文件。將多個文件映射到內存中會使這個問題更加復雜。
不適合的場景
- 您希望從開始到結束的順序從頭到尾讀取一個文件
- 這個文件有幾百兆字節或者更大。將大文件映射到內存中會快速地填充內存,并可能導致分頁,這將抵消首先映射文件的好處。對于大型順序讀取操作,禁用磁盤緩存并將文件讀入一個小內存緩沖區
- 該文件大于可用的連續虛擬內存地址空間。對于64位應用程序來說,這不是什么問題,但是對于32位應用程序來說,這是一個問題
- 該文件位于可移動驅動器上
- 該文件位于網絡驅動器上
示例代碼
//
// ViewController.m
// TestCode
//
// Created by zhangdasen on 2020/5/24.
// Copyright ? 2020 zhangdasen. All rights reserved.
//
#import "ViewController.h"
#import <sys/mman.h>
#import <sys/stat.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *path = [NSHomeDirectory() stringByAppendingPathComponent:@"test.data"];
NSLog(@"path: %@", path);
NSString *str = @"test str2";
[str writeToFile:path atomically:YES encoding:NSUTF8StringEncoding error:nil];
ProcessFile(path.UTF8String);
NSString *result = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
NSLog(@"result:%@", result);
}
int MapFile(const char * inPathName, void ** outDataPtr, size_t * outDataLength, size_t appendSize)
{
int outError;
int fileDescriptor;
struct stat statInfo;
// Return safe values on error.
outError = 0;
*outDataPtr = NULL;
*outDataLength = 0;
// Open the file.
fileDescriptor = open( inPathName, O_RDWR, 0 );
if( fileDescriptor < 0 )
{
outError = errno;
}
else
{
// We now know the file exists. Retrieve the file size.
if( fstat( fileDescriptor, &statInfo ) != 0 )
{
outError = errno;
}
else
{
ftruncate(fileDescriptor, statInfo.st_size + appendSize);
fsync(fileDescriptor);
*outDataPtr = mmap(NULL,
statInfo.st_size + appendSize,
PROT_READ|PROT_WRITE,
MAP_FILE|MAP_SHARED,
fileDescriptor,
0);
if( *outDataPtr == MAP_FAILED )
{
outError = errno;
}
else
{
// On success, return the size of the mapped file.
*outDataLength = statInfo.st_size;
}
}
// Now close the file. The kernel doesn’t use our file descriptor.
close( fileDescriptor );
}
return outError;
}
void ProcessFile(const char * inPathName)
{
size_t dataLength;
void * dataPtr;
char *appendStr = " append_key2";
int appendSize = (int)strlen(appendStr);
if( MapFile(inPathName, &dataPtr, &dataLength, appendSize) == 0) {
dataPtr = dataPtr + dataLength;
memcpy(dataPtr, appendStr, appendSize);
// Unmap files
munmap(dataPtr, appendSize + dataLength);
}
}
@end
(5)解除映射的方法
使用 mmap 后,必須調用 munmap 來解除映射,釋放分配的虛擬內存。其函數定義如下:int munmap(void *addr, size_t length);。
- addr:要解除映射的內存區域的起始地址。
- length:要解除映射的大小。
返回值:成功返回 0,失敗返回 -1。
⑴利用 mmap 訪問硬件,減少數據拷貝次數
mmap 可以將文件、設備等外部資源映射到內存地址空間,進程可以像訪問內存一樣訪問文件數據或硬件資源。當使用 mmap 訪問硬件時,數據可以直接從硬件設備通過 DMA 拷貝到內核緩沖區,然后進程可以直接訪問這個緩沖區,減少了數據拷貝的次數。
例如,在嵌入式系統中,可以使用 mmap 將物理地址映射到用戶虛擬地址空間,實現對硬件設備的直接訪問。在進行數據傳輸時,避免了傳統方式中從內核空間到用戶空間的多次數據拷貝,提高了數據傳輸的效率。
⑵通過 mmap 實現將物理地址映射到用戶虛擬地址空間:
- 打開 /dev/mem 文件獲得文件描述符 dev_mem_fd。
- 使用 mmap 函數進行映射,將物理地址映射到用戶虛擬地址空間。例如,定義一個函數 dma_mmap 來實現這個功能,函數原型為 int dma_mmap(unsigned long addr_p, unsigned int len, unsigned char** addr_v)。在這個函數中,首先打開 /dev/mem 文件,然后使用 mmap 函數進行映射,最后返回虛擬地址。
- 使用映射后的虛擬地址進行操作,例如讀寫硬件設備。
- 在使用完后,調用 dma_munmap 函數解除映射,釋放資源。函數原型為 unsigned int dma_munmap(unsigned char* addr_v, unsigned long addr_p, unsigned int len)。
⑶在嵌入式系統中,還可以通過以下方式實現物理地址到用戶虛擬地址空間的映射:
- 在驅動程序中,實現 mmap 方法,建立虛擬地址到物理地址的頁表。例如,可以使用 remap_pfn_range 函數一次建立所有頁表,或者使用 nopage VMA 方法每次建立一個頁表。
- 在用戶空間程序中,使用 mmap 函數進行映射,將文件描述符、映射大小、保護權限等參數傳入,獲得映射后的虛擬地址。然后可以通過這個虛擬地址對硬件設備進行操作。
三、頁分配函數最后
該函數定義在頭文件/include/linux/gfp.h中,它既可以在內核空間分配,也可以在用戶空間分配,它返回分配的第一個頁的描述符而非首地址,其原型為:
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
#define alloc_pages(gfp_mask, order) alloc_pages_node(numa_node_id(), gfp_mask, order)//分配連續2^order個頁面
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
if(unlikely(order >= MAX_ORDER))
return NULL;
if(nid < 0)
nid = numa_node_id();
return __alloc_pages(gfp_mask, order, noed_zonelist(nid, gfp_mask));
}
圖片
根據返回頁面數目分類,分為僅返回單頁面的函數和返回多頁面的函數。
3.1alloc_page()和alloc_pages()函數
該函數定義在頭文件/include/linux/gfp.h中,它既可以在內核空間分配,也可以在用戶空間分配,它返回分配的第一個頁的描述符而非首地址,其原型為:
#define alloc_page(gfp_mask) alloc_pages(gfp_mask, 0)
#define alloc_pages(gfp_mask, order) alloc_pages_node(numa_node_id(), gfp_mask, order)//分配連續2^order個頁面
static inline struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)
{
if(unlikely(order >= MAX_ORDER))
return NULL;
if(nid < 0)
nid = numa_node_id();
return __alloc_pages(gfp_mask, order, noed_zonelist(nid, gfp_mask));
}
3.2_get_free_pages()系列函數
它是kmalloc函數實現的基礎,返回一個或多個頁面的虛擬地址。該系列函數/宏包括 get_zeroed_page()、_ _get_free_page()和_ _get_free_pages()。在使用時,其申請標志的值及含義與 kmalloc()完全一樣,最常用的是 GFP_KERNEL 和 GFP_ATOMIC。
/*分配多個頁并返回分配內存的首地址,分配的頁數為2^order,分配的頁不清零。
order 允許的最大值是 10(即 1024 頁)或者 11(即 2048 頁),依賴于具體
的硬件平臺。*/
unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order)
{
struct page *page;
page = alloc_pages(gfp_mask, order);
if(!page)
return 0;
return (unsigned long)page_address(page);
}
#define __get_free_page(gfp_mask) __get_free_pages(gfp_mask, 0)
/*該函數返回一個指向新頁的指針并且將該頁清零*/
unsigned long get_zeroed_page(unsigned int flags);
使用_ _get_free_pages()系列函數/宏申請的內存應使用free_page(addr)或free_pages(addr, order)函數釋放:
#define __free_page(page) __free_pages((page), 0)
#define free_page(addr) free_pages((addr), 0)
void free_pages(unsigned long addr, unsigned int order)
{
if(addr != 0){
VM_BUG_ON(!virt_addr_valid((void*)addr));
__free_pages(virt_to_page((void *)addr), order);
}
}
void __free_pages(struct page *page, unsigned int order)
{
if(put_page_testzero(page)){
if(order == 0)
free_hot_page(page);
else
__free_pages_ok(page, order);
}
}
free_pages()函數是調用__free_pages()函數完成內存釋放的。
四、Buddy算法詳解最后
4.1Buddy算法
Buddy 算法堪稱 Linux 內存管理的基石,主要用于管理 DMA(直接內存存取)、常規、高端內存這 3 個區域 。它的核心理念十分巧妙,將空閑的頁以 2 的 n 次方為單位進行管理,這意味著 Linux 最底層的內存申請都是以 2 的 n 次方為單位。其最大的優勢在于能夠有效避免內存的外部碎片。在傳統的內存管理算法中,頻繁的內存分配和釋放容易導致內存空間被分割成許多小塊,這些小塊之間的空閑區域無法被有效利用,就形成了外部碎片。而 Buddy 算法通過將內存頁按照 2 的冪次方大小進行組織和管理,使得系統在分配和回收內存時,能夠更好地合并相鄰的空閑塊,從而避免了這種外部碎片的產生 。
例如,假設 ZONE_NORMAL 區域有 16 頁內存(即 2 的 4 次方),當有程序申請一頁內存時,Buddy 算法會把剩下的 15 頁巧妙地拆分成 8 + 4 + 2 + 1,分別放到不同的鏈表中。此時若再有程序申請 4 頁內存,系統可以直接從 4 頁的鏈表中分配;若再申請 4 頁,就從 8 頁的鏈表中拿出 4 頁進行分配,正好剩下 4 頁。其精髓就在于任何正整數都可以拆分成 2 的 n 次方之和。
然而,金無足赤,人無完人,Buddy 算法也有其局限性。長期運行后,系統中大片的連續內存會逐漸減少,而 1 頁、2 頁、4 頁這種小內存塊會越來越多。當需要分配大片連續內存時,就可能會出現問題,也就是說它是以產生內部碎片為代價來避免外部碎片的產生。所謂內部碎片,就是系統已經分配給用戶使用,但用戶自己沒有用到的那部分存儲空間。我們可以通過/proc/buddyinfo文件來查看內存空閑的相關情況,以便及時了解系統內存的分配狀態,為系統優化提供依據 。
4.2伙伴算法原理
Linux 便是采用這著名的伙伴系統算法來解決外部碎片的問題。把所有的空閑頁框分組為 11 塊鏈表,每一塊鏈表分別包含大小為1,2,4,8,16,32,64,128,256,512 和 1024 個連續的頁框。對1024 個頁框的最大請求對應著 4MB 大小的連續RAM 塊。每一塊的第一個頁框的物理地址是該塊大小的整數倍。例如,大小為 16個頁框的塊,其起始地址是 16 * 2^12 (2^12 = 4096,這是一個常規頁的大?。┑谋稊怠?/span>
下面通過一個簡單的例子來說明該算法的工作原理:
假設要請求一個256(129~256)個頁框的塊。算法先在256個頁框的鏈表中檢查是否有一個空閑塊。如果沒有這樣的塊,算法會查找下一個更大的頁塊,也就是,在512個頁框的鏈表中找一個空閑塊。如果存在這樣的塊,內核就把512的頁框分成兩等分,一般用作滿足需求,另一半則插入到256個頁框的鏈表中。
如果在512個頁框的塊鏈表中也沒找到空閑塊,就繼續找更大的塊——1024個頁框的塊。如果這樣的塊存在,內核就把1024個頁框塊的256個頁框用作請求,然后剩余的768個頁框中拿512個插入到512個頁框的鏈表中,再把最后的256個插入到256個頁框的鏈表中。如果1024個頁框的鏈表還是空的,算法就放棄并發出錯誤信號。
⑴相關數據結構
#define MAX_ORDER 11
struct zone {
……
struct free_area free_area[MAX_ORDER];
……
}
struct free_area {
struct list_head free_list;
unsigned long nr_free;//該組類別塊空閑的個數
};
Zone結構體中的free_area數組的第k個元素,它保存了所有連續大小為2^k的空閑塊,具體是通過將連續頁的第一個頁插入到free_list中實現的,連續頁的第一個頁的頁描述符的private字段表明改部分連續頁屬于哪一個order鏈表。
⑵伙伴算法系統初始化
Linux內核啟動時,伙伴算法還不可用,linux是通過bootmem來管理內存,在mem_init中會把bootmem位圖中空閑的內存塊插入到伙伴算法系統的free_list中。
調用流程如下:
mem_init----->__free_all_bootmem()—>free_all_bootmem()>free_all_bootmem_core(NODE_DATA(0))–>free_all_bootmem_core(pgdat)
//利用free_page 將頁面分給伙伴管理器
free_all_bootmem
return(free_all_bootmem_core(NODE_DATA(0))); //#define NODE_DATA(nid) (&contig_page_data)
bootmem_data_t *bdata = pgdat->bdata;
page = virt_to_page(phys_to_virt(bdata->node_boot_start));
idx = bdata->node_low_pfn - (bdata->node_boot_start >> PAGE_SHIFT);
map = bdata->node_bootmem_map;
for (i = 0; i < idx; )
unsigned long v = ~map[i / BITS_PER_LONG];
//如果32個頁都是空閑的
if (gofast && v == ~0UL)
count += BITS_PER_LONG;
__ClearPageReserved(page);
order = ffs(BITS_PER_LONG) - 1;
//設置32個頁的引用計數為1
set_page_refs(page, order)
//一次性釋放32個頁到空閑鏈表
__free_pages(page, order);
__free_pages_ok(page, order);
list_add(&page->lru, &list);
//page_zone定義如下return zone_table[page->flags >> NODEZONE_SHIFT];
//接收一個頁描述符的地址作為它的參數,它讀取頁描述符的flags字段的高位,并通過zone_table數組來確定相應管理區描述符的地址,最終將頁框回收到對應的管理區中
free_pages_bulk(page_zone(page), 1, &list, order);
i += BITS_PER_LONG;
page += BITS_PER_LONG;
//這32個頁中,只有部分是空閑的
else if (v)
for (m = 1; m && i < idx; m<<=1, page++, i++)
if (v & m)
count++;
__ClearPageReserved(page);
set_page_refs(page, 0);
//釋放單個頁
__free_page(page);
else
i+=BITS_PER_LONG;
page += BITS_PER_LONG;
//釋放內存分配位圖本身
page = virt_to_page(bdata->node_bootmem_map);
for (i = 0; i < ((bdata->node_low_pfn-(bdata->node_boot_start >> PAGE_SHIFT))/8 + PAGE_SIZE-1)/PAGE_SIZE; i++,page++)
__ClearPageReserved(page);
set_page_count(page, 1);
__free_page(page);
⑶伙伴算法系統分配空間
page = __rmqueue(zone, order);
//從所請求的order開始,掃描每個可用塊鏈表進行循環搜索。
for (current_order = order; current_order < MAX_ORDER; ++current_order)
area = zone->free_area + current_order;
if (list_empty(&area->free_list))
continue;
page = list_entry(area->free_list.next, struct page, lru);
//首先在空閑塊鏈表中刪除第一個頁框描述符。
list_del(&page->lru);
//清楚第一個頁框描述符的private字段,該字段表示連續頁框屬于哪一個大小的鏈表
rmv_page_order(page);
area->nr_free--;
zone->free_pages -= 1UL << order;
//如果是從更大的order鏈表中申請的,則剩下的要重新插入到鏈表中
return expand(zone, page, order, current_order, area);
unsigned long size = 1 << high;
while (high > low)
area--;
high--;
size >>= 1;
//該部分連續頁面插入到對應的free_list中
list_add(&page[size].lru, &area->free_list);
area->nr_free++;
//設置該部分連續頁面的order
set_page_order(&page[size], high);
page->private = order;
__SetPagePrivate(page);
__set_bit(PG_private, &(page)->flags)
return page;
⑷伙伴算法系統回收空間
free_pages_bulk
//linux內核將空間分為三個區,分別是ZONE_DMA、ZONE_NORMAL、ZONR_HIGH,zone_mem_map字段就是指向該區域第一個頁描述符
struct page *base = zone->zone_mem_map;
while (!list_empty(list) && count--)
page = list_entry(list->prev, struct page, lru);
list_del(&page->lru);
__free_pages_bulk
int order_size = 1 << order;
//該段空間的第一個頁的下標
page_idx = page - base;
zone->free_pages += order_size;
//最多循環10 - order次。每次都將一個塊和它的伙伴進行合并。
while (order < MAX_ORDER-1)
//尋找伙伴,如果page_idx=128,order=4,則buddy_idx=144
buddy_idx = (page_idx ^ (1 << order));
buddy = base + buddy_idx;
/**
* 判斷伙伴塊是否是大小為order的空閑頁框的第一個頁。
* 首先,伙伴的第一個頁必須是空閑的(_count == -1)
* 同時,必須屬于動態內存(PG_reserved被清0,PG_reserved為1表示留給內核或者沒有使用)
* 最后,其private字段必須是order
*/
if (!page_is_buddy(buddy, order))
break;
list_del(&buddy->lru);
area = zone->free_area + order;
//原先所在的區域空閑頁減少
area->nr_free--;
rmv_page_order(buddy);
__ClearPagePrivate(page);
page->private = 0;
page_idx &= buddy_idx;
order++;
/**
* 伙伴不能與當前塊合并。
* 將塊插入適當的鏈表,并以塊大小的order更新第一個頁框的private字段。
*/
coalesced = base + page_idx;
set_page_order(coalesced, order);
list_add(&coalesced->lru, &zone->free_area[order].free_list);
zone->free_area[order].nr_free++;
4.3分配過程
現內存總容量為16KB,用戶請求分配4KB大小的內存空間,且規定最小的內存分配單元是2KB。于是位圖分為8個區域,用1表示已分配,用0表示未分配,則初始位圖和空閑鏈表如圖所示。從上到下依次是位圖、內存塊、空閑鏈表。
圖片
由于需要分配4KB內存,數顯到鏈表中4KB位置進行查看,發現為空,于是繼續向后查找8KB位置,發現仍為空,直到到達鏈表尾16KB位置不為空。16KB塊分裂成兩個8KB的塊,其中一塊插入到鏈表相應位置,另一塊繼續分裂成兩個4KB的塊,其中一個交付使用,另一個插入到鏈表中,結果如下圖所示:
圖片
內存回收是內存分配的逆過程,假設以上存儲要釋放4KB內存,首先到鏈表中4KB位置查看是否有它的“伙伴”,發現該位置不為空,于是合并成一個8KB的塊,繼續尋找它的“伙伴”,然后合并成一個16KB的塊,插入鏈表中。若在查找過程中沒有發現“伙伴”,則直接插入到鏈表中,然后將位圖中的標記清零,表示內存可用。
當程序請求內存時,Linux 內核內存伙伴算法會按照以下步驟進行分配:
首先從空閑的內存中搜索比申請的內存大的最小的內存塊。例如,如果程序請求一個大小為特定值(假設為 N)的內存塊,伙伴算法會先在與 N 大小相同的塊鏈表中查找空閑塊。如果沒有找到合適的塊,就會去更大的塊鏈表中繼續查找。
如果這樣的內存塊存在,則將這塊內存標記為 “已用”,同時將該內存分配給應用程序。比如,假設程序請求的內存大小處于某個特定范圍(比如 129 - 256 個頁框),算法會先在 256 個頁框的鏈表中檢查是否有空閑塊。若存在這樣的塊,就可以直接分配給程序使用。
如果這樣的內存不存在,則操作系統將尋找更大塊的空閑內存,然后將這塊內存平分成兩部分,一部分返回給程序使用,另一部分作為空閑的內存塊等待下一次被分配。例如,若在特定大小的鏈表中(如 256 個頁框的鏈表)沒有找到空閑塊,算法會查找下一個更大的頁塊,如在 512 個頁框的鏈表中找一個空閑塊。如果存在這樣的塊,內核就把 512 的頁框分成兩等分,一半用作滿足需求,另一半則插入到 256 個頁框的鏈表中。如果在 512 個頁框的塊鏈表中也沒找到空閑塊,就繼續找更大的塊,如 1024 個頁框的塊。如果這樣的塊存在,內核就把 1024 個頁框塊的一部分(滿足程序需求的大?。┯米髡埱?,然后將剩余部分按照大小插入到相應的鏈表中等待后續分配。
4.4釋放過程
當程序釋放內存時,Linux 內核內存伙伴算法會執行以下操作:
操作系統首先將該內存回收,然后檢查與該內存相鄰的內存是否是同樣大小并且同樣處于空閑的狀態。例如,當釋放一個特定大小的內存塊(假設為 256 個頁框的塊)時,算法就把其插入到 256 個頁框的鏈表中,然后檢查與該內存相鄰的內存。
如果是,則將這兩塊內存合并,然后程序遞歸進行同樣的檢查,試圖合并更大的塊。如果存在同樣大小為 256 個頁框的并且空閑的內存,就將這兩塊內存合并成 512 個頁框,然后插入到 512 個頁框的鏈表中。如果合并后的 512 個頁框的內存存在大小為 512 個頁框的相鄰且空閑的內存,則將兩者合并,然后插入到 1024 個頁框的鏈表中。如果不存在合適的伙伴塊,就直接把要釋放的塊掛入鏈表頭等待后續可能的合并操作。
五、CMA算法
在應用程序中,我們申請的內存從虛擬地址角度看是連續的,因為虛擬地址本身具有連續性。但在實際的物理內存空間中,所申請的這片內存未必是連續的。不過對于有內存管理單元(MMU)的系統來說,這并不會影響應用程序的正常運行,因為 MMU 可以將物理地址映射成虛擬地址,應用程序無需關心實際的內存情況。
但如果是沒有 MMU 的情況呢?當設備需要通過 DMA 直接訪問內存時,就迫切需要一片連續的內存空間,而 CMA 機制正是為了解決這一棘手問題而誕生的。DMA zone 區域并非 DMA 設備專屬,其他程序也可以申請該區域的內存。當設備要申請 DMA zone 空間中的一大片連續內存時,如果此時已經沒有連續的大片內存,只有 1 頁、2 頁、4 頁等連續的小內存,CMA 機制就會發揮作用。
系統會事先標記某一片連續區域為 CMA 區域,在沒有大片連續內存申請時,這片區域只給可移動(moveable)的程序使用。當有大片連續內存請求到來時,系統會前往這片 CMA 區域,將所有可移動的小片內存移動到其它的非 CMA 區域,并更改對應的程序頁表,然后把空出來的 CMA 區域分配給設備,從而完美實現了 DMA 大片連續內存的分配。CMA 機制并非孤立存在,它通常是為 DMA 設備服務的。當設備調用dma_alloc_coherent函數申請內存后,為了確保得到一片連續的內存,CMA 機制就會被觸發調用,以保證申請內存的連續性。此外,CMA 區域通常被分配在高端內存,以滿足特定的內存需求場景 。
六、slab算法
當在驅動程序中,遇到反復分配、釋放同一大小的內存塊時(例如,inode、task_struct等),建議使用內存池技術(對象在前后兩次被使用時均分配在同一塊內存或同一類內存空間,且保留了基本的數據結構,這大大提高了效率)。在linux中,有一個叫做slab分配器的內存池管理技術,內存池使用的內存區叫做后備高速緩存。
salb相關頭文件在linux/slab.h中,在使用后備高速緩存前,需要創建一個kmem_cache的結構體。
6.1創建slab緩存區
該函數創建一個slab緩存(后備高速緩沖區),它是一個可以駐留任意數目全部同樣大小的后備緩存。其原型如下:
struct kmem_cache *kmem_cache_create(const char *name, size_t size, \
size_t align, unsigned long flags,\
void (*ctor)(void *, struct kmem_cache *, unsigned long),\
void (*dtor)(void *, struct kmem_cache *, unsigned ong)));
其中:
- name:創建的緩存名;
- size:可容納的緩存塊個數;
- align:后備高速緩沖區中第一個內存塊的偏移量(一般置為0);
- flags:控制如何進行分配的位掩碼,包括 SLAB_NO_REAP(即使內存緊缺也不自動收縮這塊緩存)、SLAB_HWCACHE_ALIGN ( 每 個 數 據 對 象 被 對 齊 到 一 個 緩 存 行 )、SLAB_CACHE_DMA(要求數據對象在 DMA 內存區分配)等);
- ctor:是可選的內存塊對象構造函數(初始化函數);
- dtor:是可選的內存對象塊析構函數(釋放函數)。
6.2分配slab緩存函數
一旦創建完后備高速緩沖區后,就可以調用kmem_cache_alloc()在緩存區分配一個內存塊對象了,其原型如下:
void *kmem_cache_alloc(struct kmem_cache *cachep, gfp_t flags);
cachep指向開始分配的后備高速緩存,flags與傳給kmalloc函數的參數相同,一般為GFP_KERNEL。
6.3釋放slab緩存
該函數釋放一個內存塊對象:
void *kmem_cache_free(struct kmem_cache *cachep, void *objp);
6.4銷毀slab緩存
與kmem_cache_create對應的是銷毀函數,釋放一個后備高速緩存:
int kmem_cache_destroy(struct kmem_cache *cachep);
它必須等待所有已經分配的內存塊對象被釋放后才能釋放后備高速緩存區。
6.5slab緩存使用舉例
創建一個存放線程結構體(struct thread_info)的后備高速緩存,因為在linux中涉及頻繁的線程創建與釋放,如果使用__get_free_page()函數會造成內存的大量浪費,效率也不高。所以在linux內核的初始化階段就創建了一個名為thread_info的后備高速緩存,代碼如下:
/* 創建slab緩存 */
static struct kmem_cache *thread_info_cache;
thread_info_cache = kmem_cache_create("thread_info", sizeof(struct thread_info), \
SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL, NULL);
/* 分配slab緩存 */
struct thread_info *ti;
ti = kmem_cache_alloc(thread_info_cache, GFP_KERNEL);
/* 使用slab緩存 */
...
/* 釋放slab緩存 */
kmem_cache_free(thread_info_cache, ti);
kmem_cache_destroy(thread_info_cache);
七、內存池
在 Linux 內核中還包含對內存池的支持,內存池技術也是一種非常經典的用于分配大量小對象的后備緩存技術。
7.1創建內存池
mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, \
mempool_free_t *free_fn, void *pool_data);
mempool_create()函數用于創建一個內存池,min_nr 參數是需要預分配對象的數目,alloc_fn 和 free_fn 是指向內存池機制提供的標準對象分配和回收函數的指針,其原型分別為:
typedef void *(mempool_alloc_t)(int gfp_mask, void *pool_data);
typedef void (mempool_free_t)(void *element, void *pool_data);
pool_data 是分配和回收函數用到的指針,gfp_mask 是分配標記。只有當_ _GFP_WAIT 標記被指定時,分配函數才會休眠。
7.2分配和回收對象
在內存池中分配和回收對象需由以下函數來完成:
void *mempool_alloc(mempool_t *pool, int gfp_mask);
void mempool_free(void *element, mempool_t *pool);
mempool_alloc()用來分配對象,如果內存池分配器無法提供內存,那么就可以用預分配的池。
7.3銷毀內存池
void mempool_destroy(mempool_t *pool);
mempool_create()函數創建的內存池需由 mempool_destroy()來回收。
八、應用案例分析
8.1實際場景中的應用
在服務器運維領域,內存分配方式的選擇對系統性能有著顯著影響。以運行數據庫服務的服務器為例,數據庫系統需要頻繁地讀寫數據,對內存的需求十分龐大且復雜。在這種場景下,Buddy 算法就發揮著重要作用,它能夠為數據庫分配連續的大塊內存,確保數據的快速存儲和讀取。同時,slab 算法也不可或缺,數據庫中存在許多頻繁使用的小數據結構,如數據庫連接池中的連接對象、緩存中的數據塊描述符等,slab 算法能夠高效地管理這些小內存塊的分配和回收,大大提高了數據庫系統的運行效率。
在嵌入式開發場景中,CMA 算法的應用則尤為關鍵。比如在開發一款基于 Linux 系統的智能攝像頭設備時,攝像頭需要通過 DMA 將拍攝的圖像數據快速傳輸到內存中進行處理。由于圖像數據量較大,需要連續的內存空間來保證數據傳輸的穩定性和高效性。此時,CMA 算法就可以提前預留出一片連續的內存區域作為 CMA 區域,當攝像頭設備需要申請內存時,系統能夠及時從 CMA 區域中分配出連續的內存,滿足攝像頭對 DMA 連續內存的需求,確保圖像數據的正常傳輸和處理,為智能攝像頭的穩定運行提供了有力保障 。
8.2遇到的問題及解決辦法
在實際使用過程中,內存分配可能會引發各種問題。例如,在一個長時間運行的服務器應用中,由于頻繁地進行內存分配和釋放操作,可能會導致內存碎片過多。當內存碎片達到一定程度時,即使系統中存在足夠的空閑內存,也可能無法分配出連續的大塊內存,從而導致某些需要大塊連續內存的操作失敗,如數據庫的大規模數據加載。為了解決這個問題,可以采用內存整理工具,如defragment命令(需 root 權限運行),對內存進行碎片整理,使內存空間變得更加連續,提高內存的利用率 。
再比如,在嵌入式設備開發中,可能會遇到內存分配失敗的情況。這可能是由于設備內存資源有限,而應用程序對內存的需求過大導致的。當這種情況發生時,可以通過優化應用程序的內存使用方式來解決。例如,采用內存池技術,預先分配一塊連續的內存空間,在需要時從內存池中分配內存,避免頻繁地進行系統級的內存分配和釋放操作,從而減少內存碎片的產生,提高內存分配的成功率。同時,合理調整應用程序的功能和數據結構,減少不必要的內存占用,也是解決內存分配失敗問題的有效途徑 。
8.3教你如何查看內存分配情況
了解了 Linux 內存的常見分配方式后,學會如何查看內存分配情況也是非常重要的,這有助于我們實時監控系統內存的使用狀態,及時發現潛在的問題 。
在 Linux 系統中,我們可以通過/proc/buddyinfo文件來查看 Buddy 算法的內存分配情況。這個文件詳細記錄了每個內存節點和區域中不同階的空閑內存塊數量 。例如,在一個單節點系統中,/proc/buddyinfo文件的內容可能如下:
Node 0, zone DMA1 2 1 0 1 0 1 0 1 0 1
Node 0, zone DMA32100 200 150 80 40 20 10 5 2 1 0
Node 0, zone Normal500 400 300 200 100 50 20 10 5 2 1
從這些數據中,我們可以清晰地看到不同區域(如 DMA、DMA32、Normal)中不同大小內存塊(從 order0 到 order10,對應不同的 2 的冪次方大?。┑目捎脭盗?。通過分析這些數據,我們可以判斷系統內存的碎片化程度。如果高階塊(如 order10)的可用塊較少,可能表明內存碎片化嚴重,難以分配到大塊連續內存;如果低階塊(如 order0 或 order1)的可用塊較少,則可能表示系統內存緊張 。
對于 slab 算法的內存分配情況,我們可以使用cat /proc/slabinfo命令來查看。執行該命令后,會輸出一系列的信息,每一行代表一個 slab 緩存 。例如:
slabinfo - version: 2.1
# name <active_objs> <num_objs> <objsize> <objperslab> <pagesperslab> : tunables <limit> <batchcount> <sharedfactor> : slabdata <active_slabs> <num_slabs> <sharedavail>
kmalloc-16 9284 9284 16 256 1 : tunables 0 0 0 : slabdata 36 36 0
kmalloc-32 4567 4567 32 128 1 : tunables 0 0 0 : slabdata 36 36 0
在這些輸出信息中,active_objs表示當前活躍的對象數量,num_objs表示對象總數,objsize表示每個對象的大小,objperslab表示每個 slab 中包含的對象數量,pagesperslab表示每個 slab 占用的頁數。通過這些數據,我們可以了解到各個 slab 緩存的使用情況,判斷是否存在內存分配不合理的情況 。比如,如果某個 slab 緩存中active_objs數量長期很高,而num_objs與active_objs相差不大,可能說明該 slab 緩存中的對象頻繁被使用,需要進一步優化內存分配策略 。
除了上述方法外,我們還可以使用free命令查看系統的物理內存和交換空間使用情況,包括空閑和已用內存的數量、緩沖區和緩存的大小等信息。運行free -h可以以易于閱讀的方式顯示結果 。top命令也是一個實時監測系統資源的工具,它可以顯示當前系統的進程、CPU、內存、磁盤等使用情況,在top的輸出中,我們能夠查看內存使用情況的相關信息,包括總內存、已用內存、空閑內存等 。
htop命令是一個增強版的top命令,提供了更多的交互式功能和可視化顯示,允許我們查看進程的內存使用情況,以及各個進程所占用的內存大小 。這些工具和命令相互配合,能夠幫助我們全面、深入地了解 Linux 系統的內存分配情況,為系統的優化和管理提供有力的支持 。