[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
[置顶] Linux Lab v1.4 升级部分内核到 v6.10,新增泰晓 RISC-V 实验箱支持,新增最小化内核配置支持大幅提升内核编译速度,在单终端内新增多窗口调试功能等Linux Lab 发布 v1.4 正式版,升级部分内核到 v6.10,新增泰晓实验箱支持
[置顶] 泰晓社区近日发布了一款儿童益智版 Linux 系统盘,集成了数十个教育类与益智游戏类开源软件国内首个儿童 Linux 系统来了,既可打字编程学习数理化,还能下棋研究数独提升智力
RISC-V KVM 中断处理的实现(一)
Corrector: TinyCorrect v0.1 - [tounix spaces] Author: XiakaiPan 13212017962@163.com Date: 20221201 Revisor: Walimis walimis@walimis.org Project: RISC-V Linux 内核剖析 Proposal: RISC-V 虚拟化技术调研与分析 Sponsor: PLCT Lab, ISCAS
前言
本文就 Spike 和 QEMU 中与 RISC-V Trap 处理相关的实现细节进行了较为深入的挖掘,包括 PLIC 和 CLINT 等在模拟器中的实现。从模拟器的角度来看,当前它们对于虚拟化扩展的支持,是在其内部中断处理机制的基础上,添加了虚拟化所需的 CSR 以及对应的 Trap 判断与触发逻辑。
软件版本
Software | Version |
---|---|
QEMU | 7.1.0 |
Spike | 70f8aa01b803f4dbc0461fd7c986c1ca76d4b1d9 |
Spike 实现分析
Trap 统一编码与命名习惯
RISC-V Trap 包含中断和异常,其编码在 自动生成 的编码文件中分别以如下名称开头:
- IRQ:Interrupt ReQuest,用于表示中断编码
- CAUSE:cause 是 RISC-V 中用于保存 trap 的具体编码的 CSR,包括
mcause
,scause
,vscause
等,此处专门用于表示异常编码
Trap 处理函数调用
中断处理
中断定义
// riscv/encoding.h: line 266
#define IRQ_U_SOFT 0
#define IRQ_S_SOFT 1
#define IRQ_VS_SOFT 2
#define IRQ_M_SOFT 3
#define IRQ_U_TIMER 4
#define IRQ_S_TIMER 5
#define IRQ_VS_TIMER 6
#define IRQ_M_TIMER 7
#define IRQ_U_EXT 8
#define IRQ_S_EXT 9
#define IRQ_VS_EXT 10
#define IRQ_M_EXT 11
#define IRQ_S_GEXT 12
#define IRQ_COP 12
#define IRQ_LCOF 13
Spike 定义了 sim_t
类作为最外层的模拟管理器,其包含了一个 processor
vector,Spike 模拟 CPU 的执行即是 sim
的各个 processor
调用 step
函数执行 fetch, decode, execute 等操作。trap 处理操作即在 step
函数中实现。
具体来说,trap 处理包含了两个步骤:
判断当前是否需要进行 Interrupt 处理
当且仅当 mie
和 mip
两个 CSR 的值,进行逻辑与不为 0 时,trap 才会被处理:
void take_pending_interrupt() { take_interrupt(state.mip->read() & state.mie->read()); }
确定 Interrupt 的内容
void take_interrupt(reg_t mask);
:如果 mask
为 0 则不执行 trap 处理,否则将判断 trap 具体类型,抛出一个 trap(trap_t
)
void processor_t::take_interrupt(reg_t pending_interrupts)
{
// 不执行中断处理
// Do nothing if no pending interrupts
if (!pending_interrupts) {
return;
}
// 依照 M, HS, VS 的顺序确定具体要处理哪一个特权级的 trap
// M-ints have higher priority over HS-ints and VS-ints
const reg_t mie = get_field(state.mstatus->read(), MSTATUS_MIE);
const reg_t m_enabled = state.prv < PRV_M || (state.prv == PRV_M && mie);
reg_t enabled_interrupts = pending_interrupts & ~state.mideleg->read() & -m_enabled;
if (enabled_interrupts == 0) {
// HS-ints have higher priority over VS-ints
const reg_t deleg_to_hs = state.mideleg->read() & ~state.hideleg->read();
const reg_t sie = get_field(state.sstatus->read(), MSTATUS_SIE);
const reg_t hs_enabled = state.v || state.prv < PRV_S || (state.prv == PRV_S && sie);
enabled_interrupts = pending_interrupts & deleg_to_hs & -hs_enabled;
if (state.v && enabled_interrupts == 0) {
// VS-ints have least priority and can only be taken with virt enabled
const reg_t deleg_to_vs = state.hideleg->read();
const reg_t vs_enabled = state.prv < PRV_S || (state.prv == PRV_S && sie);
enabled_interrupts = pending_interrupts & deleg_to_vs & -vs_enabled;
}
}
// 按照 MEI, MSI, MTI; SEI, SSI, STI (HS > VS) 的优先级确定 mcause/scause 最高位之外的内容
if (!state.debug_mode && enabled_interrupts) {
// nonstandard interrupts have highest priority
if (enabled_interrupts >> (IRQ_M_EXT + 1))
enabled_interrupts = enabled_interrupts >> (IRQ_M_EXT + 1) << (IRQ_M_EXT + 1);
// standard interrupt priority is MEI, MSI, MTI, SEI, SSI, STI
else if (enabled_interrupts & MIP_MEIP)
enabled_interrupts = MIP_MEIP;
else if (enabled_interrupts & MIP_MSIP)
enabled_interrupts = MIP_MSIP;
else if (enabled_interrupts & MIP_MTIP)
enabled_interrupts = MIP_MTIP;
else if (enabled_interrupts & MIP_SEIP)
enabled_interrupts = MIP_SEIP;
else if (enabled_interrupts & MIP_SSIP)
enabled_interrupts = MIP_SSIP;
else if (enabled_interrupts & MIP_STIP)
enabled_interrupts = MIP_STIP;
else if (enabled_interrupts & MIP_LCOFIP)
enabled_interrupts = MIP_LCOFIP;
else if (enabled_interrupts & MIP_VSEIP)
enabled_interrupts = MIP_VSEIP;
else if (enabled_interrupts & MIP_VSSIP)
enabled_interrupts = MIP_VSSIP;
else if (enabled_interrupts & MIP_VSTIP)
enabled_interrupts = MIP_VSTIP;
else
abort();
// 抛出异常编码(最高位为 1)
throw trap_t(((reg_t)1 << (isa->get_max_xlen() - 1)) | ctz(enabled_interrupts));
}
}
异常处理
异常定义
如上节所述,异常的指令集编码即宏定义命名由生成的 encoding.h
指定,之后在 trap.h
通过宏定义为每一类异常定义一个 trap_t
的派生类。
RISC-V 指令集规定的所有异常编码及其命名如下代码块所示:
// riscv/encoding.h: line 3147
#define CAUSE_MISALIGNED_FETCH 0x0
#define CAUSE_FETCH_ACCESS 0x1
#define CAUSE_ILLEGAL_INSTRUCTION 0x2
#define CAUSE_BREAKPOINT 0x3
#define CAUSE_MISALIGNED_LOAD 0x4
#define CAUSE_LOAD_ACCESS 0x5
#define CAUSE_MISALIGNED_STORE 0x6
#define CAUSE_STORE_ACCESS 0x7
#define CAUSE_USER_ECALL 0x8
#define CAUSE_SUPERVISOR_ECALL 0x9
#define CAUSE_VIRTUAL_SUPERVISOR_ECALL 0xa
#define CAUSE_MACHINE_ECALL 0xb
#define CAUSE_FETCH_PAGE_FAULT 0xc
#define CAUSE_LOAD_PAGE_FAULT 0xd
#define CAUSE_STORE_PAGE_FAULT 0xf
#define CAUSE_FETCH_GUEST_PAGE_FAULT 0x14
#define CAUSE_LOAD_GUEST_PAGE_FAULT 0x15
#define CAUSE_VIRTUAL_INSTRUCTION 0x16
#define CAUSE_STORE_GUEST_PAGE_FAULT 0x17
将 exception cause 编码与特定类绑定是通过如下代码实现的,共有三类 exception 类型的 trap:
MEM_TRAP
:访存相关的异常,如地址对齐、page-faultTRAP
:系统调用指令ecall
和 中断指令ebreak
INSN_TRAP
:指令相关异常,如非法指令、虚拟指令特权级问题
// riscv/trap.h: line 91
#define DECLARE_MEM_TRAP(n, x) class trap_##x : public mem_trap_t { \
public: \
trap_##x(bool gva, reg_t tval, reg_t tval2, reg_t tinst) : mem_trap_t(n, gva, tval, tval2, tinst) {} \
const char* name() { return "trap_"#x; } \
};
#define DECLARE_TRAP(n, x) class trap_##x : public trap_t { \
public: \
trap_##x() : trap_t(n) {} \
const char* name() { return "trap_"#x; } \
};
#define DECLARE_INST_TRAP(n, x) class trap_##x : public insn_trap_t { \
public: \
trap_##x(reg_t tval) : insn_trap_t(n, /* gva */false, tval) {} \
const char* name() { return "trap_"#x; } \
};
// ...
// riscv/trap.h: line 103
DECLARE_MEM_TRAP(CAUSE_MISALIGNED_FETCH, instruction_address_misaligned)
DECLARE_TRAP(CAUSE_USER_ECALL, user_ecall)
DECLARE_INST_TRAP(CAUSE_ILLEGAL_INSTRUCTION, illegal_instruction)
// ...
综上,所有 trap(exception,interrupt)在 Spike 代码中的表示方式如下图所示:
异常抛出
整个模拟器在执行过程中,通过 try, catch
来捕获并处理每个处理器执行中的异常,这些异常是在各个组件以及指令中写定的。例如访存相关的异常都是在 MMU 的实现中规定的:
// riscv/mmu.h: line 364
// ITLB lookup
inline tlb_entry_t translate_insn_addr(reg_t addr) {
// ...
return fetch_slow_path(addr);
}
// riscv/mmu.cc: line 80
tlb_entry_t mmu_t::fetch_slow_path(reg_t vaddr)
{
// ...
if (!mmio_load(paddr, sizeof fetch_temp, (uint8_t*)&fetch_temp))
throw trap_instruction_access_fault(proc->state.v, vaddr, 0, 0);
result = {(char*)&fetch_temp - vaddr, paddr - vaddr};
// ...
return result;
}
单个处理器的执行主题循环如下所示,异常和中断均是在其中的 try, catch
中捕获并处理的:
// riscv/execute.cc: line 219
// fetch/decode/execute loop
void processor_t::step(size_t n)
{
// ...
while (n > 0) {
// ...
try
{
take_pending_interrupt();
// 仿真的循环主体:抛出执行过程中的异常,之后进入 catch 中的处理语句
// Main simulation loop, slow/fast path: throw exceptions if they occur during execution
// ...
}
catch(trap_t& t)
{
take_trap(t, pc);
// ...
}
// Other catch statements, instructions counting.
// ...
}
}
Trap 处理
整个 trap 的处理过程如下图所示:
虚线表示函数对应的实现,细实线表示函数调用关系,粗实线表示参数的传递关系。
由图可知,最终的 trap 处理是通过调用 riscv/process.cc
的 take_trap
函数实现的,其定义及功能分析参见 此文 的模拟器实现中的 Spike 小节。简单来说,它完成了如下任务:
- 确定该 trap 的类型(异常或者中断)以及它将在哪个特权级被处理(默认 M,但可以通过 medeleg/mideleg 和 hedeleg/hideleg 委托给 HS 或 VS)
- 写入 CSR(
cause
,epc
,tval
)以记录 trap 内容,保存当前执行环境用于处理完成之后的恢复 - 修改
status
寄存器及当前特权级,进入 trap 处理程序运行环境
CLINT
CLINT(Core-Local INTerrupt)是 RISC-V 核间局部中断控制器,用于管理软件中断和计时器中断的注入和解除。
在 Spike 中,CLINT 是 bus
上的设备之一 device
,CPU 对 CLINT 和 mems
等的访问通过 mmio.store/load
来实现。所有的 mmio 访存均是通过调用 bus.load/store
完成的,例如 mmu_t 和 sim_t 的 load, store 操作:
而 CPU 通过 bus
实现的访存,是先通过地址所在的范围确定对应的具体设备,进而调用该设备的访存函数来完成的:
bool bus_t::load(reg_t addr, size_t len, uint8_t* bytes)
{
// Find the device with the base address closest to but
// less than addr (price-is-right search)
auto it = devices.upper_bound(addr);
if (devices.empty() || it == devices.begin()) {
// Either the bus is empty, or there weren't
// any items with a base address <= addr
return false;
}
// Found at least one item with base address <= addr
// The iterator points to the device after this, so
// go back by one item.
it--;
return it->second->load(addr - it->first, len, bytes);
}
bool bus_t::store(reg_t addr, size_t len, const uint8_t* bytes)
{
auto it = devices.upper_bound(addr);
if (devices.empty() || it == devices.begin()) {
return false;
}
it--;
return it->second->store(addr - it->first, len, bytes);
}
对于 CLINT 来说,其所担负的软件中断和计时器中断的职能,就是通过 clint_t
的 load/store
方法配合对应 CSR 的访问来实现的,参考 此文:
// riscv/clint.cc: line 48
bool clint_t::store(reg_t addr, size_t len, const uint8_t* bytes)
{
// 若地址在 MSIP 内,表示当前中断为软件中断,将 byte 内的内容写入 addr 作为偏移量所指示的位置,即写入软件中断
if (addr >= MSIP_BASE && addr + len <= MSIP_BASE + procs.size()*sizeof(msip_t)) {
std::vector<msip_t> msip(procs.size());
std::vector<msip_t> mask(procs.size(), 0);
memcpy((uint8_t*)&msip[0] + addr - MSIP_BASE, bytes, len);
memset((uint8_t*)&mask[0] + addr - MSIP_BASE, 0xff, len);
for (size_t i = 0; i < procs.size(); ++i) {
if (!(mask[i] & 0xFF)) continue;
procs[i]->state.mip->backdoor_write_with_mask(MIP_MSIP, 0);
if (!!(msip[i] & 1))
procs[i]->state.mip->backdoor_write_with_mask(MIP_MSIP, MIP_MSIP);
}
} else if (addr >= MTIMECMP_BASE && addr + len <= MTIMECMP_BASE + procs.size()*sizeof(mtimecmp_t)) {
// 设置计时器中断:向 mtimecmp 寄存器写入特定值,待到 mtime 达到该值时产生一个中断
memcpy((uint8_t*)&mtimecmp[0] + addr - MTIMECMP_BASE, bytes, len);
} else if (addr >= MTIME_BASE && addr + len <= MTIME_BASE + sizeof(mtime_t)) {
// 设置 mtime 的值
memcpy((uint8_t*)&mtime + addr - MTIME_BASE, bytes, len);
} else {
return false;
}
increment(0);
return true;
}
PLIC/Interrupt Controller/NS16550
PLIC(Platform-Level Interrupt Controller)是 RISC-V 外部中断控制器,而 NS16550 则是一个 UART(Universal Asynchronous Receiver/Transmitter)软核,用于 CPU 和外设之间的通信。CLINT,PLIC 以及 NS16550 都要挂载在设备树(fdt, flatten device tree)上。NS16550 在创建时会以 PLIC 指针为中断控制器,用来向 CPU 发送外部中断。
与 CLINT 类似,PLIC 等设备在创建之后也都几乎没有用到除计时之外的功能。
QEMU
Trap 相关术语及定义
中断统一以 IRQ 作为前缀命名:
// target/riscv/cpu_bits.h: line 609
/* Interrupt causes */
#define IRQ_U_SOFT 0
#define IRQ_S_SOFT 1
#define IRQ_VS_SOFT 2
#define IRQ_M_SOFT 3
#define IRQ_U_TIMER 4
#define IRQ_S_TIMER 5
#define IRQ_VS_TIMER 6
#define IRQ_M_TIMER 7
#define IRQ_U_EXT 8
#define IRQ_S_EXT 9
#define IRQ_VS_EXT 10
#define IRQ_M_EXT 11
#define IRQ_S_GEXT 12
#define IRQ_LOCAL_MAX 16
#define IRQ_LOCAL_GUEST_MAX (TARGET_LONG_BITS - 1)
异常定义为枚举变量:
// target/riscv/cpu_bits.h: line 581
/* Exception causes */
typedef enum RISCVException {
RISCV_EXCP_NONE = -1, /* sentinel value */
RISCV_EXCP_INST_ADDR_MIS = 0x0,
RISCV_EXCP_INST_ACCESS_FAULT = 0x1,
RISCV_EXCP_ILLEGAL_INST = 0x2,
RISCV_EXCP_BREAKPOINT = 0x3,
RISCV_EXCP_LOAD_ADDR_MIS = 0x4,
RISCV_EXCP_LOAD_ACCESS_FAULT = 0x5,
RISCV_EXCP_STORE_AMO_ADDR_MIS = 0x6,
RISCV_EXCP_STORE_AMO_ACCESS_FAULT = 0x7,
RISCV_EXCP_U_ECALL = 0x8,
RISCV_EXCP_S_ECALL = 0x9,
RISCV_EXCP_VS_ECALL = 0xa,
RISCV_EXCP_M_ECALL = 0xb,
RISCV_EXCP_INST_PAGE_FAULT = 0xc, /* since: priv-1.10.0 */
RISCV_EXCP_LOAD_PAGE_FAULT = 0xd, /* since: priv-1.10.0 */
RISCV_EXCP_STORE_PAGE_FAULT = 0xf, /* since: priv-1.10.0 */
RISCV_EXCP_SEMIHOST = 0x10,
RISCV_EXCP_INST_GUEST_PAGE_FAULT = 0x14,
RISCV_EXCP_LOAD_GUEST_ACCESS_FAULT = 0x15,
RISCV_EXCP_VIRT_INSTRUCTION_FAULT = 0x16,
RISCV_EXCP_STORE_GUEST_AMO_ACCESS_FAULT = 0x17,
} RISCVException;
所有的 CSR 操作函数其返回值均为 RISCVException
,例如:
// target/riscv/csr.c: line 1468
static RISCVException read_mtvec(CPURISCVState *env, int csrno,
target_ulong *val)
{
*val = env->mtvec;
return RISCV_EXCP_NONE;
}
// target/riscv/csr.c: line 832
static const target_ulong vs_delegable_excps = DELEGABLE_EXCPS &
~((1ULL << (RISCV_EXCP_S_ECALL)) |
(1ULL << (RISCV_EXCP_VS_ECALL)) |
(1ULL << (RISCV_EXCP_M_ECALL)) |
(1ULL << (RISCV_EXCP_INST_GUEST_PAGE_FAULT)) |
(1ULL << (RISCV_EXCP_LOAD_GUEST_ACCESS_FAULT)) |
(1ULL << (RISCV_EXCP_VIRT_INSTRUCTION_FAULT)) |
(1ULL << (RISCV_EXCP_STORE_GUEST_AMO_ACCESS_FAULT)));
Trap 处理
QEMU 内部通过 DEFINE_TYPES
注册 RISC-V CPU 的相关信息,其中就包括中断处理相关的操作。内部中断(Software,Timer)通过将中断处理函数 riscv_cpu_do_interrupt
注册到 tcg_ops
里面来实现,外部中断则通过 CPU 初始化函数中的 GPIO 设备配置来完成。
整体结构如下图所示:
用于依据 type_infos
进行初始化的函数如下:
// include/qom/object.h: line 849
/**
* DEFINE_TYPES:
* @type_array: The array containing #TypeInfo structures to register
*
* @type_array should be static constant that exists for the life time
* that the type is registered.
*/
#define DEFINE_TYPES(type_array) \
static void do_qemu_init_ ## type_array(void) \
{ \
type_register_static_array(type_array, ARRAY_SIZE(type_array)); \
} \
type_init(do_qemu_init_ ## type_array)
// include/qemu/module.h: line 56
#define type_init(function) module_init(function, MODULE_INIT_QOM)
// include/qemu/module.h: line 34
/* This should not be used directly. Use block_init etc. instead. */
#define module_init(function, type) \
static void __attribute__((constructor)) do_qemu_init_ ## function(void) \
{ \
register_module_init(function, type); \
}
#endif
// util/module.c: line 69
void register_module_init(void (*fn)(void), module_init_type type)
{
ModuleEntry *e;
ModuleTypeList *l;
e = g_malloc0(sizeof(*e));
e->init = fn;
e->type = type;
l = find_type(type);
QTAILQ_INSERT_TAIL(l, e, node);
}
总结
本文对两个模拟器中 RISC-V 的 Trap 定义和处理过程进行了较为详细地调研,为实际上手进行软硬件实现提供了一定的参考。
参考资料
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |
Read Album:
- 废弃 QEMU xilinx_zynq 板卡的 ignore_memory_transaction_failures
- RISC-V Linux 内核及周边技术动态第 115 期
- RISC-V Linux 内核及周边技术动态第 114 期
- 为 RISC-V OpenSBI 增加 Section GC 功能
- RISC-V Linux 内核及周边技术动态第 113 期