update lab2 for gitbook

update lab2 for gitbook
This commit is contained in:
chyyuu 2014-10-12 22:35:33 +08:00
parent f76449e19f
commit ea22dc5ff2
36 changed files with 979 additions and 979 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
_book
*.bak
*.org
*~

View File

@ -1,11 +1,32 @@
# Documents of uCore labs
# Summary
* [Introduction](README.md)
* [Lab 0](lab0.md)
* [Lab 1](lab1.md)
* [Lab 2](lab2.md)
* [Lab 2 物理内存管理](lab2.md)
* [实验目的](lab2/phymemlab_goal.md)
* [实验内容](lab2/phymemlab_contents.md)
* [练习](lab2/phymemlab_exercise.md)
* [项目组成](lab2/phymemlab_files.md)
* [物理内存管理](lab2/phymem_manage.md)
* [实验执行流程概述](lab2/phymemlab_overview.md)
* [探测系统物理内存布局](lab2/search_phymem_layout.md)
* [以页为单位管理物理内存](lab2/phymem_pagelevel.md)
* [物理内存页分配算法实现](lab2/phymem_allocation.md)
* [实现分页机制](lab2/paging.md)
* [段页式管理基本概念](lab2/segment_and_paging.md)
* [建立段页式管理中需要考虑的关键问题](lab2/key_problems_in_seg_page.md)
* [建立虚拟页和物理页帧的地址映射关系](lab2/setup_paging_map.md)
* [不同运行阶段的地址映射关系](lab2/maping_relations.md)
* [自映射机制](lab2/self_mapping.md)
* [实验报告要求](lab2/phymemlab_require.md)
* [附录A. 探测物理内存分布和大小的方法](lab2/probe_phymem_methods.md)
* [附录B. 实现物理内存探测](lab2/implement_probe_phymem.md)
* [附录C. 链接地址/虚地址/物理地址/加载地址以及edata/end/text的含义](phymemlab_concepts.md)
* [Lab 3](lab3.md)
* [Lab 4](lab4.md)
* [Lab 5](lab5.md)
* [Lab 6](lab6.md)
* [Lab 7](lab7.md)
* [Lab 8](lab8.md)

978
lab2.md
View File

