Linux:SMP 架构下 CPU 核间通信

文章目录

  • [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_* 消息是 LinuxSMP 架构下 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
[......]
相关推荐
AlfredZhao36 分钟前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334667 小时前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪8 小时前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠1 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush41 天前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5201 天前
Linux 11 动态监控指令top
linux
不会C语言的男孩1 天前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
古城小栈1 天前
Unix 与 Linux 异同小叙
linux·服务器·unix
凡人叶枫1 天前
Effective C++ 条款42:了解 typename 的双重意义
java·linux·服务器·c++
2601_961875241 天前
决战申论100题2026|最新|范文
linux·容器·centos·debian·ssh·fabric·vagrant