Lab 3
在Lab 2 中,我大概理解了物理内存分页机制,以及物理内存页如何通过页目录页表和虚拟内存建立关系。虽然后面的Exercise 4,5和很多Challenge都没做完...(有空再做?),中间还有个Homework: xv6 system calls也没做...
现在正准备开Lab 3的新坑
隔了3个月,命令都忘记了...
不管了...先做着吧...
Lab 3:User-Level Environments
Introduction
在本实验室中,您将实现运行受保护的用户模式环境(即“进程”)所需的基本内核设施。您将增强JOS内核,以设置数据结构来跟踪用户环境、创建单个用户环境、将程序映像加载到其中并启动它。您还将使JOS内核能够处理用户环境发出的任何系统调用,并处理它引起的任何其他异常。
Getting Started
首先进行了一连串的git命令,主要是为了存档lab2 和 拉取lab3 新文件,为了防止我的文件被覆盖之类的。。要理解一下几个命令的意思。
athena% cd ~/6.828/lab
athena% add git //git add 命令将内容从工作区添加到暂存区(或称为索引区),用来确定下一次提交时快照的样子
athena% git commit -am 'changes to lab2 after handin' //将暂存区文件提交到本地版本库 git commit -m⽤于提交暂存区的⽂件;git commit -am⽤于提交跟踪过的⽂件
这行命令没什么风险,先执行了。、
执行add git没什么反应,这里直接执行 git add .
再执行第二句commmit命令,成功
athena% git pull //git pull命令执行过程:取回远程主机某个分支的更新,再与本地的指定分支合并(可能存在需手动解决的冲突)。
git pull 执行中可能存在需手动解决的冲突,需要对冲突进行处理,若果处理不当有可能会造成代码丢失。
执行时这句时候报错:
server certificate verification failed. CAfile: none CRLfile: none
可能是mit那个网站证书过期了,执行以下代码忽略验证证书即可:
git config --global http.sslverify false
git config --global https.sslverify false
再执行一次pull命令就行了,执行后显示Already up to date,即拉取成功,而且没有冲突需要处理(万幸,我对冲突处理并不熟悉~)
athena% git checkout -b lab3 origin/lab3 //创建并切换分支
athena% git merge lab2 //让当前分支lab3合并分支lab2的内容(只改变lab3)
执行以上第一个个命令后再执行 git branch 可以看到共有3个分支 lab1,2,3,当前处于lab 3分支。
执行第二个命令后可能会要你填写commit explain,直接ctrl+o保存,ctrl+x退出即可
Part A: User Environments and Exception Handling
新的文件inc/env.h包含了JOS中用户环境的基本定义,请查看。
内核使用env.h中的数据结构来追踪每个用户环境。
在刚开始Lab 3时,我们只需要创建一个环境,但是在后续实验中
我们需要设计JOS Kernel从而支持多种环境,这种设计将会在Lab 4中使用到。
在kern/env.c中可以看到,kernel维护了三个与环境相关的变量。
struct Env *envs = NULL; // All environments 所有环境
struct Env *curenv = NULL; // The current env 当前环境
static struct Env *env_free_list; // Free environment list 空闲环境列表
JOS启动并运行后,env指针指向一个代表系统中所有环境的Env结构数组。在我们的设计中,系统最多支持NENV(env.h中的变量)个环境同时运行(尽管实际上同时运行的环境数量比NENV少得多),envs数组包含了NENV个环境的env实例。
JOS内核将所有不活动的Env结构保存在env_free_list中。这种设计使环境的分配和再分配变得容易,因为它们只需要添加到空闲列表中或从空闲列表中删除即可。
currenv用于追踪当前运行中的环境,这个变量在第一个环境运行之前的值为NULL;
Environment State
让我们先来看看env.h中 env(单个环境)的数据结构
struct Env {
struct Trapframe env_tf; // 用于保存寄存器
struct Env *env_link; // 指向env_free_list中的下一个空闲的Env
envid_t env_id; // 该环境的标识
envid_t env_parent_id; // 创建该环境的环境的标识
enum EnvType env_type; // 环境类型,大多数是ENV_TYPE_USER(用户环境)
unsigned env_status; // Status of the environment
uint32_t env_runs; // 环境状态,包含以下几个变量:ENV_FREE,ENV_RUNNABLE,ENV_RUNNING,ENV_NOT_RUNNABLE,ENV_DYING
// Address space
pde_t *env_pgdir; // 这个变量保存这个环境的页目录的内核虚拟地址。
};
就像Unix中的进程一样,一个JOS环境中包含了“thread”和“address space”的概念。thread主要由寄存器定义(env_tf),address space由env_pgdir 指向的 page directory 和 page tables 所定义。
为了运行一个环境,kernel必须使用寄存器和合理的 address space 来设置CPU。
在JOS中,各个环境不像v6中的进程那样有自己的内核堆栈。在内核中,一次只能有一个活动的JOS环境,因此JOS只需要一个内核堆栈。
Allocating the Environments Array
在Lab 2中,我们为pges[]数组分配了内存,这个数组被kernel使用,从而kernel可以跟踪pages并知道那些pages是空闲,那些是不空闲的。
现在我们需要进一步修改mem_init来分配一个类似的Env 结构的数组,叫作envs。
Eercise 1
Creating and Running Environments
Exercise 2
Handling Interrupts and Exceptions
在Exercise 2 中,我们的系统在用户环境下运行到系统调用指令 int $0x30 的时候会挂掉。另一方台面,一旦我们的系统进入了用户环境,就不能回去了。
因此我们需要内核可以从用户模式代码中恢复对处理器的控制
应该做的第一件事是彻底熟悉x86中断和异常机制。
Exercise 3.
Read Chapter 9, Exceptions and Interrupts in the 80386 Programmer's Manual
(or Chapter 5 of the IA-32 Developer's Manual), if you haven't already.
Basics of Protected Control Transfer
人话:CPU执行指令都要经历取指令、解析指令、执行指令等环节构成的指令周期,其中一个环节就是检查特定寄存器都有没有中断标志。如果有,则保存当前环境现场,将当前环境切换为内核环境,检查中断向量表,执行对应的中断程序。
异常的发生和中断的调用都会导致“受保护的控制转换(protected control transfer)”,这会使处理器从用户模式切换到内核模式(CPL=0),而不会给用户模式代码任何机会干扰内核或其他环境。
为了保证这种控制转换是受保护的,处理器只能以某种特定的方式进入内核。在x86上,有两种机制共同提供这种保护:
1. The Interrupt Descriptor Table
processor 确保中断或者异常发生时,内核可以通过几个特定的入口被访问。这些入口一般由内核自己定义的。
x86允许多达256个不同的中断或异常进入内核,每个中断或异常都对应有不同的中断变量。
中断向量是由中断的来源决定的:不同的设备、错误条件和应用程序向内核的请求会生成具有不同向量的中断。
CPU使用这个向量作为进入 processor 中断描述符表(IDT)的索引(IDT是内核在内核私有内存中设置的,很像GDT)。
中断向量包括以下两个字段:
- EIP 寄存器的内容, 指向内核处理该异常的代码段
- CS 寄存器的内容, 该值的 01 两位表示运行异常处理程序的特权级别, 在 JOS 中,异常程序都是内核态,优先级也就是 0
2. The Task State Segment
在中断或者异常发生之前,The processor 需要一个地方去保存 the old processor state,例如 processor 调用异常处理程序之前,会先保存EIP和CS的原始值。因此在异常处理完毕之后,可以继续执行之前的代码。这个保存的位置在下面所说的内核栈当中。
当x86处理器接受中断或陷阱,导致特权级别从用户模式更改到内核模式时,栈也会切换到内核内存中的栈,一个叫做 Task State Segment(TSS) 的结构指定了这个栈的位置。
The processor 处理中断/异常时将SS, ESP, EFLAGS, CS, EIP 和一个错误代码 push 进内核栈中,然后从中断描述符加载CS和EIP,并设置ESP和SS指向新的堆栈。
尽管TSS很大并且有多种用途,JOS只使用它来定义处理器从用户模式转换到内核模式时应该切换到的内核堆栈。当进入内核模式时,处理器使用TSS的ESP0和SS0字段来定义内核堆栈。JOS不使用任何其他TSS字段。
Types of Exceptions and Interrupts
x86处理器内部可以生成的所有同步异常,都使用0到31之间的中断向量,同时映射到IDT条目0到31。例如,page fault 总是通过向量14引起异常。
大于31的中断向量只被软件中断或者异步硬件中断使用,软件中断可以由int指令产生,硬件中断由外部设备在需要被关注时引起。
在本节中,我们将扩展JOS,以处理中x86内部生成的向量0-31的异常。在实验4中,我们将扩展JOS来处理外部生成的硬件中断,比如时钟中断。
An Example
通过一个除零的操作来验证我们上面所说的东西。
-
处理器切换到由TSS的SS0和ESP0字段定义的堆栈,SS0和ESP0的对应的值分别是GD_KD和KSTACKTOP。
-
处理器从KSTACKTOP地址开始,将异常参数压入内核栈:
+--------------------+ KSTACKTOP | 0x00000 | old SS | " - 4 | old ESP | " - 8 | old EFLAGS | " - 12 | 0x00000 | old CS | " - 16 | old EIP | " - 20 <---- ESP +--------------------+ -
因为我们正在处理一个除法错误,它是x86上的中断向量0,处理器读取IDT条目0并设置CS:EIP指向该条目所描述的处理函数。
-
handler函数控制并处理异常,例如通过终止用户环境。
对于某些类型的x86异常,除了步骤二中“标准”的五个参数外,处理器还会把另一个包含错误代码的参数压入栈。页面错误异常(第14个)是一个重要的示例。请参阅80386手册,以确定处理器为哪个异常号推送错误代码,以及在这种情况下错误代码的含义。当处理器推送一个错误代码时,从用户模式进入的异常处理程序的堆栈将如下所示:
+--------------------+ KSTACKTOP
| 0x00000 | old SS | " - 4
| old ESP | " - 8
| old EFLAGS | " - 12
| 0x00000 | old CS | " - 16
| old EIP | " - 20
| error code | " - 24 <---- ESP
+--------------------+
Nested(嵌套) Exceptions and Interrupts
处理器可以从内核模式和用户模式接受异常和中断。然而,只有当从用户模式进入内核时,x86处理器才会在切换栈之前将旧的寄存器状态压入内核栈,并通过IDT调用适当的异常处理程序。
如果在中断或异常发生时,处理器已经处于内核模式(CS寄存器的低2位已经为零),那么CPU只会在相同的内核栈上压入更多的值。
通过这种方式,内核可以优雅地处理内核内部代码引起的嵌套异常。
这个功能是实现保护的一个重要工具,我们将在后面的系统调用一节中看到。
如果处理器已经处于内核模式并接受嵌套异常,因为它不需要交换栈,所以它不保存旧的SS或ESP寄存器。对于不需要 push 错误代码的异常类型,内核栈在异常处理程序的入口看起来像下面这样:
+--------------------+ <---- old ESP
| old EFLAGS | " - 4
| 0x00000 | old CS | " - 8
| old EIP | " - 12
+--------------------+
对于需要 push 错误代码的异常类型,处理器会像以前一样,在旧EIP之后立即 push 错误代码。
需要注意的是,如果处理器在已经处于内核模式时接受了一个异常,并且由于诸如缺乏堆栈空间之类的任何原因不能将其原来的状态推入内核堆栈,那么处理器就无法进行恢复,只能充值自己。内核的设计应该使这种情况不会发生。
Setting Up the IDT
现在,我们将设置IDT来处理中断向量0-31(处理器异常)。
头文件inc/trap.h和kern/trap.h包含与中断和异常相关的重要定义,我们需要熟悉这些定义。
文件kern/trap.h包含严格地对内核私有的定义,而inc/trap.h包含可能有用的user-level programs 和 libraries 的定义。
需要注意,英特尔定义的在0-31范围内的一些异常被保留了下来。因为处理器永远不会生成它们,所以如何处理它们并不重要。Do whatever you think is cleanest.
您应该实现的总体控制流程如下所示:
IDT trapentry.S trap.c
+----------------+
| &handler1 |---------> handler1: trap (struct Trapframe *tf)
| | // do stuff {
| | call trap // handle the exception/interrupt
| | // ... }
+----------------+
| &handler2 |--------> handler2:
| | // do stuff
| | call trap
| | // ...
+----------------+
.
.
.
+----------------+
| &handlerX |--------> handlerX:
| | // do stuff
| | call trap
| | // ...
+----------------+
每个中断或异常都需要在 trapentry.S 中有它自己的 hanlder,同时 trap_init() 需要在IDT中初始每个 hanlder 的地址。
每个 handler 都应该在栈上创建一个Trapframe结构体(参见inc/trap.h),并使用指向Trapframe的指针调用trap()(trap.c)。Trap()会处理异常/中断或者分派给一个特定的处理函数。
Exercise 4
修改 trapentry.S 和 trap.c 并实现上述所说的。

浙公网安备 33010602011771号