linux-glibc內存管理小結3(物理頁的分配)

在上一節咱們討論了用戶態向內核申請內存的接口(系統調用), 發現內核僅僅是判斷進程的虛擬地址空間是否足夠劃分出新的區間, 實際並未分配物理內存. 這是由於內核分配內存的機制是僅當進程實際使用該地址後才爲其分配物理內存, 藉此提高物理內存使用率. 本節咱們就來看看內核到底是如何分配物理內存的.
以32bit ARM架構爲例, 首先咱們來看下ARM ref manual中關於異常的這一章. ARM一共定義了七種異常, 分別爲: 復位, 未定義指令, 軟中斷, 預取指異常, 數據訪問異常, 中斷與快中斷. 其中復位, 中斷, 快中斷較常見, 未定義指令咱們在分析kprobe時見到過, swi爲EABI glibc執行系統調用的方式, 剩下的預取指異常與數據訪問異常即咱們本節要分析的入口. 當咱們訪問一個未被MMU映射的地址時系統會拋出這兩種異常(直接跳入對應異常向量中). ARM ref manual要求在進入異常後內核能保存現場, 執行異常處理程序, 最後恢復原程序運行. 其中跳轉到異常處理程序是由硬件執行的, ARM會從0x00000000開始的向量表中選擇對應向量地址執行異常處理程序, 對於支持異常向量表重定位的CPU會從0xFFFF0000查詢向量表. 選擇從何處讀取向量表是由CP15的寄存器1的第13位決定的(見ARM ref manual Part B 2.4章). 內核在運行時必須將指定的向量表放到對應的地址上, 讓咱們來看下內核是如何實現的.
在arch/arm/kernel/entry-armv.S文件中定義了異常向量表. __vectors_start與__vectors_end分別爲其起始地址與結束地址.node

 1 .globl __vectors_start  2 __vectors_start:
 3     ARM( swi SYS_ERROR0 )  4     THUMB( svc #0 )  5     THUMB( nop )  6     W(b) vector_und + stubs_offset  7     W(ldr) pc, .LCvswi + stubs_offset  8     W(b) vector_pabt + stubs_offset  9     W(b) vector_dabt + stubs_offset 10     W(b) vector_addrexcptn + stubs_offset 11     W(b) vector_irq + stubs_offset 12     W(b) vector_fiq + stubs_offset 13     .globl __vectors_end 14 __vectors_end:
15     .data

 

其中W()(defined in arch/arm/include/asm/unified.h)宏在定義THUMB2_KERNEL時將指令擴展爲word格式, 不然即指令自己, THUMB()/ARM()(defined in arch/arm/include/asm/unified.h)宏分別在支持THUMB指令與不支持THUMB指令時起效, 對於本文未定義THUMB2_KERNEL, 即僅使用ARM指令.
能夠看到向量表中大部分爲跳轉指令, 只有復位與軟中斷稍稍不一樣, 前者發出一個軟中斷指令, 後者使用ldr指令跳轉. 另外注意的是vector_addrexcptn, 在ARM ref manual中該向量默認不使用, 在代碼中爲循環跳轉自身. 對於大部分異常其跳轉地址爲vector_xxx加上偏移stubs_offset, 這個地址是如何計算獲得的, 咱們須要首先看下early_trap_init()(defined in arch/arm/kernel/traps.c).linux

 1 void __init early_trap_init(void *vectors_base)  2 {  3     unsigned long vectors = (unsigned long)vectors_base;  4     extern char __stubs_start[], __stubs_end[];  5     extern char __vectors_start[], __vectors_end[];  6     extern char __kuser_helper_start[], __kuser_helper_end[];  7     int kuser_sz = __kuser_helper_end - __kuser_helper_start;  8     vectors_page = vectors_base;  9     memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start); 10     memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start); 11     memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz); 12     ...... 13 }

 

咱們先省略與本節內容無關的代碼, 在early_trap_init()中會將向量表與異常處理函數表分別拷貝到0xFFFF0000與0xFFFF0200(若是使用高地址異常向量表). 因爲兩張表都進行重定向, 因此跳轉向量地址需根據當前PC的相對偏移得出, stubs_offset做用就是獲取相對偏移. 那麼異常處理函數表(vector_xxx)是怎麼生成的呢? 咱們能夠看到arch/arm/kernel/entry-armv.S中如下宏:數組

 1 vector_stub dabt, ABT_MODE, 8
 2 vector_stub pabt, ABT_MODE, 4
 3 .macro vector_stub, name, mode, correction=0
 4 .align 5
 5 vector_\name:
 6     /**  7      * 計算觸發異常的指令地址  8      * 根據不一樣異常模式correction爲不一樣值  9      * 10     **/ 11     .if \correction 12     sub lr, lr, #\correction 13     .endif 14     /** 15      * 在棧上保存r0與lr, 接下來r0與lr會被修改 16      * 17     **/ 18     stmia sp, {r0, lr} 19     /** 20      * 保存觸發異常時cpsr 21      * 22     **/ 23     mrs lr, spsr 24     str lr, [sp, #8] 25     /** 26      * 進入svc模式, 修改當前spsr值 27      * 28     **/ 29     mrs r0, cpsr 30     eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE) 31     msr spsr_cxsf, r0 32     /** 33      * cpsr低四位爲模式位, 根據觸發異常時cpsr值獲取模式並計算對應異常向量地址 34      * 注意在跳轉異常向量前r0被修改成sp 35      * 36     **/ 37     and lr, lr, #0x0f 38     THUMB( adr r0, 1f ) 39     THUMB( ldr lr, [r0, lr, lsl #2] ) 40     mov r0, sp 41     ARM( ldr lr, [pc, lr, lsl #2] ) 42     movs pc, lr 43 ENDPROC(vector_\name) 44     .align 2
45 1: 46 .endm

 

在分析代碼前咱們先來看下ARM ref manual中對異常處理的描述. 首先ARM提供七種運行模式, 分別爲usr, irq, fiq, svc, abt, und與sys. 其中usr模式即用戶進程運行模式, 其它均爲特權模式. 而特權模式中又僅sys模式沒有對應模式的寄存器, 其它模式的r13(SP), r14(lr)與spsr(usr模式無該寄存器)在不一樣模式下均存在對應的影子寄存器, 尤爲fiq模式下r8到r12也有本身的影子寄存器. 對於不一樣的異常其返回方式也不一樣, 以dabt爲例其異常處理例程返回時需執行subs pc, lr, #8(若是無需從新執行引起異常的指令也能夠subs pc, lr, #4), 該指令會從lr_abt中恢復pc並從spsr_abt中恢復cpsr並從新執行指令(見ARM ref manual Part A 2.6.5章).
所以進入異常處理函數後第一件事是計算返回地址, 對於dabt一般咱們會從新執行指令, 即lr先減8. 以後要作是保存幾個關鍵寄存器防止異常現場被破壞, 其中r0因存儲棧地址原值被破壞須要保存, 而lr與spsr爲異常狀態寄存器也須要在退出異常前保存狀態(對於dabt而言, lr_abt爲引起異常地址加8, spsr_abt爲cpsr). 注意此處的棧增加與通常情形相反, 是向上的. 保存完異常現場後準備切換模式, 將spsr寄存器的模式位修改成svc模式. 同時lr存儲的spsr是產生異常前cpsr的值, 經過位與能夠獲取以前運行模式, 根據運行模式不一樣選擇不一樣異常處理函數. lr的計算爲PC加上lr乘以4(2的2次方), 舉例而言用戶態段錯誤模式位爲10000b, lr計算結果爲PC, 因ARM三級流水線架構(取指, 譯碼, 執行)PC領先實際執行指令兩條指令, 故lr爲__dabt_usr, 最後將SP賦值給r0後跳轉lr. 能夠發現雖然每種異常下都有16個函數入口, 但實際有效的入口只有兩個, 分別爲xxx_usr與xxx_svc(內核只實現了兩種模式下接口). 本節咱們僅分析dabt與pabt相關的異常處理程序.緩存

 1 .align 5
 2 __dabt_usr:
 3     usr_entry  4     kuser_cmpxchg_check  5     mov r2, sp  6     dabt_helper  7     b ret_from_exception  8     UNWIND(.fnend)  9 ENDPROC(__dabt_usr) 10 .align 5
11 __dabt_svc:
12     svc_entry 13     mov r2, sp 14     dabt_helper 15     svc_exit r5 16     UNWIND(.fnend) 17 ENDPROC(__dabt_svc) 18 .align 5
19 __pabt_usr:
20     usr_entry 21     mov r2, sp 22     pabt_helper 23     UNWIND(.fnend) 24 ENTRY(ret_from_exception) 25     UNWIND(.fnstart) 26     UNWIND(.cantunwind) 27     get_thread_info tsk 28     mov why, #0
29     b ret_to_user 30     UNWIND(.fnend) 31 ENDPROC(__pabt_usr) 32 ENDPROC(ret_from_exception) 33 .align 5
34 __pabt_svc:
35     svc_entry 36     mov r2, sp 37     pabt_helper 38     svc_exit r5 39     UNWIND(.fnend) 40 ENDPROC(__pabt_svc)

 

能夠看出異常處理入口基本大同小異, 首先是保存現場(usr_entry/svc_entry), 選擇合適的異常處理例程(dabt_helper/pabt_helper), 最後恢復現場(ret_from_exception/ret_to_user/svc_exit). 咱們先來看看如何保存異常現場.安全

 1 .macro usr_entry  2     UNWIND(.fnstart)  3     UNWIND(.cantunwind)  4     /**  5      * 壓棧r0-r12  6      * 注意壓棧寄存器排列順序是按pt_regs結構排列的, 即sp指向一個pt_regs結構  7      *  8     **/  9     sub sp, sp, #S_FRAME_SIZE 10     ARM( stmib sp, {r1 - r12} ) 11     THUMB( stmia sp, {r0 - r12} ) 12     /** 13      * r0在以前被設置爲sp, 即此處將緩存的r0, lr與cpsr取出 14      * pt_regs中的orig_r0在usr_entry中被強制賦值爲-1(對應r6) 15      * 16     **/ 17     ldmia r0, {r3 - r5} 18     add r0, sp, #S_PC 19     mov r6, #-1
