malloc()以後,內核發生了什麼?【轉】

 

考慮這樣一種常見的狀況:用戶進程調用malloc()動態分配了一塊內存空間,再對這塊內存進行訪問。這些用戶空間發生的事會引起內核空間的那些反映?本文將簡單爲您解答。緩存

1.brk系統調用服務例程

malloc()是一個API,這個函數在庫中封裝了系統調用brk。所以若是調用malloc,那麼首先會引起brk系統調用執行的過程。brk()在內核中對應的系統調用服務例程爲SYSCALL_DEFINE1(brk, unsigned long, brk),參數brk用來指定heap段新的結束地址,也就是從新指定mm_struct結構中的brk字段。架構

brk系統調用服務例程首先會肯定heap段的起始地址min_brk,而後再檢查資源的限制問題。接着,將新老heap地址分別按照頁大小對齊,對齊後的地址分別存儲與newbrk和okdbrk中。app

brk()系統調用自己既能夠縮小堆大小,又能夠擴大堆大小。縮小堆這個功能是經過調用do_munmap()完成的。若是要擴大堆的大小,那麼必須先經過find_vma_intersection()檢查擴大之後的堆是否與已經存在的某個虛擬內存重合,如何重合則直接退出。不然,調用do_brk()進行接下來擴大堆的各類工做。函數

1 SYSCALL_DEFINE1(brk, unsigned long, brk)
2 {
3         unsignedlongrlim, retval;
4         unsignedlongnewbrk, oldbrk;
5         structmm_struct *mm = current->mm;
6         unsignedlongmin_brk;
7  
8         down_write(&mm->mmap_sem);
9  
10 #ifdef CONFIG_COMPAT_BRK
11         min_brk = mm->end_code;
12 #else
13         min_brk = mm->start_brk;
14 #endif
15         if(brk < min_brk)
16                 gotoout;
17  
18         rlim = rlimit(RLIMIT_DATA);
19         if(rlim < RLIM_INFINITY && (brk - mm->start_brk) +
20                         (mm->end_data - mm->start_data) > rlim)
21  
22         newbrk = PAGE_ALIGN(brk);
23         oldbrk = PAGE_ALIGN(mm->brk);
24         if(oldbrk == newbrk)
25                 gotoset_brk;
26  
27         if(brk brk) {
28                 if(!do_munmap(mm, newbrk, oldbrk-newbrk))
29                         gotoset_brk;
30                 gotoout;
31         }
32  
33         if(find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE))
34                 gotoout;
35  
36         if(do_brk(oldbrk, newbrk-oldbrk) != oldbrk)
37                 gotoout;
38 set_brk:
39         mm->brk = brk;
40 out:
41         retval = mm->brk;
42         up_write(&mm->mmap_sem);
43         returnretval;
44 }

brk系統調用服務例程最後將返回堆的新結束地址。網站

2.擴大堆

用戶進程調用malloc()會使得內核調用brk系統調用服務例程,由於malloc老是動態的分配內存空間,所以該服務例程此時會進入第二條執行路徑中,即擴大堆。do_brk()主要完成如下工做:spa

1.經過get_unmapped_area()在當前進程的地址空間中查找一個符合len大小的線性區間,而且該線性區間的必須在addr地址以後。若是找到了這個空閒的線性區間,則返回該區間的起始地址,不然返回錯誤代碼-ENOMEM;.net

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一直將分配物理內存的工做進行延遲,直至發生缺頁異常。

3.缺頁異常的處理過程

通過上面的過程,malloc()返回了線性地址,若是此時用戶進程訪問這個線性地址,那麼就會發生缺頁異常(Page Fault)。整個缺頁異常的處理過程很是複雜,咱們這裏只關注與malloc()有關的那一條執行路徑。

當CPU產生一個異常時,將會跳轉到異常處理的整個處理流程中。對於缺頁異常,CPU將跳轉到page_fault異常處理程序中:

1 //linux-2.6.34/arch/x86/kernel/entry_32.S
2 ENTRY(page_fault)
3         RING0_EC_FRAME
4         pushl $do_page_fault
5         CFI_ADJUST_CFA_OFFSET 4
6         ALIGN
7 error_code:
8         …………
9         jmp ret_from_exception
10         CFI_ENDPROC
11 END(page_fault)

該異常處理程序會調用do_page_fault()函數,該函數經過讀取CR2寄存器得到引發缺頁的線性地址,經過各類條件判斷以便肯定一個合適的方案來處理這個異常。

3.1.do_page_fault()

