JOS实验四.docx

上传人:b****7 文档编号:10138992 上传时间:2023-02-08 格式:DOCX 页数:25 大小:1.16MB
下载 相关 举报
JOS实验四.docx_第1页
第1页 / 共25页
JOS实验四.docx_第2页
第2页 / 共25页
JOS实验四.docx_第3页
第3页 / 共25页
JOS实验四.docx_第4页
第4页 / 共25页
JOS实验四.docx_第5页
第5页 / 共25页
点击查看更多>>
下载资源
资源描述

JOS实验四.docx

《JOS实验四.docx》由会员分享,可在线阅读,更多相关《JOS实验四.docx(25页珍藏版)》请在冰豆网上搜索。

JOS实验四.docx

JOS实验四

JOS实验四

作者:

卓达城

邮箱:

zhuodc@

备注:

本文档重点说明exofork函数的返回机制(下面将以黑体标志),如果有不当,敬请发邮件到我的邮箱。

本文档最精彩的地方在于缺页中断处理函数的返回机制和堆栈的切换,文中以灰底,不同字体、加粗显示。

还有一处就是vpd和vpt的使用,这里用了回环搜索pgdir和二级页表,也用加粗显示。

如果要做实验四,要用svn把mit提供的实验代码很lab3的代码合并。

前面的实验也如此。

修改:

对于实验三,有部分地方的代码我写错了,要修改,具体如下:

函数:

load_icode

修改user_mem_check函数:

现在进入实验4:

PARTA:

实现调度算法

第一步:

修改kern/sched.c里面的函数sched_yield,具体代码如下:

这个函数很简单,就是从当前环境的下一个环境一直遍历到当前环境,如果有可以运行的就开始运行。

如果没有,就进入idle环境。

然后修改系统调用,具体是修改syscall.c里面的syscall,具体代码如下:

这里要完成parta的话不用添加那么多,但是我是做完lab4再写的,所以就多加了一些进去。

现在在init.c里面多创建几个环境,以供测试,具体如下:

这里为了达到实验效果,在bochs开始一两秒后要bochs关掉,或者在sched_yield里面把多余的调试信息删除掉,才能看到效果,不然程序会不断地刷屏。

又或者把

这两句激活,也可以达到效果的。

现在开始实现sys_exofork函数:

这个函数主要是用来创建环境,这个函数也是整个实验最为难理解的函数之一,这里将详细解释,如果讲的不好,请莫怪,因为实在比较复杂。

函数代码不多,具体如下:

要理解这个函数,又要回顾一下中断和异常这方面的知识:

我们可以大体的把cpu的中断和异常弄成四类,fault、trap、interrupt(用户调用)、abort不好用中文翻译,关于那个中断号

我们这里都用call来做说明,中断和异常进行特权级的切换(只要设计者愿意,可以在任何特权级间切换),而call是不能的,calltss段也可以切换,但是只能同特权级或者高特权级到低特权级的切换。

现在我们先不管特权级。

那么如callfunction,在function执行之前,cpu先把cs,eip(如果是段内的话,就只入栈eip)入栈,这里最值得关注的是eip,在call指令下,eip是执行call的下一条指令的,所以当function执行完之后,也就是ret之后(弹出所有刚才入栈的东西),程序在call的下一条指令执行。

中断不仅入栈cs、eip,它还会入栈ss、esp、eflag、cs、eip、errno、interruptnum。

这里的interrupt跟call是完全一样的,而且是程序员调用的。

有些也不是,例如键盘中断,总之它是一些好的情况,下面三个都是坏的情况。

对于fault跟call不一样的地方就是eip的问题,eip是指向引发异常的指令的,就是如果指令moveaxebx,引起fault,那么eip就指向这条指令,然后处理完之后,cpu重新执行moveaxebx。

这里特别注意,fault都是不能由程序员调用的,是硬件发出的。

对于trap,trap跟interrupt是一样的,但是它也是硬件发出,例如溢出,处理完后,运行下一条指令,还有就是trap执行时eflag的if位不会置位,interrupt会的。

对于abort,这个不允许程序或者任务继续执行,是用来报告错误的。

讲了一大堆之后,现在来看看函数的返回值机制,函数的返回值总是放在eax中,这也就是为什么我们的函数只能返回一个值的缘故。

讲了这么多废话,现在开始进入正题:

这句代码是在dumbfork.c里面。

我们为了便于理解可以写成:

inti=sys_exofork();

envid=i;

sys_exofork()是一个interrupt,按照之前写的我们可以知道它入栈的eip是指向envid=i的,也就是sys_exofork执行完之后要执行envid=i这句代码。

那么exofork做了什么呢?

这个我们从上面代码可以清楚的看到,它在申请了一个新的环境,然后把父环境的寄存器复制进去,然后把tf->eax变成0。

注意这里不是eax寄存器,tf是用来还原寄存器用的。

这时候,由实验3我们可以知道父环境中tf->eip是指向envid=i这条指令,请看trapencry.c这个文件。

