一.虚拟地址空间
地址空间的最大长度与实际可用的物理内存数量无关,因此被称为虚拟地址空间(Virtual Address Space)。这个虚拟地址空间的大小是由计算机的硬件平台决定的,具体来说是由CPU的位数决定的。硬件决定了地址空间的最大理论上限,即硬件的寻址空间大小,比如主流的32位处理器(IA32,MIPS,ARM)等能寻址2^32B,即4GB的大小的地址空间(0~0xFFFF FFFF)。
Linux将虚拟地址空间划分为两个部分:内核空间和用户空间。系统中每个用户进程都有自己的虚拟地址范围,从0到TASK_SIZE。通常取TASK_SIZE=0xC000 0000,即4GB的底部3GB(0x0000 0000 ~ 0xBFFF FFFF)留给用户空间的进程使用的,顶部1GB(0xC000 0000 ~ 0xFFFF FFFF)留给内核使用。
对于Windows操作系统来说,它的进程虚拟地址空间划分是操作系统占用2GB,用户进程只剩下2GB。
二.MMU
那么虚拟内存的底层内存管理机制是怎样的呢?当我们自底向上思考时,关注的主要硬件为TLB(Translation Lookaside Buffer,翻译后备缓冲)。TLB为虚拟地址(VA)到物理地址(PA) 转换的硬件机构,它是虚拟存储的硬件基础。当我们自顶向下思考时,该硬件通常称为MMU(Memory Management Unit,内存管理单元)。
虚拟存储的实现需要依靠硬件的支持,对于不同的CPU来说是不同的。但是几乎所有的硬件都采用一个叫MMU的部件来进行页映射,如图1-7所示。
在页映射模式下,CPU发出的是Virtual Address,即我们的程序看到的是虚拟地址。经过MMU转换以后就变成了Physical Address。具体来说,TLB表中的每个表项有一个页的虚拟地址(VPN代表虚拟页号)和一个物理地址(PFN代表物理帧号)。当一个程序发出一个虚拟地址后,它与TLB中的每个VPN进行比较,如果它匹配了一个表项,对应的PFN就发出。
一般MMU都集成到了CPU内部了,不会以独立的部件存在。
三.MIPS基本地址空间
在MIPS CPU里,你的程序中使用的地址绝对不会和芯片里的物理地址一样(有可能会很接近,但不会相同)。我们分别称为:程序地址和物理地址(Physical Address)。这里所讲的程序地址的含义同虚拟地址(Virtual Address)完全相同,不过不会牵扯到操作系统内存管理(进程)语境下的复杂性。
MIPS CPU可以运行在两种特权级别上:用户态和核心态(R4000之后的MIPS CPU有第三种监管者模式)。我们经常提到用户模式和核心模式,需要明确的是用户程序的运行处于操作系统的监管之下,进程的虚拟空间都在操作系统的掌握之中。从核心态切换到用户态,MIPS CPU做的工作并没有不同,只是有时是非法的。在用户态,任何一个程序地址的首位是非法的,就会引起陷阱异常。另外,在用户态下,一些指令也会引起陷阱。
如果2.1所示,在32位视图中,程序地址空间划分为4个大区域,分别用一个传统的名字(完全没有具体含义)命名。地址处在不同的区域,会有不同的属性,如下所示:
1.kuseg: 0x000 0000 – 0x7FFF FFFF (低端2G)
这些是用户模式下可用的地址,即MIPS规范约定用户空间为2G。在带有MMU的机器里,这些地址都将由MMU转换。除非已经设置好MMU,否则不要使用这2G 地址。
对于没有MMU 的机器,对这2G 地址的操作由具体实现所决定。CPU使用手册会告诉你关于这方面的信息。如果你希望编写的代码具有兼容性,可以在缺少MMU 的MIPS处理器之间移植,尽量避免使用这块区域。
2.kseg0: 0x8000 0000 – 0x9FFF FFFF(512M)
只需要把最高位清零(&0x7fffffff),这些地址就被转换为物理地址,然后把它们连续地映射到物理内存的低端512M(0x0000 0000 – 0x1FFF FFFF)空间。因为这种映射是很简单的,不需要MMU转换,通常把这些地址称为“非翻译无需转换的”(Unmapped)地址区域。
对这段地址的存取都会通过高速缓存(cached)。因此在缓存(cache)未进行初始化之前,不要使用这段地址。通常在没有MMU的系统中,这段空间用于存放大多数程序和数据。对于有MMU 的系统,操作系统的内核会存放在这个区域。
3.kseg1: 0xA000 0000 – 0xBFFF FFFF(512M)
通过将最高3位清零(&0x1fffffff)的方法来把这些地址映射为相应的物理地址,与kseg0 映射的物理地址一样,都映射到物理内存的低端512M(0x0000 0000 – 0x1FFF FFFF)空间,也是“非翻译无需转换的”(Unmapped)地址区域。但要注意,kseg1不使用缓存(Uncached)。
kseg1是唯一的在系统重启时能正常工作的内存映射地址空间,这也是为什么重新启动时的入口向量是(0xBFC0 0000)会在这个区域。这个向量对应的物理地址是0x1FC0 0000。
因此你可以使用这段地址空间来访问你的初始化程序的ROM。还有大多数人把它用来访问I/O 寄存器。如果你的硬件工程师要把这段地址映射到非低端512M 空间,你应该试图说服他们。
4.kseg2: 0xC000 0000 – 0xFFFF FFFF (1G)
这段地址空间只能在核心态下使用并且要经过MMU转换。在MMU 设置好之前,不能存取这段区域。除非你在写一个真正的操作系统,一般来说你不需要使用这段地址空间。
有时,你会看到这段地址空间被分成两等分,并称之为kseg2和kseg3,要着重指出的是其中的低半部分(kseg2)对于运行在监管者模式可用。
由于kseg0和kseg1用于操作系统分配外设I/O地址,加上kseg2的1G空间,故MIPS规范约定内核空间为2G。
四.简单系统寻址
MIPS的程序地址很少与真正的物理地址一致。但对于简单的嵌入式软件而言,在使用kseg0和kseg1这两段区域内的地址时,其程序地址和物理地址之间的关系是很密切的。
大多数的简单系统会对512MB以下的空间完全映射,对于MIPS 平台上的物理地址空间(>=32位),工业界长期形成的惯例是将0x0000 0000 ~ 0x1000 0000给内存(512M的前256M映射到kseg0),而将0x1000 0000 ~ 0x1FFF FFFF留给I/O(512M的后256M映射到kseg1,用作I/O)。可参考AR9331 Datasheet中的2.3 AR9331 Address MAP实例。从物理地址0x2000 0000(512MB)以上的地址空间不能被映射到任何地址,但是在需要时,可以通过在MMU内设置转换入口(TLB)的方式,或者使用64位CPU中的一些额外空间对较高的地址进行访问。
在核心特权下(CPU启动时),CPU可以做任何事情。在用户模式下,程序地址超过2GB(首位设置)之上是非法的,将会产生一个陷阱。如果一个CPU具有MMU,这意味着所有的用户地址在到物理内存之前必须经过MMU的转换,从而使得OS可以防止用户程序随便乱用空间。当然,这也意味着,对于一个MIPS CPU,其运行的OS如果没有使用内存映射,用户态优先级其实是多余的。
当系统内存大于256MB 时,kseg0的空间不够用,则需要启用High Memory机制,使用kseg2的空间,通过TLB建立映射去访问。对64 位情形,则不存在这个问题,因为64位Linux使用xkphys区域访问物理内存,cached和uncached 区域与整个物理地址空间一样大,足够用矣。
五. MIPS地址空间若干问题
(1)ROM/Flash启动地址
MIPS 下TLB、Cache都要OS参与管理,在其启动时OS尚未接管系统,这个时候不采用TLB、Cache机制是很重要的。在系统加电后,MIPS CPU将首先执行Boot Loader程序,第一条指令就映射在地址空间的0xBFC0 0000处。MIPS24K 起始地址改到了0xBF000000,从而使ROM/Flash空间从4MB提高到16MB。这个入口向量即ROM/Flash起始(物理)地址0x1FC00000在kseg1地址段的映射。
在AR9331 Datasheet(The AR9331 integrates an embedded MIPS 24Kc processor)中的2.2 Configuration中提到:
==============================================================
Upon reset, the CPU puts out an address of 0xBFC00000 which is mapped to the flash address space or internal ROM code,using an external pull up/down register to choose if the AR9331 will boot from the Flash or internal ROM.
==============================================================
(2)I/O设备寄存器
MIPS处理器一般把外设映射到虚拟地址0xA0000000-0xBFFFFFFF之间非缓存的kseg1所对应的512MB地址空间中。
I/O设备寄存器:似乎很显然,但还会值得再指出:MIPS没有专门的输入输出指令,所有设备寄存器都映射到某个地址空间,如果你将该空间设置为缓存的,会发生奇怪的事情。
==============================================================
I/O device registers: Perhaps obvious, but worth pointing out. MIPS has no dedicated input/output instructions, so all device registers must be mapped somewhere in the address space, and very strange things will happen if you accidentally let them be cached.
==============================================================
AR9331 Datasheet中的General Purpose I/O Output Enable(GPIO_OE)寄存器物理地址为0x18040000,则程序在读写GPIO_OE寄存器时,需要先调用宏KSEG1ADDR将物理地址转换到非缓存的kseg1地址段。
由于设备寄存器都映射到某个地址空间(ROM、RAM、I/O统一编址), 对于完全运行在核心模式下(未开启MMU)的简单嵌入式系统(例如实时VxWorks操作系统), 我们甚至可以增加串口命令mm(memory modify),然后通过mm命令修改某些I/O寄存器的值来实现即时调试。
(3)DMA描述符数组(环)
DMA描述符数组:·复杂的DMA控制器使用内存中小的数据描述符和CPU共享设备的控制/状态信息。通常CPU创建一系列待传输的信息,并告诉DMA控制器开始工作。如果你的系统使用描述符,需要将它们存放在非缓存的kseg1区域。
==============================================================
DMA descriptor arrays: Sophisticated DMA controllers share control/status information with the CPU using small descriptor data structures held in memory. Typically, the CPU uses these to create a long list of information to be transferred, and only then tells the DMA controller to begin its work. If your system uses descriptors,you’ll want to access the memory region that contains them through an uncached address region.
==============================================================
DMA描述符数组(DMA Descriptor Array)本身是一个指针数组(形如int32* hw_desc[DESC_NUM]),hw_desc[i]指向由硬件定义的描述符(h/w descriptor)。hw_desc[]地址需要配置到SoC芯片相应的寄存器中,例如AR9331 Datasheet中的DMARXDESCR(Pointer to RxDescrpitor)和DMATXDESCR_Q0(Descriptor Address for Queue 0 Tx)。很显然,这个数组需要分配或转换到非缓存的kseg1区域。
(4)主内存和外设都只能映射在512MB的地址空间中?
“主内存和外设都只能映射在512MB的地址空间中”的解释:主内存一般映射到0x80000000-0x9FFFFFFF的通过cache的512M空间中,外设则是映射到0xA0000000-0xBFFFFFFF的不通过cache的512M空间中。当然,上面说的两个512M空间都是MIPS的虚拟内存空间,它们的虚拟地址直接减去一个偏移得到的物理地址,是指向同一块512M的物理空间。所以说“主内存和外设都只能映射在512MB的地址空间中”这句话是没有错的,只是说的比较隐晦。
(5)两个虚拟地址映射到一个物理地址?
这种映射关系是不会冲突的,实际上使用时并不会真的用两个不同的虚地址去映射同一个物理地址,因为这两个区域的属性不同。kseg0是unmapped,cached;而kseg1是unmaped,uncached的,所以真正使用时kseg0一般给内核使用,kseg1一般作为设备地址空间使用。划分的话,可以各占一半,譬如在mips32里,kseg0使用0x8000_0000到0x9000_0000;kseg1使用0xB000_0000到0xC000_0000,各256M即可。
在重启将系统初始化为已知状态的底层软件中,小心地将同一块物理内存区域同时映射为缓存和非缓存是有用的,有时甚至是必要的。但对于后面运行的代码来说,可能不是这样。对于每块物理内存,确定它是不是缓存的,然后严格按照刚刚确定的方式来对待。
参考:
《MIPS体系结构透视》
6 底层内存管理与TLB
10.3 <可见缓存的问题>