ARM-Linux存储机制详解

ARM-Linux存储机制详解

1.内存管理和MMU

当ARM 要访问存储器时,MMU 先查找TLB(Translation Lookaside Buffer,旁路转换缓冲)中的虚拟地址表。如果TLB 中没有虚拟地址的入口,则转换表遍历硬件会从存放在内存的转换表中获得转换和访问器权限。一旦取到,这些信息将被放到TLB 中,这时访问存储器的TLB 入口就拿到了。
在TLB 中其实包含了以下信息:
1)控制决定是否使用高速缓冲
2)访问权限信息
3)在有cache 的系统中,如果cache 没有命中,那么物理地址作为线性获取(line fetch)硬件的输入地址。如果命中了cache 那么数据直接从cache 中得到,物理地址被忽略。
ARM 的工作流程可用下图表示:



这种机制是纯粹的高速硬件操作,并不需要操作系统来完成。操作系统只要提供内存转换表就可以了,但是需要符合一定的格式。

ARM9 的MMU 映射表分为两种,一级页表的变换和二级页表变换。两者的不同之处就是实现的变换地址空间大小不同。一级页表变换支持1 M 大小的存储空间的映射,而二级可以支持64 kB,4 kB 和1 kB 大小地址空间的映射。在LINUX 中最终使用了1 M 一级页表和4 kB 的二级页表(即 1M段区和4KB页面)。



内核中地址转换表建立过程
    地址转换表建立是和内核的启动一起完成的,页表的建立也可以分为三个阶段:
    第一阶段是发生在内核解压缩、自引导时,也就内核镜像zimage 的文件头部分。相关代码从某种意义上来讲不属于内核,它是BSP 代码中的一部分,是需要根据不同的架构来分别实现的。通过平面映射的方式建立了256M 空间节描述表。但是,这个映射表是临时的,是为了提高内核解压缩时的速度而实现的。在解压缩结束之后,进入内核代码之前,MMU 功能就被关闭了,随之的映射表也被废弃不用。
    当decompress_kernel 函数实现内核的解压缩之后,那么内核启动的第一阶段工作就完成了。接下来就准备启动真正的内核,但是内核启动时必须要先关闭MMU,以至于刚才的第一阶段映射表不能使用了。
    第二阶段是的页表创建是非常关键的。同样也是使用汇编语言来实现。在代码中,有个函数__create_page_tables,这就是创建MMU 映射表,开启MMU 做准备的重要动作。

    这是真正意义上的第一次建立被内核所用的节转换表,经过上述代码实现,所建立的节转换表对应内容:
    初始映射表:
        0x10 0000=1M           
    上表中黑体字所示比较特殊,这一节空间进行了两次映射:一个是和物理地址相同的映射;一个是映射到0xC000 0000 以上的空间。这是因为按照LINUX 内存空间划分的惯例,3G(0xc0000000)以上的空间作为内核空间,其他的作为用户内存空间。所以这样映射后,只要在前1M 的程序之内的空间开启MMU,并且使用地址跳转指令就可以使内核跑到0xC0000000 以上的空间运行了。//???????但是这个映射表还不是最终系统使用的映射表。

    第三阶段是创建更高级别的映射关系和映射方法,主要和架构相关。不同的架构有不同的映射方法,比如ARM 就是二级映射的方法实现地址映射,但是必须按照LINUX 的**映射模型PGD、PMD、PTE 来实现。    从内核代码上来看,内核启动的过程中主要有两个函数比较重要:
