Linux 中断子系统:中断处理知识点大全

时间:2021-08-24 15:50:13

Linux 中断子系统:中断处理知识点大全

Linux 中断相关节点

/proc/interrupts

cat 这个节点,会打印系统中所有的中断信息,如果是多核CPU,每个核都会打印出来。

包括每个中断的名字、中断号 IRQ number、每个中断的触发次数、在哪个CPU核处理的、是边沿触发还是电平触发,属于哪个中断控制器,都会打印出来。

/proc/irq/…

进入这个目录。会看到以中断号命名的文件夹,每个中断号文件夹下面都有几个节点,存储了这个中断的信息,比如 smp_affinity、affinity_hint、spurious等。smp_affinity 代表中断号核CPU之间的亲缘绑定关系,也就是如果某个中断号绑定了一个CPU核,那么这个中断就会一直在这个CPU上处理。

如何让某个中断在某个特定的 CPU 处理?

kernel 2.4 以后的版本才支持把不同的硬件中断请求(IRQs)分配到特定的 CPU 上,这个绑定技术被称为 SMP IRQ Affinity. 更多介绍请参看 Linux 内核源代码自带的文档:linux-4.14/Documentation/IRQ-affinity.txt

/proc/irq/{IRQ}/smp_affinity

/proc/irq/{IRQ}/smp_affinity_list

/proc/irq/{IRQ}/smp_affinity 指定给定的 irq 中断号源允许哪些CPU执行,它是一个掩码位,比如是ff,代表11111111,表示这个中夺冠可以在 8 个 CPU 执行,具体在哪一个 CPU 执行,靠分配器分配。

如果这个 /proc/irq/{IRQ}/smp_affinity 指定为 00000001,代表这个IRQ只能在最后一个CPU核进行处理,其他CPU不允许处理,大家可以测试一下,博主测试是 OK 的(GIC支持,其他中断控制器不一定)。

串口手动赋值的重启以后会消失,可以在代码中调用 irq_set_affinity 函数,指定中断的掩码,来达到某个中断被固定CPU处理的需求。

中断分发机制

对于 GIC-V2 而言,SPI 的分发是根据 Distributor 中的 Interrupt Processor Targets Registers 来决定的。对于任何一个 SPI,其都有在某个 GICD_ITARGETSRn 寄存器中有 8 个bit标识送达的 processor,如果只有一个 bit 被 set,那么就很简单了,如果该中断是当前优先级最高的中断,那么 Distributor 就会送到对应的 CPU interface,该中断最终会送达指定的 CPU。

如果该中断对应的 Interrupt Processor Targets Registers 中的那 8 个 bit 有多个 bit 被 set 的话,Distributor 如何处理呢?“依次轮着把产生的中断给各个 CPU,还是说看哪个CPU有空就给哪个CPU来着”,让硬件处理这么复杂的逻辑有些不合适,实际上,GIC 的硬件是不会进行任何判断的,也不会集成任何的算法,它就是根据Interrupt Processor Targets Registers的bit设定情况,忠实的把中断送往指定的一个processor或者多个processors。

大家可以去看看 gic_set_affinity 这个函数,这个函数确保一个中断的 Interrupt Processor Targets Registers 中的那8个bit只有一个bit被设定。

/kernel5.15/drivers/irqchip/irq-gic-v3.c

Linux 中断子系统:中断处理知识点大全

Linux 中断子系统:中断处理知识点大全

在 1244 和 1246 行,1246 行就是在 online 的 CPU 中选中一个,1263 行写入到寄存器中,GIC 会读取这个寄存器,是哪个 CPU,然后将中断发给这个CPU。中间的函数很简单,大家可以自己看。

中断状态机

对于 GIC-V2 而言,中断的状态机由 Distributor 维护,每个中断都有一个状态机。

Linux 中断子系统:中断处理知识点大全

Inactive :中断未激活(未发生)。

Pending:中断到达 GIC ,等待 CPU 的处理。

Active:中断得到 CPU 的应答,中断被CPU处理。

Active and pending :某个中断正在被 CPU 处理,这时候该中断又来了。

来看一个例子:

Linux 中断子系统:中断处理知识点大全

(a)N 和 M 用来标识两个外设中断,N 的优先级大于 M

(b)两个中断都是 SPI 类型,level trigger,active-high

(c)两个中断被配置为去同一个 CPU

(d)都被配置成 group 0,通过 FIQ 触发中断

时刻 事件
T0时刻 Distributor检测到M这个interrupt source的有效触发电平
T2时刻 Distributor将M这个interrupt source的状态设定为pending
T17时刻 大约15个clock之后,CPU interface拉低nFIQCPU信号线,向CPU报告M外设的中断请求。这时候,CPU interface的ack寄存器(GICC_IAR)的内容会修改成M interrupt source对应的ID
T42时刻 Distributor检测到N这个优先级更高的interrupt source的触发事件
T43时刻 Distributor将N这个interrupt source的状态设定为pending。同时,由于N的优先级更高,因此Distributor会标记当前优先级最高的中断
T58时刻 大约15个clock之后,CPU interface拉低nFIQCPU信号线,向CPU报告N外设的中断请求。当然,由于T17时刻已经assert CPU了,因此实际的电平信号仍然保持asserted。这时候,CPU interface的ack寄存器(GICC_IAR)的内容会被更新成N interrupt source的ID
T61时刻 软件通过读取 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开始服务该中断
T64时刻 3个clock之后,由于CPU已经ack了中断,因此GIC中CPU interface模块 deassert nFIQCPU信号线,解除发向该CPU的中断请求
T126时刻 由于中断服务程序操作了N外设的控制寄存器(ack外设的中断),因此N外设deassert了其interrupt request signal
T128时刻 Distributor解除N外设的pending状态,因此N这个interrupt source的状态设定为active
T131时刻 软件操作End of Interrupt寄存器(向GICC_EOIR寄存器写入N对应的interrupt ID),标识中断处理结束。Distributor将N这个interrupt source的状态修改为idle,注意:T61~T131是CPU服务N外设中断的的时间区域,这个期间,如果有高优先级的中断pending,会发生中断的抢占(硬件意义的),这时候CPU interface会向CPU assert 新的中断。
T146时刻 大约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
T211时刻 CPU ack M中断(通过读GICC_IAR寄存器),开始处理低优先级的中断

