网站首页

人工智能P2P分享Wind搜索发布信息网站地图标签大全

当前位置:诺佳网 > 软件工程 > 操作系统 > Linux >

xv6如何开始运行第一个用户进程

时间:2026-03-03 20:47

人气:

作者:admin

标签:

导读:xv6 如何开始运行第一个用户进程 1. 硬件复位与内核加载 qemu 是虚拟主板。它模拟了 RISC-V 处理器、内存条、串口(用于输出文字到你的终端)、以及磁盘驱动器 。xv6 的初始化始于 QEM...

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);
}

在 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();
    }
  }
}

逻辑如下

  1. 设置当前 CPU 不运行进程;
  2. 无限遍历proc[NPROC],intr_on():处理中断,intr_off():将下面的判断操作变成临界区,防止被其他进程打扰;
  3. 统计UNUSED进程数目;
  4. 找到RUNNABLE进程后,CPU 执行swtch(&c->context, &p->context)后,不再执行调度器,而是执行进程 p,等到进程 p 执行exit()/yield()/sleep(),这些函数会调用sched()->swtch(&p->context, &mycpu()->context)让 CPU 重新执行调度器,接着再执行c->proc = 0,表示当前这个 CPU 此刻在调度器上下文里,未运行任何进程;
  5. 若只有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);
}

  1. 锁的释放与环境初始化:forkret首先释放当前进程在调度过程中持有的p->lock。随后,在首个进程执行时(if(first)),负责触发文件系统初始化(fsinit)并执行内存屏障,确保多核环境下的可见性 。

  2. 用户镜像加载:通过执行kexec将当前进程地址空间替换为/init用户程序镜像 。根据 RISC-V 调用规范,将kexec的返回值(即参数个数)存入p->trapframe->a0,作为用户态程序的初始输入,并且构建用户进程页表并为其分配空间。

  3. 硬件状态机预置 (prepare_return):为从 Supervisor Mode 切换至 User Mode 建立软硬件上下文 :

    • 中断向量重定向:将stvec寄存器由kernelvec修改为uservec,确保用户态异常能正确触发蹦床逻辑 。
    • 内核锚点保存:将当前内核的页表地址(satp)、栈指针(sp)等寄存器快照存入trapframe,为后续从用户态切回内核态预留环境恢复数据 。
    • 特权级降级准备:清除sstatus寄存器的SSP位(设为 0),指定下一次执行sret指令后进入 User Mode 。同时读取epc,当执行sret后返回用户态的执行语句 。
  4. 地址空间切换与返回:此时,p->pagetable指向的是用户页表,使用MAKE_SATP宏设置用户进程页表,调用trampoline中的userret部分跳转回用户态 。

  5. 执行 sret 触发跃迁:在userret执行sret指令后,硬件将根据预设逻辑自动完成:权限从 Supervisor Mode 降至 User Mode;程序计数器(PC)跳转至sepc所指向的/init入口地址;从trapframe中恢复所有用户态通用寄存器,开始执行用户程序。

温馨提示:以上内容整理于网络,仅供参考,如果对您有帮助,留下您的阅读感言吧!
相关阅读
本类排行
相关标签
本类推荐

CPU | 内存 | 硬盘 | 显卡 | 显示器 | 主板 | 电源 | 键鼠 | 网站地图

Copyright © 2025-2035 诺佳网 版权所有 备案号:赣ICP备2025066733号
本站资料均来源互联网收集整理,作品版权归作者所有,如果侵犯了您的版权,请跟我们联系。

关注微信