Linux内核-中断-中断响应和返回

一、中断的响应和服务

在前面一篇博文中,我们分析了i386 CPU的中断机制和内核中有关的初始化,现在我们进一步分析中断的响应过程和服务(和异常的响应机制不同)。我们假设外设驱动都已经完成了初始化,并且已把相应的中断服务程序挂入到特定的中断请求队列中,系统正在用户空间正常运行,并且某个外设已经产生了一次中断请求,该请求通过中断控制器到达了CPU的“中断请求”引线INTR。CPU从中断控制器取得中断向量,然后根据具体的中断向量从中断向量表IDT中找到相应的表项,该表项为一个中断门,然后通过该中断门的设置到达了该通道的总服务程序的入口IRQxx_interrupt,即为interrupt[]中的元素(参考前面一篇文章),这里为了方便说明,再次给出IRQxx_interrupt等的源码,如下:

 pushl $vector-256
    jmp common_interrupt

    /* common_interrupt如下 */
    common_interrupt:
    SAVE_ALL
    movl %esp,%eax
    call do_IRQ
    jmp ret_from_intr


    /* SAVE_ALL如下 */
    #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后,堆栈如图:

Linux内核-中断-中断响应和返回_第1张图片

接下来将栈顶指针esp存到eax中,然后执行do_IRQ函数,该函数声明如下:

 fastcall unsigned int do_IRQ(struct pt_regs *regs)

    /* 关键字regparm表示函数到eax寄存器中去找到参数regs的值 */
    #define fastcall    __attribute__((regparm(3)))

    /* 到此可以清楚的看到,regs指向的内容为上图中系统堆栈中的内容 */
    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;
    };

接下来分析do_IRQ()函数,如下:

 /** 
     * do_IRQ执行与一个中断相关的所有中断服务例程.
     */
    fastcall unsigned int do_IRQ(struct pt_regs *regs)
    {   
        /* 通过orig_eax读回并屏蔽掉高位,又得到中断号irq */
        int irq = regs->orig_eax & 0xff;
    #ifdef CONFIG_4KSTACKS
        union irq_ctx *curctx, *irqctx;
        u32 *isp;
    #endif

        /**
         * irq_enter增加中断嵌套计数
         */
        irq_enter();

    #ifdef CONFIG_DEBUG_STACKOVERFLOW
        /* Debugging check for stack overflow: is there less than 1KB free? */
        {
            long esp;

            __asm__ __volatile__("andl %%esp,%0" :
                        "=r" (esp) : "0" (THREAD_SIZE - 1));
            if (unlikely(esp < (sizeof(struct thread_info) + STACK_WARN))) {
                printk("do_IRQ: stack overflow: %ld\n",
                    esp - sizeof(struct thread_info));
                dump_stack();
            }
        }
    #endif

    #ifdef CONFIG_4KSTACKS

        /**
         * 如果thread_union结构大小为4KB,函数切换到硬中断请求栈
         * 上一篇文章讲到有三种内核栈,所有的硬中断请求存放在harding_stack数组中
         * 所有的软中断请求存放在softirq_stack数组中,每个数组元素为irq_ctx类型的联合体
         * hardirq_ctx和softirq_ctx数组使内核快速确定CPU的硬中断请求栈和软中断请求栈,它们包含的指针分别指向相应的irq_ctx元素
         */
        curctx = (union irq_ctx *) current_thread_info();    /* 取得当前进程内核栈 */
        irqctx = hardirq_ctx[smp_processor_id()];            /* 取得CPU的硬中断请求栈 */

        /**
         * 当前在使用内核栈,而不是硬中断请求栈.就需要切换栈
         */
        if (curctx != irqctx) {
            int arg1, arg2, ebx;

            /* build the stack frame on the IRQ stack */
            isp = (u32*) ((char*)irqctx + sizeof(*irqctx));
            /**
             * 保存当前进程描述符指针
             */
            irqctx->tinfo.task = curctx->tinfo.task;
            /**
             * 把esp栈指针寄存器的当前值存入irqctx的thread_info(内核oops时使用)
             */
            irqctx->tinfo.previous_esp = current_stack_pointer;

            /**
             * 将中断请求栈的栈顶装入esp,isp即为中断栈顶
             * 调用完__do_IRQ后,从ebx中恢复esp
             */
            asm volatile(
                "       xchgl   %%ebx,%%esp      \n"
                "       call    __do_IRQ         \n"
                "       movl   %%ebx,%%esp      \n"
                : "=a" (arg1), "=d" (arg2), "=b" (ebx)
                :  "0" (irq),   "1" (regs),  "2" (isp)
                : "memory", "cc", "ecx"
            );
        } else/* 否则,内核已经在使用硬中断请求栈(发生了中断嵌套),不用切换 */
    #endif
        /* 该函数见下面分析 */
            __do_IRQ(irq, regs);

        /**
         * 递减中断计数器并检查是否有可延迟函数
         */
        irq_exit();

        /**
         * 结束后,会返回ret_from_intr函数. 
         */
        return 1;
    }

