Virtual Memory Initialization in Linux/ARM Kernel


Agenda
一、ARM Linux启动的几个阶段
二、Virtual Address的划分
三、页表建立的第一阶段
四、页表建立的第二阶段
五、怎样维护页表

本文讨论了虚拟内存系统在Linux Kernel中的初始化过程。Kernel Code版本为V3.3.0,不涉及LPAE以及SMP的情况。
一.    ARM Linux启动的几个阶段
当一个嵌入式Linux系统启动后,PC的值是设定好的,可能是0,也可能是其他的值。在PC触及的地方,常见的情况是SOC存放的一段程序,该程序称之为Boot Monitor。每当机器硬重启,或者开机的时候,都会最先执行这段Boot Monitor程序[1]。之后,Boot Monitor会将CPU转交给Uboot。这一过程类似于PC机的BIOS将执行权转交给GrubUbootARM LinuxImage加载到内存中,让CPU转向Linux执行。众所周知LinuxImage包括两部分,第一部分是一个解压程序,第二部分才属于真正的OS。解压程序首先运行,建立页表、启动MMUEnable Cache,解压缩Kernel[2]。之后,将MMU关闭,Disable Cache,进入Linux启动过程。本文所介绍的Virtual Memory Initialization都是在Linux Startup阶段,不涉及解压过程中的Virtual Memory.

二.    Virtual Address的划分
Virtual Address共有4GB的空间,该空间分为两部分,一部分是Kernel Space,另一部分是User Space.这两部分在4GBSpace所占的比重可以在build Kernel的时候配置。通常的比例大约是31User Space使用03G-16MB的空间,Kernel Space使用3G-16MB4GB的空间。每个进程都有不同的地址空间,这个地址空间指的是User SpaceKernel Space对于所有的进程都是一样的。这也是为什么每一个进程都能够通过系统调用进入特权模式使用内核提供资源的原因。不过Kernel Space并不是铁板一块,其空间划分为不同的部分[3]


三.    页表建立的第一阶段
支持Virtual Memory的硬件是MMUMMU必须开启,CPU才能运行在Virtual Memory模式中。MMU开启之后,CPU拿到的PC值是虚拟地址,在寻址的时候需要MMU的参与,将虚拟地址转化为物理地址并将物理地址发送到地址总线上在Memory中进行寻址。而将虚拟地址转化为物理地址的依据是OS提供的页表。软件就是页表。页表由操作系统创建和维护。建立页表的过程分为两个阶段,为什么要分为两个阶段呢?原因是,在开启MMU之前,OS还不清楚有多少物理内存可用,所以,在这个阶段暂且保证MMU开启之后,OS部分built-in部分的代码可以正常运行。在MMU开启之后,系统探测完物理内存有多少可用后,再进行第二阶段的映射。

第一阶段映射三段内存。其一是对开启MMU的函数进行映射,其虚拟地址等于物理地址;其二是将Linux KernelMemory中的镜像进行映射,其虚拟地址==物理地址+3G-物理内存偏移量;其三是对UART I/O进行映射,其虚拟地址由驱动开发者指定。映射图如下所示:



