1. xv6操作系统启动过程(RISC-V)

828 阅读16分钟

启动过程简介

  1. RISC-V 加电之后,会运行保存在 ROM 中的BootLoader
  2. BootLoader 将 xv6 内核加载到内存物理地址为0x80000000的地方,因为0x0~0x80000000这一段地址将用来操作 I/O 设备
  3. CPU 开始运行 kernel/entry.S 中的 _entry 函数,这个函数主要用于开辟栈空间,以便后续运行C代码
  4. 每一个CPU都会有自己的栈,将栈指针指向stack0 + 4096 * CPU_ID 位置
  5. _entry 函数调用kernel/start.cstart()函数
  6. start 函数配置M-mode一些寄存器
  7. start 函数配置 mepc 寄存器值为 kernel/main.cmain 函数地址,这样 start 函数返回时会执行 main 函数
  8. start 函数关闭分页,初始化时钟中断,最后调用 mret 指令进入到S-mode,并开始执行 main 函数
  9. main 函数初始化所有设备和子系统,初始化地址空间,创建内核页表,分配一个物理内存页给内核栈
  10. main 函数调用 userinit 函数创建第一个用户进程
  11. 第一个用户进程执行的代码就是 user/initcode.S 中代码,其实际上就是调用 exec("\init", 0), 执行init程序
  12. init程序创建文件描述符0,1,2,并开启一个shell窗口
  13. init程序子进程是一个shell,其本身在无限循环处理孤儿进程
  14. 系统成功启动

RISC-V总共有三种模式:

  • M-mode (Machine Mode)
  • S-mode (Supervisor Mode)
  • U-mode (User mode)

在系统加电之后,会处于M-mode

Makefile文件解析

在启动xv6时,我们会执行命令make qemu,我们就从这一条命令开始来解析Makefile,然后讲述xv6操作系统的整个操作过程。

在Makefile文件中找到 qemu 目标:

# 定义 qemu 具体命令
QEMU = qemu-system-riscv64

# ---- 定义 qemu 启动参数 ----
QEMUOPTS = -machine virt -bios none -kernel $K/kernel -m 128M -smp $(CPUS) -nographic
QEMUOPTS += -drive file=fs.img,if=none,format=raw,id=x0
QEMUOPTS += -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

ifeq ($(LAB),net)
QEMUOPTS += -netdev user,id=net0,hostfwd=udp::$(FWDPORT)-:2000 -object filter-dump,id=net0,netdev=net0,file=packets.pcap
QEMUOPTS += -device e1000,netdev=net0,bus=pcie.0
endif
# ---------------------------

# qemu 目标
qemu: $K/kernel fs.img
    $(QEMU) $(QEMUOPTS)

可以看到 qemu 目标依赖于 $K/kernelfs.img 这两个目标

$K/kernel 目标

下面是$K/kernel目标相关makefile代码,主要作用是编译内核文件

K=kernel
U=user

# ---- 定义编译内核文件过程中依赖的文件 ----
OBJS = \
  $K/entry.o \
  $K/kalloc.o \
  $K/string.o \
  $K/main.o \
  $K/vm.o \
  $K/proc.o \
  ...

OBJS_KCSAN = \
  $K/start.o \
  $K/console.o \
  $K/printf.o \
  $K/uart.o \
  $K/spinlock.o
# --------------------------------------

LD = $(TOOLPREFIX)ld
LDFLAGS = -z max-page-size=4096

# 可以看到 $K/kernel 目标还依赖于 $U/initcode 目标
$K/kernel: $(OBJS) $(OBJS_KCSAN) $K/kernel.ld $U/initcode
    # 指定kernel/kernel.ld为链接脚本,将目标文件进行链接,输出可执行文件kernel/kernel
	$(LD) $(LDFLAGS) -T $K/kernel.ld -o $K/kernel $(OBJS) $(OBJS_KCSAN)
	# 将 kernel 可执行文件反汇编出来,并将其保存在kernel/kernel.asm中
	$(OBJDUMP) -S $K/kernel > $K/kernel.asm
	# 将kernel 符号表入口地址打印到 kernel/kernel.sym 中
	$(OBJDUMP) -t $K/kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d' > $K/kernel.sym

sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$$/d'

在sed中d表示删除命令,s表示替换命令,a表示新增操作,c表示取代操作,i表示插入操作 所以:

  • 1,/SYMBOL TABLE/d; 表示删除第一行到出现SYMBOL TABLE字符串第一次出现的行
  • s/ .* / /; 表示将被空格包围的字符串替换成一个空格
  • /^$$/d 表示将一行中只有一个$字符的行删除

objdump -s : 显示指定 section 完整内容,默认所有的非空 section 都会被显示。

objdump -S : 尽可能反汇编出源代码,尤其当编译时指定 -g 这种调试参数时,效果比较明显,隐含 -d 参数。

objdump -t : 显示文件的符号表入口。类似于 nm -s 提供的信息。

objdump -T : 显示文件的动态符号表入口,仅对动态目标文件有意义,比如某些共享库。它显示的信息类似于 nm -D | --dynamic 显示的信息。

kernel.ld

kernel.ld文件是链接脚本,用于设置链接kernel可执行文件时的一些操作。 链接脚本最上方的两条脚本命令 OUTPUT_ARCHENTRY 实际并没有什么作用,也可以删除。ENTRY 的作用是指定ELF header中entry的值,并不能影响 .text 节内函数顺序。之前配置的GUN套件本身是用来适配RISC-V架构的,所以OUTPUT_ARCH默认为riscv。而在链接命令$(LD) $(LDFLAGS) -T $K/kernel.ld -o $K/kernel $(OBJS) $(OBJS_KCSAN),其中的$(OBJS)的第一位为entry.o,所以entry.o包含的符号会在链接合并过程中排在最前面。

OUTPUT_ARCH( "riscv" )
ENTRY( _entry )

下面开始对链接后的section进行设置,比较关键的是.text节的设置,剩下设置都是对文件的数据节进行合并。

SECTIONS
{
  /*
   * ensure that entry.S / _entry is at 0x80000000,
   * where qemu's -kernel jumps.
   */
  # 将定位器置于0x80000000,确保_entry函数在这个地址上
  . = 0x80000000;

  # 将 .text 节起点置于定位器所在地址,也就是0x80000000
  .text : {
    *(.text .text.*)   # 合并所有文件.text节和任意以.text开头的节的内容,地址为当前定位器
    . = ALIGN(0x1000); # 将定位器以0x1000(4096)为base进行地址对齐
    _trampoline = .;   # 将定位器现在位置复制给符号_trampoline,是用户进程陷入内核态的入口
    *(trampsec)     # 合并所有文件trampsec节(实际值定义在trampoline.S),地址为当前定位器
    . = ALIGN(0x1000);
    # 断言,确保trampoline不大于一个页
    ASSERT(. - _trampoline == 0x1000, "error: trampoline larger than one page");
    # 定义一个新符号etext,值为定位器当前位置
    PROVIDE(etext = .);
  }

  ########### 合并所有文件的数据节,包括.rodata.data.bss节 ###########
  .rodata : {
    . = ALIGN(16);
    *(.srodata .srodata.*) /* do not need to distinguish this from .rodata */
    . = ALIGN(16);
    *(.rodata .rodata.*)
  }

  .data : {
    . = ALIGN(16);
    *(.sdata .sdata.*) /* do not need to distinguish this from .data */
    . = ALIGN(16);
    *(.data .data.*)
  }

  .bss : {
    . = ALIGN(16);
    *(.sbss .sbss.*) /* do not need to distinguish this from .bss */
    . = ALIGN(16);
    *(.bss .bss.*)
  }
  #####################################################################

  # 定义一个新符号end,位置为当前定位器位置
  PROVIDE(end = .);
}

在Makefile中我们还看到有定义LDFLAGS = -z max-page-size=4096,这是用于定义合并后的section的最大长度,合并后的section被称为segment,也就是说这里设置一个segment最大长度为4096B,因为xv6载入新进程时会给每一个可以被载入内存的segment分配一个内存页,所以一个segment的长度不可以超过一个内存页大小。

