JOS-lab4

来源:互联网 发布:360比价软件 手机 编辑:程序博客网 时间:2024/05/18 02:56

Jos-Lab4 设计文档


By Ddnirvana

本次Lab4分为三个部分,下面也按照这个结构进行lab实现过程的说明。

Part A: Multiprocessor Support and Cooperative Multitasking

PART A是整个lab4的基础,主要就是实现多处理器的支持。并且在PART A里面不怎么好测试,而且之前LAB中的BUG全部坑在这部分了,所以还是小麻烦的==||
对PART A中的任务进行总结如下

  • page_init(): avoid adding the page at MPENTRY_PADDR to the free list
  • mem_init_mp(): map per-CPU stacks starting
  • trap_init_percpu(): modify to support multi-cpu
  • apply lock_kernel() and unlock_kernel(): apply the big kernel lock
  • spinlock: implement ticket spinlock
  • sched_yield():implement round robing scheduling and apply it to syscall
  • syscall for environment creation:implement a series of syscalls to achieve create a new environment

下面依次对上面的内容进行说明。

page_init

在初始化free page list的时候要排除一些page,主要是为了使得其他AP的启动代码可以放在这些page上。
具体的实现很简单,就是在加入free page list的时候进行一些判断。
大致代码如下:
这里写图片描述
可以看到其实就多加了一个continue的处理。

mem_init_mp()

就是为每个CPU分配单独的kernel stack。每个stack的大小为KSTKSIZE,并且stack之间还有KSTKGAP大小的空间作为一个中间部分,根据其实地址和两个大小计算要分配的空间即可。具体实现里面稍微改一下之前的分配stack的代码就可以了。
大致代码如下:
这里写图片描述

trap_init_percpu()

这里主要是修改来支持多cpu的情况。具体来说,每个cpu的trap的时候进入的内核栈是不同的,还有gdt表中的值也会不一样。将之前的ts改成根据当前cpu获取到的taskstate,然后计算相应的值,包括当前的内核栈地址,一些gdt的偏移的计算等。
大致代码如下:
这里写图片描述

kernel lock

kernel lock的机制是为了保证在同一时刻,只有一个cpu是在内核态执行的。
然后lab4的说明中给出的需要处理lock kernel的地方大概有:
i386_init() mp_main() trap() env_run()

基本上就是一个原则,进入kernel前要lock,进入用户态前要unlock。

然后其实除了上面这些之外,syscall的地方其实也是要处理lock的,因为syscall也会从用户态进入内核态,并且由于jos还支持sysenter的方式执行syscall,其实syscall完全是可以绕卡trap执行的。

由于这部分主要就是加锁和放锁,所以代码其实没有什么好展示的,针对每个处理的地方说明一下为何要这样处理。
i386_init():在启动其他cpu之前要lock_kernel,因为当前其实是在内核态的,而且env也还没有创建,所以为了防止其他核起来之后直接就去执行了要先拿着锁。
mp_main: AP在进入sched前,必然是要拿锁的,因为进sched就意味着开始进kernel执行了,不然所有的AP就一起进去了。
trap syscall:这两个地方都是应用程序在执行的时候进入kernel的方式,也进行相应的判断然后去拿锁。
env_run: 这里是从kernel进入用户态去执行用户程序,所以要放锁。

ticket spinlock

这里就是实现一种spinlock而已。ticket spinlock的核心点在于每个拿锁的人拿到一个ticket,然后放锁的时候当前ticket值会增加,然后等在下一个值上的人就能拿到锁了。
具体的实现如下:
拿锁:
这里写图片描述
放锁:
这里写图片描述

Round-Robin Scheduling

这一块主要是要实现一个轮询的调度方法,特别是在支持多env之后,必然存在一个调度的过程。
轮询调度算法的实现其实很简单,主要就是依次查看是否可以运行,如果可以的话就让他去跑了。
要注意的就是边界情况下,当前去调度的时候该CPU有可能是没有env在跑的。
大致代码如下:
这里写图片描述

Environment creation

这里其实就是实现一个比较原始的fork,不过并不是直接提供一个fork 的syscall,而是提供一系列相关的小的syscall,然后通过这些小的syscall来在用户态实现一个fork的功能。

具体的设计的syscall如下:

sys_exofork

这个syscall的功能很简单,就是创建一个近乎空白的environment了。不过,比较有趣的一点就是在exofork之后其实是有两个返回值的,对于父亲来说返回值就是儿子的envid,对于儿子来说返回值就是0。

具体实现这两个返回值的方法就在于,对于创建出来的env,将他的eax设为0.这样当它开始执行时就会将这个值作为返回值了。然后就可以实现有两个返回值的效果了。

参考代码如下:
这里写图片描述

sys_env_set_status

这个函数就是用来设置envroment状态的了,比如执行和不执行,没有什么特别多的内容不多说。
具体见参考代码如下:
这里写图片描述

sys_page_alloc

分配一个page在给定的env的对应的虚拟地址中。

具体的实现的关键步骤在于:创建一个page,然后page_Insert到指定的env的页表中。其中需要注意的是进行一些权限的检查。

具体见参考代码:
这里写图片描述

sys_page_map

这个函数可以将src中的一个page映射到dst中去。

核心思路其实和上面的函数比较像,查找src中对应的page,然后page_insert到dst的页表中去。

同样要注意权限的检查,具体见参考代码:
这里写图片描述

sys_page_unmap

就是取消一个page的映射。调用一下pmap中的remove函数即可。
具体见参考代码:
这里写图片描述

Copy-on-Write Fork

第二部分主要是实现一个copy-on-write的fork,中间还包括在用户态注册函数进行page fault的处理等。

