操作系统 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

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

  1. VMware安装Centos7和卸载超详细过程(图文)

    下载Centos7镜像 下载地址 提供几个下载Linux镜像的网站,这里我下载的是Centos7.9版本的系统,内核版本为3.10.0版本 国内外镜像源: 阿里云:http://mirrors.ali ...

  2. 电脑启动过程(超详细过程)

    Linux启动过程(超详细过程) 一.前言 二.启动过程概述 三.加电自检及初始化 四.主引导记录 五.加载kernel 六.加载init 致谢 一.前言 我开始的时候写了一篇关于Linux启动过程的 ...

  3. VMware安装Centos7超详细过程(图文)

    原文:https://www.jianshu.com/p/ce08cdbc4ddb?utm_source=tuicool&utm_medium=referral 本篇文章主要介绍了VMware ...

  4. 使用Matlab工具箱(procamcalib)进行投影仪标定---超详细过程

    使用Matlab工具箱(procamcalib)进行投影仪标定-超详细过程 一 .procamcalib工具箱应用场景 同于相机标定的方法和步骤,投影仪的原理通常被看作是相机成像的逆过程,网上关于相机 ...

  5. 【Linux】CentOS7下安装Ngnix代理服务器详细过程 附Linux 64位 Ngnix压缩包百度云盘分享

    [Linux]CentOS7下安装Ngnix代理服务器详细过程 附Linux 64位 Ngnix压缩包百度云盘分享 Ngnix基本概况 Nginx (读作"engine X") 由 ...

  6. SVM支持向量机 超详细过程讲解

    SVM支持向量机 超详细过程讲解 前言 一.线性模型 前言 此篇文章为B站浙大机器学习课程支持向量机部分的个人笔记,不喜勿喷.笔记顺序从线性模型到非线性模型,层层递进,十分易懂. 一.线性模型 先从一 ...

  7. 【PyTorch】构造VGG19网络进行本地图片分类(超详细过程)——项目介绍

    本篇博客主要解决以下3个问题: 如何自定义网络(以VGG19为例). 如何自建数据集并加载至模型中. 如何使用自定义数据训练自定义模型. 第一篇:[PyTorch]构造VGG19网络进行本地图片分类( ...

  8. 手把手教学——记录在Winxp虚拟机上安装Vxworks操作系统及其编译器Tornado的详细过程(1——WinXP操作系统)

    (Tornado2.2/Vxworks5.5.1 for Pentium)引言 本文实现了在Windows XP系统(基于虚拟机)上安装Tornado2.2,并且通过在虚拟机上安装虚拟机的方式安装Vx ...

  9. Windows10+YOLOV3+VisualStudio2017最新版本超详细过程

    最近两天在看yolo项目,所以想着把作者的项目copy一下运行看一下效果,谁知道一不小心,陷入坑中无法自拔.真实感叹作者的厉害之处. 同时也记录了自己Windows10+YOLOV3+VisualSt ...

最新文章

  1. phpstudy多站点配置好后index of/ 列表无法出现的解决
  2. anaconda创建环境
  3. 视频导切台控制说明:RGBLink MiniPro初步测试
  4. C语言按要求打印数组
  5. win32thread.c:(.text+0x60):对‘_beginthreadex’未定义的引用
  6. BZOJ 4174 tty的求助 莫比乌斯反演
  7. java重新执行_(转载)java线程 - 线程唤醒后并被执行时,是在上次阻塞的代码行重新往下执行,而不是从头开始执行...
  8. Ubuntu 16.04 LTS安装Docker并使用加速器
  9. java 二叉树 遍历_JAVA实现二叉树(简易版--实现了二叉树的各种遍历)
  10. GIS案例练习-----------第一天
  11. 晕晕沉沉的一天,ISAPI_Rewrite 2.9破解版竟然是假的
  12. php文章下一页,php获取文章上一页与下一页的方法_php技巧
  13. 使用LSV进行通视分析教程
  14. 二叉树的后序遍历(递归和非递归)
  15. 网站域名解析为什么错误?域名解析错误怎么解决?
  16. Spring实战笔记——(1)Spring之旅(上)
  17. ubuntu在目录下文件中搜索关键字
  18. 2011.4.5 凌晨 3:50分
  19. 常见电路、元器件汇总
  20. access令两列运算得到新属性_iOS版更预告 | 新玩法新精灵来袭开启你的全新旅程...

热门文章

  1. 单片机设计_电子小说阅读器(STM32 LCD触屏)
  2. SAP之VM创建SUSE虚拟机
  3. 微信公众号最佳实践 ( 8.2)星座运势
  4. iframe用法精析
  5. 用Photoshop做圆角图片
  6. 织梦mysql安装教程视频教程_dedecms(织梦网)安装教程
  7. 通过Kali利用serv-u漏洞获取win7系统管理员权限
  8. 热血江湖游进服务器显示更新,热血江湖一直进不去 无法进入游戏解决方法
  9. 高通手机型号、开机logo、默认语言设置等小修改
  10. 中班音乐计算机创编,中班音乐:碰碰船