Lab-3实验报告

Lab-3实验报告

20373915-朱文涛

实验思考题


Thinking 3.1

思考envid2env函数:

为什么envid2env中需要判断e->env_id != envid的情况?如果没有这步判断会发生什么情况?

我们先观察一下envid的具体结构:

image

事实上只有ASID可以标志一个env的唯一性,我们观察一下envid2env中关于通过envid获取对应的env的做法:

    // #define ENVX(envid)    ((envid) & (NENV - 1))
// ENVX获取envid的低10位,e即为获取的envs中的envid对应的那一项
   e = envs + ENVX(envid);

可以看出它仅仅是通过envid的后十位在envs去寻找,但实际上我们需要需要确定找到的就是envid指向的进程,且该进程仍然存活,而不是某个正好占据相同位置的新进程。如果不做此步检查,当一个老进程结束后一个新进程正好占据老进程的位置,此时再用老进程的envid就会找到新进程的env块,就会返回一个错误的进程,导致发生严重的错误。

 

Thinking 3.2

结合include/mmu.h 中的地址空间布局,思考env_setup_vm 函数:

• UTOP 和ULIM 的含义分别是什么,UTOP 和ULIM 之间的区域与UTOP以下的区域相比有什么区别?

• 请结合系统自映射机制解释代码中pgdir[PDX(UVPT)]=env_cr3的含义。

• 谈谈自己对进程中物理地址和虚拟地址的理解。

  • UTOP是用户进程可以自由使用的地址空间的最高点,ULIMkusegkseg0的分界,即用户空间和内核空间的分界线。用户进程对在UTOPULIM之间区域的内存一般没有写权限。

  • pgdir[PDX(UVPT)]实际上是满足页目录自映射的页目录项,将这一项设置为env_cr3(页目录物理地址),这样,用户进程可以直接通过UVPT这一虚拟地址访问页目录,不仅省下一页的内存占用,也加速了访问速度。

  • 进程中,直接用来访问内存的地址都是虚拟地址。真实地址只有在TLB重填时才会被用到。可以说,对用户态进程来说,物理地址是透明的,用户进程无需关心数据的物理地址,只要有虚拟地址即可正常访问。

 

Thinking 3.3 找到 user_data 这一参数的来源,思考它的作用。没有这个参数可不可以?为什么?(可以尝试说明实际的应用场景,举一个实际的库中的例子)

user_data这个参数允许我们更好的定制load_elf的行为,没有这个参数会影响系统的灵活性。我们在

load时,可能会使用多种不同的mapper,这些mapper可能会需要不同的额外数据来辅助进行映射,

void *类型的user_data是一个最好的传递额外数据的方式,因为向void *型指针强制转换可以自动完

成,同时void *可读性也更好。

在真实库中,如果某个函数需要使用到用户提供的函数,且希望具有类似泛型的,可处理多种数据的能力,就会用到这种设计。案例:qsort()函数的width参数说明了数组每一个元素的大小,方便向比较函数传参。(比较函数的参数都是void * ,需要一个元素的大小,确定是什么样的指针)

// qsort()函数
void qsort(
   void* base,
   size_t num,
   size_t width,
   int (*compare)(const void* e1,const void* e2)
);

 

Thinking 3.4 结合load_icode_mapper 的参数以及二进制镜像的大小,考虑该函数可能会面临哪几种复制的情况?你是否都考虑到了?

  • va

    • va与页面大小对齐

    • va与页面大小不对齐

  • bin_size

    • bin_size <= BYP2G

    • bin_size > BYP2G

  • va + bin_size

    • va + bin_size后还在va所在页内

    • va + bin_size后超出va所在页内

    • va + bin_size页面对齐

    • va + bin_size页面不对齐

  • sgsize

    • sgsize > bin_size:需要填充

    • sgsize = bin_size:不需要填充

    • va + sgsize页面对齐

    • va + sgsize页面不对齐

 

