arm linux 下中断流程简要分析--中断处理流程

三 响应中断

首先在分析源码之前,让我们了解一些原理性的东西, 我们都知道在处理中断要保存当前现场状态,然后才能处理中断,处理完之后还要把现场状态恢复过来才能返回到被中断的地方继续执行,这里要说明的是在指令跳转到中断向量的地方开始执行之前,CPU帮我们做了哪些事情:

 
   R14_irq =
要执行的下条指令地址 + 4   //这里的下条指令是相对于被中断指令的下条。即返回地址

   SPSR_irq = CPSR    //保存的现场状态,r0r12要由我们软件来保存(如果需要的话)

   CPSR[4:0] = 0b10010   //进入中断模式

   CPSR[5] = 0     //ARM模式下执行(不是Thumb)

   CPSR[7] = 1      //关掉IRQ中断, FIQ还是开着

   PC = 0Xffff0018  /  0x00000018  //根据异常向量表的位置,跳转到特定的中断向量处去执行。

  

   更详细的关于异常处理的细节可参考<<ARM Architecutre Reference Manual>>

    接下来我们在来分析watchdog产生中断后的处理流程:

watchdog超时时将会产生中断,中断号就是IRQ_WDT,当产生中断时,系统将从跳转表中的中断位置开始运行,对于我们这篇文章来说:是从0xffff0000 + 24处开始运行。 这个地址的指令是:

 b  vector_irq + stubs_offset

即直接跳转到 vector_irq 处去运行。这些都在中断初始化的时候分析过了。

我们来看 vector_irq,它是通过宏vector_stub来定义的:

arch/arm/kernel/entry-armv.S:

/*

 * Interrupt dispatcher

 */

    vector_stub irq, IRQ_MODE, 4    /*这是个宏定义*/

    /*下面这些都是不同模式下的irq处理函数*/

    .long   __irq_usr          @  0  (USR_26 / USR_32)

    .long   __irq_invalid          @  1  (FIQ_26 / FIQ_32)

    .long   __irq_invalid          @  2  (IRQ_26 / IRQ_32)

    .long   __irq_svc          @  3  (SVC_26 / SVC_32)

    .long   __irq_invalid          @  4

    .long   __irq_invalid          @  5

    .long   __irq_invalid          @  6

    .long   __irq_invalid          @  7

    .long   __irq_invalid          @  8

    .long   __irq_invalid          @  9

    .long   __irq_invalid          @  a

    .long   __irq_invalid          @  b

    .long   __irq_invalid          @  c

    .long   __irq_invalid          @  d

    .long   __irq_invalid          @  e

    .long   __irq_invalid          @  f

 

来看宏vector_stub

arch/arm/kernel/entry-armv.S:

    .macro  vector_stub, name, mode, correction=0

    .align  5

 

