Skip to content

lab

lab 2

  1. 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_shandler_s, 由于不是时钟中断, 会跳到 else 分支, 如果里面加输出会看到程序输出; 之后回到 trap_s, sret 又回到 ecall, 但是进入 trap_s 前是 U 态, 所以会一直在 ecall 指令重复进入 trap_s, 达到死循环的目的; 前面的输出也会循环输出

  2. lab2 中样例进入死循环的问题. 经过 GDB 调试, 发现倒数第二条样例最后一个完成的是第二个进程, 而在完成上一个样例时调用的是 task_init() + init_test_case() + schedule(), 由于 schedule() 没有初始化 current, 所以 current 仍然指向 pid = 2, 此时 __switch_topid = 2 进程保存在栈上的 ra 更新成现在的 ra, 即 __switch_to 的下一行. 当调度算法运行到需要切换到 pid = 2 时, 从栈上恢复的 ra__switch_to 的下一行, 即 return. 所以出现了再一次执行 return 的情况, 导致程序出错. 解决方法: 将 schedule() 改成 call_first_process(), 初始化 current 即可.

  3. 进程第 \(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

  1. s mode 的 putchar() 函数向某个地址存储字符

    UART16550A_DR (volatile unsigned char *)0x10000000

    这是 QEMU 设置的 MMIO 地址, 硬件

  2. 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() 流程

  1. user fork()
  2. u_syscall()
  3. trap_s
  4. handler_s
  5. syscall

wait() 流程

  1. user fork()
  2. u_syscall()
  3. trap_s
  4. handler_s
  5. syscall
  6. __switch_to