__do_IRQ(irq, regs)函数如下:

    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;
        }

        /**
         * 虽然中断是关闭的,但是还是需要使用自旋锁保护desc
         */
        spin_lock(&desc->lock);
        /**
         * 如果是旧的8259A PIC,ack就是mask_and_ack_8259A,它应答PIC上的中断并禁用这条IRQ线.屏蔽IRQ线是为了确保在这个中断处理程序结束前,
         * CPU不进一步接受这种中断的出现.
         * __do_IRQ是以禁止本地中断运行,事实上,CPU控制单元自动清eflags寄存器的IF标志.因为中断处理程序是通过IDT中断门调用的.
         * 不过,内核在执行这个中断的中断服务例程之前可能会重新激活本地中断.
         * 在使用APIC时,应答中断信赖于中断类型,可能是ack,也可能延迟到中断处理程序结束(也就是应答由end方法去做).
         * 无论如何,中断处理程序结束前,本地APIC不进一步接收这种中断,尽管这种中断可能会被其他CPU接受.
         */
        desc->handler->ack(irq);

        /**
         * 初始化主IRQ描述符的几个标志.设置IRQ_PENDING标志.也清除IRQ_WAITING和IRQ_REPLAY
         * 这几个标志可以很好的解决中断重入的问题.
         * IRQ_REPLAY标志是"挽救丢失的中断"所用.在此不详述.
         */
        status = desc->status & ~(IRQ_REPLAY | IRQ_WAITING);
        status |= IRQ_PENDING; /* we _want_ to handle it */

        action = NULL;
        /**
         * IRQ_DISABLED和IRQ_INPROGRESS被设置时,什么都不做(action==NULL)
         * 即使IRQ线被禁止,CPU也可能执行do_IRQ函数.首先,可能是因为挽救丢失的中断,其次,也可能是有问题的主板产生伪中断.
         * 所以,是否真的执行中断代码,需要根据IRQ_DISABLED标志来判断,而不仅仅是禁用IRQ线.
         * IRQ_INPROGRESS标志的作用是:如果一个CPU正在处理一个中断,那么它会设置它的IRQ_INPROGRESS.这样,其他CPU上发生同样的中断
         * 就可以检查是否在其他CPU上正在处理同种类型的中断,如果是,就什么都不做,这样做有以下好处:
         * 一是使内核结构简单,驱动程序的中断服务例程式不必是可重入的.二是可以避免弄脏当前CPU的硬件高速缓存.
         */
        if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
            action = desc->action;
            status &= ~IRQ_PENDING; /* we commit to handling */
            status |= IRQ_INPROGRESS; /* we are handling it */
        }
        desc->status = status;

        /**
         * 当前面两种情况出现时,不需要(或者是不需要马上)处理中断.就退出
         * 或者没有相关的中断服务例程时,也退出.当内核正在检测硬件设备时就会发生这种情况.
         */
        if (unlikely(!action))
            goto out;

        /**
         * 这里是需要循环处理的,并不是说调用一次handle_IRQ_event就行了.
         */
        for (;;) {
            irqreturn_t action_ret;

            /**
             * 现在打开自旋锁了,那么,其他CPU可能也接收到同类中断,并设置IRQ_PENDING标志.
             */
            spin_unlock(&desc->lock);

            /**
             * 调用中断服务例程.
             */
            action_ret = handle_IRQ_event(irq, regs, action);

            spin_lock(&desc->lock);
            if (!noirqdebug)
                note_interrupt(irq, desc, action_ret);
            /**
             * 如果其他CPU没有接收到同类中断,就退出
             * 否则,继续处理同类中断.
             */
            if (likely(!(desc->status & IRQ_PENDING)))
                break;
            /**
             * 清除了IRQ_PENDING,如果再出现IRQ_PENDING,就说明是其他CPU上接收到了同类中断.
             * 注意,IRQ_PENDING仅仅是一个标志,如果在调用中断处理函数的过程中,来了多次的同类中断,则意味着只有一次被处理,其余的都丢失了.
             */
            desc->status &= ~IRQ_PENDING;
        }
        desc->status &= ~IRQ_INPROGRESS;

    out:
        /**
         * 现在准备退出了,end方法可能是应答中断(APIC),也可能是通过end_8259A_irq方法重新激活IRQ(只要不是伪中断).
         */
        desc->handler->end(irq);
        /**
         * 好,工作已经全部完成了,释放自旋锁吧.注意两个锁的配对使用方法.
         */
        spin_unlock(&desc->lock);

        return 1;
    }

