xv6启动过程

entry,S -> start.c -> main.c -> proc.c中的 userinit 函数 -> initcode.S -> init.c

entry.S

// entry.S
	# qemu -kernel loads the kernel at 0x80000000
        # and causes each CPU to jump there.
        # kernel.ld causes the following code to
        # be placed at 0x80000000.
.section .text
.global _entry
_entry:
	# set up a stack for C.
        # stack0 is declared in start.c,
        # with a 4096-byte stack per CPU.
        # sp = stack0 + (hartid * 4096)
        la sp, stack0
        li a0, 1024*4
		csrr a1, mhartid
        addi a1, a1, 1
        mul a0, a0, a1
        add sp, sp, a0
	# jump to start() in start.c
        call start
spin:
        j spin

entry.S 用于在 QEMU 模拟器上启动 xv6 操作系统。它定义了启动过程中的入口点和初始化堆栈的过程。以下是对这段代码的逐行解释:

  1. .section .text:指定以下代码应该放置在文本段(.text)中,这是用于存储程序代码的标准段。

  2. .global _entry:声明 _entry 是一个全局符号,这意味着它可以被外部引用,例如链接器或其他汇编文件。

  3. _entry::定义了一个标签 _entry,这是当前代码段的入口点。

    1. la sp, stack0:使用 la 指令加载 stack0 的地址到 sp 寄存器。stack0 是在 start.c 中声明的一个数组,为每个 CPU 核心提供了一个 4096 字节大小的堆栈。
    2. li a0, 1024*4:将立即数 1024*4(即 4096)加载到 a0 寄存器。这个值是每个 CPU 核心堆栈的大小。
    3. csrr a1, mhartid:使用 csrr 指令读取 mhartid 寄存器的值到 a1 寄存器。mhartid 寄存器包含当前硬件线程(hart)的 ID。
    4. addi a1, a1, 1:将 a1 寄存器的值加 1。这是因为 mhartid 寄存器的值从 0 开始,而 stack0 数组的索引从 1 开始。
    5. mul a0, a0, a1:将 a0 寄存器(堆栈大小)与 a1 寄存器(hart ID 索引)相乘,计算出当前 hart 的堆栈在 stack0 数组中的偏移量。
    6. add sp, sp, a0:将计算出的偏移量加到 sp 寄存器(当前 hart 的堆栈顶部)上,设置正确的堆栈指针。
    7. call start:跳转到 start 函数。start 函数是 start.c 中定义的,它初始化操作系统并进入调度器的主循环。
  4. spin::定义了一个标签 spin

    1. j spin:无限跳转到 spin 标签,形成 spinlock(自旋锁)。如果 start 函数返回,控制权会回到这里,但由于 start 函数不应该返回,这个自旋锁实际上会阻止 CPU 执行任何其他代码。

这段代码确保每个 CPU 核心都有一个独立的堆栈(sp = stack0 + (hartid + 1) * 4KB),并且它们都跳转到 start 函数开始执行。这是操作系统启动过程的一部分,它在物理硬件或模拟器上启动操作系统之前设置必要的环境。

为什么必须首先设置栈指针?

  1. 时间顺序上的必要性:_entry 是内核的第一条指令,紧接着需要调用 start 函数。如果栈指针未设置,call 指令会尝试将返回地址压栈,但由于 sp 无效,可能会写入非法地址,导致崩溃。
  2. 硬件约束:RISC-V 的启动规范不保证 sp 的初始值,QEMU 也不会自动初始化栈指针。因此,操作系统必须显式设置。
  3. 后续依赖:start.c 中的代码会进一步调用其他函数(如 timerinit),这些函数依赖栈来存储局部变量和调用上下文。如果没有栈,整个初始化过程无法继续。

start.c

