从进入 IRQ/FIQ 中断向量开始,中断处理的完整流程:
- 保存上下文
- 切换中断栈,为进入“真正的”中断服务程序做准备
- 执行真正的中断服务程序
- 恢复之前的上下文
“真正的”中断服务程序
“真正的”意为不算那些对于所有异常、中断来说都相同的“套话” ,只讨论对于中断特有的行为。
承认一个中断
真正的中断服务程序从接受 CPU Interface 传来的中断
开始算起,这一步的实现通过读取ICC_IAR1_EL1
, 返回当前
中断的 INTID。
拿到 INTID 后,就根据不同的 ID 调用各自对应场景下的服务函数, 比如若 INTID 是对应与时钟中断,那么此步需要清楚状态寄存器、 重新开启时钟定时器。
标记中断处理完毕
做完相干的事情后,需要将该中断标记为已完成,方便后面的中断进来, 也就是上一节说的优先级下降和中断失效过程。
GICv3 支持将这两步合为一次操作,实际我们也是这样做的,通过写入
ICC_EOI1_EL1
寄存器来完成标记处理完成。此中断的状态也就从
active->inactive.
中断服务程序中,承认中断和标记完成两步操作应该是用 while 循环 包裹起来的。
反复的读取 IAR、标记中断已完成… 如果此时该 CPU 上已经没有 中断待处理了,读取 IAR 会返回特殊 INTID: 1023
中断的上下部机制
中断服务函数的停留时间应该越短越好,否则影响其他任务占用 CPU,这是老生常谈的。
以上观点存在的原因是:中断服务函数中是关闭中断的,CPU 只有串行的处理完当前 中断后, 才能继续做下一件事情,即便是高优先级任务也得等待,因为时钟中断被关闭!
所以 Linux 在 2.6 引入了中断的上下部机制,将整个中断服务函数拆分为上部和下部:
- 上部:那些不能被打断的步骤,比如保存上下文,承认和标记中断完成等
- 下部:宽松的管理方式,执行过程就算被打断也没关系,指的就是上面说的对应各自中断 应用场景下的服务函数,比如一个按键触发代表的实际行为
ARMv8 如何支持中断上下部
ARMv8 中,进入异常向量是自动关中断的,可执行msr DAIFClr, #imm
来手动开启。
所以说,直到手动开中断之前的所有操作都属于中断的上部。
那么,应该在何时开启中断呢?我认为分割后的正确中断处理流程应该是:
- 承认一个中断
- 根据 INTID 标记其应该做的行为,注意只是标记
- 标记该中断完成
- 待该 CPU 上的所有中断都完成后,开中断
- 遍历检查所有标记,如果有待完成的任务在此时执行
上面说的标记和执行过程可以用许多方式实现,包括 softirq, tasklet, workqueue 等, 都属于实现中断上下部机制的实现。
softIRQ
softirq 定义了一些中断事件和处理函数,在中断的上半部中,如果 INTID 属于定义的软中断 之一, 则添加标志其处理函数需要被执行,只是标志,并不实际执行。
当中断服务程序退出之前,会遍历软中断列表中的状态,如果有需要处理的,则调用注册的处理函数。
注意,软中断处理函数的执行是在中断上下文中,用户进程只能等待中断完成才能有机会被调用, 所以软中断的一个问题是, 如果需要执行的处理函数过多,会导致一般线程长时间不能被调度。
同样地,因为处理函数的执行在中断上下文,所以也不能执行可能导致进程睡眠的操作, 例如申请锁,可能导致优先级反转。