CHAPTER 08

ARM64 与 GICv3 的角色

RT 的主体在通用代码里,架构只需要满足几条前提。这一章回答:ARM64 为 RT 提供了什么、在 arch/arm64 里到底改了哪几行、GICv3 在 RT 内核里扮演什么角色,以及哪些延迟来自硬件和固件、RT 无能为力。

8.1RT 与架构的关系:一行 Kconfig 背后的前提

6.12 为 ARM64 打开 RT 的提交 d8fccd9ca5f9 "arm64: Allow to enable PREEMPT_RT." 只改了一行:

	select ARCH_SUPPORTS_RT

整个 arch/arm64 目录里,字面出现 PREEMPT_RT 的地方只有 3 个文件、11 行(grep -rn PREEMPT_RT arch/arm64 核验):

文件行内容
arch/arm64/kernel/fpsimd.c228, 243, 1897, 1907, 1913, 1919, 1947, 1949FPSIMD 上下文保护与 kernel-mode NEON(8.5 节)
arch/arm64/kernel/irq.c78do_softirq_own_stack() 在 RT 下不编译(8.6 节)
arch/arm64/kernel/traps.c177–178oops 信息里打印 " PREEMPT_RT"

改动这么少,是因为 RT 对架构的要求本来就不多,而 ARM64 在过去几年已经逐项满足(提交说明原话:"arm64 has all the required architecture related changes, that have been identified over time")。这些前提可以归为五类:

RT 的工作量分布:通用层厚、架构层薄 通用内核(kernel/、mm/、include/linux/)—— RT 的主体 锁:rtmutex spinlock→睡眠锁 + PI 中断线程化 kernel/irq/manage.c 软中断 / hrtimer softirq.c · hrtimer.c 调度器 migrate_disable · PI RCU / printk / mm boost · nbcon · slub 驱动层:GICv3 / ITS / vGIC 使用 raw_spinlock_t,arch timer 与 KVM 定时器用 HRTIMER_MODE_*_HARD drivers/irqchip/irq-gic-v3*.c · drivers/clocksource/arm_arch_timer.c · arch/arm64/kvm/arch_timer.c arch/arm64 —— 只需满足前提 ① 抢占入口 __el1_irq 返回前 arm64_preempt_ schedule_irq() ② 返回用户态 exit_to_user_mode → do_notify_resume ③ 上下文切换 __switch_to cpu_switch_to ④ FPSIMD fpsimd.c 的 RT 分支 (唯一实质改动) ⑤ 中断栈 softirq 不再借用 IRQ 栈
RT 的工作在通用层;ARM64 架构层提供五个前提,其中只有 FPSIMD 需要真正写 RT 专用分支。

8.2异常模型与入口:中断从哪里进内核

ARM64 的异常级别:EL0 用户态、EL1 内核、EL2 hypervisor(KVM/VHE 下内核本身就跑在 EL2)、EL3 安全监控(TF-A 固件)。中断以异常的形式到达,CPU 根据来源 EL、栈指针选择、执行态跳到 VBAR_EL1 向量表里 16 个入口之一。每个入口 128 字节(.align 7),所以偏移可以直接从顺序算出:

VBAR_EL1 向量表(entry.S:519) 0x000–0x180EL1t:sync/irq/fiq/serror (SP_EL0 作为栈,Linux 不用) 0x200EL1h sync 0x280EL1h irq → el1h_64_irq 0x300 / 0x380EL1h fiq / serror 0x400EL0 64 sync(svc 系统调用) 0x480EL0 64 irq → el0t_64_irq 0x500 / 0x580EL0 64 fiq / serror 0x600–0x780EL0 32 位(compat) el1h_64_irq_handler() entry-common.c:643 el0t_64_irq_handler() entry-common.c:945 kernel_entry __el1_irq() 返回前:抢占点 el0_interrupt() 返回前:exit_to_user_mode handle_arch_irq = gic_handle_irq set_handle_irq() · irq-gic-v3.c:2124
ARM64 的 IRQ 有两个主要入口:被打断的是内核(EL1h,偏移 0x280)还是用户态(EL0 64 位,偏移 0x480)。两条路径最终都调用同一个 gic_handle_irq(),但返回前的抢占处理不同。

