Thinking3.1

我们用两部分标识一个进程,分别为ASID和其对应进程控制块在进程数组中的位置,使用e = envs + ENVX(envid)并未对ASID部分进行检查,可能出现“前朝的剑斩本朝的官”的问题。

Thinking3.2

UTOP是用户可读写的地址空间,ULIM是用户可见但不可写的地址空间,UTOP与ULIM之间的空间与UTOP之下的空间最大区别就在于可写性,pgdir[PDX(UVPT)]=env_cr3,UVPT是页表项的自映射项,其应当保存页表的物理地址,并修改低12位中的权限位。

进程中直接操作的都是虚拟地址,而真正的读写要经过我们的软件mmu转化。

Thinking3.3

user_date是从外层函数向内层函数传递参数所用的变量,其在本实验中用于传递Env块

例如qsort,就相当于qsort(user_date,int *map(user_date));

Thinking3.4

头不对齐,尾不对齐、头对齐,尾不对齐、头不对齐,尾对齐、头尾都对齐,总体来说,需要不多写也不少写,下文做出了图示

Thinking3.5

env_tf.pc 存储的是虚拟地址,是程序运行地址;一样,其都是从elf文件中的入口复制而来,这种统一本质上反映了不同进程所见的内存空间的同一性。

Thinking3.6

epc是程序异常处理后应当返回的地址;对于此进程,我们转到其他进程的过程相当于一个异常,需要将pc置为异常结束之后应当去的地址,才能保证其正确执行

Thinking3.7

在include/stackframe.h中我们可以看到如下代码

.macro RESTORE_ALL_AND_RETRESTORE_SOMElw  k0,TF_EPC(sp)lw  sp,TF_REG29(sp)  /* Deallocate stack */jr  k0rfe
.endm

此处的RESTORE_SOME也是实现在同一文件中的函数,节选一下

 lw  $31,TF_REG31(sp)lw  $30,TF_REG30(sp)lw  $28,TF_REG28(sp)lw  $25,TF_REG25(sp)lw  $24,TF_REG24(sp)lw  $23,TF_REG23(sp)lw  $22,TF_REG22(sp)lw  $21,TF_REG21(sp)lw  $20,TF_REG20(sp)lw  $19,TF_REG19(sp)lw  $18,TF_REG18(sp)lw  $17,TF_REG17(sp)lw  $16,TF_REG16(sp)lw  $15,TF_REG15(sp)lw  $14,TF_REG14(sp)

可以看到,在RESTORE_SOME中,我们向通过sp寄存器寻址所得的地址中写入了当前寄存器的值,而sp寄存器为栈指针,其所指向的地址在get_sp中设置

.macro get_spmfc0    k1, CP0_CAUSEandi    k1, 0x107Cxori    k1, 0x1000bnez    k1, 1fnopli  sp, 0x82000000 //TIMESSTACKj   2fnop
1:bltz    sp, 2fnoplw  sp, KERNEL_SPnop2:  nop

不难看出,get_sp根据中断原因,修改sp的值,将其分别指向用户寄存器所在地址和内核寄存器所在地址,对应内核中断与时钟中断。此处的KERNEL_SP与我们的TIMESSTACK的区别就在于此,而当其指向时钟中断时,就会向我们的TIMESTACK中写入寄存器的值。

然后看一下这里的汇编函数都在干什么

RESTORE_SOME设置了TIMESTAVK中除了k0 k1 sp之外的寄存器,在RESTORE_ALL_AND_RET中,将其进行补充设置,然后jr k0,rfe,rfe的作用在实验指导书中已经说明,而jr k0是为了跳转到正确的pc去,因为rfe并不进行对pc的修改,只进行对虚拟空间的切换,这一部分对应异常结束之后的恢复部分。

Thinking3.9
LEAF(set_timer)li t0, 0xc8   //写入立即数 200sb t0, 0xb5000100    //将立即数加载至0xb5000100的低16位,即设置时钟每秒中断200次sw  sp, KERNEL_SP  //将sp存到kernel_sp(
setup_c0_status STATUS_CU0|0x1001 0 //设置cp0状态,这个宏就定义在此函数上方//cp0会变成 0x10001001,28位是1表示允许在用户模式下用cp0//12位是1表示可以响应四号中断//低两位为01,表示当前处于用户态且中断开启jr ra    //跳回nop
END(set_timer)timer_irq:sb zero, 0xb5000110 //禁止时钟中断
1:  j   sched_yield //跳转到调度函数,决定下一个时间片运行哪个进程nop/*li t1, 0xfflw    t0, delayaddu  t0, 1sw  t0, delaybeq t0,t1,1f    nop*/j   ret_from_exception   //这个宏定义在lib/genex.S//会跳转到epc去,没看太懂v.vnop
Thinking3.10

我们在lab3-1中已经实现了不考虑时钟中断的进程调用。

在lab3-2中,我们补充了时钟中断和内核函数调用,时钟中断是由cpu发出的中断。

我们设置cpu每秒的中断次数,然后设置每个进程能占用的时间块

当一个进程的时间块用完了,它就必须暂停,让cpu去处理别的内容

从而实现进程切换。

实验重难点

Env块的结构
struct Env {struct Trapframe env_tf;       // Saved registers LIST_ENTRY(Env) env_link;      // Free LIST_ENTRY u_int env_id;                  // Unique environment identifier u_int env_parent_id;           // env_id of this env's parent u_int env_status;              // Status of the environment Pde *env_pgdir;                // Kernel virtual address of page dir u_int env_cr3; LIST_ENTRY(Env) env_sched_link; u_int env_pri;
};
struct Trapframe { //lr:need to be modified(reference to linux pt_regs) TODO/* Saved main processor registers. */unsigned long regs[32]; //32位寄存器/* Saved special registers. */unsigned long cp0_status; // 状态寄存器unsigned long hi;   // unsigned long lo;unsigned long cp0_badvaddr; //异常地址unsigned long cp0_cause;//错误原因unsigned long cp0_epc;//异常处理返回地址unsigned long pc;
};

我们可以看到,每个Env块都有自己“独立”的32位寄存器和cpo寄存器,这是我们的程序进行异常恢复的关键,利用这些寄存器,我们就可以在遇到异常时,保存上下文。

其中需要注意的有 env_id与env_status

env_id的初始化

u_int mkenvid(struct Env *e) {u_int idx = e - envs;u_int asid = asid_alloc();return (asid << (1 + LOG2NENV)) | (1 << LOG2NENV) | idx; //the low bits is env number
}

低十位是Env块在Env数组中的位置,可以使用 ENVX宏获得,中间六位是ASID,可以使用GET_ENV_ASID宏获得

然后3.1和3.2都比较简单

3.3要求我们实现从它的envid得到对应进程块,但这个函数实际还附带了两个功能

1.传入envid为0,返回当前进程

