LinuxSir.cn,穿越时空的Linuxsir!

 找回密码
 注册
搜索
热搜: shell linux mysql
查看: 1165|回复: 0

基于i386的Linux实现特点剖析——关于中断

[复制链接]
发表于 2003-2-26 03:51:42 | 显示全部楼层 |阅读模式
摘要

      Linux内核在初始化阶段完成了对页式虚拟管理的初始化以后,便调用trap_init()和init_IRQ()两个函数进行中断机制的初始化。其中trap_init()主要是对一些系统保留的中断向量的初始化,而init_IRQ()则主要是用于外设的中断。
    (2003-02-24 08:52:29)

By iamafan

三、Linux下中断管理的实现

  1.中断向量表IDT的初始化

  Linux内核在初始化阶段完成了对页式虚拟管理的初始化以后,便调用trap_init()和init_IRQ()两个函数进行中断机制的初始化。其中trap_init()主要是对一些系统保留的中断向量的初始化,而init_IRQ()则主要是用于外设的中断。


  void _init trap_init(void)
  {
   #ifdef CONFIG_EISA
      if (isa_readl(0x0FFFD9) ==
  'E+('I'<<8)+('S'<<16)+('A'<<24))
         EISA_bus = 1;
   #endif

    set_trap_gate(0,&divide_error);
    set_trap_gate(1,&debug);
    set_intr_gate(2,&nmi);
    set_system_gate(3,&int3);  /*int3-5 can be called from all */
    set_system_gate(4,&overflow);
    set_system_gate(5,&bounds);
    set_trap_gate(6,&invalid_op)
    set_trap_gate(7,device_not_available);
    set_trap_gate(8,&double_fault);
    set_trap_gate(9,&coprocessor_segment_overrun);
    set_trap_gate(10,&invalid_TSS);
    set_trap_gate(11,&segment_not_present);
    set_trap_gate(12,&stack_segment);
    set_trap_gate(13,&general_protection);
    set_trap_gate(14,&page_fault);
    set_trap_gate(15,&spurious_interrupt_bug);
    set_trap_gate(16,&coprocessor_error);
    set_trap_gate(17,&alignment_check);
    set_trap_gate(18,&machine_check);
    set_trap_gate(19,&simd_coprocessor_error);
  
    set_system_gate(SYSCALL_VECTOR,&system_call);

    /*
     * default LDT is a single_entry callgate to lcall7 for iBCS
     * and a callgate to lcall27 for Solaris/x86 binaries
     */
    set_call_gate(&default_ldt[0],lcall7);
    set_call_gate(&default_ldt[4],lcall27);

    /*
   _set_gate(a,12,3,addr);
   //12 即1100b,类型码为100,即调用门
  }

  这些函数都调用同一个子程序_set_gate(),第一个参数用以设置中断描述符表idt_table中的第n项,第二个参数对应于门格式中的D位加类型位段,第三个参数是对应的DPL位段。


   #define _set_gate(gate_addr,type,dpl,addr)
   do {
      int _d0,_d1;
      _asm_ _volatile_("movw %%dx,%%ax
        ")
      "movw %4,%%dx
        "
      "mov1 %%eax,%0
        "
      "mov1 %%edx,%1"
      :"=m" (*((long * ) (gate_addr))),
       "=m" (*(1+(long *)(gate_addr))),"=&a" (_d0),"=&d" (_d1)
      :"i" ((short) (0x8000+(dpl<<13)+(type<<8))),
       "3" ((char *) (addr)),"2" (_KERNEL_CS << 16));
  }while(0)


  在第一个“:”到第二个“:”之间为输出部,有四个约束输出,将有四个变量会被改变,分别为%0、%1、%2和%3相结合。其中%0和%1都是内存单元,分别和gate_addr、gate_addr+1结合,%2于局部变量_d0结合,存放在寄存器%%eax中;%3于局部变量_d1结合,存放在寄存器%%edx中。

  第二个“:”之后的部分是输入部,输出部已经定义了%0-%3,输入部中的第一个变量为%4,而紧接着的第二、第三个变量分别等价于输出部的%3和%2。输入部中说明的个输入变量地值,包括%3和%2,都会在引用这些变量之前设置好。在执行指令部的代码之前,会先将%%eax设成(_KERNEL_CS<<16),把%%edx设为addr,%4变量设置为(0x8000+(dpl<<13)+type<<8),则%4变量对应于门结构中的第32-47位,其P位为1。
有表格

  指令部第一条指令“movw %%dx,%%ax”,将%%dx的低16位移入%%ax的低16位,这样,在%%eax中,其高16位为_KERNEL_CS,而低16位为addr的低16位,形成了门结构中的第0-31位。

  第二条指令“movx %4 ,%%dx”,将%4放入%%dx中,也就是将门结构中的第32-47位放在%%dx中,而对于%%edx而言,就对应着门结构中的高32位。

  第三条指令“mov1 %%eax,%0”,将%%edx写入变量%0中,即*gate_addr。

  第四条指令“mov1 %%eax,%1”将%%edx写入变量%1中,即*(gate_addr+1)。

  将第三、第四条指令和起来看,就是将整个门结构的内容都写道*gate_addr中去了。


  void _inti init_IRQ(void)
  {
    int i;
  #ifndef CONFIG_X86_VISWS-APIC
     init_ISA_irqs();
  #else
     init_VISWS_APIC_irqs();
  #endif
     /*
      * Cover the whole vector space,no vector can escape
      * us. (some of these will be overridden and become
      * 'special' SMP interrupts)
      */
     for (i=0;i< NR_IRQS;i++) {
         int vector = FIRST_EXTERNAL_VECTOR + i;
         if (vector != SYSCALL_VECTOR)
             set_intr_gate(vector,interrupt);
     }
   
  #ifdef CONFIG_SMP
     /*
      * IRQ0 must ve given a fixed assignment and initialized,
      * because it's used before the I0-APIC is set up.
      */
     set_intr-gate(FIRST_DEVICE_VECTOR,interrupt[0]);
     
     /*
      * The reschedule interrupt is a CPU-to-CPU reschedule-helper
      * IPI,driven by wakeup.
      */
     set_intr_gate(RESCHEDULE_VECTOR,reschedule_interrupt);
     
     /* IPI for generic function call */
     set_intr_gate(CALL_FUCTION_VECTOR,call_funtion_interrupt);
  #endif

  #ifdef CONFIG_X86_LOCAL_APIC
     /* IPI vectors for APIC spurious and error interrupts */
     set_intr_gate(SPURIOUS_APIC_VECTOR,spurious_interrupt);
     set_intr_gate(ERROR_APIC_VECTOR,error_interrupt);
  #endif

     /*
      * Set the clock to HZ Hz, we already have a valid
      * vector now;
      */
    outb_p(0x34,0x43);     /* binary, mode 2, LSB/MSB ,ch 0*/
    outb_p(LATCH & 0xff,0x40);    /* LSB */
    outb(LATCH >> 8, 0x40);   /* MSB */

  #ifndef CONFIG_VISWS
     setup_irq(2, &irq2)
  #endif
   
    /*
     * External FPU? Set up irq13 if so,for
     * original braindamaged IBM FERR coupling.
     */
    if (boot_cpu_data.hard_math && !cpu_has_fpu)
        setup_irq(13,&irq13);

  i386体系支持256各中断向量,扣除为cpu保留的向量,Linux作为通用操作系统,很难说剩下的中断向量是否够用。所以,在Linux系统中,为每个中断向量设置一个队列,而根据每个中断源所使用的中断向量,将其中断服务程序挂到相应的队列中,而数组irq_desc[]中的每个元素则是这样一个队列头部以及控制结构。当中断发生时,首先执行中断向量相对应的一段总服务程序,根据具体中断源的设备号在其所属队列中找到特定的服务程序加以执行。

  首先对PC的中断控制器8259A的初始化,并初始化了数组iirq_desc[]。接着从FIRST_EXTERNAL_VECTOR开始,设立NR_IRQS个中断向量的 IDT表项。常数FIRST_EXTERNAL_VECTOR定义为 0x20,而NR_IRQS则为224。其中还跳过了用于系统调用的向量0x80。

  忽略我们不关心的多处理器SMP结构和SG1工作站的特殊处理,剩下的就是对系统时钟的初始化。在PC中,定时器/计数器芯片8254共有三个通道,通道0是一个产生实时时钟信号的系统计时器,而程序中要设置的也就是通道0。用于控制8254的端口共有四个,前三个分别对应于单个通道的端口,最后一个通道对应于8254的控制字寄存器端口。


    outb_p(0x34,0x43);//设置通道的工作方式
    //选通通道0,先读写高字节,后读写低字节,工作于方式2,二进制数
    outb_p(LATCH&0xff,0x40);
    //写入低字节
    outb(LATCH>>8,0x40);
    //写入高字节
    //设置通道0的记数值

  到此,已经设置好了IDT,也有了一个中断向量,0号中断时钟中断。但是,虽然该中断服务的入口地址已经设置到中断向量表中,但我们还没有把0号中断具体的中断服务程序挂到0号中断的队列中去。这时,这些中断地队列都是空的,因此,即使开了中断,并产生了时钟中断,也只不过是让它在中断处理的总服务程序中空跑一趟。

  设置好了中断向量表,中断队列都还是空的。想要中断程序生效,下一步就要初始化中断请求队列,并把具体的中断服务程序挂入中断队列中去。

  2、中断请求队列的初始化

  通用中断门是让多个中断源共用的,而且允许这种共用的结构在系统运行的过程中动态的改变,所以在IDT地初始化阶段只是为每个中断向量准备一个中断请求队列,从而形成一个中断请求对列的数组,就是数组irq_desc[]。中断请求队列头部的数据结构是在include/linux/irq.h中定义的。


struct hw_interrupt_type {
  const char * typename;//赋予控制器的人工可读的名字
  unsigned int (*startup)(unsigned int irq);//允许从给定的控制器的IRQ事件发生
  void (*shutdown)(unsigned int irq);//禁止从给定的控制器的IRQ事件发生
  void (*enable)(unsigned int irq);
  void (*ack)(unsigned int irq);
  void (*end)(unsigned int irq);
  void (*set_affinity)(unsiged int irq,unsigned long mask);
};
typedef struct hw_interupt_type hw_irq_controller;
typedef struct {
  unsigned int status;   /* IRQ status */
  hw_irq_controller *handler;
  struct irqaction *action;  /*IRQ action list */
  unsigned int depth;    /* nested irq disables */
//irq_desc_t 当前用户的个数。用于保证在事件处理过程中IRQ不会被禁止
  spinlock_t lock;
}_cacheline_aligned irq_desc_t;

extern irq_desc_t irq_desc[NR_IRQS];

  每个队列头中,用指针action来维持一个由中断服务程序描述项构成的单链表,还有一个指针handler指向另一个数据结构hw_interrupt_type。Hw_interrupt_type中的一些函数指针,用于该队列的,而不是用于具体的中断源的服务。

  这些函数都是在init_ISA_irqs中设置好的。


  void_init init——ISA_irqs中设置好的。
  {
     int i;
     init_8259A(0);
     for (i=0;i

  先对8259A进行初始化,将开头16个中断请求队列的handler指针设置成指向数据结构i8259A_irq_type。


  struct irqcation {
    void (*handler)(int,void *,struct pt_regs *);
    //指向具体中断服务程序
    unsigned long flags;
    unsigned long mask;
    const char *name;
    void *dev_id;
    struct irqaction *next;
};

  IDT表初始化之初,每个中断服务队列都是空的。真正的中断服务要到具体设备的初始化程序将其中断服务程序通过reques_irq()向系统“登记”,挂入某个中断请求队列。


int request_irq(unsigned int irq,
    void (*handler)(int,void*, struct pt_regs *),
    unsigned long irqflags,
    const char * devname,
    void *dev_id)

  参数表中的irq为中断请求队列的序号,对应中断控制器的一个通道。这个中断请求号和CPU所用的中断向量是不同的,中断请求号0相当于中断向量0x20。Ireflags 是一些标志位,当其中的SA_SHIRQ标志与其它中断源共用该中断请求通道。此时,必须提供一个dev_id以供区别。

  在request_irq中分配并设置了irqaction结构,便调用setup_irq将其链入响应的中断请求队列。

3 系统调用

  系统调用是CPU主动地、同步地进入系统空间的手段。系统调用只能发生在用户空间。Linux下的系统调用是通过中断指令“INT 0x80”实现的。

  在这里,我们以sethostname()这个简单的系统调用作为例子,介绍系统调用中进入内核空间,以及完成了服务返回用户空间的过程。


  int sethostname(const char *name,size_t len);

  参数name是要设置的主机名,而len是该字符串的长度。调用返回0表示成功,-1表示失败。失败时可从全局变量errno 中得到具体的出错代码。通过反汇编libc.a得到的汇编代码如下:


sethostname.o: file format elf32-i386
Disassembly of section. text:
00000000
       0:89 da                 mov1 %ebx,%edx
       2:8b 4c 24 08           mov1 0x8(%esp,1),%ecx
       6:8b 5c 24 04           mov1 0x4(%esp,1),%ebx
       a:b8 4a 00 00 00        mov1 $0x4a,%eax
       f:cd 80                 int $0x80
       11:89 d3                mov1 %edx,%ebx
       13:3d 01 f0 ff ff       cmp1 $0xfffff001,%eax
       18:0f 83 fc ff ff       jae la
       1d:ff                           
       1a:R_386_PC32           _syscall_error
       1e:c3                   ret

  进入函数sethostname()以后,堆栈指针%esp指向返回地址,而在堆栈指针加4的地方则是调用该函数的第一个参数name,加8是第二个参数len。第二条指令是将相对于寄存器%esp位移为8的内容存入寄存器%ecx,即将参数len存入寄存器%ecx,第三条指令就是把参数name存入寄存器%ebx,第四条指令把代表sethostname()的系统调用号0x4a存入寄存器%eax,接着就是中断指令"int 0x80"。

  从系统调用返回后,先从%edx中恢复%ebx原先的内容,是在系统调用之前保存在%edx中的(第一条指令),而%edx中的内容就丢失了,这是一种约定,gcc在使用寄存器时会遵守这个约定。接着就是检查系统调用的返回值,这是放在%eax中的。若%eax得内容在0xfffff001和0xffffffff之间,即-1到-4095之间,那就是出错了,要转向_syscall_error()并从那里返回。


  sysdep.o:          file format elf32_i386
  disassemble of section.text:
  00000000 <_syscall_error):
       0:f7 d8       negl %eax
  00000002 <_syscall_error_1>:
       2:50                 push1 %eax
       3:e8 fc ff ff ff     call 4 <_syscall_error_1+0x2>
       4:R_386_PC32         _errno_location
       8:59                 pop1 %ecx
       9:89 08              mov1 %ecx,(%eax)
       b:b8 ff ff ff ff     mov1 0xffffffff,%eax
      10:c3 ret
  errno-loc.0         file of format elf32-i386
  disassembly of secton.text:
  00000000 <_errno_location>:
       0:55                 push1 %ebp
       1:89 e5              mov1 %esp,%ebp
       3:b8 00 00 00 00 mov1 $0x0,%eax
       4:R_386_32 errno
       8:89 ec              mov1 %ebp,%esp
       a:5d                 pop1 %ebp
       b:c3                 ret

  在_syscall_error中,先将%eax中的负值取绝对值,这就是出错代码,并将其压入堆栈。接着,又调用_erron_location(),将全局变量errno中。最后,在返回之前,将%eax的内容变为-1。这样,通过寄存器%ecx返回到用户空间的数值就是-1,而errno则含有具体的出错代码。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

快速回复 返回顶部 返回列表