然后函数返回,中断返回,cs和eip,eflag,ss,esp出栈,父环境运行envid=i这行代码。

这时候,父环境设置子环境的各种东西,地址映射之类,下面会讲,总之就是让子环境可以运行吧。

然后父环境调用sched_yield,然后刚才建的子环境被还原。

子环境中的tf是eip指向envid=i这句代码,然后eax被设置成tf->eax就是刚才设置的0,所以envid=i也就是envid=eax=tf->eax=0。

多么绝妙的思想啊!

现在实现sys_env_set_status函数:

这个这个函数就是把环境设置成可以运行和不可以运行,代码如下:

现在实现函数sys_page_alloc:

这个函数的功能就是分配一个内存也给envid对应的env,并且映射到地址va处。

函数staticint

sys_page_map(envid_tsrcenvid,void*srcva,

envid_tdstenvid,void*dstva,intperm)

作用是把srcenvid中srcva对应的页映射到dstenvid中的dstva处,具体代码实现如下:

函数staticint

sys_page_unmap(envid_tenvid,void*va)

解除envid中va映射的页,具体代码如下:

这里我们还可以看看envid2env这个函数,如果输入是0就返回当前环境,代码就不贴了。

此时,在init.c里面修改代码

就可以看到相应效果。

现在进入PARTB

我们先看看dumbfork.c里面的一个函数,这个函数是duppage写得很有意思。

这个函数的具体操作是:

先给子环境分配一个页(我们暂时叫做pg),并映射到dstenv的addr上

然后把pg映射到父环境的地址UTEMP中,由于现在是在父环境中运行,用的是父进程的地址空间,所以需要这样做。

然后把父进程中addr对应页的内容复制到UTEMP中,然后删除父环境中UTEMP的映射,这里并没有删除页,因为它还映射到子环境中,就是说pp_ref>0。

EX4

sys_env_set_pgfault_upcall函数,代码如下:

代码很简单,一看就可以明白。

EX5

要完成ex5,首先要在中断哪里设置好page_fault的入口,这里在dispatch函数里面已经设置过了,具体参照本文档前面关于dispatch的实现。

然后是:

void

page_fault_handler(structTrapframe*tf)

这个函数的作用是设置好调用缺页处理函数的参数,缺页处理函数的参数和堆栈,参数是一个命名为UTrapframe的数据结构,堆栈是以UXSTACKTOP为栈顶的堆栈。

先看这两句代码:

因为pagefault是一个中断,所以tf里面放的是什么大家应该比计较清楚,如果不清楚请参照trapentry.S以及其它函数。

这两句代码的作用是判断是不是迭代的缺页中断,这里有一个值得注意的地方就是

,为什么要-4呢?

这是因为我们还要留4个字节把指向utf的esp放到堆栈里面,这样它就可以作为参数传递给以后的函数。

如下:

至于为什么要这样判断是否迭代,原因很简单,就是因为如果是缺页中断迭代,esp肯定在UXSTACKTOP-PGSIZE,UXSTACKTOP-1之间>USTACKTOP。

然后是下面的代码:

以上代码就是填充utf结构的代码,其中值得注意的一句是

这里把原来的栈换成新的栈。

然后运行env_run,现在cpu切换到用户态(这很重要,因为它会影响到ret指令的操作,同级返回ret指令只popeip,不同特权级会popip和popcs,call指令道理一样),为什么?

因为这里的还原的cs是用户态的cs。

刚看这里也不会明白,如果要弄明白它到底是什么葫芦卖什么药,我们要结合下面的代码(pfentry.S):

现在开始重点研究栈的结构,结合实验2,我们可以知道进入中断之后系统的堆栈情况,如下图:

实际的esp

这里又出现一个重要问题,这里看看现在的堆栈情况到底Trapframe里面的esp和ss是什么?

由intel手册可以知道中断的入栈具体入栈情况如下:

无特权级转换有特权级转换

这里我们还有一点要提到的是当cpu切换到内核的时候,内核的堆栈从哪里得到,答案就是从TSS里面得到,JOS只定义了一个TSS段,索引是0x28,在idt_init里面设置的。

当系统出现缺页中断的时候,cpu把当前运行指令的eip(当前指令的eip)入栈,由实验2我们可以知道它是放在Trapframe里面的。

然后

又把它放到utf里面,进入缺页处理函数之后,栈的状态应该是:

 

关于UTrapframe里面的结构,请自己参照UTrapframe定义。

这几句代码是把eip==tf->eip(缺页中断的eip)放到eax里面。

刚好是指向eip的,请核对UTrapframe结构。

然后把esp==外部esp,也就是用户程序的esp(请看本页第一个图)。

然后在eax(里面是eip)放到用户栈里面。

然后把一大堆寄存器pop出来,这里有个问题,为什么没有了cs和ss等段寄存器呢?

因为都在用户态,段都一样的,所以这里不用还原段寄存器。

popl%esp,这句代码恢复到用户栈,即(USTACKTOP)。

在这句代码之前是(UXSTACKTOP)。

