Linux 中断会产生嵌套吗?

发布于:2024-04-17 ⋅ 阅读:(18) ⋅ 点赞:(0)

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. Linux 中断是否会嵌套?

2.1 分析背景

本文基于 ARMv7 架构,Linux 4.14 内核进行分析。

2.2 中断处理抢占、嵌套可能性分析

ARMv7 架构下的 IRQ 中断处理流程概要如下(使用 GICv2 中断芯片):

/* arch/arm/kernel/entry-armv.S */

/*
 * Interrupt dispatcher
 */
vector_stub irq, IRQ_MODE, 4 /* vector_irq */
	.long __irq_usr   @  0  (USR_26 / USR_32)
	......
	.long __irq_svc   @  3  (SVC_26 / SVC_32)
	......
	
	.align 5
__irq_svc:
	// 保存被中断的上下文,切换到 SVC 模式复用内核栈
	svc_entry

	// 中断处理
	irq_handler

	...

	// 恢复被中断的上下文, 使能 CPU 中断,同时从中断返回
	svc_exit r5, irq = 1   @ return from exception
ENDPROC(__irq_svc)

/*
 * Interrupt handling.
 */
.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
	// GICv2 芯片中断处理接口
	ldr r1, =handle_arch_irq /* r1 = gic_handle_irq() */
	mov r0, sp
	badr lr, 9997f
	ldr pc, [r1]
#else
	arch_irq_handler_default
#endif
9997:
 .endm

在一进步描述之前,必须先说明一个很重要的事实:ARMv7 架构 CPU ,在进入中断模式的时候,会自动禁用当前 CPU 上中断,这是硬件自动完成的,无需软件干预。也就是说,当前的代码时工作在 CPU 中断禁用的状态下。继续看后续中断处理流程:

gic_handle_irq() /* drivers/irqchip/irq-gic.c */
	...
	/*
	 * 这个循环试图一次处理多个中断, 而不是每次中断都从 CPU 的
	 * 中断向量地址重新开始执行, 这是 GIC 对中断处理的一个优化。
	 */
	do {
		/*
		 * 读取硬件中断号.
		 * 从寄存器 GIC_CPU_INTACK 读取硬件中断号, GIC 认为 CPU 响应了中断.
		 */
		irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
		...

		if (likely(irqnr > 15 && irqnr < 1020)) { /* 处理 PPI, SPI */
			if (static_key_true(&supports_deactivate))
				// EOI mode 1: Priority drop 和 Deactive 分离
				// 这里 GIC 做 Priority drop,目的是允许同等优先级的中断进来
				writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI);
			handle_domain_irq(gic->domain, irqnr, regs);
			isb();
			continue; /* 继续处理下一中断, 可以避免反复的中断上下文切换 */
		}

		if (irqnr < 16) { /* 处理 SGI */
			// 这里不细表,不影响本文分析的主题
			......
		}
	}  while (1);;
handle_domain_irq(gic->domain, irqnr, regs);
	__handle_domain_irq(domain, hwirq, true, regs)
		irq_enter();
			__irq_enter();
				...
				// 标示在硬件中断处理过程中,即 top half
				preempt_count_add(HARDIRQ_OFFSET);
				...
	
	#ifdef CONFIG_IRQ_DOMAIN
		if (lookup)
			irq = irq_find_mapping(domain, hwirq); /* 查找 硬件中断号 @hwirq 对应的 Linux 中断号 */
	#endif
		generic_handle_irq(irq); /* 中断处理 */
			struct irq_desc *desc = irq_to_desc(irq);
			...
			generic_handle_irq_desc(desc);
				desc->handle_irq(desc);
					handle_fasteoi_irq(desc) // 处理 SPI 中断
			...
		...
		irq_exit(); /* 软中断, RCU 等等处理 */
handle_fasteoi_irq(desc) // 处理 SPI 中断
	...
	handle_irq_event(desc); // 调用具体的中断 handler, 我们不关注这里的细节
	...
	cond_unmask_eoi_irq(desc, chip);
		...
		chip->irq_eoi(&desc->irq_data);
			gic_eoimode1_eoi_irq()
				...
				// 这里是关注的重点。
				// 在前面 gic_irq_handler() 的处理逻辑里面,已经做了 
				// Priority drop,使得同等优先级的中断可以进来;在这里,
				// 做 Deactive interrupt,使得中断状态由 Active 转为
				// Idle 状态。到此,GIC 将能够重新向 CPU 发送中断信号了。
				// 从前面的分析中,我们了解到,CPU 的中断此时处于禁用状态,
				// 只等 CPU 重新启用中断,就能够收到 GIC 发给它的中断信号。
				writel_relaxed(gic_irq(d), gic_cpu_base(d) + GIC_CPU_DEACTIVATE);
		...
	...

