BUAA-OS lab3实验报告,关于mos系统的进程模式的实现以及进程时间片调度的实现。
lab3_log
实验要求
- 创建一个进程并成功运行
- 实现时钟中断
- 实现两个进程之间的调度
相关文件解读
- include/trap.h: 定义了Trapframe结构体,用于在进程调度时保存当前进程上下文环境
- include/env.h: 定义了实现PCB的Env结构体(这个很重要,包含了一个进程所需要的所有的信息),与一系列进程相关的函数
思考题
Thinking 3.1
Thinking 3.1 请结合 MOS 中的页目录自映射应用解释代码中 e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_V 的含义。
- 其中UVPT的起始地址是页表项的起始虚拟地址,PDX获得的是这个虚拟地址的页目录号。
- 这个页目录项就是指向页目录自身的页目录项,我们通过PADDR(e->env_pgdir)来获取页目录(这个页目录就是页表中的一页)的物理地址。
- 自映射的意义即是我只需要知道二级页表的初始地址,我就能构造出页目录的初始地址以及指向页目录自身的PDE,这样,方便我们访问管理页表与页目录。
Thinking 3.2
Thinking 3.2 elf_load_seg 以函数指针的形式,接受外部自定义的回调函数 map_page。请你找到与之相关的 data 这一参数在此处的来源,并思考它的作用。没有这个参数可不可以?为什么?
- elf_load_seg函数再load_icode函数中被调用,我们可以看到注释中写道它在lib/elfloader.c中被定义,我们可以看到它的最后一个参数data被传入load_icode的形参e,然后最后在load_icode_mapper里又被转换为了Env*,并将src写的页面插入了env的页表中。
- data参数实现了重定位,实现了解耦合,可以实现函数的复用与拓展
Thinking 3.3
Thinking 3.3 结合 elf_load_seg 的参数和实现,考虑该函数需要处理哪些页面加载的情况。
- elf_load_seg的作用是将elf文件的段加载到内存,我们看它传入了一个文件头的参数,与二进制文件(数组)的参数,以及一个回调方法和参数。通过这个文件头读取这个文件的信息,然后通过回调函数把二进制文件写入data的页中。
Thinking 3.4
Thinking 3.4 思考上面这一段话,并根据自己在 Lab2 中的理解,回答:
• 你认为这里的 env_tf.cp0_epc 存储的是物理地址还是虚拟地址?
- mips是冯诺依曼体系结构,指令和数据不分别存储,所以指令也在内存里,也遵循内存管理的虚拟地址结构,所以cp0中应该是虚拟地址。
Thinking 3.5
Thinking 3.5 试找出 0、1、2、3 号异常处理函数的具体实现位置。8 号异常(系统调用)涉及的 do_syscall() 函数将在 Lab4 中实现。
- 通过查找功能找到
- handle_int在kern/genex.S中有个显式的函数中在处理
- 而其他三个都是通过宏定义重复调用实现的。在kern/genex.S找到了下面语句:
1 | #if !defined(LAB) || LAB >= 4 |
Thinking 3.6
Thinking 3.6 阅读 init.c、kclock.S、env_asm.S 和 genex.S 这几个文件,并尝试说出enable_irq 和 timer_irq 中每行汇编代码的作用。
1 | LEAF(enable_irq) |
- 上面是enable_irq,作用是开启中断。接下来是由kern/entry.S中的函数获得中断函数的入口。
- 开启中断的方式就是写CP0寄存器
1 | timer_irq: |
- timer_irq,作用是跳转到schedule中,执行调度。
- 具体的实现是给一个地址写入了一个0,大致的意思应该是立即执行
- KSEG1 | DEV_RTC_ADDRESS 是模拟器(GXemul)映射实时钟的位置
- INTERRUPT_ACK 意思是中断执行。
- 然后给传入了一个参数,a0=0
- 看到这个参数对应schedule函数中的yield,也就是说不使用yield指定强制切换。
Thinking 3.7
Thinking 3.7 阅读相关代码,思考操作系统是怎么根据时钟中断切换进程的。
- mos操作系统发生时钟中断的时候就会最终调用一次schedule函数。
- 而这个函数会把env_sched_list中的进程循环执行。
- 每次调用减少当前进程的一个时间片。
- 减到0,就把这个进程放到队尾,取出队首的进程执行。
Exercise3.1-3.2
- 两个队列:env_free_list, env_sched_list;一个用到lab2的LIST结构,另一个用到TAILQ的双端队列结构。相关宏都定义在include/queque.h中
- 实现kern/env.c中的env_init函数:
- 首先我们看到了一个东西
__attribute__((aligned(BY2PG)))
- 然后就是两步走
- 初始化上述两个队列,注意LIST_INIT,TAILQ_INIT是传地址进去的。
- 然后反着来,将序号从大到小插到env_free_list的表头。
- 首先我们看到了一个东西
- 然后实现map_segment函数,实现段地址映射的函数,将一段连续的物理页面,映射到一段连续的虚拟地址。
- 只需要补一句,就是把传入的参数中的pa与va每次同时增加BY2PG的大小的地址通过page_insert函数建立起联系。
- 但是要注意page_insert函数需要传入page*,所以要用到pa2page。
- 模板页目录base_pgdir的作用:各个进程的页目录都从这里取项,就不需要占用多个页的孔吉纳来构造多个进程。
Exercises3.3-3.4
- 设置进程控制块PCB:从env_free_list中取出一块,并完成设置。(在env_alloc中完成)
- 其中设置过程需要为进程初始化配置用户地址空间,需要调用env_setup_vm来完成。
- 理解进程地址空间的分布:4G的虚拟地址空间,ULIM(0x8000_0000)往上是内核,往下是kuseg。然后ULIM~UVPT(user virtual page table)是用户进程的页表,UVPT~UTOP(user top)是所有进程共享的只读空间。再往下就是用户的可读写空间了。
- 再来完成env_alloc函数
- 每个进程需要有自己的Trapframe(env_tf)来存放当时的进程的上下文,而我们看Trapframe结构体(include/trap.h中)就能够知道,内容就是我们看到的mipsR3000处理器中的一些寄存器,包括32个通用寄存器,与cp0中的几个寄存器(其中SR寄存器(status register)代表了CPU的运行状态)。
- asid_alloc函数的用法,阅读源代码,是要传入一个指针,asid写在指针指向的地址里。
- alloc中没有页面了要返回-E_NO_FREE_ENV(教程不讲!!!!)
- asid分配不上了要返回错误,所以需要用r来存下返回值。
Exercise3.5-3.8
- 这几步主要是加载可执行文件,创建进程并执行
- 首先加载二进制镜像:
- 给出了四个函数,四个函数的调用关系是:load_icode调用elf_from与elf_load_seg两个函数,elf_load_seg调用load_icode_mapper。
- 要完成load_icode_mapper(这个函数完成单个页面的加载过程,由elf_load_seg函数调用。
- 注意page转换成虚拟地址。
- 然后就是env_creat函数创建进程:
- 这是内核初始化直接创建,不是通过fork
- 创建需要先分配一个新的Env结构体,然后设置控制块,再将程序装载即可
- 最后可以进行进程的创建与切换了。
- 通过env_run函数,从现在的进程切换至传入的e进程。(进程上下文就是进程执行时所有寄存器的状态。)
- 根据切换运行进程的流程编写相应的代码。
- 保存进程上下文
- 切换PCB(curenv)
- 切换页表
- tf出栈,恢复现场
这里的实现过程其实也体现了调用关系,先实现的被后实现的调用
Exercise3.9-3.12
- kern/entry.S函数的理解。
- 保存当前上下文。
- 以异常码为索引index从数组中,取得函数的入口地址,然后跳转。
- 通过kernel.lds来链接CPU异常与TLBmiss异常的处理程序入口。
- kclock_init初始化时钟
- 在一个地址处写入时钟周期。
- 这里主要弄清楚在时钟中断的过程中发生了一些什么调用
- 教程是以首次时钟中断为例,mips_init函数中,在用kclock_init完成时钟初始化之后,调用enable_irq开启中断。然后就是由上面提到的kern/entry.S中的函数获得中断函数的入口。
- 中断处理函数是handle_int,在判断完能够中断后,调用timer_irq执行中断处理。
- 而这个处理函数的作用就是跳转到schedule中,执行调度。
- 实现schedule函数,该函数的作用是:
- 关于static int count;
- 一次声明重复使用。
- schedule中使用时间片轮转的方式,但是优先级高的进程的时间片长。
- 关于static int count;
实验体会
- 这一章里实现了创建进程到运行到调度。
- 让我更加理解了进程的本质,与调度中需要做的一些事情。
- 感觉确实难度比lab2稍低一些,因为各个函数的调用关系比较的紧密且明朗。前后需要做的工作也比较的有条理。
- 理解了整体每个函数的作用与关系,了解了整个工程的层次结构,其实具体的实现还是一件比较次要的事情。