今天增加第三個行程,讓輸出依序出現 P1 → P2 → P3。
工作完成一小段後只呼叫 yield(),由核心決定下一個工作,而不是在工作裡寫死接手者。
這就是排程器(Scheduler)開始承擔的責任:在目前可以執行的工作之間做選擇。
《Operating System Concepts》第 10 版第 5.3.3 節的 Round-Robin,使用時間片(Time Quantum)與計時器中斷。
時間到時,即使行程沒有主動交還 CPU,作業系統也能搶占並切到下一個行程。
我們今天先做合作式的輪替排程,沿用《OS in 1,000 Lines》的主動 yield() 做法。
它有循環選擇的順序,但沒有計時器強制停止工作的能力,不能直接套用搶占式 RR 的最長等待時間公式。
這個差異可以由程式驗證:如果某個工作不再呼叫 yield(),其餘工作就可能一直拿不到執行機會。
今天的完成條件是輪替、狀態與計數正確,並非完成了具有時間保證的排程器。
在 30-days-os-kernel/examples/tiny-kernel/ 執行:
make DAY=12
make DAY=12 run
完整程式在 scheduler.c,DAY=12 會建立三個行程,使用循環搜尋分支。
每個工作跑三輪,各輪結尾呼叫 yield()。
yield() 只表達「我先交還 CPU」void yield(void) {
KASSERT(current && current->state == RUNNING);
current->yield_count++;
current->state = RUNNABLE;
trace("RUNNING -> RUNNABLE");
switch_context(¤t->context, &scheduler_context);
KASSERT(current->state == RUNNING);
}
切換前先把自己標成 RUNNABLE,表示之後還想繼續執行。
等這次呼叫終於返回時,已經是核心再次選中自己,這時狀態應回到 RUNNING。
yield() 沒有把本地迴圈變數清掉,也沒有重新配置堆疊。
Day 11 的 context switch 保存了可以返回的現場,排程器只是決定何時使用它。
排程器保存一個 cursor,每次從該位置開始,最多檢查行程表一圈。
遇到 RUNNABLE 就選中它,並把下一次搜尋起點移到它的後面。
unsigned int i = (cursor + j) % count;
if (!next && procs[i].state == RUNNABLE) {
next = &procs[i];
cursor = (i + 1) % count;
break;
}
完整迴圈也會檢查是否已全部完成,避免沒有工作時一直空轉。
Dispatch 前設定 current、改成 RUNNING 並增加 run_count,切回核心後再清除 current。
因此統計的是核心真的交出 CPU 的次數,而不是工作自行印出多少行。
這個小表格只有三格,線性搜尋已足夠清楚。
暫時不需要引入複雜佇列,先把搜尋位置與狀態的關係驗證好。
預期最後統計為:
pid=1 scheduled=4 yield=3 sleep=0 wake=0
pid=2 scheduled=4 yield=3 sleep=0 wake=0
pid=3 scheduled=4 yield=3 sleep=0 wake=0
每個行程前三次取得 CPU 後都印出一輪並 yield()。
第四次恢復是為了離開迴圈、返回入口包裝,再由包裝標記 DONE。
如果把 scheduled 當成工作迴圈次數,就會把正常結果誤判為多跑了一次。
《Operating System Concepts》第 5.2 節列出等待時間、回應時間與周轉時間等指標。
我們目前只有次數,並沒有量到這些時間。
可以在 task() 中,為 P1 的每輪工作加入以下延遲,再用 make DAY=12 run 觀察:
if (current->pid == 1) {
for (unsigned int i = 0; i < 1000000; i++)
__asm__ __volatile__("nop");
}
即使最後 scheduled 仍然相同,P1 每次占用的時間已變長,其他工作也會等待更久。
這是用程式看出「計數公平」與「時間公平」的差別,不要把這個延遲迴圈換算成精確毫秒。
完成對照後移除額外延遲,保留原本三個工作量相近的版本。
若某個 PID 完全沒出現,檢查它是否為 RUNNABLE,以及 cursor 是否真的往後移。
今天的主要變更在 scheduler.c 的選擇迴圈、yield() 與 lab.h 的計數欄位。
建議 commit 訊息:
day12: implement round-robin scheduler
Day 13 會讓一個工作暫時不具備執行條件,看看排程器如何跳過它,再由另一個工作喚醒。