学过操作系统原理的读者都知道,内存管理有两种,一种是段式管理,另一种是页式管理,而页式管理更为先进。从80年代中期开始,页式内存管理进入了各种操作系统(以Unix为主)的内核,一时成为操作系统领域的一个热点。
Intel从80286开始实现其保护模式,也即段式内存管理。但是很快发现,光有段式内存管理而没有也是内存管理是不够的,那样会使它的x86系列逐渐失去竞争力以及作为主流CPU产品的地位。因此,在不久以后的80386中就实现了对页式内存管理的支持。也就是说,80386除了完成并完善从80286开始的段式内存管理的同时还实现了页式内存管理。
前面讲过,80386的段式内存管理机制,是将指令中结合段寄存器使用的32位逻辑地址映射(转换)成同样是32位的物理地址。之所以称为物理地址,是因为这是真正放到地址总线上去,并用以寻访物理上存在着的具体内存单元的地址。但是,段式存储管理机制的灵活性和效率都比较差。一方面段是可变长的,这就给盘区交换操作带来了不便;另一方面,如果为了增加灵活性而将一个进程的空间划分成很多小段时,就势必要求在程序中频繁地改变段寄存器的内容。同时,如果将段分小,虽然一个段描述表中可以额容纳8192个描述项(因为有13位下标),也未必就能保证足够使用。所以,比较好的办法还是采用页式存储管理。本来,页式存储管理并不需要建立在段式存储管理的基础之上,这是两种不同的机制。可是,在80386中,保护模式的实现是与段式存储密不可分的。例如,CPU的当前执行权限就是在有关的代码段描述项中规定的。读过Unix早期版本的读者不妨将此与PDP-11中的情况做一对比。在PDP-11中CPU的当前执行权限放在一个独立的寄存器PSW中,而与任何其他的数据结构没有关系。因此,在80386中,尽然决定利用部分已经存在的资源,而不是完全另起炉灶,那就无法绕过段式存储管理来实现页式存管。也就是说,80386的系统结构决定了它的页式存管只能建立在段式存管的基础上。这也意味着,页式存管的作用是在段式存管所映射而成的地址上再加上一层地址映射。由于此时由段式存管而成的地址不再是物理地址了,Intel就称之为线性地址。于是,段式存管先将逻辑地址映射成线性地址,然后再由页式存管将线性地址映射成物理地址;或者,当不使用页式存管时,就将线性地址直接用作物理地址。
80386把线性地址空间划分成4K字节的页面,每个页面可以被映射至物理存储空间中任意一块4K字节大小的区间(边界必须与4K字节对齐)。在段式存管中,连续的逻辑地址经过映射后在线性地址空间还是连续的。但是在页式存管中,连续的地址经过映射后在物理空间却不一定连续(其灵活性也正在于此)。这里值得指出的是,虽然页式存管时建立在段式存管的基础上,但一旦启用了页式存管,所有的线性地址都要经过页式映射,连LGDTR与LDTR中给出的段描述表起始地址也不例外。
由于页式存管的引入,对32位的线性地址有了新的解释(以前就是物理地址):
typedef struct {
unsigned int dir:10;/*用作页面表目录中的下标,该目录项指向一个页面表*/
unsigned int page:10;/*用作具体页面表中的下标,该表项指向一个物理页面*/
unsigned int offset:12;/*在4K字节物理页面内的偏移量*/
}线性地址;
这个结构可以用下图形象的表示:
可以看出,在页面目录中共有2^10=1024个目录项,每个目录项指向一个页面表,而在每个页面表中又共有1024个页面描述项。类似于GDTR和LDTR,又增加了一个新的寄存器CR3作为指向当前页面目录中的指针。这样,从线性地址到物理地址的映射过程为:
- 从CR3取得页面目录的基地址。
- 以线性地址中的dir位段为下标,在目录中取得相应页面表的基地址。
- 以线性地址中的page位段为下标,在所得到的页面表中获取相应的页面描述项。
- 将页面描述项中给出的页面基地址与线性地址中的offset位段相加得到物理地址。
- 上列映射过程可用下图直观的表示。
那么,为什么要使用两个层次,先找到目录项,再找到页面描述项,而不是像在使用段寄存器时那样一步到位呢?这是出于空间效率的考虑。如果将线性地址中的dir和page两个位段合并在一起是20位,因此页面表的大小就将是1K*1K=1M个表项。由于每个页面的大小为4K字节,总的空间大小仍为4K*1M=4G,正好事32位地址空间的大小。但是,实际上很难想象有一个进程会需要用到4G的全部空间,所以大部分表项势必是空着的。可是,在一个数组中,即使是空着不用的表项也占用空间,这样就造成了浪费。而若是分成两层,则页面表可以视需求而设置,如果目录中某项为空,就不必设立相应的页表,从而省下了存储空间。当然,在最坏的情况下,如果一个进程真的要用到全部4G的存储空间,那就不仅不能节省,反而要消耗一个目录所占用的空间,但那概率基本上是0。另外,一个页面的大小是4K字节,而每一个页面表项或目录项的大小是4字节。1024个表项正好也是4K字节,恰好可以放在一个页面中。而若多于1024项就要使用目录或页面表跨页面存放了。也正为此,在64位的alpha CPU中页面的大小是8K字节,因为目录表项和页面表项的大小都变成了8个字节。
如前所述,目录项中含有指向一个页面表的指针,而页面表项中则含有指向一个页面起始地址的指针,由于页面表和页面起始地址都总是在4K字节的边界上,这些指针的低12位都永远是0.这样,在目录项和页表项中都只要有20位用于指针就够了,而余下的12位则可以用于控制或其他的目的。于是,目录项的结构为:
typedef struct{
unsigned int ptab:20;/*页表基地址高20位*/
unsigned int avail:3;/*供系统程序员使用*/
unsigned int g:1;/*global,全局性页面*/
unsigned int ps:1;/*页面大小,0表示4K字节*/
unsigned int reserved:1/*保留,永远为0*/
unsigned int a:1;/*accessed,已被访问过*/
unsigned int pcd:1;/*关闭(不使用)缓冲存储器*/
unsigned int pwt:1;/*write-through,用于缓冲存储器*/
unsigned int u_s:1;/*为0时表示系统(或超级)权限,为1时表示用户权限*/
unsigned int r_w:1;/*只读或可写*/
unsigned int p:1;/*为0时表示相应的页面不在内存中*/
}目录项;
目录项的直观表示如下图所示:
页表项的结构基本上与此相同,但没有页面大小为ps,所以第8位保留不用,但第7位(在目录项中保留不用)则为D(dirty)标志,表示该页面已经被写过,所以已经脏了。当页面表项或目录项中的最低位P为0时,表示相应的页面或页面表不在内存,根据其他一些有关寄存器的设置,CPU可以产生一个页面错(page fault)异常(也称为缺页异常,但异常和中断其实是有区别的)。这样,内核中的有关异常服务程序就可以从磁盘上的页面交换区将相应的页面读入内存,并且相应地设置表项中的基地址,并将p位是指成1。相反,也可以将内存中暂不使用的页面写入磁盘的交换区,然后将相应页面表项的p位设置成0。这样,就可以实现页式虚存了。当p位为0时,表项的其余各位均无意义。所以可被用来临时存储其他信息,如被换出的页面在磁盘上的位置等等。
当目录项中的ps(page size)位为0,包含在该目录项所指的页面表中的所有页面的大小斗士4K字节,这也是目前linux内核中采用的页面大小。但是,Pentium处理器开始,Intel引入PSE页面大小扩充机制。当ps位为1时,页面的大小就成了4M字节,而页面表就不再使用了。这时候,线性地址中的低22位就全部用作4M字节页面中的位移。这样,总的寻址能还没有改变,即1024*4M=4G,但是映射的过程减少了一个层次。随着内存容量和磁盘容量的日益增加,磁盘访问速度的显著提升,以及对图像处理要求的日益增加,4M字节的页面大小有坑会成为主流。在这一点上,Intel倒还是有远见的。
最后,i386 CPU中还有个寄存器CR0,其最高位PG是页式映射机制的总开关。当PG位被设置成1时,CPU就开启了页式存储管理的映射机制。
从Pentium pro开始,Intel又作了扩充。这一次扩充的是物理地址的宽度,Intel在另一个控制寄存器CR4中又增加一位PAE(表示physical address extension),当PAE位设置成1时,地址总线的宽度就变成了36位(又增加了4位)。与此相应,页式存管机制也自然地有所改变。不过大多数用户还不需要使用36位(64G)物理地址空间,所以这里从略,有兴趣的读者可以参阅Intel的有关技术资料或专著。此外,Intel已经推出了64位的IA-64系统架构,linux内核也已经支持IA-64系统架构。事实上,linux原来就已经在alpha CPU上支持64位地址。除存储管理外,80386还有很强的高数缓冲存储和流水线功能、。但是对于软件、对于操作系统的内核来说,拿在很大程度上是透明的,所以内核的系列博客在有必要时才加以简单的说明,而不在此详述了。