Skip to content

Commit

Permalink
riscv-cpu-virtualization doc
Browse files Browse the repository at this point in the history
  • Loading branch information
ZhongkaiXu committed Jul 7, 2024
1 parent dec393b commit adbd395
Showing 1 changed file with 231 additions and 0 deletions.
231 changes: 231 additions & 0 deletions src/chap04/subchap01/RISCVirtualization.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# RISCV下的CPU虚拟化

摘要:围绕ArchCpu结构,介绍RISCV架构下的CPU虚拟化工作。

## 涉及的两个数据结构

hvisor支持多种架构,每个架构的CPU虚拟化需要做的工作不同,但在一个系统中又应该提供统一的接口,故我们将CPU拆分成 `PerCpu``ArchCpu` 两个数据结构。

### PerCpu

这是一个通用的CPU的描述,在 `PerCpu` 的文档中已给出介绍。

### ArchCpu

`ArchCpu` 是针对具体架构(**本文中介绍RISCV架构**)的CPU结构。由这个结构承担CPU具体的行为。

在ARM架构下,也有对应的 `ArchCpu` ,与本节介绍的 `ArchCpu` 具体结构略有不同,但他们具有相同的接口(也就是都具有初始化等行为)。

包含的字段如下:

```
pub struct ArchCpu {
pub x: [usize; 32], //x0~x31
pub hstatus: usize,
pub sstatus: usize,
pub sepc: usize,
pub stack_top: usize,
pub cpuid: usize,
// pub first_cpu: usize,
pub power_on: bool,
pub init: bool,
pub sstc: bool,
}
```

各个字段的解释如下:

- `x` :通用寄存器的值
- `hstatus` :存储Hypervisor状态寄存器的值
- `sstatus` :存储Supervisor状态寄存器的值,管理S模式的状态信息,如中断使能标志等
- `sepc` :异常处理结束的返回地址
- `stack_top` :对应的cpu栈的栈顶
- `power_on` :该cpu是否被开启
- `init` :该cpu是否已初始化
- `sstc` :是否配置了定时器中断

## 相关方法

这一部分讲解涉及的方法。

### ArchCpu::init

这个方法主要是对cpu进行初始化工作,设置初次进入vm的时候的上下文,以及一些CSR的初始化。

```
pub fn init(&mut self, entry: usize, cpu_id: usize, dtb: usize) {
write_csr!(CSR_SSCRATCH, self as *const _ as usize); //arch cpu pointer
self.sepc = entry;
self.hstatus = 1 << 7 | 2 << 32; //HSTATUS_SPV | HSTATUS_VSXL_64
self.sstatus = 1 << 8 | 1 << 63 | 3 << 13 | 3 << 15; //SPP
self.stack_top = self.stack_top() as usize;
self.x[10] = cpu_id; //cpu id
self.x[11] = dtb; //dtb addr
set_csr!(CSR_HIDELEG, 1 << 2 | 1 << 6 | 1 << 10); //HIDELEG_VSSI | HIDELEG_VSTI | HIDELEG_VSEI
set_csr!(CSR_HEDELEG, 1 << 8 | 1 << 12 | 1 << 13 | 1 << 15); //HEDELEG_ECU | HEDELEG_IPF | HEDELEG_LPF | HEDELEG_SPF
set_csr!(CSR_HCOUNTEREN, 1 << 1); //HCOUNTEREN_TM
//In VU-mode, a counter is not readable unless the applicable bits are set in both hcounteren and scounteren.
set_csr!(CSR_SCOUNTEREN, 1 << 1);
write_csr!(CSR_HTIMEDELTA, 0);
set_csr!(CSR_HENVCFG, 1 << 63);
//write_csr!(CSR_VSSTATUS, 1 << 63 | 3 << 13 | 3 << 15); //SSTATUS_SD | SSTATUS_FS_DIRTY | SSTATUS_XS_DIRTY
// enable all interupts
set_csr!(CSR_SIE, 1 << 9 | 1 << 5 | 1 << 1); //SEIE STIE SSIE
// write_csr!(CSR_HIE, 1 << 12 | 1 << 10 | 1 << 6 | 1 << 2); //SGEIE VSEIE VSTIE VSSIE
write_csr!(CSR_HIE, 0);
write_csr!(CSR_VSTVEC, 0);
write_csr!(CSR_VSSCRATCH, 0);
write_csr!(CSR_VSEPC, 0);
write_csr!(CSR_VSCAUSE, 0);
write_csr!(CSR_VSTVAL, 0);
write_csr!(CSR_HVIP, 0);
write_csr!(CSR_VSATP, 0);
}
```