汇编部分 entry_handler 宏(entry.S:572)只做三件事:kernel_entry 保存寄存器形成 pt_regs,bl el1h_64_irq_handler 进 C,返回后 b ret_to_kernel / b ret_to_user。C 入口:

static __always_inline void __el1_irq(struct pt_regs *regs,
				      void (*handler)(struct pt_regs *))
{
	enter_from_kernel_mode(regs);      /* RCU / lockdep / context tracking */

	irq_enter_rcu();                   /* preempt_count += HARDIRQ_OFFSET */
	do_interrupt_handler(regs, handler);   /* 切 IRQ 栈,调 gic_handle_irq */
	irq_exit_rcu();                    /* -= HARDIRQ_OFFSET;软中断(RT 下只唤醒) */

	arm64_preempt_schedule_irq();      /* ← 内核态抢占点 */

	exit_to_kernel_mode(regs);
}

static void noinstr el1_interrupt(struct pt_regs *regs,
				  void (*handler)(struct pt_regs *))
{
	write_sysreg(DAIF_PROCCTX_NOIRQ, daif);   /* D、A 打开,I、F 保持屏蔽 */

	if (IS_ENABLED(CONFIG_ARM64_PSEUDO_NMI) && !interrupts_enabled(regs))
		__el1_pnmi(regs, handler);         /* 被打断时 IRQ 是关的 → 只能是 pseudo-NMI */
	else
		__el1_irq(regs, handler);
}
static void noinstr el0_interrupt(struct pt_regs *regs,
				  void (*handler)(struct pt_regs *))
{
	enter_from_user_mode(regs);

	write_sysreg(DAIF_PROCCTX_NOIRQ, daif);

	if (regs->pc & BIT(55))
		arm64_apply_bp_hardening();        /* Spectre-v2 缓解 */

	irq_enter_rcu();
	do_interrupt_handler(regs, handler);
	irq_exit_rcu();

	exit_to_user_mode(regs);               /* ← 返回用户态前检查 TIF_NEED_RESCHED */
}
ARM64 不用 GENERIC_ENTRY

x86、RISC-V、LoongArch 用 kernel/entry/common.c 的通用 entry 框架;6.12 的 ARM64 还用自己的一套(arch/arm64/Kconfig 没有 select GENERIC_ENTRY)。所以 RT 相关的抢占逻辑要到 entry-common.c 里看,名字也不同:通用代码的 irqentry_exit_cond_resched() 在这里对应 arm64_preempt_schedule_irq()。

8.3抢占点:内核态的"三道门"与用户态的出口

内核态:arm64_preempt_schedule_irq()

static void __sched arm64_preempt_schedule_irq(void)
{
	if (!need_irq_preemption())      /* RT 下 = IS_ENABLED(CONFIG_PREEMPTION) = 真 */
		return;

	/* thread_info::preempt_count 同时包含 count 和 need_resched */
	if (READ_ONCE(current_thread_info()->preempt_count) != 0)
		return;                       /* 门 1:count≠0 或 不需要调度 */

	/* 用 PMR 屏蔽时,DAIF 里有位被置 = 刚处理过 NMI */
	if (system_uses_irq_prio_masking() && read_sysreg(daif))
		return;                       /* 门 2:NMI 上下文绝不调度 */

	/* 抢占会把 PSTATE 副本留在栈上,cpufeature 启用前不允许 */
	if (system_capabilities_finalized())
		preempt_schedule_irq();       /* 门 3 */
}

RT 内核里 PREEMPT_DYNAMIC 不可用(kernel/Kconfig.preempt:98),所以 need_irq_preemption() 走 #else 分支,恒为真,没有 static key 开销。

