BUAA OS LAB3 实验报告

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_RET
        RESTORE_SOME
        lw  k0,TF_EPC(sp)
        lw  sp,TF_REG29(sp)  /* Deallocate stack */
        jr  k0
        rfe
.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_sp
    mfc0    k1, CP0_CAUSE
    andi    k1, 0x107C
    xori    k1, 0x1000
    bnez    k1, 1f
    nop
    li  sp, 0x82000000 //TIMESSTACK
    j   2f
    nop
1:
    bltz    sp, 2f
    nop
    lw  sp, KERNEL_SP
    nop

2:  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	//写入立即数 200
    sb 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, 0xff
    lw    t0, delay
    addu  t0, 1
    sw  t0, delay
    beq t0,t1,1f    
    nop*/
    j   ret_from_exception	//这个宏定义在lib/genex.S
    						//会跳转到epc去,没看太懂v.v
    nop
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 Max
 o                      |       ...                  |  kseg0
 o  VPT,KSTACKTOP-----> +----------------------------+----|-------0x8040 0000-------end
 o                      |       Kernel Stack         |    | KSTKSIZE            /|\
 o                      +----------------------------+----|------                |
 o                      |       Kernel Text          |    |                    PDMAP
 o      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 stack
    if(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,0x3
        xori    t0,0x3
        mtc0    t0,CP0_STATUS

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

进程调度

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

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

我踩的坑:

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

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

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

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

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值