〇、全景: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 的问题:
- 要执行的函数先入队 (无锁链表
llist,关中断也能安全操作); - 只有队列从空变非空才发一次 IPI;
- 目标 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 来回闭环:
- 源 CPU 入队前
csd_lock(csd)置上CSD_FLAG_LOCK(smp.c:312); - 目标 CPU 执行完
csd_do_func(就是func(info))后,csd_unlock用smp_store_release清掉 LOCK(smp.c:326); - 源 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 |