start 函数负责初始化 CPU 的特权模式、内存管理和中断处理,并最终跳转到用户空间的 main 函数执行。

  1. 设置 M 模式的返回特权级别
    通过读取和修改 mstatus 寄存器,将 Previous Privilege (PP) 模式设置为 Supervisor (S) 模式。当后续执行 mret 时,CPU 将从 M 模式切换到 S 模式。
  2. 设置 M Exception Program Counter
    mepc 寄存器设置为 main 函数的地址。这意味着当 CPU 从中断或异常返回时,它将跳转到 main 函数继续执行。
  3. 禁用分页机制
    通过将 satp 寄存器设置为 0,暂时禁用分页机制。此时内核还未初始化页表(这在 main() 中的 kvminit() 完成),因此暂时禁用分页。这是在操作系统完全启动并设置好内存管理单元 (MMU) 之前的必要步骤。
  4. 设置中断和异常代理到 S 模式
    通过写入 medelegmideleg 寄存器,将所有中断和异常代理给 Supervisor 模式处理。同时,通过设置 sie 寄存器启用 Supervisor 模式的中断和异常。
  5. 配置物理内存保护
    通过设置 pmpaddrpmpcfg 寄存器,配置物理内存保护,允许 Supervisor 模式访问所有物理内存地址。
  6. 初始化时钟中断
    调用 timerinit 函数,初始化时钟中断,使 CPU 能够在需要时请求中断。
  7. 保存硬件线程 ID
    将当前硬件线程 (hart) 的 ID 保存到 tp 寄存器中,以便后续可以通过 cpuid 函数获取。
  8. 切换到 Supervisor 模式并跳转到 main 函数
    通过执行 mret 指令,切换到 Supervisor 模式,并跳转到 main 函数开始执行。这标志着操作系统启动流程的完成,控制权转移到用户空间的入口点。

这段代码是操作系统启动过程中的关键部分,它确保了 CPU 正确配置并准备好执行用户空间的程序。通过这些初始化步骤,操作系统能够管理内存、处理中断和异常,并提供一个安全的环境来运行用户程序。

// entry.S jumps here in machine mode on stack0.
void
start()
{
  // set M Previous Privilege mode to Supervisor, for mret.
  // 这些行设置了 RISC-V 机器状态寄存器(mstatus)的 Previous Privilege (PP) 模式为 Supervisor(S)模式。
  // 这是为了确保在执行 mret 指令(从中断或异常返回)时,CPU 能够返回到 Supervisor 模式。
  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.
  // requires gcc -mcmodel=medany
  // 这里将RISC-V机器异常程序计数器(mepc)设置为main()函数的地址。
  // 这是中断或异常处理完成后,CPU应该返回执行的地址。
  w_mepc((uint64)main);

  // disable paging for now.
  // 这行代码禁用了分页机制,将 RISC-V 机器的页表根地址寄存器(satp)设置为 0。
  w_satp(0);

  // delegate all interrupts and exceptions to supervisor mode.
  // 这些行设置了中断和异常的代理(delegation),允许Supervisor模式处理所有的中断和异常。
  w_medeleg(0xffff);
  w_mideleg(0xffff);
  w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

  // configure Physical Memory Protection to give supervisor mode
  // access to all of physical memory.
  // 这里配置了物理内存保护(PMP),允许Supervisor模式访问所有物理内存。
  w_pmpaddr0(0x3fffffffffffffull);
  w_pmpcfg0(0xf);

  // ask for clock interrupts.
  // 这行代码初始化了时钟中断,以便 CPU 可以在需要时请求中断。
  timerinit();

  // keep each CPU's hartid in its tp register, for cpuid().
  // 这里将当前硬件线程(hart)的ID保存到RISC-V的线程指针(tp)寄存器中,
  // 以便后续可以通过cpuid()函数获取
  int id = r_mhartid();
  w_tp(id);

  // switch to supervisor mode and jump to main().
  // 这行代码通过执行 mret 指令(从中断或异常返回)来切换到 Supervisor 模式,并跳转到 main() 函数开始执行。
  asm volatile("mret");
}

main.c

