https://www.qnx.com/developers/docs/7.1/index.html#com.qnx.doc.neutrino.getting_started/topic/s1_inter.html
前言:
在本章中,我们将了解中断、如何在 QNX Neutrino 下处理中断、中断对调度和实时性的影响以及一些中断管理策略。
QNX Neutrino 和中断
我们需要问的第一件事是“什么是中断?”
编写中断处理程序
让我们看看如何设置中断处理程序 — 调用、特性和一些策略。
总结
处理中断时,请牢记以下几点:
相关概念
中断处理(系统架构)
编写中断处理程序(QNX Neutrino 程序员指南)
相关参考
InterruptAttachEvent()
InterruptAttach()
InterruptDetach()
InterruptDisable()
InterruptEnable()
InterruptHookIdle2()
InterruptHookTrace()
InterruptLock()
InterruptMask()
InterruptUnlock()
InterruptUnmask()
InterruptWait()
一、QNX Neutrino and interrupts
我们首先要问的是“什么是中断?”
中断就是字面意思——中断正在执行的任务,并转移到另一项任务。
例如,假设您正坐在办公桌前处理工作“A”。突然,电话铃响了。一位非常重要的客户 (VIC) 需要您立即回答一些技能测试问题。回答完问题后,您可能会回去处理工作“A”,或者 VIC 可能改变了您的优先级,因此您将工作“A”推到一边,立即开始处理工作“B”。
现在让我们在 QNX Neutrino 下对此进行透视。
在任何时刻,处理器都在忙于处理最高优先级的 READY 线程(这将是处于 RUNNING 状态的线程)的工作。要引发中断,计算机总线上的一个硬件会发出一条中断线(在我们的类比中,这是电话铃声)。
一旦中断线被置位,内核就会跳转到一段代码,该代码设置环境以运行中断服务例程 (ISR),该软件确定检测到中断时应发生什么。
硬件置位中断线和执行 ISR 的第一条指令之间的时间称为中断延迟。中断延迟以微秒为单位。不同的处理器具有不同的中断延迟时间;它取决于处理器速度、缓存架构、内存速度,当然还有操作系统的效率。
在我们的比喻中,如果您戴着耳机听音乐并忽略了正在响铃的电话,那么您将需要更长的时间才能注意到这个电话“中断”。在 QNX Neutrino 下,也会发生同样的事情;有一个处理器指令可以禁用中断(例如 x86 上的 cli)。处理器在重新启用中断之前不会注意到任何中断(在 x86 上,这是 sti 操作码)。
注意:
(1) 为了避免特定于 CPU 的汇编语言调用,QNX Neutrino 提供了以下调用:InterruptEnable() 和 InterruptDisable(),以及 InterruptLock() 和 InterruptUnlock()。这些调用负责处理所有受支持平台上的所有低级细节。
(2) 您应该尽可能短地禁用中断(即,访问或处理硬件所需的最短时间)。不这样做可能会导致中断延迟增加,并且无法满足实时截止时间。
(3) 一些内核调用和库例程会重新启用中断。屏蔽中断不受影响。
ISR 通常执行尽可能少的工作,然后结束(在我们的类比中,这是与 VIC 的电话对话——我们通常不会让客户等待并进行几个小时的工作;我们只是告诉客户,“好的,我马上就去!”)。当 ISR 结束时,它可以告诉内核不应该发生任何事情(意味着 ISR 已经完全处理了事件,不需要做任何其他事情),或者内核应该执行一些可能导致线程变为 READY 的操作。
在我们的类比中,告诉内核中断已处理就像告诉客户答案一样——我们可以返回我们正在做的事情,知道客户的问题已经得到解答。
告诉内核需要执行某些操作就像告诉客户您会回复他们——电话已挂断,但可能会再次响起。
中断服务例程 (ISR)
ISR 是一段负责清除中断源的代码。
电平敏感与边缘敏感
我们一直忽略了这块拼图的另一部分。大多数 PIC 都可以编程为以电平敏感或边缘敏感模式运行。
相关章节:TODO
Interrupt handling (System Architecture)
Writing an Interrupt Handler (QNX Neutrino Programmer's Guide)
1. Interrupt service routine (ISR)
ISR 是一段负责清除中断源的代码。
这是关键点,尤其是与以下事实相结合:中断的运行优先级高于任何软件优先级。这意味着在 ISR 中花费的时间会对线程调度产生严重影响。您应该在 ISR 中花费尽可能少的时间。让我们更深入地研究一下这一点。
注意:(QNX Neutrino 7.1 或更高版本)内核在进入和离开 ISR 时保存并恢复 FPU 上下文,因此在其中使用浮点运算是安全的。
清除中断源
生成中断的硬件设备将保持中断线断言,直到确定软件处理了中断。由于硬件无法读取思想,软件必须告诉它何时响应了中断的原因。通常,这是通过从特定硬件端口读取状态寄存器或从特定内存位置读取数据块来完成的。
告诉线程做某事
ISR 如何告诉内核它现在应该安排线程去做某事?(相反,它如何告诉内核它不应该这么做?)
1.1 Clearing the interrupt source
产生中断的硬件设备将保持中断线处于有效状态,直到它确定软件处理了中断。 由于硬件无法读懂,软件必须告诉它何时响应了中断的原因。通常,这是通过从特定硬件端口读取状态寄存器或从特定内存位置读取数据块来完成的。
无论如何,硬件和软件之间通常会有某种形式的积极确认来“取消断言”中断线。(有时没有确认;例如,某个硬件可能会生成中断并假设软件会处理它。)
由于中断的优先级高于任何软件线程,因此我们应该尽可能少地在 ISR 本身上花费时间,以最大限度地减少对调度的影响。如果我们只需通过读取寄存器来清除中断源,并将该值填充到全局变量中,那么我们的工作就很简单了。
这是 ISR 为串行端口执行的处理类型。当字符到达时,串行端口硬件会产生中断。 ISR 处理程序读取包含字符的状态寄存器,并将该字符填充到循环缓冲区中。完成。总处理时间:几微秒。而且,它必须很快。考虑一下如果您以 115 Kbaud(大约每 100 µs 一个字符)接收字符会发生什么;如果您花费近 100 µs 来处理中断,那么您就没有时间做其他任何事情了!
注意:不过,请不要让我误导您——串行端口的中断服务程序可能需要更长时间才能完成。这是因为有一个尾端轮询,用于查看设备中是否还有更多字符在等待。
显然,在我们的类比中,将中断所花费的时间最小化可以视为“良好的客户服务”——通过将我们打电话的时间保持在最低限度,我们避免给其他客户发出忙音。
如果处理程序需要做大量的工作怎么办?以下是几种可能性:
清除中断源所需的时间很短,但与硬件对话所需的工作量很长(客户问了我们一个简短的问题,需要很长时间才能回答)。
清除中断源所需的时间很长(客户对问题的描述很长且很复杂)。
在第一种情况下,我们希望尽快清除中断源,然后告诉内核让线程执行与慢速硬件对话的实际工作。这里的优点是 ISR 只花费极少的时间处于超高优先级,然后其余的工作将根据常规线程优先级完成。这类似于您接听电话(超高优先级),并将实际工作委托给您的一位助手。我们将在本章后面介绍 ISR 如何告诉内核安排其他人。
在第二种情况下,情况会变得很糟糕。如果 ISR 在退出时没有清除中断源,内核将立即被可编程中断控制器(PIC - 在 x86 上,这是 8259 或同等产品)芯片重新中断。
对于 PIC 粉丝:我们将很快讨论边缘敏感和电平敏感中断。
我们将持续运行 ISR,而没有机会运行正确处理中断所需的线程级代码。
什么样的脑残硬件需要很长时间才能清除中断源?您的基本 PC 软盘控制器会保持中断断言,直到您读取了许多状态寄存器值。不幸的是,寄存器中的数据不是立即可用的,您必须轮询此状态数据。这可能需要几毫秒(用计算机术语来说是很长时间)!
解决此问题的方法是暂时屏蔽中断 - 实际上是告诉 PIC 忽略来自此特定源的中断,直到您告诉它其他情况。在这种情况下,即使中断线是从硬件断言的,PIC 也会忽略它并且不会将其告知处理器。这允许您的 ISR 安排一个线程来在 ISR 之外处理此硬件。当您的线程完成从硬件传输数据时,它可以告诉 PIC 取消屏蔽该中断。这样就可以再次识别该硬件的中断。在我们的比喻中,这就像将 VIC 的电话转接给您的助手。
1.2 Telling a thread to do something
ISR 如何告诉内核它现在应该安排一个线程来执行某些工作?(相反,它如何告诉内核它不应该这样做?)
以下是典型 ISR 的一些伪代码:
FUNCTION ISR BEGIN determine source of interrupt clear source of interrupt IF thread required to do some work THEN RETURN (event); ELSE RETURN (NULL); ENDIF END
诀窍是返回一个事件(类型为 struct sigevent,我们在“时钟、计时器和时不时地获得乐趣”一章中讨论过)而不是 NULL。请注意,返回的事件在 ISR 的堆栈框架被销毁后必须是持久的。这意味着必须在 ISR 之外声明该事件,或者使用区域参数从持久数据区域传递到 ISR,或者在 ISR 本身内声明为静态。您自己选择。
如果您返回一个事件,内核会在 ISR 返回时将其传递给线程。由于事件“提醒”线程(通过脉冲,正如我们在“消息传递”一章中讨论的那样,或者通过信号),这可能导致内核重新安排下一个获得 CPU 的线程。如果您从 ISR 返回 NULL,那么内核就知道在线程时不需要做任何特殊的事情,因此它不会重新安排任何线程 — ISR 抢占时正在运行的线程将恢复运行。
2. Level-sensitivity versus edge-sensitivity
我们遗漏了拼图的另一部分。大多数 PIC 都可以编程为在电平敏感或边缘敏感模式下运行。
在电平敏感模式下,中断线在“开启”状态下被视为由 PIC 断言。(这对应于下图中的标签“1”。)
Figure 1. Level-sensitive interrupt assertion.
我们可以看到,这会导致上面软盘控制器示例中描述的问题。每当 ISR 完成时,内核都会告诉 PIC,“好的,我已经处理了这个中断。下次激活时告诉我”(图中步骤 2)。从技术角度来说,内核会向 PIC 发送中断结束 (EOI)。PIC 查看中断线,如果中断线仍然处于活动状态,则会立即重新中断内核(步骤 3)。
我们可以通过将 PIC 编程为边沿敏感模式来解决这个问题。在这种模式下,PIC 仅在活动边沿上注意到中断。
Figure 2. Edge-sensitive interrupt assertion.
即使 ISR 无法清除中断源,当内核将 EOI 发送到 PIC(图中步骤 2)时,PIC 也不会重新中断内核,因为在 EOI 之后没有另一个活动边沿转换。为了识别该线路上的另一个中断,线路必须先变为非活动状态(步骤 4),然后变为活动状态(步骤 1)。
好吧,看来我们所有的问题都解决了!只需对所有中断使用边沿敏感即可。
不幸的是,边沿敏感模式本身存在问题。
假设您的 ISR 无法清除中断原因。当内核向 PIC 发出 EOI 时,硬件仍会断言中断线路。但是,由于 PIC 处于边沿敏感模式,因此它永远不会看到来自该设备的另一个中断。
考虑这样一种情况,其中存在一个硬件总线架构,允许两个设备(比如说 SCSI 总线适配器和以太网卡)共享同一条中断线路。这种情况是可能发生的,特别是当 PIC 上的中断源数量不足时!在这种情况下,两个 ISR 例程将附加到同一个中断向量(顺便说一下,这是合法的),并且每当内核从 PIC 获得该硬件中断级别的中断时,它就会依次调用它们。
Figure 3. Sharing interrupts: one at a time.
在这种情况下,由于当其关联的 ISR 运行时只有一个硬件设备处于活动状态(SCSI 设备),因此它正确地清除了中断源(步骤 2)。请注意,无论如何,内核都会运行以太网设备的 ISR(在步骤 3 中)——它不知道以太网硬件是否也需要维修,因此它始终运行整个链。
但请考虑这种情况:
Figure 4. Sharing interrupts: several at once.
问题就在这里。
以太网设备首先中断。这导致中断线被置位(PIC 注意到了活动边沿),并且内核调用链中的第一个中断处理程序(SCSI 磁盘驱动程序;图中的步骤 1)。SCSI 磁盘驱动程序的 ISR 查看其硬件并说:“不,不是我。哦,好吧,忽略它”(步骤 2)。然后内核调用链中的下一个 ISR,即以太网 ISR(步骤 3)。以太网 ISR 查看硬件并说:“嘿!那是我的硬件触发了中断。我要清除它。”不幸的是,当以太网 ISR 清除它时,SCSI 设备生成了一个中断(步骤 4)。
当以太网 ISR 完成清除中断源(步骤 5)时,由于 SCSI 硬件设备,中断线仍然被置位。但是,在边缘敏感模式下编程的 PIC 正在寻找非活动到活动的转换(在复合线上),然后才能识别另一个中断。这不会发生,因为内核已经调用了两个中断服务例程,现在正在等待来自 PIC 的另一个中断。
在这种情况下,电平敏感解决方案是合适的,因为当以太网 ISR 完成并且内核向 PIC 发出 EOI 时,PIC 会发现总线上仍有中断活动并重新中断内核。然后内核将运行 ISR 链,这一次 SCSI 驱动程序将有机会运行并清除中断源。
边缘敏感与电平敏感的选择取决于硬件和启动代码。某些硬件只支持其中一种;支持任一模式的硬件将由启动代码编程为其中一种。您必须查阅系统附带的 BSP(板级支持包)文档才能获得明确的答案。
二、Writing interrupt handlers
1. Attaching an interrupt handler
要连接到中断源,您可以使用 InterruptAttach() 或 InterruptAttachEvent()。
#include <sys/neutrino.h> int InterruptAttachEvent (int intr, const struct sigevent *event, unsigned flags); int InterruptAttach (int intr, const struct sigevent*(*handler)(void *area, int id), const void *area, int size, unsigned flags);
intr 参数指定您希望将指定的处理程序附加到哪个中断。传递的值由启动代码定义,该启动代码在 QNX Neutrino 启动之前初始化 PIC(以及其他内容)。(有关启动代码的更多信息,请参阅 QNX Neutrino 文档;查看实用程序参考中的 startup-*。)
注意:由于您不希望每个人都能附加中断,因此 QNX Neutrino 只允许启用了 PROCMGR_AID_INTERRUPT 或 PROCMGR_AID_INTERRUPTEVENT 功能的进程(请参阅 C 库参考中的 procmgr_ability())执行此操作。
此时,两个函数 InterruptAttach() 和 InterruptAttachEvent() 有所不同。我们先看看更简单的 InterruptAttachEvent()。然后我们再回到 InterruptAttach()。
使用 InterruptAttachEvent() 进行附加
InterruptAttachEvent() 函数需要两个附加参数:参数 event(指向应传递的 struct sigevent 的指针)和 flags 参数。InterruptAttachEvent() 告知内核,只要检测到中断,就应返回事件,并且应屏蔽中断级别。请注意,内核会解释事件并确定应将哪个线程置为 READY。
使用 InterruptAttach() 进行附加
使用 InterruptAttach(),我们指定一组不同的参数。handler 参数是要调用的函数的地址。从原型中可以看到,handler() 返回一个 struct sigevent,它指示要返回哪种事件,并带有两个参数。第一个传递的参数是 area,它只是传递给 InterruptAttach() 的区域参数。第二个参数 id 是中断的标识,也是 InterruptAttach() 的返回值。它用于识别中断以及屏蔽、取消屏蔽、锁定或解锁中断。InterruptAttach() 的第四个参数是 size,它指示您传入 area 的数据区域有多大(以字节为单位)。最后,flags 参数与为 InterruptAttachEvent() 传递的参数相同;我们将很快讨论它。
2. Now that you've attached an interrupt
此时,您已调用 InterruptAttachEvent() 或 InterruptAttach()。
以下是将 ISR 附加到硬件中断向量的代码片段,我们在代码示例中通过常量 HW_SERIAL_IRQ 对其进行了标识:
#include <sys/neutrino.h> int interruptID; const struct sigevent *intHandler (void *arg, int id) { ... } int main (int argc, char **argv) { ... interruptID = InterruptAttach (HW_SERIAL_IRQ, intHandler, &event, sizeof (event), 0); if (interruptID == -1) { fprintf (stderr, "%s: can't attach to IRQ %d\n", progname, HW_SERIAL_IRQ); perror (NULL); exit (EXIT_FAILURE); } ... return (EXIT_SUCCESS); }
这将在 ISR(名为 intHandler() 的例程;详情见下文)和硬件中断向量 HW_SERIAL_IRQ 之间建立关联。
此时,如果该中断向量上发生中断,我们的 ISR 将被调度。当我们调用 InterruptAttach() 时,内核会在 PIC 级别取消屏蔽中断源(除非它已被取消屏蔽,如果多个 ISR 共享同一个中断,就会出现这种情况)。
3. Detaching an interrupt handler
完成 ISR 后,我们可能希望断开 ISR 与中断向量之间的关联:
int InterruptDetach (int id);
我说“可能”是因为处理中断的线程通常位于服务器中,而服务器通常会永远挂起。因此可以想象,一个结构良好的服务器永远不会发出 InterruptDetach() 函数调用。此外,当线程或进程死亡时,操作系统将删除线程或进程可能与其关联的任何中断处理程序。因此,只需从 main() 末尾退出、调用 exit() 或由于 SIGSEGV 退出,就会自动将您的 ISR 与中断向量分离。(当然,您可能希望更好地处理这个问题,并阻止您的设备生成中断。如果另一个设备正在共享中断,那么没有其他办法——您必须清理,否则如果在边缘敏感模式下运行,您将不会再收到任何中断,或者如果在级别敏感模式下运行,您将收到持续不断的 ISR 调度。)
继续上面的例子,如果我们想要分离,我们将使用以下代码:
void terminateInterrupts (void) { InterruptDetach (interruptID); }
如果这是与该中断向量关联的最后一个 ISR,则内核将自动屏蔽 PIC 级别的中断源,以免产生中断。
4. The flags parameter
最后一个参数 flags 控制各种事物:
_NTO_INTR_FLAGS_END: 表示此处理程序应跟踪可能附加到同一中断源的其他处理程序。
_NTO_INTR_FLAGS_NO_UNMASK: (QNX Neutrino 6.6 或更高版本)表示内核应屏蔽中断。通常,InterruptAttach() 和 InterruptAttachEvent() 会在第一次附加中断时自动取消屏蔽中断。如果指定 _NTO_INTR_FLAGS_NO_UNMASK,内核会屏蔽中断,您必须专门调用 InterruptUnmask() 来启用它。
_NTO_INTR_FLAGS_PROCESS: 表示此处理程序与进程而不是线程相关联。归根结底,如果您指定此标志,则当进程退出时,中断处理程序将自动与中断源分离。如果不指定此标志,则当最初创建关联的线程退出时,中断处理程序将与中断源分离。
_NTO_INTR_FLAGS_TRK_MSK: 表示内核应跟踪中断被屏蔽的次数。这会导致内核的工作量增加,但是为了确保在进程或线程退出时有序取消屏蔽中断源,这是必需的。
_NTO_INTR_FLAGS_EXCLUSIVE: (QNX Neutrino 7.0.4 或更高版本)表示当前附加到中断向量的中断处理程序或事件是唯一可以附加的中断处理程序或事件。
5. The interrupt service routine
让我们看看 ISR 本身。在第一个示例中,我们将使用 InterruptAttach() 函数。然后,我们将看到完全相同的内容,只是使用了 InterruptAttachEvent()。
使用 InterruptAttach()
继续我们的例子,这是 ISR intHandler()。它查看我们假设连接到 HW_SERIAL_IRQ 的 8250 串行端口芯片:
使用 InterruptAttachEvent()
如果我们要重新编码上述示例以使用 InterruptAttachEvent(),它将如下所示:
InterruptAttach() 与 InterruptAttachEvent()
这自然会引出一个问题,为什么我要使用其中一个而不是另一个?
权衡
那么,您应该使用哪个函数?对于低频中断,您几乎总是可以使用 InterruptAttachEvent()。由于中断很少发生,因此即使您不必要地调度线程,也不会对整体系统性能产生重大影响。唯一可能再次困扰您的情况是,如果另一个设备与同一个中断链接在一起 — 在这种情况下,由于 InterruptAttachEvent() 会屏蔽中断源,因此它将有效地禁用来自另一个设备的中断,直到中断源被取消屏蔽。只有当第一个设备需要很长时间才能得到服务时,这才值得关注。从更大的角度来看,这是一个硬件系统设计问题 — 您不应该将响应缓慢的设备与高速设备链接在同一线路上。
5.1 Using InterruptAttach()
继续我们的例子,这是 ISR intHandler()。它查看我们假设连接到 HW_SERIAL_IRQ 的 8250 串行端口芯片:
switch (iir) { case IIR_MSR: serial_msr = in8 (base_reg + REG_MS); /* wake up thread */ return (event); break; case IIR_THE: /* do nothing */ break; case IIR_RX: /* note the character */ serial_rx = in8 (base_reg + REG_RX); break; case IIR_LSR: /* note the line status reg. */ serial_lsr = in8 (base_reg + REG_LS); break; default: break; } /* don't bother anyone */ return (NULL); }
我们注意到的第一件事是,ISR 接触的任何变量都必须声明为 volatile。在单处理器机箱上,这不是为了 ISR 的利益,而是为了线程级代码的利益,线程级代码可以在任何时候被 ISR 中断。当然,在 SMP 机箱上,我们可以让 ISR 与线程级代码同时运行,在这种情况下,我们必须非常小心这类事情。
使用 volatile 关键字,我们告诉编译器不要缓存任何这些变量的值,因为它们可以在执行期间的任何时候发生变化。
我们注意到的下一件事是中断服务例程本身的原型。它包含 const struct sigevent *。这表示例程 intHandler() 返回一个 struct sigevent 指针。这是所有中断服务例程的标准。
最后,请注意,ISR 决定是否向线程发送事件。只有在调制解调器状态寄存器 (MSR) 中断的情况下,我们才希望传递事件(事件由变量事件标识,当我们附加它时,该变量被方便地传递给 ISR)。在所有其他情况下,我们忽略中断(并更新一些全局变量)。但是,在所有情况下,我们都会清除中断源。这是通过 in8() 读取 I/O 端口来完成的。
5.2 Using InterruptAttachEvent()
如果我们重新编码上面的例子以使用 InterruptAttachEvent(),它将看起来像这样:
/* * part of int2.c */ #include <stdio.h> #include <sys/neutrino.h> #define HW_SERIAL_IRQ 3 #define REG_RX 0 #define REG_II 2 #define REG_LS 5 #define REG_MS 6 #define IIR_MASK 0x07 #define IIR_MSR 0x00 #define IIR_THE 0x02 #define IIR_RX 0x04 #define IIR_LSR 0x06 #define IIR_MASK 0x07 static int base_reg = 0x2f8; int main (int argc, char **argv) { int intId; // interrupt id int iir; // interrupt identification register int serial_msr; // saved contents of Modem Status Reg int serial_rx; // saved contents of RX register int serial_lsr; // saved contents of Line Status Reg struct sigevent event; // usual main() setup stuff... // set up the event intId = InterruptAttachEvent (HW_SERIAL_IRQ, &event, 0); for (;;) { // wait for an interrupt event (could use MsgReceive instead) InterruptWait (0, NULL); /* * determine the source of the interrupt (and clear it) * by reading the Interrupt Identification Register */ iir = in8 (base_reg + REG_II) & IIR_MASK; // unmask the interrupt, so we can get the next event InterruptUnmask (HW_SERIAL_IRQ, intId); /* no interrupt? */ if (iir & 1) { /* then wait again for next */ continue; } /* * figure out which interrupt source caused the interrupt, * and determine if we need to do something about it */ switch(iir) { case IIR_MSR: serial_msr = in8(base_reg + REG_MS); /* * perform whatever processing you would've done in * the other example... */ break; case IIR_THE: /* do nothing */ break; case IIR_RX: /* note the character */ serial_rx = in8 (base_reg + REG_RX); break; case IIR_LSR: /* note the line status reg. */ serial_lsr = in8 (base_reg + REG_LS); break; } } /* You won't get here. */ return (0); }
请注意,InterruptAttachEvent() 函数返回一个中断标识符(一个小整数)。我们已将其保存到变量 intId 中,以便稍后在取消屏蔽中断时使用它。
连接中断后,我们需要等待中断触发。由于我们使用了 InterruptAttachEvent(),因此我们将获得之前为每个中断创建的事件。与使用 InterruptAttach() 时发生的情况形成对比 - 在那种情况下,我们的 ISR 决定是否要为我们丢弃事件。使用 InterruptAttachEvent(),内核不知道导致中断的硬件事件对我们来说是否“重要”,因此每次发生时它都会为我们丢弃事件,屏蔽中断,并让我们决定中断是否重要。
我们在 InterruptAttach() 的代码示例中(上面)通过返回 struct sigevent 来指示应该发生某事,或者通过返回常量 NULL 来处理决定。请注意我们在修改 InterruptAttachEvent() 代码时所做的更改:
“ISR”工作现在在 main() 中的线程时间完成。
我们必须始终在收到事件后取消屏蔽中断源(因为内核会为我们屏蔽它)。
如果中断对我们来说并不重要,我们不会执行任何操作,只需在 for 语句中再次循环,等待另一个中断。
如果中断对我们来说很重要,我们会直接处理它(在 case IIR_MSR 部分中)。
您决定清除中断源的位置取决于您的硬件和您选择的通知方案。使用 SIGEV_INTR 和 InterruptWait() 的组合,内核不会“排队”多个通知;使用 SIGEV_PULSE 和 MsgReceive(),内核将排队所有通知。如果您使用信号(例如 SIGEV_SIGNAL),您可以定义信号是否排队。对于某些硬件方案,您可能需要先清除中断源,然后才能从设备中读取更多数据;对于其他硬件,您不必这样做,并且可以在中断被断言时读取数据。
注意: ISR 返回 SIGEV_THREAD 的情况让我感到十分恐惧!我建议尽可能避免使用此“功能”。
(QNX Neutrino 7.0.4 或更高版本)为了注册或使用 SIGEV_THREAD 类型的事件,您的进程必须启用 PROCMGR_AID_SIGEV_THREAD 功能。有关更多信息,请参阅 procmgr_ability()。
在上面的串行端口示例中,我们决定使用 InterruptWait(),它将对一个条目进行排队。串行端口硬件可能会在我们读取中断标识寄存器后立即断言另一个中断,但这没问题,因为最多会有一个 SIGEV_INTR 被排队。我们将在下一次 for 循环迭代中获取此通知。
5.3 InterruptAttach() versus InterruptAttachEvent()
这自然会引出一个问题:我为什么要使用其中一个而不是另一个?
InterruptAttachEvent() 最明显的优势是它比 InterruptAttach() 更易于使用 — 没有 ISR 例程(因此无需调试)。另一个优势是,由于内核空间中没有运行任何程序(而 ISR 例程则如此),因此不存在整个系统崩溃的危险。如果您确实遇到了编程错误,那么进程将崩溃,而不是整个系统。但是,根据您要实现的目标,它可能比 InterruptAttach() 更有效或更不高效。这个问题非常复杂,用几个词(如“更快”或“更好”)来概括可能还不够。我们需要看一些图片和场景。
以下是我们使用 InterruptAttach() 时发生的情况:
Figure 1. Control flow with InterruptAttach().
当前正在运行的线程(“thread1”)被中断,我们进入内核。内核保存“thread1”的上下文。然后内核进行查找以查看谁负责处理中断,并决定“ISR1”负责。此时,内核为“ISR1”设置上下文并转移控制权。“ISR1”查看硬件并决定返回 struct sigevent。内核注意到返回值,找出谁需要处理它,并使其处于就绪状态。这可能会导致内核安排另一个线程“thread2”运行。
现在,让我们将其与使用 InterruptAttachEvent() 时发生的情况进行对比:
Figure 2. Control flow with InterruptAttachEvent().
在这种情况下,服务路径要短得多。我们从当前正在运行的线程(“thread1”)切换到内核。内核没有再切换到 ISR,而是“假装”ISR 返回了一个 struct sigevent 并对其采取行动,重新安排“thread2”运行。
现在你在想,“太好了!我要忘记所有关于 InterruptAttach() 的事情,只使用更简单的 InterruptAttachEvent()。”
这不是一个好主意,因为您可能不需要为硬件生成的每个中断唤醒!回过头来看看上面的源示例——它只在串行端口上的调制解调器状态寄存器改变状态时返回事件,而不是在字符到达时、线路状态寄存器改变时或传输保持缓冲区为空时。
在这种情况下,尤其是当串行端口正在接收字符(您想要忽略)时,您将浪费大量时间重新安排线程运行,结果却让它查看串行端口并决定无论如何都不想对此采取任何行动。在这种情况下,事情看起来会像这样:
Figure 3. Control flow with InterruptAttachEvent() and unnecessary rescheduling.
所发生的一切就是,您需要进行线程间上下文切换才能进入“线程 2”,而“线程 2”会查看硬件并决定不需要对此执行任何操作,这又需要您进行一次线程间上下文切换才能返回到“线程 1”。
如果您使用了 InterruptAttach() 但不想安排其他线程(即您返回),情况将如下所示:
Figure 4. Control flow with InterruptAttach() with no thread rescheduling.
内核知道“thread1”正在运行,而 ISR 没有告诉它做任何事情,因此它可以直接继续并让“thread1”在中断后继续运行。
仅供参考,以下是 InterruptAttachEvent() 函数调用所做的事情(请注意,这不是真正的来源,因为 InterruptAttachEvent() 实际上将数据结构绑定到内核 - 它不是作为被调用的离散函数实现的!):
// the "internal" handler static const struct sigevent *internalHandler (void *arg, int id) { struct sigevent *event = arg; InterruptMask (intr, id); return (arg); } int InterruptAttachEvent (int intr, const struct sigevent *event, unsigned flags) { static struct sigevent static_event; memcpy (&static_event, event, sizeof (static_event)); return (InterruptAttach (intr, internalHandler, &static_event, sizeof (*event), flags)); }
5.4 The tradeoffs
那么,您应该使用哪个函数呢?对于低频中断,您几乎总是可以使用 InterruptAttachEvent()。由于中断很少发生,因此即使您不必要地调度线程,也不会对整体系统性能产生重大影响。唯一会让您感到困扰的情况是,如果另一个设备被链接到同一个中断 — 在这种情况下,由于 InterruptAttachEvent() 会屏蔽中断源,因此它将有效地禁用来自另一个设备的中断,直到中断源被取消屏蔽。只有当第一个设备需要很长时间才能得到服务时,这才是一个问题。从更大的角度来看,这是一个硬件系统设计问题 — 您不应该将响应缓慢的设备与高速设备链接到同一条线路上。
对于更高频率的中断,这是一个难题,有很多因素:
(1) 不必要的中断 — 如果有大量的中断,最好使用 InterruptAttach() 并在 ISR 中将其过滤掉。例如,考虑串行设备的情况。一个线程可能会发出一个命令说“给我 64 个字节”。如果 ISR 的编程知道在从硬件接收到 64 个字节之前不会发生任何有用的事情,则 ISR 已有效地过滤了中断。然后,ISR 仅在积累了 64 个字节后才返回事件。
(2) 延迟 — 如果您的硬件对断言中断请求和执行 ISR 之间的时间量很敏感,则应使用 InterruptAttach() 来最小化此中断延迟。这是因为内核在调度 ISR 时非常快。
(3) 缓冲 — 如果您的硬件中有缓冲,您可能能够使用 InterruptAttachEvent() 和单条目排队机制(如 SIGEV_INTR 和 InterruptWait())。此方法允许硬件按其需要频繁中断,同时允许线程在可能的情况下从硬件缓冲区中选取值。由于硬件正在缓冲数据,因此不存在中断延迟问题。
6. ISR functions
我们应该解决的下一个问题是 ISR 可以调用的函数列表。
让我稍微离题一下。从历史上看,ISR 如此难以编写(在大多数其他操作系统中仍然如此)的原因是 ISR 在特殊环境中运行。
使编写 ISR 变得复杂的一个特别问题是,就内核而言,ISR 实际上不是一个“合适的”线程。如果你想这样称呼它,它是一个奇怪的“硬件”线程。这意味着 ISR 不允许执行任何“线程级”的事情,如消息传递、同步、内核调用、磁盘 I/O 等。
但这不会使编写 ISR 例程变得更加困难吗?是的。因此,解决方案是在 ISR 中尽可能少地做工作,并在线程级完成其余工作,在那里您可以访问所有服务。
您在 ISR 中的目标应该是:
(1) 获取瞬时存在的信息。
(2) 清除 ISR 的来源。
(3) 可选择分派一个线程来完成“实际”工作。
这种“架构”取决于 QNX Neutrino 具有非常快的上下文切换时间。您知道您可以快速进入 ISR 来执行时间关键型工作。您还知道,当 ISR 返回事件以触发线程级工作时,该线程也会快速启动。正是这种“不要在 ISR 中执行任何操作”的理念让 QNX Neutrino ISR 如此简单!
那么,您可以在 ISR 中使用哪些调用?以下是摘要(有关官方列表,请参阅 QNX Neutrino C 库参考中的完整安全信息附录):
(1) atomic_*() 函数(例如 atomic_set())
(2) mem*() 函数(例如 memcpy())
(3) 大多数 str*() 函数(例如 strcmp())。但请注意,并非所有这些函数都是安全的,例如 strdup() — 它调用 malloc(),而 malloc() 使用互斥锁,这是不允许的。对于字符串函数,在使用之前,您应该查阅 QNX Neutrino C 库参考中的各个条目。
(4) InterruptMask()
(5) InterruptUnmask()
(6) InterruptLock()
(7) InterruptUnlock()
(8) InterruptDisable()
(9) InterruptEnable()
(10) in*() 和 out*()
基本上,经验法则是“不要使用任何会占用大量堆栈空间或时间的东西,也不要使用任何会发出内核调用的东西。”堆栈空间要求源于 ISR 的堆栈非常有限这一事实。
中断安全函数列表很有意义——您可能想要移动一些内存,在这种情况下,mem*() 和 str*() 函数是一个不错的选择。您很可能想要从硬件读取数据寄存器(以便保存临时数据变量和/或清除中断源),因此您需要使用 in*() 和 out*() 函数。
那么令人困惑的 Interrupt*() 函数选择呢?让我们成对检查它们:
InterruptMask() 和 InterruptUnmask()
这些函数负责在 PIC 级别屏蔽中断源;这样可以防止它们被传递到 CPU。通常,如果您想要在线程中执行进一步的工作并且无法在 ISR 本身中清除中断源,则可以使用此功能。在这种情况下,ISR 将发出 InterruptMask(),并且线程将在完成调用它执行的任何操作后发出 InterruptUnmask()。
请记住,InterruptMask() 和 InterruptUnmask() 是计数的 - 您必须“取消屏蔽”与“屏蔽”相同的次数,以便中断源能够再次中断您。
顺便说一句,请注意 InterruptAttachEvent() 为您执行 InterruptMask()(在内核中)- 因此您必须从中断处理线程调用 InterruptUnmask()。
InterruptLock() 和 InterruptUnlock()
这些函数用于在单处理器或多处理器系统上禁用 (InterruptLock()) 和启用 (InterruptUnlock()) 中断。如果您需要保护线程免受 ISR 的影响(或者,在 SMP 系统上,保护 ISR 免受线程的影响),则需要禁用中断。完成关键数据操作后,即可启用中断。请注意,建议使用这些函数,而不是“旧”的 InterruptDisable() 和 InterruptEnable() 函数,因为它们可以在 SMP 系统上正常运行。与“旧”函数相比,在 SMP 系统上执行检查需要额外的成本,但在单处理器系统中,成本可以忽略不计,这就是我建议您始终使用 InterruptLock() 和 InterruptUnlock() 的原因。
InterruptDisable() 和 InterruptEnable()
这些函数不应在新设计中使用。从历史上看,当 QNX Neutrino 仅适用于 x86 时,它们用于调用 x86 处理器指令 cli 和 sti。它们后来已升级以处理所有受支持的处理器,但您应该使用 InterruptLock() 和 InterruptUnlock()(以使 SMP 系统正常运行)。
值得重复的一件事是,在 SMP 系统上,可以同时运行中断服务例程和另一个线程。
三、Summary
处理中断时,请牢记以下几点:
不要在 ISR 中花费太长时间 — 执行尽可能少的工作。这有助于最大限度地减少中断延迟和调试。
当您需要在发生中断时立即访问硬件时,请使用 InterruptAttach();否则,请避免使用它。
在其他所有时间都使用 InterruptAttachEvent()。内核将安排一个线程(基于您传递的事件)来处理中断。
通过调用 InterruptLock() 和 InterruptUnlock() 来保护中断服务例程(如果使用 InterruptAttach())和线程使用的变量。
将要在线程和 ISR 之间使用的变量声明为易失性变量,以便编译器不会缓存已被 ISR 更改的“陈旧”值。
相关:TODO
https://www.qnx.com/developers/docs/7.1/index.html#com.qnx.doc.neutrino.prog/topic/inthandler_Multicore.html
https://www.qnx.com/developers/docs/7.1/index.html#com.qnx.doc.neutrino.sys_arch/topic/kernel_INTERRUPTHANDLING.html
标签:中断,ISR,Interrupts,QNX,硬件,线程,内核,官网,InterruptAttachEvent From: https://www.cnblogs.com/hellokitty2/p/18223213