# Linux 核心專題: 多核 RISC-V 模擬和 Linux 驗證
> 執行人: ranvd
> [專題解說影片](https://youtu.be/OzeSpxtGBU8?si=fbmkmKnx-dcbIph-)
### Reviewed by `nosba0957`
影片中提到 CLINT global timer 和 hart local timer 若不同步,會造成不可預期的錯誤,請問他們是如何達成同步的?
> 根據規格書上:
> $\S3.1.11$ The time CSR is a read-only shadow of the memory-mapped mtime register.
> 與
> $\S3.2.1$ Platforms provide a real-time counter, exposed as a memory-mapped machine-mode read-write register, mtime
>
> 可以知道 time(local timer) 暫存器是 mtime(global timer) 暫存器的備份,但並沒有詳細規範要如何同步,因此在硬體上取決於硬體設計,但在模擬器上由於 time 是 read-only 因此可以考慮使用指向 const 型態的的指標指向 global timer 來避免額外的運算操作。
### Reviewed by `otteryc`
進行 **n to 1** 的多核 RISC-V 系統模擬器的動機是什麼?可以解決那些問題?
## 任務簡介
在 [semu](https://github.com/sysprog21/semu) 的基礎之上,實作 hart state meangetment (HSM),使其達到多核 RISC-V 系統模擬,並使用 Linux v6.8+ 進行 SMP 驗證。
## TODO: 描述 semu 的多核處理器系統模擬的準備工作
> 介紹 ACLINT, HSM, 和 [Issue #22](https://github.com/sysprog21/semu/issues/22)
> 協助檢閱 [Issue #45](https://github.com/sysprog21/semu/pull/45)
> 解釋 device tree 的描述
> 列出相關的第一手材料,並予以解釋
### SBI HSM Extension
:::warning
注意用語:
- kernel 是作業系統「核心」
- core 是處理器「核」
:::
根據 Linux 核心文件 [RISC-V Kernel Boot Requirements and Constraints](https://www.kernel.org/doc/html/next/riscv/boot.html#kernel-entry) 可以看到有兩種進入 Linux 核心的方式,而在這次的實作中採用 Ordered booting,為了達成這種進入方式,我們必須實作 HSM 的機制去管理 RISC-V 中不同的 Hart,讓 semu 啟動時只有一個 Hart 在進入 Linux 核心,並且讓 Linux 核心可以透過 RISC-V 定義的 SBI HSM extension 去啟動不同的處理器核,使用 HSM 管理處理器核的好處不只可以更方便的管理不同的核的狀態,還可以做到 [CPU hotplug](https://docs.kernel.org/core-api/cpu_hotplug.html)。
> * `RISCV_BOOT_SPINWAIT`: the firmware releases all harts in the kernel, one hart wins a lottery and executes the early boot code while the other harts are parked waiting for the initialization to finish. This method is mostly used to support older firmwares without SBI HSM extension and M-mode RISC-V kernel.
> * `Ordered booting`: the firmware releases only one hart that will execute the initialization phase and then will start all other harts using the SBI HSM extension. The ordered booting method is the preferred booting method for booting the RISC-V kernel because it can support CPU hotplug and kexec.
以下就是 [RISC-V SBI HSM Extension](https://github.com/riscv-non-isa/riscv-sbi-doc/blob/master/src/ext-hsm.adoc) 管理核心的方式,從下圖片中可以看到 Hart 在 HSM 的管理下會有以下七種狀態。分別為:
* `STOPPED`: Hart 不在 Supervisor-mode 或更低優先權的模式下運行,可以是實際上的斷電或是沒在執行有用的指令。
* `STARTED`: Hart 正在運行
* `SUSPENDED`: Hart 處於低耗電狀態,等待中斷或特定事件發生,就會回到 `STARTED` 狀態。
* `STOP_PENDING`、`START_PENDING`、`SUSPEND_PENDING`、`RESUME_PENDING`:代表正在進入下一個狀態,但由於 semu 是模擬器,因此這幾個狀態可以直接忽略,但在實際硬體運作上,作業系統會透過 sbi_hart_get_status 來取得 hart 的狀態,並根據取得的狀態做後續的動作。
![Screenshot from 2024-06-04 16-22-20](https://hackmd.io/_uploads/H1600SnV0.png)
### SBI IPI Extension
上面 HSM 說明到,如果 Hart 在 SUSPEND 模式,則 Hart 會在收到中斷後切回 STARTED 模式。因此我們必須要有一個方式能夠讓 Hart 之間能夠把對方叫起來。因此 RISC-V SBI 就定義了 [IPI Extension](https://github.com/riscv-non-isa/riscv-sbi-doc/blob/master/src/ext-ipi.adoc) (Inter-processor Interrupt),用來叫醒其他的核。
根據 IPI Extension 的定義,當有核心透過 IPI 這個 SBI 嘗試叫醒其他 Hart 的話,就會對那個 Hart 送出 supervisor software interrupt。至於這個 supervisor software interrupt 要如何作到並沒有詳細定義,一般情況可以使用 [ACLINT](https://github.com/riscv/riscv-aclint) 或 CLINT 作到,在模擬器上也可以考慮不實作前述相關硬體,直接模擬軟體中斷即可。
> ($\S$ 7.1 in [riscv-sbi-doc](https://github.com/riscv-non-isa/riscv-sbi-doc/tree/master)) Send an inter-processor interrupt to all the harts defined in hart_mask. Interprocessor interrupts manifest at the receiving harts as the supervisor software interrupts.
### SBI TIMER Extension
Linux 核心會不斷設定 Timer 來設定下一次的 Timer 中斷來達到現代作業系統所需的一些功能,例如:排程。而 [RISC-V SBI TIMER Extension](https://github.com/riscv-non-isa/riscv-sbi-doc/blob/master/src/ext-time.adoc) 定義了上述所需的界面。Linux 核心會透過讀取 Hart 上的暫存器 time 後根據 time 的數值呼叫 `sbi_set_timer` 設定下一次的 Timer interrupt 。
根據 TIMER Extension 的定義,我們所傳入的參數是一個 Absolute time,也就當 Timer 的時間超過所傳入的參數時,就會觸發 Timer interrupt。而非呼叫 TIMER Extension 當下的 Timer 時間加上傳入的參數。
> ($\S$ 5.1 in [riscv-sbi-doc](https://github.com/riscv-non-isa/riscv-sbi-doc/tree/master)) Programs the clock for next event after stime_value time. stime_value is in absolute time. This function must clear the pending timer interrupt bit as well.
### SBI RFENCE Extension
要讓 Linux kernel 可以執行多核系統還需要實作 RFENCE Extension,不過因為在 semu 上目前不會遇到 RFENCE 所考量到的問題,因此在這次專題中並沒有實作,只有在 Linux 核心確認是否有 RFENCE Extension 時假裝我們有 RFENCE extension,讓 Linux 可以順利執行。因此暫時不進行解釋。
### ACLINT/CLINT
為了能夠實作 Timer interrupt 與 Inter-processor Interrupt,在 RISC-V 上常見的硬體主要是由 [ACLINT](https://github.com/riscv/riscv-aclint/blob/main/riscv-aclint.adoc) 或 [CLINT](https://sifive.cdn.prismic.io/sifive%2Fc89f6e5a-cf9e-44c3-a3db-04420702dcc1_sifive+e31+manual+v19.08.pdf) 負責。ACLINT 與 CLINT 主要的差別在於 ACLINT 可以同時支援 Supervisor software interrupt 與 Machine software interrupt。且 ACLINT 將不同功能的硬體模組化 (例如:Timer 與 IPI),這使得在實作上更有彈性,可針對平台的需求增加或減少不同的模組。在這次的實作中為了簡潔,因此優先考慮較為架構較為簡單的 CLINT 進行實作。
[CLINT](https://sifive.cdn.prismic.io/sifive%2Fc89f6e5a-cf9e-44c3-a3db-04420702dcc1_sifive+e31+manual+v19.08.pdf) 為 SiFive 所提供的硬體規格,詳細的 Memory map 可以參考 [Core Local Interrupt (CLINT)](https://chromitem-soc.readthedocs.io/en/latest/clint.html)。裡面主要包含三種不同的暫存器 `mtime`, `mtimecmp`, `msip`。其中 `mtime` 內的數值在一般情況下會單調遞增,為系統提供一個統一的時間。`mtimecmp` 則做為 Timer interrupt 的依據,當 `mtimecmp` 內的數值小於等於 `mtime` 時,CLINT 就會向 Hart 發出 Timer interrupt,觸發中斷的方式即是將 mip 中 MTIP 位元設為 1。
最後是 `msip`,其暫存器主要負責 Software interrupt,當 `msip` 內的數值不為 0 時,CLINT 就會對 Hart 發出 Software interrupt。觸發中斷的方式即是將 mip 中 MSIP 位元設為 1。
![image](https://hackmd.io/_uploads/B1NCRZjrR.png)
不過,如果有注意到上面提到的 SBI IPI Extension 的話可以看到,SBI 對於 IPI 的描述是
> Interprocessor interrupts manifest at the receiving harts as the supervisor software interrupts.
也就是說 IPI 要發出的是 Supervisor software interrupt,但 CLINT 發出的 Software interrupt 卻是 Machine software interrupt,這似乎與 SBI 的要求並不相符。這時就要提到 RISC-V 中有趣的機制『中斷代理』。
> ($\S$ 3.1.8 in [RISC-V Volume II](https://drive.google.com/file/d/17GeetSnT5wW3xNuAHI95-SI1gPGd5sJ_/view)) By default, all traps at any privilege level are handled in machine mode, though a machine-mode handler can redirect traps back to the appropriate level with the MRET instruction. To increase performance, implementations can provide individual read/write bits within medeleg and mideleg to indicate that certain exceptions and interrupts should be processed directly by a lower privilege level.
也就是說所有的 Trap 都會進入 machine mode,並依據 Machine Trap Delegation Registers (`medeleg` and `mideleg`) 的設定將特定的 trap 送至特權等級較低的 Supervisor-mode 處理。有了這類機制,即使 CLINT 發出的是 Machine-level 的中斷,也可以直接轉交至 Supervisor-level 處理。
不過雖然 interrupt 是可以透過 medeleg 與 mideleg 將中斷交由 Supervisor-level 處理,但在一般情況下 Linux Kernel 會執行在 Supervisor-level,且對於 Timer 的設定會透過上面提到的 SBI Timer Extension 處理,因此 RISC-V Linux Kernel 並不會直接去存取 CLINT 裝置。
### PLIC
當周邊裝置需要系統資源或是通知特定事件已經發生時會透過外部中斷的方式通知 CPU。在 RISC-V 中主要負責統整所有周邊裝置的硬體為 [PLIC](https://github.com/riscv/riscv-plic-spec/blob/master/riscv-plic.adoc)。在一般情況下,周邊裝置會與 PLIC 連接,當周邊裝置需要發出中斷時,會把中斷訊號送給 PLIC,PLIC 則會根據優先度將外部中斷送給不同的 Hart。而 Hart 是否會收到中斷取決於 Hart 對 PLIC 的設定。
### Device tree
〈[Device Tree for Dummies](https://events.static.linuxfound.org/sites/events/files/slides/petazzoni-device-tree-dummies.pdf)〉提到:
> The Device Tree is really a hardware description language. It should describe the hardware layout, and how it works. But it should not describe which particular hardware configuration you’re interested in.
也就是說,Device Tree 是用來描述硬體的特性、位址、大小、等其他硬體資訊。但並沒有辦法透過 Device Tree 去控制硬體要如何運作。更具體來說,假如我們的系統有一個 PLIC 與 Harts 相互連接,但並沒有在 Device Tree 上被描述,這時作業系統並不會知道有 PLIC 這個硬體,但即是作業系統不知道,也不影響 PLIC 送中斷給 Hart。
Device Tree 的會以階層的方式表示,如下圖所示,`/` 底下有 `cpus`、`memory`、`uart` 等各種硬體,且在 `cpus` 底下有 `cpu@0` 與 `cpu@1`。
```graphviz
digraph G{
rankdir=LR;
graph [splines=curved]
node0[shape=box style=rounded label="/"];
node11[shape=box style=rounded label="memory@0"];
node12[shape=box style=rounded label="soc@F0000000"];
node23[shape=box style=rounded label="interrupt-controller@0"];
node24[shape=box style=rounded label="uart@0x4000000"];
node15[shape=box style=rounded label="cpus"];
node21[shape=box style=rounded label="cpus@0"];
node22[shape=box style=rounded label="cpus@1"];
node0 -> node15;
node0 -> node11;
node0 -> node12
node12 -> node23;
node12 -> node24;
node15 -> node21;
node15 -> node22;
}
```
Device tree 還有另一個重點是中斷訊號的描述,可以透過 `interrupts-extended`、`interrupt-parent` 屬性來表示此裝置產生的中斷會送至哪裡,又或是使用 interrupt-controller 屬性來表示此裝置是中斷控制器。透過這些描述,我們不只可以建立出 Device tree,同時也可以建立一個 Interrupt tree,如下圖:
```graphviz
digraph G{
rankdir=LR;
edge[dir="back"]
graph [splines=curved]
node23[shape=box style=rounded label="interrupt-controller@0"];
node24[shape=box style=rounded label="uart@0x4000000"];
node21[shape=box style=rounded label="cpus@0"];
node22[shape=box style=rounded label="cpus@1"];
node22 -> node23
node21 -> node23
node23 -> node24
}
```
但 Device Tree 只定義基本的語法規則,詳細的硬體特性會根據使用場景的不同有所不同,例如在 Linux 核心就會規定 Device Tree 在描述硬體特性時應該如何設定,這種行為被稱作 bindings。
### A extension
![image](https://hackmd.io/_uploads/rJx5BwquBA.png)
Atomic 指令在多核中也扮演著重要的角色,為了防止在多核系統中,其中一顆核再對記憶體操作時被其他核影響,RISC-V 提供了 A extension,其中主要分為兩種 LR/SC 與 AMO (Atomic Memory Operation) 兩種。
首先介紹 AMO,AMO 包含 `AMOSWAP`、`AMOADD`、`AMOAND`、等其他指令,其目的在於"一次完整的"完成 RMW (read-modify-write) 步驟,以`AMOADD` 為例,`AMOADD` 將 rs1 所指向的記憶體位址內的資料放到 rd 後與 rs2 內的數值做相加並寫入 rs1 所指向的記憶體位址。
接著是 LR/SC 這兩道指令,這兩道指令通常會一起使用,LR (load-reserved) 會讀取 rs1 指向的記憶體,並將數值放進 rd 內。並將這個數值與位址紀錄至 reservation set。而 SC (Store-conditional) 會將 rs2 的數值寫入 rs1 所指向的記憶體位址,前題是 rs1 所指向的記憶位址還在 reservation set 內。下列描述是關於 SC 指令執行成功的條件。
> An SC may succeed only if no store from another hart to the reservation set can be observed to have occurred between the LR and the SC, and if there is no other SC between the LR and itself in program order. An SC may succeed only if no write from a device other than a hart to the bytes accessed by the LR instruction can be observed to have occurred between the LR and SC.
也就是說,SC 指令要成功執行就必須確保 rs1 所指向的記憶體位置還在 reservation set 內,而在 reservation set 內的數值只要被其他處理器核或裝置 Store 過就會從 reservation set 內移除。
## TODO: 修改 semu 以達到多核處理器的模擬
> 提交 pull request 並參與討論
pull request: [Preliminary SMP support #46 ](https://github.com/sysprog21/semu/pull/46)
由於 semu 原本是單核架構,原本的 `struct __vm_internel` 裡面就包含了所有模擬 hart 需要使用到的變數 (例如:暫存器、CSR、等),為了成功模擬 SMP 架構,首先要將原本的 `__vm_internel` 改成如下結構體。`hart_number` 用來記錄模擬器的核數,`*hart[]` 用來記錄不同的核所需的結構體。而其中的 `hart_t` 就是原本的 `__vm_internel`,裡面包含了暫存器、CSR、周圍設備位址和其他模擬所需資訊。
```c
struct __vm_internel {
uint32_t hart_number;
hart_t *hart[];
}
```
## TODO: 確認 SMP Linux 得以在 RISC-V 運作
> 應建立對應的自動測試機制
> 確保基本的周邊硬體在 SMP 環境運作正常
在上面提交的 [PR#46](https://github.com/sysprog21/semu/pull/46) 內可以透過 `make check SMP=8` 模擬八核系統,其中 `SMP` 後面接的數字即是模擬的核數。
CPU 資訊
```
# cat /proc/cpuinfo
processor : 0
hart : 0
isa : rv32ima_zicntr_zicsr_zifencei_zihpm
mmu : sv32
mvendorid : 0x12345678
marchid : 0x80000001
mimpid : 0x1
hart isa : rv32ima_zicntr_zicsr_zifencei_zihpm
processor : 1
hart : 1
isa : rv32ima_zicntr_zicsr_zifencei_zihpm
mmu : sv32
mvendorid : 0x12345678
marchid : 0x80000001
mimpid : 0x1
hart isa : rv32ima_zicntr_zicsr_zifencei_zihpm
processor : 2
hart : 2
isa : rv32ima_zicntr_zicsr_zifencei_zihpm
mmu : sv32
mvendorid : 0x12345678
marchid : 0x80000001
mimpid : 0x1
hart isa : rv32ima_zicntr_zicsr_zifencei_zihpm
processor : 3
hart : 3
isa : rv32ima_zicntr_zicsr_zifencei_zihpm
mmu : sv32
mvendorid : 0x12345678
marchid : 0x80000001
mimpid : 0x1
hart isa : rv32ima_zicntr_zicsr_zifencei_zihpm
processor : 4
hart : 4
isa : rv32ima_zicntr_zicsr_zifencei_zihpm
mmu : sv32
mvendorid : 0x12345678
marchid : 0x80000001
mimpid : 0x1
hart isa : rv32ima_zicntr_zicsr_zifencei_zihpm
processor : 5
hart : 5
isa : rv32ima_zicntr_zicsr_zifencei_zihpm
mmu : sv32
mvendorid : 0x12345678
marchid : 0x80000001
mimpid : 0x1
hart isa : rv32ima_zicntr_zicsr_zifencei_zihpm
processor : 6
hart : 6
isa : rv32ima_zicntr_zicsr_zifencei_zihpm
mmu : sv32
mvendorid : 0x12345678
marchid : 0x80000001
mimpid : 0x1
hart isa : rv32ima_zicntr_zicsr_zifencei_zihpm
processor : 7
hart : 7
isa : rv32ima_zicntr_zicsr_zifencei_zihpm
mmu : sv32
mvendorid : 0x12345678
marchid : 0x80000001
mimpid : 0x1
hart isa : rv32ima_zicntr_zicsr_zifencei_zihpm
```
中斷資訊
```
# cat /proc/interrupts
CPU0 CPU1 CPU2 CPU3 CPU4 CPU5 CPU6 CPU7
10: 2987 2968 2967 2966 2965 2964 2964 2962 RISC-V INTC 5 Edge riscv-timer
12: 87 0 0 0 0 0 0 0 SiFive PLIC 1 Edge ttyS0
13: 1 0 0 0 0 0 0 0 SiFive PLIC 3 Edge virtio1
14: 0 0 0 0 0 0 0 0 SiFive PLIC 2 Edge virtio0
IPI0: 6 7 8 8 12 12 14 10 Rescheduling interrupts
IPI1: 147 23 18 47 33 15 49 109 Function call interrupts
IPI2: 0 0 0 0 0 0 0 0 CPU stop interrupts
IPI3: 0 0 0 0 0 0 0 0 CPU stop (for crash dump) interrupts
IPI4: 0 0 0 0 0 0 0 0 IRQ work interrupts
IPI5: 0 0 0 0 0 0 0 0 Timer broadcast interrupts
```
## TODO: 整合除錯器
> 加分!參考 [rv32emu](https://github.com/sysprog21/rv32emu),整合 remote GDB