iT邦幫忙

2026 iThome 鐵人賽

DAY 11
0
Software Development

打造 OS Kernel:從 OS in 1,000 Lines 到 xv6系列 第 11

Day 11|Context Switch:讓 CPU 真正從 P1 切換到 P2

  • 分享至 

  • xImage
  •  

今天的輸出會在 P1P2 之間來回,兩者各自保留 round,也各自使用不同的堆疊。
同一顆 CPU 執行一段工作後,保存現場,再從另一份現場繼續,這就是上下文切換(Context Switch)。

Day 10 只是準備好行程資料,今天才真正載入那些資料。
最後兩個工作都會結束,CPU 回到核心的控制流程,印出統計資訊。

暫停的工作需要留下什麼?

《Operating System Concepts》第 10 版第 3.2.3 節,將切換描述為保存原行程狀態、載入另一個行程狀態。
暫存器與執行位置都必須有保存處,否則下次拿回 CPU 時,行程不知道自己剛才執行到哪裡。

這篇先在普通 C 函式呼叫的邊界切換。
呼叫端依 ABI 處理 caller-saved registers,切換函式保存 rasps0s11,正好對應 Day 10 的 struct context
這個較小的 context 不能替代 Day 08 在任意指令位置保存的 Trap Frame。

書中也提醒切換本身有成本,保存狀態的時間不會替行程完成實際工作。
今天先驗證正確性,不拿 QEMU 的列印速度衡量真實硬體的切換效能。

使用本系列的完整實驗檔案

Day 11~Day 18 的完整程式集中在 examples/tiny-kernel
它延續前十天的 Boot、Console 與配置器,讓各篇可以共用相依檔案,DAY 選擇此次開機執行的實驗。
這是本系列的參考實作,與《OS in 1,000 Lines》的〈行程〉採取相同的保存與恢復概念,但使用獨立 switch.S 和結構內的保存區。

在系列資料夾執行:

cd examples/tiny-kernel
make DAY=11
make DAY=11 run

以下所有操作都在這個範例目錄,產物位於 build/day11/
程式使用 RV32、單一 hart 與 128 MiB RAM,頁池擴大成 128 頁,對應的配置表和 linker 空間已一起調整。

ret 可以回到另一份現場

閱讀 switch.S,介面如下:

void switch_context(struct context *old, struct context *next);

a0 指向要保存的 context,a1 指向即將恢復的 context。
組語先把目前 rasp 寫入 old,再保存十二個 s 暫存器,之後按相同 offset 從 next 讀回。

sw ra, 0(a0)
sw sp, 4(a0)

恢復階段則從下一份 context 取得新的 rasp
最後的 ret 使用的是新載入的返回位址,因此可能回到另一個行程先前呼叫切換函式的位置。
第一次啟動行程時,這個位址則是我們預先填入的 process_start()

完整組語還包含所有 s0s11 的保存與恢復,不能只複製上面兩行就當成可用切換函式。

第一個返回位址要指向入口包裝

scheduler.c,建立行程時會設定:

p->entry = task;
p->context.sp = (uint32_t) p->stack_base + PAGE_SIZE;
p->context.ra = (uint32_t) process_start;
p->state = RUNNABLE;

process_start() 呼叫真正工作,工作返回後把狀態設成 DONE,再切回核心。
這樣工作可以正常結束,不必假設每個入口永遠不返回。
行程堆疊也要等切回核心堆疊後才回收,不能在仍使用它時把記憶體交還配置器。

今天採固定順序 dispatch,路徑是:

核心 → P1 → 核心 → P2 → 核心 → P1

中間的核心 context 保存在 scheduler_context,讓切換有共同返回點。
Day 12 才會把固定順序改成一般的可執行行程選擇。

確認各自的迴圈與堆疊真的留下來

預期輸出包含:

P1 round=0 sp=0x...
P2 round=0 sp=0x...
P1 round=1 sp=0x...
P2 round=1 sp=0x...
P1 round=2 sp=0x...
P2 round=2 sp=0x...

round 不應每次都重設為零,否則可能是每次重新啟動入口,而不是恢復舊現場。
程式也會檢查目前 sp 是否在自己的堆疊範圍內,兩個行程印出的位址應落在不同的頁面。

需要看暫存器時,先結束 QEMU,再執行 make DAY=11 debug
另一個終端機在相同目錄執行 gdb-multiarch build/day11/kernel.elf

target remote 127.0.0.1:1234
hbreak *switch_context
continue
info registers ra sp a0 a1 s0 s1
p *(struct context *) $a1

接著用 si 單步,觀察載入 sp 之後的堆疊和 ret 之後的位置。
若返回位置錯亂,優先檢查結構 offset、ra 初值與載入方向。

保存切換功能

本篇主要檔案是 lab.hswitch.Sscheduler.c,完整共用相依檔案見範例 README。
建議 commit 訊息:

day11: implement context switch

下一篇會讓核心自己挑選下一個可執行行程,工作只需要呼叫 yield(),不必知道誰會接手 CPU。

參考資料


上一篇
Day 10|Process 是什麼?第一次建立兩個 Kernel Process
下一篇
Day 12|第一次寫 Scheduler:Round-Robin 如何分享 CPU?
系列文
打造 OS Kernel:從 OS in 1,000 Lines 到 xv613
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言