$U/initcode 目标

$U/initcode: $U/initcode.S
    $(CC) $(CFLAGS) -march=rv64g -nostdinc -I. -Ikernel -c $U/initcode.S -o $U/initcode.o
    $(LD) $(LDFLAGS) -N -e start -Ttext 0 -o $U/initcode.out $U/initcode.o
    $(OBJCOPY) -S -O binary $U/initcode.out $U/initcode
    $(OBJDUMP) -S $U/initcode.o > $U/initcode.asm

ld 命令选项作用:

-N : 将 text section 和 data section 设置为可读写。

-e : 显示设置程序入口的符号名

-Ttext 0 : 表示设置.text段的起始地址为0x0

$(LD) $(LDFLAGS) -N -e start -Ttext 0 -o $U/initcode.out $U/initcode.o命令作用就是将编译好的 initcode.o 文件链接成 initcode.out 文件,然后再通过 $(OBJCOPY) -S -O binary $U/initcode.out $U/initcode 命令将 initcode.out 可执行文件的二进制代码复制到 initcode 文件中

编译出来的这个程序并不会实际在内核运行中用到,因为initcode这一段代码已经被硬编码到kernel/proc.c文件中,具体流程后续会介绍。

做完上述这些操作,xv6内核和用户程序就已经编译完成。

fs.img 目标

接下来就会去执行fs.img目标,用于制作磁盘镜像,将上面编译好的用户程序都放置到磁盘镜像中

UEXTRA=
ifeq ($(LAB),util)
    UEXTRA += user/xargstest.sh
endif

fs.img: mkfs/mkfs README $(UEXTRA) $(UPROGS)
    mkfs/mkfs fs.img README $(UEXTRA) $(UPROGS)

可以看到fs.img目标还依赖mkfs/mkfs目标

mkfs/mkfs: mkfs/mkfs.c $K/fs.h $K/param.h
    gcc $(XCFLAGS) -Werror -Wall -I. -o mkfs/mkfs mkfs/mkfs.c

这里将mkfs/mkfs.c文件编译成mkfs/mkfs可执行文件,然后再用这个可执行文件,将之前编译好的用户程序和README等文件制作成文件系统镜像fs.img,作为xv6操作系统启动的文件系统。

启动的具体流程

可以在qemu目标中可以看到,我们执行qemu命令时,会用 -kernel $K/kernel 参数指定内核文件,-file fs.img 参数指定文件系统镜像。qemu启动后,就可以理解为RISC-V机器上电了,它就会运行存储在ROM中的BootLoader程序,BootLoader程序就会去执行我们指定好的 kernel/kernel 内核。

我们在kernel/kernel.ld文件中可以得知kernel文件的入口为 _entry 函数,该函数定义在 kernel/entry.S 文件中。

kernel/entry.S

因为执行C程序需要栈,所以_entry函数中会给每个CPU都初始化一个栈。stack0 是定义在 kernel/start.c 中的一个符号,这里属于外部符号的引用了,在链接时就可以确定stack0的值。下面代码首先将sp指针设置为stack0,作为基址,然后将a0寄存器设置为4096,因为一个页为4KB,这里目的就是后续给每个CPU都分配一个页作为栈。

# kernel/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

mhartid是用于存放硬件线程号的寄存器,也就是存放的是CPU ID,将CPU ID放到a1寄存器中,因为CPU ID是从0开始的,这里要作为后续的乘数,所以需要先用add命令给a1寄存器加1,然后通过mul命令计算出当前CPU栈空间的和stack0的偏移量,用这个偏移量和sp指针相加就得到了当前CPU的栈空间的起始地址。最后调用 call start 指令执行定义在 kernel/start.c 中的start函数。

	csrr a1, mhartid
        addi a1, a1, 1
        mul a0, a0, a1
        add sp, sp, a0
	# jump to start() in start.c
        call start

kernel/start.c