handle_IRQ_event函数如下:

  /**
     * 执行中断服务例程
     */
    fastcall int handle_IRQ_event(unsigned int irq, struct pt_regs *regs,
                    struct irqaction *action)
    {
        int ret, retval = 0, status = 0;

        /**
         * 如果没有设置SA_INTERRUPT,说明中断处理程序是可以在开中断情况下执行的
         * 这也是程序中少见的,调用local_irq_enable的地方。
         * 一般来说,调用local_irq_enable是危险的,不允许,绝不允许。这里只是例外。
         */
        if (!(action->flags & SA_INTERRUPT))
            local_irq_enable();

        /**
         * 一开始,action是irqaction链表的头,irqaction表示一个ISR
         */
        do {
            /**
             * handler是中断服务例程的处理函数。它接收三个参数:
             * irq-IRQ号,它允许一个ISR处理几条IRQ。
             * dev_id-设备号,注册中断服务例程时指定,此时回传给处理函数。它允许一个ISR处理几个同类型的设备。
             * regs-指向内核栈的pt_regs。它允许ISR访问内核执行上下文。可是,哪个ISR会用它呢?
             */
            ret = action->handler(irq, action->dev_id, regs);
            if (ret == IRQ_HANDLED)
                status |= action->flags;
            /**
             * 一般来说,handler处理了本次中断,就会返回1
             * 返回0和1是有用的,这样可以让内核判断中断是否被处理了。
             * 如果过多的中断没有被处理,就说明硬件有问题,产生了伪中断。
             */
            retval |= ret;
            action = action->next;
        } while (action);

        /**
         * 如果中断是随机数的产生源,就添加一个随机因子。
         */
        if (status & SA_SAMPLE_RANDOM)
            add_interrupt_randomness(irq);

        /**
         * 退出时,总是会关中断
         */
        local_irq_disable();

        return retval;
    }

二、从中断中返回

上面从do_IRQ()函数返回后,就跳转到ret_from_intr处执行,也就是中断的返回,Linux中断返回机制如图:

Linux内核-中断-中断响应和返回_第2张图片