Thinking 3.5 思考上面这一段话,并根据自己在lab2 中的理解,回答:

你认为这里的 env_tf.pc 存储的是物理地址还是虚拟地址?

• 你觉得entry_point其值对于每个进程是否一样?该如何理解这种统一或不同?

  • “指令位置”针对的是虚拟空间,因为我们取指时用的地址是虚拟地址。

  • 不一定一样。e_entryELF文件头是有定义的:截屏2022-05-14 22.50.40

大部分ELF格式可执行文件的entry_point都是相同的,但是ELF格式文件中也允许设定程序的entry_point,这正体现了虚拟内存的优势:进程可以自主的决定自己的布局。此外,操作系统可以支持多种类型的可执行文件,这些可执行文件的entry_point也不一定相同。

 

Thinking 3.6 请查阅相关资料解释,上面提到的epc是什么?为什么要将env_tf.pc设置为epc呢?

  • epc是指即将执行的下一条指令的位置。

  • 在我们的OS里,如果要进行进程切换,一定是因为发生了中断或者异常发。进入env_run时如果当前curenv不是null,则当前进程进入中断时的寄存器状态必定在TIMESTACK处存放(中断处理时会先调用 .\include\stackframe.h中的saveall,而saveall依赖的sp指针值在时钟中断(目前唯一的中断)时正是TIMESTACK)。由于是通过中断进入的,EPC指向的值就是受害指令,如果我们以后要恢复这个进程的运行,当然是从受害指令开始重新执行,因此应设为env_tf.cp0_epc

 

Thinking 3.7 关于 TIMESTACK,请思考以下问题:

操作系统在何时将什么内容存到了 TIMESTACK 区域

TIMESTACK 和 env_asm.S 中所定义的 KERNEL_SP 的含义有何不同

  • OS在系统发生中断或者异常是将当前进程的上下文、现场信息保存在TIMESTACK,以便恢复异常后进程可以正确运行。

  • 我们可以观察在.\lib\genex.S中关于异常处理的内容,如下:

    .macro BUILD_HANDLER exception handler clear 
    .align 5
    NESTED(handle_\exception, TF_SIZE, sp)
    .set noat
    nop
    SAVE_ALL
    __build_clear_\clear
    .set at
    move a0, sp
    jal \handler
    nop
    j ret_from_exception
    nop
    END(handle_\exception)
    .endm

    这个宏表现出了异常处理的一般形式:先保存上下文(SAVE_ALL),在跳转至特定的异常处理函数,最后从异常返回。其中,保存上下文(SAVE_ALL)这个过程定义在.\include\stackframe.hSAVE_ALL先调用get_sp来获得栈指针,并把寄存器等上下文信息存入栈中,get_sp内容如下:

    .macro get_sp 
    mfc0 k1, CP0_CAUSE
    andi k1, 0x107C
    xori k1, 0x1000
    bnez k1, 1f
    nop
    li sp, 0x82000000
    j 2f
    nop
    1:
    bltz sp, 2f
    nop
    lw sp, KERNEL_SP
    nop
    2:
    nop
    .endm

    get_sp所做的事情其实是:如果CP0_CAUSE中,exccode的值为0且IRQ4值为1,则使用0x82000000作为栈。否则,如果sp> 0x80000000,则直接使用sp,否则使用KERNEL_SP作为栈地址。而0x82000000就是我们的TIME_STACK,同时,IRQ4正是时钟中断的中断请求。

    KERNEL_SP是内核处理各种异常中断时的通用的栈,而TIME_STACK专用于处理时钟中断和与之紧密联系的进程切换等任务。

 

