一直有系统学习操作系统的打算,看了很多资料,像什么 pintos,xv6,还有一些杂七杂八的中文资料,也看了许多人对不同资料的评价和好坏判断。

个人感觉,对新手来说,最重要的还是找一个经典的、有名的、广泛认可的资料认真研读、读完,而不是纠结自己应该学哪个资料。

我们先来速通 MIT6.S081 吧。


任务1:启动 xv6 (完成)

首先是 xv6 实验官网:https://pdos.csail.mit.edu/6.S081/2023/tools.html (2023)

我们使用 WSL-ubuntu20.04 做实验。

运行如下命令安装大量依赖:

sudo apt-get install git build-essential gdb-multiarch qemu-system-misc gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu 

运行下面命令看关键程序是否安装成功:

qemu-system-riscv64 --version
riscv64-linux-gnu-gcc --version

OK,现在让我们来到第一个 Lab : Utilities

git clone git://g.csail.mit.edu/xv6-labs-2023
cd xv6-labs-2023
make qemu

如下图,成功启动 xv6
在这里插入图片描述

接着使用 Ctrl + A +x 即可退出


任务2:了解 xv6 代码结构 之 搞清楚启动命令 (完成)

接下来让我们拆开 xv6,看看里面的代码是怎么写的

先看 Makefile,找到 qemu target,如下:

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

它实际上是:

qemu: kernel/kernel fs.img
	qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

K := kernel
QEMU := qemu-system-riscv64
QEMUOPTS := -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

尝试运行这个命令:

qemu-system-riscv64 -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

在这里插入图片描述

成功启动 xv6,说明最终启动 xv6 的就是这个命令,我们现在来了解这个命令的每一个项

根据 CHATGPT,如下:

qemu-system-riscv64:指定 QEMU 模拟的目标架构,这里是 RISC-V 64 位架构。

-machine virt:指定使用的机器类型,这里是 "virt",适用于虚拟化的通用平台。

-bios none:不加载任何 BIOS,通常用于直接使用内核启动,而不是通过 BIOS 启动。

-kernel kernel/kernel:指定要加载的内核映像文件路径,这里是 kernel/kernel。

-m 128M:设置虚拟机的内存大小,这里是 128MB。

-smp 3:设置虚拟机的 CPU 核心数,这里是 3 个核心。

-nographic:在没有图形界面的情况下运行虚拟机,所有输入输出通过终端进行。

-global virtio-mmio.force-legacy=false:设置 virtio-mmio 设备的全局参数,强制不使用传统模式,通常用于启用新特性。

-drive file=fs.img,if=none,format=raw,id=x0:定义一个虚拟磁盘驱动器,其中 file=fs.img 是映像文件名,if=none 表示不与任何具体接口关联,format=raw 指定映像文件格式为原始格式,id=x0 是该驱动器的标识。

-device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0:将一个 virtio 块设备添加到虚拟机,drive=x0 指定了之前定义的虚拟磁盘驱动器,bus=virtio-mmio-bus.0 指定了设备连接的总线。

这里除了 qemu,还使用了两个文件:

  • kernel 内核文件
  • fs.img 文件系统镜像 (文件系统镜像 fs.img 的生成可见 【(done) 梳理 xv6-lab-2023 fs.img 生成过程,以及 xv6 磁盘结构 mkfs.c】)

任务3:了解 xv6 代码结构 之 内核文件如何生成 (完成)

在 Makefile:122 可以看到 $K/kernel 的生成依赖和命令

kernel/kernel: kernel/entry.o kernel/kalloc.o kernel/string.o kernel/main.o kernel/vm.o kernel/proc.o kernel/swtch.o kernel/trampoline.o kernel/trap.o kernel/syscall.o kernel/sysproc.o kernel/bio.o kernel/fs.o kernel/log.o kernel/sleeplock.o kernel/file.o kernel/pipe.o kernel/exec.o kernel/sysfile.o kernel/kernelvec.o kernel/plic.o kernel/virtio_disk.o kernel/start.o kernel/console.o kernel/printf.o kernel/uart.o kernel/spinlock.o kernel/kernel.ld user/initcode
	riscv64-linux-gnu-ld -z max-page-size=4096 -T kernel/kernel.ld -o kernel/kernel kernel/entry.o kernel/kalloc.o kernel/string.o kernel/main.o kernel/vm.o kernel/proc.o kernel/swtch.o kernel/trampoline.o kernel/trap.o kernel/syscall.o kernel/sysproc.o kernel/bio.o kernel/fs.o kernel/log.o kernel/sleeplock.o kernel/file.o kernel/pipe.o kernel/exec.o kernel/sysfile.o kernel/kernelvec.o kernel/plic.o kernel/virtio_disk.o kernel/start.o kernel/console.o kernel/printf.o kernel/uart.o kernel/spinlock.o
	riscv64-linux-gnu-objdump -S kernel/kernel > kernel/kernel.asm
	riscv64-linux-gnu-objdump -t kernel/kernel | sed '1,/SYMBOL TABLE/d; s/ .* / /; /^$/d' > kernel/kernel.sym

可以看到 kernel 的生成依赖很多 .o 文件,然后第一个命令是把这些 .o 文件给链接起来变成一个大的 binary。剩下两个 objdump 命令是用来生成汇编命令以及符号表,帮助调试的。

后续两个命令不是那么重要,我们来深入看看第一个命令

$(LD) $(LDFLAGS) -T $K/kernel.ld -o $K/kernel $(OBJS) $(OBJS_KCSAN)

其中
LD := riscv64-linux-gnu-ld
LDFLAGS := -z max-page-size=4096
OBJS := kernel/entry.o kernel/kalloc.o kernel/string.o kernel/main.o kernel/vm.o kernel/proc.o kernel/swtch.o kernel/trampoline.o kernel/trap.o kernel/syscall.o kernel/sysproc.o kernel/bio.o kernel/fs.o kernel/log.o kernel/sleeplock.o kernel/file.o kernel/pipe.o kernel/exec.o kernel/sysfile.o kernel/kernelvec.o kernel/plic.o kernel/virtio_disk.o
OBJS_KCSAN := kernel/start.o kernel/console.o kernel/printf.o kernel/uart.o kernel/spinlock.o

这个命令翻译成人话就是: riscv64-linux-gnu-ld,设置最大内存页大小为 4096 字节,随后根据 kernel/kernel.ld 链接脚本的指示,把一堆 .o 文件链接成一个大的 binary


任务4:找到 xv6 内核文件的程序入点(完成)

既然这是一个大的 binary,那么肯定有程序入点,也许是 main 函数,也许是链接脚本里定义的一个地址。

最靠谱的方式是,先找到 qemu-system-riscv64 的上电地址,再来找程序入点

使用 gdb-qemu 调试内核的方式如下,启动 qemu 的时候加上 -s -S 选项:

qemu-system-riscv64 -s -S -machine virt -bios none -kernel kernel/kernel -m 128M -smp 3 -nographic -global virtio-mmio.force-legacy=false -drive file=fs.img,if=none,format=raw,id=x0 -device virtio-blk-device,drive=x0,bus=virtio-mmio-bus.0

随后换一个窗口,运行

gdb-multiarch kernel/kernel  (这一步主要是为了读取 kernel 的符号表)
target remote localhost:1234

可以看到 qemu-system-riscv64 的上电位置在 0x1000,如下图
在这里插入图片描述

运行 continue 命令即可运行 xv6

0x1000 地址的指令应该是 qemu-system-riscv64 的内置 BIOS/bootloader,它的作用是把 kernel 加载到位置 0x80000000,然后把执行流交给 0x80000000 的指令。

为了确认 0x80000000 是内核入点,这里我们可以看看 kernel/kernel.ld 链接脚本,如下

/* 输出的目标架构是 riscv */
OUTPUT_ARCH( "riscv" )
/* 程序入口点为 _entry */
ENTRY( _entry )

/* 下面的部分定义了各个段(section)在内存中的布局 */
SECTIONS
{
  /*
   * ensure that entry.S / _entry is at 0x80000000,
   * where qemu's -kernel jumps.
   */
  /* 将入口点 _entry 设置在地址 0x80000000,这是 QEMU 模拟器启动内核时的跳转地址。 */
  . = 0x80000000;

  .text : {
    *(.text .text.*)
    . = ALIGN(0x1000);
    _trampoline = .;
    *(trampsec)
    . = ALIGN(0x1000);
    ASSERT(. - _trampoline == 0x1000, "error: trampoline larger than one page");
    PROVIDE(etext = .);
  }

  .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.*)
  }

  PROVIDE(end = .);
}

可见,在链接内核文件的时候,就已经把程序入口点设为 _entry,同时把内存地址设定为 0x80000000

在 kernel/entry.S 可以找到 _entry 的定义

        # qemu -kernel loads the kernel at 0x80000000
        # and causes each hart (i.e. 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

我们进入 gdb 里的汇编指令确认一下
使用 b *0x80000000 在这个内存地址打断点
随后用 layout asm 打开汇编窗口
在这里插入图片描述