源码如下:

    ret_from_intr:
        /**
         * 把当前thread_info半截到ebp中。
         */
        GET_THREAD_INFO(%ebp)
        /**
         * 接下来判断EFLAGS和CS,确定是否运行在用户态,是否是VM模式。
         */
        movl EFLAGS(%esp), %eax     # mix EFLAGS and CS
        movb CS(%esp), %al
        testl $(VM_MASK | 3), %eax
        /**
         * 如果是运行在内核态,并且不是VM模式,就跳到resume_kernel,
         * 否则跳转到resume_userspace
         */
        jz resume_kernel        # returning to kernel or vm86-space
    /**
     * 恢复用户态程序的流程入口。
     */
     ENTRY(resume_userspace)

        cli             # make sure we don't miss an interrupt
                        # setting need_resched or sigpending
                        # between sampling and the iret
        /**
         * 检查thread_info的flag
         */
        movl TI_flags(%ebp), %ecx
        /**
         * 如果设置了_TIF_WORK_MASK中任何一位,就表示有等待处理的事情
         * 跳到work_pending处理这些挂起的事件。
         * 否则调用restore_all回到用户态。
         */
        andl $_TIF_WORK_MASK, %ecx # is there any work to be done on
                        # int/exception return?
        jne work_pending
        jmp restore_all

    #ifdef CONFIG_PREEMPT
    /**
     * 当从异常或者中断返回时,需要返回到内核,则跳转到此处。
     */
    ENTRY(resume_kernel)
        /**
         * 不知道此处为何需要再加cli,如果是从中断或者异常跳转到这里,那么已经是关中断状态了。
         * 也许是还有其他地方跳到这里吧。
         */
        cli
        /**
         * 首先判断内核是否允许抢占,请请记住ebp中保存的是thread_info
         */
        cmpl $0,TI_preempt_count(%ebp) # non-zero preempt_count ?
        /**
         * 当前不允许抢占,就继续执行内核代码。
         */
        jnz restore_all
        /**
         * 否则抢占计数为0,就判断是否有调度需求。
         */ 
    need_resched:
        movl TI_flags(%ebp), %ecx   # need_resched set ?
        /**
         * 判断是否有调度需求。
         */
        testb $_TIF_NEED_RESCHED, %cl
        /**
         * 不需要调度,就继续执行内核代码。
         */
        jz restore_all
        /**
         * 虽然有调度需求,但是当前是关中断状态,显然,这是不合理的。
         * 这时回到用户态做什么呢?timer中断都可能被关了。系统怎么工作?
         */
        testl $IF_MASK,EFLAGS(%esp)     # interrupts off (exception path) ?
        jz restore_all
        /**
         * preempt_schedule_irq会设置PREEMPT_ACTIVE标志,并把大内核锁暂时设置为-1。然后开中断并调用schedule。
         */
        call preempt_schedule_irq
        jmp need_resched
    #endif

    /**
     * 在回到用户态前,如果有挂起的任务,就处理这些挂起的任务。
     */
    work_pending:
        /**
         * 检查是否需要重新调度。
         */
        testb $_TIF_NEED_RESCHED, %cl
        /**
         * 不需要重新调度,需要回到用户态,在回到用户态前,先检查待处理的信号
         */
        jz work_notifysig
    /**
     * 否则,有调度需要,处理调度。
     */
     work_resched:
        /**
         * 调度一下。可能没有调出去,也可能出去后又调度回来了。
         */
        call schedule
        /**
         * 因为可能是调度出去后,又回来了,所以需要重新关中断。
         */
        cli             # make sure we don't miss an interrupt
                        # setting need_resched or sigpending
                        # between sampling and the iret
        /**
         * 这个处理流程是否有点眼熟呢??
         */
        movl TI_flags(%ebp), %ecx
        andl $_TIF_WORK_MASK, %ecx # is there any work to be done other
                        # than syscall tracing?
        jz restore_all
        testb $_TIF_NEED_RESCHED, %cl
        jnz work_resched

    /**
     * 好了,运行到这里,说明没有重新调度的要求。
     * 或者说有调度要求,但是调度出动后,又回来了。
     * 总之,现在是没有调度要求了。在回到用户态前,处理信号。
     * 需要注意的是:这个入口有不止从一个地方进入。
     * 一是从上面两句转入,二是从更上面的jmp跳入。
     * 接下来有两个事件需要处理:一是信号,二是VM86模式。
     * 其中VM86模式我们不太关心。信号呢,很复杂的流程,至少需要一章才说清楚。也略过。
     */
    work_notifysig:             # deal with pending signals and
                        # notify-resume requests
        testl $VM_MASK, EFLAGS(%esp)
        movl %esp, %eax
        jne work_notifysig_v86      # returning to kernel-space or
                        # vm86-space
        xorl %edx, %edx
        call do_notify_resume
        jmp restore_all

        ALIGN
    work_notifysig_v86:
        pushl %ecx          # save ti_flags for do_notify_resume
        call save_v86_state     # %eax contains pt_regs pointer
        popl %ecx
        movl %eax, %esp
        xorl %edx, %edx
        call do_notify_resume
        jmp restore_all


    restore_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

你可能感兴趣的:(Linux,kernel,中断)