Lab4实验报告

Lab4实验报告

一、思考题

Thinking 4.1
  • 内核在保存现场的时候是如何避免破坏通用寄存器的?

  • 系统陷入内核调用后可以直接从当时的 a 0 − a0- a0a3参数寄存器中得到用户调用msyscall留下的信息吗?

  • 我们是怎么做到让sys开头的函数“认为”我们提供了和用户调用msyscall时同样的参数的?

  • 内核处理系统调用的过程对Trapframe做了哪些更改?这种修改对应的用户态的变化是?

  • 保存现场时,k0寄存器暂存了sp栈指针的值,k1寄存器更新sp栈指针的值,除k0k1之外所有的通用寄存器都在修改之前被保存了,k0k1是可以暂时被随意改变的寄存器,因此修改也没关系。

  • 可以,a0-a3寄存器没有被修改过。

  • 人工将参数加载到了sys开头函数认为的位置。

  • Trapframe结构体中的cp0_epc的值增加了4,将sys开头函数的返回值存入v0寄存器。系统调用结束后,从syscall的下一条开始执行。

Thinking 4.2

思考下面的问题,并对这个问题谈谈你的理解: 请回顾 lib/env.c 文件中 mkenvid() 函数的实现,该函数不会返回0,请结合系统调用和IPC部分的实现与envid2env()函数的行为进行解释。

在这里插入图片描述

由以上mkenvid()函数可知,最终返回值的第11位始终为一,所以该函数不会返回零。

Thinking 4.3

思考下面的问题,并对这两个问题谈谈你的理解:

  • 子进程完全按照 fork() 之后父进程的代码执行,说明了什么?
  • 但是子进程却没有执行 fork() 之前父进程的代码,又说明了什么?
  • 说明了子进程和父进程具有相同的代码段。
  • 创建子进程时,PC值设置为了fork()的后一个指令,所以子进程没有执行fork()之前父进程的代码。
Thinking 4.4

关于 fork 函数的两个返回值,下面说法正确的是:

A、fork 在父进程中被调用两次,产生两个返回值

B、fork 在两个进程中分别被调用一次,产生两个不同的返回值

C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值

D、fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值

C

Thinking 4.5

我们并不应该对所有的用户空间页都使用duppage进行映射。那么究竟哪些用户空间页应该映射,哪些不应该呢?请结合本章的后续描述mm/pmap.c 中 mips_vm_init 函数进行的页面映射以及 include/mmu.h 里的内存布局图进行思考。

需要映射的是0—USTACKTOP范围内的空间。

因为其上的范围,USTACKTOP到UXSTACKTOP之间为用户进程的异常栈,而异常栈是进行异常处理的地方,不应映射;UTOP以上为内核相关页表,无权更改,也不需映射。

Thinking 4.6

在遍历地址空间存取页表项时你需要使用到vpd和vpt这两个“指针的指针”,请参考 user/entry.S 和 include/mmu.h 中的相关实现,思考并回答这几个问题:

  • vpt和vpd的作用是什么?怎样使用它们?
  • 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
  • 它们是如何体现自映射设计的?
  • 进程能够通过这种方式来修改自己的页表项吗?
  • vpd存放页目录基地址,基地址加页目录项偏移数即为va对应的页目录项;vpt为页表基地址,基地址加页表项偏移数即为va对应的页表项。

  • entry.S中,定义了页表和页目录的虚拟地址,使每个进程的页表都能在UVPT中保存。

  • 在这里插入图片描述

  • vpd指向(UVPT+(UVPT>>12)*4) ,这是自映射机制。

  • 不能,用户进程无权修改自己和内核的页表项,必须要陷入内核才能进行操作。

Thinking 4.7

page_fault_handler 函数中,你可能注意到了一个向异常处理栈复制Trapframe运行现场的过程,请思考并回答这几个问题:

  • 这里实现了一个支持类似于“中断重入”的机制,而在什么时候会出现这种“中断重入”?
  • 内核为什么需要将异常的现场Trapframe复制到用户空间?
  • 如果在缺页中断时再次响应了外部中断,便会“中断重入”。
  • 用户进程在中断结束恢复现场需要Trapframe,写入用户空间,则需要将异常的Trapframe复制到用户空间。
Thinking 4.8

到这里我们大概知道了这是一个由用户程序处理并由用户程序自身来恢复运行现场的过程,请思考并回答以下几个问题:

  • 在用户态处理页写入异常,相比于在内核态处理有什么优势?
  • 从通用寄存器的用途角度讨论,在可能被中断的用户态下进行现场的恢复,要如何做到不破坏现场中的通用寄存器?
  • 在用户态处理可以将操作交由用户自己完成,简化操作系统的复杂度,并且减小处理失误时对操作系统造成的后果。
  • 将通用寄存器入栈,然后通过sp寄存器再恢复通用寄存器。
Thinking 4.9

请思考并回答以下几个问题:

  • 为什么需要将set_pgfault_handler的调用放置在syscall_env_alloc之前?
  • 如果放置在写时复制保护机制完成之后会有怎样的效果?
  • 子进程是否需要对在entry.S定义的字__pgfault_handler赋值?
  • 父子进程共享空间,在父进程调用env_alloc的过程中可能也需要进行缺页处理。
  • 这样的话发生缺页中断不能够被捕捉到,无法进入缺页中断异常
  • 不需要,父子进程可以共享。

二、实验难点图示

1、系统调用

syscall流程图,以writef()函数为例:
在这里插入图片描述

2、进程通信机制

进程间通信机制需要通过系统调用实现进程之间的数据交流。

但是由于进程的地址空间都是独立的,要想把数据从一个地址空间转移到另一个空间,需要利用各个进程都共享的内核的2G空间,所以要使用内核中的进程控制块来实现进程通信,即修改PCB的某些属性。

3、fork()区分进程

一个进程调用fork,在两个进程中得到两个返回值,与sys_env_alloc函数密切相关。这里的难点有两个:产生两个返回值的机理,fork的流程。父进程fork中执行了系统调用syscall_env_alloc,需要从系统调用中恢复现场;子进程被创建,但是没有被调度,需要在调度的时候恢复现场。

fork()函数的大致流程如下:
在这里插入图片描述

4、缺页中断

在这里插入图片描述

5、duppage

duppage中需要分别给父进程与子进程相关的页,设置PTE_COW位,要先给子进程设置,再给父进程设置。

子进程现在是不可运行的,而父进程是可运行的。如果先给父进程某页设置了PTE_COW,父进程可能修改这一页,触发写时复制,父进程重新分配一页,但是这一页不再存在共享的冲突,没有PTE_COW,这个时候map子进程,子进程相应地址也指向这一页,但是有PTE_COW。之后如果父进程再修改这一页,由于没有PTE_COW,不会触发写时复制,这一共享页被修改了,但是对于子进程来说,不应该被修改,于是执行出现错误。

三、体会与感想

本次实验课下花了三四天的时间理解,确实明显感觉到难度上来了。这部分重在理解,第一遍写的时候还不是很懂,后来看了网上一些往届学长的难点梳理,自己也从头到尾重新梳理了一遍,理解才更加清楚了。

lab4难点有几个,系统调用、进程通信、fork等,这些也都总结在难点图示里了,这一单元有很多功能不同的函数,并且要格外注意区分用户态与核心态的执行函数,而且这一部分有些地方需要填写汇编代码,有些需要跳转,有些需要人为操纵寄存器,理解起来比较有难度。总体来说,lab4过的还是比较煎熬的,lab4需要大量之前写过的函数,所以在做lab4的时候找出来很多lab2、lab3的bug,找bug的过程真的很痛苦Orz。

关于lab4的两次上机,lab4-1属于课上样例跑对了但是交上去0分的情况,至今仍不知道问题出在哪,但是由于考lab4-1的时候我lab4的第二部分还没有做完,所以有点怀疑是课下bug没找出来导致的。考lab4-2之前其实感觉自己心里也挺没底,上机时看到题目也觉得有点悬,代码量不大,但是各种权限位属实搞得有点头大,最后竟然过了,也是挺惊喜的。

lab4没有lab1、2、3的分值大,虽然难度上去了,但是反而没有考前面的lab时的那种焦虑了,考试时心态更轻松了,答题状态貌似也好了很多,总之还是收获很大。

四、残留难点

syscall.S中handle_sys中提到了内核栈指针与用户栈指针,对于两个指针到底指向哪里,不是很清楚。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
实验概述 本次实验是MIT 6.828操作系统课程的第一次实验,主要内容是编写一个简单的操作系统内核,并在QEMU虚拟机上运行。本次实验共有9个练习,其中练习9要求实现一个简单的用户程序并运行。 练习9要求我们实现一个简单的用户程序,该程序能够在屏幕上输出一些信息,并等待用户输入,输入结束后将输入内容输出到屏幕上。用户程序的具体要求如下: - 输出一些信息,例如“Hello World!”。 - 等待用户输入,可以使用getchar()函数实现。 - 将用户输入内容输出到屏幕上。 实验过程 1. 编写用户程序 我们首先在lab1目录下创建一个user文件夹,用于存放用户程序。然后创建一个名为“test.c”的文件,编写用户程序的代码如下: ``` #include <stdio.h> int main() { printf("Hello World!\n"); char c = getchar(); printf("You entered: %c\n", c); return 0; } ``` 这段代码的功能是输出“Hello World!”并等待用户输入,输入结束后将输入内容输出到屏幕上。 2. 修改Makefile文件 为了能够编译用户程序,我们需要修改Makefile文件。具体修改如下: ``` UPROGS=\ _cat\ _echo\ _forktest\ _grep\ _init\ _kill\ _ln\ _ls\ _mkdir\ _rm\ _sh\ _stressfs\ _usertests\ _wc\ _test\ # 添加用户程序的名称 $(OBJDIR)/_test: $(OBJDIR)/test.o $(LIBDIR)/ulib.o | $(OBJDIR) $(LD) $(LDFLAGS) -N -e main -Ttext 0 -o $@ $^ $(OBJDIR)/test.o: test.c | $(OBJDIR) $(CC) $(CFLAGS) -c -o $@ $< ``` 在UPROGS变量中添加上刚刚编写的用户程序的名称“_test”,然后在Makefile文件的末尾添加如上代码。 3. 编译内核和用户程序 在终端运行命令“make”,编译内核和用户程序。 4. 运行QEMU虚拟机 在终端运行命令“make qemu”,启动QEMU虚拟机。 5. 运行用户程序 在QEMU虚拟机中,输入“test”,即可运行刚刚编写的用户程序。运行结果如下: ``` Hello World! This is a test. You entered: T ``` 可以看到,程序首先输出了“Hello World!”这个信息,然后等待用户输入。我们输入了“This is a test.”这个字符串,然后按下回车键,程序将输入内容输出到了屏幕上。 实验总结 本次实验要求我们实现一个简单的用户程序并运行。通过编写代码、修改Makefile文件、编译内核和用户程序、启动虚拟机以及运行用户程序等步骤,我们成功地完成了本次实验,并学会了如何在操作系统内核中运行用户程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值