0%

《程序员的自我修养-链接、装载与库》(一)操作系统的内存分配

在早期的计算机中,程序是直接运行在物理内存上的,程序在运行时所访问的地址都是物理地址。如果一个计算机同时只运行一个程序,这么操作不会有什么问题,只需要保证程序的内存空间需求不超过计算机物理内存的大小就行。然而现实是,为了更有效的利用硬件资源,计算机会同时运行多个程序。此时会出现一个问题,如何将计算机有限的物理内存分配给多个程序使用?

物理内存分配

我们来看一种简单的分配方式,假设我们的计算机有128MB的内存,程序A运行需要10MB,程序B需要100MB,程序C需要30MB。同时运行A和B,可以比较简单的将内存的前10MB分配给程序A,将10-110MB分配给程序B。这样可以实现程序A和B的同时运行,但是这样的内存分配策略存在明显的问题:

  • 地址空间不隔离:程序直接访问物理地址,程序所使用的内存空间不相互隔离。那么简直就是恶意程序的天堂,恶意程序可以很随意的改写其他程序的内存数据,为所欲为。除了恶意程序的侵入,还会有程序不小心修改了其他程序的数据。所以,地址空间不隔离会带来很严重的安全风险。
  • 内存使用效率低:由于没有有效的内存管理机制,通常需要执行一个程序时,系统会将整个程序载入内存中然后执行程序。例如,我们此时要执行程序C,但是程序A和B已经占用了110MB的内存,没有足够的空间给C,这时候会将其他程序的数据暂时写到磁盘里,等到要用的时候再读出来。因为程序内存是连续的,所以这里需要把程序B占用的空间数据写入到磁盘,然后在将程序C读入到内存中开始执行。下次程序B要运行时,再把程序C写入磁盘,程序B读入内存。可以看到整个过程中会有大量的数据读写,导致效率十分低下。
  • 程序运行地址不确定:程序每次载入运行时,需要为其分配一块足够大小的空闲内存空间,显然这个空闲区域每次是不固定的。这会给程序编写造成一定的麻烦,涉及到程序的重定位问题。

在计算机界有一句名言:

计算机领域的任何问题都可以通过增加一个中间层来解决。(Any problem in compute science can be solved by another layer of indirection.)

可以借助这一思想来解决上面的几个问题:增加中间层(用一种间接的地址访问方法)。把程序给出的地址看做是一种虚拟地址(Virtual Address),通过某种映射方法,将这个虚拟地址转换成实际的物理地址。这样,只要我们妥善掌控映射过程,就可以保证任意一个程序所能访问的物理内存区域跟另外一个程序互不重叠干扰,内存地址不能相互操作,已达到空间地址隔离的效果。
关于隔离,每个程序都有自己的进程,每个进程有自己的虚拟内存空间,在进程内只能访问自己的虚拟空间(程序间通信的部分除外,因为那是受程序允许的范围,这里不考虑),这样就有效的做到了进程间的隔离。
有了思路,那么具体如何设计呢?答案是分页。

分页(Paging)

把地址空间等分成固定大小的页,每一页大小有硬件或操作系统来决定。Intel公司设计了4KB和4MB页大小的芯片,目前市场上所有的PC几乎都使用的是4KB的页大小(我们使用的MAC是64位的系统,理论上64位最大支持的内存高达亿位数GB,实际上能支持到的是128GB,那么按每页4KB来计算,总共有33554432页)。来看一张图:

假设有8页虚拟内存,而物理内存只够支持6页,那么就会有一部分数据需要被保存在磁盘(Disk)中,当需要时再把它读出来即可。图中我们可以看到:

  • 有部分虚拟页面被存放在磁盘中,比如Process1的VP3、VP2位于磁盘的DP1、DP20中
  • 有部分虚拟页面没有被用或访问到,他们暂时处于未使用状态,比如VP4、VP5、VP6
  • 有部分虚拟页面被映射到了同一个物理页,实现了内存共享,比如PP3、PP0

Process1的VP2和VP3不在内存中,当进程需要使用这两个页的时候,硬件会捕获到这个信息,发生页错误(Page Fault)。然后由操作系统接管进程,负责将VP2和VP3从磁盘中读出来装入物理内存,再把这两个物理页与VP2、VP3建立映射关系。页映射还可以起到保护的作用,只需要给每个页设置相应的读写权限。
所有的硬件都采用一个叫MMU(Memory Management Unit)的部件来进行页映射:

页映射模式下,CPU发出的是Virtual Address,经过MMU(集成在CPU中)转换后变成Physical Address。

Page Fault

指当软件试图访问已映射在虚拟内存空间中,但是目前并未被加载在物理内存中的一个分页时,由CPU的内存管理单元所发出的中断。
通常情况下,用于处理此中断的程序是 操作系统的一部分。如果操作系统判断此次访问是有效的,那么操作系统会尝试将相关的分页从硬盘上的虚拟内存文件中调入内存。而如果访问是不被允许的,那么操作系统通常会结束相关的进程。
在iOS开发中,应用程序启动阶段就会有Page Fault,利用一系列手段(如二进制重排)优化Page Fault,可以减少应用的启动时间。