QNX Neutrino实时操作系统 > QNX Neutrino入门:实时程序员指南

![[上一个]](prev.gif) ![[内容]](contents.gif) ![[索引]](keyword_index.gif) ![[下一个]](next.gif)


中断

本章内容包括:

Neutrino和中断

#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){
       perro (NULL);
       exit (EXIT_FAILURE);
   }
   …
   return (EXIT_SUCCESS);
}


volatile int serial_msr;
volatile int serial_rx;
volatile int serial_lsr;
const struct sigevent *
   intHandler (void * arg, int id){
   int iir;
   struct sigevent *event = (struct sigevent *)arg;
   iir = in8 (base_reg + REG_II) & IIR_MASK;
   if ( iir & 1) return (NULL);
   switch (iir){
       case IIR_MSR:
           serial_msr = in8 (base_reg + REG_MS);
           return (event);        break;
       case IIR_THE:        break;
       case IIR_RX:
           serial_rx = in8 (base_reg + REG_RX);    break;
       case IIR_LSR:
           serial_lsr = in8 (base_reg + REG_LS);    break;
       default:    break;
   }
   return (NULL);
}


int main (int argc, char ** argv){
   int intID;
   int iir;
   int serial_msr; //Modem status register
   int serial_rx; 
   int serial_lsr; //line status register
   …
   intID = InterruptAttachEvent (HW_SERIAL_IRQ, &event, 0);
   for (; ;){
       InterruptWait (0, NULL);
       iir = in8 (base_reg + REG_II) & IIR_MASK;
       InterruptUnmask ( HW_SERIAL_IRQ, intID);
        if ( iir & 1)  continue;
       switch (iir){
              case IIR_MSR:
           serial_msr = in8 (base_reg + REG_MS);
           … //perform here
           break;
              case IIR_THE:        break;
              case IIR_RX:
           serial_rx = in8 (base_reg + REG_RX);    break;
             case IIR_LSR:
           serial_lsr = in8 (base_reg + REG_LS);    break;
             default:    break;
       }
   }
   return (0);
}

在本节中,我们将研究中断,在Neutrino下如何处理它们,它们对调度和实时性的影响以及一些中断管理策略。

我们首先要问的是,“什么是中断?”

中断的确切含义是—中断正在发生的一切,并转移到另一个任务。

例如,假设您坐在办公桌前从事“ A”工作。突然,电话响了。非常重要的客户(VIC)需要您立即回答一些技能测试问题。回答问题后,您可能会回到工作“ A”的工作,或者VIC可能已更改了优先级,因此您将工作“ A”推到一边,然后立即开始工作“ B”。

现在让我们将其放在Neutrino下。

在任何时候,处理器都在忙于处理优先级最高的READY线程(这是处于RUNNING状态的线程)的工作。要引起中断,计算机总线上的一块硬件会发出一条中断线(以我们的类比,这是电话响铃)。

一旦中断线被断言,内核就会跳转到一段代码,该代码设置环境以运行中断服务程序(ISR),这是一个软件,它确定检测到该中断时应发生的情况。

在硬件声明中断线与执行ISR的第一条指令之间经过的时间称为中断等待时间。中断等待时间以微秒为单位。不同的处理器具有不同的中断等待时间。它是处理器速度,缓存体系结构,内存速度以及操作系统效率的函数。

以我们的类比来说,如果您正在听耳机中的某些音乐而忽略了正在响铃的电话,则可能需要更长的时间才能注意到此电话“中断”。在中微子的统治下,同样的事情可能发生。有一个禁止中断的处理器指令(cli例如在x86上)。处理器在重新启用中断之前不会注意到任何中断(在x86上,这是sti操作码)。

为了避免特定于CPU的汇编语言调用,Neutrino提供了以下调用:InterruptEnable()InterruptDisable()以及InterruptLock()InterruptUnlock()。这些负责所有受支持平台上的所有低级详细信息。

ISR通常执行尽可能少的工作,然后结束(按照我们的类比,这是与VIC在电话上进行的对话-我们通常不让客户搁置并进行数小时的工作;我们只是告诉客户,“好吧,我会马上解决!”)。当ISR结束时,它可以告诉内核什么都不应该发生(意味着ISR已经完全处理了该事件,并且不需要执行任何其他操作),或者内核应该执行一些可能导致线程变为READY的操作。 。

