深圳大学操作系统综合实验二xv6线程+虚存

试验目的

加深对线程和虚存的直观认识;

掌握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管道与普通文件不同,我们在写代码的时候要注意区分普通文件和管道文件

  • 24
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值