QNX-9—QNX官网文档翻译—中断-3—Writing an Interrupt Handler
翻译:
QNX Software Development Platform --> Programming --> Programmer's Guide
https://www.qnx.com/developers/docs/7.1/index.html#com.qnx.doc.neutrino.prog/topic/inthandler.html
前言:
及时处理硬件事件的关键是硬件产生中断。中断只是处理器正在执行的操作的暂停或中断,同时请求执行其他操作。
每当硬件达到需要软件干预的状态时,它就会产生中断。与让软件不断轮询硬件(这会浪费 CPU 时间)不同,中断是“发现”硬件需要某种服务的首选方法。因此,处理中断的软件通常称为中断服务例程 (ISR)。
虽然中断处理在实时系统中至关重要,但不幸的是,在许多传统操作系统中,中断处理是一项非常困难和尴尬的任务。QNX Neutrino RTOS 并非如此。正如您将在本章中看到的,处理中断几乎是微不足道的;鉴于 QNX Neutrino 中的快速上下文切换时间,大多数(如果不是全部)“工作”(通常由 ISR 完成)实际上都是由线程完成的。
让我们来看看 QNX Neutrino 中断函数和一些处理中断的方法。要从不同角度了解中断,请参阅《QNX Neutrino 入门》的“中断”章节。
注意:
(1) 您应该尽可能短地禁用中断(即访问或处理硬件所需的最短时间)。不这样做可能会导致中断延迟增加,并且无法满足实时截止时间。
(2) 一些内核调用和库例程会重新启用中断。屏蔽中断不受影响。
多核系统上的中断
在多核系统中,每个中断都指向一个(且只有一个)CPU,但指向哪个并不重要。这种情况如何发生由板上的可编程中断控制器 (PIC) 芯片控制。在系统启动时初始化 PIC 时,您可以对它们进行编程,以将中断传送到您想要的任何 CPU;在某些 PIC 上,您甚至可以让中断在每次中断时在 CPU 之间轮换。
附加和分离中断
为了安装 ISR,软件必须告诉操作系统它希望将 ISR 与特定的中断源相关联,该中断源可以是硬件中断请求线 (IRQ) 或多个软件中断之一。
中断服务程序 (ISR)
在我们上面的示例中,函数 serint() 是 ISR。
用尽中断事件
如果您正在使用中断,您可能会看到“中断事件不足”错误。
共享中断的问题
不同的设备可以共享一个中断(例如,如果您的硬件中断线路用完了),但我们不建议您对会产生大量中断的硬件这样做。我们还建议您不要与您没有完全源代码控制的驱动程序共享中断,因为您需要确保驱动程序正确处理中断。
高级主题
现在我们已经了解了处理中断的基础知识,让我们来看看更多细节和一些高级主题。
一、Interrupts on multicore systems
在多核系统上,每个中断都指向一个(且只有一个)CPU,但指向哪个 CPU 并不重要。这种情况如何发生由板上的可编程中断控制器 (PIC) 芯片控制。在系统启动时初始化 PIC 时,您可以对它们进行编程,以将中断传递到您想要的任何 CPU;在某些 PIC 上,您甚至可以让中断在每次中断时在 CPU 之间轮换。
对于我们编写的启动程序,我们通常会进行编程,以便所有中断(用于处理器间中断的中断除外)都发送到 CPU 0。在运行 7.x 及更高版本的系统上,我们支持每个处理器中断控制器(基于 x86 的系统上的 LAPIC 中断和基于 ARM 的系统上的 GIC PPI)。对于这些中断,您可以将您感兴趣的 CPU 中断编码为 InterruptAttach() 或 InterruptAttachEvent() 向量编号 - 因此,这就是发生中断的 CPU。
有关更多信息,请参阅《构建嵌入式系统》的“ Startup Programs ”一章。
InterruptAttach() 添加的 ISR(中断服务例程)在接受中断的 CPU 上运行。
接收 InterruptAttachEvent() 设置的事件的 IST(中断服务线程)在任何 CPU 上运行,仅受调度程序和运行掩码的限制。
调用 InterruptWait() 的线程在任何 CPU 上运行,仅受调度程序和运行掩码的限制。
如前所述,通常在我们提供的启动程序中,我们将中断定向到 CPU 0,但如果您的系统设计要求您将中断路由到另一个 CPU,您可以在以下“使用 InterruptAttach() 时修改 BSP 中的中断控制器的示例”和“使用 InterruptAttachEvent() 处理特定 CPU 上的 sigevents 的示例”部分中查看示例。
1. Example of modifying the interrupt controller in a BSP when using InterruptAttach()
默认情况下,当您使用 InterruptAttach() 调用将 ISR 附加到特定中断时,ISR 将在接收中断的内核上执行。将中断路由到特定内核不是在内核中完成的,但可以在板级支持包的启动代码中完成,您可以在其中对中断控制器 (PIC/GIC) 初始化代码进行编程。
您可以调用一个新函数来修改中断控制器,以将中断传送到特定 CPU(CPU0 – CPUn,其中 n 代表 CPU 编号),假设中断控制器提供此支持。默认情况下,中断传送到 CPU0,但 IPI 除外,IPI 传送到特定 CPU。
以下示例适用于基于 NXP i.MX 8 ARMv8(aarch64)的系统,带有 GICv2 中断控制器。您无需直接修改启动库,而是可以在 BSP ($BSP_ROOT_DIR/hardware/startup/boards/imx8x/imx_init_intrinfo.c) 中修改特定于板的代码。以下是您在 imx_init_intrinfo.c 中要执行的操作,如粗体代码所示:
(1) 创建一个函数来配置中断路由
(2) 在 init_intrinfo(void) 函数中调用您在上一步中创建的函数
在 imx_init_intrinfo.c 中的 init_intrinfo(void) 函数中,您可以调用自定义函数将中断路由到您创建的特定 CPU。
/** * Example function to Route SPI Interrupts to a Specific CPU * * @param gicd GIC distributor base address. * @param aff3 (ARM_GICD_IROUTERn[39:32]) — affinity level 3, the least significant affinity level field * @param aff2 (ARM_GICD_IROUTERn[23:16]) — affinity level 2, an intermediate affinity level field * @param aff1 (ARM_GICD_IROUTERn[15:8]) — affinity level 1, an intermediate affinity level field * @param aff0 (ARM_GICD_IROUTERn[7:0]) — affinity level 0, the most significant affinity level field */ void gic_v3_intr_route(paddr_t gicd, uint8_t aff3, uint8_t aff2, uint8_t aff1, uint8_t aff0) { unsigned itn, spi_vectors, i; /* Calculate the number of interrupt lines */ uint32_t const gicd_typer = in32(gicd + ARM_GICD_TYPER); itn = ((gicd_typer & ARM_GICD_TYPER_ITLN) + 1) * 32; if (debug_flag > 1) { kprintf("GICv3: %d interrupts\n", itn); } spi_vectors = min(1020, itn) - 32; /* Route all SPI interrupts to the specific CPU */ for (i = 0; i < spi_vectors; i++) { ((uint64_t *)(gicd + ARM_GICD_IROUTERn))[i+32] = ((uint64_t)aff3 << 32) | ((uint64_t)aff2 << 16) | ((uint64_t)aff1 << 8) | ((uint64_t)aff0 << 0); } kprintf("Route %d SPI interrupt to CPU affinity %d.%d.%d.%d\n", spi_vectors, aff3, aff2, aff1, aff0); } /** * Initialize Generic Interrupt Controller (GIC). */ void init_intrinfo(void) { /* Initialize GIC */ gic_v3_init(IMX_GIC_GICD_BASE, IMX_GIC_GICR_BASE, IMX_GIC_GICC_BASE, 0, 0); /* Example to route all SPI interrupts to CPU1 (affinity 0.0.0.1) */ gic_v3_intr_route(IMX_GIC_GICD_BASE, 0, 0, 0, 1); /* Add the interrupt callouts */ add_interrupt_array(pci_steer_intr, sizeof(pci_steer_intr)); }
2. Example of using InterruptAttachEvent() to handle sigevents on a specific CPU
对于 IST(中断服务线程),当您调用 InterruptAttachEvent() 将 sigevent 与特定中断关联时,默认情况下会将 sigevent 传递给调用线程。您可能出于各种原因(例如可靠性)希望处理程序在特定 CPU(或核心)上运行。为此,您可以在设置事件 [调用 InterruptAttachEvent()] 之前使用参数 _NTO_TCTL_RUNMASK_GET_AND_SET 和运行掩码调用 ThreadCtl() 来指定线程的 CPU。运行掩码指定 sigevent 在其上运行的核心或 CPU。需要注意的是,使用此设计可能会对系统性能产生影响,如果您使用自适应分区 (APS),还可能导致分区反转。
对于 IST,您通常仍希望更改 PIC 路由,如“使用 InterruptAttach() 时修改 BSP 中的中断控制器的示例”以及设置运行掩码。如果没有路由更改,硬件将把中断传送到 CPU X,然后内核必须 IPI CPU Y 来运行 IST 代码。
如果将 IST 的运行掩码设置为中断路由到的 CPU 以外的 CPU,则可能会发生大量跨核活动并增加延迟。
以下示例代码处理 CPU1 上的中断(本例中为硬编码):
#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; /* Initialize the sigevent */ SIGEV_INTR_INIT(&event); /* Set interrupt thread to run on CPU 1 */ int cpu = 1; unsigned runmask = (1 << cpu); /* Set runmask so that the interrupt always runs on the specified CPU */ if (ThreadCtl(_NTO_TCTL_RUNMASK_GET_AND_SET, &runmask) == -1) { printf("Unable to specify runmask for CPU %d", cpu); return -1; } // Set up the event intId = InterruptAttachEvent(HW_SERIAL_IRQ, &event, 0); for (;;) { // Wait for an interrupt event (could use MsgReceive() instead) // Note: If you want to use MsgReceive(), you must make the sigevent a pulse // sigevent, create a private channel, and then create a connection to // the channel to send the pulse (to yourself). For production code, // consider also using MsgRegisterEvent() InterruptWait (0, NULL); /* * Determine the source of the interrupt, reading the Interrupt Identification Register * to clear it */ iir = in8(base_reg + REG_II) & IIR_MASK; // Unmask the interrupt, so that we can get the next event InterruptUnmask(HW_SERIAL_IRQ, intId); /* no interrupt? */ if (iir & 1) { /* Then wait again for next */ continue; } /* * Determine which interrupt source caused the interrupt and determine if we need to do with 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); }
二、Attaching and detaching interrupts
为了安装 ISR,软件必须告诉操作系统它希望将 ISR 与特定的中断源关联,该中断源可以是硬件中断请求线 (IRQ) 或多个软件中断之一。
实际中断数量取决于主板制造商提供的硬件配置。有关特定主板的中断分配,请参阅该主板 BSP 中的构建文件。
无论如何,线程使用 InterruptAttach() 或 InterruptAttachEvent() 函数调用指定它希望将哪个中断源与哪个 ISR 关联;当软件希望将 ISR 与中断源分离时,它可以调用 InterruptDetach()。例如:
#define IRQ3 3 /* A forward reference for the handler */ extern const sigevent *serint(void *, int); … /* * Associate the interrupt handler, serint, * with IRQ 3, the 2nd PC serial port */ id = InterruptAttach(IRQ3, serint, NULL, 0, 0); … /* Perform some processing. */ … /* Done; detach the interrupt source. */ InterruptDetach(id);
注意:启动代码负责确保在系统初始化期间屏蔽所有中断源。当第一次调用 InterruptAttach() 或 InterruptAttachEvent() 来处理中断向量时,内核会取消屏蔽。同样,当最后一次调用 InterruptDetach() 来处理中断向量时,内核会重新屏蔽中断线。
由于中断处理程序可能会获得对机器的控制权,因此我们不允许任何人将中断与 ISR 代码关联。进程必须启用 PROCMGR_AID_INTERRUPT 功能。有关更多信息,请参阅 C 库参考中的 procmgr_ability()。
附加 ISR 会锁定进程的内存(请参阅系统架构指南“Process Manager”一章中的“Locking memory”)。这有助于减少延迟;我们不想在 ISR 中处理页面错误。
三、Interrupt Service Routine (ISR)
在我们上面的示例中,函数 serint() 是 ISR。
通常,ISR 负责:
(1) 确定哪个硬件设备需要服务(如果有的话)
(2) 对该硬件执行某种服务(通常只需读取和/或写入硬件的寄存器即可)
(3) 更新 ISR 和应用程序中运行的某些线程之间共享的某些数据结构
(4) 通知应用程序发生了某种事件
根据硬件设备、ISR 和应用程序的复杂性,可以省略上述某些步骤。
(QNX Neutrino 7.1 或更高版本)内核在进入和离开 ISR 时保存和恢复 FPU 上下文,因此在其中使用浮点运算是安全的。
让我们依次看看这些步骤。
确定中断源
根据您的硬件配置,实际上可能有多个硬件源与中断相关联。此问题取决于您的特定硬件和总线类型。此特性(加上良好的编程风格)要求您的 ISR 确保与其相关联的硬件确实导致了中断。
服务硬件
上述讨论可能会让您得出这样的结论:“电平敏感是好的;边沿触发是坏的。”但是,另一个问题出现了。
更新通用数据结构
使用中断时出现的另一个问题是如何安全地更新 ISR 和应用程序中的线程之间正在使用的数据结构。
通知应用程序代码
由于 ISR 运行的环境非常有限,通常您需要在线程级别执行大多数(如果不是全部)实际“服务”操作。
1. Determining the source of the interrupt
根据您的硬件配置,实际上可能有多个硬件源与中断相关联。此问题取决于您的特定硬件和总线类型。此特性(加上良好的编程风格)要求您的 ISR 确保与其相关联的硬件确实导致了中断。
大多数 PIC(可编程中断控制器)芯片都可以编程为以边缘敏感或电平敏感的方式响应中断。根据此编程,中断可能是可共享的。
例如:
Figure 1. Interrupt request assertion with multiple interrupt sources.
在上述情况下,如果 PIC 以电平敏感模式运行,则只要 IRQ 为高电平,就认为该 IRQ 处于活动状态。在此配置中,虽然第二次断言(步骤 2)本身不会引起新的中断,但即使中断的原始原因被消除(步骤 3),中断仍会被视为处于活动状态。直到清除最后一个断言(步骤 4)后,中断才会被视为处于非活动状态。
在边沿触发模式下,中断仅在步骤 1 处“被注意到”一次。只有当中断线被清除并重新断言时,PIC 才会认为发生了另一个中断。
QNX Neutrino 允许 ISR 处理程序堆叠,这意味着多个 ISR 可以与一个特定的 IRQ 相关联。这样做的影响是链中的每个处理程序都必须查看其关联的硬件并确定它是否导致了中断。这在电平敏感环境中可靠地工作,但在边沿触发环境中则不行。
为了说明这一点,请考虑两个硬件设备共享一个中断的情况。我们将这些设备称为“HW-A”和“HW-B”。两个 ISR 例程按顺序连接到一个中断源(通过 InterruptAttach() 或 InterruptAttachEvent() 调用)(即,ISR-A 在链中首先连接,ISR-B 其次连接)。
现在,假设 HW-B 首先断言中断线。QNX Neutrino 检测到中断并按顺序调度两个处理程序。ISR-A 首先运行并(正确地)决定其硬件没有导致中断。然后 ISR-B 运行并(正确地)决定其硬件确实导致中断;然后它开始处理中断。但在 ISR-B 清除中断源之前,假设 HW-A 断言中断;发生的情况取决于 IRQ 的类型:
边沿触发 IRQ:
如果您有边沿触发总线,当 ISR-B 清除中断源时,IRQ 线仍保持活动状态(由 HW-A 保持)。但由于它是边沿触发的,PIC 正在等待下一次清除/断言转换,然后才决定是否发生了另一个中断。由于 ISR-A 已经运行,它不可能再次运行以真正清除中断源。结果是系统“挂起”,因为中断永远不会再次在清除和断言之间转换,因此该 IRQ 线上的任何其他中断都永远不会被识别。
电平敏感 IRQ:
在电平敏感总线上,当 ISR-B 清除中断源时,IRQ 线仍保持活动状态(由 HW-A 保持)。当 ISR-B 完成运行并且 QNX Neutrino 向 PIC 发送 EOI(中断结束)命令时,PIC 会立即重新中断内核,从而导致 ISR-A(然后是 ISR-B)运行。
由于 ISR-A 清除了中断源(并且 ISR-B 不执行任何操作,因为其相关硬件不需要维修),因此一切都按预期运行。
TODO: 没看懂。
2. Servicing the hardware
上述讨论可能会让您得出这样的结论:“电平敏感是好的,边沿触发是坏的”。然而,另一个问题出现了。
在电平敏感的环境中,您的 ISR 必须在完成之前清除中断源(或至少通过 InterruptMask() 屏蔽它)。(如果没有,那么当内核向 PIC 发出 EOI 时,PIC 将立即重新发出处理器中断,内核将永远循环,不断调用您的 ISR 代码。)
在边沿触发的环境中,没有这样的要求,因为中断在从清除转变为断言之前不会再次被注意到。
通常,要实际服务中断,您的 ISR 必须做很少的事情;它能做的最少的事情是清除中断源,然后安排一个线程来实际处理中断。这是推荐的方法,原因如下:
(1) ISR 完成和线程执行之间的上下文切换时间非常短 - 通常约为几微秒。
(2) ISR 本身可以执行的函数类型非常有限(除了下面列出的函数外,不调用任何内核函数的函数)。
(3) ISR 的运行优先级高于系统中的任何软件优先级——ISR 消耗大量处理器会对 QNX Neutrino RTOS 的实时性产生负面影响。
注意: 由于连接到中断源的硬件范围可能非常广泛,因此服务中断的具体方法超出了本文档的范围——这实际上取决于您的硬件要求您做什么。
安全函数
当 ISR 正在服务中断时,它不能进行任何内核调用(除了我们稍后将讨论的几个)。这意味着您需要小心在 ISR 中调用的库函数,因为它们的底层实现可能会使用内核调用。
2.1 Safe functions
当 ISR 正在处理中断时,它不能进行任何内核调用(除了我们稍后将讨论的几个)。这意味着您需要小心在 ISR 中调用的库函数,因为它们的底层实现可能使用内核调用。
注:有关可以从 ISR 调用的函数的列表,请参阅 C 库参考中的完整安全信息附录。
以下是 ISR 可以使用的唯一内核调用:
InterruptMask()
InterruptUnmask()
TraceEvent()
您还会发现这些函数(不是内核调用)在 ISR 中很有用:
InterruptEnable()(不推荐)
InterruptDisable()(不推荐)
InterruptLock()
InterruptUnlock()
让我们看看这些函数。
为了防止线程和 ISR 相互干扰,您需要告诉内核禁用中断。在单处理器系统上,您只需使用处理器的“disable interrupts”操作码即可禁用中断。但在 SMP 系统上,在一个处理器上禁用中断不会在另一个处理器上禁用它们。
函数 InterruptDisable()(InterruptEnable()的反向函数)在单处理器系统上执行此操作。函数 InterruptLock()(以及反向函数 InterruptUnlock())在 SMP 系统上执行此操作,但将其与自旋锁相结合以在核心之间进行同步。
注:我们建议您始终使用这些函数的 SMP 版本 — 这样可以使您的代码可移植到 SMP 系统,并且开销可以忽略不计。
InterruptMask() 和 InterruptUnmask() 函数禁用和启用 PIC 对特定硬件 IRQ 线的识别。如果您的中断处理程序 ISR 由内核通过 InterruptAttachEvent() 提供,或者您无法在电平敏感的环境中快速清除中断原因,则这些调用非常有用。(如果清除中断源很耗时,通常就会出现这种情况 — 您不想在中断处理程序中花费大量时间。)在这种情况下,ISR 将调用 InterruptMask() 并安排一个线程来执行实际工作。线程在清除中断源后将调用 InterruptUnmask()。
请注意,这两个函数是计数函数;InterruptUnmask() 的调用次数必须与 InterruptMask() 相同,才能使中断源再次被视为启用。
TraceEvent() 函数跟踪内核事件;您可以在中断处理程序中调用它,但有一些限制。有关更多信息,请参System Analysis Toolkit in User's Guide。
3. Updating common data structures
使用中断时出现的另一个问题是如何安全地更新 ISR 和应用程序中的线程之间正在使用的数据结构。
两个重要特性值得重复:
(1) ISR 的运行优先级高于任何软件线程。
(2) ISR 无法发出内核调用(除非另有说明)。
这意味着您不能在 ISR 中使用线程级同步(例如互斥体、condvar 等)。
由于 ISR 的运行优先级高于任何软件线程,因此线程必须保护自己免受 ISR 引起的任何抢占。因此,线程应该在任何关键数据操作操作周围发出 InterruptLock() 和 InterruptUnlock() 调用。由于这些调用有效地关闭了中断,因此线程应该将数据操作保持在最低限度。
对于 SMP,还有一个额外的考虑因素:一个处理器可能正在运行 ISR,而另一个处理器可能正在运行与 ISR 相关的线程。在 SMP 系统上,这些函数获取和释放自旋锁以添加跨核心同步。再次强调,在非 SMP 系统上使用这些函数是安全的;它们的工作方式与 InterruptDisable() 和 InterruptEnable() 一样,尽管性能损失很小。
在某些情况下,可以使用另一种解决方案来至少保证对数据元素的原子访问,即使用 atomic_*() 函数调用;请参阅“Atomic operations”。
在 ISR 和正常线程处理中访问的变量必须标记为 volatile。
4. Notifying the application code
InterruptAttach() 函数将 IRQ 向量 (intr) 与 ISR 处理程序 (handler) 关联,并向其传递一个通信区域 (area)。
size 和 flags 参数与我们在此的讨论无关(它们在 InterruptAttach() 函数的 C 库参考中进行了描述)。
对于 ISR,handler() 函数采用 void * 指针和 int 标识参数;它返回一个 const struct sigevent * 指针。void * area 参数是提供给 InterruptAttach() 函数的值 — 您在 InterruptAttach() 的 area 参数中输入的任何值都将传递给您的 handler() 函数。(这只是将中断处理程序 ISR 与某个数据结构耦合的一种便捷方式。如果您愿意,当然可以自由地传递 NULL 值。)
在从硬件读取一些寄存器或完成服务所需的任何处理后,ISR 可能会或可能不会决定安排一个线程来实际执行工作。为了调度线程,ISR 只需返回一个指向 const struct sigevent 结构的指针 — 内核查看该结构并将事件传递到目标。(有关可返回的事件类型的讨论,请参阅 QNX Neutrino C 库参考中的 sigevent。)如果 ISR 决定不调度线程,则只需返回 NULL 值。
如 sigevent 文档中所述,返回的事件可以是信号或脉冲。您可能会发现信号或脉冲令人满意,特别是如果您出于其他原因已经拥有信号或脉冲处理程序。
但请注意,对于 ISR,我们也可以返回 SIGEV_INTR 事件。这是一个特殊事件,实际上只对 ISR 及其关联的控制线程有意义。
从线程级别服务中断的一种非常简单、优雅且快速的方法是让一个线程专门用于中断处理。线程附加中断(通过 InterruptAttach()),然后线程阻塞,等待 ISR 告诉它执行某项操作。阻塞是通过 InterruptWait() 调用实现的。此调用阻塞,直到 ISR 返回 SIGEV_INTR 事件:
struct sigevent event; main () { // perform initializations, etc. … // start up a thread that is dedicated to interrupt processing pthread_create (NULL, NULL, int_thread, NULL); … // perform other processing, as appropriate … } // this thread is dedicated to handling and managing interrupts void *int_thread (void *arg) { /* Enable the INTERRUPT ability */ procmgr_ability(0, PROCMGR_ADN_ROOT|PROCMGR_AOP_ALLOW|PROCMGR_AID_INTERRUPT, PROCMGR_AID_EOL); … // initialize the hardware, etc. … // initialize the event as type SIGEV_INTR SIGEV_INTR_INIT( &event ); // attach the ISR to IRQ 3 InterruptAttach (IRQ3, isr_handler, NULL, 0, 0); … // perhaps boost this thread's priority here … // now service the hardware when the ISR says to while (1) { InterruptWait (NULL, NULL); // at this point, when InterruptWait unblocks, // the ISR has returned a SIGEV_INTR, indicating // that some form of work needs to be done. … // do the work … // if the isr_handler did an InterruptMask, then // this thread should do an InterruptUnmask to // allow interrupts from the hardware } } // this is the ISR const struct sigevent *isr_handler (void *arg, int id) { // look at the hardware to see if it caused the interrupt // if not, simply return (NULL); … // in a level-sensitive environment, clear the cause of // the interrupt, or at least issue InterruptMask to // disable the PIC from reinterrupting the kernel … // return a pointer to an event structure (preinitialized // by int_thread()) that contains SIGEV_INTR as its notification type. // This causes the InterruptWait() in int_thread() to unblock. return (&event); }
4.1 Using InterruptAttach()
在上面的代码示例中,我们看到了一种典型的中断处理方式。主线程创建一个特殊的中断处理线程 (int_thread())。该线程的唯一任务是在线程级别处理中断。中断处理线程将 ISR 附加到中断 (isr_handler()),然后等待 ISR 告诉它执行某项操作。ISR 通过返回一个事件结构来通知(解除阻塞),并将通知类型设置为 SIGEV_INTR。
与使用 SIGEV_SIGNAL 或 SIGEV_PULSE 事件通知类型相比,此方法具有许多优势:
(1) 应用程序不必进行 MsgReceive() 调用(这需要等待脉冲)。
(2) 应用程序不必处理信号(这可能很混乱)。
(3) 如果中断服务至关重要,则应用程序可以创建具有高优先级的 int_thread() 线程;当 isr_handler() 函数返回 SIGEV_INTR 时,如果 int_thread() 函数具有足够的优先级,它将立即运行。没有延迟,例如,在 ISR 发送脉冲和另一个线程最终调用 MsgReceive() 来获取脉冲之间的时间可能会有延迟。
使用 InterruptWait() 时唯一需要注意的警告是,附加中断的线程必须等待 SIGEV_INTR。
4.2 Using InterruptAttachEvent()
上面关于 InterruptAttach() 的大部分讨论都适用于 InterruptAttachEvent() 函数,但 ISR 显然是个例外。在这种情况下,您无需提供 ISR;内核会注意到您调用了 InterruptAttachEvent() 并自行处理中断。由于您还将 struct sigevent 绑定到 IRQ,因此内核现在可以分派事件。主要优点是我们避免了上下文切换到 ISR 并返回。
为了附加和中断事件,您必须启用 PROCMGR_AID_INTERRUPTEVENT 功能。有关更多信息,请参阅 C 库参考中的 procmgr_ability()。
需要注意的一点是,内核会在中断处理程序中自动执行 InterruptMask()。因此,当您在中断处理线程中实际清除中断源时,您需要执行 InterruptUnmask()。这就是 InterruptMask() 和 InterruptUnmask() 被计算的原因。
五、Running out of interrupt events
如果您正在处理中断,您可能会看到“Out of Interrupt Events(中断事件不足)”错误。
当系统不再能够运行用户代码并卡在内核中时,就会发生这种情况,最常见的原因是:
(1) 中断负载对于 CPU 来说太高(它花费所有时间处理中断)。
或:
(2) 有一个中断处理程序(与 InterruptAttach() 连接的,而不是 InterruptAttachEvent())没有正确清除设备的中断条件(导致上述情况)。
如果您在代码中调用 InterruptAttach(),请先查看处理程序代码,并确保在返回操作系统之前正确清除了设备的中断条件。
如果您遇到此问题,并且您确定所有中断处理程序都正常,则可能是由 BSP 中的中断调用损坏引起的。
六、Problems with shared interrupts
不同的设备可以共享一个中断(例如,如果您的硬件中断线用完了),但我们不建议您对会产生大量中断的硬件这样做。我们还建议您不要与您无法完全控制源代码的驱动程序共享中断,因为您需要确保驱动程序正确处理中断。
共享中断会降低您的性能,因为当中断触发时,共享中断的所有设备都需要运行并检查是否是为他们准备的。许多驱动程序会读取其中断处理程序中的寄存器,以查看中断是否真的是为他们准备的,如果不是,则忽略它。但有些驱动程序不会这样做;它们会安排线程级事件处理程序来检查硬件,这效率低下并会降低性能。
注:如果您有一个频繁的中断源与一个安排线程来检查硬件的驱动程序共享中断,则安排线程的开销会变得非常明显。
共享中断可能会增加中断延迟,具体取决于每个驱动程序的具体操作。中断触发后,内核不会重新启用该中断,直到所有驱动程序处理程序都通知内核它们已完成处理。如果一个驱动程序需要很长时间来处理被屏蔽的共享中断,而同一中断上的另一个设备在该时间段内引发中断,则该中断的处理可能会延迟一段未知的时间。
七、Advanced topics
现在我们已经了解了处理中断的基础知识,让我们来看看更多细节和一些高级主题。
中断环境
当您的 ISR 运行时,它会在附加它的进程的上下文中运行,只是堆栈不同。
共享中断的顺序
中断延迟
实时系统关注的另一个因素是硬件中断生成和 ISR 执行第一行代码之间所花费的时间。
原子操作
定义了一些便利函数,允许您执行原子操作(即保证不可分割或不可中断的操作)。使用这些函数可以减轻围绕某些小型、定义明确的变量操作禁用和启用中断的需要。
1. Interrupt environment
当您的 ISR 运行时,它会在附加它的进程的上下文中运行,只是堆栈不同。
由于内核使用内部中断处理堆栈来处理硬件中断,因此您的 ISR 会受到影响,因为内部堆栈很小。通常,您可以假设您有大约 200 个字节可用。
直到所有 ISR(无论是由您的代码通过 InterruptAttach() 提供的,还是由内核提供的,如果您使用 InterruptAttachEvent()的话)都运行完之后,PIC 才会获得 EOI 命令。然后内核本身发出 EOI;您的代码不应该发出 EOI 命令。
2. Ordering of shared interrupts
如果您使用中断共享,则默认情况下,当您使用 InterruptAttach() 或 InterruptAttachEvent() 附加 ISR 时,新的 ISR 将转到该中断的 ISR 列表的开头。您可以通过指定 _NTO_INTR_FLAGS_END 标志参数来明确要求将您的 ISR 放在列表末尾。
请注意,无法指定任何其他顺序(例如中间、第五、第二等)。
3.Interrupt latency
实时系统需要关注的另一个因素是从生成硬件中断到 ISR 执行第一行代码所花费的时间。
这里有两个因素需要考虑:
(1) 如果系统中的任何线程调用 InterruptDisable() 或 InterruptLock(),则在发出 InterruptEnable() 或 InterruptUnlock() 函数调用之前不会处理任何中断。注: 不是每CPU的!
(2) 无论如何,如果启用了中断,内核将开始在短时间内执行第一个 ISR 的第一行(如果多个 ISR 与一个中断相关联)(例如,在 x86 上少于 21 个 CPU 指令)。
4. Atomic operations
定义了一些便利函数,允许您执行原子操作(即保证不可分割或不可中断的操作)。使用这些函数可以减少在某些小型、定义明确的变量操作周围禁用和启用中断的需要。
QNX Neutrino 在 <atomic.h> 中定义以下内容:
atomic_add()
atomic_add_value()
atomic_clr()
atomic_clr_value()
atomic_set()
atomic_set_value()
atomic_sub()
atomic_sub_value()
atomic_toggle()
atomic_toggle_value()
C11 在 <stdatomic.h> 中定义这些:
atomic_compare_exchange_strong(), atomic_compare_exchange_strong_explicit(), atomic_compare_exchange_weak(), atomic_compare_exchange_weak_explicit()
atomic_exchange(), atomic_exchange_explicit()
atomic_fetch_add(), atomic_fetch_add_explicit()
atomic_fetch_and(), atomic_fetch_and_explicit()
atomic_fetch_or(), atomic_fetch_or_explicit()
atomic_fetch_sub(), atomic_fetch_sub_explicit()
atomic_fetch_xor(), atomic_fetch_xor_explicit()
atomic_flag_clear(), atomic_flag_clear_explicit()
atomic_flag_test_and_set(), atomic_flag_test_and_set_explicit()
atomic_init()
atomic_is_lock_free()
atomic_load(), atomic_load_explicit()
atomic_signal_fence()
atomic_store(), atomic_store_explicit()
atomic_thread_fence()
注:这些函数都有超链接进行说明的。
请参阅 QNX Neutrino C 库参考以了解更多信息。
posted on 2024-06-03 10:19 Hello-World3 阅读(159) 评论(0) 编辑 收藏 举报