以我们的类比,告诉内核已处理了中断,就像告诉客户答案—我们可以返回到正在做的事情,知道客户已经回答了他们的问题。

告诉内核需要执行一些操作,就像告诉客户您将尽快联系他们—电话已挂断,但它可能会再次振铃。

中断服务程序

ISR是一段代码,负责清除中断源。

这是一个关键点,尤其是结合以下事实:中断以高于任何软件优先级的优先级运行。这意味着花在ISR中的时间量可能会对线程调度产生严重影响。您应该在ISR中花费尽可能少的时间。让我们更深入地研究一下。

清除中断源

产生中断的硬件设备将保持中断线为有效,直到确定软件已处理该中断。由于硬件无法理解,因此软件必须在响应中断原因时告诉它。通常,这是通过从特定的硬件端口读取状态寄存器或从特定的存储位置读取数据块来完成的。

无论如何,在硬件和软件之间通常存在某种形式的肯定确认,以“取消声明”中断线路。(有时没有确认;例如,一块硬件可能会产生一个中断,并假定软件会处理该中断。)

由于中断的运行优先级高于任何软件线程,因此我们应在ISR本身中花费尽可能少的时间,以最大程度地减少对调度的影响。如果我们仅通过读取寄存器并可能将该值填充到全局变量中来清除中断源,那么我们的工作就很简单。

这是ISR对串行端口进行的处理。当字符到达时,串行端口硬件会产生一个中断。ISR处理程序读取包含该字符的状态寄存器,并将该字符填充到循环缓冲区中。做完了 总处理时间:几微秒。而且,它必须快速。考虑一下如果您接收到115 Kbaud的字符(大约每100 µs一个字符)会发生什么情况?如果您花了将近100 µs的时间处理中断,那么您将没有时间做其他任何事情!

但是,请不要让我误导您-串行端口的中断服务例程可能需要更长的时间才能完成。这是因为有一个后端民意测验,看是否有更多字符在设备中等待。

显然,在我们的类比中,将在中断中花费的时间最小化可以看作是“良好的客户服务”-通过将通话中的时间保持在最少,我们可以避免给其他客户一个忙碌的信号。

如果处理程序需要做大量工作怎么办?这有两种可能:

  • 清除中断源所需的时间很短,但是与硬件对话所需的工作量却很长(客户向我们询问了一个简短的问题,需要很长时间才能回答)。
  • 清除中断源所需的时间很长(客户对问题的描述很长且涉及很多)。

在第一种情况下,我们想尽可能快地清除中断源,然后告诉内核让一个线程来完成与速度较慢的硬件通信的实际工作。这样做的好处是,ISR仅花费极少量的时间来获得超高优先级,然后其余工作基于常规线程优先级完成。这类似于您接听电话(超高优先级),然后将实际工作委托给您的一位助手。在本章的后面,我们将看看ISR如何告诉内核安排其他人。

在第二种情况下,事情变得丑陋。如果ISR在退出时没有清除中断源,则内核将立即被可编程中断控制器(PIC,在x86上为8259或同等性能)芯片重新中断。

对于PIC爱好者:我们将在短期内讨论边缘敏感和电平敏感中断。

我们将继续运行ISR,而没有机会运行我们需要正确处理中断的线程级代码。

什么样的大脑损坏硬件需要很长时间才能清除中断源?基本的PC软盘控制器将保持中断状态,直到您读取了许多状态寄存器值为止。不幸的是,寄存器中的数据无法立即使用,因此您必须轮询此状态数据。这可能需要几毫秒的时间(以计算机术语来说很长)!

解决方案是临时屏蔽中断 —从字面上告诉PIC忽略来自此特定源的中断,直到您另行告知为止。在这种情况下,即使中断线是通过硬件置位的,PIC也会忽略它,也不会将其告知处理器。这使您的ISR可以调度线程以在ISR之外处理该硬件。当线程完成从硬件的数据传输后,它可以告诉PIC 取消屏蔽该中断。这样可以再次识别该硬件的中断。以我们的类比,这就像将VIC的呼叫转移给您的助手。

