Overview

  • 回过头来看这篇。来个总结:
    • linux在内存管理上主要是两个概念(抽象):
      • 分页paging:最主要的贡献是,运行物理内存不连续分配,但是在用户层面不可感知(仍然认为自己拥有独立且连续的可用内存空间)   -->  通过分页+页表来实现
      • 虚拟内存:允许可使用的内存比实际物理内存大。   -->  通过磁盘 + 页面置换来实现。
    • 优点:
      • 内存映射:paging之后,可以直接映射磁盘上的文件(可执行文件or动态库)到虚拟地址空间,从而做到物理内存的延时分配,只有在需要读相应文件时,才真正从磁盘加载到内存,而在内存紧张时,又清空。(这一切对应用程序都是透明的。)
      • 共享内存:比如动态库,只需要在内存中存储一部分即可,然后将其映射到不同进程的虚拟地址空间中。[进程间的内存共享也可以这样实现。]
  • 内存管理basic
    • 内存管理
      • 基本概念
      • 交换
      • 连续内存(分配)
      • 分页
    • 虚拟内存
      • 按需调页
      • 写时复制
      • 页面置换
  • 优化

 

Memory Management Basic

内存管理

Background

  • 基本硬件
    • CPU内置寄存器:访问快,CPU可以在一个时钟周期内解析并执行多个指令
    • 内存:CPU通常需要暂停,访问内存慢
    • 高速缓存cache:缓解寄存器与内存之间的速度差异。
  • 进程访问:需要确保每个进程都有独立的内存空间。
    • 内存空间的保护:
      • 通过基地址寄存器和界限地址寄存器来保护。
      • 特权指令只可在内核模式下执行,所以用户程序不能更改上述寄存器值。
  • 地址绑定
    • 源程序中地址是符号地址表示的(如count)
    • 编译器通常将这些符号地址绑定到可重定位的地址(如“本模块开始的第14字节”)
    • 链接程序或加载程序再将这些可重定位地址绑定成绝对地址(如74014)。
  • 动态加载dynamic loading
    • 一个子程序都以可重定位的形式保存在磁盘上,只有在调用时才被加载。
    • 为了获得更好的内存空间使用率(不用的子程序决不会被加载)
  • 动态链接dynamic linking
    • 二进制镜像中对每个库程序的引用都有一个存根(sub),存根是一小段代码,用来指出如何定位适当的内存驻留库程序,或如果该程序不在内存时应如何装入库。
    • 可用于库更新,一个库可以被新的版本所替代,而不需要重新链接。

交换

  • 进程可以暂时从内存中交换(swap)到备份存储(通常是快速磁盘)上。
  • 如果一个程序的绑定是在汇编or加载时就确定的,那么该进程需要交换回它原来所占有的内存空间。

内存分配

  • 内存通常分为两个区域:
    • 一个用于驻留操作系统
    • 一个用于用户进程
  • 为进程分配内存
    • 多分区方法:最简单的方法是将内存分为多个固定大小的分区,每个分区只能容纳一个进程。
    • 可变分区(variable partition):
      • os有一个表记录哪些内存可用,哪些内存已被占用。
      • 任何时候,有一组可用孔(hole)大小列表和输入队列(进程)。
      • 如何选择一个孔来分配:
        • 首次适应(first-fit): 分配第一个足够大的孔
        • 最佳适应(best-fit): 分配最小的孔
        • 最差适应(worst-fit): 分配最大的孔
    • 外部碎片问题(external fragmentation):
      • 外部碎片 vs 内部碎片:
        • 如果孔太小,那么维护一个小孔的开销比孔本身大得多  
        • --> 因此通常将内存以固定大小的块为单元(而不是字节)来分配
        • --> 给进程分配的内存 减去 进程申请的 = 内部碎片
        • 而外部碎片显然就是进程之间的小碎片
      • solution:
        • 紧缩(compaction):
          • 解决外部碎片的方法
          • 移动内存内容,以便所有空闲空间合并成一整块。
          • 但紧缩并非总是可能的:仅在重定位是动态并在运行时可采用
        • 允许物理地址空间非连续:
          • 分页
          • 分段

分页

  • 分页paging内存管理方案
  • 目的:允许进程的物理地址空间是可以非连续的
  • 实现方法:
    • 基本方法是涉及将物理内存分为固定大小的块,称为帧frame。而将逻辑内存也分为同样大小的块,称为页page。
    • 当需要执行进程时,其页从备份存储中调入到可用内存帧中。
    • 页表包含每页所在的物理内存的基地址。(由基地址与页偏移组合形成物理地址)。
  • --> 用户视角的内存和实际的物理内存的分离。
  • 页大小
    • 由硬件决定
    • 通常为2的幂  --> 这样可以方便地将逻辑地址转换为页号和页偏移。如果逻辑地址空间为2^m,页大小为2^n单元(字节or字),那么逻辑地址的高m-n未表示页号,低n位表示页偏移。
    • 从512B~16MB不等
  • 碎片问题:采用分页技术不会产生外部碎片,但会产生内部碎片。
  • 硬件支持:
    • 通常会为每个进程分配一个页表,页表的指针与其他寄存器的值(例如PC)一起存入进程控制块。
    • 页表的硬件实现:
      • 将页表作为一组专用寄存器来实现。 (maybe不适用于页表很大的场景)
      • --> 因为对内存的每次访问都需要经过分页表,因此效率很重要。
      • 大多数当代计算机都允许页表非常大,因此需要将页表放在内存中,并将页表基寄存器(page-table base register, PTBR)指向页表。
      • --> 在这种场景下,访问用户内存位置需要多一些时间:访问一个字节需要两次内存访问,从而速度减半。
      • --> 改进是采用小但专用且快速的硬件缓存:转换缓冲区(translation look-aside buffer,TLB)。[本质上就是缓存,所以可能出现TLB失效。]

