MMU tlb


1-3、ARM开发步步深入之MMU初窥

实验目的:启用MMU,映射SDRAM的地址空间,操作虚拟地址实现“点灯大法”,借此掌握MMU的使用。

实验环境及说明:恒颐S3C2410开发板H2410。H2410核心板扩展有64MB的K4S561632 SDRAM(4M*16bit*4BANK),地址范围是0x30000000~0x33FFFFFF。GPIO端口的地址范围是0x56000000~0X560000B0。

实验思路:开发板上电启动后,自动将NandFlash开始的4K数据复制到SRAM中,然后跳转到0地址开始执行,然后初始化存储控制器SDRAM,把2K后的代码从SRAM中复制到SDRAM中(存放在0x30004000,前16KB用来存放页表)、设置页表、启动MMU实现虚拟地址映射GPIO寄存器和SDRAM,最后跳转到SDRAM中(地址0xB0004000)运行。重新设置栈指针,跳到点灯代码的入口点实现点灯操作。

知识掌握:MMU地址转换、内存访问权限检查、TLB及Cache的使用
一、MMU地址转换:
1.首先弄清除为什么要使用MMU纳?MMU即内存管理单元,直白一点的讲,就像食堂的餐具,所有的学生一起吃饭时不够用,但食堂又不想再出资购买新的餐具(原因很明显:一方面要成本,另一方面又占地方。这就像增加内存一样),那么有没有解决办法?根据以往经验得知不可能全学校的学习一起都到食堂吃饭,于是食堂就找几个人负责餐具的管理(相当于MMU),他们一方面发放餐具,保证来的同学有餐具可用,另一方面又回收用完的餐具(这就相当于虚拟地址到物理地址之间建立了一个映射一样,内存还是那么多,但从任意单个程序角度都好像用不完一样)。当然如果有同学一个人拿好几套餐具肯定不允许的(这就相当于内存的权限检查)。MMU在地址转换过程中涉及到三种地址:(VA---Virtual Address,虚拟地址)---这个就相当于餐具存放的地方(大家都可以领到餐具)。CPU核心看到和用到的只是虚拟地址VA,至于VA如果去对应物理地址PA,CPU核心不理会,大家也不会去关心总共有多少餐具吧;(MVA---Modified Virtual Address,变换后的虚拟地址)---这个相当于放假的时候,人很少,只发餐具好了,用过的就不先回收了,节省人员了。Caches和MMU看不到VA,他们利用MVA转换得到PA,放假了回收餐具的人也不需要一直寻找用完的餐具;(PA---Physical Address,物理地址)---实际的餐具量,就那些。实际设备看不到VA、MVA,读写它们使用的是物理地址PA,同学们就餐一般会领到餐具。
2.虚拟地址到物理地址的转换过程。ARM使用页表来进行转换,S3C2410最多会用到两级页表,以段(Section,1M)的方式进行转换时只用到一级页表,以页(Page)的方式进行转换时用到两级页表。页的大小有3种:大页(64KB)、小页(4KB)和极小页(1KB)。本文只是以段地址转换过程为例来讲解一下,页的转换大同小异。
★首先有个页表基址寄存器(位置为协处理器CP15的寄存器C2),它里面写入的就是一级页表的地址,通过读取它就可以找到一级页表存放的起始位置。一级页表的地址是16K对齐(所以[13:0]为0,使用[31:14]存储页表基址)。一级页表使用4096个描述符来表示4GB空间,所以每个描述符对应1MB的虚拟地址,存储它对应的1MB物理空间的起始地址,或者存储下一级页表的地址。使用MVA[31:20]来索引一级页表(31-20一共12位,2^12=4096,所以是4096个描述符),得到一个描述符,每个描述符占4个字节。
★描述符最后两位为0B10时,即是段的方式映射。[31:20]为段基址,此描述符低20位填充0后就是一块1MB物理地址空间的起始地址。MVA[19:0]用来在这1MB空间中寻址。描述符的位[31:20]和MVA[19:0]构成了这个虚拟地址MVA对应的物理地址。以段的方式进行映射时,虚拟地址MVA到物理地址PA的转换过程如下:①页表基址寄存器位[31:14]和MVA[31:20]组成一个低两位为0的32位地址,MMU利用这个地址找到段描述符;②取出段描述符的位[31:20](段基址),它和MVA[19:0]组成一个32位的物理地址(这就是MVA对应的PA)。




  
二、内存的访问权限检查
内存的访问权限检查决定一块内存是否允许读/写。这由CP15寄存器C3(域访问控制)、描述符的域(Domain)、CP15寄存器C1的R/S/A位和描述符的AP位共同决定。“域”决定是否对某块内存进行权限检查,"AP"决定如何对某块内容进行权限检查。S3C2440有16个域,CP15寄存器C3中每两位对应一个域(一共32位),用来表示这个域是否进行权限检查。
每两位数据的含义:00---无访问权限(任何访问都将导致"Domain fault"异常);01---客户模式(使用段描述符、页描述符进行权限检查);10---保留(保留,目前相当于“无访问权限”);11---管理模式(不进行权限检查,允许任何访问)。"Domain"占用4位,用来表示内存属于0-15哪一个域。

