iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
Software Development

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

Day 26|追蹤一次完整 System Call:getpid() 怎麼來回 User 與 Kernel?

  • 分享至 

  • xImage
  •  

使用者程式呼叫 getpid(),最後拿回一個整數。
這個過程看起來像普通函式,實際卻走過使用者組語入口、Trap、核心 dispatcher,再恢復使用者現場。
今天用只做一次 getpid 的小程式,避免讓大量輸出掩蓋真正要追的路徑。

先準備一個容易辨認的呼叫

在 xv6 checkout 新增 user/pidprobe.c:

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

int
main(void)
{
  int pid = getpid();
  printf("pidprobe returned=%d\n", pid);
  exit(0);
}

在 Makefile 的 UPROGS 清單加入 $U/_pidprobe\。
於 30-days-os-kernel/examples/xv6-riscv/ 建置:

make TOOLPREFIX=riscv64-linux-gnu- kernel/kernel fs.img
riscv64-linux-gnu-objdump -d user/_pidprobe

搜尋反組譯中的 <getpid>,應看到載入 syscall 編號、ecall 與返回指令。
這個 stub 由 user/usys.pl 產生,不應直接修改建置產物 user/usys.S 來增加永久功能。

依階段加入有篩選條件的紀錄

以下都是暫時的診斷輸出,僅針對 pidprobe 的 getpid。
在 kernel/trap.c 加入 #include "syscall.h",並在 usertrap() 的 syscall 分支中、syscall(); 前加入:

if (strcmp(p->name, "pidprobe") == 0 &&
    p->trapframe->a7 == SYS_getpid)
  printk("getpid trap pid=%d resume=%p\n",
         p->pid, (void *)p->trapframe->epc);

此處 epc 已經增加 4,表示返回時應執行的位置,不是 ecall 原本的 PC。
RISC-V 的 ecall 是 32-bit 指令,這不能推廣成所有 Trap 都把 PC 加 4。

在 kernel/syscall.c 有效編號分支中,原本的 handler 呼叫前後加入:

int watching = num == SYS_getpid && strcmp(p->name, "pidprobe") == 0;
if (watching)
  printk("getpid dispatch pid=%d number=%d\n", p->pid, num);
p->trapframe->a0 = syscalls[num]();
if (watching)
  printk("getpid return pid=%d value=%lu\n", p->pid, p->trapframe->a0);

這一段取代原本那一行 handler 呼叫,不是把呼叫再執行一次。
在 kernel/sysproc.c 的 sys_getpid() 返回前,加上同樣按程式名稱篩選的紀錄:

if (strcmp(myproc()->name, "pidprobe") == 0)
  printk("getpid handler pid=%d\n", myproc()->pid);

預期得到一條順序清楚的時間線

在 xv6 checkout 執行 make TOOLPREFIX=riscv64-linux-gnu- CPUS=1 qemu,於 xv6 Shell 執行 pidprobe。
應依序看到:

getpid trap ...
getpid dispatch ...
getpid handler ...
getpid return ...
pidprobe returned=...

PID 與回傳值應相同,最後一行是 U-mode 程式取得結果後才輸出的證據。
這是一條事件順序,不是精確的延遲量測,printk() 本身就會改變執行時間。

a0 如何從核心回到使用者手上?

sys_getpid() 的 C 返回值先交給 dispatcher,再寫進 p->trapframe->a0。
prepare_return() 準備返回所需的控制狀態,之後 trampoline 的 userret 切回使用者頁表並恢復暫存器,最後執行 sret。

因此不是把核心的 C 呼叫堆疊直接交給使用者,也不是一般 ret 自動降低權限。
Day 17 的 Tiny Kernel 已經做過相同責任的最小版本,xv6 則把頁表切換與 trampoline 也整合進來。

留下一個真正可持續使用的計數器

在 struct proc 新增 uint64 syscalls;,並於 allocproc() 設成 0。
在 syscall() 的有效編號分支、handler 呼叫前加入:

acquire(&p->lock);
p->syscalls++;
release(&p->lock);

這個計數只包括有效且已分派的系統呼叫,未知編號不計入。
鎖要在 handler 前釋放,因為 handler 可能睡眠、結束行程或再取得其他鎖。
像 exit 這種不返回的 handler 也會被計到,所以欄位名稱不應解釋成「已成功完成的呼叫數」。

本日新增 pidprobe.c,修改 Makefile、proc.h、proc.c、trap.c、syscall.c 與 sysproc.c。
Day 29 會移除這些臨時列印,只保留計數與可選擇的 trace。

day26: trace getpid and count dispatched system calls

下一篇故意讓多個工作一起更新同一個核心計數器,看看為什麼一次讀取與一次寫入各自正確,合起來仍可能出錯。

參考資料


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

尚未有邦友留言

立即登入留言