Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1401642
  • 博文数量: 842
  • 博客积分: 12411
  • 博客等级: 上将
  • 技术积分: 5772
  • 用 户 组: 普通用户
  • 注册时间: 2011-06-14 14:43
文章分类

全部博文(842)

文章存档

2013年(157)

2012年(685)

分类: LINUX

2012-03-06 13:51:06

内核随记(一)——理解中断(2)
3、内核的中断处理
3.1、中断处理入口
由上节可知,中断向量的对应的处理程序位于interrupt数组中,下面来看看interrupt:
341 .data #数据段
342 ENTRY(interrupt)
343 .text
344
345 vector=0
346 ENTRY(irq_entries_start) 
347 .rept NR_IRQS #348-354行重复NR_IRQS次
348 ALIGN 
349 1: pushl $vector-256 #vector在354行递增 
350 jmp common_interrupt #所有的外部中断处理函数的统一部分,以后再讲述 
351 .data 
352 .long 1b #存储着指向349行的地址,但是随着348行-354被gcc展开,每次的值都不同 
353 .text 
354 vector=vector+1 
355 .endr #与347行呼应 
356 
357 ALIGN

    #公共处理函数
common_interrupt:
    SAVE_ALL            
    movl %esp,�x 
    
call do_IRQ   
    
jmp ret_from_intr 
分析如下:
首先342行和352行都处于.data段,虽然看起来它们是隔开的,但实际上被gcc安排在了连续的数据段内存 中,同理在代码段内存中,354行与350行的指令序列也是连续存储的。另外,348-354行会被gcc展开NR_IRQS次,因此每次352行都会存 储一个新的指针,该指针指向每个349行展开的新对象。最后在代码段内存中连续存储了NR_IRQS个代码片断,首地址由 irq_entries_start指向。而在数据段内存中连续存储了NR_IRQS个指针,首址存储在interrupt这个全局变量中。这样,例如 IRQ号是0 (从init_IRQ()调用,它对应的中断向量是FIRST_EXTERNAL_VECTOR)的中断通过中断门后会触发 interrput[0],从而执行:
pushl 0-256
jmp common_interrupt
的代码片断,进入到Linux内核安排好的中断入口路径。

3.2、数据结构
3.2.1、IRQ描述符
Linux支持多个外设共享一个IRQ,同时,为了维护中断向量和中断服务例程(ISR)之间的映射关系,Linux用一个irq_desc_t数据结构来描述,叫做IRQ描述符。除了分配给异常的
32个向量外,其余224(NR_IRQS)个中断向量对应的IRQ构成一个数组irq_desc[],定义如下:
//位于linux/irq.h
typedef struct irq_desc {
    unsigned 
int status;        
    hw_irq_controller 
*handler;
    
struct irqaction *action;    
    unsigned 
int depth;        
    unsigned 
int irq_count;        
    unsigned 
int irqs_unhandled;
    spinlock_t 
lock;
} ____cacheline_aligned irq_desc_t;

//IRQ描述符表
extern irq_desc_t irq_desc [NR_IRQS];
“____cacheline_aligned”表示这个数据结构的存放按32字节(高速缓存行的大小)进行对齐,以便于将来存放在高速缓存并容易存取。
status:描述IRQ中断线状态,在irq.h中定义。如下:    
#define IRQ_INPROGRESS  1   
#define IRQ_DISABLED    2    
#define IRQ_PENDING     4    
#define IRQ_REPLAY      8    
#define IRQ_AUTODETECT  16  
#define IRQ_WAITING     32   
#define IRQ_LEVEL       64    
#define IRQ_MASKED      128    
#define IRQ_PER_CPU     256     
handler:指向hw_interrupt_type描述符,这个描述符是对中断控制器的描述。下面有描述。
action:指向一个单向链表的指针,这个链表就是对中断服务例程进行描述的irqaction结构。下面有描述。
 