20     /** 21      * r3緩存的是r0, r4-r6分別緩存lr, cpsr與orig_r0 22      * 23     **/ 24     str r3, [sp] 25     stmia r0, {r4 - r6} 26     ARM( stmdb r0, {sp, lr}^ ) 27     THUMB( store_user_sp_lr r0, r1, S_SP - S_PC ) 28     zero_fp 29 #ifdef CONFIG_IRQSOFF_TRACER 30     bl trace_hardirqs_off 31 #endif 32     ct_user_exit save = 0
33 .endm 34 .macro svc_entry, stack_hole=0
35     UNWIND(.fnstart) 36     UNWIND(.save {r0 - pc}) 37     sub sp, sp, #(S_FRAME_SIZE + \stack_hole - 4) 38     SPFIX( tst sp, #4 ) 39     SPFIX( subeq sp, sp, #4 ) 40     stmia sp, {r1 - r12} 41     ldmia r0, {r3 - r5} 42     add r7, sp, #S_SP - 4
43     mov r6, #-1
44     add r2, sp, #(S_FRAME_SIZE + \stack_hole - 4) 45     SPFIX( addeq r2, r2, #4 ) 46     str r3, [sp, #-4]! 47     mov r3, lr 48     stmia r7, {r2 - r6} 49 #ifdef CONFIG_TRACE_IRQFLAGS 50     bl trace_hardirqs_off 51 #endif 52 .endm

 

咱們先來分析保存用戶現場的接口usr_entry, 該接口做用是在棧上預留一個struct pt_regs大小的空間記錄用觸發異常時寄存器狀態並將其地址保存在r0中傳遞給以後的程序. 與一般使用push壓棧方式不一樣, 因後面C函數是以結構體指針方式訪問該地址, 因此此處是先遞減空間再使用stm壓棧. 另外注意進入該接口時內核已處於SVC模式, 因此以前棧上保存的lr與spsr纔是觸發異常時的寄存器. 這裏有個存疑的問題: orig_r0爲何要設爲0xFFFFFFFF?
再來看下保存內核現場的接口svc_entry, 它與usr_entry大同小異. SPFIX()宏要求遵循ARM EABI規範(對於本文天然是起效的), 其做用是將struct pt_regs按8字節對齊(tst sp, #4結果爲0則再減4字節, 由於此時sp指向r1).
svc_entry與usr_entry的區別是前者修改了struct pt_regs中sp往上的全部寄存器, 然後者僅修改lr, cpsr, ori_r0, 注意最後一條stm指令中r2到r6依次爲: pt_regs->ARM_sp的地址, lr_svc, lr_abt(即引起異常的指令地址), spsr_abt, 0xFFFFFFFF(ori_r0).
保存完用戶現場後還要作的是將當前棧地址sp傳遞給r2, 此時sp指向的即struct pt_regs地址. 以後進入abt_handler選擇函數dabt_helper/pabt_helper, 二者幾乎如出一轍, 因此咱們僅分析下dabt_helper. 在dabt_helper中的源碼註釋告訴咱們在調用該接口時r2爲struct pt_regs指針, r4爲異常指令地址, r5爲異常cpsr, 且異常處理函數會將異常地址返回在r0中, 異常狀態寄存器返回在r1中, r9做爲保留寄存器. 咱們來看下dabt_helper究竟作了什麼.cookie

 1 .macro dabt_helper  2 #ifdef MULTI_DABORT  3     ldr ip, .LCprocfns  4     mov lr, pc  5     ldr pc, [ip, #PROCESSOR_DABT_FUNC]  6 #else  7     bl CPU_DABORT_HANDLER  8 #endif  9 .endm 10 .macro pabt_helper 11 #ifdef MULTI_PABORT 12     ldr ip, .LCprocfns 13     mov lr, pc 14     ldr pc, [ip, #PROCESSOR_PABT_FUNC] 15 #else 16     bl CPU_PABORT_HANDLER 17 #endif 18 .endm 19 #ifdef MULTI_DABORT 20 .LCprocfns:
21     .word processor 22 #endif

 

MULTI_DABORT(defined in arch/arm/include/asm/glue-df.h)宏定義了是否支持多個abort處理例程, 對於支持MULTI_DABORT的架構(在本文中顯然不是支持的), 須要索引異常處理函數句柄. .LCprocfns是全局變量processor(defined in arch/arm/include/asm/proc-fns.h)的地址, 該結構包含了架構相關的回調函數, 此處就不列舉了. PROCESSOR_PABT_FUNC爲該結構中_data_abort的偏移, 即此處將pc保存給lr(pc領先lr兩個指令, 實際是指向dabt_helper後的第一條指令), 而後將pc修改成_data_abort回調地址來完成跳轉. 對於不支持MULTI_DABORT的架構則簡單的多, 直接跳轉到CPU_PABORT_HANDLER. 該宏的定義一樣見arch/arm/include/asm/glue-df.h, 能夠看到ARMv7架構下該宏展開爲v7_early_abort.
此處爲以前出錯的文章作個補充註解, 關於此處以前段錯誤分析一文有誤. 以前分析時候覺得是內核支持多種abort處理例程, 如今回頭看MULTI_DABORT既然僅在arch/arm/include/asm/glue-df.h中而不放到config中定義, 說明更可能是與硬件相關的差別. 個人理解是可能config中同時定義瞭如armv7與v7_early相關的宏, 即內核編譯時生成了兩套abort處理接口, cpu在運行時再初始化具體的abort處理函數. 毫無疑問3536僅定義了CPU_V7宏, 天然是第二種狀況, 反彙編也證明了這一點. 但鑑於本節也未分析MULTI_DABORT的狀況, 因此前文的錯誤也暫時不修改了.數據結構

 1 c03b1600 <__dabt_usr>:  2 c03b1600:   e24dd048    sub sp, sp, #72 ; 0x48
 3 c03b1604:   e98d1ffe    stmib sp, {r1, r2, r3, r4, r5, r6, r7, r8, r9, sl, fp, ip}  4 c03b1608:   e8900038    ldm r0, {r3, r4, r5}  5 c03b160c:   e28d003c    add r0, sp, #60 ; 0x3c
 6 c03b1610:   e3e06000    mvn r6, #0
 7 c03b1614:   e58d3000    str r3, [sp]  8 c03b1618:   e8800070    stm r0, {r4, r5, r6}  9 c03b161c:   e9406000    stmdb r0, {sp, lr}^ 10 c03b1620:   e51f0048    ldr r0, [pc, #-72] ; c03b15e0 <__pabt_svc+0x60>
11 c03b1624:   e5900000    ldr r0, [r0] 12 c03b1628:   ee010f10    mcr 15, 0, r0, cr1, cr0, {0} 13 c03b162c:   e1a0200d    mov r2, sp 14 c03b1630:   ebf19c12    bl c0018680 <v7_early_abort> 15 c03b1634:   ea000086    b c03b1854 <ret_from_exception> 16 c03b1638:   e320f000    nop {0} 17 c03b163c:   e320f000    nop {0}

 

v7_early_abort()定義見defined in arch/arm/mm/abort-ev7.S, 該文件只定義了這一個函數. 上面對應的pabt的處理例程是v7_pabort()(defined in arch/arm/mm/pabort-v7.S).架構

 1 .align 5
 2 ENTRY(v7_early_abort)  3     clrex  4     mrc p15, 0, r1, c5, c0, 0
 5     mrc p15, 0, r0, c6, c0, 0
 6 #if defined(CONFIG_VERIFY_PERMISSION_FAULT)  7     ldr r3, =0x40d  8     and r3, r1, r3  9     cmp r3, #0x0d 10     bne do_DataAbort 11     mcr p15, 0, r0, c7, c8, 0
12     isb 13     mrc p15, 0, ip, c7, c4, 0
14     and r3, ip, #0x7b 15     cmp r3, #0x0b 16     bne do_DataAbort 17     bic r1, r1, #0xf 18     and ip, ip, #0x7e 19     orr r1, r1, ip, LSR #1
20 #endif 21     b do_DataAbort 22 ENDPROC(v7_early_abort) 23 .align 5
24 ENTRY(v7_pabort) 25     mrc p15, 0, r0, c6, c0, 2
26     mrc p15, 0, r1, c5, c0, 1
27     b do_PrefetchAbort 28 ENDPROC(v7_pabort)

 

v7_early_abort與v7_pabort處理流程基本一致, 都是讀FAR與FSR並調用對應的異常處理程序, 區別在於如下幾點. 在獨佔訪問時發生dabt其狀態是不可預測的, 須要調用clrex指令清除本地記錄, 另外訪問協處理器的指令也稍稍有所區別, cp15的c5是fault status register, c6是fault address register(見ARM ref manual Part B 3.7章), FSR值保存在r1中, FAR值保存在r0中. 咱們來看下do_DataAbort()/do_PrefetchAbort()(defined in arch/arm/mm/fault.c)的實現.app

 1 asmlinkage void __exception do_DataAbort(unsigned long addr, \  2     unsigned int fsr, struct pt_regs *regs)  3 {  4     const struct fsr_info *inf = fsr_info + fsr_fs(fsr);  5     struct siginfo info;  6     if (!inf->fn(addr, fsr & ~FSR_LNX_PF, regs))  7         return;  8     printk(KERN_ALERT "Unhandled fault: %s (0x%03x) at 0x%08lx\n", inf->name, fsr, addr);  9     info.si_signo = inf->sig; 10     info.si_errno = 0; 11     info.si_code  = inf->code; 12     info.si_addr  = (void __user *)addr; 13     arm_notify_die("", regs, &info, fsr, 0); 14 } 15 asmlinkage void __exception do_PrefetchAbort(unsigned long addr, \ 16     unsigned int ifsr, struct pt_regs *regs) 17 { 18     const struct fsr_info *inf = ifsr_info + fsr_fs(ifsr); 19     struct siginfo info; 20     if (!inf->fn(addr, ifsr | FSR_LNX_PF, regs)) 21         return; 22     printk(KERN_ALERT "Unhandled prefetch abort: %s (0x%03x) at 0x%08lx\n", inf->name, ifsr, addr); 23     info.si_signo = inf->sig; 24     info.si_errno = 0; 25     info.si_code  = inf->code; 26     info.si_addr  = (void __user *)addr; 27     arm_notify_die("", regs, &info, ifsr, 0); 28 }

 

兩個函數依然相似, 都是根據fsr的狀態選擇執行對應的異常回調, 如處理失敗再發送信號. 區別在於回調函數的不一樣. fsr_info(defined in arch/arm/mm/fsr-2level.c)是異常處理例程分發數組. fsr_fs()(defined in arch/arm/mm/fault.h)宏用於獲取fsr中特定位. 根據ARM ref manual描述fsr的第四位指定了fault source, 這裏或上第10位的理由在fsr_info數組中有說明: 部分架構支持第10位做爲異常類型位.函數

 1 #ifdef CONFIG_ARM_LPAE  2 static inline int fsr_fs(unsigned int fsr)  3 {  4     return fsr & FSR_FS5_0;  5 }  6 #else
 7 static inline int fsr_fs(unsigned int fsr)  8 {  9     return (fsr & FSR_FS3_0) | (fsr & FSR_FS4) >> 6; 10 } 11 #endif

 

咱們來看下fsr_info(defined in arch/arm/mm/fault.c)結構: fn爲異常處理回調, sig爲須要發送給觸發異常的task的信號, code爲該信號的碼字, name爲描述異常類型的字符串. 對比ARM ref manual Part B 3.6.1章能夠發現該是按異常類型排布的.

 1 struct fsr_info {  2     int (*fn)(unsigned long addr, unsigned int fsr, struct pt_regs *regs);  3     int sig;  4     int code;  5     const char *name;  6 };  7 #ifdef CONFIG_ARM_LPAE  8 #include "fsr-3level.c"
 9 #else
10 #include "fsr-2level.c"
11 #endif

 

咱們仍以分配內存流程爲例, 當咱們訪問一個還沒有分配物理頁(但已被brk或mmap映射)的虛擬地址時MMU會因沒法翻譯該地址而報錯page translation fault, 此時會調用對應的回調do_page_fault()(defined in arch/arm/mm/fault.c), 咱們來看下這個接口作了什麼(終於進入正題了).

 1 static int __kprobes do_page_fault(unsigned long addr, \  2     unsigned int fsr, struct pt_regs *regs)  3 {  4     struct task_struct *tsk;  5     struct mm_struct *mm;  6     int fault, sig, code;  7     int write = fsr & FSR_WRITE;  8     unsigned int flags = FAULT_FLAG_ALLOW_RETRY | \  9         FAULT_FLAG_KILLABLE | (write  FAULT_FLAG_WRITE : 0); 10     if (notify_page_fault(regs, fsr)) 11         return 0; 12     tsk = current; 13     mm  = tsk->mm; 14     //若是觸發異常的環境中使能中斷則如今也使能中斷
15     if (interrupts_enabled(regs)) 16         local_irq_enable(); 17     //線程禁止搶佔, 禁止中斷或沒有用戶上下文(內核線程)三種狀況跳轉no_context
18     if (in_atomic() || irqs_disabled() || !mm) 19         goto no_context; 20     /** 21      * 早期x86在此處會死鎖, 但內核引入異常地址跳轉表後能夠檢測是否爲安全引用地址 22      * 23     **/
24     if (!down_read_trylock(&mm->mmap_sem)) { 25         /** 26          * 前面的檢測已保證當前線程爲用戶態線程, 那麼檢測寄存器是否處於特權模式 27          * 若是是特權模式且當前地址在__ex_table段中找不到則走入no_context 28          * __ex_table是成對的地址, 其具體定義與做用見下文分析 29          * 30         **/
31         if (!user_mode(regs) && !search_exception_tables(regs->ARM_pc)) 32             goto no_context; 33 retry: 34         down_read(&mm->mmap_sem); 35     } else { 36         might_sleep(); 37 #ifdef CONFIG_DEBUG_VM 38         if (!user_mode(regs) && !search_exception_tables(regs->ARM_pc)) 39             goto no_context; 40 #endif
41     } 42     fault = __do_page_fault(mm, addr, fsr, flags, tsk); 43     /** 44      * 若是返回retry但已有信號掛起則先處理信號 45      * 無需釋放信號因其已在__lock_page_or_retry中釋放 46      * 47     **/
48     if ((fault & VM_FAULT_RETRY) && fatal_signal_pending(current)) 49         return 0; 50     perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS, 1, regs, addr); 51     if (!(fault & VM_FAULT_ERROR) && flags & FAULT_FLAG_ALLOW_RETRY) { 52         if (fault & VM_FAULT_MAJOR) { 53             tsk->maj_flt++; 54             perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MAJ, 1, regs, addr); 55         } else { 56             tsk->min_flt++; 57             perf_sw_event(PERF_COUNT_SW_PAGE_FAULTS_MIN, 1, regs, addr); 58         } 59         if (fault & VM_FAULT_RETRY) { 60             flags &= ~FAULT_FLAG_ALLOW_RETRY; 61             flags |= FAULT_FLAG_TRIED; 62             goto retry; 63         } 64     } 65     up_read(&mm->mmap_sem); 66     if (likely(!(fault & (VM_FAULT_ERROR | VM_FAULT_BADMAP | VM_FAULT_BADACCESS)))) 67         return 0; 68     //OOM
69     if (fault & VM_FAULT_OOM) { 70         pagefault_out_of_memory(); 71         return 0; 72     } 73     if (!user_mode(regs)) 74         goto no_context; 75     if (fault & VM_FAULT_SIGBUS) { 76         //內存有餘但不能修復缺頁異常
77         sig = SIGBUS; 78         code = BUS_ADRERR; 79     } else { 80         //訪問未映射的內存
81         sig = SIGSEGV; 82         code = fault == VM_FAULT_BADACCESS  SEGV_ACCERR : SEGV_MAPERR; 83     } 84     __do_user_fault(tsk, addr, fsr, sig, code, regs); 85     return 0; 86 no_context: 87     __do_kernel_fault(mm, addr, fsr, regs); 88     return 0; 89 }

 