@ -1,978 +1,2 @@
# 实验二:物理内存管理
## 1 实验目的
* 理解基于段页式内存地址的转换机制
* 理解页表的建立和使用方法
* 理解物理内存的管理方法
## 2 实验内容
# 物理内存管理
实验一过后大家做出来了一个可以启动的系统,实验二主要涉及操作系统的物理内存管理。操作系统为了使用内存,还需高效地管理内存资源。在实验二中大家会了解并且自己动手完成一个简单的物理内存管理系统。
本次实验包含三个部分。首先了解如何发现系统中的物理内存;然后了解如何建立对物理内存的初步管理,即了解连续物理内存管理;最后了解页表相关的操作,即如何建立页表来实现虚拟内存到物理内存之间的映射,对段页式内存管理机制有一个比较全面的了解。本实验里面实现的内存管理还是非常基本的,并没有涉及到对实际机器的优化,比如
针对 cache 的优化等。实际操作系统如Linux等中的内存管理是相当复杂的。
如果大家有余力,尝试完成扩展练习。
### 2.1 练习
**练习0填写已有实验**
本实验依赖实验1。请把你做的实验1的代码填入本实验中代码中有“LAB1”的注释相应部分。提示可采用merge工具比如kdiff3eclipse中的diff/merge工具understand中的diff/merge工具等。
**练习1实现 first-fit 连续物理内存分配算法(需要编程)**
在实现first fit
内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。提示:在建立空闲页块链表时需要按照空闲页块起始地址来排序形成一个有序的链表。可能会修改default\_pmm.c中的default\_initdefault\_init\_memmapdefault\_alloc\_pages
default\_free\_pages等相关函数。请仔细查看和理解default\_pmm.c中的注释。
**练习2实现寻找虚拟地址对应的页表项需要编程**
通过设置页表和对应的页表项可建立虚拟内存地址和物理内存地址的对应关系。其中的get\_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址如果此二级页表项不存在则分配一个包含此项的二级页表。本练习需要补全get\_pte函数
in
kern/mm/pmm.c实现其功能。请仔细查看和理解get\_pte函数中的注释。get\_pte函数的调用关系图如下所示
![image](lab2/image001.png)
图1 get\_pte函数的调用关系图
**练习3释放某虚地址所在的页并取消对应二级页表项的映射需要编程**
当释放一个包含某虚地址的物理内存页时需要让对应此物理内存页的管理数据结构Page做相关的清除处理使得此物理内存页成为空闲另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解page\_remove\_pte函数中的注释。为此需要补全在
kern/mm/pmm.c中的page\_remove\_pte函数。page\_remove\_pte函数的调用关系图如下所示
![image](lab2/image002.png)
图2 page\_remove\_pte函数的调用关系图
**扩展练习Challenge任意大小的内存单元slub分配算法需要编程**
如果觉得上诉练习难度不够可考虑完成此扩展练习。实现两层架构的高效内存单元分配第一层是基于页大小的内存分配第二层是在第一层基础上实现基于任意大小的内存分配。比如如果连续分配8个16字节的内存块当分配完毕后实际只消耗了一个空闲物理页。要求时空都高效可参考slub算法来实现可简化实现能够体现其主体思想即可。要求有设计文档。slub相关网页在[http://www.ibm.com/developerworks/cn/linux/l-cn-slub/](http://www.ibm.com/developerworks/cn/linux/l-cn-slub/)
。完成challenge的同学可单独提交challenge。完成得好的同学可获得最终考试成绩的加分。
### 2.2项目组成
表1 实验二文件列表
|-- boot
| |-- asm.h
| |-- bootasm.S
| \`-- bootmain.c
|-- kern
| |-- init
| | |-- entry.S
| | \`-- init.c
| |-- mm
| | |-- default\_pmm.c
| | |-- default\_pmm.h
| | |-- memlayout.h
| | |-- mmu.h
| | |-- pmm.c
| | \`-- pmm.h
| |-- sync
| | \`-- sync.h
| \`-- trap
| |-- trap.c
| |-- trapentry.S
| |-- trap.h
| \`-- vectors.S
|-- libs
| |-- atomic.h
| |-- list.h
\`-- tools
|-- kernel.ld
相对与实验一,实验二主要增加和修改的文件如上表红色部分所示。主要改动如下:
* boot/bootasm.S增加了对计算机系统中物理内存布局的探测功能
* kern/init/entry.S根据临时段表重新暂时建立好新的段空间为进行分页做好准备。
* kern/mm/default\_pmm.[ch]提供基本的基于链表方法的物理内存管理分配单位为页即4096字节
* kern/mm/pmm.[ch]pmm.h定义物理内存管理类框架struct
pmm\_manager基于此通用框架可以实现不同的物理内存管理策略和算法(default\_pmm.[ch]
实现了一个基于此框架的简单物理内存管理策略)
pmm.c包含了对此物理内存管理类框架的访问以及与建立、修改、访问页表相关的各种函数实现。
* kern/sync/sync.h为确保内存管理修改相关数据时不被中断打断提供两个功能一个是保存eflag寄存器中的中断屏蔽位信息并屏蔽中断的功能另一个是根据保存的中断屏蔽位信息来使能中断的功能可不用细看
* libs/list.h定义了通用双向链表结构以及相关的查找、插入等基本操作这是建立基于链表方法的物理内存管理以及其他内核功能的基础。其他有类似双向链表需求的内核功能模块可直接使用list.h中定义的函数。
* libs/atomic.h定义了对一个变量进行读写的原子操作确保相关操作不被中断打断。可不用细看
* tools/kernel.ldld形成执行文件的地址所用到的链接脚本。修改了ucore的起始入口和代码段的起始地址。相关细节可参看附录C。
**编译方法**
编译并运行代码的命令如下:
```
make
make qemu
```
则可以得到如下显示界面(仅供参考)
```
chenyu$ make qemu
(THU.CST) os is loading ...
Special kernel symbols:
entry 0xc010002c (phys)
etext 0xc010537f (phys)
edata 0xc01169b8 (phys)
end 0xc01178dc (phys)
Kernel executable memory footprint: 95KB
memory managment: default_pmm_manager
e820map:
memory: 0009f400, [00000000, 0009f3ff], type = 1.
memory: 00000c00, [0009f400, 0009ffff], type = 2.
memory: 00010000, [000f0000, 000fffff], type = 2.
memory: 07efd000, [00100000, 07ffcfff], type = 1.
memory: 00003000, [07ffd000, 07ffffff], type = 2.
memory: 00040000, [fffc0000, ffffffff], type = 2.
check_alloc_page() succeeded!
check_pgdir() succeeded!
check_boot_pgdir() succeeded!
-------------------- BEGIN --------------------
PDE(0e0) c0000000-f8000000 38000000 urw
|-- PTE(38000) c0000000-f8000000 38000000 -rw
PDE(001) fac00000-fb000000 00400000 -rw
|-- PTE(000e0) faf00000-fafe0000 000e0000 urw
|-- PTE(00001) fafeb000-fafec000 00001000 -rw
--------------------- END ---------------------
++ setup timer interrupts
100 ticks
100 ticks
……
```
通过上图我们可以看到ucore在显示其entry入口地址、etext代码段截止处地址、edata数据段截止处地址、和enducore截止处地址的值后探测出计算机系统中的物理内存的布局e820map下的显示内容。接下来ucore会以页为最小分配单位实现一个简单的内存分配管理完成二级页表的建立进入分页模式执行各种我们设置的检查最后显示ucore建立好的二级页表内容并在分页模式下响应时钟中断。
## 3 物理内存管理
### 3.1 实验执行流程概述
本次实验主要完成ucore内核对物理内存的管理工作。参考ucore总控函数kern\_init的代码可以清楚地看到在调用完成物理内存初始化的pmm\_init函数之前和之后是已有lab1实验的工作好像没啥修改。其实不然ucore有两个方面的扩展。首先bootloader的工作有增加在bootloader中完成了对物理内存资源的探测工作可进一步参阅附录A和附录B让ucore
kernel在后续执行中能够基于bootloader探测出的物理内存情况进行物理内存管理初始化工作。其次bootloader不像lab1那样直接调用kern\_init函数而是先调用位于lab2/kern/init/entry.S中的kern\_entry函数。kern\_entry函数的主要任务是为执行kern\_init建立一个良好的C语言运行环境设置堆栈而且临时建立了一个段映射关系为之后建立分页机制的过程做一个准备细节在3.5小节有进一步阐述。完成这些工作后才调用kern\_init函数。
kern\_init函数在完成一些输出并对lab1实验结果的检查后将进入物理内存管理初始化的工作即调用pmm\_init函数完成物理内存的管理这也是我们lab2的内容。接着是执行中断和异常相关的初始化工作即调用pic\_init函数和idt\_init函数等这些工作与lab1的中断异常初始化工作的内容是相同的。
为了完成物理内存管理这里首先需要探测可用的物理内存资源了解到物理内存位于什么地方有多大之后就以固定页面大小来划分整个物理内存空间并准备以此为最小内存分配单位来管理整个物理内存管理在内核运行过程中每页内存设定其可用状态free的used的还是reserved的这其实就对应了我们在课本上讲到的连续内存分配概念和原理的具体实现接着ucore
kernel就要建立页表
启动分页机制让CPU的MMU把预先建立好的页表中的页表项读入到TLB中根据页表项描述的虚拟页Page与物理页帧Page
Frame的对应关系完成CPU对内存的读、写和执行操作。这一部分其实就对应了我们在课本上讲到内存映射、页表、多级页表等概念和原理的具体实现。
在代码分析上建议根据执行流程来直接看源代码并可采用GDB源码调试的手段来动态地分析ucore的执行过程。内存管理相关的总体控制函数是pmm\_init函数它完成的主要工作包括
1. 初始化物理内存页管理器框架pmm\_manager
2. 建立空闲的page链表这样就可以分配以页4KB为单位的空闲内存了
3. 检查物理内存页分配算法;
4. 为确保切换到分页机制后,代码能够正常执行,先建立一个临时二级页表;
5. 建立一一映射关系的二级页表;
6. 使能分页机制;
7. 从新设置全局段描述符表;
8. 取消临时二级页表;
9. 检查页表建立是否正确;
10. 通过自映射机制完成页表的打印输出(这部分是扩展知识)
另外,主要注意的相关代码内容包括:
* boot/bootasm.S中探测内存部分从probe\_memory到finish\_probe的代码
* 管理每个物理页的Page数据结构在mm/memlayout.h中这个数据结构也是实现连续物理内存分配算法的关键数据结构可通过此数据结构来完成空闲块的链接和信息存储而基于这个数据结构的管理物理页数组起始地址就是全局变量pages具体初始化此数组的函数位于page\_init函数中
* 用于实现连续物理内存分配算法的物理内存页管理器框架pmm\_manager这个数据结构定义了实现内存分配算法的关键函数指针而同学需要完成这些函数的具体实现
* 设定二级页表和建立页表项以完成虚实地址映射关系这与硬件相关且用到不少内联函数源代码相对难懂一些。具体完成页表和页表项建立的重要函数是boot\_map\_segment函数而get\_pte函数是完成虚实映射关键的关键。
### 3.2 探测系统物理内存布局
当 ucore
被启动之后,最重要的事情就是知道还有多少内存可用,一般来说,获取内存大小的方法由
BIOS 中断调用和直接探测两种。但BIOS
中断调用方法是一般只能在实模式下完成,而直接探测方法必须在保护模式下完成。通过
BIOS 中断获取内存布局有三种方式都是基于INT 15h中断分别为88h e801h
e820h。但是 并非在所有情况下这三种方式都能工作。在 Linux kernel
里,采用的方法是依次尝试这三
种方法。而在本实验中我们通过e820h中断获取内存信息。因为e820h中断必须在实模式下使用所以我们在
bootloader 进入保护模式之前调用这个 BIOS 中断,并且把 e820 映
射结构保存在物理地址0x8000处。具体实现详见boot/bootasm.S。有关探测系统物理内存方法和具体实现的
信息参见[附录A和附录B。](http://wiki.osdev.org/How_Do_I_Determine_The_Amount_Of_RAM)
### 3.3 以页为单位管理物理内存
在获得可用物理内存范围后系统需要建立相应的数据结构来管理以物理页按4KB对齐且大小为4KB的物理内存单元为最小单位的整个物理内存以配合后续涉及的分页管理机制。每个物理页可以用一个
Page数据结构来表示。由于一个物理页需要占用一个Page结构的空间Page结构在设计时须尽可能小以减少对内存的占用。Page的定义在kern/mm/memlayout.h中。以页为单位的物理内存分配管理的实现在kern/default\_pmm.[ch]。
为了与以后的分页机制配合我们首先需要建立对整个计算机的每一个物理页的属性用结构Page来表示它包含了映射此物理页的虚拟页个数描述物理页属性的flags和双向链接各个Page结构的page\_link双向链表。
```
struct Page {
int ref; // page frame's reference counter
uint32_t flags; // array of flags that describe the status of the page frame
unsigned int property;// the num of free block, used in first fit pm manager
list_entry_t page_link;// free list link
};
```
这里看看Page数据结构的各个成员变量有何具体含义。ref表示这样页被页表的引用记数在“实现分页机制”一节会讲到。如果这个页被页表引用了即在某页表中有一个页表项设置了一个虚拟页到这个Page管理的物理页的映射关系就会把Page的ref加一反之若页表项取消即映射关系解除就会把Page的ref减一。flags表示此物理页的状态标记进一步查看kern/mm/memlayout.h中的定义可以看到
```
/* Flags describing the status of a page frame */
#define PG_reserved 0 // the page descriptor is reserved for kernel or unusable
#define PG_property 1 // the member 'property' is valid
```
这表示flags目前用到了两个bit表示页目前具有的两种属性bit
0表示此页是否被保留reserved如果是被保留的页则bit
0会设置为1且不能放到空闲页链表中即这样的页不是空闲页不能动态分配与释放。比如目前内核代码占用的空间就属于这样“被保留”的页。在本实验中bit
1表示此页是否是free的如果设置为1表示这页是free的可以被分配如果设置为0表示这页已经被分配出去了不能被再二次分配。另外本实验这里取的名字PG\_property比较不直观
主要是我们可以设计不同的页分配算法best fit, buddy
system等那么这个PG\_property就有不同的含义了。
在本实验中Page数据结构的成员变量property用来记录某连续内存空闲块的大小即地址连续的空闲页的个数。这里需要注意的是用到此成员变量的这个Page比较特殊是这个连续内存空闲块地址最小的一页即头一页
Head
Page。连续内存空闲块利用这个页的成员变量property来记录在此块内的空闲页的个数。这里去的名字property也不是很直观原因与上面类似在不同的页分配算法中property有不同的含义。
Page数据结构的成员变量page\_link是便于把多个连续内存空闲块链接在一起的双向链表指针可回顾在lab0实验指导书中有关双向链表数据结构的介绍。这里需要注意的是用到此成员变量的这个Page比较特殊是这个连续内存空闲块地址最小的一页即头一页
Head
Page。连续内存空闲块利用这个页的成员变量page\_link来链接比它地址小和大的其他连续内存空闲块。
在初始情况下也许这个物理内存的空闲物理页都是连续的这样就形成了一个大的连续内存空闲块。但随着物理页的分配与释放这个大的连续内存空闲块会分裂为一系列地址不连续的多个小连续内存空闲块且每个连续内存空闲块内部的物理页是连续的。那么为了有效地管理这些小连续内存空闲块。所有的连续内存空闲块可用一个双向链表管理起来便于分配和释放为此定义了一个free\_area\_t数据结构包含了一个list\_entry结构的双向链表指针和记录当前空闲页的个数的无符号整型变量nr\_free。其中的链表指针指向了空闲的物理页。
```
/* free_area_t - maintains a doubly linked list to record free (unused) pages */
typedef struct {
list_entry_t free_list; // the list header
unsigned int nr_free; // # of free pages in this free list
} free_area_t;
```
有了这两个数据结构ucore就可以管理起来整个以页为单位的物理内存空间。接下来需要解决两个问题
• 管理页级物理内存空间所需的Page结构的内存空间从哪里开始占多大空间
• 空闲内存空间的起始地址在哪里?
对于这两个问题我们首先根据bootloader给出的内存布局信息找出最大的物理内存地址maxpa定义在page\_init函数中的局部变量由于x86的起始物理内存地址为0所以可以得知需要管理的物理页个数为
```
npage = maxpa / PGSIZE
```
这样我们就可以预估出管理页级物理内存空间所需的Page结构的内存空间所需的内存大小为
```
sizeof(struct Page) * npage)
```
由于bootloader加载ucore的结束地址用全局指针变量end记录以上的空间没有被使用所以我们可以把end按页大小为边界去整后作为管理页级物理内存空间所需的Page结构的内存空间记为
```
pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);
```
为了简化起见从地址0到地址pages+ sizeof(struct Page) \*
npage)结束的物理内存空间设定为已占用物理内存空间起始0\~640KB的空间是空闲的地址pages+
sizeof(struct Page) \*
npage)以上的空间为空闲物理内存空间,这时的空闲空间起始地址为
```
uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);
```
为此我们需要把这两部分空间给标识出来。首先,对于所有物理空间,通过如下语句即可实现占用标记:
```
for (i = 0; i < npage; i ++) {
SetPageReserved(pages + i);
}
````
然后,根据探测到的空闲物理空间,通过如下语句即可实现空闲标记:
```
//获得空闲空间的起始地址begin和结束地址end
……
init_memmap(pa2page(begin), (end - begin) / PGSIZE);
```
其实SetPageReserved只需把物理地址对应的Page结构中的flags标志设置为PG\_reserved
表示这些页已经被使用了将来不能被用于分配。而init\_memmap函数则是把空闲物理页对应的Page结构中的flags和引用计数ref清零并加到free\_area.free\_list指向的双向列表中为将来的空闲页管理做好初始化准备工作。
关于内存分配的操作系统原理方面的知识有很多但在本实验中只实现了最简单的内存页分配算法。相应的实现在default\_pmm.c中的default\_alloc\_pages函数和default\_free\_pages函数相关实现很简单这里就不具体分析了直接看源码应该很好理解。
其实实验二在内存分配和释放方面最主要的作用是建立了一个物理内存页管理器框架,这实际上是一个函数指针列表,定义如下:
```
struct pmm_manager {
const char *name; //物理内存页管理器的名字
void (*init)(void); //初始化内存管理器
void (*init_memmap)(struct Page *base, size_t n); //初始化管理空闲内存页的数据结构
struct Page *(*alloc_pages)(size_t n); //分配n个物理内存页
void (*free_pages)(struct Page *base, size_t n); //释放n个物理内存页
size_t (*nr_free_pages)(void); //返回当前剩余的空闲页数
void (*check)(void); //用于检测分配/释放实现是否正确的辅助函数
};
```
重点是实现init\_memmap/ alloc\_pages/
free\_pages这三个函数。当完成物理内存页管理初始化工作后计算机系统的内存布局如下图所示
![image](lab2/image003.png)
图3 计算机系统的内存布局
### 3.4 物理内存页分配算法实现
如果要在ucore中实现连续物理内存分配算法则需要考虑的事情比较多相对课本上的物理内存分配算法描述要复杂不少。下面介绍一下如果要实现一个FirstFit内存分配算法的大致流程。
lab2的第一部分是完成first\_fit的分配算法。原理FirstFit内存分配算法上很简单但要在ucore中实现需要充分了解和利用ucore已有的数据结构和相关操作、关键的一些全局变量等。
**关键数据结构和变量**
first\_fit分配算法需要维护一个查找有序地址按从小到大排列空闲块以页为最小单位的连续地址空间的数据结构而双向链表是一个很好的选择。
libs/list.h定义了可挂接任意元素的通用双向链表结构和对应的操作所以需要了解如何使用这个文件提供的各种函数从而可以完成对双向链表的初始化/插入/删除等。
kern/mm/memlayout.h中定义了一个 free\_area\_t 数据结构,包含成员结构
```
list_entry_t free_list; // the list header 空闲块双向链表的头
unsigned int nr_free; // # of free pages in this free list 空闲块的总数(以页为单位)
```
显然我们可以通过此数据结构来完成对空闲块的管理。而default\_pmm.c中定义的free\_area变量就是干这个事情的。
kern/mm/pmm.h中定义了一个通用的分配算法的函数列表用pmm\_manager
表示。其中init函数就是用来初始化free\_area变量的,
first\_fit分配算法可直接重用default\_init函数的实现。init\_memmap函数需要根据现有的内存情况构建空闲块列表的初始状态。何时应该执行这个函数呢
通过分析代码,可以知道:
```
kern_init --> pmm_init-->page_init-->init_memmap--> pmm_manager->init_memmap
```
所以default\_init\_memmap需要根据page\_init函数中传递过来的参数某个连续地址的空闲块的起始页页个数来建立一个连续内存空闲块的双向链表。这里有一个假定page\_init函数是按地址从小到大的顺序传来的连续内存空闲块的。链表头是free\_area.free\_list链表项是Page数据结构的base-\>page\_link。这样我们就依靠Page数据结构中的成员变量page\_link形成了连续内存空闲块列表。
**设计实现**
default\_init\_memmap函数讲根据每个物理页帧的情况来建立空闲页链表且空闲页块应该是根据地址高低形成一个有序链表。根据上述变量的定义default\_init\_memmap可大致实现如下
```
default_init_memmap(struct Page *base, size_t n) {
struct Page *p = base;
for (; p != base + n; p ++) {
p->flags = p->property = 0;
set_page_ref(p, 0);
}
base->property = n;
SetPageProperty(base);
nr_free += n;
list_add(&free_list, &(base->page_link));
}
```
如果要分配一个页那要考虑哪些呢这里就需要考虑实现default\_alloc\_pages函数注意参数n表示要分配n个页。另外需要注意实现时尽量多考虑一些边界情况这样确保软件的鲁棒性。比如
```
if (n > nr_free) {
return NULL;
}
```
这样可以确保分配不会超出范围。也可加一些
assert函数在有错误出现时能够迅速发现。比如 n应该大于0我们就可以加上
```
assert(n \> 0);
```
这样在n<=0的情况下ucore会迅速报错。firstfit需要从空闲链表头开始查找最小的地址通过list\_next找到下一个空闲块元素通过le2page宏可以更加链表元素获得对应的Page指针p。通过p-\>property可以了解此空闲块的大小。如果\>=n这就找到了如果<n则list\_next继续查找直到list\_next==
&free\_list这表示找完了一遍了。找到后就要从新组织空闲块然后把找到的page返回。所以default\_alloc\_pages可大致实现如下
```
static struct Page *
default_alloc_pages(size_t n) {
if (n > nr_free) {
return NULL;
}
struct Page *page = NULL;
list_entry_t *le = &free_list;
while ((le = list_next(le)) != &free_list) {
struct Page *p = le2page(le, page_link);
if (p->property >= n) {
page = p;
break;
}
}
if (page != NULL) {
list_del(&(page->page_link));
if (page->property > n) {
struct Page *p = page + n;
p->property = page->property - n;
list_add(&free_list, &(p->page_link));
}
nr_free -= n;
ClearPageProperty(page);
}
return page;
}
```
default\_free\_pages函数的实现其实是default\_alloc\_pages的逆过程不过需要考虑空闲块的合并问题。这里就不再细讲了。注意上诉代码只是参考设计不是完整的正确设计。更详细的说明位于lab2/kernel/mm/default\_pmm.c的注释中。希望同学能够顺利完成本实验的第一部分。
### 3.5 实现分页机制
#### 3.5.1 段页式管理基本概念
如图4在保护模式中x86
体系结构将内存地址分成三种:逻辑地址(也称虚地址)、线性地址和物理地址。逻辑地址即是程序指令中使用的地址,物理地址是实际访问内存的地址。逻
辑地址通过段式管理的地址映射可以得到线性地址,线性地址通过页式管理的地址映射得到物理地址。
![image](lab2/image004.png)
图 4 段页式管理总体框架图
段式管理前一个实验已经讨论过。在 ucore
中段式管理只起到了一个过渡作用,它将逻辑地址不加转换直接映射成线性地址,所以我们在下面的讨论中可以对这两个地址不加区分(目前的
OS 实现也是不加区分的。对段式管理有兴趣的同学可以参照《Intel® 64 and
IA-32Architectures Software Developer s Manual Volume 3A》3.2 节。
![image](lab2/image005.png)如图5所示页式管理将线性地址分成三部分图中的
Linear Address 的 Directory 部分、 Table 部分和 Offset 部分。ucore
的页式管理通过一个二级的页表实现。一级页表的起始物理地址存放在 cr3
寄存器中,这个地址必须是一个页对齐的地址,也就是低 12 位必须为
0。目前ucore 用boot\_cr3mm/pmm.c记录这个值。
![image](lab2/image006.png)
图 5 分页机制管理
### 3.5.2建立段页式管理中需要考虑的关键问题
为了实现分页机制,需要建立好虚拟内存和物理内存的页映射关系,即正确建立二级页表。此过程涉及硬件细节,不同的地址映射关系组合,相对比较复杂。总体而言,我们需要思考如下问题:
* 如何在建立页表的过程中维护全局段描述符表GDT和页表的关系确保ucore能够在各个时间段上都能正常寻址
* 对于哪些物理内存空间需要建立页映射关系?
* 具体的页映射关系是什么?
* 页目录表的起始地址设置在哪里?
* 页表的起始地址设置在哪里,需要多大空间?
* 如何设置页目录表项的内容?
* 如何设置页表项的内容?
### 3.5.3建立虚拟页和物理页帧的地址映射关系
**从链接脚本分析 ucore 执行时的地址**
首先观察一下tools/kernel.ld文件在lab1和lab2中的区别。在lab1中
```
ENTRY(kern_init)
SECTIONS {
/* Load the kernel at this address: "." means the current address */
. = 0x100000;
.text : {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
```
这意味着在lab1中通过ld工具形成的ucore的起始虚拟地址从0x100000开始注意这个地址是虚拟地址。但由于lab1中建立的段地址映射关系为对等关系所以ucore的物理地址也是0x100000。而入口函数为kern\_init函数。在lab2中
```
ENTRY(kern_entry)
SECTIONS {
/* Load the kernel at this address: "." means the current address */
. = 0xC0100000;
.text : {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
```
这意味着lab2中通过ld工具形成的ucore的起始虚拟地址从0xC0100000开始注意这个地址也是虚拟地址。入口函数为kern\_entry函数。这与lab1有很大差别。但其实在lab1和lab2中bootloader把ucore都放在了起始物理地址为0x100000的物理内存空间。这实际上说明了ucore在lab1和lab2中采用的地址映射不同
lab1 virtual addr = linear addr = phy addr
lab2 virtual addr = linear addr = phy addr + 0xC0000000
lab1只采用了段映射机制但在lab2中启动好分页管理机制后形成的是段页式映射机制从而使得虚拟地址空间和物理地址空间之间存在如下的映射关系
Virtual Address=LinearAddress=0xC0000000+Physical Address
另外ucore的入口地址也改为了kern\_entry函数这个函数位于init/entry.S中分析代码可以看出entry.S重新建立了段映射关系从以前的
Virtual Address= Linear Address
改为
Virtual Address=Linear Address-0xC0000000
由于gcc编译出的虚拟起始地址从0xC0100000开始ucore被bootloader放置在从物理地址0x100000处开始的物理内存中。所以当kern\_entry函数完成新的段映射关系后且ucore在没有建立好页映射机制前CPU按照ucore中的虚拟地址执行能够被分段机制映射到正确的物理地址上确保ucore运行正确。
由于物理内存页管理器管理了从0到实际可用物理内存大小的物理内存空间所以对于这些物理内存空间都需要建立好页映射关系。由于目前ucore只运行在内核空间所以可以建立一个一一映射关系。假定内核虚拟地址空间的起始地址为0xC0000000则虚拟内存和物理内存的具体页映射关系为
Virtual Address=Physical Address+0xC0000000
**建立二级页表**
由于我们已经具有了一个物理内存页管理器default\_pmm\_manager我们就可以用它来获得所需的空闲物理页。在二级页表结构中页目录表占4KB空间ucore就可通过default\_pmm\_manager的default\_alloc\_pages函数获得一个空闲物理页这个页的起始物理地址就是页目录表的起始地址。同理ucore也通过这种方式获得各个页表所需的空间。页表的空间大小取决与页表要管理的物理页数n一个页表项32位即4字节可管理一个物理页页表需要占n/256个物理页空间。这样页目录表和页表所占的总大小为4096+1024\*n字节。
为把0\~KERNSIZE明确ucore设定实际物理内存不能超过KERNSIZE值即0x38000000字节896MB3670016个物理页的物理地址一一映射到页目录表项和页表项的内容其大致流程如下
1. 先通过default\_pmm\_manager获得一个空闲物理页用于页目录表
2. 调用boot\_map\_segment函数建立一一映射关系具体处理过程以页为单位进行设置
Virtual Address=Physical Address+0xC0000000
Ø 设一个逻辑地址la按页对齐故低12位为零对应的物理地址pa按页对齐故低12位为零如果在页目录表项la的高10位为索引值中的存在位PTE\_P为0表示缺少对应的页表空间则可通过default\_pmm\_manager获得一个空闲物理页给页表页表起始物理地址是按4096字节对齐的这样填写页目录表项的内容为
***页目录表项内容 = 页表起始物理地址 | PTE_U | PTE_W | PTE\_P***
进一步对于页表中对应页表项la的中10位为索引值的内容为
***页表项内容 = pa | PTE\_P | PTE\_W***
其中:
* PTE\_U位3表示用户态的软件可以读取对应地址的物理内存页内容
* PTE\_W位2表示物理内存页内容可写
* PTE\_P位1表示物理内存页存在
ucore
的内存管理经常需要查找页表给定一个虚拟地址找出这个虚拟地址在二级页表中对应的项。通过更改此项的值可以方便地将虚拟地址映射到另外的页上。可完成此功能的这个函数是get\_pte函数。它的原型为
```
pte_t *get_pte (pde_t *pgdir, uintptr_t la, bool create)
```
下面的调用关系图可以比较好地看出get\_pte在实现上诉流程中的位置
![image](lab2/image007.png)
图6 get\_pte调用关系图
这里涉及到三个类型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表示为线性地址由于段式管理只做直接映射所以它也是逻辑地址。
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。如果原来就有二级页表或者新建立了页表则只需返回对应项的地址即可。
虚拟地址只有映射上了物理页才可以正常的读写。在完成映射物理页的过程中,除了要象上面那样在页表的对应表项上填上相应的物理地址外,还要设置正确的控制位。有关
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的逆操作。
建立好一一映射的二级页表结构后接下来就要使能分页机制了这主要是通过enable\_paging函数实现的这个函数主要做了两件事
1. 通过lcr3指令把页目录表的起始地址存入CR3寄存器中
2. 通过lcr0指令把cr0中的CR0\_PG标志位设置上。
执行完enable\_paging函数后计算机系统进入了分页模式但到这一步还不够还记得ucore在最开始通过kern\_entry函数设置了临时的新段映射机制吗这个临时的新段映射机制不是最简单的对等映射导致虚拟地址和线性地址不相等。而刚才建立的页映射关系是建立在简单的段对等映射即虚拟地址=线性地址的假设基础之上的。所以我们需要进一步调整段映射关系即重新设置新的GDT建立对等段映射。
这里需要注意在进入分页模式到重新设置新GDT的过程是一个过渡过程。在这个过渡过程中已经建立了页表机制所以通过现在的段机制和页机制实现的地址映射关系为
```
Virtual Address=Linear Address + 0xC0000000 = Physical Address
+0xC0000000+0xC0000000
```
在这个特殊的阶段如果不把段映射关系改为Virtual Address = Linear
Address则通过段页式两次地址转换后无法得到正确的物理地址。为此我们需要进一步调用gdt\_init函数根据新的gdt全局段描述符表内容gdt定义位于pmm.c中恢复以前的段映射关系即使得Virtual
Address = Linear
Address。这样在执行完gdt\_init后通过的段机制和页机制实现的地址映射关系为
```
Virtual Address=Linear Address = Physical Address +0xC0000000
```
这里存在的一个问题是在调用enable\_page函数使能分页机制后到执行完毕gdt\_init函数重新建立好段页式映射机制的过程中内核使用的还是旧的段表映射也就是说enable
paging 之后,内核使用的是页表的低地址 entry。
如何保证此时内核依然能够正常工作呢其实只需让低地址目录表项的内容等于以KERNBASE开始的高地址目录表项的内容即可。目前内核大小不超过
4M 实际上是3M因为内核从 0x100000
开始编址这样就只需要让页表在0\~4MB的线性地址与KERNBASE \~
KERNBASE+4MB的线性地址获得相同的映射即可都映射到 0\~4MB
的物理地址空间具体实现在pmm.c中pmm\_init函数的语句
```
boot_pgdir[0] = boot_pgdir[PDX(KERNBASE)];
```
实际上这种映射也限制了内核的大小。当内核大小超过预期的3MB
就可能导致打开分页之后内核
crash在后面的试验中也的确出现了这种情况。解决方法同样简单就是拷贝更多的高地址项到低地址。
当执行完毕gdt\_init函数后新的段页式映射已经建立好了上面的0\~4MB的线性地址与0\~4MB的物理地址一一映射关系已经没有用了。所以可以通过如下语句解除这个老的映射关系。
```
boot_pgdir[0] = 0;
```
在page\_init函数建立完实现物理内存一一映射和页目录表自映射的页目录表和页表后一旦使能分页机制则ucore看到的内核虚拟地址空间如下图所示
![说明: proj5-vm-map](lab2/image008.png)
图7 使能分页机制后的虚拟地址空间图
### 3.5.4不同运行阶段的地址映射关系
在大多数课本中描述了基于段的映射关系基于页的映射关系以及基于段页式的映射关系和CPU访存时对应的地址转换过程。但很少涉及操作系统如何一步一步建立这个映射关系的。其实在lab1和lab2中都会涉及如何建立映射关系的操作。在lab1中我们已经碰到到了简单的段映射即对等映射关系保证了物理地址和虚拟地址相等也就是通过建立全局段描述符表让每个段的基址为0从而确定了对等映射关系。
在lab2中由于在段地址映射的基础上进一步引入了页地址映射形成了组合式的段页式地址映射。这种方式虽然更加灵活了但实现的复杂性也增加了。在lab2中ucore从计算机加电启动段式管理机制启动段页式管理机制在段页式管理机制下运行这整个过程中虚地址到物理地址的映射产生了多次变化接下来我们会逐一进行说明。
1.
首先是bootloader地址映射阶段bootloader完成了与lab1一样的工作即建立了基于段的对等映射请查看lab2/boot/bootasm.S中的finish\_probe地址处
2.
接着进入了ucore启动页机制前的地址映射阶段ucore建立了一个一一段映射关系其中虚拟地址
= 物理地址 +
0xC0000000请查看lab2/kern/init/entry.S中的kern\_entry函数
3.
再接下来是建立并使能页表的临时段页式地址映射阶段,页表要表示的是线性地址与物理地址的对应关系为:线性地址
= 物理地址 +
0xC0000000然后这里有一个小技巧让在0\~4MB的线性地址区域空间的 线性地址
0\~4MB对应的物理地址 = 线性地址 0xC0000000\~0xC0000000 +
4MB对应的物理地址这是通过lab2/kern/mm/pmm.c中第321行的代码实现的
boot\_pgdir**[**0**]** **=** boot\_pgdir**[**PDX**(**KERNBASE**)];**
注意此时CPU在寻址时还是只采用了分段机制。最后后并使能分页映射机制请查看lab2/kern/mm/pmm.c中的enable\_paging函数一旦执行完enable\_paging函数中的加载cr0指令即让CPU使能分页机制则接下来的访问是基于段页式的映射关系了。对于0xC0000000\~0xC0000000
+ 4MB这块虚拟地址空间最终会映射到哪些物理地址空间中呢
由于段映射关系没有改变使得经过段映射机制虚拟地址范围0xC0000000\~0xC0000000
+ 4MB对应的线性地址 =
0\~4MB。而根据页表建立过程的描述我们可知道线性地址空间0\~4MB与线性地址空间
0xC0000000\~0xC0000000 +
4MB对应同样的物理地址而线性地址空间0xC0000000\~0xC0000000 +
4MB对应的物理地址空间为04MB。这样对于0xC0000000\~0xC0000000 +
4MB这块虚拟地址空间段页式的地址映射关系为 虚拟地址 = 线性地址 +
0xC0000000 =物理地址 + 0xC0000000。
注意这只是针对0xC0000000\~0xC0000000 +
4MB这块虚拟地址空间。如果是0xD0000000\~0xD0000000 +
4MB这块虚拟地址空间则段页式的地址映射关系为虚拟地址 = 线性地址 +
0xC0000000 =物理地址 + 0xC0000000 +
0xC0000000。这不是我们需要的映射关系所以0xC0000000 +
4MB以上的虚拟地址访问会出页错误异常。
4.
最后一步完成收尾工作的正常段页式地址映射阶段即首先调整段映射关系这是通过加载新的全局段描述符表pmm\_init函数调用gdt\_init函数来完成实现这时的段映射关系为虚拟地址
= 线性地址。然后通过执行语句“boot\_pgdir**[**0**]** **=**
0**;**”把boot\_pgdir[0]的第一个页目录表项0\~4MB清零来取消临时的页映射关系。至此新的段页式的地址映射关系为虚拟地址
= 线性地址 =物理地址 +
0xC0000000。这也形成了ucore操作系统的内核虚拟地址空间的段页式映射关系即虚拟地址空间KERNBASE,
KERNBASE + KMEMSIZE= 线性地址空间KERNBASE, KERNBASE + KMEMSIZE =
物理地址空间0KMEMSIZE
### 3.6 自映射机制
这是扩展知识。
上一小节讲述了通过boot\_map\_segment函数建立了基于一一映射关系的页目录表项和页表项这里的映射关系为
virtual addr (KERNBASE\~KERNBASE+KMEMSIZE) = physical\_addr
(0\~KMEMSIZE)
这样只要给出一个虚地址和一个物理地址就可以设置相应PDE和PTE就可完成正确的映射关系。
如果我们这时需要按虚拟地址的地址顺序显示整个页目录表和页表的内容,则要查找页目录表的页目录表项内容,根据页目录表项内容找到页表的物理地址,再转换成对应的虚地址,然后访问页表的虚地址,搜索整个页表的每个页目录项。这样过程比较繁琐。
我们需要有一个简洁的方法来实现这个查找。ucore做了一个很巧妙的地址自映射设计把页目录表和页表放在一个连续的4MB虚拟地址空间中并设置页目录表自身的虚地址<--\>物理地址映射关系这样在已知页目录表起始虚地址的情况下通过连续扫描这特定的4MB虚拟地址空间就很容易访问每个页目录表项和页表项内容
具体而言ucore是这样设计的首先设置了一个常量memlayout.h
VPT=0xFAC00000 这个地址的二进制表示为:
1111 1010 1100 0000 0000 0000 0000 0000
高10位为1111 1010
11即10进制的1003中间10位为0低12位也为0。在pmm.c中有两个全局初始化变量
pte\_t \* const vpt = (pte\_t \*)VPT;
pde\_t \* const vpd = (pde\_t \*)PGADDR(PDX(VPT), PDX(VPT), 0);
并在pmm\_init函数执行了如下语句
boot\_pgdir[PDX(VPT)] = PADDR(boot\_pgdir) | PTE\_P | PTE\_W;
这些变量和语句有何特殊含义呢其实vpd变量的值就是页目录表的起始虚地址0xFAFEB000且它的高10位和中10位是相等的都是10进制的1003。当执行了上述语句就确保了vpd变量的值就是页目录表的起始虚地址且vpt是页目录表中第一个目录表项指向的页表的起始虚地址。此时描述内核虚拟空间的页目录表的虚地址为0xFAFEB000大小为4KB。页表的理论连续虚拟地址空间0xFAC00000\~0xFB000000大小为4MB。因为这个连续地址空间的大小为4MB可有1M个PTE即可映射4GB的地址空间。
但ucore实际上不会用完这么多项在memlayout.h中定义了常量
\#define KMEMSIZE 0x38000000
表示ucore只支持896MB的物理内存空间这个896MB只是一个设定可以根据情况改变。则最大的内核虚地址为常量
\#define KERNTOP (KERNBASE + KMEMSIZE)=0xF8000000
所以最大内核虚地址KERNTOP的页目录项虚地址为
vpd+0xF8000000/0x400000=0xFAFEB000+0x3E0=0xFAFEB3E0
最大内核虚地址KERNTOP的页表项虚地址为
vpt+0xF8000000/0x1000=0xFAC00000+0xF8000=0xFACF8000
在pmm.c中的函数print\_pgdir就是基于ucore的页表自映射方式完成了对整个页目录表和页表的内容扫描和打印。注意这里不会出现某个页表的虚地址与页目录表虚地址相同的情况。
![image](lab2/image009.png)![image](lab2/image010.png)![image](lab2/image011.png)print
pgdir函数使得 ucore 具备和 qemu 的info pg相同的功能即print pgdir能
够从内存中将当前页表内有效数据PTE\_P印出来。拷贝出的格式如下所示:
```
PDE(0e0) c0000000-f8000000 38000000 urw
|-- PTE(38000) c0000000-f8000000 38000000 -rw
PDE(001) fac00000-fb000000 00400000 -rw
|-- PTE(000e0) faf00000-fafe0000 000e0000 urw
|-- PTE(00001) fafeb000-fafec000 00001000 -rw
```
上面中的数字包括括号里的,都是十六进制。
主要的功能是从页表中将具备相同权限的 PDE 和 PTE
项目组织起来。比如上表中:
```
PDE(0e0) c0000000-f8000000 38000000 urw
```
• PDE(0e0)0e0表示 PDE 表中相邻的 224 项具有相同的权限;
• c0000000-f8000000表示 PDE 表中,这相邻的两项所映射的线性地址的范围;
• 38000000同样表示范围即f8000000减去c0000000的结果
• urwPDE 表中所给出的权限位u表示用户可读即PTE\_Ur表示PTE\_Pw表示用
户可写即PTE\_W。
```
PDE(001) fac00000-fb000000 00400000 -rw
```
表示仅 1 条连续的 PDE 表项具备相同的属性。相应的,在这条表项中遍历找到 2
组 PTE 表项,输出如下:
```
|-- PTE(000e0) faf00000-fafe0000 000e0000 urw
|-- PTE(00001) fafeb000-fafec000 00001000 -rw
```
注意:
1. PTE 中输出的权限是 PTE 表中的数据给出的,并没有和 PDE
表中权限做与运算。
2.
![image](lab2/image012.png)整个print\_pgdir函数强调两点第一是相同权限第二是连续。
3.
![image](lab2/image013.png)print\_pgdir中用到了vpt和vpd两个变量。可以参
考VPT和PGADDR两个宏。
自映射机制还可方便用户态程序访问页表。因为页表是内核维护的用户程序很难知道自己页表的映射结构。VPT
实际上在内核地址空间的,我们可以用同样的方式实现一个用户地址空间的映射(比如
pgdir[UVPT] = PADDR(pgdir) | PTE\_P | PTE\_U注意这里不能给写权限并且
pgdir 是每个进程的 page table不是
boot\_pgdir这样用户程序就可以用和内核一样的 print\_pgdir
函数遍历自己的页表结构了。
\
\
## 4 实验报告要求
从网站上下载lab2.zip后解压得到本文档和代码目录
lab2完成实验中的各个练习。完成代码编写并检查无误后在对应目录下执行
make handin 任务,即会自动生成
lab2-handin.tar.gz。最后请一定提前或按时提交到网络学堂上。
注意有***“LAB2”***的注释代码中所有需要完成的地方challenge除外都有***“LAB2”***和***“YOUR CODE”***的注释,请在提交时特别注意保持注释,并将***“YOUR CODE”***替换为自己的学号,并且将所有标有对应注释的部分填上正确的代码。
\
## 附录
**A. 探测物理内存分布和大小的方法**
操作系统需要知道了解整个计算机系统中的物理内存如何分布的哪些被可用哪些不可用。其基本方法是通过BIOS中断调用来帮助完成的。其中BIOS中断调用必须在实模式下进行所以在bootloader进入保护模式前完成这部分工作相对比较合适。这些部分由boot/bootasm.S中从probe\_memory处到finish\_probe处的代码部分完成完成。通过BIOS中断获取内存可调用参数为e820h的INT
15h BIOS中断。BIOS通过系统内存映射地址描述符Address Range
Descriptor格式来表示系统物理内存布局其具体表示如下
```
Offset Size Description
00h 8字节 base address #系统内存块基地址
08h 8字节 length in bytes #系统内存大小
10h 4字节 type of address range #内存类型
```
看下面的(Values for System Memory Map address type)
```
Values for System Memory Map address type:
01h memory, available to OS
02h reserved, not available (e.g. system ROM, memory-mapped device)
03h ACPI Reclaim Memory (usable by OS after reading ACPI tables)
04h ACPI NVS Memory (OS is required to save this memory between NVS sessions)
other not defined yet -- treat as Reserved
```
INT15h BIOS中断的详细调用参数:
```
eaxe820hINT 15的中断调用参数
edx534D4150h (即4个ASCII字符“SMAP”) ,这只是一个签名而已;
ebx如果是第一次调用或内存区域扫描完毕则为0。 如果不是,则存放上次调用之后的计数值;
ecx保存地址范围描述符的内存大小,应该大于等于20字节
es:di指向保存地址范围描述符结构的缓冲区BIOS把信息写入这个结构的起始地址。
```
此中断的返回值为:
```
cflags的CF位若INT 15中断执行成功则不置位否则置位
eax534D4150h ('SMAP')
es:di指向保存地址范围描述符的缓冲区,此时缓冲区内的数据已由BIOS填写完毕
ebx下一个地址范围描述符的计数地址
ecx 返回BIOS往ES:DI处写的地址范围描述符的字节大小
ah失败时保存出错代码
```
这样我们通过调用INT 15h
BIOS中断递增di的值20的倍数让BIOS帮我们查找出一个一个的内存布局entry并放入到一个保存地址范围描述符结构的缓冲区中供后续的ucore进一步进行物理内存管理。这个缓冲区结构定义在memlayout.h中
```
struct e820map {
int nr_map;
struct {
long long addr;
long long size;
long type;
} map[E820MAX];
};
```
****
**B. 实现物理内存探测**
物理内存探测是在bootasm.S中实现的相关代码很短如下所示
```
probe_memory:
//对0x8000处的32位单元清零,即给位于0x8000处的
//struct e820map的成员变量nr_map清零
movl $0, 0x8000
xorl %ebx, %ebx
//表示设置调用INT 15h BIOS中断后BIOS返回的映射地址描述符的起始地址
movw $0x8004, %di
start_probe:
movl $0xE820, %eax // INT 15的中断调用参数
//设置地址范围描述符的大小为20字节其大小等于struct e820map的成员变量map的大小
movl $20, %ecx
//设置edx为534D4150h (即4个ASCII字符“SMAP”),这是一个约定
movl $SMAP, %edx
//调用int 0x15中断要求BIOS返回一个用地址范围描述符表示的内存段信息
int $0x15
//如果eflags的CF位为0则表示还有内存段需要探测
jnc cont
//探测有问题,结束探测
movw $12345, 0x8000
jmp finish_probe
cont:
//设置下一个BIOS返回的映射地址描述符的起始地址
addw $20, %di
//递增struct e820map的成员变量nr_map
incl 0x8000
//如果INT0x15返回的ebx为零表示探测结束否则继续探测
cmpl $0, %ebx
jnz start_probe
finish_probe:
```
上述代码正常执行完毕后在0x8000地址处保存了从BIOS中获得的内存分布信息此信息按照struct
e820map的设置来进行填充。这部分信息将在bootloader启动ucore后由ucore的page\_init函数来根据struct
e820map的memmap定义了起始地址为0x8000来完成对整个机器中的物理内存的总体管理。
**C.链接地址/虚地址/物理地址/加载地址以及edata/end/text的含义**
**链接脚本简介**
ucore
kernel各个部分由组成kernel的各个.o或.a文件构成且各个部分在内存中地址位置由ld工具根据kernel.ld链接脚本linker
script来设定。ld工具使用命令-T指定链接脚本。链接脚本主要用于规定如何把输入文件各个.o或.a文件内的section放入输出文件lab2/bin/kernel即ELF格式的ucore内核
并控制输出文件内各部分在程序地址空间内的布局。下面简单分析一下/lab2/tools/kernel.ld来了解一下ucore内核的地址布局情况。kernel.ld的内容如下所示
```
/* Simple linker script for the ucore kernel.
See the GNU ld 'info' manual ("info ld") to learn the syntax. */
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(kern_entry)
SECTIONS {
/* Load the kernel at this address: "." means the current address */
. = 0xC0100000;
.text : {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
PROVIDE(etext = .); /* Define the 'etext' symbol to this value */
.rodata : {
*(.rodata .rodata.* .gnu.linkonce.r.*)
}
/* Include debugging information in kernel memory */
.stab : {
PROVIDE(__STAB_BEGIN__ = .);
*(.stab);
PROVIDE(__STAB_END__ = .);
BYTE(0) /* Force the linker to allocate space
for this section */
}
.stabstr : {
PROVIDE(__STABSTR_BEGIN__ = .);
*(.stabstr);
PROVIDE(__STABSTR_END__ = .);
BYTE(0) /* Force the linker to allocate space
for this section */
}
/* Adjust the address for the data segment to the next page */
. = ALIGN(0x1000);
/* The data segment */
.data : {
*(.data)
}
PROVIDE(edata = .);
.bss : {
*(.bss)
}
PROVIDE(end = .);
/DISCARD/ : {
*(.eh_frame .note.GNU-stack)
}
}
```
其实从链接脚本的内容,可以大致猜出它指定告诉链接器的各种信息:
* 内核加载地址0xC0100000
* 入口(起始代码)地址: ENTRY(kern\_entry)
* cpu机器类型i386
其最主要的信息是告诉链接器各输入文件的各section应该怎么组合应该从哪个地址开始放各个section以什么顺序放分别怎么对齐等等最终组成输出文件的各section。除此之外linker
script还可以定义各种符号如.text、.data、.bss等形成最终生成的一堆符号的列表符号表每个符号包含了符号名字符号所引用的内存地址以及其他一些属性信息。符号实际上就是一个地址的符号表示其本身不占用的程序运行的内存空间。
**链接地址/加载地址/虚地址/物理地址**
ucore 设定了ucore运行中的虚地址空间具体设置可看
lab2/kern/mm/memlayout.h 中描述的"Virtual memory map
"图可以了解虚地址和物理地址的对应关系。lab2/tools/kernel.ld描述的是执行代码的链接地址link\_addr比如内核起始地址是0xC0100000这是一个虚地址。所以我们可以认为链接地址等于虚地址。在ucore建立内核页表时设定了物理地址和虚地址的虚实映射关系是
phy addr + 0xC0000000 = virtual addr
即虚地址和物理地址之间有一个偏移。但boot loader把ucore
kernel加载到内存时采用的是加载地址load
addr这是由于ucore还没有运行即还没有启动页表映射导致这时采用的寻址方式是段寻址方式用的是boot
loader在初始化阶段设置的段映射关系其映射关系可参看bootasm.S的末尾处有关段描述符表的内容
linear addr = phy addr = virtual addr
查看 bootloader的实现代码 bootmain::bootmain.c
readseg(ph-\>p\_va & 0xFFFFFF, ph-\>p\_memsz, ph-\>p\_offset);
这里的ph-\>p\_va=0xC0XXXXXX就是ld工具根据kernel.ld设置的链接地址且链接地址等于虚地址。考虑到ph-\>p\_va
& 0xFFFFFF == 0x0XXXXXX所以bootloader加载ucore
kernel的加载地址是0x0XXXXXX, 这实际上是ucore内核所在的物理地址。简言之
OS的链接地址link addr 在tools/kernel.ld中设置好了是一个虚地址virtual
addr而ucore kernel的加载地址load addr在boot
loader中的bootmain函数中指定是一个物理地址。
小结一下ucore内核的链接地址==ucore内核的虚拟地址boot
loader加载ucore内核用到的加载地址==ucore内核的物理地址。
**edata/end/text的含义**
在基于ELF执行文件格式的代码中存在一些对代码和数据的表述基本概念如下
* BSS段bss
segment指用来存放程序中未初始化的全局变量的内存区域。BSS是英文Block
Started by Symbol的简称。BSS段属于静态内存分配。
* 数据段data
segment指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
* 代码段code segment/text
segment指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定并且内存区域通常属于只读,
某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
在lab2/kern/init/init.c的kern\_init函数中声明了外部全局变量
```
extern char edata[], end[];
```
但搜寻所有源码文件\*.[ch]没有发现有这两个变量的定义。那这两个变量从哪里来的呢其实在lab2/tools/kernel.ld中可以看到如下内容
```
.text : {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
.data : {
*(.data)
}
PROVIDE(edata = .);
.bss : {
*(.bss)
}
PROVIDE(end = .);
```
这里的“.”表示当前地址,“.text”表示代码段起始地址“.data”也是一个地址可以看出它即代表了代码段的结束地址也是数据段的起始地址。类推下去“edata”表示数据段的结束地址“.bss”表示数据段的结束地址和BSS段的起始地址而“end”表示BSS段的结束地址。
这样回头看kerne\_init中的外部全局变量可知edata[]和
end[]这些变量是ld根据kernel.ld链接脚本生成的全局变量表示相应段的起始地址或结束地址等它们不在任何一个.S、.c或.h文件中定义。

View File

@ -0,0 +1,37 @@
**实现物理内存探测**
物理内存探测是在bootasm.S中实现的相关代码很短如下所示
```
probe_memory:
//对0x8000处的32位单元清零,即给位于0x8000处的
//struct e820map的成员变量nr_map清零
movl $0, 0x8000
xorl %ebx, %ebx
//表示设置调用INT 15h BIOS中断后BIOS返回的映射地址描述符的起始地址
movw $0x8004, %di
start_probe:
movl $0xE820, %eax // INT 15的中断调用参数
//设置地址范围描述符的大小为20字节其大小等于struct e820map的成员变量map的大小
movl $20, %ecx
//设置edx为534D4150h (即4个ASCII字符“SMAP”),这是一个约定
movl $SMAP, %edx
//调用int 0x15中断要求BIOS返回一个用地址范围描述符表示的内存段信息
int $0x15
//如果eflags的CF位为0则表示还有内存段需要探测
jnc cont
//探测有问题,结束探测
movw $12345, 0x8000
jmp finish_probe
cont:
//设置下一个BIOS返回的映射地址描述符的起始地址
addw $20, %di
//递增struct e820map的成员变量nr_map
incl 0x8000
//如果INT0x15返回的ebx为零表示探测结束否则继续探测
cmpl $0, %ebx
jnz start_probe
finish_probe:
```
上述代码正常执行完毕后在0x8000地址处保存了从BIOS中获得的内存分布信息此信息按照struct
e820map的设置来进行填充。这部分信息将在bootloader启动ucore后由ucore的page\_init函数来根据struct
e820map的memmap定义了起始地址为0x8000来完成对整个机器中的物理内存的总体管理。

View File

@ -0,0 +1,11 @@
### 建立段页式管理中需要考虑的关键问题
为了实现分页机制,需要建立好虚拟内存和物理内存的页映射关系,即正确建立二级页表。此过程涉及硬件细节,不同的地址映射关系组合,相对比较复杂。总体而言,我们需要思考如下问题:
* 如何在建立页表的过程中维护全局段描述符表GDT和页表的关系确保ucore能够在各个时间段上都能正常寻址
* 对于哪些物理内存空间需要建立页映射关系?
* 具体的页映射关系是什么?
* 页目录表的起始地址设置在哪里?
* 页表的起始地址设置在哪里,需要多大空间?
* 如何设置页目录表项的内容?
* 如何设置页表项的内容?

47
lab2/maping_relations.md Normal file
View File

@ -0,0 +1,47 @@
### 不同运行阶段的地址映射关系
在大多数课本中描述了基于段的映射关系基于页的映射关系以及基于段页式的映射关系和CPU访存时对应的地址转换过程。但很少涉及操作系统如何一步一步建立这个映射关系的。其实在lab1和lab2中都会涉及如何建立映射关系的操作。在lab1中我们已经碰到到了简单的段映射即对等映射关系保证了物理地址和虚拟地址相等也就是通过建立全局段描述符表让每个段的基址为0从而确定了对等映射关系。
在lab2中由于在段地址映射的基础上进一步引入了页地址映射形成了组合式的段页式地址映射。这种方式虽然更加灵活了但实现的复杂性也增加了。在lab2中ucore从计算机加电启动段式管理机制启动段页式管理机制在段页式管理机制下运行这整个过程中虚地址到物理地址的映射产生了多次变化接下来我们会逐一进行说明。
1.
首先是bootloader地址映射阶段bootloader完成了与lab1一样的工作即建立了基于段的对等映射请查看lab2/boot/bootasm.S中的finish\_probe地址处
2.
接着进入了ucore启动页机制前的地址映射阶段ucore建立了一个一一段映射关系其中虚拟地址
= 物理地址 +
0xC0000000请查看lab2/kern/init/entry.S中的kern\_entry函数
3.
再接下来是建立并使能页表的临时段页式地址映射阶段,页表要表示的是线性地址与物理地址的对应关系为:线性地址
= 物理地址 +
0xC0000000然后这里有一个小技巧让在0\~4MB的线性地址区域空间的 线性地址
0\~4MB对应的物理地址 = 线性地址 0xC0000000\~0xC0000000 +
4MB对应的物理地址这是通过lab2/kern/mm/pmm.c中第321行的代码实现的
boot\_pgdir**[**0**]** **=** boot\_pgdir**[**PDX**(**KERNBASE**)];**
注意此时CPU在寻址时还是只采用了分段机制。最后后并使能分页映射机制请查看lab2/kern/mm/pmm.c中的enable\_paging函数一旦执行完enable\_paging函数中的加载cr0指令即让CPU使能分页机制则接下来的访问是基于段页式的映射关系了。对于0xC0000000\~0xC0000000
+ 4MB这块虚拟地址空间最终会映射到哪些物理地址空间中呢
由于段映射关系没有改变使得经过段映射机制虚拟地址范围0xC0000000\~0xC0000000
+ 4MB对应的线性地址 =
0\~4MB。而根据页表建立过程的描述我们可知道线性地址空间0\~4MB与线性地址空间
0xC0000000\~0xC0000000 +
4MB对应同样的物理地址而线性地址空间0xC0000000\~0xC0000000 +
4MB对应的物理地址空间为04MB。这样对于0xC0000000\~0xC0000000 +
4MB这块虚拟地址空间段页式的地址映射关系为 虚拟地址 = 线性地址 +
0xC0000000 =物理地址 + 0xC0000000。
注意这只是针对0xC0000000\~0xC0000000 +
4MB这块虚拟地址空间。如果是0xD0000000\~0xD0000000 +
4MB这块虚拟地址空间则段页式的地址映射关系为虚拟地址 = 线性地址 +
0xC0000000 =物理地址 + 0xC0000000 +
0xC0000000。这不是我们需要的映射关系所以0xC0000000 +
4MB以上的虚拟地址访问会出页错误异常。
4.
最后一步完成收尾工作的正常段页式地址映射阶段即首先调整段映射关系这是通过加载新的全局段描述符表pmm\_init函数调用gdt\_init函数来完成实现这时的段映射关系为虚拟地址
= 线性地址。然后通过执行语句“boot\_pgdir**[**0**]** **=**
0**;**”把boot\_pgdir[0]的第一个页目录表项0\~4MB清零来取消临时的页映射关系。至此新的段页式的地址映射关系为虚拟地址
= 线性地址 =物理地址 +
0xC0000000。这也形成了ucore操作系统的内核虚拟地址空间的段页式映射关系即虚拟地址空间KERNBASE,
KERNBASE + KMEMSIZE= 线性地址空间KERNBASE, KERNBASE + KMEMSIZE =
物理地址空间0KMEMSIZE

3
lab2/paging.md Normal file
View File

@ -0,0 +1,3 @@
## 实现分页机制
在本实验中需要重点了解和实现基于页表的页机制和以页为单位的物理内存管理方法和分配算法等。由于ucore OS是基于80386 CPU实现的所以CPU在进入保护模式后就直接使能了段机制并使得ucore OS需要在段机制的基础上建立页机制。下面比较详细地介绍了实现分页机制的过程。

87
lab2/phymem_allocation.md Normal file
View File

@ -0,0 +1,87 @@
### 物理内存页分配算法实现
如果要在ucore中实现连续物理内存分配算法则需要考虑的事情比较多相对课本上的物理内存分配算法描述要复杂不少。下面介绍一下如果要实现一个FirstFit内存分配算法的大致流程。
lab2的第一部分是完成first\_fit的分配算法。原理FirstFit内存分配算法上很简单但要在ucore中实现需要充分了解和利用ucore已有的数据结构和相关操作、关键的一些全局变量等。
**关键数据结构和变量**
first\_fit分配算法需要维护一个查找有序地址按从小到大排列空闲块以页为最小单位的连续地址空间的数据结构而双向链表是一个很好的选择。
libs/list.h定义了可挂接任意元素的通用双向链表结构和对应的操作所以需要了解如何使用这个文件提供的各种函数从而可以完成对双向链表的初始化/插入/删除等。
kern/mm/memlayout.h中定义了一个 free\_area\_t 数据结构,包含成员结构
```
list_entry_t free_list; // the list header 空闲块双向链表的头
unsigned int nr_free; // # of free pages in this free list 空闲块的总数(以页为单位)
```
显然我们可以通过此数据结构来完成对空闲块的管理。而default\_pmm.c中定义的free\_area变量就是干这个事情的。
kern/mm/pmm.h中定义了一个通用的分配算法的函数列表用pmm\_manager
表示。其中init函数就是用来初始化free\_area变量的,
first\_fit分配算法可直接重用default\_init函数的实现。init\_memmap函数需要根据现有的内存情况构建空闲块列表的初始状态。何时应该执行这个函数呢
通过分析代码,可以知道:
```
kern_init --> pmm_init-->page_init-->init_memmap--> pmm_manager->init_memmap
```
所以default\_init\_memmap需要根据page\_init函数中传递过来的参数某个连续地址的空闲块的起始页页个数来建立一个连续内存空闲块的双向链表。这里有一个假定page\_init函数是按地址从小到大的顺序传来的连续内存空闲块的。链表头是free\_area.free\_list链表项是Page数据结构的base-\>page\_link。这样我们就依靠Page数据结构中的成员变量page\_link形成了连续内存空闲块列表。
**设计实现**
default\_init\_memmap函数讲根据每个物理页帧的情况来建立空闲页链表且空闲页块应该是根据地址高低形成一个有序链表。根据上述变量的定义default\_init\_memmap可大致实现如下
```
default_init_memmap(struct Page *base, size_t n) {
struct Page *p = base;
for (; p != base + n; p ++) {
p->flags = p->property = 0;
set_page_ref(p, 0);
}
base->property = n;
SetPageProperty(base);
nr_free += n;
list_add(&free_list, &(base->page_link));
}
```
如果要分配一个页那要考虑哪些呢这里就需要考虑实现default\_alloc\_pages函数注意参数n表示要分配n个页。另外需要注意实现时尽量多考虑一些边界情况这样确保软件的鲁棒性。比如
```
if (n > nr_free) {
return NULL;
}
```
这样可以确保分配不会超出范围。也可加一些
assert函数在有错误出现时能够迅速发现。比如 n应该大于0我们就可以加上
```
assert(n \> 0);
```
这样在n<=0的情况下ucore会迅速报错。firstfit需要从空闲链表头开始查找最小的地址通过list\_next找到下一个空闲块元素通过le2page宏可以更加链表元素获得对应的Page指针p。通过p-\>property可以了解此空闲块的大小。如果\>=n这就找到了如果<n则list\_next继续查找直到list\_next==
&free\_list这表示找完了一遍了。找到后就要从新组织空闲块然后把找到的page返回。所以default\_alloc\_pages可大致实现如下
```
static struct Page *
default_alloc_pages(size_t n) {
if (n > nr_free) {
return NULL;
}
struct Page *page = NULL;
list_entry_t *le = &free_list;
while ((le = list_next(le)) != &free_list) {
struct Page *p = le2page(le, page_link);
if (p->property >= n) {
page = p;
break;
}
}
if (page != NULL) {
list_del(&(page->page_link));
if (page->property > n) {
struct Page *p = page + n;
p->property = page->property - n;
list_add(&free_list, &(p->page_link));
}
nr_free -= n;
ClearPageProperty(page);
}
return page;
}
```
default\_free\_pages函数的实现其实是default\_alloc\_pages的逆过程不过需要考虑空闲块的合并问题。这里就不再细讲了。注意上诉代码只是参考设计不是完整的正确设计。更详细的说明位于lab2/kernel/mm/default\_pmm.c的注释中。希望同学能够顺利完成本实验的第一部分。

2
lab2/phymem_manage.md Normal file
View File

@ -0,0 +1,2 @@
## 物理内存管理
接下来将首先对实验的执行流程做个介绍并进一步介绍如何探测物理内存的大小与布局如何以页为单位来管理计算机系统中的物理内存如何设计物理内存页的分配算法最后比较详细地分析了在80386的段页式硬件机制下ucore操作系统把段式内存管理的功能弱化并实现以分页为主的页式内存管理的过程。

102
lab2/phymem_pagelevel.md Normal file
View File

@ -0,0 +1,102 @@
###以页为单位管理物理内存
在获得可用物理内存范围后系统需要建立相应的数据结构来管理以物理页按4KB对齐且大小为4KB的物理内存单元为最小单位的整个物理内存以配合后续涉及的分页管理机制。每个物理页可以用一个
Page数据结构来表示。由于一个物理页需要占用一个Page结构的空间Page结构在设计时须尽可能小以减少对内存的占用。Page的定义在kern/mm/memlayout.h中。以页为单位的物理内存分配管理的实现在kern/default\_pmm.[ch]。
为了与以后的分页机制配合我们首先需要建立对整个计算机的每一个物理页的属性用结构Page来表示它包含了映射此物理页的虚拟页个数描述物理页属性的flags和双向链接各个Page结构的page\_link双向链表。
```
struct Page {
int ref; // page frame's reference counter
uint32_t flags; // array of flags that describe the status of the page frame
unsigned int property;// the num of free block, used in first fit pm manager
list_entry_t page_link;// free list link
};
```
这里看看Page数据结构的各个成员变量有何具体含义。ref表示这样页被页表的引用记数在“实现分页机制”一节会讲到。如果这个页被页表引用了即在某页表中有一个页表项设置了一个虚拟页到这个Page管理的物理页的映射关系就会把Page的ref加一反之若页表项取消即映射关系解除就会把Page的ref减一。flags表示此物理页的状态标记进一步查看kern/mm/memlayout.h中的定义可以看到
```
/* Flags describing the status of a page frame */
#define PG_reserved 0 // the page descriptor is reserved for kernel or unusable
#define PG_property 1 // the member 'property' is valid
```
这表示flags目前用到了两个bit表示页目前具有的两种属性bit
0表示此页是否被保留reserved如果是被保留的页则bit
0会设置为1且不能放到空闲页链表中即这样的页不是空闲页不能动态分配与释放。比如目前内核代码占用的空间就属于这样“被保留”的页。在本实验中bit
1表示此页是否是free的如果设置为1表示这页是free的可以被分配如果设置为0表示这页已经被分配出去了不能被再二次分配。另外本实验这里取的名字PG\_property比较不直观
主要是我们可以设计不同的页分配算法best fit, buddy
system等那么这个PG\_property就有不同的含义了。
在本实验中Page数据结构的成员变量property用来记录某连续内存空闲块的大小即地址连续的空闲页的个数。这里需要注意的是用到此成员变量的这个Page比较特殊是这个连续内存空闲块地址最小的一页即头一页
Head
Page。连续内存空闲块利用这个页的成员变量property来记录在此块内的空闲页的个数。这里去的名字property也不是很直观原因与上面类似在不同的页分配算法中property有不同的含义。
Page数据结构的成员变量page\_link是便于把多个连续内存空闲块链接在一起的双向链表指针可回顾在lab0实验指导书中有关双向链表数据结构的介绍。这里需要注意的是用到此成员变量的这个Page比较特殊是这个连续内存空闲块地址最小的一页即头一页
Head
Page。连续内存空闲块利用这个页的成员变量page\_link来链接比它地址小和大的其他连续内存空闲块。
在初始情况下也许这个物理内存的空闲物理页都是连续的这样就形成了一个大的连续内存空闲块。但随着物理页的分配与释放这个大的连续内存空闲块会分裂为一系列地址不连续的多个小连续内存空闲块且每个连续内存空闲块内部的物理页是连续的。那么为了有效地管理这些小连续内存空闲块。所有的连续内存空闲块可用一个双向链表管理起来便于分配和释放为此定义了一个free\_area\_t数据结构包含了一个list\_entry结构的双向链表指针和记录当前空闲页的个数的无符号整型变量nr\_free。其中的链表指针指向了空闲的物理页。
```
/* free_area_t - maintains a doubly linked list to record free (unused) pages */
typedef struct {
list_entry_t free_list; // the list header
unsigned int nr_free; // # of free pages in this free list
} free_area_t;
```
有了这两个数据结构ucore就可以管理起来整个以页为单位的物理内存空间。接下来需要解决两个问题
• 管理页级物理内存空间所需的Page结构的内存空间从哪里开始占多大空间
• 空闲内存空间的起始地址在哪里?
对于这两个问题我们首先根据bootloader给出的内存布局信息找出最大的物理内存地址maxpa定义在page\_init函数中的局部变量由于x86的起始物理内存地址为0所以可以得知需要管理的物理页个数为
```
npage = maxpa / PGSIZE
```
这样我们就可以预估出管理页级物理内存空间所需的Page结构的内存空间所需的内存大小为
```
sizeof(struct Page) * npage)
```
由于bootloader加载ucore的结束地址用全局指针变量end记录以上的空间没有被使用所以我们可以把end按页大小为边界去整后作为管理页级物理内存空间所需的Page结构的内存空间记为
```
pages = (struct Page *)ROUNDUP((void *)end, PGSIZE);
```
为了简化起见从地址0到地址pages+ sizeof(struct Page) \*
npage)结束的物理内存空间设定为已占用物理内存空间起始0\~640KB的空间是空闲的地址pages+
sizeof(struct Page) \*
npage)以上的空间为空闲物理内存空间,这时的空闲空间起始地址为
```
uintptr_t freemem = PADDR((uintptr_t)pages + sizeof(struct Page) * npage);
```
为此我们需要把这两部分空间给标识出来。首先,对于所有物理空间,通过如下语句即可实现占用标记:
```
for (i = 0; i < npage; i ++) {
SetPageReserved(pages + i);
}
````
然后,根据探测到的空闲物理空间,通过如下语句即可实现空闲标记:
```
//获得空闲空间的起始地址begin和结束地址end
……
init_memmap(pa2page(begin), (end - begin) / PGSIZE);
```
其实SetPageReserved只需把物理地址对应的Page结构中的flags标志设置为PG\_reserved
表示这些页已经被使用了将来不能被用于分配。而init\_memmap函数则是把空闲物理页对应的Page结构中的flags和引用计数ref清零并加到free\_area.free\_list指向的双向列表中为将来的空闲页管理做好初始化准备工作。
关于内存分配的操作系统原理方面的知识有很多但在本实验中只实现了最简单的内存页分配算法。相应的实现在default\_pmm.c中的default\_alloc\_pages函数和default\_free\_pages函数相关实现很简单这里就不具体分析了直接看源码应该很好理解。
其实实验二在内存分配和释放方面最主要的作用是建立了一个物理内存页管理器框架,这实际上是一个函数指针列表,定义如下:
```
struct pmm_manager {
const char *name; //物理内存页管理器的名字
void (*init)(void); //初始化内存管理器
void (*init_memmap)(struct Page *base, size_t n); //初始化管理空闲内存页的数据结构
struct Page *(*alloc_pages)(size_t n); //分配n个物理内存页
void (*free_pages)(struct Page *base, size_t n); //释放n个物理内存页
size_t (*nr_free_pages)(void); //返回当前剩余的空闲页数
void (*check)(void); //用于检测分配/释放实现是否正确的辅助函数
};
```
重点是实现init\_memmap/ alloc\_pages/
free\_pages这三个函数。当完成物理内存页管理初始化工作后计算机系统的内存布局如下图所示
![image](./lab2_figs/image003.png)
图3 计算机系统的内存布局

View File

View File

@ -0,0 +1,5 @@
## 实验内容
本次实验包含三个部分。首先了解如何发现系统中的物理内存;然后了解如何建立对物理内存的初步管理,即了解连续物理内存管理;最后了解页表相关的操作,即如何建立页表来实现虚拟内存到物理内存之间的映射,对段页式内存管理机制有一个比较全面的了解。本实验里面实现的内存管理还是非常基本的,并没有涉及到对实际机器的优化,比如
针对 cache 的优化等。实际操作系统如Linux等中的内存管理是相当复杂的。
如果大家有余力,尝试完成扩展练习。

View File

@ -0,0 +1,33 @@
### 练习
**练习0填写已有实验**
本实验依赖实验1。请把你做的实验1的代码填入本实验中代码中有“LAB1”的注释相应部分。提示可采用merge工具比如kdiff3eclipse中的diff/merge工具understand中的diff/merge工具等。
**练习1实现 first-fit 连续物理内存分配算法(需要编程)**
在实现first fit
内存分配算法的回收函数时,要考虑地址连续的空闲块之间的合并操作。提示:在建立空闲页块链表时需要按照空闲页块起始地址来排序形成一个有序的链表。可能会修改default\_pmm.c中的default\_initdefault\_init\_memmapdefault\_alloc\_pages
default\_free\_pages等相关函数。请仔细查看和理解default\_pmm.c中的注释。
**练习2实现寻找虚拟地址对应的页表项需要编程**
通过设置页表和对应的页表项可建立虚拟内存地址和物理内存地址的对应关系。其中的get\_pte函数是设置页表项环节中的一个重要步骤。此函数找到一个虚地址对应的二级页表项的内核虚地址如果此二级页表项不存在则分配一个包含此项的二级页表。本练习需要补全get\_pte函数
in
kern/mm/pmm.c实现其功能。请仔细查看和理解get\_pte函数中的注释。get\_pte函数的调用关系图如下所示
![image](lab2_figs/image001.png)
图1 get\_pte函数的调用关系图
**练习3释放某虚地址所在的页并取消对应二级页表项的映射需要编程**
当释放一个包含某虚地址的物理内存页时需要让对应此物理内存页的管理数据结构Page做相关的清除处理使得此物理内存页成为空闲另外还需把表示虚地址与物理地址对应关系的二级页表项清除。请仔细查看和理解page\_remove\_pte函数中的注释。为此需要补全在
kern/mm/pmm.c中的page\_remove\_pte函数。page\_remove\_pte函数的调用关系图如下所示
![image](lab2_figs/image002.png)
图2 page\_remove\_pte函数的调用关系图
**扩展练习Challenge任意大小的内存单元slub分配算法需要编程**
如果觉得上诉练习难度不够可考虑完成此扩展练习。实现两层架构的高效内存单元分配第一层是基于页大小的内存分配第二层是在第一层基础上实现基于任意大小的内存分配。比如如果连续分配8个16字节的内存块当分配完毕后实际只消耗了一个空闲物理页。要求时空都高效可参考slub算法来实现可简化实现能够体现其主体思想即可。要求有设计文档。slub相关网页在[http://www.ibm.com/developerworks/cn/linux/l-cn-slub/](http://www.ibm.com/developerworks/cn/linux/l-cn-slub/)
。完成challenge的同学可单独提交challenge。完成得好的同学可获得最终考试成绩的加分。

89
lab2/phymemlab_files.md Normal file
View File

@ -0,0 +1,89 @@
### 项目组成
表1 实验二文件列表
```
|-- boot
| |-- asm.h
| |-- bootasm.S
| \`-- bootmain.c
|-- kern
| |-- init
| | |-- entry.S
| | \`-- init.c
| |-- mm
| | |-- default\_pmm.c
| | |-- default\_pmm.h
| | |-- memlayout.h
| | |-- mmu.h
| | |-- pmm.c
| | \`-- pmm.h
| |-- sync
| | \`-- sync.h
| \`-- trap
| |-- trap.c
| |-- trapentry.S
| |-- trap.h
| \`-- vectors.S
|-- libs
| |-- atomic.h
| |-- list.h
\`-- tools
|-- kernel.ld
```
相对与实验一,实验二主要增加和修改的文件如上表红色部分所示。主要改动如下:
* boot/bootasm.S增加了对计算机系统中物理内存布局的探测功能
* kern/init/entry.S根据临时段表重新暂时建立好新的段空间为进行分页做好准备。
* kern/mm/default\_pmm.[ch]提供基本的基于链表方法的物理内存管理分配单位为页即4096字节
* kern/mm/pmm.[ch]pmm.h定义物理内存管理类框架struct
pmm\_manager基于此通用框架可以实现不同的物理内存管理策略和算法(default\_pmm.[ch]
实现了一个基于此框架的简单物理内存管理策略)
pmm.c包含了对此物理内存管理类框架的访问以及与建立、修改、访问页表相关的各种函数实现。
* kern/sync/sync.h为确保内存管理修改相关数据时不被中断打断提供两个功能一个是保存eflag寄存器中的中断屏蔽位信息并屏蔽中断的功能另一个是根据保存的中断屏蔽位信息来使能中断的功能可不用细看
* libs/list.h定义了通用双向链表结构以及相关的查找、插入等基本操作这是建立基于链表方法的物理内存管理以及其他内核功能的基础。其他有类似双向链表需求的内核功能模块可直接使用list.h中定义的函数。
* libs/atomic.h定义了对一个变量进行读写的原子操作确保相关操作不被中断打断。可不用细看
* tools/kernel.ldld形成执行文件的地址所用到的链接脚本。修改了ucore的起始入口和代码段的起始地址。相关细节可参看附录C。
**编译方法**
编译并运行代码的命令如下:
```
make
make qemu
```
则可以得到如下显示界面(仅供参考)
```
chenyu$ make qemu
(THU.CST) os is loading ...
Special kernel symbols:
entry 0xc010002c (phys)
etext 0xc010537f (phys)
edata 0xc01169b8 (phys)
end 0xc01178dc (phys)
Kernel executable memory footprint: 95KB
memory managment: default_pmm_manager
e820map:
memory: 0009f400, [00000000, 0009f3ff], type = 1.
memory: 00000c00, [0009f400, 0009ffff], type = 2.
memory: 00010000, [000f0000, 000fffff], type = 2.
memory: 07efd000, [00100000, 07ffcfff], type = 1.
memory: 00003000, [07ffd000, 07ffffff], type = 2.
memory: 00040000, [fffc0000, ffffffff], type = 2.
check_alloc_page() succeeded!
check_pgdir() succeeded!
check_boot_pgdir() succeeded!
-------------------- BEGIN --------------------
PDE(0e0) c0000000-f8000000 38000000 urw
|-- PTE(38000) c0000000-f8000000 38000000 -rw
PDE(001) fac00000-fb000000 00400000 -rw
|-- PTE(000e0) faf00000-fafe0000 000e0000 urw
|-- PTE(00001) fafeb000-fafec000 00001000 -rw
--------------------- END ---------------------
++ setup timer interrupts
100 ticks
100 ticks
……
```
通过上图我们可以看到ucore在显示其entry入口地址、etext代码段截止处地址、edata数据段截止处地址、和enducore截止处地址的值后探测出计算机系统中的物理内存的布局e820map下的显示内容。接下来ucore会以页为最小分配单位实现一个简单的内存分配管理完成二级页表的建立进入分页模式执行各种我们设置的检查最后显示ucore建立好的二级页表内容并在分页模式下响应时钟中断。

