zoukankan      html  css  js  c++  java
  • 内核随记(一)——理解中断(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,%eax /*栈顶指针保存到eax*/
        
    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;        /* IRQ status */
        hw_irq_controller 
    *handler;
        
    struct irqaction *action;    /* IRQ action list */
        unsigned 
    int depth;        /* nested irq disables */
        unsigned 
    int irq_count;        /* For detecting broken interrupts */
        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   /* 正在执行这个IRQ的一个处理程序*/
    #define IRQ_DISABLED    2    /* 由设备驱动程序已经禁用了这条IRQ中断线 */
    #define IRQ_PENDING     4    /* 一个IRQ已经出现在中断线上,且被应答,但还没有为它提供服务 */
    #define IRQ_REPLAY      8    /* 当Linux重新发送一个已被删除的IRQ时 */
    #define IRQ_AUTODETECT  16  /* 当进行硬件设备探测时,内核使用这条IRQ中断线 */
    #define IRQ_WAITING     32   /*当对硬件设备进行探测时,设置这个状态以标记正在被测试的irq */
    #define IRQ_LEVEL       64    /* IRQ level triggered */
    #define IRQ_MASKED      128    /* IRQ masked - shouldn't be seen again */
    #define IRQ_PER_CPU     256     /* IRQ is per CPU */
    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) {
                
    /*
                 * 16 old-style INTA-cycle interrupts:
                 
    */
                irq_desc[i].handler 
    = &i8259A_irq_type;
            } 
    else {
                
    /*
                 * 'high' PCI IRQs filled in on demand
                 
    */
                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
    /*
    irq:IRQ号
    **handler:中断服务例程
    **irqflags:SA_SHIRQ,SA_INTERRUPT或SA_SAMPLE_RANDOM
    **devname:设备名称,这些名称会被/proc/irq和/proc/interrupt使用
    **dev_id:主要用于设备共享
     
    */
    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;

        
    /*
         * Sanity-check: shared interrupts must pass in a real dev-ID,
         * otherwise we'll have trouble later trying to figure out
         * which interrupt is which (messes up the interrupt freeing
         * logic etc).
         
    */
        
    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 %eax; \
        pushl %ebp; \
        pushl %edi; \
        pushl %esi; \
        pushl %edx; \
        pushl %ecx; \
        pushl %ebx; \
        movl $(__USER_DS), %edx; \
        movl %edx, %ds; \
        movl %edx, %es;
    当内核执行SAVE_ALL后,内核栈中的内容如下:

    恢复现场由宏RESTORE_ALL完成
    #define RESTORE_ALL    \
        RESTORE_REGS    \
        addl $
    4, %esp;    \
    1:    iret;        \
    .section .fixup,"ax";   \
    2:    sti;        \
        movl $(__USER_DS), %edx; \
        movl %edx, %ds; \
        movl %edx, %es; \
        movl $11,%eax;    \
        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)
    {    
        
    /* high bits used in ret_from_ code */
        
    //取得中断号
        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;

            
    /*
             * No locking required for CPU-local interrupts:
             
    */
             
    //确认中断
            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;
        }
        
    /*加自旋锁.对于多CPU系统,这是必须的,因为同类型的其它中断可能产生,并被其它的CPU处理,
        **没有自旋锁,IRQ描述符将被多个CPU同时访问.
        
    */
        spin_lock(
    &desc->lock);
        
    /*确认中断.对于8259A PIC,由mask_and_ack_8259A()完成确认,并禁用当前IRQ线.
        **屏蔽中断是为了确保该中断处理程序结束前,CPU不会又接受这种中断.虽然,CPU在处理中断会自动
        **清除eflags中的IF标志,但是在执行中断服务例程前,可能重新激活本地中断.见handle_IRQ_event.
        
    */
        
    /*在多处理器上,应答中断依赖于具体的中断类型.可能由ack方法做,也可能由end方法做.不管怎样,在中断处理结束
        *前,本地APIC不再接收同样的中断,尽管这种中断可以被其它CPU接收.
        
    */
        desc
    ->handler->ack(irq);
        
    /*
         * REPLAY is when Linux resends an IRQ that was dropped earlier
         * WAITING is used by probe to mark irqs that are being tested
         
    */
         
    //清除IRQ_REPLAY和IRQ_WAITING标志
        status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
        
        
    /* IRQ_PENDING表示一个IRQ已经出现在中断线上,且被应答,但还没有为它提供服务 */
        status 
    |= IRQ_PENDING; /* we _want_ to handle it */

        
    /*
         * If the IRQ is disabled for whatever reason, we cannot
         * use the action we have.
         
    */
        action 
    = NULL;
        
    /*现在开始检查是否真的需要处理中断.在三种情况下什么也不做.
        *(1)IRQ_DISABLED被设置.即使在相应的IRQ线被禁止的情况下,do_IRQ()也可能执行.
        *(2)IRQ_INPROGRESS被设置时,在多CPU系统中,表示其它CPU正在处理同样中断的前一次发生.Linux中,同类型
        *中断的中断服务例程由同一个CPU处理.这样使得中断服务例程不必是可重入的(在同一CPU上串行执行).
        *
        *(3)action==NULL.此时,直接跳到out处执行.
        
    */
        
    if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
            action 
    = desc->action;
            
    //清除IRQ_PENDING标志
            status &= ~IRQ_PENDING; /* we commit to handling */
            
            
    /*表示当前CPU正在处理该中断,其它CPU不应该处理同样的中断,而应该让给本CPU处理.一旦设置
            *IRQ_INPROGRESS,其它CPU即使进行do_IRQ,也不会执行该程序段,则action==NULL,则其它CPU什么也不做.
            *当调用handle_IRQ_event执行中断服务例程时,由于释放了自旋锁,其它CPU可能接受到同类型的中断(本CPU
            *不会接受同类型中断),而进入do_IRQ(),并设置IRQ_PENDING.
            
    */
            status 
    |= IRQ_INPROGRESS; /* we are handling it */
        }
        desc
    ->status = status;

        
    /*
         * If there is no IRQ handler or it was disabled, exit early.
         * Since we set PENDING, if another processor is handling
         * a different instance of this same irq, the other processor
         * will take care of it.
         
    */
        
    if (unlikely(!action))
            
    goto out;

        
    /*
         * Edge triggered interrupts need to remember
         * pending events.
         * This applies to any hw interrupts that allow a second
         * instance of the same irq to arrive while we are in do_IRQ
         * or in the handler. But the code here only handles the _second_
         * instance of the irq, not the third or fourth. So it is mostly
         * useful for irq hardware that does not mask cleanly in an
         * SMP environment.
         
    */
        
    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);
            
    /*如果此时IRQ_PENDING处于清除状态,说明中断服务例程已经执行完毕,退出循环.反之,说明在执行中断服务例程时,
            *其它CPU进入过do_IRQ,并设置了IRQ_PENDING.也就是说其它CPU收到了同类型的中断.此时,应该清除
            *IRQ_INPROGRESS,并重新循环,执行中断服务例程,处理其它CPU收到的中断.
            
    */
            
    if (likely(!(desc->status & IRQ_PENDING)))
                
    break;
            desc
    ->status &= ~IRQ_PENDING;
        }
        
    /*所有中断处理完毕,则清除IRQ_INPROGRESS*/
        desc
    ->status &= ~IRQ_INPROGRESS;

    out:
        
    /*
         * The ->end() handler has to deal with interrupts which got
         * disabled while the handler was running.
         
    */
         
    //结束中断处理.对end_8259A_irq()仅仅是重新激活中断线.
         /*对于多处器,end应答中断(如果ack方法还没有做的话)
        
    */
        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;
    }

  • 相关阅读:
    斐波那契比率(2016-9-13)
    MQL5备忘(2016-8-28)
    [转载]联邦基金利率期货
    二十四节气对照表
    [Android] 为Android安装BusyBox —— 完整的bash shell(收藏用)
    如何调试分析Android中发生的tombstone
    Android log系统 转
    android 系统log文件路径
    GCM Architectural Overview
    google cloud message 运行成功0621
  • 原文地址:https://www.cnblogs.com/hustcat/p/1546011.html
Copyright © 2011-2022 走看看