Refer to <<linux
内核源代码情景分析
>> and <<Linux kernel Version:2.4.0>>
Having any problems, send mails to viloner@163.com
i386
页式内存管理机制
内存管理有两种,一种是段式管理,另一种是页式管理,而页式管理更为先进。
80386
的段式内存管理机制,是将指令中结合段寄存器使用的
32
位逻辑地址映射
(
转换
)
成同样是
32
位的物理地址。之所以称为“物理地址”,是因为这是真正放到地址总线上去,并用以寻访物理上存在着的具体内存单元的地址。但是,段式存储管理机制的灵活性和效率都比较差。一方面“段”是可变长度的,这就给盘区交换操作带来了不便
;
另一方面,如果为了增加灵活性而将一个进程的空间划分成很多小段时,就势必要求在程序中频繁地改变段寄存器的内容。同时,如果将段分小,虽然一个段描述表中可容纳
8192
个描述项
(
因为有
13
位下标
)
,也未必就能保证足够使用。所以,比较好的办法还是采用页式存储管理。本来,页式存储管理并不需要建立在段式存储管理的基础上,这是两种不同的机制。可是,在
80386
中,保护模式的实现是与段存储密不可分的。例如,
CPU
的当前执行权限是在有关的代码段描述项中规定的。因此,在
80386
中,既然决定利用部分已经存在的资源,而不是另起炉灶,那就无法绕过段式存储管理来实现页式存储管理。这也意味着,页式存储管理的作用是在由段式存储管理所映射而成的地址上再加上一层地址映射。由于此时由段式存储管理映射而成的地址不再是“物理地址”了,
Intel
就称之为“线性地址”。于是,段式存储管理先将逻辑地址映射成线性地址,然后再由页式存储管理将线性地址映射成物理地址,或者,当不使用页式管理时,就将线性地址直接用作物理地址。
80386
把线性地址空间划分成
4K
字节的页面,每个页面可以被映射至物理存储空间中任意一块
4K
字节大小的区间
(
边界必须与
4K
字节对齐
)
。在段式存储管理中,连续的逻辑地址经过映射后在线性地址空间还是连续的。但是在页式存储管理中,连续的线性地址经过映射后在物理空间却不一定连续
(
其灵活性也正在于此
)
。这里值得指出的是,虽然页式存储管理是建立在段式存储管理的基础上,但一旦启用了页式存储管理,所有的线性地址都要经过页式映射,连
GDTR
与
LDTR
中给出的段描述表起始地址也不例外。
由于页式存储管理的引入,对
32
位线性地址有了新的解释
(
以前就是物理地址
)
:
typedef struct {
unsignedint dir: 10; /*
用作页面表目录中的下标,该目录项指向一个页面表
*/
unsignedint page: 10; /*
用作具体页面表中的下标,该表项指向一个物理页面
*/
unsignedint offset: 12; /*
在
4K
字节物理页面内的偏移量
*/
}
线性地址
;
这个结构可用下图形象地表示:
线性地址的格式
可以看出,在页面目录中共有
210=1024
个目录项,每个目录项指向一个页面表,而在每个页面表中又共有
1024
个页面描述项。类似于
GDTR
和
LDTR
,又增加了一个新的寄存器
CR3
作为指向当前页面目录的指针。这样,从线性地址到物理地址的映射过程为:
1、
从
CR3
取得页面目录的基地址。
2、
以线性地址中的
dir
位段为下标,在目录中取得相应页面表的基地址。
3、
以线性地址中的
page
位段为下标,在所得到的页面表中取得相应的页面描述项。
4、
将页面描述项中给出的页面基地址与线性地址中的
offset
位段相加得到物理地址。
上述映射过程可用下图直观地表示:
页式映射示意图
那么为什么要使用两个层次,先找到目录项,再找到页面描述项,而不是像在使用段寄存器时那样一步到位呢?这是出于空间效率的考虑。如果将线性地址中的
dir
和
page
两个位段合并在一起是
20
位,因此页面表的大步就将是
1Kx1K=1M
个表项。由于每个页面的大小为
4K
字节,总的空间大小仍为
4Kx1M=4G
,正好是
32
位地址空间的大小。但是,实际上是很难想象有一个进程会需要用到
4G
的全部空间,所以大部分青藏势必是空着的。可是,在一个数组中,即使是空着的表项也占空间,这样就造成了浪费。而若分成两层,则页表可以视需要而设置,如果目录中某项为空,就不必设立相应的页表,从而省下了存储空间。当然,在最坏的情况下,如果一个进程真的要用到全部
4G
的存储空间,那就不仅不能节省,反而要多消耗一个目录所占用的空间,但那概率基本上是
0.
另外,一个页面的大小是
4K
字节,而每一个页面表项或目录项的大小是
4
个字节。
1024
个表项正好也是
4K
字节,恰好可以放在一个页面中。而若多于
1024
项就要使目录或页面表跨页面存放了。也正因如此,在
64
位的
AlphaCPU
中页面的大小是
8K
字节,因为目录项和页面表项的大小都变成了
8
个字节。如前所述,目录项中含有指向一个页面表的指针,而页面表项中则含有指向一个页面起始地址的指针。由于页面表和页面起始地址都总是在
4K
字节的边界上,这些指针的低
12
位都永远是
0.
这样,在目录项和页面项中都只要有
20
位用于指针就够了,而余下的
12
位则可以用于控制或其他的目的。于是,目录项的结构为:
typedef struct {
unsignedint ptba: 20;/*
页表基地址的高
20
位
*/
unsignedint avail: 3;/*
供系统程序员使用
*/
unsignedint g: 1/*global,
全局性页面
*/
unsignedint ps: 1/*
页面大小,
0
表示
4K
字节
*/
unsignedint reserved: 1/*
保留,永远是
0*/
unsignedint a: 1;/*accessed,
已被访问过
*/
unsignedint pcd:1;/*
关闭
(
不使用
)
缓冲存储器
*/
unsignedint pwt:1;/*write Through,
用于缓冲存储器
*/
unsignedint u_s:1;/*
为
0
时表示系统
(
或超级
)
权限,为
1
时表示用户权限
*/
unsignedint r_w: 1;/*
只读或可写
*/
unsignedint 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
字节页面中的位移。这样,总的寻址能力还是没有改变,即
1024x1024=4G
,但是映射的过程减少了一个层次。随着内存容量和磁盘容量的日益增加,磁盘访问速度的显著提高,以及对图像处理要求的日益增加,
4M
字节的页面大小有可能成为主流。
最后,
i386CPU
中还有个寄存器
CR0,
其最高位
PG
是页式映射机制的总开关。当
PG
位被设置成
1
时,
CPU
就开启了页式存储管理的映射机制。
从
Pentium Pro
开始,
Intel
又作了扩充。这一次扩充的是物理地址的宽度。
Intel
在另一个控制寄存器
CR4
中又增加了一个
PAE(Physical Address Extension)
,当
PAE
位设置成
1
时,地址总线的宽度就变成了
36
位。与此相应,页式存储管理的映射机制也自然地有所改变。