注意:start.c 的start函数代码中可能会涉及到很多RISC-V相关控制寄存器的知识,这部分知识对了解xv6操作系统影响并不大,所以可以根据自己兴趣决定是否去了解,能大致知道代码做了些什么操作既可。

start.c 中定义了一片4KB * CPU个数大小的空间,用于存放每个CPU的栈,stack0 符号在前文_entry 函数中使用到,它要求与16bit对齐。

// kernel/start.c
// entry.S needs one stack per CPU.
__attribute__ ((aligned (16))) char stack0[4096 * NCPU];

下面开始start函数,开始先设置mstatus寄存器,使其在执行完mret指令之后,进入到supervisor mode。关于mstatus寄存器内容可以看:RISC-V特权级寄存器及指令文档_delegation register-CSDN博客

// entry.S jumps here in machine mode on stack0.
void start() {
  // 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);

在xv6中总是以 r_<registername>() 形式函数用于获取指定 <registername> 寄存器的值,例如上述代码中r_mstatus() 就是获取mstatus寄存器值。 在xv6中总是以 w_<registername>(value) 形式函数用于设置指定 <registername> 寄存器值为 value,例如上述代码中 w_mstatus(x) 就是将 mstatus 寄存器值设置为 x 变量存储的值。

设置mepc寄存器为main函数地址,这样执行完mret指令之后就会跳转到main函数

  // set M Exception Program Counter to main, for mret.
  // requires gcc -mcmodel=medany
  w_mepc((uint64)main);

临时禁用分页功能,这样可以直接操作物理地址

  // disable paging for now.
  w_satp(0);

设置和中断异常相关的寄存器medelegmidelegsie到supervisor mode下,关于这些寄存器的标志位的作用具体可以查看RISC-V文档。

  // delegate all interrupts and exceptions to supervisor mode.
  // 设置medeleg寄存器为0xffff,将所有异常(exception)委托给supervisor mode处理
  w_medeleg(0xffff);
  // 设置mideleg寄存器为0xffff,将所有中断(interrupt)委托给supervisor mode处理
  w_mideleg(0xffff);
  // 设置sie寄存器,使supervisor mode开启外部中断、时钟中断和软件中断
  w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

将pmpaddr0寄存器设置为0x3fffffffffffff,使得supervisor mode下可以访问所有物理地址。将pmcfg0寄存器设置为0xf,配置物理内存保护,使得supervisor mode下可以访问所有物理内存。

  // configure Physical Memory Protection to give supervisor mode
  // access to all of physical memory.
  w_pmpaddr0(0x3fffffffffffffull);
  w_pmpcfg0(0xf);

初始化时钟中断,然后通过mhartid寄存器获取当前CPU ID,然后将CPU ID 保存到tp寄存器中。最后执行mret指令,该指令会返回到之前设置的mepc寄存器指向位置,也就是main函数。最后mret返回时,根据之前设置的一些相关寄存器的状态,机器模式会切换到supervisor mode下。

  // ask for clock interrupts.
  // 初始化时钟中断
  timerinit();
  
  // keep each CPU's hartid in its tp register, for cpuid().
  int id = r_mhartid();
  w_tp(id);
  
  // switch to supervisor mode and jump to main().
  asm volatile("mret");
}

kernel/main.c

main函数定义在 kernel/main.c 中,进行内核的一些初始化操作。

在第一个CPU执行main函数时,会进行一系列的初始化操作。关于初始化操作的详细解析会在后面文章逐一解析。

// kernel/main.c
// 作为CPU0是否初始化完成的标志
volatile static int started = 0;

// start() jumps here in supervisor mode on all CPUs.
void main() {
  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
    // 创建进程表
    procinit();      // process table
    // 设置系统中断向量
    trapinit();      // trap vectors
    trapinithart();  // install kernel trap vector
    // 系统中断初始化
    plicinit();      // set up interrupt controller
    // 设备中断初始化
    plicinithart();  // ask PLIC for device interrupts
    // 磁盘缓冲初始化
    binit();         // buffer cache
    // 磁盘节点inode初始化
    iinit();         // inode table
    // 文件系统初始化
    fileinit();      // file table
    // 磁盘初始化
    virtio_disk_init(); // emulated hard disk
    // 创建第一个用户进程
    userinit();      // first user process
    // gcc提供的原子操作,保证内存访问的操作都是原子操作
    __sync_synchronize();
    // 表明CPU0已经完成了系统初始化
    started = 1;
  } 

