机制 地址转换
前面说到了关于内存的虚拟化,程序内部使用的其实都是虚拟地址,那么这里就涉及到一个虚拟基地和物理地址的映射方案。
类比前面的CPU虚拟化,在CPU虚拟化中,提出了一个概念叫:受限直接运行(Limited Direct Execution,LDE)。
这种模式下,程序本身可以运行大部分指令,也能操作硬件,只是在一些关键的节点上(比如一些系统调用或者发生时钟中断),需要操作系统介入和管控。
确保了程序能够在正确的时间,正确的地点做正确的事情。
这种模式带来的好处就是:高效 + 管控。
何为高效呢?这是因为在这种模式下,程序本身是有很大的自由度,操作系统不需要对进程的全部过程进行监管,自然就比较高效了。
另一方面的管控主要是说:在一些关键的节点上,操作系统还是可以收回对CPU的控制权的,不至于让程序彻底失控。
那么这里的内存虚拟化其实也是借鉴类似的原则和策略。
那内存虚拟化如何做到高效呢?最高效的方案莫过于引入硬件支持,硬件层面的速度永远都是最快的,但是响应的就会提高硬件设计的复杂度。
那么管控呢?在内存虚拟化这块的管控上,其实主要就是管控进程对内存的访问不会失控,也就是说不会访问到别的进程的地址空间,否则会出现很大的安全隐患;甚至进程本身也不能影响操作系统的地址空间。
这里又加入了一个新的目标:灵活性。所谓灵活性就是要能够达进程可以以任何方式来访问自己的地址空间,不要有过多的使用约束。
基于硬件的地址转换
这里提到了一个方案:基于硬件的地址转换(address translation),顾名思义:需要借助硬件来做这个虚拟地址和物理地址的转换。
同时也需要引入硬件来记录物理内存的使用情况,哪些还处在空闲状态,哪些已经被使用了。
假设
这里提出一些假设的前提,是一些看上去比较离谱的假设,不过不用担心,还是老样子,后面随着深入,会逐步放开这些假设条件。
- 用户的地址空间假设必须都是连续的存放在物理内存中的。(按照惯例,不给太大,暂定以64KB为例)
- 再假设进程的地址空间都不大,肯定都是在物理内存大小以内的。
- 最后假设每个地址空间大小一模一样,方便起见,这里暂定以16KB为例
举例
void func() {
int x;
x = x + 3;
}
这是书上给出的例子,先不要纠结x
未初始化的问题,假设这就是一个正常的程序,反汇编之后,对应的代码可能是:
128: movl 0x0(%ebx), %eax ;load 0+ebx into eax
132: addl $0x03, %eax ;add 3 to eax register
135: movl %eax, 0x0(%ebx) ;store eax back to mem
这里对应的汇编代码中,开头的数字假设为对应指令所处的虚拟地址,所以这里可以简单分析一下这个过程中的内存访问情况:
- 从128地址获取指令,
- 执行指令,在执行过程中,需要从某个地址上(假设虚拟地址为15KB)加载对应
x
的数据 - 接着从地址132中获取指令addl
- 然后执行指令,这里没有内存访问,只是简单加法计算
- 最后从135地址中取指令movl,将新的值存入到15KB这个地址中
这里面的视角实际上都是进程内的虚拟地址视角,但是实际上,可能这段地址空间在物理内存空间中处于32KB~48KB之间这一块。
动态(基于硬件)重定位
硬件层面需要引入一对寄存器:
- 基址寄存器
- 界限寄存器
基址寄存器中存储的实际上就是地址空间在物理内存中的起始地址,比如前面例子中的32KB这个值,那么响应的如果想要在地址空间内通过虚拟地址计算物理地址,就变成了一个加法运算:
physical address = base + virtual address;
既然有动态重定位,肯定有静态重定位,这里给了一个拓展知识点:静态重定位。
早期的时候,在没有硬件支持的情况,引入过使用纯软件的方法来控制地址转换,在进程进行内存操作的时候,这类软件就会暂时接管运行,把进程内对应的虚拟地址通过软件计算的方法,修改原进程指令中关于内存地址的值。
这种方法有两个弊端:
- 无法提供保护,也就说它只负责转换计算,但是没法做到对转换后的地址是否越界的管控。
- 因为是软件层面的转换,转换完成后就会直接操作物理内存了,一旦转换完成后,很难将地址空间重新定位到其他地方。
所以现在几乎已经不讨论使用静态转换的方案了。
前面主要说的都是基址寄存器,那界限寄存器的作用呢?它主要是保护用的。
静态重定位中说到的越界保护,就可以通过界限寄存器得到很好的落实。一般界限寄存器中存储的内容分两类:
- 如果存储的是地址空间的大小,那么在进行越界校验的时候,它就是先校验,只有校验通过了才进行后续的物理地址转换
- 如果存储的是地址空间对应的物理地址的结束地址,那在进行越界校验的时候,就是先进行物理内存转换计算,再将转换后的地址值和它进行对比校验
这里紧接着补充了一个知识点:空闲列表
操作系统需要记录哪些内存没有被使用,以便于能够进行内存分配。有多重数据结构可以完成这项操作,最简单的就是空闲列表(free list),它就是一个简单的列表,记录当前没有被使用的物理内存的范围。
硬件支持:总结
- 需要一对寄存器(基址寄存器 + 界限寄存器)
- 硬件需要提供一个特殊的指令,可以修改基址+界限寄存器的值,这主要是用于操作系统切换进程时使用的,所以这是一个特权操作
- 对于非法的访问,必须提供异常通报
- 对于越界的异常,进程访问不属于自己地址空间的内容时
- 越权异常,比如进程试图修改基址寄存器和界限寄存器的值
- 需要有异常对应的特殊指令,能够让操作系统知道对应的异常处理程序位置在哪儿
操作系统问题
为了支持动态重定位,硬件添加了新的功能, 使得操作系统有了一些必须处理的新问题。硬件支持和操作系统管理结合在一起,实现了一个简单的虚拟内存。
- 在进程创建时,操作系统能够为进程找到对应的空闲空间,这里可以考虑使用前面提到的空闲列表来保证
- 进程运行结束或者异常终止的时候,操作系统能够回收之前分配给该进程的地址空间,确保不会回收多了或者少了,同时回收后需要更新对应的一些状态表信息
- 上下文切换的事后,操作系统能够将当前进程对应的基址+界限寄存器的值存入到一个每个进程都有的数据结构中,比如进程控制块PCB这种,同时将下一个进程对应的基址+界限寄存器的值更新到当前的基址寄存器和界限寄存器中。对于需要进行地址空间转移的,在进程没有运行的时候,操作系统将原地址空间的内容拷贝到新的地址空间中,然后将新地址空间的基址和界限值存入到进程指定的数据结构中,这样在下次该进程再次被调度的时候,就可以通过这个新址来更新寄存器中的值。
- 需要有异常处理程序的支持,当发生越界行为的时候,CPU 会触发异常。在这种异常产生时,操作系统必须准备采取行动。通常操作系统会做出充满敌意的反应:终止错误进程。