点击查看系列文章 =》 Interrupt Pipeline系列文章大纲-CSDN博客
原创不易,需要大家多多鼓励!您的关注、点赞、收藏就是我的创作动力!
4.3 Linux的中断处理流程
先上图,一图胜千言!
图中心的蓝色部分,是Linux通用中断子系统的核心数据结构。图中只是列出各个数据结构的核心成员。在上一个章节,已经讲过了这些数据结构建立的时机,就不再重复描述。重点还是讨论图中外围画出的中断处理流程。
第一步,从hwirq发生到转换成virq(图左侧从上到下)
当EL1_IRQ或EL0_IRQ发生后,会调用全局函数指针handle_arch_irq。以ARM的GIC V3中断控制器为例,全局函数指针handle_arch_irq指向irq-gic-v3.c中定义的gic_handle_irq。
在《4.2.3 根据DTS完成中断控制器初始化》中,描述了GIC V3中断控制器的初始化。在初始化过程中,irq-gic-v3.c中调用set_handle_irq(gic_handle_irq),设置handle_arch_irq指向gic_handle_irq,具体的调用过程如下。
start_kernel <init/main.c>
-> init_IRQ <arch/arm64/kernel/irq.c>-> irqchip_init <drivers/irqchip/irqchip.c>-> of_irq_init(__irqchip_of_table) <drivers/of/irq.c>-> gic_of_init <drivers/irqchip/irq-gic-v3.c>-> gic_init_bases <drivers/irqchip/irq-gic-v3.c>-> set_handle_irq(gic_handle_irq)
如果某个中断源被触发,GIC会将IAR寄存器(Interrupt Acknowledge Register)中该中断源对应的bit置1。在gic_handle_irq中,调用gic_read_iar得到hwirq。
每个GIC V3中断控制器,在初始化的时候,都会创建一个irq_domain,并记录在自己的私有数据结构gic_data.domain。
根据上图中的第385行代码,对于PPI/SPI中断(大于15,小于1020)和LPI中断(大于等于8192),会走到handle_domain_irq处理,会传入irq_domain和hwirq做为入参。已知irq_domain和hwirq,怎么找出virq?内核已经定义好了一个API:irq_find_mapping。
对于GIC V3来说,创建domain的调用过程:irq_domain_create_tree->__irq_domain_add(fwnode, 0, ~0, 0, ops, host_data),其中第2个参数为domain->revmap_size赋值0;第4个参数为domain->revmap_direct_max_irq赋值0。所以,最终总是使用基数树irq_domain->revmap_tree来完成反向映射的查找。
至此,hwirq转换成了virq!
第二步,找出virq对应的irq_desc(图底部从左到右)
hwirq转换成了virq之后,handle_domain_irq调用generic_handle_irq处理virq。每个virq,都有自己对应的irq_desc,怎么找到呢?内核定义了一个API: irq_to_desc。在我当前的内核中,定义了CONFIG_SPARSE_IRQ,所以它的本质就是查找基数树irq_desc_tree,得到irq_desc。
第三步,执行中断流控处理程序(图右侧从下向上)
找到了irq_desc,紧接着generic_handle_irq调用generic_handle_irq_desc,执行irq_desc->handle_irq(desc)。
这个回调函数是什么?
irq_flow_handler_t handle_irq是irq_desc的一个成员,从字面可以翻译成:中断流控函数,也有人翻译成中断high level处理函数。
中断流控(Interrupt Flow Control)是指在Linux内核中合理且正确地管理连续发生的中断事件。具体而言,当一个中断正在处理过程中,如果相同的中断再次发生,系统需要决定如何处理这一新中断,包括何时屏蔽中断、何时恢复中断、以及何时向中断控制器发送响应信号等。针对不同类型的中断电气特性(如电平触发和边沿触发),Linux提供了标准化的中断流控处理函数。这些处理函数最终会将中断控制权传递给驱动程序在注册中断时提供的处理函数或中断线程中,这些函数均定义在 kernel/irq/chip.c 文件中:
handle_simple_irq:用于处理简单的中断流控,适用于不需要复杂流控机制的中断场景。
handle_level_irq:用于处理电平触发(Level-triggered)中断的流控,确保在中断源未清除前屏蔽中断,防止重复处理。
handle_edge_irq:用于处理边沿触发(Edge-triggered)中断的流控,确保中断处理函数能够及时响应瞬时的中断信号。
handle_fasteoi_irq:用于需要快速响应结束中断(End of Interrupt, EOI)信号的中断控制器,例如GICv3,CPU只需要在每次处理完中断后发出一个end of interrupt(eoi),我们无需关注何时mask,何时unmask。
handle_percpu_irq:用于处理仅在单个CPU上响应的中断,确保这些中断只在指定的CPU上处理,避免跨CPU通信的开销。
handle_nested_irq:用于处理嵌套中断,特别是那些使用中断线程的中断,确保中断处理的顺序性和正确性。
什么时候设置的?
针对GIC V3,在每一个设备的中断初始化过程中,都会设置中断流控函数。
- SGIs: 不需要设置
- PPIs: 设置为handle_percpu_devid_irq,例如timer中断
- SPIs: 设置为handle_fasteoi_irq
- LPIs: 设置为handle_fasteoi_irq
以handle_fasteoi_irq为例,经过层层调用,到达__handle_irq_event_percpu,开始执行已经注册的中断处理程序。
第四步,执行中断处理程序(图的终点)
irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc, unsigned int *flags)
{irqreturn_t retval = IRQ_NONE; // 初始化返回值为IRQ_NONEunsigned 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); // 调用中断处理函数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:/** 捕获那些返回WAKE_THREAD但没有设置线程函数的驱动程序*/if (unlikely(!action->thread_fn)) {warn_no_thread(irq, action); // 发出警告break;}__irq_wake_thread(desc, action); // 唤醒中断线程/* Fall through to add to randomness */case IRQ_HANDLED:*flags |= action->flags; // 更新标志位break;default:break;}retval |= res; // 更新返回值}return retval; // 返回最终的处理结果
}
关键点是通过for_each_action_of_desc(desc, action)遍历action链表,同步调用action->handler,然后根据返回值判断是否要唤醒中断线程action->thread_fn。
那么action链表是如何生成的?action->handler和action->thread_fn是如何赋值的?
request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long flags, const char *name, void *dev); static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev) { return request_threaded_irq(irq, handler, NULL, flags, name, dev); } |
传统的request_irq在注册中断时,需要注册action->handler,并不会注册action->thread_fn(设置为NULL)。
request_threaded_irq在注册中断时:
- handler和thread_fn不能同时为NULL
- 如果handler为NULL,则设置为默认的irq_default_primary_handler
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)
{…………if (!handler) {if (!thread_fn)return -EINVAL;handler = irq_default_primary_handler;}action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);if (!action)return -ENOMEM;action->handler = handler;action->thread_fn = thread_fn;
…………
}
注意,中断注册时传入的参数irq是virq而不是hwirq。如果多个设备共享一个hwirq,则可以多次调用中断注册函数,向同一个virq注册多个irqaction,形成irqaction链表。注册时,必须设置IRQF_SHARED标记。当中断发生时,最终会遍历irqaction链表。
小结一下:
从一副图说起,以Linux通用中断子系统的数据结构为中心,分四步描述了Linux中断处理流程:
第一步,从hwirq发生到转换成virq(图左侧从上到下)
第二步,找出virq对应的irq_desc(图底部从左到右)
第三步,执行中断流控处理程序(图右侧从下向上)
第四步,执行中断处理程序(图的终点)
点击查看系列文章 =》 Interrupt Pipeline系列文章大纲-CSDN博客
原创不易,需要大家多多鼓励!您的关注、点赞、收藏就是我的创作动力!