操作系统 MIT JOS lab4 超详细过程,附已通过代码

操作系统 MIT JOS lab4

本次实验主要内容:
(1)多处理器系统
(2)抢占式调度
(3)类似UNIX的fork——创建子进程,以及写时复制的机制
(4)进程间通信

附有注释详细的已通过代码,链接:https://download.csdn.net/download/qhaaha/13741551,如有需要可自取。

(写在前面)cpu、处理器、核的概念在这次lab中没有必要严格区分,在表述中就混着用了~

练习

1

在这里插入图片描述

函数作用:在虚拟地址MMIO 区域分配size大小,并把它映射到物理地址pa开始的size大小空间。

在这里插入图片描述

2

在这里插入图片描述

AP的启动代码放到了MPENTRY_PADDR,这里需要将page_init函数中将MPENTRY_PADDR处的物理页标识为已用。加上红线部分:

在这里插入图片描述

3

在这里插入图片描述

函数作用:在内核页目录中构造每个cpu的内核栈,注意中间的stack gap是用来在栈溢出时触发缺页中断的,占据虚拟地址空间,但是不被映射到物理内存。

在这里插入图片描述

4

在这里插入图片描述

初始化每个核,主要包括TSS——任务状态段,应该是每个核分别的。
先回顾几个概念(lab3中已提到)
(1)Segdesc——段描述符格式:
在这里插入图片描述

(2)gdt——全局描述符表:
JOS中的gdt图示

在这里插入图片描述

初始化,可以看到代码中符合UNIX的标准段基址都设置为了0,然后红色框中是我们在trap_init_percpu中需要完成的创建的段:
在这里插入图片描述

(3)TSS 全称task state segment,是指在操作系统进程管理的过程中,任务(进程)切换时的任务现场信息。 是GDT表中一种特殊的段描述符。

在这里插入图片描述

(4)ltr指令:
任务寄存器tr保存 16 位的段选择子、32 位基地址、16 位段界限和当前任务的 TSS属性。它引用 GDT 中的 TSS 描述符。基地址指明 TSS 的第一个字节(字节 0)的线性地址,段界限确定 TSS 的字节个数。TR寄存器包含了当前正在CPU运行的进程的TSSD(任务段描述符)选择符。也包含了两个隐藏的非编程域:TSSD的base 和limit域。通过这种方式处理器就能直接对TSS寻址,而不用从GDT中索引TSS的地址。
TR寄存器---->GDT中的TSS描述符---->硬件上下文的具体数据。任务切换中cpu会把当前寄存器的数据保存到当前(旧的)tr寄存器所指向的tss数据结构里,然后把新的tss数据复制到当前寄存器里。这些操作是通过cpu的硬件实现的。
上图:
在这里插入图片描述

参考:https://blog.csdn.net/cbl709/article/details/7523951

理解了上面的概念,再看这个函数容易懂了。每个cpu都有各自的tss,所以应该是分别设置,这就是我们需要修改的地方。初始时栈顶指针esp0应该就是栈底。然后设置gdt表中对应的条目,再然后装载对应条目到tr寄存器(ltr)。

在这里插入图片描述

练习1~4回顾

写完前4个练习,我在程序执行过程上还是有些困惑,做一个简单回顾,看一下已经完成的部分是怎么被用在多cpu系统的引导和初始化上的。
首先要搞清楚哪些是所有cpu公用的,哪些是每个cpu独有的。
公用的:

  1. 操作系统(废话)
  2. 内核页目录(虚拟地址映射,也是废话,因为kernel是唯一的)
  3. 物理内存空间(好像还是废话~ 这里指整个内存只有一个,不是指具体的内存分区;事实上,同一时刻不同cpu一般不会操作同一块具体的内存)

独有的:

  1. 内核栈(因为不同的核可能同时进入到内核中执行,因此需要有不同的内核栈;不过本次lab实现的还是大内核锁~ 内核中只会有一个cpu啦)
  2. TSS描述符 (已经在练习4中考虑到这一点)
  3. 每个核的当前执行的任务
  4. 每个核的寄存器