depth:如果启用这条IRQ中断线,depth则为0,如果禁用这条IRQ中断线不止一次,则为一个正数。每当调用一次disable_irq(),该函数就对这个域的值加1;如果depth等于0,该函数就禁用这条IRQ中断线。相反,每当调用enable_irq()函数时,该函数就对这个域的值减1;如果depth变为0,该函数就启用这条IRQ中断线。
lock:保护该数据结构的自旋锁。

IRQ描述符的初始化:
//位于arch/i386/kernel/i8259.c
void __init init_ISA_irqs (void)
{
    
int i;

#ifdef CONFIG_X86_LOCAL_APIC
    init_bsp_APIC();
#endif
    
//初始化8259A
    init_8259A(0);
    
//IRQ描述符的初始化
    for (i = 0; i < NR_IRQS; i++) {
        irq_desc[i].status 
= IRQ_DISABLED;
        irq_desc[i].action 
= NULL;
        irq_desc[i].depth 
= 1;

        
if (i < 16) {
            

            irq_desc[i].handler 
= &i8259A_irq_type;
        } 
else {
            

            irq_desc[i].handler 
= &no_irq_type;
        }
    }
}
从这段程序可以看出,初始化时,让所有的中断线都处于禁用状态;每条中断线上还没有任何中断服务例程(action为0);因为中断线被禁用,因此depth为1;对中断控制器的描述分为两种情况,一种就是通常所说的8259A,另一种是其它控制器。
3.2.2、中断控制器描述符hw_interrupt_type
这个描述符包含一组指针,指向与特定的可编程中断控制器电路(PIC)打交道的低级I/O例程,定义如下:
//位于linux/irq.h
struct hw_interrupt_type {
    
const char * typename;
    unsigned 
int (*startup)(unsigned int irq);
    
void (*shutdown)(unsigned int irq);
    
void (*enable)(unsigned int irq);
    
void (*disable)(unsigned int irq);
    
void (*ack)(unsigned int irq);
    
void (*end)(unsigned int irq);
    
void (*set_affinity)(unsigned int irq, cpumask_t dest);
};

typedef 
struct hw_interrupt_type  hw_irq_controller;
Linux除了支持常见的8259A芯片外,也支持其他的PIC电路,如SMP IO-APIC、PIIX4的内部 8259 PIC及 SGI的Visual Workstation Cobalt (IO-)APIC。8259A的描述符如下:
//位于arch/i386/kernel/i8259.c
static struct hw_interrupt_type i8259A_irq_type = {
    
"XT-PIC",
    startup_8259A_irq,
    shutdown_8259A_irq,
    enable_8259A_irq,
    disable_8259A_irq,
    mask_and_ack_8259A,
    end_8259A_irq,
    NULL
};
在这个结构中的第一个域“XT-PIC”是一个名字。接下来,8259A_irq_type包含的指针指向五个不同的函数,这些函数就是对PIC编程的函数。前两个函数分别启动和关闭这个芯片的中断线。但是,在使用8259A芯片的情况下,这两个函数的作用与后两个函数是一样的,后两个函数是启用和禁用中断线。mask_and_ack_8259A函数通过把适当的字节发往8259A I/O端口来应答所接收的IRQ。end_8259A_irq在IRQ的中断处理程序结束时被调用。

3.2.3、中断服务例程描述符irqaction
为了处理多个设备共享一个IRQ,Linux中引入了irqaction数据结构。定义如下:
//位于linux/interrupt.h
struct irqaction {
    irqreturn_t (
*handler)(intvoid *struct pt_regs *);
    unsigned 
long flags;
    cpumask_t mask;
    
const char *name;
    
void *dev_id;
    
struct irqaction *next;
    
int irq;
    
struct proc_dir_entry *dir;
};
handler:指向一个具体I/O设备的中断服务例程。这是允许多个设备共享同一中断线的关键域。
flags:用一组标志描述中断线与I/O设备之间的关系。
SA_INTERRUPT
中断处理程序必须以禁用中断来执行
SA_SHIRQ
该设备允许其中断线与其他设备共享。
SA_SAMPLE_RANDOM
可以把这个设备看作是随机事件发生源;因此,内核可以用它做随机数产生器。(用户可以从/dev/random 和/dev/urandom设备文件中取得随机数而访问这种特征)
SA_PROBE
内核在执行硬件设备探测时正在使用这条中断线。
name:I/O设备名(通过读取/proc/interrupts文件,可以看到,在列出中断号时也显示设备名。)
dev_id:指定I/O设备的主设备号和次设备号。
next:指向irqaction描述符链表的下一个元素。共享同一中断线的每个硬件设备都有其对应的中断服务例程,链表中的每个元素就是对相应设备及中断服务例程的描述。
irq:IRQ线。

