引言
在现代多核处理器系统中,处理器核心的组织方式和任务分配策略直接影响系统的软件设计与通信方式。按照核心间的共享程度,多核系统通常可以分为 对称多处理(SMP, Symmetric Multi-Processing) 和 非对称多处理(AMP, Asymmetric Multi-Processing) 两类。
SMP 架构
SMP 系统中的所有核心平等地共享同一份内存和 I/O 资源。每个核心运行相同的操作系统实例,一个核通过_start初始化以后,唤醒其他的核做secondary_start_kernel,可以执行任意任务,并通过统一的调度器协调任务执行。
AMP 架构
AMP 系统中,各个核心独立运行自己的操作系统实例或裸机程序,它们之间可能不共享内存,甚至运行不同类型的操作系统。典型应用包括 异构多核 SoC,如 Cortex-A 与 Cortex-M 核组合,或者 CPU 与 DSP 的混合系统。
SMP 与 AMP 的主要区别
特性 | SMP | AMP |
---|---|---|
核心对等性 | 所有核心对等 | 核心独立 |
操作系统 | 单实例,多核心共享 | 每核心独立操作系统 |
内存访问 | 全部共享 | 部分共享或不共享 |
通信方式 | 共享内存 + IPI 中断 | 消息通道(RPMsg / Mailbox) |
通信目标 | 任务同步、调度、资源管理 | 命令下发、事件通知、数据传输 |
SMP多对称处理器间的通讯
应用场景
调度应用
场景:当一个 CPU 修改了任务优先级 / 唤醒了一个任务,但该任务更适合在另一个 CPU 上执行时。
做法:当前 CPU 向目标 CPU 发送 IPI,目标 CPU 立即触发调度器,切换到这个任务
跨核函数调用(smp_call_function)
场景:某个内核子系统需要在所有 CPU 上执行一段代码,比如linux下的fiq-debugger机制:当某个核卡死的时候依然可以通过smp_call_function让该卡死的核执行传入的回调函数,在调试的时候非常好用,比如我的代码导致了某一个核卡死了,可以通过smp_call_function的机制让卡死的核dump一些信息
做法:一个 CPU 发 IPI 给其他 CPU,让它们都执行一个回调函数。
核间通知 / 快速消息传递
- 场景:一个 CPU 发现了全局事件(如中断、数据更新),需要立即通知其他 CPU。
- 做法:通过 IPI 发送“消息”或触发事件处理函数。
在linux下多个a核间的通讯相对来说比较简单,主要是通过向需要通讯的核发一个ipi中断来实现,ipi中断以及中断处理函数在初始化的时候进行注册
IPI 的全称是 Inter-Processor Interrupt,中文一般叫 处理器间中断 或 核间中断。
它的作用就是在 **多核处理器 ** 系统里,让一个 CPU 主动“打断”另一个 CPU,从而实现 跨核通信与协作。
初始化前瞻
先看一些定义: linux内核支持的ipi调用
1 | enum ipi_msg_type { |
为每个核注册ipi中断(也就是gic控制器的SGI中断–注册为软件中断):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28void __init set_smp_ipi_range(int ipi_base, int n)
{
int i;
WARN_ON(n < MAX_IPI);
nr_ipi = min(n, MAX_IPI);
//遍历所有支持的sgi中断号
for (i = 0; i < nr_ipi; i++) {
int err;
//为每个cpu注册相同的中断处理函数
err = request_percpu_irq(ipi_base + i, ipi_handler,
"IPI", &irq_stat);
WARN_ON(err);
//把中断描述符存在本地静态变量中
ipi_desc[i] = irq_to_desc(ipi_base + i);
irq_set_status_flags(ipi_base + i, IRQ_HIDDEN);
/* The recheduling IPI is special... */
if (i == IPI_RESCHEDULE)
__irq_modify_status(ipi_base + i, 0, IRQ_RAW, ~0);
}
ipi_irq_base = ipi_base;
/* Setup the boot CPU immediately */
ipi_setup(smp_processor_id());
}看一下ipi的处理函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69//看一下都支持哪些中断处理
static void do_handle_IPI(int ipinr)
{
unsigned int cpu = smp_processor_id();
if ((unsigned)ipinr < NR_IPI)
trace_ipi_entry_rcuidle(ipi_types[ipinr]);
switch (ipinr) {
case IPI_WAKEUP:
break;
case IPI_TIMER:
//看起来是时钟相关的
tick_receive_broadcast();
break;
case IPI_RESCHEDULE:
//执行调度
scheduler_ipi();
break;
case IPI_CALL_FUNC:
//执行某个函数回调
generic_smp_call_function_interrupt();
break;
case IPI_CPU_STOP:
//暂停cpu
ipi_cpu_stop(cpu);
break;
case IPI_IRQ_WORK:
//执行延迟的 irq_work 任务
irq_work_run();
break;
case IPI_COMPLETION:
//通知ipi发起者某个操作完成了
ipi_complete(cpu);
break;
case IPI_CPU_BACKTRACE:
//打印backtrace
printk_nmi_enter();
nmi_cpu_backtrace(get_irq_regs());
printk_nmi_exit();
break;
default:
pr_crit("CPU%u: Unknown IPI message 0x%x\n",
cpu, ipinr);
break;
}
if ((unsigned)ipinr < NR_IPI)
trace_ipi_exit_rcuidle(ipi_types[ipinr]);
}
static irqreturn_t ipi_handler(int irq, void *data)
{
//通过传入相对的中断号码
do_handle_IPI(irq - ipi_irq_base);
return IRQ_HANDLED;
}
案例分析
这里拿IPI_CALL_FUNC来举例,场景是我们需要唤醒某个cpu执行一个函数(比如dump当前的reg):ps – 为了方便理解整体的链路,代码部分我进行了一定的删减,因此与实际上的linux的代码有差异,感兴趣可移步至源码kernel/smp.c与arch/arm/kernel/smp.c中阅读
处理部分主要分为两个链路,一个是发ipi的链路,一个是处理ipi的链路,先看发ipi中断的链路:
1 | //这里其实就是处理所有发送ipi中断请求的接口,属于芯片架构层(这里会调用不同中断处理器的架构注册的回调函数) |
既然中断已经发送过去了就需要看一下对应的中断处理函数,也就是generic_smp_call_function_interrupt:代码依旧只保留了关键逻辑
1 | static void flush_smp_call_function_queue(bool warn_cpu_offline) |
for test
我们可以写一段测试程序来验证一下我们上面所说的框架:
1 |
|
这里只是做了一行打印,但是我们在回调函数中的自由度是非常高的,所以可以做一些非常有意思的事情,例如dump当前cpu的一些信息
总结
所以总的来说,linux下的smp间的通信就是基于下发ipi中断来实现的,整体的流程也非常的简单:
初始化阶段
内核启动时,会调用
set_smp_ipi_range()
为 每个 CPU 注册好所有支持的 IPI 中断号(SGI → 对应
ipi_handler
)所有 IPI 的入口统一落到
ipi_handler()
→do_handle_IPI(ipinr)
通信阶段
某个 CPU 需要和别的 CPU 通讯时 → 调用
smp_cross_call(cpumask, ipi_nr)
本质就是往目标 CPU 发一个 IPI 中断(SGI)
处理阶段
目标 CPU 收到 IPI → 进入统一的
ipi_handler()
根据 IPI 类型 分发到不同的处理逻辑:
IPI_CALL_FUNC
→ 执行generic_smp_call_function_interrupt()
,在目标 CPU 上调用函数IPI_RESCHEDULE
→ 执行scheduler_ipi()
→ 触发一次schedule()
调度IPI_CPU_STOP
→ 停止 CPUIPI_IRQ_WORK
→ 执行 irq_workIPI_CPU_BACKTRACE
→ 打印 backtrace
举一反三(线程切换例子)
如果我们想让某个 CPU 立刻执行一次线程切换:
1 | // 发一个 IPI_RESCHEDULE 到目标 CPU -- 可能会封装好api,但是最终都会走到这里 |
在目标 CPU 上最终执行的就是:
1 | case IPI_RESCHEDULE: |
所以完全可以推断:**IPI_RESCHEDULE 的处理函数就是触发 schedule()**。
图示如下:
1 | graph TD |
AMP非对称多核的通讯
由于a核和m核心之间通信方式有很多,大部分要看soc内部的硬件实现,比如mailbox,共享内存等等的机制,对于机制的具体实现本节我们不过多关注,但是linux提供了一个rpmsg的框架,用于向用户屏蔽底层差异。源码位于 driver/rpmsg下
rpmsg在linux中作为一个单独的总线实际上和其他类似platform,iic,pcie总线类似,总线的主体都是由probe,remove,match组成的。
先看一下这个结构:
1 | /** |
使用方法
我们以rockchip的核间通讯的驱动为例:
首先会注册一个platform总线的驱动框架用于管理自己的设备资源:
1 | static const struct of_device_id rockchip_rpmsg_match[] = { |
在probe中会注册一个virtio设备,同时注册mailbox通道,将virtio的rx tx 接口设置为mailbox,我们可以理解mailbox是在virtio的下一层,是实际和硬件交互的层级:
1 | //这是我从probe中提取出来的一部分 |
在a核上我们可以将m核作为一个虚拟设备,使用virtio去管理,所以rgmsg基于virtio注册了一个通用的驱动,位于drivers/rpmsg/virtio_rpmsg_bus.c下:
1 |
|
当我们的soc中需要做m核和a核的通讯的时候,我们需要注册一个rpmsg driver以及一个device,而在上面的rpmsg_probe就会执行device的注册,此时我们再需要注册一个rpmsg的driver就可以让这一套机制运行起来了,由于virtio框架内容比较多,这里不纠结底层的实现了,大致流程可以从如下框图看出:
1 | sequenceDiagram |