内存管理

虚拟内存

  • 虚拟内存:操作系统为了将进程和物理内存隔离,设计的一种内存映射。

内存分段

虚拟地址

  • 段选择子
    • 段号(段表的索引)
    • 特权标志位
  • 段内偏移量(0 ~ 段界限)
  • 段表
    • 段的基地址
    • 段的界限
    • 权限等级
  • 物理地址:段的基地址 + 段内偏移量

内存分段机制

  • 将程序的虚拟地址分为4个段,每个段在段表中有一项,在这一项中找到段的基地址,再加上偏移量,就能找到物理内存中的地址:
    • 代码段
    • 数据段
    • 堆段
    • 栈段

内存分段的问题

  • 内存碎片
    • 外部内存碎片:产生多个不连续的小物理内存,导致新的程序无法被装载。
    • 内部内存碎片:程序的所有内存都被装载到物理内存中,但部分内存不常使用,导致内存浪费。
    • 解决方式
      • 内存交换:通过交换技术,减少碎片。
  • 内存交换效率低
    • 每次内存交换都需要将内存写到硬盘上,再从硬盘上读回到内存,硬盘访问速度较慢,导致性能瓶颈。
    • 解决方式
      • 内存分页:通过分页管理提高效率。

内存分页

概念

  • 将整个虚拟和物理内存空间切成固定大小的页(在Linux下,每一页的大小为4KB)。
  • 虚拟内存和物理内存之间通过页表来映射。

内存管理单元 (MMU)

  • MMU:负责将虚拟内存转换为物理内存。
  • 当进程访问的虚拟地址在页表中查不到时,会产生一个缺页异常,进入内核空间分配物理内存、更新页表、最后返回用户空间,恢复进程的运行。

分页的优势

  • 解决内存碎片:分页释放的内存是以页为单位,不会产生无法使用的小内存碎片。
  • 提高内存交换效率:内存不足时,可以将不常使用的内存页面换出,换入时只需加载少量页,提高了效率。
  • 延迟加载:程序运行时才加载需要的内存页面。

虚拟内存地址的结构

  • 页号:页表的索引。
    • 页表包含每页物理地址的基地址,加上偏移量即可得到物理地址。
  • 页内偏移量:页内的具体位置。

内存地址转换过程

  1. 将虚拟内存地址切分为页号和偏移量。
  2. 根据页号,从页表查询对应的物理页号。
  3. 将物理页号加上偏移量,得到物理内存地址。

简单页表的问题

  • 页表占用过多内存:在32位系统下,一个进程的页表需要100多万个页表项,占用4MB内存,多个进程会消耗大量内存。

解决方法:多级页表

  • 多级页表:将页表再进行分页,形成页表的页表。
    • 二级页表:一级页表覆盖整个虚拟地址空间,未被使用的一级页表项无需创建二级页表项,节省内存。
    • 多级页表:64位系统中有4级页表:
      • PGD(Page Global Directory):全局页
      • PUD(Page Upper Directory):上层页
      • PMD(Page Middle Directory):中间页
      • PTE(Page Table Entry):页表项

多级页表转换效率问题

  • TLB(Translation Lookaside Buffer):在CPU中存放最常访问的页表项的缓存,提升页表查找效率。

段页式内存管理

实现原理

  • 结合内存分段和内存分页:
    • 先将程序划分为多个有逻辑意义的段。
    • 再将段划分为多个页。

访问过程

  1. 第一次访问段表,得到页表起始地址。
  2. 访问页表,得到物理页号。
  3. 将物理页号与页内偏移组合,得到物理地址。

优势

  • 提高内存的利用率。

Linux 内存管理

虚拟地址空间分布

  • 内核空间
    • 32位系统为1GB,64位系统为128TB,占据最高位。
  • 用户空间
    • 32位系统为4GB,64位系统为128TB,占据最低位。

内核空间和用户空间的区别

  • 进程在用户态时只能访问用户空间内存。
  • 进入内核态后,才能访问内核空间内存。
  • 每个虚拟内存中的内核地址实际上关联相同的物理内存,方便进程切换到内核态后访问内核空间。

用户空间分布

  • 用户空间从低到高依次包含:
    • 程序文件段:二进制可执行代码。
    • 已初始化数据段:静态常量。
    • 未初始化数据段:未初始化的静态常量。
    • 堆段:动态分配的内存,从低地址向上增长。
    • 文件映射段:动态库、共享内存等,从低地址向上增长。
    • 栈段:局部变量和函数调用的上下文,栈大小一般为8MB。

Linux 的内存管理策略

  • Linux 主要采用分页管理,但由于 Intel 处理器的发展史,无法避免分段管理。
  • Linux 将所有段的基地址设置为0,相当于所有程序的地址空间都是线性地址空间,段仅用于访问控制和内存保护。
  • 用户态内存分布:代码段、全局变量、BSS、堆内存、映射区、函数栈。