add mini-riscv-os tw document 01-03

This commit is contained in:
ccckmit
2020-11-15 09:51:16 +08:00
parent f262ca56ba
commit eca3c684fd
7 changed files with 450 additions and 10 deletions

View File

@@ -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)

View File

@@ -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) 機制的實作方法!

View File

@@ -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 處理器上一個具體而微的協同式多工作業系統範例了!