Lab3实验报告

Lab3实验报告

一、思考题

Thinking 3.1

思考envid2env 函数:

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

在通过索引取envs数组中的第”id“个进程块时只取了envid的后10位,但是envid的后10位在生成的时候只与进程页的物理位置有关,要保证一个进程的id号完全对应,仅仅看后十位是不够的,也要确保前22位是一样的,因此e->env_id != envid这一步确定进程的id确实是传入的envid。

如果没有这步判断则可能出现输入的id并不是进程id号,而仅仅是进程的物理位置与另一个进程相同的情况,从而造成错误。

Thinking 3.2

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

  • UTOP 和ULIM 的含义分别是什么,UTOP 和ULIM 之间的区域与UTOP以下的区域相比有什么区别?
  • 请结合系统自映射机制解释代码中pgdir[PDX(UVPT)]=env_cr3的含义。
  • 谈谈自己对进程中物理地址和虚拟地址的理解。

UTOP是用户进程读写部分的最高地址;ULIM是用户进程的最高地址,是kseg0和kuseg的分界线。UTOP到ULIM之间的区域是存放用户进程的进程块和页表信息的地方,只能被用户读取而不能被用户更改,而UTOP以下部分是用户进程可以自由读写的部分。

env_cr3中保存进程的页目录的物理地址,pgdir[PDX(UVPT)]是页目录的页目录项;pgdir[PDX(UVPT)]=env_cr3把页目录的PDX(UVPT)项映射到了该进程的页目录的自身,即建立了自映射机制。

进程对应的地址都是虚拟地址,通过查询页表得到对应的物理地址;在实验中,不同的进程对于内核的2G的地址空间是相同的,而对于用户的2G有各自相互独立的地址空间,因此不同进程对应着不同的虚拟地址,但有可能会映射到相同的物理地址。

Thinking 3.3

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

不可以。

user_data是为了向内层的函数传值,缺乏它的话,传递的函数指针对应的函数就获得不了所需的外部参数。

举例,C语言中的快排qsort函数中的width参数是为了给内部调用的compare函数提供信息:

void qsort(
    void* base, 
    size_t num, 
    size_t width, 
    int (*compare)(const void* e1,const void* e2)
);
Thinking 3.4

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

有四种情况:首尾均对齐、首对齐尾不对齐、首不对齐尾对齐、首尾均不对齐。

最坏的情况即为指导书中给出的首尾均不对齐的情况。

若内存的首地址没有页对齐,则需要分配一页来存它;若加载textdata段后末尾未页对齐,需要再分配一页存储尾部;如果前一段未页对齐,并且bss段的首地址也未页对齐,需要再分配一页存储,并将相应的地方清空;如果bss段末尾未对齐,需要单独分配一页存储,并将其清空。

Thinking 3.5

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

  • 你认为这里的 env_tf.pc 存储的是物理地址还是虚拟地址?
  • 你觉得entry_point其值对于每个进程是否一样?该如何理解这种统一或不同?

env_tf.pc存储的是虚拟地址。

entry_point对于每个进程都是一样的;因为entry_point的值是一个虚拟地址,每次执行时都从一个固定的虚拟地址开始,这种统一对CPU比较友好,可以降低操作系统的复杂度;并且由于进程PCB不同,所以可以映射到不同的物理地址,这种不同可以依据不同的物理地址对应进程的不同含义,增加灵活性。

Thinking 3.6

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

epc是CP0的EPC寄存器的值,保存的是进程发生中断时进程的地址;

env_tf.pc设置为epc是为了使处理完异常中断重新执行后,可以从发生中断的地方继续向下执行。

Thinking 3.7

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

  • 操作系统在何时将什么内容存到了 TIMESTACK 区域
  • TIMESTACK 和 env_asm.S 中所定义的 KERNEL_SP 的含义有何不同

操作系统在时钟中断时,把存放CPU寄存器状态的栈的栈顶地址存到了TIMESTACK区域。

TIMESTAKCK是时钟中断时固定的栈顶地址,KERNEL_SP是其他中断时的栈顶地址。

Thinking 3.8

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

handle_sys函数在syscall.S中;

handle_inthandle_reservedhandle_tlbhandle_mod都在genex.S中。

Thinking 3.9

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

set_timer:(见注释)

 LEAF(set_timer)                        //定义set_timer函数
										
      li t0, 0xc8						//将0xb5000100写入0xc8,表示1秒钟中断200次
      sb t0, 0xb5000100
      sw  sp, KERNEL_SP					//将sp寄存器的值存到KERNEL_SP中,保存当前栈指针
 setup_c0_status STATUS_CU0|0x1001 0	//将状态寄存器的第0位和第12位置1(4号中断)
      jr ra								//函数返回
  
      nop
 END(set_timer)							//定义结束

timer_irq:(见注释)

timer_irq:
 
     sb zero, 0xb5000110				//0xb5000110写入0,关闭实时钟
 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				//跳转到ret_from_exception
     nop
Thinking 3.10

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

设置了两个链表队列,最开始时在队列1中。定时器负责产生中断,若时间片未用完,则时间片-1;当前进程的时间片用完之后,会将其从当前的队列取出,装入另一个队列的队尾。之后取出当前队列的头部进程,若为RUNNABLE状态,则执行,否则继续查找至找到为止。当前队列若为空,则切换另一个队列查找,如此进行调度。

二、实验难点图示

1、设置进程控制块

这一部分的难点主要在env_allocenv_setup_vm两个函数上。env_alloc作用是分配一个空闲PCB,在执行过程中调用了env_setup_vm函数,env_setup_vm函数的作用是初始化该进程的页目录。

在上面的思考题中已经提到过,在我们实验中,用户态的2G是私有的,并且各不相同,而内核态的2G对所有进程都是相同的。填写env_setup_vm最大的难点便在于看懂内存分布图。

ULIM以上的页目录项即和内核的页目录项完全相同;ULIM以下的为用户区特有的地址。

ULIM=0x80000000是操作系统分配给用户的2G地址空间的最大值,UTOP=0x7f400000是用户能够自由读写的地址空间的最大值。UTOP到ULIM这段空间映射的是记录页面使用情况的4M大小的pages数组,4M进程控制块envs数组和用户页表域的4M虚拟空间,用户不能写只能读,用于使用户进程查看其他进程信息。

具体到env_setup_vm函数,首先要申请页目录,此时对于用户而言,UTOP以下的区域为用户可以自由读取的区域,页目录要清零,UTOP以上的部分要和内核保持一致,要复制内核;其次为进程设置页目录和其物理地址;最后填入页目录自映射项。

对于env_allc函数,从env_free_list中申请出一个进程后进行初始化,并分配新资源,最后从程序链表中移出这一进程。

2、加载二进制镜像

这部分函数的最大难点在于对齐的问题,指导书上给了提示之后,思考时也容易了很多。

在这里插入图片描述

这一任务由load_icode函数完成,而load_icode函数又靠load_elfload_icode_mapper完成。

load_icode_mapper中,要加载一个ELF文件到内存,就要将ELF文件中所有需要加载的segment加载到对应的虚地址va上。但是va和文件大小都不一定对齐4KB,并且若一段内存不满一个页,则仍要分配一整个页面来存储。最开始需要检查开头一段是否对齐,如果不对齐,申请一个页面存储这一段,拷贝也只能拷贝这一段,接着申请之后对齐的页面,最后考虑binsize<sgsize是否成立,如果是,则将这一段全部赋0。

对于load_elf,完成对elf文件的解构,并将文件映射到内存。

最后是load_icode,这是真正的加载二进制镜像的函数。首先分配进程的运行栈空间(这里是用户栈),为栈空间预分配一个页面。即分配一个物理页,然后将用户栈的地址该物理页建立映射,映射的内存空间是 (USTACKTOP - BY2PG, USTACKTOP),之后调用load_elf函数把二进制文件加载到内存,并设置pc寄存器,要运行的进程的代码段预先被载入到了entry_ point为起点的内存中,运行进程时,CPU从pc所指的位置开始执行二进制码。

3、进程调度

lab3-2里题目少了很多,相较之下sched_yield的实现难度稍大。这个函数思路很简单,但是处理时一些细节问题很容易导致**^^^^too low^^^^**的问题。

思路:
在这里插入图片描述

关于题目的一些理解:

  • 使用两个链表,每个链表里存储着的都是待调度的进程,而且两个链表里的结点应该一样,只是存在的时间和顺序不一样;
  • 正在执行的进程是存在在当前所持链表的表头位置的,当该进程的时间片用完需要切换进程时,才将该进程从当前链表头删除,添加到另一个链表尾;

一些我踩过的坑🕳:

  • env_run()函数必须放在LIST_REMOVE()LIST_INSERT()count--等后面,因为env_run()之后的语句执行不了;
  • 必须先LIST_REMOVE()LIST_INSERT_TAIL(),因为删除的时候并不指明是从哪一个链表里删除,如果先INSERT再REMOVE,就会把刚刚插进去的删除掉;

三、体会与感想

这次lab第一部分花费时间较多,花了两天的时间都在断断续续的学习,大概有10个小时;第二部分学习所花费的时间相对较少,但也在解决**^^^TO LOW^^^**问题上花费了不少的时间,可能有6个小时左右。总的来说,感觉前半部分的内容比较多,很多题目在一开始学的时候理解起来还比较困难,不过经过不懈的软磨硬泡,最终也基本能看懂,不过还是还是很遗憾lab3第一次考试挂掉了,原因在于最后调用了env_run()(不过也可能有其他问题),感觉课上和课下关联不是很大,课下学习成果和课上考试结果貌似没有必然联系,虽然也很心疼丢掉的分,但是还是真正学到东西最重要;第二部分要填的空比较少,个人感觉写汇编的难度比较大,但是Exercise没有考察这方面的知识,上机考试也没有涉及,这次上机很顺利的通过了(泪目)。

四、指导书反馈

第一是关于curenv,我之前一直以为它和e一样,是需要自己定义的,也因此出了很多错,后来grep之后才知道curenv是全局变量,希望能针对这一点稍微提醒一下。

第二是关于load_icode_mapper这个函数,这个函数在给的时候只给了for循环,但是其实在for循环之前也有代码要填写,希望能在这里给出提示。

五、残留难点

感觉是指导书中涉及的那部分汇编的代码,让我自己写大概率写不出来。

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值