void
main()
{
  // 核0执行的代码
  if(cpuid() == 0){
    consoleinit();
    printfinit();
    printf("\n");
    printf("xv6 kernel is booting\n");
    printf("\n");
    kinit();      // physical page allocator 初始化物理页分配器,这是操作系统管理内存的关键部分。
    kvminit();    // create kernel page table    初始化内核页表,这是虚拟内存管理的基础。
    kvminithart();   // turn on paging              在当前 hart 上启用分页机制。
    procinit();      // process table   初始化进程表,这是操作系统管理进程的关键数据结构。
    trapinit();  // trap vectors    初始化中断和异常处理向量,这是操作系统响应硬件事件的关键部分。
    trapinithart();  // install kernel trap vector    在当前 hart 上安装内核陷阱向量。
    plicinit();     // set up interrupt controller 初始化 PLIC(可编程中断控制器),这是处理设备中断的关键组件。
    plicinithart();  // ask PLIC for device interrupts在当前 hart 上启用 PLIC。
    binit();      // buffer cache  初始化缓冲区缓存,这是文件系统的一部分,用于提高磁盘 I/O 性能。
    iinit();         // inode table   初始化 inode 表,这是文件系统管理文件和目录的关键数据结构。
    fileinit();      // file table    初始化文件表,这也是文件系统的一部分。
    virtio_disk_init(); // emulated hard disk 初始化虚拟 I/O 磁盘,这是模拟的硬盘设备,用于提供文件系统存储。
    userinit();      // first user process    初始化用户进程,这是创建第一个用户级进程的步骤。
    __sync_synchronize();
    started = 1;    // 确保所有初始化操作都完成后,设置 started 标志为 1,表示系统已经启动。
  } else {  // 其他核执行
    while(started == 0)
      ;
    __sync_synchronize();
    printf("hart %d starting\n", cpuid());
    kvminithart();    // turn on paging
    trapinithart();   // install kernel trap vector
    plicinithart();   // ask PLIC for device interrupts
  }

  scheduler();  // 最后,调用 scheduler() 函数启动操作系统的调度器,这是操作系统多任务处理的核心部分。一旦调度器启动,它将负责在多个进程之间进行上下文切换,确保系统高效运行。
}

main() 的初始化顺序遵循“从底层到高层”、“从基础到应用”的原则。每个步骤都为后续步骤铺路,避免未定义行为。

初始化顺序的逻辑

  1. 基础设施优先:
    • 控制台和打印(consoleinit、printfinit)→ 调试支持。
    • 内存管理(kinit、kvminit、kvminithart)→ 分配和寻址基础。
  2. 核心功能:
    • 进程(procinit)和陷阱(trapinit、trapinithart)→ 运行和管理任务。
    • 中断(plicinit、plicinithart)→ 处理外部事件。
  3. 文件系统和设备:
    • 缓冲区(binit)、inode(iinit)、文件表(fileinit)、磁盘(virtio_disk_init)→ 支持用户态文件操作。
  4. 用户态准备:
    • userinit() → 启动用户进程。

必须先初始化什么?

  • kinit() 必须在任何需要内存分配的步骤之前(如 kvminit、procinit)。
  • kvminit() 和 kvminithart() 必须在访问虚拟地址的步骤之前(如陷阱处理、设备驱动)。
  • trapinit() 和 trapinithart() 必须在可能触发中断的步骤之前(如 plicinit、virtio_disk_init)。
  • 文件系统组件(binit、iinit、fileinit)必须按顺序初始化,因为它们有层次依赖。
  • userinit() 必须最后,因为它依赖所有前置组件。

userinit

