在上一节我们讨论了用户态向内核申请内存的接口(系统调用), 发现内核仅仅是判断进程的虚拟地址空间是否足够划分出新的区间, 实际并未分配物理内存. 这是因为内核分配内存的机制是仅当进程实际使用该地址后才为其分配物理内存, 借此提升物理内存使用率. 本节我们就来看看内核究竟是如何分配物理内存的.
以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分别为其起始地址与结束地址.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_offset10 W(b) vector_addrexcptn + stubs_offset11 W(b) vector_irq + stubs_offset12 W(b) vector_fiq + stubs_offset13 .globl __vectors_end14 __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).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 \correction12 sub lr, lr, #\correction13 .endif14 /**15 * 在栈上保存r0与lr, 接下来r0与lr会被修改16 *17 **/18 stmia sp, {r0, lr}19 /**20 * 保存触发异常时cpsr21 *22 **/23 mrs lr, spsr24 str lr, [sp, #8]25 /**26 * 进入svc模式, 修改当前spsr值27 *28 **/29 mrs r0, cpsr30 eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)31 msr spsr_cxsf, r032 /**33 * cpsr低四位为模式位, 根据触发异常时cpsr值获取模式并计算对应异常向量地址34 * 注意在跳转异常向量前r0被修改为sp35 *36 **/37 and lr, lr, #0x0f38 THUMB( adr r0, 1f )39 THUMB( ldr lr, [r0, lr, lsl #2] )40 mov r0, sp41 ARM( ldr lr, [pc, lr, lsl #2] )42 movs pc, lr43 ENDPROC(vector_\name)44 .align 245 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 511 __dabt_svc:12 svc_entry13 mov r2, sp14 dabt_helper15 svc_exit r516 UNWIND(.fnend)17 ENDPROC(__dabt_svc)18 .align 519 __pabt_usr:20 usr_entry21 mov r2, sp22 pabt_helper23 UNWIND(.fnend)24 ENTRY(ret_from_exception)25 UNWIND(.fnstart)26 UNWIND(.cantunwind)27 get_thread_info tsk28 mov why, #029 b ret_to_user30 UNWIND(.fnend)31 ENDPROC(__pabt_usr)32 ENDPROC(ret_from_exception)33 .align 534 __pabt_svc:35 svc_entry36 mov r2, sp37 pabt_helper38 svc_exit r539 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_SIZE10 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_PC19 mov r6, #-120 /**21 * r3缓存的是r0, r4-r6分别缓存lr, cpsr与orig_r022 *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_fp29 #ifdef CONFIG_IRQSOFF_TRACER30 bl trace_hardirqs_off31 #endif32 ct_user_exit save = 033 .endm34 .macro svc_entry, stack_hole=035 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 - 443 mov r6, #-144 add r2, sp, #(S_FRAME_SIZE + \stack_hole - 4)45 SPFIX( addeq r2, r2, #4 )46 str r3, [sp, #-4]!47 mov r3, lr48 stmia r7, {r2 - r6}49 #ifdef CONFIG_TRACE_IRQFLAGS50 bl trace_hardirqs_off51 #endif52 .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究竟做了什么.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 .endm10 .macro pabt_helper11 #ifdef MULTI_PABORT12 ldr ip, .LCprocfns13 mov lr, pc14 ldr pc, [ip, #PROCESSOR_PABT_FUNC]15 #else16 bl CPU_PABORT_HANDLER17 #endif18 .endm19 #ifdef MULTI_DABORT20 .LCprocfns:21 .word processor22 #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, sp14 c03b1630: ebf19c12 bl c001868015 c03b1634: ea000086 b c03b1854 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, #0x0d10 bne do_DataAbort11 mcr p15, 0, r0, c7, c8, 012 isb13 mrc p15, 0, ip, c7, c4, 014 and r3, ip, #0x7b15 cmp r3, #0x0b16 bne do_DataAbort17 bic r1, r1, #0xf18 and ip, ip, #0x7e19 orr r1, r1, ip, LSR #120 #endif21 b do_DataAbort22 ENDPROC(v7_early_abort)23 .align 524 ENTRY(v7_pabort)25 mrc p15, 0, r0, c6, c0, 226 mrc p15, 0, r1, c5, c0, 127 b do_PrefetchAbort28 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)的实现.
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 #else10 #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_context18 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_context28 * __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_VM38 if (!user_mode(regs) && !search_exception_tables(regs->ARM_pc))39 goto no_context;40 #endif41 }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 //OOM69 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 fault10 sub r2, r2, ip11 b .Lc2u_dest_aligned12 .pushsection .fixup,"ax"13 .align 014 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_pfn4 #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 #include2 #include 3 #include 4 #ifdef CONFIG_ARM_LPAE5 #include 6 #else7 #include 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.h13 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.h18 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_dir10 .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_CMA27 if (allocflags_to_migratetype(gfp_mask) == MIGRATE_MOVABLE)28 alloc_flags |= ALLOC_CMA;29 #endif30 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.c12 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_HIGHMEM11 /**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 #else42 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 #endif58 //记录低端内存的结束地址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_HIGHMEM64 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 #endif75 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.c21 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 #else11 memblock_reserve(__pa(_stext), _end - _stext);12 #endif13 //实际调用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_LPAE17 if (md->pfn >= 0x100000) {18 create_36bit_mapping(md, type);19 return;20 }21 #endif22 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_LPAE23 //一个PGD包含两个指针, 分别指向两个硬件PTE24 if (addr & SECTION_SIZE)25 pmd++;26 #endif27 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 //刷新TLB33 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 << 410 orrne r3, r3, #PTE_EXT_TEX(1)11 eor r1, r1, #L_PTE_DIRTY12 tst r1, #L_PTE_RDONLY | L_PTE_DIRTY13 orrne r3, r3, #PTE_EXT_APX14 tst r1, #L_PTE_USER15 orrne r3, r3, #PTE_EXT_AP116 #ifdef CONFIG_CPU_USE_DOMAINS17 tstne r3, #PTE_EXT_APX18 bicne r3, r3, #PTE_EXT_APX | PTE_EXT_AP019 #endif20 tst r1, #L_PTE_XN21 orrne r3, r3, #PTE_EXT_XN22 tst r1, #L_PTE_YOUNG23 tstne r1, #L_PTE_VALID24 #ifndef CONFIG_CPU_USE_DOMAINS25 eorne r1, r1, #L_PTE_NONE26 tstne r1, #L_PTE_NONE27 #endif28 moveq r3, #029 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 #endif35 mov pc, lr36 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_LPAE11 pmdp[1] = __pmd(pmdval + 256 * sizeof(pte_t));12 #endif13 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 //释放系统初始化时用到的bootmem11 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 }