Lec 08 物理内存管理

Lec 08 物理内存管理

0 Contents

a

OS的职责:管理和分配物理内存资源

  • 引入虚拟内存后,物理内存分配体现在以下四个方面:
    1.用户态应用出发延迟映射,此时内核需要分配物理内存页,映射到对应的虚拟页。
    2.内核自己申请内存并且使用。用于内核自身的数据结构,通过kmalloc()完成。
    3.发生换页时,通过磁盘来扩展物理内存的容量。
    4.内核申请用于设备的DMA内存,通常需要连续的物理页。

应用出发延迟映射

  • 应用调用malloc后,与物理内存是否有关?
    -- 调用malloc后,返回的虚拟地址属于某个虚拟地址结构体。
    -- 但是虚拟地址对应的页表项的有效位可能是0
    -- 第一次访问新分配的虚拟地址时,CPU可能触发page fault

  • 操作系统应对页错误:
    -- 找到一块空闲的物理内存页
    -- 修改页表并映射物理页到触发页错误的虚拟地址所在的虚拟页
    -- 恢复并重新执行。

简单的内存管理方法导致很多外部碎片

a

会导致资源利用率下降,分配性能下降

物理内存分配器的指标

  1. 资源利用率
  2. 分配性能
  • 外部碎片和内部碎片
  • 外部碎片:单个空白部分都小于分配请求的内存大小,但加起来足够。
  • 注:蓝色部分为一分配内存,空白部分为未分配内存

a

  • 内部碎片:蓝色阴影部分是分配内存大于实际使用内存而导致的内部碎片。
  • 注:蓝色粗线框是已分配内存,蓝色部分表示实际使用的内存,蓝色阴影表示已经分配但是没有被使用部分。

1. 伙伴系统(buddy system)

  • 采取分裂与合并避免外部碎片。

a

举例分配15K内存

当一个请求需要分配 m 个物理页时,伙伴系统将寻找一个大小合适的块, 该块包含 2n个物理页,且满足 2n1<m2n

a