三、TLB和Cache
首先说两者都是利用程序访问的局部性原理,通过设置高速、小容量的存储器来提高性能。
1.(TLB---Translation Lookaside Buffers,转译查找缓存):由于从MVA到PA的转换需要访问多次内存,大大降低了CPU的性能,故提出TLB办法改进。当CPU发出一个虚拟地址时,MMU首先访问TLB。如果TLB中含有能转换这个虚拟地址的描述符,则直接利用此描述符进行地址转换和权限检查,否则MMU访问页表找到描述符后再进行地址转换和权限检查,并将这个描述符填入TLB中,下次再使用这个虚拟地址时就直接使用TLB用的描述符。使用TLB需要保证TLB中的内容与页表一致,在启动MMU之前,页表中的内容发生变化后,尤其要注意。一般的做法是在启动MMU之前使整个TLB无效,改变页表时,使所涉及的虚拟地址对应的TLB中条目无效。
2.(Cache,高速缓存):为提高程序的运行速度,在主存和CPU通用寄存器之间设置一个高速的、容量相对较小的存储器,把正在执行的指令地址附近的一部分指令或数据从主存调入这个存储器,供CPU在一段时间内使用。
★写数据的两种方式:①(Write Through,写穿式)---任一CPU发出写信号送到Cache的同时,也写入主存,保证主存的数据同步更新。优点是操作简单,但由于主存速度慢,降低了系统的写速度并占用了总线的时间。②(Write Back,回写式)---数据一般只写到Cache,这样可能出现Cache中的数据得到更新而主存中的数据不变(数据陈旧)的情况。此时可在Cache中设一个标志地址及数据陈旧的信息,只有当Cache中的数据被换出或强制进行”清空“操作时,才将原更新的数据写入主存响应的单元中,保证了Cache和主存中数据一致。
★Cache有以下两个操作:①(Clean,清空)---把Cache或Write buffer中已经脏的(修改过,但未写入主存)数据写入主存。②(Invalidate,使无效)---使之不能再使用,并不将脏的数据写入主存。
★S2C2440内置了(ICaches,指令Cache)、(DCaches,数据Cache)和(Write buffer,写缓存),操作时需要用到描述符中的C位(Ctt)和B位(Btt)。①(ICaches,指令Cache)---系统刚上电或复位时,ICaches中的内容是无效的,并且ICaches功能关闭。往Icr位(CP15协处理器中寄存器1的第12位)写1可以启动ICaches,写0停止ICaches。ICaches一般在MMU开启后使用,此时描述符的C位用来表示一段内存是否可以被Cache。若Ctt=1,允许Cache,否则不允许。如果MMU没有开启,ICaches也可以被使用,此时CPU读取指令时所涉及的内存都被当做允许Cache。ICaches关闭时,CPU每次取指都要读取主存,性能低,所以通常尽早启动ICaches。ICaches开启后,CPU每次取指时都会先在ICaches中查看是否能找到所用指令,而不管Ctt是0还是1。如果找到成为Cache命中,找不到称为Cache丢失,ICaches被开启后,CPU的取指有如下三种情况:Cache命中且Ctt为1时,从ICaches中取指,返回CPU;Cache丢失且Ctt为1时,CPU从主存中取指,并且把指令缓存到Cache中;Ctt为0时,CPU从主存中取指。②(DCaches,数据Cache)---与ICaches相似,系统刚上电或复位时,DCaches中的内容无效,并且DCaches功能关闭,Write buffer中的内容也是被废弃不用的。往Ccr位(CP15协处理器 中寄存器1的第二位)写1启动DCaches,写0停止DCaches。Write buffer和DCaches紧密结合,额米有专门的控制来开启和停止它。与ICaches不同,DCaches功能必须在MMU开启之后才能被使用。DCaches被关闭时,CPU每次都去内存取数据。DCaches被开启后,CPU每次读写数据时都会先在DCaches中查看是否能找到所要的数据,不管Ctt是0还是1,找到了称为Cache命中,找不到称为Cache丢失。
★使用Cache时需要保证Cache、Write buffer的内容和主存内容一致,保证下面两个原则:①清空DCaches,使主存数据得到更新。②使无效ICaches,使CPU取指时重新读取主存。
在实际编写程序时,要注意如下几点:①开启MMU前,使无效ICaches,DCaches和Write buffer。②关闭MMU前,清空ICaches、DCaches,即将“脏”数据写到主存上。③如果代码有变,使无效ICaches,这样CPU取指时会从新读取主存。④使用DMA操作可以被Cache的内存时:将内存的数据发送出去时,要清空Cache;将内存的数据读入时,要使无效Cache。⑤改变页表中地址映射关系时也要慎重考虑。⑥开启ICaches或DCaches时,要考虑ICaches或DCaches中的内容是否与主存保持一致。⑦对于I/O地址空间,不使用Cache和Write buffer。

四、MMU、TLB及Cache的控制指令
S3C2410除了ARM920T的CPU核心外,还有若干个协处理器,用来帮助主CPU完成一些特殊功能,对MMU、TLB及Cache等的操作就涉及到协处理器。格式如下:
<MCR|MRC>{条件} 协处理器编码,协处理器操作码1,目的寄存器,源寄存器1,源寄存器2,协处理器操作码2
<MCR|MRC> {cond} p#,<expression1>,Rd,cn,cm{,<expression2>}
MRC  //从协处理器获得数据,传给ARM920T CPU核心寄存器
MCR  //数据从ARM920T CPU核心寄存器传给协处理器
{cond}  //执行条件,省略时表示无条件执行
p#  //协处理器序号
<expression1>  //一个常数
Rd  //ARM920T CPU核心的寄存器
cn和cm  //协处理器中的寄存器
<expression2>  //一个常数
其中,<expression1>、cn、cm、<expression2>仅供协处理器使用,它们的作用如何取决于具体的协处理器。


