iT邦幫忙

2026 iThome 鐵人賽

DAY 12
0
Software Development

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

Day 12|第一次寫 Scheduler:Round-Robin 如何分享 CPU?

  • 分享至 

  • xImage
  •  

今天增加第三個行程,讓輸出依序出現 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.cDAY=12 會建立三個行程,使用循環搜尋分支。
每個工作跑三輪,各輪結尾呼叫 yield()

yield() 只表達「我先交還 CPU」

void yield(void) {
    KASSERT(current && current->state == RUNNING);
    current->yield_count++;
    current->state = RUNNABLE;
    trace("RUNNING -> RUNNABLE");
    switch_context(&current->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 當成工作迴圈次數,就會把正常結果誤判為多跑了一次。

一樣多次,不代表一樣多 CPU 時間

《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 會讓一個工作暫時不具備執行條件,看看排程器如何跳過它,再由另一個工作喚醒。

參考資料


上一篇
Day 11|Context Switch:讓 CPU 真正從 P1 切換到 P2
下一篇
Day 13|Scheduler 不只要選人:加入 Process State 與排程觀察功能
系列文
打造 OS Kernel:從 OS in 1,000 Lines 到 xv613
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言