"门 1"为什么一次比较就够:preempt_count 的打包

	union {
		u64		preempt_count;	/* 0 => preemptible, <0 => bug */
		struct {
#ifdef CONFIG_CPU_BIG_ENDIAN
			u32	need_resched;
			u32	count;
#else
			u32	count;
			u32	need_resched;
#endif
		} preempt;
	};
#define PREEMPT_NEED_RESCHED	BIT(32)
#define PREEMPT_ENABLED	(PREEMPT_NEED_RESCHED)

static inline void set_preempt_need_resched(void)
{
	current_thread_info()->preempt.need_resched = 0;   /* 取反存储:0 表示"需要" */
}
...
static inline bool should_resched(int preempt_offset)   /* :77 */
{
	u64 pc = READ_ONCE(current_thread_info()->preempt_count);
	return pc == preempt_offset;
}

need_resched 取反存储,于是"抢占计数为 0 且需要调度"等价于整个 64 位字为 0。preempt_enable() 的检查、中断返回的检查都只需一次 64 位 load 加一次比较。RT 内核里抢占点比非 RT 密集得多(每次 spin_unlock()、local_bh_enable()、rcu_read_unlock() 都可能是),这个设计直接受益。

跨 CPU 唤醒时,远端只能设 TIF_NEED_RESCHED(thread_info 的 flags),再发 IPI;本 CPU 收到后由 scheduler_ipi() 调 preempt_fold_need_resched() 把它"折叠"进 preempt.need_resched(include/linux/sched.h:1976),中断返回时门 1 才能看到。

用户态出口:exit_to_user_mode()

static void do_notify_resume(struct pt_regs *regs, unsigned long thread_flags)
{
	do {
		local_irq_enable();

		if (thread_flags & _TIF_NEED_RESCHED)
			schedule();
		...
		if (thread_flags & (_TIF_SIGPENDING | _TIF_NOTIFY_SIGNAL))
			do_signal(regs);
		...
		if (thread_flags & _TIF_FOREIGN_FPSTATE)
			fpsimd_restore_current_state();

		local_irq_disable();
		thread_flags = read_thread_flags();
	} while (thread_flags & _TIF_WORK_MASK);
}

从用户态被打断的情况与 RT 无关——任何抢占模型下,返回用户态都是安全的调度点。RT 的价值体现在内核态:一个正在执行系统调用、持有 spinlock_t 的低优先级任务,在 RT 下也能在 __el1_irq 返回时被抢占。

用户态 RT 任务被网卡中断打断 → irq 线程抢占 → 再回到用户态 PREEMPT_RT · ARM64 · EL0
  1. 用户态代码(FIFO 40 的任务)PSTATE.I = 0,正在 EL0 运行
  2. VBAR_EL1 + 0x480 → el0t_64_irqentry.S:531kernel_entry 0:保存 x0–x30、sp_el0、ELR/SPSR 到 pt_regs
  3. el0t_64_irq_handler() → __el0_irq_handler_common()entry-common.c:945 / 940
  4. el0_interrupt()entry-common.c:923enter_from_user_mode;DAIF_PROCCTX_NOIRQ
  5. do_interrupt_handler() → call_on_irq_stack()entry-common.c:304 · entry.S:876
  6. gic_handle_irq() → … → handle_fasteoi_irq()irq-gic-v3.c:964 · chip.c:687mask + primary 唤醒 irq/45(FIFO 50)
  7. try_to_wake_up() → wakeup_preempt() → resched_curr()50 > 40:本 CPU,set_tsk_need_resched + set_preempt_need_resched
  8. irq_exit_rcu()kernel/softirq.c:672
  9. exit_to_user_mode() → exit_to_user_mode_prepare()entry-common.c:176 / 161flags 含 _TIF_NEED_RESCHED
  10. do_notify_resume() → schedule()entry-common.c:130 · sched/core.c:6812
  11. __schedule(SM_NONE) → context_switch() → switch_to()sched/core.c:6613 / 5296切到 irq/45 线程,处理完它睡回去,再切回本任务
  12. …… irq/45 运行完毕,schedule() 返回 ……
  13. local_daif_mask() → ret_to_user → kernel_exit 0 → ERETentry.S:612
  14. 用户态代码继续
