本书的原著为:《Design Patterns for Embedded Systems in C ——An Embedded Software Engineering Toolkit 》,讲解的是嵌入式系统设计模式,是一本不可多得的好书。
本系列描述我对书中内容的理解。本文章描述访问硬件的设计模式之六:中断模式。
物理世界本质上是并发和异步的。
并发
是指在同一时刻有多个活动(通常是任务或进程)正在发生或进行。在单线程环境中,并发通常是通过时间片轮转等机制模拟出来的,而在多线程或多核处理器环境中,多个任务可以同时进行。
并发编程的目标是最大限度地利用计算资源,提高程序的响应性和吞吐量。为了实现并发,开发者通常会使用多线程、多进程、异步I/O等技术。异步操作
是指程序的某个部分可以在不等待其他部分完成的情况下继续执行。在异步编程中,任务的执行不会阻塞当前线程或进程,而是会立即返回并在后台继续执行。当任务完成时,通常会通过回调函数、事件、Promise等方式通知程序的其他部分。
异步编程的主要好处是提高程序的响应性和性能,因为它允许程序在等待资源(如I/O操作)时继续执行其他任务。
在嵌入式系统和低级系统编程中,中断
是一种强大的机制,用于响应和处理外部事件或内部状态变化。中断处理程序是与特定中断类型相关联的代码段,当该类型的中断发生时,处理器会自动跳转到该代码段执行。由于中断可以打断正在执行的程序,因此它们提供了一种并发和异步处理事件的方式。这对于实时系统和嵌入式系统来说尤为重要,因为这些系统需要能够及时响应和处理外部事件,以确保系统的正确性和可靠性。此外,中断处理程序还可以用于实现诸如定时器、串行通信、输入/输出等操作,从而提高了系统的功能和性能。
摘要
中断模式在嵌入式系统和实时系统中非常常见,因为这些系统需要能够及时响应和处理外部事件。为了实现中断模式,开发人员需要利用处理器和编译器提供的中断服务例程(ISR)机制。ISR是与特定中断源相关联的代码段,当中断发生时,处理器会自动跳转到该代码段执行。在ISR中,开发人员可以编写用于处理中断事件的逻辑代码。处理完中断事件后,处理器会返回到被中断的程序继续执行。
问题
中断模式是一种强大且有效的机制,用于处理紧急和高频事件。与轮询模式相比,中断模式能够确保及时响应和处理这些事件,从而提高系统的实时性和可靠性。在中断模式中,当紧急事件发生时,处理器会立即暂停当前任务,跳转到与该事件相关联的中断服务例程(ISR)进行处理。处理完成后,处理器会返回到被中断的任务继续执行。这种机制确保了紧急事件能够得到优先处理,而不会被其他低优先级的任务所阻塞。
模式结构
模式结构图如下所示:
模式详情
中断处理
中断向量是一个指向中断服务例程(ISR)的指针或地址,当中断发生时,系统会查找对应的中断向量,并跳转到该向量指向的中断服务例程去执行。因此,通过修改中断向量表,我们可以控制哪个中断服务例程响应特定的中断。
中断处理
的 install()
和 deinstall()
函数提供了一种机制来动态地修改中断向量表。这使得我们可以在运行时安装或卸载中断服务例程,从而灵活地处理不同的中断事件。
每个 handleInterrupt_x()
函数都处理一个特定的中断,并以“从中断返回”(Return From Interrupt,RTI)语句结束。这条语句依赖于编译器和处理器。如前所述,至关重要的是中断服务例程不能有参数,这就是原因。否则当它们尝试返回时,将会从CPU堆栈中弹出错误的值。
中断向量表
InterruptVectorTable
(中断向量表)实际上就是一个包含中断服务例程地址的数组。它位于处理器特定的内存位置。当中断号x发生时,CPU会暂停当前的处理,并间接调用该表中与第x个索引对应的地址。在RTI(从中断返回)指令执行后,CPU会恢复被暂停的任务。
效果
中断处理模式在嵌入式系统、操作系统和实时系统中非常常见,因为这些系统需要快速响应外部事件或内部状态变化。通过中断,系统可以在不等待当前任务完成的情况下,立即处理紧急事件,从而提高系统的响应性和实时性。
然而,由于中断会打断正常的程序流程,因此在使用中断时需要特别小心。如果中断服务例程执行时间过长,或者频繁触发中断,可能会导致系统性能下降或任务延迟。通常,当中断服务例程正在执行时,会禁用中断(不允许中断嵌套);这意味着中断服务例程必须执行得非常快,以确保不会错过其他中断。
由于中断服务例程(ISR)必须简短,因此如果在中断服务例程中调用其它系统服务时必须小心。例如,为了共享由中断发出的数据,中断服务例程可能需要将数据排入队列并快速返回;在将来的某个时刻,应用软件会在队列中发现这些数据。当数据的实际获取比其处理更为紧急时,这种机制非常有用。通过这种方式,可以将一个长时间的中断处理程序分解成两个部分:紧急部分,通过中断服务例程本身完成;处理部分,通过第二个函数完成,该函数定期检查数据或信号。
- 如果中断服务例程处理时间过长,会延迟系统对其他中断的响应,可能导致实时任务错过截止时间或系统性能下降。这通常发生在中断服务例程执行了不必要的复杂操作,或者等待了某些不可用的资源。
- 实现错误导致中断被禁用也是一个常见问题。当中断被禁用时,系统无法响应外部事件,可能导致重要任务被延迟或错过。这种错误通常是由于程序员对中断管理的不当理解或疏忽造成的。
- 然而,共享资源上的竞争状态和死锁可能是最为棘手的问题。当多个中断服务例程或任务需要访问共享资源时,如果没有采取适当的同步措施,就可能出现竞争状态。这可能导致数据不一致、系统崩溃或其他不可预测的行为。死锁是竞争状态的一种极端情况,它发生在两个或多个中断服务例程或任务相互等待对方释放资源时,导致系统无法继续执行。
为了避免这些问题,需要采取一系列措施。首先,中断服务例程应该尽可能简短和高效,避免执行复杂操作或等待不可用资源。其次,需要确保中断管理代码的正确性,避免由于实现错误导致中断被禁用。最后,对共享资源的访问需要采取适当的同步措施,如使用锁、信号量或互斥量等机制来避免竞争状态和死锁的发生。
竞争状态(Race Condition)和死锁(Deadlock)都是并发编程中常见的问题:
竞争状态
指的是当两个或多个进程在没有适当同步的情况下,访问共享的数据或资源,并且至少有一个进程在修改这些数据或资源时,最终的结果将取决于这些线程或进程的执行顺序。由于进程调度的不确定性,这可能导致不可预测和不可重复的结果。竞争状态通常是由于缺乏适当的同步机制(如互斥锁、信号量等)来确保对共享数据的互斥访问而引起的。死锁
则是指两个或多个进程在等待对方释放资源时陷入无限等待的状态,导致系统无法继续执行。这通常是由于每个进程都持有一个资源并请求另一个进程持有的资源,从而形成了一个循环等待的情况。如果没有外部干预,死锁将导致系统停滞不前,无法完成任何有用的工作。
如果中断服务例程和应用程序服务之间共享变量或数据结构(如队列),则访问这些资源构成了潜在竞争状态和死锁条件,因为你永远无法准确知道何时会执行中断服务例程。
下图展示了一种典型结构,其中中断服务例程与正常应用程序都访问同一共享资源,该共享资源是一个复杂数据结构。当应用程序正在访问共享资源时发生中断,就会出现竞争状态。想象一下,如果应用程序在读取数据的过程中(该读取操作不是原子操作)发生了中断,中断服务例程将会暂停应用程序的读取操作,转而执行中断服务例程,在此期间修改数据,然后返回到应用程序。这样,应用程序将会看到损坏的数据——部分是新的数据,部分是旧的数据。
对于这个问题,有多种解决方案,但它们都涉及到对共享资源的序列化访问。一种方法是在应用程序读取数据之前禁用中断,并在访问完成后重新启用中断。另一种方法是使用互斥信号量来保护数据,如下图所示。
在这张图中,共享资源
是受互斥信号量保护的;当调用 getData()
或 setData()
函数时,会给资源 加锁
。当函数完成时,会给资源 解锁
。如果 中断服务例程
在尝试访问数据时等待信号量锁,就可能发生死锁。由于中断服务例程已经中断了拥有锁的 应用程序
,假如中断服务程序一直等待信号量锁,则应用程序永远没有机会移除锁,因为它得不到运行,无法释放锁。当然,解决方案是中断服务例程不能等待锁。新数据可以被丢弃,或者可以创建两个共享资源,但规定中断服务例程或应用程序一次只能锁定其中一个资源。后一种解决方案有时被称为“双缓冲区”。双缓冲区方案比较复杂,如果你不希望这么做,那么唯一的选项是应用程序读取数据之前禁用中断,通常这是简单又常用的方案。