若是是內核線程或關搶佔或關中斷的狀況下, 直接調用__do_kernel_fault()(defined in arch/arm/mm/fault.c)報錯oops退出.

 1 static void __do_kernel_fault(struct mm_struct *mm, \  2     unsigned long addr, unsigned int fsr, struct pt_regs *regs)  3 {  4     //跳過異常指令
 5     if (fixup_exception(regs))  6         return;  7     bust_spinlocks(1);  8     printk(KERN_ALERT "Unable to handle kernel %s at virtual address %08lx\n", \  9         (addr < PAGE_SIZE)  "NULL pointer dereference" : "paging request", addr); 10     show_pte(mm, addr); 11     die("Oops", regs, fsr); 12     bust_spinlocks(0); 13     do_exit(SIGKILL); 14 }

 

fixup_exception()(defined in arch/arm/mm/extable.c)試圖經過查找內核預先設置的保護點來跳過引起異常的指令, 讓內核繼續正常執行. 其中search_exception_tables()(defined in kernel/extable.c)做用是查找內核預先設置的異常指令保護點, 這些異常指令保護點都存放在[__start___ex_table, __stop___ex_table], 該地址寫入lds腳本中, 中間存放的是__ex_table段, 咱們來找下該段的使用.

1 #define USER(x...)                  \ 2 9999: x;                            \
3     .pushsection __ex_table,"a";    \
4     .align 3;                       \
5     .long 9999b,9001f;              \
6     .popsection

 