5
lab2/phymemlab_goal.md Normal file
View File

@ -0,0 +1,5 @@
## 实验目的
* 理解基于段页式内存地址的转换机制
* 理解页表的建立和使用方法
* 理解物理内存的管理方法

View File

@ -0,0 +1,31 @@
### 实验执行流程概述
本次实验主要完成ucore内核对物理内存的管理工作。参考ucore总控函数kern\_init的代码可以清楚地看到在调用完成物理内存初始化的pmm\_init函数之前和之后是已有lab1实验的工作好像没啥修改。其实不然ucore有两个方面的扩展。首先bootloader的工作有增加在bootloader中完成了对物理内存资源的探测工作可进一步参阅附录A和附录B让ucore
kernel在后续执行中能够基于bootloader探测出的物理内存情况进行物理内存管理初始化工作。其次bootloader不像lab1那样直接调用kern\_init函数而是先调用位于lab2/kern/init/entry.S中的kern\_entry函数。kern\_entry函数的主要任务是为执行kern\_init建立一个良好的C语言运行环境设置堆栈而且临时建立了一个段映射关系为之后建立分页机制的过程做一个准备细节在3.5小节有进一步阐述。完成这些工作后才调用kern\_init函数。
kern\_init函数在完成一些输出并对lab1实验结果的检查后将进入物理内存管理初始化的工作即调用pmm\_init函数完成物理内存的管理这也是我们lab2的内容。接着是执行中断和异常相关的初始化工作即调用pic\_init函数和idt\_init函数等这些工作与lab1的中断异常初始化工作的内容是相同的。
为了完成物理内存管理这里首先需要探测可用的物理内存资源了解到物理内存位于什么地方有多大之后就以固定页面大小来划分整个物理内存空间并准备以此为最小内存分配单位来管理整个物理内存管理在内核运行过程中每页内存设定其可用状态free的used的还是reserved的这其实就对应了我们在课本上讲到的连续内存分配概念和原理的具体实现接着ucore
kernel就要建立页表
启动分页机制让CPU的MMU把预先建立好的页表中的页表项读入到TLB中根据页表项描述的虚拟页Page与物理页帧Page
Frame的对应关系完成CPU对内存的读、写和执行操作。这一部分其实就对应了我们在课本上讲到内存映射、页表、多级页表等概念和原理的具体实现。
在代码分析上建议根据执行流程来直接看源代码并可采用GDB源码调试的手段来动态地分析ucore的执行过程。内存管理相关的总体控制函数是pmm\_init函数它完成的主要工作包括
1. 初始化物理内存页管理器框架pmm\_manager
2. 建立空闲的page链表这样就可以分配以页4KB为单位的空闲内存了
3. 检查物理内存页分配算法;
4. 为确保切换到分页机制后,代码能够正常执行,先建立一个临时二级页表;
5. 建立一一映射关系的二级页表;
6. 使能分页机制;
7. 从新设置全局段描述符表;
8. 取消临时二级页表;
9. 检查页表建立是否正确;
10. 通过自映射机制完成页表的打印输出(这部分是扩展知识)
另外,主要注意的相关代码内容包括:
* boot/bootasm.S中探测内存部分从probe\_memory到finish\_probe的代码
* 管理每个物理页的Page数据结构在mm/memlayout.h中这个数据结构也是实现连续物理内存分配算法的关键数据结构可通过此数据结构来完成空闲块的链接和信息存储而基于这个数据结构的管理物理页数组起始地址就是全局变量pages具体初始化此数组的函数位于page\_init函数中
* 用于实现连续物理内存分配算法的物理内存页管理器框架pmm\_manager这个数据结构定义了实现内存分配算法的关键函数指针而同学需要完成这些函数的具体实现
* 设定二级页表和建立页表项以完成虚实地址映射关系这与硬件相关且用到不少内联函数源代码相对难懂一些。具体完成页表和页表项建立的重要函数是boot\_map\_segment函数而get\_pte函数是完成虚实映射关键的关键。

