文章目录
- [1. 前言](#1. 前言)
- [2. 背景](#2. 背景)
- [3. SMP 架构下 CPU 核间通信](#3. SMP 架构下 CPU 核间通信)
-
- [3.1 ARM 架构下的 IPI_* 消息实现](#3.1 ARM 架构下的 IPI_* 消息实现)
-
- [3.1.1 IPI_TIMER](#3.1.1 IPI_TIMER)
- [3.1.2 IPI_RESCHEDULE](#3.1.2 IPI_RESCHEDULE)
- [3.1.3 IPI_CALL_FUNC](#3.1.3 IPI_CALL_FUNC)
- [3.1.4 其它 IPI 消息](#3.1.4 其它 IPI 消息)
- [3.2 其它架构 IPI 实现](#3.2 其它架构 IPI 实现)
- [4. 观察 IPI 中断发生次数](#4. 观察 IPI 中断发生次数)
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 背景
本文以 Linux 4.14 内核
,ARM 架构
,GIC 中断芯片
为背景,简要分析 SMP
架构下 CPU 核间通信
。
3. SMP 架构下 CPU 核间通信
Linux 系统下,SMP
架构 CPU 核间通过预定义的 IPI(Inter-Processor Interrupts)
中断来进行通信:
c
enum ipi_msg_type {
IPI_WAKEUP,
/* 定时器广播消息 */
IPI_TIMER,
/*
* 远程 CPU 执行任务调度。
* 当 被唤醒进程 不是运行于本地 CPU 上时,就需要给 被唤醒进程 所在的远程
* CPU 发送 IPI 中断,让远程 CPU 发起调度。
* 在接收到 IPI 时无论远程 CPU 是否处于空闲态,被唤醒进程 都会在 IPI 中断
* 进入到内核态时得到执行。
*/
IPI_RESCHEDULE,
/* 在指定 CPU 集合上运行函数 */
IPI_CALL_FUNC,
/*
* 远程 CPU 停止运行。
* 先将指定的远程 CPU 从在线 CPU 位图中删除,然后将
* D,A,I,F 中断屏蔽,最后处理器进入低功耗循环。
*/
IPI_CPU_STOP,
IPI_IRQ_WORK,
IPI_COMPLETION,
IPI_CPU_BACKTRACE,
/*
* SGI8-15 can be reserved by secure firmware, and thus may
* not be usable by the kernel. Please keep the above limited
* to at most 8 entries.
*/
};
上面的 IPI_*
消息是 Linux
为 SMP
架构下 CPU 核间通信
定义的、架构无关的消息类型,Linux
负责定义这些消息的语义;而这些消息语义的具体实现,依赖于具体的硬件架构类型。本文以 ARM
架构为例,举例说明几个 IPI_*
消息的实现。
3.1 ARM 架构下的 IPI_* 消息实现
ARM
架构下通过 GIC
芯片生成 SGI(Software-generated interrupt)
中断,来实现 IPI_*
消息的语义,其过程为:通过 GIC
生成一个 SGI
中断,发送给目标 CPU 集合,然后目标 CPU 处理 SGI
中断。
3.1.1 IPI_TIMER
如 BOOT CPU
发送定时器广播事件
,唤醒系统中的其它非 BOOT CPU
。先生成一个编码了 IPI_TIMER 信息的 SGI 中断,发送给目标 CPU
:
c
/* arch/arm/kernel/smp.c */
#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
void tick_broadcast(const struct cpumask *mask)
{
smp_cross_call(mask, IPI_TIMER);
}
#endif
static void smp_cross_call(const struct cpumask *target, unsigned int ipinr)
{
trace_ipi_raise_rcuidle(target, ipi_types[ipinr]);
__smp_cross_call(target, ipinr); /* gic_raise_softirq(), ... */
}
c
#ifdef CONFIG_SMP
static void gic_raise_softirq(const struct cpumask *mask, unsigned int irq)
{
...
/*
* GIC 生成一个 SGI 中断:
* map << 16: 接收 SGI 中断的目标 CPU 掩码
* @irq : 生成的 SGI 中断编号(如 IPI_TIMER)
*/
writel_relaxed(map << 16 | irq, gic_data_dist_base(&gic_data[0]) + GIC_DIST_SOFTINT);
...
}
目标 CPU 处理编码了 IPI_TIMER
信息的 SGI
中断:
c
gic_handle_irq()
handle_IPI(irqnr, regs);
c
/* arch/arm/kernel/smp.c */
void handle_IPI(int ipinr, struct pt_regs *regs)
{
...
if ((unsigned)ipinr < NR_IPI) {
...
/* 统计 IPI 中断次数,可从 /proc/interrupts 观察到 */
__inc_irq_stat(cpu, ipi_irqs[ipinr]);
}
switch (ipinr) {
...
#ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST
case IPI_TIMER:
irq_enter();
tick_receive_broadcast();
irq_exit();
break;
#endif
...
}
...
}
3.1.2 IPI_RESCHEDULE
IPI_RESCHEDULE
用于将进程调度到非当前 CPU
运行:
c
void smp_send_reschedule(int cpu)
{
smp_cross_call(cpumask_of(cpu), IPI_RESCHEDULE); /* 流程同 IPI_TIMER */
}
看一下处理 IPI_RESCHEDULE
中断的处理流程:
c
gic_handle_irq()
handle_IPI(irqnr, regs)
switch (ipinr) {
...
case IPI_RESCHEDULE:
scheduler_ipi();
break;
...
}
void scheduler_ipi(void)
{
...
irq_enter();
sched_ttwu_pending();
...
irq_exit();
}
3.1.3 IPI_CALL_FUNC
IPI_CALL_FUNC
用来在指定 CPU 上运行函数,如 smp_call_function_single()
在指定的 CPU 核上运行回调函数:
c
/* include/linux/smp.h */
int smp_call_function_single(int cpuid, smp_call_func_t func, void *info,
int wait);
c
/* kernel/smp.c */
int smp_call_function_single(int cpu, smp_call_func_t func, void *info,
int wait)
{
call_single_data_t *csd;
call_single_data_t csd_stack = {
/* CSD_FLAG_SYNCHRONOUS 标志,指示 等待 smp call 回调执行完成再返回 */
.flags = CSD_FLAG_LOCK | CSD_FLAG_SYNCHRONOUS,
};
int this_cpu;
int err;
/*
* prevent preemption and reschedule on another processor,
* as well as CPU removal
*/
this_cpu = get_cpu();
...
csd = &csd_stack;
if (!wait) {
csd = this_cpu_ptr(&csd_data);
csd_lock(csd);
}
err = generic_exec_single(cpu, csd, func, info);
if (wait)
csd_lock_wait(csd); /* 等待 回调 执行完成 */
put_cpu();
return err;
}
static int generic_exec_single(int cpu, call_single_data_t *csd,
smp_call_func_t func, void *info)
{
if (cpu == smp_processor_id()) { /* 在当前 CPU 执行,不用发起 IPI */
/*
* We can unlock early even for the synchronous on-stack case,
* since we're doing this from the same CPU..
*/
csd_unlock(csd);
local_irq_save(flags);
func(info); /* remote_function(), ... */
local_irq_restore(flags);
return 0;
}
/* 在非当前 CPU 执行,发起 IPI */
...
csd->func = func;
csd->info = info;
/*
* 通过 IPI 中断,发起远程 CPU 函数调用。
* 回调 @csd 添加到 @cpu 的 回调列表 @call_single_queue 。
*/
if (llist_add(&csd->llist, &per_cpu(call_single_queue, cpu)))
arch_send_call_function_single_ipi(cpu);
return 0;
}
c
void arch_send_call_function_single_ipi(int cpu)
{
smp_cross_call(cpumask_of(cpu), IPI_CALL_FUNC);
}
处理 IPI_CALL_FUNC
中断,在目标 CPU 上调用回调:
c
gic_handle_irq()
handle_IPI(irqnr, regs)
switch (ipinr) {
...
case IPI_CALL_FUNC:
irq_enter();
generic_smp_call_function_interrupt();
irq_exit();
break;
...
}
c
#define generic_smp_call_function_interrupt \
generic_smp_call_function_single_interrupt
c
void generic_smp_call_function_single_interrupt(void)
{
flush_smp_call_function_queue(true);
}
static void flush_smp_call_function_queue(bool warn_cpu_offline)
{
struct llist_head *head;
struct llist_node *entry;
call_single_data_t *csd, *csd_next;
...
head = this_cpu_ptr(&call_single_queue);
entry = llist_del_all(head);
entry = llist_reverse_order(entry);
...
/* 调用当前 CPU 回调列表 @call_single_queue 中的所有回调函数 */
llist_for_each_entry_safe(csd, csd_next, entry, llist) {
smp_call_func_t func = csd->func;
void *info = csd->info;
/* Do we wait until *after* callback? */
if (csd->flags & CSD_FLAG_SYNCHRONOUS) { /* smp call 等到 func 运行完成后返回,所以先不释放锁 */
func(info);
csd_unlock(csd);
} else { /* smp call 不等 func 运行完成就返回,所以先释放锁 */
csd_unlock(csd);
func(info);
}
}
...
}
3.1.4 其它 IPI 消息
感兴趣的读者可自行阅读源码分析。
3.2 其它架构 IPI 实现
感兴趣的读者可自行阅读源码分析。
4. 观察 IPI 中断发生次数
bash
# cat /proc/interrupts
[......]
IPI0: 39038745 339833 481351 716260 Rescheduling interrupts
IPI1: 495285 1329854 3247 1819 Function call interrupts
IPI2: 0 0 0 0 CPU stop interrupts
IPI3: 0 0 0 0 CPU stop (for crash dump) interrupts
IPI4: 0 0 0 0 Timer broadcast interrupts
IPI5: 1 0 0 0 IRQ work interrupts
IPI6: 0 0 0 0 CPU wake-up interrupts
[......]