什么是中断
中断是计算机系统中一种重要的机制,指CPU在正常执行程序的过程中,由于内部或外部事件的紧急请求,暂停当前任务,转去处理该事件,待处理完成后恢复原任务继续执行,就像工作中突然接到一个紧急电话,你暂停手头工作去接听,结束后再回来继续工作,本质上中断就是异步事件通知机制。
当(外部事件发生) {
CPU暂停当前工作;
保存现场;
处理事件;
恢复现场;
继续之前的工作;
}
完整的中断处理链路
Linux 内核在启动时(start_kernel函数开始处)调用 local_irq_disable()禁用中断,待中断系统初始化(如early_irq_init、init_IRQ、softirq_init等)之后,内核才会调用local_irq_enable()启用中断。试想,如果这里一开始允许使用中断,内核启动过程中,随便拍打一下键盘,那么系统就崩了asmlinkage __visible void __init start_kernel(void){ ....... local_irq_disable(); early_boot_irqs_disabled = true; ....... early_irq_init(); init_IRQ(); tick_init(); rcu_init_nohz(); init_timers(); hrtimers_init(); softirq_init(); timekeeping_init(); ...... early_boot_irqs_disabled = false; local_irq_enable(); ......}
中断初始化调用链如下:
start_kernel() ├── setup_arch() │ ├── idt_setup_early_traps() // IDT早期初始化 │ │ ├── idt_setup_from_table(idt_table, early_idts) // 设置调试、断点陷阱 │ │ └── load_idt(&idt_descr) // 加载IDT描述符 │ └── idt_setup_early_pf() // IDT早期页错误处理初始化 │ └── idt_setup_from_table(idt_table, early_pf_idts) │ ├── trap_init() //IDT核心初始化 │ ├── setup_cpu_entry_areas() // 初始化CPU入口区域 │ ├── idt_setup_traps() │ │ └── idt_setup_from_table(idt_table, def_idts) │ ├── cea_set_pte() // 将IDT设置为只读保护 │ ├── cpu_init() // CPU初始化 │ ├── idt_setup_ist_traps() │ │ └── idt_setup_from_table(idt_table, ist_idts) │ ├── x86_init.irqs.trap_init() │ └── idt_setup_debugidt_traps() │ ├── early_irq_init() // 早期中断初始化 │ ├── arch_early_irq_init() // 架构特定早期初始化 │ ├── init_irq_default_affinity() // 设置默认CPU亲和性 │ └── irq_init_percpu_irqstack() // 初始化每CPU中断栈 │ ├── init_IRQ() // 主要中断初始化 │ ├── x86_init.irqs.intr_init() // 架构特定初始化 │ │ ├── native_init_IRQ() │ │ │ ├── x86_init.irqs.pre_vector_init() │ │ │ ├── idt_setup_apic_and_irq_gates() // IDT设置APIC和IRQ门 │ │ │ │ ├── idt_setup_from_table(idt_table, apic_idts) // APIC中断 │ │ │ │ ├── set_intr_gate() // 设置外部中断门 (32-255) │ │ │ │ └── 为每个中断向量设置处理入口 │ │ │ ├── lapic_assign_system_vectors() // 分配系统向量 │ │ │ └── setup_irq(2, &irq2) // 设置级联中断 │ │ └── x86_init.irqs.intr_mode_init() // 中断模式初始化 │ └── irq_ctx_init() // 中断上下文初始化 │ ├── softirq_init() // 软中断初始化 │ ├── open_softirq(TASKLET_SOFTIRQ, tasklet_action) │ ├── open_softirq(HI_SOFTIRQ, tasklet_hi_action) │ └── for_each_possible_cpu() // 为每个CPU创建ksoftirqd │ └── local_irq_enable() // 启用本地中断
在Linux内核中,start_kernel()会调用trap_init()来初始化前32个陷入或者异常中断,然后调用init_IRQ()来初始化设备中断。在init_IRQ()中,调用到native_init_IRQ(),最终会调用到idt_setup_apic_and_irq_gates()
void __init idt_setup_apic_and_irq_gates(void){ int i = FIRST_EXTERNAL_VECTOR; //i = 0x20 (32) void *entry; //用于存储中断处理程序的入口地址 idt_setup_from_table(idt_table, apic_idts, ARRAY_SIZE(apic_idts), true); for_each_clear_bit_from(i, system_vectors, FIRST_SYSTEM_VECTOR) { entry = irq_entries_start + 8 * (i - FIRST_EXTERNAL_VECTOR); set_intr_gate(i, entry); }#ifdef CONFIG_X86_LOCAL_APIC for_each_clear_bit_from(i, system_vectors, NR_VECTORS) { set_bit(i, system_vectors); entry = spurious_entries_start + 8 * (i - FIRST_SYSTEM_VECTOR); set_intr_gate(i, entry); }#endif}
中断描述符表
IDT(Interrupt Descriptor Table,中断描述符表)是 CPU 的一个核心数据结构,它是一张存储在内存中的“路由表”。它的唯一作用就是告诉 CPU:当发生特定编号的中断、陷阱或异常时,应该跳转到哪个内核函数去处理
x86 架构下每个cpu有独立的IDT表,可以处理256个中断/* * Linux IRQ vector layout. * * There are 256 IDT entries (per CPU - each entry is 8 bytes) which can * be defined by Linux. They are used as a jump table by the CPU when a * given vector is triggered - by a CPU-external, CPU-internal or * software-triggered event. * * Linux sets the kernel code address each entry jumps to early during * bootup, and never changes them. This is the general layout of the * IDT entries: * * Vectors 0 ... 31 : system traps and exceptions - hardcoded events * Vectors 32 ... 127 : device interrupts * Vector 128 : legacy int80 syscall interface * Vectors 129 ... LOCAL_TIMER_VECTOR-1 * Vectors LOCAL_TIMER_VECTOR ... 255 : special interrupts * * 64-bit x86 has per CPU IDT tables, 32-bit has one shared IDT table. * * This file enumerates the exact layout of them: */#define NMI_VECTOR 0x02#define MCE_VECTOR 0x12/* * IDT vectors usable for external interrupt sources start at 0x20. * (0x80 is the syscall vector, 0x30-0x3f are for ISA) */#define FIRST_EXTERNAL_VECTOR 0x20/* * Reserve the lowest usable vector (and hence lowest priority) 0x20 for * triggering cleanup after irq migration. 0x21-0x2f will still be used * for device interrupts. */#define IRQ_MOVE_CLEANUP_VECTOR FIRST_EXTERNAL_VECTOR#define IA32_SYSCALL_VECTOR 0x80/* * Vectors 0x30-0x3f are used for ISA interrupts. * round up to the next 16-vector boundary */#define ISA_IRQ_VECTOR(irq) (((FIRST_EXTERNAL_VECTOR + 16) & ~15) + irq)/* * Special IRQ vectors used by the SMP architecture, 0xf0-0xff * * some of the following vectors are 'rare', they are merged * into a single vector (CALL_FUNCTION_VECTOR) to save vector space. * TLB, reschedule and local APIC vectors are performance-critical. */#define SPURIOUS_APIC_VECTOR 0xff/* * Sanity check */#if ((SPURIOUS_APIC_VECTOR & 0x0F) != 0x0F)# error SPURIOUS_APIC_VECTOR definition error#endif#define ERROR_APIC_VECTOR 0xfe#define RESCHEDULE_VECTOR 0xfd#define CALL_FUNCTION_VECTOR 0xfc#define CALL_FUNCTION_SINGLE_VECTOR 0xfb#define THERMAL_APIC_VECTOR 0xfa#define THRESHOLD_APIC_VECTOR 0xf9#define REBOOT_VECTOR 0xf8/* * Generic system vector for platform specific use */#define X86_PLATFORM_IPI_VECTOR 0xf7/* * IRQ work vector: */#define IRQ_WORK_VECTOR 0xf6#define UV_BAU_MESSAGE 0xf5#define DEFERRED_ERROR_VECTOR 0xf4/* Vector on which hypervisor callbacks will be delivered */#define HYPERVISOR_CALLBACK_VECTOR 0xf3/* Vector for KVM to deliver posted interrupt IPI */#ifdef CONFIG_HAVE_KVM#define POSTED_INTR_VECTOR 0xf2#define POSTED_INTR_WAKEUP_VECTOR 0xf1#define POSTED_INTR_NESTED_VECTOR 0xf0#endif#define MANAGED_IRQ_SHUTDOWN_VECTOR 0xef#if IS_ENABLED(CONFIG_HYPERV)#define HYPERV_REENLIGHTENMENT_VECTOR 0xee#define HYPERV_STIMER0_VECTOR 0xed#endif#define LOCAL_TIMER_VECTOR 0xec#define NR_VECTORS 256#ifdef CONFIG_X86_LOCAL_APIC#define FIRST_SYSTEM_VECTOR LOCAL_TIMER_VECTOR#else#define FIRST_SYSTEM_VECTOR NR_VECTORS#endif
| | |
| | |
| 0x20-0x2F | |
| | ISA传统中断 |
| | |
| | |
| | |
| | 用于SMP架构的特殊中断(IPI) |
这里需要主要注意,这里的中断向量号和/proc/interrupts 看到的不是一个东西,但是他们有一个映射关系,以irq timer 0 为例:根据上述宏定义计算一下:
((32 + 16) & ~15) + 0 = 48
irq0 系统定时器是一个传统ISA设备,对应的idt表中的向量48
中断描述符
对于每一个中断,都有一个对应的描述结构体irq_desc在irq_desc结构体中,需要关注struct irqaction *action,他是中断处理程序链表irq_handler_t handler; // 主中断处理函数
参数(中断号, dev_id(用于区分共享中断链表上的不同设备))
返回值(IRQ_HANDLED, IRQ_NONE, IRQ_WAKE_THREAD)
IRQ_NONE:表示中断不是由这个设备产生的,或者这个设备没有处理这个中断。
例如,在共享中断的情况下,多个设备共享同一个中断线,当中断发生时,每个设备的中断处理函数都会被调用。
如果某个设备发现这个中断不是它产生的,它应该返回IRQ_NONE,这样中断子系统就知道这个设备没有处理中断,然后继续调用共享中断链上的下一个设备的中断处理函数。
IRQ_HANDLED:表示中断已经由这个设备处理了。
例如,在定时器中断处理函数中,我们确实处理了定时器中断,所以返回IRQ_HANDLED。
IRQ_WAKE_THREAD:表示中断处理函数要求唤醒对应的线程。
如果中断处理函数是在线程中运行的(即使用了IRQF_THREAD标志),那么当中断处理函数(上半部)返回IRQ_WAKE_THREAD时,内核会唤醒对应的线程(下半部)来处理中断。
注意:这个标志通常与线程化中断一起使用
比如:
中断处理流程
设备产生中断信号发送给APIC
APIC将信号转换为中断向量发给cpu
查找IDT,将向量号压栈,调转地址common_intrrrupt执行
在 do_IRQ中:
unsigned vector = ~regs->orig_ax;
//regs->orig_ax对应着之前在汇编中 addq $-0x80, (%rsp)调整后压入栈的中断向量号(即 ~vector,范围[-256, -1]),通过按位取反(~)操作,将其还原为原始的正数中断向量号。
desc = __this_cpu_read(vector_irq[vector]);
//根据中断向量号 vector,从当前CPU的 vector_irq映射表中,查找对应的 irq_desc描述符。
执行generic_handle_irq_desc,这个函数会沿着中断描述符的链式结构,最终调用到设备驱动注册的中断处理函数
exiting_irq();//退出中断上下文
set_irq_regs(old_regs);://恢复旧的寄存器状态
handle_irq最终通常会调用到handle_irq_event,handle_irq_event会调用handle_irq_event_percpu,后者调用handle_irq_event_percpu(第来遍历执行所有注册的中断处理函数(action->handler),至此,中断上半部就完成了。但是,如果某个action->handler返回了IRQ_WAKE_THREAD,那么还会唤醒内核中断线程(下半部)