Linux 6.6内核 CPU 深度解析(四):中断与 IPI — 一个 CPU 怎么“喊“另一个 CPU

〇、全景:IPI 是 CPU 之间靠 ICR 寄存器发的中断

多核系统里,CPU 之间不能直接"喊话",它们共用的通信媒介是 local APIC 的 ICR 寄存器(Interrupt Command Register,偏移 0x300) 。一个 CPU 往这个寄存器写一条命令,就等于向另一个(或一组)CPU 发了一个中断------这就是 IPI(Inter-Processor Interrupt,核间中断)。

IPI 不是一个单一的东西,它靠 ICR 里的 delivery mode(投递模式)字段分成好几种,语义天差地别。内核日常用的主要是三类:
#mermaid-svg-iQGFrWPEtOhsBcLj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-iQGFrWPEtOhsBcLj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-iQGFrWPEtOhsBcLj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-iQGFrWPEtOhsBcLj .error-icon{fill:#552222;}#mermaid-svg-iQGFrWPEtOhsBcLj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-iQGFrWPEtOhsBcLj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-iQGFrWPEtOhsBcLj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-iQGFrWPEtOhsBcLj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-iQGFrWPEtOhsBcLj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-iQGFrWPEtOhsBcLj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-iQGFrWPEtOhsBcLj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-iQGFrWPEtOhsBcLj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-iQGFrWPEtOhsBcLj .marker.cross{stroke:#333333;}#mermaid-svg-iQGFrWPEtOhsBcLj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-iQGFrWPEtOhsBcLj p{margin:0;}#mermaid-svg-iQGFrWPEtOhsBcLj .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-iQGFrWPEtOhsBcLj .cluster-label text{fill:#333;}#mermaid-svg-iQGFrWPEtOhsBcLj .cluster-label span{color:#333;}#mermaid-svg-iQGFrWPEtOhsBcLj .cluster-label span p{background-color:transparent;}#mermaid-svg-iQGFrWPEtOhsBcLj .label text,#mermaid-svg-iQGFrWPEtOhsBcLj span{fill:#333;color:#333;}#mermaid-svg-iQGFrWPEtOhsBcLj .node rect,#mermaid-svg-iQGFrWPEtOhsBcLj .node circle,#mermaid-svg-iQGFrWPEtOhsBcLj .node ellipse,#mermaid-svg-iQGFrWPEtOhsBcLj .node polygon,#mermaid-svg-iQGFrWPEtOhsBcLj .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-iQGFrWPEtOhsBcLj .rough-node .label text,#mermaid-svg-iQGFrWPEtOhsBcLj .node .label text,#mermaid-svg-iQGFrWPEtOhsBcLj .image-shape .label,#mermaid-svg-iQGFrWPEtOhsBcLj .icon-shape .label{text-anchor:middle;}#mermaid-svg-iQGFrWPEtOhsBcLj .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-iQGFrWPEtOhsBcLj .rough-node .label,#mermaid-svg-iQGFrWPEtOhsBcLj .node .label,#mermaid-svg-iQGFrWPEtOhsBcLj .image-shape .label,#mermaid-svg-iQGFrWPEtOhsBcLj .icon-shape .label{text-align:center;}#mermaid-svg-iQGFrWPEtOhsBcLj .node.clickable{cursor:pointer;}#mermaid-svg-iQGFrWPEtOhsBcLj .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-iQGFrWPEtOhsBcLj .arrowheadPath{fill:#333333;}#mermaid-svg-iQGFrWPEtOhsBcLj .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-iQGFrWPEtOhsBcLj .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-iQGFrWPEtOhsBcLj .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iQGFrWPEtOhsBcLj .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-iQGFrWPEtOhsBcLj .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iQGFrWPEtOhsBcLj .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-iQGFrWPEtOhsBcLj .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-iQGFrWPEtOhsBcLj .cluster text{fill:#333;}#mermaid-svg-iQGFrWPEtOhsBcLj .cluster span{color:#333;}#mermaid-svg-iQGFrWPEtOhsBcLj div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-iQGFrWPEtOhsBcLj .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-iQGFrWPEtOhsBcLj rect.text{fill:none;stroke-width:0;}#mermaid-svg-iQGFrWPEtOhsBcLj .icon-shape,#mermaid-svg-iQGFrWPEtOhsBcLj .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-iQGFrWPEtOhsBcLj .icon-shape p,#mermaid-svg-iQGFrWPEtOhsBcLj .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-iQGFrWPEtOhsBcLj .icon-shape .label rect,#mermaid-svg-iQGFrWPEtOhsBcLj .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-iQGFrWPEtOhsBcLj .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-iQGFrWPEtOhsBcLj .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-iQGFrWPEtOhsBcLj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 源 CPU

