本文是《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
保存现场主要包含以下三个操作:
保存PSTATE到SPSR_ELx寄存器
将PSTATE中的DAIF全部屏蔽
保存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)
恢复现场主要分三步:
disable中断
检查在退出中断前有没有需要处理的事情,如调度、信号处理等
将之前压栈的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
主要做了三个动作:
进入中断栈
执行中断控制器的handle_arch_irq
退出中断栈
中断栈用来保存中断的上下文,中断发生和退出时分别调用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);}
各步骤说明:
读取中断控制器寄存器GICC_IAR,获取hwirq。CPU读取IAR寄存器后,GIC将该中断标记为"Active"状态,并向CPU发送ACK信号。处理完中断后,CPU需要向GIC发送EOI(End Of Interrupt)信号。
外设触发的中断:硬件中断号0-15表示SGI类型,15-1020表示外设中断(SPI或PPI类型),8192-MAX表示LPI类型中断。
中断控制器中断处理的主体:调用handle_domain_irq进行Linux通用层处理。
软件触发的中断(SGI,irqnr < 16)。
核间交互触发的中断(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;}
进入中断上下文
根据hwirq查找Linux中断号(virq)
通过中断号找到irq_desc[NR_IRQS]中的一项,调用generic_handle_irq_desc,执行该irq号注册的action
退出中断上下文
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_PROGRESS | TRUE | FALSE |
| 是否允许本地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 | 表示该中断不会被线程化 |