告诉线程做某事

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外部声明,或使用area参数从持久数据区传递到ISR,或static在ISR自身内部声明为事件。你的选择。如果返回事件,则内核将在ISR返回时将其传递给线程。因为该事件(通过脉冲来“警告”线程(如我们在消息传递中所述)章节或通过信号),这可能导致内核重新安排下一个获取CPU的线程。如果从ISR返回NULL,则内核知道在线程时不需要执行任何特殊操作,因此它不会重新计划任何线程-在ISR抢占它时正在运行的线程可以恢复运行。

水平敏感度与边缘敏感度

这两个是啥意思?。

我们还缺少另外一个难题。可以将大多数PIC编程为在电平敏感或边缘敏感模式下运行。

在电平敏感模式下,当中断线处于“开”状态时,它被视为由PIC断言。(这对应于下图中的标签“ 1”。)

电平敏感的中断声明。

我们可以看到,这将导致上述软盘控制器示例中描述的问题。只要ISR完成,内核就会告诉PIC:“好的,我已经处理了这个中断。告诉我下次它会被激活”(图中的第2步)。用技术术语来说,内核将中断结束(EOI)发送到PIC。PIC查看中断线,如果中断线仍处于活动状态,则会立即重新中断内核(步骤3)。

我们可以通过将PIC编程为边缘敏感模式来解决此问题。在这种模式下,PIC仅在活动的边沿上注意到中断。

边沿敏感中断声明。

即使ISR无法清除中断源,当内核将EOI发送到PIC(图中的第2步)时,PIC也不会重新中断内核,因为没有其他活动对象了。 EOI之后的边缘过渡。为了识别该线路上的另一个中断,该线路必须首先变为非活动状态(步骤4),然后才变为活动状态(步骤1)。

好吧,看来我们所有的问题都已经解决了!只需对所有中断使用边沿敏感。

不幸的是,边缘敏感模式有其自身的问题。

假设您的ISR无法清除中断原因。当内核向PIC发出EOI时,硬件仍将具有中断线。但是,由于PIC在边缘敏感模式下运行,因此它永远不会看到该设备的另一个中断。

现在什么样的bozo会写一个ISR忘记清除中断源?不幸的是,并不是那样干的。考虑一种情况,其中两个设备(例如SCSI总线适配器和以太网卡)在允许的硬件总线体系结构上共享同一条中断线。(现在,您问的是,“谁来设置这样的机器?!?”,这确实发生了,尤其是在PIC上的中断源数量不足的情况下!)在这种情况下,有两个ISR例程将被附加到相同的中断向量(顺便说一句,这是合法的),并且每当它从PIC获得该硬件中断级别的中断时,内核就会依次调用它们。

共享中断—一次中断。

在这种情况下,由于在关联的ISR运行时只有一个硬件设备处于活动状态(SCSI设备),因此它可以正确清除中断源(步骤2)。请注意,无论如何,内核都会运行以太网设备的ISR(在步骤3中),因为它不知道以太网硬件是否也需要服务,因此它始终运行整个链。

但是考虑这种情况:

共享中断-一次共享多个。

这就是问题所在。

以太网设备首先中断。这导致中断线被断言(PIC记录了活动沿),内核调用了链中的第一个中断处理程序(SCSI磁盘驱动器;图中的步骤1)。SCSI磁盘驱动器的ISR查看了其硬件,并说:“不,不是我。哦,好吧,请忽略它”(第2步)。然后内核调用了链中的下一个ISR,即以太网ISR(第3步)。以太网ISR查看了硬件,说:“嘿!那是我触发中断的硬件。我要清除它。” 不幸的是,在清除它时,SCSI设备产生了一个中断(步骤4)。

以太网ISR完成清除中断源后(步骤5),由于SCSI硬件设备,中断线仍然有效。但是,以边沿敏感模式编程的PIC在识别另一个中断之前正在寻找一个非活动到活动的转换(在复合线上)。那不会发生,因为内核已经调用了两个中断服务程序,现在正在等待来自PIC的另一个中断。