写 local APIC 的 ICR 寄存器(0x300)
delivery mode = Fixed

普通 IPI(走 IDT)
delivery mode = INIT

复位信号(不经 IDT)
delivery mode = Start-Up

启动信号(不经 IDT)
运行中 CPU 通信

reschedule / smp_call_function

→ IDT → sysvec_* → 执行
把裸 CPU 复位到实模式
裸 CPU 从 vector<<12 取指

(trampoline 跳板)

一句话主线:IPI = 一个 CPU 往 local APIC 的 ICR 寄存器写一条带 delivery mode 的命令 。delivery mode 决定了这条命令的语义------Fixed 是"给跑起来的 CPU 发个普通中断"(走 IDT),INIT 是"把裸 CPU 复位",Start-Up(SIPI)是"让裸 CPU 从指定地址启动"。普通 IPI 是 CPU 之间日常通信的工具,INIT-SIPI 是唤醒没跑起来的 CPU 的唯一手段。


一、预备概念:ICR 寄存器与 delivery mode

1.1 ICR 寄存器:一条命令 = 一个中断

ICR 是 local APIC 里的一个 MMIO 寄存器,偏移 APIC_ICR = 0x300:

c 复制代码
// arch/x86/include/asm/apicdef.h (v6.6, line 70)
#define	APIC_ICR	0x300
#define	APIC_ICR2	0x310      // 高 32 位:xAPIC 目标 APICID

写 ICR 这条命令时,几个关键字段拼进一个 32 位值里:

字段 位 含义
vector 0~7(APIC_VECTOR_MASK=0x000FF) 中断向量号(SIPI 时被重定义,见 4.3)
delivery mode 8~10(0x00700) 这条命令的语义(Fixed/NMI/INIT/Start-Up...)
destination shorthand 18~19 目标:自身 / 广播(含自己)/ 广播(除自己)
destination 24~31 目标 CPU 的 APICID(xAPIC 8 位)

1.2 delivery mode 七种取值:这条命令"是什么"

delivery mode 是理解 IPI 的钥匙。v6.6 源码里它的位定义 (已左移 8 位,apicdef.h:83-91):

c 复制代码
// arch/x86/include/asm/apicdef.h (v6.6, line 83)
#define		APIC_DM_FIXED		0x00000   // 000b
#define		APIC_DM_LOWEST		0x00100   // 001b
#define		APIC_DM_SMI		0x00200   // 010b
#define		APIC_DM_REMRD		0x00300   // 011b(保留)
#define		APIC_DM_NMI		0x00400   // 100b
#define		APIC_DM_INIT		0x00500   // 101b
#define		APIC_DM_STARTUP		0x00600   // 110b ← SIPI
#define		APIC_DM_EXTINT		0x00700   // 111b

对照 Intel SDM,八种 delivery mode 的语义:

delivery mode 值 语义 vector 字段含义 是否走 IDT
Fixed 000 普通 IPI,发一个中断 正常向量号 ✅ 走 IDT[vector]
Lowest Priority 001 同 Fixed,但投给当前优先级最低的核 正常向量号 ✅
SMI 010 进系统管理模式(SMM) 忽略 ❌
Reserved 011 保留(旧 Remote Read) --- ---
NMI 100 不可屏蔽中断 忽略(固定走 vector 2) ✅ 走 NMI 门
INIT 101 复位信号:清寄存器、回实模式 忽略 ❌
Start-Up(SIPI) 110 启动信号:从指定地址取指 被重定义成"入口地址 >> 12" ❌
ExtINT 111 模拟外部 8259 的 INTR --- 特殊