`write_csr!(CSR_SSCRATCH, self as *const _ as usize)` 延续了上一个方法的内容,将 `ArchCpu` 的地址写入 `sscratch` 。将返回地址设置为入口,将 `hstatus``SPV` 字段设置为1,代表返回vm的时候vm是运行在VS态下的(或理解为异常发生前的vm是在VS态运行的);`VSXL` 字段设置了VS模式下寄存器的长度。`sstatus``SPP` 等字段给出 Trap 发生之前 CPU 处在哪个特权级等信息。`SPP``SPV` 字段,结合起来使用,能够确定在HS模式下执行 `sret` 指令应该返回哪个特权级,返回的地址由 `spec` 设置。

`HIDELEG``CSR_HEDELEG` 这两个寄存器的设置是将某些中断委托给VS态处理。`HCOUNTEREN``SCOUNTEREN` 用于限制vm能够访问的性能计数器,此处是使能了 `TM` 字段,代表允许访问 `time` 寄存器。`HTIMEDELTA` 用于调整vm读取 `time` 寄存器的值,在VS或者VU模式下读取 `time` 将返回 `HTIMEDELTA``time` 的和。`SIE` 的作用的中断使能,我们开启了所有中断。

在代码中注意 `write_csr!``set_csr!` 的区别,`write_csr!` 采用的是直接写入,也就是覆盖的方法,而 `set_csr!` 则是采用“or”的做法,设置某些位。

### ArchCpu::idle

通过执行wfi指令,将非主cpu设置为低功耗的idle状态。

设置一个特殊的内存页,包含了使得CPU进入低功耗等待状态的指令,从而在系统中未分配任何任务给某些CPU时,可以将它们置于低功耗等待状态,直到发生中断。

```
pub fn idle(&mut self) -> ! {
extern "C" {
fn vcpu_arch_entry() -> !;
}
assert!(this_cpu_id() == self.cpuid);
self.init(0, this_cpu_data().id, this_cpu_data().opaque);
// reset current cpu -> pc = 0x0 (wfi)
PARKING_MEMORY_SET.call_once(|| {
let parking_code: [u8; 4] = [0x73, 0x00, 0x50, 0x10]; // 1: wfi; b 1b
unsafe {
PARKING_INST_PAGE[..4].copy_from_slice(&parking_code);
}
let mut gpm = MemorySet::<Stage2PageTable>::new();
gpm.insert(MemoryRegion::new_with_offset_mapper(
0 as GuestPhysAddr,
unsafe { &PARKING_INST_PAGE as *const _ as HostPhysAddr - PHYS_VIRT_OFFSET },
PAGE_SIZE,
MemFlags::READ | MemFlags::WRITE | MemFlags::EXECUTE,
))
.unwrap();
gpm
});
unsafe {
PARKING_MEMORY_SET.get().unwrap().activate();
vcpu_arch_entry();
}
}
```

将该cpu的入口地址设置为0,而0地址将会被映射到 `parking page``parking page` 中设置了一些wfi指令的编码。`wfi`指令使得CPU进入等待状态,直到有中断发生。

然后进入 `vcpu_arch_entry``vcpu_arch_entry` 指向一段汇编代码,功能是根据 `sscratch` 找到 `ArchCpu` 进行上下文的恢复,然后执行 `sret` ,返回到 `spec` 的地址处执行,即执行刚刚设置的wfi指令(**而不是内核代码**),进入低功耗模式。