如果不是CPU0,则会循环等待CPU0进行系统初始化完成之后,才会进行下一步操作。

  else {
    // 循环等待CPU0初始化完成
    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();      
}

userinit函数

main 函数中调用的 userinit 函数用于启动内核后运行的第一个用户程序。

userinit 函数前有一个 initcode 数组,放置了第一个用户程序的二进制代码,这第一个用户程序执行的就是这段二进制代码。

// kernel/proc.c
// a user program that calls exec("/init")
// od -t xC initcode
uchar initcode[] = {
  0x17, 0x05, 0x00, 0x00, 0x13, 0x05, 0x45, 0x02,
  0x97, 0x05, 0x00, 0x00, 0x93, 0x85, 0x35, 0x02,
  0x93, 0x08, 0x70, 0x00, 0x73, 0x00, 0x00, 0x00,
  0x93, 0x08, 0x20, 0x00, 0x73, 0x00, 0x00, 0x00,
  0xef, 0xf0, 0x9f, 0xff, 0x2f, 0x69, 0x6e, 0x69,
  0x74, 0x00, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00,
  0x00, 0x00, 0x00, 0x00
};

这段二进制代码实际上对应的就是user/initcode.S中的代码,可以通过od -t xC user/initcode 指令验证。

Pasted image 20231204222713.png

od -t xC 指令的作用是将文件内容用十六进制形式展示 user/initcode 就是 user/initcode.S 编译后的文件,可以看makefile中 $U/initcode 目标得知

user/initcode.S 代码如下,start为其中第一个符号,编译后在二进制最前面。可以看出,start函数实际上执行的就是exec("/init", 0),也就是运行/init程序。

# user/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        # 将init符号地址存入到a0寄存器中
        la a1, argv        # 将argv符号地址存入到a1寄存器中
        li a7, SYS_exec    # 将exec系统调用号存入到a7寄存器中,具体可以看系统调用章节
        ecall              # 发起系统调用

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

# char init[] = "/init\0";
# 定义init符号,保存"/init\0"字符串
init:
  .string "/init\0"

# char *argv[] = { init, 0 };
# 定义argv符号,保存的是一个long数组,第一个元素存init符号地址,第二个元素为0
.p2align 2
argv:
  .long init
  .long 0

下面是userinit函数代码,首先会通过allocproc 函数为第一个用户进程分配进程结构体proc,然后将initproc变量指向这个第一个用户进程的进程结构体。

// kernel/proc.c
// Set up first user process.
void userinit(void) {
  struct proc *p;
  p = allocproc();
  initproc = p;

通过uvminit函数给进程分配一个页,且将initcode放置到这个页中,设置当前进程所占内存大小为PGSIZE。

  // allocate one user page and copy init's instructions
  // and data into it.
  uvminit(p->pagetable, initcode, sizeof(initcode));
  p->sz = PGSIZE;

uvminit函数其实只服务于第一个进程的创建,主要是将initcode代码放到虚拟内存地址为 0x0 的位置上面。uvminit函数在kernel/vm.c文件中。

// kernel/vm.c
// Load the user initcode into address 0 of pagetable,
// for the very first process.
// sz must be less than a page.
void uvminit(pagetable_t pagetable, uchar *src, uint sz) {
  char *mem;

  if(sz >= PGSIZE)
    panic("inituvm: more than a page");

  // 申请一个物理内存页,并初始化内存页
  mem = kalloc();
  memset(mem, 0, PGSIZE);
  // 将这个内存页进行虚拟地址映射,将其映射到进程虚拟地址0x0位置上
  mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
  // 将传入的二进制代码复制到刚刚申请的内存中去,这样访问地址0x0,就可以访问到src指向的代码
  memmove(mem, src, sz);
}

回到userinit函数,设置进程trapframe中保存的epc值为0,这样在返回用户空间时,pc寄存器就会恢复为epc保存的值,程序就会从地址0x0开始执行,也就是开始执行initcode中代码。将进程trapframe中保存的sp值为PGSIZE,也就是刚刚申请的内存页的顶端,sp是栈指针,因为栈是高地址向低地址扩展的,所以这里要将其设置为最顶部地址。

  // kernel/proc.c
  // prepare for the very first "return" from kernel to user.
  p->trapframe->epc = 0;      // user program counter
  p->trapframe->sp = PGSIZE;  // user stack pointer

将进程名称设置为initcode,将进程所在目录设置为/,将进程状态设置为RUNNABLE。当进程状态设置为RUNNABLE之后,main函数最后调用scheduler函数的时候,就会运行进程状态为RUNNABLE的进程,此时第一个用户进程initcode就开始运行了。从initcode.S代码中我们得知,第一个用户程序执行的是/init程序,其对应的是user/init.c中代码。

  safestrcpy(p->name, "initcode", sizeof(p->name));
  p->cwd = namei("/");
  p->state = RUNNABLE;
  
  // 调用allocproc函数时会加锁,所以最后需要释放锁
  release(&p->lock);
}

user/init.c

init.c主要fork了一个子进程,然后打开shell窗口,方便用户进行交互操作。其主要做的就是fork();exec("sh", 0);

首先定义传给系统调用的参数,然后通过open函数打开console控制台,其文件描述符为0,作为stdin,然后通过dup函数将console控制台对应stdout和stderr,其文件描述符分别为 1 和 2。

// user/init.c
// init: The initial user-level program
char *argv[] = { "sh", 0 };

int main(void) {
  int pid, wpid;
  // 用可读可写方式打开控制台,此时是第一个打开文件,所以其文件描述符为0,作为stdin
  if(open("console", O_RDWR) < 0){
    mknod("console", CONSOLE, 0);
    open("console", O_RDWR);
  }
  dup(0);  // stdout
  dup(0);  // stderr

fork一个子进程,然后使用exec执行shell程序。无限循环作用是用于保证前台一直会有shell开启,即使一个shell退出,也会立刻启动一个新的shell。第二层无限循环是由init进程执行,主要目的是用于不断回收孤儿进程的资源。

  // 该层for用于保证前台一直有shell在运行
  for(;;){
    printf("init: starting sh\n");
    pid = fork();
    if(pid < 0){
      printf("init: fork failed\n");
      exit(1);
    }

    if(pid == 0){
      exec("sh", argv);
      // 如果上面exec执行成功,则子进程后面语句都不会执行
      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);
	  // wpid和pid(shell进程id)相等,证明shell进程退出,应跳出此层循环,重启shell
      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.
        // 孤儿进程资源已经被wait函数释放,所以这里无需做任何操作
      }
    }
  }
}

这里 init 程序可以回收孤儿进程资源是因为在程序退出时,会将其所有子进程的父进程通过reparent函数设置为 init 进程,可以看kernel/proc.cexit函数代码,所以init 进程中调用 wait 函数可以回收这些孤儿进程资源。

自此,xv6系统启动到我们看到shell界面的所有过程的解析就结束了。执行make qemu之后,控制台界面如下

Pasted image 20231204233701.png

xv6 kernel is booting 这句打印在kernel/main.c的main函数中执行;hart %d starting 这句打印也在kernel/main.c的main函数中执行;init: starting sh 这句打印在user/init.c的main函数中执行。

参考链接

  1. xv6 系统启动代码分析(MIT 6.S081 FALL 2020)_#define entry_start 0x80000000_菜籽爱编程的博客-CSDN博客
  2. RISC-V特权级寄存器及指令文档_delegation register-CSDN博客
  3. 【学习Xv6】 内核概览 - leenjewel Blog
  4. xv6 内核启动 - 知乎 (zhihu.com)
  5. MIT6.S081操作系统实验——操作系统是如何在qemu虚拟机中启动的? - 知乎 (zhihu.com)
  6. Start xv6 and the first process - build a OS (gitbook.io)