直接上流程图
在这里插入图片描述

这是整个初始化过程中lab4修改的部分的流程图。
bootcpu是刚开始用来引导的cpu,它的初始化过程就是之前我们一直在写的——上图中的主线部分,具体哪些地方做了修改已经标记出来了。
AP(application processor)是其它的cpu,主要通过bootcpu的boot_aps引导,之后会在mp_main中初始化,也用到了练习4的trap_init_percpu这个函数,也标出来了。

然后特别去看一下新加进来的mp_init函数,这个函数是根据在一开始(这里比较模糊,应该是在BIOS阶段)就已经装载好到内存的MP Floating Pointer Structure,检索到当前硬件环境中的处理器,并完成一些系统全局的、关于多处理器最基本的变量的初始化。这个函数不特别详细的写了,主要是找一下后面会用到的一些全局变量从哪里来的,看kern/mpconfig.c中定义的全局量:
在这里插入图片描述

分别表示所有的cpu和 bootcpu是哪个,是这么个格式:
在这里插入图片描述

然后看一下mp_init中的关键操作,用红线标出来了:
在这里插入图片描述

mpconfig就是根据IntelMp 4 的规范在内存的特定位置找mp的“信息表”(并校验),然后根据信息表,将找到的处理器加入cpus数组,如果是bootcpu,就设置bootcpu指针。
关于这里的“信息表”,其实有两个数据结构:MP Floating Pointer 和MP Configuration Table(MP配置表),这部分内容参考:
https://blog.csdn.net/stupid_haiou/article/details/46430749

练习1、2、3、4的回顾就先写这么多,继续做exercise。

5 在这里插入图片描述

这个练习是在每个处理器进出内核的时候获得与释放大内核锁,使得同一时刻只有一个处理器在内核关键代码段中执行。按照提示,有以下3个地方要上锁:
kern/init.c/i386_init
在这里插入图片描述

kern/init.c/mp_main
在这里插入图片描述

kern/trap.c/trap
在这里插入图片描述

一个地方要释放锁:
kern/env.c/env_run

在这里插入图片描述

6

在这里插入图片描述

实现调度策略,这里是一个轮转调度,即调度时从上一次运行的进程(如果没有,从0)开始,查找envs数组中下一个runnable的环境运行。如果没有可以运行的环境了,这个处理器要么接着运行刚才没有运行完的前一个环境,否则进入monitor状态。
在这里插入图片描述

7

在这里插入图片描述
在这里插入图片描述

PartA的最后一个练习是实现几个新的系统调用,主要是用来为之后会完成的fork做准备,使得能在用户程序中创建进程。
sys_exofork: 通过调用env_alloc创建一个环境
在这里插入图片描述

sys_env_set_status: 设置环境的状态:可运行或不可运行。

在这里插入图片描述

sys_page_alloc: 这个函数是分配一个物理页给envid对应的环境,并“把这一页映射到envid的虚拟地址va处”。可以调用下层的page_insert函数。

上面加的那一段引号是因为我要记录一下这里遇见的一个坑:分配一个物理页之后是比较好的做法是内存清0(假如不清0,即使这一页被分配给了用户程序,由于编译器对指针越界的检查,也是不用担心引用到错误数据的;但是从内存保护和防止数据泄露的观点出发,对新分配下去的物理页的清0显然是操作系统层面必须要做的),所以下图中有两种选择:要么在1中page_alloc具有参数ALLOC_ZERO,要么2中加上注释里的memset。
我刚开始是用的第二种方法,但是被这个函数的说明坑了一下(其实还是怨自己,我大意了啊~):“把这一物理页映射到envid的虚拟地址va处”这个描述不是很严谨的,严谨来说应该是把分配的物理页映射到虚拟地址va所在的那个虚拟页中,就是说va并不要求是页对齐的,我们只是分配一物理页映射到能“容纳”va的虚拟页,也就是物理页基址应该是和虚拟页基址建立映射的。(按理说这也是显然的,毕竟内存映射是页对齐的嘛,但是va不要求页对齐这一点一开始没有注意到。包括page_insert函数,现在回看感觉函数说明也是略有瑕疵。)
我一开始直接写的是memset(va,0,PGSIZE);那就显然不对啦,应该改成:
memset(page2kva§,0,PGSIZE);不过还是方法1比较方便咯。

