Linux 内核笔记之高层中断处理

本文基于Linux kernel 4.19.0, 体系结构是aarch64

中断处理入口

ARM GICv3 GIC代码分析一文中,有分析到在GIC 控制器初始化时会设置ARM中断控制器的中断处理函数 handle_arch_irq。

static int __init gic_init_bases(void __iomem *dist_base,
   			 struct redist_region *rdist_regs,
   			 u32 nr_redist_regions,
   			 u64 redist_stride,
   			 struct fwnode_handle *handle)
{
   set_handle_irq(gic_handle_irq);      ------- (1)
}
#ifdef CONFIG_GENERIC_IRQ_MULTI_HANDLER
int __init set_handle_irq(void (*handle_irq)(struct pt_regs *))
{
	if (handle_arch_irq)
		return -EBUSY;

	handle_arch_irq = handle_irq;
	return 0;
}
#endif

那么handle_arch_irq被谁调用的呢?

中断可以通过异常向量表的形式提交给 CPU,CPU 在发生硬件中断时跳转到 el1_irq 或者 el0_irq 这样的函数体内
arch/arm64/kernel/entry.S 文件的函数 elx_irq 中进入 irq_handler 这个宏函数

	.macro	irq_handler
	ldr_l	x1, handle_arch_irq
	mov	x0, sp
	irq_stack_entry
	blr	x1
	irq_stack_exit
	.endm

	.text

因此arm64 中断的入口函数从中断向量表中__irq_svc–>irq_handler–>handle_arch_irq–>gic_handle_irq

GIC控制器中断处理过程


gic_handle_irq:[drivers/irqchip/irq-gic-v3.c]

static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
	u32 irqnr;

	do {
		irqnr = gic_read_iar();        ------------- (1)

		if (likely(irqnr > 15 && irqnr < 1020) || irqnr >= 8192) {   ------------ (2)
			int err;

			if (static_branch_likely(&supports_deactivate_key))
				gic_write_eoir(irqnr);                           ------------(3)
			else
				isb();

			err = handle_domain_irq(gic_data.domain, irqnr, regs);   ------------- (4)
			if (err) {
				WARN_ONCE(true, "Unexpected interrupt received!\n");
				if (static_branch_likely(&supports_deactivate_key)) {
					if (irqnr < 8192)
						gic_write_dir(irqnr);          
				} else {
					gic_write_eoir(irqnr);
				}
			}
			continue;
		}
		if (irqnr < 16) {
			gic_write_eoir(irqnr);
			if (static_branch_likely(&supports_deactivate_key))
				gic_write_dir(irqnr);                            ------------- (5)
#ifdef CONFIG_SMP
			/*
			 * Unlike GICv2, we don't need an smp_rmb() here.
			 * The control dependency from gic_read_iar to
			 * the ISB in gic_write_eoir is enough to ensure
			 * that any shared data read by handle_IPI will
			 * be read after the ACK.
			 */
			handle_IPI(irqnr, regs);                  
#else
			WARN_ONCE(true, "Unexpected SGI received!\n");
#endif
			continue;
		}
	} while (irqnr != ICC_IAR1_EL1_SPURIOUS);
}

(1) gic_read_iar。CPU通过读取GIC控制器的GICC_IAR( Interrupt Acknowledge Register)寄存器, 应答该中断, 并且可以得到当前发生中断的是哪一个硬件中断号。

(2) 硬件中断号0-15表示SGI类型的中断,15~1020 表示外设中断(SPI或PPI类型),8192 - MAX表示LPI类型的中断。

硬件中断号 中断类型
0-15 SGI
16 - 31 PPI
32 - 1019 SPI
1020 - 1023 用于指示特殊情况的特殊中断
1024 - 8191 Reservd
8192 - MAX LPI

(3) 和 (5) : gic_write_eoir 和 gic_write_dir放在一起分析。这两个函数分别往ICC_EOIR1_EL1和ICC_DIR_EL1寄存器中写入硬件中断号。
ICC_EOIR1_EL1, Interrupt Controller End Of Interrupt Register 1, 对寄存器的写操作表示中断的结束
ICC_DIR_EL1, Interrupt Controller Deactivate Interrupt Register, 对该寄存器的写操作将deactive指定的中断。

那么问题来了? 假设读取到了一个SPI中断, 为什么一开始就写EOI表示中断结束,此时中断处理不是还没有执行么?
在GIC v3协议中定义, 处理完中断后,软件必须通知中断控制器已经处理了中断,以便状态机可以转换到下一个状态。
GICv3架构将中断的完成分为2个阶段:
Priority Drop: 将运行优先级降回到中断之前的值。
**Deactivation:**更新当前正在处理的中断的状态机。 从活动状态转换到非活动状态。
这两个阶段可以在一起完成,也可以分为2步完成。 却决于EOImode的值。
如果EOIMode = 0, 对ICC_EOIR1_EL1寄存器的操作代表2个阶段(priority drop 和 deactivation)一起完成。
如果EOImode = 1, 对ICC_EOIR1_EL1寄存器的操作只会导致Priority Drop, 如果想要表示中断已经处理完成,还需要写ICC_DIR_EL1。