3.2.4、中断服务例程(Interrupt Service Routine)
在Linux中,中断服务例程和中断处理程序(Interrupt Handler)是两个不同的概念。可以这样认为,中断处理程序相当于某个中断向量的总处理程序,它与中断描述表(IDT)相关;中断服务例程(ISR)在中断处理过程被调用,它与IRQ描述符相关,一般来说,它是设备驱动的一部分。
(1)    注册中断服务例程
中断服务例程是硬件驱动的组成部分,如果设备要使用中断,相应的驱动程序在初始化的过程中可以通过调用request_irq函数注册中断服务例程。
//位于kernel/irq/manage.c

int request_irq(unsigned int irq,
        irqreturn_t (
*handler)(intvoid *struct pt_regs *),
        unsigned 
long irqflags, const char * devname, void *dev_id)
{
    
struct irqaction * action;
    
int retval;

    

    
if ((irqflags & SA_SHIRQ) && !dev_id)
        
return -EINVAL;
    
if (irq >= NR_IRQS)
        
return -EINVAL;
    
if (!handler)
        
return -EINVAL;
    
//分配数据结构空间
    action = kmalloc(sizeof(struct irqaction), GFP_ATOMIC);
    
if (!action)
        
return -ENOMEM;

    action
->handler = handler;
    action
->flags = irqflags;
    cpus_clear(action
->mask);
    action
->name = devname;
    action
->next = NULL;
    action
->dev_id = dev_id;
    
//调用setup_irq完成真正的注册,驱动程序也可以调用它来完成注册
    retval = setup_irq(irq, action);
    
if (retval)
        kfree(action);

    
return retval;
}
来看实时时钟初始化函数如何使用request_irq():
//位于driver/char/rtc.c
static int __init rtc_init(void)
{
request_irq(RTC_IRQ, rtc_int_handler_ptr, SA_INTERRUPT, 
"rtc", NULL);
}
再看看时钟中断初始化函数:
//位于arch/i386/mach_default/setup.c
static struct irqaction irq0  = { timer_interrupt, SA_INTERRUPT, CPU_MASK_NONE, "timer", NULL, NULL};
//由time_init()调用
void __init time_init_hook(void)
{
    setup_irq(
0&irq0);
}
3.3、中断处理流程
整个流程如下:

所有I/O中断处理函数的过程如下:
(1)把IRQ值和所有寄存器值压入内核栈;
(2) 给与IRQ中断线相连的中断控制器发送一个应答,这将允许在这条中断线上进一步发出中断请求;
(3)执行共享这个IRQ的所有设备的中断服务例程(ISR);
(4)跳到ret_from_intr()处结束。

3.3.1、保存现场与恢复现场
中断处理程序做的第一件事就是保存现场,由宏SAVE_ALL(位于entry.S中)完成:
#define SAVE_ALL \
    
cld; \
    pushl %es; \
    pushl %ds; \
    pushl �x; \
    pushl �p; \
    pushl �i; \
    pushl %esi; \
    pushl �x; \
    pushl �x; \
    pushl �x; \
    movl $(__USER_DS), �x; \
    movl �x, %ds; \
    movl �x, %es;
当内核执行SAVE_ALL后,内核栈中的内容如下:

恢复现场由宏RESTORE_ALL完成
#define RESTORE_ALL    \
    RESTORE_REGS    \
    addl $
4, %esp;    \
1:    iret;        \
.section .fixup,"ax";   \
2:    sti;        \
    movl $(__USER_DS), �x; \
    movl �x, %ds; \
    movl �x, %es; \
    movl $11,�x;    \
    call do_exit;    \
