malloc實現過程

前言

最近重溫深入理解計算機系統,看到了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來分配物理頁框,就這樣用戶進程所訪問的線性地址終於獲得了一塊物理內存。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章