Linux:SMP 架构下 CPU 核间通信
目录
1. 前言
限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
2. 背景
本文以 Linux 4.14 内核,ARM 架构,GIC 中断芯片为背景,简要分析 SMP 架构下 CPU 核间通信。
3. SMP 架构下 CPU 核间通信
Linux 系统下,SMP 架构 CPU 核间通过预定义的 IPI(Inter-Processor Interrupts)中断来进行通信:
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:
/* 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(), ... */
}
#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 中断:
gic_handle_irq()
handle_IPI(irqnr, regs);
/* 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 运行:
void smp_send_reschedule(int cpu)
{
smp_cross_call(cpumask_of(cpu), IPI_RESCHEDULE); /* 流程同 IPI_TIMER */
}
看一下处理 IPI_RESCHEDULE 中断的处理流程:
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 核上运行回调函数:
/* include/linux/smp.h */
int smp_call_function_single(int cpuid, smp_call_func_t func, void *info,
int wait);
/* 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;
}
void arch_send_call_function_single_ipi(int cpu)
{
smp_cross_call(cpumask_of(cpu), IPI_CALL_FUNC);
}
处理 IPI_CALL_FUNC 中断,在目标 CPU 上调用回调:
gic_handle_irq()
handle_IPI(irqnr, regs)
switch (ipinr) {
...
case IPI_CALL_FUNC:
irq_enter();
generic_smp_call_function_interrupt();
irq_exit();
break;
...
}
#define generic_smp_call_function_interrupt \
generic_smp_call_function_single_interrupt
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 中断发生次数
# 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
[......]

浙公网安备 33010602011771号