加深对线程和虚存的直观认识; 掌握xv6操作系统中实现线程的核心机制; 掌握xv6操作系统中实现虚存的基本方法; 可以使用Linux+Qemu仿真环境; 修改xv6内核代码实现简单线程; 修改xv6内核代码实现简单的页帧交换; 修改xv6内核代码实现(有名)管道。 硬件:桌面PC 软件:Linux 或其他操作系统 阅读实验辅助材料完成以下操作 操作部分: 修改xv6内核代码实现简单线程;(40%) 修改xv6内核代码实现文件访问权限控制;(30%) 修改xv6内核代码实现简单的页帧交换。(20%) 修改xv6内核代码实现(有名)管道(10%) 实验结果 1 修改xv6内核代码实现简单线程;1.1 我们知道,创建一个线程的开销比进程要小,因为其可以共享进程的主要资源,我们为了实现xv6的内核线程,我们需要首先实现类似linux的alloc()和free()的内存管理机制,由于在之前我们已经实现了该内存管理机制(图1-1),所以我们思路是首先实现线程的创建和回收两个系统调用。 图 1‑1 linux的free和alloc内存管理机制 1.2 为了支持内核线程,我们需要将进程控制块PCB改成线程控制块TCB,使其变成线程和进程共用,所以我们在proc结构体里添加两个变量pthread和ustack,主要记录父线程号和用户的线程栈(图1-2)。 图 1‑2 添加两个变量 1.3 Clone系统调用的实现与fork实现极其相似,不过子线程共享了父线程的进程映像和大多数其他资源,并且clone还负责初始化用户栈,使得线程回到用户态之后能找到对应的入口(图1-3)。 图 1‑3 clone代码 1.4 接着我们再把clone函数封装成sys_clone()(图1-4)。 图 1‑4 封装clone 1.5 子进程和线程结束的时候资源回收过程会不一样,对于子进程,其PCB会由父进程调用wait进行回收,而子线程的PCB/TCB是由父线程/进程调用join进行回收,并且其不需要释放虚拟地址什么的,因为其不需要撤销进程映像(图1-5),完成join后我们在sysproc.c中封装join为sys_join(图1-6)。 图 1‑5 join代码 图 1‑6 封装join |
1.6 主线程调用join的时候,子线程还没有调用exit变成僵尸线程,所以执行不了join,达不到预期的目的,所以我们子线程退出的时候要唤醒对应的主线程,我们在exit()中添加唤醒主线程的功能(图1-7)。 图 1‑7 exit添加唤醒主线程的功能 1.7 接着,我们对两个系统调用封装成用户态可调用的函数,步骤不再赘述(图1-8、1-9、1-10、1-11、1-12)。 图 1‑8 syscall.h添加系统调用号 图 1‑9 syscall.c添加中断向量表和全局引用 图 1‑10 user.h添加用户态入口 图 1‑11 usys添加 图 1‑12 defs.h添加声明 1.8 Clone与join只是实现了内核线程,为了方便用户程序调用,我们需要实现用户线程库,假设其名字为uthread.c(图1-13),并且将其加入到MakeFile的ULIB变量中,其中thread_create是创建线程,通过clone创建县城前需要malloc分配线程栈,最后借助add_thread记录在TCB数组中,Thread_join用于等待线程结束,其通过join系统回收停止的线程,然后remove_thread将该进程的线程在线程数组删除。 图 1‑13 uthread.c 1.9 接着我们编写thread_demo.c的代码,主要是调用clone和join,测试参数是否传递成功,代码思路是创建三个线程,然后通过参数传递他们的编号,然后分别输出五次,同时调用斐波那契数列函数打印共享变量,最后查看结果(图1-14)。 图 1‑14 thread_demo测试代码 1.10 最后我们执行测试代码,可以发现我们增加了全局变量global一共15次,并且递归函数调用结果正确,全局变量增加顺序表示线程之间并发进行(图1-15)。 图 1‑15 测试结果 2 修改xv6内核代码实现文件访问权限控制;2.1 为了实现文件访问权限的控制,我们要对inode进行修改,因为inode可以存储文件访问控制信息,但是我们不能改变inode大小,所以我们尝试把inode里面的部分数据大小缩小,然后添加文件类型变量(图2-1),将16bit的short的type改成8bit的char类型,然后添加char的mode变量。 图 2‑1 改变数据结构 2.2 我们也将file.h里面的inode修改成对应的结构体(图2-2),并且将内存里面的布局改写,在stat.h文件里改写stat结构体的数据类型(图2-3)。 图 2‑2 修改inode结构体 图 2‑3 改写stat结构体 2.3 同时将mkfs.c的文件里面的ialloc函数修改,因为他属于分配inode的函数,再分配的时候需要将文件类型mode设置成3,二进制为11,即可读可写(图2-4),并且将fs.c的ialloc函数对应改写(图2-5)。 图 2‑4 修改ialloc 图 2‑5 修改fs.c的ialloc 2.4 同时我们对fs.c的ilock函数进行改写,因为读取inode(图2-6)或者inode写磁盘,复制inode到stat(图2-7)都需要传递mode(图2-8),所以我们需要改写。 图 2‑6 改写ilock 图 2‑7 改写iupdate 图 2‑8 拷贝stat函数的改写 2.5 然后我们在ls.c改变其print的内容,添加上我们的文件类型输出(图2-9)。 图 2‑9 ls更改printf 2.6 然后我们重新make我们的xv6,然后执行ls指令,我们可以看到我们的文件类型都变成了3,也就是可读可写权限(图2-10)。 图 2‑10 ls输出 2.7 紧接着,我们在sysfile.c文件中添加sys_chmod函数,以此系统调用来更改文件权限(图2-11)。 图 2‑11 sys_chmod函数 2.8 然后将其转换成用户可用的chmod函数,步骤介绍略(图2-12、2-13、2-14、2-15) 图 2‑12 syscall.h添加系统调用号 图 2‑13 syscall声明以及修改中断向量表 图 2‑14 user.h添加用户态入口 图 2‑15 usys.S添加 2.9 接着我们在file.c文件中修改fileread函数以及filewrite函数,添加对权限的判断(图2-16、图2-17)。 图 2‑16 修改fileread函数判断权限 图 2‑17 修改filewrite函数判断权限 2.10 接着我们新建一个更改mode的代码,名为changemode.c,里面就是单纯根据参数来执行chmod函数,以此来修改文件权限(图2-18)。 图 2‑18 changemode测试代码 2.11 接着我们remake我们的qemu,然后进入系统中使用echo创建一个extent文件,里面写入hello(图2-19),使用ls查看文件权限为3,可读可写(图2-20),接着我们执行changemode修改content权限为1(图2-21),使用ls查看content文件权限也变成了1,也就是不可写,然后我们echo写入nihao发现写入错误(图2-23)。 图 2‑19 echo创建extent并写入hello 图 2‑20 ls查看文件权限,发现是3 图 2‑21 执行修改权限 图 2‑22 ls查看权限变成了1 图 2‑23 尝试写入出错 2.12 我们再次尝试查看文件内容,发现果然没有改变,然后使用changemode修改权限为2也就是可写不可读(图2-25),查看content权限,果然变成了2(图2-26),接着我们使用cat验证是否真的不可读,发现读取失败(图2-27)。 图 2‑24 查看内容未被更改 图 2‑25 修改文件权限为可写不可读 图 2‑26 查看content权限 图 2‑27 读取失败 3 修改xv6内核代码实现简单的页帧交换3.1 首先页帧交换涉及到缺页的情况,我们在缺页中断里面处理这种情况,才适合实现页帧交换,查阅资料得知,缺页的时候首先通过kalloc分配物理页帧,会有两种不同的处理情况: 3.1.1 kalloc成功3.1.2 kalloc失败,从进程空间找到一页换出去,然后获取新的一个空闲页3.2 在缺页的时候不论是申请物理页还是交换页帧,我们先通过各种方式找到一个空闲的页,之后在进行操作。在成功找到空闲的页之后还有两种情况: 3.2.1 进程未分配物理页,此时只需要建立新页的映射3.2.2 进程之前的页被换出,需要将之前换出来的页换回来重新建立映射。3.3 进程空间的地址通过 allocuvm 来线性拓展,当物理页帧不足时,allocuvm 函数会取消分配,同时释放之前已经获取的物理页帧。因为 swap 运行进程运行在物理页帧不足的情况下,所以要修改进程空间的地址拓展机制。 3.4 所以我们此时修改vm.c中的allocuvm函数,使得其在物理页不足的时候直接break,并且任然扩展虚拟地址,只是不分配物理页(图3-1) 图 3‑1修改allocuvm 3.5 接着我们在deallocuvm里面收缩内存空间的时候,仅仅释放实际存在的页,也就是没有交换过的页(图3-2)。 图 3‑2 deallocuvm函数 3.6 接着我们编写swap的代码,首先在bio.c里面增加度写磁盘的接口,一次性读取八个盘块作为一个物理页帧(图3-3)。 图 3‑3 bio.c交换代码 3.7 接着我们在fs.c里面添加分配,回收对应八个盘块的接口(图3-4)。 图 3‑4 fs.c中回收盘块接口 3.8 我们在defs.h里面添加上述函数的声明(图3-5) 图 3‑5 defs.h添加函数声明 3.9 进程初始化时,由于前几个页帧需要频繁进行访问,不应该进行交换,所以我们在proc结构体里面添加一个交换页的起始虚拟地址(图3-6),并且在fork函数里对该变量赋值(图3-7)。 图 3‑6 proc结构体交换起始地址添加 图 3‑7 fork对swapstart地址赋值 3.10 Exec.c里面也要对exec函数里面的proc赋值swap_start(图3-8)。 图 3‑8 exec中变量赋值 3.11 最后在mmu.h中定义PET_SWAPPED的值(图3-9)。 图 3‑9 mmu定义pte_swapped值 3.12 接着我们实现换入换出代码,主要思路是首先遍历整个进程的虚拟空间,找到一个没有被交换的页;然后申请八个盘块,并且将换出页的数据写到盘块中,最后将换出页的页表赋值为盘块号(图3-10)。 图 3‑10 换入换出代码 3.13 最后我们编写中断处理函数,他根据上面函数的思路,我们先想办法寻找一个物理页,然后根据虚拟地址是被换出还是未分配物理页,如果是被换出了,我们就在磁盘上读进新建的物理页,如果还没分配物理页,那么我们直接返回新的物理页就可以了(图3-11)。 图 3‑11 中断处理函数 3.14 在defs中添加中断处理函数后,我们在trap.c中调用中断处理函数处理缺页的情况(图3-12),然后我们在memlayout.h里面修改phystop(图3-13)。 图 3‑12 处理缺页 图 3‑13 phystop修改 3.15 然后我们编写测试代码swap_demo.c,程序尝试分配n=680(测试有大约678页)页并且超过系统内存大小,然后写入数据,第i层数据是i,并且尝试读取数据(图3-14)。 图 3‑14 swap_demo.c代码 3.16 最后我们启动测试程序,测得结果在写数据的时候,最后的677-679没有物理页,所以有三个页被换出,这三个页被映射到新的空闲页上面(图3-15) 图 3‑15 写数据结果 3.17 接着访问3000地址,因为此时任然缺页,所以又把5000换出去了,同时把3000从928盘块找回,占据5000位置,释放928盘块(图3-16) 图 3‑16 换进换出 3.18 4000和5000同理,可以发现7000没有被换出,访问正常,全部数据都正常,没有出现panic,说明有正常在工作(图3-17) 图 3‑17 交换完成 4 修改xv6内核代码实现(有名)管道4.1 查阅资料得知,有名管道它提供了一个路径名与之关联,以 FIFO 的文件形式存在于文件系统中,并且其打开方式与打开一个普通文件是一样的,这样即使与 FIFO 的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过 FIFO 相互通信,因此,通过 FIFO 不相关的进程也能交换数据。 4.2 有名管道的确定需要创建文件,读取管道内数据和读取文件原理一样,但是真实数据是存在于内存中的。所以在创建管道文件的时候,同时用kalloc分配内存作为数据交换区,该内存物理地址作为管道文件的内容,并且我们在inode添加特殊标志表示该文件是管道文件,就如上题所做的mode属性,我们定义514表示管道文件。 4.3 我们在sysfile.c添加一个sys_open_fifo函数,其与sys_open几乎没有任何区别,只是我们在创建管道的同时,需要将inode的mode属性设置为111(图4-1),同时调用kalloc申请内存并且将内存物理地址写入文件(图4-2)。 图 4‑1 设置mode 图 4‑2 创建并写入管道文件 4.4 因为更改了mode,所以在file.c时读取文件和写入文件时需要判断文件权限,如果是管道文件直接放行(图4-3)。 图 4‑3 管道文件放行 4.5 读写文件时,如果读到了管道文件,我们只能得到物理地址,所以我们需要更改sys_read和sys_write内容,对其进行判断,正常文件直接读,管道文件需要转换在读(图4-4)。 图 4‑4 sys_read和sys_write更改 4.6 然后我们在syscall.h中添加系统调用号(图4-5),剩下步骤不再赘述(图4-6) 图 4‑5 syscall.h添加系统调用号 图 4‑6 user.h添加用户态入口 图 4‑7 usys.s添加 图 4‑8 sys_write修改 4.7 我们写下测试代码fifo_demo.c,首先申请fifo文件,fork之后父进程读数据,子进程写数据到fifo文件中(图4-9)。 图 4‑9 fifo_demo.c代码 4.8 将测试程序加入到Makefile中,编译运行后进入系统运行测试程序(图4-10)。 图 4‑10 测试程序输出结果 4.9 我们打开了fifo文件,他位于0x803e1000,然后在这里申请一页物理页帧,并且foek,因为是mode=111,所以直接从内存读取数据,最后子进程输出读取结果,确实是helloworld 说明fifo有效! |
四、实验体会:(根据自己情况填写) 通过本次实验,我受益匪浅: 1、 通过对xv6系统的实验,增加了对线程和虚存的认识 2、 明白了线程和进程的区别,线程还要注意设置陷阱帧 3、 明白了文件权限是定义在inode中,修改不能单纯添加变量,而是需要调整大小,保持结构体在256bit 4、 进程空间扩展有点复杂,我们需要考虑缺页情况才能实现交换 5、 Fifo管道与普通文件不同,我们在写代码的时候要注意区分普通文件和管道文件 |