C语言的中断处理机制

一、中断机制   

 

1、实现中断响应和中断返回

  当CPU收到中断请求后,能根据具体情况决定是否响应中断,如果CPU没有更急、更重要的工作,则在执行完当前指令后响应这一中断请求。CPU中断响应过程如下:首先,将断点处的PC值(即下一条应执行指令的地址)推入堆栈保留下来,这称为保护断点,由硬件自动执行。然后,将有关的寄存器内容和标志位状态推入堆栈保留下来,这称为保护现场,由用户自己编程完成。保护断点和现场后即可执行中断服务程序,执行完毕,CPU由中断服务程序返回主程序,中断返回过程如下:首先恢复原保留寄存器的内容和标志位的状态,这称为恢复现场,由用户编程完成。然后,再加返回指令RETI,RETI指令的功能是恢复PC值,使CPU返回断点,这称为恢复断点。恢复现场和断点后,CPU将继续执行原主程序,中断响应过程到此为止。

2、实现优先权排队

  通常,系统中有多个中断源,当有多个中断源同时发出中断请求时,要求计算机能确定哪个中断更紧迫,以便首先响应。为此,计算机给每个中断源规定了优先级别,称为优先权。这样,当多个中断源同时发出中断请求时,优先权高的中断能先被响应,只有优先权高的中断处理结束后才能响应优先权低的中断。计算机按中断源优先权高低逐次响应的过程称优先权排队,这个过程可通过硬件电路来实现,亦可通过软件查询来实现。

3、实现中断嵌套

  当CPU响应某一中断时,若有优先权高的中断源发出中断请求,则CPU能中断正在进行的中断服务程序,并保留这个程序的断点(类似于子程序嵌套),响应高级中断,高级中断处理结束以后,再继续进行被中断的中断服务程序,这个过程称为中断嵌套。如果发出新的中断请求的中断源的优先权级别与正在处理的中断源同级或更低时,CPU不会响应这个中断请求,直至正在处理的中断服务程序执行完以后才能去处理新的中断请求。

二、ANSI C 中断处理机制分析


    对于一般的C语言爱好者而言,就如何在C中使用中断例程这一问题应该已经非常熟悉,例如,我们可以通过int86()函数调用13H号中断直接对磁盘物理扇区进行操作,也可以通过INT86   (   )函数调用33H号中断在屏幕上显示鼠标光标等。其实,13H号也好,33H号也好,它们只不过就是一些函数,这些函数的参数通过CPU的寄存器传递。中 断号也只不过是间接地指向函数体的起始内存单元,说它是间接的,也就是说,函数的起始段地址和偏移量是由中断号通过一种方法算得的(具体如何操作,下面会作解释)。如此一来,程序员不必要用太多的时间去写操作硬件的程序了,只要在自己的程序中设置好参数,再调用BIOS或DOS提供的中断服务程序就可以 了,大大减小了程序开发难度,缩短了程序开发周期。那么中断既然是函数,就可以由用户任意的调用、由用户任意地编写。   
    计算机内存的前1024个字节(偏移量00000H到003FFH)保存着256个中断向量,每个中断向量占4个字节,前两个字节保存着中断服务程序的入 口地址偏移量,后两个字节保存着中断程序的入口段地址,使用时,只要将它们分别调入寄存器IP及CS中,就可以转入中断服务程序实现中断调用。每当中断发 生时,CPU将中断号乘以4,在中断向量表中得到该中断向量地址,进而获得IP及CS值,从而转到中断服务程序的入口地址,调用中断。这就是中断服务程序通过中断号调用的基本过程。在计算机启动的时候,BIOS将基本的中断填入中断向量表,当DOS得到系统控制权后,它又要将一些中断向量填入表中,还要修 改一部分BIOS的中断向量。有一部分中断向量是系统为用户保留的,如60H到67H号中断,用户可以将自己的中断服务程序写入这些中断向量中。不仅如此,用户还可以自己更改和完善系统已有的中断向量。  
    在C语言中,提供了一种新的函数类型interrupt,专门用来定义中断服务程序,比如我们可以写如下的中断服务程序:  
  例1:中断服务程序

 void   interrupt   int60()     
  {     
      puts("This   is   an   example");     
  } 

