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
[......]
相关推荐
hhcgchpspk22 分钟前
一次msf免杀渗透入门实践
linux·经验分享·网络安全·系统安全·渗透·msf
小白勇闯网安圈1 小时前
Vmware的Ubuntu构建极简版Linux发行版
linux
刘某的Cloud1 小时前
shell脚本-read-输入
linux·运维·bash·shell·read
broad-sky1 小时前
Ubuntu上查看USB相机连接的是哪个口,如何查看
linux·数码相机·ubuntu
秋深枫叶红1 小时前
嵌入式第三十七篇——linux系统编程——线程控制
linux·学习·线程·系统编程
shaohui9732 小时前
ARMv7 linux中断路由以及处理
linux·gic·cpsr·armv7
三小尛2 小时前
linux的开发工具vim
linux·运维·vim
陈陈爱java2 小时前
Conda 常用命令行
linux·windows·conda
twdnote2 小时前
dokcer 环境中集成LibreOffice
linux
ChristXlx2 小时前
Linux安装redis(虚拟机适用)
linux·运维·redis