具体的任务包括:

  • sys_env_set_pgfault_upcall:syscall to set pafault handler
  • page_fault_handler: page fault handler in trap
  • _pgfault_upcall
  • set_pgfault_handler()
  • fork
  • duppage
  • pgfault

下面分别具体来阐述:

Setting the Page Fault Handler

要使得可以调用用户自己实现的page fault handler,我们需要先注册这个函数。这里会在每个env中多加一位,来保存当前env中注册的page falut handler函数。
具体的实现很简单,基本上就是一个赋值操作,具体如下:
这里写图片描述

Invoking the User Page Fault Handler

修改trap中的page fault handler,使得支持使用用户态实现的handler。
那么在trap中,首先要判断是否注册了用户自己定义的page fault handler,如果定义了的话,我们将处理转交给用户定义的函数。
具体来说主要涉及两个操作,一个是更换栈,一个是更换eip。

栈的话是要uxstack上面进行操作,并且要根据当前的栈的位置判断出是否之前是在page fault handler中触发的中断然后进行响应的处理。

另一部分就是将执行流的ip执行用户定义的函数的执行代码中去。

除此之外还需要的操作包括保存错误状态等。具体见参考代码:
这里写图片描述

User-mode Page Fault Entrypoint

要实现一个用户态调用的page fault handler的入口函数。
这里的主要是使用汇编代码,然后要负责将程序流引向真正的处理函数。比较重要的是要在处理好之后回复整个env的状态,使得env重新执行一遍触发page fault的指令。这里需要对于还原各个寄存器的值的顺序做好调整,包括栈上面的一些控制。

参考代码如下:
这里写图片描述

还要实现一个在用户态的注册函数,主要就是调用之前的系统调用了,这里也不再赘述。

Implementing Copy-on-Write Fork

实现了前面的在用户态处理page fault之后,我们终于可以进入我们的处理cow fork了。
这里要实现的函数主要是三个 fork duppage pgfault

duppage

这个函数的主要任务就是为parent和child的enviroment设置page,具体来说就是将parent页表中的页映射到child的页表中,同时设置自己和child的页表中对应的页的COW位。
修改自己的页表项的位可以使用sys_page_map自己,并且设置好权限,这样就会重新设置一遍权限了。
具体见参考代码:
这里写图片描述

pgfault

在使用COW fork之后,当fork之后子environment或者父environment需要写page的时候,就会触发page fault。那么具体的处理就是通过这个函数来实现的。这个函数的任务就是在触发page fault之后,通过判断触发的page的位来判断出是否是COW的,如果是,那么就alloc一个page,然后把cow 的page中的内容拷过来。
要注意的时候触发page fault的往往是一个具体的虚拟地址,当时其实它是在一个具体的page中的,所以在这里复制的时候是直接复制整个page的。
具体见参考代码:
这里写图片描述

fork

上面的工作主要就是做一些准备了,那么在真正的fork中也就是应用之前的一些实现基本上就好了。
说一下具体的流程:
设置当前env的pgfault函数为之前实现的处理cow的page fault函数。
sys_exofork得到一个child,使用dumpage映射page。
将child的pgfault也设置为此前实现的pgfault函数。
好了…………
具体见参考代码:
这里写图片描述

Preemptive Multitasking and Inter-Process communication (IPC)

最后一部分是抢占调度和IPC的实现了。
因为东西不多所以不列表里……

Enable IRQ

这一块的任务主要就是注册IRQ函数(在trap.c和trapentry.S中),然后在一些位置设置一些FL_IF的eflags值。

trapentry里面的函数定义大概是这样的:
这里写图片描述
其实没有什么东西==|||用宏处理一下就好了。
然后再trap_init里面注册到IDT表中。

至于FL_IF,是负责中断的一个flag,在env创建的时候把它设上。
这里写图片描述

Handling Clock Interrupts

这部分要实现抢占调度,具体的策略就是用时钟中断来处理的。
当trap中收到一个timer的中断时,然后处理方法就是进入schedule调度一下,这样一来就实现了一个抢占调度(==|||)
IRQ是已经打开了,所以在trap中把clock的处理函数写写就好了。
这里写图片描述

Inter-Process communication (IPC)

终于到最后一块了……
这里实现的是一个jos中的ipc,说实话也是很简陋的……
既然是IPC,也就是要发消息了,jos中的ipc主要可以发两种消息,一个就是一个数值,另一个就是可以传送一个page。
然后实现的函数具体来说是4个,分别是recv和send的syscall和用户态的部分。

sys_ipc_try_send

为了实现ipc,env在这里多了几个成员,其中和传送消息比较相关的是env_ipc_value env_ipc_dstva,其中ipc_value主要放传送的数值,ipc_dstva则存放接收方准备接受page的地址。
那么具体来说就是先获取到recevier的env,然后检测一下该env是否处于等待接收消息的状态;然后判断receiver的ipc_dstva和自己的srcva的范围和权限是否合法,如果没有问题则将两个ipc_dstva的虚拟地址映射到srcva的物理地址上。
最后是设置ipc_value,设置发送者的id,然后将receiver的状态设为可运行。
这里写图片描述

###sys_ipc_recv
相比send的,recv的部分的逻辑则要简单得多,主要就是设置当前状态为等待接收信息,然后设置好共享的地址,进入not_runnable的模式即可。
这里写图片描述

ipc_recv

用户态的recv,主要就是对syscall的包装。
这里要注意的是如果不需要共享page,不能将地址设为NULL(也就是0),因为这里也会被当成可以共享的page的,最好将地址设到utop上面就好了。
代码见下:
这里写图片描述

ipc_send

和recv差不多,基本上就是对syscall的包装,同样要注意的是如果不共享page的话应该是将page的地址设置到utop上面。
同时要求里面说的要循环去掉,其实我觉得好像是不需要的……


好吧……这样就完了……

0 0