中断分类
在中断的多种分类方法中,我们根据中断的来源来分类:
内部中断:例如软件中断指令, 溢出,除法错误等。操作系统从用户态切换到内核态。都会在一条指令执行完后才会触发,因为为同步。
外部中断:由外设提供。由于不知道何时中断会到来,因此为异步过程。
外部中断的整体框架如下图:
首先我们先了解几个名词:
软件中断irq:它是发生设备中断时处理器从PIC中读到的中断号码,在内核建立的中断处理框架内,会使用这个irq号来表示一个外设的中断,并调用对应的中断处理例程。
中断向量表(Vector table):除了外部中断还有异常,陷阱等异常事件,中断向量表里面的每一项都是一个中断或异常处理函数的入口地址。外部设备的中断常常对应向量表中的某一项,这是个通用的外部中断处理函数入口,因此进入通用的中断处理函数之后,系统必须要知道正在处理的中断是哪一个设备产生的,而这正是由irq决定的。中断向量表的内容由操作系统在初始化阶段来填写。对于外部中断,操作系统负责实现一个通用的外部中断处理函数,然后把这个函数的入口地址翻到中断向量表中的对应位置。
可编程中断控制器:PIC(Programmable Interrupt Controller) ,一般可通过处理器进行编程配置。
ARM GIC支持3种类型的中断:
SGI(Software Generated Interrupt): 软件产生的中断,可以用于多核间通信。一个CPU可以通过写GIC寄存器给另一个CPU产生中断。多核间调度用的IPI_WAKEUP, IPI_TIMER,.....等都是由SGI产生
PPI(Private Peripheral Interrupt): 某个CPU私有外设的中断,这类外设的中断只能发送给绑定的那个CPU
SPI(Share Peripheral Interrupt):共享外设中断,这类外设的中断可以路由到任何一个CPU
Linux外部中断处理程序框架
外部中断处理程序架构如下:
内核中断向量表初始化过程(下文在不做特别说明的状况下均以Linux5.0内核为版本进行分析)
//arch/arm64/kernel/entry.S
/**Exception vectors*/
.pushsection ".entry.text", "ax"
.align 11
ENTRY(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
#else
kernel_ventry 0, sync_invalid, 32 // Synchronous 32-bit EL0
kernel_ventry 0, irq_invalid, 32 // IRQ 32-bit EL0
kernel_ventry 0, fiq_invalid, 32 // FIQ 32-bit EL0
kernel_ventry 0, error_invalid, 32 // Error 32-bit EL0
#endifEND(vectors)
对kernel_ventry宏做解读:.align=7,说明该段代码是以2^7=128字节对齐的,这和向量表中每一个offset的大小是一致的
代码看似非常复杂,其实最终跳转到了b el()\\el()_\\label, 翻译一下,其实就是跳转到了如下这样的函数中
el1_sync_invalid
el1_irq_invalid
el1_fiq_invalid
el1_error_invalid
el1_sync:
...
el1_irq:
kernel_entry 1
enable_da_f
#ifdef CONFIG_TRACE_IRQFLAGS
bl trace_hardirqs_off
#endif
irq_handler
#ifdef CONFIG_PREEMPT
ldr x24, [tsk, #TSK_TI_PREEMPT] // get preempt count
cbnz x24, 1f // preempt count != 0
bl el1_preempt
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1
irq_stack_exit
...
el1_fiq:
...
el1_error:
...
el0_sync:
...
el0_irq:
...
el0_fiq:
...
el0_error:
...
ARM64中,当外部中断发生时, 会调用el1_irq, 在上述20行中,会call handle_arch_irq. 此为一个函数指针, 在GIC中断控制器在做初始化时设置的. 而中断来临时,handle_domain_irq会被call 到.
//kernel/irq/handle.c
int __init set_handle_irq(void(*handle_irq)(stuct pt_regs*)){
....
handle_arch_irq = handle_irq;
....
}
//drivers/irqchip/irq-gic.c
static void _exception_irq_entry gic_handle_irq(struct pt_regs* regs){
.....
//调用对应的通用中断处理函数.
handle_domain_irq(gic- >domain,irqnr,regs);
.....
}
//drivers/irqchip/irq-gic.c
static int __init __gic_init_bases(struct gic_chip_data *gic,int irq_start, struct fwnode_handle *handle){
....
set_handle_irq(gic_handle_irq);//中断向量指向的irq, 汇编语言会调用到此函数
....
}
//drivers/irqchip/irq-gic.c
static int gic_init_bases(struct gic_chip_data *gic, int irq_start,struct fwnode_handle *handle){
....
//分配irq_desc, 里面会call gic_irq_domain_map函数, 设置一吃handle的电信号处理函数
irq_base = irq_alloc_descs(irq_start, 16, gic_irqs,numa_node_id());
....
}
//driver/irqchip/irq-gic.c
static int gic_irq_domain_map(struct irq_domain *d, unsigned int irq,irq_hw_number_t hw){
struct gic_chip_data *gic = d- >host_data;
//根据中断号的不同,处理不同的handle, driver 的实现依赖于硬件的GIC 硬件spec, 参考规范
if (hw < 32) {
irq_set_percpu_devid(irq);
//设置真正的PIC的一级handle,处理PIC的电信号
irq_domain_set_info(d, irq, hw, &gic- >chip, d- >host_data,handle_percpu_devid_irq, NULL, NULL);
irq_set_status_flags(irq, IRQ_NOAUTOEN);
} else {
//设置真正的PIC的一级handle,处理PIC的电信号
irq_domain_set_info(d, irq, hw, &gic- >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)));
}
return 0;
}
//kernel/irq/chip.c
//irq > 32的电信号处理函数, 下面几个是函数的层级,一步一步call到最终bsp注册处理的函数当中.
void handle_fasteio_iq(struct irq_desc *desc){
...
handle_irq_event(desc);
...
}
//kernel/irq/handle.c
irqreturn_t handle_irq_event(struct irq_desc *desc)){
...
handle_irq_event_percpu(desc);
...
}
//kernel/irq/handle.c
irqreturn_t handle_irq_event_percpu(struct irq_desc *desc){
...
retval = __handle_irq_event_percpu(desc, &flags);
add_interrupt_randomness(desc- >irq_data.irq, flags);
...
}
//kernel/irq/handle.c
//电信号上半部最终处理的函数,这里面会去call bsp 工程师注册的上半部的函数
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags){
...
for_each_action_of_desc(desc, action) {
...
res = action- >handler(irq, action- >dev_id);//设备driver的上半部callback函数
....
switch (res) {
//上半部的返回值决定了下半部的触发方式,下节从BSP角度去看会用到.
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);//唤醒线程化传输的下半部thread_fn
/* Fall through to add to randomness */
case IRQ_HANDLED:
*flags |= action- >flags;
break;
....
}
}
...
}
//kernel/irq/handle.c
//唤醒线程化传输的下半部thread_fn
void __irq_wake_thread(struct irq_desc *desc, struct irqaction *action){
...
wake_up_process(action- >thread);
}