MIT 6.828 - 6. Lab 06: User-level Threads and Alarm


實驗總結

  1. 本次實驗用時約 20 個小時。
  2. 收獲是對 context switch 的理解更深入了,了解了進程調度的一種良好設計(即用協程分割控制流,使代碼更簡潔)。

遇到的困難包括:

  1. 沒有交叉的 gdb,調試困難。

實驗結束后的全部代碼在:https://github.com/monkey2000/xv6-riscv/tree/syscall/

測試結果:

answers-syscall.txt: OK 
uthread: 
$ make qemu-gdb
OK (2.3s) 
running alarmtest: 
$ make qemu-gdb
(3.7s) 
  alarmtest: test0: OK 
  alarmtest: test1: OK 
usertests: 
$ make qemu-gdb
OK (91.4s) 
time: OK 
Score: 100/100

0. 實驗准備

實驗指導鏈接

上來直接:

$ cd xv6-riscv-fall19
$ git checkout syscall

1. Warmup: RISC-V assembly

這塊要回答幾個問題,打開 user/call.cuser/call.asm ,有如下代碼片段:

int g(int x) {
  return x+3;
}

int f(int x) {
  return g(x);
}

void main(void) {
  printf("%d %d\n", f(8)+1, 13);
  exit(0);
}
000000000000001c <main>:

void main(void) {
  1c:	1141                	addi	sp,sp,-16
  1e:	e406                	sd	ra,8(sp)
  20:	e022                	sd	s0,0(sp)
  22:	0800                	addi	s0,sp,16
  printf("%d %d\n", f(8)+1, 13);
  24:	4635                	li	a2,13                              ; printf("%d %d\n", f(8)+1, 13);
  26:	45b1                	li	a1,12
  28:	00000517          	auipc	a0,0x0
  2c:	75050513          	addi	a0,a0,1872 # 778 <malloc+0xea>
  30:	00000097          	auipc	ra,0x0
  34:	5a0080e7          	jalr	1440(ra) # 5d0 <printf>
  exit(0);
  38:	4501                	li	a0,0
  3a:	00000097          	auipc	ra,0x0
  3e:	1fe080e7          	jalr	510(ra) # 238 <exit>


... lines omitted

00000000000005d0 <printf>:

void
printf(const char *fmt, ...)
{

....

下面來回答問題:

  1. Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
    • 根據 riscv user-level isa (在 doc/riscv-calling.pdf) , a0-a7 和 fa0-fa7 共計 16 個寄存器會用於傳遞參數
    • 具體而言,根據以上代碼,可以得知 a2 寄存器用於存傳給 printf 的參數 13
  2. Where is the function call to f from main? Where is the call to g? (Hint: the compiler may inline functions.)
    • 注意括號里的話。根據筆算我們知道 f(8)+1 = 12,又發現 main 中並為調用 f 函數,說明 f 在編譯期被直接優化成一個常量 12 塞到 a1 寄存器里了。
  3. At what address is the function printf located?
    • 觀察 main 函數,在調用時使用了代碼 auipc ra,0x0jalr 1440(ra),前者取當前指令的 pc 加上 0x0 存入 ra,后者跳轉到 ra + 1440。計算知 0x0000000000000030 + 1440 = 0x00000000000005d0。經驗證是 printf 的入口地址。
  4. What value is in the register ra just after the jalr to printf in main?
    • 此題需要看 rv spec 。jalr 指令完成后,ra 寄存器會存儲返回點位置(也即 pc + 4 )

2. Uthread: switching between threads

這題並不難,我給想復雜了。

首先我們把 Hint 里面的問題解決:thread_switch needs to save/restore only the callee-save registers. Why?
這個是因為協程切換的過程本質是一個函數調用,因此 caller-save registers 是被調用者(如 thread_a() )保存好的。

然后我們研究以下代碼該怎么寫(抄):

  1. 首先打開 kernel/swtch.S ,查閱 riscv calling convention ,驗證這段代碼可以完成寄存器的切換。注意 ra 表示返回地址,sp 表示當前棧頂。直接復制到 user/uthread_switch.S 即可。
  2. 接着在 kernel/proc.h 中,找到上述代碼配套的 context 結構體聲明,復制到 user/uthread.c 中。
  3. 修改幾行代碼。當發生協程切換時調用 uthread_switch(old_ctx, new_ctx),完成寄存器狀態的切換;當新建協程時,將 ra 設為協程入口點地址,sp 設為 thread.stack 的最高地址(棧底)。

第三部相對有思維量的代碼如下,可以證明是正確的:

t->ctx.ra = (uint64) func;
t->ctx.sp = (uint64) (&t->stack) + STACK_SIZE;

3. Alarm

這塊是以 alarm 為例實現一個 signal 系統,即 signal 觸發的時候調用進程注冊的 signal handler,運行結束后返回原來進程的位置,恢復狀態。

首先,按照實驗指導,添加 sigalarm 和 sigreturn 兩個系統調用:

// kernel/signal.c
// Only for lab 06: syscall
// sys_sigalarm() and sys_sigreturn() are implemented here
//

#include "types.h"
#include "riscv.h"
#include "defs.h"
#include "param.h"
#include "spinlock.h"
#include "proc.h"
#include "signal.h"

uint64 sys_sigalarm(void) {
    struct proc *p = myproc();
    int interval; uint64 handler_addr;
    if(argint(0, &interval) < 0 || argaddr(1, &handler_addr) < 0)
        return -1;
    
    p->alarm_interval = interval;
    p->alarm_handler = handler_addr;
    p->alarm_last_tick = ticks;

    return 0;
}

uint64 sys_sigreturn(void) {
    struct proc *p = myproc();
    p->alarm_state = 0;
    memmove(p->tf, &p->alarm_tf, sizeof(struct trapframe));
    return 0;
}

這里我給 proc 結構體新增了一些字段,來存儲 alarm 相關的信息:

// Per-process state
struct proc {
//... omitted
  int alarm_interval;          // Alarm's interval (0 for disabled)
  uint64 alarm_handler;        // Virtual address of the alarm handler (can be 0 due to xv6-rv's userspace memory layout)
  uint64 alarm_last_tick;      // Ticks of the last call
  struct trapframe alarm_tf;   // trapframe for storing original tf
  uint alarm_state;            // 1 if the handler hasn't return
};

前三個變量很好理解,這里解釋一下 alarm_tfalarm_state 的意義:

  1. alarm_tf 用於在調用 alarm handler 的過程當中,存儲原有的 trapframe(用戶態的寄存器狀態),這樣在 sigreturn() 中可以恢復這個狀態,實現恢復執行。
  2. alarm_state 用於防止因為上一個 alarm 信號還沒處理完導致的重入


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM