泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

泰晓RISC-V实验箱,转战RISC-V,开箱即用
请稍侯

RISC-V KVM 中断处理的实现(一)

XiakaiPan 创作于 2023/06/20

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 判断与触发逻辑。

软件版本

SoftwareVersion
QEMU7.1.0
Spike70f8aa01b803f4dbc0461fd7c986c1ca76d4b1d9

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 处理

当且仅当 miemip 两个 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-fault
  • TRAP:系统调用指令 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 代码中的表示方式如下图所示:

flowchart BT subgraph interrupt int[interrupt] end subgraph riscv/trap.h: abstract trap direction LR t[trap_t]---int it[insn_trap_t]-->t mt[mem_trap_t]-->t end subgraph riscv/trap.h: all exceptions tmf[trap_instruction_address_misaligned] tii[trap_illegal_instruction] tec[trap_user_ecall] oe[other exceptions, ...] tmf-->mt tii-->it tec-->t oe-.->it oe-.->mt oe-.->t end subgraph riscv/encoding.h mf[CAUSE_MISALIGNED_FETCH] ii[CAUSE_ILLEGAL_INSTRUCTION] ec[CAUSE_USER_ECALL] oc[other causes, ...] end mf-->tmf ii-->tii ec-->tec oc-->oe

下载由 Mermaid 生成的 PNG 图片

异常抛出

整个模拟器在执行过程中,通过 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 的处理过程如下图所示:

虚线表示函数对应的实现,细实线表示函数调用关系,粗实线表示参数的传递关系。

flowchart TB subgraph sim.h ss[sim_t] end subgraph execute.c ps[step] subgraph try_catch try--> ttpi[take_pending_interrupt] try-->mem[mmu]==throw mem_trap_t==>c try-->o[others]==throw insn_trap_t==>c end ps-->try_catch c[catch take_trap] end subgraph processor.cc s[sim_t::procs->step] tpi[take_pending_interrupt] ti[take_interrupt] tt[take_trap] end subgraph mmu li[load_insn] ai[access_icache] end mem-->li mem-->ai ss-->s-.->ps ttpi-.->tpi-->ti c-.->tt ttpi==throw trap_t==>c ti==interrupt==>tt

下载由 Mermaid 生成的 PNG 图片

由图可知,最终的 trap 处理是通过调用 riscv/process.cctake_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 操作:

flowchart subgraph riscv/abstract_device.h ad[abstract_device_t] end subgraph riscv/devices.h direction TB cl[clint_t]-->ad rom[rom_device_t]-->ad mem[mem_t]-->ad plic[plic_t]-->ad ns[ns16550_t]-->ad plg[mmio_plugin_device_t]-->ad subgraph bus_t d[devices:map] access[load/store] end cl-.-d rom-.-d mem-.-d plic-.-d ns-.-d plg-.-d bus_t-->ad end subgraph riscv/sim.cc:sim_t ldst[mmio_load/store]-->access end subgraph riscv/mmu.cc:mmu_t direction LR mldst[mmio_load/store]-->ldst spi[store_slow_path_intrapage]-->mldst ssp[store_slow_path]-->spi store[store, ...]-->ssp end

下载由 Mermaid 生成的 PNG 图片

而 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_tload/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 设备配置来完成。

整体结构如下图所示:

flowchart TB subgraph cpu.c define[DEFINE_TYPES: riscv_cpu_type_infos]--> ti[riscv_cpu_type_infos]-->ci[.class_init = riscv_cpu_class_init]-->tcg[riscv_tcg_ops] ti-->ii[.instance_init = riscv_cpu_init] end subgraph cpu.c ii-->igpioi[qdev_init_gpio_in]-->sirq[riscv_cpu_set_irq] end subgraph kvm.c ksirq[kvm_riscv_set_irq] end ioctl[kvm_vcpu_ioctl: KVM_INTERRUPT] sirq-->ksirq-->ioctl subgraph cpu_helper.c eint[riscv_cpu_exec_interrupt] dint[riscv_cpu_do_interrupt] end tcg-->eint-->dint tcg-->dint

下载由 Mermaid 生成的 PNG 图片

用于依据 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 定义和处理过程进行了较为详细地调研,为实际上手进行软硬件实现提供了一定的参考。

参考资料



Read Album:

Read Related:

Read Latest: