# Tenok即時作業系統
###### tags: `articles`
**作者: [鄭聖文](http://wiki.csie.ncku.edu.tw/User/shengwen)**
**E-mail:** `shengwen1997.tw[at]gmail.com`
**本文章以 `創用CC 姓名標示─非商業性─相同方式分享` 授權**
![](https://i.imgur.com/v5kNGVI.png)
如您願意參與協作或指證錯誤請聯繫我
**COSCUP 2023 演說: [Tenok: 打造用於機器人控制的微型即時作業系統](https://drive.google.com/file/d/1p8YJVPVwFAEknMXPbXzjj0y0p5qcqT2T/view?fbclid=IwAR2Xr0a5hKDGxEpMMvM0tQfdcMziAqR1vR7N-7Le-tUmbpUlYJcxVxQ0mcY)**
## I. 概要
`Tenok` 一詞在阿美族語中表示核心 (Kernel),其啟發自成大資工系師生於2013年所創作的 [rtenv](https://github.com/embedded2014/rtenv) / [rtenv+](https://github.com/embedded2014/rtenv-plus),後者被用於[嵌入式系統系列課程](http://wiki.csie.ncku.edu.tw/embedded/schedule)中作為教材。原先 Tenok 只是用於學習及重現 rtenv,但目前已強化了部分 rtenv 設計,可視為另一個作業系統開發的參考案例。
Tenok 亦參考了 [mini-arm-os](https://github.com/jserv/mini-arm-os) 以及 FreeRTOS,相較於 rtenv 有更完整的資源管理功能,且檔案系統基於 inode 邏輯,更加貼近於真實作業系統的設計。另外也包含了一個支援自動補齊、歷史檢索以及數個快捷鍵功能的 Shell 文字互動界面。
以下為 Tenok 的主要特徵以及功能:
* POSIX 風格的 API
* 同步機制: Spinlock, Mutex, Semaphore
* 行程間通訊: FIFO (Named pipe), Message Queue
* 支援浮點數運算單元 (Floating-point Unit, FPU)
* 內建 Shell 的文字互動介面
* 簡單的 rootfs 及 romfs 的檔案系統實做
Tenok 的原始碼以及展示影片可參見以下連結:
* [展示影片](https://www.youtube.com/watch?v=byWihWcMP00)
* [GitHub連結](https://github.com/shengwen-tw/tenok)
## II. 開發環境準備
Tenok 目前支援了 ARM Cortex-M4F 的硬體架構,建議使用的硬體開發板如下:
* [STM32F4DISCOVERY](https://www.st.com/en/evaluation-tools/stm32f4discovery.html)
* [32F429IDISCOVERY](https://www.st.com/en/evaluation-tools/32f429idiscovery.html)
除了實體硬體,您也可以使用 QEMU 進行實驗。
---
**參考實驗環境:** Ubuntu 20.04 (GNU/Linux 5.8.0)
1. 安裝所需套件:
```
sudo apt install build-essential git zlib1g-dev libsdl1.2-dev automake* autoconf* \
libtool libpixman-1-dev lib32gcc1 lib32ncurses5 libc6:i386 libncurses5:i386 \
libstdc++6:i386 libusb-1.0.0-dev ninja-build
```
2. 安裝 OpenOCD:
```
git clone git://git.code.sf.net/p/openocd/code openocd
cd openocd
./bootstrap
./configure --prefix=/usr/local --enable-jlink --enable-amtjtagaccel --enable-buspirate \
--enable-stlink --disable-libftdi
echo -e "all:\ninstall:" > doc/Makefile
make -j4
sudo make install
```
3. 安裝 GCC toolchain 9 (arm-none-eabi):
```
wget https://developer.arm.com/-/media/Files/downloads/gnu-rm/9-2019q4/gcc-arm-none-eabi-9-2019-q4-major-x86_64-linux.tar.bz2
tar jxf ./gcc-arm-none-eabi-9-2019-q4-major-x86_64-linux.tar.bz2
rm gcc-arm-none-eabi-9-2019-q4-major-x86_64-linux.tar.bz2
```
4. 安裝 QEMU:
```
git clone git://git.qemu.org/qemu.git
cd qemu
git submodule init
git submodule update --recursive
mkdir build
cd build
../configure
make -j $(nproc)
```
5. 編輯 `~/.bashrc` 並增加以下指令:
```
PATH=$PATH:${YOUR_PATH}/qemu/build
PATH=$PATH:${YOUR_PATH}/gcc-arm-none-eabi-9-2019-q4-major/bin
```
註: `${YOUR_PATH}` 須換成操作者的安裝路徑
6. 重啟終端機即可完成安裝
## III. 實驗步驟
### 3.1 下載原始碼
```
git clone https://github.com/shengwen-tw/tenok.git
cd tenok
```
### 3.2 使用 QEMU 模擬器
1. 打開 `Makefile` 並啟用以下設定:
```
include platform/qemu.mk
```
2. 編譯:
```
make
```
3. 啟動 QEMU 進行模擬:
```
make qemu
```
4. 若要使用 gdb 進行除錯,則再打開新的終端機並輸入:
```
make gdbauto
```
### 3.3 使用實體硬體
1. 將開發板的 PC10 (TX) 連接至 USB-TTL 模組的 RX, 開發板的 PC11 (RX) 連接至 USB-TTL 模組的 TX:
![](https://i.imgur.com/APDZqUX.jpg)
[圖] 32F429IDISCOVERY開發板
2. 打開 `Makefile` 並啟用以下其中一種設定:
```
include platform/stm32f4disc.mk #STM32F4DISCOVERY board
```
```
include platform/stm32f429disc.mk #32D4IDISCOVERY board
```
3. 編譯:
```
make
```
4. 將開發板透過 USB 連接至電腦並輸入以下指令進行燒錄:
```
make flash
```
5. 若要使用 gdb 進行除錯,則在終端機1輸入:
```
make openocd
```
6. 並在終端機2輸入:
```
make gdbauto
```
## IV. ARM Cortex-M4F 硬體機制
本章節將對 ARM Cortex-M4F 官方資料進行導讀。了解硬體機制對於設計作業系統至關重要。官方手冊的 PDF 檔可見**參考資料**一章。
---
幾個術語的釐清:
![](https://i.imgur.com/90D1hZm.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 1-4
| 名稱 | 說明 |
| -------- | -------- |
| Nested Vectored Interrupt Controller (NVIC) | ARM Cortex-M 處理器中的[中斷](https://zh.wikipedia.org/zh-tw/%E4%B8%AD%E6%96%B7) (Interrupt) 控制器,支援分組 (Grouping) 以及優先權分級 (Priority),具有低延遲響應特性 |
| System Control Block (SCB)| 用於提供 CPU 資訊、CPU 控制、進行組態設定以及回報系統異常事件 (Exception) 的模組 |
| System timer | 系統時鐘,又稱 SysTick, 是一個24位元的下數定時器。用於實作即時作業系統 (RTOS) 中的時間 Tick 功能 |
| Floating-point Unit (FPU) | 浮點數運算單元 |
---
### 4.1 核心暫存器 (Core registers)
首先來了解 ARM Cortex-M4(F) 的核心暫存器的功能,請見下圖:
![](https://i.imgur.com/dK98zfy.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-3
以下進行摘錄說明:
| 暫存器 | 用途 |
| -------- | -------- |
| R0-R12 | 32位元的通用暫存器 |
| R13 (SP) | 即 Stack Pointer,包含 PSP (Process Stack Pointer) 及 MSP (Main Stack Pointer)。當前 SP 是 MSP 或是 PSP 須依情況而定 |
| R14 (LR)| 即 Link Register,用於保存函式呼返回 (Function return) 位址或是異常返回 (Exception return) 的設定值 |
| R15 (PC)| 即 Program Counter,其指向目前執行中的指令的位址|
| PRIMASK、FAULTMASK 及 BASEPRI | 用於控制中斷的啟動/關閉 |
| PSR | 即 Program Status Register,其包含了 APSR,IPSR 以及 EPSR 三個部分,詳細定義請見下圖 |
| CONTROL | 控制 Stack、CPU 特權/非特權模式行為以及顯示 FPU 狀態的暫存器 |
接著來看 PSR (Program Status Register) 的定義:
![](https://i.imgur.com/QLdHyRb.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-4
由上圖可知,PSR 由 APSR、IPSR 以及 EPSR 所構成。以下列出 PSR 的幾個較重要位元之功能:
| PSR位元 | 位置 | 功能 |
| -------- | -------- | -------- |
| N | [31] | Negative flag |
| Z | [30] | Zero flag |
| C | [29] | Carry or borrow flag |
| V | [28] | Overflow flag |
| Q | [27] | DSP overflow and saturation flag |
| ISR_NUMBER | [8:0] | 中斷服務常式 (Interrupt Service Routine, ISR) 之編號 |
其中 ISR_NUMBER (即 EPSR[8:0]) 可用於判斷目前是處於哪個中斷函式之中,若其值為0表示目前沒有中斷發生 (即 Thread mode)。ISR_NUMBER 的詳細定義由下圖給出:
![](https://i.imgur.com/y71VYf4.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-6
可以發現 ISR_NUMBER 的0至15已由 ARM 所制定,而16以後至最大值240則由廠商 (Vendor) 規劃用於諸如 USART、I2C、SPI 等周邊 ([Peripheral](https://zh.wikipedia.org/wiki/%E5%A4%96%E9%83%A8%E8%AE%BE%E5%A4%87))。
---
### 4.2 異常事件 (Exception)
上述的中斷 (Interrupt) 實際上只是 ARM Cortex-M 處理器中所定義的 Exception (異常事件) 中的一部分。異常事件的完整的定義則由下圖給出:
![](https://i.imgur.com/AGuPu7I.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-22
由圖可知 Interrupt request (IRQ) 的編號被安排在零及正整數之後,而編號為負數的異常事件具有特殊地位。值得特別注意的是:
- **SVCall (Supervisor Call):** 一般用來實做系統呼叫 (Syscall), 具有即時響應 (Synchronous) 之特性
- **PendSV (Pendable Service):** 一般用來實做 Context switch,非即時響應 (Asynchronous),會待其他異常事件都結束之後才處理
- 除了 Reset, NMI, HardFault 的優先權固定為-3, -2, -1之外 (數值越小優先權越高),其餘異常事件優先權皆可以由開發者設定
- Vector address 是處理異常事件所定義的函式進入點位址, 即當異常事件被觸發後會跳轉到該函式
---
### 4.3 NVIC 優先權分組 (Priority grouping)
在 ARM Cortex-M4 的手冊中,Interrupt priority grouping 的描述如下:
![](https://i.imgur.com/L0DR34k.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-25
NVIC 的優先權分組提供了更細緻的優先權分組,分為分組優先權 (Group priority) 和子優先權 (Subpriority),基本規則為分組優先權先比較,相同時再比較子優先權。
而由 STM32F4 手冊中可以找到支援的優先權分組選項:
![](https://i.imgur.com/J7uAFTk.png)
[圖] 來源: STM32 Cortex®-M4 MCUs and MPUs programming manual 229/262
實際上 STM32 的 Driver library 採用 Group 0-4 的名稱來進行區分,將表格重新整理如下:
| 優先權分組 | 分組優先權範圍 | 子優先權範圍 | PRI_N[7:4]分配 |
| -------- | -------- | -------- | - |
| Group 0 | None | 0-15 | [7:4]用於子優先權 |
| Group 1 | 0-1| 0-7 | [7]用於分組優先權、[6:4]用於子優先權 |
| Group 2 | 0-3| 0-3 | [7:6]用於分組優先權、[5:4]用於子優先權 |
| Group 3 | 0-7| 0-1| [7:5]用於分組優先權、[4]用於子優先權 |
| Group 4 | 0-15| None | [7:4]用於分組優先權 |
為了簡化設計,Tenok 採用了 Group 4。
---
### 4.4 Stack Pointer: MSP 和 PSP
一般的電腦程式包含了靜態記憶體及動態記憶體管理,分別使用 Stack 以及 Heap 的記憶體區間。前者由編譯器規劃並會隨著呼叫函式、使用變數的行為而擴大範圍,後者則由使用者透過 `malloc()`、`free()` 等行為的顯式呼叫而產生變化。
![](https://i.imgur.com/wdJz0Vt.png)
[圖] 常見的 Stack 與 Heap 的記憶體分配模式 (ARM 的 Stack 採高位址遞減模式)
ARM Cortex-M 有兩個 Stack Pointer,分別為
* **MSP (Main Stack Pointer):** CPU 在 Reset 後即預設使用 MSP,一般會在 OS 啟動後提供給 Kernel space 程式使用
* **PSP (Process Stack Pointer):** 提供給 User space 的的程式使用,一般 OS 會根據Task數量分配多個區塊的記憶體,而透過改變 PSP 的指向搭配 Context switch 則可以實現多工
MSP 或是 PSP 的使用可以透過修改 CONTROL 暫存器來指定,詳細內容將於後續介紹。
---
### 4.5 Exception entry
Exception entry 意指當 CPU 進入 Exception 後所會自動發生的行為,官方手冊定義了 Exception entry 的發生時機:
![](https://i.imgur.com/u8lxMfq.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-26
意即,當有待發生中的 (Pending) 異常事件時,以下幾種情況會產生 Exception entry:
* 處理器在 Thread mode 狀態
* 新的異常事件的權限比現在處理中的異常事件還要高,那麼將發生搶占,且會形成巢狀關係
下圖給出了一個異常事件的巢狀發生範例:
![](https://i.imgur.com/npZG8fy.png)
[圖] 來源:A Beginner’s Guide on Interrupt Latency and Interrupt Latency of the Arm Cortex-M processors
當 Exception entry 發生時:
* CPU 會記錄發生中斷的當下正在執行的指令位址 (如此一來中斷結束後才能回到原來的地方),而 LR 會被寫入特殊的 EXC_RETURN 數值
* CPU 會自動進行 Stack push 將部分暫存器資料保存至 SP 指向的記憶體位址。同時根據程式是否使用到 FPU 進行浮點數運算, Stack push 行為將產生差異。詳細的內容請見下圖:
![](https://i.imgur.com/uTuEBaa.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-27
上圖之左側和右側分別展示了使用和沒有使用 FPU 的 Stack push 行為。其中,S0 至 S15 為浮點數資料暫存器、FPSCR 為 Floating-point Status and Control Register。另外可以注意到 SP 的位址在 Stack push 後會遞減,這是因為 ARM 的 Stack 設計屬於高位址遞減。
至於為甚麼要將 Stack push 分成兩種情況的原因是是效能考量。因為並非所有的程式都需要計算浮點數,如果省略沒有用到的情況將有助於提升效能,而 ARM 稱此機制為 Lazy Stacking。
---
### 4.6 Exception return
Exception return 發生在完成處理 Exception 之後,與 Exception entry 成對發生。以下行為都可以產生 Exception return:
![](https://i.imgur.com/mlrm6Ht.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-28
即: Exception return 發生在 Handler mode,當以下任何一種組合語言指令將 EXC_RETURN 值載入 PC 暫存器後便會發生:
* 使用 `LDM` 或是 `POP` 指令將資料載入到PC暫存器中
* 使用 `LDR` 指令且儲存的對象是 PC 暫存器
* 呼叫 `BX` 指令和任意暫存器產生 Branching
上面所提到的 EXC_RETURN 值是 CPU 在 Exception entry 發生時自動寫入 LR 暫存器的內容,由下圖給出:
![](https://i.imgur.com/oegyJtT.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-28
翻譯成中文意即:
| EXC_RETURN[31:0] | FPU使用 | 功能 |
| ---------- | -- | ------------------- |
| 0xFFFFFFF1 | 無 | 返回 Handler mode 且切換至 MSP |
| 0xFFFFFFF9 | 無 | 返回 Thread mode 且切換至 MSP |
| 0xFFFFFFFD | 無 | 返回 Thread mode 且切換至 PSP |
| 0xFFFFFFE1 | 有 | 返回 Handler mode 且切換至 MSP |
| 0xFFFFFFE9 | 有 | 返回 Thread mode 且切換至 MSP |
| 0xFFFFFFED | 有 | 返回 Thread mode 且切換至 PSP |
值得注意的是,在異常事件處理函式中可以透過檢測 LR[4] 來判斷 Task 是否使用到 FPU,此細節將在 Context switch 實作中提到。
關於 Handler mode 以及 Thread mode的介紹會在後續給出。
---
### 4.7 Tail chaining 機制
Tail chaining 指的是當一個 ISR 完成以後,如果還有其他等待中的 ISR,處理器會直接切換至此 ISR 並省略 Stack push 和 Stack pop 的動作。此機制避免了不必要的記憶體存取和節省了處理器的能耗,且只需要6個 Clock cycles (ARM Cortex-M3 和 Cortex-M4 的情況)。 下圖為 Tail chaining 的一個範例:
![](https://i.imgur.com/fNhg41P.png)
[圖] 來源: A Beginner’s Guide on Interrupt Latency and Interrupt Latency of the Arm Cortex-M processors
---
### 4.8 Late arrival 機制
Late arrival 指的是如果一個高優先權的異常事件比低優先權的還要晚出現,處理器仍會優先服務高優先權異常事件的機制 (如下圖,IRQ1 比 IRQ2 晚觸發仍可以先被服務)。此機制確保高優先權異常可以更快的被處理,且避免了複雜的巢狀結構發生。除此之外,Late arrival 也可以幫助減少能耗。
![](https://i.imgur.com/g4F52qb.png)
[圖] 來源: A Beginner’s Guide on Interrupt Latency and Interrupt Latency of the Arm Cortex-M processors
---
### 4.9 特權等級 (Privilege levels) 以及處理器模式 (Processor modes)
ARM Cortex 處理器使用了特權等級 (Privilege levels) 來規範程式執行時可被允許的操作,同時特權等級與處理器模式 (Processor modes) 息息相關。請見下圖:
![](https://i.imgur.com/bPazTwk.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-2
**特權等級 (Privilege levels)**
特權等級包含了特權模式 (Privileged mode) 以及非特權模式 (Unprivileged mode),前者可以存取所有硬體資源,而後者則受限。此設計用於保護作業系統核心,避免 User task 碰觸敏感資源。
* **特權模式 (Privileged mode):** 可操作所有硬體資源
* **非特權模式 (Unprivileged mode):**
- `MSR` 和 `MRS` 指令的功能受限,且不允許使用 `CPS` 指令
- 不允許操作 System timer, NVIC, 或是 System Control Block
- 某些記憶體區塊或是硬體周邊可能被禁止存取
**處理器模式 (Processor modes)**
![](https://i.imgur.com/NDCTjm7.png)
[圖] 來源: Cortex-M3 Devices Generic User Guide 2-2
上圖給出了處理器模式 (Processor modes) 的兩種狀態: 當沒有異常事件產生時,程式碼執行於 Thread mode,反之則執行於 Handler mode。
根據ARM的設計,Handler mode 固定為 Privileged mode (即異常處理函式的必執行於特權模式下),而 Thread mode 則根據 CONTROL 暫存器設定可以是 Privileged mode 或是 Unprivileged mode。
CPU 在 Reset 後會預先運作在 Privileged mode 以及 Thread mode 下。一種可行的設計是在作業系統啟動後再切換 User space 至 Unprivileged mode 來切割 User space 和 Kernel space。在此設計模式下,Privileged mode 使用 MSP 而 Unprivileged mode 則為 PSP。
然而上述的設計並非唯一可行的作法,像是 FreeRTOS 便不使用 PSP。FreeRTOS 的 Scheduler 是屬於 run-to-completion 設計,而不論是 Task 或是 Scheduler 皆使用 Privileged mode 以及 MSP。另外,由於許多 Baremetal 的應用不需要太複雜的資源管理,因此使用 Privileged mode 以及 MSP 即可足夠。
綜合以上描述,ARM Cortex-M 的模式切換可以表示為下圖:
![](https://i.imgur.com/klSoHcT.png)
[圖] 特權等級以及處理器模式之間的關係
---
### 4.10 特權等級以及 Stack Pointer 切換
特權等級以及 Stack Pointer 的切換 (MSP 或 PSP) 可由修改 CONTROL 暫存器達成。CONTROL 暫存器的定義由下圖給出:
![](https://i.imgur.com/ScZQlI7.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-9
其中:
* **FPCA[2]:** 用於顯示目前的程式的 Context 是否有使用到浮點數運算
* **SPSEL[1]:** 用於設定 Stack Pointer 是使用 MSP 或是 PSP
* **nPRIV[0]:** 用於設定特權等級
特權等級以及 Stack Pointer 通常在 Context switch 中切換。由於 CONTROL 暫存器屬於特殊暫存器,其操作必須使用 `MSR` 或是 `MRS` 指令進行:
* `MSR`: 將即時值 (Immediate value) 或是通用暫存器的內容寫入特殊暫存器中
* `MRS`: 將特殊暫存器的內容寫入通用暫存器中
需要注意的是,修改 CONTROL 暫存器需要特權模式的等級。下圖給出了 CPU 切換至非特權模式的組合語言程式範例:
![](https://i.imgur.com/PY2327b.png)
[圖] 來源: 不詳,擷自成大資工Wiki
另外手冊中明確指出了當使用 `MSR` 指令更改了 Stack Pointer 後,必須呼叫 `ISB` 指令來重置 CPU 的 Pipeline 以確保 CPU 正確執行新的 Stack 記憶體中的指令:
![](https://i.imgur.com/FkLD0mN.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-10
### 4.11 中斷的開啟以及關閉
ARM Cortex-M 提供了四道專門用於開啟/關閉中斷的指令:
![](https://i.imgur.com/Wl8CT8M.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 3-159
| 指令 | 行為 | 說明 |
| --------- | -------------------- | -------- |
| `CPSID i` | Set PRIMASK暫存器 | 關閉 Interrupts 以及可設定的 Fault handlers |
| `CPSID f` | Set FAULTMASK暫存器 | 關閉 Interrupts 以及所有 Fault handlers |
| `CPSIE i` | Clear PRIMASK暫存器 | 開啟 Interrupts 以及可設定的 Fault handlers |
| `CPSIE f` | Clear FAULTMASK暫存器 | 開啟 Interrupts 以及所有 Fault handlers |
下圖給出了 PRIMASK 暫存器的定義:
![](https://i.imgur.com/GEPMyAB.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-8
以及 Fault Mask 暫存器的定義:
![](https://i.imgur.com/BwJg0QK.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-8
除了硬開啟/關閉中斷外,ARM Cortex-M 亦提供了 Base Priority Mask 暫存器。透過寫入此暫存器的[7:0]位,可以設定大於等於此數值的中斷都會被屏蔽 (注意: 數值越大代表優先權越小)。Base Priority Mask 暫存器的定義由下圖給出:
![](https://i.imgur.com/pEDQcnU.png)
[圖] 來源: Cortex-M4 Devices Generic User Guide 2-9
使用 Base Priority Mask 暫存器的好處是不必關閉不影響作業系統運作的特定中斷函式。 在 Tenok 原始碼中對應的實作如下:
```
//tenok/kernel/kernel.c
//允許所有中斷
void reset_basepri(void)
{
asm volatile ("mov r0, #0\n"
"msr basepri, r0\n");
}
//關閉大於KERNEL_INT_PRI值的中斷
void set_basepri(void)
{
asm volatile ("mov r0, %0\n"
"msr basepri, r0\n"
:: "i"(KERNEL_INT_PRI << 4)); //由於Tenok採用NVIC Group 4,
//即高4位的PRI_N[7:4]有效,
//使用時須左移4位
}
```
語法部分可以參見已故 **Liao Wen Satoshi** 朋友所撰寫的文章: [關於GNU Inline Assembly](http://wen00072.github.io/blog/2015/12/10/about-inline-asm/)。
## V. Context switch 實作
### 5.1 ARM Architecture Procedure Call Standard (AAPCS)
在說明 Context switch 的實做前,先介紹 AAPCS (ARM Architecture Procedure Call Standard) 以利於撰寫組合語言程式。
節錄幾個會用到的重點 [(AAPCS的官方說明)](https://developer.arm.com/documentation/den0013/d/Application-Binary-Interfaces/Procedure-Call-Standard):
* R0-R3 可以用來傳入前四個函式的輸入參數,又稱為 Caller-saved 暫存器,須由呼叫者保存
* R4-R11 一般用於區域變數使用,又稱 Callee-saved 暫存器,須由函式本身保存
* R0, R1 可以用來保存回傳值
* 當參數為64位元時可以合併兩個暫存器使用(如 R0+R1, R2+R3)
* R0-R3 不夠用時須用 Stack 協助傳遞
### 5.2 Context switch 的基本流程
ARM Cortex-M4F 的 Context switch 須考慮有無使用 FPU 的兩種情況。
**Task 沒有使用到 FPU 的情況:**
![](https://i.imgur.com/Cbrqu2r.png)
[圖] 來源: Cortex-M4(F) Lazy Stacking and Context Switching 15
上圖的內容為: 當前一個 Task 沒有使用到 FPU 資源,且異常事件被觸發 (如 SysTick 發生), CONTROL 暫存器的 FPCA 位元將變為低電位,而 EXC_RETURN[4] 將變為高電位。再進入 Exception entry 時,CPU 會自動地將 R0-R3, R12, PC 和PSR推入 Stack。而此張圖並沒有說明的部分為,開發者再設計作業系統時,無論是保存舊的 Context 或是載入新的 Context,都需要自行 Pop / Push 核心暫存器的 R4-R11 部分。
此情況的 Context switch 基本流程為:
1. 舊的 Task 正在執行
2. Exception 被觸發,Context switch 開始
3. CPU 在 Exception entry 自動地將 R0-R3, R12, LR, PC 放入舊的 Stack Pointer 的位址中
4. OS 手動將 R4-R11 放入舊的 Stack pointer 的位址中
5. OS 手動從新的Stack pointer指向的內容中取出 R4-R11
6. CPU 在 Exception return 時自動地從新的 Stack Pointer 的位址中取出 R0-R3, R12, LR, PC
7. CPU 切換到新的 Task 執行
**Task 有使用到 FPU 的情況:**
![](https://i.imgur.com/7kLZU1C.png)
[圖] 來源: Cortex-M4(F) Lazy Stacking and Context Switching 17
如上圖所示,若前一個 Task 使用了 FPU 資源,CONTROL 暫存器的 FPCA 位元將成為高電位。此時若觸發異常事件 (如用 SysTick 發生),FPCA 將被反轉並拉低 EXC_RETURN[4] 的電位。當 Exception entry 被觸發後,R0-R3, R12, PC, PSR 將會被自動推入 Stack,同時也會為 FPU 的 S0-S15 和 FPSCR 在 Stack 內保留空間。此情況需要 OS 手動呼叫 `VSTM` 指令將舊的 FPU 暫存器的 S16-S31 部分保存至 Stack 內,並在要載入新的 Task 時用 `VLDM` 指令取出新的 S16-S31。而通用暫存器的部分則與先前所描述的相同。
此情況的 Context switch 基本流程為:
1. 舊的 Task 正在執行
2. Exception 被觸發,Context switch 開始
3. CPU 在 Exception entry 時自動地將 R0-R3, R12, LR, PC 放入舊的 Stack Pointer 的位址內,且會為 S0-S15 和 FPSCR 自動保存空間
4. OS 手動將 S16-S31 放入舊的 Stack Pointer 的位址內
5. OS 手動將 R4-R11 放入舊的 Stack Pointer 的位址內
6. OS 手動從新的 Stack Pointer 指向的位址中取出 R4-R11
7. Exception return 時自動地從新的 Stack pointer 指向的 Stack 中先取出 S0-S15, FPSCR,後取出 R0-R3, R12, LR, PC
8. CPU切換到新的 Task 執行
最後,以下先進行預告,Tenok 的 Context switch 的關係圖為如下:
![](https://i.imgur.com/wmwySL4.png)
[圖] Tenok的Context switch原理
### 5.3 第一個 Task 的初始化
第一個 Task 肩負著啟動 OS 的責任,我們首先介紹 `task_create()` 函式來了解其過程:
```
//tenok/kernel/kernel.c
...
#define THREAD_PSP 0xFFFFFFFD
#define INITIAL_XPSR 0x01000000
...
void task_create(task_func_t task_func, uint8_t priority)
{
...
tasks[task_cnt].pid = task_cnt; //根據目前有的Task數量分配PID
tasks[task_cnt].status = TASK_WAIT; //設定此Task為等待狀態
tasks[task_cnt].priority = priority; //設定優先權
tasks[task_cnt].stack_size = TASK_STACK_SIZE; //設定Stack大小
/* Stack初始化,使Context switch可以工作的關鍵程式碼 */
uint32_t *stack_top = tasks[task_cnt].stack + tasks[task_cnt].stack_size - 18;
stack_top[17] = INITIAL_XPSR;
stack_top[16] = (uint32_t)task_func; // PC = Task程式進入點
...
stack_top[8] = THREAD_PSP; //_LR = 0xfffffffd
tasks[task_cnt].stack_top = (struct task_stack *)stack_top;
...
}
```
上述的程式碼的行為可以繪製為下圖:
![](https://i.imgur.com/fmvIyNz.png)
[圖] 使用 `task_create()` 後 **User stack** 的變化
`task_create()` 函數的機制可以分成三部分解說:
**Exception return 的行為設定 (藍色):**
* 設定了 PC 暫存器 (要執行的 User task 的函式位址) 以及 PSR 暫存器 (將 CPU 設定為 Thumb mode)
* 關於 Thumb mode 請參閱: [嵌入式系統建構:開發運作於STM32的韌體程式](http://wiki.csie.ncku.edu.tw/embedded/Lab19/stm32-prog.pdf)
(結論而言, ARM 指令集包含 ARM 模式和 Thumb-2 模式,ARM Cortex-M4 只支援後者)
* 模擬了 Exception entry 的行爲,將 R0-R3, R12, LR, PC 以及 PSR 推入了 User stack 中。待此 User task 被載入後會被 Exception return 自動取出。
**保留傳遞 Syscall 編號使用的 Stack 空間 (綠色):**
* Syscall 編號決定了 Kernel 要處理哪一個 Syscall。稍後可以發現第一個 Task 啟動時會傳遞0為數值但實際會被Kernel忽略,待後續 User task 真正呼叫 Syscall (傳遞大於0的Syscall編號) 時才會有效。
* 命名 _R7 是因為剛好使用 R7 做傳遞 ([ABI](https://zh.wikipedia.org/zh-tw/%E5%BA%94%E7%94%A8%E4%BA%8C%E8%BF%9B%E5%88%B6%E6%8E%A5%E5%8F%A3)),不過 R7 使用前後有做 Stacking / Unstacking,因此不會影響 User context
**處理需要開發者保存 Task 的 Context (黃色):**
* 設定了 LR 暫存器 (_LR, 用於將 User space 設定為 PSP 和 Unprivileged mode)
* 處理 R4-R11 需要開發者手動保存/載入的部分
除了上面所述,這裡有兩點需要特別指出:
* R0-R12 的值都是設定為0,因為程式尚未開始執行
* 載入新的 Task 不需要考慮 FPU,因為 Task 執行前從未使用過 FPU 資源
當第一個 Task 的 Stack 設定好後,便可呼叫 `os_env_init()` 函數來切割 Kernel space 和 User space:
```
//tenok/kernel/context_switch.s
//此函數在sched_start()中被呼叫,此時CPU為Thread mode和Privilege mode並使用MSP
.type os_env_init, %function
.global os_env_init
os_env_init:
//函式輸入:
//第一個參數(由R0傳入): Stack top的位址
/* 先保存此時的Kernel context */
mrs ip, psr //將PSR複製到IP內
push {r4, r5, r6, r7, r8, r9, r10, r11, ip, lr} //Stacking: 保存R4-R11, IP和_LR到MSP
/* 切換至PSP (註: CONTROL暫存器的寫入需要特權模式) */
msr psp, r0 //複製R0到PSP
mov r0, #3 //R0設定成3 (0b0000'0011), 即Unprivilege mode和使用PSP
msr control, r0 //複製R0到CONTROL使設定生效
isb //重置Pipeline
//觸發SVC異常事件 (或是當作呼叫編號為0的Syscall),
//由於先前對Task stack的初始化,雖然os_env_init()是在Kernel中被呼叫的,
//但是會對CPU產生一個錯覺是Syscall是在Task中呼叫的
push {r7} //Stacking: 儲存舊的R7內容
mov r7, #0 //R7寫入0的數值
svc 0 //觸發SVC異常事件(此時程式會跳躍到SVC異常事件服務函式中)
/* 如果下面的程式開始執行代表SVC異常事件已經完成了 */
nop
pop {r7} //Unstacking: 還原舊的R7內容
/* Kernel space正式完成切割,從此之後進入Kernel都要先關閉中斷 */
cpsid i
cpsid f
bx lr //函式返回
```
從上述的程式碼我們知道 `os_env_init()` 執行過程中會被中斷一次並跳躍到 SVC 的函式內,SVC 的異常事件服務函式實作為:
```
//tenok/kernel/context_switch.s
.type SVC_Handler, %function
.global SVC_Handler
SVC_Handler:
/* 注意: 進入異常事件後CPU會自動切換至Handler mode和使用MSP */
/* 關閉所有中斷,只要涉及Context switch的程式都不應該被打斷 */
cpsid i
cpsid f
/* 取得User stack的Stack top位址 */
mrs r0, psp //複製PSP到R0
/* FPU的Context switch */
tst r14, #0x10 //檢測_LR[4]是否為1 (是否有使用到FPU?)
it eq //如果成立則下面一行指令會被略過
vstmdbeq r0!, {s16-s31} //Stacking: 保存User stack的FPU暫存器S16-S31
stmdb r0!, {r7} //Stacking: 保存User task的R7 (Syscall編號)內容
stmdb r0!, {r4, r5, r6, r7, r8, r9, r10, r11, lr} //Stacking: 保存User task的R4-R11和_LR
/* 載入Kernel context */
pop {r4, r5, r6, r7, r8, r9, r10, r11, ip, lr} //Unstacking: 從MSP取回R14-R11, IP和_LR
msr psr_nzcvq, ip //將IP保存的內容寫回PSR
/* 異常返回 */
bx lr //回到Kernel space
```
**讀者可能會有如下的疑惑:** 為什麼在切換至非特權模式後要再呼叫 Syscall 0 才回到 Kernel space 中呢? 這是為了依靠 SVC 異常進入 Handler mode 來取得特權。如果不這樣做,即使 `os_env_init()` 結束後直接返回 Kernel space 也無法回復至特權模式。
下圖為上述內容的視覺化描述:
![](https://i.imgur.com/5k79BQ8.png)
[圖] `os_env_init()` 函式的流程
### 5.4 從 Kernel space 切換至 User Space
完成初始化後,接著就可以考慮如何切換至 User task。假設已有一個可被執行的 Task 存在,透過 `jump_to_user_space()` 可以達成切換。
`jump_to_user_space()` 的函數原型 (Function prototype) 的定義如下:
```
//tenok/kernel/kernel.h
...
uint32_t *jump_to_user_space(uint32_t stack);
...
```
而 `jump_to_user_space()` 的函數實作為:
```
//tenok/kernel/context_switch.s
.type jump_to_user_space, %function
.global jump_to_user_space
jump_to_user_space:
//輸入參數1 (R0): User task的Stack top位址
//回傳值 (R0): 載入User task的新的Stack top位址
//由於呼叫此函數時是Thread mode和Privilege mode,
//CPU在Exception entry時會自動將Kernel的LR設定為 0xFFFFFFF9 或 0xFFFFFFE9,
//因此當Task返回至Kernel時便會正確地還原這些設定
/* 保存Kernel狀態 */
mrs ip, psr //將PSR複製到IP中
push {r4, r5, r6, r7, r8, r9, r10, r11, ip, lr} //Stacking: 保存Kernel stack的
//R4-R11,IP和_LR
/* 載入User context */
ldmia r0!, {r4, r5, r6, r7, r8, r9, r10, r11, lr} //Unstacking: 從R0指向的Stack top
//位址取回R14-R11和_LR
ldmia r0!, {r7} //Unstacking: 載入Syscall編號
/* FPU的Context switch */
tst r14, #0x10 //檢測_LR[4]是否為1 (是否有使用到FPU?)
it eq //如果成立則下面一行指令會被略過
vldmiaeq r0!, {s16-s31} //Stacking: 保存User stack的FPU暫存器S16-S31
msr psp, r0 //將R0寫入到PSP (將要執行的Task stack寫入PSP)
/* 解除中斷關閉 */
cpsie i
cpsie f
//跳躍到User task,由於先前Stack初始化時異常返回的_LR值已被設為
//THREAD_PSP (0xFFFFFFFD),因此跳躍後會自動切換為PSP
bx lr
```
### 5.5 從 User Space 切換至 Kernel Space
有兩個原因會導致 User task 重新回到 Kernel space 中:
* SysTick 的中斷被觸發 (時間到,必須重新進行排程)
* User task 呼叫 Syscall (觸發 SVC 異常事件回到 Kernel 處理 Syscall)
**由 SysTick 返回 Kernel 的情況:**
SysTick 的中斷觸發頻率為1000次/每秒,這在 `tenok/kconfig.h` 中可以見到:
```
#define OS_TICK_FREQ 1000 /* Hz */
```
而 SysTick 的異常事件處理函式的實作如下:
```
//tenok/kernel/context_switch.s
.type SysTick_Handler, %function
.global SysTick_Handler
SysTick_Handler:
/* 注意: 進入異常事件後CPU會自動切換至Handler mode和使用MSP */
/* 關閉所有中斷,只要涉及Context switch的程式都不應該被打斷 */
cpsid i
cpsid f
neg r7, r7 //將_R7變成負數,如此一來返回Kernel space時就可以和Syscall的
//request做出區別
/* 取得User stack的Stack top位址 */
mrs r0, psp //複製PSP到R0
/* FPU的Context switch */
tst r14, #0x10 //檢測_LR[4]是否為1 (是否有使用到FPU?)
it eq //如果成立則下面一行指令會被略過
vstmdbeq r0!, {s16-s31} //Stacking: 保存User stack的FPU暫存器S16-S31
stmdb r0!, {r7} //Stacking: 保存User task的R7 (Syscall編號)內容
stmdb r0!, {r4, r5, r6, r7, r8, r9, r10, r11, lr} //Stacking: 保存User task的R4-R11和_LR
/* 載入Kernel context */
pop {r4, r5, r6, r7, r8, r9, r10, r11, ip, lr} //Unstacking: 從MSP取回R14-R11, IP和_LR
msr psr_nzcvq, ip //將IP保存的內容寫回PSR
/* 異常返回 */
bx lr //回到Kernel space
```
**由 SVC 返回 Kernel 的情況:**
由於 SVC 的異常函式已在上文給出,此處就進行省略。取而代之說明 Syscall 是怎麼產生的:
我們可以以 `sleep()` 這個 Syscall 進行說明,其函式原型為:
```
//tenok/kernel/syscall.h
...
uint32_t sleep(uint32_t ticks);
...
```
而 `sleep()` 的實作如下:
```
//tenok/kernel/syscall.s
...
//定義一個GNU AS的巨集(Macro)方便使用
.macro syscall syscall_num
push {r7} //Stacking: 儲存舊的R7內容
mov r7, \syscall_num //_R7寫入Syscall編號
svc 0 //觸發SVC異常事件
nop
pop {r7} //Unstacking: 還原舊的R7內容
bx lr //函數返回
.endm
...
.type sleep, %function
.global sleep
sleep:
syscall #5 //呼叫前面定義的巨集並帶入數值為5的Syscall編號
...
```
當 CPU 回到 Kernel space 後,OS 可以透過 `syscall_handler()` 函式比對以下的 Syscall table 找到對應的 Syscall 處理函數:
```
//tenok/kernel/kernel.c
/* syscall table */
syscall_info_t syscall_table[] = {
...
DEF_SYSCALL(sleep, 5),
...
};
```
上述的的程式碼中包含了巨集函式 `DEF_SYSCALL()` 將 `sleep()` 的請求連結到服務函數 `sys_sleep()` 並標註其編號為5。此巨集的實作如下:
```
#define DEF_SYSCALL(func, _num) \
{.syscall_handler = sys_ ## func, .num = _num}
|___ 巨集黏合運算子 (Token-pasting operator)
```
而且 `syscall_handler()` 函式的實作為
```
//tenok/kernel/kernel.c
...
void syscall_handler(void)
{
/* 比較Syscall編號後呼叫對應的處理函數 */
int i;
for(i = 0; i < syscall_table_size; i++) {
if(running_task->stack_top->r7 == syscall_table[i].num) {
/* 執行Syscall */
syscall_table[i].syscall_handler();
break;
}
}
}
```
最後給出 `sys_sleep()` 的程式碼:
```
//tenok/kernel/kernel.c
void sys_sleep(void)
{
/* 設定睡眠時間 */
running_task->remained_ticks = *running_task->reg.r0;
/* 將執行中的Task改為等待狀態並放到Sleep list中 */
running_task->status = TASK_WAIT;
list_push(&sleep_list, &(running_task->list));
/* 設定甦醒後Syscall的回傳值為0 */
*(uint32_t *)running_task->reg.r0 = 0;
}
```
## VI. Linux 風格的 Linked list 實作
Tenok 實作了一個 Linux 風格的 Linked list 資料結構。相較於常見做法將要串連的結構體類型 (Structure data type) 作為指標紀錄下一個元素,Linux 將 Linked list 獨立成了一個結構體:
```
tenok/kernel/list.h
...
struct list {
struct list *prev;
struct list *next;
};
...
```
由定義可以知道此 Linked list 的設計為雙向連結,且實際上還會形成環型結構。使用時,須把 `struct list` 嵌入要使用的資料結構中,如:
```
struct object {
uint8_t data1;
...
struct list list;
};
```
同時,使用者需要再建立一列表頭變數 (List head) 用於追蹤 Linked list 中的所有元素。舉例來說,下圖為一個型態為 `struct object` 的 Linked list:
![](https://i.imgur.com/6i24VIi.png)
[圖] Linked list範例
我們接著對所有用於操作 Linked list 的函式進行說明:
```
//tenok/kernel/list.c
...
void list_init(struct list *list)
{
//功能: 初始化,Linked list使用前須進行的動作
//輸入: 列表頭(List head)
//如果傳入的列表頭為空則將其首尾連至自己
if(list != NULL) {
list->prev = list;
list->next = list;
}
}
int list_is_empty(struct list *list)
{
//功能: 查看Linked list是否為空
//輸入: 列表頭(List head)
/* 列表頭首尾連自自己代表Linked list為空 */
return list->next == list;
}
void list_remove(struct list *list)
{
//功能: 給定一個元素並將其從Linked list中移除
//輸入: 要從Linked list中移除的元素
/* 確認Linked list為非空 */
if(list != NULL) {
/* 取出中間元素並將前後元素相連 */
list->next->prev = list->prev;
list->prev->next = list->next;
}
}
void list_push(struct list *list, struct list *new)
{
//功能: 將一個新的元素推入Linked list的末端
//輸入: 列表頭 (list)、 新元素 (new)
if(list != NULL && new != NULL) {
/* 將新元素連至原末元素後端再與列表頭相連 */
new->prev = list->prev;
new->next = list;
list->prev->next = new;
list->prev = new;
}
}
struct list *list_pop(struct list *list)
{
//功能: 從Linked list的前端取出一個元素
//輸入: 回傳取出元素的位址
//第一個元素位於列表頭之後
struct list *first = list->next;
//若第一個元素等於列表頭則表示Linked list為空
if(list == first) {
return NULL;
}
//取出第一個元素並將後一個元素與列表頭相連
list->next = first->next;
list->next->prev = list;
return first;
}
```
接著會產生的問題是要如何透過 `struct list` 獲取結構體 `struct object` 本身呢? Tenok 仿照了 Linux 的設計並提供了以下的程式實作:
```
#define container_of(ptr, type, member) \
((type *)((void *)ptr - offsetof(type, member)))
#define list_entry(ptr, type, member) \
container_of(ptr, type, member)
```
其中 `offsetof()` 是一個用於計算結構成員位移 (offset of a structure member) 的內建巨集 (Macro)。接著我們以下面的程式碼作為範例說明 `container_of()` / `list_entry()` 巨集的使用:
```
/* 建立一個object的Linked list並初始化 */
struct list obj_list; //列表頭
list_init(&obj_list);
/* Push一個新元素 */
struct object new_obj = {.data1 = 10};
list_push(&obj_list, new_obj);
/* 取出第一個元素的list */
struct list *first = obj_list->next;
/* 取出第一個元素的data1數值 */
struct object *obj = container_of(first, struct object, list);
uint8_t val = obj->data1;
```
另外一個常見的應用場景是尋訪 (Traversal) 所有 Linked list 元素。這可以使用:
```
#define list_for_each(curr, list) \
for ((curr) = (list)->next; (curr) != (list); (curr) = (curr)->next)
```
並再以一個範例進行使用說明:
```
/* 建立一個object的Linked list並初始化 */
struct list obj_list; //列表頭
list_init(&obj_list);
/* Push一個新元素 */
struct object new_obj1 = {.data1 = 10};
list_push(&obj_list, new_obj1);
/* 再Push一個新元素 */
struct object new_obj2 = {.data1 = 20};
list_push(&obj_list, new_obj2);
/* 尋訪所有元素 */
struct list *curr;
list_for_each(curr, &obj_list) {
/* 取出元素N的data1數值 */
struct object *obj = container_of(curr, struct object, list);
uint8_t val = obj->data1;
}
```
由此可以知C語言是一個強大的程式語言,允許使用者透過巨集 (Macro) 自行擴充語言。
**延伸閱讀:**
* [offsetof(3) - Linux manual page](https://man7.org/linux/man-pages/man3/offsetof.3.html)
* [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#circular-linked-list)
## VII. 任務排程 (Task scheduling)
在 Tenok 中,Task 有三種狀態:
```
enum {
TASK_WAIT, //等待中
TASK_READY, //可被執行
TASK_RUNNING //執行中
} TASK_STATUS;
```
每一個 Task 都有一個對應的 Task Control Block 用於保存資訊:
```
//tenok/kernel/kernel.h
...
struct task_ctrl_blk {
struct task_stack *stack_top; //Task目前的Stack頂端
uint32_t stack[TASK_STACK_SIZE]; //Task stack
uint32_t stack_size; //Stack的大小, 目前的設計只允許固定值
uint8_t status; //Task的狀態
uint32_t pid; //Task的PID編號
int priority; //Task的優先權
char name[TASK_NAME_LEN_MAX]; //Task的名稱
uint32_t sleep_ticks; //睡眠計數器,下數到0時Task會被喚醒
bool syscall_pending; //用來表示Task的Syscall尚未完成
struct {
size_t size; //Task請求了某個檔案操作但是目前無法被滿足,並將
//大小保存於此
} file_request;
struct list list; //用來串連Task list
};
...
```
Tenok 採用了 Priority-based Round-Robin Algorithm 進行排程,有 TASK_MAX_PRIORITY + 1 個 Ready list 及一個 Sleep list 如下圖所示:
![](https://i.imgur.com/iXoBq3K.png)
[圖] Ready lists 與 Sleep list
Priority-based Round-Robin Algorithm 的基本原則為:
* 優先從具有較高優先權的 Ready list 中選取任務
* 當 Ready list 的優先權與執行中 Task 的相等,則須等待執行中 Task 所分配的 Time quantum (時間量) 被耗盡後才可切換到下一個任務
對應的程式碼為:
```
/* task lists */
//tenok/kernel/kernel.c
...
struct list ready_list[TASK_MAX_PRIORITY + 1];
struct list sleep_list;
/* tasks */
struct task_ctrl_blk tasks[TASK_CNT_MAX];
struct task_ctrl_blk *running_task = NULL; //用來儲存當前執行的Task
...
```
結合先前在 Linked list 中所介紹的內容,每一個 Task list 的結構如下:
![](https://i.imgur.com/TREaIu2.png)
[圖] Task list 的結構
以下給出排程演算法的實作:
```
//tenok/kernel/kernel.c
void schedule(void)
{
/*
* 由於特定OS API會透過Syscall關閉IRQ, 此情況下OS不應該進行排程直到呼叫的
* Task將IRQ重新啟動
*/
if(irq_off == true)
return;
/* 檢查是否有可以被喚醒的Task */
struct list *list_itr = sleep_list.next;
while(list_itr != &sleep_list) {
/* 由於Task可能從Sleep list移除,先紀錄下一個對象 */
struct list *next = list_itr->next;
/* 取得Task Control Block */
struct task_ctrl_blk *task = list_entry(list_itr, struct task_ctrl_blk, list);
/* 睡眠所需等待的Tick已經耗盡,將Task根據優先權放入Ready list中 */
if(task->sleep_ticks == 0) {
task->status = TASK_READY;
list_remove(list_itr); //remove the task from the sleep list
list_push(&ready_list[task->priority], list_itr);
}
/* 準備下一個要檢查的對象 */
list_itr = next;
}
/* 由優先權大的往小的開始找到一個包含可以執行的Task的Ready list */
int pri;
for(pri = TASK_MAX_PRIORITY; pri >= 0; pri--) {
if(list_is_empty(&ready_list[pri]) == false)
break;
}
/* Task在Time quantum (即SysTick的週期) 被耗盡前便返回了Kernel (即Syscall) */
if(running_task->status == TASK_RUNNING) {
/* 確認是否有更高優先權的任務在Ready list中等待 */
if(pri > running_task->priority) {
/* 有,進入睡眠狀態 */
prepare_to_wait(&sleep_list, &running_task->list, TASK_WAIT);
} else {
/* 沒有,直接返回不切換到別的Task */
return;
}
}
/* 從Ready list中Pop出一個Task來執行 */
struct list *next = list_pop(&ready_list[pri]);
running_task = list_entry(next, struct task_ctrl_blk, list);
running_task->status = TASK_RUNNING;
}
```
同時,如果 User task 是透過 SysTick 中斷回到 Kernel 的話會觸發 `tasks_tick_update()` 函數:
```
//tenok/kernel/kernel.c
static void tasks_tick_update(void)
{
/* 現在執行中的Task的Time quantum已經耗盡,將其設定為睡眠狀態並待新的Task被選出 */
if(running_task->status == TASK_RUNNING) {
prepare_to_wait(&sleep_list, &running_task->list, TASK_WAIT);
}
/* 更新所有睡眠中的Task還須等待的Tick量 */
struct list *curr;
list_for_each(curr, &sleep_list) {
/* 取得第N個睡眠中的Task的Task Control Block */
struct task_ctrl_blk *task = list_entry(curr, struct task_ctrl_blk, list);
/* 計數器遞減直到為0 */
if(task->sleep_ticks > 0) {
task->sleep_ticks--;
}
}
}
```
## VIII. 同步機制 (Synchronization)
Tenok 實作了 Spinlock, Mutex 以及 Semaphore, 以下簡述了三種機制的差異:
**Spinlock:**
* 基於 Busy waiting 的一種鎖
* 需要使用到原子操作 (Automic operation) 如: [Test-and-set](https://en.wikipedia.org/wiki/Test-and-set) 或 [Compare-and-swap
](https://en.wikipedia.org/wiki/Compare-and-swap) 實作
* 沒有 Context switch的開銷 (Overhead),可用於需要快速反應的短程式
**Mutex:**
* 需要使用到 Spinlock 實作
* 是 Process 層次的物件,當無法獲取資源時進行睡眠
* 有持有者 (Owner) 概念,只有上鎖者可以進行解鎖
* 產生 Critical Section 的目的是保護共享資源
* 如遇到優先權反轉 (Priority inversion) 問題,則需要如 Priority Inheritance Protocol (PIP) 等機制
**Semaphore:**
* 需要使用到 Spinlock 實作
* 是 Process 層次的物件,當無法獲取資源時進行睡眠
* 沒有限制一定要由上鎖者解鎖
* 產生 Critical Section 的目的為:
1. 使用計數器處理生產者-消費者問題 (在一個 Thread 中,`signal()` 與 `wait()` 不會同時發生)
2. 執行緒同步: 如一執行緒A通知執行緒B繼續執行
**參考閱讀:**
* [Linux 核心設計: 淺談同步機制](https://hackmd.io/@sysprog/linux-sync?type=view) (在 <*既生瑜,何生亮?*> 中說明了 Mutex 與 Semaphore 存在差異)
* [Priority Inversion on Mars](https://www.slideshare.net/jserv/priority-inversion-30367388?fbclid=IwAR0GdoqVaKk41XrMoyWtO-lHasoJ4fZVhDw7X50o1GEFdgRoJAq-aNvolss) (火星拓荒者號的優先權反轉案例)
---
### 8.1 Spinlock 實作
ARM Cortex-M 提供了 Load-Exclusive 和 Store-Exclusive 的機制用於實作如 Spinlock 的 Synchronization primitives (同步原語),如下圖所示:
![](https://i.imgur.com/UXiyG01.png)
[圖] 來源: Cortex-M3 Devices Generic User Guide 2-19
**Load-Exclusive 指令:**
* 讀取某個記憶體位址的內容,同時請求標記此位址為獨佔 (前者一定會成功,後者不一定)
**Store-Exclusive 指令:**
* 嘗試寫入某個記憶體位址,回傳的位元顯示了寫入的成功與否 (0: 成功; 1: 失敗,沒有取得記憶體的獨佔權)
ARM Cortex-M 提供了三組指令,分別為:
* `LDREX` 和 `STREX` 用於 Word (32 bits) 大小的資料
* `LDREXH` 和 `STREXH` 用於 Halfword (16 bits) 大小的資料
* `LDREXB` 和 `STREXB` 用於 Byte (8 bits) 大小的資料
了解了基本原理以後,以下給出 Spinlock 的實作:
**函式原型:**
```
//tenok/kernel/spinlock.h
...
#define spin_lock_irq(lock) \
do { \
preempt_disable(); \
spin_lock(lock); \
} while(0)
#define spin_unlock_irq(lock) \
do { \
spin_unlock(lock); \
preempt_enable(); \
} while(0)
typedef uint32_t spinlock_t;
void spin_lock(spinlock_t *lock);
void spin_unlock(spinlock_t *lock);
...
```
**實作:**
```
.syntax unified
/* Spinlock上鎖 */
.type spin_lock, %function
.global spin_lock
spin_lock:
//函式參數:
//第一個參數(由R0傳入): 鎖變數的記憶體位址
loop: ldrex r2, [r0] //將鎖變數的位址載入R2並請求獨佔此記憶體位址
cmp r2, #1 //如果鎖變數 == 1 (變數已鎖上)
beq loop //返回loop標籤重試
mov r1, #1 //將數值1複製到R1
strex r2, r1, [r0] //嘗試將1 (R1) 寫入鎖變數 ([R0]),是否寫入成功會記錄在R2內
cmp r2, #1 //如果R2 == 1 (寫入失敗)
beq loop //返回loop標籤重試
bx lr //函式返回
/* Spinlock解鎖 (比上鎖簡單,直接將鎖變數歸零即可) */
.type spin_unlock, %function
.global spin_unlock
//函式參數:
//第一個參數(由R0傳入): 鎖變數的記憶體位址
spin_unlock:
mov r1, #0 //將數值0複製到R1
str r1, [r0] //將0 (R1) 寫入鎖變數 ([R0])
bx lr //函式返回
```
---
### 8.2 Mutex 實作
有了 Spinlock 之後便可實作 Mutex,以下給出 Mutex 的實作 (注意,目前的的實作做不包含任何 Protocol 設計):
```
int pthread_mutex_init(_pthread_mutex_t *mutex, const pthread_mutex_attr_t *attr)
{
mutex->owner = NULL; //初始化擁有者為空,這個指標會指向Task Control Block的位址
mutex->lock = 0; //初始化Mutex的Spinlock
list_init(&mutex->wait_list); //初始化當Mutex需要睡眠時使用的wait_list
return 0;
}
int pthread_mutex_unlock(_pthread_mutex_t *mutex)
{
/* 進入Critical Section */
spin_lock_irq(&mutex->lock);
int retval = 0;
/* 檢查呼叫函數的Task是否為持有者 */
if(mutex->owner == running_task) {
/* 是,清空持有者 (解鎖) */
mutex->owner = NULL;
/* 檢查是否有其他Task因為嘗試存取該Mutex而正在睡眠 */
if(!list_is_empty(&mutex->wait_list)) {
/* 透過wake_up()喚醒一個Task */
wake_up(&mutex->wait_list);
}
} else {
/* 否,返回異常值 */
retval = EPERM;
}
/* 離開Critical Section */
spin_unlock_irq(&mutex->lock);
/* 回傳0 */
return retval;
}
int pthread_mutex_lock(_pthread_mutex_t *mutex)
{
/* 重複嘗試直到取得Mutex */
while(1) {
/* 進入Critical Section */
spin_lock_irq(&mutex->lock);
/* 檢查該Mutex是否早已被其他Task占用 (上鎖) */
if(mutex->owner != NULL) {
/* 是,呼叫prepare_to_wait()把自己放進wait_list中睡眠等待 */
prepare_to_wait(&mutex->wait_list, &running_task->list, TASK_WAIT);
/* 離開Critical Section */
spin_unlock_irq(&mutex->lock);
/* 讓Task進入睡眠 */
sched_yield();
} else {
/* 否,Mutex沒有被占用,可以上鎖 */
/* 將Mutex的持有者設定為自己 */
mutex->owner = running_task;
/* 離開Critical Section */
spin_unlock_irq(&mutex->lock);
/* 離開迴圈 */
break;
}
}
/* 回傳0 */
return 0;
}
```
---
### 8.3 Semaphore 實作
同樣的,Sempaphore 的實作也需要 Spinlock。以下為一 Semaphore 的虛擬碼 (Pseudo-code) 範例:
![](https://i.imgur.com/HgUmEH7.png)
[圖] 來源: Synchronization: Spinlocks - Semaphores (CS 4410 Operating Systems, Cornell University)
以下給出 Semaphore 的實作:
```
int sem_init(sem_t *sem, int pshared, unsigned int value)
{
sem->count = value; //初始化Semaphore的數量
sem->lock = 0; //初始化Semaphore的Spinlock
list_init(&sem->wait_list); //初始化Semaphore無法取得時,Task的等待清單
return 0;
}
/* sem_post()可以在User task或是Interrupt中被呼叫 */
int sem_post(sem_t *sem)
{
/* 進入Critical Section */
spin_lock_irq(&sem->lock);
int retval;
/* 預防整數溢位 */
if(sem->count >= (INT32_MAX - 1)) {
retval = -EAGAIN; //稍後用來回傳提示使用者重新嘗試
} else {
/* 增加Semaphore的數量 */
sem->count++;
/* 如有Task在等待且Semaphore數量> 0則喚醒一個Task */
if(sem->count > 0 && !list_is_empty(&sem->wait_list)) {
wake_up(&sem->wait_list);
}
retval = 0; //稍後回傳0表示成功
}
/* 離開Critical Section */
spin_unlock_irq(&sem->lock);
return 0; //回傳,程式結束
}
//sem_trywait() 可以被User task或是Kernel program呼叫。
//因為不會產生阻塞所可以用於如: chardev driver的file operation中使用
int sem_trywait(sem_t *sem)
{
/* 進入Critical Section */
spin_lock_irq(&sem->lock);
int retval;
/* 檢查Semaphore的數量 */
if(sem->count <= 0) {
/* 無法獲得Semaphore, 將Task放到等待清單中 */
prepare_to_wait(&sem->wait_list, &running_task->list, TASK_WAIT);
retval = -EAGAIN; //稍後用來回傳提示使用者重新嘗試
} else {
/* Semaphore獲取成功 */
sem->count--;
retval = 0; //成功,稍後回傳0
}
/* 離開Critical Section */
spin_unlock_irq(&sem->lock);
return retval; //回傳,程式結束
}
/* sem_wait()只能在User task中呼叫 */
int sem_wait(sem_t *sem)
{
/* 進入Critical Section */
spin_lock_irq(&sem->lock);
while(1) {
if(sem->count <= 0) {
/* 無法獲得Semaphore, 將Task放到等待清單中 */
prepare_to_wait(&sem->wait_list, &running_task->list, TASK_WAIT);
/* 離開Critical Section */
spin_unlock_irq(&sem->lock);
/* 讓Task進入睡眠 */
sched_yield();
} else {
/* 成功獲取Semaphore */
sem->count--;
/* 離開Critical Section */
spin_unlock_irq(&sem->lock);
return 0;
}
}
}
```
## IX: 檔案系統 (File System)
### 9.1 檔案以及檔案抽象層 (File Abstraction Layer)
Tenok 目前支援了五種檔案類型,分別為:
| 檔案類型 | 巨集名稱 | 數值 |
| -------- | -------- | -------- |
| FIFO (named pipe) | S_IFIFO | 0 |
| Character device | S_IFCHR | 1 |
| Block device | S_IFBLK | 2 |
| Regular file | S_IFREG | 3 |
| Directory | S_IFDIR | 4 |
為了追蹤所有存在的檔案,Tenok 建立了以下的 File table:
```
//tenok/kernel/kernel.c
...
/* file table */
struct file *files[TASK_CNT_MAX + FILE_CNT_MAX];
int file_cnt = 0;
...
```
由於各個檔案的實作不同,檔案的結構體定義使用到了先前介紹過的技巧讓 File table 指向各個檔案共通的 `struct file` 成員,待需要取出結構體本身時再呼叫 `container_of()` 巨集。另外所有檔案都使用了一份統一的軟體介面 (Software interface) 稱之為 File operations,確保了不同種類的檔案實作之間仍能有一個相同的規範遵循。下圖給出了 File table, File 以及 File operations 之間的關係:
![](https://i.imgur.com/cf6d6on.png)
[圖] File table, File 以及 File operations
以下給出了 `struct file` 和 File operations 的程式定義:
```
//tenok/kernel/fs.h
...
struct file {
struct inode *file_inode; //指向該檔案inode的指標
struct file_operations *f_op; //檔案支援的操作
};
/* 檔案支援的操作 (目前只有實作三個,可逕行擴充) */
struct file_operations {
/* 讀寫頭移動操作函式 */
long (*llseek)(struct file *filp, long offset, int whence);
/* 讀操作函式 */
ssize_t (*read)(struct file *filp, char *buf, size_t size, loff_t offset);
/* 寫操作函式 */
ssize_t (*write)(struct file *filp, const char *buf, size_t size, loff_t offset);
};
...
```
實際設計上,File table 保留了前 TASK_CNT_MAX 個空間給所有的 Task 存放 FIFO 用於行程間通訊 (Inter-Process Communication, IPC),而剩下的 FILE_CNT_MAX 數量才是真正可以使用的空間。下圖給出了上述的的空間分配規則:
![](https://i.imgur.com/PZbJby4.png)
[圖] File table 的空間分配
### 9.2 Super block, inode, dentry 以及 Block
File table 雖然記錄了所有檔案的位址,但是使用上並不夠方便。一個常見的作業系統功能是透過給定的路徑找到所要的檔案,要達到這樣的功能需要更完整的檔案系統設計,因此首先介紹 Super block、inode、dentry 以及 Block 的概念。
所謂的 Super block 一般位於磁碟分割區 (Partition) 的最前端並存有此檔案系統的基本資訊,其在 Tenok 中的程式定義如下:
```
//tenok/kernel/fs.h
struct super_block {
uint32_t s_blk_cnt; //Block的使用數量
uint32_t s_inode_cnt; //inode的使用數量
bool s_rd_only; //是否只能已讀
uint32_t s_sb_addr; //Super block的起始位址
uint32_t s_ino_addr; //inode的起始位址
uint32_t s_blk_addr; //Block的起始位址
};
```
inode 是檔案系統中拓樸結構節點的基本元素,其在程式定義為:
```
//tenok/kernel/fs.h
struct inode {
uint8_t i_mode; //檔案類型
uint8_t i_rdev; //裝置的掛載號碼 (0保留給rootfs)
bool i_sync; //掛載的檔案是否已經同步
uint32_t i_ino; //inode號碼
uint32_t i_parent; //父節點目錄的inode號碼
uint32_t i_fd; //File descriptor號碼
uint32_t i_size; //檔案大小 (Bytes)
uint32_t i_blocks; //Block的使用數量
uint32_t i_data; //資料的虛擬記憶體位址
struct list i_dentry; //dentry list的列表頭 (List head)
};
```
dentry 描述了一個目錄所包含的檔案名稱以及對應的 inode 編號。一般而言檔案名稱不會被保存在 inode 而是在 dentry 中。以下為 dentry 的程式定義:
```
//tenok/kernel/fs.h
struct dentry {
char d_name[FILE_NAME_LEN_MAX]; //檔案名稱
uint32_t d_inode; //檔案的inode號碼
uint32_t d_parent; //父目錄的inode號碼
struct list d_list; //連結多個dentry用的List
};
```
而最後 Block 就是一塊用於儲存資訊的空間。下圖給出了一個檔案系統的拓樸結構範例:
![](https://i.imgur.com/daCdlA3.png)
[圖] 檔案系統的拓樸結構範例,romfs 掛載在 root之下
在 Tenok 中檔案系統由 Super block, inodes 和 Blocks 組成,其在被儲存到 ROM 後會有以下的排列關係:
![](https://i.imgur.com/oUkLyhb.png)
[圖] Tenok 的檔案系統在 ROM 中的儲存結構
為了解釋檔案系統的實作,我們現在考慮一個只存有 `/poem.txt` 檔案和 `/document/` 目錄的情況,並將其繪製為:
![](https://i.imgur.com/OrPk1Qa.png)
[圖] 一個 Tenok 的檔案系統範例
如果 inode 的類型為目錄 (S_IFDIR),則其指向的 Block 空間將儲存 dentry 所組成的列表 (dentry list)。這個列表會給出該目錄下所有檔案的名稱、inode 編號以及指向下一個 dentry 的 `struct list`。由於檔案系統單一 Block 大小有限,不能無限地儲存 dentry。當一個 Block 無法容納新的 dentry 時,檔案系統會分配新的 Block 並透過 dentry 的 `struct list` 成員進行串連。
然而,如果 inode 的類型為標準檔案 (S_IFREG),則其指向的 Block 空間將儲存檔案的內容。同樣地,由於檔案可能需要用到多個 Block 儲存,因此在所有 Block 的頂端 (Header) 有一個用於指向下一個 Block 位址的指標,而最後一個 Block 的指標將指向 NULL。
因此,只要順著第一個 inode (即根目錄) 往下尋訪 (Traversal) 便可找到所有的檔案。舉例來說,inodes[0] 的 dentry list 便包含了 `document/` 及 `poem.txt` 的 inode 資訊。透過上圖我們可以知道 `poem.txt` 的 inode 編號為1,因此只要透過存取 inodes[1].i_data 便可得到儲存檔案內容第一個區塊的位址。
實際上,許多 Unix 作業系統採用的作法是一個 inode 可以記錄多個 Block 的位址,具有較優的效能表現,如:
![](https://i.imgur.com/8gG9Nl8.png)
[圖] inode/block 資料存取示意圖 (來源: 鳥哥私房菜網站 - 蔡德明博士)
而 Tenok 採用了類似於 FAT (File Allocation Table) 檔案系統的串連設計,若要存取第 N 個 Block 則必須先尋訪 (Traverse) 完前 N-1 個 Blocks:
![](https://i.imgur.com/T9RWANp.png)
[圖] FAT檔案系統資料存取示意圖 (來源: 鳥哥私房菜網站 - 蔡德明博士)
同時,許多成熟的檔案系統通常還包含了 inode map 以及 Block map 用來標記 inode 和 Block 的使用情況 (用於達成 inode 和 Block 的動態管理)。但是由於 Tenok 目前沒有實作此功能,因此不支援檔案的刪除。
### 9.3 檔案系統伺服器 (File System Server)
Tenok 參考了 rtenv 將檔案系統實做為 User task。傳統上作業系統可以大致被分為 Monolithic kernel 與 Microkernel 兩派。前者將作業系統的許多模組如 File system, driver 等整合於 Kernel 中以試圖提升作業系統的效能,而後者則儘可能的使 Kernel 保持精簡,並將非關鍵模組放置於 User space 中。此兩者的優缺點可見: [淺談 Microkernel 設計和真實世界中的應用
](https://hackmd.io/@sysprog/microkernel-design)。
回到 Tenok 的部分,`mknod()`, `open()`, `opendir` 以及 `mount()` 等 Syscall 會需要檔案系統從給定的檔案路徑中尋找到對應的 inode, dentry 以及 File descriptor 等。User task 在呼叫這些 Syscall 時,實際上會透過 FIFO 的 IPC 機制向 File system task 發送請求指令,而當任務完成後則會向 Task 寫回請求結果。在任務完成前,User task將會進入睡眠狀態。以下的兩張圖片描述了這個過程:
![](https://i.imgur.com/owYeaHI.png)
[圖] 步驟一: User task 向 File system task 發送檔案操作請求
![](https://i.imgur.com/vDae9vo.png)
[圖] 步驟二: File system task 回傳結果給 User task
可以注意到,圖中指定了 File system task 的 PID 為1。這是因為根據設計,第一個 Task在 載入後必須馬上呼叫 `os_service_init()` 函式載入 File system task,而在 `tenok/kconfig.h` 中也可以見到以下的設定:
```
...
#define FILE_SYSTEM_FD 1 /* file system program is assumed to be the *
* second task to be launched */
...
```
另外我們也可以在 File system task 中見到以下用於處理 User task 請求的程式結構:
```
//tenok/kernel/fs.c
...
void file_system_task(void)
{
...
switch(file_cmd) {
case FS_CREATE_FILE: {
...
break;
}
case FS_OPEN_FILE: {
...
break;
}
case FS_OPEN_DIR: {
...
break;
}
case FS_MOUNT: {
...
break;
}
}
}
}
```
最後需要補充的是,由於 Tenok 假設了上述這些 Syscall (新建檔案、開啟檔案、開啟目錄等) 不會被頻繁的呼叫,因此 File system task 的優先權被設定為最低。
## X. 行程間通訊 (Inter-Process Communication, IPC)
### 10.1 Ring Buffer 資料結構
Tenok 支援了兩種 IPC 功能: FIFO (Named pipe) 及 Message Queue 皆用到了 Ring buffer 的資料結構。Ring buffer 在概念上是一個首尾相連、先進先出 (First In, First Out, FIFO) 的資料結構,如同下圖所示:
![](https://i.imgur.com/gQzaKjr.png)
[圖] Ring buffer 的概念圖
但實際上電腦的記憶體為線性關係,因此 Ring buffer 的實作上必須使用一塊連續記憶體並記錄頭尾位置:
![](https://i.imgur.com/o8Ncg5q.png)
[圖] Ring buffer 在記憶體中實際的狀況
以下為 Ring buffer 結構體的程式定義:
```
//tenok/kernel/ringbuf.h
struct ringbuf {
int start; //開始位置
int end; //結束位置
int count; //目前Ring buffer中擁有的元素數量
void *data; //指向分配給Ring buffer的空間
size_t ring_size; //Ring buffer中可容納的元素數量
size_t type_size; //Ring buffer中單一元素的大小 (幾個Byte)
struct file file; //Ring buffer可被實作為FIFO
struct list task_wait_list; //Ring buffer無法服務Task時的等待清單
};
```
以及各個函式的實作:
```
//tenok/kernel/ringbuf.c
/* 用於更新Ring buffer開始/結束位置的子程式 */
static int ringbuf_increase(struct ringbuf *rb, int ptr)
{
/* 位置加一 */
ptr++;
/* 是否已來到Ring buffer的最末端 */
if(ptr >= rb->ring_size) {
ptr = 0; //是,位置回到最前端
}
/* 回傳新的位置 */
return ptr;
}
/* 將一個資料放入Ring buffer中 */
void ringbuf_put(struct ringbuf *rb, const void *data)
{
size_t type_size = rb->type_size;
/* 檢查Ring buffer是否已滿 */
if(ringbuf_is_full(rb) == true) {
/* 是,覆寫最舊一筆資料以及遞增Ring buffer的開始位置 */
memcpy((char *)(rb->data + (rb->start * type_size)), (char *)data, type_size);
rb->start = ringbuf_increase(rb, rb->start);
} else {
/* 否,從尾端添加一筆新資料 */
memcpy((char *)(rb->data + (rb->end * type_size)), (char *)data, type_size);
rb->count++;
}
/* 更新結束位置因為新資料已被加入Ring buffer */
rb->end = ringbuf_increase(rb, rb->end);
}
/* 從Ring buffer中取出一個資料 */
void ringbuf_get(struct ringbuf *rb, void *data)
{
size_t type_size = rb->type_size;
/* 檢查Ring buffer中是否有資料可以取用 */
if(rb->count > 0) {
/* 資料複製、更新開始位置以及資料數量減一 */
memcpy((char *)data, (char *)(rb->data + (rb->start * type_size)), type_size);
rb->start = ringbuf_increase(rb, rb->start);
rb->count--;
}
}
/* 檢查Ring buffer是否為空 */
bool ringbuf_is_empty(struct ringbuf *rb)
{
return rb->count == 0; //大小為零即空
}
/* Ring buffer是否為滿 */
bool ringbuf_is_full(struct ringbuf *rb)
{
return rb->count == rb->ring_size; //數量等於最大容量即滿
}
```
---
### 10.2 FIFO (Named Pipe)
[FIFO](https://en.wikipedia.org/wiki/Named_pipe) (一般又稱為 Named Pipe) 提供了 [Byte stream](https://zh.wikipedia.org/wiki/%E4%BD%8D%E5%85%83%E7%B5%84%E6%B5%81) (位元組流) 等級的 IPC 功能,在 Tenok 中被設計為 Syscall。
以下摘錄了 FIFO 的原始碼實作:
```
//tenok/kernel/fifo.c
...
ssize_t fifo_read(struct file *filp, char *buf, size_t size, loff_t offset);
ssize_t fifo_write(struct file *filp, const char *buf, size_t size, loff_t offset);
/* 因為FIFO使用了File abstraction layer,所以必須定義支援的File operations */
static struct file_operations fifo_ops = {
.read = fifo_read,
.write = fifo_write
};
int fifo_init(int fd, struct file **files, struct inode *file_inode, struct memory_pool *mem_pool)
{
/* Pipe (Ring buffer) 的記憶體分配和初始化 (註: 大小為Byte級) */
struct ringbuf *pipe = memory_pool_alloc(mem_pool, sizeof(struct ringbuf));
uint8_t *pipe_mem = memory_pool_alloc(mem_pool, sizeof(uint8_t) * PIPE_DEPTH);
ringbuf_init(pipe, pipe_mem, sizeof(uint8_t), PIPE_DEPTH);
/* 將FIFO掛載到File table上 */
pipe->file.f_op = &fifo_ops; //連結所支援的File operations
pipe->file.file_inode = file_inode; //紀錄由檔案系統分配的inode
files[fd] = &pipe->file; //Pipe的file成員掛載到File table上
return 0;
}
ssize_t fifo_read(struct file *filp, char *buf, size_t size, loff_t offset)
{
/* 由Pipe的file成員取得其容器,即Pipe自身 */
struct ringbuf *pipe = container_of(filp, struct ringbuf, file);
/* 如果Task要求的讀取量比實際可行的多,則將其排入等待清單中 */
if(size > pipe->count) {
/* put the current task into the waiting list */
prepare_to_wait(&pipe->task_wait_list, &running_task->list, TASK_WAIT);
/* 打開Syscall pending flag提示Kernel稍後還要回來處理 */
running_task->syscall_pending = true;
return 0; //由於請求暫時無法被滿足,先返回讓Kernel繼續工作
} else {
/* 讀取請求已經被滿足,關閉syscall pending flag */
running_task->syscall_pending = false;
}
/* 從Pipe (Ring buffer) 中取出資料 */
int i;
for(i = 0; i < size; i++) {
ringbuf_get(pipe, &buf[i]);
}
return size; //回傳接讀取到的數量
}
ssize_t fifo_write(struct file *filp, const char *buf, size_t size, loff_t offset)
{
/* 由Pipe的file成員取得其容器,即Pipe自身 */
struct ringbuf *pipe = container_of(filp, struct ringbuf, file);
/* 取出Pipe的Task等待清單 */
struct list *wait_list = &pipe->task_wait_list;
/* 將資料放入Pipe (Ring buffer) 中 */
int i;
for(i = 0; i < size; i++) {
ringbuf_put(pipe, &buf[i]);
}
/* 檢查是否有Task正在等待中 */
if(!list_is_empty(wait_list)) {
/* 從清單中取出一個Task */
struct task_ctrl_blk *task = list_entry(wait_list->next, struct task_ctrl_blk, list);
/* 檢查此Task先前請求的讀取量是否可以被滿足 */
if(task->file_request.size <= pipe->count) {
/* 是,將其喚醒重新嘗試讀取 */
wake_up(wait_list);
}
}
return size; //回傳寫入的數量
}
```
---
### 10.3 訊息佇列 (Message Queue)
Message Queue 與 FIFO 相似都提供了 IPC 功能,但主要差異在於前者支援自定義封包類型而後者僅支援 Byte 型態。同時,Tenok 為了要使 Message Queue 能在 Interrupt 中被呼叫 (參考了 FreeRTOS 中 Queue 的設計),因此沒有將其實作為 Syscall。在 Tenok 提供的 Serial0 驅動程式中便利用了這個特性實作了 RX 的資料接收。
以下摘錄了 Message Queue 的實作原始碼:
```
//tenok/kernel/mqueue.c
...
struct msg_queue mq_table[MQUEUE_CNT_MAX]; //Message Queue Table
int mq_cnt = 0; //目前OS中有的Message Queue的數量
mqd_t mq_open(const char *name, int oflag, struct mq_attr *attr)
{
/* 目前的Message Queue只被允許設定為非阻塞模式 */
if(!(attr->mq_flags & O_NONBLOCK))
return -1;
/* 分配和初始化 Pipe (Ring buffer) 的資料結構 (註: 大小為封包級) */
struct ringbuf *pipe = memory_pool_alloc(&mem_pool, sizeof(struct ringbuf));
uint8_t *pipe_mem = memory_pool_alloc(&mem_pool, attr->mq_msgsize * attr->mq_maxmsg);
ringbuf_init(pipe, pipe_mem, attr->mq_msgsize, attr->mq_maxmsg);
/* 掛載到Message Queue的Table上 (注意: Message Queue與File system屬於不同子系統) */
int mqdes = mq_cnt;
mq_table[mqdes].pipe = pipe; //將分配到的Pipe記憶統空間記錄到Message Queue Table上
mq_table[mqdes].lock = 0; //初始化Message Queue的Spinlock
strncpy(mq_table[mqdes].name, name, FILE_NAME_LEN_MAX); //紀錄Message Queue的名稱 (目前僅用做除錯使用)
mq_cnt++;
/* 回傳Message Queue的描述子 (Descriptor) */
return mqdes;
}
/* mq_receive()只能被User Task呼叫 */
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio)
{
/* 從Table中取出Message Queue */
struct msg_queue *mq = &mq_table[mqdes];
/* 取出Message Queue的Task等待清單 */
struct list *task_wait_list = &mq->pipe->task_wait_list;
ssize_t retval;
/* 進入Critical Section */
spin_lock_irq(&mq->lock);
/* 檢查Pipe (Ring buffer) 中是否有足夠的資料可以提供 */
if(msg_len > mq->pipe->count) {
/* 否,將Task放入等待清單中 */
prepare_to_wait(task_wait_list, &running_task->list, TASK_WAIT);
/* 將Task想讀取的數量紀錄到Task Control Block中 */
running_task->file_request.size = msg_len;
retval = -EAGAIN; //失敗,稍後用來回傳提示使用者重新嘗試
} else {
/* 從Pipe (Ring buffer) 中取出資料 */
int i;
for(i = 0; i < msg_len; i++) {
ringbuf_get(mq->pipe, &msg_ptr[i]);
}
retval = msg_len; //成功,稍後回傳接收到的大小
}
/* 離開Critical Section */
spin_unlock_irq(&mq->lock);
return retval; 回傳,程式結束
}
/* mq_send()可以被User task或是Interrupt呼叫 */
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio)
{
/* 從Table中取出Message Queue */
struct msg_queue *mq = &mq_table[mqdes];
/* 取出Message Queue的Task等待清單 */
struct list *task_wait_list = &mq->pipe->task_wait_list;
/* 進入Critical Section */
spin_lock_irq(&mq->lock);
/* 將資料放入Pipe (Ring buffer) 中 */
int i;
for(i = 0; i < msg_len; i++) {
ringbuf_put(mq->pipe, &msg_ptr[i]);
}
/* 檢查是否有Task正在等待中 */
if(!list_is_empty(task_wait_list)) {
/* 從清單中取出一個Task */
struct task_ctrl_blk *task = list_entry(task_wait_list->next, struct task_ctrl_blk, list);
/* 檢查此Task先前請求的接收量是否可以被滿足 */
if(task->file_request.size <= mq->pipe->count) {
/* 是,將其喚醒重新嘗試接收 */
wake_up(task_wait_list);
}
}
/* 離開Critical section */
spin_unlock_irq(&mq->lock);
return msg_len; //回傳接收到的數量
}
```
## XI: 未來方向
**High-priority goal:**
特化 Tenok 於 Real-time Robotics Control 的應用,否則作為 General-purpose 的作業系統此專案會不具足夠的特色/競爭性 (大抵是 jserv 的意見)
1. Real-time Performance 的驗證/分析
2. 透過 Meta-language 產生通訊專用的程式碼,並提供視覺化工具用於繪製重要訊號 (參考自 [ROS](http://wiki.ros.org/msg))
* 直接支援 ROS 聽起來也很好但大概率會無法達成
3. 支援 [Gazebo](https://gazebosim.org/home) 做 Software-in-the-loop (SIL) simulation
4. 提供 [MAVLink](https://mavlink.io/en/) 通訊的 System Module
5. Online variable tunning? (自動控制演算法調整參數時不用修改程式6重新編譯)
7. 完善檔案系統的設計
8. Unit-test
9. 支援 `malloc()` / `free()`
10. Task scheduling 的視覺化
11. 做完以上幾個重要功能的話就來 Port 一個簡易版的四軸飛控作為應用範例
**Low-priority goal:**
以下功能也想做,但除了作為練習,目前想不到實用的價值
1. 支援 RPi2040 並支援多核心、Load balancing 等
2. 支援 RISC-V (RV32)
## 附錄: Tenok API
### Syscall:
```
//tenok/kernel/syscall.h
void sched_yield(void);
void set_irq(uint32_t state);
void set_program_name(char *name);
int fork(void);
uint32_t sleep(uint32_t ticks);
int mount(const char *source, const char *target);
int open(const char *pathname, int flags, mode_t);
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
long lseek(int fd, long offset, int whence);
int fstat(int fd, struct stat *statbuf);
int opendir(const char *name, DIR *dir);
int readdir(DIR *dirp, struct dirent *dirent);
uint32_t getpriority(void);
int setpriority(int which, int who, int prio);
int getpid(void);
int mknod(const char *pathname, mode_t mode, dev_t dev);
```
### Function call:
```
//tenok/kernel/semaphore.h
int sem_init(sem_t *sem, int pshared, unsigned int value);
int sem_post(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_getvalue(sem_t *sem, int *sval);
//tenok/kernel/mutex.h
int pthread_mutex_init(_pthread_mutex_t *mutex, const pthread_mutex_attr_t *attr);
int pthread_mutex_unlock(_pthread_mutex_t *mutex);
int pthread_mutex_lock(_pthread_mutex_t *mutex);
//tenok/kernel/mqueue.h
mqd_t mq_open(const char *name, int oflag, struct mq_attr *attr);
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio);
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio);
//tenok/kernel/file.h
int fopen(const char *pathname, const char *mode, FILE *file);
size_t fread(void *ptr, size_t size, size_t nmemb, FILE *fstream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *fstream);
int fseek(FILE *stream, long offset, int whence);
int fileno(FILE *stream);
```
## 參考資料
1. [rtenv+: 成大資工Wiki](http://wiki.csie.ncku.edu.tw/embedded/rtenv)
2. [Build a Minimal Operating System in ARM Cortex-M3](http://wiki.csie.ncku.edu.tw/embedded/summer2015/mini-arm-os.pdf)
3. [從無到有打造 IoT 作業系統核心:以 Piko/RT 為例](https://hackmd.io/@owlfox/SyaTF2VgL/https%3A%2F%2Fhackmd.io%2Fs%2FSJHIn4b_W)
4. [FreeRTOS 內核實現與應用開發實戰指南 — 基於 STM32](https://www.tenlong.com.tw/products/9787111618256)
5. [嵌入式系統建構:開發運作於STM32的韌體程式](http://wiki.csie.ncku.edu.tw/embedded/Lab19/stm32-prog.pdf)
6. [Cortex-M4 Devices Generic User Guide - Arm](https://documentation-service.arm.com/static/5f2ac76d60a93e65927bbdc5?token=)
7. [Cortex-M4(F) Lazy Stacking and Context Switching - Application Note 298](https://developer.arm.com/documentation/dai0298/a/)
8. [STM32 Cortex®-M4 MCUs and MPUs programming manual](https://www.st.com/resource/en/programming_manual/pm0214-stm32-cortexm4-mcus-and-mpus-programming-manual-stmicroelectronics.pdf)
9. [ARM Cortex-A Series Programmer's Guide for ARMv7-A: Procedure Call Standard
](https://developer.arm.com/documentation/den0013/d/Application-Binary-Interfaces/Procedure-Call-Standard)
9. [A Beginner’s Guide on Interrupt Latency and Interrupt Latency of the Arm Cortex-M processors](https://community.arm.com/arm-community-blogs/b/architectures-and-processors-blog/posts/beginner-guide-on-interrupt-latency-and-interrupt-latency-of-the-arm-cortex-m-processors)
10. [Linux 核心設計: 淺談同步機制](https://hackmd.io/@sysprog/linux-sync?type=view)
11. [Priority Inversion on Mars](https://www.slideshare.net/jserv/priority-inversion-30367388?fbclid=IwAR0GdoqVaKk41XrMoyWtO-lHasoJ4fZVhDw7X50o1GEFdgRoJAq-aNvolss)
12. [Synchronization: Spinlocks - Semaphores (CS 4410 Operating Systems, Cornell University)](http://www.cs.cornell.edu/courses/cs4410/2013su/slides/lecture06.pdf)
13. [關於GNU Inline Assembly](http://wen00072.github.io/blog/2015/12/10/about-inline-asm/)
14. [offsetof(3) - Linux manual page](https://man7.org/linux/man-pages/man3/offsetof.3.html)
15. [你所不知道的 C 語言: linked list 和非連續記憶體](https://hackmd.io/@sysprog/c-linked-list#circular-linked-list)
16. [Linux 磁碟與檔案系統管理 - 鳥哥的私房菜網站](https://linux.vbird.org/linux_basic/centos7/0230filesystem.php)
17. [淺談 Microkernel 設計和真實世界中的應用
](https://hackmd.io/@sysprog/microkernel-design)