handle_fasteoi_irq() 的流程告诉我们,在调用完中断 handler 后,GIC 已经能够发送中断信号给 CPU 了,只等 CPU 重新启用中断,就可以接收中断信号了。接着看 handle_fasteoi_irq() 之后的中断退出流程,主要是 irq_exit()

irq_exit() /* kernel/softirq.c */
	...
	// 标示退出硬件中断处理过程,即退出 top half
	preempt_count_sub(HARDIRQ_OFFSET);

	// 这里 !in_interrupt() 判定,在 ARMv7 下,实际等同于 !in_softirq(),
	// 防止在 中断抢占、嵌套下的 softirq 处理过程 (bottom half) 的重入.
	if (!in_interrupt() && local_softirq_pending())
		invoke_softirq(); // 处理 softirq ,即进入 bottom half
	...

这里的 !in_interrupt() 判定要重点关注下,先看下它的定义:

#define softirq_count() (preempt_count() & SOFTIRQ_MASK)
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
     | NMI_MASK))

#define in_softirq()  (softirq_count())
#define in_interrupt()  (irq_count())

看到了吧,在不支持 NMI 的 ARMv7 架构下,随着 preempt_count_sub(HARDIRQ_OFFSET);HARDIRQ 的计数归 0,此处的 !in_interrupt() 已经等同于 !in_softirq(),目的是防止在中断抢占、嵌套下的 softirq 处理过程 (bottom half) 的重入。这里提前做出结论会发生 中断抢占、嵌套,有点提前,后面会分析原因。继续看中断退出接下来的流程:

invoke_softirq(); // 处理 softirq ,即进入 bottom half
	...
	__do_softirq();
		...
		pending = local_softirq_pending(); /* 读取当前 CPU 挂起的 softirq 事件 */
		...
		__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET); /* 禁用 softirq (禁用 bottom half) */
		...
	restart:	
		set_softirq_pending(0); /* 清除当前 CPU 挂起的 softirq */
		
		/* 
		 * 重点来了,CPU 中断启用了,同时从前面分析看到,GIC 也准备好了, 
		 * 所以 CPU 可以接收中断了;同时,别忘记,我们此时还处在当前中断
		 * 处理上下文中!!! 如果在处理 softirq 期间 CPU 上发生了中断,是
		 * 会产生中断抢占、嵌套的。
		 */
		local_irq_enable();
		
		h = softirq_vec;

		while ((softirq_bit = ffs(pending))) {
			// softirq 处理,不是本文重点,不做细表
			...
		}

		// 处理完 softirq 后,重新禁用 CPU 中断,这和汇编代码中重启 CPU 中断对应
		local_irq_disable();
		...

		__local_bh_enable(SOFTIRQ_OFFSET); /* 使能 softirq (使能 bottom half) */
		...

2.3 中断处理抢占、嵌套小结

从上面分析我们知道,中断抢占、嵌套,在 Linux 内核下是可能发生的,不过这些 中断抢占、嵌套 的发生大概率是合理且无害的。为什么呢?首先,中断的抢占、嵌套,是不可能发生在中断处理 top half 部分的,因为 top half 的处理期间,CPU 中断都是处于禁用状态,中断的抢占、嵌套只可能发生在 bottom half(即 softirq 处理期间);其次,通过 !in_interrupt() 限制了抢占、嵌套的中断,无法在前一中断处理 bottom half 期间,再次进入 bottom half,这样就避免中断内核栈溢出的风险;最后,由于 CPU 进入中断处理时自动禁用了中断,所以抢占、嵌套的中断处理期间,在有前一中断处理 bottom half(即 softirq) 前提下,不会在中途使能 CPU 中断,这样就避免了中断反复嵌套的问题。发生嵌套后,后续中断必定是一个接一个的串行处理,简单来讲,中断嵌套最多只会有两重:第一个中断 bottom half 期间被中断,第二个抢占、嵌套中断进来。当然,这一切的前提是,中断内核栈空间能够满足这两重中断嵌套的使用。另外,说大概率无害,是因为中断的抢占、嵌套,仍然有可能导致 bottom half(即 softirq) 处理的延迟,毕竟它们中断了正在处理 softirq

3. 参考资料

[1] Interrupts