OS_Lab4_实验报告
Lab4_实验报告
一、思考题
Thinking 4.1
内核在保存现场的时候是如何避免破坏通用寄存器的?
内核在kern/entry.S
异常入口处使用宏函数SAVE ALL
保存通用寄存器和部分CP0寄存器,其中只使用k0
和k1
寄存器来操作,不会破坏其他通用寄存器
系统陷入内核调用后可以直接从当时的 $a0-$a3 参数寄存器中得到用户调用 msyscall 留下的信息吗?
我觉得需要分类讨论,理由如下:
陷入内核调用时
kern\entry.S
在SAVE_ALL
生成一个Trapeframe
并把sp
指向Trapframe
的底部
1 | exc_gen_entry: |
如果在此时获取a0-a3
自然是可以的
但jr t0
之后进入了对应的异常处理函数
如果你对应的异常是:
tlb_refill、tlb_mod、syscall、reserved
1
2
3
4
5
6
7
8
9.macro BUILD_HANDLER exception handler
NESTED(handle_\exception, TF_SIZE + 8, zero)
move a0, sp
addiu sp, sp, -8
jal \handler
addiu sp, sp, 8
j ret_from_exception
END(handle_\exception)
.endmkern\genex.S
中move a0, sp
是为了void do_xxx(struct Trapframe *tf)
传参如果你是
syscall
,原来a0
寄存器储存的是系统调用号此时
a0
寄存器的值已经改变,不能直接调用
如果你进入对应异常是时钟中断
1
2
3
4
5
6
7
8
9
10NESTED(handle_int, TF_SIZE, zero)
mfc0 t0, CP0_CAUSE
mfc0 t2, CP0_STATUS
and t0, t2
andi t1, t0, STATUS_IM7
bnez t1, timer_irq
timer_irq:
li a0, 0
j schedule
END(handle_int)- 当你跳转到
timer_irq
时,a0
又发生了改变^_^
- 当你跳转到
我们是怎么做到让 sys 开头的函数“认为”我们提供了和用户调用 msyscall 时同样 的参数的?
user/lib/syscall_all.c
中调用 syscall_*()
时,函数的前四个参数存入 a0-a3
,剩下两个参数存进栈
调用 msyscall
、分发异常至 handle_sys
、调用 do_syscall
,此时我们取出a0-a3
以及栈帧中的两个数,得到的就是同样的后五个参数
内核处理系统调用的过程对 Trapframe 做了哪些更改?这种修改对应的用户态的变化是什么?
进行了tf->cp0_epc += 4
,因为syscall
不在延迟槽内执行,所以可以保证系统调用后返回执行下一条指令
进行了tf->regs[2] = func(arg1, arg2, arg3, arg4, arg5)
系统调用后的返回值写入了v0寄存器
Thinking 4.2
思考 envid2env 函数: 为什么 envid2env 中需要判断 e->env_id != envid 的情况?如果没有这步判断会发生什么情况?
envid
的低10位表示在envs
中的索引,高位则表示调用mkenvid
的次数
若传入的参数envid
属于一个已销毁的进程,应该返回 -E_BAD_ENV
。但如果没有判断e->env_id!=envid
,已销毁进程的envid
和新创建进程的 envid
都能通过 ENVX
宏取得相同的值,得到对应同一个进程控制块。
Thinking 4.3
思考下面的问题,并对这个问题谈谈你的理解:请回顾 kern/env.c 文件 中 mkenvid() 函数的实现,该函数不会返回 0,请结合系统调用和 IPC 部分的实现与 envid2env() 函数的行为进行解释
envid2env()
:由于mkenvid
不可能返回 0,所以我们可以通过envid
等于0来访问当前进程curenv
IPC
:如果传入的envid
为 0,也就是不合法,那当前进程会自己和自己通信,不会产生其它影响
Thinking 4.4
关于 fork 函数的两个返回值,下面说法正确的是:
A、fork 在父进程中被调用两次,产生两个返回值
B、fork 在两个进程中分别被调用一次,产生两个不同的返回值
C、fork 只在父进程中被调用了一次,在两个进程中各产生一个返回值
D、fork 只在子进程中被调用了一次,在两个进程中各产生一个返回值
C
Thinking 4.5
我们并不应该对所有的用户空间页都使用 duppage 进行映射。那么究竟哪 些用户空间页应该映射,哪些不应该呢?请结合 kern/env.c 中 env_init 函数进行的页面映射、include/mmu.h 里的内存布局图以及本章的后续描述进行思考
UXSTACKTOP
到ULIM
之间储存的是和内核相关的页表信息,在env_init()
中,已经通过map_segment()
创建了base_pgdir
模板页目录,在创建新进程时,会复制模板页目录,故此部分无需映射UXSTACKTOP-PGSIZE
到UXSTACKTOP
之间是异常栈,写时复制使用,故此部分无需映射USTACKTOP
到UXSTACKTOP-PGSIZE
之间是empty memory,故此部分无需映射UTEXT
到USTACKTOP
中,可写且不共享的内容都需要被映射
Thinking 4.6
在遍历地址空间存取页表项时你需要使用到 vpd 和 vpt 这两个指针,请参考user/include/lib.h 中的相关定义,思考并回答这几个问题:
- vpt 和 vpd 的作用是什么?怎样使用它们?
vpt
是页表起始地址 vpd
是页目录起始地址
可以直接用数组的形式,如 vpd[va>>22]
- 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
1 |
宏定义用户态页目录、页表的基地址,再通过偏移读取自身页表
- 它们是如何体现自映射设计的?
vpd
的地址是(UVPT + (PDX(UVPT) << PGSHIFT)
,在 UVPT
和 UVPT + PDMAP
之间,说明将页目录映射到了某一页表位置,也就是自映射
- 进程能够通过这种方式来修改自己的页表项吗?
不能,用户只有只读权限,若想修改页表项,需要陷入内核进行操作
Thinking 4.7
在 do_tlb_mod 函数中,你可能注意到了一个向异常处理栈复制 Trapframe 运行现场的过程,请思考并回答这几个问题:
- 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重 入”?
1 | if (tf->regs[29] < USTACKTOP || tf->regs[29] >= UXSTACKTOP) { |
在写时复制、发生缺页异常时,可能再次发生缺页异常,防止之前的异常处理过程的信息丢失,对于 sp
已经在异常栈中的情况,不再从异常栈顶开始
- 内核为什么需要将异常的现场 Trapframe 复制到用户空间?
用户态异常处理函数需要使用Trapframe
中的内容,且这是恢复现场必须的
内核态处理TLB Mod
异常:kern/tlbex.c
中tlb_mod
异常的现场Trapframe
复制到用户空间,并设置tf->cp0_epc = ...
,从异常恢复后能够以异常处理栈中保存的现场(Trapframe
)为参数,跳转到用户态异常处理函数(cow_entry
) 。
在用户态异常处理函数(cow_entry
)完成异常处理后,我们会通过sys_set_trapframe
将当前异常栈修改为传入的参数 struct Trapframe *tf
对应的Trapframe
。从该系统调用返回时,将返回epc
的位置,恢复到产生TLB Mod
时的现场。
Thinking 4.8
在用户态处理页写入异常,相比于在内核态处理有什么优势?
按照微内核的设计理念,尽可能地将功能实现在用户空间中,内核态处理发生错误可能会使得操作系统崩溃。但用户态权限有限,可以避免产生意外的改变。
Thinking 4.9
为什么需要将 syscall_set_tlb_mod_entry 的调用放置在 syscall_exofork 之前?
如果放置在写时复制保护机制完成之后会有怎样的效果?
根据讨论区助教的回答:
syscall_set_tlb_mod_entry
的调用放置在syscall_exofork
前后都没有影响。第一次syscall_set_tlb_mod_entry
是针对父进程设置的。在子进程RUNNABLE
之前父进程已经设置了子进程的tlb_mod_entry
。子进程RUNNABLE
后会从syscall_exofork
逐级调用的syscall
指令之后开始执行。如果在syscall_exofork
之后再syscall_set_tlb_mod_entry
,那么子进程也会执行这个系统调用。父进程运行时在函数调用等情形下会修改栈。在栈空间的页面标记为写时复制之后,父进程继续运行并修改栈,就会触发
TLB Mod
异常。所以在写时复制保护机制完成之前就需要syscall_set_tlb_mod_entry
。
二、实验难点
1.实现新的系统调用
实现一个新的系统调用基本上是四步走:
- 在
include/syscall.h
头文件中先定义好新的系统调用号和系统调用名。 - 在
user/include/lib.h
//syscalls 加入syscall_*函数定义(可能还会加用户函数) - 在
user/lib/syscall_lib.c
用户态先定义这个syscall_*函数,调用msyscall - 在
user/lib/ipc.c
有可能要更新? - 在
kern/syscall_all.c
中的void *syscall_table[MAX_SYSNO]
系统调用函数表中,为你定义的系统调用号添加对应的内核函数指针 - 在
kern/syscall_all.c
中:添加两个系统调用在内核中的实现函数sys_*。请保证两个函数的定义位于系统调用函数表void *syscall_table[MAX_SYSNO]
之前
2.用户态的一些名词解释
UPAGES、UENVS
map_segment 创建一个模板页目录
UVPT-UTOP
env_setup_vm memcpy模板页目录到进程的页目录
UVPT 、 ULIM
User VPT部分 进程自己的页表
user/include/lib.h
vpd 页目录
vpt 页表项
envs 进程块List 可以通过envs[ENVX(envid)]访问
pages 页物理块
UXSTACKTOP
用户态的异常处理栈
user/lib
文件夹下的envuser/lib/libos.c
const volatile struct Env *env
都是指向当前进程**user_panic(“字符串”);**用户态panic
debugf(“”); 用户态printk
user/lib/debugf.c
debugf -> vdebugf -> vprintfmt -> debug_output -> debug_flush -> syscall_print_cons
3.系统调用过程
user/lib/syscall_lib.c
1 | void syscall_*(){ |
user/lib/syscall_wrap.S
msyscall -> syscall -> 异常分发
kern/genex.S
handle_sys([8] = handle_sys) ->do_syscall
kern/syscall.c
void sys_*()
三、实验体会
为什么为什么为什么又挂Extra?我还要怎么学怎么学怎么学?????——5.8
Lab4第一次上机挂在TAILQ
宏定义没有熟练掌握,因为是取出TALQ_FIRST
之后还要进行TAILQ_REMOVE
,老生常谈的细心问题,我却不愿归结于粗心上
- 过于相信自己总结的字典!只要
grep
一下就可以找到用法,为什么不做呢? - 上机前特有的松弛感,以为一切尽在掌握?
下周上机之前,我会再将迄今为止的重点完整梳理一遍的
宜将剩勇追穷寇,不可沽名学霸王。虽然Unix期中、航概考试、军理大作业接踵而来,但是准备Lab5启动!!!——5.11