由於下面在追蹤 xv6 的相關議題,如架構與啟動,會涉及到一些暫存器的操作,以及一些暫存器操作的指令,或是在進行 System call 時,涉及的特權模式的變化。因此在這一邊簡單的介紹 RISC-V 相關的暫存器以及 CSR 和相關的指令以及特權模式。最後面提及作業系統中 Microkernel 和 Monolithic kernel 設計上的差異。
ISA 指令集架構 (Instruction Set Architecture) :
為整個電腦架構的一部份,主要是與程式設計的部分有關,包含指令集,暫存器等等, 指令集會在微架構 (microarchitecture) 上執行。微架構用來描述電腦各個元件之間如何構成與如何去執行指令集。不同微架構的電腦可以執行相同的指令集,如 Intel core 和 AMD Ryzen 共用 x86 指令集架構。指令集架構中的指令集可能會有不同的數量與長度,指令集可以分成 CISC (Complex Instruction Set Computer) 和 RISC (Reduced Instruction Set Computer),RISC-V 顧名思義屬於 RISC 的部分。
RISC-V 為開源指令集架構 (ISA),起源於 2010 年加州大學柏克萊分校。RISC-V 並非單一的 ISA,而是指整個 RISC-V ISA 的家族 RISC-V 有一個基本的指令集,以及許多擴充指令集 (下面將介紹 RISC-V 指令集使用了模組化設計)。
RV[AAA][BBB]
Extension | Description |
---|---|
I | 整數 |
M | 整數乘法和除法 |
A | Atomics |
F | 單精度浮點數 |
D | 倍精度浮點數 |
G | General Purpose (所有以上指令集的總和) = IMAFD |
C | 16-bit Compressed Instructions (壓縮指令集) |
Xext | Non-standard extension "ext" |
RISC-V 與其他 ISA 不同之處在於使用了模組化的設計,RV32I 為最基本 (Basic) 的指令集架構 (ISA),不會如同 x86 指令集隨著時間不斷的增長 (x86為了相容性考量,需要相容於舊版本的系統,因此指令集十分的龐大),模組化設計可以根據使用需求進行指令集的擴充,硬體可以選擇是否要包含那一些擴充指令集,這讓 RISC-V 能夠變得更小,更有效率,更低功耗,可以運用在嵌入式的系統上,且編譯器可以更方便的對程式碼進行優化。
而擴充指令集的命名方式如同上面所示,RV32IMFD 是將 RV32I 加上 RV32M, RV32F, RV32D 的擴充。
函式呼叫可以分成6個部分
jal
跳躍)。ret
)。為了性能考慮,我們最好把函式的一些參數都放在暫存器中,而不是將變數通通推入 Stack 中讓我們需要不斷的去存取記憶體,而如果暫存器數量少,我們會需要頻繁的恢復暫存器以及儲存暫存器的值,這也會需要不斷的存取記憶體。
由於在 RISC-V 中有足夠多的暫存器,可以確保函式呼叫有足夠好的效能,既可以把函式變數 (操作數) 存放於暫存器中,又可以避免不斷的存取記憶體恢復暫存器的值。RISC-V 中的暫存器分為臨時暫存器(函式呼叫時不保留暫存器的值),另外一種為儲存暫存器。使用 Caller-Saved 和 Callee-Saved 做為表示。
以下為32個暫存器的 RISC-V ABI 名稱 (由RISC-V ABI所定義的暫存器名稱) 和他們在函式呼叫中是否需要保留暫存器的值。
Register | ABI Name | Description | Saver |
---|---|---|---|
x0 | Zero | 數值為0且無法更動,可用於設置暫存器的值 | |
x1 | ra | 存放 Return address | Caller |
x2 | sp | Stack pointer | Callee |
x3 | gp | Global pointer | |
x4 | tp | Thread Pointer | |
x5 | t0 | Temporaries/Working register | Caller |
x6~x7 | t1~t2 | Temporaries | Caller |
x8 | s0/fp | Callee-Saved/frame pointer | Callee |
x9 | s1 | Callee-Saved | Callee |
x10~x11 | a0~a1 | Function arguments/return values | Caller |
x12~x17 | a2~a7 | Function arguments | Caller |
x18~x27 | s2~s11 | Saved registers | Callee |
x28~x31 | t3~t6 | Temporaries | Caller |
read
,則必須切換到 kernel (entering kernel) 中,CPU 提供了一條稱為 ecall
的指令可以從 User Mode 切換到 Supervisor Mode,從 kernel 指定的入口 (entry) 進入到 kernel。CSRs 為 CPU 中儲存各種資訊的暫存器(CSRs 為 CSR 的複數,表示多個 CSR),在 RISC-V 中總共有4096個 CSR (以下只會介紹部分),以 m 開頭的 CSR 表示需要至少 Machine Mode 才可以使用,s 開頭表示至少需要 Supervisor Mode 才可以使用。
前面說到,RISC-V 依靠了許多擴充來擴充指令以及暫存器,而用於擴充 CSR 以及相關操作指令的擴充為 Zicsr。
Machine | Supervisor | Description |
---|---|---|
mhartid | Hart ID | |
mstatus | sstatus | Status Register |
mtvec | stvec | Trap Vector/Handler address (trap 發生時,需要跳轉到的記憶體地址) |
mepc | sepc | Previous (Exception) Program Counter (當 trap 發生時,先前的 Program Counter) |
scause | Trap cause code (Trap發生原因) | |
mscratch | sscratch | for trap handler |
satp | address translation pointer (指向 page table) | |
mie | sie | interrupts enable (是否允許中斷) |
sip | interrupts pending | |
medeleg | ||
mideleg |
上面這一些 CSR,也有對於 CSR 進行操作的指令,注意到所有對於 CSR 讀,寫,修改的指令都是屬於原子操作,像是 CSRRW
,CSRRS
,CSRRC
...。
Kernel Mode 和上面在 RISC-V 中提及的 Supervisor Mode 為同一個 Mode,由於在 xv6 中 Machine Mode 使用的不多,因此這裡的 Kernel Mode 代指 Supervisor Mode。
當 CPU 的 thread(Hart) 處於 User Mode 時,只能執行一些非權限 (unprivileged instructions) 的指令,諸如 ADD
, SUB
, JRC
等等,而處於 Kernel Mode 時,可以執行一些特殊權限指令 (privileged instructions),例如禁用中斷 (interrupts),設置 page table 相關的暫存器等等,在處理器上有各式各樣的狀態被暫存器所保留 (也就是 CSRs),我們只能通過一些特殊權限指令進行變更,以下舉例:
csrr a0, sstatus
: 將暫存器 sstatus
的值移動到暫存器 a0
中 (read CSRs)。csrw sstatus, a0
: 將暫存器 a0
的值移動到暫存器 sstatus
中 (write CSRs)。csrrw a0, mscratch, a0
: 將暫存器 mscratch
的值移動到暫存器 a0
中,並將暫存器 a0
的值移動到暫存器 mscratch
,此交換具備原子性,也就是一個操作內所有動作,要就是全部都發生,不然就是全部都不發生 (atomic swap)。前面我們知道為了讓每一個程式之間不相互汙染記憶體,我們使用了 Process 這個概念表示一個執行中的程式,並且每一個 Process 都有自己的 Memory space,而這其中就使用到了虛擬記憶體的特性,基本上 CPU 包含了 page table,而 page table 會將虛擬記憶體地址和實體的物理記憶體地址進行對應。
每一個 Process 有自己的 page table 與物理記憶體地址進行對應,也就是每一個 Process 只能存取 page table 對應到的記憶體地址,而作業系統會設置 page table 使得每一個 Process 之間的實體可存取的記憶體地址不會發生重合的情況,讓 Process 不能夠去存取其他 Process 的記憶體地址,虛擬記憶體的特性提供了記憶體上的強隔離性。
有了上面大略的 RISC-V 概念後,我們可以對前幾天一直使用到的 System call 進行一些分析。我們在 Day-02 看到在 xv6 在 /kernel/syscall.h
中定義了每一個 System call 對應到的編號,當應用程式需要某一項 System call 時,首先會將 System call 對應到的號碼存入暫存器 a7
中,接著會執行 ecall,並且傳入一個引數代表我們想要呼叫的 System call。ecall 會讓我們從 kernel 的進入點進入到 kernel,並執行對應的 System call,執行完畢後將控制權交還給 Process。
無論是 Shell 或是其他的應用程式,當他們執行 fork()
的時候,並不是直接呼叫作業系統對應到的函式,而是通過 ecall
,並將 fork()
在 syscall.h
中對應到的數字作為引數傳入,通過 ecall
跳躍到 kernel。
在 user 資料夾中有一個名為 usys.pl
的檔案,功用為生出對應的組合語言,我們可以通過閱讀 usys.pl
得知一些 System call 的行為
sub entry {
my $name = shift;
print ".global $name\n";
print "${name}:\n";
print " li a7, SYS_${name}\n";
print " ecall\n";
print " ret\n";
}
可以看到在第5行的地方,使用了 li
(load immediate, 此為pseudo instruction,也就是多條指令合併成一條指令,方便使用)指令,會將 SYS_${name}
的值放入 a7
暫存器中,也就是對應到將 System call 的編號放入 a7
暫存器中。
接著在第6行的地方,執行了 ecall,進入到 kernel 執行對應的 System call。
剛剛上面我們提到可以通過 ecall
來進入 Kernel 執行 System call,Kernel 會檢查 System call 的參數是否會觸發一些異常的行為,導致一些 BUG 被觸發。而 kernel space 有時候會被稱為 TCB (Trusted Computing Base)。
而在作業系統設計時我們可能會面臨到一個問題,什麼樣的程式要在 Kernel Mode 底下執行,什麼程式要在 User Mode 底下執行?
其中一個做法為讓整個作業系統都在 kernel mode 底下執行,例如在 xv6 中,所有作業系統的服務都是在 kernel mode 底下執行,而這種kernel 的設計模式稱為 Monolithic Kernel Design。
為了避免大量的程式碼運作在 kernel mode 底下造成的一些安全性風險,有另外一種設計模式為 Micro Kernel Design,希望在 kernel space 底下執行的程式碼量越少越好,因此將作業系統劃分成許多塊模組,只有其中一塊在 kernel space 底下執行,剩下在 user space 執行,這樣在 user space 上面的程式在執行時遇到錯誤不至於造成太大的危害。MINIX 3 就是使用 Micro Kernel 的設計。
而許多程式都在 user space 中執行,當我們需要一些 System call,與檔案系統互動時,例如 exec()
,我們需要一些方式。Shell 會通過 kernel 所提供了 IPC (Inter-Process Communication) 送出一則訊息給 kernel,kernel 查看此訊息發現是給檔案系統的,再將訊息交由檔案系統處理。檔案系統執行完 exec()
後也會通過 IPC 回傳訊息給 Shell。而這裡我們可以看到,我們從 User space 通過 IPC 到 Kernel space,接著從 Kernel 通過 IPC 再進入到 User space,跳轉過程是 Monolithic Kernel Design 將近兩倍,因此 Micro Kernel Design 會有一些性能上的損失。且因為相較於 Monolithic Kernel Design,無法有效的共享記憶體 (Page cache),較難獲得更好的性能。
xv6 為 Monolithic Kernel Design 的設計。
< Difference Between Microkernel and Monolithic Kernel >
計算機指令及架構
osdev.org RISC-V
xv6-riscv
Operating System Concepts, 9/e
RISC-V xv6 Book