机制:地址转换

操作系统在实现对CPU的虚拟化时,采用的是受限直接执行机制(LDE)。LDE的目标是让程序大部分指令直接访问硬件,只在一些关键时机例如进程发起系统调用或者时钟中断时由操作系统介入来确保进程能够继续正确运行,这样可以实现操作系统对进程调度的高效性与可控性。在实现了高效的调度与控制之后,操作系统需要考虑的就是进程之间的安全性,也就是确保进程彼此之间的内存不会被相互影响,这就是对内存的虚拟化。当今,操作系统虚拟化内存的方式就是给每个进程分配一块独属于它的内存空间,每个进程只能访问自己的内存,无法访问超出自己内存的部分,来确保应用程序不会相互影响。这就需要一种技术,基于硬件的地址转换(hardware-based address translation),简称为地址转换。利用地址转换,进程每一次对内存的访问都会被处理,将应用程序的内存引用重定位到实际存储的物理内存地址。但是仅仅依靠地址转换是不够的,因为地址转换只能帮你得到对应的物理内存地址,转换得到的地址是否可用是否已经被占用还是需要操作系统自己来管理。

来看一个例子:

这是一段代码,定义一个变量为0,然后加三

int main()
{
    int a = 0;
    a = a + 1;

    return 0;
}

下面是他的汇编代码:

    int a = 0;
003617D8  mov         dword ptr [a],0  
    a = a + 1;
003617DF  mov         eax,dword ptr [a]  
003617E2  add         eax,1  
003617E5  mov         dword ptr [a],eax  

这段代码很容易理解,定义一个a将他的地址存放到寄存器eax上,然后通过add操作eax加一,之后将eax中的值写回到a。

这个过程中一共涉及到了三块内存,第一块就是这段汇编代码存放的内存,因为之前说过代码在程序运行期间是常量,所以通常会将代码存放在给进程分配的内存空间的最顶端,可以看下面这个图,其中的Program Code存放的也就是代码。所以第一个对内存的操作就是在这段内存中读取代码并执行。第二个对内存的操作也就是为定义的变量a申请一块栈内存,因为是一个局部变量所以存在栈里,那么对应下图也就是Stack段,可以看到Stack段是从15KB的位置开始,我们就认为a就放在15KB起的第一块上,对应上面的汇编代码也就是dword ptr [a],地址就是15KB的起始位置。第三个对内存操作是使用了eax这个寄存器。

                                   

对于进程自身来讲,上面那段代码的地址分配就是按照上面这个图,但是实际到计算机的物理内存当然就不是了,因为操作系统给不同进程分配的是不同地址的内存,假设都是16KB,那么也就是第一个进程在物理内存上的地址是0~16,第二个是17~32.,依次类推。但是在这个过程中会遇到一个问题,如果在出现第三个进程的时候,恰好进程结束了,那么0-16的内存也就是被释放了,此时进程3的内存该从33开始还是回到0?正常人想到的当然都是回到0使用,也就是进行资源重利用。那么这就用到了一个技术,基质加边界(base and bound),也称为动态重定位(dynamic relocation)。具体地说,每个CPU内部需要有两个硬件寄存器:基址(base)寄存器,和边界(bound)寄存器,通过这组基址和边界寄存器让我们能够将地址空间放置在物理内存中的任何地方,同时又能确保进程只能访问自己的地址空间。通俗来说就是将操作系统分配给进程的内存的起点认为是基址,终点认为是边界,进程只能在自己的内存基址到边界区间内进行操作,在对进程的内存地址进行转换时,计算方式就是:

物理地址  =  基址地址 + 虚拟内存地址(偏移)

其实在动态重定位出现之前,开发者们曾经使用过一种静态重定位(static relocation)的技术,这种技术是通过一个地址加载程序来将要运行的可执行程序的地址写到物理内存中期望的偏移位置。也就是说当出现进程一,它需要1KB的内存,那么会分配给他0-1,然后出现了进程二,它需要10KB的内存,会分配给他1-11。但是这种方式很容易出现问题,最简单的就是内存越界问题,如果进程一某一次误操作了超出1KB的内存,那么很可能就会修改了原本是分配给进程二的内存空间,导致进程二也出现问题。于是后来就被动态重定位所替代。

动态重定位不会出现类似于静态重定位那种内存越界的问题就是因为有了边界寄存器。在动态重定位的机制下,当一个进程想要操作超过它的边界地址的内存时,CPU会触发异常,然后会强制停止进程的操作甚至可能直接终止进程。

操作系统所做的远不止对越界的处理,从一个进程创建开始,操作系统就需要开始进行控制管理。首先操作系统需要去找到一块足够大小的空闲内存分配给进程,那么这就需要操作系统知道当前的物理内存哪一部分是空闲的,也就是操作系统上空闲列表(free list)的存在。操作系统在空闲列表上找到一块需要的足够大小的内存后,就将这块内存从空闲列表上移除,也就是标记为已经被使用,然后这块内存就分配给了对应的进程。然后在进程终止时,操作系统需要做一套相反的操作,也就是将之前分配给进程的内存重新写回到空闲列表上等待下一次的分配。此外,在进行上下文切换时,操作系统也要做一些操作。因为每个CPU每次只能执行一个进程(多个进程的同时执行是对CPU的虚拟化造成的假象),那么一个CPU也就只有一对基址、边界寄存器,在每次进行上下文切换时,都需要将CPU上的基址边界寄存器修改为切换到的进程的寄存器,同时需要保存刚刚切换走的进程的寄存器地址。在进程停止执行时,操作系统还可以对进程的基址和边界进行重分配,也就是将进程的地址空间拷贝到一个新的位置,然后用新的位置替换旧的寄存器地址,等进程再次运行时,操作的就是新的内存地址了。

 

通过地址转换,操作系统实现了控制每个进程中的内存访问,确保进程访问的地址空间始终在允许范围内。这个技术的效率最关键部分其实是硬件支持,只有硬件可以将虚拟地址快读转换为物理地址才能实现真正的内存虚拟化。

但是,动态重分配也有他的问题,也就是很可能会产生大量的内存碎片。例如,创建的每个进程分配的内存都是16KB,但是,如果进程栈和堆不是太大,所以两者之间的所有空间都被浪费了,这样的浪费被称为内部碎片(internal fragmentation),因为所分配的内存空间并不是全部使用的,是碎片化的,因此被浪费了。当然这种问题操作系统是在进行优化,最简单的优化方式就是对基址边界的概念进行泛华,也就是分段(segmentation)技术,这里就不详细叙述了,会有写一篇关于分段的详细描述的文章。

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值