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
[......]
相关推荐
cominglately10 分钟前
centos单机部署seata
linux·运维·centos
魏 无羡12 分钟前
linux CentOS系统上卸载docker
linux·kubernetes·centos
CircleMouse16 分钟前
Centos7, 使用yum工具,出现 Could not resolve host: mirrorlist.centos.org
linux·运维·服务器·centos
木子Linux1 小时前
【Linux打怪升级记 | 问题01】安装Linux系统忘记设置时区怎么办?3个方法教你回到东八区
linux·运维·服务器·centos·云计算
mit6.8241 小时前
Ubuntu 系统下性能剖析工具: perf
linux·运维·ubuntu
鹏大师运维1 小时前
聊聊开源的虚拟化平台--PVE
linux·开源·虚拟化·虚拟机·pve·存储·nfs
watermelonoops1 小时前
Windows安装Ubuntu,Deepin三系统启动问题(XXX has invalid signature 您需要先加载内核)
linux·运维·ubuntu·deepin
滴水之功2 小时前
VMware OpenWrt怎么桥接模式联网
linux·openwrt
ldinvicible2 小时前
How to run Flutter on an Embedded Device
linux
YRr YRr3 小时前
解决Ubuntu 20.04上编译OpenCV 3.2时遇到的stdlib.h缺失错误
linux·opencv·ubuntu