五. 开启分页机制

为什么有进行内存分页

目前我们的小kernel还一直在分段机制下工作,因为还只有一个loader在内存中跑,所以不会出现内存不足的问题。假如说此时未开启分页功能,而物理内存空间又不足了,如下图

mark

此时进程C想要执行,但是内存空间已经不足。要么就等待进程A或者进程B执行完成,这样就有连续的内存空间了。要么就讲进程A的A3段或者进程B的B1段换到硬盘上,腾出一部分空间,同样可以容纳进程C执行

等待是极其不好的用户体验,那么只能将段置换到硬盘上了,但是段的大小并不固定,如何段过大,那么IO操作过多,机器的响应速度就会非常慢。

出现这种情况的本质其实是在分段机制下,线性地址等价于物理地址。那么即使在进程B的下面还有10M的可用空间,但因为两块可用空间并不连续,所以进程C无法使用进程B下面的10M可用空间。

按照这种思路,只需要通过某种映射关系,将线性地址映射到任意的物理地址,就可以解决这种问题了。实现线性地址的连续,而物理地址不需要连续,于是分页机制就诞生了。

一级页表

分页机制是工作在分段机制下的,在保护模式下,通过选择子找到段基址,通过段基址:段内偏移的方式组合成线性地址,拿到线性地址之后会根据是否开启分页来找到实际的物理地址,用一副图来解释更加清晰

mark

分页机制的作用有两方面

  1. 将线性地址转换成物理地址
  2. 用大小相等的页代替大小不等的段

如图所示:

mark

需要通过分页机制来映射的线性地址便有了一个高大上的名字,虚拟地址

假设我们通过逐字节的映射方式

mark

那么页表中会存放4GB个页表项,页表的大小=4GB*4=16GB,这样显然不合理,一页不能只占1B

我们需要平衡页的大小与页的数量的关系,因为页大小*页数量=4GB,想要减少页表的大小,只能增加一页的大小。最终通过数学求极限,定下4KB为最佳页大小

mark

这种情况下,4GB的内存被划分为1MB个内存块,每个内存块的大小为4KB,

页表和内存的映射关系如图

mark

有了页表之后,如何将线性地址转换成物理地址呢?

在一级页表下,线性地址的高20位被用作页表项的索引,也就是类似于数组下标的东西。通过该索引在页表中找到对应页的物理地址,然后将该物理地址+线性地址的低12位组成真正的物理地址。过程如图所示

mark

二级页表

无论是几级页表,标准页的尺寸都是4KB,这个是不会变的。所以4GB的线性地址空间最多有1M个标准页。一级页表是将这1M个标准页放置到一张页表中,二级页表是将这1M个标准页平均放置1K个页表中,每个页表包含有1K个页表项。页表项是4字节大小,故页表的大小同样为4KB

既然将原本的一个页表划分出了1K个页表,这些页表就必须进行统一管理。为此,专门有一个页目录表来存放这些页表。页目录表中存储的页表称为页目录项(PDE), 页目录项同样为4KB,且最多有1K个页目录项,所以页目录表也是4KB

二级页表的模型如图

mark

二级页表与一级页表原理虽然相同,但在结构上有了很大的差异,所以虚拟地址到物理地址的转换方式上也发生了很大变化

首先通过虚拟地址的高10位在页目录表中定位一个页表,也就是定位也目录项

然后通过虚拟地址的中间10位在之前定位的页表中找到物理页所在的位置

最后虚拟地址剩下的12位作为找到的物理页的页内偏移

地址转换过程如图所示

mark

PDE与PTE的结构

mark

P位:存在位,为1时表示该页在内存中
RW:读写位,为1时可读可写,为0是可读不可写
US:普通用户/超级用户位,为1时表示处于用户级,也就是3级特权级
PWT:页级通写位,为1表示此项采用通写方式,表示该页不仅是普通内存,还是高速缓存
PCD:页级高速缓存禁止位,为1表示该页启用高速缓存
A:访问位,为1表示该页被CPU访问过
D:脏页位,当CPU对一个页面执行写操作,此为被赋1
PAT:页属性表位,能够在页面一级的粒度上设置内存属性
G:全局位,为1表示该页在高速缓存TLB中一直保存

启用分页机制

启用分页机制需要完成下面三步

  1. 准备好页目录表和页表
  2. 将页表地址写入控制寄存器Cr3
  3. 将寄存器Cr0的PG位置1

创建页目录表和页表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
----------创建页目录及页表----------
setup_page:
mov ecx, 4096
mov esi, 0
.clear_page_dir:
mov byte [PAGE_DIR_TABLE_POS + esi], 0
inc esi
loop .clear_page_dir

.create_pde:
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x1000 ; 此时eax为第一个页表的位置及属性
mov ebx, eax ; 此处为ebx赋值,是为.create_pte做准备,ebx为基址。

; 下面将页目录项0和0xc00都存为第一个页表的地址,
; 一个页表可表示4MB内存,这样0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的页表,
; 这是为将地址映射为内核地址做准备
or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性RW和P位为1,US为1,表示用户属性,所有特权级别都可以访问.
mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第1个目录项,在页目录表中的第1个目录项写入第一个>页表的位置(0x101000)及属性(7)
mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 一个页表项占用4字节,0xc00表示第768个页表占用的目录项,0xc00以上的目录项用于内核空间,
; 也就是页表的0xc0000000~0xffffffff共计1G属于内核,0x0~0xbfffffff共计3G属于用户进程.
sub eax, 0x1000
mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最后一个目录项指向页目录表自己的地址

;下面创建页表项(PTE)
mov ecx, 256 ; 1M低端内存 / 每页大小4k = 256
mov esi, 0
mov edx, PG_US_U | PG_RW_W | PG_P ; 属性为7,US=1,RW=1,P=1
.create_pte:
mov [ebx+esi*4],edx ; 此时的ebx已经在上面通过eax赋值为0x101000,也就是第一个页表的地址
add edx,4096
inc esi
loop .create_pte
;创建内核其它页表的PDE
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000 ; 此时eax为第二个页表的位置
or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性US,RW和P位都为1
mov ebx, PAGE_DIR_TABLE_POS
mov ecx, 254 ; 范围为第769~1022的所有目录项数量
mov esi, 769
.create_kernel_pde:
mov [ebx+esi*4], eax
inc esi
add eax, 0x1000
loop .create_kernel_pde
ret

1
2
3
4
5
6
7
8
; 把页目录地址赋给cr3
mov eax, PAGE_DIR_TABLE_POS
mov cr3, eax

; 打开cr0的pg位(第31位)
mov eax, cr0
or eax, 0x80000000
mov cr0, eax