在这种情况下,级别敏感的解决方案将是合适的,因为当以太网ISR完成并且内核向PIC发出EOI时,PIC会发现一个事实,即总线上仍然有一个中断处于活动状态并重新中断内核。然后,内核将遍历ISR链,这一次SCSI驱动程序将有机会运行并清除中断源。

对边缘敏感还是对电平敏感的选择取决于硬件和启动代码。某些硬件仅支持其中一种。支持任何一种模式的硬件将由启动代码编程为另一种。您必须查阅系统随附的BSP(板级支持软件包)文档才能获得明确的答案。

编写中断处理程序

让我们看看如何设置中断处理程序-调用,特征和一些策略。

附加中断处理程序

要附加到中断源,可以使用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参数指定其打断你希望到指定的处理程序附加到。传递的值由启动代码定义,该启动代码在Neutrino启动之前就初始化了PIC(还有其他功能)。(您的Neutrino文档中有关于启动代码的更多信息;请参见“ 实用工具参考 ”下的startup-*;例如,startup-p5064

此时,两个函数InterruptAttach()和InterruptAttachEvent()不同。首先让我们来看一下InterruptAttachEvent()。然后我们回到InterruptAttach()。

附加InterruptAttachEvent()

所述InterruptAttachEvent()函数有两个额外的参数:参数事件,这是一个指向struct sigevent应该被递送,并且一个标志参数。InterruptAttachEvent()告诉内核,每当检测到中断时都应返回该事件,并且应该屏蔽中断级别。请注意,由内核解释事件并指出应将哪个线程准备就绪。

使用_InterruptAttach()_附加

使用InterruptAttach(),我们可以指定一组不同的参数。该处理器参数是要调用的函数的地址。从原型可以看到,handler()返回一个struct sigevent,指示要返回哪种事件,并带有两个参数。第一个传递的参数是area,它只是传递给InterruptAttach()的area参数。第二个参数id是中断的标识,也是从InterruptAttach()返回的值。这用于识别中断以及屏蔽,取消屏蔽,锁定或解锁中断。第四个参数InterruptAttach()是大小,这表明有多大(以字节为单位),您通过在数据区面积为。最后,flags 参数与为_InterruptAttachEvent()_传递的参数相同;我们将在稍后讨论。

现在您已经附加了一个中断

至此,您已经调用了InterruptAttachEvent() 或InterruptAttach()。

由于附加中断并不是您希望每个人都能做的事情,因此Neutrino仅允许启用了“ I / O特权”的线程来执行中断(请参见Neutrino库参考中的_ThreadCtl()_函数)。只从正在运行的线程root的帐户或那些_的setuid()_root能够获得“I / O特权”; 因此,我们实际上将这种能力限制为root

这是一个将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共享同一个中断,情况就是这样)。

分离中断处理程序

完成ISR后,我们可能希望打破ISR与中断向量之间的关联:

int
InterruptDetach (int id);

我之所以说“可以”是因为处理中断的线程通常在服务器中找到,并且服务器通常永远挂在身边。因此可以想象,结构良好的服务器永远不会发出InterruptDetach()函数调用。同样,当线程或进程终止时,操作系统将删除该线程或进程可能与之关联的所有中断处理程序。因此,简单地从main()的末尾掉落,调用exit()或由于SIGSEGV而退出,将自动将您的ISR与中断向量分离。(当然,您可能希望更好地处理此问题,并阻止设备生成中断。如果另一个设备正在共享中断,则没有两种解决方法–您必须 清理,否则,如果在边缘敏感模式下运行,您将不会再有任何中断,或者,如果在级别敏感模式下运行,则将不断得到大量的ISR分派。)

继续上面的示例,如果要分离,将使用以下代码:

void
terminateInterrupts (void)
{
    InterruptDetach (interruptID);
}

如果这是与该中断向量关联的最后一个ISR,则内核将自动在PIC级别屏蔽中断源,以便它不会生成中断。

该标志参数

最后一个参数flags控制所有事情:

  • _NTO_INTR_FLAGS_END

表明该处理器应该去后是可以连接到相同的中断源等处理。

  • _NTO_INTR_FLAGS_PROCESS

