作者:南京航空航天大学 金航
未经作者允许,禁止转载
- 能够加载用户程序
- 能够响应处理中断
- 能够进行文件读写
- 能够进行设备访问
- 能够跑仙剑奇侠传
- 运行效率还不够高:受限于模拟器本身
- 不能同时运行多道程序:实现分时多任务
- 独立存储空间(即位置无关的代码和数据)
- 进程分时切换(即各个进程轮流使用处理机)
方案1:位置无关代码PIC
即通过全局偏移量表
实现可以将程序加载到内存任意位置上
都可以正常运行,可惜Nanos-lite
无法实现,因为我们实现的loader
只能直接复制用户程序到指定位置,不具备读偏移量表的功能
,很遗憾。
- 我们仍选择始终将用户程序加载到同一个地址上,有人会说:我在运行仙剑,但是后又运行
hello
,那么hello
的代码就把仙剑的代码冲掉了啊? - 我们将该加载程序的地址称为虚拟地址空间,意味着程序认为他自己在这里,但是实际上在物理内存中,并不在这里,我们只需要维护好虚拟地址空间到真实物理地址之间的映射关系就可以了
- 这个过程是一个
软硬件
结合管理的机制,因为程序看到的虚拟地址往哪里映射由OS
维护,负责管理内存,决定映射的地址。而程序运行时的地址空间转换就需要由硬件
维护,因为OS
无权干涉指令运行。
我们并不实现分段,但是了解一下分段,即段寄存器中存段选择子,然后从GDT
表中找到段描述符,然后和偏移相加得线性地址
- 按需分配,以免造成地址空间浪费
- 粒度比分段小得多,便于组织管理
- 分页机制的地址转换图见讲义
- 注意对页表的
P
位是否可用位进行检验 - 对
R/W
位进行写权限检验 - 对
U/S
位进行访问权限建议(即拦截ring 3
试图访问ring 0
)
- 内存管理单元,属于硬件,用于虚拟地址到物理地址的转换
- 操作系统在加载程序时将程序相关的页表信息告诉
MMU
MMU
根据页表内容和映射关系进行地址转换MM
在Nanos-lite
的mm.c
中
注意:一定要注意观察和区分,哪些地方用到的是虚拟地址,哪些地方是物理地址。
- 需要添加
CR0
和CR3
两个寄存器,其中CR0
和CR3
的数据结构框架已帮我们定义好,在mmu.h
中,你只需要添加就可以,这个头文件很重要,很多地方都需要引用它。 - 注意初始化
CR0
寄存器 - 添加操作
CR0
和CR3
的相关指令 - 需要对
CR0
进行初始化
- 先验证
CR0
的paging
位,如果没有开启,仍如修改前一样调用paddr_read/write
直接传入虚拟地址获得/写入数据即可 - 先不着急实现数据跨越两个页的情况
- 开启了分页,于是调用
page_translate
函数得到转换好的物理地址
- 该函数用于地址转换,传入虚拟地址作为参数,函数返回值为物理地址
- 该函数的实现过程即为我们理论课学到的页级转换过程(先找页目录项,然后取出)
- 注意使用
assert
来验证present
位,否则会造成调试困难 PDE
和PTE
的数据结构框架已帮我们定义好,在mmu.h
中- 注意每个页目录想和每个页表项存储在内存中的地址均为物理地址,使用
paddr_read
去读取,如果使用vaddr_read
去读取会造成死递归(为什么?) - 此外,还需要实现访问位和脏位的功能
- 需要在
page_translate
中插入Log
并截图表示实现成功(截图后可去除Log以免影响性能) - 如何编写这个函数?
- 根据
CR3
寄存器得到页目录表基址(是个物理地址) - 用这个基址和从虚拟地址中隐含的
页目录
字段项结合计算出所需页目录项地址(是个物理地址) - 从内存中读出这个页目录项,并对有效位进行检验
- 将取出的
PDE
和虚拟地址的页表
字段相组合,得到所需页表项地址(是个物理地址) - 从内存中读出这个页表项,并对有效位进行检验
- 检验
PDE
的accessed
位,如果为0
则需变为1
,并写回到页目录项所在地址 - 检验
PTE
的accessed
位如果为0
,或者PTE
的脏位为0
且现在正在做写内存操作,满足这两个条件之一时需要将accessed
位,然后更新dirty
位,最后并写回到页表项所在地址 - 页级地址转换结束,返回转换结果(是个物理地址)
跨页的实现思路:
- 什么时候表示跨页了? 首先判断要读/写的长度和本页剩余的长度,若前者大则说明要读/写的数据跨页了
- 需要跨页时,首先读/写??的部分数据
- 然后计算新页的首地址,并将该地址转换为物理地址
- 对??的物理地址再做一次读/写操作
- 注意事项:如果是读跨页,则需组合两次读出的数据(位操作实现);如果是写跨页,应注意两个页分别需要写的数据长度是多少,以及写到两页上的内容分别是什么
- 启动时,内存中什么有效数据都没有,我们需要定义宏
HAS_PTE
,这样一来,Nanos-lite
就会在启动时初始化MM
(已实现)了 - 初始化
MM
的内容包括:设置空闲物理页的首地址,这样以后才可以通过调用new_page()
来分配新的空闲物理页;填写内核中的页目录和页表,通过AM
实现(已实现) - 注意:我们实现的是简易的分页机制,只分配就可以了,不需要回收
- 到目前为止,用户程序总是被加载到
0x04000000
的地址上 - 但是这个地址很低,且和内核的虚拟地址空间重合,内核可能在分配需要空间时破坏用户程序
- 我们既然有虚拟地址了,就不再受空间限制,加载到任意的虚拟地址都是可以接受的(前提是不和内核冲突),只要做好到物理地址的映射就行
- 修改链接地址参数
-Ttext
为0x8048000
,为什么是这个地址?(和工业界一个巧合) - 修改
loader
的加载地址为DEFAULT_ENTRY
,这样才能正确地将程序加载过去 - 修改
Nanos-lite
的main
函数,不直接使用loader
函数,而是调用load_proc
函数来加载用户程序
- 调用
_protect
创建用户进程的虚拟地址空间(其中用到的PCB
叫做进程描述块,用于描述进程状态,有兴趣做PA4.2
的同学到时候再了解),并将虚拟地址空间信息装入as
字段中,每个进程有属于自己的页目录基地址,存放在ptr
字段中 - 调用
loader
来加载程序 - 调用
_switch
切换到程序的虚拟地址空间 - 跳转到程序入口点
- 后面的操作和分时多任务有关,暂时不讲
该函数用于将一页虚拟内存页映射到物理内存中,建立映射关系
- 从虚拟地址空间中获得页目录基地址
- 根据该页目录基地址和传入的虚拟地址选中一个页目录项
- 如果需要新的页表,使用
palloc_f
申请一张页表,并将其地址组合其他标志位,存入本个页目录项中 - 根据传入的虚拟地址选中一个页表项
- 将传入的物理地址组合其他标志位,形成一个页表项,存入选中的页表项
对loader
做如下修改:
- 打开待装入的文件后,还需要获取文件大小
- 需要循环判断是否已创建足够的页来装入程序
- 对于程序需要的每一页,做三个事情,即4,5,6步:
- 使用
Nanos-lite
的MM
提供的new_page()
函数获取一个空闲物理页 - 使用映射函数
_map()
将本虚拟空间内当前正在处理的这个页和上一步申请到的空闲物理页建立映射 - 读一页内容,写到这个物理页上
- 每一页都处理完毕后,关闭文件,并返回程序入口点地址(虚拟地址)
作为你成功实现分页的依据,你需要在loader()
函数里面插入一个Log
,插入到每次调用_map()
函数前,通过Log()
显示出每次程序调用_map()
传入的第二个和第三个参数(va
和pa
),代码如下:
void *pa = ???;
void *va = ???;
Log("Map va to pa: 0x%08x to 0x%08x", va, pa);
_map(???, va, pa);
参考输出的va
和pa
每个人都可能会不同,但是总有一个正常范围,只要能正常运行dummy
的一定是在正常范围内的,你的报告中一定要出现这个截图才能证明你正确实现了本节的功能
- 我们之前总是认为
sys_brk
系统调用总是申请成功,因而总是返回0
- 现在,我们要通过分页来真正实现堆区管理,首先令
sys_brk
调用mm_brk
,并传入适当参数 MM
中,实现堆区管理的过程如下(填充即可):- 通过一个循环将从
current->max_brk
到new_brk
为止的部分,以页为单位分别申请一页空闲页并分别建立映射
PA4.1到此结束
本学期必做部分到此结束