源码里还有一套枚举 (apicdef.h:434-439)用于 IO-APIC 重定向表:APIC_DELIVERY_MODE_FIXED=0 / LOWESTPRIO=1 / SMI=2 / NMI=4 / INIT=5 / EXTINT=7,值是"未左移"的 0~7。别和上面这套"已左移 8 位"的位定义(APIC_DM_*)搞混------前者是数值,后者是拼进 ICR 的掩码。

1.3 核心区别:普通 IPI 走 IDT,INIT/SIPI 不走

一句话切中要害:普通 IPI(Fixed)是"已经跑起来的 CPU 之间喊话"(目标 CPU 查 IDT 正常处理),INIT 和 SIPI 是"对付没跑起来的裸 CPU"(一个复位、一个给入口地址,都不经过 IDT)。

为什么裸 CPU 不经过 IDT?因为"没跑起来"意味着它根本没有 IDT、没有执行任何内核代码。普通 IPI 发过去,它既没有 IDT 可查,也没有 handler 可调。所以 x86 单独设计了 INIT 和 SIPI 这两个"裸 CPU 专用"的 delivery mode------这是理解后面第四节(INIT-SIPI 唤醒)和 smp_call_function(普通 IPI)之间区别的基石。


二、普通 IPI:运行中 CPU 的"喊话"

2.1 内核用了哪些 IPI 向量

x86 把高位的向量号(FIRST_SYSTEM_VECTOR = LOCAL_TIMER_VECTOR = 0xec 起,一直到 0xff)划给"系统专用",其中大部分就是 IPI 向量:

c 复制代码
// arch/x86/include/asm/irq_vectors.h (v6.6, line 62)
#define RESCHEDULE_VECTOR		0xfd   // 重新调度
#define CALL_FUNCTION_VECTOR		0xfc   // 跨核函数调用(广播)
#define CALL_FUNCTION_SINGLE_VECTOR	0xfb   // 跨核函数调用(单核)
#define REBOOT_VECTOR			0xf8   // 重启
#define X86_PLATFORM_IPI_VECTOR		0xf7   // 平台 IPI(HV 等)
#define IRQ_WORK_VECTOR			0xf6   // irq_work 延迟执行
// ...
#define POSTED_INTR_VECTOR		0xf2   // KVM posted interrupt
#define LOCAL_TIMER_VECTOR		0xec   // 本地时钟(最低的系统向量)
#define FIRST_SYSTEM_VECTOR		LOCAL_TIMER_VECTOR

注意这些向量从 0xff 往低处分配 ,同一区间里还挤着不少非 IPI 的系统向量 ------SPURIOUS_APIC_VECTOR=0xff(伪中断)、ERROR_APIC_VECTOR=0xfe(APIC 错误)、DEFERRED_ERROR_VECTOR=0xf4(延迟错误)、Hyper-V 回调向量等,我上面只挑了 IPI 相关的列。其中 POSTED_INTR_VECTOR 是 KVM 的 posted interrupt------在 vCPU 运行期间直接把中断注入 guest、减少 VM exit(属 CPU 虚拟化方向)。

2.2 怎么发:组装 ICR 值,写进寄存器

发普通 IPI 的核心,就是把"vector + delivery mode + 目标"拼成 ICR 值写进去。组装宏只有几行:

c 复制代码
// arch/x86/kernel/apic/local.h (v6.6, line 31)
static inline unsigned int __prepare_ICR(unsigned int shortcut, int vector,
					 unsigned int dest)
{
	unsigned int icr = shortcut | dest;

	switch (vector) {
	default:
		icr |= APIC_DM_FIXED | vector;   // 普通 IPI:Fixed + vector 号
		break;
	case NMI_VECTOR:
		icr |= APIC_DM_NMI;              // NMI IPI:只设 delivery mode,不带 vector
		break;
	}
	return icr;
}