为什么要单独给__turn_mmu_on函数进行映射呢?因为在MMU开启之前,CPU拿到的地址都是物理地址,在寻址的时候,不用经过MMU,直接发在地址总线上到Memory中进行寻址。所以,当开启MMU之后,但由于MMU的存在,PC拿到的地址需要先经过MMU的转化再发送到地址总线上进行寻址,于是,必须建立对__turn_mmu_on建立一个页表,使得虚拟地址与物理地址相等。可是,PC不可能总是拿和物理地址相等的虚拟地址吧,必须想办法让PC的值拿到3G以上虚拟空间的地址。默认情况下,PC的值是根据上一次PC的值+4得到的。要使PC的值来一次跨越,需要使用mov指令。在进入__turn_mmu_on之前,采用adr指令,让某个寄存器拿到某函数的链接地址,然后通过mov指令,使PC的值从“物理地址”跨越到虚拟地址。来看下代码是怎么实现的:
arch/arm/kernel/head.S
 139        ldr     r13, =__mmap_switched           @ address to jump to after
 140                                                @ mmu has been enabled
 141        adr     lr, BSYM(1f)                    @ return (PIC) address
 142        mov     r8, r4                          @ set TTBR1 to swapper_pg_dir
 143 ARM(   add     pc, r10, #PROCINFO_INITFUNC     )
 144 THUMB( add     r12, r10, #PROCINFO_INITFUNC    )
 145 THUMB( mov     pc, r12                         )
 1461:      b       __enable_mmu
__enable_mmu->__turn_mmu_on:
 451ENTRY(__turn_mmu_on)
 452        mov     r0, r0
 453        instr_sync
 454        mcr     p15, 0, r0, c1, c0, 0           @ write control reg
 455        mrc     p15, 0, r3, c0, c0, 0           @ read id reg
 456        instr_sync
 457        mov     r3, r3
 458        mov     r3, r13
 459        mov     pc, r3
 460__turn_mmu_on_end:
 461ENDPROC(__turn_mmu_on)
 462        .popsection

Linux KernelMMUmapping所建立的内存区域是PHYS_OFFSET~_END。该区域映射到了0xC0000000之上的虚拟地址。映射的内容包括uBoot传给Kernel的参数atags0号进程使用的页表,以及Kernel被加载到内存中的各个Section

UART驱动的映射比较搞。Kernel只获取了UART驱动的起始地址。映射的区域是起始地址~min(0xFFFFFFF,起始地址+512MB)。对应的代码段是:
 269        /*
 270         * Map in IO space for serial debugging.
 271         * This allows debug messages to be output
 272         * via a serial console before paging_init.
 273         */
 274        addruart r7, r3, r0
 275
 276        mov     r3, r3, lsr #SECTION_SHIFT
 277        mov     r3, r3, lsl #PMD_ORDER
 278
 279        add     r0, r4, r3
 280        rsb     r3, r3, #0x4000                 @ PTRS_PER_PGD*sizeof(long)
 281        cmp     r3, #0x0800                     @ limit to 512MB
 282        movhi   r3, #0x0800
 283        add     r6, r0, r3
 284        mov     r3, r7, lsr #SECTION_SHIFT
 285        ldr     r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
 286        orr     r3, r7, r3, lsl #SECTION_SHIFT
 287#ifdef CONFIG_ARM_LPAE
 288        mov     r7, #1 << (54 - 32)             @ XN
 289#else
 290        orr     r3, r3, #PMD_SECT_XN
 291#endif
 2921:      str     r3, [r0], #4
 293#ifdef CONFIG_ARM_LPAE
 294        str     r7, [r0], #4
 295#endif
 296        add     r3, r3, #1 << SECTION_SHIFT
 297        cmp     r0, r6
在这三段页表建立后,软件还需要为硬件做的事情是:开启DCacheIcache invalidate TLBI+D),配置Domain Access Control Register.以及配置PRRRNRRR,然后将页表的基值(PHYS_OFFSET+0x4000)放到TTBR0TTBR1中,将TTBCR赋值为0.然后enable MMU.从此Virtual Memory System正式建立。

四.    页表建立的第二阶段
第一阶段建立完成后,I/O 以及Main Memory还都没有建立映射呢。所以,在之后还要进一步建立页表。建立第二部分的页表也分为三部分:1. Main Memory2. I/O Memory. 3. Vector Page.

1.       Main Memory的映射
Main Memory是主存。在第一阶段中,对Main Memory只映射了很小一部分的物理空间。在MMU开启之后,需要将全部的物理内存都进行映射。可是,主存到底有多大,目前我们还不知道。如何知道?有两种渠道,三种方式。第一种渠道是通过Boot Loader;第二种渠道是通过配置Kernel时候的CONFIG_CMDLINE。对于通过Boot Loader得到主存大小的,有两种方式,其一是FDT,其二是Atags。这两种方式的原理是一样的,都是Boot Loader将硬件信息存放在特定的数据结构中,其数据结构的地址存放在R2寄存器中传递到Kernel中。通常其数据结构的地址存放在物理地址最开始的16KB空间中,其大小不超过一个页(4KB)。

Kernel是怎样将这些信息检测出来呢?在Kernel中对于通过Atags可能传递的每一个类型参数,设置了对应的处理函数,并将这些类型和处理函数放在特殊的.section,对于Atags传递的每一个参数,Kernel都要遍历这个section,找到匹配的类型,然后调用处理函数。

对与CONFIG_CMDLINE方式指定主存大小的方法,Kernel对于其处理方式和Atags的处理是类似的。关于这两种方式,内核详细的处理过程可以在查看之前的博文:Linux/ARM物理内存探测(1) - CMDLINELinux/ARM物理内存探测(2)-uBoot tags

Kernel得到具体的内存信息后,就要建立映射了。建立映射的方式线性映射的。物理内存开始的地方对应0xC0000000. 在第二部分虚拟地址划分中谈到过:ARM Linux默认的vmalloc的大小是240MB,即最小的VMALLOC区间的大小是240MB(3G+768MB~0xFF000000).于是,1). 当主存的大小超过768MB,超过768MB的主存将不会线性映射到Kernel Space中。2). 当主存小于768MB的时候,主存全部线性映射到Kernel Space中。这样VMALLOC区间将为:3G+physical Memory Length~0xFF000000.

