🐱作者:一只大喵咪1201
🐱专栏:《Linux驱动》
🔥格言:你只管努力,剩下的交给时间!
目录
- 🏀Linux系统的中断
- ⚽中断分类
- 软中断和硬中断
- 中断的上半部和下半部
- ⚽tasklet
- ⚽工作队列
- ⚽threaded_irq
- 🏀Linux中断系统中的重要数据结构
- ⚽irq_desc数组
- ⚽irqaction结构体
- ⚽irq_data结构体
- 🏀总结
🏀Linux系统的中断
如上图所示,本喵使用的IMX6ULL
也是ARM架构,中断也是异常的一种,CPU在运行的过程中,会被各种异常打断,包括:
- 未定义指令异常
- 指令、数据访问异常
- SWI(软中断)
- 快中断
- 中断
导致中断发生的情况有很多,比如:
- 按键
- 定时器
- ADC转化完成
- UART发生接收中断
- 等等
这些中断又汇集到中断控制器,由中断控制器选择优先级高的中断通知CPU。
CPU每执行完一条指令后都会检查是否有中断/异常产生,如果有的话就开始处理:
- 保存现场
在IMUX6ULL
中,现场完全是由软件保存的,不会像STM32F103
一样硬件帮忙保存R0~R3
寄存器的值到栈中。
- 处理异常/中断
由硬件分辨出中是中断以后,去异常向量表中寻找中断处理函数,并且跳转执行。
如上图所示就是Linux内核或者是u-boot
中的异常向量表。每一条指令对应一种异常:
- 发生复位时,CPU就去执行第 1 条指令:
b reset
。 - 发生中断时,CPU就去执行
ldr pc, _irq
这条指令。- 无论什么类型的中断,都是去执行这条指令,在
_irq
中断函数中由软件分辨具体的中断源。
- 无论什么类型的中断,都是去执行这条指令,在
这些指令存放的位置是固定的,比如本喵使用的IMX6ULL
芯片,中断向量_irq
的入口地址就是0x18
,当发生中断时,CPU就强制跳转到0x18
处执行代码。
在向量表里,一般放置的都是一条跳转指令,发生异常/中断时,CPU就会执行向量表中的跳转指令,去调用更复杂处理函数。
- 向量表的位置并不总是从 0 地址开始,很多芯片可以设置某个
vector base
寄存器,指定向量表在其他位置。
比如设置vector base
为0x80000000
, 指定为内存的某个地址,但是向量表中的各个异常向量的偏移地址是固定的:
- 复位向量偏移地址是
0
。 - 中断是
0x18
。
⚽中断分类
中断中断,中断的是Linux中当前正在运行的进程和线程。
如上图所示,对于单核的CPU,此时有进程A和进程B两个进程在运行(Linux内核中认为线程是轻量级进程):
- 在进程A运行的期间,产生了中断:
- 保存A的现场
- 执行中断处理函数
- 没有更高优先级的进程,恢复A的现场
- 继续运行A进程。。。,产生了定时器中断(系统的心跳——时钟):
- 保存A的现场
- A的时间片没有用完
- 没有更高优先级的进程,恢复A的现场
- 继续运行A进程。。。,产生了定时器中断:
- 保存A的现场
- A的时间片用完了
- 找出进程B的现场,恢复B的现场
在Linux中,中断的处理有两个原则:
- 中断不能嵌套。
假设中断可以嵌套的话会发生什么呢?
- 假设正在处理1号中断,此时更高优先级的2号中断产生了,在处理2号中断之前,要保存1号中断的现场。
- 开始处理2号中断,此时更高优先级的3号中断产生了,在处理3号中断之前,要保存2号中断的现场。
- …
如此嵌套下去,会导致栈空间不足,现场保存出现问题,从而导致系统奔溃,所以为了安全和简便,在Linux不允许中断嵌套。
- 中断越快越好。
如果中断的处理时间较长,Linux中的线程以及进程就无法得到执行,尤其是GUI的进程,这样就会导致整个系统非常卡顿。
软中断和硬中断
Linux 系统把中断的意义扩展了,对于按键中断等硬件产生的中断,称之为硬件中断(hard irq)。每个硬件中断都有对应的处理函数,比如按键中断、 网卡中断,处理它们的中断函数肯定不一样。
如上图所示,可以简单认为有一个硬件中断数组,里面存放着不同硬件中断处理函数的指针。
- 当发生A中断时,对应的
irq_function_A
函数被调用。
除了硬件中断外,还人为的制造了软件中断,每一个软件中断也对应有一个中断处理函数。
如上图所示,也可以简单的认为有一个软件中断数组,里面存放着不同软件中断处理函数的指针。
- 当发生软件中断X时,对应的
irq_function_X
函数被调用。
软件中断的产生由flag
决定,只要在软件中将其置为1就表示发生了该中断。
如上图所示,软件中断也有很多类型,我们比较常用的就是TASKLET_SOFTIRQ
,它表示中断的下半部。
中断的上半部和下半部
如果一个中断处理必须要耗费比较长的时间来处理呢?比如键盘上的按键中断,它每产生一次后就需要扫描整个键盘,这是比较耗时的。
由于Linux中,中断不能嵌套,所以段时间内,系统是关中断的,此时就不会处理其他中断,再有中断产生时系统就会出问题。
为了遵循中断处理必须快的原则,可以将耗时较长的中断函数分为上半部分和下半部分:
- 上半部分处理紧急的事情。
- 下半部分处理不紧急的事情。
如上图所示,在中断的上半部中紧做紧急的事情,这个过程的中断是关闭的。比如给键盘发送信号,清除中断标志位,防止它不断向CPU发送中断信号,然后重新开中断。
在中断的下半部中处理那些不紧急的事,此时是开中断的,可以产生其他中断。
- 在处理完中断上半部时,通过软件触发中断下半部的处理。
- 中断下半部的处理发生在上半部处理完毕后。
⚽tasklet
当下半部比较耗时,并且处理比较简单时,可以使用tasklet
来处理下半部,tasklet
是软件中断一种类型。
如上图所示代码为执行中断处理的上半部和下半部流程,这样不清晰,画图来说明一下。
如上图所示是上半部和下半部的处理流程图,假设有中断A和中断B两个中断:
- 中断A发生:
- 开始处理后,处于关中断状态,让
preempt_count++
,该值为1。 - 执行中断上半部,处理紧急事情。
- 将
preempt_count--
,该值为0。 - 判断
preempt_count
是否非0,此时是0,不符合条件,执行N
分支。 - 再让
preempt_count++
,此时该值为1。 - 开中断,允许其他中断产生。
- 执行中断下半部,处理不紧急事情。
- 处理完毕后关中断。
- 再让
preempt_count--
,此时该值为0。
整个中断流程走完后会重新打开中断。
- 中断A处理的过程中,再次发生A中断或者中断B发生:
- 开始处理后,处于关中断状态,让
preempt_count++
,该值为1。 - 执行中断上半部,处理紧急事情。
- 将
preempt_count--
,该值为0。 - 判断
preempt_count
是否非0,此时是0,不符合条件,执行N
分支。 - 再让
preempt_count++
,此时该值为1。 - 开中断,允许其他中断产生。
在中断A执行下半部的时候,被其他中断打了,于是开始处理新的中断:
- 开始处理新中断后,处于关中断状态,让
preempt_count++
,该值为2。 - 执行新中断的上半部。
- 让
preempt_count--
,该值为1。 - 判断
preempt_count
是否非0,此时是1不是0,执行Y
分支,直接退出新中断的处理。
新中断退出时重新打开了中断,允许其他中断产生。
- 恢复中断A的下半部处理,接着执行中断A的下半部。
- 处理完毕后关中断。
- 再让
preempt_count--
,此时该值为0。
- 由于
preempt_count
的存在,新中断仅处理了上半部,没有处理下半部就直接退出了。- 被打断的中断A下半部没有受到影响,仍然恢复了执行。
中断的上半部和下半部是N : 1
的关系,无论产生多少次相同类型的中断,中断的上半部会执行多次,但是中断的下半部只执行一次。
如果在处理中断A的下半部时,产生的是B中断,并且B中断的下半部是另一个软件中断,那么在B中断处理完上半部退出后:
- 先恢复中断A的下半部继续处理。
- 中断A的下半部处理完毕后,再去处理中断B的下半部。
⚽工作队列
在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。但是毕竟整个中断的处理还没走完,这期间进程是无法执行的。
假设下半部要执行 1~2 分钟,在这 1~2 分钟里进程都是无法响应的,Linux中根本无法忍受这种情况,所以,如果中断下半部要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:
- 在中断上半部唤醒内核线程,用内核线程去处理中断下半部。
- 此时内核线程和应用层线程都一起竞争CPU资源,系统不会卡顿。
- 内核线程是Linux系统帮我们创建的,线程名为
kworker
。
kworker
线程会去工作队列work queu
中取出一个一个工作work
,来执行它里面的函数。
所以在使用内核线程处理中断的下半部时:
- 创建并填充
work
结构体:
如上图所示,先创建一个work
结构体,然后调用DECLARE_WORK
把要让内核线程执行的函数指针填充到work
结构体中。
- 将
work
结构体提交给work queue
,调用schedule_work
实现。 - 执行
work
中的中断下半部函数。- 到底由谁执行不用我们管。
schedule_work
还会把内核线程kworker
唤醒。
- 在中断场景中,可以在中断上半部调用
schedule_work
函数将work
结构体提供给work queue
。- 既然此时中断下半部是在线程中运行,那对应的函数就可以阻塞和休眠,并不会影响其他进程和线程。
⚽threaded_irq
用kworker
内核线程来处理中断下半部时,一个kworker
线程只能由一个 CPU 执行, 多个中断的work
都放在work queue
中由同一个kworker
线程来处理,在单 CPU 系统中是没有问题的。
但是在多核系统中,明明有那么多 CPU 空着,偏偏让多个中断的下半部挤在一个CPU上处理并不合适。
新技术threaded_irq
可以为每一个中断的下半部都创建一个内核线程,多个中断的下半部内核线程可以分配到多个CPU上执行,提高了效率。
如上图所示request_threaded_irq
函数,用来为每一个中断下半部创建一个线程。
irq
:表示哪个中断,后面本喵再详细讲解。handler
:中断上半部处理函数,可以为空。thread_fn
:中断下半部内核线程处理函数。
其他参数在用到时候再进行说明。
🏀Linux中断系统中的重要数据结构
如上图所示便是Linux系统中最重要的数据结构,弄明白这个图也就了解了Linux的中断系统。
前面说,硬件中断和软件中断的处理函数放在一个数组中,确实是这样,只是这个数组是一个结构体数组,而核心便是irq_desc
结构体。
如上图所示中断结构图,产生中断时:
- 外部设备1~外部设备n共享一个GPIO中断B,该中断是GPIO中的某一个引脚。
- 多个GPIO中断汇聚到GIC(通用中断控制器)的A号中断。
- GIC再去中断CPU,来处理中断源。
CPU处理中断时:
- 先读取GIC中的寄存器获得中断号A。
- 再从GPIO中得到中断号B。
- 最后判断是哪一个外部设备发生了中断。
⚽irq_desc数组
irq_desc
数组中的每一个元素都是irq_desc
结构体。
如上图所示irq_desc
结构体的定义,irq_desc
数组中的每一项都有一个函数指针handle_irq
,还有一个action
链表。
CPU在处理中断时,中断处理函数的来源有三个:
- GIC的中断处理函数:
-
CPU根据GIC中的寄存器,确定了中断号A,从而去调用中断处理函数
irq_desc[A].handle_irq
。 -
该函数会读取GPIO控制器中的寄存器,确定是哪个引脚发生了中断,从而确定中断号B,再去调用
irq_desc[B].handle_irq
函数。
- 中断A是CPU感受到的顶层中断。
- 模块的中断处理函数:
对于GPIO模块的中断B,BSP开发人员会设置对应的处理函数,一般是:
handle_level_irq
:电平触发处理函数。handle_edge_irq
:边沿触发处理函数。
但是此时中断B是一个共享中断,该引脚上的外部设备1~外部设备n都可能产生中断,可能是一个设备,也可能是多个设备。
所以irq_desc[B].handle_irq
需要判断是哪个外部设备产生的中断。
如上图所示,此时就会遍历irq_desc[B]
结构体中的action
链表,链表中的每一项都能代表一个外部设备,并且有外部设备的中断处理函数。
- 从链表中能找到产生中断的外部设备。进而调用外部设备的中断处理函数。
- 遍历寻找和调用同样是由BSP开发工程师实现的。
- 外部设备提供的中断处理函数:
外部设备可能是芯片,也可能总是简单的按键,它们的中断处理函数由自己的驱动程序提供,因为:
- 它是最熟悉这个设备的人。
- 它知道如何判断设备是否发生了中断。
- 它知道发生了中断后该如何处理。
所以对于共享中断B,它的irq_desc[B]
结构体中的action
链表中就会存放着多个外部设备的中断处理函数。
一旦irq_desc[B].handle_irq
中断处理函数确定是哪个外部设备产生了外部中断,就会调用外部设备的中断处理函数。
- 根据上面分析,虽然GIC中断控制器和GPIO中断控制器有上下级之分,但是它们所包含的中断号都在
irq_desc
数组中。
⚽irqaction结构体
iqr_desc[B]
中的链表action
里,每一项都是irqaction
结构体变量。
如上图所示irqaction
结构体的定义,当外部设备的驱动程序调用request_irq
和request_threaded_irq
注册中断处理函数时,内核就会构造一个irqaction
结构体变量,而且会初始化name
,dev_id
等成员。
- 只用
request_irq
注册中断处理函数时,注册的就是上半部,意味着完全由上半部来处理。
最重要的是handler
和thread_fn
以及thread
三个成员:
handler
:是中断处理函数的上半部,用来处理紧急的事情。thread_fn
:是内核线程中断处理函数的下半部,用来处理不紧急且耗时的事情。thread
:是用来处理中断下半部的内核线程,当handler
执行完毕后,Linux会唤醒该内核线程,执行thread_fn
中断下半部处理函数。
在初始化这三个成员时,要注意:
- 可以不提供
handler
只提供thread_fn
,完全由内核线程来处理中断。 - 也可以既提供
handler
也提供thread_fn
,这就是中断上半部、下半部。
至于dev_id
成员,是在调用request_irq
时传入的,该成员有两个作用:
- 中断处理函数执行时,能够用得上
dev_id
。 - 卸载中断时要传入
dev_id
,这样才能在action
链表中根据dev_id
找到对应项。- 所以在共享中断中必须提供
dev_id
,非共享中断可以不提供。
- 所以在共享中断中必须提供
⚽irq_data结构体
在irq_des
数组的每个成员,如irq_desc[A]
中,除了有struct iqraction
类型的链表action
外,还有类型是struct iqr_data
的成员irq_data
。
如上图所示irq_data
结构体的定义,它就是个中转站,里面有irq_chip
指针和irq_domain
指针,都是指向别的结构体。
irq
:软件中断号(虚拟中断号)。hirq
:硬件中断号。
- 这里的软件中断号是软件根据硬件中断号映射出来的,和前面的软件中断的中断号不同。
- 通过软件中断号可以在
irq_desc
数组中找到相应中断的处理函数,如irq_desc[B].handler_irq
。
我们在驱动程序中使用request_irq
和request_threaded_irq
注册中断处理函数的时候,传入的irq
参数就是这个虚拟的软件中断号。
irq_data
中的irq_domain
成员会建立hirq
和irq
之间的映射关系,将hirq
映射为全局的irq
。
irq_domain结构体:
如上图所示,irq_domain
结构体中有一个irq_domain_ops
结构体成员,里面存放有xlate
函数和map
函数:
xlate
:用来解析设备树中的中断属性,提取出hwirq
,type
等信息。map
:把hwirq
转化为irq
。
假设现在有gpio1_5
和gpio2_5
俩个引脚是中断源,此时这两组使用的硬件中断号hirq
都是5,只通过hirq
是无法区别这两个引脚的。
此时就需要根据gpio1
和gpio2
各自的irq_domain
结构体使用xlate
来区分了,并且使用map
将这两个硬件中断号转化成两个不同的软件中断号。
- 将转化后的
hirq
和irq
的映射关系存放到linear_revmap
成员数组中。
此时就能根据硬件中断号hirq
直接找到映射出来的软件中断号irq
了。
irq_chip结构体:
如上图所示irq_chip
结构体的定义,这个结构体跟芯片息息相关,作用就是对GPIO等中断控制器模块中的中断源进行使能等操作。
irq_enable
:使能中断。irq_disable
:使能中断。irq_mask
:屏蔽中断。irq_unmask
:解除屏蔽。
我们在request_irq
注册了中断后,并不需要手工去使能中断,原因就是系统会调用irq_chip
里的函数帮我们使能。
我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是因为系统会帮我们调用irq_chip
中的相关函数。
但是对于外部设备相关的清中断操作,还是需要我们自己做的。 就像上图里的外部设备 1~外部设备 n,因为外设备千变万化,内核里可没有对应的清除中断操作。
🏀总结
对于Linux系统的中断,要知道有软件中断和硬件中断,并且将比较耗时但处理简单的中断程序分为上半部和下半部:
- 处理上半部时是关中断的,此时无法产生其他中断。
- 处理下半部时是开中断的,可以产生其他中断。
对于中断下半部,又分为三种处理方式:
- 软件中断
tasklet
。 - 内核线程
kworker
,只有一个内核线程去处理多个中断下半部。 threaded_irq
,为每一个中断的下半部创建一个内核线程。
要了解Linux中是如何描述和处理中断的,清除irq_desc
数组的大致构成和工作原理。