注意 default 分支:普通 IPI 就是 APIC_DM_FIXED | vector ------delivery mode 填 Fixed,vector 字段填真正的向量号。而 NMI 是特例:只填 APIC_DM_NMI,vector 字段不填(NMI 固定走 vector 2)。

往上,内核按"目标"分了几个发送函数,最终都落到"写 ICR":

c 复制代码
// arch/x86/kernel/apic/ipi.c (v6.6, line 144)
static void __default_send_IPI_shortcut(unsigned int shortcut, int vector)
{
	if (unlikely(vector == NMI_VECTOR))
		apic_mem_wait_icr_idle_timeout();   // 发 NMI 前等 ICR 空闲(防 panic 时挂死)
	else
		apic_mem_wait_icr_idle();           // 等上一条 ICR 命令完成

	// 写 ICR 寄存器(shortcut 把目标编码进 bits 18-19,不需要 ICR2)
	native_apic_mem_write(APIC_ICR, __prepare_ICR(shortcut, vector, 0));
}

指定单个目标 CPU 时,要先写 ICR2(目标 APICID 放高 32 位),再写 ICR:

c 复制代码
// arch/x86/kernel/apic/ipi.c (v6.6, line 165)
void __default_send_IPI_dest_field(unsigned int dest_mask, int vector,
				   unsigned int dest_mode)
{
	// ...
	native_apic_mem_write(APIC_ICR2, __prepare_ICR2(dest_mask));   // 目标 APICID
	native_apic_mem_write(APIC_ICR, __prepare_ICR(0, vector, dest_mode));
}

2.3 IPI 门:目标 CPU 收到后走哪条 IDT 条目

目标 CPU 收到普通 IPI 后,和普通中断一样走 IDT。这些 IPI 向量在 idt_setup_apic_and_irq_gates 里通过 apic_idts[] 表一对一装门(详见中断系列的《中断初始化》一篇):

c 复制代码
// arch/x86/kernel/idt.c (v6.6, line 129)
static const __initconst struct idt_data apic_idts[] = {
	INTG(RESCHEDULE_VECTOR,			asm_sysvec_reschedule_ipi),
	INTG(CALL_FUNCTION_VECTOR,		asm_sysvec_call_function),
	INTG(CALL_FUNCTION_SINGLE_VECTOR,	asm_sysvec_call_function_single),
	// ...
	INTG(IRQ_WORK_VECTOR,			asm_sysvec_irq_work),
	// ...
};

所以一个普通 IPI 的完整生命周期是:源 CPU 写 ICR → 目标 local APIC 收到 → 目标 CPU 走 IDT[vector] → sysvec_* 处理函数。这跟设备中断的区别只是"来源从设备变成了另一个 CPU",投递机制完全一样。


三、跨核函数调用 smp_call_function:普通 IPI 最重要的应用

普通 IPI 里最重要的用途是 smp_call_function------一个 CPU 让另一个 CPU 执行一段函数。这是整个内核跨核协作的地基:刷 TLB、更新 per-CPU 状态、RCU 加速、调度域负载均衡......到处都用它。

3.1 入口:single 与 many

c 复制代码
// kernel/smp.c (v6.6, line 590)
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 = {
		.node = { .u_flags = CSD_FLAG_LOCK | CSD_TYPE_SYNC, },
	};
	// ...
	csd->func = func;              // 要执行的函数
	csd->info = info;              // 参数
	err = generic_exec_single(cpu, csd);   // 入队 + 发 IPI
	if (wait)
		csd_lock_wait(csd);        // 同步等待目标执行完
	return err;
}

func 和 info 被塞进一个 csd(call_single_data)结构。generic_exec_single 判断目标是不是自己:

c 复制代码
// kernel/smp.c (v6.6, line 379)
static int generic_exec_single(int cpu, struct __call_single_data *csd)
{
	if (cpu == smp_processor_id()) {
		// 目标就是自己:直接执行,不发 IPI
		csd_do_func(func, info, NULL);
		return 0;
	}
	// 目标在别的 CPU:入队 + 发 IPI
	__smp_call_single_queue(cpu, &csd->node.llist);
	return 0;
}

