diff --git a/01-HelloOs/README.md b/01-HelloOs/README.md index 01cc50c..a2f4940 100644 --- a/01-HelloOs/README.md +++ b/01-HelloOs/README.md @@ -2,7 +2,7 @@ ## Build & Run -``` +```sh user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/01-HelloOs (master) $ make clean rm -f *.elf diff --git a/03-MultiTasking/README.md b/03-MultiTasking/README.md index 38dd11c..f2f7121 100644 --- a/03-MultiTasking/README.md +++ b/03-MultiTasking/README.md @@ -1,17 +1,21 @@ -# 04-MultiTasking +# 03-MultiTasking ## Build & Run -``` -user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/04-MultiTasking (master) +```sh +user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/03-MultiTasking +(master) $ make clean rm -f *.elf -user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/04-MultiTasking (master) +user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/03-MultiTasking +(master) $ make -riscv64-unknown-elf-gcc -nostdlib -fno-builtin -mcmodel=medany -march=rv32ima -mabi=ilp32 -T os.ld -o os.elf start.s sys.s lib.c task.c os.c user.c +riscv64-unknown-elf-gcc -nostdlib -fno-builtin -mcmodel=medany -march=rv32ima +-mabi=ilp32 -T os.ld -o os.elf start.s sys.s lib.c task.c os.c user.c -user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/04-MultiTasking (master) +user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/03-MultiTasking +(master) $ make qemu Press Ctrl-A and then X to exit QEMU qemu-system-riscv32 -nographic -smp 4 -machine virt -bios none -kernel os.elf diff --git a/04-TimerInterrupt/README.md b/04-TimerInterrupt/README.md index 93887ac..54033d2 100644 --- a/04-TimerInterrupt/README.md +++ b/04-TimerInterrupt/README.md @@ -2,7 +2,7 @@ ## Build & Run -``` +```sh user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/04-TimerInterrupt (master) $ make clean rm -f *.elf diff --git a/05-Preemptive/README.md b/05-Preemptive/README.md index fe2ec46..34bd099 100644 --- a/05-Preemptive/README.md +++ b/05-Preemptive/README.md @@ -2,7 +2,7 @@ ## Build & Run -``` +```sh user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/05-Preemptive (master) $ make clean rm -f *.elf diff --git a/doc/tw/01-HelloOs.md b/doc/tw/01-HelloOs.md index 9c03d75..507767c 100644 --- a/doc/tw/01-HelloOs.md +++ b/doc/tw/01-HelloOs.md @@ -109,7 +109,34 @@ clean: rm -f *.elf ``` -你可以看到我們使用 riscv64-unknown-elf-gcc 去編譯,然後用 qemu-system-riscv32 去執行, 01-HelloOs的執行過程如下: +Makefile 的有些語法不容易懂,特別是下列的符號: + +``` +$@ : 該規則的目標文件 (Target file) +$* : 代表 targets 所指定的檔案,但不包含副檔名 +$< : 依賴文件列表中的第一個依賴文件 (Dependencies file) +$^ : 依賴文件列表中的所有依賴文件 +$? : 依賴文件列表中新於目標文件的文件列表 +$* : 代表 targets 所指定的檔案,但不包含副檔名 + +?= 語法 : 若變數未定義,則替它指定新的值。 +:= 語法 : make 會將整個 Makefile 展開後,再決定變數的值。 +``` + +所以上述 Makefile 中的下列兩行: + +```Makefile +os.elf: start.s os.c + $(CC) $(CFLAGS) -T os.ld -o os.elf $^ +``` + +其中的 `$^` 被代換成 `start.s os.c` ,於是整個 `$(CC) $(CFLAGS) -T os.ld -o os.elf $^` 整行展開後就變成了下列指令。 + +``` +riscv64-unknown-elf-gcc -nostdlib -fno-builtin -mcmodel=medany -march=rv32ima -mabi=ilp32 -T os.ld -o os.elf start.s os.c +``` + +在 Makefile 中我們使用 riscv64-unknown-elf-gcc 去編譯,然後用 qemu-system-riscv32 去執行, 01-HelloOs 的執行過程如下: ``` user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/01-HelloOs (master) diff --git a/doc/tw/02-ContextSwitch.md b/doc/tw/02-ContextSwitch.md index e69de29..8181567 100644 --- a/doc/tw/02-ContextSwitch.md +++ b/doc/tw/02-ContextSwitch.md @@ -0,0 +1,175 @@ +# 02-ContextSwitch + +專案 -- https://github.com/ccc-c/mini-riscv-os/tree/master/02-ContextSwitch + +在前一章的 [01-HelloOs](01-HelloOs.md) 中我們介紹了如何在 RISC-V 架構下印出字串到 UART 序列埠的方法,這一章我們將往作業系統邁進,介紹神秘的《內文切換》(Context-Switch) 技術。 + +## os.c + +以下是 02-ContextSwitch 的主程式 os.c ,該程式除了 os 本身以外,還有一個《任務》(task)。 + +* https://github.com/ccc-c/mini-riscv-os/blob/master/02-ContextSwitch/os.c + +```cpp +#include "os.h" + +#define STACK_SIZE 1024 +uint8_t task0_stack[STACK_SIZE]; +struct context ctx_os; +struct context ctx_task; + +extern void sys_switch(); + +void user_task0(void) +{ + lib_puts("Task0: Context Switch Success !\n"); + while (1) {} // stop here. +} + +int os_main(void) +{ + lib_puts("OS start\n"); + ctx_task.ra = (reg_t) user_task0; + ctx_task.sp = (reg_t) &task0_stack[STACK_SIZE-1]; + sys_switch(&ctx_os, &ctx_task); + return 0; +} +``` + +任務 task 是一個函數,在上面的 os.c 裡是 user_task0,為了進行切換,我們將 ctx_task.ra 設為 user_task0,由於 ra 是 return address 暫存器,其功能為在函數返回執行 ret 指令時,用 ra 取代程式計數器 pc,這樣在執行 ret 指令時就能跳到該函數去執行。 + +```cpp + ctx_task.ra = (reg_t) user_task0; + ctx_task.sp = (reg_t) &task0_stack[STACK_SIZE-1]; + sys_switch(&ctx_os, &ctx_task); +``` + +但是每個任務都必須有堆疊空間,才能在 C 語言環境中進行函數呼叫。所以我們分配了 task0 的堆疊空間,並用 ctx_task.sp 指向堆疊開頭。 + +然後我們呼叫了 `sys_switch(&ctx_os, &ctx_task)` 從主程式切換到 task0,其中的 sys_switch 是個位於 [sys.s](https://github.com/ccc-c/mini-riscv-os/blob/master/02-ContextSwitch/sys.s) 裏組合語言函數,內容如下: + +```s +# Context switch +# +# void sys_switch(struct context *old, struct context *new); +# +# Save current registers in old. Load from new. + +.globl sys_switch +.align 4 +sys_switch: + ctx_save a0 # a0 => struct context *old + ctx_load a1 # a1 => struct context *new + ret # pc=ra; swtch to new task (new->ra) +``` + +在 RISC-V 中,參數主要放在 a0, a1, ..., a7 這些暫存器當中,當參數超過八個時,才會放在堆疊裏傳遞。 + +sys_switch 對應的 C 語言函數如下: + +```cpp +void sys_switch(struct context *old, struct context *new); +``` + +上述程式的 a0 對應 old (舊任務的 context),a1 對應 new (新任務的context),整個 sys_switch 的功能是儲存舊任務的 context ,然後載入新任務 context 開始執行。 + +最後的一個 ret 指令非常重要,因為當新任務的 context 載入時會把 ra 暫存器也載進來,於是當 ret 執行時,就會設定 pc=ra,然後跳到新任務 (例如 `void user_task0(void)` 去執行了。 + +sys_switch 中的 `ctx_save` 和 `ctx_load` 是兩個組合語言巨集,其定義如下: + +```s +# ============ MACRO ================== +.macro ctx_save base + sw ra, 0(\base) + sw sp, 4(\base) + sw s0, 8(\base) + sw s1, 12(\base) + sw s2, 16(\base) + sw s3, 20(\base) + sw s4, 24(\base) + sw s5, 28(\base) + sw s6, 32(\base) + sw s7, 36(\base) + sw s8, 40(\base) + sw s9, 44(\base) + sw s10, 48(\base) + sw s11, 52(\base) +.endm + +.macro ctx_load base + lw ra, 0(\base) + lw sp, 4(\base) + lw s0, 8(\base) + lw s1, 12(\base) + lw s2, 16(\base) + lw s3, 20(\base) + lw s4, 24(\base) + lw s5, 28(\base) + lw s6, 32(\base) + lw s7, 36(\base) + lw s8, 40(\base) + lw s9, 44(\base) + lw s10, 48(\base) + lw s11, 52(\base) +.endm +# ============ Macro END ================== +``` + +RISC-V 行程切換時必須儲存 ra, sp, s0, ... s11 等暫存器,上述的程式碼基本上是我從 xv6 這個教學作業系統中抄來後修改為 RISC-V 32 位元版的,其原始網址如下: + +* https://github.com/mit-pdos/xv6-riscv/blob/riscv/kernel/swtch.S + +在 [riscv.h](https://github.com/ccc-c/mini-riscv-os/blob/master/02-ContextSwitch/riscv.h) 這個表頭檔中,我們定義了 struct context 這個對應的 C 語言結構,其內容如下: + +```cpp +// Saved registers for kernel context switches. +struct context { + reg_t ra; + reg_t sp; + + // callee-saved + reg_t s0; + reg_t s1; + reg_t s2; + reg_t s3; + reg_t s4; + reg_t s5; + reg_t s6; + reg_t s7; + reg_t s8; + reg_t s9; + reg_t s10; + reg_t s11; +}; +``` + +這樣,我們就介紹完《內文切換》任務的細節了,於是下列主程式就能順利地從 os_main 切換到 user_task0 了。 + +```cpp +int os_main(void) +{ + lib_puts("OS start\n"); + ctx_task.ra = (reg_t) user_task0; + ctx_task.sp = (reg_t) &task0_stack[STACK_SIZE-1]; + sys_switch(&ctx_os, &ctx_task); + return 0; +} +``` + +以下是整個專案的執行結果: + +```sh +user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/03-ContextSwitch (master) +$ make +riscv64-unknown-elf-gcc -nostdlib -fno-builtin -mcmodel=medany -march=rv32ima -mabi=ilp32 -T os.ld -o os.elf start.s sys.s lib.c os.c + +user@DESKTOP-96FRN6B MINGW64 /d/ccc109/sp/11-os/mini-riscv-os/03-ContextSwitch (master) +$ make qemu +Press Ctrl-A and then X to exit QEMU +qemu-system-riscv32 -nographic -smp 4 -machine virt -bios none -kernel os.elf +OS start +Task0: Context Switch Success ! +QEMU: Terminated +``` + +以上就是 RISC-V 裏的《內文切換》(Context-Switch) 機制的實作方法! diff --git a/doc/tw/03-MultiTasking.md b/doc/tw/03-MultiTasking.md index 1f85102..1712283 100644 --- a/doc/tw/03-MultiTasking.md +++ b/doc/tw/03-MultiTasking.md @@ -1,5 +1,239 @@ # 03-MultiTasking +[os.c]:https://github.com/ccc-c/mini-riscv-os/blob/master/03-MultiTasking/os.c + +[task.c]:https://github.com/ccc-c/mini-riscv-os/blob/master/03-MultiTasking/task.c + +[user.c]:https://github.com/ccc-c/mini-riscv-os/blob/master/03-MultiTasking/user.c + +[sys.s]:https://github.com/ccc-c/mini-riscv-os/blob/master/03-MultiTasking/sys.s + 專案 -- https://github.com/ccc-c/mini-riscv-os/tree/master/03-MultiTasking +在前一章的 [02-ContextSwitch](02-ContextSwitch.md) 中我們介紹了 RISC-V 架構下的內文切換機制,這一章我們將進入多行程的世界,介紹如何撰寫一個《協同式多工》作業系統。 + +## 協同式多工 + +現代的作業系統,都有透過時間中斷強制中止行程的《搶先》(Preemptive) 功能,這樣就能在某行程霸佔 CPU 太久時,強制將其中斷,切換給別的行程執行。 + +但是在沒有時間中斷機制的系統中,作業系統《無法中斷惡霸行程》,因此必須依賴各個行程主動交回控制權給作業系統,才能讓所有行程都有機會執行。 + +這種仰賴自動交還機制的多行程系統,稱為《協同式多工》(Coorperative Multitasking) 系統。 + +1991 年微軟推出的 Windows 3.1 ,還有單板電腦 arduino 上的 [HeliOS](https://github.com/MannyPeterson/HeliOS),都是《協同式多工》機制的作業系統。 + +在本章中,我們將設計一個在 RISC-V 處理器上的《協同式多工》作業系統。 + +首先讓我們來看看該系統的執行結果。 + +```sh +$ make qemu +Press Ctrl-A and then X to exit QEMU +qemu-system-riscv32 -nographic -smp 4 -machine virt -bios none -kernel os.elf +OS start +OS: Activate next task +Task0: Created! +Task0: Now, return to kernel mode +OS: Back to OS + +OS: Activate next task +Task1: Created! +Task1: Now, return to kernel mode +OS: Back to OS + +OS: Activate next task +Task0: Running... +OS: Back to OS + +OS: Activate next task +Task1: Running... +OS: Back to OS + +OS: Activate next task +Task0: Running... +OS: Back to OS + +OS: Activate next task +Task1: Running... +OS: Back to OS + +OS: Activate next task +Task0: Running... +OS: Back to OS + +OS: Activate next task +Task1: Running... +OS: Back to OS + +OS: Activate next task +Task0: Running... +QEMU: Terminated +``` + +您可以看到該系統在兩個任務 Task0, Task1 之間不斷切換,但實際上的切換過程如下: + +``` +OS=>Task0=>OS=>Task1=>OS=>Task0=>OS=>Task1 .... +``` + +## 使用者任務 [user.c] + +在 [user.c] 中我們定義了 user_task0 與 user_task1 兩個 task,並且在 user_init 函數終將這兩個 task 初始化。 + +* https://github.com/ccc-c/mini-riscv-os/blob/master/03-MultiTasking/user.c + +```cpp +#include "os.h" + +void user_task0(void) +{ + lib_puts("Task0: Created!\n"); + lib_puts("Task0: Now, return to kernel mode\n"); + os_kernel(); + while (1) { + lib_puts("Task0: Running...\n"); + lib_delay(1000); + os_kernel(); + } +} + +void user_task1(void) +{ + lib_puts("Task1: Created!\n"); + lib_puts("Task1: Now, return to kernel mode\n"); + os_kernel(); + while (1) { + lib_puts("Task1: Running...\n"); + lib_delay(1000); + os_kernel(); + } +} + +void user_init() { + task_create(&user_task0); + task_create(&user_task1); +} +``` + +## 主程式 [os.c] + +然後在作業系統主程式 os.c 當中,我們使用大輪迴的方式,安排每個行程按照順序輪迴的執行。 + +* https://github.com/ccc-c/mini-riscv-os/blob/master/03-MultiTasking/os.c + +```cpp +#include "os.h" + +void os_kernel() { + task_os(); +} + +void os_start() { + lib_puts("OS start\n"); + user_init(); +} + +int os_main(void) +{ + os_start(); + + int current_task = 0; + while (1) { + lib_puts("OS: Activate next task\n"); + task_go(current_task); + lib_puts("OS: Back to OS\n"); + current_task = (current_task + 1) % taskTop; // Round Robin Scheduling + lib_puts("\n"); + } + return 0; +} +``` + +上述排程方法原則上和 [Round Robin Scheduling](https://en.wikipedia.org/wiki/Round-robin_scheduling) 一致,但是 Round Robin Scheduling 原則上必須搭配時間中斷機制,但本章的程式碼沒有時間中斷,所以只能說是協同式多工版本的 Round Robin Scheduling。 + +協同式多工必須依賴各個 task 主動交回控制權,像是在 user_task0 裏,每當呼叫 os_kernel() 函數時,就會呼叫內文切換機制,將控制權交回給作業系統 [os.c] 。 + +```cpp +void user_task0(void) +{ + lib_puts("Task0: Created!\n"); + lib_puts("Task0: Now, return to kernel mode\n"); + os_kernel(); + while (1) { + lib_puts("Task0: Running...\n"); + lib_delay(1000); + os_kernel(); + } +} +``` + +其中 [os.c] 的 os_kernel() 會呼叫 [task.c] 的 task_os() + +```cpp +void os_kernel() { + task_os(); +} +``` + +而 task_os() 則會呼叫組合語言 [sys.s] 裏的 sys_switch 去切換回作業系統中。 + +```cpp +// switch back to os +void task_os() { + struct context *ctx = ctx_now; + ctx_now = &ctx_os; + sys_switch(ctx, &ctx_os); +} +``` + +於是整個系統就在 os_main(), user_task0(), user_task1() 三者的協作下,以互相禮讓的方式輪流執行著。 + +[os.c] + +```cpp +int os_main(void) +{ + os_start(); + + int current_task = 0; + while (1) { + lib_puts("OS: Activate next task\n"); + task_go(current_task); + lib_puts("OS: Back to OS\n"); + current_task = (current_task + 1) % taskTop; // Round Robin Scheduling + lib_puts("\n"); + } + return 0; +} +``` + +[user.c] + +```cpp +void user_task0(void) +{ + lib_puts("Task0: Created!\n"); + lib_puts("Task0: Now, return to kernel mode\n"); + os_kernel(); + while (1) { + lib_puts("Task0: Running...\n"); + lib_delay(1000); + os_kernel(); + } +} + +void user_task1(void) +{ + lib_puts("Task1: Created!\n"); + lib_puts("Task1: Now, return to kernel mode\n"); + os_kernel(); + while (1) { + lib_puts("Task1: Running...\n"); + lib_delay(1000); + os_kernel(); + } +} +``` + +以上就是 RISC-V 處理器上一個具體而微的協同式多工作業系統範例了!