当前位置:首页>Linux>Linux中断处理全流程深度解析:从CPU跳转到驱动handler执行【第14篇·共15篇】

Linux中断处理全流程深度解析:从CPU跳转到驱动handler执行【第14篇·共15篇】

  • 2026-10-11 07:28:21
Linux中断处理全流程深度解析:从CPU跳转到驱动handler执行【第14篇·共15篇】
摘要

本文是《Linux中断子系统》系列第3章的第六篇(终篇),完整梳理Linux中断处理的全链路流程。从CPU侧出发,分析ARM64异常向量表(vectors)→ el0_irq/el1_irq → irq_handler宏 → handle_arch_irq(即gic_handle_irq)的跳转路径,并讲解保护现场(kernel_entry)、中断处理、恢复现场(kernel_exit)三个阶段。随后分析GIC层的处理:gic_handle_irq读取GICC_IAR获取hwirq,调用handle_domain_irq → irq_find_mapping将hwirq转为virq → generic_handle_irq → desc->handle_irq。深入讲解中断流控层(handle_fasteoi_irq和handle_edge_irq)的行为差异、IRQS_ONESHOT标志的作用机制、irq_may_run检查逻辑,以及PCIe MSI中断特有的mask_irq → its_mask_msi_irq → pci_msi_mask_irq → pci_msi_update_mask(写PCI配置空间Mask寄存器)流程。最后介绍用户驱动层request_irq/request_threaded_irq注册的irqaction如何被handle_irq_event_percpu依次调用,以及各类中断标志位(IRQF_SHARED、IRQF_ONESHOT、IRQF_PERCPU等)的含义。


3.5 中断处理

3.5.1 CPU跳转

当完成中断的注册后,所有结构的组织关系都已经建立好,剩下的工作就是当信号来临时,进行中断的处理工作。本节站在前面知识点的基础上,把中断触发、中断处理等整个流程走一遍。

CPU在执行完一条指令,会检查当前的状态,如果发现有事件到来,在执行下一条指令之前处理事件。

假设当前在EL0运行一个应用程序,触发了一个EL0的IRQ中断,则处理器会做如下操作:

先会跳到ARM64对应的异常向量表:

/* * Exception vectors. */	.pushsection ".entry.text", "ax"	.align	11ENTRY(vectors)	kernel_ventry	1, sync_invalid			// Synchronous EL1t	kernel_ventry	1, irq_invalid			// IRQ EL1t	kernel_ventry	1, fiq_invalid			// FIQ EL1t	kernel_ventry	1, error_invalid		// Error EL1t	kernel_ventry	1, sync				// Synchronous EL1h	kernel_ventry	1, irq				// IRQ EL1h	kernel_ventry	1, fiq_invalid			// FIQ EL1h	kernel_ventry	1, error			// Error EL1h	kernel_ventry	0, sync				// Synchronous 64-bit EL0	kernel_ventry	0, irq				// IRQ 64-bit EL0	kernel_ventry	0, fiq_invalid			// FIQ 64-bit EL0	kernel_ventry	0, error			// Error 64-bit EL0#ifdef CONFIG_COMPAT	kernel_ventry	0, sync_compat, 32// Synchronous 32-bit EL0	kernel_ventry	0, irq_compat, 32// IRQ 32-bit EL0	kernel_ventry	0, fiq_invalid_compat, 32// FIQ 32-bit EL0	kernel_ventry	0, error_compat, 32// Error 32-bit EL0#endifEND(vectors)

ARM64的异常向量表vectors中设置了各种异常的入口。kernel_ventry展开后,有效的异常入口有两个同步异常el0_sync、el1_sync和两个异步异常el0_irq、el1_irq,其他异常入口暂时都invalid。中断属于异步异常。

中断的处理分为三个部分:保护现场、中断处理、恢复现场。其中el0_irq和el1_irq的具体实现略有不同,但处理流程大致相同。以el0_irq为例进行梳理。

ARM中断处理函数入口的汇编代码 linux-4.20.10\arch\arm64\kernel\entry.S:

.align	6el0_irq:	kernel_entry 0el0_irq_naked:	enable_da_f#ifdef CONFIG_TRACE_IRQFLAGS	bl	trace_hardirqs_off#endif	ct_user_exit	irq_handler#ifdef CONFIG_TRACE_IRQFLAGS	bl	trace_hardirqs_on#endif	b	ret_to_userENDPROC(el0_irq)el1_irq:	kernel_entry 1	enable_da_f#ifdef CONFIG_TRACE_IRQFLAGS	bl	trace_hardirqs_off#endif	irq_handler#ifdef CONFIG_PREEMPT	ldr	w24, [tsk, #TSK_TI_PREEMPT]	// get preempt count	cbnz	w24, 1f				// preempt count != 0	ldr	x0, [tsk, #TSK_TI_FLAGS]	// get flags	tbz	x0, #TIF_NEED_RESCHED, 1f	// needs rescheduling?	bl	el1_preempt1:#endif#ifdef CONFIG_TRACE_IRQFLAGS	bl	trace_hardirqs_on#endif	kernel_exit 1ENDPROC(el1_irq)

汇编代码中中断处理宏的定义:

/* * Interrupt handling. */	.macro	irq_handler	ldr_l	x1, handle_arch_irq	mov	x0, sp	irq_stack_entry	blr	x1	irq_stack_exit	.endm	.text

保护现场

kernel_entry 0中,kernel_entry是一个宏,此宏会将CPU寄存器按照pt_regs结构体的定义将第一现场保存到栈上。

enable_da_f是关闭中断:

/* IRQ is the lowest priority flag, unconditionally unmask the rest. */.macro enable_da_fmsr     daifclr, #(8 | 4 | 1).endm

保存现场主要包含以下三个操作:

  1. 保存PSTATE到SPSR_ELx寄存器

  2. 将PSTATE中的DAIF全部屏蔽

  3. 保存PC寄存器的值到ELR_ELx寄存器

恢复现场

/* * "slow" syscall return path. */ret_to_user:	disable_daif	ldr	x1, [tsk, #TSK_TI_FLAGS]	and	x2, x1, #_TIF_WORK_MASK	cbnz	x2, work_pendingfinish_ret_to_user:	enable_step_tsk x1, x2	kernel_exit 0ENDPROC(ret_to_user)

恢复现场主要分三步:

  1. disable中断

  2. 检查在退出中断前有没有需要处理的事情,如调度、信号处理等

  3. 将之前压栈的pt_regs弹出,恢复现场

中断处理过程

汇编代码中中断处理函数的定义:

/* * Interrupt handling. */	.macro	irq_handler	ldr_l	x1, handle_arch_irq  // 定义	mov	x0, sp	irq_stack_entry  // 进入中断栈	blr	x1           // 执行中断控制器的 handle_arch_irq	irq_stack_exit   // 退出中断栈	.endm	.text

主要做了三个动作:

  1. 进入中断栈

  2. 执行中断控制器的handle_arch_irq

  3. 退出中断栈

中断栈用来保存中断的上下文,中断发生和退出时分别调用irq_stack_entry和irq_stack_exit来进入和退出中断栈。中断栈在内核启动时就创建好,内核在启动过程中会为每个CPU创建一个per-cpu的中断栈:start_kernel → init_IRQ → init_irq_stacks。

irq_handler → handle_arch_irq

3.5.2 中断控制器的处理过程

handle_arch_irq在内核启动过程中初始化中断控制器时设置的具体handler:init_IRQ → irqchip_init() → gic_of_init → gic_init_bases → set_handle_irq(gic_handle_irq),将handle_arch_irq指针指向gic_handle_irq函数:

static int __init gic_init_bases(...){    .....    set_handle_irq(gic_handle_irq);    .....}void __init set_handle_irq(void (*handle_irq)(struct pt_regs *)){    if (handle_arch_irq)        return;    handle_arch_irq = handle_irq;}

中断处理最终会进入gic_handle_irq:

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_key_true(&supports_deactivate))                gic_write_eoir(irqnr);            else                isb();            err = handle_domain_irq(gic_data.domain, irqnr, regs);  // (3)            if (err) {                WARN_ONCE(true, "Unexpected interrupt received!\n");                if (static_key_true(&supports_deactivate)) {                    if (irqnr < 8192)                        gic_write_dir(irqnr);                } else {                    gic_write_eoir(irqnr);                }            }            continue;        }        if (irqnr < 16) {                                            // (4)            gic_write_eoir(irqnr);            if (static_key_true(&supports_deactivate))                gic_write_dir(irqnr);#ifdef CONFIG_SMP            handle_IPI(irqnr, regs);                                 // (5)#endif            continue;        }    } while (irqnr != ICC_IAR1_EL1_SPURIOUS);}

各步骤说明:

  1. 读取中断控制器寄存器GICC_IAR,获取hwirq。CPU读取IAR寄存器后,GIC将该中断标记为"Active"状态,并向CPU发送ACK信号。处理完中断后,CPU需要向GIC发送EOI(End Of Interrupt)信号。

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

  3. 中断控制器中断处理的主体:调用handle_domain_irq进行Linux通用层处理。

  4. 软件触发的中断(SGI,irqnr < 16)。

  5. 核间交互触发的中断(IPI)。

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    if (unlikely(!irq || irq >= nr_irqs)) {        ack_bad_irq(irq);        ret = -EINVAL;    } else {        generic_handle_irq(irq);               // (3)    }    irq_exit();                                // (4)    set_irq_regs(old_regs);    return ret;}
  1. 进入中断上下文

  2. 根据hwirq查找Linux中断号(virq)

  3. 通过中断号找到irq_desc[NR_IRQS]中的一项,调用generic_handle_irq_desc,执行该irq号注册的action

  4. 退出中断上下文

3.5.3 Linux通用层处理

调用desc->handle_irq指向的回调函数。

irq_domain_set_info根据硬件中断号的范围设置irq_desc->handle_irq的指针,共享中断入口为handle_fasteoi_irq,私有中断入口为handle_percpu_devid_irq:

  • handle_fasteoi_irq:处理共享中断,遍历irqaction链表,逐个调用action->handler()函数(即设备驱动程序通过request_irq/request_threaded_irq注册的中断处理函数);如果中断线程化处理,还会调用__irq_wake_thread()唤醒内核线程。

  • handle_percpu_devid_irq:处理per-CPU中断,调用中断控制器的处理函数进行硬件操作,再调用action->handler()进行中断处理。

3.5.4 中断流控层处理函数

中断流控层简介:早期内核版本中,几乎所有中断都由__do_IRQ函数处理,但因各种中断请求的电气特性或中断控制器特性不同,导致以下这些处理会有所不同:

  • 何时对中断控制器发出ack回应

  • mask_irq和unmask_irq的处理

  • 中断控制器是否需要EOI回应

  • 何时打开CPU本地IRQ中断以允许IRQ嵌套

  • 中断数据结构的同步和保护

为此,通用中断子系统把几种常用的流控类型进行了抽象,并实现了相应的标准函数,只要选择相应的函数赋值给irq_desc.handle_irq字段即可:

typedefvoid(*irq_flow_handler_t)(unsignedint irq, struct irq_desc *desc);
/* include/linux/irq.h */extern void handle_level_irq(struct irq_desc *desc);extern void handle_fasteoi_irq(struct irq_desc *desc);extern void handle_edge_irq(struct irq_desc *desc);extern void handle_edge_eoi_irq(struct irq_desc *desc);extern void handle_simple_irq(struct irq_desc *desc);extern void handle_untracked_irq(struct irq_desc *desc);extern void handle_percpu_irq(struct irq_desc *desc);extern void handle_percpu_devid_irq(struct irq_desc *desc);extern void handle_bad_irq(struct irq_desc *desc);extern void handle_nested_irq(unsigned int irq);

驱动程序和板级代码可以通过以下API设置irq的流控函数:

irq_domain_set_infoirq_set_handler();irq_set_chip_and_handler();irq_set_chip_and_handler_name();handle_simple_irq      // 用于简易流控处理handle_level_irq       // 用于电平触发中断的流控处理handle_edge_irq        // 用于边沿触发中断的流控处理handle_fasteoi_irq     // 用于需要响应eoi的中断控制器handle_percpu_irq      // 用于只在单一cpu响应的中断handle_nested_irq      // 用于处理使用线程的嵌套中断

handle_fasteoi_irq

现代中断控制器(如ARM GIC)通常在硬件上实现了中断流控功能,CPU只需在每次处理完中断后发出EOI。handle_fasteoi_irq是GIC ARM V8的处理函数。

ARM GIC-V3入口调用链:

gic_handle_irq    +-> handle_domain_irq        +-> handle_irq_desc            +-> generic_handle_irq_desc                +-> desc->handle_irq(desc) = handle_fasteoi_irq

IRQS_ONESHOT的作用:

enum {    IRQS_AUTODETECT        = 0x00000001,  // 处于自动侦测状态    IRQS_SPURIOUS_DISABLED = 0x00000002,  // 被视为"伪中断"并被禁用    IRQS_POLL_INPROGRESS   = 0x00000008,  // 正处于轮询调用action    /*     表示一次性触发的中断,不能嵌套     (1) 在硬件中断处理完成之后才能打开中断     (2) 在中断线程化中保持中断关闭状态,直到该中断源上所有的         thread_fn完成之后才能打开中断     (3) 如果执行request_threaded_irq()时主处理程序为NULL且中断         控制器不支持硬件ONESHOT功能,那应该显式地设置该标志位    */    IRQS_ONESHOT           = 0x00000020,    IRQS_REPLAY            = 0x00000040,  // 重新发送一次中断    IRQS_WAITING           = 0x00000080,  // 处于等待状态    IRQS_PENDING           = 0x00000200,  // 该中断被挂起    IRQS_SUSPENDED         = 0x00000800,  // 该中断被暂停};
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);/* 如果该中断没有指定action描述符或该中断被关闭,设置状态为IRQS_PENDING,	   且mask_irq()屏蔽该中断 */if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {		desc->istate |= IRQS_PENDING;		mask_irq(desc);  // 对MSI设备进行mask操作goto out;	}	kstat_incr_irqs_this_cpu(desc);/* 如果中断是IRQS_ONESHOT,不支持中断嵌套,调用mask_irq()屏蔽该中断源 */if (desc->istate & IRQS_ONESHOT)		mask_irq(desc);	handle_irq_event(desc);/* 根据不同条件执行unmask_irq()解除中断屏蔽,或者发送EOI信号 */	cond_unmask_eoi_irq(desc, chip);	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);}

irq_may_run检查逻辑:

staticboolirq_may_run(struct irq_desc *desc){unsigned int mask = IRQD_IRQ_INPROGRESS | IRQD_WAKEUP_ARMED;/* 如果中断没有在处理中,且不是已武装的唤醒中断,则可以运行 */if (!irqd_has_set(&desc->irq_data, mask))return true;/* 如果是已武装的唤醒源,标记为pending并挂起,通知电源管理 */if (irq_pm_check_wakeup(desc))return false;/* 处理潜在的并发轮询 */return irq_check_poll(desc);}

handle_irq_event会设置IRQD_IRQ_INPROGRESS标志:

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;}

handle_edge_irq

该函数用于处理边沿触发中断的流控操作。边沿触发中断的特点是只有在电平发生跳变时才发出中断请求,因为跳变是一瞬间,处理不当容易漏掉中断请求。为避免这种情况,屏蔽中断的时间必须越短越好。

状态红色(另一CPU正在处理)绿色(正常情况)
IRQ_PROGRESSTRUEFALSE
是否允许本地cpu中断禁止允许
是否允许该设备再次触发中断禁止允许
是否处于中断上下文处于中断上下文处于进程上下文
void handle_edge_irq(struct irq_desc *desc){	raw_spin_lock(&desc->lock);	desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);if (!irq_may_run(desc)) {		desc->istate |= IRQS_PENDING;		mask_ack_irq(desc);goto out_unlock;	}if (irqd_irq_disabled(&desc->irq_data) || !desc->action) {		desc->istate |= IRQS_PENDING;		mask_ack_irq(desc);goto out_unlock;	}	kstat_incr_irqs_this_cpu(desc);/* Start handling the irq */	desc->irq_data.chip->irq_ack(&desc->irq_data);do {if (unlikely(!desc->action)) {			mask_irq(desc);goto out_unlock;		}		/* 如果处理irq时有另一个irq到达,可能已经屏蔽了该irq,在此解除屏蔽 */if (unlikely(desc->istate & IRQS_PENDING)) {if (!irqd_irq_disabled(&desc->irq_data) &&			    irqd_irq_masked(&desc->irq_data))				unmask_irq(desc);		}		handle_irq_event(desc);	} while ((desc->istate & IRQS_PENDING) &&		 !irqd_irq_disabled(&desc->irq_data));out_unlock:	raw_spin_unlock(&desc->lock);}