指示此处理程序与进程而不是线程相关联。归结为,如果指定此标志,则在进程退出时,中断处理程序将自动与中断源分离。如果未指定此标志,则首先创建关联的线程退出时,中断处理程序将与中断源分离。

  • _NTO_INTR_FLAGS_TRK_MSK

指示内核应跟踪屏蔽中断的次数。这会给内核带来更多的工作,但是需要确保在进程或线程退出时有序地取消屏蔽中断源。

中断服务程序

让我们看一下ISR本身。在第一个示例中,我们将研究如何使用 _InterruptAttach()函数。然后,除了InterruptAttachEvent()_之外,我们将看到完全相同的东西。

使用InterruptAttach()

继续我们的示例,这是ISR intHandler()。它查看我们假定已连接到HW_SERIAL_IRQ的8250串行端口芯片:

/*
 * int1.c
*/

#include <stdio.h>
#include <sys/neutrino.h>

#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

volatile int serial_msr;   // saved contents of Modem Status Reg
volatile int serial_rx;    // saved contents of RX register
volatile int serial_lsr;   // saved contents of Line Status Reg
static int base_reg = 0x2f8;

const struct sigevent *
intHandler (void *arg, int id)
{
    int  iir;
    struct sigevent *event = (struct sigevent *)arg;

    /*
     * determine the source of the interrupt
     * by reading the Interrupt Identification Register
    */

    iir = in8 (base_reg + REG_II) & IIR_MASK;

    /* no interrupt? */
    if (iir & 1) {
        /* then no event */
        return (NULL);
    }

    /*
     * figure out which interrupt source caused the interrupt,
     * and determine if a thread needs to do something about it.
     * (The constants are based on the 8250 serial port's interrupt
     * identification register.)
    */

    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)中断的情况下,我们才希望传递事件(该事件由变量event标识,该事件在附加时方便地传递给ISR)。在所有其他情况下,我们将忽略中断(并更新一些全局变量)。但是,在所有情况下,我们都会清除中断源。这是通过in8()读取I / O端口来完成的。

使用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()处理了上述决定(如上),方法是返回a struct sigevent表示应该发生某种情况,或者返回常量NULL。注意为InterruptAttachEvent()_修改代码时对代码所做的更改:

  • 现在,“ ISR”工作在main()的线程时间完成。
  • 收到事件后,我们必须始终取消屏蔽中断源(因为内核会为我们屏蔽该中断源)。
  • 如果中断对我们不重要,我们什么也不做,只是在for语句中再次循环,等待另一个中断。
  • 如果中断对我们很重要,我们将直接处理(case IIR_MSR部分)。

您决定清除中断源的位置取决于您的硬件和所选的通知方案。结合SIGEV_INTR和InterruptWait(),内核不会“排队”多个通知;使用SIGEV_PULSE和MsgReceive(),内核将使所有通知排队。如果您使用信号(例如SIGEV_SIGNAL),则可以定义是否将信号排队。对于某些硬件方案,可能需要先清除中断源,然后才能从设备中读取更多数据。与其他硬件相比,在声明中断时,您不必而且可以读取数据。

一个返回SIGEV_THREAD的ISR是使我充满恐惧的一种情况!我建议尽可能避免使用此“功能”。

在上面的串行端口示例中,我们决定使用InterruptWait(),它将对一个条目进行排队。读取中断识别寄存器后,串行端口硬件可能会立即声明另一个中断,但这很好,因为最多会有一个SIGEV_INTR排队。我们将在下一个循环迭代中获取此通知for

InterruptAttach()与InterruptAttachEvent()

这自然使我们想到了一个问题:“为什么我要在另一个上使用一个?”

_InterruptAttachEvent()最明显的优点是,它比InterruptAttach()更易于使用-没有ISR例程(因此无需调试)。另一个优点是,由于内核空间中没有运行(就像ISR例程那样),因此没有崩溃整个系统的危险。如果确实遇到编程错误,则该过程将崩溃,而不是整个系统崩溃。但是,取决于您要实现的目标,它可能比InterruptAttach()_效率更高或更低。这个问题非常复杂,以至于将其简化为几个词(例如“更快”或“更好”)可能就不够了。我们将需要看一些图片和场景。