合并过程如何定位伙伴块

  • 高效地找到伙伴块
    -- 互为伙伴的两个块的物理地址仅有一位不同
    -- 而且块的大小决定是哪一位
  • 例如:
    -- 块A(0-8K)和块B(8-16K)互为伙伴块
    -- 块A和B的物理地址分别是 0x0 和 0x2000
    仅有第13位不同,块大小是8K(213

a

伙伴系统:以页为粒度的物理内存管理

  • 分配物理页/连续物理页(2n
    -- 直接映射物理页的物理地址与虚拟地址
  • 资源利用率
    -- 外部碎片程度降低
  • 分配性能
  • 分配和合并的最差时间复杂度为O(listnum), 最好时间复杂度为O(1)

? 真的不出现外部碎片了吗?
不是。因为伙伴系统不能合并不同阶次的页块,并且最小只能分配4K的物理内存页,因此需要更细颗粒度的分配机制

回顾:直接映射机制

  • OS一次性将所有物理内存映射到一段虚拟地址空间
    -- 任一物理地址和虚拟地址仅相差一个偏移量

    • OS可迅速根据物理地址或虚拟地址互相计算
      -- 看上去类似最传统的段机制,实际通过配置页表来实现
  • 直接映射机制的特点
    -- 内核使用直接映射的虚拟地址,不会触发page fault
    -- 更方便找到连续的物理页:只要寻找连续的虚拟页

  • 对内核来说:
    -- 已映射的地址不一定正在使用(需要通过kmalloc才能用)
    -- 正在使用的地址通常已映射(例外:vmalloc)

  • 对应用来说:
    -- 正在使用的地址不一定已映射(on-demand paging)
    -- 已映射的地址一定正在使用(否则不会被映射)

  • 同一个物理地址可以有多个虚拟地址
    -- 如应用任一已映射的虚拟地址,在内核的直接映射下也有一个虚拟地址

伙伴系统代码实现

描述物理页的代码实现
a

操作系统维护struct physical_page数组

a

a

a

2. SLAB/SLOB/SLUB:细粒度内存管理

内核运行中需要进行动态内存分配

  • 内核自身用到的数据结构
    -- 为每个进程创建的process,VMA等数据结构
    -- 动态性:用时分配,用完释放,类似用户态的malloc
    -- 数据结构大小往往小于页粒度

  • 为什么不使用与malloc类似的机制?
    -- 即先分配虚拟地址,然后通过on-demand paging分配物理页

  1. 没必要(内核最高权限,访问所有物理地址
  2. 复杂度:内核为应用处理缺页,若内核自己缺页,则处理过程中不能再次触发缺页
  3. 若要不触发page fault,则必须已经映射完成

SLAB:建立在伙伴系统之上的分配器

  • 目标:快速分配小内存对象
    -- 内核中的数据结构大小远小于4K(例如VMA)

  • SLAB分配器家族:SLAB、SLUB、SLOB
    -- 上世纪 90 年代,Jeff Bonwick在Solaris 2.4中首创SLAB
    -- 2007年左右,Christoph Lameter在Linux中提出SLUB
    -- Linux-2.6.23之后SLUB成为默认分配器
    -- 发展过程中,提出针对内存稀缺场景的SLOB

  • 后续以主流的SLUB为例讲解

  • 观察
    -- 操作系统频繁分配的对象大小相对比较固定

  • 基本思想
    -- 从伙伴系统获得大块内存(名为slab)
    -- 对每份大块内存进一步细分成固定大小的小块内存进行管理
    -- 块的大小通常是 2n 个字节(一般来说,3n<12
    -- 也可为特定数据结构增加特殊大小的块,从而减小内部碎片

SLUB设计

  • 只分配固定大小块
  • 对于每个固定块大小,SLUB 分配器都会使用独立的内存资源池进行分配
  • 采用best

a

SLAB设计

  • 初始化资源池,将连续的物理页划分成若干等份的slot

a

  • 空闲链表用于区分是否空闲。分配时直接分配空闲slot

a

  • 分配N字节:
  • 1.定位到大小最合适的资源池(假设只有一个slab),
  • 2.从slab中取走Next_Free指向的第一个slot
  • 释放:将Next_Free指针指向待释放内存(slot)

a

  • 释放时如何找到Next_Free?
    思路:根据待释放内存地址计算slab起始地址
    ADDR & ~(SLAB_SIZE-1)

  • 上述方法在kfree(addr)接口下可行吗?

  • 没有size信息,无法判断addr是被slab分配的,还是伙伴系统分配的

  • 在物理页结构中记录所属于的slab信息

  • 新增slab:当某个资源池slab分配完了, 再从伙伴系统分配一个slab

a

  • 组织多个64字节slot的SLAB?
  • 引入两个指针currentpartial
  • 分配:
    • Current指向一个slab,并从其中分配;
    • 当Current slab全满,则从Partial链表中取出一个放入Current
  • 释放:
    • 释放后,若某个slab不再全满,则加入partial
    • 释放后,若某个slab全空则可还给伙伴系统

a

SLAB 小结

针对每种slot大小维护两个指针:

  • current仅指向一个 slab

    • 分配时使用、按需更新
  • partial指向未满slab链表

    • 释放时若全free,则还给伙伴系统
  • 优势:

    • 减少内部碎片(可根据开发需求)
    • 分配效率高(常数时间)

从伙伴系统获得的物理内存块称为slab
slab内部组织为空闲链表

slot一般选择2n(3n<12)大小。可以额外增加特殊大小来减少内部碎片。

2.突破物理内存容量限制

换页机制(Swapping)

  • 换页的基本思想
    -- 用磁盘作为物理内存的补充,且对上层应用透明
    -- 应用对虚拟内存的使用,不受物理内存大小限制
  • 如何实现
    -- 磁盘上划分专门的Swap分区,或专门的Swap文件
    -- 在处理缺页异常时,触发物理内存页的换入换出

a

? 如何判断缺页异常是由于换页引起的

  • 导致缺页异常的三种可能
    -- 访问非法虚拟地址
    -- 按需分配(尚未分配真正的物理页)
    -- 内存页数据被换出到磁盘上
  • OS如何区分?
    -- 利用VMA区分是否为合法虚拟地址(合法缺页异常)
    -- 利用页表项内容区分是按需分配还是需要换入

应用进程地址空间中的虚拟页可能存在四种状态,分别是:

  1. 未分配;
  2. 已分配但尚未为其分配物理页;
  3. 已分配且映射到物理页;
  4. 已分配但对应物理页被换出。

请问当应用进程访问某虚拟页时,在上述四种状态下,操作系统会分别做什么?

  1. 检查是否虚拟页的大小符合虚拟地址空间大小,如果符合则为其分配虚拟地址空间中的虚拟页,否则抛出page fault。然后直接读取物理内存为其分配映射及物理空间。如果物理空间为满则也会出现page fault。分配后更新TLB,进行TLB读取。读取成功后将数据写入Cache。最后从Cache中读出物理页。(TLB缺失,Cache缺失)
  2. 检查物理内存空间,如果有则为其分配物理空间。然后刷新TLB和cache。(TLB缺失,Cache缺失)
  3. 直接读取TLB和Cache中的映射和数据即可。
  4. 发生TLB缺失后,进入缺页处理,从磁盘中换出页表,更新TLB,更新Cache,再次读取Cache即可。

(仅供参考)

a

换出操作时机?

  • 策略A
    -- 当用完所有物理页后,再按需换出
    -- 回顾:alloc_page,通过伙伴系统进行内存分配
    -- 问题:当内存资源紧张时,大部分物理页分配操作都需要触发换出,造成分配时延高
  • 策略B
    -- 设立阈值,在空闲的物理页数量低于阈值时,操作系统择机(如系统空闲时) 换出部分页,直到空闲页数量超过阈值
    -- Linux Watermark:高水位线、低水位线、 最小水位线

换页机制代价

  • 优势:突破物理内存容量限制
  • 劣势:缺页异常+磁盘操作导致访问延迟增加
  • 如何取得平衡?
  • 预取机制 (Prefetching)
    -- 预测接卸来进程要使用的页,提前换入
    -- 在缺页异常处理函数中,根据应用程序访存具有的空间本地性进行预取

换页机制

  • 页替换策略
    -- 选择一些物理页换出到磁盘
    -- 猜测哪些页面应该被换出(短期内大概率不会被访问)
    -- 策略实现的开销

OPT:理想换页策略

假设物理内存中可以存放三个物理页,初始为空,某应用程序一共需要访问物理页面 1~5,OPT:优先换出未来最长时间内不会再访问的页面

a

FIFO

操作系统维护一个队列用于记录换入内存的物理页号,每换入一个物理页就把其页号加到队尾,因此最先换进的物理页号总是处于队头位置

a

Belady's Anomaly

a

second Chance策略

FIFO 策略的一种改进版本:为每一个物理页号维护一个访问标志位。
如果访问的页面号已经处在队列中,则置上其访问标志位。
换页时查看队头:1)无标志则换出;2)有标志则去除并放入队尾,继续寻找

a

LRU策略

OS维护一个链表,在每次内存访问后,OS把刚刚访问的内存页调整到链表尾端;每次都选择换出位于链表头部的页面
缺点-1:对于特定的序列,效果可能非常差,如循环访问内存
缺点-2:需要排序的内存页可能非常多,导致很高的额外负载

a

时钟算法策略

  • 精准的LRU策略难以实现
  • 物理页环形排列(类似时钟)
    -- 为每个物理页维护一个访问位
    -- 当物理页被访问时, 把访问位设成T
    -- OS依次(如顺时针)查看每个页的“访问位”
    • 如果是T,则置成F
    • 如果是F,则驱逐该页

a

针臂:

  • 指向换入位置
  • 需要换出时:从下一个位置开始转

img

实现时钟算法

  • 每个物理页需要有一个“访问位”
    -- MMU在页表项里面为虚拟页打上“访问位”
    -- 回顾:页表项中的Access Flag
  • 如何实现
    -- OS在描述物理页的结构体里面记录页表项位置
    • 当物理页被填写到某张页表中时,把页表项的位置记录在元数据中(在Linux中称为“反向映射”:reverse mapping)
    • 根据物理页对应的页表项中的“访问位”判断是否驱逐
    • 驱逐某页时应该清空其对应的所有页表项(例如共享内存)

a

页替换策略小结

  • 常见的替换策略
    -- FIFO、LRU/MRU、时钟算法、随机替换 …
  • 替换策略评价标准
    -- 缺页发生的概率 (参照理想但不能实现的OPT策略)
    -- 策略本身的性能开销
    • 如何高效地记录物理页的使用情况?
      -- 页表项中Access/Dirty Bits

The Thrashing Problem(颠簸现象)

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

工作集模型(有效避免thrashing)

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

  • 工作集模型:All-or-nothing
    -- 进程工作集要么都在内存中,要么全都换出
    -- 避免thrashing,提高系统整体性能表现

跟踪工作集w(t,x)

  • 工作集时钟中断固定间隔发生,处理函数扫描内存页
    -- 若访问位为1,则表示此次tick中被访问
    • 记录上次使用时间为当前时间
      -- 若访问位为0,则表示此次tick中未访问
    • Age = 当前时间 – 上次使用时间
    • 若Age大于设置的x,则不在工作集
      -- 最后,OS将所有访问位清0
    • 注意:访问位由CPU在访问时设为1

换页

  • 换出操作:物理内存不够时
    -- OS选择不常用的物理内存(不同的选择策略)
    -- OS将内存中的数据写入磁盘块,并记录磁盘块与内存的关联
    -- OS更新页表,将对应页表项的valid bit设置为0
  • 换入操作:当换出的页被访问时,触发page fault
    -- OS判断该地址所在页被换出,找到对应的磁盘块
    -- OS分配空闲的物理内存页;若没有空闲页,则再次进行换出操作
    -- OS将磁盘块中的数据读入前一步找到的内存页
    -- OS更新页表,将对应页表项的valid bit设置为1

DMA内存(Direct Memory Access)

  • DMA:设备绕过CPU直接访存
    -- 由于绕过CPU的MMU,因此直接访问物理地址
    -- 通常需要大段连续的物理内存
  • 操作系统必须有能力分配连续的物理页
    -- 需要用一种高效的方式来组织和管理物理页

总结

a

posted @   木木ちゃん  阅读(63)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
点击右上角即可分享
微信分享提示