Lab4_实验报告

一、思考题

Thinking 4.1

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

内核在kern/entry.S异常入口处使用宏函数SAVE ALL保存通用寄存器和部分CP0寄存器,其中只使用k0k1寄存器来操作,不会破坏其他通用寄存器

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

我觉得需要分类讨论,理由如下:

陷入内核调用时

  • kern\entry.SSAVE_ALL生成一个Trapeframe 并把sp 指向 Trapframe 的底部
1
2
3
4
5
6
7
8
9
10
exc_gen_entry:
SAVE_ALL
mfc0 t0, CP0_STATUS
and t0, t0, ~(STATUS_UM | STATUS_EXL | STATUS_IE)
mtc0 t0, CP0_STATUS
/* Exercise 3.9: Your code here. */
mfc0 t0, CP0_CAUSE
andi t0, 0x7c
lw t0, exception_handlers(t0)
jr t0

如果在此时获取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)
    .endm
    • kern\genex.Smove a0, sp是为了void do_xxx(struct Trapframe *tf) 传参

      如果你是syscall,原来a0寄存器储存的是系统调用号

      此时a0寄存器的值已经改变,不能直接调用

  • 如果你进入对应异常是时钟中断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    NESTED(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 里的内存布局图以及本章的后续描述进行思考

图片

  • UXSTACKTOPULIM之间储存的是和内核相关的页表信息,在env_init()中,已经通过map_segment()创建了base_pgdir模板页目录,在创建新进程时,会复制模板页目录,故此部分无需映射
  • UXSTACKTOP-PGSIZEUXSTACKTOP 之间是异常栈,写时复制使用,故此部分无需映射
  • USTACKTOPUXSTACKTOP-PGSIZE 之间是empty memory,故此部分无需映射
  • UTEXTUSTACKTOP 中,可写且不共享的内容都需要被映射

Thinking 4.6

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

  • vpt 和 vpd 的作用是什么?怎样使用它们?

vpt是页表起始地址 vpd是页目录起始地址

可以直接用数组的形式,如 vpd[va>>22]

  • 从实现的角度谈一下为什么进程能够通过这种方式来存取自身的页表?
1
2
#define vpt ((const volatile Pte *)UVPT)
#define vpd ((const volatile Pde *)(UVPT + (PDX(UVPT) << PGSHIFT)))

宏定义用户态页目录、页表的基地址,再通过偏移读取自身页表

  • 它们是如何体现自映射设计的?

vpd 的地址是(UVPT + (PDX(UVPT) << PGSHIFT),在 UVPTUVPT + PDMAP 之间,说明将页目录映射到了某一页表位置,也就是自映射

  • 进程能够通过这种方式来修改自己的页表项吗?

不能,用户只有只读权限,若想修改页表项,需要陷入内核进行操作

Thinking 4.7

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

  • 这里实现了一个支持类似于“异常重入”的机制,而在什么时候会出现这种“异常重 入”?
1
2
3
if (tf->regs[29] < USTACKTOP || tf->regs[29] >= UXSTACKTOP) {
tf->regs[29] = UXSTACKTOP;
}

在写时复制、发生缺页异常时,可能再次发生缺页异常,防止之前的异常处理过程的信息丢失,对于 sp 已经在异常栈中的情况,不再从异常栈顶开始

  • 内核为什么需要将异常的现场 Trapframe 复制到用户空间?

用户态异常处理函数需要使用Trapframe 中的内容,且这是恢复现场必须的

内核态处理TLB Mod异常:kern/tlbex.ctlb_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.实现新的系统调用

实现一个新的系统调用基本上是四步走:

  1. include/syscall.h头文件中先定义好新的系统调用号系统调用名
  2. user/include/lib.h //syscalls 加入syscall_*函数定义(可能还会加用户函数)
  3. user/lib/syscall_lib.c用户态先定义这个syscall_*函数,调用msyscall
  4. user/lib/ipc.c有可能要更新?
  5. kern/syscall_all.c 中的 void *syscall_table[MAX_SYSNO] 系统调用函数表中,为你定义的系统调用号添加对应的内核函数指针
  6. 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文件夹下的env

    user/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
2
3
void syscall_*(){
return msyscall(变长参数)
}

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