呼叫malloc()函式之後,核心發生了什麼?附malloc()和free()實現的原始碼
特此宣告:本文參照了另外一篇文章和一個帖子,再結合自己的理解總結了malloc()函式的實現機制。
我們經常會在C程式中呼叫malloc()函式動態分配一塊連續的記憶體空間並使用它們。那麼,這些使用者空間發生的事會引發核心空間什麼樣的反應呢?
malloc()是一個API,這個函式在庫中封裝了系統呼叫brk。因此如果呼叫malloc,那麼首先會引發brk系統呼叫執行的過程。brk()在核心中對應的系統呼叫服務例程為SYSCALL_DEFINE1(brk,
unsigned long, brk),引數brk用來指定heap段新的結束地址,也就是重新指定mm_struct結構中的brk欄位。
brk系統呼叫服務例程首先會確定heap段的起始地址min_brk,然後再檢查資源的限制問題。接著,將新老heap地址分別按照頁大小對齊,對齊後的地址分別儲存在newbrk和okdbrk中。
brk()系統呼叫本身既可以縮小堆大小,又可以擴大堆大小。縮小堆這個功能是通過呼叫do_munmap()完成的。如果要擴大堆的大小,那麼必須先通過find_vma_intersection()檢查擴大以後的堆是否與已經存在的某個虛擬記憶體重合,如何重合則直接退出。否則,呼叫do_brk()進行接下來擴大堆的各種工作。
brk系統呼叫服務例程最後將返回堆的新結束地址。<span style="font-size:18px;">SYSCALL_DEFINE1(brk, unsigned long, brk) { unsigned long rlim, retval; unsigned long newbrk, oldbrk; struct mm_struct *mm = current->mm; unsigned long min_brk; down_write(&mm->mmap_sem); #ifdef CONFIG_COMPAT_BRK min_brk = mm->end_code; #else min_brk = mm->start_brk; #endif if (brk < min_brk) goto out; rlim = rlimit(RLIMIT_DATA); if (rlim < RLIM_INFINITY && (brk - mm->start_brk) + (mm->end_data - mm->start_data) > rlim) newbrk = PAGE_ALIGN(brk); oldbrk = PAGE_ALIGN(mm->brk); if (oldbrk == newbrk) goto set_brk; if (brk brk) { if (!do_munmap(mm, newbrk, oldbrk-newbrk)) goto set_brk; goto out; } if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE)) goto out; if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk) goto out; set_brk: mm->brk = brk; out: retval = mm->brk; up_write(&mm->mmap_sem); return retval; }</span>
使用者程序呼叫malloc()會使得核心呼叫brk系統呼叫服務例程,因為malloc總是動態的分配記憶體空間,因此該服務例程此時會進入第二條執行路徑中,即擴大堆。do_brk()主要完成以下工作:
1.通過get_unmapped_area()在當前程序的地址空間中查詢一個符合len大小的線性區間,並且該線性區間的必須在addr地址之後。如果找到了這個空閒的線性區間,則返回該區間的起始地址,否則返回錯誤程式碼-ENOMEM;
2.通過find_vma_prepare()在當前程序所有線性區組成的紅黑樹中依次遍歷每個vma,以確定上一步找到的新區間之前的線性區物件的位置。如果addr位於某個現存的vma中,則呼叫do_munmap()刪除這個線性區。如果刪除成功則繼續查詢,否則返回錯誤程式碼。
3.目前已經找到了一個合適大小的空閒線性區,接下來通過vma_merge()去試著將當前的線性區與臨近的線性區進行合併。如果合併成功,那麼該函式將返回prev這個線性區的vm_area_struct結構指標,同時結束do_brk()。否則,繼續分配新的線性區。
4.接下來通過kmem_cache_zalloc()在特定的slab快取記憶體vm_area_cachep中為這個線性區分配vm_area_struct結構的描述符。
5.初始化vma結構中的各個欄位。
6.更新mm_struct結構中的vm_total欄位,它用來同級當前程序所擁有的vma數量。
7.如果當前vma設定了VM_LOCKED欄位,那麼通過mlock_vma_pages_range()立即為這個線性區分配物理頁框。否則,do_brk()結束。
可以看到,do_brk()主要是為當前程序分配一個新的線性區,在沒有設定VM_LOCKED標誌的情況下,它不會立刻為該線性區分配物理頁框,而是通過vma一直將分配實體記憶體的工作進行延遲,直至發生缺頁異常。
經過上面的過程,malloc()返回了線性地址,如果此時使用者程序訪問這個線性地址,那麼就會發生缺頁異常(Page Fault)。整個缺頁異常的處理過程非常複雜,我們這裡只關注與malloc()有關的那一條執行路徑。
當CPU產生一個異常時,將會跳轉到異常處理的整個處理流程中。對於缺頁異常,CPU將跳轉到page_fault異常處理程式中。異常處理程式會呼叫do_page_fault()函式,該函式通過讀取CR2暫存器獲得引起缺頁的線性地址,通過各種條件判斷以便確定一個合適的方案來處理這個異常。
do_page_fault()函式:
該函式通過各種條件來檢測當前發生異常的情況,但至少do_page_fault()會區分出引發缺頁的兩種情況:由程式設計錯誤引發異常,以及由程序地址空間中還未分配實體記憶體的線性地址引發。對於後一種情況,通常還分為使用者空間所引發的缺頁異常和核心空間引發的缺頁異常。
核心引發的異常是由vmalloc()產生的,它只用於核心空間記憶體的分配。顯然,我們這裡需要關注的是使用者空間所引發的異常情況。這部分工作從do_page_fault()中的good_area標號處開始執行,主要通過handle_mm_fault()完成。
<span style="font-size:18px;">dotraplinkage void __kprobes do_page_fault(struct pt_regs *regs, unsigned long error_code)
{
…… ……
good_area:
write = error_code & PF_WRITE;
if (unlikely(access_error(error_code, write, vma))) {
bad_area_access_error(regs, error_code, address);
return;
}
fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0);
}</span>
handle_mm_fault()函式:
該函式的主要功能是為引發缺頁的程序分配一個物理頁框,它先確定與引發缺頁的線性地址對應的各級頁目錄項是否存在,如何不存在則分進行分配。具體如何分配這個頁框是通過呼叫handle_pte_fault()完成的。<span style="font-size:18px;">int handle_mm_fault(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, unsigned int flags)
{
pgd_t *pgd;
pud_t *pud;
pmd_t *pmd;
pte_t *pte;
…… ……
pgd = pgd_offset(mm, address);
pud = pud_alloc(mm, pgd, address);
if (!pud)
return VM_FAULT_OOM;
pmd = pmd_alloc(mm, pud, address);
if (!pmd)
return VM_FAULT_OOM;
pte = pte_alloc_map(mm, pmd, address);
if (!pte)
return VM_FAULT_OOM;
return handle_pte_fault(mm, vma, address, pte, pmd, flags);
}</span>
handle_pte_fault()函式:
該函式根據頁表項pte所描述的物理頁框是否在實體記憶體中,分為兩大類:
請求調頁:被訪問的頁框不再主存中,那麼此時必須分配一個頁框。
寫時複製:被訪問的頁存在,但是該頁是隻讀的,核心需要對該頁進行寫操作,此時核心將這個已存在的只讀頁中的資料複製到一個新的頁框中。
使用者程序訪問由malloc()分配的記憶體空間屬於第一種情況。對於請求調頁,handle_pte_fault()仍然將其細分為三種情況:
1.如果頁表項確實為空(pte_none(entry)),那麼必須分配頁框。如果當前程序實現了vma操作函式集合中的fault鉤子函式,那麼這種情況屬於基於檔案的記憶體對映,它呼叫do_linear_fault()進行分配物理頁框。否則,核心將呼叫針對匿名對映分配物理頁框的函式do_anonymous_page()。
2.如果檢測出該頁表項為非線性對映(pte_file(entry)),則呼叫do_nonlinear_fault()分配物理頁。
3.如果頁框事先被分配,但是此刻已經由主存換出到了外存,則呼叫do_swap_page()完成頁框分配。
由malloc分配的記憶體將會呼叫do_anonymous_page()分配物理頁框。
<span style="font-size:18px;">static inline int handle_pte_fault(struct mm_struct *mm, struct vm_area_struct *vma, unsigned long address, pte_t *pte, pmd_t *pmd, unsigned int flags)
{
…… ……
if (!pte_present(entry)) {
if (pte_none(entry)) {
if (vma->vm_ops) {
if (likely(vma->vm_ops->fault))
return do_linear_fault(mm, vma, address,
pte, pmd, flags, entry);
}
return do_anonymous_page(mm, vma, address,
pte, pmd, flags);
}
if (pte_file(entry))
return do_nonlinear_fault(mm, vma, address,
pte, pmd, flags, entry);
return do_swap_page(mm, vma, address,
pte, pmd, flags, entry);
}
…… ……
}</span>
do_anonymous_page()函式:
此時,缺頁異常處理程式終於要為當前程序分配物理頁框了。它通過alloc_zeroed_user_highpage_movable()來完成這個過程。我們層層撥開這個函式的外衣,發現它最終呼叫了alloc_pages()。<span style="font-size:18px;">static int do_anonymous_page(struct mm_struct *mm, struct vm_area_struct *vma,
unsigned long address, pte_t *page_table, pmd_t *pmd,
unsigned int flags)
{
…… ……
if (unlikely(anon_vma_prepare(vma)))
goto oom;
page = alloc_zeroed_user_highpage_movable(vma, address);
if (!page)
goto oom;
…… ……
}</span>
經過這樣一個複雜的過程,使用者程序所訪問的線性地址終於對應到了一塊實體記憶體。下面附上我自認為比較完善的malloc()和free()函式原始碼:
<span style="font-size:18px;">#include <unistd.h>
#include <stdlib.h>
//塊首
union header
{
struct{
union header *next;//指向下一空閒快的指標
unsigned int size;//空閒塊的大小
}s;
long x;//對齊
};
typedef union header Header;
#define NALLOC 1024;//請求的最小單位數,每頁大小為1KB
static Header* moreSys(unsigned int num);//向系統申請一塊記憶體
void* userMalloc(unsigned int nbytes);//從使用者管理區申請記憶體
void userFree(void *ap);//釋放記憶體,放入到使用者管理區
static Header base;//定義空閒連結串列頭
static Header *free_list = NULL;//空閒連結串列的起始查詢指標
void* userMalloc(unsigned int nbytes)
{
Header *p;
Header *prev;
unsigned int unitNum;
//將申請的位元組數nbytes轉換成unitNum個塊首單位,多計算一個作為管理塊首
unitNum = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;
if ((prev = free_list) == NULL)//如果無空閒連結串列,定義空閒連結串列
{
base.s.next = free_list = prev = &base;
base.s.size = 1;
}
for (p = prev->s.next; ; p = p->s.next, prev = p)
{
if (p->s.size >= unitNum)//空閒塊足夠大
{
if (p->s.size <= (unitNum + 1))
{
prev->s.next = p->s.next;
}
else//偏大,切出需要的一塊
{
p->s.size = unitNum;
p += p->s.size;
p->s.size = unitNum;
}
free_list = prev;
return (void *)(p+1);
}
if (p == free_list)
{
if ((p = moreSys(unitNum)) == NULL)//無合適塊,向系統申請
{
return NULL;
}
}
}
}
static Header* moreSys(unsigned int num)
{
char *cp;
Header *up;
if(num < NALLOC)
num = NALLOC;//向系統申請的最小量
cp = sbrk(num * sizeof(Header));
if (cp == (char *)-1)
{
return NULL;//無空閒頁面,返回空地址
}
up = (Header *)cp;
up->s.size = num;
userFree(up + 1);
return free_list;
}</span>
<span style="font-size:18px;">//回收記憶體到空閒鏈上
void Free(void *ap)
{
Header *bp, *p;
bp = (Header *)ap - 1; //指向塊首
for(p = free_list; !(bp>p && bp<p->s.next); p = p->s.next) //按地址定位空閒塊在連結串列
//中的位置
if(p>=p->s.next && (bp>p || bp<p->s.next))
break; //空閒塊在兩端
if(bp + bp->s.size == p->s.next) { //看空閒塊是否與已有的塊相鄰,相鄰就合併
bp->s.size += p->s.next->s.size;
bp->s.next = p->s.next->s.next;
}
else
bp->s.next = p->s.next;
if(p + p->s.size == bp) {
p->s.size += bp->s.size;
p->s.next = bp->s.next;
}
else
p->s.next = bp;
free_list = p;
}</span>