1.2.6 下半部要做的事情太多并且很复杂:工作队列
在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。但是毕竟整个中断的处理还没走完,这期间APP是无法执行的。
假设下半部要执行1、2分钟,在这1、2分钟里APP都是无法响应的。
这谁受得了?
所以,如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和APP都一样竞争执行,APP有机会执行,系统不会卡顿。
这个内核线程是系统帮我们创建的,一般是kworker线程,内核中有很多这样的线程:
kworker线程要去“工作队列”(work queue)上取出一个一个“工作”(work),来执行它里面的函数。
那我们怎么使用work、work queue呢?
a. 创建work:
你得先写出一个函数,然后用这个函数填充一个work结构体。比如:
b. 要执行这个函数时,把work提交给work queue就可以了:
上述函数会把work提供给系统默认的work queue:system_wq,它是一个队列。
c. 谁来执行work中的函数?
不用我们管,schedule_work函数不仅仅是把work放入队列,还会把kworker线程唤醒。此线程抢到时间运行时,它就会从队列中取出work,执行里面的函数。
d. 谁把work提交给work queue?
在中断场景中,可以在中断上半部调用schedule_work函数。
总结:
a. 很耗时的中断处理,应该放到线程里去
b. 可以使用work、work queue
c. 在中断上半部调用schedule_work函数,触发work的处理
d. 既然是在线程中运行,那对应的函数可以休眠。
1.2.7 新技术:threaded irq
使用线程来处理中断,并不是什么新鲜事。使用work就可以实现,但是需要定义work、调用schedule_work,好麻烦啊。
太懒了太懒了,就这2步你们都不愿意做。
好,内核是为懒人服务的,再杀出一个函数:
你可以只提供thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数。
说你懒是开玩笑,内核开发者也不会那么在乎懒人。
以前用work来线程化地处理中断,一个worker线程只能由一个CPU执行,多个中断的work都由同一个worker线程来处理,在单CPU系统中也只能忍着了。但是在SMP系统中,明明有那么多CPU空着,你偏偏让多个中断挤在这个CPU上?
新技术threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个CPU上执行,这提高了效率。
1.3 Linux中断系统中的重要数据结构
本节内容,可以从request_irq(include/linux/interrupt.h)函数一路分析得到。
能弄清楚下面这个图,对Linux中断系统的掌握也基本到位了。
最核心的结构体是irq_desc,之前为了易于理解,我们说在Linux内核中有一个中断数组,对于每一个硬件中断,都有一个数组项,这个数组就是irq_desc数组。
注意:如果内核配置了CONFIG_SPARSE_IRQ,那么它就会用基数树(radix tree)来代替irq_desc数组。SPARSE的意思是“稀疏”,假设大小为1000的数组中只用到2个数组项,那不是浪费嘛?所以在中断比较“稀疏”的情况下可以用基数树来代替数组。
1.3.1 irq_desc数组
irq_desc结构体在include/linux/irqdesc.h中定义,主要内容如下图:
每一个irq_desc数组项中都有一个函数:handle_irq,还有一个action链表。要理解它们,需要先看中断结构图:
外部设备1、外部设备n共享一个GPIO中断B,多个GPIO中断汇聚到GIC(通用中断控制器)的A号中断,GIC再去中断CPU。那么软件处理时就是反过来,先读取GIC获得中断号A,再细分出GPIO中断B,最后判断是哪一个外部芯片发生了中断。
所以,中断的处理函数来源有三:
① GIC的处理函数:
假设irq_desc[A].handle_irq是XXX_gpio_irq_handler(XXX指厂家),这个函数需要读取芯片的GPIO控制器,细分发生的是哪一个GPIO中断(假设是B),再去调用irq_desc[B]. handle_irq。
注意:irq_desc[A].handle_irq细分出中断后B,调用对应的irq_desc[B].handle_irq。
显然中断A是CPU感受到的顶层的中断,GIC中断CPU时,CPU读取GIC状态得到中断A。
② 模块的中断处理函数:
比如对于GPIO模块向GIC发出的中断B,它的处理函数是irq_desc[B].handle_irq。
BSP开发人员会设置对应的处理函数,一般是handle_level_irq或handle_edge_irq,从名字上看是用来处理电平触发的中断、边沿触发的中断。
注意:导致GPIO中断B发生的原因很多,可能是外部设备1,可能是外部设备n,可能只是某一个设备,也可能是多个设备。所以irq_desc[B].handle_irq会调用某个链表里的函数,这些函数由外部设备提供。这些函数自行判断该中断是否自己产生,若是则处理。
③ 外部设备提供的处理函数:
这里说的“外部设备”可能是芯片,也可能总是简单的按键。它们的处理函数由自己驱动程序提供,这是最熟悉这个设备的“人”:它知道如何判断设备是否发生了中断,如何处理中断。
对于共享中断,比如GPIO中断B,它的中断来源可能有多个,每个中断源对应一个中断处理函数。所以irq_desc[B]中应该有一个链表,存放着多个中断源的处理函数。
一旦程序确定发生了GPIO中断B,那么就会从链表里把那些函数取出来,一一执行。
这个链表就是action链表。
对于我们举的这个例子来说,irq_desc数组如下:
1.3.2 irqaction结构体
irqaction结构体在include/linux/interrupt.h中定义,主要内容如下图:
当调用request_irq、request_threaded_irq注册中断处理函数时,内核就会构造一个irqaction结构体。在里面保存name、dev_id等,最重要的是handler、thread_fn、thread。
handler是中断处理的上半部函数,用来处理紧急的事情。
thread_fn对应一个内核线程thread,当handler执行完毕,Linux内核会唤醒对应的内核线程。在内核线程里,会调用thread_fn函数。
可以提供handler而不提供thread_fn,就退化为一般的request_irq函数。
可以不提供handler只提供thread_fn,完全由内核线程来处理中断。
也可以既提供handler也提供thread_fn,这就是中断上半部、下半部。
里面还有一个名为sedondary的irqaction结构体,它的作用以后再分析。
在reqeust_irq时可以传入dev_id,为何需要dev_id?作用有2:
① 中断处理函数执行时,可以使用dev_id
② 卸载中断时要传入dev_id,这样才能在action链表中根据dev_id找到对应项
所以在共享中断中必须提供dev_id,非共享中断可以不提供。
1.3.3 irq_data结构体
irq_data结构体在include/linux/irq.h中定义,主要内容如下图:
它就是个中转站,里面有irq_chip指针 irq_domain指针,都是指向别的结构体。
比较有意思的是irq、hwirq,irq是软件中断号,hwirq是硬件中断号。比如上面我们举的例子,在GPIO中断B是软件中断号,可以找到irq_desc[B]这个数组项;GPIO里的第x号中断,这就是hwirq。
谁来建立irq、hwirq之间的联系呢?由irq_domain来建立。irq_domain会把本地的hwirq映射为全局的irq,什么意思?比如GPIO控制器里有第1号中断,UART模块里也有第1号中断,这两个“第1号中断”是不一样的,它们属于不同的“域”──irq_domain。
1.3.4 irq_domain结构体
irq_domain结构体在include/linux/irqdomain.h中定义,主要内容如下图:
当我们后面从设备树讲起,如何在设备树中指定中断,设备树的中断如何被转换为irq时,irq_domain将会起到极大的作为。
这里基于入门的解度简单讲讲,在设备树中你会看到这样的属性:
interrupt-parent = <&gpio1>; interrupts = <5 IRQ_TYPE_EDGE_RISING>;
它表示要使用gpio1里的第5号中断,hwirq就是5。
但是我们在驱动中会使用request_irq(irq, handler)这样的函数来注册中断,irq是什么?它是软件中断号,它应该从“gpio1的第5号中断”转换得来。
谁把hwirq转换为irq?由gpio1的相关数据结构,就是gpio1对应的irq_domain结构体。
irq_domain结构体中有一个irq_domain_ops结构体,里面有各种操作函数,主要是:
① xlate
用来解析设备树的中断属性,提取出hwirq、type等信息。
② map
把hwirq转换为irq。
1.3.5 irq_chip结构体
irq_chip结构体在include/linux/irq.h中定义,主要内容如下图:
这个结构体跟“chip”即芯片相关,里面各成员的作用在头文件中也列得很清楚,摘录部分如下:
@irq_startup: start up the interrupt (defaults to ->enable if NULL) @irq_shutdown: shut down the interrupt (defaults to ->disable if NULL) @irq_enable: enable the interrupt (defaults to chip->unmask if NULL) @irq_disable: disable the interrupt @irq_ack: start of a new interrupt @irq_mask: mask an interrupt source @irq_mask_ack: ack and mask an interrupt source @irq_unmask: unmask an interrupt source @irq_eoi: end of interrupt
我们在request_irq后,并不需要手工去使能中断,原因就是系统调用对应的irq_chip里的函数帮我们使能了中断。
我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用irq_chip中的相关函数。
但是对于外部设备相关的清中断操作,还是需要我们自己做的。
就像上面图里的“外部设备1“、“外部设备n”,外设备千变万化,内核里可没有对应的清除中断操作
1.4 在设备树中指定中断_在代码中获得中断
1.4.1 设备树里中断节点的语法
参考文档:
内核Documentation\devicetree\bindings\interrupt-controller\interrupts.txt
1.4.1.1 设备树里的中断控制器
中断的硬件框图如下:
在硬件上,“中断控制器”只有GIC这一个,但是我们在软件上也可以把上图中的“GPIO”称为“中断控制器”。很多芯片有多个GPIO模块,比如GPIO1、GPIO2等等。所以软件上的“中断控制器”就有很多个:GIC、GPIO1、GPIO2等等。
GPIO1连接到GIC,GPIO2连接到GIC,所以GPIO1的父亲是GIC,GPIO2的父亲是GIC。
假设GPIO1有32个中断源,但是它把其中的16个汇聚起来向GIC发出一个中断,把另外16个汇聚起来向GIC发出另一个中断。这就意味着GPIO1会用到GIC的两个中断,会涉及GIC里的2个hwirq。
这些层级关系、中断号(hwirq),都会在设备树中有所体现。
在设备树中,中断控制器节点中必须有一个属性:interrupt-controller,表明它是“中断控制器”。
还必须有一个属性:#interrupt-cells,表明引用这个中断控制器的话需要多少个cell。
#interrupt-cells的值一般有如下取值:
① #interrupt-cells=<1>
别的节点要使用这个中断控制器时,只需要一个cell来表明使用“哪一个中断”。
② #interrupt-cells=<2>
别的节点要使用这个中断控制器时,需要一个cell来表明使用“哪一个中断”;
还需要另一个cell来描述中断,一般是表明触发类型:
第2个cell的bits[3:0] 用来表示中断触发类型(trigger type and level flags):
1 = low-to-high edge triggered,上升沿触发 2 = high-to-low edge triggered,下降沿触发 4 = active high level-sensitive,高电平触发 8 = active low level-sensitive,低电平触发
示例如下:
vic: intc@10140000 { compatible = “arm,versatile-vic”; interrupt-controller; #interrupt-cells = <1>; reg = <0x10140000 0x1000>; };
如果中断控制器有级联关系,下级的中断控制器还需要表明它的“interrupt-parent”是谁,用了interrupt-parent”中的哪一个“interrupts”,请看下一小节。