View File

@ -0,0 +1,6 @@
## 实验报告要求
从网站上下载lab2的源码后完成实验中的各个练习。完成代码编写并检查无误后在对应目录下执行
make handin 任务即会自动生成lab2-handin.tar.gz。
注意有***“LAB2”***的注释代码中所有需要完成的地方challenge除外都有***“LAB2”***和***“YOUR CODE”***的注释,请在提交时特别注意保持注释,并将***“YOUR CODE”***替换为自己的学号,并且将所有标有对应注释的部分填上正确的代码。

View File

@ -0,0 +1,55 @@
**探测物理内存分布和大小的方法**
操作系统需要知道了解整个计算机系统中的物理内存如何分布的哪些被可用哪些不可用。其基本方法是通过BIOS中断调用来帮助完成的。其中BIOS中断调用必须在实模式下进行所以在bootloader进入保护模式前完成这部分工作相对比较合适。这些部分由boot/bootasm.S中从probe\_memory处到finish\_probe处的代码部分完成完成。通过BIOS中断获取内存可调用参数为e820h的INT
15h BIOS中断。BIOS通过系统内存映射地址描述符Address Range
Descriptor格式来表示系统物理内存布局其具体表示如下
```
Offset Size Description
00h 8字节 base address #系统内存块基地址
08h 8字节 length in bytes #系统内存大小
10h 4字节 type of address range #内存类型
```
看下面的(Values for System Memory Map address type)
```
Values for System Memory Map address type:
01h memory, available to OS
02h reserved, not available (e.g. system ROM, memory-mapped device)
03h ACPI Reclaim Memory (usable by OS after reading ACPI tables)
04h ACPI NVS Memory (OS is required to save this memory between NVS sessions)
other not defined yet -- treat as Reserved
```
INT15h BIOS中断的详细调用参数:
```
eaxe820hINT 15的中断调用参数
edx534D4150h (即4个ASCII字符“SMAP”) ,这只是一个签名而已;
ebx如果是第一次调用或内存区域扫描完毕则为0。 如果不是,则存放上次调用之后的计数值;
ecx保存地址范围描述符的内存大小,应该大于等于20字节
es:di指向保存地址范围描述符结构的缓冲区BIOS把信息写入这个结构的起始地址。
```
此中断的返回值为:
```
cflags的CF位若INT 15中断执行成功则不置位否则置位
eax534D4150h ('SMAP')
es:di指向保存地址范围描述符的缓冲区,此时缓冲区内的数据已由BIOS填写完毕
ebx下一个地址范围描述符的计数地址
ecx 返回BIOS往ES:DI处写的地址范围描述符的字节大小
ah失败时保存出错代码
```
这样我们通过调用INT 15h
BIOS中断递增di的值20的倍数让BIOS帮我们查找出一个一个的内存布局entry并放入到一个保存地址范围描述符结构的缓冲区中供后续的ucore进一步进行物理内存管理。这个缓冲区结构定义在memlayout.h中
```
struct e820map {
int nr_map;
struct {
long long addr;
long long size;
long type;
} map[E820MAX];
};
```
****