硬件 / 异常入口硬中断上下文内核态任务上下文用户态

8.4上下文切换:cpu_switch_to 只存 13 个寄存器

RT 内核切换更频繁(每个线程化中断至少两次),切换本身的开销值得看一眼。通用层 context_switch() → switch_to() 宏 → ARM64 的 __switch_to()(arch/arm64/kernel/process.c:577)先处理"惰性"状态:

  • fpsimd_thread_switch(next):保存当前的 FPSIMD/SVE 状态(用户态或内核态 NEON),不立即恢复下一任务的用户态 FPSIMD,只设 TIF_FOREIGN_FPSTATE,等它返回用户态时在 do_notify_resume() 里恢复。
  • tls_thread_switch()(TPIDR_EL0)、entry_task_switch()、ssbs_thread_switch()、mte_thread_switch() 等。

然后是真正的切换:

SYM_FUNC_START(cpu_switch_to)
	save_and_disable_daif x11
	mov	x10, #THREAD_CPU_CONTEXT
	add	x8, x0, x10
	mov	x9, sp
	stp	x19, x20, [x8], #16		// store callee-saved registers
	stp	x21, x22, [x8], #16
	stp	x23, x24, [x8], #16
	stp	x25, x26, [x8], #16
	stp	x27, x28, [x8], #16
	stp	x29, x9, [x8], #16
	str	lr, [x8]
	add	x8, x1, x10
	ldp	x19, x20, [x8], #16		// restore callee-saved registers
	...
	ldr	lr, [x8]
	mov	sp, x9
	msr	sp_el0, x1			// sp_el0 = current(task_struct 指针)
	ptrauth_keys_install_kernel x1, x8, x9, x10
	scs_save x0
	scs_load_current
	restore_irq x11
	ret
SYM_FUNC_END(cpu_switch_to)

只有 callee-saved 的 x19–x29、sp、lr 需要保存——因为 cpu_switch_to 是一次普通函数调用,caller-saved 寄存器已由编译器处理。sp_el0 在内核态被复用为 current 指针,这也是 current 在 ARM64 上只需一条 mrs 的原因。页表切换(TTBR0)在更早的 switch_mm 里完成,内核线程之间切换(比如 irq 线程 ↔ ksoftirqd)不切换地址空间,开销更小。

8.5FPSIMD:ARM64 唯一需要 RT 专用分支的地方

ARM64 的 FPSIMD/SVE 寄存器是惰性管理的:CPU 上"当前装着谁的 FP 状态"记在 per-CPU 变量 fpsimd_last_state 里。修改这些 per-CPU 状态时必须保证不被打断去做别的 FP 操作。非 RT 下用 local_bh_disable() 同时达到两个目的——既挡软中断(软中断里可能用 kernel-mode NEON 做加密/校验和),也顺带关了抢占。

非 RT 下 local_bh_disable() 的隐含效果
preempt_count += SOFTIRQ_DISABLE_OFFSET
  → 挡住软中断   ✓
  → 不可抢占     ✓(白嫖的副作用)
RT 下 local_bh_disable() 的真实效果
拿 per-CPU softirq_ctrl.lock(睡眠锁)
  → 挡住同 CPU 的软中断处理  ✓
  → 仍然可抢占                ✗
     另一个任务可以上来改 FP 状态
 * On RT kernels local_bh_disable() is not sufficient because it only
 * serializes soft interrupt related sections via a local lock, but stays
 * preemptible. Disabling preemption is the right choice here as bottom
 * half processing is always in thread context on RT kernels so it
 * implicitly prevents bottom half processing as well.
 */
static void get_cpu_fpsimd_context(void)
{
	if (!IS_ENABLED(CONFIG_PREEMPT_RT))
		local_bh_disable();
	else
		preempt_disable();
}

