pa4 多道程式和nemu執行RT-thread

namezhyp發表於2024-11-16
首先看一下講義裡提到的yield os,這個os裡面只有兩道程式切換的模擬內容,只要做過pa3就很容易理解:
#define STACK_SIZE (4096 * 8)
typedef union {
  uint8_t stack[STACK_SIZE];
  struct { Context *cp; };
} PCB;
static PCB pcb[2], pcb_boot, *current = &pcb_boot;

static void f(void *arg) {
  while (1) {
    putch("?AB"[(uintptr_t)arg > 2 ? 0 : (uintptr_t)arg]);
    for (int volatile i = 0; i < 100000; i++) ;
    yield();
  }
}

static Context *schedule(Event ev, Context *prev) {
  current->cp = prev;
  current = (current == &pcb[0] ? &pcb[1] : &pcb[0]);
  return current->cp;
}

int main() {
  cte_init(schedule);
  pcb[0].cp = kcontext((Area) { pcb[0].stack, &pcb[0] + 1 }, f, (void *)1L);
  pcb[1].cp = kcontext((Area) { pcb[1].stack, &pcb[1] + 1 }, f, (void *)2L);
  yield();
  panic("Should not reach here!");
}

首先定義了PCB結構體,裡面只包含一個棧和上下文(在這裡上下文只考慮通用+特殊暫存器的值)。然後宣告長度2陣列來放兩道程式。 在main函式里,cte init註冊回撥函式schedule,他的功能就是在儲存當前pcb的資訊,並切換到另一個pcb。隨後建立兩個pcb上下文,然後呼叫yield(),從此開始執行兩個程式並不斷切換,達到的效果就是不斷地輸出AB。

在pa3裡,我們實現了異常響應,程式觸發yield時執行一些其他動作,然後返回到之前的位置,恢復上下文繼續執行。而現在,如果我們不返回,而是從b的棧裡,把程式B的上下文載入進來,那麼就做到了多道程式。

在yield-os裡,f()起到了類似核心執行緒的作用。而kconfig()的功能則是建立上下文。按照講義+union,如果沒有設定cp指標,那就是棧頂指標,反之則是cp,cp又在這個32K的記憶體區間裡指向了上下文。

|               |
+---------------+ <---- kstack.end   高地址
|               |
|    context    |
|               |
+---------------+ <--+
|               |    |
|               |    |
|               |    |
|               |    |
+---------------+    |
|       cp      | ---+
+---------------+ <---- kstack.start   低地址
|               |

  棧的設計是先入後出。將程式裝載入記憶體時,我們一般把棧放在最大地址的位置。同時,讓棧頂和棧底指標都指向棧的底部,也就是最大地址處。隨著棧的裝入,棧頂指標逐漸向著低地址方向移動。

  講義要求我們實現kcontext函式,這個函式的功能是初始化上下文,那麼我們需要做的就是生成一份暫存器並初始化再返回即可。在這裡,我們首先需要初始化一份ctx,就用 context *ctx = (context *)(kstack.end - sizeof(context)),然後清零。

  在riscv32裡,前8個引數透過a0到a7傳遞,之後還有引數則透過棧傳遞。返回值透過a0傳遞。所以引數arg要放入a0。entry要放入mepc(這裡我還沒完全理解,ecall指令裡,我們將原計劃的下一條命令放入mepc,這裡將entry放入mepc,那不會在和後面觸發yield時被覆蓋嗎)。為了difftest,還需要把mstatus設定0x1800。此外,我們還需要把當前的棧頂指標放入gpr[2]--sp暫存器。而pidr用於分頁目錄指標,此時我們不需要,設定為NULL。
  這樣一來,yield-os就可以正常輸出AB了。

-- ----- -- ------- ---- ----- - - - -- -------

  在rt-thread裡。講義要求實現三個函式stac_init 和兩個switch,配套的ev_handle也需要修改。先說ev_handle,這個函式就是回撥函式,每次在觸發異常響應時,在irq_handle裡被呼叫執行。對應兩個switch函式。
  stack_init函式的作用是建立一套上下文。此時就不能直接呼叫CTE的kcontext函式,因為引數不匹配。rt-therad裡還多了一個texit函式,tentry是原本要執行的核心執行緒,而texit是執行緒執行完後負責清理工作的函式。其實就是tentry執行完再執行texit的意思。 按照講義,我們可以設定一個包裹函式當作核心執行緒,把tentry para texit三個引數打包成arg,傳給包裹函式。包裹函式收到以後再拆開。
  值得一提的是,在這裡不需要用內聯彙編,會把這裡搞的更麻煩,只要把三個引數傳給包裹函式,正常呼叫kcontext就行。但在實際做的時候我也遇到了一個問題:kcontext的首個引數是area結構體,也就是堆疊的首尾指標,他的大小應該設定為多少?stack_addr就是首,那尾呢?我的解決思路就是從PCB結構體裡摳出來stack_size,發現是16384(或者是別的值),就設定為了這個值。另:stack_init裡不可以呼叫rt_thread_self,因為PCB還沒建立好。
  兩個switch函式稍微麻煩一些,但講義裡也已經給了提示。可以利用PCB裡面的user_data。講義提示user_data可能本身也是一個有用的值,那我們怎麼呢?和stack_init的思路類似,我們宣告一個結構體,裡面包含from to 和原本的user_data三個變數。
  在兩個switch裡,先宣告結構體變數(用static,擴充生命週期,但它還是一個區域性變數),用rt_thread_self取出PCB,然後拿出這個user_data,和from to 打包塞進去,然後觸發Yield。在ev_handle裡,再把user_data取出,此時的user_data存的是結構體指標,此時拆開,就得到了from to 和原本的user_data按需處理即可。

相關文章