1 | // 启动时初始化进程表 |
p - pool 表示什么?
假设我们有一个名为 pool 的数组,其中包含了多个类型为 struct proc 的元素,并且有一个指针 p 指向其中的某个元素。
当 p 指向 pool 数组的第一个元素时,p - pool 的结果将是 0,因为指针相对于数组首地址的偏移量为 0。
当 p 指向 pool 数组的第二个元素时,p - pool 的结果将是 1,因为指针相对于数组首地址的偏移量为 1。
以此类推,当 p 指向 pool 数组的第 N 个元素时,p - pool 的结果将是 N-1,因为指针相对于数组首地址的偏移量为 N-1。
总结来说,如果 p 是指向 pool 数组中第 N 个元素的指针,那么 p - pool 的结果将是 N-1。
原调度函数每次都会从 pool 数组的第一个元素开始遍历,这样会导致每次都是从第一个进程开始运行,而不是从上次运行的进程开始运行。需要修改为如下:
1 | // 调度程序永不返回。它循环执行以下操作: |
LAB1
1 | --- |
问答作业
问题一
正确进入 U 态后,程序的特征还应有:使用 S 态特权指令,访问 S 态寄存器后会报错。请同学们可以自行测试这些内容(参考 前三个测例,描述程序出错行为,同时注意注明你使用的 sbi 及其版本。
测试前三个测试用例指的是uCore-Tutorial-Code-2023S/user/src/
目录下的三个bad
测试用例,查看user
项目的 Makefile 可以发现在编译时修改CHAPTER
参数值为2_bad
即可编译运行这些测试用例。
1 | [rustsbi] RustSBI version 0.3.0-alpha.2, adapting to RISC-V SBI v1.0.0 |
第一个进程测试用例如下:
1 | int main() |
在您提供的代码中,将空指针分配给指针变量*p 后,试图对其进行解引用并将值 0 赋给该指针。由于用户模式下禁止直接访问物理内存,操作系统会检测到这个非法操作并触发异常。因此,该程序 IllegalInstruction in application, core dumped.
在 RISC-V 架构中,U 模式是最低的用户模式,用户程序无法直接访问物理内存或其他特权级别资源。这种限制是为了确保操作系统的安全性和稳定性。
第二个进程测试用例如下:
1 | int main() |
试图使用汇编语言执行 sret 指令,该指令用于从中断或异常处理程序返回。由于用户模式下禁止直接访问特权级别寄存器,操作系统会检测到这个非法操作并触发异常。因此,该程序 IllegalInstruction in application, core dumped。
第三个进程测试用例如下:
1 | int main() |
原因同上,试图使用汇编语言执行 csrr 指令,该指令用于从特权级别寄存器中读取值。由于用户模式下禁止直接访问特权级别寄存器,操作系统会检测到这个非法操作并触发异常。因此,该程序 IllegalInstruction in application, core dumped。
在操作系统代码中,触发异常后会进入void usertrap()
函数,该函数会根据 scause
寄存器的值判断异常类型,用例中的结果进入了case IllegalInstruction
,其中 IllegalInstruction = 2
。我们查阅手册 riscv-privileged.pdf
,可以查到 IllegalInstruction
的值为 2,与预期相符。
问题二
请结合用例理解 trampoline.S 中两个函数 userret
和 uservec
的作用,并回答如下几个问题:
L79: 刚进入 userret
时,a0
、a1
分别代表了什么值。
在进入userret
函数时,a0
和a1
分别代表以下值:
a0
: TRAPFRAME 的地址,指向当前进程的陷阱帧(trapframe)结构体。a1
: 用户页表的地址,即进程的页表(pagetable)。这个地址会被写入到satp
寄存器中,用于切换到用户模式的页表。
L87-L88: sfence
指令有何作用?为什么要执行该指令,当前章节中,删掉该指令会导致错误吗?
1 | csrw satp, a1 |
sfence
指令(Store Fence)的作用是确保之前的存储操作完成,并且对其他处理器上的核心可见。
执行sfence
指令的主要目的是为了保证内存访问的顺序性和一致性。在多核处理器系统中,不同的核心可能会有自己的缓存,当一个核心修改了共享内存中的数据时,为了保证其他核心能够看到这个修改,需要使用sfence
指令来刷新缓存并将修改写回共享内存。
在代码中,sfence
指令被用于确保对用户页表的修改对其他处理器上的核心可见。因为目前我只使用了单核处理器,所以不会出现多核处理器的情况,因此sfence
指令的作用是确保对用户页表的修改对当前核心可见。
因此,当前章节中,删掉该指令不会导致错误。
L96-L125: 为何注释中说要除去 a0
?哪一个地址代表 a0
?现在 a0
的值存在何处?
1 | # restore all but a0 from TRAPFRAME |
a0
是保存在 sscratch
寄存器中的,首先,该代码通过 ld
指令从 TRAPFRAME
中加载各个寄存器的值。然后,这些值被存储在相应的寄存器中,以便在恢复用户上下文时使用。
接下来,代码使用 csrrw
指令将 sscratch 寄存器的值与 a0
(即 TRAPFRAME
)进行交换。这样做是为了将用户的 a0
(TRAPFRAME
)保存在 sscratch
寄存器中,以便后续步骤可以正确地恢复用户上下文。
最后,通过 sret
指令返回到用户模式,并将控制权交给用户代码。在执行 sret
指令后,处理器将根据用户上下文中的 sepc
寄存器的值跳转到用户代码的指令地址。返回的同时,处理器还会自动恢复 sstatus
寄存器的值,以确保正确的特权级别和中断状态。
userret
:中发生状态切换在哪一条指令?为何执行之后会进入用户态?
在userret
函数中,发生状态切换的指令是sret
指令。
sret
指令用于从内核模式切换到用户模式,并将控制权交给用户代码。执行sret
指令后,处理器会根据用户上下文中的sepc
寄存器的值跳转到用户代码的指令地址。
执行sret
指令之后进入用户态的原因是,该指令会自动恢复sstatus
寄存器的值,以确保正确的特权级别和中断状态。当sret
指令执行后,处理器将从内核态切换回用户态,程序将继续执行用户代码。这意味着userret
函数成功完成了从内核切换到用户模式的过程。
L29:执行之后,a0 和 sscratch 中各是什么值,为什么?
1 | csrrw a0, sscratch, a0 |
在执行指令后,a0
和sscratch
中的值发生了互换。
假设原始a0
寄存器中的值为 X,而sscratch
寄存器中的值为 Y。执行csrrw a0, sscratch, a0
指令后,a0
寄存器中的值变为 Y,而sscratch
寄存器中的值变为 X。
这是因为csrrw
指令是一个特权指令,用于将某个 CSR(Control and Status Register)的值读取到目标寄存器,然后将目标寄存器的值写回到该 CSR 中。在这里,csrrw a0, sscratch, a0
指令将sscratch
寄存器的值读取到a0
寄存器中,同时将a0
寄存器中的值写回到sscratch
寄存器中,从而实现了两者之间的数据交换。
L32-L61: 从 trapframe 第几项开始保存?为什么?是否从该项开始保存了所有的值,如果不是,为什么?
1 |
|
进入 S 态是哪一条指令发生的?
L75-L76: ld t0, 16(a0)
执行之后,t0
中的值是什么,解释该值的由来?
1 | ld t0, 16(a0) |
ld t0, 16(a0)
就是从 trapframe
中恢复 t0
寄存器值,t0
保存了kernel_trap
的入口地址。使用 jr t0
,就跳转到了我们早先设定在 trapframe->kernel_trap
中的地址,也就是 trap.c
之中的 usertrap
函数。这个函数在 main
的初始化之中已经调用了。