RT 下软中断永远在线程里跑,关抢占就自动挡住了它们,所以一个 preempt_disable() 同时满足两个条件。

kernel-mode NEON 在 6.12 中是可抢占的

容易误解的一点:kernel_neon_begin() / kernel_neon_end() 之间并不是不可抢占区。从 6.8 起 ARM64 引入 TIF_KERNEL_FPSTATE:kernel_neon_begin() 只在保存用户态 FP 状态的那一小段里持有 get_cpu_fpsimd_context(),然后设置该标志并放开;之后任务被抢占时,fpsimd_thread_switch() 会把它的内核态 NEON 寄存器存起来,切回时再恢复(fpsimd.c:1565)。

void kernel_neon_begin(void)
{
	...
	get_cpu_fpsimd_context();

	if (test_thread_flag(TIF_KERNEL_FPSTATE)) {
		/* 嵌套使用:只可能是软中断打断了任务级 kernel NEON —— RT 下不存在 */
		BUG_ON(IS_ENABLED(CONFIG_PREEMPT_RT) || !in_serving_softirq());
		fpsimd_save_kernel_state(current);
	} else {
		fpsimd_save_user_state();
		/* RT:软中断在独立线程里,不会打断任务级 kernel NEON,
		 *     所以总是可以设标志,让内核态 NEON 状态随上下文切换保存 */
		if (IS_ENABLED(CONFIG_PREEMPT_RT) || !in_serving_softirq())
			set_thread_flag(TIF_KERNEL_FPSTATE);
	}
	fpsimd_flush_cpu_state();

	put_cpu_fpsimd_context();       /* ← 返回时已经重新可抢占 */
}

非 RT 与 RT 的根本区别可以总结为两句:

非 RT:软中断可以打断任务(会发生 NEON 嵌套),但持 bh 的任务不可抢占
RT  :软中断是线程,不会打断任务(NEON 嵌套不存在),但任务处处可抢占

所以 RT 下"嵌套 kernel NEON"被 BUG_ON 直接判为不可能,kernel_neon_end()(fpsimd.c:1939)也只需清标志。may_use_simd() 在硬中断、关中断、NMI 中返回假(arch/arm64/include/asm/simd.h:24),这些上下文里只能走标量回退路径。

8.6IRQ 栈与软中断栈

ARM64 每个 CPU 有一个独立 IRQ 栈(IRQ_STACK_SIZE = THREAD_SIZE,无 KASAN 时 16 KiB,arch/arm64/include/asm/memory.h:144)。do_interrupt_handler() 发现自己还在任务栈上时,用 call_on_irq_stack() 切过去。非 RT 下软中断也借这个栈:

#ifndef CONFIG_PREEMPT_RT
static void ____do_softirq(struct pt_regs *regs)
{
	__do_softirq();
}

void do_softirq_own_stack(void)
{
	call_on_irq_stack(NULL, ____do_softirq);
}
#endif

RT 下软中断在 ksoftirqd 或 irq 线程里、用线程自己的内核栈执行,这个函数没有调用者,于是整段不编译(引入它的提交是 8eb858c44b98 "arm64: run softirqs on the per-CPU IRQ stack")。结果是 RT 下 IRQ 栈上只会出现真正的硬中断帧,栈深度更可预测。

8.7GICv3 在 RT 内核里:优先级、PMR 与"故意不嵌套"

Linux 只用三个优先级

GICv3 支持最多 256 级优先级和抢占嵌套(ICC_BPR1_EL1 分组、ICC_RPR_EL1 运行优先级)。Linux 一个都不用来做设备之间的分级:

#define GICV3_PRIO_UNMASKED	0xe0    /* PMR 设为它:IRQ 和 NMI 都放行 */
#define GICV3_PRIO_IRQ		0xc0    /* 所有普通中断统一用这个 */
#define GICV3_PRIO_NMI		0x80    /* pseudo-NMI 专用 */

#define GICV3_PRIO_PSR_I_SET	(1 << 4)
GIC 优先级:数值越小越高;只有优先级数值 < PMR 的中断才能送达 CPU 0x00 0x80 0xc0 0xe0 0xf0 0xff 最高 最低 pseudo-NMI perf / backtrace / CPU stop 所有普通 IRQ SPI / PPI / LPI / IPI PMR = 0xe0(IRQON) 开中断:0x80、0xc0 都能进 PMR = 0xc0(IRQOFF) 关中断:只有 0x80 能进 0xf0 = DEFAULT_PMR_VALUE(初始化) 普通 IRQ 之间同一优先级 + 取异常时 PSTATE.I=1 → 一个 handler 运行时,同一 CPU 上的其他普通 IRQ 一律等待
GICv3 优先级刻度。普通中断全在 0xc0,pseudo-NMI 在 0x80;PMR 在 0xe0 与 0xc0 之间切换就实现了"开/关中断"。

为什么故意不用硬件嵌套?这和 RT 的理念完全一致:

  • 栈深不可预测:嵌套中断叠在同一个固定大小的 IRQ 栈上。
  • 最坏延迟不可分析:低优先级中断的延迟 = 所有更高优先级中断的总和,且组合爆炸。
  • 第二套调度器:GIC 优先级是硬件里的一套静态优先级,与调度器的任务优先级无法统一。RT 的选择正好相反——把中断变成线程,让唯一的调度器来排序。中断线程化之后,"网卡比串口重要"用 chrt 表达,而不是用 GICD_IPRIORITYR。

pseudo-NMI:PMR 代替 PSTATE.I

ARM64 没有架构级 NMI(ARMv8.8 的 FEAT_NMI 在 6.12 中尚未被 Linux 用于此目的)。打开 CONFIG_ARM64_PSEUDO_NMI 并在命令行加 irqchip.gicv3_pseudo_nmi=1(arch/arm64/kernel/cpufeature.c:1097)后,local_irq_disable() 不再置 PSTATE.I,而是写 ICC_PMR_EL1:

static __always_inline void __daif_local_irq_disable(void)
{
	barrier();
	asm volatile("msr daifset, #3");          /* 置 I 和 F */
	barrier();
}

static __always_inline void __pmr_local_irq_disable(void)   /* :59 */
{
	...
	barrier();
	write_sysreg_s(GIC_PRIO_IRQOFF, SYS_ICC_PMR_EL1);       /* 0xc0:只放行 NMI */
	barrier();
}

代价与 RT 相关的有三点:

  1. 开关中断变贵:写 PMR 后在某些实现上需要 pmr_sync()(DSB),比 msr daifset 慢。RT 内核里显式关中断的地方少了很多(spin_lock_irqsave 不再关中断),这一代价被部分抵消。
  2. 中断入口更复杂:kernel_entry 在 IRQ 入口把 PMR 存进 pt_regs,并改成 GIC_PRIO_IRQON | GIC_PRIO_PSR_I_SET(entry.S:319);__gic_handle_irq_from_irqson() 读 IAR 后查 ICC_RPR_EL1 判断是不是 NMI(irq-gic-v3.c:904),再决定走 NMI 还是 IRQ 路径。
  3. NMI 上下文不可调度:arm64_preempt_schedule_irq() 的"门 2"就是为它准备的。NMI handler(perf 采样、hardlockup 检测)的执行时间是 RT 内核里另一个连关中断都挡不住的延迟来源;对延迟极敏感的系统,通常不打开 pseudo-NMI,或者只在调试时打开。

GIC 中断状态机

stateDiagram-v2
  [*] --> Inactive
  Inactive --> Pending: 设备触发 / 写 SGI1R / ITS 翻译
  Pending --> Active: CPU 读 ICC_IAR1_EL1 (ack)
  Active --> ActivePending: 电平仍有效 或 边沿再次到来
  ActivePending --> Pending: deactivate
  Active --> Inactive: deactivate (EOImode0 写 EOIR, EOImode1 写 DIR)
  Pending --> Inactive: 设备撤销电平
  note right of Active
    EOImode1 下写 ICC_EOIR1_EL1 只做 priority drop,
    状态仍是 Active,直到写 ICC_DIR_EL1
  end note
