查看: 992|回复: 0

[经验] 分析Linux的中断响应,这些细节得注意

[复制链接]
  • TA的每日心情
    开心
    2018-6-30 10:21
  • 签到天数: 1 天

    连续签到: 1 天

    [LV.1]初来乍到

    发表于 2020-8-28 09:02:33 | 显示全部楼层 |阅读模式
    分享到:
    响应中断的学习与分析

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

       R14_irq = 要执行的下条指令地址 + 4   //这里的下条指令是相对于被中断指令的下条。即返回地址
       SPSR_irq = CPSR    //保存的现场状态,r0到r12要由我们软件来保存(如果需要的话)。
       CPSR[4:0] = 0b10010   //进入中断模式
       CPSR[5] = 0     //在ARM模式下执行(不是Thumb下)
       CPSR[7] = 1      //关掉IRQ中断, FIQ还是开着
       PC = 0Xffff0018  /  0x00000018  //根据异常向量表的位置,跳转到特定的中断向量处去执行。
      
       更详细的关于异常处理的细节可参考<>
        接下来我们在来分析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_ (parent PC) and spsr_
        @ (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_ (parent PC) and spsr_
        @ (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被设成管理模式,因此这条语句过后的代码也就跑在了管理模式下。
    此时的栈情况如下:

    S_FRAME_SIZE, S_PC在arch/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_pc是pc寄存器存放在这个数组中的偏移。

    接着看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就是通过把sp低13位清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_info中flags域的值
        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  从栈上获取用户态下的r0到lr的值
        mov r0, r0
        add sp, sp, #S_FRAME_SIZE - S_PC    @栈地址恢复,避免多个中断后溢出
        movs    pc, lr      @ return & move spsr_svc into cpsr, 返回被中断代码处继续执行,并把spsr赋给cpsp,即恢复被中断处的现场状态。这样CPU又可以从被中断的地方继续执行了,而且这个时候所有的寄存器值(r0到r12),包括状态寄存器值(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;
    }
    该函数主要就是调用了action的handler函数,也就是我们用request_irq注册的中断例程。这里需要注意的是:如果我们注册中断的时候指明可以共享的话,则必须在我们的中断例程里判断当前产生的中断是否就是我们自己的中断,这可以通过传进来的参数来判断(该参数就是我们注册时提供的)。
    OK, 到这里整个中断的流程就大致分析完了。




    回复

    使用道具 举报

    您需要登录后才可以回帖 注册/登录

    本版积分规则

    关闭

    站长推荐上一条 /3 下一条



    手机版|小黑屋|与非网

    GMT+8, 2025-1-13 13:46 , Processed in 0.120007 second(s), 15 queries , MemCache On.

    ICP经营许可证 苏B2-20140176  苏ICP备14012660号-2   苏州灵动帧格网络科技有限公司 版权所有.

    苏公网安备 32059002001037号

    Powered by Discuz! X3.4

    Copyright © 2001-2024, Tencent Cloud.