Linux 抢占机制

GIC 中断控制器支持中断优先级抢占,一个高优先级中断可以抢占一个低优先级且处于active状态的中断,即GIC仲裁单元会记录和比较当前优先级最高的pending状态,然后去抢占当前中断,并且发送这个最高优先级的中断请求给CPU。

从GIC角度看,GIC 会发送高优先级中断请求给CPU。但是CPU不一定响应!!!因为在中断处理过程中,CPU处于关中断状态(关闭本CPU),需要等低优先级中断处理完毕,直到发送 EOI 给GIC,然后CPU才会响应pending状态中优先级最高的中断进行处理。所以 Linux 下:

1、高优先级中断无法抢占正在执行的低优先级中断。

2、同处于 pending 状态的中断,优先响应高优先级中断进行处理。

3、同优先级同是 pending 状态的中断,选择硬件中断号 ID 最小的一个发给CPU。

这样是可以理解的,如果万一中断大量爆发,中断如果允许嵌套的话,栈会越来越大,会爆掉,所以为了防止这种情况发生,Linux中中断不允许嵌套,单CPU中,在一个中断处理完之前,不会相应另外一个中断,哪怕优先级比它高。

FreeRTOS 中是允许高优先级中断抢占正在执行的低优先级中断,不同系统设定不一样。

中断与进程

进程调度是一个复杂的机制, 根据需求的不同,在不同时刻会切换调度机制,CPU会根据进程优先级、时间片等信息,对不同进程进行调度。

中断可以打断进程的运行,任意一个中断的优先级都比所有的进程高。

在中断处理过程中,主要是 GIC 和 CPU 的交互,即便 GIC 支持高优先级中断抢占正在执行的低优先级中断,发信号给 CPU core,但是 CPU core 可以不处理,因为 Linux 中当 CPU core 执行中断处理时,是关中断和关抢占的状态,不再相应中断信号。

也就意味着,在中断优先级这个概念中,只有当 GIC 同时存在多个 pending 的中断,这时候会选择优先级最高的去执行,高优先级会抢占低优先级中断(哪怕低优先级先来)。如果低优先级中断处于 active 状态,是不可以被抢占的,这是前后关系。抢占只存在于同时是pending 状态的时候。

Linux 为什么中断不允许休眠?

所谓的睡眠,就是调用 schedule 让出 CPU,调度器选择另外个进程继续执行,这个过程涉及进程栈空间的切换。

1、假如中断上下文中调用 schedule ,此时获取的 struct thread info 数据结构是发生中断时该进程栈信息,而不是中断上下文调用 schedule 时任何信息。这就导致再也无法返回中断上下文中调用 schedule 的地方。

2、中断上下文处于关中断中,需要发送个 EOI 通知 GIC 中断处理结束,GIC 和CPUinterface 才会进入下一次中断处理。如果中途 schedule,那么整个系统的中断都会被屏蔽掉。

一般进入中断后,需要关中断,也会关抢占,同时注意不可以调用schedule。

unhandled interrupt 和 spurious interrupt

未处理中断和虚假中断

在中断处理的最后,总会有一段代码如下:

  1. irqreturn_t
  2. handle_irq_event_percpu(struct irq_desc *desc, struct irqaction *action)
  3. {
  4. ……
  5. if (!noirqdebug)
  6. note_interrupt(irq, desc, retval);
  7. return retval;
  8. }

note_interrupt就是进行unhandled interrupt和spurious interrupt处理的。对于这类中断,linux kernel有一套复杂的机制来处理,你可以通过command line参数(noirqdebug)来控制开关该功能。

当发生了一个中断,但是没有被处理(有两种可能,一种是根本没有注册的 specific handler,第二种是有 handler,但是 handler 否认是自己对应的设备触发的中断),怎么办?毫无疑问这是一个异常状况,那么 kernel 是否要立刻采取措施将该 IRQ disable 呢?也不太合适,毕竟 interrupt request 信号线是允许共享的,直接 disable 该 IRQ 有可能会下手太狠,kernel 采取了这样的策略:如果该 IRQ 触发了 100,000 次,但是 99,900 次没有处理,在这种条件下,我们就是 disable 这个 interrupt request line。

中断线和中断号是一个意思。

相关的控制数据在中断描述符中,如下:

  1. struct irq_desc {
  2. ……
  3. unsigned int irq_count;--------记录发生的中断的次数,每100,000则回滚
  4. unsigned long last_unhandled;-----上一次没有处理的IRQ的时间点
  5. unsigned int irqs_unhandled;------没有处理的次数
  6. ……
  7. }

中断的生命周期

Linux 中断子系统:中断处理知识点大全

原文链接:https://mp.weixin.qq.com/s/F0yVudljVSaBGCIYFhGpMA