(1) start_kernel->setup_arch->paging_init->bootmem_init->bootmem_init_node->create_mapping
(2) start_kernel->setup_arch->paging_init->devicemaps_init
(3) start_kernel->trap_init
函数一:重新建立了一级映射表,添加了对内存空间的二级映射,同时建立了bootmem 这个内存管理器。
函数二:在再bootmem 的帮助下实现了平台部分设备IO 地址的映射。
函数三:则是完成了中断矢量的映射。
    这样就完成了关于平台的基本映射。但是,地址映射远远没有结束,驱动需要地址空间映射,用户程序需要地址空间映射等,这些就靠内核的整个内存管理模块来实现了。按照一般的ARM 平台的映射习惯最后得到的映射如下:


    以上是在内核启动不同阶段的页表操作过程,到此为止,内核基本完成了映射表的配置。这个映射表不是固定不变的映射表,随着内核的运行而改变。这样才能够实 现操作系统的内存管理,端口映射管理功能,比如:相同物理地址的重复映射,外设IO 口管理等。好在LINUX提供进行内存管理操作的方法。



附:
Q1:板载SDRAM是64M,物理地址是0x3000 0000 - 0x33ff ffff,在初始化MMU页表时,把SDRAM的虚拟地址也映射到这个范围,是为什么?是不是为了跟没有MMU下的地址操作变得一样,也就是方便移植?
A1:和x86相似,内核使用虚拟地址时,由于在使用分页前后引用的地址都要是在物理内存中的同一个地址(因为连接的时候都是相对一个 PAGE_OFFSET),而你的PAGE_OFFSET和PHYS_OFFSET都是0x3000000;所以物理地址和虚拟地址就是同样的了
    在x86上,PAGE_OFFSET是0xC000 0000,所以在没有分页的使用引用变量需要使用变量的值-               0XC000 0000,而你的系统上则不需要。就是个映射,能简单就简单。



Q2:每个进程都有自己的运行空间,意思是不是指有各自的页表?即MVA?
A2:因为页表不一样,所以进程之间就不会相互影响了,如果是内存共享,那么页表也是不一样的,但是都是映射到同一物理地址。


Q3:试着在x86上反复运行同一个程序读同一个地址的值,发现读出来的值会不同,是不是因为每次运行,都建立了不同的内存映射关系,导致读取的物理地址有可能不同?
A3:读出来的都是虚拟地址,看不到物理地址,你读的是栈上的地址吧,全局的就是绝对虚拟地址了


Q4:初始化的MMU页表和进程的虚拟地址MVA是什么关系?是一个表吗?ARM里根据不同进程的PID,会重建MVA(“转换后的虚拟地址”),会不会把原来初始化的对应好的那部分页表内容给覆盖掉?
A4:内核和进程是两套页表。你说的初始化的页表是内核自己使用的,进程的页表会在执行进程时(execve)建立,根据可执行文件的内容,分配相应的物理内存(不说延迟分配),然后把进程的页表更改相应的项,这样进程就有自己的页表了。


~~~~~~~
linux的MMU是如何与软件结合完成内存映射的?     

问:

    linux的内存映射是通过MMU硬件完成的,但是我们又知道linux在I386的二级映射中通过 如下三个函数来获取PGD、PMD和PTE的表项的。既然有了这三个函数,那MMU的硬件到底完成了内存映射过程中的哪些工作??
#define pgd_offset(mm, address) ((mm)->pgd+pgd_index(address))


static inline pmd_t * pmd_offset(pgd_t * dir, unsigned long address)
{
return (pmd_t *) dir;
}


#define pte_offset(dir, address) ((pte_t *) pmd_page(*(dir)) + __pte_offset(address))

难道是pgd_offset、pmd_t、pte_offset这三个函数的实现依赖于MMU?  
换句话说:linux的MMU是如何与软件(如上述三个函数)结合完成内存映射的?

答:

MMU的作用有两个:地址翻译和地址保护

软件的职责是配置页表,硬件的职责是根据页表完成地址翻译和保护工作。

那三个函数是用来访问页表的。如果cpu没有硬件MMU那么这张表将毫无意义。

你必须从cpu的角度去理解内存映射这个概念。内存映射不是调用一个函数,然后读取返回值。而是cpu通过MMU把一条指令中要访问的地址转换为物理地址,然后发送到总线上的过程

你可能感兴趣的:(ARM-Linux存储机制详解)