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

泰晓Linux系统盘,不用安装,即插即跑
请稍侯

RISC-V IPI 实现

sugarfillet 创作于 2024/08/27

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 寄存器值定义表如下:

InterruptException CodeDescription
10Reserved
11Supervisor software interrupt
12Reserved
13Machine software interrupt
14Reserved
15Supervisor timer interrupt
16Reserved
17Machine timer interrupt
18Reserved
19Supervisor external interrupt
110Reserved
111Machine external interrupt
112–15Reserved
1≥16Designated for platform use
00Instruction address misaligned
01Instruction 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 typetrigger funcdeal func
IPI_RESCHEDULEsmp_send_reschedulescheduler_ipi
IPI_CALL_FUNCarch_send_call_function_ipi_maskgeneric_smp_call_function_interrupt
IPI_CPU_STOPsmp_send_stopipi_stop
IPI_CPU_CRASH_STOPcrash_smp_send_stopipi_cpu_crash_stop
IPI_IRQ_WORKarch_irq_work_raiseirq_work_run
IPI_TIMERtick_broadcasttick_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 功能。

参考资料



Read Album:

Read Related:

Read Latest: