计算机除了计算能力之外还有存储能力,存储能力即计算机拥有一系列的存储介质,我们可以在存储介质上存储我们的代码和数据。计算机体系结构中约定了哪些地方可以用来存储数据:CPU内的寄存器、内存和外存。不同的存储介质,容量、速度和价格都是不同的。为了建立一个合理的系统,我们将计算机系统中的存储介质组织成一个层次结构。操作系统针对层次结构下的存储单元进行管理。操作系统的存储管理就是用来管理这些存储介质的。最基本的管理要求就是当一个进程需要使用存储单元的时候,操作系统能够分一块存储介质给它使用,等它不用的时候还回给操作系统。
计算机体系结构:包括CPU、内存和I/O设备。
CPU中有寄存器和高速缓存。寄存器的空间是非常小的,可能是32位或64位,能存的诗句也就几十字节或几百字节。CPU中还有高速缓存,高速缓存仅次于CPU寄存器,处理速度接近CPU频率,容量远小于内存,速度远快于内存,当CPU要读取的数据存在于高速缓存时(命中),可以直接返回数据而不需要访问内存,合理利用内存访问的局部性特点,可以达到极高的命中率。内存也是比较大的能存储数据的介质,他的最小访问单位是字节,也就是8bit,通常我们的计算机系统是32位的总线,也就是说可以同时从内存中读写32位也就是4字节。一次读写32位时牵扯到地址对齐,因此读写时不能从任意地方开始读一个4字节。
内存层次:
了解了上面这些,我们可以看下计算机中内存的层次关系:首先是处理器中有两级缓存,这一部分内存管理是由硬件实现的,因此用户或操作系统是无法显示地进行控制的。再下面一层则是内存,内存再下面一层可以是外存中的虚拟内存,这两个层次是由操作系统机型管理的。当CPU能层高速缓存中获取所需读取的内容无需访问内存,能够在内存中获取的内容无需访问虚拟内存,内存缺页时才会从虚拟内存中读取需要的内容。而这几个层次上的内存的访问速度是有这巨大差别的。最快的L1缓存可以达到接近CPU频率的速度,内存可以达到1.3GHz,外存较慢需要5ms,可以看到最快和最慢能差到百万数量级,如何将这几个层次有机地结合到一起,挑战性还是很大的。
内存管理:
我们希望将内存管理做到什么程度呢?如下图,系统中有不同的进程,进程通过操作系统内核访问物理内存,访问受内存管理单元(MMU)的控制。我们希望每个进程认为它自己独占内存,因此进程能看到的是逻辑地址空间。MMU负责将逻辑地址空间映射到真实的物理地址空间。而这些都对内存管理提出了很高的要求,主要需要满足如下几条:
- 抽象,进程使用逻辑地址空间;
- 保护,由逻辑地址空间映射到物理地址空间时不同进程之间不能访问到其他进程的内存;
- 共享,所用进程共用操作系统内核;
- 虚拟化,进程可以使用比实际内存空间更大的地址空间。
其中的保护性和共享性是相对矛盾的两个要求,既要保护好各自的空间,又要访问相同内存。甚至逻辑地址空间中看到的可以存储数据的空间是大于实际物理内存的。
操作系统为了达到上述效果,具体采用的内存管理方式有以下几条:
- 重定位。在最初的计算机中,是直接使用总线上的物理地址来编写程序的,程序要读写某个内存单元,直接访问它所在的物理地址。这有很大缺陷,你的程序只能在指定类型的机器上运行。重定位通过将进程中的逻辑地址加上重定位寄存器的值获得物理地址。这样通过修改重定位寄存器的值就可以将程序运行下去。为了实现,程序和操作系统里面都需要有相应的支持。
- 分段。实际我们在写程序的时候,逻辑结构并不是必须连成一片的区域,而是把程序分为数据、代码、堆栈。这三个部分是相对独立的。这样每一块需要的空间就变少了。分段依然需要每一段内的内容是连续的,这个要求依然很高。
- 分页。分页就是把内存分成最基本的单位,选取合适的大小的连续区域作为一页,一页作为内存管理的基本单位。
- 虚拟存储。目前多数系统(如Linux)采用按需页式虚拟存储。将不常用内容转移到外存上存储,这也使得逻辑地址空间可以大于实际物理内存空间。
上述4条内存管理方式的实现高度依赖硬件。比如与计算机存储架构紧耦合;MMU的结构是什么样的,CPU能够识别的页表是什么样的,也就是MMU如何处理CPU存储访问请求的硬件。
地址空间
从逻辑地址到最后在总线上出现的物理地址,有一个转换的过程,在学习具体的转换算法之前,我们需要学习地址生成的过程。生成过程中并不是所有地址都允许访问的,因此还需要对地址的合法性进行检查。
地址空间定义:计算机地址空间有两种,物理地址空间和逻辑地址空间。
- 物理地址空间就是硬件支持的真实地址空间,这是受硬件支持的,比如32位的系统,就是从0到4G-1的位置。
- 逻辑地址空间就是在CPU中运行的进程看到的地址,这个地址是从物理地址空间中分配的一段或几段子空间。
地址生成及处理过程:
整体可以分为如下几个步骤,ALU需要逻辑地址中的内容(读或写),MMU对逻辑地址进行转换,转换为物理地址,CPU控制逻辑给总线发送物理地址请求。内存发送物理地址的内容给CPU或者将CPU给的数据存储到物理地址。操作系统做的是建立逻辑地址LA和物理地址PA之间的映射。
逻辑地址生成:
生成过程如下图。比如最开始是高级语言写了一段代码,其中有一个函数foo();首先要进行编译,编译为机器能够识别的汇编语言;之后进行汇编,将函数名从字符串变为地址75;而这个函数可能是某个函数库中的成员,而函数库的位置程序是不知道的,这就需要链接,将需要的文件拼接起来,此时可能在原先的代码前面插入了一段代码,比如地址偏移了100,此时函数地址变为了175;最后需要进行程序加载即重定位,因为这段程序不一定是从逻辑地址的0位开始的,比如是从1000开始的,那么函数地址成为1175。经历了这么多步骤之后,生成的1175才是我们需要的逻辑地址。
那么逻辑地址是在什么时间生成的呢:
- 编译时。假定起始地址已知,也就是我知道我的程序要放在哪,那么编译时就可以生成逻辑地址。但是当起始地址改变时,比如修改了代码,必须重新编译。老旧的功能机买来之后不允许下载软件,这种情况下地址通常就是写死的。
- 加载时。当你修改了代码或者安装了各种新的软件,写程序时是无法知道新的代码存储在什么地址的。也就是编译时起始位置未知时,编译器需生成可重定位的代码,等到加载时重定位,生成绝对地址。通常在可执行文件的前面有一个重定位表,加载的时候改成绝对地址,程序就可以跑了。
- 执行时。执行到这条指令之前一直使用相对地址,当执行到这一条指令的时候,才可以去知道它确切访问的地址。这种情况出现在使用虚拟存储的系统里。优点就是在程序执行过程中就可以将代码移动,而前面两种生成时机都不可以。但是需要地址转化硬件支持,相对较麻烦。
地址检查
CPU执行到某条指令,得到它的逻辑地址,首先根据逻辑地址判断所在它的偏移量是否在所在段(比如数据段)的长度之内,如果超出了段长度,认为是非法请求,否则认为是合法的。此时加上段基址得到物理地址,进行访问。在这个过程中操作系统要做的就是设置段起始地址和最大逻辑地址空间(段长度)。