[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
RISC-V IPI 实现
Corrector: TinyCorrect v0.1 - [codeinline pangu epw] Author: sugarfillet sugarfillet@yeah.net Date: 2023/02/09 Revisor: Falcon falcon@tinylab.org Project: RISC-V Linux 内核剖析 Proposal: RISC-V Linux SMP 技术调研与分析 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 的中断处理与应用感兴趣的同学可以参考这个 课程,课程里通过 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 中。
// 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 设备不工作。
// 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 的软件中断。关键代码如下:
// 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()
。
// 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。关键代码如下:
// 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
文件中查询。
// 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 功能。
参考资料
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |
Read Album:
- TinyBPT 和面向 buildroot 的二进制包管理服务(1):设计简介与框架
- RISC-V Linux 内核及周边技术动态第 118 期
- RISC-V Linux 内核及周边技术动态第 117 期
- 实时分析工具 rtla timerlat 介绍(二):延迟测试原理
- 实时分析工具 rtla timerlat 介绍(一):交叉编译及使用