diff --git a/_posts/2024-08-27-11-58-54-riscv-ipi.md b/_posts/2024-08-27-11-58-54-riscv-ipi.md new file mode 100644 index 000000000..44c11e91e --- /dev/null +++ b/_posts/2024-08-27-11-58-54-riscv-ipi.md @@ -0,0 +1,316 @@ +--- +layout: post +author: 'sugarfillet' +title: 'RISC-V IPI 实现' +draft: false +album: 'RISC-V Linux' +license: 'cc-by-nc-nd-4.0' +permalink: /riscv-ipi/ +description: 'RISC-V SMP IPI 实现分析' +category: + - 开源项目 + - Risc-V +tags: + - Linux + - RISC-V + - IPI + - SMP + - 多核同步 + - 负载均衡 + - 核间中断 +--- + +> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.1 - [codeinline pangu epw] +> Author: sugarfillet +> Date: 2023/02/09 +> Revisor: Falcon falcon@tinylab.org +> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux) +> Proposal: [RISC-V Linux SMP 技术调研与分析](https://gitee.com/tinylab/riscv-linux/issues/I5MU96) +> Sponsor: PLCT Lab, ISCAS + + +## 前言 + +IPI 全称为 Inter-Processor Interrupt,即处理器之间的中断,在此基础上,可以在 SMP 系统中实现多核同步,多核负载均衡功能。本文对 RISC-V Linux 中的 IPI 实现进行分析。 + +**说明**: + +* 本文的 Linux 版本采用 `Linux v6.2-rc5` + +## RISC-V 中断类型 + +RISC-V 处理器将异常控制流称之为 trap,同时为可以支持 trap 处理的两种特权模式(M-mode、S-mode)分别定义一组寄存器用于执行 trap 处理。当 trap 发生时,Hart 自动执行如下流程,之后 Hart 会跳转到 `xtvec` 设置的异常处理函数。 + +- 发生异常的指令的 PC 被存入 xepc,且 PC 被设置为 xtvec +- xcause 根据 trap 类型设置,xtval 被设置成出错的地址或者其它特定异常的信息字 +- 把 xstatus CSR 中的 XIE 置零,屏蔽中断,且 XIE 之前的值被保存在 XPIE 中 +- 发生 trap 时的权限模式被保存在 xstatus 的 XPP 域,然后设置当前模式为 X 模式 + +> 注:上文的 x 和 X 可以替换为对应模式首字符来理解:S-mode (s 和 S)、M-mode (m 和 M) + +RISC-V privileged 文档(文档版本为 20211203)的 Table 3.6 和 Table 4.2 分别对 mcause 和 scause 寄存器的值做了说明,从这两个表中,我们可以知道:trap 分为中断和异常两种,当产生中断时,`xcause` 的 Interupt 位置 1,异常时置 0。中断在两种特权模式下又分为软件中断、时钟中断、外部中断。mcause 寄存器值定义表如下: + +| Interrupt | Exception Code | Description | +|-----------|----------------|--------------------------------| +| 1 | 0 | Reserved | +| 1 | 1 | Supervisor software interrupt | +| 1 | 2 | Reserved | +| 1 | 3 | Machine software interrupt | +| 1 | 4 | Reserved | +| 1 | 5 | Supervisor timer interrupt | +| 1 | 6 | Reserved | +| 1 | 7 | Machine timer interrupt | +| 1 | 8 | Reserved | +| 1 | 9 | Supervisor external interrupt | +| 1 | 10 | Reserved | +| 1 | 11 | Machine external interrupt | +| 1 | 12–15 | Reserved | +| 1 | ≥16 | Designated for platform use | +| 0 | 0 | Instruction address misaligned | +| 0 | 1 | Instruction access fault | +| .. | .. | .. | + +M-mode 下,软件中断通过编程 CLINT.MSIP(Core Local Interruptor)寄存器来触发;时钟中断通过编程 CLINT.MTIMECMP 和 CLINT.MTIME 寄存器来触发;而外部中断是通过 PLIC(Platform-Level Interrupt Controller)控制器转发外设中断到 Hart。对 M-mode 的中断处理与应用感兴趣的同学可以参考这个 [课程][1],课程里通过 CLINT 时钟中断实现抢占式调度,CLINT 软件中断实现兼容的协作式调度,PLIC 外部中断实现串口输入的功能。 + +S-mode 下,外部中断与 M-mode 类似通过 PLIC 转发外部设备中断;时钟中断在 Linux 中通过 riscv-timer 时钟源来触发,此时钟源底层通过 SBI Timer 扩展或者编程 Sstc 相关寄存器来实现,这里不做展开,详细可参考(`drivers/clocksource/timer-riscv.c`);而软件中断在 Linux 中主要用于核间的 IPI,底层通过调用 SBI IPI 扩展的 `sbi_send_ipi()` 接口触发。 + +## HLIC PLIC CLINT + +如内核文档 `Documentation/devicetree/bindings/interrupt-controller/riscv,cpu-intc.txt` 中所述,RISC-V 每个 Hart 有关中断控制的本地寄存器由各自的 HLIC (Hart-Level Interrupt Controller) 进行管理。所有的中断类型最终都会路由到 HLIC 进行处理,包括 PLIC 转发的外部中断,CLINT/riscv-timer 产生的时钟中断,以及 CLINT 产生的或者 HLIC 自己管理的软件中断。我们以 `arch/riscv/boot/dts/starfive/jh7100.dtsi` 为例来说明互相的连接关系: + +- 在每个 CPU 节点中定义一个 HLIC 控制器,`compatible` 选项定义为 "riscv,cpu-intc"。 +- plic 节点定义一个 "sifive,plic-1.0.0" 中断控制器,通过 "interrupts-extended" 映射 11 号和 9 号中断到每个 HLIC 的外部中断(这里的 11 和 9 分别代表 M-mode 和 S-mode 外部中断在 xcause 对应的异常码)。 +- clint 节点定义一个 "sifive,clint0" 时钟设备,该节点通过 "interrupts-extended" 映射 3 号和 7 号中断到每个 HLIC 的软件中断和时钟中断(这里的 3 和 7 分别代表 M-mode 软件中断和时钟中断在 mcasue 对应的异常码)。 +- riscv-timer 设备在此设备的初始化流程中的 `irq_create_mapping(domain, RV_IRQ_TIMER);` 调用中映射时钟中断到 HLIC 的中断域(这里的 `RV_IRQ_TIMER` (5) 代表 S-mode 时钟中断在 xcause 对应的异常码),该设备不体现在 dts 中。 +- 软件中断则由 HLIC 自己来管理,此中断不体现在 dts 中。 + +```c +// arch/riscv/boot/dts/starfive/jh7100.dtsi + + U74_0: cpu@0 { + compatible = "sifive,u74-mc", "riscv"; + ... + mmu-type = "riscv,sv39"; + riscv,isa = "rv64imafdc"; + + cpu0_intc: interrupt-controller { + compatible = "riscv,cpu-intc"; + interrupt-controller; + #interrupt-cells = <1>; + }; + }; + + clint: clint@2000000 { + compatible = "starfive,jh7100-clint", "sifive,clint0"; + reg = <0x0 0x2000000 0x0 0x10000>; + interrupts-extended = <&cpu0_intc 3 &cpu0_intc 7 + &cpu1_intc 3 &cpu1_intc 7>; + }; + + plic: interrupt-controller@c000000 { + compatible = "starfive,jh7100-plic", "sifive,plic-1.0.0"; + reg = <0x0 0xc000000 0x0 0x4000000>; + interrupts-extended = <&cpu0_intc 11 &cpu0_intc 9 + &cpu1_intc 11 &cpu1_intc 9>; + interrupt-controller; + #address-cells = <0>; + #interrupt-cells = <1>; + riscv,ndev = <133>; + }; +``` + +> CLINT 设备提供时钟中断和软件中断不会和 RISC-V-timer 设备的时钟中断以及 HLIC 管理的软件中断冲突么? + +如 RISC-V Kconfig 文件所示,CLINT 设备只在不支持 MMU 的 RISC-V 处理器上运行 M-mode Linux 的环境中使能,并提供 `clint_ipi_ops` 操作集来提供软件中断的支持,而 S-mode Linux 的默认的时钟源为 riscv-timer。所以上文的 dts 以及 QEMU 默认的 dts 中,"mmu-type" 选项和 "clint" 节点似乎不应该同时存在,同时存在的结果就是 clint 设备不工作。 + +```c +// arch/riscv/Kconfig.socs :33 + +config SOC_VIRT + bool "QEMU Virt Machine" + select CLINT_TIMER if RISCV_M_MODE + ... + help + This enables support for QEMU Virt Machine. + +// arch/riscv/Kconfig : 14 + +config RISCV + ... + select CLINT_TIMER if !MMU + ... +``` + +总之,RISC-V 系统的中断连接如下,其中 S-mode Linux 不访问 CLINT 设备: + +``` + ------- +| |<--> Soft <----------------------v +| HLIC |<--- Timer ----- [riscv-timer] [CLINT] +| |<--- Exter ----- [PLIC] --|<--- Int1 + ------- |<--- Int2 + +``` + +## HLIC 初始化 + +HLIC 在 RISC-V Linux 中通过 "riscv,cpu-intc" 中断控制器来实现,其初始化函数 `riscv_intc_init()` 在 `init_IRQ()` 阶段被调用。 + +此函数调用 `irq_domain_add_linear()` 注册中断域,并设置根中断处理函数为 `riscv_intc_irq()`,之后通过设置热插拔状态 `CPUHP_AP_IRQ_RISCV_STARTING` 在热插拔线程的 `ONLINE` 阶段调用 `riscv_intc_cpu_starting()` 函数以开启当前 CPU 的 sie 寄存器的 SSIE 位,表示开启 S-mode 的软件中断。关键代码如下: + +```c +// drivers/irqchip/irq-riscv-intc.c : 95 + +IRQCHIP_DECLARE(riscv, "riscv,cpu-intc", riscv_intc_init); + +riscv_intc_init() + // only need to do INTC initialization for boot hart + + irq_domain_add_linear(node, BITS_PER_LONG, &riscv_intc_domain_ops, NULL); // Allocate and register a linear revmap irq_domain. + __irq_domain_add + init domain->* domain->ops = ops; + debugfs_add_domain_dir(domain); + list_add(&domain->link, &irq_domain_list); + + set_handle_irq(&riscv_intc_irq); // set root irq handler + + cpuhp_setup_state(CPUHP_AP_IRQ_RISCV_STARTING, + "irqchip/riscv/intc:starting", + riscv_intc_cpu_starting, // csr_set(CSR_IE, BIT(RV_IRQ_SOFT)); + riscv_intc_cpu_dying); // csr_clear(CSR_IE, BIT(RV_IRQ_SOFT)); +``` + +HIIC 中断处理函数 `riscv_intc_irq()`,获取 `pt_regs` 的 cause 成员(即 scause 寄存器),如果为软件中断则执行 `handle_IPI()` 进行 IPI 的处理,否则调用 `generic_handle_domain_irq()` 执行时钟中断和外部中断,此函数最终会调用到具体的中断处理函数,比如:RISC-V-timer 为 `riscv_timer_interrupt()`。 + +```c +// drivers/irqchip/irq-riscv-intc.c : 139 + +riscv_intc_irq(struct pt_regs *regs) + cause = regs->cause & ~CAUSE_IRQ_FLAG; + case RV_IRQ_SOFT: + handle_IPI(regs); + default: + generic_handle_domain_irq(intc_domain, cause); + return handle_irq_desc(irq_resolve_mapping(domain, hwirq)); + // find irq_desc in ` irq_domain_get_irq_data(domain, hwirq)` by cause + generic_handle_irq_desc(desc); + desc->handle_irq(desc); // handle_percpu_devid_irq + desc->action->handler() + +// drivers/clocksource/timer-riscv.c : 195 + +riscv_timer_init_dt() + riscv_clock_event_irq = irq_create_mapping(domain, RV_IRQ_TIMER); + error = request_percpu_irq(riscv_clock_event_irq, riscv_timer_interrupt, riscv-timer, &riscv_clock_event); +``` + +## IPI 中断的触发与处理 + +RISC-V Linux 中提供 `send_ipi_mask()`、`send_ipi_single()` 两个函数用于在指定 `cpu` 或者 `cpumask` 触发 `op` 参数指定的 IPI 中断事件类型。这两个函数首先在 percpu 变量 `ipi_data[cpu].bits` 中设置 IPI 事件类型表示触发此类事件,之后调用 SBI 的 IPI 扩展接口 `sbi_send_ipi()` 发送 IPI。关键代码如下: + +```c +// arch/riscv/kernel/smp.c : 120 + +send_ipi_single(int cpu, enum ipi_message_type op) +send_ipi_mask(const struct cpumask *mask, enum ipi_message_type op) + set_bit(op, &ipi_data[cpu].bits); + ipi_ops->ipi_inject(mask); // sbi_ipi_ops.ipi_inject sbi_send_cpumask_ipi + if SBI_EXT_IPI __sbi_send_ipi_v02 + hartid = cpuid_to_hartid_map(cpuid); + ret = sbi_ecall(SBI_EXT_IPI, SBI_EXT_IPI_SEND_IPI, hmask, hbase, 0, 0, 0, 0); // sbi_send_ipi() +``` + +RISC-V SBI 规范(规范版本为 1.0-rc1)第六章的 "sPI: s-mode IPI" 扩展中描述了 `sbi_send_ipi()` 接口,当调用此接口时会向 `hart_mask` 中定义的所有 Hart 发送 IPI,而目标 Hart 在接收时以 S 模式的软件中断处理。规范原文引用如下: + +> Send an inter-processor interrupt to all the harts defined in hart_mask. Interprocessor interrupts +> manifest at the receiving harts as the supervisor software interrupts. + +`handle_IPI()` 函数负责执行 IPI 中断的处理,从 percpu 变量 `ipi_data[cpu].bits` 中取出 IPI 事件类型,调用不同的处理函数进行事件处理,比如:`IPI_CALL_FUNC` 事件的处理函数为 `generic_smp_call_function_interrupt()` 函数。同时使用 `ipi_data[cpu].stats[]` 数组进行不同 IPI 事件的计数,从而可以在 `/proc/interrupts` 文件中查询。 + +```c +// arch/riscv/kernel/smp.c :154 + +handle_IPI() + + unsigned long *pending_ipis = &ipi_data[cpu].bits; + unsigned long *stats = ipi_data[cpu].stats; // show_ipi_stats /proc/interrupts + ops = xchg(pending_ipis, 0); + if (ops & (1 << IPI_RESCHEDULE)) { + stats[IPI_RESCHEDULE]++; + scheduler_ipi(); + } + IPI_CALL_FUNC + generic_smp_call_function_interrupt(); + IPI_CPU_STOP + ipi_stop(); + IPI_CPU_CRASH_STOP + ipi_cpu_crash_stop(cpu, get_irq_regs()); + IPI_IRQ_WORK + irq_work_run(); + IPI_TIMER + tick_receive_broadcast(); +``` + +## IPI 中断事件 + +在上文的 IPI 中断触发与处理机制的基础上,RISC-V Linux 提供 6 种 IPI 中断事件的支持,以实现具体的跨 CPU 功能。这些事件在 `ipi_message_type` 枚举与 `ipi_names` 数组中定义如下: + +``` +static const char * const ipi_names[] = { + [IPI_RESCHEDULE] = "Rescheduling interrupts", + [IPI_CALL_FUNC] = "Function call interrupts", + [IPI_CPU_STOP] = "CPU stop interrupts", + [IPI_CPU_CRASH_STOP] = "CPU stop (for crash dump) interrupts", + [IPI_IRQ_WORK] = "IRQ work interrupts", + [IPI_TIMER] = "Timer broadcast interrupts", +}; +``` + +- IPI_RESCHEDULE + + SMP 系统中,调度器更加倾向于把任务负载分摊到每个 CPU 上,不至于出现单核繁忙的情况,那么当调度器把任务负载从一个 CPU 卸载到其他的空闲 CPU 时,就会触发 `IPI_RESCHEDULE` 事件,而目标 CPU 中当前任务如果设置了重新调度位,则执行调度。`IPI_RESCHEDULE` 事件通过调用 `smp_send_reschedule()` 函数来触发,而在目标 CPU 上的 IPI 处理函数 `handle_IPI()` 中则调用 `scheduler_ipi()` 函数选择性地执行重新调度。 + +- IPI_CALL_FUNC + + Linux 提供 `smp_call_function()` 函数用来在多个 CPU 上执行函数,比如:在 ftrace 更新全局的跟踪函数后会调用此接口在其他 CPU 上执行 `smp_rmb()` 用于通知其他 CPU 此全局函数的更新,在 sysrq 中也会调用此接口打印每个 CPU 上的调用栈等相关信息。`smp_call_function()` 接口把要执行的函数及其参数存储在目标 CPU 的调用函数队列 `call_single_queue` 上,之后会调用 `arch_send_call_function_ipi_mask()` 触发 `IPI_CALL_FUNC` 事件。而在目标 CPU 上的 IPI 处理过程中则调用 `generic_smp_call_function_interrupt()` 函数从调用函数队列中取出**调用函数**去执行。 + +- IPI_CPU_STOP + + `IPI_CPU_STOP` 事件在 `panic()` 流程中调用 `smp_send_stop()` 触发,用于停止其他 CPU,而目标 CPU 处理此事件时,则通过 `ipi_stop()` 执行 wfi 循环。 + +- IPI_CPU_CRASH_STOP + + `IPI_CPU_CRASH_STOP` 事件在 crash 之后执行 kexec 之前时调用 `crash_smp_send_stop()` 触发,用来停止未 crash 的 CPU 并保存其寄存器,而目标 CPU 处理此事件时,调用 `ipi_cpu_crash_stop(cpu, get_irq_regs())` 保存进程信息和寄存器信息到 core 文件,之后调用 `cpu_ops[cpu]->cpu_stop()` 进入 SBI HSM 扩展的 STOP 状态。 + +- IPI_IRQ_WORK + + Irq Work 机制用于在中断上下文中执行回调函数,`IPI_IRQ_WORK` 事件在 irq_work 入队列时调用 `arch_irq_work_raise()` 触发,而目标 CPU 处理此事件时,调用 `irq_work_run()` 执行 irq_work 的回调函数。 + +- IPI_TIMER + + 当 CPU 进入 idle 状态时可能会关闭本地时钟,系统时钟通过调用 `tick_broadcast()` 触发 `IPI_TIMER` 事件让 CPU 从 idle 状态退出,而目标 CPU 处理此事件时,调用 `tick_receive_broadcast()` 执行当前 CPU 上的时钟源(clock event)的时钟处理函数。 + +以上 IPI 事件的触发和处理函数整理成下表,以便查询: + +| IPI type | trigger func | deal func | +|--------------------|----------------------------------|-------------------------------------| +| IPI_RESCHEDULE | smp_send_reschedule | scheduler_ipi | +| IPI_CALL_FUNC | arch_send_call_function_ipi_mask | generic_smp_call_function_interrupt | +| IPI_CPU_STOP | smp_send_stop | ipi_stop | +| IPI_CPU_CRASH_STOP | crash_smp_send_stop | ipi_cpu_crash_stop | +| IPI_IRQ_WORK | arch_irq_work_raise | irq_work_run | +| IPI_TIMER | tick_broadcast | tick_receive_broadcast | + +## 小结 + +RISC-V Linux 中通过 HLIC 来集中处理三种中断类型(软件中断、时钟中断、外部中断),其中 S-mode 的软件中断主要用于 IPI。通过调用 SBI IPI 扩展接口 `sbi_send_ipi()` 向目标 Hart 发送 IPI,在目标 Hart 在收到 IPI 中断时,HLIC 的中断处理函数 `riscv_intc_irq()` 以 S 模式的软件中断来处理。在此基础上又定义 6 种不同的 IPI 事件以实现各种具体的跨 CPU 功能。 + +## 参考资料 + +- [riscv-operating-system-mooc][1] +- [riscv irq handle][2] +- [time framework][3] + +[1]: https://gitee.com/unicornx/riscv-operating-system-mooc +[2]: https://tinylab.org/riscv-irq-analysis-part3-interrupt-handling-cpu/ +[3]: http://www.wowotech.net/timer_subsystem/time-subsyste-architecture.html