程序装载

  1. 背景
    • 通过链接器,把多个目标文件合并成一个最终可执行文件
    • 运行可执行文件时,其实是通过一个装载器,解析ELF或者PE格式的可执行文件
      • 装载器会把对应的指令和数据加载到内存里面,让CPU去执行
  2. 装载器需要满足两个条件
    • 可执行程序加载后占用的内存空间应该是连续
      • 执行程序时,程序计数器是顺序地一条一条指令执行下去
    • 需要同时加载很多个程序,并且不能让程序自己规定在内存中加载的位置
  3. 内存地址
    • 虚拟内存地址:指令里用到的内存地址
    • 物理内存地址:内存硬件里的空间地址
  4. 一个思路
    • 物理内存里面找一段连续的内存空间,分配给装载的程序
      • 然后把这段连续的内存空间地址和整个程序指令里指定的内存地址做一个映射
    • 程序里有指令和各种内存地址,而我们只需要关心虚拟内存地址即可
    • 对任何一个程序来说,它所看到的都是同样的内存地址
      • 维护一个虚拟内存物理内存映射
      • 实际程序指令执行的时候,会通过虚拟内存地址,找到对应的物理内存地址,然后执行
    • 因为是连续的内存地址空间,只需要维护映射关系的起始地址对应的空间大小即可

内存分段

  1. 分段(Segmentation):找出一段连续的物理内存和虚拟内存地址进行映射
  2. 段:系统分配出来的那个连续的内存空间
  3. 解决了程序本身不需要关心具体的物理内存地址的问题,但仍然存在两个问题
  • 内存碎片(Memory Fragmentation)
  • 性能瓶颈(内存交换用到了硬盘,如果需要换出的程序所占用的物理内存很大,就会很慢)

内存碎片

内存交换

  1. 内存交换:Memory Swapping
  2. 把Python程序占用的256MB内存写到硬盘上,然后再从硬盘读回来到内存里面
  • 读回来的时候,不再把它加载到原来的位置,而是紧跟在已经占用了512MB内存后面
  • 这样就有了连续的256MB的内存空间
  1. Linux会分配一个Swap硬盘分区,专门给Linux进行内存交换的

性能瓶颈

  1. 虚拟内存内存分段内存交换的组合仍然会遇到性能瓶颈
  2. 硬盘的访问速度要比内存慢很多!!
  • 每次内存交换,都需要把一大段连续的内存数据写入到硬盘

内存分页

  1. 内存分段的主要问题:内存碎片 + 内存交换的空间太大
  2. 分段是分配一整段连续的空间给到程序,分页是把整个物理内存空间切成一段段固定尺寸的大小
  • 而对应程序所需要占用的虚拟内存空间,也同样切成一段段固定尺寸的大小
  • 这样一个连续且尺寸固定的内存空间,叫作
  1. 从虚拟内存到物理内存的映射,不再是拿整段连续内存的物理地址,而是按照一个个
  2. 页的尺寸一般远小于整个程序的大小,Linux下,页大小通常是4KB
  3. 这样,内存空间都是预先划分好了,就没有了不能使用的内存碎片,而只有被释放出来的很多4KB的页
  • 即使内存空间不够,需要让现有的、正在运行的其它程序,通过内存交换释放出一些内存的出来
  • 这样,一次性写入磁盘的也只有少数的一个页或者几个页,不会花太多的时间
  1. 内存分页的思路:化整为零!!
1
2
$ getconf PAGE_SIZE
4096

缺页错误

  1. 分页的方式使得在加载程序的时候,不再需要一次性都把程序加载到物理内存中
  • 完全可以在进行虚拟内存和物理内存的页之间的映射之后,并不把页加载到物理内存里面
  • 只在程序运行中,需要用到对应虚拟内存里面的指令和数据时,再加载到物理内存里面去 – 懒加载
  1. 操作系统
  • 当要读取特定的页,却发现数据并没有加载到物理内存里面,就会触发一个来自于CPU的缺页错误(Page Fault)
  • 操作系统会捕捉到缺页错误,然后将对应的页,从存放在硬盘上的虚拟内存读取出来,加载到物理内存里面
  1. 好处
  • 可以运行那些远大于我们实际物理内存的程序(如大型游戏)
  • 任何程序都不需要一次性加载完所有指令和数据,按需加载即可
  1. 通过虚拟内存内存分页内存交换的结合,可以让程序不再需要考虑物理地址程序加载内存管理等问题
  • 对任何一个程序来说,都只需要把内存当成一块完整且连续的空间来直接使用即可

参考资料

深入浅出计算机组成原理