虽然在这里也进行了一些初始化的工作,但是并没有将cpu的初始化标志 `init` 设置为 `true` ,所以后续真正要唤醒并运行该cpu时,会重新进行初始化(在run方法中体现)。

### ArchCpu::run

该方法主要内容是进行了一些初始化,设置了正确的cpu执行入口,以及修改cpu已初始化的标志。

```
pub fn run(&mut self) -> ! {
extern "C" {
fn vcpu_arch_entry() -> !;
}
assert!(this_cpu_id() == self.cpuid);
//change power_on
this_cpu_data().activate_gpm();
if !self.init {
self.init(
this_cpu_data().cpu_on_entry,
this_cpu_data().id,
this_cpu_data().opaque, //dtb_ipa
);
self.init = true;
}
self.power_on = true;
info!("CPU{} run@{:#x}", self.cpuid, self.sepc);
info!("CPU{:#x?}", self);
unsafe {
vcpu_arch_entry();
}
}
```

可以看到这次的初始化,正确设置了cpu的入口地址,该入口地址指向内核的代码。然后进入 `vcpu_arch_entry` 执行,恢复上下文,**再返回到内核的代码执行**

### vcpu_arch_entry / VM_ENTRY

这是一段汇编代码,描述的是从hvisor进入vm的时候需要处理的工作。首先就是通过 `sscratch` 寄存器得到原本的 `ArchCpu` 中的上下文信息,再将 `hstatus``sstatus``sepc` 设置成之前我们保存的值,保证返回到vm的时候是VS态、并且从正确位置开始执行。最后恢复通用寄存器的值,并使用 `sret` 返回vm。

```
.macro VM_ENTRY
csrr x31, sscratch
ld x1, 32*8(x31)
csrw hstatus, x1
ld x1, 33*8(x31)
csrw sstatus, x1
ld x1, 34*8(x31)
csrw sepc, x1
ld x0, 0*8(x31)
...
ld x31, 31*8(x31)
sret
j .
.endm
```

### VM_EXIT

从vm退出进入hvisor时,也需要保存vm退出时候的相关状态。

首先还是通过 `sscratch` 寄存器得到 `ArchCpu` 的地址,但在这里我们会交换 `sscratch``x31` 的信息,而不是直接覆盖 `x31` 。然后将除 `x31` 外的通用寄存器的值进行保存,那么 `x31` 的信息现在在 `sscratch` 中,所以我们先把 `x31` 的值保存到 `sp` ,再交换 `x31``sscratch` ,将 `x31` 的信息通过 `sp` 存到 `ArchCpu` 的对应位置。

再将 `hstatus``sstatus``sepc` 进行保存,当我们在hvisor处理完工作,将要返回vm的时候,需要用到VM_ENTRY的代码,将这三个寄存器的值进行恢复到vm进入hvisor之前的状态,所以在这里我们应该进行保存。

`ld sp, 35*8(sp)``ArchCpu` 所保存的内核栈的栈顶放到 `sp` 中进行使用,便于我们在hvisor中去使用内核栈。

`csrr a0, sscratch` 这一句,将 `sscratch` 的值放到 `a0` 寄存器中,当我们保存完上下文,跳转到异常处理函数的时候,参数将会通过 `a0` 传递,那么在异常处理的时候就能够访问保存起来的上下文,比如退出码等等。

```
.macro VM_EXIT
csrrw x31, sscratch, x31
sd x0, 0*8(x31)
...
sd x30, 30*8(x31)
mv sp, x31
csrrw x31, sscratch, x31
sd x31, 31*8(sp)
csrr t0, hstatus
sd t0, 32*8(sp)
csrr t0, sstatus
sd t0, 33*8(sp)
csrr t0, sepc
sd t0, 34*8(sp)
ld sp, 35*8(sp)
csrr a0, sscratch
.endm
```

0 comments on commit adbd395

Please sign in to comment.