边沿中断处理要点:

  • 不立即mask irq,只ack irq(清除硬件pending状态),允许其他CPU响应同一中断

  • 若检测到IRQ_INPROGRESS(另一CPU正在处理),仅设置IRQS_PENDING并mask_ack_irq后退出

  • 在循环中持续处理IRQS_PENDING状态,避免丢失中断

handle_level_irq

电平中断的特点是:只要设备的中断请求引脚保持在预设的触发电平,中断就会一直被请求。为避免同一中断被重复响应,必须在处理中断前先mask irq,然后ack irq(复位设备的中断请求引脚),响应完成后再unmask irq。

中断流控层中的Mask行为

针对PCIe的MSI中断,在处理过程中需要对MSI中断进行MASK屏蔽。

desc->irq_data.chip->irq_mask 调用链:

mask_irq	|- desc->irq_data.chip->irq_mask        |- its_mask_msi_irq            |- pci_msi_mask_irq                |- __pci_msi_mask_desc(desc, BIT(data->irq - desc->irq));                    |- pci_msi_mask(desc, mask);						|- pci_msi_update_mask(desc, 0, mask);

ARM GIC-V3 ITS MSI中的具体实现(irq-gic-v3-its-pci-msi.c):

static void its_mask_msi_irq(struct irq_data *d){	pci_msi_mask_irq(d);	irq_chip_mask_parent(d);}static void its_unmask_msi_irq(struct irq_data *d){	pci_msi_unmask_irq(d);	irq_chip_unmask_parent(d);}static struct irq_chip its_msi_irq_chip = {	.name			= "ITS-MSI",	.irq_unmask		= its_unmask_msi_irq,	.irq_mask		= its_mask_msi_irq,	.irq_eoi		= irq_chip_eoi_parent,	.irq_write_msi_msg	= pci_msi_domain_write_msg,};