在这里插入图片描述

sys_page_map: 将srcenvid 的srcva所在物理页映射到dstenvid的dstva所在虚拟页,权限为perm,借助page_lookup和page_insert。

在这里插入图片描述

sys_page_unmap: 解除映射,借助page_remove。
在这里插入图片描述

然后在syscall中加上对应的函数:
在这里插入图片描述

至于修改inc/syscall.h中的参数enum还有lib/syscall.c中的接口,助教已经帮忙做好。

这样练习7也做完了,整个PartA也结束了。(这么多只有5分??~)

8

在这里插入图片描述

加一个系统调用,还是不要忘了在syscall中加上这个case(不贴图了)
sys_env_set_pgfault_upcall: 设置一个环境特有的缺页中断处理函数
在这里插入图片描述

这里用到的缺页错误处理函数属于环境本身,要在inc/env.h中添加一项:

在这里插入图片描述

9

在这里插入图片描述

回顾inc/memlayout.h中的虚拟地址空间图,橙色框:向下增长的普通用户栈和数据段以及向上增长的堆空间(用户程序主要所在,占据接近一半的虚拟地址空间(接近2G)); 红色框:是用户异常栈(一页大小,用于用户定义的异常处理函数)。
图中可以解答异常栈溢出的问题,异常栈下面还有一页大小的empty memory,当异常栈溢出时会导致系统层面的缺页,这会最终引发panic,所以不需要特别考虑溢出的情况。
在这里插入图片描述

然后在kern/trap.c中修改page_fault_handler,如果是用户程序的缺页错误,看一看它有没有定义自己的缺页处理函数,如果有的话就在其异常栈上构造栈帧,并且将控制转移过去。
这里使用的栈帧是结构UTrapframe (inc/trap.h)中定义的,保存错误信息,和Trapframe结构很类似。

还要考虑递归的情况:如果在异常栈上处理缺页的过程中发生了缺页,就要在当前异常栈下继续构建出一个栈帧,这里提示中要求说要空出32bit(1个word)的栈空间:
在这里插入图片描述

原因:后面的pfentry中的汇编码会有解释。
在这里插入图片描述
在这里插入图片描述

10、11

在这里插入图片描述

需要编写汇编指令,功能是:调用C语言的缺页处理函数,并在处理结束后回到被中断的位置。有点抽象,先看下一个练习会更好的体会一下这个汇编指令段的作用。
在这里插入图片描述

先上代码:这个就是给用户使用的设置处理函数的顶层封装,注意如果是第一次设置要分配一页作为用户异常栈。
在这里插入图片描述

调用练习8所写的sys_env_set_pgfault_upcall函数,注意到设置的处理函数并不直接是传进来的参数——C语言处理函数,而是_pgfault_upcall,即练习10中需要完成的汇编代码段。(不能直接使用C函数的原因:这个处理函数的返回涉及栈的切换等操作,并不是一般的C语言程序return的规范,需要汇编代码支持。)

设置完之后,在用户程序发生缺页错误时,根据练习9中kern/trap.c/page_fault_handler中的执行逻辑,会去执行_pgfault_upcall——汇编段的代码。通过设置eip,如下:
在这里插入图片描述

明白了功能需求,再回来看练习10中汇编代码怎么写。
我们需要写的是在缺页错误处理结束之后,返回错误之前的状态。乍一看,需要做的仅是恢复所有寄存器的状态,这里恢复寄存器包括eip和esp,所以也暗含了恢复错误处执行的代码和当时的栈。但是仔细一想会发现这样两个问题:
(1)在从用户异常栈恢复寄存器esp到用户普通栈后(递归的情况是到较浅的异常栈),就难以引用到之前的栈空间,这就需要之前关于异常栈的部分已经处理好了。这个问题在我们的UTrapframe格式似乎解决方法是显然的,esp在栈帧的底部(空间上的顶部),只需要在已经弹出其它所有寄存器后,直接popl %esp就好了。一举两得,将异常栈中用于本次处理的栈帧清空了,也恢复了esp。但是这样又会带来新的问题(2)。
(2)前面的方案中要求esp是最后被弹出的寄存器,换言之eip在之前已经被弹出,在eip被弹出时控制实际上就转移回用户normal程序(或更浅层的异常处理),后面能继续进行本身就是矛盾的。