2.传入checkprem为1,检查进程的父子关系(只能检查直接父子关系,很鸡肋

地址空间

需要完整地认识地址空间以及进程对地址空间的映射关系,不然cvlogin都不知道他在干嘛0.0

 o                      +----------------------------+----|-------Physics Memory Maxo                      |       ...                  |  kseg0o  VPT,KSTACKTOP-----> +----------------------------+----|-------0x8040 0000-------endo                      |       Kernel Stack         |    | KSTKSIZE            /|\o                      +----------------------------+----|------                |o                      |       Kernel Text          |    |                    PDMAPo      KERNBASE -----> +----------------------------+----|-------0x8001 0000    | o                      |   Interrupts & Exception   |   \|/                    \|/o      ULIM     -----> +----------------------------+------------0x8000 0000-------    o                      |         User VPT           |     PDMAP                /|\ o      UVPT     -----> +----------------------------+------------0x7fc0 0000    |o                      |         PAGES              |     PDMAP                 |o      UPAGES   -----> +----------------------------+------------0x7f80 0000    |o                      |         ENVS               |     PDMAP                 |o  UTOP,UENVS   -----> +----------------------------+------------0x7f40 0000    |o  UXSTACKTOP -/       |     user exception stack   |     BY2PG                 |o                      +----------------------------+------------0x7f3f f000    |o                      |       Invalid memory       |     BY2PG                 |o      USTACKTOP ----> +----------------------------+------------0x7f3f e000    |o                      |     normal user stack      |     BY2PG                 |o                      +----------------------------+------------0x7f3f d000    |

在思考题中已经有了关于UTOP与ULIM的具体意义,补充一下别的用到的东西

USTACKTOP 用户栈

UXSTACKTOP 用户异常栈(其实没用,但是我debug很久,也没有发现它不是真的用户栈

KSTACKTOP 内核栈

UTOP与ULIM之间的部分是env数组、page数组还有页目录,这一部分都属于内核,是指导书中提到的“内核暴露给用户”的部分,需要从内核中复制

boot_pgdir作为内核页表,我们在初始化的时候,就以及将对应内容填充到UTOP与ULIM之间了,所以用pgdir[i] = boot_pgdir[i]就行。

然后就可以写env_setup_vm函数了

static int
env_setup_vm(struct Env *e)
{int i, r;struct Page *p = NULL;Pde *pgdir;if ((r = page_alloc(&p)) < 0) {     panic("env_setup_vm - page alloc error\n");return r;}     //没空间就警告一下p->pp_ref++; pgdir = (Pde *)page2kva(p);for(i = 0;i < PDX(UTOP);i++) { //用户空间都还没映射,全是0就行pgdir[i] = 0;}for(i = PDX(UTOP);i < PDX(ULIM);i++) {    //从内核里直接抄pgdir[i] = boot_pgdir[i];}for(i = PDX(ULIM);i < 1024;i++) { //没抄满,不行,强迫症pgdir[i] = boot_pgdir[i];}e->env_pgdir = pgdir;e->env_cr3 = PADDR(pgdir);e->env_pgdir[PDX(VPT)] = e->env_cr3; //内核也有一个VPT存当前进程的页表,反正抄出来了,设置一下e->env_pgdir[PDX(UVPT)]  = e->env_cr3 | PTE_V; return 0;
}

现在我们可以为进程开辟它自己的进程控制块了,但它只是一个花架子,因为它还没能与实际地址建立联系

所以在env_alloc中我们要为它分配空间

int
env_alloc(struct Env **new, u_int parent_id)
{int r;struct Env *e;if(LIST_EMPTY(&env_free_list)) {*new = NULL;return -E_NO_FREE_ENV;}e = LIST_FIRST(&env_free_list); //拿一块没用过的env_setup_vm(e); //为e设置虚拟内存e->env_id = mkenvid(e);   //填写必要信息e->env_status = ENV_RUNNABLE;   e->env_parent_id = parent_id;//e->env_runs = 0;/* Step 4: Focus on initializing the sp register and cp0_status of env_tf field, located at this new Env. */e->env_tf.cp0_status = 0x10001004;e->env_tf.regs[29] = USTACKTOP; //当然我们从注释也能看出,第四步除了需要设置cp0status以外,还需要设置栈指针。在MIPS 中,栈寄存器是第29 号寄存器,注意这里的栈是用户栈,不是内核栈。LIST_REMOVE(e,env_link);LIST_INSERT_HEAD(&env_sched_list[0], e, env_sched_link);*new = e;return 0;}
加载程序

这块很碎,实验中的设计是:加载一个段->加载所有段->加载整个程序

先来看加载一个段的部分

重点在于,不多写一字节,不少写一字节,只要我们保证自己的函数做到这一点,就是对的。

bin_size开头,如果页对齐,皆大欢喜,直接写即可,但是如果不对齐,我们需要先判断这一页是不是已经申请过了

bin_size结尾,页不对齐,不能多写,可能造成覆盖

sgsize同理。

sgsize部分置0是因为对应的.bss保存未初始化的全局变量,其默认值就是0(奇怪的实现增加了

然后是load_elf,本质需要我们从elf文件中拿出刚刚写的函数所需的东西,需要我们复习ELF文件的内容,做完课后习题就会好很多

最后是load_icode,我们在这里除了需要将程序加载至内存,还需要为程序申请它自己的栈。

{struct Page *p = NULL;u_long entry_point;u_long r;u_long perm;/* Step 1: alloc a page. */r = page_alloc(&p); //this is user normal stackif(r != 0) return ;/* Step 2: Use appropriate perm to set initial stack for new Env. *//* Hint: Should the user-stack be writable? */r = page_insert(e->env_pgdir,p,USTACKTOP - BY2PG,PTE_R); //为虚拟地址建立映射,其应当映射到之前内存图中标出的栈的位置if(r != 0) return ;/* Step 3: load the binary using elf loader. */load_elf(binary,size,&entry_point,(void *)e,load_icode_mapper);/* Step 4: Set CPU's PC register as appropriate value. */e->env_tf.pc = entry_point;
}

创建进程基本直接调用该函数,只是额外添加了优先级

void
env_create_priority(u_char *binary, int size, int priority)
{struct Env *e;int r;r = env_alloc(&e,0);if(r != 0) return r;e->env_pri = priority;load_icode(e,binary,size);
}
进程运行

至此我们都只关心一个进程,也确实实现了让一个进程跑起来的部分,但还需要让两个进程能互相切换。

void
env_run(struct Env *e)
{/* Step 1: save register state of curenv. */
​/* Step 2: Set 'curenv' to the new environment. */
​/* Step 3: Use lcontext() to switch to its address space. */
​/* Step 4: Use env_pop_tf() to restore the environment's*   environment registers and return to user mode.* * Hint: You should use GET_ENV_ASID there. Think why?*   (read <see mips run linux>, page 135-144)*/if(curenv){struct Trapframe *old;old = (struct Trapframp *)(TIMESTACK - sizeof(struct Trapframe));bcopy(old, &(curenv->env_tf), sizeof(struct Trapframe));curenv->env_tf.pc = curenv->env_tf.cp0_epc;}curenv = e; curenv->env_runs++;lcontext(curenv->env_pgdir);env_pop_tf(&(e->env_tf), GET_ENV_ASID(e->env_id));
}

第一步保存寄存器,我们将当前寄存器的值保存到进程控制块,设置pc,在思考题中都已经考虑过了

第三步是一个汇编函数,我们只要知道传入的参数将被保存至a0-a4,就很容易理解其含义

第四步是比较难的一点

第一步传参不难理解,因为我们需要修改其中寄存器的值,需要以基地址出发,进行相对寻址

第二个参数与之前的TLB有关,我们判断一个表在不在TLB中时,需要将其ASID与存储在CP0_ENTRYHI中的ASID进行比较,相同才说明页表项有效(因此,我们的TLB并不会在进程切换时进行刷新,其只会在发生页缺失的时候刷新

结合汇编进行理解

        move        k0,a0 //第一个参数,就是Env中寄存器的首地址mtc0    a1,CP0_ENTRYHI //ASID写入CP0_ENTRYHI//对特殊寄存器的写不能直接写,要用这些汇编指令//mtc0可以写cp0里面的//mthi mtlo写hi lo,同理就行,还有一个读mfc0    t0,CP0_STATUS //修改cp0状态,你也不想你的程序被欺负吧?ori t0,0x3xori    t0,0x3mtc0    t0,CP0_STATUS

剩下的部分都是对RESTORE_ALL_AND_RET的重复,因为相当于恢复e的进程。

进程调度

这部分似乎最难的不是调度算法(login神中神

而是debug(^^^^^TOO LOW^^^^^

我踩的坑:

pmap.c中的位运算要用括号保证其优先级,表现为断言错误

&&和&写混(这lab2居然能过,离谱

页的权限位设置错误,特别是缺少对RTE_V权限位的设置,这会导致你的内存不断“消失”,表现为在进行一段循环后停止

init/init.c要写对,如果不对会出现很离谱的toolow,如果删删改改忘了应该是什么样子,抄方法二的

BUAA OS LAB3 实验报告相关推荐

  1. ucore lab3实验报告

    Lab3实验报告 Lab3实验报告 练习0填写以有实验 练习1给未被映射的地址映上物理页 问题回答 练习2补充完成基于FIFO的页面替换算法 问题回答 实验运行截图 扩展练习 Challenge 借助 ...

  2. HIT 软件构造 lab3实验报告

    2020年春季学期 计算机学院<软件构造>课程 Lab 3实验报告 姓名 赵旭东 学号 1180300223 班号 1803002 电子邮件 1264887178@qq.com 手机号码 ...

  3. [HITSC]哈工大2020春软件构造Lab3实验报告

    Github地址 1 实验目标概述 本次实验覆盖课程第 3.4.5 章的内容,目标是编写具有可复用性和可维护 性的软件,主要使用以下软件构造技术: 子类型.泛型.多态.重写.重载 继承.代理.组合 常 ...

  4. python实验报告代写价格_代写OS python程序作业、代写代写OS作业、代写OS实验报告...

    代写OS python程序作业.代写代写OS作业.代写OS实验报告 日期:2018-06-11 03:21 CSE 304 - Operating Systems DUE: June 11. Subm ...

  5. OS课程 ucore_lab1实验报告

    OS课程 ucore_lab1实验报告 练习一:理解通过make生成执行文件的过程.     列出本实验各练习中对应的OS原理的知识点,并说明本实验中的实现部分如何对应和体现了原理中的基本概念和关键知 ...

  6. linux 实验 ps,OS第1次实验报告:熟悉使用Linux命令和剖析ps命令

    零.个人信息 姓名:陈韵 学号:201821121053 班级:计算1812 一.实验目的 熟悉Linux命令行操作 二.实验内容 使用man查询命令使用手册 基本命令使用 三.实验报告 1. 实验环 ...

  7. BUAA 强化学习DQN代码及实验报告参考

    DQN实验报告 一.DQN实现方式 助教给的参考代码由两个文件组成,一个是game.py,一个是train.py.game.py的内容是迷宫界面绘制和agent行走方式.奖励规则的有关代码,而trai ...

  8. 《移动项目实践》实验报告——Android Studio环境搭建

    源代码:https://gitee.com/shentuzhigang/mini-project/tree/master/android-helloworld 实验内容 安装JAVA JDK,并配置环 ...

  9. 操作系统实验报告11:ucore Lab 2

    ucore实验报告2 实验内容 uCore Lab 2:物理内存管理 (1) 编译运行 uCore Lab 2 的工程代码: (2) 完成 uCore Lab 2 练习 1-3 的编程作业: (3) ...

最新文章

  1. Windows文件操作的直接函数调用
  2. windows7访问03文件服务器慢
  3. php 词法分析,【PHP7源码学习】2019-03-20 PHP词法分析
  4. 带负荷测试要求二次最小电流_带负荷检查
  5. SpringBoot 上传多个文件
  6. .NET Core 批量重置 Azure Blob Storage 的 mime type
  7. Python logging模块实现同时向控制台和文件打印日志
  8. 技术系统优化还可以这样做?
  9. [GO]删除切片的某个值
  10. java第二季_Java入门第二季
  11. Keil5下载和安装教程
  12. abaqus6.14 帮助 Abaqus Example Problems Guide翻译
  13. 前端基础之HTML特殊字符集和表情集
  14. OpenShift 4 - 在 GitOps 中使用 SealedSecret 保护敏感数据
  15. C++11中的原子操作(atomic operation)和自旋锁
  16. C++ builder 添加资源文件
  17. 基于云的文档管理系统——随时随地办公
  18. Go语言 编写代码统计出字符串中汉字的数量
  19. MATLAB Floor 用法
  20. visio中如何取消跨线和去掉页边距

热门文章

  1. 引起短波通讯服务器终端,短波通信终端设备
  2. Vue-router props 如何传递参数 ,传参请看这里
  3. 微软服务器开启锐速,ServerSpeeder 锐速服务器加速软件常用命令说明
  4. Python使用百度通用API进行翻译
  5. 谷歌浏览器如何安装vue调试工具
  6. 考研复试面试题(本科课程--运筹学篇)----2020考研
  7. 留一法 leaveOneOut(LOO)
  8. 常用计算机功率,【推荐】ZOL在线功率计算器 了解自用电脑的最大功率
  9. 虚拟服务器ip怎么配,怎么配置基于IP地址的虚拟主机
  10. 网络安全基础知识篇----nginx安装