首页
社区
课程
招聘
[原创]OS虚拟内存管理 - Zircon(ARM64)
发表于: 2019-2-23 11:35 6761

[原创]OS虚拟内存管理 - Zircon(ARM64)

2019-2-23 11:35
6761

几篇前面写的 Google 新系统 Fuchsia 的源码分析,原本发在 CSDN 上的,没什么人看就转过来大家讨论讨论 233.

虚拟内存是通用计算机处理器中 MMU 单元所带来的特性,这也是能否运行通用操作系统的关键。类似 Cortex-M 系列的嵌入式CPU是没有 MMU 的,so 只能跑一些 RT 微内核。
下文所分析的 Zircon 内核原本是 Google 用在 Android 设备 Bootloader 部分的 LK 内核发展而来,而 LK -> Zircon 的最关键的部分就是加入了虚拟内存管理。

能够面向普通消费大众的 OS 必然会容纳成千上万个程序在 OS 中欢快运行,怎样为每个程序构建一个安全稳定的内存空间是必须的,同时 OS 也必须想办法保护自己的内存空间,否则外来程序的运行非常容易造成 OS 本身的错误奔溃。

简单的来说,OS 通过虚拟内存将内存空间划分为以下结构,以达到安全隔离的目的。
在这里插入图片描述

用户空间简单的来说就是提供给用户应用程序运行的内存空间,每个应用进程原则上内存空间也是相互隔离的。
他们之间的通讯通过另外一种叫 IPC 的方式进行,一般由内核提供这一中间人的角色,帮助在两个进程空间之间传输数据。

在内核中也有很多线程,之所以叫线程而不叫线程,是因为这些内核线程都运行在同一个内核空间内。
一般内核拥有较为高级的权限,能够同时访问用户空间和内核空间

如果 CPU 没有 MMU,那么 OS 想实现上面所说的内存结构是不可能的。
简单想一下就知道在编程时,汇编指令并没有限制我们想访问的内存地址范围,理论上我们可以访问并且修改内存的各个区域。
而 MMU 这时候起到了关键性的作用。

当没有 MMU 的时候,寻址指令可以通过总线直接访问到实际的设备或者内存地址
一路畅通无阻
这里省略缓存
在这里插入图片描述

MMU 拦截了 CPU 发出的寻址信号,这时候 CPU 发出的时虚拟地址(VA)。
用软件的思维来说,我们把寻址操作想成 Java 中的 Field Access 操作,那 MMU 就是 Hook 住了所有的 Field Access,然后根据配置的 Hook 逻辑替换成真正的地址(物理地址 PA)

从上述逻辑我们不难看出 MMU 确实可以帮助 OS 在硬件层面实现内存区域的隔离保护。

除此之外,CPU 一般还会提供多个运行等级,OS 内核一般运行在较为高级的等级,而 MMU 则设定为较高等级的运行状态才能进行修改配置。这样就避免了用户程序更改 MMU 从而绕过 OS 内存隔离

在这里插入图片描述

ArchVmAspaceInterface 是个接口,描述了各个平台下虚拟内存管理需要实现的重要接口。主要需要根据各个平台的 MMU 独立实现。
而下面我们要介绍的就是 ARM64 平台的实现。

简称 VMO,Zircon 虚拟内存基础对象,描述一块数据,一片内存
VmObject 可以被映射到其他虚拟内存空间(VmAddressRegion::CreateVmMapping)
VMO 是 Zircon 共享内存机制的基本数据单元

VmObject 继承自 DoublyLinkedListable,很明显是个循环链表。

并且是个树形结构

VMO 有两个子类

也就是说 VMO 既可以是一页或者多页虚拟内存
也可以是一段物理内存

上面说了可以通过 VmAddressRegion::CreateVmMapping 将一个 VMO 映射到其他的地址。

VmAspace(Virtual Memory Address Space),虚拟内存地址空间,在 Zircon 中使用这个类描述一块虚拟内存寻址空间
VmAspace 继承自 DoublyLinkedListable,是个循环链表。

结构体,描述虚拟内存的基本单位:内存页

Kernel Stack,内核栈,就是每个内核线程都要有的栈
这是个结构体并不是类,原因是他需要直接嵌入到 thread 结构体中,而不是作为一个指针。

虚拟内存地址区域,描述了一块连续的虚拟内存地址空间,被上边 VmAspace 所包含, 也就是说一块 VmAspace 包含多片 VmAddressRegion。
VmAddressRegion 有 “Alive” 和 “Dead” 两种状态,当 “Alive” 时,VmAddressRegion 代表着一块已经被映射的虚拟内存,而 “Dead” 的时候则无任何意义。

以上我们简单介绍了 Zircon 虚拟内存管理的架构,下面就分析 Zircon 在 Arm64 平台上的实现。

ARM64 支持 4K, 16K,64K 三种大小的虚拟内存页大小
而在 Zircon 中,无论是内核空间,还是用户空间,都默认使用 4K 内存页

如此 ARM64 在 4K 页大小下 VA 的格式为:

高位 ---> 低位
| 页表基地址 | L0 页表 index | L1 页表 index | L2 页表 index | L3 内存页 index | 页内偏移 |
| ----- |----- | ----- | ----- |----- | ------ |
| 16 | 9 | 9 | 9 | 9 | 12 |

该阶段的任务是为启动阶段 Zircon 内核构建页表。
boot-mmu.cpp -> arm64_boot_map()

首先在 boot 的汇编阶段,页表地址寄存器已经被指向了顶级页表的地址:

页表实际为树型结构