.previous;        \
.section __ex_table,"a";\
    .align 4;    \
    .long 1b,2b;    \
.previous
3.3.2、do_IRQ()函数
该函数的大致内容如下:
//arch/i386/kernel/irq.c
fastcall unsigned int do_IRQ(struct pt_regs *regs)
{    
    

    
//取得中断号
    int irq = regs->orig_eax & 0xff;
    
//增加代表嵌套中断数量的计数器的值,该值保存在current->thread_info->preempt_count
    irq_enter();
    __do_IRQ(irq, regs);
    
//减中断计数器preempt_count的值,检查是否有软中断要处理
    irq_exit();
}
结构体pt_regs如下,位于inclue/asm-i386/ptrace.h:
struct pt_regs {
    long ebx
;
    long ecx;
    long edx;
    long esi;
    long edi;
    long ebp;
    long eax;
    int  xds;
    int  xes;
    long orig_eax;
    long eip;
    int  xcs;
    long eflags;
    long esp;
    int  xss;
};
与内核栈相比,是内核栈中内容的一致。
3.3.3、__do_IRQ()函数
该函数的内容如下:
//位于kernel/irq/handle.c
fastcall unsigned int __do_IRQ(unsigned int irq, struct pt_regs *regs)
{
    irq_desc_t 
*desc = irq_desc + irq;
    
struct irqaction * action;
    unsigned 
int status;

    kstat_this_cpu.irqs[irq]
++;
    
if (desc->status & IRQ_PER_CPU) {
        irqreturn_t action_ret;

        

         
//确认中断
        desc->handler->ack(irq);
        action_ret 
= handle_IRQ_event(irq, regs, desc->action);
        
if (!noirqdebug)
            note_interrupt(irq, desc, action_ret);
        desc
->handler->end(irq);
        
return 1;
    }
    

    spin_lock(
&desc->lock);
    

    

    desc
->handler->ack(irq);
    

     
//清除IRQ_REPLAY和IRQ_WAITING标志
    status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
    
    

    status 
|= IRQ_PENDING; 

    

    action 
= NULL;
    

    
if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
        action 
= desc->action;
        
//清除IRQ_PENDING标志
        status &= ~IRQ_PENDING; 
        
        

        status 
|= IRQ_INPROGRESS; 
    }
    desc
->status = status;

    

    
if (unlikely(!action))
        
goto out;

    

    
for (;;) {
        irqreturn_t action_ret;
        
//释放自旋锁
        spin_unlock(&desc->lock);

        action_ret 
= handle_IRQ_event(irq, regs, action);
        
//加自旋锁
        spin_lock(&desc->lock);
        
if (!noirqdebug)
            note_interrupt(irq, desc, action_ret);
        

        
if (likely(!(desc->status & IRQ_PENDING)))
            
break;
        desc
->status &= ~IRQ_PENDING;
    }
    

    desc
->status &= ~IRQ_INPROGRESS;

out:
    

     
//结束中断处理.对end_8259A_irq()仅仅是重新激活中断线.
     
    desc
->handler->end(irq);
    
//最后,释放自旋锁,
    spin_unlock(&desc->lock);

    
return 1;
}
3.3.4、handle_IRQ_event
//kernel/irq/handle.c
fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
                struct irqaction *action)
{
    int ret, retval = 0, status = 0;
    //开启本地中断,对于单CPU,仅仅是sti指令
    if (!(action->flags & SA_INTERRUPT))
        local_irq_enable();
    //依次调用共享该中断向量的服务例程
    do {
        //调用中断服务例程
        ret = action->handler(irq, action->dev_id, regs);
        if (ret == IRQ_HANDLED)
            status |= action->flags;
        retval |= ret;
        action = action->next;
    } while (action);

    if (status & SA_SAMPLE_RANDOM)
        add_interrupt_randomness(irq);
    //关本地中断,对于单CPU,为cli指令
    local_irq_disable();

    return retval;
}
阅读(2008) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~