深入理解MMU的工作过程
MMU: Memory Manage Unit - 存储器管理单元
1. 虚拟存储器的实现 (Virtual Memory)
虚拟存储器的基本思想是程序, 数据, 堆栈的总的大小可以超过物理存储器的大小, 操作系统把当前使用的部分保存在内存中, 而把其他未被使用的部分保存在磁盘上。
2 MMU的实现
任何时候, 计算机上都存在一个程序能够产生的地址集合, 我们称之为地址范围。 这个范围的大小由CPU的位数决定。 例如一个32位的CPU, 它的地址范围是0~0xFFFFFFFF(4G), 而对于一个64位的CPU, 它的地址范围为0~0xFFFFFFFFFFFFFFFF (64T). 这个范围就是我们的程序能够产生的地址范围, 我们把这个地址范围称为虚拟地址空间, 该空间中的某一个地址我们称之为虚拟地址。
在没有使用虚拟存储器的机器上, 虚拟地址被直接送到内存总线上, 使具有相同地址的物理存储器被读写。 而在使用了虚拟存储器的情况下, 虚拟地址不是被直接送到内存地址总线上, 而是送到内存管理单元 - MMU. 它由一个或一组芯片组成, 一般存在于处理器中, 其功能是把虚拟地址映射为物理地址。
mmu开启以后会有以下特点
- 多个程序独立运行
- 虚拟地址是连续的(物理内存可以有碎片)
- 允许操作系统管理内存
而MMU使用虚拟地址中最重要的位来索引转换表中的条目, 并确定正在访问哪个块。 除了地址转换之外, MMU还可以控制每个内存区域的内存访问权限, 内存顺序和缓存策略。
3. 页表
- 作用
- 地址转换: 将虚拟地址转换为物理地址
- 权限管理: 管理CPU对物理页的访问, 如读写执行权限
- 隔离地址空间: 隔离各个进程的地址空间, 使其互不影响, 提高系统的安全性
- 页表位置
- 页表存放在物理内存中,打开mmu之后, 如果需要修改页表, 需要将页表所在的物理地址映射到虚拟地址才能访问页表。 页表基地址寄存器和各级页表项中存放的都是物理地址。
4. 地址转换
- 硬件 (遍历页表, 将虚拟地址转化为物理地址, 页面权限管理)
- mmu: 查询tlb或者遍历页表
- tlb: 缓存最近转换的页表条目。 有了TLB之后, CPU访问某个虚拟内存地址的过程如下:
- CPU产生一个虚拟地址
- MMU从TLB中获取页表, 翻译成物理地址
- MMU把物理地址发送给L1/L2/L3/内存
- L1/L2/L3/内存将地址对应数据返回给CPU.
- cr3
- 软件
- 应用程序: 访问虚拟内存即可如执行指令, 读写内存, 没有权限管理页表。 不管虚拟内存如何转换为物理内存, 对应用来说透明
- Linux内核: 内核初始化建立内核页表, 实现缺页异常等机制为用户任务按需分配并映射页表。 当然, 内核也可以遍历页表, 如缺页异常时遍历进程页表。
5. 页表遍历过程
下面以arm64处理器架构多级页表遍历作为例子
Linux内核中可以将页表扩展到5级, 分别是页全局目录(PGD: Page Global Directory), 页4级目录 (Page 4th Directory, P4D), 页上级目录 (Page Upper Directory, PUD), 页中间目录 (Page Middle Directory, PMD), 直接页表 (Page Table, PT). 而支持arm64的linux使用4级页表结构分别是pgd, pud, pmd, pt. arm64手册中将他们分别叫做L0, L1, L2. L3转换表。
tlb miss时,mmu会进行多级页表遍历遍历过程如下:
a. mmu根据虚拟地址的最高位判断使用哪个页表基地址寄存器作为起点。 当最高位为0时, 使用ttbr0_el1作为起点(访问的是用户空间地址), 最高位为1时, 使用ttbr1_el1作为起点 (访问的时内核空间地址)。mmu从相应的页表基地址寄存器中获得L0转换表基地址。
b. 找到L0, 从虚拟地址中获得L1索引, 依次找到L3转换表
c. 从页表项中取出物理页帧号然后加上物理地址偏移 (VA[11,0])获得最终的物理地址。
6. 进程如何使用内存
所有进程都必须占用一定数量的内存, 它或是用来存放从磁盘载入的程序代码,或是存放自用户输入的数据等。 不过进程对这些内存的管理方式因内存用途不一而不尽相同, 有些内存是事先实现静态分配和统一回收的, 而有些却是按需要动态分配和回收的。 对任何一个普通进程来讲, 它都会涉及到5种不同的数据段
- 代码段: 用来存放可执行文件的操作指令。 它是可执行程序在内存中的镜像。 代码段需要防止在运行时被非法修改, 所以只准许读取操作, 而不允许写入操作
- 数据段: 数据段用来存放可执行文件中已初始化全局变量。 换句话说就是存放程序静态分配的变量和全局变量。
- BSS段: BSS段包含了程序中未初始化的全局变量, 在内存中bss段全部置零。
- 堆(heap): 堆是用于存放进程运行中被动态分配的内存段, 它的大小并不固定, 可动态扩张或缩减。 当进程调用malloc等函数分配内存时, 新分配的内存就被动态添加到堆上(堆被扩张),当利用free等函数释放内存时, 被释放的内存从堆中被剔除。
- 栈: 栈是用户存放程序临时创建的局部变量。 除此以外, 在函数被调用时, 其参数也会被压入发起调用的进程栈中, 并且待到调用结束后, 函数的返回值也会被存放回栈中。 由于栈的先进先出特点, 所以栈特别方便用来保存/恢复调用现场。 从这个意义上讲, 我们可以把堆栈看成一个寄存, 交换临时数据的内存区。
-