本文基于ArceOS简化版的代码(依靠riscv64_qemu架构),介绍一个简单的组件化的Unikernel的启动流程。
仓库链接:https://github.com/arceos-org/oscamp.git
Unikernel
应用与内核:
- 处于相同的特权级-内核态
- 共享同一个地址空间
- 编译形成一个Image,一体运行
Unikernel可以看作是操作系统内核的基础,基于此,可以发展出更常见的宏内核
组件化
将内核按功能划分为多个模块,最基本内核只包含基础模块,其余模块按需配置:
启动流程
BIOS启动
硬件会把BIOS固件程序放到物理地址为0x0的地方,并开始执行,对硬件进行一些初始化,然后加载BootLoader(操作系统引导程序的加载者)到0x8000_0000物理地址,并开始执行。此处用的是OpenSBI v1.0 。
其他架构会有其他的BootLoader。
SBI启动
SBI即为 (RISC-V Supervisor Binary Interface),SBI直接运行在系统M模式,可以作为一个bootloader也可以是一个M模式下运行的后台程序,SBI程序拥有最高的权限,可以访问所有的硬件资源,向S-MODE的OS提供了统一的硬件功能调用接口(在此处,就是给axhal提供一些打印、时钟等的支持)。
在本处代码中,SBI作为bootloader时,最后加载对应的内核执行文件到起始物理地址为0x8020_0000的内存中。
内核执行文件链接时有如下链接脚本文件指导,修改默认的链接操作:
- 修改入口为_start,默认可能为main
- 添加了一些地址符号(在生成的elf的符号表中),如_skernel等,后续内核程序可以通过该符号得知一些特定的地址值。
- 为起始的一些数据结构分配对应的物理内存(通过对齐的方式预留),如boot_page_table和boot_stack
- 指定各个具体段的布局,如把.text.boot(_start所在地)放在.text段的最前面
(rust编程中可以通过#[link_section = ".data.boot_page_table"]
的方式指定某个item所属的段名称) - 指定输出架构,应该是会根据架构的不同选择之后axhal中不同的内核程序(axhal提供了各种架构,也就是各种BootLoader的适配)
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# oscamp/arceos/modules/axhal/linker.lds.S
OUTPUT_ARCH(%ARCH%)
BASE_ADDRESS = %KERNEL_BASE%;
ENTRY(_start)
SECTIONS
{
. = BASE_ADDRESS;
_skernel = .;
.text : ALIGN(4K) {
_stext = .;
*(.text.boot)
*(.text .text.*)
. = ALIGN(4K);
_etext = .;
}
.rodata : ALIGN(4K) {
_srodata = .;
*(.rodata .rodata.*)
*(.srodata .srodata.*)
*(.sdata2 .sdata2.*)
. = ALIGN(4K);
_erodata = .;
}
.data : ALIGN(4K) {
_sdata = .;
*(.data.boot_page_table)
. = ALIGN(4K);
*(.data .data.*)
*(.sdata .sdata.*)
*(.got .got.*)
}
...
}
执行Boot
作为操作系统的初始化程序,一些与架构有关的操作要在Boot中完成,如MMU的页表格式:
保存参数
建立栈
栈空间已经分配完成(在链接时),把栈顶指针sp设置完成,就算是建立了栈,注意,此处的栈是高地址往低地址增长。函数的调用依赖于栈的有效设置,所以为了尽早可以正常使用函数,而不是像_start这样自己写汇编,就要尽早建立栈。建立页表并启用MMU(内存管理单元)
页表空间也已分配完成,此处先对一个G的物理空间形成了恒等映射和“偏移映射”,偏移映射是给应用和全局内存分配器用的,恒等映射应该是给一些更底层的模块用的(保留一个直接访问物理内存的形式)。这个页表非常的粗粒度,而且根据下面的实际所有物理段的显示,此处显然只形成了非mmio的映射,所以此时并不支持一些外部设备。
值得注意的是,这里的映射是巨页映射,即直接在根页表中给出了1G的页映射,这是避免在初始化时加载过多的页表,简化系统启动。MMU在解析巨页映射时,直接就会计算出对应虚拟地址的物理地址,无需正常查三级页表。
巨页映射则这个项就是页表项,RWX至少有一位为1,如果指向的是一级子页表,RWX应该全为0
而所谓的启动mmu,其实就是给satp寄存器设置值,把页表的模式和页表地址告诉它,此后的地址访问,就会默认为虚拟地址,然后经过MMU映射后再得到物理地址(不设置satp就是默认直接访问)。1
2
3
4
5
6
7
8
9
10
11
12[PA:0x80200000, PA:0x80206000) .text (READ | EXECUTE | RESERVED)
[PA:0x80206000, PA:0x80209000) .rodata (READ | RESERVED)
[PA:0x80209000, PA:0x8020c000) .data .tdata .tbss .percpu (READ | WRITE | RESERVED)
[PA:0x8020c000, PA:0x8024c000) boot stack (READ | WRITE | RESERVED)
[PA:0x8024c000, PA:0x80270000) .bss (READ | WRITE | RESERVED)
[PA:0x80270000, PA:0x88000000) free memory (READ | WRITE | FREE)
[PA:0x101000, PA:0x102000) mmio (READ | WRITE | DEVICE | RESERVED)
[PA:0xc000000, PA:0xc210000) mmio (READ | WRITE | DEVICE | RESERVED)
[PA:0x10000000, PA:0x10001000) mmio (READ | WRITE | DEVICE | RESERVED)[PA:0x10001000, PA:0x10009000) mmio (READ | WRITE | DEVICE | RESERVED)
[PA:0x22000000, PA:0x24000000) mmio (READ | WRITE | DEVICE | RESERVED)
[PA:0x30000000, PA:0x40000000) mmio (READ | WRITE | DEVICE | RESERVED)
[PA:0x40000000, PA:0x80000000) mmio (READ | WRITE | DEVICE | RESERVED)修正sp寄存器
访问sp寄存器的值,应该是虚拟地址,所以要加一个偏移进行修正恢复参数,进入axruntime
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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72// oscamp/arceos/modules/axhal/src/platform/riscv64_qemu_virt/boot.rs
static mut BOOT_STACK: [u8; TASK_STACK_SIZE] = [0; TASK_STACK_SIZE];
static mut BOOT_PT_SV39: [u64; 512] = [0; 512];
unsafe fn init_boot_page_table() {
// 0x8000_0000..0xc000_0000, VRWX_GAD, 1G block
BOOT_PT_SV39[2] = (0x80000 << 10) | 0xef;
// 0xffff_ffc0_8000_0000..0xffff_ffc0_c000_0000, VRWX_GAD, 1G block
BOOT_PT_SV39[0x102] = (0x80000 << 10) | 0xef;
}
unsafe fn init_mmu() {
let page_table_root = BOOT_PT_SV39.as_ptr() as usize;
satp::set(satp::Mode::Sv39, 0, page_table_root >> 12);
riscv::asm::sfence_vma_all();
}
unsafe extern "C" fn _start() -> ! {
// PC = 0x8020_0000
// OpenSBI传来的两个参数
// a0 = hartid 用于将来识别CPU
// a1 = dtb 传入DTB的指针
core::arch::asm!("
mv s0, a0 // save hartid
mv s1, a1 // save DTB pointer
la sp, {boot_stack}
li t0, {boot_stack_size}
add sp, sp, t0 // setup boot stack
call {init_boot_page_table}
call {init_mmu} // setup boot page table and enabel MMU
li s2, {phys_virt_offset} // fix up virtual high address
add sp, sp, s2
mv a0, s0
mv a1, s1
la a2, {entry}
add a2, a2, s2
jalr a2 // call rust_entry(hartid, dtb)
j .",
phys_virt_offset = const PHYS_VIRT_OFFSET,
boot_stack_size = const TASK_STACK_SIZE,
boot_stack = sym BOOT_STACK,
init_boot_page_table = sym init_boot_page_table,
init_mmu = sym init_mmu,
entry = sym super::rust_entry,
options(noreturn),
)
}
// oscamp/arceos/modules/axhal/src/platform/riscv64_qemu_virt/mod.rs
unsafe extern "C" fn rust_entry(cpu_id: usize, dtb: usize) {
// .bss段 存放未初始化的静态变量和全局变量(.data段是已经初始化了的)此处由操作系统来支持将其初始化为全0
crate::mem::clear_bss();
// 把当前CPU设置为主CPU
crate::cpu::init_primary(cpu_id);
// 用于设置异常或中断处理的入口地址到stvec寄存器,保证异常发生时能够跳转到trap_handler之类的函数。
crate::arch::set_trap_vector_base(trap_vector_base as usize);
// UART (一种串口通信接口) 的初始化
self::time::init_early();
rust_main(cpu_id, dtb); // 进入axruntime
}
架构无关的初始化
不是说执行的代码真的与平台无关,只是已经进入了axruntime,各个架构都运行统一的一个初始化框架,但涉及平台的调用时,会按统一的接口往下调用axhal中绑定的具体架构的代码。
其实到了此处,一个最基本的内核算是初始化完成了,有基本的MMU,print helloworld可以正常执行。
如下面代码所示,基本就是打印一些信息,然后根据feature的不同,加入不同的组件。
然后,正如前面所述,unikernel就一个镜像,所以在编译链接时,通过A参数来指定APP文件,内核就可以直接调用到APP文件中的main函数并执行相应的应用。
1 | pub extern "C" fn rust_main(cpu_id: usize, dtb: usize) -> ! { |