专业建站公司联系方式,建设网站设备预算,长沙网站建设 鼎誉,wordpress 搜索产品#x1f431;作者#xff1a;一只大喵咪1201 #x1f431;专栏#xff1a;《Linux驱动》 #x1f525;格言#xff1a;你只管努力#xff0c;剩下的交给时间#xff01; 目录 #x1f3c0;Linux系统的中断⚽中断分类软中断和硬中断中断的上半部和下半部 ⚽tasklet⚽工… 作者一只大喵咪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模块的中断BBSP开发人员会设置对应的处理函数一般是
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结构体变量而且会初始化namedev_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用来解析设备树中的中断属性提取出hwirqtype等信息。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数组的大致构成和工作原理。