iT邦幫忙

2026 iThome 鐵人賽

DAY 21
0
Software Development

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

Day 21|恐龍書的 PCB 在 xv6 裡長什麼樣?深入 struct proc

  • 分享至 

  • xImage
  •  

在 xv6 Shell 按下 Ctrl+P,終端機會列出目前的行程。
其中有正在等待輸入的 Shell,也有開機時建立的 init,這些名字背後都對應到核心中的一格 struct proc。

今天從這張表出發,追到行程剛建立的那一刻,看看 PID、核心堆疊與頁表何時準備好。
本文沿用 Day 19 固定的 xv6 commit 9e3161a9abf5f51ea402562d1874caf6c4926597,後續改動會在同一份開發分支累積。

PCB 不是只有 PID 的通訊錄

《Operating System Concepts》把行程控制區塊(Process Control Block,PCB)用來組織作業系統管理行程所需的狀態。
在 xv6 中,對應的結構是 kernel/proc.h 裡的 struct proc,但內容不會與教科書示意圖逐欄相同。

欄位 回答的問題
pid、state 這是誰,目前能否執行?
context 下次恢復核心執行時,從哪裡繼續?
trapframe 使用者程式被中斷時的暫存器在哪裡?
pagetable、sz 使用者位址空間如何組成?
kstack 這個行程進入核心時使用哪一份堆疊?
ofile、cwd 開啟的檔案與目前目錄是什麼?

context 與 trapframe 最容易混淆。
前者支援核心上下文切換,後者保存使用者 Trap 的現場,不能因為兩者都有暫存器就合併成一個概念。

Checkpoint 1:先保留未修改的觀察

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

git status --short
git switch -c kernel-observer
make TOOLPREFIX=riscv64-linux-gnu- CPUS=1 qemu

若分支已存在,直接使用自己的開發分支,不必重建。
啟動後按 Ctrl+P,再執行 echo pcb-ready,最後再按一次 Ctrl+P。
短命的 echo 可能已退出,因此不一定出現在第二份列表,這正好提醒我們:列表是一個觀察時點,不是歷史紀錄。

原本的 procdump() 為了方便卡死時除錯,刻意不取得所有行程鎖。
因此它適合作為診斷畫面,不應被宣稱為多核心下的一致性快照。

Checkpoint 2:在建立成功的位置留下紀錄

先結束 QEMU,在 kernel/proc.c 的 allocproc() 定義前加入:

static void
obs_created(struct proc *p)
{
  if (!holding(&p->lock))
    panic("obs_created lock");
  printk("create pid=%d state=%d kstack=%p pagetable=%p\n",
         p->pid, p->state, (void *)p->kstack, (void *)p->pagetable);
}

在 allocproc() 最後,設定完 p->context.sp 之後、成功的 return p; 之前呼叫:

obs_created(p);

這個位置已成功配置 Trap Frame 與頁表,而且仍持有 p->lock。
不要放在每個失敗返回之前,也不要將尚未初始化的指標拿來輸出。

重新執行 make TOOLPREFIX=riscv64-linux-gnu- CPUS=1 qemu,再於 xv6 Shell 輸入 echo pcb-ready。
應看見新的 create 訊息,位址請以自己的建置結果為準。

為什麼此時不是 RUNNABLE?

固定版本的 allocproc() 先把狀態設成 USED,代表這格已被占用,但還沒完成成為可執行行程的全部準備。
建立者還要填入使用者映像或複製父行程資源,之後才改成 RUNNABLE。

同樣地,剛建立的頁表不表示使用者程式已完整載入。
看到非零指標,只證明有一份頁表結構,不能據此推論 text、stack 都已存在。

行程退出後,為什麼不能立即消失?

沿著 kexit()、kwait() 與 freeproc() 閱讀,行程結束後會先保留 ZOMBIE 狀態,讓父行程取得結束結果,再回收可重用的行程表位置。
這與 Day 11 的 DONE 有相似目的,但 xv6 還必須處理父子關係與等待。

核心堆疊也不是在每次 allocproc() 都重新配置。
本版本的 proc_mapstacks() 在初始化時替行程表各格建立核心堆疊映射,後續會重用對應的位置。
所以先後兩個不同 PID 出現相同 kstack,不一定是錯誤,可能是同一格被再次使用。

觀察欄位前,先找保護它的規則

proc.h 的註解把欄位分成不同保護範圍。
部分欄位需要 p->lock,父子關係由 wait_lock 協調,另一些則由目前行程自己管理。
不能把「我已取得 p->lock」當成可以任意讀寫其他行程所有欄位的通行證。

這也是後面設計統計 API 時要先決定的事:究竟讀取自己的資料,還是想取得全系統的一致快照?
兩者需要的同步設計不同。

本日修改 kernel/proc.c,加入建立成功的診斷 helper,不改變原本行程生命週期。
這個開機診斷只供前期檢查,Day 29 整合時會移除,避免干擾統計測試。

建議 commit:

day21: inspect xv6 process allocation and lifecycle

下一篇沿著 context 走進 swtch.S,看看行程資料如何真的接手 CPU。

參考資料


上一篇
Day 20|xv6 Boot:從 entry.S 一路 Trace 到 main()
下一篇
Day 22|swtch.S:把 Tiny OS 與 xv6 Context Switch 放在一起比較
系列文
打造 OS Kernel:從 OS in 1,000 Lines 到 xv6 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言