前言
最近重溫深入理解計算機系統,看到了malloc的實現,malloc主要涉及到兩個系統調用brk和mmap.廢話不多說直接上源碼,一切盡在源碼之中。
brk系統調用的入口函數爲sys_brk()
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;
#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)
goto out;
mm->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)
mm_populate(oldbrk, newbrk - oldbrk);
return brk;
out:
retval = origbrk;
up_write(&mm->mmap_sem);
return retval;
}
我們知道,堆是從低地址向高地址增長的,sys_brk 函數的參數brk是新的堆頂位置,而當前的mm->brk是原來堆頂的位置。
brk系統調用服務例程首先會確定heap段的起始地址min_brk,然後再檢查資源的限制問題。接着,將新老heap地址分別按照頁大小對齊,對齊後的地址分別存儲與new_brk和old_brk中。如果兩者相同,說明這次增加的堆的量很小,還在一個頁,不需要另行分配,直接跳轉至set_brk,設置mm->brk爲新的brk就可以。
如果發現新舊堆頂不在一個頁裏面,這將麻煩了,說明要進行跨頁分配。如果發現新堆頂小於舊堆頂,這說明不是新分配內存,而是釋放內存了,釋放的還不小,至少釋放了一頁,於是調用do_munmap來將這些頁的內存映射去掉。
如果堆要擴大,就要調用find_vma,我們知道vm_area_struct通過vm_rb將這個區域放在紅黑樹上,這個函數將在紅黑樹上查找。找到原堆頂所在的vm_area_struct的下一個vm_area_struct,看當前的堆頂和下一個vm_area_struct之間還能不能分配一個完整的頁。如果不能,只好直接退出返回,內存空間都被佔滿。
如果還有空間就調用do_brk進一步分配堆空間,從舊堆頂開始,分配計算出新舊堆頂之間的頁數。
do_brk源碼
static int do_brk(unsigned long addr, unsigned long len, struct list_head *uf)
{
return do_brk_flags(addr, len, 0, uf);
}
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;
/* 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;
error = get_unmapped_area(NULL, addr, len, 0, MAP_FIXED);
if (offset_in_page(error))
return error;
error = mlock_future_check(mm, mm->def_flags, len);
if (error)
return error;
/*
* mm->mmap_sem is required to protect against another thread
* changing the mappings in case we sleep.
*/
verify_mm_writelocked(mm);
/*
* Clear old maps. this also does some error checking for us
*/
while (find_vma_links(mm, addr, addr + len, &prev, &rb_link,
&rb_parent)) {
if (do_munmap(mm, addr, len, 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? */
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);
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;
}
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),該異常處理程序會調用do_page_fault函數。
do_page_fault()
由編程錯誤引發異常,以及由進程地址空間中還未分配物理內存的線性地址引發。對於後一種情況,通常還分爲用戶空間所引發的缺頁異常和內核空間引發的缺頁異常。
內核引發的異常是由vmalloc產生的,它只用於內核空間內存分配。顯然,我們關注的是用戶態空間引發的異常,主要由handle_mm_fault完成處理。
handle_mm_fault()
該函數主要功能是爲引發缺頁的進程分配一個物理頁框,它先確定引發缺頁的線性地址對應的各級頁目錄項是否存在,如果不存在則進行分配,具體分配頁框通過調用handle_pte_fault完成。
handle_pte_fault()
該函數根據頁表項pte所描述的物理頁框是否在物理內存中,分爲兩大類:
請求調頁:被請求的頁框不在主存中,那麼此時必須分配一個頁框。
寫時複製:被訪問的頁存在,但是該頁是隻讀的,內存需要對該頁進行寫操作,此時內核將這個已存在的只讀頁中的數據複製到一個新的頁框中。
malloc引發的異常屬於第一種,對於請求雕也,handle_pte_fault()仍然將其細分爲三種情況:
- 如果頁表項確實爲空(pte_none(entry)),那麼必須分配頁框。如果當前進程實現了vma操作函數集合中的fault鉤子函數,那麼這種情況屬於基於文件的內存映射,它調用do_linear_fault()進行分配物理頁框。否則,內核將調用針對匿名映射分配物理頁框的函數do_anonymous_page()。
- 如果檢測出該頁表項爲非線性映射(pte_file(entry)),則調用do_nonlinear_fault()分配物理頁。
- 如果頁框事先被分配,但是此刻已經由主存換出到了外存,則調用do_swap_page()完成頁框分配。
由malloc分配的內存將會調用do_anonymous_page()分配物理頁框。
do_anonymous_page()
最終還是通過alloc_pages來分配物理頁框,就這樣用戶進程所訪問的線性地址終於獲得了一塊物理內存。