1、IPI概况
IPI,全称是Inter-Processor Interrupt,是在soc内多个core之间触发的中断,这一点有别与常见的外设中断,因此内核专门预留了部分中断号给IPI,在arm64架构上是0-15这16个中断号。以常用的gicv3中断控制器驱动为例,IPI中断的处理在如下代码中:
<drivers/irqchip/irq-gic-v3.c>618 static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs) 619 {620 u32 irqnr; 621 622 irqnr = gic_read_iar(); /* 通过读gic中断控制器的iar寄存器获得当前pending的中断号 */ 623 ....../* 此处省略对中断号[16,1020)的处理流程 */655 if (irqnr < 16) { 656 gic_write_eoir(irqnr); /* 做eoi(end of interrupt)清中断 */657 if (static_branch_likely(&supports_deactivate_key)) 658 gic_write_dir(irqnr); 659 #ifdef CONFIG_SMP /*如果配置了对称多处理器。才会有核间中断需要处理*/ 660 /* 661 * Unlike GICv2, we don't need an smp_rmb() here. 662 * The control dependency from gic_read_iar to 663 * the ISB in gic_write_eoir is enough to ensure 664 * that any shared data read by handle_IPI will 665 * be read after the ACK. 666 */ 667 handle_IPI(irqnr, regs); /* IPI中断处理 */ 668 #else 669 WARN_ONCE(true, "Unexpected SGI received!\n"); /* 如果未定义SMP,报错 */ 670 #endif 671 } 672 }
handle_IPI
是处理IPI中断的核心
<arch/arm64/kernel/smp.c>873 /* 874 * Main handler for inter-processor interrupts 875 */ 876 void handle_IPI(int ipinr, struct pt_regs *regs) 877 { 878 unsigned int cpu = smp_processor_id(); /*获得当前运行改代码的cpu id*/ 879 struct pt_regs *old_regs = set_irq_regs(regs); /*pt_regs结构体主要包含当前的寄存器信息,x0到x31,sp,pc,pstate等,这里是把当前cpu的pt_regs保存到old_regs中,将参数中的regs设置到本cpu中,old_regs用来做中断处理完对中断前现场的恢复 */ 880 881 if ((unsigned)ipinr < NR_IPI) { /*当前有效的IPI中断为7(NR_IPI)个*/ 882 trace_ipi_entry_rcuidle(ipi_types[ipinr]); /* ftrace记录进入ipi中断,用于debug */883 __inc_irq_stat(cpu, ipi_irqs[ipinr]); /* 统计各cpu不同类型的ipi中断数量,cat /proc/interrupts中IPI相关中断的数据即来自于此 */ 884 } 885 886 switch (ipinr) { 887 case IPI_RESCHEDULE: 888 scheduler_ipi(); /* 如果IPI类型是触发重调度,那就进该逻辑,详细描述见第2小结 */889 break; 890 891 case IPI_CALL_FUNC: 892 irq_enter(); /*将preempt_count中HARDIRQ的部分+1,可以禁止调度或抢占的发生,直到执行到irq_exit,以下同不再赘述 */ 893 generic_smp_call_function_interrupt();/* 执行本cpu所有function回调(别的cpu给安排的),详细描述见第3小结 */ 894 irq_exit(); 895 break; 896 897 case IPI_CPU_STOP: 898 irq_enter(); 899 local_cpu_stop(); /* 将本cpu停下来,进入低功耗状态,详细描述见第4小结 */ 900 irq_exit(); 901 break; 902 903 case IPI_CPU_CRASH_STOP: 904 if (IS_ENABLED(CONFIG_KEXEC_CORE)) { /* 如果配置了KEXEC,即在系统panic时会进入第二内核,才会有本IPI中断的操作,详细描述见第5小结 */ 905 irq_enter(); 906 ipi_cpu_crash_stop(cpu, regs); 907 908 unreachable(); 909 } 910 break; 911 912 #ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST 913 case IPI_TIMER: 914 irq_enter(); 915 tick_receive_broadcast(); /* 接收timer广播,执行timer的中断回调,详细描述见第6小结 */ 916 irq_exit(); 917 break; 918 #endif 919 920 #ifdef CONFIG_IRQ_WORK 921 case IPI_IRQ_WORK: 922 irq_enter(); 923 irq_work_run(); /* 本cpu执行irq_work,详细描述见第7小结 */ 924 irq_exit(); 925 break; 926 #endif 927 928 #ifdef CONFIG_ARM64_ACPI_PARKING_PROTOCOL 929 case IPI_WAKEUP: /* 从parked状态中唤醒本cpu,详细描述见第8小结 */930 WARN_ONCE(!acpi_parking_protocol_valid(cpu), 931 "CPU%u: Wake-up IPI outside the ACPI parking protocol\n", 932 cpu); 933 break; 934 #endif 935 936 default: 937 pr_crit("CPU%u: Unknown IPI message 0x%x\n", cpu, ipinr); 938 break; 939 } 940 941 if ((unsigned)ipinr < NR_IPI) 942 trace_ipi_exit_rcuidle(ipi_types[ipinr]); 943 set_irq_regs(old_regs); 944 }
2、IPI_RESCHEDULE
一般的使用场景(可参考文末推进阅读[2]中对resched_curr的描述)是先为进程设置TIF_NEED_RESCHED
标志(意味着需要调度该进程),如果发现该进程没有在当前CPU上,那就通过smp_send_reschedule
接口触发IPI_RESCHEDULE中断给相应CPU,相应CPU最终进入hadle_IPI的scheduler_ipi中
<kernel/sched/core.c>
2312 void scheduler_ipi(void)
2313 {
2314 /*
2315 * Fold TIF_NEED_RESCHED into the preempt_count; anybody setting
2316 * TIF_NEED_RESCHED remotely (for the first time) will also send
2317 * this IPI.
2318 */
2319 preempt_fold_need_resched(); /* 更新thread_info结构体中need_resched变量的值,暂不清楚这个操作的目的*/
2320
2321 if (llist_empty(&this_rq()->wake_list) && !got_nohz_idle_kick()) /*如果runqueue的wack_list是空的,那就什么都不做 (nohz这个判断不清楚是什么意思)*/
2322 return;
2323
2324 /*
2325 * Not all reschedule IPI handlers call irq_enter/irq_exit, since
2326 * traditionally all their work was done from the interrupt return
2327 * path. Now that we actually do some work, we need to make sure
2328 * we do call them.
2329 *
2330 * Some archs already do call them, luckily irq_enter/exit nest
2331 * properly.
2332 *
2333 * Arguably we should visit all archs and update all handlers,
2334 * however a fair share of IPIs are still resched only so this would
2335 * somewhat pessimize the simple resched case.
2336 */
2337 irq_enter(); /*将preempt_count中HARDIRQ的部分+1,可以禁止调度或抢占的发生,直到执行到irq_exit*/
2338 sched_ttwu_pending(); /* 尝试唤醒pending着的线程*/
2339
2340 /*
2341 * Check if someone kicked us for doing the nohz idle load balance.
2342 */
2343 if (unlikely(got_nohz_idle_kick())) {
2344 this_rq()->idle_balance = 1;
2345 raise_softirq_irqoff(SCHED_SOFTIRQ); /* 触发软中断做负载均衡,这里细节暂不清楚 */
2346 }
2347 irq_exit();
2348 }
3、IPI_CALL_FUNC
当希望在某个cpu上调用一个函数(func)时,可能会触发IPI_CALL_FUNC中断(“可能”是因为如果希望执行func的cpu与本cpu相同,那就不用费劲发中断了,直接执行),常用smp_call_function
接口触发target cpu的IPI_CALL_FUNC软中断(会在之后的文章中详细讲这个过程),target cpu会经历以下调用handle_IPI->generic_smp_call_function_interrupt-->generic_smp_call_function_single_interrupt-->flush_smp_call_function_queue
调用一遍所有pending状态中的function回调。
<kernel/smp.c>
210 static void flush_smp_call_function_queue(bool warn_cpu_offline)
211 {
212 struct llist_head *head;
213 struct llist_node *entry;
214 call_single_data_t *csd, *csd_next;
215 static bool warned;
216
217 lockdep_assert_irqs_disabled();
218
219 head = this_cpu_ptr(&call_single_queue); /*拿到本cpu的call_single_queue全局链表,当其他cpu想让本cpu执行某个func时,会通过generic_exec_single()接口往对应cpu的call_signal_queue中添加相应的call_single_data_t结构,这其中包含着需要执行的func回调*/
220 entry = llist_del_all(head);
221 entry = llist_reverse_order(entry);
222
223 /* There shouldn't be any pending callbacks on an offline CPU. */
224 if (unlikely(warn_cpu_offline && !cpu_online(smp_processor_id()) &&
225 !warned && !llist_empty(head))) { /* 如果发现本cpu不是在online的状态,就报错(不过我不理解为什么一个offline的cpu能响应这个CALL_FUNC的IPI中断呢?) */
226 warned = true;
227 WARN(1, "IPI on offline CPU %d\n", smp_processor_id());
228
229 /*
230 * We don't have to use the _safe() variant here
231 * because we are not invoking the IPI handlers yet.
232 */
233 llist_for_each_entry(csd, entry, llist)
234 pr_warn("IPI callback %pS sent to offline CPU\n",
235 csd->func);
236 }
237
238 llist_for_each_entry_safe(csd, csd_next, entry, llist) { /* 循环遍历call_single_queue链表,把每个成员的func执行一遍 */
239 smp_call_func_t func = csd->func;
240 void *info = csd->info;
241
242 /* Do we wait until *after* callback? */
243 if (csd->flags & CSD_FLAG_SYNCHRONOUS) {/* 根据func回调设置的属性,是否是同步接口,决定执行顺序 */
244 func(info); /* 如果是同步的,就先执行func回调,再放相应csd句柄的锁 */
245 csd_unlock(csd);
246 } else {
247 csd_unlock(csd); /* 如果是异步的,先放相应csd句柄的锁,再执行func回调 */
248 func(info);
249 }
250 }
251
252 /*
253 * Handle irq works queued remotely by irq_work_queue_on().
254 * Smp functions above are typically synchronous so they
255 * better run first since some other CPUs may be busy waiting
256 * for them.
257 */
258 irq_work_run(); /*执行irq works(暂不清楚这里的背景,会在之后的文章讨论这个话题)*/
259 }
260
4、IPI_CPU_STOP
当某个cpu想让其他cpu停下来时,会发送该IPI中断,主要的接口是machine_halt
, machine_power_off
,machine_restart
,这些接口都会调用smp_send_stop
触发IPI_CPU_STOP中断,target cpu接收到该中断后,做如下处理:
<arch/arm64/kernel/smp.c>830 static void local_cpu_stop(void) 831 { 832 set_cpu_online(smp_processor_id(), false);/*把当前cpu offline*/833 834 local_daif_mask();/* 设置pstate的D A I F状态位为1,关闭本cpu的系统调试(D),系统错误SError interrupt(A),中断IRQ interrupt(I), 快速中断FIQ interrupt(F)*/835 sdei_mask_local_cpu(); /* 不清楚这里在做什么 */ 836 cpu_park_loop(); /* 通过wfe和wfi指令,让当前cpu进入low-power standby模式 */ 837 }
5、IPI_CPU_CRASH_STOP
在系统crash时,发生crash的cpu给其他cpu发送该中断,在使能了KEXEC的系统中,做出以下响应,主要是将寄存器信息保存下来,传递给第二内核。KEXEC常用来做系统crash时在不重启的情况下快速进入第二内核。第二内核的目的是把当前的ddr内存镜像保存下来,方便之后通过crash tool分析系统crash问题,感兴趣可以参考该文章了解。
<arch/arm64/kernel/smp.c>853 static void ipi_cpu_crash_stop(unsigned int cpu, struct pt_regs *regs) 854 { 855 #ifdef CONFIG_KEXEC_CORE 856 crash_save_cpu(regs, cpu); /* 保存当前cpu寄存器信息,方便定位问题*/857 858 atomic_dec(&waiting_for_crash_ipi); 859 860 local_irq_disable(); 861 sdei_mask_local_cpu(); 862 863 #ifdef CONFIG_HOTPLUG_CPU 864 if (cpu_ops[cpu]->cpu_die) 865 cpu_ops[cpu]->cpu_die(cpu); 866 #endif 867 868 /* just in case */ 869 cpu_park_loop(); /* 本cpu进入低功耗,但一般不需要,crash的cpu会拉着整个系统重启 */ 870 #endif 871 }
6、IPI_TIMER
当某cpu调用tick_broadcast(const struct cpumask *mask)
时,即调用IPI_TIMER中断给相应cpu(通过mask指定cpu)发送timer的广播中断,target cpu执行以下函数进行响应
<kernel/time/tick-broadcast.c>244 #ifdef CONFIG_GENERIC_CLOCKEVENTS_BROADCAST 245 int tick_receive_broadcast(void) 246 { 247 struct tick_device *td = this_cpu_ptr(&tick_cpu_device); 248 struct clock_event_device *evt = td->evtdev; /*拿出本cpu的tick_device的clock_event_device,这在某个cpu上是唯一的 */ 249 250 if (!evt) 251 return -ENODEV; 252 253 if (!evt->event_handler) 254 return -EINVAL; 255 256 evt->event_handler(evt); /* 执行tick clock的回调函数 */ 257 return 0; 258 } 259 #endif
7、IPI_IRQ_WORK
暂不清楚irq_work的应用场景,当收到该IPI中断时,处理如下:
<kernel/irq_work.c>
178 void irq_work_run(void)
179 { /*将本cpuraised_list和lazy_list链表上挂载的irq_work执行一遍*/
180 irq_work_run_list(this_cpu_ptr(&raised_list));
181 irq_work_run_list(this_cpu_ptr(&lazy_list));
182 }
183 EXPORT_SYMBOL_GPL(irq_work_run); 142 static void irq_work_run_list(struct llist_head *list)
143 {
144 struct irq_work *work, *tmp;
145 struct llist_node *llnode;
146 unsigned long flags;
147
148 BUG_ON(!irqs_disabled());
149
150 if (llist_empty(list))
151 return;
152
153 llnode = llist_del_all(list);
154 llist_for_each_entry_safe(work, tmp, llnode, llnode) {
155 /*
156 * Clear the PENDING bit, after this point the @work
157 * can be re-used.
158 * Make it immediately visible so that other CPUs trying
159 * to claim that work don't rely on us to handle their data
160 * while we are in the middle of the func.
161 */
162 flags = work->flags & ~IRQ_WORK_PENDING;
163 xchg(&work->flags, flags);
164
165 work->func(work); /*遍历链表中所有的work,将其回调全执行一遍 */
166 /*
167 * Clear the BUSY bit and return to the free state if
168 * no-one else claimed it meanwhile.
169 */
170 (void)cmpxchg(&work->flags, flags, flags & ~IRQ_WORK_BUSY);
171 }
172 }
173
8、IPI_WAKEUP
当cpu接收到该IPI中断,即从parked的状态(应该是wfi和wfe的低功耗状态)唤醒过来,更多信息参见patch comment(arm64: kernel: implement ACPI parking protocol)
<arch/arm64/kernel/acpi_parking_protocol.c>39 bool acpi_parking_protocol_valid(int cpu) 40 { 41 struct cpu_mailbox_entry *cpu_entry = &cpu_mailbox_entries[cpu]; 42 43 return cpu_entry->mailbox_addr && cpu_entry->version; /* 不明白这样做是怎么唤醒的cpu */ 44 }
在 Linux 内核中,IPI(Inter-Processor Interrupt,处理器间中断)用于让一个 CPU 向另一个 CPU 发送中断请求,以便执行特定任务。不同类型的 IPI 适用于不同的场景,以下是它们的区别和适用场景:
1. IPI_CALL_FUNC
作用
- 让目标 CPU 执行一个回调函数,通常用于跨 CPU 调用。
- 典型的函数是
smp_call_function*()
系列(如smp_call_function_single()
)。
适用场景
- 让另一个 CPU 执行特定代码,例如刷新 TLB、更新某些共享数据。
- 主要用于 SMP(对称多处理)系统中的 核间通信。
调用路径
smp_call_function_single() → generic_exec_single() → arch_send_call_function_ipi()
示例
当某个 CPU 需要让其他 CPU 刷新 TLB 时,可能会调用:
smp_call_function(flush_tlb_func, NULL, 1);
2. IPI_RESCHEDULE
作用
- 用于触发目标 CPU 立即进行调度(即
schedule()
),一般是 有更高优先级任务需要运行 时触发。 - 不携带额外数据,只告诉目标 CPU 需要重新调度。
适用场景
- 负载均衡(
sched_balance
) - 任务迁移(
wake_up_process()
) - CPU 负载变化时让空闲 CPU 重新调度
调用路径
reschedule_ipi_handler() → scheduler_tick() → schedule()
示例
- 当有新任务需要调度到目标 CPU 时,触发 IPI_RESCHEDULE
resched_cpu(cpu);
3. IPI_CPU_STOP
作用
- 让目标 CPU 立即停止执行,常用于 CPU 离线(hotplug)或系统崩溃 时。
适用场景
- CPU hotplug(动态启停 CPU)
- 系统关机或重启
- 紧急情况下关闭 CPU(如崩溃恢复)
调用路径
stop_machine() → stop_cpu()
示例
- 当一个 CPU 需要下线(offlining)时,其他 CPU 需要发送 IPI_CPU_STOP
cpu_down(1);
4. IPI_WAKEUP
作用
- 唤醒目标 CPU(如果它处于休眠状态)。
适用场景
- 处理器从
idle
状态被唤醒(wakeup_secondary_cpu()
) - SMP 系统中的电源管理
调用路径
wakeup_secondary_cpu() → send_wakeup_ipi()
示例
- 当 CPU 进入深度睡眠,需要用 IPI_WAKEUP 唤醒
arch_send_wakeup_ipi_mask(cpumask);
5. IPI_IRQ_WORK
作用
- 让目标 CPU 执行 deferred IRQ work(延迟的中断工作),常用于软中断或异步任务。
适用场景
- 异步中断处理(如
irq_work_run()
) - 时钟同步、RCU 任务、日志处理等
调用路径
irq_work_queue_on() → arch_send_call_function_single_ipi()
示例
- 在一个 CPU 上触发 IRQ_WORK
irq_work_queue(&work);
6. IPI_TIMER
作用
- 用于目标 CPU 处理定时器事件,主要在 NO_HZ 模式下使用。
适用场景
NO_HZ
模式下的 tickless 计时- 全局时钟同步
- 调度器 tick 处理
调用路径
tick_receive_broadcast() → clockevents_handle_nohz_tick()
示例
- 在 NO_HZ 模式下,某 CPU 可能需要被 IPI_TIMER 唤醒来更新时间
clockevents_handle_nohz_tick();
7. IPI_CPU_CRASH_STOP
作用
- 用于紧急情况(如内核崩溃)时,通知所有 CPU 立即停止执行,通常在
panic()
期间使用。
适用场景
- Kernel panic 处理
- 系统紧急关机
- 避免崩溃后其他 CPU 继续执行
调用路径
smp_send_stop() → native_send_ipi_all()
示例
- 当系统 panic 时,停止所有 CPU
smp_send_stop();
总结
IPI 类型 | 作用 | 适用场景 |
---|---|---|
IPI_CALL_FUNC | 让目标 CPU 执行回调函数 | TLB 刷新、SMP 通信、跨 CPU 调用 |
IPI_RESCHEDULE | 让目标 CPU 立即重新调度 | 负载均衡、任务迁移、高优先级任务 |
IPI_CPU_STOP | 让目标 CPU 立即停止 | CPU hotplug、系统关机 |
IPI_WAKEUP | 唤醒目标 CPU(如果处于休眠状态) | 处理器电源管理、CPU 休眠唤醒 |
IPI_IRQ_WORK | 让目标 CPU 处理延迟的 IRQ 任务 | 软中断、RCU 任务 |
IPI_TIMER | 让目标 CPU 处理定时器事件 | NO_HZ 模式、tickless 计时 |
IPI_CPU_CRASH_STOP | 紧急停止所有 CPU,通常用于 panic | Kernel panic 处理 |
每种 IPI 都有特定的用途,最常见的是 IPI_RESCHEDULE
(调度 IPI)和 IPI_CALL_FUNC
(跨 CPU 调用)。如果你在 simpleperf
或 ftrace
看到过多的某种 IPI,可以根据上面的表格判断可能的原因。