该中断的功能就是显示一个字符串,为什么不用printf   (   )函数呢?这就牵涉到DOS的重入问题,后面将作一些介绍。   
        一个简单的中断服务程序写好了,如何把它的函数入口地址填写到中断向量表中,以便在产生中断的时候能转入中断服务程序去执行呢?这里要用到setvect   (   )和getvect   (  )函数。setvect   (   )有两个参数:中断号和函数的入口地址,其功能是将指定的函数安装到指定的中断向量中,getvect   (   )函数有一个参数:中断号,返回值是该中断的入口地址。在安装中断以前,最好用disable   (   )函数关闭中断,以防止在安装过程中又产生新的中断而导致程序运行混乱,待安装完成后,再用enable   (   )函数开放中断,使程序正常运行。现在我们可以把上面的例子再丰富一下:   
例2:中断服务程序的编写、安装和使用

  #include   <dos.h>        
  #include   <stdio.h>         
  #ifdef   __cplusplus         
  #define   __ARGU   ...         
  #else     
      
  #define   __ARGU         
  #endif         
  void   interrupt   int60   (__ARGU)     /*中断服务函数*/         
  {     
     puts("This   is  an   example");     
  }         
  void   install   (void   interrupt  (*fadd)(__ARGU),int   num)   /*安装中断*/     
  {     
     disable();   /*关闭中断*/     
     setvect(num,   fadd);   /*设置中断*/    
     enable();   /*开放中断*/     
  }     
  void   main()     
  {     
     install   (int60,0x60);/*将int60函数安装到0x60中断*/     
     geninterrupt   (0x60);   /*人为产生0x60号中断*/     
  }     

有一定经验的读者很容易得到该程序的执行结果:在屏幕上显示“This   is  an   example!”。   
    编写、安装中断服务程序的方法就介绍这些。下面再浅谈一下内存驻留程序(TSR)的编写和使用。在C语言中,可以用keep  (   )函数将程序驻留内存。这个函数有两个参数:status和size。size为驻留内存长度,可以用size=_SS+_SP/16-_psp得到,当然这也是一种估算的方法,并不是精确值。函数执行完以后,出口状态信息保存在status中。比如,对于上面的例子,将“geninterrupt   (0x60);”改写成“keep(0,_SS+_SP/16-_psp);”后再执行程序,这一段程序就被驻留,此后在其它的任何软件或程序设计中,只要用到了60H号中断,就会在屏幕上显示“This   is  an   example!”的字样。要恢复系统对60H号中断的定义,只能重新启动计算机。   
    像上面的例子其实还很不完善,它没有考虑DOS系统环境的状态、没有考虑程序是否已经驻留内存、没有考虑退出内存驻留等问题。对于第二个问题还是很容易解决的:执行程序一开始就读取某一函数中断入口地址(如63H号中断)判断是否为空(NULL),如果为空就先将该地址置为非空再驻留内存,若为非空则表示 已经驻留并退出程序。这一步判断非常重要,否则将会因为重复驻留占用过多内存空间而最后造成系统崩溃。至于其它两个问题,在此不多作说明,有兴趣的读者可以参考一些有关书籍。   
    不仅如此,我们还可以通过在DOS下使用热键(Hotkey)来调用内存驻留程序。比如将《希望汉字系统》自带的《希望词典》驻留内存后,在任意时刻按下 Ctrl+F11键,就能激活程序,出现词典界面。微机的键盘中有一个微处理芯片,用来扫描和检测每个按键的按下和释放状态。大多数按键都有一个扫描码,告知CPU当前的状态,但一些特殊的键如PrintScreen、Ctrl+Break等不会产生扫描码,而直接产生中断。正因为如此,我们可以将Ctrl+Break产生的中断号指向我们自己写好的程序入口地址,那么当按下Ctrl+Break后,系统就会调用我们自己的程序去执行,这实际上也就是修改了Ctrl+Break的中断向量。至于其它按键激活程序则可以利用9H号键盘中断捕获的扫描码来实现,在此不多作说明。例如,执行下面的程序后,退回DOS系统,在任意的时候按下Ctrl+Break后,屏幕的底色就会变成红色。   
例3:中断服务程序编写、安装和使用,内存驻留

  #include   <dos.h>     
  #include   <conio.h>     
  #ifdef   __cplusplus     
  #define   __ARGU   ...     
  #else     
  #define   __ARGU     
  #endif     
  void   interrupt   newint(__ARGU);   /*函数声明*/     
  void   install   (void   interrupt  (*fadd)(__ARGU),   int   num);     
  int   main()     
  {     
      install   (newint,0x1b);   /*Ctrl+Break中断号:1BH*/     
      keep(0,_SS+(_SP/16)-_psp);   /*驻留程序*/    
      return   0;     
  }     
  void   interrupt   newint(__ARGU)     
  {     
      textbackground(4);   /*设置屏幕底色为红色*/     
      clrscr();   /*清除屏幕*/     
  }     
  void   install   (void   interrupt   (*fadd)(__ARGU),  int   num)     
  {     
      disable();     
      setvect(num,fadd);   /*设置中断*/     
      enable();     
  }     

由于13H号中断是BIOS提供的磁盘中断服务程序,对于DOS下的应用程序,它们的存盘、读盘功能都是通过调用这一中断来实现的。有许多DOS下的病毒就喜欢修改13H号中断来破坏系统,例如,修改13H号中断服务程序,将其改成:  
例4:病毒体程序伪代码

void   interrupt   new13(__ARGU)       
  {     
      if   (病毒发作条件成熟)     
      {   修改入口参数指向病毒程序入口地址;     
          执行病毒代码;     
      }     
      调用原来的13H中断;     
  }  

  只要当任一软件(如EDIT.COM等)对磁盘有操作并且病毒发作条件成熟时,病毒就被激活。当然,这样做会导致可用内存空

三、Linux中断编程

    在Linux下,硬件中断被称为IRQs[Interrupt   Requests  (这是Linux起源的Intel架构上的标准术语)的缩写]。有两种IRQs,短的和长的。一个短的IRQ预期占用非常短的一段时间,在那期间,机器的剩余部分被阻塞,没有其他的中断将被处理。长的IRQ占用的时间长些,在那期间其他中断有可能发生(但不能是来自同一设备)。只要是可能的,声明一个长中断是较好的。   

  当CPU接收到一个中断,它停止它正在做的任何事情(除非它正在处理一个更重要的中断,在那种情况下,它将处理完那个中断后才来处理现在的这个),在堆栈中保存某些参数并调用中断处理程序。这意味着在中断处理程序自身中有些东西是不能允许的,因为系统处于一种未知的状态。解决的办法是中断处理程序马上做完需要做的,通常是从硬件里面读什么或向硬件发送什么然后安排处理稍后的新信息(这被称为‘bottom  half’)并返回。然后内核保证只要可能就调用bottom   half   --当这在运行,内核模块中允许做的所有事情将被允许。   

  实现这个办法是当接收到相关的IRQ(在   Intel  平台下有16个)时去调用request_irq以使中断处理程序被调用。这个函数接收IRQ号,函数名,标志/proc/interrupts中的名字及一个传送给中断处理程序的参数作为其参数。标志可以包括SA_SHIRQ以指明你愿意和其他的中断处理程序分享那个IRQ(通常因为几个硬件设备在同一IRQ)以及SA_INTERRUPT以指明这是一个快速中断。这个函数只在那个IRQ上没有处理程序的情况下成功,或者你愿意两者共享。   

  然后从中断处理程序中我们和硬件通信,联合tq_immediate使用queue_task_irq和mark_bh(BH_IMMEDIATE)调度bottom half。我们在2.0版中不使用标准的queue_task   的原因是中断有可能在其他人的  queue_task(queue_task_irq从这被一个全局锁保护--在2.2版中没有queue_task_irq而queue_task被一个锁保护。)中发生。我们需要   mark_bh  是因为Linux   的早期版本只能有32个   bottom  half,而现在它们中的一个(BH_IMMEDIATE)用于还没有得到bottom  half入口的驱动程序的bottom half连接表。

  

Intel 架构键盘  



    警告:   这章剩下的内容都特别指定为完全的基于Intel架构。如果你不是在这个平台下运行,它没有用。甚至不要试图去编译这里的代码。  

  在为这章写范例代码的时候我有一个问题。一方面,对于一个有用的范例,它应该运行于每个人的计算机上且有意味深长的结果。另一方面,内核已经包含了所有的通用设备的驱动程序,并且那些设备驱动程序不能和我将要写的共存。我发现的结果是写一些键盘中断的东西并且先关闭通常的键盘的中断句柄。因为在内核源文件(明确的,  drivers/char/keyboard.c)中它被定义为静态符号,所以没有办法恢复它。如果你重视你的文件系统,在   insmod  这些代码前,在另一个终端上sleep   120   ;  reboot   。   

  这个代码将自己绑定为   IRQ  1,这是Intel   架构下的键盘控制器的IRQ(中断请求)。然后当它收到键盘中断时它就读键盘的状态(   那就是inb(0x64)的目的)和扫描代码,该代码即是键盘的返回值。然后,内核一认为它是可行的它就运行给出键所使用的代码(扫描代码的前7位)和它是被按下(第8位为0)还是被释放(第8位为1)的got_char函数。  

范例   intrpt.c  

/*   intrpt.c   -   中断句柄   */    
  
/*   Copyright   (C)   1998   by   Ori  Pomerantz   */     
  
/*   必要头文件   */     
  
/*   标准头文件   */     
#include   /*   内核工作   */     
#include   /*   明确指定是模块   */     
  
/*   处理   CONFIG_MODVERSIONS  */     
#if   CONFIG_MODVERSIONS==1     
#define   MODVERSIONS     
#include     
#endif     
  
#include     
#include     
  
/*   我们想中断   */     
#include     
  
#include     
  
/*   在   2.2.3   版/usr/include/linux/version.h  中包含这个宏,    
*   但   2.0.35   版不包含-因此在这加入以被需要   */    
#ifndef   KERNEL_VERSION     
#define   KERNEL_VERSION(a,b,c)   ((a)*65536+(b)*256+(c))     
#endif     
  
/*   Bottom   Half   -   一旦内核模块认为它做任何事都是安全的时候这将被内核调用。   */    
static   void   got_char(void   *scancode)     
{     
printk( "Scan   Code   %x   %s.n ",     
(int)   *((char   *)   scancode)   &   0x7F,    
*((char   *)   scancode)   &   0x80   ?   "Released"   :   "Pressed ");     
}     
  
/*   这个函数为键盘中断服务。它读取来自键盘的相关信息然后安排当内核认为bottom  half安全的时候让它运行   */     
void   irq_handler(int   irq,     
void   *dev_id,     
struct   pt_regs   *regs)     
{     
/*   这些变量是静态的,因为它们需要对   bottom  half   可见(通过指针)。   */     
static   unsigned   char   scancode;     
static   struct   tq_struct   task   =     
{NULL,   0,   got_char,   &scancode};     
unsigned   char   status;     
  
/*   Read   keyboard   status   */     
status   =   inb(0x64);     
scancode   =   inb(0x60);     
  
/*   安排   bottom   half  运行   */     
#if   LINUX_VERSION_CODE   >   KERNEL_VERSION(2,2,0)     
queue_task(&task,   &tq_immediate);     
#else     
queue_task_irq(&task,   &tq_immediate);     
#endif     
mark_bh(IMMEDIATE_BH);     
}     
  
/*   初始化模块--登记   IRQ  句柄   */     
int   init_module()     
{     
/*   既然键盘的句柄不能和我们的共存,在我们做事情前我们不得不关闭它(释放它的   IRQ)。    
*   因为我们不知道它在哪儿,所以以后没有办法恢复它--因此当我们做完时计算机将被重新启动。    
*/     
free_irq(1,   NULL);     
  
/*   请求   IRQ   1,键盘的   IRQ,指向我们的  irq_handler。   */     
return   request_irq(     
1,   /*   PC上的键盘的   IRQ   号   */    
irq_handler,   /*   我们的句柄   */     
SA_SHIRQ,     
/*   SA_SHIRQ   意味着我们将另一个句柄用于这个   IRQ。    
*    
*   SA_INTERRUPT   能使句柄为一个快速中断。    
*/     
"test_keyboard_irq_handler ",   NULL);     
}     
  
/*   清除   */     
void   cleanup_module()     
{     
/*   它在这儿只是为了完全。它是完全不相关的,因为我们没有办法恢复通常的键盘中断因此计算机完全没用   *   了,需要被重新启动。   */    
free_irq(1,   NULL);     
} 

四、Windows中断编程

 
1、中断机制  

 
(1)实模式中断   
     为了便于理解,我们先回顾实模式中断。在实模式下,中断向量表IVT起到相当重要的作用。无论来自外部硬件的中断或是内部的软中断INTn,在CPU中都产生同样的响应。   
     ①CPU将当前的指令指针寄存器(IP)、代码段寄存器(CS)、标志寄存器压入堆栈。   
     ②然后CPU使用   n值作为指向中断向量表IVT的索引,在IVT中找出服务例程的远地址。   
     ③CPU将此远地垃装入CS:IP寄存器中,并开始执行服务例程。   
     ④中断例程总以IRET指令结束。此指令使存在堆栈中的三个值弹出并填入CS、IP和标志寄存器,CPU继续执行原来的指令。   
(2)保护模式中断   
    保护模式中断过程与实模式中断过程类似,但它不再使用中断向量表IVT,而使用中断描述符表(IDT)。值得一提的是,Windows运行时IVT还存在,应用程序并不使用它,Windows仍然使用,但含义已不同。  
    IVT结构:IVT在RAM的   0000:0000之上,占据开始的1024字节。它仍然由   BIOS启动例程设置,由DOS填充到RAM中。   
    ①IDT中断描述符表:保护模式下,Windows操作系统为实现中断机制而建立的一个特殊表,即中断描述符表IDT。该表被用来保存中断服务例程的线性地址,它们是真正的24位或32位地址,没有段:偏移值结构。中断描述器表最多可含有256个例程说明。   
    ②当中断或异常发生时,处理过程与实模式类似当前的CS;IP值和标志寄存器值被存储。保存的内容还包括CPU其他内部寄存器的值,以及目  
前正在被执行的任务的有关信息(若必须发生任务切换的话)。CPU设法获取中断向量后,以它为索引值查找IDT中的服务例程远地址,接着将控制转移到该处的服务例程。这是与实模式转移到IVT的不同所在。保护模式使用IDTR寄存器分配和定位内存中的IDT中断描述符表。IDT在内存中是可移动的,与 IVT固定在内存中刚好相反。IDT中断描述符表在Windows中起决定性的作用。理解了windows保护模式的中断机制。有助于我们理解中断服务程序的设计,它的关键就在于如何将服务例程的地址放入IDT中断描述符表中。当中断发生时,如何将断点地址及CPU各寄存器值保护起来,中断结束时,如何将保护的值恢复。windows系统本身并不提供实现上述功能的API,而DOS保护模式接口DPMI正具备了上述的功能。   

2、Windows下中断服务程序的设计

    

    下面我们首先介绍DPMI接口,然后基于它实现Windows下中断服务程序的设计。   
(1)DOS保护模式接口DMPI 

    Windows除了标准服务外,还支持一组特殊的DOS服务,称为DOS保护模式接口DPMI,由一些INT2FH和INT31H服务组成。它使应用程序能够访问   PC系列计算机的扩充内存,同时维护系统的保护功能。DPMI通过软件中断31h来定义了一个新的接口,使得保护模式的应用程序能够用它作分配内存,修改描述符以及调用实模式软件等工作。   
    Windows为应用程序提供DPMI服务。即Windows是DPMI的宿主(host),应用程序是DPMI的客户(client),可通过INT31H调用得到DPMI服务。 INT   31H本身提供多功能。其中它的中断管理服务允许保护模式用于拦截实模式中断,并且挂住处理器异常。有些服务能够和   DPMI宿主合作,以维护应用程序的虚拟中断标志。   
    可以用INT31H来挂住保护模式中断向量,以中断方式处理外部实时事件。利用   INT  21H,功能0205H:设置保护模式中断向量,将特定中断的保护模式处理程序的地址置入中断向量里。调用方式:  
    AX=0205H,BL=中断号,CX:(E)DX=中断处理程序选择符:偏移值。返回:执行成功CF=清零,执行失败CF,置位。   
    挂住/解挂中断向量的时机很重要。主窗口第一次被创建时会传送它WM—CREATE消息,这时是挂住中断向量的最好时机。退出时需解挂向量,否则 Windows可能崩溃。上窗口接收到WM_DESTROY之后进行解挂工作,是最适合的。解挂向量可先用INT35H,0204H功能将老的中断向量保存,退出时用INT35H,0205H恢复。   

(2)编程实现   
    有了DPMI的支持,我们就可以很方便地处理数据采集、串行通信等工业过程中的实时事件。下面以Windows3.1平台下中断方式实现的串行通信为例,说明中断程序的编制和实现。为便于参考,给出了详细的代码。开发平台BC3.1/BC4.5,其本身支持0.9版的DPMI,无需运行其它支持DPMI的软件。编程语言C,可与C++混合编译。   
    初始化COM1,9600波特率,每字符8bits,1个停止位,中断接收,查询发送。

//windows   asy   COmmunica60n     
//by   Li   Xiumi98     
//last   modified   on   June25,1996     
#include <windows.h>     
#include <dos.h>     
void   interrupt   far   DataReceive()   ;    
void   interrupt   far(   *   old_vector)();    
unsigned   char   dataCom_r[1024],datacom_s[1024]:     
int   inflag=0   ;     
unsigned   int   s8259;     
int   InitCom1()     
{     
    s8259=inportb(0x21);     
    outportb(0x21,s8259&0xe8);     
    outportb(0x3fb,0x83);     
    outportb(0x3f8,0x0c);     
    outportb(0x3f9,0x00);     
    outportb(0x3fb,0x03);     
    outportb(0x3fc,0x08);     
    outportb(0x3f9,0x01);     
    return   1;     
}     
  
void   interrupt   far   DataReceive()     
{     
    static   int   i=0   ;     
    char   rechar   =0   ;     
    rechar=inportb(0x3f8);     
    if(inflag==0)     
    {     
        if(rechar!= 's '&&i==0)     
    {     
        i=0;     
        goto   l1;     
    }     
    datacom_r[i++]=rechar;     
    if(rechar== 'e ')     
    {     
        inflag=1;     
        i=0;     
    }     
    }     
    l1:outportb(0x20,0x20);     
}     
  
  
void   InitCom(void)     
{     
    asm{     
        cli;     
    mov   ax,204h     
    mov   bl,0ch     
    int   31h     
    sti     
    }     
    old_vector=MK_FP(_CX,_DX);     
    asm{     
    cli     
    mov   ax,205h     
    mov   bl,0ch     
    mov   cx,seg   datareceive     
    mov   dx,offset   datareceive     
    int   31h     
    sti     
    }     
    InitCom();     
}     
  
void   restore_Comm(void)     
{     
    outportb(0x21,s8259);     
    asm{     
    cli     
    mov   ax,205h     
    mov   bl,0ch     
    mov   cx,seg   old_vector     
    mov   dx,offset   old_vector     
    int   31h     
    sti     
    }     
} 

在窗口第一次被创建时会传送它WM_CREATE消息,这时调用initCom()即可。在主窗口关闭时,即主窗口中收到  WM_DESTROY消息时,调用Restore   Comm()恢复原来的状态。   
    这样在对串口初始化,设置中断服务例程后,通信事件发生时,会立即跳入中断子程序中执行,越过系统的消息队列,达到实时处理通信事件的目的。而数据处理模块可通过全局标志f1,8访问全局的数据通信缓冲区获取实时数据。这种实现方式与基于消息机制的Windows通信API实现相比具有实时性强的的特点,因为它超过了Windows系统的两极消息机制,上述程序已在实际系统中得到应用。在windows3.1支持下同时运行三个Windows任务,服务器SERVER(内有实时串行通信,多个网络数据子服务),客户CLIENT,FOXPRO数据库系统。整个系统运行良好。切换到WIN95平台下,系统也运行良好。

发布了19 篇原创文章 · 获赞 4 · 访问量 2097

猜你喜欢

转载自blog.csdn.net/a419116194/article/details/104065091