所以回答上面的问题, 当前Linux GIC的代码,默认irq chip是EIOmode=1, 所以单独的写EOIR1_EL1不是代表中断结束。

static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
			      irq_hw_number_t hw)
{
	if (static_branch_likely(&supports_deactivate_key))
		chip = &gic_eoimode1_chip;
}

(4)handle_domain_irq. 中断控制器中断处理的主体。


handle_domain_irq:[kernel/irq/irqdesc.c]

int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
			bool lookup, struct pt_regs *regs)
{
	struct pt_regs *old_regs = set_irq_regs(regs);
	unsigned int irq = hwirq;
	int ret = 0;

	irq_enter();                             ------------------- (1)

#ifdef CONFIG_IRQ_DOMAIN
	if (lookup)
		irq = irq_find_mapping(domain, hwirq);       --------------- (2)
#endif

	/*
	 * Some hardware gives randomly wrong interrupts.  Rather
	 * than crashing, do something sensible.
	 */
	if (unlikely(!irq || irq >= nr_irqs)) {
		ack_bad_irq(irq);
		ret = -EINVAL;
	} else {
		generic_handle_irq(irq);                  ---------------- (4)
	}

	irq_exit();                              --------------- (3)
	set_irq_regs(old_regs);
	return ret;
}

(1) irq_enter显式的告诉Linux 内核现在要进入中断上下文了。

#define __irq_enter()					\
	do {						\
		account_irq_enter_time(current);	\
		preempt_count_add(HARDIRQ_OFFSET);	\
		trace_hardirq_enter();			\
	} while (0)

__irq_enter宏通过preempt_count_add()增加当前进程struct thread_info中的preempt_count成员里的HARDIRQ域的值。

(2) irq_find_mapping()通过硬件中断号去查找IRQ中断号。

(3) irq_exit() 表示硬件中断处理已经完成。与irq_enter()相反, 通过preempt_count_sub(),减少HARDIRQ域的值。

(4) 接下来看generic_handle_irq()函数


generic_handle_irq:[kernel/irq/irqdesc.c]
generic_handle_irq–>generic_handle_irq_desc

static inline void generic_handle_irq_desc(struct irq_desc *desc)
{
	desc->handle_irq(desc);             ------------- (1)
}

(1) 调用desc->handle_irq指向的回调函数.

这个回调函数是在哪里定义的呢?
在解析ACPI表或者DTS时,会枚举映射中断号(参考:硬中断和软中断的映射
irq_of_parse_and_map-> irq_create_of_mapping->irq_create_fwspec_mapping->irq_domain_alloc_irqs

irq_domain_alloc_irqs会调用irq_domain_ops定义的alloc函数

static const struct irq_domain_ops gic_irq_domain_ops = {
	.translate = gic_irq_domain_translate,
	.alloc = gic_irq_domain_alloc,
	.free = gic_irq_domain_free,
	.select = gic_irq_domain_select,
};

gic_irq_domain_alloc()->gic_irq_domain_map(), 然后定义irq_desc->handle_irq的回调函数

static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,
			      irq_hw_number_t hw)
{
	struct irq_chip *chip = &gic_chip;

	if (static_branch_likely(&supports_deactivate_key))
		chip = &gic_eoimode1_chip;
		
	/* PPIs */
	if (hw < 32) {
		irq_set_percpu_devid(irq);
		irq_domain_set_info(d, irq, hw, chip, d->host_data,
				    handle_percpu_devid_irq, NULL, NULL);
		irq_set_status_flags(irq, IRQ_NOAUTOEN);
	}
	/* SPIs */
	if (hw >= 32 && hw < gic_data.irq_nr) {
		irq_domain_set_info(d, irq, hw, chip, d->host_data,
				    handle_fasteoi_irq, NULL, NULL);
		irq_set_probe(irq);
		irqd_set_single_target(irq_desc_get_irq_data(irq_to_desc(irq)));
	}
	/* LPIs */
	if (hw >= 8192 && hw < GIC_ID_NR) {
		if (!gic_dist_supports_lpis())
			return -EPERM;
		irq_domain_set_info(d, irq, hw, chip, d->host_data,
				    handle_fasteoi_irq, NULL, NULL);
	}

	return 0;
}

SPI和LPI类型的中断, desc->handler()回调函数是handle_fasteoi_irq()。
PPI类型的中断, desc->handler()回调函数是handle_percpu_devid_irq()。


handle_fasteoi_irq:[kernel/irq/chip.c]