pci_msi_mask_irq → __pci_msi_mask_desc → pci_msi_mask → pci_msi_update_mask:

static noinline voidpci_msi_update_mask(struct msi_desc *desc, u32 clear, u32 set){	raw_spinlock_t *lock = &desc->dev->msi_lock;	unsigned long flags;	raw_spin_lock_irqsave(lock, flags);	desc->msi_mask &= ~clear;	desc->msi_mask |= set;	pci_write_config_dword(msi_desc_to_pci_dev(desc), desc->mask_pos,			       desc->msi_mask);	raw_spin_unlock_irqrestore(lock, flags);}

mask_irq的调用时机:是在中断控制器收到中断之后、具体的驱动处理函数之前调用。

mask_irq = desc->irq_data.chip->irq_mask = its_mask_msi_irq = pci_msi_mask_irq

以55号中断为例:

handler:  handle_fasteoi_irqdevice:   0000:00:02.0dstate:   0x31400200            IRQD_ACTIVATED            IRQD_IRQ_STARTED            IRQD_SINGLE_TARGET            IRQD_AFFINITY_ON_ACTIVATE            IRQD_HANDLE_ENFORCE_IRQCTXdomain:  :intc@8000000:its@8080000-3 hwirq:   0x8001 chip:    ITS-MSI parent:    domain:  :intc@8000000:its@8080000-5     hwirq:   0x2001     chip:    ITS     parent:        domain:  :intc@8000000-1         hwirq:   0x2001         chip:    GICv3

3.5.5 Linux用户驱动层中断的处理

中断标志位说明:

中断标志位描述
IRQF_SHARED多个设备共享一个中断号。需要外设硬件支持,在中断处理程序中查询哪个外设发生了中断,会给中断处理带来一定延迟
IRQF_TIMER标记一个时钟中断
IRQF_PERCPU属于特定某个CPU的中断
IRQF_NOBALANCING禁止多CPU之间的中断均衡
IRQF_IRQPOLL中断被用作轮询
IRQF_ONESHOT表示一次性触发的中断,不能嵌套:(1)在硬件中断处理完成之后才能打开中断;(2)在中断线程化中保持中断关闭状态,直到该中断源上所有的thread_fn完成之后才能打开中断;(3)如果执行request_threaded_irq()时主处理程序为NULL且中断控制器不支持硬件ONESHOT功能,则应该显式设置该标志位
IRQF_NO_SUSPEND在系统睡眠过程中不要关闭该中断
IRQF_FORCE_RESUME在系统唤醒过程中必须强制打开该中断
IRQF_NO_THREAD表示该中断不会被线程化

最新文章

随机文章