对比汇编指令,可以看到汇编指令并不完全一致,这是因为 riscv64 中有伪指令。
大体上是一致的,说明 kernel/entry.S 的 _entry 确实是内核程序入点。


任务5:搞清楚 xv6 内核怎么从汇编跳转到 C (完成)

先来看一下 qemu 刚刚跳转到 0x8000_0000 时的寄存器状态,如下:

ra             0x0      0x0
sp             0x0      0x0
gp             0x0      0x0
tp             0x0      0x0
t0             0x80000000       2147483648
t1             0x0      0
t2             0x0      0
fp             0x0      0x0
s1             0x0      0
a0             0x1      1
a1             0x1020   4128
a2             0x0      0
a3             0x0      0
a4             0x0      0
a5             0x0      0
a6             0x0      0
a7             0x0      0
s2             0x0      0
s3             0x0      0
s4             0x0      0
s5             0x0      0
s6             0x0      0
s7             0x0      0
s8             0x0      0
s9             0x0      0
s10            0x0      0
s11            0x0      0
t3             0x0      0
t4             0x0      0
t5             0x0      0
t6             0x0      0
pc             0x80000000       0x80000000 <_entry>

可以看到除了部分寄存器,大部分寄存器都是 0x0。(部分寄存器应该是 qemu 内置 BIOS 设置的)

再来看 kernel/entry.S

        # qemu -kernel loads the kernel at 0x80000000
        # and causes each hart (i.e. 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

从注释来看,qemu 的每个核心都跳转到了 _entry,看来此时有多个 core 在执行同一块代码
_entry 在跳转到 C 代码之前,给每一个 core 分配了一个栈。

这里有个疑问:为什么要分配这些栈?这些栈有什么用?
回答:设置栈指针,实际上是为了支持函数的调用和返回。在古早时代,人们为了在函数之间传递参数、从函数返回会使用各种各样的方法。最后大浪淘沙被认为最有效的方式就是 “设置栈指针,把参数压入栈和部分寄存器中”。今天的 gcc/clang 编译器也会在编译 C 语言的时候,在函数开头进行压栈,在函数返回时弹栈。为了让我们能顺利地从汇编语言跳转到 C 语言(或者说,由 gcc 编译出来的汇编代码),我们需要在进入 C 语言之前设置栈指针

汇编中涉及到的 stack0,在 kernel/start.c 中定义

__attribute__ ((aligned (16))) char stack0[4096 * NCPU];

可以看到是一个对 16 字节对齐的数组(或者说一段内存),大小为 4096 * NCPU, NCPU=8,是 xv6 支持的最大 core 数量。栈跟16字节对齐很可能是为了去适配 128 bits 长度的寄存器。

另外,entry.S 中使用了一个特殊寄存器 mhartid,根据 CHATGPT:

在 RISC-V 架构中,mhartid 指的是机器级别的 hart(硬件线程)标识符。以下是一些详细信息:

Hart:在 RISC-V 术语中,hart 是一个执行的硬件线程,类似于多核处理器中的逻辑核心。每个 hart 可以独立执行指令。

mhartid 寄存器:mhartid 是一个特殊的寄存器,用于存储当前 hart 的 ID。这个 ID 通常是一个非负整数,唯一标识系统中的每个 hart。例如,如果一个系统有四个 harts,它们的 mhartid 值通常为 0、1、2 和 3。

用途:mhartid 寄存器可以被操作系统和应用程序用来确定当前 hart 的身份。这在负载均衡、线程管理和其他多线程或多核功能中非常有用。

可见,mhartid 是用来识别 core 的编号的。

于是,整个 _entry 的代码逻辑就是:
1.给所有的 core 的栈指针设置为 stack0
2.a0 = 4096,也就是每个 core 能拥有的栈大小
3.a1 = mhartid 获取 core 编号
4.a1 += 1,a1 = 自己的 core 编号+1
5.a0 = a1 * a0 = (core编号+1) * (栈大小 4096字节)
6.sp = sp + a0,也就是 (stack0 起始地址 + (core编号+1) * (栈大小 4096字节))
7.跳转进入 C 语言中的 start 函数

总结一下,起始就是通过各个 cores 的编号,给各个 cores 分配它们的栈空间。这里给 sp 分配的是栈顶,因为 riscv64 中的栈通常是从上往下增长的。

这里有个疑问:为什么栈从上往下增长
回答:根据谷歌,部分架构栈从下往上增长,所以这大概率是历史原因

分配完栈后,就可以跳转到 C 语言中,和 gcc 编译出的汇编代码协作了。


