Skip to content

Commit

Permalink
2.1 riscv上手 (#12)
Browse files Browse the repository at this point in the history
* 2.1 riscv上手
4.5.2 PLIC
4.5.3 AIA

* edit 2.1 riscv quick start 4.5.2 PLIC  4.5.3 AIA
  • Loading branch information
SanchezDorso authored Jul 20, 2024
1 parent c2776f4 commit 44f0e22
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 0 deletions.
87 changes: 87 additions & 0 deletions src/chap02/QemuRISC-V.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 安装qemu
安装QEMU 7.2.12:
```
wget https://download.qemu.org/qemu-7.2.12.tar.xz
# 解压
tar xvJf qemu-7.2.12.tar.xz
cd qemu-7.2.12
# 配置Riscv支持
./configure --target-list=riscv64-softmmu,riscv64-linux-user
make -j$(nproc)
#加入环境变量
export PATH=$PATH:/path/to/qemu-7.2.12/build
#测试是否安装成功
qemu-system-riscv64 --version
```
# 安装交叉编译器
riscv的交叉编译器需从riscv-gnu-toolchain获取并编译。
```
# 安装必要工具
sudo apt-get install autoconf automake autotools-dev curl python3 python3-pip libmpc-dev libmpfr-dev libgmp-dev gawk build-essential bison flex texinfo gperf libtool patchutils bc zlib1g-dev libexpat-dev ninja-build git cmake libglib2.0-dev libslirp-dev
git clone https://github.com/riscv/riscv-gnu-toolchain
cd riscv-gnu-toolchain
git rm qemu
git submodule update --init --recursive
#上述操作会占用超过5GB以上磁盘空间
# 如果git报网络错误,可以执行:
git config --global http.postbuffer 524288000
```
之后开始编译工具链:
```
cd riscv-gnu-toolchain
mkdir build
cd build
../configure --prefix=/opt/riscv64
sudo make linux -j $(nproc)
# 编译完成后,将工具链加入环境变量
echo 'export PATH=/opt/riscv64/bin:$PATH' >> ~/.bashrc
source ~/.bashrc
```
这样就得到了 riscv64-unknown-linux-gnu工具链。
# 编译Linux
```
git clone https://github.com/torvalds/linux -b v6.2 --depth=1
cd linux
git checkout v6.2
make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- defconfig
make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- modules -j$(nproc)
# 开始编译
make ARCH=riscv CROSS_COMPILE=riscv64-unknown-linux-gnu- Image -j$(nproc)
```
# 制作ubuntu根文件系统
```
wget http://cdimage.ubuntu.com/ubuntu-base/releases/20.04/release/ubuntu-base-20.04.2-base-riscv64.tar.gz
mkdir rootfs
dd if=/dev/zero of=riscv_rootfs.img bs=1M count=1024 oflag=direct
mkfs.ext4 riscv_rootfs.img
sudo mount -t ext4 riscv_rootfs.img rootfs/
sudo tar -xzf ubuntu-base-20.04.2-base-riscv64.tar.gz -C rootfs/
sudo cp /path-to-qemu/build/qemu-system-riscv64 rootfs/usr/bin/
sudo cp /etc/resolv.conf rootfs/etc/resolv.conf
sudo mount -t proc /proc rootfs/proc
sudo mount -t sysfs /sys rootfs/sys
sudo mount -o bind /dev rootfs/dev
sudo mount -o bind /dev/pts rootfs/dev/pts
sudo chroot rootfs
# 进入chroot后,安装必要的软件包:
apt-get update
apt-get install git sudo vim bash-completion \
kmod net-tools iputils-ping resolvconf ntpdate
exit
sudo umount rootfs/proc
sudo umount rootfs/sys
sudo umount rootfs/dev/pts
sudo umount rootfs/dev
sudo umount rootfs
```
# 运行hvisor
将做好的根文件系统、Linux内核镜像放在hvisor目录下的指定位置,在hvisor根目录下执行`make run ARCH=riscv64`即可

默认情况下使用 PLIC,执行`make run ARCH=riscv64 IRQ=aia`开启AIA规范

# 可能出现的问题
linux运行后显示`/bin/sh: 0: can't access tty; job control turned off`,在控制台输入`bash`
Binary file added src/chap04/img/riscv_aia_aplicdomain.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/chap04/img/riscv_aia_intfile.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/chap04/img/riscv_aia_ipi.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/chap04/img/riscv_aia_struct.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/chap04/img/riscv_plic_flow.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added src/chap04/img/riscv_plic_struct.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
148 changes: 148 additions & 0 deletions src/chap04/subchap02/RISC-AIA.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
# 总体结构
AIA主要包括两个部分,消息中断控制器 IMSIC 和高级平台级中断控制器 APLIC ,总体结构如图所示

<img src="../img/riscv_aia_struct.jpg" style="zoom: 50%;" />

外设既可以选择发送消息中断,也可以选择通过线连接的方式发送有线中断。

如果外设 A 支持MSI,那么只需要向指定 hart 的中断文件写入指定的数据,之后 IMSIC 就会向目标处理器投送一个中断。

对于所有设备,都可以通过中断线与 APLIC 连接, APLIC 将会根据配置,选择中断投送模式为:
* 有线中断
* MSI

在hvisor中,中断的投送模式为 MSI

在hvisor中使用 `IRQ=aia`开启 AIA 规范后,时钟中断的处理仍然一致,软件中断和外部中断的处理有些变化
# 外部中断
## IMSIC

hvisor中一个物理 CPU 对应一个虚拟 CPU ,它们都拥有自己的中断文件

<img src="../img/riscv_aia_intfile.png" style="zoom: 80%;" />

向某个中断文件写入,即可触发指定 hart 指定特权级别的外部中断

为 IMSIC 提供二阶段地址映射表
```rs
let paddr = 0x2800_0000 as HostPhysAddr;
let size = PAGE_SIZE;
self.gpm.insert(MemoryRegion::new_with_offset_mapper(
paddr as GuestPhysAddr,
paddr + PAGE_SIZE * 1,
size,
MemFlags::READ | MemFlags::WRITE,
))?;
...
```

## APLIC
### 结构
全局只有一个 APLIC

有线中断到来时,首先到达位于机器模式的根中断域(OpenSBI),之后中断路由到子中断域(hvisor),hvisor将中断信号按照 APLIC 配置好的 target 的寄存器,以 MSI 的方式发送给虚拟机对应的 CPU。

<img src="../img/riscv_aia_aplicdomain.png" style="zoom: 70%;" />

在 AIA 规范手册中指定了 APLIC 各个字段的字节偏移。定义 APLIC 结构体如下,通过以下方法实现对 APLIC 字段的读写
```rs
#[repr(C)]
pub struct Aplic {
pub base: usize,
pub size: usize,
}
impl Aplic {
pub fn new(base: usize, size: usize) -> Self {
Self {
base,
size,
}
}
pub fn read_domaincfg(&self) -> u32{
let addr = self.base + APLIC_DOMAINCFG_BASE;
unsafe { core::ptr::read_volatile(addr as *const u32) }
}
pub fn set_domaincfg(&self, bigendian: bool, msimode: bool, enabled: bool){
...
let addr = self.base + APLIC_DOMAINCFG_BASE;
let src = (enabled << 8) | (msimode << 2) | bigendian;
unsafe {
core::ptr::write_volatile(addr as *mut u32, src);
}
}
...
}
```

### 初始化
根据设备树中的基地址和大小初始化 APLIC
```rs
pub fn primary_init_early(host_fdt: &Fdt) {
let aplic_info = host_fdt.find_node("/soc/aplic").unwrap();
init_aplic(
aplic_info.reg().unwrap().next().unwrap().starting_address as usize,
aplic_info.reg().unwrap().next().unwrap().size.unwrap(),
);
}
pub fn init_aplic(aplic_base: usize, aplic_size: usize) {
let aplic = Aplic::new(aplic_base, aplic_size);
APLIC.call_once(|| RwLock::new(aplic));
}
pub static APLIC: Once<RwLock<Aplic>> = Once::new();
pub fn host_aplic<'a>() -> &'a RwLock<Aplic> {
APLIC.get().expect("Uninitialized hypervisor aplic!")
}
```
APLIC全局只有一个,因此加锁避免读写冲突,使用 host_aplic() 方法进行访问

虚拟机启动时,将访问 APLIC 的地址空间进行初始化配置,这个地址空间是未被映射的。因此会触发缺页异常,陷入到 hvisor 中来处理
```rs
pub fn guest_page_fault_handler(current_cpu: &mut ArchCpu) {
...
if addr >= host_aplic_base && addr < host_aplic_base + host_aplic_size {
let mut inst: u32 = read_csr!(CSR_HTINST) as u32;
...
if let Some(inst) = inst {
vaplic_emul_handler(current_cpu, addr, inst);
current_cpu.sepc += ins_size;
}
...
}
}
```
判断访问的地址空间属于 APLIC 的范围,解析访问指令,进入 vaplic_emul_handler 实现对虚拟机中 APLIC 的模拟

```rs
pub fn vaplic_emul_handler(
current_cpu: &mut ArchCpu,
addr: GuestPhysAddr,
inst: Instruction,
) {
let host_aplic = host_aplic();
let offset = addr.wrapping_sub(host_aplic.read().base);
if offset >= APLIC_DOMAINCFG_BASE && offset < APLIC_SOURCECFG_BASE {
match inst {
Instruction::Sw(i) => {
...
host_aplic.write().set_domaincfg(bigendian, msimode, enabled);
}
Instruction::Lw(i) => {
let value = host_aplic.read().read_domaincfg();
current_cpu.x[i.rd() as usize] = value as usize;
}
_ => panic!("Unexpected instruction {:?}", inst),
}
}
...
}
```
## 中断过程
hvisor 通过缺页异常的方式完成对虚拟机模拟 APLIC 初始化后,进入到虚拟机中,以键盘按下产生的中断为例:中断信号首先来到 OpenSBI ,之后中断路由至 hvisor ,根据target寄存器的配置,向虚拟中断文件写入触发虚拟机的外部中断。
# 软件中断
开启 AIA 规范后,虚拟机的 linux 内核会通过 msi 的方式来发送 IPI,不需要再使用 ecall 指令陷入到 hvisor 中

<img src="../img/riscv_aia_ipi.jpg" style="zoom:40%;" />

如图所示,在hvisor中,向指定hart的中断文件写入,即可触发 IPI。

在虚拟机中,只需要向指定的虚拟中断文件写入,即可实现虚拟机中的 IPI,无需hvisor的模拟支持。
155 changes: 155 additions & 0 deletions src/chap04/subchap02/RISC-PLIC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# 中断的来源
在 hvisor 中有三种中断类型:时钟中断,软件中断和外部中断。

时钟中断:当 time 寄存器变得大于 timecmp 寄存器时,产生一个时钟中断

软件中断: 在多核系统中,一个 hart 向另一个 hart 发送核间中断,通过SBI调用来实现

外部中断: 外部设备通过中断线将中断信号传给处理器

# 时钟中断
虚拟机需要触发时钟中断时,通过 ecall 指令陷入到 hvisor 中
```rust
ExceptionType::ECALL_VS => {
trace!("ECALL_VS");
sbi_vs_handler(current_cpu);
current_cpu.sepc += 4;
}
...
pub fn sbi_vs_handler(current_cpu: &mut ArchCpu) {
let eid: usize = current_cpu.x[17];
let fid: usize = current_cpu.x[16];
let sbi_ret;
match eid {
...
SBI_EID::SET_TIMER => {
sbi_ret = sbi_time_handler(fid, current_cpu);
}
...
}
}
```
如果没有开启 sstc 扩展,则需要通过 SBI 调用陷入到机器模式,设置 mtimecmp 寄存器,清零虚拟机的时钟中断挂起位,打开 hvisor 的时钟中断使能位;如果开启了 sstc 扩展,则可以直接设置 stimecmp 。
```rs
pub fn sbi_time_handler(fid: usize, current_cpu: &mut ArchCpu) -> SbiRet {
...
if current_cpu.sstc {
write_csr!(CSR_VSTIMECMP, stime);
} else {
set_timer(stime);
unsafe {
// clear guest timer interrupt pending
hvip::clear_vstip();
// enable timer interrupt
sie::set_stimer();
}
}
return sbi_ret;
}
```
当 time 寄存器变得大于 timecmp 寄存器时,产生一个时钟中断

中断触发后,保存陷入上下文,并分发到相对应的处理函数中
```rs
InterruptType::STI => {
unsafe {
hvip::set_vstip();
sie::clear_stimer();
}
}
```
将虚拟机的时钟中断挂起位置为1,即向虚拟机注入时钟中断,将hvisor的时钟中断使能位清零,完成中断处理

# 软件中断
虚拟机需要发送 IPI 时,通过 ecall 指令陷入到 hvisor 中
```rs
SBI_EID::SEND_IPI => {
...
sbi_ret = sbi_call_5(
eid,
fid,
current_cpu.x[10],
current_cpu.x[11],
current_cpu.x[12],
current_cpu.x[13],
current_cpu.x[14],
);
}
```
再通过 SBI 调用陷入到机器模式中向指定的 hart 发送 IPI ,设置 mip 寄存器的 SSIP 为1即可向hvisor注入核间中断

中断触发后,保存陷入上下文,并分发到相对应的处理函数中
```rs
pub fn handle_ssi(current_cpu: &mut ArchCpu) {
...
clear_csr!(CSR_SIP, 1 << 1);
set_csr!(CSR_HVIP, 1 << 2);
check_events();
}
```
将虚拟机的软件中断挂起位置为1,向虚拟机中注入软件中断。之后判断核间中断的类型,唤醒或阻塞cpu,或是处理 VIRTIO 的相关的中断请求

# 外部中断
## PLIC
RISC-V 通过 PLIC 实现对外部中断处理,PLIC 不支持虚拟化,不支持 MSI

<img src="../img/riscv_plic_struct.png" style="zoom: 50%;" />

PLIC 架构框图

PLIC的中断流程示意图如下

<img src="../img/riscv_plic_flow.png" style="zoom:70%;" />


中断源通过中断线向 PLIC 发送一个中断信号,只有当中断的优先级大于阈值的时候,才可以通过阈值寄存器的筛选。

之后读取 claim 寄存器得到 pending 的优先级最高的中断,之后清除对应的 pending 位。传给目标hart进行中断处理

处理完成后向 complete 寄存器写入中断号,可以接收下一个中断请求
## 初始化
初始化的过程与AIA类似
## 处理过程
虚拟机中的外部中断触发时,将访问 vPLIC 的地址空间,然而 PLIC 并不支持虚拟化,这个地址空间是未被映射的。因此会触发缺页异常,陷入到 hvisor 中来处理

异常触发后,保存陷入上下文,进入到缺页异常处理函数中

```rs
pub fn guest_page_fault_handler(current_cpu: &mut ArchCpu) {
...
if addr >= host_plic_base && addr < host_plic_base + PLIC_TOTAL_SIZE {
let mut inst: u32 = read_csr!(CSR_HTINST) as u32;
...
if let Some(inst) = inst {
if addr >= host_plic_base + PLIC_GLOBAL_SIZE {
vplic_hart_emul_handler(current_cpu, addr, inst);
} else {
vplic_global_emul_handler(current_cpu, addr, inst);
}
current_cpu.sepc += ins_size;
}
...
}
}
```
判断发生缺页异常的地址是否在 PLIC 的地址空间内,之后解析发生异常的指令,根据访问地址和访问指令,修改 PLIC 的地址空间来实现对于 vPLIC 的模拟配置
```rs
pub fn vplic_hart_emul_handler(current_cpu: &mut ArchCpu, addr: GuestPhysAddr, inst: Instruction) {
...
if offset >= PLIC_GLOBAL_SIZE && offset < PLIC_TOTAL_SIZE {
...
if index == 0 {
// threshold
match inst {
Instruction::Sw(i) => {
// guest write threshold register to plic core
let value = current_cpu.x[i.rs2() as usize] as u32;
host_plic.write().set_threshold(context, value);
}
_ => panic!("Unexpected instruction threshold {:?}", inst),
}
...
}
}
}
```

0 comments on commit 44f0e22

Please sign in to comment.