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
[......]
相关推荐
朝九晚五ฺ3 小时前
【Linux探索学习】第十四弹——进程优先级:深入理解操作系统中的进程优先级
linux·运维·学习
自由的dream3 小时前
Linux的桌面
linux
xiaozhiwise4 小时前
Makefile 之 自动化变量
linux
意疏6 小时前
【Linux 篇】Docker 的容器之海与镜像之岛:于 Linux 系统内探索容器化的奇妙航行
linux·docker
BLEACH-heiqiyihu6 小时前
RedHat7—Linux中kickstart自动安装脚本制作
linux·运维·服务器
一只爱撸猫的程序猿6 小时前
一个简单的Linux 服务器性能优化案例
linux·mysql·nginx
我的K84098 小时前
Flink整合Hudi及使用
linux·服务器·flink
1900438 小时前
linux6:常见命令介绍
linux·运维·服务器
Camellia-Echo8 小时前
【Linux从青铜到王者】Linux进程间通信(一)——待完善
linux·运维·服务器