这是我们使用_InterruptAttach()_时发生的情况:

使用_ InterruptAttach()_控制流。

当前正在运行的线程(“ thread1”)被中断,我们进入了内核。内核保存“ thread1”的上下文。然后内核进行查找以查看谁负责处理中断,并确定“ ISR1”负责。此时,内核为“ ISR1”设置上下文并转移控制权。“ ISR1”查看硬件并决定返回a struct sigevent。内核会注意到返回值,找出需要处理的人,并使它们准备就绪。这可能导致内核安排另一个线程“ thread2”运行。

现在,我们来对比一下使用_InterruptAttachEvent()_时发生的情况:

使用_ InterruptAttachEvent()_控制流。

在这种情况下,维修路径要短得多。我们从当前正在运行的线程(“ thread1”)到内核进行了一次上下文切换。内核没有在ISR中进行另一次上下文切换,而是简单地“假装” ISR返回了a struct sigevent并对其执行操作,从而重新安排了“ thread2”的运行时间。

现在您在想,“太好了!我将忘记所有有关 InterruptAttach()的信息,而只使用更简单的InterruptAttachEvent()即可。”

这不是一个好主意,因为您可能不需要为硬件生成的每个中断唤醒!回到上面的源代码示例中,它仅在串行端口上的调制解调器状态寄存器更改状态时才返回事件,而不是在字符到达时,行状态寄存器更改时才返回事件,并且在发送保持缓冲区为空时不返回事件。 。

在那种情况下,特别是如果串行端口正在接收字符(您要忽略),您将浪费大量时间重新安排线程的运行时间,而只是让它查看串行端口并确定它没有运行。无论如何都不想做任何事情。在这种情况下,情况将如下所示:

使用_ InterruptAttachEvent()_和不必要的重新计划来控制流。

发生的所有事情是您引发了线程到线程上下文切换,进入“ thread2”,该线程查看硬件,并决定它不需要执行任何操作,从而使您花费了另一个线程到线程上下文切换的代价。回到“ thread1”。

下面是如果你使用的东西是什么样子InterruptAttach() ,但没有要安排不同的线程(即,你回来):

使用_ InterruptAttach()_进行控制流,而无需重新安排线程。

内核知道“ 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));
}

权衡取舍

那么,您应该使用哪个功能?对于低频中断,您几乎总是可以使用InterruptAttachEvent()逃脱。由于中断很少发生,因此即使您不必要地调度线程,也不会对整体系统性能产生重大影响。唯一一次可以使您困扰的是,如果另一个设备从同一中断中被链接,在这种情况下,因为InterruptAttachEvent() 屏蔽中断源,它将有效地禁止其他设备的中断,直到未屏蔽中断源为止。仅当第一台设备需要很长时间才能维修时,这才是一个问题。从更大的角度看,这是一个硬件系统设计问题-您不应将响应速度较慢的设备与高速设备连接在同一条线上。

对于高频中断,这是一个折腾,有很多因素:

  • 不必要的中断-如果会有大量中断,则最好使用InterruptAttach()并在ISR中将其过滤掉。例如,考虑串行设备的情况。线程可能会发出一条命令,告诉我“获取64个字节”。如果对ISR进行编程时要知道在从硬件接收到64个字节之前不会有任何用处,那么ISR已经有效地过滤了中断。然后,只有在累积了64个字节后,ISR才会返回事件。
  • 延迟-如果您的硬件对声明中断请求和执行ISR之间的时间间隔很敏感,则应使用_InterruptAttach()_来最小化此中断延迟。这是因为内核在分发ISR方面非常快。
  • 缓冲-如果您的硬件中有缓冲,则可以摆脱_InterruptAttachEvent()和像SIGEV_INTR和InterruptWait()_这样的单项排队机制。该方法让硬件尽可能多地中断,同时让线程在可能的情况下从硬件缓冲区中选择值。由于硬件正在缓冲数据,因此中断延迟没有问题。

ISR功能

我们应该解决的下一个问题是允许ISR调用的功能列表。

