考慮這樣一種常見的狀況:用戶進程調用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) |
3 |
unsigned long rlim, retval; |
4 |
unsigned long newbrk, oldbrk; |
5 |
struct mm_struct *mm = current->mm; |
8 |
down_write(&mm->mmap_sem); |
10 |
#ifdef CONFIG_COMPAT_BRK |
11 |
min_brk = mm->end_code; |
13 |
min_brk = mm->start_brk; |
18 |
rlim = rlimit(RLIMIT_DATA); |
19 |
if (rlim < RLIM_INFINITY && (brk - mm->start_brk) + |
20 |
(mm->end_data - mm->start_data) > rlim) |
22 |
newbrk = PAGE_ALIGN(brk); |
23 |
oldbrk = PAGE_ALIGN(mm->brk); |
28 |
if (!do_munmap(mm, newbrk, oldbrk-newbrk)) |
33 |
if (find_vma_intersection(mm, oldbrk, newbrk+PAGE_SIZE)) |
36 |
if (do_brk(oldbrk, newbrk-oldbrk) != oldbrk) |
42 |
up_write(&mm->mmap_sem); |
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異常處理程序中:
5 |
CFI_ADJUST_CFA_OFFSET 4 |
該異常處理程序會調用do_page_fault()函數,該函數經過讀取CR2寄存器得到引發缺頁的線性地址,經過各類條件判斷以便肯定一個合適的方案來處理這個異常。
3.1.do_page_fault()
該函數經過各類條件來檢測當前發生異常的狀況,但至少do_page_fault()會區分出引起缺頁的兩種狀況:由編程錯誤引起異常,以及由進程地址空間中還未分配物理內存的線性地址引起。對於後一種狀況,一般還分爲用戶空間所引起的缺頁異常和內核空間引起的缺頁異常。
內核引起的異常是由vmalloc()產生的,它只用於內核空間內存的分配。顯然,咱們這裏須要關注的是用戶空間所引起的異常狀況。這部分工做從do_page_fault()中的good_area標號處開始執行,主要經過handle_mm_fault()完成。
2 |
dotraplinkage void __kprobes |
3 |
do_page_fault( struct pt_regs *regs, unsigned long error_code) |
7 |
write = error_code & PF_WRITE; |
9 |
if (unlikely(access_error(error_code, write, vma))) { |
10 |
bad_area_access_error(regs, error_code, address); |
13 |
fault = handle_mm_fault(mm, vma, address, write ? FAULT_FLAG_WRITE : 0); |
3.2.handle_mm_fault()
該函數的主要功能是爲引起缺頁的進程分配一個物理頁框,它先肯定與引起缺頁的線性地址對應的各級頁目錄項是否存在,如何不存在則分進行分配。具體如何分配這個頁框是經過調用handle_pte_fault()完成的。
1 |
int handle_mm_fault( struct mm_struct *mm, struct vm_area_struct *vma, |
2 |
unsigned long address, unsigned int flags) |
9 |
pgd = pgd_offset(mm, address); |
10 |
pud = pud_alloc(mm, pgd, address); |
13 |
pmd = pmd_alloc(mm, pud, address); |
16 |
pte = pte_alloc_map(mm, pmd, address); |
19 |
return handle_pte_fault(mm, vma, address, pte, pmd, flags); |
3.3.handle_pte_fault()
該函數根據頁表項pte所描述的物理頁框是否在物理內存中,分爲兩大類:
請求調頁:被訪問的頁框再也不主存中,那麼此時必須分配一個頁框。
寫時複製:被訪問的頁存在,可是該頁是隻讀的,內核須要對該頁進行寫操做,此時內核將這個已存在的只讀頁中的數據複製到一個新的頁框中。
用戶進程訪問由malloc()分配的內存空間屬於第一種狀況。對於請求調頁,handle_pte_fault()仍然將其細分爲三種狀況:
1 |
static inline int handle_pte_fault( struct mm_struct *mm, |
2 |
struct vm_area_struct *vma, unsigned long address, |
3 |
pte_t *pte, pmd_t *pmd, unsigned int flags) |
6 |
if (!pte_present(entry)) { |
9 |
if (likely(vma->vm_ops->fault)) |
10 |
return do_linear_fault(mm, vma, address, |
11 |
pte, pmd, flags, entry); |
13 |
return do_anonymous_page(mm, vma, address, |
17 |
return do_nonlinear_fault(mm, vma, address, |
18 |
pte, pmd, flags, entry); |
19 |
return do_swap_page(mm, vma, address, |
20 |
pte, pmd, flags, entry); |
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 |
static int do_anonymous_page( struct mm_struct *mm, struct vm_area_struct *vma, |
2 |
unsigned long address, pte_t *page_table, pmd_t *pmd, |
6 |
if (unlikely(anon_vma_prepare(vma))) |
8 |
page = alloc_zeroed_user_highpage_movable(vma, address); |
通過這樣一個複雜的過程,用戶進程所訪問的線性地址終於對應到了一塊物理內存。
參考:
1.《深刻理解LINUX內核》
2.《深刻LINUX內核架構》
原文地址:http://edsionte.com/techblog/archives/4174