MIT 6.828 JOS/XV6 lab3–PARTA
Long long time ago, I finished Lab2. And now, let me face Lab3
目标:建立用户环境,可以追踪进程的运行情况,可以创建一个新的用户环境。也要完成系统调用和可能引发的异常
在这门课看来,环境和进程是可以对等的,都指程序运行期间的抽象。不直接叫进程是因为jos中实现的系统调用和UNIX是有差别的
描述环境的数据结构
首先是ID,定义一个新的类型,32位整型的,使用最低的10位来表示进程ID,也就是最多支持1024个进程/线程
进程的状态有以下几种
Env数据结构可以完整的描述一个进程
其中EnvType就一种
这个数据结构基本上保存了进程调度与运行所需要的一切,首先是寄存器的所有状态,进程ID, 父进程ID,进程类型和状态,以及非常重要的页表目录的地址
JOS的进程描述符还是相对简陋的,因为现在的JOS不支持多个进程,也不支持多核,在XV6中则有支持
进程描述符链表
进程描述符是在系统启动的时候全部生成好了的
将所有的进程描述符全部连成一个链表,这样可以省去很多新建和释放进程描述符的工作
Exercise1 初始化进程描述符表
这个和初始化内核区域的页描述符是非常类似的,有了前面的代码,这里也就好说多了
在创建了这部分数据结构之后,需要注意的是,这些描述符属于内核的数据结构了,需要将他们映射到特定的虚拟内存位置上,并且设定为用户可读的
创建于运行进程
由于现在还没有建立文件系统,所以说加载的可执行文件是嵌入到内核中的
在进行编译链接的时候,连接器直接把一个Elf文件链接进了内核的可执行文件中
Exercise2
-
Env_init()函数
前面已经为进程描述符表分配了内存空间,现在要初始化这些描述符,就是将所有的描述符的进程id置位0,状态置位free,然后依次的放入到空闲列表中,初始化的工作在循环开始前的memset就直接完成了
-
env_setup_vm()函数
就是要为当前的进程分配一个页,用来存放页表目录,同时将内核部分的内存的映射完成
所有的进程,不论是内核还是用户,在虚地址UTOP之上的内容都是一样的。
而对于内核而言,UTOP之上的映射已经在LAB2中完成了,如下所示
这个函数中完成了虚地址UPAGES和UVPT和内核栈的映射
再看一下内存图
其中的UVPT都是存放页表目录的地方,对于内核而言,是存放内核页表目录的地方,而对于用户是存放用户页表目录的地方
而UPAGES是存放内核页表的地方
所以对应了那句话:UTOP之上的映射都是一样的
下面看看实际函数是怎么实现的
首先申请一个物理页,然后将其作为页表目录的所在,注意虚地址和实地址的转换,因为在UTOP之上都是一样的,所以可以直接把kern_pgdir的内容全部拷贝过来
但是唯独UVPT这个地方是不一样的,因为要放的是自己的页表目录
Region_alloc函数
就是根据给定的也表目录,申请长度为len的内存,并将其映射到虚地址va上去
首先需要将va和len进行对齐,然后将申请到的页插入到页表目录中去
-
Load_icode函数
这个函数是希望将之前直接链接进内核的可执行二进制文件取出来执行
那么首先要做的事情,就是解析elf文件的文件头和各段的程序头,然后根据程序头文件中的信息,将指定字节的信息拷贝指定的虚地址处
但是这里需要注意的是:
这里的拷贝到指定的虚地址处,是指用户空间的虚地址,而不是内核空间的虚地址,所以还需要用lcr3函数加载用户空间的页表目录才能将地址转换为用户空间地址
而载入elf文件并开始执行的程序段在boot/main.c中有,可以参考那部分代码
这里的memsz和filesz不一致的原因,是因为在编译之后,elf文件中的bss段中存在一些没有被初始化的静态变量,这些变量不占用文件存储空间,但是在实际载入之后会占用内存空间
注意上面还需要初始化用户栈
-
Env_create函数
创建一个新的进程需要做的事情基本都在这里了
首先是申请一个进程描述符,调用前面的env_setup_vm函数来完成页表目录的设置和内核区域的映射,然后将制定的二进制文件载入到内存中来
先看env_alloc函数
然后就是env_create函数
-
接下来,就是真正切换进程的函数:env_run
最后一句env_pop_tf函数,就是将当前进程的trapframe通过弹栈的形式,切换当前的运行环境
前面这一部分就是进程创建的整个流程,可以参考下面的调用关系图
但是现在这个系统还是跑不起来,因为还没有异常处理机制
当系统启动完成,会加载链接到内部的那个程序,然后执行,但是因为这个程序内部会调用int指令,所以会产生中断,但是还没有建立任何允许从用户空间到内核空间的方式,所以会产生一次保护异常,然后发现保护异常也处理不了,然后又发生了一次异常,直到发生了三次保护异常,CPU就会重置
X86架构下基本的保护机制
为了能够让在中断或者异常发生的时候,当前运行的代码不会随机的选择如何进入内核或者怎样进入内核,而是由内核精细的控制的,基本上基于以下两种方式
- 中断描述符表
- 任务状态段
指的是用来暂时存储引发中断或者异常的进程的状态。
当发生了终端或者异常的时候,如果发生了从用户态到内核态的转换,也需要切换运行栈。那么任务状态段(task state segment)指明了这个栈的段选择子和地址。处理器会将各种寄存器和错误码都压入到新的栈中。然后从中断描述符中加载CS EIP等,然后让ESP和SS指向新的栈
Exercise4
-
Edit trapentry.S
在ucore中,入口向量是直接生成的,就是把全部的256项都列出来,但是jos不是这么做的,在trapentry.S内先定义了两个宏,然后利用这两个宏来定义相应中断的入口
给定一个全局函数名和中断号,然后所做的就是和ucore中类似的了,如果需要压错误码的话,由CPU来完成这件事,但是如果没有错误码的话,就压入一个0,这样可以保证结构体trapframe得结构保持一致
下面是利用上面给定的宏定义的针对特定中断的处理例程
所有的中断,都是要先压入错误码,然后压入中断号,接下来的事都是一样的了,就是继续压栈,在栈上形成一个结构体,以esp为指针
这里的trapframe的定义与ucore中稍有不同
trapframe中少了fs和gs
然后就是在trap.c中初始化中断向量表了
注意断点和系统调用的权限设定,SETGATE的最后一个参数表示的是引发该中断需要的特权级,很明显,系统调用和断点是可以在用户态下引发的,而其他的则是因为错误而陷入到了内核中