由于在用户段,没有段切换,所以ret指令只把eippop出来(对于不同的情况,ret指令的操作是不一样的),然后程序就会在eip哪里继续运行,这里就是产生缺页中断的代码处。

subl$4,%esp是把esp指向刚才压入的eip,ret指令将会把它pop出来。

EX7

set_pgfault_handler函数:

这个函数也是相对简单,但是写完这个函数以后我们应该要明白pagefault的调用流程。

如下:

缺页中断发生->跳到汇编代码

->运行handler函数,就是上面的最后一句代码设置的函数。

现在可以进入测试:

修改init.c,然后编译运行就可以看到想要的结果。

staticvoid

pgfault(structUTrapframe*utf)

代码如下:

这个函数大概就是判断当前缺页的类型是否为FEC_WR(缺页中断时有错误码的,可以根据错误码判断,cpu自动压入),当前页是否为PTE_COW。

然后把要写的页的内容复制到PFTEMP,然后再把地址映射到PFTEMP,具体实现跟duppage相同,请看上面的描述在partb开始哪里。

现在看看vpt和vpd到底是怎么回事?

先看看env_setup_vm里面的两句代码,我们还可以知道vpt=UVPT的

具体的值是0xef400000,也就是高10位以后全部是零。

看下面两句代码:

由于对vpt进行了映射,所以当代码访问vpt的时候会出现下面的情况:

回顾一下cpu在分页的情况下的寻址就很容易看懂下面的图了。

pgdir

pgdir

vpt

二级页表的首地址(由于vpt的低12位都是0)

这里放的是页描述符。

(页的物理地址+属性)

有代码可以知道vpd就是把UVPT的头10位复制到UVPT的中间10,那样会有什么样的效果呢?

pgdir

pgdir

vpd

vpdpgdir

这里放的是二级页表的物理地址+权限

有了以上基础,我们可以看看fork到底是怎么样运行的。

主要是理解for循环,其它的都不难理解。

先看for循环

先对vpd进行遍历,其实就是对pgdir进行遍历,这个有了上面的图很容易理解。

当找到存在的话,然后遍历它对应的页。

这里想一想vpt+i*NPTENTRIES对应的是什么?

就是第i个页目录对应的二级页表的首地址,再加上j就是我们要映射的页。

这里细细想一下就会感到jos作者思想之精妙。

现在再回过头来看pagefault函数,就很容易明白了。

最后看看duppage函数

这个函数就是判断当前页是否为用户可写或者cow(copyonwrite),如果不是,直接映射,不改属性,如果是就映射两次,并把属性改成PTE_COW。

这里无论父进程还是子进程都要做这个事情。

为什么呢?

(这里要说明一点,父进程跟子进程是完全隔离的)。

当父进程建立了一个子进程(环境)之后,子进程addr映射父进程(环境)的一页p,如果我们不复制多一次,就是没有下面这句代码:

子进程写addr之前,如果父进程改变p的内容,那么子进程里面的数据也变了,这不是jos的设计思想。

但是如果有上面这句代码的话,当父进程要写的时候,同样发生copyonwrite,所以不会改变原来的那一页的内容,就是子进程里面的数据也是不会变的。

改变init.c里面的内容,就可以看到相应的结果。

现在进入PARTC

首先实现时钟中断,相对上面的东西,这个太简单了,要改的地方是:

trapentry.S

trap.c

idt_init函数

trap_dispatch(structTrapframe*tf)函数

env_alloc函数(env.c)

这里设置是让用户程序运行的时候允许硬件中断。

根据jos的要求,是内核态不能发生中断,用户态必须发生中断。

我们这里可能会问当系统切换到内核的时候,哪里设置了eflag让它不能发生中断呢?

答案在中断的机制里面,当系统发生interrupt时,eflag自动把if位变0,trap不会。

我们在我们的代码中把idt里面的映射都设为interrupt,我们看看:

0表示是interrupt,1表示是trap。

具体请看上面关于中断的解释。

实现IPC

sys_ipc_try_send(envid_tenvid,uint32_tvalue,void*srcva,unsignedperm)函数:

判断srcva是否为空,如果是直接通过

传送值,如果不是进行页映射,然后把接收数据的进程的状态改成可以运行,其它判断的不多说了,比较简单。

sys_ipc_recv(void*dstva)

把当前进程阻塞,然后准备接收数据。

然后是

int32_t

ipc_recv(envid_t*from_env_store,void*pg,int*perm_store)

也比较简单,不做解释

ipc_send(envid_tto_env,uint32_tval,void*pg,intperm)函数

这以后就可以进行进程通信了

如果前面没有改syscall函数,现在可以把它改好,但是如果是按照上面做下来,应该就改了

然后修改init.c

就可以见到相应结果。

源代码可以通过邮箱叫我提供!

展开阅读全文
相关资源
猜你喜欢
相关搜索

当前位置:首页 > 医药卫生 > 药学

copyright@ 2008-2022 冰豆网网站版权所有

经营许可证编号:鄂ICP备2022015515号-1