final version

This commit is contained in:
Wende Tan 2019-02-26 19:13:39 +08:00
parent 94db30c3ff
commit 948200449a
2 changed files with 30 additions and 68 deletions

View File

@ -2,16 +2,17 @@
**建立二级页表**
80368的采用了二级页表来建立线性地址与物理地址之间的映射关系。由于我们已经具有了一个物理内存页管理器default\_pmm\_manager支持动态分配和释放内存页的功能我们就可以用它来获得所需的空闲物理页。在二级页表结构中页目录表占4KB空间可通过alloc\_page函数获得一个空闲物理页作为页目录表Page Directory TablePDT。同理ucore也通过这种类似方式获得一个页表(Page Table,PT)所需的4KB空间。
Intel 80386采用了二级页表来建立线性地址与物理地址之间的映射关系。由于我们已经具有了一个物理内存页管理器default\_pmm\_manager支持动态分配和释放内存页的功能我们就可以用它来获得所需的空闲物理页。在二级页表结构中页目录表占4KB空间可通过alloc\_page函数获得一个空闲物理页作为页目录表Page Directory TablePDT。同理ucore也通过这种类似方式获得一个页表Page TablePT所需的4KB空间。
整个页目录表和页表所占空间大小取决与二级页表要管理和映射的物理页数。假定当前物理内存0~16MB每物理页也称Page Frame大小为4KB则有4096个物理页也就意味这有4个页目录项和4096个页表项需要设置。一个页目录项(Page Directory EntryPDE)和一个页表项(Page Table EntryPTE)占4B。即使是4个页目录项也需要一个完整的页目录表占4KB。而4096个页表项需要16KB即4096*4B的空间也就是4个物理页16KB的空间。所以对16MB物理页建立一一映射的16MB虚拟页需要5个物理页即20KB的空间来形成二级页表。
整个页目录表和页表所占空间大小取决与二级页表要管理和映射的物理页数。假定当前物理内存0~16MB每物理页也称Page Frame大小为4KB则有4096个物理页也就意味这有4个页目录项和4096个页表项需要设置。一个页目录项Page Directory EntryPDE和一个页表项Page Table EntryPTE占4B。即使是4个页目录项也需要一个完整的页目录表占4KB。而4096个页表项需要16KB即4096*4B的空间也就是4个物理页16KB的空间。所以对16MB物理页建立一一映射的16MB虚拟页需要5个物理页即20KB的空间来形成二级页表。
为把0\~KERNSIZE明确ucore设定实际物理内存不能超过KERNSIZE值即0x38000000字节896MB3670016个物理页的物理地址一一映射到页目录项和页表项的内容其大致流程如下
完成前一节所述的前两个阶段的地址映射变化后,为把0\~KERNSIZE明确ucore设定实际物理内存不能超过KERNSIZE值即0x38000000字节896MB3670016个物理页的物理地址一一映射到页目录项和页表项的内容其大致流程如下
1. 先通过alloc\_page获得一个空闲物理页用于页目录表
2. 调用boot\_map\_segment函数建立一一映射关系具体处理过程以页为单位进行设置
1. 指向页目录表的指针已存储在boot_pgdir变量中。
2. 映射0~4MB的首个页表已经填充好。
3. 调用boot\_map\_segment函数进一步建立一一映射关系具体处理过程以页为单位进行设置
```
virt addr = phy addr + 0xC0000000
linear addr = phy addr + 0xC0000000
```
设一个32bit线性地址la有一个对应的32bit物理地址pa如果在以la的高10位为索引值的页目录项中的存在位PTE\_P为0表示缺少对应的页表空间则可通过alloc\_page获得一个空闲物理页给页表页表起始物理地址是按4096字节对齐的这样填写页目录项的内容为
```
@ -27,10 +28,9 @@ virt addr = phy addr + 0xC0000000
* PTE\_W位2表示物理内存页内容可写
* PTE\_P位1表示物理内存页存在
ucore
的内存管理经常需要查找页表给定一个虚拟地址找出这个虚拟地址在二级页表中对应的项。通过更改此项的值可以方便地将虚拟地址映射到另外的页上。可完成此功能的这个函数是get\_pte函数。它的原型为
ucore的内存管理经常需要查找页表给定一个虚拟地址找出这个虚拟地址在二级页表中对应的项。通过更改此项的值可以方便地将虚拟地址映射到另外的页上。可完成此功能的这个函数是get\_pte函数。它的原型为
```c
pte_t *get_pte (pde_t *pgdir, uintptr_t la, bool create)
pte_t *get_pte(pde_t *pgdir, uintptr_t la, bool create)
```
下面的调用关系图可以比较好地看出get\_pte在实现上述流程中的位置
@ -38,71 +38,24 @@ pte_t *get_pte (pde_t *pgdir, uintptr_t la, bool create)
图6 get\_pte调用关系图
这里涉及到三个类型pte t、pde t和uintptr
t。通过参见mm/mmlayout.h和libs/types.h可知它们其实都是unsigned
int类型。在此做区分是为了分清概念。
这里涉及到三个类型pte\_t、pde\_t和uintptr\_t。通过参见mm/mmlayout.h和libs/types.h可知它们其实都是unsigned int类型。在此做区分是为了分清概念。
pde\_t全称为 page directory
entry也就是一级页表的表项注意pgdir实际不是表
而是一级页表本身。实际上应该新定义一个类型pgd\_t来表示一级页表本身。pte
t全 称为 page table entry表示二级页表的表项。uintptr
t表示为线性地址由于段式管理只做直接映射所以它也是逻辑地址。
pde\_t全称为 page directory entry也就是一级页表的表项注意pgdir实际不是表项而是一级页表本身。实际上应该新定义一个类型pgd\_t来表示一级页表本身。pte\_t全称为 page table entry表示二级页表的表项。uintptr\_t表示为线性地址由于段式管理只做直接映射所以它也是逻辑地址。
pgdir给出页表起始地址。通过查找这个页表我们需要给出二级页表中对应项的地址。
虽然目前我们只有boot\_pgdir一个页表但是引入进程的概念之后每个进程都会有自己的页
表。
pgdir给出页表起始地址。通过查找这个页表我们需要给出二级页表中对应项的地址。虽然目前我们只有boot\_pgdir一个页表但是引入进程的概念之后每个进程都会有自己的页表。
有可能根本就没有对应的二级页表的情况所以二级页表不必要一开始就分配而是等到需要的时候再添加对应的二级页表。如果在查找二级页表项时发现对应的二级页表不存在则需要根据create参数的值来处理是否创建新的二级页表。如果create参数为0则get\_pte返回NULL如果create参数不为0则get\_pte需要申请一个新的物理页通过alloc\_page来实现可在mm/pmm.h中找到它的定义再在一级页表中添加页目录项指向表示二级页表的新物理页。注意新申请的页必须全部设定为零因为这个页所代表的虚拟地址都没有被映射。
当建立从一级页表到二级页表的映射时,需要注意设置控制位。这里应该设置同时设置
上PTE\_U、PTE\_W和PTE\_P定义可在mm/mmu.h。如果原来就有二级页表或者新建立了页表则只需返回对应项的地址即可。
当建立从一级页表到二级页表的映射时需要注意设置控制位。这里应该设置同时设置上PTE\_U、PTE\_W和PTE\_P定义可在mm/mmu.h。如果原来就有二级页表或者新建立了页表则只需返回对应项的地址即可。
虚拟地址只有映射上了物理页才可以正常的读写。在完成映射物理页的过程中,除了要象上面那样在页表的对应表项上填上相应的物理地址外,还要设置正确的控制位。有关
x86 中页表控制位的详细信息请参照《Intel® 64 and IA-32 Architectures
Software Developer s Manual Volume 3A》4.11 节。
虚拟地址只有映射上了物理页才可以正常的读写。在完成映射物理页的过程中,除了要象上面那样在页表的对应表项上填上相应的物理地址外,还要设置正确的控制位。有关 x86 中页表控制位的详细信息请参照《Intel® 64 and IA-32 Architectures Software Developer s Manual Volume 3A》4.11 节。
只有当一级二级页表的项都设置了用户写权限后,用户才能对对应的物理地址进行读写。
所以我们可以在一级页表先给用户写权限,再在二级页表上面根据需要限制用户的权限,对物理页进行保护。由于一个物理页可能被映射到不同的虚拟地址上去(譬如一块内存在不同进程
间共享当这个页需要在一个地址上解除映射时操作系统不能直接把这个页回收而是要先看看它还有没有映射到别的虚拟地址上。这是通过查找管理该物理页的Page数据结构的成员变量ref用来表示虚拟页到物理页的映射关系的个数来实现的如果ref为0了表示没有虚拟页到物理页的映射关系了就可以把这个物理页给回收了从而这个物理页是free的了可以再被分配。page\_insert函数将物理页映射在了页表上。可参看page\_insert函数的实现来了解ucore内核是如何维护这个变量的。当不需要再访问这块虚拟地址时可以把这块物理页回收并在将来用在其他地方。取消映射由page\_remove来做这其实是page
insert的逆操作。
只有当一级二级页表的项都设置了用户写权限后用户才能对对应的物理地址进行读写。所以我们可以在一级页表先给用户写权限再在二级页表上面根据需要限制用户的权限对物理页进行保护。由于一个物理页可能被映射到不同的虚拟地址上去譬如一块内存在不同进程间共享当这个页需要在一个地址上解除映射时操作系统不能直接把这个页回收而是要先看看它还有没有映射到别的虚拟地址上。这是通过查找管理该物理页的Page数据结构的成员变量ref用来表示虚拟页到物理页的映射关系的个数来实现的如果ref为0了表示没有虚拟页到物理页的映射关系了就可以把这个物理页给回收了从而这个物理页是free的了可以再被分配。page\_insert函数将物理页映射在了页表上。可参看page\_insert函数的实现来了解ucore内核是如何维护这个变量的。当不需要再访问这块虚拟地址时可以把这块物理页回收并在将来用在其他地方。取消映射由page\_remove来做这其实是page\_insert的逆操作。
建立好一一映射的二级页表结构后,接下来就要使能分页机制了这主要是通过enable\_paging函数实现的这个函数主要做了两件事
建立好一一映射的二级页表结构后由于分页机制在前一节所述的前两个阶段已经开启分页机制到此初始化完毕。当执行完毕gdt\_init函数后新的段页式映射已经建立好了。
1. 通过lcr3指令把页目录表的起始地址存入CR3寄存器中
2. 通过lcr0指令把cr0中的CR0\_PG标志位设置上。
执行完enable\_paging函数后计算机系统进入了分页模式但到这一步还没建立好完整的段页式映射。还记得ucore在最开始通过kern\_entry函数设置了临时的新段映射机制吗这个临时的新段映射不是最简单的对等映射导致虚拟地址和线性地址不相等。这里需要注意刚进入分页模式的时刻是一个过渡过程。在这个过渡过程中虚拟地址线性地址以及物理地址之间的映射关系为
```
virt addr = linear addr + 0xC0000000 = phy addr + 2 * 0xC0000000
```
而我们希望的段页式映射的最终映射关系为:
```
virt addr = linear addr = phy addr + 0xC0000000
```
这里最终的段映射是简单的段对等映射virt addr = linear addr。所以我们需要进一步调整段映射关系即重新设置新的GDT建立对等段映射。在这个特殊的阶段如果不把段映射关系改为virt addr = linear addr则通过段页式两次地址转换后无法得到正确的物理地址。为此我们需要进一步调用gdt\_init函数根据新的gdt全局段描述符表内容gdt定义位于pmm.c中恢复简单的段对等映射关系即使得virt addr = linear addr。这样在执行完gdt\_init后通过的段机制和页机制实现的地址映射关系为
```
virt addr=linear addr = phy addr +0xC0000000
```
这里存在的一个问题是在调用enable\_page函数到执行gdt\_init函数之前内核使用的还是旧的段表映射
```
virt addr = linear addr + 0xC0000000 = phy addr + 2 * 0xC0000000
```
如何保证此时内核依然能够正常工作呢其实只需让index为0的页目录项的内容等于以索引值为(KERNBASE>>22)的目录表项的内容即可。目前内核大小不超过
4M 实际上是3M因为内核从 0x100000开始编址这样就只需要让页表在0\~4MB的线性地址与KERNBASE \~ KERNBASE+4MB的线性地址获得相同的映射即可都映射到 0\~4MB的物理地址空间具体实现在pmm.c中pmm\_init函数的语句
```c
boot_pgdir[0] = boot_pgdir[PDX(KERNBASE)];
```
实际上这种映射也限制了内核的大小。当内核大小超过预期的3MB
就可能导致打开分页之后内核crash在后面的试验中也的确出现了这种情况。解决方法同样简单就是拷贝更多的高地址对应的页目录项内容到低地址对应的页目录项中即可。
当执行完毕gdt\_init函数后新的段页式映射已经建立好了上面的0\~4MB的线性地址与0\~4MB的物理地址一一映射关系已经没有用了。
所以可以通过如下语句解除这个老的映射关系。
```c
boot_pgdir[0] = 0;
```
在page\_init函数建立完实现物理内存一一映射和页目录表自映射的页目录表和页表后一旦使能分页机制则ucore看到的内核虚拟地址空间如下图所示
在pmm\_init函数建立完实现物理内存一一映射和页目录表自映射的页目录表和页表后ucore看到的内核虚拟地址空间如下图所示
![](../lab2_figs/image008.png)
图7 使能分页机制后的虚拟地址空间图
图7 最终虚拟地址空间图

View File

@ -45,7 +45,14 @@ SECTIONS {
**第二个阶段**创建初始页目录表开启分页模式从kern\_entry函数开始到pmm_init函数被执行之前。
这一阶段启动了页映射机制,启动后虚拟地址、线性地址以及物理地址之间的临时映射关系为:
编译好的ucore自带了一个设置好的页目录表和相应的页表将0~4M的线性地址一一映射到物理地址。
了解了一一映射的二级页表结构后接下来就要使能分页机制了这主要是通过几条汇编指令在kern/init/entry.S中实现的主要做了两件事
1. 通过`movl %eax, %cr3`指令把页目录表的起始地址存入CR3寄存器中
2. 通过`movl %eax, %cr0`指令把cr0中的CR0\_PG标志位设置上。
执行完这几条指令后,计算机系统进入了分页模式!虚拟地址、线性地址以及物理地址之间的临时映射关系为:
```
lab2 stage 2 before:
@ -53,9 +60,11 @@ SECTIONS {
virt addr = linear addr = phy addr + 0xC0000000 # 线性地址在0xC0000000~0xC0000000+4MB之内三者的映射关系
```
可以看到其实仅仅比第一个阶段增加了下面一行的0xC0000000偏移的映射。
可以看到其实仅仅比第一个阶段增加了下面一行的0xC0000000偏移的映射并且作用范围缩小到了0~4M。在下一个节点会将作用范围继续扩充到0~KMEMSIZE
然后使用了一个绝对跳转来使内核跳转到高虚拟地址代码在kern/init/entry.S中
实际上这种映射限制了内核的大小。当内核大小超过预期的4MB 实际上是3M因为内核从 0x100000开始编址就可能导致打开分页之后内核crash在某些试验中也的确出现了这种情况。解决方法同样简单就是正确填充更多的页目录项即可。
此时的内核EIP还在0~4M的低虚拟地址区域运行而在之后这个区域的虚拟内存是要给用户程序使用的。为此需要使用一个绝对跳转来使内核跳转到高虚拟地址代码在kern/init/entry.S中
```asm
# update eip