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的中斷處理函數已經處理完畢。


總結

以一個流程圖對本文進行補充:
在這裏插入圖片描述

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