练习参考答案#

课后练习#

编程题#

  1. * 扩展内核,能够显示操作系统切换任务的过程。

  2. ** 扩展内核,能够统计每个应用执行后的完成时间:用户态完成时间和内核态完成时间。

  3. ** 编写浮点应用程序A,并扩展内核,支持面向浮点应用的正常切换与抢占。

  4. ** 编写应用程序或扩展内核,能够统计任务切换的大致开销。

  5. *** 扩展内核,支持在内核态响应中断。

  6. *** 扩展内核,支持在内核运行的任务(简称内核任务),并支持内核任务的抢占式切换。

注:上述扩展内核的编程基于 rcore/ucore tutorial v3: Branch ch3

问答题#

  1. * 协作式调度与抢占式调度的区别是什么?

    协作式调度中,进程主动放弃 (yield) 执行资源,暂停运行,将占用的资源让给其它进程;抢占式调度中,进程会被强制打断暂停,释放资源让给别的进程。

  2. * 中断、异常和系统调用有何异同之处?

    • 相同点

      • 都会从通常的控制流中跳出,进入 trap handler 进行处理。

    • 不同点

      • 中断的来源是异步的外部事件,由外设、时钟、别的 hart 等外部来源,与 CPU 正在做什么没关系。

      • 异常是 CPU 正在执行的指令遇到问题无法正常进行而产生的。

      • 系统调用是程序有意想让操作系统帮忙执行一些操作,用专门的指令(如 ecall )触发的。

  3. * RISC-V支持哪些中断/异常?

  4. * 如何判断进入操作系统内核的起因是由于中断还是异常?

  5. ** 在 RISC-V 中断机制中,PLIC 和 CLINT 各起到了什么作用?

    CLINT 处理时钟中断 (MTI) 和核间的软件中断 (MSI);PLIC 处理外部来源的中断 (MEI)。

    PLIC 的规范文档: https://github.com/riscv/riscv-plic-spec

  6. ** 基于RISC-V 的操作系统支持中断嵌套?请给出进一步的解释说明。

  7. ** 本章提出的任务的概念与前面提到的进程的概念之间有何区别与联系?

  8. * 简单描述一下任务的地址空间中有哪些类型的数据和代码。

  9. * 任务控制块保存哪些内容?

  10. * 任务上下文切换需要保存与恢复哪些内容?

    需要保存通用寄存器的值,PC;恢复的时候除了保存的内容以外还要恢复特权级到用户态。

  11. * 特权级上下文和任务上下文有何异同?

  12. * 上下文切换为什么需要用汇编语言实现?

    上下文切换过程中,需要我们直接控制所有的寄存器。C 和 Rust 编译器在编译代码的时候都会“自作主张”使用通用寄存器,以及我们不知道的情况下访问栈,这是我们需要避免的。

    切换到内核的时候,保存好用户态状态之后,我们将栈指针指向内核栈,相当于构建好一个高级语言可以正常运行的环境,这时候就可以由高级语言接管了。

  13. * 有哪些可能的时机导致任务切换?

    系统调用(包括进程结束执行)、时钟中断。

  14. ** 在设计任务控制块时,为何采用分离的内核栈和用户栈,而不用一个栈?

    用户程序可以任意修改栈指针,将其指向任意位置,而内核在运行的时候总希望在某一个合法的栈上,所以需要用分开的两个栈。

    此外,利用后面的章节的知识可以保护内核和用户栈,让用户无法读写内核栈上的内容,保证安全。

  15. *** (以下答案以 Linux 5.17 为准)

    1. arch/riscv/kernel/entry.S 里的 handle_exceptionarch/riscv/kernel/head.S 里的 setup_trap_vector

    2. arch/riscv/kernel/entry.S 里的 __switch_to

    3. TrapContext 对应 pt_regsTaskContext 对应 task_struct (在 task_struct 中也包含一些其它的和调度相关的信息)

    4. tp 指向当前被打断的任务的 task_struct (参见 arch/riscv/include/asm/current.h 里的宏 current ); sscratch0

    5. sscratch 指向当前正在运行的任务的 task_struct ,这样设计可以用来区分异常来自用户态还是内核态。

    6. 所有通用寄存器, sstatus, sepc, scause

    7. 内核栈底; arch/riscv/include/asm/processor.h 里的 task_pt_regs

    8. arch/riscv/kernel/syscall_table.c 里的 sys_call_table 作为跳转表,根据系统调用编号调用。

    9. 从保存的 pt_regs 中读保存的 a0a7 到机器寄存器里,这样系统调用实现的 C 函数就会作为参数接收到这些值,返回值是将返回的 a0 写入保存的 pt_regs ,然后切换回用户态的代码负责将其“恢复”到 a0