#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按需處理即可。