GICv3 中断状态机。RT 的 ONESHOT 线程化不依赖 Active 状态来挡住重复中断,而是在 Distributor 里清 enable 位(见 第 5 章)。

8.8不会被线程化的 ARM64 中断

IPI 是 SGI,用 request_percpu_irq 注册

enum ipi_msg_type {
	IPI_RESCHEDULE,
	IPI_CALL_FUNC,
	IPI_CPU_STOP,
	IPI_CPU_STOP_NMI,
	IPI_TIMER,
	IPI_IRQ_WORK,
	...
};

这些 IPI 在 set_smp_ipi_range() 里用 request_percpu_irq()(或可作为 NMI 的用 request_percpu_nmi())注册(smp.c:1083)。SGI 走 handle_percpu_devid_irq,带 IRQ_NOTHREAD,永远在硬中断里执行。其中对 RT 最关键的是 IPI_RESCHEDULE:远端唤醒一个 RT 任务时,arch_smp_send_reschedule()(smp.c:1099)→ gic_ipi_send_mask() → gic_send_sgi() 写 ICC_SGI1R_EL1(irq-gic-v3.c:1413),目标 CPU 进入中断后 do_handle_IPI() 调 scheduler_ipi(),在中断返回时完成抢占。

sequenceDiagram
  participant C0 as CPU0 (唤醒者)
  participant GIC as GICv3 Redistributor
  participant C3 as CPU3 (运行 FIFO 10 任务)
  C0->>C0: try_to_wake_up(RT 任务, 亲和 CPU3)
  C0->>C0: ttwu_queue / activate, resched_curr(rq3)
  C0->>C0: 设 TIF_NEED_RESCHED, smp_send_reschedule(3)
  C0->>GIC: msr ICC_SGI1R_EL1 (SGI IPI_RESCHEDULE, 目标 CPU3)
  GIC->>C3: SGI 送达
  C3->>C3: el1h_64_irq 或 el0t_64_irq
  C3->>C3: gic_handle_irq, handle_percpu_devid_irq
  C3->>C3: ipi_handler, do_handle_IPI, scheduler_ipi
  Note over C3: preempt_fold_need_resched 写 preempt.need_resched=0
  C3->>C3: 中断返回: arm64_preempt_schedule_irq 或 do_notify_resume
  C3->>C3: __schedule, cpu_switch_to 到 RT 任务
跨 CPU 唤醒的完整硬件路径。如果目标 CPU 正处于不可抢占区(非 RT 下的 spinlock 临界区),第 9 步之后还要等它自己走到 preempt_enable();RT 的意义就是让这种区间尽可能短。

arch timer 是 PPI

ARM 通用定时器(CNTP/CNTV)通过 PPI 送达(常见是虚拟定时器 INTID 27、非安全物理定时器 30,由 DT/ACPI 描述),驱动用 request_percpu_irq(ppi, arch_timer_handler_virt, ...) 注册(drivers/clocksource/arm_arch_timer.c:1251)。它的 handler 调用 evt->event_handler,高精度模式下就是 hrtimer_interrupt()——在 RT 下只直接执行标了 HRTIMER_MODE_*_HARD 的定时器(包括 RT 任务的 clock_nanosleep 唤醒和 tick),其余推给 HRTIMER_SOFTIRQ(第 6 章)。cyclictest 测到的"中断延迟"正是这条 PPI 路径。

ITS / LPI 与 MSI