View File

@ -0,0 +1,13 @@
### 探测系统物理内存布局
当 ucore
被启动之后,最重要的事情就是知道还有多少内存可用,一般来说,获取内存大小的方法由
BIOS 中断调用和直接探测两种。但BIOS
中断调用方法是一般只能在实模式下完成,而直接探测方法必须在保护模式下完成。通过
BIOS 中断获取内存布局有三种方式都是基于INT 15h中断分别为88h e801h
e820h。但是 并非在所有情况下这三种方式都能工作。在 Linux kernel
里,采用的方法是依次尝试这三
种方法。而在本实验中我们通过e820h中断获取内存信息。因为e820h中断必须在实模式下使用所以我们在
bootloader 进入保护模式之前调用这个 BIOS 中断,并且把 e820 映
射结构保存在物理地址0x8000处。具体实现详见boot/bootasm.S。有关探测系统物理内存方法和具体实现的
信息参见[附录A和附录B。](http://wiki.osdev.org/How_Do_I_Determine_The_Amount_Of_RAM)

View File

@ -0,0 +1,22 @@
### 段页式管理基本概念
如图4在保护模式中x86
体系结构将内存地址分成三种:逻辑地址(也称虚地址)、线性地址和物理地址。逻辑地址即是程序指令中使用的地址,物理地址是实际访问内存的地址。逻
辑地址通过段式管理的地址映射可以得到线性地址,线性地址通过页式管理的地址映射得到物理地址。
![image](lab2_figs/image004.png)
图 4 段页式管理总体框架图
段式管理前一个实验已经讨论过。在 ucore
中段式管理只起到了一个过渡作用,它将逻辑地址不加转换直接映射成线性地址,所以我们在下面的讨论中可以对这两个地址不加区分(目前的
OS 实现也是不加区分的。对段式管理有兴趣的同学可以参照《Intel® 64 and
IA-32Architectures Software Developer s Manual Volume 3A》3.2 节。
![image](lab2_figs/image005.png)如图5所示页式管理将线性地址分成三部分图中的
Linear Address 的 Directory 部分、 Table 部分和 Offset 部分。ucore
的页式管理通过一个二级的页表实现。一级页表的起始物理地址存放在 cr3
寄存器中,这个地址必须是一个页对齐的地址,也就是低 12 位必须为
0。目前ucore 用boot\_cr3mm/pmm.c记录这个值。
![image](lab2_figs/image006.png)
图 5 分页机制管理

97
lab2/self_mapping.md Normal file
View File

@ -0,0 +1,97 @@
### 自映射机制
这是扩展知识。
上一小节讲述了通过boot\_map\_segment函数建立了基于一一映射关系的页目录表项和页表项这里的映射关系为
virtual addr (KERNBASE\~KERNBASE+KMEMSIZE) = physical\_addr
(0\~KMEMSIZE)
这样只要给出一个虚地址和一个物理地址就可以设置相应PDE和PTE就可完成正确的映射关系。
如果我们这时需要按虚拟地址的地址顺序显示整个页目录表和页表的内容,则要查找页目录表的页目录表项内容,根据页目录表项内容找到页表的物理地址,再转换成对应的虚地址,然后访问页表的虚地址,搜索整个页表的每个页目录项。这样过程比较繁琐。
我们需要有一个简洁的方法来实现这个查找。ucore做了一个很巧妙的地址自映射设计把页目录表和页表放在一个连续的4MB虚拟地址空间中并设置页目录表自身的虚地址<--\>物理地址映射关系这样在已知页目录表起始虚地址的情况下通过连续扫描这特定的4MB虚拟地址空间就很容易访问每个页目录表项和页表项内容
具体而言ucore是这样设计的首先设置了一个常量memlayout.h
VPT=0xFAC00000 这个地址的二进制表示为:
1111 1010 1100 0000 0000 0000 0000 0000
高10位为1111 1010
11即10进制的1003中间10位为0低12位也为0。在pmm.c中有两个全局初始化变量
pte\_t \* const vpt = (pte\_t \*)VPT;
pde\_t \* const vpd = (pde\_t \*)PGADDR(PDX(VPT), PDX(VPT), 0);
并在pmm\_init函数执行了如下语句
boot\_pgdir[PDX(VPT)] = PADDR(boot\_pgdir) | PTE\_P | PTE\_W;
这些变量和语句有何特殊含义呢其实vpd变量的值就是页目录表的起始虚地址0xFAFEB000且它的高10位和中10位是相等的都是10进制的1003。当执行了上述语句就确保了vpd变量的值就是页目录表的起始虚地址且vpt是页目录表中第一个目录表项指向的页表的起始虚地址。此时描述内核虚拟空间的页目录表的虚地址为0xFAFEB000大小为4KB。页表的理论连续虚拟地址空间0xFAC00000\~0xFB000000大小为4MB。因为这个连续地址空间的大小为4MB可有1M个PTE即可映射4GB的地址空间。
但ucore实际上不会用完这么多项在memlayout.h中定义了常量
\#define KMEMSIZE 0x38000000
表示ucore只支持896MB的物理内存空间这个896MB只是一个设定可以根据情况改变。则最大的内核虚地址为常量
\#define KERNTOP (KERNBASE + KMEMSIZE)=0xF8000000
所以最大内核虚地址KERNTOP的页目录项虚地址为
vpd+0xF8000000/0x400000=0xFAFEB000+0x3E0=0xFAFEB3E0
最大内核虚地址KERNTOP的页表项虚地址为
vpt+0xF8000000/0x1000=0xFAC00000+0xF8000=0xFACF8000
在pmm.c中的函数print\_pgdir就是基于ucore的页表自映射方式完成了对整个页目录表和页表的内容扫描和打印。注意这里不会出现某个页表的虚地址与页目录表虚地址相同的情况。
![image](lab2_figs/image009.png)![image](lab2_figs/image010.png)![image](lab2_figs/image011.png)print
pgdir函数使得 ucore 具备和 qemu 的info pg相同的功能即print pgdir能
够从内存中将当前页表内有效数据PTE\_P印出来。拷贝出的格式如下所示:
```
PDE(0e0) c0000000-f8000000 38000000 urw
|-- PTE(38000) c0000000-f8000000 38000000 -rw
PDE(001) fac00000-fb000000 00400000 -rw
|-- PTE(000e0) faf00000-fafe0000 000e0000 urw
|-- PTE(00001) fafeb000-fafec000 00001000 -rw
```
上面中的数字包括括号里的,都是十六进制。
主要的功能是从页表中将具备相同权限的 PDE 和 PTE
项目组织起来。比如上表中:
```
PDE(0e0) c0000000-f8000000 38000000 urw
```
• PDE(0e0)0e0表示 PDE 表中相邻的 224 项具有相同的权限;
• c0000000-f8000000表示 PDE 表中,这相邻的两项所映射的线性地址的范围;
• 38000000同样表示范围即f8000000减去c0000000的结果
• urwPDE 表中所给出的权限位u表示用户可读即PTE\_Ur表示PTE\_Pw表示用
户可写即PTE\_W。
```
PDE(001) fac00000-fb000000 00400000 -rw
```
表示仅 1 条连续的 PDE 表项具备相同的属性。相应的,在这条表项中遍历找到 2
组 PTE 表项,输出如下:
```
|-- PTE(000e0) faf00000-fafe0000 000e0000 urw
|-- PTE(00001) fafeb000-fafec000 00001000 -rw
```
注意:
1. PTE 中输出的权限是 PTE 表中的数据给出的,并没有和 PDE
表中权限做与运算。
2.
![image](lab2_figs/image012.png)整个print\_pgdir函数强调两点第一是相同权限第二是连续。
3.
![image](lab2_figs/image013.png)print\_pgdir中用到了vpt和vpd两个变量。可以参
考VPT和PGADDR两个宏。
自映射机制还可方便用户态程序访问页表。因为页表是内核维护的用户程序很难知道自己页表的映射结构。VPT
实际上在内核地址空间的,我们可以用同样的方式实现一个用户地址空间的映射(比如
pgdir[UVPT] = PADDR(pgdir) | PTE\_P | PTE\_U注意这里不能给写权限并且
pgdir 是每个进程的 page table不是
boot\_pgdir这样用户程序就可以用和内核一样的 print\_pgdir
函数遍历自己的页表结构了。

156
lab2/setup_paging_map.md Normal file
View File

@ -0,0 +1,156 @@
###建立虚拟页和物理页帧的地址映射关系
**从链接脚本分析 ucore 执行时的地址**
首先观察一下tools/kernel.ld文件在lab1和lab2中的区别。在lab1中
```
ENTRY(kern_init)
SECTIONS {
/* Load the kernel at this address: "." means the current address */
. = 0x100000;
.text : {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
```
这意味着在lab1中通过ld工具形成的ucore的起始虚拟地址从0x100000开始注意这个地址是虚拟地址。但由于lab1中建立的段地址映射关系为对等关系所以ucore的物理地址也是0x100000。而入口函数为kern\_init函数。在lab2中
```
ENTRY(kern_entry)
SECTIONS {
/* Load the kernel at this address: "." means the current address */
. = 0xC0100000;
.text : {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
```
这意味着lab2中通过ld工具形成的ucore的起始虚拟地址从0xC0100000开始注意这个地址也是虚拟地址。入口函数为kern\_entry函数。这与lab1有很大差别。但其实在lab1和lab2中bootloader把ucore都放在了起始物理地址为0x100000的物理内存空间。这实际上说明了ucore在lab1和lab2中采用的地址映射不同
lab1 virtual addr = linear addr = phy addr
lab2 virtual addr = linear addr = phy addr + 0xC0000000
lab1只采用了段映射机制但在lab2中启动好分页管理机制后形成的是段页式映射机制从而使得虚拟地址空间和物理地址空间之间存在如下的映射关系
Virtual Address=LinearAddress=0xC0000000+Physical Address
另外ucore的入口地址也改为了kern\_entry函数这个函数位于init/entry.S中分析代码可以看出entry.S重新建立了段映射关系从以前的
Virtual Address= Linear Address
改为
Virtual Address=Linear Address-0xC0000000
由于gcc编译出的虚拟起始地址从0xC0100000开始ucore被bootloader放置在从物理地址0x100000处开始的物理内存中。所以当kern\_entry函数完成新的段映射关系后且ucore在没有建立好页映射机制前CPU按照ucore中的虚拟地址执行能够被分段机制映射到正确的物理地址上确保ucore运行正确。
由于物理内存页管理器管理了从0到实际可用物理内存大小的物理内存空间所以对于这些物理内存空间都需要建立好页映射关系。由于目前ucore只运行在内核空间所以可以建立一个一一映射关系。假定内核虚拟地址空间的起始地址为0xC0000000则虚拟内存和物理内存的具体页映射关系为
Virtual Address=Physical Address+0xC0000000
**建立二级页表**
由于我们已经具有了一个物理内存页管理器default\_pmm\_manager我们就可以用它来获得所需的空闲物理页。在二级页表结构中页目录表占4KB空间ucore就可通过default\_pmm\_manager的default\_alloc\_pages函数获得一个空闲物理页这个页的起始物理地址就是页目录表的起始地址。同理ucore也通过这种方式获得各个页表所需的空间。页表的空间大小取决与页表要管理的物理页数n一个页表项32位即4字节可管理一个物理页页表需要占n/256个物理页空间。这样页目录表和页表所占的总大小为4096+1024\*n字节。
为把0\~KERNSIZE明确ucore设定实际物理内存不能超过KERNSIZE值即0x38000000字节896MB3670016个物理页的物理地址一一映射到页目录表项和页表项的内容其大致流程如下
1. 先通过default\_pmm\_manager获得一个空闲物理页用于页目录表
2. 调用boot\_map\_segment函数建立一一映射关系具体处理过程以页为单位进行设置
Virtual Address=Physical Address+0xC0000000
Ø 设一个逻辑地址la按页对齐故低12位为零对应的物理地址pa按页对齐故低12位为零如果在页目录表项la的高10位为索引值中的存在位PTE\_P为0表示缺少对应的页表空间则可通过default\_pmm\_manager获得一个空闲物理页给页表页表起始物理地址是按4096字节对齐的这样填写页目录表项的内容为
***页目录表项内容 = 页表起始物理地址 | PTE_U | PTE_W | PTE\_P***
进一步对于页表中对应页表项la的中10位为索引值的内容为
***页表项内容 = pa | PTE\_P | PTE\_W***
其中:
* PTE\_U位3表示用户态的软件可以读取对应地址的物理内存页内容
* PTE\_W位2表示物理内存页内容可写
* PTE\_P位1表示物理内存页存在
ucore
的内存管理经常需要查找页表给定一个虚拟地址找出这个虚拟地址在二级页表中对应的项。通过更改此项的值可以方便地将虚拟地址映射到另外的页上。可完成此功能的这个函数是get\_pte函数。它的原型为
```
pte_t *get_pte (pde_t *pgdir, uintptr_t la, bool create)
```
下面的调用关系图可以比较好地看出get\_pte在实现上诉流程中的位置
![image](lab2_figs/image007.png)
图6 get\_pte调用关系图
这里涉及到三个类型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表示为线性地址由于段式管理只做直接映射所以它也是逻辑地址。
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。如果原来就有二级页表或者新建立了页表则只需返回对应项的地址即可。
虚拟地址只有映射上了物理页才可以正常的读写。在完成映射物理页的过程中,除了要象上面那样在页表的对应表项上填上相应的物理地址外,还要设置正确的控制位。有关
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的逆操作。
建立好一一映射的二级页表结构后接下来就要使能分页机制了这主要是通过enable\_paging函数实现的这个函数主要做了两件事
1. 通过lcr3指令把页目录表的起始地址存入CR3寄存器中
2. 通过lcr0指令把cr0中的CR0\_PG标志位设置上。
执行完enable\_paging函数后计算机系统进入了分页模式但到这一步还不够还记得ucore在最开始通过kern\_entry函数设置了临时的新段映射机制吗这个临时的新段映射机制不是最简单的对等映射导致虚拟地址和线性地址不相等。而刚才建立的页映射关系是建立在简单的段对等映射即虚拟地址=线性地址的假设基础之上的。所以我们需要进一步调整段映射关系即重新设置新的GDT建立对等段映射。
这里需要注意在进入分页模式到重新设置新GDT的过程是一个过渡过程。在这个过渡过程中已经建立了页表机制所以通过现在的段机制和页机制实现的地址映射关系为
```
Virtual Address=Linear Address + 0xC0000000 = Physical Address
+0xC0000000+0xC0000000
```
在这个特殊的阶段如果不把段映射关系改为Virtual Address = Linear
Address则通过段页式两次地址转换后无法得到正确的物理地址。为此我们需要进一步调用gdt\_init函数根据新的gdt全局段描述符表内容gdt定义位于pmm.c中恢复以前的段映射关系即使得Virtual
Address = Linear
Address。这样在执行完gdt\_init后通过的段机制和页机制实现的地址映射关系为
```
Virtual Address=Linear Address = Physical Address +0xC0000000
```
这里存在的一个问题是在调用enable\_page函数使能分页机制后到执行完毕gdt\_init函数重新建立好段页式映射机制的过程中内核使用的还是旧的段表映射也就是说enable
paging 之后,内核使用的是页表的低地址 entry。
如何保证此时内核依然能够正常工作呢其实只需让低地址目录表项的内容等于以KERNBASE开始的高地址目录表项的内容即可。目前内核大小不超过
4M 实际上是3M因为内核从 0x100000
开始编址这样就只需要让页表在0\~4MB的线性地址与KERNBASE \~
KERNBASE+4MB的线性地址获得相同的映射即可都映射到 0\~4MB
的物理地址空间具体实现在pmm.c中pmm\_init函数的语句
```
boot_pgdir[0] = boot_pgdir[PDX(KERNBASE)];
```
实际上这种映射也限制了内核的大小。当内核大小超过预期的3MB
就可能导致打开分页之后内核
crash在后面的试验中也的确出现了这种情况。解决方法同样简单就是拷贝更多的高地址项到低地址。
当执行完毕gdt\_init函数后新的段页式映射已经建立好了上面的0\~4MB的线性地址与0\~4MB的物理地址一一映射关系已经没有用了。所以可以通过如下语句解除这个老的映射关系。
```
boot_pgdir[0] = 0;
```
在page\_init函数建立完实现物理内存一一映射和页目录表自映射的页目录表和页表后一旦使能分页机制则ucore看到的内核虚拟地址空间如下图所示
![说明: proj5-vm-map](lab2/image008.png)
图7 使能分页机制后的虚拟地址空间图

BIN
lab2_figs/image001.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

BIN
lab2_figs/image002.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
lab2_figs/image003.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
lab2_figs/image004.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
lab2_figs/image005.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
lab2_figs/image006.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
lab2_figs/image007.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
lab2_figs/image008.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
lab2_figs/image009.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
lab2_figs/image010.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
lab2_figs/image011.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
lab2_figs/image012.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
lab2_figs/image013.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

150
phymemlab_concepts.md Normal file
View File

@ -0,0 +1,150 @@
**链接地址/虚地址/物理地址/加载地址以及edata/end/text的含义**
**链接脚本简介**
ucore
kernel各个部分由组成kernel的各个.o或.a文件构成且各个部分在内存中地址位置由ld工具根据kernel.ld链接脚本linker
script来设定。ld工具使用命令-T指定链接脚本。链接脚本主要用于规定如何把输入文件各个.o或.a文件内的section放入输出文件lab2/bin/kernel即ELF格式的ucore内核
并控制输出文件内各部分在程序地址空间内的布局。下面简单分析一下/lab2/tools/kernel.ld来了解一下ucore内核的地址布局情况。kernel.ld的内容如下所示
```
/* Simple linker script for the ucore kernel.
See the GNU ld 'info' manual ("info ld") to learn the syntax. */
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
OUTPUT_ARCH(i386)
ENTRY(kern_entry)
SECTIONS {
/* Load the kernel at this address: "." means the current address */
. = 0xC0100000;
.text : {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
PROVIDE(etext = .); /* Define the 'etext' symbol to this value */
.rodata : {
*(.rodata .rodata.* .gnu.linkonce.r.*)
}
/* Include debugging information in kernel memory */
.stab : {
PROVIDE(__STAB_BEGIN__ = .);
*(.stab);
PROVIDE(__STAB_END__ = .);
BYTE(0) /* Force the linker to allocate space
for this section */
}
.stabstr : {
PROVIDE(__STABSTR_BEGIN__ = .);
*(.stabstr);
PROVIDE(__STABSTR_END__ = .);
BYTE(0) /* Force the linker to allocate space
for this section */
}
/* Adjust the address for the data segment to the next page */
. = ALIGN(0x1000);
/* The data segment */
.data : {
*(.data)
}
PROVIDE(edata = .);
.bss : {
*(.bss)
}
PROVIDE(end = .);
/DISCARD/ : {
*(.eh_frame .note.GNU-stack)
}
}
```
其实从链接脚本的内容,可以大致猜出它指定告诉链接器的各种信息:
* 内核加载地址0xC0100000
* 入口(起始代码)地址: ENTRY(kern\_entry)
* cpu机器类型i386
其最主要的信息是告诉链接器各输入文件的各section应该怎么组合应该从哪个地址开始放各个section以什么顺序放分别怎么对齐等等最终组成输出文件的各section。除此之外linker
script还可以定义各种符号如.text、.data、.bss等形成最终生成的一堆符号的列表符号表每个符号包含了符号名字符号所引用的内存地址以及其他一些属性信息。符号实际上就是一个地址的符号表示其本身不占用的程序运行的内存空间。
**链接地址/加载地址/虚地址/物理地址**
ucore 设定了ucore运行中的虚地址空间具体设置可看
lab2/kern/mm/memlayout.h 中描述的"Virtual memory map
"图可以了解虚地址和物理地址的对应关系。lab2/tools/kernel.ld描述的是执行代码的链接地址link\_addr比如内核起始地址是0xC0100000这是一个虚地址。所以我们可以认为链接地址等于虚地址。在ucore建立内核页表时设定了物理地址和虚地址的虚实映射关系是
phy addr + 0xC0000000 = virtual addr
即虚地址和物理地址之间有一个偏移。但boot loader把ucore
kernel加载到内存时采用的是加载地址load
addr这是由于ucore还没有运行即还没有启动页表映射导致这时采用的寻址方式是段寻址方式用的是boot
loader在初始化阶段设置的段映射关系其映射关系可参看bootasm.S的末尾处有关段描述符表的内容
linear addr = phy addr = virtual addr
查看 bootloader的实现代码 bootmain::bootmain.c
readseg(ph-\>p\_va & 0xFFFFFF, ph-\>p\_memsz, ph-\>p\_offset);
这里的ph-\>p\_va=0xC0XXXXXX就是ld工具根据kernel.ld设置的链接地址且链接地址等于虚地址。考虑到ph-\>p\_va
& 0xFFFFFF == 0x0XXXXXX所以bootloader加载ucore
kernel的加载地址是0x0XXXXXX, 这实际上是ucore内核所在的物理地址。简言之
OS的链接地址link addr 在tools/kernel.ld中设置好了是一个虚地址virtual
addr而ucore kernel的加载地址load addr在boot
loader中的bootmain函数中指定是一个物理地址。
小结一下ucore内核的链接地址==ucore内核的虚拟地址boot
loader加载ucore内核用到的加载地址==ucore内核的物理地址。
**edata/end/text的含义**
在基于ELF执行文件格式的代码中存在一些对代码和数据的表述基本概念如下
* BSS段bss
segment指用来存放程序中未初始化的全局变量的内存区域。BSS是英文Block
Started by Symbol的简称。BSS段属于静态内存分配。
* 数据段data
segment指用来存放程序中已初始化的全局变量的一块内存区域。数据段属于静态内存分配。
* 代码段code segment/text
segment指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定并且内存区域通常属于只读,
某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
在lab2/kern/init/init.c的kern\_init函数中声明了外部全局变量
```
extern char edata[], end[];
```
但搜寻所有源码文件\*.[ch]没有发现有这两个变量的定义。那这两个变量从哪里来的呢其实在lab2/tools/kernel.ld中可以看到如下内容
```
.text : {
*(.text .stub .text.* .gnu.linkonce.t.*)
}
.data : {
*(.data)
}
PROVIDE(edata = .);
.bss : {
*(.bss)
}
PROVIDE(end = .);
```
这里的“.”表示当前地址,“.text”表示代码段起始地址“.data”也是一个地址可以看出它即代表了代码段的结束地址也是数据段的起始地址。类推下去“edata”表示数据段的结束地址“.bss”表示数据段的结束地址和BSS段的起始地址而“end”表示BSS段的结束地址。
这样回头看kerne\_init中的外部全局变量可知edata[]和
end[]这些变量是ld根据kernel.ld链接脚本生成的全局变量表示相应段的起始地址或结束地址等它们不在任何一个.S、.c或.h文件中定义。