有一种这样的解决方案:在normal stack中压入trap-time的eip,还如同上面一样依次弹出寄存器,只不过直接跳过eip,最后弹出esp并切换到normal stack。此时normal stack比起trap-time只多了32bit的eip在栈顶,正好不就是ret指令需要的格式吗?
过程如下图,1,2,3三步。
在这里插入图片描述

看到这里就明白了前面递归异常时空出32bit的用心良苦了!为了盛放这个trap-time eip。

具体看代码和注释:
在这里插入图片描述

完成了练习8~11,本次实验第二大块——环境特有的缺页错误处理,也完成了。

12

在这里插入图片描述

这个是关于用户程序创建子进程的,按照写时复制的规则,之前在练习7中写的几个系统调用和8~11的缺页处理在这里会起作用。
先看pgfault,这个就是前面练习说的用户的定义的缺页错误处理函数,比较简单,思路:
(1)缺页发生的地址是addr
(2)分配一个物理页,将addr所在页已有的内容复制进来
(3)将addr所在页映射到新的物理页。
这里用到了全局标志uvpt(在lib/entry.S中定义),用来像数组一样引用虚拟地址空间UVPT之上的区域,UVPT的作用在内存管理lab中已经详述。这个函数里只需要知道,uvpt是pte_t的数组,所以uvpt[pn]就是虚拟地址第pn页对应的页表条目就可以了。
entry.S中uvpt等一些全局标志的定义:
在这里插入图片描述
在这里插入图片描述

由于C语言只能使用虚拟地址引用内存,这里用到一个技巧, 先用一个预留好的PFTEMP虚拟地址去映射刚分配出的物理页,然后完成从虚存addr到PFTEMP的复制,再将addr映射过去就可以,PFTEMP的位置在这里:
在这里插入图片描述

然后是复制页映射的函数duppage(envid,pn),作用是将当前环境虚拟地址中第pn页的映射复制到环境envid中,会被下面写的fork调用。对于只读的页只需要复制映射;对于可写的或者已经是COW(写时复制)的页,需要在复制映射的同时标记当前环境和目标环境的页表中这一页为COW,这样在将来两个环境中写这一页时,会触发缺页错误并复制这一页,防止由于写共享页导致的错误。
在这里插入图片描述

这里标记cow的顺序必须是先子进程再父进程,原因//**

然后是fork,作用自然就是创建一个子进程。
(1)首先用之前写好的函数设置缺页错误处理函数;
(2)然后调用sys_exofork创建一个最初的不能运行的子进程。这里看一下inc/lib.h中的sys_exofork函数:
在这里插入图片描述

可以看到是通过系统调用陷入内核执行之前在kern中写的sys_exofork了,为什么需要是内联函数?还没懂,会了再来补充//**
需要再回过去看一下kern/syscall.c中的sys_exofork,再粘一遍代码:
在这里插入图片描述

可以看到,倒数第三行复制了当前状态父进程的寄存器信息到子进程中。先搞清楚一点:何谓当前状态?就是父进程在inc/lib.h陷入内核之前的状态,准确的说就是下面这一句话之前的状态:

在这里插入图片描述

那么当有一个时刻子进程或父进程分别从内核中“醒来”,它们能看到还是只有寄存器状态,(包括栈寄存器esp和代码寄存器eip),所以它们还是会沿着eip指向的代码段继续运行下去。这个过程对于父进程来说是显然的,它就像什么都没有发生一样会沿着之前中断的代码继续运行下去;对于子进程,它就产生了之前也执行过父亲代码的幻觉,沿着前面所说的“当前状态”继续运行下去,也就是该从sys_exofork中返回了:
在这里插入图片描述