在这一点上,我只谈一点题外话。从历史上看,ISR很难编写(并且在大多数其他操作系统中仍然如此)的原因是,ISR在特殊的环境中运行。

使ISR编写变得复杂的一件事是,就内核而言,ISR实际上并不是一个“适当的”线程。如果要调用它,那就是这个奇怪的“硬件”线程。这意味着不允许ISR做任何“线程级”的事情,例如消息传递,同步,内核调用,磁盘I / O等。

但这是否使编写ISR例程变得更加困难?是的,它确实。因此,解决方案是在ISR中进行尽可能少的工作,并在线程级(您可以访问所有服务)进行其余工作。

您在ISR中的目标应该是:

  • 获取临时信息。
  • 清除ISR的来源。
  • (可选)调度线程以完成“实际”工作。

这种“架构”取决于Neutrino具有非常快的上下文切换时间的事实。您知道可以快速进入ISR来进行时间紧迫的工作。您还知道,当ISR返回事件以触发线程级工作时,该线程也将快速启动。正是这种“在ISR中什么都不做”的哲学使Neutrino ISR变得如此简单!

那么,您可以在ISR中使用哪些呼叫?这是一个摘要(有关正式列表,请参阅《中微子库参考》中的“安全信息摘要”附录):

中断安全函数的列表很有意义-您可能需要移动一些内存,在这种情况下,_mem _()和str _()_函数是一个不错的选择。您很可能希望从硬件中读取数据寄存器(以保存临时数据变量和/或清除中断源),因此您将需要使用 _in _()和out _()_函数。

令人困惑的Interrupt *() 函数呢?让我们成对检查它们:

这些功能负责在PIC级别屏蔽中断源。这样可以防止它们传递到CPU。通常,如果要在线程中执行进一步的工作并且无法在ISR本身中清除中断源,则可以使用此方法。在这种情况下,ISR将发出InterruptMask(),并且线程将在完成其被调用执行的任何操作后发出InterruptUnmask()。请记住,InterruptMask()和InterruptUnmask()正在计数 -您必须“取消屏蔽”与“被屏蔽”的次数相同,以便中断源能够再次中断您。顺便说一下,请注意InterruptAttachEvent()为您(在内核中)执行InterruptMask() -因此,您必须从中断处理线程中调用InterruptUnmask()。

这些函数用于在单处理器或多处理器系统上禁用(InterruptLock())和启用(InterruptUnlock())中断。如果需要保护线程免受ISR(或者另外,在SMP系统上,ISR不受线程保护),则希望禁用中断。完成关键数据操作后,即可启用中断。请注意,建议在“旧” InterruptDisable()和InterruptEnable()函数上使用这些函数,因为它们将在SMP系统上正常运行。在SMP系统上执行“旧”功能需要额外的费用,但是在单处理器系统中这可以忽略不计,这就是为什么我建议您始终使用InterruptLock()的原因。和InterruptUnlock()。

这些功能不应在新设计中使用。从历史上看,它们被用于调用在x86处理器指令clisti当中微子被x86的只。从那以后,它们已经升级为可以处理所有受支持的处理器,但是您应该使用InterruptLock()和InterruptUnlock()(以使SMP系统满意)。

有一两件事值得重复的是,在SMP系统上,可以有两个中断服务程序_,并_ 运行另一个线程在同一时间。

摘要

处理中断时,请记住以下几点:

  • 在ISR中花的时间不要太长-尽您所能减少的工作量。这有助于最小化中断延迟和调试。
  • 当您需要在发生中断时立即访问硬件时,请使用InterruptAttach()。否则,请避免。
  • 在所有其他时间使用InterruptAttachEvent()。内核将调度一个线程(基于您传递的事件)来处理中断。
  • 通过调用InterruptLock()和InterruptUnlock()保护中断服务例程(如果使用InterruptAttach())和线程使用的变量。
  • 声明将在线程和ISR之间使用的变量,以volatile使编译器不会缓存由ISR更改的“陈旧”值。

问题:

  • 中断必须是硬件中断?
  • 中断实现了什么功能?
  • 什么条件下会考虑到中断?
  • 中断的逻辑是什么?