示例代码解析:
开启MMU,并将虚拟地址0xA0000000~0xA0100000映射到物理地址0x56000000~0x56100000(GPFCON物理地址为0x56000050,GPFDAT物理地址为0x56000054);将虚拟地址0xB0000000~0xB3FFFFFF映射到物理地址0x30000000~0x33FFFFFF。本示例以段的方式进行地址映射,只使用一级页表,通过上面内容可知一级页表使用4096个描述符来表示4G空间(每个描述符对应1MB),每个描述符占4字节,所以一级页表占16KB。使用SDRAM的开始16KB存放一级页表,所以剩下的内存开始地址就为0x30004000,这个地址最终会对应虚拟地址0xB0004000(所以代码运行地址为0xB0004000)。

★程序执行主要流程的示例代码。
.text
.global _start
_start:
    bl  disable_watch_dog                   @ 关闭WATCHDOG,否则CPU会不断重启
    bl  mem_control_setup                  @ 设置存储控制器以使用SDRAM
    ldr sp, =4096                                    @ 设置栈指针,以下是C函数调用前需要设好栈
    bl  copy_2th_to_sdram                   @ 将第二部分代码复制到SDRAM
    bl  create_page_table                     @ 设置页表
    bl  mmu_init                                      @ 启动MMU,启动以后下面代码都用虚拟地址
    ldr sp, =0xB4000000                       @ 重设栈指针,指向SDRAM顶端(使用虚拟地址)
    ldr pc, =0xB0004000                        @ 跳到SDRAM中继续执行第二部分代码
halt_loop:
    b   halt_loop

