在先前的文章已經有詳細介紹 RISC-V 的暫存器。由於本篇文章有閱讀原始碼的需求,所以將暫存器的對照表貼上方便讀者使用。
我們日常所使用的電腦都能夠同時處理多個任務,像是: 同時執行瀏覽器以及程式編輯器。
看似簡單的功能,作業系統其實在背後做了非常多的處理,試想: 在單核心電腦上運行 Windows 作業系統,而單核心一次僅能完成一個作業時,電腦是如何達到多工的需求呢?
其實,即使我們一次打開了很多應用程式,單核心電腦也只能在同一時間處理一項任務。為了讓使用者不易察覺出來,電腦會先花一點時間處理程式 A,再處理程式 B, C...,就像是:
A -> B -> C -> A (done!) -> B -> C -> B -> C (done!) -> B (done!)
透過在任務之間進行快速切換,做到多工的功能。
不同的程式在運行時 (Process) 也會有各自的資料,因此電腦在切換下一個 Process 前會需要將目前運行中的 Process 內的資料儲存下來。
在作業系統中,有一個特別的資料結構被用來存放 Process 的獨立資料,該資料結構被稱之為 Process control block
,其構造如下圖:
PCB content: | Description |
---|---|
Process state | 可以是 new、ready、running、waiting 或 blocked 等。 |
Process Id | 用於識別 Process,就像人類的身分證字號一樣。 |
Program counter | 待執行指令的位址。 |
registers | 該 Process 執行時 CPU 暫存器存放的資料。 |
memory limits | -- |
list of open files | 紀錄開啟了哪些檔案 |
上下文交換 (Context switch) 能讓多個 Process 共享同一個 CPU 的運算資源。
內文切換有三種常見狀況:
在本篇所閱讀的原始碼中屬於第一種狀況 (從 main 轉移到 task0)。
本篇使用
mini-riscv-os
作為教學範例。
首先,我們可以在 os.c
看到 task0
以及主迴圈的定義:
#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;
}
系統開機後,會在主畫面印出 OS start
,隨後開始進行 Context switch
。
為了順利切換到 task0
執行,我們需要將 ra (return address)
以及 sp (Stack pointer)
分別指向 task0
和 task 的 stack 位址。
ctx_task.ra = (reg_t) user_task0;
ctx_task.sp = (reg_t) &task0_stack[STACK_SIZE-1];
關於任務的堆疊空間,我們可以參考你所不知道的 C 語言:函式呼叫篇取得更進一步的資訊。
指派完成後,開始進行上下文交換:
sys_switch(&ctx_os, &ctx_task);
sys_switch 被定義在 sys.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)
根據 sys.s
,我們可以清楚的知道 sys_switch
的 payload 會分別存在 a0
以及載入到 a1
暫存器中。
至於為什麼會存放在
a0
和載入到a1
暫存器,可以參考一開始所附上的暫存器參考表。
BTW: 在 RISC-V 中,若a0
-a7
都被放滿,多餘的參數才會存放在堆疊內。
此外,ctx_save
以及 ctx_load
的定義也同樣可以在 sys.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 ==================
其目的就是將處理器的暫存器狀態儲存/載入,讓下方的程式碼可以做到上下文切換的作用:
# 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)
在 ret
指令執行後,便會將 ra
暫存器存放的內容 (函式 user_task0
的位置) 指派給程式計數器。
如此一來,上下文切換就順利完成了!