任务6:搞明白 xv6 进入 main 函数之前做了什么 (完成)

先看 start.c 的 start 函数
代码如下:

// entry.S jumps here in machine mode on stack0.
void
start()
{
  // set M Previous Privilege mode to Supervisor, for mret.
  // 读取 mstatus 寄存器,把 MPP 那两个 bits 清零,再设置为 S mode
  // 当 mret 发生时,机器权限会被设置成 MPP 设置的权限
  unsigned long x = r_mstatus();
  x &= ~MSTATUS_MPP_MASK;
  x |= MSTATUS_MPP_S;
  w_mstatus(x);

  // 把 mepc 的值设置为 main 函数的地址
  // mepc: 当低权级的代码遇到 exception 时,会把当前 PC 写入 mepc
  // 在 machine mode 处理完异常后,再调用 mret 返回 mepc 指向的地址
  // set M Exception Program Counter to main, for mret.
  // requires gcc -mcmodel=medany
  w_mepc((uint64)main);

  // disable paging for now.
  // 关闭 supervisor mode 的页表翻译
  w_satp(0);

  // delegate all interrupts and exceptions to supervisor mode.
  // 根据手册,默认情况下,机器所有的 trap 都在 M mode 完成。当然 M mode 也可以
  // 通过 mret 来把 handler 转到 S mode。但为了提高效率,通过设置 medeleg or mideleg 
  // 中的任意一个比特,就可以把 S mode 和 U mode 的 trap 转移到 S mode 去处理
  w_medeleg(0xffff);
  w_mideleg(0xffff);
  // 开启 supervisor 级别的外部中断、时钟中断、软件中断
  w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);

  // 这两行函数调用:把 0x0000_0000 ~ 0xFFFF_FFFF 内存段都设置成可读可写可执行
  // configure Physical Memory Protection to give supervisor mode
  // access to all of physical memory.
  // pmpaddr0 在 64 位下,bits(63, 54) 是 WIRI 置空
  w_pmpaddr0(0x3fffffffffffffull);
  // 对应地址区域可读、可写、可执行、顶部匹配模式
  // If TOR is selected, the associated address register forms the top of the address range, and the
  // preceding PMP address register forms the bottom of the address range.
  w_pmpcfg0(0xf);

  // 设置时钟中断 (TODO: 为什么时钟中断不会被分派给 supervisor mode)
  // ask for clock interrupts.
  timerinit();

  // 把 hartid 存入 tp 寄存器,thread pointer
  // keep each CPU's hartid in its tp register, for cpuid().
  int id = r_mhartid();
  w_tp(id);

  // 调用 mret,进入 supervisor mode,同时进入 main 函数
  // switch to supervisor mode and jump to main().
  asm volatile("mret");
}

这里我花了点时间研究怎么查看机器当前的特权级别,但手册里没有提到这种寄存器。估计只能通过执行特权指令看结果,来判断自己位于哪个特权级。事实上,这让做一个提高安全性能的 work 提供了帮助,比如我可以在 machine mode 设置异常向量表以及一些异常处理程序,欺骗 S mode 说它是 M mode。

从 start 函数来看,主要做的工作有:
1.设置 mret 时应该到达的权限等级为 supervisor mode
2.设置 mret 时应该跳转到的代码地址为 main
3.关闭 supervisor mode 的页表翻译
4.把中断分发到 supervisor mode
5.开启 supervisor mode 的外部中断、时钟中断、软件中断
6.设置整块内存可读可写可执行
7.初始化时钟中断
8.读取 mhartid
9.执行 mret

再来仔细看看时钟中断初始化代码