所以为了让上层的C语言函数中能区分是父进程还是子进程,倒数两行表示sys_exofork在子进程和父进程中的返回值是不相同的,对于父进程就是return,对于子进程就是设置tf中的寄存器eax,代表返回值。可见:父进程返回子进程id,子进程返回0。
在这里插入图片描述

(3)
接下来根据的代码就会有两个进程执行它了,所以要分开考虑。
对于父进程:
a. 将所有已经用过的页在子进程中复制映射,调用之前写的duppage。这里注意要限制是USTACKTOP以下的范围,不包括异常栈。
b. 然后提前为子进程分配好专门的物理页用于映射子进程的异常栈。直接用sys_page_alloc,不用考虑unmap的问题,因为a中异常栈的映射没有被复制。为什么异常栈不是COW的?很简单,如果连异常栈都没有,也没法处理COW带来的缺页问题了。
c. 设置缺页处理的入口,调用sys_env_set_pgfault_upcall
d. 将子进程状态设置为可以运行,之后子进程就可以被调度了。

对于子进程:修改全局的thisenv指针。
在这里插入图片描述
在这里插入图片描述

(4)最后返回sonID,和sys_exofork一样,还是用父子不同的返回值帮助更上层的用户程序区分父子进程。

到这里PartB就完成了。

13

在这里插入图片描述

PartC的第一个练习,主要是通过时钟中断的方式实现抢占式调度。
首先设置中断描述符表IDT,在kern/trapentry.S和kern/trap.c中做对应修改,这一部分和进程lab过程一样,不赘述:
trapentry.S

在这里插入图片描述

trap.c声明并设置
在这里插入图片描述
在这里插入图片描述

然后,在env_alloc中设置flag以允许外中断:
在这里插入图片描述

同时在cpu进入闲置状态的时候也需要设置为允许外中断,用到的是STI指令,全称为Set Interupt,该指令的作用是允许中断发生。在kern/sched.c中sched_halt中:
在这里插入图片描述

14

在这里插入图片描述

在trap_dispatch中添加下面这种情况,由于之前的进程时间片用完,这里应该调度新的进程抢占之前的。按照提示要先调用lapic_eoi//**
在这里插入图片描述

15

在这里插入图片描述

最后一个练习是实现环境间通信(IPC),需要实现内核(kern)和库(lib)层面的发送接收共4个函数,有值传递和页面传递两种方式。
先看kern/syscall.c中的两个,接收和发送消息的系统调用。

sys_ipc_try_send,作用是给目的环境envid发送消息,具体而言包括这些操作:
在这里插入图片描述

如果srcva是有效值,说明是一个页传递,还需要将srcva对应的物理页映射到目的环境envid等待页的虚拟地址dstva处,同时设置权限为perm。
在这里插入图片描述

还涉及一系列参数检查,见下:
注意这个系统调用如其名字只是“try”一次,如果参数错误或者失败,就直接返回了。在真正的消息传递中,消息传递如果因为接收者环境还没有进入接收状态而失败,发送者是应该继续发送的,这个操作是由上层的库函数完成的,后面会写到。
在这里插入图片描述

sys_ipc_recv:
接收信息的系统调用,主要作用是设置等待标志,然后放弃cpu。
在这里插入图片描述

还是不要忘了加两个case,系统调用库里面的接口助教已经写好不用管:
在这里插入图片描述

然后是lib/ipc.c中的两个函数。
ipc_recv:调用刚才的sys_ipc_recv,没什么好说的。
在这里插入图片描述

ipc_send:反复调用刚才所写的sys_ipc_try_send,直到结果被收到。如果是E_IPC_NOT_RECV之外的错误,就直接panic了,否则通过sys_yield这个系统调用继续休眠。(sys_yield实际上就是进行sched_yield的系统调用。)

在这里插入图片描述

到此15个练习就完成了。

make grade 截图:

在这里插入图片描述

补充题目:优先级调度算法
首先在inc/env.h中加上这个环境的优先级这一项:

在这里插入图片描述

在sched.c中添加优先级调度,我的思路是:先把当前环境状态设置为runnable,然后从所有环境里面找优先级最高的。这样假如优先级最高的环境调用yield,它会继续执行下去。
在这里插入图片描述

添加这种调度方式的系统调用,添加系统调用的过程同前面一样,略过。

又添加了一个系统调用sys_set_priority,允许用户设置优先级(其实还涉及权限问题,但是我在测试的时候就先允许用户设置了)

在yield.c中将调度方式改成优先级:
在这里插入图片描述

然后在kern/init.c/i386_init中新建3个用户yield环境,它们的优先级分别为1,2,3。

注意这样还没有完,这里不要忘了,之前我们还添加了时钟中断,在时钟中断的处理中需要使用调度函数,也需要对应修改!之前这一点忘了,改了20min。
kern/trap.c/trap_dispatch这里:

在这里插入图片描述

然后就make qemu(默认单核)测试以下,输出如下:
在这里插入图片描述
在这里插入图片描述

可以看到是优先级最高的第三个进程最先完成,然后第二个、第一个依次完成,符合优先级调度的要求。

问题回答

(1) 详细描述 JOS 启动多个 APs(Application Processors)的过程。

这个问题在练习1-4的回顾中正好已经详述,这里再把前面的内容粘贴在此:

流程图
在这里插入图片描述

这是整个初始化过程中lab4修改的部分的流程图。
bootcpu是刚开始用来引导的cpu,它的初始化过程就是之前我们一直在写的——上图中的主线部分,具体哪些地方做了修改已经标记出来了。
AP(application processor)是其它的cpu,主要通过bootcpu的boot_aps引导,之后会在mp_main中初始化,也用到了练习4的trap_init_percpu这个函数,也标出来了。

然后特别去看一下新加进来的mp_init函数,这个函数是根据在一开始(这里比较模糊,应该是在BIOS阶段)就已经装载好到内存的MP Floating Pointer Structure,检索到当前硬件环境中的处理器,并完成一些系统全局的、关于多处理器最基本的变量的初始化。这个函数不特别详细的写了,主要是找一下后面会用到的一些全局变量从哪里来的,看kern/mpconfig.c中定义的全局量:
在这里插入图片描述

分别表示所有的cpu和 bootcpu是哪个,是这么个关系:
在这里插入图片描述

然后看一下mp_init中的关键操作,用红线标出来了:
在这里插入图片描述

mpconfig就是根据IntelMp 4 的规范在内存的特定位置找mp的“信息表”(并校验),然后根据信息表,将找到的处理器加入cpus数组,如果是bootcpu,就设置bootcpu指针。
关于这里的“信息表”,其实有两个数据结构:MP Floating Pointer 和MP Configuration Table(MP配置表),这部分内容参考:
https://blog.csdn.net/stupid_haiou/article/details/46430749

在一系列引导核的初始化结束之后,会调用boot_aps引导其它cpu:

在这里插入图片描述

之后,其它AP会执行mp_main,完成独有的初始化,然后就可以yield调度环境进来执行:
在这里插入图片描述

(2) 详细描述:

a) 在 JOS 中,执行 COW(Copy-On-Write)fork 时,用户程序依次执行了哪些步骤?这些步骤包含了哪些系统调用?

(1)首先用之前写好的函数设置缺页错误处理函数;
(2)然后调用sys_exofork创建一个最初的不能运行的子进程。看一下inc/lib.h中的sys_exofork函数:
在这里插入图片描述

可以看到是通过系统调用陷入内核执行之前在kern中写的sys_exofork了,为什么需要是内联函数?还没懂,会了再来补充//**
需要再回过去看一下kern/syscall.c中的sys_exofork,再粘一遍代码:
在这里插入图片描述

可以看到,倒数第三行复制了当前状态父进程的寄存器信息到子进程中。先搞清楚一点:何谓当前状态?就是父进程在inc/lib.h陷入内核之前的状态,准确的说就是下面这一句话之前的状态:
在这里插入图片描述

