加深对进程调度的直观认识; 掌握xv6操作系统中调度代码的编码实现方法; 掌握xv6操作系统中信号量的编码实现; 可以使用Linux+Qemu仿真环境; 修改xv6内核代码实现基本系统调用; 修改xv6内核代码实现指定的调度方法; 修改xv6内核代码实现进程间同步所需的信号量机制。 硬件:桌面PC 软件:Linux 或其他操作系统 阅读实验辅助材料完成以下操作 操作部分: 1) 基本实验操作:创建Helloworld演示程序并运行,实现任意功能的系统调用;(20%) 2) 调度器实验:(20%) a) 修改xv6调度器的时间片,从1个时钟tick扩展到N个tick才引起一次切换; b) 修改xv6调度器,实现优先级调度; 3) 实现xv6系统上简单的信号量机制;(20%) 4) 实现简单的slab内核内存管理:将K个(例如16个)页帧用作slab目的,slab大小为16、32、64、128、256、512、1024、2048共8中尺寸;提供分配和回收的系统调用;(20%) 5) 为xv6增加copy_on_write的能力,使得创建子进程时,暂时共享父进程映像,直到某一方改写内存时才为子进程分配新的页帧用于保存进程映像。1.给出设计思路;2.分析原有xv6基础之上增加了copy_on_write之后,分析不同进程映像构成和读写访问模式下,那些情况下才有利于系统性能(20%) 实验结果 1 基本实验操作:创建Helloworld演示程序并运行,实现任意功能的系统调用;1.1 首先在xv6目录下创建HelloWorld.c,写入如下代码(图1-1)。 图 1‑1 HelloWorld.c代码 1.2 然后进入Makefile文件,修改UPROGS 变量,新增一个_HelloWorld\(图1-2)。 图 1‑2 修改UPROGS 变量 1.3 Make clean后make,然后进入qemu,运行helloworld,即可看见print我们想要的字符串了(图1-3)。 图 1‑3 helloworld 1.4 接下来,我们实现任意功能的系统调用,首先我们创建一个pcpuid.c的文件,然后写入代码(图1-4),该进程主要是打印cpuid。接着我们在Makefile文件中修改UPROGS变量,在其中添加我们的c文件名(图1-5),然后再syscall.h文件中添加上我们想要添加的系统调用号(图1-6),在user.h中加入我们的函数原型声明(图1-7),在usys.S中定义我们的用户态入口(图1-8)。 图 1‑4 pcpuid.c文件及其代码 图 1‑5 修改Makefile 图 1‑6 添加系统调用号 图 1‑7 加入函数原型声明 图 1‑8 添加用户态入口 1.5 我们接着在syscall.c的大概102行左右的分发函数表加上我们要跳转的函数并且在之前加上声明函数(图1-9),接着我们在sysproc.c中实现sys_getcpuid(图1-10),接着我们在proc.c中实现我们的内核态的getcpuid()(图1-11),最后在defs.h中实现函数声明原型,方便调用(图1-12)。 图 1‑9 修改syscall.c 图 1‑10 在sysproc.c中实现sys_getcpuid 图 1‑11实现getcpuid 图 1‑12 修改defs.h 1.6 可以看到,我们进入qemu后输入pcpuid,就打印了我们的cpuid,所以添加系统调用成功(图1-13)。 图 1‑13 打印cpuid 2 修改xv6调度器的时间片,从1个时钟tick扩展到N个tick才引起一次切换;修改xv6调度器,实现优先级调度;2.1 要修改时间片,基本思路是在每个时间片里添加循环,在每个进程切换之前执行完循环才能切换,所以我们首先要在proc.h定义一个变量标志着时间片,所以在proc.h中添加一个新变量标志着时间片,用于cpu轮转控制(图2-1)。 图 2‑1 修改proc结构体 2.2 我们在procdump函数中添加打印信息显示时间片剩余(图2-2)。 图 2‑2 在procdump中打印时间片剩余 2.3 我们在trap.c中有关时间片切换的条件语句中加入判断,每次想要切换时必须得判断tickk值是否为0,否则tickk—直到为0才切换(图2-3)。 图 2‑3 修改trap.c 2.4 最后,我们写下recycle.c的代码来验证时间片是否改变(图2-4),代码中主要采取循环(图2-4),在我们添加c文件名至Makefile之后,我们打开qemu,输入recycle命令,接着我们按下ctrl+p,就可以看到打印在终端的信息,我们发现剩余时间片一直在变化,说明添加时间片成功(图2-5)。 图 2‑4 recycle.c 图 2‑5 qemu实时反馈结果 2.5 至于优先级调度,遇上题思路类似,在 proc.c中的scheduler调度器中判断谁优先级最高就调谁,我们先是在proc结构体中加一个变量(图2-6),在proc中,我们首先要对每个进程设置初始优先级为10,然后再scheduler中找到当前进程最高优先级并且切换到对应进程中区(图2-7)。 图 2‑6 修改proc结构体 图 2‑7 在proc中设置调度器调度策略 2.6 这时基本设置好了调度器策略,但是测试的话需要比较直观的显示及调整优先级,所以我们必须考虑测试程序的直观显示,或者说可以实时调整进程优先级,所以我们需要创建一个系统调用,例如名叫changepri的系统调用,参数为进程pid和想要修改的priority(图2-8)。 图 2‑8 changepri内核函数实现 2.7 接着,我们在sysproc中实现sys_changepri,其中使用的argint指的是获取参数(图2-9),接下来就是我们熟悉的设置系统调用的步骤(图2-10/2-11/2-12/2-13/2-14)。 图 2‑9 sysproc中实现sys_changepri 图 2‑10 defs添加函数声明 图 2‑11 usys.S添加添加用户态入口 图 2‑12 user.h函数原型声明添加 图 2‑13 syscall.h系统调用号添加 图 2‑14 syscall.c系统调用跳转表添加 2.8 接着我们根据我们写下来的系统调用写出我们的测试程序,程序思路:首先设置一个优先级为19的主进程,然后fork一个子进程,让其先进入分支并且设置优先级为5,这样即使主进程在后一个else分支,根据优先级调度我们任然会有限运行高优先级的主进程(图2-15),此时将Makefile的CPU核数调为1,开始测试即可,我们可以看到,优先级为19的主进程输入100个1之后,优先级为5的子进程才输出100个0(图2-16)。 图 2‑15 pri_changed.c测试程序 图 2‑16 输出结果 3 实现xv6系统上简单的信号量机制;3.1 Xv6没有信号量机制,所以也没有提供共享内存这样的共享资源,所以首先我们要在系统中定义一个共享变量sh_var_for_sem_demo,通过 sh_var_read()和 sh_var_write()进行读写操作。以此作为验证信号量工作正常的功能展示。 3.2 再验证信号量的时候我们需要提供临界资源,所以我们也定义sh_var_for_sem_demo 全局变量于spinlock.h中(图3-1),接着我们在defs.h头文件中添加临界资源声明(图3-2)。 图 3‑1 临界资源设置 图 3‑2 defs.h中设置临界资源变量声明 3.3 为了实现临界资源访问的效果,我们需要设置临界资源读写的系统调用,如图3-3,我们设置sh_var_read以及sh_var_write的系统调用,用于读取全局变量sh_var_for_sem_demo(图3-4/3-5/3-6)。读取的逻辑就是简单的返回全局变量数值,写信号量的逻辑就是根据传进来的参数,设置临界资源值并返回(图3-7)。 图 3‑3 syscall.h中添加系统调用号 图 3‑4 user.h中添加用户态函数原型 图 3‑5 usys.S实现两函数 图 3‑6添加跳转表以及外部声明 图 3‑7 sysproc.c书写信号量代码 3.4 我们到目前为止已经实现了sem信号量的内核设置,于是我们写下sem-test1.c来测试我们的临界资源是否可以正常读取与书写(图3-8),按照代码逻辑,我们结果应该是200000,但是最后不仅子进程和父进程的结果不一样,而且也远远低于20w(图3-9),究其原因,是因为信号量这种临界资源没有加上互斥锁,所以导致两个进程互相覆盖之前所写的值。 图 3‑8 书写sem-test1测试代码 图 3‑9 qemu测试结果 3.5 因此,我们要给信号量添加互斥锁的功能。 3.6 首先我们在spinlock.h中添加关于信号量的结构体sem以及其相关变量(图3-10)。 图 3‑10 sem结构设置 3.7 我们需要初始化信号量,让其自带自旋锁功能,实现互斥访问(图3-11),并且在main.c中添加初始化函数,让qemu启动时在用户进程启动前就自动自带信号量(图3-12),并且在defs中添加初始化声明(图3-13)。此外我们要编写四个系统调用: 3.7.1 Sem_create():创建信号量,返回值是sem[]的数组下标,参数为信号量初值。实现思路是首先获取参数,遍历信号量数组,找到第一个未被分配的信号量,然后分配参数中的值给信号量,期间保证互斥锁实现原子操作(图3-14)。3.7.2 sem_p():对信号量--操作,参数为信号量id,当对应id=0时睡眠队列中对应pid进程,并添加进队列中该进程号,返回0成功,-1失败。实现思路是根据传参的信号量数组下标值,我们将其资源计数--,并且判断资源是否小于0,如果小于就睡眠进程并释放信号量(图3-16)。3.7.3 sem_v():信号量++操作,参数为信号量id,当对应id=0时唤醒队列中进程,并清除队列中该进程号,返回0成功,-1失败。实现思路是首先传参,然后根据信号量数组下标值进行资源计数释放操作(++),然后检查是否资源计数是否被别的进程需要,若有则唤醒等待的一个进程(图3-17)。3.7.4 sem_free():释放指定id信号量。参数为信号量id,返回0成功,-1失败。实现思路是根据传参的信号量下标,我们判断如果其对应的信号量已被分配使用且资源计数大于0,则将其释放,把allocated变成0(图3-15)。图 3‑11 spinlock.c中初始化信号量函数书写 图 3‑12 main中添加初始化信号量函数 图 3‑13 defs添加initsem函数声明 图 3‑14 sysproc实现创建信号量函数 图 3‑15 实现释放信号量函数 图 3‑16 实现信号量--函数 图 3‑17 信号量++函数 3.8 由于wake函数会把所有等待相同资源的进程唤醒,所以我们在proc.c中重新定义一个wakeup1p函数,此函数旨在唤醒特定信号量进程,主要思路就是扫描进程数组,查找信号量对应进程,若其处于睡眠阻塞状态,则将其变成就绪态(图3-18)。 图 3‑18 wake特定信号量进程函数实现 3.9 接着我们进行收尾工作,首先给四个系统调用创建系统调用号(图3-19),然后在usys中声明函数态原型(图3-20),在usys.S中实现系统调用(图3-21),最后在syscall中实现跳转表以及外部声明(图3-22)。 图 3‑19 syscall添加系统调用号 图 3‑20 usys设置函数态原型 图 3‑21 usys实现系统调用 图 3‑22 syscall实现跳转表以及外部引用 3.10 最后我们写下sem-test2.c测试程序,该程序思路是首先创建信号量,然后fork一个子进程,写信号量之前先占用资源计数上锁,写完然后解锁,并且让父进程等待子进程完成后再释放信号量,这样能够保证父子进程增加信号量时不会被覆盖并且也不会产生僵尸进程(图3-23)。 图 3‑23 sem-test2基准测试程序 3.11 我们进入qemu测试可以看到,我们最后的sum为20万,结果正确,信号量添加成功(图3-24)。 图 3‑24 测试结果 4 实现简单的slab内核内存管理:将K个(例如16个)页帧用作slab目的,slab大小为16、32、64、128、256、512、1024、2048共8中尺寸;提供分配和回收的系统调用;4.1 上网查阅资料得知:SLAB分配器专为小内存分配而生,他的第二个任务是维护常用对象的缓存,最后一项任务是提高CPU硬件缓存的利用率。 4.2 根据题目提示,slab大小分为8个尺寸,由于他涉及内存管理,所以我们需要在系统启动的时候申请8页的物理页,8个尺寸总和大约4096字节,也就是一个物理页大小。 4.3 分析过后,我们确定了slab需要首先根据用户传入的size选一个合适大小的slab,然后在8个大小的slab中找到第一个未被使用的slab 对象,然后其状态位设置为已使用,最后将该虚拟地址和该级slab所在页建立映射(图4-1)。 图 4‑1 slab定义以及初始化 4.4 在写slab分配函数之前,我们需要深入了解页表的知识,这需要我们看过vm.c的代码,在写slab时我们要对页表定义其状态标志位,我们要设置成可写并且是用户页(图4-2),对于映射页帧,我们是用xv6中的mappages函数(图4-3),该函数目的在于将我们设置好的slab映射到物理页上面去。 图 4‑2 PTE详解 图 4‑3 mappages函数详解 4.5 最后我们写下slab_alloc函数,此函数旨在实现根据size寻找合适的slab并分配,然后将其映射到对应的物理页中,并返回物理页地址偏移(图4-4)。 图 4‑4 slab_alloc函数实现 4.6 接着我们实现slab_free函数,该函数主要思路是通过slab的虚拟地址映射到内核地址,然后遍历slabs[]数组找到其偏移对应的物理地址,将其状态位设置为0,物理页清空,虚拟地址对应页表清空,解除映射(图4-5). 图 4‑5 slab_free函数实现 4.7 在defs.h中添加三个函数的声明(图4-6),并且在系统启动时,用户进程启动之前加入slab初始化函数于main.c中(图4-7), 图 4‑6 defs添加函数声明 图 4‑7 main.c中初始化slab 4.8 因为slab操作离不开页表的支持,而页表是伴随进程产生的,所以我们进入proc.c中添加对应的调用,分别是创建slab和释放slab(图4-8)。 图 4‑8 分配和释放函数实现 4.9 接着我们将myMalloc以及myFree添加进去系统调用中(图4-9/4-10、4-11/4-12/4-13/4-14)。 图 4‑9 defs添加函数入口 图 4‑10 syscall添加跳转表和外部引用 图 4‑11 syscall添加系统调用号 图 4‑12 sysproc实现内核态函数 图 4‑13 usys添加系统调用 图 4‑14 user中添加用户态入口 4.10 接着我们实现slab的测试程序slabdemo.c,此程序思路是创建4个slab块,每个都比较接近32B,按照我们的设计逻辑,他自然会分配4个32Byte的slab boject(图4-15)。 图 4‑15 slab测试程序 4.11 我们打开qemu,可以看到在启动系统时其打印了slab分配的起始地址(图4-16)。 图 4‑16 slab初始化分配地址 4.12 运行后我们可以分析得到:当创建3个slab object之后,我们把第二个slab object释放掉,然后再添加第四个slab object,此时第四个slab object添加进来,因为每个slab的虚拟地址是间隔4096byte后再加上其size的偏移,但是第四个slab object的物理地址还是映射到了第二个slab object上面(图4-17)。 图 4‑17 运行结果分析 5 为xv6增加copy_on_write的能力,使得创建子进程时,暂时共享父进程映像,直到某一方改写内存时才为子进程分配新的页帧用于保存进程映像。1.给出设计思路;2.分析原有xv6基础之上增加了copy_on_write之后,分析不同进程映像构成和读写访问模式下,那些情况下才有利于系统性能5.1 设计思路: 5.2 首先我们实现内核自旋锁和物理页的引用计数器,就是在kalloc.c中添加一个名叫pageref的数组,让他来记录引用物理页和自旋锁次数(图5-1)。PGSHIFT就是页号的一个取对数(图5-2),PHYSTOP就是页地址的末端(图5-3),PHYSTOP移位PGSHIFT就是计算有多少个页。 图 5‑1 kmem结构体中添加引用计数器 图 5‑2 PGSHIFT定义 图 5‑3 PGSTOP解释 5.3 接着,我们在kalloc.c文件中编写初始化与使用这个数组的函数,使得其他程序方便实用(图5-4)。接着我们在defs.h中添加声明(图5-5),接着我们在kalloc函数中添加我们的初始化代码,使得每个物理页帧对应的计数器初始化为1(图5-6)。 图 5‑4 kalloc添加接口 图 5‑5 defs添加声明 图 5‑6 初始化计数器 5.4 接着我们修改kfree函数,但是这里涉及到两种情况:如果一个物理页引用次数减为0了,那我们需要释放他,如果>0,那我们就不应该释放(图5-7)。 图 5‑7 修改后kfree函数 5.5 我们观察proc.c中的fork函数,我们发现子进程copy父进程页表使用了copyuvm函数来copy(图5-8),所以我们根据copyuvm来仿照写一个copyuvm_write,不同的是我们对于子进程前三个页,直接复制,其他页则将父进程的物理地址映射上去,并且添加引用计数器(图5-9)。 图 5‑8 copyuvm使用 图 5‑9 copyuvm_onwrite代码 5.6 紧接着我们将完成的copyuvm_onwrite函数加入到defs头文件中(图5-10),然后再proc.c中实现一个myFork函数,这个函数跟Fork函数的唯一区别在于将copyuvm换成了copyuvm_onwrite(图5-11),然后图5-12至图5-16就是实现他们系统调用的过程。 图 5‑10 defs添加引用 图 5‑11 myFork函数代码 图 5‑12 defs添加引用 图 5‑13 sysproc添加用户态接口 图 5‑14 syscall添加系统调用号 图 5‑15 syscall添加跳转表以及外部引用 图 5‑16 usys添加系统调用 5.7 最后我们书写一个测试程序来实现这个copyonwrite功能,父进程首先打印其信息,然后等待子进程完成,子进程会更改信息然后打印,父进程最后打印查看是否已经更改信息(图5-17)。若更改则证明父子共享一个页表,说明实现了copy_on_write。 图 5‑17 cowdemo.c测试程序 5.8 我们运行测试程序,可以发现父进程首先缓冲区为A,子进程覆盖写了B之后,父进程也为B了,所以说明我们实现了copy on write(图5-18)! 图 5‑18 测试结果 5.9 但是这个还有个漏洞,就是如果引用次数为1时,表明进程已经独享不用复制,为了减少物理页浪费,我们写一个copy_on_write函数(图5-19),后续添加defs步骤略,然后我们看到traps.h中页错误中断号为14(图5-20),然后我们在trap.c中添加一段终端的处理,先获取地址,然后跟随进程页表一起传入copy_on_write得到复制(图5-21),此外,我们还需要在copyuvm_onwrite函数中加上限制,如果页>=3则不可写(图5-22)。 图 5‑19 copy_on_write函数实现 图 5‑20 页中断号 图 5‑21 中断后进入copy_on_write重新复制 图 5‑22 改写权限为不可写 5.10 最后我们运行测试程序,可以看到copy_on_write起效了,并且子进程因为写语句进入终端,复制完父进程页表后做了新的映射(图5-23)。 图 5‑23 完成隔离 |
四、实验体会:(根据自己情况填写) 通过本次实验,我受益匪浅: 1、 通过对xv6系统的实验,增加了对进程调度的直观认识,明白了如何添加系统调用与简单程序。 2、 明白了调度在xv6中的重要作用以及运行原理,并且明白了如何简单的实现信号量机制。 3、 明白了slab内核内存管理的实现方法以及运行原理。 4、 掌握了系统里copy_on_write的编码实现。 |
深圳大学操作系统综合实验一处理机调度
最新推荐文章于 2024-01-30 15:13:38 发布