虚拟内存

  • 虚拟内存技术允许执行进程不必完全在内存中
  • --> 优点:程序可以比物理内存大。

背景

  • 程序有一些几乎不执行的代码:比如处理异常错误的代码
  • 数组、链表和表通常分配了比实际所需更多的内存
  • 程序的某些功能很少使用

实现:按需调页

  • 按需调页demand paging:在需要时才调入相应的页。
    • “定义”需要的时候:
  • 懒惰交换lazy swapper
  • pager调页程序:
    • 当换入进程时,pager推测在该进程再次换出之前会用到哪些页

写时复制

  • 通过采用类似页面共享的技术,采用系统调用fork创建进程的开始阶段可能不需要按需调页。
    • 提供了快速进程创建,且最小化新建进程必须分配的新页面的数量
  • 采用写时复制copy-on-write技术:运行父子进程开始时共享同一页面。
    • 这些页面标记为写时复制页。

页面置换

  • 页面置换page replacement
  • 基本页置换
    • 基本方法:如果没有空闲帧,那么就查找当前没有使用的帧,并将其释放。
    • 如何释放:将其内容写到交换空间,并改变页表,以表示该页不在内存中。
  • FIFO页置换
    • 为每个页记录着该页调入内存的时间
  • 最优置换
    • 置换最长时间不会使用的页
    • 难以实现,因为需要用到引用串的未来之食
  • LRU置换
    • least recently used
  • 近似LRU置换

优化

  • 有了分页 + 物理内存,就满足了正常工作的基本条件。但还有一些使内存访问更高效的optimization。

虚拟地址: 物理地址

MMU

  • MMU(Memory Management Unit)是CPU的一个用来将进程的虚拟地址转换成物理地址的模块。
  • 将虚拟地址转换成物理地址的速度直接影响着系统的速度,所以CPU包含了这个模块用来加速。

TLB

  • TLB(Translation Lookaside Buffer)
  • 上述MMU的输入是page table,而page table又存储在内存中,跟CPU的cache相比,内存的速度很慢。
  • 所以TLB是用来加速虚拟地址到物理地址的转换的。
  • TLB存在于CPU的L1 cache内,用来缓存已经找到的映射。

huge pages

  • 由于CPU的cache有限,所以TLB内缓存的数据也有限,而采用了huge pages后,由于每页的内存变大,TLB记录数不变却能覆盖更大的地址空间。
  • 同时也能减少调用MMU的次数,加快了地址转换。

内存大小

  • 由于实际情况下,物理内存要比虚拟内存小很多,所以os必须很小心的分配物理内存,以最大化内存利用率。
  • 按需分配物理页
  • 交换空间
    • 发生场景:实际物理内存不够时。
    • 使用场景:
      • 被修改过的磁盘文件。[如果物理内存里面的数据来自于磁盘上的文件,那么内核将直接将该部分数据从内存中移除掉来释放出更多的内存,当下次有进程需要访问这部分数据时,再将它从磁盘上加载到内存中来。]
      •   --> 但是,如果这部分数据被修改过且没被写入文件,那这部分数据就变成了脏数据,脏数据不能被直接删掉,只能被移动到交换空间上去。
      • 对于anonymous的内存数据,在磁盘上没有对应的文件,这部分数据不能直接被删除,而是被系统移到交换空间上去。
      •       --> 注意,可执行文件和动态库文件不会被修改,但通过mmap+private的方式映射到内存的磁盘文件有可能被修改,这种方式映射的内存比较特殊,没修改之前是file backed,修改后但没有写回磁盘之前就变成了anonymous的。
    • 总结:交换空间就是磁盘上预留的一块特殊空间,被系统用来临时存放内存中不常被访问的数据,当下次有进程需要访问交换空间上的数据时,系统再将数据加载到内存中。由于交换空间在磁盘上,所以访问速度要比内存慢很多,频繁的读写交换空间会带来性能问题。

 Summary

  • 内存的使用过程:
    1. 进程发出内存申请请求
    2. 系统会检查进程的虚拟地址空间是否还有剩余,若有,给进程分配虚拟地址
    3. 系统为这块虚拟地址创建相应的memory mapping(可能多个),并将它放进该进程的page table。
    4. 系统返回虚拟地址给进程,进程开始访问该虚拟地址
    5. CPU根据虚拟地址在该进程的page table中找到相应的memory mapping,若该mapping没和物理内存关联,则会产生缺页中断。 [按需分配]
    6. os收到缺页中断后,分配真正的物理内存并将它关联到相应的memory mapping。
    7. 中断完成后,CPU就可以访问该内存了。
  •       --> 缺页中断不是每次都会发生,只有系统觉得有必要延迟分配内存的时候才发生。否则在创建memory mapping的时候会直接分配物理内存并关联。