lab3
实验目的:
- 创建一个进程,并成功运行
- 实现时钟中断,通过时钟中断内核获得执行权
- 实现进程调度,创建两个进程,通过时钟中断切换进程执行
使用进程控制卡Env来跟踪用户进程,并建立一个简单的用户进程,加载一个程序镜像到指定的内存空间,并让它运行起来。
进程
- 由于没有实现线程,进程既是基本的分配单元,也是基本的执行单元。
- 每个进程都是一个实体,有自己的地址空间,通常包括代码段、数据段和堆栈。
- 程序是一个没有声明的实体,只有被操作系统执行时才能称为一个活动的实体,而执行中的程序,就是进程
PCB进程控制块
本质就是一个数据结构
env_init
traverse 是遍历的意思
段地址映射
为base_pgdir
分配了一页物理内存,将其转换为内核虚拟地址,并使用map_segment
来进行映射。
pages
映射到UPAGES
envs
映射到UENVS
其中补全的部分我是这么写的
struct Page* pp = pa2page(pa + i);
page_insert(pgdir, asid, pp, va + i, perm);
其实可以把pp直接替换为中间那一部分。
注意是va+i
和pa+i
进程的标识
就是
env_id
这里用自己的话解释一下为什么还需要一个asid
实际上是为了让每一个进程有一个自己的tlb
,这样当这个进程结束时(或者进程切换的时候)只需要把这个进程的tlb
刷新掉即可。
既然需要有一个自己的tlb
那就得有一个标识符,那为什么不直接用env_id
呢,我认为主要是为了让tlb
只有有限个,太多了就炸内存了,这波属于是宁愿少点,不能多了。
创建进程
终于开始创建进程了
实际上创建进程在不考虑进程的交互性(也就是说这是个活的实体),实际上就是个数据结构,那么相应的数据结构就是PCB
进程控制块。
因此也可以理解为设置一个进程控制块。
- 设置进程控制块实际上是告诉了我们这个进程是什么
- 那么进程还要有自己的地址空间,包括代码段、数据段和堆栈
OK,这里开始解释后面的进程调度和中断的开启。
在MIPS处理器中,中断是由硬件控制的,也就是CP0
寄存器中保存着我们需要的信息。
那么具体是如何设置的呢?
这里我们考虑的相对简单,还只开启了时钟中断。
这里就是需要将
IM7
设置为1,表示时钟中断可以被响应,同时还有IE
位,也就是Interupt Enable
中断使能位要打开才能正确的中断
还有第二个问题,中断需要调度进程,同时需要进入内核态。
害,理论是真的很重要!!!
用户态的标志:EXL=0, UM=1
其他都是内核态
那么如果我们刚开始在内核态接受到了时钟中断,中断结束后我们还应当是在内核态,但是由于
eret
这个命令在所有进程调度的最后都会被执行,同时eret
会进EXL
设置为0(也就是希望返回到用户态),因此我们还需要手动维护一下EXL
使得保持在内核态。
elf简单复习一下
ELF文件结构为:
- ELF头,包括程序的基本信息,比如体系结构和操作系统,同时也把包括了节头表和段头表相对文件的偏移量offset
- 段头表,program header table ,主要包含程序中各个段segment的信息,段的信息需要在运行时刻使用
- 节头表,section header table,主要包括程序中各个节section的信息,节的信息需要在程序编译和链接的时候使用
加载二进制文件
ELF文件的类型有三种:可重定位文件,可执行文件,可被共享的对象文件。
这里指可执行文件
要加载一个ELF文件到内存,只需要将其中所有需要加载的program segment程序段到对应的虚拟地址上即可。(这里我认为主要是因为这里是可执行文件,所以只需要将各个需要在运行时刻使用的信息存放过去即可)
目前还是在内核态,alloc
的一个页面是一个物理页面。
要得到其物理地址,由于是内核态,因此需要调用page2kva
来得到物理地址。
这一点从page_alloc()
中的memset((void*)page2kva(pp),0,PAGE_SIZE);
可以初见端倪。
而且本身也非常合理。
给进程分配地址空间,实际上是将可执行文件的信息进行复制。
这里用到了一个回调函数,还算比较好理解。
load_icode
函数最后需要将env_tf.cp0_epc
设置为e_entry
,这个是说当执行进程时还是需要从最开始这个可执行文件的开始位置开始执行。
创建进程
进程控制块也设置好了,地址空间也安排好了,终于可以创建进程了。
这里的创建进程是在内核初始化时直接创建进程。
- 分配一个新的Env结构体
- 设置进程控制块
- 将程序载入到进程的地址空间
这里可以修改为
if (env_alloc(&e,0) < 0) return NULL;
进程运行与切换
要运行一个进程:
- 需要保存当前进程的上下文(如果当前没有进程就pass)
- 恢复要启动的进程的上下文,然后运行该进程
进程的上下文朴素的理解就是当时运行这个进程时硬件的情况,也就是各个寄存器的状态,也就是Trapframe
的内容
包括
- 通用寄存器
HI
、LO
- CP0的
Status
、EPC
、Cause
和BadVAddr
寄存器
在这里寄存器状态保存的地方是KSTACKTOP
以下的大小为TrapFrame
的区域
运行一个进程:
- 保存当前进程的上下文信息(没有就开摆)
- 切换
curenv
为即将运行的进程(这里还没有切换) - 设置全局变量
cur_pgdir
为当前进程的页目录地址,在TLB
重填是将用到这个全局变量 - 调用
env_top_tf
函数,恢复现场(就是即将运行的进程的现场/上下文信息)、异常返回
中断与异常
CP0寄存器
- Status 状态寄存器,包括中断引脚使能,其他CPU模式等位域
- Cause 记录导致异常的原因
- EPC 异常结束后程序恢复执行的位置
处理异常是由硬件完成的,MIPS CPU处理一个异常的步骤包括
- 设置EPC指向从异常返回的地址
- 设置EXL位,强制CPU进入内核态,并禁止中断
- 设置Cause寄存器,记录异常发生的原因
- CPU从异常入口位置取指,此后交给软件处理,也即是我们的操作系统来处理
梳理
这里再把整个流程进行梳理
进程创建
几个问题
-
tests/lab3_*
这几个里面的pre_env_run.c
是干嘛用的,tests/lab3_4
还有个quick_sort.c
是干嘛的第一直觉感觉是创建一个进程,但是仔细一看还是不太清楚