// Set up first user process.
void
userinit(void)
{
  struct proc *p;

  p = allocproc();
  initproc = p;
  
  // allocate one user page and copy init's instructions
  // and data into it.
  // 初始化新进程的页表,并为它分配一页用户内存。
  // initcode 是一个包含启动代码的数组,它的大小是一页(PGSIZE)。这页内存将包含init进程的代码和数据。
  uvminit(p->pagetable, initcode, sizeof(initcode));
  p->sz = PGSIZE;

  // prepare for the very first "return" from kernel to user.
  // 设置新进程的初始用户程序计数器(EPC)和用户栈指针(SP)。EPC 设置为 0,这通常是用户程序入口点的地址。
  // SP 设置为一页的大小,这是用户栈的起始地址。
  p->trapframe->epc = 0;      // user program counter
  p->trapframe->sp = PGSIZE;  // user stack pointer

  // 使用 safestrcpy 函数复制字符串 "initcode" 到新进程的名字字段。这个字段用于调试和识别进程。
  safestrcpy(p->name, "initcode", sizeof(p->name));
  // 获取根目录的 inode 并将其赋给新进程的当前工作目录(cwd)
  p->cwd = namei("/");

  p->state = RUNNABLE;

  release(&p->lock);
}

allocproc() 中会通过 p->context.ra = (uint64)forkret; 设置 ra 寄存器,ra 寄存器会存储新创建的进程进行第一个 switch 调用会返回的位置,即 forkret 函数。

// A fork child's very first scheduling by scheduler()
// will swtch to forkret.
void
forkret(void)
{
  static int first = 1;

  // Still holding p->lock from scheduler.
  release(&myproc()->lock);

  if (first) {
    // File system initialization must be run in the context of a
    // regular process (e.g., because it calls sleep), and thus cannot
    // be run from main().
    first = 0;
    fsinit(ROOTDEV);
  }
  // 这是重点!!!从内核空间 -> 用户空间
  usertrapret();
}

执行完 userinit 后,main 函数运行 scheduler 进行第一次调度,调度现在仅有的 RUNNABLE 状态的进程 init

initcode.S

# Initial process that execs /init.
# This code runs in user space.

#include "syscall.h"

# exec(init, argv)
.globl start
start:
        la a0, init
        la a1, argv
        li a7, SYS_exec
        ecall

# for(;;) exit();
exit:
        li a7, SYS_exit
        ecall
        jal exit

# char init[] = "/init\0";
init:
  .string "/init\0"

# char *argv[] = { init, 0 };
.p2align 2
argv:
  .long init
  .long 0

start 部分调用 SYS_exec 系统调用执行 init 函数。

// init.c
// init: The initial user-level program

#include "kernel/types.h"
#include "kernel/stat.h"
#include "kernel/spinlock.h"
#include "kernel/sleeplock.h"
#include "kernel/fs.h"
#include "kernel/file.h"
#include "user/user.h"
#include "kernel/fcntl.h"

char *argv[] = { "sh", 0 };

int
main(void)
{
  int pid, wpid;

  if(open("console", O_RDWR) < 0){
    mknod("console", CONSOLE, 0);
    open("console", O_RDWR);
  }
  dup(0);  // stdout
  dup(0);  // stderr

  for(;;){
    printf("init: starting sh\n");
    pid = fork();
    if(pid < 0){
      printf("init: fork failed\n");
      exit(1);
    }
    if(pid == 0){
      exec("sh", argv);
      printf("init: exec sh failed\n");
      exit(1);
    }

    for(;;){
      // this call to wait() returns if the shell exits,
      // or if a parentless process exits.
      wpid = wait((int *) 0);
      if(wpid == pid){
        // the shell exited; restart it.
        break;
      } else if(wpid < 0){
        printf("init: wait returned an error\n");
        exit(1);
      } else {
        // it was a parentless process; do nothing.
      }
    }
  }
}

init 进程是系统启动后的第一个进程,它的主要任务是启动 shell(通常是一个命令行解释器),并且如果 shell 进程退出,则不断地重启它。这样可以确保系统始终有一个可用的 shell 来接受用户命令。

通过这种机制,init 进程确保了系统中始终有一个可用的 shell,即使用户退出了当前的 shell 会话。这种设计使得 xv6 操作系统具有更好的健壮性和可用性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值