void
timerinit()
{
  // each CPU has a separate source of timer interrupts.
  // 读取 mhartid
  int id = r_mhartid();

  // ask the CLINT for a timer interrupt.
  int interval = 1000000; // cycles; about 1/10th second in qemu.
  *(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;

  // prepare information in scratch[] for timervec.
  // scratch[0..2] : space for timervec to save registers.
  // scratch[3] : address of CLINT MTIMECMP register.
  // scratch[4] : desired interval (in cycles) between timer interrupts.
  uint64 *scratch = &timer_scratch[id][0];
  scratch[3] = CLINT_MTIMECMP(id);
  scratch[4] = interval;
  // mscratch 寄存器用于机器模式下的程序临时保存某些数据。
  w_mscratch((uint64)scratch);

  // set the machine-mode trap handler.
  // mtvec = timervec,意思是,发生 machine mode 中断时,会跳转到 timervec
  w_mtvec((uint64)timervec);

  // enable machine-mode interrupts.
  // 开启机器模式下中断
  w_mstatus(r_mstatus() | MSTATUS_MIE);

  // enable machine-mode timer interrupts.
  // 开启机器模式下的时钟中断
  w_mie(r_mie() | MIE_MTIE);
}

从这里来看,是设置了时钟中断,跳转到 timervec 代码地址,从这里看,这个中断应该是由 machine mode 处理。我们可以验证一下。

我们先在 main 函数写一个死循环。

随后执行 make qemu-gdb 随后使用 gdb 相连,对 timervec 打断点

到达后,执行 info all-registers,即可看到此时权限级别在 machine mode。

注意:进行汇编级调试的时候,要使用 si 和 ni

TODO: 看起来这个中断并没有被分派给 superivisor mode。也许 machine mode 的时钟和 supervisor mode 的时钟是不同的?

再来看看 timervec,如下

.globl timervec
.align 4
timervec:
        # start.c has set up the memory that mscratch points to:
        # scratch[0,8,16] : register save area.
        # scratch[24] : address of CLINT's MTIMECMP register.
        # scratch[32] : desired interval between interrupts.

        # 把 a0 的值读入 mscratch, 把 mscratch 的值读入 a0
        csrrw a0, mscratch, a0
        sd a1, 0(a0)
        sd a2, 8(a0)
        sd a3, 16(a0)

        # 设置下一个时钟中断的时间
        # schedule the next timer interrupt
        # by adding interval to mtimecmp.
        ld a1, 24(a0) # CLINT_MTIMECMP(hart)
        ld a2, 32(a0) # interval
        ld a3, 0(a1)
        add a3, a3, a2
        sd a3, 0(a1)

        # arrange for a supervisor software interrupt
        # after this handler returns.
        # An interrupt i will trap to S-mode if both of the following are true: (a) either the current privilege mode
        # is S and the SIE bit in the sstatus register is set, or the current privilege mode has less privilege than
        # S-mode; and (b) bit i is set in both sip and sie.
        # 这里的 2 是 10,也就是 supervisor software interrupt
        li a1, 2
        csrw sip, a1

        ld a3, 16(a0)
        ld a2, 8(a0)
        ld a1, 0(a0)
        # 把 a0 的值读入 mscratch, 把 mscratch 的值读入 a0
        # TODO: 必须要这么做,supervisor software interrupt 才会被 trap to S mode 吗?
        csrrw a0, mscratch, a0

        # 从 machine mode 返回
        mret

TODO: 从这里来看,是设置了 supervisor mode 的 supervisor software interrupt,还有下一个 interval 的时钟中断,暂时不明白这是想干嘛。

不过,总而言之,执行 mret 后确实是会跳转到 main 函数是没错的。


任务6:观察 main 函数结构 (完成)

这里可以观察一下 main 函数代码,如下:

// 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
    iinit();         // inode table
    fileinit();      // file table
    virtio_disk_init(); // emulated hard disk
    userinit();      // first user process
    // 在调用这个函数之前的所有读写操作在这个函数之后的读写操作之前完成。
    // 这意味着,所有在这个屏障之前的内存操作都不会被重排到这个屏障之后。
    __sync_synchronize();
    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();        
}

可以看到 main 函数做了很多事情。
main 函数做的事情基本也就是 xv6 内核会做的事情。
1.初始化 console
2.初始化 printf
3.初始化页表
4.初始化进程控制
5.初始化中断
6.初始化磁盘 buffer
7.初始化文件系统 inode
8.初始化文件
9.启动第一个用户程序
10.进入调度

调度 scheduler() 的代码如下:

void
scheduler(void)
{
  struct proc *p;
  struct cpu *c = mycpu();

  c->proc = 0;
  for(;;){
    // The most recent process to run may have had interrupts
    // turned off; enable them to avoid a deadlock if all
    // processes are waiting.
    intr_on();

    for(p = proc; p < &proc[NPROC]; p++) {
      acquire(&p->lock);
      if(p->state == RUNNABLE) {
        // Switch to chosen process.  It is the process's job
        // to release its lock and then reacquire it
        // before jumping back to us.
        p->state = RUNNING;
        c->proc = p;
        swtch(&c->context, &p->context);

        // Process is done running for now.
        // It should have changed its p->state before coming back.
        c->proc = 0;
      }
      release(&p->lock);
    }
  }
}

从代码上来看,基本上就是在一个数组里挑选进程,然后通过上下文切换执行进程。

xv6 后续的其它代码我们可以一边做 xv6 的实验一边熟悉,今天就先看到这里吧。


Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