iT邦幫忙

2026 iThome 鐵人賽

DAY 24
0
Software Development

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

Day 24|Sleep/Wakeup:等待 I/O 的 Process 到底去了哪裡?

  • 分享至 

  • xImage
  •  

子行程開始讀 pipe,父行程卻還沒有寫入資料。
畫面暫時沒有新文字,不表示 CPU 必須停在讀取迴圈裡等待,核心可以先執行其他工作。
今天用這個情境觀察 SLEEPING、喚醒與再次排程。

先確認我們正在讀哪個版本

以下沿用 Day 19 的固定 commit 與 Day 21 開始的開發分支。
這份 xv6 使用 sleep_prepare(chan) 登記等待事件,再呼叫無參數的核心 sleep()。
使用者空間的延遲函式則叫 pause(ticks),不要與舊版教學的 sleep(chan, lock) 或使用者 sleep(n) 混用。

用一個 byte 看見等待

在 xv6 checkout 新增 user/pipewait.c:

#include "kernel/types.h"
#include "user/user.h"

int
main(void)
{
  int fd[2];
  if (pipe(fd) < 0)
    exit(1);
  int pid = fork();
  if (pid < 0)
    exit(1);
  if (pid == 0) {
    close(fd[1]);
    char ch = 0;
    printf("reader pid=%d waiting\n", getpid());
    int n = read(fd[0], &ch, 1);
    close(fd[0]);
    printf("reader received=%d value=%c\n", n, ch);
    exit(n != 1 || ch != 'K');
  }
  close(fd[0]);
  pause(20);
  int ok = write(fd[1], "K", 1) == 1;
  close(fd[1]);
  int status = -1;
  if (wait(&status) < 0)
    exit(1);
  exit(!ok || status != 0);
}

把 $U/_pipewait\ 加到 Makefile 的 UPROGS 清單內,保留續行。
父行程延遲只是提高子行程先等待的機會,不是正式的同步保證,真正保證讀寫關係的是 pipe 協定。
若要精確驗證等待是否發生,還必須看核心的狀態紀錄。

事件先到了,會不會永遠睡下去?

kernel/pipe.c 的 piperead() 先在 pipe lock 保護下確認緩衝區為空,再登記等待 channel,之後才放開 pipe lock 並嘗試睡眠。
這個順序要避免 lost wakeup:通知已經發生,但讀取端卻在通知之後才開始等待。

本版本的 wakeup() 會清除符合的 p->chan。
如果通知發生在 sleep_prepare() 與 sleep() 之間,後者看到 channel 已清除,就不再進入 SLEEPING。
如果真的已經睡下去,喚醒才會把狀態改成 RUNNABLE。

這裡有兩個不同的問題:事件通知有沒有被記住,以及行程是否已經實際交還 CPU。
不是每次呼叫 sleep() 都一定發生一次上下文切換。

把狀態轉移加進計數器

沿用 Day 22 的 scheduled,在 kernel/proc.h 的 struct proc 加入以下欄位,全部以 p->lock 保護:

uint64 sleeps;
uint64 wakes;
uint64 yields;

在 allocproc() 設定 USED 的位置初始化三個欄位為 0。
接著在 kernel/proc.c 加入以下三處更新,不要改動原本取得與釋放鎖的位置:

位置 插入內容
sleep(),p->state = SLEEPING; 後 p->sleeps++;
wakeup(),SLEEPING 分支內改成 RUNNABLE 後 p->wakes++;
yield(),改成 RUNNABLE 後 p->yields++;

為了這次觀察,可以在 sleep 更新後加:

if (p->sleeps <= 3)
  printk("sleep pid=%d chan=%p\n", p->pid, p->chan);

在 wake 更新後加同樣有上限的紀錄,channel 使用 wakeup() 的參數 chan,因為 p->chan 此時已被清除:

if (p->wakes <= 3)
  printk("wake pid=%d chan=%p\n", p->pid, chan);

跑一次,按 PID 對照

在 30-days-os-kernel/examples/xv6-riscv/ 執行:

make TOOLPREFIX=riscv64-linux-gnu- CPUS=1 qemu

於 xv6 Shell 執行 pipewait,應先看到 reader 訊息,之後看到 received=1 value=K。
對照 reader PID 的 sleep/wake,並觀察被喚醒之後仍需由 scheduler 選中才會繼續執行。
其他行程也可能輸出等待紀錄,不能把相鄰兩行直接當成同一事件。

計數的名字必須對得上實際定義

sleeps 計的是實際改成 SLEEPING 的次數,不是等待函式呼叫數。
wakes 只計本次 wakeup() 造成的 SLEEPING → RUNNABLE,不包含 kill 路徑等其他喚醒方式。
yields 也包含 timer 導致的核心 yield,不等於使用者自願讓出 CPU。

這些差異會保留到最後的 Observer,不能到報表階段再把欄位解釋成另一種意義。
Day 29 會關閉這些即時文字,保留計數更新,減少量測干擾。

本日修改 proc.h、proc.c 與 Makefile,新增 user/pipewait.c。
建議 commit:

day24: observe pipe blocking and wakeup transitions

下一篇暫時離開狀態轉移,回到每個行程的位址空間,印出真正的 Sv39 頁表。

參考資料


上一篇
Day 23|深入 xv6 Scheduler:Process 到底怎麼拿到 CPU?
下一篇
Day 25|深入 xv6 Virtual Memory:把 walk() 與 mappages() 跑給你看
系列文
打造 OS Kernel:從 OS in 1,000 Lines 到 xv6 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言