今天 P1 一開始就進入等待,暫時離開可執行的集合。
等 P2 發出喚醒通知,P1 才重新取得排程資格,並繼續原本的工作。
我們會把每次狀態轉換印出來,讓「沒輪到它」和「它目前不能執行」成為兩件能從紀錄辨認的事。
《Operating System Concepts》第 10 版第 3.1.2 節,區分正在執行、已準備好與等待事件的行程。
Ready 等的是 CPU,Waiting 等的則是某個條件,例如 I/O 完成。
本系列用 RUNNABLE 對應 Ready,用 WAITING 表示暫時不能繼續。
若把所有工作都當成可執行,等待者就只能一直檢查條件,浪費執行機會。
把它移出可執行集合,排程器才有辦法把 CPU 留給能推進的工作。
今天用另一個行程呼叫喚醒函式模擬事件,不接實際裝置,也不按經過幾秒判定喚醒。
這讓每次狀態改變都有一個可追蹤的來源。
在 30-days-os-kernel/examples/tiny-kernel/ 執行:
make DAY=13 run
閱讀 scheduler.c 的 task(),P1 先呼叫 sleep_current(),P2 則呼叫 wakeup_pid(1)。
之後三個工作再各自完成三輪。
void sleep_current(void) {
KASSERT(current && current->state == RUNNING);
current->sleep_count++;
current->state = WAITING;
trace("RUNNING -> WAITING");
switch_context(¤t->context, &scheduler_context);
}
這裡先改狀態再切回核心,排程器之後搜尋時就會跳過 P1。
如果誤設成 RUNNABLE,它只是普通的 yield(),並沒有真正等待事件。
wakeup_pid() 只把符合 PID 且狀態為 WAITING 的行程改成 RUNNABLE,並增加 wake_count。
它不直接呼叫行程入口,也不立刻執行 context switch。
因此正常順序是:
P1:RUNNING → WAITING
P2:將 P1 改成 RUNNABLE
核心稍後選中 P1:RUNNABLE → RUNNING
這個區分很重要,否則容易把「有人通知事件已發生」誤讀成「等待者已經跑完接下來的工作」。
本次維持循環搜尋,所以 P1 會依序等到自己的下一個執行機會。
預期結果:
pid=1 scheduled=5 yield=3 sleep=1 wake=1
pid=2 scheduled=4 yield=3 sleep=0 wake=0
pid=3 scheduled=4 yield=3 sleep=0 wake=0
P1 比昨天多取得一次 CPU,因為第一次執行先去等待,喚醒後才開始三輪工作。
這次額外切入並不是它完成了更多迴圈,而是執行路徑增加了一次睡眠與恢復。
run_count 計算 dispatch,yield_count 只計算明確的 yield(),所以 sleep 不會偷偷算進 yield。
先把指標的定義固定下來,之後做 Kernel Observer 才能比較不同測試。
排程器找不到 RUNNABLE 時,還要區分所有工作都已 DONE,或仍有工作停在 WAITING。
範例對後者印出 Panic,因為目前沒有計時器或裝置中斷會帶來新事件。
如果改成無條件 wfi,很可能只會得到一個永遠等不到進展的畫面。
可在實驗副本中暫時移除 P2 的 wakeup_pid(1),重跑後預期看到:
all live tasks waiting; no event source
完成後恢復喚醒呼叫。
這個對照用來抓出遺漏通知的情況,不是通用 deadlock detector。
我們固定單核心、沒有搶占,而且檢查與狀態修改之間不會主動 yield()。
因此這個小實驗中的轉換不會被另一個工作插入。
若加入多核心或可在中途介入的中斷,條件檢查、進入等待與喚醒之間就需要更嚴格的同步。
Day 24 讀 xv6 的 sleep() 與 wakeup() 時,會看到鎖如何避免 Lost Wakeup,也就是事件已通知但等待者卻錯過了它。
今天的主要檔案仍是 scheduler.c 與 lab.h,建議 commit 訊息:
day13: add process states and scheduling trace
Day 14 轉向記憶體視角,讓相同實體頁透過不同虛擬位址被看見,開始準備行程的位址空間。