时间:2026-03-03 20:47
人气:
作者:admin
qemu 是虚拟主板。它模拟了 RISC-V 处理器、内存条、串口(用于输出文字到你的终端)、以及磁盘驱动器 。xv6 的初始化始于 QEMU 模拟的硬件复位 。根据kernel.ld链接脚本的约束,内核镜像被加载至物理地址0x80000000。
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 预留内存块的最高地址 。
\[sp = stack0 + (mhartid + 1) \times 4096 \]这里的 \(4096\) 是为每个核心分配的物理启动栈空间 。请注意,此时尚未开启分页,我们直接在物理内存上操作 。
// 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清零,暂时关闭页表翻译,直接使用物理地址 。
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 权限 。时钟中断处理并不在中断向量表中。
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),从而允许核接收并响应来自串口和磁盘的中断 。
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();
}
}
}
逻辑如下 :
到这里,调度器肯定是可以调度初始创建的用户进程来执行的 。
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 上时执行的内核入口 。
trapframe 物理页映射了进程从用户态切换至内核态时所需的所有关键现场信息,具体包括 :
trampoline 页面映射了两段至关重要的底层汇编逻辑,负责跨特权级的上下文切换 :
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 建立软硬件上下文 :
地址空间切换与返回:此时,p->pagetable指向的是用户页表,使用MAKE_SATP宏设置用户进程页表,调用trampoline中的userret部分跳转回用户态 。
执行 sret 触发跃迁:在userret执行sret指令后,硬件将根据预设逻辑自动完成:权限从 Supervisor Mode 降至 User Mode;程序计数器(PC)跳转至sepc所指向的/init入口地址;从trapframe中恢复所有用户态通用寄存器,开始执行用户程序。