該函數經過各類條件來檢測當前發生異常的狀況,但至少do_page_fault()會區分出引起缺頁的兩種狀況:由編程錯誤引起異常,以及由進程地址空間中還未分配物理內存的線性地址引起。對於後一種狀況,一般還分爲用戶空間所引起的缺頁異常和內核空間引起的缺頁異常。

內核引起的異常是由vmalloc()產生的,它只用於內核空間內存的分配。顯然,咱們這裏須要關注的是用戶空間所引起的異常狀況。這部分工做從do_page_fault()中的good_area標號處開始執行,主要經過handle_mm_fault()完成。

1 //linux-2.6.34/arch/x86/mm/fault.c
2 dotraplinkagevoid__kprobes
3 do_page_fault(structpt_regs *regs, unsigned longerror_code)
4 {
5 …… ……
6 good_area:
7         write = error_code & PF_WRITE;
8  
9         if(unlikely(access_error(error_code, write, vma))) {
10                 bad_area_access_error(regs, error_code, address);
11                 return;
12         }
13         fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0);
14 …… ……
15 }

3.2.handle_mm_fault()

該函數的主要功能是爲引起缺頁的進程分配一個物理頁框,它先肯定與引起缺頁的線性地址對應的各級頁目錄項是否存在,如何不存在則分進行分配。具體如何分配這個頁框是經過調用handle_pte_fault()完成的。

1 inthandle_mm_fault(structmm_struct *mm, structvm_area_struct *vma,
2                 unsignedlongaddress, unsigned intflags)
3 {
4         pgd_t *pgd;
5         pud_t *pud;
6         pmd_t *pmd;
7         pte_t *pte;
8         …… ……
9         pgd = pgd_offset(mm, address);
10         pud = pud_alloc(mm, pgd, address);
11         if(!pud)
12                 returnVM_FAULT_OOM;
13         pmd = pmd_alloc(mm, pud, address);
14         if(!pmd)
15                 returnVM_FAULT_OOM;
16         pte = pte_alloc_map(mm, pmd, address);
17         if(!pte)
18                 returnVM_FAULT_OOM;
19           returnhandle_pte_fault(mm, vma, address, pte, pmd, flags);
20 }

3.3.handle_pte_fault()

該函數根據頁表項pte所描述的物理頁框是否在物理內存中,分爲兩大類:

請求調頁:被訪問的頁框再也不主存中,那麼此時必須分配一個頁框。

寫時複製:被訪問的頁存在,可是該頁是隻讀的,內核須要對該頁進行寫操做,此時內核將這個已存在的只讀頁中的數據複製到一個新的頁框中。

用戶進程訪問由malloc()分配的內存空間屬於第一種狀況。對於請求調頁,handle_pte_fault()仍然將其細分爲三種狀況:

1 staticinline int handle_pte_fault(structmm_struct *mm,
2                 structvm_area_struct *vma, unsigned longaddress,
3                 pte_t *pte, pmd_t *pmd, unsigned intflags)
4 {
5         …… ……
6         if(!pte_present(entry)) {
7                 if(pte_none(entry)) {
8                         if(vma->vm_ops) {
9                                 if(likely(vma->vm_ops->fault))
10                                         returndo_linear_fault(mm, vma, address,
11                                                 pte, pmd, flags, entry);
12                         }
13                         returndo_anonymous_page(mm, vma, address,
14                                                  pte, pmd, flags);
15                 }
16                 if(pte_file(entry))
17                         returndo_nonlinear_fault(mm, vma, address,
18                                         pte, pmd, flags, entry);
19                 returndo_swap_page(mm, vma, address,
20                                         pte, pmd, flags, entry);
21         }
22 …… ……
23 }

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()分配物理頁框。

3.4.do_anonymous_page()

此時,缺頁異常處理程序終於要爲當前進程分配物理頁框了。它經過alloc_zeroed_user_highpage_movable()來完成這個過程。咱們層層撥開這個函數的外衣,發現它最終調用了alloc_pages()。

1 staticint do_anonymous_page(structmm_struct *mm, structvm_area_struct *vma,
2                 unsignedlongaddress, pte_t *page_table, pmd_t *pmd,
3                 unsignedintflags)
4 {
5 …… ……
6         if(unlikely(anon_vma_prepare(vma)))
7                 gotooom;
8         page = alloc_zeroed_user_highpage_movable(vma, address);
9         if(!page)
10                 gotooom;
11 …… ……
12 }

通過這樣一個複雜的過程,用戶進程所訪問的線性地址終於對應到了一塊物理內存。

參考:

1.《深刻理解LINUX內核》

2.《深刻LINUX內核架構

 

原文地址:http://edsionte.com/techblog/archives/4174

相關文章
相關標籤/搜索