在上一节咱们讨论了用户态向内核申请内存的接口(系统调用), 发现内核仅仅是判断进程的虚拟地址空间是否足够划分出新的区间, 实际并未分配物理内存. 这是由于内核分配内存的机制是仅当进程实际使用该地址后才为其分配物理内存, 借此提高物理内存使用率. 本节咱们就来看看内核到底是如何分配物理内存的.
以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 }