3.2 闭环:入队 + 发 IPI → 目标清队列执行

跨核的路径是 __smp_call_single_queue,它是整个机制最精妙的一行:

c 复制代码
// kernel/smp.c (v6.6, line 370)
if (llist_add(node, &per_cpu(call_single_queue, cpu)))   // ① 入队到目标 CPU 的队列
	send_call_function_single_ipi(cpu);              // ② 队列从空变非空,才发 IPI

llist_add 返回"入队前队列是否为空"。只有当队列从空变非空时才发 IPI------因为如果队列里本来就有活,说明之前已经发过 IPI 了,目标 CPU 迟早会来清队列,不用重复发。

目标 CPU 收到 IPI 后,走 sysvec_call_function_single → generic_smp_call_function_single_interrupt → __flush_smp_call_function_queue:

c 复制代码
// kernel/smp.c (v6.6, line 415)
void generic_smp_call_function_single_interrupt(void)
{
	__flush_smp_call_function_queue(true);
}

// kernel/smp.c (v6.6, line 434)
static void __flush_smp_call_function_queue(bool warn_cpu_offline)
{
	// ...
	entry = llist_del_all(head);          // ③ 一次性摘下整个队列
	entry = llist_reverse_order(entry);
	// ④ 遍历队列,逐个执行 csd->func
}

多核版本 smp_call_function_many_cond(smp.c:745)逻辑一样,只是对每个目标 CPU 入队,然后按数量选发送方式:

c 复制代码
// kernel/smp.c (v6.6, line 825)
if (nr_cpus == 1)
	send_call_function_single_ipi(last_cpu);              // 单核:SINGLE 向量
else if (likely(nr_cpus > 1))
	send_call_function_ipi_mask(cfd->cpumask_ipi);        // 多核:CALL_FUNCTION 向量广播

所以 CALL_FUNCTION_SINGLE_VECTOR 和 CALL_FUNCTION_VECTOR 两个向量就是这么分工的------单核发单个、多核广播。

3.3 为什么是"队列 + IPI"而不是"直接执行"

这是 IPI 最值得讲的 Why。IPI 可能丢:local APIC 的 IRR 里每个 vector 只有 1 个 bit,同 vector 的多个 IPI 在目标 CPU 处理之前到达,会被合并成一个(后到的那个"没了")。如果设计成"发一次 IPI = 执行一次函数",那合并一次就丢一次调用。

队列 + "空才发 IPI" 这个 trick 优雅地解决了丢 IPI 的问题:

  1. 要执行的函数先入队 (无锁链表 llist,关中断也能安全操作);
  2. 只有队列从空变非空才发一次 IPI;
  3. 目标 CPU 收到 IPI 后,把整个队列摘下来逐个执行。

即使某次 IPI 丢了,函数还躺在队列里,下一次 IPI(或目标 CPU 自己 flush)会把它补掉。IPI 只是"敲门",真正的活(csd->func)存在队列里,敲门没听见就等下次敲门。这个设计直接呼应了 ipi.c:62 里 reschedule IPI 的注释------"Worst case is that we lose a reschedule"(最坏情况是丢一次重新调度,反正调度器会再来)。

3.4 等待完成:wait 参数与 csd_lock 闭环

smp_call_function 系列的 wait 参数,就是"发完 IPI 后要不要等所有 CPU 执行完再继续"。smp_call_function_many 的 kernel-doc(smp.c:859)说得直白:"If @wait is true, then returns once @func has returned"------wait=true 时,函数返回就意味着 func 已经在所有目标 CPU 上跑完了。

最典型的"广播 + 等待"API 是 on_each_cpu:

c 复制代码
// kernel/smp.c (v6.6, line 1003)
void on_each_cpu_cond_mask(smp_cond_func_t cond_func, smp_call_func_t func,
			   void *info, bool wait, const struct cpumask *mask)
{
	unsigned int scf_flags = SCF_RUN_LOCAL;

	if (wait)
		scf_flags |= SCF_WAIT;

	preempt_disable();
	smp_call_function_many_cond(mask, func, info, scf_flags, cond_func);
	preempt_enable();
}