void handle_fasteoi_irq(struct irq_desc *desc)
{
	struct irq_chip *chip = desc->irq_data.chip;

	raw_spin_lock(&desc->lock);

	if (!irq_may_run(desc))
		goto out;

	desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);

	/*
	 * If its disabled or no action available
	 * then mask it and get out of here:
	 */
	if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {    -------- (1)
		desc->istate |= IRQS_PENDING;
		mask_irq(desc);
		goto out;
	}

	kstat_incr_irqs_this_cpu(desc);               ------------- (2)
	if (desc->istate & IRQS_ONESHOT)
		mask_irq(desc);

	preflow_handler(desc);                       
	handle_irq_event(desc);                      -------------- (3)

	cond_unmask_eoi_irq(desc, chip);              -------------- (4)

	raw_spin_unlock(&desc->lock);
	return;
out:
	if (!(chip->flags & IRQCHIP_EOI_IF_HANDLED))
		chip->irq_eoi(&desc->irq_data);
	raw_spin_unlock(&desc->lock);
}
EXPORT_SYMBOL_GPL(handle_fasteoi_irq);

(1) 如果某个中断没有定义action描述符或者该中断被关闭了IRQD_IRQ_DISABLED, 那么设置该中断状态为IRQS_PENDING, 并调用irq_mask()函数屏蔽该中断。

(2) 我们一般在终端通过cat /proc/intterrupts查看中断计数, 这个计数是在这里进行增加的。

(3) handle_irq_event()是中断处理的核心函数,开始真正的处理硬件中断了。这一部分放在下面分析。

(4) cond_unmask_eoi_irq(). 呼应gic_handle_irq入口函数的EOI处理, 这里写EOI寄存器,表示完成了硬中断处理。


handle_irq_event:[kernel/irq/handle.c]

irqreturn_t handle_irq_event(struct irq_desc *desc)
{
	irqreturn_t ret;

	desc->istate &= ~IRQS_PENDING;
	irqd_set(&desc->irq_data, IRQD_IRQ_INPROGRESS);
	raw_spin_unlock(&desc->lock);

	ret = handle_irq_event_percpu(desc);

	raw_spin_lock(&desc->lock);
	irqd_clear(&desc->irq_data, IRQD_IRQ_INPROGRESS);
	return ret;
}

首先把pending标志位清除,然后设置IRQD_IRQ_INPROGRESS标志,表示正在处理硬件中断。

handle_irq_event()->handle_percpu_devid_irq()->__handle_irq_event_percpu()

irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{
	irqreturn_t retval = IRQ_NONE;
	unsigned int irq = desc->irq_data.irq;
	struct irqaction *action;

	record_irq_time(desc);

	for_each_action_of_desc(desc, action) {
		irqreturn_t res;

		trace_irq_handler_entry(irq, action);
		res = action->handler(irq, action->dev_id);  -------------- (1)
		trace_irq_handler_exit(irq, action, res);

		if (WARN_ONCE(!irqs_disabled(),"irq %u handler %pF enabled interrupts\n",
			      irq, action->handler))
			local_irq_disable();

		switch (res) {
		case IRQ_WAKE_THREAD:
			/*
			 * Catch drivers which return WAKE_THREAD but
			 * did not set up a thread function
			 */
			if (unlikely(!action->thread_fn)) {
				warn_no_thread(irq, action);
				break;
			}

			__irq_wake_thread(desc, action);    ----------- (2)

			/* Fall through to add to randomness */
		case IRQ_HANDLED:                ---------- (3)
			*flags |= action->flags;          
			break;

		default:
			break;
		}

		retval |= res;
	}

	return retval;
}

(1) 用irq号找到对应的irq_desc,irq_desc->action->handler就是注册中断时,申请的中断处理函数。

(2) 如果中断号的处理函数返回IRQ_WAKE_THREAD,表示需要唤醒中断线程。
__irq_wake_thread()->唤醒中断线程->irq_thread()

static irqreturn_t irq_thread_fn(struct irq_desc *desc,
		struct irqaction *action)
{
	irqreturn_t ret;

	ret = action->thread_fn(action->irq, action->dev_id);
	irq_finalize_oneshot(desc, action);
	return ret;
}

action()->thread_fn()对应的是request_threaded_irq()时, 中断线程执行的部分.

中断申请API:request_threaded_irq

int request_threaded_irq(unsigned int irq, irq_handler_t handler,
			 irq_handler_t thread_fn, unsigned long irqflags,
			 const char *devname, void *dev_id)

@handler: Function to be called when the IRQ occurs. Primary handler for threaded interrupts.
If NULL and thread_fn != NULL the default primary handler is installed
@thread_fn: Function called from the irq handler thread.
If NULL, no irq thread is created

(3) 如果中断号的处理函数返回IRQ_HANDLED,说明该action的中断处理函数已经处理完毕。


总结

以一个流程图对本文进行补充:
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章