Skip to content

Latest commit

 

History

History
439 lines (261 loc) · 16.2 KB

5-内存.md

File metadata and controls

439 lines (261 loc) · 16.2 KB

内存

1. 虚拟地址

1.1 分段机制

  • 虚拟地址空间分成若干个不同大小的段
    • 段表存储着分段信息,可供MMU查询
    • 虚拟地址分为:段号 + 段内地址(偏移)
  • 物理内存也是以段为单位进行分配
    • 虚拟地址空间中相邻的段,对应的物理 内存可以不相邻
  • 存在问题
    • 分配的粒度太粗(不连续,不等长),外部碎片
    • 段与段之间留下碎片空间,降低主存利用率

1.2 分页机制

  • 更细粒度的内存管理
    • 物理内存也被划分成连续的、等长的物理页
    • 虚拟页和物理页的页长相等
    • 任意虚拟页可以映射到任意物理页
    • 大大缓解分段机制中常见的外部碎片
  • 虚拟地址分为
    • 虚拟页号 + 页内偏移

2. 页表

2.1 单级页表的问题

空间占用大:

32位地址空间,页4K,页表项4B:页表大小:232 / 4K * 4 = 4MB

64位地址空间,页4K,页表项8B,页表大小:264 / 4K * 8 = 33,554,432 GB

因为单级页表可以看成以虚拟地址的虚拟页号作为索引的数组,整个数组的起始地址(物理地址)存储在页表基地址寄存器中。所以整个数组必须连续存在,其中没有被用到的数组项也需要预留。

2.2 AARCH64的4级页表

AARCH64的4级页表地址翻译

AARCH64的4级页表

页表项:页表项

最后12位是属性位哦!

页表使能

  • CPU启动流程
    • 上电后默认进入物理寻址模式
    • 系统软件配置控制寄存器,使能页表,进入虚拟寻址模式
  • AARCH64
    • SCTLR_EL1 (System Control Register, EL1)
    • 第0位(M位)置1,即在EL0和EL1权限级使能页表
  • 对比x86_64
    • CR0,第31位(PG位)置1,使能页表

空间占用

一个页表页为4K

假设整个应用程序的虚拟地址空间中只有两个虚拟页被占用,分别对应于最低和最高的两个虚拟地址,那么需要1个0级页表,2个1级页表,2个2级页表,2个3级页表,共7个页表,28k物理内存空间。

2.3 TLB

2.3.1 TLB刷新

不同的应用程序虚拟地址空间不一样,而TLB是虚拟地址到物理地址的映射,所以切换应用程序的时候,需要刷新TLB

🔺 操作系统在进行页表切换的时候,需要主动刷新TLB

AARCH64

应用程序和操作系统使用不同的页表,TTBR0_EL1,TTBR1_EL1,系统调用过程不需要切换页表

x86-64

内核映射到应用页表的高地址,系统调用也不需要切换页表,不用刷TLB

降低TLB刷新的开销

  • 为不同的页表打上标签
    • TLB缓存项都具有页表标签,切换页表不再需要刷新TLB
  • x86_64:PCID(Process Context ID)
  • PCID存储在CR3的低位中
  • 在KPTI使用后变得尤为重要
  • AARCH64:ASID(Address Space ID)
    • OS为不同进程分配8/16 ASID,将ASID填写在TTBR0_EL1的高8/16位
    • ASID位数由TCR_EL1的第36位(AS位)决定

🔺 用了ASID后切换页表不需要刷新TLB,但是修改页表映射后,仍需要刷新TLB

全局TLB

第三级页表的Lower attributes的第11位是nG(not Global)位

  • nG == 0: 相应TLB缓存项对所有进程有效
  • nG == 1: 仅对特定进程(ASID)有效

🔺为什么需要全局TLB

内核空间和用户空间是分开的,并且内核空间是所有进程共享。既然内核空间是共享的,进程A切换进程B的时候,如果进程B访问的地址位于内核空间,完全可以使用进程A缓存的TLB。

但是现在由于ASID不一样,导致TLB miss。我们针对内核空间这种全局共享的映射关系称之为global映射。针对每个进程的映射称之为non-global映射。所以,我们在最后一级页表中引入一个bit(non-global (nG) bit)代表是不是global映射。