SCF_RUN_LOCAL(本地也跑)+ SCF_WAIT(等远端跑完)------这就是"所有 CPU 上都执行一遍,等它们都完成"的标准姿势。

等待的实现,靠 csd 上的一个锁标志 CSD_FLAG_LOCK 来回闭环:

  1. 源 CPU 入队前 csd_lock(csd) 置上 CSD_FLAG_LOCK(smp.c:312);
  2. 目标 CPU 执行完 csd_do_func(就是 func(info))后,csd_unlock 用 smp_store_release 清掉 LOCK(smp.c:326);
  3. 源 CPU 在 csd_lock_wait 里用 smp_cond_load_acquire 自旋等 LOCK 被清 (smp.c:293)------LOCK 清了,说明目标执行完了,才返回走下一步。

多核版本 smp_call_function_many_cond(smp.c:839)就是遍历每个目标 CPU 的 csd,逐个 csd_lock_wait:

c 复制代码
// kernel/smp.c (v6.6, line 839)
if (run_remote && wait) {
	for_each_cpu(cpu, cfd->cpumask) {
		call_single_data_t *csd;
		csd = per_cpu_ptr(cfd->csd, cpu);
		csd_lock_wait(csd);       // 逐个等每个目标 CPU 执行完
	}
}

两个值得注意的点:

  • acquire/release 语义 :csd_unlock 的 smp_store_release 和 csd_lock_wait 的 smp_cond_load_acquire 是一对内存屏障------保证源 CPU 一看到 LOCK 被清,就一定也看得到目标 CPU 执行 func 时写下的所有数据(不会读到半截的脏数据)。
  • 超时保护 :csd_lock_timeout = 5000(smp.c:171,单位 ms)。如果目标 CPU 5 秒都没执行完(可能卡死了),csd_lock_wait_toolong(:213)会报 bug,而不是让源 CPU 永远自旋。

四、INIT-SIPI:唤醒裸 CPU

前面铺垫的"普通 IPI 走 IDT"在这里成了关键反例。唤醒裸 CPU 不能用普通 IPI,必须用 INIT-SIPI 组合。

4.1 为什么普通 IPI 叫不醒裸 CPU

AP(Application Processor)上电后处于一种叫 "wait for SIPI" 的状态------它没初始化 local APIC 以外的任何东西,没有 IDT、没跑任何内核代码 。此时给它发一个 Fixed 的普通 IPI,它没有 IDT 可查、没有 handler 可调,等于对牛弹琴。