★设置页表。
void create_page_table(void)
{

/*
* 用于段描述符的一些宏定义:[31:20]段基址,[11:10]AP,[8:5]Domain,[3]C,[2]B,[1:0]0b10为段描述符
*/
#define MMU_FULL_ACCESS     (3 << 10)   /* 访问权限AP */
#define MMU_DOMAIN          (0 << 5)    /* 属于哪个域 Domain*/
#define MMU_SPECIAL         (1 << 4)    /* 必须是1 */
#define MMU_CACHEABLE       (1 << 3)    /* cacheable C位*/
#define MMU_BUFFERABLE      (1 << 2)    /* bufferable B位*/
#define MMU_SECTION         (2)         /* 表示这是段描述符 */
#define MMU_SECDESC         (MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | MMU_SECTION)
#define MMU_SECDESC_WB      (MMU_FULL_ACCESS | MMU_DOMAIN | MMU_SPECIAL | MMU_CACHEABLE | MMU_BUFFERABLE | MMU_SECTION)
#define MMU_SECTION_SIZE    0x00100000        /*每个段描述符对应1MB大小空间*/


    unsigned long virtuladdr, physicaladdr;
    unsigned long *mmu_tlb_base = (unsigned long *)0x30000000;        /*SDRAM开始地址存放页表*/
    
    /*
     * Steppingstone的起始物理地址为0,第一部分程序的起始运行地址也是0, 为了在开启MMU后仍能运行第一部分的程序, 将0~1M的虚拟地址映射到同样的物理地址
     */
    virtuladdr = 0;
    physicaladdr = 0;
    /*虚拟地址[31:20]用于索引一级页表,找到它对应的描述符,对应于(virtualaddr>>20)。段描述符中[31:20]保存段的物理地址,对应(physicaladdr & 0xFFF00000)*/
    *(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | MMU_SECDESC_WB;

    /*
     * 0x56000000是GPIO寄存器的起始物理地址,GPBCON和GPBDAT这两个寄存器的物理地址0x56000010、0x56000014, 为了在第二部分程序中能以地址0xA0000010、0xA0000014来操作GPBCON、GPBDAT,
     * 把从0xA0000000开始的1M虚拟地址空间映射到从0x56000000开始的1M物理地址空间
     */
    virtuladdr = 0xA0000000;
    physicaladdr = 0x56000000;
    *(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | MMU_SECDESC;


    /*
     * SDRAM的物理地址范围是0x30000000~0x33FFFFFF, 将虚拟地址0xB0000000~0xB3FFFFFF映射到物理地址0x30000000~0x33FFFFFF上, 总共64M,涉及64个段描述符
     */
    virtuladdr = 0xB0000000;
    physicaladdr = 0x30000000;
    while (virtuladdr < 0xB4000000)
    {
        *(mmu_tlb_base + (virtuladdr >> 20)) = (physicaladdr & 0xFFF00000) | MMU_SECDESC_WB;
        virtuladdr += MMU_SECTION_SIZE;
        physicaladdr += MMU_SECTION_SIZE;
    }
}

★ 启动MMU。
void mmu_init(void)
{
    unsigned long ttb = 0x30000000;


__asm__(
    "mov    r0, #0\n"
    "mcr    p15, 0, r0, c7, c7, 0\n"    /* 使无效ICaches和DCaches */
    
    "mcr    p15, 0, r0, c7, c10, 4\n"   /* drain write buffer on v4 */
    "mcr    p15, 0, r0, c8, c7, 0\n"    /* 使无效指令、数据TLB */
    
    "mov    r4, %0\n"                   /* r4 = 页表基址 */
    "mcr    p15, 0, r4, c2, c0, 0\n"    /* 设置页表基址寄存器 */
    
    "mvn    r0, #0\n"                  
    "mcr    p15, 0, r0, c3, c0, 0\n"    /* 域访问控制寄存器设为0xFFFFFFFF, 不进行权限检查*/    
    /*
     * 对于控制寄存器,先读出其值,在这基础上修改感兴趣的位,然后再写入
     */
    "mrc    p15, 0, r0, c1, c0, 0\n"    /* 读出控制寄存器的值 */
    
    /* 控制寄存器的低16位含义为:.RVI ..RS B... .CAM
     * R : 表示换出Cache中的条目时使用的算法,0 = Random replacement;1 = Round robin replacement
     * V : 表示异常向量表所在的位置,0 = Low addresses = 0x00000000;1 = High addresses = 0xFFFF0000
     * I : 0 = 关闭ICaches;1 = 开启ICaches
     * R、S : 用来与页表中的描述符一起确定内存的访问权限
     * B : 0 = CPU为小字节序;1 = CPU为大字节序
     * C : 0 = 关闭DCaches;1 = 开启DCaches
     * A : 0 = 数据访问时不进行地址对齐检查;1 = 数据访问时进行地址对齐检查
     * M : 0 = 关闭MMU;1 = 开启MMU
     */
    
    /*  
     * 先清除不需要的位,往下若需要则重新设置它们    
     */
                                        /* .RVI ..RS B... .CAM */
    "bic    r0, r0, #0x3000\n"          /* ..11 .... .... .... 清除V、I位 */
    "bic    r0, r0, #0x0300\n"          /* .... ..11 .... .... 清除R、S位 */
    "bic    r0, r0, #0x0087\n"          /* .... .... 1... .111 清除B/C/A/M */


    /*
     * 设置需要的位
     */
    "orr    r0, r0, #0x0002\n"          /* .... .... .... ..1. 开启对齐检查 */
    "orr    r0, r0, #0x0004\n"          /* .... .... .... .1.. 开启DCaches */
    "orr    r0, r0, #0x1000\n"          /* ...1 .... .... .... 开启ICaches */
    "orr    r0, r0, #0x0001\n"          /* .... .... .... ...1 使能MMU */
    
    "mcr    p15, 0, r0, c1, c0, 0\n"    /* 将修改的值写入控制寄存器 */
    : /* 无输出 */
    : "r" (ttb) );
}



1 页面

      分页存储管理是将作业的逻辑地址划分为一系列同等大小的部分,称为页。并为各页加以编号,每个作业的页的编号都是从0开始的。与之类似, 把可用的物理内存也划分为同样大小的连续的部分,称为块或页框。同样为块也进行标号,从0#开始。 在为进程分配内存空间时,以页为单位,每个内存中的块存放一页用户作业。只要内存中有足够多的块,这些块可以相邻也可以不相邻,就可以存放整个作业了。


    页面的大小对于内存利用和系统开销来说非常重要,页面太大,在作业的最后一页必然会剩余较大不能利用的空间-- 内碎片。页面太小,虽然可以减小内碎片的大小,但是一个作业的页太多,会使得作业页表太长而占用内存,同时系统频繁地进行页面转化,加重系统开销。因此, 页面的大小应该适中,通常为512B - 8KB,windows系统的页面大小为 4KB




2 地址结构


   分页系统中的地址结构由两部分组成,页号和页内偏移量。可以解释为一个二元组(p,w),其中p是页号,w是页面p中的偏移量或者相对于p页开始的位置。 下图(a) 中的地址长度为32位,其中0 - 9位为页内偏移量,每页的大小为2的10次方 = 1k;10 - 31位为页号,共计2的22次方 = 4M页。在图(b)中,地址长度同样为32位,其中0 - 11位页内偏移量,每页的大小为2的12次方 = 4k;12 - 31位为页号,共计2的20次方 = 1M页,由此可知不同的系统页的大小是不一样的。





页面与页表 <wbr>与 <wbr>段


                          (a)页面大小为1KB(2的10次方)

页面与页表 <wbr>与 <wbr>段
                          (b)页面大小为4KB(2的12次方)


    对于特定的机器来说,其地址结构是一定的。若给定逻辑地址A,页面大小为L,则页号p和页内偏移量w分别为


p = INT [A/L]               w = [A]MODL


例如:系统的页面大小事1K,设A = 3096,则由上式得出 p =3,w =24。






3 页表


    在分页存储管理中,页的存放可以是连续的,也可以是不连续的,这就增加了逻辑地址到物理地址转换的难度。 如何在内存中找到页所对应的物理块是地址装换的关键。为此, 系统为每个进程创建了一个页表。在进程逻辑地址空间中的每一页,依次在页表中有一个表项,记录了该页对应的物理块号。如下图所示




      页面与页表 <wbr>与 <wbr>段


    在配置了页表之后 ,通过查找页表就可以很容易地找到该页在内存中的位置。页表具有逻辑地址到物理地址映射的作用。


    对于页的保护通常设置一个存取控制字段。当这个字段占一位时,用于规定该页中的内容允许写还是读;如果存取控制字段占两位,那么它可以表示存取控制为读写、只读和只运行三种。当进程写一个只读页时,系统就会通过中断来报错。




二 地址变换结构


    为了实现分页管理逻辑地址到物理地址的转换,系统中必须设置地址变换机构,用来实现地址映射。由于页的大小和块的大小是一样的,当把进程的某一页放入内存时,该页内地址的页偏移量和块内偏移量是一致的,因此地址转换时就不必考虑偏移量,只考虑逻辑页号和实际物理号的对应即可。页表中存放的就是页号和其对应的物理块号,所以地址变换就要借助页表来完成。




1 基本地址变换


      地址变换的第一步就是检索页表。为了实现快速的检索页表,最好把页表放在寄存器中,每一个表项都用一个寄存器。但是有一个问题,通常计算机中的寄存器都不多,而页表可能非常大,现代计算机的虚拟地址至少是32位的,比如,页的大小为4KB,那么32位的地址空间将有1M个页面,64位的地址空间则更多。虚拟空间中的1M个页面需要1M个表项。并且,每个进程都有自己的页表。因此,页表通常存放在内存中。在系统中只设置一个页表寄存器,其中存放页表的开始地址和页表长度。平时进程未执行时,页表的开始地址和页表的长度放在PCB中,当进程运行时,把这两个数据装入页表寄存器中。


    当进程要访问某个地址中的数据时,地址变换机构首先自动地将地址转换成页号和页内偏移量,然后根据页号来检索页表。在检索之前要判断页号是否大于等于页表长度,如果页号大于等于页表长度,说明超出了有效地址范围,于是产生一个错误中断。否则,把页号和页表项长度相乘得到的结果与页表开始地址相加,就得到了该页表项在页表中的地址,从而找到对应的物理块号,把物理块号装入物理地址寄存器中,同时把页内偏移量送入物理地址寄存器对应的块内偏移量中,由此得到真正的物理地址。




    由于页表是放在内存中的,那么一次数据访问需要两次访问内存,第一次访问页表,找到对应的物理号,然后与偏移量拼接形成物理地址;第二次从第一次得到的物理地址结构中访问数据。系统的运行速度一般都受到CPU从内存中取得指令和数据的速率的限制,一次数据两次访问内存会使计算机的处理速度降低50%。如何有效的解决这个问题?


    采取的解决方法是在地址变换结构中增加一个具有并行查找能力的特殊的高速缓冲寄存器,这种设备称为转换检测缓冲区,又称为快表,用于存放当前访问过的页表项。此时,当给出一个有效地址时,地址变换机构首先通过将该页号p同TLB中的所有表项同时进行比较,判断该表是否在其中,如果发现可匹配的页面,则直接取出其页表项得到物理块号,而不必通过页表。如果地址变换机构没有可匹配的项,就进行正常的页表查询。首先从TLB中淘汰一个表项,然后用新找到的页表项替换它。这样,如果这一页很快再次被访问,那么第二次自然将会命中。


    因为寄存器的价格原因,快表的结构不可能很大,通常能存放16 - 512个页表项,这对中小型作业来说,有可能把全部页表放入快表中,对于大型作业,可以将常用的页表项放入其中。由于程序的局部性原则,快表的引入极大改善了系统的效率,数据显示,从快表中查找到页表项的概率可以达到90%。这样因为访问页表而访问内存的次数就会大大减少,从而降低系统的开销。




  2 多级页表


    现代的计算机都有非常大的逻辑地址空间,以32位计算机为例,假设页的大小为4KB,那么一个作业的页最多可以达到2的20次方个,这意味着该作业的 页表现为2的20次方。假设一个页表现占用一个字节,那么该页表的大小为2的20次方B,即需要1MB的内存空间。并且要求者1MB的内存空间是连续的。这显然是不现实的,解决这个问题的最好方法是:把页表页看成普通的文件,对它进行离散分配,即对页表再分页,由此形成多级页表的思想。


    以二级页表为例,将页表进行分页后,离散的存放在不同的物理块中,这样,对这些离散分配的页表再建立页表,即二级页表。在下图中,32位的虚地址划分成10位的外层页表域,10位的内层页表域和12位的页内偏移量。


     


      页面与页表 <wbr>与 <wbr>段




    32位逻辑地址空间使用两级页表映射到32位物理地址空间,每个页面大小为4KB。一级页表的开始物理地址被存放在页表基址寄存器中,一级地址映射使用逻辑地址的最高10位来索引,并产生第二级页表的物理地址。下面10位用来索引第二级页表,产生出的物理页的地址和逻辑地址的最低12位相结合以生成物理地址。如果某二级页表中没有实际映射,就可将其删除并在顶级页表中标记为不可用。许多分页方案在构造的时候都使各级页表的大小和页的大小一致,这样存储它们占用的空间可以和进程使用的内存页使用相同的分配方案。


    对于32位的机器,采用二级页表是合适的;但对于64位的机器,采用二级页表是不合适的,因此必须采用多级页表。


    分页存储管理方式虽然可以解决程序和数据无需连续存储空间的问题,但是这种内存管理方式依然要求整个作业都要装入内存运行,既没有解决大作业小内存的问题,此外,该种管理方式纯粹从存储的角度去考虑,没有考虑到程序本身的存储问题。


   


  说明总结:单页时,由于页表项是连续的(对应着物理地址空间),造成必须占用一整块连续地址空间,是不可取的,所以采用多页表(主要是二级页表),但造成整个逻辑块被分散。 






基本分段分配方式


    分页存储管可以实现内存利用率的提高,但是分页的纯物理解决方案,一个逻辑段被离散的放在很多个物理块中。很多时候,程序员希望把一个程序按照它的逻辑结构存放在内存中。


    一个程序的逻辑段在程序运行过程中有的大小会发生变化,如数据段和堆栈;而有的逻辑段的大小在运行过程中不发生变化,如代码段。在分页存储管理方式中,对于随时动态增长的段的存储管理是非常困难的,一旦断的增长涉及重新分配物理块,那么就涉及页表的修改等问题。


    一个具有n个过程的程序,在分页存储管理中,过程被一个一个紧紧地放在一起,中间没有间隙,结果是修改一个过程的大小会影响其他进程的起始地址,进而又需要修改所有调用被移动进程的进程,以使它们的访问指向这些过程的新地址。在一个有数百个过程的程序中,这个操作的开销是相当大的。


    在分页存储管理中,一个逻辑段可能存放在n个物理块中,如果几个程序共享这个逻辑段就需要早每个程序的页表中添加n项,以便实现地址转换。这样势必会增加页表的大小,进而增加系统的开销,对于逻辑页的保护也是同样的道理。


    针对这些问题,一个非常通用的办法是为一个程序提供多个相互独立的称为段的地址空间。每个段由一个从0到最大的线性地址序列构成。各个段的长度可以是0到某个允许的最大值之间的一个值。不同段的长度可以不同,而且通常也不同。段的长度在运行期间可以改变,堆栈段的长度在数据被压入时会增长。在数据被弹出时又会减小。


    因为每个段都是一个独立的空间,它们可以独立地增长或减小而不会影响到其他的段。段是一个逻辑实体,一个段可能包括一个过程,一个数组,一个堆栈,一组数值变量,但一般它不会同时包含多种不同类型的内容。分段分配方式实现段长度的扩充,段的动态链接以及段的保护和共享都比页式存储容易实现。


   


1 段表


    在分段存储管理中,逻辑地址结构是二维的,即段号和段内偏移量。如下图所示。在该地址结构中,允许一个作业最长有64K个段,每个段的最大长度为64KB。


   


    页面与页表 <wbr>与 <wbr>段


     


    在分段分配方式中,系统为每个分段分配一个连续的分区,进程中的各个段可以离散地装入内存中的不同分区中。为了实现逻辑地址到物理地址的转换,在系统中为每个进程建立一张段表。每个段在段表中都占有一个表项,其中记录了该段的起始地址和段的地址,如图所示,段表可以存放在一组寄存器中,这样有助于提高地址转换速度;但更常见的是放在内存中。在配置了段表之后,执行中的进程可以通过查找段表找到每个段所对应的内存区。


     


      页面与页表 <wbr>与 <wbr>段


2 采用大小不等的段,逻辑地址和物理地址之间不再是简单的对应关系。考虑一个 n+m 位的地址,左边的n位为段号,右边的m位为段内地址。当进程进入运行状态时,它的段表地址被装入到段表寄存器中。     




一、MMU的介绍

MMU全称Memory Management Unit,中文称内存管理单元

主要有两个功能:
        A.将虚拟地址转换成实际的物理地址
        B.对物理内存设置访问权限

二、MMU的工作过程

在s3c2410中MMU是由协处理器(cp15)控制的,s3c2410/s3c2440最多会用到两级页表:以段(Section,1MB)的方式进行转换时只用到一级页表,以页(page)的方式进行转换时用到两级页表。页的大小有3种:大页(64KB),小页(4KB),极小页(1KB)。

明确一个概念:
        条目也称为"描述符"(Descriptor),有:段描述符,大页描述符,小页描述符,极小页描述符----它们保存段、大页、小页或极小页的起始物理地址;粗页表描述符、细页表描述符---他们保存二级页表的物理地址

转换过程如下:
        (1) 根据给定的虚拟地址找到一级页表中的条目
        (2)如果此条目是段描述符,则返回物理地址,转换结束
        (3)如果此条目是二级页表描述符,继续利用虚拟地址在二级页表中找到下一个条目;
        (4)如果这第二个条目是叶描述符,则返回物理地址,转换结束;
        (5)其他情况出错

注意:这里面所有的转换过程都是由MMU完成的

以段的方式映射实例说明:

例如:虚拟地址 0xa0004000
        注意:当MMU打开以后,所有的地址都会被MMU拦截,然后将其转换,cpu是不管虚拟地址还是实际物理地址的。

转换如下:

先来看看TTB

简单的来说,它保存了一级页表所存放的实际物理地址,要求16KB对齐,以段的方式映射,4GB的虚拟地址空间,需要段描述符4096个(每个段描述符映射1M空间),没个描述符占用4byte,所以一段的方式映射一级页表占用的空间为16KB。

在这里我们假设,我们的一级页表存放在物理地址:0x30000000.

第一步:
        获得虚拟地址所对应的段描述符所在的地址
        addr = TTB&0xffffc000 | ((viraddr >> 20) << 2 ) = 0x30000000 & 0xfffc000 | ((0xa0004000 >> 20) << 2)= 0x30000000 | (0xa00 << 2) = 0x30002800

第二步:
        从0x30002800取出虚拟地址所对应的段描述符

段描述的构造我们到后面再来讲解,这里我们假设我们把0xa0004000映射到实际的物理地址0x30004000,则这里的[31:20]为0x300

第三步:
        组合成实际的物理地址

phyaddr = 0x300 << 20 | (0xa0004000 & 0xfffff) = 0x30004000

三.实验
        目标:以段的方式映射s3c2410的地址空间,一级页表存放在0x30000000

流程:
        A.计算每个虚拟地址对应段描述符所在的地址(addr),方法如下:

B.构造段描述符

注意:Section base address 存放的是实际的物理地址的[31:20]

C.存放段描述符
        (unsigned int *)addr = section descriptor

D.使能MMU

整个流程比较复杂的就是段描述符的构造,具体的流程大家可以直接看芯片手册,写的很详细

实例代码:

/*Nand 启动sdram的起始地址*/
        #define SRAM_START_ADDR        0x00000000
        /*内存空间地址*/
        #define VMRAM_ADDR_START        0xa0000000
        #define SDRAM_ADDR_END 0x34000000
        /*IO空间地址*/
        #define VMIO_ADDR_START 0xb0000000
        #define PHIO_ADDR_START 0x56000000
        #define PHIO_ADDR_END 0x56010000
        /*用SDRAM起始地址开始的16KB,存放页表*/
        #define PAGE_TABLE_BASE 0x30000000
        /*MASK*/
        #define PAGE_TABLE_BASE_MASK 0xffffc000
        #define VIRADDR_MASK 0xfff00000
        #define PHYADDR_MASK 0xfff00000
        /*页表项内容*/
        #define PAGE_TABLE_SECTION_AP (0x01 << 10)
        #define APGE_TABLE_SECTION_DOMAIN (0x0 << 5)
        #define PAGE_TABLE_SECTION_CACHE_WB (0x0 << 2)
        #define PAGE_TABLE_SECTION_4BIT (1 << 4)
        #define PAGE_TABLE_SECTION_TYPE (0x2)
        /*段大小*/
        #define SECTION_SIZE 0x100000
        //根据虚拟地址和页表基地址确定页表项所在的物理地址

unsigned int get_pgtindex_addr(unsigned int viraddr,unsigned int pgtaddr)
        {
        unsigned int addr;
        /*[31:14]页表基地地址
        *[13: 2]虚拟地址>>20位得到的page index
        *[1 : 0]总是为0,因为每一项占用4byte
        */
        addr = (pgtaddr & PAGE_TABLE_BASE_MASK) | (((viraddr & VIRADDR_MASK) >> 20) << 2);
        return addr;
        }

//获取页表项

unsigned int get_page_entry(unsigned int phyaddr)
        {
        unsigned int entry_value;
        /*[31:20]section base address
                *[19:12]
                *[11:10]AP
                *[9]
                *[8:5]Domain
                *[4]:1
                *[3]:C
                *[2]:B
                *[1:0]:Type
                */
        entry_value = (phyaddr & PHYADDR_MASK) | PAGE_TABLE_SECTION_AP |\
        PAGE_TABLE_SECTION_CACHE_WB | PAGE_TABLE_SECTION_4BIT|\
        PAGE_TABLE_SECTION_TYPE;
        return entry_value;
        }

/*创建一级页表:段描述符*/

void create_page_table()
        {
        int i;
        unsigned int pgt_index_addr;
        unsigned int viraddr,phyaddr,pgtaddr;
        /*我们代码的起始运行地址0x00000000在这里需要注意的是:当我们开启MMU后,cpu发出的地址都会被MMU拦截,要想程序正常运行,pc所用的的地址必须是虚拟地址。然而此时cpu执行下一条指令实际运行的地址是物理地址,但是MMU会将此物理地址当作虚拟虚拟地址处理。晕,乱套了。为了解决这个问题,我们通常的做法是,让开启MMU的附近地址指令的虚拟地址和物理地址空间做一个等价的映射。在这里我们将0x00000000开始的1M物理空间映射到0x00000000开始的虚拟地址空间。*/
        phyaddr = SRAM_START_ADDR;
        viraddr = phyaddr;
        pgtaddr = PAGE_TABLE_BASE;
        pgt_index_addr = get_pgtindex_addr(viraddr,pgtaddr);
        *(volatile unsigned int *)pgt_index_addr = get_page_entry(phyaddr);
        #if 1
        /*映射64MSDRAM*/
        for(phyaddr = SDRAM_ADDR_START,viraddr = VMRAM_ADDR_START;\
        phyaddr < SDRAM_ADDR_END;phyaddr += SECTION_SIZE,\
        viraddr += SECTION_SIZE)
        {
                pgtaddr = PAGE_TABLE_BASE;
                pgt_index_addr = get_pgtindex_addr(viraddr,pgtaddr);
                *(volatile unsigned int *)pgt_index_addr = get_page_entry(phyaddr);
        }
        #endif
        /*映射IO地址空间*/
        phyaddr = PHIO_ADDR_START;
        viraddr = VMIO_ADDR_START;
        pgtaddr = PAGE_TABLE_BASE;
        pgt_index_addr = get_pgtindex_addr(viraddr,pgtaddr);
        *(volatile unsigned int *)pgt_index_addr = get_page_entry(phyaddr);
        return;
        }

/*
        Care must be taken if the translated address differs from the
        untranslated address as several instructions following the
        enabling of the MMU may have been prefetched with the MMU off
        (using physical = virtual address - flat translation) and enabling
        the MMU may be considered as a branch with delayed execution. A similar
        situation occurs when the MMU is disabled. Consider the following code
        sequence:
        MRC    p15, 0, R1, c1, C0, 0: Read control rejectio
        ORR    R1, #0x1
        MCR    p15,0,R1,C1, C0,0 ; Enable MMUS
        Fetch    Flat
        Fetch    Flat
        Fetch    Translated
        */

void init_mmu()
        {
        unsigned long mmu_table_base = PAGE_TABLE_BASE;
        asm(
                /*set Translation Table Base(TTB) register*/
                "mrc p15,0,r0,c2,c0,0\n"
                "mov r0,%0\n"
                "mcr p15,0,r0,c2,c0,0\n"
                /*set Domain Access Control register*/
                "mrc p15,0,r0,c3,c0,0\n"
                "mvn r0,#0\n"
                "mcr p15,0,r0,c3,c0,0\n"
                /*Enable MMU*/
                "mrc p15,0,r0,c1,c0,0\n"
                "orr r0, #0x1\n"
                "mcr p15,0,r0,c1,c0,0\n"
                "mov r0,r0\n"
                "mov r0,r0\n"
                "mov r0,r0\n"
                :
                :"r"(mmu_table_base)
                :"r0"
                );
        return;
        }
        start.S
        .text
        .global _start
        _start:

#define pWTCON 0x53000000
        #define  CLKDIVN  0x4c000014
        #define  MPLLCON  0x4c000004
        #define  MEMBASE  0x48000000
        #define  SRAM_2_ADDR  2048
        #define  SDRAM_2_ADDR  0x30004000
        #define  SRAM_SIZE  4096

start_code:
        @set the cpu to SVC32 mode
        mrs r0,cpsr
        bic r0,r0,#0x1f
        orr r0,r0,#0xd3
        msr cpsr,r0

@打开指令cache
        mrc p15,0,r0,c1,c0,0
        @orr r0,r0,#0x1000
        mcr p15,0,r0,c1,c0,0

@设置栈指针位置
        ldr sp,=4096
        @关看门狗
        bl disable_watchdog
        @初始化系统时钟
        bl init_sys_clock
        @初始化内存
        bl init_sdram
        @拷贝SRAM的代码到SDRAM
        bl copy_to_sdram
        @创建页表
        bl create_page_table
        @启动MMU
        bl init_mmu
        @运行led程序
        ldr sp,=0xa3000000 @重设sp指针,mmu之后,@cpu操作的地址都是虚拟地址
        ldr pc,_main
        halt_loop:
        b halt_loop
        _main:
        .word main
        disable_watchdog:
        @关看门狗,不然cpu会不断重启
        ldr r0,=pWTCON
        mov r1,#0
        str r1,[r0]
        mov pc,lr
        init_sys_clock:
        @目前为止,cpu工作在12MHZ频率下
        @提升cpu工作频率FCLK:HCLK:PCLK=1:2:4
        ldr r0,=CLKDIVN
        mov r1,#3
        str r1,[r0]
        @ifHDIVN=1,must asynchronous buf mode
        mrc p15,0,r0,c1,c0,0
        orr r0,r0,#0xc0000000
        mcr p15,0,r0,c1,c0,0
        @设置MPLL,使cpu工作在202.80MHZ
        ldr r0,=MPLLCON
        ldr r1,=0x000a1031
        str r1,[r0]
        mov pc,lr

copy_to_sdram:
        ldr  r0,=SRAM_2_ADDR  @第二阶段代码起始地址(2048)
        ldr  r1,=SDRAM_2_ADDR  @第二阶段代码存放的物理地址(0x30004000)

1:
        ldr  r2,[r0],#4
        str  r2,[r1],#4
        cmp  r0,#SRAM_SIZE
        bne  1b
        mov  pc,lr
        init_sdram:
        @初始化sdram
        ldr  r0,=MEMBASE  @13个寄存器的首地址
        adrl  r1,SMRDATA  @13个寄存器值存放的地址
        mov  r2,#52 @13 * 4 = 52
        add  r2,r2,r1

1:
        ldr  r3,[r1],#4
        str  r3,[r0],#4
        cmp  r1,r2
        bne  1b
        /*every thing is fine now*/
        mov  pc,lr
        .ltorg  @声明一个数据缓冲池的开始
        SMRDATA:
        .word  0x2201d110        @BWSCON 设置BANK3位宽16,使能nWait,使能UB/LB
        .word  0x0700 @BANKCON0
        .word  0x700 @BANKCON1
        .word  0x700 @BANKCON2
        .word  0x700 @BANKCON3
        .word  0x700 @BANKCON4
        .word  0x700 @BANKCON5
        .word  (3 << 15) + (1 << 0) @BANKCON6
        .word  0x18001 @BANKCON7
        .word  (1 << 23) + (2 << 18) + (1256 << 0) @REFRESH
        .word  (1 << 7) + (1 << 0) @BANKSIZE
        .word  (3 << 4) @MRSRB6
        .word  (3 << 4) @MRSRB7

led.c
        //#include "s3c2410.h"
        /*虚拟地址*/
        #define GPFCON        (*(volatile unsigned long *) 0xb0000050)
        #define GPFDAT        (*(volatile unsigned long *) 0xb0000054)
        //初始化
        static inline void led_init()
        {
        //GPFCON -> [8:15]清零
        GPFCON &= ~(0xff << 8);
        //GPF4 GPF5 GPF6 GPF7设为输出模式
        GPFCON |= 0x55 << 8;
        //输出高低平,关闭四路LED灯
        GPFDAT |= 0xf << 4;
        return;
        }
        //关闭LED
        static inline int led_off()
        {
        GPFDAT |= 0xf << 4;
        return 0;
        }
        //延时函数
        static inline int delay_time(int time)
        {
        int i,j;
        //让两个for循环作为延时
        for(i = 0;i < time;i ++)
        for(j = 0;j < time;j ++);
        return 0;
        }
        //流水灯
        static inline int run_water_led(int count)
        {
        int i = 0;
        while(count --)
        {
        led_off();
        delay_time(500);
        for(i = 4;i < 8;i ++)
        {
                GPFDAT &= ~(0x1 << i);
                delay_time(500);
                }
        }
        return 0;
        }

int main()
        {
                led_init();
                run_water_led(5);
                led_off();
                delay_time(5000);
                return 0;
        }
        Makefile:
        led.bin:start.S led.c
        arm-none-linux-gnueabi-gcc -c start.S -o start.o
        arm-none-linux-gnueabi-gcc -c mmu.c -o mmu.o
        arm-none-linux-gnueabi-gcc -c led.c -o led.o
        #arm-none-linux-gnueabi-ld -Ttext 0x00000000 start.o led.o -o led_elf
        arm-none-linux-gnueabi-ld -Tmap.lds start.o mmu.o led.o -o led_elf
        arm-none-linux-gnueabi-objcopy -O binary -S led_elf led.bin
        cp led.bin /tftpboot
        clean:
        rm -rf *.o led_elf led.bin
        连接脚本(map.lds)
        OUTPUT_FORMAT("elf32-littlearm", "elf32-littlearm", "elf32-littlearm")
        /*OUTPUT_FORMAT("elf32-arm", "elf32-arm", "elf32-arm")*/
        OUTPUT_ARCH(arm)
        ENTRY(_start)
        SECTIONS
        {
                firtst 0x00000000:
                {
                        start.o
                        mmu.o
                }
                second 0xa0004000:
                AT(2048)
                        {
                                led.o
                        }
        }




  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值