当虚拟地址映射物理地址关系缓存到TLB时,将nG bit也存储下来。当判断是否命中TLB时,当比较tag相等时,再判断是不是global映射,如果是的话,直接判断TLB hit,无需比较ASID。当不是global映射时,最后比较ASID判断是否TLB hit。

(直白的说,内核地址在TLB里都标记为global?)

tlb结构

2.3.2 TLB与多核

  • 需要刷新其它核的TLB吗?
  • 一个进程可能在多个核上运行
  • 如何知道需要刷新哪些核?
    • 操作系统知道进程调度信息
  • 怎么刷新其他核?
    • AARCH64: 可在local CPU上刷新其它核TLB
      • TLBI ASIDE1IS
    • x86_64:
      • 发送IPI中断某个核,通知它主动刷新

2.3.3 TLB结构

为什么分级?我推测是利用局部性加快查询速度?

tlb

2.4 按需分配与换页

▲ 被分配使用的虚拟页,在内存中可能也没有相应的物理页映射

  • 情景1:
    • 两个应用程序各自需要使用 3GB 的物理内存
    • 整个机器实际上总共只有 4GB 的物理内存
  • 情景2:
    • 一个应用程序申请预先分配足够大的(虚拟)内存
    • 实际上其中大部分的虚拟页最终都不会用到

2.4.1 换页

基本思想

将物理内存里面存不下的内容放到磁盘上

虚拟内存使用不受物理内存大小限制

过程

  1. 操作系统希望从物理页A那里回收物理页P(对应A的虚拟页V)
  2. 操作系统把物理页P的内容写到磁盘上,并在A的页表中去掉虚拟页V的映射
  3. 操作系统记录物理页P被放在磁盘上的位置
  4. 回收物理页P分配给其他应用程序
  5. 此时,A的虚拟页V处于已分配但未映射的状态

预取

因为换页会涉及耗时的磁盘操作,因此操作系统往往会引入预取机制进行优化

当发生换入操作时,预测还有哪些页即将被访问,提前将他们一并换入物理内存

(以上针对第一个场景)

  • 替换策略的评价标准
    • 缺页发生的概率
    • 策略本身的性能开销
  • 🔺如何高效地记录物理页的使用情况
    • Recap:上节课说到的页表项中Access/Dirty Bits

Thrashing Problem:

  • 直接原因
    • 过于频繁的缺页异常(物理内存总需求过大)
  • 大部分 CPU 时间都被用来处理缺页异常
    • 等待缓慢的磁盘 I/O 操作
    • 仅剩小部分的时间用于执行真正有意义的工作
  • 调度器造成问题加剧
    • 等待磁盘 I/O导致CPU利用率下降
    • 调度器载入更多的进程以期提高CPU利用率
    • 触发更多的缺页异常、进一步降低CPU利用率、导致连锁反

工作集模型

一个进程在时间t的工作集W(t, x) (Peter Denning):其在时间段(t - x, t)内使用的内存页集合,也被视为其在未来(下一个x时间内)会访问的页集合, 如果希望进程能够顺利进展,则需要讲该集合保持在内存中。

  • 工作集时钟中断固定间隔发生,处理函数扫描内存页
    • 访问位为1则说明在此次tick中被访问, 记录上次使用时间为当前时间
    • 访问位为0(此次tick中未访问)
      • Age = 当前时间 – 上次使用时间
      • 若Age大于设置的x,则不在工作集
      • 将所有访问位清0
    • 注意访问位(access bit)需要硬件支持
    • 需要CPU硬件在程序访问某个页的时候自动的将访问位设为1

2.4.2 缺页异常

换页机制能正常工作的前提是当应用程序访问已分配但未映射的虚拟页时会触发缺页异常

2.4.3 按需分配

(针对第二个场景)

当应用程序申请内存分配的时候,操作系统可选择将新分配的虚拟页标记位已分配但未映射。当应用程序真的要访问这个虚拟页时,再映射到物理页。

2.5 物理内存管理之buddy system

buddy1