4级页表则为 4 层树结构,每个叶子有 512 个子节点

总共有 2^0 + 2^9 + 2^18 + 2^27 张页表
2^36 个内存页
2^48 bit 内存

注意:以上数量为最大数量,实际如果没有用到相应的地址则不会构建对应的页表

那么构建页表的过程,其实就是自顶向下构建树的过程
如果 L0 页表基地址为 kernel_table0 想把一块长为 len(页大小的倍数) 的内存,从 paddr 映射到 vaddr 则需要经历以下过程:

代码:

Boot 完毕后与 Boot 中的页表映射区别在于:

具体体现为:由原来的 L0 -> L1 -> L2 ->L3 固定顺序构建的逻辑,改为动态递归构建

1.首先要算出特定页大小的 index 在 VA 中的长度:

2.然后要解决的是计算出在特定页大小某一等级页表的 index 在 VA 中的偏移
MMU_LX_X(page_shift, level)

max_level (page_shift - 3) + page_shift = 64
page_shift + (max_level - level)
(page_shift - 3) = MMU_LX_X

上边 2式 - 1式 : MMU_LX_X(page_shift, level) = ((4 - (level)) * ((page_shift) - 3) + 3)

3.构建步骤变更为:
假设先在将
vaddr -> paddr
页大小 = 2^page_shift
index_shift 为当前等级页表在 VA 中的偏移

取出当前(假设0) level 的 index
1.vaddr 高 16 位抹 0 = vaddr_rel,用 vaddr - kernal_base,kernel_base 是 kernel 的基地址,因为整个内存空间是一张页表,所以 VA 的高 16 位必然相同
VA = - L0 index - L1 index - LN index - 页内偏移
2.然后 index = vaddr_rel >> index_shift
VA = - L0 index -
最后剩下 L0 index

算出页内偏移
1.vaddr 高 16 位抹 0,用 vaddr - kernal_base,kernel_base 是 kernel 的基地址,因为整个内存空间是一张页表,所以 VA 的高 16 位必然相同
VA = - L0 index - L1 index - LN index - 页内偏移
2.当前等级页表所能管理的内存块大小 block_size = 1 << index_shift;
3.vaddr 当前 index_shift - 64 位抹 0 = vaddr_rem,通过 vaddr & (block_size - 1)
VA = - - L1 index - LN index - 页内偏移
4.block_size - vaddr_rem = 页内偏移,还需要和剩下需要映射的内存大小 size 比一下大小 MIN(size, block_size - vaddr_rem);
VA = - - 页内偏移

判断是否是最后一级页表,是则根据传入的页表属性填充最后一级页表项目,否则递归执行,下一级页表 index 偏移 = index_shift - (page_size_shift - 3)

代码:

这个方法等于是模拟了 MMU 的工作:
逻辑与上面类似,就不重复了

简单来说也是去每一级页表 index -> 由 index 一级一级查找下一个页表,最后找到目标内存页面
PA = 目标页基地址 + 页内偏移

当我们在不同的进程中切换时,需要为每个进程切换不同的进程空间。

Zircon 会为每个用户进程创建独立的虚拟内存空间,也就是说会为每个进程创建单独的页表。
当切换内存空间时,只需要重新对 MMU 的页表寄存器赋值就可以了,修改为当前进程空间的 L0 页表地址。

从本文开始可以知道,MMU 会将近期查到的页地址存储在 Cache 中,叫做 TLB 的东西。下一次查询就可以先查 TLB 以加快寻址速度,不然但的话一次寻址 MMU 将多次访问内存。
当 CPU 运行的代码从用户进程 A 切换到用户进程 B 时,由于 A 和 B 的进程地址空间一般是不一样的,所以当前的 TLB 对于 B 进程完全没有意义,而且可能导致 B 错误的访问到 A 的进程空间。
所以,我们就需要将用户空间的 TLB 完全清空,而这样一来,当 CPU 再次切换到进程 A 时,MMU 就需要重新从内存中查页表,内存压力将越来越大。。。

幸好 ARM64 想到了这一点,还记得 VA 的高 16 位么?其中就承载了一个叫 ASID 的信息,每条 VA 都会携带上对应进程的 ASID,这样的话 MMU 就有能力区别 TLB 中的 VA 是哪个进程的。
这样的话,进程切换将完全没有必要 Flush TLB,而是让 Cache 自然淘汰。
当然,当进程死亡的时候,还是可以主动 Flush TLB 的,而且因为 ASID,CPU 也有能力单独抹掉某个进程空间的 TLB。

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 1
支持
分享
最新回复 (5)
雪    币: 1395
活跃值: (526)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
2
看雪不支持外链图片貌似,自行点开看吧,不过也没什么好看的
2019-2-23 11:37
0
雪    币: 1395
活跃值: (526)
能力值: ( LV9,RANK:160 )
在线值:
发帖
回帖
粉丝
3
表格出了点问题。。。将就一下吧
2019-2-23 11:43
0
雪    币: 6573
活跃值: (3893)
能力值: (RANK:200 )
在线值:
发帖
回帖
粉丝
4
坑大 看雪不支持外链图片貌似,自行点开看吧,不过也没什么好看的
建议把图片重新贴过来,防止以后失效
2019-2-23 12:30
0
雪    币: 26205
活跃值: (63302)
能力值: (RANK:135 )
在线值:
发帖
回帖
粉丝
5
图片没了。。。。
2019-3-26 11:40
0
雪    币: 15
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
图片失效
2019-4-24 15:41
0
游客
登录 | 注册 方可回帖
返回
//