那么当有一个时刻子进程或父进程分别从内核中“醒来”,它们能看到还是只有寄存器状态,(包括栈寄存器esp和代码寄存器eip),所以它们还是会沿着eip指向的代码段继续运行下去。这个过程对于父进程来说是显然的,它就像什么都没有发生一样会沿着之前中断的代码继续运行下去;对于子进程,它就产生了之前也执行过父亲代码的幻觉,沿着前面所说的“当前状态”继续运行下去,也就是该从sys_exofork中返回了:
在这里插入图片描述

所以为了让上层的C语言函数中能区分是父进程还是子进程,倒数两行表示sys_exofork在子进程和父进程中的返回值是不相同的,对于父进程就是return,对于子进程就是设置tf中的寄存器eax,代表返回值。可见:父进程返回子进程id,子进程返回0。
在这里插入图片描述

(3)
接下来根据的代码就会有两个进程执行它了,所以要分开考虑。
对于父进程:
a. 将所有已经用过的页在子进程中复制映射,调用之前写的duppage。这里注意要限制是USTACKTOP以下的范围,不包括异常栈。
b. 然后提前为子进程分配好专门的物理页用于映射子进程的异常栈。直接用sys_page_alloc,不用考虑unmap的问题,因为a中异常栈的映射没有被复制。为什么异常栈不是COW的?很简单,如果连异常栈都没有,也没法处理COW带来的缺页问题了。
c. 设置缺页处理的入口,调用sys_env_set_pgfault_upcall
d. 将子进程状态设置为可以运行,之后子进程就可以被调度了。

对于子进程:修改全局的thisenv指针。
在这里插入图片描述
在这里插入图片描述

(4)最后返回sonID,和sys_exofork一样,还是用父子不同的返回值帮助更上层的用户程序区分父子进程。

b) 当进程发生 COW 相关的 page fault 时,这个中断是被如何处理的?其中哪些步骤

在内核中,哪些步骤在用户空间中?

处理过程:
(1)首先通过trap陷入内核,经trap_dispatch分配调用缺页处理page_fault_handle
,如果有用户定义的处理程序,会通过下面的过程切换用户环境到用户异常栈上执行,具体过程不再重复(前面对应部分已经写过了)
在这里插入图片描述

(2)然后通过pfentry.S中的_pgfault_upcall,将控制转移到用户定义的全局_pgfault_handler句柄,执行用户的定义的代码。

(3)处理完成后,还是通过_pgfault_upcall,直接从异常栈回到normal栈,并恢复之前错误发生时的现场,不需要经过内核。
在这里插入图片描述

整个过程中,只有(1)中的trap部分在内核中,其它(2)(3)中的部分全在用户态下进行。

(3) user/primes.c 这段代码非常有趣,请详细解释一下这段代码是如何执行的,画出代码

流程图,并指出所谓的“素数”体现在哪里?
这个测试的执行逻辑见下面的伪代码:
在这里插入图片描述

是通过进程通信的方式生成前若干个质数,具体方式为对于每一个进程:
它从左边进程接收到的最小的数p,可以确定p是质数;
对于从左边进程接收到的其它数q,都用p去除,如果可以除尽,说明q不是质数,删除之,否则,传递给右边的进程。

这么做的正确性在于:
一方面,对于每个质数p,由于所有比p小的数都不整除p,所以没有进程会“删除”p,所以p将会在某一个进程中成为最小数,继而被打印;
另一方面,对于每个进程收到的最小数p,可以归纳得出左边所有进程的最小数是不能整除p的,进而推出比p小的所有质数都不能整除p,所以p是质数。

流程图:
在这里插入图片描述

摘自:https://swtch.com/~rsc/thread/

运行结果:
在这里插入图片描述

。。。

在这里插入图片描述

可以看出,由于共有id为0x1002到0x13ff共1022个进程可以用,所以这些进程依次打印出前1022个质数。

至此本次lab就结束了。

**注:代码中调度算法选择的是轮转调度,如果需要测试yield.c中优先级调度,需要修改trap_dispatch中的调度算法为:sched_yield_priority

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值