vector_/name:

    .if /correction

    sub lr, lr, #/correction

    .endif

 

    @

    @ Save r0, lr_<exception> (parent PC) and spsr_<exception>

    @ (parent CPSR)

    @

    stmia   sp, {r0, lr}       @ save r0, lr

    mrs lr, spsr

    str lr, [sp, #8]       @ save spsr

 

    @

    @ Prepare for SVC32 mode.  IRQs remain disabled.

    @

    mrs r0, cpsr

    eor r0, r0, #(/mode ^ SVC_MODE)

    msr spsr_cxsf, r0

 

    @

    @ the branch table must immediately follow this code

    @

    and lr, lr, #0x0f

    mov r0, sp

    ldr lr, [pc, lr, lsl #2]

    movs    pc, lr          @ branch to handler in SVC mode

    .endm

 

这样展开后 vector_irq 如下所示:

arch/arm/kernel/entry-armv.S:

vector_irq:

    .if 4

@ lr保存的是被打断指令处地址+8的值,(看上面的分析,PC 得到), 这里-4则就是中断

@ 处理完后的返回地址, 在中断处理完后该值会赋给 PC

    sub lr, lr, #4 

    .endif

 

    @

    @ Save r0, lr_<exception> (parent PC) and spsr_<exception>

    @ (parent CPSR)

    @ r0后面会用到所以要保存。

    stmia   sp, {r0, lr}       @ save r0, lr,保存r0,lr到栈上,这里的栈是中断模式下的。

    mrs lr, spsr    @获取spsr的值,该值保存了被中断处执行环境的状态(参考上面的分析)

    str lr, [sp, #8]       @ save spsr, 保存到栈上

 

    @

    @ Prepare for SVC32 mode.  IRQs remain disabled.

    @

    mrs r0, cpsr

    eor r0, r0, #( IRQ_MODE ^ SVC_MODE)

    msr spsr_cxsf, r0   @spsr设置成管理模式

 

    @

    @ the branch table must immediately follow this code

    @

    and lr, lr, #0x0f

    mov r0, sp

    ldr lr, [pc, lr, lsl #2]

    movs    pc, lr          @ branch to handler in SVC mode @ pc = lr, cpsr = spsr

    .endm

 

movs 的目的对象如果是pc的话,则还会把spsr赋值给cpsr,上面我们看到spsr被设成管理模式,因此这条语句过后的代码也就跑在了管理模式下。

此时的栈情况如下:


可以看到该汇编代码主要是把被中断的代码在执行过程中的状态(cpsr), 返回地址(lr)等保存在中断模式下的栈里,然后进入到管理模式下去执行中断,同时令r0 = sp,这样可以在管理模式下找到该地址,进而获取spsr等信息。该汇编代码最终根据被中断的代码所处的模式跳转到相应的处理程序中去。

注意管理模式下的栈和中断模式下的栈不是同一个。同时由于在上面的代码中栈指针(sp)没有进行移位,因此即使后面的代码没对这个栈进行出栈操作,也不会因为不断的产生中断而导致栈溢出。

下面我们以用户模式产生中断为例,它将跳转到__irq_usr(vector_irq的定义)

arch/arm/kernel/entry-armv.S:

__irq_usr:

    usr_entry  @宏,保存各寄存器,便于返回的时候恢复

 

    get_thread_info tsk  @获取保存当前task信息的地址

#ifdef CONFIG_PREEMPT

    ldr r8, [tsk, #TI_PREEMPT]     @ get preempt count

    add r7, r8, #1         @ increment it

    str r7, [tsk, #TI_PREEMPT]

#endif

 

    irq_handler   @处理中断

#ifdef CONFIG_PREEMPT

    ldr r0, [tsk, #TI_PREEMPT]

    str r8, [tsk, #TI_PREEMPT]

    teq r0, r7

    strne   r0, [r0, -r0]

#endif

 

    mov why, #0

    b   ret_to_user  @返回

 

先看usr_entry

arch/arm/kernel/entry-armv.S:

    .macro  usr_entry

    sub sp, sp, #S_FRAME_SIZE  @栈指针下移,空出一段空间存放各寄存器

    stmib   sp, {r1 - r12}   @保存r1r12的值

 

    ldmia   r0, {r1 - r3}    @r0指向的地址处的值传给r1r13,即r1=r0,r2=lr,r3=spsr

    add r0, sp, #S_PC      @ here for interlock avoidance @不会描述,看下面的图吧

    mov r4, #-1         @  ""  ""     ""        ""

 

    str r1, [sp]        @ save the "real" r0 copied @r1存放的是实际r0的值,这里就是存储

                   @ from the exception stack      @r0的值

 

#if __LINUX_ARM_ARCH__ < 6 && !defined(CONFIG_NEEDS_SYSCALL_FOR_CMPXCHG)

#ifndef CONFIG_MMU

#warning "NPTL on non MMU needs fixing"

#else

    @ make sure our user space atomic helper is aborted

    cmp r2, #TASK_SIZE

    bichs   r3, r3, #PSR_Z_BIT

#endif

#endif

 

    @

    @ We are now ready to fill in the remaining blanks on the stack:

    @

    @  r2 - lr_<exception>, already fixed up for correct return/restart

    @  r3 - spsr_<exception>

    @  r4 - orig_r0 (see pt_regs definition in ptrace.h)

    @

    @ Also, separately save sp_usr and lr_usr

    @

    stmia   r0, {r2 - r4}   @存储返回地址,现场状态等(被中断代码处的)

    stmdb   r0, {sp, lr}^   @存储用户模式下的sp,lr,好像这里的顺序和规范上倒了一下J

 

    @

    @ Enable the alignment trap while in kernel mode

    @

    alignment_trap r0

 

    @

    @ Clear FP to mark the first stack frame

    @

    zero_fp

    .endm

这个宏主要就是保存各个寄存器值到栈上相应的位置,这个宏执行完后的栈如下所示:


S_FRAME_SIZES_PCarch/arm/kernel/Asm-offsets.c:中定义

  DEFINE(S_FRAME_SIZE,     sizeof(struct pt_regs));

  DEFINE(S_PC,         offsetof(struct pt_regs, ARM_pc));

include/asm-arm/Ptrace.h:

struct pt_regs {

    long uregs[18];

};

#define ARM_pc     uregs[15]

呵呵,pt_regs中对应的就是上面栈上的18个寄存器,ARM_pcpc寄存器存放在这个数组中的偏移。

 

接着看get_thread_info, 它也是个宏,用来获取当前线程的地址。在我的一篇linux启动代码分析里曾写过线程的定义方式:

include/linux/Sched.h:

union thread_union {

    struct thread_info thread_info;  /*线程属性*/

    unsigned long stack[THREAD_SIZE/sizeof(long)];  /**/

};

由它定义的线程是8K字节对齐的, 并且在这8K的最低地址处存放的就是thread_info对象,即该栈拥有者线程的对象,而get_thread_info就是通过把sp13位清0(8K边界)来获取当前thread_info对象的地址。

   arch/arm/kernel/entry-armv.S:

    .macro  get_thread_info, rd

    mov /rd, sp, lsr #13

    mov /rd, /rd, lsl #13

    .endm

调用该宏后寄存器tsk里存放的就是当前线程的地址了, tsk是哪个寄存器呢,呵呵我们在看:

arch/arm/kernel/entry-header.S:

tsk .req    r9      @ current thread_info

呵呵,tsk只是r9的别名而已, 因此这时r9里保存的就是当前线程的地址。

 

我们接着看irq_handler:

arch/arm/kernel/entry-armv.S:

.macro  irq_handler

1:  get_irqnr_and_base r0, r6, r5, lr  @平台相关,获取中断号

    movne   r1, sp  @如果r0(中断号)不等于0,则r1指向sp所在地址,即pt_regs对象地址(看上图)

    @

    @ routine called with r0 = irq number, r1 = struct pt_regs *

    @

    adrne   lr, 1b   @ 如果r0(中断号)不等于0 lr(返回地址)等于标号1处,即

@ get_irqnr_and_base r0, r6, r5, lr的那行,即循环处理所有的中断。

    bne asm_do_IRQ   @处理该中断

 

#ifdef CONFIG_SMP

    /*

     * XXX

     *

     * this macro assumes that irqstat (r6) and base (r5) are

     * preserved from get_irqnr_and_base above

     */

    test_for_ipi r0, r6, r5, lr

    movne   r0, sp

    adrne   lr, 1b

    bne do_IPI

 

#ifdef CONFIG_LOCAL_TIMERS

    test_for_ltirq r0, r6, r5, lr

    movne   r0, sp

    adrne   lr, 1b

    bne do_local_timer

#endif

#endif

 

    .endm

 

get_irqnr_and_base是平台相关的,这里就不列出来了,对于s3c2410,代码在include/asm-arm/s3c2410/entry-macro.S里,该宏处理完后,r0 = 中断号, 接下来r1赋值为sp地址(pt_regs对象地址), 最后调用c函数asm_do_IRQ, r0, r1作为参数被传递进去。asm_do_IRQ()处理完后将返回到lr指向的地址处即上面汇编部分标号为1的地址处继续执行。

 

我们把__irq_usr的汇编部分分析完后再来分析asm_do_IRQ()c函数。

Arch/arm/kernel/entry-armv.S:

__irq_usr:

   ……

   ……

   mov why, #0   @ why = 0, why r8的别名,

   b ret_to_user  @返回到用户模式下

 

我们看ret_to_user

arch/arm/kernel/entry-common.S:

ENTRY(ret_to_user)

ret_slow_syscall:

    disable_irq            @ disable interrupts  @关中断,

    ldr r1, [tsk, #TI_FLAGS] @获取thread_infoflags域的值

    tst r1, #_TIF_WORK_MASK  @判断task是否被阻塞

    bne work_pending         @根据需要进行进程的切换。

no_work_pending:

    @ slow_restore_user_regs

    ldr r1, [sp, #S_PSR]       @ get calling cpsr 获取被中断代码处的状态(cpsp)

    ldr lr, [sp, #S_PC]!       @ get pc   获取返回地址(被中断代码的下条代码处的地址)

    msr spsr_cxsf, r1          @ save in spsr_svc, spsr里保存好被中断代码处的状态(cpsp)

    ldmdb   sp, {r0 - lr}^      @ get calling r1 – lr  从栈上获取用户态下的r0lr的值

    mov r0, r0

    add sp, sp, #S_FRAME_SIZE - S_PC    @栈地址恢复,避免多个中断后溢出

    movs    pc, lr      @ return & move spsr_svc into cpsr, 返回被中断代码处继续执行,并把spsr赋给cpsp,即恢复被中断处的现场状态。这样CPU又可以从被中断的地方继续执行了,而且这个时候所有的寄存器值(r0r12),包括状态寄存器值(cpsr)都是源码被中断时的值。

我们顺便看下work_pending

arch/arm/kernel/entry-common.S:

work_pending:

    tst r1, #_TIF_NEED_RESCHED  @判断是否需要调度进程

    bne work_resched   @进程调度

    tst r1, #_TIF_NOTIFY_RESUME | _TIF_SIGPENDING

    beq no_work_pending   @无需调度,返回

    mov r0, sp             @ 'regs'

    mov r2, why            @ 'syscall'

    bl  do_notify_resume

    b   ret_slow_syscall       @ Check work again

由该汇编可知,如果在用户模式下产生中断的话,在返回的时候,会根据需要进行进程调度,而从代码可知,如果中断发生在管理等内核模式下的话是不会进行进程调度的。

 

Ok, 中断的流程大体就是这样的,下面我们就开始分析c函数里的中断流程。

先来看asm_do_IRQ

arch/arm/kernel/Irq.c:

/*

 * do_IRQ handles all hardware IRQ's.  Decoded IRQs should not

 * come via this function.  Instead, they should provide their

 * own 'handler'

 */

asmlinkage void asm_do_IRQ(unsigned int irq, struct pt_regs *regs)

{

    struct irqdesc *desc = irq_desc + irq; /*获取中断描述符*/

 

    /*

     * Some hardware gives randomly wrong interrupts.  Rather

     * than crashing, do something sensible.

     */

    if (irq >= NR_IRQS)  /*参数检查*/

        desc = &bad_irq_desc;

 

    irq_enter(); 

 

    desc_handle_irq(irq, desc, regs);  /*中断处理*/

 

    /* AT91 specific workaround */

    irq_finish(irq);

 

    irq_exit();

}

该函数的调用desc_handle_irq()来继续处理中断。

include/asm-arm/mach/Irq.h:

/*

 * Obsolete inline function for calling irq descriptor handlers.

 */

static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc,

                   struct pt_regs *regs)

{

    desc->handle_irq(irq, desc, regs);

}

调用中断描述符的handler_irq函数来处理该中断,对于IRQ_WDT 就是do_edge_IRQ(前面分析过)。

include/asm-arm/mach/Irq.h:

#define do_edge_IRQ handle_edge_irq

 

kernel/irq/Chip.c:

/**

 *  handle_edge_irq - edge type IRQ handler

 *  @irq:   the interrupt number

 *  @desc:  the interrupt description structure for this irq

 *  @regs:  pointer to a register structure

 *

 *  Interrupt occures on the falling and/or rising edge of a hardware

 *  signal. The occurence is latched into the irq controller hardware

 *  and must be acked in order to be reenabled. After the ack another

 *  interrupt can happen on the same source even before the first one

 *  is handled by the assosiacted event handler. If this happens it

 *  might be necessary to disable (mask) the interrupt depending on the

 *  controller hardware. This requires to reenable the interrupt inside

 *  of the loop which handles the interrupts which have arrived while

 *  the handler was running. If all pending interrupts are handled, the

 *  loop is left.

 */

void fastcall

handle_edge_irq(unsigned int irq, struct irq_desc *desc, struct pt_regs *regs)

{

    const unsigned int cpu = smp_processor_id();

 

    spin_lock(&desc->lock);

 

    desc->status &= ~(IRQ_REPLAY | IRQ_WAITING);

 

    /*

     * If we're currently running this IRQ, or its disabled,

     * we shouldn't process the IRQ. Mark it pending, handle

     * the necessary masking and go out

     */

     /*

       * 如果该中断正在处理或者该中断被disable掉了的话,就不处理该中断,并清掉pending

* 寄存器里的相应位

       */

    if (unlikely((desc->status & (IRQ_INPROGRESS | IRQ_DISABLED)) ||

            !desc->action)) {

        desc->status |= (IRQ_PENDING | IRQ_MASKED);

        mask_ack_irq(desc, irq);  /*mask该中断,pending标志位*/

        goto out_unlock;

    }

 

    kstat_cpu(cpu).irqs[irq]++;  /*统计中断数量*/

 

    /* Start handling the irq */

/*开始处理中断,先清掉pending标志位*/

    desc->chip->ack(irq); 

 

    /* Mark the IRQ currently in progress.*/

    desc->status |= IRQ_INPROGRESS;  /*标上正在处理的标记*/

 

    do {

        struct irqaction *action = desc->action;   /*获取该中断的action*/

        irqreturn_t action_ret; 

 

        if (unlikely(!action)) {

            desc->chip->mask(irq)   /*如果没有注册action,则mask该中断*/;

            goto out_unlock;

        }

 

        /*

         * When another irq arrived while we were handling

         * one, we could have masked the irq.

         * Renable it, if it was not disabled in meantime.

         */

         /*

          * 如果以前被mask掉的话,在这里把它打开

          */

        if (unlikely((desc->status &

                   (IRQ_PENDING | IRQ_MASKED | IRQ_DISABLED)) ==

                  (IRQ_PENDING | IRQ_MASKED))) {

            desc->chip->unmask(irq); /*unmask该中断*/

            desc->status &= ~IRQ_MASKED;

        }

 

        desc->status &= ~IRQ_PENDING; 

        spin_unlock(&desc->lock);

        action_ret = handle_IRQ_event(irq, regs, action);  /*处理中断事件*/

        if (!noirqdebug)

            note_interrupt(irq, desc, action_ret, regs);

        spin_lock(&desc->lock);

    /*如果有IRQ_PENDING状态,则说明又有中断产生过,则继续执行*/

    } while ((desc->status & (IRQ_PENDING | IRQ_DISABLED)) == IRQ_PENDING);

 

    desc->status &= ~IRQ_INPROGRESS;

out_unlock:

    spin_unlock(&desc->lock);

}

该函数的大体功能都在函数体内解释出来了,这里我们对调用的每个函数在进行分析。

先看mask_ack_irq

kernel/irq/Chip.c:

static inline void mask_ack_irq(struct irq_desc *desc, int irq)

{

    if (desc->chip->mask_ack)  /*对于IRQ_WDT, 该函数没定义*/

        desc->chip->mask_ack(irq);

    else {

        desc->chip->mask(irq); /*对于IRQ_WDT,该函数就是s3c_irq_mask*/

        desc->chip->ack(irq);  /*对于IRQ_WDT,该函数就是s3c_irq_ack*/

    }

}

可以看到它调用具体平台相关的mask函数来处理该中断。

我们来看s3c_irq_mask

arch/arm/mach-s3c2410/Irq.c:

static void

s3c_irq_mask(unsigned int irqno)

{

    unsigned long mask;

 

    irqno -= IRQ_EINT0;

 

    mask = __raw_readl(S3C2410_INTMSK);

    mask |= 1UL << irqno;   /*mask掉对应的中断号*/

    __raw_writel(mask, S3C2410_INTMSK);  /*MASK寄存器*/

}

改函数仅仅是把MASK寄存器中对应的中断mask掉,即不再响应该中断

arch/arm/mach-s3c2410/Irq.c:

static inline void

s3c_irq_ack(unsigned int irqno)

{

    unsigned long bitval = 1UL << (irqno - IRQ_EINT0);

/*清除pending寄存器的相应位*/

    __raw_writel(bitval, S3C2410_SRCPND);

    __raw_writel(bitval, S3C2410_INTPND);

}

由上面这两个函数可以看出来mask_ack_irq的作用是先mask掉该中断,并清除pending位,中断被mask掉后系统就不再响应了, 而pending位被清掉说明系统中该中断没有触发。一般在中断处理完后都要清pending位, 要不然系统会认为该中断又被触发了。

handle_edge_irq()里调用的unmask函数,其实就是打开相应的中断,让系统响应这个中断,代码就不列出来了。

接下来中断看handle_IRQ_event(),它才是真正的中断处理函数。

kernel/irq/handle.c:

/**

 * handle_IRQ_event - irq action chain handler

 * @irq:    the interrupt number

 * @regs:   pointer to a register structure

 * @action: the interrupt action chain for this irq

 *

 * Handles the action chain of an irq event

 */

irqreturn_t handle_IRQ_event(unsigned int irq, struct pt_regs *regs,

                 struct irqaction *action)

{

    irqreturn_t ret, retval = IRQ_NONE;

    unsigned int status = 0;

 

    handle_dynamic_tick(action);

 

    /*下面这个if判断:当执行action操作时是否可以打开中断*/

    if (!(action->flags & IRQF_DISABLED))

        local_irq_enable_in_hardirq();  /*打开中断*/

 

    do {

        /*

* 中断handler,也就是我们通过request_irq注册的中断函数,对于IRQ_WDT就是

* s3c2410wdt_irq

*/

        ret = action->handler(irq, action->dev_id, regs);

        if (ret == IRQ_HANDLED)

            status |= action->flags;

        retval |= ret;

        action = action->next; /*记得吗,如果该中断可以共享的话,它就不为NULL*/

    } while (action);

 

    if (status & IRQF_SAMPLE_RANDOM)

        add_interrupt_randomness(irq);

    local_irq_disable();

 

    return retval;

}

该函数主要就是调用了actionhandler函数,也就是我们用request_irq注册的中断例程。这里需要注意的是:如果我们注册中断的时候指明可以共享的话,则必须在我们的中断例程里判断当前产生的中断是否就是我们自己的中断,这可以通过传进来的参数来判断(该参数就是我们注册时提供的)

OK, 到这里整个中断的流程就大致分析完了。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值