注1:
vmalloc区间默认大小是240MB(arch/arm/mm/mmu.c)
static void * __initdata vmalloc_min =
(void *)(VMALLOC_END - (240 << 20) - VMALLOC_OFFSET);

/*
 * vmalloc=size forces the vmalloc area to be exactly 'size'
 * bytes. This can be used to increase (or decrease) the vmalloc
 * area - the default is 240m.
 */

注2:
建立映射的时候,除了给出地址的对应关系外,还要给出访问权限以及Cacheable,Bufferable,Shareable之类的信息。在访问权限方面,对于Kernel Space的映射,当用户进程在User mode下时,不能访问Kernel Space的空间;当用户进程在Kernel Mode下,对Kernel Space是可读可写的。

2.       I/O Memory的映射
ARM中,I/O和物理内存是统一编址的。I/O的映射是通过Special MachineI/O初始化的时候调用iotableI/O编址进入Kernel Space的。其I/O端口的物理地址,要映射的虚拟地址以及虚拟地址的长度都要求Special Machine静态定义好。

3.       Vector Page的映射
MMU enable,依靠system control register cp15.c1寄存器的第0位。system control register cp15.c1寄存器中的V位,也决定当出现异常的时候PC的值是多少。如果V位置位,则当异常发生的时候其PC值为0Xffff0000, 如果V不置位,则异常发生时,PC值为0x0. 通常情况,V位都是置位的。于是,当V置位时,需要为0xffff0000的虚拟地址映射vector page. 当映射完成后,需要将vector的内容memcpy0Xffff0000映射的物理页(vector page). 注意一点:vector page的物理页被映射了两次,一次是线性映射,一次是映射到0xffff0000. 0xffff0000对应的Page tablememory属性是read only. vector page线性映射的page table中定义的memory属性是privileged mode--r/w, user mod--no access. 在通过memcpy填充vector page的时候,用的虚拟地址是线性映射的虚拟地址(如果直接用0Xffff0000,就出现读写错误啦J)

下图是 PHYS_OFFSET:8MB, Main Memory Length:600MB, I/O:0xF6000000,Length 48MB的映射图:


五.    怎样维护页表

对于Kernel Space的页表建立后,基本上是一劳永逸了。不是完全一劳永逸,是因为对于非线性映射的页表在Kernel运行中可以动态修改的。这个修改直接针对init_mm—0号进程的页表,也尊称为主页表。由于Kernel Space的页表对于各个进程是共享的,所以,如果主页表修改后,会通过page fault的方式同步到其他进程的Kernel Space页表中。

ARM Linux Kernel采用了Section 1-level 2-level混合的映射方式。对于Section映射,只要把映射物理内存基值1MB对齐,存放在PGD中,就可以了。对于2-level的映射,ARM Linux就有点小复杂了。复杂的原因是很多人学习Linux都是从X86开始的,所以遇到ARM这种映射方式就觉得有点奇怪。首先对于2-level的映射,在第一级的页表映射项中,下一级映射的基值占了22位,也就是1KB对齐的(看下图)。

又由于在二级映射的时候,虚拟地址只用了8(19~12),所以应该可以寻址256index,每项占4个字节,所以,每个二级页表是1KB。但是,由于Linux在做内存管理的时候,采取4KB作为一个管理单元,于是每个页就可以放4个二级页表了。但是,由于在硬件支持的页表项中,属性为不足以提供足够的信息为Linux进行内存管理使用,于是,ARM Linux就需要在其他地方提供额外的信息来让上层的内存管理使用[4]。于是,ARM Linux采用的方法是使用两个页表项。一个MMU使用,另外一个提供额外的属性信息。所以,事实上一个4KB的页就提供了两个二级页表。每个二级页表由一个Linux版本和Hardware版本共同组成。


每一块物理内存,可以对应N个虚拟地址。以上介绍的是Kernel Space映射,对于User Space的映射要依赖Page fault来进行。每一个User Space的映射都是2-level的。

参考:
[3]. /Documentation/arm/memory.txt
[4]. http://elinux.org/Tims_Notes_on_ARM_memory_allocation

注:2012年8月29日,添加了访问权限的内容。

评论

此博客中的热门博文

Linux/ARM Page Table Entry 属性设置分析

提交了30次才AC ---【附】POJ 2488解题报告

笔记