什么是中断???
当 CPU 正在处理内部数据时,外界发生了紧急情况,要求 CPU 暂停当前
的工作转去处理这个 异步事件
。处理完毕后,再回到原来被中断的地址
,继续原来的工作,这样的过程称为中断。实现这一功能的系统称为 中断系统
,申请 CPU 中断的请求源称为 中断源
。中断是一种异常
,异常是导致处理器脱离正常运行转向执行特殊代码的任何事件,如果不及时
进行处理,轻则系统出错
,重则会导致系统毁灭性地瘫痪
。所以正确地处理异常,避免错误的发生是提高软件鲁棒性(稳定性)非常重要的一环。如下图是一个简单的中断示意图。
中断能打断线程的运行,无论该线程具有什么样的优先级,因此中断一般用于处理比较紧急的事件,而且只做简单处理
,例如标记该事件,在使用 RT-Thread系统时,一般建议使用 信号量、消息或事件标志组 等标志中断的发生,将这些内核对象发布给处理线程,处理线程再做具体处理。
通过中断机制,在外设不需要CPU介入时,CPU可以执行其它线程,而当外设需要CPU时通过产生中断信号使CPU立即停止当前线程转而来响应中断请求。这样可以使CPU避免把大量时间耗费在等待、查询外设状态的操作上,因此将大大提高系统实时性以及执行效率。
RT-Thread源码中有许多处临界段的地方, 临界段虽然保护了关键代码的执行不被打断, 但也会影响系统的实时,任何使用了操作系统的中断响应都不会比裸机快。比如,某个时候有一个线程在运行中,并且该线程部分程序将中断屏蔽掉,也就是进入临界段中,这个时候如果有一个紧急的中断事件被触发,这个中断就会被挂起,不能得到及时响应,必须等到中断开启才可以得到响应, 如果屏蔽中断时间超过了紧急中断能够容忍的限度,危害是可想而知的。所以,操作系统的中断在某些时候会有适当的中断延迟,因此调用中断屏蔽函数进入临界段的时候,也需快进快出
。
中断介绍
与中断相关的硬件可以划分为三类:外设、中断控制器、CPU本身。
外设
:当外设需要请求CPU时,产生一个中断信号,该信号连接至中断控制器。中断控制器
:中断控制器是CPU众多外设中的一个,它一方面接收其它外设中断信号的输入,另一方面,它会发出中断信号给CPU。可以通过对中断控制器编程实现对中断源的优先级、触发方式、打开和关闭源等设置操作。在Cortex-M系列控制器中常用的中断控制器是NVIC(内嵌向量中断控制器Nested Vectored Interrupt Controller)。CPU
:CPU会响应中断源的请求,中断当前正在执行的线程,转而执行中断处理程序。NVIC最多支持240个中断,每个中断最多256个优先级。
中断相关名词
中断号
:每个中断请求信号都会有特定的标志,使得计算机能够判断是哪个设备提出的中断请求,这个标志就是中断号。中断请求
:“紧急事件”需向CPU提出申请,要求CPU暂停当前执行的线程,转而处理该“紧急事件”,这一申请过程称为中断请求。中断优先级
:为使系统能够及时响应并处理所有中断,系统根据中断时间的重要性和紧迫程度,将中断源分为若干个级别,称作中断优先级。中断处理程序
:当外设产生中断请求后,CPU暂停当前的线程,转而响应中断申请,即执行中断处理程序。中断触发
:中断源发出并送给CPU控制信号,将中断触发器置“1”,表明该中断源产生了中断,要求CPU去响应该中断,CPU暂停当前线程,执行相应的中断处理程序。中断触发类型
:外部中断申请通过一个物理信号发送到NVIC,可以是电平触发或边沿触发。中断向量
:中断服务程序的入口地址。中断向量表
:存储中断向量的存储区,中断向量与中断号对应,中断向量在中断向量表中按照中断号顺序存储。临界段
:代码的临界段也称为临界区,一旦这部分代码开始执行,则不允许任何中断打断。为确保临界段代码的执行不被中断,在进入临界段之前须关中断,而临界段代码执行完毕后,要立即开中断。RT-Thread支持中断屏蔽和中断使能。
中断管理的运作机制
当中断产生时,处理机将按如下的顺序执行:
1、保存当前处理机状态信息
2、载入异常或中断处理函数到PC寄存器
3、把控制权转交给处理函数并开始执行
4、当处理函数执行完成时,恢复处理器状态信息
5、从异常或中断中返回到前一个程序执行点
中断发生的环境有两种情况:在线程的上下文
中,在中断服务函数处理上下文
中。
- 线程在工作的时候,如果此时发生了一个中断,无论中断的优先级是多大,都会打断当前线程的执行,从而转到对应的中断服务函数中执行。
- 在执行中断服务例程的过程中,如果有更高优先级别的中断源触发中断,由于当前处于中断处理上下文环境中,根据不同的处理器构架可能有不同的处理方式,比如新的中断等待挂起直到当前中断处理离开后再行响应;或新的高优先级中断打断当前中断处理过程,而去直接响应这个更高优先级的新中断源。后面这种情况,称之为
中断嵌套
。在硬实时环境中,前一种情况是不允许发生的,不能使响应中断的时间尽量的短。而在软件处理(软实时环境)上,RT-Thread允许中断嵌套
,即在一个中断服务例程期间,处理器可以响应另外一个优先级更高的中断。
中断延迟
即使操作系统的响应很快了,但对于中断的处理仍然存在着中断延迟响应的问题,我们称之为中断延迟
(Interrupt Latency) 。
中断延迟是指从硬件中断发生到开始执行中断处理程序第一条指令之间的这段时间
。也就是:系统接收到中断信号到操作系统作出响应,并完成换到转入中断服务程序的时间。也可以简单地理解为:(外部)硬件(设备)发生中断,到系统执行中断服务子程序(ISR)的第一条指令的时间
。
中断的处理过程是:外界硬件发生了中断后,CPU到中断处理器读取中断向量
,并且查找中断向量表,找到对应的中断服务子程序
(ISR)的首地址,然后跳转
到对应的ISR去做相应处理。这部分时间,我称之为:识别中断时间。
在允许中断嵌套的实时操作系统中,中断也是基于优先级的,允许高优先级
中断抢断
正在处理的低优先级
中断,所以,如果当前正在处理更高优先级的中断,即使此时有低优先级的中断,也系统不会
立刻响应,而是等到高优先级的中断处理完之后,才会响应。而即使在不支持中断嵌套,即中断是没有优先级
的,中断是不允许被中断
的,所以,如果当前系统正在处理一个中断,而此时另一个中断到来了,系统也是不会立即响应的,而只是等处理完当前的中断之后,才会处理后来的中断。此部分时间,我称其为:等待中断打开时间。
在操作系统中,很多时候我们会主动进入临界段,系统不允许当前状态被中断打断,故而在临界区发生的中断会被挂起,直到退出临界段时候打开中断。此部分时间,我称其为:关闭中断时间。
中断延迟可以定义为,从中断开始的时刻到中断服务例程开始执行的时刻之间的时间段。中断延迟 = 识别中断时间 + [等待中断打开时间] + [关闭中断时间]。
注意:“[ ]”的时间是不一定都存在的,此处为最大可能的中断延迟时间。
中断处理过程
RT-Thread 中断管理中,将中断处理程序分为中断前导程序、用户中断服务程序、中断后续程序三部分,如下图:
中断前导程序
1、保存 CPU 中断现场,这部分跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。
对于 Cortex-M 来说,该工作由硬件自动完成。当一个中断触发并且系统进行响应时,处理器硬件会将当前运行部分的上下文寄存器自动压入中断栈中,这部分的寄存器包括
PSR、PC、LR、R12、R3-R0
寄存器。
2、通知内核进入中断状态,调用 rt_interrupt_enter() 函数,作用是把全局变量 rt_interrupt_nest 加 1,用它来记录中断嵌套的层数,代码如下所示。
void rt_interrupt_enter(void)
{
rt_base_t level;
level = rt_hw_interrupt_disable();
rt_interrupt_nest ++;
rt_hw_interrupt_enable(level);
}
用户中断服务程序
在用户中断服务程序(ISR)中,分为两种情况,第一种情况是不进行线程切换,这种情况下用户中断服务程序和中断后续程序运行完毕后退出中断模式,返回被中断的线程。另一种情况是,在中断处理过程中需要进行线程切换,这种情况会调用rt_hw_context_switch_interrupt()
函数进行上下文切换,该函数跟 CPU 架构相关,不同 CPU 架构的实现方式有差异。
在 Cortex-M 架构中,rt_hw_context_switch_interrupt() 的函数实现流程如下图所示,它将设置需要切换的线程 rt_interrupt_to_thread 变量,然后触发 PendSV 异常(PendSV 异常是专门用来辅助上下文切换的,且被初始化为最低优先级的异常)。PendSV 异常被触发后,不会立即进行 PendSV 异常中断处理程序,因为此时还在中断处理中,只有当中断后续程序运行完毕,真正退出中断处理后,才进入 PendSV 异常中断处理程序。
中断后续程序
1、通知内核离开中断状态,通过调用 rt_interrupt_leave() 函数,将全局变量 rt_interrupt_nest 减 1,代码如下所示。
void rt_interrupt_leave(void)
{
rt_base_t level;
level = rt_hw_interrupt_disable();
rt_interrupt_nest --;
rt_hw_interrupt_enable(level);
}
2 、恢复中断前的 CPU 上下文,如果在中断处理过程中未进行线程切换,那么恢复 from 线程的 CPU 上下文,如果在中断中进行了线程切换,那么恢复 to 线程的 CPU 上下文。这部分实现跟 CPU 架构相关,不同 CPU 架构的实现方式有差异,在 Cortex-M 架构中实现流程如下图所示。
中断栈
在中断处理过程中,在系统响应中断前,软件代码(或处理器)需要把当前线程的上下文保存下来(通常保存在当前线程的线程栈中)
,再调用中断服务程序进行中断响应、处理。在进行中断处理时(实质是调用用户的中断服务程序函数),中断处理函数中很可能会有自己的局部变量,这些都需要相应的栈空间来保存,所以中断响应依然需要一个栈空间来做为上下文,运行中断处理函数。中断栈可以保存在打断线程的栈
中,当从中断中退出时,返回相应的线程继续执行。
中断栈也可以与线程栈完全分离开来,即每次进入中断时,在保存完打断线程上下文后,切换到新的中断栈中独立运行。在中断退出时,再做相应的上下文恢复。使用独立中断栈相对来说更容易实现,并且对于线程栈使用情况也比较容易了解和掌握(否则必须要为中断栈预留空间,如果系统支持中断嵌套,还需要考虑应该为嵌套中断预留多大的空间)。
RT-Thread 采用的方式是提供独立的中断栈
,即中断发生时,中断的前期处理程序会将用户的栈指针更换到系统事先留出的中断栈空间中,等中断退出时再恢复用户的栈指针。这样中断就不会占用线程的栈空间
,从而提高了内存空间的利用率,且随着线程的增加,这种减少内存占用的效果也越明显。
在 Cortex-M 处理器内核里有两个堆栈指针,一个是主堆栈指针(MSP),是默认的堆栈指针,在运行第一个线程之前和在中断和异常服务程序里使用;另一个是线程堆栈指针(PSP),在线程里使用。在中断和异常服务程序退出时,修改 LR 寄存器的第 2 位的值为 1,线程的 SP 就由 MSP 切换到 PSP。
本节笔记参考于:野火-《RT-Thread内核实现与应用开发实战》
RT-Thread官方文档:中断管理