xv6 如何開始運行第一個用户進程
1. 硬件復位與內核加載
qemu 是虛擬主板。它模擬了 RISC-V 處理器、內存條、串口(用於輸出文字到你的終端)、以及磁盤驅動器 。xv6 的初始化始於 QEMU 模擬的硬件復位 。根據kernel.ld鏈接腳本的約束,內核鏡像被加載至物理地址0x80000000。
2. 啓動棧的分配與物理操作
stack0 是一全局變量,在start.c定義,使用編譯器指令強制指向這塊內存的地址進行 16 位對齊,分配了ncpu * 頁大小的內存 。
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];
寄存器物理上存在於每個 CPU 內部,對寄存器的操作對每個 CPU 來説都只是在操作自己的 CPU 。對於entry.S中部分代碼:
la sp, stack0
li a0, 1024*4
csrr a1, mhartid
addi a1, a1, 1
mul a0, a0, a1
add sp, sp, a0
由於 RISC-V 的棧是向下增長的,我們需要將sp指向每個 CPU 預留內存塊的最高地址 。
這裏的 \(4096\) 是為每個核心分配的物理啓動棧空間 。請注意,此時尚未開啓分頁,我們直接在物理內存上操作 。
3. 特權級切換:從 Machine 到 Supervisor
// set M Previous Privilege mode to Supervisor, for mret.
unsigned long x = r_mstatus();
x &= ~MSTATUS_MPP_MASK;
x |= MSTATUS_MPP_S;
w_mstatus(x);
// set M Exception Program Counter to main, for mret.
w_mepc((uint64)main);
// disable paging for now.
w_satp(0);
asm volatile("mret");
xv6 的三級權限:Machine mode、Supervisor mode、User mode 。從entry.S跳轉到start(),手動修改mstatus寄存器,將“先前模式”(MPP)設置為 Supervisor 。並將main函數的地址填入mepc(機器模式異常程序計數器)。當執行mret時,CPU 硬件會誤以為剛才發生了一個異常,最終跳回異常發生前的地址(main),並恢復當時的模式(S-Mode) 。由於現在並無分頁機制,satp清零,暫時關閉頁表翻譯,直接使用物理地址 。
4. 時鐘中斷初始化
void timerinit() {
// enable supervisor-mode timer interrupts.
w_mie(r_mie() | MIE_STIE);
// enable the sstc extension (i.e. stimecmp).
w_menvcfg(r_menvcfg() | (1L << 63));
// allow supervisor to use stimecmp and time.
w_mcounteren(r_mcounteren() | 2);
// ask for the very first timer interrupt.
w_stimecmp(r_time() + 1000000);
}
在 Machine mode 的權限下,將時鐘中斷的處理權限交給 Supervisor mode 權限 。時鐘中斷處理並不在中斷向量表中。
5. 內核初始化與多核同步策略
void kvminithart() {
// wait for any previous writes to the page table memory to finish.
sfence_vma();
w_satp(MAKE_SATP(kernel_pagetable));
// flush stale entries from the TLB.
sfence_vma();
}
// set up to take exceptions and traps while in the kernel.
void trapinithart(void) {
w_stvec((uint64)kernelvec);
}
void plicinithart(void) {
int hart = cpuid();
// set enable bits for this hart's S-mode for the uart and virtio disk.
*(uint32*)PLIC_SENABLE(hart) = (1 << UART0_IRQ) | (1 << VIRTIO0_IRQ);
// set this hart's S-mode priority threshold to 0.
*(uint32*)PLIC_SPRIORITY(hart) = 0;
}
void main() {
if(cpuid() == 0){
userinit();
__sync_synchronize();
started = 1;
} else {
while (atomic_read4((int*)&started) == 0)
;
__sync_synchronize();
kvminithart(); // turn on paging
trapinithart(); // install kernel trap vector
plicinithart(); // ask PLIC for device interrupts
}
}
在main函數執行階段,內核採取了非對稱初始化策略 :
-
主核全局初始化:由 0 號 Hart(主核)負責一次性初始化所有全局對象(如物理內存分配器、磁盤驅動等),並靜態構建首個用户程序 。
-
多核同步序列:其餘從核(Secondary Harts)在同步變量started上進行自旋等待 。直至主核完成全局自舉並更新started信號後,從核方可進入各自的局部初始化流程 。
-
隔離的局部初始化:各個核心的初始化操作在硬件層面上是相互隔離的,以構建獨立的運行上下文 :
-
內存管理:配置satp寄存器指向統一的內核頁表 。通過鎖機制(Locks)與內存一致性訪問(Memory Barriers)確保多進程並行執行時的原子性,防止各核心間進程數據的非法篡改或污染 。
-
異常處理:由於各核心需獨立處理其運行進程觸發的 Trap(陷阱),因此必須分別為當前核設置stvec指向kernelvec。通過加載該中斷向量,確保核心在發生中斷或異常時能夠準確跳轉至處理程序 。
-
中斷控制 (PLIC):配置平台級中斷控制器(PLIC),使能 Supervisor 模式下的外部中斷源——串口(UART0_IRQ)與磁盤(VIRTIO0_IRQ),從而允許核接收並響應來自串口和磁盤的中斷 。
-
6. 進程調度器 (Scheduler)
main函數最後執行了scheduler()。
struct proc proc[NPROC];
void scheduler(void) {
struct proc *p;
struct cpu c = mycpu();
c->proc = 0;
for (;;) {
intr_on();
intr_off();
int nproc = 0;
for (p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if (p->state != UNUSED) {
nproc++;
}
if (p->state == RUNNABLE) {
p->state = RUNNING;
c->proc = p;
swtch(&c->context, &p->context);
c->proc = 0;
}
release(&p->lock);
}
if (nproc <= 2) { // only init and sh exist
intr_on();
}
}
}
邏輯如下 :
- 設置當前 CPU 不運行進程;
- 無限遍歷proc[NPROC],intr_on():處理中斷,intr_off():將下面的判斷操作變成臨界區,防止被其他進程打擾;
- 統計UNUSED進程數目;
- 找到RUNNABLE進程後,CPU 執行swtch(&c->context, &p->context)後,不再執行調度器,而是執行進程 p,等到進程 p 執行exit()/yield()/sleep(),這些函數會調用sched()->swtch(&p->context, &mycpu()->context)讓 CPU 重新執行調度器,接着再執行c->proc = 0,表示當前這個 CPU 此刻在調度器上下文裏,未運行任何進程;
- 若只有init進程和sh進程,開中斷。
7. 初始進程的分配與構造
到這裏,調度器肯定是可以調度初始創建的用户進程來執行的 。
static struct proc* allocproc(void) {
struct proc* p;
// ... 查找 UNUSED 進程 ...
found:
p->pid = allocpid();
p->state = USED;
// Allocate a trapframe page.
p->trapframe = (struct trapframe*)kalloc();
// An empty user page table.
p->pagetable = proc_pagetable(p);
// Set up new context to start executing at forkret.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
return p;
}
void userinit(void) {
struct proc* p;
p = allocproc();
initproc = p;
p->cwd = namei("/");
p->state = RUNNABLE;
release(&p->lock);
}
這裏可以看到userinit->alloproc中,使用了allocpid()為進程分配了 pid,分配了trapframe頁,以及頁表 。最下面設置了進程 p 的ra為forkret和棧,當調度到進程 p 時,swtch最後ret指令會返回ra指向的forkret所在地址,從forkret開始運行,也就是説,forkret 是進程第一次被調度到 CPU 上時執行的內核入口 。
8. Trapframe 和 Trampoline
-
trapframe 物理頁映射了進程從用户態切換至內核態時所需的所有關鍵現場信息,具體包括 :
- 用户態現場恢復:保存了 31 個通用寄存器(除 x0 外)的快照,以及epc(異常程序計數器)。epc記錄了發生異常時用户程序的指令地址,以便以後跳回來。能夠保證用户程序被中斷後,以後能原封不動地恢復運行。
- 內核態執行環境:存儲了進入內核所需的必要參數,包括:satp(內核頁表的地址)、sp(內核棧指針)、當前 CPU 的 Hart ID、以及系統調用處理函數usertrap的地址。
-
trampoline 頁面映射了兩段至關重要的底層彙編邏輯,負責跨特權級的上下文切換 :
- uservec:負責把用户寄存器存入trapframe,並將頁表從用户頁表切換到內核頁表。
- userret:負責把頁表切換回用户頁表,並從trapframe恢復寄存器,最後跳回用户態。
9. forkret 流程與特權級躍遷
void prepare_return(void) {
struct proc* p = myproc();
intr_off();
uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);
w_stvec(trampoline_uservec);
p->trapframe->kernel_satp = r_satp();
p->trapframe->kernel_sp = p->kstack + PGSIZE;
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp();
unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
x |= SSTATUS_SPIE; // enable interrupts in user mode
w_sstatus(x);
w_sepc(p->trapframe->epc);
}
void forkret(void) {
extern char userret[];
static int first = 1;
struct proc* p = myproc();
release(&p->lock); // Still holding p->lock from scheduler.
if (first) {
fsinit(ROOTDEV);
first = 0;
__sync_synchronize();
p->trapframe->a0 = kexec("/init", (char*[]){"/init", 0});
}
prepare_return();
uint64 satp = MAKE_SATP(p->pagetable);
uint64 trampoline_userret = TRAMPOLINE + (userret - trampoline);
((void (*)(uint64))trampoline_userret)(satp);
}
-
鎖的釋放與環境初始化:forkret首先釋放當前進程在調度過程中持有的p->lock。隨後,在首個進程執行時(if(first)),負責觸發文件系統初始化(fsinit)並執行內存屏障,確保多核環境下的可見性 。
-
用户鏡像加載:通過執行kexec將當前進程地址空間替換為/init用户程序鏡像 。根據 RISC-V 調用規範,將kexec的返回值(如參數個數)存入p->trapframe->a0,作為用户態程序的初始輸入 。
-
硬件狀態機預置 (prepare_return):為從 Supervisor Mode 切換至 User Mode 建立軟硬件上下文 :
-
中斷向量重定向:將stvec寄存器由kernelvec修改為uservec,確保用户態異常能正確觸發蹦牀邏輯 。
-
內核錨點保存:將當前內核的頁表地址(satp)、棧指針(sp)等寄存器快照存入trapframe,為後續從用户態切回內核態預留環境恢復數據 。
-
特權級降級準備:清除sstatus寄存器的SSP位(設為 0),指定下一次執行sret指令後進入 User Mode 。同時讀取epc,當執行sret後返回用户態的執行語句 。
-
-
地址空間切換與返回:最後設置用户進程頁表,調用trampoline_userret跳轉回用户態 。
-
執行 sret 觸發躍遷:在userret執行sret指令後,硬件將根據預設邏輯自動完成:權限從 Supervisor Mode 降至 User Mode;程序計數器(PC)跳轉至sepc所指向的/init入口地址;從trapframe中恢復所有用户態通用寄存器,開始執行用户程序。