buddy2

  • 高效地找到伙伴块
    • 互为伙伴的两个块的物理地址仅有一位不同
    • 一个是0,另一个是1
    • 块的大小决定是哪一位

2.6 Rowhammer攻击

攻击者利用物理内存缺陷,极频繁访问某一行,其相邻行某些位会发生翻转

巧妙地利用位翻转,可以实施包括提权在内的多种攻击

安全防御

  • 为抵御Rowhammer攻击,实际上操作系统需要知道部分硬件细节,从而能够在物理内存分配时主动加入 一些Guard Page
  • 为抵御cache Side Channel攻击,操作系统需要知道同样cache映射细节

3. 操作系统内存管理的功能

3.1 共享内存

  • 节约内存 : 共享库
  • 进程通信:传递数据

3.2 写时拷贝(copy-on-write)

实现

  • 修改页表权限项
  • 在缺页时拷贝、恢复

3.3 内存去重

  • memory deduplication

    • 基于写时拷贝机制
    • 在内存中扫描发现具有相同内容的物理页面
    • 执行去重
    • 操作系统发起,对用户态透明
  • 如何发现相同页

    • 异或
    • 哈系数

内存去重潜在安全隐患

  • 导致新的side channel
    • 访问被合并的页会导致访问延迟明显 (写被合并的页的时候会发生COW)
  • 潜在攻击 – 攻击者可以确认目标进程中含有构造数据

3.4 内存压缩

当内存资源不充足的时候, 选择将一些“最近不太会 使用”的内存页进行数据压缩,从而释放出空闲内存

**Linux **

  • swap:换页过程中磁盘的缓存
  • 将准备换出的数据压缩并先写入 zswap 区域 (内存)
  • 好处:减少甚至避免磁盘I/O;增加设备寿命

3.5 大页

  • 在4级页表中,某些页表项只保留两级或三级页表
  • L2页表项的第1位
    • 标识着该页表项中存储的物理地址(页号)是指向 L3 页表页(该位是 1)还是指向一个 2M 的物理页(该位 是 0)
  • L1页表项的第1位
    • 类似地,可以指向一个 1G 的物理页

大页

利弊

  • 好处
    • 减少TLB缓存项的使用,提高 TLB 命中率
    • 减少页表的级数,提升遍历页表的效率
  • 案例
    • 提供API允许应用程序进行显示的大页分配
    • 透明大页(Transparent Huge Pages) 机制
  • 弊端
    • 未使用整个大页而造成物理内存资源浪费
    • 增加管理内存的复杂度

计算

在32位的机器中,巨页的大小是4M;在64位机器中,巨页的大小是2M,为什么巨页的大小不一致。

64位机器使用了4级页表,每一级页表使用9 bit做索引,大页包括了21 bit(48-4 * 9+9),因此大小是2M;在32位的机器中,使用了2级页表,每一级页表使用了10 bit做索引,因此大页大小为4M(22 bit(32-2 * 10+10))

在32位机器中,一个页表项占4 bytes;而在64位机器中一个页表项占8 bytes

两种机器都选4k作为最小页大小时,32位机器一个L3页表能指向的合计内存区域为64位机器的两倍。所以巨页大小为64位机器的两倍。

32位机器:

一页为4k,所以一个页表页可以存放1024个页表项: $$ 4k \div 4 bytes = 2^{10} = 1024 $$ 也就是说L3页表页中有1024个页表项,又因为一个页表项可以指向一个4k的物理页,所以一个L3页表页合计可以指向4M的物理页: $$ 1024 * 4k = 2^{12} k = 4M $$

64位机器:

64位机器一个页表项占8 bytes,所以一个页表也可以放512个页表项。

所以L3页表页可以指向2M的页表页: $$ 512 * 4k = 2M $$

为什么OS/MMU使用多级页表映射虚拟地址到物理地址中?使用4级页表的最大内存空间消耗是多少?

减少页表的空间开销 $$ 4KB+4KB2^9+4KB(2^9)^2+4KB*(2^9)^3 $$ 使用多级页表原因

使用多级页表是为了压缩页表在内存中的占用大小。

