上一篇文章中,引入了Linux对于中断的一些简略流程以及中断抽象为具体实际形象。此文章主要是继续加深对Linux对中断的处理流程以及一些相应的数据结构。
目录
Linux对中断的扩展:硬件中断、软件中断
多中断处理
中断上下部处理流程
发生中断A,并被中断A打断
发生中断A,并被中断B打断
下半部处理时间过长
Linux中断系统重要的数据结构
irq_desc
irqaction结构体
irq_data结构体
irq_domain 结构体
irq_chip 结构体
Linux对中断的扩展:硬件中断、软件中断
对于像按键等硬件所产生的中断称之为硬件中断,Linux中每一个硬件中断都有一个对应的中断号以及处理函数。可以这么简单的理解,有一个硬件中断函数数组,数组中第几个数据对应第几个中断号,以及对应的中断处理函数。
相应的,也有软件中断,相比于硬件中断,软件中断多了一个flag标志位,用来表示中断是否发生了 。
对于软件中断,只要flag标志位被置为1,那么就表示发生了该软件中断。同时,相比于硬件中断,软件中断的优先级会更低,优先处理的是硬件中断。如下是软件中断的枚举。
那么如何触发软件中断呢,最核心的函数是raise_softirq,它的形参是软件中断号,当设置后,系统会设置改软件中断为待处理状态,等到硬件中断处理完成后,会处理这个软件中断。
void raise_softirq(unsigned int nr);
那如何注册软件中断的处理函数呢,系统使用open_softirq函数,这个函数可以给对应的软件中断注册处理函数。
void open_softirq(int nr, void (*action) (struct soft_action*));
多中断处理
如果一个中断需要消耗过多的时间来处理,那么我们可以把中断分为中断上部和中断下部,中断上部用来处理紧急的中断,下部用来处理非紧急的中断。当我们在中断上部处理紧急的中断的时候,是关闭中断的,当处理完紧急的中断,来到中断下部,此时重新打开中断,处理比较不紧急的中断。
中断上下部处理流程
发生中断A,并被中断A打断
假如发生中断A,中断上半部优先级高首先执行,count首先++,执行完中断后--,此时count为0,进入中断下半部,此时count++,开启中断,此时再次发生中断A打断中断下半部,继续走到步骤1这里,count++,执行中断,count--,此时count等于1,并不会进入中断下半部,而是直接完成处理,此时恢复现场,到步骤7,执行完中断A的下半部,count--,此时count为0。完成完整的中断流程。可见,不管多少个中断,中断上半部和中断下半部是N对1的关系,也就是中断下半部只会执行一次。
发生中断A,并被中断B打断
假如发生中断A,中断上半部优先级高首先执行,count首先++,执行完中断后--,此时count为0,进入中断下半部,此时count++,开启中断,此时,B中断打断了A中断的中断下半部,回到步骤1,此时执行的是中断B的中断上半部,count++,执行中断B,count--,此时count等于1,并不会进入中断下半部,而是直接完成处理,此时恢复现场,到步骤7,执行完中断A以及中断B的下半部,count--,此时count为0。完成完整的中断流程。可见,中断下半部的处理是多个中断下半部一起进行处理的,而不是单独处理。
下半部处理时间过长
一般来说,中断下半部是用来处理那些比较不重要的软件中断,那么假如中断下半部处理的中断也需要占用非常久的时间怎么办呢。
毕竟还是在中断中,其他进程线程是无法执行的,假如存在GUI的进程,会导致页面卡死。所以,假如中断所需要的时间实在是太耗时了,我们就不使用软件中断来处理,而是使用内核线程来处理,此时中断下半部就是内核线程,和进程一样都有竞争执行的机会。
如下为一些系统的内核线程
kworker线程是内核线程的一种,它要去工作队列(work queue)上取出一个个工作(work)来执行,那么我们该怎么把一个个work放在work queue上面呢。
一、创建work,先写一个处理函数,在调用函数把这个处理函数填充进work结构体中。
二、结构体创建完成了,我们需要把该结构体提交给work queue,一般在中断上半部,调用schedule_work 函数把work交给work queue
三、放进后,只需要等线程抢占运行即可,这样中断下半部就可以和其他进程抢占CPU执行,这样就不存在因为中断时间过久导致其他进程过久无法相应
总的来说,对于那些很耗时的中断,我们可以交给内核线程来进行处理,内核线程我们可以看成线程,不当成中断来看待,那对应可以抢占运行,也会休眠。
Linux中断系统重要的数据结构
irq_desc
irq_desc结构体的主要内容如下:
每一个 irq_desc 数组项中都有一个函数:handle_irq,还有一个 action 链表。handle_irq为中断处理函数,action链表主要存放各个中断控制器上下级中断的中断处理函数。
外部中断1、中断n共享了B号中断,多个GPIO控制器的中断汇集到GIC的A号中断。当发生了中断,是从左到右依次中断,最后中断掉CPU。
CPU处理的时候,则是从右到左依次进行处理,CPU会首先读取GIC,获取中断号A,调用中断A的handle_irq(irq_desc[A].handle_irq 中断处理函数,BSP工程师提供),获取到发生中断的中断号B,此时调用irq_desc[B]. handle_irq,中断B为共享中断,irq_desc[B]存在一个action链表,里面记录了各个共享中断的外部设备中断函数,一旦确定了中断来自中断B,此时会一一执行中断B中(irq_desc[B])的action链表,把各个B号中断下的外接设备的中断函数一一执行,以此确定到底是哪个外部设备发生了中断。
irqaction结构体
irqaction 结构体存在于irq_desc结构体下,主要的内容如下:
当调用 request_irq、request_threaded_irq 注册中断处理函数时,内核就会构造一个 irqaction 结构体。在里面保存 name、dev_id 等,最重要的 是 handler、thread_fn、thread。
handler 是中断处理的上半部函数,用来处理紧急的事情。 thread_fn 对应一个内核线程 thread,当 handler 执行完毕,Linux内核会唤醒对应的内核线程。在内核线程里,会调用 thread_fn 函数。
irq_data结构体
它就是个中转站,里面有 irq_chip 指针 irq_domain 指针,都是指向别的 结构体。
还有两个成员: irq、hwirq,irq 是软件中断号,hwirq 是硬件中断号。 比如上面我们举的例子,在 GPIO 中断 B 是软件中断号,可以找到 irq_desc[B] 这个数组项;GPIO 里的第 x 号中断,这就是 hwirq。比如说GPIO3的5号引脚中断和GPIO4的5号引脚中断,两个的软件中断号是不同的。
irq_domain结构体中的函数会把hwirq映射为软件中断号irq。
irq_domain 结构体
interrupt-parent = <&gpio1>;
interrupts = <5 IRO_TYPE_EDGE_RISING>;
如上表示 GPIO1的5号引脚,通过上升沿触发中断。此时,hwirq就是5,但是它属于的domain属于GPIO1。
irq_domain 结构体中的如下两个函数用来解析设备树中得到的中断数据:
xlate :用来解析设备树的中断属性,提取出 hwirq、type 等信息。比如上面举例的,hwirq就是5,type 就是IRO_TYPE_EDGE_RISING
map :把 hwirq 转换为 irq。
irq_chip 结构体
irq_chip 结构体的主要成员如下:
在 request_irq 创建中断后,并不需要手工去使能中断,原因就是系统调用对 应的 irq_chip 里的函数帮我们使能了中断。
我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是 系统帮我们调用 irq_chip 中的相关函数。 但是对于外部设备相关的清中断操作,还是需要我们自己做的。