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 预留内存块的最高地址 。
\[sp = stack0 + (mhartid + 1) \times 4096\]
这里的 \(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);
- }
复制代码 这里可以看到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 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
- }
- }
复制代码
- 锁的释放与环境初始化: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中恢复所有用户态通用寄存器,开始执行用户程序。
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |