xv6-in-openSBI

最近完成操作系统大赛,需要为 xv6 实现一个 SBI 来完成指定的引导

TODO:理一下 xv6 原本的引导过程

引导过程

OpenSBI

OpenSBI的主要功能就是在M态初始化硬件,然后以S态跳转到内核的位置,完成整个启动过程。此外,OpenSBI作为S态内核的执行环境(EEI),可以以 ecall 的方式为S态内核提供一些只有M态才能实现的功能

每个cpu启动时,都在M态。在将控制权交给操作系统时,转换为 S 态

qemu 自带一个 opensbi

指定 -bios default 这个选项

由于 opensbi 占据了 0x80000000 到 0x80200000 这段区域,因此我们需要把 xv6 原本这段区域(Kernel)迁移到 0x80200000

加入opensbi后的内存布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
PHYSICAL_MEMORY_TOP-> +----------------------------+----
| |
| Free Physical memory |
| |
kernelEnd -----> +----------------------------+----
| |
| Kernel |
| |
0x8020 0000 -----> +----------------------------+----
| |
| Open SBI |
| |
0x8000 0000 -----> +----------------------------+----
| |
| |
| |
+----------------------------+---
| |
| MMIO |
| |
+----------------------------+----
| |
| |
| |
+----------------------------+

为了加入 SBI,需要更改的地方

  1. kernel.ld 里面的内核起始地址
  2. entry.s 里面,
    • 由于 sbi 把 hart id 放在了 a0 寄存器,xv6 是从 tp 寄存器读出 hart id 的,因此这里需要一个 mv 指令
    • 然后在这里要设置好对应核的 stack,每个核开了 4096 byte,需要设置好 sp 寄存器
  3. 然后更改 xv6 的时钟中断实现
    • 因为 xv6 的时钟中断是在 m 态实现的,要实现 sbi 我们一直都会在 S 态,所以利用sbi提供的接口在 s 态实现了时钟中断
    • 更改 trap.cscause 为时钟中断时的动作

多核启动流程

由于 qemu 启动时的启动核不一定是 0,可能是随机的,目前采用的方式是

注意这种启动方式可能启动核不是 0,而是其他,后期可能需要更改

  • 设置了一个共享变量 volatile static int boot_hart = -1;

  • 启动核进入 if 判断,初始化自己,然后更改这个共享变量,利用 sbi 来唤醒其他核

    初始化流程:

    • 更改 boot hart

    • 初始化 printf (采用uart)

    • 打印启动信息

    • 初始化物理内存分配

    • 初始化 kernel page table

    • 开启分页

    • 初始化进程

    • 初始化根文件系统 (在proc.c里面)

    • 初始化 trap 并设置好 kernelvec

    • 初始化中断代理 PLIC

    • 初始化 buffer cache

    • 初始化 File table

    • 初始化 virtio 驱动

    • 开启 init 进程

    • 使用 SBI_HART_START 启动其他核

      初始化根文件系统

      开启 init 进程

  • 其他核进入else,初始化自己,调用那些 *inithart的 函数

    初始化流程

    • 开启分页
    • 设置 kernelvec
    • PLIC 中断代理开启
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
volatile static int boot_hart = -1;
extern void _entry();
void start_harts();

// start() jumps here in supervisor mode on all CPUs.
void
main()
{
if(boot_hart == -1){
boot_hart = cpuid();
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
//started = 1;
__sync_synchronize();
//启动其他核
start_harts();
} 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
}

set_next_trigger();
scheduler();
}

void
start_harts(){
for(int i = 0;i < NCPU; i++ ){
if(sbi_hart_get_status(i) == SBI_HSM_STATE_STOPPED){
sbi_hart_start(i,(uint64)_entry,0);
}
}
}

摘自FarmOs:

虽然说一个核就能完成大部分初始化工作,但因为架构要求,多核还是有一些需要各自初始化的代码。多核环境下需要注意这些事项:

  • 每个核需要设置独立的页表。允许各个核在运行阶段使用不同的页表
  • 每个核有独立的核内时钟,为每个核单独计时
  • 每个核有独立的中断处理向量,我们一般将其设为同一个位置
  • 每个核有独立的外部中断控制器
  • 当一个核与其他核访问同一个变量或同一块内存时,为了缓存能够及时同步,需要加 fence 指令,保证之前的所有读写指令在fence之后都已同步到内存。对应gcc的语法为 __sync_synchronize()

SBI和BIOS和BOOTLOADER的关系

摘自 rCore

SBI 是 RISC-V Supervisor Binary Interface 规范的缩写,OpenSBI 是RISC-V官方用C语言开发的SBI参考实现;RustSBI 是用Rust语言实现的SBI。

BIOS 是 Basic Input/Output System,作用是引导计算机系统的启动以及硬件测试,并向OS提供硬件抽象层。

机器上电之后,会从ROM中读取引导代码,引导整个计算机软硬件系统的启动。而整个启动过程是分为多个阶段的,现行通用的多阶段引导模型为:

ROM -> LOADER -> RUNTIME -> BOOTLOADER -> OS

  • Loader 要干的事情,就是内存初始化,以及加载 Runtime 和 BootLoader 程序。而Loader自己也是一段程序,常见的Loader就包括 BIOS 和 UEFI,后者是前者的继任者。
  • Runtime 固件程序是为了提供运行时服务(runtime services),它是对硬件最基础的抽象,对OS提供服务,当我们要在同一套硬件系统中运行不同的操作系统,或者做硬件级别的虚拟化时,就离不开Runtime服务的支持。SBI就是RISC-V架构的Runtime规范。
  • BootLoader 要干的事情包括文件系统引导、网卡引导、操作系统启动配置项设置、操作系统加载等等。常见的 BootLoader 包括GRUB,U-Boot,LinuxBoot等。

而 BIOS/UEFI 的大多数实现,都是 Loader、Runtime、BootLoader 三合一的,所以不能粗暴的认为 SBI 跟 BIOS/UEFI 有直接的可比性。

如果把BIOS当做一个泛化的术语使用,而不是指某个具体实现的话,那么可以认为 SBI 是 BIOS 的组成部分之一。

也可参考这份文稿《An Introduction to RISC-V Boot Flow》的P5, P7, P9-11。

题外话:
计算机最重要的思想之一就是分层抽象,在任意两层之间,还可以按照设计者的意愿再次添加抽象层。而软件架构的设计和实现,是为了解决现实世界的具体问题,会面临资源、财力、物力、人力、时间等多种因素的掣肘,就会诞生一些“不那么规矩”、“不那么单纯”的架构或组件/软件,它们往往会跨层次,跨模块,大模块拆小,小模块合并,甚至打破一些“金科玉律”等等。

所以,相比于弄懂一个名词,更多的精力应该放在理解事物的本质上,只要把解决问题的流程和方法弄明白了,解决问题的过程中所用到的子流程、工具、方法,你爱怎么叫怎么叫,甚至自己发明名词也可以(只是与外人沟通可能会不太顺畅)。


xv6-in-openSBI
http://example.com/2024/04/15/xv6-in-openSBI/
Author
Jianhui Yin
Posted on
April 15, 2024
Licensed under