多级页表允许在整个页表结构中出现空洞,而单级页表需要每一项都实际存在(假设页的大小位4k页表项占8 bytes,那么要占空间33554432GB)。

在实际使用中,应用程序的虚拟地址空间中的绝大部分处于未分配状态,多级页表可以部分创建,能够极大地节约所占空间。

最大空间消耗:

使用四级页表最大空间消耗为使用全部虚拟空间时

假设使用全部48位虚拟空间,即 $$ 2^{48} $$ 因为一个页表页可以放512个页表项

L1512个页表页,L2512*512个页表,L3512*512*512个页表页

则共需空间 $$ (1+512+521^{2}+512^{3})*4kB ≈ 513GB $$

在ARM-mmu架构中,mmu是如何区分页条目是否被使用

检查页表条目中第一个属性位

第三级页表页中的页表项中,第0位表示该项是否有效

使用巨页的优点和缺点分别是什么

优点:TLB miss少,缺页异常少等。缺点:内存分配粒度大,可能造成内存浪费

内存的属性位AP和UXN已经能够隔离用户态和内核态,为什么还需要两个ttbr寄存器?

▲ AP(第3级页表页中的页表项):读写权限位

▲ XN:EL0能不能执行

▲ PNX:EL1能不能执行

两个基地址寄存器寄存器相对于一个基地址寄存器的好处是:”不同进程可共用独立的内核页表,不再需要修改每个进程页表的高地址区域来映射内核页,内核的设计和实现更加方便“。

两个ttbr寄存器并不能防住meltdown攻击,某些arm架构的CPU仍然存在meltdown的漏洞。使用软件防御meltdown攻击的方法是使用KPTI(Kernel page-table isolation)。对于arm来说,需要三张页表,两张kernel页表和一张user页表。对于x86来说,需要两张页表,对应kernel mode和user mode的两张页表。在用户态时,页表里面只映射了部分的kernel空间,而进入内核态之后,会切换页表,切换到full kernel space的页表。通过这种方式,我们可以使用纯软件的方法防御meltdown攻击,但是这会带来切换页表以及flush tlb的额外开销。

TLB能够缓存虚拟地址到物理地址的映射,当发生进程间的上下文切换的时候,需要刷掉所有的TLB条目,这是为什么?你能想出一种解决方式,使得TLB条目在上下文切换的时候不需要被刷掉吗?

不同的进程可能使用相同的虚拟地址;使用ASID技术

为什么上下文切换要刷TLB

因为TLB是使用虚拟地址进行查询的,不同的进程使用不同的页表,同一个虚拟地址VA可能对应不同的物理地址PA1PA2

当进程1访问VA后,TLB会缓存VAPA1的映射;在切换到进程2后,页表已经发生了变化,再次访问虚拟地址VA,如果没有刷新TLB,则会查询到VAPA1的映射,而非PA2,进而产生错误。

所以发生进程间上下文切换时,需要刷掉所有TLB条目。

不需要刷TLB的解决方式:

可以为TLB缓存项打上标签。

操作系统为不同的应用程序(进程)分配不同的标签(ASID)作为进程的身份标签,将该标签写入进程页表基地址寄存器的空闲位。TLB中的缓存项也会包含ASID这个标签,从而使TLB中属于不同进程的缓存项被区分开。所以切换进程时,不用刷新TLB

在ARMv8结构之前,内存属性中没有DBM(Dirty Bit Modifier)位。这意味着硬件不支持脏页。所以OS需要如何模拟并且记录脏页呢?给出一种可行的解决办法

软件模拟,初始化的时候将所有的页的可写位都置为0而OS中记录当前页具有写权限,当CPU触发了一个写请求的时候会产生fault,OS能够检察该写请求是否合法,如果合法将对应的页条目中的可写属性位置为1

利用读写权限模拟记录脏页

增加一个读写权限位。先将所有页的读写权限都设为只读,当要一个页时,会触发permission fault,将该位改成可写,此时该页也变为脏页。之后的写,因为已经将权限改为可写,所以不会触发permission fault。

也就是说当该位位可写,则说明被写过,是脏页。如果是只读,则不是脏页。