Thinking 3.8 试找出上述 5 个异常处理函数的具体实现位置。

  • handle_int、handle_tlb、handle_tlb、handle_mod定义在`lib/genex.S中。

  • handle_sys定义在lib/syscall.S中。

 

Thinking 3.9 阅读 kclock_asm.S 和 genex.S 两个文件,并尝试说出set_timer 和timer_irq 函数中每行汇编代码的作用

  • 先观察lib/kclock_asm.S中的set_timer函数:

LEAF(set_timer)
#向0xb5000100中写入0xc8,其中0xb5000000 是模拟器(gxemul) 映射实时钟的位置。
#偏移量为0x100 表示来设置实时钟中断的频率。
#0xc8 则表示1 秒钟中断200次,如果写入0,表示关闭实时钟。
       li t0, 0xc8
       sb t0, 0xb5000100
       #设置KERNEl_SP,内核异常处理栈的值
       sw      sp, KERNEL_SP
       #设置CP0的Status寄存器
       #STATUS_CU0为仅开启CU0,表示CP0存在的状态;
       #0x1001,最低位1开启终端,第13位1使能IRQ4即时钟中断。
setup_c0_status STATUS_CU0|0x1001 0
#返回
       jr ra
       nop
END(set_timer)
  • 在观察处于lib/genex.S中的timer_irq函数:

timer_irq:
#先关闭时钟中断
       sb zero, 0xb5000110
       #跳转到调度函数中毒对进程进行调度
1:     j       sched_yield
       nop
       #跳转到恢复异常函数中
       j       ret_from_exception
       nop

 

Thinking 3.10 阅读相关代码,思考操作系统是怎么根据时钟周期切换进程的。

  • 时钟中断发生时,系统在保存上下文之后跳转到sched_yield函数,进行进程的调度。

  • sched_yield函数首先判断当前进程时间片是否用完,若未用完继续执行当前进程,否则根据调度算法选择一个新进程继续执行,原进程上下文被保存并再次进入就绪队列。

  • 最终,新的进程通过调用env_run函数被执行。下次时钟中断发生时,重复上述步骤。

 

实验难点


位图法管理进程

//管理64个进程
static u_int asid_bitmap[2] = {0}; //64位管理64个ASID

static u_int asid_alloc() {
   int i, index, inner;
   for (i = 0; i < 64; ++i) {
       index = i >> 5;
       inner = i & 31;
       //如果对应第i位ASID没有被申请,则申请
       if ((asid_bitmap[index] & (1 << inner)) == 0) {
           asid_bitmap[index] |= 1 << inner;
           return i;
      }
  }
//无空闲ASID后报错
   panic("too many processes!");
}

static void asid_free(u_int i) {
   int index, inner;
   index = i >> 5;
   inner = i & 31;
//当一个进程被释放时,其对应的ASID也被释放
   asid_bitmap[index] &= ~(1 << inner);
}

u_int mkenvid(struct Env *e) {
   u_int idx = e - envs;
   u_int asid = asid_alloc();
//观察envid的组成,其实只有asid可以保证一个进程的唯一性
   return (asid << (1 + LOG2NENV)) | (1 << LOG2NENV) | idx;
}

 

加载段信息

load_icode_mapper,加载一节的内容到内存中

最坏的情况:

 

static int load_icode_mapper(u_long va, u_int32_t sgsize,
                            u_char *bin, u_int32_t bin_size, void *user_data)
{
   struct Env *env = (struct Env *)user_data;
   struct Page *p = NULL;
   u_long i;
   int r;
//offset为起va相对va所在页首地址的偏移
   u_long offset = va - ROUNDDOWN(va, BY2PG);
//size是va到va所在页的尾地址的大小,即该节的在第一页中的大小
   u_long size = BY2PG - offset;
 
   **************先拷贝第一页中不对齐的那部分即[va,(va<<12 + 1) >> 12 -1]*****************
   if (offset >= 0) {
    //判断2段业内共享
    if ((p = page_lookup(env->env_pgdir, va, NULL)) == 0) {
      if ((r = page_alloc(&p)) < 0) return r;
    //注意这里映射是虚拟地址为va所在页的起始地址
        if ((r = page_insert(env->env_pgdir,p,va - offset,PTE_R)) < 0) return r;
    }
    //使用min旨在考虑va + bin_size仍然在va所在页面内
       bcopy((void *)bin,(void *)(page2kva(p) + offset),MIN(bin_size,size));
  }

***************拷贝bin_size中剩余的部分,即[(va<<12 + 1) >> 12,va + i -1]******************
   //从i=size开始拷贝,因为[0,size-1]已经被拷贝了
   for (i = size; i < bin_size; i += BY2PG) {
       if ((r = page_alloc(&p)) < 0) return r;
       if ((r = page_insert(env->env_pgdir,p,va + i,PTE_R)) < 0) return r;
    //这里的min旨在考虑bin_size结束时不足一页的情况
    //多余部分不许手动拷贝0,因为page_alloc已经做了这件事
       bcopy((void *)(bin + i),(void *)page2kva(p),MIN(bin_size - i,BY2PG));

  }

********************拷贝[va+i,va+sg_size-1]部分区域********************
   while (i < sgsize) {
    //因为page_alloc已经手动页面清零,所以我们只需要建立映射关系即可
       if ((r = page_alloc(&p)) < 0) return r;
       if ((r = page_insert(env->env_pgdir,p,va + i,PTE_R)) < 0) return r;
       i += BY2PG;
  }
   return 0;
}

 

进程的调度

首先观察一下我们MOS中简单的时间片轮转调度算法:

void sched_yield(void)
{
   static int count = 0; // remaining time slices of current env
   static int point = 0; // current env_sched_list index
static struct Env *e;
   
//如果当前进程已经结束(或者刚开始)
if (count <= 0) {
do {
    //如果当前调度队列为空,换另一个调度队列
if (LIST_EMPTY(&env_sched_list[point])) {
point = 1 - point;
}
//取出调度队列队首进程
e = LIST_FIRST(&env_sched_list[point]);
//如果能取出来,就预先把它插入另一个队列中,同时开始运行这个进程
if (e != NULL) {
LIST_REMOVE(e, env_sched_link);
LIST_INSERT_TAIL(&env_sched_list[1 - point], e, env_sched_link);
count = e->env_pri;
}
    //当调度队列为空或者进程状态部位ENV_RUNNABLE,则一直做上述循环
} while (e == NULL || e->env_status != ENV_RUNNABLE);
}
count --;
env_run(e);
}

 

中断处理流程

中断处理的大致流程如下:

image

 

 

体验与感想


耗时

耗时20h+,花费大量时间在思考各种函数的作用和联系,同时程序出现了bug,花费一定时间debug

感想

感觉难度陡升,主要有以下困惑和难点:

  • 各种汇编函数理解起来比较困难,和我们在mars中编写的汇编代码有一点差距,比较影响理解,希望教程组可以指出。

  • sched_yield()load_icode_mapper()等函数需要普安端的条件较多,实验中仅给了一张图和较少提示,书写起来比较困难。

  • 希望时钟中断流程、进程创建调用到销毁流程中可以有图示说明各函数的调用关系,这样方便学者理解,更快厘清思路。

 

实验指导疑难反馈


疑惑一

bcopybzero为什么必须在对齐地址上使用(或者直接改一下这两个函数?),因为这会导致在需要手动copy或清空不对齐地址的内容上发生一定错误。

 

疑惑二

env_destroy时需要将KERNEL_SP中的Trapframe拷贝到TIMESTACK中,但是进程被摧毁后,已经将curenv设置为null了,理论上来说在之后运行sched_yield()的时候,curenvnullenv_run()也应该不需要保存TIMESTACK里的信息了吧?

 

疑惑三

Lab3-2是怎么评测正确性的?能不能优化一下判别方式呢?

posted @ 2022-07-06 22:17  `Demon  阅读(26)  评论(0编辑  收藏  举报