Linux 中断子系统:中断处理知识点大全
<div id="navCategory"><h5 class="catalogue">目录</h5><ul class="first_class_ul"><li>Linux 中断相关节点
<ul class="second_class_ul"><li>
/proc/interrupts
</li><li>
/proc/irq/…
</li><li>
如何让某个中断在某个特定的 CPU 处理?
</li><li>
中断分发机制
</li><li>
中断状态机
</li><li>
Linux 抢占机制
</li><li>
中断与进程
</li><li>
Linux 为什么中断不允许休眠?
</li><li>
unhandled interrupt 和 spurious interrupt
</li><li>
中断的生命周期
</li></ul></li></ul></div><p>
<img title="Linux 中断子系统:中断处理知识点大全" alt="Linux 中断子系统:中断处理知识点大全" border="0" src="https://zhuji.jb51.net/uploads/img/202305/524f325d35adcbcbf43463d2df35e790.jpg"></p>
<p class="maodian"></p><h2>
Linux 中断相关节点
</h2>
<p class="maodian"></p><h3>
/proc/interrupts
</h3>
<p>
cat 这个节点,会打印系统中所有的中断信息,如果是多核CPU,每个核都会打印出来。
</p>
<p>
包括每个中断的名字、中断号 IRQ number、每个中断的触发次数、在哪个CPU核处理的、是边沿触发还是电平触发,属于哪个中断控制器,都会打印出来。
</p>
<p class="maodian"></p><h3>
/proc/irq/…
</h3>
<p>
进入这个目录。会看到以中断号命名的文件夹,每个中断号文件夹下面都有几个节点,存储了这个中断的信息,比如 smp_affinity、affinity_hint、spurious等。smp_affinity 代表中断号核CPU之间的亲缘绑定关系,也就是如果某个中断号绑定了一个CPU核,那么这个中断就会一直在这个CPU上处理。
</p>
<p class="maodian"></p><h3>
如何让某个中断在某个特定的 CPU 处理?
</h3>
<p>
kernel 2.4 以后的版本才支持把不同的硬件中断请求(IRQs)分配到特定的 CPU 上,这个绑定技术被称为 SMP IRQ Affinity. 更多介绍请参看 Linux 内核源代码自带的文档:linux-4.14/Documentation/IRQ-affinity.txt
</p>
<p>
/proc/irq/{IRQ}/smp_affinity
</p>
<p>
/proc/irq/{IRQ}/smp_affinity_list
</p>
<p>
/proc/irq/{IRQ}/smp_affinity 指定给定的 irq 中断号源允许哪些CPU执行,它是一个掩码位,比如是ff,代表11111111,表示这个中夺冠可以在 8 个 CPU 执行,具体在哪一个 CPU 执行,靠分配器分配。
</p>
<p>
如果这个 /proc/irq/{IRQ}/smp_affinity 指定为 00000001,代表这个IRQ只能在最后一个CPU核进行处理,其他CPU不允许处理,大家可以测试一下,博主测试是 OK 的(GIC支持,其他中断控制器不一定)。
</p>
<p>
串口手动赋值的重启以后会消失,可以在代码中调用 irq_set_affinity 函数,指定中断的掩码,来达到某个中断被固定CPU处理的需求。
</p>
<p class="maodian"></p><h3>
中断分发机制
</h3>
<p>
对于 GIC-V2 而言,SPI 的分发是根据 Distributor 中的 Interrupt Processor Targets Registers 来决定的。对于任何一个 SPI,其都有在某个 GICD_ITARGETSRn 寄存器中有 8 个bit标识送达的 processor,如果只有一个 bit 被 set,那么就很简单了,如果该中断是当前优先级最高的中断,那么 Distributor 就会送到对应的 CPU interface,该中断最终会送达指定的 CPU。
</p>
<p>
如果该中断对应的 Interrupt Processor Targets Registers 中的那 8 个 bit 有多个 bit 被 set 的话,Distributor 如何处理呢?“依次轮着把产生的中断给各个 CPU,还是说看哪个CPU有空就给哪个CPU来着”,让硬件处理这么复杂的逻辑有些不合适,实际上,GIC 的硬件是不会进行任何判断的,也不会集成任何的算法,它就是根据Interrupt Processor Targets Registers的bit设定情况,忠实的把中断送往指定的一个processor或者多个processors。
</p>
<p>
大家可以去看看 gic_set_affinity 这个函数,这个函数确保一个中断的 Interrupt Processor Targets Registers 中的那8个bit只有一个bit被设定。
</p>
<p>
/kernel5.15/drivers/irqchip/irq-gic-v3.c
</p>
<p>
<img title="Linux 中断子系统:中断处理知识点大全" alt="Linux 中断子系统:中断处理知识点大全" border="0" src="https://zhuji.jb51.net/uploads/img/202305/540ecbe434a4f2e5e0992a672d3a96ee.jpg"></p>
<p>
<img title="Linux 中断子系统:中断处理知识点大全" alt="Linux 中断子系统:中断处理知识点大全" border="0" src="https://zhuji.jb51.net/uploads/img/202305/83d0e4b5da7e67ece37e89f15e83ad83.jpg"></p>
<p>
在 1244 和 1246 行,1246 行就是在 online 的 CPU 中选中一个,1263 行写入到寄存器中,GIC 会读取这个寄存器,是哪个 CPU,然后将中断发给这个CPU。中间的函数很简单,大家可以自己看。
</p>
<p class="maodian"></p><h3>
中断状态机
</h3>
<p>
对于 GIC-V2 而言,中断的状态机由 Distributor 维护,每个中断都有一个状态机。
</p>
<p>
<img title="Linux 中断子系统:中断处理知识点大全" alt="Linux 中断子系统:中断处理知识点大全" border="0" src="https://zhuji.jb51.net/uploads/img/202305/3d5be3dafdb7414e0ca5eaee0f2ccff3.jpg"></p>
<p>
Inactive :中断未激活(未发生)。
</p>
<p>
Pending:中断到达 GIC ,等待 CPU 的处理。
</p>
<p>
Active:中断得到 CPU 的应答,中断被CPU处理。
</p>
<p>
Active and pending :某个中断正在被 CPU 处理,这时候该中断又来了。
</p>
<p>
来看一个例子:
</p>
<p>
<img title="Linux 中断子系统:中断处理知识点大全" alt="Linux 中断子系统:中断处理知识点大全" border="0" src="https://zhuji.jb51.net/uploads/img/202305/e9900433860eca5238b21aa8fd5f5943.jpg"></p>
<p>
(a)N 和 M 用来标识两个外设中断,N 的优先级大于 M
</p>
<p>
(b)两个中断都是 SPI 类型,level trigger,active-high
</p>
<p>
(c)两个中断被配置为去同一个 CPU
</p>
<p>
(d)都被配置成 group 0,通过 FIQ 触发中断
</p>
<table cellspacing="1" cellpadding="1" width="600" border="1"><tbody>
<tr>
<td>
时刻
</td>
<td>
事件
</td>
</tr>
<tr>
<td>
T0时刻
</td>
<td>
Distributor检测到M这个interrupt source的有效触发电平
</td>
</tr>
<tr>
<td>
T2时刻
</td>
<td>
Distributor将M这个interrupt source的状态设定为pending
</td>
</tr>
<tr>
<td>
T17时刻
</td>
<td>
大约15个clock之后,CPU interface拉低nFIQCPU信号线,向CPU报告M外设的中断请求。这时候,CPU interface的ack寄存器(GICC_IAR)的内容会修改成M interrupt source对应的ID
</td>
</tr>
<tr>
<td>
T42时刻
</td>
<td>
Distributor检测到N这个优先级更高的interrupt source的触发事件
</td>
</tr>
<tr>
<td>
T43时刻
</td>
<td>
Distributor将N这个interrupt source的状态设定为pending。同时,由于N的优先级更高,因此Distributor会标记当前优先级最高的中断
</td>
</tr>
<tr>
<td>
T58时刻
</td>
<td>
大约15个clock之后,CPU interface拉低nFIQCPU信号线,向CPU报告N外设的中断请求。当然,由于T17时刻已经assert CPU了,因此实际的电平信号仍然保持asserted。这时候,CPU interface的ack寄存器(GICC_IAR)的内容会被更新成N interrupt source的ID
</td>
</tr>
<tr>
<td>
T61时刻
</td>
<td>
软件通过读取 ack 寄存器的内容,获取了当前优先级最高的,并且状态是pending的interrupt ID(也就是N interrupt source对应的ID),通过读该寄存器,CPU也就ack了该interrupt source N。这时候,Distributor将N这个interrupt source的状态设定为pending and active(因为是电平触发,只要外部仍然有asserted的电平信号,那么一定就是pending的,而该中断是正在被CPU处理的中断,因此状态是pending and active)注意:T61标识CPU开始服务该中断
</td>
</tr>
<tr>
<td>
T64时刻
</td>
<td>
3个clock之后,由于CPU已经ack了中断,因此GIC中CPU interface模块 deassert nFIQCPU信号线,解除发向该CPU的中断请求
</td>
</tr>
<tr>
<td>
T126时刻
</td>
<td>
由于中断服务程序操作了N外设的控制寄存器(ack外设的中断),因此N外设deassert了其interrupt request signal
</td>
</tr>
<tr>
<td>
T128时刻
</td>
<td>
Distributor解除N外设的pending状态,因此N这个interrupt source的状态设定为active
</td>
</tr>
<tr>
<td>
T131时刻
</td>
<td>
软件操作End of Interrupt寄存器(向GICC_EOIR寄存器写入N对应的interrupt ID),标识中断处理结束。Distributor将N这个interrupt source的状态修改为idle,注意:T61~T131是CPU服务N外设中断的的时间区域,这个期间,如果有高优先级的中断pending,会发生中断的抢占(硬件意义的),这时候CPU interface会向CPU assert 新的中断。
</td>
</tr>
<tr>
<td>
T146时刻
</td>
<td>
大约15个clock之后,Distributor向CPU interface报告当前pending且优先级最高的interrupt source,也就是M了。漫长的pending之后,M终于迎来了春天。CPU interface拉低nFIQCPU信号线,向CPU报告M外设的中断请求。这时候,CPU interface的ack寄存器(GICC_IAR)的内容会修改成M interrupt source对应的ID
</td>
</tr>
<tr>
<td>
T211时刻
</td>
<td>
CPU ack M中断(通过读GICC_IAR寄存器),开始处理低优先级的中断
</td>
</tr>
</tbody></table>
<p class="maodian"></p><h3>
Linux 抢占机制
</h3>
<p>
GIC 中断控制器支持中断优先级抢占,一个高优先级中断可以抢占一个低优先级且处于active状态的中断,即GIC仲裁单元会记录和比较当前优先级最高的pending状态,然后去抢占当前中断,并且发送这个最高优先级的中断请求给CPU。
</p>
<p>
从GIC角度看,GIC 会发送高优先级中断请求给CPU。但是CPU不一定响应!!!因为在中断处理过程中,CPU处于关中断状态(关闭本CPU),需要等低优先级中断处理完毕,直到发送 EOI 给GIC,然后CPU才会响应pending状态中优先级最高的中断进行处理。所以 Linux 下:
</p>
<p>
1、高优先级中断无法抢占正在执行的低优先级中断。
</p>
<p>
2、同处于 pending 状态的中断,优先响应高优先级中断进行处理。
</p>
<p>
3、同优先级同是 pending 状态的中断,选择硬件中断号 ID 最小的一个发给CPU。
</p>
<p>
这样是可以理解的,如果万一中断大量爆发,中断如果允许嵌套的话,栈会越来越大,会爆掉,所以为了防止这种情况发生,Linux中中断不允许嵌套,单CPU中,在一个中断处理完之前,不会相应另外一个中断,哪怕优先级比它高。
</p>
<p>
FreeRTOS 中是允许高优先级中断抢占正在执行的低优先级中断,不同系统设定不一样。
</p>
<p class="maodian"></p><h3>
中断与进程
</h3>
<p>
进程调度是一个复杂的机制, 根据需求的不同,在不同时刻会切换调度机制,CPU会根据进程优先级、时间片等信息,对不同进程进行调度。
</p>
<p>
中断可以打断进程的运行,任意一个中断的优先级都比所有的进程高。
</p>
<p>
在中断处理过程中,主要是 GIC 和 CPU 的交互,即便 GIC 支持高优先级中断抢占正在执行的低优先级中断,发信号给 CPU core,但是 CPU core 可以不处理,因为 Linux 中当 CPU core 执行中断处理时,是关中断和关抢占的状态,不再相应中断信号。
</p>
<p>
也就意味着,在中断优先级这个概念中,只有当 GIC 同时存在多个 pending 的中断,这时候会选择优先级最高的去执行,高优先级会抢占低优先级中断(哪怕低优先级先来)。如果低优先级中断处于 active 状态,是不可以被抢占的,这是前后关系。抢占只存在于同时是pending 状态的时候。
</p>
<p class="maodian"></p><h3>
Linux 为什么中断不允许休眠?
</h3>
<p>
所谓的睡眠,就是调用 schedule 让出 CPU,调度器选择另外个进程继续执行,这个过程涉及进程栈空间的切换。
</p>
<p>
1、假如中断上下文中调用 schedule ,此时获取的 struct thread info 数据结构是发生中断时该进程栈信息,而不是中断上下文调用 schedule 时任何信息。这就导致再也无法返回中断上下文中调用 schedule 的地方。
</p>
<p>
2、中断上下文处于关中断中,需要发送个 EOI 通知 GIC 中断处理结束,GIC 和CPUinterface 才会进入下一次中断处理。如果中途 schedule,那么整个系统的中断都会被屏蔽掉。
</p>
<p>
一般进入中断后,需要关中断,也会关抢占,同时注意不可以调用schedule。
</p>
<p class="maodian"></p><h3>
unhandled interrupt 和 spurious interrupt
</h3>
<p>
未处理中断和虚假中断
</p>
<p>
在中断处理的最后,总会有一段代码如下:
</p>
<ol class="dp-sql">
<li class="alt">
<span><span>irqreturn_t </span></span>
</li>
<li>
<span>handle_irq_event_percpu(struct irq_desc *<span class="keyword">desc</span><span>, struct irqaction *</span><span class="keyword">action</span><span>) </span></span>
</li>
<li class="alt">
<span>{ </span>
</li>
<li>
<span></span>
</li>
<li class="alt">
<span>…… </span>
</li>
<li>
<span></span>
</li>
<li class="alt">
<span>if (!noirqdebug) </span>
</li>
<li>
<span>note_interrupt(irq, <span class="keyword">desc</span><span>, retval); </span></span>
</li>
<li class="alt">
<span><span class="keyword">return</span><span> retval; </span></span>
</li>
<li>
<span>} </span>
</li>
</ol>
<p>
note_interrupt就是进行unhandled interrupt和spurious interrupt处理的。对于这类中断,linux kernel有一套复杂的机制来处理,你可以通过command line参数(noirqdebug)来控制开关该功能。
</p>
<p>
当发生了一个中断,但是没有被处理(有两种可能,一种是根本没有注册的 specific handler,第二种是有 handler,但是 handler 否认是自己对应的设备触发的中断),怎么办?毫无疑问这是一个异常状况,那么 kernel 是否要立刻采取措施将该 IRQ disable 呢?也不太合适,毕竟 interrupt request 信号线是允许共享的,直接 disable 该 IRQ 有可能会下手太狠,kernel 采取了这样的策略:如果该 IRQ 触发了 100,000 次,但是 99,900 次没有处理,在这种条件下,我们就是 disable 这个 interrupt request line。
</p>
<p>
中断线和中断号是一个意思。
</p>
<p>
相关的控制数据在中断描述符中,如下:
</p>
<ol class="dp-sql">
<li class="alt">
<span><span>struct irq_desc { </span></span>
</li>
<li>
<span>…… </span>
</li>
<li class="alt">
<span>unsigned <span class="keyword">int</span><span> irq_count;--------记录发生的中断的次数,每100,000则回滚 </span></span>
</li>
<li>
<span>unsigned long last_unhandled;-----上一次没有处理的IRQ的时间点 </span>
</li>
<li class="alt">
<span>unsigned <span class="keyword">int</span><span> irqs_unhandled;------没有处理的次数 </span></span>
</li>
<li>
<span>…… </span>
</li>
<li class="alt">
<span>} </span>
</li>
</ol>
<p class="maodian"></p><h3>
中断的生命周期
</h3>
<p>
<img title="Linux 中断子系统:中断处理知识点大全" alt="Linux 中断子系统:中断处理知识点大全" border="0" src="https://zhuji.jb51.net/uploads/img/202305/51cbb0d10aba351158d2c7cab3c13b0c.jpg"></p>
<p>
原文链接:https://mp.weixin.qq.com/s/F0yVudljVSaBGCIYFhGpMA
</p>
頁:
[1]