所以 x86 设计了两个"裸 CPU 专用"的 delivery mode:

  • INIT:复位信号。把目标 CPU 清空寄存器、打回实模式,像一个干净的上电状态。
  • Start-Up(SIPI) :启动信号。让目标 CPU 从指定地址开始执行实模式代码------这就是 AP 拉起那一篇(#2)讲 trampoline 的入口。

4.2 发送序列:一次 INIT + 两次 STARTUP

v6.6 的发送代码在 wakeup_secondary_cpu_via_init(完整链路见 AP 拉起那一篇,即 #2):

c 复制代码
// arch/x86/kernel/smpboot.c (v6.6, line 810)
static void send_init_sequence(int phys_apicid)
{
	// INIT assert(电平触发 + 断言)
	apic_icr_write(APIC_INT_LEVELTRIG | APIC_INT_ASSERT | APIC_DM_INIT, phys_apicid);
	// ...
	// INIT deassert(撤销断言)
	apic_icr_write(APIC_INT_LEVELTRIG | APIC_DM_INIT, phys_apicid);
}

// arch/x86/kernel/smpboot.c (v6.6, line 838)
static int wakeup_secondary_cpu_via_init(int phys_apicid, unsigned long start_eip)
{
	send_init_sequence(phys_apicid);        // 先 INIT 复位
	// ... 判断要不要发 STARTUP(num_starts,取决于 APIC 是否 integrated)
	for (j = 1; j <= num_starts; j++) {
		apic_icr_write(APIC_DM_STARTUP | (start_eip >> 12), phys_apicid);   // 再 STARTUP
	}
}

注意区别:INIT 用的是 APIC_DM_INIT,且要 APIC_INT_LEVELTRIG | APIC_INT_ASSERT 模拟"电平触发 + 断言/撤销";STARTUP 用的是 APIC_DM_STARTUP。这两条命令都不走 __prepare_ICR------因为它们的语义不是"发普通中断",而是"复位"和"启动"。

4.3 SIPI 的 vector 字段被重定义:start_eip >> 12

这是 SIPI 和普通 IPI 最本质的区别。看上一节那行 APIC_DM_STARTUP | (start_eip >> 12):

  • vector 字段只有 8 位,放不下完整物理地址;
  • 于是约定:SIPI 的 vector 字段存的是"入口物理地址 >> 12" ,目标 CPU 收到后从 vector << 12 处取指;
  • 这就要求入口地址低 12 位为 0(即 4KB 对齐),且必须位于低内存(实模式只能寻址 1MB 以下)。

这正是 trampoline 必须待在低内存、且页对齐的原因------AP 拉起那一篇(#2)已经详细讲过这条链路(trampoline → startup_32 → startup_64 → secondary_startup_64)。


五、NMI IPI:普通中断失效时的"最后手段"

还有一个特殊的 delivery mode 值得一提:NMI IPI (APIC_DM_NMI)。

看 __prepare_ICR 的 case NMI_VECTOR 分支就知道它和普通 IPI 的区别:NMI IPI 只填 delivery mode,不带 vector 号(NMI 固定走 vector 2 的 NMI 门)。NMI 的特点是不可屏蔽(cli 挡不住),所以内核在"普通中断可能全失效"的场景用它:

  • 看门狗:hard lockup 检测靠 NMI IPI 打醒卡死的 CPU(详见中断系列的《NMI/看门狗》一篇);
  • panic 时停掉其他 CPU :native_stop_other_cpus(smp.c:149)先发 REBOOT_VECTOR 普通 IPI 让其他核停,超时还没停的再用 NMI IPI 强制停下(准备 kdump)。

这也是为什么 __default_send_IPI_shortcut 里对 NMI 有特殊处理------发 NMI 前用带超时的 apic_mem_wait_icr_idle_timeout(ipi.c:152),因为 panic CPU 停其他核时如果死等 ICR 空闲,系统会挂死。


六、为什么这样设计(Why 层)

6.1 为什么 IPI 要复用 IDT/中断框架,而不是另起炉灶

普通 IPI(Fixed)走的是和外部设备中断完全一样的投递链路:local APIC → IDT → handler。这省掉了为"CPU 之间通信"单独设计一套机制的成本------硬件上,local APIC 的 ICR 发出去的本来就是"中断",CPU 收中断的入口就那一个(IDT),复用是自然选择。内核要做的事只有两件:① 给 IPI 分配几个专用向量;② 往 ICR 写正确格式的命令。

6.2 为什么 INIT/SIPI 要单独设计、不复用 IDT

和 6.1 恰好相反:复用 IDT 的前提是目标 CPU 有 IDT。裸 CPU 没有,所以必须设计"不经 IDT"的裸 CPU 专用 delivery mode。这是硬件层面就定死的约束(Intel SDM 规定 AP 上电只能等 SIPI),不是软件选择。INIT 负责"复位到干净状态",SIPI 负责"给入口地址"------两个各司其职,因为"复位"和"从哪开始跑"是两件事。

6.3 为什么 smp_call_function 用"队列 + 空才发 IPI"

见 3.3。核心是把"敲门"(IPI)和"干活"(csd->func)解耦:IPI 可以丢,所以只让它当敲门砖;真正要执行的东西存在无锁队列里,丢了下次补。如果反过来"发 IPI = 执行一次",丢 IPI 就是丢活,内核根本不敢让 IPI 有任何丢失可能,设计会复杂得多。

6.4 为什么单核/多核要分两个向量(SINGLE / CALL_FUNCTION)

从 smp_call_function_many_cond 的 nr_cpus == 1 分支能看到:单核用 CALL_FUNCTION_SINGLE_VECTOR,多核用 CALL_FUNCTION_VECTOR。分开的动机是投递效率 ------多核场景能用 ICR 的 shorthand 广播 (APIC_DEST_ALLINC/ALLBUT,一条命令发所有核),单核则直接指定目标。irq_vectors.h:49 的注释还透露了 vector 空间的稀缺------这些系统向量挤在 0xf0~0xff 的高位区间,注释说"一些 rare(不常用)的向量会被合并进 CALL_FUNCTION_VECTOR 以省 vector 空间",而 TLB、reschedule、local APIC 这几个性能关键的向量必须独占一个 vector(不能用通用入口,否则拖慢关键路径)。这套"单核精确投递、多核广播"的分工,就是性能考量的结果。


七、与已有博客的交叉引用

这篇串起了三处之前零散讲过的内容:

已有博客 本篇覆盖的关系
CPU #2 AP 拉起 本篇第四节把 INIT-SIPI 放到 IPI 框架里重新解释------它只是 delivery mode 取 INIT/Start-Up 的特殊 IPI,与普通 IPI 的本质区别是"不走 IDT"
中断系列《中断初始化》 本篇 2.3 交叉引用 apic_idts 装 IPI 门,说明普通 IPI 走的是和外部中断一样的投递链路
中断系列《NMI/看门狗》 本篇第五节解释 NMI IPI 的 APIC_DM_NMI 与普通 IPI 的 APIC_DM_FIXED 在 ICR 组装上的区别

附:本篇关键源码索引

符号 位置
APIC_ICR / APIC_ICR2(ICR 寄存器偏移) arch/x86/include/asm/apicdef.h:70 / :93
APIC_DM_*(delivery mode 位定义) arch/x86/include/asm/apicdef.h:83
APIC_DELIVERY_MODE_*(delivery mode 枚举) arch/x86/include/asm/apicdef.h:434
IPI 向量定义(RESCHEDULE_VECTOR 等) arch/x86/include/asm/irq_vectors.h:62
apic_idts[](IPI 门) arch/x86/kernel/idt.c:129
__prepare_ICR(组装 ICR 值) arch/x86/kernel/apic/local.h:31
__default_send_IPI_shortcut / _dest_field arch/x86/kernel/apic/ipi.c:144 / :165
native_smp_send_reschedule / native_send_call_func_ipi arch/x86/kernel/apic/ipi.c:67 / :81
smp_call_function_single / _many_cond kernel/smp.c:590 / :745
generic_exec_single / __smp_call_single_queue kernel/smp.c:379 / :370
__flush_smp_call_function_queue kernel/smp.c:434
send_init_sequence / wakeup_secondary_cpu_via_init arch/x86/kernel/smpboot.c:810 / :838
相关推荐
吠品1 小时前
DicomViewer24 的 CI 又红了:MPR 测试偶发失败排查记
java·服务器·前端
志栋智能2 小时前
安全超自动化如何支持快速安全扩容?
运维·服务器·数据库·架构·自动化
承渊政道5 小时前
星空组网+Python实战:在另一台电脑上查看CSV报表
运维·开发语言·python·csv·星空组网
木卫二号Coding10 小时前
推荐Linux统计文件大小并操作删除工具-ncdu
linux·运维·服务器
志栋智能11 小时前
超自动化运维:实现IT服务消费化的基础
运维·自动化
198******1263411 小时前
2026企业AI办公工具选型指南:从评估框架到场景适配
大数据·运维·人工智能
吴声子夜歌11 小时前
Nginx应用与运维——Nginx HTTP模块详解(数据处理功能模块)
运维·nginx·http
zhangrelay11 小时前
机械臂末端规划智能模型的误区演示
linux·笔记·学习·ubuntu·ros2
2601_9672127211 小时前
专利型电源轨道系统技术路线对比与选型框架
大数据·运维·网络·经验分享