---
# System prepended metadata

title: 'Multi-threading on top of Operating System in 1,000 Lines'
tags: [courses]

---

# Multi-threading on top of Operating System in 1,000 Lines

你們的任務是詳閱 [Operating System in 1,000 Lines](https://operating-system-in-1000-lines.vercel.app/en) 並在 QEMU 上實驗，先逐步建構出小型的RISC-V 作業系統，隨後擴充程式碼，使其具備部分 POSIX Thread 介面，例如 mutex, conditionvariable, semaphore, barrier 等，而且該考慮到 priority inversion。

參見: https://hackmd.io/@sysprog/concurrency-thread-package

[期末專題 hackmd](https://hackmd.io/@sysprog/BJBqnuPBye)
## 事前資料閱讀


### 基本資料
[isa-zicsr](https://drive.google.com/file/d/1uviu1nH-tScFfgrovvFCrj7Omv8tFtkp/view)
[ISA-privilged](https://drive.google.com/file/d/17GeetSnT5wW3xNuAHI95-SI1gPGd5sJ_/view)

### ISA
#### [CSR instructions]([isa-zicsr](https://drive.google.com/file/d/17GeetSnT5wW3xNuAHI95-SI1gPGd5sJ_/view)) p.65
![{F2D02CFA-268A-48FC-BA90-61767751FEDA}](https://hackmd.io/_uploads/S1ze7MY8kg.png)


##### Conditions determining whether a CSR instruction reads or writes the specified CSR.
* Register operand

| **Instruction**   | **rd is x0** | **rs1 is x0** | **Reads CSR** | **Writes CSR** |
|--------------------|--------------|---------------|---------------|----------------|
| CSRRW             | Yes          | -             | No            | Yes            |
| CSRRW             | No           | -             | Yes           | Yes            |
| CSRRS/CSRRC       | -            | Yes           | Yes           | No             |
| CSRRS/CSRRC       | -            | No            | Yes           | Yes            |

* Immediate operand

| **Instruction**       | **rd is x0** | **uimm = 0** | **Reads CSR** | **Writes CSR** |
|------------------------|--------------|--------------|---------------|----------------|
| CSRRWI                | Yes          | -            | No            | Yes            |
| CSRRWI                | No           | -            | Yes           | Yes            |
| CSRRSI/CSRRCI         | -            | Yes          | Yes           | No             |
| CSRRSI/CSRRCI         | -            | No           | Yes           | Yes            |


### 作業系統層次架構
```graphviz
digraph RISC_V_OS {
    rankdir=TB;
    node [shape=box, style=filled, fillcolor=lightblue];

    UserSpace [label="User Space\nHigh-level APIs\n(e.g., write(), sleep())"];
    OSKernel [label="OS Kernel\nSystem Calls\n(e.g., sys_write(), sys_sleep())"];
    SBI [label="SBI (Supervisor Binary Interface)\nHardware Abstraction Layer\n(e.g., UART, Timer, Shutdown)"];
    Hardware [label="Hardware\nUART, Timer, CPU"];

    subgraph cluster_UserSpace {
        label = "User Space";
        style = rounded;
        node [shape=box];
        write [label="write(fd, buf, count)"];
        sleep [label="sleep(ticks)"];
    }

    subgraph cluster_OSKernel {
        label = "OS Kernel";
        style = rounded;
        node [shape=box];
        sys_write [label="sys_write(fd, buf, count)"];
        sys_sleep [label="sys_sleep(ticks)"];
        SBIWrapper [label="SBI Wrappers"];
    }

    subgraph cluster_SBI {
        label = "SBI";
        style = rounded;
        node [shape=box];
        sbi_console_putchar [label="sbi_console_putchar"];
        sbi_set_timer [label="sbi_set_timer"];
        sbi_shutdown [label="sbi_shutdown"];
    }

    subgraph cluster_Hardware {
        label = "Hardware";
        style = rounded;
        node [shape=box];
        UART [label="UART (Serial Port)"];
        Timer [label="Timer"];
        CPU [label="CPU"];
    }

    // Connections
    write -> sys_write;
    sleep -> sys_sleep;
    sys_write -> sbi_console_putchar;
    sys_sleep -> sbi_set_timer;
    sbi_console_putchar -> UART;
    sbi_set_timer -> Timer;
    sbi_shutdown -> CPU;
}
```

### 巨集語法

`__VA_ARGS__` 是 C 的一個標準功能，用於處理可變參數宏。它允許宏接收不定數量的參數
`##` 當可變參數為空時，## 會移除前面多餘的逗號，避免編譯錯誤。

### 異常處理
RISC-V 中異常處理的簡化過程
異常發生後，處理器：

1. 保存當前指令的地址到 sepc (保存異常指令地址的 CSR )。
記錄異常原因到 scause。
根據 stvec 的值跳轉到異常處理程序。
在進入異常處理程序之前：

2. 堆疊指針（SP）被設置到內核專用的空間（可以透過 sscratch 暫存舊的 SP）。
一些必要的暫存器值可能已經自動保存到堆疊。
異常處理程序：

3. 呼叫 handle_trap 函數，使用 a0 中的堆疊指針，根據 scause 判斷異常的原因。
根據異常類型進行具體處理，並可能修改保存的寄存器值。
處理完成後：

4. 恢復保存的寄存器內容。
使用 sret 返回到異常發生的位置，繼續執行原本的程式。
:::info
* sscratch 當 trap 發生時，暫存此執行緒的 kernel sp
* 保存異常狀態的結構的地址會保存在 a0 傳遞給 exception_handler
* `WRITE_CSR` 設置 `stvec` 為進入異常處理入口 (`kernel_entry`) 的地址。
`kernel_entry` 主要責任是：
	1. 保存當前處理器狀態（寄存器、堆疊等）。
	2. 確定異常的原因（通過讀取 scause, sepc, stval 等 CSR）。
	3. 根據異常原因執行對應的處理邏輯（如非法指令、頁面錯誤或中斷處理）。
	4. 恢復處理器狀態，並繼續執行原程序或進行其他操作。
:::

### 模擬 trap

```clike
void kernel_main(void)
{
    memset(__bss, 0, (size_t)__bss_end - (size_t)__bss);

    WRITE_CSR(stvec, (uint32_t)kernel_entry);
    __asm__ __volatile__("unimp"); // pseudo instruction to trigger an illegal instruction trap

    for (;;)
    {
        __asm__ __volatile__("wfi");
    }
}
```

:::info
`#reg` 將 reg 轉換成字串
```
 __asm__ __volatile__("csrw " #reg ", %0" ::"r"(__tmp))
 ```
 :::
 
### Linker Script
特殊的腳本語言，用於控制 鏈接器（Linker） 在將多個目標文件（object files）組合成可執行文件（executable）時，如何安排記憶體的使用和程式的佈局。

可以定義例如 `__stack_top ` 等各種符號，讓 c 語言可以使用來控制記憶體分配。 

### memory allocation
`PAGE_SIZE` 定義在 `common.h` 而非在 `kernel.h` 因為它為通用的變數，其他模組也可能用到。不單只用於內核管理，廣泛的記憶體管理都會用到。

`next_paddr` 記錄了分配的下一個空閒地址，因此需要在每次呼叫間保留值，靜態變數非常適合這種需求。

## 實作
### 系統資訊
```bash!
Operating System: Ubuntu 22.04.4 LTS              
          Kernel: Linux 6.8.0-47-generic
    Architecture: x86-64
```

### 介紹
在極簡 RISC-V OS，往「單行程、多執行緒（POSIX threads）」擴充：在不破壞原本多工/分頁/系統呼叫/簡易檔案與 shell 的骨架上，加上 TCB。

### 實作說明
1. 建立 TCB（[thread.h](https://github.com/nelson0720j/posix_thread_in_1k_lines/commit/22497e4897d0d74114d925f52515051546b3d2e6#diff-76e2e90299ffb45b1f53bfb55d71710faeae10eb96be201efebe5e622e47b59bR17)）

```clike
struct thread {
    int tid;
    enum thread_state state;
    uint32_t kernel_stack_sp;
    struct process *parent;
    uint8_t kernel_stack[8192];

    int priority;
};
```
2. 建立 [thread](https://github.com/nelson0720j/posix_thread_in_1k_lines/commit/22497e4897d0d74114d925f52515051546b3d2e6#diff-161b2a279f4c67a1ab075a7890ecf6f3f1d483d910910fa52b3715e25cfdcbd7R83)
* 從 threads[] 找空位
* 準備 kernel stack 初值
* 放到就緒隊列
```clike
int thread_create(struct process *proc, void (*fn)(void *), void *arg){
    // 尋找一個空的threads[i]
    int i;
    for(i=0; i<MAX_THREADS; i++){
        if(threads[i].state == THREAD_UNUSED){
            break;
        }
    }
    if(i == MAX_THREADS){
        return -1; // no slot
    }
    struct thread *t = &threads[i];
    t->tid = alloc_tid++;
    t->state = THREAD_RUNNABLE;
    t->parent = proc;

    // 初始化 kernel stack
    uint32_t *sp = (uint32_t*)(t->kernel_stack + sizeof(t->kernel_stack));
    
    *--sp = (uint32_t)&thread_trampoline;
    
    // 預留空間給 callee-saved registers (s0~s11)
    for(int i=0; i<12; i++){
        *--sp = 0;
    }
    *--sp = (uint32_t)arg;
    *--sp = (uint32_t)fn;
    t->kernel_stack_sp = (uint32_t)sp;

    // 放到就緒隊列
    enqueue_thread(t);
    return t->tid;
}
```
:::spoiler
|--------------------------|  <- stack top (高位址)
|  fn                     |
|  arg                    |
|  s0                     |
|  s1                     |
|  ...                    |
|  s11                    |
|  return address (ra=thread_trampoline)
|--------------------------|  <- stack bottom (低位址)

:::
3. 第一次執行入口（[thread_trampoline](https://github.com/nelson0720j/posix_thread_in_1k_lines/commit/22497e4897d0d74114d925f52515051546b3d2e6#diff-161b2a279f4c67a1ab075a7890ecf6f3f1d483d910910fa52b3715e25cfdcbd7R68)，在 S-mode）
* 從當前 kernel stack 取出 (fn, arg)
* 直接 jalr 呼叫 fn(arg)
* 回來就 thread_exit()

```clike
__attribute__((naked)) static void thread_trampoline(void){
    __asm__ volatile (
        // 從當前 kernel stack 取出 fn 與 arg
        "lw a0, 0(sp)\n"   // a0=arg
        "lw a1, 4(sp)\n"   // a1=fn

        "jalr a1\n" // 跳到真正要執行的函式 fn 的位址

        // 執行完 fn() 就 thread_exit
        "call thread_exit\n"
        "1: j 1b\n"
    );
}
```

4. [排程](https://github.com/nelson0720j/posix_thread_in_1k_lines/commit/22497e4897d0d74114d925f52515051546b3d2e6#diff-161b2a279f4c67a1ab075a7890ecf6f3f1d483d910910fa52b3715e25cfdcbd7R140)與情境切換
* 簡單 RR：目前 RUNNABLE 的先丟回佇列，再取下一個
* 若跨 process，先切 satp（page table），sfence.vma (清除TLB)
* 設 sscratch 為 要切入 thread 的 kernel stack top
* 呼叫 switch_context(&prev_sp, &next_sp)，只交換 kernel SP，恢復 * s0~s11、ra，跳到對方的 kernel stack 上繼續執行

5. user side 介面與測試
* user space 中包裝好 sys call [thread_create(fn,arg) / thread_exit()](https://github.com/nelson0720j/posix_thread_in_1k_lines/commit/22497e4897d0d74114d925f52515051546b3d2e6#diff-d44815bd5187b5fda25f879bf3d1b536966c0f5bc596ec776eed9b49f1035b7eR40)
* shell 新增 thread 指令

### user stack
```
        kernel (S-mode)
          |
          | trap_return (從 trapframe 還原 user 狀態，sret) 等同 kernel_entry 尾端
          v
        sret → U-mode
          |
          | thread_trampoline_u
          v
        user fn(arg)
```
建立 user stack
1. TCB 新增以下:
`struct trapframe *tf` // trapframe 位址
`*user_stack_top`
建 thread 時，先把它放到自己的 kernel stack 頂端
2. thread create 新增:
分配 user stack
在該 thread 的 kernel stack 上預留一塊位址，讓 trapframe 在 kernel stack 上面
設定 sepc 為 thread_trampoline_u
sstatus 設成 SPP=U
3. 排程切換
若換到不同 process：切 satp + sfence.vma
更新 `sscratch = next->kstack_top`
更新 kernel stack 位址 `switch_context(&prev->kernel_stack_sp, &next->kernel_stack_sp)`
4. trap 入口/出口
建立 thread_trampoline_u (user-side thread 的入口點（U-mode）)

流程:
1. thread_create
2. 第一次 context switch
3. 在 thread_trampoline：
kernel thread：直接 call fn(arg)；結束呼叫 thread_exit()。
user thread：載入 trapframe，sret → U-mode 進入 thread_trampoline_u。
4. thread_trampoline_u：取出 fn/arg → 執行 → syscall(thread_exit) 回 kernel 清理。