PCIe 设备的 MSI/MSI-X 经 ITS 翻译为 LPI(INTID ≥ 8192),走 handle_fasteoi_irq,可以线程化。LPI 的 mask/unmask 不是写 GICD_ICENABLER,而是改内存中的 LPI 配置表并通过 ITS 命令(INV)使其生效,同样有不可忽略的成本。多队列网卡每个队列一个 MSI-X 向量、一个 irq/N-xxx 线程,可以分别绑核、分别设优先级——这是在 RT 系统里隔离网络负载的基本手段。ITS 驱动内部的锁都是 raw_spinlock_t(drivers/irqchip/irq-gic-v3-its.c:100 起),因为它们在硬中断和 irqchip 回调里被使用。

KVM

ARM64 KVM 的虚拟定时器仿真必须及时把中断注入 vCPU,因此三处 hrtimer 都显式用 HRTIMER_MODE_ABS_HARD(arch/arm64/kvm/arch_timer.c:215, 993, 1021);vGIC 的 irq_lock、ap_list_lock 是 raw_spinlock_t(include/kvm/arm_vgic.h:119, 336)。没有 _HARD,RT 下这些定时器会被推到软中断线程,客户机时钟中断的抖动就取决于宿主机 ksoftirqd 的调度。

8.9RT 管不了的 ARM64 硬件与固件延迟

RT 能控制的只是"Linux 内核什么时候愿意让出 CPU"。下面这些发生在内核视野之外或硬件内部,PREEMPT_RT 不改变它们:

来源机制缓解手段
EL3 固件(TF-A)PSCI 调用(CPU_ON/OFF、CPU_SUSPEND)、SMC 服务、SoC 私有 SMC;EL3 运行时 Linux 完全停顿审查固件;避免运行期频繁 SMC
SDEI固件直接向 Linux 投递事件(类似 NMI),绕过 GIC 屏蔽RT 系统上不启用或确认用途
cpuidle 深睡退出WFI 很快,但 PSCI 深度状态(核心/簇掉电)的退出延迟可达数十到上百微秒量级限制 idle 深度(/dev/cpu_dma_latency、cpuidle.off=1)。6.12 的 cpuidle-psci 在 RT 下把簇级(domain)idle 状态限制在系统休眠时才用(drivers/cpuidle/cpuidle-psci.c:243–248、cpuidle-psci-domain.c:70–76)
DVFS调频调压期间的切换窗口;低频下所有路径整体变慢performance governor,固定频率
TLB / 缓存维护广播TLBI ...IS + DSB ISH 需要所有核响应;munmap、页迁移、KSM 等都会触发RT 进程预先 mlockall,避免运行期大规模映射变更;隔离核
缓存与总线争用共享 L3/DSU、内存控制器、其他 master 的 DMA核隔离、缓存划分(MPAM,取决于 SoC 与内核支持)
虚拟化vCPU 被宿主机抢占、stage-2 缺页宿主机也跑 RT、vCPU 钉核
定位延迟时的分工

rtla timerlat 报告的"IRQ latency"包含从定时器到期到 arch timer handler 开始执行的全部时间:GIC 送达、关中断窗口、EL3/SDEI 打断、idle 退出。如果这个数字已经很大,问题多半在硬件、固件或残余的关中断区;如果 IRQ latency 小而"Thread latency"大,问题在调度路径——这才是 RT 补丁负责的部分。详见 第 10 章。

8.10本章小结

  • ARM64 为 RT 只改了 3 个文件 11 行;真正的前提是:中断返回内核态时有抢占点(arm64_preempt_schedule_irq)、返回用户态时检查重调度、上下文切换廉价、FPSIMD 的 per-CPU 状态不再依赖 local_bh_disable 关抢占、软中断不再借用 IRQ 栈。
  • preempt_count 与 need_resched 打包成一个 64 位字,让 RT 下密集的抢占检查只需一次比较。
  • GICv3 在 Linux 中只用三个优先级、故意不嵌套;RT 把中断间的分级交给调度器(线程优先级),而不是 GIC。
  • IPI(SGI)、arch timer(PPI)、pseudo-NMI 始终在硬中断/NMI 上下文执行,构成 RT 的延迟下限;固件、idle、DVFS、TLB 广播等硬件因素在 RT 视野之外,需要系统层面配置。