內核代碼中有多處使用該段的宏定義, 限於篇幅咱們僅看下USER()(defined in arch/arm/include/asm/assembler.h)宏, 該宏首先執行了參數x, 而後定義了以後內容爲__ex_table段內容(.pushsection與.popsection做用爲將二者間的指令加入指定段而非放入當前代碼段/數據段), 該段定義了兩個long型空間, 分別用於保存以前的9999標籤與以後的9001標籤(b=backward f=forward)的地址. 最近的9999b即參數x, 9001f則依賴該宏使用的地方, 咱們來看下該宏的使用.

 1 .Lc2u_dest_not_aligned:
 2     rsb ip, ip, #4
 3     cmp ip, #2
 4     ldrb r3, [r1], #1
 5     USER(TUSER( strb) r3, [r0], #1) @ May fault  6     ldrgeb r3, [r1], #1
 7     USER(TUSER( strgeb) r3, [r0], #1) @ May fault  8     ldrgtb r3, [r1], #1
 9     USER(TUSER( strgtb) r3, [r0], #1) @ May fault 10     sub r2, r2, ip 11     b .Lc2u_dest_aligned 12 .pushsection .fixup,"ax"
13 .align 0
14 9001: ldmfd sp!, {r0, r4 - r7, pc} 15 .popsection

 

在arch/arm/lib/uaccess.S中有多個使用該宏的彙編, 這些宏都用於實現__copy_to_user()/__copy_from_user(), 注意在函數定義尾部有定義9001標籤, 該標籤訂義在fixup段中.
至此咱們已經能夠得出__ex_table的做用了, 它是內核爲防止某些關鍵路徑上訪問出錯致使oops的一種補救手段, __ex_table中每兩個地址組成一個異常指令表項, 其中前者是可能觸發異常的指令, 後者是補救指令. 讓咱們回到fixup_exception()看看它是如何使用的. search_exception_tables()經過二分查找找到異常指令對(內核假定該數組已通過排序, 如何排序的沒找到), 將pc修改成補救指令地址. 若是能經過這種方式避免oops是最好的, 然而不是全部指令都能這麼操做, 當內核找不到對應的異常指令時只有選擇oops. 此時內核會先調用show_pte()打印出錯地址所在頁表信息, 調用die()打印堆棧與寄存器信息(oops打印大部分出於此)並調用通知鏈回調, 最後不管是否出錯都會調用do_exit()退出.
回到do_page_fault(), 若是非內核線程或中斷引發的缺頁異常, 則走入__do_page_fault()(defined in arch/arm/mm/fault.c)流程.

 1 static int __kprobes __do_page_fault(struct mm_struct *mm, \  2     unsigned long addr, unsigned int fsr, unsigned int flags, struct task_struct *tsk)  3 {  4     struct vm_area_struct *vma;  5     int fault;  6     /**  7      * 判斷該地址是否屬於已被分配的虛擬內存區間  8      * 若是不在進程vm管理區間內則直接返回錯誤VM_FAULT_BADMAP  9      * 不然判斷是否屬於棧空間, 對於棧空間會額外判斷是否下溢是否可擴展 10      * 非棧空間直接走handle_mm_fault分支 11      * 12     **/
13     vma = find_vma(mm, addr); 14     fault = VM_FAULT_BADMAP; 15     if (unlikely(!vma)) 16         goto out; 17     if (unlikely(vma->vm_start > addr)) 18         goto check_stack; 19 good_area: 20     //判斷讀寫權限是否一致
21     if (access_error(fsr, vma)) { 22         fault = VM_FAULT_BADACCESS; 23         goto out; 24     } 25     return handle_mm_fault(mm, vma, addr & PAGE_MASK, flags); 26 check_stack: 27     if (vma->vm_flags & VM_GROWSDOWN && \ 28         addr >= FIRST_USER_ADDRESS && !expand_stack(vma, addr)) 29         goto good_area; 30 out: 31     return fault; 32 }

 

__do_page_fault()首先判斷該虛擬地址是否合法, 而後判斷該區間屬於棧空間仍是其它空間. 對於棧空間首要保證是不能下溢到FIRST_USER_ADDRESS, 此外還會調用expand_stack()判斷是否棧地址可擴展. 對於合法可擴展的vma地址還要調用access_error()判斷讀寫權限是否一致, 若是vma只讀但異常是寫操做則依然返回錯誤VM_FAULT_BADACCESS. 最後才調用handle_mm_fault()(defined in mm/memory.c).

 1 int handle_mm_fault(struct mm_struct *mm, \  2     struct vm_area_struct *vma, unsigned long address, unsigned int flags)  3 {  4     pgd_t *pgd;  5     pud_t *pud;  6     pmd_t *pmd;  7     pte_t *pte;  8     //設置進程狀態的緣由?
 9     __set_current_state(TASK_RUNNING); 10     count_vm_event(PGFAULT); 11     mem_cgroup_count_vm_event(mm, PGFAULT); 12     check_sync_rss_stat(current); 13     if (unlikely(is_vm_hugetlb_page(vma))) 14         return hugetlb_fault(mm, vma, address, flags); 15 retry: 16     pgd = pgd_offset(mm, address); 17     pud = pud_alloc(mm, pgd, address); 18     if (!pud) 19         return VM_FAULT_OOM; 20     pmd = pmd_alloc(mm, pud, address); 21     if (!pmd) 22         return VM_FAULT_OOM; 23     //huge page, 略過
24     if (pmd_none(*pmd) && transparent_hugepage_enabled(vma)) { 25         if (!vma->vm_ops) 26             return do_huge_pmd_anonymous_page(mm, vma, address, pmd, flags); 27     } else { 28         pmd_t orig_pmd = *pmd; 29         int ret; 30         barrier(); 31         if (pmd_trans_huge(orig_pmd)) { 32             unsigned int dirty = flags & FAULT_FLAG_WRITE; 33             if (pmd_trans_splitting(orig_pmd)) 34                 return 0; 35             if (pmd_numa(orig_pmd)) 36                 return do_huge_pmd_numa_page(mm, vma, address, orig_pmd, pmd); 37             if (dirty && !pmd_write(orig_pmd)) { 38                 ret = do_huge_pmd_wp_page(mm, vma, address, pmd, orig_pmd); 39                 if (unlikely(ret & VM_FAULT_OOM)) 40                     goto retry; 41                 return ret; 42             } else { 43                 huge_pmd_set_accessed(mm, vma, address, pmd, orig_pmd, dirty); 44             } 45             return 0; 46         } 47     } 48     if (pmd_numa(*pmd)) 49         return do_pmd_numa_page(mm, vma, address, pmd); 50     if (unlikely(pmd_none(*pmd)) && unlikely(__pte_alloc(mm, vma, pmd, address))) 51         return VM_FAULT_OOM; 52     if (unlikely(pmd_trans_huge(*pmd))) 53         return 0; 54     pte = pte_offset_map(pmd, address); 55     return handle_pte_fault(mm, vma, address, pte, pmd, flags); 56 }

  

handle_mm_fault()看似很複雜, 但大部分代碼與HUGEPAGE有關能夠先忽略. 在二級頁表架構下實際經過pgd_offset()獲取pgd, 再經過__pte_alloc()(defined in mm/memory.c)分配pte, 最後調用handle_pte_fault分配物理頁(注意前者分配物理頁用於存儲二級頁表pte, 後者分配物理頁纔是真正用於請求物理地址的頁).

至此缺頁異常的處理流程基本告一段落. 再進一步分析代碼前讓咱們先來看看幾個基本概念. 再次強調如下分析基於hi3536(32bit ARMv7架構)芯片, 使能MMU與HIGHMEM, 未使能SPARSEMEM, HUGEPAGE與NUMA.

1. 內核內存管理代碼都在mm/目錄下, 但對應特定架構的實如今arch/$(ARCH)/mm/目錄下, 其中$(ARCH)在本文中即arm, 其實在前面咱們就已經看到arch目錄下的函數了.

2. 在前文咱們提到過內核的地址空間分離策略(內核佔據共用的高地址空間, 進程使用獨立的低地址空間), 其中低地址空間的物理內存映射是非惟一的而高地址空間的物理內存映射是惟一的. 然而在某些狀況下內核須要較多的物理內存, 而一對一的映射致使內核總物理內存是給定的有限值, 爲此內核引入了高端內存(highmem)與低端內存(lowmem)的概念. 後者爲一般的內核的地址空間, 採用線性映射的方式, 前者則根據須要映射不一樣(總線地址)的物理內存. 高端內存的大小是可修改的, 其值影響系統的性能, 若是設置的過小而內核又常常訪問非線性映射區域會致使頻繁的頁映射, 若是設置的太大則又減小了內核線性地址空間. 在後文中咱們會看到如何經過修改vmalloc_min來修改高端內存大小. 此處咱們先來看下內核中物理內存, (內核低端內存的)虛擬地址與頁框號的映射關係爲理解代碼作好鋪墊.

物理地址與(內核低端內存的)虛擬地址的映射宏爲__virt_to_phys/__phys_to_virt(defined in arch/arm/include/asm/memory.h), 這兩個宏僅限於該文件內部使用, 外部使用必須使用它們的封裝接口(以保證正確的包含了必要的頭文件): virt_to_phys/phys_to_virt/__pa/__va.

1 #define __virt_to_phys(x) ((x) - PAGE_OFFSET + PHYS_OFFSET)
2 #define __phys_to_virt(x) ((x) - PHYS_OFFSET + PAGE_OFFSET)

 

其中PHYS_OFFSET(defined in arch/arm/include/asm/memory.h)做用是獲取物理地址的起始偏移. 在本文中定義爲PLAT_PHYS_OFFSET(defined in arch/arm/mach-hi3536/include/mach/memory.h), 其值爲UL(0x40000000), 即DDRM的總線地址. PAGE_OFFSET(defined in arch/arm/include/asm/memory.h)定義爲CONFIG_PAGE_OFFSET(defined in arch/arm/Kconfig), 該值根據內核態與用戶態地址空間劃分決定, 默認爲0xC0000000(即1:3劃分). 因而可知內核虛擬地址與物理地址是一對一線性映射的.

物理地址與頁框號(PFN, page frame number)的映射宏爲__phys_to_pfn()/__pfn_to_phys()(defined in arch/arm/include/asm/memory.h). 頁框號即物理地址所在頁的序號, 經過對物理地址位移一個頁大小獲得.

1 #define __phys_to_pfn(paddr)    ((unsigned long)((paddr) >> PAGE_SHIFT))
2 #define __pfn_to_phys(pfn)      ((phys_addr_t)(pfn) << PAGE_SHIFT)

 

頁框號與頁結構指針的映射宏爲page_to_pfn/pfn_to_page(defined in include/asm-generic/memory_model.h). 其中宏ARCH_PFN_OFFSET定義爲宏PHYS_PFN_OFFSET, (PHYS_OFFSET >> PAGE_SHIFT), 即物理內存地址的頁偏移. mem_map(defined in mm/memory.c)爲全局變量, 指向頁結構數組的起始地址, 該值在bootmem_init()中初始化, 咱們在後文中會詳細分析.

1 #define __pfn_to_page(pfn)  (mem_map + ((pfn) - ARCH_PFN_OFFSET))
2 #define __page_to_pfn(page) ((unsigned long)((page) - mem_map) + ARCH_PFN_OFFSET)
3 #define page_to_pfn __page_to_pfn
4 #define pfn_to_page __pfn_to_page

 

3. 咱們知道內核一直採用分頁管理內存, 從早期的三級頁表(PGD->PMD->PTE)到四級頁表(PGD->PUD->PMD->PTE), 最近的內核已經開始支持五級頁表. 可是對於某個特定的架構並無必要提供這麼多級頁表的支持, 以本文爲例, 32bit總線架構最大支持4GB尋址, 二級頁表足以知足需求, 即便在支持LPAE的架構下也只需三級頁表. 所以內核在各個架構目錄下定義了一套頭文件用於頁表映射, 在本文中爲arch/arm/include/asm/pgtable.h. 該頭文件又包含了如下頭文件:

1 #include <asm-generic/pgtable-nopud.h>
2 #include <asm/memory.h>
3 #include <asm/pgtable-hwdef.h>
4 #ifdef CONFIG_ARM_LPAE 5 #include <asm/pgtable-3level.h>
6 #else
7 #include <asm/pgtable-2level.h>
8 #endif

 

pgtable-nopud.h包含了無PUD時對應宏與常量的映射, 根據是否開啓LPAE(large physical address extension)選擇包含pgtable-2level.h/pgtable-3level.h.
這裏補充下根據ARM ref manual, ARM架構的硬件設計是使用二級頁表. 其中第一級包含4096個表項, 第二級包含256個表項, 其中每一個表項爲長度32bit的字, 所以第一級頁表需佔用16KB空間. 而(早期的)內核使用三級頁表, 雖然(經過僅使用PGD與PTE)三級頁表能夠被簡單的包裝成二級頁表, 但內核同時指望一個PTE對應一個頁, 且包含一個dirty位. 所以內核將實現稍稍扭曲一下: 第一級頁表包含2048個表項, 每一個表項8字節(第二級有兩個硬件指針, 即每一個二級頁表對應兩個一級頁表的表項), 第二級包含兩個連續的硬件PTE表, 每一個硬件PTE包含256個表項, 一共512個表項. 此時第二級表使用了512*4=2048字節空間, 剩餘的一半正好用於內核本身的PTE, 即第一級頁表指向的4K的頁中前半部分爲內核PTE, 後半部分爲硬件PTE. 這麼設計的理由如前所述, 一來第一級頁表指向的4K頁能夠被完整的填充, 二來能夠在同一頁中同時維護硬件PTE與內核PTE(硬件PTE無accessed位與dirty位, 需內核PTE來維護). 二級頁表能夠尋址4G空間(2048*512*4096). 在支持LPAE時則使用三級頁表, 三級頁表的實現與二級頁表相似, 區別在於第一級頁表使用512個8字節的表項, 第二級頁表與第三極頁表與二級頁表中的第二級頁表相同. 所以三級頁表能夠尋址512G空間(512*512*512*4096).

還要注意的是在以上頭文件中以L_PTE_xxx開頭的宏是用於判斷內核PTE的宏, 而以PTE_xxx開頭的宏是用於判斷硬件PTE的宏. 對二級頁表而言以PMD_xxx開頭的宏即指向第一級頁表PGD. 其中dirty位在授予硬件寫權限時置位, 即向一個乾淨的頁執行寫操做會觸發permission fault, 且內核內存管理層會在handle_pte_fault()時將該頁標記爲dirty, 爲了使硬件知悉權限改變, 必需要刷新TLB, 這是在ptep_set_access_flags()中完成的. accessed位與young位也採用相似的方式, 咱們僅在young位置位時容許訪問該頁, 訪問該頁時會觸發fault, 而handle_pte_fault()會置位young位只要該頁在內核PTE表項中被標記爲存在, 而ptep_set_access_flags()會保證TLB的更新. 可是當young位被清除時咱們不會清除硬件PTE, 當前內核在這種狀況下不會刷新TLB, 這意味着TLB會一直保存翻譯緩衝直到TLB因爲壓力而主動驅逐或進程上下文切換修改用戶空間映射.

讓咱們經過代碼來理解頁表數據結構的轉換關係, 首先看下如何經過虛擬地址獲取所在頁的PGD, PUD與PMD.

 1 //defined in arch/arm/include/asm/pgtable-2level.h
 2 #define PGDIR_SHIFT 21
 3 //defined in arch/arm/include/asm/pgtable.h
 4 #define pgd_index(addr)         ((addr) >> PGDIR_SHIFT)
 5 #define pgd_offset(mm, addr)    ((mm)->pgd + pgd_index(addr))
 6 #define pgd_offset_k(addr)      pgd_offset(&init_mm, addr)
 7 //defined in arch/arm/mm/mm.h
 8 static inline pmd_t *pmd_off_k(unsigned long virt)  9 { 10     return pmd_offset(pud_offset(pgd_offset_k(virt), virt), virt); 11 } 12 //defined in arch/arm/include/asm/pgtable-2level.h
13 static inline pmd_t *pmd_offset(pud_t *pud, unsigned long addr) 14 { 15     return (pmd_t *)pud; 16 } 17 //defined in include/asm-generic/pgtable-nopud.h
18 static inline pud_t *pud_offset(pgd_t * pgd, unsigned long address) 19 { 20     return (pud_t *)pgd; 21 }

 

pgd_offset_k()宏是內核獲取內核態虛擬地址所在PGD的接口, 它經過全局變量init_mm.pgd加上虛擬地址右移21位獲得(內核PGD是2048個, 對應地址的高11位). pmd_off_k()是內核態獲取pmd偏移的內聯函數, 它經過傳入的虛擬地址virt計算所在pgd進而獲得pud與pmd, 從代碼中看出在二級頁表結構下pgd=pud=pmd. 關於init_mm(defined in mm/init-mm.c)後文還會詳述, 此處稍稍說起一下. 其pgd成員爲靜態編譯時肯定了地址的swapper_pg_dir(defined in arch/arm/kernel/head.S).

 1 #define KERNEL_RAM_VADDR (PAGE_OFFSET + TEXT_OFFSET)
 2 #ifdef CONFIG_ARM_LPAE  3 #define PG_DIR_SIZE 0x5000
 4 #define PMD_ORDER   3
 5 #else
 6 #define PG_DIR_SIZE 0x4000
 7 #define PMD_ORDER   2
 8 #endif
 9 .globl  swapper_pg_dir 10 .equ    swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE

 

其中TEXT_OFFSET(defined in arch/arm/Makefile)是編譯腳本指定的宏, 其值定義爲$(textofs-y), textofs-y(defined in arch/arm/Makefile)定義爲0x00008000. 故kernel在內核中的起始地址KERNEL_RAM_VADDR前16K/20K(對於二級頁表結構須要2048*8=0x4000個字節存儲PGD, 對於三級頁表結構每一個PMD須要一個頁加上PGD自己一共須要5個頁). 注意TEXT_OFFSET的設置必然要大於這些偏移的長度, 不然內核訪問會下溢到用戶空間. swapper_pg_dir(declared in arch/arm/include/asm/pgtable.h)對外的聲明是個pgd_t數組, 其長度爲2048(三級頁表結構下長度爲4).

4. 更多的ARM內存模型可參考Documentation/arm/memory.txt中的敘述.

5. 關於TLB的操做, 待補充.

關於物理頁與虛擬頁的關係咱們先分析到這裏, 讓咱們回到handle_mm_fault(). pgd_offset()用於獲取給定address對應的pgd, pud_alloc()/pmd_alloc()見include/asm-generic/4level-fixup.h, 該文件爲對內核通用4級頁表與實際架構使用的二級頁表的適配, 從前文分析可知在二級頁表下其結果均指向pgd(pud直接等於pgd實現四級頁錶轉換爲三級頁表, 在不支持LAPE狀況下pmd表項只有一項, 即等於pgd), 若是保存進程pte的頁表未分配則需調用__pte_alloc()(defined in mm/memory.c)分配pte. 略過HUGEPAGE與NUMA的部分, 咱們先來看看__pte_alloc().

 1 int __pte_alloc(struct mm_struct *mm, \  2     struct vm_area_struct *vma, pmd_t *pmd, unsigned long address)  3 {  4     //分配pte
 5     pgtable_t new = pte_alloc_one(mm, address);  6     int wait_split_huge_page;  7     if (!new)  8         return -ENOMEM;  9     //寫同步, 目的是保證pte的初始化在該pte對全部cpu可見前已完成
10     smp_wmb(); 11     //加入進程頁表管理
12     spin_lock(&mm->page_table_lock); 13     wait_split_huge_page = 0; 14     if (likely(pmd_none(*pmd))) { 15         mm->nr_ptes++; 16         pmd_populate(mm, pmd, new); 17         new = NULL; 18     } else if (unlikely(pmd_trans_splitting(*pmd))) 19         wait_split_huge_page = 1; 20     spin_unlock(&mm->page_table_lock); 21     if (new) 22         pte_free(mm, new); 23     if (wait_split_huge_page) 24         wait_split_huge_page(vma->anon_vma, pmd); 25     return 0; 26 }

 

pte_alloc_one()(defined in arch/arm/include/asm/pgalloc.h)會調用alloc_pages()(defined in include/linux/gfp.h)返回一個頁表結構體, 在無NUMA時最終調用__alloc_pages_nodemask()(defined in mm/page_alloc.c), __alloc_pages_nodemask()又會調用get_page_from_freelist()(defined in mm/page_alloc.c).

 1 __alloc_pages_nodemask(gfp_t gfp_mask, unsigned int order, \  2     struct zonelist *zonelist, nodemask_t *nodemask)  3 {  4     enum zone_type high_zoneidx = gfp_zone(gfp_mask);  5     struct zone *preferred_zone;  6     struct page *page = NULL;  7     int migratetype = allocflags_to_migratetype(gfp_mask);  8     unsigned int cpuset_mems_cookie;  9     int alloc_flags = ALLOC_WMARK_LOW|ALLOC_CPUSET; 10     struct mem_cgroup *memcg = NULL; 11     gfp_mask &= gfp_allowed_mask; 12     lockdep_trace_alloc(gfp_mask); 13     might_sleep_if(gfp_mask & __GFP_WAIT); 14     if (should_fail_alloc_page(gfp_mask, order)) 15         return NULL; 16     if (unlikely(!zonelist->_zonerefs->zone)) 17         return NULL; 18     if (!memcg_kmem_newpage_charge(gfp_mask, &memcg, order)) 19         return NULL; 20 retry_cpuset: 21     cpuset_mems_cookie = get_mems_allowed(); 22     first_zones_zonelist(zonelist, high_zoneidx, \ 23         nodemask  : &cpuset_current_mems_allowed, &preferred_zone); 24     if (!preferred_zone) 25         goto out; 26 #ifdef CONFIG_CMA 27     if (allocflags_to_migratetype(gfp_mask) == MIGRATE_MOVABLE) 28         alloc_flags |= ALLOC_CMA; 29 #endif
30     page = get_page_from_freelist(gfp_mask|__GFP_HARDWALL, nodemask, order, \ 31         zonelist, high_zoneidx, alloc_flags, preferred_zone, migratetype); 32     if (unlikely(!page)) { 33         gfp_mask = memalloc_noio_flags(gfp_mask); 34         page = __alloc_pages_slowpath(gfp_mask, order, zonelist, \ 35             high_zoneidx, nodemask, preferred_zone, migratetype); 36     } 37     trace_mm_page_alloc(page, order, gfp_mask, migratetype); 38 out: 39     if (unlikely(!put_mems_allowed(cpuset_mems_cookie) && !page)) 40         goto retry_cpuset; 41     memcg_kmem_commit_charge(page, memcg, order); 42     return page; 43 }

 

獲取空閒頁後還要將其加入進程管理, 若是pmd指向地址爲空說明未分配過頁, pmd_populate()(defined in arch/arm/include/asm/pgalloc.h)調用__pmd_populate()(defined in arch/arm/include/asm/pgalloc.h)將頁表地址與頁標記錄入其中並刷新TLB.

 1 static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte, pmdval_t prot)  2 {  3     pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;  4     pmdp[0] = __pmd(pmdval);  5 #ifndef CONFIG_ARM_LPAE  6     pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));  7 #endif
 8     flush_pmd_entry(pmdp);  9 } 10 static inline void pmd_populate(struct mm_struct *mm, pmd_t *pmdp, pgtable_t ptep) 11 { 12     __pmd_populate(pmdp, page_to_phys(ptep), _PAGE_USER_TABLE); 13 }

 

若是pte存在或調用__pte_alloc()獲取新的pte後再調用handle_pte_fault()(defined in mm/memory.c)來分配實際物理頁. 根據vma類型不一樣, 實際又調用不一樣的接口函數, 此處咱們僅分析匿名映射分配接口do_anonymous_page()(defined in mm/memory.c).

 1 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)  4 {  5     pte_t entry;  6     spinlock_t *ptl;  7     entry = *pte;  8     if (!pte_present(entry)) {  9         if (pte_none(entry)) { 10             //文件映射
11             if (vma->vm_ops) { 12                 if (likely(vma->vm_ops->fault)) 13                     return do_linear_fault(mm, vma, address, pte, pmd, flags, entry); 14             } 15             //匿名映射
16             return do_anonymous_page(mm, vma, address, pte, pmd, flags); 17         } 18         if (pte_file(entry)) 19             return do_nonlinear_fault(mm, vma, address, pte, pmd, flags, entry); 20         return do_swap_page(mm, vma, address, pte, pmd, flags, entry); 21     } 22     //NUMA, 略過
23     if (pte_numa(entry)) 24         return do_numa_page(mm, vma, address, entry, pte, pmd); 25     ptl = pte_lockptr(mm, pmd); 26     spin_lock(ptl); 27     if (unlikely(!pte_same(*pte, entry))) 28         goto unlock; 29     if (flags & FAULT_FLAG_WRITE) { 30         if (!pte_write(entry)) 31             return do_wp_page(mm, vma, address, pte, pmd, ptl, entry); 32         entry = pte_mkdirty(entry); 33     } 34     entry = pte_mkyoung(entry); 35     if (ptep_set_access_flags(vma, address, pte, entry, flags & FAULT_FLAG_WRITE)) { 36         update_mmu_cache(vma, address, pte); 37     } else { 38         if (flags & FAULT_FLAG_WRITE) 39             flush_tlb_fix_spurious_fault(vma, address); 40     } 41 unlock: 42     pte_unmap_unlock(pte, ptl); 43     return 0; 44 }

 

do_anonymous_page()調用alloc_zeroed_user_highpage_movable()(defined in include/linux/highmem.h), 後者最終一樣調用__alloc_pages_nodemask()分配物理頁. 在獲取物理頁後還要經過set_pte_at()(defined in arch/arm/include/asm/pgtable.h)賦值pte.

 1 static int do_anonymous_page(struct mm_struct *mm, \  2     struct vm_area_struct *vma, unsigned long address, \  3     pte_t *page_table, pmd_t *pmd, unsigned int flags)  4 {  5     struct page *page;  6     spinlock_t *ptl;  7     pte_t entry;  8     //未設置HIGHPTE, 略過
 9     pte_unmap(page_table); 10     //若是地址屬於棧空間則擴展對應棧的虛擬地址
11     if (check_stack_guard_page(vma, address) < 0) 12         return VM_FAULT_SIGBUS; 13     //只讀訪問
14     if (!(flags & FAULT_FLAG_WRITE)) { 15         entry = pte_mkspecial(pfn_pte(my_zero_pfn(address), vma->vm_page_prot)); 16         page_table = pte_offset_map_lock(mm, pmd, address, &ptl); 17         if (!pte_none(*page_table)) 18             goto unlock; 19         goto setpte; 20     } 21     //記錄anon_vma映射
22     if (unlikely(anon_vma_prepare(vma))) 23         goto oom; 24     //分配物理頁
25     page = alloc_zeroed_user_highpage_movable(vma, address); 26     if (!page) 27         goto oom; 28     __SetPageUptodate(page); 29     if (mem_cgroup_newpage_charge(page, mm, GFP_KERNEL)) 30         goto oom_free_page; 31     entry = mk_pte(page, vma->vm_page_prot); 32     if (vma->vm_flags & VM_WRITE) 33         entry = pte_mkwrite(pte_mkdirty(entry)); 34     page_table = pte_offset_map_lock(mm, pmd, address, &ptl); 35     if (!pte_none(*page_table)) 36         goto release; 37     //記錄進程匿名頁分配
38     inc_mm_counter_fast(mm, MM_ANONPAGES); 39     page_add_new_anon_rmap(page, vma, address); 40 setpte: 41     //設置進程頁表項
42     set_pte_at(mm, address, page_table, entry); 43     //ARMv6後架構在set_pte_at()中已完成更新cache操做
44     update_mmu_cache(vma, address, page_table); 45 unlock: 46     pte_unmap_unlock(page_table, ptl); 47     return 0; 48 release: 49     mem_cgroup_uncharge_page(page); 50     page_cache_release(page); 51     goto unlock; 52 oom_free_page: 53     page_cache_release(page); 54 oom: 55     return VM_FAULT_OOM; 56 }

 

至此內核物理頁分配的流程大體理清了, 其中關於內核是如何管理物理頁的咱們留到後面再分析. 這裏再討論下物理內存的初始化流程.
讓咱們先思考一個問題, 若是物理頁與虛擬地址的映射不是固定的, 那麼理論上執行映射的代碼自己所在的物理頁也會被重映射, 因此一定須要一段固定映射的內存(低端內存)來保存內核關鍵代碼與數據結構(好比一級頁表). 如何實現內存的線性映射, 讓咱們看看內存在內核中是如何初始化的.

在內核啓動時執行的第一個C函數start_kernel()(defined in init/main.c)中會執行一系列初始化, 其中就包括調用setup_arch()(defined in arch/arm/kernel/setup.c)執行架構與內存相關初始化.

 1 void __init setup_arch(char **cmdline_p)  2 {  3     ......  4     //init_mm(defined in mm/init-mm.c)是全局變量, 用於內核自身的內存管理
 5     init_mm.start_code = (unsigned long) _text;  6     init_mm.end_code   = (unsigned long) _etext;  7     init_mm.end_data   = (unsigned long) _edata;  8     init_mm.brk        = (unsigned long) _end;  9     //爲meminfo排序
10     sort(&meminfo.bank, meminfo.nr_banks, sizeof(meminfo.bank[0]), meminfo_cmp, NULL); 11     sanity_check_meminfo(); 12     arm_memblock_init(&meminfo, mdesc); 13     paging_init(mdesc); 14     ...... 15 }

 

咱們僅列出與內存相關的接口調用. 此處涉及兩個全局變量init_mm與meminfo. 前者在上一節咱們已經見過了, 它是內核自身的mm_struct, 在此處會初始化它的代碼段與brk地址. 後者爲物理內存管理結構meminfo, 其結構以下, 其中nr_banks爲物理內存bank數量, 每一個bank包含3個成員, 分別爲起始物理地址, 長度與是否用於高端內存的標記位.

 1 //defined in arch/arm/include/asm/setup.h
 2 struct membank {  3     phys_addr_t start;  4     phys_addr_t size;  5     unsigned int highmem;  6 };  7 struct meminfo {  8     int nr_banks;  9     struct membank bank[NR_BANKS]; 10 }; 11 //defined in arch/arm/mm/init.c
12 struct meminfo meminfo;

 

meminfo的初始化有兩種方式: 一種是調用early_mem()經過解析傳入的bootargs肯定內存信息(再調用arm_add_memory()填充bank), 另外一種是經過arm_add_memory()直接指定一個bank. 二者都是__init接口, 二者的調用都必須在setup_arch()中爲meminfo排序以前, 在執行sanity_check_meminfo()以後再添加bank是無效的. 若是內核開發人員須要預留一塊內存給特定業務, 能夠調用arm_add_memory()指定一塊reserved內存. sanity_check_meminfo()(defined in arch/arm/mm/mmu.c)做用是檢查meminfo中不合法的bank, 並將低端內存的所在bank與高端內存所在bank拆分.

 1 void __init sanity_check_meminfo(void)  2 {  3     int i, j, highmem = 0;  4     for (i = 0, j = 0; i < meminfo.nr_banks; i++) {  5         struct membank *bank = &meminfo.bank[j];  6         *bank = meminfo.bank[i];  7         //總線地址超過尋址空間確定須要高端內存映射
 8         if (bank->start > ULONG_MAX)  9             highmem = 1; 10 #ifdef CONFIG_HIGHMEM 11         /** 12          * 小於PAGE_OFFSET的地址空間是用戶態地址空間 13          * 大於vmalloc_min的地址空間是高端內存地址空間 14          * 落在以上兩個區間的物理內存均用於高端內存的映射 15          * vmalloc_min值爲(void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET) 16          * 其中VMALLOC_END爲0xFF000000(see Documentation/arm/memory.txt for detail) 17          * VMALLOC_OFFSET爲8M, 在高端內存地址區間的起始, 用於防止訪問越界 18          * 240即高端內存地址區間的長度, 爲可調整值, 若設置太小會致使頻繁的調度內存 19          * 20         **/
21         if (__va(bank->start) >= vmalloc_min || __va(bank->start) < (void *)PAGE_OFFSET) 22             highmem = 1; 23         bank->highmem = highmem; 24         //對於物理內存跨越lowmem與highmem區間的狀況, 將bank分割爲兩塊
25         if (!highmem && __va(bank->start) < vmalloc_min && \ 26             bank->size > vmalloc_min - __va(bank->start)) { 27             if (meminfo.nr_banks >= NR_BANKS) { 28                 printk(KERN_CRIT "NR_BANKS too low, " \ 29                     "ignoring high memory\n"); 30             } else { 31                 memmove(bank + 1, bank, (meminfo.nr_banks - i) * sizeof(*bank)); 32                 meminfo.nr_banks++; 33                 i++; 34                 bank[1].size -= vmalloc_min - __va(bank->start); 35                 bank[1].start = __pa(vmalloc_min - 1) + 1; 36                 bank[1].highmem = highmem = 1; 37                 j++; 38             } 39             bank->size = vmalloc_min - __va(bank->start); 40         } 41 #else
42         bank->highmem = highmem; 43         //未定義高端內存直接跳過
44         if (highmem) { 45             continue; 46         } 47         if (__va(bank->start) >= vmalloc_min || \ 48             __va(bank->start) < (void *)PAGE_OFFSET) { 49             continue; 50         } 51         //部分跨越的狀況截取未跨越部分的內存使用
52         if (__va(bank->start + bank->size - 1) >= vmalloc_min || \ 53             __va(bank->start + bank->size - 1) <= __va(bank->start)) { 54             unsigned long newsize = vmalloc_min - __va(bank->start); 55             bank->size = newsize; 56         } 57 #endif
58         //記錄低端內存的結束地址
59         if (!bank->highmem && bank->start + bank->size > arm_lowmem_limit) 60             arm_lowmem_limit = bank->start + bank->size; 61         j++; 62     } 63 #ifdef CONFIG_HIGHMEM 64     if (highmem) { 65         const char *reason = NULL; 66         if (cache_is_vipt_aliasing()) { 67             reason = "with VIPT aliasing cache"; 68         } 69         if (reason) { 70             while (j > 0 && meminfo.bank[j - 1].highmem) 71                 j--; 72         } 73     } 74 #endif
75     meminfo.nr_banks = j; 76     high_memory = __va(arm_lowmem_limit - 1) + 1; 77     memblock_set_current_limit(arm_lowmem_limit); 78 }

 

由於內核lowmem地址空間與物理內存地址是線性映射的關係, sanity_check_meminfo()會首先保證lowmem的映射, 只有不與lowmem地址空間線性映射的地址才用於highmem, 若是一個bank中存在部分線性映射的地址則將其劃分爲兩塊. 最後使用arm_lowmem_limit初始化memblock.current_limit, 咱們先來看下它的定義.

 1 //defined in include/linux/memblock.h
 2 struct memblock_region {  3     phys_addr_t base;  4     phys_addr_t size;  5 #ifdef CONFIG_HAVE_MEMBLOCK_NODE_MAP  6     int nid;  7 #endif
 8 };  9 struct memblock_type { 10     unsigned long cnt; 11     unsigned long max; 12     phys_addr_t total_size; 13     struct memblock_region *regions; 14 }; 15 struct memblock { 16     phys_addr_t current_limit; 17     struct memblock_type memory; 18     struct memblock_type reserved; 19 }; 20 //defined in mm/memblock.c
21 struct memblock memblock __initdata_memblock = { 22     .memory.regions     = memblock_memory_init_regions, 23     .memory.cnt         = 1, 24     .memory.max         = INIT_MEMBLOCK_REGIONS, 25     .reserved.regions   = memblock_reserved_init_regions, 26     .reserved.cnt       = 1, 27     .reserved.max       = INIT_MEMBLOCK_REGIONS, 28     .current_limit      = MEMBLOCK_ALLOC_ANYWHERE, 29 };

 

memblock有兩個memblock_type結構, 分別用做內存區段與保留內存區段, current_limit是lowmem的結束地址(與vmalloc_min的區別是vmalloc_min是理論lowmem的結束地址, 而current_limit爲實際物理地址能覆蓋的lowmem地址). 在setup_arch()中初始化meminfo後就會調用arm_lowmem_limit()(defined in arch/arm/mm/init.c)初始化全局變量memblock.
經過比較結構體能夠看出meminfo與memblock的區別, 前者記錄的是物理內存的區間, 後者記錄的是內核保留內存與可用內存的區間. 在初始化meminfo後就須要初始化memblock, arm_memblock_init()(defined in arch/arm/mm/init.c).

 1 void __init arm_memblock_init(struct meminfo *mi, struct machine_desc *mdesc)  2 {  3     int i;  4     //將每一個bank添加到memblock.memory中
 5     for (i = 0; i < mi->nr_banks; i++)  6         memblock_add(mi->bank[i].start, mi->bank[i].size);  7     //將內核代碼段/數據段加入memblock.reserved中
 8 #ifdef CONFIG_XIP_KERNEL  9     memblock_reserve(__pa(_sdata), _end - _sdata); 10 #else
11     memblock_reserve(__pa(_stext), _end - _stext); 12 #endif
13     //實際調用memblock_reserve(__pa(swapper_pg_dir), SWAPPER_PG_DIR_SIZE)
14     arm_mm_memblock_reserve(); 15     //device tree內存預留
16     arm_dt_memblock_reserve(); 17     if (mdesc->reserve) 18         mdesc->reserve(); 19     //CMA內存預留
20     dma_contiguous_reserve(min(arm_dma_limit, arm_lowmem_limit)); 21     arm_memblock_steal_permitted = false; 22     memblock_allow_resize(); 23     memblock_dump_all(); 24 }

 

arm_memblock_init()實現比較簡單就不展開了, 其做用是將meminfo中全部bank放入memblock.memory管理, 再將內核自身所在內存, 內核保存頁表的空間, 設備驅動預留的內存, CMA內存依次放入memblock.reserved管理, 最後能夠經過memblock_dump_all()打印memblock信息. 值得一提的是上文提到過的swapper_pg_dir, 該地址起始的空間用於保存頁表信息. SWAPPER_PG_DIR_SIZE爲保存頁表信息所需長度, 對於二級頁表須要2048個PGD結構, 對於三級頁表須要四個頁(每一個頁保存512個PMD結構)加上一個額外頁用於保存PGD. 另外注意是CMA內存也在此處預留(CMA內存之後有空分析).
完成物理內存與預留內存的統計後就要開始創建頁表, setup_arch()會調用paging_init()(defined in arch/arm/mm/mmu.c)創建頁表, 初始化全零內存頁, 壞頁與壞頁表.

 1 void __init paging_init(struct machine_desc *mdesc)  2 {  3     void *zero_page;  4     memblock_set_current_limit(arm_lowmem_limit);  5     build_mem_type_table();  6     prepare_page_table();  7     map_lowmem();  8     dma_contiguous_remap();  9     devicemaps_init(mdesc); 10     kmap_init(); 11     tcm_init(); 12     top_pmd = pmd_off_k(0xffff0000); 13     zero_page = early_alloc(PAGE_SIZE); 14     bootmem_init(); 15     empty_zero_page = virt_to_page(zero_page); 16     __flush_dcache_page(NULL, empty_zero_page); 17 }

 

build_mem_type_table()(defined in arch/arm/mm/mmu.c)用於初始化mem_types[]. mem_types[]根據不一樣內存類型設定頁表的相關屬性, 因與架構強相關此處暫不詳述.
prepare_page_table()(defined in arch/arm/mm/mmu.c)會將內核鏡像所在地址如下空間與除memblock.memory中第一塊內存區間外其它內核空間的頁表映射清空. (對於二級頁表結構)清空頁表映射的方式是清除PMD中的表項並同步刷新TLB, 讓咱們看下它的實現pmd_clear()(defined in arch/arm/include/asm/pgtable-2level.h).

1 #define pmd_clear(pmdp)         \
2     do {                        \ 3         pmdp[0] = __pmd(0);     \ 4         pmdp[1] = __pmd(0);     \ 5         clean_pmd_entry(pmdp);  \ 6     } while (0)

 

清空頁表映射後再調用map_lowmem()(defined in arch/arm/mm/mmu.c)創建對低端內存的頁表映射, 該接口實際調用create_mapping()(defined in arch/arm/mm/mmu.c), 注意傳入的參數, 其地址是memblock.memory中的起始地址與結束地址, 即映射是根據實際物理內存來的, 而類型爲MT_MEMORY, force_pages爲false, 即無需強制分配頁.

 1 static void __init create_mapping(struct map_desc *md, bool force_pages)  2 {  3     unsigned long addr, length, end;  4     phys_addr_t phys;  5     const struct mem_type *type;  6     pgd_t *pgd;  7     if (md->virtual != vectors_base() && md->virtual < TASK_SIZE) {  8         return;  9     } 10     if ((md->type == MT_DEVICE || md->type == MT_ROM) && md->virtual >= PAGE_OFFSET && \ 11         (md->virtual < VMALLOC_START || md->virtual >= VMALLOC_END)) { 12         printk(KERN_WARNING "BUG: mapping for 0x%08llx at 0x%08lx out of vmalloc space\n", \ 13             (long long)__pfn_to_phys((u64)md->pfn), md->virtual); 14     } 15     type = &mem_types[md->type]; 16 #ifndef CONFIG_ARM_LPAE 17     if (md->pfn >= 0x100000) { 18         create_36bit_mapping(md, type); 19         return; 20     } 21 #endif
22     addr = md->virtual & PAGE_MASK; 23     phys = __pfn_to_phys(md->pfn); 24     length = PAGE_ALIGN(md->length + (md->virtual & ~PAGE_MASK)); 25     if (type->prot_l1 == 0 && ((addr | phys | length) & ~SECTION_MASK)) { 26         return; 27     } 28     pgd = pgd_offset_k(addr); 29     end = addr + length; 30     do { 31         unsigned long next = pgd_addr_end(addr, end); 32         alloc_init_pud(pgd, addr, next, phys, type, force_pages); 33         phys += next - addr; 34         addr = next; 35     } while (pgd++, addr != end); 36 } 37 static void __init alloc_init_pud(pgd_t *pgd, unsigned long addr, unsigned long end, \ 38     unsigned long phys, const struct mem_type *type, bool force_pages) 39 { 40     pud_t *pud = pud_offset(pgd, addr); 41     unsigned long next; 42     do { 43         next = pud_addr_end(addr, end); 44         alloc_init_pmd(pud, addr, next, phys, type, force_pages); 45         phys += next - addr; 46     } while (pud++, addr = next, addr != end); 47 }

 

create_mapping()首先會檢查內存區間是否在用戶空間. 對於36位總線架構又不支持LPAE的狀況調用create_36bit_mapping()建立映射, 不然調用alloc_init_pud()(defined in arch/arm/mm/mmu.c). 因爲二級頁表/三級頁表無PUD, alloc_init_pud()僅調用alloc_init_pmd()(defined in arch/arm/mm/mmu.c)分配PMD.

 1 static void __init alloc_init_pmd(pud_t *pud, unsigned long addr, unsigned long end, \  2     phys_addr_t phys, const struct mem_type *type, bool force_pages)  3 {  4     pmd_t *pmd = pmd_offset(pud, addr);  5     unsigned long next;  6     do {  7         next = pmd_addr_end(addr, end);  8         //若是內存類型支持按section分配且地址邊界按section對齊且不強制要求分配頁則映射整個section  9         //以上任意條件不知足則按頁分配
10         if (type->prot_sect && ((addr | next | phys) & ~SECTION_MASK) == 0 && !force_pages) { 11             __map_init_section(pmd, addr, next, phys, type); 12         } else { 13             alloc_init_pte(pmd, addr, next, __phys_to_pfn(phys), type); 14         } 15         phys += next - addr; 16     } while (pmd++, addr = next, addr != end); 17 } 18 static void __init __map_init_section(pmd_t *pmd, unsigned long addr, \ 19     unsigned long end, phys_addr_t phys, const struct mem_type *type) 20 { 21     pmd_t *p = pmd; 22 #ifndef CONFIG_ARM_LPAE 23     //一個PGD包含兩個指針, 分別指向兩個硬件PTE
24     if (addr & SECTION_SIZE) 25         pmd++; 26 #endif
27     do { 28         //爲PMD賦值, 注意此處phys是以SECTION_SIZE對齊的物理地址, 低位用於標記位
29         *pmd = __pmd(phys | type->prot_sect); 30         phys += SECTION_SIZE; 31     } while (pmd++, addr += SECTION_SIZE, addr != end); 32     //刷新TLB
33     flush_pmd_entry(p); 34 } 35 static void __init alloc_init_pte(pmd_t *pmd, unsigned long addr, \ 36     unsigned long end, unsigned long pfn, const struct mem_type *type) 37 { 38     pte_t *start_pte = early_pte_alloc(pmd); 39     pte_t *pte = start_pte + pte_index(addr); 40     BUG_ON(!pmd_none(*pmd) && pmd_bad(*pmd) && ((addr | end) & ~PMD_MASK)); 41     do { 42         set_pte_ext(pte, pfn_pte(pfn, __pgprot(type->prot_pte)), 0); 43         pfn++; 44     } while (pte++, addr += PAGE_SIZE, addr != end); 45     early_pte_install(pmd, start_pte, type->prot_l1); 46 }

 

alloc_init_pmd()將頁表初始化分紅兩種狀況. 第一種是按section映射, section便是前文提到過硬件第一級頁表. 內核將兩個地址連續的硬件第一級頁表合併爲PGD, 故pgd_t結構中包含兩個pmdval_t. 在第一種狀況下內核僅初始化PGD的兩個成員(注意低位標記位使用的是type->prot_sect)並同步刷新TLB, 二級頁表並未初始化(初始化頁表映射時一般走該分支). 第二種狀況就須要按頁分配. 首先early_pte_alloc()(defined in arch/arm/mm/mmu.c)會判斷PMD是否爲合法值, 若是爲非法值即須要分配一塊空間存儲該PMD指向的頁表(通常不可能), 不然調用pmd_page_vaddr()(defined in arch/arm/include/asm/pgtable.h)獲取該PMD保存的物理地址按頁對齊後的虛擬地址. 所以start_pte爲該PMD指向頁表項中的第一個項的虛擬地址, 而pte爲要分配頁所在的頁表項的虛擬地址(此處頁表項指內核PTE而非硬件PTE, 由於硬件PTE排在內核PTE後). pfn_pte()(defined in arch/arm/include/asm/pgtable.h)根據給定pfn與內存類型的標記位生成對應PTE, 而pfn又是上面傳入的物理內存起始地址對應的頁框號.

 1 #define set_pte_ext(ptep, pte, ext) cpu_set_pte_ext(ptep, pte, ext)  2 ENTRY(cpu_v7_set_pte_ext)  3 #ifdef CONFIG_MMU  4     str r1, [r0]  5     bic r3, r1, #0x000003f0  6     bic r3, r3, #PTE_TYPE_MASK  7     orr r3, r3, r2  8     orr r3, r3, #PTE_EXT_AP0 | 2
 9     tst r1, #1 << 4
10     orrne r3, r3, #PTE_EXT_TEX(1) 11     eor r1, r1, #L_PTE_DIRTY 12     tst r1, #L_PTE_RDONLY | L_PTE_DIRTY 13     orrne r3, r3, #PTE_EXT_APX 14     tst r1, #L_PTE_USER 15     orrne r3, r3, #PTE_EXT_AP1 16 #ifdef CONFIG_CPU_USE_DOMAINS 17     tstne r3, #PTE_EXT_APX 18     bicne r3, r3, #PTE_EXT_APX | PTE_EXT_AP0 19 #endif 20     tst r1, #L_PTE_XN 21     orrne r3, r3, #PTE_EXT_XN 22     tst r1, #L_PTE_YOUNG 23     tstne r1, #L_PTE_VALID 24 #ifndef CONFIG_CPU_USE_DOMAINS 25     eorne r1, r1, #L_PTE_NONE 26     tstne r1, #L_PTE_NONE 27 #endif 28     moveq r3, #0
29     ARM( str r3, [r0, #2048]! ) 30     THUMB( add r0, r0, #2048 ) 31     THUMB( str r3, [r0] ) 32     ALT_SMP(mov pc,lr) 33     ALT_UP (mcr p15, 0, r0, c7, c10, 1) 34 #endif 35     mov pc, lr 36 ENDPROC(cpu_v7_set_pte_ext)

 

set_pte_ext()(defined in arch/arm/include/asm/pgtable-2level.h)定義爲cpu_set_pte_ext(), 後者也是宏, 根據不一樣架構或CPU拼接爲一個新函數, 在本文中基於ARMv7且無CPU_NAME狀況下實際調用cpu_v7_set_pte_ext()(defined in arch/arm/mm/proc-v7-2level.S). 其中R0爲內核的頁表項地址, 在alloc_init_pte()中會循環遞增, R1爲對應物理地址加上內核頁表標記位後的值. 在每次進入set_pte_ext()時都會將R1保存在R0的地址上, 即初始化該頁表項. 再根據內核頁表項得出硬件PTE(保存在R3中)並賦值給R0起始偏移2048字節的地址, 最後將LR賦值給PC(是否由於arch/arm/mm/proc-syms.c中導出cpu_set_pte_ext()符號因此做爲函數存在, 須要跳轉返回? 不然沒有必要跳轉返回). 另外ALT_UP()宏是幹什麼的也沒看懂, 反彙編出來沒有這條指令, 難道此處不作刷新TLB操做?
在循環設置完PTE後調用early_pte_install()(defined in arch/arm/mm/mmu.c)更新PMD(爲L1頁表, 標記位爲type->prot_l1)並刷新TLB.

 1 static void __init early_pte_install(pmd_t *pmd, pte_t *pte, unsigned long prot)  2 {  3     __pmd_populate(pmd, __pa(pte), prot);  4     BUG_ON(pmd_bad(*pmd));  5 }  6 static inline void __pmd_populate(pmd_t *pmdp, phys_addr_t pte, pmdval_t prot)  7 {  8     pmdval_t pmdval = (pte + PTE_HWTABLE_OFF) | prot;  9     pmdp[0] = __pmd(pmdval); 10 #ifndef CONFIG_ARM_LPAE 11     pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t)); 12 #endif
13     flush_pmd_entry(pmdp); 14 }

 

從新回到paging_init(), 再完成低端內存映射後還會調用dma_contiguous_remap()映射CMA內存, 調用devicemaps_init()完成設備樹初始化, 與本節內容關聯不大暫且略過. kmap_init()(defined in arch/arm/mm/mmu.c)用於初始化高端內存映射.

 1 static void __init kmap_init(void)  2 {  3 #ifdef CONFIG_HIGHMEM  4     pkmap_page_table = early_pte_alloc_and_install(pmd_off_k(PKMAP_BASE), PKMAP_BASE, _PAGE_KERNEL_TABLE);  5 #endif
 6 }  7 static pte_t *__init early_pte_alloc_and_install(pmd_t *pmd, unsigned long addr, unsigned long prot)  8 {  9     if (pmd_none(*pmd)) { 10         pte_t *pte = early_pte_alloc(pmd); 11         early_pte_install(pmd, pte, prot); 12     } 13     BUG_ON(pmd_bad(*pmd)); 14     return pte_offset_kernel(pmd, addr); 15 }

 

pkmap_page_table是內核高端內存映射頁表的指針, kmap_init()會調用early_pte_alloc_and_install()來初始化該指針. early_pte_alloc_and_install()首先判斷傳入pmd是否合法, 爲空則重走上面分配PTE的流程, 不然就獲取PKMAP_BASE地址對應的頁表項所在第二級頁表中的地址. 這裏有點沒看懂, 什麼PKMAP_BASE是(PAGE_OFFSET - PMD_SIZE)? 看了下Documentation/arm/memory.txt裏有提到這個值, 貌似意思是PKMAP_BASE到PAGE_OFFSET之間是內核用於映射高端內存頁表的空間?
回到paging_init(), 初始化pkmap_page_table()後還會調用tcm_init()(defined in arch/arm/kernel/tcm.c)初始化TCM, 3536貌似無TCM所以本文暫不分析. top_pmd指向的是0xFFFF0000地址對應的PMD, 由於0xFFFF0000地址往上是向量表及其它特殊用途的內存地址. empty_zero_page是指向一個空頁的指針, 用於零初始化數據與copy on write操做. 該頁空間是從memblock中尋找一個頁大小空間並保留出來的, 在paging_init()最後會執行一次dcache刷新該空頁.
最後來看下bootmem_init()(defined in arch/arm/mm/init.c). bootmem_init()中涉及SPARSEMEM內容暫且略過, 那麼就只調用了arm_bootmem_init()(defined in arch/arm/mm/init.c)與arm_bootmem_free()(defined in arch/arm/mm/init.c). 前者將低端內存區間標記爲保留區段, 後者計算該區間內存空洞大小.

 1 void __init bootmem_init(void)  2 {  3     unsigned long min, max_low, max_high;  4     max_low = max_high = 0;  5     //根據meminfo查詢物理內存起始地址, 低端內存的結束地址, 高端內存的最高地址
 6     find_limits(&min, &max_low, &max_high);  7     arm_bootmem_init(min, max_low);  8     //未開啓SPARSE略過
 9     arm_memory_present(); 10     //未開啓SPARSE略過
11     sparse_init(); 12     arm_bootmem_free(min, max_low, max_high); 13     max_low_pfn = max_low - PHYS_PFN_OFFSET; 14     max_pfn = max_high - PHYS_PFN_OFFSET; 15 }

 

至此setup_arch()中關於內存操做到此結束, 總結一下setup_arch()首先獲取物理內存的總線地址與大小填入meminfo與memblock中, 計算總內存與保留內存, 劃分低端內存與高端內存, 去初始化全部頁表映射並對低端內存從新映射頁表.

 1 asmlinkage void __init start_kernel(void)  2 {  3     ......  4     //將init_mm.owner設爲init_task, 需開啓MM_OWNER
 5     mm_init_owner(&init_mm, &init_task);  6     mm_init_cpumask(&init_mm);  7     build_all_zonelists(NULL, NULL);  8     page_alloc_init();  9     mm_init(); 10     ...... 11 }

 

回到start_kernel(), 執行一系列初始化後調用mm_init()(defined in init/main.c)執行內存初始化.

1 static void __init mm_init(void) 2 { 3     page_cgroup_init_flatmem(); 4     mem_init(); 5     kmem_cache_init(); 6     percpu_init_late(); 7     pgtable_cache_init(); 8     vmalloc_init(); 9 }

 

page_cgroup_init_flatmem()爲cgroup管理內存初始化, 略過不作分析. mem_init()(defined in arch/arm/mm/init.c)取消物理內存空洞對應的低端內存映射, 釋放系統未使用的bootmem並統計全部空閒與預留內存頁數量. 而後初始化slab與vmalloc, 內存初始化到此結束.

 1 void __init mem_init(void)  2 {  3     unsigned long reserved_pages, free_pages;  4     struct memblock_region *reg;  5     int i;  6     //max_pfn爲物理內存最大地址對應的頁框號, max_mapnr爲物理內存能映射的頁總數
 7     max_mapnr = pfn_to_page(max_pfn + PHYS_PFN_OFFSET) - mem_map;  8     //釋放(物理內存bank間的空洞帶來的)未使用的低端內存
 9     free_unused_memmap(&meminfo); 10     //釋放系統初始化時用到的bootmem
11     totalram_pages += free_all_bootmem(); 12     //釋放高端內存頁並初始化對應頁的結構體
13     free_highpages(); 14     reserved_pages = free_pages = 0; 15     for_each_bank(i, &meminfo) { 16         struct membank *bank = &meminfo.bank[i]; 17         unsigned int pfn1, pfn2; 18         struct page *page, *end; 19         pfn1 = bank_pfn_start(bank); 20         pfn2 = bank_pfn_end(bank); 21         page = pfn_to_page(pfn1); 22         end  = pfn_to_page(pfn2 - 1) + 1; 23         do { 24             if (PageReserved(page)) 25                 reserved_pages++; 26             else if (!page_count(page)) 27                 free_pages++; 28             page++; 29         } while (page < end); 30     } 31     //計算並打印實際內存頁
32     printk(KERN_INFO "Memory:"); 33     num_physpages = 0; 34     for_each_memblock(memory, reg) { 35         unsigned long pages = memblock_region_memory_end_pfn(reg) - memblock_region_memory_base_pfn(reg); 36         num_physpages += pages; 37         printk(" %ldMB", pages >> (20 - PAGE_SHIFT)); 38     } 39     //打印信息, 略
40     ...... 41 }
相關文章
相關標籤/搜索