lab
lab 2
-
lab2 中, 我们是什么时候第一次进入U态的?
在
__init_sepc中调用sret时. 因为程序一开始执行main函数, 到call_first_process时选择第一个进程, 进行一次__switch_to. 在退出时会进入__init_sepc中, 此时的SPP = 0, 调用sret会将特权级设成 U 态, 所以此时第一次进入 U 态.特别地, 执行
sret进入 U 态跳转到0x80200000, 即head.S中_start的位置. 此时执行call main后,main()的第一句puts()需要 S 态权限, U 态执行时会触发trap_s, 而sepc被设置为ecall指令的位置, 此时执行trap_s到handler_s, 由于不是时钟中断, 会跳到else分支, 如果里面加输出会看到程序输出; 之后回到trap_s,sret又回到ecall, 但是进入trap_s前是 U 态, 所以会一直在ecall指令重复进入trap_s, 达到死循环的目的; 前面的输出也会循环输出 -
lab2 中样例进入死循环的问题. 经过 GDB 调试, 发现倒数第二条样例最后一个完成的是第二个进程, 而在完成上一个样例时调用的是
task_init() + init_test_case() + schedule(), 由于schedule()没有初始化current, 所以current仍然指向pid = 2, 此时__switch_to将pid = 2进程保存在栈上的ra更新成现在的ra, 即__switch_to的下一行. 当调度算法运行到需要切换到pid = 2时, 从栈上恢复的ra为__switch_to的下一行, 即return. 所以出现了再一次执行return的情况, 导致程序出错. 解决方法: 将schedule()改成call_first_process(), 初始化current即可. -
进程第 \(1\) 次 / 第 \(n\) 次切换流程:
=====================================================================================
===============================A已经启动,B第一次启动====================================
=====================================================================================
A进程执行中,发生时钟中断
在trap_s中将中断上下文压栈
栈顶 栈底
+--------------------------------------------------------------------+
|Atask_struct saved register |
+--------------------------------------------------------------------+
按顺序依次 do_timer --> schedule --> switch_to (还有其他函数,在此不做展示)
+--------------------------------------------------------------------+
|Atask_struct (switch_to)(schedule)(do_timer) ... saved register |
+--------------------------------------------------------------------+
↑
sp, ra → switch_to 函数代码地址
现在执行 __switch_to, 保存了 A 进程的进程上下文,加载了 B 进程的进程上下文
+--------------------------------------------------------------------+
|Atask_struct (switch_to)(schedule)(do_timer)saved register |
+--------------------------------------------------------------------+
+--------------------------------------------------------------------+
|Btask_struct |
+--------------------------------------------------------------------+
↑
sp,
ra → ?
__switch_to 通过 ret 跳转到 B 进程的 ra 处,此时的 ra 的值是我们初始化的值
1. 现在处于中断处理的 S 模式,开始执行代码的时候需要用 sret 指令返回 sepc 开始执行。
2. 第一次进入 B 进程,B 进程栈上什么也没有
因此需要一个特殊的启动函数 init_sepc ,他需要完成两件事情
1. sepc 设置成任务真正要开始执行的代码地址
2. 直接利用 sret 开始执行
=====================================================================================
===============================一个正常的切换流程=======================================
=====================================================================================
A进程执行中,发生时钟中断
+--------------------------------------------------------------------+
|Atask_struct saved register |
+--------------------------------------------------------------------+
按顺序依次 do_timer --> schedule --> switch_to 函数
+--------------------------------------------------------------------+
|Atask_struct (switch_to)(schedule)(do_timer)saved register |
+--------------------------------------------------------------------+
↑
sp, ra → switch_to 函数代码地址
现在执行 __switch_to, 保存了 A 进程的 callee saved,加载了 B 进程的 callee saved
+--------------------------------------------------------------------+
|Atask_struct (switch_to)(schedule)(do_timer)saved register |
+--------------------------------------------------------------------+
+--------------------------------------------------------------------+
|Btask_struct (switch_to)(schedule)(do_timer)saved register |
+--------------------------------------------------------------------+
↑
sp, ra → switch_to 函数代码地址
1. __switch_to 通过 ret 跳转到 B 进程的 ra 处
2. 继续执行,结束 switch_to 函数
3. 继续执行,结束 schedule 函数
4. 继续执行,结束 do_timer 函数
5. 继续执行 trap_s 的剩余部分
trap_s 剩余部分代码,恢复 B 进程的 saved register
+--------------------------------------------------------------------+
|Btask_struct saved register |
+--------------------------------------------------------------------+
↑
sp
最后,trap_s 使用 sepc 跳转回中断发生前代码执行的位置。
(因此 trap_s 需要保护 sepc 寄存器,即将 sepc 寄存器也保存在栈上一份)
lab 3
sscratch, sepc, ra 设置
正常进入 trap_s: sscratch 存内核栈, 与 sp 交换后变成用户栈, sp 变成内核栈
__switch_to 时同时交换 sp 和 sscratch, 即不同进程的内核栈和用户栈
第一次进入某个进程 task->sscratch 初始值是用户栈; task->sp 初始值是 task[i] + PAGE_SIZE, 即内核栈底; task->ra 初始值是 __init_sepc
fork() 出的新进程 task->sscratch 初始值是进入 fork syscall 时的 sscratch, 即用户栈指针(virtual); task->sp = register(31)*8; task->ra = trap_s_bottom
lab 4
-
s mode 的
putchar()函数向某个地址存储字符UART16550A_DR (volatile unsigned char *)0x10000000这是 QEMU 设置的 MMIO 地址, 硬件
-
U 态
prinf(s, ...)调用流程:调用 U 态 的
vprintfmt(putchar, s, vl)函数, 将格式字符串 + 参数列表转换为输出的字符串, 存储在buffer中; (这里的putchar()是 U 态的putchar, 即static inline int putchar(int c) { buffer[tail++] = (char)c; return 0; })调用
u_syscall(SYS_WRITE, (uint64_t)fd, (uint64_t)buffer, (uint64_t)tail, 0, 0, 0);在
u_syscall()中用汇编,a7传入syscall_num,a0~a5传入参数, 调用ecall, 返回值存在a0, a1中ecall唤起 S 态中断, 进入 trap, 之后进入handler_s()handler_s()判断是否是 ecall from U mode, 之后调用syscall(a7, a0, a1, a2, a3, a4, a5), 返回值存入a0, a1; 其中所有寄存器都需要读取/更改进入handler_s()之前保存在栈上的值syscall()中用 S 态putchar()输出到UART, 完成输出
lab5
用户空间(virtual): 0x01000000 代码, 0x01001000 用户栈
内核空间: task[i] 为内核栈起始位置
fork() 流程
- user
fork() u_syscall()trap_shandler_ssyscall
wait() 流程
- user
fork() u_syscall()trap_shandler_ssyscall__switch_to